Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
537c8af
Add outcome for previous round missing request ids
yashnevatia Sep 14, 2026
5753fe6
gate behind config flag
yashnevatia Sep 15, 2026
846dda9
Merge branch 'main' into add-outcome-for-prevMissingReqIDs
yashnevatia Sep 15, 2026
330bae5
bump libs in evm cap
yashnevatia Sep 15, 2026
823809e
Merge branch 'add-outcome-for-prevMissingReqIDs' of ssh://github.com/…
yashnevatia Sep 15, 2026
70cd73b
use settings flag and check for quorum
yashnevatia Sep 18, 2026
8406273
make generate
yashnevatia Sep 18, 2026
91b16df
rename field
yashnevatia Sep 18, 2026
73d6ca5
merging with main
yashnevatia Sep 18, 2026
ae39458
clean evm
yashnevatia Sep 18, 2026
c6ea99d
remove reports test
yashnevatia Sep 18, 2026
0cfaab7
byzantine
yashnevatia Sep 18, 2026
faec3ba
bump libs to use byzantine
yashnevatia Sep 18, 2026
608578d
merging with main but retaining our mods
yashnevatia Sep 23, 2026
cbbb0f5
only keep ocr changes in this
yashnevatia Sep 23, 2026
4451402
Test hack in main pr
yashnevatia Sep 23, 2026
569a4b0
bumping common merged with main and adding log lines
yashnevatia Sep 23, 2026
2d8f07f
merging
yashnevatia Sep 25, 2026
f6f0bfb
removing test hack
yashnevatia Sep 28, 2026
5b1246b
bump common in libs
yashnevatia Sep 28, 2026
46ffec0
replace log line
yashnevatia Sep 28, 2026
70290d3
return directly
yashnevatia Sep 28, 2026
7250618
make tidy;make generate
yashnevatia Sep 28, 2026
9031ad9
bump golangci-lint
yashnevatia Sep 28, 2026
68e5768
clean
yashnevatia Sep 28, 2026
73c3b08
gomodguard_v2
yashnevatia Sep 28, 2026
0bdc90e
gomodguard_v2
yashnevatia Sep 28, 2026
c171d01
gomodguard_v2
yashnevatia Sep 28, 2026
c7354c1
golang 1.27.1
yashnevatia Sep 28, 2026
eb5b3da
lint
yashnevatia Sep 28, 2026
011d379
bump go
yashnevatia Sep 28, 2026
fd51724
update mocks
yashnevatia Sep 28, 2026
6d4117c
remove stellar service mock declaration
yashnevatia Sep 28, 2026
011a3d1
aptos proto mock version
yashnevatia Sep 28, 2026
efb7ddc
merging
yashnevatia Oct 1, 2026
04a88b6
rm chain_capabilities/monitoring/mocks/logger.go
yashnevatia Oct 1, 2026
a88249c
err
yashnevatia Oct 1, 2026
170a064
Revert root go files
yashnevatia Oct 1, 2026
eb49dd5
bump root go
yashnevatia Oct 1, 2026
adbc1e6
Merge branch 'main' into add-outcome-for-prevMissingReqIDs
yashnevatia Oct 1, 2026
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
7 changes: 3 additions & 4 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ linters:
- depguard
- errorlint
- exhaustive
- gomodguard
- gomodguard_v2
- gosec
- loggercheck
- misspell
Expand Down Expand Up @@ -88,9 +88,8 @@ linters:
errorf: false
exhaustive:
default-signifies-exhaustive: true
gomodguard:
blocked:
local-replace-directives: false
gomodguard_v2:
local-replace-directives: false
gosec:
excludes:
- G101
Expand Down
92 changes: 83 additions & 9 deletions libs/chainconsensus/oracle/reporting_plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import (
ctypes "github.com/smartcontractkit/capabilities/libs/chainconsensus/types"

"github.com/smartcontractkit/chainlink-common/pkg/logger"
"github.com/smartcontractkit/chainlink-common/pkg/settings/cresettings"
"github.com/smartcontractkit/chainlink-common/pkg/settings/limits"
)

