-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into will/unknown-addr-2
- Loading branch information
Showing
115 changed files
with
1,792 additions
and
1,232 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
124 changes: 124 additions & 0 deletions
124
pkg/capabilities/consensus/ocr3/aggregators/identical.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
package aggregators | ||
|
||
import ( | ||
"crypto/sha256" | ||
"fmt" | ||
|
||
"google.golang.org/protobuf/proto" | ||
|
||
"github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/types" | ||
"github.com/smartcontractkit/chainlink-common/pkg/logger" | ||
"github.com/smartcontractkit/chainlink-common/pkg/values" | ||
|
||
ocrcommon "github.com/smartcontractkit/libocr/commontypes" | ||
) | ||
|
||
type identicalAggregator struct { | ||
config aggregatorConfig | ||
lggr logger.Logger | ||
} | ||
|
||
type aggregatorConfig struct { | ||
// Length of the list of observations that each node is expected to provide. | ||
// Aggregator's output (i.e. EncodableOutcome) will be a values.Map with the same | ||
// number of elements and keyed by indices 0,1,2,... (unless KeyOverrides are provided). | ||
// Defaults to 1. | ||
ExpectedObservationsLen int | ||
// If non-empty, the keys in the outcome map will be replaced with these values. | ||
// If non-empty, must be of length ExpectedObservationsLen. | ||
KeyOverrides []string | ||
} | ||
|
||
type counter struct { | ||
fullObservation values.Value | ||
count int | ||
} | ||
|
||
var _ types.Aggregator = (*identicalAggregator)(nil) | ||
|
||
func (a *identicalAggregator) Aggregate(lggr logger.Logger, _ *types.AggregationOutcome, observations map[ocrcommon.OracleID][]values.Value, f int) (*types.AggregationOutcome, error) { | ||
counters := []map[[32]byte]*counter{} | ||
for i := 0; i < a.config.ExpectedObservationsLen; i++ { | ||
counters = append(counters, map[[32]byte]*counter{}) | ||
} | ||
for nodeID, nodeObservations := range observations { | ||
if len(nodeObservations) == 0 || nodeObservations[0] == nil { | ||
lggr.Warnf("node %d contributed with empty observations", nodeID) | ||
continue | ||
} | ||
if len(nodeObservations) != a.config.ExpectedObservationsLen { | ||
lggr.Warnf("node %d contributed with an incorrect number of observations %d - ignoring them", nodeID, len(nodeObservations)) | ||
continue | ||
} | ||
for idx, observation := range nodeObservations { | ||
marshalled, err := proto.MarshalOptions{Deterministic: true}.Marshal(values.Proto(observation)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
sha := sha256.Sum256(marshalled) | ||
elem, ok := counters[idx][sha] | ||
if !ok { | ||
counters[idx][sha] = &counter{ | ||
fullObservation: observation, | ||
count: 1, | ||
} | ||
} else { | ||
elem.count++ | ||
} | ||
} | ||
} | ||
return a.collectHighestCounts(counters, f) | ||
} | ||
|
||
func (a *identicalAggregator) collectHighestCounts(counters []map[[32]byte]*counter, f int) (*types.AggregationOutcome, error) { | ||
useOverrides := len(a.config.KeyOverrides) == len(counters) | ||
outcome := make(map[string]any) | ||
for idx, shaToCounter := range counters { | ||
highestCount := 0 | ||
var highestObservation values.Value | ||
for _, counter := range shaToCounter { | ||
if counter.count > highestCount { | ||
highestCount = counter.count | ||
highestObservation = counter.fullObservation | ||
} | ||
} | ||
if highestCount < 2*f+1 { | ||
return nil, fmt.Errorf("can't reach consensus on observations with index %d", idx) | ||
} | ||
if useOverrides { | ||
outcome[a.config.KeyOverrides[idx]] = highestObservation | ||
} else { | ||
outcome[fmt.Sprintf("%d", idx)] = highestObservation | ||
} | ||
} | ||
valMap, err := values.NewMap(outcome) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return &types.AggregationOutcome{ | ||
EncodableOutcome: values.ProtoMap(valMap), | ||
Metadata: nil, | ||
ShouldReport: true, | ||
}, nil | ||
} | ||
|
||
func NewIdenticalAggregator(config values.Map) (*identicalAggregator, error) { | ||
parsedConfig, err := ParseConfig(config) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to parse config (%+v): %w", config, err) | ||
} | ||
return &identicalAggregator{ | ||
config: parsedConfig, | ||
}, nil | ||
} | ||
|
||
func ParseConfig(config values.Map) (aggregatorConfig, error) { | ||
parsedConfig := aggregatorConfig{} | ||
if err := config.UnwrapTo(&parsedConfig); err != nil { | ||
return aggregatorConfig{}, err | ||
} | ||
if parsedConfig.ExpectedObservationsLen == 0 { | ||
parsedConfig.ExpectedObservationsLen = 1 | ||
} | ||
return parsedConfig, nil | ||
} |
93 changes: 93 additions & 0 deletions
93
pkg/capabilities/consensus/ocr3/aggregators/identical_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
package aggregators_test | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/smartcontractkit/libocr/commontypes" | ||
|
||
"github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/aggregators" | ||
"github.com/smartcontractkit/chainlink-common/pkg/logger" | ||
"github.com/smartcontractkit/chainlink-common/pkg/values" | ||
) | ||
|
||
func TestDataFeedsAggregator_Aggregate(t *testing.T) { | ||
config := getConfig(t, nil) | ||
agg, err := aggregators.NewIdenticalAggregator(*config) | ||
require.NoError(t, err) | ||
|
||
observations := map[commontypes.OracleID][]values.Value{ | ||
0: {values.NewString("a")}, | ||
1: {values.NewString("a")}, | ||
2: {values.NewString("a")}, | ||
3: {values.NewString("a")}, | ||
} | ||
outcome, err := agg.Aggregate(logger.Nop(), nil, observations, 1) | ||
require.NoError(t, err) | ||
require.True(t, outcome.ShouldReport) | ||
require.Equal(t, "", outcome.EncoderName) | ||
require.Nil(t, outcome.EncoderConfig) | ||
|
||
m, err := values.FromMapValueProto(outcome.EncodableOutcome) | ||
require.NoError(t, err) | ||
|
||
require.Len(t, m.Underlying, 1) | ||
require.Equal(t, m.Underlying["0"], values.NewString("a")) | ||
} | ||
|
||
func TestDataFeedsAggregator_Aggregate_OverrideWithKeys(t *testing.T) { | ||
config := getConfig(t, []string{"outcome"}) | ||
agg, err := aggregators.NewIdenticalAggregator(*config) | ||
require.NoError(t, err) | ||
|
||
observations := map[commontypes.OracleID][]values.Value{ | ||
0: {values.NewString("a")}, | ||
1: {values.NewString("a")}, | ||
2: {values.NewString("a")}, | ||
3: {values.NewString("a")}, | ||
} | ||
outcome, err := agg.Aggregate(logger.Nop(), nil, observations, 1) | ||
require.NoError(t, err) | ||
require.True(t, outcome.ShouldReport) | ||
require.Equal(t, "", outcome.EncoderName) | ||
require.Nil(t, outcome.EncoderConfig) | ||
|
||
m, err := values.FromMapValueProto(outcome.EncodableOutcome) | ||
require.NoError(t, err) | ||
|
||
require.Len(t, m.Underlying, 1) | ||
require.Equal(t, m.Underlying["outcome"], values.NewString("a")) | ||
} | ||
|
||
func TestDataFeedsAggregator_Aggregate_NoConsensus(t *testing.T) { | ||
config := getConfig(t, []string{"outcome"}) | ||
agg, err := aggregators.NewIdenticalAggregator(*config) | ||
require.NoError(t, err) | ||
|
||
encoderStr := "evm" | ||
encoderName := values.NewString(encoderStr) | ||
encoderCfg, err := values.NewMap(map[string]any{"foo": "bar"}) | ||
require.NoError(t, err) | ||
|
||
observations := map[commontypes.OracleID][]values.Value{ | ||
0: {values.NewString("a"), encoderName, encoderCfg}, | ||
1: {values.NewString("b"), encoderName, encoderCfg}, | ||
2: {values.NewString("b"), encoderName, encoderCfg}, | ||
3: {values.NewString("a"), encoderName, encoderCfg}, | ||
} | ||
outcome, err := agg.Aggregate(logger.Nop(), nil, observations, 1) | ||
require.Nil(t, outcome) | ||
require.ErrorContains(t, err, "can't reach consensus on observations with index 0") | ||
} | ||
|
||
func getConfig(t *testing.T, overrideKeys []string) *values.Map { | ||
unwrappedConfig := map[string]any{ | ||
"expectedObservationsLen": len(overrideKeys), | ||
"keyOverrides": overrideKeys, | ||
} | ||
|
||
config, err := values.NewMap(unwrappedConfig) | ||
require.NoError(t, err) | ||
return config | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.