From 84b034ee2aa42fc44ccaf5b16365c63fa8da36a9 Mon Sep 17 00:00:00 2001
From: thomjg <103059015+thomjg@users.noreply.github.com>
Date: Mon, 20 Apr 2026 20:52:58 -0400
Subject: [PATCH 01/14] Adding emitted job spec telem for OCR2 median jobs
---
core/config/app_config.go | 1 +
core/config/job_spec_reporter_config.go | 14 +
core/config/toml/types.go | 52 ++
core/services/chainlink/application.go | 17 +
core/services/chainlink/config_general.go | 4 +
.../chainlink/config_job_spec_reporter.go | 42 +
core/services/chainlink/config_test.go | 7 +
.../chainlink/mocks/general_config.go | 47 +
.../testdata/config-empty-effective.toml | 3 +
.../chainlink/testdata/config-full.toml | 6 +
.../config-multi-chain-effective.toml | 3 +
core/services/feeds/mocks/orm.go | 59 ++
core/services/feeds/orm.go | 16 +
core/services/job/mocks/spawner.go | 33 +
core/services/job/spawner.go | 52 ++
.../nodestatusreporter/jobspec/events/emit.go | 34 +
.../jobspec/events/emit_test.go | 66 ++
.../jobspec/events/generate.go | 3 +
.../jobspec/events/job_spec.pb.go | 837 ++++++++++++++++++
.../jobspec/events/job_spec.proto | 119 +++
.../jobspec/events/types.go | 9 +
.../jobspec/job_spec_reporter.go | 397 +++++++++
.../jobspec/job_spec_reporter_test.go | 369 ++++++++
23 files changed, 2190 insertions(+)
create mode 100644 core/config/job_spec_reporter_config.go
create mode 100644 core/services/chainlink/config_job_spec_reporter.go
create mode 100644 core/services/nodestatusreporter/jobspec/events/emit.go
create mode 100644 core/services/nodestatusreporter/jobspec/events/emit_test.go
create mode 100644 core/services/nodestatusreporter/jobspec/events/generate.go
create mode 100644 core/services/nodestatusreporter/jobspec/events/job_spec.pb.go
create mode 100644 core/services/nodestatusreporter/jobspec/events/job_spec.proto
create mode 100644 core/services/nodestatusreporter/jobspec/events/types.go
create mode 100644 core/services/nodestatusreporter/jobspec/job_spec_reporter.go
create mode 100644 core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go
diff --git a/core/config/app_config.go b/core/config/app_config.go
index 4de8a5d80e2..c2692c3baf9 100644
--- a/core/config/app_config.go
+++ b/core/config/app_config.go
@@ -65,6 +65,7 @@ type AppConfig interface {
CCV() CCV
Billing() Billing
BridgeStatusReporter() BridgeStatusReporter
+ JobSpecReporter() JobSpecReporter
Sharding() Sharding
LOOPP() LOOPP
}
diff --git a/core/config/job_spec_reporter_config.go b/core/config/job_spec_reporter_config.go
new file mode 100644
index 00000000000..b18b0860ae2
--- /dev/null
+++ b/core/config/job_spec_reporter_config.go
@@ -0,0 +1,14 @@
+package config
+
+import "time"
+
+type JobSpecReporter interface {
+ Enabled() bool
+ PollingInterval() time.Duration
+ // EnabledOCR2PluginTypes is the allowlist of OCR2 plugin types to emit for (e.g. "median", "ocr2keeper").
+ // An empty slice means all OCR2 plugin types are allowed.
+ EnabledOCR2PluginTypes() []string
+ // EmitNonOCR2Jobs controls whether non-OCR2 jobs (VRF, Keeper, Functions, CCIP, Workflow, …)
+ // emit the generic envelope. Default false for the initial rollout.
+ EmitNonOCR2Jobs() bool
+}
diff --git a/core/config/toml/types.go b/core/config/toml/types.go
index cd1b765f269..441d01b47ee 100644
--- a/core/config/toml/types.go
+++ b/core/config/toml/types.go
@@ -9,6 +9,7 @@ import (
"reflect"
"regexp"
"strings"
+ "time"
"github.com/google/uuid"
"go.uber.org/zap/zapcore"
@@ -65,6 +66,7 @@ type Core struct {
CRE CreConfig `toml:",omitempty"`
Billing Billing `toml:",omitempty"`
BridgeStatusReporter BridgeStatusReporter `toml:",omitempty"`
+ JobSpecReporter JobSpecReporter `toml:",omitempty"`
Sharding Sharding `toml:",omitempty"`
LOOPP LOOPP `toml:",omitempty"`
}
@@ -112,6 +114,7 @@ func (c *Core) SetFrom(f *Core) {
c.CRE.setFrom(&f.CRE)
c.Billing.setFrom(&f.Billing)
c.BridgeStatusReporter.setFrom(&f.BridgeStatusReporter)
+ c.JobSpecReporter.setFrom(&f.JobSpecReporter)
c.Sharding.setFrom(&f.Sharding)
c.LOOPP.setFrom(&f.LOOPP)
@@ -3086,6 +3089,55 @@ func (e *BridgeStatusReporter) ValidateConfig() error {
return nil
}
+type JobSpecReporter struct {
+ Enabled *bool
+ PollingInterval *commonconfig.Duration
+ EnabledOCR2PluginTypes *[]string
+ EmitNonOCR2Jobs *bool
+}
+
+func (e *JobSpecReporter) setFrom(f *JobSpecReporter) {
+ if f.Enabled != nil {
+ e.Enabled = f.Enabled
+ }
+ if f.PollingInterval != nil {
+ e.PollingInterval = f.PollingInterval
+ }
+ if f.EnabledOCR2PluginTypes != nil {
+ e.EnabledOCR2PluginTypes = f.EnabledOCR2PluginTypes
+ }
+ if f.EmitNonOCR2Jobs != nil {
+ e.EmitNonOCR2Jobs = f.EmitNonOCR2Jobs
+ }
+}
+
+func (e *JobSpecReporter) ValidateConfig() error {
+ if e.Enabled == nil || !*e.Enabled {
+ return nil
+ }
+
+ if e.PollingInterval == nil {
+ defaultInterval := commonconfig.MustNewDuration(time.Hour)
+ e.PollingInterval = defaultInterval
+ }
+
+ if e.PollingInterval.Duration() < config.MinimumPollingInterval {
+ return configutils.ErrInvalid{Name: "PollingInterval", Value: e.PollingInterval.Duration(), Msg: "must be greater than or equal to: " + config.MinimumPollingInterval.String()}
+ }
+
+ if e.EnabledOCR2PluginTypes == nil {
+ defaultTypes := []string{"median"}
+ e.EnabledOCR2PluginTypes = &defaultTypes
+ }
+
+ if e.EmitNonOCR2Jobs == nil {
+ defaultEmitNonOCR2 := false
+ e.EmitNonOCR2Jobs = &defaultEmitNonOCR2
+ }
+
+ return nil
+}
+
type JobDistributor struct {
DisplayName *string
}
diff --git a/core/services/chainlink/application.go b/core/services/chainlink/application.go
index b9f70144d12..8a0ec574f8e 100644
--- a/core/services/chainlink/application.go
+++ b/core/services/chainlink/application.go
@@ -7,6 +7,7 @@ import (
"fmt"
"math/big"
"net/http"
+ "os"
"strconv"
"sync"
"time"
@@ -71,6 +72,7 @@ import (
"github.com/smartcontractkit/chainlink/v2/core/services/keystore"
"github.com/smartcontractkit/chainlink/v2/core/services/llo/retirement"
"github.com/smartcontractkit/chainlink/v2/core/services/nodestatusreporter/bridgestatus"
+ "github.com/smartcontractkit/chainlink/v2/core/services/nodestatusreporter/jobspec"
"github.com/smartcontractkit/chainlink/v2/core/services/ocr"
"github.com/smartcontractkit/chainlink/v2/core/services/ocr2"
"github.com/smartcontractkit/chainlink/v2/core/services/ocrbootstrap"
@@ -824,6 +826,21 @@ func NewApplication(ctx context.Context, opts ApplicationOpts) (Application, err
feedsService = &feeds.NullService{}
}
+ hostname, _ := os.Hostname()
+ var feedsORMForReporter feeds.ORM
+ if cfg.Feature().FeedsManager() {
+ feedsORMForReporter = feeds.NewORM(opts.DS, globalLogger)
+ }
+ jobSpecReporter := jobspec.NewJobSpecReporter(
+ cfg.JobSpecReporter(),
+ jobSpawner,
+ feedsORMForReporter,
+ beholder.GetEmitter(),
+ jobspec.NodeInfo{CSAPublicKey: csaPubKeyHex, NodeVersion: static.Version, Hostname: hostname},
+ globalLogger,
+ )
+ srvcs = append(srvcs, jobSpecReporter)
+
for _, s := range srvcs {
if s == nil {
panic("service unexpectedly nil")
diff --git a/core/services/chainlink/config_general.go b/core/services/chainlink/config_general.go
index cf1dca6593c..25b58ca1114 100644
--- a/core/services/chainlink/config_general.go
+++ b/core/services/chainlink/config_general.go
@@ -600,6 +600,10 @@ func (g *generalConfig) BridgeStatusReporter() coreconfig.BridgeStatusReporter {
return &bridgeStatusReporterConfig{c: g.c.BridgeStatusReporter}
}
+func (g *generalConfig) JobSpecReporter() coreconfig.JobSpecReporter {
+ return &jobSpecReporterConfig{c: g.c.JobSpecReporter}
+}
+
func (g *generalConfig) Sharding() coreconfig.Sharding {
return &shardingConfig{s: g.c.Sharding}
}
diff --git a/core/services/chainlink/config_job_spec_reporter.go b/core/services/chainlink/config_job_spec_reporter.go
new file mode 100644
index 00000000000..791cbaaa1f0
--- /dev/null
+++ b/core/services/chainlink/config_job_spec_reporter.go
@@ -0,0 +1,42 @@
+package chainlink
+
+import (
+ "time"
+
+ "github.com/smartcontractkit/chainlink/v2/core/config"
+ "github.com/smartcontractkit/chainlink/v2/core/config/toml"
+)
+
+var _ config.JobSpecReporter = (*jobSpecReporterConfig)(nil)
+
+type jobSpecReporterConfig struct {
+ c toml.JobSpecReporter
+}
+
+func (e *jobSpecReporterConfig) Enabled() bool {
+ if e.c.Enabled == nil {
+ return false
+ }
+ return *e.c.Enabled
+}
+
+func (e *jobSpecReporterConfig) PollingInterval() time.Duration {
+ if e.c.PollingInterval == nil {
+ return time.Hour
+ }
+ return e.c.PollingInterval.Duration()
+}
+
+func (e *jobSpecReporterConfig) EnabledOCR2PluginTypes() []string {
+ if e.c.EnabledOCR2PluginTypes == nil {
+ return []string{"median"}
+ }
+ return *e.c.EnabledOCR2PluginTypes
+}
+
+func (e *jobSpecReporterConfig) EmitNonOCR2Jobs() bool {
+ if e.c.EmitNonOCR2Jobs == nil {
+ return false
+ }
+ return *e.c.EmitNonOCR2Jobs
+}
diff --git a/core/services/chainlink/config_test.go b/core/services/chainlink/config_test.go
index 87ef592f7c2..f8dd91a41fe 100644
--- a/core/services/chainlink/config_test.go
+++ b/core/services/chainlink/config_test.go
@@ -678,6 +678,13 @@ func TestConfig_Marshal(t *testing.T) {
IgnoreInvalidBridges: ptr(true),
IgnoreJoblessBridges: ptr(false),
}
+ enabledOCR2PluginTypes := []string{"median"}
+ full.JobSpecReporter = toml.JobSpecReporter{
+ Enabled: ptr(true),
+ PollingInterval: commoncfg.MustNewDuration(time.Hour),
+ EnabledOCR2PluginTypes: &enabledOCR2PluginTypes,
+ EmitNonOCR2Jobs: ptr(false),
+ }
full.Sharding = toml.Sharding{
ShardingEnabled: ptr(false),
ArbiterPort: ptr[uint16](9876),
diff --git a/core/services/chainlink/mocks/general_config.go b/core/services/chainlink/mocks/general_config.go
index e0c6395cfa3..fed24c8ee20 100644
--- a/core/services/chainlink/mocks/general_config.go
+++ b/core/services/chainlink/mocks/general_config.go
@@ -354,6 +354,53 @@ func (_c *GeneralConfig_BridgeStatusReporter_Call) RunAndReturn(run func() confi
return _c
}
+// JobSpecReporter provides a mock function with no fields
+func (_m *GeneralConfig) JobSpecReporter() config.JobSpecReporter {
+ ret := _m.Called()
+
+ if len(ret) == 0 {
+ panic("no return value specified for JobSpecReporter")
+ }
+
+ var r0 config.JobSpecReporter
+ if rf, ok := ret.Get(0).(func() config.JobSpecReporter); ok {
+ r0 = rf()
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(config.JobSpecReporter)
+ }
+ }
+
+ return r0
+}
+
+// GeneralConfig_JobSpecReporter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'JobSpecReporter'
+type GeneralConfig_JobSpecReporter_Call struct {
+ *mock.Call
+}
+
+// JobSpecReporter is a helper method to define mock.On call
+func (_e *GeneralConfig_Expecter) JobSpecReporter() *GeneralConfig_JobSpecReporter_Call {
+ return &GeneralConfig_JobSpecReporter_Call{Call: _e.mock.On("JobSpecReporter")}
+}
+
+func (_c *GeneralConfig_JobSpecReporter_Call) Run(run func()) *GeneralConfig_JobSpecReporter_Call {
+ _c.Call.Run(func(args mock.Arguments) {
+ run()
+ })
+ return _c
+}
+
+func (_c *GeneralConfig_JobSpecReporter_Call) Return(_a0 config.JobSpecReporter) *GeneralConfig_JobSpecReporter_Call {
+ _c.Call.Return(_a0)
+ return _c
+}
+
+func (_c *GeneralConfig_JobSpecReporter_Call) RunAndReturn(run func() config.JobSpecReporter) *GeneralConfig_JobSpecReporter_Call {
+ _c.Call.Return(run)
+ return _c
+}
+
// CCV provides a mock function with no fields
func (_m *GeneralConfig) CCV() config.CCV {
ret := _m.Called()
diff --git a/core/services/chainlink/testdata/config-empty-effective.toml b/core/services/chainlink/testdata/config-empty-effective.toml
index c6498692f99..0588f5acb94 100644
--- a/core/services/chainlink/testdata/config-empty-effective.toml
+++ b/core/services/chainlink/testdata/config-empty-effective.toml
@@ -399,6 +399,9 @@ PollingInterval = '5m0s'
IgnoreInvalidBridges = true
IgnoreJoblessBridges = false
+[JobSpecReporter]
+Enabled = false
+
[Sharding]
ShardingEnabled = false
ArbiterPort = 9876
diff --git a/core/services/chainlink/testdata/config-full.toml b/core/services/chainlink/testdata/config-full.toml
index a784f98a591..34ca1644255 100644
--- a/core/services/chainlink/testdata/config-full.toml
+++ b/core/services/chainlink/testdata/config-full.toml
@@ -437,6 +437,12 @@ PollingInterval = '5m0s'
IgnoreInvalidBridges = true
IgnoreJoblessBridges = false
+[JobSpecReporter]
+Enabled = true
+PollingInterval = '1h0m0s'
+EnabledOCR2PluginTypes = ['median']
+EmitNonOCR2Jobs = false
+
[Sharding]
ShardingEnabled = false
ArbiterPort = 9876
diff --git a/core/services/chainlink/testdata/config-multi-chain-effective.toml b/core/services/chainlink/testdata/config-multi-chain-effective.toml
index 9a2654d9409..a861e25dd5f 100644
--- a/core/services/chainlink/testdata/config-multi-chain-effective.toml
+++ b/core/services/chainlink/testdata/config-multi-chain-effective.toml
@@ -399,6 +399,9 @@ PollingInterval = '5m0s'
IgnoreInvalidBridges = true
IgnoreJoblessBridges = false
+[JobSpecReporter]
+Enabled = false
+
[Sharding]
ShardingEnabled = false
ArbiterPort = 9876
diff --git a/core/services/feeds/mocks/orm.go b/core/services/feeds/mocks/orm.go
index 6ce30a0a78c..c19b13a4c2d 100644
--- a/core/services/feeds/mocks/orm.go
+++ b/core/services/feeds/mocks/orm.go
@@ -1093,6 +1093,65 @@ func (_c *ORM_GetJobProposalByRemoteUUID_Call) RunAndReturn(run func(context.Con
return _c
}
+// GetJobProposalByExternalJobID provides a mock function with given fields: ctx, externalJobID
+func (_m *ORM) GetJobProposalByExternalJobID(ctx context.Context, externalJobID uuid.UUID) (*feeds.JobProposal, error) {
+ ret := _m.Called(ctx, externalJobID)
+
+ if len(ret) == 0 {
+ panic("no return value specified for GetJobProposalByExternalJobID")
+ }
+
+ var r0 *feeds.JobProposal
+ var r1 error
+ if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) (*feeds.JobProposal, error)); ok {
+ return rf(ctx, externalJobID)
+ }
+ if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) *feeds.JobProposal); ok {
+ r0 = rf(ctx, externalJobID)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(*feeds.JobProposal)
+ }
+ }
+
+ if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok {
+ r1 = rf(ctx, externalJobID)
+ } else {
+ r1 = ret.Error(1)
+ }
+
+ return r0, r1
+}
+
+// ORM_GetJobProposalByExternalJobID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetJobProposalByExternalJobID'
+type ORM_GetJobProposalByExternalJobID_Call struct {
+ *mock.Call
+}
+
+// GetJobProposalByExternalJobID is a helper method to define mock.On call
+// - ctx context.Context
+// - externalJobID uuid.UUID
+func (_e *ORM_Expecter) GetJobProposalByExternalJobID(ctx interface{}, externalJobID interface{}) *ORM_GetJobProposalByExternalJobID_Call {
+ return &ORM_GetJobProposalByExternalJobID_Call{Call: _e.mock.On("GetJobProposalByExternalJobID", ctx, externalJobID)}
+}
+
+func (_c *ORM_GetJobProposalByExternalJobID_Call) Run(run func(ctx context.Context, externalJobID uuid.UUID)) *ORM_GetJobProposalByExternalJobID_Call {
+ _c.Call.Run(func(args mock.Arguments) {
+ run(args[0].(context.Context), args[1].(uuid.UUID))
+ })
+ return _c
+}
+
+func (_c *ORM_GetJobProposalByExternalJobID_Call) Return(_a0 *feeds.JobProposal, _a1 error) *ORM_GetJobProposalByExternalJobID_Call {
+ _c.Call.Return(_a0, _a1)
+ return _c
+}
+
+func (_c *ORM_GetJobProposalByExternalJobID_Call) RunAndReturn(run func(context.Context, uuid.UUID) (*feeds.JobProposal, error)) *ORM_GetJobProposalByExternalJobID_Call {
+ _c.Call.Return(run)
+ return _c
+}
+
// GetLatestSpec provides a mock function with given fields: ctx, jpID
func (_m *ORM) GetLatestSpec(ctx context.Context, jpID int64) (*feeds.JobProposalSpec, error) {
ret := _m.Called(ctx, jpID)
diff --git a/core/services/feeds/orm.go b/core/services/feeds/orm.go
index c0edd00a3a3..a6b6015667f 100644
--- a/core/services/feeds/orm.go
+++ b/core/services/feeds/orm.go
@@ -40,6 +40,7 @@ type ORM interface {
DeleteProposal(ctx context.Context, id int64) error
GetJobProposal(ctx context.Context, id int64) (*JobProposal, error)
GetJobProposalByRemoteUUID(ctx context.Context, uuid uuid.UUID) (*JobProposal, error)
+ GetJobProposalByExternalJobID(ctx context.Context, externalJobID uuid.UUID) (*JobProposal, error)
ListJobProposalsByManagersIDs(ctx context.Context, ids []int64) ([]JobProposal, error)
UpdateJobProposalStatus(ctx context.Context, id int64, status JobProposalStatus) error // NEEDED?
UpsertJobProposal(ctx context.Context, jp *JobProposal) (int64, error)
@@ -432,6 +433,21 @@ AND status <> $2;
return jp, errors.Wrap(err, "GetJobProposalByRemoteUUID failed")
}
+// GetJobProposalByExternalJobID gets a non-deleted job proposal by its external job ID.
+func (o *orm) GetJobProposalByExternalJobID(ctx context.Context, externalJobID uuid.UUID) (jp *JobProposal, err error) {
+ stmt := `
+SELECT *
+FROM job_proposals
+WHERE external_job_id = $1
+AND status <> $2
+LIMIT 1;
+`
+
+ jp = new(JobProposal)
+ err = o.ds.GetContext(ctx, jp, stmt, externalJobID, JobProposalStatusDeleted)
+ return jp, errors.Wrap(err, "GetJobProposalByExternalJobID failed")
+}
+
// ListJobProposalsByManagersIDs gets job proposals by feeds managers IDs.
func (o *orm) ListJobProposalsByManagersIDs(ctx context.Context, ids []int64) ([]JobProposal, error) {
stmt := `
diff --git a/core/services/job/mocks/spawner.go b/core/services/job/mocks/spawner.go
index 28ff46461b1..2df19de32a3 100644
--- a/core/services/job/mocks/spawner.go
+++ b/core/services/job/mocks/spawner.go
@@ -441,6 +441,39 @@ func (_c *Spawner_StartService_Call) RunAndReturn(run func(context.Context, job.
return _c
}
+// RegisterListener provides a mock function with given fields: l
+func (_m *Spawner) RegisterListener(l job.Listener) {
+ _m.Called(l)
+}
+
+// Spawner_RegisterListener_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RegisterListener'
+type Spawner_RegisterListener_Call struct {
+ *mock.Call
+}
+
+// RegisterListener is a helper method to define mock.On call
+// - l job.Listener
+func (_e *Spawner_Expecter) RegisterListener(l interface{}) *Spawner_RegisterListener_Call {
+ return &Spawner_RegisterListener_Call{Call: _e.mock.On("RegisterListener", l)}
+}
+
+func (_c *Spawner_RegisterListener_Call) Run(run func(l job.Listener)) *Spawner_RegisterListener_Call {
+ _c.Call.Run(func(args mock.Arguments) {
+ run(args[0].(job.Listener))
+ })
+ return _c
+}
+
+func (_c *Spawner_RegisterListener_Call) Return() *Spawner_RegisterListener_Call {
+ _c.Call.Return()
+ return _c
+}
+
+func (_c *Spawner_RegisterListener_Call) RunAndReturn(run func(job.Listener)) *Spawner_RegisterListener_Call {
+ _c.Call.Return(run)
+ return _c
+}
+
// NewSpawner creates a new instance of Spawner. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewSpawner(t interface {
diff --git a/core/services/job/spawner.go b/core/services/job/spawner.go
index e883fb23b47..08e78043025 100644
--- a/core/services/job/spawner.go
+++ b/core/services/job/spawner.go
@@ -17,6 +17,13 @@ import (
)
type (
+ // Listener is notified when jobs are started or stopped by the Spawner.
+ // Callbacks are invoked asynchronously and must not block.
+ Listener interface {
+ OnJobStarted(ctx context.Context, jb Job)
+ OnJobStopped(ctx context.Context, jb Job)
+ }
+
// Spawner manages the spinning up and down of the long-running
// services that perform the work described by job specs. Each active job spec
// has 1 or more of these services associated with it.
@@ -35,6 +42,10 @@ type (
// NOTE: Prefer to use CreateJob, this is only publicly exposed for use in tests
// to start a job that was previously manually inserted into DB
StartService(ctx context.Context, spec Job) error
+
+ // RegisterListener registers a Listener to be notified on job start/stop.
+ // Safe to call before or after Start().
+ RegisterListener(Listener)
}
Checker interface {
@@ -52,6 +63,9 @@ type (
activeJobsMu sync.RWMutex
lggr logger.Logger
+ listeners []Listener
+ listenersMu sync.RWMutex
+
chStop services.StopChan
lbDependentAwaiters []utils.DependentAwaiter
}
@@ -274,6 +288,7 @@ func (js *spawner) CreateJob(ctx context.Context, ds sqlutil.DataSource, jb *Job
js.lggr.Errorw("Error starting job services", "type", jb.Type, "jobID", jb.ID, "err", err)
} else {
js.lggr.Infow("Started job services", "type", jb.Type, "jobID", jb.ID)
+ js.notifyListeners(true, *jb)
}
delegate.AfterJobCreated(*jb)
@@ -340,6 +355,7 @@ func (js *spawner) DeleteJob(ctx context.Context, ds sqlutil.DataSource, jobID i
if exists {
// Stop the service and remove the job from memory, which will always happen even if closing the services fail.
js.stopService(jobID)
+ js.notifyListeners(false, aj.spec)
}
lggr.Infow("Stopped and deleted job")
@@ -357,6 +373,42 @@ func (js *spawner) ActiveJobs() map[int32]Job {
return m
}
+func (js *spawner) RegisterListener(l Listener) {
+ js.listenersMu.Lock()
+ defer js.listenersMu.Unlock()
+ js.listeners = append(js.listeners, l)
+}
+
+// notifyListeners dispatches an event to all registered listeners in a
+// best-effort, non-blocking, panic-safe goroutine.
+func (js *spawner) notifyListeners(started bool, jb Job) {
+ js.listenersMu.RLock()
+ ls := make([]Listener, len(js.listeners))
+ copy(ls, js.listeners)
+ js.listenersMu.RUnlock()
+
+ if len(ls) == 0 {
+ return
+ }
+
+ ctx, cancel := js.chStop.NewCtx()
+ go func() {
+ defer cancel()
+ defer func() {
+ if r := recover(); r != nil {
+ js.lggr.Errorw("Panic in job spawner listener", "recover", r)
+ }
+ }()
+ for _, l := range ls {
+ if started {
+ l.OnJobStarted(ctx, jb)
+ } else {
+ l.OnJobStopped(ctx, jb)
+ }
+ }
+ }()
+}
+
func (js *spawner) activeJobIDs() []int32 {
js.activeJobsMu.RLock()
defer js.activeJobsMu.RUnlock()
diff --git a/core/services/nodestatusreporter/jobspec/events/emit.go b/core/services/nodestatusreporter/jobspec/events/emit.go
new file mode 100644
index 00000000000..f317922bba1
--- /dev/null
+++ b/core/services/nodestatusreporter/jobspec/events/emit.go
@@ -0,0 +1,34 @@
+package events
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "google.golang.org/protobuf/proto"
+
+ "github.com/smartcontractkit/chainlink-common/pkg/beholder"
+)
+
+// EmitJobSpecEvent emits a Job Spec event through the provided beholder.Emitter.
+func EmitJobSpecEvent(ctx context.Context, emitter beholder.Emitter, event *JobSpecEvent) error {
+ if event.Timestamp == "" {
+ event.Timestamp = time.Now().Format(time.RFC3339Nano)
+ }
+
+ eventBytes, err := proto.Marshal(event)
+ if err != nil {
+ return fmt.Errorf("failed to marshal JobSpecEvent: %w", err)
+ }
+
+ err = emitter.Emit(ctx, eventBytes,
+ "beholder_data_schema", SchemaJobSpec,
+ "beholder_domain", "data-feeds",
+ "beholder_entity", fmt.Sprintf("%s.%s", ProtoPkg, JobSpecEventEntity),
+ )
+ if err != nil {
+ return fmt.Errorf("failed to emit JobSpecEvent: %w", err)
+ }
+
+ return nil
+}
diff --git a/core/services/nodestatusreporter/jobspec/events/emit_test.go b/core/services/nodestatusreporter/jobspec/events/emit_test.go
new file mode 100644
index 00000000000..6c742afe512
--- /dev/null
+++ b/core/services/nodestatusreporter/jobspec/events/emit_test.go
@@ -0,0 +1,66 @@
+package events_test
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "google.golang.org/protobuf/proto"
+
+ "github.com/smartcontractkit/chainlink-common/pkg/beholder"
+ "github.com/smartcontractkit/chainlink-common/pkg/beholder/beholdertest"
+
+ "github.com/smartcontractkit/chainlink/v2/core/services/nodestatusreporter/jobspec/events"
+)
+
+func TestEmitJobSpecEvent_RoundTrip(t *testing.T) {
+ // NewObserver sets the global beholder client; use GetEmitter() to obtain the emitter.
+ observer := beholdertest.NewObserver(t)
+ emitter := beholder.GetEmitter()
+
+ event := &events.JobSpecEvent{
+ ExternalJobId: "test-job-id",
+ InternalJobId: 42,
+ Name: "test-job",
+ JobType: "offchainreporting2",
+ EmissionTrigger: "heartbeat",
+ }
+
+ err := events.EmitJobSpecEvent(context.Background(), emitter, event)
+ require.NoError(t, err)
+
+ msgs := observer.Messages(t, "beholder_entity", events.ProtoPkg+"."+events.JobSpecEventEntity)
+ require.Len(t, msgs, 1)
+
+ msg := msgs[0]
+ require.Equal(t, events.SchemaJobSpec, msg.Attrs["beholder_data_schema"])
+ require.Equal(t, "data-feeds", msg.Attrs["beholder_domain"])
+
+ var decoded events.JobSpecEvent
+ require.NoError(t, proto.Unmarshal(msg.Body, &decoded))
+ require.Equal(t, "test-job-id", decoded.ExternalJobId)
+ require.Equal(t, int32(42), decoded.InternalJobId)
+ require.Equal(t, "test-job", decoded.Name)
+ require.Equal(t, "heartbeat", decoded.EmissionTrigger)
+ require.NotEmpty(t, decoded.Timestamp)
+}
+
+func TestEmitJobSpecEvent_SetsTimestampIfEmpty(t *testing.T) {
+ observer := beholdertest.NewObserver(t)
+ emitter := beholder.GetEmitter()
+
+ event := &events.JobSpecEvent{
+ ExternalJobId: "ts-test",
+ }
+ require.Empty(t, event.Timestamp)
+
+ err := events.EmitJobSpecEvent(context.Background(), emitter, event)
+ require.NoError(t, err)
+
+ msgs := observer.Messages(t, "beholder_entity", events.ProtoPkg+"."+events.JobSpecEventEntity)
+ require.Len(t, msgs, 1)
+
+ var decoded events.JobSpecEvent
+ require.NoError(t, proto.Unmarshal(msgs[0].Body, &decoded))
+ require.NotEmpty(t, decoded.Timestamp)
+}
diff --git a/core/services/nodestatusreporter/jobspec/events/generate.go b/core/services/nodestatusreporter/jobspec/events/generate.go
new file mode 100644
index 00000000000..df6a81e0d62
--- /dev/null
+++ b/core/services/nodestatusreporter/jobspec/events/generate.go
@@ -0,0 +1,3 @@
+package events
+
+//go:generate protoc --go_out=. --go_opt=paths=source_relative job_spec.proto
diff --git a/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go b/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go
new file mode 100644
index 00000000000..103d2eecd53
--- /dev/null
+++ b/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go
@@ -0,0 +1,837 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.36.11
+// protoc v5.29.3
+// source: job_spec.proto
+
+package events
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+ unsafe "unsafe"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// JobSpecEvent is emitted for each active job on a heartbeat, on job creation,
+// and on job deletion. For the initial rollout only offchainreporting2 jobs
+// with pluginType = "median" are emitted (configurable via EnabledOCR2PluginTypes
+// and EmitNonOCR2Jobs in the JobSpecReporter config section).
+type JobSpecEvent struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ // Job identity — covers every TOML-writable field on job.Job plus DB-assigned columns.
+ ExternalJobId string `protobuf:"bytes,1,opt,name=external_job_id,json=externalJobId,proto3" json:"external_job_id,omitempty"`
+ InternalJobId int32 `protobuf:"varint,2,opt,name=internal_job_id,json=internalJobId,proto3" json:"internal_job_id,omitempty"`
+ Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
+ JobType string `protobuf:"bytes,4,opt,name=job_type,json=jobType,proto3" json:"job_type,omitempty"`
+ SchemaVersion uint32 `protobuf:"varint,5,opt,name=schema_version,json=schemaVersion,proto3" json:"schema_version,omitempty"`
+ GasLimit uint32 `protobuf:"varint,6,opt,name=gas_limit,json=gasLimit,proto3" json:"gas_limit,omitempty"`
+ ForwardingAllowed bool `protobuf:"varint,7,opt,name=forwarding_allowed,json=forwardingAllowed,proto3" json:"forwarding_allowed,omitempty"`
+ StreamId *uint32 `protobuf:"varint,8,opt,name=stream_id,json=streamId,proto3,oneof" json:"stream_id,omitempty"`
+ MaxTaskDurationSeconds float64 `protobuf:"fixed64,9,opt,name=max_task_duration_seconds,json=maxTaskDurationSeconds,proto3" json:"max_task_duration_seconds,omitempty"`
+ CreatedAt string `protobuf:"bytes,10,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
+ // Observation pipeline — the job's observationSource field.
+ ObservationSource string `protobuf:"bytes,11,opt,name=observation_source,json=observationSource,proto3" json:"observation_source,omitempty"`
+ PipelineSpecId int32 `protobuf:"varint,12,opt,name=pipeline_spec_id,json=pipelineSpecId,proto3" json:"pipeline_spec_id,omitempty"`
+ // Bridge names extracted from the observationSource DOT DAG (top-level only).
+ BridgeNames []string `protobuf:"bytes,13,rep,name=bridge_names,json=bridgeNames,proto3" json:"bridge_names,omitempty"`
+ // Proposal lifecycle fields — zero/empty when the job was created manually
+ // (not via a Feeds Manager / Job Distributor).
+ FeedsManagerId int64 `protobuf:"varint,14,opt,name=feeds_manager_id,json=feedsManagerId,proto3" json:"feeds_manager_id,omitempty"`
+ RemoteUuid string `protobuf:"bytes,15,opt,name=remote_uuid,json=remoteUuid,proto3" json:"remote_uuid,omitempty"`
+ SpecVersion int32 `protobuf:"varint,16,opt,name=spec_version,json=specVersion,proto3" json:"spec_version,omitempty"`
+ ProposedAt string `protobuf:"bytes,17,opt,name=proposed_at,json=proposedAt,proto3" json:"proposed_at,omitempty"`
+ ApprovedAt string `protobuf:"bytes,18,opt,name=approved_at,json=approvedAt,proto3" json:"approved_at,omitempty"`
+ AcceptLatencySeconds float64 `protobuf:"fixed64,19,opt,name=accept_latency_seconds,json=acceptLatencySeconds,proto3" json:"accept_latency_seconds,omitempty"`
+ // OCR2-specific fields — absent for non-OCR2 job types.
+ Ocr2OracleSpec *OCR2OracleSpecInfo `protobuf:"bytes,20,opt,name=ocr2_oracle_spec,json=ocr2OracleSpec,proto3" json:"ocr2_oracle_spec,omitempty"`
+ // Node identity.
+ CsaPublicKey string `protobuf:"bytes,21,opt,name=csa_public_key,json=csaPublicKey,proto3" json:"csa_public_key,omitempty"`
+ NodeVersion string `protobuf:"bytes,22,opt,name=node_version,json=nodeVersion,proto3" json:"node_version,omitempty"`
+ Hostname string `protobuf:"bytes,23,opt,name=hostname,proto3" json:"hostname,omitempty"`
+ // Event metadata.
+ // emission_trigger is one of "heartbeat", "create", or "delete".
+ EmissionTrigger string `protobuf:"bytes,24,opt,name=emission_trigger,json=emissionTrigger,proto3" json:"emission_trigger,omitempty"`
+ Timestamp string `protobuf:"bytes,25,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *JobSpecEvent) Reset() {
+ *x = JobSpecEvent{}
+ mi := &file_job_spec_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *JobSpecEvent) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*JobSpecEvent) ProtoMessage() {}
+
+func (x *JobSpecEvent) ProtoReflect() protoreflect.Message {
+ mi := &file_job_spec_proto_msgTypes[0]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use JobSpecEvent.ProtoReflect.Descriptor instead.
+func (*JobSpecEvent) Descriptor() ([]byte, []int) {
+ return file_job_spec_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *JobSpecEvent) GetExternalJobId() string {
+ if x != nil {
+ return x.ExternalJobId
+ }
+ return ""
+}
+
+func (x *JobSpecEvent) GetInternalJobId() int32 {
+ if x != nil {
+ return x.InternalJobId
+ }
+ return 0
+}
+
+func (x *JobSpecEvent) GetName() string {
+ if x != nil {
+ return x.Name
+ }
+ return ""
+}
+
+func (x *JobSpecEvent) GetJobType() string {
+ if x != nil {
+ return x.JobType
+ }
+ return ""
+}
+
+func (x *JobSpecEvent) GetSchemaVersion() uint32 {
+ if x != nil {
+ return x.SchemaVersion
+ }
+ return 0
+}
+
+func (x *JobSpecEvent) GetGasLimit() uint32 {
+ if x != nil {
+ return x.GasLimit
+ }
+ return 0
+}
+
+func (x *JobSpecEvent) GetForwardingAllowed() bool {
+ if x != nil {
+ return x.ForwardingAllowed
+ }
+ return false
+}
+
+func (x *JobSpecEvent) GetStreamId() uint32 {
+ if x != nil && x.StreamId != nil {
+ return *x.StreamId
+ }
+ return 0
+}
+
+func (x *JobSpecEvent) GetMaxTaskDurationSeconds() float64 {
+ if x != nil {
+ return x.MaxTaskDurationSeconds
+ }
+ return 0
+}
+
+func (x *JobSpecEvent) GetCreatedAt() string {
+ if x != nil {
+ return x.CreatedAt
+ }
+ return ""
+}
+
+func (x *JobSpecEvent) GetObservationSource() string {
+ if x != nil {
+ return x.ObservationSource
+ }
+ return ""
+}
+
+func (x *JobSpecEvent) GetPipelineSpecId() int32 {
+ if x != nil {
+ return x.PipelineSpecId
+ }
+ return 0
+}
+
+func (x *JobSpecEvent) GetBridgeNames() []string {
+ if x != nil {
+ return x.BridgeNames
+ }
+ return nil
+}
+
+func (x *JobSpecEvent) GetFeedsManagerId() int64 {
+ if x != nil {
+ return x.FeedsManagerId
+ }
+ return 0
+}
+
+func (x *JobSpecEvent) GetRemoteUuid() string {
+ if x != nil {
+ return x.RemoteUuid
+ }
+ return ""
+}
+
+func (x *JobSpecEvent) GetSpecVersion() int32 {
+ if x != nil {
+ return x.SpecVersion
+ }
+ return 0
+}
+
+func (x *JobSpecEvent) GetProposedAt() string {
+ if x != nil {
+ return x.ProposedAt
+ }
+ return ""
+}
+
+func (x *JobSpecEvent) GetApprovedAt() string {
+ if x != nil {
+ return x.ApprovedAt
+ }
+ return ""
+}
+
+func (x *JobSpecEvent) GetAcceptLatencySeconds() float64 {
+ if x != nil {
+ return x.AcceptLatencySeconds
+ }
+ return 0
+}
+
+func (x *JobSpecEvent) GetOcr2OracleSpec() *OCR2OracleSpecInfo {
+ if x != nil {
+ return x.Ocr2OracleSpec
+ }
+ return nil
+}
+
+func (x *JobSpecEvent) GetCsaPublicKey() string {
+ if x != nil {
+ return x.CsaPublicKey
+ }
+ return ""
+}
+
+func (x *JobSpecEvent) GetNodeVersion() string {
+ if x != nil {
+ return x.NodeVersion
+ }
+ return ""
+}
+
+func (x *JobSpecEvent) GetHostname() string {
+ if x != nil {
+ return x.Hostname
+ }
+ return ""
+}
+
+func (x *JobSpecEvent) GetEmissionTrigger() string {
+ if x != nil {
+ return x.EmissionTrigger
+ }
+ return ""
+}
+
+func (x *JobSpecEvent) GetTimestamp() string {
+ if x != nil {
+ return x.Timestamp
+ }
+ return ""
+}
+
+// OCR2OracleSpecInfo carries all fields of job.OCR2OracleSpec.
+type OCR2OracleSpecInfo struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ SpecId int32 `protobuf:"varint,1,opt,name=spec_id,json=specId,proto3" json:"spec_id,omitempty"`
+ ContractId string `protobuf:"bytes,2,opt,name=contract_id,json=contractId,proto3" json:"contract_id,omitempty"`
+ FeedId string `protobuf:"bytes,3,opt,name=feed_id,json=feedId,proto3" json:"feed_id,omitempty"`
+ Relay string `protobuf:"bytes,4,opt,name=relay,proto3" json:"relay,omitempty"`
+ ChainId string `protobuf:"bytes,5,opt,name=chain_id,json=chainId,proto3" json:"chain_id,omitempty"`
+ PluginType string `protobuf:"bytes,6,opt,name=plugin_type,json=pluginType,proto3" json:"plugin_type,omitempty"`
+ TransmitterId string `protobuf:"bytes,7,opt,name=transmitter_id,json=transmitterId,proto3" json:"transmitter_id,omitempty"`
+ OcrKeyBundleId string `protobuf:"bytes,8,opt,name=ocr_key_bundle_id,json=ocrKeyBundleId,proto3" json:"ocr_key_bundle_id,omitempty"`
+ MonitoringEndpoint string `protobuf:"bytes,9,opt,name=monitoring_endpoint,json=monitoringEndpoint,proto3" json:"monitoring_endpoint,omitempty"`
+ P2Pv2Bootstrappers []string `protobuf:"bytes,10,rep,name=p2pv2_bootstrappers,json=p2pv2Bootstrappers,proto3" json:"p2pv2_bootstrappers,omitempty"`
+ AllowNoBootstrappers bool `protobuf:"varint,11,opt,name=allow_no_bootstrappers,json=allowNoBootstrappers,proto3" json:"allow_no_bootstrappers,omitempty"`
+ BlockchainTimeoutSeconds float64 `protobuf:"fixed64,12,opt,name=blockchain_timeout_seconds,json=blockchainTimeoutSeconds,proto3" json:"blockchain_timeout_seconds,omitempty"`
+ ContractConfigTrackerPollIntervalSeconds float64 `protobuf:"fixed64,13,opt,name=contract_config_tracker_poll_interval_seconds,json=contractConfigTrackerPollIntervalSeconds,proto3" json:"contract_config_tracker_poll_interval_seconds,omitempty"`
+ ContractConfigConfirmations uint32 `protobuf:"varint,14,opt,name=contract_config_confirmations,json=contractConfigConfirmations,proto3" json:"contract_config_confirmations,omitempty"`
+ CaptureEaTelemetry bool `protobuf:"varint,15,opt,name=capture_ea_telemetry,json=captureEaTelemetry,proto3" json:"capture_ea_telemetry,omitempty"`
+ CaptureAutomationCustomTelemetry bool `protobuf:"varint,16,opt,name=capture_automation_custom_telemetry,json=captureAutomationCustomTelemetry,proto3" json:"capture_automation_custom_telemetry,omitempty"`
+ SpecCreatedAt string `protobuf:"bytes,17,opt,name=spec_created_at,json=specCreatedAt,proto3" json:"spec_created_at,omitempty"`
+ SpecUpdatedAt string `protobuf:"bytes,18,opt,name=spec_updated_at,json=specUpdatedAt,proto3" json:"spec_updated_at,omitempty"`
+ // Raw JSON passthroughs — always populated; authoritative for any field not
+ // captured in the typed sub-messages below.
+ RelayConfigJson string `protobuf:"bytes,19,opt,name=relay_config_json,json=relayConfigJson,proto3" json:"relay_config_json,omitempty"`
+ PluginConfigJson string `protobuf:"bytes,20,opt,name=plugin_config_json,json=pluginConfigJson,proto3" json:"plugin_config_json,omitempty"`
+ OnchainSigningStrategyJson string `protobuf:"bytes,21,opt,name=onchain_signing_strategy_json,json=onchainSigningStrategyJson,proto3" json:"onchain_signing_strategy_json,omitempty"`
+ // Typed EVM relay config — populated only when relay == "evm".
+ EvmRelayConfig *OCR2EVMRelayConfig `protobuf:"bytes,22,opt,name=evm_relay_config,json=evmRelayConfig,proto3" json:"evm_relay_config,omitempty"`
+ // Typed median plugin config — populated only when plugin_type == "median".
+ MedianPluginConfig *OCR2MedianPluginConfig `protobuf:"bytes,23,opt,name=median_plugin_config,json=medianPluginConfig,proto3" json:"median_plugin_config,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *OCR2OracleSpecInfo) Reset() {
+ *x = OCR2OracleSpecInfo{}
+ mi := &file_job_spec_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *OCR2OracleSpecInfo) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*OCR2OracleSpecInfo) ProtoMessage() {}
+
+func (x *OCR2OracleSpecInfo) ProtoReflect() protoreflect.Message {
+ mi := &file_job_spec_proto_msgTypes[1]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use OCR2OracleSpecInfo.ProtoReflect.Descriptor instead.
+func (*OCR2OracleSpecInfo) Descriptor() ([]byte, []int) {
+ return file_job_spec_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *OCR2OracleSpecInfo) GetSpecId() int32 {
+ if x != nil {
+ return x.SpecId
+ }
+ return 0
+}
+
+func (x *OCR2OracleSpecInfo) GetContractId() string {
+ if x != nil {
+ return x.ContractId
+ }
+ return ""
+}
+
+func (x *OCR2OracleSpecInfo) GetFeedId() string {
+ if x != nil {
+ return x.FeedId
+ }
+ return ""
+}
+
+func (x *OCR2OracleSpecInfo) GetRelay() string {
+ if x != nil {
+ return x.Relay
+ }
+ return ""
+}
+
+func (x *OCR2OracleSpecInfo) GetChainId() string {
+ if x != nil {
+ return x.ChainId
+ }
+ return ""
+}
+
+func (x *OCR2OracleSpecInfo) GetPluginType() string {
+ if x != nil {
+ return x.PluginType
+ }
+ return ""
+}
+
+func (x *OCR2OracleSpecInfo) GetTransmitterId() string {
+ if x != nil {
+ return x.TransmitterId
+ }
+ return ""
+}
+
+func (x *OCR2OracleSpecInfo) GetOcrKeyBundleId() string {
+ if x != nil {
+ return x.OcrKeyBundleId
+ }
+ return ""
+}
+
+func (x *OCR2OracleSpecInfo) GetMonitoringEndpoint() string {
+ if x != nil {
+ return x.MonitoringEndpoint
+ }
+ return ""
+}
+
+func (x *OCR2OracleSpecInfo) GetP2Pv2Bootstrappers() []string {
+ if x != nil {
+ return x.P2Pv2Bootstrappers
+ }
+ return nil
+}
+
+func (x *OCR2OracleSpecInfo) GetAllowNoBootstrappers() bool {
+ if x != nil {
+ return x.AllowNoBootstrappers
+ }
+ return false
+}
+
+func (x *OCR2OracleSpecInfo) GetBlockchainTimeoutSeconds() float64 {
+ if x != nil {
+ return x.BlockchainTimeoutSeconds
+ }
+ return 0
+}
+
+func (x *OCR2OracleSpecInfo) GetContractConfigTrackerPollIntervalSeconds() float64 {
+ if x != nil {
+ return x.ContractConfigTrackerPollIntervalSeconds
+ }
+ return 0
+}
+
+func (x *OCR2OracleSpecInfo) GetContractConfigConfirmations() uint32 {
+ if x != nil {
+ return x.ContractConfigConfirmations
+ }
+ return 0
+}
+
+func (x *OCR2OracleSpecInfo) GetCaptureEaTelemetry() bool {
+ if x != nil {
+ return x.CaptureEaTelemetry
+ }
+ return false
+}
+
+func (x *OCR2OracleSpecInfo) GetCaptureAutomationCustomTelemetry() bool {
+ if x != nil {
+ return x.CaptureAutomationCustomTelemetry
+ }
+ return false
+}
+
+func (x *OCR2OracleSpecInfo) GetSpecCreatedAt() string {
+ if x != nil {
+ return x.SpecCreatedAt
+ }
+ return ""
+}
+
+func (x *OCR2OracleSpecInfo) GetSpecUpdatedAt() string {
+ if x != nil {
+ return x.SpecUpdatedAt
+ }
+ return ""
+}
+
+func (x *OCR2OracleSpecInfo) GetRelayConfigJson() string {
+ if x != nil {
+ return x.RelayConfigJson
+ }
+ return ""
+}
+
+func (x *OCR2OracleSpecInfo) GetPluginConfigJson() string {
+ if x != nil {
+ return x.PluginConfigJson
+ }
+ return ""
+}
+
+func (x *OCR2OracleSpecInfo) GetOnchainSigningStrategyJson() string {
+ if x != nil {
+ return x.OnchainSigningStrategyJson
+ }
+ return ""
+}
+
+func (x *OCR2OracleSpecInfo) GetEvmRelayConfig() *OCR2EVMRelayConfig {
+ if x != nil {
+ return x.EvmRelayConfig
+ }
+ return nil
+}
+
+func (x *OCR2OracleSpecInfo) GetMedianPluginConfig() *OCR2MedianPluginConfig {
+ if x != nil {
+ return x.MedianPluginConfig
+ }
+ return nil
+}
+
+// OCR2EVMRelayConfig carries the well-known fields of the EVM relay config JSON.
+// relay_config_json on OCR2OracleSpecInfo remains authoritative for any field
+// not represented here.
+type OCR2EVMRelayConfig struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ ChainId string `protobuf:"bytes,1,opt,name=chain_id,json=chainId,proto3" json:"chain_id,omitempty"`
+ FromBlock uint64 `protobuf:"varint,2,opt,name=from_block,json=fromBlock,proto3" json:"from_block,omitempty"`
+ EffectiveTransmitterId string `protobuf:"bytes,3,opt,name=effective_transmitter_id,json=effectiveTransmitterId,proto3" json:"effective_transmitter_id,omitempty"`
+ EnableDualTransmission bool `protobuf:"varint,4,opt,name=enable_dual_transmission,json=enableDualTransmission,proto3" json:"enable_dual_transmission,omitempty"`
+ EnableTriggerCapability bool `protobuf:"varint,5,opt,name=enable_trigger_capability,json=enableTriggerCapability,proto3" json:"enable_trigger_capability,omitempty"`
+ LloDonId uint64 `protobuf:"varint,6,opt,name=llo_don_id,json=lloDonId,proto3" json:"llo_don_id,omitempty"`
+ FeedId string `protobuf:"bytes,7,opt,name=feed_id,json=feedId,proto3" json:"feed_id,omitempty"`
+ SendingKeys []string `protobuf:"bytes,8,rep,name=sending_keys,json=sendingKeys,proto3" json:"sending_keys,omitempty"`
+ ProviderType string `protobuf:"bytes,9,opt,name=provider_type,json=providerType,proto3" json:"provider_type,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *OCR2EVMRelayConfig) Reset() {
+ *x = OCR2EVMRelayConfig{}
+ mi := &file_job_spec_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *OCR2EVMRelayConfig) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*OCR2EVMRelayConfig) ProtoMessage() {}
+
+func (x *OCR2EVMRelayConfig) ProtoReflect() protoreflect.Message {
+ mi := &file_job_spec_proto_msgTypes[2]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use OCR2EVMRelayConfig.ProtoReflect.Descriptor instead.
+func (*OCR2EVMRelayConfig) Descriptor() ([]byte, []int) {
+ return file_job_spec_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *OCR2EVMRelayConfig) GetChainId() string {
+ if x != nil {
+ return x.ChainId
+ }
+ return ""
+}
+
+func (x *OCR2EVMRelayConfig) GetFromBlock() uint64 {
+ if x != nil {
+ return x.FromBlock
+ }
+ return 0
+}
+
+func (x *OCR2EVMRelayConfig) GetEffectiveTransmitterId() string {
+ if x != nil {
+ return x.EffectiveTransmitterId
+ }
+ return ""
+}
+
+func (x *OCR2EVMRelayConfig) GetEnableDualTransmission() bool {
+ if x != nil {
+ return x.EnableDualTransmission
+ }
+ return false
+}
+
+func (x *OCR2EVMRelayConfig) GetEnableTriggerCapability() bool {
+ if x != nil {
+ return x.EnableTriggerCapability
+ }
+ return false
+}
+
+func (x *OCR2EVMRelayConfig) GetLloDonId() uint64 {
+ if x != nil {
+ return x.LloDonId
+ }
+ return 0
+}
+
+func (x *OCR2EVMRelayConfig) GetFeedId() string {
+ if x != nil {
+ return x.FeedId
+ }
+ return ""
+}
+
+func (x *OCR2EVMRelayConfig) GetSendingKeys() []string {
+ if x != nil {
+ return x.SendingKeys
+ }
+ return nil
+}
+
+func (x *OCR2EVMRelayConfig) GetProviderType() string {
+ if x != nil {
+ return x.ProviderType
+ }
+ return ""
+}
+
+// OCR2MedianPluginConfig carries all fields of median/config.PluginConfig and
+// the nested JuelsPerFeeCoinCache.
+type OCR2MedianPluginConfig struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ JuelsPerFeeCoinSource string `protobuf:"bytes,1,opt,name=juels_per_fee_coin_source,json=juelsPerFeeCoinSource,proto3" json:"juels_per_fee_coin_source,omitempty"`
+ // Empty string when gasPriceSubunitsSource is not configured.
+ GasPriceSubunitsSource string `protobuf:"bytes,2,opt,name=gas_price_subunits_source,json=gasPriceSubunitsSource,proto3" json:"gas_price_subunits_source,omitempty"`
+ // juels_per_fee_coin_cache_disabled is true when JuelsPerFeeCoinCache is nil
+ // (disabled when nil, per source comment) or when Disable is explicitly true.
+ JuelsPerFeeCoinCacheDisabled bool `protobuf:"varint,3,opt,name=juels_per_fee_coin_cache_disabled,json=juelsPerFeeCoinCacheDisabled,proto3" json:"juels_per_fee_coin_cache_disabled,omitempty"`
+ JuelsPerFeeCoinCacheUpdateIntervalSeconds float64 `protobuf:"fixed64,4,opt,name=juels_per_fee_coin_cache_update_interval_seconds,json=juelsPerFeeCoinCacheUpdateIntervalSeconds,proto3" json:"juels_per_fee_coin_cache_update_interval_seconds,omitempty"`
+ JuelsPerFeeCoinCacheStalenessAlertThresholdSeconds float64 `protobuf:"fixed64,5,opt,name=juels_per_fee_coin_cache_staleness_alert_threshold_seconds,json=juelsPerFeeCoinCacheStalenessAlertThresholdSeconds,proto3" json:"juels_per_fee_coin_cache_staleness_alert_threshold_seconds,omitempty"`
+ // Verbatim JSON of DeviationFunctionDefinition (map[string]any).
+ DeviationFuncJson string `protobuf:"bytes,6,opt,name=deviation_func_json,json=deviationFuncJson,proto3" json:"deviation_func_json,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *OCR2MedianPluginConfig) Reset() {
+ *x = OCR2MedianPluginConfig{}
+ mi := &file_job_spec_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *OCR2MedianPluginConfig) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*OCR2MedianPluginConfig) ProtoMessage() {}
+
+func (x *OCR2MedianPluginConfig) ProtoReflect() protoreflect.Message {
+ mi := &file_job_spec_proto_msgTypes[3]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use OCR2MedianPluginConfig.ProtoReflect.Descriptor instead.
+func (*OCR2MedianPluginConfig) Descriptor() ([]byte, []int) {
+ return file_job_spec_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *OCR2MedianPluginConfig) GetJuelsPerFeeCoinSource() string {
+ if x != nil {
+ return x.JuelsPerFeeCoinSource
+ }
+ return ""
+}
+
+func (x *OCR2MedianPluginConfig) GetGasPriceSubunitsSource() string {
+ if x != nil {
+ return x.GasPriceSubunitsSource
+ }
+ return ""
+}
+
+func (x *OCR2MedianPluginConfig) GetJuelsPerFeeCoinCacheDisabled() bool {
+ if x != nil {
+ return x.JuelsPerFeeCoinCacheDisabled
+ }
+ return false
+}
+
+func (x *OCR2MedianPluginConfig) GetJuelsPerFeeCoinCacheUpdateIntervalSeconds() float64 {
+ if x != nil {
+ return x.JuelsPerFeeCoinCacheUpdateIntervalSeconds
+ }
+ return 0
+}
+
+func (x *OCR2MedianPluginConfig) GetJuelsPerFeeCoinCacheStalenessAlertThresholdSeconds() float64 {
+ if x != nil {
+ return x.JuelsPerFeeCoinCacheStalenessAlertThresholdSeconds
+ }
+ return 0
+}
+
+func (x *OCR2MedianPluginConfig) GetDeviationFuncJson() string {
+ if x != nil {
+ return x.DeviationFuncJson
+ }
+ return ""
+}
+
+var File_job_spec_proto protoreflect.FileDescriptor
+
+const file_job_spec_proto_rawDesc = "" +
+ "\n" +
+ "\x0ejob_spec.proto\x12\vjob_spec.v1\"\xe5\a\n" +
+ "\fJobSpecEvent\x12&\n" +
+ "\x0fexternal_job_id\x18\x01 \x01(\tR\rexternalJobId\x12&\n" +
+ "\x0finternal_job_id\x18\x02 \x01(\x05R\rinternalJobId\x12\x12\n" +
+ "\x04name\x18\x03 \x01(\tR\x04name\x12\x19\n" +
+ "\bjob_type\x18\x04 \x01(\tR\ajobType\x12%\n" +
+ "\x0eschema_version\x18\x05 \x01(\rR\rschemaVersion\x12\x1b\n" +
+ "\tgas_limit\x18\x06 \x01(\rR\bgasLimit\x12-\n" +
+ "\x12forwarding_allowed\x18\a \x01(\bR\x11forwardingAllowed\x12 \n" +
+ "\tstream_id\x18\b \x01(\rH\x00R\bstreamId\x88\x01\x01\x129\n" +
+ "\x19max_task_duration_seconds\x18\t \x01(\x01R\x16maxTaskDurationSeconds\x12\x1d\n" +
+ "\n" +
+ "created_at\x18\n" +
+ " \x01(\tR\tcreatedAt\x12-\n" +
+ "\x12observation_source\x18\v \x01(\tR\x11observationSource\x12(\n" +
+ "\x10pipeline_spec_id\x18\f \x01(\x05R\x0epipelineSpecId\x12!\n" +
+ "\fbridge_names\x18\r \x03(\tR\vbridgeNames\x12(\n" +
+ "\x10feeds_manager_id\x18\x0e \x01(\x03R\x0efeedsManagerId\x12\x1f\n" +
+ "\vremote_uuid\x18\x0f \x01(\tR\n" +
+ "remoteUuid\x12!\n" +
+ "\fspec_version\x18\x10 \x01(\x05R\vspecVersion\x12\x1f\n" +
+ "\vproposed_at\x18\x11 \x01(\tR\n" +
+ "proposedAt\x12\x1f\n" +
+ "\vapproved_at\x18\x12 \x01(\tR\n" +
+ "approvedAt\x124\n" +
+ "\x16accept_latency_seconds\x18\x13 \x01(\x01R\x14acceptLatencySeconds\x12I\n" +
+ "\x10ocr2_oracle_spec\x18\x14 \x01(\v2\x1f.job_spec.v1.OCR2OracleSpecInfoR\x0eocr2OracleSpec\x12$\n" +
+ "\x0ecsa_public_key\x18\x15 \x01(\tR\fcsaPublicKey\x12!\n" +
+ "\fnode_version\x18\x16 \x01(\tR\vnodeVersion\x12\x1a\n" +
+ "\bhostname\x18\x17 \x01(\tR\bhostname\x12)\n" +
+ "\x10emission_trigger\x18\x18 \x01(\tR\x0femissionTrigger\x12\x1c\n" +
+ "\ttimestamp\x18\x19 \x01(\tR\ttimestampB\f\n" +
+ "\n" +
+ "_stream_id\"\x96\t\n" +
+ "\x12OCR2OracleSpecInfo\x12\x17\n" +
+ "\aspec_id\x18\x01 \x01(\x05R\x06specId\x12\x1f\n" +
+ "\vcontract_id\x18\x02 \x01(\tR\n" +
+ "contractId\x12\x17\n" +
+ "\afeed_id\x18\x03 \x01(\tR\x06feedId\x12\x14\n" +
+ "\x05relay\x18\x04 \x01(\tR\x05relay\x12\x19\n" +
+ "\bchain_id\x18\x05 \x01(\tR\achainId\x12\x1f\n" +
+ "\vplugin_type\x18\x06 \x01(\tR\n" +
+ "pluginType\x12%\n" +
+ "\x0etransmitter_id\x18\a \x01(\tR\rtransmitterId\x12)\n" +
+ "\x11ocr_key_bundle_id\x18\b \x01(\tR\x0eocrKeyBundleId\x12/\n" +
+ "\x13monitoring_endpoint\x18\t \x01(\tR\x12monitoringEndpoint\x12/\n" +
+ "\x13p2pv2_bootstrappers\x18\n" +
+ " \x03(\tR\x12p2pv2Bootstrappers\x124\n" +
+ "\x16allow_no_bootstrappers\x18\v \x01(\bR\x14allowNoBootstrappers\x12<\n" +
+ "\x1ablockchain_timeout_seconds\x18\f \x01(\x01R\x18blockchainTimeoutSeconds\x12_\n" +
+ "-contract_config_tracker_poll_interval_seconds\x18\r \x01(\x01R(contractConfigTrackerPollIntervalSeconds\x12B\n" +
+ "\x1dcontract_config_confirmations\x18\x0e \x01(\rR\x1bcontractConfigConfirmations\x120\n" +
+ "\x14capture_ea_telemetry\x18\x0f \x01(\bR\x12captureEaTelemetry\x12M\n" +
+ "#capture_automation_custom_telemetry\x18\x10 \x01(\bR captureAutomationCustomTelemetry\x12&\n" +
+ "\x0fspec_created_at\x18\x11 \x01(\tR\rspecCreatedAt\x12&\n" +
+ "\x0fspec_updated_at\x18\x12 \x01(\tR\rspecUpdatedAt\x12*\n" +
+ "\x11relay_config_json\x18\x13 \x01(\tR\x0frelayConfigJson\x12,\n" +
+ "\x12plugin_config_json\x18\x14 \x01(\tR\x10pluginConfigJson\x12A\n" +
+ "\x1donchain_signing_strategy_json\x18\x15 \x01(\tR\x1aonchainSigningStrategyJson\x12I\n" +
+ "\x10evm_relay_config\x18\x16 \x01(\v2\x1f.job_spec.v1.OCR2EVMRelayConfigR\x0eevmRelayConfig\x12U\n" +
+ "\x14median_plugin_config\x18\x17 \x01(\v2#.job_spec.v1.OCR2MedianPluginConfigR\x12medianPluginConfig\"\xfd\x02\n" +
+ "\x12OCR2EVMRelayConfig\x12\x19\n" +
+ "\bchain_id\x18\x01 \x01(\tR\achainId\x12\x1d\n" +
+ "\n" +
+ "from_block\x18\x02 \x01(\x04R\tfromBlock\x128\n" +
+ "\x18effective_transmitter_id\x18\x03 \x01(\tR\x16effectiveTransmitterId\x128\n" +
+ "\x18enable_dual_transmission\x18\x04 \x01(\bR\x16enableDualTransmission\x12:\n" +
+ "\x19enable_trigger_capability\x18\x05 \x01(\bR\x17enableTriggerCapability\x12\x1c\n" +
+ "\n" +
+ "llo_don_id\x18\x06 \x01(\x04R\blloDonId\x12\x17\n" +
+ "\afeed_id\x18\a \x01(\tR\x06feedId\x12!\n" +
+ "\fsending_keys\x18\b \x03(\tR\vsendingKeys\x12#\n" +
+ "\rprovider_type\x18\t \x01(\tR\fproviderType\"\xe3\x03\n" +
+ "\x16OCR2MedianPluginConfig\x128\n" +
+ "\x19juels_per_fee_coin_source\x18\x01 \x01(\tR\x15juelsPerFeeCoinSource\x129\n" +
+ "\x19gas_price_subunits_source\x18\x02 \x01(\tR\x16gasPriceSubunitsSource\x12G\n" +
+ "!juels_per_fee_coin_cache_disabled\x18\x03 \x01(\bR\x1cjuelsPerFeeCoinCacheDisabled\x12c\n" +
+ "0juels_per_fee_coin_cache_update_interval_seconds\x18\x04 \x01(\x01R)juelsPerFeeCoinCacheUpdateIntervalSeconds\x12v\n" +
+ ":juels_per_fee_coin_cache_staleness_alert_threshold_seconds\x18\x05 \x01(\x01R2juelsPerFeeCoinCacheStalenessAlertThresholdSeconds\x12.\n" +
+ "\x13deviation_func_json\x18\x06 \x01(\tR\x11deviationFuncJsonBZZXgithub.com/smartcontractkit/chainlink/v2/core/services/nodestatusreporter/jobspec/eventsb\x06proto3"
+
+var (
+ file_job_spec_proto_rawDescOnce sync.Once
+ file_job_spec_proto_rawDescData []byte
+)
+
+func file_job_spec_proto_rawDescGZIP() []byte {
+ file_job_spec_proto_rawDescOnce.Do(func() {
+ file_job_spec_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_job_spec_proto_rawDesc), len(file_job_spec_proto_rawDesc)))
+ })
+ return file_job_spec_proto_rawDescData
+}
+
+var file_job_spec_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
+var file_job_spec_proto_goTypes = []any{
+ (*JobSpecEvent)(nil), // 0: job_spec.v1.JobSpecEvent
+ (*OCR2OracleSpecInfo)(nil), // 1: job_spec.v1.OCR2OracleSpecInfo
+ (*OCR2EVMRelayConfig)(nil), // 2: job_spec.v1.OCR2EVMRelayConfig
+ (*OCR2MedianPluginConfig)(nil), // 3: job_spec.v1.OCR2MedianPluginConfig
+}
+var file_job_spec_proto_depIdxs = []int32{
+ 1, // 0: job_spec.v1.JobSpecEvent.ocr2_oracle_spec:type_name -> job_spec.v1.OCR2OracleSpecInfo
+ 2, // 1: job_spec.v1.OCR2OracleSpecInfo.evm_relay_config:type_name -> job_spec.v1.OCR2EVMRelayConfig
+ 3, // 2: job_spec.v1.OCR2OracleSpecInfo.median_plugin_config:type_name -> job_spec.v1.OCR2MedianPluginConfig
+ 3, // [3:3] is the sub-list for method output_type
+ 3, // [3:3] is the sub-list for method input_type
+ 3, // [3:3] is the sub-list for extension type_name
+ 3, // [3:3] is the sub-list for extension extendee
+ 0, // [0:3] is the sub-list for field type_name
+}
+
+func init() { file_job_spec_proto_init() }
+func file_job_spec_proto_init() {
+ if File_job_spec_proto != nil {
+ return
+ }
+ file_job_spec_proto_msgTypes[0].OneofWrappers = []any{}
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: unsafe.Slice(unsafe.StringData(file_job_spec_proto_rawDesc), len(file_job_spec_proto_rawDesc)),
+ NumEnums: 0,
+ NumMessages: 4,
+ NumExtensions: 0,
+ NumServices: 0,
+ },
+ GoTypes: file_job_spec_proto_goTypes,
+ DependencyIndexes: file_job_spec_proto_depIdxs,
+ MessageInfos: file_job_spec_proto_msgTypes,
+ }.Build()
+ File_job_spec_proto = out.File
+ file_job_spec_proto_goTypes = nil
+ file_job_spec_proto_depIdxs = nil
+}
diff --git a/core/services/nodestatusreporter/jobspec/events/job_spec.proto b/core/services/nodestatusreporter/jobspec/events/job_spec.proto
new file mode 100644
index 00000000000..263e76b4121
--- /dev/null
+++ b/core/services/nodestatusreporter/jobspec/events/job_spec.proto
@@ -0,0 +1,119 @@
+syntax = "proto3";
+
+option go_package = "github.com/smartcontractkit/chainlink/v2/core/services/nodestatusreporter/jobspec/events";
+
+package job_spec.v1;
+
+// JobSpecEvent is emitted for each active job on a heartbeat, on job creation,
+// and on job deletion. For the initial rollout only offchainreporting2 jobs
+// with pluginType = "median" are emitted (configurable via EnabledOCR2PluginTypes
+// and EmitNonOCR2Jobs in the JobSpecReporter config section).
+message JobSpecEvent {
+ // Job identity — covers every TOML-writable field on job.Job plus DB-assigned columns.
+ string external_job_id = 1;
+ int32 internal_job_id = 2;
+ string name = 3;
+ string job_type = 4;
+ uint32 schema_version = 5;
+ uint32 gas_limit = 6;
+ bool forwarding_allowed = 7;
+ optional uint32 stream_id = 8;
+ double max_task_duration_seconds = 9;
+ string created_at = 10;
+
+ // Observation pipeline — the job's observationSource field.
+ string observation_source = 11;
+ int32 pipeline_spec_id = 12;
+
+ // Bridge names extracted from the observationSource DOT DAG (top-level only).
+ repeated string bridge_names = 13;
+
+ // Proposal lifecycle fields — zero/empty when the job was created manually
+ // (not via a Feeds Manager / Job Distributor).
+ int64 feeds_manager_id = 14;
+ string remote_uuid = 15;
+ int32 spec_version = 16;
+ string proposed_at = 17;
+ string approved_at = 18;
+ double accept_latency_seconds = 19;
+
+ // OCR2-specific fields — absent for non-OCR2 job types.
+ OCR2OracleSpecInfo ocr2_oracle_spec = 20;
+
+ // Node identity.
+ string csa_public_key = 21;
+ string node_version = 22;
+ string hostname = 23;
+
+ // Event metadata.
+ // emission_trigger is one of "heartbeat", "create", or "delete".
+ string emission_trigger = 24;
+ string timestamp = 25;
+}
+
+// OCR2OracleSpecInfo carries all fields of job.OCR2OracleSpec.
+message OCR2OracleSpecInfo {
+ int32 spec_id = 1;
+ string contract_id = 2;
+ string feed_id = 3;
+ string relay = 4;
+ string chain_id = 5;
+ string plugin_type = 6;
+ string transmitter_id = 7;
+ string ocr_key_bundle_id = 8;
+ string monitoring_endpoint = 9;
+ repeated string p2pv2_bootstrappers = 10;
+ bool allow_no_bootstrappers = 11;
+ double blockchain_timeout_seconds = 12;
+ double contract_config_tracker_poll_interval_seconds = 13;
+ uint32 contract_config_confirmations = 14;
+ bool capture_ea_telemetry = 15;
+ bool capture_automation_custom_telemetry = 16;
+ string spec_created_at = 17;
+ string spec_updated_at = 18;
+
+ // Raw JSON passthroughs — always populated; authoritative for any field not
+ // captured in the typed sub-messages below.
+ string relay_config_json = 19;
+ string plugin_config_json = 20;
+ string onchain_signing_strategy_json = 21;
+
+ // Typed EVM relay config — populated only when relay == "evm".
+ OCR2EVMRelayConfig evm_relay_config = 22;
+
+ // Typed median plugin config — populated only when plugin_type == "median".
+ OCR2MedianPluginConfig median_plugin_config = 23;
+}
+
+// OCR2EVMRelayConfig carries the well-known fields of the EVM relay config JSON.
+// relay_config_json on OCR2OracleSpecInfo remains authoritative for any field
+// not represented here.
+message OCR2EVMRelayConfig {
+ string chain_id = 1;
+ uint64 from_block = 2;
+ string effective_transmitter_id = 3;
+ bool enable_dual_transmission = 4;
+ bool enable_trigger_capability = 5;
+ uint64 llo_don_id = 6;
+ string feed_id = 7;
+ repeated string sending_keys = 8;
+ string provider_type = 9;
+}
+
+// OCR2MedianPluginConfig carries all fields of median/config.PluginConfig and
+// the nested JuelsPerFeeCoinCache.
+message OCR2MedianPluginConfig {
+ string juels_per_fee_coin_source = 1;
+
+ // Empty string when gasPriceSubunitsSource is not configured.
+ string gas_price_subunits_source = 2;
+
+ // juels_per_fee_coin_cache_disabled is true when JuelsPerFeeCoinCache is nil
+ // (disabled when nil, per source comment) or when Disable is explicitly true.
+ bool juels_per_fee_coin_cache_disabled = 3;
+ double juels_per_fee_coin_cache_update_interval_seconds = 4;
+ double juels_per_fee_coin_cache_staleness_alert_threshold_seconds = 5;
+
+ // Verbatim JSON of DeviationFunctionDefinition (map[string]any).
+ string deviation_func_json = 6;
+}
diff --git a/core/services/nodestatusreporter/jobspec/events/types.go b/core/services/nodestatusreporter/jobspec/events/types.go
new file mode 100644
index 00000000000..ed54756d06f
--- /dev/null
+++ b/core/services/nodestatusreporter/jobspec/events/types.go
@@ -0,0 +1,9 @@
+package events
+
+const (
+ ProtoPkg = "job_spec.v1"
+ // JobSpecEventEntity represents a Job Spec event
+ JobSpecEventEntity string = "JobSpecEvent"
+ // SchemaJobSpec represents the schema for Job Spec events
+ SchemaJobSpec string = "/job-spec-events/v1"
+)
diff --git a/core/services/nodestatusreporter/jobspec/job_spec_reporter.go b/core/services/nodestatusreporter/jobspec/job_spec_reporter.go
new file mode 100644
index 00000000000..4ecc91f67f3
--- /dev/null
+++ b/core/services/nodestatusreporter/jobspec/job_spec_reporter.go
@@ -0,0 +1,397 @@
+package jobspec
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/pkg/errors"
+ "google.golang.org/protobuf/proto"
+
+ "github.com/smartcontractkit/chainlink-common/pkg/beholder"
+ "github.com/smartcontractkit/chainlink-common/pkg/services"
+
+ coreconfig "github.com/smartcontractkit/chainlink/v2/core/config"
+ "github.com/smartcontractkit/chainlink/v2/core/logger"
+ medianconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/median/config"
+ "github.com/smartcontractkit/chainlink/v2/core/services/feeds"
+ "github.com/smartcontractkit/chainlink/v2/core/services/job"
+ "github.com/smartcontractkit/chainlink/v2/core/services/nodestatusreporter/jobspec/events"
+ "github.com/smartcontractkit/chainlink/v2/core/services/pipeline"
+
+ commontypes "github.com/smartcontractkit/chainlink-common/pkg/types"
+)
+
+// NodeInfo contains static node identity values injected at construction time.
+type NodeInfo struct {
+ CSAPublicKey string
+ NodeVersion string
+ Hostname string
+}
+
+const ServiceName = "JobSpecReporter"
+
+// Service polls active jobs and emits full job-spec telemetry via Beholder.
+// It mirrors the BridgeStatusReporter structure and implements job.Listener
+// to also emit immediately on create/delete.
+type Service struct {
+ services.Service
+ eng *services.Engine
+
+ config coreconfig.JobSpecReporter
+ spawner job.Spawner
+ feedsORM feeds.ORM // optional; nil-safe
+ emitter beholder.Emitter
+ nodeInfo NodeInfo
+}
+
+// NewJobSpecReporter creates a new Job Spec Reporter Service.
+func NewJobSpecReporter(
+ config coreconfig.JobSpecReporter,
+ spawner job.Spawner,
+ feedsORM feeds.ORM,
+ emitter beholder.Emitter,
+ nodeInfo NodeInfo,
+ lggr logger.Logger,
+) *Service {
+ s := &Service{
+ config: config,
+ spawner: spawner,
+ feedsORM: feedsORM,
+ emitter: emitter,
+ nodeInfo: nodeInfo,
+ }
+ s.Service, s.eng = services.Config{
+ Name: ServiceName,
+ Start: s.start,
+ }.NewServiceEngine(lggr)
+ return s
+}
+
+// start starts the Job Spec Reporter Service.
+func (s *Service) start(ctx context.Context) error {
+ if !s.config.Enabled() {
+ s.eng.Info("Job Spec Reporter Service is disabled")
+ return nil
+ }
+
+ s.eng.Info("Starting Job Spec Reporter Service")
+ s.spawner.RegisterListener(s)
+ ticker := services.NewTicker(s.config.PollingInterval())
+ s.eng.GoTick(ticker, s.pollAllJobs)
+
+ return nil
+}
+
+// HealthReport returns the service health.
+func (s *Service) HealthReport() map[string]error {
+ return map[string]error{ServiceName: s.Ready()}
+}
+
+// OnJobStarted implements job.Listener — called after a job service starts successfully.
+func (s *Service) OnJobStarted(ctx context.Context, jb job.Job) {
+ if !s.shouldEmit(&jb) {
+ return
+ }
+ if err := s.emitForJob(ctx, jb, "create"); err != nil {
+ s.eng.Warnw("Failed to emit job spec telemetry on create", "jobID", jb.ID, "error", err)
+ }
+}
+
+// OnJobStopped implements job.Listener — called after a job is deleted.
+func (s *Service) OnJobStopped(ctx context.Context, jb job.Job) {
+ if !s.shouldEmit(&jb) {
+ return
+ }
+ if err := s.emitForJob(ctx, jb, "delete"); err != nil {
+ s.eng.Warnw("Failed to emit job spec telemetry on delete", "jobID", jb.ID, "error", err)
+ }
+}
+
+// pollAllJobs is called on each heartbeat tick and emits telemetry for every
+// active job that passes the shouldEmit gate.
+func (s *Service) pollAllJobs(ctx context.Context) {
+ activeJobs := s.spawner.ActiveJobs()
+ for _, jb := range activeJobs {
+ jbCopy := jb
+ if !s.shouldEmit(&jbCopy) {
+ continue
+ }
+ if err := s.emitForJob(ctx, jbCopy, "heartbeat"); err != nil {
+ s.eng.Warnw("Failed to emit job spec telemetry", "jobID", jb.ID, "error", err)
+ }
+ }
+}
+
+// ShouldEmit is exported for testing. It returns true when the given job
+// should produce a telemetry event based on the current config.
+// Use shouldEmit for internal calls (identical logic).
+func (s *Service) ShouldEmit(j *job.Job) bool {
+ return s.shouldEmit(j)
+}
+
+// shouldEmit returns true when the given job should produce a telemetry event
+// based on the current config. This gate applies symmetrically to heartbeats
+// and create/delete events.
+func (s *Service) shouldEmit(j *job.Job) bool {
+ if j == nil {
+ return false
+ }
+ if j.Type != job.OffchainReporting2 || j.OCR2OracleSpec == nil {
+ return s.config.EmitNonOCR2Jobs()
+ }
+ allowed := s.config.EnabledOCR2PluginTypes()
+ if len(allowed) == 0 {
+ return true
+ }
+ pt := string(j.OCR2OracleSpec.PluginType)
+ for _, a := range allowed {
+ if a == pt {
+ return true
+ }
+ }
+ return false
+}
+
+// EmitForJob is exported for testing. It converts a job to a JobSpecEvent and
+// emits it via Beholder. Use emitForJob for internal calls (identical logic).
+func (s *Service) EmitForJob(ctx context.Context, jb job.Job, trigger string) error {
+ return s.emitForJob(ctx, jb, trigger)
+}
+
+// emitForJob converts a job to a JobSpecEvent and emits it via Beholder.
+func (s *Service) emitForJob(ctx context.Context, jb job.Job, trigger string) error {
+ event, err := s.buildEvent(ctx, jb, trigger)
+ if err != nil {
+ return fmt.Errorf("building event: %w", err)
+ }
+
+ if err := events.EmitJobSpecEvent(ctx, s.emitter, event); err != nil {
+ return fmt.Errorf("emitting event: %w", err)
+ }
+ return nil
+}
+
+// buildEvent converts a job.Job into the protobuf JobSpecEvent.
+func (s *Service) buildEvent(ctx context.Context, jb job.Job, trigger string) (*events.JobSpecEvent, error) {
+ event := &events.JobSpecEvent{
+ ExternalJobId: jb.ExternalJobID.String(),
+ InternalJobId: jb.ID,
+ Name: jb.Name.ValueOrZero(),
+ JobType: string(jb.Type),
+ SchemaVersion: jb.SchemaVersion,
+ ForwardingAllowed: jb.ForwardingAllowed,
+ MaxTaskDurationSeconds: jb.MaxTaskDuration.Duration().Seconds(),
+ CreatedAt: jb.CreatedAt.Format(time.RFC3339Nano),
+ CsaPublicKey: s.nodeInfo.CSAPublicKey,
+ NodeVersion: s.nodeInfo.NodeVersion,
+ Hostname: s.nodeInfo.Hostname,
+ EmissionTrigger: trigger,
+ Timestamp: time.Now().Format(time.RFC3339Nano),
+ }
+
+ if jb.GasLimit.Valid {
+ event.GasLimit = jb.GasLimit.Uint32
+ }
+ if jb.StreamID != nil {
+ sid := *jb.StreamID
+ event.StreamId = proto.Uint32(sid)
+ }
+
+ if jb.PipelineSpec != nil {
+ event.ObservationSource = jb.PipelineSpec.DotDagSource
+ event.PipelineSpecId = jb.PipelineSpec.ID
+ event.BridgeNames = extractBridgeNames(jb.Pipeline)
+ }
+
+ s.populateProposalLifecycle(ctx, jb, event)
+
+ if jb.Type == job.OffchainReporting2 && jb.OCR2OracleSpec != nil {
+ ocr2Info, err := buildOCR2OracleSpecInfo(jb.OCR2OracleSpec)
+ if err != nil {
+ return nil, fmt.Errorf("building OCR2OracleSpecInfo: %w", err)
+ }
+ event.Ocr2OracleSpec = ocr2Info
+ }
+
+ return event, nil
+}
+
+// populateProposalLifecycle fills the proposal/approval fields when the job was
+// created via the Feeds Manager. Missing rows (manually created job) are silently
+// ignored.
+func (s *Service) populateProposalLifecycle(ctx context.Context, jb job.Job, event *events.JobSpecEvent) {
+ if s.feedsORM == nil || jb.ExternalJobID == (uuid.UUID{}) {
+ return
+ }
+
+ prop, err := s.feedsORM.GetJobProposalByExternalJobID(ctx, jb.ExternalJobID)
+ if err != nil {
+ return
+ }
+
+ spec, err := s.feedsORM.GetApprovedSpec(ctx, prop.ID)
+ if err != nil {
+ return
+ }
+
+ event.FeedsManagerId = prop.FeedsManagerID
+ event.RemoteUuid = prop.RemoteUUID.String()
+ event.SpecVersion = int32(spec.Version)
+ event.ProposedAt = spec.CreatedAt.Format(time.RFC3339Nano)
+ event.ApprovedAt = spec.StatusUpdatedAt.Format(time.RFC3339Nano)
+ event.AcceptLatencySeconds = spec.StatusUpdatedAt.Sub(spec.CreatedAt).Seconds()
+}
+
+// extractBridgeNames returns the names of all bridge tasks in the top-level
+// observationSource pipeline. Tasks in sub-pipelines (e.g. juelsPerFeeCoinSource)
+// are not included.
+func extractBridgeNames(p pipeline.Pipeline) []string {
+ var names []string
+ for _, task := range p.Tasks {
+ if task.Type() == pipeline.TaskTypeBridge {
+ bt := task.(*pipeline.BridgeTask)
+ names = append(names, bt.Name)
+ }
+ }
+ return names
+}
+
+// evmRelayConfig is a minimal struct for decoding EVM relay config JSON fields
+// that we want to surface in OCR2EVMRelayConfig without importing the EVM module.
+type evmRelayConfig struct {
+ ChainID string `json:"chainID"`
+ FromBlock uint64 `json:"fromBlock"`
+ EffectiveTransmitterID string `json:"effectiveTransmitterID"`
+ EnableDualTransmission bool `json:"enableDualTransmission"`
+ EnableTriggerCapability bool `json:"enableTriggerCapability"`
+ LLODonID uint64 `json:"lloDonID"`
+ FeedID string `json:"feedID"`
+ SendingKeys []string `json:"sendingKeys"`
+ ProviderType string `json:"providerType"`
+}
+
+// buildOCR2OracleSpecInfo converts an OCR2OracleSpec into its proto representation.
+func buildOCR2OracleSpecInfo(spec *job.OCR2OracleSpec) (*events.OCR2OracleSpecInfo, error) {
+ relayConfigJSON := ""
+ if raw, err := json.Marshal(spec.RelayConfig); err == nil {
+ relayConfigJSON = string(raw)
+ }
+ pluginConfigJSON := ""
+ if raw, err := json.Marshal(spec.PluginConfig); err == nil {
+ pluginConfigJSON = string(raw)
+ }
+ onchainStrategyJSON := ""
+ if raw, err := json.Marshal(spec.OnchainSigningStrategy); err == nil {
+ onchainStrategyJSON = string(raw)
+ }
+
+ feedID := ""
+ if spec.FeedID != nil {
+ feedID = spec.FeedID.Hex()
+ }
+
+ info := &events.OCR2OracleSpecInfo{
+ SpecId: spec.ID,
+ ContractId: spec.ContractID,
+ FeedId: feedID,
+ Relay: spec.Relay,
+ ChainId: spec.ChainID,
+ PluginType: string(spec.PluginType),
+ TransmitterId: spec.TransmitterID.ValueOrZero(),
+ OcrKeyBundleId: spec.OCRKeyBundleID.ValueOrZero(),
+ MonitoringEndpoint: spec.MonitoringEndpoint.ValueOrZero(),
+ P2Pv2Bootstrappers: spec.P2PV2Bootstrappers,
+ AllowNoBootstrappers: spec.AllowNoBootstrappers,
+ BlockchainTimeoutSeconds: spec.BlockchainTimeout.Duration().Seconds(),
+ ContractConfigTrackerPollIntervalSeconds: spec.ContractConfigTrackerPollInterval.Duration().Seconds(),
+ ContractConfigConfirmations: uint32(spec.ContractConfigConfirmations),
+ CaptureEaTelemetry: spec.CaptureEATelemetry,
+ CaptureAutomationCustomTelemetry: spec.CaptureAutomationCustomTelemetry,
+ SpecCreatedAt: spec.CreatedAt.Format(time.RFC3339Nano),
+ SpecUpdatedAt: spec.UpdatedAt.Format(time.RFC3339Nano),
+ RelayConfigJson: relayConfigJSON,
+ PluginConfigJson: pluginConfigJSON,
+ OnchainSigningStrategyJson: onchainStrategyJSON,
+ }
+
+ if spec.Relay == "evm" {
+ evmCfg, err := buildEVMRelayConfig(spec)
+ if err != nil {
+ return nil, errors.Wrap(err, "building EVM relay config")
+ }
+ info.EvmRelayConfig = evmCfg
+ }
+
+ if spec.PluginType == commontypes.Median {
+ medianCfg, err := buildMedianPluginConfig(spec)
+ if err != nil {
+ return nil, errors.Wrap(err, "building median plugin config")
+ }
+ info.MedianPluginConfig = medianCfg
+ }
+
+ return info, nil
+}
+
+// buildEVMRelayConfig decodes the EVM relay config JSON into the proto message.
+func buildEVMRelayConfig(spec *job.OCR2OracleSpec) (*events.OCR2EVMRelayConfig, error) {
+ raw, err := json.Marshal(spec.RelayConfig)
+ if err != nil {
+ return nil, fmt.Errorf("marshaling relay config: %w", err)
+ }
+
+ var cfg evmRelayConfig
+ if err := json.Unmarshal(raw, &cfg); err != nil {
+ return nil, fmt.Errorf("unmarshaling EVM relay config: %w", err)
+ }
+
+ return &events.OCR2EVMRelayConfig{
+ ChainId: cfg.ChainID,
+ FromBlock: cfg.FromBlock,
+ EffectiveTransmitterId: cfg.EffectiveTransmitterID,
+ EnableDualTransmission: cfg.EnableDualTransmission,
+ EnableTriggerCapability: cfg.EnableTriggerCapability,
+ LloDonId: cfg.LLODonID,
+ FeedId: cfg.FeedID,
+ SendingKeys: cfg.SendingKeys,
+ ProviderType: cfg.ProviderType,
+ }, nil
+}
+
+// buildMedianPluginConfig decodes the plugin config JSON into the typed proto message.
+func buildMedianPluginConfig(spec *job.OCR2OracleSpec) (*events.OCR2MedianPluginConfig, error) {
+ raw, err := json.Marshal(spec.PluginConfig)
+ if err != nil {
+ return nil, fmt.Errorf("marshaling plugin config: %w", err)
+ }
+
+ var cfg medianconfig.PluginConfig
+ if err := json.Unmarshal(raw, &cfg); err != nil {
+ return nil, fmt.Errorf("unmarshaling median plugin config: %w", err)
+ }
+
+ medianProto := &events.OCR2MedianPluginConfig{
+ JuelsPerFeeCoinSource: cfg.JuelsPerFeeCoinPipeline,
+ GasPriceSubunitsSource: cfg.GasPriceSubunitsPipeline,
+ }
+
+ if cfg.JuelsPerFeeCoinCache == nil {
+ medianProto.JuelsPerFeeCoinCacheDisabled = true
+ } else {
+ medianProto.JuelsPerFeeCoinCacheDisabled = cfg.JuelsPerFeeCoinCache.Disable
+ medianProto.JuelsPerFeeCoinCacheUpdateIntervalSeconds = cfg.JuelsPerFeeCoinCache.UpdateInterval.Duration().Seconds()
+ medianProto.JuelsPerFeeCoinCacheStalenessAlertThresholdSeconds = cfg.JuelsPerFeeCoinCache.StalenessAlertThreshold.Duration().Seconds()
+ }
+
+ if cfg.DeviationFunctionDefinition != nil {
+ devFuncRaw, err := json.Marshal(cfg.DeviationFunctionDefinition)
+ if err == nil {
+ medianProto.DeviationFuncJson = string(devFuncRaw)
+ }
+ }
+
+ return medianProto, nil
+}
diff --git a/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go b/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go
new file mode 100644
index 00000000000..f13f7e3f2ef
--- /dev/null
+++ b/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go
@@ -0,0 +1,369 @@
+package jobspec_test
+
+import (
+ "context"
+ "database/sql"
+ "testing"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/protobuf/proto"
+ "gopkg.in/guregu/null.v4"
+
+ "github.com/smartcontractkit/chainlink-common/pkg/beholder"
+ "github.com/smartcontractkit/chainlink-common/pkg/beholder/beholdertest"
+ commontypes "github.com/smartcontractkit/chainlink-common/pkg/types"
+
+ "github.com/smartcontractkit/chainlink/v2/core/logger"
+ "github.com/smartcontractkit/chainlink/v2/core/services/feeds"
+ feedsmocks "github.com/smartcontractkit/chainlink/v2/core/services/feeds/mocks"
+ "github.com/smartcontractkit/chainlink/v2/core/services/job"
+ jobmocks "github.com/smartcontractkit/chainlink/v2/core/services/job/mocks"
+ "github.com/smartcontractkit/chainlink/v2/core/services/nodestatusreporter/jobspec"
+ "github.com/smartcontractkit/chainlink/v2/core/services/nodestatusreporter/jobspec/events"
+ "github.com/smartcontractkit/chainlink/v2/core/services/pipeline"
+)
+
+// stubConfig implements config.JobSpecReporter for tests.
+type stubConfig struct {
+ enabled bool
+ pollingInterval time.Duration
+ enabledOCR2PluginTypes []string
+ emitNonOCR2Jobs bool
+}
+
+func (s *stubConfig) Enabled() bool { return s.enabled }
+func (s *stubConfig) PollingInterval() time.Duration { return s.pollingInterval }
+func (s *stubConfig) EnabledOCR2PluginTypes() []string { return s.enabledOCR2PluginTypes }
+func (s *stubConfig) EmitNonOCR2Jobs() bool { return s.emitNonOCR2Jobs }
+
+func defaultConfig() *stubConfig {
+ return &stubConfig{
+ enabled: true,
+ pollingInterval: time.Hour,
+ enabledOCR2PluginTypes: []string{"median"},
+ emitNonOCR2Jobs: false,
+ }
+}
+
+// makeMedianJob creates a minimal OCR2 median job for testing.
+func makeMedianJob() job.Job {
+ extID := uuid.New()
+ return job.Job{
+ ID: 1,
+ ExternalJobID: extID,
+ Name: null.StringFrom("test-median-job"),
+ Type: job.OffchainReporting2,
+ SchemaVersion: 1,
+ PipelineSpec: &pipeline.Spec{
+ ID: 10,
+ DotDagSource: `ds1 [type=bridge name="my-bridge"]`,
+ },
+ Pipeline: pipeline.Pipeline{
+ Tasks: []pipeline.Task{
+ &pipeline.BridgeTask{
+ BaseTask: pipeline.NewBaseTask(0, "ds1", nil, nil, 0),
+ Name: "my-bridge",
+ },
+ },
+ },
+ OCR2OracleSpec: &job.OCR2OracleSpec{
+ ID: 1,
+ ContractID: "0x1234567890abcdef",
+ Relay: "evm",
+ ChainID: "1",
+ PluginType: commontypes.Median,
+ RelayConfig: job.JSONConfig{"chainID": "1"},
+ PluginConfig: job.JSONConfig{"juelsPerFeeCoinSource": `ds1 [type=http method=GET url="https://example.com"]`},
+ OnchainSigningStrategy: job.JSONConfig{},
+ P2PV2Bootstrappers: []string{"12D3KooW@host:6688"},
+ ContractConfigConfirmations: 1,
+ },
+ CreatedAt: time.Now(),
+ }
+}
+
+// makeNonMedianOCR2Job creates a non-median OCR2 job.
+func makeNonMedianOCR2Job() job.Job {
+ jb := makeMedianJob()
+ jb.ID = 2
+ jb.ExternalJobID = uuid.New()
+ jb.Name = null.StringFrom("test-keeper-job")
+ jb.OCR2OracleSpec = &job.OCR2OracleSpec{
+ ID: 2,
+ ContractID: "0xabcdef1234567890",
+ Relay: "evm",
+ ChainID: "1",
+ PluginType: commontypes.OCR2PluginType("ocr2keeper"),
+ RelayConfig: job.JSONConfig{"chainID": "1"},
+ PluginConfig: job.JSONConfig{},
+ OnchainSigningStrategy: job.JSONConfig{},
+ }
+ return jb
+}
+
+// makeVRFJob creates a non-OCR2 job.
+func makeVRFJob() job.Job {
+ return job.Job{
+ ID: 3,
+ ExternalJobID: uuid.New(),
+ Name: null.StringFrom("test-vrf-job"),
+ Type: job.VRF,
+ SchemaVersion: 1,
+ PipelineSpec: &pipeline.Spec{ID: 30, DotDagSource: ""},
+ Pipeline: pipeline.Pipeline{},
+ CreatedAt: time.Now(),
+ }
+}
+
+// newTestReporter creates a Service wired to the current global beholder emitter.
+// Call beholdertest.NewObserver(t) before this to set up the test emitter.
+func newTestReporter(t *testing.T, cfg *stubConfig, spawner job.Spawner, feedsORM feeds.ORM, nodeInfo jobspec.NodeInfo) *jobspec.Service {
+ t.Helper()
+ return jobspec.NewJobSpecReporter(cfg, spawner, feedsORM, beholder.GetEmitter(), nodeInfo, logger.TestLogger(t))
+}
+
+// ── shouldEmit gate tests ──────────────────────────────────────────────────────
+
+func TestShouldEmit_DefaultConfig(t *testing.T) {
+ beholdertest.NewObserver(t)
+ spawner := jobmocks.NewSpawner(t)
+ spawner.On("RegisterListener", mock.Anything).Maybe()
+
+ svc := newTestReporter(t, defaultConfig(), spawner, nil, jobspec.NodeInfo{})
+
+ cases := []struct {
+ name string
+ jb *job.Job
+ want bool
+ }{
+ {"median OCR2 job emits", jobPtr(makeMedianJob()), true},
+ {"non-median OCR2 job skipped", jobPtr(makeNonMedianOCR2Job()), false},
+ {"non-OCR2 (VRF) job skipped", jobPtr(makeVRFJob()), false},
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ assert.Equal(t, tc.want, svc.ShouldEmit(tc.jb))
+ })
+ }
+}
+
+func TestShouldEmit_AllOCR2Types(t *testing.T) {
+ beholdertest.NewObserver(t)
+ cfg := defaultConfig()
+ cfg.enabledOCR2PluginTypes = []string{} // empty = allow all
+
+ spawner := jobmocks.NewSpawner(t)
+ spawner.On("RegisterListener", mock.Anything).Maybe()
+ svc := newTestReporter(t, cfg, spawner, nil, jobspec.NodeInfo{})
+
+ assert.True(t, svc.ShouldEmit(jobPtr(makeMedianJob())))
+ assert.True(t, svc.ShouldEmit(jobPtr(makeNonMedianOCR2Job())))
+ assert.False(t, svc.ShouldEmit(jobPtr(makeVRFJob())))
+}
+
+func TestShouldEmit_NonOCR2Enabled(t *testing.T) {
+ beholdertest.NewObserver(t)
+ cfg := defaultConfig()
+ cfg.emitNonOCR2Jobs = true
+
+ spawner := jobmocks.NewSpawner(t)
+ spawner.On("RegisterListener", mock.Anything).Maybe()
+ svc := newTestReporter(t, cfg, spawner, nil, jobspec.NodeInfo{})
+
+ assert.True(t, svc.ShouldEmit(jobPtr(makeVRFJob())))
+ assert.True(t, svc.ShouldEmit(jobPtr(makeMedianJob())))
+}
+
+// ── conversion tests ──────────────────────────────────────────────────────────
+
+func TestBuildEvent_MedianJob(t *testing.T) {
+ observer := beholdertest.NewObserver(t)
+ spawner := jobmocks.NewSpawner(t)
+ spawner.On("RegisterListener", mock.Anything).Maybe()
+
+ feedsORM := feedsmocks.NewORM(t)
+ jb := makeMedianJob()
+ feedsORM.On("GetJobProposalByExternalJobID", mock.Anything, jb.ExternalJobID).Return(nil, sql.ErrNoRows).Maybe()
+
+ svc := newTestReporter(t, defaultConfig(), spawner, feedsORM, jobspec.NodeInfo{
+ CSAPublicKey: "csa-key",
+ NodeVersion: "1.0.0",
+ Hostname: "test-host",
+ })
+
+ err := svc.EmitForJob(context.Background(), jb, "heartbeat")
+ require.NoError(t, err)
+
+ msgs := observer.Messages(t, "beholder_entity", events.ProtoPkg+"."+events.JobSpecEventEntity)
+ require.Len(t, msgs, 1)
+
+ var ev events.JobSpecEvent
+ require.NoError(t, proto.Unmarshal(msgs[0].Body, &ev))
+
+ assert.Equal(t, jb.ExternalJobID.String(), ev.ExternalJobId)
+ assert.Equal(t, jb.ID, ev.InternalJobId)
+ assert.Equal(t, "test-median-job", ev.Name)
+ assert.Equal(t, "offchainreporting2", ev.JobType)
+ assert.Equal(t, "heartbeat", ev.EmissionTrigger)
+ assert.Equal(t, "csa-key", ev.CsaPublicKey)
+ assert.Equal(t, "1.0.0", ev.NodeVersion)
+ assert.Equal(t, "test-host", ev.Hostname)
+ assert.Equal(t, []string{"my-bridge"}, ev.BridgeNames)
+ require.NotNil(t, ev.Ocr2OracleSpec)
+ assert.Equal(t, "evm", ev.Ocr2OracleSpec.Relay)
+ assert.Equal(t, "median", ev.Ocr2OracleSpec.PluginType)
+ require.NotNil(t, ev.Ocr2OracleSpec.MedianPluginConfig)
+ assert.NotEmpty(t, ev.Ocr2OracleSpec.MedianPluginConfig.JuelsPerFeeCoinSource)
+ require.NotNil(t, ev.Ocr2OracleSpec.EvmRelayConfig)
+ assert.Equal(t, "1", ev.Ocr2OracleSpec.EvmRelayConfig.ChainId)
+}
+
+func TestBuildEvent_NonMedianOCR2Job(t *testing.T) {
+ observer := beholdertest.NewObserver(t)
+ spawner := jobmocks.NewSpawner(t)
+ spawner.On("RegisterListener", mock.Anything).Maybe()
+ feedsORM := feedsmocks.NewORM(t)
+ jb := makeNonMedianOCR2Job()
+ feedsORM.On("GetJobProposalByExternalJobID", mock.Anything, jb.ExternalJobID).Return(nil, sql.ErrNoRows).Maybe()
+
+ svc := newTestReporter(t, defaultConfig(), spawner, feedsORM, jobspec.NodeInfo{})
+ err := svc.EmitForJob(context.Background(), jb, "create")
+ require.NoError(t, err)
+
+ msgs := observer.Messages(t, "beholder_entity", events.ProtoPkg+"."+events.JobSpecEventEntity)
+ require.Len(t, msgs, 1)
+
+ var ev events.JobSpecEvent
+ require.NoError(t, proto.Unmarshal(msgs[0].Body, &ev))
+
+ require.NotNil(t, ev.Ocr2OracleSpec)
+ assert.Equal(t, "ocr2keeper", ev.Ocr2OracleSpec.PluginType)
+ assert.Nil(t, ev.Ocr2OracleSpec.MedianPluginConfig) // not median
+ assert.NotEmpty(t, ev.Ocr2OracleSpec.RelayConfigJson)
+}
+
+func TestBuildEvent_NonOCR2Job(t *testing.T) {
+ observer := beholdertest.NewObserver(t)
+ spawner := jobmocks.NewSpawner(t)
+ spawner.On("RegisterListener", mock.Anything).Maybe()
+
+ svc := newTestReporter(t, defaultConfig(), spawner, nil, jobspec.NodeInfo{})
+
+ jb := makeVRFJob()
+ err := svc.EmitForJob(context.Background(), jb, "heartbeat")
+ require.NoError(t, err)
+
+ msgs := observer.Messages(t, "beholder_entity", events.ProtoPkg+"."+events.JobSpecEventEntity)
+ require.Len(t, msgs, 1)
+
+ var ev events.JobSpecEvent
+ require.NoError(t, proto.Unmarshal(msgs[0].Body, &ev))
+
+ assert.Equal(t, "vrf", ev.JobType)
+ assert.Nil(t, ev.Ocr2OracleSpec) // no OCR2 spec
+}
+
+// ── OnJobStarted / OnJobStopped listener tests ────────────────────────────────
+
+func TestOnJobStarted_EmitsCreate(t *testing.T) {
+ observer := beholdertest.NewObserver(t)
+ spawner := jobmocks.NewSpawner(t)
+ spawner.On("RegisterListener", mock.Anything).Maybe()
+ feedsORM := feedsmocks.NewORM(t)
+ jb := makeMedianJob()
+ feedsORM.On("GetJobProposalByExternalJobID", mock.Anything, jb.ExternalJobID).Return(nil, sql.ErrNoRows).Maybe()
+
+ svc := newTestReporter(t, defaultConfig(), spawner, feedsORM, jobspec.NodeInfo{})
+ svc.OnJobStarted(context.Background(), jb)
+
+ msgs := observer.Messages(t, "beholder_entity", events.ProtoPkg+"."+events.JobSpecEventEntity)
+ require.Len(t, msgs, 1)
+
+ var ev events.JobSpecEvent
+ require.NoError(t, proto.Unmarshal(msgs[0].Body, &ev))
+ assert.Equal(t, "create", ev.EmissionTrigger)
+}
+
+func TestOnJobStopped_EmitsDelete(t *testing.T) {
+ observer := beholdertest.NewObserver(t)
+ spawner := jobmocks.NewSpawner(t)
+ spawner.On("RegisterListener", mock.Anything).Maybe()
+ feedsORM := feedsmocks.NewORM(t)
+ jb := makeMedianJob()
+ feedsORM.On("GetJobProposalByExternalJobID", mock.Anything, jb.ExternalJobID).Return(nil, sql.ErrNoRows).Maybe()
+
+ svc := newTestReporter(t, defaultConfig(), spawner, feedsORM, jobspec.NodeInfo{})
+ svc.OnJobStopped(context.Background(), jb)
+
+ msgs := observer.Messages(t, "beholder_entity", events.ProtoPkg+"."+events.JobSpecEventEntity)
+ require.Len(t, msgs, 1)
+
+ var ev events.JobSpecEvent
+ require.NoError(t, proto.Unmarshal(msgs[0].Body, &ev))
+ assert.Equal(t, "delete", ev.EmissionTrigger)
+}
+
+func TestOnJobStarted_SkippedWhenGateFails(t *testing.T) {
+ observer := beholdertest.NewObserver(t)
+ spawner := jobmocks.NewSpawner(t)
+ spawner.On("RegisterListener", mock.Anything).Maybe()
+
+ // default config only allows median, so VRF should not emit
+ svc := newTestReporter(t, defaultConfig(), spawner, nil, jobspec.NodeInfo{})
+ svc.OnJobStarted(context.Background(), makeVRFJob())
+
+ msgs := observer.Messages(t, "beholder_entity", events.ProtoPkg+"."+events.JobSpecEventEntity)
+ require.Empty(t, msgs)
+}
+
+// ── proposal-latency test ─────────────────────────────────────────────────────
+
+func TestBuildEvent_ProposalLifecycle(t *testing.T) {
+ observer := beholdertest.NewObserver(t)
+ spawner := jobmocks.NewSpawner(t)
+ spawner.On("RegisterListener", mock.Anything).Maybe()
+ feedsORM := feedsmocks.NewORM(t)
+
+ jb := makeMedianJob()
+ proposedAt := time.Now().Add(-5 * time.Minute)
+ approvedAt := time.Now().Add(-2 * time.Minute)
+
+ prop := &feeds.JobProposal{
+ ID: 100,
+ FeedsManagerID: 7,
+ RemoteUUID: uuid.New(),
+ }
+ spec := &feeds.JobProposalSpec{
+ ID: 200,
+ Version: 3,
+ CreatedAt: proposedAt,
+ StatusUpdatedAt: approvedAt,
+ }
+
+ feedsORM.On("GetJobProposalByExternalJobID", mock.Anything, jb.ExternalJobID).Return(prop, nil)
+ feedsORM.On("GetApprovedSpec", mock.Anything, prop.ID).Return(spec, nil)
+
+ svc := newTestReporter(t, defaultConfig(), spawner, feedsORM, jobspec.NodeInfo{})
+ err := svc.EmitForJob(context.Background(), jb, "heartbeat")
+ require.NoError(t, err)
+
+ msgs := observer.Messages(t, "beholder_entity", events.ProtoPkg+"."+events.JobSpecEventEntity)
+ require.Len(t, msgs, 1)
+
+ var ev events.JobSpecEvent
+ require.NoError(t, proto.Unmarshal(msgs[0].Body, &ev))
+
+ assert.Equal(t, int64(7), ev.FeedsManagerId)
+ assert.Equal(t, prop.RemoteUUID.String(), ev.RemoteUuid)
+ assert.Equal(t, int32(3), ev.SpecVersion)
+ assert.InDelta(t, approvedAt.Sub(proposedAt).Seconds(), ev.AcceptLatencySeconds, 1.0)
+}
+
+// ── helpers ───────────────────────────────────────────────────────────────────
+
+func jobPtr(jb job.Job) *job.Job { return &jb }
From d0a6cf8023dfad0a181dae5c71b63fd8b0d78e96 Mon Sep 17 00:00:00 2001
From: thomjg <103059015+thomjg@users.noreply.github.com>
Date: Mon, 20 Apr 2026 21:38:06 -0400
Subject: [PATCH 02/14] Modifying JobSpecReporter to reduce complexity
---
core/services/chainlink/application.go | 13 +-
core/services/job/spawner.go | 22 +-
.../jobspec/events/emit_test.go | 4 +-
.../jobspec/events/job_spec.pb.go | 105 ++++++--
.../jobspec/events/job_spec.proto | 11 +-
.../jobspec/job_spec_reporter.go | 239 ++++++++----------
.../jobspec/job_spec_reporter_test.go | 122 ++++-----
7 files changed, 276 insertions(+), 240 deletions(-)
diff --git a/core/services/chainlink/application.go b/core/services/chainlink/application.go
index 8a0ec574f8e..57b190a2c63 100644
--- a/core/services/chainlink/application.go
+++ b/core/services/chainlink/application.go
@@ -802,8 +802,9 @@ func NewApplication(ctx context.Context, opts ApplicationOpts) (Application, err
srvcs = append(srvcs, jobSpawner, pipelineRunner)
var feedsService feeds.Service
+ var feedsORM feeds.ORM
if cfg.Feature().FeedsManager() {
- feedsORM := feeds.NewORM(opts.DS, globalLogger)
+ feedsORM = feeds.NewORM(opts.DS, globalLogger)
feedsService = feeds.NewService(
feedsORM,
jobORM,
@@ -827,16 +828,14 @@ func NewApplication(ctx context.Context, opts ApplicationOpts) (Application, err
}
hostname, _ := os.Hostname()
- var feedsORMForReporter feeds.ORM
- if cfg.Feature().FeedsManager() {
- feedsORMForReporter = feeds.NewORM(opts.DS, globalLogger)
- }
jobSpecReporter := jobspec.NewJobSpecReporter(
cfg.JobSpecReporter(),
jobSpawner,
- feedsORMForReporter,
+ feedsORM,
beholder.GetEmitter(),
- jobspec.NodeInfo{CSAPublicKey: csaPubKeyHex, NodeVersion: static.Version, Hostname: hostname},
+ csaPubKeyHex,
+ static.Version,
+ hostname,
globalLogger,
)
srvcs = append(srvcs, jobSpecReporter)
diff --git a/core/services/job/spawner.go b/core/services/job/spawner.go
index 08e78043025..fab27516ec9 100644
--- a/core/services/job/spawner.go
+++ b/core/services/job/spawner.go
@@ -288,7 +288,7 @@ func (js *spawner) CreateJob(ctx context.Context, ds sqlutil.DataSource, jb *Job
js.lggr.Errorw("Error starting job services", "type", jb.Type, "jobID", jb.ID, "err", err)
} else {
js.lggr.Infow("Started job services", "type", jb.Type, "jobID", jb.ID)
- js.notifyListeners(true, *jb)
+ js.notifyStarted(*jb)
}
delegate.AfterJobCreated(*jb)
@@ -355,7 +355,7 @@ func (js *spawner) DeleteJob(ctx context.Context, ds sqlutil.DataSource, jobID i
if exists {
// Stop the service and remove the job from memory, which will always happen even if closing the services fail.
js.stopService(jobID)
- js.notifyListeners(false, aj.spec)
+ js.notifyStopped(aj.spec)
}
lggr.Infow("Stopped and deleted job")
@@ -379,9 +379,17 @@ func (js *spawner) RegisterListener(l Listener) {
js.listeners = append(js.listeners, l)
}
-// notifyListeners dispatches an event to all registered listeners in a
+func (js *spawner) notifyStarted(jb Job) {
+ js.dispatchToListeners(func(ctx context.Context, l Listener) { l.OnJobStarted(ctx, jb) })
+}
+
+func (js *spawner) notifyStopped(jb Job) {
+ js.dispatchToListeners(func(ctx context.Context, l Listener) { l.OnJobStopped(ctx, jb) })
+}
+
+// dispatchToListeners invokes fn against each registered listener in a
// best-effort, non-blocking, panic-safe goroutine.
-func (js *spawner) notifyListeners(started bool, jb Job) {
+func (js *spawner) dispatchToListeners(fn func(context.Context, Listener)) {
js.listenersMu.RLock()
ls := make([]Listener, len(js.listeners))
copy(ls, js.listeners)
@@ -400,11 +408,7 @@ func (js *spawner) notifyListeners(started bool, jb Job) {
}
}()
for _, l := range ls {
- if started {
- l.OnJobStarted(ctx, jb)
- } else {
- l.OnJobStopped(ctx, jb)
- }
+ fn(ctx, l)
}
}()
}
diff --git a/core/services/nodestatusreporter/jobspec/events/emit_test.go b/core/services/nodestatusreporter/jobspec/events/emit_test.go
index 6c742afe512..4d491b2af58 100644
--- a/core/services/nodestatusreporter/jobspec/events/emit_test.go
+++ b/core/services/nodestatusreporter/jobspec/events/emit_test.go
@@ -23,7 +23,7 @@ func TestEmitJobSpecEvent_RoundTrip(t *testing.T) {
InternalJobId: 42,
Name: "test-job",
JobType: "offchainreporting2",
- EmissionTrigger: "heartbeat",
+ EmissionTrigger: events.EmissionTrigger_EMISSION_TRIGGER_HEARTBEAT,
}
err := events.EmitJobSpecEvent(context.Background(), emitter, event)
@@ -41,7 +41,7 @@ func TestEmitJobSpecEvent_RoundTrip(t *testing.T) {
require.Equal(t, "test-job-id", decoded.ExternalJobId)
require.Equal(t, int32(42), decoded.InternalJobId)
require.Equal(t, "test-job", decoded.Name)
- require.Equal(t, "heartbeat", decoded.EmissionTrigger)
+ require.Equal(t, events.EmissionTrigger_EMISSION_TRIGGER_HEARTBEAT, decoded.EmissionTrigger)
require.NotEmpty(t, decoded.Timestamp)
}
diff --git a/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go b/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go
index 103d2eecd53..0b5b688a876 100644
--- a/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go
+++ b/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go
@@ -21,6 +21,59 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
+// EmissionTrigger identifies the event that caused the JobSpecEvent to be emitted.
+type EmissionTrigger int32
+
+const (
+ EmissionTrigger_EMISSION_TRIGGER_UNSPECIFIED EmissionTrigger = 0
+ EmissionTrigger_EMISSION_TRIGGER_HEARTBEAT EmissionTrigger = 1
+ EmissionTrigger_EMISSION_TRIGGER_CREATE EmissionTrigger = 2
+ EmissionTrigger_EMISSION_TRIGGER_DELETE EmissionTrigger = 3
+)
+
+// Enum value maps for EmissionTrigger.
+var (
+ EmissionTrigger_name = map[int32]string{
+ 0: "EMISSION_TRIGGER_UNSPECIFIED",
+ 1: "EMISSION_TRIGGER_HEARTBEAT",
+ 2: "EMISSION_TRIGGER_CREATE",
+ 3: "EMISSION_TRIGGER_DELETE",
+ }
+ EmissionTrigger_value = map[string]int32{
+ "EMISSION_TRIGGER_UNSPECIFIED": 0,
+ "EMISSION_TRIGGER_HEARTBEAT": 1,
+ "EMISSION_TRIGGER_CREATE": 2,
+ "EMISSION_TRIGGER_DELETE": 3,
+ }
+)
+
+func (x EmissionTrigger) Enum() *EmissionTrigger {
+ p := new(EmissionTrigger)
+ *p = x
+ return p
+}
+
+func (x EmissionTrigger) String() string {
+ return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (EmissionTrigger) Descriptor() protoreflect.EnumDescriptor {
+ return file_job_spec_proto_enumTypes[0].Descriptor()
+}
+
+func (EmissionTrigger) Type() protoreflect.EnumType {
+ return &file_job_spec_proto_enumTypes[0]
+}
+
+func (x EmissionTrigger) Number() protoreflect.EnumNumber {
+ return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use EmissionTrigger.Descriptor instead.
+func (EmissionTrigger) EnumDescriptor() ([]byte, []int) {
+ return file_job_spec_proto_rawDescGZIP(), []int{0}
+}
+
// JobSpecEvent is emitted for each active job on a heartbeat, on job creation,
// and on job deletion. For the initial rollout only offchainreporting2 jobs
// with pluginType = "median" are emitted (configurable via EnabledOCR2PluginTypes
@@ -58,9 +111,8 @@ type JobSpecEvent struct {
NodeVersion string `protobuf:"bytes,22,opt,name=node_version,json=nodeVersion,proto3" json:"node_version,omitempty"`
Hostname string `protobuf:"bytes,23,opt,name=hostname,proto3" json:"hostname,omitempty"`
// Event metadata.
- // emission_trigger is one of "heartbeat", "create", or "delete".
- EmissionTrigger string `protobuf:"bytes,24,opt,name=emission_trigger,json=emissionTrigger,proto3" json:"emission_trigger,omitempty"`
- Timestamp string `protobuf:"bytes,25,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
+ EmissionTrigger EmissionTrigger `protobuf:"varint,24,opt,name=emission_trigger,json=emissionTrigger,proto3,enum=job_spec.v1.EmissionTrigger" json:"emission_trigger,omitempty"`
+ Timestamp string `protobuf:"bytes,25,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -256,11 +308,11 @@ func (x *JobSpecEvent) GetHostname() string {
return ""
}
-func (x *JobSpecEvent) GetEmissionTrigger() string {
+func (x *JobSpecEvent) GetEmissionTrigger() EmissionTrigger {
if x != nil {
return x.EmissionTrigger
}
- return ""
+ return EmissionTrigger_EMISSION_TRIGGER_UNSPECIFIED
}
func (x *JobSpecEvent) GetTimestamp() string {
@@ -700,7 +752,7 @@ var File_job_spec_proto protoreflect.FileDescriptor
const file_job_spec_proto_rawDesc = "" +
"\n" +
- "\x0ejob_spec.proto\x12\vjob_spec.v1\"\xe5\a\n" +
+ "\x0ejob_spec.proto\x12\vjob_spec.v1\"\x83\b\n" +
"\fJobSpecEvent\x12&\n" +
"\x0fexternal_job_id\x18\x01 \x01(\tR\rexternalJobId\x12&\n" +
"\x0finternal_job_id\x18\x02 \x01(\x05R\rinternalJobId\x12\x12\n" +
@@ -729,8 +781,8 @@ const file_job_spec_proto_rawDesc = "" +
"\x10ocr2_oracle_spec\x18\x14 \x01(\v2\x1f.job_spec.v1.OCR2OracleSpecInfoR\x0eocr2OracleSpec\x12$\n" +
"\x0ecsa_public_key\x18\x15 \x01(\tR\fcsaPublicKey\x12!\n" +
"\fnode_version\x18\x16 \x01(\tR\vnodeVersion\x12\x1a\n" +
- "\bhostname\x18\x17 \x01(\tR\bhostname\x12)\n" +
- "\x10emission_trigger\x18\x18 \x01(\tR\x0femissionTrigger\x12\x1c\n" +
+ "\bhostname\x18\x17 \x01(\tR\bhostname\x12G\n" +
+ "\x10emission_trigger\x18\x18 \x01(\x0e2\x1c.job_spec.v1.EmissionTriggerR\x0femissionTrigger\x12\x1c\n" +
"\ttimestamp\x18\x19 \x01(\tR\ttimestampB\f\n" +
"\n" +
"_stream_id\"\x96\t\n" +
@@ -779,7 +831,12 @@ const file_job_spec_proto_rawDesc = "" +
"!juels_per_fee_coin_cache_disabled\x18\x03 \x01(\bR\x1cjuelsPerFeeCoinCacheDisabled\x12c\n" +
"0juels_per_fee_coin_cache_update_interval_seconds\x18\x04 \x01(\x01R)juelsPerFeeCoinCacheUpdateIntervalSeconds\x12v\n" +
":juels_per_fee_coin_cache_staleness_alert_threshold_seconds\x18\x05 \x01(\x01R2juelsPerFeeCoinCacheStalenessAlertThresholdSeconds\x12.\n" +
- "\x13deviation_func_json\x18\x06 \x01(\tR\x11deviationFuncJsonBZZXgithub.com/smartcontractkit/chainlink/v2/core/services/nodestatusreporter/jobspec/eventsb\x06proto3"
+ "\x13deviation_func_json\x18\x06 \x01(\tR\x11deviationFuncJson*\x8d\x01\n" +
+ "\x0fEmissionTrigger\x12 \n" +
+ "\x1cEMISSION_TRIGGER_UNSPECIFIED\x10\x00\x12\x1e\n" +
+ "\x1aEMISSION_TRIGGER_HEARTBEAT\x10\x01\x12\x1b\n" +
+ "\x17EMISSION_TRIGGER_CREATE\x10\x02\x12\x1b\n" +
+ "\x17EMISSION_TRIGGER_DELETE\x10\x03BZZXgithub.com/smartcontractkit/chainlink/v2/core/services/nodestatusreporter/jobspec/eventsb\x06proto3"
var (
file_job_spec_proto_rawDescOnce sync.Once
@@ -793,22 +850,25 @@ func file_job_spec_proto_rawDescGZIP() []byte {
return file_job_spec_proto_rawDescData
}
+var file_job_spec_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_job_spec_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_job_spec_proto_goTypes = []any{
- (*JobSpecEvent)(nil), // 0: job_spec.v1.JobSpecEvent
- (*OCR2OracleSpecInfo)(nil), // 1: job_spec.v1.OCR2OracleSpecInfo
- (*OCR2EVMRelayConfig)(nil), // 2: job_spec.v1.OCR2EVMRelayConfig
- (*OCR2MedianPluginConfig)(nil), // 3: job_spec.v1.OCR2MedianPluginConfig
+ (EmissionTrigger)(0), // 0: job_spec.v1.EmissionTrigger
+ (*JobSpecEvent)(nil), // 1: job_spec.v1.JobSpecEvent
+ (*OCR2OracleSpecInfo)(nil), // 2: job_spec.v1.OCR2OracleSpecInfo
+ (*OCR2EVMRelayConfig)(nil), // 3: job_spec.v1.OCR2EVMRelayConfig
+ (*OCR2MedianPluginConfig)(nil), // 4: job_spec.v1.OCR2MedianPluginConfig
}
var file_job_spec_proto_depIdxs = []int32{
- 1, // 0: job_spec.v1.JobSpecEvent.ocr2_oracle_spec:type_name -> job_spec.v1.OCR2OracleSpecInfo
- 2, // 1: job_spec.v1.OCR2OracleSpecInfo.evm_relay_config:type_name -> job_spec.v1.OCR2EVMRelayConfig
- 3, // 2: job_spec.v1.OCR2OracleSpecInfo.median_plugin_config:type_name -> job_spec.v1.OCR2MedianPluginConfig
- 3, // [3:3] is the sub-list for method output_type
- 3, // [3:3] is the sub-list for method input_type
- 3, // [3:3] is the sub-list for extension type_name
- 3, // [3:3] is the sub-list for extension extendee
- 0, // [0:3] is the sub-list for field type_name
+ 2, // 0: job_spec.v1.JobSpecEvent.ocr2_oracle_spec:type_name -> job_spec.v1.OCR2OracleSpecInfo
+ 0, // 1: job_spec.v1.JobSpecEvent.emission_trigger:type_name -> job_spec.v1.EmissionTrigger
+ 3, // 2: job_spec.v1.OCR2OracleSpecInfo.evm_relay_config:type_name -> job_spec.v1.OCR2EVMRelayConfig
+ 4, // 3: job_spec.v1.OCR2OracleSpecInfo.median_plugin_config:type_name -> job_spec.v1.OCR2MedianPluginConfig
+ 4, // [4:4] is the sub-list for method output_type
+ 4, // [4:4] is the sub-list for method input_type
+ 4, // [4:4] is the sub-list for extension type_name
+ 4, // [4:4] is the sub-list for extension extendee
+ 0, // [0:4] is the sub-list for field type_name
}
func init() { file_job_spec_proto_init() }
@@ -822,13 +882,14 @@ func file_job_spec_proto_init() {
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_job_spec_proto_rawDesc), len(file_job_spec_proto_rawDesc)),
- NumEnums: 0,
+ NumEnums: 1,
NumMessages: 4,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_job_spec_proto_goTypes,
DependencyIndexes: file_job_spec_proto_depIdxs,
+ EnumInfos: file_job_spec_proto_enumTypes,
MessageInfos: file_job_spec_proto_msgTypes,
}.Build()
File_job_spec_proto = out.File
diff --git a/core/services/nodestatusreporter/jobspec/events/job_spec.proto b/core/services/nodestatusreporter/jobspec/events/job_spec.proto
index 263e76b4121..6fcfadf9b89 100644
--- a/core/services/nodestatusreporter/jobspec/events/job_spec.proto
+++ b/core/services/nodestatusreporter/jobspec/events/job_spec.proto
@@ -46,11 +46,18 @@ message JobSpecEvent {
string hostname = 23;
// Event metadata.
- // emission_trigger is one of "heartbeat", "create", or "delete".
- string emission_trigger = 24;
+ EmissionTrigger emission_trigger = 24;
string timestamp = 25;
}
+// EmissionTrigger identifies the event that caused the JobSpecEvent to be emitted.
+enum EmissionTrigger {
+ EMISSION_TRIGGER_UNSPECIFIED = 0;
+ EMISSION_TRIGGER_HEARTBEAT = 1;
+ EMISSION_TRIGGER_CREATE = 2;
+ EMISSION_TRIGGER_DELETE = 3;
+}
+
// OCR2OracleSpecInfo carries all fields of job.OCR2OracleSpec.
message OCR2OracleSpecInfo {
int32 spec_id = 1;
diff --git a/core/services/nodestatusreporter/jobspec/job_spec_reporter.go b/core/services/nodestatusreporter/jobspec/job_spec_reporter.go
index 4ecc91f67f3..2dcec447b80 100644
--- a/core/services/nodestatusreporter/jobspec/job_spec_reporter.go
+++ b/core/services/nodestatusreporter/jobspec/job_spec_reporter.go
@@ -2,35 +2,28 @@ package jobspec
import (
"context"
+ "database/sql"
"encoding/json"
+ "errors"
"fmt"
"time"
"github.com/google/uuid"
- "github.com/pkg/errors"
"google.golang.org/protobuf/proto"
"github.com/smartcontractkit/chainlink-common/pkg/beholder"
"github.com/smartcontractkit/chainlink-common/pkg/services"
+ commontypes "github.com/smartcontractkit/chainlink-common/pkg/types"
coreconfig "github.com/smartcontractkit/chainlink/v2/core/config"
"github.com/smartcontractkit/chainlink/v2/core/logger"
- medianconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/median/config"
"github.com/smartcontractkit/chainlink/v2/core/services/feeds"
"github.com/smartcontractkit/chainlink/v2/core/services/job"
"github.com/smartcontractkit/chainlink/v2/core/services/nodestatusreporter/jobspec/events"
+ medianconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/median/config"
"github.com/smartcontractkit/chainlink/v2/core/services/pipeline"
-
- commontypes "github.com/smartcontractkit/chainlink-common/pkg/types"
)
-// NodeInfo contains static node identity values injected at construction time.
-type NodeInfo struct {
- CSAPublicKey string
- NodeVersion string
- Hostname string
-}
-
const ServiceName = "JobSpecReporter"
// Service polls active jobs and emits full job-spec telemetry via Beholder.
@@ -40,28 +33,33 @@ type Service struct {
services.Service
eng *services.Engine
- config coreconfig.JobSpecReporter
- spawner job.Spawner
- feedsORM feeds.ORM // optional; nil-safe
- emitter beholder.Emitter
- nodeInfo NodeInfo
+ config coreconfig.JobSpecReporter
+ spawner job.Spawner
+ feedsORM feeds.ORM
+ emitter beholder.Emitter
+ csaPublicKey string
+ nodeVersion string
+ hostname string
}
-// NewJobSpecReporter creates a new Job Spec Reporter Service.
func NewJobSpecReporter(
config coreconfig.JobSpecReporter,
spawner job.Spawner,
feedsORM feeds.ORM,
emitter beholder.Emitter,
- nodeInfo NodeInfo,
+ csaPublicKey string,
+ nodeVersion string,
+ hostname string,
lggr logger.Logger,
) *Service {
s := &Service{
- config: config,
- spawner: spawner,
- feedsORM: feedsORM,
- emitter: emitter,
- nodeInfo: nodeInfo,
+ config: config,
+ spawner: spawner,
+ feedsORM: feedsORM,
+ emitter: emitter,
+ csaPublicKey: csaPublicKey,
+ nodeVersion: nodeVersion,
+ hostname: hostname,
}
s.Service, s.eng = services.Config{
Name: ServiceName,
@@ -70,7 +68,6 @@ func NewJobSpecReporter(
return s
}
-// start starts the Job Spec Reporter Service.
func (s *Service) start(ctx context.Context) error {
if !s.config.Enabled() {
s.eng.Info("Job Spec Reporter Service is disabled")
@@ -85,57 +82,47 @@ func (s *Service) start(ctx context.Context) error {
return nil
}
-// HealthReport returns the service health.
func (s *Service) HealthReport() map[string]error {
return map[string]error{ServiceName: s.Ready()}
}
// OnJobStarted implements job.Listener — called after a job service starts successfully.
func (s *Service) OnJobStarted(ctx context.Context, jb job.Job) {
- if !s.shouldEmit(&jb) {
+ if !s.ShouldEmit(&jb) {
return
}
- if err := s.emitForJob(ctx, jb, "create"); err != nil {
+ if err := s.EmitForJob(ctx, jb, events.EmissionTrigger_EMISSION_TRIGGER_CREATE); err != nil {
s.eng.Warnw("Failed to emit job spec telemetry on create", "jobID", jb.ID, "error", err)
}
}
// OnJobStopped implements job.Listener — called after a job is deleted.
func (s *Service) OnJobStopped(ctx context.Context, jb job.Job) {
- if !s.shouldEmit(&jb) {
+ if !s.ShouldEmit(&jb) {
return
}
- if err := s.emitForJob(ctx, jb, "delete"); err != nil {
+ if err := s.EmitForJob(ctx, jb, events.EmissionTrigger_EMISSION_TRIGGER_DELETE); err != nil {
s.eng.Warnw("Failed to emit job spec telemetry on delete", "jobID", jb.ID, "error", err)
}
}
// pollAllJobs is called on each heartbeat tick and emits telemetry for every
-// active job that passes the shouldEmit gate.
+// active job that passes the ShouldEmit gate.
func (s *Service) pollAllJobs(ctx context.Context) {
- activeJobs := s.spawner.ActiveJobs()
- for _, jb := range activeJobs {
- jbCopy := jb
- if !s.shouldEmit(&jbCopy) {
+ for _, jb := range s.spawner.ActiveJobs() {
+ if !s.ShouldEmit(&jb) {
continue
}
- if err := s.emitForJob(ctx, jbCopy, "heartbeat"); err != nil {
+ if err := s.EmitForJob(ctx, jb, events.EmissionTrigger_EMISSION_TRIGGER_HEARTBEAT); err != nil {
s.eng.Warnw("Failed to emit job spec telemetry", "jobID", jb.ID, "error", err)
}
}
}
-// ShouldEmit is exported for testing. It returns true when the given job
-// should produce a telemetry event based on the current config.
-// Use shouldEmit for internal calls (identical logic).
-func (s *Service) ShouldEmit(j *job.Job) bool {
- return s.shouldEmit(j)
-}
-
-// shouldEmit returns true when the given job should produce a telemetry event
+// ShouldEmit returns true when the given job should produce a telemetry event
// based on the current config. This gate applies symmetrically to heartbeats
// and create/delete events.
-func (s *Service) shouldEmit(j *job.Job) bool {
+func (s *Service) ShouldEmit(j *job.Job) bool {
if j == nil {
return false
}
@@ -155,14 +142,8 @@ func (s *Service) shouldEmit(j *job.Job) bool {
return false
}
-// EmitForJob is exported for testing. It converts a job to a JobSpecEvent and
-// emits it via Beholder. Use emitForJob for internal calls (identical logic).
-func (s *Service) EmitForJob(ctx context.Context, jb job.Job, trigger string) error {
- return s.emitForJob(ctx, jb, trigger)
-}
-
-// emitForJob converts a job to a JobSpecEvent and emits it via Beholder.
-func (s *Service) emitForJob(ctx context.Context, jb job.Job, trigger string) error {
+// EmitForJob converts a job to a JobSpecEvent and emits it via Beholder.
+func (s *Service) EmitForJob(ctx context.Context, jb job.Job, trigger events.EmissionTrigger) error {
event, err := s.buildEvent(ctx, jb, trigger)
if err != nil {
return fmt.Errorf("building event: %w", err)
@@ -175,7 +156,7 @@ func (s *Service) emitForJob(ctx context.Context, jb job.Job, trigger string) er
}
// buildEvent converts a job.Job into the protobuf JobSpecEvent.
-func (s *Service) buildEvent(ctx context.Context, jb job.Job, trigger string) (*events.JobSpecEvent, error) {
+func (s *Service) buildEvent(ctx context.Context, jb job.Job, trigger events.EmissionTrigger) (*events.JobSpecEvent, error) {
event := &events.JobSpecEvent{
ExternalJobId: jb.ExternalJobID.String(),
InternalJobId: jb.ID,
@@ -185,9 +166,9 @@ func (s *Service) buildEvent(ctx context.Context, jb job.Job, trigger string) (*
ForwardingAllowed: jb.ForwardingAllowed,
MaxTaskDurationSeconds: jb.MaxTaskDuration.Duration().Seconds(),
CreatedAt: jb.CreatedAt.Format(time.RFC3339Nano),
- CsaPublicKey: s.nodeInfo.CSAPublicKey,
- NodeVersion: s.nodeInfo.NodeVersion,
- Hostname: s.nodeInfo.Hostname,
+ CsaPublicKey: s.csaPublicKey,
+ NodeVersion: s.nodeVersion,
+ Hostname: s.hostname,
EmissionTrigger: trigger,
Timestamp: time.Now().Format(time.RFC3339Nano),
}
@@ -206,7 +187,9 @@ func (s *Service) buildEvent(ctx context.Context, jb job.Job, trigger string) (*
event.BridgeNames = extractBridgeNames(jb.Pipeline)
}
- s.populateProposalLifecycle(ctx, jb, event)
+ if err := s.populateProposalLifecycle(ctx, jb, event); err != nil {
+ s.eng.Warnw("Failed to populate proposal lifecycle", "jobID", jb.ID, "error", err)
+ }
if jb.Type == job.OffchainReporting2 && jb.OCR2OracleSpec != nil {
ocr2Info, err := buildOCR2OracleSpecInfo(jb.OCR2OracleSpec)
@@ -220,21 +203,27 @@ func (s *Service) buildEvent(ctx context.Context, jb job.Job, trigger string) (*
}
// populateProposalLifecycle fills the proposal/approval fields when the job was
-// created via the Feeds Manager. Missing rows (manually created job) are silently
-// ignored.
-func (s *Service) populateProposalLifecycle(ctx context.Context, jb job.Job, event *events.JobSpecEvent) {
- if s.feedsORM == nil || jb.ExternalJobID == (uuid.UUID{}) {
- return
+// created via the Feeds Manager. Jobs not managed by the Feeds Manager are
+// returned without error via sql.ErrNoRows.
+func (s *Service) populateProposalLifecycle(ctx context.Context, jb job.Job, event *events.JobSpecEvent) error {
+ if s.feedsORM == nil || jb.ExternalJobID == uuid.Nil {
+ return nil
}
prop, err := s.feedsORM.GetJobProposalByExternalJobID(ctx, jb.ExternalJobID)
if err != nil {
- return
+ if errors.Is(err, sql.ErrNoRows) {
+ return nil
+ }
+ return fmt.Errorf("fetching job proposal: %w", err)
}
spec, err := s.feedsORM.GetApprovedSpec(ctx, prop.ID)
if err != nil {
- return
+ if errors.Is(err, sql.ErrNoRows) {
+ return nil
+ }
+ return fmt.Errorf("fetching approved spec: %w", err)
}
event.FeedsManagerId = prop.FeedsManagerID
@@ -243,6 +232,7 @@ func (s *Service) populateProposalLifecycle(ctx context.Context, jb job.Job, eve
event.ProposedAt = spec.CreatedAt.Format(time.RFC3339Nano)
event.ApprovedAt = spec.StatusUpdatedAt.Format(time.RFC3339Nano)
event.AcceptLatencySeconds = spec.StatusUpdatedAt.Sub(spec.CreatedAt).Seconds()
+ return nil
}
// extractBridgeNames returns the names of all bridge tasks in the top-level
@@ -251,10 +241,14 @@ func (s *Service) populateProposalLifecycle(ctx context.Context, jb job.Job, eve
func extractBridgeNames(p pipeline.Pipeline) []string {
var names []string
for _, task := range p.Tasks {
- if task.Type() == pipeline.TaskTypeBridge {
- bt := task.(*pipeline.BridgeTask)
- names = append(names, bt.Name)
+ if task.Type() != pipeline.TaskTypeBridge {
+ continue
+ }
+ bt, ok := task.(*pipeline.BridgeTask)
+ if !ok {
+ continue
}
+ names = append(names, bt.Name)
}
return names
}
@@ -262,30 +256,30 @@ func extractBridgeNames(p pipeline.Pipeline) []string {
// evmRelayConfig is a minimal struct for decoding EVM relay config JSON fields
// that we want to surface in OCR2EVMRelayConfig without importing the EVM module.
type evmRelayConfig struct {
- ChainID string `json:"chainID"`
- FromBlock uint64 `json:"fromBlock"`
- EffectiveTransmitterID string `json:"effectiveTransmitterID"`
- EnableDualTransmission bool `json:"enableDualTransmission"`
- EnableTriggerCapability bool `json:"enableTriggerCapability"`
- LLODonID uint64 `json:"lloDonID"`
- FeedID string `json:"feedID"`
- SendingKeys []string `json:"sendingKeys"`
- ProviderType string `json:"providerType"`
+ ChainID string `json:"chainID"`
+ FromBlock uint64 `json:"fromBlock"`
+ EffectiveTransmitterID string `json:"effectiveTransmitterID"`
+ EnableDualTransmission bool `json:"enableDualTransmission"`
+ EnableTriggerCapability bool `json:"enableTriggerCapability"`
+ LLODonID uint64 `json:"lloDonID"`
+ FeedID string `json:"feedID"`
+ SendingKeys []string `json:"sendingKeys"`
+ ProviderType string `json:"providerType"`
}
// buildOCR2OracleSpecInfo converts an OCR2OracleSpec into its proto representation.
func buildOCR2OracleSpecInfo(spec *job.OCR2OracleSpec) (*events.OCR2OracleSpecInfo, error) {
- relayConfigJSON := ""
- if raw, err := json.Marshal(spec.RelayConfig); err == nil {
- relayConfigJSON = string(raw)
+ relayConfigRaw, err := json.Marshal(spec.RelayConfig)
+ if err != nil {
+ return nil, fmt.Errorf("marshaling relay config: %w", err)
}
- pluginConfigJSON := ""
- if raw, err := json.Marshal(spec.PluginConfig); err == nil {
- pluginConfigJSON = string(raw)
+ pluginConfigRaw, err := json.Marshal(spec.PluginConfig)
+ if err != nil {
+ return nil, fmt.Errorf("marshaling plugin config: %w", err)
}
- onchainStrategyJSON := ""
- if raw, err := json.Marshal(spec.OnchainSigningStrategy); err == nil {
- onchainStrategyJSON = string(raw)
+ onchainStrategyRaw, err := json.Marshal(spec.OnchainSigningStrategy)
+ if err != nil {
+ return nil, fmt.Errorf("marshaling onchain signing strategy: %w", err)
}
feedID := ""
@@ -294,41 +288,41 @@ func buildOCR2OracleSpecInfo(spec *job.OCR2OracleSpec) (*events.OCR2OracleSpecIn
}
info := &events.OCR2OracleSpecInfo{
- SpecId: spec.ID,
- ContractId: spec.ContractID,
- FeedId: feedID,
- Relay: spec.Relay,
- ChainId: spec.ChainID,
- PluginType: string(spec.PluginType),
- TransmitterId: spec.TransmitterID.ValueOrZero(),
- OcrKeyBundleId: spec.OCRKeyBundleID.ValueOrZero(),
- MonitoringEndpoint: spec.MonitoringEndpoint.ValueOrZero(),
- P2Pv2Bootstrappers: spec.P2PV2Bootstrappers,
- AllowNoBootstrappers: spec.AllowNoBootstrappers,
- BlockchainTimeoutSeconds: spec.BlockchainTimeout.Duration().Seconds(),
- ContractConfigTrackerPollIntervalSeconds: spec.ContractConfigTrackerPollInterval.Duration().Seconds(),
- ContractConfigConfirmations: uint32(spec.ContractConfigConfirmations),
- CaptureEaTelemetry: spec.CaptureEATelemetry,
- CaptureAutomationCustomTelemetry: spec.CaptureAutomationCustomTelemetry,
- SpecCreatedAt: spec.CreatedAt.Format(time.RFC3339Nano),
- SpecUpdatedAt: spec.UpdatedAt.Format(time.RFC3339Nano),
- RelayConfigJson: relayConfigJSON,
- PluginConfigJson: pluginConfigJSON,
- OnchainSigningStrategyJson: onchainStrategyJSON,
+ SpecId: spec.ID,
+ ContractId: spec.ContractID,
+ FeedId: feedID,
+ Relay: spec.Relay,
+ ChainId: spec.ChainID,
+ PluginType: string(spec.PluginType),
+ TransmitterId: spec.TransmitterID.ValueOrZero(),
+ OcrKeyBundleId: spec.OCRKeyBundleID.ValueOrZero(),
+ MonitoringEndpoint: spec.MonitoringEndpoint.ValueOrZero(),
+ P2Pv2Bootstrappers: spec.P2PV2Bootstrappers,
+ AllowNoBootstrappers: spec.AllowNoBootstrappers,
+ BlockchainTimeoutSeconds: spec.BlockchainTimeout.Duration().Seconds(),
+ ContractConfigTrackerPollIntervalSeconds: spec.ContractConfigTrackerPollInterval.Duration().Seconds(),
+ ContractConfigConfirmations: uint32(spec.ContractConfigConfirmations),
+ CaptureEaTelemetry: spec.CaptureEATelemetry,
+ CaptureAutomationCustomTelemetry: spec.CaptureAutomationCustomTelemetry,
+ SpecCreatedAt: spec.CreatedAt.Format(time.RFC3339Nano),
+ SpecUpdatedAt: spec.UpdatedAt.Format(time.RFC3339Nano),
+ RelayConfigJson: string(relayConfigRaw),
+ PluginConfigJson: string(pluginConfigRaw),
+ OnchainSigningStrategyJson: string(onchainStrategyRaw),
}
if spec.Relay == "evm" {
- evmCfg, err := buildEVMRelayConfig(spec)
+ evmCfg, err := buildEVMRelayConfig(relayConfigRaw)
if err != nil {
- return nil, errors.Wrap(err, "building EVM relay config")
+ return nil, fmt.Errorf("building EVM relay config: %w", err)
}
info.EvmRelayConfig = evmCfg
}
if spec.PluginType == commontypes.Median {
- medianCfg, err := buildMedianPluginConfig(spec)
+ medianCfg, err := buildMedianPluginConfig(pluginConfigRaw)
if err != nil {
- return nil, errors.Wrap(err, "building median plugin config")
+ return nil, fmt.Errorf("building median plugin config: %w", err)
}
info.MedianPluginConfig = medianCfg
}
@@ -337,14 +331,9 @@ func buildOCR2OracleSpecInfo(spec *job.OCR2OracleSpec) (*events.OCR2OracleSpecIn
}
// buildEVMRelayConfig decodes the EVM relay config JSON into the proto message.
-func buildEVMRelayConfig(spec *job.OCR2OracleSpec) (*events.OCR2EVMRelayConfig, error) {
- raw, err := json.Marshal(spec.RelayConfig)
- if err != nil {
- return nil, fmt.Errorf("marshaling relay config: %w", err)
- }
-
+func buildEVMRelayConfig(relayConfigJSON []byte) (*events.OCR2EVMRelayConfig, error) {
var cfg evmRelayConfig
- if err := json.Unmarshal(raw, &cfg); err != nil {
+ if err := json.Unmarshal(relayConfigJSON, &cfg); err != nil {
return nil, fmt.Errorf("unmarshaling EVM relay config: %w", err)
}
@@ -362,19 +351,14 @@ func buildEVMRelayConfig(spec *job.OCR2OracleSpec) (*events.OCR2EVMRelayConfig,
}
// buildMedianPluginConfig decodes the plugin config JSON into the typed proto message.
-func buildMedianPluginConfig(spec *job.OCR2OracleSpec) (*events.OCR2MedianPluginConfig, error) {
- raw, err := json.Marshal(spec.PluginConfig)
- if err != nil {
- return nil, fmt.Errorf("marshaling plugin config: %w", err)
- }
-
+func buildMedianPluginConfig(pluginConfigJSON []byte) (*events.OCR2MedianPluginConfig, error) {
var cfg medianconfig.PluginConfig
- if err := json.Unmarshal(raw, &cfg); err != nil {
+ if err := json.Unmarshal(pluginConfigJSON, &cfg); err != nil {
return nil, fmt.Errorf("unmarshaling median plugin config: %w", err)
}
medianProto := &events.OCR2MedianPluginConfig{
- JuelsPerFeeCoinSource: cfg.JuelsPerFeeCoinPipeline,
+ JuelsPerFeeCoinSource: cfg.JuelsPerFeeCoinPipeline,
GasPriceSubunitsSource: cfg.GasPriceSubunitsPipeline,
}
@@ -388,9 +372,10 @@ func buildMedianPluginConfig(spec *job.OCR2OracleSpec) (*events.OCR2MedianPlugin
if cfg.DeviationFunctionDefinition != nil {
devFuncRaw, err := json.Marshal(cfg.DeviationFunctionDefinition)
- if err == nil {
- medianProto.DeviationFuncJson = string(devFuncRaw)
+ if err != nil {
+ return nil, fmt.Errorf("marshaling deviation function definition: %w", err)
}
+ medianProto.DeviationFuncJson = string(devFuncRaw)
}
return medianProto, nil
diff --git a/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go b/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go
index f13f7e3f2ef..41845bd189f 100644
--- a/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go
+++ b/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go
@@ -49,12 +49,10 @@ func defaultConfig() *stubConfig {
}
}
-// makeMedianJob creates a minimal OCR2 median job for testing.
func makeMedianJob() job.Job {
- extID := uuid.New()
return job.Job{
ID: 1,
- ExternalJobID: extID,
+ ExternalJobID: uuid.New(),
Name: null.StringFrom("test-median-job"),
Type: job.OffchainReporting2,
SchemaVersion: 1,
@@ -86,7 +84,6 @@ func makeMedianJob() job.Job {
}
}
-// makeNonMedianOCR2Job creates a non-median OCR2 job.
func makeNonMedianOCR2Job() job.Job {
jb := makeMedianJob()
jb.ID = 2
@@ -105,7 +102,6 @@ func makeNonMedianOCR2Job() job.Job {
return jb
}
-// makeVRFJob creates a non-OCR2 job.
func makeVRFJob() job.Job {
return job.Job{
ID: 3,
@@ -121,28 +117,39 @@ func makeVRFJob() job.Job {
// newTestReporter creates a Service wired to the current global beholder emitter.
// Call beholdertest.NewObserver(t) before this to set up the test emitter.
-func newTestReporter(t *testing.T, cfg *stubConfig, spawner job.Spawner, feedsORM feeds.ORM, nodeInfo jobspec.NodeInfo) *jobspec.Service {
+func newTestReporter(t *testing.T, cfg *stubConfig, feedsORM feeds.ORM) *jobspec.Service {
t.Helper()
- return jobspec.NewJobSpecReporter(cfg, spawner, feedsORM, beholder.GetEmitter(), nodeInfo, logger.TestLogger(t))
+ spawner := jobmocks.NewSpawner(t)
+ return jobspec.NewJobSpecReporter(cfg, spawner, feedsORM, beholder.GetEmitter(), "csa-key", "1.0.0", "test-host", logger.TestLogger(t))
+}
+
+// newFeedsORMWithoutProposal returns a feeds ORM mock that responds as if the
+// given job was not created via the feeds manager.
+func newFeedsORMWithoutProposal(t *testing.T, jb job.Job) *feedsmocks.ORM {
+ t.Helper()
+ feedsORM := feedsmocks.NewORM(t)
+ feedsORM.On("GetJobProposalByExternalJobID", mock.Anything, jb.ExternalJobID).Return(nil, sql.ErrNoRows).Maybe()
+ return feedsORM
}
// ── shouldEmit gate tests ──────────────────────────────────────────────────────
func TestShouldEmit_DefaultConfig(t *testing.T) {
beholdertest.NewObserver(t)
- spawner := jobmocks.NewSpawner(t)
- spawner.On("RegisterListener", mock.Anything).Maybe()
+ svc := newTestReporter(t, defaultConfig(), nil)
- svc := newTestReporter(t, defaultConfig(), spawner, nil, jobspec.NodeInfo{})
+ median := makeMedianJob()
+ nonMedian := makeNonMedianOCR2Job()
+ vrf := makeVRFJob()
cases := []struct {
name string
jb *job.Job
want bool
}{
- {"median OCR2 job emits", jobPtr(makeMedianJob()), true},
- {"non-median OCR2 job skipped", jobPtr(makeNonMedianOCR2Job()), false},
- {"non-OCR2 (VRF) job skipped", jobPtr(makeVRFJob()), false},
+ {"median OCR2 job emits", &median, true},
+ {"non-median OCR2 job skipped", &nonMedian, false},
+ {"non-OCR2 (VRF) job skipped", &vrf, false},
}
for _, tc := range cases {
@@ -157,13 +164,15 @@ func TestShouldEmit_AllOCR2Types(t *testing.T) {
cfg := defaultConfig()
cfg.enabledOCR2PluginTypes = []string{} // empty = allow all
- spawner := jobmocks.NewSpawner(t)
- spawner.On("RegisterListener", mock.Anything).Maybe()
- svc := newTestReporter(t, cfg, spawner, nil, jobspec.NodeInfo{})
+ svc := newTestReporter(t, cfg, nil)
- assert.True(t, svc.ShouldEmit(jobPtr(makeMedianJob())))
- assert.True(t, svc.ShouldEmit(jobPtr(makeNonMedianOCR2Job())))
- assert.False(t, svc.ShouldEmit(jobPtr(makeVRFJob())))
+ median := makeMedianJob()
+ nonMedian := makeNonMedianOCR2Job()
+ vrf := makeVRFJob()
+
+ assert.True(t, svc.ShouldEmit(&median))
+ assert.True(t, svc.ShouldEmit(&nonMedian))
+ assert.False(t, svc.ShouldEmit(&vrf))
}
func TestShouldEmit_NonOCR2Enabled(t *testing.T) {
@@ -171,32 +180,24 @@ func TestShouldEmit_NonOCR2Enabled(t *testing.T) {
cfg := defaultConfig()
cfg.emitNonOCR2Jobs = true
- spawner := jobmocks.NewSpawner(t)
- spawner.On("RegisterListener", mock.Anything).Maybe()
- svc := newTestReporter(t, cfg, spawner, nil, jobspec.NodeInfo{})
+ svc := newTestReporter(t, cfg, nil)
+
+ median := makeMedianJob()
+ vrf := makeVRFJob()
- assert.True(t, svc.ShouldEmit(jobPtr(makeVRFJob())))
- assert.True(t, svc.ShouldEmit(jobPtr(makeMedianJob())))
+ assert.True(t, svc.ShouldEmit(&vrf))
+ assert.True(t, svc.ShouldEmit(&median))
}
// ── conversion tests ──────────────────────────────────────────────────────────
func TestBuildEvent_MedianJob(t *testing.T) {
observer := beholdertest.NewObserver(t)
- spawner := jobmocks.NewSpawner(t)
- spawner.On("RegisterListener", mock.Anything).Maybe()
- feedsORM := feedsmocks.NewORM(t)
jb := makeMedianJob()
- feedsORM.On("GetJobProposalByExternalJobID", mock.Anything, jb.ExternalJobID).Return(nil, sql.ErrNoRows).Maybe()
-
- svc := newTestReporter(t, defaultConfig(), spawner, feedsORM, jobspec.NodeInfo{
- CSAPublicKey: "csa-key",
- NodeVersion: "1.0.0",
- Hostname: "test-host",
- })
+ svc := newTestReporter(t, defaultConfig(), newFeedsORMWithoutProposal(t, jb))
- err := svc.EmitForJob(context.Background(), jb, "heartbeat")
+ err := svc.EmitForJob(context.Background(), jb, events.EmissionTrigger_EMISSION_TRIGGER_HEARTBEAT)
require.NoError(t, err)
msgs := observer.Messages(t, "beholder_entity", events.ProtoPkg+"."+events.JobSpecEventEntity)
@@ -209,7 +210,7 @@ func TestBuildEvent_MedianJob(t *testing.T) {
assert.Equal(t, jb.ID, ev.InternalJobId)
assert.Equal(t, "test-median-job", ev.Name)
assert.Equal(t, "offchainreporting2", ev.JobType)
- assert.Equal(t, "heartbeat", ev.EmissionTrigger)
+ assert.Equal(t, events.EmissionTrigger_EMISSION_TRIGGER_HEARTBEAT, ev.EmissionTrigger)
assert.Equal(t, "csa-key", ev.CsaPublicKey)
assert.Equal(t, "1.0.0", ev.NodeVersion)
assert.Equal(t, "test-host", ev.Hostname)
@@ -225,14 +226,11 @@ func TestBuildEvent_MedianJob(t *testing.T) {
func TestBuildEvent_NonMedianOCR2Job(t *testing.T) {
observer := beholdertest.NewObserver(t)
- spawner := jobmocks.NewSpawner(t)
- spawner.On("RegisterListener", mock.Anything).Maybe()
- feedsORM := feedsmocks.NewORM(t)
+
jb := makeNonMedianOCR2Job()
- feedsORM.On("GetJobProposalByExternalJobID", mock.Anything, jb.ExternalJobID).Return(nil, sql.ErrNoRows).Maybe()
+ svc := newTestReporter(t, defaultConfig(), newFeedsORMWithoutProposal(t, jb))
- svc := newTestReporter(t, defaultConfig(), spawner, feedsORM, jobspec.NodeInfo{})
- err := svc.EmitForJob(context.Background(), jb, "create")
+ err := svc.EmitForJob(context.Background(), jb, events.EmissionTrigger_EMISSION_TRIGGER_CREATE)
require.NoError(t, err)
msgs := observer.Messages(t, "beholder_entity", events.ProtoPkg+"."+events.JobSpecEventEntity)
@@ -249,13 +247,11 @@ func TestBuildEvent_NonMedianOCR2Job(t *testing.T) {
func TestBuildEvent_NonOCR2Job(t *testing.T) {
observer := beholdertest.NewObserver(t)
- spawner := jobmocks.NewSpawner(t)
- spawner.On("RegisterListener", mock.Anything).Maybe()
- svc := newTestReporter(t, defaultConfig(), spawner, nil, jobspec.NodeInfo{})
+ svc := newTestReporter(t, defaultConfig(), nil)
jb := makeVRFJob()
- err := svc.EmitForJob(context.Background(), jb, "heartbeat")
+ err := svc.EmitForJob(context.Background(), jb, events.EmissionTrigger_EMISSION_TRIGGER_HEARTBEAT)
require.NoError(t, err)
msgs := observer.Messages(t, "beholder_entity", events.ProtoPkg+"."+events.JobSpecEventEntity)
@@ -272,13 +268,9 @@ func TestBuildEvent_NonOCR2Job(t *testing.T) {
func TestOnJobStarted_EmitsCreate(t *testing.T) {
observer := beholdertest.NewObserver(t)
- spawner := jobmocks.NewSpawner(t)
- spawner.On("RegisterListener", mock.Anything).Maybe()
- feedsORM := feedsmocks.NewORM(t)
- jb := makeMedianJob()
- feedsORM.On("GetJobProposalByExternalJobID", mock.Anything, jb.ExternalJobID).Return(nil, sql.ErrNoRows).Maybe()
- svc := newTestReporter(t, defaultConfig(), spawner, feedsORM, jobspec.NodeInfo{})
+ jb := makeMedianJob()
+ svc := newTestReporter(t, defaultConfig(), newFeedsORMWithoutProposal(t, jb))
svc.OnJobStarted(context.Background(), jb)
msgs := observer.Messages(t, "beholder_entity", events.ProtoPkg+"."+events.JobSpecEventEntity)
@@ -286,18 +278,14 @@ func TestOnJobStarted_EmitsCreate(t *testing.T) {
var ev events.JobSpecEvent
require.NoError(t, proto.Unmarshal(msgs[0].Body, &ev))
- assert.Equal(t, "create", ev.EmissionTrigger)
+ assert.Equal(t, events.EmissionTrigger_EMISSION_TRIGGER_CREATE, ev.EmissionTrigger)
}
func TestOnJobStopped_EmitsDelete(t *testing.T) {
observer := beholdertest.NewObserver(t)
- spawner := jobmocks.NewSpawner(t)
- spawner.On("RegisterListener", mock.Anything).Maybe()
- feedsORM := feedsmocks.NewORM(t)
- jb := makeMedianJob()
- feedsORM.On("GetJobProposalByExternalJobID", mock.Anything, jb.ExternalJobID).Return(nil, sql.ErrNoRows).Maybe()
- svc := newTestReporter(t, defaultConfig(), spawner, feedsORM, jobspec.NodeInfo{})
+ jb := makeMedianJob()
+ svc := newTestReporter(t, defaultConfig(), newFeedsORMWithoutProposal(t, jb))
svc.OnJobStopped(context.Background(), jb)
msgs := observer.Messages(t, "beholder_entity", events.ProtoPkg+"."+events.JobSpecEventEntity)
@@ -305,16 +293,14 @@ func TestOnJobStopped_EmitsDelete(t *testing.T) {
var ev events.JobSpecEvent
require.NoError(t, proto.Unmarshal(msgs[0].Body, &ev))
- assert.Equal(t, "delete", ev.EmissionTrigger)
+ assert.Equal(t, events.EmissionTrigger_EMISSION_TRIGGER_DELETE, ev.EmissionTrigger)
}
func TestOnJobStarted_SkippedWhenGateFails(t *testing.T) {
observer := beholdertest.NewObserver(t)
- spawner := jobmocks.NewSpawner(t)
- spawner.On("RegisterListener", mock.Anything).Maybe()
// default config only allows median, so VRF should not emit
- svc := newTestReporter(t, defaultConfig(), spawner, nil, jobspec.NodeInfo{})
+ svc := newTestReporter(t, defaultConfig(), nil)
svc.OnJobStarted(context.Background(), makeVRFJob())
msgs := observer.Messages(t, "beholder_entity", events.ProtoPkg+"."+events.JobSpecEventEntity)
@@ -325,8 +311,6 @@ func TestOnJobStarted_SkippedWhenGateFails(t *testing.T) {
func TestBuildEvent_ProposalLifecycle(t *testing.T) {
observer := beholdertest.NewObserver(t)
- spawner := jobmocks.NewSpawner(t)
- spawner.On("RegisterListener", mock.Anything).Maybe()
feedsORM := feedsmocks.NewORM(t)
jb := makeMedianJob()
@@ -348,8 +332,8 @@ func TestBuildEvent_ProposalLifecycle(t *testing.T) {
feedsORM.On("GetJobProposalByExternalJobID", mock.Anything, jb.ExternalJobID).Return(prop, nil)
feedsORM.On("GetApprovedSpec", mock.Anything, prop.ID).Return(spec, nil)
- svc := newTestReporter(t, defaultConfig(), spawner, feedsORM, jobspec.NodeInfo{})
- err := svc.EmitForJob(context.Background(), jb, "heartbeat")
+ svc := newTestReporter(t, defaultConfig(), feedsORM)
+ err := svc.EmitForJob(context.Background(), jb, events.EmissionTrigger_EMISSION_TRIGGER_HEARTBEAT)
require.NoError(t, err)
msgs := observer.Messages(t, "beholder_entity", events.ProtoPkg+"."+events.JobSpecEventEntity)
@@ -363,7 +347,3 @@ func TestBuildEvent_ProposalLifecycle(t *testing.T) {
assert.Equal(t, int32(3), ev.SpecVersion)
assert.InDelta(t, approvedAt.Sub(proposedAt).Seconds(), ev.AcceptLatencySeconds, 1.0)
}
-
-// ── helpers ───────────────────────────────────────────────────────────────────
-
-func jobPtr(jb job.Job) *job.Job { return &jb }
From b675c39e68c581f75ca49dd42434908b2d63a58b Mon Sep 17 00:00:00 2001
From: thomjg <103059015+thomjg@users.noreply.github.com>
Date: Mon, 20 Apr 2026 21:46:26 -0400
Subject: [PATCH 03/14] Updating comments to make them more concise
---
core/config/job_spec_reporter_config.go | 8 ++--
core/services/job/spawner.go | 15 +++----
.../nodestatusreporter/jobspec/events/emit.go | 2 +-
.../jobspec/events/emit_test.go | 2 +-
.../jobspec/job_spec_reporter.go | 41 ++++++++-----------
.../jobspec/job_spec_reporter_test.go | 24 ++++-------
6 files changed, 40 insertions(+), 52 deletions(-)
diff --git a/core/config/job_spec_reporter_config.go b/core/config/job_spec_reporter_config.go
index b18b0860ae2..aca5c9de272 100644
--- a/core/config/job_spec_reporter_config.go
+++ b/core/config/job_spec_reporter_config.go
@@ -5,10 +5,10 @@ import "time"
type JobSpecReporter interface {
Enabled() bool
PollingInterval() time.Duration
- // EnabledOCR2PluginTypes is the allowlist of OCR2 plugin types to emit for (e.g. "median", "ocr2keeper").
- // An empty slice means all OCR2 plugin types are allowed.
+ // EnabledOCR2PluginTypes is the allowlist of OCR2 plugin types to emit for
+ // (e.g. "median", "ocr2keeper"). An empty slice means emit for all types.
EnabledOCR2PluginTypes() []string
- // EmitNonOCR2Jobs controls whether non-OCR2 jobs (VRF, Keeper, Functions, CCIP, Workflow, …)
- // emit the generic envelope. Default false for the initial rollout.
+ // EmitNonOCR2Jobs toggles emission for non-OCR2 job types (VRF, Keeper,
+ // Functions, CCIP, Workflow, …). Defaults to false.
EmitNonOCR2Jobs() bool
}
diff --git a/core/services/job/spawner.go b/core/services/job/spawner.go
index fab27516ec9..2b1ad5b1d5c 100644
--- a/core/services/job/spawner.go
+++ b/core/services/job/spawner.go
@@ -17,8 +17,8 @@ import (
)
type (
- // Listener is notified when jobs are started or stopped by the Spawner.
- // Callbacks are invoked asynchronously and must not block.
+ // Listener is notified when the Spawner starts or stops a job.
+ // Callbacks run asynchronously and must not block.
Listener interface {
OnJobStarted(ctx context.Context, jb Job)
OnJobStopped(ctx context.Context, jb Job)
@@ -43,9 +43,9 @@ type (
// to start a job that was previously manually inserted into DB
StartService(ctx context.Context, spec Job) error
- // RegisterListener registers a Listener to be notified on job start/stop.
- // Safe to call before or after Start().
- RegisterListener(Listener)
+ // RegisterListener adds l to the set of listeners notified on job start/stop.
+ // Safe to call before or after Start.
+ RegisterListener(l Listener)
}
Checker interface {
@@ -387,8 +387,9 @@ func (js *spawner) notifyStopped(jb Job) {
js.dispatchToListeners(func(ctx context.Context, l Listener) { l.OnJobStopped(ctx, jb) })
}
-// dispatchToListeners invokes fn against each registered listener in a
-// best-effort, non-blocking, panic-safe goroutine.
+// dispatchToListeners fans out fn to every registered listener in a single
+// best-effort goroutine. Panics are recovered so a faulty listener cannot
+// bring the spawner down.
func (js *spawner) dispatchToListeners(fn func(context.Context, Listener)) {
js.listenersMu.RLock()
ls := make([]Listener, len(js.listeners))
diff --git a/core/services/nodestatusreporter/jobspec/events/emit.go b/core/services/nodestatusreporter/jobspec/events/emit.go
index f317922bba1..e28d05fcb29 100644
--- a/core/services/nodestatusreporter/jobspec/events/emit.go
+++ b/core/services/nodestatusreporter/jobspec/events/emit.go
@@ -10,7 +10,7 @@ import (
"github.com/smartcontractkit/chainlink-common/pkg/beholder"
)
-// EmitJobSpecEvent emits a Job Spec event through the provided beholder.Emitter.
+// EmitJobSpecEvent emits a Job Spec event through the provided emitter.
func EmitJobSpecEvent(ctx context.Context, emitter beholder.Emitter, event *JobSpecEvent) error {
if event.Timestamp == "" {
event.Timestamp = time.Now().Format(time.RFC3339Nano)
diff --git a/core/services/nodestatusreporter/jobspec/events/emit_test.go b/core/services/nodestatusreporter/jobspec/events/emit_test.go
index 4d491b2af58..b311fa52ca8 100644
--- a/core/services/nodestatusreporter/jobspec/events/emit_test.go
+++ b/core/services/nodestatusreporter/jobspec/events/emit_test.go
@@ -14,7 +14,7 @@ import (
)
func TestEmitJobSpecEvent_RoundTrip(t *testing.T) {
- // NewObserver sets the global beholder client; use GetEmitter() to obtain the emitter.
+ // NewObserver installs the global beholder client; GetEmitter returns the one to emit through.
observer := beholdertest.NewObserver(t)
emitter := beholder.GetEmitter()
diff --git a/core/services/nodestatusreporter/jobspec/job_spec_reporter.go b/core/services/nodestatusreporter/jobspec/job_spec_reporter.go
index 2dcec447b80..4258ac36913 100644
--- a/core/services/nodestatusreporter/jobspec/job_spec_reporter.go
+++ b/core/services/nodestatusreporter/jobspec/job_spec_reporter.go
@@ -26,9 +26,8 @@ import (
const ServiceName = "JobSpecReporter"
-// Service polls active jobs and emits full job-spec telemetry via Beholder.
-// It mirrors the BridgeStatusReporter structure and implements job.Listener
-// to also emit immediately on create/delete.
+// Service polls active jobs and pushes their specs to Beholder, and also emits
+// on job create/delete via the job.Listener interface.
type Service struct {
services.Service
eng *services.Engine
@@ -86,7 +85,7 @@ func (s *Service) HealthReport() map[string]error {
return map[string]error{ServiceName: s.Ready()}
}
-// OnJobStarted implements job.Listener — called after a job service starts successfully.
+// OnJobStarted emits a create event when a job starts.
func (s *Service) OnJobStarted(ctx context.Context, jb job.Job) {
if !s.ShouldEmit(&jb) {
return
@@ -96,7 +95,7 @@ func (s *Service) OnJobStarted(ctx context.Context, jb job.Job) {
}
}
-// OnJobStopped implements job.Listener — called after a job is deleted.
+// OnJobStopped emits a delete event when a job is removed.
func (s *Service) OnJobStopped(ctx context.Context, jb job.Job) {
if !s.ShouldEmit(&jb) {
return
@@ -106,8 +105,7 @@ func (s *Service) OnJobStopped(ctx context.Context, jb job.Job) {
}
}
-// pollAllJobs is called on each heartbeat tick and emits telemetry for every
-// active job that passes the ShouldEmit gate.
+// pollAllJobs emits heartbeat telemetry for every active job that passes the emit gate.
func (s *Service) pollAllJobs(ctx context.Context) {
for _, jb := range s.spawner.ActiveJobs() {
if !s.ShouldEmit(&jb) {
@@ -119,9 +117,8 @@ func (s *Service) pollAllJobs(ctx context.Context) {
}
}
-// ShouldEmit returns true when the given job should produce a telemetry event
-// based on the current config. This gate applies symmetrically to heartbeats
-// and create/delete events.
+// ShouldEmit reports whether the job passes the config-driven emit gate.
+// Applied to heartbeat, create, and delete events alike.
func (s *Service) ShouldEmit(j *job.Job) bool {
if j == nil {
return false
@@ -142,7 +139,7 @@ func (s *Service) ShouldEmit(j *job.Job) bool {
return false
}
-// EmitForJob converts a job to a JobSpecEvent and emits it via Beholder.
+// EmitForJob builds and emits a JobSpecEvent for the given job and trigger.
func (s *Service) EmitForJob(ctx context.Context, jb job.Job, trigger events.EmissionTrigger) error {
event, err := s.buildEvent(ctx, jb, trigger)
if err != nil {
@@ -155,7 +152,7 @@ func (s *Service) EmitForJob(ctx context.Context, jb job.Job, trigger events.Emi
return nil
}
-// buildEvent converts a job.Job into the protobuf JobSpecEvent.
+// buildEvent converts a job.Job into its protobuf JobSpecEvent representation.
func (s *Service) buildEvent(ctx context.Context, jb job.Job, trigger events.EmissionTrigger) (*events.JobSpecEvent, error) {
event := &events.JobSpecEvent{
ExternalJobId: jb.ExternalJobID.String(),
@@ -202,9 +199,8 @@ func (s *Service) buildEvent(ctx context.Context, jb job.Job, trigger events.Emi
return event, nil
}
-// populateProposalLifecycle fills the proposal/approval fields when the job was
-// created via the Feeds Manager. Jobs not managed by the Feeds Manager are
-// returned without error via sql.ErrNoRows.
+// populateProposalLifecycle fills in proposal/approval fields for jobs created
+// via the Feeds Manager. Jobs not managed by Feeds Manager are a no-op.
func (s *Service) populateProposalLifecycle(ctx context.Context, jb job.Job, event *events.JobSpecEvent) error {
if s.feedsORM == nil || jb.ExternalJobID == uuid.Nil {
return nil
@@ -235,9 +231,8 @@ func (s *Service) populateProposalLifecycle(ctx context.Context, jb job.Job, eve
return nil
}
-// extractBridgeNames returns the names of all bridge tasks in the top-level
-// observationSource pipeline. Tasks in sub-pipelines (e.g. juelsPerFeeCoinSource)
-// are not included.
+// extractBridgeNames returns the names of bridge tasks in the top-level pipeline.
+// Tasks inside sub-pipelines (e.g. juelsPerFeeCoinSource) are not included.
func extractBridgeNames(p pipeline.Pipeline) []string {
var names []string
for _, task := range p.Tasks {
@@ -253,8 +248,8 @@ func extractBridgeNames(p pipeline.Pipeline) []string {
return names
}
-// evmRelayConfig is a minimal struct for decoding EVM relay config JSON fields
-// that we want to surface in OCR2EVMRelayConfig without importing the EVM module.
+// evmRelayConfig mirrors the EVM relay config JSON so we can surface its fields
+// in OCR2EVMRelayConfig without depending on the EVM module.
type evmRelayConfig struct {
ChainID string `json:"chainID"`
FromBlock uint64 `json:"fromBlock"`
@@ -267,7 +262,7 @@ type evmRelayConfig struct {
ProviderType string `json:"providerType"`
}
-// buildOCR2OracleSpecInfo converts an OCR2OracleSpec into its proto representation.
+// buildOCR2OracleSpecInfo converts an OCR2OracleSpec into the proto message.
func buildOCR2OracleSpecInfo(spec *job.OCR2OracleSpec) (*events.OCR2OracleSpecInfo, error) {
relayConfigRaw, err := json.Marshal(spec.RelayConfig)
if err != nil {
@@ -330,7 +325,7 @@ func buildOCR2OracleSpecInfo(spec *job.OCR2OracleSpec) (*events.OCR2OracleSpecIn
return info, nil
}
-// buildEVMRelayConfig decodes the EVM relay config JSON into the proto message.
+// buildEVMRelayConfig decodes the EVM relay config JSON into OCR2EVMRelayConfig.
func buildEVMRelayConfig(relayConfigJSON []byte) (*events.OCR2EVMRelayConfig, error) {
var cfg evmRelayConfig
if err := json.Unmarshal(relayConfigJSON, &cfg); err != nil {
@@ -350,7 +345,7 @@ func buildEVMRelayConfig(relayConfigJSON []byte) (*events.OCR2EVMRelayConfig, er
}, nil
}
-// buildMedianPluginConfig decodes the plugin config JSON into the typed proto message.
+// buildMedianPluginConfig decodes the median plugin config JSON into OCR2MedianPluginConfig.
func buildMedianPluginConfig(pluginConfigJSON []byte) (*events.OCR2MedianPluginConfig, error) {
var cfg medianconfig.PluginConfig
if err := json.Unmarshal(pluginConfigJSON, &cfg); err != nil {
diff --git a/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go b/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go
index 41845bd189f..dd5dc8d82c5 100644
--- a/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go
+++ b/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go
@@ -115,16 +115,16 @@ func makeVRFJob() job.Job {
}
}
-// newTestReporter creates a Service wired to the current global beholder emitter.
-// Call beholdertest.NewObserver(t) before this to set up the test emitter.
+// newTestReporter returns a Service wired to the current global beholder emitter.
+// The caller must set up the test emitter via beholdertest.NewObserver(t) first.
func newTestReporter(t *testing.T, cfg *stubConfig, feedsORM feeds.ORM) *jobspec.Service {
t.Helper()
spawner := jobmocks.NewSpawner(t)
return jobspec.NewJobSpecReporter(cfg, spawner, feedsORM, beholder.GetEmitter(), "csa-key", "1.0.0", "test-host", logger.TestLogger(t))
}
-// newFeedsORMWithoutProposal returns a feeds ORM mock that responds as if the
-// given job was not created via the feeds manager.
+// newFeedsORMWithoutProposal returns a feeds ORM mock that behaves as if the
+// given job was created outside of the Feeds Manager.
func newFeedsORMWithoutProposal(t *testing.T, jb job.Job) *feedsmocks.ORM {
t.Helper()
feedsORM := feedsmocks.NewORM(t)
@@ -132,8 +132,6 @@ func newFeedsORMWithoutProposal(t *testing.T, jb job.Job) *feedsmocks.ORM {
return feedsORM
}
-// ── shouldEmit gate tests ──────────────────────────────────────────────────────
-
func TestShouldEmit_DefaultConfig(t *testing.T) {
beholdertest.NewObserver(t)
svc := newTestReporter(t, defaultConfig(), nil)
@@ -162,7 +160,7 @@ func TestShouldEmit_DefaultConfig(t *testing.T) {
func TestShouldEmit_AllOCR2Types(t *testing.T) {
beholdertest.NewObserver(t)
cfg := defaultConfig()
- cfg.enabledOCR2PluginTypes = []string{} // empty = allow all
+ cfg.enabledOCR2PluginTypes = []string{} // empty allowlist = all OCR2 types
svc := newTestReporter(t, cfg, nil)
@@ -189,8 +187,6 @@ func TestShouldEmit_NonOCR2Enabled(t *testing.T) {
assert.True(t, svc.ShouldEmit(&median))
}
-// ── conversion tests ──────────────────────────────────────────────────────────
-
func TestBuildEvent_MedianJob(t *testing.T) {
observer := beholdertest.NewObserver(t)
@@ -241,7 +237,7 @@ func TestBuildEvent_NonMedianOCR2Job(t *testing.T) {
require.NotNil(t, ev.Ocr2OracleSpec)
assert.Equal(t, "ocr2keeper", ev.Ocr2OracleSpec.PluginType)
- assert.Nil(t, ev.Ocr2OracleSpec.MedianPluginConfig) // not median
+ assert.Nil(t, ev.Ocr2OracleSpec.MedianPluginConfig)
assert.NotEmpty(t, ev.Ocr2OracleSpec.RelayConfigJson)
}
@@ -261,11 +257,9 @@ func TestBuildEvent_NonOCR2Job(t *testing.T) {
require.NoError(t, proto.Unmarshal(msgs[0].Body, &ev))
assert.Equal(t, "vrf", ev.JobType)
- assert.Nil(t, ev.Ocr2OracleSpec) // no OCR2 spec
+ assert.Nil(t, ev.Ocr2OracleSpec)
}
-// ── OnJobStarted / OnJobStopped listener tests ────────────────────────────────
-
func TestOnJobStarted_EmitsCreate(t *testing.T) {
observer := beholdertest.NewObserver(t)
@@ -299,7 +293,7 @@ func TestOnJobStopped_EmitsDelete(t *testing.T) {
func TestOnJobStarted_SkippedWhenGateFails(t *testing.T) {
observer := beholdertest.NewObserver(t)
- // default config only allows median, so VRF should not emit
+ // default config only allows median, so a VRF job should be skipped
svc := newTestReporter(t, defaultConfig(), nil)
svc.OnJobStarted(context.Background(), makeVRFJob())
@@ -307,8 +301,6 @@ func TestOnJobStarted_SkippedWhenGateFails(t *testing.T) {
require.Empty(t, msgs)
}
-// ── proposal-latency test ─────────────────────────────────────────────────────
-
func TestBuildEvent_ProposalLifecycle(t *testing.T) {
observer := beholdertest.NewObserver(t)
feedsORM := feedsmocks.NewORM(t)
From 141e16fcc99fe8fb672f447d262f8732604b3fd0 Mon Sep 17 00:00:00 2001
From: thomjg <103059015+thomjg@users.noreply.github.com>
Date: Mon, 20 Apr 2026 21:53:59 -0400
Subject: [PATCH 04/14] Continued updates to comments to make them more concise
---
.../jobspec/events/job_spec.pb.go | 46 ++++++++-----------
.../jobspec/events/job_spec.proto | 46 ++++++++-----------
2 files changed, 38 insertions(+), 54 deletions(-)
diff --git a/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go b/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go
index 0b5b688a876..78a174913fb 100644
--- a/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go
+++ b/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go
@@ -21,7 +21,7 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
-// EmissionTrigger identifies the event that caused the JobSpecEvent to be emitted.
+// EmissionTrigger is the reason a JobSpecEvent was emitted.
type EmissionTrigger int32
const (
@@ -74,13 +74,10 @@ func (EmissionTrigger) EnumDescriptor() ([]byte, []int) {
return file_job_spec_proto_rawDescGZIP(), []int{0}
}
-// JobSpecEvent is emitted for each active job on a heartbeat, on job creation,
-// and on job deletion. For the initial rollout only offchainreporting2 jobs
-// with pluginType = "median" are emitted (configurable via EnabledOCR2PluginTypes
-// and EmitNonOCR2Jobs in the JobSpecReporter config section).
+// JobSpecEvent carries a job's spec, emitted on heartbeat, create, and delete.
type JobSpecEvent struct {
state protoimpl.MessageState `protogen:"open.v1"`
- // Job identity — covers every TOML-writable field on job.Job plus DB-assigned columns.
+ // Job identity
ExternalJobId string `protobuf:"bytes,1,opt,name=external_job_id,json=externalJobId,proto3" json:"external_job_id,omitempty"`
InternalJobId int32 `protobuf:"varint,2,opt,name=internal_job_id,json=internalJobId,proto3" json:"internal_job_id,omitempty"`
Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
@@ -91,26 +88,25 @@ type JobSpecEvent struct {
StreamId *uint32 `protobuf:"varint,8,opt,name=stream_id,json=streamId,proto3,oneof" json:"stream_id,omitempty"`
MaxTaskDurationSeconds float64 `protobuf:"fixed64,9,opt,name=max_task_duration_seconds,json=maxTaskDurationSeconds,proto3" json:"max_task_duration_seconds,omitempty"`
CreatedAt string `protobuf:"bytes,10,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
- // Observation pipeline — the job's observationSource field.
+ // Observation pipeline
ObservationSource string `protobuf:"bytes,11,opt,name=observation_source,json=observationSource,proto3" json:"observation_source,omitempty"`
PipelineSpecId int32 `protobuf:"varint,12,opt,name=pipeline_spec_id,json=pipelineSpecId,proto3" json:"pipeline_spec_id,omitempty"`
- // Bridge names extracted from the observationSource DOT DAG (top-level only).
+ // Top-level bridge names in the observation pipeline.
BridgeNames []string `protobuf:"bytes,13,rep,name=bridge_names,json=bridgeNames,proto3" json:"bridge_names,omitempty"`
- // Proposal lifecycle fields — zero/empty when the job was created manually
- // (not via a Feeds Manager / Job Distributor).
+ // Proposal lifecycle — zero/empty for jobs not managed by a Feeds Manager.
FeedsManagerId int64 `protobuf:"varint,14,opt,name=feeds_manager_id,json=feedsManagerId,proto3" json:"feeds_manager_id,omitempty"`
RemoteUuid string `protobuf:"bytes,15,opt,name=remote_uuid,json=remoteUuid,proto3" json:"remote_uuid,omitempty"`
SpecVersion int32 `protobuf:"varint,16,opt,name=spec_version,json=specVersion,proto3" json:"spec_version,omitempty"`
ProposedAt string `protobuf:"bytes,17,opt,name=proposed_at,json=proposedAt,proto3" json:"proposed_at,omitempty"`
ApprovedAt string `protobuf:"bytes,18,opt,name=approved_at,json=approvedAt,proto3" json:"approved_at,omitempty"`
AcceptLatencySeconds float64 `protobuf:"fixed64,19,opt,name=accept_latency_seconds,json=acceptLatencySeconds,proto3" json:"accept_latency_seconds,omitempty"`
- // OCR2-specific fields — absent for non-OCR2 job types.
+ // OCR2-only; absent for other job types.
Ocr2OracleSpec *OCR2OracleSpecInfo `protobuf:"bytes,20,opt,name=ocr2_oracle_spec,json=ocr2OracleSpec,proto3" json:"ocr2_oracle_spec,omitempty"`
- // Node identity.
+ // Node identity
CsaPublicKey string `protobuf:"bytes,21,opt,name=csa_public_key,json=csaPublicKey,proto3" json:"csa_public_key,omitempty"`
NodeVersion string `protobuf:"bytes,22,opt,name=node_version,json=nodeVersion,proto3" json:"node_version,omitempty"`
Hostname string `protobuf:"bytes,23,opt,name=hostname,proto3" json:"hostname,omitempty"`
- // Event metadata.
+ // Event metadata
EmissionTrigger EmissionTrigger `protobuf:"varint,24,opt,name=emission_trigger,json=emissionTrigger,proto3,enum=job_spec.v1.EmissionTrigger" json:"emission_trigger,omitempty"`
Timestamp string `protobuf:"bytes,25,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
unknownFields protoimpl.UnknownFields
@@ -322,7 +318,7 @@ func (x *JobSpecEvent) GetTimestamp() string {
return ""
}
-// OCR2OracleSpecInfo carries all fields of job.OCR2OracleSpec.
+// OCR2OracleSpecInfo mirrors job.OCR2OracleSpec.
type OCR2OracleSpecInfo struct {
state protoimpl.MessageState `protogen:"open.v1"`
SpecId int32 `protobuf:"varint,1,opt,name=spec_id,json=specId,proto3" json:"spec_id,omitempty"`
@@ -343,14 +339,14 @@ type OCR2OracleSpecInfo struct {
CaptureAutomationCustomTelemetry bool `protobuf:"varint,16,opt,name=capture_automation_custom_telemetry,json=captureAutomationCustomTelemetry,proto3" json:"capture_automation_custom_telemetry,omitempty"`
SpecCreatedAt string `protobuf:"bytes,17,opt,name=spec_created_at,json=specCreatedAt,proto3" json:"spec_created_at,omitempty"`
SpecUpdatedAt string `protobuf:"bytes,18,opt,name=spec_updated_at,json=specUpdatedAt,proto3" json:"spec_updated_at,omitempty"`
- // Raw JSON passthroughs — always populated; authoritative for any field not
- // captured in the typed sub-messages below.
+ // Raw JSON passthroughs — always populated; authoritative over the typed
+ // sub-messages below.
RelayConfigJson string `protobuf:"bytes,19,opt,name=relay_config_json,json=relayConfigJson,proto3" json:"relay_config_json,omitempty"`
PluginConfigJson string `protobuf:"bytes,20,opt,name=plugin_config_json,json=pluginConfigJson,proto3" json:"plugin_config_json,omitempty"`
OnchainSigningStrategyJson string `protobuf:"bytes,21,opt,name=onchain_signing_strategy_json,json=onchainSigningStrategyJson,proto3" json:"onchain_signing_strategy_json,omitempty"`
- // Typed EVM relay config — populated only when relay == "evm".
+ // Populated when relay == "evm".
EvmRelayConfig *OCR2EVMRelayConfig `protobuf:"bytes,22,opt,name=evm_relay_config,json=evmRelayConfig,proto3" json:"evm_relay_config,omitempty"`
- // Typed median plugin config — populated only when plugin_type == "median".
+ // Populated when plugin_type == "median".
MedianPluginConfig *OCR2MedianPluginConfig `protobuf:"bytes,23,opt,name=median_plugin_config,json=medianPluginConfig,proto3" json:"median_plugin_config,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
@@ -547,9 +543,7 @@ func (x *OCR2OracleSpecInfo) GetMedianPluginConfig() *OCR2MedianPluginConfig {
return nil
}
-// OCR2EVMRelayConfig carries the well-known fields of the EVM relay config JSON.
-// relay_config_json on OCR2OracleSpecInfo remains authoritative for any field
-// not represented here.
+// OCR2EVMRelayConfig is a typed view of the EVM relay config JSON.
type OCR2EVMRelayConfig struct {
state protoimpl.MessageState `protogen:"open.v1"`
ChainId string `protobuf:"bytes,1,opt,name=chain_id,json=chainId,proto3" json:"chain_id,omitempty"`
@@ -658,19 +652,17 @@ func (x *OCR2EVMRelayConfig) GetProviderType() string {
return ""
}
-// OCR2MedianPluginConfig carries all fields of median/config.PluginConfig and
-// the nested JuelsPerFeeCoinCache.
+// OCR2MedianPluginConfig mirrors median/config.PluginConfig.
type OCR2MedianPluginConfig struct {
state protoimpl.MessageState `protogen:"open.v1"`
JuelsPerFeeCoinSource string `protobuf:"bytes,1,opt,name=juels_per_fee_coin_source,json=juelsPerFeeCoinSource,proto3" json:"juels_per_fee_coin_source,omitempty"`
- // Empty string when gasPriceSubunitsSource is not configured.
+ // Empty when gasPriceSubunitsSource is not configured.
GasPriceSubunitsSource string `protobuf:"bytes,2,opt,name=gas_price_subunits_source,json=gasPriceSubunitsSource,proto3" json:"gas_price_subunits_source,omitempty"`
- // juels_per_fee_coin_cache_disabled is true when JuelsPerFeeCoinCache is nil
- // (disabled when nil, per source comment) or when Disable is explicitly true.
+ // True when JuelsPerFeeCoinCache is nil or its Disable flag is set.
JuelsPerFeeCoinCacheDisabled bool `protobuf:"varint,3,opt,name=juels_per_fee_coin_cache_disabled,json=juelsPerFeeCoinCacheDisabled,proto3" json:"juels_per_fee_coin_cache_disabled,omitempty"`
JuelsPerFeeCoinCacheUpdateIntervalSeconds float64 `protobuf:"fixed64,4,opt,name=juels_per_fee_coin_cache_update_interval_seconds,json=juelsPerFeeCoinCacheUpdateIntervalSeconds,proto3" json:"juels_per_fee_coin_cache_update_interval_seconds,omitempty"`
JuelsPerFeeCoinCacheStalenessAlertThresholdSeconds float64 `protobuf:"fixed64,5,opt,name=juels_per_fee_coin_cache_staleness_alert_threshold_seconds,json=juelsPerFeeCoinCacheStalenessAlertThresholdSeconds,proto3" json:"juels_per_fee_coin_cache_staleness_alert_threshold_seconds,omitempty"`
- // Verbatim JSON of DeviationFunctionDefinition (map[string]any).
+ // Verbatim JSON of DeviationFunctionDefinition.
DeviationFuncJson string `protobuf:"bytes,6,opt,name=deviation_func_json,json=deviationFuncJson,proto3" json:"deviation_func_json,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
diff --git a/core/services/nodestatusreporter/jobspec/events/job_spec.proto b/core/services/nodestatusreporter/jobspec/events/job_spec.proto
index 6fcfadf9b89..384dda41cc9 100644
--- a/core/services/nodestatusreporter/jobspec/events/job_spec.proto
+++ b/core/services/nodestatusreporter/jobspec/events/job_spec.proto
@@ -4,12 +4,9 @@ option go_package = "github.com/smartcontractkit/chainlink/v2/core/services/node
package job_spec.v1;
-// JobSpecEvent is emitted for each active job on a heartbeat, on job creation,
-// and on job deletion. For the initial rollout only offchainreporting2 jobs
-// with pluginType = "median" are emitted (configurable via EnabledOCR2PluginTypes
-// and EmitNonOCR2Jobs in the JobSpecReporter config section).
+// JobSpecEvent carries a job's spec, emitted on heartbeat, create, and delete.
message JobSpecEvent {
- // Job identity — covers every TOML-writable field on job.Job plus DB-assigned columns.
+ // Job identity
string external_job_id = 1;
int32 internal_job_id = 2;
string name = 3;
@@ -21,15 +18,14 @@ message JobSpecEvent {
double max_task_duration_seconds = 9;
string created_at = 10;
- // Observation pipeline — the job's observationSource field.
+ // Observation pipeline
string observation_source = 11;
int32 pipeline_spec_id = 12;
- // Bridge names extracted from the observationSource DOT DAG (top-level only).
+ // Top-level bridge names in the observation pipeline.
repeated string bridge_names = 13;
- // Proposal lifecycle fields — zero/empty when the job was created manually
- // (not via a Feeds Manager / Job Distributor).
+ // Proposal lifecycle — zero/empty for jobs not managed by a Feeds Manager.
int64 feeds_manager_id = 14;
string remote_uuid = 15;
int32 spec_version = 16;
@@ -37,20 +33,20 @@ message JobSpecEvent {
string approved_at = 18;
double accept_latency_seconds = 19;
- // OCR2-specific fields — absent for non-OCR2 job types.
+ // OCR2-only; absent for other job types.
OCR2OracleSpecInfo ocr2_oracle_spec = 20;
- // Node identity.
+ // Node identity
string csa_public_key = 21;
string node_version = 22;
string hostname = 23;
- // Event metadata.
+ // Event metadata
EmissionTrigger emission_trigger = 24;
string timestamp = 25;
}
-// EmissionTrigger identifies the event that caused the JobSpecEvent to be emitted.
+// EmissionTrigger is the reason a JobSpecEvent was emitted.
enum EmissionTrigger {
EMISSION_TRIGGER_UNSPECIFIED = 0;
EMISSION_TRIGGER_HEARTBEAT = 1;
@@ -58,7 +54,7 @@ enum EmissionTrigger {
EMISSION_TRIGGER_DELETE = 3;
}
-// OCR2OracleSpecInfo carries all fields of job.OCR2OracleSpec.
+// OCR2OracleSpecInfo mirrors job.OCR2OracleSpec.
message OCR2OracleSpecInfo {
int32 spec_id = 1;
string contract_id = 2;
@@ -79,22 +75,20 @@ message OCR2OracleSpecInfo {
string spec_created_at = 17;
string spec_updated_at = 18;
- // Raw JSON passthroughs — always populated; authoritative for any field not
- // captured in the typed sub-messages below.
+ // Raw JSON passthroughs — always populated; authoritative over the typed
+ // sub-messages below.
string relay_config_json = 19;
string plugin_config_json = 20;
string onchain_signing_strategy_json = 21;
- // Typed EVM relay config — populated only when relay == "evm".
+ // Populated when relay == "evm".
OCR2EVMRelayConfig evm_relay_config = 22;
- // Typed median plugin config — populated only when plugin_type == "median".
+ // Populated when plugin_type == "median".
OCR2MedianPluginConfig median_plugin_config = 23;
}
-// OCR2EVMRelayConfig carries the well-known fields of the EVM relay config JSON.
-// relay_config_json on OCR2OracleSpecInfo remains authoritative for any field
-// not represented here.
+// OCR2EVMRelayConfig is a typed view of the EVM relay config JSON.
message OCR2EVMRelayConfig {
string chain_id = 1;
uint64 from_block = 2;
@@ -107,20 +101,18 @@ message OCR2EVMRelayConfig {
string provider_type = 9;
}
-// OCR2MedianPluginConfig carries all fields of median/config.PluginConfig and
-// the nested JuelsPerFeeCoinCache.
+// OCR2MedianPluginConfig mirrors median/config.PluginConfig.
message OCR2MedianPluginConfig {
string juels_per_fee_coin_source = 1;
- // Empty string when gasPriceSubunitsSource is not configured.
+ // Empty when gasPriceSubunitsSource is not configured.
string gas_price_subunits_source = 2;
- // juels_per_fee_coin_cache_disabled is true when JuelsPerFeeCoinCache is nil
- // (disabled when nil, per source comment) or when Disable is explicitly true.
+ // True when JuelsPerFeeCoinCache is nil or its Disable flag is set.
bool juels_per_fee_coin_cache_disabled = 3;
double juels_per_fee_coin_cache_update_interval_seconds = 4;
double juels_per_fee_coin_cache_staleness_alert_threshold_seconds = 5;
- // Verbatim JSON of DeviationFunctionDefinition (map[string]any).
+ // Verbatim JSON of DeviationFunctionDefinition.
string deviation_func_json = 6;
}
From 9e29959f1de1bd0a527d6921ff85c364cef5b6bd Mon Sep 17 00:00:00 2001
From: thomjg <103059015+thomjg@users.noreply.github.com>
Date: Tue, 21 Apr 2026 19:23:41 -0400
Subject: [PATCH 05/14] Adding support for OCR1 DF1 job specs
---
.../jobspec/events/job_spec.pb.go | 24 ++++++++-
.../jobspec/events/job_spec.proto | 5 ++
.../jobspec/job_spec_reporter.go | 9 ++++
.../jobspec/job_spec_reporter_test.go | 53 +++++++++++++++++++
4 files changed, 89 insertions(+), 2 deletions(-)
diff --git a/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go b/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go
index 78a174913fb..7c467ac649c 100644
--- a/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go
+++ b/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go
@@ -109,6 +109,10 @@ type JobSpecEvent struct {
// Event metadata
EmissionTrigger EmissionTrigger `protobuf:"varint,24,opt,name=emission_trigger,json=emissionTrigger,proto3,enum=job_spec.v1.EmissionTrigger" json:"emission_trigger,omitempty"`
Timestamp string `protobuf:"bytes,25,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
+ // Primary on-chain contract — populated for single-contract job types
+ // (OCR1, OCR2, Flux Monitor, Keeper). For OCR2, copied from ocr2_oracle_spec.
+ ContractAddress string `protobuf:"bytes,26,opt,name=contract_address,json=contractAddress,proto3" json:"contract_address,omitempty"`
+ ChainId string `protobuf:"bytes,27,opt,name=chain_id,json=chainId,proto3" json:"chain_id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -318,6 +322,20 @@ func (x *JobSpecEvent) GetTimestamp() string {
return ""
}
+func (x *JobSpecEvent) GetContractAddress() string {
+ if x != nil {
+ return x.ContractAddress
+ }
+ return ""
+}
+
+func (x *JobSpecEvent) GetChainId() string {
+ if x != nil {
+ return x.ChainId
+ }
+ return ""
+}
+
// OCR2OracleSpecInfo mirrors job.OCR2OracleSpec.
type OCR2OracleSpecInfo struct {
state protoimpl.MessageState `protogen:"open.v1"`
@@ -744,7 +762,7 @@ var File_job_spec_proto protoreflect.FileDescriptor
const file_job_spec_proto_rawDesc = "" +
"\n" +
- "\x0ejob_spec.proto\x12\vjob_spec.v1\"\x83\b\n" +
+ "\x0ejob_spec.proto\x12\vjob_spec.v1\"\xc9\b\n" +
"\fJobSpecEvent\x12&\n" +
"\x0fexternal_job_id\x18\x01 \x01(\tR\rexternalJobId\x12&\n" +
"\x0finternal_job_id\x18\x02 \x01(\x05R\rinternalJobId\x12\x12\n" +
@@ -775,7 +793,9 @@ const file_job_spec_proto_rawDesc = "" +
"\fnode_version\x18\x16 \x01(\tR\vnodeVersion\x12\x1a\n" +
"\bhostname\x18\x17 \x01(\tR\bhostname\x12G\n" +
"\x10emission_trigger\x18\x18 \x01(\x0e2\x1c.job_spec.v1.EmissionTriggerR\x0femissionTrigger\x12\x1c\n" +
- "\ttimestamp\x18\x19 \x01(\tR\ttimestampB\f\n" +
+ "\ttimestamp\x18\x19 \x01(\tR\ttimestamp\x12)\n" +
+ "\x10contract_address\x18\x1a \x01(\tR\x0fcontractAddress\x12\x19\n" +
+ "\bchain_id\x18\x1b \x01(\tR\achainIdB\f\n" +
"\n" +
"_stream_id\"\x96\t\n" +
"\x12OCR2OracleSpecInfo\x12\x17\n" +
diff --git a/core/services/nodestatusreporter/jobspec/events/job_spec.proto b/core/services/nodestatusreporter/jobspec/events/job_spec.proto
index 384dda41cc9..92e57dfdc95 100644
--- a/core/services/nodestatusreporter/jobspec/events/job_spec.proto
+++ b/core/services/nodestatusreporter/jobspec/events/job_spec.proto
@@ -44,6 +44,11 @@ message JobSpecEvent {
// Event metadata
EmissionTrigger emission_trigger = 24;
string timestamp = 25;
+
+ // Primary on-chain contract — populated for single-contract job types
+ // (OCR1, OCR2, Flux Monitor, Keeper). For OCR2, copied from ocr2_oracle_spec.
+ string contract_address = 26;
+ string chain_id = 27;
}
// EmissionTrigger is the reason a JobSpecEvent was emitted.
diff --git a/core/services/nodestatusreporter/jobspec/job_spec_reporter.go b/core/services/nodestatusreporter/jobspec/job_spec_reporter.go
index 4258ac36913..888353a70ce 100644
--- a/core/services/nodestatusreporter/jobspec/job_spec_reporter.go
+++ b/core/services/nodestatusreporter/jobspec/job_spec_reporter.go
@@ -194,6 +194,15 @@ func (s *Service) buildEvent(ctx context.Context, jb job.Job, trigger events.Emi
return nil, fmt.Errorf("building OCR2OracleSpecInfo: %w", err)
}
event.Ocr2OracleSpec = ocr2Info
+ event.ContractAddress = jb.OCR2OracleSpec.ContractID
+ event.ChainId = jb.OCR2OracleSpec.ChainID
+ }
+
+ if jb.Type == job.OffchainReporting && jb.OCROracleSpec != nil {
+ event.ContractAddress = jb.OCROracleSpec.ContractAddress.String()
+ if jb.OCROracleSpec.EVMChainID != nil {
+ event.ChainId = jb.OCROracleSpec.EVMChainID.String()
+ }
}
return event, nil
diff --git a/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go b/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go
index dd5dc8d82c5..0988f751496 100644
--- a/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go
+++ b/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go
@@ -15,8 +15,11 @@ import (
"github.com/smartcontractkit/chainlink-common/pkg/beholder"
"github.com/smartcontractkit/chainlink-common/pkg/beholder/beholdertest"
+ "github.com/smartcontractkit/chainlink-common/pkg/sqlutil"
commontypes "github.com/smartcontractkit/chainlink-common/pkg/types"
+ evmtypes "github.com/smartcontractkit/chainlink-evm/pkg/types"
+
"github.com/smartcontractkit/chainlink/v2/core/logger"
"github.com/smartcontractkit/chainlink/v2/core/services/feeds"
feedsmocks "github.com/smartcontractkit/chainlink/v2/core/services/feeds/mocks"
@@ -211,6 +214,8 @@ func TestBuildEvent_MedianJob(t *testing.T) {
assert.Equal(t, "1.0.0", ev.NodeVersion)
assert.Equal(t, "test-host", ev.Hostname)
assert.Equal(t, []string{"my-bridge"}, ev.BridgeNames)
+ assert.Equal(t, "0x1234567890abcdef", ev.ContractAddress)
+ assert.Equal(t, "1", ev.ChainId)
require.NotNil(t, ev.Ocr2OracleSpec)
assert.Equal(t, "evm", ev.Ocr2OracleSpec.Relay)
assert.Equal(t, "median", ev.Ocr2OracleSpec.PluginType)
@@ -339,3 +344,51 @@ func TestBuildEvent_ProposalLifecycle(t *testing.T) {
assert.Equal(t, int32(3), ev.SpecVersion)
assert.InDelta(t, approvedAt.Sub(proposedAt).Seconds(), ev.AcceptLatencySeconds, 1.0)
}
+
+func TestBuildEvent_ContractFields_OCR1(t *testing.T) {
+ observer := beholdertest.NewObserver(t)
+ cfg := &stubConfig{
+ enabled: true,
+ pollingInterval: time.Hour,
+ emitNonOCR2Jobs: true, // OCR1 is non-OCR2
+ }
+ svc := newTestReporter(t, cfg, nil)
+
+ jb := makeOCR1Job()
+ err := svc.EmitForJob(context.Background(), jb, events.EmissionTrigger_EMISSION_TRIGGER_HEARTBEAT)
+ require.NoError(t, err)
+
+ msgs := observer.Messages(t, "beholder_entity", events.ProtoPkg+"."+events.JobSpecEventEntity)
+ require.Len(t, msgs, 1)
+
+ var ev events.JobSpecEvent
+ require.NoError(t, proto.Unmarshal(msgs[0].Body, &ev))
+
+ assert.Equal(t, "0x9d9305445F404E925563d5D5EcC65C815Ec1655b", ev.ContractAddress)
+ assert.Equal(t, "11155111", ev.ChainId)
+ assert.Equal(t, "offchainreporting", ev.JobType)
+}
+
+func makeOCR1Job() job.Job {
+ return job.Job{
+ ID: 4,
+ ExternalJobID: uuid.New(),
+ Name: null.StringFrom("test-ocr1-job"),
+ Type: job.OffchainReporting,
+ SchemaVersion: 1,
+ PipelineSpec: &pipeline.Spec{ID: 40, DotDagSource: `ds1 [type=bridge name="bridge-gsr"]`},
+ Pipeline: pipeline.Pipeline{
+ Tasks: []pipeline.Task{
+ &pipeline.BridgeTask{
+ BaseTask: pipeline.NewBaseTask(0, "ds1", nil, nil, 0),
+ Name: "bridge-gsr",
+ },
+ },
+ },
+ OCROracleSpec: &job.OCROracleSpec{
+ ContractAddress: evmtypes.MustEIP55Address("0x9d9305445F404E925563d5D5EcC65C815Ec1655b"),
+ EVMChainID: sqlutil.NewI(11155111),
+ },
+ CreatedAt: time.Now(),
+ }
+}
From 8c341c88c3038648d4026d43d510d08d9aa1efb5 Mon Sep 17 00:00:00 2001
From: thomjg <103059015+thomjg@users.noreply.github.com>
Date: Tue, 21 Apr 2026 20:09:10 -0400
Subject: [PATCH 06/14] Fix CI: regenerate mocks, fix unconvert lint, update
testdata
---
core/config/docs/core.toml | 11 +++
.../chainlink/mocks/general_config.go | 94 +++++++++----------
.../testdata/config-empty-effective.toml | 3 +
.../config-multi-chain-effective.toml | 3 +
core/services/feeds/mocks/orm.go | 64 ++++++-------
core/services/job/mocks/spawner.go | 66 ++++++-------
.../jobspec/job_spec_reporter.go | 2 +-
.../testdata/config-empty-effective.toml | 6 ++
core/web/resolver/testdata/config-full.toml | 6 ++
.../config-multi-chain-effective.toml | 6 ++
core/web/testdata/body/health.html | 3 +
core/web/testdata/body/health.json | 9 ++
core/web/testdata/body/health.txt | 1 +
.../scripts/config/merge_raw_configs.txtar | 6 ++
testdata/scripts/health/default.txtar | 10 ++
.../scripts/health/multi-chain-loopp.txtar | 10 ++
testdata/scripts/health/multi-chain.txtar | 10 ++
testdata/scripts/node/validate/default.txtar | 6 ++
.../node/validate/defaults-override.txtar | 6 ++
.../disk-based-logging-disabled.txtar | 6 ++
.../validate/disk-based-logging-no-dir.txtar | 6 ++
.../node/validate/disk-based-logging.txtar | 6 ++
.../node/validate/fallback-override.txtar | 6 ++
.../node/validate/invalid-ocr-p2p.txtar | 6 ++
testdata/scripts/node/validate/invalid.txtar | 6 ++
testdata/scripts/node/validate/valid.txtar | 6 ++
testdata/scripts/node/validate/warnings.txtar | 6 ++
27 files changed, 257 insertions(+), 113 deletions(-)
diff --git a/core/config/docs/core.toml b/core/config/docs/core.toml
index fdaf318108d..00c60438603 100644
--- a/core/config/docs/core.toml
+++ b/core/config/docs/core.toml
@@ -948,6 +948,17 @@ IgnoreInvalidBridges = true # Default
# IgnoreJoblessBridges skips bridges that have no associated jobs.
IgnoreJoblessBridges = false # Default
+# JobSpecReporter holds settings for the Job Spec Reporter service, which periodically emits job spec telemetry.
+[JobSpecReporter]
+# Enabled enables the Job Spec Reporter service.
+Enabled = false # Default
+# PollingInterval is how often to emit a heartbeat event for each tracked job.
+PollingInterval = "1h" # Default
+# EnabledOCR2PluginTypes restricts OCR2 telemetry to jobs with these plugin types.
+EnabledOCR2PluginTypes = ["median"] # Default
+# EmitNonOCR2Jobs emits telemetry for non-OCR2 job types (OCR1, Flux Monitor, Keeper).
+EmitNonOCR2Jobs = false # Default
+
[CRE]
# UseLocalTimeProvider should be set true if the DON Time OCR Plugin is not running
UseLocalTimeProvider = true # Default
diff --git a/core/services/chainlink/mocks/general_config.go b/core/services/chainlink/mocks/general_config.go
index fed24c8ee20..4371546e57c 100644
--- a/core/services/chainlink/mocks/general_config.go
+++ b/core/services/chainlink/mocks/general_config.go
@@ -354,53 +354,6 @@ func (_c *GeneralConfig_BridgeStatusReporter_Call) RunAndReturn(run func() confi
return _c
}
-// JobSpecReporter provides a mock function with no fields
-func (_m *GeneralConfig) JobSpecReporter() config.JobSpecReporter {
- ret := _m.Called()
-
- if len(ret) == 0 {
- panic("no return value specified for JobSpecReporter")
- }
-
- var r0 config.JobSpecReporter
- if rf, ok := ret.Get(0).(func() config.JobSpecReporter); ok {
- r0 = rf()
- } else {
- if ret.Get(0) != nil {
- r0 = ret.Get(0).(config.JobSpecReporter)
- }
- }
-
- return r0
-}
-
-// GeneralConfig_JobSpecReporter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'JobSpecReporter'
-type GeneralConfig_JobSpecReporter_Call struct {
- *mock.Call
-}
-
-// JobSpecReporter is a helper method to define mock.On call
-func (_e *GeneralConfig_Expecter) JobSpecReporter() *GeneralConfig_JobSpecReporter_Call {
- return &GeneralConfig_JobSpecReporter_Call{Call: _e.mock.On("JobSpecReporter")}
-}
-
-func (_c *GeneralConfig_JobSpecReporter_Call) Run(run func()) *GeneralConfig_JobSpecReporter_Call {
- _c.Call.Run(func(args mock.Arguments) {
- run()
- })
- return _c
-}
-
-func (_c *GeneralConfig_JobSpecReporter_Call) Return(_a0 config.JobSpecReporter) *GeneralConfig_JobSpecReporter_Call {
- _c.Call.Return(_a0)
- return _c
-}
-
-func (_c *GeneralConfig_JobSpecReporter_Call) RunAndReturn(run func() config.JobSpecReporter) *GeneralConfig_JobSpecReporter_Call {
- _c.Call.Return(run)
- return _c
-}
-
// CCV provides a mock function with no fields
func (_m *GeneralConfig) CCV() config.CCV {
ret := _m.Called()
@@ -1388,6 +1341,53 @@ func (_c *GeneralConfig_JobPipeline_Call) RunAndReturn(run func() config.JobPipe
return _c
}
+// JobSpecReporter provides a mock function with no fields
+func (_m *GeneralConfig) JobSpecReporter() config.JobSpecReporter {
+ ret := _m.Called()
+
+ if len(ret) == 0 {
+ panic("no return value specified for JobSpecReporter")
+ }
+
+ var r0 config.JobSpecReporter
+ if rf, ok := ret.Get(0).(func() config.JobSpecReporter); ok {
+ r0 = rf()
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(config.JobSpecReporter)
+ }
+ }
+
+ return r0
+}
+
+// GeneralConfig_JobSpecReporter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'JobSpecReporter'
+type GeneralConfig_JobSpecReporter_Call struct {
+ *mock.Call
+}
+
+// JobSpecReporter is a helper method to define mock.On call
+func (_e *GeneralConfig_Expecter) JobSpecReporter() *GeneralConfig_JobSpecReporter_Call {
+ return &GeneralConfig_JobSpecReporter_Call{Call: _e.mock.On("JobSpecReporter")}
+}
+
+func (_c *GeneralConfig_JobSpecReporter_Call) Run(run func()) *GeneralConfig_JobSpecReporter_Call {
+ _c.Call.Run(func(args mock.Arguments) {
+ run()
+ })
+ return _c
+}
+
+func (_c *GeneralConfig_JobSpecReporter_Call) Return(_a0 config.JobSpecReporter) *GeneralConfig_JobSpecReporter_Call {
+ _c.Call.Return(_a0)
+ return _c
+}
+
+func (_c *GeneralConfig_JobSpecReporter_Call) RunAndReturn(run func() config.JobSpecReporter) *GeneralConfig_JobSpecReporter_Call {
+ _c.Call.Return(run)
+ return _c
+}
+
// Keeper provides a mock function with no fields
func (_m *GeneralConfig) Keeper() config.Keeper {
ret := _m.Called()
diff --git a/core/services/chainlink/testdata/config-empty-effective.toml b/core/services/chainlink/testdata/config-empty-effective.toml
index 0588f5acb94..43372ef0375 100644
--- a/core/services/chainlink/testdata/config-empty-effective.toml
+++ b/core/services/chainlink/testdata/config-empty-effective.toml
@@ -401,6 +401,9 @@ IgnoreJoblessBridges = false
[JobSpecReporter]
Enabled = false
+PollingInterval = '1h0m0s'
+EnabledOCR2PluginTypes = ['median']
+EmitNonOCR2Jobs = false
[Sharding]
ShardingEnabled = false
diff --git a/core/services/chainlink/testdata/config-multi-chain-effective.toml b/core/services/chainlink/testdata/config-multi-chain-effective.toml
index a861e25dd5f..f8c8bc32eb8 100644
--- a/core/services/chainlink/testdata/config-multi-chain-effective.toml
+++ b/core/services/chainlink/testdata/config-multi-chain-effective.toml
@@ -401,6 +401,9 @@ IgnoreJoblessBridges = false
[JobSpecReporter]
Enabled = false
+PollingInterval = '1h0m0s'
+EnabledOCR2PluginTypes = ['median']
+EmitNonOCR2Jobs = false
[Sharding]
ShardingEnabled = false
diff --git a/core/services/feeds/mocks/orm.go b/core/services/feeds/mocks/orm.go
index c19b13a4c2d..7b527b0727c 100644
--- a/core/services/feeds/mocks/orm.go
+++ b/core/services/feeds/mocks/orm.go
@@ -1034,21 +1034,21 @@ func (_c *ORM_GetJobProposal_Call) RunAndReturn(run func(context.Context, int64)
return _c
}
-// GetJobProposalByRemoteUUID provides a mock function with given fields: ctx, _a1
-func (_m *ORM) GetJobProposalByRemoteUUID(ctx context.Context, _a1 uuid.UUID) (*feeds.JobProposal, error) {
- ret := _m.Called(ctx, _a1)
+// GetJobProposalByExternalJobID provides a mock function with given fields: ctx, externalJobID
+func (_m *ORM) GetJobProposalByExternalJobID(ctx context.Context, externalJobID uuid.UUID) (*feeds.JobProposal, error) {
+ ret := _m.Called(ctx, externalJobID)
if len(ret) == 0 {
- panic("no return value specified for GetJobProposalByRemoteUUID")
+ panic("no return value specified for GetJobProposalByExternalJobID")
}
var r0 *feeds.JobProposal
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) (*feeds.JobProposal, error)); ok {
- return rf(ctx, _a1)
+ return rf(ctx, externalJobID)
}
if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) *feeds.JobProposal); ok {
- r0 = rf(ctx, _a1)
+ r0 = rf(ctx, externalJobID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*feeds.JobProposal)
@@ -1056,7 +1056,7 @@ func (_m *ORM) GetJobProposalByRemoteUUID(ctx context.Context, _a1 uuid.UUID) (*
}
if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok {
- r1 = rf(ctx, _a1)
+ r1 = rf(ctx, externalJobID)
} else {
r1 = ret.Error(1)
}
@@ -1064,50 +1064,50 @@ func (_m *ORM) GetJobProposalByRemoteUUID(ctx context.Context, _a1 uuid.UUID) (*
return r0, r1
}
-// ORM_GetJobProposalByRemoteUUID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetJobProposalByRemoteUUID'
-type ORM_GetJobProposalByRemoteUUID_Call struct {
+// ORM_GetJobProposalByExternalJobID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetJobProposalByExternalJobID'
+type ORM_GetJobProposalByExternalJobID_Call struct {
*mock.Call
}
-// GetJobProposalByRemoteUUID is a helper method to define mock.On call
+// GetJobProposalByExternalJobID is a helper method to define mock.On call
// - ctx context.Context
-// - _a1 uuid.UUID
-func (_e *ORM_Expecter) GetJobProposalByRemoteUUID(ctx interface{}, _a1 interface{}) *ORM_GetJobProposalByRemoteUUID_Call {
- return &ORM_GetJobProposalByRemoteUUID_Call{Call: _e.mock.On("GetJobProposalByRemoteUUID", ctx, _a1)}
+// - externalJobID uuid.UUID
+func (_e *ORM_Expecter) GetJobProposalByExternalJobID(ctx interface{}, externalJobID interface{}) *ORM_GetJobProposalByExternalJobID_Call {
+ return &ORM_GetJobProposalByExternalJobID_Call{Call: _e.mock.On("GetJobProposalByExternalJobID", ctx, externalJobID)}
}
-func (_c *ORM_GetJobProposalByRemoteUUID_Call) Run(run func(ctx context.Context, _a1 uuid.UUID)) *ORM_GetJobProposalByRemoteUUID_Call {
+func (_c *ORM_GetJobProposalByExternalJobID_Call) Run(run func(ctx context.Context, externalJobID uuid.UUID)) *ORM_GetJobProposalByExternalJobID_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(uuid.UUID))
})
return _c
}
-func (_c *ORM_GetJobProposalByRemoteUUID_Call) Return(_a0 *feeds.JobProposal, _a1 error) *ORM_GetJobProposalByRemoteUUID_Call {
+func (_c *ORM_GetJobProposalByExternalJobID_Call) Return(_a0 *feeds.JobProposal, _a1 error) *ORM_GetJobProposalByExternalJobID_Call {
_c.Call.Return(_a0, _a1)
return _c
}
-func (_c *ORM_GetJobProposalByRemoteUUID_Call) RunAndReturn(run func(context.Context, uuid.UUID) (*feeds.JobProposal, error)) *ORM_GetJobProposalByRemoteUUID_Call {
+func (_c *ORM_GetJobProposalByExternalJobID_Call) RunAndReturn(run func(context.Context, uuid.UUID) (*feeds.JobProposal, error)) *ORM_GetJobProposalByExternalJobID_Call {
_c.Call.Return(run)
return _c
}
-// GetJobProposalByExternalJobID provides a mock function with given fields: ctx, externalJobID
-func (_m *ORM) GetJobProposalByExternalJobID(ctx context.Context, externalJobID uuid.UUID) (*feeds.JobProposal, error) {
- ret := _m.Called(ctx, externalJobID)
+// GetJobProposalByRemoteUUID provides a mock function with given fields: ctx, _a1
+func (_m *ORM) GetJobProposalByRemoteUUID(ctx context.Context, _a1 uuid.UUID) (*feeds.JobProposal, error) {
+ ret := _m.Called(ctx, _a1)
if len(ret) == 0 {
- panic("no return value specified for GetJobProposalByExternalJobID")
+ panic("no return value specified for GetJobProposalByRemoteUUID")
}
var r0 *feeds.JobProposal
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) (*feeds.JobProposal, error)); ok {
- return rf(ctx, externalJobID)
+ return rf(ctx, _a1)
}
if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) *feeds.JobProposal); ok {
- r0 = rf(ctx, externalJobID)
+ r0 = rf(ctx, _a1)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*feeds.JobProposal)
@@ -1115,7 +1115,7 @@ func (_m *ORM) GetJobProposalByExternalJobID(ctx context.Context, externalJobID
}
if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok {
- r1 = rf(ctx, externalJobID)
+ r1 = rf(ctx, _a1)
} else {
r1 = ret.Error(1)
}
@@ -1123,31 +1123,31 @@ func (_m *ORM) GetJobProposalByExternalJobID(ctx context.Context, externalJobID
return r0, r1
}
-// ORM_GetJobProposalByExternalJobID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetJobProposalByExternalJobID'
-type ORM_GetJobProposalByExternalJobID_Call struct {
+// ORM_GetJobProposalByRemoteUUID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetJobProposalByRemoteUUID'
+type ORM_GetJobProposalByRemoteUUID_Call struct {
*mock.Call
}
-// GetJobProposalByExternalJobID is a helper method to define mock.On call
+// GetJobProposalByRemoteUUID is a helper method to define mock.On call
// - ctx context.Context
-// - externalJobID uuid.UUID
-func (_e *ORM_Expecter) GetJobProposalByExternalJobID(ctx interface{}, externalJobID interface{}) *ORM_GetJobProposalByExternalJobID_Call {
- return &ORM_GetJobProposalByExternalJobID_Call{Call: _e.mock.On("GetJobProposalByExternalJobID", ctx, externalJobID)}
+// - _a1 uuid.UUID
+func (_e *ORM_Expecter) GetJobProposalByRemoteUUID(ctx interface{}, _a1 interface{}) *ORM_GetJobProposalByRemoteUUID_Call {
+ return &ORM_GetJobProposalByRemoteUUID_Call{Call: _e.mock.On("GetJobProposalByRemoteUUID", ctx, _a1)}
}
-func (_c *ORM_GetJobProposalByExternalJobID_Call) Run(run func(ctx context.Context, externalJobID uuid.UUID)) *ORM_GetJobProposalByExternalJobID_Call {
+func (_c *ORM_GetJobProposalByRemoteUUID_Call) Run(run func(ctx context.Context, _a1 uuid.UUID)) *ORM_GetJobProposalByRemoteUUID_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(uuid.UUID))
})
return _c
}
-func (_c *ORM_GetJobProposalByExternalJobID_Call) Return(_a0 *feeds.JobProposal, _a1 error) *ORM_GetJobProposalByExternalJobID_Call {
+func (_c *ORM_GetJobProposalByRemoteUUID_Call) Return(_a0 *feeds.JobProposal, _a1 error) *ORM_GetJobProposalByRemoteUUID_Call {
_c.Call.Return(_a0, _a1)
return _c
}
-func (_c *ORM_GetJobProposalByExternalJobID_Call) RunAndReturn(run func(context.Context, uuid.UUID) (*feeds.JobProposal, error)) *ORM_GetJobProposalByExternalJobID_Call {
+func (_c *ORM_GetJobProposalByRemoteUUID_Call) RunAndReturn(run func(context.Context, uuid.UUID) (*feeds.JobProposal, error)) *ORM_GetJobProposalByRemoteUUID_Call {
_c.Call.Return(run)
return _c
}
diff --git a/core/services/job/mocks/spawner.go b/core/services/job/mocks/spawner.go
index 2df19de32a3..22b6013d4be 100644
--- a/core/services/job/mocks/spawner.go
+++ b/core/services/job/mocks/spawner.go
@@ -348,6 +348,39 @@ func (_c *Spawner_Ready_Call) RunAndReturn(run func() error) *Spawner_Ready_Call
return _c
}
+// RegisterListener provides a mock function with given fields: l
+func (_m *Spawner) RegisterListener(l job.Listener) {
+ _m.Called(l)
+}
+
+// Spawner_RegisterListener_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RegisterListener'
+type Spawner_RegisterListener_Call struct {
+ *mock.Call
+}
+
+// RegisterListener is a helper method to define mock.On call
+// - l job.Listener
+func (_e *Spawner_Expecter) RegisterListener(l interface{}) *Spawner_RegisterListener_Call {
+ return &Spawner_RegisterListener_Call{Call: _e.mock.On("RegisterListener", l)}
+}
+
+func (_c *Spawner_RegisterListener_Call) Run(run func(l job.Listener)) *Spawner_RegisterListener_Call {
+ _c.Call.Run(func(args mock.Arguments) {
+ run(args[0].(job.Listener))
+ })
+ return _c
+}
+
+func (_c *Spawner_RegisterListener_Call) Return() *Spawner_RegisterListener_Call {
+ _c.Call.Return()
+ return _c
+}
+
+func (_c *Spawner_RegisterListener_Call) RunAndReturn(run func(job.Listener)) *Spawner_RegisterListener_Call {
+ _c.Run(run)
+ return _c
+}
+
// Start provides a mock function with given fields: _a0
func (_m *Spawner) Start(_a0 context.Context) error {
ret := _m.Called(_a0)
@@ -441,39 +474,6 @@ func (_c *Spawner_StartService_Call) RunAndReturn(run func(context.Context, job.
return _c
}
-// RegisterListener provides a mock function with given fields: l
-func (_m *Spawner) RegisterListener(l job.Listener) {
- _m.Called(l)
-}
-
-// Spawner_RegisterListener_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RegisterListener'
-type Spawner_RegisterListener_Call struct {
- *mock.Call
-}
-
-// RegisterListener is a helper method to define mock.On call
-// - l job.Listener
-func (_e *Spawner_Expecter) RegisterListener(l interface{}) *Spawner_RegisterListener_Call {
- return &Spawner_RegisterListener_Call{Call: _e.mock.On("RegisterListener", l)}
-}
-
-func (_c *Spawner_RegisterListener_Call) Run(run func(l job.Listener)) *Spawner_RegisterListener_Call {
- _c.Call.Run(func(args mock.Arguments) {
- run(args[0].(job.Listener))
- })
- return _c
-}
-
-func (_c *Spawner_RegisterListener_Call) Return() *Spawner_RegisterListener_Call {
- _c.Call.Return()
- return _c
-}
-
-func (_c *Spawner_RegisterListener_Call) RunAndReturn(run func(job.Listener)) *Spawner_RegisterListener_Call {
- _c.Call.Return(run)
- return _c
-}
-
// NewSpawner creates a new instance of Spawner. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewSpawner(t interface {
diff --git a/core/services/nodestatusreporter/jobspec/job_spec_reporter.go b/core/services/nodestatusreporter/jobspec/job_spec_reporter.go
index 888353a70ce..28f381c61d2 100644
--- a/core/services/nodestatusreporter/jobspec/job_spec_reporter.go
+++ b/core/services/nodestatusreporter/jobspec/job_spec_reporter.go
@@ -233,7 +233,7 @@ func (s *Service) populateProposalLifecycle(ctx context.Context, jb job.Job, eve
event.FeedsManagerId = prop.FeedsManagerID
event.RemoteUuid = prop.RemoteUUID.String()
- event.SpecVersion = int32(spec.Version)
+ event.SpecVersion = spec.Version
event.ProposedAt = spec.CreatedAt.Format(time.RFC3339Nano)
event.ApprovedAt = spec.StatusUpdatedAt.Format(time.RFC3339Nano)
event.AcceptLatencySeconds = spec.StatusUpdatedAt.Sub(spec.CreatedAt).Seconds()
diff --git a/core/web/resolver/testdata/config-empty-effective.toml b/core/web/resolver/testdata/config-empty-effective.toml
index c6498692f99..43372ef0375 100644
--- a/core/web/resolver/testdata/config-empty-effective.toml
+++ b/core/web/resolver/testdata/config-empty-effective.toml
@@ -399,6 +399,12 @@ PollingInterval = '5m0s'
IgnoreInvalidBridges = true
IgnoreJoblessBridges = false
+[JobSpecReporter]
+Enabled = false
+PollingInterval = '1h0m0s'
+EnabledOCR2PluginTypes = ['median']
+EmitNonOCR2Jobs = false
+
[Sharding]
ShardingEnabled = false
ArbiterPort = 9876
diff --git a/core/web/resolver/testdata/config-full.toml b/core/web/resolver/testdata/config-full.toml
index 5543f86326f..68001b07d6b 100644
--- a/core/web/resolver/testdata/config-full.toml
+++ b/core/web/resolver/testdata/config-full.toml
@@ -416,6 +416,12 @@ PollingInterval = '5m0s'
IgnoreInvalidBridges = true
IgnoreJoblessBridges = false
+[JobSpecReporter]
+Enabled = false
+PollingInterval = '1h0m0s'
+EnabledOCR2PluginTypes = ['median']
+EmitNonOCR2Jobs = false
+
[Sharding]
ShardingEnabled = false
ArbiterPort = 9876
diff --git a/core/web/resolver/testdata/config-multi-chain-effective.toml b/core/web/resolver/testdata/config-multi-chain-effective.toml
index f0cc5322857..6ce956f61d7 100644
--- a/core/web/resolver/testdata/config-multi-chain-effective.toml
+++ b/core/web/resolver/testdata/config-multi-chain-effective.toml
@@ -399,6 +399,12 @@ PollingInterval = '5m0s'
IgnoreInvalidBridges = true
IgnoreJoblessBridges = false
+[JobSpecReporter]
+Enabled = false
+PollingInterval = '1h0m0s'
+EnabledOCR2PluginTypes = ['median']
+EmitNonOCR2Jobs = false
+
[Sharding]
ShardingEnabled = false
ArbiterPort = 9876
diff --git a/core/web/testdata/body/health.html b/core/web/testdata/body/health.html
index cf189621cf5..1b9a4ca70d1 100644
--- a/core/web/testdata/body/health.html
+++ b/core/web/testdata/body/health.html
@@ -93,6 +93,9 @@
JobSpawner
+
+ JobSpecReporter
+
LLOTransmissionReaper
diff --git a/core/web/testdata/body/health.json b/core/web/testdata/body/health.json
index 31054ef17e7..8a2138abe59 100644
--- a/core/web/testdata/body/health.json
+++ b/core/web/testdata/body/health.json
@@ -171,6 +171,15 @@
"output": ""
}
},
+ {
+ "type": "checks",
+ "id": "JobSpecReporter",
+ "attributes": {
+ "name": "JobSpecReporter",
+ "status": "passing",
+ "output": ""
+ }
+ },
{
"type": "checks",
"id": "LLOTransmissionReaper",
diff --git a/core/web/testdata/body/health.txt b/core/web/testdata/body/health.txt
index 59a77f4a057..dab28b06375 100644
--- a/core/web/testdata/body/health.txt
+++ b/core/web/testdata/body/health.txt
@@ -18,6 +18,7 @@ ok EVM.1399100.Txm.WrappedEvmEstimator
ok HeadReporter
ok Heartbeat
ok JobSpawner
+ok JobSpecReporter
ok LLOTransmissionReaper
ok Mailbox.Monitor
ok Mercury.WSRPCPool
diff --git a/testdata/scripts/config/merge_raw_configs.txtar b/testdata/scripts/config/merge_raw_configs.txtar
index b84a9e33e31..316d8f23aa4 100644
--- a/testdata/scripts/config/merge_raw_configs.txtar
+++ b/testdata/scripts/config/merge_raw_configs.txtar
@@ -546,6 +546,12 @@ PollingInterval = '5m0s'
IgnoreInvalidBridges = true
IgnoreJoblessBridges = false
+[JobSpecReporter]
+Enabled = false
+PollingInterval = '1h0m0s'
+EnabledOCR2PluginTypes = ['median']
+EmitNonOCR2Jobs = false
+
[Sharding]
ShardingEnabled = false
ArbiterPort = 9876
diff --git a/testdata/scripts/health/default.txtar b/testdata/scripts/health/default.txtar
index 675d2561a79..9fa76e1db1d 100644
--- a/testdata/scripts/health/default.txtar
+++ b/testdata/scripts/health/default.txtar
@@ -37,6 +37,7 @@ ok CRE.DispatcherWrapper
ok HeadReporter
ok Heartbeat
ok JobSpawner
+ok JobSpecReporter
ok LLOTransmissionReaper
ok Mailbox.Monitor
ok Mercury.WSRPCPool
@@ -106,6 +107,15 @@ ok WorkflowStore
"output": ""
}
},
+ {
+ "type": "checks",
+ "id": "JobSpecReporter",
+ "attributes": {
+ "name": "JobSpecReporter",
+ "status": "passing",
+ "output": ""
+ }
+ },
{
"type": "checks",
"id": "LLOTransmissionReaper",
diff --git a/testdata/scripts/health/multi-chain-loopp.txtar b/testdata/scripts/health/multi-chain-loopp.txtar
index 561bcd58a82..616d5f8e8ce 100644
--- a/testdata/scripts/health/multi-chain-loopp.txtar
+++ b/testdata/scripts/health/multi-chain-loopp.txtar
@@ -138,6 +138,7 @@ ok EVM.1.RelayerService.PluginRelayerClient.PluginEVM.Txm.WrappedEvmEstimator
ok HeadReporter
ok Heartbeat
ok JobSpawner
+ok JobSpecReporter
ok LLOTransmissionReaper
ok Mailbox.Monitor
ok Mercury.WSRPCPool
@@ -499,6 +500,15 @@ ok WorkflowStore
"output": ""
}
},
+ {
+ "type": "checks",
+ "id": "JobSpecReporter",
+ "attributes": {
+ "name": "JobSpecReporter",
+ "status": "passing",
+ "output": ""
+ }
+ },
{
"type": "checks",
"id": "LLOTransmissionReaper",
diff --git a/testdata/scripts/health/multi-chain.txtar b/testdata/scripts/health/multi-chain.txtar
index ad456fc2db8..a3300d75b24 100644
--- a/testdata/scripts/health/multi-chain.txtar
+++ b/testdata/scripts/health/multi-chain.txtar
@@ -74,6 +74,7 @@ ok EVM.1.Txm.WrappedEvmEstimator
ok HeadReporter
ok Heartbeat
ok JobSpawner
+ok JobSpecReporter
ok LLOTransmissionReaper
ok Mailbox.Monitor
ok Mercury.WSRPCPool
@@ -268,6 +269,15 @@ ok WorkflowStore
"output": ""
}
},
+ {
+ "type": "checks",
+ "id": "JobSpecReporter",
+ "attributes": {
+ "name": "JobSpecReporter",
+ "status": "passing",
+ "output": ""
+ }
+ },
{
"type": "checks",
"id": "LLOTransmissionReaper",
diff --git a/testdata/scripts/node/validate/default.txtar b/testdata/scripts/node/validate/default.txtar
index ac36b6ccdcd..11ffa452db7 100644
--- a/testdata/scripts/node/validate/default.txtar
+++ b/testdata/scripts/node/validate/default.txtar
@@ -411,6 +411,12 @@ PollingInterval = '5m0s'
IgnoreInvalidBridges = true
IgnoreJoblessBridges = false
+[JobSpecReporter]
+Enabled = false
+PollingInterval = '1h0m0s'
+EnabledOCR2PluginTypes = ['median']
+EmitNonOCR2Jobs = false
+
[Sharding]
ShardingEnabled = false
ArbiterPort = 9876
diff --git a/testdata/scripts/node/validate/defaults-override.txtar b/testdata/scripts/node/validate/defaults-override.txtar
index bac1c47a96f..0389cecf3ed 100644
--- a/testdata/scripts/node/validate/defaults-override.txtar
+++ b/testdata/scripts/node/validate/defaults-override.txtar
@@ -472,6 +472,12 @@ PollingInterval = '5m0s'
IgnoreInvalidBridges = true
IgnoreJoblessBridges = false
+[JobSpecReporter]
+Enabled = false
+PollingInterval = '1h0m0s'
+EnabledOCR2PluginTypes = ['median']
+EmitNonOCR2Jobs = false
+
[Sharding]
ShardingEnabled = false
ArbiterPort = 9876
diff --git a/testdata/scripts/node/validate/disk-based-logging-disabled.txtar b/testdata/scripts/node/validate/disk-based-logging-disabled.txtar
index b2764eaa702..d7684de0f84 100644
--- a/testdata/scripts/node/validate/disk-based-logging-disabled.txtar
+++ b/testdata/scripts/node/validate/disk-based-logging-disabled.txtar
@@ -455,6 +455,12 @@ PollingInterval = '5m0s'
IgnoreInvalidBridges = true
IgnoreJoblessBridges = false
+[JobSpecReporter]
+Enabled = false
+PollingInterval = '1h0m0s'
+EnabledOCR2PluginTypes = ['median']
+EmitNonOCR2Jobs = false
+
[Sharding]
ShardingEnabled = false
ArbiterPort = 9876
diff --git a/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar b/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar
index 4de0b5cd61e..15553429942 100644
--- a/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar
+++ b/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar
@@ -455,6 +455,12 @@ PollingInterval = '5m0s'
IgnoreInvalidBridges = true
IgnoreJoblessBridges = false
+[JobSpecReporter]
+Enabled = false
+PollingInterval = '1h0m0s'
+EnabledOCR2PluginTypes = ['median']
+EmitNonOCR2Jobs = false
+
[Sharding]
ShardingEnabled = false
ArbiterPort = 9876
diff --git a/testdata/scripts/node/validate/disk-based-logging.txtar b/testdata/scripts/node/validate/disk-based-logging.txtar
index 91213ee5e56..61c714ec01b 100644
--- a/testdata/scripts/node/validate/disk-based-logging.txtar
+++ b/testdata/scripts/node/validate/disk-based-logging.txtar
@@ -455,6 +455,12 @@ PollingInterval = '5m0s'
IgnoreInvalidBridges = true
IgnoreJoblessBridges = false
+[JobSpecReporter]
+Enabled = false
+PollingInterval = '1h0m0s'
+EnabledOCR2PluginTypes = ['median']
+EmitNonOCR2Jobs = false
+
[Sharding]
ShardingEnabled = false
ArbiterPort = 9876
diff --git a/testdata/scripts/node/validate/fallback-override.txtar b/testdata/scripts/node/validate/fallback-override.txtar
index 6dd459a0379..b94cca4a038 100644
--- a/testdata/scripts/node/validate/fallback-override.txtar
+++ b/testdata/scripts/node/validate/fallback-override.txtar
@@ -553,6 +553,12 @@ PollingInterval = '5m0s'
IgnoreInvalidBridges = true
IgnoreJoblessBridges = false
+[JobSpecReporter]
+Enabled = false
+PollingInterval = '1h0m0s'
+EnabledOCR2PluginTypes = ['median']
+EmitNonOCR2Jobs = false
+
[Sharding]
ShardingEnabled = false
ArbiterPort = 9876
diff --git a/testdata/scripts/node/validate/invalid-ocr-p2p.txtar b/testdata/scripts/node/validate/invalid-ocr-p2p.txtar
index 0a79fc46a4e..35d46c4f2c3 100644
--- a/testdata/scripts/node/validate/invalid-ocr-p2p.txtar
+++ b/testdata/scripts/node/validate/invalid-ocr-p2p.txtar
@@ -440,6 +440,12 @@ PollingInterval = '5m0s'
IgnoreInvalidBridges = true
IgnoreJoblessBridges = false
+[JobSpecReporter]
+Enabled = false
+PollingInterval = '1h0m0s'
+EnabledOCR2PluginTypes = ['median']
+EmitNonOCR2Jobs = false
+
[Sharding]
ShardingEnabled = false
ArbiterPort = 9876
diff --git a/testdata/scripts/node/validate/invalid.txtar b/testdata/scripts/node/validate/invalid.txtar
index cf42fe31f61..9aea3e84334 100644
--- a/testdata/scripts/node/validate/invalid.txtar
+++ b/testdata/scripts/node/validate/invalid.txtar
@@ -451,6 +451,12 @@ PollingInterval = '5m0s'
IgnoreInvalidBridges = true
IgnoreJoblessBridges = false
+[JobSpecReporter]
+Enabled = false
+PollingInterval = '1h0m0s'
+EnabledOCR2PluginTypes = ['median']
+EmitNonOCR2Jobs = false
+
[Sharding]
ShardingEnabled = false
ArbiterPort = 9876
diff --git a/testdata/scripts/node/validate/valid.txtar b/testdata/scripts/node/validate/valid.txtar
index 4195e48f423..2280c426f2d 100644
--- a/testdata/scripts/node/validate/valid.txtar
+++ b/testdata/scripts/node/validate/valid.txtar
@@ -452,6 +452,12 @@ PollingInterval = '5m0s'
IgnoreInvalidBridges = true
IgnoreJoblessBridges = false
+[JobSpecReporter]
+Enabled = false
+PollingInterval = '1h0m0s'
+EnabledOCR2PluginTypes = ['median']
+EmitNonOCR2Jobs = false
+
[Sharding]
ShardingEnabled = false
ArbiterPort = 9876
diff --git a/testdata/scripts/node/validate/warnings.txtar b/testdata/scripts/node/validate/warnings.txtar
index 65d3a0a5332..f6f5f72d818 100644
--- a/testdata/scripts/node/validate/warnings.txtar
+++ b/testdata/scripts/node/validate/warnings.txtar
@@ -434,6 +434,12 @@ PollingInterval = '5m0s'
IgnoreInvalidBridges = true
IgnoreJoblessBridges = false
+[JobSpecReporter]
+Enabled = false
+PollingInterval = '1h0m0s'
+EnabledOCR2PluginTypes = ['median']
+EmitNonOCR2Jobs = false
+
[Sharding]
ShardingEnabled = false
ArbiterPort = 9876
From 2c23b54554f6b6ad1b39f4ebb71855742d3a70f8 Mon Sep 17 00:00:00 2001
From: thomjg <103059015+thomjg@users.noreply.github.com>
Date: Tue, 21 Apr 2026 21:29:12 -0400
Subject: [PATCH 07/14] Regenerate docs/CONFIG.md for JobSpecReporter
---
docs/CONFIG.md | 34 ++++++++++++++++++++++++++++++++++
1 file changed, 34 insertions(+)
diff --git a/docs/CONFIG.md b/docs/CONFIG.md
index ec89861fd80..df727f9c822 100644
--- a/docs/CONFIG.md
+++ b/docs/CONFIG.md
@@ -2666,6 +2666,40 @@ IgnoreJoblessBridges = false # Default
```
IgnoreJoblessBridges skips bridges that have no associated jobs.
+## JobSpecReporter
+```toml
+[JobSpecReporter]
+Enabled = false # Default
+PollingInterval = "1h" # Default
+EnabledOCR2PluginTypes = ["median"] # Default
+EmitNonOCR2Jobs = false # Default
+```
+JobSpecReporter holds settings for the Job Spec Reporter service, which periodically emits job spec telemetry.
+
+### Enabled
+```toml
+Enabled = false # Default
+```
+Enabled enables the Job Spec Reporter service.
+
+### PollingInterval
+```toml
+PollingInterval = "1h" # Default
+```
+PollingInterval is how often to emit a heartbeat event for each tracked job.
+
+### EnabledOCR2PluginTypes
+```toml
+EnabledOCR2PluginTypes = ["median"] # Default
+```
+EnabledOCR2PluginTypes restricts OCR2 telemetry to jobs with these plugin types.
+
+### EmitNonOCR2Jobs
+```toml
+EmitNonOCR2Jobs = false # Default
+```
+EmitNonOCR2Jobs emits telemetry for non-OCR2 job types (OCR1, Flux Monitor, Keeper).
+
## CRE
```toml
[CRE]
From b5a48cd25bcc5d4e673d58d1ffef82d834108eed Mon Sep 17 00:00:00 2001
From: thomjg <103059015+thomjg@users.noreply.github.com>
Date: Sun, 26 Apr 2026 16:45:19 -0400
Subject: [PATCH 08/14] Adding OCR1 job spec telemetry fields
---
.../jobspec/events/job_spec.pb.go | 250 ++++++++++++++++--
.../jobspec/events/job_spec.proto | 25 ++
.../jobspec/job_spec_reporter.go | 54 ++++
.../jobspec/job_spec_reporter_test.go | 79 +++++-
4 files changed, 387 insertions(+), 21 deletions(-)
diff --git a/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go b/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go
index 7c467ac649c..a566d729157 100644
--- a/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go
+++ b/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go
@@ -113,8 +113,10 @@ type JobSpecEvent struct {
// (OCR1, OCR2, Flux Monitor, Keeper). For OCR2, copied from ocr2_oracle_spec.
ContractAddress string `protobuf:"bytes,26,opt,name=contract_address,json=contractAddress,proto3" json:"contract_address,omitempty"`
ChainId string `protobuf:"bytes,27,opt,name=chain_id,json=chainId,proto3" json:"chain_id,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
+ // OCR1-only; absent for other job types.
+ Ocr1OracleSpec *OCR1OracleSpecInfo `protobuf:"bytes,28,opt,name=ocr1_oracle_spec,json=ocr1OracleSpec,proto3" json:"ocr1_oracle_spec,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
}
func (x *JobSpecEvent) Reset() {
@@ -336,6 +338,13 @@ func (x *JobSpecEvent) GetChainId() string {
return ""
}
+func (x *JobSpecEvent) GetOcr1OracleSpec() *OCR1OracleSpecInfo {
+ if x != nil {
+ return x.Ocr1OracleSpec
+ }
+ return nil
+}
+
// OCR2OracleSpecInfo mirrors job.OCR2OracleSpec.
type OCR2OracleSpecInfo struct {
state protoimpl.MessageState `protogen:"open.v1"`
@@ -670,6 +679,187 @@ func (x *OCR2EVMRelayConfig) GetProviderType() string {
return ""
}
+// OCR1OracleSpecInfo mirrors job.OCROracleSpec.
+type OCR1OracleSpecInfo struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ SpecId int32 `protobuf:"varint,1,opt,name=spec_id,json=specId,proto3" json:"spec_id,omitempty"`
+ ContractAddress string `protobuf:"bytes,2,opt,name=contract_address,json=contractAddress,proto3" json:"contract_address,omitempty"`
+ EvmChainId string `protobuf:"bytes,3,opt,name=evm_chain_id,json=evmChainId,proto3" json:"evm_chain_id,omitempty"`
+ P2Pv2Bootstrappers []string `protobuf:"bytes,4,rep,name=p2pv2_bootstrappers,json=p2pv2Bootstrappers,proto3" json:"p2pv2_bootstrappers,omitempty"`
+ IsBootstrapPeer bool `protobuf:"varint,5,opt,name=is_bootstrap_peer,json=isBootstrapPeer,proto3" json:"is_bootstrap_peer,omitempty"`
+ EncryptedOcrKeyBundleId string `protobuf:"bytes,6,opt,name=encrypted_ocr_key_bundle_id,json=encryptedOcrKeyBundleId,proto3" json:"encrypted_ocr_key_bundle_id,omitempty"`
+ TransmitterAddress string `protobuf:"bytes,7,opt,name=transmitter_address,json=transmitterAddress,proto3" json:"transmitter_address,omitempty"`
+ ObservationTimeoutSeconds float64 `protobuf:"fixed64,8,opt,name=observation_timeout_seconds,json=observationTimeoutSeconds,proto3" json:"observation_timeout_seconds,omitempty"`
+ BlockchainTimeoutSeconds float64 `protobuf:"fixed64,9,opt,name=blockchain_timeout_seconds,json=blockchainTimeoutSeconds,proto3" json:"blockchain_timeout_seconds,omitempty"`
+ ContractConfigTrackerSubscribeIntervalSeconds float64 `protobuf:"fixed64,10,opt,name=contract_config_tracker_subscribe_interval_seconds,json=contractConfigTrackerSubscribeIntervalSeconds,proto3" json:"contract_config_tracker_subscribe_interval_seconds,omitempty"`
+ ContractConfigTrackerPollIntervalSeconds float64 `protobuf:"fixed64,11,opt,name=contract_config_tracker_poll_interval_seconds,json=contractConfigTrackerPollIntervalSeconds,proto3" json:"contract_config_tracker_poll_interval_seconds,omitempty"`
+ ContractConfigConfirmations uint32 `protobuf:"varint,12,opt,name=contract_config_confirmations,json=contractConfigConfirmations,proto3" json:"contract_config_confirmations,omitempty"`
+ DatabaseTimeoutSeconds float64 `protobuf:"fixed64,13,opt,name=database_timeout_seconds,json=databaseTimeoutSeconds,proto3" json:"database_timeout_seconds,omitempty"`
+ ObservationGracePeriodSeconds float64 `protobuf:"fixed64,14,opt,name=observation_grace_period_seconds,json=observationGracePeriodSeconds,proto3" json:"observation_grace_period_seconds,omitempty"`
+ ContractTransmitterTransmitTimeoutSeconds float64 `protobuf:"fixed64,15,opt,name=contract_transmitter_transmit_timeout_seconds,json=contractTransmitterTransmitTimeoutSeconds,proto3" json:"contract_transmitter_transmit_timeout_seconds,omitempty"`
+ CaptureEaTelemetry bool `protobuf:"varint,16,opt,name=capture_ea_telemetry,json=captureEaTelemetry,proto3" json:"capture_ea_telemetry,omitempty"`
+ SpecCreatedAt string `protobuf:"bytes,17,opt,name=spec_created_at,json=specCreatedAt,proto3" json:"spec_created_at,omitempty"`
+ SpecUpdatedAt string `protobuf:"bytes,18,opt,name=spec_updated_at,json=specUpdatedAt,proto3" json:"spec_updated_at,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *OCR1OracleSpecInfo) Reset() {
+ *x = OCR1OracleSpecInfo{}
+ mi := &file_job_spec_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *OCR1OracleSpecInfo) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*OCR1OracleSpecInfo) ProtoMessage() {}
+
+func (x *OCR1OracleSpecInfo) ProtoReflect() protoreflect.Message {
+ mi := &file_job_spec_proto_msgTypes[3]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use OCR1OracleSpecInfo.ProtoReflect.Descriptor instead.
+func (*OCR1OracleSpecInfo) Descriptor() ([]byte, []int) {
+ return file_job_spec_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *OCR1OracleSpecInfo) GetSpecId() int32 {
+ if x != nil {
+ return x.SpecId
+ }
+ return 0
+}
+
+func (x *OCR1OracleSpecInfo) GetContractAddress() string {
+ if x != nil {
+ return x.ContractAddress
+ }
+ return ""
+}
+
+func (x *OCR1OracleSpecInfo) GetEvmChainId() string {
+ if x != nil {
+ return x.EvmChainId
+ }
+ return ""
+}
+
+func (x *OCR1OracleSpecInfo) GetP2Pv2Bootstrappers() []string {
+ if x != nil {
+ return x.P2Pv2Bootstrappers
+ }
+ return nil
+}
+
+func (x *OCR1OracleSpecInfo) GetIsBootstrapPeer() bool {
+ if x != nil {
+ return x.IsBootstrapPeer
+ }
+ return false
+}
+
+func (x *OCR1OracleSpecInfo) GetEncryptedOcrKeyBundleId() string {
+ if x != nil {
+ return x.EncryptedOcrKeyBundleId
+ }
+ return ""
+}
+
+func (x *OCR1OracleSpecInfo) GetTransmitterAddress() string {
+ if x != nil {
+ return x.TransmitterAddress
+ }
+ return ""
+}
+
+func (x *OCR1OracleSpecInfo) GetObservationTimeoutSeconds() float64 {
+ if x != nil {
+ return x.ObservationTimeoutSeconds
+ }
+ return 0
+}
+
+func (x *OCR1OracleSpecInfo) GetBlockchainTimeoutSeconds() float64 {
+ if x != nil {
+ return x.BlockchainTimeoutSeconds
+ }
+ return 0
+}
+
+func (x *OCR1OracleSpecInfo) GetContractConfigTrackerSubscribeIntervalSeconds() float64 {
+ if x != nil {
+ return x.ContractConfigTrackerSubscribeIntervalSeconds
+ }
+ return 0
+}
+
+func (x *OCR1OracleSpecInfo) GetContractConfigTrackerPollIntervalSeconds() float64 {
+ if x != nil {
+ return x.ContractConfigTrackerPollIntervalSeconds
+ }
+ return 0
+}
+
+func (x *OCR1OracleSpecInfo) GetContractConfigConfirmations() uint32 {
+ if x != nil {
+ return x.ContractConfigConfirmations
+ }
+ return 0
+}
+
+func (x *OCR1OracleSpecInfo) GetDatabaseTimeoutSeconds() float64 {
+ if x != nil {
+ return x.DatabaseTimeoutSeconds
+ }
+ return 0
+}
+
+func (x *OCR1OracleSpecInfo) GetObservationGracePeriodSeconds() float64 {
+ if x != nil {
+ return x.ObservationGracePeriodSeconds
+ }
+ return 0
+}
+
+func (x *OCR1OracleSpecInfo) GetContractTransmitterTransmitTimeoutSeconds() float64 {
+ if x != nil {
+ return x.ContractTransmitterTransmitTimeoutSeconds
+ }
+ return 0
+}
+
+func (x *OCR1OracleSpecInfo) GetCaptureEaTelemetry() bool {
+ if x != nil {
+ return x.CaptureEaTelemetry
+ }
+ return false
+}
+
+func (x *OCR1OracleSpecInfo) GetSpecCreatedAt() string {
+ if x != nil {
+ return x.SpecCreatedAt
+ }
+ return ""
+}
+
+func (x *OCR1OracleSpecInfo) GetSpecUpdatedAt() string {
+ if x != nil {
+ return x.SpecUpdatedAt
+ }
+ return ""
+}
+
// OCR2MedianPluginConfig mirrors median/config.PluginConfig.
type OCR2MedianPluginConfig struct {
state protoimpl.MessageState `protogen:"open.v1"`
@@ -688,7 +878,7 @@ type OCR2MedianPluginConfig struct {
func (x *OCR2MedianPluginConfig) Reset() {
*x = OCR2MedianPluginConfig{}
- mi := &file_job_spec_proto_msgTypes[3]
+ mi := &file_job_spec_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -700,7 +890,7 @@ func (x *OCR2MedianPluginConfig) String() string {
func (*OCR2MedianPluginConfig) ProtoMessage() {}
func (x *OCR2MedianPluginConfig) ProtoReflect() protoreflect.Message {
- mi := &file_job_spec_proto_msgTypes[3]
+ mi := &file_job_spec_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -713,7 +903,7 @@ func (x *OCR2MedianPluginConfig) ProtoReflect() protoreflect.Message {
// Deprecated: Use OCR2MedianPluginConfig.ProtoReflect.Descriptor instead.
func (*OCR2MedianPluginConfig) Descriptor() ([]byte, []int) {
- return file_job_spec_proto_rawDescGZIP(), []int{3}
+ return file_job_spec_proto_rawDescGZIP(), []int{4}
}
func (x *OCR2MedianPluginConfig) GetJuelsPerFeeCoinSource() string {
@@ -762,7 +952,7 @@ var File_job_spec_proto protoreflect.FileDescriptor
const file_job_spec_proto_rawDesc = "" +
"\n" +
- "\x0ejob_spec.proto\x12\vjob_spec.v1\"\xc9\b\n" +
+ "\x0ejob_spec.proto\x12\vjob_spec.v1\"\x94\t\n" +
"\fJobSpecEvent\x12&\n" +
"\x0fexternal_job_id\x18\x01 \x01(\tR\rexternalJobId\x12&\n" +
"\x0finternal_job_id\x18\x02 \x01(\x05R\rinternalJobId\x12\x12\n" +
@@ -795,7 +985,8 @@ const file_job_spec_proto_rawDesc = "" +
"\x10emission_trigger\x18\x18 \x01(\x0e2\x1c.job_spec.v1.EmissionTriggerR\x0femissionTrigger\x12\x1c\n" +
"\ttimestamp\x18\x19 \x01(\tR\ttimestamp\x12)\n" +
"\x10contract_address\x18\x1a \x01(\tR\x0fcontractAddress\x12\x19\n" +
- "\bchain_id\x18\x1b \x01(\tR\achainIdB\f\n" +
+ "\bchain_id\x18\x1b \x01(\tR\achainId\x12I\n" +
+ "\x10ocr1_oracle_spec\x18\x1c \x01(\v2\x1f.job_spec.v1.OCR1OracleSpecInfoR\x0eocr1OracleSpecB\f\n" +
"\n" +
"_stream_id\"\x96\t\n" +
"\x12OCR2OracleSpecInfo\x12\x17\n" +
@@ -836,7 +1027,28 @@ const file_job_spec_proto_rawDesc = "" +
"llo_don_id\x18\x06 \x01(\x04R\blloDonId\x12\x17\n" +
"\afeed_id\x18\a \x01(\tR\x06feedId\x12!\n" +
"\fsending_keys\x18\b \x03(\tR\vsendingKeys\x12#\n" +
- "\rprovider_type\x18\t \x01(\tR\fproviderType\"\xe3\x03\n" +
+ "\rprovider_type\x18\t \x01(\tR\fproviderType\"\xbb\b\n" +
+ "\x12OCR1OracleSpecInfo\x12\x17\n" +
+ "\aspec_id\x18\x01 \x01(\x05R\x06specId\x12)\n" +
+ "\x10contract_address\x18\x02 \x01(\tR\x0fcontractAddress\x12 \n" +
+ "\fevm_chain_id\x18\x03 \x01(\tR\n" +
+ "evmChainId\x12/\n" +
+ "\x13p2pv2_bootstrappers\x18\x04 \x03(\tR\x12p2pv2Bootstrappers\x12*\n" +
+ "\x11is_bootstrap_peer\x18\x05 \x01(\bR\x0fisBootstrapPeer\x12<\n" +
+ "\x1bencrypted_ocr_key_bundle_id\x18\x06 \x01(\tR\x17encryptedOcrKeyBundleId\x12/\n" +
+ "\x13transmitter_address\x18\a \x01(\tR\x12transmitterAddress\x12>\n" +
+ "\x1bobservation_timeout_seconds\x18\b \x01(\x01R\x19observationTimeoutSeconds\x12<\n" +
+ "\x1ablockchain_timeout_seconds\x18\t \x01(\x01R\x18blockchainTimeoutSeconds\x12i\n" +
+ "2contract_config_tracker_subscribe_interval_seconds\x18\n" +
+ " \x01(\x01R-contractConfigTrackerSubscribeIntervalSeconds\x12_\n" +
+ "-contract_config_tracker_poll_interval_seconds\x18\v \x01(\x01R(contractConfigTrackerPollIntervalSeconds\x12B\n" +
+ "\x1dcontract_config_confirmations\x18\f \x01(\rR\x1bcontractConfigConfirmations\x128\n" +
+ "\x18database_timeout_seconds\x18\r \x01(\x01R\x16databaseTimeoutSeconds\x12G\n" +
+ " observation_grace_period_seconds\x18\x0e \x01(\x01R\x1dobservationGracePeriodSeconds\x12`\n" +
+ "-contract_transmitter_transmit_timeout_seconds\x18\x0f \x01(\x01R)contractTransmitterTransmitTimeoutSeconds\x120\n" +
+ "\x14capture_ea_telemetry\x18\x10 \x01(\bR\x12captureEaTelemetry\x12&\n" +
+ "\x0fspec_created_at\x18\x11 \x01(\tR\rspecCreatedAt\x12&\n" +
+ "\x0fspec_updated_at\x18\x12 \x01(\tR\rspecUpdatedAt\"\xe3\x03\n" +
"\x16OCR2MedianPluginConfig\x128\n" +
"\x19juels_per_fee_coin_source\x18\x01 \x01(\tR\x15juelsPerFeeCoinSource\x129\n" +
"\x19gas_price_subunits_source\x18\x02 \x01(\tR\x16gasPriceSubunitsSource\x12G\n" +
@@ -863,24 +1075,26 @@ func file_job_spec_proto_rawDescGZIP() []byte {
}
var file_job_spec_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
-var file_job_spec_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
+var file_job_spec_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
var file_job_spec_proto_goTypes = []any{
(EmissionTrigger)(0), // 0: job_spec.v1.EmissionTrigger
(*JobSpecEvent)(nil), // 1: job_spec.v1.JobSpecEvent
(*OCR2OracleSpecInfo)(nil), // 2: job_spec.v1.OCR2OracleSpecInfo
(*OCR2EVMRelayConfig)(nil), // 3: job_spec.v1.OCR2EVMRelayConfig
- (*OCR2MedianPluginConfig)(nil), // 4: job_spec.v1.OCR2MedianPluginConfig
+ (*OCR1OracleSpecInfo)(nil), // 4: job_spec.v1.OCR1OracleSpecInfo
+ (*OCR2MedianPluginConfig)(nil), // 5: job_spec.v1.OCR2MedianPluginConfig
}
var file_job_spec_proto_depIdxs = []int32{
2, // 0: job_spec.v1.JobSpecEvent.ocr2_oracle_spec:type_name -> job_spec.v1.OCR2OracleSpecInfo
0, // 1: job_spec.v1.JobSpecEvent.emission_trigger:type_name -> job_spec.v1.EmissionTrigger
- 3, // 2: job_spec.v1.OCR2OracleSpecInfo.evm_relay_config:type_name -> job_spec.v1.OCR2EVMRelayConfig
- 4, // 3: job_spec.v1.OCR2OracleSpecInfo.median_plugin_config:type_name -> job_spec.v1.OCR2MedianPluginConfig
- 4, // [4:4] is the sub-list for method output_type
- 4, // [4:4] is the sub-list for method input_type
- 4, // [4:4] is the sub-list for extension type_name
- 4, // [4:4] is the sub-list for extension extendee
- 0, // [0:4] is the sub-list for field type_name
+ 4, // 2: job_spec.v1.JobSpecEvent.ocr1_oracle_spec:type_name -> job_spec.v1.OCR1OracleSpecInfo
+ 3, // 3: job_spec.v1.OCR2OracleSpecInfo.evm_relay_config:type_name -> job_spec.v1.OCR2EVMRelayConfig
+ 5, // 4: job_spec.v1.OCR2OracleSpecInfo.median_plugin_config:type_name -> job_spec.v1.OCR2MedianPluginConfig
+ 5, // [5:5] is the sub-list for method output_type
+ 5, // [5:5] is the sub-list for method input_type
+ 5, // [5:5] is the sub-list for extension type_name
+ 5, // [5:5] is the sub-list for extension extendee
+ 0, // [0:5] is the sub-list for field type_name
}
func init() { file_job_spec_proto_init() }
@@ -895,7 +1109,7 @@ func file_job_spec_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_job_spec_proto_rawDesc), len(file_job_spec_proto_rawDesc)),
NumEnums: 1,
- NumMessages: 4,
+ NumMessages: 5,
NumExtensions: 0,
NumServices: 0,
},
diff --git a/core/services/nodestatusreporter/jobspec/events/job_spec.proto b/core/services/nodestatusreporter/jobspec/events/job_spec.proto
index 92e57dfdc95..9000642d7cf 100644
--- a/core/services/nodestatusreporter/jobspec/events/job_spec.proto
+++ b/core/services/nodestatusreporter/jobspec/events/job_spec.proto
@@ -49,6 +49,9 @@ message JobSpecEvent {
// (OCR1, OCR2, Flux Monitor, Keeper). For OCR2, copied from ocr2_oracle_spec.
string contract_address = 26;
string chain_id = 27;
+
+ // OCR1-only; absent for other job types.
+ OCR1OracleSpecInfo ocr1_oracle_spec = 28;
}
// EmissionTrigger is the reason a JobSpecEvent was emitted.
@@ -106,6 +109,28 @@ message OCR2EVMRelayConfig {
string provider_type = 9;
}
+// OCR1OracleSpecInfo mirrors job.OCROracleSpec.
+message OCR1OracleSpecInfo {
+ int32 spec_id = 1;
+ string contract_address = 2;
+ string evm_chain_id = 3;
+ repeated string p2pv2_bootstrappers = 4;
+ bool is_bootstrap_peer = 5;
+ string encrypted_ocr_key_bundle_id = 6;
+ string transmitter_address = 7;
+ double observation_timeout_seconds = 8;
+ double blockchain_timeout_seconds = 9;
+ double contract_config_tracker_subscribe_interval_seconds = 10;
+ double contract_config_tracker_poll_interval_seconds = 11;
+ uint32 contract_config_confirmations = 12;
+ double database_timeout_seconds = 13;
+ double observation_grace_period_seconds = 14;
+ double contract_transmitter_transmit_timeout_seconds = 15;
+ bool capture_ea_telemetry = 16;
+ string spec_created_at = 17;
+ string spec_updated_at = 18;
+}
+
// OCR2MedianPluginConfig mirrors median/config.PluginConfig.
message OCR2MedianPluginConfig {
string juels_per_fee_coin_source = 1;
diff --git a/core/services/nodestatusreporter/jobspec/job_spec_reporter.go b/core/services/nodestatusreporter/jobspec/job_spec_reporter.go
index 28f381c61d2..bcbfe138322 100644
--- a/core/services/nodestatusreporter/jobspec/job_spec_reporter.go
+++ b/core/services/nodestatusreporter/jobspec/job_spec_reporter.go
@@ -203,6 +203,7 @@ func (s *Service) buildEvent(ctx context.Context, jb job.Job, trigger events.Emi
if jb.OCROracleSpec.EVMChainID != nil {
event.ChainId = jb.OCROracleSpec.EVMChainID.String()
}
+ event.Ocr1OracleSpec = buildOCR1OracleSpecInfo(jb.OCROracleSpec)
}
return event, nil
@@ -334,6 +335,59 @@ func buildOCR2OracleSpecInfo(spec *job.OCR2OracleSpec) (*events.OCR2OracleSpecIn
return info, nil
}
+func buildOCR1OracleSpecInfo(spec *job.OCROracleSpec) *events.OCR1OracleSpecInfo {
+ evmChainID := ""
+ if spec.EVMChainID != nil {
+ evmChainID = spec.EVMChainID.String()
+ }
+
+ keyBundleID := ""
+ if spec.EncryptedOCRKeyBundleID != nil {
+ keyBundleID = spec.EncryptedOCRKeyBundleID.String()
+ }
+
+ transmitterAddress := ""
+ if spec.TransmitterAddress != nil {
+ transmitterAddress = spec.TransmitterAddress.String()
+ }
+
+ var dbTimeoutSeconds float64
+ if spec.DatabaseTimeout != nil {
+ dbTimeoutSeconds = spec.DatabaseTimeout.Duration().Seconds()
+ }
+
+ var gracePeriodSeconds float64
+ if spec.ObservationGracePeriod != nil {
+ gracePeriodSeconds = spec.ObservationGracePeriod.Duration().Seconds()
+ }
+
+ var transmitTimeoutSeconds float64
+ if spec.ContractTransmitterTransmitTimeout != nil {
+ transmitTimeoutSeconds = spec.ContractTransmitterTransmitTimeout.Duration().Seconds()
+ }
+
+ return &events.OCR1OracleSpecInfo{
+ SpecId: spec.ID,
+ ContractAddress: spec.ContractAddress.String(),
+ EvmChainId: evmChainID,
+ P2Pv2Bootstrappers: spec.P2PV2Bootstrappers,
+ IsBootstrapPeer: spec.IsBootstrapPeer,
+ EncryptedOcrKeyBundleId: keyBundleID,
+ TransmitterAddress: transmitterAddress,
+ ObservationTimeoutSeconds: spec.ObservationTimeout.Duration().Seconds(),
+ BlockchainTimeoutSeconds: spec.BlockchainTimeout.Duration().Seconds(),
+ ContractConfigTrackerSubscribeIntervalSeconds: spec.ContractConfigTrackerSubscribeInterval.Duration().Seconds(),
+ ContractConfigTrackerPollIntervalSeconds: spec.ContractConfigTrackerPollInterval.Duration().Seconds(),
+ ContractConfigConfirmations: uint32(spec.ContractConfigConfirmations),
+ DatabaseTimeoutSeconds: dbTimeoutSeconds,
+ ObservationGracePeriodSeconds: gracePeriodSeconds,
+ ContractTransmitterTransmitTimeoutSeconds: transmitTimeoutSeconds,
+ CaptureEaTelemetry: spec.CaptureEATelemetry,
+ SpecCreatedAt: spec.CreatedAt.Format(time.RFC3339Nano),
+ SpecUpdatedAt: spec.UpdatedAt.Format(time.RFC3339Nano),
+ }
+}
+
// buildEVMRelayConfig decodes the EVM relay config JSON into OCR2EVMRelayConfig.
func buildEVMRelayConfig(relayConfigJSON []byte) (*events.OCR2EVMRelayConfig, error) {
var cfg evmRelayConfig
diff --git a/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go b/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go
index 0988f751496..65195797819 100644
--- a/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go
+++ b/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go
@@ -13,6 +13,8 @@ import (
"google.golang.org/protobuf/proto"
"gopkg.in/guregu/null.v4"
+ "github.com/lib/pq"
+ "github.com/smartcontractkit/chainlink-common/keystore/corekeys"
"github.com/smartcontractkit/chainlink-common/pkg/beholder"
"github.com/smartcontractkit/chainlink-common/pkg/beholder/beholdertest"
"github.com/smartcontractkit/chainlink-common/pkg/sqlutil"
@@ -350,7 +352,7 @@ func TestBuildEvent_ContractFields_OCR1(t *testing.T) {
cfg := &stubConfig{
enabled: true,
pollingInterval: time.Hour,
- emitNonOCR2Jobs: true, // OCR1 is non-OCR2
+ emitNonOCR2Jobs: true,
}
svc := newTestReporter(t, cfg, nil)
@@ -364,12 +366,67 @@ func TestBuildEvent_ContractFields_OCR1(t *testing.T) {
var ev events.JobSpecEvent
require.NoError(t, proto.Unmarshal(msgs[0].Body, &ev))
+ // top-level contract identity
assert.Equal(t, "0x9d9305445F404E925563d5D5EcC65C815Ec1655b", ev.ContractAddress)
assert.Equal(t, "11155111", ev.ChainId)
assert.Equal(t, "offchainreporting", ev.JobType)
+
+ // OCR1 sub-message
+ require.NotNil(t, ev.Ocr1OracleSpec)
+ ocr1 := ev.Ocr1OracleSpec
+ assert.Equal(t, int32(99), ocr1.SpecId)
+ assert.Equal(t, "0x9d9305445F404E925563d5D5EcC65C815Ec1655b", ocr1.ContractAddress)
+ assert.Equal(t, "11155111", ocr1.EvmChainId)
+ assert.Equal(t, []string{"12D3KooW@bootstrap:6688"}, ocr1.P2Pv2Bootstrappers)
+ assert.False(t, ocr1.IsBootstrapPeer)
+ assert.Equal(t, "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", ocr1.EncryptedOcrKeyBundleId)
+ assert.Equal(t, "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B", ocr1.TransmitterAddress)
+ assert.InDelta(t, 30.0, ocr1.ObservationTimeoutSeconds, 0.001)
+ assert.InDelta(t, 20.0, ocr1.BlockchainTimeoutSeconds, 0.001)
+ assert.InDelta(t, 120.0, ocr1.ContractConfigTrackerSubscribeIntervalSeconds, 0.001)
+ assert.InDelta(t, 60.0, ocr1.ContractConfigTrackerPollIntervalSeconds, 0.001)
+ assert.Equal(t, uint32(3), ocr1.ContractConfigConfirmations)
+ assert.InDelta(t, 10.0, ocr1.DatabaseTimeoutSeconds, 0.001)
+ assert.InDelta(t, 1.0, ocr1.ObservationGracePeriodSeconds, 0.001)
+ assert.InDelta(t, 5.0, ocr1.ContractTransmitterTransmitTimeoutSeconds, 0.001)
+ assert.True(t, ocr1.CaptureEaTelemetry)
+ assert.Equal(t, "2026-01-01T00:00:00Z", ocr1.SpecCreatedAt)
+ assert.Equal(t, "2026-02-01T00:00:00Z", ocr1.SpecUpdatedAt)
+
+ // OCR2 sub-message absent for OCR1 jobs
+ assert.Nil(t, ev.Ocr2OracleSpec)
+}
+
+func TestBuildEvent_OCR2Job_HasNoOCR1Spec(t *testing.T) {
+ observer := beholdertest.NewObserver(t)
+ jb := makeMedianJob()
+ feedsORM := newFeedsORMWithoutProposal(t, jb)
+ svc := newTestReporter(t, defaultConfig(), feedsORM)
+
+ err := svc.EmitForJob(context.Background(), jb, events.EmissionTrigger_EMISSION_TRIGGER_HEARTBEAT)
+ require.NoError(t, err)
+
+ msgs := observer.Messages(t, "beholder_entity", events.ProtoPkg+"."+events.JobSpecEventEntity)
+ require.Len(t, msgs, 1)
+
+ var ev events.JobSpecEvent
+ require.NoError(t, proto.Unmarshal(msgs[0].Body, &ev))
+
+ assert.Nil(t, ev.Ocr1OracleSpec)
+ assert.NotNil(t, ev.Ocr2OracleSpec)
}
func makeOCR1Job() job.Job {
+ keyHash, err := corekeys.Sha256HashFromHex("0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20")
+ if err != nil {
+ panic(err)
+ }
+ transmitter := evmtypes.MustEIP55Address("0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B")
+ specCreatedAt := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
+ specUpdatedAt := time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC)
+ dbTimeout := sqlutil.Interval(10 * time.Second)
+ gracePeriod := sqlutil.Interval(1 * time.Second)
+ transmitTimeout := sqlutil.Interval(5 * time.Second)
return job.Job{
ID: 4,
ExternalJobID: uuid.New(),
@@ -386,8 +443,24 @@ func makeOCR1Job() job.Job {
},
},
OCROracleSpec: &job.OCROracleSpec{
- ContractAddress: evmtypes.MustEIP55Address("0x9d9305445F404E925563d5D5EcC65C815Ec1655b"),
- EVMChainID: sqlutil.NewI(11155111),
+ ID: 99,
+ ContractAddress: evmtypes.MustEIP55Address("0x9d9305445F404E925563d5D5EcC65C815Ec1655b"),
+ EVMChainID: sqlutil.NewI(11155111),
+ P2PV2Bootstrappers: pq.StringArray{"12D3KooW@bootstrap:6688"},
+ IsBootstrapPeer: false,
+ EncryptedOCRKeyBundleID: &keyHash,
+ TransmitterAddress: &transmitter,
+ ObservationTimeout: sqlutil.Interval(30 * time.Second),
+ BlockchainTimeout: sqlutil.Interval(20 * time.Second),
+ ContractConfigTrackerSubscribeInterval: sqlutil.Interval(2 * time.Minute),
+ ContractConfigTrackerPollInterval: sqlutil.Interval(1 * time.Minute),
+ ContractConfigConfirmations: 3,
+ DatabaseTimeout: &dbTimeout,
+ ObservationGracePeriod: &gracePeriod,
+ ContractTransmitterTransmitTimeout: &transmitTimeout,
+ CaptureEATelemetry: true,
+ CreatedAt: specCreatedAt,
+ UpdatedAt: specUpdatedAt,
},
CreatedAt: time.Now(),
}
From 3ce2277a231c4df2e958d93ac2b98c11ca2c8b31 Mon Sep 17 00:00:00 2001
From: thomjg <103059015+thomjg@users.noreply.github.com>
Date: Sun, 26 Apr 2026 16:56:25 -0400
Subject: [PATCH 09/14] Removing redundant proto fields and updating comments
---
.../nodestatusreporter/jobspec/events/emit.go | 1 -
.../jobspec/events/job_spec.pb.go | 86 ++++++++-----------
.../jobspec/events/job_spec.proto | 33 ++++---
.../jobspec/events/types.go | 8 +-
.../jobspec/job_spec_reporter.go | 39 ++++-----
.../jobspec/job_spec_reporter_test.go | 22 +----
6 files changed, 70 insertions(+), 119 deletions(-)
diff --git a/core/services/nodestatusreporter/jobspec/events/emit.go b/core/services/nodestatusreporter/jobspec/events/emit.go
index e28d05fcb29..e567983a0f3 100644
--- a/core/services/nodestatusreporter/jobspec/events/emit.go
+++ b/core/services/nodestatusreporter/jobspec/events/emit.go
@@ -10,7 +10,6 @@ import (
"github.com/smartcontractkit/chainlink-common/pkg/beholder"
)
-// EmitJobSpecEvent emits a Job Spec event through the provided emitter.
func EmitJobSpecEvent(ctx context.Context, emitter beholder.Emitter, event *JobSpecEvent) error {
if event.Timestamp == "" {
event.Timestamp = time.Now().Format(time.RFC3339Nano)
diff --git a/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go b/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go
index a566d729157..cd3675a8a01 100644
--- a/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go
+++ b/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go
@@ -680,26 +680,25 @@ func (x *OCR2EVMRelayConfig) GetProviderType() string {
}
// OCR1OracleSpecInfo mirrors job.OCROracleSpec.
+// contract_address and evm_chain_id live on the parent JobSpecEvent.
type OCR1OracleSpecInfo struct {
state protoimpl.MessageState `protogen:"open.v1"`
SpecId int32 `protobuf:"varint,1,opt,name=spec_id,json=specId,proto3" json:"spec_id,omitempty"`
- ContractAddress string `protobuf:"bytes,2,opt,name=contract_address,json=contractAddress,proto3" json:"contract_address,omitempty"`
- EvmChainId string `protobuf:"bytes,3,opt,name=evm_chain_id,json=evmChainId,proto3" json:"evm_chain_id,omitempty"`
- P2Pv2Bootstrappers []string `protobuf:"bytes,4,rep,name=p2pv2_bootstrappers,json=p2pv2Bootstrappers,proto3" json:"p2pv2_bootstrappers,omitempty"`
- IsBootstrapPeer bool `protobuf:"varint,5,opt,name=is_bootstrap_peer,json=isBootstrapPeer,proto3" json:"is_bootstrap_peer,omitempty"`
- EncryptedOcrKeyBundleId string `protobuf:"bytes,6,opt,name=encrypted_ocr_key_bundle_id,json=encryptedOcrKeyBundleId,proto3" json:"encrypted_ocr_key_bundle_id,omitempty"`
- TransmitterAddress string `protobuf:"bytes,7,opt,name=transmitter_address,json=transmitterAddress,proto3" json:"transmitter_address,omitempty"`
- ObservationTimeoutSeconds float64 `protobuf:"fixed64,8,opt,name=observation_timeout_seconds,json=observationTimeoutSeconds,proto3" json:"observation_timeout_seconds,omitempty"`
- BlockchainTimeoutSeconds float64 `protobuf:"fixed64,9,opt,name=blockchain_timeout_seconds,json=blockchainTimeoutSeconds,proto3" json:"blockchain_timeout_seconds,omitempty"`
- ContractConfigTrackerSubscribeIntervalSeconds float64 `protobuf:"fixed64,10,opt,name=contract_config_tracker_subscribe_interval_seconds,json=contractConfigTrackerSubscribeIntervalSeconds,proto3" json:"contract_config_tracker_subscribe_interval_seconds,omitempty"`
- ContractConfigTrackerPollIntervalSeconds float64 `protobuf:"fixed64,11,opt,name=contract_config_tracker_poll_interval_seconds,json=contractConfigTrackerPollIntervalSeconds,proto3" json:"contract_config_tracker_poll_interval_seconds,omitempty"`
- ContractConfigConfirmations uint32 `protobuf:"varint,12,opt,name=contract_config_confirmations,json=contractConfigConfirmations,proto3" json:"contract_config_confirmations,omitempty"`
- DatabaseTimeoutSeconds float64 `protobuf:"fixed64,13,opt,name=database_timeout_seconds,json=databaseTimeoutSeconds,proto3" json:"database_timeout_seconds,omitempty"`
- ObservationGracePeriodSeconds float64 `protobuf:"fixed64,14,opt,name=observation_grace_period_seconds,json=observationGracePeriodSeconds,proto3" json:"observation_grace_period_seconds,omitempty"`
- ContractTransmitterTransmitTimeoutSeconds float64 `protobuf:"fixed64,15,opt,name=contract_transmitter_transmit_timeout_seconds,json=contractTransmitterTransmitTimeoutSeconds,proto3" json:"contract_transmitter_transmit_timeout_seconds,omitempty"`
- CaptureEaTelemetry bool `protobuf:"varint,16,opt,name=capture_ea_telemetry,json=captureEaTelemetry,proto3" json:"capture_ea_telemetry,omitempty"`
- SpecCreatedAt string `protobuf:"bytes,17,opt,name=spec_created_at,json=specCreatedAt,proto3" json:"spec_created_at,omitempty"`
- SpecUpdatedAt string `protobuf:"bytes,18,opt,name=spec_updated_at,json=specUpdatedAt,proto3" json:"spec_updated_at,omitempty"`
+ P2Pv2Bootstrappers []string `protobuf:"bytes,2,rep,name=p2pv2_bootstrappers,json=p2pv2Bootstrappers,proto3" json:"p2pv2_bootstrappers,omitempty"`
+ IsBootstrapPeer bool `protobuf:"varint,3,opt,name=is_bootstrap_peer,json=isBootstrapPeer,proto3" json:"is_bootstrap_peer,omitempty"`
+ EncryptedOcrKeyBundleId string `protobuf:"bytes,4,opt,name=encrypted_ocr_key_bundle_id,json=encryptedOcrKeyBundleId,proto3" json:"encrypted_ocr_key_bundle_id,omitempty"`
+ TransmitterAddress string `protobuf:"bytes,5,opt,name=transmitter_address,json=transmitterAddress,proto3" json:"transmitter_address,omitempty"`
+ ObservationTimeoutSeconds float64 `protobuf:"fixed64,6,opt,name=observation_timeout_seconds,json=observationTimeoutSeconds,proto3" json:"observation_timeout_seconds,omitempty"`
+ BlockchainTimeoutSeconds float64 `protobuf:"fixed64,7,opt,name=blockchain_timeout_seconds,json=blockchainTimeoutSeconds,proto3" json:"blockchain_timeout_seconds,omitempty"`
+ ContractConfigTrackerSubscribeIntervalSeconds float64 `protobuf:"fixed64,8,opt,name=contract_config_tracker_subscribe_interval_seconds,json=contractConfigTrackerSubscribeIntervalSeconds,proto3" json:"contract_config_tracker_subscribe_interval_seconds,omitempty"`
+ ContractConfigTrackerPollIntervalSeconds float64 `protobuf:"fixed64,9,opt,name=contract_config_tracker_poll_interval_seconds,json=contractConfigTrackerPollIntervalSeconds,proto3" json:"contract_config_tracker_poll_interval_seconds,omitempty"`
+ ContractConfigConfirmations uint32 `protobuf:"varint,10,opt,name=contract_config_confirmations,json=contractConfigConfirmations,proto3" json:"contract_config_confirmations,omitempty"`
+ DatabaseTimeoutSeconds float64 `protobuf:"fixed64,11,opt,name=database_timeout_seconds,json=databaseTimeoutSeconds,proto3" json:"database_timeout_seconds,omitempty"`
+ ObservationGracePeriodSeconds float64 `protobuf:"fixed64,12,opt,name=observation_grace_period_seconds,json=observationGracePeriodSeconds,proto3" json:"observation_grace_period_seconds,omitempty"`
+ ContractTransmitterTransmitTimeoutSeconds float64 `protobuf:"fixed64,13,opt,name=contract_transmitter_transmit_timeout_seconds,json=contractTransmitterTransmitTimeoutSeconds,proto3" json:"contract_transmitter_transmit_timeout_seconds,omitempty"`
+ CaptureEaTelemetry bool `protobuf:"varint,14,opt,name=capture_ea_telemetry,json=captureEaTelemetry,proto3" json:"capture_ea_telemetry,omitempty"`
+ SpecCreatedAt string `protobuf:"bytes,15,opt,name=spec_created_at,json=specCreatedAt,proto3" json:"spec_created_at,omitempty"`
+ SpecUpdatedAt string `protobuf:"bytes,16,opt,name=spec_updated_at,json=specUpdatedAt,proto3" json:"spec_updated_at,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -741,20 +740,6 @@ func (x *OCR1OracleSpecInfo) GetSpecId() int32 {
return 0
}
-func (x *OCR1OracleSpecInfo) GetContractAddress() string {
- if x != nil {
- return x.ContractAddress
- }
- return ""
-}
-
-func (x *OCR1OracleSpecInfo) GetEvmChainId() string {
- if x != nil {
- return x.EvmChainId
- }
- return ""
-}
-
func (x *OCR1OracleSpecInfo) GetP2Pv2Bootstrappers() []string {
if x != nil {
return x.P2Pv2Bootstrappers
@@ -1027,28 +1012,25 @@ const file_job_spec_proto_rawDesc = "" +
"llo_don_id\x18\x06 \x01(\x04R\blloDonId\x12\x17\n" +
"\afeed_id\x18\a \x01(\tR\x06feedId\x12!\n" +
"\fsending_keys\x18\b \x03(\tR\vsendingKeys\x12#\n" +
- "\rprovider_type\x18\t \x01(\tR\fproviderType\"\xbb\b\n" +
+ "\rprovider_type\x18\t \x01(\tR\fproviderType\"\xee\a\n" +
"\x12OCR1OracleSpecInfo\x12\x17\n" +
- "\aspec_id\x18\x01 \x01(\x05R\x06specId\x12)\n" +
- "\x10contract_address\x18\x02 \x01(\tR\x0fcontractAddress\x12 \n" +
- "\fevm_chain_id\x18\x03 \x01(\tR\n" +
- "evmChainId\x12/\n" +
- "\x13p2pv2_bootstrappers\x18\x04 \x03(\tR\x12p2pv2Bootstrappers\x12*\n" +
- "\x11is_bootstrap_peer\x18\x05 \x01(\bR\x0fisBootstrapPeer\x12<\n" +
- "\x1bencrypted_ocr_key_bundle_id\x18\x06 \x01(\tR\x17encryptedOcrKeyBundleId\x12/\n" +
- "\x13transmitter_address\x18\a \x01(\tR\x12transmitterAddress\x12>\n" +
- "\x1bobservation_timeout_seconds\x18\b \x01(\x01R\x19observationTimeoutSeconds\x12<\n" +
- "\x1ablockchain_timeout_seconds\x18\t \x01(\x01R\x18blockchainTimeoutSeconds\x12i\n" +
- "2contract_config_tracker_subscribe_interval_seconds\x18\n" +
- " \x01(\x01R-contractConfigTrackerSubscribeIntervalSeconds\x12_\n" +
- "-contract_config_tracker_poll_interval_seconds\x18\v \x01(\x01R(contractConfigTrackerPollIntervalSeconds\x12B\n" +
- "\x1dcontract_config_confirmations\x18\f \x01(\rR\x1bcontractConfigConfirmations\x128\n" +
- "\x18database_timeout_seconds\x18\r \x01(\x01R\x16databaseTimeoutSeconds\x12G\n" +
- " observation_grace_period_seconds\x18\x0e \x01(\x01R\x1dobservationGracePeriodSeconds\x12`\n" +
- "-contract_transmitter_transmit_timeout_seconds\x18\x0f \x01(\x01R)contractTransmitterTransmitTimeoutSeconds\x120\n" +
- "\x14capture_ea_telemetry\x18\x10 \x01(\bR\x12captureEaTelemetry\x12&\n" +
- "\x0fspec_created_at\x18\x11 \x01(\tR\rspecCreatedAt\x12&\n" +
- "\x0fspec_updated_at\x18\x12 \x01(\tR\rspecUpdatedAt\"\xe3\x03\n" +
+ "\aspec_id\x18\x01 \x01(\x05R\x06specId\x12/\n" +
+ "\x13p2pv2_bootstrappers\x18\x02 \x03(\tR\x12p2pv2Bootstrappers\x12*\n" +
+ "\x11is_bootstrap_peer\x18\x03 \x01(\bR\x0fisBootstrapPeer\x12<\n" +
+ "\x1bencrypted_ocr_key_bundle_id\x18\x04 \x01(\tR\x17encryptedOcrKeyBundleId\x12/\n" +
+ "\x13transmitter_address\x18\x05 \x01(\tR\x12transmitterAddress\x12>\n" +
+ "\x1bobservation_timeout_seconds\x18\x06 \x01(\x01R\x19observationTimeoutSeconds\x12<\n" +
+ "\x1ablockchain_timeout_seconds\x18\a \x01(\x01R\x18blockchainTimeoutSeconds\x12i\n" +
+ "2contract_config_tracker_subscribe_interval_seconds\x18\b \x01(\x01R-contractConfigTrackerSubscribeIntervalSeconds\x12_\n" +
+ "-contract_config_tracker_poll_interval_seconds\x18\t \x01(\x01R(contractConfigTrackerPollIntervalSeconds\x12B\n" +
+ "\x1dcontract_config_confirmations\x18\n" +
+ " \x01(\rR\x1bcontractConfigConfirmations\x128\n" +
+ "\x18database_timeout_seconds\x18\v \x01(\x01R\x16databaseTimeoutSeconds\x12G\n" +
+ " observation_grace_period_seconds\x18\f \x01(\x01R\x1dobservationGracePeriodSeconds\x12`\n" +
+ "-contract_transmitter_transmit_timeout_seconds\x18\r \x01(\x01R)contractTransmitterTransmitTimeoutSeconds\x120\n" +
+ "\x14capture_ea_telemetry\x18\x0e \x01(\bR\x12captureEaTelemetry\x12&\n" +
+ "\x0fspec_created_at\x18\x0f \x01(\tR\rspecCreatedAt\x12&\n" +
+ "\x0fspec_updated_at\x18\x10 \x01(\tR\rspecUpdatedAt\"\xe3\x03\n" +
"\x16OCR2MedianPluginConfig\x128\n" +
"\x19juels_per_fee_coin_source\x18\x01 \x01(\tR\x15juelsPerFeeCoinSource\x129\n" +
"\x19gas_price_subunits_source\x18\x02 \x01(\tR\x16gasPriceSubunitsSource\x12G\n" +
diff --git a/core/services/nodestatusreporter/jobspec/events/job_spec.proto b/core/services/nodestatusreporter/jobspec/events/job_spec.proto
index 9000642d7cf..7b0152849ff 100644
--- a/core/services/nodestatusreporter/jobspec/events/job_spec.proto
+++ b/core/services/nodestatusreporter/jobspec/events/job_spec.proto
@@ -110,25 +110,24 @@ message OCR2EVMRelayConfig {
}
// OCR1OracleSpecInfo mirrors job.OCROracleSpec.
+// contract_address and evm_chain_id live on the parent JobSpecEvent.
message OCR1OracleSpecInfo {
int32 spec_id = 1;
- string contract_address = 2;
- string evm_chain_id = 3;
- repeated string p2pv2_bootstrappers = 4;
- bool is_bootstrap_peer = 5;
- string encrypted_ocr_key_bundle_id = 6;
- string transmitter_address = 7;
- double observation_timeout_seconds = 8;
- double blockchain_timeout_seconds = 9;
- double contract_config_tracker_subscribe_interval_seconds = 10;
- double contract_config_tracker_poll_interval_seconds = 11;
- uint32 contract_config_confirmations = 12;
- double database_timeout_seconds = 13;
- double observation_grace_period_seconds = 14;
- double contract_transmitter_transmit_timeout_seconds = 15;
- bool capture_ea_telemetry = 16;
- string spec_created_at = 17;
- string spec_updated_at = 18;
+ repeated string p2pv2_bootstrappers = 2;
+ bool is_bootstrap_peer = 3;
+ string encrypted_ocr_key_bundle_id = 4;
+ string transmitter_address = 5;
+ double observation_timeout_seconds = 6;
+ double blockchain_timeout_seconds = 7;
+ double contract_config_tracker_subscribe_interval_seconds = 8;
+ double contract_config_tracker_poll_interval_seconds = 9;
+ uint32 contract_config_confirmations = 10;
+ double database_timeout_seconds = 11;
+ double observation_grace_period_seconds = 12;
+ double contract_transmitter_transmit_timeout_seconds = 13;
+ bool capture_ea_telemetry = 14;
+ string spec_created_at = 15;
+ string spec_updated_at = 16;
}
// OCR2MedianPluginConfig mirrors median/config.PluginConfig.
diff --git a/core/services/nodestatusreporter/jobspec/events/types.go b/core/services/nodestatusreporter/jobspec/events/types.go
index ed54756d06f..4d7d7537247 100644
--- a/core/services/nodestatusreporter/jobspec/events/types.go
+++ b/core/services/nodestatusreporter/jobspec/events/types.go
@@ -1,9 +1,7 @@
package events
const (
- ProtoPkg = "job_spec.v1"
- // JobSpecEventEntity represents a Job Spec event
- JobSpecEventEntity string = "JobSpecEvent"
- // SchemaJobSpec represents the schema for Job Spec events
- SchemaJobSpec string = "/job-spec-events/v1"
+ ProtoPkg = "job_spec.v1"
+ JobSpecEventEntity = "JobSpecEvent"
+ SchemaJobSpec = "/job-spec-events/v1"
)
diff --git a/core/services/nodestatusreporter/jobspec/job_spec_reporter.go b/core/services/nodestatusreporter/jobspec/job_spec_reporter.go
index bcbfe138322..e2b0910be7a 100644
--- a/core/services/nodestatusreporter/jobspec/job_spec_reporter.go
+++ b/core/services/nodestatusreporter/jobspec/job_spec_reporter.go
@@ -336,11 +336,6 @@ func buildOCR2OracleSpecInfo(spec *job.OCR2OracleSpec) (*events.OCR2OracleSpecIn
}
func buildOCR1OracleSpecInfo(spec *job.OCROracleSpec) *events.OCR1OracleSpecInfo {
- evmChainID := ""
- if spec.EVMChainID != nil {
- evmChainID = spec.EVMChainID.String()
- }
-
keyBundleID := ""
if spec.EncryptedOCRKeyBundleID != nil {
keyBundleID = spec.EncryptedOCRKeyBundleID.String()
@@ -367,24 +362,22 @@ func buildOCR1OracleSpecInfo(spec *job.OCROracleSpec) *events.OCR1OracleSpecInfo
}
return &events.OCR1OracleSpecInfo{
- SpecId: spec.ID,
- ContractAddress: spec.ContractAddress.String(),
- EvmChainId: evmChainID,
- P2Pv2Bootstrappers: spec.P2PV2Bootstrappers,
- IsBootstrapPeer: spec.IsBootstrapPeer,
- EncryptedOcrKeyBundleId: keyBundleID,
- TransmitterAddress: transmitterAddress,
- ObservationTimeoutSeconds: spec.ObservationTimeout.Duration().Seconds(),
- BlockchainTimeoutSeconds: spec.BlockchainTimeout.Duration().Seconds(),
- ContractConfigTrackerSubscribeIntervalSeconds: spec.ContractConfigTrackerSubscribeInterval.Duration().Seconds(),
- ContractConfigTrackerPollIntervalSeconds: spec.ContractConfigTrackerPollInterval.Duration().Seconds(),
- ContractConfigConfirmations: uint32(spec.ContractConfigConfirmations),
- DatabaseTimeoutSeconds: dbTimeoutSeconds,
- ObservationGracePeriodSeconds: gracePeriodSeconds,
- ContractTransmitterTransmitTimeoutSeconds: transmitTimeoutSeconds,
- CaptureEaTelemetry: spec.CaptureEATelemetry,
- SpecCreatedAt: spec.CreatedAt.Format(time.RFC3339Nano),
- SpecUpdatedAt: spec.UpdatedAt.Format(time.RFC3339Nano),
+ SpecId: spec.ID,
+ P2Pv2Bootstrappers: spec.P2PV2Bootstrappers,
+ IsBootstrapPeer: spec.IsBootstrapPeer,
+ EncryptedOcrKeyBundleId: keyBundleID,
+ TransmitterAddress: transmitterAddress,
+ ObservationTimeoutSeconds: spec.ObservationTimeout.Duration().Seconds(),
+ BlockchainTimeoutSeconds: spec.BlockchainTimeout.Duration().Seconds(),
+ ContractConfigTrackerSubscribeIntervalSeconds: spec.ContractConfigTrackerSubscribeInterval.Duration().Seconds(),
+ ContractConfigTrackerPollIntervalSeconds: spec.ContractConfigTrackerPollInterval.Duration().Seconds(),
+ ContractConfigConfirmations: uint32(spec.ContractConfigConfirmations),
+ DatabaseTimeoutSeconds: dbTimeoutSeconds,
+ ObservationGracePeriodSeconds: gracePeriodSeconds,
+ ContractTransmitterTransmitTimeoutSeconds: transmitTimeoutSeconds,
+ CaptureEaTelemetry: spec.CaptureEATelemetry,
+ SpecCreatedAt: spec.CreatedAt.Format(time.RFC3339Nano),
+ SpecUpdatedAt: spec.UpdatedAt.Format(time.RFC3339Nano),
}
}
diff --git a/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go b/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go
index 65195797819..d96a4354c54 100644
--- a/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go
+++ b/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go
@@ -225,6 +225,7 @@ func TestBuildEvent_MedianJob(t *testing.T) {
assert.NotEmpty(t, ev.Ocr2OracleSpec.MedianPluginConfig.JuelsPerFeeCoinSource)
require.NotNil(t, ev.Ocr2OracleSpec.EvmRelayConfig)
assert.Equal(t, "1", ev.Ocr2OracleSpec.EvmRelayConfig.ChainId)
+ assert.Nil(t, ev.Ocr1OracleSpec)
}
func TestBuildEvent_NonMedianOCR2Job(t *testing.T) {
@@ -375,8 +376,6 @@ func TestBuildEvent_ContractFields_OCR1(t *testing.T) {
require.NotNil(t, ev.Ocr1OracleSpec)
ocr1 := ev.Ocr1OracleSpec
assert.Equal(t, int32(99), ocr1.SpecId)
- assert.Equal(t, "0x9d9305445F404E925563d5D5EcC65C815Ec1655b", ocr1.ContractAddress)
- assert.Equal(t, "11155111", ocr1.EvmChainId)
assert.Equal(t, []string{"12D3KooW@bootstrap:6688"}, ocr1.P2Pv2Bootstrappers)
assert.False(t, ocr1.IsBootstrapPeer)
assert.Equal(t, "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", ocr1.EncryptedOcrKeyBundleId)
@@ -397,25 +396,6 @@ func TestBuildEvent_ContractFields_OCR1(t *testing.T) {
assert.Nil(t, ev.Ocr2OracleSpec)
}
-func TestBuildEvent_OCR2Job_HasNoOCR1Spec(t *testing.T) {
- observer := beholdertest.NewObserver(t)
- jb := makeMedianJob()
- feedsORM := newFeedsORMWithoutProposal(t, jb)
- svc := newTestReporter(t, defaultConfig(), feedsORM)
-
- err := svc.EmitForJob(context.Background(), jb, events.EmissionTrigger_EMISSION_TRIGGER_HEARTBEAT)
- require.NoError(t, err)
-
- msgs := observer.Messages(t, "beholder_entity", events.ProtoPkg+"."+events.JobSpecEventEntity)
- require.Len(t, msgs, 1)
-
- var ev events.JobSpecEvent
- require.NoError(t, proto.Unmarshal(msgs[0].Body, &ev))
-
- assert.Nil(t, ev.Ocr1OracleSpec)
- assert.NotNil(t, ev.Ocr2OracleSpec)
-}
-
func makeOCR1Job() job.Job {
keyHash, err := corekeys.Sha256HashFromHex("0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20")
if err != nil {
From 93b6dab65f4d423feee165811cd429d9e429465b Mon Sep 17 00:00:00 2001
From: thomjg <103059015+thomjg@users.noreply.github.com>
Date: Sun, 26 Apr 2026 17:09:56 -0400
Subject: [PATCH 10/14] Removing duplicate fields from OCR2 proto definition
---
.../jobspec/events/job_spec.pb.go | 120 ++++++++----------
.../jobspec/events/job_spec.proto | 47 ++++---
.../jobspec/job_spec_reporter.go | 4 +-
.../jobspec/job_spec_reporter_test.go | 2 +-
4 files changed, 76 insertions(+), 97 deletions(-)
diff --git a/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go b/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go
index cd3675a8a01..f5036dbea8c 100644
--- a/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go
+++ b/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go
@@ -110,7 +110,7 @@ type JobSpecEvent struct {
EmissionTrigger EmissionTrigger `protobuf:"varint,24,opt,name=emission_trigger,json=emissionTrigger,proto3,enum=job_spec.v1.EmissionTrigger" json:"emission_trigger,omitempty"`
Timestamp string `protobuf:"bytes,25,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
// Primary on-chain contract — populated for single-contract job types
- // (OCR1, OCR2, Flux Monitor, Keeper). For OCR2, copied from ocr2_oracle_spec.
+ // (OCR1, OCR2, Flux Monitor, Keeper).
ContractAddress string `protobuf:"bytes,26,opt,name=contract_address,json=contractAddress,proto3" json:"contract_address,omitempty"`
ChainId string `protobuf:"bytes,27,opt,name=chain_id,json=chainId,proto3" json:"chain_id,omitempty"`
// OCR1-only; absent for other job types.
@@ -346,35 +346,34 @@ func (x *JobSpecEvent) GetOcr1OracleSpec() *OCR1OracleSpecInfo {
}
// OCR2OracleSpecInfo mirrors job.OCR2OracleSpec.
+// contract_id and chain_id live on the parent JobSpecEvent.
type OCR2OracleSpecInfo struct {
state protoimpl.MessageState `protogen:"open.v1"`
SpecId int32 `protobuf:"varint,1,opt,name=spec_id,json=specId,proto3" json:"spec_id,omitempty"`
- ContractId string `protobuf:"bytes,2,opt,name=contract_id,json=contractId,proto3" json:"contract_id,omitempty"`
- FeedId string `protobuf:"bytes,3,opt,name=feed_id,json=feedId,proto3" json:"feed_id,omitempty"`
- Relay string `protobuf:"bytes,4,opt,name=relay,proto3" json:"relay,omitempty"`
- ChainId string `protobuf:"bytes,5,opt,name=chain_id,json=chainId,proto3" json:"chain_id,omitempty"`
- PluginType string `protobuf:"bytes,6,opt,name=plugin_type,json=pluginType,proto3" json:"plugin_type,omitempty"`
- TransmitterId string `protobuf:"bytes,7,opt,name=transmitter_id,json=transmitterId,proto3" json:"transmitter_id,omitempty"`
- OcrKeyBundleId string `protobuf:"bytes,8,opt,name=ocr_key_bundle_id,json=ocrKeyBundleId,proto3" json:"ocr_key_bundle_id,omitempty"`
- MonitoringEndpoint string `protobuf:"bytes,9,opt,name=monitoring_endpoint,json=monitoringEndpoint,proto3" json:"monitoring_endpoint,omitempty"`
- P2Pv2Bootstrappers []string `protobuf:"bytes,10,rep,name=p2pv2_bootstrappers,json=p2pv2Bootstrappers,proto3" json:"p2pv2_bootstrappers,omitempty"`
- AllowNoBootstrappers bool `protobuf:"varint,11,opt,name=allow_no_bootstrappers,json=allowNoBootstrappers,proto3" json:"allow_no_bootstrappers,omitempty"`
- BlockchainTimeoutSeconds float64 `protobuf:"fixed64,12,opt,name=blockchain_timeout_seconds,json=blockchainTimeoutSeconds,proto3" json:"blockchain_timeout_seconds,omitempty"`
- ContractConfigTrackerPollIntervalSeconds float64 `protobuf:"fixed64,13,opt,name=contract_config_tracker_poll_interval_seconds,json=contractConfigTrackerPollIntervalSeconds,proto3" json:"contract_config_tracker_poll_interval_seconds,omitempty"`
- ContractConfigConfirmations uint32 `protobuf:"varint,14,opt,name=contract_config_confirmations,json=contractConfigConfirmations,proto3" json:"contract_config_confirmations,omitempty"`
- CaptureEaTelemetry bool `protobuf:"varint,15,opt,name=capture_ea_telemetry,json=captureEaTelemetry,proto3" json:"capture_ea_telemetry,omitempty"`
- CaptureAutomationCustomTelemetry bool `protobuf:"varint,16,opt,name=capture_automation_custom_telemetry,json=captureAutomationCustomTelemetry,proto3" json:"capture_automation_custom_telemetry,omitempty"`
- SpecCreatedAt string `protobuf:"bytes,17,opt,name=spec_created_at,json=specCreatedAt,proto3" json:"spec_created_at,omitempty"`
- SpecUpdatedAt string `protobuf:"bytes,18,opt,name=spec_updated_at,json=specUpdatedAt,proto3" json:"spec_updated_at,omitempty"`
+ FeedId string `protobuf:"bytes,2,opt,name=feed_id,json=feedId,proto3" json:"feed_id,omitempty"`
+ Relay string `protobuf:"bytes,3,opt,name=relay,proto3" json:"relay,omitempty"`
+ PluginType string `protobuf:"bytes,4,opt,name=plugin_type,json=pluginType,proto3" json:"plugin_type,omitempty"`
+ TransmitterId string `protobuf:"bytes,5,opt,name=transmitter_id,json=transmitterId,proto3" json:"transmitter_id,omitempty"`
+ OcrKeyBundleId string `protobuf:"bytes,6,opt,name=ocr_key_bundle_id,json=ocrKeyBundleId,proto3" json:"ocr_key_bundle_id,omitempty"`
+ MonitoringEndpoint string `protobuf:"bytes,7,opt,name=monitoring_endpoint,json=monitoringEndpoint,proto3" json:"monitoring_endpoint,omitempty"`
+ P2Pv2Bootstrappers []string `protobuf:"bytes,8,rep,name=p2pv2_bootstrappers,json=p2pv2Bootstrappers,proto3" json:"p2pv2_bootstrappers,omitempty"`
+ AllowNoBootstrappers bool `protobuf:"varint,9,opt,name=allow_no_bootstrappers,json=allowNoBootstrappers,proto3" json:"allow_no_bootstrappers,omitempty"`
+ BlockchainTimeoutSeconds float64 `protobuf:"fixed64,10,opt,name=blockchain_timeout_seconds,json=blockchainTimeoutSeconds,proto3" json:"blockchain_timeout_seconds,omitempty"`
+ ContractConfigTrackerPollIntervalSeconds float64 `protobuf:"fixed64,11,opt,name=contract_config_tracker_poll_interval_seconds,json=contractConfigTrackerPollIntervalSeconds,proto3" json:"contract_config_tracker_poll_interval_seconds,omitempty"`
+ ContractConfigConfirmations uint32 `protobuf:"varint,12,opt,name=contract_config_confirmations,json=contractConfigConfirmations,proto3" json:"contract_config_confirmations,omitempty"`
+ CaptureEaTelemetry bool `protobuf:"varint,13,opt,name=capture_ea_telemetry,json=captureEaTelemetry,proto3" json:"capture_ea_telemetry,omitempty"`
+ CaptureAutomationCustomTelemetry bool `protobuf:"varint,14,opt,name=capture_automation_custom_telemetry,json=captureAutomationCustomTelemetry,proto3" json:"capture_automation_custom_telemetry,omitempty"`
+ SpecCreatedAt string `protobuf:"bytes,15,opt,name=spec_created_at,json=specCreatedAt,proto3" json:"spec_created_at,omitempty"`
+ SpecUpdatedAt string `protobuf:"bytes,16,opt,name=spec_updated_at,json=specUpdatedAt,proto3" json:"spec_updated_at,omitempty"`
// Raw JSON passthroughs — always populated; authoritative over the typed
// sub-messages below.
- RelayConfigJson string `protobuf:"bytes,19,opt,name=relay_config_json,json=relayConfigJson,proto3" json:"relay_config_json,omitempty"`
- PluginConfigJson string `protobuf:"bytes,20,opt,name=plugin_config_json,json=pluginConfigJson,proto3" json:"plugin_config_json,omitempty"`
- OnchainSigningStrategyJson string `protobuf:"bytes,21,opt,name=onchain_signing_strategy_json,json=onchainSigningStrategyJson,proto3" json:"onchain_signing_strategy_json,omitempty"`
+ RelayConfigJson string `protobuf:"bytes,17,opt,name=relay_config_json,json=relayConfigJson,proto3" json:"relay_config_json,omitempty"`
+ PluginConfigJson string `protobuf:"bytes,18,opt,name=plugin_config_json,json=pluginConfigJson,proto3" json:"plugin_config_json,omitempty"`
+ OnchainSigningStrategyJson string `protobuf:"bytes,19,opt,name=onchain_signing_strategy_json,json=onchainSigningStrategyJson,proto3" json:"onchain_signing_strategy_json,omitempty"`
// Populated when relay == "evm".
- EvmRelayConfig *OCR2EVMRelayConfig `protobuf:"bytes,22,opt,name=evm_relay_config,json=evmRelayConfig,proto3" json:"evm_relay_config,omitempty"`
+ EvmRelayConfig *OCR2EVMRelayConfig `protobuf:"bytes,20,opt,name=evm_relay_config,json=evmRelayConfig,proto3" json:"evm_relay_config,omitempty"`
// Populated when plugin_type == "median".
- MedianPluginConfig *OCR2MedianPluginConfig `protobuf:"bytes,23,opt,name=median_plugin_config,json=medianPluginConfig,proto3" json:"median_plugin_config,omitempty"`
+ MedianPluginConfig *OCR2MedianPluginConfig `protobuf:"bytes,21,opt,name=median_plugin_config,json=medianPluginConfig,proto3" json:"median_plugin_config,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -416,13 +415,6 @@ func (x *OCR2OracleSpecInfo) GetSpecId() int32 {
return 0
}
-func (x *OCR2OracleSpecInfo) GetContractId() string {
- if x != nil {
- return x.ContractId
- }
- return ""
-}
-
func (x *OCR2OracleSpecInfo) GetFeedId() string {
if x != nil {
return x.FeedId
@@ -437,13 +429,6 @@ func (x *OCR2OracleSpecInfo) GetRelay() string {
return ""
}
-func (x *OCR2OracleSpecInfo) GetChainId() string {
- if x != nil {
- return x.ChainId
- }
- return ""
-}
-
func (x *OCR2OracleSpecInfo) GetPluginType() string {
if x != nil {
return x.PluginType
@@ -686,7 +671,7 @@ type OCR1OracleSpecInfo struct {
SpecId int32 `protobuf:"varint,1,opt,name=spec_id,json=specId,proto3" json:"spec_id,omitempty"`
P2Pv2Bootstrappers []string `protobuf:"bytes,2,rep,name=p2pv2_bootstrappers,json=p2pv2Bootstrappers,proto3" json:"p2pv2_bootstrappers,omitempty"`
IsBootstrapPeer bool `protobuf:"varint,3,opt,name=is_bootstrap_peer,json=isBootstrapPeer,proto3" json:"is_bootstrap_peer,omitempty"`
- EncryptedOcrKeyBundleId string `protobuf:"bytes,4,opt,name=encrypted_ocr_key_bundle_id,json=encryptedOcrKeyBundleId,proto3" json:"encrypted_ocr_key_bundle_id,omitempty"`
+ OcrKeyBundleId string `protobuf:"bytes,4,opt,name=ocr_key_bundle_id,json=ocrKeyBundleId,proto3" json:"ocr_key_bundle_id,omitempty"`
TransmitterAddress string `protobuf:"bytes,5,opt,name=transmitter_address,json=transmitterAddress,proto3" json:"transmitter_address,omitempty"`
ObservationTimeoutSeconds float64 `protobuf:"fixed64,6,opt,name=observation_timeout_seconds,json=observationTimeoutSeconds,proto3" json:"observation_timeout_seconds,omitempty"`
BlockchainTimeoutSeconds float64 `protobuf:"fixed64,7,opt,name=blockchain_timeout_seconds,json=blockchainTimeoutSeconds,proto3" json:"blockchain_timeout_seconds,omitempty"`
@@ -754,9 +739,9 @@ func (x *OCR1OracleSpecInfo) GetIsBootstrapPeer() bool {
return false
}
-func (x *OCR1OracleSpecInfo) GetEncryptedOcrKeyBundleId() string {
+func (x *OCR1OracleSpecInfo) GetOcrKeyBundleId() string {
if x != nil {
- return x.EncryptedOcrKeyBundleId
+ return x.OcrKeyBundleId
}
return ""
}
@@ -973,34 +958,31 @@ const file_job_spec_proto_rawDesc = "" +
"\bchain_id\x18\x1b \x01(\tR\achainId\x12I\n" +
"\x10ocr1_oracle_spec\x18\x1c \x01(\v2\x1f.job_spec.v1.OCR1OracleSpecInfoR\x0eocr1OracleSpecB\f\n" +
"\n" +
- "_stream_id\"\x96\t\n" +
+ "_stream_id\"\xda\b\n" +
"\x12OCR2OracleSpecInfo\x12\x17\n" +
- "\aspec_id\x18\x01 \x01(\x05R\x06specId\x12\x1f\n" +
- "\vcontract_id\x18\x02 \x01(\tR\n" +
- "contractId\x12\x17\n" +
- "\afeed_id\x18\x03 \x01(\tR\x06feedId\x12\x14\n" +
- "\x05relay\x18\x04 \x01(\tR\x05relay\x12\x19\n" +
- "\bchain_id\x18\x05 \x01(\tR\achainId\x12\x1f\n" +
- "\vplugin_type\x18\x06 \x01(\tR\n" +
+ "\aspec_id\x18\x01 \x01(\x05R\x06specId\x12\x17\n" +
+ "\afeed_id\x18\x02 \x01(\tR\x06feedId\x12\x14\n" +
+ "\x05relay\x18\x03 \x01(\tR\x05relay\x12\x1f\n" +
+ "\vplugin_type\x18\x04 \x01(\tR\n" +
"pluginType\x12%\n" +
- "\x0etransmitter_id\x18\a \x01(\tR\rtransmitterId\x12)\n" +
- "\x11ocr_key_bundle_id\x18\b \x01(\tR\x0eocrKeyBundleId\x12/\n" +
- "\x13monitoring_endpoint\x18\t \x01(\tR\x12monitoringEndpoint\x12/\n" +
- "\x13p2pv2_bootstrappers\x18\n" +
- " \x03(\tR\x12p2pv2Bootstrappers\x124\n" +
- "\x16allow_no_bootstrappers\x18\v \x01(\bR\x14allowNoBootstrappers\x12<\n" +
- "\x1ablockchain_timeout_seconds\x18\f \x01(\x01R\x18blockchainTimeoutSeconds\x12_\n" +
- "-contract_config_tracker_poll_interval_seconds\x18\r \x01(\x01R(contractConfigTrackerPollIntervalSeconds\x12B\n" +
- "\x1dcontract_config_confirmations\x18\x0e \x01(\rR\x1bcontractConfigConfirmations\x120\n" +
- "\x14capture_ea_telemetry\x18\x0f \x01(\bR\x12captureEaTelemetry\x12M\n" +
- "#capture_automation_custom_telemetry\x18\x10 \x01(\bR captureAutomationCustomTelemetry\x12&\n" +
- "\x0fspec_created_at\x18\x11 \x01(\tR\rspecCreatedAt\x12&\n" +
- "\x0fspec_updated_at\x18\x12 \x01(\tR\rspecUpdatedAt\x12*\n" +
- "\x11relay_config_json\x18\x13 \x01(\tR\x0frelayConfigJson\x12,\n" +
- "\x12plugin_config_json\x18\x14 \x01(\tR\x10pluginConfigJson\x12A\n" +
- "\x1donchain_signing_strategy_json\x18\x15 \x01(\tR\x1aonchainSigningStrategyJson\x12I\n" +
- "\x10evm_relay_config\x18\x16 \x01(\v2\x1f.job_spec.v1.OCR2EVMRelayConfigR\x0eevmRelayConfig\x12U\n" +
- "\x14median_plugin_config\x18\x17 \x01(\v2#.job_spec.v1.OCR2MedianPluginConfigR\x12medianPluginConfig\"\xfd\x02\n" +
+ "\x0etransmitter_id\x18\x05 \x01(\tR\rtransmitterId\x12)\n" +
+ "\x11ocr_key_bundle_id\x18\x06 \x01(\tR\x0eocrKeyBundleId\x12/\n" +
+ "\x13monitoring_endpoint\x18\a \x01(\tR\x12monitoringEndpoint\x12/\n" +
+ "\x13p2pv2_bootstrappers\x18\b \x03(\tR\x12p2pv2Bootstrappers\x124\n" +
+ "\x16allow_no_bootstrappers\x18\t \x01(\bR\x14allowNoBootstrappers\x12<\n" +
+ "\x1ablockchain_timeout_seconds\x18\n" +
+ " \x01(\x01R\x18blockchainTimeoutSeconds\x12_\n" +
+ "-contract_config_tracker_poll_interval_seconds\x18\v \x01(\x01R(contractConfigTrackerPollIntervalSeconds\x12B\n" +
+ "\x1dcontract_config_confirmations\x18\f \x01(\rR\x1bcontractConfigConfirmations\x120\n" +
+ "\x14capture_ea_telemetry\x18\r \x01(\bR\x12captureEaTelemetry\x12M\n" +
+ "#capture_automation_custom_telemetry\x18\x0e \x01(\bR captureAutomationCustomTelemetry\x12&\n" +
+ "\x0fspec_created_at\x18\x0f \x01(\tR\rspecCreatedAt\x12&\n" +
+ "\x0fspec_updated_at\x18\x10 \x01(\tR\rspecUpdatedAt\x12*\n" +
+ "\x11relay_config_json\x18\x11 \x01(\tR\x0frelayConfigJson\x12,\n" +
+ "\x12plugin_config_json\x18\x12 \x01(\tR\x10pluginConfigJson\x12A\n" +
+ "\x1donchain_signing_strategy_json\x18\x13 \x01(\tR\x1aonchainSigningStrategyJson\x12I\n" +
+ "\x10evm_relay_config\x18\x14 \x01(\v2\x1f.job_spec.v1.OCR2EVMRelayConfigR\x0eevmRelayConfig\x12U\n" +
+ "\x14median_plugin_config\x18\x15 \x01(\v2#.job_spec.v1.OCR2MedianPluginConfigR\x12medianPluginConfig\"\xfd\x02\n" +
"\x12OCR2EVMRelayConfig\x12\x19\n" +
"\bchain_id\x18\x01 \x01(\tR\achainId\x12\x1d\n" +
"\n" +
@@ -1012,12 +994,12 @@ const file_job_spec_proto_rawDesc = "" +
"llo_don_id\x18\x06 \x01(\x04R\blloDonId\x12\x17\n" +
"\afeed_id\x18\a \x01(\tR\x06feedId\x12!\n" +
"\fsending_keys\x18\b \x03(\tR\vsendingKeys\x12#\n" +
- "\rprovider_type\x18\t \x01(\tR\fproviderType\"\xee\a\n" +
+ "\rprovider_type\x18\t \x01(\tR\fproviderType\"\xdb\a\n" +
"\x12OCR1OracleSpecInfo\x12\x17\n" +
"\aspec_id\x18\x01 \x01(\x05R\x06specId\x12/\n" +
"\x13p2pv2_bootstrappers\x18\x02 \x03(\tR\x12p2pv2Bootstrappers\x12*\n" +
- "\x11is_bootstrap_peer\x18\x03 \x01(\bR\x0fisBootstrapPeer\x12<\n" +
- "\x1bencrypted_ocr_key_bundle_id\x18\x04 \x01(\tR\x17encryptedOcrKeyBundleId\x12/\n" +
+ "\x11is_bootstrap_peer\x18\x03 \x01(\bR\x0fisBootstrapPeer\x12)\n" +
+ "\x11ocr_key_bundle_id\x18\x04 \x01(\tR\x0eocrKeyBundleId\x12/\n" +
"\x13transmitter_address\x18\x05 \x01(\tR\x12transmitterAddress\x12>\n" +
"\x1bobservation_timeout_seconds\x18\x06 \x01(\x01R\x19observationTimeoutSeconds\x12<\n" +
"\x1ablockchain_timeout_seconds\x18\a \x01(\x01R\x18blockchainTimeoutSeconds\x12i\n" +
diff --git a/core/services/nodestatusreporter/jobspec/events/job_spec.proto b/core/services/nodestatusreporter/jobspec/events/job_spec.proto
index 7b0152849ff..3d27140986a 100644
--- a/core/services/nodestatusreporter/jobspec/events/job_spec.proto
+++ b/core/services/nodestatusreporter/jobspec/events/job_spec.proto
@@ -46,7 +46,7 @@ message JobSpecEvent {
string timestamp = 25;
// Primary on-chain contract — populated for single-contract job types
- // (OCR1, OCR2, Flux Monitor, Keeper). For OCR2, copied from ocr2_oracle_spec.
+ // (OCR1, OCR2, Flux Monitor, Keeper).
string contract_address = 26;
string chain_id = 27;
@@ -63,37 +63,36 @@ enum EmissionTrigger {
}
// OCR2OracleSpecInfo mirrors job.OCR2OracleSpec.
+// contract_id and chain_id live on the parent JobSpecEvent.
message OCR2OracleSpecInfo {
int32 spec_id = 1;
- string contract_id = 2;
- string feed_id = 3;
- string relay = 4;
- string chain_id = 5;
- string plugin_type = 6;
- string transmitter_id = 7;
- string ocr_key_bundle_id = 8;
- string monitoring_endpoint = 9;
- repeated string p2pv2_bootstrappers = 10;
- bool allow_no_bootstrappers = 11;
- double blockchain_timeout_seconds = 12;
- double contract_config_tracker_poll_interval_seconds = 13;
- uint32 contract_config_confirmations = 14;
- bool capture_ea_telemetry = 15;
- bool capture_automation_custom_telemetry = 16;
- string spec_created_at = 17;
- string spec_updated_at = 18;
+ string feed_id = 2;
+ string relay = 3;
+ string plugin_type = 4;
+ string transmitter_id = 5;
+ string ocr_key_bundle_id = 6;
+ string monitoring_endpoint = 7;
+ repeated string p2pv2_bootstrappers = 8;
+ bool allow_no_bootstrappers = 9;
+ double blockchain_timeout_seconds = 10;
+ double contract_config_tracker_poll_interval_seconds = 11;
+ uint32 contract_config_confirmations = 12;
+ bool capture_ea_telemetry = 13;
+ bool capture_automation_custom_telemetry = 14;
+ string spec_created_at = 15;
+ string spec_updated_at = 16;
// Raw JSON passthroughs — always populated; authoritative over the typed
// sub-messages below.
- string relay_config_json = 19;
- string plugin_config_json = 20;
- string onchain_signing_strategy_json = 21;
+ string relay_config_json = 17;
+ string plugin_config_json = 18;
+ string onchain_signing_strategy_json = 19;
// Populated when relay == "evm".
- OCR2EVMRelayConfig evm_relay_config = 22;
+ OCR2EVMRelayConfig evm_relay_config = 20;
// Populated when plugin_type == "median".
- OCR2MedianPluginConfig median_plugin_config = 23;
+ OCR2MedianPluginConfig median_plugin_config = 21;
}
// OCR2EVMRelayConfig is a typed view of the EVM relay config JSON.
@@ -115,7 +114,7 @@ message OCR1OracleSpecInfo {
int32 spec_id = 1;
repeated string p2pv2_bootstrappers = 2;
bool is_bootstrap_peer = 3;
- string encrypted_ocr_key_bundle_id = 4;
+ string ocr_key_bundle_id = 4;
string transmitter_address = 5;
double observation_timeout_seconds = 6;
double blockchain_timeout_seconds = 7;
diff --git a/core/services/nodestatusreporter/jobspec/job_spec_reporter.go b/core/services/nodestatusreporter/jobspec/job_spec_reporter.go
index e2b0910be7a..eae5c446a68 100644
--- a/core/services/nodestatusreporter/jobspec/job_spec_reporter.go
+++ b/core/services/nodestatusreporter/jobspec/job_spec_reporter.go
@@ -294,10 +294,8 @@ func buildOCR2OracleSpecInfo(spec *job.OCR2OracleSpec) (*events.OCR2OracleSpecIn
info := &events.OCR2OracleSpecInfo{
SpecId: spec.ID,
- ContractId: spec.ContractID,
FeedId: feedID,
Relay: spec.Relay,
- ChainId: spec.ChainID,
PluginType: string(spec.PluginType),
TransmitterId: spec.TransmitterID.ValueOrZero(),
OcrKeyBundleId: spec.OCRKeyBundleID.ValueOrZero(),
@@ -365,7 +363,7 @@ func buildOCR1OracleSpecInfo(spec *job.OCROracleSpec) *events.OCR1OracleSpecInfo
SpecId: spec.ID,
P2Pv2Bootstrappers: spec.P2PV2Bootstrappers,
IsBootstrapPeer: spec.IsBootstrapPeer,
- EncryptedOcrKeyBundleId: keyBundleID,
+ OcrKeyBundleId: keyBundleID,
TransmitterAddress: transmitterAddress,
ObservationTimeoutSeconds: spec.ObservationTimeout.Duration().Seconds(),
BlockchainTimeoutSeconds: spec.BlockchainTimeout.Duration().Seconds(),
diff --git a/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go b/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go
index d96a4354c54..837449c8e90 100644
--- a/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go
+++ b/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go
@@ -378,7 +378,7 @@ func TestBuildEvent_ContractFields_OCR1(t *testing.T) {
assert.Equal(t, int32(99), ocr1.SpecId)
assert.Equal(t, []string{"12D3KooW@bootstrap:6688"}, ocr1.P2Pv2Bootstrappers)
assert.False(t, ocr1.IsBootstrapPeer)
- assert.Equal(t, "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", ocr1.EncryptedOcrKeyBundleId)
+ assert.Equal(t, "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", ocr1.OcrKeyBundleId)
assert.Equal(t, "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B", ocr1.TransmitterAddress)
assert.InDelta(t, 30.0, ocr1.ObservationTimeoutSeconds, 0.001)
assert.InDelta(t, 20.0, ocr1.BlockchainTimeoutSeconds, 0.001)
From 8f657218032aba56bf47f759e8fba3ccb2e242ac Mon Sep 17 00:00:00 2001
From: thomjg <103059015+thomjg@users.noreply.github.com>
Date: Sun, 26 Apr 2026 17:28:16 -0400
Subject: [PATCH 11/14] Linting fix
---
.../nodestatusreporter/jobspec/job_spec_reporter_test.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go b/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go
index 837449c8e90..d1f9d36bcec 100644
--- a/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go
+++ b/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go
@@ -7,13 +7,13 @@ import (
"time"
"github.com/google/uuid"
+ "github.com/lib/pq"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"gopkg.in/guregu/null.v4"
- "github.com/lib/pq"
"github.com/smartcontractkit/chainlink-common/keystore/corekeys"
"github.com/smartcontractkit/chainlink-common/pkg/beholder"
"github.com/smartcontractkit/chainlink-common/pkg/beholder/beholdertest"
From 702471057634d26fd8ba40ec070c73fe3affe8f2 Mon Sep 17 00:00:00 2001
From: thomjg <103059015+thomjg@users.noreply.github.com>
Date: Mon, 27 Apr 2026 15:28:10 -0400
Subject: [PATCH 12/14] Importing from chainlink-protos instead
---
.../jobspec/events/generate.go | 2 +-
.../jobspec/events/job_spec.pb.go | 1091 +----------------
.../jobspec/events/job_spec.proto | 147 +--
go.mod | 1 +
go.sum | 2 +
5 files changed, 25 insertions(+), 1218 deletions(-)
diff --git a/core/services/nodestatusreporter/jobspec/events/generate.go b/core/services/nodestatusreporter/jobspec/events/generate.go
index df6a81e0d62..28254e8a0a0 100644
--- a/core/services/nodestatusreporter/jobspec/events/generate.go
+++ b/core/services/nodestatusreporter/jobspec/events/generate.go
@@ -1,3 +1,3 @@
package events
-//go:generate protoc --go_out=. --go_opt=paths=source_relative job_spec.proto
+// Job spec protobufs are generated in github.com/smartcontractkit/chainlink-protos/data-feeds.
diff --git a/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go b/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go
index f5036dbea8c..7e11e16520e 100644
--- a/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go
+++ b/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go
@@ -1,1088 +1,27 @@
-// Code generated by protoc-gen-go. DO NOT EDIT.
-// versions:
-// protoc-gen-go v1.36.11
-// protoc v5.29.3
+// Code generated by chainlink-protos import shim. DO NOT EDIT.
// source: job_spec.proto
package events
-import (
- protoreflect "google.golang.org/protobuf/reflect/protoreflect"
- protoimpl "google.golang.org/protobuf/runtime/protoimpl"
- reflect "reflect"
- sync "sync"
- unsafe "unsafe"
-)
-
-const (
- // Verify that this generated code is sufficiently up-to-date.
- _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
- // Verify that runtime/protoimpl is sufficiently up-to-date.
- _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
-)
+import job_specv1 "github.com/smartcontractkit/chainlink-protos/data-feeds/job_spec/v1"
-// EmissionTrigger is the reason a JobSpecEvent was emitted.
-type EmissionTrigger int32
+type EmissionTrigger = job_specv1.EmissionTrigger
const (
- EmissionTrigger_EMISSION_TRIGGER_UNSPECIFIED EmissionTrigger = 0
- EmissionTrigger_EMISSION_TRIGGER_HEARTBEAT EmissionTrigger = 1
- EmissionTrigger_EMISSION_TRIGGER_CREATE EmissionTrigger = 2
- EmissionTrigger_EMISSION_TRIGGER_DELETE EmissionTrigger = 3
+ EmissionTrigger_EMISSION_TRIGGER_UNSPECIFIED = job_specv1.EmissionTrigger_EMISSION_TRIGGER_UNSPECIFIED
+ EmissionTrigger_EMISSION_TRIGGER_HEARTBEAT = job_specv1.EmissionTrigger_EMISSION_TRIGGER_HEARTBEAT
+ EmissionTrigger_EMISSION_TRIGGER_CREATE = job_specv1.EmissionTrigger_EMISSION_TRIGGER_CREATE
+ EmissionTrigger_EMISSION_TRIGGER_DELETE = job_specv1.EmissionTrigger_EMISSION_TRIGGER_DELETE
)
-// Enum value maps for EmissionTrigger.
var (
- EmissionTrigger_name = map[int32]string{
- 0: "EMISSION_TRIGGER_UNSPECIFIED",
- 1: "EMISSION_TRIGGER_HEARTBEAT",
- 2: "EMISSION_TRIGGER_CREATE",
- 3: "EMISSION_TRIGGER_DELETE",
- }
- EmissionTrigger_value = map[string]int32{
- "EMISSION_TRIGGER_UNSPECIFIED": 0,
- "EMISSION_TRIGGER_HEARTBEAT": 1,
- "EMISSION_TRIGGER_CREATE": 2,
- "EMISSION_TRIGGER_DELETE": 3,
- }
+ EmissionTrigger_name = job_specv1.EmissionTrigger_name
+ EmissionTrigger_value = job_specv1.EmissionTrigger_value
+ File_job_spec_proto = job_specv1.File_job_spec_v1_job_spec_event_proto
)
-func (x EmissionTrigger) Enum() *EmissionTrigger {
- p := new(EmissionTrigger)
- *p = x
- return p
-}
-
-func (x EmissionTrigger) String() string {
- return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
-}
-
-func (EmissionTrigger) Descriptor() protoreflect.EnumDescriptor {
- return file_job_spec_proto_enumTypes[0].Descriptor()
-}
-
-func (EmissionTrigger) Type() protoreflect.EnumType {
- return &file_job_spec_proto_enumTypes[0]
-}
-
-func (x EmissionTrigger) Number() protoreflect.EnumNumber {
- return protoreflect.EnumNumber(x)
-}
-
-// Deprecated: Use EmissionTrigger.Descriptor instead.
-func (EmissionTrigger) EnumDescriptor() ([]byte, []int) {
- return file_job_spec_proto_rawDescGZIP(), []int{0}
-}
-
-// JobSpecEvent carries a job's spec, emitted on heartbeat, create, and delete.
-type JobSpecEvent struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- // Job identity
- ExternalJobId string `protobuf:"bytes,1,opt,name=external_job_id,json=externalJobId,proto3" json:"external_job_id,omitempty"`
- InternalJobId int32 `protobuf:"varint,2,opt,name=internal_job_id,json=internalJobId,proto3" json:"internal_job_id,omitempty"`
- Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
- JobType string `protobuf:"bytes,4,opt,name=job_type,json=jobType,proto3" json:"job_type,omitempty"`
- SchemaVersion uint32 `protobuf:"varint,5,opt,name=schema_version,json=schemaVersion,proto3" json:"schema_version,omitempty"`
- GasLimit uint32 `protobuf:"varint,6,opt,name=gas_limit,json=gasLimit,proto3" json:"gas_limit,omitempty"`
- ForwardingAllowed bool `protobuf:"varint,7,opt,name=forwarding_allowed,json=forwardingAllowed,proto3" json:"forwarding_allowed,omitempty"`
- StreamId *uint32 `protobuf:"varint,8,opt,name=stream_id,json=streamId,proto3,oneof" json:"stream_id,omitempty"`
- MaxTaskDurationSeconds float64 `protobuf:"fixed64,9,opt,name=max_task_duration_seconds,json=maxTaskDurationSeconds,proto3" json:"max_task_duration_seconds,omitempty"`
- CreatedAt string `protobuf:"bytes,10,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
- // Observation pipeline
- ObservationSource string `protobuf:"bytes,11,opt,name=observation_source,json=observationSource,proto3" json:"observation_source,omitempty"`
- PipelineSpecId int32 `protobuf:"varint,12,opt,name=pipeline_spec_id,json=pipelineSpecId,proto3" json:"pipeline_spec_id,omitempty"`
- // Top-level bridge names in the observation pipeline.
- BridgeNames []string `protobuf:"bytes,13,rep,name=bridge_names,json=bridgeNames,proto3" json:"bridge_names,omitempty"`
- // Proposal lifecycle — zero/empty for jobs not managed by a Feeds Manager.
- FeedsManagerId int64 `protobuf:"varint,14,opt,name=feeds_manager_id,json=feedsManagerId,proto3" json:"feeds_manager_id,omitempty"`
- RemoteUuid string `protobuf:"bytes,15,opt,name=remote_uuid,json=remoteUuid,proto3" json:"remote_uuid,omitempty"`
- SpecVersion int32 `protobuf:"varint,16,opt,name=spec_version,json=specVersion,proto3" json:"spec_version,omitempty"`
- ProposedAt string `protobuf:"bytes,17,opt,name=proposed_at,json=proposedAt,proto3" json:"proposed_at,omitempty"`
- ApprovedAt string `protobuf:"bytes,18,opt,name=approved_at,json=approvedAt,proto3" json:"approved_at,omitempty"`
- AcceptLatencySeconds float64 `protobuf:"fixed64,19,opt,name=accept_latency_seconds,json=acceptLatencySeconds,proto3" json:"accept_latency_seconds,omitempty"`
- // OCR2-only; absent for other job types.
- Ocr2OracleSpec *OCR2OracleSpecInfo `protobuf:"bytes,20,opt,name=ocr2_oracle_spec,json=ocr2OracleSpec,proto3" json:"ocr2_oracle_spec,omitempty"`
- // Node identity
- CsaPublicKey string `protobuf:"bytes,21,opt,name=csa_public_key,json=csaPublicKey,proto3" json:"csa_public_key,omitempty"`
- NodeVersion string `protobuf:"bytes,22,opt,name=node_version,json=nodeVersion,proto3" json:"node_version,omitempty"`
- Hostname string `protobuf:"bytes,23,opt,name=hostname,proto3" json:"hostname,omitempty"`
- // Event metadata
- EmissionTrigger EmissionTrigger `protobuf:"varint,24,opt,name=emission_trigger,json=emissionTrigger,proto3,enum=job_spec.v1.EmissionTrigger" json:"emission_trigger,omitempty"`
- Timestamp string `protobuf:"bytes,25,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
- // Primary on-chain contract — populated for single-contract job types
- // (OCR1, OCR2, Flux Monitor, Keeper).
- ContractAddress string `protobuf:"bytes,26,opt,name=contract_address,json=contractAddress,proto3" json:"contract_address,omitempty"`
- ChainId string `protobuf:"bytes,27,opt,name=chain_id,json=chainId,proto3" json:"chain_id,omitempty"`
- // OCR1-only; absent for other job types.
- Ocr1OracleSpec *OCR1OracleSpecInfo `protobuf:"bytes,28,opt,name=ocr1_oracle_spec,json=ocr1OracleSpec,proto3" json:"ocr1_oracle_spec,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
-}
-
-func (x *JobSpecEvent) Reset() {
- *x = JobSpecEvent{}
- mi := &file_job_spec_proto_msgTypes[0]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
-}
-
-func (x *JobSpecEvent) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*JobSpecEvent) ProtoMessage() {}
-
-func (x *JobSpecEvent) ProtoReflect() protoreflect.Message {
- mi := &file_job_spec_proto_msgTypes[0]
- if x != nil {
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- if ms.LoadMessageInfo() == nil {
- ms.StoreMessageInfo(mi)
- }
- return ms
- }
- return mi.MessageOf(x)
-}
-
-// Deprecated: Use JobSpecEvent.ProtoReflect.Descriptor instead.
-func (*JobSpecEvent) Descriptor() ([]byte, []int) {
- return file_job_spec_proto_rawDescGZIP(), []int{0}
-}
-
-func (x *JobSpecEvent) GetExternalJobId() string {
- if x != nil {
- return x.ExternalJobId
- }
- return ""
-}
-
-func (x *JobSpecEvent) GetInternalJobId() int32 {
- if x != nil {
- return x.InternalJobId
- }
- return 0
-}
-
-func (x *JobSpecEvent) GetName() string {
- if x != nil {
- return x.Name
- }
- return ""
-}
-
-func (x *JobSpecEvent) GetJobType() string {
- if x != nil {
- return x.JobType
- }
- return ""
-}
-
-func (x *JobSpecEvent) GetSchemaVersion() uint32 {
- if x != nil {
- return x.SchemaVersion
- }
- return 0
-}
-
-func (x *JobSpecEvent) GetGasLimit() uint32 {
- if x != nil {
- return x.GasLimit
- }
- return 0
-}
-
-func (x *JobSpecEvent) GetForwardingAllowed() bool {
- if x != nil {
- return x.ForwardingAllowed
- }
- return false
-}
-
-func (x *JobSpecEvent) GetStreamId() uint32 {
- if x != nil && x.StreamId != nil {
- return *x.StreamId
- }
- return 0
-}
-
-func (x *JobSpecEvent) GetMaxTaskDurationSeconds() float64 {
- if x != nil {
- return x.MaxTaskDurationSeconds
- }
- return 0
-}
-
-func (x *JobSpecEvent) GetCreatedAt() string {
- if x != nil {
- return x.CreatedAt
- }
- return ""
-}
-
-func (x *JobSpecEvent) GetObservationSource() string {
- if x != nil {
- return x.ObservationSource
- }
- return ""
-}
-
-func (x *JobSpecEvent) GetPipelineSpecId() int32 {
- if x != nil {
- return x.PipelineSpecId
- }
- return 0
-}
-
-func (x *JobSpecEvent) GetBridgeNames() []string {
- if x != nil {
- return x.BridgeNames
- }
- return nil
-}
-
-func (x *JobSpecEvent) GetFeedsManagerId() int64 {
- if x != nil {
- return x.FeedsManagerId
- }
- return 0
-}
-
-func (x *JobSpecEvent) GetRemoteUuid() string {
- if x != nil {
- return x.RemoteUuid
- }
- return ""
-}
-
-func (x *JobSpecEvent) GetSpecVersion() int32 {
- if x != nil {
- return x.SpecVersion
- }
- return 0
-}
-
-func (x *JobSpecEvent) GetProposedAt() string {
- if x != nil {
- return x.ProposedAt
- }
- return ""
-}
-
-func (x *JobSpecEvent) GetApprovedAt() string {
- if x != nil {
- return x.ApprovedAt
- }
- return ""
-}
-
-func (x *JobSpecEvent) GetAcceptLatencySeconds() float64 {
- if x != nil {
- return x.AcceptLatencySeconds
- }
- return 0
-}
-
-func (x *JobSpecEvent) GetOcr2OracleSpec() *OCR2OracleSpecInfo {
- if x != nil {
- return x.Ocr2OracleSpec
- }
- return nil
-}
-
-func (x *JobSpecEvent) GetCsaPublicKey() string {
- if x != nil {
- return x.CsaPublicKey
- }
- return ""
-}
-
-func (x *JobSpecEvent) GetNodeVersion() string {
- if x != nil {
- return x.NodeVersion
- }
- return ""
-}
-
-func (x *JobSpecEvent) GetHostname() string {
- if x != nil {
- return x.Hostname
- }
- return ""
-}
-
-func (x *JobSpecEvent) GetEmissionTrigger() EmissionTrigger {
- if x != nil {
- return x.EmissionTrigger
- }
- return EmissionTrigger_EMISSION_TRIGGER_UNSPECIFIED
-}
-
-func (x *JobSpecEvent) GetTimestamp() string {
- if x != nil {
- return x.Timestamp
- }
- return ""
-}
-
-func (x *JobSpecEvent) GetContractAddress() string {
- if x != nil {
- return x.ContractAddress
- }
- return ""
-}
-
-func (x *JobSpecEvent) GetChainId() string {
- if x != nil {
- return x.ChainId
- }
- return ""
-}
-
-func (x *JobSpecEvent) GetOcr1OracleSpec() *OCR1OracleSpecInfo {
- if x != nil {
- return x.Ocr1OracleSpec
- }
- return nil
-}
-
-// OCR2OracleSpecInfo mirrors job.OCR2OracleSpec.
-// contract_id and chain_id live on the parent JobSpecEvent.
-type OCR2OracleSpecInfo struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- SpecId int32 `protobuf:"varint,1,opt,name=spec_id,json=specId,proto3" json:"spec_id,omitempty"`
- FeedId string `protobuf:"bytes,2,opt,name=feed_id,json=feedId,proto3" json:"feed_id,omitempty"`
- Relay string `protobuf:"bytes,3,opt,name=relay,proto3" json:"relay,omitempty"`
- PluginType string `protobuf:"bytes,4,opt,name=plugin_type,json=pluginType,proto3" json:"plugin_type,omitempty"`
- TransmitterId string `protobuf:"bytes,5,opt,name=transmitter_id,json=transmitterId,proto3" json:"transmitter_id,omitempty"`
- OcrKeyBundleId string `protobuf:"bytes,6,opt,name=ocr_key_bundle_id,json=ocrKeyBundleId,proto3" json:"ocr_key_bundle_id,omitempty"`
- MonitoringEndpoint string `protobuf:"bytes,7,opt,name=monitoring_endpoint,json=monitoringEndpoint,proto3" json:"monitoring_endpoint,omitempty"`
- P2Pv2Bootstrappers []string `protobuf:"bytes,8,rep,name=p2pv2_bootstrappers,json=p2pv2Bootstrappers,proto3" json:"p2pv2_bootstrappers,omitempty"`
- AllowNoBootstrappers bool `protobuf:"varint,9,opt,name=allow_no_bootstrappers,json=allowNoBootstrappers,proto3" json:"allow_no_bootstrappers,omitempty"`
- BlockchainTimeoutSeconds float64 `protobuf:"fixed64,10,opt,name=blockchain_timeout_seconds,json=blockchainTimeoutSeconds,proto3" json:"blockchain_timeout_seconds,omitempty"`
- ContractConfigTrackerPollIntervalSeconds float64 `protobuf:"fixed64,11,opt,name=contract_config_tracker_poll_interval_seconds,json=contractConfigTrackerPollIntervalSeconds,proto3" json:"contract_config_tracker_poll_interval_seconds,omitempty"`
- ContractConfigConfirmations uint32 `protobuf:"varint,12,opt,name=contract_config_confirmations,json=contractConfigConfirmations,proto3" json:"contract_config_confirmations,omitempty"`
- CaptureEaTelemetry bool `protobuf:"varint,13,opt,name=capture_ea_telemetry,json=captureEaTelemetry,proto3" json:"capture_ea_telemetry,omitempty"`
- CaptureAutomationCustomTelemetry bool `protobuf:"varint,14,opt,name=capture_automation_custom_telemetry,json=captureAutomationCustomTelemetry,proto3" json:"capture_automation_custom_telemetry,omitempty"`
- SpecCreatedAt string `protobuf:"bytes,15,opt,name=spec_created_at,json=specCreatedAt,proto3" json:"spec_created_at,omitempty"`
- SpecUpdatedAt string `protobuf:"bytes,16,opt,name=spec_updated_at,json=specUpdatedAt,proto3" json:"spec_updated_at,omitempty"`
- // Raw JSON passthroughs — always populated; authoritative over the typed
- // sub-messages below.
- RelayConfigJson string `protobuf:"bytes,17,opt,name=relay_config_json,json=relayConfigJson,proto3" json:"relay_config_json,omitempty"`
- PluginConfigJson string `protobuf:"bytes,18,opt,name=plugin_config_json,json=pluginConfigJson,proto3" json:"plugin_config_json,omitempty"`
- OnchainSigningStrategyJson string `protobuf:"bytes,19,opt,name=onchain_signing_strategy_json,json=onchainSigningStrategyJson,proto3" json:"onchain_signing_strategy_json,omitempty"`
- // Populated when relay == "evm".
- EvmRelayConfig *OCR2EVMRelayConfig `protobuf:"bytes,20,opt,name=evm_relay_config,json=evmRelayConfig,proto3" json:"evm_relay_config,omitempty"`
- // Populated when plugin_type == "median".
- MedianPluginConfig *OCR2MedianPluginConfig `protobuf:"bytes,21,opt,name=median_plugin_config,json=medianPluginConfig,proto3" json:"median_plugin_config,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
-}
-
-func (x *OCR2OracleSpecInfo) Reset() {
- *x = OCR2OracleSpecInfo{}
- mi := &file_job_spec_proto_msgTypes[1]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
-}
-
-func (x *OCR2OracleSpecInfo) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*OCR2OracleSpecInfo) ProtoMessage() {}
-
-func (x *OCR2OracleSpecInfo) ProtoReflect() protoreflect.Message {
- mi := &file_job_spec_proto_msgTypes[1]
- if x != nil {
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- if ms.LoadMessageInfo() == nil {
- ms.StoreMessageInfo(mi)
- }
- return ms
- }
- return mi.MessageOf(x)
-}
-
-// Deprecated: Use OCR2OracleSpecInfo.ProtoReflect.Descriptor instead.
-func (*OCR2OracleSpecInfo) Descriptor() ([]byte, []int) {
- return file_job_spec_proto_rawDescGZIP(), []int{1}
-}
-
-func (x *OCR2OracleSpecInfo) GetSpecId() int32 {
- if x != nil {
- return x.SpecId
- }
- return 0
-}
-
-func (x *OCR2OracleSpecInfo) GetFeedId() string {
- if x != nil {
- return x.FeedId
- }
- return ""
-}
-
-func (x *OCR2OracleSpecInfo) GetRelay() string {
- if x != nil {
- return x.Relay
- }
- return ""
-}
-
-func (x *OCR2OracleSpecInfo) GetPluginType() string {
- if x != nil {
- return x.PluginType
- }
- return ""
-}
-
-func (x *OCR2OracleSpecInfo) GetTransmitterId() string {
- if x != nil {
- return x.TransmitterId
- }
- return ""
-}
-
-func (x *OCR2OracleSpecInfo) GetOcrKeyBundleId() string {
- if x != nil {
- return x.OcrKeyBundleId
- }
- return ""
-}
-
-func (x *OCR2OracleSpecInfo) GetMonitoringEndpoint() string {
- if x != nil {
- return x.MonitoringEndpoint
- }
- return ""
-}
-
-func (x *OCR2OracleSpecInfo) GetP2Pv2Bootstrappers() []string {
- if x != nil {
- return x.P2Pv2Bootstrappers
- }
- return nil
-}
-
-func (x *OCR2OracleSpecInfo) GetAllowNoBootstrappers() bool {
- if x != nil {
- return x.AllowNoBootstrappers
- }
- return false
-}
-
-func (x *OCR2OracleSpecInfo) GetBlockchainTimeoutSeconds() float64 {
- if x != nil {
- return x.BlockchainTimeoutSeconds
- }
- return 0
-}
-
-func (x *OCR2OracleSpecInfo) GetContractConfigTrackerPollIntervalSeconds() float64 {
- if x != nil {
- return x.ContractConfigTrackerPollIntervalSeconds
- }
- return 0
-}
-
-func (x *OCR2OracleSpecInfo) GetContractConfigConfirmations() uint32 {
- if x != nil {
- return x.ContractConfigConfirmations
- }
- return 0
-}
-
-func (x *OCR2OracleSpecInfo) GetCaptureEaTelemetry() bool {
- if x != nil {
- return x.CaptureEaTelemetry
- }
- return false
-}
-
-func (x *OCR2OracleSpecInfo) GetCaptureAutomationCustomTelemetry() bool {
- if x != nil {
- return x.CaptureAutomationCustomTelemetry
- }
- return false
-}
-
-func (x *OCR2OracleSpecInfo) GetSpecCreatedAt() string {
- if x != nil {
- return x.SpecCreatedAt
- }
- return ""
-}
-
-func (x *OCR2OracleSpecInfo) GetSpecUpdatedAt() string {
- if x != nil {
- return x.SpecUpdatedAt
- }
- return ""
-}
-
-func (x *OCR2OracleSpecInfo) GetRelayConfigJson() string {
- if x != nil {
- return x.RelayConfigJson
- }
- return ""
-}
-
-func (x *OCR2OracleSpecInfo) GetPluginConfigJson() string {
- if x != nil {
- return x.PluginConfigJson
- }
- return ""
-}
-
-func (x *OCR2OracleSpecInfo) GetOnchainSigningStrategyJson() string {
- if x != nil {
- return x.OnchainSigningStrategyJson
- }
- return ""
-}
-
-func (x *OCR2OracleSpecInfo) GetEvmRelayConfig() *OCR2EVMRelayConfig {
- if x != nil {
- return x.EvmRelayConfig
- }
- return nil
-}
-
-func (x *OCR2OracleSpecInfo) GetMedianPluginConfig() *OCR2MedianPluginConfig {
- if x != nil {
- return x.MedianPluginConfig
- }
- return nil
-}
-
-// OCR2EVMRelayConfig is a typed view of the EVM relay config JSON.
-type OCR2EVMRelayConfig struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- ChainId string `protobuf:"bytes,1,opt,name=chain_id,json=chainId,proto3" json:"chain_id,omitempty"`
- FromBlock uint64 `protobuf:"varint,2,opt,name=from_block,json=fromBlock,proto3" json:"from_block,omitempty"`
- EffectiveTransmitterId string `protobuf:"bytes,3,opt,name=effective_transmitter_id,json=effectiveTransmitterId,proto3" json:"effective_transmitter_id,omitempty"`
- EnableDualTransmission bool `protobuf:"varint,4,opt,name=enable_dual_transmission,json=enableDualTransmission,proto3" json:"enable_dual_transmission,omitempty"`
- EnableTriggerCapability bool `protobuf:"varint,5,opt,name=enable_trigger_capability,json=enableTriggerCapability,proto3" json:"enable_trigger_capability,omitempty"`
- LloDonId uint64 `protobuf:"varint,6,opt,name=llo_don_id,json=lloDonId,proto3" json:"llo_don_id,omitempty"`
- FeedId string `protobuf:"bytes,7,opt,name=feed_id,json=feedId,proto3" json:"feed_id,omitempty"`
- SendingKeys []string `protobuf:"bytes,8,rep,name=sending_keys,json=sendingKeys,proto3" json:"sending_keys,omitempty"`
- ProviderType string `protobuf:"bytes,9,opt,name=provider_type,json=providerType,proto3" json:"provider_type,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
-}
-
-func (x *OCR2EVMRelayConfig) Reset() {
- *x = OCR2EVMRelayConfig{}
- mi := &file_job_spec_proto_msgTypes[2]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
-}
-
-func (x *OCR2EVMRelayConfig) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*OCR2EVMRelayConfig) ProtoMessage() {}
-
-func (x *OCR2EVMRelayConfig) ProtoReflect() protoreflect.Message {
- mi := &file_job_spec_proto_msgTypes[2]
- if x != nil {
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- if ms.LoadMessageInfo() == nil {
- ms.StoreMessageInfo(mi)
- }
- return ms
- }
- return mi.MessageOf(x)
-}
-
-// Deprecated: Use OCR2EVMRelayConfig.ProtoReflect.Descriptor instead.
-func (*OCR2EVMRelayConfig) Descriptor() ([]byte, []int) {
- return file_job_spec_proto_rawDescGZIP(), []int{2}
-}
-
-func (x *OCR2EVMRelayConfig) GetChainId() string {
- if x != nil {
- return x.ChainId
- }
- return ""
-}
-
-func (x *OCR2EVMRelayConfig) GetFromBlock() uint64 {
- if x != nil {
- return x.FromBlock
- }
- return 0
-}
-
-func (x *OCR2EVMRelayConfig) GetEffectiveTransmitterId() string {
- if x != nil {
- return x.EffectiveTransmitterId
- }
- return ""
-}
-
-func (x *OCR2EVMRelayConfig) GetEnableDualTransmission() bool {
- if x != nil {
- return x.EnableDualTransmission
- }
- return false
-}
-
-func (x *OCR2EVMRelayConfig) GetEnableTriggerCapability() bool {
- if x != nil {
- return x.EnableTriggerCapability
- }
- return false
-}
-
-func (x *OCR2EVMRelayConfig) GetLloDonId() uint64 {
- if x != nil {
- return x.LloDonId
- }
- return 0
-}
-
-func (x *OCR2EVMRelayConfig) GetFeedId() string {
- if x != nil {
- return x.FeedId
- }
- return ""
-}
-
-func (x *OCR2EVMRelayConfig) GetSendingKeys() []string {
- if x != nil {
- return x.SendingKeys
- }
- return nil
-}
-
-func (x *OCR2EVMRelayConfig) GetProviderType() string {
- if x != nil {
- return x.ProviderType
- }
- return ""
-}
-
-// OCR1OracleSpecInfo mirrors job.OCROracleSpec.
-// contract_address and evm_chain_id live on the parent JobSpecEvent.
-type OCR1OracleSpecInfo struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- SpecId int32 `protobuf:"varint,1,opt,name=spec_id,json=specId,proto3" json:"spec_id,omitempty"`
- P2Pv2Bootstrappers []string `protobuf:"bytes,2,rep,name=p2pv2_bootstrappers,json=p2pv2Bootstrappers,proto3" json:"p2pv2_bootstrappers,omitempty"`
- IsBootstrapPeer bool `protobuf:"varint,3,opt,name=is_bootstrap_peer,json=isBootstrapPeer,proto3" json:"is_bootstrap_peer,omitempty"`
- OcrKeyBundleId string `protobuf:"bytes,4,opt,name=ocr_key_bundle_id,json=ocrKeyBundleId,proto3" json:"ocr_key_bundle_id,omitempty"`
- TransmitterAddress string `protobuf:"bytes,5,opt,name=transmitter_address,json=transmitterAddress,proto3" json:"transmitter_address,omitempty"`
- ObservationTimeoutSeconds float64 `protobuf:"fixed64,6,opt,name=observation_timeout_seconds,json=observationTimeoutSeconds,proto3" json:"observation_timeout_seconds,omitempty"`
- BlockchainTimeoutSeconds float64 `protobuf:"fixed64,7,opt,name=blockchain_timeout_seconds,json=blockchainTimeoutSeconds,proto3" json:"blockchain_timeout_seconds,omitempty"`
- ContractConfigTrackerSubscribeIntervalSeconds float64 `protobuf:"fixed64,8,opt,name=contract_config_tracker_subscribe_interval_seconds,json=contractConfigTrackerSubscribeIntervalSeconds,proto3" json:"contract_config_tracker_subscribe_interval_seconds,omitempty"`
- ContractConfigTrackerPollIntervalSeconds float64 `protobuf:"fixed64,9,opt,name=contract_config_tracker_poll_interval_seconds,json=contractConfigTrackerPollIntervalSeconds,proto3" json:"contract_config_tracker_poll_interval_seconds,omitempty"`
- ContractConfigConfirmations uint32 `protobuf:"varint,10,opt,name=contract_config_confirmations,json=contractConfigConfirmations,proto3" json:"contract_config_confirmations,omitempty"`
- DatabaseTimeoutSeconds float64 `protobuf:"fixed64,11,opt,name=database_timeout_seconds,json=databaseTimeoutSeconds,proto3" json:"database_timeout_seconds,omitempty"`
- ObservationGracePeriodSeconds float64 `protobuf:"fixed64,12,opt,name=observation_grace_period_seconds,json=observationGracePeriodSeconds,proto3" json:"observation_grace_period_seconds,omitempty"`
- ContractTransmitterTransmitTimeoutSeconds float64 `protobuf:"fixed64,13,opt,name=contract_transmitter_transmit_timeout_seconds,json=contractTransmitterTransmitTimeoutSeconds,proto3" json:"contract_transmitter_transmit_timeout_seconds,omitempty"`
- CaptureEaTelemetry bool `protobuf:"varint,14,opt,name=capture_ea_telemetry,json=captureEaTelemetry,proto3" json:"capture_ea_telemetry,omitempty"`
- SpecCreatedAt string `protobuf:"bytes,15,opt,name=spec_created_at,json=specCreatedAt,proto3" json:"spec_created_at,omitempty"`
- SpecUpdatedAt string `protobuf:"bytes,16,opt,name=spec_updated_at,json=specUpdatedAt,proto3" json:"spec_updated_at,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
-}
-
-func (x *OCR1OracleSpecInfo) Reset() {
- *x = OCR1OracleSpecInfo{}
- mi := &file_job_spec_proto_msgTypes[3]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
-}
-
-func (x *OCR1OracleSpecInfo) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*OCR1OracleSpecInfo) ProtoMessage() {}
-
-func (x *OCR1OracleSpecInfo) ProtoReflect() protoreflect.Message {
- mi := &file_job_spec_proto_msgTypes[3]
- if x != nil {
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- if ms.LoadMessageInfo() == nil {
- ms.StoreMessageInfo(mi)
- }
- return ms
- }
- return mi.MessageOf(x)
-}
-
-// Deprecated: Use OCR1OracleSpecInfo.ProtoReflect.Descriptor instead.
-func (*OCR1OracleSpecInfo) Descriptor() ([]byte, []int) {
- return file_job_spec_proto_rawDescGZIP(), []int{3}
-}
-
-func (x *OCR1OracleSpecInfo) GetSpecId() int32 {
- if x != nil {
- return x.SpecId
- }
- return 0
-}
-
-func (x *OCR1OracleSpecInfo) GetP2Pv2Bootstrappers() []string {
- if x != nil {
- return x.P2Pv2Bootstrappers
- }
- return nil
-}
-
-func (x *OCR1OracleSpecInfo) GetIsBootstrapPeer() bool {
- if x != nil {
- return x.IsBootstrapPeer
- }
- return false
-}
-
-func (x *OCR1OracleSpecInfo) GetOcrKeyBundleId() string {
- if x != nil {
- return x.OcrKeyBundleId
- }
- return ""
-}
-
-func (x *OCR1OracleSpecInfo) GetTransmitterAddress() string {
- if x != nil {
- return x.TransmitterAddress
- }
- return ""
-}
-
-func (x *OCR1OracleSpecInfo) GetObservationTimeoutSeconds() float64 {
- if x != nil {
- return x.ObservationTimeoutSeconds
- }
- return 0
-}
-
-func (x *OCR1OracleSpecInfo) GetBlockchainTimeoutSeconds() float64 {
- if x != nil {
- return x.BlockchainTimeoutSeconds
- }
- return 0
-}
-
-func (x *OCR1OracleSpecInfo) GetContractConfigTrackerSubscribeIntervalSeconds() float64 {
- if x != nil {
- return x.ContractConfigTrackerSubscribeIntervalSeconds
- }
- return 0
-}
-
-func (x *OCR1OracleSpecInfo) GetContractConfigTrackerPollIntervalSeconds() float64 {
- if x != nil {
- return x.ContractConfigTrackerPollIntervalSeconds
- }
- return 0
-}
-
-func (x *OCR1OracleSpecInfo) GetContractConfigConfirmations() uint32 {
- if x != nil {
- return x.ContractConfigConfirmations
- }
- return 0
-}
-
-func (x *OCR1OracleSpecInfo) GetDatabaseTimeoutSeconds() float64 {
- if x != nil {
- return x.DatabaseTimeoutSeconds
- }
- return 0
-}
-
-func (x *OCR1OracleSpecInfo) GetObservationGracePeriodSeconds() float64 {
- if x != nil {
- return x.ObservationGracePeriodSeconds
- }
- return 0
-}
-
-func (x *OCR1OracleSpecInfo) GetContractTransmitterTransmitTimeoutSeconds() float64 {
- if x != nil {
- return x.ContractTransmitterTransmitTimeoutSeconds
- }
- return 0
-}
-
-func (x *OCR1OracleSpecInfo) GetCaptureEaTelemetry() bool {
- if x != nil {
- return x.CaptureEaTelemetry
- }
- return false
-}
-
-func (x *OCR1OracleSpecInfo) GetSpecCreatedAt() string {
- if x != nil {
- return x.SpecCreatedAt
- }
- return ""
-}
-
-func (x *OCR1OracleSpecInfo) GetSpecUpdatedAt() string {
- if x != nil {
- return x.SpecUpdatedAt
- }
- return ""
-}
-
-// OCR2MedianPluginConfig mirrors median/config.PluginConfig.
-type OCR2MedianPluginConfig struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- JuelsPerFeeCoinSource string `protobuf:"bytes,1,opt,name=juels_per_fee_coin_source,json=juelsPerFeeCoinSource,proto3" json:"juels_per_fee_coin_source,omitempty"`
- // Empty when gasPriceSubunitsSource is not configured.
- GasPriceSubunitsSource string `protobuf:"bytes,2,opt,name=gas_price_subunits_source,json=gasPriceSubunitsSource,proto3" json:"gas_price_subunits_source,omitempty"`
- // True when JuelsPerFeeCoinCache is nil or its Disable flag is set.
- JuelsPerFeeCoinCacheDisabled bool `protobuf:"varint,3,opt,name=juels_per_fee_coin_cache_disabled,json=juelsPerFeeCoinCacheDisabled,proto3" json:"juels_per_fee_coin_cache_disabled,omitempty"`
- JuelsPerFeeCoinCacheUpdateIntervalSeconds float64 `protobuf:"fixed64,4,opt,name=juels_per_fee_coin_cache_update_interval_seconds,json=juelsPerFeeCoinCacheUpdateIntervalSeconds,proto3" json:"juels_per_fee_coin_cache_update_interval_seconds,omitempty"`
- JuelsPerFeeCoinCacheStalenessAlertThresholdSeconds float64 `protobuf:"fixed64,5,opt,name=juels_per_fee_coin_cache_staleness_alert_threshold_seconds,json=juelsPerFeeCoinCacheStalenessAlertThresholdSeconds,proto3" json:"juels_per_fee_coin_cache_staleness_alert_threshold_seconds,omitempty"`
- // Verbatim JSON of DeviationFunctionDefinition.
- DeviationFuncJson string `protobuf:"bytes,6,opt,name=deviation_func_json,json=deviationFuncJson,proto3" json:"deviation_func_json,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
-}
-
-func (x *OCR2MedianPluginConfig) Reset() {
- *x = OCR2MedianPluginConfig{}
- mi := &file_job_spec_proto_msgTypes[4]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
-}
-
-func (x *OCR2MedianPluginConfig) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*OCR2MedianPluginConfig) ProtoMessage() {}
-
-func (x *OCR2MedianPluginConfig) ProtoReflect() protoreflect.Message {
- mi := &file_job_spec_proto_msgTypes[4]
- if x != nil {
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- if ms.LoadMessageInfo() == nil {
- ms.StoreMessageInfo(mi)
- }
- return ms
- }
- return mi.MessageOf(x)
-}
-
-// Deprecated: Use OCR2MedianPluginConfig.ProtoReflect.Descriptor instead.
-func (*OCR2MedianPluginConfig) Descriptor() ([]byte, []int) {
- return file_job_spec_proto_rawDescGZIP(), []int{4}
-}
-
-func (x *OCR2MedianPluginConfig) GetJuelsPerFeeCoinSource() string {
- if x != nil {
- return x.JuelsPerFeeCoinSource
- }
- return ""
-}
-
-func (x *OCR2MedianPluginConfig) GetGasPriceSubunitsSource() string {
- if x != nil {
- return x.GasPriceSubunitsSource
- }
- return ""
-}
-
-func (x *OCR2MedianPluginConfig) GetJuelsPerFeeCoinCacheDisabled() bool {
- if x != nil {
- return x.JuelsPerFeeCoinCacheDisabled
- }
- return false
-}
-
-func (x *OCR2MedianPluginConfig) GetJuelsPerFeeCoinCacheUpdateIntervalSeconds() float64 {
- if x != nil {
- return x.JuelsPerFeeCoinCacheUpdateIntervalSeconds
- }
- return 0
-}
-
-func (x *OCR2MedianPluginConfig) GetJuelsPerFeeCoinCacheStalenessAlertThresholdSeconds() float64 {
- if x != nil {
- return x.JuelsPerFeeCoinCacheStalenessAlertThresholdSeconds
- }
- return 0
-}
-
-func (x *OCR2MedianPluginConfig) GetDeviationFuncJson() string {
- if x != nil {
- return x.DeviationFuncJson
- }
- return ""
-}
-
-var File_job_spec_proto protoreflect.FileDescriptor
-
-const file_job_spec_proto_rawDesc = "" +
- "\n" +
- "\x0ejob_spec.proto\x12\vjob_spec.v1\"\x94\t\n" +
- "\fJobSpecEvent\x12&\n" +
- "\x0fexternal_job_id\x18\x01 \x01(\tR\rexternalJobId\x12&\n" +
- "\x0finternal_job_id\x18\x02 \x01(\x05R\rinternalJobId\x12\x12\n" +
- "\x04name\x18\x03 \x01(\tR\x04name\x12\x19\n" +
- "\bjob_type\x18\x04 \x01(\tR\ajobType\x12%\n" +
- "\x0eschema_version\x18\x05 \x01(\rR\rschemaVersion\x12\x1b\n" +
- "\tgas_limit\x18\x06 \x01(\rR\bgasLimit\x12-\n" +
- "\x12forwarding_allowed\x18\a \x01(\bR\x11forwardingAllowed\x12 \n" +
- "\tstream_id\x18\b \x01(\rH\x00R\bstreamId\x88\x01\x01\x129\n" +
- "\x19max_task_duration_seconds\x18\t \x01(\x01R\x16maxTaskDurationSeconds\x12\x1d\n" +
- "\n" +
- "created_at\x18\n" +
- " \x01(\tR\tcreatedAt\x12-\n" +
- "\x12observation_source\x18\v \x01(\tR\x11observationSource\x12(\n" +
- "\x10pipeline_spec_id\x18\f \x01(\x05R\x0epipelineSpecId\x12!\n" +
- "\fbridge_names\x18\r \x03(\tR\vbridgeNames\x12(\n" +
- "\x10feeds_manager_id\x18\x0e \x01(\x03R\x0efeedsManagerId\x12\x1f\n" +
- "\vremote_uuid\x18\x0f \x01(\tR\n" +
- "remoteUuid\x12!\n" +
- "\fspec_version\x18\x10 \x01(\x05R\vspecVersion\x12\x1f\n" +
- "\vproposed_at\x18\x11 \x01(\tR\n" +
- "proposedAt\x12\x1f\n" +
- "\vapproved_at\x18\x12 \x01(\tR\n" +
- "approvedAt\x124\n" +
- "\x16accept_latency_seconds\x18\x13 \x01(\x01R\x14acceptLatencySeconds\x12I\n" +
- "\x10ocr2_oracle_spec\x18\x14 \x01(\v2\x1f.job_spec.v1.OCR2OracleSpecInfoR\x0eocr2OracleSpec\x12$\n" +
- "\x0ecsa_public_key\x18\x15 \x01(\tR\fcsaPublicKey\x12!\n" +
- "\fnode_version\x18\x16 \x01(\tR\vnodeVersion\x12\x1a\n" +
- "\bhostname\x18\x17 \x01(\tR\bhostname\x12G\n" +
- "\x10emission_trigger\x18\x18 \x01(\x0e2\x1c.job_spec.v1.EmissionTriggerR\x0femissionTrigger\x12\x1c\n" +
- "\ttimestamp\x18\x19 \x01(\tR\ttimestamp\x12)\n" +
- "\x10contract_address\x18\x1a \x01(\tR\x0fcontractAddress\x12\x19\n" +
- "\bchain_id\x18\x1b \x01(\tR\achainId\x12I\n" +
- "\x10ocr1_oracle_spec\x18\x1c \x01(\v2\x1f.job_spec.v1.OCR1OracleSpecInfoR\x0eocr1OracleSpecB\f\n" +
- "\n" +
- "_stream_id\"\xda\b\n" +
- "\x12OCR2OracleSpecInfo\x12\x17\n" +
- "\aspec_id\x18\x01 \x01(\x05R\x06specId\x12\x17\n" +
- "\afeed_id\x18\x02 \x01(\tR\x06feedId\x12\x14\n" +
- "\x05relay\x18\x03 \x01(\tR\x05relay\x12\x1f\n" +
- "\vplugin_type\x18\x04 \x01(\tR\n" +
- "pluginType\x12%\n" +
- "\x0etransmitter_id\x18\x05 \x01(\tR\rtransmitterId\x12)\n" +
- "\x11ocr_key_bundle_id\x18\x06 \x01(\tR\x0eocrKeyBundleId\x12/\n" +
- "\x13monitoring_endpoint\x18\a \x01(\tR\x12monitoringEndpoint\x12/\n" +
- "\x13p2pv2_bootstrappers\x18\b \x03(\tR\x12p2pv2Bootstrappers\x124\n" +
- "\x16allow_no_bootstrappers\x18\t \x01(\bR\x14allowNoBootstrappers\x12<\n" +
- "\x1ablockchain_timeout_seconds\x18\n" +
- " \x01(\x01R\x18blockchainTimeoutSeconds\x12_\n" +
- "-contract_config_tracker_poll_interval_seconds\x18\v \x01(\x01R(contractConfigTrackerPollIntervalSeconds\x12B\n" +
- "\x1dcontract_config_confirmations\x18\f \x01(\rR\x1bcontractConfigConfirmations\x120\n" +
- "\x14capture_ea_telemetry\x18\r \x01(\bR\x12captureEaTelemetry\x12M\n" +
- "#capture_automation_custom_telemetry\x18\x0e \x01(\bR captureAutomationCustomTelemetry\x12&\n" +
- "\x0fspec_created_at\x18\x0f \x01(\tR\rspecCreatedAt\x12&\n" +
- "\x0fspec_updated_at\x18\x10 \x01(\tR\rspecUpdatedAt\x12*\n" +
- "\x11relay_config_json\x18\x11 \x01(\tR\x0frelayConfigJson\x12,\n" +
- "\x12plugin_config_json\x18\x12 \x01(\tR\x10pluginConfigJson\x12A\n" +
- "\x1donchain_signing_strategy_json\x18\x13 \x01(\tR\x1aonchainSigningStrategyJson\x12I\n" +
- "\x10evm_relay_config\x18\x14 \x01(\v2\x1f.job_spec.v1.OCR2EVMRelayConfigR\x0eevmRelayConfig\x12U\n" +
- "\x14median_plugin_config\x18\x15 \x01(\v2#.job_spec.v1.OCR2MedianPluginConfigR\x12medianPluginConfig\"\xfd\x02\n" +
- "\x12OCR2EVMRelayConfig\x12\x19\n" +
- "\bchain_id\x18\x01 \x01(\tR\achainId\x12\x1d\n" +
- "\n" +
- "from_block\x18\x02 \x01(\x04R\tfromBlock\x128\n" +
- "\x18effective_transmitter_id\x18\x03 \x01(\tR\x16effectiveTransmitterId\x128\n" +
- "\x18enable_dual_transmission\x18\x04 \x01(\bR\x16enableDualTransmission\x12:\n" +
- "\x19enable_trigger_capability\x18\x05 \x01(\bR\x17enableTriggerCapability\x12\x1c\n" +
- "\n" +
- "llo_don_id\x18\x06 \x01(\x04R\blloDonId\x12\x17\n" +
- "\afeed_id\x18\a \x01(\tR\x06feedId\x12!\n" +
- "\fsending_keys\x18\b \x03(\tR\vsendingKeys\x12#\n" +
- "\rprovider_type\x18\t \x01(\tR\fproviderType\"\xdb\a\n" +
- "\x12OCR1OracleSpecInfo\x12\x17\n" +
- "\aspec_id\x18\x01 \x01(\x05R\x06specId\x12/\n" +
- "\x13p2pv2_bootstrappers\x18\x02 \x03(\tR\x12p2pv2Bootstrappers\x12*\n" +
- "\x11is_bootstrap_peer\x18\x03 \x01(\bR\x0fisBootstrapPeer\x12)\n" +
- "\x11ocr_key_bundle_id\x18\x04 \x01(\tR\x0eocrKeyBundleId\x12/\n" +
- "\x13transmitter_address\x18\x05 \x01(\tR\x12transmitterAddress\x12>\n" +
- "\x1bobservation_timeout_seconds\x18\x06 \x01(\x01R\x19observationTimeoutSeconds\x12<\n" +
- "\x1ablockchain_timeout_seconds\x18\a \x01(\x01R\x18blockchainTimeoutSeconds\x12i\n" +
- "2contract_config_tracker_subscribe_interval_seconds\x18\b \x01(\x01R-contractConfigTrackerSubscribeIntervalSeconds\x12_\n" +
- "-contract_config_tracker_poll_interval_seconds\x18\t \x01(\x01R(contractConfigTrackerPollIntervalSeconds\x12B\n" +
- "\x1dcontract_config_confirmations\x18\n" +
- " \x01(\rR\x1bcontractConfigConfirmations\x128\n" +
- "\x18database_timeout_seconds\x18\v \x01(\x01R\x16databaseTimeoutSeconds\x12G\n" +
- " observation_grace_period_seconds\x18\f \x01(\x01R\x1dobservationGracePeriodSeconds\x12`\n" +
- "-contract_transmitter_transmit_timeout_seconds\x18\r \x01(\x01R)contractTransmitterTransmitTimeoutSeconds\x120\n" +
- "\x14capture_ea_telemetry\x18\x0e \x01(\bR\x12captureEaTelemetry\x12&\n" +
- "\x0fspec_created_at\x18\x0f \x01(\tR\rspecCreatedAt\x12&\n" +
- "\x0fspec_updated_at\x18\x10 \x01(\tR\rspecUpdatedAt\"\xe3\x03\n" +
- "\x16OCR2MedianPluginConfig\x128\n" +
- "\x19juels_per_fee_coin_source\x18\x01 \x01(\tR\x15juelsPerFeeCoinSource\x129\n" +
- "\x19gas_price_subunits_source\x18\x02 \x01(\tR\x16gasPriceSubunitsSource\x12G\n" +
- "!juels_per_fee_coin_cache_disabled\x18\x03 \x01(\bR\x1cjuelsPerFeeCoinCacheDisabled\x12c\n" +
- "0juels_per_fee_coin_cache_update_interval_seconds\x18\x04 \x01(\x01R)juelsPerFeeCoinCacheUpdateIntervalSeconds\x12v\n" +
- ":juels_per_fee_coin_cache_staleness_alert_threshold_seconds\x18\x05 \x01(\x01R2juelsPerFeeCoinCacheStalenessAlertThresholdSeconds\x12.\n" +
- "\x13deviation_func_json\x18\x06 \x01(\tR\x11deviationFuncJson*\x8d\x01\n" +
- "\x0fEmissionTrigger\x12 \n" +
- "\x1cEMISSION_TRIGGER_UNSPECIFIED\x10\x00\x12\x1e\n" +
- "\x1aEMISSION_TRIGGER_HEARTBEAT\x10\x01\x12\x1b\n" +
- "\x17EMISSION_TRIGGER_CREATE\x10\x02\x12\x1b\n" +
- "\x17EMISSION_TRIGGER_DELETE\x10\x03BZZXgithub.com/smartcontractkit/chainlink/v2/core/services/nodestatusreporter/jobspec/eventsb\x06proto3"
-
-var (
- file_job_spec_proto_rawDescOnce sync.Once
- file_job_spec_proto_rawDescData []byte
-)
-
-func file_job_spec_proto_rawDescGZIP() []byte {
- file_job_spec_proto_rawDescOnce.Do(func() {
- file_job_spec_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_job_spec_proto_rawDesc), len(file_job_spec_proto_rawDesc)))
- })
- return file_job_spec_proto_rawDescData
-}
-
-var file_job_spec_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
-var file_job_spec_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
-var file_job_spec_proto_goTypes = []any{
- (EmissionTrigger)(0), // 0: job_spec.v1.EmissionTrigger
- (*JobSpecEvent)(nil), // 1: job_spec.v1.JobSpecEvent
- (*OCR2OracleSpecInfo)(nil), // 2: job_spec.v1.OCR2OracleSpecInfo
- (*OCR2EVMRelayConfig)(nil), // 3: job_spec.v1.OCR2EVMRelayConfig
- (*OCR1OracleSpecInfo)(nil), // 4: job_spec.v1.OCR1OracleSpecInfo
- (*OCR2MedianPluginConfig)(nil), // 5: job_spec.v1.OCR2MedianPluginConfig
-}
-var file_job_spec_proto_depIdxs = []int32{
- 2, // 0: job_spec.v1.JobSpecEvent.ocr2_oracle_spec:type_name -> job_spec.v1.OCR2OracleSpecInfo
- 0, // 1: job_spec.v1.JobSpecEvent.emission_trigger:type_name -> job_spec.v1.EmissionTrigger
- 4, // 2: job_spec.v1.JobSpecEvent.ocr1_oracle_spec:type_name -> job_spec.v1.OCR1OracleSpecInfo
- 3, // 3: job_spec.v1.OCR2OracleSpecInfo.evm_relay_config:type_name -> job_spec.v1.OCR2EVMRelayConfig
- 5, // 4: job_spec.v1.OCR2OracleSpecInfo.median_plugin_config:type_name -> job_spec.v1.OCR2MedianPluginConfig
- 5, // [5:5] is the sub-list for method output_type
- 5, // [5:5] is the sub-list for method input_type
- 5, // [5:5] is the sub-list for extension type_name
- 5, // [5:5] is the sub-list for extension extendee
- 0, // [0:5] is the sub-list for field type_name
-}
-
-func init() { file_job_spec_proto_init() }
-func file_job_spec_proto_init() {
- if File_job_spec_proto != nil {
- return
- }
- file_job_spec_proto_msgTypes[0].OneofWrappers = []any{}
- type x struct{}
- out := protoimpl.TypeBuilder{
- File: protoimpl.DescBuilder{
- GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
- RawDescriptor: unsafe.Slice(unsafe.StringData(file_job_spec_proto_rawDesc), len(file_job_spec_proto_rawDesc)),
- NumEnums: 1,
- NumMessages: 5,
- NumExtensions: 0,
- NumServices: 0,
- },
- GoTypes: file_job_spec_proto_goTypes,
- DependencyIndexes: file_job_spec_proto_depIdxs,
- EnumInfos: file_job_spec_proto_enumTypes,
- MessageInfos: file_job_spec_proto_msgTypes,
- }.Build()
- File_job_spec_proto = out.File
- file_job_spec_proto_goTypes = nil
- file_job_spec_proto_depIdxs = nil
-}
+type JobSpecEvent = job_specv1.JobSpecEvent
+type OCR1OracleSpecInfo = job_specv1.OCR1OracleSpecInfo
+type OCR2EVMRelayConfig = job_specv1.OCR2EVMRelayConfig
+type OCR2MedianPluginConfig = job_specv1.OCR2MedianPluginConfig
+type OCR2OracleSpecInfo = job_specv1.OCR2OracleSpecInfo
diff --git a/core/services/nodestatusreporter/jobspec/events/job_spec.proto b/core/services/nodestatusreporter/jobspec/events/job_spec.proto
index 3d27140986a..ad09b38d192 100644
--- a/core/services/nodestatusreporter/jobspec/events/job_spec.proto
+++ b/core/services/nodestatusreporter/jobspec/events/job_spec.proto
@@ -1,146 +1,11 @@
syntax = "proto3";
-option go_package = "github.com/smartcontractkit/chainlink/v2/core/services/nodestatusreporter/jobspec/events";
-
package job_spec.v1;
-// JobSpecEvent carries a job's spec, emitted on heartbeat, create, and delete.
-message JobSpecEvent {
- // Job identity
- string external_job_id = 1;
- int32 internal_job_id = 2;
- string name = 3;
- string job_type = 4;
- uint32 schema_version = 5;
- uint32 gas_limit = 6;
- bool forwarding_allowed = 7;
- optional uint32 stream_id = 8;
- double max_task_duration_seconds = 9;
- string created_at = 10;
-
- // Observation pipeline
- string observation_source = 11;
- int32 pipeline_spec_id = 12;
-
- // Top-level bridge names in the observation pipeline.
- repeated string bridge_names = 13;
-
- // Proposal lifecycle — zero/empty for jobs not managed by a Feeds Manager.
- int64 feeds_manager_id = 14;
- string remote_uuid = 15;
- int32 spec_version = 16;
- string proposed_at = 17;
- string approved_at = 18;
- double accept_latency_seconds = 19;
-
- // OCR2-only; absent for other job types.
- OCR2OracleSpecInfo ocr2_oracle_spec = 20;
-
- // Node identity
- string csa_public_key = 21;
- string node_version = 22;
- string hostname = 23;
-
- // Event metadata
- EmissionTrigger emission_trigger = 24;
- string timestamp = 25;
-
- // Primary on-chain contract — populated for single-contract job types
- // (OCR1, OCR2, Flux Monitor, Keeper).
- string contract_address = 26;
- string chain_id = 27;
-
- // OCR1-only; absent for other job types.
- OCR1OracleSpecInfo ocr1_oracle_spec = 28;
-}
-
-// EmissionTrigger is the reason a JobSpecEvent was emitted.
-enum EmissionTrigger {
- EMISSION_TRIGGER_UNSPECIFIED = 0;
- EMISSION_TRIGGER_HEARTBEAT = 1;
- EMISSION_TRIGGER_CREATE = 2;
- EMISSION_TRIGGER_DELETE = 3;
-}
+import public "job_spec/v1/job_spec_event.proto";
+import public "job_spec/v1/ocr1_oracle_spec_info.proto";
+import public "job_spec/v1/ocr2_evm_relay_config.proto";
+import public "job_spec/v1/ocr2_median_plugin_config.proto";
+import public "job_spec/v1/ocr2_oracle_spec_info.proto";
-// OCR2OracleSpecInfo mirrors job.OCR2OracleSpec.
-// contract_id and chain_id live on the parent JobSpecEvent.
-message OCR2OracleSpecInfo {
- int32 spec_id = 1;
- string feed_id = 2;
- string relay = 3;
- string plugin_type = 4;
- string transmitter_id = 5;
- string ocr_key_bundle_id = 6;
- string monitoring_endpoint = 7;
- repeated string p2pv2_bootstrappers = 8;
- bool allow_no_bootstrappers = 9;
- double blockchain_timeout_seconds = 10;
- double contract_config_tracker_poll_interval_seconds = 11;
- uint32 contract_config_confirmations = 12;
- bool capture_ea_telemetry = 13;
- bool capture_automation_custom_telemetry = 14;
- string spec_created_at = 15;
- string spec_updated_at = 16;
-
- // Raw JSON passthroughs — always populated; authoritative over the typed
- // sub-messages below.
- string relay_config_json = 17;
- string plugin_config_json = 18;
- string onchain_signing_strategy_json = 19;
-
- // Populated when relay == "evm".
- OCR2EVMRelayConfig evm_relay_config = 20;
-
- // Populated when plugin_type == "median".
- OCR2MedianPluginConfig median_plugin_config = 21;
-}
-
-// OCR2EVMRelayConfig is a typed view of the EVM relay config JSON.
-message OCR2EVMRelayConfig {
- string chain_id = 1;
- uint64 from_block = 2;
- string effective_transmitter_id = 3;
- bool enable_dual_transmission = 4;
- bool enable_trigger_capability = 5;
- uint64 llo_don_id = 6;
- string feed_id = 7;
- repeated string sending_keys = 8;
- string provider_type = 9;
-}
-
-// OCR1OracleSpecInfo mirrors job.OCROracleSpec.
-// contract_address and evm_chain_id live on the parent JobSpecEvent.
-message OCR1OracleSpecInfo {
- int32 spec_id = 1;
- repeated string p2pv2_bootstrappers = 2;
- bool is_bootstrap_peer = 3;
- string ocr_key_bundle_id = 4;
- string transmitter_address = 5;
- double observation_timeout_seconds = 6;
- double blockchain_timeout_seconds = 7;
- double contract_config_tracker_subscribe_interval_seconds = 8;
- double contract_config_tracker_poll_interval_seconds = 9;
- uint32 contract_config_confirmations = 10;
- double database_timeout_seconds = 11;
- double observation_grace_period_seconds = 12;
- double contract_transmitter_transmit_timeout_seconds = 13;
- bool capture_ea_telemetry = 14;
- string spec_created_at = 15;
- string spec_updated_at = 16;
-}
-
-// OCR2MedianPluginConfig mirrors median/config.PluginConfig.
-message OCR2MedianPluginConfig {
- string juels_per_fee_coin_source = 1;
-
- // Empty when gasPriceSubunitsSource is not configured.
- string gas_price_subunits_source = 2;
-
- // True when JuelsPerFeeCoinCache is nil or its Disable flag is set.
- bool juels_per_fee_coin_cache_disabled = 3;
- double juels_per_fee_coin_cache_update_interval_seconds = 4;
- double juels_per_fee_coin_cache_staleness_alert_threshold_seconds = 5;
-
- // Verbatim JSON of DeviationFunctionDefinition.
- string deviation_func_json = 6;
-}
+option go_package = "github.com/smartcontractkit/chainlink/v2/core/services/nodestatusreporter/jobspec/events";
diff --git a/go.mod b/go.mod
index b22788f12cb..8599fbd5ae0 100644
--- a/go.mod
+++ b/go.mod
@@ -99,6 +99,7 @@ require (
github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260410144512-ca02ad6ed16a
github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260409211238-5b99921cbc7c
+ github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b
github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785
github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0
diff --git a/go.sum b/go.sum
index a2e4440ef63..42adcc02bab 100644
--- a/go.sum
+++ b/go.sum
@@ -1277,6 +1277,8 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:5JdppgngCOUS76p61zCinSCgOhPeYQ+OcDUuome5THQ=
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260409211238-5b99921cbc7c h1:7V+V3R//Z/g1kiz/to9MGT4KmcSTcbv2YSsRDD0RN5A=
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260409211238-5b99921cbc7c/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8=
+github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432 h1:qKM3C6m+Hnaekp4LvZcMwmB6t3g3nhUU8RYu1w9e0mI=
+github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432/go.mod h1:vL1bDgPSJjV0EqHYs4dDlR+EEE0cJchgvGLYXhwIjXY=
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM=
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b/go.mod h1:qSTSwX3cBP3FKQwQacdjArqv0g6QnukjV4XuzO6UyoY=
github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785 h1:oli+2uLU6jcrJGCuYFqk3475hiwL17SWlITWLv+tx/w=
From 8f1b98ea826c4df0afa05a9e2923df9fd1c9adfd Mon Sep 17 00:00:00 2001
From: thomjg <103059015+thomjg@users.noreply.github.com>
Date: Mon, 27 Apr 2026 17:31:53 -0400
Subject: [PATCH 13/14] Add data-feeds proto module deps
---
core/scripts/go.mod | 1 +
core/scripts/go.sum | 2 ++
deployment/go.mod | 1 +
deployment/go.sum | 2 ++
go.md | 8 ++++++++
integration-tests/go.mod | 1 +
integration-tests/go.sum | 2 ++
integration-tests/load/go.mod | 1 +
integration-tests/load/go.sum | 2 ++
system-tests/lib/go.mod | 1 +
system-tests/lib/go.sum | 2 ++
system-tests/tests/go.mod | 1 +
system-tests/tests/go.sum | 2 ++
13 files changed, 26 insertions(+)
diff --git a/core/scripts/go.mod b/core/scripts/go.mod
index 024dcdd64dc..a60102dd865 100644
--- a/core/scripts/go.mod
+++ b/core/scripts/go.mod
@@ -506,6 +506,7 @@ require (
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/heartbeat v0.0.0-20260115142640-f6b99095c12e // indirect
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0.0.0-20251211142334-5c3421fe2c8d // indirect
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d // indirect
+ github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432 // indirect
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect
github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785 // indirect
github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 // indirect
diff --git a/core/scripts/go.sum b/core/scripts/go.sum
index 0359a597ff4..73c27311699 100644
--- a/core/scripts/go.sum
+++ b/core/scripts/go.sum
@@ -1692,6 +1692,8 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:5JdppgngCOUS76p61zCinSCgOhPeYQ+OcDUuome5THQ=
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877 h1:6UueUIbck1Ogarm9rm/9TS6b09mKgMmx+YE8XFg63AQ=
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8=
+github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432 h1:qKM3C6m+Hnaekp4LvZcMwmB6t3g3nhUU8RYu1w9e0mI=
+github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432/go.mod h1:vL1bDgPSJjV0EqHYs4dDlR+EEE0cJchgvGLYXhwIjXY=
github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 h1:q+VDPcxWrj5k9QizSYfUOSMnDH3Sd5HvbPguZOgfXTY=
github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0/go.mod h1:/dVVLXrsp+V0AbcYGJo3XMzKg3CkELsweA/TTopCsKE=
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM=
diff --git a/deployment/go.mod b/deployment/go.mod
index afe257dad5c..4ba1980e847 100644
--- a/deployment/go.mod
+++ b/deployment/go.mod
@@ -436,6 +436,7 @@ require (
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/heartbeat v0.0.0-20260115142640-f6b99095c12e // indirect
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0.0.0-20251211142334-5c3421fe2c8d // indirect
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d // indirect
+ github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432 // indirect
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect
github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785 // indirect
github.com/smartcontractkit/chainlink-protos/op-catalog v0.0.4 // indirect
diff --git a/deployment/go.sum b/deployment/go.sum
index de265950030..41249fab250 100644
--- a/deployment/go.sum
+++ b/deployment/go.sum
@@ -1436,6 +1436,8 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:5JdppgngCOUS76p61zCinSCgOhPeYQ+OcDUuome5THQ=
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877 h1:6UueUIbck1Ogarm9rm/9TS6b09mKgMmx+YE8XFg63AQ=
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8=
+github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432 h1:qKM3C6m+Hnaekp4LvZcMwmB6t3g3nhUU8RYu1w9e0mI=
+github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432/go.mod h1:vL1bDgPSJjV0EqHYs4dDlR+EEE0cJchgvGLYXhwIjXY=
github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 h1:q+VDPcxWrj5k9QizSYfUOSMnDH3Sd5HvbPguZOgfXTY=
github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0/go.mod h1:/dVVLXrsp+V0AbcYGJo3XMzKg3CkELsweA/TTopCsKE=
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM=
diff --git a/go.md b/go.md
index 6af15e44716..b139573b8be 100644
--- a/go.md
+++ b/go.md
@@ -124,6 +124,8 @@ flowchart LR
click chainlink-protos/chainlink-ccv/verifier href "https://github.com/smartcontractkit/chainlink-protos"
chainlink-protos/cre/go --> chain-selectors
click chainlink-protos/cre/go href "https://github.com/smartcontractkit/chainlink-protos"
+ chainlink-protos/data-feeds
+ click chainlink-protos/data-feeds href "https://github.com/smartcontractkit/chainlink-protos"
chainlink-protos/job-distributor
click chainlink-protos/job-distributor href "https://github.com/smartcontractkit/chainlink-protos"
chainlink-protos/linking-service/go
@@ -168,6 +170,7 @@ flowchart LR
chainlink/v2 --> chainlink-ccv
chainlink/v2 --> chainlink-evm/contracts/cre/gobindings
chainlink/v2 --> chainlink-feeds
+ chainlink/v2 --> chainlink-protos/data-feeds
chainlink/v2 --> chainlink-protos/ring/go
chainlink/v2 --> cre-sdk-go/capabilities/networking/http
chainlink/v2 --> cre-sdk-go/capabilities/scheduler/cron
@@ -244,6 +247,7 @@ flowchart LR
chainlink-protos/chainlink-ccv/message-discovery
chainlink-protos/chainlink-ccv/verifier
chainlink-protos/cre/go
+ chainlink-protos/data-feeds
chainlink-protos/job-distributor
chainlink-protos/linking-service/go
chainlink-protos/node-platform
@@ -402,6 +406,8 @@ flowchart LR
click chainlink-protos/chainlink-ccv/verifier href "https://github.com/smartcontractkit/chainlink-protos"
chainlink-protos/cre/go --> chain-selectors
click chainlink-protos/cre/go href "https://github.com/smartcontractkit/chainlink-protos"
+ chainlink-protos/data-feeds
+ click chainlink-protos/data-feeds href "https://github.com/smartcontractkit/chainlink-protos"
chainlink-protos/job-distributor
click chainlink-protos/job-distributor href "https://github.com/smartcontractkit/chainlink-protos"
chainlink-protos/linking-service/go
@@ -570,6 +576,7 @@ flowchart LR
chainlink/v2 --> chainlink-ccv
chainlink/v2 --> chainlink-evm/contracts/cre/gobindings
chainlink/v2 --> chainlink-feeds
+ chainlink/v2 --> chainlink-protos/data-feeds
chainlink/v2 --> chainlink-protos/ring/go
chainlink/v2 --> cre-sdk-go/capabilities/networking/http
chainlink/v2 --> cre-sdk-go/capabilities/scheduler/cron
@@ -689,6 +696,7 @@ flowchart LR
chainlink-protos/chainlink-ccv/message-discovery
chainlink-protos/chainlink-ccv/verifier
chainlink-protos/cre/go
+ chainlink-protos/data-feeds
chainlink-protos/job-distributor
chainlink-protos/linking-service/go
chainlink-protos/node-platform
diff --git a/integration-tests/go.mod b/integration-tests/go.mod
index b0262691278..0a4568e24fc 100644
--- a/integration-tests/go.mod
+++ b/integration-tests/go.mod
@@ -419,6 +419,7 @@ require (
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0.0.0-20251211142334-5c3421fe2c8d // indirect
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d // indirect
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877 // indirect
+ github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432 // indirect
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect
github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785 // indirect
github.com/smartcontractkit/chainlink-protos/op-catalog v0.0.4 // indirect
diff --git a/integration-tests/go.sum b/integration-tests/go.sum
index d7ea502e549..4de48a2e074 100644
--- a/integration-tests/go.sum
+++ b/integration-tests/go.sum
@@ -1421,6 +1421,8 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:5JdppgngCOUS76p61zCinSCgOhPeYQ+OcDUuome5THQ=
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877 h1:6UueUIbck1Ogarm9rm/9TS6b09mKgMmx+YE8XFg63AQ=
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8=
+github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432 h1:qKM3C6m+Hnaekp4LvZcMwmB6t3g3nhUU8RYu1w9e0mI=
+github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432/go.mod h1:vL1bDgPSJjV0EqHYs4dDlR+EEE0cJchgvGLYXhwIjXY=
github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 h1:q+VDPcxWrj5k9QizSYfUOSMnDH3Sd5HvbPguZOgfXTY=
github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0/go.mod h1:/dVVLXrsp+V0AbcYGJo3XMzKg3CkELsweA/TTopCsKE=
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM=
diff --git a/integration-tests/load/go.mod b/integration-tests/load/go.mod
index b317f1a5688..771ad09bb1c 100644
--- a/integration-tests/load/go.mod
+++ b/integration-tests/load/go.mod
@@ -497,6 +497,7 @@ require (
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0.0.0-20251211142334-5c3421fe2c8d // indirect
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d // indirect
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877 // indirect
+ github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432 // indirect
github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 // indirect
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect
github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785 // indirect
diff --git a/integration-tests/load/go.sum b/integration-tests/load/go.sum
index 6f2865428ce..f2770cc2317 100644
--- a/integration-tests/load/go.sum
+++ b/integration-tests/load/go.sum
@@ -1689,6 +1689,8 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:5JdppgngCOUS76p61zCinSCgOhPeYQ+OcDUuome5THQ=
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877 h1:6UueUIbck1Ogarm9rm/9TS6b09mKgMmx+YE8XFg63AQ=
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8=
+github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432 h1:qKM3C6m+Hnaekp4LvZcMwmB6t3g3nhUU8RYu1w9e0mI=
+github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432/go.mod h1:vL1bDgPSJjV0EqHYs4dDlR+EEE0cJchgvGLYXhwIjXY=
github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 h1:q+VDPcxWrj5k9QizSYfUOSMnDH3Sd5HvbPguZOgfXTY=
github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0/go.mod h1:/dVVLXrsp+V0AbcYGJo3XMzKg3CkELsweA/TTopCsKE=
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM=
diff --git a/system-tests/lib/go.mod b/system-tests/lib/go.mod
index ce013d03e53..d4bb7f0db94 100644
--- a/system-tests/lib/go.mod
+++ b/system-tests/lib/go.mod
@@ -473,6 +473,7 @@ require (
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/heartbeat v0.0.0-20260115142640-f6b99095c12e // indirect
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0.0.0-20251211142334-5c3421fe2c8d // indirect
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d // indirect
+ github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432 // indirect
github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785 // indirect
github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 // indirect
github.com/smartcontractkit/chainlink-protos/ring/go v0.0.0-20260331131315-f08a616d8dcd // indirect
diff --git a/system-tests/lib/go.sum b/system-tests/lib/go.sum
index ec1c7e40f4b..ce3a9557cac 100644
--- a/system-tests/lib/go.sum
+++ b/system-tests/lib/go.sum
@@ -1657,6 +1657,8 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:5JdppgngCOUS76p61zCinSCgOhPeYQ+OcDUuome5THQ=
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877 h1:6UueUIbck1Ogarm9rm/9TS6b09mKgMmx+YE8XFg63AQ=
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8=
+github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432 h1:qKM3C6m+Hnaekp4LvZcMwmB6t3g3nhUU8RYu1w9e0mI=
+github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432/go.mod h1:vL1bDgPSJjV0EqHYs4dDlR+EEE0cJchgvGLYXhwIjXY=
github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 h1:q+VDPcxWrj5k9QizSYfUOSMnDH3Sd5HvbPguZOgfXTY=
github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0/go.mod h1:/dVVLXrsp+V0AbcYGJo3XMzKg3CkELsweA/TTopCsKE=
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM=
diff --git a/system-tests/tests/go.mod b/system-tests/tests/go.mod
index a4d40d8d283..15dcf373acd 100644
--- a/system-tests/tests/go.mod
+++ b/system-tests/tests/go.mod
@@ -156,6 +156,7 @@ require (
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/heartbeat v0.0.0-20260115142640-f6b99095c12e // indirect
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0.0.0-20251211142334-5c3421fe2c8d // indirect
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d // indirect
+ github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432 // indirect
github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785 // indirect
github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260421131224-c46cbfe7bc6c // indirect
github.com/smartcontractkit/chainlink-solana/contracts v0.0.0-20260421131224-c46cbfe7bc6c // indirect
diff --git a/system-tests/tests/go.sum b/system-tests/tests/go.sum
index ca1d6d96b66..8a385ce9044 100644
--- a/system-tests/tests/go.sum
+++ b/system-tests/tests/go.sum
@@ -1872,6 +1872,8 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:5JdppgngCOUS76p61zCinSCgOhPeYQ+OcDUuome5THQ=
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877 h1:6UueUIbck1Ogarm9rm/9TS6b09mKgMmx+YE8XFg63AQ=
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8=
+github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432 h1:qKM3C6m+Hnaekp4LvZcMwmB6t3g3nhUU8RYu1w9e0mI=
+github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432/go.mod h1:vL1bDgPSJjV0EqHYs4dDlR+EEE0cJchgvGLYXhwIjXY=
github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 h1:q+VDPcxWrj5k9QizSYfUOSMnDH3Sd5HvbPguZOgfXTY=
github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0/go.mod h1:/dVVLXrsp+V0AbcYGJo3XMzKg3CkELsweA/TTopCsKE=
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM=
From a0889cba8f352e3481a12a4db3adbe6e997c6c52 Mon Sep 17 00:00:00 2001
From: thomjg <103059015+thomjg@users.noreply.github.com>
Date: Tue, 28 Apr 2026 21:15:57 -0400
Subject: [PATCH 14/14] Updating source for job_spec protos
---
core/scripts/go.mod | 2 +-
core/scripts/go.sum | 4 ++--
core/services/nodestatusreporter/jobspec/events/emit.go | 3 ++-
core/services/nodestatusreporter/jobspec/events/emit_test.go | 3 ++-
core/services/nodestatusreporter/jobspec/events/types.go | 2 ++
deployment/go.mod | 2 +-
deployment/go.sum | 4 ++--
go.mod | 2 +-
go.sum | 4 ++--
integration-tests/go.mod | 2 +-
integration-tests/go.sum | 4 ++--
integration-tests/load/go.mod | 2 +-
integration-tests/load/go.sum | 4 ++--
system-tests/lib/go.mod | 2 +-
system-tests/lib/go.sum | 4 ++--
system-tests/tests/go.mod | 2 +-
system-tests/tests/go.sum | 4 ++--
17 files changed, 27 insertions(+), 23 deletions(-)
diff --git a/core/scripts/go.mod b/core/scripts/go.mod
index a60102dd865..df562344156 100644
--- a/core/scripts/go.mod
+++ b/core/scripts/go.mod
@@ -506,7 +506,7 @@ require (
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/heartbeat v0.0.0-20260115142640-f6b99095c12e // indirect
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0.0.0-20251211142334-5c3421fe2c8d // indirect
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d // indirect
- github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432 // indirect
+ github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260428231113-4e8d71d4ba0a // indirect
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect
github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785 // indirect
github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 // indirect
diff --git a/core/scripts/go.sum b/core/scripts/go.sum
index 73c27311699..1ed268bdc8d 100644
--- a/core/scripts/go.sum
+++ b/core/scripts/go.sum
@@ -1692,8 +1692,8 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:5JdppgngCOUS76p61zCinSCgOhPeYQ+OcDUuome5THQ=
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877 h1:6UueUIbck1Ogarm9rm/9TS6b09mKgMmx+YE8XFg63AQ=
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8=
-github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432 h1:qKM3C6m+Hnaekp4LvZcMwmB6t3g3nhUU8RYu1w9e0mI=
-github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432/go.mod h1:vL1bDgPSJjV0EqHYs4dDlR+EEE0cJchgvGLYXhwIjXY=
+github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260428231113-4e8d71d4ba0a h1:UPejHeV2qjuZxc9TLa6d0q1KbC9oW29eBWFSynVvXv8=
+github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260428231113-4e8d71d4ba0a/go.mod h1:vL1bDgPSJjV0EqHYs4dDlR+EEE0cJchgvGLYXhwIjXY=
github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 h1:q+VDPcxWrj5k9QizSYfUOSMnDH3Sd5HvbPguZOgfXTY=
github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0/go.mod h1:/dVVLXrsp+V0AbcYGJo3XMzKg3CkELsweA/TTopCsKE=
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM=
diff --git a/core/services/nodestatusreporter/jobspec/events/emit.go b/core/services/nodestatusreporter/jobspec/events/emit.go
index e567983a0f3..d7500160773 100644
--- a/core/services/nodestatusreporter/jobspec/events/emit.go
+++ b/core/services/nodestatusreporter/jobspec/events/emit.go
@@ -21,8 +21,9 @@ func EmitJobSpecEvent(ctx context.Context, emitter beholder.Emitter, event *JobS
}
err = emitter.Emit(ctx, eventBytes,
+ "source", EventSource,
"beholder_data_schema", SchemaJobSpec,
- "beholder_domain", "data-feeds",
+ "beholder_domain", BeholderDomain,
"beholder_entity", fmt.Sprintf("%s.%s", ProtoPkg, JobSpecEventEntity),
)
if err != nil {
diff --git a/core/services/nodestatusreporter/jobspec/events/emit_test.go b/core/services/nodestatusreporter/jobspec/events/emit_test.go
index b311fa52ca8..5902a6c14e5 100644
--- a/core/services/nodestatusreporter/jobspec/events/emit_test.go
+++ b/core/services/nodestatusreporter/jobspec/events/emit_test.go
@@ -34,7 +34,8 @@ func TestEmitJobSpecEvent_RoundTrip(t *testing.T) {
msg := msgs[0]
require.Equal(t, events.SchemaJobSpec, msg.Attrs["beholder_data_schema"])
- require.Equal(t, "data-feeds", msg.Attrs["beholder_domain"])
+ require.Equal(t, events.BeholderDomain, msg.Attrs["beholder_domain"])
+ require.Equal(t, events.EventSource, msg.Attrs["source"])
var decoded events.JobSpecEvent
require.NoError(t, proto.Unmarshal(msg.Body, &decoded))
diff --git a/core/services/nodestatusreporter/jobspec/events/types.go b/core/services/nodestatusreporter/jobspec/events/types.go
index 4d7d7537247..68f012a9b05 100644
--- a/core/services/nodestatusreporter/jobspec/events/types.go
+++ b/core/services/nodestatusreporter/jobspec/events/types.go
@@ -4,4 +4,6 @@ const (
ProtoPkg = "job_spec.v1"
JobSpecEventEntity = "JobSpecEvent"
SchemaJobSpec = "/job-spec-events/v1"
+ EventSource = "chainlink-protos"
+ BeholderDomain = "data-feeds.job-spec"
)
diff --git a/deployment/go.mod b/deployment/go.mod
index 4ba1980e847..737451bc523 100644
--- a/deployment/go.mod
+++ b/deployment/go.mod
@@ -436,7 +436,7 @@ require (
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/heartbeat v0.0.0-20260115142640-f6b99095c12e // indirect
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0.0.0-20251211142334-5c3421fe2c8d // indirect
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d // indirect
- github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432 // indirect
+ github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260428231113-4e8d71d4ba0a // indirect
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect
github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785 // indirect
github.com/smartcontractkit/chainlink-protos/op-catalog v0.0.4 // indirect
diff --git a/deployment/go.sum b/deployment/go.sum
index 41249fab250..1b058cb711c 100644
--- a/deployment/go.sum
+++ b/deployment/go.sum
@@ -1436,8 +1436,8 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:5JdppgngCOUS76p61zCinSCgOhPeYQ+OcDUuome5THQ=
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877 h1:6UueUIbck1Ogarm9rm/9TS6b09mKgMmx+YE8XFg63AQ=
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8=
-github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432 h1:qKM3C6m+Hnaekp4LvZcMwmB6t3g3nhUU8RYu1w9e0mI=
-github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432/go.mod h1:vL1bDgPSJjV0EqHYs4dDlR+EEE0cJchgvGLYXhwIjXY=
+github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260428231113-4e8d71d4ba0a h1:UPejHeV2qjuZxc9TLa6d0q1KbC9oW29eBWFSynVvXv8=
+github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260428231113-4e8d71d4ba0a/go.mod h1:vL1bDgPSJjV0EqHYs4dDlR+EEE0cJchgvGLYXhwIjXY=
github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 h1:q+VDPcxWrj5k9QizSYfUOSMnDH3Sd5HvbPguZOgfXTY=
github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0/go.mod h1:/dVVLXrsp+V0AbcYGJo3XMzKg3CkELsweA/TTopCsKE=
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM=
diff --git a/go.mod b/go.mod
index a5f0fbb9860..6e6ff7eb5da 100644
--- a/go.mod
+++ b/go.mod
@@ -97,7 +97,7 @@ require (
github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260410144512-ca02ad6ed16a
github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877
- github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432
+ github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260428231113-4e8d71d4ba0a
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b
github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785
github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0
diff --git a/go.sum b/go.sum
index 323793a51a3..1159f6f9b88 100644
--- a/go.sum
+++ b/go.sum
@@ -1282,8 +1282,8 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:5JdppgngCOUS76p61zCinSCgOhPeYQ+OcDUuome5THQ=
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877 h1:6UueUIbck1Ogarm9rm/9TS6b09mKgMmx+YE8XFg63AQ=
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8=
-github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432 h1:qKM3C6m+Hnaekp4LvZcMwmB6t3g3nhUU8RYu1w9e0mI=
-github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432/go.mod h1:vL1bDgPSJjV0EqHYs4dDlR+EEE0cJchgvGLYXhwIjXY=
+github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260428231113-4e8d71d4ba0a h1:UPejHeV2qjuZxc9TLa6d0q1KbC9oW29eBWFSynVvXv8=
+github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260428231113-4e8d71d4ba0a/go.mod h1:vL1bDgPSJjV0EqHYs4dDlR+EEE0cJchgvGLYXhwIjXY=
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM=
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b/go.mod h1:qSTSwX3cBP3FKQwQacdjArqv0g6QnukjV4XuzO6UyoY=
github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785 h1:oli+2uLU6jcrJGCuYFqk3475hiwL17SWlITWLv+tx/w=
diff --git a/integration-tests/go.mod b/integration-tests/go.mod
index 0a4568e24fc..cad902b64b5 100644
--- a/integration-tests/go.mod
+++ b/integration-tests/go.mod
@@ -419,7 +419,7 @@ require (
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0.0.0-20251211142334-5c3421fe2c8d // indirect
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d // indirect
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877 // indirect
- github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432 // indirect
+ github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260428231113-4e8d71d4ba0a // indirect
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect
github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785 // indirect
github.com/smartcontractkit/chainlink-protos/op-catalog v0.0.4 // indirect
diff --git a/integration-tests/go.sum b/integration-tests/go.sum
index 4de48a2e074..11d25557baa 100644
--- a/integration-tests/go.sum
+++ b/integration-tests/go.sum
@@ -1421,8 +1421,8 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:5JdppgngCOUS76p61zCinSCgOhPeYQ+OcDUuome5THQ=
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877 h1:6UueUIbck1Ogarm9rm/9TS6b09mKgMmx+YE8XFg63AQ=
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8=
-github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432 h1:qKM3C6m+Hnaekp4LvZcMwmB6t3g3nhUU8RYu1w9e0mI=
-github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432/go.mod h1:vL1bDgPSJjV0EqHYs4dDlR+EEE0cJchgvGLYXhwIjXY=
+github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260428231113-4e8d71d4ba0a h1:UPejHeV2qjuZxc9TLa6d0q1KbC9oW29eBWFSynVvXv8=
+github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260428231113-4e8d71d4ba0a/go.mod h1:vL1bDgPSJjV0EqHYs4dDlR+EEE0cJchgvGLYXhwIjXY=
github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 h1:q+VDPcxWrj5k9QizSYfUOSMnDH3Sd5HvbPguZOgfXTY=
github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0/go.mod h1:/dVVLXrsp+V0AbcYGJo3XMzKg3CkELsweA/TTopCsKE=
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM=
diff --git a/integration-tests/load/go.mod b/integration-tests/load/go.mod
index 771ad09bb1c..7b7d4b0b0f1 100644
--- a/integration-tests/load/go.mod
+++ b/integration-tests/load/go.mod
@@ -497,7 +497,7 @@ require (
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0.0.0-20251211142334-5c3421fe2c8d // indirect
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d // indirect
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877 // indirect
- github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432 // indirect
+ github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260428231113-4e8d71d4ba0a // indirect
github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 // indirect
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect
github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785 // indirect
diff --git a/integration-tests/load/go.sum b/integration-tests/load/go.sum
index f2770cc2317..646277ace70 100644
--- a/integration-tests/load/go.sum
+++ b/integration-tests/load/go.sum
@@ -1689,8 +1689,8 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:5JdppgngCOUS76p61zCinSCgOhPeYQ+OcDUuome5THQ=
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877 h1:6UueUIbck1Ogarm9rm/9TS6b09mKgMmx+YE8XFg63AQ=
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8=
-github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432 h1:qKM3C6m+Hnaekp4LvZcMwmB6t3g3nhUU8RYu1w9e0mI=
-github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432/go.mod h1:vL1bDgPSJjV0EqHYs4dDlR+EEE0cJchgvGLYXhwIjXY=
+github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260428231113-4e8d71d4ba0a h1:UPejHeV2qjuZxc9TLa6d0q1KbC9oW29eBWFSynVvXv8=
+github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260428231113-4e8d71d4ba0a/go.mod h1:vL1bDgPSJjV0EqHYs4dDlR+EEE0cJchgvGLYXhwIjXY=
github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 h1:q+VDPcxWrj5k9QizSYfUOSMnDH3Sd5HvbPguZOgfXTY=
github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0/go.mod h1:/dVVLXrsp+V0AbcYGJo3XMzKg3CkELsweA/TTopCsKE=
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM=
diff --git a/system-tests/lib/go.mod b/system-tests/lib/go.mod
index d4bb7f0db94..08791a08dd8 100644
--- a/system-tests/lib/go.mod
+++ b/system-tests/lib/go.mod
@@ -473,7 +473,7 @@ require (
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/heartbeat v0.0.0-20260115142640-f6b99095c12e // indirect
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0.0.0-20251211142334-5c3421fe2c8d // indirect
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d // indirect
- github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432 // indirect
+ github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260428231113-4e8d71d4ba0a // indirect
github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785 // indirect
github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 // indirect
github.com/smartcontractkit/chainlink-protos/ring/go v0.0.0-20260331131315-f08a616d8dcd // indirect
diff --git a/system-tests/lib/go.sum b/system-tests/lib/go.sum
index ce3a9557cac..d7f6fcaed36 100644
--- a/system-tests/lib/go.sum
+++ b/system-tests/lib/go.sum
@@ -1657,8 +1657,8 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:5JdppgngCOUS76p61zCinSCgOhPeYQ+OcDUuome5THQ=
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877 h1:6UueUIbck1Ogarm9rm/9TS6b09mKgMmx+YE8XFg63AQ=
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8=
-github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432 h1:qKM3C6m+Hnaekp4LvZcMwmB6t3g3nhUU8RYu1w9e0mI=
-github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432/go.mod h1:vL1bDgPSJjV0EqHYs4dDlR+EEE0cJchgvGLYXhwIjXY=
+github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260428231113-4e8d71d4ba0a h1:UPejHeV2qjuZxc9TLa6d0q1KbC9oW29eBWFSynVvXv8=
+github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260428231113-4e8d71d4ba0a/go.mod h1:vL1bDgPSJjV0EqHYs4dDlR+EEE0cJchgvGLYXhwIjXY=
github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 h1:q+VDPcxWrj5k9QizSYfUOSMnDH3Sd5HvbPguZOgfXTY=
github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0/go.mod h1:/dVVLXrsp+V0AbcYGJo3XMzKg3CkELsweA/TTopCsKE=
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM=
diff --git a/system-tests/tests/go.mod b/system-tests/tests/go.mod
index 15dcf373acd..9930078b29a 100644
--- a/system-tests/tests/go.mod
+++ b/system-tests/tests/go.mod
@@ -156,7 +156,7 @@ require (
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/heartbeat v0.0.0-20260115142640-f6b99095c12e // indirect
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0.0.0-20251211142334-5c3421fe2c8d // indirect
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d // indirect
- github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432 // indirect
+ github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260428231113-4e8d71d4ba0a // indirect
github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785 // indirect
github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260421131224-c46cbfe7bc6c // indirect
github.com/smartcontractkit/chainlink-solana/contracts v0.0.0-20260421131224-c46cbfe7bc6c // indirect
diff --git a/system-tests/tests/go.sum b/system-tests/tests/go.sum
index 8a385ce9044..b7ee19215b0 100644
--- a/system-tests/tests/go.sum
+++ b/system-tests/tests/go.sum
@@ -1872,8 +1872,8 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:5JdppgngCOUS76p61zCinSCgOhPeYQ+OcDUuome5THQ=
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877 h1:6UueUIbck1Ogarm9rm/9TS6b09mKgMmx+YE8XFg63AQ=
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8=
-github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432 h1:qKM3C6m+Hnaekp4LvZcMwmB6t3g3nhUU8RYu1w9e0mI=
-github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260427191023-4c493ae93432/go.mod h1:vL1bDgPSJjV0EqHYs4dDlR+EEE0cJchgvGLYXhwIjXY=
+github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260428231113-4e8d71d4ba0a h1:UPejHeV2qjuZxc9TLa6d0q1KbC9oW29eBWFSynVvXv8=
+github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260428231113-4e8d71d4ba0a/go.mod h1:vL1bDgPSJjV0EqHYs4dDlR+EEE0cJchgvGLYXhwIjXY=
github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 h1:q+VDPcxWrj5k9QizSYfUOSMnDH3Sd5HvbPguZOgfXTY=
github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0/go.mod h1:/dVVLXrsp+V0AbcYGJo3XMzKg3CkELsweA/TTopCsKE=
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM=