From 10efc218e791e78b4e3a78e67d8545fbe5c011d2 Mon Sep 17 00:00:00 2001 From: Bolek Kulbabinski <1416262+bolekk@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:11:56 -0700 Subject: [PATCH 1/2] [CRE] Remove obsolete v1 capabilities (part 1) --- .../readcontract/action_builders_generated.go | 92 - .../readcontract_action-schema.json | 62 - .../readcontract_action_generated.go | 118 - .../readcontracttest/action_mock_generated.go | 27 - .../consensus/ocr3/aggregators/reduce_test.go | 2016 ----------------- .../consensus/ocr3/benchmark_test.go | 494 ---- .../ocr3/datafeeds/data_feeds_types.pb.go | 324 --- .../ocr3/datafeeds/data_feeds_types.proto | 26 - .../datafeeds/example_llo_aggregator_test.go | 128 -- .../ocr3/datafeeds/feeds_aggregator.go | 395 ---- .../ocr3/datafeeds/feeds_aggregator_test.go | 254 --- .../consensus/ocr3/datafeeds/helpers_test.go | 6 - .../ocr3/datafeeds/llo_aggregator.go | 463 ---- .../ocr3/datafeeds/llo_aggregator_test.go | 722 ------ .../ocr3/datafeeds/securemint_aggregator.go | 497 ---- .../datafeeds/securemint_aggregator_test.go | 630 ------ ...data_feeds_consensus_builders_generated.go | 113 - .../ocr3cap_data_feeds_consensus-schema.json | 115 - .../ocr3cap_data_feeds_consensus_generated.go | 220 -- .../datastreams/mocks/report_codec.go | 201 -- pkg/capabilities/datastreams/types.go | 187 -- pkg/capabilities/datastreams/types_test.go | 69 - .../triggers/cron/cron_trigger-schema.json | 39 - .../triggers/cron/cron_trigger_generated.go | 86 - .../cron/crontest/trigger_mock_generated.go | 17 - .../cron/trigger_builders_generated.go | 73 - .../triggers/mercury_remote_aggregator.go | 97 - .../mercury_remote_aggregator_test.go | 108 - pkg/capabilities/triggers/mercury_trigger.go | 260 --- .../triggers/mercury_trigger_test.go | 373 --- .../triggers/on_demand_trigger.go | 2 +- .../triggers/on_demand_trigger_test.go | 22 - .../streams/streams_trigger-schema.json | 139 -- .../streams/streams_trigger_generated.go | 208 -- .../streamstest/trigger_mock_generated.go | 17 - .../streams/trigger_builders_generated.go | 271 --- .../testdata/fixtures/mercury/test.yaml | 14 - .../testdata/fixtures/ondemand/test.yaml | 6 - pkg/workflows/sdk/builder_test.go | 676 ------ pkg/workflows/sdk/compute_test.go | 254 --- 40 files changed, 1 insertion(+), 9820 deletions(-) delete mode 100644 pkg/capabilities/actions/readcontract/action_builders_generated.go delete mode 100644 pkg/capabilities/actions/readcontract/readcontract_action-schema.json delete mode 100644 pkg/capabilities/actions/readcontract/readcontract_action_generated.go delete mode 100644 pkg/capabilities/actions/readcontract/readcontracttest/action_mock_generated.go delete mode 100644 pkg/capabilities/consensus/ocr3/aggregators/reduce_test.go delete mode 100644 pkg/capabilities/consensus/ocr3/benchmark_test.go delete mode 100644 pkg/capabilities/consensus/ocr3/datafeeds/data_feeds_types.pb.go delete mode 100644 pkg/capabilities/consensus/ocr3/datafeeds/data_feeds_types.proto delete mode 100644 pkg/capabilities/consensus/ocr3/datafeeds/example_llo_aggregator_test.go delete mode 100644 pkg/capabilities/consensus/ocr3/datafeeds/feeds_aggregator.go delete mode 100644 pkg/capabilities/consensus/ocr3/datafeeds/feeds_aggregator_test.go delete mode 100644 pkg/capabilities/consensus/ocr3/datafeeds/helpers_test.go delete mode 100644 pkg/capabilities/consensus/ocr3/datafeeds/llo_aggregator.go delete mode 100644 pkg/capabilities/consensus/ocr3/datafeeds/llo_aggregator_test.go delete mode 100644 pkg/capabilities/consensus/ocr3/datafeeds/securemint_aggregator.go delete mode 100644 pkg/capabilities/consensus/ocr3/datafeeds/securemint_aggregator_test.go delete mode 100644 pkg/capabilities/consensus/ocr3/ocr3cap/data_feeds_consensus_builders_generated.go delete mode 100644 pkg/capabilities/consensus/ocr3/ocr3cap/ocr3cap_data_feeds_consensus-schema.json delete mode 100644 pkg/capabilities/consensus/ocr3/ocr3cap/ocr3cap_data_feeds_consensus_generated.go delete mode 100644 pkg/capabilities/datastreams/mocks/report_codec.go delete mode 100644 pkg/capabilities/datastreams/types.go delete mode 100644 pkg/capabilities/datastreams/types_test.go delete mode 100644 pkg/capabilities/triggers/cron/cron_trigger-schema.json delete mode 100644 pkg/capabilities/triggers/cron/cron_trigger_generated.go delete mode 100644 pkg/capabilities/triggers/cron/crontest/trigger_mock_generated.go delete mode 100644 pkg/capabilities/triggers/cron/trigger_builders_generated.go delete mode 100644 pkg/capabilities/triggers/mercury_remote_aggregator.go delete mode 100644 pkg/capabilities/triggers/mercury_remote_aggregator_test.go delete mode 100644 pkg/capabilities/triggers/mercury_trigger.go delete mode 100644 pkg/capabilities/triggers/mercury_trigger_test.go delete mode 100644 pkg/capabilities/triggers/streams/streams_trigger-schema.json delete mode 100644 pkg/capabilities/triggers/streams/streams_trigger_generated.go delete mode 100644 pkg/capabilities/triggers/streams/streamstest/trigger_mock_generated.go delete mode 100644 pkg/capabilities/triggers/streams/trigger_builders_generated.go delete mode 100644 pkg/capabilities/triggers/testdata/fixtures/mercury/test.yaml delete mode 100644 pkg/capabilities/triggers/testdata/fixtures/ondemand/test.yaml delete mode 100644 pkg/workflows/sdk/builder_test.go delete mode 100644 pkg/workflows/sdk/compute_test.go diff --git a/pkg/capabilities/actions/readcontract/action_builders_generated.go b/pkg/capabilities/actions/readcontract/action_builders_generated.go deleted file mode 100644 index 65b4b587a4..0000000000 --- a/pkg/capabilities/actions/readcontract/action_builders_generated.go +++ /dev/null @@ -1,92 +0,0 @@ -// Code generated by github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli, DO NOT EDIT. - -package readcontract - -import ( - "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - "github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk" -) - -func (cfg Config) New(w *sdk.WorkflowSpecFactory, id string, ref string, input ActionInput) OutputCap { - - def := sdk.StepDefinition{ - ID: id, Ref: ref, - Inputs: input.ToSteps(), - Config: map[string]any{ - "ContractAddress": cfg.ContractAddress, - "ContractName": cfg.ContractName, - "ContractReaderConfig": cfg.ContractReaderConfig, - "ReadIdentifier": cfg.ReadIdentifier, - }, - CapabilityType: capabilities.CapabilityTypeAction, - } - - step := sdk.Step[Output]{Definition: def} - raw := step.AddTo(w) - return OutputWrapper(raw) -} - -// OutputWrapper allows access to field from an sdk.CapDefinition[Output] -func OutputWrapper(raw sdk.CapDefinition[Output]) OutputCap { - wrapped, ok := raw.(OutputCap) - if ok { - return wrapped - } - return &outputCap{CapDefinition: raw} -} - -type OutputCap interface { - sdk.CapDefinition[Output] - LatestValue() sdk.CapDefinition[any] - private() -} - -type outputCap struct { - sdk.CapDefinition[Output] -} - -func (*outputCap) private() {} -func (c *outputCap) LatestValue() sdk.CapDefinition[any] { - return sdk.AccessField[Output, any](c.CapDefinition, "LatestValue") -} - -func ConstantOutput(value Output) OutputCap { - return &outputCap{CapDefinition: sdk.ConstantDefinition(value)} -} - -func NewOutputFromFields( - latestValue sdk.CapDefinition[any]) OutputCap { - return &simpleOutput{ - CapDefinition: sdk.ComponentCapDefinition[Output]{ - "LatestValue": latestValue.Ref(), - }, - latestValue: latestValue, - } -} - -type simpleOutput struct { - sdk.CapDefinition[Output] - latestValue sdk.CapDefinition[any] -} - -func (c *simpleOutput) LatestValue() sdk.CapDefinition[any] { - return c.latestValue -} - -func (c *simpleOutput) private() {} - -type ActionInput struct { - ConfidenceLevel sdk.CapDefinition[string] - Params sdk.CapDefinition[InputParams] - StepDependency sdk.CapDefinition[any] -} - -func (input ActionInput) ToSteps() sdk.StepInputs { - return sdk.StepInputs{ - Mapping: map[string]any{ - "ConfidenceLevel": input.ConfidenceLevel.Ref(), - "Params": input.Params.Ref(), - "StepDependency": input.StepDependency.Ref(), - }, - } -} diff --git a/pkg/capabilities/actions/readcontract/readcontract_action-schema.json b/pkg/capabilities/actions/readcontract/readcontract_action-schema.json deleted file mode 100644 index 472f167578..0000000000 --- a/pkg/capabilities/actions/readcontract/readcontract_action-schema.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/smartcontractkit/chainlink-common/pkg/capabilities/actions/readcontract/read-contract", - "$defs": { - "Config": { - "type": "object", - "properties": { - "ContractReaderConfig": { - "type": "string" - }, - "ReadIdentifier": { - "type": "string" - }, - "ContractAddress": { - "type": "string" - }, - "ContractName": { - "type": "string" - } - }, - "required": ["ContractReaderConfig", "ReadIdentifier", "ContractAddress", "ContractName"] - }, - "Input": { - "type": "object", - "properties": { - "ConfidenceLevel": { - "type": "string" - }, - "Params": { - "type": "object", - "additionalProperties": true - }, - "StepDependency": { - "type": ["object", "string", "boolean", "null", "array"], - "description": "an optional step reference that is a non-data dependency for the current step" - } - }, - "required": ["ConfidenceLevel", "Params"] - }, - "Output": { - "type": "object", - "properties": { - "LatestValue": { - "type": ["object", "string", "boolean", "null", "array"] - } - }, - "required": ["LatestValue"] - } - }, - "type": "object", - "properties": { - "Config": { - "$ref": "#/$defs/Config" - }, - "Inputs": { - "$ref": "#/$defs/Input" - }, - "Outputs": { - "$ref": "#/$defs/Output" - } - } -} diff --git a/pkg/capabilities/actions/readcontract/readcontract_action_generated.go b/pkg/capabilities/actions/readcontract/readcontract_action_generated.go deleted file mode 100644 index 789534ed5d..0000000000 --- a/pkg/capabilities/actions/readcontract/readcontract_action_generated.go +++ /dev/null @@ -1,118 +0,0 @@ -// Code generated by github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli, DO NOT EDIT. - -package readcontract - -import ( - "encoding/json" - "fmt" -) - -type Action struct { - // Config corresponds to the JSON schema field "Config". - Config *Config `json:"Config,omitempty" yaml:"Config,omitempty" mapstructure:"Config,omitempty"` - - // Inputs corresponds to the JSON schema field "Inputs". - Inputs *Input `json:"Inputs,omitempty" yaml:"Inputs,omitempty" mapstructure:"Inputs,omitempty"` - - // Outputs corresponds to the JSON schema field "Outputs". - Outputs *Output `json:"Outputs,omitempty" yaml:"Outputs,omitempty" mapstructure:"Outputs,omitempty"` -} - -type Config struct { - // ContractAddress corresponds to the JSON schema field "ContractAddress". - ContractAddress string `json:"ContractAddress" yaml:"ContractAddress" mapstructure:"ContractAddress"` - - // ContractName corresponds to the JSON schema field "ContractName". - ContractName string `json:"ContractName" yaml:"ContractName" mapstructure:"ContractName"` - - // ContractReaderConfig corresponds to the JSON schema field - // "ContractReaderConfig". - ContractReaderConfig string `json:"ContractReaderConfig" yaml:"ContractReaderConfig" mapstructure:"ContractReaderConfig"` - - // ReadIdentifier corresponds to the JSON schema field "ReadIdentifier". - ReadIdentifier string `json:"ReadIdentifier" yaml:"ReadIdentifier" mapstructure:"ReadIdentifier"` -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *Config) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - if _, ok := raw["ContractAddress"]; raw != nil && !ok { - return fmt.Errorf("field ContractAddress in Config: required") - } - if _, ok := raw["ContractName"]; raw != nil && !ok { - return fmt.Errorf("field ContractName in Config: required") - } - if _, ok := raw["ContractReaderConfig"]; raw != nil && !ok { - return fmt.Errorf("field ContractReaderConfig in Config: required") - } - if _, ok := raw["ReadIdentifier"]; raw != nil && !ok { - return fmt.Errorf("field ReadIdentifier in Config: required") - } - type Plain Config - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - *j = Config(plain) - return nil -} - -type Input struct { - // ConfidenceLevel corresponds to the JSON schema field "ConfidenceLevel". - ConfidenceLevel string `json:"ConfidenceLevel" yaml:"ConfidenceLevel" mapstructure:"ConfidenceLevel"` - - // Params corresponds to the JSON schema field "Params". - Params InputParams `json:"Params" yaml:"Params" mapstructure:"Params"` - - // an optional step reference that is a non-data dependency for the current step - StepDependency interface{} `json:"StepDependency,omitempty" yaml:"StepDependency,omitempty" mapstructure:"StepDependency,omitempty"` -} - -type InputParams map[string]interface{} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *Input) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - if _, ok := raw["ConfidenceLevel"]; raw != nil && !ok { - return fmt.Errorf("field ConfidenceLevel in Input: required") - } - if _, ok := raw["Params"]; raw != nil && !ok { - return fmt.Errorf("field Params in Input: required") - } - type Plain Input - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - *j = Input(plain) - return nil -} - -type Output struct { - // LatestValue corresponds to the JSON schema field "LatestValue". - LatestValue interface{} `json:"LatestValue" yaml:"LatestValue" mapstructure:"LatestValue"` -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *Output) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - if _, ok := raw["LatestValue"]; raw != nil && !ok { - return fmt.Errorf("field LatestValue in Output: required") - } - type Plain Output - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - *j = Output(plain) - return nil -} diff --git a/pkg/capabilities/actions/readcontract/readcontracttest/action_mock_generated.go b/pkg/capabilities/actions/readcontract/readcontracttest/action_mock_generated.go deleted file mode 100644 index 159c598a52..0000000000 --- a/pkg/capabilities/actions/readcontract/readcontracttest/action_mock_generated.go +++ /dev/null @@ -1,27 +0,0 @@ -// Code generated by github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli, DO NOT EDIT. - -// Code generated by github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli, DO NOT EDIT. - -package readcontracttest - -import ( - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/actions/readcontract" - "github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk/testutils" -) - -// Action registers a new capability mock with the runner -// if another mock is registered for the same capability with for a step, it will take priority for that step. -func Action(runner *testutils.Runner, id string, fn func(input readcontract.Input) (readcontract.Output, error)) *testutils.Mock[readcontract.Input, readcontract.Output] { - mock := testutils.MockCapability[readcontract.Input, readcontract.Output](id, fn) - runner.MockCapability(id, nil, mock) - return mock -} - -// ActionForStep registers a new capability mock with the runner, but only for a given step. -// if another mock was registered for the same capability without a step, this mock will take priority for that step. -func ActionForStep(runner *testutils.Runner, id string, step string, mockFn func(input readcontract.Input) (readcontract.Output, error)) *testutils.Mock[readcontract.Input, readcontract.Output] { - fn := mockFn - mock := testutils.MockCapability[readcontract.Input, readcontract.Output](id, fn) - runner.MockCapability(id, &step, mock) - return mock -} diff --git a/pkg/capabilities/consensus/ocr3/aggregators/reduce_test.go b/pkg/capabilities/consensus/ocr3/aggregators/reduce_test.go deleted file mode 100644 index 7c44a30fb2..0000000000 --- a/pkg/capabilities/consensus/ocr3/aggregators/reduce_test.go +++ /dev/null @@ -1,2016 +0,0 @@ -package aggregators_test - -import ( - "maps" - "math/big" - "testing" - "time" - - "github.com/shopspring/decimal" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" - - "github.com/smartcontractkit/libocr/commontypes" - - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/aggregators" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/types" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/datastreams" - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-protos/cre/go/values" - "github.com/smartcontractkit/chainlink-protos/cre/go/values/pb" -) - -var ( - feedIDA = datastreams.FeedID("0x0001013ebd4ed3f5889fb5a8a52b42675c60c1a8c42bc79eaa72dcd922ac4292") - idABytes = feedIDA.Bytes() - feedIDB = datastreams.FeedID("0x0003c317fec7fad514c67aacc6366bf2f007ce37100e3cddcacd0ccaa1f3746d") - idBBytes = feedIDB.Bytes() - now = time.Now() -) - -func TestReduceAggregator_Aggregate(t *testing.T) { - t.Run("happy path", func(t *testing.T) { - cases := []struct { - name string - fields []aggregators.AggregationField - extraConfig map[string]any - observationsFactory func() map[commontypes.OracleID][]values.Value - shouldReport bool - expectedState any - expectedOutcome map[string]any - previousOutcome func(t *testing.T) *types.AggregationOutcome - }{ - { - name: "aggregate on int64 median", - 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": int64(100), - "Timestamp": 12341414929, - }) - require.NoError(t, err) - return map[commontypes.OracleID][]values.Value{1: {mockValue}, 2: {mockValue}, 3: {mockValue}} - }, - shouldReport: true, - expectedOutcome: map[string]any{ - "Reports": []any{ - map[string]any{ - "FeedID": idABytes[:], - "Timestamp": int64(12341414929), - "Price": int64(100), - }, - }, - }, - expectedState: map[string]any{ - "FeedID": idABytes[:], - "Timestamp": int64(12341414929), - "Price": int64(100), - }, - }, - { - name: "aggregate on uint64 median", - 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) - return map[commontypes.OracleID][]values.Value{1: {mockValue}, 2: {mockValue}, 3: {mockValue}} - }, - 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), - }, - }, - { - name: "aggregate on decimal median", - fields: []aggregators.AggregationField{ - { - InputKey: "BenchmarkPrice", - OutputKey: "Price", - Method: "median", - DeviationString: "10", - DeviationType: "percent", - }, - }, - extraConfig: map[string]any{}, - observationsFactory: func() map[commontypes.OracleID][]values.Value { - mockValue, err := values.WrapMap(map[string]any{ - "BenchmarkPrice": decimal.NewFromInt(32), - }) - require.NoError(t, err) - return map[commontypes.OracleID][]values.Value{1: {mockValue}, 2: {mockValue}, 3: {mockValue}} - }, - shouldReport: true, - expectedOutcome: map[string]any{ - "Reports": []any{ - map[string]any{ - "Price": decimal.NewFromInt(32), - }, - }, - }, - expectedState: map[string]any{ - "Price": decimal.NewFromInt(32), - }, - }, - { - name: "aggregate on float64 median", - fields: []aggregators.AggregationField{ - { - InputKey: "BenchmarkPrice", - OutputKey: "Price", - Method: "median", - DeviationString: "10", - DeviationType: "percent", - }, - }, - extraConfig: map[string]any{}, - observationsFactory: func() map[commontypes.OracleID][]values.Value { - mockValue, err := values.WrapMap(map[string]any{ - "BenchmarkPrice": float64(1.2), - }) - require.NoError(t, err) - return map[commontypes.OracleID][]values.Value{1: {mockValue}, 2: {mockValue}, 3: {mockValue}} - }, - shouldReport: true, - expectedOutcome: map[string]any{ - "Reports": []any{ - map[string]any{ - "Price": float64(1.2), - }, - }, - }, - expectedState: map[string]any{ - "Price": float64(1.2), - }, - }, - { - name: "aggregate on time median", - fields: []aggregators.AggregationField{ - { - InputKey: "BenchmarkPrice", - OutputKey: "Price", - Method: "median", - DeviationString: "10", - DeviationType: "percent", - }, - }, - extraConfig: map[string]any{}, - observationsFactory: func() map[commontypes.OracleID][]values.Value { - mockValue, err := values.WrapMap(map[string]any{ - "BenchmarkPrice": now, - }) - require.NoError(t, err) - return map[commontypes.OracleID][]values.Value{1: {mockValue}, 2: {mockValue}, 3: {mockValue}} - }, - shouldReport: true, - expectedOutcome: map[string]any{ - "Reports": []any{ - map[string]any{ - "Price": time.Time(now).UTC(), - }, - }, - }, - expectedState: map[string]any{ - "Price": now.UTC(), - }, - }, - { - name: "aggregate on big int median", - fields: []aggregators.AggregationField{ - { - InputKey: "BenchmarkPrice", - OutputKey: "Price", - Method: "median", - DeviationString: "10", - DeviationType: "percent", - }, - }, - extraConfig: map[string]any{}, - observationsFactory: func() map[commontypes.OracleID][]values.Value { - mockValue, err := values.WrapMap(map[string]any{ - "BenchmarkPrice": big.NewInt(100), - }) - require.NoError(t, err) - return map[commontypes.OracleID][]values.Value{1: {mockValue}, 2: {mockValue}, 3: {mockValue}} - }, - shouldReport: true, - expectedOutcome: map[string]any{ - "Reports": []any{ - map[string]any{ - "Price": big.NewInt(100), - }, - }, - }, - expectedState: map[string]any{ - "Price": big.NewInt(100), - }, - }, - { - name: "aggregate with previous outcome", - 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": int64(100), - "Timestamp": 12341414929, - }) - require.NoError(t, err) - return map[commontypes.OracleID][]values.Value{1: {mockValue}, 2: {mockValue}, 3: {mockValue}} - }, - shouldReport: true, - expectedOutcome: map[string]any{ - "Reports": []any{ - map[string]any{ - "FeedID": idABytes[:], - "Timestamp": int64(12341414929), - "Price": int64(100), - }, - }, - }, - expectedState: map[string]any{ - "FeedID": idABytes[:], - "Timestamp": int64(12341414929), - "Price": int64(100), - }, - previousOutcome: func(t *testing.T) *types.AggregationOutcome { - m, err := values.NewMap(map[string]any{}) - require.NoError(t, err) - pm := values.Proto(m) - bm, err := proto.Marshal(pm) - require.NoError(t, err) - return &types.AggregationOutcome{Metadata: bm} - }, - }, - { - name: "aggregate on bytes mode", - fields: []aggregators.AggregationField{ - { - InputKey: "FeedID", - OutputKey: "FeedID", - Method: "mode", - }, - }, - extraConfig: map[string]any{}, - observationsFactory: func() map[commontypes.OracleID][]values.Value { - mockValue1, err := values.WrapMap(map[string]any{ - "FeedID": idABytes[:], - }) - require.NoError(t, err) - mockValue2, err := values.WrapMap(map[string]any{ - "FeedID": idBBytes[:], - }) - require.NoError(t, err) - return map[commontypes.OracleID][]values.Value{1: {mockValue1}, 2: {mockValue1}, 3: {mockValue2}, 4: {mockValue1}} - }, - shouldReport: true, - expectedOutcome: map[string]any{ - "Reports": []any{ - map[string]any{ - "FeedID": idABytes[:], - }, - }, - }, - expectedState: map[string]any{ - "FeedID": idABytes[:], - }, - }, - { - name: "aggregate on string mode", - fields: []aggregators.AggregationField{ - { - InputKey: "BenchmarkPrice", - OutputKey: "Price", - Method: "mode", - }, - }, - extraConfig: map[string]any{}, - observationsFactory: func() map[commontypes.OracleID][]values.Value { - mockValue1, err := values.WrapMap(map[string]any{ - "BenchmarkPrice": "1", - }) - require.NoError(t, err) - mockValue2, err := values.WrapMap(map[string]any{ - "BenchmarkPrice": "2", - }) - require.NoError(t, err) - return map[commontypes.OracleID][]values.Value{1: {mockValue1}, 2: {mockValue1}, 3: {mockValue2}} - }, - shouldReport: true, - expectedOutcome: map[string]any{ - "Reports": []any{ - map[string]any{ - "Price": "1", - }, - }, - }, - expectedState: map[string]any{ - "Price": "1", - }, - }, - { - name: "aggregate on bool mode", - fields: []aggregators.AggregationField{ - { - InputKey: "BenchmarkPrice", - OutputKey: "Price", - Method: "mode", - }, - }, - extraConfig: map[string]any{}, - observationsFactory: func() map[commontypes.OracleID][]values.Value { - mockValue1, err := values.WrapMap(map[string]any{ - "BenchmarkPrice": true, - }) - require.NoError(t, err) - mockValue2, err := values.WrapMap(map[string]any{ - "BenchmarkPrice": false, - }) - require.NoError(t, err) - return map[commontypes.OracleID][]values.Value{1: {mockValue1}, 2: {mockValue1}, 3: {mockValue2}} - }, - shouldReport: true, - expectedOutcome: map[string]any{ - "Reports": []any{ - map[string]any{ - "Price": true, - }, - }, - }, - expectedState: map[string]any{ - "Price": true, - }, - }, - { - name: "aggregate on non-indexable type", - fields: []aggregators.AggregationField{ - { - // Omitting "InputKey" - OutputKey: "Price", - Method: "median", - }, - }, - extraConfig: map[string]any{}, - observationsFactory: func() map[commontypes.OracleID][]values.Value { - mockValue, err := values.Wrap(1) - require.NoError(t, err) - return map[commontypes.OracleID][]values.Value{1: {mockValue}, 2: {mockValue}, 3: {mockValue}} - }, - shouldReport: true, - expectedOutcome: map[string]any{ - "Reports": []any{ - map[string]any{ - "Price": int64(1), - }, - }, - }, - expectedState: map[string]any{"Price": int64(1)}, - }, - { - name: "aggregate on list type", - fields: []aggregators.AggregationField{ - { - InputKey: "1", - OutputKey: "Price", - Method: "median", - }, - }, - extraConfig: map[string]any{}, - observationsFactory: func() map[commontypes.OracleID][]values.Value { - mockValue, err := values.NewList([]any{"1", "2", "3"}) - require.NoError(t, err) - return map[commontypes.OracleID][]values.Value{1: {mockValue}, 2: {mockValue}, 3: {mockValue}} - }, - shouldReport: true, - expectedOutcome: map[string]any{ - "Reports": []any{ - map[string]any{ - "Price": "2", - }, - }, - }, - expectedState: map[string]any{ - "Price": "2", - }, - }, - { - name: "submap", - fields: []aggregators.AggregationField{ - { - InputKey: "FeedID", - OutputKey: "FeedID", - Method: "mode", - }, - { - InputKey: "BenchmarkPrice", - OutputKey: "Price", - Method: "median", - DeviationString: "10", - DeviationType: "percent", - SubMapField: true, - }, - { - InputKey: "Timestamp", - OutputKey: "Timestamp", - Method: "median", - DeviationString: "100", - DeviationType: "absolute", - }, - }, - extraConfig: map[string]any{ - "SubMapKey": "Report", - }, - observationsFactory: func() map[commontypes.OracleID][]values.Value { - mockValue, err := values.WrapMap(map[string]any{ - "FeedID": idABytes[:], - "BenchmarkPrice": int64(100), - "Timestamp": 12341414929, - }) - require.NoError(t, err) - return map[commontypes.OracleID][]values.Value{1: {mockValue}, 2: {mockValue}, 3: {mockValue}} - }, - shouldReport: true, - expectedOutcome: map[string]any{ - "Reports": []any{ - map[string]any{ - "FeedID": idABytes[:], - "Timestamp": int64(12341414929), - "Report": map[string]any{ - "Price": int64(100), - }, - }, - }, - }, - expectedState: map[string]any{ - "FeedID": idABytes[:], - "Price": int64(100), - "Timestamp": int64(12341414929), - }, - }, - { - name: "report format value", - fields: []aggregators.AggregationField{ - { - OutputKey: "Price", - Method: "median", - }, - }, - extraConfig: map[string]any{ - "reportFormat": "value", - }, - observationsFactory: func() map[commontypes.OracleID][]values.Value { - mockValue, err := values.Wrap(1) - require.NoError(t, err) - return map[commontypes.OracleID][]values.Value{1: {mockValue}, 2: {mockValue}, 3: {mockValue}} - }, - shouldReport: true, - expectedOutcome: map[string]any{ - "Reports": int64(1), - }, - expectedState: map[string]any{"Price": int64(1)}, - }, - { - name: "report format array", - fields: []aggregators.AggregationField{ - { - OutputKey: "Price", - Method: "median", - }, - }, - extraConfig: map[string]any{ - "reportFormat": "array", - }, - observationsFactory: func() map[commontypes.OracleID][]values.Value { - mockValue, err := values.Wrap(1) - require.NoError(t, err) - return map[commontypes.OracleID][]values.Value{1: {mockValue}, 2: {mockValue}, 3: {mockValue}} - }, - shouldReport: true, - expectedOutcome: map[string]any{ - "Reports": []any{map[string]any{"Price": int64(1)}}, - }, - 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, - }) - require.NoError(t, err) - 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) { - config := getConfigReduceAggregator(t, tt.fields, tt.extraConfig) - agg, err := aggregators.NewReduceAggregator(*config) - require.NoError(t, err) - - pb := &pb.Map{} - - var po *types.AggregationOutcome - if tt.previousOutcome != nil { - po = tt.previousOutcome(t) - } - - outcome, err := agg.Aggregate(logger.Nop(), po, tt.observationsFactory(), 1) - require.NoError(t, err) - require.Equal(t, tt.shouldReport, outcome.ShouldReport) - - // validate metadata - err = proto.Unmarshal(outcome.Metadata, pb) - require.NoError(t, err) - vmap, err := values.FromMapValueProto(pb) - require.NoError(t, err) - state, err := vmap.Unwrap() - require.NoError(t, err) - require.Equal(t, tt.expectedState, state) - - // validate encodable outcome - val, err := values.FromMapValueProto(outcome.EncodableOutcome) - require.NoError(t, err) - topLevelMap, err := val.Unwrap() - require.NoError(t, err) - mm, ok := topLevelMap.(map[string]any) - require.True(t, ok) - - require.NoError(t, err) - - require.Equal(t, tt.expectedOutcome, mm) - }) - } - }) - - t.Run("error path", func(t *testing.T) { - cases := []struct { - name string - previousOutcome *types.AggregationOutcome - fields []aggregators.AggregationField - extraConfig map[string]any - observationsFactory func() map[commontypes.OracleID][]values.Value - errString string - }{ - { - name: "not enough observations", - previousOutcome: nil, - fields: []aggregators.AggregationField{ - { - Method: "median", - OutputKey: "Price", - }, - }, - extraConfig: map[string]any{}, - observationsFactory: func() map[commontypes.OracleID][]values.Value { - return map[commontypes.OracleID][]values.Value{} - }, - errString: "not enough observations, have 0 want 3", - }, - { - name: "invalid previous outcome not pb", - previousOutcome: &types.AggregationOutcome{ - Metadata: []byte{1, 2, 3}, - }, - fields: []aggregators.AggregationField{ - { - Method: "median", - OutputKey: "Price", - }, - }, - extraConfig: map[string]any{}, - observationsFactory: func() map[commontypes.OracleID][]values.Value { - mockValue, err := values.Wrap(int64(100)) - require.NoError(t, err) - return map[commontypes.OracleID][]values.Value{1: {mockValue}, 2: {mockValue}, 3: {mockValue}} - }, - errString: "initializeCurrentState Unmarshal error:", // the proto package discourages full string error comparisons - }, - { - name: "not enough extracted values", - previousOutcome: nil, - fields: []aggregators.AggregationField{ - { - InputKey: "Price", - OutputKey: "Price", - Method: "median", - }, - }, - extraConfig: map[string]any{}, - observationsFactory: func() map[commontypes.OracleID][]values.Value { - mockValue, err := values.WrapMap(map[string]any{"Price": int64(100)}) - require.NoError(t, err) - mockValueEmpty := values.EmptyMap() - return map[commontypes.OracleID][]values.Value{1: {mockValue}, 2: {mockValue}, 3: {mockValueEmpty}} - }, - errString: "not enough observations provided Price, have 2 want 3", - }, - { - name: "reduce error median", - previousOutcome: nil, - fields: []aggregators.AggregationField{ - { - Method: "median", - OutputKey: "Price", - }, - }, - extraConfig: map[string]any{}, - observationsFactory: func() map[commontypes.OracleID][]values.Value { - mockValue, err := values.Wrap(true) - require.NoError(t, err) - return map[commontypes.OracleID][]values.Value{1: {mockValue}, 2: {mockValue}, 3: {mockValue}} - }, - errString: "unable to reduce on method median, err: unable to convert type bool to decimal", - }, - { - name: "reduce error mode with mode quorum of: ocr", - previousOutcome: nil, - fields: []aggregators.AggregationField{ - { - Method: "mode", - ModeQuorum: "ocr", - OutputKey: "Price", - }, - }, - extraConfig: map[string]any{}, - observationsFactory: func() map[commontypes.OracleID][]values.Value { - mockValue, err := values.Wrap(1) - require.NoError(t, err) - mockValue2, err := values.Wrap(2) - require.NoError(t, err) - mockValue3, err := values.Wrap(3) - require.NoError(t, err) - return map[commontypes.OracleID][]values.Value{1: {mockValue}, 2: {mockValue2}, 3: {mockValue3}} - }, - errString: "unable to reduce on method mode, err: mode quorum not reached. have: 1, want: 2", - }, - { - name: "reduce error mode with mode quorum of: all", - previousOutcome: nil, - fields: []aggregators.AggregationField{ - { - Method: "mode", - ModeQuorum: "all", - OutputKey: "Price", - }, - }, - extraConfig: map[string]any{}, - observationsFactory: func() map[commontypes.OracleID][]values.Value { - mockValue, err := values.Wrap(1) - require.NoError(t, err) - mockValue2, err := values.Wrap(2) - require.NoError(t, err) - return map[commontypes.OracleID][]values.Value{1: {mockValue}, 2: {mockValue2}, 3: {mockValue2}} - }, - errString: "unable to reduce on method mode, err: mode quorum not reached. have: 2, want: 3", - }, - } - for _, tt := range cases { - t.Run(tt.name, func(t *testing.T) { - config := getConfigReduceAggregator(t, tt.fields, tt.extraConfig) - agg, err := aggregators.NewReduceAggregator(*config) - require.NoError(t, err) - - _, err = agg.Aggregate(logger.Nop(), tt.previousOutcome, tt.observationsFactory(), 1) - require.ErrorContains(t, err, tt.errString) - }) - } - }) -} - -func TestInputChanges(t *testing.T) { - 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", - }, - } - config := getConfigReduceAggregator(t, fields, map[string]any{}) - agg, err := aggregators.NewReduceAggregator(*config) - require.NoError(t, err) - - // First Round - mockValue1, err := values.WrapMap(map[string]any{ - "FeedID": idABytes[:], - "BenchmarkPrice": int64(100), - "Timestamp": 12341414929, - }) - require.NoError(t, err) - pb := &pb.Map{} - outcome, err := agg.Aggregate(logger.Nop(), nil, map[commontypes.OracleID][]values.Value{1: {mockValue1}, 2: {mockValue1}, 3: {mockValue1}}, 1) - require.NoError(t, err) - shouldReport := true - require.Equal(t, shouldReport, outcome.ShouldReport) - - // validate metadata - proto.Unmarshal(outcome.Metadata, pb) - vmap, err := values.FromMapValueProto(pb) - require.NoError(t, err) - state, err := vmap.Unwrap() - require.NoError(t, err) - expectedState1 := map[string]any{ - "FeedID": idABytes[:], - "Price": int64(100), - "Timestamp": int64(12341414929), - } - require.Equal(t, expectedState1, state) - - // validate encodable outcome - val, err := values.FromMapValueProto(outcome.EncodableOutcome) - require.NoError(t, err) - topLevelMap, err := val.Unwrap() - require.NoError(t, err) - mm, ok := topLevelMap.(map[string]any) - require.True(t, ok) - - require.NoError(t, err) - expectedOutcome1 := map[string]any{ - "Reports": []any{ - map[string]any{ - "FeedID": idABytes[:], - "Timestamp": int64(12341414929), - "Price": int64(100), - }, - }, - } - require.Equal(t, expectedOutcome1, mm) - - // Second Round - mockValue2, err := values.WrapMap(map[string]any{ - "FeedID": true, - "Timestamp": int64(12341414929), - "BenchmarkPrice": int64(100), - }) - require.NoError(t, err) - outcome, err = agg.Aggregate(logger.Nop(), nil, map[commontypes.OracleID][]values.Value{1: {mockValue2}, 2: {mockValue2}, 3: {mockValue2}}, 1) - require.NoError(t, err) - require.Equal(t, shouldReport, outcome.ShouldReport) - - // validate metadata - proto.Unmarshal(outcome.Metadata, pb) - vmap, err = values.FromMapValueProto(pb) - require.NoError(t, err) - state, err = vmap.Unwrap() - require.NoError(t, err) - expectedState2 := map[string]any{ - "FeedID": true, - "Price": int64(100), - "Timestamp": int64(12341414929), - } - require.Equal(t, expectedState2, state) - - // validate encodable outcome - val, err = values.FromMapValueProto(outcome.EncodableOutcome) - require.NoError(t, err) - topLevelMap, err = val.Unwrap() - require.NoError(t, err) - mm, ok = topLevelMap.(map[string]any) - require.True(t, ok) - - require.NoError(t, err) - expectedOutcome2 := map[string]any{ - "Reports": []any{ - map[string]any{ - "FeedID": true, - "Timestamp": int64(12341414929), - "Price": int64(100), - }, - }, - } - - require.Equal(t, expectedOutcome2, mm) -} - -func TestMedianAggregator_ParseConfig(t *testing.T) { - t.Run("happy path", func(t *testing.T) { - cases := []struct { - name string - inputFactory func() map[string]any - outputFactory func() aggregators.ReduceAggConfig - }{ - { - name: "no inputkey", - inputFactory: func() map[string]any { - return map[string]any{ - "fields": []aggregators.AggregationField{ - { - Method: "median", - OutputKey: "Price", - }, - }, - } - }, - outputFactory: func() aggregators.ReduceAggConfig { - return aggregators.ReduceAggConfig{ - Fields: []aggregators.AggregationField{ - { - InputKey: "", - OutputKey: "Price", - Method: "median", - DeviationString: "", - Deviation: decimal.Decimal{}, - DeviationType: "none", - }, - }, - OutputFieldName: "Reports", - ReportFormat: "map", - } - }, - }, - { - name: "reportFormat map, aggregation method mode, deviation", - inputFactory: func() map[string]any { - return map[string]any{ - "fields": []aggregators.AggregationField{ - { - InputKey: "FeedID", - OutputKey: "FeedId", - Method: "mode", - DeviationString: "1.1", - DeviationType: "absolute", - }, - }, - } - }, - outputFactory: func() aggregators.ReduceAggConfig { - return aggregators.ReduceAggConfig{ - Fields: []aggregators.AggregationField{ - { - InputKey: "FeedID", - OutputKey: "FeedId", - Method: "mode", - ModeQuorum: "ocr", - DeviationString: "1.1", - Deviation: decimal.NewFromFloat(1.1), - DeviationType: "absolute", - }, - }, - OutputFieldName: "Reports", - ReportFormat: "map", - } - }, - }, - { - name: "reportFormat array, aggregation method median, no deviation", - inputFactory: func() map[string]any { - return map[string]any{ - "fields": []aggregators.AggregationField{ - { - InputKey: "FeedID", - OutputKey: "FeedId", - Method: "median", - }, - }, - "outputFieldName": "Reports", - "reportFormat": "array", - } - }, - outputFactory: func() aggregators.ReduceAggConfig { - return aggregators.ReduceAggConfig{ - Fields: []aggregators.AggregationField{ - { - InputKey: "FeedID", - OutputKey: "FeedId", - Method: "median", - DeviationString: "", - Deviation: decimal.Decimal{}, - DeviationType: "none", - }, - }, - OutputFieldName: "Reports", - ReportFormat: "array", - } - }, - }, - } - - for _, tt := range cases { - t.Run(tt.name, func(t *testing.T) { - vMap, err := values.NewMap(tt.inputFactory()) - require.NoError(t, err) - parsedConfig, err := aggregators.ParseConfigReduceAggregator(*vMap) - require.NoError(t, err) - require.Equal(t, tt.outputFactory(), parsedConfig) - }) - } - }) - - t.Run("unhappy path", func(t *testing.T) { - cases := []struct { - name string - configFactory func() *values.Map - }{ - { - name: "empty", - configFactory: func() *values.Map { - return values.EmptyMap() - }, - }, - { - name: "invalid report format", - configFactory: func() *values.Map { - vMap, err := values.NewMap(map[string]any{ - "fields": []aggregators.AggregationField{ - { - InputKey: "FeedID", - OutputKey: "FeedID", - Method: "median", - }, - }, - "reportFormat": "invalid", - }) - require.NoError(t, err) - return vMap - }, - }, - { - name: "field with no method", - configFactory: func() *values.Map { - vMap, err := values.NewMap(map[string]any{ - "fields": []aggregators.AggregationField{ - { - InputKey: "FeedID", - OutputKey: "FeedID", - }, - }, - }) - require.NoError(t, err) - return vMap - }, - }, - { - name: "field with empty method", - configFactory: func() *values.Map { - vMap, err := values.NewMap(map[string]any{ - "fields": []aggregators.AggregationField{ - { - InputKey: "FeedID", - OutputKey: "FeedID", - Method: "", - }, - }, - }) - require.NoError(t, err) - return vMap - }, - }, - { - name: "field with invalid method", - configFactory: func() *values.Map { - vMap, err := values.NewMap(map[string]any{ - "fields": []aggregators.AggregationField{ - { - InputKey: "FeedID", - OutputKey: "FeedID", - Method: "invalid", - }, - }, - }) - require.NoError(t, err) - return vMap - }, - }, - { - name: "field with deviation string but no deviation type", - configFactory: func() *values.Map { - vMap, err := values.NewMap(map[string]any{ - "fields": []aggregators.AggregationField{ - { - InputKey: "FeedID", - OutputKey: "FeedID", - Method: "median", - DeviationString: "1", - }, - }, - }) - require.NoError(t, err) - return vMap - }, - }, - { - name: "field with deviation string but empty deviation type", - configFactory: func() *values.Map { - vMap, err := values.NewMap(map[string]any{ - "fields": []aggregators.AggregationField{ - { - InputKey: "FeedID", - OutputKey: "FeedID", - Method: "median", - DeviationString: "1", - DeviationType: "", - }, - }, - }) - require.NoError(t, err) - return vMap - }, - }, - { - name: "field with invalid deviation type", - configFactory: func() *values.Map { - vMap, err := values.NewMap(map[string]any{ - "fields": []aggregators.AggregationField{ - { - InputKey: "FeedID", - OutputKey: "FeedID", - Method: "median", - DeviationString: "1", - DeviationType: "invalid", - }, - }, - }) - require.NoError(t, err) - return vMap - }, - }, - { - name: "field with deviation type but no deviation string", - configFactory: func() *values.Map { - vMap, err := values.NewMap(map[string]any{ - "fields": []aggregators.AggregationField{ - { - InputKey: "FeedID", - OutputKey: "FeedID", - Method: "median", - DeviationType: "absolute", - }, - }, - }) - require.NoError(t, err) - return vMap - }, - }, - { - name: "field with deviation type but empty deviation string", - configFactory: func() *values.Map { - vMap, err := values.NewMap(map[string]any{ - "fields": []aggregators.AggregationField{ - { - InputKey: "FeedID", - OutputKey: "FeedID", - Method: "median", - DeviationType: "absolute", - DeviationString: "", - }, - }, - }) - require.NoError(t, err) - return vMap - }, - }, - { - name: "field with invalid deviation string", - configFactory: func() *values.Map { - vMap, err := values.NewMap(map[string]any{ - "fields": []aggregators.AggregationField{ - { - InputKey: "FeedID", - OutputKey: "FeedID", - Method: "median", - DeviationType: "absolute", - DeviationString: "1-1", - }, - }, - }) - require.NoError(t, err) - return vMap - }, - }, - { - name: "field with sub report, but no sub report key", - configFactory: func() *values.Map { - vMap, err := values.NewMap(map[string]any{ - "fields": []aggregators.AggregationField{ - { - InputKey: "FeedID", - OutputKey: "FeedID", - Method: "median", - SubMapField: true, - }, - }, - }) - require.NoError(t, err) - return vMap - }, - }, - { - name: "sub report key, but no sub report fields", - configFactory: func() *values.Map { - vMap, err := values.NewMap(map[string]any{ - "subMapKey": "Report", - "fields": []aggregators.AggregationField{ - { - InputKey: "FeedID", - OutputKey: "FeedID", - Method: "median", - }, - }, - }) - require.NoError(t, err) - return vMap - }, - }, - { - name: "clashing output keys", - configFactory: func() *values.Map { - vMap, err := values.NewMap(map[string]any{ - "fields": []aggregators.AggregationField{ - { - InputKey: "FeedID", - OutputKey: "FeedID", - Method: "median", - }, - { - InputKey: "FeedID", - OutputKey: "FeedID", - Method: "median", - }, - }, - }) - require.NoError(t, err) - return vMap - }, - }, - { - name: "map/array type, no output key", - configFactory: func() *values.Map { - vMap, err := values.NewMap(map[string]any{ - "fields": []aggregators.AggregationField{ - { - InputKey: "FeedID", - Method: "median", - }, - }, - }) - require.NoError(t, err) - return vMap - }, - }, - { - name: "report type value with multiple fields", - configFactory: func() *values.Map { - vMap, err := values.NewMap(map[string]any{ - "reportFormat": "value", - "fields": []aggregators.AggregationField{ - { - InputKey: "FeedID", - Method: "median", - OutputKey: "FeedID", - }, - { - InputKey: "Price", - Method: "median", - OutputKey: "Price", - }, - }, - }) - require.NoError(t, err) - return vMap - }, - }, - { - name: "invalid mode quorum", - configFactory: func() *values.Map { - vMap, err := values.NewMap(map[string]any{ - "fields": []aggregators.AggregationField{ - { - InputKey: "Price", - Method: "mode", - ModeQuorum: "invalid", - OutputKey: "Price", - }, - }, - }) - require.NoError(t, err) - return vMap - }, - }, - } - - for _, tt := range cases { - t.Run(tt.name, func(t *testing.T) { - _, err := aggregators.ParseConfigReduceAggregator(*tt.configFactory()) - require.Error(t, err) - }, - ) - } - }) -} - -func TestAggregateShouldReport(t *testing.T) { - extraConfig := map[string]any{ - "reportFormat": "array", - } - - cases := []struct { - name string - fields []aggregators.AggregationField - mockValueFirstRound *values.Map - shouldReportFirstRound bool - stateFirstRound map[string]any - mockValueSecondRound *values.Map - shouldReportSecondRound bool - stateSecondRound map[string]any - mockValueThirdRound *values.Map - shouldReportThirdRound bool - stateThirdRound map[string]any - }{ - { - name: "OK-report_only_when_deviation_exceeded", - fields: []aggregators.AggregationField{ - { - InputKey: "Timestamp", - OutputKey: "Time", - Method: "median", - DeviationString: "30", - DeviationType: "absolute", - }, - }, - mockValueFirstRound: func() *values.Map { - mockValue, err := values.WrapMap(map[string]any{ - "Timestamp": decimal.NewFromInt(10), - }) - require.NoError(t, err) - return mockValue - }(), - shouldReportFirstRound: true, - stateFirstRound: map[string]any{"Time": decimal.NewFromInt(10)}, - - mockValueSecondRound: func() *values.Map { - mockValue, err := values.WrapMap(map[string]any{ - "Timestamp": decimal.NewFromInt(30), - }) - require.NoError(t, err) - return mockValue - }(), - shouldReportSecondRound: false, - // the delta between 10 and 30 is 20, which is less than the deviation of 30, so the state should remain the same - stateSecondRound: map[string]any(map[string]any{"Time": decimal.NewFromInt(10)}), - - mockValueThirdRound: func() *values.Map { - mockValue, err := values.WrapMap(map[string]any{ - "Timestamp": decimal.NewFromInt(45), - }) - require.NoError(t, err) - return mockValue - }(), - shouldReportThirdRound: true, - // the delta between 10 and 45 is 35, which is more than the deviation of 30, thats why the state is updated - stateThirdRound: map[string]any{"Time": decimal.NewFromInt(45)}, - }, - { - name: "NOK-do_not_report_if_deviation_type_any_byte_field_does_not_change", - fields: []aggregators.AggregationField{ - { - InputKey: "FeedID", - OutputKey: "FeedID", - Method: "mode", - ModeQuorum: "any", - DeviationType: "any", - }, - { - InputKey: "Timestamp", - OutputKey: "Time", - Method: "median", - DeviationString: "30", - DeviationType: "absolute", - }, - }, - mockValueFirstRound: func() *values.Map { - mockValue, err := values.WrapMap(map[string]any{ - "FeedID": idABytes[:], - "Timestamp": decimal.NewFromInt(10), - }) - require.NoError(t, err) - return mockValue - }(), - shouldReportFirstRound: true, - stateFirstRound: map[string]any(map[string]any{ - "FeedID": idABytes[:], - "Time": decimal.NewFromInt(10), - }), - - mockValueSecondRound: func() *values.Map { - mockValue, err := values.WrapMap(map[string]any{ - "FeedID": idABytes[:], - "Timestamp": decimal.NewFromInt(20), - }) - require.NoError(t, err) - return mockValue - }(), - shouldReportSecondRound: false, - stateSecondRound: map[string]any(map[string]any{ - "FeedID": idABytes[:], - "Time": decimal.NewFromInt(10), - }), - }, - { - name: "NOK-do_not_report_if_deviation_type_any_bool_field_does_not_change", - fields: []aggregators.AggregationField{ - { - InputKey: "BoolField", - OutputKey: "BoolField", - Method: "mode", - ModeQuorum: "any", - DeviationType: "any", - }, - { - InputKey: "Timestamp", - OutputKey: "Time", - Method: "median", - DeviationString: "30", - DeviationType: "absolute", - }, - }, - mockValueFirstRound: func() *values.Map { - mockValue, err := values.WrapMap(map[string]any{ - "BoolField": true, - "Timestamp": decimal.NewFromInt(10), - }) - require.NoError(t, err) - return mockValue - }(), - shouldReportFirstRound: true, - stateFirstRound: map[string]any{ - "BoolField": true, - "Time": decimal.NewFromInt(10), - }, - - mockValueSecondRound: func() *values.Map { - mockValue, err := values.WrapMap(map[string]any{ - "BoolField": true, - "Timestamp": decimal.NewFromInt(20), - }) - require.NoError(t, err) - return mockValue - }(), - shouldReportSecondRound: false, - stateSecondRound: map[string]any(map[string]any{ - "BoolField": true, - "Time": decimal.NewFromInt(10), - }), - }, - { - name: "OK-report_if_deviation_type_any_byte_field_is_changed", - fields: []aggregators.AggregationField{ - { - InputKey: "FeedID", - OutputKey: "FeedID", - Method: "mode", - ModeQuorum: "any", - DeviationType: "any", - }, - { - InputKey: "Timestamp", - OutputKey: "Time", - Method: "median", - DeviationString: "30", - DeviationType: "absolute", - }, - }, - mockValueFirstRound: func() *values.Map { - mockValue, err := values.WrapMap(map[string]any{ - "FeedID": idABytes[:], - "Timestamp": decimal.NewFromInt(10), - }) - require.NoError(t, err) - return mockValue - }(), - shouldReportFirstRound: true, - stateFirstRound: map[string]any{ - "FeedID": idABytes[:], - "Time": decimal.NewFromInt(10), - }, - - mockValueSecondRound: func() *values.Map { - mockValue, err := values.WrapMap(map[string]any{ - "FeedID": idBBytes[:], - "Timestamp": decimal.NewFromInt(20), - }) - require.NoError(t, err) - return mockValue - }(), - shouldReportSecondRound: true, - stateSecondRound: map[string]any(map[string]any{ - "FeedID": idBBytes[:], - "Time": decimal.NewFromInt(10), - }), - }, - { - name: "OK-report_if_deviation_type_any_bool_field_is_changed", - fields: []aggregators.AggregationField{ - { - InputKey: "BoolField", - OutputKey: "BoolField", - Method: "mode", - ModeQuorum: "any", - DeviationType: "any", - }, - { - InputKey: "Timestamp", - OutputKey: "Time", - Method: "median", - DeviationString: "30", - DeviationType: "absolute", - }, - }, - mockValueFirstRound: func() *values.Map { - mockValue, err := values.WrapMap(map[string]any{ - "BoolField": true, - "Timestamp": decimal.NewFromInt(10), - }) - require.NoError(t, err) - return mockValue - }(), - shouldReportFirstRound: true, - stateFirstRound: map[string]any{ - "BoolField": true, - "Time": decimal.NewFromInt(10), - }, - - mockValueSecondRound: func() *values.Map { - mockValue, err := values.WrapMap(map[string]any{ - "BoolField": false, - "Timestamp": decimal.NewFromInt(20), - }) - require.NoError(t, err) - return mockValue - }(), - shouldReportSecondRound: true, - stateSecondRound: map[string]any(map[string]any{ - "BoolField": false, - "Time": decimal.NewFromInt(10), - }), - }, - { - name: "OK-report_if_deviation_type_any_string_field_is_changed", - fields: []aggregators.AggregationField{ - { - InputKey: "FeedID", - OutputKey: "FeedID", - Method: "mode", - ModeQuorum: "any", - DeviationType: "any", - }, - { - InputKey: "Timestamp", - OutputKey: "Time", - Method: "median", - DeviationString: "30", - DeviationType: "absolute", - }, - }, - mockValueFirstRound: func() *values.Map { - mockValue, err := values.WrapMap(map[string]any{ - "FeedID": "A", - "Timestamp": decimal.NewFromInt(10), - }) - require.NoError(t, err) - return mockValue - }(), - shouldReportFirstRound: true, - stateFirstRound: map[string]any{ - "FeedID": "A", - "Time": decimal.NewFromInt(10), - }, - - mockValueSecondRound: func() *values.Map { - mockValue, err := values.WrapMap(map[string]any{ - "FeedID": "B", - "Timestamp": decimal.NewFromInt(20), - }) - require.NoError(t, err) - return mockValue - }(), - shouldReportSecondRound: true, - stateSecondRound: map[string]any(map[string]any{ - "FeedID": "B", - "Time": decimal.NewFromInt(10), - }), - }, - { - name: "NOK-do_not_report_if_deviation_type_any_string_field_does_not_change", - fields: []aggregators.AggregationField{ - { - InputKey: "FeedID", - OutputKey: "FeedID", - Method: "mode", - ModeQuorum: "any", - DeviationType: "any", - }, - { - InputKey: "Timestamp", - OutputKey: "Time", - Method: "median", - DeviationString: "30", - DeviationType: "absolute", - }, - }, - mockValueFirstRound: func() *values.Map { - mockValue, err := values.WrapMap(map[string]any{ - "FeedID": "A", - "Timestamp": decimal.NewFromInt(10), - }) - require.NoError(t, err) - return mockValue - }(), - shouldReportFirstRound: true, - stateFirstRound: map[string]any{ - "FeedID": "A", - "Time": decimal.NewFromInt(10), - }, - - mockValueSecondRound: func() *values.Map { - mockValue, err := values.WrapMap(map[string]any{ - "FeedID": "A", - "Timestamp": decimal.NewFromInt(20), - }) - require.NoError(t, err) - return mockValue - }(), - shouldReportSecondRound: false, - stateSecondRound: map[string]any(map[string]any{ - "FeedID": "A", - "Time": decimal.NewFromInt(10), - }), - }, - { - name: "OK-report_if_deviation_type_any_map_field_is_changed", - fields: []aggregators.AggregationField{ - { - InputKey: "FeedID", - OutputKey: "FeedID", - Method: "mode", - ModeQuorum: "any", - DeviationType: "any", - }, - { - InputKey: "Timestamp", - OutputKey: "Time", - Method: "median", - DeviationString: "30", - DeviationType: "absolute", - }, - }, - mockValueFirstRound: func() *values.Map { - mockValue, err := values.WrapMap(map[string]any{ - "FeedID": map[string]any{"A": "A"}, - "Timestamp": decimal.NewFromInt(10), - }) - require.NoError(t, err) - return mockValue - }(), - shouldReportFirstRound: true, - stateFirstRound: map[string]any{ - "FeedID": map[string]any{"A": "A"}, - "Time": decimal.NewFromInt(10), - }, - - mockValueSecondRound: func() *values.Map { - mockValue, err := values.WrapMap(map[string]any{ - "FeedID": map[string]any{"A": "B"}, - "Timestamp": decimal.NewFromInt(20), - }) - require.NoError(t, err) - return mockValue - }(), - shouldReportSecondRound: true, - stateSecondRound: map[string]any(map[string]any{ - "FeedID": map[string]any{"A": "B"}, - "Time": decimal.NewFromInt(10), - }), - }, - { - name: "NOK-do_not_report_if_deviation_type_any_map_field_does_not_change", - fields: []aggregators.AggregationField{ - { - InputKey: "FeedID", - OutputKey: "FeedID", - Method: "mode", - ModeQuorum: "any", - DeviationType: "any", - }, - { - InputKey: "Timestamp", - OutputKey: "Time", - Method: "median", - DeviationString: "30", - DeviationType: "absolute", - }, - }, - mockValueFirstRound: func() *values.Map { - mockValue, err := values.WrapMap(map[string]any{ - "FeedID": map[string]any{"A": "A"}, - "Timestamp": decimal.NewFromInt(10), - }) - require.NoError(t, err) - return mockValue - }(), - shouldReportFirstRound: true, - stateFirstRound: map[string]any{ - "FeedID": map[string]any{"A": "A"}, - "Time": decimal.NewFromInt(10), - }, - - mockValueSecondRound: func() *values.Map { - mockValue, err := values.WrapMap(map[string]any{ - "FeedID": map[string]any{"A": "A"}, - "Timestamp": decimal.NewFromInt(20), - }) - require.NoError(t, err) - return mockValue - }(), - shouldReportSecondRound: false, - stateSecondRound: map[string]any(map[string]any{ - "FeedID": map[string]any{"A": "A"}, - "Time": decimal.NewFromInt(10), - }), - }, - { - name: "OK-report_if_deviation_type_any_slice_field_is_changed", - fields: []aggregators.AggregationField{ - { - InputKey: "FeedID", - OutputKey: "FeedID", - Method: "mode", - ModeQuorum: "any", - DeviationType: "any", - }, - { - InputKey: "Timestamp", - OutputKey: "Time", - Method: "median", - DeviationString: "30", - DeviationType: "absolute", - }, - }, - mockValueFirstRound: func() *values.Map { - mockValue, err := values.WrapMap(map[string]any{ - "FeedID": []any{"A"}, - "Timestamp": decimal.NewFromInt(10), - }) - require.NoError(t, err) - return mockValue - }(), - shouldReportFirstRound: true, - stateFirstRound: map[string]any{ - "FeedID": []any{"A"}, - "Time": decimal.NewFromInt(10), - }, - - mockValueSecondRound: func() *values.Map { - mockValue, err := values.WrapMap(map[string]any{ - "FeedID": []any{"B"}, - "Timestamp": decimal.NewFromInt(20), - }) - require.NoError(t, err) - return mockValue - }(), - shouldReportSecondRound: true, - stateSecondRound: map[string]any(map[string]any{ - "FeedID": []any{"B"}, - "Time": decimal.NewFromInt(10), - }), - }, - { - name: "NOK-do_not_report_if_deviation_type_any_slice_field_does_not_change", - fields: []aggregators.AggregationField{ - { - InputKey: "FeedID", - OutputKey: "FeedID", - Method: "mode", - ModeQuorum: "any", - DeviationType: "any", - }, - { - InputKey: "Timestamp", - OutputKey: "Time", - Method: "median", - DeviationString: "30", - DeviationType: "absolute", - }, - }, - mockValueFirstRound: func() *values.Map { - mockValue, err := values.WrapMap(map[string]any{ - "FeedID": []any{"A"}, - "Timestamp": decimal.NewFromInt(10), - }) - require.NoError(t, err) - return mockValue - }(), - shouldReportFirstRound: true, - stateFirstRound: map[string]any{ - "FeedID": []any{"A"}, - "Time": decimal.NewFromInt(10), - }, - - mockValueSecondRound: func() *values.Map { - mockValue, err := values.WrapMap(map[string]any{ - "FeedID": []any{"A"}, - "Timestamp": decimal.NewFromInt(20), - }) - require.NoError(t, err) - return mockValue - }(), - shouldReportSecondRound: false, - stateSecondRound: map[string]any(map[string]any{ - "FeedID": []any{"A"}, - "Time": decimal.NewFromInt(10), - }), - }, - { - name: "OK-report_if_deviation_type_any_numeric_field_is_changed", - fields: []aggregators.AggregationField{ - { - InputKey: "FeedID", - OutputKey: "FeedID", - Method: "mode", - ModeQuorum: "any", - DeviationType: "any", - }, - { - InputKey: "Timestamp", - OutputKey: "Time", - Method: "median", - DeviationString: "30", - DeviationType: "absolute", - }, - }, - mockValueFirstRound: func() *values.Map { - mockValue, err := values.WrapMap(map[string]any{ - "FeedID": int64(1), - "Timestamp": decimal.NewFromInt(10), - }) - require.NoError(t, err) - return mockValue - }(), - shouldReportFirstRound: true, - stateFirstRound: map[string]any{ - "FeedID": int64(1), - "Time": decimal.NewFromInt(10), - }, - - mockValueSecondRound: func() *values.Map { - mockValue, err := values.WrapMap(map[string]any{ - "FeedID": int64(2), - "Timestamp": decimal.NewFromInt(20), - }) - require.NoError(t, err) - return mockValue - }(), - shouldReportSecondRound: true, - stateSecondRound: map[string]any(map[string]any{ - "FeedID": int64(2), - "Time": decimal.NewFromInt(10), - }), - }, - { - name: "OK-report_if_deviation_type_any_numeric_field_is_changed", - fields: []aggregators.AggregationField{ - { - InputKey: "FeedID", - OutputKey: "FeedID", - Method: "mode", - ModeQuorum: "any", - DeviationType: "any", - }, - { - InputKey: "Timestamp", - OutputKey: "Time", - Method: "median", - DeviationString: "30", - DeviationType: "absolute", - }, - }, - mockValueFirstRound: func() *values.Map { - mockValue, err := values.WrapMap(map[string]any{ - "FeedID": int64(1), - "Timestamp": decimal.NewFromInt(10), - }) - require.NoError(t, err) - return mockValue - }(), - shouldReportFirstRound: true, - stateFirstRound: map[string]any{ - "FeedID": int64(1), - "Time": decimal.NewFromInt(10), - }, - - mockValueSecondRound: func() *values.Map { - mockValue, err := values.WrapMap(map[string]any{ - "FeedID": int64(1), - "Timestamp": decimal.NewFromInt(20), - }) - require.NoError(t, err) - return mockValue - }(), - shouldReportSecondRound: false, - stateSecondRound: map[string]any(map[string]any{ - "FeedID": int64(1), - "Time": decimal.NewFromInt(10), - }), - }, - } - - for _, tc := range cases { - config := getConfigReduceAggregator(t, tc.fields, extraConfig) - agg, err := aggregators.NewReduceAggregator(*config) - require.NoError(t, err) - - pb := &pb.Map{} - - // 1st round - firstOutcome, err := agg.Aggregate(logger.Nop(), nil, map[commontypes.OracleID][]values.Value{1: {tc.mockValueFirstRound}, 2: {tc.mockValueFirstRound}, 3: {tc.mockValueFirstRound}}, 1) - require.NoError(t, err) - require.Equal(t, tc.shouldReportFirstRound, firstOutcome.ShouldReport) - - // validate metadata - proto.Unmarshal(firstOutcome.Metadata, pb) - vmap, err := values.FromMapValueProto(pb) - require.NoError(t, err) - state, err := vmap.Unwrap() - require.NoError(t, err) - require.Equal(t, map[string]any(tc.stateFirstRound), state) - - // 2nd round - secondOutcome, err := agg.Aggregate(logger.Nop(), firstOutcome, map[commontypes.OracleID][]values.Value{1: {tc.mockValueSecondRound}, 2: {tc.mockValueSecondRound}, 3: {tc.mockValueSecondRound}}, 1) - require.NoError(t, err) - require.Equal(t, tc.shouldReportSecondRound, secondOutcome.ShouldReport) - - // validate metadata - proto.Unmarshal(secondOutcome.Metadata, pb) - vmap, err = values.FromMapValueProto(pb) - require.NoError(t, err) - state, err = vmap.Unwrap() - require.NoError(t, err) - require.Equal(t, tc.stateSecondRound, state) - - // skip if there is no third round - if tc.mockValueThirdRound == nil { - continue - } - - // 3rd round - thirdOutcome, err := agg.Aggregate(logger.Nop(), secondOutcome, map[commontypes.OracleID][]values.Value{1: {tc.mockValueThirdRound}, 2: {tc.mockValueThirdRound}, 3: {tc.mockValueThirdRound}}, 1) - require.NoError(t, err) - require.True(t, thirdOutcome.ShouldReport) - - // validate metadata - proto.Unmarshal(thirdOutcome.Metadata, pb) - vmap, err = values.FromMapValueProto(pb) - require.NoError(t, err) - state, err = vmap.Unwrap() - require.NoError(t, err) - require.Equal(t, tc.stateThirdRound, state) - } -} - -func getConfigReduceAggregator(t *testing.T, fields []aggregators.AggregationField, override map[string]any) *values.Map { - unwrappedConfig := map[string]any{ - "fields": fields, - "outputFieldName": "Reports", - "reportFormat": "array", - } - maps.Copy(unwrappedConfig, override) - config, err := values.NewMap(unwrappedConfig) - require.NoError(t, err) - return config -} diff --git a/pkg/capabilities/consensus/ocr3/benchmark_test.go b/pkg/capabilities/consensus/ocr3/benchmark_test.go deleted file mode 100644 index 767b9c0498..0000000000 --- a/pkg/capabilities/consensus/ocr3/benchmark_test.go +++ /dev/null @@ -1,494 +0,0 @@ -package ocr3_test - -import ( - "context" - "fmt" - "runtime" - "strconv" - "testing" - "time" - - "github.com/shopspring/decimal" - "github.com/stretchr/testify/require" - "go.uber.org/zap/zapcore" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/timestamppb" - - ocrcommon "github.com/smartcontractkit/libocr/commontypes" - "github.com/smartcontractkit/libocr/offchainreporting2/types" - "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" - - "github.com/smartcontractkit/chainlink-protos/cre/go/values" - - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/datafeeds" - pbtypes "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/capabilities/datastreams" - "github.com/smartcontractkit/chainlink-common/pkg/logger" -) - -// mockCapability implements CapabilityIface for testing -type mockCapability struct { - aggregators map[string]pbtypes.Aggregator -} - -func (m *mockCapability) GetAggregator(workflowID string) (pbtypes.Aggregator, error) { - return m.aggregators[workflowID], nil -} - -func (m *mockCapability) GetEncoderByWorkflowID(workflowID string) (pbtypes.Encoder, error) { - return nil, nil // Not used in benchmark -} - -func (m *mockCapability) GetEncoderByName(encoderName string, config *values.Map) (pbtypes.Encoder, error) { - return nil, nil // Not used in benchmark -} - -func (m *mockCapability) GetRegisteredWorkflowsIDs() []string { - ids := make([]string, 0, len(m.aggregators)) - for id := range m.aggregators { - ids = append(ids, id) - } - return ids -} - -func (m *mockCapability) UnregisterWorkflowID(workflowID string) { - delete(m.aggregators, workflowID) -} - -func BenchmarkReportingPlugin_Outcome_LLOAggregator(b *testing.B) { - // Define test matrix parameters - workflowCounts := []int{1, 2, 4, 8, 16, 32, 64, 128} - streamCounts := []int{32, 64, 128, 256, 512, 1024} - - // Create one logger for all benchmarks to reduce setup overhead - c := logger.Config{ - Level: zapcore.InfoLevel, // Set to InfoLevel for benchmarks to reduce log noise - } - lggr, err := c.New() - require.NoError(b, err, "failed to create logger for benchmark") - - // Run benchmarks for each combination - for _, numWorkflows := range workflowCounts { - for _, numStreamsPerWorkflow := range streamCounts { - benchName := fmt.Sprintf("workflows=%d/streams=%d", numWorkflows, numStreamsPerWorkflow) - b.Run(benchName, func(b *testing.B) { - runBenchmarkWithParams(b, lggr, numWorkflows, numStreamsPerWorkflow) - }) - } - } -} - -func BenchmarkReportingPlugin_Observation_LLOAggregator(b *testing.B) { - // Define test matrix parameters - workflowCounts := []int{1, 2, 4, 8, 16, 32, 64, 128} - streamCounts := []int{32, 64, 128, 256, 512, 1024} - - // Create one logger for all benchmarks to reduce setup overhead - c := logger.Config{ - Level: zapcore.InfoLevel, // Set to InfoLevel for benchmarks to reduce log noise - } - lggr, err := c.New() - require.NoError(b, err, "failed to create logger for benchmark") - - // Run benchmarks for each combination - for _, numWorkflows := range workflowCounts { - for _, numStreamsPerWorkflow := range streamCounts { - benchName := fmt.Sprintf("workflows=%d/streams=%d", numWorkflows, numStreamsPerWorkflow) - b.Run(benchName, func(b *testing.B) { - runObservationBenchmarkWithParams(b, lggr, numWorkflows, numStreamsPerWorkflow) - }) - } - } -} - -// runObservationBenchmarkWithParams runs a benchmark with the specified parameters -func runObservationBenchmarkWithParams(b *testing.B, lggr logger.Logger, numWorkflows, numStreamsPerWorkflow int) { - const ( - numOracles = 4 // Total nodes - f = 1 // Fault tolerance - ) - - // Create request store with requests for each workflow - store := requests.NewStore[*ocr3.ReportRequest]() - - // Create capability with LLO aggregators for each workflow - mockCap := &mockCapability{ - aggregators: make(map[string]pbtypes.Aggregator, numWorkflows), - } - - // Create LLO aggregators for each workflow and populate the store - for i := range numWorkflows { - workflowID := fmt.Sprintf("workflow-%d", i) - executionID := fmt.Sprintf("execution-%d", i) - - // Create aggregator - agg, err := createLLOAggregator(b, numStreamsPerWorkflow) - require.NoError(b, err) - mockCap.aggregators[workflowID] = agg - - // Populate store with observation data - lloEvent := createLLOEvent(b, numStreamsPerWorkflow, time.Now()) - wrappedEvent, err := values.Wrap(lloEvent) - require.NoError(b, err) - - // Create list with the LLO event - listVal, err := values.NewList([]any{wrappedEvent}) - require.NoError(b, err) - - // Create and add request to store - req := &ocr3.ReportRequest{ - WorkflowID: workflowID, - WorkflowExecutionID: executionID, - WorkflowName: fmt.Sprintf("Workflow %d", i), - WorkflowOwner: "test-owner", - WorkflowDonID: 1, - WorkflowDonConfigVersion: 1, - ReportID: fmt.Sprintf("report-%d", i), - KeyID: "test-key", - Observations: listVal, - } - - require.NoError(b, store.Add(req)) - } - - // Create reporting plugin - plugin, err := ocr3.NewReportingPlugin( - store, - mockCap, - numWorkflows, // batchSize matches numWorkflows - ocr3types.ReportingPluginConfig{ - N: numOracles, - F: f, - }, - &pbtypes.ReportingPluginConfig{ - OutcomePruningThreshold: 100, - }, - lggr, - ) - require.NoError(b, err) - - // Create test query with workflow IDs - query, err := createTestQuery(numWorkflows) - require.NoError(b, err) - - // Create outcome context (not really used for Observation) - outctx := ocr3types.OutcomeContext{ - SeqNr: 1, - PreviousOutcome: nil, // Not needed for Observation benchmark - } - - // Reset timer and enable memory allocation reporting - - b.ReportAllocs() - - // Preallocate memory stats variables - var memStatsBefore, memStatsAfter runtime.MemStats - - // Track cumulative metrics - var totalMemUsage uint64 - var totalObservationSize int - - // Run the benchmark - for b.Loop() { - runtime.GC() // Run garbage collection before measurement to reduce noise - runtime.ReadMemStats(&memStatsBefore) - - // Call Observation function - observation, err := plugin.Observation(context.Background(), outctx, query) - require.NoError(b, err) - - // Measure memory usage - runtime.ReadMemStats(&memStatsAfter) - memUsage := memStatsAfter.TotalAlloc - memStatsBefore.TotalAlloc - totalMemUsage += memUsage - - // Measure observation size - observationSize := len(observation) - totalObservationSize += observationSize - - // Basic validation of observation - var parsedObservation pbtypes.Observations - err = proto.Unmarshal(observation, &parsedObservation) - require.NoError(b, err) - require.Len(b, parsedObservation.Observations, numWorkflows) - } - - // Report average metrics - if b.N > 0 { - b.ReportMetric(float64(totalMemUsage)/float64(b.N), "B/memory") - b.ReportMetric(float64(totalObservationSize)/float64(b.N), "B/observation_size") - // Report streams per second metric to understand throughput - streamsProcessed := numWorkflows * numStreamsPerWorkflow - b.ReportMetric(float64(streamsProcessed), "streams/op") - } -} - -// runBenchmarkWithParams runs a benchmark with the specified parameters -func runBenchmarkWithParams(b *testing.B, lggr logger.Logger, numWorkflows, numStreamsPerWorkflow int) { - // Test parameters - const ( - numOracles = 4 // Total nodes - f = 1 // Fault tolerance - ) - - // Create request store - store := requests.NewStore[*ocr3.ReportRequest]() - - // Create capability with LLO aggregators for each workflow - mockCap := &mockCapability{ - aggregators: make(map[string]pbtypes.Aggregator, numWorkflows), - } - - // Create LLO aggregators for each workflow - for i := range numWorkflows { - workflowID := fmt.Sprintf("workflow-%d", i) - agg, err := createLLOAggregator(b, numStreamsPerWorkflow) - require.NoError(b, err) - mockCap.aggregators[workflowID] = agg - } - - // Create reporting plugin - plugin, err := ocr3.NewReportingPlugin( - store, - mockCap, - numWorkflows, // batchSize - ocr3types.ReportingPluginConfig{ - N: numOracles, - F: f, - }, - &pbtypes.ReportingPluginConfig{ - OutcomePruningThreshold: 100, - }, - lggr, - ) - require.NoError(b, err) - - // Create test query with 10 workflow IDs - query, err := createTestQuery(numWorkflows) - require.NoError(b, err) - - // Create previous outcome with the same 10 workflow IDs - previousOutcome, err := createTestPreviousOutcome(numWorkflows, numStreamsPerWorkflow) - require.NoError(b, err) - - // Create attributed observations from all oracles - aos := createTestAttributedObservations(b, numOracles, numWorkflows, numStreamsPerWorkflow) - - // Create outcome context - outctx := ocr3types.OutcomeContext{ - SeqNr: 1, - PreviousOutcome: previousOutcome, - } - - // Reset timer and enable memory allocation reporting - - b.ReportAllocs() - - // Run the benchmark - for b.Loop() { - var memStatsBefore, memStatsAfter runtime.MemStats - runtime.ReadMemStats(&memStatsBefore) - - // Call Outcome function - outcome, err := plugin.Outcome(context.Background(), outctx, query, aos) - require.NoError(b, err) - - // Measure memory usage - runtime.ReadMemStats(&memStatsAfter) - memUsage := memStatsAfter.TotalAlloc - memStatsBefore.TotalAlloc - - // Measure outcome size - outcomeSize := len(outcome) - - // Report custom metrics - b.ReportMetric(float64(memUsage), "B/memory") - b.ReportMetric(float64(outcomeSize), "B/outcome_size") - - // Validate outcome contents - var parsedOutcome pbtypes.Outcome - err = proto.Unmarshal(outcome, &parsedOutcome) - require.NoError(b, err) - require.Len(b, parsedOutcome.Outcomes, numWorkflows) - } -} - -// Helper functions - -// createTestQuery generates a query with the specified number of workflow IDs -func createTestQuery(numWorkflows int) ([]byte, error) { - ids := make([]*pbtypes.Id, numWorkflows) - for i := range numWorkflows { - ids[i] = &pbtypes.Id{ - WorkflowExecutionId: fmt.Sprintf("execution-%d", i), - WorkflowId: fmt.Sprintf("workflow-%d", i), - WorkflowOwner: "test-owner", - WorkflowName: fmt.Sprintf("Workflow %d", i), - WorkflowDonId: 1, - WorkflowDonConfigVersion: 1, - ReportId: fmt.Sprintf("report-%d", i), - KeyId: "test-key", - } - } - - query := &pbtypes.Query{ - Ids: ids, - } - - return proto.MarshalOptions{Deterministic: true}.Marshal(query) -} - -// createTestPreviousOutcome generates a previous outcome with consistent LLOOutcomeMetadata -func createTestPreviousOutcome(numWorkflows, numStreamsPerWorkflow int) ([]byte, error) { - outcome := &pbtypes.Outcome{ - Outcomes: make(map[string]*pbtypes.AggregationOutcome, numWorkflows), - CurrentReports: []*pbtypes.Report{}, - } - - // Create an identical LLOOutcomeMetadata for all workflows - baseMetadata := &datafeeds.LLOOutcomeMetadata{ - StreamInfo: make(map[uint32]*datafeeds.LLOStreamInfo, numStreamsPerWorkflow), - } - - // Populate with stream info - baseTime := time.Now().Add(-10 * time.Minute).UnixNano() - zeroPrice, _ := decimal.Zero.MarshalBinary() - - for i := range numStreamsPerWorkflow { - streamID := uint32(i) - baseMetadata.StreamInfo[streamID] = &datafeeds.LLOStreamInfo{ - Timestamp: baseTime, - Price: zeroPrice, - } - } - - // Marshal once - metadataBytes, err := proto.Marshal(baseMetadata) - if err != nil { - return nil, err - } - - // Create outcome entries for each workflow, using the same metadata - for i := range numWorkflows { - workflowID := fmt.Sprintf("workflow-%d", i) - outcome.Outcomes[workflowID] = &pbtypes.AggregationOutcome{ - Metadata: metadataBytes, - LastSeenAt: 1, - ShouldReport: false, - Timestamp: timestamppb.Now(), - EncodableOutcome: nil, // Not needed for benchmark - } - } - - return proto.MarshalOptions{Deterministic: true}.Marshal(outcome) -} - -// createTestAttributedObservations generates attributed observations from multiple oracles -func createTestAttributedObservations(b *testing.B, numOracles, numWorkflows, numStreamsPerWorkflow int) []types.AttributedObservation { - aos := make([]types.AttributedObservation, numOracles) - ts := timestamppb.Now() // Use a consistent timestamp for all observations to ensure consensus - for oracle := range numOracles { - observationsProto := &pbtypes.Observations{ - Observations: make([]*pbtypes.Observation, numWorkflows), - RegisteredWorkflowIds: make([]string, numWorkflows), - Timestamp: ts, - } - - // Create an observation for each workflow - for i := range numWorkflows { - workflowID := fmt.Sprintf("workflow-%d", i) - executionID := fmt.Sprintf("execution-%d", i) - observationsProto.RegisteredWorkflowIds[i] = workflowID - - // Create LLO events - lloEvent := createLLOEvent(b, numStreamsPerWorkflow, ts.AsTime()) - wrappedEvent, err := values.Wrap(lloEvent) - require.NoError(b, err) - - // Create list value with the LLO event - listVal, err := values.NewList([]any{wrappedEvent}) - require.NoError(b, err) - - listProto := values.Proto(listVal).GetListValue() - require.NotNil(b, listProto, "listProto should not be nil") // Ensure listProto is not nil - - // Add observation for this workflow - observationsProto.Observations[i] = &pbtypes.Observation{ - Id: &pbtypes.Id{ - WorkflowExecutionId: executionID, - WorkflowId: workflowID, - WorkflowOwner: "test-owner", - WorkflowName: fmt.Sprintf("Workflow %d", i), - WorkflowDonId: 1, - WorkflowDonConfigVersion: 1, - ReportId: fmt.Sprintf("report-%d", i), - KeyId: "test-key", - }, - Observations: listProto, - } - } - - // Marshal the observations - obsBytes, err := proto.MarshalOptions{Deterministic: true}.Marshal(observationsProto) - require.NoError(b, err) - - // Create attributed observation - aos[oracle] = types.AttributedObservation{ - Observation: obsBytes, - Observer: ocrcommon.OracleID(oracle), - } - } - - return aos -} - -// createLLOEvent creates an LLO event with the specified number of streams -func createLLOEvent(b *testing.B, numStreams int, ts time.Time) *datastreams.LLOStreamsTriggerEvent { - timestamp := uint64(ts.UnixNano()) - event := &datastreams.LLOStreamsTriggerEvent{ - ObservationTimestampNanoseconds: timestamp, - Payload: make([]*datastreams.LLOStreamDecimal, 0, numStreams), - } - - // Create stream values with consistent prices - for i := range numStreams { - price := decimal.NewFromInt(int64(100 + i%10)) // Use a few different price values - binary, err := price.MarshalBinary() - require.NoError(b, err) - - event.Payload = append(event.Payload, &datastreams.LLOStreamDecimal{ - StreamID: uint32(i), - Decimal: binary, - }) - } - - return event -} - -// createLLOAggregator creates an LLO aggregator with the specified number of streams -func createLLOAggregator(b *testing.B, numStreams int) (pbtypes.Aggregator, error) { - // Create feed configs for all streams - streamConfigs := make(map[string]datafeeds.FeedConfig, numStreams) - for i := range numStreams { - streamConfigs[strconv.Itoa(i)] = datafeeds.FeedConfig{ - // Deviation: decimal.NewFromFloat(0.01), // 1% deviation threshold - Heartbeat: 3600, // 1 hour heartbeat - RemappedIDHex: fmt.Sprintf("0x%064x", i+1000), // Unique remapped ID - } - } - - // Create LLO config - c := datafeeds.LLOAggregatorConfig{ - Streams: streamConfigs, - } - - // Create LLO aggregator - //return datafeeds.NewLLOAggregator(configMap) - m, err := c.ToMap() - if err != nil { - // Handle error in creating LLO aggregator - return nil, fmt.Errorf("failed to create LLO aggregator: %w", err) - } - return datafeeds.NewLLOAggregator(*m) -} diff --git a/pkg/capabilities/consensus/ocr3/datafeeds/data_feeds_types.pb.go b/pkg/capabilities/consensus/ocr3/datafeeds/data_feeds_types.pb.go deleted file mode 100644 index da76da518f..0000000000 --- a/pkg/capabilities/consensus/ocr3/datafeeds/data_feeds_types.pb.go +++ /dev/null @@ -1,324 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.5 -// protoc v5.29.3 -// source: capabilities/consensus/ocr3/datafeeds/data_feeds_types.proto - -package datafeeds - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type DataFeedsOutcomeMetadata struct { - state protoimpl.MessageState `protogen:"open.v1"` - FeedInfo map[string]*DataFeedsMercuryReportInfo `protobuf:"bytes,1,rep,name=feedInfo,proto3" json:"feedInfo,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *DataFeedsOutcomeMetadata) Reset() { - *x = DataFeedsOutcomeMetadata{} - mi := &file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *DataFeedsOutcomeMetadata) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DataFeedsOutcomeMetadata) ProtoMessage() {} - -func (x *DataFeedsOutcomeMetadata) ProtoReflect() protoreflect.Message { - mi := &file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use DataFeedsOutcomeMetadata.ProtoReflect.Descriptor instead. -func (*DataFeedsOutcomeMetadata) Descriptor() ([]byte, []int) { - return file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_rawDescGZIP(), []int{0} -} - -func (x *DataFeedsOutcomeMetadata) GetFeedInfo() map[string]*DataFeedsMercuryReportInfo { - if x != nil { - return x.FeedInfo - } - return nil -} - -type DataFeedsMercuryReportInfo struct { - state protoimpl.MessageState `protogen:"open.v1"` - ObservationTimestamp int64 `protobuf:"varint,3,opt,name=observation_timestamp,json=observationTimestamp,proto3" json:"observation_timestamp,omitempty"` - BenchmarkPrice []byte `protobuf:"bytes,5,opt,name=benchmark_price,json=benchmarkPrice,proto3" json:"benchmark_price,omitempty"` // big.Int - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *DataFeedsMercuryReportInfo) Reset() { - *x = DataFeedsMercuryReportInfo{} - mi := &file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *DataFeedsMercuryReportInfo) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DataFeedsMercuryReportInfo) ProtoMessage() {} - -func (x *DataFeedsMercuryReportInfo) ProtoReflect() protoreflect.Message { - mi := &file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_msgTypes[1] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use DataFeedsMercuryReportInfo.ProtoReflect.Descriptor instead. -func (*DataFeedsMercuryReportInfo) Descriptor() ([]byte, []int) { - return file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_rawDescGZIP(), []int{1} -} - -func (x *DataFeedsMercuryReportInfo) GetObservationTimestamp() int64 { - if x != nil { - return x.ObservationTimestamp - } - return 0 -} - -func (x *DataFeedsMercuryReportInfo) GetBenchmarkPrice() []byte { - if x != nil { - return x.BenchmarkPrice - } - return nil -} - -type LLOOutcomeMetadata struct { - state protoimpl.MessageState `protogen:"open.v1"` - StreamInfo map[uint32]*LLOStreamInfo `protobuf:"bytes,1,rep,name=streamInfo,proto3" json:"streamInfo,omitempty" protobuf_key:"varint,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *LLOOutcomeMetadata) Reset() { - *x = LLOOutcomeMetadata{} - mi := &file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *LLOOutcomeMetadata) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*LLOOutcomeMetadata) ProtoMessage() {} - -func (x *LLOOutcomeMetadata) ProtoReflect() protoreflect.Message { - mi := &file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_msgTypes[2] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use LLOOutcomeMetadata.ProtoReflect.Descriptor instead. -func (*LLOOutcomeMetadata) Descriptor() ([]byte, []int) { - return file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_rawDescGZIP(), []int{2} -} - -func (x *LLOOutcomeMetadata) GetStreamInfo() map[uint32]*LLOStreamInfo { - if x != nil { - return x.StreamInfo - } - return nil -} - -type LLOStreamInfo struct { - state protoimpl.MessageState `protogen:"open.v1"` - Timestamp int64 `protobuf:"varint,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` - Price []byte `protobuf:"bytes,2,opt,name=price,proto3" json:"price,omitempty"` // Decimal - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *LLOStreamInfo) Reset() { - *x = LLOStreamInfo{} - mi := &file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *LLOStreamInfo) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*LLOStreamInfo) ProtoMessage() {} - -func (x *LLOStreamInfo) ProtoReflect() protoreflect.Message { - mi := &file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_msgTypes[3] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use LLOStreamInfo.ProtoReflect.Descriptor instead. -func (*LLOStreamInfo) Descriptor() ([]byte, []int) { - return file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_rawDescGZIP(), []int{3} -} - -func (x *LLOStreamInfo) GetTimestamp() int64 { - if x != nil { - return x.Timestamp - } - return 0 -} - -func (x *LLOStreamInfo) GetPrice() []byte { - if x != nil { - return x.Price - } - return nil -} - -var File_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto protoreflect.FileDescriptor - -var file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_rawDesc = string([]byte{ - 0x0a, 0x3c, 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x2f, 0x63, - 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2f, 0x6f, 0x63, 0x72, 0x33, 0x2f, 0x64, 0x61, - 0x74, 0x61, 0x66, 0x65, 0x65, 0x64, 0x73, 0x2f, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x66, 0x65, 0x65, - 0x64, 0x73, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0f, - 0x64, 0x61, 0x74, 0x61, 0x66, 0x65, 0x65, 0x64, 0x73, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x22, - 0xd9, 0x01, 0x0a, 0x18, 0x44, 0x61, 0x74, 0x61, 0x46, 0x65, 0x65, 0x64, 0x73, 0x4f, 0x75, 0x74, - 0x63, 0x6f, 0x6d, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x53, 0x0a, 0x08, - 0x66, 0x65, 0x65, 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x37, - 0x2e, 0x64, 0x61, 0x74, 0x61, 0x66, 0x65, 0x65, 0x64, 0x73, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, - 0x2e, 0x44, 0x61, 0x74, 0x61, 0x46, 0x65, 0x65, 0x64, 0x73, 0x4f, 0x75, 0x74, 0x63, 0x6f, 0x6d, - 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x46, 0x65, 0x65, 0x64, 0x49, 0x6e, - 0x66, 0x6f, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x66, 0x65, 0x65, 0x64, 0x49, 0x6e, 0x66, - 0x6f, 0x1a, 0x68, 0x0a, 0x0d, 0x46, 0x65, 0x65, 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x12, 0x41, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x64, 0x61, 0x74, 0x61, 0x66, 0x65, 0x65, 0x64, 0x73, 0x5f, - 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x46, 0x65, 0x65, 0x64, 0x73, 0x4d, - 0x65, 0x72, 0x63, 0x75, 0x72, 0x79, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x8c, 0x01, 0x0a, 0x1a, - 0x44, 0x61, 0x74, 0x61, 0x46, 0x65, 0x65, 0x64, 0x73, 0x4d, 0x65, 0x72, 0x63, 0x75, 0x72, 0x79, - 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x33, 0x0a, 0x15, 0x6f, 0x62, - 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x14, 0x6f, 0x62, 0x73, 0x65, 0x72, - 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, - 0x27, 0x0a, 0x0f, 0x62, 0x65, 0x6e, 0x63, 0x68, 0x6d, 0x61, 0x72, 0x6b, 0x5f, 0x70, 0x72, 0x69, - 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x62, 0x65, 0x6e, 0x63, 0x68, 0x6d, - 0x61, 0x72, 0x6b, 0x50, 0x72, 0x69, 0x63, 0x65, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x4a, 0x04, - 0x08, 0x02, 0x10, 0x03, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x22, 0xc8, 0x01, 0x0a, 0x12, 0x4c, - 0x4c, 0x4f, 0x4f, 0x75, 0x74, 0x63, 0x6f, 0x6d, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x12, 0x53, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x64, 0x61, 0x74, 0x61, 0x66, 0x65, 0x65, 0x64, - 0x73, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x4c, 0x4c, 0x4f, 0x4f, 0x75, 0x74, 0x63, 0x6f, - 0x6d, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, - 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x73, 0x74, 0x72, 0x65, - 0x61, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x1a, 0x5d, 0x0a, 0x0f, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, - 0x49, 0x6e, 0x66, 0x6f, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x34, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x64, 0x61, 0x74, - 0x61, 0x66, 0x65, 0x65, 0x64, 0x73, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x4c, 0x4c, 0x4f, - 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x43, 0x0a, 0x0d, 0x4c, 0x4c, 0x4f, 0x53, 0x74, 0x72, 0x65, - 0x61, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x05, 0x70, 0x72, 0x69, 0x63, 0x65, 0x42, 0x26, 0x5a, 0x24, 0x70, 0x6b, - 0x67, 0x2f, 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x2f, 0x63, - 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2f, 0x64, 0x61, 0x74, 0x61, 0x66, 0x65, 0x65, - 0x64, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -}) - -var ( - file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_rawDescOnce sync.Once - file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_rawDescData []byte -) - -func file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_rawDescGZIP() []byte { - file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_rawDescOnce.Do(func() { - file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_rawDesc), len(file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_rawDesc))) - }) - return file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_rawDescData -} - -var file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_msgTypes = make([]protoimpl.MessageInfo, 6) -var file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_goTypes = []any{ - (*DataFeedsOutcomeMetadata)(nil), // 0: datafeeds_types.DataFeedsOutcomeMetadata - (*DataFeedsMercuryReportInfo)(nil), // 1: datafeeds_types.DataFeedsMercuryReportInfo - (*LLOOutcomeMetadata)(nil), // 2: datafeeds_types.LLOOutcomeMetadata - (*LLOStreamInfo)(nil), // 3: datafeeds_types.LLOStreamInfo - nil, // 4: datafeeds_types.DataFeedsOutcomeMetadata.FeedInfoEntry - nil, // 5: datafeeds_types.LLOOutcomeMetadata.StreamInfoEntry -} -var file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_depIdxs = []int32{ - 4, // 0: datafeeds_types.DataFeedsOutcomeMetadata.feedInfo:type_name -> datafeeds_types.DataFeedsOutcomeMetadata.FeedInfoEntry - 5, // 1: datafeeds_types.LLOOutcomeMetadata.streamInfo:type_name -> datafeeds_types.LLOOutcomeMetadata.StreamInfoEntry - 1, // 2: datafeeds_types.DataFeedsOutcomeMetadata.FeedInfoEntry.value:type_name -> datafeeds_types.DataFeedsMercuryReportInfo - 3, // 3: datafeeds_types.LLOOutcomeMetadata.StreamInfoEntry.value:type_name -> datafeeds_types.LLOStreamInfo - 4, // [4:4] is the sub-list for method output_type - 4, // [4:4] is the sub-list for method input_type - 4, // [4:4] is the sub-list for extension type_name - 4, // [4:4] is the sub-list for extension extendee - 0, // [0:4] is the sub-list for field type_name -} - -func init() { file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_init() } -func file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_init() { - if File_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto != nil { - return - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_rawDesc), len(file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_rawDesc)), - NumEnums: 0, - NumMessages: 6, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_goTypes, - DependencyIndexes: file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_depIdxs, - MessageInfos: file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_msgTypes, - }.Build() - File_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto = out.File - file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_goTypes = nil - file_capabilities_consensus_ocr3_datafeeds_data_feeds_types_proto_depIdxs = nil -} diff --git a/pkg/capabilities/consensus/ocr3/datafeeds/data_feeds_types.proto b/pkg/capabilities/consensus/ocr3/datafeeds/data_feeds_types.proto deleted file mode 100644 index 972b7774a2..0000000000 --- a/pkg/capabilities/consensus/ocr3/datafeeds/data_feeds_types.proto +++ /dev/null @@ -1,26 +0,0 @@ -syntax = "proto3"; - -option go_package = "pkg/capabilities/consensus/datafeeds"; - -package datafeeds_types; - -message DataFeedsOutcomeMetadata { - map feedInfo = 1; -} - -message DataFeedsMercuryReportInfo { - reserved 1, 2, 4; - int64 observation_timestamp = 3; - bytes benchmark_price = 5; // big.Int -} - -// LLOOutcomeMetadata is the state representation of the OCR3 consensus capability -// when running the LLO aggregator. -message LLOOutcomeMetadata { - map streamInfo = 1; -} - -message LLOStreamInfo { - int64 timestamp = 1; // Nanoseconds. Unix timestamp of observation produced by the LLO oracle - bytes price = 2; // binary representation of [llo.Decimal]: https://github.com/smartcontractkit/chainlink-data-streams/blob/d33e95631485bbcfdc22d209875035e3c73199d0/llo/stream_value.go#L147 -} \ No newline at end of file diff --git a/pkg/capabilities/consensus/ocr3/datafeeds/example_llo_aggregator_test.go b/pkg/capabilities/consensus/ocr3/datafeeds/example_llo_aggregator_test.go deleted file mode 100644 index 2de6d92a09..0000000000 --- a/pkg/capabilities/consensus/ocr3/datafeeds/example_llo_aggregator_test.go +++ /dev/null @@ -1,128 +0,0 @@ -package datafeeds_test - -import ( - "fmt" - - "github.com/shopspring/decimal" - - ocrcommon "github.com/smartcontractkit/libocr/commontypes" - - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/datafeeds" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/types" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/datastreams" - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-protos/cre/go/values" -) - -// Example of using LLOAggregator.Aggregate with multiple oracles and price streams -// It constructs a LLOAggregator with two streams, simulates observations from three oracles, -// and demonstrates how to process the aggregation outcome. -// go test -run ExampleLLOAggregator_Aggregate -func ExampleLLOAggregator_Aggregate() { - // Create a logger - lggr, err := logger.New() - if err != nil { - panic(err) - } - // 1. Create aggregator with 2 stream configs - configMap, _ := values.NewMap(map[string]any{ - "streams": map[string]any{ - "1": map[string]any{ - "deviation": "0.01", // 1% deviation threshold - "heartbeat": 3600, // 1 hour heartbeat - "remappedID": "0x680084f7347baFfb5C323c2982dfC90e04F9F918", - }, - "2": map[string]any{ - "deviation": "0.02", // 2% deviation threshold - "heartbeat": 1800, // 30 min heartbeat - "remappedID": "0x00001237347baFfb5C323c1112dfC90e0789FFFF", - }, - }, - "allowedPartialStaleness": "0.2", // 20% partial staleness - }) - - aggregator, err := datafeeds.NewLLOAggregator(*configMap) - if err != nil { - panic(err) - } - - // 2. Create empty previous outcome (first round); empty previousOutcome will cause all streams to be updated - var previousOutcome *types.AggregationOutcome - - // 3. Create observations from 3 oracles - observations := make(map[ocrcommon.OracleID][]values.Value) - timestamp := uint64(61116379204) //uint64(time.Now().UnixNano()) //nolint: gosec // G115 - - price1, err := decimal.NewFromString("1250427975000000000000") - if err != nil { - panic(err) - } - price2, err := decimal.NewFromString("39250250000000000000000") - if err != nil { - panic(err) - } - - // Setup price data for 2 streams - prices := map[uint32]decimal.Decimal{ - 1: price1, // ETH/USD price - 2: price2, // BTC/USD price - } - - // Create the same observation for each oracle to ensure f+1 consensus - for i := ocrcommon.OracleID(1); i <= 3; i++ { - // Create LLO event with price payload - event := &datastreams.LLOStreamsTriggerEvent{ - ObservationTimestampNanoseconds: timestamp, - Payload: make([]*datastreams.LLOStreamDecimal, 0, len(prices)), - } - - // Add each price to the payload - for streamID, price := range prices { - // Convert decimal to binary representation - priceBinary, _ := price.MarshalBinary() - - event.Payload = append(event.Payload, &datastreams.LLOStreamDecimal{ - StreamID: streamID, - Decimal: priceBinary, - }) - } - - // Wrap the event in a values.Value - val, err2 := values.Wrap(event) - if err2 != nil { - panic(err2) - } - observations[i] = []values.Value{val} - } - - // 4. Call Aggregate with f=1 - outcome, err := aggregator.Aggregate(lggr, previousOutcome, observations, 1) - if err != nil { - fmt.Printf("Error: %v\n", err) - return - } - - // 5. Print results - fmt.Printf("Should report: %v\n", outcome.ShouldReport) - - // Decode the results to view updated streams - if outcome.ShouldReport { - streamIDs, reports, err := processOutcome(outcome) - if err != nil { - panic(err) - } - fmt.Printf("Updated streams: %d\n", len(streamIDs)) - - // Print details of each updated stream - for i, report := range reports { - fmt.Printf(" Stream %d: ID=%d, Price=%s, Timestamp=%d, RemappedID=%x\n", - i+1, report.StreamID, report.Price.String(), timestamp, report.RemappedID) - } - } - - // Output: - // Should report: true - // Updated streams: 2 - // Stream 1: ID=1, Price=1250427975000000000000, Timestamp=61116379204, RemappedID=680084f7347baffb5c323c2982dfc90e04f9f918 - // Stream 2: ID=2, Price=39250250000000000000000, Timestamp=61116379204, RemappedID=00001237347baffb5c323c1112dfc90e0789ffff -} diff --git a/pkg/capabilities/consensus/ocr3/datafeeds/feeds_aggregator.go b/pkg/capabilities/consensus/ocr3/datafeeds/feeds_aggregator.go deleted file mode 100644 index 56fe002e39..0000000000 --- a/pkg/capabilities/consensus/ocr3/datafeeds/feeds_aggregator.go +++ /dev/null @@ -1,395 +0,0 @@ -package datafeeds - -import ( - "encoding/hex" - "errors" - "fmt" - "math" - "math/big" - "slices" - "strings" - "time" - - "github.com/shopspring/decimal" - "google.golang.org/protobuf/proto" - - ocrcommon "github.com/smartcontractkit/libocr/commontypes" - - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/types" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/datastreams" - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-protos/cre/go/values" -) - -type EVMEncoderKey = string - -const ( - // Aggregator outputs reports in the following format: - // []Reports{FeedID []byte, RawReport []byte, Price *big.Int, Timestamp int64} - // Example of a compatible EVM encoder ABI config: - // (bytes32 FeedID, bytes RawReport, uint256 Price, uint64 Timestamp)[] Reports - - // The following constants are used in value maps to ensure consistent naming while the underlying - // implementation is untyped. - TopLevelListOutputFieldName = EVMEncoderKey("Reports") - FeedIDOutputFieldName = EVMEncoderKey("FeedID") - RawReportOutputFieldName = EVMEncoderKey("RawReport") - PriceOutputFieldName = EVMEncoderKey("Price") - TimestampOutputFieldName = EVMEncoderKey("Timestamp") - RemappedIDOutputFieldName = EVMEncoderKey("RemappedID") - StreamIDOutputFieldName = EVMEncoderKey("StreamID") - DataIDOutputFieldName = EVMEncoderKey("DataID") - AnswerOutputFieldName = EVMEncoderKey("Answer") - - addrLen = 20 -) - -type aggregatorConfig struct { - Feeds map[datastreams.FeedID]FeedConfig - // AllowedPartialStaleness is an optional optimization that tries to maximize batching. - // Once any deviation or heartbeat threshold hits, we will include all other feeds that are - // within the AllowedPartialStaleness range of their own heartbeat. - // For example, setting 0.2 will include all feeds that are within 20% of their heartbeat. - AllowedPartialStaleness float64 `mapstructure:"-"` - AllowedPartialStalenessStr string `mapstructure:"allowedPartialStaleness"` -} - -// FeedConfig defines the configuration for each individual feed used by the aggregator. -// It's map representation is used directly in user-defined workflows to specify the configuration for each feed. -type FeedConfig struct { - Heartbeat int // seconds - Deviation string `mapstructure:"deviation"` - RemappedIDHex string `mapstructure:"remappedId"` // DO NOT CHANGE THIS. It's user facing in existing DataFeeds configurations and should be kept consistent for backward compatibility. - // internal fields set by [ParseConfig] after parsing the config - // work around mapstructure limitations to allow for decimal.Decimal and byte slices - parsedDeviation decimal.Decimal - remappedID []byte -} - -func (c FeedConfig) HeartbeatNanos() int64 { - return int64(c.Heartbeat) * time.Second.Nanoseconds() -} - -func (c FeedConfig) DeviationAsDecimal() decimal.Decimal { - return c.parsedDeviation -} - -func (c FeedConfig) RemappedID() []byte { - return c.remappedID -} - -type dataFeedsAggregator struct { - config aggregatorConfig - reportCodec datastreams.ReportCodec -} - -var _ types.Aggregator = (*dataFeedsAggregator)(nil) - -// This Aggregator has two phases: -// 1. Agree on valid trigger signers by extracting them from event metadata and aggregating using MODE (at least F+1 copies needed). -// 2. For each FeedID, select latest valid report, using signers list obtained in phase 1. -// -// EncodableOutcome is a list of aggregated price points. -// Metadata is a map of feedID -> (timestamp, price) representing onchain state (see DataFeedsOutcomeMetadata proto) -func (a *dataFeedsAggregator) Aggregate(lggr logger.Logger, previousOutcome *types.AggregationOutcome, observations map[ocrcommon.OracleID][]values.Value, f int) (*types.AggregationOutcome, error) { - allowedSigners, minRequiredSignatures, events := a.extractSignersAndPayloads(lggr, observations, f) - if len(events) > 0 && minRequiredSignatures == 0 { - return nil, errors.New("cannot process non-empty observation payloads with minRequiredSignatures set to 0") - } - lggr.Debugw("extracted signers", "nAllowedSigners", len(allowedSigners), "minRequired", minRequiredSignatures, "nEvents", len(events)) - // find latest valid report for each feed ID - latestReportPerFeed := make(map[datastreams.FeedID]datastreams.FeedReport) - for nodeID, event := range events { - mercuryReports, err := a.reportCodec.Unwrap(event) - if err != nil { - lggr.Errorf("node %d contributed with invalid reports: %v", nodeID, err) - continue - } - for _, report := range mercuryReports { - latest, ok := latestReportPerFeed[datastreams.FeedID(report.FeedID)] - if !ok || report.ObservationTimestamp > latest.ObservationTimestamp { - // lazy signature validation - if err := a.reportCodec.Validate(report, allowedSigners, minRequiredSignatures); err != nil { - lggr.Errorf("node %d contributed with an invalid report: %v", nodeID, err) - } else { - latestReportPerFeed[datastreams.FeedID(report.FeedID)] = report - } - } - } - } - lggr.Debugw("collected latestReportPerFeed", "len", len(latestReportPerFeed)) - - currentState, err := a.initializeCurrentState(lggr, previousOutcome) - if err != nil { - return nil, err - } - - reportsNeedingUpdate := []datastreams.FeedReport{} - allIDs := []string{} - for feedID := range currentState.FeedInfo { - allIDs = append(allIDs, feedID) - } - - lggr.Debugw("determined feeds to check", "nFeedIds", len(allIDs)) - // ensure deterministic order of reportsNeedingUpdate - slices.Sort(allIDs) - candidateIDs := []string{} - for _, feedIDStr := range allIDs { - previousReportInfo := currentState.FeedInfo[feedIDStr] - feedID, err2 := datastreams.NewFeedID(feedIDStr) - if err2 != nil { - lggr.Errorw("could not convert %s to feedID", "feedID", feedID) - continue - } - latestReport, ok := latestReportPerFeed[feedID] - if !ok { - lggr.Errorw("no new Mercury report for feed", "feedID", feedID) - continue - } - config := a.config.Feeds[feedID] - oldPrice := big.NewInt(0).SetBytes(previousReportInfo.BenchmarkPrice) - newPrice := big.NewInt(0).SetBytes(latestReport.BenchmarkPrice) - currDeviation := deviation(oldPrice, newPrice) - currStaleness := latestReport.ObservationTimestamp - previousReportInfo.ObservationTimestamp - lggr.Debugw("checking deviation and heartbeat", - "feedID", feedID, - "currentTs", latestReport.ObservationTimestamp, - "oldTs", previousReportInfo.ObservationTimestamp, - "currStaleness", currStaleness, - "heartbeat", config.Heartbeat, - "oldPrice", oldPrice, - "newPrice", newPrice, - "currDeviation", currDeviation, - "deviation", config.DeviationAsDecimal().InexactFloat64(), - ) - if currStaleness > int64(config.Heartbeat) || - currDeviation > config.DeviationAsDecimal().InexactFloat64() { - previousReportInfo.ObservationTimestamp = latestReport.ObservationTimestamp - previousReportInfo.BenchmarkPrice = latestReport.BenchmarkPrice - reportsNeedingUpdate = append(reportsNeedingUpdate, latestReport) - } else if float64(currStaleness) > float64(config.Heartbeat)*(1.0-a.config.AllowedPartialStaleness) { - candidateIDs = append(candidateIDs, feedIDStr) - } - } - - // optimization that allows for more efficient batching - if len(reportsNeedingUpdate) > 0 { - for _, feedIDStr := range candidateIDs { - previousReportInfo := currentState.FeedInfo[feedIDStr] - latestReport := latestReportPerFeed[datastreams.FeedID(feedIDStr)] - previousReportInfo.ObservationTimestamp = latestReport.ObservationTimestamp - previousReportInfo.BenchmarkPrice = latestReport.BenchmarkPrice - reportsNeedingUpdate = append(reportsNeedingUpdate, latestReport) - } - } - - marshalledState, err := proto.MarshalOptions{Deterministic: true}.Marshal(currentState) - if err != nil { - return nil, err - } - - toWrap := make([]any, 0, len(reportsNeedingUpdate)) - for _, report := range reportsNeedingUpdate { - feedID := datastreams.FeedID(report.FeedID).Bytes() - remappedID := a.config.Feeds[datastreams.FeedID(report.FeedID)].RemappedID() - if len(remappedID) == 0 { // fall back to original ID - remappedID = feedID[:] - } - toWrap = append(toWrap, - map[EVMEncoderKey]any{ - FeedIDOutputFieldName: feedID[:], - RawReportOutputFieldName: report.FullReport, - PriceOutputFieldName: big.NewInt(0).SetBytes(report.BenchmarkPrice), - TimestampOutputFieldName: report.ObservationTimestamp, - RemappedIDOutputFieldName: remappedID, - }) - } - - wrappedReportsNeedingUpdates, err := values.NewMap(map[EVMEncoderKey]any{ - TopLevelListOutputFieldName: toWrap, - }) - if err != nil { - return nil, err - } - reportsProto := values.Proto(wrappedReportsNeedingUpdates) - - lggr.Debugw("Aggregate complete", "nReportsNeedingUpdate", len(reportsNeedingUpdate)) - return &types.AggregationOutcome{ - EncodableOutcome: reportsProto.GetMapValue(), - Metadata: marshalledState, - ShouldReport: len(reportsNeedingUpdate) > 0, - }, nil -} - -func (a *dataFeedsAggregator) initializeCurrentState(lggr logger.Logger, previousOutcome *types.AggregationOutcome) (*DataFeedsOutcomeMetadata, error) { - currentState := &DataFeedsOutcomeMetadata{} - if previousOutcome != nil { - err := proto.Unmarshal(previousOutcome.Metadata, currentState) - if err != nil { - return nil, err - } - } - // initialize empty state for missing feeds - if currentState.FeedInfo == nil { - currentState.FeedInfo = make(map[string]*DataFeedsMercuryReportInfo) - } - for feedID := range a.config.Feeds { - if _, ok := currentState.FeedInfo[feedID.String()]; !ok { - currentState.FeedInfo[feedID.String()] = &DataFeedsMercuryReportInfo{ - ObservationTimestamp: 0, // will always trigger an update - BenchmarkPrice: big.NewInt(0).Bytes(), - } - lggr.Debugw("initializing empty onchain state for feed", "feedID", feedID.String()) - } - } - // remove obsolete feeds from state - for feedID := range currentState.FeedInfo { - if _, ok := a.config.Feeds[datastreams.FeedID(feedID)]; !ok { - delete(currentState.FeedInfo, feedID) - lggr.Debugw("removed obsolete feedID from state", "feedID", feedID) - } - } - lggr.Debugw("current state initialized", "state", currentState, "previousOutcome", previousOutcome) - return currentState, nil -} - -func (a *dataFeedsAggregator) extractSignersAndPayloads(lggr logger.Logger, observations map[ocrcommon.OracleID][]values.Value, fConsensus int) ([][]byte, int, map[ocrcommon.OracleID]values.Value) { - events := make(map[ocrcommon.OracleID]values.Value) - signers := make(map[[addrLen]byte]int) - mins := make(map[int]int) - for nodeID, nodeObservations := range observations { - // we only expect a single observation per node - a Streams trigger event - if len(nodeObservations) == 0 || nodeObservations[0] == nil { - lggr.Warnf("node %d contributed with empty observations", nodeID) - continue - } - if len(nodeObservations) > 1 { - lggr.Warnf("node %d contributed with more than one observation", nodeID) - continue - } - triggerEvent := &datastreams.StreamsTriggerEvent{} - if err := nodeObservations[0].UnwrapTo(triggerEvent); err != nil { - lggr.Warnf("could not parse observations from node %d: %v", nodeID, err) - continue - } - meta := triggerEvent.Metadata - currentNodeSigners, err := extractUniqueSigners(meta.Signers) - if err != nil { - lggr.Warnf("could not extract signers from node %d: %v", nodeID, err) - continue - } - for signer := range currentNodeSigners { - signers[signer]++ - } - mins[meta.MinRequiredSignatures]++ - events[nodeID] = nodeObservations[0] - } - // Agree on signers list and min-required. It's technically possible to have F+1 valid values from one trigger DON and F+1 from another trigger DON. - // In that case both values are legitimate and signers list will contain nodes from both DONs. However, min-required value will be the higher one (if different). - allowedSigners := [][]byte{} - for signer, count := range signers { - if count >= fConsensus+1 { - allowedSigners = append(allowedSigners, signer[:]) - } - } - minRequired := 0 - for minCandidate, count := range mins { - if count >= fConsensus+1 && minCandidate > minRequired { - minRequired = minCandidate - } - } - return allowedSigners, minRequired, events -} - -func extractUniqueSigners(signers [][]byte) (map[[addrLen]byte]struct{}, error) { - uniqueSigners := make(map[[addrLen]byte]struct{}) - for _, signer := range signers { - if len(signer) != addrLen { - return nil, fmt.Errorf("invalid signer length: %d", len(signer)) - } - var signerBytes [addrLen]byte - copy(signerBytes[:], signer) - uniqueSigners[signerBytes] = struct{}{} - } - return uniqueSigners, nil -} - -func deviation(oldPrice, newPrice *big.Int) float64 { - diff := &big.Int{} - diff.Sub(oldPrice, newPrice) - diff.Abs(diff) - if oldPrice.Cmp(big.NewInt(0)) == 0 { - if diff.Cmp(big.NewInt(0)) == 0 { - return 0.0 - } - return math.MaxFloat64 - } - diffFl, _ := diff.Float64() - oldFl, _ := oldPrice.Float64() - return diffFl / oldFl -} - -// (krehermann) found it surprisingly tricky to faithfully convert from decimal.Decimal to big.Int -// so i just used the same logic as in the original code -func deviationDecimal(oldPrice, newPrice decimal.Decimal) float64 { - diff := oldPrice.Sub(newPrice).Abs() - if oldPrice.IsZero() { - if diff.IsZero() { - return 0.0 - } - return math.MaxFloat64 - } - diffFl, _ := diff.Float64() - oldFl, _ := oldPrice.Float64() - return diffFl / oldFl -} - -func NewDataFeedsAggregator(config values.Map, reportCodec datastreams.ReportCodec) (types.Aggregator, error) { - parsedConfig, err := ParseConfig(config) - if err != nil { - return nil, fmt.Errorf("failed to parse config (%+v): %w", config, err) - } - return &dataFeedsAggregator{ - config: parsedConfig, - reportCodec: reportCodec, - }, nil -} - -func ParseConfig(config values.Map) (aggregatorConfig, error) { - parsedConfig := aggregatorConfig{} - if err := config.UnwrapTo(&parsedConfig); err != nil { - return aggregatorConfig{}, err - } - - for feedID, feedCfg := range parsedConfig.Feeds { - if feedCfg.Deviation != "" { - if _, err := datastreams.NewFeedID(feedID.String()); err != nil { - return aggregatorConfig{}, fmt.Errorf("cannot parse feedID config for feed %s: %w", feedID, err) - } - dec, err := decimal.NewFromString(feedCfg.Deviation) - if err != nil { - return aggregatorConfig{}, fmt.Errorf("cannot parse deviation config for feed %s: %w", feedID, err) - } - feedCfg.parsedDeviation = dec - parsedConfig.Feeds[feedID] = feedCfg - } - trimmed, nonEmpty := strings.CutPrefix(feedCfg.RemappedIDHex, "0x") - if nonEmpty { - rawRemappedID, err := hex.DecodeString(trimmed) - if err != nil { - return aggregatorConfig{}, fmt.Errorf("cannot parse remappedId config for feed %s: %w", feedID, err) - } - feedCfg.remappedID = rawRemappedID - parsedConfig.Feeds[feedID] = feedCfg - } - } - // convert allowedPartialStaleness from string to float64 - if parsedConfig.AllowedPartialStalenessStr != "" { - allowedPartialStaleness, err := decimal.NewFromString(parsedConfig.AllowedPartialStalenessStr) - if err != nil { - return aggregatorConfig{}, fmt.Errorf("cannot parse allowedPartialStaleness: %w", err) - } - parsedConfig.AllowedPartialStaleness = allowedPartialStaleness.InexactFloat64() - } - return parsedConfig, nil -} diff --git a/pkg/capabilities/consensus/ocr3/datafeeds/feeds_aggregator_test.go b/pkg/capabilities/consensus/ocr3/datafeeds/feeds_aggregator_test.go deleted file mode 100644 index 4c8443d078..0000000000 --- a/pkg/capabilities/consensus/ocr3/datafeeds/feeds_aggregator_test.go +++ /dev/null @@ -1,254 +0,0 @@ -package datafeeds_test - -import ( - "crypto/rand" - "encoding/hex" - "math" - "math/big" - "testing" - - "github.com/shopspring/decimal" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" - - "github.com/smartcontractkit/libocr/commontypes" - - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/datafeeds" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/datastreams" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/datastreams/mocks" - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-protos/cre/go/values" -) - -var ( - feedIDA = datastreams.FeedID("0x0001013ebd4ed3f5889fb5a8a52b42675c60c1a8c42bc79eaa72dcd922ac4292") - remappedIDA = "0x680084f7347baFfb5C323c2982dfC90e04F9F918" - deviationA = decimal.NewFromFloat(0.1) - heartbeatA = 60 - feedIDB = datastreams.FeedID("0x0003c317fec7fad514c67aacc6366bf2f007ce37100e3cddcacd0ccaa1f3746d") - deviationB = decimal.NewFromFloat(0.01) - heartbeatB = 360 - mercuryFullReportA = []byte("report") - allowedPartialStaleness = 0.2 -) - -func TestDataFeedsAggregator_Aggregate_TwoRounds(t *testing.T) { - mockTriggerEvent, err := values.Wrap(datastreams.StreamsTriggerEvent{ - Metadata: datastreams.Metadata{ - Signers: [][]byte{newSigner(t), newSigner(t)}, - MinRequiredSignatures: 1, - }, - }) - require.NoError(t, err) - config := getConfig(t, feedIDA.String(), "0.1", heartbeatA) - codec := mocks.NewReportCodec(t) - agg, err := datafeeds.NewDataFeedsAggregator(*config, codec) - require.NoError(t, err) - - // first round, empty previous Outcome, empty observations - outcome, err := agg.Aggregate(logger.Nop(), nil, map[commontypes.OracleID][]values.Value{}, 1) - require.NoError(t, err) - require.False(t, outcome.ShouldReport) - - // validate metadata - newState := &datafeeds.DataFeedsOutcomeMetadata{} - err = proto.Unmarshal(outcome.Metadata, newState) - require.NoError(t, err) - require.Len(t, newState.FeedInfo, 2) - _, ok := newState.FeedInfo[feedIDA.String()] - require.True(t, ok) - require.Equal(t, []byte(nil), newState.FeedInfo[feedIDA.String()].BenchmarkPrice) - - // second round, non-empty previous Outcome, one observation - latestMercuryReports := []datastreams.FeedReport{ - { - FeedID: feedIDA.String(), - ObservationTimestamp: 1, - BenchmarkPrice: big.NewInt(100).Bytes(), - FullReport: mercuryFullReportA, - }, - } - codec.On("Unwrap", mock.Anything).Return(latestMercuryReports, nil) - codec.On("Validate", mock.Anything, mock.Anything, mock.Anything).Return(nil) - outcome, err = agg.Aggregate(logger.Nop(), outcome, map[commontypes.OracleID][]values.Value{1: {mockTriggerEvent}, 2: {mockTriggerEvent}}, 1) - require.NoError(t, err) - require.True(t, outcome.ShouldReport) - - // validate metadata - err = proto.Unmarshal(outcome.Metadata, newState) - require.NoError(t, err) - require.Len(t, newState.FeedInfo, 2) - _, ok = newState.FeedInfo[feedIDA.String()] - require.True(t, ok) - require.Equal(t, big.NewInt(100).Bytes(), newState.FeedInfo[feedIDA.String()].BenchmarkPrice) - - // validate encodable outcome - val, err := values.FromMapValueProto(outcome.EncodableOutcome) - require.NoError(t, err) - topLevelMap, err := val.Unwrap() - require.NoError(t, err) - mm, ok := topLevelMap.(map[string]any) - require.True(t, ok) - - idBytes := feedIDA.Bytes() - remappedIDABytes, err := hex.DecodeString(remappedIDA[2:]) - require.NoError(t, err) - expected := map[string]any{ - datafeeds.TopLevelListOutputFieldName: []any{ - map[string]any{ - datafeeds.FeedIDOutputFieldName: idBytes[:], - datafeeds.RawReportOutputFieldName: mercuryFullReportA, - datafeeds.TimestampOutputFieldName: int64(1), - datafeeds.PriceOutputFieldName: big.NewInt(100), - datafeeds.RemappedIDOutputFieldName: remappedIDABytes, - }, - }, - } - require.Equal(t, expected, mm) -} - -func TestDataFeedsAggregator_Aggregate_AllowedPartialStaleness(t *testing.T) { - mockTriggerEvent, err := values.Wrap(datastreams.StreamsTriggerEvent{ - Metadata: datastreams.Metadata{ - Signers: [][]byte{newSigner(t), newSigner(t)}, - MinRequiredSignatures: 1, - }, - }) - require.NoError(t, err) - config := getConfig(t, feedIDA.String(), "0.1", heartbeatA) - codec := mocks.NewReportCodec(t) - agg, err := datafeeds.NewDataFeedsAggregator(*config, codec) - require.NoError(t, err) - - // first round, both feeds are stale - latestReportsRound1 := []datastreams.FeedReport{ - { - FeedID: feedIDA.String(), - ObservationTimestamp: 1000, - BenchmarkPrice: big.NewInt(100).Bytes(), - }, - { - FeedID: feedIDB.String(), - ObservationTimestamp: 1100, - BenchmarkPrice: big.NewInt(200).Bytes(), - }, - } - codec.On("Unwrap", mock.Anything).Return(latestReportsRound1, nil).Twice() - codec.On("Validate", mock.Anything, mock.Anything, mock.Anything).Return(nil) - outcome, err := agg.Aggregate(logger.Nop(), nil, map[commontypes.OracleID][]values.Value{1: {mockTriggerEvent}, 2: {mockTriggerEvent}}, 1) - require.NoError(t, err) - require.True(t, outcome.ShouldReport) - require.Len(t, outcome.EncodableOutcome.Fields[datafeeds.TopLevelListOutputFieldName].GetListValue().Fields, 2) - - // second round, B hits deviation, A is not stale - latestReportsRound2 := []datastreams.FeedReport{ - { - FeedID: feedIDA.String(), - ObservationTimestamp: 1010, - BenchmarkPrice: big.NewInt(100).Bytes(), - }, - { - FeedID: feedIDB.String(), - ObservationTimestamp: 1110, - BenchmarkPrice: big.NewInt(400).Bytes(), - }, - } - codec.On("Unwrap", mock.Anything).Return(latestReportsRound2, nil).Twice() - codec.On("Validate", mock.Anything, mock.Anything, mock.Anything).Return(nil) - outcome, err = agg.Aggregate(logger.Nop(), outcome, map[commontypes.OracleID][]values.Value{1: {mockTriggerEvent}, 2: {mockTriggerEvent}}, 1) - require.NoError(t, err) - require.True(t, outcome.ShouldReport) - require.Len(t, outcome.EncodableOutcome.Fields[datafeeds.TopLevelListOutputFieldName].GetListValue().Fields, 1) - - // third round, B hits deviation, A is within allowed partial staleness threshold - latestReportsRound3 := []datastreams.FeedReport{ - { - FeedID: feedIDA.String(), - ObservationTimestamp: 1055, - BenchmarkPrice: big.NewInt(100).Bytes(), - }, - { - FeedID: feedIDB.String(), - ObservationTimestamp: 1150, - BenchmarkPrice: big.NewInt(600).Bytes(), - }, - } - codec.On("Unwrap", mock.Anything).Return(latestReportsRound3, nil).Twice() - codec.On("Validate", mock.Anything, mock.Anything, mock.Anything).Return(nil) - outcome, err = agg.Aggregate(logger.Nop(), outcome, map[commontypes.OracleID][]values.Value{1: {mockTriggerEvent}, 2: {mockTriggerEvent}}, 1) - require.NoError(t, err) - require.True(t, outcome.ShouldReport) - require.Len(t, outcome.EncodableOutcome.Fields[datafeeds.TopLevelListOutputFieldName].GetListValue().Fields, 2) -} - -func TestDataFeedsAggregator_Aggregate_Failures(t *testing.T) { - mockTriggerEvent, err := values.Wrap(datastreams.StreamsTriggerEvent{ - Metadata: datastreams.Metadata{ - Signers: [][]byte{newSigner(t), newSigner(t)}, - MinRequiredSignatures: 1, - }, - }) - require.NoError(t, err) - - config := getConfig(t, feedIDA.String(), "0.1", heartbeatA) - codec := mocks.NewReportCodec(t) - agg, err := datafeeds.NewDataFeedsAggregator(*config, codec) - require.NoError(t, err) - - // no valid signers - each one should appear at least twice to be valid - _, err = agg.Aggregate(logger.Nop(), nil, map[commontypes.OracleID][]values.Value{1: {mockTriggerEvent}}, 1) - require.Error(t, err) -} - -func TestDataFeedsAggregator_ParseConfig(t *testing.T) { - t.Run("happy path", func(t *testing.T) { - config := getConfig(t, feedIDA.String(), "0.1", heartbeatA) - parsedConfig, err := datafeeds.ParseConfig(*config) - require.NoError(t, err) - require.Equal(t, deviationA, parsedConfig.Feeds[feedIDA].DeviationAsDecimal()) - require.Equal(t, heartbeatA, parsedConfig.Feeds[feedIDA].Heartbeat) - require.Equal(t, deviationB, parsedConfig.Feeds[feedIDB].DeviationAsDecimal()) - require.Equal(t, heartbeatB, parsedConfig.Feeds[feedIDB].Heartbeat) - require.InEpsilon(t, allowedPartialStaleness, parsedConfig.AllowedPartialStaleness, math.SmallestNonzeroFloat64) - }) - - t.Run("invalid ID", func(t *testing.T) { - config := getConfig(t, "bad_id", "0.1", heartbeatA) - _, err := datafeeds.ParseConfig(*config) - require.Error(t, err) - }) - - t.Run("invalid deviation string", func(t *testing.T) { - config := getConfig(t, feedIDA.String(), "bad_number", heartbeatA) - _, err := datafeeds.ParseConfig(*config) - require.Error(t, err) - }) -} - -func getConfig(t *testing.T, feedID string, deviation string, heartbeat int) *values.Map { - unwrappedConfig := map[string]any{ - "feeds": map[string]any{ - feedID: map[string]any{ - "deviation": deviation, - "heartbeat": heartbeat, - "remappedID": remappedIDA, - }, - feedIDB.String(): map[string]any{ - "deviation": deviationB.String(), - "heartbeat": heartbeatB, - }, - }, - "allowedPartialStaleness": "0.2", - } - config, err := values.NewMap(unwrappedConfig) - require.NoError(t, err) - return config -} - -func newSigner(t *testing.T) []byte { - buf := make([]byte, 20) - _, err := rand.Read(buf) - require.NoError(t, err) - return buf -} diff --git a/pkg/capabilities/consensus/ocr3/datafeeds/helpers_test.go b/pkg/capabilities/consensus/ocr3/datafeeds/helpers_test.go deleted file mode 100644 index 060870e431..0000000000 --- a/pkg/capabilities/consensus/ocr3/datafeeds/helpers_test.go +++ /dev/null @@ -1,6 +0,0 @@ -package datafeeds - -// exported for testing only -var LLOStreamPrices = lloStreamPrices - -var DecimalToBigInt = decimalToBigInt diff --git a/pkg/capabilities/consensus/ocr3/datafeeds/llo_aggregator.go b/pkg/capabilities/consensus/ocr3/datafeeds/llo_aggregator.go deleted file mode 100644 index 0fec833a84..0000000000 --- a/pkg/capabilities/consensus/ocr3/datafeeds/llo_aggregator.go +++ /dev/null @@ -1,463 +0,0 @@ -package datafeeds - -import ( - "encoding/hex" - "errors" - "fmt" - "math/big" - "slices" - "strconv" - "strings" - "time" - - "google.golang.org/protobuf/proto" - - "github.com/shopspring/decimal" - - ocrcommon "github.com/smartcontractkit/libocr/commontypes" - - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/types" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/datastreams" - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-protos/cre/go/values" -) - -var ( - ErrInvalidConfig = errors.New("invalid config") - ErrInsufficientConsensus = errors.New("insufficient consensus") - ErrEmptyObservation = errors.New("empty observation") -) - -// LLOAggregatorConfig is the config for the LLO aggregator. -// Example config: -// remappedID but a hex string -// streams: -// -// "1": -// deviation: "0.1" -// heartbeat: 10 -// remappedID: "0x680084f7347baFfb5C323c2982dfC90e04F9F918" -// "2": -// deviation: "0.2" -// heartbeat: 20 -// -// allowedPartialStaleness: "0.2" -// The streams are the stream IDs that the aggregator will aggregate. -type LLOAggregatorConfig struct { - // workaround for the fact that mapstructure doesn't support uint32 keys - //streams map[uint32]feedConfig `mapstructure:"-"` - Streams map[string]FeedConfig `mapstructure:"streams"` - // allowedPartialStaleness is an optional optimization that tries to maximize batching. - // Once any deviation or heartbeat threshold hits, we will include all other feeds that are - // within the allowedPartialStaleness range of their own heartbeat. - // For example, setting 0.2 will include all feeds that are within 20% of their heartbeat. - //allowedPartialStaleness float64 `mapstructure:"-"` - // workaround for the fact that mapstructure doesn't support float64 keys - AllowedPartialStaleness string `mapstructure:"allowedPartialStaleness"` -} - -// ToMap converts the LLOAggregatorConfig to a values.Map, which is suitable for the -// [NewAggegator] function in the OCR3 Aggregator interface. -func (c LLOAggregatorConfig) ToMap() (*values.Map, error) { - v, err := values.WrapMap(c) - if err != nil { - // this should never happen since we are wrapping a struct - return &values.Map{}, fmt.Errorf("failed to wrap LLOAggregatorConfig: %w", err) - } - return v, nil -} - -func NewLLOConfig(m values.Map) (LLOAggregatorConfig, error) { - // Create a default LLOAggregatorConfig - config := LLOAggregatorConfig{ - Streams: make(map[string]FeedConfig), - } - if err := m.UnwrapTo(&config); err != nil { - return LLOAggregatorConfig{}, fmt.Errorf("failed to unwrap values.Map to LLOAggregatorConfig: %w", err) - } - - return config, nil -} - -func (c LLOAggregatorConfig) convertToInternal() (parsedLLOAggregatorConfig, error) { - parsedConfig := parsedLLOAggregatorConfig{ - streams: make(map[uint32]FeedConfig), - } - cfgErr := func(err error) error { - cfgErr := fmt.Errorf("llo aggregator config: %w", ErrInvalidConfig) - return errors.Join(cfgErr, err) - } - for s, cfg := range c.Streams { - id, err := strconv.ParseUint(s, 10, 32) - if err != nil { - // this should never happen since we are using a mapstructure-compatible config - return parsedConfig, cfgErr(fmt.Errorf("cannot parse stream ID %s: %w", s, err)) - } - id32 := uint32(id) //nolint:gosec // G115 - parsedConfig.streams[id32] = cfg - } - // TODO some copy-pasta from feeds_aggregator.go - maybe reuse the same code? - for streamID, cfg := range parsedConfig.streams { - if cfg.RemappedIDHex == "" { - return parsedConfig, cfgErr(fmt.Errorf("remappedID is required for stream %d", streamID)) - } - if cfg.Deviation != "" { - dec, err := decimal.NewFromString(cfg.Deviation) - if err != nil { - return parsedConfig, cfgErr(fmt.Errorf("cannot parse deviation config for feed %d: %w", streamID, err)) - } - cfg.parsedDeviation = dec - parsedConfig.streams[streamID] = cfg - } - trimmed := strings.TrimPrefix(cfg.RemappedIDHex, "0x") - rawRemappedID, err := hex.DecodeString(trimmed) - if err != nil { - return parsedConfig, cfgErr(fmt.Errorf("cannot parse remappedId config for feed %d: %w", streamID, err)) - } - cfg.remappedID = rawRemappedID - parsedConfig.streams[streamID] = cfg - } - // convert allowedPartialStaleness from string to float64 - if c.AllowedPartialStaleness != "" { - allowedPartialStaleness, err := decimal.NewFromString(c.AllowedPartialStaleness) - if err != nil { - return parsedConfig, cfgErr(fmt.Errorf("cannot parse allowedPartialStaleness: %w", err)) - } - parsedConfig.allowedPartialStaleness = allowedPartialStaleness.InexactFloat64() - } - return parsedConfig, nil -} - -// parsedLLOAggregatorConfig is the internal representation of the LLO aggregator config. -// the separation is because mapstructure only supports string keys. -// the are exposed in LLOAggregatorConfig for the config which is then processed into this internal representation. -type parsedLLOAggregatorConfig struct { - streams map[uint32]FeedConfig - allowedPartialStaleness float64 -} - -var _ types.Aggregator = (*LLOAggregator)(nil) - -type LLOAggregator struct { - config parsedLLOAggregatorConfig -} - -// NewLLOAggregator creates a new LLOAggregator instance based on the provided configuration. -// The config should be a [values.Map] that has represents from the [LLOAggregatorConfig]. See [LLOAggreagatorConfig.ToMap] -func NewLLOAggregator(config values.Map) (types.Aggregator, error) { - parsedConfig, err := parseLLOConfig(config) - if err != nil { - return nil, fmt.Errorf("failed to parse config (%+v): %w", config, err) - } - return &LLOAggregator{ - config: parsedConfig, - }, nil -} - -// Aggregate implements the Aggregator interface -// For this implementation, we expect the LLO events to be the same across all nodes. -// And we expect the every observation only contains a single LLO event, ie len(observations.["some-oracle-id"]) == 1. -func (a *LLOAggregator) Aggregate(lggr logger.Logger, previousOutcome *types.AggregationOutcome, observations map[ocrcommon.OracleID][]values.Value, f int) (*types.AggregationOutcome, error) { - lggr = logger.Named(lggr, "LLOAggregator") - if len(observations) == 0 { - return nil, ErrEmptyObservation - } - lloEvents := a.extractLLOEvents(lggr, observations) - if len(lloEvents) != len(observations) { - lggr.Warnw("missing LLO events", "nNodes", len(observations), "nEvents", len(lloEvents)) - } - currentState, err := a.initializeLLOState(lggr, previousOutcome) - if err != nil { - return nil, fmt.Errorf("failed to initialize llo state: %w", err) - } - - allStreamIDs := []uint32{} - for streamID := range currentState.StreamInfo { - allStreamIDs = append(allStreamIDs, streamID) - } - slices.Sort(allStreamIDs) - lggr.Debugw("determined streams to aggregate", "nStreamIds", len(allStreamIDs)) - - observationTimestamp, prices, err := lloStreamPrices(lggr, allStreamIDs, lloEvents, f) - if err != nil { - return nil, fmt.Errorf("failed to get latest prices: %w", err) - } - lggr = logger.With(lggr, "observationTimestamp", observationTimestamp) - - mustUpdateIDs := []uint32{} // streamIDs that need to be updated per deviation or heartbeat - maybeUpdateIDs := []uint32{} // streamIDs that are within AllowedPartialStaleness percentage of their heartbeat - for _, streamID := range allStreamIDs { - previousStreamInfo := currentState.StreamInfo[streamID] - config := a.config.streams[streamID] - oldPrice := new(decimal.Decimal) - if uerr := oldPrice.UnmarshalBinary(previousStreamInfo.Price); uerr != nil { - lggr.Errorw("failed to unmarshal previous price", "streamID", streamID, "err", uerr) - continue - } - - // If we don't have a price for this stream, we don't include it in the report. - // This is to prevent zeroing out onchain values due to a bad LLO Stream Job/observation. - newPrice, ok := prices[streamID] - if !ok { - lggr.Debugw("skipping over hb and deviation checks due to missing price update", "streamID", streamID) - } else { - priceDeviation := deviationDecimal(*oldPrice, newPrice) - timeDiffNs := observationTimestamp.UnixNano() - previousStreamInfo.Timestamp - lggr.Debugw("checking deviation and heartbeat", - "streamID", streamID, - "observationNs", observationTimestamp, - "perviousNs", previousStreamInfo.Timestamp, - "currStalenessNs", timeDiffNs, - "heartbeatSec", config.Heartbeat, - "oldPrice", oldPrice, - "newPrice", newPrice, - "currDeviation", priceDeviation, - "deviation", config.DeviationAsDecimal().InexactFloat64(), - ) - if timeDiffNs > config.HeartbeatNanos() || - priceDeviation > config.DeviationAsDecimal().InexactFloat64() { - // this stream needs an update - previousStreamInfo.Timestamp = observationTimestamp.UnixNano() - var err2 error - previousStreamInfo.Price, err2 = prices[streamID].MarshalBinary() - if err2 != nil { - lggr.Errorw("failed to marshal price", "streamID", streamID, "err", err2) - continue - } - mustUpdateIDs = append(mustUpdateIDs, streamID) - } else if float64(timeDiffNs) > float64(config.HeartbeatNanos())*(1.0-a.config.allowedPartialStaleness) { - maybeUpdateIDs = append(maybeUpdateIDs, streamID) - } - } - } - - // optimization that allows for more efficient batching - // if there is at least one stream that actually hit its deviation or heartbeat threshold, - // append all others that were within AllowedPartialStaleness percentage of their heartbeat - if len(mustUpdateIDs) > 0 { - mustUpdateIDs = append(mustUpdateIDs, maybeUpdateIDs...) - // deterministic order - slices.Sort(mustUpdateIDs) - } - - marshalledState, err := proto.MarshalOptions{Deterministic: true}.Marshal(currentState) - if err != nil { - return nil, fmt.Errorf("failed to marshal current state: %w", err) - } - - toWrap := make([]*EVMEncodableStreamUpdate, 0, len(mustUpdateIDs)) - for _, streamID := range mustUpdateIDs { - remappedID := a.config.streams[streamID].RemappedID() - newPrice := prices[streamID] - w := &EVMEncodableStreamUpdate{ - StreamID: streamID, - Price: decimalToBigInt(newPrice), - Timestamp: uint32(observationTimestamp.Unix()), //nolint:gosec // G115 - RemappedID: remappedID, - } - toWrap = append(toWrap, w) - } - - wrappedReportsNeedingUpdates, err := values.NewMap(map[string]any{ - TopLevelListOutputFieldName: toWrap, - }) - if err != nil { - return nil, err - } - reportsProto := values.Proto(wrappedReportsNeedingUpdates) - - lggr.Debugw("Aggregate complete", "nStreamsNeedingUpdate", len(mustUpdateIDs)) - return &types.AggregationOutcome{ - EncodableOutcome: reportsProto.GetMapValue(), - Metadata: marshalledState, - ShouldReport: len(mustUpdateIDs) > 0, - }, nil -} - -// EVMEncodableStreamUpdate is the EVM encodable representation of a stream update. -// The field name must match the field name in the EVM encoder, and must be a valid EVMEncoderKey. -type EVMEncodableStreamUpdate struct { - StreamID uint32 - Price *big.Int - Timestamp uint32 // unix timestamp in seconds - RemappedID []byte -} - -func decimalToBigInt(d decimal.Decimal) *big.Int { - return d.BigInt() -} - -// extractLLOEvents decodes the untyped wire format into LLOStreamsTriggerEvent. -// every observation ios expected to be len 1, a single wrapped LLOStreamsTriggerEvent. -func (a *LLOAggregator) extractLLOEvents(lggr logger.Logger, observations map[ocrcommon.OracleID][]values.Value) map[ocrcommon.OracleID]*datastreams.LLOStreamsTriggerEvent { - events := make(map[ocrcommon.OracleID]*datastreams.LLOStreamsTriggerEvent) - for nodeID, nodeObservations := range observations { - lggr = logger.With(lggr, "nodeID", nodeID) - // do not error on unexected number of observations - if len(nodeObservations) == 0 || nodeObservations[0] == nil { - lggr.Warn("empty observations") - continue - } - if len(nodeObservations) > 1 { - lggr.Warn("more than one observation") - continue - } - triggerEvent := &datastreams.LLOStreamsTriggerEvent{} - if err := nodeObservations[0].UnwrapTo(triggerEvent); err != nil { - lggr.Warnw("could not parse observations", "err", err) - continue - } - payload := make([]*datastreams.LLOStreamDecimal, 0, len(triggerEvent.Payload)) - seenStreamIDs := make(map[uint32]struct{}) - for _, p := range triggerEvent.Payload { - if _, ok := seenStreamIDs[p.StreamID]; ok { - lggr.Warnw("duplicate streamID in observation", "streamID", p.StreamID) - continue - } - seenStreamIDs[p.StreamID] = struct{}{} - payload = append(payload, p) - } - triggerEvent.Payload = payload - events[nodeID] = triggerEvent - } - return events -} - -// AggregationOutcome.Metadata is used to store extra data that is passed between OCR rounds as part of previous outcome. -// For LLO aggregator, that data is a serialized LLOOutcomeMetadata proto. -// This helper initializes current state by adjusting previous state with current config (adding missing streams, removing obsolete ones). -func (a *LLOAggregator) initializeLLOState(lggr logger.Logger, previousOutcome *types.AggregationOutcome) (*LLOOutcomeMetadata, error) { - currentState := &LLOOutcomeMetadata{ - StreamInfo: make(map[uint32]*LLOStreamInfo), - } - if previousOutcome != nil && len(previousOutcome.Metadata) != 0 { - err := proto.Unmarshal(previousOutcome.Metadata, currentState) - if err != nil { - return nil, err - } - } - - zero, err := decimal.Zero.MarshalBinary() - if err != nil { - return nil, fmt.Errorf("failed to marshal zero: %w", err) - } - for streamID := range a.config.streams { - if _, ok := currentState.StreamInfo[streamID]; !ok { - currentState.StreamInfo[streamID] = &LLOStreamInfo{ - Timestamp: 0, // trigger an update for every realistic heartbeat value - Price: zero, - } - lggr.Debugw("initializing empty stream info", "streamID", streamID) - } - } - // remove obsolete streams from state - for streamID := range currentState.StreamInfo { - if _, ok := a.config.streams[streamID]; !ok { - delete(currentState.StreamInfo, streamID) - lggr.Debugw("removed obsolete stream", "streamID", streamID) - } - } - return currentState, nil -} - -// getObservationTimestamp returns the observation timestamp that appears at least f+1 times in the LLO events. -// it is optimistic and takes the first one that appears at least f+1 times. this is valid be we know that LLO events are coming from an OCR consensus output. -// ErrInsufficientConsensus is returned if no timestamp appears at least f+1 times. -func getObservationTimestamp(lloEvents map[ocrcommon.OracleID]*datastreams.LLOStreamsTriggerEvent, f int) (time.Time, error) { - // All honest nodes are expected to include the same streams trigger event in their observation. - // We can trust the timestamp that appears at least f+1 times. - counts := make(map[uint64]int) - for _, event := range lloEvents { - counts[event.ObservationTimestampNanoseconds]++ - if counts[event.ObservationTimestampNanoseconds] >= f+1 { - return time.Unix(0, int64(event.ObservationTimestampNanoseconds)), nil //nolint:gosec // G115 - } - } - return time.Time{}, fmt.Errorf("%w: no timestamp appeared at least %d times", ErrInsufficientConsensus, f+1) -} - -// lloStreamPrices returns the prices for the streams at the consensus observation timestamp. -// it ignores any events that are not from the consensus observation timestamp. -func lloStreamPrices(lggr logger.Logger, wantStreamIDs []uint32, lloEvents map[ocrcommon.OracleID]*datastreams.LLOStreamsTriggerEvent, f int) (observationTimestamp time.Time, out map[uint32]decimal.Decimal, err error) { - // All honest nodes are expected to include the same streams trigger event in their observation. - // We can trust any price that appears at least f+1 times. - - out = make(map[uint32]decimal.Decimal) - - // Create a filter for the stream IDs we are interested in and initialize the candidate prices from which the output will be selected - idFilter := make(map[uint32]struct{}) - candidatePrices := make(map[uint32]map[string]int) // streamID -> price -> count; string for price to avoid using decimal.Decimal as a map key - for _, streamID := range wantStreamIDs { - idFilter[streamID] = struct{}{} - candidatePrices[streamID] = make(map[string]int) - } - - // count all the prices across all events for the stream IDs we are interested in - observationTimestamp, err = getObservationTimestamp(lloEvents, f) - if err != nil { - return time.Time{}, nil, err - } - observationTimestampNS := uint64(observationTimestamp.UnixNano()) //nolint:gosec // G115 - for _, event := range lloEvents { - if event.ObservationTimestampNanoseconds != observationTimestampNS { - // Ignore events with different timestamps. This shouldn't happen unless there are malicious nodes - lggr.Warnw("observation timestamp mismatch", "expected", observationTimestamp, "actual", event.ObservationTimestampNanoseconds) - continue - } - // Check if the event contains the stream ID we are interested in - for _, p := range event.Payload { - if _, ok := idFilter[p.StreamID]; !ok { - continue - } - // Convert the binary representation to decimal.Decimal - price := new(decimal.Decimal) - if err := price.UnmarshalBinary(p.Decimal); err != nil { - lggr.Errorw("failed to unmarshal decimal", "streamID", p.StreamID, "err", err) - continue - } - // string key b/c decimal.Decimal is not comparable - candidatePrices[p.StreamID][price.String()]++ - } - } - - // find the price that appears at least f+1 times for each stream ID in the candidate prices - for streamID, priceCount := range candidatePrices { - found := false - for priceStr, count := range priceCount { - if count >= f+1 { - // Convert the string back to decimal.Decimal - price, err := decimal.NewFromString(priceStr) - if err != nil { - // this shouldn't happen since we just created the string from a decimal.Decimal - lggr.Errorw("failed to parse price", "streamID", streamID, "priceStr", priceStr, "err", err) - } - out[streamID] = price - found = true - break - } - } - if !found { - lggr.Warnw("no price found in candidates with quorum", "streamID", streamID, "f", f, "candidates", priceCount, "err", ErrInsufficientConsensus) - } - } - if len(out) != len(wantStreamIDs) { - lggr.Warnw("not all streams have prices", "wantStreamIDs", len(wantStreamIDs), "out", len(out)) - } - - return observationTimestamp, out, nil -} - -// parseLLOConfig parses the user-facing, type-less, LLO aggregator in the internal typed config. -func parseLLOConfig(config values.Map) (parsedLLOAggregatorConfig, error) { - converter := LLOAggregatorConfig{ - Streams: make(map[string]FeedConfig), - } - if err := config.UnwrapTo(&converter); err != nil { - return parsedLLOAggregatorConfig{}, err - } - x, err := converter.convertToInternal() - if err != nil { - return parsedLLOAggregatorConfig{}, err - } - return x, nil -} diff --git a/pkg/capabilities/consensus/ocr3/datafeeds/llo_aggregator_test.go b/pkg/capabilities/consensus/ocr3/datafeeds/llo_aggregator_test.go deleted file mode 100644 index 3f840fb8ac..0000000000 --- a/pkg/capabilities/consensus/ocr3/datafeeds/llo_aggregator_test.go +++ /dev/null @@ -1,722 +0,0 @@ -package datafeeds_test - -import ( - "encoding/hex" - "fmt" - "sort" - "testing" - "time" - - "github.com/shopspring/decimal" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" - - ocrcommon "github.com/smartcontractkit/libocr/commontypes" - - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/datafeeds" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/types" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/datastreams" - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-protos/cre/go/values" -) - -func TestGetLatestPrices(t *testing.T) { - t.Parallel() - lggr := logger.Test(t) - - testTime := time.Unix(3164233, 0) - - tests := []struct { - name string - streamIDs []uint32 - events map[ocrcommon.OracleID]*datastreams.LLOStreamsTriggerEvent - f int - expectedTimestamp time.Time - expectedPrices map[uint32]decimal.Decimal - expectError bool - }{ - { - name: "successful price consensus", - streamIDs: []uint32{1, 2}, - events: map[ocrcommon.OracleID]*datastreams.LLOStreamsTriggerEvent{ - 1: createLLOEvent(t, testTime, map[uint32]decimal.Decimal{ - 1: decimal.NewFromFloat(100.5), - 2: decimal.NewFromFloat(200.5), - }), - 2: createLLOEvent(t, testTime, map[uint32]decimal.Decimal{ - 1: decimal.NewFromFloat(100.5), - 2: decimal.NewFromFloat(200.5), - }), - 3: createLLOEvent(t, testTime, map[uint32]decimal.Decimal{ - 1: decimal.NewFromFloat(100.5), - 2: decimal.NewFromFloat(200.5), - }), - }, - f: 1, - expectedTimestamp: testTime, - expectedPrices: map[uint32]decimal.Decimal{ - 1: decimal.NewFromFloat(100.5), - 2: decimal.NewFromFloat(200.5), - }, - expectError: false, - }, - { - name: "insufficient price consensus", - streamIDs: []uint32{1, 2}, - events: map[ocrcommon.OracleID]*datastreams.LLOStreamsTriggerEvent{ - 1: createLLOEvent(t, testTime, map[uint32]decimal.Decimal{ - 1: decimal.NewFromFloat(100.5), - 2: decimal.NewFromFloat(200.5), - }), - 2: createLLOEvent(t, testTime, map[uint32]decimal.Decimal{ - 1: decimal.NewFromFloat(101.5), // Different value - 2: decimal.NewFromFloat(201.5), // Different value - }), - 3: createLLOEvent(t, testTime, map[uint32]decimal.Decimal{ - 1: decimal.NewFromFloat(102.5), // Different value - 2: decimal.NewFromFloat(202.5), // Different value - }), - }, - f: 1, - expectedTimestamp: testTime, - expectedPrices: map[uint32]decimal.Decimal{}, // No consensus - expectError: false, - }, - { - name: "mixed consensus", - streamIDs: []uint32{1, 2}, - events: map[ocrcommon.OracleID]*datastreams.LLOStreamsTriggerEvent{ - 1: createLLOEvent(t, testTime, map[uint32]decimal.Decimal{ - 1: decimal.NewFromFloat(100.5), - 2: decimal.NewFromFloat(200.5), - }), - 2: createLLOEvent(t, testTime, map[uint32]decimal.Decimal{ - 1: decimal.NewFromFloat(100.5), - 2: decimal.NewFromFloat(201.5), // Different value - }), - 3: createLLOEvent(t, testTime, map[uint32]decimal.Decimal{ - 1: decimal.NewFromFloat(100.5), - 2: decimal.NewFromFloat(202.5), // Different value - }), - }, - f: 1, - expectedTimestamp: testTime, - expectedPrices: map[uint32]decimal.Decimal{ - 1: decimal.NewFromFloat(100.5), // Consensus for stream 1 - // No consensus for stream 2 - }, - expectError: false, - }, - { - name: "no timestamp consensus", - streamIDs: []uint32{1, 2}, - events: map[ocrcommon.OracleID]*datastreams.LLOStreamsTriggerEvent{ - 1: createLLOEvent(t, testTime, map[uint32]decimal.Decimal{ - 1: decimal.NewFromFloat(100.5), - 2: decimal.NewFromFloat(200.5), - }), - 2: createLLOEvent(t, testTime.Add(time.Second), map[uint32]decimal.Decimal{ - 1: decimal.NewFromFloat(100.5), - 2: decimal.NewFromFloat(200.5), - }), - 3: createLLOEvent(t, testTime.Add(2*time.Second), map[uint32]decimal.Decimal{ - 1: decimal.NewFromFloat(100.5), - 2: decimal.NewFromFloat(200.5), - }), - }, - f: 1, - expectError: true, // No timestamp consensus - }, - { - name: "empty event list", - streamIDs: []uint32{1, 2}, - events: map[ocrcommon.OracleID]*datastreams.LLOStreamsTriggerEvent{}, - f: 1, - expectError: true, // No events to check - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - ts, prices, err := datafeeds.LLOStreamPrices(lggr, tc.streamIDs, tc.events, tc.f) - - if tc.expectError { - assert.Error(t, err) - return - } - - require.NoError(t, err) - assert.Equal(t, tc.expectedTimestamp, ts) - - // Check all expected prices - for streamID, expectedPrice := range tc.expectedPrices { - actualPrice, exists := prices[streamID] - assert.True(t, exists, "Expected price for stream %d not found", streamID) - assert.True(t, expectedPrice.Equal(actualPrice), - "Expected price %s for stream %d, got %s", - expectedPrice.String(), streamID, actualPrice.String()) - } - - // Ensure no extra prices - assert.Len(t, prices, len(tc.expectedPrices), "Unexpected number of prices") - }) - } -} - -func TestLLOAggregator_Aggregate(t *testing.T) { - lggr := logger.Test(t) - - testStartTime := time.Now() - remappedHex1 := "0x680084f7347baFfb5C323c2982dfC90e04F9F918" - - remapped1, err := hex.DecodeString(remappedHex1[2:]) - require.NoError(t, err) - remappedHex2 := "0x00001237347baFfb5C323c1112dfC90e0789FFFF" - remapped2, err := hex.DecodeString(remappedHex2[2:]) - require.NoError(t, err) - remappedHex3 := "0xaaaa59b7347baFfb5C323c1112dfC90e0789FEDC" - - tests := []struct { - name string - config datafeeds.LLOAggregatorConfig - previousOutcome *types.AggregationOutcome - observations map[ocrcommon.OracleID][]values.Value - f int - expectedShouldReport bool - expectedStreamIDs []uint32 - expectError bool - wantUpdates []*datafeeds.EVMEncodableStreamUpdate - }{ - - { - name: "update due to no previous outcome", - config: datafeeds.LLOAggregatorConfig{ - Streams: map[string]datafeeds.FeedConfig{ - "1": datafeeds.FeedConfig{ - Deviation: decimal.NewFromFloat(0.01).String(), // 1% - Heartbeat: 3600, // 1 hour - RemappedIDHex: remappedHex1, - }, - }, - }, - observations: createObservations(t, testStartTime, map[uint32]decimal.Decimal{ //nolint: gosec // G115 - 1: decimal.NewFromFloat(102.123), // 2% change, exceeds 1% threshold - }), - f: 1, - expectedShouldReport: true, - expectedStreamIDs: []uint32{1}, - wantUpdates: []*datafeeds.EVMEncodableStreamUpdate{ - { - StreamID: 1, - Price: datafeeds.DecimalToBigInt(decimal.NewFromFloat(102.123)), - Timestamp: uint32(testStartTime.Unix()), //nolint: gosec // G115 - RemappedID: remapped1, - }, - }, - - expectError: false, - }, - - { - name: "update due to deviation", - config: datafeeds.LLOAggregatorConfig{ - Streams: map[string]datafeeds.FeedConfig{ - "1": { - Deviation: decimal.NewFromFloat(0.01).String(), // 1% - Heartbeat: 3600, // 1 hour - RemappedIDHex: remappedHex1, - }, - }, - }, - previousOutcome: createPreviousOutcome(t, map[uint32]struct { - price decimal.Decimal - timestamp int64 - }{ - 1: { - price: decimal.NewFromFloat(100), - timestamp: testStartTime.Add(-10 * time.Minute).UnixNano(), - }, - }), - observations: createObservations(t, testStartTime, map[uint32]decimal.Decimal{ //nolint: gosec // G115 - 1: decimal.NewFromFloat(102.00000000001), // 2% change, exceeds 1% threshold - }), - f: 1, - expectedShouldReport: true, - expectedStreamIDs: []uint32{1}, - wantUpdates: []*datafeeds.EVMEncodableStreamUpdate{ - { - StreamID: 1, - Price: datafeeds.DecimalToBigInt(decimal.NewFromFloat(102.00000000001)), - Timestamp: uint32(testStartTime.Unix()), //nolint: gosec // G115 - RemappedID: remapped1, - }, - }, - - expectError: false, - }, - - { - name: "update due to heartbeat", - config: datafeeds.LLOAggregatorConfig{ - Streams: map[string]datafeeds.FeedConfig{ - "1": { - Deviation: decimal.NewFromFloat(0.1).String(), // 10% - Heartbeat: 300, // 5 min - RemappedIDHex: remappedHex1, - }, - }, - }, - previousOutcome: createPreviousOutcome(t, map[uint32]struct { - price decimal.Decimal - timestamp int64 - }{ - 1: { - price: decimal.NewFromFloat(100), - timestamp: testStartTime.Add(-6 * time.Minute).UnixNano(), // Over heartbeat - }, - }), - observations: createObservations(t, testStartTime, map[uint32]decimal.Decimal{ //nolint: gosec // G115 - 1: decimal.NewFromFloat(101), // 1% change, under 10% threshold - }), - f: 1, - expectedShouldReport: true, - expectedStreamIDs: []uint32{1}, - wantUpdates: []*datafeeds.EVMEncodableStreamUpdate{ - { - StreamID: 1, - Price: datafeeds.DecimalToBigInt(decimal.NewFromFloat(101)), - Timestamp: uint32(testStartTime.Unix()), //nolint: gosec // G115 - RemappedID: remapped1, - }, - }, - expectError: false, - }, - - { - name: "no update needed", - config: datafeeds.LLOAggregatorConfig{ - Streams: map[string]datafeeds.FeedConfig{ - "1": { - Deviation: decimal.NewFromFloat(0.1).String(), // 10% - Heartbeat: 3600, // 1 hour - RemappedIDHex: remappedHex1, - }, - }, - }, - previousOutcome: createPreviousOutcome(t, map[uint32]struct { - price decimal.Decimal - timestamp int64 - }{ - 1: { - price: decimal.NewFromInt(100), - timestamp: time.Now().Add(-30 * time.Minute).UnixNano(), // Under heartbeat - }, - }), - observations: createObservations(t, time.Now(), map[uint32]decimal.Decimal{ //nolint: gosec // G115 - 1: decimal.NewFromInt(105), // 5% change, under 10% threshold - }), - f: 1, - expectedShouldReport: false, // No update needed - expectedStreamIDs: []uint32{}, - expectError: false, - }, - - { - name: "partial staleness optimization", - config: datafeeds.LLOAggregatorConfig{ - Streams: map[string]datafeeds.FeedConfig{ - "1": { - Deviation: decimal.NewFromFloat(0.1).String(), // 10% - Heartbeat: 3600, // 1 hour - RemappedIDHex: remappedHex1, - }, - "2": { - Deviation: decimal.NewFromFloat(0.1).String(), // 10% - Heartbeat: 300, // 5 min - RemappedIDHex: remappedHex2, - }, - "3": { - Deviation: decimal.NewFromFloat(0.1).String(), // 10% - Heartbeat: 300, // 5 min - RemappedIDHex: remappedHex3, - }, - }, - AllowedPartialStaleness: "0.2", // 20% allowed partial staleness - }, - previousOutcome: createPreviousOutcome(t, map[uint32]struct { - price decimal.Decimal - timestamp int64 - }{ - 1: { - price: decimal.NewFromFloat(100), - timestamp: testStartTime.Add(-50 * time.Minute).UnixNano(), // 83% of heartbeat, within 20% staleness - }, - 2: { - price: decimal.NewFromFloat(200), - timestamp: testStartTime.Add(-6 * time.Minute).UnixNano(), // Over heartbeat - }, - 3: { - price: decimal.NewFromFloat(200), - timestamp: testStartTime.Add(-1 * time.Minute).UnixNano(), // Under heartbeat, outside optimization - }, - }), - observations: createObservations(t, testStartTime, map[uint32]decimal.Decimal{ //nolint: gosec // G115 - 1: decimal.NewFromFloat(105), // 5% change, under 10% threshold - 2: decimal.NewFromFloat(202), // 1% change, under 10% threshold - 3: decimal.NewFromFloat(205), // 2.5% change, under 10% threshold - }), - f: 1, - expectedShouldReport: true, - expectedStreamIDs: []uint32{1, 2}, // Both update due to optimization - wantUpdates: []*datafeeds.EVMEncodableStreamUpdate{ - { - StreamID: 1, - Price: datafeeds.DecimalToBigInt(decimal.NewFromFloat(105)), //big.NewInt(105), - Timestamp: uint32(testStartTime.Unix()), //nolint: gosec // G115 - RemappedID: remapped1, - }, - { - StreamID: 2, - Price: datafeeds.DecimalToBigInt(decimal.NewFromFloat(202)), //big.NewInt(202), - Timestamp: uint32(testStartTime.Unix()), //nolint: gosec // G115 - RemappedID: remapped2, - }, - }, - - expectError: false, - }, - - { - name: "empty observations", - config: datafeeds.LLOAggregatorConfig{ - Streams: map[string]datafeeds.FeedConfig{ - "1": { - Deviation: decimal.NewFromFloat(0.1).String(), // 10% - Heartbeat: 3600, // 1 hour - RemappedIDHex: remappedHex1, - }, - }, - }, - previousOutcome: createPreviousOutcome(t, map[uint32]struct { - price decimal.Decimal - timestamp int64 - }{}), - - observations: map[ocrcommon.OracleID][]values.Value{}, - f: 1, - expectedShouldReport: false, - expectError: true, // Should error with empty observations and no previous outcome - }, - - { - name: "previous outcome, empty observation, no update due to heartbeat", - config: datafeeds.LLOAggregatorConfig{ - Streams: map[string]datafeeds.FeedConfig{ - "1": { - Deviation: decimal.NewFromFloat(0.1).String(), // 10% - Heartbeat: 3600, // 1 hour - RemappedIDHex: remappedHex1, - }, - "2": { - Deviation: decimal.NewFromFloat(0.1).String(), // 10% - Heartbeat: 3600, // 1 hour - RemappedIDHex: remappedHex2, - }, - }, - }, - previousOutcome: createPreviousOutcome(t, map[uint32]struct { - price decimal.Decimal - timestamp int64 - }{ - 1: { - price: decimal.NewFromFloat(100), - timestamp: testStartTime.Add(-61 * time.Minute).UnixNano(), // Over 1 the hour heartbeat - }, - 2: { - price: decimal.NewFromFloat(101), - timestamp: testStartTime.Add(-10 * time.Minute).UnixNano(), // 10 minutes ago, under the 1 hour heartbeat - }, - }), - - observations: createObservations(t, testStartTime, map[uint32]decimal.Decimal{ //nolint: gosec // G115 - 2: decimal.NewFromFloat(101), // no change - }), - f: 1, - expectedShouldReport: false, // No update despite heartbeat, as no observation for stream 1 - expectedStreamIDs: []uint32{}, - }, - - { - name: "previous outcome, empty observation, no update before heartbeat", - config: datafeeds.LLOAggregatorConfig{ - Streams: map[string]datafeeds.FeedConfig{ - "1": { - Deviation: decimal.NewFromFloat(0.1).String(), // 10% - Heartbeat: 3600, // 1 hour - RemappedIDHex: remappedHex1, - }, - "2": { - Deviation: decimal.NewFromFloat(0.1).String(), // 10% - Heartbeat: 3600, // 1 hour - RemappedIDHex: remappedHex2, - }, - }, - }, - previousOutcome: createPreviousOutcome(t, map[uint32]struct { - price decimal.Decimal - timestamp int64 - }{ - 1: { - price: decimal.NewFromFloat(100), - timestamp: testStartTime.Add(-10 * time.Minute).UnixNano(), // 10 minutes ago, under the 1 hour heartbeat - }, - 2: { - price: decimal.NewFromFloat(101), - timestamp: testStartTime.Add(-10 * time.Minute).UnixNano(), // 10 minutes ago, under the 1 hour heartbeat - }, - }), - - observations: createObservations(t, testStartTime, map[uint32]decimal.Decimal{ //nolint: gosec // G115 - 2: decimal.NewFromFloat(101), // no change - }), - f: 1, - expectedShouldReport: false, - expectedStreamIDs: []uint32{}, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - cfgMap, err := tc.config.ToMap() - require.NoError(t, err, "Failed to convert config to values.Map") - aggregator, err := datafeeds.NewLLOAggregator(*cfgMap) - require.NoError(t, err) - - outcome, err := aggregator.Aggregate(lggr, tc.previousOutcome, tc.observations, tc.f) - - if tc.expectError { - assert.Error(t, err) - return - } - - require.NoError(t, err) - assert.Equal(t, tc.expectedShouldReport, outcome.ShouldReport) - - if outcome.ShouldReport { - // Verify that the correct streams were updated - reportedStreams, reports := extractUpdatedStreamIDs(t, outcome) - assert.ElementsMatch(t, tc.expectedStreamIDs, reportedStreams) - assert.Len(t, reports, len(tc.expectedStreamIDs)) - sort.Slice(reports, func(i, j int) bool { - return reports[i].StreamID < reports[j].StreamID - }) - sort.Slice(tc.wantUpdates, func(i, j int) bool { - return tc.wantUpdates[i].StreamID < tc.wantUpdates[j].StreamID - }) - for i, report := range reports { - assert.Equal(t, tc.wantUpdates[i].StreamID, report.StreamID) - assert.Equal(t, tc.wantUpdates[i].Price, report.Price) - assert.Equal(t, tc.wantUpdates[i].Timestamp, report.Timestamp) - } - } - }) - } -} - -func TestLLOAggregatorConfig_RoundTrip(t *testing.T) { - testCases := []struct { - name string - config datafeeds.LLOAggregatorConfig - }{ - { - name: "typical config with multiple streams", - config: datafeeds.LLOAggregatorConfig{ - Streams: map[string]datafeeds.FeedConfig{ - "1": { - RemappedIDHex: "0x680084f7347baFfb5C323c2982dfC90e04F9F918", - Deviation: "0.01", - Heartbeat: 3600, - }, - "2": { - RemappedIDHex: "0x00001237347baFfb5C323c1112dfC90e0789FFFF", - Deviation: "0.02", - Heartbeat: 1800, - }, - "42": { - RemappedIDHex: "0xF8D170535513B67Ce18aF7A45E9a1F1A93c0F9ac", - Deviation: "0.005", - Heartbeat: 7200, - }, - }, - AllowedPartialStaleness: "0.2", - }, - }, - { - name: "config with single stream", - config: datafeeds.LLOAggregatorConfig{ - Streams: map[string]datafeeds.FeedConfig{ - "1": { - RemappedIDHex: "0x680084f7347baFfb5C323c2982dfC90e04F9F918", - Deviation: "0.01", - Heartbeat: 3600, - }, - }, - AllowedPartialStaleness: "0.1", - }, - }, - { - name: "config with empty staleness", - config: datafeeds.LLOAggregatorConfig{ - Streams: map[string]datafeeds.FeedConfig{ - "1": { - RemappedIDHex: "0x680084f7347baFfb5C323c2982dfC90e04F9F918", - Deviation: "0.01", - Heartbeat: 3600, - }, - }, - AllowedPartialStaleness: "", - }, - }, - { - name: "config with empty streams", - config: datafeeds.LLOAggregatorConfig{ - Streams: map[string]datafeeds.FeedConfig{}, - AllowedPartialStaleness: "0.2", - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - // Step 1: Convert original config to values.Map - configMap, err := tc.config.ToMap() - require.NoError(t, err, "ToMap should not error") - require.NotNil(t, configMap, "ToMap should return non-nil map") - - // Step 2: Convert values.Map back to config - roundTrippedConfig, err := datafeeds.NewLLOConfig(*configMap) - require.NoError(t, err, "NewLLOConfig should not error") - - // Step 3: Compare original and round-tripped configs - // We need to compare each field specifically since the derived fields won't be compared correctly - assert.Len(t, roundTrippedConfig.Streams, len(tc.config.Streams), - "Number of streams should match") - - // Compare streams - for streamID, origFeed := range tc.config.Streams { - roundTrippedFeed, exists := roundTrippedConfig.Streams[streamID] - assert.True(t, exists, "Stream %s should exist in round-tripped config", streamID) - - if exists { - assert.Equal(t, origFeed.RemappedIDHex, roundTrippedFeed.RemappedIDHex, - "RemappedIDHex should match for stream %s", streamID) - assert.Equal(t, origFeed.Deviation, roundTrippedFeed.Deviation, - "DeviationString should match for stream %s", streamID) - assert.Equal(t, origFeed.Heartbeat, roundTrippedFeed.Heartbeat, - "Heartbeat should match for stream %s", streamID) - } - } - - // Compare staleness - assert.Equal(t, tc.config.AllowedPartialStaleness, roundTrippedConfig.AllowedPartialStaleness, - "AllowedPartialStalenessStr should match") - }) - } -} - -// Helper functions - -func createLLOEvent(t *testing.T, obs time.Time, prices map[uint32]decimal.Decimal) *datastreams.LLOStreamsTriggerEvent { - event := &datastreams.LLOStreamsTriggerEvent{ - ObservationTimestampNanoseconds: uint64(obs.UnixNano()), //nolint: gosec // G115 - Payload: make([]*datastreams.LLOStreamDecimal, 0, len(prices)), - } - - for streamID, price := range prices { - binary, err := price.MarshalBinary() - require.NoError(t, err) - - event.Payload = append(event.Payload, &datastreams.LLOStreamDecimal{ - StreamID: streamID, - Decimal: binary, - }) - } - - return event -} - -func createPreviousOutcome(t *testing.T, streams map[uint32]struct { - price decimal.Decimal - timestamp int64 // UnixNano -}) *types.AggregationOutcome { - state := &datafeeds.LLOOutcomeMetadata{ - StreamInfo: make(map[uint32]*datafeeds.LLOStreamInfo), - } - - for streamID, info := range streams { - priceBytes, err := info.price.MarshalBinary() - require.NoError(t, err) - - state.StreamInfo[streamID] = &datafeeds.LLOStreamInfo{ - Timestamp: info.timestamp, - Price: priceBytes, - } - } - - marshalledState, err := proto.Marshal(state) - require.NoError(t, err) - - return &types.AggregationOutcome{ - Metadata: marshalledState, - } -} - -func createObservations(t *testing.T, ts time.Time, prices map[uint32]decimal.Decimal) map[ocrcommon.OracleID][]values.Value { - observations := make(map[ocrcommon.OracleID][]values.Value) - - // Create three observations with identical data to ensure f+1 consensus - for i := ocrcommon.OracleID(1); i <= 3; i++ { - event := createLLOEvent(t, ts, prices) - - val, err := values.Wrap(event) - require.NoError(t, err) - - observations[i] = []values.Value{val} - } - - return observations -} - -func extractUpdatedStreamIDs(t *testing.T, outcome *types.AggregationOutcome) ([]uint32, []*datafeeds.EVMEncodableStreamUpdate) { - streamIDs, reports, err := processOutcome(outcome) - require.NoError(t, err) - - return streamIDs, reports -} - -func processOutcome(outcome *types.AggregationOutcome) ([]uint32, []*datafeeds.EVMEncodableStreamUpdate, error) { - // TODOD here add the decoder of the slice - decodedMap, err := values.FromMapValueProto(outcome.EncodableOutcome) - if err != nil { - return nil, nil, err - } - - reportsAny, ok := decodedMap.Underlying[datafeeds.TopLevelListOutputFieldName] - if !ok { - return nil, nil, fmt.Errorf("missing field %s", datafeeds.TopLevelListOutputFieldName) - } - - var reportsList []*datafeeds.EVMEncodableStreamUpdate // each element is a WrappableUpdate - err = reportsAny.UnwrapTo(&reportsList) - if err != nil { - return nil, nil, err - } - - streamIDs := make([]uint32, 0, len(reportsList)) - for _, reportAny := range reportsList { - streamIDs = append(streamIDs, reportAny.StreamID) - } - - return streamIDs, reportsList, nil -} diff --git a/pkg/capabilities/consensus/ocr3/datafeeds/securemint_aggregator.go b/pkg/capabilities/consensus/ocr3/datafeeds/securemint_aggregator.go deleted file mode 100644 index 69a99179ab..0000000000 --- a/pkg/capabilities/consensus/ocr3/datafeeds/securemint_aggregator.go +++ /dev/null @@ -1,497 +0,0 @@ -package datafeeds - -import ( - "crypto/sha256" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "math/big" - "strconv" - "strings" - - chainselectors "github.com/smartcontractkit/chain-selectors" - ocrcommon "github.com/smartcontractkit/libocr/commontypes" - ocr2types "github.com/smartcontractkit/libocr/offchainreporting2/types" - ocr3types "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" - - "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/types" - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/types/chains/solana" - "github.com/smartcontractkit/chainlink-protos/cre/go/values" -) - -type SolanaEncoderKey = string - -const ( - /* - OutputFormat for solana: - "account_context_hash": <"hash">, - "payload": []reports{timestamp uint32, answer *big.Int, dataId [16]byte } - Solana encoder compatible idl config: - encoderConfig := map[string]any{ - report_schema": `{ - "kind": "struct", - "fields": [ - { "name": "payload", "type": { "vec": { "defined": "DecimalReport" } } } - ] - }`, - "defined_types": `[ - { - "name":"DecimalReport", - "type":{ - "kind":"struct", - "fields":[ - { "name":"timestamp", "type":"u32" }, - { "name":"answer", "type":"u128" }, - { "name": "dataId", "type": {"array": ["u8",16]}} - ] - } - } - ]`, - } - - */ - TopLevelPayloadListFieldName = SolanaEncoderKey("payload") - TopLevelAccountCtxHashFieldName = SolanaEncoderKey("account_context_hash") - SolTimestampOutputFieldName = SolanaEncoderKey("timestamp") - SolAnswerOutputFieldName = SolanaEncoderKey("answer") - SolDataIDOutputFieldName = SolanaEncoderKey("dataId") -) - -// secureMintReport represents the inner report structure, mimics the Report type in the SM plugin repo -type secureMintReport struct { - ConfigDigest ocr2types.ConfigDigest `json:"configDigest"` - SeqNr uint64 `json:"seqNr"` - Block uint64 `json:"block"` - Mintable *big.Int `json:"mintable"` -} - -type wrappedMintReport struct { - Report secureMintReport `json:"report"` - SolanaAccountContext solana.AccountMetaSlice `json:"solanaAccountContext,omitempty"` -} - -// chainSelector represents the chain selector type, mimics the ChainSelector type in the SM plugin repo -type chainSelector uint64 - -// SecureMintAggregatorConfig is the config for the SecureMint aggregator. -// This aggregator is designed to pick out reports for a specific chain selector. -type SecureMintAggregatorConfig struct { - // TargetChainSelector is the chain selector to look for - TargetChainSelector chainSelector `mapstructure:"targetChainSelector"` - DataID [16]byte `mapstructure:"dataID"` -} - -// ToMap converts the SecureMintAggregatorConfig to a values.Map, which is suitable for the -// [NewAggregator] function in the OCR3 Aggregator interface. -func (c SecureMintAggregatorConfig) ToMap() (*values.Map, error) { - v, err := values.WrapMap(c) - if err != nil { - // this should never happen since we are wrapping a struct - return &values.Map{}, fmt.Errorf("failed to wrap SecureMintAggregatorConfig: %w", err) - } - return v, nil -} - -var _ types.Aggregator = (*SecureMintAggregator)(nil) - -type SecureMintAggregator struct { - config SecureMintAggregatorConfig - formatters *formatterFactory -} - -type chainReportFormatter interface { - packReport(lggr logger.Logger, report *wrappedMintReport) (*values.Map, error) -} - -type evmReportFormatter struct { - targetChainSelector chainSelector - dataID [16]byte -} - -func (f *evmReportFormatter) packReport(lggr logger.Logger, wreport *wrappedMintReport) (*values.Map, error) { - report := wreport.Report - smReportAsAnswer, err := packSecureMintReportIntoUint224ForEVM(report.Mintable, report.Block) - if err != nil { - return nil, fmt.Errorf("failed to pack secure mint report for evm into uint224: %w", err) - } - - lggr.Debugw("packed report into answer", "smReportAsAnswer", smReportAsAnswer) - - // This is what the DF Cache contract expects: - // abi: "(bytes16 dataId, uint32 timestamp, uint224 answer)[] Reports" - toWrap := []any{ - map[EVMEncoderKey]any{ - DataIDOutputFieldName: f.dataID, - AnswerOutputFieldName: smReportAsAnswer, - TimestampOutputFieldName: uint32(report.SeqNr), - }, - } - - wrappedReport, err := values.NewMap(map[string]any{ - TopLevelListOutputFieldName: toWrap, - }) - if err != nil { - return nil, fmt.Errorf("failed to wrap report: %w", err) - } - - return wrappedReport, nil -} - -func newEVMReportFormatter(chainSelector chainSelector, config SecureMintAggregatorConfig) chainReportFormatter { - return &evmReportFormatter{targetChainSelector: chainSelector, dataID: config.DataID} -} - -type solanaReportFormatter struct { - targetChainSelector chainSelector - dataID [16]byte -} - -func (f *solanaReportFormatter) packReport(lggr logger.Logger, wreport *wrappedMintReport) (*values.Map, error) { - report := wreport.Report - // pack answer - smReportAsAnswer, err := packSecureMintReportIntoU128ForSolana(report.Mintable, report.Block) - if err != nil { - return nil, fmt.Errorf("failed to pack secure mint report for solana into u128: %w", err) - } - lggr.Debugw("packed report into answer", "smReportAsAnswer", smReportAsAnswer) - - // hash account contexts - var accounts = make([]byte, 0) - for _, acc := range wreport.SolanaAccountContext { - accounts = append(accounts, acc.PublicKey[:]...) - } - lggr.Debugf("accounts length: %d", len(wreport.SolanaAccountContext)) - accountContextHash := sha256.Sum256(accounts) - lggr.Debugw("calculated account context hash", "accountContextHash", accountContextHash) - - if report.SeqNr > (1<<32 - 1) { // timestamp must fit in u32 in solana - return nil, fmt.Errorf("timestamp exceeds u32 bounds: %v", report.SeqNr) - } - - toWrap := []any{ - map[SolanaEncoderKey]any{ - SolTimestampOutputFieldName: uint32(report.SeqNr), - SolAnswerOutputFieldName: smReportAsAnswer, - SolDataIDOutputFieldName: f.dataID, - }, - } - - wrappedReport, err := values.NewMap(map[string]any{ - TopLevelAccountCtxHashFieldName: accountContextHash, - TopLevelPayloadListFieldName: toWrap, - }) - - if err != nil { - return nil, fmt.Errorf("failed to wrap report: %w", err) - } - - return wrappedReport, nil -} - -func newSolanaReportFormatter(chainSelector chainSelector, config SecureMintAggregatorConfig) chainReportFormatter { - return &solanaReportFormatter{targetChainSelector: chainSelector, dataID: config.DataID} -} - -// chainReportFormatterBuilder is a function that returns a chainReportFormatter for a given chain selector and config -type chainReportFormatterBuilder func(chainSelector chainSelector, config SecureMintAggregatorConfig) chainReportFormatter - -type formatterFactory struct { - builders map[chainSelector]chainReportFormatterBuilder -} - -// register registers a new chain report formatter builder for a given chain selector -func (r *formatterFactory) register(chSel chainSelector, builder chainReportFormatterBuilder) { - r.builders[chSel] = builder -} - -// get uses a chain report formatter builder to create a chain report formatter -func (r *formatterFactory) get(chSel chainSelector, config SecureMintAggregatorConfig) (chainReportFormatter, error) { - b, ok := r.builders[chSel] - if !ok { - return nil, fmt.Errorf("no formatter registered for chain selector: %d", chSel) - } - - return b(chSel, config), nil -} - -// newFormatterFactory collects all chain report formatters per chain family so that they can be used to pack reports for different chains -func newFormatterFactory() *formatterFactory { - r := formatterFactory{ - builders: map[chainSelector]chainReportFormatterBuilder{}, - } - - // EVM - for _, selector := range chainselectors.EvmChainIdToChainSelector() { - r.register(chainSelector(selector), newEVMReportFormatter) - } - - // Solana - for _, selector := range chainselectors.SolanaChainIdToChainSelector() { - r.register(chainSelector(selector), newSolanaReportFormatter) - } - - return &r -} - -// NewSecureMintAggregator creates a new SecureMintAggregator instance based on the provided configuration. -// The config should be a [values.Map] that represents the [SecureMintAggregatorConfig]. See [SecureMintAggregatorConfig.ToMap] -func NewSecureMintAggregator(config values.Map) (types.Aggregator, error) { - parsedConfig, err := parseSecureMintConfig(config) - if err != nil { - return nil, fmt.Errorf("failed to parse config (%+v): %w", config, err) - } - registry := newFormatterFactory() - - return &SecureMintAggregator{ - config: parsedConfig, - formatters: registry, - }, nil -} - -// Aggregate implements the Aggregator interface -// This implementation: -// 1. Extracts OCRTriggerEvent from observations -// 2. Deserializes the inner ReportWithInfo to get chain selector and report -// 3. Validates chain selector matches target and sequence number is higher than previous -// 4. Returns the report in the format expected by the DF Cache, packing the mintable and block number into the decimal 'answer' field -func (a *SecureMintAggregator) Aggregate(lggr logger.Logger, previousOutcome *types.AggregationOutcome, observations map[ocrcommon.OracleID][]values.Value, f int) (*types.AggregationOutcome, error) { - lggr = logger.Named(lggr, "SecureMintAggregator") - - lggr.Debugw("Aggregate called", "config", a.config, "observations", len(observations), "f", f, "previousOutcome", previousOutcome) - - if len(observations) == 0 { - return nil, errors.New("no observations") - } - - // Extract and validate reports from all observations - validReports, err := a.extractAndValidateReports(lggr, observations, previousOutcome) - if err != nil { - return nil, fmt.Errorf("failed to extract and validate reports: %w", err) - } - - if len(validReports) == 0 { - lggr.Infow("no reports selected", "targetChainSelector", a.config.TargetChainSelector) - return &types.AggregationOutcome{ - ShouldReport: false, - }, nil - } - - // Take the first valid report - targetReport := validReports[0] - - // Create the aggregation outcome - outcome, err := a.createOutcome(lggr, targetReport) - if err != nil { - return nil, fmt.Errorf("failed to create outcome: %w", err) - } - - lggr.Debugw("SecureMint Aggregate complete", "targetChainSelector", a.config.TargetChainSelector) - return outcome, nil -} - -type ObsWithCtx struct { - Event capabilities.OCRTriggerEvent `mapstructure:"event"` - Solana solana.AccountMetaSlice `mapstructure:"solana"` -} - -// extractAndValidateReports extracts OCRTriggerEvent from observations and validates them -func (a *SecureMintAggregator) extractAndValidateReports(lggr logger.Logger, observations map[ocrcommon.OracleID][]values.Value, previousOutcome *types.AggregationOutcome) ([]*wrappedMintReport, error) { - var validReports []*wrappedMintReport - var foundMatchingChainSelector bool - - for nodeID, nodeObservations := range observations { - lggr = logger.With(lggr, "nodeID", nodeID) - - for _, observation := range nodeObservations { - lggr.Debugw("processing observation", "observation", observation) - - // Extract OCRTriggerEvent from the observations - - obsWithContext := &ObsWithCtx{} - - if err := observation.UnwrapTo(obsWithContext); err != nil { - lggr.Warnw("could not unwrap OCRTriggerEvent", "err", err, "observation", observation) - continue - } - - lggr.Debugw("Obs with context", "obs with ctx", obsWithContext) - - // Deserialize the ReportWithInfo - var reportWithInfo ocr3types.ReportWithInfo[chainSelector] - if err := json.Unmarshal(obsWithContext.Event.Report, &reportWithInfo); err != nil { - lggr.Errorw("failed to unmarshal ReportWithInfo", "err", err) - continue - } - - // Check if chain selector matches target - if reportWithInfo.Info != a.config.TargetChainSelector { - lggr.Debugw("chain selector mismatch", "got", reportWithInfo.Info, "expected", a.config.TargetChainSelector) - continue - } - - // We found a matching chain selector - foundMatchingChainSelector = true - - // Deserialize the inner secureMintReport - var innerReport secureMintReport - if err := json.Unmarshal(reportWithInfo.Report, &innerReport); err != nil { - lggr.Errorw("failed to unmarshal secureMintReport", "err", err) - continue - } - report := &wrappedMintReport{ - Report: innerReport, - SolanaAccountContext: obsWithContext.Solana, - } - - validReports = append(validReports, report) - } - } - - // Return appropriate error based on what we found - if !foundMatchingChainSelector { - lggr.Infow("no reports found for target chain selector, ignoring", "targetChainSelector", a.config.TargetChainSelector) - return nil, nil - } - - return validReports, nil -} - -// createOutcome creates the final aggregation outcome which can be sent to the KeystoneForwarder -func (a *SecureMintAggregator) createOutcome(lggr logger.Logger, report *wrappedMintReport) (*types.AggregationOutcome, error) { - lggr = logger.Named(lggr, "SecureMintAggregator") - lggr.Debugw("createOutcome called", "report", report) - - reportFormatter, err := a.formatters.get( - a.config.TargetChainSelector, - a.config, - ) - if err != nil { - return nil, fmt.Errorf("encountered issue fetching report formatter in createOutcome %w", err) - } - - wrappedReport, err := reportFormatter.packReport(lggr, report) - - if err != nil { - return nil, fmt.Errorf("encountered issue generating report in createOutcome %w", err) - } - - reportsProto := values.Proto(wrappedReport) - - // Store the sequence number in metadata for next round - metadata := []byte{byte(report.Report.SeqNr)} // Simple metadata for now - - aggOutcome := &types.AggregationOutcome{ - EncodableOutcome: reportsProto.GetMapValue(), - Metadata: metadata, - LastSeenAt: report.Report.SeqNr, - ShouldReport: true, // Always report since we found and verified the target report - } - - lggr.Debugw("SecureMint AggregationOutcome created", "aggOutcome", aggOutcome) - return aggOutcome, nil -} - -// parseSecureMintConfig parses the user-facing, type-less, SecureMint aggregator config into the internal typed config. -func parseSecureMintConfig(config values.Map) (SecureMintAggregatorConfig, error) { - type rawConfig struct { - TargetChainSelector string `mapstructure:"targetChainSelector"` - DataID string `mapstructure:"dataID"` - } - - var rawCfg rawConfig - if err := config.UnwrapTo(&rawCfg); err != nil { - return SecureMintAggregatorConfig{}, fmt.Errorf("failed to unwrap values.Map %+v: %w", config, err) - } - - if rawCfg.TargetChainSelector == "" { - return SecureMintAggregatorConfig{}, errors.New("targetChainSelector is required") - } - - sel, err := strconv.ParseUint(rawCfg.TargetChainSelector, 10, 64) - if err != nil { - return SecureMintAggregatorConfig{}, fmt.Errorf("invalid chain selector: %w", err) - } - - if rawCfg.DataID == "" { - return SecureMintAggregatorConfig{}, errors.New("dataID is required") - } - - // strip 0x prefix if present - dataID := strings.TrimPrefix(rawCfg.DataID, "0x") - - decodedDataID, err := hex.DecodeString(dataID) - if err != nil { - return SecureMintAggregatorConfig{}, fmt.Errorf("invalid dataID: %v %w", dataID, err) - } - - if len(decodedDataID) != 16 { - return SecureMintAggregatorConfig{}, fmt.Errorf("dataID must be 16 bytes, got %d", len(decodedDataID)) - } - - parsedConfig := SecureMintAggregatorConfig{ - TargetChainSelector: chainSelector(sel), - DataID: [16]byte(decodedDataID), - } - - return parsedConfig, nil -} - -var maxMintableEVM = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 128), big.NewInt(1)) // 2^128 - 1 - -// packSecureMintReportIntoUint224ForEVM packs the mintable and block number into a single uint224 so that it can be used as a price in the DF Cache contract -// (top 32 - not used / middle 64 - block number / lower 128 - mintable amount) -func packSecureMintReportIntoUint224ForEVM(mintable *big.Int, blockNumber uint64) (*big.Int, error) { - // Handle nil mintable - if mintable == nil { - return nil, errors.New("mintable cannot be nil") - } - - // Validate that mintable fits in 128 bits - if mintable.Cmp(maxMintableEVM) > 0 { - return nil, fmt.Errorf("mintable amount %v exceeds maximum 128-bit value %v", mintable, maxMintableEVM) - } - - packed := big.NewInt(0) - // Put mintable in lower 128 bits - packed.Or(packed, mintable) - - // Put block number in middle 64 bits (bits 128-191) - blockNumberAsBigInt := new(big.Int).SetUint64(blockNumber) - packed.Or(packed, new(big.Int).Lsh(blockNumberAsBigInt, 128)) - - return packed, nil -} - -var maxMintableSolana = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 91), big.NewInt(1)) // 2^91 - 1 -var maxBlockNumberSolana uint64 = 1<<36 - 1 // 2^36 - 1 - -// TODO: will ripcord be added for top bit? -// (top 1 - not used / middle 36 - block number / lower 91 - mintable amount) -func packSecureMintReportIntoU128ForSolana(mintable *big.Int, blockNumber uint64) (*big.Int, error) { - // Handle nil mintable - if mintable == nil { - return nil, errors.New("mintable cannot be nil") - } - - // Validate that mintable fits in 91 bits - if mintable.Cmp(maxMintableSolana) > 0 { - return nil, fmt.Errorf("mintable amount %v exceeds maximum 91-bit value %v", mintable, maxMintableSolana) - } - - packed := big.NewInt(0) - // Put mintable in lower 91 bits - packed.Or(packed, mintable) - - if blockNumber > maxBlockNumberSolana { - return nil, fmt.Errorf("block number %d exceeds maximum 36-bit value %d", blockNumber, maxBlockNumberSolana) - } - - // Put block number in middle 36 bits (bits 91-126) - blockNumberAsBigInt := new(big.Int).SetUint64(blockNumber) - packed.Or(packed, new(big.Int).Lsh(blockNumberAsBigInt, 91)) - - return packed, nil -} diff --git a/pkg/capabilities/consensus/ocr3/datafeeds/securemint_aggregator_test.go b/pkg/capabilities/consensus/ocr3/datafeeds/securemint_aggregator_test.go deleted file mode 100644 index a32c2901cc..0000000000 --- a/pkg/capabilities/consensus/ocr3/datafeeds/securemint_aggregator_test.go +++ /dev/null @@ -1,630 +0,0 @@ -package datafeeds - -import ( - "crypto/sha256" - "encoding/hex" - "encoding/json" - "math/big" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/types" - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/types/chains/solana" - ocrcommon "github.com/smartcontractkit/libocr/commontypes" - ocr2types "github.com/smartcontractkit/libocr/offchainreporting2/types" - ocr3types "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" - - "github.com/smartcontractkit/chainlink-protos/cre/go/values" -) - -var ( - // Test chain selectors - ethSepoliaChainSelector = chainSelector(16015286601757825753) // Ethereum Sepolia testnet - bnbTestnetChainSelector = chainSelector(13264668187771770619) // Binance Smart Chain testnet - solDevnetChainSelector = chainSelector(16423721717087811551) // Solana devnet -) - -func TestSecureMintAggregator_Aggregate(t *testing.T) { - lggr := logger.Test(t) - - type tcase struct { - name string - chainSelector string - dataID string - solAccounts [][32]byte - previousOutcome *types.AggregationOutcome - seqNr uint64 - observations map[ocrcommon.OracleID][]values.Value - f int - expectedShouldReport bool - expectError bool - errorContains string - shouldReportAssertFn func(t *testing.T, tc tcase, topLevelMap map[string]any) - } - acc1 := [32]byte{4, 5, 6} - acc2 := [32]byte{3, 2, 1} - - ethReportAssertFn := func(t *testing.T, tc tcase, topLevelMap map[string]any) { - // Check that we have the expected reports - reportsList, ok := topLevelMap[TopLevelListOutputFieldName].([]any) - require.True(t, ok) - assert.Len(t, reportsList, 1) - - // Check the first (and only) report - report, ok := reportsList[0].(map[string]any) - assert.True(t, ok) - - // Verify dataID - dataIDBytes, ok := report[DataIDOutputFieldName].([]byte) - assert.True(t, ok, "expected dataID to be []byte but got %T", report[DataIDOutputFieldName]) - assert.Len(t, dataIDBytes, 16) - assert.Equal(t, tc.dataID, "0x"+hex.EncodeToString(dataIDBytes)) - - // Verify other fields exist - answer, ok := report[AnswerOutputFieldName].(*big.Int) - assert.True(t, ok) - assert.NotNil(t, answer) - - timestamp := report[TimestampOutputFieldName].(int64) - assert.Equal(t, int64(tc.seqNr), timestamp) - } - - solReportAssertFn := func(t *testing.T, tc tcase, topLevelMap map[string]any) { - // Check that we have the expected reports - reportsList, ok := topLevelMap[TopLevelPayloadListFieldName].([]any) - assert.True(t, ok) - assert.Len(t, reportsList, 1) - - // Check that we have expected account hash - accHash, ok := topLevelMap[TopLevelAccountCtxHashFieldName].([]byte) - require.True(t, ok, "expected account hash to be []byte but got %T", topLevelMap[TopLevelAccountCtxHashFieldName]) - require.Len(t, accHash, 32) - expHash := sha256.Sum256(append(acc1[:], acc2[:]...)) - assert.Equal(t, expHash, ([32]byte)(accHash)) - - // Check the first (and only) report - report, ok := reportsList[0].(map[string]any) - assert.True(t, ok) - // Verify dataID - dataIDBytes, ok := report[SolDataIDOutputFieldName].([]byte) - assert.True(t, ok, "expected dataID to be []byte but got %T", report[DataIDOutputFieldName]) - assert.Len(t, dataIDBytes, 16) - assert.Equal(t, tc.dataID, "0x"+hex.EncodeToString(dataIDBytes)) - - // Verify other fields exist - answer, ok := report[SolAnswerOutputFieldName].(*big.Int) - assert.True(t, ok) - assert.NotNil(t, answer) - - timestamp := report[SolTimestampOutputFieldName].(int64) - assert.Equal(t, int64(tc.seqNr), timestamp) - } - - tests := []tcase{ - { - name: "successful eth report extraction", - chainSelector: "16015286601757825753", - dataID: "0x01c508f42b0201320000000000000000", - seqNr: 10, - observations: createSecureMintObservations(t, []ocrTriggerEventData{ - { - chainSelector: ethSepoliaChainSelector, - seqNr: 10, - report: &secureMintReport{ - ConfigDigest: ocr2types.ConfigDigest{0: 1, 31: 2}, - SeqNr: 10, - Block: 1000, - Mintable: big.NewInt(99), - }, - }, - { - chainSelector: bnbTestnetChainSelector, - seqNr: 10, - report: &secureMintReport{ - ConfigDigest: ocr2types.ConfigDigest{0: 2, 31: 3}, - SeqNr: 10, - Block: 1100, - Mintable: big.NewInt(200), - }, - }, - }), - f: 1, - expectedShouldReport: true, - expectError: false, - shouldReportAssertFn: ethReportAssertFn, - }, - { - name: "no matching chain selector found", - chainSelector: "16015286601757825753", - dataID: "0x01c508f42b0201320000000000000000", - seqNr: 10, - observations: createSecureMintObservations(t, []ocrTriggerEventData{ - { - chainSelector: bnbTestnetChainSelector, - seqNr: 10, - report: &secureMintReport{ - ConfigDigest: ocr2types.ConfigDigest{0: 1, 31: 2}, - SeqNr: 10, - Block: 1000, - Mintable: big.NewInt(99), - }, - }, - }), - f: 1, - expectError: false, - expectedShouldReport: false, - shouldReportAssertFn: ethReportAssertFn, - }, - { - name: "no observations", - chainSelector: "16015286601757825753", - dataID: "0x01c508f42b0201320000000000000000", - seqNr: 10, - observations: map[ocrcommon.OracleID][]values.Value{}, - f: 1, - expectError: true, - errorContains: "no observations", - }, - { - name: "successful sol report extraction", - chainSelector: "16423721717087811551", // solana devnet - dataID: "0x01c508f42b0201320000000000000000", - seqNr: 10, - solAccounts: [][32]byte{acc1, acc2}, - observations: createSecureMintObservations(t, []ocrTriggerEventData{ - { - chainSelector: solDevnetChainSelector, - seqNr: 10, - report: &secureMintReport{ - ConfigDigest: ocr2types.ConfigDigest{0: 1, 31: 2}, - SeqNr: 10, - Block: 1000, - Mintable: big.NewInt(99), - }, - accCtx: solana.AccountMetaSlice{&solana.AccountMeta{PublicKey: acc1}, &solana.AccountMeta{PublicKey: acc2}}, - }, - { - chainSelector: bnbTestnetChainSelector, - seqNr: 10, - report: &secureMintReport{ - ConfigDigest: ocr2types.ConfigDigest{0: 2, 31: 3}, - SeqNr: 10, - Block: 1100, - Mintable: big.NewInt(200), - }, - accCtx: solana.AccountMetaSlice{&solana.AccountMeta{PublicKey: acc1}, &solana.AccountMeta{PublicKey: acc2}}, - }, - }), - f: 1, - expectedShouldReport: true, - expectError: false, - shouldReportAssertFn: solReportAssertFn, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // Create aggregator - rawCfg := map[string]any{ - "targetChainSelector": tc.chainSelector, - "dataID": tc.dataID, - } - if len(tc.solAccounts) > 0 { - accountMetaSlice := make(solana.AccountMetaSlice, len(tc.solAccounts)) - for i, acc := range tc.solAccounts { - accountMetaSlice[i] = &solana.AccountMeta{PublicKey: acc} - } - - rawCfg["solana"] = map[string]any{ - "remaining_accounts": accountMetaSlice, - } - } - - configMap, err := values.WrapMap(rawCfg) - require.NoError(t, err) - aggregator, err := NewSecureMintAggregator(*configMap) - require.NoError(t, err) - - // Run aggregation - outcome, err := aggregator.Aggregate(lggr, tc.previousOutcome, tc.observations, tc.f) - - // Check error expectations - if tc.expectError { - assert.Error(t, err) - if tc.errorContains != "" { - assert.Contains(t, err.Error(), tc.errorContains) - } - return - } - - require.NoError(t, err) - assert.Equal(t, tc.expectedShouldReport, outcome.ShouldReport) - - if outcome.ShouldReport { - // Verify the output structure matches the feeds aggregator format - val, err := values.FromMapValueProto(outcome.EncodableOutcome) - require.NoError(t, err) - - topLevelMap, err := val.Unwrap() - require.NoError(t, err) - mm, ok := topLevelMap.(map[string]any) - require.True(t, ok) - - tc.shouldReportAssertFn(t, tc, mm) - } - }) - } -} - -func TestSecureMintAggregatorConfig_Validation(t *testing.T) { - acc1 := [32]byte{4, 5, 6} - - tests := []struct { - name string - chainSelector string - dataID string - solanaAccounts solana.AccountMetaSlice - expectedChainSelector chainSelector - expectedDataID [16]byte - expectError bool - errorMsg string - }{ - { - name: "valid chain selector, dataID and solana accounts", - chainSelector: "1", - dataID: "0x01c508f42b0201320000000000000000", - solanaAccounts: solana.AccountMetaSlice{&solana.AccountMeta{PublicKey: acc1, IsWritable: true, IsSigner: false}}, - expectedChainSelector: 1, - expectedDataID: [16]byte{0x01, 0xc5, 0x08, 0xf4, 0x2b, 0x02, 0x01, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, - expectError: false, - }, - { - name: "large chain selector", - chainSelector: "16015286601757825753", // ethereum-testnet-sepolia - dataID: "0x01c508f42b0201320000000000000000", - expectedChainSelector: 16015286601757825753, - expectedDataID: [16]byte{0x01, 0xc5, 0x08, 0xf4, 0x2b, 0x02, 0x01, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, - expectError: false, - }, - { - name: "dataID without 0x prefix", - chainSelector: "1", - dataID: "01c508f42b0201320000000000000000", - expectedChainSelector: 1, - expectedDataID: [16]byte{0x01, 0xc5, 0x08, 0xf4, 0x2b, 0x02, 0x01, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, - expectError: false}, - { - name: "invalid chain selector", - chainSelector: "invalid", - expectError: true, - errorMsg: "invalid chain selector", - }, - { - name: "negative chain selector", - chainSelector: "-1", - dataID: "0x01c508f42b0201320000000000000000", - expectError: true, - errorMsg: "invalid chain selector", - }, - { - name: "invalid dataID", - chainSelector: "1", - dataID: "invalid_data_id", - expectError: true, - errorMsg: "invalid dataID", - }, - { - name: "dataID too short", - chainSelector: "1", - dataID: "0x0000", - expectError: true, - errorMsg: "dataID must be 16 bytes", - }, - { - name: "dataID with odd length", - chainSelector: "1", - dataID: "0x0", - expectError: true, - errorMsg: "odd length hex string", - }, - { - name: "dataID too long", - chainSelector: "1", - dataID: "0x01111111111111111111111111111111111111111111", - expectError: true, - errorMsg: "dataID must be 16 bytes", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - rawCfg := map[string]any{ - "targetChainSelector": tt.chainSelector, - "dataID": tt.dataID, - } - if len(tt.solanaAccounts) > 0 { - rawCfg["solana"] = map[string]any{ - "remaining_accounts": tt.solanaAccounts, - } - } - - configMap, err := values.WrapMap(rawCfg) - require.NoError(t, err) - - aggregator, err := NewSecureMintAggregator(*configMap) - if tt.expectError { - assert.Error(t, err) - if tt.errorMsg != "" { - assert.Contains(t, err.Error(), tt.errorMsg) - } - return - } - - require.NoError(t, err) - assert.Equal(t, tt.expectedChainSelector, aggregator.(*SecureMintAggregator).config.TargetChainSelector) - assert.Equal(t, tt.expectedDataID, aggregator.(*SecureMintAggregator).config.DataID) - }) - } -} - -// Helper types and functions - -type ocrTriggerEventData struct { - chainSelector chainSelector - seqNr uint64 - report *secureMintReport - accCtx solana.AccountMetaSlice -} - -func createSecureMintObservations(t *testing.T, events []ocrTriggerEventData) map[ocrcommon.OracleID][]values.Value { - observations := make(map[ocrcommon.OracleID][]values.Value) - - // Create three observations with identical data to ensure f+1 consensus - for i := ocrcommon.OracleID(1); i <= 3; i++ { - // For each oracle, create observations for all events - var oracleObservations []values.Value - for _, event := range events { - // Create the ReportWithInfo - ocr3Report := &ocr3types.ReportWithInfo[chainSelector]{ - Report: createReportBytes(t, event.report), - Info: event.chainSelector, - } - - // Marshal the ReportWithInfo - jsonReport, err := json.Marshal(ocr3Report) - require.NoError(t, err) - - // Create the OCRTriggerEvent - triggerEvent := &capabilities.OCRTriggerEvent{ - ConfigDigest: event.report.ConfigDigest[:], - SeqNr: event.seqNr, - Report: jsonReport, - Sigs: []capabilities.OCRAttributedOnchainSignature{ - { - Signature: []byte("signature1"), - Signer: 1, - }, - { - Signature: []byte("signature2"), - Signer: 2, - }, - }, - } - - // wrap with account context if present - val, err := values.Wrap(map[string]any{ - "event": triggerEvent, - "solana": event.accCtx, - }) - require.NoError(t, err) - oracleObservations = append(oracleObservations, val) - } - - observations[i] = oracleObservations - } - - return observations -} - -func createReportBytes(t *testing.T, report *secureMintReport) []byte { - reportBytes, err := json.Marshal(report) - require.NoError(t, err) - return reportBytes -} - -func TestPackSecureMintReportForIntoUint224(t *testing.T) { - tests := []struct { - name string - mintable *big.Int - blockNumber uint64 - expected *big.Int - expectError bool - errorMsg string - }{ - { - name: "zero values", - mintable: big.NewInt(0), - blockNumber: 0, - expected: big.NewInt(0), - expectError: false, - }, - { - name: "small positive values", - mintable: big.NewInt(100), - blockNumber: 12345, - expected: new(big.Int).Add(big.NewInt(100), new(big.Int).Lsh(big.NewInt(12345), 128)), - expectError: false, - }, - { - name: "maximum mintable value (2^128 - 1)", - mintable: new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 128), big.NewInt(1)), - blockNumber: 999999, - expected: new(big.Int).Add( - new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 128), big.NewInt(1)), - new(big.Int).Lsh(big.NewInt(999999), 128), - ), - expectError: false, - }, - { - name: "large block number", - mintable: big.NewInt(500), - blockNumber: 18446744073709551615, // max uint64 - expected: new(big.Int).Add(big.NewInt(500), new(big.Int).Lsh(new(big.Int).SetUint64(18446744073709551615), 128)), - expectError: false, - }, - { - name: "mintable exceeds 128 bits", - mintable: new(big.Int).Lsh(big.NewInt(1), 128), // 2^128 - blockNumber: 1000, - expectError: true, - errorMsg: "mintable amount", - }, - { - name: "very large mintable that exceeds 128 bits", - mintable: new(big.Int).Lsh(big.NewInt(1), 256), // 2^256 - blockNumber: 1000, - expectError: true, - errorMsg: "mintable amount", - }, - { - name: "nil mintable", - mintable: nil, - blockNumber: 1000, - expectError: true, - errorMsg: "mintable cannot be nil", - }, - { - name: "bit pattern verification - mintable 1, block 1", - mintable: big.NewInt(1), - blockNumber: 1, - expected: new(big.Int).Add(big.NewInt(1), new(big.Int).Lsh(big.NewInt(1), 128)), - expectError: false, - }, - { - name: "bit pattern verification - mintable 0xFFFFFFFF, block 0xFFFFFFFF", - mintable: big.NewInt(0xFFFFFFFF), - blockNumber: 0xFFFFFFFF, - expected: new(big.Int).Add(big.NewInt(0xFFFFFFFF), new(big.Int).Lsh(big.NewInt(0xFFFFFFFF), 128)), - expectError: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := packSecureMintReportIntoUint224ForEVM(tt.mintable, tt.blockNumber) - - if tt.expectError { - assert.Error(t, err) - if tt.errorMsg != "" { - assert.Contains(t, err.Error(), tt.errorMsg) - } - return - } - - require.NoError(t, err) - assert.NotNil(t, result) - - if tt.expected != nil { - assert.Equal(t, tt.expected, result) - } - - // Additional validation: ensure the result fits in 224 bits - maxUint224 := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 224), big.NewInt(1)) - assert.LessOrEqual(t, result.Cmp(maxUint224), 0, "result should fit in 224 bits") - - // Verify bit layout if we have expected values and not a large block number - if tt.expected != nil { - verifyBitLayout(t, result, tt.mintable, tt.blockNumber) - } - }) - } -} - -func TestPackSecureMintReportForIntoUint224_EdgeCases(t *testing.T) { - // Test edge cases and boundary conditions - tests := []struct { - name string - mintable *big.Int - blockNumber uint64 - expectError bool - }{ - { - name: "mintable exactly at 128-bit boundary", - mintable: new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 128), big.NewInt(1)), // 2^128 - 1 - blockNumber: 1000, - expectError: false, - }, - { - name: "mintable one over 128-bit boundary", - mintable: new(big.Int).Lsh(big.NewInt(1), 128), // 2^128 - blockNumber: 1000, - expectError: true, - }, - { - name: "block number at max uint64", - mintable: big.NewInt(100), - blockNumber: 0xFFFFFFFFFFFFFFFF, - expectError: false, - }, - { - name: "both values at maximum", - mintable: new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 128), big.NewInt(1)), - blockNumber: 0xFFFFFFFFFFFFFFFF, - expectError: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := packSecureMintReportIntoUint224ForEVM(tt.mintable, tt.blockNumber) - - if tt.expectError { - assert.Error(t, err) - return - } - - require.NoError(t, err) - assert.NotNil(t, result) - - // Verify the result is within uint224 bounds - maxUint224 := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 224), big.NewInt(1)) - assert.LessOrEqual(t, result.Cmp(maxUint224), 0, "result should fit in 224 bits") - }) - } -} - -// verifyBitLayout verifies that the packed result has the correct bit layout -// mintable should be in bits 0-127, block number in bits 128-191 -func verifyBitLayout(t *testing.T, packed *big.Int, mintable *big.Int, blockNumber uint64) { - // Extract mintable from lower 128 bits - mintableMask := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 128), big.NewInt(1)) - extractedMintable := new(big.Int).And(packed, mintableMask) - - // Extract block number from bits 128-191 - blockNumberMask := new(big.Int).Lsh(new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 64), big.NewInt(1)), 128) - extractedBlockNumber := new(big.Int).And(packed, blockNumberMask) - extractedBlockNumber = new(big.Int).Rsh(extractedBlockNumber, 128) - - // Always use big.NewInt(0) for zero-value mintable - expectedMintable := mintable - if mintable == nil || (mintable != nil && mintable.Sign() == 0) { - expectedMintable = big.NewInt(0) - } - - assert.Equal(t, expectedMintable, extractedMintable, "mintable bits should match") - assert.Equal(t, new(big.Int).SetUint64(blockNumber), extractedBlockNumber, "block number bits should match") -} - -func TestMaxMintableConstant(t *testing.T) { - // Verify the maxMintable constant is correctly defined - expectedMax := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 128), big.NewInt(1)) - assert.Equal(t, expectedMax, maxMintableEVM, "maxMintable should be 2^128 - 1") - - // Verify it's exactly 128 bits - bitLen := maxMintableEVM.BitLen() - assert.Equal(t, 128, bitLen, "maxMintable should be exactly 128 bits") -} diff --git a/pkg/capabilities/consensus/ocr3/ocr3cap/data_feeds_consensus_builders_generated.go b/pkg/capabilities/consensus/ocr3/ocr3cap/data_feeds_consensus_builders_generated.go deleted file mode 100644 index ed7bfad077..0000000000 --- a/pkg/capabilities/consensus/ocr3/ocr3cap/data_feeds_consensus_builders_generated.go +++ /dev/null @@ -1,113 +0,0 @@ -// Code generated by github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli, DO NOT EDIT. - -package ocr3cap - -import ( - "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/triggers/streams" - "github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk" -) - -func (cfg DataFeedsConsensusConfig) New(w *sdk.WorkflowSpecFactory, ref string, input DataFeedsConsensusInput) SignedReportCap { - - def := sdk.StepDefinition{ - ID: "offchain_reporting@1.0.0", Ref: ref, - Inputs: input.ToSteps(), - Config: map[string]any{ - "aggregation_config": cfg.AggregationConfig, - "aggregation_method": cfg.AggregationMethod, - "encoder": cfg.Encoder, - "encoder_config": cfg.EncoderConfig, - "key_id": cfg.KeyId, - "report_id": cfg.ReportId, - }, - CapabilityType: capabilities.CapabilityTypeConsensus, - } - - step := sdk.Step[SignedReport]{Definition: def} - raw := step.AddTo(w) - return SignedReportWrapper(raw) -} - -// FeedValueWrapper allows access to field from an sdk.CapDefinition[FeedValue] -func FeedValueWrapper(raw sdk.CapDefinition[FeedValue]) FeedValueCap { - wrapped, ok := raw.(FeedValueCap) - if ok { - return wrapped - } - return &feedValueCap{CapDefinition: raw} -} - -type FeedValueCap interface { - sdk.CapDefinition[FeedValue] - Deviation() sdk.CapDefinition[string] - Heartbeat() sdk.CapDefinition[uint64] - RemappedID() sdk.CapDefinition[string] - private() -} - -type feedValueCap struct { - sdk.CapDefinition[FeedValue] -} - -func (*feedValueCap) private() {} -func (c *feedValueCap) Deviation() sdk.CapDefinition[string] { - return sdk.AccessField[FeedValue, string](c.CapDefinition, "deviation") -} -func (c *feedValueCap) Heartbeat() sdk.CapDefinition[uint64] { - return sdk.AccessField[FeedValue, uint64](c.CapDefinition, "heartbeat") -} -func (c *feedValueCap) RemappedID() sdk.CapDefinition[string] { - return sdk.AccessField[FeedValue, string](c.CapDefinition, "remappedID") -} - -func ConstantFeedValue(value FeedValue) FeedValueCap { - return &feedValueCap{CapDefinition: sdk.ConstantDefinition(value)} -} - -func NewFeedValueFromFields( - deviation sdk.CapDefinition[string], - heartbeat sdk.CapDefinition[uint64], - remappedID sdk.CapDefinition[string]) FeedValueCap { - return &simpleFeedValue{ - CapDefinition: sdk.ComponentCapDefinition[FeedValue]{ - "deviation": deviation.Ref(), - "heartbeat": heartbeat.Ref(), - "remappedID": remappedID.Ref(), - }, - deviation: deviation, - heartbeat: heartbeat, - remappedID: remappedID, - } -} - -type simpleFeedValue struct { - sdk.CapDefinition[FeedValue] - deviation sdk.CapDefinition[string] - heartbeat sdk.CapDefinition[uint64] - remappedID sdk.CapDefinition[string] -} - -func (c *simpleFeedValue) Deviation() sdk.CapDefinition[string] { - return c.deviation -} -func (c *simpleFeedValue) Heartbeat() sdk.CapDefinition[uint64] { - return c.heartbeat -} -func (c *simpleFeedValue) RemappedID() sdk.CapDefinition[string] { - return c.remappedID -} - -func (c *simpleFeedValue) private() {} - -type DataFeedsConsensusInput struct { - Observations sdk.CapDefinition[[]streams.Feed] -} - -func (input DataFeedsConsensusInput) ToSteps() sdk.StepInputs { - return sdk.StepInputs{ - Mapping: map[string]any{ - "observations": input.Observations.Ref(), - }, - } -} diff --git a/pkg/capabilities/consensus/ocr3/ocr3cap/ocr3cap_data_feeds_consensus-schema.json b/pkg/capabilities/consensus/ocr3/ocr3cap/ocr3cap_data_feeds_consensus-schema.json deleted file mode 100644 index c9164d112a..0000000000 --- a/pkg/capabilities/consensus/ocr3/ocr3cap/ocr3cap_data_feeds_consensus-schema.json +++ /dev/null @@ -1,115 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/ocr3cap/offchain_reporting@1.0.0", - "$defs": { - "FeedValue": { - "type": "object", - "properties": { - "deviation": { - "type": "string", - "description": "The deviation that is required to generate a new report. Expressed as a percentage. For example, 0.01 is 1% deviation." - }, - "heartbeat": { - "type": "integer", - "minimum": 1, - "description": "The interval in seconds after which a new report is generated, regardless of whether any deviations have occurred. New reports reset the timer." - }, - "remappedID": { - "type": [ - "string", - "null" - ], - "description": "An optional remapped ID for the feed." - } - }, - "additionalProperties": false, - "required": [ - "deviation", - "heartbeat" - ] - } - }, - "properties": { - "config": { - "properties": { - "aggregation_method": { - "type": "string", - "enum": [ - "data_feeds" - ] - }, - "aggregation_config": { - "type": "object", - "properties": { - "allowedPartialStaleness": { - "type": "string", - "description": "Allowed partial staleness as a number between 0 and 1." - }, - "feeds": { - "type": "object", - "propertyNames": { - "$ref": "../../../triggers/streams/streams_trigger-schema.json#/$defs/feedId" - }, - "additionalProperties": { - "$ref": "#/$defs/FeedValue" - } - } - }, - "additionalProperties": false, - "required": [ - "allowedPartialStaleness", - "feeds" - ] - }, - "encoder": { - "$ref": "ocr3cap_common-schema.json#/$defs/encoder" - }, - "encoder_config": { - "$ref": "ocr3cap_common-schema.json#/$defs/encoder_config" - }, - "key_id": { - "$ref": "ocr3cap_common-schema.json#/$defs/key_id" - }, - "report_id": { - "$ref": "ocr3cap_common-schema.json#/$defs/report_id" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "aggregation_method", - "aggregation_config", - "encoder", - "encoder_config", - "key_id", - "report_id" - ] - }, - "inputs": { - "properties": { - "observations": { - "type": "array", - "items": { - "$ref": "../../../triggers/streams/streams_trigger-schema.json#/$defs/Feed" - } - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "observations" - ] - }, - "outputs": { - "$ref": "ocr3cap_common-schema.json#/$defs/SignedReport" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "config", - "inputs", - "outputs" - ], - "description": "OCR3 consensus exposed as a capability." -} diff --git a/pkg/capabilities/consensus/ocr3/ocr3cap/ocr3cap_data_feeds_consensus_generated.go b/pkg/capabilities/consensus/ocr3/ocr3cap/ocr3cap_data_feeds_consensus_generated.go deleted file mode 100644 index a39f57a5c1..0000000000 --- a/pkg/capabilities/consensus/ocr3/ocr3cap/ocr3cap_data_feeds_consensus_generated.go +++ /dev/null @@ -1,220 +0,0 @@ -// Code generated by github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli, DO NOT EDIT. - -package ocr3cap - -import ( - "encoding/json" - "fmt" - - "reflect" - - streams "github.com/smartcontractkit/chainlink-common/pkg/capabilities/triggers/streams" -) - -// OCR3 consensus exposed as a capability. -type DataFeedsConsensus struct { - // Config corresponds to the JSON schema field "config". - Config DataFeedsConsensusConfig `json:"config" yaml:"config" mapstructure:"config"` - - // Inputs corresponds to the JSON schema field "inputs". - Inputs DataFeedsConsensusInputs `json:"inputs" yaml:"inputs" mapstructure:"inputs"` - - // Outputs corresponds to the JSON schema field "outputs". - Outputs SignedReport `json:"outputs" yaml:"outputs" mapstructure:"outputs"` -} - -type DataFeedsConsensusConfig struct { - // AggregationConfig corresponds to the JSON schema field "aggregation_config". - AggregationConfig DataFeedsConsensusConfigAggregationConfig `json:"aggregation_config" yaml:"aggregation_config" mapstructure:"aggregation_config"` - - // AggregationMethod corresponds to the JSON schema field "aggregation_method". - AggregationMethod DataFeedsConsensusConfigAggregationMethod `json:"aggregation_method" yaml:"aggregation_method" mapstructure:"aggregation_method"` - - // Encoder corresponds to the JSON schema field "encoder". - Encoder Encoder `json:"encoder" yaml:"encoder" mapstructure:"encoder"` - - // EncoderConfig corresponds to the JSON schema field "encoder_config". - EncoderConfig EncoderConfig `json:"encoder_config" yaml:"encoder_config" mapstructure:"encoder_config"` - - // KeyId corresponds to the JSON schema field "key_id". - KeyId KeyId `json:"key_id" yaml:"key_id" mapstructure:"key_id"` - - // ReportId corresponds to the JSON schema field "report_id". - ReportId ReportId `json:"report_id" yaml:"report_id" mapstructure:"report_id"` -} - -type DataFeedsConsensusConfigAggregationConfig struct { - // Allowed partial staleness as a number between 0 and 1. - AllowedPartialStaleness string `json:"allowedPartialStaleness" yaml:"allowedPartialStaleness" mapstructure:"allowedPartialStaleness"` - - // Feeds corresponds to the JSON schema field "feeds". - Feeds DataFeedsConsensusConfigAggregationConfigFeeds `json:"feeds" yaml:"feeds" mapstructure:"feeds"` -} - -type DataFeedsConsensusConfigAggregationConfigFeeds map[string]FeedValue - -// UnmarshalJSON implements json.Unmarshaler. -func (j *DataFeedsConsensusConfigAggregationConfig) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - if _, ok := raw["allowedPartialStaleness"]; raw != nil && !ok { - return fmt.Errorf("field allowedPartialStaleness in DataFeedsConsensusConfigAggregationConfig: required") - } - if _, ok := raw["feeds"]; raw != nil && !ok { - return fmt.Errorf("field feeds in DataFeedsConsensusConfigAggregationConfig: required") - } - type Plain DataFeedsConsensusConfigAggregationConfig - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - *j = DataFeedsConsensusConfigAggregationConfig(plain) - return nil -} - -type DataFeedsConsensusConfigAggregationMethod string - -const DataFeedsConsensusConfigAggregationMethodDataFeeds DataFeedsConsensusConfigAggregationMethod = "data_feeds" - -var enumValues_DataFeedsConsensusConfigAggregationMethod = []interface{}{ - "data_feeds", -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *DataFeedsConsensusConfigAggregationMethod) UnmarshalJSON(b []byte) error { - var v string - if err := json.Unmarshal(b, &v); err != nil { - return err - } - var ok bool - for _, expected := range enumValues_DataFeedsConsensusConfigAggregationMethod { - if reflect.DeepEqual(v, expected) { - ok = true - break - } - } - if !ok { - return fmt.Errorf("invalid value (expected one of %#v): %#v", enumValues_DataFeedsConsensusConfigAggregationMethod, v) - } - *j = DataFeedsConsensusConfigAggregationMethod(v) - return nil -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *DataFeedsConsensusConfig) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - if _, ok := raw["aggregation_config"]; raw != nil && !ok { - return fmt.Errorf("field aggregation_config in DataFeedsConsensusConfig: required") - } - if _, ok := raw["aggregation_method"]; raw != nil && !ok { - return fmt.Errorf("field aggregation_method in DataFeedsConsensusConfig: required") - } - if _, ok := raw["encoder"]; raw != nil && !ok { - return fmt.Errorf("field encoder in DataFeedsConsensusConfig: required") - } - if _, ok := raw["encoder_config"]; raw != nil && !ok { - return fmt.Errorf("field encoder_config in DataFeedsConsensusConfig: required") - } - if _, ok := raw["key_id"]; raw != nil && !ok { - return fmt.Errorf("field key_id in DataFeedsConsensusConfig: required") - } - if _, ok := raw["report_id"]; raw != nil && !ok { - return fmt.Errorf("field report_id in DataFeedsConsensusConfig: required") - } - type Plain DataFeedsConsensusConfig - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - *j = DataFeedsConsensusConfig(plain) - return nil -} - -type DataFeedsConsensusInputs struct { - // Observations corresponds to the JSON schema field "observations". - Observations []streams.Feed `json:"observations" yaml:"observations" mapstructure:"observations"` -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *DataFeedsConsensusInputs) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - if _, ok := raw["observations"]; raw != nil && !ok { - return fmt.Errorf("field observations in DataFeedsConsensusInputs: required") - } - type Plain DataFeedsConsensusInputs - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - *j = DataFeedsConsensusInputs(plain) - return nil -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *DataFeedsConsensus) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - if _, ok := raw["config"]; raw != nil && !ok { - return fmt.Errorf("field config in DataFeedsConsensus: required") - } - if _, ok := raw["inputs"]; raw != nil && !ok { - return fmt.Errorf("field inputs in DataFeedsConsensus: required") - } - if _, ok := raw["outputs"]; raw != nil && !ok { - return fmt.Errorf("field outputs in DataFeedsConsensus: required") - } - type Plain DataFeedsConsensus - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - *j = DataFeedsConsensus(plain) - return nil -} - -type FeedValue struct { - // The deviation that is required to generate a new report. Expressed as a - // percentage. For example, 0.01 is 1% deviation. - Deviation string `json:"deviation" yaml:"deviation" mapstructure:"deviation"` - - // The interval in seconds after which a new report is generated, regardless of - // whether any deviations have occurred. New reports reset the timer. - Heartbeat uint64 `json:"heartbeat" yaml:"heartbeat" mapstructure:"heartbeat"` - - // An optional remapped ID for the feed. - RemappedID *string `json:"remappedID,omitempty" yaml:"remappedID,omitempty" mapstructure:"remappedID,omitempty"` -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *FeedValue) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - if _, ok := raw["deviation"]; raw != nil && !ok { - return fmt.Errorf("field deviation in FeedValue: required") - } - if _, ok := raw["heartbeat"]; raw != nil && !ok { - return fmt.Errorf("field heartbeat in FeedValue: required") - } - type Plain FeedValue - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - if 1 > plain.Heartbeat { - return fmt.Errorf("field %s: must be >= %v", "heartbeat", 1) - } - *j = FeedValue(plain) - return nil -} diff --git a/pkg/capabilities/datastreams/mocks/report_codec.go b/pkg/capabilities/datastreams/mocks/report_codec.go deleted file mode 100644 index 3ac74c7d2d..0000000000 --- a/pkg/capabilities/datastreams/mocks/report_codec.go +++ /dev/null @@ -1,201 +0,0 @@ -// Code generated by mockery v2.53.6. DO NOT EDIT. - -package mocks - -import ( - datastreams "github.com/smartcontractkit/chainlink-common/pkg/capabilities/datastreams" - mock "github.com/stretchr/testify/mock" - - values "github.com/smartcontractkit/chainlink-protos/cre/go/values" -) - -// ReportCodec is an autogenerated mock type for the ReportCodec type -type ReportCodec struct { - mock.Mock -} - -type ReportCodec_Expecter struct { - mock *mock.Mock -} - -func (_m *ReportCodec) EXPECT() *ReportCodec_Expecter { - return &ReportCodec_Expecter{mock: &_m.Mock} -} - -// Unwrap provides a mock function with given fields: wrapped -func (_m *ReportCodec) Unwrap(wrapped values.Value) ([]datastreams.FeedReport, error) { - ret := _m.Called(wrapped) - - if len(ret) == 0 { - panic("no return value specified for Unwrap") - } - - var r0 []datastreams.FeedReport - var r1 error - if rf, ok := ret.Get(0).(func(values.Value) ([]datastreams.FeedReport, error)); ok { - return rf(wrapped) - } - if rf, ok := ret.Get(0).(func(values.Value) []datastreams.FeedReport); ok { - r0 = rf(wrapped) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]datastreams.FeedReport) - } - } - - if rf, ok := ret.Get(1).(func(values.Value) error); ok { - r1 = rf(wrapped) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// ReportCodec_Unwrap_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Unwrap' -type ReportCodec_Unwrap_Call struct { - *mock.Call -} - -// Unwrap is a helper method to define mock.On call -// - wrapped values.Value -func (_e *ReportCodec_Expecter) Unwrap(wrapped interface{}) *ReportCodec_Unwrap_Call { - return &ReportCodec_Unwrap_Call{Call: _e.mock.On("Unwrap", wrapped)} -} - -func (_c *ReportCodec_Unwrap_Call) Run(run func(wrapped values.Value)) *ReportCodec_Unwrap_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(values.Value)) - }) - return _c -} - -func (_c *ReportCodec_Unwrap_Call) Return(_a0 []datastreams.FeedReport, _a1 error) *ReportCodec_Unwrap_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *ReportCodec_Unwrap_Call) RunAndReturn(run func(values.Value) ([]datastreams.FeedReport, error)) *ReportCodec_Unwrap_Call { - _c.Call.Return(run) - return _c -} - -// Validate provides a mock function with given fields: feedReport, allowedSigners, minRequiredSignatures -func (_m *ReportCodec) Validate(feedReport datastreams.FeedReport, allowedSigners [][]byte, minRequiredSignatures int) error { - ret := _m.Called(feedReport, allowedSigners, minRequiredSignatures) - - if len(ret) == 0 { - panic("no return value specified for Validate") - } - - var r0 error - if rf, ok := ret.Get(0).(func(datastreams.FeedReport, [][]byte, int) error); ok { - r0 = rf(feedReport, allowedSigners, minRequiredSignatures) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// ReportCodec_Validate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Validate' -type ReportCodec_Validate_Call struct { - *mock.Call -} - -// Validate is a helper method to define mock.On call -// - feedReport datastreams.FeedReport -// - allowedSigners [][]byte -// - minRequiredSignatures int -func (_e *ReportCodec_Expecter) Validate(feedReport interface{}, allowedSigners interface{}, minRequiredSignatures interface{}) *ReportCodec_Validate_Call { - return &ReportCodec_Validate_Call{Call: _e.mock.On("Validate", feedReport, allowedSigners, minRequiredSignatures)} -} - -func (_c *ReportCodec_Validate_Call) Run(run func(feedReport datastreams.FeedReport, allowedSigners [][]byte, minRequiredSignatures int)) *ReportCodec_Validate_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(datastreams.FeedReport), args[1].([][]byte), args[2].(int)) - }) - return _c -} - -func (_c *ReportCodec_Validate_Call) Return(_a0 error) *ReportCodec_Validate_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *ReportCodec_Validate_Call) RunAndReturn(run func(datastreams.FeedReport, [][]byte, int) error) *ReportCodec_Validate_Call { - _c.Call.Return(run) - return _c -} - -// Wrap provides a mock function with given fields: reports -func (_m *ReportCodec) Wrap(reports []datastreams.FeedReport) (values.Value, error) { - ret := _m.Called(reports) - - if len(ret) == 0 { - panic("no return value specified for Wrap") - } - - var r0 values.Value - var r1 error - if rf, ok := ret.Get(0).(func([]datastreams.FeedReport) (values.Value, error)); ok { - return rf(reports) - } - if rf, ok := ret.Get(0).(func([]datastreams.FeedReport) values.Value); ok { - r0 = rf(reports) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(values.Value) - } - } - - if rf, ok := ret.Get(1).(func([]datastreams.FeedReport) error); ok { - r1 = rf(reports) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// ReportCodec_Wrap_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Wrap' -type ReportCodec_Wrap_Call struct { - *mock.Call -} - -// Wrap is a helper method to define mock.On call -// - reports []datastreams.FeedReport -func (_e *ReportCodec_Expecter) Wrap(reports interface{}) *ReportCodec_Wrap_Call { - return &ReportCodec_Wrap_Call{Call: _e.mock.On("Wrap", reports)} -} - -func (_c *ReportCodec_Wrap_Call) Run(run func(reports []datastreams.FeedReport)) *ReportCodec_Wrap_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].([]datastreams.FeedReport)) - }) - return _c -} - -func (_c *ReportCodec_Wrap_Call) Return(_a0 values.Value, _a1 error) *ReportCodec_Wrap_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *ReportCodec_Wrap_Call) RunAndReturn(run func([]datastreams.FeedReport) (values.Value, error)) *ReportCodec_Wrap_Call { - _c.Call.Return(run) - return _c -} - -// NewReportCodec creates a new instance of ReportCodec. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewReportCodec(t interface { - mock.TestingT - Cleanup(func()) -}) *ReportCodec { - mock := &ReportCodec{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/capabilities/datastreams/types.go b/pkg/capabilities/datastreams/types.go deleted file mode 100644 index 13b20c3d7c..0000000000 --- a/pkg/capabilities/datastreams/types.go +++ /dev/null @@ -1,187 +0,0 @@ -package datastreams - -import ( - "encoding/hex" - "errors" - "fmt" - "strings" - - "github.com/smartcontractkit/chainlink-protos/cre/go/values" -) - -// hex-encoded 32-byte value, prefixed with "0x", all lowercase -type FeedID string - -const FeedIDBytesLen = 32 - -var ErrInvalidFeedID = errors.New("invalid feed ID") - -func (id FeedID) String() string { - return string(id) -} - -// Bytes() converts the FeedID string into a [32]byte -// value. -// Note: this function panics if the underlying -// string isn't of the right length. For production (i.e.) -// non-test uses, please create the FeedID via the NewFeedID -// constructor, which will validate the string. -func (id FeedID) Bytes() [FeedIDBytesLen]byte { - b, _ := hex.DecodeString(string(id)[2:]) - return [FeedIDBytesLen]byte(b) -} - -func (id FeedID) validate() error { - if len(id) != 2*FeedIDBytesLen+2 { - return ErrInvalidFeedID - } - if !strings.HasPrefix(string(id), "0x") { - return ErrInvalidFeedID - } - if strings.ToLower(string(id)) != string(id) { - return ErrInvalidFeedID - } - _, err := hex.DecodeString(string(id)[2:]) - return err -} - -func NewFeedID(s string) (FeedID, error) { - id := FeedID(s) - return id, id.validate() -} - -func FeedIDFromBytes(b [FeedIDBytesLen]byte) FeedID { - return FeedID("0x" + hex.EncodeToString(b[:])) -} - -type FeedReport struct { - FeedID string - FullReport []byte - ReportContext []byte - Signatures [][]byte - - // Fields below are derived from FullReport - // NOTE: BenchmarkPrice is a byte representation of big.Int. We can't use big.Int - // directly due to Value serialization problems using mapstructure. - BenchmarkPrice []byte - ObservationTimestamp int64 -} - -// passed alongside Streams trigger events -type Metadata struct { - Signers [][]byte - MinRequiredSignatures int -} - -// StreamsTriggerEvent is the underlying type passed to the dataFeedsAggregator.Aggregate -// function via the untyped observation, which originates in the asset don. -type StreamsTriggerEvent struct { - Payload []FeedReport - Metadata Metadata - Timestamp int64 -} - -// LLOStreamsTriggerEvent is the underlying type passed to the LLOAggregator.Aggregate -// function via the untyped observation, which originates on the asset don via the LLO OCR3 plugin. -type LLOStreamsTriggerEvent struct { - Payload []*LLOStreamDecimal - ObservationTimestampNanoseconds uint64 -} - -type LLOStreamDecimal struct { - StreamID uint32 - Decimal []byte // binary representation of [llo.Decimal]: https://github.com/smartcontractkit/chainlink-data-streams/blob/d33e95631485bbcfdc22d209875035e3c73199d0/llo/stream_value.go#L147 - // future: may add aggregation type {MODE, MEDIAN, etc...} -} - -type ReportCodec interface { - // unwrap StreamsTriggerEvent and convert to a list of FeedReport - Unwrap(wrapped values.Value) ([]FeedReport, error) - - // wrap a list of FeedReport to a wrapped StreamsTriggerEvent Value - Wrap(reports []FeedReport) (values.Value, error) - - // validate signatures on a single FeedReport - Validate(feedReport FeedReport, allowedSigners [][]byte, minRequiredSignatures int) error -} - -// Helpers for unwrapping a StreamsTriggerPayload into a []FeedReport - more efficient than using mapstructure/reflection -func UnwrapStreamsTriggerEventToFeedReportList(wrapped values.Value) ([]FeedReport, error) { - result := []FeedReport{} - triggerEvent, ok := wrapped.(*values.Map) - if !ok { - return nil, fmt.Errorf("unexpected value %+v for trigger payload: expected map, got %T", wrapped, wrapped) - } - - p, ok := triggerEvent.Underlying["Payload"] - if !ok { - return nil, errors.New("expected map to have Payload field") - } - - plst, ok := p.(*values.List) - if !ok { - return nil, errors.New("expected Payload to be a list") - } - for _, v := range plst.Underlying { - report := FeedReport{} - mp, ok := v.(*values.Map) - if !ok { - return nil, fmt.Errorf("unexpected value %+v for feed report: expected map, got %T", v, v) - } - var err error - report.FeedID, err = getStringField(mp, "FeedID") - if err != nil { - return nil, err - } - report.FullReport, err = getBytesField(mp, "FullReport") - if err != nil { - return nil, err - } - report.ReportContext, err = getBytesField(mp, "ReportContext") - if err != nil { - return nil, err - } - sigListVal, ok := mp.Underlying["Signatures"] - if !ok { - return nil, errors.New("missing Signatures key") - } - sigList, ok := sigListVal.(*values.List) - if !ok { - return nil, errors.New("expected list type for Signatures") - } - for idx, sig := range sigList.Underlying { - sigVal, ok := sig.(*values.Bytes) - if !ok { - return nil, fmt.Errorf("expected bytes type for signature %d", idx) - } - report.Signatures = append(report.Signatures, sigVal.Underlying) - } - result = append(result, report) - } - - return result, nil -} - -func getStringField(mp *values.Map, key string) (string, error) { - val, ok := mp.Underlying[key] - if !ok { - return "", fmt.Errorf("missing key %s", key) - } - strVal, ok := val.(*values.String) - if !ok { - return "", fmt.Errorf("expected string type for key %s", key) - } - return strVal.Underlying, nil -} - -func getBytesField(mp *values.Map, key string) ([]byte, error) { - val, ok := mp.Underlying[key] - if !ok { - return nil, fmt.Errorf("missing key %s", key) - } - byleVal, ok := val.(*values.Bytes) - if !ok { - return nil, fmt.Errorf("expected bytes type for key %s", key) - } - return byleVal.Underlying, nil -} diff --git a/pkg/capabilities/datastreams/types_test.go b/pkg/capabilities/datastreams/types_test.go deleted file mode 100644 index f642bb4aa1..0000000000 --- a/pkg/capabilities/datastreams/types_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package datastreams_test - -import ( - "crypto/rand" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/datastreams" - "github.com/smartcontractkit/chainlink-protos/cre/go/values" -) - -const ( - feedIDAStr = "0x1111111111111111111100000000000000000000000000000000000000000000" - feedIDBStr = "0x2222222222222222222200000000000000000000000000000000000000000000" - testFullReportAHex = "0x1111aabbccddeeff" - testFullReportBHex = "0x2222aabbccddeeff" -) - -func TestFeedID_Validate(t *testing.T) { - _, err := datastreams.NewFeedID("012345678901234567890123456789012345678901234567890123456789000000") - require.Error(t, err) - - _, err = datastreams.NewFeedID("0x1234") - require.Error(t, err) - - _, err = datastreams.NewFeedID("0x123zzz") - require.Error(t, err) - - _, err = datastreams.NewFeedID("0x0001013ebd4ed3f5889FB5a8a52b42675c60c1a8c42bc79eaa72dcd922ac4292") - require.Error(t, err) - - _, err = datastreams.NewFeedID(feedIDAStr) - require.NoError(t, err) -} - -func Test_UnwrapStreamsTriggerEventToFeedReportList(t *testing.T) { - feedReports := []datastreams.FeedReport{ - { - FeedID: feedIDAStr, - FullReport: randomByteArray(t, 1000), - ReportContext: randomByteArray(t, 96), - Signatures: [][]byte{randomByteArray(t, 65), randomByteArray(t, 65)}, - }, - { - FeedID: feedIDBStr, - FullReport: randomByteArray(t, 1000), - ReportContext: randomByteArray(t, 96), - Signatures: [][]byte{randomByteArray(t, 65), randomByteArray(t, 65)}, - }, - } - - payload := datastreams.StreamsTriggerEvent{ - Payload: feedReports, - } - wrapped, err := values.Wrap(payload) - require.NoError(t, err) - - unwrapped, err := datastreams.UnwrapStreamsTriggerEventToFeedReportList(wrapped) - require.NoError(t, err) - require.Equal(t, feedReports, unwrapped) -} - -func randomByteArray(t *testing.T, n int) []byte { - b := make([]byte, n) - _, err := rand.Read(b[:]) - require.NoError(t, err) - return b -} diff --git a/pkg/capabilities/triggers/cron/cron_trigger-schema.json b/pkg/capabilities/triggers/cron/cron_trigger-schema.json deleted file mode 100644 index f170b24f48..0000000000 --- a/pkg/capabilities/triggers/cron/cron_trigger-schema.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/smartcontractkit/chainlink-common/pkg/capabilities/triggers/cron/cron-trigger@1.0.0", - "$defs": { - "Payload": { - "type": "object", - "properties": { - "ScheduledExecutionTime": { - "type": "string", - "description": "Time that cron trigger's task execution had been scheduled to occur (RFC3339Nano formatted)" - } - }, - "required": ["ScheduledExecutionTime"], - "additionalProperties": false - }, - "Config": { - "type": "object", - "properties": { - "schedule": { - "type": "string" - } - }, - "required": ["schedule"], - "additionalProperties": false - } - }, - "type": "object", - "properties": { - "config": { - "$ref": "#/$defs/Config" - }, - "outputs": { - "$ref": "#/$defs/Payload" - } - }, - "required": ["config", "outputs"], - "additionalProperties": false, - "description": "A trigger that uses a cron schedule to run periodically at fixed times, dates, or intervals." -} \ No newline at end of file diff --git a/pkg/capabilities/triggers/cron/cron_trigger_generated.go b/pkg/capabilities/triggers/cron/cron_trigger_generated.go deleted file mode 100644 index a054729a4b..0000000000 --- a/pkg/capabilities/triggers/cron/cron_trigger_generated.go +++ /dev/null @@ -1,86 +0,0 @@ -// Code generated by github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli, DO NOT EDIT. - -package cron - -import ( - "encoding/json" - "fmt" -) - -type Config struct { - // Schedule corresponds to the JSON schema field "schedule". - Schedule string `json:"schedule" yaml:"schedule" mapstructure:"schedule"` -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *Config) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - if _, ok := raw["schedule"]; raw != nil && !ok { - return fmt.Errorf("field schedule in Config: required") - } - type Plain Config - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - *j = Config(plain) - return nil -} - -type Payload struct { - // Time that cron trigger's task execution had been scheduled to occur - // (RFC3339Nano formatted) - ScheduledExecutionTime string `json:"ScheduledExecutionTime" yaml:"ScheduledExecutionTime" mapstructure:"ScheduledExecutionTime"` -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *Payload) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - if _, ok := raw["ScheduledExecutionTime"]; raw != nil && !ok { - return fmt.Errorf("field ScheduledExecutionTime in Payload: required") - } - type Plain Payload - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - *j = Payload(plain) - return nil -} - -// A trigger that uses a cron schedule to run periodically at fixed times, dates, -// or intervals. -type Trigger struct { - // Config corresponds to the JSON schema field "config". - Config Config `json:"config" yaml:"config" mapstructure:"config"` - - // Outputs corresponds to the JSON schema field "outputs". - Outputs Payload `json:"outputs" yaml:"outputs" mapstructure:"outputs"` -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *Trigger) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - if _, ok := raw["config"]; raw != nil && !ok { - return fmt.Errorf("field config in Trigger: required") - } - if _, ok := raw["outputs"]; raw != nil && !ok { - return fmt.Errorf("field outputs in Trigger: required") - } - type Plain Trigger - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - *j = Trigger(plain) - return nil -} diff --git a/pkg/capabilities/triggers/cron/crontest/trigger_mock_generated.go b/pkg/capabilities/triggers/cron/crontest/trigger_mock_generated.go deleted file mode 100644 index ad683b1d52..0000000000 --- a/pkg/capabilities/triggers/cron/crontest/trigger_mock_generated.go +++ /dev/null @@ -1,17 +0,0 @@ -// Code generated by github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli, DO NOT EDIT. - -// Code generated by github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli, DO NOT EDIT. - -package crontest - -import ( - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/triggers/cron" - "github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk/testutils" -) - -// Trigger registers a new capability mock with the runner -func Trigger(runner *testutils.Runner, fn func() (cron.Payload, error)) *testutils.TriggerMock[cron.Payload] { - mock := testutils.MockTrigger[cron.Payload]("cron-trigger@1.0.0", fn) - runner.MockCapability("cron-trigger@1.0.0", nil, mock) - return mock -} diff --git a/pkg/capabilities/triggers/cron/trigger_builders_generated.go b/pkg/capabilities/triggers/cron/trigger_builders_generated.go deleted file mode 100644 index 7a7dc7f320..0000000000 --- a/pkg/capabilities/triggers/cron/trigger_builders_generated.go +++ /dev/null @@ -1,73 +0,0 @@ -// Code generated by github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli, DO NOT EDIT. - -package cron - -import ( - "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - "github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk" -) - -func (cfg Config) New(w *sdk.WorkflowSpecFactory) PayloadCap { - ref := "trigger" - def := sdk.StepDefinition{ - ID: "cron-trigger@1.0.0", Ref: ref, - Inputs: sdk.StepInputs{}, - Config: map[string]any{ - "schedule": cfg.Schedule, - }, - CapabilityType: capabilities.CapabilityTypeTrigger, - } - - step := sdk.Step[Payload]{Definition: def} - raw := step.AddTo(w) - return PayloadWrapper(raw) -} - -// PayloadWrapper allows access to field from an sdk.CapDefinition[Payload] -func PayloadWrapper(raw sdk.CapDefinition[Payload]) PayloadCap { - wrapped, ok := raw.(PayloadCap) - if ok { - return wrapped - } - return &payloadCap{CapDefinition: raw} -} - -type PayloadCap interface { - sdk.CapDefinition[Payload] - ScheduledExecutionTime() sdk.CapDefinition[string] - private() -} - -type payloadCap struct { - sdk.CapDefinition[Payload] -} - -func (*payloadCap) private() {} -func (c *payloadCap) ScheduledExecutionTime() sdk.CapDefinition[string] { - return sdk.AccessField[Payload, string](c.CapDefinition, "ScheduledExecutionTime") -} - -func ConstantPayload(value Payload) PayloadCap { - return &payloadCap{CapDefinition: sdk.ConstantDefinition(value)} -} - -func NewPayloadFromFields( - scheduledExecutionTime sdk.CapDefinition[string]) PayloadCap { - return &simplePayload{ - CapDefinition: sdk.ComponentCapDefinition[Payload]{ - "ScheduledExecutionTime": scheduledExecutionTime.Ref(), - }, - scheduledExecutionTime: scheduledExecutionTime, - } -} - -type simplePayload struct { - sdk.CapDefinition[Payload] - scheduledExecutionTime sdk.CapDefinition[string] -} - -func (c *simplePayload) ScheduledExecutionTime() sdk.CapDefinition[string] { - return c.scheduledExecutionTime -} - -func (c *simplePayload) private() {} diff --git a/pkg/capabilities/triggers/mercury_remote_aggregator.go b/pkg/capabilities/triggers/mercury_remote_aggregator.go deleted file mode 100644 index 2e888876b4..0000000000 --- a/pkg/capabilities/triggers/mercury_remote_aggregator.go +++ /dev/null @@ -1,97 +0,0 @@ -package triggers - -import ( - "errors" - "sort" - - "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/datastreams" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" - "github.com/smartcontractkit/chainlink-common/pkg/logger" -) - -type mercuryRemoteAggregator struct { - codec datastreams.ReportCodec - allowedSigners [][]byte - minRequiredSignatures int - previousLatestReports map[datastreams.FeedID]datastreams.FeedReport - capID string - lggr logger.Logger -} - -// This aggregator is used by TriggerSubscriber to aggregate trigger events from multiple remote nodes. -// NOTE: Once Mercury supports parallel composition (and thus guarantee identical sets of reports), -// this will be replaced by the default MODE aggregator. -func NewMercuryRemoteAggregator(codec datastreams.ReportCodec, allowedSigners [][]byte, minRequiredSignatures int, capID string, lggr logger.Logger) *mercuryRemoteAggregator { - if allowedSigners == nil { - allowedSigners = [][]byte{} - } - return &mercuryRemoteAggregator{ - codec: codec, - allowedSigners: allowedSigners, - minRequiredSignatures: minRequiredSignatures, - previousLatestReports: make(map[datastreams.FeedID]datastreams.FeedReport), - capID: capID, - lggr: lggr, - } -} - -func (a *mercuryRemoteAggregator) Aggregate(triggerEventID string, responses [][]byte) (capabilities.TriggerResponse, error) { - latestReports := make(map[datastreams.FeedID]datastreams.FeedReport) - latestGlobalTs := int64(0) // to be used as the timestamp of the combined trigger event - for _, response := range responses { - unmarshaled, err := pb.UnmarshalTriggerResponse(response) - if err != nil { - a.lggr.Errorw("could not unmarshal one of capability responses (faulty sender?)", "error", err) - continue - } - feedReports, err := a.codec.Unwrap(unmarshaled.Event.Outputs) - if err != nil { - a.lggr.Errorw("could not unwrap one of capability responses", "error", err) - continue - } - // save latest valid report for each feed ID - for _, report := range feedReports { - latestReport, ok := latestReports[datastreams.FeedID(report.FeedID)] - if !ok { - // on first occurrence of a feed ID, check if we saw it in any of the past events - latestReport, ok = a.previousLatestReports[datastreams.FeedID(report.FeedID)] - if ok { - latestReports[datastreams.FeedID(report.FeedID)] = latestReport - if latestReport.ObservationTimestamp > latestGlobalTs { - latestGlobalTs = report.ObservationTimestamp - } - } - } - if !ok || report.ObservationTimestamp > latestReport.ObservationTimestamp { - // lazy signature validation - if err := a.codec.Validate(report, a.allowedSigners, a.minRequiredSignatures); err != nil { - a.lggr.Errorw("invalid report", "error", err) - } else { - latestReports[datastreams.FeedID(report.FeedID)] = report - a.previousLatestReports[datastreams.FeedID(report.FeedID)] = report - if report.ObservationTimestamp > latestGlobalTs { - latestGlobalTs = report.ObservationTimestamp - } - } - } - } - } - if len(latestReports) == 0 { - return capabilities.TriggerResponse{}, errors.New("no valid reports found") - } - reportList := []datastreams.FeedReport{} - allIDs := []string{} - for _, report := range latestReports { - allIDs = append(allIDs, report.FeedID) - } - sort.Strings(allIDs) - for _, feedID := range allIDs { - reportList = append(reportList, latestReports[datastreams.FeedID(feedID)]) - } - meta := datastreams.Metadata{ - Signers: a.allowedSigners, - MinRequiredSignatures: a.minRequiredSignatures, - } - return WrapReports(reportList, triggerEventID, latestGlobalTs, meta, a.capID) -} diff --git a/pkg/capabilities/triggers/mercury_remote_aggregator_test.go b/pkg/capabilities/triggers/mercury_remote_aggregator_test.go deleted file mode 100644 index 2f3660c57a..0000000000 --- a/pkg/capabilities/triggers/mercury_remote_aggregator_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package triggers - -import ( - "math/big" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/datastreams" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-protos/cre/go/values" -) - -const ( - eventID = "ev_id_1" - rawReport1 = "abcd" - rawReport2 = "efgh" - capID = "streams-trigger@3.2.1" -) - -type testMercuryCodec struct { -} - -func (c testMercuryCodec) Unwrap(wrapped values.Value) ([]datastreams.FeedReport, error) { - dest := datastreams.StreamsTriggerEvent{} - err := wrapped.UnwrapTo(&dest) - return dest.Payload, err -} - -func (c testMercuryCodec) Validate(report datastreams.FeedReport, _ [][]byte, _ int) error { - return nil -} - -func (c testMercuryCodec) Wrap(reports []datastreams.FeedReport) (values.Value, error) { - return values.Wrap(reports) -} - -func TestMercuryRemoteAggregator(t *testing.T) { - agg := NewMercuryRemoteAggregator(testMercuryCodec{}, nil, 0, capID, logger.Nop()) - signatures := [][]byte{{1, 2, 3}} - - feed1Old := datastreams.FeedReport{ - FeedID: feedOne, - BenchmarkPrice: big.NewInt(100).Bytes(), - ObservationTimestamp: 100, - FullReport: []byte(rawReport1), - ReportContext: []byte{}, - Signatures: signatures, - } - feed1New := datastreams.FeedReport{ - FeedID: feedOne, - BenchmarkPrice: big.NewInt(200).Bytes(), - ObservationTimestamp: 200, - FullReport: []byte(rawReport1), - ReportContext: []byte{}, - Signatures: signatures, - } - feed2Old := datastreams.FeedReport{ - FeedID: feedTwo, - BenchmarkPrice: big.NewInt(300).Bytes(), - ObservationTimestamp: 300, - FullReport: []byte(rawReport2), - ReportContext: []byte{}, - Signatures: signatures, - } - feed2New := datastreams.FeedReport{ - FeedID: feedTwo, - BenchmarkPrice: big.NewInt(400).Bytes(), - ObservationTimestamp: 400, - FullReport: []byte(rawReport2), - ReportContext: []byte{}, - Signatures: signatures, - } - - rawNode1Resp := getRawResponse(t, []datastreams.FeedReport{feed1Old, feed2New}, 400) - rawNode2Resp := getRawResponse(t, []datastreams.FeedReport{feed1New, feed2Old}, 300) - - // aggregator should return latest value for each feedID - aggResponse, err := agg.Aggregate(eventID, [][]byte{rawNode1Resp, rawNode2Resp}) - require.NoError(t, err) - aggEvent := aggResponse.Event - decodedReports, err := testMercuryCodec{}.Unwrap(aggEvent.Outputs) - require.NoError(t, err) - - require.Len(t, decodedReports, 2) - require.Equal(t, feed1New, decodedReports[0]) - require.Equal(t, feed2New, decodedReports[1]) - - // never roll back to an older report - rawNode3Resp := getRawResponse(t, []datastreams.FeedReport{feed1Old, feed2Old}, 400) - aggResponse, err = agg.Aggregate(eventID, [][]byte{rawNode3Resp}) - require.NoError(t, err) - decodedReports, err = testMercuryCodec{}.Unwrap(aggResponse.Event.Outputs) - require.NoError(t, err) - - require.Len(t, decodedReports, 2) - require.Equal(t, feed1New, decodedReports[0]) - require.Equal(t, feed2New, decodedReports[1]) -} - -func getRawResponse(t *testing.T, reports []datastreams.FeedReport, timestamp int64) []byte { - resp, err := WrapReports(reports, eventID, timestamp, datastreams.Metadata{}, capID) - require.NoError(t, err) - rawResp, err := pb.MarshalTriggerResponse(resp) - require.NoError(t, err) - return rawResp -} diff --git a/pkg/capabilities/triggers/mercury_trigger.go b/pkg/capabilities/triggers/mercury_trigger.go deleted file mode 100644 index 42a5172e14..0000000000 --- a/pkg/capabilities/triggers/mercury_trigger.go +++ /dev/null @@ -1,260 +0,0 @@ -package triggers - -import ( - "context" - "encoding/json" - "fmt" - "strconv" - "sync" - "time" - - "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/datastreams" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/triggers/streams" - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/services" - "github.com/smartcontractkit/chainlink-protos/cre/go/values" -) - -const ( - defaultCapabilityName = "streams-trigger" - defaultCapabilityVersion = "1.1.0" - defaultTickerResolutionMs = 1000 - // TODO pending capabilities configuration implementation - this should be configurable with a sensible default - defaultSendChannelBufferSize = 1000 -) - -// This Trigger Service allows for the registration and deregistration of triggers. You can also send reports to the service. -type MercuryTriggerService struct { - capabilities.CapabilityInfo - tickerResolutionMs int64 - subscribers map[string]*subscriber - latestReports map[datastreams.FeedID]datastreams.FeedReport - mu sync.Mutex - stopCh services.StopChan - wg sync.WaitGroup - lggr logger.Logger - metaOverride datastreams.Metadata // usually empty, but set to a value in mock trigger -} - -var _ capabilities.TriggerCapability = (*MercuryTriggerService)(nil) -var _ services.Service = &MercuryTriggerService{} - -type subscriber struct { - ch chan<- capabilities.TriggerResponse - workflowID string - config streams.TriggerConfig -} - -// Mercury Trigger will send events to each subscriber every MaxFrequencyMs (configurable per subscriber). -// Event generation happens whenever local unix time is a multiple of tickerResolutionMs. Therefore, -// all subscribers' MaxFrequencyMs values need to be a multiple of tickerResolutionMs. -func NewMercuryTriggerService(tickerResolutionMs int64, capName string, capVersion string, lggr logger.Logger) (*MercuryTriggerService, error) { - if tickerResolutionMs == 0 { - tickerResolutionMs = defaultTickerResolutionMs - } - if capName == "" { - capName = defaultCapabilityName - } - if capVersion == "" { - capVersion = defaultCapabilityVersion - } - capInfo, err := capabilities.NewCapabilityInfo( - capName+"@"+capVersion, - capabilities.CapabilityTypeTrigger, - "Streams Trigger", - ) - if err != nil { - return nil, err - } - return &MercuryTriggerService{ - CapabilityInfo: capInfo, - tickerResolutionMs: tickerResolutionMs, - subscribers: make(map[string]*subscriber), - latestReports: make(map[datastreams.FeedID]datastreams.FeedReport), - stopCh: make(services.StopChan), - lggr: logger.Named(lggr, "MercuryTriggerService")}, nil -} - -func (o *MercuryTriggerService) SetMetaOverride(meta datastreams.Metadata) { - o.metaOverride = meta -} - -func (o *MercuryTriggerService) ProcessReport(reports []datastreams.FeedReport) error { - o.mu.Lock() - defer o.mu.Unlock() - o.lggr.Debugw("ProcessReport", "nReports", len(reports)) - for _, report := range reports { - feedID := datastreams.FeedID(report.FeedID) - o.latestReports[feedID] = report - } - return nil -} - -func (o *MercuryTriggerService) AckEvent(ctx context.Context, triggerID string, eventID string, method string) error { - return nil -} - -func (o *MercuryTriggerService) RegisterTrigger(ctx context.Context, req capabilities.TriggerRegistrationRequest) (<-chan capabilities.TriggerResponse, error) { - wid := req.Metadata.WorkflowID - - o.mu.Lock() - defer o.mu.Unlock() - - config, err := o.ValidateConfig(req.Config) - if err != nil { - return nil, err - } - - // If triggerId is already registered, return an error - if _, ok := o.subscribers[req.TriggerID]; ok { - return nil, fmt.Errorf("triggerId %s already registered", o.ID) - } - - if int64(config.MaxFrequencyMs)%o.tickerResolutionMs != 0 { - return nil, fmt.Errorf("MaxFrequencyMs must be a multiple of %d", o.tickerResolutionMs) - } - - ch := make(chan capabilities.TriggerResponse, defaultSendChannelBufferSize) - o.subscribers[req.TriggerID] = - &subscriber{ - ch: ch, - workflowID: wid, - config: *config, - } - return ch, nil -} - -func (o *MercuryTriggerService) ValidateConfig(config *values.Map) (*streams.TriggerConfig, error) { - cfg := &streams.TriggerConfig{} - if err := config.UnwrapTo(cfg); err != nil { - return nil, err - } - - // TODO QOL improvement, the generator for the builders can add a validate function that just copies code after unmarshalling to Plain - b, _ := json.Marshal(cfg) - if err := json.Unmarshal(b, cfg); err != nil { - return nil, err - } - - return cfg, nil -} - -func (o *MercuryTriggerService) UnregisterTrigger(ctx context.Context, req capabilities.TriggerRegistrationRequest) error { - o.mu.Lock() - defer o.mu.Unlock() - - subscriber, ok := o.subscribers[req.TriggerID] - if !ok { - return fmt.Errorf("triggerId %s not registered", o.ID) - } - close(subscriber.ch) - delete(o.subscribers, req.TriggerID) - return nil -} - -func (o *MercuryTriggerService) loop() { - defer o.wg.Done() - now := time.Now().UnixMilli() - nextWait := o.tickerResolutionMs - now%o.tickerResolutionMs - - for { - select { - case <-o.stopCh: - return - case <-time.After(time.Duration(nextWait) * time.Millisecond): - startTs := time.Now().UnixMilli() - // find closest timestamp that is a multiple of o.tickerResolutionMs - aligned := (startTs + o.tickerResolutionMs/2) / o.tickerResolutionMs * o.tickerResolutionMs - o.process(aligned) - endTs := time.Now().UnixMilli() - if endTs-startTs > o.tickerResolutionMs { - o.lggr.Errorw("processing took longer than ticker resolution", "duration", endTs-startTs, "tickerResolutionMs", o.tickerResolutionMs) - } - nextWait = getNextWaitIntervalMs(aligned, o.tickerResolutionMs, endTs) - } - } -} - -func getNextWaitIntervalMs(lastTs, tickerResolutionMs, currentTs int64) int64 { - desiredNext := lastTs + tickerResolutionMs - nextWait := max(desiredNext-currentTs, 0) - return nextWait -} - -func (o *MercuryTriggerService) process(timestamp int64) { - o.mu.Lock() - defer o.mu.Unlock() - for _, sub := range o.subscribers { - if timestamp%int64(sub.config.MaxFrequencyMs) == 0 { - reportList := make([]datastreams.FeedReport, 0) - for _, feedID := range sub.config.FeedIds { - if latest, ok := o.latestReports[datastreams.FeedID(feedID)]; ok { - reportList = append(reportList, latest) - } - } - - // use 32-byte-padded timestamp as EventID (human-readable) - eventID := fmt.Sprintf("streams_%024s", strconv.FormatInt(timestamp, 10)) - capabilityResponse, err := WrapReports(reportList, eventID, timestamp, o.metaOverride, o.ID) - if err != nil { - o.lggr.Errorw("error wrapping reports", "err", err) - continue - } - - o.lggr.Debugw("ProcessReport pushing event", "nReports", len(reportList), "eventID", eventID) - select { - case sub.ch <- capabilityResponse: - default: - o.lggr.Errorw("subscriber channel full, dropping event", "eventID", eventID, "workflowID", sub.workflowID) - } - } - } -} - -func WrapReports(reportList []datastreams.FeedReport, eventID string, timestamp int64, meta datastreams.Metadata, capID string) (capabilities.TriggerResponse, error) { - out := datastreams.StreamsTriggerEvent{ - Payload: reportList, - Metadata: meta, - Timestamp: timestamp, - } - outputsv, err := values.WrapMap(out) - if err != nil { - return capabilities.TriggerResponse{}, err - } - - // Create a new TriggerRegistrationResponse with the MercuryTriggerEvent - return capabilities.TriggerResponse{ - Event: capabilities.TriggerEvent{ - TriggerType: capID, - ID: eventID, - Outputs: outputsv, - }, - }, nil -} - -func (o *MercuryTriggerService) Start(ctx context.Context) error { - o.wg.Add(1) - go o.loop() - o.lggr.Info("MercuryTriggerService started") - return nil -} - -func (o *MercuryTriggerService) Close() error { - close(o.stopCh) - o.wg.Wait() - o.lggr.Info("MercuryTriggerService closed") - return nil -} - -func (o *MercuryTriggerService) Ready() error { - return nil -} - -func (o *MercuryTriggerService) HealthReport() map[string]error { - return nil -} - -func (o *MercuryTriggerService) Name() string { - return o.lggr.Name() -} diff --git a/pkg/capabilities/triggers/mercury_trigger_test.go b/pkg/capabilities/triggers/mercury_trigger_test.go deleted file mode 100644 index 57d40ad759..0000000000 --- a/pkg/capabilities/triggers/mercury_trigger_test.go +++ /dev/null @@ -1,373 +0,0 @@ -package triggers - -import ( - "context" - "crypto/rand" - "encoding/hex" - "math/big" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/datastreams" - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-protos/cre/go/values" -) - -// registerTrigger will do the following: -// -// 1. Register a trigger with the given feedIDs and triggerID -// 2. Return the trigger events channel, registerUnregisterRequest, and test context -func registerTrigger( - ctx context.Context, - t *testing.T, - ts *MercuryTriggerService, - feedIDs []string, - triggerID string, -) ( - <-chan capabilities.TriggerResponse, - capabilities.TriggerRegistrationRequest, -) { - config, err := values.NewMap(map[string]any{ - "feedIds": feedIDs, - "maxFrequencyMs": 100, - }) - require.NoError(t, err) - - requestMetadata := capabilities.RequestMetadata{ - WorkflowID: "workflow-id-1", - } - registerRequest := capabilities.TriggerRegistrationRequest{ - Metadata: requestMetadata, - TriggerID: triggerID, - Config: config, - } - triggerEventsCh, err := ts.RegisterTrigger(ctx, registerRequest) - require.NoError(t, err) - - return triggerEventsCh, registerRequest -} - -const ( - triggerID = "streams-trigger@4.5.6" - feedOne = "0x1111111111111111111100000000000000000000000000000000000000000000" - feedTwo = "0x2222222222222222222200000000000000000000000000000000000000000000" - feedThree = "0x3333333333333333333300000000000000000000000000000000000000000000" - feedFour = "0x4444444444444444444400000000000000000000000000000000000000000000" - feedFive = "0x5555555555555555555500000000000000000000000000000000000000000000" -) - -func TestMercuryTrigger(t *testing.T) { - ts, err := NewMercuryTriggerService(100, "", "4.5.6", logger.Nop()) - require.NoError(t, err) - ctx := t.Context() - err = ts.Start(ctx) - require.NoError(t, err) - // use registerTriggerHelper to register a trigger - callback, registerUnregisterRequest := registerTrigger( - ctx, - t, - ts, - []string{feedOne}, - "test-id-1", - ) - - // Send events to trigger and check for them in the callback - mfr := []datastreams.FeedReport{ - { - FeedID: feedOne, - FullReport: []byte("0x1234"), - BenchmarkPrice: big.NewInt(2).Bytes(), - ObservationTimestamp: 3, - Signatures: [][]byte{}, - }, - } - err = ts.ProcessReport(mfr) - assert.NoError(t, err) - msg := <-callback - triggerEvent, reports := upwrapTriggerEvent(t, msg) - assert.Equal(t, triggerID, triggerEvent.TriggerType) - assert.Len(t, reports, 1) - assert.Equal(t, mfr[0], reports[0]) - - // Unregister the trigger and check that events no longer go on the callback - require.NoError(t, ts.UnregisterTrigger(ctx, registerUnregisterRequest)) - err = ts.ProcessReport(mfr) - require.NoError(t, err) - require.Empty(t, callback) - require.NoError(t, ts.Close()) -} - -func TestMultipleMercuryTriggers(t *testing.T) { - ts, err := NewMercuryTriggerService(100, "", "4.5.6", logger.Nop()) - require.NoError(t, err) - ctx := t.Context() - err = ts.Start(ctx) - require.NoError(t, err) - callback1, cr1 := registerTrigger( - ctx, - t, - ts, - []string{ - feedOne, - feedThree, - feedFour, - }, - "test-id-1", - ) - - callback2, cr2 := registerTrigger( - ctx, - t, - ts, - []string{ - feedTwo, - feedThree, - feedFive, - }, - "test-id-2", - ) - - // Send events to trigger and check for them in the callback - mfr1 := []datastreams.FeedReport{ - { - FeedID: feedOne, - FullReport: []byte("0x1234"), - BenchmarkPrice: big.NewInt(20).Bytes(), - ObservationTimestamp: 5, - Signatures: [][]byte{}, - }, - { - FeedID: feedThree, - FullReport: []byte("0x1234"), - BenchmarkPrice: big.NewInt(25).Bytes(), - ObservationTimestamp: 8, - Signatures: [][]byte{}, - }, - { - FeedID: feedTwo, - FullReport: []byte("0x1234"), - BenchmarkPrice: big.NewInt(30).Bytes(), - ObservationTimestamp: 10, - Signatures: [][]byte{}, - }, - { - FeedID: feedFour, - FullReport: []byte("0x1234"), - BenchmarkPrice: big.NewInt(40).Bytes(), - ObservationTimestamp: 15, - Signatures: [][]byte{}, - }, - } - - err = ts.ProcessReport(mfr1) - assert.NoError(t, err) - - msg := <-callback1 - triggerEvent, reports := upwrapTriggerEvent(t, msg) - assert.Equal(t, triggerID, triggerEvent.TriggerType) - assert.Len(t, reports, 3) - assert.Equal(t, mfr1[0], reports[0]) - assert.Equal(t, mfr1[1], reports[1]) - assert.Equal(t, mfr1[3], reports[2]) - - msg = <-callback2 - triggerEvent, reports = upwrapTriggerEvent(t, msg) - assert.Equal(t, triggerID, triggerEvent.TriggerType) - assert.Len(t, reports, 2) - assert.Equal(t, mfr1[2], reports[0]) - assert.Equal(t, mfr1[1], reports[1]) - - require.NoError(t, ts.UnregisterTrigger(ctx, cr1)) - mfr2 := []datastreams.FeedReport{ - { - FeedID: feedThree, - FullReport: []byte("0x1234"), - BenchmarkPrice: big.NewInt(50).Bytes(), - ObservationTimestamp: 20, - }, - } - err = ts.ProcessReport(mfr2) - assert.NoError(t, err) - - retryCount := 0 - for rMsg := range callback2 { - triggerEvent, reports = upwrapTriggerEvent(t, rMsg) - require.NoError(t, err) - require.Len(t, reports, 2) - require.Equal(t, triggerID, triggerEvent.TriggerType) - price := big.NewInt(0).SetBytes(reports[1].BenchmarkPrice) - if price.Cmp(big.NewInt(50)) == 0 { - // expect to eventually get updated feed value - break - } - require.Less(t, retryCount, 100) - retryCount++ - } - - require.NoError(t, ts.UnregisterTrigger(ctx, cr2)) - err = ts.ProcessReport(mfr1) - assert.NoError(t, err) - assert.Empty(t, callback1) - assert.Empty(t, callback2) - require.NoError(t, ts.Close()) -} - -func TestMercuryTrigger_RegisterTriggerErrors(t *testing.T) { - ts, err := NewMercuryTriggerService(100, "", "4.5.6", logger.Nop()) - require.NoError(t, err) - ctx := t.Context() - require.NoError(t, ts.Start(ctx)) - - cm := map[string]any{ - "feedIds": []string{feedOne}, - "maxFrequencyMs": 90, - } - configWrapped, err := values.NewMap(cm) - require.NoError(t, err) - - cr := capabilities.TriggerRegistrationRequest{ - Metadata: capabilities.RequestMetadata{ - WorkflowID: "workflow-id-1", - }, - Config: configWrapped, - TriggerID: "test-id-1", - } - _, err = ts.RegisterTrigger(ctx, cr) - require.Error(t, err) - - cm = map[string]any{ - "feedIds": []string{feedOne}, - "maxFrequencyMs": 0, - } - configWrapped, err = values.NewMap(cm) - require.NoError(t, err) - cr.Config = configWrapped - _, err = ts.RegisterTrigger(ctx, cr) - require.Error(t, err) - - cm = map[string]any{ - "feedIds": []string{}, - "maxFrequencyMs": 1000, - } - configWrapped, err = values.NewMap(cm) - require.NoError(t, err) - cr.Config = configWrapped - _, err = ts.RegisterTrigger(ctx, cr) - require.Error(t, err) - - require.NoError(t, ts.Close()) -} - -func TestGetNextWaitIntervalMs(t *testing.T) { - // getNextWaitIntervalMs args = (lastTs, tickerResolutionMs, currentTs) - - // expected cases - assert.Equal(t, int64(900), getNextWaitIntervalMs(12000, 1000, 12100)) - assert.Equal(t, int64(200), getNextWaitIntervalMs(12000, 1000, 12800)) - - // slow processing - assert.Equal(t, int64(0), getNextWaitIntervalMs(12000, 1000, 13000)) - assert.Equal(t, int64(0), getNextWaitIntervalMs(12000, 1000, 14600)) -} - -func upwrapTriggerEvent(t *testing.T, req capabilities.TriggerResponse) (capabilities.TriggerEvent, []datastreams.FeedReport) { - require.NotNil(t, req.Event.Outputs) - mercuryReports, err := testMercuryCodec{}.Unwrap(req.Event.Outputs) - require.NoError(t, err) - return req.Event, mercuryReports -} - -func TestMercuryTrigger_ConfigValidation(t *testing.T) { - var newConfig = func(t *testing.T, feedIDs []string, maxFrequencyMs int) *values.Map { - cm := map[string]any{ - "feedIds": feedIDs, - "maxFrequencyMs": maxFrequencyMs, - } - configWrapped, err := values.NewMap(cm) - require.NoError(t, err) - - return configWrapped - } - - var newConfigSingleFeed = func(t *testing.T, feedID string) *values.Map { - return newConfig(t, []string{feedID}, 1000) - } - - ts, err := NewMercuryTriggerService(1000, "", "4.5.6", logger.Nop()) - require.NoError(t, err) - rawConf := newConfigSingleFeed(t, "012345678901234567890123456789012345678901234567890123456789000000") - conf, err := ts.ValidateConfig(rawConf) - require.Error(t, err) - require.Empty(t, conf) - - rawConf = newConfigSingleFeed(t, "0x1234") - conf, err = ts.ValidateConfig(rawConf) - require.Error(t, err) - require.Empty(t, conf) - - rawConf = newConfigSingleFeed(t, "0x123zzz") - conf, err = ts.ValidateConfig(rawConf) - require.Error(t, err) - require.Empty(t, conf) - - rawConf = newConfigSingleFeed(t, "0x0001013ebd4ed3f5889FB5a8a52b42675c60c1a8c42bc79eaa72dcd922ac4292") - conf, err = ts.ValidateConfig(rawConf) - require.Error(t, err) - require.Empty(t, conf) - - passingFeedID := "0x0001013ebd4ed3f5889fb5a8a52b42675c60c1a8c42bc79eaa72dcd922ac4292" - // test maxfreq < 1 - rawConf = newConfig(t, []string{passingFeedID}, 0) - conf, err = ts.ValidateConfig(rawConf) - require.Error(t, err) - require.Empty(t, conf) - - rawConf = newConfig(t, []string{passingFeedID}, -1) - conf, err = ts.ValidateConfig(rawConf) - require.Error(t, err) - require.Empty(t, conf) - - rawConf = newConfigSingleFeed(t, passingFeedID) - conf, err = ts.ValidateConfig(rawConf) - require.NoError(t, err) - require.NotEmpty(t, conf) -} - -func TestMercuryTrigger_WrapReports(t *testing.T) { - S := 31 // signers - P := 50 // feeds - B := 1000 // report size in bytes - meta := datastreams.Metadata{} - for range S { - meta.Signers = append(meta.Signers, randomByteArray(t, 20)) - } - reportList := []datastreams.FeedReport{} - for range P { - signatures := [][]byte{} - for range S { - signatures = append(signatures, randomByteArray(t, 65)) - } - reportList = append(reportList, datastreams.FeedReport{ - FeedID: "0x" + hex.EncodeToString(randomByteArray(t, 32)), - FullReport: randomByteArray(t, B), - ReportContext: randomByteArray(t, 96), - Signatures: signatures, - BenchmarkPrice: big.NewInt(56789).Bytes(), - ObservationTimestamp: 876543, - }) - } - wrapped, err := WrapReports(reportList, "event_id", 1234, meta, triggerID) - require.NoError(t, err) - require.NotNil(t, wrapped.Event) - require.Len(t, wrapped.Event.Outputs.Underlying["Payload"].(*values.List).Underlying, P) -} - -func randomByteArray(t *testing.T, n int) []byte { - b := make([]byte, n) - _, err := rand.Read(b[:]) - require.NoError(t, err) - return b -} diff --git a/pkg/capabilities/triggers/on_demand_trigger.go b/pkg/capabilities/triggers/on_demand_trigger.go index 54660ce9aa..a71068d719 100644 --- a/pkg/capabilities/triggers/on_demand_trigger.go +++ b/pkg/capabilities/triggers/on_demand_trigger.go @@ -81,7 +81,7 @@ func (o *OnDemand) RegisterTrigger(ctx context.Context, req capabilities.Trigger o.mu.Lock() defer o.mu.Unlock() - ch := make(chan capabilities.TriggerResponse, defaultSendChannelBufferSize) + ch := make(chan capabilities.TriggerResponse, 100) o.chans[workflowID(wid)] = ch return ch, nil } diff --git a/pkg/capabilities/triggers/on_demand_trigger_test.go b/pkg/capabilities/triggers/on_demand_trigger_test.go index ffbd1ce17c..32f7d618b1 100644 --- a/pkg/capabilities/triggers/on_demand_trigger_test.go +++ b/pkg/capabilities/triggers/on_demand_trigger_test.go @@ -1,7 +1,6 @@ package triggers import ( - "os" "testing" "github.com/stretchr/testify/assert" @@ -9,7 +8,6 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/utils" "github.com/smartcontractkit/chainlink-protos/cre/go/values" ) @@ -85,23 +83,3 @@ func TestOnDemand_(t *testing.T) { assert.Len(t, callback, 1) assert.Equal(t, er, <-callback) } - -func TestOnDemandTrigger_GenerateSchema(t *testing.T) { - ts := NewOnDemand(logger.Nop()) - schema, err := ts.Schema() - require.NotNil(t, schema) - require.NoError(t, err) - - var shouldUpdate = true - fixturePath := "./testdata/fixtures/ondemand/schema.json" - if shouldUpdate { - err = os.WriteFile(fixturePath, []byte(schema), 0600) - require.NoError(t, err) - defer os.Remove(fixturePath) - } - - fixture, err := os.ReadFile(fixturePath) - require.NoError(t, err) - - utils.AssertJSONEqual(t, fixture, []byte(schema)) -} diff --git a/pkg/capabilities/triggers/streams/streams_trigger-schema.json b/pkg/capabilities/triggers/streams/streams_trigger-schema.json deleted file mode 100644 index 848dbc70ff..0000000000 --- a/pkg/capabilities/triggers/streams/streams_trigger-schema.json +++ /dev/null @@ -1,139 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/smartcontractkit/chainlink-common/pkg/capabilities/triggers/streams/streams-trigger@1.0.0", - "description": "Streams Trigger", - "$defs": { - "feedId": { - "type": "string", - "pattern": "^0x[0-9a-f]{64}$", - "description": "The ID of the data feed." - }, - "SignersMetadata": { - "type": "object", - "properties": { - "Signers": { - "type": "array", - "items": { - "type": "string", - "description": "The id of the signer." - } - }, - "MinRequiredSignatures": { - "type": "integer" - } - }, - "required": [ - "Signers", - "MinRequiredSignatures" - ] - }, - "FeedReport": { - "type": "object", - "properties": { - "FeedID": { - "$ref": "#/$defs/feedId" - }, - "FullReport": { - "type": "array", - "items": { - "type": "integer", - "minimum": 0, - "maximum": 255 - } - }, - "ReportContext": { - "type": "array", - "items": { - "type": "integer", - "minimum": 0, - "maximum": 255, - "description": "Report context represented as bytes. This is required to validate the signatures." - } - }, - "Signatures": { - "type": "array", - "items": { - "type": "array", - "items": { - "type": "integer", - "minimum": 0, - "maximum": 255, - "description": "Signature over full report and report context." - } - } - }, - "BenchmarkPrice": { - "type": "array", - "items": { - "type": "integer", - "minimum": 0, - "maximum": 255, - "description": "This value is extracted from the fullReport." - } - }, - "ObservationTimestamp": { - "type": "integer" - } - }, - "required": [ - "FeedID", - "FullReport", - "ReportContext", - "Signatures", - "BenchmarkPrice", - "ObservationTimestamp" - ] - }, - "Feed": { - "type": "object", - "properties": { - "Timestamp": { - "type": "integer" - }, - "Metadata": { - "$ref": "#/$defs/SignersMetadata" - }, - "Payload": { - "type": "array", - "items": { - "$ref": "#/$defs/FeedReport" - } - } - }, - "required": [ - "Timestamp", - "Metadata", - "Payload" - ] - } - }, - "properties": { - "config": { - "properties": { - "feedIds": { - "type": "array", - "description": "The IDs of the data feeds that will have their reports included in the trigger event.", - "items": { - "$ref": "#/$defs/feedId" - }, - "minItems": 1, - "uniqueItems": true - }, - "maxFrequencyMs": { - "type": "integer", - "description": "The interval in seconds after which a new trigger event is generated.", - "minimum": 1 - } - }, - "additionalProperties": false, - "type": "object", - "required": ["feedIds", "maxFrequencyMs"] - }, - "outputs": { - "$ref": "#/$defs/Feed" - } - }, - "additionalProperties": false, - "type": "object", - "required": ["config"] -} diff --git a/pkg/capabilities/triggers/streams/streams_trigger_generated.go b/pkg/capabilities/triggers/streams/streams_trigger_generated.go deleted file mode 100644 index 593a01f22f..0000000000 --- a/pkg/capabilities/triggers/streams/streams_trigger_generated.go +++ /dev/null @@ -1,208 +0,0 @@ -// Code generated by github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli, DO NOT EDIT. - -package streams - -import ( - "encoding/json" - "fmt" - "regexp" -) - -type Feed struct { - // Metadata corresponds to the JSON schema field "Metadata". - Metadata SignersMetadata `json:"Metadata" yaml:"Metadata" mapstructure:"Metadata"` - - // Payload corresponds to the JSON schema field "Payload". - Payload []FeedReport `json:"Payload" yaml:"Payload" mapstructure:"Payload"` - - // Timestamp corresponds to the JSON schema field "Timestamp". - Timestamp int64 `json:"Timestamp" yaml:"Timestamp" mapstructure:"Timestamp"` -} - -// The ID of the data feed. -type FeedId string - -// UnmarshalJSON implements json.Unmarshaler. -func (j *FeedId) UnmarshalJSON(b []byte) error { - type Plain FeedId - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - if matched, _ := regexp.MatchString("^0x[0-9a-f]{64}$", string(plain)); !matched { - return fmt.Errorf("field %s pattern match: must match %s", "^0x[0-9a-f]{64}$", "") - } - *j = FeedId(plain) - return nil -} - -type FeedReport struct { - // BenchmarkPrice corresponds to the JSON schema field "BenchmarkPrice". - BenchmarkPrice []uint8 `json:"BenchmarkPrice" yaml:"BenchmarkPrice" mapstructure:"BenchmarkPrice"` - - // FeedID corresponds to the JSON schema field "FeedID". - FeedID FeedId `json:"FeedID" yaml:"FeedID" mapstructure:"FeedID"` - - // FullReport corresponds to the JSON schema field "FullReport". - FullReport []uint8 `json:"FullReport" yaml:"FullReport" mapstructure:"FullReport"` - - // ObservationTimestamp corresponds to the JSON schema field - // "ObservationTimestamp". - ObservationTimestamp int64 `json:"ObservationTimestamp" yaml:"ObservationTimestamp" mapstructure:"ObservationTimestamp"` - - // ReportContext corresponds to the JSON schema field "ReportContext". - ReportContext []uint8 `json:"ReportContext" yaml:"ReportContext" mapstructure:"ReportContext"` - - // Signatures corresponds to the JSON schema field "Signatures". - Signatures [][]uint8 `json:"Signatures" yaml:"Signatures" mapstructure:"Signatures"` -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *FeedReport) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - if _, ok := raw["BenchmarkPrice"]; raw != nil && !ok { - return fmt.Errorf("field BenchmarkPrice in FeedReport: required") - } - if _, ok := raw["FeedID"]; raw != nil && !ok { - return fmt.Errorf("field FeedID in FeedReport: required") - } - if _, ok := raw["FullReport"]; raw != nil && !ok { - return fmt.Errorf("field FullReport in FeedReport: required") - } - if _, ok := raw["ObservationTimestamp"]; raw != nil && !ok { - return fmt.Errorf("field ObservationTimestamp in FeedReport: required") - } - if _, ok := raw["ReportContext"]; raw != nil && !ok { - return fmt.Errorf("field ReportContext in FeedReport: required") - } - if _, ok := raw["Signatures"]; raw != nil && !ok { - return fmt.Errorf("field Signatures in FeedReport: required") - } - type Plain FeedReport - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - *j = FeedReport(plain) - return nil -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *Feed) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - if _, ok := raw["Metadata"]; raw != nil && !ok { - return fmt.Errorf("field Metadata in Feed: required") - } - if _, ok := raw["Payload"]; raw != nil && !ok { - return fmt.Errorf("field Payload in Feed: required") - } - if _, ok := raw["Timestamp"]; raw != nil && !ok { - return fmt.Errorf("field Timestamp in Feed: required") - } - type Plain Feed - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - *j = Feed(plain) - return nil -} - -type SignersMetadata struct { - // MinRequiredSignatures corresponds to the JSON schema field - // "MinRequiredSignatures". - MinRequiredSignatures int64 `json:"MinRequiredSignatures" yaml:"MinRequiredSignatures" mapstructure:"MinRequiredSignatures"` - - // Signers corresponds to the JSON schema field "Signers". - Signers []string `json:"Signers" yaml:"Signers" mapstructure:"Signers"` -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *SignersMetadata) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - if _, ok := raw["MinRequiredSignatures"]; raw != nil && !ok { - return fmt.Errorf("field MinRequiredSignatures in SignersMetadata: required") - } - if _, ok := raw["Signers"]; raw != nil && !ok { - return fmt.Errorf("field Signers in SignersMetadata: required") - } - type Plain SignersMetadata - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - *j = SignersMetadata(plain) - return nil -} - -// Streams Trigger -type Trigger struct { - // Config corresponds to the JSON schema field "config". - Config TriggerConfig `json:"config" yaml:"config" mapstructure:"config"` - - // Outputs corresponds to the JSON schema field "outputs". - Outputs *Feed `json:"outputs,omitempty" yaml:"outputs,omitempty" mapstructure:"outputs,omitempty"` -} - -type TriggerConfig struct { - // The IDs of the data feeds that will have their reports included in the trigger - // event. - FeedIds []FeedId `json:"feedIds" yaml:"feedIds" mapstructure:"feedIds"` - - // The interval in seconds after which a new trigger event is generated. - MaxFrequencyMs uint64 `json:"maxFrequencyMs" yaml:"maxFrequencyMs" mapstructure:"maxFrequencyMs"` -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *TriggerConfig) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - if _, ok := raw["feedIds"]; raw != nil && !ok { - return fmt.Errorf("field feedIds in TriggerConfig: required") - } - if _, ok := raw["maxFrequencyMs"]; raw != nil && !ok { - return fmt.Errorf("field maxFrequencyMs in TriggerConfig: required") - } - type Plain TriggerConfig - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - if plain.FeedIds != nil && len(plain.FeedIds) < 1 { - return fmt.Errorf("field %s length: must be >= %d", "feedIds", 1) - } - if 1 > plain.MaxFrequencyMs { - return fmt.Errorf("field %s: must be >= %v", "maxFrequencyMs", 1) - } - *j = TriggerConfig(plain) - return nil -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *Trigger) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - if _, ok := raw["config"]; raw != nil && !ok { - return fmt.Errorf("field config in Trigger: required") - } - type Plain Trigger - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - *j = Trigger(plain) - return nil -} diff --git a/pkg/capabilities/triggers/streams/streamstest/trigger_mock_generated.go b/pkg/capabilities/triggers/streams/streamstest/trigger_mock_generated.go deleted file mode 100644 index f3223d9790..0000000000 --- a/pkg/capabilities/triggers/streams/streamstest/trigger_mock_generated.go +++ /dev/null @@ -1,17 +0,0 @@ -// Code generated by github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli, DO NOT EDIT. - -// Code generated by github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli, DO NOT EDIT. - -package streamstest - -import ( - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/triggers/streams" - "github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk/testutils" -) - -// Trigger registers a new capability mock with the runner -func Trigger(runner *testutils.Runner, fn func() (streams.Feed, error)) *testutils.TriggerMock[streams.Feed] { - mock := testutils.MockTrigger[streams.Feed]("streams-trigger@1.0.0", fn) - runner.MockCapability("streams-trigger@1.0.0", nil, mock) - return mock -} diff --git a/pkg/capabilities/triggers/streams/trigger_builders_generated.go b/pkg/capabilities/triggers/streams/trigger_builders_generated.go deleted file mode 100644 index 2a8692f9f4..0000000000 --- a/pkg/capabilities/triggers/streams/trigger_builders_generated.go +++ /dev/null @@ -1,271 +0,0 @@ -// Code generated by github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli, DO NOT EDIT. - -package streams - -import ( - "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - "github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk" -) - -func (cfg TriggerConfig) New(w *sdk.WorkflowSpecFactory) FeedCap { - ref := "trigger" - def := sdk.StepDefinition{ - ID: "streams-trigger@1.0.0", Ref: ref, - Inputs: sdk.StepInputs{}, - Config: map[string]any{ - "feedIds": cfg.FeedIds, - "maxFrequencyMs": cfg.MaxFrequencyMs, - }, - CapabilityType: capabilities.CapabilityTypeTrigger, - } - - step := sdk.Step[Feed]{Definition: def} - raw := step.AddTo(w) - return FeedWrapper(raw) -} - -// FeedWrapper allows access to field from an sdk.CapDefinition[Feed] -func FeedWrapper(raw sdk.CapDefinition[Feed]) FeedCap { - wrapped, ok := raw.(FeedCap) - if ok { - return wrapped - } - return &feedCap{CapDefinition: raw} -} - -type FeedCap interface { - sdk.CapDefinition[Feed] - Metadata() SignersMetadataCap - Payload() sdk.CapDefinition[[]FeedReport] - Timestamp() sdk.CapDefinition[int64] - private() -} - -type feedCap struct { - sdk.CapDefinition[Feed] -} - -func (*feedCap) private() {} -func (c *feedCap) Metadata() SignersMetadataCap { - return SignersMetadataWrapper(sdk.AccessField[Feed, SignersMetadata](c.CapDefinition, "Metadata")) -} -func (c *feedCap) Payload() sdk.CapDefinition[[]FeedReport] { - return sdk.AccessField[Feed, []FeedReport](c.CapDefinition, "Payload") -} -func (c *feedCap) Timestamp() sdk.CapDefinition[int64] { - return sdk.AccessField[Feed, int64](c.CapDefinition, "Timestamp") -} - -func ConstantFeed(value Feed) FeedCap { - return &feedCap{CapDefinition: sdk.ConstantDefinition(value)} -} - -func NewFeedFromFields( - metadata SignersMetadataCap, - payload sdk.CapDefinition[[]FeedReport], - timestamp sdk.CapDefinition[int64]) FeedCap { - return &simpleFeed{ - CapDefinition: sdk.ComponentCapDefinition[Feed]{ - "Metadata": metadata.Ref(), - "Payload": payload.Ref(), - "Timestamp": timestamp.Ref(), - }, - metadata: metadata, - payload: payload, - timestamp: timestamp, - } -} - -type simpleFeed struct { - sdk.CapDefinition[Feed] - metadata SignersMetadataCap - payload sdk.CapDefinition[[]FeedReport] - timestamp sdk.CapDefinition[int64] -} - -func (c *simpleFeed) Metadata() SignersMetadataCap { - return c.metadata -} -func (c *simpleFeed) Payload() sdk.CapDefinition[[]FeedReport] { - return c.payload -} -func (c *simpleFeed) Timestamp() sdk.CapDefinition[int64] { - return c.timestamp -} - -func (c *simpleFeed) private() {} - -// FeedIdWrapper allows access to field from an sdk.CapDefinition[FeedId] -func FeedIdWrapper(raw sdk.CapDefinition[FeedId]) FeedIdCap { - wrapped, ok := raw.(FeedIdCap) - if ok { - return wrapped - } - return FeedIdCap(raw) -} - -type FeedIdCap sdk.CapDefinition[FeedId] - -// FeedReportWrapper allows access to field from an sdk.CapDefinition[FeedReport] -func FeedReportWrapper(raw sdk.CapDefinition[FeedReport]) FeedReportCap { - wrapped, ok := raw.(FeedReportCap) - if ok { - return wrapped - } - return &feedReportCap{CapDefinition: raw} -} - -type FeedReportCap interface { - sdk.CapDefinition[FeedReport] - BenchmarkPrice() sdk.CapDefinition[[]uint8] - FeedID() FeedIdCap - FullReport() sdk.CapDefinition[[]uint8] - ObservationTimestamp() sdk.CapDefinition[int64] - ReportContext() sdk.CapDefinition[[]uint8] - Signatures() sdk.CapDefinition[[][]uint8] - private() -} - -type feedReportCap struct { - sdk.CapDefinition[FeedReport] -} - -func (*feedReportCap) private() {} -func (c *feedReportCap) BenchmarkPrice() sdk.CapDefinition[[]uint8] { - return sdk.AccessField[FeedReport, []uint8](c.CapDefinition, "BenchmarkPrice") -} -func (c *feedReportCap) FeedID() FeedIdCap { - return FeedIdWrapper(sdk.AccessField[FeedReport, FeedId](c.CapDefinition, "FeedID")) -} -func (c *feedReportCap) FullReport() sdk.CapDefinition[[]uint8] { - return sdk.AccessField[FeedReport, []uint8](c.CapDefinition, "FullReport") -} -func (c *feedReportCap) ObservationTimestamp() sdk.CapDefinition[int64] { - return sdk.AccessField[FeedReport, int64](c.CapDefinition, "ObservationTimestamp") -} -func (c *feedReportCap) ReportContext() sdk.CapDefinition[[]uint8] { - return sdk.AccessField[FeedReport, []uint8](c.CapDefinition, "ReportContext") -} -func (c *feedReportCap) Signatures() sdk.CapDefinition[[][]uint8] { - return sdk.AccessField[FeedReport, [][]uint8](c.CapDefinition, "Signatures") -} - -func ConstantFeedReport(value FeedReport) FeedReportCap { - return &feedReportCap{CapDefinition: sdk.ConstantDefinition(value)} -} - -func NewFeedReportFromFields( - benchmarkPrice sdk.CapDefinition[[]uint8], - feedID FeedIdCap, - fullReport sdk.CapDefinition[[]uint8], - observationTimestamp sdk.CapDefinition[int64], - reportContext sdk.CapDefinition[[]uint8], - signatures sdk.CapDefinition[[][]uint8]) FeedReportCap { - return &simpleFeedReport{ - CapDefinition: sdk.ComponentCapDefinition[FeedReport]{ - "BenchmarkPrice": benchmarkPrice.Ref(), - "FeedID": feedID.Ref(), - "FullReport": fullReport.Ref(), - "ObservationTimestamp": observationTimestamp.Ref(), - "ReportContext": reportContext.Ref(), - "Signatures": signatures.Ref(), - }, - benchmarkPrice: benchmarkPrice, - feedID: feedID, - fullReport: fullReport, - observationTimestamp: observationTimestamp, - reportContext: reportContext, - signatures: signatures, - } -} - -type simpleFeedReport struct { - sdk.CapDefinition[FeedReport] - benchmarkPrice sdk.CapDefinition[[]uint8] - feedID FeedIdCap - fullReport sdk.CapDefinition[[]uint8] - observationTimestamp sdk.CapDefinition[int64] - reportContext sdk.CapDefinition[[]uint8] - signatures sdk.CapDefinition[[][]uint8] -} - -func (c *simpleFeedReport) BenchmarkPrice() sdk.CapDefinition[[]uint8] { - return c.benchmarkPrice -} -func (c *simpleFeedReport) FeedID() FeedIdCap { - return c.feedID -} -func (c *simpleFeedReport) FullReport() sdk.CapDefinition[[]uint8] { - return c.fullReport -} -func (c *simpleFeedReport) ObservationTimestamp() sdk.CapDefinition[int64] { - return c.observationTimestamp -} -func (c *simpleFeedReport) ReportContext() sdk.CapDefinition[[]uint8] { - return c.reportContext -} -func (c *simpleFeedReport) Signatures() sdk.CapDefinition[[][]uint8] { - return c.signatures -} - -func (c *simpleFeedReport) private() {} - -// SignersMetadataWrapper allows access to field from an sdk.CapDefinition[SignersMetadata] -func SignersMetadataWrapper(raw sdk.CapDefinition[SignersMetadata]) SignersMetadataCap { - wrapped, ok := raw.(SignersMetadataCap) - if ok { - return wrapped - } - return &signersMetadataCap{CapDefinition: raw} -} - -type SignersMetadataCap interface { - sdk.CapDefinition[SignersMetadata] - MinRequiredSignatures() sdk.CapDefinition[int64] - Signers() sdk.CapDefinition[[]string] - private() -} - -type signersMetadataCap struct { - sdk.CapDefinition[SignersMetadata] -} - -func (*signersMetadataCap) private() {} -func (c *signersMetadataCap) MinRequiredSignatures() sdk.CapDefinition[int64] { - return sdk.AccessField[SignersMetadata, int64](c.CapDefinition, "MinRequiredSignatures") -} -func (c *signersMetadataCap) Signers() sdk.CapDefinition[[]string] { - return sdk.AccessField[SignersMetadata, []string](c.CapDefinition, "Signers") -} - -func ConstantSignersMetadata(value SignersMetadata) SignersMetadataCap { - return &signersMetadataCap{CapDefinition: sdk.ConstantDefinition(value)} -} - -func NewSignersMetadataFromFields( - minRequiredSignatures sdk.CapDefinition[int64], - signers sdk.CapDefinition[[]string]) SignersMetadataCap { - return &simpleSignersMetadata{ - CapDefinition: sdk.ComponentCapDefinition[SignersMetadata]{ - "MinRequiredSignatures": minRequiredSignatures.Ref(), - "Signers": signers.Ref(), - }, - minRequiredSignatures: minRequiredSignatures, - signers: signers, - } -} - -type simpleSignersMetadata struct { - sdk.CapDefinition[SignersMetadata] - minRequiredSignatures sdk.CapDefinition[int64] - signers sdk.CapDefinition[[]string] -} - -func (c *simpleSignersMetadata) MinRequiredSignatures() sdk.CapDefinition[int64] { - return c.minRequiredSignatures -} -func (c *simpleSignersMetadata) Signers() sdk.CapDefinition[[]string] { - return c.signers -} - -func (c *simpleSignersMetadata) private() {} diff --git a/pkg/capabilities/triggers/testdata/fixtures/mercury/test.yaml b/pkg/capabilities/triggers/testdata/fixtures/mercury/test.yaml deleted file mode 100644 index faf3c4d39a..0000000000 --- a/pkg/capabilities/triggers/testdata/fixtures/mercury/test.yaml +++ /dev/null @@ -1,14 +0,0 @@ -config: - feedIds: - - "0x1111111111111111111100000000000000000000000000000000000000000000" - maxFrequencyMs: 1 -inputs: - triggerId: "asdfkaslfef" -outputs: - TriggerType: "mercury" - ID: "myid" - Timestamp: "" - Metadata: - Payload: - -# yaml-language-server: $schema=./schema.json diff --git a/pkg/capabilities/triggers/testdata/fixtures/ondemand/test.yaml b/pkg/capabilities/triggers/testdata/fixtures/ondemand/test.yaml deleted file mode 100644 index 5f039d90e4..0000000000 --- a/pkg/capabilities/triggers/testdata/fixtures/ondemand/test.yaml +++ /dev/null @@ -1,6 +0,0 @@ -config: {} -output: - Err: - Value: "hello" - -# yaml-language-server: $schema=./schema.json diff --git a/pkg/workflows/sdk/builder_test.go b/pkg/workflows/sdk/builder_test.go deleted file mode 100644 index 73e94d5227..0000000000 --- a/pkg/workflows/sdk/builder_test.go +++ /dev/null @@ -1,676 +0,0 @@ -package sdk_test - -import ( - _ "embed" - "testing" - - "github.com/stretchr/testify/require" - "sigs.k8s.io/yaml" - - "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli/cmd/testdata/fixtures/capabilities/anymapaction" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli/cmd/testdata/fixtures/capabilities/basictrigger" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli/cmd/testdata/fixtures/capabilities/mapaction" - ocr3 "github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/ocr3cap" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/targets/chainwriter" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/triggers/streams" - "github.com/smartcontractkit/chainlink-common/pkg/workflows" - "github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk" - "github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk/testdata/fixtures/capabilities/listtrigger" - "github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk/testdata/fixtures/capabilities/notstreams" - "github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk/testutils" -) - -//go:generate go run github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli/cmd/generate-types --dir $GOFILE - -// Note that the set of tests in this file cover the conversion from existing YAML -> code -// along with testing the structure of what is generated from the builders. -// This implicitly tests the code generators functionally, as the generated code is used in the tests. - -type Config struct { - Streams *streams.TriggerConfig - Ocr *ocr3.DataFeedsConsensusConfig - ChainWriter *chainwriter.TargetConfig - TargetChain string -} - -func NewWorkflowSpec(rawConfig []byte) (*sdk.WorkflowSpecFactory, error) { - conf, err := UnmarshalYaml[Config](rawConfig) - if err != nil { - return nil, err - } - - workflow := sdk.NewWorkflowSpecFactory() - streamsTrigger := conf.Streams.New(workflow) - consensus := conf.Ocr.New(workflow, "ccip_feeds", ocr3.DataFeedsConsensusInput{ - Observations: sdk.ListOf[streams.Feed](streamsTrigger)}, - ) - - conf.ChainWriter.New(workflow, conf.TargetChain, chainwriter.TargetInput{SignedReport: consensus}) - - return workflow, nil -} - -// ModifiedConfig, and the test it's used in, show how you can structure config to remove copy/paste issues when data -// needs to be repeated in multiple capability configurations. -type ModifiedConfig struct { - AllowedPartialStaleness string - MaxFrequencyMs uint64 - DefaultHeartbeat uint64 `yaml:"default_heartbeat" json:"default_heartbeat"` - DefaultDeviation string `yaml:"default_deviation" json:"default_deviation"` - FeedInfo []FeedInfo `yaml:"feed_info" json:"feed_info"` - ReportID ocr3.ReportId `yaml:"report_id" json:"report_id"` - KeyID ocr3.KeyId `yaml:"key_id" json:"key_id"` - Encoder ocr3.Encoder - EncoderConfig ocr3.EncoderConfig `yaml:"encoder_config" json:"encoder_config"` - ChainWriter *chainwriter.TargetConfig - TargetChain string -} - -type FeedInfo struct { - FeedID streams.FeedId - Deviation *string - Heartbeat *uint64 - RemappedID *string -} - -func NewWorkflowRemapped(rawConfig []byte) (*sdk.WorkflowSpecFactory, error) { - conf, err := UnmarshalYaml[ModifiedConfig](rawConfig) - if err != nil { - return nil, err - } - - streamsConfig := streams.TriggerConfig{MaxFrequencyMs: conf.MaxFrequencyMs} - ocr3Config := ocr3.DataFeedsConsensusConfig{ - AggregationMethod: "data_feeds", - Encoder: conf.Encoder, - EncoderConfig: conf.EncoderConfig, - ReportId: conf.ReportID, - KeyId: conf.KeyID, - AggregationConfig: ocr3.DataFeedsConsensusConfigAggregationConfig{ - AllowedPartialStaleness: conf.AllowedPartialStaleness, - }, - } - - feeds := ocr3.DataFeedsConsensusConfigAggregationConfigFeeds{} - for _, elm := range conf.FeedInfo { - streamsConfig.FeedIds = append(streamsConfig.FeedIds, elm.FeedID) - feed := ocr3.FeedValue{ - Deviation: conf.DefaultDeviation, - Heartbeat: conf.DefaultHeartbeat, - RemappedID: elm.RemappedID, - } - if elm.Deviation != nil { - feed.Deviation = *elm.Deviation - } - - if elm.Heartbeat != nil { - feed.Heartbeat = *elm.Heartbeat - } - - feeds[string(elm.FeedID)] = feed - } - ocr3Config.AggregationConfig.Feeds = feeds - - workflow := sdk.NewWorkflowSpecFactory() - streamsTrigger := streamsConfig.New(workflow) - - consensus := ocr3Config.New(workflow, "ccip_feeds", ocr3.DataFeedsConsensusInput{ - Observations: sdk.ListOf[streams.Feed](streamsTrigger), - }) - - conf.ChainWriter.New(workflow, conf.TargetChain, chainwriter.TargetInput{SignedReport: consensus}) - - return workflow, nil -} - -const anyFakeFeedID = "0x0000000000000000000000000000000000000000000000000000000000000000" - -func NewWorkflowSpecFromPrimitives(rawConfig []byte) (*sdk.WorkflowSpecFactory, error) { - conf, err := UnmarshalYaml[NotStreamsConfig](rawConfig) - if err != nil { - return nil, err - } - - workflow := sdk.NewWorkflowSpecFactory() - notStreamsTrigger := conf.NotStream.New(workflow) - - md := streams.NewSignersMetadataFromFields( - sdk.ConstantDefinition(int64(1)), sdk.ListOf(notStreamsTrigger.Metadata().Signer())) - - payload := streams.NewFeedReportFromFields( - notStreamsTrigger.Payload().BuyPrice(), - sdk.ConstantDefinition[streams.FeedId](anyFakeFeedID), - notStreamsTrigger.Payload().FullReport(), - notStreamsTrigger.Payload().ObservationTimestamp(), - notStreamsTrigger.Payload().ReportContext(), - sdk.ListOf(notStreamsTrigger.Payload().Signature()), - ) - - feedsInput := streams.NewFeedFromFields( - md, - sdk.ListOf[streams.FeedReport](payload), - notStreamsTrigger.Timestamp(), - ) - - ocrConfig := ocr3.DataFeedsConsensusConfig{ - AggregationConfig: ocr3.DataFeedsConsensusConfigAggregationConfig{ - AllowedPartialStaleness: conf.Ocr.AllowedPartialStaleness, - Feeds: map[string]ocr3.FeedValue{ - anyFakeFeedID: { - Deviation: conf.Ocr.Deviation, - Heartbeat: conf.Ocr.Heartbeat, - }, - }, - }, - AggregationMethod: conf.Ocr.AggregationMethod, - Encoder: conf.Ocr.Encoder, - EncoderConfig: conf.Ocr.EncoderConfig, - ReportId: conf.Ocr.ReportID, - KeyId: conf.Ocr.KeyID, - } - - consensus := ocrConfig.New(workflow, "data-feeds-report", ocr3.DataFeedsConsensusInput{ - Observations: sdk.ListOf[streams.Feed](feedsInput), - }) - - conf.ChainWriter.New(workflow, conf.TargetChain, chainwriter.TargetInput{SignedReport: consensus}) - - return workflow, nil -} - -//go:embed testdata/fixtures/workflows/sepolia.yaml -var sepoliaConfig []byte - -//go:embed testdata/fixtures/workflows/sepolia_defaults.yaml -var sepoliaDefaultConfig []byte - -//go:embed testdata/fixtures/workflows/expected_sepolia.yaml -var expectedSepolia []byte - -//go:embed testdata/fixtures/workflows/notstreamssepolia.yaml -var notStreamSepoliaConfig []byte - -func TestBuilder_ValidSpec(t *testing.T) { - t.Run("basic config", func(t *testing.T) { - runSepoliaStagingTest(t, sepoliaConfig, NewWorkflowSpec) - }) - - t.Run("remapping config", func(t *testing.T) { - runSepoliaStagingTest(t, sepoliaDefaultConfig, NewWorkflowRemapped) - }) - - // This test intentionally uses a similar complex type to the real steams trigger - // this helps assure that mapping works correctly under many circumstances, including hard-coding - // and wrapping values into arrays, while still remaining somewhat realistic - t.Run("mapping different types without compute", func(t *testing.T) { - factory, err := NewWorkflowSpecFromPrimitives(notStreamSepoliaConfig) - require.NoError(t, err) - - actual, err := factory.Spec() - require.NoError(t, err) - - expected := sdk.WorkflowSpec{ - Triggers: []sdk.StepDefinition{ - { - ID: "notstreams@1.0.0", - Ref: "trigger", - Inputs: sdk.StepInputs{}, - Config: map[string]any{"maxFrequencyMs": 5000}, - CapabilityType: capabilities.CapabilityTypeTrigger, - }, - }, - Actions: make([]sdk.StepDefinition, 0), - Consensus: []sdk.StepDefinition{ - { - ID: "offchain_reporting@1.0.0", - Ref: "data-feeds-report", - Inputs: sdk.StepInputs{ - Mapping: map[string]any{"observations": []map[string]any{ - { - "Metadata": map[string]any{ - "MinRequiredSignatures": 1, - "Signers": []string{"$(trigger.outputs.Metadata.Signer)"}, - }, - "Payload": []map[string]any{ - { - "BenchmarkPrice": "$(trigger.outputs.Payload.BuyPrice)", - "FeedID": anyFakeFeedID, - "FullReport": "$(trigger.outputs.Payload.FullReport)", - "ObservationTimestamp": "$(trigger.outputs.Payload.ObservationTimestamp)", - "ReportContext": "$(trigger.outputs.Payload.ReportContext)", - "Signatures": []string{"$(trigger.outputs.Payload.Signature)"}, - }, - }, - "Timestamp": "$(trigger.outputs.Timestamp)", - }, - }}, - }, - Config: map[string]any{ - "aggregation_config": ocr3.DataFeedsConsensusConfigAggregationConfig{ - AllowedPartialStaleness: "0.5", - Feeds: map[string]ocr3.FeedValue{ - anyFakeFeedID: { - Deviation: "0.5", - Heartbeat: 3600, - }, - }, - }, - "aggregation_method": "data_feeds", - "encoder": "EVM", - "encoder_config": ocr3.EncoderConfig{ - "Abi": "(bytes32 FeedID, uint224 Price, uint32 Timestamp)[] Reports", - }, - "report_id": "0001", - "key_id": "evm", - }, - CapabilityType: capabilities.CapabilityTypeConsensus, - }, - }, - Targets: []sdk.StepDefinition{ - { - ID: "write_ethereum-testnet-sepolia@1.0.0", - Inputs: sdk.StepInputs{ - Mapping: map[string]any{"signed_report": "$(data-feeds-report.outputs)"}, - }, - Config: map[string]any{ - "address": "0xE0082363396985ae2FdcC3a9F816A586Eed88416", - "deltaStage": "45s", - "schedule": "oneAtATime", - "cre_step_timeout": 0, - }, - CapabilityType: capabilities.CapabilityTypeTarget, - }, - }, - } - - testutils.AssertWorkflowSpec(t, expected, actual) - }) - - t.Run("maps work correctly", func(t *testing.T) { - workflow := sdk.NewWorkflowSpecFactory() - trigger := basictrigger.TriggerConfig{Name: "1", Number: 1}.New(workflow) - mapaction.ActionConfig{}.New(workflow, "ref", mapaction.ActionInput{Payload: sdk.Map[string, mapaction.ActionInputsPayload](map[string]sdk.CapDefinition[string]{"Foo": trigger.CoolOutput()})}) - spec, err := workflow.Spec() - require.NoError(t, err) - testutils.AssertWorkflowSpec(t, sdk.WorkflowSpec{ - Triggers: []sdk.StepDefinition{ - { - ID: "basic-test-trigger@1.0.0", - Ref: "trigger", - Inputs: sdk.StepInputs{}, - Config: map[string]any{ - "name": "1", - "number": uint64(1), - }, - CapabilityType: capabilities.CapabilityTypeTrigger, - }, - }, - Actions: []sdk.StepDefinition{ - { - ID: "mapaction@1.0.0", - Ref: "ref", - Inputs: sdk.StepInputs{ - Mapping: map[string]any{"payload": map[string]string{"Foo": "$(trigger.outputs.cool_output)"}}, - }, - Config: map[string]any{}, - CapabilityType: capabilities.CapabilityTypeAction, - }, - }, - Consensus: []sdk.StepDefinition{}, - Targets: []sdk.StepDefinition{}, - }, spec) - }) - - t.Run("any maps work correctly", func(t *testing.T) { - workflow := sdk.NewWorkflowSpecFactory() - trigger := basictrigger.TriggerConfig{Name: "1", Number: 1}.New(workflow) - anymapaction.MapActionConfig{}.New(workflow, "ref", anymapaction.MapActionInput{Payload: sdk.AnyMap[anymapaction.MapActionInputsPayload](sdk.CapMap{"Foo": trigger.CoolOutput()})}) - spec, err := workflow.Spec() - require.NoError(t, err) - testutils.AssertWorkflowSpec(t, sdk.WorkflowSpec{ - Triggers: []sdk.StepDefinition{ - { - ID: "basic-test-trigger@1.0.0", - Ref: "trigger", - Inputs: sdk.StepInputs{}, - Config: map[string]any{ - "name": "1", - "number": uint64(1), - }, - CapabilityType: capabilities.CapabilityTypeTrigger, - }, - }, - Actions: []sdk.StepDefinition{ - { - ID: "anymapaction@1.0.0", - Ref: "ref", - Inputs: sdk.StepInputs{ - Mapping: map[string]any{"payload": map[string]string{"Foo": "$(trigger.outputs.cool_output)"}}, - }, - Config: map[string]any{}, - CapabilityType: capabilities.CapabilityTypeAction, - }, - }, - Consensus: []sdk.StepDefinition{}, - Targets: []sdk.StepDefinition{}, - }, spec) - }) - - t.Run("ToListDefinition works correctly for list elements", func(t *testing.T) { - workflow := sdk.NewWorkflowSpecFactory() - trigger := listtrigger.TriggerConfig{Name: "1"}.New(workflow) - asList := sdk.ToListDefinition[string](trigger.CoolOutput()) - sdk.Compute1(workflow, "compute", sdk.Compute1Inputs[[]string]{Arg0: asList}, func(_ sdk.Runtime, inputs []string) (string, error) { - return inputs[0], nil - }) - sdk.Compute1(workflow, "compute again", sdk.Compute1Inputs[string]{Arg0: asList.Index(0)}, func(runtime sdk.Runtime, input string) (string, error) { - return input, nil - }) - - spec, err := workflow.Spec() - require.NoError(t, err) - - testutils.AssertWorkflowSpec(t, sdk.WorkflowSpec{ - Triggers: []sdk.StepDefinition{ - { - ID: "list@1.0.0", - Ref: "trigger", - Inputs: sdk.StepInputs{}, - Config: map[string]any{"name": "1"}, - CapabilityType: capabilities.CapabilityTypeTrigger, - }, - }, - Actions: []sdk.StepDefinition{ - { - ID: "custom-compute@1.0.0", - Ref: "compute", - Inputs: sdk.StepInputs{ - Mapping: map[string]any{"Arg0": "$(trigger.outputs.cool_output)"}, - }, - Config: map[string]any{ - "config": "$(ENV.config)", - "binary": "$(ENV.binary)", - }, - CapabilityType: capabilities.CapabilityTypeAction, - }, - { - ID: "custom-compute@1.0.0", - Ref: "compute again", - Inputs: sdk.StepInputs{ - Mapping: map[string]any{"Arg0": "$(trigger.outputs.cool_output.0)"}, - }, - Config: map[string]any{ - "config": "$(ENV.config)", - "binary": "$(ENV.binary)", - }, - CapabilityType: capabilities.CapabilityTypeAction, - }, - }, - Consensus: []sdk.StepDefinition{}, - Targets: []sdk.StepDefinition{}, - }, spec) - }) - - t.Run("ToListDefinition works correctly for built up lists", func(t *testing.T) { - workflow := sdk.NewWorkflowSpecFactory() - trigger := basictrigger.TriggerConfig{Name: "1"}.New(workflow) - asList := sdk.ToListDefinition(sdk.ListOf(trigger.CoolOutput())) - sdk.Compute1(workflow, "compute", sdk.Compute1Inputs[[]string]{Arg0: asList}, func(_ sdk.Runtime, inputs []string) (string, error) { - return inputs[0], nil - }) - sdk.Compute1(workflow, "compute again", sdk.Compute1Inputs[string]{Arg0: asList.Index(0)}, func(runtime sdk.Runtime, input string) (string, error) { - return input, nil - }) - - spec, err := workflow.Spec() - require.NoError(t, err) - - testutils.AssertWorkflowSpec(t, sdk.WorkflowSpec{ - Triggers: []sdk.StepDefinition{ - { - ID: "basic-test-trigger@1.0.0", - Ref: "trigger", - Inputs: sdk.StepInputs{}, - Config: map[string]any{"name": "1", "number": uint64(0)}, - CapabilityType: capabilities.CapabilityTypeTrigger, - }, - }, - Actions: []sdk.StepDefinition{ - { - ID: "custom-compute@1.0.0", - Ref: "compute", - Inputs: sdk.StepInputs{ - Mapping: map[string]any{"Arg0": []any{"$(trigger.outputs.cool_output)"}}, - }, - Config: map[string]any{ - "config": "$(ENV.config)", - "binary": "$(ENV.binary)", - }, - CapabilityType: capabilities.CapabilityTypeAction, - }, - { - ID: "custom-compute@1.0.0", - Ref: "compute again", - Inputs: sdk.StepInputs{ - Mapping: map[string]any{"Arg0": "$(trigger.outputs.cool_output)"}, - }, - Config: map[string]any{ - "config": "$(ENV.config)", - "binary": "$(ENV.binary)", - }, - CapabilityType: capabilities.CapabilityTypeAction, - }, - }, - Consensus: []sdk.StepDefinition{}, - Targets: []sdk.StepDefinition{}, - }, spec) - }) - - t.Run("ToListDefinition works correctly for hard-coded lists", func(t *testing.T) { - workflow := sdk.NewWorkflowSpecFactory() - trigger := basictrigger.TriggerConfig{Name: "1"}.New(workflow) - list := sdk.ToListDefinition(sdk.ConstantDefinition([]string{"1", "2"})) - sdk.Compute2(workflow, "compute", sdk.Compute2Inputs[string, []string]{Arg0: trigger.CoolOutput(), Arg1: list}, func(_ sdk.Runtime, t string, l []string) (string, error) { - return "", nil - }) - sdk.Compute2(workflow, "compute again", sdk.Compute2Inputs[string, string]{Arg0: trigger.CoolOutput(), Arg1: list.Index(0)}, func(_ sdk.Runtime, t string, l string) (string, error) { - return "", nil - }) - - spec, err := workflow.Spec() - require.NoError(t, err) - - testutils.AssertWorkflowSpec(t, sdk.WorkflowSpec{ - Triggers: []sdk.StepDefinition{ - { - ID: "basic-test-trigger@1.0.0", - Ref: "trigger", - Inputs: sdk.StepInputs{}, - Config: map[string]any{"name": "1", "number": uint64(0)}, - CapabilityType: capabilities.CapabilityTypeTrigger, - }, - }, - Actions: []sdk.StepDefinition{ - { - ID: "custom-compute@1.0.0", - Ref: "compute", - Inputs: sdk.StepInputs{ - Mapping: map[string]any{ - "Arg0": "$(trigger.outputs.cool_output)", - "Arg1": []string{"1", "2"}, - }, - }, - Config: map[string]any{ - "config": "$(ENV.config)", - "binary": "$(ENV.binary)", - }, - CapabilityType: capabilities.CapabilityTypeAction, - }, - { - ID: "custom-compute@1.0.0", - Ref: "compute again", - Inputs: sdk.StepInputs{ - Mapping: map[string]any{ - "Arg0": "$(trigger.outputs.cool_output)", - "Arg1": "1", - }, - }, - Config: map[string]any{ - "config": "$(ENV.config)", - "binary": "$(ENV.binary)", - }, - CapabilityType: capabilities.CapabilityTypeAction, - }, - }, - Consensus: []sdk.StepDefinition{}, - Targets: []sdk.StepDefinition{}, - }, spec) - }) - - t.Run("AnyListOf works like list of but returns a type any", func(t *testing.T) { - workflow1 := sdk.NewWorkflowSpecFactory() - trigger := basictrigger.TriggerConfig{Name: "foo", Number: 0} - list := sdk.ListOf(trigger.New(workflow1).CoolOutput()) - sdk.Compute1(workflow1, "compute", sdk.Compute1Inputs[[]string]{Arg0: list}, func(_ sdk.Runtime, inputs []string) (string, error) { - return inputs[0], nil - }) - - workflow2 := sdk.NewWorkflowSpecFactory() - anyList := sdk.AnyListOf(trigger.New(workflow2).CoolOutput()) - sdk.Compute1(workflow2, "compute", sdk.Compute1Inputs[[]any]{Arg0: anyList}, func(_ sdk.Runtime, inputs []any) (any, error) { - return inputs[0], nil - }) - - spec1, err := workflow1.Spec() - require.NoError(t, err) - spec2, err := workflow2.Spec() - require.NoError(t, err) - - testutils.AssertWorkflowSpec(t, spec1, spec2) - }) - - t.Run("duplicate names causes errors", func(t *testing.T) { - conf, err := UnmarshalYaml[Config](sepoliaConfig) - require.NoError(t, err) - - workflow := sdk.NewWorkflowSpecFactory() - streamsTrigger := conf.Streams.New(workflow) - consensus := conf.Ocr.New(workflow, "ccip_feeds", ocr3.DataFeedsConsensusInput{ - Observations: sdk.ListOf[streams.Feed](streamsTrigger)}, - ) - - consensus2 := conf.Ocr.New(workflow, "ccip_feeds", ocr3.DataFeedsConsensusInput{ - Observations: sdk.ListOf[streams.Feed](streamsTrigger)}, - ) - - conf.ChainWriter.New(workflow, conf.TargetChain, chainwriter.TargetInput{SignedReport: consensus}) - - conf.ChainWriter.New(workflow, conf.TargetChain, chainwriter.TargetInput{SignedReport: consensus2}) - - _, err = workflow.Spec() - require.Error(t, err) - }) - - t.Run("empty ref causes an error", func(t *testing.T) { - conf, err := UnmarshalYaml[Config](sepoliaConfig) - require.NoError(t, err) - - workflow := sdk.NewWorkflowSpecFactory() - streamsTrigger := conf.Streams.New(workflow) - consensus := conf.Ocr.New(workflow, "", ocr3.DataFeedsConsensusInput{ - Observations: sdk.ListOf[streams.Feed](streamsTrigger)}, - ) - - conf.ChainWriter.New(workflow, conf.TargetChain, chainwriter.TargetInput{SignedReport: consensus}) - - _, err = workflow.Spec() - require.Error(t, err) - }) - - t.Run("bad capability type causes an error", func(t *testing.T) { - conf, err := UnmarshalYaml[Config](sepoliaConfig) - require.NoError(t, err) - - workflow := sdk.NewWorkflowSpecFactory() - badStep := sdk.Step[streams.Feed]{ - Definition: sdk.StepDefinition{ - ID: "streams-trigger@1.0.0", - Ref: "Trigger", - Inputs: sdk.StepInputs{}, - Config: map[string]any{}, - CapabilityType: "fake", - }, - } - - badCap := badStep.AddTo(workflow) - - consensus := conf.Ocr.New(workflow, "", ocr3.DataFeedsConsensusInput{ - Observations: sdk.ListOf[streams.Feed](badCap)}, - ) - - conf.ChainWriter.New(workflow, conf.TargetChain, chainwriter.TargetInput{SignedReport: consensus}) - - _, err = workflow.Spec() - require.Error(t, err) - }) - - t.Run("Capabilities can be used multiple times with different references", func(t *testing.T) { - conf, err := UnmarshalYaml[Config](sepoliaConfig) - require.NoError(t, err) - - workflow := sdk.NewWorkflowSpecFactory() - streamsTrigger := conf.Streams.New(workflow) - consensus := conf.Ocr.New(workflow, "ccip_feeds", ocr3.DataFeedsConsensusInput{ - Observations: sdk.ListOf[streams.Feed](streamsTrigger)}, - ) - - consensus2 := conf.Ocr.New(workflow, "ccip_feeds_different", ocr3.DataFeedsConsensusInput{ - Observations: sdk.ListOf[streams.Feed](streamsTrigger)}, - ) - - conf.ChainWriter.New(workflow, conf.TargetChain, chainwriter.TargetInput{SignedReport: consensus}) - - conf.ChainWriter.New(workflow, conf.TargetChain, chainwriter.TargetInput{SignedReport: consensus2}) - - _, err = workflow.Spec() - require.NoError(t, err) - }) -} - -func runSepoliaStagingTest(t *testing.T, config []byte, gen func([]byte) (*sdk.WorkflowSpecFactory, error)) { - testFactory, err := gen(config) - require.NoError(t, err) - - testWorkflowSpec, err := testFactory.Spec() - require.NoError(t, err) - - expectedSpecYaml, err := UnmarshalYaml[workflows.WorkflowSpecYaml](expectedSepolia) - require.NoError(t, err) - expectedSpec := expectedSpecYaml.ToWorkflowSpec() - testutils.AssertWorkflowSpec(t, expectedSpec, testWorkflowSpec) -} - -type NotStreamsConfig struct { - NotStream *notstreams.TriggerConfig `yaml:"not_stream" json:"not_stream"` - Ocr *ModifiedConsensusConfig - ChainWriter *chainwriter.TargetConfig - TargetChain string -} - -type ModifiedConsensusConfig struct { - AllowedPartialStaleness string `json:"allowedPartialStaleness" yaml:"allowedPartialStaleness" mapstructure:"allowedPartialStaleness"` - Deviation string `json:"deviation" yaml:"deviation" mapstructure:"deviation"` - Heartbeat uint64 `json:"heartbeat" yaml:"heartbeat" mapstructure:"heartbeat"` - AggregationMethod ocr3.DataFeedsConsensusConfigAggregationMethod `json:"aggregation_method" yaml:"aggregation_method" mapstructure:"aggregation_method"` - Encoder ocr3.Encoder `json:"encoder" yaml:"encoder" mapstructure:"encoder"` - EncoderConfig ocr3.EncoderConfig `json:"encoder_config" yaml:"encoder_config" mapstructure:"encoder_config"` - ReportID ocr3.ReportId `json:"report_id" yaml:"report_id" mapstructure:"report_id"` - KeyID ocr3.KeyId `json:"key_id" yaml:"key_id" mapstructure:"key_id"` -} - -func UnmarshalYaml[T any](raw []byte) (*T, error) { - var v T - err := yaml.Unmarshal(raw, &v) - return &v, err -} diff --git a/pkg/workflows/sdk/compute_test.go b/pkg/workflows/sdk/compute_test.go deleted file mode 100644 index 26a6e1ad0b..0000000000 --- a/pkg/workflows/sdk/compute_test.go +++ /dev/null @@ -1,254 +0,0 @@ -package sdk_test - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk" - "github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk/testdata/fixtures/capabilities/notstreams" - - "github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk/testutils" - - "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli/cmd/testdata/fixtures/capabilities/basictrigger" - ocr3 "github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/ocr3cap" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/targets/chainwriter" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/triggers/streams" - "github.com/smartcontractkit/chainlink-protos/cre/go/values" -) - -func TestCompute(t *testing.T) { - anyNotStreamsInput := notstreams.Feed{ - Metadata: notstreams.SignerMetadata{Signer: "signer1"}, - Payload: notstreams.FeedReport{ - BuyPrice: []byte{1, 2, 3}, - FullReport: []byte("report"), - ObservationTimestamp: 2, - ReportContext: []byte("context"), - SellPrice: []byte{1, 2, 4}, - Signature: []byte("sig"), - }, - Timestamp: 1690838088, - } - structToMap, err := values.CreateMapFromStruct(anyNotStreamsInput) - require.NoError(t, err) - - nsf, err := values.NewMap(map[string]any{"Arg0": structToMap}) - require.NoError(t, err) - - t.Run("creates correct workflow spec", func(t *testing.T) { - workflow := createWorkflow(convertFeed) - - spec, err2 := workflow.Spec() - require.NoError(t, err2) - expectedSpec := sdk.WorkflowSpec{ - Triggers: []sdk.StepDefinition{ - { - ID: "notstreams@1.0.0", - Ref: "trigger", - Inputs: sdk.StepInputs{}, - Config: map[string]any{"maxFrequencyMs": 5000}, - CapabilityType: capabilities.CapabilityTypeTrigger, - }, - }, - Actions: []sdk.StepDefinition{ - { - ID: "custom-compute@1.0.0", - Ref: "Compute", - Inputs: sdk.StepInputs{ - Mapping: map[string]any{"Arg0": "$(trigger.outputs)"}, - }, - Config: map[string]any{ - "binary": "$(ENV.binary)", - "config": "$(ENV.config)", - }, - CapabilityType: capabilities.CapabilityTypeAction, - }, - }, - Consensus: []sdk.StepDefinition{ - { - ID: "offchain_reporting@1.0.0", - Ref: "data-feeds-report", - Inputs: sdk.StepInputs{ - Mapping: map[string]any{"observations": "$(Compute.outputs.Value)"}, - }, - Config: map[string]any{ - "aggregation_config": ocr3.DataFeedsConsensusConfigAggregationConfig{ - AllowedPartialStaleness: "false", - Feeds: map[string]ocr3.FeedValue{ - anyFakeFeedID: { - Deviation: "0.5", - Heartbeat: 3600, - }, - }, - }, - "aggregation_method": "data_feeds", - "encoder": ocr3.EncoderEVM, - "encoder_config": ocr3.EncoderConfig{}, - "report_id": "0001", - "key_id": "evm", - }, - CapabilityType: capabilities.CapabilityTypeConsensus, - }, - }, - Targets: []sdk.StepDefinition{ - { - ID: "write_ethereum-testnet-sepolia@1.0.0", - Inputs: sdk.StepInputs{ - Mapping: map[string]any{"signed_report": "$(data-feeds-report.outputs)"}, - }, - Config: map[string]any{ - "address": "0xE0082363396985ae2FdcC3a9F816A586Eed88416", - "deltaStage": "45s", - "schedule": "oneAtATime", - "cre_step_timeout": 0, - }, - CapabilityType: capabilities.CapabilityTypeTarget, - }, - }, - } - - testutils.AssertWorkflowSpec(t, expectedSpec, spec) - }) - - t.Run("compute runs the function and returns the value", func(t *testing.T) { - workflow := createWorkflow(convertFeed) - - fn := workflow.GetFn("Compute") - require.NotNil(t, fn) - - req := capabilities.CapabilityRequest{Inputs: nsf} - actual, err := fn(&testutils.NoopRuntime{}, req) - require.NoError(t, err) - - expected, err := convertFeed(nil, anyNotStreamsInput) - require.NoError(t, err) - - computed := &sdk.ComputeOutput[[]streams.Feed]{} - err = actual.Value.UnwrapTo(computed) - require.NoError(t, err) - - assert.Equal(t, expected, computed.Value) - }) - - t.Run("compute supports passing in config via a struct", func(t *testing.T) { - computeFn := func(_ sdk.Runtime, config ComputeConfig, inputs basictrigger.TriggerOutputs) (ComputeOutput, error) { - return ComputeOutput{ - MySecret: string(config.Fidelity), - }, nil - } - conf := ComputeConfig{Fidelity: sdk.Secret("fidelity")} - workflow := createComputeWithConfigWorkflow( - conf, - computeFn, - ) - _, err := workflow.Spec() - require.NoError(t, err) - - fn := workflow.GetFn("Compute") - require.NotNil(t, fn) - - mc, err := values.WrapMap(conf) - require.NoError(t, err) - - req := capabilities.CapabilityRequest{Inputs: nsf, Config: mc} - actual, err := fn(&testutils.NoopRuntime{}, req) - require.NoError(t, err) - - expected, err := computeFn(nil, conf, basictrigger.TriggerOutputs{}) - require.NoError(t, err) - - uw, _ := actual.Value.Unwrap() - fmt.Printf("%+v", uw) - - computed := &sdk.ComputeOutput[ComputeOutput]{} - err = actual.Value.UnwrapTo(computed) - require.NoError(t, err) - - assert.Equal(t, expected, computed.Value) - }) -} - -type ComputeConfig struct { - Fidelity sdk.SecretValue -} - -type ComputeOutput struct { - MySecret string -} - -func createComputeWithConfigWorkflow(config ComputeConfig, fn func(_ sdk.Runtime, config ComputeConfig, input basictrigger.TriggerOutputs) (ComputeOutput, error)) *sdk.WorkflowSpecFactory { - workflow := sdk.NewWorkflowSpecFactory() - - triggerCfg := basictrigger.TriggerConfig{Name: "trigger", Number: 100} - trigger := triggerCfg.New(workflow) - - cc := &sdk.ComputeConfig[ComputeConfig]{ - Config: config, - } - sdk.Compute1WithConfig( - workflow, - "Compute", - cc, - sdk.Compute1Inputs[basictrigger.TriggerOutputs]{Arg0: trigger}, - fn, - ) - - return workflow -} - -func createWorkflow(fn func(_ sdk.Runtime, inputFeed notstreams.Feed) ([]streams.Feed, error)) *sdk.WorkflowSpecFactory { - workflow := sdk.NewWorkflowSpecFactory() - - trigger := notstreams.TriggerConfig{MaxFrequencyMs: 5000}.New(workflow) - computed := sdk.Compute1(workflow, "Compute", sdk.Compute1Inputs[notstreams.Feed]{Arg0: trigger}, fn) - - consensus := ocr3.DataFeedsConsensusConfig{ - AggregationConfig: ocr3.DataFeedsConsensusConfigAggregationConfig{ - AllowedPartialStaleness: "false", - Feeds: map[string]ocr3.FeedValue{ - anyFakeFeedID: { - Deviation: "0.5", - Heartbeat: 3600, - }, - }, - }, - AggregationMethod: "data_feeds", - Encoder: ocr3.EncoderEVM, - EncoderConfig: ocr3.EncoderConfig{}, - ReportId: "0001", - KeyId: "evm", - }.New(workflow, "data-feeds-report", ocr3.DataFeedsConsensusInput{ - Observations: computed.Value(), - }) - - chainwriter.TargetConfig{ - Address: "0xE0082363396985ae2FdcC3a9F816A586Eed88416", - DeltaStage: "45s", - Schedule: "oneAtATime", - }.New(workflow, "write_ethereum-testnet-sepolia@1.0.0", chainwriter.TargetInput{SignedReport: consensus}) - - return workflow -} - -func convertFeed(_ sdk.Runtime, inputFeed notstreams.Feed) ([]streams.Feed, error) { - return []streams.Feed{ - { - Metadata: streams.SignersMetadata{Signers: []string{inputFeed.Metadata.Signer}}, - Payload: []streams.FeedReport{ - { - BenchmarkPrice: inputFeed.Payload.BuyPrice, - FeedID: anyFakeFeedID, - FullReport: inputFeed.Payload.FullReport, - ObservationTimestamp: inputFeed.Payload.ObservationTimestamp, - ReportContext: inputFeed.Payload.ReportContext, - Signatures: [][]byte{inputFeed.Payload.Signature}, - }, - }, - Timestamp: inputFeed.Timestamp, - }, - }, nil -} From 39b53fd837e4a8e06aa4713afd3ef4b9fa0e7ad4 Mon Sep 17 00:00:00 2001 From: Bolek Kulbabinski <1416262+bolekk@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:30:01 -0700 Subject: [PATCH 2/2] remove test fixtures --- .mockery.yaml | 3 - .../listtrigger/list_trigger-schema.json | 33 --- .../listtrigger/list_trigger_generated.go | 81 ------ .../listtriggertest/trigger_mock_generated.go | 17 -- .../listtrigger/trigger_builders_generated.go | 73 ------ .../notstreams/notstreams_trigger-schema.json | 111 -------- .../notstreams_trigger_generated.go | 173 ------------ .../notstreamstest/trigger_mock_generated.go | 17 -- .../notstreams/trigger_builders_generated.go | 248 ------------------ 9 files changed, 756 deletions(-) delete mode 100644 pkg/workflows/sdk/testdata/fixtures/capabilities/listtrigger/list_trigger-schema.json delete mode 100644 pkg/workflows/sdk/testdata/fixtures/capabilities/listtrigger/list_trigger_generated.go delete mode 100644 pkg/workflows/sdk/testdata/fixtures/capabilities/listtrigger/listtriggertest/trigger_mock_generated.go delete mode 100644 pkg/workflows/sdk/testdata/fixtures/capabilities/listtrigger/trigger_builders_generated.go delete mode 100644 pkg/workflows/sdk/testdata/fixtures/capabilities/notstreams/notstreams_trigger-schema.json delete mode 100644 pkg/workflows/sdk/testdata/fixtures/capabilities/notstreams/notstreams_trigger_generated.go delete mode 100644 pkg/workflows/sdk/testdata/fixtures/capabilities/notstreams/notstreamstest/trigger_mock_generated.go delete mode 100644 pkg/workflows/sdk/testdata/fixtures/capabilities/notstreams/trigger_builders_generated.go diff --git a/.mockery.yaml b/.mockery.yaml index 1d438df01c..0c07c90140 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -4,9 +4,6 @@ outpkg: mocks filename: "{{ .InterfaceName | snakecase }}.go" fail-on-missing: true packages: - github.com/smartcontractkit/chainlink-common/pkg/capabilities/datastreams: - interfaces: - ReportCodec: github.com/smartcontractkit/chainlink-common/pkg/loop/internal/types: interfaces: Relayer: diff --git a/pkg/workflows/sdk/testdata/fixtures/capabilities/listtrigger/list_trigger-schema.json b/pkg/workflows/sdk/testdata/fixtures/capabilities/listtrigger/list_trigger-schema.json deleted file mode 100644 index 499c37f0d4..0000000000 --- a/pkg/workflows/sdk/testdata/fixtures/capabilities/listtrigger/list_trigger-schema.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk/testdata/fixtures/capabilities/listtrigger/list@1.0.0", - "description": "Basic Test Trigger", - "properties": { - "config": { - "properties": { - "name": { - "type": "string" - } - }, - "additionalProperties": false, - "type": "object", - "required": ["name"] - }, - "outputs": { - "properties": { - "cool_output": { - "type" : "array", - "items" : { - "type": "string" - } - } - }, - "additionalProperties": false, - "type": "object", - "required": ["cool_output"] - } - }, - "additionalProperties": false, - "type": "object", - "required": ["config"] -} diff --git a/pkg/workflows/sdk/testdata/fixtures/capabilities/listtrigger/list_trigger_generated.go b/pkg/workflows/sdk/testdata/fixtures/capabilities/listtrigger/list_trigger_generated.go deleted file mode 100644 index aa1de38eb7..0000000000 --- a/pkg/workflows/sdk/testdata/fixtures/capabilities/listtrigger/list_trigger_generated.go +++ /dev/null @@ -1,81 +0,0 @@ -// Code generated by github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli, DO NOT EDIT. - -package listtrigger - -import ( - "encoding/json" - "fmt" -) - -// Basic Test Trigger -type Trigger struct { - // Config corresponds to the JSON schema field "config". - Config TriggerConfig `json:"config" yaml:"config" mapstructure:"config"` - - // Outputs corresponds to the JSON schema field "outputs". - Outputs *TriggerOutputs `json:"outputs,omitempty" yaml:"outputs,omitempty" mapstructure:"outputs,omitempty"` -} - -type TriggerConfig struct { - // Name corresponds to the JSON schema field "name". - Name string `json:"name" yaml:"name" mapstructure:"name"` -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *TriggerConfig) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - if _, ok := raw["name"]; raw != nil && !ok { - return fmt.Errorf("field name in TriggerConfig: required") - } - type Plain TriggerConfig - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - *j = TriggerConfig(plain) - return nil -} - -type TriggerOutputs struct { - // CoolOutput corresponds to the JSON schema field "cool_output". - CoolOutput []string `json:"cool_output" yaml:"cool_output" mapstructure:"cool_output"` -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *TriggerOutputs) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - if _, ok := raw["cool_output"]; raw != nil && !ok { - return fmt.Errorf("field cool_output in TriggerOutputs: required") - } - type Plain TriggerOutputs - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - *j = TriggerOutputs(plain) - return nil -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *Trigger) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - if _, ok := raw["config"]; raw != nil && !ok { - return fmt.Errorf("field config in Trigger: required") - } - type Plain Trigger - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - *j = Trigger(plain) - return nil -} diff --git a/pkg/workflows/sdk/testdata/fixtures/capabilities/listtrigger/listtriggertest/trigger_mock_generated.go b/pkg/workflows/sdk/testdata/fixtures/capabilities/listtrigger/listtriggertest/trigger_mock_generated.go deleted file mode 100644 index 43f05e0a2d..0000000000 --- a/pkg/workflows/sdk/testdata/fixtures/capabilities/listtrigger/listtriggertest/trigger_mock_generated.go +++ /dev/null @@ -1,17 +0,0 @@ -// Code generated by github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli, DO NOT EDIT. - -// Code generated by github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli, DO NOT EDIT. - -package listtriggertest - -import ( - "github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk/testdata/fixtures/capabilities/listtrigger" - "github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk/testutils" -) - -// Trigger registers a new capability mock with the runner -func Trigger(runner *testutils.Runner, fn func() (listtrigger.TriggerOutputs, error)) *testutils.TriggerMock[listtrigger.TriggerOutputs] { - mock := testutils.MockTrigger[listtrigger.TriggerOutputs]("list@1.0.0", fn) - runner.MockCapability("list@1.0.0", nil, mock) - return mock -} diff --git a/pkg/workflows/sdk/testdata/fixtures/capabilities/listtrigger/trigger_builders_generated.go b/pkg/workflows/sdk/testdata/fixtures/capabilities/listtrigger/trigger_builders_generated.go deleted file mode 100644 index c9bececc69..0000000000 --- a/pkg/workflows/sdk/testdata/fixtures/capabilities/listtrigger/trigger_builders_generated.go +++ /dev/null @@ -1,73 +0,0 @@ -// Code generated by github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli, DO NOT EDIT. - -package listtrigger - -import ( - "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - "github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk" -) - -func (cfg TriggerConfig) New(w *sdk.WorkflowSpecFactory) TriggerOutputsCap { - ref := "trigger" - def := sdk.StepDefinition{ - ID: "list@1.0.0", Ref: ref, - Inputs: sdk.StepInputs{}, - Config: map[string]any{ - "name": cfg.Name, - }, - CapabilityType: capabilities.CapabilityTypeTrigger, - } - - step := sdk.Step[TriggerOutputs]{Definition: def} - raw := step.AddTo(w) - return TriggerOutputsWrapper(raw) -} - -// TriggerOutputsWrapper allows access to field from an sdk.CapDefinition[TriggerOutputs] -func TriggerOutputsWrapper(raw sdk.CapDefinition[TriggerOutputs]) TriggerOutputsCap { - wrapped, ok := raw.(TriggerOutputsCap) - if ok { - return wrapped - } - return &triggerOutputsCap{CapDefinition: raw} -} - -type TriggerOutputsCap interface { - sdk.CapDefinition[TriggerOutputs] - CoolOutput() sdk.CapDefinition[[]string] - private() -} - -type triggerOutputsCap struct { - sdk.CapDefinition[TriggerOutputs] -} - -func (*triggerOutputsCap) private() {} -func (c *triggerOutputsCap) CoolOutput() sdk.CapDefinition[[]string] { - return sdk.AccessField[TriggerOutputs, []string](c.CapDefinition, "cool_output") -} - -func ConstantTriggerOutputs(value TriggerOutputs) TriggerOutputsCap { - return &triggerOutputsCap{CapDefinition: sdk.ConstantDefinition(value)} -} - -func NewTriggerOutputsFromFields( - coolOutput sdk.CapDefinition[[]string]) TriggerOutputsCap { - return &simpleTriggerOutputs{ - CapDefinition: sdk.ComponentCapDefinition[TriggerOutputs]{ - "cool_output": coolOutput.Ref(), - }, - coolOutput: coolOutput, - } -} - -type simpleTriggerOutputs struct { - sdk.CapDefinition[TriggerOutputs] - coolOutput sdk.CapDefinition[[]string] -} - -func (c *simpleTriggerOutputs) CoolOutput() sdk.CapDefinition[[]string] { - return c.coolOutput -} - -func (c *simpleTriggerOutputs) private() {} diff --git a/pkg/workflows/sdk/testdata/fixtures/capabilities/notstreams/notstreams_trigger-schema.json b/pkg/workflows/sdk/testdata/fixtures/capabilities/notstreams/notstreams_trigger-schema.json deleted file mode 100644 index e8bf9f9946..0000000000 --- a/pkg/workflows/sdk/testdata/fixtures/capabilities/notstreams/notstreams_trigger-schema.json +++ /dev/null @@ -1,111 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk/testdata/fixtures/capabilities/notstreams/notstreams@1.0.0", - "description": "Streams Trigger", - "$defs": { - "SignerMetadata": { - "type": "object", - "properties": { - "Signer": { - "type": "string" - } - }, - "required": ["Signer"] - }, - "FeedReport": { - "type": "object", - "properties": { - "FullReport": { - "type": "array", - "items": { - "type": "integer", - "minimum": 0, - "maximum": 255 - } - }, - "ReportContext": { - "type": "array", - "items": { - "type": "integer", - "minimum": 0, - "maximum": 255 - } - }, - "Signature": { - "type": "array", - "items": { - "type": "integer", - "minimum": 0, - "maximum": 255 - } - }, - "BuyPrice": { - "type": "array", - "items": { - "type": "integer", - "minimum": 0, - "maximum": 255 - } - }, - "SellPrice": { - "type": "array", - "items": { - "type": "integer", - "minimum": 0, - "maximum": 255 - } - }, - "ObservationTimestamp": { - "type": "integer" - } - }, - "required": [ - "FullReport", - "ReportContext", - "Signature", - "BuyPrice", - "SellPrice", - "ObservationTimestamp" - ] - }, - "Feed": { - "type": "object", - "properties": { - "Timestamp": { - "type": "integer" - }, - "Metadata": { - "$ref": "#/$defs/SignerMetadata" - }, - "Payload": { - "$ref": "#/$defs/FeedReport" - } - }, - "required": [ - "Timestamp", - "Metadata", - "Payload" - ] - } - }, - "properties": { - "config": { - "properties": { - "maxFrequencyMs": { - "type": "integer", - "description": "The interval in seconds after which a new trigger event is generated.", - "minimum": 1 - } - }, - "additionalProperties": false, - "type": "object", - "required": ["maxFrequencyMs"] - }, - "outputs": { - "$ref": "#/$defs/Feed" - } - }, - "additionalProperties": false, - "type": "object", - "required": ["config"] -} diff --git a/pkg/workflows/sdk/testdata/fixtures/capabilities/notstreams/notstreams_trigger_generated.go b/pkg/workflows/sdk/testdata/fixtures/capabilities/notstreams/notstreams_trigger_generated.go deleted file mode 100644 index db2d1fcf33..0000000000 --- a/pkg/workflows/sdk/testdata/fixtures/capabilities/notstreams/notstreams_trigger_generated.go +++ /dev/null @@ -1,173 +0,0 @@ -// Code generated by github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli, DO NOT EDIT. - -package notstreams - -import ( - "encoding/json" - "fmt" -) - -type Feed struct { - // Metadata corresponds to the JSON schema field "Metadata". - Metadata SignerMetadata `json:"Metadata" yaml:"Metadata" mapstructure:"Metadata"` - - // Payload corresponds to the JSON schema field "Payload". - Payload FeedReport `json:"Payload" yaml:"Payload" mapstructure:"Payload"` - - // Timestamp corresponds to the JSON schema field "Timestamp". - Timestamp int64 `json:"Timestamp" yaml:"Timestamp" mapstructure:"Timestamp"` -} - -type FeedReport struct { - // BuyPrice corresponds to the JSON schema field "BuyPrice". - BuyPrice []uint8 `json:"BuyPrice" yaml:"BuyPrice" mapstructure:"BuyPrice"` - - // FullReport corresponds to the JSON schema field "FullReport". - FullReport []uint8 `json:"FullReport" yaml:"FullReport" mapstructure:"FullReport"` - - // ObservationTimestamp corresponds to the JSON schema field - // "ObservationTimestamp". - ObservationTimestamp int64 `json:"ObservationTimestamp" yaml:"ObservationTimestamp" mapstructure:"ObservationTimestamp"` - - // ReportContext corresponds to the JSON schema field "ReportContext". - ReportContext []uint8 `json:"ReportContext" yaml:"ReportContext" mapstructure:"ReportContext"` - - // SellPrice corresponds to the JSON schema field "SellPrice". - SellPrice []uint8 `json:"SellPrice" yaml:"SellPrice" mapstructure:"SellPrice"` - - // Signature corresponds to the JSON schema field "Signature". - Signature []uint8 `json:"Signature" yaml:"Signature" mapstructure:"Signature"` -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *FeedReport) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - if _, ok := raw["BuyPrice"]; raw != nil && !ok { - return fmt.Errorf("field BuyPrice in FeedReport: required") - } - if _, ok := raw["FullReport"]; raw != nil && !ok { - return fmt.Errorf("field FullReport in FeedReport: required") - } - if _, ok := raw["ObservationTimestamp"]; raw != nil && !ok { - return fmt.Errorf("field ObservationTimestamp in FeedReport: required") - } - if _, ok := raw["ReportContext"]; raw != nil && !ok { - return fmt.Errorf("field ReportContext in FeedReport: required") - } - if _, ok := raw["SellPrice"]; raw != nil && !ok { - return fmt.Errorf("field SellPrice in FeedReport: required") - } - if _, ok := raw["Signature"]; raw != nil && !ok { - return fmt.Errorf("field Signature in FeedReport: required") - } - type Plain FeedReport - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - *j = FeedReport(plain) - return nil -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *Feed) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - if _, ok := raw["Metadata"]; raw != nil && !ok { - return fmt.Errorf("field Metadata in Feed: required") - } - if _, ok := raw["Payload"]; raw != nil && !ok { - return fmt.Errorf("field Payload in Feed: required") - } - if _, ok := raw["Timestamp"]; raw != nil && !ok { - return fmt.Errorf("field Timestamp in Feed: required") - } - type Plain Feed - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - *j = Feed(plain) - return nil -} - -type SignerMetadata struct { - // Signer corresponds to the JSON schema field "Signer". - Signer string `json:"Signer" yaml:"Signer" mapstructure:"Signer"` -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *SignerMetadata) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - if _, ok := raw["Signer"]; raw != nil && !ok { - return fmt.Errorf("field Signer in SignerMetadata: required") - } - type Plain SignerMetadata - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - *j = SignerMetadata(plain) - return nil -} - -// Streams Trigger -type Trigger struct { - // Config corresponds to the JSON schema field "config". - Config TriggerConfig `json:"config" yaml:"config" mapstructure:"config"` - - // Outputs corresponds to the JSON schema field "outputs". - Outputs *Feed `json:"outputs,omitempty" yaml:"outputs,omitempty" mapstructure:"outputs,omitempty"` -} - -type TriggerConfig struct { - // The interval in seconds after which a new trigger event is generated. - MaxFrequencyMs uint64 `json:"maxFrequencyMs" yaml:"maxFrequencyMs" mapstructure:"maxFrequencyMs"` -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *TriggerConfig) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - if _, ok := raw["maxFrequencyMs"]; raw != nil && !ok { - return fmt.Errorf("field maxFrequencyMs in TriggerConfig: required") - } - type Plain TriggerConfig - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - if 1 > plain.MaxFrequencyMs { - return fmt.Errorf("field %s: must be >= %v", "maxFrequencyMs", 1) - } - *j = TriggerConfig(plain) - return nil -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *Trigger) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - if _, ok := raw["config"]; raw != nil && !ok { - return fmt.Errorf("field config in Trigger: required") - } - type Plain Trigger - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - *j = Trigger(plain) - return nil -} diff --git a/pkg/workflows/sdk/testdata/fixtures/capabilities/notstreams/notstreamstest/trigger_mock_generated.go b/pkg/workflows/sdk/testdata/fixtures/capabilities/notstreams/notstreamstest/trigger_mock_generated.go deleted file mode 100644 index 057a29629c..0000000000 --- a/pkg/workflows/sdk/testdata/fixtures/capabilities/notstreams/notstreamstest/trigger_mock_generated.go +++ /dev/null @@ -1,17 +0,0 @@ -// Code generated by github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli, DO NOT EDIT. - -// Code generated by github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli, DO NOT EDIT. - -package notstreamstest - -import ( - "github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk/testdata/fixtures/capabilities/notstreams" - "github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk/testutils" -) - -// Trigger registers a new capability mock with the runner -func Trigger(runner *testutils.Runner, fn func() (notstreams.Feed, error)) *testutils.TriggerMock[notstreams.Feed] { - mock := testutils.MockTrigger[notstreams.Feed]("notstreams@1.0.0", fn) - runner.MockCapability("notstreams@1.0.0", nil, mock) - return mock -} diff --git a/pkg/workflows/sdk/testdata/fixtures/capabilities/notstreams/trigger_builders_generated.go b/pkg/workflows/sdk/testdata/fixtures/capabilities/notstreams/trigger_builders_generated.go deleted file mode 100644 index 42998133c4..0000000000 --- a/pkg/workflows/sdk/testdata/fixtures/capabilities/notstreams/trigger_builders_generated.go +++ /dev/null @@ -1,248 +0,0 @@ -// Code generated by github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli, DO NOT EDIT. - -package notstreams - -import ( - "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - "github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk" -) - -func (cfg TriggerConfig) New(w *sdk.WorkflowSpecFactory) FeedCap { - ref := "trigger" - def := sdk.StepDefinition{ - ID: "notstreams@1.0.0", Ref: ref, - Inputs: sdk.StepInputs{}, - Config: map[string]any{ - "maxFrequencyMs": cfg.MaxFrequencyMs, - }, - CapabilityType: capabilities.CapabilityTypeTrigger, - } - - step := sdk.Step[Feed]{Definition: def} - raw := step.AddTo(w) - return FeedWrapper(raw) -} - -// FeedWrapper allows access to field from an sdk.CapDefinition[Feed] -func FeedWrapper(raw sdk.CapDefinition[Feed]) FeedCap { - wrapped, ok := raw.(FeedCap) - if ok { - return wrapped - } - return &feedCap{CapDefinition: raw} -} - -type FeedCap interface { - sdk.CapDefinition[Feed] - Metadata() SignerMetadataCap - Payload() FeedReportCap - Timestamp() sdk.CapDefinition[int64] - private() -} - -type feedCap struct { - sdk.CapDefinition[Feed] -} - -func (*feedCap) private() {} -func (c *feedCap) Metadata() SignerMetadataCap { - return SignerMetadataWrapper(sdk.AccessField[Feed, SignerMetadata](c.CapDefinition, "Metadata")) -} -func (c *feedCap) Payload() FeedReportCap { - return FeedReportWrapper(sdk.AccessField[Feed, FeedReport](c.CapDefinition, "Payload")) -} -func (c *feedCap) Timestamp() sdk.CapDefinition[int64] { - return sdk.AccessField[Feed, int64](c.CapDefinition, "Timestamp") -} - -func ConstantFeed(value Feed) FeedCap { - return &feedCap{CapDefinition: sdk.ConstantDefinition(value)} -} - -func NewFeedFromFields( - metadata SignerMetadataCap, - payload FeedReportCap, - timestamp sdk.CapDefinition[int64]) FeedCap { - return &simpleFeed{ - CapDefinition: sdk.ComponentCapDefinition[Feed]{ - "Metadata": metadata.Ref(), - "Payload": payload.Ref(), - "Timestamp": timestamp.Ref(), - }, - metadata: metadata, - payload: payload, - timestamp: timestamp, - } -} - -type simpleFeed struct { - sdk.CapDefinition[Feed] - metadata SignerMetadataCap - payload FeedReportCap - timestamp sdk.CapDefinition[int64] -} - -func (c *simpleFeed) Metadata() SignerMetadataCap { - return c.metadata -} -func (c *simpleFeed) Payload() FeedReportCap { - return c.payload -} -func (c *simpleFeed) Timestamp() sdk.CapDefinition[int64] { - return c.timestamp -} - -func (c *simpleFeed) private() {} - -// FeedReportWrapper allows access to field from an sdk.CapDefinition[FeedReport] -func FeedReportWrapper(raw sdk.CapDefinition[FeedReport]) FeedReportCap { - wrapped, ok := raw.(FeedReportCap) - if ok { - return wrapped - } - return &feedReportCap{CapDefinition: raw} -} - -type FeedReportCap interface { - sdk.CapDefinition[FeedReport] - BuyPrice() sdk.CapDefinition[[]uint8] - FullReport() sdk.CapDefinition[[]uint8] - ObservationTimestamp() sdk.CapDefinition[int64] - ReportContext() sdk.CapDefinition[[]uint8] - SellPrice() sdk.CapDefinition[[]uint8] - Signature() sdk.CapDefinition[[]uint8] - private() -} - -type feedReportCap struct { - sdk.CapDefinition[FeedReport] -} - -func (*feedReportCap) private() {} -func (c *feedReportCap) BuyPrice() sdk.CapDefinition[[]uint8] { - return sdk.AccessField[FeedReport, []uint8](c.CapDefinition, "BuyPrice") -} -func (c *feedReportCap) FullReport() sdk.CapDefinition[[]uint8] { - return sdk.AccessField[FeedReport, []uint8](c.CapDefinition, "FullReport") -} -func (c *feedReportCap) ObservationTimestamp() sdk.CapDefinition[int64] { - return sdk.AccessField[FeedReport, int64](c.CapDefinition, "ObservationTimestamp") -} -func (c *feedReportCap) ReportContext() sdk.CapDefinition[[]uint8] { - return sdk.AccessField[FeedReport, []uint8](c.CapDefinition, "ReportContext") -} -func (c *feedReportCap) SellPrice() sdk.CapDefinition[[]uint8] { - return sdk.AccessField[FeedReport, []uint8](c.CapDefinition, "SellPrice") -} -func (c *feedReportCap) Signature() sdk.CapDefinition[[]uint8] { - return sdk.AccessField[FeedReport, []uint8](c.CapDefinition, "Signature") -} - -func ConstantFeedReport(value FeedReport) FeedReportCap { - return &feedReportCap{CapDefinition: sdk.ConstantDefinition(value)} -} - -func NewFeedReportFromFields( - buyPrice sdk.CapDefinition[[]uint8], - fullReport sdk.CapDefinition[[]uint8], - observationTimestamp sdk.CapDefinition[int64], - reportContext sdk.CapDefinition[[]uint8], - sellPrice sdk.CapDefinition[[]uint8], - signature sdk.CapDefinition[[]uint8]) FeedReportCap { - return &simpleFeedReport{ - CapDefinition: sdk.ComponentCapDefinition[FeedReport]{ - "BuyPrice": buyPrice.Ref(), - "FullReport": fullReport.Ref(), - "ObservationTimestamp": observationTimestamp.Ref(), - "ReportContext": reportContext.Ref(), - "SellPrice": sellPrice.Ref(), - "Signature": signature.Ref(), - }, - buyPrice: buyPrice, - fullReport: fullReport, - observationTimestamp: observationTimestamp, - reportContext: reportContext, - sellPrice: sellPrice, - signature: signature, - } -} - -type simpleFeedReport struct { - sdk.CapDefinition[FeedReport] - buyPrice sdk.CapDefinition[[]uint8] - fullReport sdk.CapDefinition[[]uint8] - observationTimestamp sdk.CapDefinition[int64] - reportContext sdk.CapDefinition[[]uint8] - sellPrice sdk.CapDefinition[[]uint8] - signature sdk.CapDefinition[[]uint8] -} - -func (c *simpleFeedReport) BuyPrice() sdk.CapDefinition[[]uint8] { - return c.buyPrice -} -func (c *simpleFeedReport) FullReport() sdk.CapDefinition[[]uint8] { - return c.fullReport -} -func (c *simpleFeedReport) ObservationTimestamp() sdk.CapDefinition[int64] { - return c.observationTimestamp -} -func (c *simpleFeedReport) ReportContext() sdk.CapDefinition[[]uint8] { - return c.reportContext -} -func (c *simpleFeedReport) SellPrice() sdk.CapDefinition[[]uint8] { - return c.sellPrice -} -func (c *simpleFeedReport) Signature() sdk.CapDefinition[[]uint8] { - return c.signature -} - -func (c *simpleFeedReport) private() {} - -// SignerMetadataWrapper allows access to field from an sdk.CapDefinition[SignerMetadata] -func SignerMetadataWrapper(raw sdk.CapDefinition[SignerMetadata]) SignerMetadataCap { - wrapped, ok := raw.(SignerMetadataCap) - if ok { - return wrapped - } - return &signerMetadataCap{CapDefinition: raw} -} - -type SignerMetadataCap interface { - sdk.CapDefinition[SignerMetadata] - Signer() sdk.CapDefinition[string] - private() -} - -type signerMetadataCap struct { - sdk.CapDefinition[SignerMetadata] -} - -func (*signerMetadataCap) private() {} -func (c *signerMetadataCap) Signer() sdk.CapDefinition[string] { - return sdk.AccessField[SignerMetadata, string](c.CapDefinition, "Signer") -} - -func ConstantSignerMetadata(value SignerMetadata) SignerMetadataCap { - return &signerMetadataCap{CapDefinition: sdk.ConstantDefinition(value)} -} - -func NewSignerMetadataFromFields( - signer sdk.CapDefinition[string]) SignerMetadataCap { - return &simpleSignerMetadata{ - CapDefinition: sdk.ComponentCapDefinition[SignerMetadata]{ - "Signer": signer.Ref(), - }, - signer: signer, - } -} - -type simpleSignerMetadata struct { - sdk.CapDefinition[SignerMetadata] - signer sdk.CapDefinition[string] -} - -func (c *simpleSignerMetadata) Signer() sdk.CapDefinition[string] { - return c.signer -} - -func (c *simpleSignerMetadata) private() {}