From 78b976e91adfe69e1733c3e5df5f2ae67b392874 Mon Sep 17 00:00:00 2001 From: Jordan Krage Date: Mon, 28 Sep 2026 14:41:32 +0000 Subject: [PATCH 1/6] core/services/workflows/syncer/v2: close engine feature flags (#23836) * core/services/workflows/syncer/v2: close engine feature flags * core/services/v2: close test limiters --- core/services/workflows/syncer/v2/handler.go | 2 +- core/services/workflows/v2/config.go | 7 +++++++ core/services/workflows/v2/config_test.go | 4 +++- core/services/workflows/v2/engine_test.go | 2 ++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/core/services/workflows/syncer/v2/handler.go b/core/services/workflows/syncer/v2/handler.go index 8fffb25c9f8..f0ffcae60d5 100644 --- a/core/services/workflows/syncer/v2/handler.go +++ b/core/services/workflows/syncer/v2/handler.go @@ -422,7 +422,7 @@ func (h *eventHandler) close() error { // untouched. A spec that is genuinely released stops being snapshotted // (the spec meter sub-service unregisters itself after this hook runs). cs := make([]io.Closer, 0, len(es)+1) - cs = append(cs, h.engineLimiters) + cs = append(cs, h.engineLimiters, h.featureFlags) for _, e := range es { cs = append(cs, e) } diff --git a/core/services/workflows/v2/config.go b/core/services/workflows/v2/config.go index 268c994c56e..bf993076a24 100644 --- a/core/services/workflows/v2/config.go +++ b/core/services/workflows/v2/config.go @@ -403,6 +403,13 @@ func NewFeatureFlags(lf limits.Factory, cfgFn func(*cresettings.Workflows)) (*En }, nil } +func (e *EngineFeatureFlags) Close() error { + if e == nil || e.WorkflowTagBackfill == nil { + return nil + } + return e.WorkflowTagBackfill.Close() +} + const ( defaultHeartbeatFrequencyMs = 1000 * 60 // 1 minute defaultShutdownTimeoutMs = 5000 diff --git a/core/services/workflows/v2/config_test.go b/core/services/workflows/v2/config_test.go index ee32cce2380..c54b3a0467e 100644 --- a/core/services/workflows/v2/config_test.go +++ b/core/services/workflows/v2/config_test.go @@ -71,13 +71,15 @@ func defaultTestConfig(t *testing.T, cfgFn func(*cresettings.Workflows)) *v2.Eng lggr := logger.TestLogger(t) sLimiter, err := syncerlimiter.NewWorkflowLimits(lggr, syncerlimiter.Config{}, lf) require.NoError(t, err) + t.Cleanup(func() { assert.NoError(t, sLimiter.Close()) }) limiters, err := v2.NewLimiters(lf, cfgFn) require.NoError(t, err) + t.Cleanup(func() { assert.NoError(t, limiters.Close()) }) featureFlags, err := v2.NewFeatureFlags(lf, cfgFn) require.NoError(t, err) + t.Cleanup(func() { assert.NoError(t, featureFlags.Close()) }) subscriberMock := capmocks.NewDonSubscriber(t) subscriberMock.EXPECT().Subscribe(matches.AnyContext).Return(make(<-chan commoncaps.DON), func() {}, nil).Maybe() - t.Cleanup(func() { assert.NoError(t, limiters.Close()) }) return &v2.EngineConfig{ Lggr: lggr, diff --git a/core/services/workflows/v2/engine_test.go b/core/services/workflows/v2/engine_test.go index a3b73b6e3b3..f7c32ffd1c7 100644 --- a/core/services/workflows/v2/engine_test.go +++ b/core/services/workflows/v2/engine_test.go @@ -231,6 +231,7 @@ WorkflowLimit = "1" PerOwner: 0, }, limits.Factory{Settings: getter}) require.NoError(t, err) + t.Cleanup(func() { assert.NoError(t, sLimiter.Close()) }) module := modulemocks.NewModuleV2(t) module.EXPECT().Start() @@ -3291,6 +3292,7 @@ func createTestEngineForDonVersionTest( sLimiter, err := syncerlimiter.NewWorkflowLimits(lggr, syncerlimiter.Config{}, lf) require.NoError(t, err) + t.Cleanup(func() { assert.NoError(t, sLimiter.Close()) }) // Use a mock WASM module (only mock we need!) wasmModule := modulemocks.NewModuleV2(t) From e09a3c402d95126c4ad2f9c4c870602c33b45b44 Mon Sep 17 00:00:00 2001 From: Bolek <1416262+bolekk@users.noreply.github.com> Date: Mon, 28 Sep 2026 15:00:17 +0000 Subject: [PATCH 2/6] [CRE-6792] Disallow secrets calls during trigger subscriptions (#23829) --- .../cmd/cre/utils/standalone_engine.go | 2 +- .../workflows/monitoring/monitoring.go | 20 +++ .../workflows/syncer/v2/activation_errors.go | 8 +- .../syncer/v2/activation_errors_test.go | 15 +++ core/services/workflows/v2/base_engine.go | 8 +- .../workflows/v2/coordinated_engine.go | 1 + .../v2/disallowed_execution_helper.go | 27 +++- .../v2/disallowed_execution_helper_test.go | 43 ++++++ core/services/workflows/v2/engine.go | 4 + .../v2/engine_init_failure_metrics_test.go | 122 ++++++++++++++++++ core/services/workflows/v2/engine_test.go | 36 ++++++ core/services/workflows/v2/init_failures.go | 45 +++++++ .../workflows/v2/init_failures_test.go | 90 +++++++++++++ 13 files changed, 407 insertions(+), 14 deletions(-) create mode 100644 core/services/workflows/v2/disallowed_execution_helper_test.go create mode 100644 core/services/workflows/v2/engine_init_failure_metrics_test.go create mode 100644 core/services/workflows/v2/init_failures.go create mode 100644 core/services/workflows/v2/init_failures_test.go diff --git a/core/services/workflows/cmd/cre/utils/standalone_engine.go b/core/services/workflows/cmd/cre/utils/standalone_engine.go index 2d8ab448468..0ce6df6a408 100644 --- a/core/services/workflows/cmd/cre/utils/standalone_engine.go +++ b/core/services/workflows/cmd/cre/utils/standalone_engine.go @@ -176,7 +176,7 @@ func NewStandaloneEngine( Request: &sdkpb.ExecuteRequest_Subscribe{}, MaxResponseSize: uint64(moduleExecuteMaxResponseSizeBytes), Config: config, - }, v2.NewDisallowedExecutionHelper(lggr, nil, &types.LocalTimeProvider{}, secretsFetcher)) + }, v2.NewDisallowedExecutionHelper(lggr, nil, &types.LocalTimeProvider{})) if err != nil { return nil, nil, fmt.Errorf("failed to execute subscribe: %w", err) } diff --git a/core/services/workflows/monitoring/monitoring.go b/core/services/workflows/monitoring/monitoring.go index 1289af6952d..4ec9c9e2055 100644 --- a/core/services/workflows/monitoring/monitoring.go +++ b/core/services/workflows/monitoring/monitoring.go @@ -31,6 +31,7 @@ type EngineMetrics struct { workflowExecutionLatencyGauge metric.Int64Gauge // ms workflowStepErrorCounter metric.Int64Counter workflowInitializationCounter metric.Int64Counter + workflowInitializationFailureCounter metric.Int64Counter workflowTriggerEventErrorCounter metric.Int64Counter workflowTriggerEventQueueFullCounter metric.Int64Counter @@ -164,6 +165,13 @@ func InitMonitoringResources() (em *EngineMetrics, err error) { return nil, fmt.Errorf("failed to register workflow initialization counter: %w", err) } + em.workflowInitializationFailureCounter, err = beholder.GetMeter().Int64Counter( + "platform_engine_workflow_initialization_failures_total", + metric.WithDescription("Count of failed engine initializations by failure reason")) + if err != nil { + return nil, fmt.Errorf("failed to register workflow initialization failure counter: %w", err) + } + em.workflowStepErrorCounter, err = beholder.GetMeter().Int64Counter("platform_engine_workflow_errors") if err != nil { return nil, fmt.Errorf("failed to register workflow step error counter: %w", err) @@ -633,6 +641,18 @@ func (c WorkflowsMetricLabeler) IncrementWorkflowInitializationCounter(ctx conte c.em.workflowInitializationCounter.Add(ctx, 1, metric.WithAttributes(otelLabels...)) } +// IncrementWorkflowInitializationFailureCounter records one failed engine +// initialization. reason must be a low-cardinality value identifying the +// failing phase; see the initFailure* constants in the engine package. +func (c WorkflowsMetricLabeler) IncrementWorkflowInitializationFailureCounter(ctx context.Context, reason string) { + if reason == "" { + reason = "unknown" + } + otelLabels := beholder.OtelAttributes(c.Labels).AsStringAttributes() + otelLabels = append(otelLabels, attribute.String("reason", reason)) + c.em.workflowInitializationFailureCounter.Add(ctx, 1, metric.WithAttributes(otelLabels...)) +} + func (c WorkflowsMetricLabeler) IncrementWorkflowTriggerEventErrorCounter(ctx context.Context) { otelLabels := beholder.OtelAttributes(c.Labels).AsStringAttributes() c.em.workflowTriggerEventErrorCounter.Add(ctx, 1, metric.WithAttributes(otelLabels...)) diff --git a/core/services/workflows/syncer/v2/activation_errors.go b/core/services/workflows/syncer/v2/activation_errors.go index fb6048603f5..e8078d40601 100644 --- a/core/services/workflows/syncer/v2/activation_errors.go +++ b/core/services/workflows/syncer/v2/activation_errors.go @@ -7,6 +7,7 @@ import ( eventsv2 "github.com/smartcontractkit/chainlink-protos/workflows/go/v2" "github.com/smartcontractkit/chainlink/v2/core/services/workflows/types" + wfv2 "github.com/smartcontractkit/chainlink/v2/core/services/workflows/v2" ) // ActivationRetryPolicy controls how failed workflow activations are retried. @@ -96,7 +97,12 @@ func isPermanentEngineInitError(err error) bool { // cron failure when it can't parse the schedule strings.Contains(msg, "failed to initialize job"), // cron rejection when the schedule fires faster than the allowed minimum - strings.Contains(msg, "maximum fastest cron schedule"): + strings.Contains(msg, "maximum fastest cron schedule"), + // guest attempted a disallowed host call (capability or secrets) during + // the trigger subscription phase; a deterministic user error that can + // never succeed on retry + strings.Contains(msg, wfv2.ErrCapabilityCallDuringSubscription.Error()), + strings.Contains(msg, wfv2.ErrSecretsCallDuringSubscription.Error()): return true } err = errors.Unwrap(err) diff --git a/core/services/workflows/syncer/v2/activation_errors_test.go b/core/services/workflows/syncer/v2/activation_errors_test.go index 3fef6808a3e..cffab58bcbb 100644 --- a/core/services/workflows/syncer/v2/activation_errors_test.go +++ b/core/services/workflows/syncer/v2/activation_errors_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink/v2/core/services/workflows/types" + wfv2 "github.com/smartcontractkit/chainlink/v2/core/services/workflows/v2" ) func Test_classifyActivationError(t *testing.T) { @@ -60,6 +61,20 @@ func Test_classifyActivationError(t *testing.T) { errors.New("[3]InvalidArgument: maximum fastest cron schedule is 30s")), want: ActivationNonRetryable, }, + { + // mirrors the engine's wrapping of the guest error string: the + // host rejects the secrets call, the guest reports it back and + // Subscribe fails + name: "secrets call during trigger subscription is non-retryable", + err: fmt.Errorf("failed to execute subscribe: failed to get secrets for call 1: %s", + wfv2.ErrSecretsCallDuringSubscription.Error()), + want: ActivationNonRetryable, + }, + { + name: "capability call during trigger subscription is non-retryable", + err: fmt.Errorf("failed to execute subscribe: %s", wfv2.ErrCapabilityCallDuringSubscription.Error()), + want: ActivationNonRetryable, + }, { name: "unknown error is retryable", err: errors.New("unexpected engine failure"), diff --git a/core/services/workflows/v2/base_engine.go b/core/services/workflows/v2/base_engine.go index d5879177a70..7bf11b8b288 100644 --- a/core/services/workflows/v2/base_engine.go +++ b/core/services/workflows/v2/base_engine.go @@ -250,7 +250,7 @@ func (e *baseEngine) Subscribe(ctx context.Context) ([]*sdkpb.TriggerSubscriptio Request: &sdkpb.ExecuteRequest_Subscribe{}, MaxResponseSize: uint64(moduleExecuteMaxResponseSizeBytes), Config: e.cfg.WorkflowConfig, - }, NewDisallowedExecutionHelper(e.logger(), userLogChan, timeProvider, e.secretsFetcher(e.cfg.WorkflowID))) + }, NewDisallowedExecutionHelper(e.logger(), userLogChan, timeProvider)) if err != nil { return nil, fmt.Errorf("failed to execute subscribe: %w", err) } @@ -766,7 +766,7 @@ func (e *baseEngine) startExecution(ctx context.Context, event triggers.Coordina return nil } -func (e *baseEngine) secretsFetcher(phaseID string) SecretsFetcher { +func (e *baseEngine) secretsFetcher(executionID string) SecretsFetcher { if e.cfg.SecretsFetcher != nil { return e.cfg.SecretsFetcher } @@ -782,9 +782,7 @@ func (e *baseEngine) secretsFetcher(phaseID string) SecretsFetcher { e.cfg.WorkflowOwner, e.cfg.WorkflowName.String(), e.cfg.WorkflowID, - // phaseID is the executionID if called during an execution, - // or the workflowID if called during trigger subscription - phaseID, + executionID, e.cfg.WorkflowEncryptionKey, e.cfg.OverrideFetcher, ) diff --git a/core/services/workflows/v2/coordinated_engine.go b/core/services/workflows/v2/coordinated_engine.go index d4079ae13a5..c84b9d4c10e 100644 --- a/core/services/workflows/v2/coordinated_engine.go +++ b/core/services/workflows/v2/coordinated_engine.go @@ -107,6 +107,7 @@ func (e *coordinatedEngine) init(ctx context.Context) { defer span.End() if err := e.base.initDONSubscribe(ctx); err != nil { + e.base.metrics.IncrementWorkflowInitializationFailureCounter(ctx, initFailureDONSubscribe) e.base.cfg.Hooks.OnInitialized(err) return } diff --git a/core/services/workflows/v2/disallowed_execution_helper.go b/core/services/workflows/v2/disallowed_execution_helper.go index 054bbab6b6a..08c3ada5a9d 100644 --- a/core/services/workflows/v2/disallowed_execution_helper.go +++ b/core/services/workflows/v2/disallowed_execution_helper.go @@ -16,22 +16,35 @@ type disallowedExecutionHelper struct { lggr logger.Logger UserLogChan chan<- *protoevents.LogLine TimeProvider - SecretsFetcher } -func NewDisallowedExecutionHelper(lggr logger.Logger, userLogChan chan<- *protoevents.LogLine, timeProvider TimeProvider, secretsFetcher SecretsFetcher) *disallowedExecutionHelper { +func NewDisallowedExecutionHelper(lggr logger.Logger, userLogChan chan<- *protoevents.LogLine, timeProvider TimeProvider) *disallowedExecutionHelper { return &disallowedExecutionHelper{ - lggr: lggr, - UserLogChan: userLogChan, - TimeProvider: timeProvider, - SecretsFetcher: secretsFetcher, + lggr: lggr, + UserLogChan: userLogChan, + TimeProvider: timeProvider, } } var _ host.ExecutionHelper = &disallowedExecutionHelper{} +var ( + // ErrCapabilityCallDuringSubscription is returned to a guest that attempts + // a capability call during the trigger subscription phase. Capability + // calls are only allowed during workflow executions. + ErrCapabilityCallDuringSubscription = errors.New("capability calls cannot be made during trigger subscription") + // ErrSecretsCallDuringSubscription is returned to a guest that attempts a + // secrets call during the trigger subscription phase. Secrets calls are + // only allowed during workflow executions. + ErrSecretsCallDuringSubscription = errors.New("secrets calls cannot be made during trigger subscription") +) + func (d disallowedExecutionHelper) CallCapability(_ context.Context, _ *sdkpb.CapabilityRequest) (*sdkpb.CapabilityResponse, error) { - return nil, errors.New("capability calls cannot be made during this execution") + return nil, ErrCapabilityCallDuringSubscription +} + +func (d disallowedExecutionHelper) GetSecrets(_ context.Context, _ *sdkpb.GetSecretsRequest) ([]*sdkpb.SecretResponse, error) { + return nil, ErrSecretsCallDuringSubscription } func (d disallowedExecutionHelper) GetWorkflowExecutionID() string { diff --git a/core/services/workflows/v2/disallowed_execution_helper_test.go b/core/services/workflows/v2/disallowed_execution_helper_test.go new file mode 100644 index 00000000000..bcb63131639 --- /dev/null +++ b/core/services/workflows/v2/disallowed_execution_helper_test.go @@ -0,0 +1,43 @@ +package v2 + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + sdkpb "github.com/smartcontractkit/chainlink-protos/cre/go/sdk" + protoevents "github.com/smartcontractkit/chainlink-protos/workflows/go/events" + eventsv2 "github.com/smartcontractkit/chainlink-protos/workflows/go/v2" + "github.com/smartcontractkit/chainlink/v2/core/services/workflows/types" +) + +func TestDisallowedExecutionHelper_GetSecretsReturnsError(t *testing.T) { + t.Parallel() + + h := NewDisallowedExecutionHelper(logger.Test(t), make(chan<- *protoevents.LogLine), &types.LocalTimeProvider{}) + + _, err := h.GetSecrets(t.Context(), &sdkpb.GetSecretsRequest{}) + require.ErrorIs(t, err, ErrSecretsCallDuringSubscription) + require.ErrorContains(t, err, "secrets calls cannot be made during trigger subscription") +} + +func TestDisallowedExecutionHelper_CallCapabilityReturnsError(t *testing.T) { + t.Parallel() + + h := NewDisallowedExecutionHelper(logger.Test(t), make(chan<- *protoevents.LogLine), &types.LocalTimeProvider{}) + + _, err := h.CallCapability(context.Background(), &sdkpb.CapabilityRequest{}) + require.ErrorIs(t, err, ErrCapabilityCallDuringSubscription) + require.ErrorContains(t, err, "capability calls cannot be made during trigger subscription") +} + +func TestDisallowedExecutionHelper_OtherHelpers(t *testing.T) { + t.Parallel() + + h := NewDisallowedExecutionHelper(logger.Test(t), make(chan<- *protoevents.LogLine), &types.LocalTimeProvider{}) + + require.Empty(t, h.GetWorkflowExecutionID()) + require.Error(t, h.EmitUserMetric(context.Background(), &eventsv2.WorkflowUserMetric{})) +} diff --git a/core/services/workflows/v2/engine.go b/core/services/workflows/v2/engine.go index 4b1d4a33990..4ee09347ee7 100644 --- a/core/services/workflows/v2/engine.go +++ b/core/services/workflows/v2/engine.go @@ -138,10 +138,12 @@ func (e *engine) init(ctx context.Context) { defer span.End() if err := e.useWorkflowLimit(ctx); err != nil { + e.base.metrics.IncrementWorkflowInitializationFailureCounter(ctx, initFailureWorkflowLimit) e.base.cfg.Hooks.OnInitialized(err) return } if err := e.base.initDONSubscribe(ctx); err != nil { + e.base.metrics.IncrementWorkflowInitializationFailureCounter(ctx, initFailureDONSubscribe) e.base.cfg.Hooks.OnInitialized(err) return } @@ -149,11 +151,13 @@ func (e *engine) init(ctx context.Context) { subscriptions, err := e.base.Subscribe(ctx) if err != nil { e.base.logger().Errorw("failed to subscribe to triggers", "err", err) + e.base.metrics.IncrementWorkflowInitializationFailureCounter(ctx, initFailureReasonForSubscribe(err)) e.base.cfg.Hooks.OnInitialized(err) return } if err := e.runTriggerSubscriptionPhase(ctx, subscriptions); err != nil { e.base.logger().Errorw("Workflow Engine initialization failed", "err", err) + e.base.metrics.IncrementWorkflowInitializationFailureCounter(ctx, initFailureTriggerRegistration) e.base.cfg.Hooks.OnInitialized(err) return } diff --git a/core/services/workflows/v2/engine_init_failure_metrics_test.go b/core/services/workflows/v2/engine_init_failure_metrics_test.go new file mode 100644 index 00000000000..a9e2f60c271 --- /dev/null +++ b/core/services/workflows/v2/engine_init_failure_metrics_test.go @@ -0,0 +1,122 @@ +package v2_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" + + "github.com/smartcontractkit/chainlink-common/pkg/beholder" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services/servicetest" + regmocks "github.com/smartcontractkit/chainlink-common/pkg/types/core/mocks" + modulemocks "github.com/smartcontractkit/chainlink-common/pkg/workflows/wasm/host/mocks" + sdkpb "github.com/smartcontractkit/chainlink-protos/cre/go/sdk" + v2 "github.com/smartcontractkit/chainlink/v2/core/services/workflows/v2" + "github.com/smartcontractkit/chainlink/v2/core/utils/matches" +) + +// setupInitFailureTestMeter swaps the global beholder client for one backed +// by a manual meter reader, so the engine's counters can be collected. Mirrors +// setupTestMeter in the internal test package. +// +// The swap is global: parallel full-engine tests may register their counters +// on this meter too, so assertions here must tolerate foreign increments +// (reason-unique labels and lower bounds, not exact totals). +func setupInitFailureTestMeter(t *testing.T) *sdkmetric.ManualReader { + t.Helper() + + reader := sdkmetric.NewManualReader() + mp := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader)) + t.Cleanup(func() { _ = mp.Shutdown(context.Background()) }) + + prevClient := beholder.GetClient() + t.Cleanup(func() { beholder.SetClient(prevClient) }) + + client := beholder.NoopClientConfig{Lggr: logger.Test(t)}.New() + client.Meter = mp.Meter("beholder") + client.MeterProvider = mp + beholder.SetClient(client) + + return reader +} + +// collectInitFailureCounter sums the named counter and returns the values +// observed for the "reason" attribute. +func collectInitFailureCounter(t *testing.T, reader *sdkmetric.ManualReader, name string) (int64, []string) { + t.Helper() + + var rm metricdata.ResourceMetrics + require.NoError(t, reader.Collect(context.Background(), &rm)) + + var total int64 + var reasons []string + for _, sm := range rm.ScopeMetrics { + for _, m := range sm.Metrics { + if m.Name != name { + continue + } + data, ok := m.Data.(metricdata.Sum[int64]) + require.True(t, ok, "expected Sum[int64] for %s, got %T", name, m.Data) + for _, dp := range data.DataPoints { + total += dp.Value + if r, ok := dp.Attributes.Value("reason"); ok { + reasons = append(reasons, r.AsString()) + } + } + } + } + return total, reasons +} + +// TestEngine_InitFailureCounter_DisallowedSecretsCall verifies end to end +// that a module failing the Subscribe phase by attempting a secrets call +// during trigger subscription fails engine initialization and increments the +// initialization failure counter with the disallowed-secrets reason. +// +//nolint:paralleltest // swaps the global beholder client +func TestEngine_InitFailureCounter_DisallowedSecretsCall(t *testing.T) { + reader := setupInitFailureTestMeter(t) + + module := modulemocks.NewModuleV2(t) + capreg := regmocks.NewCapabilitiesRegistry(t) + capreg.EXPECT().LocalNode(matches.AnyContext).Return(newNode(t), nil) + + initDoneCh := make(chan error, 1) + + cfg := defaultTestConfig(t, nil) + cfg.Module = module + cfg.CapRegistry = capreg + cfg.Hooks = v2.LifecycleHooks{ + OnInitialized: func(err error) { + initDoneCh <- err + }, + } + + engine, err := v2.NewEngine(cfg) + require.NoError(t, err) + + module.EXPECT().Start().Once() + module.EXPECT().Close().Once() + // the module reports the host's secrets rejection back as its error result + module.EXPECT().Execute(matches.AnyContext, mock.Anything, mock.Anything). + Return(&sdkpb.ExecutionResult{ + Result: &sdkpb.ExecutionResult_Error{ + Error: "failed to get secrets for call 1: secrets calls cannot be made during trigger subscription", + }, + }, nil).Once() + + servicetest.Run(t, engine) + + initErr := <-initDoneCh + require.ErrorContains(t, initErr, "secrets calls cannot be made during trigger subscription") + + // count may include foreign increments from parallel engine tests sharing + // the swapped meter; the disallowed-secrets reason is unique to this test + gotCount, gotReasons := collectInitFailureCounter(t, reader, "platform_engine_workflow_initialization_failures_total") + require.GreaterOrEqual(t, gotCount, int64(1)) + require.Contains(t, gotReasons, "disallowed_secrets_call_during_subscription") +} diff --git a/core/services/workflows/v2/engine_test.go b/core/services/workflows/v2/engine_test.go index f7c32ffd1c7..d15b3eb0991 100644 --- a/core/services/workflows/v2/engine_test.go +++ b/core/services/workflows/v2/engine_test.go @@ -433,6 +433,42 @@ func TestEngine_TriggerSubscriptions(t *testing.T) { }) } +func TestEngine_TriggerSubscriptionPhase_DisallowsSecretsCalls(t *testing.T) { + t.Parallel() + + module := modulemocks.NewModuleV2(t) + capreg := regmocks.NewCapabilitiesRegistry(t) + capreg.EXPECT().LocalNode(matches.AnyContext).Return(newNode(t), nil) + + initDoneCh := make(chan error) + + cfg := defaultTestConfig(t, nil) + cfg.Module = module + cfg.CapRegistry = capreg + cfg.Hooks = v2.LifecycleHooks{ + OnInitialized: func(err error) { + initDoneCh <- err + }, + } + + engine, err := v2.NewEngine(cfg) + require.NoError(t, err) + + module.EXPECT().Start().Once() + module.EXPECT().Execute(matches.AnyContext, mock.Anything, mock.Anything). + Run(func(_ context.Context, _ *sdkpb.ExecuteRequest, helper host.ExecutionHelper) { + _, err := helper.GetSecrets(context.Background(), &sdkpb.GetSecretsRequest{}) + assert.ErrorContains(t, err, "secrets calls cannot be made during trigger subscription") + }). + Return(newTriggerSubs(0), nil).Once() + require.NoError(t, engine.Start(t.Context())) + + require.NoError(t, <-initDoneCh) + + module.EXPECT().Close().Once() + require.NoError(t, engine.Close()) +} + func TestEngine_TriggerRegistrationLogging(t *testing.T) { t.Parallel() diff --git a/core/services/workflows/v2/init_failures.go b/core/services/workflows/v2/init_failures.go new file mode 100644 index 00000000000..caa91bd0061 --- /dev/null +++ b/core/services/workflows/v2/init_failures.go @@ -0,0 +1,45 @@ +package v2 + +import "strings" + +// Engine initialization failure reasons labeling the +// platform_engine_workflow_initialization_failures_total counter. Values must +// stay low-cardinality: these are metric labels, not logs. +const ( + // initFailureWorkflowLimit labels the node's workflow-count limit being + // reached before initialization began. + initFailureWorkflowLimit = "workflow_limit" + // initFailureDONSubscribe labels a failure subscribing to DON + // configuration notifications. + initFailureDONSubscribe = "don_subscribe" + // initFailureSubscribe labels the module's Subscribe (trigger + // subscription) request failing for any reason not classified below. + initFailureSubscribe = "subscribe" + // initFailureTriggerRegistration labels a failure registering the + // module's trigger subscriptions with the capabilities registry. + initFailureTriggerRegistration = "trigger_registration" + // initFailureDisallowedCapabilityCall labels a module that attempted a + // capability call during the trigger subscription phase. + initFailureDisallowedCapabilityCall = "disallowed_capability_call_during_subscription" + // initFailureDisallowedSecretsCall labels a module that attempted a + // secrets call during the trigger subscription phase. + initFailureDisallowedSecretsCall = "disallowed_secrets_call_during_subscription" +) + +// initFailureReasonForSubscribe classifies a failed Subscribe phase for the +// initialization failure counter. The disallowed-host-call rejections cross +// the WASM boundary as plain strings (the guest re-reports the host's error +// text), so they are matched by text rather than errors.Is. +func initFailureReasonForSubscribe(err error) string { + if err == nil { + return initFailureSubscribe + } + switch { + case strings.Contains(err.Error(), ErrSecretsCallDuringSubscription.Error()): + return initFailureDisallowedSecretsCall + case strings.Contains(err.Error(), ErrCapabilityCallDuringSubscription.Error()): + return initFailureDisallowedCapabilityCall + default: + return initFailureSubscribe + } +} diff --git a/core/services/workflows/v2/init_failures_test.go b/core/services/workflows/v2/init_failures_test.go new file mode 100644 index 00000000000..f71ba6033fe --- /dev/null +++ b/core/services/workflows/v2/init_failures_test.go @@ -0,0 +1,90 @@ +package v2 + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/metrics" + "github.com/smartcontractkit/chainlink/v2/core/services/workflows/monitoring" +) + +func Test_initFailureReasonForSubscribe(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + err error + want string + }{ + { + name: "nil error is a generic subscribe failure", + err: nil, + want: initFailureSubscribe, + }, + { + // mirrors the chain: host rejects the secrets call, the guest + // re-reports the host's error text, Subscribe wraps it as a string + name: "guest-reported secrets rejection is classified", + err: fmt.Errorf("failed to execute subscribe: failed to get secrets for call 1: %s", ErrSecretsCallDuringSubscription.Error()), + want: initFailureDisallowedSecretsCall, + }, + { + name: "guest-reported capability rejection is classified", + err: fmt.Errorf("failed to execute subscribe: %s", ErrCapabilityCallDuringSubscription.Error()), + want: initFailureDisallowedCapabilityCall, + }, + { + name: "module error is a generic subscribe failure", + err: fmt.Errorf("failed to execute subscribe: %w", errors.New("wasm trap")), + want: initFailureSubscribe, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + require.Equal(t, tt.want, initFailureReasonForSubscribe(tt.err)) + }) + } +} + +// TestEngine_WorkflowInitializationFailureCounter verifies the counter +// records failures with the reason label, defaulting an empty reason to +// "unknown". +// +//nolint:paralleltest // setupTestMeter swaps the global beholder client +func TestEngine_WorkflowInitializationFailureCounter(t *testing.T) { + const counterName = "platform_engine_workflow_initialization_failures_total" + + t.Run("records failures with reason label", func(t *testing.T) { + reader := setupTestMeter(t) + + em, err := monitoring.InitMonitoringResources() + require.NoError(t, err) + labeler := monitoring.NewWorkflowsMetricLabeler(metrics.NewLabeler(), em) + + labeler.IncrementWorkflowInitializationFailureCounter(t.Context(), initFailureDisallowedSecretsCall) + labeler.IncrementWorkflowInitializationFailureCounter(t.Context(), initFailureTriggerRegistration) + labeler.IncrementWorkflowInitializationFailureCounter(t.Context(), "") + + gotCount, gotReasons := collectCounterValue(t, reader, counterName) + require.Equal(t, int64(3), gotCount) + require.ElementsMatch(t, []string{initFailureDisallowedSecretsCall, initFailureTriggerRegistration, "unknown"}, gotReasons) + }) + + t.Run("not incremented on successful initialization", func(t *testing.T) { + reader := setupTestMeter(t) + + em, err := monitoring.InitMonitoringResources() + require.NoError(t, err) + labeler := monitoring.NewWorkflowsMetricLabeler(metrics.NewLabeler(), em) + + labeler.IncrementWorkflowInitializationCounter(t.Context()) + + gotCount, _ := collectCounterValue(t, reader, counterName) + require.Zero(t, gotCount) + }) +} From f5e79c2a0c3c3779b08eb34cc59ca4539440123c Mon Sep 17 00:00:00 2001 From: Krish-vemula <152236536+Krish-vemula@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:49:01 +0000 Subject: [PATCH 3/6] Update tooling to prevent deploying duplicate capabilities across DONs (#23814) * Update tooling to prevent deploying duplicate capabilities across DONs * fmt err * add validation via donFamilies * add toggle for the validation * remove excludeDONName * Scope duplicate capability validation by DON family and enable it by default --- .../v2/changeset/add_capabilities.go | 18 ++- .../v2/changeset/add_capabilities_test.go | 128 +++++++++++++++++- .../sequences/validate_capabilities.go | 87 ++++++++++++ .../v2/changeset/types.go | 32 +++++ .../v2/changeset/update_don.go | 18 ++- .../v2/changeset/update_don_test.go | 98 ++++++++++++++ 6 files changed, 376 insertions(+), 5 deletions(-) create mode 100644 deployment/cre/capabilities_registry/v2/changeset/sequences/validate_capabilities.go diff --git a/deployment/cre/capabilities_registry/v2/changeset/add_capabilities.go b/deployment/cre/capabilities_registry/v2/changeset/add_capabilities.go index da9e46daff9..9e4621bdc0e 100644 --- a/deployment/cre/capabilities_registry/v2/changeset/add_capabilities.go +++ b/deployment/cre/capabilities_registry/v2/changeset/add_capabilities.go @@ -39,11 +39,15 @@ type AddCapabilitiesInput struct { // capability here, the changeset will fail if it cannot read the current config // count from the registry, preventing accidental config count collisions. FirstOCR3ConfigCapabilities map[string][]string `json:"firstOCR3ConfigCapabilities" yaml:"firstOCR3ConfigCapabilities"` + + // SkipDuplicateCapabilityValidation disables the precondition check that rejects assigning + // the same capability to multiple DONs within a DON family. + SkipDuplicateCapabilityValidation bool `json:"skipDuplicateCapabilityValidation,omitempty" yaml:"skipDuplicateCapabilityValidation,omitempty"` } type AddCapabilities struct{} -func (u AddCapabilities) VerifyPreconditions(_ cldf.Environment, config AddCapabilitiesInput) error { +func (u AddCapabilities) VerifyPreconditions(e cldf.Environment, config AddCapabilitiesInput) error { if len(config.DonCapabilityConfigs) == 0 { return errors.New("donCapabilityConfigs must contain at least one DON entry") } @@ -55,6 +59,18 @@ func (u AddCapabilities) VerifyPreconditions(_ cldf.Environment, config AddCapab return fmt.Errorf("donCapabilityConfigs[%q] must contain at least one capability config", donName) } } + + if !config.SkipDuplicateCapabilityValidation { + existingDONs, err := getExistingDONsForPreconditionCheck(e, config.RegistryChainSel, config.RegistryQualifier) + if err != nil { + return err + } + + if err := sequences.ValidateNoDuplicateCapabilitiesAcrossDONs(config.DonCapabilityConfigs, existingDONs); err != nil { + return fmt.Errorf("AddCapabilities precondition failed: %w", err) + } + } + return nil } diff --git a/deployment/cre/capabilities_registry/v2/changeset/add_capabilities_test.go b/deployment/cre/capabilities_registry/v2/changeset/add_capabilities_test.go index e8f21a818ed..a7198073c46 100644 --- a/deployment/cre/capabilities_registry/v2/changeset/add_capabilities_test.go +++ b/deployment/cre/capabilities_registry/v2/changeset/add_capabilities_test.go @@ -78,6 +78,7 @@ func TestAddCapabilities_VerifyPreconditions(t *testing.T) { chainSelector := h.RegistrySelector capCfg := []contracts.CapabilityConfig{{Capability: contracts.Capability{CapabilityID: "cap@1.0.0"}, Config: map[string]any{"k": "v"}}} + anotherCapCfg := []contracts.CapabilityConfig{{Capability: contracts.Capability{CapabilityID: "another-cap@1.0.0"}, Config: map[string]any{"k": "v"}}} // Empty map err := cs.VerifyPreconditions(h.Runtime.Environment(), changeset.AddCapabilitiesInput{ @@ -113,23 +114,144 @@ func TestAddCapabilities_VerifyPreconditions(t *testing.T) { // Valid (single DON) err = cs.VerifyPreconditions(h.Runtime.Environment(), changeset.AddCapabilitiesInput{ RegistryChainSel: chainSelector, - RegistryQualifier: "qual", + RegistryQualifier: test.RegistryQualifier, DonCapabilityConfigs: map[string][]contracts.CapabilityConfig{ "don-1": capCfg, }, }) require.NoError(t, err) - // Valid (multiple DONs) + // Valid (multiple DONs, disjoint capabilities) err = cs.VerifyPreconditions(h.Runtime.Environment(), changeset.AddCapabilitiesInput{ RegistryChainSel: chainSelector, - RegistryQualifier: "qual", + RegistryQualifier: test.RegistryQualifier, + DonCapabilityConfigs: map[string][]contracts.CapabilityConfig{ + "don-1": capCfg, + "don-2": anotherCapCfg, + }, + }) + require.NoError(t, err) + + // Invalid: same capability ID assigned to two different DONs in the input itself + err = cs.VerifyPreconditions(h.Runtime.Environment(), changeset.AddCapabilitiesInput{ + RegistryChainSel: chainSelector, + RegistryQualifier: test.RegistryQualifier, DonCapabilityConfigs: map[string][]contracts.CapabilityConfig{ "don-1": capCfg, "don-2": capCfg, }, }) + require.Error(t, err) + assert.Contains(t, err.Error(), "is assigned to both DON") + + // Invalid: capability already assigned to an existing on-chain DON, being assigned to a new DON + existingCapCfg := []contracts.CapabilityConfig{{Capability: contracts.Capability{CapabilityID: test.TestCapabilityID}, Config: map[string]any{"k": "v"}}} + err = cs.VerifyPreconditions(h.Runtime.Environment(), changeset.AddCapabilitiesInput{ + RegistryChainSel: chainSelector, + RegistryQualifier: test.RegistryQualifier, + DonCapabilityConfigs: map[string][]contracts.CapabilityConfig{ + "don-1": existingCapCfg, + }, + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "already assigned to on-chain DON") + + // Valid: re-assigning a capability to the on-chain DON that already holds it + err = cs.VerifyPreconditions(h.Runtime.Environment(), changeset.AddCapabilitiesInput{ + RegistryChainSel: chainSelector, + RegistryQualifier: test.RegistryQualifier, + DonCapabilityConfigs: map[string][]contracts.CapabilityConfig{ + test.DONName: existingCapCfg, + }, + }) require.NoError(t, err) + + // Valid: duplicate checks are skipped when explicitly opted out (e.g. mainline tiering migrations) + err = cs.VerifyPreconditions(h.Runtime.Environment(), changeset.AddCapabilitiesInput{ + RegistryChainSel: chainSelector, + RegistryQualifier: test.RegistryQualifier, + SkipDuplicateCapabilityValidation: true, + DonCapabilityConfigs: map[string][]contracts.CapabilityConfig{ + "don-1": capCfg, + "don-2": capCfg, + "don-3": existingCapCfg, + }, + }) + require.NoError(t, err) +} + +// The same capability may live on DONs in different families (e.g. zone-a and zone-b), +// but not on two DONs within the same family. +func TestAddCapabilities_VerifyPreconditions_DONFamilyScoping(t *testing.T) { + cs := changeset.AddCapabilities{} + + h := test.NewTestHarness(t) + + // Register a DON in a different family holding the same capability as test.DONName. + otherZoneDON := "test-don-zone-b" + err := h.Runtime.Exec( + runtime.ChangesetTask(changeset.ConfigureCapabilitiesRegistry{}, changeset.ConfigureCapabilitiesRegistryInput{ + ChainSelector: h.RegistrySelector, + CapabilitiesRegistryAddress: h.RegistryAddress.Hex(), + DONs: []changeset.CapabilitiesRegistryNewDONParams{ + { + Name: otherZoneDON, + DonFamilies: []string{"zone-b"}, // harness DON is in "test-family" + Config: map[string]any{"defaultConfig": map[string]any{}}, + CapabilityConfigurations: []changeset.CapabilitiesRegistryCapabilityConfiguration{ + {CapabilityID: test.TestCapabilityID}, + }, + Nodes: h.Don.GetP2PIDs().Strings(), + F: 1, + IsPublic: true, + }, + }, + }), + ) + require.NoError(t, err) + + existingCapCfg := []contracts.CapabilityConfig{{Capability: contracts.Capability{CapabilityID: test.TestCapabilityID}, Config: map[string]any{"k": "v"}}} + + // Valid: the zone-b DON shares no family with the harness DON that already holds the capability. + err = cs.VerifyPreconditions(h.Runtime.Environment(), changeset.AddCapabilitiesInput{ + RegistryChainSel: h.RegistrySelector, + RegistryQualifier: test.RegistryQualifier, + DonCapabilityConfigs: map[string][]contracts.CapabilityConfig{ + otherZoneDON: existingCapCfg, + }, + }) + require.NoError(t, err) + + // Invalid: a DON in the same family as the harness DON cannot also hold the capability. + sameFamilyDON := "test-don-same-family" + err = h.Runtime.Exec( + runtime.ChangesetTask(changeset.ConfigureCapabilitiesRegistry{}, changeset.ConfigureCapabilitiesRegistryInput{ + ChainSelector: h.RegistrySelector, + CapabilitiesRegistryAddress: h.RegistryAddress.Hex(), + DONs: []changeset.CapabilitiesRegistryNewDONParams{ + { + Name: sameFamilyDON, + DonFamilies: []string{"test-family"}, // same family as the harness DON + Config: map[string]any{"defaultConfig": map[string]any{}}, + Nodes: h.Don.GetP2PIDs().Strings(), + F: 1, + IsPublic: true, + }, + }, + }), + ) + require.NoError(t, err) + + err = cs.VerifyPreconditions(h.Runtime.Environment(), changeset.AddCapabilitiesInput{ + RegistryChainSel: h.RegistrySelector, + RegistryQualifier: test.RegistryQualifier, + DonCapabilityConfigs: map[string][]contracts.CapabilityConfig{ + sameFamilyDON: existingCapCfg, + }, + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "already assigned to on-chain DON") + assert.Contains(t, err.Error(), test.DONName) } func addNewCapability(t *testing.T, h *test.Harness, capID string) { diff --git a/deployment/cre/capabilities_registry/v2/changeset/sequences/validate_capabilities.go b/deployment/cre/capabilities_registry/v2/changeset/sequences/validate_capabilities.go new file mode 100644 index 00000000000..c82a32d3ae7 --- /dev/null +++ b/deployment/cre/capabilities_registry/v2/changeset/sequences/validate_capabilities.go @@ -0,0 +1,87 @@ +package sequences + +import ( + "fmt" + "slices" + + capabilities_registry_v2 "github.com/smartcontractkit/chainlink-evm/gethwrappers/workflow/generated/capabilities_registry_wrapper_v2" + + "github.com/smartcontractkit/chainlink/deployment/cre/capabilities_registry/v2/changeset/operations/contracts" +) + +// ValidateNoDuplicateCapabilitiesAcrossDONs ensures a capability is not assigned to two +// different DONs within the same DON family. DONs in disjoint families (e.g. zone-a and +// zone-b) may hold the same capability. +func ValidateNoDuplicateCapabilitiesAcrossDONs( + donCapabilityConfigs map[string][]contracts.CapabilityConfig, + existingDONs []capabilities_registry_v2.CapabilitiesRegistryDONInfo, +) error { + existingByName := make(map[string]capabilities_registry_v2.CapabilitiesRegistryDONInfo, len(existingDONs)) + for _, don := range existingDONs { + existingByName[don.Name] = don + } + + // capID -> DON name it is claimed by, across the input map itself. + claimedByInput := make(map[string]string, len(donCapabilityConfigs)) + for donName, configs := range donCapabilityConfigs { + for _, cfg := range configs { + capID := cfg.Capability.CapabilityID + otherDON, ok := claimedByInput[capID] + if !ok { + claimedByInput[capID] = donName + continue + } + if otherDON == donName { + continue + } + if !donsShareFamily(otherDON, donName, existingByName) { + continue + } + return fmt.Errorf( + "capability %q is assigned to both DON %q and DON %q, which share a DON family; "+ + "a capability can only be assigned to one DON per family", + capID, otherDON, donName) + } + } + + for _, don := range existingDONs { + for _, cfg := range don.CapabilityConfigurations { + donName, ok := claimedByInput[cfg.CapabilityId] + if !ok { + continue + } + if don.Name == donName { + // Re-assigning a capability to the DON that already holds it is not a conflict. + continue + } + if !donsShareFamily(don.Name, donName, existingByName) { + continue + } + return fmt.Errorf( + "capability %q is already assigned to on-chain DON %q, which shares a DON family with DON %q; "+ + "a capability can only be assigned to one DON per family", + cfg.CapabilityId, don.Name, donName) + } + } + + return nil +} + +// donsShareFamily reports whether two DONs share at least one DON family. A DON not found +// on-chain is conservatively treated as sharing a family with every other DON. +func donsShareFamily( + donA, donB string, + existingByName map[string]capabilities_registry_v2.CapabilitiesRegistryDONInfo, +) bool { + a, okA := existingByName[donA] + b, okB := existingByName[donB] + if !okA || !okB { + return true + } + for _, family := range a.DonFamilies { + if slices.Contains(b.DonFamilies, family) { + return true + } + } + return false +} diff --git a/deployment/cre/capabilities_registry/v2/changeset/types.go b/deployment/cre/capabilities_registry/v2/changeset/types.go index 60fcc346bb5..d14a6b8938b 100644 --- a/deployment/cre/capabilities_registry/v2/changeset/types.go +++ b/deployment/cre/capabilities_registry/v2/changeset/types.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" @@ -180,3 +181,34 @@ func (don CapabilitiesRegistryNewDONParams) applyModifiersToCapabilityConfigs(e return capabilityConfigurations, nil } + +// getExistingDONsForPreconditionCheck reads the current on-chain DONs for the capabilities +// registry identified by chainSel/qualifier, so preconditions can check a proposed capability +// assignment against what other DONs already have configured. +func getExistingDONsForPreconditionCheck( + e cldf.Environment, chainSel uint64, qualifier string, +) ([]capabilities_registry_v2.CapabilitiesRegistryDONInfo, error) { + chain, ok := e.BlockChains.EVMChains()[chainSel] + if !ok { + return nil, fmt.Errorf("chain not found for selector %d", chainSel) + } + + registryAddressRef, err := e.DataStore.Addresses().Get(pkg.GetCapRegV2AddressRefKey(chainSel, qualifier)) + if err != nil { + return nil, fmt.Errorf("failed to get registry address: %w", err) + } + + capReg, err := capabilities_registry_v2.NewCapabilitiesRegistry( + common.HexToAddress(registryAddressRef.Address), chain.Client, + ) + if err != nil { + return nil, fmt.Errorf("failed to create CapabilitiesRegistry: %w", err) + } + + existingDONs, err := pkg.GetDONs(&bind.CallOpts{}, capReg) + if err != nil { + return nil, fmt.Errorf("failed to get existing DONs: %w", err) + } + + return existingDONs, nil +} diff --git a/deployment/cre/capabilities_registry/v2/changeset/update_don.go b/deployment/cre/capabilities_registry/v2/changeset/update_don.go index a5b5e9d8e03..e3d910ec158 100644 --- a/deployment/cre/capabilities_registry/v2/changeset/update_don.go +++ b/deployment/cre/capabilities_registry/v2/changeset/update_don.go @@ -62,16 +62,32 @@ type UpdateDONInput struct { // Safe to combine with MergeCapabilityConfigsWithOnChain=true when CapabilityConfigs is empty. Nodes []string `json:"nodes,omitempty" yaml:"nodes,omitempty"` + // SkipDuplicateCapabilityValidation disables the precondition check that rejects assigning + // the same capability to multiple DONs within a DON family. + SkipDuplicateCapabilityValidation bool `json:"skipDuplicateCapabilityValidation,omitempty" yaml:"skipDuplicateCapabilityValidation,omitempty"` + MCMSConfig *crecontracts.MCMSConfig `json:"mcmsConfig" yaml:"mcmsConfig"` } type UpdateDON struct{} -func (u UpdateDON) VerifyPreconditions(_ cldf.Environment, config UpdateDONInput) error { +func (u UpdateDON) VerifyPreconditions(e cldf.Environment, config UpdateDONInput) error { if config.DONName == "" { return errors.New("must provide a non-empty DONName") } + if !config.SkipDuplicateCapabilityValidation { + existingDONs, err := getExistingDONsForPreconditionCheck(e, config.RegistryChainSel, config.RegistryQualifier) + if err != nil { + return err + } + + donCapabilityConfigs := map[string][]contracts.CapabilityConfig{config.DONName: config.CapabilityConfigs} + if err := sequences.ValidateNoDuplicateCapabilitiesAcrossDONs(donCapabilityConfigs, existingDONs); err != nil { + return fmt.Errorf("UpdateDON precondition failed: %w", err) + } + } + return nil } diff --git a/deployment/cre/capabilities_registry/v2/changeset/update_don_test.go b/deployment/cre/capabilities_registry/v2/changeset/update_don_test.go index ad9ca2d7c4d..9727f19994b 100644 --- a/deployment/cre/capabilities_registry/v2/changeset/update_don_test.go +++ b/deployment/cre/capabilities_registry/v2/changeset/update_don_test.go @@ -365,6 +365,104 @@ func TestUpdateDONChangeset_VerifyPreconditions_EmptyName(t *testing.T) { require.ErrorContains(t, err, "must provide a non-empty DONName") } +// A capability already assigned to a different on-chain DON in the same family cannot be +// assigned to another DON in that family. +func TestUpdateDONChangeset_VerifyPreconditions_RejectsCapabilityAlreadyOnAnotherDON(t *testing.T) { + t.Parallel() + fx := setupRegistryForUpdateDON(t, false, false) + + otherDONName := fx.donName + "-other" + err := fx.rt.Exec( + runtime.ChangesetTask(changeset.ConfigureCapabilitiesRegistry{}, changeset.ConfigureCapabilitiesRegistryInput{ + ChainSelector: fx.selector, + CapabilitiesRegistryAddress: fx.address, + DONs: []changeset.CapabilitiesRegistryNewDONParams{ + { + Name: otherDONName, + DonFamilies: []string{"upd-family"}, // same family as fx.donName + Config: map[string]any{"defaultConfig": map[string]any{}}, + Nodes: []string{p2pID1, p2pID2}, + F: 1, + IsPublic: true, + }, + }, + }), + ) + require.NoError(t, err) + + var cs changeset.UpdateDON + err = cs.VerifyPreconditions(fx.rt.Environment(), changeset.UpdateDONInput{ + RegistryQualifier: fx.qualifier, + RegistryChainSel: fx.selector, + DONName: otherDONName, + CapabilityConfigs: []contracts.CapabilityConfig{ + {Capability: contracts.Capability{CapabilityID: fx.capIDs[0]}}, // already assigned to fx.donName + }, + }) + require.Error(t, err) + require.ErrorContains(t, err, "already assigned to on-chain DON") + require.ErrorContains(t, err, fx.donName) + + // Re-applying to the DON that already has the capability is not a conflict. + err = cs.VerifyPreconditions(fx.rt.Environment(), changeset.UpdateDONInput{ + RegistryQualifier: fx.qualifier, + RegistryChainSel: fx.selector, + DONName: fx.donName, + CapabilityConfigs: []contracts.CapabilityConfig{ + {Capability: contracts.Capability{CapabilityID: fx.capIDs[0]}}, + }, + }) + require.NoError(t, err) + + // With validation explicitly skipped (e.g. mainline tiering migrations), the same assignment is allowed. + err = cs.VerifyPreconditions(fx.rt.Environment(), changeset.UpdateDONInput{ + RegistryQualifier: fx.qualifier, + RegistryChainSel: fx.selector, + DONName: otherDONName, + SkipDuplicateCapabilityValidation: true, + CapabilityConfigs: []contracts.CapabilityConfig{ + {Capability: contracts.Capability{CapabilityID: fx.capIDs[0]}}, + }, + }) + require.NoError(t, err) +} + +// The same capability may be assigned to DONs in different families (e.g. zone-a and zone-b). +func TestUpdateDONChangeset_VerifyPreconditions_AllowsCapabilityOnDONInDifferentFamily(t *testing.T) { + t.Parallel() + fx := setupRegistryForUpdateDON(t, false, false) + + otherDONName := fx.donName + "-other-zone" + err := fx.rt.Exec( + runtime.ChangesetTask(changeset.ConfigureCapabilitiesRegistry{}, changeset.ConfigureCapabilitiesRegistryInput{ + ChainSelector: fx.selector, + CapabilitiesRegistryAddress: fx.address, + DONs: []changeset.CapabilitiesRegistryNewDONParams{ + { + Name: otherDONName, + DonFamilies: []string{"other-family"}, // disjoint from fx.donName's "upd-family" + Config: map[string]any{"defaultConfig": map[string]any{}}, + Nodes: []string{p2pID1, p2pID2}, + F: 1, + IsPublic: true, + }, + }, + }), + ) + require.NoError(t, err) + + var cs changeset.UpdateDON + err = cs.VerifyPreconditions(fx.rt.Environment(), changeset.UpdateDONInput{ + RegistryQualifier: fx.qualifier, + RegistryChainSel: fx.selector, + DONName: otherDONName, + CapabilityConfigs: []contracts.CapabilityConfig{ + {Capability: contracts.Capability{CapabilityID: fx.capIDs[0]}}, // already assigned to fx.donName, but in another family + }, + }) + require.NoError(t, err) +} + // Chain not found: Apply should fail early with a clear message. func TestUpdateDONChangeset_ByName_ChainNotFound(t *testing.T) { t.Parallel() From 60425ff1b1c6b3362e67ea4498db7d5408550ff4 Mon Sep 17 00:00:00 2001 From: Prashant Yadav <34992934+prashantkumar1982@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:51:18 +0000 Subject: [PATCH 4/6] chore(cre): run gateway DON on PR image in confidential-workflows mixed-env (#23831) The confidential-workflows mixed-env topology pinned the bootstrap-gateway DON to the baseline (develop) image. The Gateway DON only uses one node per request, so it should run the PR image to exercise the PR's code on the gateway/vault relay path. Co-authored-by: Claude Opus 4.8 --- .../configs/mixed-env-confidential-workflows.toml.tmpl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core/scripts/cre/environment/configs/mixed-env-confidential-workflows.toml.tmpl b/core/scripts/cre/environment/configs/mixed-env-confidential-workflows.toml.tmpl index 191d17150b8..62a7230ff24 100644 --- a/core/scripts/cre/environment/configs/mixed-env-confidential-workflows.toml.tmpl +++ b/core/scripts/cre/environment/configs/mixed-env-confidential-workflows.toml.tmpl @@ -246,12 +246,11 @@ image = "postgres:15.0" port = 13200 - # The bootstrap/gateway node does not run the reporting plugin, so its code - # version does not affect the non-determinism signal. Keep it on develop for stability. + # The Gateway DON uses only 1 node per request, so it should run the PR image, exercise the code in PR. [[nodesets.node_specs]] roles = ["bootstrap", "gateway"] [nodesets.node_specs.node] - image = "${CRE_BASELINE_IMAGE}" + image = "${CRE_PR_IMAGE}" pull_image = ${CRE_PULL_IMAGE} # 5002 is the web API capabilities port for incoming requests # 15002 is the vault port for incoming requests From 3e14778bac0f560743c22b0423244723d44e165e Mon Sep 17 00:00:00 2001 From: Bolek <1416262+bolekk@users.noreply.github.com> Date: Mon, 28 Sep 2026 17:05:10 +0000 Subject: [PATCH 5/6] [CRE-5836] Remove IsLegacyDAG stubs after common pin bump (#23839) Bump chainlink-common to v0.11.2-0.20260928161532-9b1062f5076f, which includes the merged legacy DAG removals (wasm/host #2382, capabilities-CLI codegen #2413, wasm root #2416, sdk root + wasm/pb + YAML DAG parsing #2418). IsLegacyDAG no longer exists on host.ModuleBase, so drop the dead stubs left by #23727: EvictableModule/ConfidentialModule IsLegacyDAG methods, the syncer/v2 test fake, and 2 stub assertions. dominikbraun/graph indirect dep drops with the bump. --- core/scripts/go.mod | 3 +-- core/scripts/go.sum | 6 ++---- core/services/workflows/syncer/v2/evictable_module.go | 4 ---- core/services/workflows/syncer/v2/evictable_module_test.go | 6 ++---- core/services/workflows/v2/confidential_module.go | 5 ++--- core/services/workflows/v2/confidential_module_test.go | 1 - deployment/go.mod | 3 +-- deployment/go.sum | 6 ++---- go.mod | 3 +-- go.sum | 6 ++---- integration-tests/go.mod | 3 +-- integration-tests/go.sum | 6 ++---- integration-tests/load/go.mod | 3 +-- integration-tests/load/go.sum | 6 ++---- system-tests/lib/go.mod | 3 +-- system-tests/lib/go.sum | 6 ++---- system-tests/tests/go.mod | 3 +-- system-tests/tests/go.sum | 6 ++---- 18 files changed, 25 insertions(+), 54 deletions(-) diff --git a/core/scripts/go.mod b/core/scripts/go.mod index 4e6f72719aa..3c44ec151b9 100644 --- a/core/scripts/go.mod +++ b/core/scripts/go.mod @@ -33,7 +33,7 @@ require ( github.com/shopspring/decimal v1.4.0 github.com/smartcontractkit/chain-selectors v1.0.111 github.com/smartcontractkit/chainlink-ccip/chains/evm v0.0.0-20260916222901-720a003dab50 - github.com/smartcontractkit/chainlink-common v0.11.2-0.20260921174556-538f4bf0cb6f + github.com/smartcontractkit/chainlink-common v0.11.2-0.20260928161532-9b1062f5076f github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260903141829-ef07b52a737d github.com/smartcontractkit/chainlink-deployments-framework v0.122.2 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260917132551-2a2b38e8b744 @@ -210,7 +210,6 @@ require ( github.com/docker/docker-credential-helpers v0.9.8 // indirect github.com/docker/go-connections v0.7.0 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/dominikbraun/graph v0.23.0 // indirect github.com/doyensec/safeurl v0.2.5 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/dvsekhvalnov/jose2go v1.7.0 // indirect diff --git a/core/scripts/go.sum b/core/scripts/go.sum index 8da9be8c9a7..2b1cbe117e4 100644 --- a/core/scripts/go.sum +++ b/core/scripts/go.sum @@ -540,8 +540,6 @@ github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQ github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo= -github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= github.com/doyensec/safeurl v0.2.5 h1:kKu0JNQy0tJ8jkDyB5h6Aml9vWWniq+mpoa12EGLcOQ= github.com/doyensec/safeurl v0.2.5/go.mod h1:3H0cgRpPYPSpgxRRn5yGD35Ns/LgGX/BVWSBbzUqXtY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -1685,8 +1683,8 @@ github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260916222901-720a github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260916222901-720a003dab50/go.mod h1:Dm/Abz0vr7F2v9TygUX947q8HIwJEJikDfSdanX1Cic= github.com/smartcontractkit/chainlink-ccv v0.13.1-0.20260918171034-c93b0d2ef3c0 h1:3f+Ijgr0vghawMUAitgFILQpt4KsoYHGv1Zb/YBeeQM= github.com/smartcontractkit/chainlink-ccv v0.13.1-0.20260918171034-c93b0d2ef3c0/go.mod h1:gX7FJzSnGQx4RAlsn2rRW8IQBlEFZYiXFXEEB5jnQgA= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260921174556-538f4bf0cb6f h1:mj37sJTagtZwTVhU7IIxu6/XCGiK740XmcwXihdKtvk= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260921174556-538f4bf0cb6f/go.mod h1:BniOJQOh52lgDDNyfOQJEmQUVQ68txTSeT0Qxwt0zQI= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260928161532-9b1062f5076f h1:gasuIea3r0AIuEkXDQvIw+TclWt184xBRZGwed+fP6I= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260928161532-9b1062f5076f/go.mod h1:2AvwgNr3I6s0Ny1qmovy/stHQIihEv/9g74aNYzQYYo= github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260903141829-ef07b52a737d h1:M9xuiIvdiHtim4nlxCVajyC/Mk63tAg9ESWb/biAhNA= github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260903141829-ef07b52a737d/go.mod h1:rI2njmTlyTvh+xhmORmK6qX3+V1ycDAqGJLu8LS8WpU= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260915184316-2730f1867c92 h1:Z1OIGukDhJD+Gknr5G07Vtml24E7BwKaIQoWzV/SdqA= diff --git a/core/services/workflows/syncer/v2/evictable_module.go b/core/services/workflows/syncer/v2/evictable_module.go index 65eca305478..200d6a1a5a0 100644 --- a/core/services/workflows/syncer/v2/evictable_module.go +++ b/core/services/workflows/syncer/v2/evictable_module.go @@ -220,10 +220,6 @@ func (m *EvictableModule) Close() { } } -func (m *EvictableModule) IsLegacyDAG() bool { - return false -} - func (m *EvictableModule) Execute(ctx context.Context, request *sdkpb.ExecuteRequest, handler host.ExecutionHelper) (*sdkpb.ExecutionResult, error) { // Each loaded module is held behind a refcounted loadedModule. Pinning is // a CAS-conditional refcount increment: it succeeds only if the holder is diff --git a/core/services/workflows/syncer/v2/evictable_module_test.go b/core/services/workflows/syncer/v2/evictable_module_test.go index e4d7e789d5a..abf5fb52ec2 100644 --- a/core/services/workflows/syncer/v2/evictable_module_test.go +++ b/core/services/workflows/syncer/v2/evictable_module_test.go @@ -44,9 +44,8 @@ type fakeModule struct { closeCalls atomic.Int32 } -func (f *fakeModule) Start() {} -func (f *fakeModule) IsLegacyDAG() bool { return false } -func (f *fakeModule) Close() { f.closeCalls.Add(1) } +func (f *fakeModule) Start() {} +func (f *fakeModule) Close() { f.closeCalls.Add(1) } func (f *fakeModule) Execute(_ context.Context, _ *sdkpb.ExecuteRequest, _ host.ExecutionHelper) (*sdkpb.ExecutionResult, error) { return &sdkpb.ExecutionResult{}, nil } @@ -180,7 +179,6 @@ func TestEvictable_DelegatesToInner(t *testing.T) { //nolint:paralleltest // pac result, err := em.Execute(context.Background(), &sdkpb.ExecuteRequest{}, nil) require.NoError(t, err) assert.NotNil(t, result) - assert.False(t, em.IsLegacyDAG()) } func TestEvictable_LastUsedUpdated(t *testing.T) { //nolint:paralleltest // package-level hooks and eviction config diff --git a/core/services/workflows/v2/confidential_module.go b/core/services/workflows/v2/confidential_module.go index 4f64cbd939e..4bb56129078 100644 --- a/core/services/workflows/v2/confidential_module.go +++ b/core/services/workflows/v2/confidential_module.go @@ -150,9 +150,8 @@ func NewConfidentialModule(capRegistry registry.CapabilitiesRegistry, executionH }, nil } -func (m *ConfidentialModule) Start() {} -func (m *ConfidentialModule) Close() {} -func (m *ConfidentialModule) IsLegacyDAG() bool { return false } +func (m *ConfidentialModule) Start() {} +func (m *ConfidentialModule) Close() {} func (m *ConfidentialModule) Execute( ctx context.Context, diff --git a/core/services/workflows/v2/confidential_module_test.go b/core/services/workflows/v2/confidential_module_test.go index e6334ff7f60..60eef3c0c75 100644 --- a/core/services/workflows/v2/confidential_module_test.go +++ b/core/services/workflows/v2/confidential_module_test.go @@ -823,7 +823,6 @@ func TestConfidentialModule_InterfaceMethods(t *testing.T) { // These are no-ops but should not panic. mod.Start() mod.Close() - assert.False(t, mod.IsLegacyDAG()) } func mustNewConfidentialModule(t *testing.T, capRegistry *regmocks.CapabilitiesRegistry, executionHandlers *confidentialrelay.ExecutionHandlers, binaryURL string, binaryHash []byte, workflowID, workflowOwner, workflowName, workflowTag string, enabledGate limits.GateLimiter, lggr logger.Logger) *ConfidentialModule { diff --git a/deployment/go.mod b/deployment/go.mod index a698ba1f784..a5c18580870 100644 --- a/deployment/go.mod +++ b/deployment/go.mod @@ -46,7 +46,7 @@ require ( github.com/smartcontractkit/chainlink-ccip/chains/solana/deployment v0.0.0-20260916222901-720a003dab50 github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260916222901-720a003dab50 github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260916222901-720a003dab50 - github.com/smartcontractkit/chainlink-common v0.11.2-0.20260921174556-538f4bf0cb6f + github.com/smartcontractkit/chainlink-common v0.11.2-0.20260928161532-9b1062f5076f github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260903141829-ef07b52a737d github.com/smartcontractkit/chainlink-data-streams v1.1.1 github.com/smartcontractkit/chainlink-deployments-framework v0.122.2 @@ -219,7 +219,6 @@ require ( github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-connections v0.7.0 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/dominikbraun/graph v0.23.0 // indirect github.com/doyensec/safeurl v0.2.5 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/dvsekhvalnov/jose2go v1.7.0 // indirect diff --git a/deployment/go.sum b/deployment/go.sum index b682ecfd148..524ce8bf8e5 100644 --- a/deployment/go.sum +++ b/deployment/go.sum @@ -454,8 +454,6 @@ github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo= -github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= github.com/doyensec/safeurl v0.2.5 h1:kKu0JNQy0tJ8jkDyB5h6Aml9vWWniq+mpoa12EGLcOQ= github.com/doyensec/safeurl v0.2.5/go.mod h1:3H0cgRpPYPSpgxRRn5yGD35Ns/LgGX/BVWSBbzUqXtY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -1405,8 +1403,8 @@ github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260916222901-720a github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260916222901-720a003dab50/go.mod h1:Dm/Abz0vr7F2v9TygUX947q8HIwJEJikDfSdanX1Cic= github.com/smartcontractkit/chainlink-ccv v0.13.1-0.20260918171034-c93b0d2ef3c0 h1:3f+Ijgr0vghawMUAitgFILQpt4KsoYHGv1Zb/YBeeQM= github.com/smartcontractkit/chainlink-ccv v0.13.1-0.20260918171034-c93b0d2ef3c0/go.mod h1:gX7FJzSnGQx4RAlsn2rRW8IQBlEFZYiXFXEEB5jnQgA= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260921174556-538f4bf0cb6f h1:mj37sJTagtZwTVhU7IIxu6/XCGiK740XmcwXihdKtvk= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260921174556-538f4bf0cb6f/go.mod h1:BniOJQOh52lgDDNyfOQJEmQUVQ68txTSeT0Qxwt0zQI= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260928161532-9b1062f5076f h1:gasuIea3r0AIuEkXDQvIw+TclWt184xBRZGwed+fP6I= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260928161532-9b1062f5076f/go.mod h1:2AvwgNr3I6s0Ny1qmovy/stHQIihEv/9g74aNYzQYYo= github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260903141829-ef07b52a737d h1:M9xuiIvdiHtim4nlxCVajyC/Mk63tAg9ESWb/biAhNA= github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260903141829-ef07b52a737d/go.mod h1:rI2njmTlyTvh+xhmORmK6qX3+V1ycDAqGJLu8LS8WpU= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260915184316-2730f1867c92 h1:Z1OIGukDhJD+Gknr5G07Vtml24E7BwKaIQoWzV/SdqA= diff --git a/go.mod b/go.mod index d83b5706203..73c4825a890 100644 --- a/go.mod +++ b/go.mod @@ -82,7 +82,7 @@ require ( github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260908155618-50f521d70e62 github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260908155618-50f521d70e62 github.com/smartcontractkit/chainlink-ccv v0.13.1-0.20260918171034-c93b0d2ef3c0 - github.com/smartcontractkit/chainlink-common v0.11.2-0.20260921174556-538f4bf0cb6f + github.com/smartcontractkit/chainlink-common v0.11.2-0.20260928161532-9b1062f5076f github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260903141829-ef07b52a737d github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260915184316-2730f1867c92 github.com/smartcontractkit/chainlink-data-streams v1.1.1 @@ -214,7 +214,6 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect github.com/dgraph-io/badger/v4 v4.7.0 // indirect github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect - github.com/dominikbraun/graph v0.23.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/dvsekhvalnov/jose2go v1.7.0 // indirect github.com/emicklei/dot v1.6.2 // indirect diff --git a/go.sum b/go.sum index 9ca84dc2eb3..e067dafc04d 100644 --- a/go.sum +++ b/go.sum @@ -311,8 +311,6 @@ github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo= -github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= github.com/doyensec/safeurl v0.2.5 h1:kKu0JNQy0tJ8jkDyB5h6Aml9vWWniq+mpoa12EGLcOQ= github.com/doyensec/safeurl v0.2.5/go.mod h1:3H0cgRpPYPSpgxRRn5yGD35Ns/LgGX/BVWSBbzUqXtY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -1106,8 +1104,8 @@ github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260 github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260908155618-50f521d70e62/go.mod h1:c++Ed0jeYn4w8WIfiAboRwN6ClRRVGglL0qwB/QjJvM= github.com/smartcontractkit/chainlink-ccv v0.13.1-0.20260918171034-c93b0d2ef3c0 h1:3f+Ijgr0vghawMUAitgFILQpt4KsoYHGv1Zb/YBeeQM= github.com/smartcontractkit/chainlink-ccv v0.13.1-0.20260918171034-c93b0d2ef3c0/go.mod h1:gX7FJzSnGQx4RAlsn2rRW8IQBlEFZYiXFXEEB5jnQgA= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260921174556-538f4bf0cb6f h1:mj37sJTagtZwTVhU7IIxu6/XCGiK740XmcwXihdKtvk= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260921174556-538f4bf0cb6f/go.mod h1:BniOJQOh52lgDDNyfOQJEmQUVQ68txTSeT0Qxwt0zQI= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260928161532-9b1062f5076f h1:gasuIea3r0AIuEkXDQvIw+TclWt184xBRZGwed+fP6I= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260928161532-9b1062f5076f/go.mod h1:2AvwgNr3I6s0Ny1qmovy/stHQIihEv/9g74aNYzQYYo= github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260903141829-ef07b52a737d h1:M9xuiIvdiHtim4nlxCVajyC/Mk63tAg9ESWb/biAhNA= github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260903141829-ef07b52a737d/go.mod h1:rI2njmTlyTvh+xhmORmK6qX3+V1ycDAqGJLu8LS8WpU= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260915184316-2730f1867c92 h1:Z1OIGukDhJD+Gknr5G07Vtml24E7BwKaIQoWzV/SdqA= diff --git a/integration-tests/go.mod b/integration-tests/go.mod index 74ea5146188..f07c1d098d7 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -34,7 +34,7 @@ require ( github.com/smartcontractkit/chainlink-ccip/chains/evm v0.0.0-20260916222901-720a003dab50 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260916222901-720a003dab50 github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260916222901-720a003dab50 - github.com/smartcontractkit/chainlink-common v0.11.2-0.20260921174556-538f4bf0cb6f + github.com/smartcontractkit/chainlink-common v0.11.2-0.20260928161532-9b1062f5076f github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260903141829-ef07b52a737d github.com/smartcontractkit/chainlink-deployments-framework v0.122.2 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260917132551-2a2b38e8b744 @@ -185,7 +185,6 @@ require ( github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-connections v0.7.0 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/dominikbraun/graph v0.23.0 // indirect github.com/doyensec/safeurl v0.2.5 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/dvsekhvalnov/jose2go v1.7.0 // indirect diff --git a/integration-tests/go.sum b/integration-tests/go.sum index f37e92a6dfd..8b7eceeeb22 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -441,8 +441,6 @@ github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo= -github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= github.com/doyensec/safeurl v0.2.5 h1:kKu0JNQy0tJ8jkDyB5h6Aml9vWWniq+mpoa12EGLcOQ= github.com/doyensec/safeurl v0.2.5/go.mod h1:3H0cgRpPYPSpgxRRn5yGD35Ns/LgGX/BVWSBbzUqXtY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -1394,8 +1392,8 @@ github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260916222901-720a github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260916222901-720a003dab50/go.mod h1:Dm/Abz0vr7F2v9TygUX947q8HIwJEJikDfSdanX1Cic= github.com/smartcontractkit/chainlink-ccv v0.13.1-0.20260918171034-c93b0d2ef3c0 h1:3f+Ijgr0vghawMUAitgFILQpt4KsoYHGv1Zb/YBeeQM= github.com/smartcontractkit/chainlink-ccv v0.13.1-0.20260918171034-c93b0d2ef3c0/go.mod h1:gX7FJzSnGQx4RAlsn2rRW8IQBlEFZYiXFXEEB5jnQgA= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260921174556-538f4bf0cb6f h1:mj37sJTagtZwTVhU7IIxu6/XCGiK740XmcwXihdKtvk= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260921174556-538f4bf0cb6f/go.mod h1:BniOJQOh52lgDDNyfOQJEmQUVQ68txTSeT0Qxwt0zQI= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260928161532-9b1062f5076f h1:gasuIea3r0AIuEkXDQvIw+TclWt184xBRZGwed+fP6I= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260928161532-9b1062f5076f/go.mod h1:2AvwgNr3I6s0Ny1qmovy/stHQIihEv/9g74aNYzQYYo= github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260903141829-ef07b52a737d h1:M9xuiIvdiHtim4nlxCVajyC/Mk63tAg9ESWb/biAhNA= github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260903141829-ef07b52a737d/go.mod h1:rI2njmTlyTvh+xhmORmK6qX3+V1ycDAqGJLu8LS8WpU= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260915184316-2730f1867c92 h1:Z1OIGukDhJD+Gknr5G07Vtml24E7BwKaIQoWzV/SdqA= diff --git a/integration-tests/load/go.mod b/integration-tests/load/go.mod index 2aadbe7a5c1..6f29b5d19f7 100644 --- a/integration-tests/load/go.mod +++ b/integration-tests/load/go.mod @@ -24,7 +24,7 @@ require ( github.com/smartcontractkit/chainlink-ccip/chains/evm v0.0.0-20260916222901-720a003dab50 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260916222901-720a003dab50 github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260916222901-720a003dab50 - github.com/smartcontractkit/chainlink-common v0.11.2-0.20260921174556-538f4bf0cb6f + github.com/smartcontractkit/chainlink-common v0.11.2-0.20260928161532-9b1062f5076f github.com/smartcontractkit/chainlink-deployments-framework v0.122.2 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260917132551-2a2b38e8b744 github.com/smartcontractkit/chainlink-testing-framework/framework v0.16.8 @@ -182,7 +182,6 @@ require ( github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-connections v0.7.0 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/dominikbraun/graph v0.23.0 // indirect github.com/doyensec/safeurl v0.2.5 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/dvsekhvalnov/jose2go v1.7.0 // indirect diff --git a/integration-tests/load/go.sum b/integration-tests/load/go.sum index 4df6d217e75..0848f18e14f 100644 --- a/integration-tests/load/go.sum +++ b/integration-tests/load/go.sum @@ -515,8 +515,6 @@ github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo= -github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= github.com/doyensec/safeurl v0.2.5 h1:kKu0JNQy0tJ8jkDyB5h6Aml9vWWniq+mpoa12EGLcOQ= github.com/doyensec/safeurl v0.2.5/go.mod h1:3H0cgRpPYPSpgxRRn5yGD35Ns/LgGX/BVWSBbzUqXtY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -1632,8 +1630,8 @@ github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260916222901-720a github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260916222901-720a003dab50/go.mod h1:Dm/Abz0vr7F2v9TygUX947q8HIwJEJikDfSdanX1Cic= github.com/smartcontractkit/chainlink-ccv v0.13.1-0.20260918171034-c93b0d2ef3c0 h1:3f+Ijgr0vghawMUAitgFILQpt4KsoYHGv1Zb/YBeeQM= github.com/smartcontractkit/chainlink-ccv v0.13.1-0.20260918171034-c93b0d2ef3c0/go.mod h1:gX7FJzSnGQx4RAlsn2rRW8IQBlEFZYiXFXEEB5jnQgA= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260921174556-538f4bf0cb6f h1:mj37sJTagtZwTVhU7IIxu6/XCGiK740XmcwXihdKtvk= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260921174556-538f4bf0cb6f/go.mod h1:BniOJQOh52lgDDNyfOQJEmQUVQ68txTSeT0Qxwt0zQI= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260928161532-9b1062f5076f h1:gasuIea3r0AIuEkXDQvIw+TclWt184xBRZGwed+fP6I= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260928161532-9b1062f5076f/go.mod h1:2AvwgNr3I6s0Ny1qmovy/stHQIihEv/9g74aNYzQYYo= github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260903141829-ef07b52a737d h1:M9xuiIvdiHtim4nlxCVajyC/Mk63tAg9ESWb/biAhNA= github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260903141829-ef07b52a737d/go.mod h1:rI2njmTlyTvh+xhmORmK6qX3+V1ycDAqGJLu8LS8WpU= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260915184316-2730f1867c92 h1:Z1OIGukDhJD+Gknr5G07Vtml24E7BwKaIQoWzV/SdqA= diff --git a/system-tests/lib/go.mod b/system-tests/lib/go.mod index 5f3bb3c7041..1346828d714 100644 --- a/system-tests/lib/go.mod +++ b/system-tests/lib/go.mod @@ -37,7 +37,7 @@ require ( github.com/smartcontractkit/chain-selectors v1.0.111 github.com/smartcontractkit/chainlink-aptos v0.0.0-20260923062809-ca886ade4d26 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260916222901-720a003dab50 - github.com/smartcontractkit/chainlink-common v0.11.2-0.20260921174556-538f4bf0cb6f + github.com/smartcontractkit/chainlink-common v0.11.2-0.20260928161532-9b1062f5076f github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260903141829-ef07b52a737d github.com/smartcontractkit/chainlink-confidential-compute v1.3.0 github.com/smartcontractkit/chainlink-deployments-framework v0.122.2 @@ -219,7 +219,6 @@ require ( github.com/docker/docker-credential-helpers v0.9.8 // indirect github.com/docker/go-connections v0.7.0 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/dominikbraun/graph v0.23.0 // indirect github.com/doyensec/safeurl v0.2.5 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/dvsekhvalnov/jose2go v1.7.0 // indirect diff --git a/system-tests/lib/go.sum b/system-tests/lib/go.sum index e3bf489d441..0bfe752f939 100644 --- a/system-tests/lib/go.sum +++ b/system-tests/lib/go.sum @@ -535,8 +535,6 @@ github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQ github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo= -github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= github.com/doyensec/safeurl v0.2.5 h1:kKu0JNQy0tJ8jkDyB5h6Aml9vWWniq+mpoa12EGLcOQ= github.com/doyensec/safeurl v0.2.5/go.mod h1:3H0cgRpPYPSpgxRRn5yGD35Ns/LgGX/BVWSBbzUqXtY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -1656,8 +1654,8 @@ github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260916222901-720a github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260916222901-720a003dab50/go.mod h1:Dm/Abz0vr7F2v9TygUX947q8HIwJEJikDfSdanX1Cic= github.com/smartcontractkit/chainlink-ccv v0.13.1-0.20260918171034-c93b0d2ef3c0 h1:3f+Ijgr0vghawMUAitgFILQpt4KsoYHGv1Zb/YBeeQM= github.com/smartcontractkit/chainlink-ccv v0.13.1-0.20260918171034-c93b0d2ef3c0/go.mod h1:gX7FJzSnGQx4RAlsn2rRW8IQBlEFZYiXFXEEB5jnQgA= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260921174556-538f4bf0cb6f h1:mj37sJTagtZwTVhU7IIxu6/XCGiK740XmcwXihdKtvk= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260921174556-538f4bf0cb6f/go.mod h1:BniOJQOh52lgDDNyfOQJEmQUVQ68txTSeT0Qxwt0zQI= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260928161532-9b1062f5076f h1:gasuIea3r0AIuEkXDQvIw+TclWt184xBRZGwed+fP6I= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260928161532-9b1062f5076f/go.mod h1:2AvwgNr3I6s0Ny1qmovy/stHQIihEv/9g74aNYzQYYo= github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260903141829-ef07b52a737d h1:M9xuiIvdiHtim4nlxCVajyC/Mk63tAg9ESWb/biAhNA= github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260903141829-ef07b52a737d/go.mod h1:rI2njmTlyTvh+xhmORmK6qX3+V1ycDAqGJLu8LS8WpU= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260915184316-2730f1867c92 h1:Z1OIGukDhJD+Gknr5G07Vtml24E7BwKaIQoWzV/SdqA= diff --git a/system-tests/tests/go.mod b/system-tests/tests/go.mod index 8057161b687..82f34694426 100644 --- a/system-tests/tests/go.mod +++ b/system-tests/tests/go.mod @@ -76,7 +76,7 @@ require ( github.com/smartcontractkit/chain-selectors v1.0.111 github.com/smartcontractkit/chainlink-aptos v0.0.0-20260923062809-ca886ade4d26 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260916222901-720a003dab50 - github.com/smartcontractkit/chainlink-common v0.11.2-0.20260921174556-538f4bf0cb6f + github.com/smartcontractkit/chainlink-common v0.11.2-0.20260928161532-9b1062f5076f github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260903141829-ef07b52a737d github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260915184316-2730f1867c92 github.com/smartcontractkit/chainlink-confidential-compute v1.3.0 @@ -292,7 +292,6 @@ require ( github.com/docker/docker-credential-helpers v0.9.8 // indirect github.com/docker/go-connections v0.7.0 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/dominikbraun/graph v0.23.0 // indirect github.com/doyensec/safeurl v0.2.5 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/dvsekhvalnov/jose2go v1.7.0 // indirect diff --git a/system-tests/tests/go.sum b/system-tests/tests/go.sum index ed6c08db2c8..fc68484ffa6 100644 --- a/system-tests/tests/go.sum +++ b/system-tests/tests/go.sum @@ -593,8 +593,6 @@ github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQ github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo= -github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= github.com/doyensec/safeurl v0.2.5 h1:kKu0JNQy0tJ8jkDyB5h6Aml9vWWniq+mpoa12EGLcOQ= github.com/doyensec/safeurl v0.2.5/go.mod h1:3H0cgRpPYPSpgxRRn5yGD35Ns/LgGX/BVWSBbzUqXtY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -1835,8 +1833,8 @@ github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260916222901-720a github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260916222901-720a003dab50/go.mod h1:Dm/Abz0vr7F2v9TygUX947q8HIwJEJikDfSdanX1Cic= github.com/smartcontractkit/chainlink-ccv v0.13.1-0.20260918171034-c93b0d2ef3c0 h1:3f+Ijgr0vghawMUAitgFILQpt4KsoYHGv1Zb/YBeeQM= github.com/smartcontractkit/chainlink-ccv v0.13.1-0.20260918171034-c93b0d2ef3c0/go.mod h1:gX7FJzSnGQx4RAlsn2rRW8IQBlEFZYiXFXEEB5jnQgA= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260921174556-538f4bf0cb6f h1:mj37sJTagtZwTVhU7IIxu6/XCGiK740XmcwXihdKtvk= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260921174556-538f4bf0cb6f/go.mod h1:BniOJQOh52lgDDNyfOQJEmQUVQ68txTSeT0Qxwt0zQI= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260928161532-9b1062f5076f h1:gasuIea3r0AIuEkXDQvIw+TclWt184xBRZGwed+fP6I= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260928161532-9b1062f5076f/go.mod h1:2AvwgNr3I6s0Ny1qmovy/stHQIihEv/9g74aNYzQYYo= github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260903141829-ef07b52a737d h1:M9xuiIvdiHtim4nlxCVajyC/Mk63tAg9ESWb/biAhNA= github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260903141829-ef07b52a737d/go.mod h1:rI2njmTlyTvh+xhmORmK6qX3+V1ycDAqGJLu8LS8WpU= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260915184316-2730f1867c92 h1:Z1OIGukDhJD+Gknr5G07Vtml24E7BwKaIQoWzV/SdqA= From 929e7ef955d03e11ba7e2c38d2eba4b5aceebf1f Mon Sep 17 00:00:00 2001 From: Bolek <1416262+bolekk@users.noreply.github.com> Date: Mon, 28 Sep 2026 18:21:09 +0000 Subject: [PATCH 6/6] [CRE][Sharding] Fetch workflows from base DON family when family has a "_workflows" suffix (#23833) Fetch workflows from base DON family when family has a "_workflows" suffix Move shardNameMarker into a new shardownership/types package alongside a new WorkflowFamilySuffix constant, and add ExpandWorkflowFamilies so contract/file/GRPC workflow sources also fetch from "" when a DON belongs to family "_workflows". Co-authored-by: Claude Sonnet 5 --- .../shardownership/shard_index_mapper.go | 14 ++---- .../workflows/shardownership/types/types.go | 15 ++++++ .../syncer/v2/contract_workflow_source.go | 2 +- .../syncer/v2/file_workflow_source.go | 2 +- .../syncer/v2/grpc_workflow_source.go | 3 +- core/services/workflows/syncer/v2/helpers.go | 30 ++++++++++++ .../workflows/syncer/v2/helpers_test.go | 49 +++++++++++++++++++ 7 files changed, 103 insertions(+), 12 deletions(-) create mode 100644 core/services/workflows/shardownership/types/types.go create mode 100644 core/services/workflows/syncer/v2/helpers_test.go diff --git a/core/services/workflows/shardownership/shard_index_mapper.go b/core/services/workflows/shardownership/shard_index_mapper.go index ea66dfb8d49..46603654ad1 100644 --- a/core/services/workflows/shardownership/shard_index_mapper.go +++ b/core/services/workflows/shardownership/shard_index_mapper.go @@ -11,13 +11,9 @@ import ( commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry" "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/workflows/shardownership/types" ) -// shardNameMarker is the marker a workflow DON's name is suffixed with to -// encode its shard index (e.g. "workflow-1-zone-a-shard-1" -> shard 1). A -// name with no such suffix (e.g. "workflow-1-zone-a") is shard index 0. -const shardNameMarker = "shard-" - // ShardIndexMapper tracks the workflow DONs currently known to the Capabilities // Registry and detects their shard indices (0, 1, 2, ...). // It's a global singleton object used by ManualShardResolver and, in consequence, @@ -117,13 +113,13 @@ func shardDONsByIndex(reg *registry.RegistryMetadata, localDON commoncap.DON, na // workflow DON's name (e.g. "workflow-1-zone-a-shard-1" -> 1). A name with no // "shard-" suffix is shard index 0. func shardIndexFromName(name string) (uint32, error) { - idx := strings.LastIndex(name, shardNameMarker) + idx := strings.LastIndex(name, types.ShardNameMarker) if idx == -1 { return 0, nil } - suffix := name[idx+len(shardNameMarker):] + suffix := name[idx+len(types.ShardNameMarker):] if len(suffix) != 1 || suffix[0] < '0' || suffix[0] > '9' { - return 0, fmt.Errorf("expected DON name %q to end with %q followed by a single digit, got suffix %q", name, shardNameMarker, suffix) + return 0, fmt.Errorf("expected DON name %q to end with %q followed by a single digit, got suffix %q", name, types.ShardNameMarker, suffix) } return uint32(suffix[0] - '0'), nil } @@ -134,7 +130,7 @@ func shardIndexFromName(name string) (uint32, error) { // "workflow-1-zone-a-shard-1" -> "workflow-1-zone-a"). A name with no such // suffix is returned unchanged. func shardGroupNamePrefix(name string) string { - idx := strings.LastIndex(name, shardNameMarker) + idx := strings.LastIndex(name, types.ShardNameMarker) if idx == -1 { return name } diff --git a/core/services/workflows/shardownership/types/types.go b/core/services/workflows/shardownership/types/types.go new file mode 100644 index 00000000000..cce88707a28 --- /dev/null +++ b/core/services/workflows/shardownership/types/types.go @@ -0,0 +1,15 @@ +package types + +const ( + // ShardNameMarker is the marker a workflow DON's name is suffixed with to + // encode its shard index (e.g. "workflow-1-zone-a-shard-1" -> shard 1). A + // name with no such suffix (e.g. "workflow-1-zone-a") is shard index 0. + ShardNameMarker = "shard-" + + // WorkflowFamilySuffix is the suffix appended to a DON family name to + // denote its dedicated workflow-fetching family. A DON belonging to a + // family named "WorkflowFamilySuffix" (e.g. "zone-a_workflows") + // should also fetch workflows registered under the base family with the + // suffix stripped (e.g. "zone-a"). + WorkflowFamilySuffix = "_workflows" +) diff --git a/core/services/workflows/syncer/v2/contract_workflow_source.go b/core/services/workflows/syncer/v2/contract_workflow_source.go index 6d75fa478fa..8d8317f6474 100644 --- a/core/services/workflows/syncer/v2/contract_workflow_source.go +++ b/core/services/workflows/syncer/v2/contract_workflow_source.go @@ -72,7 +72,7 @@ func (c *ContractWorkflowSource) ListWorkflowMetadata(ctx context.Context, don c var headAtLastRead *commontypes.Head var allWorkflows []WorkflowMetadataView - for _, family := range don.Families { + for _, family := range ExpandWorkflowFamilies(don.Families) { params := GetWorkflowListByDONParams{ DonFamily: family, Start: big.NewInt(0), diff --git a/core/services/workflows/syncer/v2/file_workflow_source.go b/core/services/workflows/syncer/v2/file_workflow_source.go index 5cb2ec36346..b18f3c80f48 100644 --- a/core/services/workflows/syncer/v2/file_workflow_source.go +++ b/core/services/workflows/syncer/v2/file_workflow_source.go @@ -107,7 +107,7 @@ func (f *FileWorkflowSource) ListWorkflowMetadata(ctx context.Context, don capab // Build a set of DON families for efficient lookup donFamilySet := make(map[string]bool) - for _, family := range don.Families { + for _, family := range ExpandWorkflowFamilies(don.Families) { donFamilySet[family] = true } diff --git a/core/services/workflows/syncer/v2/grpc_workflow_source.go b/core/services/workflows/syncer/v2/grpc_workflow_source.go index 820c0e05b50..de1ac32a931 100644 --- a/core/services/workflows/syncer/v2/grpc_workflow_source.go +++ b/core/services/workflows/syncer/v2/grpc_workflow_source.go @@ -173,10 +173,11 @@ func (g *GRPCWorkflowSource) ListWorkflowMetadata(ctx context.Context, don capab var allViews []WorkflowMetadataView var start int64 + families := ExpandWorkflowFamilies(don.Families) // Fetch all pages for { - workflows, hasMore, err := g.fetchPageWithRetry(ctx, don.Families, start) + workflows, hasMore, err := g.fetchPageWithRetry(ctx, families, start) if err != nil { return nil, nil, err } diff --git a/core/services/workflows/syncer/v2/helpers.go b/core/services/workflows/syncer/v2/helpers.go index 60e33a9d07f..0f6c15b9f38 100644 --- a/core/services/workflows/syncer/v2/helpers.go +++ b/core/services/workflows/syncer/v2/helpers.go @@ -3,6 +3,7 @@ package v2 import ( "context" "errors" + "strings" "sync" "github.com/ethereum/go-ethereum/common/hexutil" @@ -13,9 +14,38 @@ import ( eventsv2 "github.com/smartcontractkit/chainlink-protos/workflows/go/v2" "github.com/smartcontractkit/chainlink/v2/core/services/job" "github.com/smartcontractkit/chainlink/v2/core/services/workflows/ratelimiter" + shardownershiptypes "github.com/smartcontractkit/chainlink/v2/core/services/workflows/shardownership/types" "github.com/smartcontractkit/chainlink/v2/core/services/workflows/syncerlimiter" ) +// ExpandWorkflowFamilies returns families with an extra entry added for every +// family ending in shardownershiptypes.WorkflowFamilySuffix (e.g. +// "zone-a_workflows"): the base family name with that suffix stripped (e.g. +// "zone-a"). This lets a DON belonging to a "_workflows" family also +// fetch workflows registered directly under the bare "" family. Order +// is preserved and duplicates are dropped. +func ExpandWorkflowFamilies(families []string) []string { + seen := make(map[string]bool, len(families)) + expanded := make([]string, 0, len(families)) + + add := func(family string) { + if seen[family] { + return + } + seen[family] = true + expanded = append(expanded, family) + } + + for _, family := range families { + add(family) + if base, ok := strings.CutSuffix(family, shardownershiptypes.WorkflowFamilySuffix); ok { + add(base) + } + } + + return expanded +} + var rlConfig = ratelimiter.Config{ GlobalRPS: 1000.0, GlobalBurst: 1000, diff --git a/core/services/workflows/syncer/v2/helpers_test.go b/core/services/workflows/syncer/v2/helpers_test.go new file mode 100644 index 00000000000..4366e363dad --- /dev/null +++ b/core/services/workflows/syncer/v2/helpers_test.go @@ -0,0 +1,49 @@ +package v2 + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestExpandWorkflowFamilies(t *testing.T) { + t.Parallel() + tests := []struct { + name string + families []string + want []string + }{ + { + name: "no workflows suffix", + families: []string{"zone-a"}, + want: []string{"zone-a"}, + }, + { + name: "workflows suffix expands to base family", + families: []string{"zone-a_workflows"}, + want: []string{"zone-a_workflows", "zone-a"}, + }, + { + name: "base family already present is not duplicated", + families: []string{"zone-a_workflows", "zone-a"}, + want: []string{"zone-a_workflows", "zone-a"}, + }, + { + name: "mixed families", + families: []string{"zone-a", "zone-b_workflows"}, + want: []string{"zone-a", "zone-b_workflows", "zone-b"}, + }, + { + name: "empty input", + families: []string{}, + want: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tt.want, ExpandWorkflowFamilies(tt.families)) + }) + } +}