Skip to content
Open
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
eb68e8d
Simplifies std cap job spec
vyzaldysanchez Jun 29, 2026
48089e3
Merge branch 'develop' into task/CRE-1775/simplify-job-spec
vyzaldysanchez Aug 19, 2026
089aa75
Fixes CI
vyzaldysanchez Aug 19, 2026
2fda0d0
Updates
vyzaldysanchez Aug 19, 2026
94438ec
Updates
vyzaldysanchez Aug 20, 2026
88da669
Fixes lint
vyzaldysanchez Aug 20, 2026
013623e
Fixes lint
vyzaldysanchez Aug 21, 2026
c3b2a1f
Fixes CI
vyzaldysanchez Aug 21, 2026
1a0e496
Updates
vyzaldysanchez Aug 25, 2026
1a20801
Updates
vyzaldysanchez Aug 25, 2026
3e207b6
Updates
vyzaldysanchez Aug 26, 2026
cdc0bff
Fixes lint
vyzaldysanchez Aug 26, 2026
546ffba
Merge remote-tracking branch 'origin/develop' into task/CRE-1775/simp…
vyzaldysanchez Aug 26, 2026
244a80e
Merge remote-tracking branch 'origin/develop' into task/CRE-1775/simp…
vyzaldysanchez Aug 26, 2026
47cecf4
Fixes tests
vyzaldysanchez Aug 26, 2026
76a7c06
Fixes tests
vyzaldysanchez Aug 26, 2026
081323d
Merge remote-tracking branch 'origin/develop' into task/CRE-1775/simp…
vyzaldysanchez Aug 26, 2026
1911ac6
Renames var
vyzaldysanchez Aug 26, 2026
32b4d8a
Fixes tests
vyzaldysanchez Aug 26, 2026
17f83b5
Trigger fresh CI run
vyzaldysanchez Aug 26, 2026
cba450a
Fixes tests
vyzaldysanchez Aug 27, 2026
c5aba15
Adds tests
vyzaldysanchez Aug 27, 2026
5f6e5c6
Adds tests
vyzaldysanchez Aug 27, 2026
87c7df6
Adds implementation
vyzaldysanchez Aug 27, 2026
851861d
Updates refactor
vyzaldysanchez Aug 27, 2026
379f8b3
Updates refactor
vyzaldysanchez Sep 1, 2026
a1fa718
Updates refactor
vyzaldysanchez Sep 1, 2026
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
32 changes: 29 additions & 3 deletions core/capabilities/localcapmgr/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import (
"sync"
"time"

ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types"

capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb"
"github.com/smartcontractkit/chainlink-common/pkg/logger"
"github.com/smartcontractkit/chainlink-common/pkg/services"
"github.com/smartcontractkit/chainlink/v2/core/config"
Expand Down Expand Up @@ -65,7 +68,9 @@ type localCapabilityManager struct {
// Wraps standardcapabilities.Delegate.NewServices to avoid direct dependency on the Delegate.
// donID is the authoritative on-chain DON ID this plugin process is being spawned for; it is
// known here because Reconcile keys desired state by (capID, donID).
type NewServicesFn func(ctx context.Context, capID string, donID uint32, command string, configJSON string) ([]job.ServiceCtx, error)
// ocr3Config is the on-chain OCR3 config parsed from the capability configuration, or nil when
// none is present; it lets the delegate align the node's signer/transmitter with the registry.
type NewServicesFn func(ctx context.Context, capID string, donID uint32, command string, configJSON string, ocr3Config *ocrtypes.ContractConfig) ([]job.ServiceCtx, error)

func NewLocalCapabilityManager(lggr logger.Logger, localCfg config.LocalCapabilities, newServicesFn NewServicesFn) (LocalCapabilityManager, error) {
metrics, err := newMetrics()
Expand Down Expand Up @@ -208,8 +213,9 @@ func (m *localCapabilityManager) startCapability(ctx context.Context, info *capa
return nil, fmt.Errorf("build config for %s: %w", info.capID, err)
}

// TODO(CRE-1775): pass also Ocr3Configs and OracleFactoryConfigs if present onchain
svcs, err := m.newServicesFn(ctx, info.capID, info.donID, command, configJSON)
// TODO(CRE-1775): also derive and pass OracleFactoryConfigs if present onchain.
ocr3Config := extractDefaultOCR3Config(info.config)
svcs, err := m.newServicesFn(ctx, info.capID, info.donID, command, configJSON, ocr3Config)
if err != nil {
return nil, fmt.Errorf("build services for %s: %w", info.capID, err)
}
Expand Down Expand Up @@ -293,6 +299,26 @@ func (m *localCapabilityManager) buildConfigJSON(info *capabilityInfo) (string,
return string(b), nil
}

// extractDefaultOCR3Config returns the `default` on-chain OCR3 config parsed from the
// capability configuration, or nil when the configuration is empty, cannot be parsed,
// or carries no OCR3 config. The delegate uses it to align the node's signer and
// transmitter with the registry.
// By `default,` we mean the config from the registry stored under the "default" key.
func extractDefaultOCR3Config(cc registrysyncer.CapabilityConfiguration) *ocrtypes.ContractConfig {
if len(cc.Config) == 0 {
return nil
}
parsed, err := cc.Unmarshal()
if err != nil {
return nil
}
cfg, ok := parsed.Ocr3Configs[capabilitiespb.OCR3ConfigDefaultKey]
if !ok {
return nil
}
return &cfg
}

func (m *localCapabilityManager) closeServices(rc *runningCapability) error {
return services.MultiCloser(rc.services).Close()
}
Expand Down
42 changes: 40 additions & 2 deletions core/capabilities/localcapmgr/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"

ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types"

"github.com/smartcontractkit/chainlink-common/pkg/capabilities"
capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb"
valuespb "github.com/smartcontractkit/chainlink-protos/cre/go/values/pb"
Expand Down Expand Up @@ -101,11 +103,47 @@ func TestBuildDesiredState_NilLocalConfig(t *testing.T) {
assert.Empty(t, desired, "nil config should not allow any capabilities")
}

func noopServiceBuilder(_ context.Context, _ string, _ uint32, _ string, _ string) ([]job.ServiceCtx, error) {
func TestExtractDefaultOCR3Config(t *testing.T) {
t.Parallel()

t.Run("empty config returns nil", func(t *testing.T) {
t.Parallel()
assert.Nil(t, extractDefaultOCR3Config(registrysyncer.CapabilityConfiguration{}))
})

t.Run("config without OCR3 returns nil", func(t *testing.T) {
t.Parallel()
cc := registrysyncer.CapabilityConfiguration{Config: mustMarshalCapConfig(t, map[string]string{"k": "v"})}
assert.Nil(t, extractDefaultOCR3Config(cc))
})

t.Run("returns default OCR3 config", func(t *testing.T) {
t.Parallel()
raw, err := proto.Marshal(&capabilitiespb.CapabilityConfig{
Ocr3Configs: map[string]*capabilitiespb.OCR3Config{
capabilitiespb.OCR3ConfigDefaultKey: {
Signers: [][]byte{{0x01, 0x02}},
Transmitters: [][]byte{{0xab, 0xcd}},
F: 1,
},
},
})
require.NoError(t, err)

got := extractDefaultOCR3Config(registrysyncer.CapabilityConfiguration{Config: raw})
require.NotNil(t, got)
require.Len(t, got.Signers, 1)
assert.Equal(t, ocrtypes.OnchainPublicKey{0x01, 0x02}, got.Signers[0])
require.Len(t, got.Transmitters, 1)
assert.Equal(t, ocrtypes.Account("abcd"), got.Transmitters[0])
})
}

func noopServiceBuilder(_ context.Context, _ string, _ uint32, _ string, _ string, _ *ocrtypes.ContractConfig) ([]job.ServiceCtx, error) {
return []job.ServiceCtx{&mockService{}}, nil
}

func failingServiceBuilder(_ context.Context, _ string, _ uint32, _ string, _ string) ([]job.ServiceCtx, error) {
func failingServiceBuilder(_ context.Context, _ string, _ uint32, _ string, _ string, _ *ocrtypes.ContractConfig) ([]job.ServiceCtx, error) {
return nil, assert.AnError
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
[capability_configs.consensus]
binary_name = "consensus"

# Global values for consensus are optional - uses empty jobspec config by default
# Global values for consensus are optional - uses empty jobspec config by default.
# Oracle factory contract/chain/bootstrap/signing fields are resolved at runtime:
# - ocr_contract_address, chain_id: Capabilities.ExternalRegistry (CapabilitiesRegistry address/chain)
# - bootstrap_peers: Capabilities.Peering.V2.DefaultBootstrappers
# - ocr_key_bundle_id, transmitter_id, onchainSigningStrategy: node keystore/OCR config
[capability_configs.consensus.values]
# MaxRequestSizeBytes = 200000 # Default: 100000
# RequestBatchSize = 1 # Default: 1
Expand Down
44 changes: 44 additions & 0 deletions core/services/chainlink/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import (
"go.opentelemetry.io/otel/trace"
"go.uber.org/zap/zapcore"

ocrcommontypes "github.com/smartcontractkit/libocr/commontypes"

"github.com/smartcontractkit/chainlink-common/pkg/beholder"
"github.com/smartcontractkit/chainlink-common/pkg/durableemitter"
"github.com/smartcontractkit/chainlink-common/pkg/loop"
Expand Down Expand Up @@ -209,6 +211,33 @@ type ApplicationOpts struct {
DonTimeStore *dontime.Store
}

// safeDefaultBootstrappers returns the configured default bootstrappers from
// Capabilities.Peering.V2, or nil when the config or sub-configs are not set.
func safeDefaultBootstrappers(cfg GeneralConfig) []ocrcommontypes.BootstrapperLocator {
if cfg == nil || cfg.Capabilities() == nil || cfg.Capabilities().Peering() == nil || cfg.Capabilities().Peering().V2() == nil {
return nil
}
return cfg.Capabilities().Peering().V2().DefaultBootstrappers()
}

// safeExternalRegistryAddress returns the Capabilities ExternalRegistry address,
// or empty string when the config or sub-configs are not set.
func safeExternalRegistryAddress(cfg GeneralConfig) string {
if cfg == nil || cfg.Capabilities() == nil || cfg.Capabilities().ExternalRegistry() == nil {
return ""
}
return cfg.Capabilities().ExternalRegistry().Address()
}

// safeExternalRegistryChainID returns the Capabilities ExternalRegistry chain ID,
// or empty string when the config or sub-configs are not set.
func safeExternalRegistryChainID(cfg GeneralConfig) string {
if cfg == nil || cfg.Capabilities() == nil || cfg.Capabilities().ExternalRegistry() == nil {
return ""
}
return cfg.Capabilities().ExternalRegistry().ChainID()
}

// NewApplication initializes a new store if one is not already
// present at the configured root directory (default: ~/.chainlink),
// the logger at the same directory and returns the Application to
Expand Down Expand Up @@ -713,6 +742,9 @@ func NewApplication(ctx context.Context, opts ApplicationOpts) (Application, err
atomicSettings,
creServices.OCRConfigService,
cfg.Capabilities().Local(),
safeDefaultBootstrappers(cfg),
safeExternalRegistryAddress(cfg),
safeExternalRegistryChainID(cfg),
)
delegates[job.StandardCapabilities] = stdcapDelegate
if creServices.SetDelegatesDeps != nil {
Expand Down Expand Up @@ -773,13 +805,25 @@ func NewApplication(ctx context.Context, opts ApplicationOpts) (Application, err
OrgResolver: creServices.OrgResolver,
LimitsFactory: limitsFactory,
OCRConfigService: creServices.OCRConfigService,
DefaultBootstrappers: safeDefaultBootstrappers(cfg),
CapRegistryAddress: safeExternalRegistryAddress(cfg),
CapRegistryChainID: safeExternalRegistryChainID(cfg),
},
ocr2DelegateConfig,
)
if ocr2Delegate == nil {
return nil, errors.New("ocr2.NewDelegate() returned nil")
}
delegates[job.OffchainReporting2] = ocr2Delegate
if creServices.SetOCR2DelegatesDeps != nil {
depSvc, depErr := creServices.SetOCR2DelegatesDeps(ocr2Delegate)
if depErr != nil {
return nil, fmt.Errorf("failed to set CRE OCR2 delegates dependencies: %w", depErr)
}
if depSvc != nil {
srvcs = append(srvcs, depSvc)
}
}
delegates[job.Bootstrap] = ocrbootstrap.NewDelegateBootstrap(
opts.DS,
jobORM,
Expand Down
33 changes: 31 additions & 2 deletions core/services/cre/cre.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"google.golang.org/grpc/credentials"

chainselectors "github.com/smartcontractkit/chain-selectors"
ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types"

"github.com/smartcontractkit/chainlink-common/keystore/corekeys/p2pkey"
"github.com/smartcontractkit/chainlink-common/keystore/corekeys/workflowkey"
Expand Down Expand Up @@ -46,6 +47,7 @@ import (
"github.com/smartcontractkit/chainlink/v2/core/services/job"
"github.com/smartcontractkit/chainlink/v2/core/services/keystore"
"github.com/smartcontractkit/chainlink/v2/core/services/ocr/capregconfig"
ocr "github.com/smartcontractkit/chainlink/v2/core/services/ocr2"
"github.com/smartcontractkit/chainlink/v2/core/services/ocrcommon"
p2pmain "github.com/smartcontractkit/chainlink/v2/core/services/p2p"
p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types"
Expand All @@ -65,6 +67,9 @@ import (
v2 "github.com/smartcontractkit/chainlink/v2/core/services/workflows/v2"
)

// dontimeCapabilityID is the registry capability ID for DONTime.
const dontimeCapabilityID = "dontime@1.0.0"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's be careful and match the prefix "dontime" in case the version changes in the future.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Linter says this is unused?


// Keystore is the minimal interface needed from keystore for CRE
type Keystore interface {
CSA() keystore.CSA
Expand Down Expand Up @@ -124,6 +129,9 @@ type Services struct {

// callback to wire Delegates into CRE services (e.g. Launcher) when ready
SetDelegatesDeps func(*standardcapabilities.Delegate) (commonsrv.Service, error)

// callback to wire OCR2 Delegates into CRE services (e.g. Launcher) when ready
SetOCR2DelegatesDeps func(*ocr.Delegate) (commonsrv.Service, error)
}

func (s *Services) close() error {
Expand Down Expand Up @@ -472,15 +480,36 @@ func (s *Services) newRegistrySyncer(
// callback to wire LocalCapabilityManager into the launcher if local capabilities are configured.
localCfg := cfg.Capabilities().Local()
if localCfg != nil && len(localCfg.RegistryBasedLaunchAllowlist()) > 0 {
// Both delegates are wired into a single LocalCapabilityManager.
// The newServicesFn routes to the correct delegate based on the capability ID:
// - "dontime@1.0.0" → OCR2 delegate (DonTimePlugin)
// - everything else → standard capabilities delegate
s.SetDelegatesDeps = func(stdcapDelegate *standardcapabilities.Delegate) (commonsrv.Service, error) {
newServicesFn := func(ctx context.Context, capID string, donID uint32, command string, configJSON string) ([]job.ServiceCtx, error) {
return stdcapDelegate.NewServices(ctx, command, configJSON, 0, capID, uuid.New(), job.OracleFactoryConfig{}, donID)
// ocr2Delegate will be set by SetOCR2DelegatesDeps later.
var ocr2Delegate *ocr.Delegate

newServicesFn := func(ctx context.Context, capID string, donID uint32, command string, configJSON string, ocr3Config *ocrtypes.ContractConfig) ([]job.ServiceCtx, error) {
if capID == dontimeCapabilityID {
if ocr2Delegate == nil {
return nil, fmt.Errorf("OCR2 delegate not yet initialized for capability %q", capID)
}
return ocr2Delegate.NewServices(ctx, capID, donID, commontypes.DonTimePlugin, configJSON, ocr3Config)
}
return stdcapDelegate.NewServices(ctx, command, configJSON, 0, capID, uuid.New(), nil, donID, ocr3Config)
}

localCapMgr, lcmErr := localcapmgr.NewLocalCapabilityManager(lggr, localCfg, newServicesFn)
if lcmErr != nil {
return nil, fmt.Errorf("could not create local capability manager: %w", lcmErr)
}
wfLauncher.SetLocalCapabilityManager(localCapMgr)

// Store the OCR2 delegate setter so SetOCR2DelegatesDeps can wire it later.
s.SetOCR2DelegatesDeps = func(d *ocr.Delegate) (commonsrv.Service, error) {
ocr2Delegate = d
return nil, nil // already wired into the LocalCapabilityManager above
}

return localCapMgr, nil
}
}
Expand Down
12 changes: 12 additions & 0 deletions core/services/ocr/capregconfig/ocrconfigservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ func NewOCRConfigService(lggr logger.Logger, peerIDProviderFn PeerIDProvider, ch
}
}

// GetContractConfig returns the cached registry-based OCR contract config for the
// given capability/key, if one has been received from the registry.
func (s *ocrConfigService) GetContractConfig(capabilityID string, ocrConfigKey string) (ocrtypes.ContractConfig, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
cached, ok := s.configs[configKey{CapabilityID: capabilityID, OCRConfigKey: ocrConfigKey}]
if !ok || cached == nil {
return ocrtypes.ContractConfig{}, false
}
return cached.ContractConfig, true
}

func (s *ocrConfigService) Start(ctx context.Context) error {
return s.StartOnce("OCRConfigService", func() error {
if s.peerIDProviderFn == nil {
Expand Down
64 changes: 64 additions & 0 deletions core/services/ocr/capregconfig/ocrconfigservice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,70 @@ func TestOCRConfigService_OnNewRegistry(t *testing.T) {
assert.True(t, ok)
}

func TestOCRConfigService_GetContractConfig(t *testing.T) {
t.Parallel()
lggr := logger.Test(t)
svc := NewOCRConfigService(lggr, testPeerIDProvider(), 1, "0x1234567890abcdef")

ctx := t.Context()
require.NoError(t, svc.Start(ctx))
defer svc.Close()

// Before any registry update, GetContractConfig should return false.
_, ok := svc.GetContractConfig("consensus@1.0.0", capabilitiespb.OCR3ConfigDefaultKey)
assert.False(t, ok)

// Add config via registry update.
ocrConfig := &capabilitiespb.OCR3Config{
Signers: [][]byte{[]byte("signer1"), []byte("signer2"), []byte("signer3"), []byte("signer4")},
Transmitters: [][]byte{[]byte("tx1"), []byte("tx2"), []byte("tx3"), []byte("tx4")},
F: 1,
OnchainConfig: []byte("onchain"),
OffchainConfigVersion: 1,
OffchainConfig: []byte("offchain"),
ConfigCount: 5,
}

capConfig := &capabilitiespb.CapabilityConfig{
Ocr3Configs: map[string]*capabilitiespb.OCR3Config{
capabilitiespb.OCR3ConfigDefaultKey: ocrConfig,
},
}

configBytes, err := proto.Marshal(capConfig)
require.NoError(t, err)

don := registrysyncer.DON{
CapabilityConfigurations: map[string]registrysyncer.CapabilityConfiguration{
"consensus@1.0.0": {Config: configBytes},
},
}
don.Members = []ragetypes.PeerID{testPeerID()}

registry := &registrysyncer.LocalRegistry{
Logger: lggr,
IDsToDONs: map[registrysyncer.DonID]registrysyncer.DON{
1: don,
},
IDsToNodes: map[ragetypes.PeerID]registrysyncer.NodeInfo{},
IDsToCapabilities: map[string]registrysyncer.Capability{},
}

err = svc.OnNewRegistry(ctx, registry)
require.NoError(t, err)

// After registry update, GetContractConfig should return the cached config.
got, ok := svc.GetContractConfig("consensus@1.0.0", capabilitiespb.OCR3ConfigDefaultKey)
require.True(t, ok)
assert.Equal(t, uint64(5), got.ConfigCount)
assert.Len(t, got.Signers, 4)
assert.Len(t, got.Transmitters, 4)

// Unknown capability should return false.
_, ok = svc.GetContractConfig("unknown@1.0.0", capabilitiespb.OCR3ConfigDefaultKey)
assert.False(t, ok)
}

func TestOCRConfigService_GetConfigTracker(t *testing.T) {
lggr := logger.Test(t)
svc := NewOCRConfigService(lggr, testPeerIDProvider(), 1, "0x1234567890abcdef")
Expand Down
7 changes: 7 additions & 0 deletions core/services/ocr/capregconfig/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,11 @@ type OCRConfigService interface {
ocrConfigKey string,
legacyDigester ocrtypes.OffchainConfigDigester,
) (ocrtypes.OffchainConfigDigester, error)

// GetContractConfig returns the registry-based OCR contract config cached for the
// specified capability, if available. It exposes the parsed on-chain config
// (signers, transmitters, etc.) so callers can align a node's transmitter and
// signing key with what the registry expects. The bool is false when no
// registry config has been cached yet for the given capability/key.
GetContractConfig(capabilityID string, ocrConfigKey string) (ocrtypes.ContractConfig, bool)
}
Loading
Loading