diff --git a/.changeset/chilled-months-bow.md b/.changeset/chilled-months-bow.md new file mode 100644 index 00000000000..d3bbf7f97e3 --- /dev/null +++ b/.changeset/chilled-months-bow.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +#added oracle support in standard capabilities diff --git a/core/scripts/go.mod b/core/scripts/go.mod index af7ddc2f98f..5c9043111da 100644 --- a/core/scripts/go.mod +++ b/core/scripts/go.mod @@ -22,7 +22,7 @@ require ( github.com/prometheus/client_golang v1.20.0 github.com/shopspring/decimal v1.4.0 github.com/smartcontractkit/chainlink-automation v1.0.4 - github.com/smartcontractkit/chainlink-common v0.2.3-0.20241001210038-dd59341432bd + github.com/smartcontractkit/chainlink-common v0.2.3-0.20241008170407-8bfcea33a98d github.com/smartcontractkit/chainlink/v2 v2.0.0-00010101000000-000000000000 github.com/smartcontractkit/libocr v0.0.0-20240717100443-f6226e09bee7 github.com/spf13/cobra v1.8.1 diff --git a/core/scripts/go.sum b/core/scripts/go.sum index 684155bba51..edb7d16b965 100644 --- a/core/scripts/go.sum +++ b/core/scripts/go.sum @@ -1099,8 +1099,8 @@ github.com/smartcontractkit/chainlink-automation v1.0.4 h1:iyW181JjKHLNMnDleI8um github.com/smartcontractkit/chainlink-automation v1.0.4/go.mod h1:u4NbPZKJ5XiayfKHD/v3z3iflQWqvtdhj13jVZXj/cM= github.com/smartcontractkit/chainlink-ccip v0.0.0-20241009125450-4290917273fb h1:bYT7j5syymCtAKv/gGG7CLp4APe/VDWkig9Ph7mc8fQ= github.com/smartcontractkit/chainlink-ccip v0.0.0-20241009125450-4290917273fb/go.mod h1:WbtjuYMnDAYojL3CSWmruc1ecSBgSTggzXJ6F1U6bxw= -github.com/smartcontractkit/chainlink-common v0.2.3-0.20241001210038-dd59341432bd h1:DraA9kRpkyI02OEx7QDbSq3bCYxVFZ78TdOP2rUvHts= -github.com/smartcontractkit/chainlink-common v0.2.3-0.20241001210038-dd59341432bd/go.mod h1:F6WUS6N4mP5ScwpwyTyAJc9/vjR+GXbMCRUOVekQi1g= +github.com/smartcontractkit/chainlink-common v0.2.3-0.20241008170407-8bfcea33a98d h1:GpGKzVtDWSb8cesHSvm+LQpCJpFdfVPk3us5wAY6ixI= +github.com/smartcontractkit/chainlink-common v0.2.3-0.20241008170407-8bfcea33a98d/go.mod h1:+yz2So1Lp90C3ATfMmqMEJaIr3zfG8e4xHa/3u1kIDk= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240911175228-daf2600bb7b7 h1:lTGIOQYLk1Ufn++X/AvZnt6VOcuhste5yp+C157No/Q= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240911175228-daf2600bb7b7/go.mod h1:BMYE1vC/pGmdFSsOJdPrAA0/4gZ0Xo0SxTMdGspBtRo= github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240916152957-433914114bd2 h1:yRk4ektpx/UxwarqAfgxUXLrsYXlaNeP1NOwzHGrK2Q= diff --git a/core/services/chainlink/application.go b/core/services/chainlink/application.go index c9e72244cf1..784abac9516 100644 --- a/core/services/chainlink/application.go +++ b/core/services/chainlink/application.go @@ -453,15 +453,6 @@ func NewApplication(opts ApplicationOpts) (Application, error) { pipelineRunner, cfg.JobPipeline(), ), - job.StandardCapabilities: standardcapabilities.NewDelegate( - globalLogger, - opts.DS, jobORM, - opts.CapabilitiesRegistry, - loopRegistrarConfig, - telemetryManager, - pipelineRunner, - opts.RelayerChainInteroperators, - gatewayConnectorWrapper), } webhookJobRunner = delegates[job.Webhook].(*webhook.Delegate).WebhookJobRunner() ) @@ -501,6 +492,20 @@ func NewApplication(opts ApplicationOpts) (Application, error) { return nil, fmt.Errorf("P2P stack required for OCR or OCR2") } + // If peer wrapper is initialized, Oracle Factory dependency will be available to standard capabilities + delegates[job.StandardCapabilities] = standardcapabilities.NewDelegate( + globalLogger, + opts.DS, jobORM, + opts.CapabilitiesRegistry, + loopRegistrarConfig, + telemetryManager, + pipelineRunner, + opts.RelayerChainInteroperators, + gatewayConnectorWrapper, + keyStore, + peerWrapper, + ) + if cfg.OCR().Enabled() { delegates[job.OffchainReporting] = ocr.NewDelegate( opts.DS, diff --git a/core/services/job/models.go b/core/services/job/models.go index f4b773a1bfb..2c225cd0f2b 100644 --- a/core/services/job/models.go +++ b/core/services/job/models.go @@ -948,12 +948,35 @@ func (w *WorkflowSpec) RawSpec(ctx context.Context) ([]byte, error) { return rs, nil } +type OracleFactoryConfig struct { + Enabled bool `toml:"enabled"` + BootstrapPeers []string `toml:"bootstrap_peers"` // e.g.,["12D3KooWEBVwbfdhKnicois7FTYVsBFGFcoMhMCKXQC57BQyZMhz@localhost:6690"] + OCRContractAddress string `toml:"ocr_contract_address"` // e.g., 0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6 + ChainID string `toml:"chain_id"` // e.g., "31337" + Network string `toml:"network"` // e.g., "evm" +} + +// Value returns this instance serialized for database storage. +func (ofc OracleFactoryConfig) Value() (driver.Value, error) { + return json.Marshal(ofc) +} + +// Scan reads the database value and returns an instance. +func (ofc *OracleFactoryConfig) Scan(value interface{}) error { + b, ok := value.([]byte) + if !ok { + return errors.Errorf("expected bytes got %T", b) + } + return json.Unmarshal(b, &ofc) +} + type StandardCapabilitiesSpec struct { - ID int32 - CreatedAt time.Time `toml:"-"` - UpdatedAt time.Time `toml:"-"` - Command string `toml:"command"` - Config string `toml:"config"` + ID int32 + CreatedAt time.Time `toml:"-"` + UpdatedAt time.Time `toml:"-"` + Command string `toml:"command" db:"command"` + Config string `toml:"config" db:"config"` + OracleFactory OracleFactoryConfig `toml:"oracle_factory" db:"oracle_factory"` } func (w *StandardCapabilitiesSpec) GetID() string { diff --git a/core/services/job/orm.go b/core/services/job/orm.go index d02e0b29200..071ca37203e 100644 --- a/core/services/job/orm.go +++ b/core/services/job/orm.go @@ -418,8 +418,8 @@ func (o *orm) CreateJob(ctx context.Context, jb *Job) error { } jb.WorkflowSpecID = &specID case StandardCapabilities: - sql := `INSERT INTO standardcapabilities_specs (command, config, created_at, updated_at) - VALUES (:command, :config, NOW(), NOW()) + sql := `INSERT INTO standardcapabilities_specs (command, config, oracle_factory, created_at, updated_at) + VALUES (:command, :config, :oracle_factory, NOW(), NOW()) RETURNING id;` specID, err := tx.prepareQuerySpecID(ctx, sql, jb.StandardCapabilitiesSpec) if err != nil { diff --git a/core/services/ocr2/plugins/generic/oraclefactory.go b/core/services/ocr2/plugins/generic/oraclefactory.go new file mode 100644 index 00000000000..7d44a239d2e --- /dev/null +++ b/core/services/ocr2/plugins/generic/oraclefactory.go @@ -0,0 +1,140 @@ +package generic + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "github.com/prometheus/client_golang/prometheus" + ocr "github.com/smartcontractkit/libocr/offchainreporting2plus" + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" + + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/job" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ocr2key" + "github.com/smartcontractkit/chainlink/v2/core/services/ocrcommon" + "github.com/smartcontractkit/chainlink/v2/core/services/telemetry" + + "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" +) + +type oracleFactory struct { + database ocr3types.Database + jobID int32 + jobName string + jobORM job.ORM + kb ocr2key.KeyBundle + lggr logger.Logger + config job.OracleFactoryConfig + peerWrapper *ocrcommon.SingletonPeerWrapper + relayerSet *RelayerSet + transmitterID string +} + +type OracleFactoryParams struct { + JobID int32 + JobName string + JobORM job.ORM + KB ocr2key.KeyBundle + Logger logger.Logger + Config job.OracleFactoryConfig + PeerWrapper *ocrcommon.SingletonPeerWrapper + RelayerSet *RelayerSet + TransmitterID string +} + +func NewOracleFactory(params OracleFactoryParams) (core.OracleFactory, error) { + return &oracleFactory{ + database: OracleFactoryDB(params.JobID, params.Logger), + jobID: params.JobID, + jobName: params.JobName, + jobORM: params.JobORM, + kb: params.KB, + lggr: params.Logger, + config: params.Config, + peerWrapper: params.PeerWrapper, + relayerSet: params.RelayerSet, + transmitterID: params.TransmitterID, + }, nil +} + +func (of *oracleFactory) NewOracle(ctx context.Context, args core.OracleArgs) (core.Oracle, error) { + if !of.peerWrapper.IsStarted() { + return nil, errors.New("peer wrapper not started") + } + + relayer, err := of.relayerSet.Get(ctx, types.RelayID{Network: of.config.Network, ChainID: of.config.ChainID}) + if err != nil { + return nil, fmt.Errorf("error when getting relayer: %w", err) + } + + var relayConfig = struct { + ChainID string `json:"chainID"` + EffectiveTransmitterID string `json:"effectiveTransmitterID"` + SendingKeys []string `json:"sendingKeys"` + }{ + ChainID: of.config.ChainID, + EffectiveTransmitterID: of.transmitterID, + SendingKeys: []string{of.transmitterID}, + } + relayConfigBytes, err := json.Marshal(relayConfig) + if err != nil { + return nil, fmt.Errorf("error when marshalling relay config: %w", err) + } + + pluginProvider, err := relayer.NewPluginProvider(ctx, core.RelayArgs{ + ContractID: of.config.OCRContractAddress, + ProviderType: "plugin", + RelayConfig: relayConfigBytes, + }, core.PluginArgs{ + TransmitterID: of.transmitterID, + }) + if err != nil { + return nil, fmt.Errorf("error when getting offchain digester: %w", err) + } + + bootstrapPeers, err := ocrcommon.ParseBootstrapPeers(of.config.BootstrapPeers) + if err != nil { + return nil, fmt.Errorf("failed to parse bootstrap peers: %w", err) + } + + oracle, err := ocr.NewOracle(ocr.OCR3OracleArgs[[]byte]{ + // We are relying on the relayer plugin provider for the offchain config digester + // and the contract config tracker to save time. + ContractConfigTracker: pluginProvider.ContractConfigTracker(), + OffchainConfigDigester: pluginProvider.OffchainConfigDigester(), + LocalConfig: args.LocalConfig, + ContractTransmitter: NewContractTransmitter(of.transmitterID, args.ContractTransmitter), + ReportingPluginFactory: args.ReportingPluginFactoryService, + BinaryNetworkEndpointFactory: of.peerWrapper.Peer2, + V2Bootstrappers: bootstrapPeers, + Database: of.database, + Logger: ocrcommon.NewOCRWrapper(of.lggr, true, func(ctx context.Context, msg string) { + logger.Sugared(of.lggr).ErrorIf(of.jobORM.RecordError(ctx, of.jobID, msg), "unable to record error") + }), + MonitoringEndpoint: &telemetry.NoopAgent{}, + OffchainKeyring: of.kb, + OnchainKeyring: ocrcommon.NewOCR3OnchainKeyringAdapter(of.kb), + MetricsRegisterer: prometheus.WrapRegistererWith(map[string]string{"job_name": of.jobName}, prometheus.DefaultRegisterer), + }) + + if err != nil { + return nil, fmt.Errorf("%w: failed to create new OCR oracle", err) + } + + return &adaptedOracle{oracle: oracle}, nil +} + +type adaptedOracle struct { + oracle ocr.Oracle +} + +func (a *adaptedOracle) Start(ctx context.Context) error { + return a.oracle.Start() +} + +func (a *adaptedOracle) Close(ctx context.Context) error { + return a.oracle.Close() +} diff --git a/core/services/ocr2/plugins/generic/oraclefactorydb.go b/core/services/ocr2/plugins/generic/oraclefactorydb.go new file mode 100644 index 00000000000..5db6f8a2ffe --- /dev/null +++ b/core/services/ocr2/plugins/generic/oraclefactorydb.go @@ -0,0 +1,135 @@ +package generic + +import ( + "context" + "encoding/json" + "fmt" + "time" + + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + + "github.com/smartcontractkit/chainlink/v2/core/logger" +) + +type oracleFactoryDb struct { + // The ID is used for logging and error messages + // A single standard capabilities spec can instantiate multiple oracles + // TODO: NewOracle should take a unique identifier for the oracle + specID int32 + lggr logger.SugaredLogger + config *ocrtypes.ContractConfig + states map[ocrtypes.ConfigDigest]*ocrtypes.PersistentState + pendingTransmissions map[ocrtypes.ReportTimestamp]ocrtypes.PendingTransmission + protocolStates map[ocrtypes.ConfigDigest]map[string][]byte +} + +var ( + _ ocrtypes.Database = &oracleFactoryDb{} +) + +// NewDB returns a new DB scoped to this instanceID +func OracleFactoryDB(specID int32, lggr logger.Logger) *oracleFactoryDb { + return &oracleFactoryDb{ + specID: specID, + lggr: logger.Sugared(lggr.Named("OracleFactoryMemoryDb")), + states: make(map[ocrtypes.ConfigDigest]*ocrtypes.PersistentState), + pendingTransmissions: make(map[ocrtypes.ReportTimestamp]ocrtypes.PendingTransmission), + protocolStates: make(map[ocrtypes.ConfigDigest]map[string][]byte), + } +} + +func (ofdb *oracleFactoryDb) ReadState(ctx context.Context, cd ocrtypes.ConfigDigest) (ps *ocrtypes.PersistentState, err error) { + ps, ok := ofdb.states[cd] + if !ok { + return nil, fmt.Errorf("state not found for standard capabilities spec ID %d, config digest %s", ofdb.specID, cd) + } + + return ps, nil +} + +func (ofdb *oracleFactoryDb) WriteState(ctx context.Context, cd ocrtypes.ConfigDigest, state ocrtypes.PersistentState) error { + ofdb.states[cd] = &state + return nil +} + +func (ofdb *oracleFactoryDb) ReadConfig(ctx context.Context) (c *ocrtypes.ContractConfig, err error) { + if ofdb.config == nil { + // Returning nil, nil because this is a cache miss + return nil, nil + } + return ofdb.config, nil +} + +func (ofdb *oracleFactoryDb) WriteConfig(ctx context.Context, c ocrtypes.ContractConfig) error { + ofdb.config = &c + + cBytes, err := json.Marshal(c) + if err != nil { + return fmt.Errorf("MemoryDB: WriteConfig failed to marshal config: %w", err) + } + + ofdb.lggr.Debugw("MemoryDB: WriteConfig", "ocrtypes.ContractConfig", string(cBytes)) + + return nil +} + +func (ofdb *oracleFactoryDb) StorePendingTransmission(ctx context.Context, t ocrtypes.ReportTimestamp, tx ocrtypes.PendingTransmission) error { + ofdb.pendingTransmissions[t] = tx + return nil +} + +func (ofdb *oracleFactoryDb) PendingTransmissionsWithConfigDigest(ctx context.Context, cd ocrtypes.ConfigDigest) (map[ocrtypes.ReportTimestamp]ocrtypes.PendingTransmission, error) { + m := make(map[ocrtypes.ReportTimestamp]ocrtypes.PendingTransmission) + for k, v := range ofdb.pendingTransmissions { + if k.ConfigDigest == cd { + m[k] = v + } + } + + return m, nil +} + +func (ofdb *oracleFactoryDb) DeletePendingTransmission(ctx context.Context, t ocrtypes.ReportTimestamp) error { + delete(ofdb.pendingTransmissions, t) + return nil +} + +func (ofdb *oracleFactoryDb) DeletePendingTransmissionsOlderThan(ctx context.Context, t time.Time) error { + for k, v := range ofdb.pendingTransmissions { + if v.Time.Before(t) { + delete(ofdb.pendingTransmissions, k) + } + } + + return nil +} + +func (ofdb *oracleFactoryDb) ReadProtocolState( + ctx context.Context, + configDigest ocrtypes.ConfigDigest, + key string, +) ([]byte, error) { + value, ok := ofdb.protocolStates[configDigest][key] + if !ok { + // Previously implementation returned nil if the state is not found + return nil, nil + } + return value, nil +} + +func (ofdb *oracleFactoryDb) WriteProtocolState( + ctx context.Context, + configDigest ocrtypes.ConfigDigest, + key string, + value []byte, +) error { + if value == nil { + delete(ofdb.protocolStates[configDigest], key) + } else { + if ofdb.protocolStates[configDigest] == nil { + ofdb.protocolStates[configDigest] = make(map[string][]byte) + } + ofdb.protocolStates[configDigest][key] = value + } + return nil +} diff --git a/core/services/ocr2/plugins/generic/oraclefactorytransmitter.go b/core/services/ocr2/plugins/generic/oraclefactorytransmitter.go new file mode 100644 index 00000000000..41a2c3fa1c4 --- /dev/null +++ b/core/services/ocr2/plugins/generic/oraclefactorytransmitter.go @@ -0,0 +1,36 @@ +package generic + +import ( + "context" + + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" + "github.com/smartcontractkit/libocr/offchainreporting2plus/types" +) + +var _ ocr3types.ContractTransmitter[[]byte] = (*contractTransmitter)(nil) + +type contractTransmitter struct { + impl ocr3types.ContractTransmitter[[]byte] + transmitterID string +} + +func NewContractTransmitter(transmitterID string, impl ocr3types.ContractTransmitter[[]byte]) *contractTransmitter { + return &contractTransmitter{ + impl: impl, + transmitterID: transmitterID, + } +} + +func (ct *contractTransmitter) Transmit( + ctx context.Context, + configDigest types.ConfigDigest, + seqNr uint64, + reportWithInfo ocr3types.ReportWithInfo[[]byte], + attributedOnchainSignature []types.AttributedOnchainSignature, +) error { + return ct.impl.Transmit(ctx, configDigest, seqNr, reportWithInfo, attributedOnchainSignature) +} + +func (ct *contractTransmitter) FromAccount() (types.Account, error) { + return types.Account(ct.transmitterID), nil +} diff --git a/core/services/standardcapabilities/delegate.go b/core/services/standardcapabilities/delegate.go index c22134bb48b..7c370d4f8de 100644 --- a/core/services/standardcapabilities/delegate.go +++ b/core/services/standardcapabilities/delegate.go @@ -18,7 +18,12 @@ import ( webapitarget "github.com/smartcontractkit/chainlink/v2/core/capabilities/webapi/target" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/job" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/chaintype" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ethkey" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ocr2key" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/generic" + "github.com/smartcontractkit/chainlink/v2/core/services/ocrcommon" "github.com/smartcontractkit/chainlink/v2/core/services/pipeline" "github.com/smartcontractkit/chainlink/v2/core/services/telemetry" "github.com/smartcontractkit/chainlink/v2/plugins" @@ -39,6 +44,8 @@ type Delegate struct { pipelineRunner pipeline.Runner relayers RelayGetter gatewayConnectorWrapper *gatewayconnector.ServiceWrapper + ks keystore.Master + peerWrapper *ocrcommon.SingletonPeerWrapper isNewlyCreatedJob bool } @@ -49,11 +56,33 @@ const ( commandOverrideForCustomComputeAction = "__builtin_custom-compute-action" ) -func NewDelegate(logger logger.Logger, ds sqlutil.DataSource, jobORM job.ORM, registry core.CapabilitiesRegistry, - cfg plugins.RegistrarConfig, monitoringEndpointGen telemetry.MonitoringEndpointGenerator, pipelineRunner pipeline.Runner, - relayers RelayGetter, gatewayConnectorWrapper *gatewayconnector.ServiceWrapper) *Delegate { - return &Delegate{logger: logger, ds: ds, jobORM: jobORM, registry: registry, cfg: cfg, monitoringEndpointGen: monitoringEndpointGen, pipelineRunner: pipelineRunner, - relayers: relayers, isNewlyCreatedJob: false, gatewayConnectorWrapper: gatewayConnectorWrapper} +func NewDelegate( + logger logger.Logger, + ds sqlutil.DataSource, + jobORM job.ORM, + registry core.CapabilitiesRegistry, + cfg plugins.RegistrarConfig, + monitoringEndpointGen telemetry.MonitoringEndpointGenerator, + pipelineRunner pipeline.Runner, + relayers RelayGetter, + gatewayConnectorWrapper *gatewayconnector.ServiceWrapper, + ks keystore.Master, + peerWrapper *ocrcommon.SingletonPeerWrapper, +) *Delegate { + return &Delegate{ + logger: logger, + ds: ds, + jobORM: jobORM, + registry: registry, + cfg: cfg, + monitoringEndpointGen: monitoringEndpointGen, + pipelineRunner: pipelineRunner, + relayers: relayers, + isNewlyCreatedJob: false, + gatewayConnectorWrapper: gatewayConnectorWrapper, + ks: ks, + peerWrapper: peerWrapper, + } } func (d *Delegate) JobType() job.Type { @@ -78,6 +107,63 @@ func (d *Delegate) ServicesForSpec(ctx context.Context, spec job.Job) ([]job.Ser return nil, fmt.Errorf("failed to create relayer set: %w", err) } + ocrKeyBundles, err := d.ks.OCR2().GetAll() + if err != nil { + return nil, err + } + + if len(ocrKeyBundles) > 1 { + return nil, fmt.Errorf("expected exactly one OCR key bundle, but found: %d", len(ocrKeyBundles)) + } + + var ocrKeyBundle ocr2key.KeyBundle + if len(ocrKeyBundles) == 0 { + ocrKeyBundle, err = d.ks.OCR2().Create(ctx, chaintype.EVM) + if err != nil { + return nil, errors.Wrap(err, "failed to create OCR key bundle") + } + } else { + ocrKeyBundle = ocrKeyBundles[0] + } + + ethKeyBundles, err := d.ks.Eth().GetAll(ctx) + if err != nil { + return nil, err + } + if len(ethKeyBundles) > 1 { + return nil, fmt.Errorf("expected exactly one ETH key bundle, but found: %d", len(ethKeyBundles)) + } + + var ethKeyBundle ethkey.KeyV2 + if len(ethKeyBundles) == 0 { + ethKeyBundle, err = d.ks.Eth().Create(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to create ETH key bundle") + } + } else { + ethKeyBundle = ethKeyBundles[0] + } + + log.Debug("oracleFactoryConfig: ", spec.StandardCapabilitiesSpec.OracleFactory) + + if spec.StandardCapabilitiesSpec.OracleFactory.Enabled && d.peerWrapper == nil { + return nil, errors.New("P2P stack required for Oracle Factory") + } + + oracleFactory, err := generic.NewOracleFactory(generic.OracleFactoryParams{ + Logger: log, + JobORM: d.jobORM, + JobID: spec.ID, + JobName: spec.Name.ValueOrZero(), + KB: ocrKeyBundle, + Config: spec.StandardCapabilitiesSpec.OracleFactory, + PeerWrapper: d.peerWrapper, + RelayerSet: relayerSet, + TransmitterID: ethKeyBundle.Address.String(), + }) + if err != nil { + return nil, fmt.Errorf("failed to create oracle factory: %w", err) + } // NOTE: special cases for built-in capabilities (to be moved into LOOPPs in the future) if spec.StandardCapabilitiesSpec.Command == commandOverrideForWebAPITrigger { if d.gatewayConnectorWrapper == nil { @@ -122,7 +208,7 @@ func (d *Delegate) ServicesForSpec(ctx context.Context, spec job.Job) ([]job.Ser } standardCapability := newStandardCapabilities(log, spec.StandardCapabilitiesSpec, d.cfg, telemetryService, kvStore, d.registry, errorLog, - pr, relayerSet) + pr, relayerSet, oracleFactory) return []job.ServiceCtx{standardCapability}, nil } @@ -161,6 +247,22 @@ func ValidatedStandardCapabilitiesSpec(tomlString string) (job.Job, error) { return jb, errors.Errorf("standard capabilities command must be set") } + // Skip validation if Oracle Factory is not enabled + if !jb.StandardCapabilitiesSpec.OracleFactory.Enabled { + return jb, nil + } + + // If Oracle Factory is enabled, it must have at least one bootstrap peer + if len(jb.StandardCapabilitiesSpec.OracleFactory.BootstrapPeers) == 0 { + return jb, errors.New("no bootstrap peers found") + } + + // Validate bootstrap peers + _, err = ocrcommon.ParseBootstrapPeers(jb.StandardCapabilitiesSpec.OracleFactory.BootstrapPeers) + if err != nil { + return jb, errors.Wrap(err, "failed to parse bootstrap peers") + } + return jb, nil } diff --git a/core/services/standardcapabilities/delegate_test.go b/core/services/standardcapabilities/delegate_test.go new file mode 100644 index 00000000000..27b6734f911 --- /dev/null +++ b/core/services/standardcapabilities/delegate_test.go @@ -0,0 +1,121 @@ +package standardcapabilities_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/v2/core/services/job" + "github.com/smartcontractkit/chainlink/v2/core/services/standardcapabilities" +) + +func Test_ValidatedStandardCapabilitiesSpec(t *testing.T) { + type testCase struct { + name string + tomlString string + expectedError string + expectedSpec *job.StandardCapabilitiesSpec + } + + testCases := []testCase{ + { + name: "invalid TOML string", + tomlString: `[[]`, + expectedError: "toml error on load standard capabilities", + }, + { + name: "incorrect job type", + tomlString: ` + type="nonstandardcapabilities" + `, + expectedError: "standard capabilities unsupported job type", + }, + { + name: "command unset", + tomlString: ` + type="standardcapabilities" + `, + expectedError: "standard capabilities command must be set", + }, + { + name: "invalid oracle config: malformed peer", + tomlString: ` + type="standardcapabilities" + command="path/to/binary" + + [oracle_factory] + enabled=true + bootstrap_peers = [ + "invalid_p2p_id@invalid_ip:1111" + ] + `, + expectedError: "failed to parse bootstrap peers", + }, + { + name: "invalid oracle config: missing bootstrap peers", + tomlString: ` + type="standardcapabilities" + command="path/to/binary" + + [oracle_factory] + enabled=true + `, + expectedError: "no bootstrap peers found", + }, + { + name: "valid spec", + tomlString: ` + type="standardcapabilities" + command="path/to/binary" + `, + }, + { + name: "valid spec with oracle config", + tomlString: ` + type="standardcapabilities" + command="path/to/binary" + + [capabilities] + target = "enabled" + + [oracle_factory] + enabled=true + bootstrap_peers = [ + "12D3KooWEBVwbfdhKnicois7FTYVsBFGFcoMhMCKXQC57BQyZMhz@localhost:6690" + ] + network="evm" + chain_id="31337" + ocr_contract_address="0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" + `, + expectedSpec: &job.StandardCapabilitiesSpec{ + Command: "path/to/binary", + OracleFactory: job.OracleFactoryConfig{ + Enabled: true, + BootstrapPeers: []string{ + "12D3KooWEBVwbfdhKnicois7FTYVsBFGFcoMhMCKXQC57BQyZMhz@localhost:6690", + }, + OCRContractAddress: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", + ChainID: "31337", + Network: "evm", + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + jobSpec, err := standardcapabilities.ValidatedStandardCapabilitiesSpec(tc.tomlString) + + if tc.expectedError != "" { + assert.ErrorContains(t, err, tc.expectedError) + } else { + require.NoError(t, err) + } + + if tc.expectedSpec != nil { + assert.EqualValues(t, tc.expectedSpec, jobSpec.StandardCapabilitiesSpec) + } + }) + } +} diff --git a/core/services/standardcapabilities/standard_capabilities.go b/core/services/standardcapabilities/standard_capabilities.go index a8d007d5df8..fe3dad7bb2f 100644 --- a/core/services/standardcapabilities/standard_capabilities.go +++ b/core/services/standardcapabilities/standard_capabilities.go @@ -23,18 +23,23 @@ type standardCapabilities struct { errorLog core.ErrorLog pipelineRunner core.PipelineRunnerService relayerSet core.RelayerSet + oracleFactory core.OracleFactory capabilitiesLoop *loop.StandardCapabilitiesService } -func newStandardCapabilities(log logger.Logger, spec *job.StandardCapabilitiesSpec, +func newStandardCapabilities( + log logger.Logger, + spec *job.StandardCapabilitiesSpec, pluginRegistrar plugins.RegistrarConfig, telemetryService core.TelemetryService, store core.KeyValueStore, CapabilitiesRegistry core.CapabilitiesRegistry, errorLog core.ErrorLog, pipelineRunner core.PipelineRunnerService, - relayerSet core.RelayerSet) *standardCapabilities { + relayerSet core.RelayerSet, + oracleFactory core.OracleFactory, +) *standardCapabilities { return &standardCapabilities{ log: log, spec: spec, @@ -45,6 +50,7 @@ func newStandardCapabilities(log logger.Logger, spec *job.StandardCapabilitiesSp errorLog: errorLog, pipelineRunner: pipelineRunner, relayerSet: relayerSet, + oracleFactory: oracleFactory, } } @@ -73,7 +79,7 @@ func (s *standardCapabilities) Start(ctx context.Context) error { } if err = s.capabilitiesLoop.Service.Initialise(ctx, s.spec.Config, s.telemetryService, s.store, s.CapabilitiesRegistry, s.errorLog, - s.pipelineRunner, s.relayerSet); err != nil { + s.pipelineRunner, s.relayerSet, s.oracleFactory); err != nil { return fmt.Errorf("error initialising standard capabilities service: %v", err) } diff --git a/core/services/workflows/engine.go b/core/services/workflows/engine.go index ed584ba5aec..680c29371a0 100644 --- a/core/services/workflows/engine.go +++ b/core/services/workflows/engine.go @@ -11,9 +11,8 @@ import ( "github.com/jonboulle/clockwork" - "github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk" - "github.com/smartcontractkit/chainlink-common/pkg/workflows/exec" + "github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/services" diff --git a/core/store/migrate/migrations/0255_standard_capabilities_extension.sql b/core/store/migrate/migrations/0255_standard_capabilities_extension.sql new file mode 100644 index 00000000000..d81b4864eb4 --- /dev/null +++ b/core/store/migrate/migrations/0255_standard_capabilities_extension.sql @@ -0,0 +1,9 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE standardcapabilities_specs +ADD COLUMN oracle_factory JSONB; +-- +goose StatementEnd +-- +goose Down +-- +goose StatementBegin +ALTER TABLE standardcapabilities_specs DROP COLUMN oracle_factory; +-- +goose StatementEnd diff --git a/go.mod b/go.mod index 93696685c60..a2c9b1b211f 100644 --- a/go.mod +++ b/go.mod @@ -76,7 +76,7 @@ require ( github.com/smartcontractkit/chain-selectors v1.0.23 github.com/smartcontractkit/chainlink-automation v1.0.4 github.com/smartcontractkit/chainlink-ccip v0.0.0-20241009125450-4290917273fb - github.com/smartcontractkit/chainlink-common v0.2.3-0.20241001210038-dd59341432bd + github.com/smartcontractkit/chainlink-common v0.2.3-0.20241008170407-8bfcea33a98d github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240911175228-daf2600bb7b7 github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240916152957-433914114bd2 github.com/smartcontractkit/chainlink-feeds v0.0.0-20240910155501-42f20443189f diff --git a/go.sum b/go.sum index 2e4a7775a60..8fad9ec97ad 100644 --- a/go.sum +++ b/go.sum @@ -1060,8 +1060,8 @@ github.com/smartcontractkit/chainlink-automation v1.0.4 h1:iyW181JjKHLNMnDleI8um github.com/smartcontractkit/chainlink-automation v1.0.4/go.mod h1:u4NbPZKJ5XiayfKHD/v3z3iflQWqvtdhj13jVZXj/cM= github.com/smartcontractkit/chainlink-ccip v0.0.0-20241009125450-4290917273fb h1:bYT7j5syymCtAKv/gGG7CLp4APe/VDWkig9Ph7mc8fQ= github.com/smartcontractkit/chainlink-ccip v0.0.0-20241009125450-4290917273fb/go.mod h1:WbtjuYMnDAYojL3CSWmruc1ecSBgSTggzXJ6F1U6bxw= -github.com/smartcontractkit/chainlink-common v0.2.3-0.20241001210038-dd59341432bd h1:DraA9kRpkyI02OEx7QDbSq3bCYxVFZ78TdOP2rUvHts= -github.com/smartcontractkit/chainlink-common v0.2.3-0.20241001210038-dd59341432bd/go.mod h1:F6WUS6N4mP5ScwpwyTyAJc9/vjR+GXbMCRUOVekQi1g= +github.com/smartcontractkit/chainlink-common v0.2.3-0.20241008170407-8bfcea33a98d h1:GpGKzVtDWSb8cesHSvm+LQpCJpFdfVPk3us5wAY6ixI= +github.com/smartcontractkit/chainlink-common v0.2.3-0.20241008170407-8bfcea33a98d/go.mod h1:+yz2So1Lp90C3ATfMmqMEJaIr3zfG8e4xHa/3u1kIDk= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240911175228-daf2600bb7b7 h1:lTGIOQYLk1Ufn++X/AvZnt6VOcuhste5yp+C157No/Q= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240911175228-daf2600bb7b7/go.mod h1:BMYE1vC/pGmdFSsOJdPrAA0/4gZ0Xo0SxTMdGspBtRo= github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240916152957-433914114bd2 h1:yRk4ektpx/UxwarqAfgxUXLrsYXlaNeP1NOwzHGrK2Q= diff --git a/integration-tests/go.mod b/integration-tests/go.mod index d3ad17f908c..34cf2df9cbb 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -40,7 +40,7 @@ require ( github.com/smartcontractkit/chain-selectors v1.0.23 github.com/smartcontractkit/chainlink-automation v1.0.4 github.com/smartcontractkit/chainlink-ccip v0.0.0-20241009125450-4290917273fb - github.com/smartcontractkit/chainlink-common v0.2.3-0.20241001210038-dd59341432bd + github.com/smartcontractkit/chainlink-common v0.2.3-0.20241008170407-8bfcea33a98d github.com/smartcontractkit/chainlink-testing-framework/havoc v1.50.0 github.com/smartcontractkit/chainlink-testing-framework/lib v1.50.9 github.com/smartcontractkit/chainlink-testing-framework/lib/grafana v1.50.0 diff --git a/integration-tests/go.sum b/integration-tests/go.sum index 90c7f08b0f9..bd416855c59 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -1439,8 +1439,8 @@ github.com/smartcontractkit/chainlink-automation v1.0.4 h1:iyW181JjKHLNMnDleI8um github.com/smartcontractkit/chainlink-automation v1.0.4/go.mod h1:u4NbPZKJ5XiayfKHD/v3z3iflQWqvtdhj13jVZXj/cM= github.com/smartcontractkit/chainlink-ccip v0.0.0-20241009125450-4290917273fb h1:bYT7j5syymCtAKv/gGG7CLp4APe/VDWkig9Ph7mc8fQ= github.com/smartcontractkit/chainlink-ccip v0.0.0-20241009125450-4290917273fb/go.mod h1:WbtjuYMnDAYojL3CSWmruc1ecSBgSTggzXJ6F1U6bxw= -github.com/smartcontractkit/chainlink-common v0.2.3-0.20241001210038-dd59341432bd h1:DraA9kRpkyI02OEx7QDbSq3bCYxVFZ78TdOP2rUvHts= -github.com/smartcontractkit/chainlink-common v0.2.3-0.20241001210038-dd59341432bd/go.mod h1:F6WUS6N4mP5ScwpwyTyAJc9/vjR+GXbMCRUOVekQi1g= +github.com/smartcontractkit/chainlink-common v0.2.3-0.20241008170407-8bfcea33a98d h1:GpGKzVtDWSb8cesHSvm+LQpCJpFdfVPk3us5wAY6ixI= +github.com/smartcontractkit/chainlink-common v0.2.3-0.20241008170407-8bfcea33a98d/go.mod h1:+yz2So1Lp90C3ATfMmqMEJaIr3zfG8e4xHa/3u1kIDk= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240911175228-daf2600bb7b7 h1:lTGIOQYLk1Ufn++X/AvZnt6VOcuhste5yp+C157No/Q= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240911175228-daf2600bb7b7/go.mod h1:BMYE1vC/pGmdFSsOJdPrAA0/4gZ0Xo0SxTMdGspBtRo= github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240916152957-433914114bd2 h1:yRk4ektpx/UxwarqAfgxUXLrsYXlaNeP1NOwzHGrK2Q= diff --git a/integration-tests/load/go.mod b/integration-tests/load/go.mod index 8b3fed5432b..85b57698a3e 100644 --- a/integration-tests/load/go.mod +++ b/integration-tests/load/go.mod @@ -15,7 +15,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.33.0 github.com/slack-go/slack v0.12.2 - github.com/smartcontractkit/chainlink-common v0.2.3-0.20241001210038-dd59341432bd + github.com/smartcontractkit/chainlink-common v0.2.3-0.20241008170407-8bfcea33a98d github.com/smartcontractkit/chainlink-testing-framework/lib v1.50.9 github.com/smartcontractkit/chainlink-testing-framework/seth v1.50.1 github.com/smartcontractkit/chainlink-testing-framework/wasp v1.50.0 diff --git a/integration-tests/load/go.sum b/integration-tests/load/go.sum index 8bdc23dc35f..b921352eeb5 100644 --- a/integration-tests/load/go.sum +++ b/integration-tests/load/go.sum @@ -1415,8 +1415,8 @@ github.com/smartcontractkit/chainlink-automation v1.0.4 h1:iyW181JjKHLNMnDleI8um github.com/smartcontractkit/chainlink-automation v1.0.4/go.mod h1:u4NbPZKJ5XiayfKHD/v3z3iflQWqvtdhj13jVZXj/cM= github.com/smartcontractkit/chainlink-ccip v0.0.0-20241009125450-4290917273fb h1:bYT7j5syymCtAKv/gGG7CLp4APe/VDWkig9Ph7mc8fQ= github.com/smartcontractkit/chainlink-ccip v0.0.0-20241009125450-4290917273fb/go.mod h1:WbtjuYMnDAYojL3CSWmruc1ecSBgSTggzXJ6F1U6bxw= -github.com/smartcontractkit/chainlink-common v0.2.3-0.20241001210038-dd59341432bd h1:DraA9kRpkyI02OEx7QDbSq3bCYxVFZ78TdOP2rUvHts= -github.com/smartcontractkit/chainlink-common v0.2.3-0.20241001210038-dd59341432bd/go.mod h1:F6WUS6N4mP5ScwpwyTyAJc9/vjR+GXbMCRUOVekQi1g= +github.com/smartcontractkit/chainlink-common v0.2.3-0.20241008170407-8bfcea33a98d h1:GpGKzVtDWSb8cesHSvm+LQpCJpFdfVPk3us5wAY6ixI= +github.com/smartcontractkit/chainlink-common v0.2.3-0.20241008170407-8bfcea33a98d/go.mod h1:+yz2So1Lp90C3ATfMmqMEJaIr3zfG8e4xHa/3u1kIDk= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240911175228-daf2600bb7b7 h1:lTGIOQYLk1Ufn++X/AvZnt6VOcuhste5yp+C157No/Q= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240911175228-daf2600bb7b7/go.mod h1:BMYE1vC/pGmdFSsOJdPrAA0/4gZ0Xo0SxTMdGspBtRo= github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240916152957-433914114bd2 h1:yRk4ektpx/UxwarqAfgxUXLrsYXlaNeP1NOwzHGrK2Q= diff --git a/plugins/cmd/capabilities/log-event-trigger/main.go b/plugins/cmd/capabilities/log-event-trigger/main.go index 7cf66f9c847..d01485a743f 100644 --- a/plugins/cmd/capabilities/log-event-trigger/main.go +++ b/plugins/cmd/capabilities/log-event-trigger/main.go @@ -92,6 +92,7 @@ func (cs *LogEventTriggerGRPCService) Initialise( errorLog core.ErrorLog, pipelineRunner core.PipelineRunnerService, relayerSet core.RelayerSet, + oracleFactory core.OracleFactory, ) error { cs.s.Logger.Debugf("Initialising %s", serviceName)