From aec73ec3d3660e377cee204820aa077694489972 Mon Sep 17 00:00:00 2001 From: ilija Date: Fri, 17 May 2024 14:05:53 +0200 Subject: [PATCH 01/21] Inital commit for log poller filters in config --- core/services/relay/evm/bindings.go | 18 +++++++---- core/services/relay/evm/chain_reader.go | 30 +++++++++++------ core/services/relay/evm/event_binding.go | 36 ++++++++++----------- core/services/relay/evm/types/types.go | 41 ++++++++++++++++++------ 4 files changed, 82 insertions(+), 43 deletions(-) diff --git a/core/services/relay/evm/bindings.go b/core/services/relay/evm/bindings.go index e13fcbc02d5..bac17d798e1 100644 --- a/core/services/relay/evm/bindings.go +++ b/core/services/relay/evm/bindings.go @@ -5,13 +5,19 @@ import ( "fmt" commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" ) // key is contract name type contractBindings map[string]readBindings -// key is read name -type readBindings map[string]readBinding +type readBindings struct { + // contractFilter is used to filter over all events or any subset of events with same filtering parameters. + // if an event is present in the contract filter, it can't define its own filter in the event binding. + contractFilter logpoller.Filter + // key is read name + bindings map[string]readBinding +} func (b contractBindings) GetReadBinding(contractName, readName string) (readBinding, error) { rb, rbExists := b[contractName] @@ -19,7 +25,7 @@ func (b contractBindings) GetReadBinding(contractName, readName string) (readBin return nil, fmt.Errorf("%w: no contract named %s", commontypes.ErrInvalidType, contractName) } - reader, readerExists := rb[readName] + reader, readerExists := rb.bindings[readName] if !readerExists { return nil, fmt.Errorf("%w: no readName named %s in contract %s", commontypes.ErrInvalidType, readName, contractName) } @@ -32,7 +38,7 @@ func (b contractBindings) AddReadBinding(contractName, readName string, reader r rbs = readBindings{} b[contractName] = rbs } - rbs[readName] = reader + rbs.bindings[readName] = reader } func (b contractBindings) Bind(ctx context.Context, boundContracts []commontypes.BoundContract) error { @@ -41,7 +47,7 @@ func (b contractBindings) Bind(ctx context.Context, boundContracts []commontypes if !rbsExist { return fmt.Errorf("%w: no contract named %s", commontypes.ErrInvalidConfig, bc.Name) } - for _, r := range rbs { + for _, r := range rbs.bindings { if err := r.Bind(ctx, bc); err != nil { return err } @@ -52,7 +58,7 @@ func (b contractBindings) Bind(ctx context.Context, boundContracts []commontypes func (b contractBindings) ForEach(ctx context.Context, fn func(readBinding, context.Context) error) error { for _, rbs := range b { - for _, rb := range rbs { + for _, rb := range rbs.bindings { if err := fn(rb, ctx); err != nil { return err } diff --git a/core/services/relay/evm/chain_reader.go b/core/services/relay/evm/chain_reader.go index 4a8c3691d1a..d8625308a0b 100644 --- a/core/services/relay/evm/chain_reader.go +++ b/core/services/relay/evm/chain_reader.go @@ -18,6 +18,7 @@ import ( evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services" "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" @@ -162,9 +163,9 @@ func (cr *chainReader) addMethod( return fmt.Errorf("%w: method %s doesn't exist", commontypes.ErrInvalidConfig, chainReaderDefinition.ChainSpecificName) } - if len(chainReaderDefinition.EventInputFields) != 0 { + if chainReaderDefinition.EventDefinitions != nil { return fmt.Errorf( - "%w: method %s has event topic fields defined, but is not an event", + "%w: method %s has event definition, but is not an event", commontypes.ErrInvalidConfig, chainReaderDefinition.ChainSpecificName) } @@ -207,16 +208,27 @@ func (cr *chainReader) addEvent(contractName, eventName string, a abi.ABI, chain return err } + eventDefinitions := chainReaderDefinition.EventDefinitions + pollingFilter := eventDefinitions.PollingFilter eb := &eventBinding{ - contractName: contractName, - eventName: eventName, - lp: cr.lp, + contractName: contractName, + eventName: eventName, + lp: cr.lp, + logPollerFilter: logpoller.Filter{ + EventSigs: evmtypes.HashArray{event.ID}, + Topic2: pollingFilter.Topic2, + Topic3: pollingFilter.Topic3, + Topic4: pollingFilter.Topic4, + Retention: pollingFilter.Retention.Duration(), + MaxLogsKept: pollingFilter.MaxLogsKept, + LogsPerBlock: pollingFilter.LogsPerBlock, + }, hash: event.ID, inputInfo: inputInfo, inputModifier: inputModifier, codecTopicInfo: codecTopicInfo, topics: make(map[string]topicDetail), - eventDataWords: chainReaderDefinition.GenericDataWordNames, + eventDataWords: eventDefinitions.GenericDataWordNames, id: wrapItemType(contractName, eventName, false) + uuid.NewString(), } @@ -224,7 +236,7 @@ func (cr *chainReader) addEvent(contractName, eventName string, a abi.ABI, chain // set topic mappings for QueryKeys for topicIndex, topic := range event.Inputs { - genericTopicName, ok := chainReaderDefinition.GenericTopicNames[topic.Name] + genericTopicName, ok := eventDefinitions.GenericTopicNames[topic.Name] if ok { eb.topics[genericTopicName] = topicDetail{ Argument: topic, @@ -262,7 +274,7 @@ func (cr *chainReader) getEventInput(def types.ChainReaderDefinition, contractNa } func verifyEventInputsUsed(chainReaderDefinition types.ChainReaderDefinition, indexArgNames map[string]bool) error { - for _, value := range chainReaderDefinition.EventInputFields { + for _, value := range chainReaderDefinition.EventDefinitions.InputFields { if !indexArgNames[abi.ToCamelCase(value)] { return fmt.Errorf("%w: %s is not an indexed argument of event %s", commontypes.ErrInvalidConfig, value, chainReaderDefinition.ChainSpecificName) } @@ -298,7 +310,7 @@ func (cr *chainReader) addDecoderDef(contractName, itemType string, outputs abi. func setupEventInput(event abi.Event, def types.ChainReaderDefinition) ([]abi.Argument, types.CodecEntry, map[string]bool) { topicFieldDefs := map[string]bool{} - for _, value := range def.EventInputFields { + for _, value := range def.EventDefinitions.InputFields { capFirstValue := abi.ToCamelCase(value) topicFieldDefs[capFirstValue] = true } diff --git a/core/services/relay/evm/event_binding.go b/core/services/relay/evm/event_binding.go index 98903f1463d..c94fff29fc3 100644 --- a/core/services/relay/evm/event_binding.go +++ b/core/services/relay/evm/event_binding.go @@ -21,19 +21,20 @@ import ( ) type eventBinding struct { - address common.Address - contractName string - eventName string - lp logpoller.LogPoller - hash common.Hash - codec commontypes.RemoteCodec - pending bool - bound bool - registerCalled bool - lock sync.Mutex - inputInfo types.CodecEntry - inputModifier codec.Modifier - codecTopicInfo types.CodecEntry + address common.Address + contractName string + eventName string + logPollerFilter logpoller.Filter + lp logpoller.LogPoller + hash common.Hash + codec commontypes.RemoteCodec + pending bool + bound bool + registerCalled bool + lock sync.Mutex + inputInfo types.CodecEntry + inputModifier codec.Modifier + codecTopicInfo types.CodecEntry // topics maps a generic topic name (key) to topic data topics map[string]topicDetail // eventDataWords maps a generic name to a word index @@ -63,11 +64,7 @@ func (e *eventBinding) Register(ctx context.Context) error { return nil } - if err := e.lp.RegisterFilter(ctx, logpoller.Filter{ - Name: e.id, - EventSigs: evmtypes.HashArray{e.hash}, - Addresses: evmtypes.AddressArray{e.address}, - }); err != nil { + if err := e.lp.RegisterFilter(ctx, e.logPollerFilter); err != nil { return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) } return nil @@ -140,7 +137,8 @@ func (e *eventBinding) Bind(ctx context.Context, binding commontypes.BoundContra } e.address = common.HexToAddress(binding.Address) - e.pending = binding.Pending + e.logPollerFilter.Name = logpoller.FilterName(e.id, e.address) + e.logPollerFilter.Addresses = evmtypes.AddressArray{e.address} e.bound = true if e.registerCalled { diff --git a/core/services/relay/evm/types/types.go b/core/services/relay/evm/types/types.go index 08eaba3fe1a..0d9402241cc 100644 --- a/core/services/relay/evm/types/types.go +++ b/core/services/relay/evm/types/types.go @@ -16,6 +16,8 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/codec" "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/types" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" + "github.com/smartcontractkit/chainlink/v2/core/store/models" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big" ) @@ -37,12 +39,26 @@ type ChainCodecConfig struct { type ChainContractReader struct { ContractABI string `json:"contractABI" toml:"contractABI"` + + ContractPollingFilter ContractPollingFilter `json:"contractPollingFilter"` // key is genericName from config Configs map[string]*ChainReaderDefinition `json:"configs" toml:"configs"` } type ChainReaderDefinition chainReaderDefinitionFields +type EventDefinitions struct { + PollingFilter PollingFilter `json:"pollingFilter"` + // GenericTopicNames helps QueryingKeys not rely on EVM specific topic names. Key is chain specific name, value is generic name. + // This helps us translate chain agnostic querying key "transfer-value" to EVM specific "evmTransferEvent-weiAmountTopic". + GenericTopicNames map[string]string `json:"genericTopicNames,omitempty"` + // key is a predefined generic name for evm log event data word + // for eg. first evm data word(32bytes) of USDC log event is value so the key can be called value + GenericDataWordNames map[string]uint8 `json:"genericDataWordNames,omitempty"` + // InputFields allows you to choose which indexed fields are expected from the input + InputFields []string `json:"inputFields,omitempty"` +} + // chainReaderDefinitionFields has the fields for ChainReaderDefinition but no methods. // This is necessary because package json recognizes the text encoding methods used for TOML, // and would infinitely recurse on itself. @@ -53,15 +69,7 @@ type chainReaderDefinitionFields struct { ReadType ReadType `json:"readType,omitempty"` InputModifications codec.ModifiersConfig `json:"inputModifications,omitempty"` OutputModifications codec.ModifiersConfig `json:"outputModifications,omitempty"` - - // EventInputFields allows you to choose which indexed fields are expected from the input - EventInputFields []string `json:"eventInputFields,omitempty"` - // GenericTopicNames helps QueryingKeys not rely on EVM specific topic names. Key is chain specific name, value is generic name. - // This helps us translate chain agnostic querying key "transfer-value" to EVM specific "evmTransferEvent-weiAmountTopic". - GenericTopicNames map[string]string `json:"genericTopicNames,omitempty"` - // key is a predefined generic name for evm log event data word - // for eg. first evm data word(32bytes) of USDC log event is value so the key can be called value - GenericDataWordNames map[string]uint8 `json:"genericDataWordNames,omitempty"` + EventDefinitions *EventDefinitions `json:"eventDefinitions,omitempty"` } func (d *ChainReaderDefinition) MarshalText() ([]byte, error) { @@ -111,6 +119,21 @@ func (r *ReadType) UnmarshalText(text []byte) error { return fmt.Errorf("unrecognized ReadType: %s", string(text)) } +type PollingFilter struct { + Topic2 evmtypes.HashArray `json:"topic2"` // list of possible values for topic2 + Topic3 evmtypes.HashArray `json:"topic3"` // list of possible values for topic3 + Topic4 evmtypes.HashArray `json:"topic4"` // list of possible values for topic4 + Retention models.Interval `json:"retention"` // maximum amount of time to retain logs + MaxLogsKept uint64 `json:"maxLogsKept"` // maximum number of logs to retain ( 0 = unlimited ) + LogsPerBlock uint64 `json:"logsPerBlock"` // rate limit ( maximum # of logs per block, 0 = unlimited ) +} + +type ContractPollingFilter struct { + EventKeys []string `json:"eventKeys"` // list of possible values for eventsig (aka topic1) + // contract wide polling filter + PollingFilter PollingFilter +} + type RelayConfig struct { ChainID *big.Big `json:"chainID"` FromBlock uint64 `json:"fromBlock"` From 58c637d5839d5ef32063a3f10b3a00b47e048c8a Mon Sep 17 00:00:00 2001 From: ilija Date: Wed, 29 May 2024 18:19:44 +0200 Subject: [PATCH 02/21] Change Chain Reader Lp filters from per event binding to per contract --- core/chains/evm/logpoller/log_poller.go | 18 ++--- core/services/relay/evm/binding.go | 4 +- core/services/relay/evm/bindings.go | 79 ++++++++++++++++---- core/services/relay/evm/chain_reader.go | 44 +++++------ core/services/relay/evm/chain_reader_test.go | 6 +- core/services/relay/evm/event_binding.go | 71 ++++-------------- core/services/relay/evm/method_binding.go | 3 +- core/services/relay/evm/types/types.go | 22 +----- 8 files changed, 112 insertions(+), 135 deletions(-) diff --git a/core/chains/evm/logpoller/log_poller.go b/core/chains/evm/logpoller/log_poller.go index 26978b18d48..dc7f989c67b 100644 --- a/core/chains/evm/logpoller/log_poller.go +++ b/core/chains/evm/logpoller/log_poller.go @@ -173,15 +173,15 @@ func NewLogPoller(orm ORM, ec Client, lggr logger.Logger, opts Opts) *logPoller } type Filter struct { - Name string // see FilterName(id, args) below - Addresses evmtypes.AddressArray - EventSigs evmtypes.HashArray // list of possible values for eventsig (aka topic1) - Topic2 evmtypes.HashArray // list of possible values for topic2 - Topic3 evmtypes.HashArray // list of possible values for topic3 - Topic4 evmtypes.HashArray // list of possible values for topic4 - Retention time.Duration // maximum amount of time to retain logs - MaxLogsKept uint64 // maximum number of logs to retain ( 0 = unlimited ) - LogsPerBlock uint64 // rate limit ( maximum # of logs per block, 0 = unlimited ) + Name string `json:"name"` // see FilterName(id, args) below + Addresses evmtypes.AddressArray `json:"addresses"` + EventSigs evmtypes.HashArray `json:"eventSigs"` // list of possible values for eventsig (aka topic1) + Topic2 evmtypes.HashArray `json:"topic2"` // list of possible values for topic2 + Topic3 evmtypes.HashArray `json:"topic3"` // list of possible values for topic3 + Topic4 evmtypes.HashArray `json:"topic4"` // list of possible values for topic4 + Retention time.Duration `json:"retention"` // maximum amount of time to retain logs + MaxLogsKept uint64 `json:"maxLogsKept"` // maximum number of logs to retain ( 0 = unlimited ) + LogsPerBlock uint64 `json:"logsPerBlock"` // rate limit ( maximum # of logs per block, 0 = unlimited ) } // FilterName is a suggested convenience function for clients to construct unique filter names diff --git a/core/services/relay/evm/binding.go b/core/services/relay/evm/binding.go index d7a04dcc9b9..273aebed2ef 100644 --- a/core/services/relay/evm/binding.go +++ b/core/services/relay/evm/binding.go @@ -10,8 +10,6 @@ import ( type readBinding interface { GetLatestValue(ctx context.Context, params, returnVal any) error QueryKey(ctx context.Context, filter query.KeyFilter, limitAndSort query.LimitAndSort, sequenceDataType any) ([]commontypes.Sequence, error) - Bind(ctx context.Context, binding commontypes.BoundContract) error + Bind(binding commontypes.BoundContract) SetCodec(codec commontypes.RemoteCodec) - Register(ctx context.Context) error - Unregister(ctx context.Context) error } diff --git a/core/services/relay/evm/bindings.go b/core/services/relay/evm/bindings.go index bac17d798e1..74915d198b8 100644 --- a/core/services/relay/evm/bindings.go +++ b/core/services/relay/evm/bindings.go @@ -3,23 +3,28 @@ package evm import ( "context" "fmt" + "sync" + + "github.com/ethereum/go-ethereum/common" commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" ) // key is contract name -type contractBindings map[string]readBindings +type bindings map[string]*contractBindings -type readBindings struct { +type contractBindings struct { // contractFilter is used to filter over all events or any subset of events with same filtering parameters. // if an event is present in the contract filter, it can't define its own filter in the event binding. - contractFilter logpoller.Filter + contractFilter logpoller.Filter + filterLock sync.Mutex + areEventFiltersRegistered bool // key is read name bindings map[string]readBinding } -func (b contractBindings) GetReadBinding(contractName, readName string) (readBinding, error) { +func (b bindings) GetReadBinding(contractName, readName string) (readBinding, error) { rb, rbExists := b[contractName] if !rbExists { return nil, fmt.Errorf("%w: no contract named %s", commontypes.ErrInvalidType, contractName) @@ -32,37 +37,79 @@ func (b contractBindings) GetReadBinding(contractName, readName string) (readBin return reader, nil } -func (b contractBindings) AddReadBinding(contractName, readName string, reader readBinding) { +func (b bindings) AddReadBinding(contractName, readName string, rb readBinding) { rbs, rbsExists := b[contractName] if !rbsExists { - rbs = readBindings{} + rbs = &contractBindings{} b[contractName] = rbs } - rbs.bindings[readName] = reader + rbs.bindings[readName] = rb } -func (b contractBindings) Bind(ctx context.Context, boundContracts []commontypes.BoundContract) error { +func (b bindings) Bind(ctx context.Context, logPoller logpoller.LogPoller, boundContracts []commontypes.BoundContract) error { for _, bc := range boundContracts { rbs, rbsExist := b[bc.Name] if !rbsExist { return fmt.Errorf("%w: no contract named %s", commontypes.ErrInvalidConfig, bc.Name) } + + rbs.contractFilter.Addresses = append(rbs.contractFilter.Addresses, common.HexToAddress(bc.Address)) + rbs.contractFilter.Name = logpoller.FilterName(bc.Name, bc.Address) + + if err := rbs.UnregisterEventFilters(ctx, logPoller); err != nil { + return err + } + + // if contract event filter isn't already registered then it will be by chain reader on startup + // if it is already registered then we are overriding filters registered on startup + if rbs.areEventFiltersRegistered { + return rbs.RegisterEventFilters(ctx, logPoller) + } + for _, r := range rbs.bindings { - if err := r.Bind(ctx, bc); err != nil { - return err - } + r.Bind(bc) } } return nil } -func (b contractBindings) ForEach(ctx context.Context, fn func(readBinding, context.Context) error) error { +func (b bindings) ForEach(ctx context.Context, fn func(context.Context, *contractBindings) error) error { for _, rbs := range b { - for _, rb := range rbs.bindings { - if err := fn(rb, ctx); err != nil { - return err - } + if err := fn(ctx, rbs); err != nil { + return err } } return nil } + +func (rb *contractBindings) RegisterEventFilters(ctx context.Context, logPoller logpoller.LogPoller) error { + rb.filterLock.Lock() + defer rb.filterLock.Unlock() + + rb.areEventFiltersRegistered = true + + if logPoller.HasFilter(rb.contractFilter.Name) { + return nil + } + + if err := logPoller.RegisterFilter(ctx, rb.contractFilter); err != nil { + return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) + } + + return nil + +} + +func (rb *contractBindings) UnregisterEventFilters(ctx context.Context, logPoller logpoller.LogPoller) error { + rb.filterLock.Lock() + defer rb.filterLock.Unlock() + + if !logPoller.HasFilter(rb.contractFilter.Name) { + return nil + } + + if err := logPoller.UnregisterFilter(ctx, rb.contractFilter.Name); err != nil { + return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) + } + return nil +} diff --git a/core/services/relay/evm/chain_reader.go b/core/services/relay/evm/chain_reader.go index 213816aeacd..dda0da7a781 100644 --- a/core/services/relay/evm/chain_reader.go +++ b/core/services/relay/evm/chain_reader.go @@ -8,17 +8,14 @@ import ( "time" "github.com/ethereum/go-ethereum/accounts/abi" - "github.com/google/uuid" "github.com/smartcontractkit/chainlink-common/pkg/codec" - "github.com/smartcontractkit/chainlink-common/pkg/types/query" - commonservices "github.com/smartcontractkit/chainlink-common/pkg/services" commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink-common/pkg/types/query" evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" - evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services" "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" @@ -33,7 +30,7 @@ type chainReader struct { lggr logger.Logger lp logpoller.LogPoller client evmclient.Client - contractBindings contractBindings + contractBindings bindings parsed *parsedTypes codec commontypes.RemoteCodec commonservices.StateMachine @@ -48,7 +45,7 @@ func NewChainReaderService(ctx context.Context, lggr logger.Logger, lp logpoller lggr: lggr.Named("ChainReader"), lp: lp, client: client, - contractBindings: contractBindings{}, + contractBindings: bindings{}, parsed: &parsedTypes{encoderDefs: map[string]types.CodecEntry{}, decoderDefs: map[string]types.CodecEntry{}}, } @@ -61,8 +58,10 @@ func NewChainReaderService(ctx context.Context, lggr logger.Logger, lp logpoller return nil, err } - err = cr.contractBindings.ForEach(ctx, func(b readBinding, c context.Context) error { - b.SetCodec(cr.codec) + err = cr.contractBindings.ForEach(ctx, func(c context.Context, rbs *contractBindings) error { + for _, rb := range rbs.bindings { + rb.SetCodec(cr.codec) + } return nil }) @@ -83,7 +82,7 @@ func (cr *chainReader) GetLatestValue(ctx context.Context, contractName, method } func (cr *chainReader) Bind(ctx context.Context, bindings []commontypes.BoundContract) error { - return cr.contractBindings.Bind(ctx, bindings) + return cr.contractBindings.Bind(ctx, cr.lp, bindings) } func (cr *chainReader) QueryKey(ctx context.Context, contractName string, filter query.KeyFilter, limitAndSort query.LimitAndSort, sequenceDataType any) ([]commontypes.Sequence, error) { @@ -119,13 +118,17 @@ func (cr *chainReader) init(chainContractReaders map[string]types.ChainContractR return err } } + + cr.contractBindings[contractName].contractFilter = chainContractReader.LogPollerFilter } return nil } func (cr *chainReader) Start(ctx context.Context) error { return cr.StartOnce("ChainReader", func() error { - return cr.contractBindings.ForEach(ctx, readBinding.Register) + return cr.contractBindings.ForEach(ctx, func(c context.Context, rbs *contractBindings) error { + return rbs.RegisterEventFilters(ctx, cr.lp) + }) }) } @@ -133,7 +136,9 @@ func (cr *chainReader) Close() error { return cr.StopOnce("ChainReader", func() error { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() - return cr.contractBindings.ForEach(ctx, readBinding.Unregister) + return cr.contractBindings.ForEach(ctx, func(c context.Context, rbs *contractBindings) error { + return rbs.UnregisterEventFilters(ctx, cr.lp) + }) }) } @@ -209,27 +214,16 @@ func (cr *chainReader) addEvent(contractName, eventName string, a abi.ABI, chain } eventDefinitions := chainReaderDefinition.EventDefinitions - pollingFilter := eventDefinitions.PollingFilter eb := &eventBinding{ - contractName: contractName, - eventName: eventName, - lp: cr.lp, - logPollerFilter: logpoller.Filter{ - EventSigs: evmtypes.HashArray{event.ID}, - Topic2: pollingFilter.Topic2, - Topic3: pollingFilter.Topic3, - Topic4: pollingFilter.Topic4, - Retention: pollingFilter.Retention.Duration(), - MaxLogsKept: pollingFilter.MaxLogsKept, - LogsPerBlock: pollingFilter.LogsPerBlock, - }, + contractName: contractName, + eventName: eventName, + lp: cr.lp, hash: event.ID, inputInfo: inputInfo, inputModifier: inputModifier, codecTopicInfo: codecTopicInfo, topics: make(map[string]topicDetail), eventDataWords: eventDefinitions.GenericDataWordNames, - id: wrapItemType(contractName, eventName, false) + uuid.NewString(), } cr.contractBindings.AddReadBinding(contractName, eventName, eb) diff --git a/core/services/relay/evm/chain_reader_test.go b/core/services/relay/evm/chain_reader_test.go index fc62022f8b5..163fb2bd573 100644 --- a/core/services/relay/evm/chain_reader_test.go +++ b/core/services/relay/evm/chain_reader_test.go @@ -196,12 +196,12 @@ func (it *chainReaderInterfaceTester) Setup(t *testing.T) { EventWithFilterName: { ChainSpecificName: "Triggered", ReadType: types.Event, - EventInputFields: []string{"Field"}, + EventDefinitions: &types.EventDefinitions{InputFields: []string{"Field"}}, }, triggerWithDynamicTopic: { ChainSpecificName: triggerWithDynamicTopic, ReadType: types.Event, - EventInputFields: []string{"fieldHash"}, + EventDefinitions: &types.EventDefinitions{InputFields: []string{"fieldHash"}}, InputModifications: codec.ModifiersConfig{ &codec.RenameModifierConfig{Fields: map[string]string{"FieldHash": "Field"}}, }, @@ -209,7 +209,7 @@ func (it *chainReaderInterfaceTester) Setup(t *testing.T) { triggerWithAllTopics: { ChainSpecificName: triggerWithAllTopics, ReadType: types.Event, - EventInputFields: []string{"Field1", "Field2", "Field3"}, + EventDefinitions: &types.EventDefinitions{InputFields: []string{"Field1", "Field2", "Field3"}}, }, MethodReturningSeenStruct: { ChainSpecificName: "returnSeen", diff --git a/core/services/relay/evm/event_binding.go b/core/services/relay/evm/event_binding.go index c94fff29fc3..8bbf75a0332 100644 --- a/core/services/relay/evm/event_binding.go +++ b/core/services/relay/evm/event_binding.go @@ -5,7 +5,6 @@ import ( "fmt" "reflect" "strings" - "sync" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" @@ -21,27 +20,23 @@ import ( ) type eventBinding struct { - address common.Address - contractName string - eventName string - logPollerFilter logpoller.Filter - lp logpoller.LogPoller - hash common.Hash - codec commontypes.RemoteCodec - pending bool - bound bool - registerCalled bool - lock sync.Mutex - inputInfo types.CodecEntry - inputModifier codec.Modifier - codecTopicInfo types.CodecEntry + address common.Address + contractName string + eventName string + lp logpoller.LogPoller + hash common.Hash + codec commontypes.RemoteCodec + pending bool + bound bool + inputInfo types.CodecEntry + inputModifier codec.Modifier + codecTopicInfo types.CodecEntry // topics maps a generic topic name (key) to topic data topics map[string]topicDetail // eventDataWords maps a generic name to a word index // key is a predefined generic name for evm log event data word // for eg. first evm data word(32bytes) of USDC log event is value so the key can be called value eventDataWords map[string]uint8 - id string } type topicDetail struct { @@ -55,40 +50,12 @@ func (e *eventBinding) SetCodec(codec commontypes.RemoteCodec) { e.codec = codec } -func (e *eventBinding) Register(ctx context.Context) error { - e.lock.Lock() - defer e.lock.Unlock() - - e.registerCalled = true - if !e.bound || e.lp.HasFilter(e.id) { - return nil - } - - if err := e.lp.RegisterFilter(ctx, e.logPollerFilter); err != nil { - return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) - } - return nil -} - -func (e *eventBinding) Unregister(ctx context.Context) error { - e.lock.Lock() - defer e.lock.Unlock() - - if !e.lp.HasFilter(e.id) { - return nil - } - - if err := e.lp.UnregisterFilter(ctx, e.id); err != nil { - return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) - } - return nil -} - func (e *eventBinding) GetLatestValue(ctx context.Context, params, into any) error { if !e.bound { return fmt.Errorf("%w: event not bound", commontypes.ErrInvalidType) } + // TODO BCF-3247 change GetLatestValue to use chain agnostic confidence levels confs := evmtypes.Finalized if e.pending { confs = evmtypes.Unconfirmed @@ -131,20 +98,10 @@ func (e *eventBinding) QueryKey(ctx context.Context, filter query.KeyFilter, lim return e.decodeLogsIntoSequences(ctx, logs, sequenceDataType) } -func (e *eventBinding) Bind(ctx context.Context, binding commontypes.BoundContract) error { - if err := e.Unregister(ctx); err != nil { - return err - } - +func (e *eventBinding) Bind(binding commontypes.BoundContract) { e.address = common.HexToAddress(binding.Address) - e.logPollerFilter.Name = logpoller.FilterName(e.id, e.address) - e.logPollerFilter.Addresses = evmtypes.AddressArray{e.address} + e.pending = binding.Pending e.bound = true - - if e.registerCalled { - return e.Register(ctx) - } - return nil } func (e *eventBinding) getLatestValueWithoutFilters(ctx context.Context, confs evmtypes.Confirmations, into any) error { diff --git a/core/services/relay/evm/method_binding.go b/core/services/relay/evm/method_binding.go index 7484d17c3ef..ed6c204315a 100644 --- a/core/services/relay/evm/method_binding.go +++ b/core/services/relay/evm/method_binding.go @@ -68,8 +68,7 @@ func (m *methodBinding) QueryKey(_ context.Context, _ query.KeyFilter, _ query.L return nil, nil } -func (m *methodBinding) Bind(_ context.Context, binding commontypes.BoundContract) error { +func (m *methodBinding) Bind(binding commontypes.BoundContract) { m.address = common.HexToAddress(binding.Address) m.bound = true - return nil } diff --git a/core/services/relay/evm/types/types.go b/core/services/relay/evm/types/types.go index 0d9402241cc..d9cf51a317a 100644 --- a/core/services/relay/evm/types/types.go +++ b/core/services/relay/evm/types/types.go @@ -16,9 +16,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/codec" "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/types" - evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" - "github.com/smartcontractkit/chainlink/v2/core/store/models" - + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big" ) @@ -40,7 +38,7 @@ type ChainCodecConfig struct { type ChainContractReader struct { ContractABI string `json:"contractABI" toml:"contractABI"` - ContractPollingFilter ContractPollingFilter `json:"contractPollingFilter"` + LogPollerFilter logpoller.Filter `json:"logPollerFilter,omitempty"` // key is genericName from config Configs map[string]*ChainReaderDefinition `json:"configs" toml:"configs"` } @@ -48,7 +46,6 @@ type ChainContractReader struct { type ChainReaderDefinition chainReaderDefinitionFields type EventDefinitions struct { - PollingFilter PollingFilter `json:"pollingFilter"` // GenericTopicNames helps QueryingKeys not rely on EVM specific topic names. Key is chain specific name, value is generic name. // This helps us translate chain agnostic querying key "transfer-value" to EVM specific "evmTransferEvent-weiAmountTopic". GenericTopicNames map[string]string `json:"genericTopicNames,omitempty"` @@ -119,21 +116,6 @@ func (r *ReadType) UnmarshalText(text []byte) error { return fmt.Errorf("unrecognized ReadType: %s", string(text)) } -type PollingFilter struct { - Topic2 evmtypes.HashArray `json:"topic2"` // list of possible values for topic2 - Topic3 evmtypes.HashArray `json:"topic3"` // list of possible values for topic3 - Topic4 evmtypes.HashArray `json:"topic4"` // list of possible values for topic4 - Retention models.Interval `json:"retention"` // maximum amount of time to retain logs - MaxLogsKept uint64 `json:"maxLogsKept"` // maximum number of logs to retain ( 0 = unlimited ) - LogsPerBlock uint64 `json:"logsPerBlock"` // rate limit ( maximum # of logs per block, 0 = unlimited ) -} - -type ContractPollingFilter struct { - EventKeys []string `json:"eventKeys"` // list of possible values for eventsig (aka topic1) - // contract wide polling filter - PollingFilter PollingFilter -} - type RelayConfig struct { ChainID *big.Big `json:"chainID"` FromBlock uint64 `json:"fromBlock"` From 53a7dfb3387b3b3d96d46cdf35bdeb54ef73803d Mon Sep 17 00:00:00 2001 From: ilija Date: Thu, 30 May 2024 11:54:54 +0200 Subject: [PATCH 03/21] Fix lint --- core/services/relay/evm/bindings.go | 1 - core/services/relay/evm/chain_reader.go | 18 +++++++++--------- core/services/relay/evm/chain_reader_test.go | 4 ++-- core/services/relay/evm/event_binding.go | 4 ++-- core/services/relay/evm/types/types.go | 1 - 5 files changed, 13 insertions(+), 15 deletions(-) diff --git a/core/services/relay/evm/bindings.go b/core/services/relay/evm/bindings.go index 74915d198b8..1bd0adc36ec 100644 --- a/core/services/relay/evm/bindings.go +++ b/core/services/relay/evm/bindings.go @@ -97,7 +97,6 @@ func (rb *contractBindings) RegisterEventFilters(ctx context.Context, logPoller } return nil - } func (rb *contractBindings) UnregisterEventFilters(ctx context.Context, logPoller logpoller.LogPoller) error { diff --git a/core/services/relay/evm/chain_reader.go b/core/services/relay/evm/chain_reader.go index 65a45ff288f..c834499c303 100644 --- a/core/services/relay/evm/chain_reader.go +++ b/core/services/relay/evm/chain_reader.go @@ -224,15 +224,15 @@ func (cr *chainReader) addEvent(contractName, eventName string, a abi.ABI, chain eventDefinitions := chainReaderDefinition.EventDefinitions eb := &eventBinding{ - contractName: contractName, - eventName: eventName, - lp: cr.lp, - hash: event.ID, - inputInfo: inputInfo, - inputModifier: inputModifier, - codecTopicInfo: codecTopicInfo, - topics: make(map[string]topicDetail), - eventDataWords: eventDefinitions.GenericDataWordNames, + contractName: contractName, + eventName: eventName, + lp: cr.lp, + hash: event.ID, + inputInfo: inputInfo, + inputModifier: inputModifier, + codecTopicInfo: codecTopicInfo, + topics: make(map[string]topicDetail), + eventDataWords: eventDefinitions.GenericDataWordNames, confirmationsMapping: confirmations, } diff --git a/core/services/relay/evm/chain_reader_test.go b/core/services/relay/evm/chain_reader_test.go index d8a8deb0932..c1825d0b885 100644 --- a/core/services/relay/evm/chain_reader_test.go +++ b/core/services/relay/evm/chain_reader_test.go @@ -197,7 +197,7 @@ func (it *chainReaderInterfaceTester) Setup(t *testing.T) { EventWithFilterName: { ChainSpecificName: "Triggered", ReadType: types.Event, - EventDefinitions: &types.EventDefinitions{InputFields: []string{"Field"}}, + EventDefinitions: &types.EventDefinitions{InputFields: []string{"Field"}}, ConfidenceConfirmations: map[string]int{"0.0": 0, "1.0": -1}, }, triggerWithDynamicTopic: { @@ -212,7 +212,7 @@ func (it *chainReaderInterfaceTester) Setup(t *testing.T) { triggerWithAllTopics: { ChainSpecificName: triggerWithAllTopics, ReadType: types.Event, - EventDefinitions: &types.EventDefinitions{InputFields: []string{"Field1", "Field2", "Field3"}}, + EventDefinitions: &types.EventDefinitions{InputFields: []string{"Field1", "Field2", "Field3"}}, ConfidenceConfirmations: map[string]int{"0.0": 0, "1.0": -1}, }, MethodReturningSeenStruct: { diff --git a/core/services/relay/evm/event_binding.go b/core/services/relay/evm/event_binding.go index 431f2484f1d..fac7b3e42e7 100644 --- a/core/services/relay/evm/event_binding.go +++ b/core/services/relay/evm/event_binding.go @@ -35,8 +35,8 @@ type eventBinding struct { topics map[string]topicDetail // eventDataWords maps a generic name to a word index // key is a predefined generic name for evm log event data word - // for eg. first evm data word(32bytes) of USDC log event is value so the key can be called value - eventDataWords map[string]uint8 + // for e.g. first evm data word(32bytes) of USDC log event is value so the key can be called value + eventDataWords map[string]uint8 confirmationsMapping map[primitives.ConfidenceLevel]evmtypes.Confirmations } diff --git a/core/services/relay/evm/types/types.go b/core/services/relay/evm/types/types.go index 117a12bc4f4..defbb3ac9a2 100644 --- a/core/services/relay/evm/types/types.go +++ b/core/services/relay/evm/types/types.go @@ -72,7 +72,6 @@ type chainReaderDefinitionFields struct { ConfidenceConfirmations map[string]int `json:"confidenceConfirmations,omitempty"` } - func (d *ChainReaderDefinition) MarshalText() ([]byte, error) { var b bytes.Buffer e := json.NewEncoder(&b) From 0055f696edec425fffbe8c02bea852c82fa6665a Mon Sep 17 00:00:00 2001 From: ilija Date: Thu, 30 May 2024 16:29:03 +0200 Subject: [PATCH 04/21] Add ChReader contract log poller filter init --- core/services/relay/evm/chain_reader.go | 18 +++++++- core/services/relay/evm/types/types.go | 42 ++++++++++++++++--- .../actions/ocr2_helpers_local.go | 1 + 3 files changed, 54 insertions(+), 7 deletions(-) diff --git a/core/services/relay/evm/chain_reader.go b/core/services/relay/evm/chain_reader.go index c834499c303..5cb57628154 100644 --- a/core/services/relay/evm/chain_reader.go +++ b/core/services/relay/evm/chain_reader.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "reflect" + "slices" "strings" "time" @@ -105,11 +106,25 @@ func (cr *chainReader) init(chainContractReaders map[string]types.ChainContractR return err } + var eventSigsForContractFilter evmtypes.HashArray for typeName, chainReaderDefinition := range chainContractReader.Configs { switch chainReaderDefinition.ReadType { case types.Method: err = cr.addMethod(contractName, typeName, contractAbi, *chainReaderDefinition) case types.Event: + if chainReaderDefinition.HasPollingFilter() { + if slices.Contains(chainContractReader.GenericEventNames, typeName) { + return fmt.Errorf( + "%w: invalid chain reader polling filter definition, "+ + "can't have polling filter defined both on contract and event level: %s", + commontypes.ErrInvalidConfig, + chainReaderDefinition.ReadType) + } + } else { + eventSig := contractAbi.Events[chainReaderDefinition.ChainSpecificName] + eventSigsForContractFilter = append(eventSigsForContractFilter, eventSig.ID) + } + err = cr.addEvent(contractName, typeName, contractAbi, *chainReaderDefinition) default: return fmt.Errorf( @@ -122,8 +137,7 @@ func (cr *chainReader) init(chainContractReaders map[string]types.ChainContractR return err } } - - cr.contractBindings[contractName].contractFilter = chainContractReader.LogPollerFilter + cr.contractBindings[contractName].contractFilter = chainContractReader.PollingFilter.ToLPFilter(eventSigsForContractFilter) } return nil } diff --git a/core/services/relay/evm/types/types.go b/core/services/relay/evm/types/types.go index defbb3ac9a2..e632e45d667 100644 --- a/core/services/relay/evm/types/types.go +++ b/core/services/relay/evm/types/types.go @@ -17,7 +17,9 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big" + "github.com/smartcontractkit/chainlink/v2/core/store/models" ) type ChainReaderConfig struct { @@ -35,10 +37,35 @@ type ChainCodecConfig struct { ModifierConfigs codec.ModifiersConfig `toml:"modifierConfigs,omitempty"` } -type ChainContractReader struct { - ContractABI string `json:"contractABI" toml:"contractABI"` +type ContractPollingFilter struct { + GenericEventNames []string `json:"genericEventNames"` + PollingFilter +} + +type PollingFilter struct { + Topic2 evmtypes.HashArray `json:"topic2"` // list of possible values for topic2 + Topic3 evmtypes.HashArray `json:"topic3"` // list of possible values for topic3 + Topic4 evmtypes.HashArray `json:"topic4"` // list of possible values for topic4 + Retention models.Interval `json:"retention"` // maximum amount of time to retain logs + MaxLogsKept uint64 `json:"maxLogsKept"` // maximum number of logs to retain ( 0 = unlimited ) + LogsPerBlock uint64 `json:"logsPerBlock"` // rate limit ( maximum # of logs per block, 0 = unlimited ) +} + +func (f *PollingFilter) ToLPFilter(eventSigs evmtypes.HashArray) logpoller.Filter { + return logpoller.Filter{ + EventSigs: eventSigs, + Topic2: f.Topic2, + Topic3: f.Topic3, + Topic4: f.Topic4, + Retention: f.Retention.Duration(), + MaxLogsKept: f.MaxLogsKept, + LogsPerBlock: f.LogsPerBlock, + } +} - LogPollerFilter logpoller.Filter `json:"logPollerFilter,omitempty"` +type ChainContractReader struct { + ContractABI string `json:"contractABI" toml:"contractABI"` + ContractPollingFilter `json:"pollingFilter,omitempty"` // key is genericName from config Configs map[string]*ChainReaderDefinition `json:"configs" toml:"configs"` } @@ -50,10 +77,11 @@ type EventDefinitions struct { // This helps us translate chain agnostic querying key "transfer-value" to EVM specific "evmTransferEvent-weiAmountTopic". GenericTopicNames map[string]string `json:"genericTopicNames,omitempty"` // key is a predefined generic name for evm log event data word - // for eg. first evm data word(32bytes) of USDC log event is value so the key can be called value + // for e.g. first evm data word(32bytes) of USDC log event is value so the key can be called value GenericDataWordNames map[string]uint8 `json:"genericDataWordNames,omitempty"` // InputFields allows you to choose which indexed fields are expected from the input - InputFields []string `json:"inputFields,omitempty"` + InputFields []string `json:"inputFields,omitempty"` + *PollingFilter `json:"pollingFilter,omitempty"` } // chainReaderDefinitionFields has the fields for ChainReaderDefinition but no methods. @@ -72,6 +100,10 @@ type chainReaderDefinitionFields struct { ConfidenceConfirmations map[string]int `json:"confidenceConfirmations,omitempty"` } +func (d *ChainReaderDefinition) HasPollingFilter() bool { + return d.EventDefinitions == nil && d.EventDefinitions.PollingFilter == nil +} + func (d *ChainReaderDefinition) MarshalText() ([]byte, error) { var b bytes.Buffer e := json.NewEncoder(&b) diff --git a/integration-tests/actions/ocr2_helpers_local.go b/integration-tests/actions/ocr2_helpers_local.go index 8a0a02c050f..e733f8292ee 100644 --- a/integration-tests/actions/ocr2_helpers_local.go +++ b/integration-tests/actions/ocr2_helpers_local.go @@ -153,6 +153,7 @@ func CreateOCRv2JobsLocal( ReadType: evmtypes.Event, }, }, + // TODO add polling filter definition }, }, } From a1cdcf93e1ce6e7f575e5ffd5a42c09138b4d2f6 Mon Sep 17 00:00:00 2001 From: ilija Date: Fri, 31 May 2024 15:57:55 +0200 Subject: [PATCH 05/21] Add per event polling filter override for Chain Reader --- core/services/relay/evm/binding.go | 2 + core/services/relay/evm/bindings.go | 63 ++++++++++++++++-------- core/services/relay/evm/chain_reader.go | 40 +++++++++------ core/services/relay/evm/event_binding.go | 59 +++++++++++++++++----- core/services/relay/evm/types/types.go | 5 +- 5 files changed, 120 insertions(+), 49 deletions(-) diff --git a/core/services/relay/evm/binding.go b/core/services/relay/evm/binding.go index 273aebed2ef..03ac7c56a8e 100644 --- a/core/services/relay/evm/binding.go +++ b/core/services/relay/evm/binding.go @@ -11,5 +11,7 @@ type readBinding interface { GetLatestValue(ctx context.Context, params, returnVal any) error QueryKey(ctx context.Context, filter query.KeyFilter, limitAndSort query.LimitAndSort, sequenceDataType any) ([]commontypes.Sequence, error) Bind(binding commontypes.BoundContract) + Register(ctx context.Context) error + Unregister(ctx context.Context) error SetCodec(codec commontypes.RemoteCodec) } diff --git a/core/services/relay/evm/bindings.go b/core/services/relay/evm/bindings.go index 1bd0adc36ec..921a9e8aaf2 100644 --- a/core/services/relay/evm/bindings.go +++ b/core/services/relay/evm/bindings.go @@ -6,20 +6,25 @@ import ( "sync" "github.com/ethereum/go-ethereum/common" + "github.com/google/uuid" commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" ) // key is contract name type bindings map[string]*contractBindings +type FilterRegisterer struct { + pollingFilter logpoller.Filter + filterLock sync.Mutex + isRegistered bool +} + type contractBindings struct { - // contractFilter is used to filter over all events or any subset of events with same filtering parameters. - // if an event is present in the contract filter, it can't define its own filter in the event binding. - contractFilter logpoller.Filter - filterLock sync.Mutex - areEventFiltersRegistered bool + // FilterRegisterer is used to manage polling filter registration. + FilterRegisterer // key is read name bindings map[string]readBinding } @@ -53,22 +58,25 @@ func (b bindings) Bind(ctx context.Context, logPoller logpoller.LogPoller, bound return fmt.Errorf("%w: no contract named %s", commontypes.ErrInvalidConfig, bc.Name) } - rbs.contractFilter.Addresses = append(rbs.contractFilter.Addresses, common.HexToAddress(bc.Address)) - rbs.contractFilter.Name = logpoller.FilterName(bc.Name, bc.Address) + rbs.pollingFilter.Addresses = evmtypes.AddressArray{common.HexToAddress(bc.Address)} + rbs.pollingFilter.Name = logpoller.FilterName(bc.Name+"."+uuid.NewString(), bc.Address) - if err := rbs.UnregisterEventFilters(ctx, logPoller); err != nil { + // we are changing contract address reference, so we need to unregister old filters + if err := rbs.Unregister(ctx, logPoller); err != nil { return err } - // if contract event filter isn't already registered then it will be by chain reader on startup - // if it is already registered then we are overriding filters registered on startup - if rbs.areEventFiltersRegistered { - return rbs.RegisterEventFilters(ctx, logPoller) - } - for _, r := range rbs.bindings { r.Bind(bc) } + + // if contract event filters aren't already registered then they will on startup + // if they are already registered then we are overriding them because contract binding (address) has changed + if rbs.isRegistered { + if err := rbs.Register(ctx, logPoller); err != nil { + return err + } + } } return nil } @@ -82,33 +90,46 @@ func (b bindings) ForEach(ctx context.Context, fn func(context.Context, *contrac return nil } -func (rb *contractBindings) RegisterEventFilters(ctx context.Context, logPoller logpoller.LogPoller) error { +// Register registers polling filters. +func (rb *contractBindings) Register(ctx context.Context, logPoller logpoller.LogPoller) error { rb.filterLock.Lock() defer rb.filterLock.Unlock() - rb.areEventFiltersRegistered = true + rb.isRegistered = true - if logPoller.HasFilter(rb.contractFilter.Name) { + if logPoller.HasFilter(rb.pollingFilter.Name) { return nil } - if err := logPoller.RegisterFilter(ctx, rb.contractFilter); err != nil { + if err := logPoller.RegisterFilter(ctx, rb.pollingFilter); err != nil { return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) } + for _, binding := range rb.bindings { + if err := binding.Register(ctx); err != nil { + return err + } + } return nil } -func (rb *contractBindings) UnregisterEventFilters(ctx context.Context, logPoller logpoller.LogPoller) error { +// Unregister unregisters polling filters. +func (rb *contractBindings) Unregister(ctx context.Context, logPoller logpoller.LogPoller) error { rb.filterLock.Lock() defer rb.filterLock.Unlock() - if !logPoller.HasFilter(rb.contractFilter.Name) { + if !logPoller.HasFilter(rb.pollingFilter.Name) { return nil } - if err := logPoller.UnregisterFilter(ctx, rb.contractFilter.Name); err != nil { + if err := logPoller.UnregisterFilter(ctx, rb.pollingFilter.Name); err != nil { return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) } + + for _, binding := range rb.bindings { + if err := binding.Unregister(ctx); err != nil { + return err + } + } return nil } diff --git a/core/services/relay/evm/chain_reader.go b/core/services/relay/evm/chain_reader.go index 5cb57628154..c1f94d5605f 100644 --- a/core/services/relay/evm/chain_reader.go +++ b/core/services/relay/evm/chain_reader.go @@ -106,23 +106,27 @@ func (cr *chainReader) init(chainContractReaders map[string]types.ChainContractR return err } - var eventSigsForContractFilter evmtypes.HashArray + var eventSigsForPollingFilter evmtypes.HashArray for typeName, chainReaderDefinition := range chainContractReader.Configs { switch chainReaderDefinition.ReadType { case types.Method: err = cr.addMethod(contractName, typeName, contractAbi, *chainReaderDefinition) case types.Event: - if chainReaderDefinition.HasPollingFilter() { - if slices.Contains(chainContractReader.GenericEventNames, typeName) { - return fmt.Errorf( - "%w: invalid chain reader polling filter definition, "+ - "can't have polling filter defined both on contract and event level: %s", - commontypes.ErrInvalidConfig, - chainReaderDefinition.ReadType) - } - } else { - eventSig := contractAbi.Events[chainReaderDefinition.ChainSpecificName] - eventSigsForContractFilter = append(eventSigsForContractFilter, eventSig.ID) + partOfContractPollingFilter := slices.Contains(chainContractReader.GenericEventNames, typeName) + hasContractFilterOverride := chainReaderDefinition.HasPollingFilter() + if !partOfContractPollingFilter && !chainReaderDefinition.HasPollingFilter() { + return fmt.Errorf( + "%w: chain reader has no polling filter defined for contract: %s event: %s", + commontypes.ErrInvalidConfig, contractName, typeName) + } + if hasContractFilterOverride && partOfContractPollingFilter { + return fmt.Errorf( + "%w: conflicting chain reader polling filter definitions for contract: %s event: %s, can't have polling filter defined both on contract and event level", + commontypes.ErrInvalidConfig, contractName, typeName) + } + + if !hasContractFilterOverride { + eventSigsForPollingFilter = append(eventSigsForPollingFilter, contractAbi.Events[chainReaderDefinition.ChainSpecificName].ID) } err = cr.addEvent(contractName, typeName, contractAbi, *chainReaderDefinition) @@ -132,12 +136,11 @@ func (cr *chainReader) init(chainContractReaders map[string]types.ChainContractR commontypes.ErrInvalidConfig, chainReaderDefinition.ReadType) } - if err != nil { return err } } - cr.contractBindings[contractName].contractFilter = chainContractReader.PollingFilter.ToLPFilter(eventSigsForContractFilter) + cr.contractBindings[contractName].pollingFilter = chainContractReader.PollingFilter.ToLPFilter(eventSigsForPollingFilter) } return nil } @@ -145,7 +148,7 @@ func (cr *chainReader) init(chainContractReaders map[string]types.ChainContractR func (cr *chainReader) Start(ctx context.Context) error { return cr.StartOnce("ChainReader", func() error { return cr.contractBindings.ForEach(ctx, func(c context.Context, rbs *contractBindings) error { - return rbs.RegisterEventFilters(ctx, cr.lp) + return rbs.Register(ctx, cr.lp) }) }) } @@ -155,7 +158,7 @@ func (cr *chainReader) Close() error { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() return cr.contractBindings.ForEach(ctx, func(c context.Context, rbs *contractBindings) error { - return rbs.UnregisterEventFilters(ctx, cr.lp) + return rbs.Unregister(ctx, cr.lp) }) }) } @@ -212,6 +215,10 @@ func (cr *chainReader) addEvent(contractName, eventName string, a abi.ABI, chain return fmt.Errorf("%w: event %s doesn't exist", commontypes.ErrInvalidConfig, chainReaderDefinition.ChainSpecificName) } + if chainReaderDefinition.EventDefinitions == nil { + return fmt.Errorf("%w: event %s doesn't have event definitions set", commontypes.ErrInvalidConfig, chainReaderDefinition.ChainSpecificName) + } + filterArgs, codecTopicInfo, indexArgNames := setupEventInput(event, chainReaderDefinition) if err := verifyEventInputsUsed(chainReaderDefinition, indexArgNames); err != nil { return err @@ -240,6 +247,7 @@ func (cr *chainReader) addEvent(contractName, eventName string, a abi.ABI, chain eb := &eventBinding{ contractName: contractName, eventName: eventName, + logPollerFilter: eventDefinitions.PollingFilter.ToLPFilter(evmtypes.HashArray{a.Events[event.Name].ID}), lp: cr.lp, hash: event.ID, inputInfo: inputInfo, diff --git a/core/services/relay/evm/event_binding.go b/core/services/relay/evm/event_binding.go index fac7b3e42e7..cc4f7891c77 100644 --- a/core/services/relay/evm/event_binding.go +++ b/core/services/relay/evm/event_binding.go @@ -8,6 +8,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" + "github.com/google/uuid" "github.com/smartcontractkit/chainlink-common/pkg/codec" commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" @@ -20,17 +21,19 @@ import ( ) type eventBinding struct { - address common.Address - contractName string - eventName string - lp logpoller.LogPoller - hash common.Hash - codec commontypes.RemoteCodec - pending bool - bound bool - inputInfo types.CodecEntry - inputModifier codec.Modifier - codecTopicInfo types.CodecEntry + FilterRegisterer + address common.Address + contractName string + eventName string + lp logpoller.LogPoller + logPollerFilter logpoller.Filter + hash common.Hash + codec commontypes.RemoteCodec + pending bool + bound bool + inputInfo types.CodecEntry + inputModifier codec.Modifier + codecTopicInfo types.CodecEntry // topics maps a generic topic name (key) to topic data topics map[string]topicDetail // eventDataWords maps a generic name to a word index @@ -51,6 +54,35 @@ func (e *eventBinding) SetCodec(codec commontypes.RemoteCodec) { e.codec = codec } +func (e *eventBinding) Register(ctx context.Context) error { + e.filterLock.Lock() + defer e.filterLock.Unlock() + + e.isRegistered = true + if !e.bound || e.lp.HasFilter(e.pollingFilter.Name) { + return nil + } + + if err := e.lp.RegisterFilter(ctx, e.logPollerFilter); err != nil { + return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) + } + return nil +} + +func (e *eventBinding) Unregister(ctx context.Context) error { + e.filterLock.Lock() + defer e.filterLock.Unlock() + + if !e.lp.HasFilter(e.pollingFilter.Name) { + return nil + } + + if err := e.lp.UnregisterFilter(ctx, e.pollingFilter.Name); err != nil { + return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) + } + return nil +} + func (e *eventBinding) GetLatestValue(ctx context.Context, params, into any) error { if !e.bound { return fmt.Errorf("%w: event not bound", commontypes.ErrInvalidType) @@ -102,6 +134,11 @@ func (e *eventBinding) QueryKey(ctx context.Context, filter query.KeyFilter, lim func (e *eventBinding) Bind(binding commontypes.BoundContract) { e.address = common.HexToAddress(binding.Address) e.pending = binding.Pending + + id := fmt.Sprintf("%s,%s,%s", e.contractName, e.eventName, uuid.NewString()) + e.logPollerFilter.Name = logpoller.FilterName(id, e.address) + e.logPollerFilter.Addresses = evmtypes.AddressArray{e.address} + e.bound = true } diff --git a/core/services/relay/evm/types/types.go b/core/services/relay/evm/types/types.go index e632e45d667..a8c78b517dc 100644 --- a/core/services/relay/evm/types/types.go +++ b/core/services/relay/evm/types/types.go @@ -101,7 +101,10 @@ type chainReaderDefinitionFields struct { } func (d *ChainReaderDefinition) HasPollingFilter() bool { - return d.EventDefinitions == nil && d.EventDefinitions.PollingFilter == nil + if d.EventDefinitions == nil && d.EventDefinitions.PollingFilter == nil { + return false + } + return true } func (d *ChainReaderDefinition) MarshalText() ([]byte, error) { From 474097a6f9102e727b0a2bdeeec2da6d8ed7e941 Mon Sep 17 00:00:00 2001 From: ilija Date: Tue, 4 Jun 2024 18:29:05 +0200 Subject: [PATCH 06/21] Fix event definitions and filters handling in CR --- core/chains/evm/logpoller/log_poller.go | 18 ++-- .../features/ocr2/features_ocr2_test.go | 2 + core/services/relay/evm/bindings.go | 18 ++-- core/services/relay/evm/chain_reader.go | 86 +++++++++++-------- core/services/relay/evm/chain_reader_test.go | 2 - core/services/relay/evm/event_binding.go | 45 ++++++---- core/services/relay/evm/types/types.go | 16 ++-- .../actions/ocr2_helpers_local.go | 5 +- 8 files changed, 108 insertions(+), 84 deletions(-) diff --git a/core/chains/evm/logpoller/log_poller.go b/core/chains/evm/logpoller/log_poller.go index dc7f989c67b..26978b18d48 100644 --- a/core/chains/evm/logpoller/log_poller.go +++ b/core/chains/evm/logpoller/log_poller.go @@ -173,15 +173,15 @@ func NewLogPoller(orm ORM, ec Client, lggr logger.Logger, opts Opts) *logPoller } type Filter struct { - Name string `json:"name"` // see FilterName(id, args) below - Addresses evmtypes.AddressArray `json:"addresses"` - EventSigs evmtypes.HashArray `json:"eventSigs"` // list of possible values for eventsig (aka topic1) - Topic2 evmtypes.HashArray `json:"topic2"` // list of possible values for topic2 - Topic3 evmtypes.HashArray `json:"topic3"` // list of possible values for topic3 - Topic4 evmtypes.HashArray `json:"topic4"` // list of possible values for topic4 - Retention time.Duration `json:"retention"` // maximum amount of time to retain logs - MaxLogsKept uint64 `json:"maxLogsKept"` // maximum number of logs to retain ( 0 = unlimited ) - LogsPerBlock uint64 `json:"logsPerBlock"` // rate limit ( maximum # of logs per block, 0 = unlimited ) + Name string // see FilterName(id, args) below + Addresses evmtypes.AddressArray + EventSigs evmtypes.HashArray // list of possible values for eventsig (aka topic1) + Topic2 evmtypes.HashArray // list of possible values for topic2 + Topic3 evmtypes.HashArray // list of possible values for topic3 + Topic4 evmtypes.HashArray // list of possible values for topic4 + Retention time.Duration // maximum amount of time to retain logs + MaxLogsKept uint64 // maximum number of logs to retain ( 0 = unlimited ) + LogsPerBlock uint64 // rate limit ( maximum # of logs per block, 0 = unlimited ) } // FilterName is a suggested convenience function for clients to construct unique filter names diff --git a/core/internal/features/ocr2/features_ocr2_test.go b/core/internal/features/ocr2/features_ocr2_test.go index 9b950f487bf..5dc39ccfc7a 100644 --- a/core/internal/features/ocr2/features_ocr2_test.go +++ b/core/internal/features/ocr2/features_ocr2_test.go @@ -319,6 +319,8 @@ fromBlock = %d if test.chainReaderAndCodec { chainReaderSpec = ` [relayConfig.chainReader.contracts.median] +contractPollingFilter.genericEventNames = ["LatestRoundRequested"] + contractABI = ''' [ { diff --git a/core/services/relay/evm/bindings.go b/core/services/relay/evm/bindings.go index 921a9e8aaf2..0e4d9a9da4c 100644 --- a/core/services/relay/evm/bindings.go +++ b/core/services/relay/evm/bindings.go @@ -23,10 +23,10 @@ type FilterRegisterer struct { } type contractBindings struct { - // FilterRegisterer is used to manage polling filter registration. + // FilterRegisterer is used to manage polling filter registration for the contact wide event filter. FilterRegisterer // key is read name - bindings map[string]readBinding + readBindings map[string]readBinding } func (b bindings) GetReadBinding(contractName, readName string) (readBinding, error) { @@ -35,7 +35,7 @@ func (b bindings) GetReadBinding(contractName, readName string) (readBinding, er return nil, fmt.Errorf("%w: no contract named %s", commontypes.ErrInvalidType, contractName) } - reader, readerExists := rb.bindings[readName] + reader, readerExists := rb.readBindings[readName] if !readerExists { return nil, fmt.Errorf("%w: no readName named %s in contract %s", commontypes.ErrInvalidType, readName, contractName) } @@ -45,10 +45,10 @@ func (b bindings) GetReadBinding(contractName, readName string) (readBinding, er func (b bindings) AddReadBinding(contractName, readName string, rb readBinding) { rbs, rbsExists := b[contractName] if !rbsExists { - rbs = &contractBindings{} + rbs = &contractBindings{readBindings: make(map[string]readBinding)} b[contractName] = rbs } - rbs.bindings[readName] = rb + rbs.readBindings[readName] = rb } func (b bindings) Bind(ctx context.Context, logPoller logpoller.LogPoller, boundContracts []commontypes.BoundContract) error { @@ -61,12 +61,12 @@ func (b bindings) Bind(ctx context.Context, logPoller logpoller.LogPoller, bound rbs.pollingFilter.Addresses = evmtypes.AddressArray{common.HexToAddress(bc.Address)} rbs.pollingFilter.Name = logpoller.FilterName(bc.Name+"."+uuid.NewString(), bc.Address) - // we are changing contract address reference, so we need to unregister old filters + // we are changing contract address reference, so we need to unregister old filters if they exist if err := rbs.Unregister(ctx, logPoller); err != nil { return err } - for _, r := range rbs.bindings { + for _, r := range rbs.readBindings { r.Bind(bc) } @@ -105,7 +105,7 @@ func (rb *contractBindings) Register(ctx context.Context, logPoller logpoller.Lo return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) } - for _, binding := range rb.bindings { + for _, binding := range rb.readBindings { if err := binding.Register(ctx); err != nil { return err } @@ -126,7 +126,7 @@ func (rb *contractBindings) Unregister(ctx context.Context, logPoller logpoller. return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) } - for _, binding := range rb.bindings { + for _, binding := range rb.readBindings { if err := binding.Unregister(ctx); err != nil { return err } diff --git a/core/services/relay/evm/chain_reader.go b/core/services/relay/evm/chain_reader.go index c1f94d5605f..9ee5ffd63bd 100644 --- a/core/services/relay/evm/chain_reader.go +++ b/core/services/relay/evm/chain_reader.go @@ -7,6 +7,7 @@ import ( "reflect" "slices" "strings" + "sync" "time" "github.com/ethereum/go-ethereum/accounts/abi" @@ -64,7 +65,7 @@ func NewChainReaderService(ctx context.Context, lggr logger.Logger, lp logpoller } err = cr.contractBindings.ForEach(ctx, func(c context.Context, rbs *contractBindings) error { - for _, rb := range rbs.bindings { + for _, rb := range rbs.readBindings { rb.SetCodec(cr.codec) } return nil @@ -106,27 +107,29 @@ func (cr *chainReader) init(chainContractReaders map[string]types.ChainContractR return err } - var eventSigsForPollingFilter evmtypes.HashArray + contractFilterEvents := chainContractReader.ContractPollingFilter.GenericEventNames + var eventSigsForContractFilter evmtypes.HashArray for typeName, chainReaderDefinition := range chainContractReader.Configs { switch chainReaderDefinition.ReadType { case types.Method: err = cr.addMethod(contractName, typeName, contractAbi, *chainReaderDefinition) case types.Event: - partOfContractPollingFilter := slices.Contains(chainContractReader.GenericEventNames, typeName) - hasContractFilterOverride := chainReaderDefinition.HasPollingFilter() - if !partOfContractPollingFilter && !chainReaderDefinition.HasPollingFilter() { + partOfContractFilter := slices.Contains(contractFilterEvents, typeName) + if !partOfContractFilter && !chainReaderDefinition.HasPollingFilter() { return fmt.Errorf( - "%w: chain reader has no polling filter defined for contract: %s event: %s", + "%w: chain reader has no polling filter defined for contract: %s, event: %s", commontypes.ErrInvalidConfig, contractName, typeName) } - if hasContractFilterOverride && partOfContractPollingFilter { + + eventOverridesContractFilter := chainReaderDefinition.HasPollingFilter() + if eventOverridesContractFilter && partOfContractFilter { return fmt.Errorf( "%w: conflicting chain reader polling filter definitions for contract: %s event: %s, can't have polling filter defined both on contract and event level", commontypes.ErrInvalidConfig, contractName, typeName) } - if !hasContractFilterOverride { - eventSigsForPollingFilter = append(eventSigsForPollingFilter, contractAbi.Events[chainReaderDefinition.ChainSpecificName].ID) + if !eventOverridesContractFilter { + eventSigsForContractFilter = append(eventSigsForContractFilter, contractAbi.Events[chainReaderDefinition.ChainSpecificName].ID) } err = cr.addEvent(contractName, typeName, contractAbi, *chainReaderDefinition) @@ -140,7 +143,7 @@ func (cr *chainReader) init(chainContractReaders map[string]types.ChainContractR return err } } - cr.contractBindings[contractName].pollingFilter = chainContractReader.PollingFilter.ToLPFilter(eventSigsForPollingFilter) + cr.contractBindings[contractName].pollingFilter = chainContractReader.PollingFilter.ToLPFilter(eventSigsForContractFilter) } return nil } @@ -189,13 +192,6 @@ func (cr *chainReader) addMethod( return fmt.Errorf("%w: method %s doesn't exist", commontypes.ErrInvalidConfig, chainReaderDefinition.ChainSpecificName) } - if chainReaderDefinition.EventDefinitions != nil { - return fmt.Errorf( - "%w: method %s has event definition, but is not an event", - commontypes.ErrInvalidConfig, - chainReaderDefinition.ChainSpecificName) - } - cr.contractBindings.AddReadBinding(contractName, methodName, &methodBinding{ contractName: contractName, method: methodName, @@ -215,12 +211,13 @@ func (cr *chainReader) addEvent(contractName, eventName string, a abi.ABI, chain return fmt.Errorf("%w: event %s doesn't exist", commontypes.ErrInvalidConfig, chainReaderDefinition.ChainSpecificName) } - if chainReaderDefinition.EventDefinitions == nil { - return fmt.Errorf("%w: event %s doesn't have event definitions set", commontypes.ErrInvalidConfig, chainReaderDefinition.ChainSpecificName) + var inputFields []string + if chainReaderDefinition.EventDefinitions != nil { + inputFields = chainReaderDefinition.EventDefinitions.InputFields } - filterArgs, codecTopicInfo, indexArgNames := setupEventInput(event, chainReaderDefinition) - if err := verifyEventInputsUsed(chainReaderDefinition, indexArgNames); err != nil { + filterArgs, codecTopicInfo, indexArgNames := setupEventInput(event, inputFields) + if err := verifyEventInputsUsed(eventName, inputFields, indexArgNames); err != nil { return err } @@ -243,44 +240,57 @@ func (cr *chainReader) addEvent(contractName, eventName string, a abi.ABI, chain return err } - eventDefinitions := chainReaderDefinition.EventDefinitions eb := &eventBinding{ contractName: contractName, eventName: eventName, - logPollerFilter: eventDefinitions.PollingFilter.ToLPFilter(evmtypes.HashArray{a.Events[event.Name].ID}), lp: cr.lp, hash: event.ID, inputInfo: inputInfo, inputModifier: inputModifier, codecTopicInfo: codecTopicInfo, topics: make(map[string]topicDetail), - eventDataWords: eventDefinitions.GenericDataWordNames, + eventDataWords: make(map[string]uint8), confirmationsMapping: confirmations, } + if eventDefinitions := chainReaderDefinition.EventDefinitions; eventDefinitions != nil { + if eventDefinitions.PollingFilter != nil { + eb.FilterRegisterer = &FilterRegisterer{ + pollingFilter: eventDefinitions.PollingFilter.ToLPFilter(evmtypes.HashArray{a.Events[event.Name].ID}), + filterLock: sync.Mutex{}, + } + } + + if eventDefinitions.GenericDataWordNames != nil { + eb.eventDataWords = eventDefinitions.GenericDataWordNames + } + + cr.addQueryingReadBindings(contractName, eventDefinitions.GenericTopicNames, event.Inputs, eb) + } + cr.contractBindings.AddReadBinding(contractName, eventName, eb) - // set topic mappings for QueryKeys - for topicIndex, topic := range event.Inputs { - genericTopicName, ok := eventDefinitions.GenericTopicNames[topic.Name] + return cr.addDecoderDef(contractName, eventName, event.Inputs, chainReaderDefinition) +} + +// addQueryingReadBindings reuses the eventBinding and maps it to topic and dataWord keys used for QueryKey. +func (cr *chainReader) addQueryingReadBindings(contractName string, genericTopicNames map[string]string, eventInputs abi.Arguments, eb *eventBinding) { + // add topic read readBindings for QueryKey + for topicIndex, topic := range eventInputs { + genericTopicName, ok := genericTopicNames[topic.Name] if ok { eb.topics[genericTopicName] = topicDetail{ Argument: topic, Index: uint64(topicIndex), } } - - // this way querying by key/s values comparison can find its bindings cr.contractBindings.AddReadBinding(contractName, genericTopicName, eb) } - // set data word mappings for QueryKeys + // add data word read readBindings for QueryKey for genericDataWordName := range eb.eventDataWords { - // this way querying by key/s values comparison can find its bindings cr.contractBindings.AddReadBinding(contractName, genericDataWordName, eb) } - - return cr.addDecoderDef(contractName, eventName, event.Inputs, chainReaderDefinition) } func (cr *chainReader) getEventInput(def types.ChainReaderDefinition, contractName, eventName string) ( @@ -299,10 +309,10 @@ func (cr *chainReader) getEventInput(def types.ChainReaderDefinition, contractNa return inputInfo, inMod, nil } -func verifyEventInputsUsed(chainReaderDefinition types.ChainReaderDefinition, indexArgNames map[string]bool) error { - for _, value := range chainReaderDefinition.EventDefinitions.InputFields { +func verifyEventInputsUsed(eventName string, inputFields []string, indexArgNames map[string]bool) error { + for _, value := range inputFields { if !indexArgNames[abi.ToCamelCase(value)] { - return fmt.Errorf("%w: %s is not an indexed argument of event %s", commontypes.ErrInvalidConfig, value, chainReaderDefinition.ChainSpecificName) + return fmt.Errorf("%w: %s is not an indexed argument of event %s", commontypes.ErrInvalidConfig, value, eventName) } } return nil @@ -334,9 +344,9 @@ func (cr *chainReader) addDecoderDef(contractName, itemType string, outputs abi. return output.Init() } -func setupEventInput(event abi.Event, def types.ChainReaderDefinition) ([]abi.Argument, types.CodecEntry, map[string]bool) { +func setupEventInput(event abi.Event, inputFields []string) ([]abi.Argument, types.CodecEntry, map[string]bool) { topicFieldDefs := map[string]bool{} - for _, value := range def.EventDefinitions.InputFields { + for _, value := range inputFields { capFirstValue := abi.ToCamelCase(value) topicFieldDefs[capFirstValue] = true } diff --git a/core/services/relay/evm/chain_reader_test.go b/core/services/relay/evm/chain_reader_test.go index c1825d0b885..d4c304ad9b0 100644 --- a/core/services/relay/evm/chain_reader_test.go +++ b/core/services/relay/evm/chain_reader_test.go @@ -49,10 +49,8 @@ const ( func TestChainReaderInterfaceTests(t *testing.T) { t.Parallel() it := &chainReaderInterfaceTester{} - RunChainReaderInterfaceTests(t, it) RunChainReaderInterfaceTests(t, commontestutils.WrapChainReaderTesterForLoop(it)) - t.Run("Dynamically typed topics can be used to filter and have type correct in return", func(t *testing.T) { it.Setup(t) diff --git a/core/services/relay/evm/event_binding.go b/core/services/relay/evm/event_binding.go index cc4f7891c77..a5294851460 100644 --- a/core/services/relay/evm/event_binding.go +++ b/core/services/relay/evm/event_binding.go @@ -21,19 +21,20 @@ import ( ) type eventBinding struct { - FilterRegisterer - address common.Address - contractName string - eventName string - lp logpoller.LogPoller - logPollerFilter logpoller.Filter - hash common.Hash - codec commontypes.RemoteCodec - pending bool - bound bool - inputInfo types.CodecEntry - inputModifier codec.Modifier - codecTopicInfo types.CodecEntry + address common.Address + contractName string + eventName string + lp logpoller.LogPoller + // FilterRegisterer in eventBinding is to be used as an override for lp filter defined in the contract binding. + // If FilterRegisterer is nil, this event should be registered with the lp filter defined in the contract binding. + *FilterRegisterer + hash common.Hash + codec commontypes.RemoteCodec + pending bool + bound bool + inputInfo types.CodecEntry + inputModifier codec.Modifier + codecTopicInfo types.CodecEntry // topics maps a generic topic name (key) to topic data topics map[string]topicDetail // eventDataWords maps a generic name to a word index @@ -55,6 +56,10 @@ func (e *eventBinding) SetCodec(codec commontypes.RemoteCodec) { } func (e *eventBinding) Register(ctx context.Context) error { + if e.FilterRegisterer == nil { + return nil + } + e.filterLock.Lock() defer e.filterLock.Unlock() @@ -63,13 +68,17 @@ func (e *eventBinding) Register(ctx context.Context) error { return nil } - if err := e.lp.RegisterFilter(ctx, e.logPollerFilter); err != nil { + if err := e.lp.RegisterFilter(ctx, e.pollingFilter); err != nil { return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) } return nil } func (e *eventBinding) Unregister(ctx context.Context) error { + if e.FilterRegisterer == nil { + return nil + } + e.filterLock.Lock() defer e.filterLock.Unlock() @@ -135,9 +144,11 @@ func (e *eventBinding) Bind(binding commontypes.BoundContract) { e.address = common.HexToAddress(binding.Address) e.pending = binding.Pending - id := fmt.Sprintf("%s,%s,%s", e.contractName, e.eventName, uuid.NewString()) - e.logPollerFilter.Name = logpoller.FilterName(id, e.address) - e.logPollerFilter.Addresses = evmtypes.AddressArray{e.address} + if e.FilterRegisterer != nil { + id := fmt.Sprintf("%s,%s,%s", e.contractName, e.eventName, uuid.NewString()) + e.pollingFilter.Name = logpoller.FilterName(id, e.address) + e.pollingFilter.Addresses = evmtypes.AddressArray{e.address} + } e.bound = true } diff --git a/core/services/relay/evm/types/types.go b/core/services/relay/evm/types/types.go index a8c78b517dc..73c41a0fa56 100644 --- a/core/services/relay/evm/types/types.go +++ b/core/services/relay/evm/types/types.go @@ -39,7 +39,7 @@ type ChainCodecConfig struct { type ContractPollingFilter struct { GenericEventNames []string `json:"genericEventNames"` - PollingFilter + PollingFilter `json:"pollingFilter"` } type PollingFilter struct { @@ -65,7 +65,7 @@ func (f *PollingFilter) ToLPFilter(eventSigs evmtypes.HashArray) logpoller.Filte type ChainContractReader struct { ContractABI string `json:"contractABI" toml:"contractABI"` - ContractPollingFilter `json:"pollingFilter,omitempty"` + ContractPollingFilter `json:"contractPollingFilter,omitempty" toml:"contractPollingFilter,omitempty"` // key is genericName from config Configs map[string]*ChainReaderDefinition `json:"configs" toml:"configs"` } @@ -80,7 +80,10 @@ type EventDefinitions struct { // for e.g. first evm data word(32bytes) of USDC log event is value so the key can be called value GenericDataWordNames map[string]uint8 `json:"genericDataWordNames,omitempty"` // InputFields allows you to choose which indexed fields are expected from the input - InputFields []string `json:"inputFields,omitempty"` + InputFields []string `json:"inputFields,omitempty"` + // PollingFilter should be defined on a contract level in ContractPollingFilter, + // unless event needs to override the contract level filter arguments. + // This will create a separate log poller filter for this event. *PollingFilter `json:"pollingFilter,omitempty"` } @@ -94,17 +97,14 @@ type chainReaderDefinitionFields struct { ReadType ReadType `json:"readType,omitempty"` InputModifications codec.ModifiersConfig `json:"inputModifications,omitempty"` OutputModifications codec.ModifiersConfig `json:"outputModifications,omitempty"` - EventDefinitions *EventDefinitions `json:"eventDefinitions,omitempty"` + EventDefinitions *EventDefinitions `json:"eventDefinitions,omitempty" toml:"eventDefinitions,omitempty"` // ConfidenceConfirmations is a mapping between a ConfidenceLevel and the confirmations associated. Confidence levels // should be valid float values. ConfidenceConfirmations map[string]int `json:"confidenceConfirmations,omitempty"` } func (d *ChainReaderDefinition) HasPollingFilter() bool { - if d.EventDefinitions == nil && d.EventDefinitions.PollingFilter == nil { - return false - } - return true + return d.EventDefinitions != nil && d.EventDefinitions.PollingFilter != nil } func (d *ChainReaderDefinition) MarshalText() ([]byte, error) { diff --git a/integration-tests/actions/ocr2_helpers_local.go b/integration-tests/actions/ocr2_helpers_local.go index e733f8292ee..ac28925532b 100644 --- a/integration-tests/actions/ocr2_helpers_local.go +++ b/integration-tests/actions/ocr2_helpers_local.go @@ -132,6 +132,10 @@ func CreateOCRv2JobsLocal( ocrSpec.OCR2OracleSpec.RelayConfig["chainReader"] = evmtypes.ChainReaderConfig{ Contracts: map[string]evmtypes.ChainContractReader{ "median": { + ContractPollingFilter: evmtypes.ContractPollingFilter{ + GenericEventNames: []string{"LatestRoundRequested"}, + PollingFilter: evmtypes.PollingFilter{}, + }, ContractABI: `[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"requester","type":"address"},{"indexed":false,"internalType":"bytes32","name":"configDigest","type":"bytes32"},{"indexed":false,"internalType":"uint32","name":"epoch","type":"uint32"},{"indexed":false,"internalType":"uint8","name":"round","type":"uint8"}],"name":"RoundRequested","type":"event"},{"inputs":[],"name":"latestTransmissionDetails","outputs":[{"internalType":"bytes32","name":"configDigest","type":"bytes32"},{"internalType":"uint32","name":"epoch","type":"uint32"},{"internalType":"uint8","name":"round","type":"uint8"},{"internalType":"int192","name":"latestAnswer_","type":"int192"},{"internalType":"uint64","name":"latestTimestamp_","type":"uint64"}],"stateMutability":"view","type":"function"}]`, Configs: map[string]*evmtypes.ChainReaderDefinition{ "LatestTransmissionDetails": { @@ -153,7 +157,6 @@ func CreateOCRv2JobsLocal( ReadType: evmtypes.Event, }, }, - // TODO add polling filter definition }, }, } From 46e211d87fd04d065a733748668fe33d0f09857a Mon Sep 17 00:00:00 2001 From: ilija Date: Tue, 4 Jun 2024 18:50:25 +0200 Subject: [PATCH 07/21] Add changeset and lint --- .changeset/eleven-buckets-search.md | 5 +++++ core/services/relay/evm/chain_reader.go | 3 +-- core/services/relay/evm/types/types.go | 2 +- integration-tests/actions/ocr2_helpers_local.go | 1 - 4 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 .changeset/eleven-buckets-search.md diff --git a/.changeset/eleven-buckets-search.md b/.changeset/eleven-buckets-search.md new file mode 100644 index 00000000000..6c68fbcfdcc --- /dev/null +++ b/.changeset/eleven-buckets-search.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +#internal Add Log Poller support to Chain Reader through setting them in config. All filters should be part of the contract wide filter unless an event needs specific polling configuration, which can be set on a per event basis.. diff --git a/core/services/relay/evm/chain_reader.go b/core/services/relay/evm/chain_reader.go index 9ee5ffd63bd..1ae6286ad3f 100644 --- a/core/services/relay/evm/chain_reader.go +++ b/core/services/relay/evm/chain_reader.go @@ -107,14 +107,13 @@ func (cr *chainReader) init(chainContractReaders map[string]types.ChainContractR return err } - contractFilterEvents := chainContractReader.ContractPollingFilter.GenericEventNames var eventSigsForContractFilter evmtypes.HashArray for typeName, chainReaderDefinition := range chainContractReader.Configs { switch chainReaderDefinition.ReadType { case types.Method: err = cr.addMethod(contractName, typeName, contractAbi, *chainReaderDefinition) case types.Event: - partOfContractFilter := slices.Contains(contractFilterEvents, typeName) + partOfContractFilter := slices.Contains(chainContractReader.GenericEventNames, typeName) if !partOfContractFilter && !chainReaderDefinition.HasPollingFilter() { return fmt.Errorf( "%w: chain reader has no polling filter defined for contract: %s, event: %s", diff --git a/core/services/relay/evm/types/types.go b/core/services/relay/evm/types/types.go index 73c41a0fa56..b871b820363 100644 --- a/core/services/relay/evm/types/types.go +++ b/core/services/relay/evm/types/types.go @@ -82,7 +82,7 @@ type EventDefinitions struct { // InputFields allows you to choose which indexed fields are expected from the input InputFields []string `json:"inputFields,omitempty"` // PollingFilter should be defined on a contract level in ContractPollingFilter, - // unless event needs to override the contract level filter arguments. + // unless event needs to override the contract level filter options. // This will create a separate log poller filter for this event. *PollingFilter `json:"pollingFilter,omitempty"` } diff --git a/integration-tests/actions/ocr2_helpers_local.go b/integration-tests/actions/ocr2_helpers_local.go index ac28925532b..733b6903552 100644 --- a/integration-tests/actions/ocr2_helpers_local.go +++ b/integration-tests/actions/ocr2_helpers_local.go @@ -134,7 +134,6 @@ func CreateOCRv2JobsLocal( "median": { ContractPollingFilter: evmtypes.ContractPollingFilter{ GenericEventNames: []string{"LatestRoundRequested"}, - PollingFilter: evmtypes.PollingFilter{}, }, ContractABI: `[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"requester","type":"address"},{"indexed":false,"internalType":"bytes32","name":"configDigest","type":"bytes32"},{"indexed":false,"internalType":"uint32","name":"epoch","type":"uint32"},{"indexed":false,"internalType":"uint8","name":"round","type":"uint8"}],"name":"RoundRequested","type":"event"},{"inputs":[],"name":"latestTransmissionDetails","outputs":[{"internalType":"bytes32","name":"configDigest","type":"bytes32"},{"internalType":"uint32","name":"epoch","type":"uint32"},{"internalType":"uint8","name":"round","type":"uint8"},{"internalType":"int192","name":"latestAnswer_","type":"int192"},{"internalType":"uint64","name":"latestTimestamp_","type":"uint64"}],"stateMutability":"view","type":"function"}]`, Configs: map[string]*evmtypes.ChainReaderDefinition{ From ed7fbc36d57ceccd669a6be44ae687e4920a0eeb Mon Sep 17 00:00:00 2001 From: ilija Date: Tue, 4 Jun 2024 18:57:43 +0200 Subject: [PATCH 08/21] Update variable naming in chain reader bindings.go --- core/services/relay/evm/bindings.go | 80 ++++++++++++------------- core/services/relay/evm/chain_reader.go | 6 +- 2 files changed, 43 insertions(+), 43 deletions(-) diff --git a/core/services/relay/evm/bindings.go b/core/services/relay/evm/bindings.go index 0e4d9a9da4c..bf20092e9c6 100644 --- a/core/services/relay/evm/bindings.go +++ b/core/services/relay/evm/bindings.go @@ -14,7 +14,7 @@ import ( ) // key is contract name -type bindings map[string]*contractBindings +type bindings map[string]*contractBinding type FilterRegisterer struct { pollingFilter logpoller.Filter @@ -22,58 +22,58 @@ type FilterRegisterer struct { isRegistered bool } -type contractBindings struct { +type contractBinding struct { // FilterRegisterer is used to manage polling filter registration for the contact wide event filter. FilterRegisterer - // key is read name + // key is read name method, event or event keys used for queryKey. readBindings map[string]readBinding } func (b bindings) GetReadBinding(contractName, readName string) (readBinding, error) { - rb, rbExists := b[contractName] - if !rbExists { + cb, cbExists := b[contractName] + if !cbExists { return nil, fmt.Errorf("%w: no contract named %s", commontypes.ErrInvalidType, contractName) } - reader, readerExists := rb.readBindings[readName] - if !readerExists { + rb, rbExists := cb.readBindings[readName] + if !rbExists { return nil, fmt.Errorf("%w: no readName named %s in contract %s", commontypes.ErrInvalidType, readName, contractName) } - return reader, nil + return rb, nil } func (b bindings) AddReadBinding(contractName, readName string, rb readBinding) { - rbs, rbsExists := b[contractName] - if !rbsExists { - rbs = &contractBindings{readBindings: make(map[string]readBinding)} - b[contractName] = rbs + cb, cbExists := b[contractName] + if !cbExists { + cb = &contractBinding{readBindings: make(map[string]readBinding)} + b[contractName] = cb } - rbs.readBindings[readName] = rb + cb.readBindings[readName] = rb } func (b bindings) Bind(ctx context.Context, logPoller logpoller.LogPoller, boundContracts []commontypes.BoundContract) error { for _, bc := range boundContracts { - rbs, rbsExist := b[bc.Name] - if !rbsExist { + cb, cbExists := b[bc.Name] + if !cbExists { return fmt.Errorf("%w: no contract named %s", commontypes.ErrInvalidConfig, bc.Name) } - rbs.pollingFilter.Addresses = evmtypes.AddressArray{common.HexToAddress(bc.Address)} - rbs.pollingFilter.Name = logpoller.FilterName(bc.Name+"."+uuid.NewString(), bc.Address) + cb.pollingFilter.Addresses = evmtypes.AddressArray{common.HexToAddress(bc.Address)} + cb.pollingFilter.Name = logpoller.FilterName(bc.Name+"."+uuid.NewString(), bc.Address) // we are changing contract address reference, so we need to unregister old filters if they exist - if err := rbs.Unregister(ctx, logPoller); err != nil { + if err := cb.Unregister(ctx, logPoller); err != nil { return err } - for _, r := range rbs.readBindings { - r.Bind(bc) + for _, rb := range cb.readBindings { + rb.Bind(bc) } // if contract event filters aren't already registered then they will on startup // if they are already registered then we are overriding them because contract binding (address) has changed - if rbs.isRegistered { - if err := rbs.Register(ctx, logPoller); err != nil { + if cb.isRegistered { + if err := cb.Register(ctx, logPoller); err != nil { return err } } @@ -81,9 +81,9 @@ func (b bindings) Bind(ctx context.Context, logPoller logpoller.LogPoller, bound return nil } -func (b bindings) ForEach(ctx context.Context, fn func(context.Context, *contractBindings) error) error { - for _, rbs := range b { - if err := fn(ctx, rbs); err != nil { +func (b bindings) ForEach(ctx context.Context, fn func(context.Context, *contractBinding) error) error { + for _, cb := range b { + if err := fn(ctx, cb); err != nil { return err } } @@ -91,22 +91,22 @@ func (b bindings) ForEach(ctx context.Context, fn func(context.Context, *contrac } // Register registers polling filters. -func (rb *contractBindings) Register(ctx context.Context, logPoller logpoller.LogPoller) error { - rb.filterLock.Lock() - defer rb.filterLock.Unlock() +func (cb *contractBinding) Register(ctx context.Context, logPoller logpoller.LogPoller) error { + cb.filterLock.Lock() + defer cb.filterLock.Unlock() - rb.isRegistered = true + cb.isRegistered = true - if logPoller.HasFilter(rb.pollingFilter.Name) { + if logPoller.HasFilter(cb.pollingFilter.Name) { return nil } - if err := logPoller.RegisterFilter(ctx, rb.pollingFilter); err != nil { + if err := logPoller.RegisterFilter(ctx, cb.pollingFilter); err != nil { return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) } - for _, binding := range rb.readBindings { - if err := binding.Register(ctx); err != nil { + for _, rb := range cb.readBindings { + if err := rb.Register(ctx); err != nil { return err } } @@ -114,20 +114,20 @@ func (rb *contractBindings) Register(ctx context.Context, logPoller logpoller.Lo } // Unregister unregisters polling filters. -func (rb *contractBindings) Unregister(ctx context.Context, logPoller logpoller.LogPoller) error { - rb.filterLock.Lock() - defer rb.filterLock.Unlock() +func (cb *contractBinding) Unregister(ctx context.Context, logPoller logpoller.LogPoller) error { + cb.filterLock.Lock() + defer cb.filterLock.Unlock() - if !logPoller.HasFilter(rb.pollingFilter.Name) { + if !logPoller.HasFilter(cb.pollingFilter.Name) { return nil } - if err := logPoller.UnregisterFilter(ctx, rb.pollingFilter.Name); err != nil { + if err := logPoller.UnregisterFilter(ctx, cb.pollingFilter.Name); err != nil { return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) } - for _, binding := range rb.readBindings { - if err := binding.Unregister(ctx); err != nil { + for _, rb := range cb.readBindings { + if err := rb.Unregister(ctx); err != nil { return err } } diff --git a/core/services/relay/evm/chain_reader.go b/core/services/relay/evm/chain_reader.go index 1ae6286ad3f..c107d4d8443 100644 --- a/core/services/relay/evm/chain_reader.go +++ b/core/services/relay/evm/chain_reader.go @@ -64,7 +64,7 @@ func NewChainReaderService(ctx context.Context, lggr logger.Logger, lp logpoller return nil, err } - err = cr.contractBindings.ForEach(ctx, func(c context.Context, rbs *contractBindings) error { + err = cr.contractBindings.ForEach(ctx, func(c context.Context, rbs *contractBinding) error { for _, rb := range rbs.readBindings { rb.SetCodec(cr.codec) } @@ -149,7 +149,7 @@ func (cr *chainReader) init(chainContractReaders map[string]types.ChainContractR func (cr *chainReader) Start(ctx context.Context) error { return cr.StartOnce("ChainReader", func() error { - return cr.contractBindings.ForEach(ctx, func(c context.Context, rbs *contractBindings) error { + return cr.contractBindings.ForEach(ctx, func(c context.Context, rbs *contractBinding) error { return rbs.Register(ctx, cr.lp) }) }) @@ -159,7 +159,7 @@ func (cr *chainReader) Close() error { return cr.StopOnce("ChainReader", func() error { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() - return cr.contractBindings.ForEach(ctx, func(c context.Context, rbs *contractBindings) error { + return cr.contractBindings.ForEach(ctx, func(c context.Context, rbs *contractBinding) error { return rbs.Unregister(ctx, cr.lp) }) }) From 23b7a8d39377ec8cd52668fb059c19a39af151f7 Mon Sep 17 00:00:00 2001 From: ilija Date: Wed, 5 Jun 2024 01:50:52 +0200 Subject: [PATCH 09/21] Change write target test --- core/services/relay/evm/write_target_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/services/relay/evm/write_target_test.go b/core/services/relay/evm/write_target_test.go index 76060dce990..b39f0854a2d 100644 --- a/core/services/relay/evm/write_target_test.go +++ b/core/services/relay/evm/write_target_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/smartcontractkit/chainlink-common/pkg/values" + lpmocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller/mocks" "github.com/smartcontractkit/chainlink/v2/core/internal/cltest" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" @@ -35,6 +36,7 @@ var forwardABI = types.MustGetABI(forwarder.KeystoneForwarderMetaData.ABI) func TestEvmWrite(t *testing.T) { chain := evmmocks.NewChain(t) txManager := txmmocks.NewMockEvmTxManager(t) + logPoller := lpmocks.NewLogPoller(t) evmClient := evmclimocks.NewClient(t) // This probably isn't the best way to do this, but couldn't find a simpler way to mock the CallContract response @@ -46,7 +48,7 @@ func TestEvmWrite(t *testing.T) { chain.On("ID").Return(big.NewInt(11155111)) chain.On("TxManager").Return(txManager) - chain.On("LogPoller").Return(nil) + chain.On("LogPoller").Return(logPoller) chain.On("Client").Return(evmClient) cfg := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { @@ -63,6 +65,7 @@ func TestEvmWrite(t *testing.T) { evmCfg := evmtest.NewChainScopedConfig(t, cfg) chain.On("Config").Return(evmCfg) + logPoller.On("HasFilter", mock.Anything).Return(false) db := pgtest.NewSqlxDB(t) keyStore := cltest.NewKeyStore(t, db) From 09b7c37ec85e1f6b3716b5152bde02f8670847a8 Mon Sep 17 00:00:00 2001 From: ilija Date: Wed, 5 Jun 2024 15:56:09 +0200 Subject: [PATCH 10/21] Fix filter handling --- core/chains/evm/logpoller/orm.go | 1 + core/services/relay/evm/bindings.go | 16 ++++++++-------- core/services/relay/evm/chain_reader.go | 4 +++- core/services/relay/evm/chain_reader_test.go | 20 ++++++++++++++++---- core/services/relay/evm/event_binding.go | 3 ++- 5 files changed, 30 insertions(+), 14 deletions(-) diff --git a/core/chains/evm/logpoller/orm.go b/core/chains/evm/logpoller/orm.go index 8a4c46bd7a6..b9a1ca39f4a 100644 --- a/core/chains/evm/logpoller/orm.go +++ b/core/chains/evm/logpoller/orm.go @@ -971,6 +971,7 @@ func (o *DSORM) SelectIndexedLogsWithSigsExcluding(ctx context.Context, sigA, si return logs, nil } +// TODO flaky BCF-3258 func (o *DSORM) FilteredLogs(ctx context.Context, filter query.KeyFilter, limitAndSort query.LimitAndSort, _ string) ([]Log, error) { qs, args, err := (&pgDSLParser{}).buildQuery(o.chainID, filter.Expressions, limitAndSort) if err != nil { diff --git a/core/services/relay/evm/bindings.go b/core/services/relay/evm/bindings.go index bf20092e9c6..8b5bf44f694 100644 --- a/core/services/relay/evm/bindings.go +++ b/core/services/relay/evm/bindings.go @@ -27,6 +27,7 @@ type contractBinding struct { FilterRegisterer // key is read name method, event or event keys used for queryKey. readBindings map[string]readBinding + bound bool } func (b bindings) GetReadBinding(contractName, readName string) (readBinding, error) { @@ -60,6 +61,7 @@ func (b bindings) Bind(ctx context.Context, logPoller logpoller.LogPoller, bound cb.pollingFilter.Addresses = evmtypes.AddressArray{common.HexToAddress(bc.Address)} cb.pollingFilter.Name = logpoller.FilterName(bc.Name+"."+uuid.NewString(), bc.Address) + cb.bound = true // we are changing contract address reference, so we need to unregister old filters if they exist if err := cb.Unregister(ctx, logPoller); err != nil { @@ -91,19 +93,17 @@ func (b bindings) ForEach(ctx context.Context, fn func(context.Context, *contrac } // Register registers polling filters. -func (cb *contractBinding) Register(ctx context.Context, logPoller logpoller.LogPoller) error { +func (cb *contractBinding) Register(ctx context.Context, lp logpoller.LogPoller) error { cb.filterLock.Lock() defer cb.filterLock.Unlock() - cb.isRegistered = true - - if logPoller.HasFilter(cb.pollingFilter.Name) { - return nil + if cb.bound && len(cb.pollingFilter.EventSigs) > 0 && !lp.HasFilter(cb.pollingFilter.Name) { + if err := lp.RegisterFilter(ctx, cb.pollingFilter); err != nil { + return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) + } } - if err := logPoller.RegisterFilter(ctx, cb.pollingFilter); err != nil { - return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) - } + cb.isRegistered = true for _, rb := range cb.readBindings { if err := rb.Register(ctx); err != nil { diff --git a/core/services/relay/evm/chain_reader.go b/core/services/relay/evm/chain_reader.go index c107d4d8443..4302344c54f 100644 --- a/core/services/relay/evm/chain_reader.go +++ b/core/services/relay/evm/chain_reader.go @@ -107,6 +107,7 @@ func (cr *chainReader) init(chainContractReaders map[string]types.ChainContractR return err } + // TODO validate that contract polling filter can't be empty if there are events defined var eventSigsForContractFilter evmtypes.HashArray for typeName, chainReaderDefinition := range chainContractReader.Configs { switch chainReaderDefinition.ReadType { @@ -127,7 +128,8 @@ func (cr *chainReader) init(chainContractReaders map[string]types.ChainContractR commontypes.ErrInvalidConfig, contractName, typeName) } - if !eventOverridesContractFilter { + if !eventOverridesContractFilter && + !slices.Contains(eventSigsForContractFilter, contractAbi.Events[chainReaderDefinition.ChainSpecificName].ID) { eventSigsForContractFilter = append(eventSigsForContractFilter, contractAbi.Events[chainReaderDefinition.ChainSpecificName].ID) } diff --git a/core/services/relay/evm/chain_reader_test.go b/core/services/relay/evm/chain_reader_test.go index d4c304ad9b0..046b341cb64 100644 --- a/core/services/relay/evm/chain_reader_test.go +++ b/core/services/relay/evm/chain_reader_test.go @@ -47,6 +47,7 @@ const ( ) func TestChainReaderInterfaceTests(t *testing.T) { + // TODO flaky BCF-3258 t.Parallel() it := &chainReaderInterfaceTester{} RunChainReaderInterfaceTests(t, it) @@ -168,6 +169,9 @@ func (it *chainReaderInterfaceTester) Setup(t *testing.T) { Contracts: map[string]types.ChainContractReader{ AnyContractName: { ContractABI: chain_reader_tester.ChainReaderTesterMetaData.ABI, + ContractPollingFilter: types.ContractPollingFilter{ + GenericEventNames: []string{EventName, EventWithFilterName}, + }, Configs: map[string]*types.ChainReaderDefinition{ MethodTakingLatestParamsReturningTestStruct: { ChainSpecificName: "getElementAtIndex", @@ -201,16 +205,24 @@ func (it *chainReaderInterfaceTester) Setup(t *testing.T) { triggerWithDynamicTopic: { ChainSpecificName: triggerWithDynamicTopic, ReadType: types.Event, - EventDefinitions: &types.EventDefinitions{InputFields: []string{"fieldHash"}}, + EventDefinitions: &types.EventDefinitions{ + InputFields: []string{"fieldHash"}, + // no specific reason for filter being defined here insted on contract level, + // this is just for test case variety + PollingFilter: &types.PollingFilter{}, + }, InputModifications: codec.ModifiersConfig{ &codec.RenameModifierConfig{Fields: map[string]string{"FieldHash": "Field"}}, }, ConfidenceConfirmations: map[string]int{"0.0": 0, "1.0": -1}, }, triggerWithAllTopics: { - ChainSpecificName: triggerWithAllTopics, - ReadType: types.Event, - EventDefinitions: &types.EventDefinitions{InputFields: []string{"Field1", "Field2", "Field3"}}, + ChainSpecificName: triggerWithAllTopics, + ReadType: types.Event, + EventDefinitions: &types.EventDefinitions{ + InputFields: []string{"Field1", "Field2", "Field3"}, + PollingFilter: &types.PollingFilter{}, + }, ConfidenceConfirmations: map[string]int{"0.0": 0, "1.0": -1}, }, MethodReturningSeenStruct: { diff --git a/core/services/relay/evm/event_binding.go b/core/services/relay/evm/event_binding.go index a5294851460..62f104f34f0 100644 --- a/core/services/relay/evm/event_binding.go +++ b/core/services/relay/evm/event_binding.go @@ -63,7 +63,6 @@ func (e *eventBinding) Register(ctx context.Context) error { e.filterLock.Lock() defer e.filterLock.Unlock() - e.isRegistered = true if !e.bound || e.lp.HasFilter(e.pollingFilter.Name) { return nil } @@ -71,6 +70,8 @@ func (e *eventBinding) Register(ctx context.Context) error { if err := e.lp.RegisterFilter(ctx, e.pollingFilter); err != nil { return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) } + e.isRegistered = true + return nil } From 4e239148681de4e4d4fa8e302b238c222b80f9a8 Mon Sep 17 00:00:00 2001 From: ilija Date: Thu, 6 Jun 2024 13:24:18 +0200 Subject: [PATCH 11/21] Change contractBinding to not manage readBinding lp filters and lint --- core/services/relay/evm/binding.go | 2 +- core/services/relay/evm/bindings.go | 49 ++++++++++------------- core/services/relay/evm/chain_reader.go | 19 ++++++--- core/services/relay/evm/event_binding.go | 14 ++++++- core/services/relay/evm/method_binding.go | 3 +- 5 files changed, 51 insertions(+), 36 deletions(-) diff --git a/core/services/relay/evm/binding.go b/core/services/relay/evm/binding.go index 03ac7c56a8e..07c3e092340 100644 --- a/core/services/relay/evm/binding.go +++ b/core/services/relay/evm/binding.go @@ -10,7 +10,7 @@ import ( type readBinding interface { GetLatestValue(ctx context.Context, params, returnVal any) error QueryKey(ctx context.Context, filter query.KeyFilter, limitAndSort query.LimitAndSort, sequenceDataType any) ([]commontypes.Sequence, error) - Bind(binding commontypes.BoundContract) + Bind(ctx context.Context, binding commontypes.BoundContract) error Register(ctx context.Context) error Unregister(ctx context.Context) error SetCodec(codec commontypes.RemoteCodec) diff --git a/core/services/relay/evm/bindings.go b/core/services/relay/evm/bindings.go index 8b5bf44f694..f670a95603d 100644 --- a/core/services/relay/evm/bindings.go +++ b/core/services/relay/evm/bindings.go @@ -22,8 +22,10 @@ type FilterRegisterer struct { isRegistered bool } +// contractBinding stores read bindings and manages the common contract event filter. type contractBinding struct { - // FilterRegisterer is used to manage polling filter registration for the contact wide event filter. + // FilterRegisterer is used to manage polling filter registration for the common contract filter. + // The common contract filter should be used by events that share filtering args. FilterRegisterer // key is read name method, event or event keys used for queryKey. readBindings map[string]readBinding @@ -52,7 +54,8 @@ func (b bindings) AddReadBinding(contractName, readName string, rb readBinding) cb.readBindings[readName] = rb } -func (b bindings) Bind(ctx context.Context, logPoller logpoller.LogPoller, boundContracts []commontypes.BoundContract) error { +// Bind binds contract addresses and creates event binding filters and the common contract filter. +func (b bindings) Bind(ctx context.Context, lp logpoller.LogPoller, boundContracts []commontypes.BoundContract) error { for _, bc := range boundContracts { cb, cbExists := b[bc.Name] if !cbExists { @@ -63,19 +66,21 @@ func (b bindings) Bind(ctx context.Context, logPoller logpoller.LogPoller, bound cb.pollingFilter.Name = logpoller.FilterName(bc.Name+"."+uuid.NewString(), bc.Address) cb.bound = true - // we are changing contract address reference, so we need to unregister old filters if they exist - if err := cb.Unregister(ctx, logPoller); err != nil { + // we are changing contract address reference, so we need to unregister old filter it exists + if err := cb.Unregister(ctx, lp); err != nil { return err } - for _, rb := range cb.readBindings { - rb.Bind(bc) + // if contract event filter isn't already registered then it will be on startup + // if its already registered then we are overriding it because(address) has changed + if cb.isRegistered { + if err := cb.Register(ctx, lp); err != nil { + return err + } } - // if contract event filters aren't already registered then they will on startup - // if they are already registered then we are overriding them because contract binding (address) has changed - if cb.isRegistered { - if err := cb.Register(ctx, logPoller); err != nil { + for _, rb := range cb.readBindings { + if err := rb.Bind(ctx, bc); err != nil { return err } } @@ -92,7 +97,7 @@ func (b bindings) ForEach(ctx context.Context, fn func(context.Context, *contrac return nil } -// Register registers polling filters. +// Register registers the common contract filter. func (cb *contractBinding) Register(ctx context.Context, lp logpoller.LogPoller) error { cb.filterLock.Lock() defer cb.filterLock.Unlock() @@ -101,35 +106,25 @@ func (cb *contractBinding) Register(ctx context.Context, lp logpoller.LogPoller) if err := lp.RegisterFilter(ctx, cb.pollingFilter); err != nil { return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) } + cb.isRegistered = true } - cb.isRegistered = true - - for _, rb := range cb.readBindings { - if err := rb.Register(ctx); err != nil { - return err - } - } return nil } -// Unregister unregisters polling filters. -func (cb *contractBinding) Unregister(ctx context.Context, logPoller logpoller.LogPoller) error { +// Unregister unregisters the common contract filter. +func (cb *contractBinding) Unregister(ctx context.Context, lp logpoller.LogPoller) error { cb.filterLock.Lock() defer cb.filterLock.Unlock() - if !logPoller.HasFilter(cb.pollingFilter.Name) { + if !lp.HasFilter(cb.pollingFilter.Name) { return nil } - if err := logPoller.UnregisterFilter(ctx, cb.pollingFilter.Name); err != nil { + if err := lp.UnregisterFilter(ctx, cb.pollingFilter.Name); err != nil { return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) } + cb.isRegistered = false - for _, rb := range cb.readBindings { - if err := rb.Unregister(ctx); err != nil { - return err - } - } return nil } diff --git a/core/services/relay/evm/chain_reader.go b/core/services/relay/evm/chain_reader.go index 4302344c54f..6e5e6f137ba 100644 --- a/core/services/relay/evm/chain_reader.go +++ b/core/services/relay/evm/chain_reader.go @@ -107,7 +107,6 @@ func (cr *chainReader) init(chainContractReaders map[string]types.ChainContractR return err } - // TODO validate that contract polling filter can't be empty if there are events defined var eventSigsForContractFilter evmtypes.HashArray for typeName, chainReaderDefinition := range chainContractReader.Configs { switch chainReaderDefinition.ReadType { @@ -151,8 +150,13 @@ func (cr *chainReader) init(chainContractReaders map[string]types.ChainContractR func (cr *chainReader) Start(ctx context.Context) error { return cr.StartOnce("ChainReader", func() error { - return cr.contractBindings.ForEach(ctx, func(c context.Context, rbs *contractBinding) error { - return rbs.Register(ctx, cr.lp) + return cr.contractBindings.ForEach(ctx, func(c context.Context, cb *contractBinding) error { + for _, rb := range cb.readBindings { + if err := rb.Register(ctx); err != nil { + return err + } + } + return cb.Register(ctx, cr.lp) }) }) } @@ -161,8 +165,13 @@ func (cr *chainReader) Close() error { return cr.StopOnce("ChainReader", func() error { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() - return cr.contractBindings.ForEach(ctx, func(c context.Context, rbs *contractBinding) error { - return rbs.Unregister(ctx, cr.lp) + return cr.contractBindings.ForEach(ctx, func(c context.Context, cb *contractBinding) error { + for _, rb := range cb.readBindings { + if err := rb.Unregister(ctx); err != nil { + return err + } + } + return cb.Unregister(ctx, cr.lp) }) }) } diff --git a/core/services/relay/evm/event_binding.go b/core/services/relay/evm/event_binding.go index 62f104f34f0..50f00f8d089 100644 --- a/core/services/relay/evm/event_binding.go +++ b/core/services/relay/evm/event_binding.go @@ -90,6 +90,8 @@ func (e *eventBinding) Unregister(ctx context.Context) error { if err := e.lp.UnregisterFilter(ctx, e.pollingFilter.Name); err != nil { return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) } + e.isRegistered = false + return nil } @@ -141,17 +143,25 @@ func (e *eventBinding) QueryKey(ctx context.Context, filter query.KeyFilter, lim return e.decodeLogsIntoSequences(ctx, logs, sequenceDataType) } -func (e *eventBinding) Bind(binding commontypes.BoundContract) { +func (e *eventBinding) Bind(ctx context.Context, binding commontypes.BoundContract) error { + if err := e.Unregister(ctx); err != nil { + return err + } + e.address = common.HexToAddress(binding.Address) e.pending = binding.Pending + e.bound = true if e.FilterRegisterer != nil { id := fmt.Sprintf("%s,%s,%s", e.contractName, e.eventName, uuid.NewString()) e.pollingFilter.Name = logpoller.FilterName(id, e.address) e.pollingFilter.Addresses = evmtypes.AddressArray{e.address} + if e.isRegistered { + return e.Register(ctx) + } } - e.bound = true + return nil } func (e *eventBinding) getLatestValueWithoutFilters(ctx context.Context, confs evmtypes.Confirmations, into any) error { diff --git a/core/services/relay/evm/method_binding.go b/core/services/relay/evm/method_binding.go index ed6c204315a..7484d17c3ef 100644 --- a/core/services/relay/evm/method_binding.go +++ b/core/services/relay/evm/method_binding.go @@ -68,7 +68,8 @@ func (m *methodBinding) QueryKey(_ context.Context, _ query.KeyFilter, _ query.L return nil, nil } -func (m *methodBinding) Bind(binding commontypes.BoundContract) { +func (m *methodBinding) Bind(_ context.Context, binding commontypes.BoundContract) error { m.address = common.HexToAddress(binding.Address) m.bound = true + return nil } From affbb79d0ecf887451d172d2bfea869dcfd7b613 Mon Sep 17 00:00:00 2001 From: ilija Date: Thu, 6 Jun 2024 13:31:03 +0200 Subject: [PATCH 12/21] Move contractBinding to a separate file --- core/services/relay/evm/bindings.go | 54 ++----------------- core/services/relay/evm/chain_reader.go | 2 +- core/services/relay/evm/contract_binding.go | 58 +++++++++++++++++++++ core/services/relay/evm/event_binding.go | 12 ++--- 4 files changed, 68 insertions(+), 58 deletions(-) create mode 100644 core/services/relay/evm/contract_binding.go diff --git a/core/services/relay/evm/bindings.go b/core/services/relay/evm/bindings.go index f670a95603d..4fdd983d3ab 100644 --- a/core/services/relay/evm/bindings.go +++ b/core/services/relay/evm/bindings.go @@ -3,7 +3,6 @@ package evm import ( "context" "fmt" - "sync" "github.com/ethereum/go-ethereum/common" "github.com/google/uuid" @@ -13,25 +12,9 @@ import ( evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" ) -// key is contract name +// bindings manage all contract bindings, key is contract name. type bindings map[string]*contractBinding -type FilterRegisterer struct { - pollingFilter logpoller.Filter - filterLock sync.Mutex - isRegistered bool -} - -// contractBinding stores read bindings and manages the common contract event filter. -type contractBinding struct { - // FilterRegisterer is used to manage polling filter registration for the common contract filter. - // The common contract filter should be used by events that share filtering args. - FilterRegisterer - // key is read name method, event or event keys used for queryKey. - readBindings map[string]readBinding - bound bool -} - func (b bindings) GetReadBinding(contractName, readName string) (readBinding, error) { cb, cbExists := b[contractName] if !cbExists { @@ -54,7 +37,8 @@ func (b bindings) AddReadBinding(contractName, readName string, rb readBinding) cb.readBindings[readName] = rb } -// Bind binds contract addresses and creates event binding filters and the common contract filter. +// Bind binds contract addresses to contract binding and read bindings. +// Bind also registers the common contract polling filter and eventBindings polling filters. func (b bindings) Bind(ctx context.Context, lp logpoller.LogPoller, boundContracts []commontypes.BoundContract) error { for _, bc := range boundContracts { cb, cbExists := b[bc.Name] @@ -96,35 +80,3 @@ func (b bindings) ForEach(ctx context.Context, fn func(context.Context, *contrac } return nil } - -// Register registers the common contract filter. -func (cb *contractBinding) Register(ctx context.Context, lp logpoller.LogPoller) error { - cb.filterLock.Lock() - defer cb.filterLock.Unlock() - - if cb.bound && len(cb.pollingFilter.EventSigs) > 0 && !lp.HasFilter(cb.pollingFilter.Name) { - if err := lp.RegisterFilter(ctx, cb.pollingFilter); err != nil { - return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) - } - cb.isRegistered = true - } - - return nil -} - -// Unregister unregisters the common contract filter. -func (cb *contractBinding) Unregister(ctx context.Context, lp logpoller.LogPoller) error { - cb.filterLock.Lock() - defer cb.filterLock.Unlock() - - if !lp.HasFilter(cb.pollingFilter.Name) { - return nil - } - - if err := lp.UnregisterFilter(ctx, cb.pollingFilter.Name); err != nil { - return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) - } - cb.isRegistered = false - - return nil -} diff --git a/core/services/relay/evm/chain_reader.go b/core/services/relay/evm/chain_reader.go index 6e5e6f137ba..d58b570ef66 100644 --- a/core/services/relay/evm/chain_reader.go +++ b/core/services/relay/evm/chain_reader.go @@ -265,7 +265,7 @@ func (cr *chainReader) addEvent(contractName, eventName string, a abi.ABI, chain if eventDefinitions := chainReaderDefinition.EventDefinitions; eventDefinitions != nil { if eventDefinitions.PollingFilter != nil { - eb.FilterRegisterer = &FilterRegisterer{ + eb.filterRegisterer = &filterRegisterer{ pollingFilter: eventDefinitions.PollingFilter.ToLPFilter(evmtypes.HashArray{a.Events[event.Name].ID}), filterLock: sync.Mutex{}, } diff --git a/core/services/relay/evm/contract_binding.go b/core/services/relay/evm/contract_binding.go new file mode 100644 index 00000000000..ab64a927a85 --- /dev/null +++ b/core/services/relay/evm/contract_binding.go @@ -0,0 +1,58 @@ +package evm + +import ( + "context" + "fmt" + "sync" + + commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" +) + +type filterRegisterer struct { + pollingFilter logpoller.Filter + filterLock sync.Mutex + isRegistered bool +} + +// contractBinding stores read bindings and manages the common contract event filter. +type contractBinding struct { + // filterRegisterer is used to manage polling filter registration for the common contract filter. + // The common contract filter should be used by events that share filtering args. + filterRegisterer + // key is read name method, event or event keys used for queryKey. + readBindings map[string]readBinding + bound bool +} + +// Register registers the common contract filter. +func (cb *contractBinding) Register(ctx context.Context, lp logpoller.LogPoller) error { + cb.filterLock.Lock() + defer cb.filterLock.Unlock() + + if cb.bound && len(cb.pollingFilter.EventSigs) > 0 && !lp.HasFilter(cb.pollingFilter.Name) { + if err := lp.RegisterFilter(ctx, cb.pollingFilter); err != nil { + return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) + } + cb.isRegistered = true + } + + return nil +} + +// Unregister unregisters the common contract filter. +func (cb *contractBinding) Unregister(ctx context.Context, lp logpoller.LogPoller) error { + cb.filterLock.Lock() + defer cb.filterLock.Unlock() + + if !lp.HasFilter(cb.pollingFilter.Name) { + return nil + } + + if err := lp.UnregisterFilter(ctx, cb.pollingFilter.Name); err != nil { + return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) + } + cb.isRegistered = false + + return nil +} diff --git a/core/services/relay/evm/event_binding.go b/core/services/relay/evm/event_binding.go index 50f00f8d089..e02f6f7b4a3 100644 --- a/core/services/relay/evm/event_binding.go +++ b/core/services/relay/evm/event_binding.go @@ -25,9 +25,9 @@ type eventBinding struct { contractName string eventName string lp logpoller.LogPoller - // FilterRegisterer in eventBinding is to be used as an override for lp filter defined in the contract binding. - // If FilterRegisterer is nil, this event should be registered with the lp filter defined in the contract binding. - *FilterRegisterer + // filterRegisterer in eventBinding is to be used as an override for lp filter defined in the contract binding. + // If filterRegisterer is nil, this event should be registered with the lp filter defined in the contract binding. + *filterRegisterer hash common.Hash codec commontypes.RemoteCodec pending bool @@ -56,7 +56,7 @@ func (e *eventBinding) SetCodec(codec commontypes.RemoteCodec) { } func (e *eventBinding) Register(ctx context.Context) error { - if e.FilterRegisterer == nil { + if e.filterRegisterer == nil { return nil } @@ -76,7 +76,7 @@ func (e *eventBinding) Register(ctx context.Context) error { } func (e *eventBinding) Unregister(ctx context.Context) error { - if e.FilterRegisterer == nil { + if e.filterRegisterer == nil { return nil } @@ -152,7 +152,7 @@ func (e *eventBinding) Bind(ctx context.Context, binding commontypes.BoundContra e.pending = binding.Pending e.bound = true - if e.FilterRegisterer != nil { + if e.filterRegisterer != nil { id := fmt.Sprintf("%s,%s,%s", e.contractName, e.eventName, uuid.NewString()) e.pollingFilter.Name = logpoller.FilterName(id, e.address) e.pollingFilter.Addresses = evmtypes.AddressArray{e.address} From 883d971814dc616432d59540d9c33c8470048b92 Mon Sep 17 00:00:00 2001 From: ilija Date: Thu, 6 Jun 2024 18:07:56 +0200 Subject: [PATCH 13/21] Improve event and contract bind handling and lint --- core/services/relay/evm/binding.go | 6 +- core/services/relay/evm/bindings.go | 26 ++----- core/services/relay/evm/chain_reader.go | 6 +- core/services/relay/evm/chain_reader_test.go | 2 +- core/services/relay/evm/contract_binding.go | 45 ++++++++++-- core/services/relay/evm/event_binding.go | 75 ++++++++++++-------- core/services/relay/evm/method_binding.go | 14 ++-- core/services/relay/evm/write_target_test.go | 5 +- 8 files changed, 107 insertions(+), 72 deletions(-) diff --git a/core/services/relay/evm/binding.go b/core/services/relay/evm/binding.go index 07c3e092340..9c7fd186dec 100644 --- a/core/services/relay/evm/binding.go +++ b/core/services/relay/evm/binding.go @@ -8,10 +8,10 @@ import ( ) type readBinding interface { - GetLatestValue(ctx context.Context, params, returnVal any) error - QueryKey(ctx context.Context, filter query.KeyFilter, limitAndSort query.LimitAndSort, sequenceDataType any) ([]commontypes.Sequence, error) Bind(ctx context.Context, binding commontypes.BoundContract) error + SetCodec(codec commontypes.RemoteCodec) Register(ctx context.Context) error Unregister(ctx context.Context) error - SetCodec(codec commontypes.RemoteCodec) + GetLatestValue(ctx context.Context, params, returnVal any) error + QueryKey(ctx context.Context, filter query.KeyFilter, limitAndSort query.LimitAndSort, sequenceDataType any) ([]commontypes.Sequence, error) } diff --git a/core/services/relay/evm/bindings.go b/core/services/relay/evm/bindings.go index 4fdd983d3ab..733752d5ced 100644 --- a/core/services/relay/evm/bindings.go +++ b/core/services/relay/evm/bindings.go @@ -4,12 +4,8 @@ import ( "context" "fmt" - "github.com/ethereum/go-ethereum/common" - "github.com/google/uuid" - commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" - evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" ) // bindings manage all contract bindings, key is contract name. @@ -31,13 +27,16 @@ func (b bindings) GetReadBinding(contractName, readName string) (readBinding, er func (b bindings) AddReadBinding(contractName, readName string, rb readBinding) { cb, cbExists := b[contractName] if !cbExists { - cb = &contractBinding{readBindings: make(map[string]readBinding)} + cb = &contractBinding{ + name: contractName, + readBindings: make(map[string]readBinding), + } b[contractName] = cb } cb.readBindings[readName] = rb } -// Bind binds contract addresses to contract binding and read bindings. +// Bind binds contract addresses to contract bindings and read bindings. // Bind also registers the common contract polling filter and eventBindings polling filters. func (b bindings) Bind(ctx context.Context, lp logpoller.LogPoller, boundContracts []commontypes.BoundContract) error { for _, bc := range boundContracts { @@ -46,23 +45,10 @@ func (b bindings) Bind(ctx context.Context, lp logpoller.LogPoller, boundContrac return fmt.Errorf("%w: no contract named %s", commontypes.ErrInvalidConfig, bc.Name) } - cb.pollingFilter.Addresses = evmtypes.AddressArray{common.HexToAddress(bc.Address)} - cb.pollingFilter.Name = logpoller.FilterName(bc.Name+"."+uuid.NewString(), bc.Address) - cb.bound = true - - // we are changing contract address reference, so we need to unregister old filter it exists - if err := cb.Unregister(ctx, lp); err != nil { + if err := cb.Bind(ctx, lp, bc); err != nil { return err } - // if contract event filter isn't already registered then it will be on startup - // if its already registered then we are overriding it because(address) has changed - if cb.isRegistered { - if err := cb.Register(ctx, lp); err != nil { - return err - } - } - for _, rb := range cb.readBindings { if err := rb.Bind(ctx, bc); err != nil { return err diff --git a/core/services/relay/evm/chain_reader.go b/core/services/relay/evm/chain_reader.go index d58b570ef66..d5300d8eeb0 100644 --- a/core/services/relay/evm/chain_reader.go +++ b/core/services/relay/evm/chain_reader.go @@ -64,8 +64,8 @@ func NewChainReaderService(ctx context.Context, lggr logger.Logger, lp logpoller return nil, err } - err = cr.contractBindings.ForEach(ctx, func(c context.Context, rbs *contractBinding) error { - for _, rb := range rbs.readBindings { + err = cr.contractBindings.ForEach(ctx, func(c context.Context, cb *contractBinding) error { + for _, rb := range cb.readBindings { rb.SetCodec(cr.codec) } return nil @@ -148,6 +148,7 @@ func (cr *chainReader) init(chainContractReaders map[string]types.ChainContractR return nil } +// Start registers polling filters if contracts are already bound. func (cr *chainReader) Start(ctx context.Context) error { return cr.StartOnce("ChainReader", func() error { return cr.contractBindings.ForEach(ctx, func(c context.Context, cb *contractBinding) error { @@ -161,6 +162,7 @@ func (cr *chainReader) Start(ctx context.Context) error { }) } +// Close unregisters polling filters for bound contracts. func (cr *chainReader) Close() error { return cr.StopOnce("ChainReader", func() error { ctx, cancel := context.WithTimeout(context.Background(), time.Second) diff --git a/core/services/relay/evm/chain_reader_test.go b/core/services/relay/evm/chain_reader_test.go index 046b341cb64..7b4001683cd 100644 --- a/core/services/relay/evm/chain_reader_test.go +++ b/core/services/relay/evm/chain_reader_test.go @@ -47,7 +47,7 @@ const ( ) func TestChainReaderInterfaceTests(t *testing.T) { - // TODO flaky BCF-3258 + // TODO QueryKey test is flaky BCF-3258 t.Parallel() it := &chainReaderInterfaceTester{} RunChainReaderInterfaceTests(t, it) diff --git a/core/services/relay/evm/contract_binding.go b/core/services/relay/evm/contract_binding.go index ab64a927a85..d5f68894da8 100644 --- a/core/services/relay/evm/contract_binding.go +++ b/core/services/relay/evm/contract_binding.go @@ -5,36 +5,68 @@ import ( "fmt" "sync" + "github.com/ethereum/go-ethereum/common" + "github.com/google/uuid" + commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" ) type filterRegisterer struct { pollingFilter logpoller.Filter filterLock sync.Mutex - isRegistered bool + // registerCalled is used to determine if Register was called during Chain Reader service Start. + // This is done to avoid calling Register while the service is not running because log poller is most likely also not running. + registerCalled bool } // contractBinding stores read bindings and manages the common contract event filter. type contractBinding struct { + name string // filterRegisterer is used to manage polling filter registration for the common contract filter. // The common contract filter should be used by events that share filtering args. filterRegisterer // key is read name method, event or event keys used for queryKey. readBindings map[string]readBinding - bound bool + // bound determines if address is set to the contract binding. + bound bool +} + +// Bind binds contract addresses to contract binding and registers the common contract polling filter. +func (cb *contractBinding) Bind(ctx context.Context, lp logpoller.LogPoller, boundContract commontypes.BoundContract) error { + if cb.bound { + // we are changing contract address reference, so we need to unregister old filter it exists + if err := cb.Unregister(ctx, lp); err != nil { + return err + } + } + + cb.pollingFilter.Addresses = evmtypes.AddressArray{common.HexToAddress(boundContract.Address)} + cb.pollingFilter.Name = logpoller.FilterName(boundContract.Name+"."+uuid.NewString(), boundContract.Address) + cb.bound = true + + if cb.registerCalled { + return cb.Register(ctx, lp) + } + + return nil } // Register registers the common contract filter. func (cb *contractBinding) Register(ctx context.Context, lp logpoller.LogPoller) error { + cb.registerCalled = true + if !cb.bound { + return nil + } + cb.filterLock.Lock() defer cb.filterLock.Unlock() - if cb.bound && len(cb.pollingFilter.EventSigs) > 0 && !lp.HasFilter(cb.pollingFilter.Name) { + if len(cb.pollingFilter.EventSigs) > 0 && !lp.HasFilter(cb.pollingFilter.Name) { if err := lp.RegisterFilter(ctx, cb.pollingFilter); err != nil { return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) } - cb.isRegistered = true } return nil @@ -42,6 +74,10 @@ func (cb *contractBinding) Register(ctx context.Context, lp logpoller.LogPoller) // Unregister unregisters the common contract filter. func (cb *contractBinding) Unregister(ctx context.Context, lp logpoller.LogPoller) error { + if !cb.bound { + return nil + } + cb.filterLock.Lock() defer cb.filterLock.Unlock() @@ -52,7 +88,6 @@ func (cb *contractBinding) Unregister(ctx context.Context, lp logpoller.LogPolle if err := lp.UnregisterFilter(ctx, cb.pollingFilter.Name); err != nil { return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) } - cb.isRegistered = false return nil } diff --git a/core/services/relay/evm/event_binding.go b/core/services/relay/evm/event_binding.go index e02f6f7b4a3..bd3a1613fe3 100644 --- a/core/services/relay/evm/event_binding.go +++ b/core/services/relay/evm/event_binding.go @@ -28,9 +28,10 @@ type eventBinding struct { // filterRegisterer in eventBinding is to be used as an override for lp filter defined in the contract binding. // If filterRegisterer is nil, this event should be registered with the lp filter defined in the contract binding. *filterRegisterer - hash common.Hash - codec commontypes.RemoteCodec - pending bool + hash common.Hash + codec commontypes.RemoteCodec + pending bool + // bound determines if address is set to the contract binding. bound bool inputInfo types.CodecEntry inputModifier codec.Modifier @@ -55,28 +56,56 @@ func (e *eventBinding) SetCodec(codec commontypes.RemoteCodec) { e.codec = codec } +func (e *eventBinding) Bind(ctx context.Context, binding commontypes.BoundContract) error { + if e.bound { + // we are changing contract address reference, so we need to unregister old filter it exists + if err := e.Unregister(ctx); err != nil { + return err + } + } + + e.address = common.HexToAddress(binding.Address) + e.pending = binding.Pending + e.bound = true + + // filterRegisterer isn't required here because the event can also be polled for by the contractBinding common filter. + if e.filterRegisterer != nil { + id := fmt.Sprintf("%s.%s.%s", e.contractName, e.eventName, uuid.NewString()) + e.pollingFilter.Name = logpoller.FilterName(id, e.address) + e.pollingFilter.Addresses = evmtypes.AddressArray{e.address} + if e.registerCalled { + return e.Register(ctx) + } + } + return nil +} + func (e *eventBinding) Register(ctx context.Context) error { if e.filterRegisterer == nil { return nil } + e.registerCalled = true + if !e.bound { + return nil + } + e.filterLock.Lock() defer e.filterLock.Unlock() - if !e.bound || e.lp.HasFilter(e.pollingFilter.Name) { + if e.lp.HasFilter(e.pollingFilter.Name) { return nil } if err := e.lp.RegisterFilter(ctx, e.pollingFilter); err != nil { return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) } - e.isRegistered = true return nil } func (e *eventBinding) Unregister(ctx context.Context) error { - if e.filterRegisterer == nil { + if !e.bound || e.filterRegisterer == nil { return nil } @@ -90,14 +119,13 @@ func (e *eventBinding) Unregister(ctx context.Context) error { if err := e.lp.UnregisterFilter(ctx, e.pollingFilter.Name); err != nil { return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) } - e.isRegistered = false return nil } func (e *eventBinding) GetLatestValue(ctx context.Context, params, into any) error { - if !e.bound { - return fmt.Errorf("%w: event not bound", commontypes.ErrInvalidType) + if err := e.validateBound(); err != nil { + return err } // TODO BCF-3247 change GetLatestValue to use chain agnostic confidence levels @@ -114,8 +142,8 @@ func (e *eventBinding) GetLatestValue(ctx context.Context, params, into any) err } func (e *eventBinding) QueryKey(ctx context.Context, filter query.KeyFilter, limitAndSort query.LimitAndSort, sequenceDataType any) ([]commontypes.Sequence, error) { - if !e.bound { - return nil, fmt.Errorf("%w: event not bound", commontypes.ErrInvalidType) + if err := e.validateBound(); err != nil { + return nil, err } remapped, err := e.remap(filter) @@ -143,24 +171,15 @@ func (e *eventBinding) QueryKey(ctx context.Context, filter query.KeyFilter, lim return e.decodeLogsIntoSequences(ctx, logs, sequenceDataType) } -func (e *eventBinding) Bind(ctx context.Context, binding commontypes.BoundContract) error { - if err := e.Unregister(ctx); err != nil { - return err - } - - e.address = common.HexToAddress(binding.Address) - e.pending = binding.Pending - e.bound = true - - if e.filterRegisterer != nil { - id := fmt.Sprintf("%s,%s,%s", e.contractName, e.eventName, uuid.NewString()) - e.pollingFilter.Name = logpoller.FilterName(id, e.address) - e.pollingFilter.Addresses = evmtypes.AddressArray{e.address} - if e.isRegistered { - return e.Register(ctx) - } +func (e *eventBinding) validateBound() error { + if !e.bound { + return fmt.Errorf( + "%w: event %s that belongs to contract: %s, not bound", + commontypes.ErrInvalidType, + e.eventName, + e.contractName, + ) } - return nil } diff --git a/core/services/relay/evm/method_binding.go b/core/services/relay/evm/method_binding.go index 7484d17c3ef..3a212bfea4b 100644 --- a/core/services/relay/evm/method_binding.go +++ b/core/services/relay/evm/method_binding.go @@ -28,15 +28,17 @@ func (m *methodBinding) SetCodec(codec commontypes.RemoteCodec) { m.codec = codec } -func (m *methodBinding) Register(_ context.Context) error { +func (m *methodBinding) Bind(_ context.Context, binding commontypes.BoundContract) error { + m.address = common.HexToAddress(binding.Address) + m.bound = true return nil } -func (m *methodBinding) Unregister(_ context.Context) error { +func (m *methodBinding) Register(_ context.Context) error { return nil } -func (m *methodBinding) UnregisterAll(_ context.Context) error { +func (m *methodBinding) Unregister(_ context.Context) error { return nil } @@ -67,9 +69,3 @@ func (m *methodBinding) GetLatestValue(ctx context.Context, params, returnValue func (m *methodBinding) QueryKey(_ context.Context, _ query.KeyFilter, _ query.LimitAndSort, _ any) ([]commontypes.Sequence, error) { return nil, nil } - -func (m *methodBinding) Bind(_ context.Context, binding commontypes.BoundContract) error { - m.address = common.HexToAddress(binding.Address) - m.bound = true - return nil -} diff --git a/core/services/relay/evm/write_target_test.go b/core/services/relay/evm/write_target_test.go index b39f0854a2d..76060dce990 100644 --- a/core/services/relay/evm/write_target_test.go +++ b/core/services/relay/evm/write_target_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/smartcontractkit/chainlink-common/pkg/values" - lpmocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller/mocks" "github.com/smartcontractkit/chainlink/v2/core/internal/cltest" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" @@ -36,7 +35,6 @@ var forwardABI = types.MustGetABI(forwarder.KeystoneForwarderMetaData.ABI) func TestEvmWrite(t *testing.T) { chain := evmmocks.NewChain(t) txManager := txmmocks.NewMockEvmTxManager(t) - logPoller := lpmocks.NewLogPoller(t) evmClient := evmclimocks.NewClient(t) // This probably isn't the best way to do this, but couldn't find a simpler way to mock the CallContract response @@ -48,7 +46,7 @@ func TestEvmWrite(t *testing.T) { chain.On("ID").Return(big.NewInt(11155111)) chain.On("TxManager").Return(txManager) - chain.On("LogPoller").Return(logPoller) + chain.On("LogPoller").Return(nil) chain.On("Client").Return(evmClient) cfg := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { @@ -65,7 +63,6 @@ func TestEvmWrite(t *testing.T) { evmCfg := evmtest.NewChainScopedConfig(t, cfg) chain.On("Config").Return(evmCfg) - logPoller.On("HasFilter", mock.Anything).Return(false) db := pgtest.NewSqlxDB(t) keyStore := cltest.NewKeyStore(t, db) From 4cf0ec7e8c5c02a00f0d44e1088f9a3e5863b924 Mon Sep 17 00:00:00 2001 From: ilija Date: Fri, 7 Jun 2024 00:59:22 +0200 Subject: [PATCH 14/21] Handle concurrency edge cases in reader bindings --- core/services/relay/evm/chain_reader.go | 62 ++++++++++----------- core/services/relay/evm/contract_binding.go | 21 ++++--- core/services/relay/evm/event_binding.go | 23 ++++++-- 3 files changed, 63 insertions(+), 43 deletions(-) diff --git a/core/services/relay/evm/chain_reader.go b/core/services/relay/evm/chain_reader.go index d5300d8eeb0..3a4a5dbfeca 100644 --- a/core/services/relay/evm/chain_reader.go +++ b/core/services/relay/evm/chain_reader.go @@ -74,32 +74,6 @@ func NewChainReaderService(ctx context.Context, lggr logger.Logger, lp logpoller return cr, err } -func (cr *chainReader) Name() string { return cr.lggr.Name() } - -var _ commontypes.ContractTypeProvider = &chainReader{} - -func (cr *chainReader) GetLatestValue(ctx context.Context, contractName, method string, params any, returnVal any) error { - b, err := cr.contractBindings.GetReadBinding(contractName, method) - if err != nil { - return err - } - - return b.GetLatestValue(ctx, params, returnVal) -} - -func (cr *chainReader) Bind(ctx context.Context, bindings []commontypes.BoundContract) error { - return cr.contractBindings.Bind(ctx, cr.lp, bindings) -} - -func (cr *chainReader) QueryKey(ctx context.Context, contractName string, filter query.KeyFilter, limitAndSort query.LimitAndSort, sequenceDataType any) ([]commontypes.Sequence, error) { - b, err := cr.contractBindings.GetReadBinding(contractName, filter.Key) - if err != nil { - return nil, err - } - - return b.QueryKey(ctx, filter, limitAndSort, sequenceDataType) -} - func (cr *chainReader) init(chainContractReaders map[string]types.ChainContractReader) error { for contractName, chainContractReader := range chainContractReaders { contractAbi, err := abi.JSON(strings.NewReader(chainContractReader.ContractABI)) @@ -113,15 +87,15 @@ func (cr *chainReader) init(chainContractReaders map[string]types.ChainContractR case types.Method: err = cr.addMethod(contractName, typeName, contractAbi, *chainReaderDefinition) case types.Event: - partOfContractFilter := slices.Contains(chainContractReader.GenericEventNames, typeName) - if !partOfContractFilter && !chainReaderDefinition.HasPollingFilter() { + partOfContractCommonFilter := slices.Contains(chainContractReader.GenericEventNames, typeName) + if !partOfContractCommonFilter && !chainReaderDefinition.HasPollingFilter() { return fmt.Errorf( "%w: chain reader has no polling filter defined for contract: %s, event: %s", commontypes.ErrInvalidConfig, contractName, typeName) } eventOverridesContractFilter := chainReaderDefinition.HasPollingFilter() - if eventOverridesContractFilter && partOfContractFilter { + if eventOverridesContractFilter && partOfContractCommonFilter { return fmt.Errorf( "%w: conflicting chain reader polling filter definitions for contract: %s event: %s, can't have polling filter defined both on contract and event level", commontypes.ErrInvalidConfig, contractName, typeName) @@ -148,6 +122,32 @@ func (cr *chainReader) init(chainContractReaders map[string]types.ChainContractR return nil } +func (cr *chainReader) Name() string { return cr.lggr.Name() } + +var _ commontypes.ContractTypeProvider = &chainReader{} + +func (cr *chainReader) GetLatestValue(ctx context.Context, contractName, method string, params any, returnVal any) error { + b, err := cr.contractBindings.GetReadBinding(contractName, method) + if err != nil { + return err + } + + return b.GetLatestValue(ctx, params, returnVal) +} + +func (cr *chainReader) Bind(ctx context.Context, bindings []commontypes.BoundContract) error { + return cr.contractBindings.Bind(ctx, cr.lp, bindings) +} + +func (cr *chainReader) QueryKey(ctx context.Context, contractName string, filter query.KeyFilter, limitAndSort query.LimitAndSort, sequenceDataType any) ([]commontypes.Sequence, error) { + b, err := cr.contractBindings.GetReadBinding(contractName, filter.Key) + if err != nil { + return nil, err + } + + return b.QueryKey(ctx, filter, limitAndSort, sequenceDataType) +} + // Start registers polling filters if contracts are already bound. func (cr *chainReader) Start(ctx context.Context) error { return cr.StartOnce("ChainReader", func() error { @@ -287,7 +287,7 @@ func (cr *chainReader) addEvent(contractName, eventName string, a abi.ABI, chain // addQueryingReadBindings reuses the eventBinding and maps it to topic and dataWord keys used for QueryKey. func (cr *chainReader) addQueryingReadBindings(contractName string, genericTopicNames map[string]string, eventInputs abi.Arguments, eb *eventBinding) { - // add topic read readBindings for QueryKey + // add topic readBindings for QueryKey for topicIndex, topic := range eventInputs { genericTopicName, ok := genericTopicNames[topic.Name] if ok { @@ -299,7 +299,7 @@ func (cr *chainReader) addQueryingReadBindings(contractName string, genericTopic cr.contractBindings.AddReadBinding(contractName, genericTopicName, eb) } - // add data word read readBindings for QueryKey + // add data word readBindings for QueryKey for genericDataWordName := range eb.eventDataWords { cr.contractBindings.AddReadBinding(contractName, genericDataWordName, eb) } diff --git a/core/services/relay/evm/contract_binding.go b/core/services/relay/evm/contract_binding.go index d5f68894da8..da2d7ed9bd1 100644 --- a/core/services/relay/evm/contract_binding.go +++ b/core/services/relay/evm/contract_binding.go @@ -30,11 +30,17 @@ type contractBinding struct { // key is read name method, event or event keys used for queryKey. readBindings map[string]readBinding // bound determines if address is set to the contract binding. - bound bool + bound bool + bindLock sync.Mutex } // Bind binds contract addresses to contract binding and registers the common contract polling filter. func (cb *contractBinding) Bind(ctx context.Context, lp logpoller.LogPoller, boundContract commontypes.BoundContract) error { + // it's enough to just lock bound here since Register/Unregister are only called from here and from Start/Close + // even if they somehow happen at the same time it will be fine because of filter lock and hasFilter check + cb.bindLock.Lock() + defer cb.bindLock.Unlock() + if cb.bound { // we are changing contract address reference, so we need to unregister old filter it exists if err := cb.Unregister(ctx, lp); err != nil { @@ -55,14 +61,15 @@ func (cb *contractBinding) Bind(ctx context.Context, lp logpoller.LogPoller, bou // Register registers the common contract filter. func (cb *contractBinding) Register(ctx context.Context, lp logpoller.LogPoller) error { + cb.filterLock.Lock() + defer cb.filterLock.Unlock() + cb.registerCalled = true + // can't be true before filters params are set so there is no race with a bad filter outcome if !cb.bound { return nil } - cb.filterLock.Lock() - defer cb.filterLock.Unlock() - if len(cb.pollingFilter.EventSigs) > 0 && !lp.HasFilter(cb.pollingFilter.Name) { if err := lp.RegisterFilter(ctx, cb.pollingFilter); err != nil { return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) @@ -74,13 +81,13 @@ func (cb *contractBinding) Register(ctx context.Context, lp logpoller.LogPoller) // Unregister unregisters the common contract filter. func (cb *contractBinding) Unregister(ctx context.Context, lp logpoller.LogPoller) error { + cb.filterLock.Lock() + defer cb.filterLock.Unlock() + if !cb.bound { return nil } - cb.filterLock.Lock() - defer cb.filterLock.Unlock() - if !lp.HasFilter(cb.pollingFilter.Name) { return nil } diff --git a/core/services/relay/evm/event_binding.go b/core/services/relay/evm/event_binding.go index bd3a1613fe3..a3f1fa9d6a2 100644 --- a/core/services/relay/evm/event_binding.go +++ b/core/services/relay/evm/event_binding.go @@ -5,6 +5,7 @@ import ( "fmt" "reflect" "strings" + "sync" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" @@ -33,6 +34,7 @@ type eventBinding struct { pending bool // bound determines if address is set to the contract binding. bound bool + bindLock sync.Mutex inputInfo types.CodecEntry inputModifier codec.Modifier codecTopicInfo types.CodecEntry @@ -57,6 +59,11 @@ func (e *eventBinding) SetCodec(codec commontypes.RemoteCodec) { } func (e *eventBinding) Bind(ctx context.Context, binding commontypes.BoundContract) error { + // it's enough to just lock bound here since Register/Unregister are only called from here and from Start/Close + // even if they somehow happen at the same time it will be fine because of filter lock and hasFilter check + e.bindLock.Lock() + defer e.bindLock.Unlock() + if e.bound { // we are changing contract address reference, so we need to unregister old filter it exists if err := e.Unregister(ctx); err != nil { @@ -66,17 +73,18 @@ func (e *eventBinding) Bind(ctx context.Context, binding commontypes.BoundContra e.address = common.HexToAddress(binding.Address) e.pending = binding.Pending - e.bound = true // filterRegisterer isn't required here because the event can also be polled for by the contractBinding common filter. if e.filterRegisterer != nil { id := fmt.Sprintf("%s.%s.%s", e.contractName, e.eventName, uuid.NewString()) e.pollingFilter.Name = logpoller.FilterName(id, e.address) e.pollingFilter.Addresses = evmtypes.AddressArray{e.address} + e.bound = true if e.registerCalled { return e.Register(ctx) } } + e.bound = true return nil } @@ -85,14 +93,15 @@ func (e *eventBinding) Register(ctx context.Context) error { return nil } + e.filterLock.Lock() + defer e.filterLock.Unlock() + e.registerCalled = true + // can't be true before filters params are set so there is no race with a bad filter outcome if !e.bound { return nil } - e.filterLock.Lock() - defer e.filterLock.Unlock() - if e.lp.HasFilter(e.pollingFilter.Name) { return nil } @@ -105,13 +114,17 @@ func (e *eventBinding) Register(ctx context.Context) error { } func (e *eventBinding) Unregister(ctx context.Context) error { - if !e.bound || e.filterRegisterer == nil { + if e.filterRegisterer == nil { return nil } e.filterLock.Lock() defer e.filterLock.Unlock() + if !e.bound { + return nil + } + if !e.lp.HasFilter(e.pollingFilter.Name) { return nil } From c8c873946a9ab74a4c9bc43363104c075f991ee4 Mon Sep 17 00:00:00 2001 From: ilija Date: Fri, 7 Jun 2024 01:20:03 +0200 Subject: [PATCH 15/21] Minor lint --- core/services/relay/evm/chain_reader.go | 48 ++++++++++++------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/core/services/relay/evm/chain_reader.go b/core/services/relay/evm/chain_reader.go index 3a4a5dbfeca..643db0d4496 100644 --- a/core/services/relay/evm/chain_reader.go +++ b/core/services/relay/evm/chain_reader.go @@ -43,6 +43,7 @@ type chainReader struct { } var _ ChainReaderService = (*chainReader)(nil) +var _ commontypes.ContractTypeProvider = &chainReader{} // NewChainReaderService is a constructor for ChainReader, returns nil if there is any error // Note that the ChainReaderService returned does not support anonymous events. @@ -124,30 +125,6 @@ func (cr *chainReader) init(chainContractReaders map[string]types.ChainContractR func (cr *chainReader) Name() string { return cr.lggr.Name() } -var _ commontypes.ContractTypeProvider = &chainReader{} - -func (cr *chainReader) GetLatestValue(ctx context.Context, contractName, method string, params any, returnVal any) error { - b, err := cr.contractBindings.GetReadBinding(contractName, method) - if err != nil { - return err - } - - return b.GetLatestValue(ctx, params, returnVal) -} - -func (cr *chainReader) Bind(ctx context.Context, bindings []commontypes.BoundContract) error { - return cr.contractBindings.Bind(ctx, cr.lp, bindings) -} - -func (cr *chainReader) QueryKey(ctx context.Context, contractName string, filter query.KeyFilter, limitAndSort query.LimitAndSort, sequenceDataType any) ([]commontypes.Sequence, error) { - b, err := cr.contractBindings.GetReadBinding(contractName, filter.Key) - if err != nil { - return nil, err - } - - return b.QueryKey(ctx, filter, limitAndSort, sequenceDataType) -} - // Start registers polling filters if contracts are already bound. func (cr *chainReader) Start(ctx context.Context) error { return cr.StartOnce("ChainReader", func() error { @@ -179,10 +156,33 @@ func (cr *chainReader) Close() error { } func (cr *chainReader) Ready() error { return nil } + func (cr *chainReader) HealthReport() map[string]error { return map[string]error{cr.Name(): nil} } +func (cr *chainReader) GetLatestValue(ctx context.Context, contractName, method string, params any, returnVal any) error { + b, err := cr.contractBindings.GetReadBinding(contractName, method) + if err != nil { + return err + } + + return b.GetLatestValue(ctx, params, returnVal) +} + +func (cr *chainReader) Bind(ctx context.Context, bindings []commontypes.BoundContract) error { + return cr.contractBindings.Bind(ctx, cr.lp, bindings) +} + +func (cr *chainReader) QueryKey(ctx context.Context, contractName string, filter query.KeyFilter, limitAndSort query.LimitAndSort, sequenceDataType any) ([]commontypes.Sequence, error) { + b, err := cr.contractBindings.GetReadBinding(contractName, filter.Key) + if err != nil { + return nil, err + } + + return b.QueryKey(ctx, filter, limitAndSort, sequenceDataType) +} + func (cr *chainReader) CreateContractType(contractName, itemType string, forEncoding bool) (any, error) { return cr.codec.CreateType(wrapItemType(contractName, itemType, forEncoding), forEncoding) } From a9aa2f01c72797d2cb99a80722c15f3118daf5b6 Mon Sep 17 00:00:00 2001 From: ilija Date: Mon, 10 Jun 2024 13:36:07 +0200 Subject: [PATCH 16/21] Add event validation test and some comments to chain reader --- core/services/relay/evm/bindings.go | 2 + core/services/relay/evm/chain_reader.go | 2 +- core/services/relay/evm/chain_reader_test.go | 106 +++++++++++++++++++ core/services/relay/evm/types/types.go | 2 +- 4 files changed, 110 insertions(+), 2 deletions(-) diff --git a/core/services/relay/evm/bindings.go b/core/services/relay/evm/bindings.go index 733752d5ced..42cb933fda4 100644 --- a/core/services/relay/evm/bindings.go +++ b/core/services/relay/evm/bindings.go @@ -12,6 +12,7 @@ import ( type bindings map[string]*contractBinding func (b bindings) GetReadBinding(contractName, readName string) (readBinding, error) { + // GetReadBindings should only be called after Chain Reader init. cb, cbExists := b[contractName] if !cbExists { return nil, fmt.Errorf("%w: no contract named %s", commontypes.ErrInvalidType, contractName) @@ -25,6 +26,7 @@ func (b bindings) GetReadBinding(contractName, readName string) (readBinding, er } func (b bindings) AddReadBinding(contractName, readName string, rb readBinding) { + // Adding read bindings outside of Chain Reader init is not thread safe. cb, cbExists := b[contractName] if !cbExists { cb = &contractBinding{ diff --git a/core/services/relay/evm/chain_reader.go b/core/services/relay/evm/chain_reader.go index 643db0d4496..9fef7260f1d 100644 --- a/core/services/relay/evm/chain_reader.go +++ b/core/services/relay/evm/chain_reader.go @@ -79,7 +79,7 @@ func (cr *chainReader) init(chainContractReaders map[string]types.ChainContractR for contractName, chainContractReader := range chainContractReaders { contractAbi, err := abi.JSON(strings.NewReader(chainContractReader.ContractABI)) if err != nil { - return err + return fmt.Errorf("failed to parse abi for contract: %s, err: %w", contractName, err) } var eventSigsForContractFilter evmtypes.HashArray diff --git a/core/services/relay/evm/chain_reader_test.go b/core/services/relay/evm/chain_reader_test.go index 7b4001683cd..7600272da5d 100644 --- a/core/services/relay/evm/chain_reader_test.go +++ b/core/services/relay/evm/chain_reader_test.go @@ -46,6 +46,112 @@ const ( triggerWithAllTopics = "TriggeredWithFourTopics" ) +func TestChainReaderEventsInitValidation(t *testing.T) { + tests := []struct { + name string + chainContractReaders map[string]types.ChainContractReader + expectedError error + }{ + { + name: "Invalid ABI", + chainContractReaders: map[string]types.ChainContractReader{ + "InvalidContract": { + ContractABI: "{invalid json}", + Configs: map[string]*types.ChainReaderDefinition{}, + }, + }, + expectedError: fmt.Errorf("failed to parse abi"), + }, + { + name: "Conflicting polling filter definitions", + chainContractReaders: map[string]types.ChainContractReader{ + "ContractWithConflict": { + ContractABI: "[]", + Configs: map[string]*types.ChainReaderDefinition{ + "EventWithConflict": { + ChainSpecificName: "EventName", + ReadType: types.Event, + EventDefinitions: &types.EventDefinitions{ + PollingFilter: &types.PollingFilter{}, + }, + }, + }, + ContractPollingFilter: types.ContractPollingFilter{ + GenericEventNames: []string{"EventWithConflict"}, + }, + }, + }, + expectedError: fmt.Errorf( + "%w: conflicting chain reader polling filter definitions for contract: %s event: %s, can't have polling filter defined both on contract and event level", + clcommontypes.ErrInvalidConfig, "ContractWithConflict", "EventWithConflict"), + }, + { + name: "No polling filter defined", + chainContractReaders: map[string]types.ChainContractReader{ + "ContractWithNoFilter": { + ContractABI: "[]", + Configs: map[string]*types.ChainReaderDefinition{ + "EventWithNoFilter": { + ChainSpecificName: "EventName", + ReadType: types.Event, + }, + }, + }, + }, + expectedError: fmt.Errorf( + "%w: chain reader has no polling filter defined for contract: %s, event: %s", + clcommontypes.ErrInvalidConfig, "ContractWithNoFilter", "EventWithNoFilter"), + }, + { + name: "Invalid chain reader definition read type", + chainContractReaders: map[string]types.ChainContractReader{ + "ContractWithInvalidReadType": { + ContractABI: "[]", + Configs: map[string]*types.ChainReaderDefinition{ + "InvalidReadType": { + ChainSpecificName: "InvalidName", + ReadType: types.ReadType(2), + }, + }, + }, + }, + expectedError: fmt.Errorf( + "%w: invalid chain reader definition read type", + clcommontypes.ErrInvalidConfig), + }, + { + name: "Event not present in ABI", + chainContractReaders: map[string]types.ChainContractReader{ + "ContractWithConflict": { + ContractABI: "[{\"anonymous\":false,\"inputs\":[],\"name\":\"WrongEvent\",\"type\":\"event\"}]", + Configs: map[string]*types.ChainReaderDefinition{ + "SomeEvent": { + ChainSpecificName: "EventName", + ReadType: types.Event, + }, + }, + ContractPollingFilter: types.ContractPollingFilter{ + GenericEventNames: []string{"SomeEvent"}, + }, + }, + }, + expectedError: fmt.Errorf( + "%w: event %s doesn't exist", + clcommontypes.ErrInvalidConfig, "EventName"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := evm.NewChainReaderService(testutils.Context(t), logger.NullLogger, nil, nil, types.ChainReaderConfig{Contracts: tt.chainContractReaders}) + require.Error(t, err) + if err != nil { + assert.Contains(t, err.Error(), tt.expectedError.Error()) + } + }) + } +} + func TestChainReaderInterfaceTests(t *testing.T) { // TODO QueryKey test is flaky BCF-3258 t.Parallel() diff --git a/core/services/relay/evm/types/types.go b/core/services/relay/evm/types/types.go index 6aa0a8c3a82..e29b1e6b77f 100644 --- a/core/services/relay/evm/types/types.go +++ b/core/services/relay/evm/types/types.go @@ -56,7 +56,7 @@ type CodecConfig struct { type ChainCodecConfig struct { TypeABI string `json:"typeAbi" toml:"typeABI"` - ModifierConfigs codec.ModifiersConfig `toml:"modifierConfigs,omitempty"` + ModifierConfigs codec.ModifiersConfig `json:"modifierConfigs,omitempty" toml:"modifierConfigs,omitempty"` } type ContractPollingFilter struct { From 0cfc95a374448d83d29ffe981706f69b45760c07 Mon Sep 17 00:00:00 2001 From: ilija Date: Mon, 10 Jun 2024 18:49:52 +0200 Subject: [PATCH 17/21] Add Chain Reader config json marshall test --- core/services/relay/evm/types/types_test.go | 115 ++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/core/services/relay/evm/types/types_test.go b/core/services/relay/evm/types/types_test.go index fb394ddf38b..13dcf611fcb 100644 --- a/core/services/relay/evm/types/types_test.go +++ b/core/services/relay/evm/types/types_test.go @@ -1,15 +1,21 @@ package types import ( + "encoding/json" "fmt" "testing" + "time" + "github.com/ethereum/go-ethereum/common" "github.com/pelletier/go-toml" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink-common/pkg/codec" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/store/models" ) // ChainID *big.Big `json:"chainID"` @@ -39,3 +45,112 @@ FeedID = "0x%x" assert.Equal(t, fromBlock, rc.FromBlock) assert.Equal(t, feedID.Hex(), rc.FeedID.Hex()) } + +func Test_ChainReaderConfig(t *testing.T) { + tests := []struct { + name string + jsonInput string + expected ChainReaderConfig + }{ + { + name: "Valid JSON", + jsonInput: ` +{ + "contracts":{ + "Contract1":{ + "contractABI":"[ { \"anonymous\": false, \"inputs\": [ { \"indexed\": true, \"internalType\": \"address\", \"name\": \"requester\", \"type\": \"address\" }, { \"indexed\": false, \"internalType\": \"bytes32\", \"name\": \"configDigest\", \"type\": \"bytes32\" }, { \"indexed\": false, \"internalType\": \"uint32\", \"name\": \"epoch\", \"type\": \"uint32\" }, { \"indexed\": false, \"internalType\": \"uint8\", \"name\": \"round\", \"type\": \"uint8\" } ], \"name\": \"RoundRequested\", \"type\": \"event\" }, { \"inputs\": [], \"name\": \"latestTransmissionDetails\", \"outputs\": [ { \"internalType\": \"bytes32\", \"name\": \"configDigest\", \"type\": \"bytes32\" }, { \"internalType\": \"uint32\", \"name\": \"epoch\", \"type\": \"uint32\" }, { \"internalType\": \"uint8\", \"name\": \"round\", \"type\": \"uint8\" }, { \"internalType\": \"int192\", \"name\": \"latestAnswer_\", \"type\": \"int192\" }, { \"internalType\": \"uint64\", \"name\": \"latestTimestamp_\", \"type\": \"uint64\" } ], \"stateMutability\": \"view\", \"type\": \"function\" }]", + "contractPollingFilter":{ + "genericEventNames":[ + "event1", + "event2" + ], + "pollingFilter":{ + "topic2":[ + "0x1abbe4784b1fb071039bb9cb50b82978fb5d3ab98fb512c032e75786b93e2c52" + ], + "topic3":[ + "0x2abbe4784b1fb071039bb9cb50b82978fb5d3ab98fb512c032e75786b93e2c52" + ], + "topic4":[ + "0x3abbe4784b1fb071039bb9cb50b82978fb5d3ab98fb512c032e75786b93e2c52" + ], + "retention":"1m0s", + "maxLogsKept":100, + "logsPerBlock":10 + } + }, + "configs":{ + "config1":"{\"cacheEnabled\":true,\"chainSpecificName\":\"specificName1\",\"inputModifications\":[{\"Fields\":[\"ts\"],\"Type\":\"epoch to time\"},{\"Fields\":{\"a\":\"b\"},\"Type\":\"rename\"}],\"outputModifications\":[{\"Fields\":[\"ts\"],\"Type\":\"epoch to time\"},{\"Fields\":{\"c\":\"d\"},\"Type\":\"rename\"}],\"eventDefinitions\":{\"genericTopicNames\":{\"TopicKey1\":\"TopicVal1\"},\"genericDataWordNames\":{\"DataWordKey\":1},\"inputFields\":[\"Event1\",\"Event2\"],\"pollingFilter\":{\"topic2\":[\"0x4abbe4784b1fb071039bb9cb50b82978fb5d3ab98fb512c032e75786b93e2c52\"],\"topic3\":[\"0x5abbe4784b1fb071039bb9cb50b82978fb5d3ab98fb512c032e75786b93e2c52\"],\"topic4\":[\"0x6abbe4784b1fb071039bb9cb50b82978fb5d3ab98fb512c032e75786b93e2c52\"],\"retention\":\"1m0s\",\"maxLogsKept\":100,\"logsPerBlock\":10}},\"confidenceConfirmations\":{\"0.0\":0,\"1.0\":-1}}" + } + } + } +} +`, expected: ChainReaderConfig{ + Contracts: map[string]ChainContractReader{ + "Contract1": { + ContractABI: "[ { \"anonymous\": false, \"inputs\": [ { \"indexed\": true, \"internalType\": \"address\", \"name\": \"requester\", \"type\": \"address\" }, { \"indexed\": false, \"internalType\": \"bytes32\", \"name\": \"configDigest\", \"type\": \"bytes32\" }, { \"indexed\": false, \"internalType\": \"uint32\", \"name\": \"epoch\", \"type\": \"uint32\" }, { \"indexed\": false, \"internalType\": \"uint8\", \"name\": \"round\", \"type\": \"uint8\" } ], \"name\": \"RoundRequested\", \"type\": \"event\" }, { \"inputs\": [], \"name\": \"latestTransmissionDetails\", \"outputs\": [ { \"internalType\": \"bytes32\", \"name\": \"configDigest\", \"type\": \"bytes32\" }, { \"internalType\": \"uint32\", \"name\": \"epoch\", \"type\": \"uint32\" }, { \"internalType\": \"uint8\", \"name\": \"round\", \"type\": \"uint8\" }, { \"internalType\": \"int192\", \"name\": \"latestAnswer_\", \"type\": \"int192\" }, { \"internalType\": \"uint64\", \"name\": \"latestTimestamp_\", \"type\": \"uint64\" } ], \"stateMutability\": \"view\", \"type\": \"function\" }]", + ContractPollingFilter: ContractPollingFilter{ + GenericEventNames: []string{"event1", "event2"}, + PollingFilter: PollingFilter{ + Topic2: evmtypes.HashArray{common.HexToHash("0x1abbe4784b1fb071039bb9cb50b82978fb5d3ab98fb512c032e75786b93e2c52")}, + Topic3: evmtypes.HashArray{common.HexToHash("0x2abbe4784b1fb071039bb9cb50b82978fb5d3ab98fb512c032e75786b93e2c52")}, + Topic4: evmtypes.HashArray{common.HexToHash("0x3abbe4784b1fb071039bb9cb50b82978fb5d3ab98fb512c032e75786b93e2c52")}, + Retention: models.Interval(time.Minute * 1), + MaxLogsKept: 100, + LogsPerBlock: 10, + }, + }, + Configs: map[string]*ChainReaderDefinition{ + "config1": { + CacheEnabled: true, + ChainSpecificName: "specificName1", + ReadType: Method, + InputModifications: codec.ModifiersConfig{ + &codec.EpochToTimeModifierConfig{ + Fields: []string{"ts"}, + }, + &codec.RenameModifierConfig{ + Fields: map[string]string{ + "a": "b", + }, + }, + }, + OutputModifications: codec.ModifiersConfig{ + &codec.EpochToTimeModifierConfig{ + Fields: []string{"ts"}, + }, + &codec.RenameModifierConfig{ + Fields: map[string]string{ + "c": "d", + }, + }, + }, + ConfidenceConfirmations: map[string]int{"0.0": 0, "1.0": -1}, + EventDefinitions: &EventDefinitions{ + GenericTopicNames: map[string]string{"TopicKey1": "TopicVal1"}, + GenericDataWordNames: map[string]uint8{"DataWordKey": 1}, + InputFields: []string{"Event1", "Event2"}, + PollingFilter: &PollingFilter{ + Topic2: evmtypes.HashArray{common.HexToHash("0x4abbe4784b1fb071039bb9cb50b82978fb5d3ab98fb512c032e75786b93e2c52")}, + Topic3: evmtypes.HashArray{common.HexToHash("0x5abbe4784b1fb071039bb9cb50b82978fb5d3ab98fb512c032e75786b93e2c52")}, + Topic4: evmtypes.HashArray{common.HexToHash("0x6abbe4784b1fb071039bb9cb50b82978fb5d3ab98fb512c032e75786b93e2c52")}, + Retention: models.Interval(time.Minute * 1), + MaxLogsKept: 100, + LogsPerBlock: 10, + }, + }, + }, + }, + }, + }, + }, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var config ChainReaderConfig + config.Contracts = make(map[string]ChainContractReader) + assert.Nil(t, json.Unmarshal([]byte(tt.jsonInput), &config)) + assert.Equal(t, tt.expected, config) + }) + } +} From 4a272d003cf49e9d70fa85a7e3616d42ef6a09ce Mon Sep 17 00:00:00 2001 From: ilija Date: Tue, 11 Jun 2024 15:03:22 +0200 Subject: [PATCH 18/21] Use require instead of assert for Test_ChainReaderConfig --- core/services/relay/evm/types/types_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/services/relay/evm/types/types_test.go b/core/services/relay/evm/types/types_test.go index 13dcf611fcb..37d5e77693a 100644 --- a/core/services/relay/evm/types/types_test.go +++ b/core/services/relay/evm/types/types_test.go @@ -149,8 +149,8 @@ func Test_ChainReaderConfig(t *testing.T) { t.Run(tt.name, func(t *testing.T) { var config ChainReaderConfig config.Contracts = make(map[string]ChainContractReader) - assert.Nil(t, json.Unmarshal([]byte(tt.jsonInput), &config)) - assert.Equal(t, tt.expected, config) + require.Nil(t, json.Unmarshal([]byte(tt.jsonInput), &config)) + require.Equal(t, tt.expected, config) }) } } From d3f8ae08e1bde6201d6f8fa55f83d81bde3f84b6 Mon Sep 17 00:00:00 2001 From: ilija Date: Tue, 11 Jun 2024 18:13:22 +0200 Subject: [PATCH 19/21] Add comments to AddReadBinding --- core/services/relay/evm/bindings.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/services/relay/evm/bindings.go b/core/services/relay/evm/bindings.go index 42cb933fda4..9ad73c01926 100644 --- a/core/services/relay/evm/bindings.go +++ b/core/services/relay/evm/bindings.go @@ -25,8 +25,8 @@ func (b bindings) GetReadBinding(contractName, readName string) (readBinding, er return rb, nil } +// AddReadBinding adds read bindings. Calling this outside of Chain Reader init is not thread safe. func (b bindings) AddReadBinding(contractName, readName string, rb readBinding) { - // Adding read bindings outside of Chain Reader init is not thread safe. cb, cbExists := b[contractName] if !cbExists { cb = &contractBinding{ From 81d9b9d85ca0522d7f1f2e2c161908ad8a5ab76e Mon Sep 17 00:00:00 2001 From: ilija Date: Thu, 13 Jun 2024 13:05:07 +0200 Subject: [PATCH 20/21] Rename CR verifyEventInputsUsed to verifyEventIndexedInputsUsed --- core/services/relay/evm/chain_reader.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/services/relay/evm/chain_reader.go b/core/services/relay/evm/chain_reader.go index 9fef7260f1d..9cfe846d55d 100644 --- a/core/services/relay/evm/chain_reader.go +++ b/core/services/relay/evm/chain_reader.go @@ -229,7 +229,7 @@ func (cr *chainReader) addEvent(contractName, eventName string, a abi.ABI, chain } filterArgs, codecTopicInfo, indexArgNames := setupEventInput(event, inputFields) - if err := verifyEventInputsUsed(eventName, inputFields, indexArgNames); err != nil { + if err := verifyEventIndexedInputsUsed(eventName, inputFields, indexArgNames); err != nil { return err } @@ -321,7 +321,7 @@ func (cr *chainReader) getEventInput(def types.ChainReaderDefinition, contractNa return inputInfo, inMod, nil } -func verifyEventInputsUsed(eventName string, inputFields []string, indexArgNames map[string]bool) error { +func verifyEventIndexedInputsUsed(eventName string, inputFields []string, indexArgNames map[string]bool) error { for _, value := range inputFields { if !indexArgNames[abi.ToCamelCase(value)] { return fmt.Errorf("%w: %s is not an indexed argument of event %s", commontypes.ErrInvalidConfig, value, eventName) From 3acb2c4297197772f600b239c45e2b4212625473 Mon Sep 17 00:00:00 2001 From: ilija Date: Thu, 13 Jun 2024 16:26:10 +0200 Subject: [PATCH 21/21] Resolve merge issues in chain_reader_interface_tester.go --- .../chain_reader_interface_tester.go | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/core/services/relay/evm/evmtesting/chain_reader_interface_tester.go b/core/services/relay/evm/evmtesting/chain_reader_interface_tester.go index a2c9180d3bf..61a45996ac2 100644 --- a/core/services/relay/evm/evmtesting/chain_reader_interface_tester.go +++ b/core/services/relay/evm/evmtesting/chain_reader_interface_tester.go @@ -83,6 +83,9 @@ func (it *EVMChainReaderInterfaceTester[T]) Setup(t T) { Contracts: map[string]types.ChainContractReader{ AnyContractName: { ContractABI: chain_reader_tester.ChainReaderTesterMetaData.ABI, + ContractPollingFilter: types.ContractPollingFilter{ + GenericEventNames: []string{EventName, EventWithFilterName}, + }, Configs: map[string]*types.ChainReaderDefinition{ MethodTakingLatestParamsReturningTestStruct: { ChainSpecificName: "getElementAtIndex", @@ -110,22 +113,30 @@ func (it *EVMChainReaderInterfaceTester[T]) Setup(t T) { EventWithFilterName: { ChainSpecificName: "Triggered", ReadType: types.Event, - EventInputFields: []string{"Field"}, + EventDefinitions: &types.EventDefinitions{InputFields: []string{"Field"}}, ConfidenceConfirmations: map[string]int{"0.0": 0, "1.0": -1}, }, triggerWithDynamicTopic: { ChainSpecificName: triggerWithDynamicTopic, ReadType: types.Event, - EventInputFields: []string{"fieldHash"}, + EventDefinitions: &types.EventDefinitions{ + InputFields: []string{"fieldHash"}, + // no specific reason for filter being defined here insted on contract level, + // this is just for test case variety + PollingFilter: &types.PollingFilter{}, + }, InputModifications: codec.ModifiersConfig{ &codec.RenameModifierConfig{Fields: map[string]string{"FieldHash": "Field"}}, }, ConfidenceConfirmations: map[string]int{"0.0": 0, "1.0": -1}, }, triggerWithAllTopics: { - ChainSpecificName: triggerWithAllTopics, - ReadType: types.Event, - EventInputFields: []string{"Field1", "Field2", "Field3"}, + ChainSpecificName: triggerWithAllTopics, + ReadType: types.Event, + EventDefinitions: &types.EventDefinitions{ + InputFields: []string{"Field1", "Field2", "Field3"}, + PollingFilter: &types.PollingFilter{}, + }, ConfidenceConfirmations: map[string]int{"0.0": 0, "1.0": -1}, }, MethodReturningSeenStruct: {