const (
Expand Down Expand Up @@ -63,11 +65,12 @@ func (c Config) matchingThreshold() int {
}

type reportingPlugin struct {
config Config
logger logger.SugaredLogger
blocksProvider BlocksProvider
requestsStore RequestsHandler
metrics metrics.ConsensusMetrics
config Config
logger logger.SugaredLogger
blocksProvider BlocksProvider
requestsStore RequestsHandler
metrics metrics.ConsensusMetrics
enableMissingRequestRecovery limits.GateLimiter
}

func newReportingPlugin(
Expand All @@ -76,14 +79,22 @@ func newReportingPlugin(
blocksProvider BlocksProvider,
requestsStore RequestsHandler,
metrics metrics.ConsensusMetrics,
) *reportingPlugin {
return &reportingPlugin{
limitsFactory limits.Factory,
) (*reportingPlugin, error) {
rp := &reportingPlugin{
config: config,
logger: logger,
blocksProvider: blocksProvider,
requestsStore: requestsStore,
metrics: metrics,
}

var err error
rp.enableMissingRequestRecovery, err = limits.MakeGateLimiter(limitsFactory, cresettings.Default.MissingRequestRecoveryEnabled)
if err != nil {
return nil, fmt.Errorf("failed to create missing request recovery gate limiter: %w", err)
}
return rp, nil
}

func (rp *reportingPlugin) Query(ctx context.Context, outctx ocr3types.OutcomeContext) (types.Query, error) {
Expand Down Expand Up @@ -185,6 +196,12 @@ func (rp *reportingPlugin) Observation(
return nil, fmt.Errorf("failed to add observations for missing requests from previous outcome: %w", err)
}

open, err := rp.enableMissingRequestRecovery.IsOpen(ctx)
if err != nil {
rp.logger.Errorw("error checking if enableMissingRequestRecovery is enabled", "error", err)
}
observation.EnableMissingRequestRecovery = open

// add observations for requests provided by the leader in the query
err = rp.addObservations(ctx, query.RequestIDs, observation)
if err != nil {
Expand Down Expand Up @@ -294,6 +311,11 @@ func (rp *reportingPlugin) getMissingRequestIDs(roundRequests map[string]struct{
}
}

if len(missingRequestIDs) > 0 {
rp.logger.Infow("Proposing missing request IDs: present in local store but absent from the leader's query",
"missingRequestIDs", missingRequestIDs)
}

return missingRequestIDs, nil
}

Expand Down Expand Up @@ -563,9 +585,34 @@ func (rp *reportingPlugin) agreeOnMissingRequestIDs(aos []attributedObservation)
}

sort.Strings(result)
if len(result) > 0 {
rp.logger.Infow("Quorum agreed on missing request IDs: committing them to the outcome for recovery in the next round",
"missingRequestIDs", result)
}
return result, nil
}

// agreeOnFeatureEnableMissingRequestRecovery reaches DON-wide quorum, once per round, on
// whether missing-request recovery is enabled. This is a per-node, per-round capability flag
// unrelated to any specific request ID, so it is voted on independently of per-request
// observation quorum (which the aggregation loop in Outcome enforces on its own).
// TODO: clean up settings flag and logic when all nodes in all envs support missing-request recovery: https://smartcontract-it.atlassian.net/browse/PLEX-3461
func (rp *reportingPlugin) agreeOnEnableMissingRequestRecovery(aos []attributedObservation) bool {
minMatching := byzQuorumSize(rp.config.N, rp.config.F)
counter := 0
for _, ob := range aos {
if ob.Observation.EnableMissingRequestRecovery {
counter++
if counter >= minMatching {
rp.logger.Infow("Quorum reached for enabling missing request recovery: recovered requests will be aggregated into this round's outcome",
"votes", counter, "required", minMatching)
return true
}
}
}
return false
}

func (rp *reportingPlugin) agreeOnEventuallyConsistentValue(requestID string, aos []attributedObservation) ([]byte, int, error) {
iterator := func(yield func(commontypes.OracleID, *observation[[32]byte, []byte]) bool) {
for _, ob := range aos {
Expand Down Expand Up @@ -786,7 +833,34 @@ func (rp *reportingPlugin) Outcome(
return nil, fmt.Errorf("failed to unmarshal request IDs: %w", err)
}

for _, requestID := range query.RequestIDs {
requestIDs := query.RequestIDs
if prevOutcome := rp.tryUnmarshalPreviousOutcome(outctx); prevOutcome != nil && rp.agreeOnEnableMissingRequestRecovery(aos) {
seen := make(map[string]struct{}, len(query.RequestIDs))
for _, requestID := range query.RequestIDs {
seen[requestID] = struct{}{}
}

// Requests that the leader's query omitted but that a quorum of nodes still supplied
// observations for (via addObservationsOfPrevMissingRequests) must still be aggregated here,
// otherwise they would be recycled into MissingRequestIDs forever instead of getting resolved.
recoveredRequestIDs := make([]string, 0, len(prevOutcome.MissingRequestIDs))
for _, requestID := range prevOutcome.MissingRequestIDs {
if _, ok := seen[requestID]; ok {
continue
}
seen[requestID] = struct{}{}
requestIDs = append(requestIDs, requestID)
recoveredRequestIDs = append(recoveredRequestIDs, requestID)
}

if len(recoveredRequestIDs) > 0 {
rp.logger.Infow("Missing request recovery: adding previously missing requests to this round's outcome "+
"(they were omitted by the leader's query but agreed upon by a quorum in the previous round)",
"recoveredRequestIDs", recoveredRequestIDs)
}
}

for _, requestID := range requestIDs {
observationType, err := rp.agreeOnObservationType(requestID, aos)
if err != nil {
rp.logger.Infow("Could not determine observation type", "requestID", requestID, "err", err)
Expand Down Expand Up @@ -969,5 +1043,5 @@ func (rp *reportingPlugin) ShouldTransmitAcceptedReport(
}

func (rp *reportingPlugin) Close() error {
return nil
return rp.enableMissingRequestRecovery.Close()
}
11 changes: 10 additions & 1 deletion libs/chainconsensus/oracle/reporting_plugin_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"google.golang.org/protobuf/proto"

"github.com/smartcontractkit/chainlink-common/pkg/logger"
"github.com/smartcontractkit/chainlink-common/pkg/settings/limits"

"github.com/smartcontractkit/capabilities/libs/chainconsensus/metrics"

Expand All @@ -21,19 +22,22 @@ type ReportingPluginFactory struct {
requestsStore RequestsHandler
blocksProvider BlocksProvider
metrics metrics.ConsensusMetrics
limitsFactory limits.Factory
}

func NewReportingPluginFactory(
logger logger.SugaredLogger,
requestsStore RequestsHandler,
blocksProvider BlocksProvider,
metrics metrics.ConsensusMetrics,
limitsFactory limits.Factory,
) *ReportingPluginFactory {
return &ReportingPluginFactory{
logger: logger,
requestsStore: requestsStore,
blocksProvider: blocksProvider,
metrics: metrics,
limitsFactory: limitsFactory,
}
}

Expand Down Expand Up @@ -69,7 +73,12 @@ func (rpf *ReportingPluginFactory) NewReportingPlugin(

pluginLogger := rpf.logger.Named("ChainReadReportingPlugin")

return newReportingPlugin(cfg, pluginLogger, rpf.blocksProvider, rpf.requestsStore, rpf.metrics), ocr3types.ReportingPluginInfo{
plugin, err := newReportingPlugin(cfg, pluginLogger, rpf.blocksProvider, rpf.requestsStore, rpf.metrics, rpf.limitsFactory)
if err != nil {
return nil, ocr3types.ReportingPluginInfo{}, fmt.Errorf("failed to create reporting plugin: %w", err)
}

return plugin, ocr3types.ReportingPluginInfo{
Name: rpf.logger.Name() + ".chain-reads-oracle",
Limits: ocr3types.ReportingPluginLimits{
MaxQueryLength: int(offchainCfg.MaxQueryLengthBytes),
Expand Down
Loading
Loading