From a93f9e9fd1e990a582b97b238935df1c3551e40e Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Mon, 2 Sep 2024 15:40:03 +0100 Subject: [PATCH 01/36] Log Event Trigger Capability --- .../log_event_trigger/log_event_trigger.go | 99 ++++++++++++++ .../log_event_trigger/trigger/store.go | 68 ++++++++++ .../log_event_trigger/trigger/trigger.go | 122 ++++++++++++++++++ 3 files changed, 289 insertions(+) create mode 100644 core/capabilities/log_event_trigger/log_event_trigger.go create mode 100644 core/capabilities/log_event_trigger/trigger/store.go create mode 100644 core/capabilities/log_event_trigger/trigger/trigger.go diff --git a/core/capabilities/log_event_trigger/log_event_trigger.go b/core/capabilities/log_event_trigger/log_event_trigger.go new file mode 100644 index 00000000000..23f4f1d8b31 --- /dev/null +++ b/core/capabilities/log_event_trigger/log_event_trigger.go @@ -0,0 +1,99 @@ +package log_event_trigger + +import ( + "context" + "fmt" + + "github.com/hashicorp/go-plugin" + + "github.com/smartcontractkit/chainlink/v2/core/capabilities/log_event_trigger/trigger" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/loop" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" +) + +const ( + serviceName = "LogEventTriggerCapability" +) + +type LogEventServiceGRPC struct { + trigger capabilities.TriggerCapability + s *loop.Server +} + +func main() { + s := loop.MustNewStartedServer(serviceName) + defer s.Stop() + + s.Logger.Infof("Starting %s", serviceName) + + stopCh := make(chan struct{}) + defer close(stopCh) + + plugin.Serve(&plugin.ServeConfig{ + HandshakeConfig: loop.StandardCapabilitiesHandshakeConfig(), + Plugins: map[string]plugin.Plugin{ + loop.PluginStandardCapabilitiesName: &loop.StandardCapabilitiesLoop{ + PluginServer: &LogEventServiceGRPC{ + s: s, + }, + BrokerConfig: loop.BrokerConfig{Logger: s.Logger, StopCh: stopCh, GRPCOpts: s.GRPCOpts}, + }, + }, + GRPCServer: s.GRPCOpts.NewServer, + }) +} + +func (cs *LogEventServiceGRPC) Start(ctx context.Context) error { + return nil +} + +func (cs *LogEventServiceGRPC) Close() error { + return nil +} + +func (cs *LogEventServiceGRPC) Ready() error { + return nil +} + +func (cs *LogEventServiceGRPC) HealthReport() map[string]error { + return nil +} + +func (cs *LogEventServiceGRPC) Name() string { + return serviceName +} + +func (cs *LogEventServiceGRPC) Infos(ctx context.Context) ([]capabilities.CapabilityInfo, error) { + triggerInfo, err := cs.trigger.Info(ctx) + if err != nil { + return nil, err + } + + return []capabilities.CapabilityInfo{ + triggerInfo, + }, nil +} + +func (cs *LogEventServiceGRPC) Initialise( + ctx context.Context, + config string, + telemetryService core.TelemetryService, + store core.KeyValueStore, + capabilityRegistry core.CapabilitiesRegistry, + errorLog core.ErrorLog, + pipelineRunner core.PipelineRunnerService, + relayerSet core.RelayerSet, +) error { + cs.s.Logger.Debugf("Initialising %s", serviceName) + cs.trigger = trigger.New(trigger.Params{ + Logger: cs.s.Logger, + }) + + if err := capabilityRegistry.Add(ctx, cs.trigger); err != nil { + return fmt.Errorf("error when adding cron trigger to the registry: %w", err) + } + + return nil +} diff --git a/core/capabilities/log_event_trigger/trigger/store.go b/core/capabilities/log_event_trigger/trigger/store.go new file mode 100644 index 00000000000..ef49fe52cfa --- /dev/null +++ b/core/capabilities/log_event_trigger/trigger/store.go @@ -0,0 +1,68 @@ +package trigger + +import ( + "fmt" + "sync" +) + +type NewCapabilityFn[T any, Resp any] func() (T, chan Resp) + +// Interface of the capabilities store +type CapabilitiesStore[T any, Resp any] interface { + Read(capabilityID string) (value T, ok bool) + ReadAll() (values map[string]T) + Write(capabilityID string, value T) + InsertIfNotExists(capabilityID string, fn NewCapabilityFn[T, Resp]) (chan Resp, error) + Delete(capabilityID string) +} + +// Implementation for the CapabilitiesStore interface +type capabilitiesStore[T any, Resp any] struct { + mu sync.RWMutex + capabilities map[string]T +} + +var _ CapabilitiesStore[string, string] = (CapabilitiesStore[string, string])(nil) + +// Constructor for capabilitiesStore struct implementing CapabilitiesStore interface +func NewCapabilitiesStore[T any, Resp any]() CapabilitiesStore[T, Resp] { + return &capabilitiesStore[T, Resp]{ + capabilities: map[string]T{}, + } +} + +func (cs *capabilitiesStore[T, Resp]) Read(capabilityID string) (value T, ok bool) { + cs.mu.RLock() + defer cs.mu.RUnlock() + trigger, ok := cs.capabilities[capabilityID] + return trigger, ok +} + +func (cs *capabilitiesStore[T, Resp]) ReadAll() (values map[string]T) { + cs.mu.RLock() + defer cs.mu.RUnlock() + return cs.capabilities +} + +func (cs *capabilitiesStore[T, Resp]) Write(capabilityID string, value T) { + cs.mu.Lock() + defer cs.mu.Unlock() + cs.capabilities[capabilityID] = value +} + +func (cs *capabilitiesStore[T, Resp]) InsertIfNotExists(capabilityID string, fn NewCapabilityFn[T, Resp]) (chan Resp, error) { + cs.mu.Lock() + defer cs.mu.Unlock() + if _, ok := cs.capabilities[capabilityID]; ok { + return nil, fmt.Errorf("capabilityID %v already exists", capabilityID) + } + value, respCh := fn() + cs.capabilities[capabilityID] = value + return respCh, nil +} + +func (cs *capabilitiesStore[T, Resp]) Delete(capabilityID string) { + cs.mu.Lock() + defer cs.mu.Unlock() + delete(cs.capabilities, capabilityID) +} diff --git a/core/capabilities/log_event_trigger/trigger/trigger.go b/core/capabilities/log_event_trigger/trigger/trigger.go new file mode 100644 index 00000000000..557f151b0c5 --- /dev/null +++ b/core/capabilities/log_event_trigger/trigger/trigger.go @@ -0,0 +1,122 @@ +package trigger + +import ( + "context" + "errors" + "fmt" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" +) + +const ID = "cron-trigger@1.0.0" + +const defaultSendChannelBufferSize = 1000 + +var logEventTriggerInfo = capabilities.MustNewCapabilityInfo( + ID, + capabilities.CapabilityTypeTrigger, + "A trigger that listens for specific contract log events and starts a workflow run.", +) + +// Log Event Trigger Capability Config +type Config struct { +} + +// Log Event Trigger Capability Payload +type Payload struct { + // Time that Log Event Trigger's task execution occurred (RFC3339Nano formatted) + ActualExecutionTime string +} + +// Log Event Trigger Capability Response +type Response struct { + capabilities.TriggerEvent + Metadata struct{} + Payload Payload +} + +type logEventTrigger struct { + ch chan<- capabilities.TriggerResponse +} + +// Log Event Trigger Capabilities Manager +// Manages different log event triggers using an underlying triggerStore +type LogEventTriggerManager struct { + capabilities.CapabilityInfo + capabilities.Validator[Config, struct{}, capabilities.TriggerResponse] + lggr logger.Logger + triggers CapabilitiesStore[logEventTrigger, capabilities.TriggerResponse] +} + +type Params struct { + Logger logger.Logger +} + +var _ capabilities.TriggerCapability = (*LogEventTriggerManager)(nil) +var _ services.Service = &LogEventTriggerManager{} + +// Creates a new Cron Trigger Service. +// Scheduling will commence on calling .Start() +func New(p Params) *LogEventTriggerManager { + l := logger.Named(p.Logger, "Log Event Trigger Capability Service") + + logEventStore := NewCapabilitiesStore[logEventTrigger, capabilities.TriggerResponse]() + + return &LogEventTriggerManager{ + CapabilityInfo: logEventTriggerInfo, + lggr: l, + triggers: logEventStore, + } +} + +// Register a new trigger +// Can register triggers before the service is actively scheduling +func (s *LogEventTriggerManager) RegisterTrigger(ctx context.Context, req capabilities.TriggerRegistrationRequest) (<-chan capabilities.TriggerResponse, error) { + if req.Config == nil { + return nil, errors.New("config is required to register a log event trigger") + } + _, err := s.ValidateConfig(req.Config) + if err != nil { + return nil, err + } + respCh, err := s.triggers.InsertIfNotExists(req.TriggerID, func() (logEventTrigger, chan capabilities.TriggerResponse) { + callbackCh := make(chan capabilities.TriggerResponse, defaultSendChannelBufferSize) + return logEventTrigger{ + ch: callbackCh, + }, callbackCh + }) + if err != nil { + return nil, fmt.Errorf("log_event_trigger %v", err) + } + return respCh, nil +} + +func (s *LogEventTriggerManager) UnregisterTrigger(ctx context.Context, req capabilities.TriggerRegistrationRequest) error { + return nil +} + +// Start the service. +func (s *LogEventTriggerManager) Start(ctx context.Context) error { + return nil +} + +// Close stops the Service. +// After this call the Service cannot be started again, +// The service will need to be re-built to start scheduling again. +func (s *LogEventTriggerManager) Close() error { + return nil +} + +func (s *LogEventTriggerManager) Ready() error { + return nil +} + +func (s *LogEventTriggerManager) HealthReport() map[string]error { + return map[string]error{s.Name(): nil} +} + +func (s *LogEventTriggerManager) Name() string { + return "Service" +} From 072214d95b8f37ca2046e814583a8c1582aa754b Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Mon, 2 Sep 2024 15:44:56 +0100 Subject: [PATCH 02/36] Minor refactoring --- .../log_event_trigger/log_event_trigger.go | 20 ++++++++-------- .../log_event_trigger/trigger/trigger.go | 24 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/core/capabilities/log_event_trigger/log_event_trigger.go b/core/capabilities/log_event_trigger/log_event_trigger.go index 23f4f1d8b31..5535e335e53 100644 --- a/core/capabilities/log_event_trigger/log_event_trigger.go +++ b/core/capabilities/log_event_trigger/log_event_trigger.go @@ -17,7 +17,7 @@ const ( serviceName = "LogEventTriggerCapability" ) -type LogEventServiceGRPC struct { +type LogEventTriggerGRPCService struct { trigger capabilities.TriggerCapability s *loop.Server } @@ -35,7 +35,7 @@ func main() { HandshakeConfig: loop.StandardCapabilitiesHandshakeConfig(), Plugins: map[string]plugin.Plugin{ loop.PluginStandardCapabilitiesName: &loop.StandardCapabilitiesLoop{ - PluginServer: &LogEventServiceGRPC{ + PluginServer: &LogEventTriggerGRPCService{ s: s, }, BrokerConfig: loop.BrokerConfig{Logger: s.Logger, StopCh: stopCh, GRPCOpts: s.GRPCOpts}, @@ -45,27 +45,27 @@ func main() { }) } -func (cs *LogEventServiceGRPC) Start(ctx context.Context) error { +func (cs *LogEventTriggerGRPCService) Start(ctx context.Context) error { return nil } -func (cs *LogEventServiceGRPC) Close() error { +func (cs *LogEventTriggerGRPCService) Close() error { return nil } -func (cs *LogEventServiceGRPC) Ready() error { +func (cs *LogEventTriggerGRPCService) Ready() error { return nil } -func (cs *LogEventServiceGRPC) HealthReport() map[string]error { +func (cs *LogEventTriggerGRPCService) HealthReport() map[string]error { return nil } -func (cs *LogEventServiceGRPC) Name() string { +func (cs *LogEventTriggerGRPCService) Name() string { return serviceName } -func (cs *LogEventServiceGRPC) Infos(ctx context.Context) ([]capabilities.CapabilityInfo, error) { +func (cs *LogEventTriggerGRPCService) Infos(ctx context.Context) ([]capabilities.CapabilityInfo, error) { triggerInfo, err := cs.trigger.Info(ctx) if err != nil { return nil, err @@ -76,7 +76,7 @@ func (cs *LogEventServiceGRPC) Infos(ctx context.Context) ([]capabilities.Capabi }, nil } -func (cs *LogEventServiceGRPC) Initialise( +func (cs *LogEventTriggerGRPCService) Initialise( ctx context.Context, config string, telemetryService core.TelemetryService, @@ -87,7 +87,7 @@ func (cs *LogEventServiceGRPC) Initialise( relayerSet core.RelayerSet, ) error { cs.s.Logger.Debugf("Initialising %s", serviceName) - cs.trigger = trigger.New(trigger.Params{ + cs.trigger = trigger.NewLogEventTriggerService(trigger.Params{ Logger: cs.s.Logger, }) diff --git a/core/capabilities/log_event_trigger/trigger/trigger.go b/core/capabilities/log_event_trigger/trigger/trigger.go index 557f151b0c5..4dd39aea8aa 100644 --- a/core/capabilities/log_event_trigger/trigger/trigger.go +++ b/core/capabilities/log_event_trigger/trigger/trigger.go @@ -43,7 +43,7 @@ type logEventTrigger struct { // Log Event Trigger Capabilities Manager // Manages different log event triggers using an underlying triggerStore -type LogEventTriggerManager struct { +type LogEventTriggerService struct { capabilities.CapabilityInfo capabilities.Validator[Config, struct{}, capabilities.TriggerResponse] lggr logger.Logger @@ -54,17 +54,17 @@ type Params struct { Logger logger.Logger } -var _ capabilities.TriggerCapability = (*LogEventTriggerManager)(nil) -var _ services.Service = &LogEventTriggerManager{} +var _ capabilities.TriggerCapability = (*LogEventTriggerService)(nil) +var _ services.Service = &LogEventTriggerService{} // Creates a new Cron Trigger Service. // Scheduling will commence on calling .Start() -func New(p Params) *LogEventTriggerManager { +func NewLogEventTriggerService(p Params) *LogEventTriggerService { l := logger.Named(p.Logger, "Log Event Trigger Capability Service") logEventStore := NewCapabilitiesStore[logEventTrigger, capabilities.TriggerResponse]() - return &LogEventTriggerManager{ + return &LogEventTriggerService{ CapabilityInfo: logEventTriggerInfo, lggr: l, triggers: logEventStore, @@ -73,7 +73,7 @@ func New(p Params) *LogEventTriggerManager { // Register a new trigger // Can register triggers before the service is actively scheduling -func (s *LogEventTriggerManager) RegisterTrigger(ctx context.Context, req capabilities.TriggerRegistrationRequest) (<-chan capabilities.TriggerResponse, error) { +func (s *LogEventTriggerService) RegisterTrigger(ctx context.Context, req capabilities.TriggerRegistrationRequest) (<-chan capabilities.TriggerResponse, error) { if req.Config == nil { return nil, errors.New("config is required to register a log event trigger") } @@ -93,30 +93,30 @@ func (s *LogEventTriggerManager) RegisterTrigger(ctx context.Context, req capabi return respCh, nil } -func (s *LogEventTriggerManager) UnregisterTrigger(ctx context.Context, req capabilities.TriggerRegistrationRequest) error { +func (s *LogEventTriggerService) UnregisterTrigger(ctx context.Context, req capabilities.TriggerRegistrationRequest) error { return nil } // Start the service. -func (s *LogEventTriggerManager) Start(ctx context.Context) error { +func (s *LogEventTriggerService) Start(ctx context.Context) error { return nil } // Close stops the Service. // After this call the Service cannot be started again, // The service will need to be re-built to start scheduling again. -func (s *LogEventTriggerManager) Close() error { +func (s *LogEventTriggerService) Close() error { return nil } -func (s *LogEventTriggerManager) Ready() error { +func (s *LogEventTriggerService) Ready() error { return nil } -func (s *LogEventTriggerManager) HealthReport() map[string]error { +func (s *LogEventTriggerService) HealthReport() map[string]error { return map[string]error{s.Name(): nil} } -func (s *LogEventTriggerManager) Name() string { +func (s *LogEventTriggerService) Name() string { return "Service" } From 833c0c128364d0de54cbabec07465d9fbfc6704f Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Mon, 2 Sep 2024 15:49:29 +0100 Subject: [PATCH 03/36] Moved main script to plugins/cmd --- core/capabilities/log_event_trigger/{trigger => }/store.go | 2 +- .../capabilities/log_event_trigger/{trigger => }/trigger.go | 2 +- .../cmd/capabilities/log-event-trigger/main.go | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) rename core/capabilities/log_event_trigger/{trigger => }/store.go (98%) rename core/capabilities/log_event_trigger/{trigger => }/trigger.go (99%) rename core/capabilities/log_event_trigger/log_event_trigger.go => plugins/cmd/capabilities/log-event-trigger/main.go (95%) diff --git a/core/capabilities/log_event_trigger/trigger/store.go b/core/capabilities/log_event_trigger/store.go similarity index 98% rename from core/capabilities/log_event_trigger/trigger/store.go rename to core/capabilities/log_event_trigger/store.go index ef49fe52cfa..2460f05fedb 100644 --- a/core/capabilities/log_event_trigger/trigger/store.go +++ b/core/capabilities/log_event_trigger/store.go @@ -1,4 +1,4 @@ -package trigger +package log_event_trigger import ( "fmt" diff --git a/core/capabilities/log_event_trigger/trigger/trigger.go b/core/capabilities/log_event_trigger/trigger.go similarity index 99% rename from core/capabilities/log_event_trigger/trigger/trigger.go rename to core/capabilities/log_event_trigger/trigger.go index 4dd39aea8aa..1bbc0cdfcf8 100644 --- a/core/capabilities/log_event_trigger/trigger/trigger.go +++ b/core/capabilities/log_event_trigger/trigger.go @@ -1,4 +1,4 @@ -package trigger +package log_event_trigger import ( "context" diff --git a/core/capabilities/log_event_trigger/log_event_trigger.go b/plugins/cmd/capabilities/log-event-trigger/main.go similarity index 95% rename from core/capabilities/log_event_trigger/log_event_trigger.go rename to plugins/cmd/capabilities/log-event-trigger/main.go index 5535e335e53..719faeded65 100644 --- a/core/capabilities/log_event_trigger/log_event_trigger.go +++ b/plugins/cmd/capabilities/log-event-trigger/main.go @@ -1,4 +1,4 @@ -package log_event_trigger +package main import ( "context" @@ -6,7 +6,7 @@ import ( "github.com/hashicorp/go-plugin" - "github.com/smartcontractkit/chainlink/v2/core/capabilities/log_event_trigger/trigger" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/log_event_trigger" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/loop" @@ -87,7 +87,7 @@ func (cs *LogEventTriggerGRPCService) Initialise( relayerSet core.RelayerSet, ) error { cs.s.Logger.Debugf("Initialising %s", serviceName) - cs.trigger = trigger.NewLogEventTriggerService(trigger.Params{ + cs.trigger = log_event_trigger.NewLogEventTriggerService(log_event_trigger.Params{ Logger: cs.s.Logger, }) From 73644e644a48b581fdd74f2b9059574dd0f38a6f Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Mon, 2 Sep 2024 15:53:15 +0100 Subject: [PATCH 04/36] Added initial implementation for UnregisterTrigger --- core/capabilities/log_event_trigger/trigger.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/capabilities/log_event_trigger/trigger.go b/core/capabilities/log_event_trigger/trigger.go index 1bbc0cdfcf8..b008a065541 100644 --- a/core/capabilities/log_event_trigger/trigger.go +++ b/core/capabilities/log_event_trigger/trigger.go @@ -90,10 +90,20 @@ func (s *LogEventTriggerService) RegisterTrigger(ctx context.Context, req capabi if err != nil { return nil, fmt.Errorf("log_event_trigger %v", err) } + s.lggr.Debugw("log_event_trigger::RegisterTrigger", "triggerId", req.TriggerID) return respCh, nil } func (s *LogEventTriggerService) UnregisterTrigger(ctx context.Context, req capabilities.TriggerRegistrationRequest) error { + trigger, ok := s.triggers.Read(req.TriggerID) + if !ok { + return fmt.Errorf("triggerId %s not found", req.TriggerID) + } + // Close callback channel + close(trigger.ch) + // Remove from triggers context + s.triggers.Delete(req.TriggerID) + s.lggr.Debugw("log_event_trigger::UnregisterTrigger", "triggerId", req.TriggerID) return nil } From 1b617a6a0529dfba84785d43d241f825b2e4e1c1 Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Mon, 9 Sep 2024 16:58:15 +0100 Subject: [PATCH 05/36] Create NewContractReader in RegisterTrigger flow of the trigger capability --- .../trigger.go => logeventtrigger/service.go} | 67 +++++++++---- .../store.go | 29 +++--- core/capabilities/logeventtrigger/trigger.go | 93 +++++++++++++++++++ go.mod | 2 +- go.sum | 4 +- .../log-event-trigger/main.go | 26 +++++- 6 files changed, 184 insertions(+), 37 deletions(-) rename core/capabilities/{log_event_trigger/trigger.go => logeventtrigger/service.go} (61%) rename core/capabilities/{log_event_trigger => logeventtrigger}/store.go (70%) create mode 100644 core/capabilities/logeventtrigger/trigger.go rename plugins/cmd/{capabilities => evm-chain-capabilities}/log-event-trigger/main.go (72%) diff --git a/core/capabilities/log_event_trigger/trigger.go b/core/capabilities/logeventtrigger/service.go similarity index 61% rename from core/capabilities/log_event_trigger/trigger.go rename to core/capabilities/logeventtrigger/service.go index b008a065541..ee19e2275fc 100644 --- a/core/capabilities/log_event_trigger/trigger.go +++ b/core/capabilities/logeventtrigger/service.go @@ -1,16 +1,20 @@ -package log_event_trigger +package logeventtrigger import ( "context" "errors" "fmt" + "github.com/ethereum/go-ethereum/common" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" + + evmtypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" ) -const ID = "cron-trigger@1.0.0" +const ID = "log-event-trigger-%s-%d@1.0.0" const defaultSendChannelBufferSize = 1000 @@ -20,8 +24,15 @@ var logEventTriggerInfo = capabilities.MustNewCapabilityInfo( "A trigger that listens for specific contract log events and starts a workflow run.", ) -// Log Event Trigger Capability Config -type Config struct { +// Log Event Trigger Capability RequestConfig +type RequestConfig struct { + ContractName string `json:"contractName"` + ContractAddress common.Address `json:"contractAddress"` + ContractReaderConfig evmtypes.ChainReaderConfig `json:"contractReaderConfig"` +} + +// Log Event Trigger Capability Input +type Input struct { } // Log Event Trigger Capability Payload @@ -37,21 +48,29 @@ type Response struct { Payload Payload } -type logEventTrigger struct { - ch chan<- capabilities.TriggerResponse -} - // Log Event Trigger Capabilities Manager // Manages different log event triggers using an underlying triggerStore type LogEventTriggerService struct { capabilities.CapabilityInfo - capabilities.Validator[Config, struct{}, capabilities.TriggerResponse] - lggr logger.Logger - triggers CapabilitiesStore[logEventTrigger, capabilities.TriggerResponse] + capabilities.Validator[RequestConfig, Input, capabilities.TriggerResponse] + lggr logger.Logger + triggers CapabilitiesStore[logEventTrigger, capabilities.TriggerResponse] + relayer core.Relayer + logEventConfig LogEventConfig +} + +// Common capability level config across all workflows +type LogEventConfig struct { + ChainId uint64 `json:"chainId"` + Network string `json:"network"` + LookbackBlocks uint64 `json:"lookbakBlocks"` + PollPeriod uint64 `json:"pollPeriod"` } type Params struct { - Logger logger.Logger + Logger logger.Logger + Relayer core.Relayer + LogEventConfig LogEventConfig } var _ capabilities.TriggerCapability = (*LogEventTriggerService)(nil) @@ -68,24 +87,32 @@ func NewLogEventTriggerService(p Params) *LogEventTriggerService { CapabilityInfo: logEventTriggerInfo, lggr: l, triggers: logEventStore, + relayer: p.Relayer, + logEventConfig: p.LogEventConfig, } } +func (s *LogEventTriggerService) Info(ctx context.Context) (capabilities.CapabilityInfo, error) { + return capabilities.NewCapabilityInfo( + fmt.Sprintf(ID, s.logEventConfig.Network, s.logEventConfig.ChainId), + capabilities.CapabilityTypeTrigger, + "A trigger that listens for specific contract log events and starts a workflow run.", + ) +} + // Register a new trigger // Can register triggers before the service is actively scheduling func (s *LogEventTriggerService) RegisterTrigger(ctx context.Context, req capabilities.TriggerRegistrationRequest) (<-chan capabilities.TriggerResponse, error) { if req.Config == nil { return nil, errors.New("config is required to register a log event trigger") } - _, err := s.ValidateConfig(req.Config) + reqConfig, err := s.ValidateConfig(req.Config) if err != nil { return nil, err } - respCh, err := s.triggers.InsertIfNotExists(req.TriggerID, func() (logEventTrigger, chan capabilities.TriggerResponse) { - callbackCh := make(chan capabilities.TriggerResponse, defaultSendChannelBufferSize) - return logEventTrigger{ - ch: callbackCh, - }, callbackCh + // Add log event trigger with Contract details to CapabilitiesStore + respCh, err := s.triggers.InsertIfNotExists(req.TriggerID, func() (*logEventTrigger, chan capabilities.TriggerResponse, error) { + return newLogEventTrigger(ctx, reqConfig, s.logEventConfig, s.relayer) }) if err != nil { return nil, fmt.Errorf("log_event_trigger %v", err) @@ -109,6 +136,10 @@ func (s *LogEventTriggerService) UnregisterTrigger(ctx context.Context, req capa // Start the service. func (s *LogEventTriggerService) Start(ctx context.Context) error { + if s.relayer == nil { + return errors.New("service has shutdown, it must be built again to restart") + } + return nil } diff --git a/core/capabilities/log_event_trigger/store.go b/core/capabilities/logeventtrigger/store.go similarity index 70% rename from core/capabilities/log_event_trigger/store.go rename to core/capabilities/logeventtrigger/store.go index 2460f05fedb..d9c9234aaf7 100644 --- a/core/capabilities/log_event_trigger/store.go +++ b/core/capabilities/logeventtrigger/store.go @@ -1,25 +1,25 @@ -package log_event_trigger +package logeventtrigger import ( "fmt" "sync" ) -type NewCapabilityFn[T any, Resp any] func() (T, chan Resp) +type RegisterCapabilityFn[T any, Resp any] func() (*T, chan Resp, error) // Interface of the capabilities store type CapabilitiesStore[T any, Resp any] interface { - Read(capabilityID string) (value T, ok bool) - ReadAll() (values map[string]T) - Write(capabilityID string, value T) - InsertIfNotExists(capabilityID string, fn NewCapabilityFn[T, Resp]) (chan Resp, error) + Read(capabilityID string) (value *T, ok bool) + ReadAll() (values map[string]*T) + Write(capabilityID string, value *T) + InsertIfNotExists(capabilityID string, fn RegisterCapabilityFn[T, Resp]) (chan Resp, error) Delete(capabilityID string) } // Implementation for the CapabilitiesStore interface type capabilitiesStore[T any, Resp any] struct { mu sync.RWMutex - capabilities map[string]T + capabilities map[string]*T } var _ CapabilitiesStore[string, string] = (CapabilitiesStore[string, string])(nil) @@ -27,36 +27,39 @@ var _ CapabilitiesStore[string, string] = (CapabilitiesStore[string, string])(ni // Constructor for capabilitiesStore struct implementing CapabilitiesStore interface func NewCapabilitiesStore[T any, Resp any]() CapabilitiesStore[T, Resp] { return &capabilitiesStore[T, Resp]{ - capabilities: map[string]T{}, + capabilities: map[string]*T{}, } } -func (cs *capabilitiesStore[T, Resp]) Read(capabilityID string) (value T, ok bool) { +func (cs *capabilitiesStore[T, Resp]) Read(capabilityID string) (value *T, ok bool) { cs.mu.RLock() defer cs.mu.RUnlock() trigger, ok := cs.capabilities[capabilityID] return trigger, ok } -func (cs *capabilitiesStore[T, Resp]) ReadAll() (values map[string]T) { +func (cs *capabilitiesStore[T, Resp]) ReadAll() (values map[string]*T) { cs.mu.RLock() defer cs.mu.RUnlock() return cs.capabilities } -func (cs *capabilitiesStore[T, Resp]) Write(capabilityID string, value T) { +func (cs *capabilitiesStore[T, Resp]) Write(capabilityID string, value *T) { cs.mu.Lock() defer cs.mu.Unlock() cs.capabilities[capabilityID] = value } -func (cs *capabilitiesStore[T, Resp]) InsertIfNotExists(capabilityID string, fn NewCapabilityFn[T, Resp]) (chan Resp, error) { +func (cs *capabilitiesStore[T, Resp]) InsertIfNotExists(capabilityID string, fn RegisterCapabilityFn[T, Resp]) (chan Resp, error) { cs.mu.Lock() defer cs.mu.Unlock() if _, ok := cs.capabilities[capabilityID]; ok { return nil, fmt.Errorf("capabilityID %v already exists", capabilityID) } - value, respCh := fn() + value, respCh, err := fn() + if err != nil { + return nil, fmt.Errorf("error registering capability: %v", err) + } cs.capabilities[capabilityID] = value return respCh, nil } diff --git a/core/capabilities/logeventtrigger/trigger.go b/core/capabilities/logeventtrigger/trigger.go new file mode 100644 index 00000000000..228efc42cc4 --- /dev/null +++ b/core/capabilities/logeventtrigger/trigger.go @@ -0,0 +1,93 @@ +package logeventtrigger + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" + + evmtypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" +) + +type logEventTrigger struct { + ch chan<- capabilities.TriggerResponse + + // Contract address and Event Signature to monitor for + contractName string + contractAddress common.Address + contractReaderConfig evmtypes.ChainReaderConfig + contractReader types.ContractReader + + // Log Event Trigger config with pollPeriod and lookbackBlocks + logEventConfig LogEventConfig + ticker *time.Ticker + done chan bool +} + +// Construct for logEventTrigger struct +func newLogEventTrigger(ctx context.Context, + reqConfig *RequestConfig, + logEventConfig LogEventConfig, + relayer core.Relayer) (*logEventTrigger, chan capabilities.TriggerResponse, error) { + jsonBytes, err := json.Marshal(reqConfig.ContractReaderConfig) + if err != nil { + return nil, nil, err + } + + // Create a New Contract Reader client, which brings a corresponding ContractReader gRPC service + // in Chainlink Core service + contractReader, err := relayer.NewContractReader(ctx, jsonBytes) + if err != nil { + return nil, nil, + fmt.Errorf("error fetching contractReader for chainID %d from relayerSet: %v", logEventConfig.ChainId, err) + } + + // Bind Contract in ContractReader + boundContracts := []types.BoundContract{{Name: reqConfig.ContractName, Address: reqConfig.ContractAddress.Hex()}} + err = contractReader.Bind(ctx, boundContracts) + if err != nil { + return nil, nil, err + } + + callbackCh := make(chan capabilities.TriggerResponse, defaultSendChannelBufferSize) + ticker := time.NewTicker(time.Duration(logEventConfig.PollPeriod) * time.Millisecond) + done := make(chan bool) + + // Initialise a Log Event Trigger + l := &logEventTrigger{ + ch: callbackCh, + contractName: reqConfig.ContractName, + contractAddress: reqConfig.ContractAddress, + contractReaderConfig: reqConfig.ContractReaderConfig, + contractReader: contractReader, + logEventConfig: logEventConfig, + ticker: ticker, + done: done, + } + go l.Listen() + + return l, callbackCh, nil +} + +// Listen to contract events and trigger workflow runs +func (l *logEventTrigger) Listen() { + // Listen for events from lookbackPeriod + for { + select { + case <-l.done: + return + case t := <-l.ticker.C: + fmt.Println("Tick at", t) + } + } +} + +// Stop contract event listener for the current contract +func (l *logEventTrigger) Stop() { + l.done <- true +} diff --git a/go.mod b/go.mod index 7c2ac71a610..6d400bcd039 100644 --- a/go.mod +++ b/go.mod @@ -75,7 +75,7 @@ require ( github.com/smartcontractkit/chain-selectors v1.0.21 github.com/smartcontractkit/chainlink-automation v1.0.4 github.com/smartcontractkit/chainlink-ccip v0.0.0-20240902144105-70b5719fd098 - github.com/smartcontractkit/chainlink-common v0.2.2-0.20240903184200-6488292a85e3 + github.com/smartcontractkit/chainlink-common v0.2.2-0.20240905131601-1128f33dc70b github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240710121324-3ed288aa9b45 github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240904093355-e40169857652 github.com/smartcontractkit/chainlink-feeds v0.0.0-20240710170203-5b41615da827 diff --git a/go.sum b/go.sum index aae278aadde..e7c602bd7b1 100644 --- a/go.sum +++ b/go.sum @@ -1147,8 +1147,8 @@ github.com/smartcontractkit/chainlink-automation v1.0.4 h1:iyW181JjKHLNMnDleI8um github.com/smartcontractkit/chainlink-automation v1.0.4/go.mod h1:u4NbPZKJ5XiayfKHD/v3z3iflQWqvtdhj13jVZXj/cM= github.com/smartcontractkit/chainlink-ccip v0.0.0-20240902144105-70b5719fd098 h1:gZsXQ//TbsaD9bcvR2wOdao7AgNDIS/Uml0FEF0vJuI= github.com/smartcontractkit/chainlink-ccip v0.0.0-20240902144105-70b5719fd098/go.mod h1:Z9lQ5t20kRk28pzRLnqAJZUVOw8E6/siA3P3MLyKqoM= -github.com/smartcontractkit/chainlink-common v0.2.2-0.20240903184200-6488292a85e3 h1:fkfOoAPviqO2rN8ngvejsDa7WKcw4paGEFA4/Znu0L0= -github.com/smartcontractkit/chainlink-common v0.2.2-0.20240903184200-6488292a85e3/go.mod h1:D/qaCoq0SxXzg5NRN5FtBRv98VBf+D2NOC++RbvvuOc= +github.com/smartcontractkit/chainlink-common v0.2.2-0.20240905131601-1128f33dc70b h1:TV7fPyY/4hGaehG2XowJ8elJCg+bIB0WN8YOqT/MRc4= +github.com/smartcontractkit/chainlink-common v0.2.2-0.20240905131601-1128f33dc70b/go.mod h1:D/qaCoq0SxXzg5NRN5FtBRv98VBf+D2NOC++RbvvuOc= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240710121324-3ed288aa9b45 h1:NBQLtqk8zsyY4qTJs+NElI3aDFTcAo83JHvqD04EvB0= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240710121324-3ed288aa9b45/go.mod h1:LV0h7QBQUpoC2UUi6TcUvcIFm1xjP/DtEcqV8+qeLUs= github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240904093355-e40169857652 h1:0aZ3HiEz2bMM5ywHAyKlFMN95qTzpNDn7uvnHLrFX6s= diff --git a/plugins/cmd/capabilities/log-event-trigger/main.go b/plugins/cmd/evm-chain-capabilities/log-event-trigger/main.go similarity index 72% rename from plugins/cmd/capabilities/log-event-trigger/main.go rename to plugins/cmd/evm-chain-capabilities/log-event-trigger/main.go index 719faeded65..94b4c5dc9c0 100644 --- a/plugins/cmd/capabilities/log-event-trigger/main.go +++ b/plugins/cmd/evm-chain-capabilities/log-event-trigger/main.go @@ -2,14 +2,16 @@ package main import ( "context" + "encoding/json" "fmt" "github.com/hashicorp/go-plugin" - "github.com/smartcontractkit/chainlink/v2/core/capabilities/log_event_trigger" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/logeventtrigger" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/loop" + "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink-common/pkg/types/core" ) @@ -20,6 +22,7 @@ const ( type LogEventTriggerGRPCService struct { trigger capabilities.TriggerCapability s *loop.Server + config logeventtrigger.LogEventConfig } func main() { @@ -87,8 +90,25 @@ func (cs *LogEventTriggerGRPCService) Initialise( relayerSet core.RelayerSet, ) error { cs.s.Logger.Debugf("Initialising %s", serviceName) - cs.trigger = log_event_trigger.NewLogEventTriggerService(log_event_trigger.Params{ - Logger: cs.s.Logger, + + var logEventConfig logeventtrigger.LogEventConfig + err := json.Unmarshal([]byte(config), &logEventConfig) + if err != nil { + return fmt.Errorf("error decoding log_event_trigger config: %v", err) + } + + relayID := types.NewRelayID(logEventConfig.Network, fmt.Sprintf("%d", logEventConfig.ChainId)) + relayer, err := relayerSet.Get(ctx, relayID) + if err != nil { + return fmt.Errorf("error fetching relayer for chainID %d from relayerSet: %v", logEventConfig.ChainId, err) + } + + // Set relayer and trigger in LogEventTriggerGRPCService + cs.config = logEventConfig + cs.trigger = logeventtrigger.NewLogEventTriggerService(logeventtrigger.Params{ + Logger: cs.s.Logger, + Relayer: relayer, + LogEventConfig: logEventConfig, }) if err := capabilityRegistry.Add(ctx, cs.trigger); err != nil { From b09646b70f3a75f27d3ecec951cad76543a04e5f Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Tue, 10 Sep 2024 13:16:43 +0100 Subject: [PATCH 06/36] Refactoring to integrate with ChainReader QueryKey API --- core/capabilities/logeventtrigger/service.go | 18 ++---- core/capabilities/logeventtrigger/trigger.go | 64 +++++++++++++++----- 2 files changed, 54 insertions(+), 28 deletions(-) diff --git a/core/capabilities/logeventtrigger/service.go b/core/capabilities/logeventtrigger/service.go index ee19e2275fc..8c1eb4f291b 100644 --- a/core/capabilities/logeventtrigger/service.go +++ b/core/capabilities/logeventtrigger/service.go @@ -5,13 +5,10 @@ import ( "errors" "fmt" - "github.com/ethereum/go-ethereum/common" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/types/core" - - evmtypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" ) const ID = "log-event-trigger-%s-%d@1.0.0" @@ -24,13 +21,6 @@ var logEventTriggerInfo = capabilities.MustNewCapabilityInfo( "A trigger that listens for specific contract log events and starts a workflow run.", ) -// Log Event Trigger Capability RequestConfig -type RequestConfig struct { - ContractName string `json:"contractName"` - ContractAddress common.Address `json:"contractAddress"` - ContractReaderConfig evmtypes.ChainReaderConfig `json:"contractReaderConfig"` -} - // Log Event Trigger Capability Input type Input struct { } @@ -79,7 +69,7 @@ var _ services.Service = &LogEventTriggerService{} // Creates a new Cron Trigger Service. // Scheduling will commence on calling .Start() func NewLogEventTriggerService(p Params) *LogEventTriggerService { - l := logger.Named(p.Logger, "Log Event Trigger Capability Service") + l := logger.Named(p.Logger, "LogEventTriggerCapabilityService: ") logEventStore := NewCapabilitiesStore[logEventTrigger, capabilities.TriggerResponse]() @@ -115,9 +105,9 @@ func (s *LogEventTriggerService) RegisterTrigger(ctx context.Context, req capabi return newLogEventTrigger(ctx, reqConfig, s.logEventConfig, s.relayer) }) if err != nil { - return nil, fmt.Errorf("log_event_trigger %v", err) + return nil, fmt.Errorf("LogEventTrigger %v", err) } - s.lggr.Debugw("log_event_trigger::RegisterTrigger", "triggerId", req.TriggerID) + s.lggr.Debugw("RegisterTrigger", "triggerId", req.TriggerID) return respCh, nil } @@ -130,7 +120,7 @@ func (s *LogEventTriggerService) UnregisterTrigger(ctx context.Context, req capa close(trigger.ch) // Remove from triggers context s.triggers.Delete(req.TriggerID) - s.lggr.Debugw("log_event_trigger::UnregisterTrigger", "triggerId", req.TriggerID) + s.lggr.Debugw("UnregisterTrigger", "triggerId", req.TriggerID) return nil } diff --git a/core/capabilities/logeventtrigger/trigger.go b/core/capabilities/logeventtrigger/trigger.go index 228efc42cc4..91899026b51 100644 --- a/core/capabilities/logeventtrigger/trigger.go +++ b/core/capabilities/logeventtrigger/trigger.go @@ -8,20 +8,32 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink-common/pkg/types/core" evmtypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" ) +// Log Event Trigger Capability Request Config Details +type RequestConfig struct { + ContractName string `json:"contractName"` + ContractAddress common.Address `json:"contractAddress"` + ContractEventName string `json:"contractEventName"` + ContractReaderConfig evmtypes.ChainReaderConfig `json:"contractReaderConfig"` +} + +// LogEventTrigger struct to listen for Contract events using ContractReader gRPC client +// in a loop with a periodic delay of pollPeriod milliseconds, which is specified in +// the job spec type logEventTrigger struct { - ch chan<- capabilities.TriggerResponse + ch chan<- capabilities.TriggerResponse + lggr logger.Logger + ctx context.Context // Contract address and Event Signature to monitor for - contractName string - contractAddress common.Address - contractReaderConfig evmtypes.ChainReaderConfig - contractReader types.ContractReader + reqConfig *RequestConfig + contractReader types.ContractReader // Log Event Trigger config with pollPeriod and lookbackBlocks logEventConfig LogEventConfig @@ -54,20 +66,29 @@ func newLogEventTrigger(ctx context.Context, return nil, nil, err } + // Get current block HEAD/tip of the blockchain to start polling from + + // Setup callback channel, logger and ticker to poll ContractReader callbackCh := make(chan capabilities.TriggerResponse, defaultSendChannelBufferSize) ticker := time.NewTicker(time.Duration(logEventConfig.PollPeriod) * time.Millisecond) done := make(chan bool) + lggr, err := logger.New() + if err != nil { + return nil, nil, fmt.Errorf("could not initialise logger for LogEventTrigger") + } // Initialise a Log Event Trigger l := &logEventTrigger{ - ch: callbackCh, - contractName: reqConfig.ContractName, - contractAddress: reqConfig.ContractAddress, - contractReaderConfig: reqConfig.ContractReaderConfig, - contractReader: contractReader, - logEventConfig: logEventConfig, - ticker: ticker, - done: done, + ch: callbackCh, + lggr: logger.Named(lggr, "LogEventTrigger: "), + ctx: ctx, + + reqConfig: reqConfig, + contractReader: contractReader, + + logEventConfig: logEventConfig, + ticker: ticker, + done: done, } go l.Listen() @@ -82,7 +103,22 @@ func (l *logEventTrigger) Listen() { case <-l.done: return case t := <-l.ticker.C: - fmt.Println("Tick at", t) + l.lggr.Infof("Polling event logs at", t) + // iter, err := l.contractReader.QueryKey( + // l.ctx, + // l.reqConfig.ContractName, + // query.KeyFilter{ + // Key: l.reqConfig.ContractEventName, + // Expressions: []query.Expression{ + // query.Confidence(primitives.Finalized), + // query.Block(fmt.Sprintf("%d", l.logEventConfig.LookbackBlocks), primitives.Gte), + // }, + // }, + // query.LimitAndSort{ + // SortBy: []query.SortBy{query.NewSortByTimestamp(query.Asc)}, + // }, + // &ev, + // ) } } } From 4e49c39a3c3e06ed70caf95bccdf5cfbc23d092f Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Tue, 17 Sep 2024 12:27:38 +0100 Subject: [PATCH 07/36] Integrate with ChainReader QueryKey interface --- .../{ => triggers}/logeventtrigger/service.go | 1 + .../{ => triggers}/logeventtrigger/store.go | 0 .../{ => triggers}/logeventtrigger/trigger.go | 78 +++++++++++++++---- go.mod | 2 +- go.sum | 2 + .../log-event-trigger/main.go | 2 +- 6 files changed, 68 insertions(+), 17 deletions(-) rename core/capabilities/{ => triggers}/logeventtrigger/service.go (99%) rename core/capabilities/{ => triggers}/logeventtrigger/store.go (100%) rename core/capabilities/{ => triggers}/logeventtrigger/trigger.go (63%) rename plugins/cmd/{evm-chain-capabilities => capabilities}/log-event-trigger/main.go (97%) diff --git a/core/capabilities/logeventtrigger/service.go b/core/capabilities/triggers/logeventtrigger/service.go similarity index 99% rename from core/capabilities/logeventtrigger/service.go rename to core/capabilities/triggers/logeventtrigger/service.go index 8c1eb4f291b..a1fa3317849 100644 --- a/core/capabilities/logeventtrigger/service.go +++ b/core/capabilities/triggers/logeventtrigger/service.go @@ -60,6 +60,7 @@ type LogEventConfig struct { type Params struct { Logger logger.Logger Relayer core.Relayer + RelayerSet core.RelayerSet LogEventConfig LogEventConfig } diff --git a/core/capabilities/logeventtrigger/store.go b/core/capabilities/triggers/logeventtrigger/store.go similarity index 100% rename from core/capabilities/logeventtrigger/store.go rename to core/capabilities/triggers/logeventtrigger/store.go diff --git a/core/capabilities/logeventtrigger/trigger.go b/core/capabilities/triggers/logeventtrigger/trigger.go similarity index 63% rename from core/capabilities/logeventtrigger/trigger.go rename to core/capabilities/triggers/logeventtrigger/trigger.go index 91899026b51..dd5f47d649e 100644 --- a/core/capabilities/logeventtrigger/trigger.go +++ b/core/capabilities/triggers/logeventtrigger/trigger.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "strconv" "time" "github.com/ethereum/go-ethereum/common" @@ -11,6 +12,9 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink-common/pkg/types/core" + "github.com/smartcontractkit/chainlink-common/pkg/types/query" + "github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives" + "github.com/smartcontractkit/chainlink-common/pkg/values" evmtypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" ) @@ -34,6 +38,9 @@ type logEventTrigger struct { // Contract address and Event Signature to monitor for reqConfig *RequestConfig contractReader types.ContractReader + relayer core.Relayer + callbackCh chan capabilities.TriggerResponse + startBlockNum uint64 // Log Event Trigger config with pollPeriod and lookbackBlocks logEventConfig LogEventConfig @@ -67,6 +74,14 @@ func newLogEventTrigger(ctx context.Context, } // Get current block HEAD/tip of the blockchain to start polling from + latestHead, err := relayer.LatestHead(ctx) + if err != nil { + return nil, nil, fmt.Errorf("error getting latestHead from relayer client: %v", err) + } + height, err := strconv.ParseUint(latestHead.Height, 10, 64) + if err != nil { + return nil, nil, fmt.Errorf("invalid height in latestHead from relayer client: %v", err) + } // Setup callback channel, logger and ticker to poll ContractReader callbackCh := make(chan capabilities.TriggerResponse, defaultSendChannelBufferSize) @@ -85,6 +100,9 @@ func newLogEventTrigger(ctx context.Context, reqConfig: reqConfig, contractReader: contractReader, + relayer: relayer, + callbackCh: callbackCh, + startBlockNum: height, logEventConfig: logEventConfig, ticker: ticker, @@ -98,29 +116,59 @@ func newLogEventTrigger(ctx context.Context, // Listen to contract events and trigger workflow runs func (l *logEventTrigger) Listen() { // Listen for events from lookbackPeriod + var logs []types.Sequence + var err error + logData := make(map[string]any) for { select { case <-l.done: return case t := <-l.ticker.C: l.lggr.Infof("Polling event logs at", t) - // iter, err := l.contractReader.QueryKey( - // l.ctx, - // l.reqConfig.ContractName, - // query.KeyFilter{ - // Key: l.reqConfig.ContractEventName, - // Expressions: []query.Expression{ - // query.Confidence(primitives.Finalized), - // query.Block(fmt.Sprintf("%d", l.logEventConfig.LookbackBlocks), primitives.Gte), - // }, - // }, - // query.LimitAndSort{ - // SortBy: []query.SortBy{query.NewSortByTimestamp(query.Asc)}, - // }, - // &ev, - // ) + logs, err = l.contractReader.QueryKey( + l.ctx, + types.BoundContract{Name: l.reqConfig.ContractName, Address: l.reqConfig.ContractAddress.Hex()}, + query.KeyFilter{ + Key: l.reqConfig.ContractEventName, + Expressions: []query.Expression{ + query.Confidence(primitives.Finalized), + query.Block(fmt.Sprintf("%d", l.startBlockNum-l.logEventConfig.LookbackBlocks), primitives.Gte), + }, + }, + query.LimitAndSort{ + SortBy: []query.SortBy{query.NewSortByTimestamp(query.Asc)}, + }, + logData, + ) + if err != nil { + l.lggr.Fatalw("QueryKey failure", "err", err) + continue + } + for _, log := range logs { + triggerResp := createTriggerResponse(log) + go func(resp capabilities.TriggerResponse) { + l.callbackCh <- resp + }(triggerResp) + } + } + } +} + +// Create log event trigger capability response +func createTriggerResponse(log types.Sequence) capabilities.TriggerResponse { + wrappedPayload, err := values.WrapMap(log.Data) + if err != nil { + return capabilities.TriggerResponse{ + Err: fmt.Errorf("error wrapping trigger event: %s", err), } } + return capabilities.TriggerResponse{ + Event: capabilities.TriggerEvent{ + TriggerType: ID, + ID: log.Cursor, + Outputs: wrappedPayload, + }, + } } // Stop contract event listener for the current contract diff --git a/go.mod b/go.mod index 6bc724ecd8b..c161a7a63d7 100644 --- a/go.mod +++ b/go.mod @@ -75,7 +75,7 @@ require ( github.com/smartcontractkit/chain-selectors v1.0.23 github.com/smartcontractkit/chainlink-automation v1.0.4 github.com/smartcontractkit/chainlink-ccip v0.0.0-20240916105522-d6e26aedf629 - github.com/smartcontractkit/chainlink-common v0.2.2-0.20240913161926-ce5d667907ce + github.com/smartcontractkit/chainlink-common v0.2.2-0.20240917090032-47eac983684d github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240911175228-daf2600bb7b7 github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240906125718-9f0a98d32fbc github.com/smartcontractkit/chainlink-feeds v0.0.0-20240910155501-42f20443189f diff --git a/go.sum b/go.sum index b58bd4c9b73..f135e8c1f21 100644 --- a/go.sum +++ b/go.sum @@ -1046,6 +1046,8 @@ github.com/smartcontractkit/chainlink-ccip v0.0.0-20240916105522-d6e26aedf629 h1 github.com/smartcontractkit/chainlink-ccip v0.0.0-20240916105522-d6e26aedf629/go.mod h1:X1f4CKlR1RilSgzArQv5HNvMrVSt+Zloihm3REwxhdQ= github.com/smartcontractkit/chainlink-common v0.2.2-0.20240913161926-ce5d667907ce h1:qXS0aWiDFDoLRCB+kSGnzp77iYT2luflUyzE5BnNmpY= github.com/smartcontractkit/chainlink-common v0.2.2-0.20240913161926-ce5d667907ce/go.mod h1:sjiiPwd4KsYOCf68MwL86EKphdXeT66EY7j53WH5DCc= +github.com/smartcontractkit/chainlink-common v0.2.2-0.20240917090032-47eac983684d h1:EoxwIM9/OyCcH7Lb+SL2WY+9zxmcNJD1YNZuYxw70Zg= +github.com/smartcontractkit/chainlink-common v0.2.2-0.20240917090032-47eac983684d/go.mod h1:l8NTByXUdGGJX+vyKYI6yX1/HIpM14F8Wm9BkU3Q4Qo= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240911175228-daf2600bb7b7 h1:lTGIOQYLk1Ufn++X/AvZnt6VOcuhste5yp+C157No/Q= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240911175228-daf2600bb7b7/go.mod h1:BMYE1vC/pGmdFSsOJdPrAA0/4gZ0Xo0SxTMdGspBtRo= github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240906125718-9f0a98d32fbc h1:tRmTlaoAt+7FakMXXgeCuRPmzzBo5jsGpeCVvcU6KMc= diff --git a/plugins/cmd/evm-chain-capabilities/log-event-trigger/main.go b/plugins/cmd/capabilities/log-event-trigger/main.go similarity index 97% rename from plugins/cmd/evm-chain-capabilities/log-event-trigger/main.go rename to plugins/cmd/capabilities/log-event-trigger/main.go index 94b4c5dc9c0..50c9bf96a5b 100644 --- a/plugins/cmd/evm-chain-capabilities/log-event-trigger/main.go +++ b/plugins/cmd/capabilities/log-event-trigger/main.go @@ -7,7 +7,7 @@ import ( "github.com/hashicorp/go-plugin" - "github.com/smartcontractkit/chainlink/v2/core/capabilities/logeventtrigger" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/triggers/logeventtrigger" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/loop" From d89020e5eca0ea3ed59186d0bda415d5c71f8562 Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Tue, 17 Sep 2024 12:58:36 +0100 Subject: [PATCH 08/36] Minor changes --- .../triggers/{logeventtrigger => logevent}/service.go | 6 +++--- .../triggers/{logeventtrigger => logevent}/store.go | 2 +- .../triggers/{logeventtrigger => logevent}/trigger.go | 9 ++++----- plugins/cmd/capabilities/log-event-trigger/main.go | 8 ++++---- 4 files changed, 12 insertions(+), 13 deletions(-) rename core/capabilities/triggers/{logeventtrigger => logevent}/service.go (97%) rename core/capabilities/triggers/{logeventtrigger => logevent}/store.go (98%) rename core/capabilities/triggers/{logeventtrigger => logevent}/trigger.go (96%) diff --git a/core/capabilities/triggers/logeventtrigger/service.go b/core/capabilities/triggers/logevent/service.go similarity index 97% rename from core/capabilities/triggers/logeventtrigger/service.go rename to core/capabilities/triggers/logevent/service.go index a1fa3317849..5a7ab332b51 100644 --- a/core/capabilities/triggers/logeventtrigger/service.go +++ b/core/capabilities/triggers/logevent/service.go @@ -1,4 +1,4 @@ -package logeventtrigger +package logevent import ( "context" @@ -117,8 +117,8 @@ func (s *LogEventTriggerService) UnregisterTrigger(ctx context.Context, req capa if !ok { return fmt.Errorf("triggerId %s not found", req.TriggerID) } - // Close callback channel - close(trigger.ch) + // Close callback channel and stop log event trigger listener + trigger.Stop() // Remove from triggers context s.triggers.Delete(req.TriggerID) s.lggr.Debugw("UnregisterTrigger", "triggerId", req.TriggerID) diff --git a/core/capabilities/triggers/logeventtrigger/store.go b/core/capabilities/triggers/logevent/store.go similarity index 98% rename from core/capabilities/triggers/logeventtrigger/store.go rename to core/capabilities/triggers/logevent/store.go index d9c9234aaf7..0f8cebfab02 100644 --- a/core/capabilities/triggers/logeventtrigger/store.go +++ b/core/capabilities/triggers/logevent/store.go @@ -1,4 +1,4 @@ -package logeventtrigger +package logevent import ( "fmt" diff --git a/core/capabilities/triggers/logeventtrigger/trigger.go b/core/capabilities/triggers/logevent/trigger.go similarity index 96% rename from core/capabilities/triggers/logeventtrigger/trigger.go rename to core/capabilities/triggers/logevent/trigger.go index dd5f47d649e..37abe35d17a 100644 --- a/core/capabilities/triggers/logeventtrigger/trigger.go +++ b/core/capabilities/triggers/logevent/trigger.go @@ -1,4 +1,4 @@ -package logeventtrigger +package logevent import ( "context" @@ -39,7 +39,6 @@ type logEventTrigger struct { reqConfig *RequestConfig contractReader types.ContractReader relayer core.Relayer - callbackCh chan capabilities.TriggerResponse startBlockNum uint64 // Log Event Trigger config with pollPeriod and lookbackBlocks @@ -101,7 +100,6 @@ func newLogEventTrigger(ctx context.Context, reqConfig: reqConfig, contractReader: contractReader, relayer: relayer, - callbackCh: callbackCh, startBlockNum: height, logEventConfig: logEventConfig, @@ -124,7 +122,7 @@ func (l *logEventTrigger) Listen() { case <-l.done: return case t := <-l.ticker.C: - l.lggr.Infof("Polling event logs at", t) + l.lggr.Infof("Polling event logs from ContractReader using QueryKey at", t) logs, err = l.contractReader.QueryKey( l.ctx, types.BoundContract{Name: l.reqConfig.ContractName, Address: l.reqConfig.ContractAddress.Hex()}, @@ -147,7 +145,7 @@ func (l *logEventTrigger) Listen() { for _, log := range logs { triggerResp := createTriggerResponse(log) go func(resp capabilities.TriggerResponse) { - l.callbackCh <- resp + l.ch <- resp }(triggerResp) } } @@ -173,5 +171,6 @@ func createTriggerResponse(log types.Sequence) capabilities.TriggerResponse { // Stop contract event listener for the current contract func (l *logEventTrigger) Stop() { + close(l.ch) l.done <- true } diff --git a/plugins/cmd/capabilities/log-event-trigger/main.go b/plugins/cmd/capabilities/log-event-trigger/main.go index 50c9bf96a5b..66d1a2cbe4a 100644 --- a/plugins/cmd/capabilities/log-event-trigger/main.go +++ b/plugins/cmd/capabilities/log-event-trigger/main.go @@ -7,7 +7,7 @@ import ( "github.com/hashicorp/go-plugin" - "github.com/smartcontractkit/chainlink/v2/core/capabilities/triggers/logeventtrigger" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/triggers/logevent" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/loop" @@ -22,7 +22,7 @@ const ( type LogEventTriggerGRPCService struct { trigger capabilities.TriggerCapability s *loop.Server - config logeventtrigger.LogEventConfig + config logevent.LogEventConfig } func main() { @@ -91,7 +91,7 @@ func (cs *LogEventTriggerGRPCService) Initialise( ) error { cs.s.Logger.Debugf("Initialising %s", serviceName) - var logEventConfig logeventtrigger.LogEventConfig + var logEventConfig logevent.LogEventConfig err := json.Unmarshal([]byte(config), &logEventConfig) if err != nil { return fmt.Errorf("error decoding log_event_trigger config: %v", err) @@ -105,7 +105,7 @@ func (cs *LogEventTriggerGRPCService) Initialise( // Set relayer and trigger in LogEventTriggerGRPCService cs.config = logEventConfig - cs.trigger = logeventtrigger.NewLogEventTriggerService(logeventtrigger.Params{ + cs.trigger = logevent.NewLogEventTriggerService(logevent.Params{ Logger: cs.s.Logger, Relayer: relayer, LogEventConfig: logEventConfig, From 2967430dafdb24a20ec614f5a6edaac940f466d9 Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Wed, 18 Sep 2024 16:17:47 +0100 Subject: [PATCH 09/36] Send cursor in QueryKey in subsequent calls --- core/capabilities/triggers/logevent/trigger.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/core/capabilities/triggers/logevent/trigger.go b/core/capabilities/triggers/logevent/trigger.go index 37abe35d17a..37453a96ae8 100644 --- a/core/capabilities/triggers/logevent/trigger.go +++ b/core/capabilities/triggers/logevent/trigger.go @@ -117,6 +117,9 @@ func (l *logEventTrigger) Listen() { var logs []types.Sequence var err error logData := make(map[string]any) + limitAndSort := query.LimitAndSort{ + SortBy: []query.SortBy{query.NewSortByTimestamp(query.Asc)}, + } for { select { case <-l.done: @@ -133,9 +136,7 @@ func (l *logEventTrigger) Listen() { query.Block(fmt.Sprintf("%d", l.startBlockNum-l.logEventConfig.LookbackBlocks), primitives.Gte), }, }, - query.LimitAndSort{ - SortBy: []query.SortBy{query.NewSortByTimestamp(query.Asc)}, - }, + limitAndSort, logData, ) if err != nil { @@ -147,6 +148,7 @@ func (l *logEventTrigger) Listen() { go func(resp capabilities.TriggerResponse) { l.ch <- resp }(triggerResp) + limitAndSort.Limit = query.Limit{Cursor: log.Cursor} } } } From 75904af14be89ae4de7863f54536bdf9af1e897d Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Wed, 25 Sep 2024 18:05:54 +0100 Subject: [PATCH 10/36] Test utils for LOOP capability --- core/capabilities/testutils/test_harness.go | 131 ++++++++++++++++++ .../triggers/logevent/trigger_test.go | 1 + 2 files changed, 132 insertions(+) create mode 100644 core/capabilities/testutils/test_harness.go create mode 100644 core/capabilities/triggers/logevent/trigger_test.go diff --git a/core/capabilities/testutils/test_harness.go b/core/capabilities/testutils/test_harness.go new file mode 100644 index 00000000000..e50b989cf33 --- /dev/null +++ b/core/capabilities/testutils/test_harness.go @@ -0,0 +1,131 @@ +package testutils + +import ( + "context" + "encoding/json" + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/eth/ethconfig" + "github.com/test-go/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/headtracker" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated/log_emitter" + "github.com/smartcontractkit/chainlink/v2/core/internal/cltest" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ethkey" + "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" + evmrelaytypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" +) + +// Test harness for handling a LOOP Capability functionality +type EVMLoopCapabilityTH struct { + // Backend details + Lggr logger.Logger + ChainID *big.Int + Backend *backends.SimulatedBackend + EVMClient evmclient.Client + + ContractsOwner *bind.TransactOpts + ContractsOwnerKey ethkey.KeyV2 + + LogEmitterAddress *common.Address + LogEmitterContract *log_emitter.LogEmitter + + HeadTracker logpoller.HeadTracker + LogPoller logpoller.LogPoller +} + +// Test harness to create a simulated backend for testing a LOOPCapability +func NewEVMLoopCapabilityTH(t *testing.T) *EVMLoopCapabilityTH { + lggr, _ := logger.NewLogger() + + ownerKey := cltest.MustGenerateRandomKey(t) + contractsOwner, err := bind.NewKeyedTransactorWithChainID(ownerKey.ToEcdsaPrivKey(), testutils.SimulatedChainID) + require.NoError(t, err) + + // Setup simulated go-ethereum EVM backend + genesisData := core.GenesisAlloc{ + contractsOwner.From: {Balance: assets.Ether(100000).ToInt()}, + } + chainID := testutils.SimulatedChainID + gasLimit := uint32(ethconfig.Defaults.Miner.GasCeil) + backend := cltest.NewSimulatedBackend(t, genesisData, gasLimit) + blockTime := time.UnixMilli(int64(backend.Blockchain().CurrentHeader().Time)) + err = backend.AdjustTime(time.Since(blockTime) - 24*time.Hour) + require.NoError(t, err) + backend.Commit() + + // Setup backend client + client := evmclient.NewSimulatedBackendClient(t, backend, chainID) + + // Deploy necessary contracts + // Deploy LogEmitter + logEmitterAddress, _, _, err := + log_emitter.DeployLogEmitter(contractsOwner, backend) + require.NoError(t, err) + logEmitter, err := log_emitter.NewLogEmitter(logEmitterAddress, backend) + require.NoError(t, err) + + th := &EVMLoopCapabilityTH{ + Lggr: lggr, + ChainID: chainID, + Backend: backend, + EVMClient: client, + + ContractsOwner: contractsOwner, + ContractsOwnerKey: ownerKey, + + LogEmitterAddress: &logEmitterAddress, + LogEmitterContract: logEmitter, + } + th.HeadTracker, th.LogPoller = th.SetupCoreServices(t) + + return th +} + +// Setup core services like log poller and head tracker for the simulated backend +func (th *EVMLoopCapabilityTH) SetupCoreServices(t *testing.T) (logpoller.HeadTracker, logpoller.LogPoller) { + db := pgtest.NewSqlxDB(t) + const finalityDepth = 2 + ht := headtracker.NewSimulatedHeadTracker(th.EVMClient, false, finalityDepth) + lp := logpoller.NewLogPoller( + logpoller.NewORM(testutils.SimulatedChainID, db, th.Lggr), + th.EVMClient, + th.Lggr, + ht, + logpoller.Opts{ + PollPeriod: 100 * time.Millisecond, + FinalityDepth: finalityDepth, + BackfillBatchSize: 3, + RpcBatchSize: 2, + KeepFinalizedBlocksDepth: 1000, + }, + ) + return ht, lp +} + +func (th *EVMLoopCapabilityTH) SetupContractReader(t *testing.T, ctx context.Context, cfg []byte) (types.ContractReader, error) { + crCfg := &evmrelaytypes.ChainReaderConfig{} + if err := json.Unmarshal(cfg, crCfg); err != nil { + return nil, err + } + + svc, err := evm.NewChainReaderService(ctx, th.Lggr, th.LogPoller, th.HeadTracker, th.EVMClient, *crCfg) + if err != nil { + return nil, err + } + + return svc, svc.Start(ctx) +} diff --git a/core/capabilities/triggers/logevent/trigger_test.go b/core/capabilities/triggers/logevent/trigger_test.go new file mode 100644 index 00000000000..6a1e83a4b99 --- /dev/null +++ b/core/capabilities/triggers/logevent/trigger_test.go @@ -0,0 +1 @@ +package logevent_test From c26d024926516e3180de84cdaa81a776304ca40c Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Thu, 26 Sep 2024 13:26:21 +0100 Subject: [PATCH 11/36] Happy path test for log event trigger capability --- core/capabilities/testutils/test_harness.go | 10 +- .../capabilities/triggers/logevent/service.go | 12 +- .../triggers/logevent/service_test.go | 133 ++++++++++++++++++ .../capabilities/triggers/logevent/trigger.go | 33 +++-- .../triggers/logevent/trigger_test.go | 1 - 5 files changed, 161 insertions(+), 28 deletions(-) create mode 100644 core/capabilities/triggers/logevent/service_test.go delete mode 100644 core/capabilities/triggers/logevent/trigger_test.go diff --git a/core/capabilities/testutils/test_harness.go b/core/capabilities/testutils/test_harness.go index e50b989cf33..9ce03eb71a8 100644 --- a/core/capabilities/testutils/test_harness.go +++ b/core/capabilities/testutils/test_harness.go @@ -30,7 +30,7 @@ import ( ) // Test harness for handling a LOOP Capability functionality -type EVMLoopCapabilityTH struct { +type EVMLOOPCapabilityTH struct { // Backend details Lggr logger.Logger ChainID *big.Int @@ -48,7 +48,7 @@ type EVMLoopCapabilityTH struct { } // Test harness to create a simulated backend for testing a LOOPCapability -func NewEVMLoopCapabilityTH(t *testing.T) *EVMLoopCapabilityTH { +func NewEVMLOOPCapabilityTH(t *testing.T) *EVMLOOPCapabilityTH { lggr, _ := logger.NewLogger() ownerKey := cltest.MustGenerateRandomKey(t) @@ -78,7 +78,7 @@ func NewEVMLoopCapabilityTH(t *testing.T) *EVMLoopCapabilityTH { logEmitter, err := log_emitter.NewLogEmitter(logEmitterAddress, backend) require.NoError(t, err) - th := &EVMLoopCapabilityTH{ + th := &EVMLOOPCapabilityTH{ Lggr: lggr, ChainID: chainID, Backend: backend, @@ -96,7 +96,7 @@ func NewEVMLoopCapabilityTH(t *testing.T) *EVMLoopCapabilityTH { } // Setup core services like log poller and head tracker for the simulated backend -func (th *EVMLoopCapabilityTH) SetupCoreServices(t *testing.T) (logpoller.HeadTracker, logpoller.LogPoller) { +func (th *EVMLOOPCapabilityTH) SetupCoreServices(t *testing.T) (logpoller.HeadTracker, logpoller.LogPoller) { db := pgtest.NewSqlxDB(t) const finalityDepth = 2 ht := headtracker.NewSimulatedHeadTracker(th.EVMClient, false, finalityDepth) @@ -116,7 +116,7 @@ func (th *EVMLoopCapabilityTH) SetupCoreServices(t *testing.T) (logpoller.HeadTr return ht, lp } -func (th *EVMLoopCapabilityTH) SetupContractReader(t *testing.T, ctx context.Context, cfg []byte) (types.ContractReader, error) { +func (th *EVMLOOPCapabilityTH) NewContractReader(t *testing.T, ctx context.Context, cfg []byte) (types.ContractReader, error) { crCfg := &evmrelaytypes.ChainReaderConfig{} if err := json.Unmarshal(cfg, crCfg); err != nil { return nil, err diff --git a/core/capabilities/triggers/logevent/service.go b/core/capabilities/triggers/logevent/service.go index 5a7ab332b51..ac7b2a78502 100644 --- a/core/capabilities/triggers/logevent/service.go +++ b/core/capabilities/triggers/logevent/service.go @@ -15,12 +15,6 @@ const ID = "log-event-trigger-%s-%d@1.0.0" const defaultSendChannelBufferSize = 1000 -var logEventTriggerInfo = capabilities.MustNewCapabilityInfo( - ID, - capabilities.CapabilityTypeTrigger, - "A trigger that listens for specific contract log events and starts a workflow run.", -) - // Log Event Trigger Capability Input type Input struct { } @@ -74,13 +68,15 @@ func NewLogEventTriggerService(p Params) *LogEventTriggerService { logEventStore := NewCapabilitiesStore[logEventTrigger, capabilities.TriggerResponse]() - return &LogEventTriggerService{ - CapabilityInfo: logEventTriggerInfo, + s := &LogEventTriggerService{ lggr: l, triggers: logEventStore, relayer: p.Relayer, logEventConfig: p.LogEventConfig, } + s.CapabilityInfo, _ = s.Info(context.Background()) + s.Validator = capabilities.NewValidator[RequestConfig, Input, capabilities.TriggerResponse](capabilities.ValidatorArgs{Info: s.CapabilityInfo}) + return s } func (s *LogEventTriggerService) Info(ctx context.Context) (capabilities.CapabilityInfo, error) { diff --git a/core/capabilities/triggers/logevent/service_test.go b/core/capabilities/triggers/logevent/service_test.go new file mode 100644 index 00000000000..53871ef4b29 --- /dev/null +++ b/core/capabilities/triggers/logevent/service_test.go @@ -0,0 +1,133 @@ +package logevent_test + +import ( + "encoding/json" + "fmt" + "math/big" + "testing" + "time" + + commoncaps "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" + commonmocks "github.com/smartcontractkit/chainlink-common/pkg/types/core/mocks" + commonvalues "github.com/smartcontractkit/chainlink-common/pkg/values" + "github.com/test-go/testify/mock" + "github.com/test-go/testify/require" + + "github.com/smartcontractkit/chainlink/v2/core/capabilities/testutils" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/triggers/logevent" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated/log_emitter" + coretestutils "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" +) + +// Test for Log Event Trigger Capability happy path for EVM +func TestLogEventTriggerEVMHappyPath(t *testing.T) { + th := testutils.NewEVMLOOPCapabilityTH(t) + logEventConfig := logevent.LogEventConfig{ + ChainId: th.ChainID.Uint64(), + Network: "evm", + LookbackBlocks: 1000, + PollPeriod: 500, + } + + // Create new contract reader + reqConfig := logevent.RequestConfig{ + ContractName: "LogEmitter", + ContractAddress: th.LogEmitterAddress.Hex(), + ContractEventName: "Log1", + } + contractReaderCfg := evmtypes.ChainReaderConfig{ + Contracts: map[string]evmtypes.ChainContractReader{ + reqConfig.ContractName: { + ContractPollingFilter: evmtypes.ContractPollingFilter{ + GenericEventNames: []string{reqConfig.ContractEventName}, + }, + ContractABI: log_emitter.LogEmitterABI, + Configs: map[string]*evmtypes.ChainReaderDefinition{ + reqConfig.ContractEventName: { + ChainSpecificName: reqConfig.ContractEventName, + ReadType: evmtypes.Event, + }, + }, + }, + }, + } + + // Encode contractReaderConfig as JSON and decode it into a map[string]any for + // the capability request config. Log Event Trigger capability takes in a + // []byte as ContractReaderConfig to not depend on evm ChainReaderConfig type + // and be chain agnostic + contractReaderCfgBytes, err := json.Marshal(contractReaderCfg) + require.NoError(t, err) + contractReaderCfgMap := make(map[string]any) + err = json.Unmarshal(contractReaderCfgBytes, &contractReaderCfgMap) + require.NoError(t, err) + // Encode the config map as JSON to specify in the expected call in mocked object + // The LogEventTrigger Capability receives a config map, encodes it and + // calls NewContractReader with it + contractReaderCfgBytes, _ = json.Marshal(contractReaderCfgMap) + + reqConfig.ContractReaderConfig = contractReaderCfgMap + + config, err := commonvalues.WrapMap(reqConfig) + require.NoError(t, err) + req := commoncaps.TriggerRegistrationRequest{ + TriggerID: "logeventtrigger_log1", + Config: config, + Metadata: commoncaps.RequestMetadata{ + ReferenceID: "logeventtrigger", + }, + } + + // Create a new contract reader to return from mock relayer + ctx := coretestutils.Context(t) + contractReader, err := th.NewContractReader(t, ctx, contractReaderCfgBytes) + require.NoError(t, err) + + // Fetch latest head from simulated backend to return from mock relayer + height, err := th.EVMClient.LatestBlockHeight(ctx) + require.NoError(t, err) + block, err := th.EVMClient.BlockByNumber(ctx, height) + require.NoError(t, err) + + // Mock relayer to return a New ContractReader instead of gRPC client of a ContractReader + relayer := commonmocks.NewRelayer(t) + relayer.On("NewContractReader", mock.Anything, contractReaderCfgBytes).Return(contractReader, nil).Once() + relayer.On("LatestHead", mock.Anything).Return(commontypes.Head{ + Height: height.String(), + Hash: block.Hash().Bytes(), + Timestamp: block.Time(), + }, nil).Once() + + // Create Log Event Trigger Service and register trigger + logEventTriggerService := logevent.NewLogEventTriggerService(logevent.Params{ + Logger: th.Lggr, + Relayer: relayer, + LogEventConfig: logEventConfig, + }) + log1Ch, err := logEventTriggerService.RegisterTrigger(ctx, req) + require.NoError(t, err) + + // Send a blockchain transaction that emits logs + go func() { + _, err := + th.LogEmitterContract.EmitLog1(th.ContractsOwner, []*big.Int{big.NewInt(10)}) + require.NoError(t, err) + th.Backend.Commit() + th.Backend.Commit() + th.Backend.Commit() + }() + + // Wait for logs with a timeout + timeout := 5 * time.Second + for { + select { + case <-time.After(timeout): + require.NoError(t, fmt.Errorf("Timeout waiting for Log1 event from ContractReader")) + case log1 := <-log1Ch: + require.NoError(t, log1.Err, "error listening for Log1 event from ContractReader") + require.NotNil(t, log1.Event.Outputs) + } + } +} diff --git a/core/capabilities/triggers/logevent/trigger.go b/core/capabilities/triggers/logevent/trigger.go index 37453a96ae8..e30c183c61d 100644 --- a/core/capabilities/triggers/logevent/trigger.go +++ b/core/capabilities/triggers/logevent/trigger.go @@ -7,7 +7,6 @@ import ( "strconv" "time" - "github.com/ethereum/go-ethereum/common" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/types" @@ -15,16 +14,16 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/types/query" "github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives" "github.com/smartcontractkit/chainlink-common/pkg/values" - - evmtypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" ) // Log Event Trigger Capability Request Config Details type RequestConfig struct { - ContractName string `json:"contractName"` - ContractAddress common.Address `json:"contractAddress"` - ContractEventName string `json:"contractEventName"` - ContractReaderConfig evmtypes.ChainReaderConfig `json:"contractReaderConfig"` + ContractName string `json:"contractName"` + ContractAddress string `json:"contractAddress"` + ContractEventName string `json:"contractEventName"` + // Log Event Trigger capability takes in a []byte as ContractReaderConfig + // to not depend on evm ChainReaderConfig type and be chain agnostic + ContractReaderConfig map[string]any `json:"contractReaderConfig"` } // LogEventTrigger struct to listen for Contract events using ContractReader gRPC client @@ -66,7 +65,7 @@ func newLogEventTrigger(ctx context.Context, } // Bind Contract in ContractReader - boundContracts := []types.BoundContract{{Name: reqConfig.ContractName, Address: reqConfig.ContractAddress.Hex()}} + boundContracts := []types.BoundContract{{Name: reqConfig.ContractName, Address: reqConfig.ContractAddress}} err = contractReader.Bind(ctx, boundContracts) if err != nil { return nil, nil, err @@ -81,6 +80,10 @@ func newLogEventTrigger(ctx context.Context, if err != nil { return nil, nil, fmt.Errorf("invalid height in latestHead from relayer client: %v", err) } + startBlockNum := uint64(0) + if height > logEventConfig.LookbackBlocks { + startBlockNum = height - logEventConfig.LookbackBlocks + } // Setup callback channel, logger and ticker to poll ContractReader callbackCh := make(chan capabilities.TriggerResponse, defaultSendChannelBufferSize) @@ -100,7 +103,7 @@ func newLogEventTrigger(ctx context.Context, reqConfig: reqConfig, contractReader: contractReader, relayer: relayer, - startBlockNum: height, + startBlockNum: startBlockNum, logEventConfig: logEventConfig, ticker: ticker, @@ -125,15 +128,17 @@ func (l *logEventTrigger) Listen() { case <-l.done: return case t := <-l.ticker.C: - l.lggr.Infof("Polling event logs from ContractReader using QueryKey at", t) + l.lggr.Infof("Polling event logs from ContractReader using QueryKey at", t, + "startBlockNum", l.startBlockNum, + "limit", limitAndSort.Limit) logs, err = l.contractReader.QueryKey( l.ctx, - types.BoundContract{Name: l.reqConfig.ContractName, Address: l.reqConfig.ContractAddress.Hex()}, + types.BoundContract{Name: l.reqConfig.ContractName, Address: l.reqConfig.ContractAddress}, query.KeyFilter{ Key: l.reqConfig.ContractEventName, Expressions: []query.Expression{ - query.Confidence(primitives.Finalized), - query.Block(fmt.Sprintf("%d", l.startBlockNum-l.logEventConfig.LookbackBlocks), primitives.Gte), + query.Confidence(primitives.Unconfirmed), + query.Block(fmt.Sprintf("%d", l.startBlockNum), primitives.Gte), }, }, limitAndSort, @@ -156,7 +161,7 @@ func (l *logEventTrigger) Listen() { // Create log event trigger capability response func createTriggerResponse(log types.Sequence) capabilities.TriggerResponse { - wrappedPayload, err := values.WrapMap(log.Data) + wrappedPayload, err := values.WrapMap(log) if err != nil { return capabilities.TriggerResponse{ Err: fmt.Errorf("error wrapping trigger event: %s", err), diff --git a/core/capabilities/triggers/logevent/trigger_test.go b/core/capabilities/triggers/logevent/trigger_test.go deleted file mode 100644 index 6a1e83a4b99..00000000000 --- a/core/capabilities/triggers/logevent/trigger_test.go +++ /dev/null @@ -1 +0,0 @@ -package logevent_test From 3880a5c47fd429d7e6a4ebc6438ca8eb7e4154ea Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Thu, 26 Sep 2024 13:26:43 +0100 Subject: [PATCH 12/36] Float64 fix in values --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 6c796344ae8..fb19872e559 100644 --- a/go.mod +++ b/go.mod @@ -76,7 +76,7 @@ require ( github.com/smartcontractkit/chain-selectors v1.0.23 github.com/smartcontractkit/chainlink-automation v1.0.4 github.com/smartcontractkit/chainlink-ccip v0.0.0-20240924115754-8858b0423283 - github.com/smartcontractkit/chainlink-common v0.2.3-0.20240925085218-aded1b263ecc + github.com/smartcontractkit/chainlink-common v0.2.3-0.20240926104717-2b9284714c16 github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240911175228-daf2600bb7b7 github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240916152957-433914114bd2 github.com/smartcontractkit/chainlink-feeds v0.0.0-20240910155501-42f20443189f diff --git a/go.sum b/go.sum index 4b270409c3e..734fabf1166 100644 --- a/go.sum +++ b/go.sum @@ -1050,6 +1050,8 @@ github.com/smartcontractkit/chainlink-ccip v0.0.0-20240924115754-8858b0423283 h1 github.com/smartcontractkit/chainlink-ccip v0.0.0-20240924115754-8858b0423283/go.mod h1:KP82vFCqm+M1G1t6Vos5CewGUGYJkxxCEdxnta4uLlE= github.com/smartcontractkit/chainlink-common v0.2.3-0.20240925085218-aded1b263ecc h1:ALbyaoRzUSXQ2NhGFKVOyJqO22IB5yQjhjKWbIZGbrI= github.com/smartcontractkit/chainlink-common v0.2.3-0.20240925085218-aded1b263ecc/go.mod h1:F6WUS6N4mP5ScwpwyTyAJc9/vjR+GXbMCRUOVekQi1g= +github.com/smartcontractkit/chainlink-common v0.2.3-0.20240926104717-2b9284714c16 h1:YJPXa8QAIjRJSL3HXr3mOCDbbh1CpTPKGztqy2Odzps= +github.com/smartcontractkit/chainlink-common v0.2.3-0.20240926104717-2b9284714c16/go.mod h1:F6WUS6N4mP5ScwpwyTyAJc9/vjR+GXbMCRUOVekQi1g= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240911175228-daf2600bb7b7 h1:lTGIOQYLk1Ufn++X/AvZnt6VOcuhste5yp+C157No/Q= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240911175228-daf2600bb7b7/go.mod h1:BMYE1vC/pGmdFSsOJdPrAA0/4gZ0Xo0SxTMdGspBtRo= github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240916152957-433914114bd2 h1:yRk4ektpx/UxwarqAfgxUXLrsYXlaNeP1NOwzHGrK2Q= From 60976f7b9430f8c2ead1743e90780df15055bf9a Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Thu, 26 Sep 2024 17:00:59 +0100 Subject: [PATCH 13/36] Happy path integration test for Log Event Trigger Capability --- .../testutils/{test_harness.go => backend.go} | 33 +--- core/capabilities/testutils/chain_reader.go | 178 ++++++++++++++++++ .../capabilities/triggers/logevent/service.go | 6 +- .../triggers/logevent/service_test.go | 105 +++-------- .../capabilities/triggers/logevent/trigger.go | 22 ++- 5 files changed, 234 insertions(+), 110 deletions(-) rename core/capabilities/testutils/{test_harness.go => backend.go} (77%) create mode 100644 core/capabilities/testutils/chain_reader.go diff --git a/core/capabilities/testutils/test_harness.go b/core/capabilities/testutils/backend.go similarity index 77% rename from core/capabilities/testutils/test_harness.go rename to core/capabilities/testutils/backend.go index 9ce03eb71a8..9ec15a56899 100644 --- a/core/capabilities/testutils/test_harness.go +++ b/core/capabilities/testutils/backend.go @@ -9,17 +9,15 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/eth/ethconfig" + "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/test-go/testify/require" - "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/headtracker" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated/log_emitter" "github.com/smartcontractkit/chainlink/v2/core/internal/cltest" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" @@ -29,8 +27,9 @@ import ( evmrelaytypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" ) -// Test harness for handling a LOOP Capability functionality -type EVMLOOPCapabilityTH struct { +// Test harness with EVM backend and chainlink core services like +// Log Poller and Head Tracker +type EVMBackendTH struct { // Backend details Lggr logger.Logger ChainID *big.Int @@ -40,15 +39,12 @@ type EVMLOOPCapabilityTH struct { ContractsOwner *bind.TransactOpts ContractsOwnerKey ethkey.KeyV2 - LogEmitterAddress *common.Address - LogEmitterContract *log_emitter.LogEmitter - HeadTracker logpoller.HeadTracker LogPoller logpoller.LogPoller } // Test harness to create a simulated backend for testing a LOOPCapability -func NewEVMLOOPCapabilityTH(t *testing.T) *EVMLOOPCapabilityTH { +func NewEVMBackendTH(t *testing.T) *EVMBackendTH { lggr, _ := logger.NewLogger() ownerKey := cltest.MustGenerateRandomKey(t) @@ -70,15 +66,7 @@ func NewEVMLOOPCapabilityTH(t *testing.T) *EVMLOOPCapabilityTH { // Setup backend client client := evmclient.NewSimulatedBackendClient(t, backend, chainID) - // Deploy necessary contracts - // Deploy LogEmitter - logEmitterAddress, _, _, err := - log_emitter.DeployLogEmitter(contractsOwner, backend) - require.NoError(t, err) - logEmitter, err := log_emitter.NewLogEmitter(logEmitterAddress, backend) - require.NoError(t, err) - - th := &EVMLOOPCapabilityTH{ + th := &EVMBackendTH{ Lggr: lggr, ChainID: chainID, Backend: backend, @@ -86,9 +74,6 @@ func NewEVMLOOPCapabilityTH(t *testing.T) *EVMLOOPCapabilityTH { ContractsOwner: contractsOwner, ContractsOwnerKey: ownerKey, - - LogEmitterAddress: &logEmitterAddress, - LogEmitterContract: logEmitter, } th.HeadTracker, th.LogPoller = th.SetupCoreServices(t) @@ -96,7 +81,7 @@ func NewEVMLOOPCapabilityTH(t *testing.T) *EVMLOOPCapabilityTH { } // Setup core services like log poller and head tracker for the simulated backend -func (th *EVMLOOPCapabilityTH) SetupCoreServices(t *testing.T) (logpoller.HeadTracker, logpoller.LogPoller) { +func (th *EVMBackendTH) SetupCoreServices(t *testing.T) (logpoller.HeadTracker, logpoller.LogPoller) { db := pgtest.NewSqlxDB(t) const finalityDepth = 2 ht := headtracker.NewSimulatedHeadTracker(th.EVMClient, false, finalityDepth) @@ -113,10 +98,12 @@ func (th *EVMLOOPCapabilityTH) SetupCoreServices(t *testing.T) (logpoller.HeadTr KeepFinalizedBlocksDepth: 1000, }, ) + require.NoError(t, ht.Start(testutils.Context(t))) + require.NoError(t, lp.Start(testutils.Context(t))) return ht, lp } -func (th *EVMLOOPCapabilityTH) NewContractReader(t *testing.T, ctx context.Context, cfg []byte) (types.ContractReader, error) { +func (th *EVMBackendTH) NewContractReader(t *testing.T, ctx context.Context, cfg []byte) (types.ContractReader, error) { crCfg := &evmrelaytypes.ChainReaderConfig{} if err := json.Unmarshal(cfg, crCfg); err != nil { return nil, err diff --git a/core/capabilities/testutils/chain_reader.go b/core/capabilities/testutils/chain_reader.go new file mode 100644 index 00000000000..cf9e0ea5c71 --- /dev/null +++ b/core/capabilities/testutils/chain_reader.go @@ -0,0 +1,178 @@ +package testutils + +import ( + "encoding/json" + "fmt" + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + commoncaps "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" + commonvalues "github.com/smartcontractkit/chainlink-common/pkg/values" + "github.com/test-go/testify/require" + + "github.com/smartcontractkit/chainlink/v2/core/capabilities/triggers/logevent" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated/log_emitter" + coretestutils "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/logger" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" +) + +// Test harness with EVM backend and chainlink core services like +// Log Poller and Head Tracker +type ContractReaderTH struct { + BackendTH *EVMBackendTH + + LogEmitterAddress *common.Address + LogEmitterContract *log_emitter.LogEmitter + LogEmitterContractReader commontypes.ContractReader + LogEmitterRegRequest commoncaps.TriggerRegistrationRequest + LogEmitterContractReaderCfg []byte +} + +// Creates a new test harness for Contract Reader tests +func NewContractReaderTH(t *testing.T) *ContractReaderTH { + backendTH := NewEVMBackendTH(t) + + // Deploy a test contract LogEmitter for testing ContractReader + logEmitterAddress, _, _, err := + log_emitter.DeployLogEmitter(backendTH.ContractsOwner, backendTH.Backend) + require.NoError(t, err) + logEmitter, err := log_emitter.NewLogEmitter(logEmitterAddress, backendTH.Backend) + require.NoError(t, err) + + // Create new contract reader + reqConfig := logevent.RequestConfig{ + ContractName: "LogEmitter", + ContractAddress: logEmitterAddress.Hex(), + ContractEventName: "Log1", + } + contractReaderCfg := evmtypes.ChainReaderConfig{ + Contracts: map[string]evmtypes.ChainContractReader{ + reqConfig.ContractName: { + ContractPollingFilter: evmtypes.ContractPollingFilter{ + GenericEventNames: []string{reqConfig.ContractEventName}, + }, + ContractABI: log_emitter.LogEmitterABI, + Configs: map[string]*evmtypes.ChainReaderDefinition{ + reqConfig.ContractEventName: { + ChainSpecificName: reqConfig.ContractEventName, + ReadType: evmtypes.Event, + }, + }, + }, + }, + } + + // Encode contractReaderConfig as JSON and decode it into a map[string]any for + // the capability request config. Log Event Trigger capability takes in a + // []byte as ContractReaderConfig to not depend on evm ChainReaderConfig type + // and be chain agnostic + contractReaderCfgBytes, err := json.Marshal(contractReaderCfg) + require.NoError(t, err) + contractReaderCfgMap := make(map[string]any) + err = json.Unmarshal(contractReaderCfgBytes, &contractReaderCfgMap) + require.NoError(t, err) + // Encode the config map as JSON to specify in the expected call in mocked object + // The LogEventTrigger Capability receives a config map, encodes it and + // calls NewContractReader with it + contractReaderCfgBytes, _ = json.Marshal(contractReaderCfgMap) + + reqConfig.ContractReaderConfig = contractReaderCfgMap + + config, err := commonvalues.WrapMap(reqConfig) + require.NoError(t, err) + req := commoncaps.TriggerRegistrationRequest{ + TriggerID: "logeventtrigger_log1", + Config: config, + Metadata: commoncaps.RequestMetadata{ + ReferenceID: "logeventtrigger", + }, + } + + // Create a new contract reader to return from mock relayer + ctx := coretestutils.Context(t) + contractReader, err := backendTH.NewContractReader(t, ctx, contractReaderCfgBytes) + require.NoError(t, err) + + return &ContractReaderTH{ + BackendTH: backendTH, + + LogEmitterAddress: &logEmitterAddress, + LogEmitterContract: logEmitter, + LogEmitterContractReader: contractReader, + LogEmitterRegRequest: req, + LogEmitterContractReaderCfg: contractReaderCfgBytes, + } +} + +// Wait for a specific log to be emitted to a response channel by ChainReader +func WaitForLog(lggr logger.Logger, logCh <-chan commoncaps.TriggerResponse, timeout time.Duration) ( + *commoncaps.TriggerResponse, map[string]any, error) { + select { + case <-time.After(timeout): + return nil, nil, fmt.Errorf("timeout waiting for Log1 event from ContractReader") + case log := <-logCh: + lggr.Infow("Received log from ContractReader", "event", log.Event.ID) + if log.Err != nil { + return nil, nil, fmt.Errorf("error listening for Log1 event from ContractReader: %v", log.Err) + } + v := make(map[string]any) + err := log.Event.Outputs.UnwrapTo(&v) + if err != nil { + return nil, nil, fmt.Errorf("error unwrapping log to map: (log %v) %v", log.Event.Outputs, log.Err) + } + return &log, v, nil + } +} + +// Get the string value of a key from a generic map[string]any +func GetStrVal(m map[string]any, k string) (string, error) { + v, ok := m[k] + if !ok { + return "", fmt.Errorf("key %s not found", k) + } + vstr, ok := v.(string) + if !ok { + return "", fmt.Errorf("key %s not a string (%T)", k, v) + } + return vstr, nil +} + +// Get int value of a key from a generic map[string]any +func GetBigIntVal(m map[string]any, k string) (*big.Int, error) { + v, ok := m[k] + if !ok { + return nil, fmt.Errorf("key %s not found", k) + } + val, ok := v.(*big.Int) + if !ok { + return nil, fmt.Errorf("key %s not a *big.Int (%T)", k, v) + } + return val, nil +} + +// Get the int value from a map[string]map[string]any +func GetBigIntValL2(m map[string]any, level1Key string, level2Key string) (*big.Int, error) { + v, ok := m[level1Key] + if !ok { + return nil, fmt.Errorf("key %s not found", level1Key) + } + level2Map, ok := v.(map[string]any) + if !ok { + return nil, fmt.Errorf("key %s not a map[string]any (%T)", level1Key, v) + } + return GetBigIntVal(level2Map, level2Key) +} + +// Pretty print a map as a JSON string +func PrintMap(m map[string]any, prefix string, lggr logger.Logger) error { + json, err := json.MarshalIndent(m, "", " ") + if err != nil { + return err + } + lggr.Infow(prefix, "map", string(json)) + return nil +} diff --git a/core/capabilities/triggers/logevent/service.go b/core/capabilities/triggers/logevent/service.go index ac7b2a78502..162b2facaae 100644 --- a/core/capabilities/triggers/logevent/service.go +++ b/core/capabilities/triggers/logevent/service.go @@ -51,6 +51,10 @@ type LogEventConfig struct { PollPeriod uint64 `json:"pollPeriod"` } +func (config LogEventConfig) Version(capabilityVersion string) string { + return fmt.Sprintf(capabilityVersion, config.Network, config.ChainId) +} + type Params struct { Logger logger.Logger Relayer core.Relayer @@ -81,7 +85,7 @@ func NewLogEventTriggerService(p Params) *LogEventTriggerService { func (s *LogEventTriggerService) Info(ctx context.Context) (capabilities.CapabilityInfo, error) { return capabilities.NewCapabilityInfo( - fmt.Sprintf(ID, s.logEventConfig.Network, s.logEventConfig.ChainId), + s.logEventConfig.Version(ID), capabilities.CapabilityTypeTrigger, "A trigger that listens for specific contract log events and starts a workflow run.", ) diff --git a/core/capabilities/triggers/logevent/service_test.go b/core/capabilities/triggers/logevent/service_test.go index 53871ef4b29..ea909a9f0b9 100644 --- a/core/capabilities/triggers/logevent/service_test.go +++ b/core/capabilities/triggers/logevent/service_test.go @@ -1,99 +1,43 @@ package logevent_test import ( - "encoding/json" - "fmt" "math/big" "testing" "time" - commoncaps "github.com/smartcontractkit/chainlink-common/pkg/capabilities" commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" commonmocks "github.com/smartcontractkit/chainlink-common/pkg/types/core/mocks" - commonvalues "github.com/smartcontractkit/chainlink-common/pkg/values" "github.com/test-go/testify/mock" "github.com/test-go/testify/require" "github.com/smartcontractkit/chainlink/v2/core/capabilities/testutils" "github.com/smartcontractkit/chainlink/v2/core/capabilities/triggers/logevent" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated/log_emitter" coretestutils "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" - evmtypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" ) // Test for Log Event Trigger Capability happy path for EVM func TestLogEventTriggerEVMHappyPath(t *testing.T) { - th := testutils.NewEVMLOOPCapabilityTH(t) + th := testutils.NewContractReaderTH(t) + logEventConfig := logevent.LogEventConfig{ - ChainId: th.ChainID.Uint64(), + ChainId: th.BackendTH.ChainID.Uint64(), Network: "evm", LookbackBlocks: 1000, - PollPeriod: 500, - } - - // Create new contract reader - reqConfig := logevent.RequestConfig{ - ContractName: "LogEmitter", - ContractAddress: th.LogEmitterAddress.Hex(), - ContractEventName: "Log1", - } - contractReaderCfg := evmtypes.ChainReaderConfig{ - Contracts: map[string]evmtypes.ChainContractReader{ - reqConfig.ContractName: { - ContractPollingFilter: evmtypes.ContractPollingFilter{ - GenericEventNames: []string{reqConfig.ContractEventName}, - }, - ContractABI: log_emitter.LogEmitterABI, - Configs: map[string]*evmtypes.ChainReaderDefinition{ - reqConfig.ContractEventName: { - ChainSpecificName: reqConfig.ContractEventName, - ReadType: evmtypes.Event, - }, - }, - }, - }, - } - - // Encode contractReaderConfig as JSON and decode it into a map[string]any for - // the capability request config. Log Event Trigger capability takes in a - // []byte as ContractReaderConfig to not depend on evm ChainReaderConfig type - // and be chain agnostic - contractReaderCfgBytes, err := json.Marshal(contractReaderCfg) - require.NoError(t, err) - contractReaderCfgMap := make(map[string]any) - err = json.Unmarshal(contractReaderCfgBytes, &contractReaderCfgMap) - require.NoError(t, err) - // Encode the config map as JSON to specify in the expected call in mocked object - // The LogEventTrigger Capability receives a config map, encodes it and - // calls NewContractReader with it - contractReaderCfgBytes, _ = json.Marshal(contractReaderCfgMap) - - reqConfig.ContractReaderConfig = contractReaderCfgMap - - config, err := commonvalues.WrapMap(reqConfig) - require.NoError(t, err) - req := commoncaps.TriggerRegistrationRequest{ - TriggerID: "logeventtrigger_log1", - Config: config, - Metadata: commoncaps.RequestMetadata{ - ReferenceID: "logeventtrigger", - }, + PollPeriod: 1000, } // Create a new contract reader to return from mock relayer ctx := coretestutils.Context(t) - contractReader, err := th.NewContractReader(t, ctx, contractReaderCfgBytes) - require.NoError(t, err) // Fetch latest head from simulated backend to return from mock relayer - height, err := th.EVMClient.LatestBlockHeight(ctx) + height, err := th.BackendTH.EVMClient.LatestBlockHeight(ctx) require.NoError(t, err) - block, err := th.EVMClient.BlockByNumber(ctx, height) + block, err := th.BackendTH.EVMClient.BlockByNumber(ctx, height) require.NoError(t, err) // Mock relayer to return a New ContractReader instead of gRPC client of a ContractReader relayer := commonmocks.NewRelayer(t) - relayer.On("NewContractReader", mock.Anything, contractReaderCfgBytes).Return(contractReader, nil).Once() + relayer.On("NewContractReader", mock.Anything, th.LogEmitterContractReaderCfg).Return(th.LogEmitterContractReader, nil).Once() relayer.On("LatestHead", mock.Anything).Return(commontypes.Head{ Height: height.String(), Hash: block.Hash().Bytes(), @@ -102,32 +46,35 @@ func TestLogEventTriggerEVMHappyPath(t *testing.T) { // Create Log Event Trigger Service and register trigger logEventTriggerService := logevent.NewLogEventTriggerService(logevent.Params{ - Logger: th.Lggr, + Logger: th.BackendTH.Lggr, Relayer: relayer, LogEventConfig: logEventConfig, }) - log1Ch, err := logEventTriggerService.RegisterTrigger(ctx, req) + log1Ch, err := logEventTriggerService.RegisterTrigger(ctx, th.LogEmitterRegRequest) require.NoError(t, err) + expectedLogVal := int64(10) + // Send a blockchain transaction that emits logs go func() { _, err := - th.LogEmitterContract.EmitLog1(th.ContractsOwner, []*big.Int{big.NewInt(10)}) + th.LogEmitterContract.EmitLog1(th.BackendTH.ContractsOwner, []*big.Int{big.NewInt(expectedLogVal)}) require.NoError(t, err) - th.Backend.Commit() - th.Backend.Commit() - th.Backend.Commit() + th.BackendTH.Backend.Commit() + th.BackendTH.Backend.Commit() }() // Wait for logs with a timeout - timeout := 5 * time.Second - for { - select { - case <-time.After(timeout): - require.NoError(t, fmt.Errorf("Timeout waiting for Log1 event from ContractReader")) - case log1 := <-log1Ch: - require.NoError(t, log1.Err, "error listening for Log1 event from ContractReader") - require.NotNil(t, log1.Event.Outputs) - } - } + _, output, err := testutils.WaitForLog(th.BackendTH.Lggr, log1Ch, 5*time.Second) + require.NoError(t, err) + // Verify if valid cursor is returned + cursor, err := testutils.GetStrVal(output, "Cursor") + require.NoError(t, err) + require.True(t, len(cursor) > 130) + // Verify if Arg0 is correct + actualLogVal, err := testutils.GetBigIntValL2(output, "Data", "Arg0") + require.NoError(t, err) + require.Equal(t, expectedLogVal, actualLogVal.Int64()) + + testutils.PrintMap(output, "EmitLog", th.BackendTH.Lggr) } diff --git a/core/capabilities/triggers/logevent/trigger.go b/core/capabilities/triggers/logevent/trigger.go index e30c183c61d..ec30fdc8b36 100644 --- a/core/capabilities/triggers/logevent/trigger.go +++ b/core/capabilities/triggers/logevent/trigger.go @@ -86,7 +86,7 @@ func newLogEventTrigger(ctx context.Context, } // Setup callback channel, logger and ticker to poll ContractReader - callbackCh := make(chan capabilities.TriggerResponse, defaultSendChannelBufferSize) + callbackCh := make(chan capabilities.TriggerResponse) ticker := time.NewTicker(time.Duration(logEventConfig.PollPeriod) * time.Millisecond) done := make(chan bool) lggr, err := logger.New() @@ -120,6 +120,7 @@ func (l *logEventTrigger) Listen() { var logs []types.Sequence var err error logData := make(map[string]any) + cursor := "" limitAndSort := query.LimitAndSort{ SortBy: []query.SortBy{query.NewSortByTimestamp(query.Asc)}, } @@ -130,7 +131,10 @@ func (l *logEventTrigger) Listen() { case t := <-l.ticker.C: l.lggr.Infof("Polling event logs from ContractReader using QueryKey at", t, "startBlockNum", l.startBlockNum, - "limit", limitAndSort.Limit) + "cursor", cursor) + if cursor != "" { + limitAndSort.Limit = query.Limit{Cursor: cursor} + } logs, err = l.contractReader.QueryKey( l.ctx, types.BoundContract{Name: l.reqConfig.ContractName, Address: l.reqConfig.ContractAddress}, @@ -142,25 +146,29 @@ func (l *logEventTrigger) Listen() { }, }, limitAndSort, - logData, + &logData, ) if err != nil { l.lggr.Fatalw("QueryKey failure", "err", err) continue } + if len(logs) == 1 && logs[0].Cursor == cursor { + l.lggr.Infow("No new logs since", "cursor", cursor) + continue + } for _, log := range logs { - triggerResp := createTriggerResponse(log) + triggerResp := createTriggerResponse(log, l.logEventConfig.Version(ID)) go func(resp capabilities.TriggerResponse) { l.ch <- resp }(triggerResp) - limitAndSort.Limit = query.Limit{Cursor: log.Cursor} + cursor = log.Cursor } } } } // Create log event trigger capability response -func createTriggerResponse(log types.Sequence) capabilities.TriggerResponse { +func createTriggerResponse(log types.Sequence, version string) capabilities.TriggerResponse { wrappedPayload, err := values.WrapMap(log) if err != nil { return capabilities.TriggerResponse{ @@ -169,7 +177,7 @@ func createTriggerResponse(log types.Sequence) capabilities.TriggerResponse { } return capabilities.TriggerResponse{ Event: capabilities.TriggerEvent{ - TriggerType: ID, + TriggerType: version, ID: log.Cursor, Outputs: wrappedPayload, }, From a4a7f0e28d6912bc029e267ba35f43673f22951d Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Thu, 26 Sep 2024 17:09:34 +0100 Subject: [PATCH 14/36] Fix code lint annotations --- .../capabilities/triggers/logevent/service.go | 38 +++++++++---------- .../triggers/logevent/service_test.go | 6 +-- .../capabilities/triggers/logevent/trigger.go | 18 ++++----- .../capabilities/log-event-trigger/main.go | 8 ++-- 4 files changed, 34 insertions(+), 36 deletions(-) diff --git a/core/capabilities/triggers/logevent/service.go b/core/capabilities/triggers/logevent/service.go index 162b2facaae..409d6ca8309 100644 --- a/core/capabilities/triggers/logevent/service.go +++ b/core/capabilities/triggers/logevent/service.go @@ -34,45 +34,45 @@ type Response struct { // Log Event Trigger Capabilities Manager // Manages different log event triggers using an underlying triggerStore -type LogEventTriggerService struct { +type TriggerService struct { capabilities.CapabilityInfo capabilities.Validator[RequestConfig, Input, capabilities.TriggerResponse] lggr logger.Logger triggers CapabilitiesStore[logEventTrigger, capabilities.TriggerResponse] relayer core.Relayer - logEventConfig LogEventConfig + logEventConfig Config } // Common capability level config across all workflows -type LogEventConfig struct { - ChainId uint64 `json:"chainId"` +type Config struct { + ChainID uint64 `json:"chainId"` Network string `json:"network"` LookbackBlocks uint64 `json:"lookbakBlocks"` PollPeriod uint64 `json:"pollPeriod"` } -func (config LogEventConfig) Version(capabilityVersion string) string { - return fmt.Sprintf(capabilityVersion, config.Network, config.ChainId) +func (config Config) Version(capabilityVersion string) string { + return fmt.Sprintf(capabilityVersion, config.Network, config.ChainID) } type Params struct { Logger logger.Logger Relayer core.Relayer RelayerSet core.RelayerSet - LogEventConfig LogEventConfig + LogEventConfig Config } -var _ capabilities.TriggerCapability = (*LogEventTriggerService)(nil) -var _ services.Service = &LogEventTriggerService{} +var _ capabilities.TriggerCapability = (*TriggerService)(nil) +var _ services.Service = &TriggerService{} // Creates a new Cron Trigger Service. // Scheduling will commence on calling .Start() -func NewLogEventTriggerService(p Params) *LogEventTriggerService { +func NewLogEventTriggerService(p Params) *TriggerService { l := logger.Named(p.Logger, "LogEventTriggerCapabilityService: ") logEventStore := NewCapabilitiesStore[logEventTrigger, capabilities.TriggerResponse]() - s := &LogEventTriggerService{ + s := &TriggerService{ lggr: l, triggers: logEventStore, relayer: p.Relayer, @@ -83,7 +83,7 @@ func NewLogEventTriggerService(p Params) *LogEventTriggerService { return s } -func (s *LogEventTriggerService) Info(ctx context.Context) (capabilities.CapabilityInfo, error) { +func (s *TriggerService) Info(ctx context.Context) (capabilities.CapabilityInfo, error) { return capabilities.NewCapabilityInfo( s.logEventConfig.Version(ID), capabilities.CapabilityTypeTrigger, @@ -93,7 +93,7 @@ func (s *LogEventTriggerService) Info(ctx context.Context) (capabilities.Capabil // Register a new trigger // Can register triggers before the service is actively scheduling -func (s *LogEventTriggerService) RegisterTrigger(ctx context.Context, req capabilities.TriggerRegistrationRequest) (<-chan capabilities.TriggerResponse, error) { +func (s *TriggerService) RegisterTrigger(ctx context.Context, req capabilities.TriggerRegistrationRequest) (<-chan capabilities.TriggerResponse, error) { if req.Config == nil { return nil, errors.New("config is required to register a log event trigger") } @@ -112,7 +112,7 @@ func (s *LogEventTriggerService) RegisterTrigger(ctx context.Context, req capabi return respCh, nil } -func (s *LogEventTriggerService) UnregisterTrigger(ctx context.Context, req capabilities.TriggerRegistrationRequest) error { +func (s *TriggerService) UnregisterTrigger(ctx context.Context, req capabilities.TriggerRegistrationRequest) error { trigger, ok := s.triggers.Read(req.TriggerID) if !ok { return fmt.Errorf("triggerId %s not found", req.TriggerID) @@ -126,7 +126,7 @@ func (s *LogEventTriggerService) UnregisterTrigger(ctx context.Context, req capa } // Start the service. -func (s *LogEventTriggerService) Start(ctx context.Context) error { +func (s *TriggerService) Start(ctx context.Context) error { if s.relayer == nil { return errors.New("service has shutdown, it must be built again to restart") } @@ -137,18 +137,18 @@ func (s *LogEventTriggerService) Start(ctx context.Context) error { // Close stops the Service. // After this call the Service cannot be started again, // The service will need to be re-built to start scheduling again. -func (s *LogEventTriggerService) Close() error { +func (s *TriggerService) Close() error { return nil } -func (s *LogEventTriggerService) Ready() error { +func (s *TriggerService) Ready() error { return nil } -func (s *LogEventTriggerService) HealthReport() map[string]error { +func (s *TriggerService) HealthReport() map[string]error { return map[string]error{s.Name(): nil} } -func (s *LogEventTriggerService) Name() string { +func (s *TriggerService) Name() string { return "Service" } diff --git a/core/capabilities/triggers/logevent/service_test.go b/core/capabilities/triggers/logevent/service_test.go index ea909a9f0b9..8b4ed636b01 100644 --- a/core/capabilities/triggers/logevent/service_test.go +++ b/core/capabilities/triggers/logevent/service_test.go @@ -7,8 +7,8 @@ import ( commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" commonmocks "github.com/smartcontractkit/chainlink-common/pkg/types/core/mocks" + "github.com/stretchr/testify/require" "github.com/test-go/testify/mock" - "github.com/test-go/testify/require" "github.com/smartcontractkit/chainlink/v2/core/capabilities/testutils" "github.com/smartcontractkit/chainlink/v2/core/capabilities/triggers/logevent" @@ -19,8 +19,8 @@ import ( func TestLogEventTriggerEVMHappyPath(t *testing.T) { th := testutils.NewContractReaderTH(t) - logEventConfig := logevent.LogEventConfig{ - ChainId: th.BackendTH.ChainID.Uint64(), + logEventConfig := logevent.Config{ + ChainID: th.BackendTH.ChainID.Uint64(), Network: "evm", LookbackBlocks: 1000, PollPeriod: 1000, diff --git a/core/capabilities/triggers/logevent/trigger.go b/core/capabilities/triggers/logevent/trigger.go index ec30fdc8b36..835a09b768a 100644 --- a/core/capabilities/triggers/logevent/trigger.go +++ b/core/capabilities/triggers/logevent/trigger.go @@ -32,7 +32,6 @@ type RequestConfig struct { type logEventTrigger struct { ch chan<- capabilities.TriggerResponse lggr logger.Logger - ctx context.Context // Contract address and Event Signature to monitor for reqConfig *RequestConfig @@ -41,7 +40,7 @@ type logEventTrigger struct { startBlockNum uint64 // Log Event Trigger config with pollPeriod and lookbackBlocks - logEventConfig LogEventConfig + logEventConfig Config ticker *time.Ticker done chan bool } @@ -49,7 +48,7 @@ type logEventTrigger struct { // Construct for logEventTrigger struct func newLogEventTrigger(ctx context.Context, reqConfig *RequestConfig, - logEventConfig LogEventConfig, + logEventConfig Config, relayer core.Relayer) (*logEventTrigger, chan capabilities.TriggerResponse, error) { jsonBytes, err := json.Marshal(reqConfig.ContractReaderConfig) if err != nil { @@ -61,7 +60,7 @@ func newLogEventTrigger(ctx context.Context, contractReader, err := relayer.NewContractReader(ctx, jsonBytes) if err != nil { return nil, nil, - fmt.Errorf("error fetching contractReader for chainID %d from relayerSet: %v", logEventConfig.ChainId, err) + fmt.Errorf("error fetching contractReader for chainID %d from relayerSet: %v", logEventConfig.ChainID, err) } // Bind Contract in ContractReader @@ -87,7 +86,7 @@ func newLogEventTrigger(ctx context.Context, // Setup callback channel, logger and ticker to poll ContractReader callbackCh := make(chan capabilities.TriggerResponse) - ticker := time.NewTicker(time.Duration(logEventConfig.PollPeriod) * time.Millisecond) + ticker := time.NewTicker(time.Duration(int64(logEventConfig.PollPeriod)) * time.Millisecond) done := make(chan bool) lggr, err := logger.New() if err != nil { @@ -98,7 +97,6 @@ func newLogEventTrigger(ctx context.Context, l := &logEventTrigger{ ch: callbackCh, lggr: logger.Named(lggr, "LogEventTrigger: "), - ctx: ctx, reqConfig: reqConfig, contractReader: contractReader, @@ -109,13 +107,13 @@ func newLogEventTrigger(ctx context.Context, ticker: ticker, done: done, } - go l.Listen() + go l.Listen(ctx) return l, callbackCh, nil } // Listen to contract events and trigger workflow runs -func (l *logEventTrigger) Listen() { +func (l *logEventTrigger) Listen(ctx context.Context) { // Listen for events from lookbackPeriod var logs []types.Sequence var err error @@ -129,14 +127,14 @@ func (l *logEventTrigger) Listen() { case <-l.done: return case t := <-l.ticker.C: - l.lggr.Infof("Polling event logs from ContractReader using QueryKey at", t, + l.lggr.Infow("Polling event logs from ContractReader using QueryKey at", "time", t, "startBlockNum", l.startBlockNum, "cursor", cursor) if cursor != "" { limitAndSort.Limit = query.Limit{Cursor: cursor} } logs, err = l.contractReader.QueryKey( - l.ctx, + ctx, types.BoundContract{Name: l.reqConfig.ContractName, Address: l.reqConfig.ContractAddress}, query.KeyFilter{ Key: l.reqConfig.ContractEventName, diff --git a/plugins/cmd/capabilities/log-event-trigger/main.go b/plugins/cmd/capabilities/log-event-trigger/main.go index 66d1a2cbe4a..dc00ef59420 100644 --- a/plugins/cmd/capabilities/log-event-trigger/main.go +++ b/plugins/cmd/capabilities/log-event-trigger/main.go @@ -22,7 +22,7 @@ const ( type LogEventTriggerGRPCService struct { trigger capabilities.TriggerCapability s *loop.Server - config logevent.LogEventConfig + config logevent.Config } func main() { @@ -91,16 +91,16 @@ func (cs *LogEventTriggerGRPCService) Initialise( ) error { cs.s.Logger.Debugf("Initialising %s", serviceName) - var logEventConfig logevent.LogEventConfig + var logEventConfig logevent.Config err := json.Unmarshal([]byte(config), &logEventConfig) if err != nil { return fmt.Errorf("error decoding log_event_trigger config: %v", err) } - relayID := types.NewRelayID(logEventConfig.Network, fmt.Sprintf("%d", logEventConfig.ChainId)) + relayID := types.NewRelayID(logEventConfig.Network, fmt.Sprintf("%d", logEventConfig.ChainID)) relayer, err := relayerSet.Get(ctx, relayID) if err != nil { - return fmt.Errorf("error fetching relayer for chainID %d from relayerSet: %v", logEventConfig.ChainId, err) + return fmt.Errorf("error fetching relayer for chainID %d from relayerSet: %v", logEventConfig.ChainID, err) } // Set relayer and trigger in LogEventTriggerGRPCService From 1cc9ce0ed73f85c29f7f78c2c5b41e9445025709 Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Fri, 27 Sep 2024 10:31:19 +0100 Subject: [PATCH 15/36] Addressed PR comments --- core/capabilities/triggers/logevent/service.go | 4 ++-- core/capabilities/triggers/logevent/trigger.go | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/core/capabilities/triggers/logevent/service.go b/core/capabilities/triggers/logevent/service.go index 409d6ca8309..64e17706fca 100644 --- a/core/capabilities/triggers/logevent/service.go +++ b/core/capabilities/triggers/logevent/service.go @@ -108,7 +108,7 @@ func (s *TriggerService) RegisterTrigger(ctx context.Context, req capabilities.T if err != nil { return nil, fmt.Errorf("LogEventTrigger %v", err) } - s.lggr.Debugw("RegisterTrigger", "triggerId", req.TriggerID) + s.lggr.Infow("RegisterTrigger", "triggerId", req.TriggerID, "WorkflowExecutionID", req.Metadata.WorkflowExecutionID) return respCh, nil } @@ -121,7 +121,7 @@ func (s *TriggerService) UnregisterTrigger(ctx context.Context, req capabilities trigger.Stop() // Remove from triggers context s.triggers.Delete(req.TriggerID) - s.lggr.Debugw("UnregisterTrigger", "triggerId", req.TriggerID) + s.lggr.Infow("UnregisterTrigger", "triggerId", req.TriggerID, "WorkflowExecutionID", req.Metadata.WorkflowExecutionID) return nil } diff --git a/core/capabilities/triggers/logevent/trigger.go b/core/capabilities/triggers/logevent/trigger.go index 835a09b768a..7744054af70 100644 --- a/core/capabilities/triggers/logevent/trigger.go +++ b/core/capabilities/triggers/logevent/trigger.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "strconv" + "sync" "time" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" @@ -43,6 +44,7 @@ type logEventTrigger struct { logEventConfig Config ticker *time.Ticker done chan bool + wg sync.WaitGroup } // Construct for logEventTrigger struct @@ -156,7 +158,9 @@ func (l *logEventTrigger) Listen(ctx context.Context) { } for _, log := range logs { triggerResp := createTriggerResponse(log, l.logEventConfig.Version(ID)) + l.wg.Add(1) go func(resp capabilities.TriggerResponse) { + defer l.wg.Done() l.ch <- resp }(triggerResp) cursor = log.Cursor @@ -184,6 +188,15 @@ func createTriggerResponse(log types.Sequence, version string) capabilities.Trig // Stop contract event listener for the current contract func (l *logEventTrigger) Stop() { + l.lggr.Infow("Closing trigger server for (waiting for waitGroup)", "ChainID", l.logEventConfig.ChainID, + "ContractName", l.reqConfig.ContractName, + "ContractAddress", l.reqConfig.ContractAddress, + "ContractEventName", l.reqConfig.ContractEventName) + l.wg.Wait() close(l.ch) l.done <- true + l.lggr.Infow("Closed trigger server for", "ChainID", l.logEventConfig.ChainID, + "ContractName", l.reqConfig.ContractName, + "ContractAddress", l.reqConfig.ContractAddress, + "ContractEventName", l.reqConfig.ContractEventName) } From 4a56bb5721ae6a6255bbc9ff4a5c53ff272dca7f Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Mon, 30 Sep 2024 10:00:56 +0100 Subject: [PATCH 16/36] Added changeset --- .changeset/chilly-crews-retire.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/chilly-crews-retire.md diff --git a/.changeset/chilly-crews-retire.md b/.changeset/chilly-crews-retire.md new file mode 100644 index 00000000000..28b531a9ddb --- /dev/null +++ b/.changeset/chilly-crews-retire.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +#added log-event-trigger LOOPP capability, using ChainReader From 344f6eb6c1466a510f5c5b5291bf6b8dc4d60a43 Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Mon, 30 Sep 2024 10:52:18 +0100 Subject: [PATCH 17/36] Addressed Lint errors --- core/capabilities/triggers/logevent/service.go | 3 +++ .../capabilities/triggers/logevent/service_test.go | 14 ++++++++------ core/capabilities/triggers/logevent/trigger.go | 9 +++++++-- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/core/capabilities/triggers/logevent/service.go b/core/capabilities/triggers/logevent/service.go index 64e17706fca..beec6d24693 100644 --- a/core/capabilities/triggers/logevent/service.go +++ b/core/capabilities/triggers/logevent/service.go @@ -138,6 +138,9 @@ func (s *TriggerService) Start(ctx context.Context) error { // After this call the Service cannot be started again, // The service will need to be re-built to start scheduling again. func (s *TriggerService) Close() error { + for _, trigger := range s.triggers.ReadAll() { + trigger.Stop() + } return nil } diff --git a/core/capabilities/triggers/logevent/service_test.go b/core/capabilities/triggers/logevent/service_test.go index 8b4ed636b01..1879a927e63 100644 --- a/core/capabilities/triggers/logevent/service_test.go +++ b/core/capabilities/triggers/logevent/service_test.go @@ -5,11 +5,11 @@ import ( "testing" "time" - commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" - commonmocks "github.com/smartcontractkit/chainlink-common/pkg/types/core/mocks" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/test-go/testify/mock" + commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" + commonmocks "github.com/smartcontractkit/chainlink-common/pkg/types/core/mocks" "github.com/smartcontractkit/chainlink/v2/core/capabilities/testutils" "github.com/smartcontractkit/chainlink/v2/core/capabilities/triggers/logevent" coretestutils "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" @@ -57,7 +57,7 @@ func TestLogEventTriggerEVMHappyPath(t *testing.T) { // Send a blockchain transaction that emits logs go func() { - _, err := + _, err = th.LogEmitterContract.EmitLog1(th.BackendTH.ContractsOwner, []*big.Int{big.NewInt(expectedLogVal)}) require.NoError(t, err) th.BackendTH.Backend.Commit() @@ -67,14 +67,16 @@ func TestLogEventTriggerEVMHappyPath(t *testing.T) { // Wait for logs with a timeout _, output, err := testutils.WaitForLog(th.BackendTH.Lggr, log1Ch, 5*time.Second) require.NoError(t, err) + err = testutils.PrintMap(output, "EmitLog", th.BackendTH.Lggr) + require.NoError(t, err) // Verify if valid cursor is returned cursor, err := testutils.GetStrVal(output, "Cursor") require.NoError(t, err) - require.True(t, len(cursor) > 130) + require.True(t, len(cursor) > 60) // Verify if Arg0 is correct actualLogVal, err := testutils.GetBigIntValL2(output, "Data", "Arg0") require.NoError(t, err) require.Equal(t, expectedLogVal, actualLogVal.Int64()) - testutils.PrintMap(output, "EmitLog", th.BackendTH.Lggr) + logEventTriggerService.Close() } diff --git a/core/capabilities/triggers/logevent/trigger.go b/core/capabilities/triggers/logevent/trigger.go index 7744054af70..9f24ca7c03b 100644 --- a/core/capabilities/triggers/logevent/trigger.go +++ b/core/capabilities/triggers/logevent/trigger.go @@ -8,6 +8,7 @@ import ( "sync" "time" + "github.com/ethereum/go-ethereum/common/math" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/types" @@ -87,8 +88,12 @@ func newLogEventTrigger(ctx context.Context, } // Setup callback channel, logger and ticker to poll ContractReader - callbackCh := make(chan capabilities.TriggerResponse) - ticker := time.NewTicker(time.Duration(int64(logEventConfig.PollPeriod)) * time.Millisecond) + callbackCh := make(chan capabilities.TriggerResponse, defaultSendChannelBufferSize) + pollPeriod := int64(1000) // default pollPeriod is 1s + if logEventConfig.PollPeriod <= math.MaxInt64 { + pollPeriod = int64(logEventConfig.PollPeriod) + } + ticker := time.NewTicker(time.Duration(pollPeriod) * time.Millisecond) done := make(chan bool) lggr, err := logger.New() if err != nil { From 3c629e4cd5c1663e29039b34fa9826144691101b Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Mon, 30 Sep 2024 11:03:20 +0100 Subject: [PATCH 18/36] Addressed PR comments --- core/capabilities/triggers/logevent/service.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/core/capabilities/triggers/logevent/service.go b/core/capabilities/triggers/logevent/service.go index beec6d24693..f66f52deef0 100644 --- a/core/capabilities/triggers/logevent/service.go +++ b/core/capabilities/triggers/logevent/service.go @@ -25,13 +25,6 @@ type Payload struct { ActualExecutionTime string } -// Log Event Trigger Capability Response -type Response struct { - capabilities.TriggerEvent - Metadata struct{} - Payload Payload -} - // Log Event Trigger Capabilities Manager // Manages different log event triggers using an underlying triggerStore type TriggerService struct { From 842da5a9dce7187f97aaa8ac8a3445e07ddd5e2f Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Mon, 30 Sep 2024 11:16:42 +0100 Subject: [PATCH 19/36] Addressed more lint issues --- core/capabilities/triggers/logevent/service.go | 2 +- core/capabilities/triggers/logevent/trigger.go | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/core/capabilities/triggers/logevent/service.go b/core/capabilities/triggers/logevent/service.go index f66f52deef0..10bef10cbf3 100644 --- a/core/capabilities/triggers/logevent/service.go +++ b/core/capabilities/triggers/logevent/service.go @@ -41,7 +41,7 @@ type Config struct { ChainID uint64 `json:"chainId"` Network string `json:"network"` LookbackBlocks uint64 `json:"lookbakBlocks"` - PollPeriod uint64 `json:"pollPeriod"` + PollPeriod uint32 `json:"pollPeriod"` } func (config Config) Version(capabilityVersion string) string { diff --git a/core/capabilities/triggers/logevent/trigger.go b/core/capabilities/triggers/logevent/trigger.go index 9f24ca7c03b..6374305453d 100644 --- a/core/capabilities/triggers/logevent/trigger.go +++ b/core/capabilities/triggers/logevent/trigger.go @@ -8,7 +8,6 @@ import ( "sync" "time" - "github.com/ethereum/go-ethereum/common/math" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/types" @@ -89,11 +88,7 @@ func newLogEventTrigger(ctx context.Context, // Setup callback channel, logger and ticker to poll ContractReader callbackCh := make(chan capabilities.TriggerResponse, defaultSendChannelBufferSize) - pollPeriod := int64(1000) // default pollPeriod is 1s - if logEventConfig.PollPeriod <= math.MaxInt64 { - pollPeriod = int64(logEventConfig.PollPeriod) - } - ticker := time.NewTicker(time.Duration(pollPeriod) * time.Millisecond) + ticker := time.NewTicker(time.Duration(logEventConfig.PollPeriod) * time.Millisecond) done := make(chan bool) lggr, err := logger.New() if err != nil { From 17b6c1a9ae88e780e89599cdf8638f9d4063838c Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Mon, 30 Sep 2024 12:02:03 +0100 Subject: [PATCH 20/36] Simplified trigger ctx creation and cancel flows --- .../capabilities/triggers/logevent/service.go | 25 +++++++--- .../capabilities/triggers/logevent/trigger.go | 48 ++++++++----------- 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/core/capabilities/triggers/logevent/service.go b/core/capabilities/triggers/logevent/service.go index 10bef10cbf3..d9e9d1e76f3 100644 --- a/core/capabilities/triggers/logevent/service.go +++ b/core/capabilities/triggers/logevent/service.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "sync" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/logger" @@ -34,6 +35,8 @@ type TriggerService struct { triggers CapabilitiesStore[logEventTrigger, capabilities.TriggerResponse] relayer core.Relayer logEventConfig Config + stopChan services.StopChan + wg sync.WaitGroup } // Common capability level config across all workflows @@ -61,7 +64,7 @@ var _ services.Service = &TriggerService{} // Creates a new Cron Trigger Service. // Scheduling will commence on calling .Start() func NewLogEventTriggerService(p Params) *TriggerService { - l := logger.Named(p.Logger, "LogEventTriggerCapabilityService: ") + l := logger.Named(p.Logger, "LogEventTriggerCapabilityService") logEventStore := NewCapabilitiesStore[logEventTrigger, capabilities.TriggerResponse]() @@ -70,6 +73,8 @@ func NewLogEventTriggerService(p Params) *TriggerService { triggers: logEventStore, relayer: p.Relayer, logEventConfig: p.LogEventConfig, + stopChan: make(services.StopChan), + wg: sync.WaitGroup{}, } s.CapabilityInfo, _ = s.Info(context.Background()) s.Validator = capabilities.NewValidator[RequestConfig, Input, capabilities.TriggerResponse](capabilities.ValidatorArgs{Info: s.CapabilityInfo}) @@ -96,12 +101,19 @@ func (s *TriggerService) RegisterTrigger(ctx context.Context, req capabilities.T } // Add log event trigger with Contract details to CapabilitiesStore respCh, err := s.triggers.InsertIfNotExists(req.TriggerID, func() (*logEventTrigger, chan capabilities.TriggerResponse, error) { - return newLogEventTrigger(ctx, reqConfig, s.logEventConfig, s.relayer) + ctx, cancel := s.stopChan.NewCtx() + l, ch, err := newLogEventTrigger(ctx, cancel, s.lggr, req.Metadata.WorkflowID, reqConfig, s.logEventConfig, s.relayer) + s.wg.Add(1) + go func() { + defer s.wg.Done() + l.Listen(ctx) // Blocking call, until ctx.Done is issued + }() + return l, ch, err }) if err != nil { return nil, fmt.Errorf("LogEventTrigger %v", err) } - s.lggr.Infow("RegisterTrigger", "triggerId", req.TriggerID, "WorkflowExecutionID", req.Metadata.WorkflowExecutionID) + s.lggr.Infow("RegisterTrigger", "triggerId", req.TriggerID, "WorkflowID", req.Metadata.WorkflowID) return respCh, nil } @@ -114,7 +126,7 @@ func (s *TriggerService) UnregisterTrigger(ctx context.Context, req capabilities trigger.Stop() // Remove from triggers context s.triggers.Delete(req.TriggerID) - s.lggr.Infow("UnregisterTrigger", "triggerId", req.TriggerID, "WorkflowExecutionID", req.Metadata.WorkflowExecutionID) + s.lggr.Infow("UnregisterTrigger", "triggerId", req.TriggerID, "WorkflowID", req.Metadata.WorkflowID) return nil } @@ -131,9 +143,8 @@ func (s *TriggerService) Start(ctx context.Context) error { // After this call the Service cannot be started again, // The service will need to be re-built to start scheduling again. func (s *TriggerService) Close() error { - for _, trigger := range s.triggers.ReadAll() { - trigger.Stop() - } + close(s.stopChan) + s.wg.Wait() return nil } diff --git a/core/capabilities/triggers/logevent/trigger.go b/core/capabilities/triggers/logevent/trigger.go index 6374305453d..504a9a98e6c 100644 --- a/core/capabilities/triggers/logevent/trigger.go +++ b/core/capabilities/triggers/logevent/trigger.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "strconv" - "sync" "time" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" @@ -43,12 +42,14 @@ type logEventTrigger struct { // Log Event Trigger config with pollPeriod and lookbackBlocks logEventConfig Config ticker *time.Ticker - done chan bool - wg sync.WaitGroup + cancel context.CancelFunc } // Construct for logEventTrigger struct func newLogEventTrigger(ctx context.Context, + cancel context.CancelFunc, + lggr logger.Logger, + workflowID string, reqConfig *RequestConfig, logEventConfig Config, relayer core.Relayer) (*logEventTrigger, chan capabilities.TriggerResponse, error) { @@ -89,16 +90,11 @@ func newLogEventTrigger(ctx context.Context, // Setup callback channel, logger and ticker to poll ContractReader callbackCh := make(chan capabilities.TriggerResponse, defaultSendChannelBufferSize) ticker := time.NewTicker(time.Duration(logEventConfig.PollPeriod) * time.Millisecond) - done := make(chan bool) - lggr, err := logger.New() - if err != nil { - return nil, nil, fmt.Errorf("could not initialise logger for LogEventTrigger") - } // Initialise a Log Event Trigger l := &logEventTrigger{ ch: callbackCh, - lggr: logger.Named(lggr, "LogEventTrigger: "), + lggr: logger.Named(lggr, fmt.Sprintf("LogEventTrigger.%s", workflowID)), reqConfig: reqConfig, contractReader: contractReader, @@ -107,15 +103,15 @@ func newLogEventTrigger(ctx context.Context, logEventConfig: logEventConfig, ticker: ticker, - done: done, + cancel: cancel, } - go l.Listen(ctx) - return l, callbackCh, nil } // Listen to contract events and trigger workflow runs func (l *logEventTrigger) Listen(ctx context.Context) { + defer l.cancel() + // Listen for events from lookbackPeriod var logs []types.Sequence var err error @@ -126,7 +122,11 @@ func (l *logEventTrigger) Listen(ctx context.Context) { } for { select { - case <-l.done: + case <-ctx.Done(): + l.lggr.Infow("Closing trigger server for (waiting for waitGroup)", "ChainID", l.logEventConfig.ChainID, + "ContractName", l.reqConfig.ContractName, + "ContractAddress", l.reqConfig.ContractAddress, + "ContractEventName", l.reqConfig.ContractEventName) return case t := <-l.ticker.C: l.lggr.Infow("Polling event logs from ContractReader using QueryKey at", "time", t, @@ -158,11 +158,7 @@ func (l *logEventTrigger) Listen(ctx context.Context) { } for _, log := range logs { triggerResp := createTriggerResponse(log, l.logEventConfig.Version(ID)) - l.wg.Add(1) - go func(resp capabilities.TriggerResponse) { - defer l.wg.Done() - l.ch <- resp - }(triggerResp) + l.ch <- triggerResp cursor = log.Cursor } } @@ -187,16 +183,10 @@ func createTriggerResponse(log types.Sequence, version string) capabilities.Trig } // Stop contract event listener for the current contract +// This function is called when UnregisterTrigger is called individually +// for a specific ContractAddress and EventName +// When the whole capability service is stopped, stopChan of the service +// is closed, which would stop all triggers func (l *logEventTrigger) Stop() { - l.lggr.Infow("Closing trigger server for (waiting for waitGroup)", "ChainID", l.logEventConfig.ChainID, - "ContractName", l.reqConfig.ContractName, - "ContractAddress", l.reqConfig.ContractAddress, - "ContractEventName", l.reqConfig.ContractEventName) - l.wg.Wait() - close(l.ch) - l.done <- true - l.lggr.Infow("Closed trigger server for", "ChainID", l.logEventConfig.ChainID, - "ContractName", l.reqConfig.ContractName, - "ContractAddress", l.reqConfig.ContractAddress, - "ContractEventName", l.reqConfig.ContractEventName) + l.cancel() } From 2786cef05f09d02cb1a3c45b85c5e5460804aed2 Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Mon, 30 Sep 2024 12:09:01 +0100 Subject: [PATCH 21/36] Added comment --- core/capabilities/triggers/logevent/trigger.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/capabilities/triggers/logevent/trigger.go b/core/capabilities/triggers/logevent/trigger.go index 504a9a98e6c..41dd8fabbc0 100644 --- a/core/capabilities/triggers/logevent/trigger.go +++ b/core/capabilities/triggers/logevent/trigger.go @@ -152,6 +152,10 @@ func (l *logEventTrigger) Listen(ctx context.Context) { l.lggr.Fatalw("QueryKey failure", "err", err) continue } + // ChainReader QueryKey API provides logs including the cursor value and not + // after the cursor value. If the response only consists of the log corresponding + // to the cursor and no log after it, then we understand that there are no new + // logs if len(logs) == 1 && logs[0].Cursor == cursor { l.lggr.Infow("No new logs since", "cursor", cursor) continue From 31954eaf5cd0624acf4796b781bca17348663428 Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Mon, 30 Sep 2024 13:56:45 +0100 Subject: [PATCH 22/36] Addressed PR comments --- core/capabilities/testutils/backend.go | 2 +- core/capabilities/triggers/logevent/service.go | 13 ++++--------- core/capabilities/triggers/logevent/service_test.go | 4 ++-- core/capabilities/triggers/logevent/store.go | 9 ++++++--- core/capabilities/triggers/logevent/trigger.go | 2 +- plugins/cmd/capabilities/log-event-trigger/main.go | 2 +- 6 files changed, 15 insertions(+), 17 deletions(-) diff --git a/core/capabilities/testutils/backend.go b/core/capabilities/testutils/backend.go index 9ec15a56899..fac9124f9ff 100644 --- a/core/capabilities/testutils/backend.go +++ b/core/capabilities/testutils/backend.go @@ -45,7 +45,7 @@ type EVMBackendTH struct { // Test harness to create a simulated backend for testing a LOOPCapability func NewEVMBackendTH(t *testing.T) *EVMBackendTH { - lggr, _ := logger.NewLogger() + lggr := logger.TestLogger(t) ownerKey := cltest.MustGenerateRandomKey(t) contractsOwner, err := bind.NewKeyedTransactorWithChainID(ownerKey.ToEcdsaPrivKey(), testutils.SimulatedChainID) diff --git a/core/capabilities/triggers/logevent/service.go b/core/capabilities/triggers/logevent/service.go index d9e9d1e76f3..9f839c87856 100644 --- a/core/capabilities/triggers/logevent/service.go +++ b/core/capabilities/triggers/logevent/service.go @@ -41,7 +41,7 @@ type TriggerService struct { // Common capability level config across all workflows type Config struct { - ChainID uint64 `json:"chainId"` + ChainID string `json:"chainId"` Network string `json:"network"` LookbackBlocks uint64 `json:"lookbakBlocks"` PollPeriod uint32 `json:"pollPeriod"` @@ -54,7 +54,6 @@ func (config Config) Version(capabilityVersion string) string { type Params struct { Logger logger.Logger Relayer core.Relayer - RelayerSet core.RelayerSet LogEventConfig Config } @@ -63,7 +62,7 @@ var _ services.Service = &TriggerService{} // Creates a new Cron Trigger Service. // Scheduling will commence on calling .Start() -func NewLogEventTriggerService(p Params) *TriggerService { +func NewTriggerService(p Params) *TriggerService { l := logger.Named(p.Logger, "LogEventTriggerCapabilityService") logEventStore := NewCapabilitiesStore[logEventTrigger, capabilities.TriggerResponse]() @@ -74,7 +73,6 @@ func NewLogEventTriggerService(p Params) *TriggerService { relayer: p.Relayer, logEventConfig: p.LogEventConfig, stopChan: make(services.StopChan), - wg: sync.WaitGroup{}, } s.CapabilityInfo, _ = s.Info(context.Background()) s.Validator = capabilities.NewValidator[RequestConfig, Input, capabilities.TriggerResponse](capabilities.ValidatorArgs{Info: s.CapabilityInfo}) @@ -100,7 +98,8 @@ func (s *TriggerService) RegisterTrigger(ctx context.Context, req capabilities.T return nil, err } // Add log event trigger with Contract details to CapabilitiesStore - respCh, err := s.triggers.InsertIfNotExists(req.TriggerID, func() (*logEventTrigger, chan capabilities.TriggerResponse, error) { + var respCh chan capabilities.TriggerResponse + respCh, err = s.triggers.InsertIfNotExists(req.TriggerID, func() (*logEventTrigger, chan capabilities.TriggerResponse, error) { ctx, cancel := s.stopChan.NewCtx() l, ch, err := newLogEventTrigger(ctx, cancel, s.lggr, req.Metadata.WorkflowID, reqConfig, s.logEventConfig, s.relayer) s.wg.Add(1) @@ -132,10 +131,6 @@ func (s *TriggerService) UnregisterTrigger(ctx context.Context, req capabilities // Start the service. func (s *TriggerService) Start(ctx context.Context) error { - if s.relayer == nil { - return errors.New("service has shutdown, it must be built again to restart") - } - return nil } diff --git a/core/capabilities/triggers/logevent/service_test.go b/core/capabilities/triggers/logevent/service_test.go index 1879a927e63..c2e75d89483 100644 --- a/core/capabilities/triggers/logevent/service_test.go +++ b/core/capabilities/triggers/logevent/service_test.go @@ -20,7 +20,7 @@ func TestLogEventTriggerEVMHappyPath(t *testing.T) { th := testutils.NewContractReaderTH(t) logEventConfig := logevent.Config{ - ChainID: th.BackendTH.ChainID.Uint64(), + ChainID: th.BackendTH.ChainID.String(), Network: "evm", LookbackBlocks: 1000, PollPeriod: 1000, @@ -45,7 +45,7 @@ func TestLogEventTriggerEVMHappyPath(t *testing.T) { }, nil).Once() // Create Log Event Trigger Service and register trigger - logEventTriggerService := logevent.NewLogEventTriggerService(logevent.Params{ + logEventTriggerService := logevent.NewTriggerService(logevent.Params{ Logger: th.BackendTH.Lggr, Relayer: relayer, LogEventConfig: logEventConfig, diff --git a/core/capabilities/triggers/logevent/store.go b/core/capabilities/triggers/logevent/store.go index 0f8cebfab02..56e4a6f235b 100644 --- a/core/capabilities/triggers/logevent/store.go +++ b/core/capabilities/triggers/logevent/store.go @@ -51,11 +51,14 @@ func (cs *capabilitiesStore[T, Resp]) Write(capabilityID string, value *T) { } func (cs *capabilitiesStore[T, Resp]) InsertIfNotExists(capabilityID string, fn RegisterCapabilityFn[T, Resp]) (chan Resp, error) { - cs.mu.Lock() - defer cs.mu.Unlock() - if _, ok := cs.capabilities[capabilityID]; ok { + cs.mu.RLock() + _, ok := cs.capabilities[capabilityID] + cs.mu.RUnlock() + if ok { return nil, fmt.Errorf("capabilityID %v already exists", capabilityID) } + cs.mu.Lock() + defer cs.mu.Unlock() value, respCh, err := fn() if err != nil { return nil, fmt.Errorf("error registering capability: %v", err) diff --git a/core/capabilities/triggers/logevent/trigger.go b/core/capabilities/triggers/logevent/trigger.go index 41dd8fabbc0..0d81e8f2a9a 100644 --- a/core/capabilities/triggers/logevent/trigger.go +++ b/core/capabilities/triggers/logevent/trigger.go @@ -63,7 +63,7 @@ func newLogEventTrigger(ctx context.Context, contractReader, err := relayer.NewContractReader(ctx, jsonBytes) if err != nil { return nil, nil, - fmt.Errorf("error fetching contractReader for chainID %d from relayerSet: %v", logEventConfig.ChainID, err) + fmt.Errorf("error fetching contractReader for chainID %s from relayerSet: %v", logEventConfig.ChainID, err) } // Bind Contract in ContractReader diff --git a/plugins/cmd/capabilities/log-event-trigger/main.go b/plugins/cmd/capabilities/log-event-trigger/main.go index dc00ef59420..84ff8a73912 100644 --- a/plugins/cmd/capabilities/log-event-trigger/main.go +++ b/plugins/cmd/capabilities/log-event-trigger/main.go @@ -105,7 +105,7 @@ func (cs *LogEventTriggerGRPCService) Initialise( // Set relayer and trigger in LogEventTriggerGRPCService cs.config = logEventConfig - cs.trigger = logevent.NewLogEventTriggerService(logevent.Params{ + cs.trigger = logevent.NewTriggerService(logevent.Params{ Logger: cs.s.Logger, Relayer: relayer, LogEventConfig: logEventConfig, From c48b80d66c90393d9d438ae701abbf62b5de04d5 Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Mon, 30 Sep 2024 14:47:45 +0100 Subject: [PATCH 23/36] Implemented Start/Close pattern in logEventTrigger and used stopChan to track listener --- .../capabilities/triggers/logevent/service.go | 23 +++++---------- core/capabilities/triggers/logevent/store.go | 10 +++++-- .../capabilities/triggers/logevent/trigger.go | 29 +++++++++++++------ 3 files changed, 35 insertions(+), 27 deletions(-) diff --git a/core/capabilities/triggers/logevent/service.go b/core/capabilities/triggers/logevent/service.go index 9f839c87856..007a9d73852 100644 --- a/core/capabilities/triggers/logevent/service.go +++ b/core/capabilities/triggers/logevent/service.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "sync" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/logger" @@ -35,8 +34,6 @@ type TriggerService struct { triggers CapabilitiesStore[logEventTrigger, capabilities.TriggerResponse] relayer core.Relayer logEventConfig Config - stopChan services.StopChan - wg sync.WaitGroup } // Common capability level config across all workflows @@ -72,7 +69,6 @@ func NewTriggerService(p Params) *TriggerService { triggers: logEventStore, relayer: p.Relayer, logEventConfig: p.LogEventConfig, - stopChan: make(services.StopChan), } s.CapabilityInfo, _ = s.Info(context.Background()) s.Validator = capabilities.NewValidator[RequestConfig, Input, capabilities.TriggerResponse](capabilities.ValidatorArgs{Info: s.CapabilityInfo}) @@ -100,13 +96,11 @@ func (s *TriggerService) RegisterTrigger(ctx context.Context, req capabilities.T // Add log event trigger with Contract details to CapabilitiesStore var respCh chan capabilities.TriggerResponse respCh, err = s.triggers.InsertIfNotExists(req.TriggerID, func() (*logEventTrigger, chan capabilities.TriggerResponse, error) { - ctx, cancel := s.stopChan.NewCtx() - l, ch, err := newLogEventTrigger(ctx, cancel, s.lggr, req.Metadata.WorkflowID, reqConfig, s.logEventConfig, s.relayer) - s.wg.Add(1) - go func() { - defer s.wg.Done() - l.Listen(ctx) // Blocking call, until ctx.Done is issued - }() + l, ch, err := newLogEventTrigger(ctx, s.lggr, req.Metadata.WorkflowID, reqConfig, s.logEventConfig, s.relayer) + if err != nil { + return l, ch, err + } + err = l.Start(ctx) return l, ch, err }) if err != nil { @@ -122,7 +116,7 @@ func (s *TriggerService) UnregisterTrigger(ctx context.Context, req capabilities return fmt.Errorf("triggerId %s not found", req.TriggerID) } // Close callback channel and stop log event trigger listener - trigger.Stop() + trigger.Close() // Remove from triggers context s.triggers.Delete(req.TriggerID) s.lggr.Infow("UnregisterTrigger", "triggerId", req.TriggerID, "WorkflowID", req.Metadata.WorkflowID) @@ -138,9 +132,8 @@ func (s *TriggerService) Start(ctx context.Context) error { // After this call the Service cannot be started again, // The service will need to be re-built to start scheduling again. func (s *TriggerService) Close() error { - close(s.stopChan) - s.wg.Wait() - return nil + triggers := s.triggers.ReadAll() + return services.MultiCloser(triggers).Close() } func (s *TriggerService) Ready() error { diff --git a/core/capabilities/triggers/logevent/store.go b/core/capabilities/triggers/logevent/store.go index 56e4a6f235b..bebdcdcb845 100644 --- a/core/capabilities/triggers/logevent/store.go +++ b/core/capabilities/triggers/logevent/store.go @@ -10,7 +10,7 @@ type RegisterCapabilityFn[T any, Resp any] func() (*T, chan Resp, error) // Interface of the capabilities store type CapabilitiesStore[T any, Resp any] interface { Read(capabilityID string) (value *T, ok bool) - ReadAll() (values map[string]*T) + ReadAll() (values []*T) Write(capabilityID string, value *T) InsertIfNotExists(capabilityID string, fn RegisterCapabilityFn[T, Resp]) (chan Resp, error) Delete(capabilityID string) @@ -38,10 +38,14 @@ func (cs *capabilitiesStore[T, Resp]) Read(capabilityID string) (value *T, ok bo return trigger, ok } -func (cs *capabilitiesStore[T, Resp]) ReadAll() (values map[string]*T) { +func (cs *capabilitiesStore[T, Resp]) ReadAll() (values []*T) { cs.mu.RLock() defer cs.mu.RUnlock() - return cs.capabilities + vals := make([]*T, 0) + for _, v := range cs.capabilities { + vals = append(vals, v) + } + return vals } func (cs *capabilitiesStore[T, Resp]) Write(capabilityID string, value *T) { diff --git a/core/capabilities/triggers/logevent/trigger.go b/core/capabilities/triggers/logevent/trigger.go index 0d81e8f2a9a..37670c99270 100644 --- a/core/capabilities/triggers/logevent/trigger.go +++ b/core/capabilities/triggers/logevent/trigger.go @@ -9,6 +9,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink-common/pkg/types/core" "github.com/smartcontractkit/chainlink-common/pkg/types/query" @@ -42,12 +43,12 @@ type logEventTrigger struct { // Log Event Trigger config with pollPeriod and lookbackBlocks logEventConfig Config ticker *time.Ticker - cancel context.CancelFunc + stopChan services.StopChan + done chan bool } // Construct for logEventTrigger struct func newLogEventTrigger(ctx context.Context, - cancel context.CancelFunc, lggr logger.Logger, workflowID string, reqConfig *RequestConfig, @@ -103,14 +104,22 @@ func newLogEventTrigger(ctx context.Context, logEventConfig: logEventConfig, ticker: ticker, - cancel: cancel, + stopChan: make(services.StopChan), + done: make(chan bool), } return l, callbackCh, nil } -// Listen to contract events and trigger workflow runs -func (l *logEventTrigger) Listen(ctx context.Context) { - defer l.cancel() +func (l *logEventTrigger) Start(ctx context.Context) error { + go l.listen() + return nil +} + +// Start to contract events and trigger workflow runs +func (l *logEventTrigger) listen() { + ctx, cancel := l.stopChan.NewCtx() + defer cancel() + defer close(l.done) // Listen for events from lookbackPeriod var logs []types.Sequence @@ -186,11 +195,13 @@ func createTriggerResponse(log types.Sequence, version string) capabilities.Trig } } -// Stop contract event listener for the current contract +// Close contract event listener for the current contract // This function is called when UnregisterTrigger is called individually // for a specific ContractAddress and EventName // When the whole capability service is stopped, stopChan of the service // is closed, which would stop all triggers -func (l *logEventTrigger) Stop() { - l.cancel() +func (l *logEventTrigger) Close() error { + close(l.stopChan) + <-l.done + return nil } From 33bed46abf79fe30424e0716ab29eea986ca4420 Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Mon, 30 Sep 2024 14:54:33 +0100 Subject: [PATCH 24/36] Addressed more PR comments --- core/capabilities/triggers/logevent/service.go | 6 ------ core/capabilities/triggers/logevent/service_test.go | 1 + core/capabilities/triggers/logevent/trigger.go | 5 ++++- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/core/capabilities/triggers/logevent/service.go b/core/capabilities/triggers/logevent/service.go index 007a9d73852..0e46258c4a9 100644 --- a/core/capabilities/triggers/logevent/service.go +++ b/core/capabilities/triggers/logevent/service.go @@ -19,12 +19,6 @@ const defaultSendChannelBufferSize = 1000 type Input struct { } -// Log Event Trigger Capability Payload -type Payload struct { - // Time that Log Event Trigger's task execution occurred (RFC3339Nano formatted) - ActualExecutionTime string -} - // Log Event Trigger Capabilities Manager // Manages different log event triggers using an underlying triggerStore type TriggerService struct { diff --git a/core/capabilities/triggers/logevent/service_test.go b/core/capabilities/triggers/logevent/service_test.go index c2e75d89483..d4c52804525 100644 --- a/core/capabilities/triggers/logevent/service_test.go +++ b/core/capabilities/triggers/logevent/service_test.go @@ -62,6 +62,7 @@ func TestLogEventTriggerEVMHappyPath(t *testing.T) { require.NoError(t, err) th.BackendTH.Backend.Commit() th.BackendTH.Backend.Commit() + th.BackendTH.Backend.Commit() }() // Wait for logs with a timeout diff --git a/core/capabilities/triggers/logevent/trigger.go b/core/capabilities/triggers/logevent/trigger.go index 37670c99270..472c2b249c6 100644 --- a/core/capabilities/triggers/logevent/trigger.go +++ b/core/capabilities/triggers/logevent/trigger.go @@ -150,7 +150,7 @@ func (l *logEventTrigger) listen() { query.KeyFilter{ Key: l.reqConfig.ContractEventName, Expressions: []query.Expression{ - query.Confidence(primitives.Unconfirmed), + query.Confidence(primitives.Finalized), query.Block(fmt.Sprintf("%d", l.startBlockNum), primitives.Gte), }, }, @@ -170,6 +170,9 @@ func (l *logEventTrigger) listen() { continue } for _, log := range logs { + if log.Cursor == cursor { + continue + } triggerResp := createTriggerResponse(log, l.logEventConfig.Version(ID)) l.ch <- triggerResp cursor = log.Cursor From 48c672848a9b405ce3a767b297c5fc3bb6180367 Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Mon, 30 Sep 2024 15:22:46 +0100 Subject: [PATCH 25/36] Handled errors from Info and Close methods --- .../capabilities/triggers/logevent/service.go | 19 +++++++++++++------ .../triggers/logevent/service_test.go | 3 ++- .../capabilities/log-event-trigger/main.go | 9 ++++++--- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/core/capabilities/triggers/logevent/service.go b/core/capabilities/triggers/logevent/service.go index 0e46258c4a9..204c9384a53 100644 --- a/core/capabilities/triggers/logevent/service.go +++ b/core/capabilities/triggers/logevent/service.go @@ -11,7 +11,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/types/core" ) -const ID = "log-event-trigger-%s-%d@1.0.0" +const ID = "log-event-trigger-%s-%s@1.0.0" const defaultSendChannelBufferSize = 1000 @@ -53,7 +53,7 @@ var _ services.Service = &TriggerService{} // Creates a new Cron Trigger Service. // Scheduling will commence on calling .Start() -func NewTriggerService(p Params) *TriggerService { +func NewTriggerService(p Params) (*TriggerService, error) { l := logger.Named(p.Logger, "LogEventTriggerCapabilityService") logEventStore := NewCapabilitiesStore[logEventTrigger, capabilities.TriggerResponse]() @@ -64,9 +64,13 @@ func NewTriggerService(p Params) *TriggerService { relayer: p.Relayer, logEventConfig: p.LogEventConfig, } - s.CapabilityInfo, _ = s.Info(context.Background()) + var err error + s.CapabilityInfo, err = s.Info(context.Background()) + if err != nil { + return s, err + } s.Validator = capabilities.NewValidator[RequestConfig, Input, capabilities.TriggerResponse](capabilities.ValidatorArgs{Info: s.CapabilityInfo}) - return s + return s, nil } func (s *TriggerService) Info(ctx context.Context) (capabilities.CapabilityInfo, error) { @@ -98,7 +102,7 @@ func (s *TriggerService) RegisterTrigger(ctx context.Context, req capabilities.T return l, ch, err }) if err != nil { - return nil, fmt.Errorf("LogEventTrigger %v", err) + return nil, fmt.Errorf("create new trigger failed %w", err) } s.lggr.Infow("RegisterTrigger", "triggerId", req.TriggerID, "WorkflowID", req.Metadata.WorkflowID) return respCh, nil @@ -110,7 +114,10 @@ func (s *TriggerService) UnregisterTrigger(ctx context.Context, req capabilities return fmt.Errorf("triggerId %s not found", req.TriggerID) } // Close callback channel and stop log event trigger listener - trigger.Close() + err := trigger.Close() + if err != nil { + return fmt.Errorf("error closing trigger %s (chainID %s): %w", req.TriggerID, s.logEventConfig.ChainID, err) + } // Remove from triggers context s.triggers.Delete(req.TriggerID) s.lggr.Infow("UnregisterTrigger", "triggerId", req.TriggerID, "WorkflowID", req.Metadata.WorkflowID) diff --git a/core/capabilities/triggers/logevent/service_test.go b/core/capabilities/triggers/logevent/service_test.go index d4c52804525..7b11d0309e3 100644 --- a/core/capabilities/triggers/logevent/service_test.go +++ b/core/capabilities/triggers/logevent/service_test.go @@ -45,11 +45,12 @@ func TestLogEventTriggerEVMHappyPath(t *testing.T) { }, nil).Once() // Create Log Event Trigger Service and register trigger - logEventTriggerService := logevent.NewTriggerService(logevent.Params{ + logEventTriggerService, err := logevent.NewTriggerService(logevent.Params{ Logger: th.BackendTH.Lggr, Relayer: relayer, LogEventConfig: logEventConfig, }) + require.NoError(t, err) log1Ch, err := logEventTriggerService.RegisterTrigger(ctx, th.LogEmitterRegRequest) require.NoError(t, err) diff --git a/plugins/cmd/capabilities/log-event-trigger/main.go b/plugins/cmd/capabilities/log-event-trigger/main.go index 84ff8a73912..2b21208a3aa 100644 --- a/plugins/cmd/capabilities/log-event-trigger/main.go +++ b/plugins/cmd/capabilities/log-event-trigger/main.go @@ -97,19 +97,22 @@ func (cs *LogEventTriggerGRPCService) Initialise( return fmt.Errorf("error decoding log_event_trigger config: %v", err) } - relayID := types.NewRelayID(logEventConfig.Network, fmt.Sprintf("%d", logEventConfig.ChainID)) + relayID := types.NewRelayID(logEventConfig.Network, logEventConfig.ChainID) relayer, err := relayerSet.Get(ctx, relayID) if err != nil { - return fmt.Errorf("error fetching relayer for chainID %d from relayerSet: %v", logEventConfig.ChainID, err) + return fmt.Errorf("error fetching relayer for chainID %s from relayerSet: %v", logEventConfig.ChainID, err) } // Set relayer and trigger in LogEventTriggerGRPCService cs.config = logEventConfig - cs.trigger = logevent.NewTriggerService(logevent.Params{ + cs.trigger, err = logevent.NewTriggerService(logevent.Params{ Logger: cs.s.Logger, Relayer: relayer, LogEventConfig: logEventConfig, }) + if err != nil { + return fmt.Errorf("error creating new trigger for chainID %s: %v", logEventConfig.ChainID, err) + } if err := capabilityRegistry.Add(ctx, cs.trigger); err != nil { return fmt.Errorf("error when adding cron trigger to the registry: %w", err) From 3be348889c51e55180e1347dbae1d43e0d179d76 Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Mon, 30 Sep 2024 15:41:12 +0100 Subject: [PATCH 26/36] Fixed lint errors and pass ctx to Info --- core/capabilities/testutils/backend.go | 4 ++-- core/capabilities/triggers/logevent/service.go | 14 +++++++------- .../capabilities/triggers/logevent/service_test.go | 2 +- core/services/relay/evm/chain_reader.go | 1 - plugins/cmd/capabilities/log-event-trigger/main.go | 2 +- 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/core/capabilities/testutils/backend.go b/core/capabilities/testutils/backend.go index fac9124f9ff..6efa3048758 100644 --- a/core/capabilities/testutils/backend.go +++ b/core/capabilities/testutils/backend.go @@ -11,9 +11,9 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/eth/ethconfig" - "github.com/smartcontractkit/chainlink-common/pkg/types" - "github.com/test-go/testify/require" + "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/headtracker" diff --git a/core/capabilities/triggers/logevent/service.go b/core/capabilities/triggers/logevent/service.go index 204c9384a53..399c40fac8d 100644 --- a/core/capabilities/triggers/logevent/service.go +++ b/core/capabilities/triggers/logevent/service.go @@ -53,7 +53,7 @@ var _ services.Service = &TriggerService{} // Creates a new Cron Trigger Service. // Scheduling will commence on calling .Start() -func NewTriggerService(p Params) (*TriggerService, error) { +func NewTriggerService(ctx context.Context, p Params) (*TriggerService, error) { l := logger.Named(p.Logger, "LogEventTriggerCapabilityService") logEventStore := NewCapabilitiesStore[logEventTrigger, capabilities.TriggerResponse]() @@ -65,7 +65,7 @@ func NewTriggerService(p Params) (*TriggerService, error) { logEventConfig: p.LogEventConfig, } var err error - s.CapabilityInfo, err = s.Info(context.Background()) + s.CapabilityInfo, err = s.Info(ctx) if err != nil { return s, err } @@ -94,12 +94,12 @@ func (s *TriggerService) RegisterTrigger(ctx context.Context, req capabilities.T // Add log event trigger with Contract details to CapabilitiesStore var respCh chan capabilities.TriggerResponse respCh, err = s.triggers.InsertIfNotExists(req.TriggerID, func() (*logEventTrigger, chan capabilities.TriggerResponse, error) { - l, ch, err := newLogEventTrigger(ctx, s.lggr, req.Metadata.WorkflowID, reqConfig, s.logEventConfig, s.relayer) - if err != nil { - return l, ch, err + l, ch, tErr := newLogEventTrigger(ctx, s.lggr, req.Metadata.WorkflowID, reqConfig, s.logEventConfig, s.relayer) + if tErr != nil { + return l, ch, tErr } - err = l.Start(ctx) - return l, ch, err + tErr = l.Start(ctx) + return l, ch, tErr }) if err != nil { return nil, fmt.Errorf("create new trigger failed %w", err) diff --git a/core/capabilities/triggers/logevent/service_test.go b/core/capabilities/triggers/logevent/service_test.go index 7b11d0309e3..4c573a20839 100644 --- a/core/capabilities/triggers/logevent/service_test.go +++ b/core/capabilities/triggers/logevent/service_test.go @@ -45,7 +45,7 @@ func TestLogEventTriggerEVMHappyPath(t *testing.T) { }, nil).Once() // Create Log Event Trigger Service and register trigger - logEventTriggerService, err := logevent.NewTriggerService(logevent.Params{ + logEventTriggerService, err := logevent.NewTriggerService(ctx, logevent.Params{ Logger: th.BackendTH.Lggr, Relayer: relayer, LogEventConfig: logEventConfig, diff --git a/core/services/relay/evm/chain_reader.go b/core/services/relay/evm/chain_reader.go index e5041f5486a..6b9f9411789 100644 --- a/core/services/relay/evm/chain_reader.go +++ b/core/services/relay/evm/chain_reader.go @@ -19,7 +19,6 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/types/query" "github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives" "github.com/smartcontractkit/chainlink-common/pkg/values" - 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" diff --git a/plugins/cmd/capabilities/log-event-trigger/main.go b/plugins/cmd/capabilities/log-event-trigger/main.go index 2b21208a3aa..0c5f7d07aee 100644 --- a/plugins/cmd/capabilities/log-event-trigger/main.go +++ b/plugins/cmd/capabilities/log-event-trigger/main.go @@ -105,7 +105,7 @@ func (cs *LogEventTriggerGRPCService) Initialise( // Set relayer and trigger in LogEventTriggerGRPCService cs.config = logEventConfig - cs.trigger, err = logevent.NewTriggerService(logevent.Params{ + cs.trigger, err = logevent.NewTriggerService(ctx, logevent.Params{ Logger: cs.s.Logger, Relayer: relayer, LogEventConfig: logEventConfig, From de4485cbda77de5f551afb60314d1033ddddedc6 Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Mon, 30 Sep 2024 16:22:49 +0100 Subject: [PATCH 27/36] Handle race conditions in log event trigger service --- core/capabilities/testutils/backend.go | 2 +- core/capabilities/testutils/chain_reader.go | 6 ++--- .../capabilities/triggers/logevent/service.go | 27 +++++++++++++------ 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/core/capabilities/testutils/backend.go b/core/capabilities/testutils/backend.go index 6efa3048758..f41bcc6838d 100644 --- a/core/capabilities/testutils/backend.go +++ b/core/capabilities/testutils/backend.go @@ -103,7 +103,7 @@ func (th *EVMBackendTH) SetupCoreServices(t *testing.T) (logpoller.HeadTracker, return ht, lp } -func (th *EVMBackendTH) NewContractReader(t *testing.T, ctx context.Context, cfg []byte) (types.ContractReader, error) { +func (th *EVMBackendTH) NewContractReader(ctx context.Context, t *testing.T, cfg []byte) (types.ContractReader, error) { crCfg := &evmrelaytypes.ChainReaderConfig{} if err := json.Unmarshal(cfg, crCfg); err != nil { return nil, err diff --git a/core/capabilities/testutils/chain_reader.go b/core/capabilities/testutils/chain_reader.go index cf9e0ea5c71..976099c2a73 100644 --- a/core/capabilities/testutils/chain_reader.go +++ b/core/capabilities/testutils/chain_reader.go @@ -8,11 +8,11 @@ import ( "time" "github.com/ethereum/go-ethereum/common" + "github.com/test-go/testify/require" + commoncaps "github.com/smartcontractkit/chainlink-common/pkg/capabilities" commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" commonvalues "github.com/smartcontractkit/chainlink-common/pkg/values" - "github.com/test-go/testify/require" - "github.com/smartcontractkit/chainlink/v2/core/capabilities/triggers/logevent" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated/log_emitter" coretestutils "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" @@ -94,7 +94,7 @@ func NewContractReaderTH(t *testing.T) *ContractReaderTH { // Create a new contract reader to return from mock relayer ctx := coretestutils.Context(t) - contractReader, err := backendTH.NewContractReader(t, ctx, contractReaderCfgBytes) + contractReader, err := backendTH.NewContractReader(ctx, t, contractReaderCfgBytes) require.NoError(t, err) return &ContractReaderTH{ diff --git a/core/capabilities/triggers/logevent/service.go b/core/capabilities/triggers/logevent/service.go index 399c40fac8d..36b6b467d53 100644 --- a/core/capabilities/triggers/logevent/service.go +++ b/core/capabilities/triggers/logevent/service.go @@ -22,12 +22,14 @@ type Input struct { // Log Event Trigger Capabilities Manager // Manages different log event triggers using an underlying triggerStore type TriggerService struct { + services.StateMachine capabilities.CapabilityInfo capabilities.Validator[RequestConfig, Input, capabilities.TriggerResponse] lggr logger.Logger triggers CapabilitiesStore[logEventTrigger, capabilities.TriggerResponse] relayer core.Relayer logEventConfig Config + stopCh services.StopChan } // Common capability level config across all workflows @@ -63,6 +65,7 @@ func NewTriggerService(ctx context.Context, p Params) (*TriggerService, error) { triggers: logEventStore, relayer: p.Relayer, logEventConfig: p.LogEventConfig, + stopCh: make(services.StopChan), } var err error s.CapabilityInfo, err = s.Info(ctx) @@ -93,14 +96,19 @@ func (s *TriggerService) RegisterTrigger(ctx context.Context, req capabilities.T } // Add log event trigger with Contract details to CapabilitiesStore var respCh chan capabilities.TriggerResponse - respCh, err = s.triggers.InsertIfNotExists(req.TriggerID, func() (*logEventTrigger, chan capabilities.TriggerResponse, error) { - l, ch, tErr := newLogEventTrigger(ctx, s.lggr, req.Metadata.WorkflowID, reqConfig, s.logEventConfig, s.relayer) - if tErr != nil { + ok := s.IfNotStopped(func() { + respCh, err = s.triggers.InsertIfNotExists(req.TriggerID, func() (*logEventTrigger, chan capabilities.TriggerResponse, error) { + l, ch, tErr := newLogEventTrigger(ctx, s.lggr, req.Metadata.WorkflowID, reqConfig, s.logEventConfig, s.relayer) + if tErr != nil { + return l, ch, tErr + } + tErr = l.Start(ctx) return l, ch, tErr - } - tErr = l.Start(ctx) - return l, ch, tErr + }) }) + if !ok { + return nil, fmt.Errorf("cannot create new trigger since LogEventTriggerService has been stopped") + } if err != nil { return nil, fmt.Errorf("create new trigger failed %w", err) } @@ -133,8 +141,11 @@ func (s *TriggerService) Start(ctx context.Context) error { // After this call the Service cannot be started again, // The service will need to be re-built to start scheduling again. func (s *TriggerService) Close() error { - triggers := s.triggers.ReadAll() - return services.MultiCloser(triggers).Close() + return s.StopOnce("Log Event Trigger Capability Service", func() error { + s.lggr.Infow("Stopping LogEventTrigger Capability Service") + triggers := s.triggers.ReadAll() + return services.MultiCloser(triggers).Close() + }) } func (s *TriggerService) Ready() error { From ecf40ebe5be2b862618d4d274eef8b8c65828d2a Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Mon, 30 Sep 2024 16:41:41 +0100 Subject: [PATCH 28/36] Fixed lint errors --- core/capabilities/testutils/chain_reader.go | 2 +- core/capabilities/triggers/logevent/service.go | 17 +++++++---------- .../triggers/logevent/service_test.go | 9 ++++----- .../cmd/capabilities/log-event-trigger/main.go | 6 +----- 4 files changed, 13 insertions(+), 21 deletions(-) diff --git a/core/capabilities/testutils/chain_reader.go b/core/capabilities/testutils/chain_reader.go index 976099c2a73..90b4e4ff478 100644 --- a/core/capabilities/testutils/chain_reader.go +++ b/core/capabilities/testutils/chain_reader.go @@ -8,7 +8,7 @@ import ( "time" "github.com/ethereum/go-ethereum/common" - "github.com/test-go/testify/require" + "github.com/stretchr/testify/require" commoncaps "github.com/smartcontractkit/chainlink-common/pkg/capabilities" commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" diff --git a/core/capabilities/triggers/logevent/service.go b/core/capabilities/triggers/logevent/service.go index 36b6b467d53..8e19b8f62c2 100644 --- a/core/capabilities/triggers/logevent/service.go +++ b/core/capabilities/triggers/logevent/service.go @@ -44,27 +44,24 @@ func (config Config) Version(capabilityVersion string) string { return fmt.Sprintf(capabilityVersion, config.Network, config.ChainID) } -type Params struct { - Logger logger.Logger - Relayer core.Relayer - LogEventConfig Config -} - var _ capabilities.TriggerCapability = (*TriggerService)(nil) var _ services.Service = &TriggerService{} // Creates a new Cron Trigger Service. // Scheduling will commence on calling .Start() -func NewTriggerService(ctx context.Context, p Params) (*TriggerService, error) { - l := logger.Named(p.Logger, "LogEventTriggerCapabilityService") +func NewTriggerService(ctx context.Context, + lggr logger.Logger, + relayer core.Relayer, + logEventConfig Config) (*TriggerService, error) { + l := logger.Named(lggr, "LogEventTriggerCapabilityService") logEventStore := NewCapabilitiesStore[logEventTrigger, capabilities.TriggerResponse]() s := &TriggerService{ lggr: l, triggers: logEventStore, - relayer: p.Relayer, - logEventConfig: p.LogEventConfig, + relayer: relayer, + logEventConfig: logEventConfig, stopCh: make(services.StopChan), } var err error diff --git a/core/capabilities/triggers/logevent/service_test.go b/core/capabilities/triggers/logevent/service_test.go index 4c573a20839..f96411a4bc2 100644 --- a/core/capabilities/triggers/logevent/service_test.go +++ b/core/capabilities/triggers/logevent/service_test.go @@ -45,11 +45,10 @@ func TestLogEventTriggerEVMHappyPath(t *testing.T) { }, nil).Once() // Create Log Event Trigger Service and register trigger - logEventTriggerService, err := logevent.NewTriggerService(ctx, logevent.Params{ - Logger: th.BackendTH.Lggr, - Relayer: relayer, - LogEventConfig: logEventConfig, - }) + logEventTriggerService, err := logevent.NewTriggerService(ctx, + th.BackendTH.Lggr, + relayer, + logEventConfig) require.NoError(t, err) log1Ch, err := logEventTriggerService.RegisterTrigger(ctx, th.LogEmitterRegRequest) require.NoError(t, err) diff --git a/plugins/cmd/capabilities/log-event-trigger/main.go b/plugins/cmd/capabilities/log-event-trigger/main.go index 0c5f7d07aee..8abecf54aeb 100644 --- a/plugins/cmd/capabilities/log-event-trigger/main.go +++ b/plugins/cmd/capabilities/log-event-trigger/main.go @@ -105,11 +105,7 @@ func (cs *LogEventTriggerGRPCService) Initialise( // Set relayer and trigger in LogEventTriggerGRPCService cs.config = logEventConfig - cs.trigger, err = logevent.NewTriggerService(ctx, logevent.Params{ - Logger: cs.s.Logger, - Relayer: relayer, - LogEventConfig: logEventConfig, - }) + cs.trigger, err = logevent.NewTriggerService(ctx, cs.s.Logger, relayer, logEventConfig) if err != nil { return fmt.Errorf("error creating new trigger for chainID %s: %v", logEventConfig.ChainID, err) } From e23f0d134fb01aa9fa89ac08fb97ee55998c0dad Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Mon, 30 Sep 2024 17:29:30 +0100 Subject: [PATCH 29/36] Minor change --- core/capabilities/testutils/backend.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/capabilities/testutils/backend.go b/core/capabilities/testutils/backend.go index f41bcc6838d..be8107c2973 100644 --- a/core/capabilities/testutils/backend.go +++ b/core/capabilities/testutils/backend.go @@ -56,9 +56,9 @@ func NewEVMBackendTH(t *testing.T) *EVMBackendTH { contractsOwner.From: {Balance: assets.Ether(100000).ToInt()}, } chainID := testutils.SimulatedChainID - gasLimit := uint32(ethconfig.Defaults.Miner.GasCeil) + gasLimit := uint32(ethconfig.Defaults.Miner.GasCeil) //nolint:gosec backend := cltest.NewSimulatedBackend(t, genesisData, gasLimit) - blockTime := time.UnixMilli(int64(backend.Blockchain().CurrentHeader().Time)) + blockTime := time.UnixMilli(int64(backend.Blockchain().CurrentHeader().Time)) //nolint:gosec err = backend.AdjustTime(time.Since(blockTime) - 24*time.Hour) require.NoError(t, err) backend.Commit() From 017c1f7fdb7dba47e1ac9208bb69dda7c9aea7bb Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Mon, 30 Sep 2024 17:51:46 +0100 Subject: [PATCH 30/36] Test fix and lint fixes --- core/capabilities/testutils/backend.go | 3 --- core/capabilities/testutils/chain_reader.go | 10 ---------- core/capabilities/triggers/logevent/service_test.go | 5 ++--- 3 files changed, 2 insertions(+), 16 deletions(-) diff --git a/core/capabilities/testutils/backend.go b/core/capabilities/testutils/backend.go index be8107c2973..1236cd00d74 100644 --- a/core/capabilities/testutils/backend.go +++ b/core/capabilities/testutils/backend.go @@ -58,9 +58,6 @@ func NewEVMBackendTH(t *testing.T) *EVMBackendTH { chainID := testutils.SimulatedChainID gasLimit := uint32(ethconfig.Defaults.Miner.GasCeil) //nolint:gosec backend := cltest.NewSimulatedBackend(t, genesisData, gasLimit) - blockTime := time.UnixMilli(int64(backend.Blockchain().CurrentHeader().Time)) //nolint:gosec - err = backend.AdjustTime(time.Since(blockTime) - 24*time.Hour) - require.NoError(t, err) backend.Commit() // Setup backend client diff --git a/core/capabilities/testutils/chain_reader.go b/core/capabilities/testutils/chain_reader.go index 90b4e4ff478..4d5cea92f93 100644 --- a/core/capabilities/testutils/chain_reader.go +++ b/core/capabilities/testutils/chain_reader.go @@ -166,13 +166,3 @@ func GetBigIntValL2(m map[string]any, level1Key string, level2Key string) (*big. } return GetBigIntVal(level2Map, level2Key) } - -// Pretty print a map as a JSON string -func PrintMap(m map[string]any, prefix string, lggr logger.Logger) error { - json, err := json.MarshalIndent(m, "", " ") - if err != nil { - return err - } - lggr.Infow(prefix, "map", string(json)) - return nil -} diff --git a/core/capabilities/triggers/logevent/service_test.go b/core/capabilities/triggers/logevent/service_test.go index f96411a4bc2..bab12368f0b 100644 --- a/core/capabilities/triggers/logevent/service_test.go +++ b/core/capabilities/triggers/logevent/service_test.go @@ -66,10 +66,9 @@ func TestLogEventTriggerEVMHappyPath(t *testing.T) { }() // Wait for logs with a timeout - _, output, err := testutils.WaitForLog(th.BackendTH.Lggr, log1Ch, 5*time.Second) - require.NoError(t, err) - err = testutils.PrintMap(output, "EmitLog", th.BackendTH.Lggr) + _, output, err := testutils.WaitForLog(th.BackendTH.Lggr, log1Ch, 15*time.Second) require.NoError(t, err) + th.BackendTH.Lggr.Infow("EmitLog", "output", output) // Verify if valid cursor is returned cursor, err := testutils.GetStrVal(output, "Cursor") require.NoError(t, err) From d70aff4226e4ac6f34e9bb3f7a384cdfacb6a857 Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Mon, 30 Sep 2024 19:05:22 +0100 Subject: [PATCH 31/36] Move EVM specific tests out of chain-agnostic capability --- .../relay/evm/capabilities/log_event_trigger_test.go} | 2 +- core/{ => services/relay/evm}/capabilities/testutils/backend.go | 0 .../relay/evm}/capabilities/testutils/chain_reader.go | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename core/{capabilities/triggers/logevent/service_test.go => services/relay/evm/capabilities/log_event_trigger_test.go} (96%) rename core/{ => services/relay/evm}/capabilities/testutils/backend.go (100%) rename core/{ => services/relay/evm}/capabilities/testutils/chain_reader.go (100%) diff --git a/core/capabilities/triggers/logevent/service_test.go b/core/services/relay/evm/capabilities/log_event_trigger_test.go similarity index 96% rename from core/capabilities/triggers/logevent/service_test.go rename to core/services/relay/evm/capabilities/log_event_trigger_test.go index bab12368f0b..9c59dd2d2c5 100644 --- a/core/capabilities/triggers/logevent/service_test.go +++ b/core/services/relay/evm/capabilities/log_event_trigger_test.go @@ -10,9 +10,9 @@ import ( commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" commonmocks "github.com/smartcontractkit/chainlink-common/pkg/types/core/mocks" - "github.com/smartcontractkit/chainlink/v2/core/capabilities/testutils" "github.com/smartcontractkit/chainlink/v2/core/capabilities/triggers/logevent" coretestutils "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/capabilities/testutils" ) // Test for Log Event Trigger Capability happy path for EVM diff --git a/core/capabilities/testutils/backend.go b/core/services/relay/evm/capabilities/testutils/backend.go similarity index 100% rename from core/capabilities/testutils/backend.go rename to core/services/relay/evm/capabilities/testutils/backend.go diff --git a/core/capabilities/testutils/chain_reader.go b/core/services/relay/evm/capabilities/testutils/chain_reader.go similarity index 100% rename from core/capabilities/testutils/chain_reader.go rename to core/services/relay/evm/capabilities/testutils/chain_reader.go From 97e18b8e0b51d9457cdf35258d438f9c01cc3ffe Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Mon, 30 Sep 2024 19:20:34 +0100 Subject: [PATCH 32/36] Set block time --- core/services/relay/evm/capabilities/testutils/backend.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/services/relay/evm/capabilities/testutils/backend.go b/core/services/relay/evm/capabilities/testutils/backend.go index 1236cd00d74..be8107c2973 100644 --- a/core/services/relay/evm/capabilities/testutils/backend.go +++ b/core/services/relay/evm/capabilities/testutils/backend.go @@ -58,6 +58,9 @@ func NewEVMBackendTH(t *testing.T) *EVMBackendTH { chainID := testutils.SimulatedChainID gasLimit := uint32(ethconfig.Defaults.Miner.GasCeil) //nolint:gosec backend := cltest.NewSimulatedBackend(t, genesisData, gasLimit) + blockTime := time.UnixMilli(int64(backend.Blockchain().CurrentHeader().Time)) //nolint:gosec + err = backend.AdjustTime(time.Since(blockTime) - 24*time.Hour) + require.NoError(t, err) backend.Commit() // Setup backend client From 723b872771129f9edc853c21531d901a4250fec9 Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Mon, 30 Sep 2024 19:26:39 +0100 Subject: [PATCH 33/36] Check existence of trigger in slow path --- core/capabilities/triggers/logevent/store.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/capabilities/triggers/logevent/store.go b/core/capabilities/triggers/logevent/store.go index bebdcdcb845..ac9d3741cd1 100644 --- a/core/capabilities/triggers/logevent/store.go +++ b/core/capabilities/triggers/logevent/store.go @@ -63,6 +63,10 @@ func (cs *capabilitiesStore[T, Resp]) InsertIfNotExists(capabilityID string, fn } cs.mu.Lock() defer cs.mu.Unlock() + _, ok = cs.capabilities[capabilityID] + if ok { + return nil, fmt.Errorf("capabilityID %v already exists", capabilityID) + } value, respCh, err := fn() if err != nil { return nil, fmt.Errorf("error registering capability: %v", err) From 9b6d66bcd3f0f3b8efdd4a63aae3bd3c54d7406a Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Mon, 30 Sep 2024 19:46:07 +0100 Subject: [PATCH 34/36] Complete usage of services.Service with StartOnce and StopOnce with tests updated --- core/capabilities/triggers/logevent/service.go | 17 ++++++++--------- core/capabilities/triggers/logevent/trigger.go | 8 ++++---- .../evm/capabilities/log_event_trigger_test.go | 7 +++++-- .../evm/capabilities/testutils/chain_reader.go | 3 ++- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/core/capabilities/triggers/logevent/service.go b/core/capabilities/triggers/logevent/service.go index 8e19b8f62c2..7ed4855e097 100644 --- a/core/capabilities/triggers/logevent/service.go +++ b/core/capabilities/triggers/logevent/service.go @@ -131,28 +131,27 @@ func (s *TriggerService) UnregisterTrigger(ctx context.Context, req capabilities // Start the service. func (s *TriggerService) Start(ctx context.Context) error { - return nil + return s.StartOnce("LogEventTriggerCapabilityService", func() error { + s.lggr.Info("Starting LogEventTriggerCapabilityService") + return nil + }) } // Close stops the Service. // After this call the Service cannot be started again, // The service will need to be re-built to start scheduling again. func (s *TriggerService) Close() error { - return s.StopOnce("Log Event Trigger Capability Service", func() error { - s.lggr.Infow("Stopping LogEventTrigger Capability Service") + return s.StopOnce("LogEventTriggerCapabilityService", func() error { + s.lggr.Infow("Stopping LogEventTriggerCapabilityService") triggers := s.triggers.ReadAll() return services.MultiCloser(triggers).Close() }) } -func (s *TriggerService) Ready() error { - return nil -} - func (s *TriggerService) HealthReport() map[string]error { - return map[string]error{s.Name(): nil} + return map[string]error{s.Name(): s.Healthy()} } func (s *TriggerService) Name() string { - return "Service" + return s.lggr.Name() } diff --git a/core/capabilities/triggers/logevent/trigger.go b/core/capabilities/triggers/logevent/trigger.go index 472c2b249c6..9a0e1d036c7 100644 --- a/core/capabilities/triggers/logevent/trigger.go +++ b/core/capabilities/triggers/logevent/trigger.go @@ -64,7 +64,7 @@ func newLogEventTrigger(ctx context.Context, contractReader, err := relayer.NewContractReader(ctx, jsonBytes) if err != nil { return nil, nil, - fmt.Errorf("error fetching contractReader for chainID %s from relayerSet: %v", logEventConfig.ChainID, err) + fmt.Errorf("error fetching contractReader for chainID %s from relayerSet: %w", logEventConfig.ChainID, err) } // Bind Contract in ContractReader @@ -77,11 +77,11 @@ func newLogEventTrigger(ctx context.Context, // Get current block HEAD/tip of the blockchain to start polling from latestHead, err := relayer.LatestHead(ctx) if err != nil { - return nil, nil, fmt.Errorf("error getting latestHead from relayer client: %v", err) + return nil, nil, fmt.Errorf("error getting latestHead from relayer client: %w", err) } height, err := strconv.ParseUint(latestHead.Height, 10, 64) if err != nil { - return nil, nil, fmt.Errorf("invalid height in latestHead from relayer client: %v", err) + return nil, nil, fmt.Errorf("invalid height in latestHead from relayer client: %w", err) } startBlockNum := uint64(0) if height > logEventConfig.LookbackBlocks { @@ -158,7 +158,7 @@ func (l *logEventTrigger) listen() { &logData, ) if err != nil { - l.lggr.Fatalw("QueryKey failure", "err", err) + l.lggr.Errorw("QueryKey failure", "err", err) continue } // ChainReader QueryKey API provides logs including the cursor value and not diff --git a/core/services/relay/evm/capabilities/log_event_trigger_test.go b/core/services/relay/evm/capabilities/log_event_trigger_test.go index 9c59dd2d2c5..1638c54782c 100644 --- a/core/services/relay/evm/capabilities/log_event_trigger_test.go +++ b/core/services/relay/evm/capabilities/log_event_trigger_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink-common/pkg/services/servicetest" commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" commonmocks "github.com/smartcontractkit/chainlink-common/pkg/types/core/mocks" "github.com/smartcontractkit/chainlink/v2/core/capabilities/triggers/logevent" @@ -50,6 +51,10 @@ func TestLogEventTriggerEVMHappyPath(t *testing.T) { relayer, logEventConfig) require.NoError(t, err) + + // Start the service + servicetest.Run(t, logEventTriggerService) + log1Ch, err := logEventTriggerService.RegisterTrigger(ctx, th.LogEmitterRegRequest) require.NoError(t, err) @@ -77,6 +82,4 @@ func TestLogEventTriggerEVMHappyPath(t *testing.T) { actualLogVal, err := testutils.GetBigIntValL2(output, "Data", "Arg0") require.NoError(t, err) require.Equal(t, expectedLogVal, actualLogVal.Int64()) - - logEventTriggerService.Close() } diff --git a/core/services/relay/evm/capabilities/testutils/chain_reader.go b/core/services/relay/evm/capabilities/testutils/chain_reader.go index 4d5cea92f93..3f0bf82da81 100644 --- a/core/services/relay/evm/capabilities/testutils/chain_reader.go +++ b/core/services/relay/evm/capabilities/testutils/chain_reader.go @@ -78,7 +78,8 @@ func NewContractReaderTH(t *testing.T) *ContractReaderTH { // Encode the config map as JSON to specify in the expected call in mocked object // The LogEventTrigger Capability receives a config map, encodes it and // calls NewContractReader with it - contractReaderCfgBytes, _ = json.Marshal(contractReaderCfgMap) + contractReaderCfgBytes, err = json.Marshal(contractReaderCfgMap) + require.NoError(t, err) reqConfig.ContractReaderConfig = contractReaderCfgMap From ecd045eb49c1e0f5819590705acb610a8e8fcf4a Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Mon, 30 Sep 2024 20:00:14 +0100 Subject: [PATCH 35/36] Wait for all goroutines to exit in test --- .../relay/evm/capabilities/log_event_trigger_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/services/relay/evm/capabilities/log_event_trigger_test.go b/core/services/relay/evm/capabilities/log_event_trigger_test.go index 1638c54782c..f2104529b7f 100644 --- a/core/services/relay/evm/capabilities/log_event_trigger_test.go +++ b/core/services/relay/evm/capabilities/log_event_trigger_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -61,10 +62,13 @@ func TestLogEventTriggerEVMHappyPath(t *testing.T) { expectedLogVal := int64(10) // Send a blockchain transaction that emits logs + done := make(chan struct{}) + t.Cleanup(func() { <-done }) go func() { + defer close(done) _, err = th.LogEmitterContract.EmitLog1(th.BackendTH.ContractsOwner, []*big.Int{big.NewInt(expectedLogVal)}) - require.NoError(t, err) + assert.NoError(t, err) th.BackendTH.Backend.Commit() th.BackendTH.Backend.Commit() th.BackendTH.Backend.Commit() From cd33c86e4bf533fb071b1aad37dd0405a2ad7128 Mon Sep 17 00:00:00 2001 From: Sri Kidambi <1702865+kidambisrinivas@users.noreply.github.com> Date: Mon, 30 Sep 2024 20:42:53 +0100 Subject: [PATCH 36/36] Cleanup logpoller and headtracker after test --- core/services/relay/evm/capabilities/testutils/backend.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/services/relay/evm/capabilities/testutils/backend.go b/core/services/relay/evm/capabilities/testutils/backend.go index be8107c2973..ef5761b3e4c 100644 --- a/core/services/relay/evm/capabilities/testutils/backend.go +++ b/core/services/relay/evm/capabilities/testutils/backend.go @@ -100,6 +100,8 @@ func (th *EVMBackendTH) SetupCoreServices(t *testing.T) (logpoller.HeadTracker, ) require.NoError(t, ht.Start(testutils.Context(t))) require.NoError(t, lp.Start(testutils.Context(t))) + t.Cleanup(func() { ht.Close() }) + t.Cleanup(func() { lp.Close() }) return ht, lp }