diff --git a/pkg/capabilities/consensus/ocr3/aggregators/reduce_aggregator.go b/pkg/capabilities/consensus/ocr3/aggregators/reduce_aggregator.go index 43acd1951..ddcfd5a26 100644 --- a/pkg/capabilities/consensus/ocr3/aggregators/reduce_aggregator.go +++ b/pkg/capabilities/consensus/ocr3/aggregators/reduce_aggregator.go @@ -306,11 +306,15 @@ func (a *reduceAggregator) extractValues(lggr logger.Logger, observations map[oc // values are then re-wrapped here to handle aggregating against Value types // which is used for mode aggregation switch val := val.(type) { - case map[string]interface{}: + case map[string]any: _, ok := val[aggregationKey] if !ok { continue } + if val[aggregationKey] == nil { + lggr.Warnf("node %d contributed with a nil value under key %s", nodeID, aggregationKey) + continue + } rewrapped, err := values.Wrap(val[aggregationKey]) if err != nil { @@ -318,12 +322,17 @@ func (a *reduceAggregator) extractValues(lggr logger.Logger, observations map[oc continue } vals = append(vals, rewrapped) - case []interface{}: + case []any: i, err := strconv.Atoi(aggregationKey) if err != nil { lggr.Warnf("aggregation key %s could not be used to index a list type", aggregationKey) continue } + if i >= len(val) { + lggr.Warnf("node %d contributed with an array shorter than index %s", nodeID, aggregationKey) + continue + } + rewrapped, err := values.Wrap(val[i]) if err != nil { lggr.Warnf("unable to wrap value %s", val[i]) diff --git a/pkg/capabilities/consensus/ocr3/aggregators/reduce_test.go b/pkg/capabilities/consensus/ocr3/aggregators/reduce_test.go index d2217406b..16ed166b2 100644 --- a/pkg/capabilities/consensus/ocr3/aggregators/reduce_test.go +++ b/pkg/capabilities/consensus/ocr3/aggregators/reduce_test.go @@ -567,6 +567,61 @@ func TestReduceAggregator_Aggregate(t *testing.T) { }, expectedState: map[string]any{"Price": int64(1)}, }, + { + name: "handle nils gracefully", + fields: []aggregators.AggregationField{ + { + InputKey: "FeedID", + OutputKey: "FeedID", + Method: "mode", + }, + { + InputKey: "BenchmarkPrice", + OutputKey: "Price", + Method: "median", + DeviationString: "10", + DeviationType: "percent", + }, + { + InputKey: "Timestamp", + OutputKey: "Timestamp", + Method: "median", + DeviationString: "100", + DeviationType: "absolute", + }, + }, + extraConfig: map[string]any{}, + observationsFactory: func() map[commontypes.OracleID][]values.Value { + mockValue, err := values.WrapMap(map[string]any{ + "FeedID": idABytes[:], + "BenchmarkPrice": uint64(100), + "Timestamp": 12341414929, + }) + require.NoError(t, err) + mockValueWithNil, err := values.WrapMap(map[string]any{ + "FeedID": idABytes[:], + "BenchmarkPrice": uint64(100), + "Timestamp": 12341414929, + }) + mockValueWithNil.Underlying["BenchmarkPrice"] = nil // simulate failed wraping of uint64 + return map[commontypes.OracleID][]values.Value{1: {mockValue}, 2: {mockValue}, 3: {mockValue}, 4: {mockValueWithNil}} + }, + shouldReport: true, + expectedOutcome: map[string]any{ + "Reports": []any{ + map[string]any{ + "FeedID": idABytes[:], + "Timestamp": int64(12341414929), + "Price": uint64(100), + }, + }, + }, + expectedState: map[string]any{ + "FeedID": idABytes[:], + "Timestamp": int64(12341414929), + "Price": uint64(100), + }, + }, } for _, tt := range cases { t.Run(tt.name, func(t *testing.T) {