diff --git a/build/devenv/cciptestinterfaces/interface.go b/build/devenv/cciptestinterfaces/interface.go index 1edee1a74..3b318e629 100644 --- a/build/devenv/cciptestinterfaces/interface.go +++ b/build/devenv/cciptestinterfaces/interface.go @@ -222,7 +222,7 @@ type OnChainCommittees struct { // express only the values they want to override; nil/zero means "use adapter default". type ChainLaneProfile struct { BaseExecutionGasCost *uint32 - FeeQuoterDestChainConfig ccipChangesets.FeeQuoterDestChainConfigOverrides + FeeQuoterDestChainConfig adapters.FeeQuoterDestChainConfigOverrides ExecutorDestChainConfig *adapters.ExecutorDestChainConfig DefaultExecutorQualifier string DefaultInboundCCVs []datastore.AddressRef diff --git a/build/devenv/components/indexer/component.go b/build/devenv/components/indexer/component.go index e032d8003..d71ec73b9 100644 --- a/build/devenv/components/indexer/component.go +++ b/build/devenv/components/indexer/component.go @@ -11,7 +11,6 @@ import ( devenvruntime "github.com/smartcontractkit/chainlink-ccv/build/devenv/runtime" "github.com/smartcontractkit/chainlink-ccv/build/devenv/services" ccvdeployment "github.com/smartcontractkit/chainlink-ccv/deployment" - ccvadapters "github.com/smartcontractkit/chainlink-ccv/deployment/adapters" ccvchangesets "github.com/smartcontractkit/chainlink-ccv/deployment/changesets" "github.com/smartcontractkit/chainlink-ccv/indexer/pkg/config" "github.com/smartcontractkit/chainlink-deployments-framework/deployment" @@ -60,7 +59,7 @@ func (c *component) RunPhase4( } firstIdx := inputs[0] - cs := ccvchangesets.GenerateIndexerConfig(ccvadapters.GetRegistry()) + cs := ccvchangesets.GenerateIndexerConfig() localEnv := *e output, err := cs.Apply(localEnv, ccvchangesets.GenerateIndexerConfigInput{ ServiceIdentifier: "indexer", diff --git a/build/devenv/components/protocol_contracts/component.go b/build/devenv/components/protocol_contracts/component.go index 2f792e52b..f4d4309c5 100644 --- a/build/devenv/components/protocol_contracts/component.go +++ b/build/devenv/components/protocol_contracts/component.go @@ -21,7 +21,6 @@ import ( "github.com/smartcontractkit/chainlink-ccv/build/devenv/services/committeeverifier" "github.com/smartcontractkit/chainlink-ccv/build/devenv/timing" ccvdeployment "github.com/smartcontractkit/chainlink-ccv/deployment" - ccvadapters "github.com/smartcontractkit/chainlink-ccv/deployment/adapters" ccvchangesets "github.com/smartcontractkit/chainlink-ccv/deployment/changesets" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" @@ -214,7 +213,7 @@ func (p *component) RunPhase3( if !ok { return nil, nil, fmt.Errorf("committee %q not found in topology", aggregatorInput.CommitteeName) } - cs := ccvchangesets.GenerateAggregatorConfig(ccvadapters.GetRegistry()) + cs := ccvchangesets.GenerateAggregatorConfig() output, err := cs.Apply(*e, ccvchangesets.GenerateAggregatorConfigInput{ ServiceIdentifier: instanceName + "-aggregator", CommitteeQualifier: aggregatorInput.CommitteeName, diff --git a/build/devenv/environment.go b/build/devenv/environment.go index f2475f85b..898ada1f9 100644 --- a/build/devenv/environment.go +++ b/build/devenv/environment.go @@ -39,7 +39,6 @@ import ( "github.com/smartcontractkit/chainlink-ccv/build/devenv/services/committeeverifier" executorsvc "github.com/smartcontractkit/chainlink-ccv/build/devenv/services/executor" ccvdeployment "github.com/smartcontractkit/chainlink-ccv/deployment" - ccvadapters "github.com/smartcontractkit/chainlink-ccv/deployment/adapters" ccvchangesets "github.com/smartcontractkit/chainlink-ccv/deployment/changesets" ccvshared "github.com/smartcontractkit/chainlink-ccv/deployment/shared" "github.com/smartcontractkit/chainlink-ccv/indexer/pkg/config" @@ -486,7 +485,7 @@ func generateExecutorJobSpecs( if !ok { return nil, fmt.Errorf("executor pool %q not found in topology", qualifier) } - cs := ccvchangesets.ApplyExecutorConfig(ccvadapters.GetRegistry()) + cs := ccvchangesets.ApplyExecutorConfig() output, err := cs.Apply(*e, ccvchangesets.ApplyExecutorConfigInput{ ExecutorQualifier: qualifier, NOPs: ccvchangesets.NOPInputsFromTopology(topology), @@ -584,7 +583,7 @@ func generateVerifierJobSpecs( if !ok { return nil, fmt.Errorf("committee %q not found in topology", committeeName) } - cs := ccvchangesets.ApplyVerifierConfig(ccvadapters.GetRegistry()) + cs := ccvchangesets.ApplyVerifierConfig() output, err := cs.Apply(*e, ccvchangesets.ApplyVerifierConfigInput{ CommitteeQualifier: committeeName, DefaultExecutorQualifier: devenvcommon.DefaultExecutorQualifier, diff --git a/build/devenv/environment_monolith.go b/build/devenv/environment_monolith.go index dcbb276da..24e18d634 100644 --- a/build/devenv/environment_monolith.go +++ b/build/devenv/environment_monolith.go @@ -26,7 +26,6 @@ import ( "github.com/smartcontractkit/chainlink-ccv/build/devenv/timing" "github.com/smartcontractkit/chainlink-ccv/build/devenv/util" ccvdeployment "github.com/smartcontractkit/chainlink-ccv/deployment" - ccvadapters "github.com/smartcontractkit/chainlink-ccv/deployment/adapters" ccvchangesets "github.com/smartcontractkit/chainlink-ccv/deployment/changesets" ccvshared "github.com/smartcontractkit/chainlink-ccv/deployment/shared" "github.com/smartcontractkit/chainlink-ccv/indexer/pkg/config" @@ -461,7 +460,7 @@ func NewEnvironment() (in *Cfg, err error) { if !ok { return nil, fmt.Errorf("committee %q not found in topology", aggregatorInput.CommitteeName) } - cs := ccvchangesets.GenerateAggregatorConfig(ccvadapters.GetRegistry()) + cs := ccvchangesets.GenerateAggregatorConfig() output, err := cs.Apply(*e, ccvchangesets.GenerateAggregatorConfigInput{ ServiceIdentifier: instanceName + "-aggregator", CommitteeQualifier: aggregatorInput.CommitteeName, @@ -502,7 +501,7 @@ func NewEnvironment() (in *Cfg, err error) { // One shared config is generated; all indexers use the same config and duplicated secrets/auth. if len(in.Aggregator) > 0 && len(in.Indexer) > 0 { firstIdx := in.Indexer[0] - cs := ccvchangesets.GenerateIndexerConfig(ccvadapters.GetRegistry()) + cs := ccvchangesets.GenerateIndexerConfig() output, err := cs.Apply(*e, ccvchangesets.GenerateIndexerConfigInput{ ServiceIdentifier: "indexer", CommitteeVerifierNameToQualifier: firstIdx.CommitteeVerifierNameToQualifier, @@ -718,7 +717,7 @@ func NewEnvironment() (in *Cfg, err error) { } // Use changeset to generate token verifier config from on-chain state - cs := ccvchangesets.GenerateTokenVerifierConfig(ccvadapters.GetRegistry()) + cs := ccvchangesets.GenerateTokenVerifierConfig() output, err := cs.Apply(*e, ccvchangesets.GenerateTokenVerifierConfigInput{ ServiceIdentifier: "TokenVerifier", ChainSelectors: selectors, diff --git a/build/devenv/environment_phased.go b/build/devenv/environment_phased.go index 93dfe3ea1..dcf77e90a 100644 --- a/build/devenv/environment_phased.go +++ b/build/devenv/environment_phased.go @@ -15,7 +15,6 @@ import ( executorsvc "github.com/smartcontractkit/chainlink-ccv/build/devenv/services/executor" "github.com/smartcontractkit/chainlink-ccv/build/devenv/timing" ccvdeployment "github.com/smartcontractkit/chainlink-ccv/deployment" - ccvadapters "github.com/smartcontractkit/chainlink-ccv/deployment/adapters" ccvchangesets "github.com/smartcontractkit/chainlink-ccv/deployment/changesets" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" "github.com/smartcontractkit/chainlink-deployments-framework/deployment" @@ -211,7 +210,7 @@ func runPhasedEnvironmentFinish( } // Use changeset to generate token verifier config from on-chain state - cs := ccvchangesets.GenerateTokenVerifierConfig(ccvadapters.GetRegistry()) + cs := ccvchangesets.GenerateTokenVerifierConfig() output, err := cs.Apply(*e, ccvchangesets.GenerateTokenVerifierConfigInput{ ServiceIdentifier: "TokenVerifier", ChainSelectors: selectors, diff --git a/build/devenv/evm/impl.go b/build/devenv/evm/impl.go index 827a73a88..f590568c4 100644 --- a/build/devenv/evm/impl.go +++ b/build/devenv/evm/impl.go @@ -1423,7 +1423,7 @@ func evmFeeQuoterDestChainConfigOverride(selector uint64) *lanes.FeeQuoterDestCh func (m *CCIP17EVMConfig) GetChainLaneProfile(_ *deployment.Environment, selector uint64) (cciptestinterfaces.ChainLaneProfile, error) { return cciptestinterfaces.ChainLaneProfile{ - FeeQuoterDestChainConfig: ccipChangesets.FeeQuoterDestChainConfigOverrides{ + FeeQuoterDestChainConfig: adapters.FeeQuoterDestChainConfigOverrides{ USDPerUnitGas: big.NewInt(1e6), }, }, nil diff --git a/build/devenv/go.mod b/build/devenv/go.mod index 9185edd3f..e402cccc1 100644 --- a/build/devenv/go.mod +++ b/build/devenv/go.mod @@ -19,8 +19,8 @@ require ( github.com/prometheus/client_golang v1.23.2 github.com/rs/zerolog v1.34.0 github.com/smartcontractkit/chain-selectors v1.0.98 - github.com/smartcontractkit/chainlink-ccip/chains/evm v0.0.0-20260512180815-d7a89b0a5784 - github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260512180815-d7a89b0a5784 + github.com/smartcontractkit/chainlink-ccip/chains/evm v0.0.0-20260520210346-c3a890f82ece + github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260520171506-73298226c668 github.com/smartcontractkit/chainlink-common v0.11.2-0.20260417081611-8bdbd9f45629 github.com/smartcontractkit/chainlink-deployments-framework v0.101.1 github.com/smartcontractkit/chainlink-testing-framework/framework v0.16.1 @@ -38,7 +38,7 @@ require ( github.com/lib/pq v1.11.1 github.com/sethvargo/go-retry v0.3.0 github.com/smartcontractkit/chainlink-ccv v0.0.2-0.20260501160109-2b5b8d344776 - github.com/smartcontractkit/chainlink-ccv/deployment v0.0.2-0.20260512122614-dac96f8f568b + github.com/smartcontractkit/chainlink-ccv/deployment v0.0.2-0.20260520182028-cfd627ddb60c github.com/smartcontractkit/chainlink-common/keystore v1.0.2 github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260119171452-39c98c3b33cd github.com/smartcontractkit/chainlink-protos/chainlink-ccv/committee-verifier v0.0.0-20251211142334-5c3421fe2c8d diff --git a/build/devenv/go.sum b/build/devenv/go.sum index 6111a96f0..90f7353d4 100644 --- a/build/devenv/go.sum +++ b/build/devenv/go.sum @@ -1122,14 +1122,14 @@ github.com/smartcontractkit/chain-selectors v1.0.98 h1:fuI7CQ1o5cX64eO4/Lvwtfhdp github.com/smartcontractkit/chain-selectors v1.0.98/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260306142855-8d629e752265 h1:Q/sYLdOefZUKc/Bxssq1mg8ptQE/AOot2WI+QcLoiVA= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260306142855-8d629e752265/go.mod h1:CQGkKp3YDsUuxixxmmngmRKfh6yIcftGEZsQrsSIIM8= -github.com/smartcontractkit/chainlink-ccip/chains/evm v0.0.0-20260512180815-d7a89b0a5784 h1:s0rW7GaIKf9m+kfv0dFJPdSbHaMqQiv3xamQP2RVWNE= -github.com/smartcontractkit/chainlink-ccip/chains/evm v0.0.0-20260512180815-d7a89b0a5784/go.mod h1:0dcvbCc+HRK+nIweRSHu3FjkNTV9gi+irBIB3mUUe68= +github.com/smartcontractkit/chainlink-ccip/chains/evm v0.0.0-20260520210346-c3a890f82ece h1:nilQlJxDw3XcmEgdk5xT0NpsawCQqthNLbaLLvoUDe4= +github.com/smartcontractkit/chainlink-ccip/chains/evm v0.0.0-20260520210346-c3a890f82ece/go.mod h1:91QdgssWiCOrxybF6FADOgTYznpzf/iE15cOxWOkbrU= github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260129103204-4c8453dd8139 h1:jkChf04hhdiMBApbb+lLDxHMY62Md6UeM7v++GSw3K8= github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260129103204-4c8453dd8139/go.mod h1:wuhagkM/lU0GbV2YcrROOH0GlsfXJYwm6qmpa4CK70w= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260129103204-4c8453dd8139 h1:tw3K4UkH5XfW5SoyYkvAlbzrccoGSLdz/XkxD6nyGC8= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260129103204-4c8453dd8139/go.mod h1:1WcontO9PeuKdUf5HXfs3nuICtzUvFNnyCmrHkTCF9Y= -github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260512180815-d7a89b0a5784 h1:WND8+86KW8p/mI/JBmn3BW3C4z6EqIjTRGx7EEqHYYM= -github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260512180815-d7a89b0a5784/go.mod h1:Ls0oszLvhzV3/D0ivG85sh8qmmcsVhKplmepQdFq98E= +github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260520171506-73298226c668 h1:ksqhvP0ykSunYR2H5dhn0HEV8ZIsC5S+RoXYjqidr/I= +github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260520171506-73298226c668/go.mod h1:Ls0oszLvhzV3/D0ivG85sh8qmmcsVhKplmepQdFq98E= github.com/smartcontractkit/chainlink-common v0.11.2-0.20260417081611-8bdbd9f45629 h1:YZCgnZteDIaV+Jrb6DDk4NQVJJ/ZwwYUaX9De/XUoCA= github.com/smartcontractkit/chainlink-common v0.11.2-0.20260417081611-8bdbd9f45629/go.mod h1:RnNTmxoheJYec/Gl/9t3wPLtFIHrlYjmWDdwZZJjchw= github.com/smartcontractkit/chainlink-common/keystore v1.0.2 h1:AWisx4JT3QV8tcgh6J5NCrex+wAgTYpWyHsyNPSXzsQ= diff --git a/deployment/adapters/chain_contracts_deploy.go b/deployment/adapters/chain_contracts_deploy.go new file mode 100644 index 000000000..4159c0838 --- /dev/null +++ b/deployment/adapters/chain_contracts_deploy.go @@ -0,0 +1,64 @@ +package adapters + +import ( + "github.com/Masterminds/semver/v3" + + mcmstypes "github.com/smartcontractkit/mcms/types" + + "github.com/smartcontractkit/chainlink-deployments-framework/chain" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" +) + +// ExecutorDeployParams describes a single executor to deploy on a chain. +type ExecutorDeployParams struct { + Qualifier string + Version *semver.Version +} + +// ProtocolContractsDeployInput is the per-chain input for deploying CCIP +// protocol contracts (RMNRemote, OnRamp, OffRamp, FeeQuoter, Router, Executors). +// Committee verifiers are deployed separately via CommitteeVerifierDeployAdapter. +type ProtocolContractsDeployInput struct { + // ChainSelector is the chain to deploy on. + ChainSelector uint64 + // DeployerContract is the deployer/factory address (e.g. CREATE2Factory on EVM). + DeployerContract string + // DeployTestRouter deploys a TestRouter alongside the production Router. + DeployTestRouter bool + // ExistingAddresses are already-deployed addresses on this chain. The adapter + // uses these for idempotency — contracts that already exist are not redeployed. + ExistingAddresses []datastore.AddressRef + // Executors lists executor instances to deploy. + Executors []ExecutorDeployParams + // DeployerKeyOwned when true means deployed contracts remain owned by the + // deployer key. When false, ownership is transferred to the RBAC timelock. + DeployerKeyOwned bool + // FamilyExtras carries chain-family-specific deploy parameters (e.g. RMN + // params, gas limits) that the adapter interprets. + FamilyExtras map[string]any +} + +// ProtocolContractsDeployOutput is the output of a protocol contracts deployment. +type ProtocolContractsDeployOutput struct { + // Addresses are newly deployed contract addresses. + Addresses []datastore.AddressRef + // BatchOps are MCMS batch operations (e.g. ownership transfer). + BatchOps []mcmstypes.BatchOperation + // RefsToTransferOwnership are addresses whose ownership should be + // transferred to the RBAC timelock in a follow-up step. + RefsToTransferOwnership []datastore.AddressRef +} + +// ProtocolContractsDeployAdapter deploys CCIP protocol contracts on a single +// chain. Implementations are chain-family-specific and registered via the +// singleton FamilyRegistry. +// +// The adapter's sequence is expected to be idempotent: re-running on a chain +// where contracts already exist reconciles any drifted config rather than +// redeploying. +type ProtocolContractsDeployAdapter interface { + // DeployProtocolContracts returns the per-family sequence that deploys the + // core CCIP protocol contracts on one chain. + DeployProtocolContracts() *operations.Sequence[ProtocolContractsDeployInput, ProtocolContractsDeployOutput, chain.BlockChains] +} diff --git a/deployment/adapters/lane_config.go b/deployment/adapters/lane_config.go new file mode 100644 index 000000000..f9459b69e --- /dev/null +++ b/deployment/adapters/lane_config.go @@ -0,0 +1,72 @@ +package adapters + +import ( + mcmstypes "github.com/smartcontractkit/mcms/types" + + "github.com/smartcontractkit/chainlink-deployments-framework/chain" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" +) + +// LaneConfigInput is the per-chain input for configuring lanes to/from remote chains. +// The adapter resolves local contract addresses (OnRamp, OffRamp, Router, FeeQuoter) +// from ExistingAddresses; callers do not need to supply them explicitly. +type LaneConfigInput struct { + // ChainSelector is the local chain being configured. + ChainSelector uint64 + // UseTestRouter selects the TestRouter instead of the production Router. + UseTestRouter bool + // ExistingAddresses are the deployed addresses on this chain, used by the + // adapter to resolve OnRamp, OffRamp, Router, FeeQuoter, etc. + ExistingAddresses []datastore.AddressRef + // RemoteChains maps remote chain selector → lane config for that remote chain. + RemoteChains map[uint64]RemoteLaneConfig +} + +// RemoteLaneConfig describes how the local chain should be configured for a +// specific remote chain. Nil pointer fields fall back to adapter defaults. +type RemoteLaneConfig struct { + // AllowTrafficFrom enables/disables inbound traffic from this remote chain. + // Nil uses the adapter default (typically true). + AllowTrafficFrom *bool + // ExecutorQualifier identifies the executor on the local chain for traffic + // from this remote chain. Empty uses the adapter default. + ExecutorQualifier string + // InboundCCVQualifiers are committee verifier qualifiers on the local chain + // used to verify inbound traffic from this remote chain. + InboundCCVQualifiers []string + // OutboundCCVQualifiers are committee verifier qualifiers on the local chain + // used to verify outbound traffic to this remote chain. + OutboundCCVQualifiers []string + // BaseExecutionGasCost overrides the default base execution gas cost. + BaseExecutionGasCost *uint32 + // TokenReceiverAllowed overrides whether token receivers are allowed. + TokenReceiverAllowed *bool + // MessageNetworkFeeUSDCents overrides the message network fee. + MessageNetworkFeeUSDCents *uint16 + // TokenNetworkFeeUSDCents overrides the token network fee. + TokenNetworkFeeUSDCents *uint16 + // FamilyExtras carries chain-family-specific configuration (e.g. FeeQuoter + // overrides, executor dest chain config) that the adapter interprets. + FamilyExtras map[string]any +} + +// LaneConfigOutput is the output of a lane configuration sequence. +type LaneConfigOutput struct { + // Addresses are any newly registered addresses. + Addresses []datastore.AddressRef + // BatchOps are MCMS batch operations for proposals. Empty in deployer-key mode. + BatchOps []mcmstypes.BatchOperation +} + +// LaneConfigAdapter handles onchain lane configuration on a single chain. +// Implementations are chain-family-specific and registered via Registry. +// +// The adapter's ConfigureLane sequence is expected to be idempotent: re-running +// for an already-configured lane reconciles any drifted config rather than +// creating duplicate state. +type LaneConfigAdapter interface { + // ConfigureLane returns the per-family sequence that configures lanes on a + // single chain for traffic to/from the specified remote chains. + ConfigureLane() *operations.Sequence[LaneConfigInput, LaneConfigOutput, chain.BlockChains] +} diff --git a/deployment/adapters/registry.go b/deployment/adapters/registry.go index 9e86b57b5..18682f72d 100644 --- a/deployment/adapters/registry.go +++ b/deployment/adapters/registry.go @@ -8,124 +8,152 @@ import ( "github.com/smartcontractkit/chainlink-deployments-framework/datastore" ) -// ChainAdapters bundles all chain-family-specific adapter implementations. -// A nil field means the adapter is not supported for that family. -type ChainAdapters struct { - Aggregator AggregatorConfigAdapter - Executor ExecutorConfigAdapter - Verifier VerifierConfigAdapter - Indexer IndexerConfigAdapter - TokenVerifier TokenVerifierConfigAdapter - CommitteeVerifierOnchain CommitteeVerifierOnchainAdapter - CommitteeVerifierDeploy CommitteeVerifierDeployAdapter -} - -// Registry is a single registry mapping chain family → ChainAdapters. -// Use GetRegistry() to obtain the process-wide singleton. -type Registry struct { - mu sync.Mutex - adapters map[string]ChainAdapters -} +// -------------------------------------------------------------------- +// Generic per-family registry +// -------------------------------------------------------------------- -var ( - singletonRegistry *Registry - registryOnce sync.Once -) - -func GetRegistry() *Registry { - registryOnce.Do(func() { - singletonRegistry = &Registry{ - adapters: make(map[string]ChainAdapters), - } - }) - return singletonRegistry +// FamilyRegistry is a simple per-chain-family adapter registry. +// Each adapter interface gets its own singleton FamilyRegistry[T]. +type FamilyRegistry[T any] struct { + mu sync.Mutex + adapters map[string]T // family → adapter } -// Register merges a into the existing ChainAdapters for the given family. -// Non-nil fields in a overwrite the corresponding field in the existing entry; -// nil fields leave the existing value unchanged. This allows separate packages -// (e.g. ccip for onchain adapters, ccv/evm for offchain adapters) to each -// register their piece independently without conflicting. -func (r *Registry) Register(family string, a ChainAdapters) { - r.mu.Lock() - defer r.mu.Unlock() - existing := r.adapters[family] - if a.Aggregator != nil { - existing.Aggregator = a.Aggregator - } - if a.Executor != nil { - existing.Executor = a.Executor - } - if a.Verifier != nil { - existing.Verifier = a.Verifier - } - if a.Indexer != nil { - existing.Indexer = a.Indexer - } - if a.TokenVerifier != nil { - existing.TokenVerifier = a.TokenVerifier - } - if a.CommitteeVerifierOnchain != nil { - existing.CommitteeVerifierOnchain = a.CommitteeVerifierOnchain - } - if a.CommitteeVerifierDeploy != nil { - existing.CommitteeVerifierDeploy = a.CommitteeVerifierDeploy - } - r.adapters[family] = existing +func newFamilyRegistry[T any]() *FamilyRegistry[T] { + return &FamilyRegistry[T]{adapters: make(map[string]T)} } -func (r *Registry) Get(family string) (ChainAdapters, bool) { +// Register sets the adapter for the given chain family. +func (r *FamilyRegistry[T]) Register(family string, adapter T) { r.mu.Lock() defer r.mu.Unlock() - a, ok := r.adapters[family] - return a, ok + r.adapters[family] = adapter } -func (r *Registry) GetByChain(chainSelector uint64) (ChainAdapters, error) { +// Get returns the adapter registered for the chain that owns chainSelector. +func (r *FamilyRegistry[T]) Get(chainSelector uint64) (T, error) { family, err := chainsel.GetSelectorFamily(chainSelector) if err != nil { - return ChainAdapters{}, fmt.Errorf("failed to get chain family for selector %d: %w", chainSelector, err) + var zero T + return zero, fmt.Errorf("failed to get chain family for selector %d: %w", chainSelector, err) } - a, ok := r.Get(family) + r.mu.Lock() + v, ok := r.adapters[family] + r.mu.Unlock() if !ok { - return ChainAdapters{}, fmt.Errorf("no adapters registered for chain family %q", family) + var zero T + return zero, fmt.Errorf("no adapter registered for chain family %q (selector %d)", family, chainSelector) } - return a, nil + return v, nil } -// AllDeployedCommitteeVerifierChains returns every destination chain selector that has a -// committee verifier deployed for the given qualifier, across all registered chain families. -// Discovery is datastore-only — no onchain calls are made. The per-family implementation -// (e.g. EVM) lives in chainlink-ccip/chains/evm and registers via Register at startup. -func (r *Registry) AllDeployedCommitteeVerifierChains(ds datastore.DataStore, qualifier string) []uint64 { +// ForEach calls fn for every registered adapter. Intended for cross-family +// discovery (e.g. AllDeployedCommitteeVerifierChains). +func (r *FamilyRegistry[T]) ForEach(fn func(family string, adapter T)) { r.mu.Lock() defer r.mu.Unlock() - var chains []uint64 - for _, a := range r.adapters { - if a.Aggregator != nil { - chains = append(chains, a.Aggregator.GetDeployedChains(ds, qualifier)...) - } + for f, a := range r.adapters { + fn(f, a) } - return chains } -// AllDeployedExecutorChains collects all chain selectors with executor proxies deployed, -// across all registered families. -func (r *Registry) AllDeployedExecutorChains(ds datastore.DataStore, qualifier string) []uint64 { +// IsEmpty reports whether any adapter has been registered. +func (r *FamilyRegistry[T]) IsEmpty() bool { r.mu.Lock() defer r.mu.Unlock() + return len(r.adapters) == 0 +} + +// -------------------------------------------------------------------- +// Per-adapter-type singletons +// -------------------------------------------------------------------- + +var ( + aggregatorRegistry *FamilyRegistry[AggregatorConfigAdapter] + aggregatorOnce sync.Once + executorRegistry *FamilyRegistry[ExecutorConfigAdapter] + executorOnce sync.Once + verifierRegistry *FamilyRegistry[VerifierConfigAdapter] + verifierOnce sync.Once + indexerRegistry *FamilyRegistry[IndexerConfigAdapter] + indexerOnce sync.Once + tokenVerifierRegistry *FamilyRegistry[TokenVerifierConfigAdapter] + tokenVerifierOnce sync.Once + committeeVerifierOnchainRegistry *FamilyRegistry[CommitteeVerifierOnchainAdapter] + committeeVerifierOnchainOnce sync.Once + committeeVerifierDeployRegistry *FamilyRegistry[CommitteeVerifierDeployAdapter] + committeeVerifierDeployOnce sync.Once + laneConfigRegistry *FamilyRegistry[LaneConfigAdapter] + laneConfigOnce sync.Once + protocolContractsDeployRegistry *FamilyRegistry[ProtocolContractsDeployAdapter] + protocolContractsDeployOnce sync.Once +) + +func GetAggregatorRegistry() *FamilyRegistry[AggregatorConfigAdapter] { + aggregatorOnce.Do(func() { aggregatorRegistry = newFamilyRegistry[AggregatorConfigAdapter]() }) + return aggregatorRegistry +} + +func GetExecutorRegistry() *FamilyRegistry[ExecutorConfigAdapter] { + executorOnce.Do(func() { executorRegistry = newFamilyRegistry[ExecutorConfigAdapter]() }) + return executorRegistry +} + +func GetVerifierRegistry() *FamilyRegistry[VerifierConfigAdapter] { + verifierOnce.Do(func() { verifierRegistry = newFamilyRegistry[VerifierConfigAdapter]() }) + return verifierRegistry +} + +func GetIndexerRegistry() *FamilyRegistry[IndexerConfigAdapter] { + indexerOnce.Do(func() { indexerRegistry = newFamilyRegistry[IndexerConfigAdapter]() }) + return indexerRegistry +} + +func GetTokenVerifierRegistry() *FamilyRegistry[TokenVerifierConfigAdapter] { + tokenVerifierOnce.Do(func() { tokenVerifierRegistry = newFamilyRegistry[TokenVerifierConfigAdapter]() }) + return tokenVerifierRegistry +} + +func GetCommitteeVerifierOnchainRegistry() *FamilyRegistry[CommitteeVerifierOnchainAdapter] { + committeeVerifierOnchainOnce.Do(func() { committeeVerifierOnchainRegistry = newFamilyRegistry[CommitteeVerifierOnchainAdapter]() }) + return committeeVerifierOnchainRegistry +} + +func GetCommitteeVerifierDeployRegistry() *FamilyRegistry[CommitteeVerifierDeployAdapter] { + committeeVerifierDeployOnce.Do(func() { committeeVerifierDeployRegistry = newFamilyRegistry[CommitteeVerifierDeployAdapter]() }) + return committeeVerifierDeployRegistry +} + +func GetLaneConfigRegistry() *FamilyRegistry[LaneConfigAdapter] { + laneConfigOnce.Do(func() { laneConfigRegistry = newFamilyRegistry[LaneConfigAdapter]() }) + return laneConfigRegistry +} + +func GetProtocolContractsDeployRegistry() *FamilyRegistry[ProtocolContractsDeployAdapter] { + protocolContractsDeployOnce.Do(func() { protocolContractsDeployRegistry = newFamilyRegistry[ProtocolContractsDeployAdapter]() }) + return protocolContractsDeployRegistry +} + +// -------------------------------------------------------------------- +// Cross-family discovery helpers +// -------------------------------------------------------------------- + +// AllDeployedCommitteeVerifierChains returns every chain selector that has a +// committee verifier deployed for the given qualifier, across all families. +func AllDeployedCommitteeVerifierChains(ds datastore.DataStore, qualifier string) []uint64 { var chains []uint64 - for _, a := range r.adapters { - if a.Executor != nil { - chains = append(chains, a.Executor.GetDeployedChains(ds, qualifier)...) - } - } + GetAggregatorRegistry().ForEach(func(_ string, a AggregatorConfigAdapter) { + chains = append(chains, a.GetDeployedChains(ds, qualifier)...) + }) return chains } -// HasAdapters reports whether any chain family has been registered. -func (r *Registry) HasAdapters() bool { - r.mu.Lock() - defer r.mu.Unlock() - return len(r.adapters) > 0 +// AllDeployedExecutorChains returns every chain selector with executor proxies +// deployed, across all families. +func AllDeployedExecutorChains(ds datastore.DataStore, qualifier string) []uint64 { + var chains []uint64 + GetExecutorRegistry().ForEach(func(_ string, a ExecutorConfigAdapter) { + chains = append(chains, a.GetDeployedChains(ds, qualifier)...) + }) + return chains } diff --git a/deployment/changesets/add_nop_to_committee.go b/deployment/changesets/add_nop_to_committee.go index 73a953eb9..460e81188 100644 --- a/deployment/changesets/add_nop_to_committee.go +++ b/deployment/changesets/add_nop_to_committee.go @@ -104,17 +104,17 @@ type AddNOPOffchainInput struct { // // Onchain-first ordering is safe because adding a new signer does not raise the quorum // requirement — the existing signers already satisfy the current threshold. -func AddNOPToCommittee(registry *adapters.Registry) deployment.ChangeSetV2[AddNOPToCommitteeInput] { +func AddNOPToCommittee() deployment.ChangeSetV2[AddNOPToCommitteeInput] { validate := func(e deployment.Environment, cfg AddNOPToCommitteeInput) error { - return validateStep1NOP(e, cfg.CommitteeQualifier, cfg.NOPAlias, cfg.SourceChainSelectors, registry) + return validateStep1NOP(e, cfg.CommitteeQualifier, cfg.NOPAlias, cfg.SourceChainSelectors) } apply := func(e deployment.Environment, cfg AddNOPToCommitteeInput) (deployment.ChangesetOutput, error) { - signerFamily, err := getSignerFamilyFromRegistry(registry, cfg.SourceChainSelectors) + signerFamily, err := getSignerFamilyFromRegistry(cfg.SourceChainSelectors) if err != nil { return deployment.ChangesetOutput{}, err } - if err := applySignerChangesOnchain(e, registry, cfg.CommitteeQualifier, cfg.NOPAlias, signerFamily, + if err := applySignerChangesOnchain(e, cfg.CommitteeQualifier, cfg.NOPAlias, signerFamily, cfg.SourceChainSelectors, cfg.NewThreshold, buildAddSignerChange); err != nil { return deployment.ChangesetOutput{}, err } @@ -126,7 +126,7 @@ func AddNOPToCommittee(registry *adapters.Registry) deployment.ChangeSetV2[AddNO // validateStep1NOP is the shared validation for the step-1 onchain changesets (AddNOPToCommittee // and RemoveNOPFromCommittee). -func validateStep1NOP(e deployment.Environment, qualifier, nopAlias string, sourceChainSelectors []uint64, registry *adapters.Registry) error { +func validateStep1NOP(e deployment.Environment, qualifier, nopAlias string, sourceChainSelectors []uint64) error { if e.Offchain == nil { return fmt.Errorf("offchain client is required") } @@ -139,7 +139,7 @@ func validateStep1NOP(e deployment.Environment, qualifier, nopAlias string, sour if nopAlias == "" { return fmt.Errorf("NOP alias is required") } - if _, err := getSignerFamilyFromRegistry(registry, sourceChainSelectors); err != nil { + if _, err := getSignerFamilyFromRegistry(sourceChainSelectors); err != nil { return err } return nil @@ -150,7 +150,6 @@ func validateStep1NOP(e deployment.Environment, qualifier, nopAlias string, sour // committee, builds the change via buildChange, and submits an ApplySignatureConfigs call. func applySignerChangesOnchain( e deployment.Environment, - registry *adapters.Registry, committeeQualifier string, nopAlias string, signerFamily string, @@ -165,7 +164,7 @@ func applySignerChangesOnchain( return err } - committeeChains := registry.AllDeployedCommitteeVerifierChains(e.DataStore, committeeQualifier) + committeeChains := adapters.AllDeployedCommitteeVerifierChains(e.DataStore, committeeQualifier) if len(committeeChains) == 0 { return fmt.Errorf( "no dest chains found with committee verifier for qualifier %q — ensure adapters are registered and the committee is deployed", @@ -173,7 +172,7 @@ func applySignerChangesOnchain( ) } - committeeStates, err := scanCommitteeStatesForChains(ctx, e, registry, committeeQualifier, committeeChains) + committeeStates, err := scanCommitteeStatesForChains(ctx, e, committeeQualifier, committeeChains) if err != nil { return err } @@ -187,8 +186,11 @@ func applySignerChangesOnchain( if len(change.NewConfigs) == 0 { continue // this dest chain has no configs for the requested source chains } - a, _ := registry.GetByChain(sel) - if applyErr := a.CommitteeVerifierOnchain.ApplySignatureConfigs(ctx, e, sel, committeeQualifier, change); applyErr != nil { + onchain, onchainErr := adapters.GetCommitteeVerifierOnchainRegistry().Get(sel) + if onchainErr != nil { + return fmt.Errorf("dest chain %d: %w", sel, onchainErr) + } + if applyErr := onchain.ApplySignatureConfigs(ctx, e, sel, committeeQualifier, change); applyErr != nil { return fmt.Errorf("dest chain %d: ApplySignatureConfigs failed: %w", sel, applyErr) } applied++ @@ -217,7 +219,7 @@ func applySignerChangesOnchain( // When NOPAlias and Aggregators are both set, verifier jobs are provisioned for the new NOP // via JD in the same run. The signer address is taken from ExpectedSignerAddress if set, // otherwise fetched from JD. -func AddNOPOffchain(registry *adapters.Registry) deployment.ChangeSetV2[AddNOPOffchainInput] { +func AddNOPOffchain() deployment.ChangeSetV2[AddNOPOffchainInput] { validate := func(e deployment.Environment, cfg AddNOPOffchainInput) error { if cfg.CommitteeQualifier == "" { return fmt.Errorf("committee qualifier is required") @@ -238,27 +240,23 @@ func AddNOPOffchain(registry *adapters.Registry) deployment.ChangeSetV2[AddNOPOf return fmt.Errorf("executor qualifier is required for job provisioning") } - committeeChains := registry.AllDeployedCommitteeVerifierChains(e.DataStore, cfg.CommitteeQualifier) + committeeChains := adapters.AllDeployedCommitteeVerifierChains(e.DataStore, cfg.CommitteeQualifier) if len(committeeChains) == 0 { return fmt.Errorf("no dest chains found for committee %q — step-1 may not have been applied or adapters are not registered", cfg.CommitteeQualifier) } for _, sel := range committeeChains { - a, err := registry.GetByChain(sel) - if err != nil { + if _, err := adapters.GetCommitteeVerifierOnchainRegistry().Get(sel); err != nil { return fmt.Errorf("dest chain %d: %w", sel, err) } - if a.CommitteeVerifierOnchain == nil { - return fmt.Errorf("dest chain %d: no CommitteeVerifierOnchain adapter registered", sel) - } - if a.Aggregator == nil { - return fmt.Errorf("dest chain %d: no Aggregator adapter registered", sel) + if _, err := adapters.GetAggregatorRegistry().Get(sel); err != nil { + return fmt.Errorf("dest chain %d: %w", sel, err) } } // Safety backstop: assert the new signer is present onchain on every dest chain for // every source chain. Catches hook misfires and out-of-order manual invocations. if cfg.ExpectedSignerAddress != "" { - committeeStates, err := scanCommitteeStatesForChains(e.GetContext(), e, registry, cfg.CommitteeQualifier, committeeChains) + committeeStates, err := scanCommitteeStatesForChains(e.GetContext(), e, cfg.CommitteeQualifier, committeeChains) if err != nil { return err } @@ -292,12 +290,12 @@ func AddNOPOffchain(registry *adapters.Registry) deployment.ChangeSetV2[AddNOPOf } apply := func(e deployment.Environment, cfg AddNOPOffchainInput) (deployment.ChangesetOutput, error) { - committeeChains := registry.AllDeployedCommitteeVerifierChains(e.DataStore, cfg.CommitteeQualifier) + committeeChains := adapters.AllDeployedCommitteeVerifierChains(e.DataStore, cfg.CommitteeQualifier) if len(committeeChains) == 0 { return deployment.ChangesetOutput{}, fmt.Errorf("no dest chains found for committee %q", cfg.CommitteeQualifier) } - committee, err := buildAggregatorCommittee(e, registry, cfg.CommitteeQualifier, committeeChains, nil) + committee, err := buildAggregatorCommittee(e, cfg.CommitteeQualifier, committeeChains, nil) if err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("failed to build aggregator config: %w", err) } @@ -314,7 +312,7 @@ func AddNOPOffchain(registry *adapters.Registry) deployment.ChangeSetV2[AddNOPOf } } - manageDS, reports, err := provisionVerifierJobForNOP(e, registry, cfg, committeeChains, outputDS.Seal()) + manageDS, reports, err := provisionVerifierJobForNOP(e, cfg, committeeChains, outputDS.Seal()) if err != nil { return deployment.ChangesetOutput{Reports: reports}, err } @@ -330,12 +328,11 @@ func AddNOPOffchain(registry *adapters.Registry) deployment.ChangeSetV2[AddNOPOf // new job metadata. func provisionVerifierJobForNOP( e deployment.Environment, - registry *adapters.Registry, cfg AddNOPOffchainInput, committeeChains []uint64, baseDS datastore.DataStore, ) (datastore.MutableDataStore, []operations.Report[any, any], error) { - signerFamily, err := getSignerFamilyFromRegistry(registry, cfg.SourceChainSelectors) + signerFamily, err := getSignerFamilyFromRegistry(cfg.SourceChainSelectors) if err != nil { return nil, nil, fmt.Errorf("failed to determine signer family for job provisioning: %w", err) } @@ -349,7 +346,7 @@ func provisionVerifierJobForNOP( signerAddress = addr } - contractAddresses, err := buildVerifierContractConfigs(registry, e, committeeChains, cfg.CommitteeQualifier, cfg.ExecutorQualifier) + contractAddresses, err := buildVerifierContractConfigs(e, committeeChains, cfg.CommitteeQualifier, cfg.ExecutorQualifier) if err != nil { return nil, nil, fmt.Errorf("failed to build verifier contract configs: %w", err) } diff --git a/deployment/changesets/add_nop_to_committee_test.go b/deployment/changesets/add_nop_to_committee_test.go index d1f1e9336..84c072caa 100644 --- a/deployment/changesets/add_nop_to_committee_test.go +++ b/deployment/changesets/add_nop_to_committee_test.go @@ -160,12 +160,8 @@ func TestBuildAddSignerChange_DoesNotMutateOriginalSigners(t *testing.T) { // ---- AddNOPToCommittee validation ---- -func newEVMRegistry(onchain adapters.CommitteeVerifierOnchainAdapter) *adapters.Registry { - r := adapters.GetRegistry() - r.Register(chainsel.FamilyEVM, adapters.ChainAdapters{ - CommitteeVerifierOnchain: onchain, - }) - return r +func registerEVMOnchain(onchain adapters.CommitteeVerifierOnchainAdapter) { + adapters.GetCommitteeVerifierOnchainRegistry().Register(chainsel.FamilyEVM, onchain) } func newTestEnvironmentWithOffchain() deployment.Environment { @@ -175,7 +171,8 @@ func newTestEnvironmentWithOffchain() deployment.Environment { } func TestAddNOPToCommittee_Validation_MissingQualifier(t *testing.T) { - cs := AddNOPToCommittee(newEVMRegistry(&stubOnchainAdapter{})) + registerEVMOnchain(&stubOnchainAdapter{}) + cs := AddNOPToCommittee() err := cs.VerifyPreconditions(newTestEnvironmentWithOffchain(), AddNOPToCommitteeInput{ SourceChainSelectors: []uint64{chainsel.TEST_90000001.Selector}, NOPAlias: testNOPAlias, @@ -185,7 +182,8 @@ func TestAddNOPToCommittee_Validation_MissingQualifier(t *testing.T) { } func TestAddNOPToCommittee_Validation_MissingSourceChainSelectors(t *testing.T) { - cs := AddNOPToCommittee(newEVMRegistry(&stubOnchainAdapter{})) + registerEVMOnchain(&stubOnchainAdapter{}) + cs := AddNOPToCommittee() err := cs.VerifyPreconditions(newTestEnvironmentWithOffchain(), AddNOPToCommitteeInput{ CommitteeQualifier: testQualifier, NOPAlias: testNOPAlias, @@ -195,7 +193,8 @@ func TestAddNOPToCommittee_Validation_MissingSourceChainSelectors(t *testing.T) } func TestAddNOPToCommittee_Validation_MissingNOPAlias(t *testing.T) { - cs := AddNOPToCommittee(newEVMRegistry(&stubOnchainAdapter{})) + registerEVMOnchain(&stubOnchainAdapter{}) + cs := AddNOPToCommittee() err := cs.VerifyPreconditions(newTestEnvironmentWithOffchain(), AddNOPToCommitteeInput{ CommitteeQualifier: testQualifier, SourceChainSelectors: []uint64{chainsel.TEST_90000001.Selector}, @@ -205,7 +204,8 @@ func TestAddNOPToCommittee_Validation_MissingNOPAlias(t *testing.T) { } func TestAddNOPToCommittee_Validation_RequiresOffchainClient(t *testing.T) { - cs := AddNOPToCommittee(newEVMRegistry(&stubOnchainAdapter{})) + registerEVMOnchain(&stubOnchainAdapter{}) + cs := AddNOPToCommittee() err := cs.VerifyPreconditions(deployment.Environment{}, AddNOPToCommitteeInput{ CommitteeQualifier: testQualifier, SourceChainSelectors: []uint64{chainsel.TEST_90000001.Selector}, @@ -218,7 +218,8 @@ func TestAddNOPToCommittee_Validation_RequiresOffchainClient(t *testing.T) { // ---- AddNOPOffchain validation ---- func TestAddNOPOffchain_Validation_MissingQualifier(t *testing.T) { - cs := AddNOPOffchain(newFullEVMRegistry(&stubFullAdapter{})) + registerFullEVMAdapters(&stubFullAdapter{}) + cs := AddNOPOffchain() err := cs.VerifyPreconditions(deployment.Environment{}, AddNOPOffchainInput{ SourceChainSelectors: []uint64{chainsel.TEST_90000001.Selector}, ServiceIdentifiers: []string{"svc1"}, @@ -228,7 +229,8 @@ func TestAddNOPOffchain_Validation_MissingQualifier(t *testing.T) { } func TestAddNOPOffchain_Validation_MissingSourceChainSelectors(t *testing.T) { - cs := AddNOPOffchain(newFullEVMRegistry(&stubFullAdapter{})) + registerFullEVMAdapters(&stubFullAdapter{}) + cs := AddNOPOffchain() err := cs.VerifyPreconditions(deployment.Environment{}, AddNOPOffchainInput{ CommitteeQualifier: testQualifier, ServiceIdentifiers: []string{"svc1"}, @@ -238,7 +240,8 @@ func TestAddNOPOffchain_Validation_MissingSourceChainSelectors(t *testing.T) { } func TestAddNOPOffchain_Validation_MissingServiceIdentifiers(t *testing.T) { - cs := AddNOPOffchain(newFullEVMRegistry(&stubFullAdapter{})) + registerFullEVMAdapters(&stubFullAdapter{}) + cs := AddNOPOffchain() err := cs.VerifyPreconditions(deployment.Environment{}, AddNOPOffchainInput{ CommitteeQualifier: testQualifier, SourceChainSelectors: []uint64{chainsel.TEST_90000001.Selector}, @@ -248,7 +251,8 @@ func TestAddNOPOffchain_Validation_MissingServiceIdentifiers(t *testing.T) { } func TestAddNOPOffchain_Validation_MissingNOPAlias(t *testing.T) { - cs := AddNOPOffchain(newFullEVMRegistry(&stubFullAdapter{})) + registerFullEVMAdapters(&stubFullAdapter{}) + cs := AddNOPOffchain() err := cs.VerifyPreconditions(deployment.Environment{}, AddNOPOffchainInput{ CommitteeQualifier: testQualifier, SourceChainSelectors: []uint64{chainsel.TEST_90000001.Selector}, @@ -259,7 +263,8 @@ func TestAddNOPOffchain_Validation_MissingNOPAlias(t *testing.T) { } func TestAddNOPOffchain_Validation_MissingAggregators(t *testing.T) { - cs := AddNOPOffchain(newFullEVMRegistry(&stubFullAdapter{})) + registerFullEVMAdapters(&stubFullAdapter{}) + cs := AddNOPOffchain() err := cs.VerifyPreconditions(deployment.Environment{}, AddNOPOffchainInput{ CommitteeQualifier: testQualifier, SourceChainSelectors: []uint64{chainsel.TEST_90000001.Selector}, @@ -271,7 +276,8 @@ func TestAddNOPOffchain_Validation_MissingAggregators(t *testing.T) { } func TestAddNOPOffchain_Validation_MissingExecutorQualifier(t *testing.T) { - cs := AddNOPOffchain(newFullEVMRegistry(&stubFullAdapter{})) + registerFullEVMAdapters(&stubFullAdapter{}) + cs := AddNOPOffchain() err := cs.VerifyPreconditions(deployment.Environment{}, AddNOPOffchainInput{ CommitteeQualifier: testQualifier, SourceChainSelectors: []uint64{chainsel.TEST_90000001.Selector}, @@ -303,7 +309,8 @@ func TestAddNOPOffchain_Validation_BackstopPassesWhenSignerPresent(t *testing.T) verifierAddrs: map[uint64]string{sel1: "0x1111"}, } - cs := AddNOPOffchain(newFullEVMRegistry(adapter)) + registerFullEVMAdapters(adapter) + cs := AddNOPOffchain() env := deployment.Environment{ BlockChains: newTestBlockChains([]uint64{sel1}), DataStore: datastore.NewMemoryDataStore().Seal(), @@ -340,7 +347,8 @@ func TestAddNOPOffchain_Validation_BackstopFailsWhenSignerAbsent(t *testing.T) { verifierAddrs: map[uint64]string{sel1: "0x1111"}, } - cs := AddNOPOffchain(newFullEVMRegistry(adapter)) + registerFullEVMAdapters(adapter) + cs := AddNOPOffchain() env := deployment.Environment{ BlockChains: newTestBlockChains([]uint64{sel1}), DataStore: datastore.NewMemoryDataStore().Seal(), @@ -366,7 +374,8 @@ func TestAddNOPOffchain_Validation_BackstopSkippedWhenAddressEmpty(t *testing.T) states: map[uint64][]*adapters.CommitteeState{sel1: {}}, verifierAddrs: map[uint64]string{sel1: "0x1111"}, } - cs := AddNOPOffchain(newFullEVMRegistry(adapter)) + registerFullEVMAdapters(adapter) + cs := AddNOPOffchain() env := deployment.Environment{ BlockChains: newTestBlockChains([]uint64{sel1}), DataStore: datastore.NewMemoryDataStore().Seal(), @@ -408,8 +417,8 @@ func TestAddNOPOffchain_Apply_WritesAggregatorConfigToDataStore(t *testing.T) { verifierAddrs: map[uint64]string{sel1: verifierAddr}, } - r := newFullEVMRegistry(adapter) - cs := AddNOPOffchain(r) + registerFullEVMAdapters(adapter) + cs := AddNOPOffchain() env := newTestEnvForApply(t, nopAlias, []uint64{sel1}) @@ -465,8 +474,8 @@ func TestAddNOPOffchain_Apply_UsesAllDiscoveredDestChains(t *testing.T) { verifierAddrs: map[uint64]string{sel1: addr1, sel2: addr2}, } - r := newFullEVMRegistry(adapter) - cs := AddNOPOffchain(r) + registerFullEVMAdapters(adapter) + cs := AddNOPOffchain() env := newTestEnvForApply(t, nopAlias, []uint64{sel1, sel2}) @@ -498,7 +507,8 @@ func TestAddNOPOffchain_Apply_ScanError(t *testing.T) { scanErr: fmt.Errorf("rpc timeout"), } - cs := AddNOPOffchain(newFullEVMRegistry(adapter)) + registerFullEVMAdapters(adapter) + cs := AddNOPOffchain() env := deployment.Environment{ BlockChains: newTestBlockChains([]uint64{sel1}), } @@ -521,7 +531,8 @@ func TestAddNOPOffchain_Apply_CommitteeNotFound(t *testing.T) { }, } - cs := AddNOPOffchain(newFullEVMRegistry(adapter)) + registerFullEVMAdapters(adapter) + cs := AddNOPOffchain() env := deployment.Environment{ BlockChains: newTestBlockChains([]uint64{sel1}), } @@ -556,8 +567,8 @@ func TestAddNOPOffchain_Apply_PreservesExistingDataStoreEntries(t *testing.T) { verifierAddrs: map[uint64]string{sel1: verifierAddr}, } - r := newFullEVMRegistry(adapter) - cs := AddNOPOffchain(r) + registerFullEVMAdapters(adapter) + cs := AddNOPOffchain() env := newTestEnvForApply(t, nopAlias, []uint64{sel1}) @@ -742,14 +753,10 @@ func (s *stubFullAdapter) GetSignerAddressFamily() string { return chainsel.FamilyEVM } -func newFullEVMRegistry(a *stubFullAdapter) *adapters.Registry { - r := adapters.GetRegistry() - r.Register(chainsel.FamilyEVM, adapters.ChainAdapters{ - CommitteeVerifierOnchain: a, - Aggregator: a, - Verifier: a, - }) - return r +func registerFullEVMAdapters(a *stubFullAdapter) { + adapters.GetCommitteeVerifierOnchainRegistry().Register(chainsel.FamilyEVM, a) + adapters.GetAggregatorRegistry().Register(chainsel.FamilyEVM, a) + adapters.GetVerifierRegistry().Register(chainsel.FamilyEVM, a) } func newTestBlockChains(selectors []uint64) cldf_chain.BlockChains { diff --git a/deployment/changesets/apply_executor_config.go b/deployment/changesets/apply_executor_config.go index e345e41fd..28ba5bd03 100644 --- a/deployment/changesets/apply_executor_config.go +++ b/deployment/changesets/apply_executor_config.go @@ -54,7 +54,7 @@ type ApplyExecutorConfigInput struct { // // The input is imperative — callers pass the pool description and the // participating NOPs directly, with no *EnvironmentTopology. -func ApplyExecutorConfig(registry *adapters.Registry) deployment.ChangeSetV2[ApplyExecutorConfigInput] { +func ApplyExecutorConfig() deployment.ChangeSetV2[ApplyExecutorConfigInput] { validate := func(e deployment.Environment, cfg ApplyExecutorConfigInput) error { if cfg.ExecutorQualifier == "" { return fmt.Errorf("executor qualifier is required") @@ -108,7 +108,7 @@ func ApplyExecutorConfig(registry *adapters.Registry) deployment.ChangeSetV2[App } apply := func(e deployment.Environment, cfg ApplyExecutorConfigInput) (deployment.ChangesetOutput, error) { - selectors := registry.AllDeployedExecutorChains(e.DataStore, cfg.ExecutorQualifier) + selectors := adapters.AllDeployedExecutorChains(e.DataStore, cfg.ExecutorQualifier) if len(selectors) == 0 { return runOrphanJobCleanup( @@ -135,7 +135,7 @@ func ApplyExecutorConfig(registry *adapters.Registry) deployment.ChangeSetV2[App return deployment.ChangesetOutput{}, err } - chainConfigs, err := buildExecutorChainConfigs(registry, e.DataStore, selectors, cfg.ExecutorQualifier) + chainConfigs, err := buildExecutorChainConfigs(e.DataStore, selectors, cfg.ExecutorQualifier) if err != nil { return deployment.ChangesetOutput{}, err } @@ -194,21 +194,17 @@ func ApplyExecutorConfig(registry *adapters.Registry) deployment.ChangeSetV2[App } func buildExecutorChainConfigs( - registry *adapters.Registry, ds datastore.DataStore, selectors []uint64, qualifier string, ) (map[string]executor.ChainConfiguration, error) { chainConfigs := make(map[string]executor.ChainConfiguration, len(selectors)) for _, sel := range selectors { - a, err := registry.GetByChain(sel) + exec, err := adapters.GetExecutorRegistry().Get(sel) if err != nil { - return nil, fmt.Errorf("no adapter for chain %d: %w", sel, err) + return nil, fmt.Errorf("no executor config adapter registered for chain %d: %w", sel, err) } - if a.Executor == nil { - return nil, fmt.Errorf("no executor config adapter registered for chain %d", sel) - } - cfg, err := a.Executor.BuildChainConfig(ds, sel, qualifier) + cfg, err := exec.BuildChainConfig(ds, sel, qualifier) if err != nil { return nil, fmt.Errorf("failed to build config for chain %d: %w", sel, err) } diff --git a/deployment/changesets/apply_executor_config_test.go b/deployment/changesets/apply_executor_config_test.go index 9c72944f9..13a0c6012 100644 --- a/deployment/changesets/apply_executor_config_test.go +++ b/deployment/changesets/apply_executor_config_test.go @@ -10,13 +10,10 @@ import ( chainsel "github.com/smartcontractkit/chain-selectors" "github.com/smartcontractkit/chainlink-deployments-framework/deployment" - "github.com/smartcontractkit/chainlink-ccv/deployment/adapters" "github.com/smartcontractkit/chainlink-ccv/deployment/shared" ) -func newEmptyExecutorRegistry() *adapters.Registry { - return adapters.GetRegistry() -} +// No adapter registration needed — validation tests don't reach the adapter layer. func sampleExecutorPool(sel uint64, aliases ...shared.NOPAlias) ExecutorPoolInput { return ExecutorPoolInput{ @@ -32,7 +29,7 @@ func sampleExecutorPool(sel uint64, aliases ...shared.NOPAlias) ExecutorPoolInpu } func TestApplyExecutorConfig_Validation_RequiresQualifier(t *testing.T) { - cs := ApplyExecutorConfig(newEmptyExecutorRegistry()) + cs := ApplyExecutorConfig() err := cs.VerifyPreconditions(deployment.Environment{}, ApplyExecutorConfigInput{ NOPs: []NOPInput{{Alias: "nop1"}}, IndexerAddress: []string{"indexer:1234"}, @@ -43,7 +40,7 @@ func TestApplyExecutorConfig_Validation_RequiresQualifier(t *testing.T) { } func TestApplyExecutorConfig_Validation_RequiresNOPs(t *testing.T) { - cs := ApplyExecutorConfig(newEmptyExecutorRegistry()) + cs := ApplyExecutorConfig() err := cs.VerifyPreconditions(deployment.Environment{}, ApplyExecutorConfigInput{ ExecutorQualifier: "default-executor", IndexerAddress: []string{"indexer:1234"}, @@ -54,7 +51,7 @@ func TestApplyExecutorConfig_Validation_RequiresNOPs(t *testing.T) { } func TestApplyExecutorConfig_Validation_RequiresIndexerAddress(t *testing.T) { - cs := ApplyExecutorConfig(newEmptyExecutorRegistry()) + cs := ApplyExecutorConfig() err := cs.VerifyPreconditions(deployment.Environment{}, ApplyExecutorConfigInput{ ExecutorQualifier: "default-executor", NOPs: []NOPInput{{Alias: "nop1"}}, @@ -65,7 +62,7 @@ func TestApplyExecutorConfig_Validation_RequiresIndexerAddress(t *testing.T) { } func TestApplyExecutorConfig_Validation_RequiresPoolChainConfigs(t *testing.T) { - cs := ApplyExecutorConfig(newEmptyExecutorRegistry()) + cs := ApplyExecutorConfig() err := cs.VerifyPreconditions(deployment.Environment{}, ApplyExecutorConfigInput{ ExecutorQualifier: "default-executor", NOPs: []NOPInput{{Alias: "nop1"}}, @@ -76,7 +73,7 @@ func TestApplyExecutorConfig_Validation_RequiresPoolChainConfigs(t *testing.T) { } func TestApplyExecutorConfig_Validation_DuplicateNOPAliasRejected(t *testing.T) { - cs := ApplyExecutorConfig(newEmptyExecutorRegistry()) + cs := ApplyExecutorConfig() err := cs.VerifyPreconditions(deployment.Environment{}, ApplyExecutorConfigInput{ ExecutorQualifier: "default-executor", NOPs: []NOPInput{{Alias: "nop1"}, {Alias: "nop1"}}, @@ -88,7 +85,7 @@ func TestApplyExecutorConfig_Validation_DuplicateNOPAliasRejected(t *testing.T) } func TestApplyExecutorConfig_Validation_PoolReferencesUnknownNOPRejected(t *testing.T) { - cs := ApplyExecutorConfig(newEmptyExecutorRegistry()) + cs := ApplyExecutorConfig() err := cs.VerifyPreconditions(deployment.Environment{}, ApplyExecutorConfigInput{ ExecutorQualifier: "default-executor", NOPs: []NOPInput{{Alias: "nop1"}}, @@ -100,7 +97,7 @@ func TestApplyExecutorConfig_Validation_PoolReferencesUnknownNOPRejected(t *test } func TestApplyExecutorConfig_Validation_TargetNOPMustBeInPool(t *testing.T) { - cs := ApplyExecutorConfig(newEmptyExecutorRegistry()) + cs := ApplyExecutorConfig() err := cs.VerifyPreconditions(deployment.Environment{}, ApplyExecutorConfigInput{ ExecutorQualifier: "default-executor", NOPs: []NOPInput{{Alias: "nop1"}, {Alias: "nop2"}}, @@ -113,7 +110,7 @@ func TestApplyExecutorConfig_Validation_TargetNOPMustBeInPool(t *testing.T) { } func TestApplyExecutorConfig_Validation_ProductionRejectsPyroscope(t *testing.T) { - cs := ApplyExecutorConfig(newEmptyExecutorRegistry()) + cs := ApplyExecutorConfig() err := cs.VerifyPreconditions(deployment.Environment{Name: "mainnet"}, ApplyExecutorConfigInput{ ExecutorQualifier: "default-executor", NOPs: []NOPInput{{Alias: "nop1"}}, @@ -126,7 +123,7 @@ func TestApplyExecutorConfig_Validation_ProductionRejectsPyroscope(t *testing.T) } func TestApplyExecutorConfig_Validation_AcceptsValidImperativeInput(t *testing.T) { - cs := ApplyExecutorConfig(newEmptyExecutorRegistry()) + cs := ApplyExecutorConfig() err := cs.VerifyPreconditions(deployment.Environment{}, ApplyExecutorConfigInput{ ExecutorQualifier: "default-executor", NOPs: []NOPInput{{Alias: "nop1", Mode: shared.NOPModeCL}}, diff --git a/deployment/changesets/apply_verifier_config.go b/deployment/changesets/apply_verifier_config.go index 36f02a277..cc86cde4c 100644 --- a/deployment/changesets/apply_verifier_config.go +++ b/deployment/changesets/apply_verifier_config.go @@ -60,7 +60,7 @@ type ApplyVerifierConfigInput struct { // // The input is imperative — callers pass the committee description and the // participating NOPs directly, with no *EnvironmentTopology. -func ApplyVerifierConfig(registry *adapters.Registry) deployment.ChangeSetV2[ApplyVerifierConfigInput] { +func ApplyVerifierConfig() deployment.ChangeSetV2[ApplyVerifierConfigInput] { validate := func(e deployment.Environment, cfg ApplyVerifierConfigInput) error { if cfg.CommitteeQualifier == "" { return fmt.Errorf("committee qualifier is required") @@ -137,7 +137,7 @@ func ApplyVerifierConfig(registry *adapters.Registry) deployment.ChangeSetV2[App ) } - signerFamily, err := getSignerFamilyFromRegistry(registry, selectors) + signerFamily, err := getSignerFamilyFromRegistry(selectors) if err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("failed to determine signer address family: %w", err) } @@ -158,7 +158,7 @@ func ApplyVerifierConfig(registry *adapters.Registry) deployment.ChangeSetV2[App return deployment.ChangesetOutput{}, err } - contractAddresses, err := buildVerifierContractConfigs(registry, e, selectors, cfg.CommitteeQualifier, cfg.DefaultExecutorQualifier) + contractAddresses, err := buildVerifierContractConfigs(e, selectors, cfg.CommitteeQualifier, cfg.DefaultExecutorQualifier) if err != nil { return deployment.ChangesetOutput{}, err } @@ -230,7 +230,7 @@ func ApplyVerifierConfig(registry *adapters.Registry) deployment.ChangeSetV2[App // getSignerFamilyFromRegistry returns the signing key family implied by the selected // chains. If a verifier adapter is registered for a selector, it must agree with the // family derived from that selector. -func getSignerFamilyFromRegistry(registry *adapters.Registry, selectors []uint64) (string, error) { +func getSignerFamilyFromRegistry(selectors []uint64) (string, error) { if len(selectors) == 0 { return "", fmt.Errorf("at least one committee chain selector is required") } @@ -250,11 +250,11 @@ func getSignerFamilyFromRegistry(registry *adapters.Registry, selectors []uint64 ) } - a, err := registry.GetByChain(sel) - if err != nil || a.Verifier == nil { + verifier, err := adapters.GetVerifierRegistry().Get(sel) + if err != nil { continue } - if adapterFamily := a.Verifier.GetSignerAddressFamily(); adapterFamily != signerFamily { + if adapterFamily := verifier.GetSignerAddressFamily(); adapterFamily != signerFamily { return "", fmt.Errorf( "chain %d: verifier adapter signer family %q does not match chain family %q", sel, adapterFamily, signerFamily, @@ -265,7 +265,6 @@ func getSignerFamilyFromRegistry(registry *adapters.Registry, selectors []uint64 } func buildVerifierContractConfigs( - registry *adapters.Registry, e deployment.Environment, selectors []uint64, committeeQualifier string, @@ -273,14 +272,11 @@ func buildVerifierContractConfigs( ) (map[string]*adapters.VerifierContractAddresses, error) { configs := make(map[string]*adapters.VerifierContractAddresses, len(selectors)) for _, sel := range selectors { - a, err := registry.GetByChain(sel) + verifier, err := adapters.GetVerifierRegistry().Get(sel) if err != nil { - return nil, fmt.Errorf("no adapter for chain %d: %w", sel, err) - } - if a.Verifier == nil { - return nil, fmt.Errorf("no verifier config adapter registered for chain %d", sel) + return nil, fmt.Errorf("no verifier config adapter registered for chain %d: %w", sel, err) } - addrs, err := a.Verifier.ResolveVerifierContractAddresses(e.DataStore, sel, committeeQualifier, executorQualifier) + addrs, err := verifier.ResolveVerifierContractAddresses(e.DataStore, sel, committeeQualifier, executorQualifier) if err != nil { return nil, fmt.Errorf("failed to resolve contract addresses for chain %d: %w", sel, err) } diff --git a/deployment/changesets/apply_verifier_config_test.go b/deployment/changesets/apply_verifier_config_test.go index ead916a63..b2ec20a49 100644 --- a/deployment/changesets/apply_verifier_config_test.go +++ b/deployment/changesets/apply_verifier_config_test.go @@ -13,7 +13,8 @@ import ( ) func TestApplyVerifierConfig_Validation_RequiresCommitteeQualifier(t *testing.T) { - cs := ApplyVerifierConfig(newEVMRegistry(&stubOnchainAdapter{})) + registerEVMOnchain(&stubOnchainAdapter{}) + cs := ApplyVerifierConfig() err := cs.VerifyPreconditions(deployment.Environment{}, ApplyVerifierConfigInput{ DefaultExecutorQualifier: "default-executor", NOPs: []NOPInput{{Alias: "nop1"}}, @@ -23,7 +24,8 @@ func TestApplyVerifierConfig_Validation_RequiresCommitteeQualifier(t *testing.T) } func TestApplyVerifierConfig_Validation_RequiresExecutorQualifier(t *testing.T) { - cs := ApplyVerifierConfig(newEVMRegistry(&stubOnchainAdapter{})) + registerEVMOnchain(&stubOnchainAdapter{}) + cs := ApplyVerifierConfig() err := cs.VerifyPreconditions(deployment.Environment{}, ApplyVerifierConfigInput{ CommitteeQualifier: "default", NOPs: []NOPInput{{Alias: "nop1"}}, @@ -33,7 +35,8 @@ func TestApplyVerifierConfig_Validation_RequiresExecutorQualifier(t *testing.T) } func TestApplyVerifierConfig_Validation_RequiresNOPs(t *testing.T) { - cs := ApplyVerifierConfig(newEVMRegistry(&stubOnchainAdapter{})) + registerEVMOnchain(&stubOnchainAdapter{}) + cs := ApplyVerifierConfig() err := cs.VerifyPreconditions(deployment.Environment{}, ApplyVerifierConfigInput{ CommitteeQualifier: "default", DefaultExecutorQualifier: "default-executor", @@ -44,7 +47,8 @@ func TestApplyVerifierConfig_Validation_RequiresNOPs(t *testing.T) { } func TestApplyVerifierConfig_Validation_RequiresAggregator(t *testing.T) { - cs := ApplyVerifierConfig(newEVMRegistry(&stubOnchainAdapter{})) + registerEVMOnchain(&stubOnchainAdapter{}) + cs := ApplyVerifierConfig() err := cs.VerifyPreconditions(deployment.Environment{}, ApplyVerifierConfigInput{ CommitteeQualifier: "default", DefaultExecutorQualifier: "default-executor", @@ -55,7 +59,8 @@ func TestApplyVerifierConfig_Validation_RequiresAggregator(t *testing.T) { } func TestApplyVerifierConfig_Validation_DuplicateNOPAliasRejected(t *testing.T) { - cs := ApplyVerifierConfig(newEVMRegistry(&stubOnchainAdapter{})) + registerEVMOnchain(&stubOnchainAdapter{}) + cs := ApplyVerifierConfig() err := cs.VerifyPreconditions(deployment.Environment{}, ApplyVerifierConfigInput{ CommitteeQualifier: "default", DefaultExecutorQualifier: "default-executor", @@ -67,7 +72,8 @@ func TestApplyVerifierConfig_Validation_DuplicateNOPAliasRejected(t *testing.T) } func TestApplyVerifierConfig_Validation_QualifierMismatchRejected(t *testing.T) { - cs := ApplyVerifierConfig(newEVMRegistry(&stubOnchainAdapter{})) + registerEVMOnchain(&stubOnchainAdapter{}) + cs := ApplyVerifierConfig() err := cs.VerifyPreconditions(deployment.Environment{}, ApplyVerifierConfigInput{ CommitteeQualifier: "primary", DefaultExecutorQualifier: "default-executor", @@ -83,7 +89,8 @@ func TestApplyVerifierConfig_Validation_QualifierMismatchRejected(t *testing.T) func TestApplyVerifierConfig_Validation_TargetNOPMustExist(t *testing.T) { sel1 := chainsel.TEST_90000001.Selector - cs := ApplyVerifierConfig(newEVMRegistry(&stubOnchainAdapter{})) + registerEVMOnchain(&stubOnchainAdapter{}) + cs := ApplyVerifierConfig() err := cs.VerifyPreconditions(deployment.Environment{}, ApplyVerifierConfigInput{ CommitteeQualifier: "default", DefaultExecutorQualifier: "default-executor", @@ -102,7 +109,8 @@ func TestApplyVerifierConfig_Validation_TargetNOPMustExist(t *testing.T) { func TestApplyVerifierConfig_Validation_ChainReferencesUnknownNOPRejected(t *testing.T) { sel1 := chainsel.TEST_90000001.Selector - cs := ApplyVerifierConfig(newEVMRegistry(&stubOnchainAdapter{})) + registerEVMOnchain(&stubOnchainAdapter{}) + cs := ApplyVerifierConfig() err := cs.VerifyPreconditions(deployment.Environment{}, ApplyVerifierConfigInput{ CommitteeQualifier: "default", DefaultExecutorQualifier: "default-executor", @@ -120,7 +128,8 @@ func TestApplyVerifierConfig_Validation_ChainReferencesUnknownNOPRejected(t *tes func TestApplyVerifierConfig_Validation_ProductionRejectsPyroscope(t *testing.T) { sel1 := chainsel.TEST_90000001.Selector - cs := ApplyVerifierConfig(newEVMRegistry(&stubOnchainAdapter{})) + registerEVMOnchain(&stubOnchainAdapter{}) + cs := ApplyVerifierConfig() err := cs.VerifyPreconditions(deployment.Environment{Name: "mainnet"}, ApplyVerifierConfigInput{ CommitteeQualifier: "default", DefaultExecutorQualifier: "default-executor", @@ -139,7 +148,8 @@ func TestApplyVerifierConfig_Validation_ProductionRejectsPyroscope(t *testing.T) func TestApplyVerifierConfig_Validation_AcceptsValidImperativeInput(t *testing.T) { sel1 := chainsel.TEST_90000001.Selector - cs := ApplyVerifierConfig(newEVMRegistry(&stubOnchainAdapter{})) + registerEVMOnchain(&stubOnchainAdapter{}) + cs := ApplyVerifierConfig() err := cs.VerifyPreconditions(deployment.Environment{}, ApplyVerifierConfigInput{ CommitteeQualifier: "default", DefaultExecutorQualifier: "default-executor", diff --git a/deployment/changesets/decrease_threshold.go b/deployment/changesets/decrease_threshold.go index 93a1c3575..cdbed5815 100644 --- a/deployment/changesets/decrease_threshold.go +++ b/deployment/changesets/decrease_threshold.go @@ -34,7 +34,7 @@ type DecreaseThresholdInput struct { // // In deployer-key mode the transaction is submitted directly inside Apply. // MCMS-mode support is deferred to Phase 0 (CLD post-proposal hook prerequisite). -func DecreaseThreshold(registry *adapters.Registry) deployment.ChangeSetV2[DecreaseThresholdInput] { +func DecreaseThreshold() deployment.ChangeSetV2[DecreaseThresholdInput] { validate := func(e deployment.Environment, cfg DecreaseThresholdInput) error { if cfg.CommitteeQualifier == "" { return fmt.Errorf("committee qualifier is required") @@ -46,15 +46,11 @@ func DecreaseThreshold(registry *adapters.Registry) deployment.ChangeSetV2[Decre return fmt.Errorf("new threshold must be greater than zero") } for _, sel := range cfg.ChainSelectors { - a, err := registry.GetByChain(sel) - if err != nil { + if _, err := adapters.GetCommitteeVerifierOnchainRegistry().Get(sel); err != nil { return fmt.Errorf("chain %d: %w", sel, err) } - if a.CommitteeVerifierOnchain == nil { - return fmt.Errorf("chain %d: no CommitteeVerifierOnchain adapter registered", sel) - } - if a.Aggregator == nil { - return fmt.Errorf("chain %d: no Aggregator adapter registered", sel) + if _, err := adapters.GetAggregatorRegistry().Get(sel); err != nil { + return fmt.Errorf("chain %d: %w", sel, err) } } return nil @@ -63,7 +59,7 @@ func DecreaseThreshold(registry *adapters.Registry) deployment.ChangeSetV2[Decre apply := func(e deployment.Environment, cfg DecreaseThresholdInput) (deployment.ChangesetOutput, error) { ctx := context.Background() - committeeStates, err := scanCommitteeStatesForChains(ctx, e, registry, cfg.CommitteeQualifier, cfg.ChainSelectors) + committeeStates, err := scanCommitteeStatesForChains(ctx, e, cfg.CommitteeQualifier, cfg.ChainSelectors) if err != nil { return deployment.ChangesetOutput{}, err } @@ -78,8 +74,11 @@ func DecreaseThreshold(registry *adapters.Registry) deployment.ChangeSetV2[Decre return deployment.ChangesetOutput{}, fmt.Errorf("chain %d: failed to build signature config change: %w", sel, err) } - a, _ := registry.GetByChain(sel) - if err := a.CommitteeVerifierOnchain.ApplySignatureConfigs(ctx, e, sel, cfg.CommitteeQualifier, change); err != nil { + onchain, err := adapters.GetCommitteeVerifierOnchainRegistry().Get(sel) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("chain %d: %w", sel, err) + } + if err := onchain.ApplySignatureConfigs(ctx, e, sel, cfg.CommitteeQualifier, change); err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("chain %d: ApplySignatureConfigs failed: %w", sel, err) } } @@ -111,7 +110,7 @@ type DecreaseThresholdOffchainInput struct { // It asserts that the onchain threshold has already been lowered to ExpectedThreshold, // then regenerates the aggregator config to match. Triggered by the CLD post-proposal // hook after timelock execution. -func DecreaseThresholdOffchain(registry *adapters.Registry) deployment.ChangeSetV2[DecreaseThresholdOffchainInput] { +func DecreaseThresholdOffchain() deployment.ChangeSetV2[DecreaseThresholdOffchainInput] { validate := func(e deployment.Environment, cfg DecreaseThresholdOffchainInput) error { if cfg.CommitteeQualifier == "" { return fmt.Errorf("committee qualifier is required") @@ -129,7 +128,7 @@ func DecreaseThresholdOffchain(registry *adapters.Registry) deployment.ChangeSet // Safety backstop: assert the onchain threshold already matches ExpectedThreshold // on every chain. Catches hook misfires and out-of-order manual invocations. ctx := context.Background() - committeeStates, err := scanCommitteeStatesForChains(ctx, e, registry, cfg.CommitteeQualifier, cfg.ChainSelectors) + committeeStates, err := scanCommitteeStatesForChains(ctx, e, cfg.CommitteeQualifier, cfg.ChainSelectors) if err != nil { return err } @@ -147,7 +146,7 @@ func DecreaseThresholdOffchain(registry *adapters.Registry) deployment.ChangeSet } apply := func(e deployment.Environment, cfg DecreaseThresholdOffchainInput) (deployment.ChangesetOutput, error) { - committee, err := buildAggregatorCommittee(e, registry, cfg.CommitteeQualifier, cfg.ChainSelectors, &cfg.ExpectedThreshold) + committee, err := buildAggregatorCommittee(e, cfg.CommitteeQualifier, cfg.ChainSelectors, &cfg.ExpectedThreshold) if err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("failed to build aggregator config: %w", err) } diff --git a/deployment/changesets/deploy_committee_verifier.go b/deployment/changesets/deploy_committee_verifier.go index 1714b547d..465772050 100644 --- a/deployment/changesets/deploy_committee_verifier.go +++ b/deployment/changesets/deploy_committee_verifier.go @@ -7,7 +7,6 @@ import ( "github.com/smartcontractkit/chainlink-deployments-framework/datastore" "github.com/smartcontractkit/chainlink-deployments-framework/deployment" - "github.com/smartcontractkit/chainlink-deployments-framework/operations" "github.com/smartcontractkit/chainlink-ccv/deployment/adapters" ) @@ -51,137 +50,76 @@ func (c DeployCommitteeVerifierInput) resolveChainCfg(sel uint64) DeployCommitte // DeployCommitteeVerifier is the chain-agnostic changeset for deploying and // idempotently configuring CommitteeVerifier contracts (and their resolvers) -// across many chains. It is the granular, CCV-modular alternative to the -// full-stack DeployChainContracts in chainlink-ccip: a CCV can be deployed -// and configured separately from the rest of the protocol. +// across many chains. // -// Per-chain work is dispatched through registry.ChainAdapters.CommitteeVerifierDeploy, -// which proxies to the chain-family-specific deploy (e.g. on EVM, the existing -// sequences.DeployCommitteeVerifier). Adapters are expected to be idempotent: -// re-running on a chain where the verifier already exists is a no-op (or -// reconciles any drifted dynamic config). +// Per-chain work is dispatched through the CommitteeVerifierDeployAdapter. +// Adapters are expected to be idempotent: re-running on a chain where the +// verifier already exists is a no-op (or reconciles any drifted dynamic config). // // Deployed contracts remain owned by the deployer key. Wiring up MCMS-based -// ownership transfer is tracked as a follow-up (CCIP-11432) so the deploy + -// transfer can be composed once the CLD pieces land. -func DeployCommitteeVerifier(registry *adapters.Registry) deployment.ChangeSetV2[DeployCommitteeVerifierInput] { +// ownership transfer is tracked as a follow-up (CCIP-11432). +func DeployCommitteeVerifier() deployment.ChangeSetV2[DeployCommitteeVerifierInput] { validate := func(e deployment.Environment, cfg DeployCommitteeVerifierInput) error { - if len(cfg.ChainSelectors) == 0 { - return errors.New("at least one chain selector is required") - } - if len(cfg.Committees) == 0 { - return errors.New("at least one committee is required") - } - - envSelectors := e.BlockChains.ListChainSelectors() - seen := make(map[uint64]bool, len(cfg.ChainSelectors)) - for _, sel := range cfg.ChainSelectors { - if seen[sel] { - return fmt.Errorf("duplicate chain selector %d in ChainSelectors", sel) - } - seen[sel] = true - if !slices.Contains(envSelectors, sel) { - return fmt.Errorf("chain selector %d is not available in environment", sel) - } - - if cfg.resolveChainCfg(sel).DeployerContract == "" { - return fmt.Errorf("DeployerContract is required for chain %d", sel) - } - - chainAdapter, err := registry.GetByChain(sel) - if err != nil { - return fmt.Errorf("chain %d: %w", sel, err) - } - if chainAdapter.CommitteeVerifierDeploy == nil { - return fmt.Errorf("chain %d: no CommitteeVerifierDeploy adapter registered", sel) - } - } - for sel := range cfg.ChainCfgs { - if !slices.Contains(cfg.ChainSelectors, sel) { - return fmt.Errorf("ChainCfgs contains selector %d which is not in ChainSelectors", sel) - } - } - - seenQualifier := make(map[string]bool, len(cfg.Committees)) - for _, committee := range cfg.Committees { - if committee.Qualifier == "" { - return errors.New("committee qualifier is required") - } - if seenQualifier[committee.Qualifier] { - return fmt.Errorf("duplicate committee qualifier %q in Committees", committee.Qualifier) - } - seenQualifier[committee.Qualifier] = true - if committee.Version == nil { - return fmt.Errorf("committee %q: Version is required", committee.Qualifier) - } - if committee.FeeAggregator == "" { - return fmt.Errorf("committee %q: FeeAggregator is required", committee.Qualifier) - } - } - - return nil + return validateCommitteeVerifierDeploy(e, cfg) } apply := func(e deployment.Environment, cfg DeployCommitteeVerifierInput) (deployment.ChangesetOutput, error) { ds := datastore.NewMemoryDataStore() - var allReports []operations.Report[any, any] - - for _, sel := range cfg.ChainSelectors { - chainAdapter, err := registry.GetByChain(sel) - if err != nil { - return deployment.ChangesetOutput{Reports: allReports}, - fmt.Errorf("chain %d: %w", sel, err) - } - - existingAddresses := e.DataStore.Addresses().Filter( - datastore.AddressRefByChainSelector(sel), - ) - - for _, committee := range cfg.Committees { - input := adapters.DeployCommitteeVerifierInput{ - ChainSelector: sel, - DeployerContract: cfg.resolveChainCfg(sel).DeployerContract, - ExistingAddresses: existingAddresses, - Params: committee, - } + reports, err := deployCommitteeVerifiersOnChains(e, cfg, ds) + return deployment.ChangesetOutput{Reports: reports, DataStore: ds}, err + } - e.Logger.Infow( - "Deploying CommitteeVerifier", - "chain", sel, - "committee", committee.Qualifier, - ) + return deployment.CreateChangeSet(apply, validate) +} - report, err := operations.ExecuteSequence( - e.OperationsBundle, - chainAdapter.CommitteeVerifierDeploy.DeployCommitteeVerifier(), - e.BlockChains, - input, - ) - allReports = append(allReports, report.ExecutionReports...) - if err != nil { - return deployment.ChangesetOutput{Reports: allReports}, - fmt.Errorf("chain %d committee %q: deploy failed: %w", sel, committee.Qualifier, err) - } +// validateCommitteeVerifierDeploy validates the input for committee verifier deployment. +func validateCommitteeVerifierDeploy(e deployment.Environment, cfg DeployCommitteeVerifierInput) error { + if len(cfg.ChainSelectors) == 0 { + return errors.New("at least one chain selector is required") + } + if len(cfg.Committees) == 0 { + return errors.New("at least one committee is required") + } - for _, ref := range report.Output.Addresses { - if addErr := ds.Addresses().Add(ref); addErr != nil && - !errors.Is(addErr, datastore.ErrAddressRefExists) { - return deployment.ChangesetOutput{Reports: allReports, DataStore: ds}, - fmt.Errorf("chain %d committee %q: failed to add %s %s at %s to datastore: %w", - sel, committee.Qualifier, ref.Type, ref.Version, ref.Address, addErr) - } - // Mirror into existingAddresses for subsequent committee deploys on - // the same chain so addresses just-deployed by this loop are visible. - existingAddresses = append(existingAddresses, ref) - } - } + envSelectors := e.BlockChains.ListChainSelectors() + seen := make(map[uint64]bool, len(cfg.ChainSelectors)) + for _, sel := range cfg.ChainSelectors { + if seen[sel] { + return fmt.Errorf("duplicate chain selector %d in ChainSelectors", sel) + } + seen[sel] = true + if !slices.Contains(envSelectors, sel) { + return fmt.Errorf("chain selector %d is not available in environment", sel) + } + if cfg.resolveChainCfg(sel).DeployerContract == "" { + return fmt.Errorf("DeployerContract is required for chain %d", sel) + } + if _, err := adapters.GetCommitteeVerifierDeployRegistry().Get(sel); err != nil { + return fmt.Errorf("chain %d: %w", sel, err) + } + } + for sel := range cfg.ChainCfgs { + if !slices.Contains(cfg.ChainSelectors, sel) { + return fmt.Errorf("ChainCfgs contains selector %d which is not in ChainSelectors", sel) } + } - return deployment.ChangesetOutput{ - Reports: allReports, - DataStore: ds, - }, nil + seenQualifier := make(map[string]bool, len(cfg.Committees)) + for _, committee := range cfg.Committees { + if committee.Qualifier == "" { + return errors.New("committee qualifier is required") + } + if seenQualifier[committee.Qualifier] { + return fmt.Errorf("duplicate committee qualifier %q in Committees", committee.Qualifier) + } + seenQualifier[committee.Qualifier] = true + if committee.Version == nil { + return fmt.Errorf("committee %q: Version is required", committee.Qualifier) + } + if committee.FeeAggregator == "" { + return fmt.Errorf("committee %q: FeeAggregator is required", committee.Qualifier) + } } - return deployment.CreateChangeSet(apply, validate) + return nil } diff --git a/deployment/changesets/deploy_committee_verifier_test.go b/deployment/changesets/deploy_committee_verifier_test.go index 7f76e7de7..49f5b0851 100644 --- a/deployment/changesets/deploy_committee_verifier_test.go +++ b/deployment/changesets/deploy_committee_verifier_test.go @@ -35,16 +35,8 @@ func (s *stubDeployAdapter) DeployCommitteeVerifier() *operations.Sequence[adapt return stubDeploySequence } -// newDeployTestRegistry registers the stub deploy adapter for the EVM family -// in the singleton registry. Other test files may already have registered -// fields for the same family; Register only overwrites non-nil fields, so -// adding CommitteeVerifierDeploy here does not disturb them. -func newDeployTestRegistry() *adapters.Registry { - r := adapters.GetRegistry() - r.Register(chainsel.FamilyEVM, adapters.ChainAdapters{ - CommitteeVerifierDeploy: &stubDeployAdapter{}, - }) - return r +func registerDeployAdapter() { + adapters.GetCommitteeVerifierDeployRegistry().Register(chainsel.FamilyEVM, &stubDeployAdapter{}) } func newDeployTestEnv(selectors []uint64) deployment.Environment { @@ -73,7 +65,8 @@ func validInput(selectors []uint64) DeployCommitteeVerifierInput { } func TestDeployCommitteeVerifier_Validation_NoChainSelectors(t *testing.T) { - cs := DeployCommitteeVerifier(newDeployTestRegistry()) + registerDeployAdapter() + cs := DeployCommitteeVerifier() err := cs.VerifyPreconditions(newDeployTestEnv(nil), DeployCommitteeVerifierInput{ Committees: []adapters.CommitteeVerifierDeployParams{validCommittee()}, }) @@ -82,7 +75,8 @@ func TestDeployCommitteeVerifier_Validation_NoChainSelectors(t *testing.T) { func TestDeployCommitteeVerifier_Validation_NoCommittees(t *testing.T) { sel := chainsel.TEST_90000001.Selector - cs := DeployCommitteeVerifier(newDeployTestRegistry()) + registerDeployAdapter() + cs := DeployCommitteeVerifier() err := cs.VerifyPreconditions(newDeployTestEnv([]uint64{sel}), DeployCommitteeVerifierInput{ ChainSelectors: []uint64{sel}, DefaultCfg: DeployCommitteeVerifierPerChainCfg{ @@ -94,7 +88,8 @@ func TestDeployCommitteeVerifier_Validation_NoCommittees(t *testing.T) { func TestDeployCommitteeVerifier_Validation_DuplicateChainSelectors(t *testing.T) { sel := chainsel.TEST_90000001.Selector - cs := DeployCommitteeVerifier(newDeployTestRegistry()) + registerDeployAdapter() + cs := DeployCommitteeVerifier() err := cs.VerifyPreconditions(newDeployTestEnv([]uint64{sel}), validInput([]uint64{sel, sel})) require.ErrorContains(t, err, "duplicate chain selector") } @@ -102,14 +97,16 @@ func TestDeployCommitteeVerifier_Validation_DuplicateChainSelectors(t *testing.T func TestDeployCommitteeVerifier_Validation_ChainNotInEnvironment(t *testing.T) { envSel := chainsel.TEST_90000001.Selector otherSel := chainsel.TEST_90000002.Selector - cs := DeployCommitteeVerifier(newDeployTestRegistry()) + registerDeployAdapter() + cs := DeployCommitteeVerifier() err := cs.VerifyPreconditions(newDeployTestEnv([]uint64{envSel}), validInput([]uint64{otherSel})) require.ErrorContains(t, err, "is not available in environment") } func TestDeployCommitteeVerifier_Validation_MissingDeployerContract(t *testing.T) { sel := chainsel.TEST_90000001.Selector - cs := DeployCommitteeVerifier(newDeployTestRegistry()) + registerDeployAdapter() + cs := DeployCommitteeVerifier() input := validInput([]uint64{sel}) input.DefaultCfg.DeployerContract = "" err := cs.VerifyPreconditions(newDeployTestEnv([]uint64{sel}), input) @@ -118,7 +115,8 @@ func TestDeployCommitteeVerifier_Validation_MissingDeployerContract(t *testing.T func TestDeployCommitteeVerifier_Validation_DeployerContractFromPerChainOverride(t *testing.T) { sel := chainsel.TEST_90000001.Selector - cs := DeployCommitteeVerifier(newDeployTestRegistry()) + registerDeployAdapter() + cs := DeployCommitteeVerifier() input := validInput([]uint64{sel}) // Default is empty — would normally fail — but per-chain override supplies the value. input.DefaultCfg.DeployerContract = "" @@ -131,7 +129,8 @@ func TestDeployCommitteeVerifier_Validation_DeployerContractFromPerChainOverride func TestDeployCommitteeVerifier_Validation_ChainCfgsSelectorNotInChainSelectors(t *testing.T) { sel := chainsel.TEST_90000001.Selector otherSel := chainsel.TEST_90000002.Selector - cs := DeployCommitteeVerifier(newDeployTestRegistry()) + registerDeployAdapter() + cs := DeployCommitteeVerifier() input := validInput([]uint64{sel}) input.ChainCfgs = map[uint64]DeployCommitteeVerifierPerChainCfg{ otherSel: {DeployerContract: "0x0000000000000000000000000000000000000FAC"}, @@ -142,7 +141,8 @@ func TestDeployCommitteeVerifier_Validation_ChainCfgsSelectorNotInChainSelectors func TestDeployCommitteeVerifier_Validation_MissingCommitteeQualifier(t *testing.T) { sel := chainsel.TEST_90000001.Selector - cs := DeployCommitteeVerifier(newDeployTestRegistry()) + registerDeployAdapter() + cs := DeployCommitteeVerifier() input := validInput([]uint64{sel}) input.Committees[0].Qualifier = "" err := cs.VerifyPreconditions(newDeployTestEnv([]uint64{sel}), input) @@ -151,7 +151,8 @@ func TestDeployCommitteeVerifier_Validation_MissingCommitteeQualifier(t *testing func TestDeployCommitteeVerifier_Validation_DuplicateCommitteeQualifier(t *testing.T) { sel := chainsel.TEST_90000001.Selector - cs := DeployCommitteeVerifier(newDeployTestRegistry()) + registerDeployAdapter() + cs := DeployCommitteeVerifier() input := validInput([]uint64{sel}) input.Committees = append(input.Committees, validCommittee()) err := cs.VerifyPreconditions(newDeployTestEnv([]uint64{sel}), input) @@ -160,7 +161,8 @@ func TestDeployCommitteeVerifier_Validation_DuplicateCommitteeQualifier(t *testi func TestDeployCommitteeVerifier_Validation_MissingCommitteeVersion(t *testing.T) { sel := chainsel.TEST_90000001.Selector - cs := DeployCommitteeVerifier(newDeployTestRegistry()) + registerDeployAdapter() + cs := DeployCommitteeVerifier() input := validInput([]uint64{sel}) input.Committees[0].Version = nil err := cs.VerifyPreconditions(newDeployTestEnv([]uint64{sel}), input) @@ -169,7 +171,8 @@ func TestDeployCommitteeVerifier_Validation_MissingCommitteeVersion(t *testing.T func TestDeployCommitteeVerifier_Validation_MissingFeeAggregator(t *testing.T) { sel := chainsel.TEST_90000001.Selector - cs := DeployCommitteeVerifier(newDeployTestRegistry()) + registerDeployAdapter() + cs := DeployCommitteeVerifier() input := validInput([]uint64{sel}) input.Committees[0].FeeAggregator = "" err := cs.VerifyPreconditions(newDeployTestEnv([]uint64{sel}), input) @@ -178,6 +181,7 @@ func TestDeployCommitteeVerifier_Validation_MissingFeeAggregator(t *testing.T) { func TestDeployCommitteeVerifier_Validation_HappyPath(t *testing.T) { sel := chainsel.TEST_90000001.Selector - cs := DeployCommitteeVerifier(newDeployTestRegistry()) + registerDeployAdapter() + cs := DeployCommitteeVerifier() require.NoError(t, cs.VerifyPreconditions(newDeployTestEnv([]uint64{sel}), validInput([]uint64{sel}))) } diff --git a/deployment/changesets/deploy_protocol_contracts.go b/deployment/changesets/deploy_protocol_contracts.go new file mode 100644 index 000000000..77b5e033c --- /dev/null +++ b/deployment/changesets/deploy_protocol_contracts.go @@ -0,0 +1,221 @@ +package changesets + +import ( + "errors" + "fmt" + "slices" + + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + + "github.com/smartcontractkit/chainlink-ccv/deployment/adapters" +) + +// DeployProtocolContractsPerChainCfg carries the per-chain configuration for +// deploying CCIP protocol contracts (RMN, OnRamp, OffRamp, FeeQuoter, Router, +// Executors). Committee verifiers are deployed separately. +type DeployProtocolContractsPerChainCfg struct { + // DeployerContract is the deployer/factory address (e.g. CREATE2Factory on EVM). + DeployerContract string + // DeployTestRouter deploys a TestRouter alongside the production Router. + DeployTestRouter bool + // Executors lists executor instances to deploy. + Executors []adapters.ExecutorDeployParams + // DeployerKeyOwned when true means deployed contracts remain owned by the + // deployer key. When false, ownership is transferred to the RBAC timelock. + DeployerKeyOwned bool + // FamilyExtras carries chain-family-specific deploy parameters. + FamilyExtras map[string]any +} + +// DeployProtocolContractsInput is the imperative input for the +// DeployProtocolContracts changeset. +type DeployProtocolContractsInput struct { + // ChainSelectors are the chains to deploy on. + ChainSelectors []uint64 + // DefaultCfg supplies fallback per-chain values. Used when a chain has no + // entry in ChainCfgs. + DefaultCfg DeployProtocolContractsPerChainCfg + // ChainCfgs overrides DefaultCfg for specific chains. + ChainCfgs map[uint64]DeployProtocolContractsPerChainCfg +} + +func (c DeployProtocolContractsInput) resolveChainCfg(sel uint64) DeployProtocolContractsPerChainCfg { + if override, ok := c.ChainCfgs[sel]; ok { + return override + } + return c.DefaultCfg +} + +// DeployProtocolContracts is a single-entry, onchain-only changeset that +// deploys the core CCIP protocol contracts (RMN, OnRamp, OffRamp, FeeQuoter, +// Router, Executors) on the specified chains. +// +// Committee verifiers are NOT deployed by this changeset — use +// DeployCommitteeVerifier for that, or OnboardChain for both in one pass. +func DeployProtocolContracts() deployment.ChangeSetV2[DeployProtocolContractsInput] { + validate := func(e deployment.Environment, cfg DeployProtocolContractsInput) error { + return validateProtocolContractsDeploy(e, cfg) + } + + apply := func(e deployment.Environment, cfg DeployProtocolContractsInput) (deployment.ChangesetOutput, error) { + ds := datastore.NewMemoryDataStore() + reports, err := deployProtocolContractsOnChains(e, cfg, ds) + return deployment.ChangesetOutput{Reports: reports, DataStore: ds}, err + } + + return deployment.CreateChangeSet(apply, validate) +} + +// validateProtocolContractsDeploy validates the input for protocol contract deployment. +func validateProtocolContractsDeploy(e deployment.Environment, cfg DeployProtocolContractsInput) error { + if len(cfg.ChainSelectors) == 0 { + return errors.New("at least one chain selector is required") + } + + envSelectors := e.BlockChains.ListChainSelectors() + seen := make(map[uint64]bool, len(cfg.ChainSelectors)) + for _, sel := range cfg.ChainSelectors { + if seen[sel] { + return fmt.Errorf("duplicate chain selector %d in ChainSelectors", sel) + } + seen[sel] = true + if !slices.Contains(envSelectors, sel) { + return fmt.Errorf("chain selector %d is not available in environment", sel) + } + if cfg.resolveChainCfg(sel).DeployerContract == "" { + return fmt.Errorf("DeployerContract is required for chain %d", sel) + } + if _, err := adapters.GetProtocolContractsDeployRegistry().Get(sel); err != nil { + return fmt.Errorf("chain %d: %w", sel, err) + } + } + + for sel := range cfg.ChainCfgs { + if !slices.Contains(cfg.ChainSelectors, sel) { + return fmt.Errorf("ChainCfgs contains selector %d which is not in ChainSelectors", sel) + } + } + + return nil +} + +// deployProtocolContractsOnChains deploys protocol contracts on every chain in +// cfg and writes deployed addresses to ds. This is the shared core used by +// both DeployProtocolContracts and OnboardChain. +func deployProtocolContractsOnChains( + e deployment.Environment, + cfg DeployProtocolContractsInput, + ds datastore.MutableDataStore, +) ([]operations.Report[any, any], error) { + var allReports []operations.Report[any, any] + + for _, sel := range cfg.ChainSelectors { + chainCfg := cfg.resolveChainCfg(sel) + + deployer, err := adapters.GetProtocolContractsDeployRegistry().Get(sel) + if err != nil { + return allReports, fmt.Errorf("chain %d: %w", sel, err) + } + + existingAddresses := e.DataStore.Addresses().Filter( + datastore.AddressRefByChainSelector(sel), + ) + + input := adapters.ProtocolContractsDeployInput{ + ChainSelector: sel, + DeployerContract: chainCfg.DeployerContract, + DeployTestRouter: chainCfg.DeployTestRouter, + ExistingAddresses: existingAddresses, + Executors: chainCfg.Executors, + DeployerKeyOwned: chainCfg.DeployerKeyOwned, + FamilyExtras: chainCfg.FamilyExtras, + } + + e.Logger.Infow("Deploying protocol contracts", + "chain", sel, + "deployTestRouter", chainCfg.DeployTestRouter, + "executorCount", len(chainCfg.Executors), + ) + + report, err := operations.ExecuteSequence( + e.OperationsBundle, + deployer.DeployProtocolContracts(), + e.BlockChains, + input, + ) + allReports = append(allReports, report.ExecutionReports...) + if err != nil { + return allReports, fmt.Errorf("chain %d: deploy failed: %w", sel, err) + } + + for _, ref := range report.Output.Addresses { + if addErr := ds.Addresses().Add(ref); addErr != nil && + !errors.Is(addErr, datastore.ErrAddressRefExists) { + return allReports, fmt.Errorf("chain %d: failed to add %s %s at %s to datastore: %w", + sel, ref.Type, ref.Version, ref.Address, addErr) + } + } + } + + return allReports, nil +} + +// deployCommitteeVerifiersOnChains deploys committee verifiers on every chain +// in cfg and writes deployed addresses to ds. This is the shared core used by +// both DeployCommitteeVerifier and OnboardChain. +func deployCommitteeVerifiersOnChains( + e deployment.Environment, + cfg DeployCommitteeVerifierInput, + ds datastore.MutableDataStore, +) ([]operations.Report[any, any], error) { + var allReports []operations.Report[any, any] + + for _, sel := range cfg.ChainSelectors { + deployer, err := adapters.GetCommitteeVerifierDeployRegistry().Get(sel) + if err != nil { + return allReports, fmt.Errorf("chain %d: %w", sel, err) + } + + existingAddresses := e.DataStore.Addresses().Filter( + datastore.AddressRefByChainSelector(sel), + ) + + for _, committee := range cfg.Committees { + input := adapters.DeployCommitteeVerifierInput{ + ChainSelector: sel, + DeployerContract: cfg.resolveChainCfg(sel).DeployerContract, + ExistingAddresses: existingAddresses, + Params: committee, + } + + e.Logger.Infow("Deploying CommitteeVerifier", + "chain", sel, + "committee", committee.Qualifier, + ) + + report, err := operations.ExecuteSequence( + e.OperationsBundle, + deployer.DeployCommitteeVerifier(), + e.BlockChains, + input, + ) + allReports = append(allReports, report.ExecutionReports...) + if err != nil { + return allReports, fmt.Errorf("chain %d committee %q: deploy failed: %w", sel, committee.Qualifier, err) + } + + for _, ref := range report.Output.Addresses { + if addErr := ds.Addresses().Add(ref); addErr != nil && + !errors.Is(addErr, datastore.ErrAddressRefExists) { + return allReports, fmt.Errorf("chain %d committee %q: failed to add %s %s at %s to datastore: %w", + sel, committee.Qualifier, ref.Type, ref.Version, ref.Address, addErr) + } + existingAddresses = append(existingAddresses, ref) + } + } + } + + return allReports, nil +} diff --git a/deployment/changesets/deploy_protocol_contracts_test.go b/deployment/changesets/deploy_protocol_contracts_test.go new file mode 100644 index 000000000..8237925ea --- /dev/null +++ b/deployment/changesets/deploy_protocol_contracts_test.go @@ -0,0 +1,161 @@ +package changesets + +import ( + "testing" + + "github.com/Masterminds/semver/v3" + "github.com/stretchr/testify/require" + + chainsel "github.com/smartcontractkit/chain-selectors" + cldf_chain "github.com/smartcontractkit/chainlink-deployments-framework/chain" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + + "github.com/smartcontractkit/chainlink-ccv/deployment/adapters" +) + +// stubProtocolContractsDeployAdapter implements adapters.ProtocolContractsDeployAdapter +// for validation tests. +type stubProtocolContractsDeployAdapter struct{} + +var _ adapters.ProtocolContractsDeployAdapter = (*stubProtocolContractsDeployAdapter)(nil) + +var stubProtocolContractsDeploySequence = operations.NewSequence( + "stub-deploy-protocol-contracts", + semver.MustParse("1.0.0"), + "stub sequence used only by validation tests", + func(_ operations.Bundle, _ cldf_chain.BlockChains, _ adapters.ProtocolContractsDeployInput) (adapters.ProtocolContractsDeployOutput, error) { + return adapters.ProtocolContractsDeployOutput{}, nil + }, +) + +func (s *stubProtocolContractsDeployAdapter) DeployProtocolContracts() *operations.Sequence[adapters.ProtocolContractsDeployInput, adapters.ProtocolContractsDeployOutput, cldf_chain.BlockChains] { + return stubProtocolContractsDeploySequence +} + +func registerProtocolContractsDeployAdapter() { + adapters.GetProtocolContractsDeployRegistry().Register(chainsel.FamilyEVM, &stubProtocolContractsDeployAdapter{}) +} + +func newProtocolDeployTestEnv(selectors []uint64) deployment.Environment { + return deployment.Environment{ + BlockChains: newTestBlockChains(selectors), + DataStore: datastore.NewMemoryDataStore().Seal(), + } +} + +func validProtocolDeployInput(selectors []uint64) DeployProtocolContractsInput { + return DeployProtocolContractsInput{ + ChainSelectors: selectors, + DefaultCfg: DeployProtocolContractsPerChainCfg{ + DeployerContract: "0x0000000000000000000000000000000000000FAC", + }, + } +} + +func TestDeployProtocolContracts_Validation_NoChainSelectors(t *testing.T) { + registerProtocolContractsDeployAdapter() + cs := DeployProtocolContracts() + err := cs.VerifyPreconditions(newProtocolDeployTestEnv(nil), DeployProtocolContractsInput{ + DefaultCfg: DeployProtocolContractsPerChainCfg{ + DeployerContract: "0x0000000000000000000000000000000000000FAC", + }, + }) + require.ErrorContains(t, err, "at least one chain selector is required") +} + +func TestDeployProtocolContracts_Validation_DuplicateChainSelectors(t *testing.T) { + sel := chainsel.TEST_90000001.Selector + registerProtocolContractsDeployAdapter() + cs := DeployProtocolContracts() + err := cs.VerifyPreconditions(newProtocolDeployTestEnv([]uint64{sel}), validProtocolDeployInput([]uint64{sel, sel})) + require.ErrorContains(t, err, "duplicate chain selector") +} + +func TestDeployProtocolContracts_Validation_ChainNotInEnv(t *testing.T) { + envSel := chainsel.TEST_90000001.Selector + otherSel := chainsel.TEST_90000002.Selector + registerProtocolContractsDeployAdapter() + cs := DeployProtocolContracts() + err := cs.VerifyPreconditions(newProtocolDeployTestEnv([]uint64{envSel}), validProtocolDeployInput([]uint64{otherSel})) + require.ErrorContains(t, err, "is not available in environment") +} + +func TestDeployProtocolContracts_Validation_MissingDeployerContract(t *testing.T) { + sel := chainsel.TEST_90000001.Selector + registerProtocolContractsDeployAdapter() + cs := DeployProtocolContracts() + input := validProtocolDeployInput([]uint64{sel}) + input.DefaultCfg.DeployerContract = "" + err := cs.VerifyPreconditions(newProtocolDeployTestEnv([]uint64{sel}), input) + require.ErrorContains(t, err, "DeployerContract is required") +} + +func TestDeployProtocolContracts_Validation_DeployerContractFromPerChainOverride(t *testing.T) { + sel := chainsel.TEST_90000001.Selector + registerProtocolContractsDeployAdapter() + cs := DeployProtocolContracts() + input := validProtocolDeployInput([]uint64{sel}) + input.DefaultCfg.DeployerContract = "" + input.ChainCfgs = map[uint64]DeployProtocolContractsPerChainCfg{ + sel: {DeployerContract: "0x0000000000000000000000000000000000000FAC"}, + } + require.NoError(t, cs.VerifyPreconditions(newProtocolDeployTestEnv([]uint64{sel}), input)) +} + +func TestDeployProtocolContracts_Validation_ChainCfgsSelectorNotInChainSelectors(t *testing.T) { + sel := chainsel.TEST_90000001.Selector + otherSel := chainsel.TEST_90000002.Selector + registerProtocolContractsDeployAdapter() + cs := DeployProtocolContracts() + input := validProtocolDeployInput([]uint64{sel}) + input.ChainCfgs = map[uint64]DeployProtocolContractsPerChainCfg{ + otherSel: {DeployerContract: "0x0000000000000000000000000000000000000FAC"}, + } + err := cs.VerifyPreconditions(newProtocolDeployTestEnv([]uint64{sel, otherSel}), input) + require.ErrorContains(t, err, "ChainCfgs contains selector") +} + +func TestDeployProtocolContracts_Validation_HappyPath(t *testing.T) { + sel := chainsel.TEST_90000001.Selector + registerProtocolContractsDeployAdapter() + cs := DeployProtocolContracts() + require.NoError(t, cs.VerifyPreconditions(newProtocolDeployTestEnv([]uint64{sel}), validProtocolDeployInput([]uint64{sel}))) +} + +func TestDeployProtocolContracts_Validation_MultipleChains(t *testing.T) { + sel1 := chainsel.TEST_90000001.Selector + sel2 := chainsel.TEST_90000002.Selector + registerProtocolContractsDeployAdapter() + cs := DeployProtocolContracts() + require.NoError(t, cs.VerifyPreconditions( + newProtocolDeployTestEnv([]uint64{sel1, sel2}), + validProtocolDeployInput([]uint64{sel1, sel2}), + )) +} + +func TestDeployProtocolContracts_ResolveChainCfg_Default(t *testing.T) { + input := DeployProtocolContractsInput{ + DefaultCfg: DeployProtocolContractsPerChainCfg{ + DeployerContract: "0xDEFAULT", + DeployTestRouter: true, + }, + } + cfg := input.resolveChainCfg(1) + require.Equal(t, "0xDEFAULT", cfg.DeployerContract) + require.True(t, cfg.DeployTestRouter) +} + +func TestDeployProtocolContracts_ResolveChainCfg_Override(t *testing.T) { + input := DeployProtocolContractsInput{ + DefaultCfg: DeployProtocolContractsPerChainCfg{ + DeployerContract: "0xDEFAULT", + }, + ChainCfgs: map[uint64]DeployProtocolContractsPerChainCfg{ + 42: {DeployerContract: "0xOVERRIDE"}, + }, + } + cfg := input.resolveChainCfg(42) + require.Equal(t, "0xOVERRIDE", cfg.DeployerContract) +} diff --git a/deployment/changesets/generate_aggregator_config.go b/deployment/changesets/generate_aggregator_config.go index cea0140dc..43609d8bb 100644 --- a/deployment/changesets/generate_aggregator_config.go +++ b/deployment/changesets/generate_aggregator_config.go @@ -42,7 +42,7 @@ type GenerateAggregatorConfigInput struct { // The input is imperative — callers pass the chain selectors directly, with no // *EnvironmentTopology. For coupled-committee products that need to publish the // post-change threshold ahead of the onchain mutation, set ThresholdOverride. -func GenerateAggregatorConfig(registry *adapters.Registry) deployment.ChangeSetV2[GenerateAggregatorConfigInput] { +func GenerateAggregatorConfig() deployment.ChangeSetV2[GenerateAggregatorConfigInput] { validate := func(e deployment.Environment, cfg GenerateAggregatorConfigInput) error { if cfg.ServiceIdentifier == "" { return fmt.Errorf("service identifier is required") @@ -70,7 +70,7 @@ func GenerateAggregatorConfig(registry *adapters.Registry) deployment.ChangeSetV } apply := func(e deployment.Environment, cfg GenerateAggregatorConfigInput) (deployment.ChangesetOutput, error) { - committee, err := buildAggregatorCommittee(e, registry, cfg.CommitteeQualifier, cfg.ChainSelectors, cfg.ThresholdOverride) + committee, err := buildAggregatorCommittee(e, cfg.CommitteeQualifier, cfg.ChainSelectors, cfg.ThresholdOverride) if err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("failed to build aggregator config: %w", err) } @@ -94,7 +94,6 @@ func GenerateAggregatorConfig(registry *adapters.Registry) deployment.ChangeSetV func buildAggregatorCommittee( e deployment.Environment, - registry *adapters.Registry, committeeQualifier string, chainSelectors []uint64, thresholdOverride *uint8, @@ -108,18 +107,15 @@ func buildAggregatorCommittee( seen := make(map[chainQualifier]bool) allCommittees := make(map[string][]*adapters.CommitteeState) for _, sel := range chainSelectors { - a, err := registry.GetByChain(sel) - if err != nil { - return nil, err - } - if a.Aggregator == nil { - return nil, fmt.Errorf("no aggregator config adapter registered for chain %d", sel) + if _, err := adapters.GetAggregatorRegistry().Get(sel); err != nil { + return nil, fmt.Errorf("no aggregator config adapter registered for chain %d: %w", sel, err) } - if a.CommitteeVerifierOnchain == nil { - return nil, fmt.Errorf("no CommitteeVerifierOnchain adapter registered for chain %d", sel) + onchain, err := adapters.GetCommitteeVerifierOnchainRegistry().Get(sel) + if err != nil { + return nil, fmt.Errorf("no CommitteeVerifierOnchain adapter registered for chain %d: %w", sel, err) } - states, err := a.CommitteeVerifierOnchain.ScanCommitteeStates(ctx, e, sel) + states, err := onchain.ScanCommitteeStates(ctx, e, sel) if err != nil { return nil, fmt.Errorf("failed to scan committee states on chain %d: %w", sel, err) } @@ -141,12 +137,12 @@ func buildAggregatorCommittee( return nil, fmt.Errorf("committee %q not found in deployed verifier state", committeeQualifier) } - quorumConfigs, err := buildQuorumConfigs(e.DataStore, registry, committeeStates, committeeQualifier, chainSelectors, thresholdOverride) + quorumConfigs, err := buildQuorumConfigs(e.DataStore, committeeStates, committeeQualifier, chainSelectors, thresholdOverride) if err != nil { return nil, fmt.Errorf("failed to build quorum configs: %w", err) } - destVerifiers, err := buildDestinationVerifiers(e.DataStore, registry, committeeQualifier, chainSelectors) + destVerifiers, err := buildDestinationVerifiers(e.DataStore, committeeQualifier, chainSelectors) if err != nil { return nil, fmt.Errorf("failed to build destination verifiers: %w", err) } @@ -159,7 +155,6 @@ func buildAggregatorCommittee( func buildQuorumConfigs( ds datastore.DataStore, - registry *adapters.Registry, committeeStates []*adapters.CommitteeState, committeeQualifier string, chainSelectors []uint64, @@ -190,15 +185,12 @@ func buildQuorumConfigs( continue } - a, err := registry.GetByChain(sigConfig.SourceChainSelector) + agg, err := adapters.GetAggregatorRegistry().Get(sigConfig.SourceChainSelector) if err != nil { - return nil, err - } - if a.Aggregator == nil { - return nil, fmt.Errorf("no aggregator config adapter registered for chain %d", sigConfig.SourceChainSelector) + return nil, fmt.Errorf("no aggregator config adapter registered for chain %d: %w", sigConfig.SourceChainSelector, err) } - sourceVerifierAddr, err := a.Aggregator.ResolveSourceVerifierAddress(ds, sigConfig.SourceChainSelector, committeeQualifier) + sourceVerifierAddr, err := agg.ResolveSourceVerifierAddress(ds, sigConfig.SourceChainSelector, committeeQualifier) if err != nil { return nil, fmt.Errorf("failed to resolve source verifier for chain %d: %w", sigConfig.SourceChainSelector, err) } @@ -258,22 +250,18 @@ func validateSignatureConfigConsistency( func buildDestinationVerifiers( ds datastore.DataStore, - registry *adapters.Registry, committeeQualifier string, destChainSelectors []uint64, ) (map[string]string, error) { destVerifiers := make(map[string]string, len(destChainSelectors)) for _, chainSelector := range destChainSelectors { - a, err := registry.GetByChain(chainSelector) + agg, err := adapters.GetAggregatorRegistry().Get(chainSelector) if err != nil { - return nil, err - } - if a.Aggregator == nil { - return nil, fmt.Errorf("no aggregator config adapter registered for chain %d", chainSelector) + return nil, fmt.Errorf("no aggregator config adapter registered for chain %d: %w", chainSelector, err) } - addr, err := a.Aggregator.ResolveDestinationVerifierAddress(ds, chainSelector, committeeQualifier) + addr, err := agg.ResolveDestinationVerifierAddress(ds, chainSelector, committeeQualifier) if err != nil { return nil, fmt.Errorf("failed to resolve destination verifier for chain %d: %w", chainSelector, err) } diff --git a/deployment/changesets/generate_indexer_config.go b/deployment/changesets/generate_indexer_config.go index 0907c0b93..643ad2a4b 100644 --- a/deployment/changesets/generate_indexer_config.go +++ b/deployment/changesets/generate_indexer_config.go @@ -20,7 +20,7 @@ type GenerateIndexerConfigInput struct { LombardVerifierNameToQualifier map[string]string } -func GenerateIndexerConfig(registry *adapters.Registry) deployment.ChangeSetV2[GenerateIndexerConfigInput] { +func GenerateIndexerConfig() deployment.ChangeSetV2[GenerateIndexerConfigInput] { validate := func(e deployment.Environment, cfg GenerateIndexerConfigInput) error { if cfg.ServiceIdentifier == "" { return fmt.Errorf("service identifier is required") @@ -34,7 +34,7 @@ func GenerateIndexerConfig(registry *adapters.Registry) deployment.ChangeSetV2[G } apply := func(e deployment.Environment, cfg GenerateIndexerConfigInput) (deployment.ChangesetOutput, error) { - verifierMap, err := buildIndexerVerifierMap(e.DataStore, registry, e.BlockChains.ListChainSelectors(), cfg) + verifierMap, err := buildIndexerVerifierMap(e.DataStore, e.BlockChains.ListChainSelectors(), cfg) if err != nil { return deployment.ChangesetOutput{}, err } @@ -62,7 +62,6 @@ func GenerateIndexerConfig(registry *adapters.Registry) deployment.ChangeSetV2[G func buildIndexerVerifierMap( ds datastore.DataStore, - registry *adapters.Registry, selectors []uint64, cfg GenerateIndexerConfigInput, ) (map[string][]string, error) { @@ -79,7 +78,7 @@ func buildIndexerVerifierMap( for _, km := range kindMappings { for name, qualifier := range km.nameToQualifier { - addresses, err := collectVerifierAddresses(ds, registry, selectors, qualifier, km.kind) + addresses, err := collectVerifierAddresses(ds, selectors, qualifier, km.kind) if err != nil { return nil, fmt.Errorf("failed to resolve addresses for verifier %q (qualifier %q): %w", name, qualifier, err) } @@ -92,12 +91,11 @@ func buildIndexerVerifierMap( func collectVerifierAddresses( ds datastore.DataStore, - registry *adapters.Registry, selectors []uint64, qualifier string, kind adapters.VerifierKind, ) ([]string, error) { - if !registry.HasAdapters() { + if adapters.GetIndexerRegistry().IsEmpty() { return nil, fmt.Errorf("no indexer config adapter registered") } @@ -105,15 +103,12 @@ func collectVerifierAddresses( var addresses []string for _, sel := range selectors { - a, err := registry.GetByChain(sel) + indexer, err := adapters.GetIndexerRegistry().Get(sel) if err != nil { - return nil, err - } - if a.Indexer == nil { - return nil, fmt.Errorf("no indexer config adapter registered for chain %d", sel) + return nil, fmt.Errorf("no indexer config adapter registered for chain %d: %w", sel, err) } - addrs, err := a.Indexer.ResolveVerifierAddresses(ds, sel, qualifier, kind) + addrs, err := indexer.ResolveVerifierAddresses(ds, sel, qualifier, kind) if err != nil { var missingErr *adapters.MissingIndexerVerifierAddressesError if errors.As(err, &missingErr) { diff --git a/deployment/changesets/generate_token_verifier_config.go b/deployment/changesets/generate_token_verifier_config.go index 8c7727c67..b684cf9a1 100644 --- a/deployment/changesets/generate_token_verifier_config.go +++ b/deployment/changesets/generate_token_verifier_config.go @@ -72,7 +72,7 @@ type GenerateTokenVerifierConfigInput struct { CCTP CCTPConfigInput } -func GenerateTokenVerifierConfig(registry *adapters.Registry) deployment.ChangeSetV2[GenerateTokenVerifierConfigInput] { +func GenerateTokenVerifierConfig() deployment.ChangeSetV2[GenerateTokenVerifierConfigInput] { validate := func(e deployment.Environment, cfg GenerateTokenVerifierConfigInput) error { if cfg.ServiceIdentifier == "" { return fmt.Errorf("service identifier is required") @@ -103,15 +103,12 @@ func GenerateTokenVerifierConfig(registry *adapters.Registry) deployment.ChangeS lombardVerifierResolverAddresses := make(map[string]string) for _, sel := range selectors { - a, err := registry.GetByChain(sel) + tv, err := adapters.GetTokenVerifierRegistry().Get(sel) if err != nil { - return deployment.ChangesetOutput{}, err - } - if a.TokenVerifier == nil { - return deployment.ChangesetOutput{}, fmt.Errorf("no token verifier config adapter registered for chain %d", sel) + return deployment.ChangesetOutput{}, fmt.Errorf("no token verifier config adapter registered for chain %d: %w", sel, err) } - addrs, err := a.TokenVerifier.ResolveTokenVerifierAddresses( + addrs, err := tv.ResolveTokenVerifierAddresses( e.DataStore, sel, cctpCfg.Qualifier, lombardCfg.Qualifier, ) if err != nil { diff --git a/deployment/changesets/increase_threshold.go b/deployment/changesets/increase_threshold.go index 2220b5a28..110a0ae7d 100644 --- a/deployment/changesets/increase_threshold.go +++ b/deployment/changesets/increase_threshold.go @@ -49,7 +49,7 @@ type IncreaseThresholdInput struct { // until the onchain change catches up. Submitting the onchain change first would cause // verifiers to accept bundles with fewer signers than the new offchain config requires, // producing under-signed messages from the aggregator's perspective. -func IncreaseThresholdOffchain(registry *adapters.Registry) deployment.ChangeSetV2[IncreaseThresholdOffchainInput] { +func IncreaseThresholdOffchain() deployment.ChangeSetV2[IncreaseThresholdOffchainInput] { validate := func(e deployment.Environment, cfg IncreaseThresholdOffchainInput) error { if cfg.CommitteeQualifier == "" { return fmt.Errorf("committee qualifier is required") @@ -64,20 +64,16 @@ func IncreaseThresholdOffchain(registry *adapters.Registry) deployment.ChangeSet return fmt.Errorf("at least one service identifier is required") } for _, sel := range cfg.ChainSelectors { - a, err := registry.GetByChain(sel) - if err != nil { + if _, err := adapters.GetCommitteeVerifierOnchainRegistry().Get(sel); err != nil { return fmt.Errorf("chain %d: %w", sel, err) } - if a.CommitteeVerifierOnchain == nil { - return fmt.Errorf("chain %d: no CommitteeVerifierOnchain adapter registered", sel) - } - if a.Aggregator == nil { - return fmt.Errorf("chain %d: no Aggregator adapter registered", sel) + if _, err := adapters.GetAggregatorRegistry().Get(sel); err != nil { + return fmt.Errorf("chain %d: %w", sel, err) } } ctx := context.Background() - committeeStates, err := scanCommitteeStatesForChains(ctx, e, registry, cfg.CommitteeQualifier, cfg.ChainSelectors) + committeeStates, err := scanCommitteeStatesForChains(ctx, e, cfg.CommitteeQualifier, cfg.ChainSelectors) if err != nil { return err } @@ -85,7 +81,7 @@ func IncreaseThresholdOffchain(registry *adapters.Registry) deployment.ChangeSet } apply := func(e deployment.Environment, cfg IncreaseThresholdOffchainInput) (deployment.ChangesetOutput, error) { - committee, err := buildAggregatorCommittee(e, registry, cfg.CommitteeQualifier, cfg.ChainSelectors, &cfg.NewThreshold) + committee, err := buildAggregatorCommittee(e, cfg.CommitteeQualifier, cfg.ChainSelectors, &cfg.NewThreshold) if err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("failed to build aggregator config: %w", err) } @@ -125,7 +121,7 @@ func IncreaseThresholdOffchain(registry *adapters.Registry) deployment.ChangeSet // // In deployer-key mode the transaction is submitted directly inside Apply. // MCMS-mode support is deferred to Phase 0 (CLD post-proposal hook prerequisite). -func IncreaseThreshold(registry *adapters.Registry) deployment.ChangeSetV2[IncreaseThresholdInput] { +func IncreaseThreshold() deployment.ChangeSetV2[IncreaseThresholdInput] { validate := func(e deployment.Environment, cfg IncreaseThresholdInput) error { if cfg.CommitteeQualifier == "" { return fmt.Errorf("committee qualifier is required") @@ -140,13 +136,9 @@ func IncreaseThreshold(registry *adapters.Registry) deployment.ChangeSetV2[Incre return fmt.Errorf("at least one service identifier is required") } for _, sel := range cfg.ChainSelectors { - a, err := registry.GetByChain(sel) - if err != nil { + if _, err := adapters.GetCommitteeVerifierOnchainRegistry().Get(sel); err != nil { return fmt.Errorf("chain %d: %w", sel, err) } - if a.CommitteeVerifierOnchain == nil { - return fmt.Errorf("chain %d: no CommitteeVerifierOnchain adapter registered", sel) - } } // Safety backstop 1: assert every aggregator's DataStore config already reflects @@ -174,7 +166,7 @@ func IncreaseThreshold(registry *adapters.Registry) deployment.ChangeSetV2[Incre // Safety backstop 2: assert the onchain threshold has not yet been raised to // NewThreshold. Catches double-fires and out-of-order manual invocations. ctx := context.Background() - committeeStates, err := scanCommitteeStatesForChains(ctx, e, registry, cfg.CommitteeQualifier, cfg.ChainSelectors) + committeeStates, err := scanCommitteeStatesForChains(ctx, e, cfg.CommitteeQualifier, cfg.ChainSelectors) if err != nil { return err } @@ -184,7 +176,7 @@ func IncreaseThreshold(registry *adapters.Registry) deployment.ChangeSetV2[Incre apply := func(e deployment.Environment, cfg IncreaseThresholdInput) (deployment.ChangesetOutput, error) { ctx := context.Background() - committeeStates, err := scanCommitteeStatesForChains(ctx, e, registry, cfg.CommitteeQualifier, cfg.ChainSelectors) + committeeStates, err := scanCommitteeStatesForChains(ctx, e, cfg.CommitteeQualifier, cfg.ChainSelectors) if err != nil { return deployment.ChangesetOutput{}, err } @@ -195,8 +187,11 @@ func IncreaseThreshold(registry *adapters.Registry) deployment.ChangeSetV2[Incre return deployment.ChangesetOutput{}, fmt.Errorf("chain %d: failed to build signature config change: %w", sel, err) } - a, _ := registry.GetByChain(sel) - if err := a.CommitteeVerifierOnchain.ApplySignatureConfigs(ctx, e, sel, cfg.CommitteeQualifier, change); err != nil { + onchain, err := adapters.GetCommitteeVerifierOnchainRegistry().Get(sel) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("chain %d: %w", sel, err) + } + if err := onchain.ApplySignatureConfigs(ctx, e, sel, cfg.CommitteeQualifier, change); err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("chain %d: ApplySignatureConfigs failed: %w", sel, err) } } @@ -213,17 +208,16 @@ func IncreaseThreshold(registry *adapters.Registry) deployment.ChangeSetV2[Incre func scanCommitteeStatesForChains( ctx context.Context, e deployment.Environment, - registry *adapters.Registry, qualifier string, chainSelectors []uint64, ) (map[uint64]*adapters.CommitteeState, error) { result := make(map[uint64]*adapters.CommitteeState, len(chainSelectors)) for _, sel := range chainSelectors { - a, err := registry.GetByChain(sel) + onchain, err := adapters.GetCommitteeVerifierOnchainRegistry().Get(sel) if err != nil { return nil, fmt.Errorf("chain %d: %w", sel, err) } - states, err := a.CommitteeVerifierOnchain.ScanCommitteeStates(ctx, e, sel) + states, err := onchain.ScanCommitteeStates(ctx, e, sel) if err != nil { return nil, fmt.Errorf("chain %d: ScanCommitteeStates failed: %w", sel, err) } diff --git a/deployment/changesets/lane_expansion.go b/deployment/changesets/lane_expansion.go new file mode 100644 index 000000000..322ab56ea --- /dev/null +++ b/deployment/changesets/lane_expansion.go @@ -0,0 +1,235 @@ +package changesets + +// LaneExpansion changeset overview +// +// LaneExpansion is a single-entry, onchain-only product for wiring a new +// source→destination lane between two already-deployed chains (§5.2.1). +// +// The changeset configures both sides of the lane: +// 1. On the source chain: configures the OnRamp with destination chain config +// and wires it into the selected Router (TestRouter or production). +// 2. On the destination chain: configures the OffRamp with source chain config +// and wires it into the selected Router. +// +// No offchain coupling exists — verifiers, executors, and indexers discover +// lanes by polling onchain state. +// +// In MCMS mode the changeset returns BatchOps for both chains. In deployer-key +// mode transactions are submitted directly. + +import ( + "errors" + "fmt" + "slices" + + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + + "github.com/smartcontractkit/chainlink-ccv/deployment/adapters" +) + +// LaneChainOverrides carries optional per-chain overrides for a lane. +// Nil pointer fields fall back to adapter defaults. +type LaneChainOverrides struct { + // AllowTrafficFrom enables/disables inbound traffic. Nil uses adapter default. + AllowTrafficFrom *bool + // BaseExecutionGasCost overrides the default base execution gas cost. + BaseExecutionGasCost *uint32 + // TokenReceiverAllowed overrides whether token receivers are allowed. + TokenReceiverAllowed *bool + // MessageNetworkFeeUSDCents overrides the message network fee. + MessageNetworkFeeUSDCents *uint16 + // TokenNetworkFeeUSDCents overrides the token network fee. + TokenNetworkFeeUSDCents *uint16 + // FamilyExtras carries chain-family-specific overrides (e.g. FeeQuoter dest + // chain config, executor dest chain config) that the adapter interprets. + FamilyExtras map[string]any +} + +// LaneExpansionInput is the imperative input for the LaneExpansion changeset. +type LaneExpansionInput struct { + // SrcChainSelector is the source chain of the lane. + SrcChainSelector uint64 + // DestChainSelector is the destination chain of the lane. + DestChainSelector uint64 + // UseTestRouter selects the TestRouter instead of the production Router. + // Set to true for initial integration testing; use PromoteLaneRouter to + // switch to the production Router later. + UseTestRouter bool + // ExecutorQualifier identifies the executor to use on each chain for this + // lane. Empty uses the adapter default. + ExecutorQualifier string + // InboundCCVQualifiers are committee verifier qualifiers for inbound traffic + // verification on each side of the lane. + InboundCCVQualifiers []string + // OutboundCCVQualifiers are committee verifier qualifiers for outbound traffic + // verification on each side of the lane. + OutboundCCVQualifiers []string + // SrcChainOverrides optionally overrides config for the source chain side. + SrcChainOverrides *LaneChainOverrides + // DestChainOverrides optionally overrides config for the destination chain side. + DestChainOverrides *LaneChainOverrides +} + +// LaneExpansion is a single-entry, onchain-only changeset that wires a new +// source→destination lane between two already-deployed chains (§5.2.1). +// +// Both sides of the lane are configured: the source chain's OnRamp and the +// destination chain's OffRamp, each wired into the selected Router +// (TestRouter when UseTestRouter is true, production Router otherwise). +// +// The changeset dispatches to the per-chain LaneConfigAdapter registered for +// each chain's family. The adapter handles address resolution (OnRamp, +// OffRamp, Router, FeeQuoter) from the DataStore and executes the onchain +// configuration calls. +func LaneExpansion() deployment.ChangeSetV2[LaneExpansionInput] { + validate := func(e deployment.Environment, cfg LaneExpansionInput) error { + return validateLaneInput(e, cfg.SrcChainSelector, cfg.DestChainSelector) + } + + apply := func(e deployment.Environment, cfg LaneExpansionInput) (deployment.ChangesetOutput, error) { + return applyLaneConfig(e, cfg.SrcChainSelector, cfg.DestChainSelector, + cfg.UseTestRouter, cfg.ExecutorQualifier, + cfg.InboundCCVQualifiers, cfg.OutboundCCVQualifiers, + cfg.SrcChainOverrides, cfg.DestChainOverrides) + } + + return deployment.CreateChangeSet(apply, validate) +} + +// validateLaneInput validates the common preconditions for lane expansion and +// router promotion. +func validateLaneInput( + e deployment.Environment, + srcChainSelector, destChainSelector uint64, +) error { + if srcChainSelector == 0 { + return errors.New("source chain selector is required") + } + if destChainSelector == 0 { + return errors.New("destination chain selector is required") + } + if srcChainSelector == destChainSelector { + return errors.New("source and destination chain selectors must be different") + } + + envSelectors := e.BlockChains.ListChainSelectors() + if !slices.Contains(envSelectors, srcChainSelector) { + return fmt.Errorf("source chain selector %d is not available in environment", srcChainSelector) + } + if !slices.Contains(envSelectors, destChainSelector) { + return fmt.Errorf("destination chain selector %d is not available in environment", destChainSelector) + } + + for _, sel := range []uint64{srcChainSelector, destChainSelector} { + if _, err := adapters.GetLaneConfigRegistry().Get(sel); err != nil { + return fmt.Errorf("chain %d: %w", sel, err) + } + } + + return nil +} + +// applyLaneConfig configures both sides of a lane. It dispatches to the +// LaneConfigAdapter on each chain. +func applyLaneConfig( + e deployment.Environment, + srcChainSelector, destChainSelector uint64, + useTestRouter bool, + executorQualifier string, + inboundCCVQualifiers, outboundCCVQualifiers []string, + srcOverrides, destOverrides *LaneChainOverrides, +) (deployment.ChangesetOutput, error) { + outputDS := datastore.NewMemoryDataStore() + var allReports []operations.Report[any, any] + + // Configure both sides: src chain with dest as remote, dest chain with src as remote. + sides := []struct { + localSel uint64 + remoteSel uint64 + overrides *LaneChainOverrides + }{ + {localSel: srcChainSelector, remoteSel: destChainSelector, overrides: srcOverrides}, + {localSel: destChainSelector, remoteSel: srcChainSelector, overrides: destOverrides}, + } + + for _, side := range sides { + remoteLaneCfg := buildRemoteLaneConfig( + executorQualifier, inboundCCVQualifiers, outboundCCVQualifiers, side.overrides, + ) + + existingAddresses := e.DataStore.Addresses().Filter( + datastore.AddressRefByChainSelector(side.localSel), + ) + + input := adapters.LaneConfigInput{ + ChainSelector: side.localSel, + UseTestRouter: useTestRouter, + ExistingAddresses: existingAddresses, + RemoteChains: map[uint64]adapters.RemoteLaneConfig{ + side.remoteSel: remoteLaneCfg, + }, + } + + laneAdapter, err := adapters.GetLaneConfigRegistry().Get(side.localSel) + if err != nil { + return deployment.ChangesetOutput{Reports: allReports, DataStore: outputDS}, + fmt.Errorf("chain %d: %w", side.localSel, err) + } + + report, err := operations.ExecuteSequence( + e.OperationsBundle, + laneAdapter.ConfigureLane(), + e.BlockChains, + input, + ) + allReports = append(allReports, report.ExecutionReports...) + if err != nil { + return deployment.ChangesetOutput{Reports: allReports, DataStore: outputDS}, + fmt.Errorf("chain %d: ConfigureLane failed: %w", side.localSel, err) + } + + for _, ref := range report.Output.Addresses { + if addErr := outputDS.Addresses().Add(ref); addErr != nil && + !errors.Is(addErr, datastore.ErrAddressRefExists) { + return deployment.ChangesetOutput{Reports: allReports, DataStore: outputDS}, + fmt.Errorf("chain %d: failed to add address %s to datastore: %w", + side.localSel, ref.Address, addErr) + } + } + + e.Logger.Infow("Lane configured", + "localChain", side.localSel, + "remoteChain", side.remoteSel, + "useTestRouter", useTestRouter, + ) + } + + return deployment.ChangesetOutput{ + Reports: allReports, + DataStore: outputDS, + }, nil +} + +// buildRemoteLaneConfig assembles a RemoteLaneConfig from the changeset input. +func buildRemoteLaneConfig( + executorQualifier string, + inboundCCVQualifiers, outboundCCVQualifiers []string, + overrides *LaneChainOverrides, +) adapters.RemoteLaneConfig { + cfg := adapters.RemoteLaneConfig{ + ExecutorQualifier: executorQualifier, + InboundCCVQualifiers: inboundCCVQualifiers, + OutboundCCVQualifiers: outboundCCVQualifiers, + } + if overrides != nil { + cfg.AllowTrafficFrom = overrides.AllowTrafficFrom + cfg.BaseExecutionGasCost = overrides.BaseExecutionGasCost + cfg.TokenReceiverAllowed = overrides.TokenReceiverAllowed + cfg.MessageNetworkFeeUSDCents = overrides.MessageNetworkFeeUSDCents + cfg.TokenNetworkFeeUSDCents = overrides.TokenNetworkFeeUSDCents + cfg.FamilyExtras = overrides.FamilyExtras + } + return cfg +} diff --git a/deployment/changesets/lane_expansion_test.go b/deployment/changesets/lane_expansion_test.go new file mode 100644 index 000000000..ea11e4982 --- /dev/null +++ b/deployment/changesets/lane_expansion_test.go @@ -0,0 +1,162 @@ +package changesets + +import ( + "testing" + + "github.com/Masterminds/semver/v3" + "github.com/stretchr/testify/require" + + chainsel "github.com/smartcontractkit/chain-selectors" + cldf_chain "github.com/smartcontractkit/chainlink-deployments-framework/chain" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + + "github.com/smartcontractkit/chainlink-ccv/deployment/adapters" +) + +// stubLaneConfigAdapter implements adapters.LaneConfigAdapter for validation +// tests. The sequence body is never executed in the validation path. +type stubLaneConfigAdapter struct{} + +var _ adapters.LaneConfigAdapter = (*stubLaneConfigAdapter)(nil) + +var stubLaneConfigSequence = operations.NewSequence( + "stub-configure-lane", + semver.MustParse("1.0.0"), + "stub sequence used only by validation tests", + func(_ operations.Bundle, _ cldf_chain.BlockChains, _ adapters.LaneConfigInput) (adapters.LaneConfigOutput, error) { + return adapters.LaneConfigOutput{}, nil + }, +) + +func (s *stubLaneConfigAdapter) ConfigureLane() *operations.Sequence[adapters.LaneConfigInput, adapters.LaneConfigOutput, cldf_chain.BlockChains] { + return stubLaneConfigSequence +} + +func registerLaneConfigAdapter() { + adapters.GetLaneConfigRegistry().Register(chainsel.FamilyEVM, &stubLaneConfigAdapter{}) +} + +func newLaneTestEnv(selectors []uint64) deployment.Environment { + return deployment.Environment{ + BlockChains: newTestBlockChains(selectors), + DataStore: datastore.NewMemoryDataStore().Seal(), + } +} + +func TestLaneExpansion_Validation_MissingSrcChain(t *testing.T) { + sel := chainsel.TEST_90000001.Selector + registerLaneConfigAdapter() + cs := LaneExpansion() + err := cs.VerifyPreconditions(newLaneTestEnv([]uint64{sel}), LaneExpansionInput{ + DestChainSelector: sel, + }) + require.ErrorContains(t, err, "source chain selector is required") +} + +func TestLaneExpansion_Validation_MissingDestChain(t *testing.T) { + sel := chainsel.TEST_90000001.Selector + registerLaneConfigAdapter() + cs := LaneExpansion() + err := cs.VerifyPreconditions(newLaneTestEnv([]uint64{sel}), LaneExpansionInput{ + SrcChainSelector: sel, + }) + require.ErrorContains(t, err, "destination chain selector is required") +} + +func TestLaneExpansion_Validation_SameChain(t *testing.T) { + sel := chainsel.TEST_90000001.Selector + registerLaneConfigAdapter() + cs := LaneExpansion() + err := cs.VerifyPreconditions(newLaneTestEnv([]uint64{sel}), LaneExpansionInput{ + SrcChainSelector: sel, + DestChainSelector: sel, + }) + require.ErrorContains(t, err, "source and destination chain selectors must be different") +} + +func TestLaneExpansion_Validation_SrcChainNotInEnv(t *testing.T) { + sel1 := chainsel.TEST_90000001.Selector + sel2 := chainsel.TEST_90000002.Selector + registerLaneConfigAdapter() + cs := LaneExpansion() + err := cs.VerifyPreconditions(newLaneTestEnv([]uint64{sel2}), LaneExpansionInput{ + SrcChainSelector: sel1, + DestChainSelector: sel2, + }) + require.ErrorContains(t, err, "source chain selector") + require.ErrorContains(t, err, "is not available in environment") +} + +func TestLaneExpansion_Validation_DestChainNotInEnv(t *testing.T) { + sel1 := chainsel.TEST_90000001.Selector + sel2 := chainsel.TEST_90000002.Selector + registerLaneConfigAdapter() + cs := LaneExpansion() + err := cs.VerifyPreconditions(newLaneTestEnv([]uint64{sel1}), LaneExpansionInput{ + SrcChainSelector: sel1, + DestChainSelector: sel2, + }) + require.ErrorContains(t, err, "destination chain selector") + require.ErrorContains(t, err, "is not available in environment") +} + +func TestLaneExpansion_Validation_HappyPath(t *testing.T) { + sel1 := chainsel.TEST_90000001.Selector + sel2 := chainsel.TEST_90000002.Selector + registerLaneConfigAdapter() + cs := LaneExpansion() + err := cs.VerifyPreconditions(newLaneTestEnv([]uint64{sel1, sel2}), LaneExpansionInput{ + SrcChainSelector: sel1, + DestChainSelector: sel2, + UseTestRouter: true, + }) + require.NoError(t, err) +} + +func TestPromoteLaneRouter_Validation_HappyPath(t *testing.T) { + sel1 := chainsel.TEST_90000001.Selector + sel2 := chainsel.TEST_90000002.Selector + registerLaneConfigAdapter() + cs := PromoteLaneRouter() + err := cs.VerifyPreconditions(newLaneTestEnv([]uint64{sel1, sel2}), PromoteLaneRouterInput{ + SrcChainSelector: sel1, + DestChainSelector: sel2, + }) + require.NoError(t, err) +} + +func TestPromoteLaneRouter_Validation_MissingSrcChain(t *testing.T) { + sel := chainsel.TEST_90000001.Selector + registerLaneConfigAdapter() + cs := PromoteLaneRouter() + err := cs.VerifyPreconditions(newLaneTestEnv([]uint64{sel}), PromoteLaneRouterInput{ + DestChainSelector: sel, + }) + require.ErrorContains(t, err, "source chain selector is required") +} + +func TestBuildRemoteLaneConfig_WithOverrides(t *testing.T) { + allow := true + gasCost := uint32(100000) + cfg := buildRemoteLaneConfig("exec-default", []string{"ccv-a"}, []string{"ccv-b"}, &LaneChainOverrides{ + AllowTrafficFrom: &allow, + BaseExecutionGasCost: &gasCost, + FamilyExtras: map[string]any{"key": "value"}, + }) + require.Equal(t, "exec-default", cfg.ExecutorQualifier) + require.Equal(t, []string{"ccv-a"}, cfg.InboundCCVQualifiers) + require.Equal(t, []string{"ccv-b"}, cfg.OutboundCCVQualifiers) + require.Equal(t, &allow, cfg.AllowTrafficFrom) + require.Equal(t, &gasCost, cfg.BaseExecutionGasCost) + require.Equal(t, map[string]any{"key": "value"}, cfg.FamilyExtras) +} + +func TestBuildRemoteLaneConfig_NoOverrides(t *testing.T) { + cfg := buildRemoteLaneConfig("exec-default", nil, nil, nil) + require.Equal(t, "exec-default", cfg.ExecutorQualifier) + require.Nil(t, cfg.AllowTrafficFrom) + require.Nil(t, cfg.BaseExecutionGasCost) + require.Nil(t, cfg.FamilyExtras) +} diff --git a/deployment/changesets/onboard_chain.go b/deployment/changesets/onboard_chain.go new file mode 100644 index 000000000..8ec028b7c --- /dev/null +++ b/deployment/changesets/onboard_chain.go @@ -0,0 +1,91 @@ +package changesets + +// OnboardChain changeset overview +// +// OnboardChain is a single-entry, onchain-only product that deploys everything +// a new chain needs to participate in CCIP 2.0: protocol contracts (RMN, +// OnRamp, OffRamp, FeeQuoter, Router, Executors) AND committee verifiers. +// +// It composes the same deploy helpers used by DeployProtocolContracts and +// DeployCommitteeVerifier, so re-running is idempotent and the result is +// identical to running the two changesets sequentially. +// +// After OnboardChain, the chain is deployed but not yet connected to any lanes. +// Use LaneExpansion to wire lanes, then ApplyVerifierConfig / ApplyExecutorConfig +// for offchain setup. + +import ( + "fmt" + + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" +) + +// OnboardChainInput is the imperative input for the OnboardChain changeset. +type OnboardChainInput struct { + // ProtocolContracts configures the protocol contract deployment + // (RMN, OnRamp, OffRamp, FeeQuoter, Router, Executors). + ProtocolContracts DeployProtocolContractsInput + // CommitteeVerifiers configures the committee verifier deployment. + // ChainSelectors must match ProtocolContracts.ChainSelectors. + CommitteeVerifiers DeployCommitteeVerifierInput +} + +// OnboardChain deploys both protocol contracts and committee verifiers on the +// specified chains in a single changeset. It is the composite entry point for +// the chain addition workflow (§5.1). +// +// Protocol contracts are deployed first, then committee verifiers (which may +// reference protocol contract addresses via the adapter's ExistingAddresses). +// Both use the same shared helpers as their standalone counterparts, so +// idempotency guarantees are identical. +func OnboardChain() deployment.ChangeSetV2[OnboardChainInput] { + validate := func(e deployment.Environment, cfg OnboardChainInput) error { + if err := validateProtocolContractsDeploy(e, cfg.ProtocolContracts); err != nil { + return fmt.Errorf("protocol contracts: %w", err) + } + if err := validateCommitteeVerifierDeploy(e, cfg.CommitteeVerifiers); err != nil { + return fmt.Errorf("committee verifiers: %w", err) + } + return nil + } + + apply := func(e deployment.Environment, cfg OnboardChainInput) (deployment.ChangesetOutput, error) { + ds := datastore.NewMemoryDataStore() + var allReports []operations.Report[any, any] + + // Phase 1: deploy protocol contracts. + reports, err := deployProtocolContractsOnChains(e, cfg.ProtocolContracts, ds) + allReports = append(allReports, reports...) + if err != nil { + return deployment.ChangesetOutput{Reports: allReports, DataStore: ds}, + fmt.Errorf("protocol contracts: %w", err) + } + + // Phase 2: deploy committee verifiers. Use a merged DataStore so the + // CCV adapter can see the protocol addresses just deployed. + ccvEnv := e + merged := datastore.NewMemoryDataStore() + if err := merged.Merge(e.DataStore); err != nil { + return deployment.ChangesetOutput{Reports: allReports, DataStore: ds}, + fmt.Errorf("failed to merge base datastore: %w", err) + } + if err := merged.Merge(ds.Seal()); err != nil { + return deployment.ChangesetOutput{Reports: allReports, DataStore: ds}, + fmt.Errorf("failed to merge protocol contracts datastore: %w", err) + } + ccvEnv.DataStore = merged.Seal() + + reports, err = deployCommitteeVerifiersOnChains(ccvEnv, cfg.CommitteeVerifiers, ds) + allReports = append(allReports, reports...) + if err != nil { + return deployment.ChangesetOutput{Reports: allReports, DataStore: ds}, + fmt.Errorf("committee verifiers: %w", err) + } + + return deployment.ChangesetOutput{Reports: allReports, DataStore: ds}, nil + } + + return deployment.CreateChangeSet(apply, validate) +} diff --git a/deployment/changesets/promote_lane_router.go b/deployment/changesets/promote_lane_router.go new file mode 100644 index 000000000..24d00ab40 --- /dev/null +++ b/deployment/changesets/promote_lane_router.go @@ -0,0 +1,67 @@ +package changesets + +// PromoteLaneRouter changeset overview +// +// PromoteLaneRouter is a single-entry, onchain-only product that switches a +// lane from the TestRouter to the production Router (§5.2.2). +// +// Structurally it is a re-run of LaneExpansion with UseTestRouter=false. The +// distinction is semantic: PromoteLaneRouter assumes the lane already exists +// (was previously configured via LaneExpansion with UseTestRouter=true) and +// only swaps the Router reference on both sides. +// +// No offchain coupling — verifiers, executors, and indexers are unaffected by +// the Router swap. + +import ( + "github.com/smartcontractkit/chainlink-deployments-framework/deployment" +) + +// PromoteLaneRouterInput is the imperative input for the PromoteLaneRouter changeset. +type PromoteLaneRouterInput struct { + // SrcChainSelector is the source chain of the lane to promote. + SrcChainSelector uint64 + // DestChainSelector is the destination chain of the lane to promote. + DestChainSelector uint64 + // ExecutorQualifier identifies the executor to use on each chain for this + // lane. Empty uses the adapter default. Should match the value used in + // the original LaneExpansion. + ExecutorQualifier string + // InboundCCVQualifiers are committee verifier qualifiers for inbound traffic + // verification. Should match the original LaneExpansion. + InboundCCVQualifiers []string + // OutboundCCVQualifiers are committee verifier qualifiers for outbound traffic + // verification. Should match the original LaneExpansion. + OutboundCCVQualifiers []string + // SrcChainOverrides optionally overrides config for the source chain side. + SrcChainOverrides *LaneChainOverrides + // DestChainOverrides optionally overrides config for the destination chain side. + DestChainOverrides *LaneChainOverrides +} + +// PromoteLaneRouter is a single-entry, onchain-only changeset that switches a +// lane from the TestRouter to the production Router (§5.2.2). +// +// It re-configures the OnRamp and OffRamp on both sides of the lane to use the +// production Router address and wires them into the production Router via +// ApplyRampUpdates. The lane must have been previously configured via +// LaneExpansion. +// +// The changeset dispatches to the same LaneConfigAdapter as LaneExpansion, +// with UseTestRouter=false. The adapter's idempotency guarantees ensure that +// re-running is safe. +func PromoteLaneRouter() deployment.ChangeSetV2[PromoteLaneRouterInput] { + validate := func(e deployment.Environment, cfg PromoteLaneRouterInput) error { + return validateLaneInput(e, cfg.SrcChainSelector, cfg.DestChainSelector) + } + + apply := func(e deployment.Environment, cfg PromoteLaneRouterInput) (deployment.ChangesetOutput, error) { + return applyLaneConfig(e, cfg.SrcChainSelector, cfg.DestChainSelector, + false, // UseTestRouter=false — production Router + cfg.ExecutorQualifier, + cfg.InboundCCVQualifiers, cfg.OutboundCCVQualifiers, + cfg.SrcChainOverrides, cfg.DestChainOverrides) + } + + return deployment.CreateChangeSet(apply, validate) +} diff --git a/deployment/changesets/remove_nop_from_committee.go b/deployment/changesets/remove_nop_from_committee.go index 91e88803a..1935761e9 100644 --- a/deployment/changesets/remove_nop_from_committee.go +++ b/deployment/changesets/remove_nop_from_committee.go @@ -60,17 +60,17 @@ type RemoveNOPOffchainInput struct { // Onchain-first ordering is required because removing a signer from the contract immediately // stops that signer's votes from being counted, while the aggregator still collects from // them harmlessly until step-2 updates the offchain config. -func RemoveNOPFromCommittee(registry *adapters.Registry) deployment.ChangeSetV2[RemoveNOPFromCommitteeInput] { +func RemoveNOPFromCommittee() deployment.ChangeSetV2[RemoveNOPFromCommitteeInput] { validate := func(e deployment.Environment, cfg RemoveNOPFromCommitteeInput) error { - return validateStep1NOP(e, cfg.CommitteeQualifier, cfg.NOPAlias, cfg.SourceChainSelectors, registry) + return validateStep1NOP(e, cfg.CommitteeQualifier, cfg.NOPAlias, cfg.SourceChainSelectors) } apply := func(e deployment.Environment, cfg RemoveNOPFromCommitteeInput) (deployment.ChangesetOutput, error) { - signerFamily, err := getSignerFamilyFromRegistry(registry, cfg.SourceChainSelectors) + signerFamily, err := getSignerFamilyFromRegistry(cfg.SourceChainSelectors) if err != nil { return deployment.ChangesetOutput{}, err } - if err := applySignerChangesOnchain(e, registry, cfg.CommitteeQualifier, cfg.NOPAlias, signerFamily, + if err := applySignerChangesOnchain(e, cfg.CommitteeQualifier, cfg.NOPAlias, signerFamily, cfg.SourceChainSelectors, cfg.NewThreshold, buildRemoveSignerChange); err != nil { return deployment.ChangesetOutput{}, err } @@ -92,7 +92,7 @@ func RemoveNOPFromCommittee(registry *adapters.Registry) deployment.ChangeSetV2[ // // When NOPAlias is set, all verifier jobs scoped to this committee for that NOP are revoked // from JD and removed from the DataStore in the same run. -func RemoveNOPOffchain(registry *adapters.Registry) deployment.ChangeSetV2[RemoveNOPOffchainInput] { +func RemoveNOPOffchain() deployment.ChangeSetV2[RemoveNOPOffchainInput] { validate := func(e deployment.Environment, cfg RemoveNOPOffchainInput) error { if cfg.CommitteeQualifier == "" { return fmt.Errorf("committee qualifier is required") @@ -107,27 +107,23 @@ func RemoveNOPOffchain(registry *adapters.Registry) deployment.ChangeSetV2[Remov return fmt.Errorf("NOP alias is required for job revocation") } - committeeChains := registry.AllDeployedCommitteeVerifierChains(e.DataStore, cfg.CommitteeQualifier) + committeeChains := adapters.AllDeployedCommitteeVerifierChains(e.DataStore, cfg.CommitteeQualifier) if len(committeeChains) == 0 { return fmt.Errorf("no dest chains found for committee %q — step-1 may not have been applied or adapters are not registered", cfg.CommitteeQualifier) } for _, sel := range committeeChains { - a, err := registry.GetByChain(sel) - if err != nil { + if _, err := adapters.GetCommitteeVerifierOnchainRegistry().Get(sel); err != nil { return fmt.Errorf("dest chain %d: %w", sel, err) } - if a.CommitteeVerifierOnchain == nil { - return fmt.Errorf("dest chain %d: no CommitteeVerifierOnchain adapter registered", sel) - } - if a.Aggregator == nil { - return fmt.Errorf("dest chain %d: no Aggregator adapter registered", sel) + if _, err := adapters.GetAggregatorRegistry().Get(sel); err != nil { + return fmt.Errorf("dest chain %d: %w", sel, err) } } // Safety backstop: assert the removed signer is absent onchain on every dest chain for // every source chain. Catches hook misfires and out-of-order manual invocations. if cfg.RemovedSignerAddress != "" { - committeeStates, err := scanCommitteeStatesForChains(e.GetContext(), e, registry, cfg.CommitteeQualifier, committeeChains) + committeeStates, err := scanCommitteeStatesForChains(e.GetContext(), e, cfg.CommitteeQualifier, committeeChains) if err != nil { return err } @@ -156,12 +152,12 @@ func RemoveNOPOffchain(registry *adapters.Registry) deployment.ChangeSetV2[Remov } apply := func(e deployment.Environment, cfg RemoveNOPOffchainInput) (deployment.ChangesetOutput, error) { - committeeChains := registry.AllDeployedCommitteeVerifierChains(e.DataStore, cfg.CommitteeQualifier) + committeeChains := adapters.AllDeployedCommitteeVerifierChains(e.DataStore, cfg.CommitteeQualifier) if len(committeeChains) == 0 { return deployment.ChangesetOutput{}, fmt.Errorf("no dest chains found for committee %q", cfg.CommitteeQualifier) } - committee, err := buildAggregatorCommittee(e, registry, cfg.CommitteeQualifier, committeeChains, nil) + committee, err := buildAggregatorCommittee(e, cfg.CommitteeQualifier, committeeChains, nil) if err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("failed to build aggregator config: %w", err) } diff --git a/deployment/changesets/remove_nop_from_committee_test.go b/deployment/changesets/remove_nop_from_committee_test.go index 7ff7f4056..04eb8e16a 100644 --- a/deployment/changesets/remove_nop_from_committee_test.go +++ b/deployment/changesets/remove_nop_from_committee_test.go @@ -154,7 +154,8 @@ func TestBuildRemoveSignerChange_ReturnsEmptyWhenNoSourceChainsMatch(t *testing. // ---- RemoveNOPFromCommittee validation ---- func TestRemoveNOPFromCommittee_Validation_MissingQualifier(t *testing.T) { - cs := RemoveNOPFromCommittee(newEVMRegistry(&stubOnchainAdapter{})) + registerEVMOnchain(&stubOnchainAdapter{}) + cs := RemoveNOPFromCommittee() err := cs.VerifyPreconditions(newTestEnvironmentWithOffchain(), RemoveNOPFromCommitteeInput{ SourceChainSelectors: []uint64{chainsel.TEST_90000001.Selector}, NOPAlias: testNOPAlias, @@ -164,7 +165,8 @@ func TestRemoveNOPFromCommittee_Validation_MissingQualifier(t *testing.T) { } func TestRemoveNOPFromCommittee_Validation_MissingSourceChainSelectors(t *testing.T) { - cs := RemoveNOPFromCommittee(newEVMRegistry(&stubOnchainAdapter{})) + registerEVMOnchain(&stubOnchainAdapter{}) + cs := RemoveNOPFromCommittee() err := cs.VerifyPreconditions(newTestEnvironmentWithOffchain(), RemoveNOPFromCommitteeInput{ CommitteeQualifier: testQualifier, NOPAlias: testNOPAlias, @@ -174,7 +176,8 @@ func TestRemoveNOPFromCommittee_Validation_MissingSourceChainSelectors(t *testin } func TestRemoveNOPFromCommittee_Validation_MissingNOPAlias(t *testing.T) { - cs := RemoveNOPFromCommittee(newEVMRegistry(&stubOnchainAdapter{})) + registerEVMOnchain(&stubOnchainAdapter{}) + cs := RemoveNOPFromCommittee() err := cs.VerifyPreconditions(newTestEnvironmentWithOffchain(), RemoveNOPFromCommitteeInput{ CommitteeQualifier: testQualifier, SourceChainSelectors: []uint64{chainsel.TEST_90000001.Selector}, @@ -184,7 +187,8 @@ func TestRemoveNOPFromCommittee_Validation_MissingNOPAlias(t *testing.T) { } func TestRemoveNOPFromCommittee_Validation_RequiresOffchainClient(t *testing.T) { - cs := RemoveNOPFromCommittee(newEVMRegistry(&stubOnchainAdapter{})) + registerEVMOnchain(&stubOnchainAdapter{}) + cs := RemoveNOPFromCommittee() err := cs.VerifyPreconditions(deployment.Environment{}, RemoveNOPFromCommitteeInput{ CommitteeQualifier: testQualifier, SourceChainSelectors: []uint64{chainsel.TEST_90000001.Selector}, @@ -197,7 +201,8 @@ func TestRemoveNOPFromCommittee_Validation_RequiresOffchainClient(t *testing.T) // ---- RemoveNOPOffchain validation ---- func TestRemoveNOPOffchain_Validation_MissingQualifier(t *testing.T) { - cs := RemoveNOPOffchain(newFullEVMRegistry(&stubFullAdapter{})) + registerFullEVMAdapters(&stubFullAdapter{}) + cs := RemoveNOPOffchain() err := cs.VerifyPreconditions(deployment.Environment{}, RemoveNOPOffchainInput{ SourceChainSelectors: []uint64{chainsel.TEST_90000001.Selector}, ServiceIdentifiers: []string{"svc1"}, @@ -207,7 +212,8 @@ func TestRemoveNOPOffchain_Validation_MissingQualifier(t *testing.T) { } func TestRemoveNOPOffchain_Validation_MissingSourceChainSelectors(t *testing.T) { - cs := RemoveNOPOffchain(newFullEVMRegistry(&stubFullAdapter{})) + registerFullEVMAdapters(&stubFullAdapter{}) + cs := RemoveNOPOffchain() err := cs.VerifyPreconditions(deployment.Environment{}, RemoveNOPOffchainInput{ CommitteeQualifier: testQualifier, ServiceIdentifiers: []string{"svc1"}, @@ -217,7 +223,8 @@ func TestRemoveNOPOffchain_Validation_MissingSourceChainSelectors(t *testing.T) } func TestRemoveNOPOffchain_Validation_MissingServiceIdentifiers(t *testing.T) { - cs := RemoveNOPOffchain(newFullEVMRegistry(&stubFullAdapter{})) + registerFullEVMAdapters(&stubFullAdapter{}) + cs := RemoveNOPOffchain() err := cs.VerifyPreconditions(deployment.Environment{}, RemoveNOPOffchainInput{ CommitteeQualifier: testQualifier, SourceChainSelectors: []uint64{chainsel.TEST_90000001.Selector}, @@ -227,7 +234,8 @@ func TestRemoveNOPOffchain_Validation_MissingServiceIdentifiers(t *testing.T) { } func TestRemoveNOPOffchain_Validation_MissingNOPAlias(t *testing.T) { - cs := RemoveNOPOffchain(newFullEVMRegistry(&stubFullAdapter{})) + registerFullEVMAdapters(&stubFullAdapter{}) + cs := RemoveNOPOffchain() err := cs.VerifyPreconditions(deployment.Environment{}, RemoveNOPOffchainInput{ CommitteeQualifier: testQualifier, SourceChainSelectors: []uint64{chainsel.TEST_90000001.Selector}, @@ -258,7 +266,8 @@ func TestRemoveNOPOffchain_Validation_BackstopPassesWhenSignerAbsent(t *testing. verifierAddrs: map[uint64]string{sel1: "0x1111"}, } - cs := RemoveNOPOffchain(newFullEVMRegistry(adapter)) + registerFullEVMAdapters(adapter) + cs := RemoveNOPOffchain() env := deployment.Environment{ BlockChains: newTestBlockChains([]uint64{sel1}), DataStore: datastore.NewMemoryDataStore().Seal(), @@ -294,7 +303,8 @@ func TestRemoveNOPOffchain_Validation_BackstopFailsWhenSignerStillPresent(t *tes verifierAddrs: map[uint64]string{sel1: "0x1111"}, } - cs := RemoveNOPOffchain(newFullEVMRegistry(adapter)) + registerFullEVMAdapters(adapter) + cs := RemoveNOPOffchain() env := deployment.Environment{ BlockChains: newTestBlockChains([]uint64{sel1}), DataStore: datastore.NewMemoryDataStore().Seal(), @@ -334,7 +344,8 @@ func TestRemoveNOPOffchain_Apply_WritesUpdatedAggregatorConfig(t *testing.T) { verifierAddrs: map[uint64]string{sel1: verifierAddr}, } - cs := RemoveNOPOffchain(newFullEVMRegistry(adapter)) + registerFullEVMAdapters(adapter) + cs := RemoveNOPOffchain() env := deployment.Environment{ Logger: logger.Test(t), BlockChains: newTestBlockChains([]uint64{sel1}), @@ -385,7 +396,8 @@ func TestRemoveNOPOffchain_Apply_UsesAllDiscoveredDestChains(t *testing.T) { verifierAddrs: map[uint64]string{sel1: addr1, sel2: addr2}, } - cs := RemoveNOPOffchain(newFullEVMRegistry(adapter)) + registerFullEVMAdapters(adapter) + cs := RemoveNOPOffchain() env := deployment.Environment{ Logger: logger.Test(t), BlockChains: newTestBlockChains([]uint64{sel1, sel2}), @@ -415,7 +427,8 @@ func TestRemoveNOPOffchain_Apply_ScanError(t *testing.T) { states: map[uint64][]*adapters.CommitteeState{sel1: nil}, scanErr: fmt.Errorf("node unreachable"), } - cs := RemoveNOPOffchain(newFullEVMRegistry(adapter)) + registerFullEVMAdapters(adapter) + cs := RemoveNOPOffchain() env := deployment.Environment{ BlockChains: newTestBlockChains([]uint64{sel1}), } @@ -448,7 +461,8 @@ func TestRemoveNOPOffchain_Apply_MultipleServiceIdentifiers(t *testing.T) { verifierAddrs: map[uint64]string{sel1: verifierAddr}, } - cs := RemoveNOPOffchain(newFullEVMRegistry(adapter)) + registerFullEVMAdapters(adapter) + cs := RemoveNOPOffchain() env := deployment.Environment{ Logger: logger.Test(t), BlockChains: newTestBlockChains([]uint64{sel1}),