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=