Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ require (
github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260323124644-faea187e6997
github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e
github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7
github.com/smartcontractkit/libocr v0.0.0-20250912173940-f3ab0246e23d
github.com/smartcontractkit/libocr v0.0.0-20260403184524-b6409238958d
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0
go.opentelemetry.io/otel v1.43.0
Expand Down Expand Up @@ -84,6 +84,7 @@ require (
)

require (
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect
github.com/apache/arrow-go/v18 v18.3.1 // indirect
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
Expand All @@ -94,6 +95,8 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.1 // indirect
github.com/cloudevents/sdk-go/v2 v2.16.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/ethereum/go-ethereum v1.17.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-logr/logr v1.4.3 // indirect
Expand All @@ -109,6 +112,7 @@ require (
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/hako/durafmt v0.0.0-20200710122514-c0fb7b4da026 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/holiman/uint256 v1.3.2 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
Expand Down
14 changes: 12 additions & 2 deletions go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

167 changes: 167 additions & 0 deletions pkg/capabilities/consensus/ocr3/factory_ocr3_1.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package ocr3

import (
"context"
"time"

"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/durationpb"

"github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3_1types"
"github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types"

"github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/types"
"github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/requests"
"github.com/smartcontractkit/chainlink-common/pkg/logger"
"github.com/smartcontractkit/chainlink-common/pkg/services"
)

// OCR3_1 observation/report bytes defaults. Held below the libocr hard caps:
// MaxMaxObservationBytes = 512 KiB (halved vs OCR3)
// MaxMaxQueryBytes = 512 KiB
// MaxMaxReportBytes = 5 MiB
// Any DON offchain config that exceeds these will fail ReportingPluginInfo
// validation at factory time — preflight rotation (plan §3.7) is mandatory.
const (
defaultMaxObservationBytesOCR3_1 = 400 * 1024 // 400 KiB (~80% of the 512 KiB cap)
defaultMaxQueryBytesOCR3_1 = 400 * 1024
defaultMaxReportsPlusPrecursorBytesOCR3_1 = 1 * 1024 * 1024 // 1 MiB — small, precursor only
defaultMaxReportBytesOCR3_1 = 1 * 1024 * 1024
defaultMaxReportCountOCR3_1 = 20

// KV write budget. Bounded by batch size × AggregationOutcome size.
// Well below the libocr caps (10_000 keys / 10 MiB).
defaultMaxKeyValueModifiedKeysOCR3_1 = 1024
defaultMaxKeyValueModifiedKeysPlusValuesBytesOCR3_1 = 4 * 1024 * 1024

// Blob limits. v1 uses blobs for observation payloads only.
defaultMaxBlobPayloadBytesOCR3_1 = 1 * 1024 * 1024 // 1 MiB per blob
defaultMaxPerOracleUnexpiredBlobCountOCR3_1 = 500
defaultMaxPerOracleUnexpiredBlobCumulativePayloadBytesOCR3_1 = 500 * 1024 * 1024
)

type factoryOCR3_1 struct {
store *requests.Store[*ReportRequest]
capability *capability
lggr logger.Logger

services.StateMachine
}

func newFactoryOCR3_1(
s *requests.Store[*ReportRequest],
c *capability,
lggr logger.Logger,
) (*factoryOCR3_1, error) {
return &factoryOCR3_1{
store: s,
capability: c,
lggr: logger.Named(lggr, "OCR3_1ReportingPluginFactory"),
}, nil
}

// NewReportingPlugin implements ocr3_1types.ReportingPluginFactory[[]byte].
// The BlobBroadcastFetcher must not be captured long-term; libocr only
// guarantees it within method scopes (see ocr3_1types/plugin.go doc). We
// deliberately do not stash it on the factory — each method on the plugin
// receives it fresh.
func (o *factoryOCR3_1) NewReportingPlugin(
_ context.Context,
config ocr3types.ReportingPluginConfig,
_ ocr3_1types.BlobBroadcastFetcher,
) (ocr3_1types.ReportingPlugin[[]byte], ocr3_1types.ReportingPluginInfo, error) {
var configProto types.ReportingPluginConfig
if err := proto.Unmarshal(config.OffchainConfig, &configProto); err != nil {
return nil, ocr3_1types.ReportingPluginInfo1{}, err
}

// Defaults: OCR3_1 caps are tighter than OCR3, so we cannot inherit the
// OCR3 1 MiB defaults. Any value the operator supplied is kept; zero
// values are filled with OCR3_1-safe defaults.
if configProto.MaxQueryLengthBytes <= 0 {
configProto.MaxQueryLengthBytes = defaultMaxQueryBytesOCR3_1
}
if configProto.MaxObservationLengthBytes <= 0 {
configProto.MaxObservationLengthBytes = defaultMaxObservationBytesOCR3_1
}
if configProto.MaxOutcomeLengthBytes <= 0 {
configProto.MaxOutcomeLengthBytes = defaultMaxReportsPlusPrecursorBytesOCR3_1
}
if configProto.MaxReportLengthBytes <= 0 {
configProto.MaxReportLengthBytes = defaultMaxReportBytesOCR3_1
}
if configProto.MaxReportCount <= 0 {
configProto.MaxReportCount = defaultMaxReportCountOCR3_1
}
if configProto.OutcomePruningThreshold <= 0 {
configProto.OutcomePruningThreshold = defaultOutcomePruningThreshold
}
if configProto.RequestTimeout == nil {
configProto.RequestTimeout = durationpb.New(defaultRequestExpiry)
}
// OCR3_1-only fields: honor operator-supplied values; fall back to
// defaults when unset. Keeps OCR3 offchain configs forward-compatible.
if configProto.MaxReportsPlusPrecursorBytes == 0 {
configProto.MaxReportsPlusPrecursorBytes = defaultMaxReportsPlusPrecursorBytesOCR3_1
}
if configProto.MaxKeyValueModifiedKeysPlusValuesBytes == 0 {
configProto.MaxKeyValueModifiedKeysPlusValuesBytes = defaultMaxKeyValueModifiedKeysPlusValuesBytesOCR3_1
}
if configProto.MaxBlobPayloadBytes == 0 {
configProto.MaxBlobPayloadBytes = defaultMaxBlobPayloadBytesOCR3_1
}
if configProto.BlobExpirationK == 0 {
configProto.BlobExpirationK = defaultBlobExpirationK
}
if configProto.MaxKeyValueModifiedKeys == 0 {
configProto.MaxKeyValueModifiedKeys = defaultMaxKeyValueModifiedKeysOCR3_1
}
if configProto.MaxPerOracleUnexpiredBlobCount == 0 {
configProto.MaxPerOracleUnexpiredBlobCount = defaultMaxPerOracleUnexpiredBlobCountOCR3_1
}
if configProto.MaxPerOracleUnexpiredBlobCumulativePayloadBytes == 0 {
configProto.MaxPerOracleUnexpiredBlobCumulativePayloadBytes = defaultMaxPerOracleUnexpiredBlobCumulativePayloadBytesOCR3_1
}
o.capability.setRequestTimeout(configProto.RequestTimeout.AsDuration())

rp, err := newReportingPluginOCR3_1(o.store, o.capability, config, &configProto, o.lggr)
if err != nil {
return nil, ocr3_1types.ReportingPluginInfo1{}, err
}

info := ocr3_1types.ReportingPluginInfo1{
Name: "OCR3_1 CRE Consensus Plugin",
Limits: ocr3_1types.ReportingPluginLimits{
MaxQueryBytes: int(configProto.MaxQueryLengthBytes),
MaxObservationBytes: int(configProto.MaxObservationLengthBytes),
MaxReportsPlusPrecursorBytes: int(configProto.MaxReportsPlusPrecursorBytes),
MaxReportBytes: int(configProto.MaxReportLengthBytes),
MaxReportCount: int(configProto.MaxReportCount),

MaxKeyValueModifiedKeys: int(configProto.MaxKeyValueModifiedKeys),
MaxKeyValueModifiedKeysPlusValuesBytes: int(configProto.MaxKeyValueModifiedKeysPlusValuesBytes),

MaxBlobPayloadBytes: int(configProto.MaxBlobPayloadBytes),
MaxPerOracleUnexpiredBlobCount: int(configProto.MaxPerOracleUnexpiredBlobCount),
MaxPerOracleUnexpiredBlobCumulativePayloadBytes: int(configProto.MaxPerOracleUnexpiredBlobCumulativePayloadBytes),
},
}
return rp, info, nil
}

func (o *factoryOCR3_1) Start(ctx context.Context) error {
return o.StartOnce("OCR3_1ReportingPlugin", func() error { return nil })
}

func (o *factoryOCR3_1) Close() error {
return o.StopOnce("OCR3_1ReportingPlugin", func() error { return nil })
}

func (o *factoryOCR3_1) Name() string { return o.lggr.Name() }
func (o *factoryOCR3_1) HealthReport() map[string]error { return map[string]error{o.Name(): o.Healthy()} }

// Ensure factoryOCR3_1 satisfies the libocr interface.
var _ ocr3_1types.ReportingPluginFactory[[]byte] = (*factoryOCR3_1)(nil)

// ensure time import is retained regardless of future edits
var _ = time.Second
114 changes: 114 additions & 0 deletions pkg/capabilities/consensus/ocr3/ocr3_1.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package ocr3

import (
"context"
"errors"
"time"

"github.com/jonboulle/clockwork"

"github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/types"
"github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/requests"
"github.com/smartcontractkit/chainlink-common/pkg/logger"
"github.com/smartcontractkit/chainlink-common/pkg/loop"
"github.com/smartcontractkit/chainlink-common/pkg/loop/reportingplugins"
"github.com/smartcontractkit/chainlink-common/pkg/types/core"
)

// CapabilityOCR3_1 is the OCR3_1 entry point, parallel to Capability in
// ocr3.go. It is intentionally its own type so the OCR3 path remains
// untouched during the staged rollout (plan §3.8).
//
// Unlike the OCR3 Capability, this one does not implement the LOOP
// ProviderServer interface in v1 — following the Vault precedent where the
// OCR3_1 plugin is instantiated directly in-process rather than over LOOP's
// gRPC boundary. Adding a LOOP sibling is a separate follow-up (plan §3.12).
type CapabilityOCR3_1 struct {
loop.Plugin
reportingplugins.PluginProviderServer
config Config
capabilityRegistry core.CapabilitiesRegistry
}

// NewOCR3_1 constructs the OCR3_1 capability using the same Config shape as
// NewOCR3. Defaults mirror the OCR3 path so migration does not require
// reconfiguring the caller-supplied fields.
func NewOCR3_1(config Config) *CapabilityOCR3_1 {
if config.RequestTimeout == nil {
dre := defaultRequestExpiry
config.RequestTimeout = &dre
}
if config.SendBufferSize == 0 {
config.SendBufferSize = defaultSendBufferSize
}
if config.clock == nil {
config.clock = clockwork.NewRealClock()
}
if config.store == nil {
config.store = requests.NewStore[*ReportRequest]()
}
if config.capability == nil {
ci := NewCapability(
config.store,
config.clock,
*config.RequestTimeout,
config.AggregatorFactory,
config.EncoderFactory,
config.Logger,
config.SendBufferSize,
)
config.capability = ci
}
cp := &CapabilityOCR3_1{
Plugin: loop.Plugin{Logger: config.Logger},
PluginProviderServer: reportingplugins.PluginProviderServer{},
config: config,
}
cp.SubService(config.capability)
return cp
}

// NewReportingPluginFactoryOCR3_1 returns the OCR3_1 factory directly
// (*factoryOCR3_1 implements ocr3_1types.ReportingPluginFactory[[]byte]).
//
// Callers that drive libocr's OCR3_1 oracle harness should use this entry
// point. The integration-test framework in chainlink wires through here.
func (o *CapabilityOCR3_1) NewReportingPluginFactoryOCR3_1(
ctx context.Context,
_ core.ReportingPluginServiceConfig,
capabilityRegistry core.CapabilitiesRegistry,
) (*factoryOCR3_1, error) {
f, err := newFactoryOCR3_1(o.config.store, o.config.capability, o.config.Logger)
if err != nil {
return nil, err
}
if err := capabilityRegistry.Add(ctx, o.config.capability); err != nil {
return nil, err
}
o.capabilityRegistry = capabilityRegistry
return f, nil
}

// NewValidationServiceOCR3_1 mirrors the OCR3 validation-service entry.
// No behavioral difference — validation is over offchain config bytes which
// share a schema across OCR3 / OCR3_1 (with the new OCR3_1 fields additive).
func (o *CapabilityOCR3_1) NewValidationServiceOCR3_1(ctx context.Context) (core.ValidationService, error) {
s := &validationService{lggr: o.Logger}
o.SubService(s)
return s, nil
}

func (o *CapabilityOCR3_1) Close() error {
err := o.Plugin.Close()
if o.capabilityRegistry != nil {
err = errors.Join(err, o.capabilityRegistry.Remove(context.TODO(), o.config.capability.ID))
}
return err
}

// ensure unused imports are retained against future additions
var (
_ = time.Second
_ *types.ReportingPluginConfig
_ = logger.Nop
)
Loading
Loading