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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build/devenv/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ require (
github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9 // indirect
github.com/smartcontractkit/chainlink-protos/op-catalog v0.0.4 // indirect
github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 // indirect
github.com/smartcontractkit/chainlink-protos/svr v1.1.1-0.20260203131522-bb8bc5c423b3 // indirect
github.com/smartcontractkit/chainlink-ton v0.0.0-20260415120434-cecc380f8d87 // indirect
github.com/smartcontractkit/chainlink/v2 v2.29.0 // indirect
github.com/smartcontractkit/wsrpc v0.8.5-0.20250502134807-c57d3d995945 // indirect
Expand Down
2 changes: 2 additions & 0 deletions build/devenv/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1170,6 +1170,8 @@ github.com/smartcontractkit/chainlink-protos/op-catalog v0.0.4 h1:AEnxv4HM3WD1Rb
github.com/smartcontractkit/chainlink-protos/op-catalog v0.0.4/go.mod h1:PjZD54vr6rIKEKQj6HNA4hllvYI/QpT+Zefj3tqkFAs=
github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 h1:0eroOyBwmdoGUwUdvMI0/J7m5wuzNnJDMglSOK1sfNY=
github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0/go.mod h1:m/A3lqD7ms/RsQ9BT5P2uceYY0QX5mIt4KQxT2G6qEo=
github.com/smartcontractkit/chainlink-protos/svr v1.1.1-0.20260203131522-bb8bc5c423b3 h1:X8Pekpv+cy0eW1laZTwATuYLTLZ6gRTxz1ZWOMtU74o=
github.com/smartcontractkit/chainlink-protos/svr v1.1.1-0.20260203131522-bb8bc5c423b3/go.mod h1:TcOliTQU6r59DwG4lo3U+mFM9WWyBHGuFkkxQpvSujo=
github.com/smartcontractkit/chainlink-sui v0.0.0-20260205175622-33e65031f9a9 h1:KyPROV+v7P8VdiU7JhVuGLcDlEBsURSpQmSCgNBTY+s=
github.com/smartcontractkit/chainlink-sui v0.0.0-20260205175622-33e65031f9a9/go.mod h1:KpEWZJMLwbdMHeHQz9rbkES0vRrx4nk6OQXyhlHb9/8=
github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.13 h1:quHuZ/2I7XZ8pdRw5UAwKW/idsPchHN7KnQ69YWzxS4=
Expand Down
38 changes: 38 additions & 0 deletions changelog/2026-04-22_standalone_executor_registry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Use Registry in Standalone Executor

## Summary

### Standalone executor is now family agnostic.

`cmd/executor/standalone/main.go` now uses `chainaccess.NewRegistry` to build
chain accessors instead of constructing EVM readers and transmitters directly.
The chain init loop calls `accessor.DestinationReader()` and
`accessor.ContractTransmitter()`, and skips any chain where either returns an
error.

### `chainaccess.Accessor` getter signatures changed

Two new getters were added to `chainaccess.Accessor` and the existing getter was modified.
They return an error when the capability is not available for the chain, rather than returning nil:

```go
// Before
SourceReader() SourceReader

// After
SourceReader() (SourceReader, error)
DestinationReader() (DestinationReader, error)
ContractTransmitter() (ContractTransmitter, error)
```

Callers must check the error before using the returned value.

### `GetAccessor` should succeeds even when capabilities are partially unavailable

`AccessorFactory.GetAccessor` should returns a valid `Accessor` whenever the
chain selector is recognized, even if one or more of its capabilities
(e.g. `SourceReader` when `on_ramp_addresses` is absent) could not be constructed.
Missing capabilities are reported as errors only when the corresponding getter is called.

Previously, the GetAccessor call would fail because there was only a single capability.

80 changes: 31 additions & 49 deletions cmd/executor/standalone/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"time"

"github.com/BurntSushi/toml"
"github.com/ethereum/go-ethereum/common"
"github.com/grafana/pyroscope-go"
"go.uber.org/zap/zapcore"

Expand All @@ -20,12 +19,10 @@ import (
x "github.com/smartcontractkit/chainlink-ccv/executor/pkg/executor"
"github.com/smartcontractkit/chainlink-ccv/executor/pkg/leaderelector"
"github.com/smartcontractkit/chainlink-ccv/executor/pkg/monitoring"
"github.com/smartcontractkit/chainlink-ccv/integration/pkg/accessors/evm"
_ "github.com/smartcontractkit/chainlink-ccv/integration/pkg/accessors/evm"
"github.com/smartcontractkit/chainlink-ccv/integration/pkg/backofftimeprovider"
"github.com/smartcontractkit/chainlink-ccv/integration/pkg/ccvstreamer"
"github.com/smartcontractkit/chainlink-ccv/integration/pkg/contracttransmitter"
"github.com/smartcontractkit/chainlink-ccv/integration/pkg/cursechecker"
"github.com/smartcontractkit/chainlink-ccv/integration/pkg/destinationreader"
"github.com/smartcontractkit/chainlink-ccv/pkg/chainaccess"
"github.com/smartcontractkit/chainlink-ccv/protocol"
"github.com/smartcontractkit/chainlink-ccv/protocol/common/logging"
Expand All @@ -35,7 +32,6 @@ import (

const (
configPathEnvVar = "EXECUTOR_CONFIG_PATH"
privateKeyEnvVar = "EXECUTOR_TRANSMITTER_PRIVATE_KEY"
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

There should be a better way to inject this into the executor.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

In the bootstrap approach this would be provided by the keystore

// indexerPollingInterval describes how frequently we ask indexer for new messages.
// This should be kept at 1s for consistent behavior across all executors.
indexerPollingInterval = 1 * time.Second
Expand Down Expand Up @@ -71,7 +67,7 @@ func main() {
}
lggr = logger.Named(lggr, "executor")

executorConfig, blockchainInfo, err := loadConfiguration(configPath)
executorConfig, registry, err := loadConfiguration(lggr, configPath)
if err != nil {
lggr.Errorw("Failed to load configuration", "path", configPath, "error", err)
os.Exit(1)
Expand Down Expand Up @@ -99,7 +95,6 @@ func main() {
protocol.InitChainSelectorCache()

lggr.Infow("Executor configuration", "config", executorConfig)
lggr.Infow("Blockchain information", "blockchainInfo", blockchainInfo)

//
// Setup OTEL Monitoring (via beholder)
Expand Down Expand Up @@ -159,53 +154,30 @@ func main() {
destReaders := make(map[protocol.ChainSelector]chainaccess.DestinationReader)
enabledDestChains := make([]protocol.ChainSelector, 0)
rmnReaders := make(map[protocol.ChainSelector]chainaccess.RMNCurseReader)
for selector, chain := range blockchainInfo.GetAllInfos() {
strSel := strconv.FormatUint(uint64(selector), 10)
chainConfig := executorConfig.ChainConfiguration[strSel]

chainClient, err := evm.CreateMultiNodeClientFromInfo(ctx, chain, lggr)
for strSel := range executorConfig.ChainConfiguration {
selectorUint, err := strconv.ParseUint(strSel, 10, 64)
if err != nil {
lggr.Errorw("Failed to create chain client", "error", err, "chainSelector", strSel)
lggr.Errorw("Invalid chain selector in configuration", "error", err, "chainSelector", strSel)
continue
}
dr, err := destinationreader.NewEvmDestinationReader(
destinationreader.Params{
Lggr: lggr,
ChainSelector: selector,
ChainClient: chainClient,
OfframpAddress: chainConfig.OffRampAddress,
RmnRemoteAddress: chainConfig.RmnAddress,
ExecutionVisabilityWindow: executorConfig.MaxRetryDuration,
Monitoring: executorMonitoring,
})
selector := protocol.ChainSelector(selectorUint)

accessor, err := registry.GetAccessor(ctx, selector)
if err != nil {
lggr.Errorw("Failed to create destination reader", "error", err, "chainSelector", strSel)
chainClient.Close()
lggr.Errorw("Failed to get accessor for chain", "error", err, "chainSelector", strSel)
continue
}

pk := os.Getenv(privateKeyEnvVar)
if pk == "" {
lggr.Errorf("Environment variable %s is not set", privateKeyEnvVar)
os.Exit(1)
}
dr, drErr := accessor.DestinationReader()
ct, ctErr := accessor.ContractTransmitter()

ct, err := contracttransmitter.NewEVMContractTransmitterFromRPC(
ctx,
lggr,
selector,
chain.Nodes[0].InternalHTTPUrl,
pk,
common.HexToAddress(chainConfig.OffRampAddress),
)
if err != nil {
lggr.Errorw("Failed to create contract transmitter", "error", err)
os.Exit(1)
}
if dr != nil {
destReaders[selector] = dr
rmnReaders[selector] = dr
if drErr != nil || ctErr != nil {
lggr.Warnw("Skipping chain: missing DestinationReader or ContractTransmitter", "chainSelector", strSel, "destReaderErr", drErr, "transmitterErr", ctErr)
continue
}

destReaders[selector] = dr
rmnReaders[selector] = dr
contractTransmitters[selector] = ct
enabledDestChains = append(enabledDestChains, selector)
}
Expand Down Expand Up @@ -346,15 +318,25 @@ func main() {
lggr.Infow("Execution service stopped gracefully")
}

func loadConfiguration(filepath string) (*executor.Configuration, *chainaccess.Infos[evm.Info], error) {
var config executor.ConfigWithBlockchainInfo[evm.Info]
if _, err := toml.DecodeFile(filepath, &config); err != nil {
func loadConfiguration(lggr logger.Logger, filepath string) (*executor.Configuration, *chainaccess.Registry, error) {
content, err := os.ReadFile(filepath) // #nosec G304 -- config file path is operator-controlled
if err != nil {
return nil, nil, err
}

var config executor.ConfigWithBlockchainInfo[any]
if _, err := toml.Decode(string(content), &config); err != nil {
return nil, nil, err
}
normalizedConfig, err := config.GetNormalizedConfig()
if err != nil {
return nil, nil, err
}
return normalizedConfig, &config.BlockchainInfos, nil

registry, err := chainaccess.NewRegistry(lggr, string(content))
if err != nil {
return nil, nil, err
}
Comment on lines +336 to +339
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The main change. Create the registry, then use it in place of the other constructor calls that used to be in this file.


return normalizedConfig, registry, nil
}
7 changes: 5 additions & 2 deletions deployment/changesets/apply_executor_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/BurntSushi/toml"

"github.com/smartcontractkit/chainlink-ccv/pkg/chainaccess"
"github.com/smartcontractkit/chainlink-deployments-framework/datastore"
"github.com/smartcontractkit/chainlink-deployments-framework/deployment"
"github.com/smartcontractkit/chainlink-deployments-framework/operations"
Expand Down Expand Up @@ -328,8 +329,10 @@ func buildExecutorJobSpecs(
sortedPool := slices.Clone(chainCfg.NOPAliases)
slices.Sort(sortedPool)
chainCfgs[chainSelectorStr] = executor.ChainConfiguration{
OffRampAddress: adapterCfg.OffRampAddress,
RmnAddress: adapterCfg.RmnAddress,
DestinationChainConfig: chainaccess.DestinationChainConfig{
OffRampAddress: adapterCfg.OffRampAddress,
RmnAddress: adapterCfg.RmnAddress,
},
DefaultExecutorAddress: adapterCfg.DefaultExecutorAddress,
ExecutorPool: sortedPool,
ExecutionInterval: chainCfg.ExecutionInterval,
Expand Down
11 changes: 7 additions & 4 deletions evm/executor_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import (
"fmt"

chainsel "github.com/smartcontractkit/chain-selectors"
dsutils "github.com/smartcontractkit/chainlink-ccip/deployment/utils/datastore"
rmnremote "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_6_0/operations/rmn_remote"
execop "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/operations/executor"
offrampop "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/operations/offramp"
rmnremote "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_6_0/operations/rmn_remote"
"github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/sequences"
dsutils "github.com/smartcontractkit/chainlink-ccip/deployment/utils/datastore"
"github.com/smartcontractkit/chainlink-ccv/pkg/chainaccess"
"github.com/smartcontractkit/chainlink-deployments-framework/datastore"

"github.com/smartcontractkit/chainlink-ccv/executor"
Expand Down Expand Up @@ -69,8 +70,10 @@ func (a *evmExecutorConfigAdapter) BuildChainConfig(ds datastore.DataStore, chai
}

return executor.ChainConfiguration{
OffRampAddress: offRampAddr,
RmnAddress: rmnRemoteAddr,
DestinationChainConfig: chainaccess.DestinationChainConfig{
OffRampAddress: offRampAddr,
RmnAddress: rmnRemoteAddr,
},
DefaultExecutorAddress: executorAddr,
}, nil
}
10 changes: 5 additions & 5 deletions executor/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,11 @@ type Configuration struct {
// ChainConfiguration is all the configuration an executor needs to know about a specific chain.
// This is separate from chain-specific RPC information in BlockchainInfos.
type ChainConfiguration struct {
// RMN address is the address of the RMN contract to check for curse state.
RmnAddress string `toml:"rmn_address"`
// OffRamp address is the address of the offramp contract to send messages to.
OffRampAddress string `toml:"off_ramp_address"`
// Executor pool is the list of executor IDs used for turn taking. This executor's ID must be in the list.
// DestinationChainConfig holds the off-ramp and RMN addresses. It is embedded so that the
// TOML field paths (off_ramp_address, rmn_address) are identical to what the chainaccess
// Registry reads via ExecutorConfig, allowing both to overlay the same config file.
chainaccess.DestinationChainConfig
// ExecutorPool is the list of executor IDs used for turn taking. This executor's ID must be in the list.
ExecutorPool []string `toml:"executor_pool"`
// ExecutionInterval is how long each executor has to process a message before the next executor in the cluster takes over.
ExecutionInterval time.Duration `toml:"execution_interval"`
Expand Down
8 changes: 6 additions & 2 deletions executor/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import (
"time"

"github.com/stretchr/testify/require"

"github.com/smartcontractkit/chainlink-ccv/pkg/chainaccess"
)

func validChainConfig() ChainConfiguration {
return ChainConfiguration{
RmnAddress: "0x1234567890abcdef",
OffRampAddress: "0xabcdef1234567890",
DestinationChainConfig: chainaccess.DestinationChainConfig{
RmnAddress: "0x1234567890abcdef",
OffRampAddress: "0xabcdef1234567890",
},
DefaultExecutorAddress: "0xdeadbeef12345678",
ExecutorPool: []string{"executor-1", "executor-2"},
}
Expand Down
Loading
Loading