From 69f7bd68199b91c4ef4be65b5701e4d45a250350 Mon Sep 17 00:00:00 2001 From: Jordan Krage Date: Tue, 6 Aug 2024 23:55:49 +0200 Subject: [PATCH 1/9] use services.Config.NewService/Engine (#13851) * use services.Config.NewService/Engine * feedback --- common/headtracker/head_broadcaster.go | 70 +++---- common/headtracker/head_listener.go | 73 ++++--- common/headtracker/head_tracker.go | 105 ++++------ core/bridges/cache.go | 76 ++----- core/chains/evm/headtracker/head_listener.go | 28 --- .../evm/headtracker/head_listener_test.go | 188 ++++++++--------- core/chains/evm/headtracker/head_tracker.go | 4 +- core/chains/evm/monitor/balance.go | 76 +++---- core/recovery/recover.go | 42 ++-- core/services/chainlink/application.go | 13 +- .../fluxmonitorv2/deviation_checker.go | 4 +- core/services/fluxmonitorv2/flux_monitor.go | 95 ++++----- .../fluxmonitorv2/flux_monitor_test.go | 16 +- core/services/fluxmonitorv2/helpers_test.go | 12 +- core/services/fluxmonitorv2/poll_manager.go | 10 +- core/services/nurse.go | 192 ++++++++---------- core/services/nurse_test.go | 3 +- .../relay/evm/functions/logpoller_wrapper.go | 116 +++++------ core/services/synchronization/helpers_test.go | 6 +- .../telemetry_ingress_batch_client.go | 133 ++++++------ .../telemetry_ingress_batch_worker.go | 64 ++---- .../telemetry_ingress_batch_worker_test.go | 4 - .../telemetry_ingress_client.go | 90 +++----- core/services/telemetry/manager.go | 92 ++++----- core/services/telemetry/manager_test.go | 2 +- 25 files changed, 608 insertions(+), 906 deletions(-) delete mode 100644 core/chains/evm/headtracker/head_listener.go diff --git a/common/headtracker/head_broadcaster.go b/common/headtracker/head_broadcaster.go index 7edcccfccbd..c81c61141f2 100644 --- a/common/headtracker/head_broadcaster.go +++ b/common/headtracker/head_broadcaster.go @@ -42,13 +42,12 @@ type HeadBroadcaster[H types.Head[BLOCK_HASH], BLOCK_HASH types.Hashable] interf } type headBroadcaster[H types.Head[BLOCK_HASH], BLOCK_HASH types.Hashable] struct { - services.StateMachine - logger logger.Logger + services.Service + eng *services.Engine + callbacks callbackSet[H, BLOCK_HASH] mailbox *mailbox.Mailbox[H] mutex sync.Mutex - chClose services.StopChan - wgDone sync.WaitGroup latest H lastCallbackID int } @@ -60,41 +59,29 @@ func NewHeadBroadcaster[ ]( lggr logger.Logger, ) HeadBroadcaster[H, BLOCK_HASH] { - return &headBroadcaster[H, BLOCK_HASH]{ - logger: logger.Named(lggr, "HeadBroadcaster"), + hb := &headBroadcaster[H, BLOCK_HASH]{ callbacks: make(callbackSet[H, BLOCK_HASH]), mailbox: mailbox.NewSingle[H](), - chClose: make(chan struct{}), } + hb.Service, hb.eng = services.Config{ + Name: "HeadBroadcaster", + Start: hb.start, + Close: hb.close, + }.NewServiceEngine(lggr) + return hb } -func (hb *headBroadcaster[H, BLOCK_HASH]) Start(context.Context) error { - return hb.StartOnce("HeadBroadcaster", func() error { - hb.wgDone.Add(1) - go hb.run() - return nil - }) -} - -func (hb *headBroadcaster[H, BLOCK_HASH]) Close() error { - return hb.StopOnce("HeadBroadcaster", func() error { - hb.mutex.Lock() - // clear all callbacks - hb.callbacks = make(callbackSet[H, BLOCK_HASH]) - hb.mutex.Unlock() - - close(hb.chClose) - hb.wgDone.Wait() - return nil - }) +func (hb *headBroadcaster[H, BLOCK_HASH]) start(context.Context) error { + hb.eng.Go(hb.run) + return nil } -func (hb *headBroadcaster[H, BLOCK_HASH]) Name() string { - return hb.logger.Name() -} - -func (hb *headBroadcaster[H, BLOCK_HASH]) HealthReport() map[string]error { - return map[string]error{hb.Name(): hb.Healthy()} +func (hb *headBroadcaster[H, BLOCK_HASH]) close() error { + hb.mutex.Lock() + // clear all callbacks + hb.callbacks = make(callbackSet[H, BLOCK_HASH]) + hb.mutex.Unlock() + return nil } func (hb *headBroadcaster[H, BLOCK_HASH]) BroadcastNewLongestChain(head H) { @@ -121,15 +108,13 @@ func (hb *headBroadcaster[H, BLOCK_HASH]) Subscribe(callback HeadTrackable[H, BL return } -func (hb *headBroadcaster[H, BLOCK_HASH]) run() { - defer hb.wgDone.Done() - +func (hb *headBroadcaster[H, BLOCK_HASH]) run(ctx context.Context) { for { select { - case <-hb.chClose: + case <-ctx.Done(): return case <-hb.mailbox.Notify(): - hb.executeCallbacks() + hb.executeCallbacks(ctx) } } } @@ -137,10 +122,10 @@ func (hb *headBroadcaster[H, BLOCK_HASH]) run() { // DEV: the head relayer makes no promises about head delivery! Subscribing // Jobs should expect to the relayer to skip heads if there is a large number of listeners // and all callbacks cannot be completed in the allotted time. -func (hb *headBroadcaster[H, BLOCK_HASH]) executeCallbacks() { +func (hb *headBroadcaster[H, BLOCK_HASH]) executeCallbacks(ctx context.Context) { head, exists := hb.mailbox.Retrieve() if !exists { - hb.logger.Info("No head to retrieve. It might have been skipped") + hb.eng.Info("No head to retrieve. It might have been skipped") return } @@ -149,7 +134,7 @@ func (hb *headBroadcaster[H, BLOCK_HASH]) executeCallbacks() { hb.latest = head hb.mutex.Unlock() - hb.logger.Debugw("Initiating callbacks", + hb.eng.Debugw("Initiating callbacks", "headNum", head.BlockNumber(), "numCallbacks", len(callbacks), ) @@ -157,9 +142,6 @@ func (hb *headBroadcaster[H, BLOCK_HASH]) executeCallbacks() { wg := sync.WaitGroup{} wg.Add(len(callbacks)) - ctx, cancel := hb.chClose.NewCtx() - defer cancel() - for _, callback := range callbacks { go func(trackable HeadTrackable[H, BLOCK_HASH]) { defer wg.Done() @@ -168,7 +150,7 @@ func (hb *headBroadcaster[H, BLOCK_HASH]) executeCallbacks() { defer cancel() trackable.OnNewLongestChain(cctx, head) elapsed := time.Since(start) - hb.logger.Debugw(fmt.Sprintf("Finished callback in %s", elapsed), + hb.eng.Debugw(fmt.Sprintf("Finished callback in %s", elapsed), "callbackType", reflect.TypeOf(trackable), "blockNumber", head.BlockNumber(), "time", elapsed) }(callback) } diff --git a/common/headtracker/head_listener.go b/common/headtracker/head_listener.go index 25715b35280..d240caab3c3 100644 --- a/common/headtracker/head_listener.go +++ b/common/headtracker/head_listener.go @@ -29,14 +29,15 @@ var ( }, []string{"ChainID"}) ) -// headHandler is a callback that handles incoming heads -type headHandler[H types.Head[BLOCK_HASH], BLOCK_HASH types.Hashable] func(ctx context.Context, header H) error +// HeadHandler is a callback that handles incoming heads +type HeadHandler[H types.Head[BLOCK_HASH], BLOCK_HASH types.Hashable] func(ctx context.Context, header H) error // HeadListener is a chain agnostic interface that manages connection of Client that receives heads from the blockchain node type HeadListener[H types.Head[BLOCK_HASH], BLOCK_HASH types.Hashable] interface { - // ListenForNewHeads kicks off the listen loop (not thread safe) - // done() must be executed upon leaving ListenForNewHeads() - ListenForNewHeads(onSubscribe func(), handleNewHead headHandler[H, BLOCK_HASH], done func()) + services.Service + + // ListenForNewHeads runs the listen loop (not thread safe) + ListenForNewHeads(ctx context.Context) // ReceivingHeads returns true if the listener is receiving heads (thread safe) ReceivingHeads() bool @@ -54,10 +55,13 @@ type headListener[ ID types.ID, BLOCK_HASH types.Hashable, ] struct { + services.Service + eng *services.Engine + config htrktypes.Config client htrktypes.Client[HTH, S, ID, BLOCK_HASH] - logger logger.Logger - chStop services.StopChan + onSubscription func(context.Context) + handleNewHead HeadHandler[HTH, BLOCK_HASH] chHeaders chan HTH headSubscription types.Subscription connected atomic.Bool @@ -74,38 +78,43 @@ func NewHeadListener[ lggr logger.Logger, client CLIENT, config htrktypes.Config, - chStop chan struct{}, + onSubscription func(context.Context), + handleNewHead HeadHandler[HTH, BLOCK_HASH], ) HeadListener[HTH, BLOCK_HASH] { - return &headListener[HTH, S, ID, BLOCK_HASH]{ - config: config, - client: client, - logger: logger.Named(lggr, "HeadListener"), - chStop: chStop, + hl := &headListener[HTH, S, ID, BLOCK_HASH]{ + config: config, + client: client, + onSubscription: onSubscription, + handleNewHead: handleNewHead, } + hl.Service, hl.eng = services.Config{ + Name: "HeadListener", + Start: hl.start, + }.NewServiceEngine(lggr) + return hl } -func (hl *headListener[HTH, S, ID, BLOCK_HASH]) Name() string { - return hl.logger.Name() +func (hl *headListener[HTH, S, ID, BLOCK_HASH]) start(context.Context) error { + hl.eng.Go(hl.ListenForNewHeads) + return nil } -func (hl *headListener[HTH, S, ID, BLOCK_HASH]) ListenForNewHeads(onSubscription func(), handleNewHead headHandler[HTH, BLOCK_HASH], done func()) { - defer done() +func (hl *headListener[HTH, S, ID, BLOCK_HASH]) ListenForNewHeads(ctx context.Context) { defer hl.unsubscribe() - ctx, cancel := hl.chStop.NewCtx() - defer cancel() - for { if !hl.subscribe(ctx) { break } - onSubscription() - err := hl.receiveHeaders(ctx, handleNewHead) + if hl.onSubscription != nil { + hl.onSubscription(ctx) + } + err := hl.receiveHeaders(ctx, hl.handleNewHead) if ctx.Err() != nil { break } else if err != nil { - hl.logger.Errorw("Error in new head subscription, unsubscribed", "err", err) + hl.eng.Errorw("Error in new head subscription, unsubscribed", "err", err) continue } break @@ -131,7 +140,7 @@ func (hl *headListener[HTH, S, ID, BLOCK_HASH]) HealthReport() map[string]error return map[string]error{hl.Name(): err} } -func (hl *headListener[HTH, S, ID, BLOCK_HASH]) receiveHeaders(ctx context.Context, handleNewHead headHandler[HTH, BLOCK_HASH]) error { +func (hl *headListener[HTH, S, ID, BLOCK_HASH]) receiveHeaders(ctx context.Context, handleNewHead HeadHandler[HTH, BLOCK_HASH]) error { var noHeadsAlarmC <-chan time.Time var noHeadsAlarmT *time.Ticker noHeadsAlarmDuration := hl.config.BlockEmissionIdleWarningThreshold() @@ -142,7 +151,7 @@ func (hl *headListener[HTH, S, ID, BLOCK_HASH]) receiveHeaders(ctx context.Conte for { select { - case <-hl.chStop: + case <-ctx.Done(): return nil case blockHeader, open := <-hl.chHeaders: @@ -158,13 +167,13 @@ func (hl *headListener[HTH, S, ID, BLOCK_HASH]) receiveHeaders(ctx context.Conte return errors.New("head listener: chHeaders prematurely closed") } if !blockHeader.IsValid() { - hl.logger.Error("got nil block header") + hl.eng.Error("got nil block header") continue } // Compare the chain ID of the block header to the chain ID of the client if !blockHeader.HasChainID() || blockHeader.ChainID().String() != chainId.String() { - hl.logger.Panicf("head listener for %s received block header for %s", chainId, blockHeader.ChainID()) + hl.eng.Panicf("head listener for %s received block header for %s", chainId, blockHeader.ChainID()) } promNumHeadsReceived.WithLabelValues(chainId.String()).Inc() @@ -184,7 +193,7 @@ func (hl *headListener[HTH, S, ID, BLOCK_HASH]) receiveHeaders(ctx context.Conte case <-noHeadsAlarmC: // We haven't received a head on the channel for a long time, log a warning - hl.logger.Warnf("have not received a head for %v", noHeadsAlarmDuration) + hl.eng.Warnf("have not received a head for %v", noHeadsAlarmDuration) hl.receivingHeads.Store(false) } } @@ -198,19 +207,19 @@ func (hl *headListener[HTH, S, ID, BLOCK_HASH]) subscribe(ctx context.Context) b for { hl.unsubscribe() - hl.logger.Debugf("Subscribing to new heads on chain %s", chainId.String()) + hl.eng.Debugf("Subscribing to new heads on chain %s", chainId.String()) select { - case <-hl.chStop: + case <-ctx.Done(): return false case <-time.After(subscribeRetryBackoff.Duration()): err := hl.subscribeToHead(ctx) if err != nil { promEthConnectionErrors.WithLabelValues(chainId.String()).Inc() - hl.logger.Warnw("Failed to subscribe to heads on chain", "chainID", chainId.String(), "err", err) + hl.eng.Warnw("Failed to subscribe to heads on chain", "chainID", chainId.String(), "err", err) } else { - hl.logger.Debugf("Subscribed to heads on chain %s", chainId.String()) + hl.eng.Debugf("Subscribed to heads on chain %s", chainId.String()) return true } } diff --git a/common/headtracker/head_tracker.go b/common/headtracker/head_tracker.go index 851458591b8..8546d856b67 100644 --- a/common/headtracker/head_tracker.go +++ b/common/headtracker/head_tracker.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "math/big" - "sync" "time" "github.com/prometheus/client_golang/prometheus" @@ -51,7 +50,9 @@ type headTracker[ ID types.ID, BLOCK_HASH types.Hashable, ] struct { - services.StateMachine + services.Service + eng *services.Engine + log logger.SugaredLogger headBroadcaster HeadBroadcaster[HTH, BLOCK_HASH] headSaver HeadSaver[HTH, BLOCK_HASH] @@ -64,8 +65,6 @@ type headTracker[ backfillMB *mailbox.Mailbox[HTH] broadcastMB *mailbox.Mailbox[HTH] headListener HeadListener[HTH, BLOCK_HASH] - chStop services.StopChan - wgDone sync.WaitGroup getNilHead func() HTH } @@ -85,52 +84,52 @@ func NewHeadTracker[ mailMon *mailbox.Monitor, getNilHead func() HTH, ) HeadTracker[HTH, BLOCK_HASH] { - chStop := make(chan struct{}) - lggr = logger.Named(lggr, "HeadTracker") - return &headTracker[HTH, S, ID, BLOCK_HASH]{ + ht := &headTracker[HTH, S, ID, BLOCK_HASH]{ headBroadcaster: headBroadcaster, client: client, chainID: client.ConfiguredChainID(), config: config, htConfig: htConfig, - log: logger.Sugared(lggr), backfillMB: mailbox.NewSingle[HTH](), broadcastMB: mailbox.New[HTH](HeadsBufferSize), - chStop: chStop, - headListener: NewHeadListener[HTH, S, ID, BLOCK_HASH](lggr, client, config, chStop), headSaver: headSaver, mailMon: mailMon, getNilHead: getNilHead, } + ht.Service, ht.eng = services.Config{ + Name: "HeadTracker", + NewSubServices: func(lggr logger.Logger) []services.Service { + ht.headListener = NewHeadListener[HTH, S, ID, BLOCK_HASH](lggr, client, config, + // NOTE: Always try to start the head tracker off with whatever the + // latest head is, without waiting for the subscription to send us one. + // + // In some cases the subscription will send us the most recent head + // anyway when we connect (but we should not rely on this because it is + // not specced). If it happens this is fine, and the head will be + // ignored as a duplicate. + func(ctx context.Context) { + err := ht.handleInitialHead(ctx) + if err != nil { + ht.log.Errorw("Error handling initial head", "err", err.Error()) + } + }, ht.handleNewHead) + return []services.Service{ht.headListener} + }, + Start: ht.start, + Close: ht.close, + }.NewServiceEngine(lggr) + ht.log = logger.Sugared(ht.eng) + return ht } // Start starts HeadTracker service. -func (ht *headTracker[HTH, S, ID, BLOCK_HASH]) Start(ctx context.Context) error { - return ht.StartOnce("HeadTracker", func() error { - ht.log.Debugw("Starting HeadTracker", "chainID", ht.chainID) - // NOTE: Always try to start the head tracker off with whatever the - // latest head is, without waiting for the subscription to send us one. - // - // In some cases the subscription will send us the most recent head - // anyway when we connect (but we should not rely on this because it is - // not specced). If it happens this is fine, and the head will be - // ignored as a duplicate. - onSubscribe := func() { - err := ht.handleInitialHead(ctx) - if err != nil { - ht.log.Errorw("Error handling initial head", "err", err.Error()) - } - } +func (ht *headTracker[HTH, S, ID, BLOCK_HASH]) start(context.Context) error { + ht.eng.Go(ht.backfillLoop) + ht.eng.Go(ht.broadcastLoop) - ht.wgDone.Add(3) - go ht.headListener.ListenForNewHeads(onSubscribe, ht.handleNewHead, ht.wgDone.Done) - go ht.backfillLoop() - go ht.broadcastLoop() + ht.mailMon.Monitor(ht.broadcastMB, "HeadTracker", "Broadcast", ht.chainID.String()) - ht.mailMon.Monitor(ht.broadcastMB, "HeadTracker", "Broadcast", ht.chainID.String()) - - return nil - }) + return nil } func (ht *headTracker[HTH, S, ID, BLOCK_HASH]) handleInitialHead(ctx context.Context) error { @@ -176,23 +175,8 @@ func (ht *headTracker[HTH, S, ID, BLOCK_HASH]) handleInitialHead(ctx context.Con return nil } -// Close stops HeadTracker service. -func (ht *headTracker[HTH, S, ID, BLOCK_HASH]) Close() error { - return ht.StopOnce("HeadTracker", func() error { - close(ht.chStop) - ht.wgDone.Wait() - return ht.broadcastMB.Close() - }) -} - -func (ht *headTracker[HTH, S, ID, BLOCK_HASH]) Name() string { - return ht.log.Name() -} - -func (ht *headTracker[HTH, S, ID, BLOCK_HASH]) HealthReport() map[string]error { - report := map[string]error{ht.Name(): ht.Healthy()} - services.CopyHealth(report, ht.headListener.HealthReport()) - return report +func (ht *headTracker[HTH, S, ID, BLOCK_HASH]) close() error { + return ht.broadcastMB.Close() } func (ht *headTracker[HTH, S, ID, BLOCK_HASH]) Backfill(ctx context.Context, headWithChain HTH) (err error) { @@ -265,15 +249,13 @@ func (ht *headTracker[HTH, S, ID, BLOCK_HASH]) handleNewHead(ctx context.Context promOldHead.WithLabelValues(ht.chainID.String()).Inc() err := fmt.Errorf("got very old block with number %d (highest seen was %d)", head.BlockNumber(), prevHead.BlockNumber()) ht.log.Critical("Got very old block. Either a very deep re-org occurred, one of the RPC nodes has gotten far out of sync, or the chain went backwards in block numbers. This node may not function correctly without manual intervention.", "err", err) - ht.SvcErrBuffer.Append(err) + ht.eng.EmitHealthErr(err) } } return nil } -func (ht *headTracker[HTH, S, ID, BLOCK_HASH]) broadcastLoop() { - defer ht.wgDone.Done() - +func (ht *headTracker[HTH, S, ID, BLOCK_HASH]) broadcastLoop(ctx context.Context) { samplingInterval := ht.htConfig.SamplingInterval() if samplingInterval > 0 { ht.log.Debugf("Head sampling is enabled - sampling interval is set to: %v", samplingInterval) @@ -281,7 +263,7 @@ func (ht *headTracker[HTH, S, ID, BLOCK_HASH]) broadcastLoop() { defer debounceHead.Stop() for { select { - case <-ht.chStop: + case <-ctx.Done(): return case <-debounceHead.C: item := ht.broadcastMB.RetrieveLatestAndClear() @@ -295,7 +277,7 @@ func (ht *headTracker[HTH, S, ID, BLOCK_HASH]) broadcastLoop() { ht.log.Info("Head sampling is disabled - callback will be called on every head") for { select { - case <-ht.chStop: + case <-ctx.Done(): return case <-ht.broadcastMB.Notify(): for { @@ -310,15 +292,10 @@ func (ht *headTracker[HTH, S, ID, BLOCK_HASH]) broadcastLoop() { } } -func (ht *headTracker[HTH, S, ID, BLOCK_HASH]) backfillLoop() { - defer ht.wgDone.Done() - - ctx, cancel := ht.chStop.NewCtx() - defer cancel() - +func (ht *headTracker[HTH, S, ID, BLOCK_HASH]) backfillLoop(ctx context.Context) { for { select { - case <-ht.chStop: + case <-ctx.Done(): return case <-ht.backfillMB.Notify(): for { diff --git a/core/bridges/cache.go b/core/bridges/cache.go index 4b5a6552447..e97874a35e5 100644 --- a/core/bridges/cache.go +++ b/core/bridges/cache.go @@ -10,11 +10,9 @@ import ( "golang.org/x/exp/maps" + "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/sqlutil" - - "github.com/smartcontractkit/chainlink/v2/core/logger" - "github.com/smartcontractkit/chainlink/v2/core/utils" ) const ( @@ -25,13 +23,11 @@ const ( type Cache struct { // dependencies and configurations ORM - lggr logger.Logger interval time.Duration // service state - services.StateMachine - wg sync.WaitGroup - chStop services.StopChan + services.Service + eng *services.Engine // data state bridgeTypesCache sync.Map @@ -43,17 +39,20 @@ var _ ORM = (*Cache)(nil) var _ services.Service = (*Cache)(nil) func NewCache(base ORM, lggr logger.Logger, upsertInterval time.Duration) *Cache { - return &Cache{ + c := &Cache{ ORM: base, - lggr: lggr.Named(CacheServiceName), interval: upsertInterval, - chStop: make(chan struct{}), bridgeLastValueCache: make(map[string]BridgeResponse), } + c.Service, c.eng = services.Config{ + Name: CacheServiceName, + Start: c.start, + }.NewServiceEngine(lggr) + return c } func (c *Cache) WithDataSource(ds sqlutil.DataSource) ORM { - return NewCache(NewORM(ds), c.lggr, c.interval) + return NewCache(NewORM(ds), c.eng, c.interval) } func (c *Cache) FindBridge(ctx context.Context, name BridgeName) (BridgeType, error) { @@ -190,51 +189,17 @@ func (c *Cache) UpsertBridgeResponse(ctx context.Context, dotId string, specId i return nil } -func (c *Cache) Start(_ context.Context) error { - return c.StartOnce(CacheServiceName, func() error { - c.wg.Add(1) - - go c.run() - - return nil - }) -} - -func (c *Cache) Close() error { - return c.StopOnce(CacheServiceName, func() error { - close(c.chStop) - c.wg.Wait() - - return nil - }) -} - -func (c *Cache) HealthReport() map[string]error { - return map[string]error{c.Name(): c.Healthy()} -} - -func (c *Cache) Name() string { - return c.lggr.Name() -} - -func (c *Cache) run() { - defer c.wg.Done() - - for { - timer := time.NewTimer(utils.WithJitter(c.interval)) +func (c *Cache) start(_ context.Context) error { + ticker := services.TickerConfig{ + Initial: c.interval, + JitterPct: services.DefaultJitter, + }.NewTicker(c.interval) + c.eng.GoTick(ticker, c.doBulkUpsert) - select { - case <-timer.C: - c.doBulkUpsert() - case <-c.chStop: - timer.Stop() - - return - } - } + return nil } -func (c *Cache) doBulkUpsert() { +func (c *Cache) doBulkUpsert(ctx context.Context) { c.mu.RLock() values := maps.Values(c.bridgeLastValueCache) c.mu.RUnlock() @@ -243,11 +208,8 @@ func (c *Cache) doBulkUpsert() { return } - ctx, cancel := c.chStop.NewCtx() - defer cancel() - if err := c.ORM.BulkUpsertBridgeResponse(ctx, values); err != nil { - c.lggr.Warnf("bulk upsert of bridge responses failed: %s", err.Error()) + c.eng.Warnf("bulk upsert of bridge responses failed: %s", err.Error()) } } diff --git a/core/chains/evm/headtracker/head_listener.go b/core/chains/evm/headtracker/head_listener.go deleted file mode 100644 index 04535a34868..00000000000 --- a/core/chains/evm/headtracker/head_listener.go +++ /dev/null @@ -1,28 +0,0 @@ -package headtracker - -import ( - "math/big" - - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/common" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink/v2/common/headtracker" - - htrktypes "github.com/smartcontractkit/chainlink/v2/common/headtracker/types" - evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" - evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" -) - -type headListener = headtracker.HeadListener[*evmtypes.Head, common.Hash] - -func NewHeadListener( - lggr logger.Logger, - ethClient evmclient.Client, - config htrktypes.Config, chStop chan struct{}, -) headListener { - return headtracker.NewHeadListener[ - *evmtypes.Head, - ethereum.Subscription, *big.Int, common.Hash, - ](lggr, ethClient, config, chStop) -} diff --git a/core/chains/evm/headtracker/head_listener_test.go b/core/chains/evm/headtracker/head_listener_test.go index 29b090bbffe..2e459af2a2b 100644 --- a/core/chains/evm/headtracker/head_listener_test.go +++ b/core/chains/evm/headtracker/head_listener_test.go @@ -16,9 +16,9 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + "github.com/smartcontractkit/chainlink/v2/common/headtracker" commonmocks "github.com/smartcontractkit/chainlink/v2/common/types/mocks" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/toml" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/headtracker" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/testutils" evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" ) @@ -40,17 +40,10 @@ func Test_HeadListener_HappyPath(t *testing.T) { evmcfg := testutils.NewTestChainScopedConfig(t, func(c *toml.EVMConfig) { c.NoNewHeadsThreshold = &commonconfig.Duration{} }) - chStop := make(chan struct{}) - hl := headtracker.NewHeadListener(lggr, ethClient, evmcfg.EVM(), chStop) var headCount atomic.Int32 - handler := func(context.Context, *evmtypes.Head) error { - headCount.Add(1) - return nil - } - - subscribeAwaiter := testutils.NewAwaiter() unsubscribeAwaiter := testutils.NewAwaiter() + subscribeAwaiter := testutils.NewAwaiter() var chHeads chan<- *evmtypes.Head var chErr = make(chan error) var chSubErr <-chan error = chErr @@ -66,23 +59,23 @@ func Test_HeadListener_HappyPath(t *testing.T) { close(chErr) }) - doneAwaiter := testutils.NewAwaiter() - done := func() { - doneAwaiter.ItHappened() - } - go hl.ListenForNewHeads(func() {}, handler, done) - - subscribeAwaiter.AwaitOrFail(t, tests.WaitTimeout(t)) - require.Eventually(t, hl.Connected, tests.WaitTimeout(t), tests.TestInterval) + func() { + hl := headtracker.NewHeadListener(lggr, ethClient, evmcfg.EVM(), nil, func(context.Context, *evmtypes.Head) error { + headCount.Add(1) + return nil + }) + require.NoError(t, hl.Start(tests.Context(t))) + defer func() { assert.NoError(t, hl.Close()) }() - chHeads <- testutils.Head(0) - chHeads <- testutils.Head(1) - chHeads <- testutils.Head(2) + subscribeAwaiter.AwaitOrFail(t, tests.WaitTimeout(t)) + require.Eventually(t, hl.Connected, tests.WaitTimeout(t), tests.TestInterval) - require.True(t, hl.ReceivingHeads()) + chHeads <- testutils.Head(0) + chHeads <- testutils.Head(1) + chHeads <- testutils.Head(2) - close(chStop) - doneAwaiter.AwaitOrFail(t) + require.True(t, hl.ReceivingHeads()) + }() unsubscribeAwaiter.AwaitOrFail(t) require.Equal(t, int32(3), headCount.Load()) @@ -101,14 +94,8 @@ func Test_HeadListener_NotReceivingHeads(t *testing.T) { evmcfg := testutils.NewTestChainScopedConfig(t, func(c *toml.EVMConfig) { c.NoNewHeadsThreshold = commonconfig.MustNewDuration(time.Second) }) - chStop := make(chan struct{}) - hl := headtracker.NewHeadListener(lggr, ethClient, evmcfg.EVM(), chStop) firstHeadAwaiter := testutils.NewAwaiter() - handler := func(context.Context, *evmtypes.Head) error { - firstHeadAwaiter.ItHappened() - return nil - } subscribeAwaiter := testutils.NewAwaiter() var chHeads chan<- *evmtypes.Head @@ -125,25 +112,25 @@ func Test_HeadListener_NotReceivingHeads(t *testing.T) { close(chErr) }) - doneAwaiter := testutils.NewAwaiter() - done := func() { - doneAwaiter.ItHappened() - } - go hl.ListenForNewHeads(func() {}, handler, done) - - subscribeAwaiter.AwaitOrFail(t, tests.WaitTimeout(t)) + func() { + hl := headtracker.NewHeadListener(lggr, ethClient, evmcfg.EVM(), nil, func(context.Context, *evmtypes.Head) error { + firstHeadAwaiter.ItHappened() + return nil + }) + require.NoError(t, hl.Start(tests.Context(t))) + defer func() { assert.NoError(t, hl.Close()) }() - chHeads <- testutils.Head(0) - firstHeadAwaiter.AwaitOrFail(t) + subscribeAwaiter.AwaitOrFail(t, tests.WaitTimeout(t)) - require.True(t, hl.ReceivingHeads()) + chHeads <- testutils.Head(0) + firstHeadAwaiter.AwaitOrFail(t) - time.Sleep(time.Second * 2) + require.True(t, hl.ReceivingHeads()) - require.False(t, hl.ReceivingHeads()) + time.Sleep(time.Second * 2) - close(chStop) - doneAwaiter.AwaitOrFail(t) + require.False(t, hl.ReceivingHeads()) + }() } func Test_HeadListener_SubscriptionErr(t *testing.T) { @@ -161,19 +148,11 @@ func Test_HeadListener_SubscriptionErr(t *testing.T) { for _, test := range cases { test := test t.Run(test.name, func(t *testing.T) { - l := logger.Test(t) + lggr := logger.Test(t) ethClient := testutils.NewEthClientMockWithDefaultChain(t) evmcfg := testutils.NewTestChainScopedConfig(t, nil) - chStop := make(chan struct{}) - hl := headtracker.NewHeadListener(l, ethClient, evmcfg.EVM(), chStop) hnhCalled := make(chan *evmtypes.Head) - hnh := func(_ context.Context, header *evmtypes.Head) error { - hnhCalled <- header - return nil - } - doneAwaiter := testutils.NewAwaiter() - done := doneAwaiter.ItHappened chSubErrTest := make(chan error) var chSubErr <-chan error = chSubErrTest @@ -189,63 +168,66 @@ func Test_HeadListener_SubscriptionErr(t *testing.T) { headsCh = args.Get(1).(chan<- *evmtypes.Head) subscribeAwaiter.ItHappened() }) - go func() { - hl.ListenForNewHeads(func() {}, hnh, done) - }() - - // Put a head on the channel to ensure we test all code paths - subscribeAwaiter.AwaitOrFail(t, tests.WaitTimeout(t)) - head := testutils.Head(0) - headsCh <- head - - h := <-hnhCalled - assert.Equal(t, head, h) - - // Expect a call to unsubscribe on error - sub.On("Unsubscribe").Once().Run(func(_ mock.Arguments) { - close(headsCh) - // geth guarantees that Unsubscribe closes the errors channel - if !test.closeErr { + func() { + hl := headtracker.NewHeadListener(lggr, ethClient, evmcfg.EVM(), nil, func(_ context.Context, header *evmtypes.Head) error { + hnhCalled <- header + return nil + }) + require.NoError(t, hl.Start(tests.Context(t))) + defer func() { assert.NoError(t, hl.Close()) }() + + // Put a head on the channel to ensure we test all code paths + subscribeAwaiter.AwaitOrFail(t, tests.WaitTimeout(t)) + head := testutils.Head(0) + headsCh <- head + + h := <-hnhCalled + assert.Equal(t, head, h) + + // Expect a call to unsubscribe on error + sub.On("Unsubscribe").Once().Run(func(_ mock.Arguments) { + close(headsCh) + // geth guarantees that Unsubscribe closes the errors channel + if !test.closeErr { + close(chSubErrTest) + } + }) + // Expect a resubscribe + chSubErrTest2 := make(chan error) + var chSubErr2 <-chan error = chSubErrTest2 + sub2 := commonmocks.NewSubscription(t) + sub2.On("Err").Return(chSubErr2) + subscribeAwaiter2 := testutils.NewAwaiter() + + var headsCh2 chan<- *evmtypes.Head + ethClient.On("SubscribeNewHead", mock.Anything, mock.AnythingOfType("chan<- *types.Head")).Return(sub2, nil).Once().Run(func(args mock.Arguments) { + headsCh2 = args.Get(1).(chan<- *evmtypes.Head) + subscribeAwaiter2.ItHappened() + }) + + // Sending test error + if test.closeErr { close(chSubErrTest) + } else { + chSubErrTest <- test.err } - }) - // Expect a resubscribe - chSubErrTest2 := make(chan error) - var chSubErr2 <-chan error = chSubErrTest2 - sub2 := commonmocks.NewSubscription(t) - sub2.On("Err").Return(chSubErr2) - subscribeAwaiter2 := testutils.NewAwaiter() - - var headsCh2 chan<- *evmtypes.Head - ethClient.On("SubscribeNewHead", mock.Anything, mock.AnythingOfType("chan<- *types.Head")).Return(sub2, nil).Once().Run(func(args mock.Arguments) { - headsCh2 = args.Get(1).(chan<- *evmtypes.Head) - subscribeAwaiter2.ItHappened() - }) - - // Sending test error - if test.closeErr { - close(chSubErrTest) - } else { - chSubErrTest <- test.err - } - // Wait for it to resubscribe - subscribeAwaiter2.AwaitOrFail(t, tests.WaitTimeout(t)) + // Wait for it to resubscribe + subscribeAwaiter2.AwaitOrFail(t, tests.WaitTimeout(t)) - head2 := testutils.Head(1) - headsCh2 <- head2 + head2 := testutils.Head(1) + headsCh2 <- head2 - h2 := <-hnhCalled - assert.Equal(t, head2, h2) + h2 := <-hnhCalled + assert.Equal(t, head2, h2) - // Second call to unsubscribe on close - sub2.On("Unsubscribe").Once().Run(func(_ mock.Arguments) { - close(headsCh2) - // geth guarantees that Unsubscribe closes the errors channel - close(chSubErrTest2) - }) - close(chStop) - doneAwaiter.AwaitOrFail(t) + // Second call to unsubscribe on close + sub2.On("Unsubscribe").Once().Run(func(_ mock.Arguments) { + close(headsCh2) + // geth guarantees that Unsubscribe closes the errors channel + close(chSubErrTest2) + }) + }() }) } } diff --git a/core/chains/evm/headtracker/head_tracker.go b/core/chains/evm/headtracker/head_tracker.go index d6c2cdc64e7..f7607189f7e 100644 --- a/core/chains/evm/headtracker/head_tracker.go +++ b/core/chains/evm/headtracker/head_tracker.go @@ -2,10 +2,8 @@ package headtracker import ( "context" - "math/big" "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/common" "go.uber.org/zap/zapcore" "github.com/smartcontractkit/chainlink-common/pkg/logger" @@ -27,7 +25,7 @@ func NewHeadTracker( headSaver httypes.HeadSaver, mailMon *mailbox.Monitor, ) httypes.HeadTracker { - return headtracker.NewHeadTracker[*evmtypes.Head, ethereum.Subscription, *big.Int, common.Hash]( + return headtracker.NewHeadTracker[*evmtypes.Head, ethereum.Subscription]( lggr, ethClient, config, diff --git a/core/chains/evm/monitor/balance.go b/core/chains/evm/monitor/balance.go index b8194a38af9..3e28d5c436a 100644 --- a/core/chains/evm/monitor/balance.go +++ b/core/chains/evm/monitor/balance.go @@ -33,14 +33,15 @@ type ( } balanceMonitor struct { - services.StateMachine - logger logger.Logger + services.Service + eng *services.Engine + ethClient evmclient.Client chainID *big.Int chainIDStr string ethKeyStore keystore.Eth ethBalances map[gethCommon.Address]*assets.Eth - ethBalancesMtx *sync.RWMutex + ethBalancesMtx sync.RWMutex sleeperTask *utils.SleeperTask } @@ -53,59 +54,42 @@ var _ BalanceMonitor = (*balanceMonitor)(nil) func NewBalanceMonitor(ethClient evmclient.Client, ethKeyStore keystore.Eth, lggr logger.Logger) *balanceMonitor { chainId := ethClient.ConfiguredChainID() bm := &balanceMonitor{ - services.StateMachine{}, - logger.Named(lggr, "BalanceMonitor"), - ethClient, - chainId, - chainId.String(), - ethKeyStore, - make(map[gethCommon.Address]*assets.Eth), - new(sync.RWMutex), - nil, + ethClient: ethClient, + chainID: chainId, + chainIDStr: chainId.String(), + ethKeyStore: ethKeyStore, + ethBalances: make(map[gethCommon.Address]*assets.Eth), } + bm.Service, bm.eng = services.Config{ + Name: "BalanceMonitor", + Start: bm.start, + Close: bm.close, + }.NewServiceEngine(lggr) bm.sleeperTask = utils.NewSleeperTask(&worker{bm: bm}) return bm } -func (bm *balanceMonitor) Start(ctx context.Context) error { - return bm.StartOnce("BalanceMonitor", func() error { - // Always query latest balance on start - (&worker{bm}).WorkCtx(ctx) - return nil - }) -} - -// Close shuts down the BalanceMonitor, should not be used after this -func (bm *balanceMonitor) Close() error { - return bm.StopOnce("BalanceMonitor", func() error { - return bm.sleeperTask.Stop() - }) -} - -func (bm *balanceMonitor) Ready() error { +func (bm *balanceMonitor) start(ctx context.Context) error { + // Always query latest balance on start + (&worker{bm}).WorkCtx(ctx) return nil } -func (bm *balanceMonitor) Name() string { - return bm.logger.Name() -} - -func (bm *balanceMonitor) HealthReport() map[string]error { - return map[string]error{bm.Name(): bm.Healthy()} +// Close shuts down the BalanceMonitor, should not be used after this +func (bm *balanceMonitor) close() error { + return bm.sleeperTask.Stop() } // OnNewLongestChain checks the balance for each key -func (bm *balanceMonitor) OnNewLongestChain(_ context.Context, head *evmtypes.Head) { - ok := bm.IfStarted(func() { - bm.checkBalance(head) - }) +func (bm *balanceMonitor) OnNewLongestChain(_ context.Context, _ *evmtypes.Head) { + ok := bm.sleeperTask.IfStarted(bm.checkBalances) if !ok { - bm.logger.Debugw("BalanceMonitor: ignoring OnNewLongestChain call, balance monitor is not started", "state", bm.State()) + bm.eng.Debugw("BalanceMonitor: ignoring OnNewLongestChain call, balance monitor is not started", "state", bm.sleeperTask.State()) } } -func (bm *balanceMonitor) checkBalance(head *evmtypes.Head) { - bm.logger.Debugw("BalanceMonitor: signalling balance worker") +func (bm *balanceMonitor) checkBalances() { + bm.eng.Debugw("BalanceMonitor: signalling balance worker") bm.sleeperTask.WakeUp() } @@ -117,7 +101,7 @@ func (bm *balanceMonitor) updateBalance(ethBal assets.Eth, address gethCommon.Ad bm.ethBalances[address] = ðBal bm.ethBalancesMtx.Unlock() - lgr := logger.Named(bm.logger, "BalanceLog") + lgr := logger.Named(bm.eng, "BalanceLog") lgr = logger.With(lgr, "address", address.Hex(), "ethBalance", ethBal.String(), @@ -151,7 +135,7 @@ func (bm *balanceMonitor) promUpdateEthBalance(balance *assets.Eth, from gethCom balanceFloat, err := ApproximateFloat64(balance) if err != nil { - bm.logger.Error(fmt.Errorf("updatePrometheusEthBalance: %v", err)) + bm.eng.Error(fmt.Errorf("updatePrometheusEthBalance: %v", err)) return } @@ -174,7 +158,7 @@ func (w *worker) Work() { func (w *worker) WorkCtx(ctx context.Context) { enabledAddresses, err := w.bm.ethKeyStore.EnabledAddressesForChain(ctx, w.bm.chainID) if err != nil { - w.bm.logger.Error("BalanceMonitor: error getting keys", err) + w.bm.eng.Error("BalanceMonitor: error getting keys", err) } var wg sync.WaitGroup @@ -198,12 +182,12 @@ func (w *worker) checkAccountBalance(ctx context.Context, address gethCommon.Add bal, err := w.bm.ethClient.BalanceAt(ctx, address, nil) if err != nil { - w.bm.logger.Errorw(fmt.Sprintf("BalanceMonitor: error getting balance for key %s", address.Hex()), + w.bm.eng.Errorw(fmt.Sprintf("BalanceMonitor: error getting balance for key %s", address.Hex()), "err", err, "address", address, ) } else if bal == nil { - w.bm.logger.Errorw(fmt.Sprintf("BalanceMonitor: error getting balance for key %s: invariant violation, bal may not be nil", address.Hex()), + w.bm.eng.Errorw(fmt.Sprintf("BalanceMonitor: error getting balance for key %s: invariant violation, bal may not be nil", address.Hex()), "err", err, "address", address, ) diff --git a/core/recovery/recover.go b/core/recovery/recover.go index 8e485abc556..61315defa9a 100644 --- a/core/recovery/recover.go +++ b/core/recovery/recover.go @@ -3,38 +3,38 @@ package recovery import ( "github.com/getsentry/sentry-go" - "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + corelogger "github.com/smartcontractkit/chainlink/v2/core/logger" ) func ReportPanics(fn func()) { - defer func() { - if err := recover(); err != nil { - sentry.CurrentHub().Recover(err) - sentry.Flush(logger.SentryFlushDeadline) + HandleFn(fn, func(err any) { + sentry.CurrentHub().Recover(err) + sentry.Flush(corelogger.SentryFlushDeadline) - panic(err) - } - }() - fn() + panic(err) + }) } func WrapRecover(lggr logger.Logger, fn func()) { - defer func() { - if err := recover(); err != nil { - lggr.Recover(err) + WrapRecoverHandle(lggr, fn, nil) +} + +func WrapRecoverHandle(lggr logger.Logger, fn func(), onPanic func(recovered any)) { + HandleFn(fn, func(recovered any) { + logger.Sugared(lggr).Criticalw("Recovered goroutine panic", "panic", recovered) + + if onPanic != nil { + onPanic(recovered) } - }() - fn() + }) } -func WrapRecoverHandle(lggr logger.Logger, fn func(), onPanic func(interface{})) { +func HandleFn(fn func(), onPanic func(recovered any)) { defer func() { - if err := recover(); err != nil { - lggr.Recover(err) - - if onPanic != nil { - onPanic(err) - } + if recovered := recover(); recovered != nil { + onPanic(recovered) } }() fn() diff --git a/core/services/chainlink/application.go b/core/services/chainlink/application.go index 138ca25ed3b..c23ec08a692 100644 --- a/core/services/chainlink/application.go +++ b/core/services/chainlink/application.go @@ -148,7 +148,6 @@ type ChainlinkApplication struct { shutdownOnce sync.Once srvcs []services.ServiceCtx HealthChecker services.Checker - Nurse *services.Nurse logger logger.SugaredLogger AuditLogger audit.AuditLogger closeLogger func() error @@ -277,14 +276,9 @@ func NewApplication(opts ApplicationOpts) (Application, error) { } ap := cfg.AutoPprof() - var nurse *services.Nurse if ap.Enabled() { globalLogger.Info("Nurse service (automatic pprof profiling) is enabled") - nurse = services.NewNurse(ap, globalLogger) - err := nurse.Start() - if err != nil { - return nil, err - } + srvcs = append(srvcs, services.NewNurse(ap, globalLogger)) } else { globalLogger.Info("Nurse service (automatic pprof profiling) is disabled") } @@ -588,7 +582,6 @@ func NewApplication(opts ApplicationOpts) (Application, error) { SessionReaper: sessionReaper, ExternalInitiatorManager: externalInitiatorManager, HealthChecker: healthChecker, - Nurse: nurse, logger: globalLogger, AuditLogger: auditLogger, closeLogger: opts.CloseLogger, @@ -708,10 +701,6 @@ func (app *ChainlinkApplication) stop() (err error) { err = multierr.Append(err, app.FeedsService.Close()) } - if app.Nurse != nil { - err = multierr.Append(err, app.Nurse.Close()) - } - if app.profiler != nil { err = multierr.Append(err, app.profiler.Stop()) } diff --git a/core/services/fluxmonitorv2/deviation_checker.go b/core/services/fluxmonitorv2/deviation_checker.go index 51e85de371e..9dc399b09f9 100644 --- a/core/services/fluxmonitorv2/deviation_checker.go +++ b/core/services/fluxmonitorv2/deviation_checker.go @@ -3,7 +3,7 @@ package fluxmonitorv2 import ( "github.com/shopspring/decimal" - "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink-common/pkg/logger" ) // DeviationThresholds carries parameters used by the threshold-trigger logic @@ -26,7 +26,7 @@ func NewDeviationChecker(rel, abs float64, lggr logger.Logger) *DeviationChecker Rel: rel, Abs: abs, }, - lggr: lggr.Named("DeviationChecker").With("threshold", rel, "absoluteThreshold", abs), + lggr: logger.Sugared(lggr).Named("DeviationChecker").With("threshold", rel, "absoluteThreshold", abs), } } diff --git a/core/services/fluxmonitorv2/flux_monitor.go b/core/services/fluxmonitorv2/flux_monitor.go index 9175feb1a68..b8154ab6797 100644 --- a/core/services/fluxmonitorv2/flux_monitor.go +++ b/core/services/fluxmonitorv2/flux_monitor.go @@ -13,6 +13,7 @@ import ( "github.com/pkg/errors" "github.com/shopspring/decimal" + "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/sqlutil" @@ -22,7 +23,6 @@ import ( evmutils "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated/flags_wrapper" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated/flux_aggregator_wrapper" - "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/recovery" "github.com/smartcontractkit/chainlink/v2/core/services/fluxmonitorv2/promfm" "github.com/smartcontractkit/chainlink/v2/core/services/job" @@ -56,7 +56,10 @@ const DefaultHibernationPollPeriod = 24 * time.Hour // FluxMonitor polls external price adapters via HTTP to check for price swings. type FluxMonitor struct { - services.StateMachine + services.Service + eng *services.Engine + logger logger.SugaredLogger + contractAddress common.Address oracleAddress common.Address jobSpec job.Job @@ -77,13 +80,8 @@ type FluxMonitor struct { logBroadcaster log.Broadcaster chainID *big.Int - logger logger.SugaredLogger - backlog *utils.BoundedPriorityQueue[log.Broadcast] chProcessLogs chan struct{} - - chStop services.StopChan - waitOnStop chan struct{} } // NewFluxMonitor returns a new instance of PollingDeviationChecker. @@ -105,7 +103,7 @@ func NewFluxMonitor( flags Flags, fluxAggregator flux_aggregator_wrapper.FluxAggregatorInterface, logBroadcaster log.Broadcaster, - fmLogger logger.Logger, + lggr logger.Logger, chainID *big.Int, ) (*FluxMonitor, error) { fm := &FluxMonitor{ @@ -126,7 +124,6 @@ func NewFluxMonitor( flags: flags, logBroadcaster: logBroadcaster, fluxAggregator: fluxAggregator, - logger: logger.Sugared(fmLogger), chainID: chainID, backlog: utils.NewBoundedPriorityQueue[log.Broadcast](map[uint]int{ // We want reconnecting nodes to be able to submit to a round @@ -136,9 +133,13 @@ func NewFluxMonitor( PriorityFlagChangedLog: 2, }), chProcessLogs: make(chan struct{}, 1), - chStop: make(services.StopChan), - waitOnStop: make(chan struct{}), } + fm.Service, fm.eng = services.Config{ + Name: "FluxMonitor", + Start: fm.start, + Close: fm.close, + }.NewServiceEngine(lggr) + fm.logger = logger.Sugared(fm.eng) return fm, nil } @@ -220,7 +221,7 @@ func NewFromJobSpec( return nil, err } - fmLogger := lggr.With( + fmLogger := logger.With(lggr, "jobID", jobSpec.ID, "contract", fmSpec.ContractAddress.Hex(), ) @@ -279,14 +280,9 @@ const ( // Start implements the job.Service interface. It begins the CSP consumer in a // single goroutine to poll the price adapters and listen to NewRound events. -func (fm *FluxMonitor) Start(context.Context) error { - return fm.StartOnce("FluxMonitor", func() error { - fm.logger.Debug("Starting Flux Monitor for job") - - go fm.consume() - - return nil - }) +func (fm *FluxMonitor) start(context.Context) error { + fm.eng.Go(fm.consume) + return nil } func (fm *FluxMonitor) IsHibernating() bool { @@ -304,16 +300,12 @@ func (fm *FluxMonitor) IsHibernating() bool { return !isFlagLowered } -// Close implements the job.Service interface. It stops this instance from +// close stops this instance from // polling, cleaning up resources. -func (fm *FluxMonitor) Close() error { - return fm.StopOnce("FluxMonitor", func() error { - fm.pollManager.Stop() - close(fm.chStop) - <-fm.waitOnStop +func (fm *FluxMonitor) close() error { + fm.pollManager.Stop() - return nil - }) + return nil } // JobID implements the listener.Listener interface. @@ -354,10 +346,8 @@ func (fm *FluxMonitor) HandleLog(ctx context.Context, broadcast log.Broadcast) { } } -func (fm *FluxMonitor) consume() { - defer close(fm.waitOnStop) - - if err := fm.SetOracleAddress(); err != nil { +func (fm *FluxMonitor) consume(ctx context.Context) { + if err := fm.SetOracleAddress(ctx); err != nil { fm.logger.Warnw( "unable to set oracle address, this flux monitor job may not work correctly", "err", err, @@ -398,46 +388,46 @@ func (fm *FluxMonitor) consume() { for { select { - case <-fm.chStop: + case <-ctx.Done(): return case <-fm.chProcessLogs: - recovery.WrapRecover(fm.logger, fm.processLogs) + recovery.WrapRecover(fm.logger, func() { fm.processLogs(ctx) }) case at := <-fm.pollManager.PollTickerTicks(): tickLogger.Debugf("Poll ticker fired on %v", formatTime(at)) recovery.WrapRecover(fm.logger, func() { - fm.pollIfEligible(PollRequestTypePoll, fm.deviationChecker, nil) + fm.pollIfEligible(ctx, PollRequestTypePoll, fm.deviationChecker, nil) }) case at := <-fm.pollManager.IdleTimerTicks(): tickLogger.Debugf("Idle timer fired on %v", formatTime(at)) recovery.WrapRecover(fm.logger, func() { - fm.pollIfEligible(PollRequestTypeIdle, NewZeroDeviationChecker(fm.logger), nil) + fm.pollIfEligible(ctx, PollRequestTypeIdle, NewZeroDeviationChecker(fm.logger), nil) }) case at := <-fm.pollManager.RoundTimerTicks(): tickLogger.Debugf("Round timer fired on %v", formatTime(at)) recovery.WrapRecover(fm.logger, func() { - fm.pollIfEligible(PollRequestTypeRound, fm.deviationChecker, nil) + fm.pollIfEligible(ctx, PollRequestTypeRound, fm.deviationChecker, nil) }) case at := <-fm.pollManager.HibernationTimerTicks(): tickLogger.Debugf("Hibernation timer fired on %v", formatTime(at)) recovery.WrapRecover(fm.logger, func() { - fm.pollIfEligible(PollRequestTypeHibernation, NewZeroDeviationChecker(fm.logger), nil) + fm.pollIfEligible(ctx, PollRequestTypeHibernation, NewZeroDeviationChecker(fm.logger), nil) }) case at := <-fm.pollManager.RetryTickerTicks(): tickLogger.Debugf("Retry ticker fired on %v", formatTime(at)) recovery.WrapRecover(fm.logger, func() { - fm.pollIfEligible(PollRequestTypeRetry, NewZeroDeviationChecker(fm.logger), nil) + fm.pollIfEligible(ctx, PollRequestTypeRetry, NewZeroDeviationChecker(fm.logger), nil) }) case at := <-fm.pollManager.DrumbeatTicks(): tickLogger.Debugf("Drumbeat ticker fired on %v", formatTime(at)) recovery.WrapRecover(fm.logger, func() { - fm.pollIfEligible(PollRequestTypeDrumbeat, NewZeroDeviationChecker(fm.logger), nil) + fm.pollIfEligible(ctx, PollRequestTypeDrumbeat, NewZeroDeviationChecker(fm.logger), nil) }) case request := <-fm.pollManager.Poll(): @@ -446,7 +436,7 @@ func (fm *FluxMonitor) consume() { break default: recovery.WrapRecover(fm.logger, func() { - fm.pollIfEligible(request.Type, fm.deviationChecker, nil) + fm.pollIfEligible(ctx, request.Type, fm.deviationChecker, nil) }) } } @@ -460,11 +450,7 @@ func formatTime(at time.Time) string { // SetOracleAddress sets the oracle address which matches the node's keys. // If none match, it uses the first available key -func (fm *FluxMonitor) SetOracleAddress() error { - // fm on deprecation path, using dangling context - ctx, cancel := fm.chStop.NewCtx() - defer cancel() - +func (fm *FluxMonitor) SetOracleAddress(ctx context.Context) error { oracleAddrs, err := fm.fluxAggregator.GetOracles(nil) if err != nil { fm.logger.Error("failed to get list of oracles from FluxAggregator contract") @@ -502,10 +488,7 @@ func (fm *FluxMonitor) SetOracleAddress() error { return errors.New("No keys found") } -func (fm *FluxMonitor) processLogs() { - ctx, cancel := fm.chStop.NewCtx() - defer cancel() - +func (fm *FluxMonitor) processLogs(ctx context.Context) { for ctx.Err() == nil && !fm.backlog.Empty() { broadcast := fm.backlog.Take() fm.processBroadcast(ctx, broadcast) @@ -529,7 +512,7 @@ func (fm *FluxMonitor) processBroadcast(ctx context.Context, broadcast log.Broad decodedLog := broadcast.DecodedLog() switch log := decodedLog.(type) { case *flux_aggregator_wrapper.FluxAggregatorNewRound: - fm.respondToNewRoundLog(*log, broadcast) + fm.respondToNewRoundLog(ctx, *log, broadcast) case *flux_aggregator_wrapper.FluxAggregatorAnswerUpdated: fm.respondToAnswerUpdatedLog(*log) fm.markLogAsConsumed(ctx, broadcast, decodedLog, started) @@ -540,7 +523,7 @@ func (fm *FluxMonitor) processBroadcast(ctx context.Context, broadcast log.Broad // Only reactivate if it is hibernating if fm.pollManager.isHibernating.Load() { fm.pollManager.Awaken(fm.initialRoundState()) - fm.pollIfEligible(PollRequestTypeAwaken, NewZeroDeviationChecker(fm.logger), broadcast) + fm.pollIfEligible(ctx, PollRequestTypeAwaken, NewZeroDeviationChecker(fm.logger), broadcast) } default: fm.logger.Errorf("unknown log %v of type %T", log, log) @@ -589,10 +572,8 @@ func (fm *FluxMonitor) respondToAnswerUpdatedLog(log flux_aggregator_wrapper.Flu // The NewRound log tells us that an oracle has initiated a new round. This tells us that we // need to poll and submit an answer to the contract regardless of the deviation. -func (fm *FluxMonitor) respondToNewRoundLog(log flux_aggregator_wrapper.FluxAggregatorNewRound, lb log.Broadcast) { +func (fm *FluxMonitor) respondToNewRoundLog(ctx context.Context, log flux_aggregator_wrapper.FluxAggregatorNewRound, lb log.Broadcast) { started := time.Now() - ctx, cancel := fm.chStop.NewCtx() - defer cancel() newRoundLogger := fm.logger.With( "round", log.RoundId, @@ -819,10 +800,8 @@ func (fm *FluxMonitor) checkEligibilityAndAggregatorFunding(roundState flux_aggr return nil } -func (fm *FluxMonitor) pollIfEligible(pollReq PollRequestType, deviationChecker *DeviationChecker, broadcast log.Broadcast) { +func (fm *FluxMonitor) pollIfEligible(ctx context.Context, pollReq PollRequestType, deviationChecker *DeviationChecker, broadcast log.Broadcast) { started := time.Now() - ctx, cancel := fm.chStop.NewCtx() - defer cancel() l := fm.logger.With( "threshold", deviationChecker.Thresholds.Rel, diff --git a/core/services/fluxmonitorv2/flux_monitor_test.go b/core/services/fluxmonitorv2/flux_monitor_test.go index b3a5bcee6b9..1d1ed676e48 100644 --- a/core/services/fluxmonitorv2/flux_monitor_test.go +++ b/core/services/fluxmonitorv2/flux_monitor_test.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/google/uuid" + "github.com/jmoiron/sqlx" "github.com/onsi/gomega" "github.com/pkg/errors" "github.com/shopspring/decimal" @@ -18,11 +19,10 @@ import ( "github.com/stretchr/testify/require" "gopkg.in/guregu/null.v4" - "github.com/jmoiron/sqlx" - "github.com/smartcontractkit/chainlink-common/pkg/assets" "github.com/smartcontractkit/chainlink-common/pkg/services/servicetest" "github.com/smartcontractkit/chainlink-common/pkg/sqlutil" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" txmgrcommon "github.com/smartcontractkit/chainlink/v2/common/txmgr" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/log" logmocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/log/mocks" @@ -491,7 +491,7 @@ func TestFluxMonitor_PollIfEligible(t *testing.T) { oracles := []common.Address{nodeAddr, testutils.NewAddress()} tm.fluxAggregator.On("GetOracles", nilOpts).Return(oracles, nil) - require.NoError(t, fm.SetOracleAddress()) + require.NoError(t, fm.SetOracleAddress(tests.Context(t))) fm.ExportedPollIfEligible(thresholds.rel, thresholds.abs) }) } @@ -526,7 +526,7 @@ func TestFluxMonitor_PollIfEligible_Creates_JobErr(t *testing.T) { Once() tm.fluxAggregator.On("GetOracles", nilOpts).Return(oracles, nil) - require.NoError(t, fm.SetOracleAddress()) + require.NoError(t, fm.SetOracleAddress(tests.Context(t))) fm.ExportedPollIfEligible(1, 1) } @@ -1171,7 +1171,7 @@ func TestFluxMonitor_RoundTimeoutCausesPoll_timesOutAtZero(t *testing.T) { tm.fluxAggregator.On("Address").Return(common.Address{}) tm.fluxAggregator.On("GetOracles", nilOpts).Return(oracles, nil) - require.NoError(t, fm.SetOracleAddress()) + require.NoError(t, fm.SetOracleAddress(tests.Context(t))) fm.ExportedRoundState(t) servicetest.Run(t, fm) @@ -1506,7 +1506,7 @@ func TestFluxMonitor_DoesNotDoubleSubmit(t *testing.T) { Return(nil) tm.fluxAggregator.On("GetOracles", nilOpts).Return(oracles, nil) - require.NoError(t, fm.SetOracleAddress()) + require.NoError(t, fm.SetOracleAddress(tests.Context(t))) tm.fluxAggregator.On("LatestRoundData", nilOpts).Return(flux_aggregator_wrapper.LatestRoundData{ Answer: big.NewInt(10), @@ -1635,7 +1635,7 @@ func TestFluxMonitor_DoesNotDoubleSubmit(t *testing.T) { Once() tm.fluxAggregator.On("GetOracles", nilOpts).Return(oracles, nil) - require.NoError(t, fm.SetOracleAddress()) + require.NoError(t, fm.SetOracleAddress(tests.Context(t))) fm.ExportedPollIfEligible(0, 0) // Now fire off the NewRound log and ensure it does not respond this time @@ -1732,7 +1732,7 @@ func TestFluxMonitor_DoesNotDoubleSubmit(t *testing.T) { Once() tm.fluxAggregator.On("GetOracles", nilOpts).Return(oracles, nil) - require.NoError(t, fm.SetOracleAddress()) + require.NoError(t, fm.SetOracleAddress(tests.Context(t))) fm.ExportedPollIfEligible(0, 0) // Now fire off the NewRound log and ensure it does not respond this time diff --git a/core/services/fluxmonitorv2/helpers_test.go b/core/services/fluxmonitorv2/helpers_test.go index d321ddc35c3..80db82351c7 100644 --- a/core/services/fluxmonitorv2/helpers_test.go +++ b/core/services/fluxmonitorv2/helpers_test.go @@ -19,11 +19,15 @@ func (fm *FluxMonitor) Format(f fmt.State, verb rune) { } func (fm *FluxMonitor) ExportedPollIfEligible(threshold, absoluteThreshold float64) { - fm.pollIfEligible(PollRequestTypePoll, NewDeviationChecker(threshold, absoluteThreshold, fm.logger), nil) + ctx, cancel := fm.eng.NewCtx() + defer cancel() + fm.pollIfEligible(ctx, PollRequestTypePoll, NewDeviationChecker(threshold, absoluteThreshold, fm.logger), nil) } func (fm *FluxMonitor) ExportedProcessLogs() { - fm.processLogs() + ctx, cancel := fm.eng.NewCtx() + defer cancel() + fm.processLogs(ctx) } func (fm *FluxMonitor) ExportedBacklog() *utils.BoundedPriorityQueue[log.Broadcast] { @@ -36,7 +40,9 @@ func (fm *FluxMonitor) ExportedRoundState(t *testing.T) { } func (fm *FluxMonitor) ExportedRespondToNewRoundLog(log *flux_aggregator_wrapper.FluxAggregatorNewRound, broadcast log.Broadcast) { - fm.respondToNewRoundLog(*log, broadcast) + ctx, cancel := fm.eng.NewCtx() + defer cancel() + fm.respondToNewRoundLog(ctx, *log, broadcast) } func (fm *FluxMonitor) ExportedRespondToFlagsRaisedLog() { diff --git a/core/services/fluxmonitorv2/poll_manager.go b/core/services/fluxmonitorv2/poll_manager.go index 78b99aec4d5..aca6c75a311 100644 --- a/core/services/fluxmonitorv2/poll_manager.go +++ b/core/services/fluxmonitorv2/poll_manager.go @@ -5,8 +5,8 @@ import ( "sync/atomic" "time" + "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated/flux_aggregator_wrapper" - "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/utils" ) @@ -64,7 +64,7 @@ type PollManager struct { } // NewPollManager initializes a new PollManager -func NewPollManager(cfg PollManagerConfig, logger logger.Logger) (*PollManager, error) { +func NewPollManager(cfg PollManagerConfig, lggr logger.Logger) (*PollManager, error) { minBackoffDuration := cfg.MinRetryBackoffDuration if cfg.IdleTimerPeriod < minBackoffDuration { minBackoffDuration = cfg.IdleTimerPeriod @@ -82,7 +82,7 @@ func NewPollManager(cfg PollManagerConfig, logger logger.Logger) (*PollManager, p := &PollManager{ cfg: cfg, - logger: logger.Named("PollManager"), + logger: logger.Named(lggr, "PollManager"), hibernationTimer: utils.NewResettableTimer(), pollTicker: utils.NewPausableTicker(cfg.PollTickerInterval), @@ -277,7 +277,7 @@ func (pm *PollManager) startIdleTimer(roundStartedAtUTC uint64) { deadline := startedAt.Add(pm.cfg.IdleTimerPeriod) deadlineDuration := time.Until(deadline) - log := pm.logger.With( + log := logger.With(pm.logger, "pollFrequency", pm.cfg.PollTickerInterval, "idleDuration", pm.cfg.IdleTimerPeriod, "startedAt", roundStartedAtUTC, @@ -300,7 +300,7 @@ func (pm *PollManager) startIdleTimer(roundStartedAtUTC uint64) { // startRoundTimer starts the round timer func (pm *PollManager) startRoundTimer(roundTimesOutAt uint64) { - log := pm.logger.With( + log := logger.With(pm.logger, "pollFrequency", pm.cfg.PollTickerInterval, "idleDuration", pm.cfg.IdleTimerPeriod, "timesOutAt", roundTimesOutAt, diff --git a/core/services/nurse.go b/core/services/nurse.go index a9069b5181d..7f3cad13e71 100644 --- a/core/services/nurse.go +++ b/core/services/nurse.go @@ -3,6 +3,7 @@ package services import ( "bytes" "compress/gzip" + "context" "fmt" "io/fs" "os" @@ -19,22 +20,21 @@ import ( commonconfig "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink-common/pkg/timeutil" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/utils" ) type Nurse struct { - services.StateMachine + services.Service + eng *services.Engine cfg Config - log logger.Logger checks map[string]CheckFunc checksMu sync.RWMutex chGather chan gatherRequest - chStop chan struct{} - wgDone sync.WaitGroup } type Config interface { @@ -66,85 +66,63 @@ const ( ) func NewNurse(cfg Config, log logger.Logger) *Nurse { - return &Nurse{ + n := &Nurse{ cfg: cfg, - log: log.Named("Nurse"), checks: make(map[string]CheckFunc), chGather: make(chan gatherRequest, 1), - chStop: make(chan struct{}), } + n.Service, n.eng = services.Config{ + Name: "Nurse", + Start: n.start, + }.NewServiceEngine(log) + + return n } -func (n *Nurse) Start() error { - return n.StartOnce("Nurse", func() error { - // This must be set *once*, and it must occur as early as possible - if n.cfg.MemProfileRate() != runtime.MemProfileRate { - runtime.MemProfileRate = n.cfg.BlockProfileRate() - } +func (n *Nurse) start(_ context.Context) error { + // This must be set *once*, and it must occur as early as possible + if n.cfg.MemProfileRate() != runtime.MemProfileRate { + runtime.MemProfileRate = n.cfg.BlockProfileRate() + } - n.log.Debugf("Starting nurse with config %+v", n.cfg) - runtime.SetCPUProfileRate(n.cfg.CPUProfileRate()) - runtime.SetBlockProfileRate(n.cfg.BlockProfileRate()) - runtime.SetMutexProfileFraction(n.cfg.MutexProfileFraction()) + n.eng.Debugf("Starting nurse with config %+v", n.cfg) + runtime.SetCPUProfileRate(n.cfg.CPUProfileRate()) + runtime.SetBlockProfileRate(n.cfg.BlockProfileRate()) + runtime.SetMutexProfileFraction(n.cfg.MutexProfileFraction()) - err := utils.EnsureDirAndMaxPerms(n.cfg.ProfileRoot(), 0744) - if err != nil { - return err - } + err := utils.EnsureDirAndMaxPerms(n.cfg.ProfileRoot(), 0744) + if err != nil { + return err + } - n.AddCheck("mem", n.checkMem) - n.AddCheck("goroutines", n.checkGoroutines) - - n.wgDone.Add(1) - // Checker - go func() { - defer n.wgDone.Done() - for { - select { - case <-n.chStop: - return - case <-time.After(n.cfg.PollInterval().Duration()): - } - - func() { - n.checksMu.RLock() - defer n.checksMu.RUnlock() - for reason, checkFunc := range n.checks { - if unwell, meta := checkFunc(); unwell { - n.GatherVitals(reason, meta) - break - } - } - }() - } - }() - - n.wgDone.Add(1) - // Responder - go func() { - defer n.wgDone.Done() - for { - select { - case <-n.chStop: - return - case req := <-n.chGather: - n.gatherVitals(req.reason, req.meta) - } - } - }() + n.AddCheck("mem", n.checkMem) + n.AddCheck("goroutines", n.checkGoroutines) - return nil + // Checker + n.eng.GoTick(timeutil.NewTicker(n.cfg.PollInterval().Duration), func(ctx context.Context) { + n.checksMu.RLock() + defer n.checksMu.RUnlock() + for reason, checkFunc := range n.checks { + if unwell, meta := checkFunc(); unwell { + n.GatherVitals(ctx, reason, meta) + break + } + } }) -} -func (n *Nurse) Close() error { - return n.StopOnce("Nurse", func() error { - n.log.Debug("Nurse closing...") - defer n.log.Debug("Nurse closed") - close(n.chStop) - n.wgDone.Wait() - return nil + // Responder + n.eng.Go(func(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + case req := <-n.chGather: + n.gatherVitals(req.reason, req.meta) + } + } }) + + return nil } func (n *Nurse) AddCheck(reason string, checkFunc CheckFunc) { @@ -153,9 +131,9 @@ func (n *Nurse) AddCheck(reason string, checkFunc CheckFunc) { n.checks[reason] = checkFunc } -func (n *Nurse) GatherVitals(reason string, meta Meta) { +func (n *Nurse) GatherVitals(ctx context.Context, reason string, meta Meta) { select { - case <-n.chStop: + case <-ctx.Done(): case n.chGather <- gatherRequest{reason, meta}: default: } @@ -189,14 +167,14 @@ func (n *Nurse) checkGoroutines() (bool, Meta) { func (n *Nurse) gatherVitals(reason string, meta Meta) { loggerFields := (logger.Fields{"reason": reason}).Merge(logger.Fields(meta)) - n.log.Debugw("Nurse is gathering vitals", loggerFields.Slice()...) + n.eng.Debugw("Nurse is gathering vitals", loggerFields.Slice()...) size, err := n.totalProfileBytes() if err != nil { - n.log.Errorw("could not fetch total profile bytes", loggerFields.With("err", err).Slice()...) + n.eng.Errorw("could not fetch total profile bytes", loggerFields.With("err", err).Slice()...) return } else if size >= uint64(n.cfg.MaxProfileSize()) { - n.log.Warnw("cannot write pprof profile, total profile size exceeds configured PPROF_MAX_PROFILE_SIZE", + n.eng.Warnw("cannot write pprof profile, total profile size exceeds configured PPROF_MAX_PROFILE_SIZE", loggerFields.With("total", size, "max", n.cfg.MaxProfileSize()).Slice()..., ) return @@ -206,7 +184,7 @@ func (n *Nurse) gatherVitals(reason string, meta Meta) { err = n.appendLog(now, reason, meta) if err != nil { - n.log.Warnw("cannot write pprof profile", loggerFields.With("err", err).Slice()...) + n.eng.Warnw("cannot write pprof profile", loggerFields.With("err", err).Slice()...) return } var wg sync.WaitGroup @@ -227,7 +205,7 @@ func (n *Nurse) gatherVitals(reason string, meta Meta) { wg.Add(1) go n.gather("heap", now, &wg) } else { - n.log.Info("skipping heap collection because runtime.MemProfileRate = 0") + n.eng.Info("skipping heap collection because runtime.MemProfileRate = 0") } wg.Add(1) @@ -236,15 +214,13 @@ func (n *Nurse) gatherVitals(reason string, meta Meta) { go n.gather("threadcreate", now, &wg) ch := make(chan struct{}) - n.wgDone.Add(1) - go func() { - defer n.wgDone.Done() + n.eng.Go(func(ctx context.Context) { defer close(ch) wg.Wait() - }() + }) select { - case <-n.chStop: + case <-n.eng.StopChan: case <-ch: } } @@ -252,7 +228,7 @@ func (n *Nurse) gatherVitals(reason string, meta Meta) { func (n *Nurse) appendLog(now time.Time, reason string, meta Meta) error { filename := filepath.Join(n.cfg.ProfileRoot(), "nurse.log") - n.log.Debugf("creating nurse log %s", filename) + n.eng.Debugf("creating nurse log %s", filename) file, err := os.Create(filename) if err != nil { @@ -288,34 +264,34 @@ func (n *Nurse) appendLog(now time.Time, reason string, meta Meta) error { func (n *Nurse) gatherCPU(now time.Time, wg *sync.WaitGroup) { defer wg.Done() - n.log.Debugf("gather cpu %d ...", now.UnixMicro()) - defer n.log.Debugf("gather cpu %d done", now.UnixMicro()) + n.eng.Debugf("gather cpu %d ...", now.UnixMicro()) + defer n.eng.Debugf("gather cpu %d done", now.UnixMicro()) wc, err := n.createFile(now, cpuProfName, false) if err != nil { - n.log.Errorw("could not write cpu profile", "err", err) + n.eng.Errorw("could not write cpu profile", "err", err) return } defer wc.Close() err = pprof.StartCPUProfile(wc) if err != nil { - n.log.Errorw("could not start cpu profile", "err", err) + n.eng.Errorw("could not start cpu profile", "err", err) return } select { - case <-n.chStop: - n.log.Debug("gather cpu received stop") + case <-n.eng.StopChan: + n.eng.Debug("gather cpu received stop") case <-time.After(n.cfg.GatherDuration().Duration()): - n.log.Debugf("gather cpu duration elapsed %s. stoping profiling.", n.cfg.GatherDuration().Duration().String()) + n.eng.Debugf("gather cpu duration elapsed %s. stoping profiling.", n.cfg.GatherDuration().Duration().String()) } pprof.StopCPUProfile() err = wc.Close() if err != nil { - n.log.Errorw("could not close cpu profile", "err", err) + n.eng.Errorw("could not close cpu profile", "err", err) return } } @@ -323,23 +299,23 @@ func (n *Nurse) gatherCPU(now time.Time, wg *sync.WaitGroup) { func (n *Nurse) gatherTrace(now time.Time, wg *sync.WaitGroup) { defer wg.Done() - n.log.Debugf("gather trace %d ...", now.UnixMicro()) - defer n.log.Debugf("gather trace %d done", now.UnixMicro()) + n.eng.Debugf("gather trace %d ...", now.UnixMicro()) + defer n.eng.Debugf("gather trace %d done", now.UnixMicro()) wc, err := n.createFile(now, traceProfName, true) if err != nil { - n.log.Errorw("could not write trace profile", "err", err) + n.eng.Errorw("could not write trace profile", "err", err) return } defer wc.Close() err = trace.Start(wc) if err != nil { - n.log.Errorw("could not start trace profile", "err", err) + n.eng.Errorw("could not start trace profile", "err", err) return } select { - case <-n.chStop: + case <-n.eng.StopChan: case <-time.After(n.cfg.GatherTraceDuration().Duration()): } @@ -347,7 +323,7 @@ func (n *Nurse) gatherTrace(now time.Time, wg *sync.WaitGroup) { err = wc.Close() if err != nil { - n.log.Errorw("could not close trace profile", "err", err) + n.eng.Errorw("could not close trace profile", "err", err) return } } @@ -355,18 +331,18 @@ func (n *Nurse) gatherTrace(now time.Time, wg *sync.WaitGroup) { func (n *Nurse) gather(typ string, now time.Time, wg *sync.WaitGroup) { defer wg.Done() - n.log.Debugf("gather %s %d ...", typ, now.UnixMicro()) - n.log.Debugf("gather %s %d done", typ, now.UnixMicro()) + n.eng.Debugf("gather %s %d ...", typ, now.UnixMicro()) + n.eng.Debugf("gather %s %d done", typ, now.UnixMicro()) p := pprof.Lookup(typ) if p == nil { - n.log.Errorf("Invariant violation: pprof type '%v' does not exist", typ) + n.eng.Errorf("Invariant violation: pprof type '%v' does not exist", typ) return } p0, err := collectProfile(p) if err != nil { - n.log.Errorw(fmt.Sprintf("could not collect %v profile", typ), "err", err) + n.eng.Errorw(fmt.Sprintf("could not collect %v profile", typ), "err", err) return } @@ -374,14 +350,14 @@ func (n *Nurse) gather(typ string, now time.Time, wg *sync.WaitGroup) { defer t.Stop() select { - case <-n.chStop: + case <-n.eng.StopChan: return case <-t.C: } p1, err := collectProfile(p) if err != nil { - n.log.Errorw(fmt.Sprintf("could not collect %v profile", typ), "err", err) + n.eng.Errorw(fmt.Sprintf("could not collect %v profile", typ), "err", err) return } ts := p1.TimeNanos @@ -391,7 +367,7 @@ func (n *Nurse) gather(typ string, now time.Time, wg *sync.WaitGroup) { p1, err = profile.Merge([]*profile.Profile{p0, p1}) if err != nil { - n.log.Errorw(fmt.Sprintf("could not compute delta for %v profile", typ), "err", err) + n.eng.Errorw(fmt.Sprintf("could not compute delta for %v profile", typ), "err", err) return } @@ -400,19 +376,19 @@ func (n *Nurse) gather(typ string, now time.Time, wg *sync.WaitGroup) { wc, err := n.createFile(now, typ, false) if err != nil { - n.log.Errorw(fmt.Sprintf("could not write %v profile", typ), "err", err) + n.eng.Errorw(fmt.Sprintf("could not write %v profile", typ), "err", err) return } defer wc.Close() err = p1.Write(wc) if err != nil { - n.log.Errorw(fmt.Sprintf("could not write %v profile", typ), "err", err) + n.eng.Errorw(fmt.Sprintf("could not write %v profile", typ), "err", err) return } err = wc.Close() if err != nil { - n.log.Errorw(fmt.Sprintf("could not close file for %v profile", typ), "err", err) + n.eng.Errorw(fmt.Sprintf("could not close file for %v profile", typ), "err", err) return } } @@ -437,7 +413,7 @@ func (n *Nurse) createFile(now time.Time, typ string, shouldGzip bool) (*utils.D filename += ".gz" } fullpath := filepath.Join(n.cfg.ProfileRoot(), filename) - n.log.Debugf("creating file %s", fullpath) + n.eng.Debugf("creating file %s", fullpath) file, err := os.Create(fullpath) if err != nil { diff --git a/core/services/nurse_test.go b/core/services/nurse_test.go index 4597eeb456b..ed6f6872dc9 100644 --- a/core/services/nurse_test.go +++ b/core/services/nurse_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" commonconfig "github.com/smartcontractkit/chainlink-common/pkg/config" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/utils" @@ -102,7 +103,7 @@ func TestNurse(t *testing.T) { nrse := NewNurse(newMockConfig(t), l) nrse.AddCheck("test", func() (bool, Meta) { return true, Meta{} }) - require.NoError(t, nrse.Start()) + require.NoError(t, nrse.Start(tests.Context(t))) defer func() { require.NoError(t, nrse.Close()) }() require.NoError(t, nrse.appendLog(time.Now(), "test", Meta{})) diff --git a/core/services/relay/evm/functions/logpoller_wrapper.go b/core/services/relay/evm/functions/logpoller_wrapper.go index 559b1ec33f5..b0d04b11871 100644 --- a/core/services/relay/evm/functions/logpoller_wrapper.go +++ b/core/services/relay/evm/functions/logpoller_wrapper.go @@ -22,7 +22,8 @@ import ( ) type logPollerWrapper struct { - services.StateMachine + services.Service + eng *services.Engine routerContract *functions_router.FunctionsRouter pluginConfig config.PluginConfig @@ -38,9 +39,6 @@ type logPollerWrapper struct { detectedRequests detectedEvents detectedResponses detectedEvents mu sync.Mutex - closeWait sync.WaitGroup - stopCh services.StopChan - lggr logger.Logger } type detectedEvent struct { @@ -94,7 +92,7 @@ func NewLogPollerWrapper(routerContractAddress common.Address, pluginConfig conf return nil, errors.Errorf("invalid config: number of required confirmation blocks >= pastBlocksToPoll") } - return &logPollerWrapper{ + w := &logPollerWrapper{ routerContract: routerContract, pluginConfig: pluginConfig, requestBlockOffset: requestBlockOffset, @@ -106,40 +104,25 @@ func NewLogPollerWrapper(routerContractAddress common.Address, pluginConfig conf logPoller: logPoller, client: client, subscribers: make(map[string]evmRelayTypes.RouteUpdateSubscriber), - stopCh: make(services.StopChan), - lggr: lggr.Named("LogPollerWrapper"), - }, nil -} - -func (l *logPollerWrapper) Start(context.Context) error { - return l.StartOnce("LogPollerWrapper", func() error { - l.lggr.Infow("starting LogPollerWrapper", "routerContract", l.routerContract.Address().Hex(), "contractVersion", l.pluginConfig.ContractVersion) - l.mu.Lock() - defer l.mu.Unlock() - if l.pluginConfig.ContractVersion != 1 { - return errors.New("only contract version 1 is supported") - } - l.closeWait.Add(1) - go l.checkForRouteUpdates() - return nil - }) -} - -func (l *logPollerWrapper) Close() error { - return l.StopOnce("LogPollerWrapper", func() (err error) { - l.lggr.Info("closing LogPollerWrapper") - close(l.stopCh) - l.closeWait.Wait() - return nil - }) + } + w.Service, w.eng = services.Config{ + Name: "LoggPollerWrapper", + Start: w.start, + }.NewServiceEngine(lggr) + return w, nil } -func (l *logPollerWrapper) HealthReport() map[string]error { - return map[string]error{l.Name(): l.Ready()} +func (l *logPollerWrapper) start(context.Context) error { + l.eng.Infow("starting LogPollerWrapper", "routerContract", l.routerContract.Address().Hex(), "contractVersion", l.pluginConfig.ContractVersion) + l.mu.Lock() + defer l.mu.Unlock() + if l.pluginConfig.ContractVersion != 1 { + return errors.New("only contract version 1 is supported") + } + l.eng.Go(l.checkForRouteUpdates) + return nil } -func (l *logPollerWrapper) Name() string { return l.lggr.Name() } - // methods of LogPollerWrapper func (l *logPollerWrapper) LatestEvents(ctx context.Context) ([]evmRelayTypes.OracleRequest, []evmRelayTypes.OracleResponse, error) { l.mu.Lock() @@ -166,7 +149,7 @@ func (l *logPollerWrapper) LatestEvents(ctx context.Context) ([]evmRelayTypes.Or resultsReq := []evmRelayTypes.OracleRequest{} resultsResp := []evmRelayTypes.OracleResponse{} if len(coordinators) == 0 { - l.lggr.Debug("LatestEvents: no non-zero coordinators to check") + l.eng.Debug("LatestEvents: no non-zero coordinators to check") return resultsReq, resultsResp, errors.New("no non-zero coordinators to check") } @@ -174,32 +157,32 @@ func (l *logPollerWrapper) LatestEvents(ctx context.Context) ([]evmRelayTypes.Or requestEndBlock := latestBlockNum - l.requestBlockOffset requestLogs, err := l.logPoller.Logs(ctx, startBlockNum, requestEndBlock, functions_coordinator.FunctionsCoordinatorOracleRequest{}.Topic(), coordinator) if err != nil { - l.lggr.Errorw("LatestEvents: fetching request logs from LogPoller failed", "startBlock", startBlockNum, "endBlock", requestEndBlock) + l.eng.Errorw("LatestEvents: fetching request logs from LogPoller failed", "startBlock", startBlockNum, "endBlock", requestEndBlock) return nil, nil, err } - l.lggr.Debugw("LatestEvents: fetched request logs", "nRequestLogs", len(requestLogs), "latestBlock", latest, "startBlock", startBlockNum, "endBlock", requestEndBlock) + l.eng.Debugw("LatestEvents: fetched request logs", "nRequestLogs", len(requestLogs), "latestBlock", latest, "startBlock", startBlockNum, "endBlock", requestEndBlock) requestLogs = l.filterPreviouslyDetectedEvents(requestLogs, &l.detectedRequests, "requests") responseEndBlock := latestBlockNum - l.responseBlockOffset responseLogs, err := l.logPoller.Logs(ctx, startBlockNum, responseEndBlock, functions_coordinator.FunctionsCoordinatorOracleResponse{}.Topic(), coordinator) if err != nil { - l.lggr.Errorw("LatestEvents: fetching response logs from LogPoller failed", "startBlock", startBlockNum, "endBlock", responseEndBlock) + l.eng.Errorw("LatestEvents: fetching response logs from LogPoller failed", "startBlock", startBlockNum, "endBlock", responseEndBlock) return nil, nil, err } - l.lggr.Debugw("LatestEvents: fetched request logs", "nResponseLogs", len(responseLogs), "latestBlock", latest, "startBlock", startBlockNum, "endBlock", responseEndBlock) + l.eng.Debugw("LatestEvents: fetched request logs", "nResponseLogs", len(responseLogs), "latestBlock", latest, "startBlock", startBlockNum, "endBlock", responseEndBlock) responseLogs = l.filterPreviouslyDetectedEvents(responseLogs, &l.detectedResponses, "responses") parsingContract, err := functions_coordinator.NewFunctionsCoordinator(coordinator, l.client) if err != nil { - l.lggr.Error("LatestEvents: creating a contract instance for parsing failed") + l.eng.Error("LatestEvents: creating a contract instance for parsing failed") return nil, nil, err } - l.lggr.Debugw("LatestEvents: parsing logs", "nRequestLogs", len(requestLogs), "nResponseLogs", len(responseLogs), "coordinatorAddress", coordinator.Hex()) + l.eng.Debugw("LatestEvents: parsing logs", "nRequestLogs", len(requestLogs), "nResponseLogs", len(responseLogs), "coordinatorAddress", coordinator.Hex()) for _, log := range requestLogs { gethLog := log.ToGethLog() oracleRequest, err := parsingContract.ParseOracleRequest(gethLog) if err != nil { - l.lggr.Errorw("LatestEvents: failed to parse a request log, skipping", "err", err) + l.eng.Errorw("LatestEvents: failed to parse a request log, skipping", "err", err) continue } @@ -212,7 +195,7 @@ func (l *logPollerWrapper) LatestEvents(ctx context.Context) ([]evmRelayTypes.Or bytes32Type, errType7 := abi.NewType("bytes32", "bytes32", nil) if errType1 != nil || errType2 != nil || errType3 != nil || errType4 != nil || errType5 != nil || errType6 != nil || errType7 != nil { - l.lggr.Errorw("LatestEvents: failed to initialize types", "errType1", errType1, + l.eng.Errorw("LatestEvents: failed to initialize types", "errType1", errType1, "errType2", errType2, "errType3", errType3, "errType4", errType4, "errType5", errType5, "errType6", errType6, "errType7", errType7, ) continue @@ -244,7 +227,7 @@ func (l *logPollerWrapper) LatestEvents(ctx context.Context) ([]evmRelayTypes.Or oracleRequest.Commitment.TimeoutTimestamp, ) if err != nil { - l.lggr.Errorw("LatestEvents: failed to pack commitment bytes, skipping", "err", err) + l.eng.Errorw("LatestEvents: failed to pack commitment bytes, skipping", "err", err) } resultsReq = append(resultsReq, evmRelayTypes.OracleRequest{ @@ -266,7 +249,7 @@ func (l *logPollerWrapper) LatestEvents(ctx context.Context) ([]evmRelayTypes.Or gethLog := log.ToGethLog() oracleResponse, err := parsingContract.ParseOracleResponse(gethLog) if err != nil { - l.lggr.Errorw("LatestEvents: failed to parse a response log, skipping") + l.eng.Errorw("LatestEvents: failed to parse a response log, skipping") continue } resultsResp = append(resultsResp, evmRelayTypes.OracleResponse{ @@ -275,13 +258,13 @@ func (l *logPollerWrapper) LatestEvents(ctx context.Context) ([]evmRelayTypes.Or } } - l.lggr.Debugw("LatestEvents: done", "nRequestLogs", len(resultsReq), "nResponseLogs", len(resultsResp), "startBlock", startBlockNum, "endBlock", latestBlockNum) + l.eng.Debugw("LatestEvents: done", "nRequestLogs", len(resultsReq), "nResponseLogs", len(resultsResp), "startBlock", startBlockNum, "endBlock", latestBlockNum) return resultsReq, resultsResp, nil } func (l *logPollerWrapper) filterPreviouslyDetectedEvents(logs []logpoller.Log, detectedEvents *detectedEvents, filterType string) []logpoller.Log { if len(logs) > maxLogsToProcess { - l.lggr.Errorw("filterPreviouslyDetectedEvents: too many logs to process, only processing latest maxLogsToProcess logs", "filterType", filterType, "nLogs", len(logs), "maxLogsToProcess", maxLogsToProcess) + l.eng.Errorw("filterPreviouslyDetectedEvents: too many logs to process, only processing latest maxLogsToProcess logs", "filterType", filterType, "nLogs", len(logs), "maxLogsToProcess", maxLogsToProcess) logs = logs[len(logs)-maxLogsToProcess:] } l.mu.Lock() @@ -290,7 +273,7 @@ func (l *logPollerWrapper) filterPreviouslyDetectedEvents(logs []logpoller.Log, for _, log := range logs { var requestId [32]byte if len(log.Topics) < 2 || len(log.Topics[1]) != 32 { - l.lggr.Errorw("filterPreviouslyDetectedEvents: invalid log, skipping", "filterType", filterType, "log", log) + l.eng.Errorw("filterPreviouslyDetectedEvents: invalid log, skipping", "filterType", filterType, "log", log) continue } copy(requestId[:], log.Topics[1]) // requestId is the second topic (1st topic is the event signature) @@ -310,7 +293,7 @@ func (l *logPollerWrapper) filterPreviouslyDetectedEvents(logs []logpoller.Log, expiredRequests++ } detectedEvents.detectedEventsOrdered = detectedEvents.detectedEventsOrdered[expiredRequests:] - l.lggr.Debugw("filterPreviouslyDetectedEvents: done", "filterType", filterType, "nLogs", len(logs), "nFilteredLogs", len(filteredLogs), "nExpiredRequests", expiredRequests, "previouslyDetectedCacheSize", len(detectedEvents.detectedEventsOrdered)) + l.eng.Debugw("filterPreviouslyDetectedEvents: done", "filterType", filterType, "nLogs", len(logs), "nFilteredLogs", len(filteredLogs), "nExpiredRequests", expiredRequests, "previouslyDetectedCacheSize", len(detectedEvents.detectedEventsOrdered)) return filteredLogs } @@ -319,7 +302,7 @@ func (l *logPollerWrapper) SubscribeToUpdates(ctx context.Context, subscriberNam if l.pluginConfig.ContractVersion == 0 { // in V0, immediately set contract address to Oracle contract and never update again if err := subscriber.UpdateRoutes(ctx, l.routerContract.Address(), l.routerContract.Address()); err != nil { - l.lggr.Errorw("LogPollerWrapper: Failed to update routes", "subscriberName", subscriberName, "err", err) + l.eng.Errorw("LogPollerWrapper: Failed to update routes", "subscriberName", subscriberName, "err", err) } } else if l.pluginConfig.ContractVersion == 1 { l.mu.Lock() @@ -328,37 +311,36 @@ func (l *logPollerWrapper) SubscribeToUpdates(ctx context.Context, subscriberNam } } -func (l *logPollerWrapper) checkForRouteUpdates() { - defer l.closeWait.Done() +func (l *logPollerWrapper) checkForRouteUpdates(ctx context.Context) { freqSec := l.pluginConfig.ContractUpdateCheckFrequencySec if freqSec == 0 { - l.lggr.Errorw("LogPollerWrapper: ContractUpdateCheckFrequencySec is zero - route update checks disabled") + l.eng.Errorw("LogPollerWrapper: ContractUpdateCheckFrequencySec is zero - route update checks disabled") return } - updateOnce := func() { + updateOnce := func(ctx context.Context) { // NOTE: timeout == frequency here, could be changed to a separate config value timeout := time.Duration(l.pluginConfig.ContractUpdateCheckFrequencySec) * time.Second - ctx, cancel := l.stopCh.CtxCancel(context.WithTimeout(context.Background(), timeout)) + ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() active, proposed, err := l.getCurrentCoordinators(ctx) if err != nil { - l.lggr.Errorw("LogPollerWrapper: error calling getCurrentCoordinators", "err", err) + l.eng.Errorw("LogPollerWrapper: error calling getCurrentCoordinators", "err", err) return } l.handleRouteUpdate(ctx, active, proposed) } - updateOnce() // update once right away + updateOnce(ctx) // update once right away ticker := time.NewTicker(time.Duration(freqSec) * time.Second) defer ticker.Stop() for { select { - case <-l.stopCh: + case <-ctx.Done(): return case <-ticker.C: - updateOnce() + updateOnce(ctx) } } } @@ -394,22 +376,22 @@ func (l *logPollerWrapper) handleRouteUpdate(ctx context.Context, activeCoordina defer l.mu.Unlock() if activeCoordinator == (common.Address{}) { - l.lggr.Error("LogPollerWrapper: cannot update activeCoordinator to zero address") + l.eng.Error("LogPollerWrapper: cannot update activeCoordinator to zero address") return } if activeCoordinator == l.activeCoordinator && proposedCoordinator == l.proposedCoordinator { - l.lggr.Debug("LogPollerWrapper: no changes to routes") + l.eng.Debug("LogPollerWrapper: no changes to routes") return } errActive := l.registerFilters(ctx, activeCoordinator) errProposed := l.registerFilters(ctx, proposedCoordinator) if errActive != nil || errProposed != nil { - l.lggr.Errorw("LogPollerWrapper: Failed to register filters", "errorActive", errActive, "errorProposed", errProposed) + l.eng.Errorw("LogPollerWrapper: Failed to register filters", "errorActive", errActive, "errorProposed", errProposed) return } - l.lggr.Debugw("LogPollerWrapper: new routes", "activeCoordinator", activeCoordinator.Hex(), "proposedCoordinator", proposedCoordinator.Hex()) + l.eng.Debugw("LogPollerWrapper: new routes", "activeCoordinator", activeCoordinator.Hex(), "proposedCoordinator", proposedCoordinator.Hex()) l.activeCoordinator = activeCoordinator l.proposedCoordinator = proposedCoordinator @@ -417,7 +399,7 @@ func (l *logPollerWrapper) handleRouteUpdate(ctx context.Context, activeCoordina for _, subscriber := range l.subscribers { err := subscriber.UpdateRoutes(ctx, activeCoordinator, proposedCoordinator) if err != nil { - l.lggr.Errorw("LogPollerWrapper: Failed to update routes", "err", err) + l.eng.Errorw("LogPollerWrapper: Failed to update routes", "err", err) } } @@ -430,9 +412,9 @@ func (l *logPollerWrapper) handleRouteUpdate(ctx context.Context, activeCoordina continue } if err := l.logPoller.UnregisterFilter(ctx, filter.Name); err != nil { - l.lggr.Errorw("LogPollerWrapper: Failed to unregister filter", "filterName", filter.Name, "err", err) + l.eng.Errorw("LogPollerWrapper: Failed to unregister filter", "filterName", filter.Name, "err", err) } - l.lggr.Debugw("LogPollerWrapper: Successfully unregistered filter", "filterName", filter.Name) + l.eng.Debugw("LogPollerWrapper: Successfully unregistered filter", "filterName", filter.Name) } } diff --git a/core/services/synchronization/helpers_test.go b/core/services/synchronization/helpers_test.go index 7bb2dde7633..aea9bf77f49 100644 --- a/core/services/synchronization/helpers_test.go +++ b/core/services/synchronization/helpers_test.go @@ -12,15 +12,15 @@ import ( // NewTestTelemetryIngressClient calls NewTelemetryIngressClient and injects telemClient. func NewTestTelemetryIngressClient(t *testing.T, url *url.URL, serverPubKeyHex string, ks keystore.CSA, logging bool, telemClient telemPb.TelemClient) TelemetryService { - tc := NewTelemetryIngressClient(url, serverPubKeyHex, ks, logging, logger.TestLogger(t), 100, "test", "test") + tc := NewTelemetryIngressClient(url, serverPubKeyHex, ks, logging, logger.TestLogger(t), 100) tc.(*telemetryIngressClient).telemClient = telemClient return tc } // NewTestTelemetryIngressBatchClient calls NewTelemetryIngressBatchClient and injects telemClient. func NewTestTelemetryIngressBatchClient(t *testing.T, url *url.URL, serverPubKeyHex string, ks keystore.CSA, logging bool, telemClient telemPb.TelemClient, sendInterval time.Duration, uniconn bool) TelemetryService { - tc := NewTelemetryIngressBatchClient(url, serverPubKeyHex, ks, logging, logger.TestLogger(t), 100, 50, sendInterval, time.Second, uniconn, "test", "test") - tc.(*telemetryIngressBatchClient).close = func() error { return nil } + tc := NewTelemetryIngressBatchClient(url, serverPubKeyHex, ks, logging, logger.TestLogger(t), 100, 50, sendInterval, time.Second, uniconn) + tc.(*telemetryIngressBatchClient).closeFn = func() error { return nil } tc.(*telemetryIngressBatchClient).telemClient = telemClient return tc } diff --git a/core/services/synchronization/telemetry_ingress_batch_client.go b/core/services/synchronization/telemetry_ingress_batch_client.go index cade98cf606..26ce1e3066a 100644 --- a/core/services/synchronization/telemetry_ingress_batch_client.go +++ b/core/services/synchronization/telemetry_ingress_batch_client.go @@ -12,8 +12,9 @@ import ( "github.com/smartcontractkit/wsrpc" "github.com/smartcontractkit/wsrpc/examples/simple/keys" + "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services" - "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink-common/pkg/timeutil" "github.com/smartcontractkit/chainlink/v2/core/services/keystore" telemPb "github.com/smartcontractkit/chainlink/v2/core/services/synchronization/telem" ) @@ -37,21 +38,18 @@ func (NoopTelemetryIngressBatchClient) Name() string { return func (NoopTelemetryIngressBatchClient) Ready() error { return nil } type telemetryIngressBatchClient struct { - services.StateMachine + services.Service + eng *services.Engine + url *url.URL ks keystore.CSA serverPubKeyHex string connected atomic.Bool telemClient telemPb.TelemClient - close func() error - - globalLogger logger.Logger - logging bool - lggr logger.Logger + closeFn func() error - wgDone sync.WaitGroup - chDone services.StopChan + logging bool telemBufferSize uint telemMaxBatchSize uint @@ -66,8 +64,8 @@ type telemetryIngressBatchClient struct { // NewTelemetryIngressBatchClient returns a client backed by wsrpc that // can send telemetry to the telemetry ingress server -func NewTelemetryIngressBatchClient(url *url.URL, serverPubKeyHex string, ks keystore.CSA, logging bool, lggr logger.Logger, telemBufferSize uint, telemMaxBatchSize uint, telemSendInterval time.Duration, telemSendTimeout time.Duration, useUniconn bool, network string, chainID string) TelemetryService { - return &telemetryIngressBatchClient{ +func NewTelemetryIngressBatchClient(url *url.URL, serverPubKeyHex string, ks keystore.CSA, logging bool, lggr logger.Logger, telemBufferSize uint, telemMaxBatchSize uint, telemSendInterval time.Duration, telemSendTimeout time.Duration, useUniconn bool) TelemetryService { + c := &telemetryIngressBatchClient{ telemBufferSize: telemBufferSize, telemMaxBatchSize: telemMaxBatchSize, telemSendInterval: telemSendInterval, @@ -75,13 +73,17 @@ func NewTelemetryIngressBatchClient(url *url.URL, serverPubKeyHex string, ks key url: url, ks: ks, serverPubKeyHex: serverPubKeyHex, - globalLogger: lggr, logging: logging, - lggr: lggr.Named("TelemetryIngressBatchClient").Named(network).Named(chainID), - chDone: make(services.StopChan), workers: make(map[string]*telemetryIngressBatchWorker), useUniConn: useUniconn, } + c.Service, c.eng = services.Config{ + Name: "TelemetryIngressBatchClient", + Start: c.start, + Close: c.close, + }.NewServiceEngine(lggr) + + return c } // Start connects the wsrpc client to the telemetry ingress server @@ -90,71 +92,53 @@ func NewTelemetryIngressBatchClient(url *url.URL, serverPubKeyHex string, ks key // an error and wsrpc will continue to retry the connection. Eventually when the ingress // server does come back up, wsrpc will establish the connection without any interaction // on behalf of the node operator. -func (tc *telemetryIngressBatchClient) Start(ctx context.Context) error { - return tc.StartOnce("TelemetryIngressBatchClient", func() error { - clientPrivKey, err := tc.getCSAPrivateKey() - if err != nil { - return err - } +func (tc *telemetryIngressBatchClient) start(ctx context.Context) error { + clientPrivKey, err := tc.getCSAPrivateKey() + if err != nil { + return err + } - serverPubKey := keys.FromHex(tc.serverPubKeyHex) - - // Initialize a new wsrpc client caller - // This is used to call RPC methods on the server - if tc.telemClient == nil { // only preset for tests - if tc.useUniConn { - tc.wgDone.Add(1) - go func() { - defer tc.wgDone.Done() - ctx2, cancel := tc.chDone.NewCtx() - defer cancel() - conn, err := wsrpc.DialUniWithContext(ctx2, tc.lggr, tc.url.String(), clientPrivKey, serverPubKey) - if err != nil { - if ctx2.Err() != nil { - tc.lggr.Warnw("gave up connecting to telemetry endpoint", "err", err) - } else { - tc.lggr.Criticalw("telemetry endpoint dial errored unexpectedly", "err", err, "server pubkey", tc.serverPubKeyHex) - tc.SvcErrBuffer.Append(err) - } - return - } - tc.telemClient = telemPb.NewTelemClient(conn) - tc.close = conn.Close - tc.connected.Store(true) - }() - } else { - // Spawns a goroutine that will eventually connect - conn, err := wsrpc.DialWithContext(ctx, tc.url.String(), wsrpc.WithTransportCreds(clientPrivKey, serverPubKey), wsrpc.WithLogger(tc.lggr)) + serverPubKey := keys.FromHex(tc.serverPubKeyHex) + + // Initialize a new wsrpc client caller + // This is used to call RPC methods on the server + if tc.telemClient == nil { // only preset for tests + if tc.useUniConn { + tc.eng.Go(func(ctx context.Context) { + conn, err := wsrpc.DialUniWithContext(ctx, tc.eng, tc.url.String(), clientPrivKey, serverPubKey) if err != nil { - return fmt.Errorf("could not start TelemIngressBatchClient, Dial returned error: %v", err) + if ctx.Err() != nil { + tc.eng.Warnw("gave up connecting to telemetry endpoint", "err", err) + } else { + tc.eng.Criticalw("telemetry endpoint dial errored unexpectedly", "err", err, "server pubkey", tc.serverPubKeyHex) + tc.eng.EmitHealthErr(err) + } + return } tc.telemClient = telemPb.NewTelemClient(conn) - tc.close = func() error { conn.Close(); return nil } + tc.closeFn = conn.Close + tc.connected.Store(true) + }) + } else { + // Spawns a goroutine that will eventually connect + conn, err := wsrpc.DialWithContext(ctx, tc.url.String(), wsrpc.WithTransportCreds(clientPrivKey, serverPubKey), wsrpc.WithLogger(tc.eng)) + if err != nil { + return fmt.Errorf("could not start TelemIngressBatchClient, Dial returned error: %v", err) } + tc.telemClient = telemPb.NewTelemClient(conn) + tc.closeFn = func() error { conn.Close(); return nil } } + } - return nil - }) + return nil } // Close disconnects the wsrpc client from the ingress server and waits for all workers to exit -func (tc *telemetryIngressBatchClient) Close() error { - return tc.StopOnce("TelemetryIngressBatchClient", func() error { - close(tc.chDone) - tc.wgDone.Wait() - if (tc.useUniConn && tc.connected.Load()) || !tc.useUniConn { - return tc.close() - } - return nil - }) -} - -func (tc *telemetryIngressBatchClient) Name() string { - return tc.lggr.Name() -} - -func (tc *telemetryIngressBatchClient) HealthReport() map[string]error { - return map[string]error{tc.Name(): tc.Healthy()} +func (tc *telemetryIngressBatchClient) close() error { + if (tc.useUniConn && tc.connected.Load()) || !tc.useUniConn { + return tc.closeFn() + } + return nil } // getCSAPrivateKey gets the client's CSA private key @@ -175,7 +159,7 @@ func (tc *telemetryIngressBatchClient) getCSAPrivateKey() (privkey []byte, err e // and a warning is logged. func (tc *telemetryIngressBatchClient) Send(ctx context.Context, telemData []byte, contractID string, telemType TelemetryType) { if tc.useUniConn && !tc.connected.Load() { - tc.lggr.Warnw("not connected to telemetry endpoint", "endpoint", tc.url.String()) + tc.eng.Warnw("not connected to telemetry endpoint", "endpoint", tc.url.String()) return } payload := TelemPayload{ @@ -206,18 +190,17 @@ func (tc *telemetryIngressBatchClient) findOrCreateWorker(payload TelemPayload) if !found { worker = NewTelemetryIngressBatchWorker( tc.telemMaxBatchSize, - tc.telemSendInterval, tc.telemSendTimeout, tc.telemClient, - &tc.wgDone, - tc.chDone, make(chan TelemPayload, tc.telemBufferSize), payload.ContractID, payload.TelemType, - tc.globalLogger, + tc.eng, tc.logging, ) - worker.Start() + tc.eng.GoTick(timeutil.NewTicker(func() time.Duration { + return tc.telemSendInterval + }), worker.Send) tc.workers[workerKey] = worker } diff --git a/core/services/synchronization/telemetry_ingress_batch_worker.go b/core/services/synchronization/telemetry_ingress_batch_worker.go index e7ea6595811..7eca26f02c9 100644 --- a/core/services/synchronization/telemetry_ingress_batch_worker.go +++ b/core/services/synchronization/telemetry_ingress_batch_worker.go @@ -2,13 +2,12 @@ package synchronization import ( "context" - "sync" "sync/atomic" "time" "github.com/smartcontractkit/chainlink-common/pkg/services" - "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink-common/pkg/logger" telemPb "github.com/smartcontractkit/chainlink/v2/core/services/synchronization/telem" ) @@ -18,11 +17,8 @@ type telemetryIngressBatchWorker struct { services.Service telemMaxBatchSize uint - telemSendInterval time.Duration telemSendTimeout time.Duration telemClient telemPb.TelemClient - wgDone *sync.WaitGroup - chDone services.StopChan chTelemetry chan TelemPayload contractID string telemType TelemetryType @@ -35,65 +31,45 @@ type telemetryIngressBatchWorker struct { // telemetry to the ingress server via WSRPC func NewTelemetryIngressBatchWorker( telemMaxBatchSize uint, - telemSendInterval time.Duration, telemSendTimeout time.Duration, telemClient telemPb.TelemClient, - wgDone *sync.WaitGroup, - chDone chan struct{}, chTelemetry chan TelemPayload, contractID string, telemType TelemetryType, - globalLogger logger.Logger, + lggr logger.Logger, logging bool, ) *telemetryIngressBatchWorker { return &telemetryIngressBatchWorker{ - telemSendInterval: telemSendInterval, telemSendTimeout: telemSendTimeout, telemMaxBatchSize: telemMaxBatchSize, telemClient: telemClient, - wgDone: wgDone, - chDone: chDone, chTelemetry: chTelemetry, contractID: contractID, telemType: telemType, logging: logging, - lggr: globalLogger.Named("TelemetryIngressBatchWorker"), + lggr: logger.Named(lggr, "TelemetryIngressBatchWorker"), } } -// Start sends batched telemetry to the ingress server on an interval -func (tw *telemetryIngressBatchWorker) Start() { - tw.wgDone.Add(1) - sendTicker := time.NewTicker(tw.telemSendInterval) - - go func() { - defer tw.wgDone.Done() - - for { - select { - case <-sendTicker.C: - if len(tw.chTelemetry) == 0 { - continue - } +// Send sends batched telemetry to the ingress server on an interval +func (tw *telemetryIngressBatchWorker) Send(ctx context.Context) { + if len(tw.chTelemetry) == 0 { + return + } - // Send batched telemetry to the ingress server, log any errors - telemBatchReq := tw.BuildTelemBatchReq() - ctx, cancel := tw.chDone.CtxCancel(context.WithTimeout(context.Background(), tw.telemSendTimeout)) - _, err := tw.telemClient.TelemBatch(ctx, telemBatchReq) - cancel() + // Send batched telemetry to the ingress server, log any errors + telemBatchReq := tw.BuildTelemBatchReq() + ctx, cancel := context.WithTimeout(ctx, tw.telemSendTimeout) + _, err := tw.telemClient.TelemBatch(ctx, telemBatchReq) + cancel() - if err != nil { - tw.lggr.Warnf("Could not send telemetry: %v", err) - continue - } - if tw.logging { - tw.lggr.Debugw("Successfully sent telemetry to ingress server", "contractID", telemBatchReq.ContractId, "telemType", telemBatchReq.TelemetryType, "telemetry", telemBatchReq.Telemetry) - } - case <-tw.chDone: - return - } - } - }() + if err != nil { + tw.lggr.Warnf("Could not send telemetry: %v", err) + return + } + if tw.logging { + tw.lggr.Debugw("Successfully sent telemetry to ingress server", "contractID", telemBatchReq.ContractId, "telemType", telemBatchReq.TelemetryType, "telemetry", telemBatchReq.Telemetry) + } } // logBufferFullWithExpBackoff logs messages at diff --git a/core/services/synchronization/telemetry_ingress_batch_worker_test.go b/core/services/synchronization/telemetry_ingress_batch_worker_test.go index 109022c7135..bf44ee9195a 100644 --- a/core/services/synchronization/telemetry_ingress_batch_worker_test.go +++ b/core/services/synchronization/telemetry_ingress_batch_worker_test.go @@ -1,7 +1,6 @@ package synchronization_test import ( - "sync" "testing" "time" @@ -22,11 +21,8 @@ func TestTelemetryIngressWorker_BuildTelemBatchReq(t *testing.T) { chTelemetry := make(chan synchronization.TelemPayload, 10) worker := synchronization.NewTelemetryIngressBatchWorker( uint(maxTelemBatchSize), - time.Millisecond*1, time.Second, mocks.NewTelemClient(t), - &sync.WaitGroup{}, - make(chan struct{}), chTelemetry, "0xa", synchronization.OCR, diff --git a/core/services/synchronization/telemetry_ingress_client.go b/core/services/synchronization/telemetry_ingress_client.go index dc4ced31d09..1ed55bb5468 100644 --- a/core/services/synchronization/telemetry_ingress_client.go +++ b/core/services/synchronization/telemetry_ingress_client.go @@ -4,15 +4,14 @@ import ( "context" "errors" "net/url" - "sync" "sync/atomic" "time" "github.com/smartcontractkit/wsrpc" "github.com/smartcontractkit/wsrpc/examples/simple/keys" + "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services" - "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/keystore" telemPb "github.com/smartcontractkit/chainlink/v2/core/services/synchronization/telem" ) @@ -35,82 +34,59 @@ func (NoopTelemetryIngressClient) Name() string { return "Noop func (NoopTelemetryIngressClient) Ready() error { return nil } type telemetryIngressClient struct { - services.StateMachine + services.Service + eng *services.Engine + url *url.URL ks keystore.CSA serverPubKeyHex string telemClient telemPb.TelemClient logging bool - lggr logger.Logger - wgDone sync.WaitGroup - chDone services.StopChan dropMessageCount atomic.Uint32 chTelemetry chan TelemPayload } // NewTelemetryIngressClient returns a client backed by wsrpc that // can send telemetry to the telemetry ingress server -func NewTelemetryIngressClient(url *url.URL, serverPubKeyHex string, ks keystore.CSA, logging bool, lggr logger.Logger, telemBufferSize uint, network string, chainID string) TelemetryService { - return &telemetryIngressClient{ +func NewTelemetryIngressClient(url *url.URL, serverPubKeyHex string, ks keystore.CSA, logging bool, lggr logger.Logger, telemBufferSize uint) TelemetryService { + c := &telemetryIngressClient{ url: url, ks: ks, serverPubKeyHex: serverPubKeyHex, logging: logging, - lggr: lggr.Named("TelemetryIngressClient").Named(network).Named(chainID), chTelemetry: make(chan TelemPayload, telemBufferSize), - chDone: make(services.StopChan), } + c.Service, c.eng = services.Config{ + Name: "TelemetryIngressClient", + Start: c.start, + }.NewServiceEngine(lggr) + return c } // Start connects the wsrpc client to the telemetry ingress server -func (tc *telemetryIngressClient) Start(context.Context) error { - return tc.StartOnce("TelemetryIngressClient", func() error { - privkey, err := tc.getCSAPrivateKey() - if err != nil { - return err - } - - tc.connect(privkey) - - return nil - }) -} - -// Close disconnects the wsrpc client from the ingress server -func (tc *telemetryIngressClient) Close() error { - return tc.StopOnce("TelemetryIngressClient", func() error { - close(tc.chDone) - tc.wgDone.Wait() - return nil - }) -} +func (tc *telemetryIngressClient) start(context.Context) error { + privkey, err := tc.getCSAPrivateKey() + if err != nil { + return err + } -func (tc *telemetryIngressClient) Name() string { - return tc.lggr.Name() -} + tc.connect(privkey) -func (tc *telemetryIngressClient) HealthReport() map[string]error { - return map[string]error{tc.Name(): tc.Healthy()} + return nil } func (tc *telemetryIngressClient) connect(clientPrivKey []byte) { - tc.wgDone.Add(1) - - go func() { - defer tc.wgDone.Done() - ctx, cancel := tc.chDone.NewCtx() - defer cancel() - + tc.eng.Go(func(ctx context.Context) { serverPubKey := keys.FromHex(tc.serverPubKeyHex) - conn, err := wsrpc.DialWithContext(ctx, tc.url.String(), wsrpc.WithTransportCreds(clientPrivKey, serverPubKey), wsrpc.WithLogger(tc.lggr)) + conn, err := wsrpc.DialWithContext(ctx, tc.url.String(), wsrpc.WithTransportCreds(clientPrivKey, serverPubKey), wsrpc.WithLogger(tc.eng)) if err != nil { if ctx.Err() != nil { - tc.lggr.Warnw("gave up connecting to telemetry endpoint", "err", err) + tc.eng.Warnw("gave up connecting to telemetry endpoint", "err", err) } else { - tc.lggr.Criticalw("telemetry endpoint dial errored unexpectedly", "err", err) - tc.SvcErrBuffer.Append(err) + tc.eng.Criticalw("telemetry endpoint dial errored unexpectedly", "err", err) + tc.eng.EmitHealthErr(err) } return } @@ -126,16 +102,12 @@ func (tc *telemetryIngressClient) connect(clientPrivKey []byte) { tc.handleTelemetry() // Wait for close - <-tc.chDone - }() + <-ctx.Done() + }) } func (tc *telemetryIngressClient) handleTelemetry() { - tc.wgDone.Add(1) - go func() { - defer tc.wgDone.Done() - ctx, cancel := tc.chDone.NewCtx() - defer cancel() + tc.eng.Go(func(ctx context.Context) { for { select { case p := <-tc.chTelemetry: @@ -148,17 +120,17 @@ func (tc *telemetryIngressClient) handleTelemetry() { } _, err := tc.telemClient.Telem(ctx, telemReq) if err != nil { - tc.lggr.Errorf("Could not send telemetry: %v", err) + tc.eng.Errorf("Could not send telemetry: %v", err) continue } if tc.logging { - tc.lggr.Debugw("successfully sent telemetry to ingress server", "contractID", p.ContractID, "telemetry", p.Telemetry) + tc.eng.Debugw("successfully sent telemetry to ingress server", "contractID", p.ContractID, "telemetry", p.Telemetry) } - case <-tc.chDone: + case <-ctx.Done(): return } } - }() + }) } // logBufferFullWithExpBackoff logs messages at @@ -176,7 +148,7 @@ func (tc *telemetryIngressClient) handleTelemetry() { func (tc *telemetryIngressClient) logBufferFullWithExpBackoff(payload TelemPayload) { count := tc.dropMessageCount.Add(1) if count > 0 && (count%100 == 0 || count&(count-1) == 0) { - tc.lggr.Warnw("telemetry ingress client buffer full, dropping message", "telemetry", payload.Telemetry, "droppedCount", count) + tc.eng.Warnw("telemetry ingress client buffer full, dropping message", "telemetry", payload.Telemetry, "droppedCount", count) } } diff --git a/core/services/telemetry/manager.go b/core/services/telemetry/manager.go index a65759a5c62..73a94b4b127 100644 --- a/core/services/telemetry/manager.go +++ b/core/services/telemetry/manager.go @@ -1,29 +1,29 @@ package telemetry import ( - "context" "net/url" "strings" "time" "github.com/pkg/errors" - "go.uber.org/multierr" - "github.com/smartcontractkit/libocr/commontypes" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + common "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink/v2/core/config" - "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/keystore" "github.com/smartcontractkit/chainlink/v2/core/services/synchronization" ) type Manager struct { - services.StateMachine - bufferSize uint - endpoints []*telemetryEndpoint - ks keystore.CSA - lggr logger.Logger + services.Service + eng *services.Engine + + bufferSize uint + endpoints []*telemetryEndpoint + ks keystore.CSA + logging bool maxBatchSize uint sendInterval time.Duration @@ -45,9 +45,7 @@ type telemetryEndpoint struct { func NewManager(cfg config.TelemetryIngress, csaKeyStore keystore.CSA, lggr logger.Logger) *Manager { m := &Manager{ bufferSize: cfg.BufferSize(), - endpoints: nil, ks: csaKeyStore, - lggr: lggr.Named("TelemetryManager"), logging: cfg.Logging(), maxBatchSize: cfg.MaxBatchSize(), sendInterval: cfg.SendInterval(), @@ -55,44 +53,21 @@ func NewManager(cfg config.TelemetryIngress, csaKeyStore keystore.CSA, lggr logg uniConn: cfg.UniConn(), useBatchSend: cfg.UseBatchSend(), } - for _, e := range cfg.Endpoints() { - if err := m.addEndpoint(e); err != nil { - m.lggr.Error(err) - } - } - return m -} - -func (m *Manager) Start(ctx context.Context) error { - return m.StartOnce("TelemetryManager", func() error { - var err error - for _, e := range m.endpoints { - err = multierr.Append(err, e.client.Start(ctx)) - } - return err - }) -} -func (m *Manager) Close() error { - return m.StopOnce("TelemetryManager", func() error { - var err error - for _, e := range m.endpoints { - err = multierr.Append(err, e.client.Close()) - } - return err - }) -} - -func (m *Manager) Name() string { - return m.lggr.Name() -} + m.Service, m.eng = services.Config{ + Name: "TelemetryManager", + NewSubServices: func(lggr common.Logger) (subs []services.Service) { + for _, e := range cfg.Endpoints() { + if sub, err := m.newEndpoint(e, lggr, cfg); err != nil { + lggr.Error(err) + } else { + subs = append(subs, sub) + } + } + return + }, + }.NewServiceEngine(lggr) -func (m *Manager) HealthReport() map[string]error { - hr := map[string]error{m.Name(): m.Healthy()} - - for _, e := range m.endpoints { - services.CopyHealth(hr, e.client.HealthReport()) - } - return hr + return m } // GenMonitoringEndpoint creates a new monitoring endpoints based on the existing available endpoints defined in the core config TOML, if no endpoint for the network and chainID exists, a NOOP agent will be used and the telemetry will not be sent @@ -100,7 +75,7 @@ func (m *Manager) GenMonitoringEndpoint(network string, chainID string, contract e, found := m.getEndpoint(network, chainID) if !found { - m.lggr.Warnf("no telemetry endpoint found for network %q chainID %q, telemetry %q for contactID %q will NOT be sent", network, chainID, telemType, contractID) + m.eng.Warnf("no telemetry endpoint found for network %q chainID %q, telemetry %q for contactID %q will NOT be sent", network, chainID, telemType, contractID) return &NoopAgent{} } @@ -111,32 +86,33 @@ func (m *Manager) GenMonitoringEndpoint(network string, chainID string, contract return NewIngressAgent(e.client, network, chainID, contractID, telemType) } -func (m *Manager) addEndpoint(e config.TelemetryIngressEndpoint) error { +func (m *Manager) newEndpoint(e config.TelemetryIngressEndpoint, lggr logger.Logger, cfg config.TelemetryIngress) (services.Service, error) { if e.Network() == "" { - return errors.New("cannot add telemetry endpoint, network cannot be empty") + return nil, errors.New("cannot add telemetry endpoint, network cannot be empty") } if e.ChainID() == "" { - return errors.New("cannot add telemetry endpoint, chainID cannot be empty") + return nil, errors.New("cannot add telemetry endpoint, chainID cannot be empty") } if e.URL() == nil { - return errors.New("cannot add telemetry endpoint, URL cannot be empty") + return nil, errors.New("cannot add telemetry endpoint, URL cannot be empty") } if e.ServerPubKey() == "" { - return errors.New("cannot add telemetry endpoint, ServerPubKey cannot be empty") + return nil, errors.New("cannot add telemetry endpoint, ServerPubKey cannot be empty") } if _, found := m.getEndpoint(e.Network(), e.ChainID()); found { - return errors.Errorf("cannot add telemetry endpoint for network %q and chainID %q, endpoint already exists", e.Network(), e.ChainID()) + return nil, errors.Errorf("cannot add telemetry endpoint for network %q and chainID %q, endpoint already exists", e.Network(), e.ChainID()) } + lggr = logger.Sugared(lggr).Named(e.Network()).Named(e.ChainID()) var tClient synchronization.TelemetryService if m.useBatchSend { - tClient = synchronization.NewTelemetryIngressBatchClient(e.URL(), e.ServerPubKey(), m.ks, m.logging, m.lggr, m.bufferSize, m.maxBatchSize, m.sendInterval, m.sendTimeout, m.uniConn, e.Network(), e.ChainID()) + tClient = synchronization.NewTelemetryIngressBatchClient(e.URL(), e.ServerPubKey(), m.ks, cfg.Logging(), lggr, cfg.BufferSize(), cfg.MaxBatchSize(), cfg.SendInterval(), cfg.SendTimeout(), cfg.UniConn()) } else { - tClient = synchronization.NewTelemetryIngressClient(e.URL(), e.ServerPubKey(), m.ks, m.logging, m.lggr, m.bufferSize, e.Network(), e.ChainID()) + tClient = synchronization.NewTelemetryIngressClient(e.URL(), e.ServerPubKey(), m.ks, cfg.Logging(), lggr, cfg.BufferSize()) } te := telemetryEndpoint{ @@ -148,7 +124,7 @@ func (m *Manager) addEndpoint(e config.TelemetryIngressEndpoint) error { } m.endpoints = append(m.endpoints, &te) - return nil + return te.client, nil } func (m *Manager) getEndpoint(network string, chainID string) (*telemetryEndpoint, bool) { diff --git a/core/services/telemetry/manager_test.go b/core/services/telemetry/manager_test.go index 4e55cb75752..fef065b572c 100644 --- a/core/services/telemetry/manager_test.go +++ b/core/services/telemetry/manager_test.go @@ -156,7 +156,7 @@ func TestNewManager(t *testing.T) { require.Equal(t, uint(123), m.bufferSize) require.Equal(t, ks, m.ks) - require.Equal(t, "TelemetryManager", m.lggr.Name()) + require.Equal(t, "TelemetryManager", m.Name()) require.Equal(t, true, m.logging) require.Equal(t, uint(51), m.maxBatchSize) require.Equal(t, time.Millisecond*512, m.sendInterval) From 4843d84c260c0300eecfad413cca60af481280bf Mon Sep 17 00:00:00 2001 From: Lukasz <120112546+lukaszcl@users.noreply.github.com> Date: Wed, 7 Aug 2024 15:39:35 +0200 Subject: [PATCH 2/9] Update e2e tests definition for CI and automation workflow (#13908) * Update e2e tests definition for CI * Update test --- .github/e2e-tests.yml | 32 ++++++++++++++++--- .../run-automation-ondemand-e2e-tests.yml | 10 +++++- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/.github/e2e-tests.yml b/.github/e2e-tests.yml index 0d92d1900dc..b2c9f12fcaf 100644 --- a/.github/e2e-tests.yml +++ b/.github/e2e-tests.yml @@ -47,6 +47,8 @@ runner-test-matrix: test_env_type: k8s-remote-runner runs_on: ubuntu-latest test_cmd: cd integration-tests/ && go test soak/ocr_test.go -v -test.run ^TestOCRv1Soak$ -test.parallel=1 -timeout 30m -count=1 -json + test_config_override_required: true + test_secrets_required: true test_inputs: test_suite: soak @@ -543,15 +545,37 @@ runner-test-matrix: chainlink_upgrade_version: develop pyroscope_env: ci-smoke-automation-upgrade-tests - - id: integration-tests/reorg/automation_reorg_test.go + - id: integration-tests/reorg/automation_reorg_test.go^TestAutomationReorg/registry_2_0 path: integration-tests/reorg/automation_reorg_test.go runs_on: ubuntu-latest - test_env_type: k8s-remote-runner + test_env_type: docker + test_inputs: + test_suite: reorg + workflows: + - Run Automation On Demand Tests (TEST WORKFLOW) + test_cmd: cd integration-tests/reorg && DETACH_RUNNER=false go test -v -test.run ^TestAutomationReorg/registry_2_0 -test.parallel=1 -timeout 30m -count=1 -json + pyroscope_env: ci-automation-on-demand-reorg + + - id: integration-tests/reorg/automation_reorg_test.go^TestAutomationReorg/registry_2_1 + path: integration-tests/reorg/automation_reorg_test.go + runs_on: ubuntu-latest + test_env_type: docker + test_inputs: + test_suite: reorg + workflows: + - Run Automation On Demand Tests (TEST WORKFLOW) + test_cmd: cd integration-tests/reorg && DETACH_RUNNER=false go test -v -test.run ^TestAutomationReorg/registry_2_1 -test.parallel=2 -timeout 30m -count=1 -json + pyroscope_env: ci-automation-on-demand-reorg + + - id: integration-tests/reorg/automation_reorg_test.go^TestAutomationReorg/registry_2_2 + path: integration-tests/reorg/automation_reorg_test.go + runs_on: ubuntu-latest + test_env_type: docker test_inputs: test_suite: reorg workflows: - Run Automation On Demand Tests (TEST WORKFLOW) - test_cmd: cd integration-tests/reorg && DETACH_RUNNER=false go test -v -test.run ^TestAutomationReorg$ -test.parallel=7 -timeout 60m -count=1 -json + test_cmd: cd integration-tests/reorg && DETACH_RUNNER=false go test -v -test.run ^TestAutomationReorg/registry_2_2 -test.parallel=2 -timeout 30m -count=1 -json pyroscope_env: ci-automation-on-demand-reorg - id: integration-tests/chaos/automation_chaos_test.go @@ -560,7 +584,7 @@ runner-test-matrix: runs_on: ubuntu-latest workflows: - Run Automation On Demand Tests (TEST WORKFLOW) - test_cmd: cd integration-tests/chaos && DETACH_RUNNER=false go test -v -test.run ^TestAutomationChaos$ -test.parallel=15 -timeout 60m -count=1 -json + test_cmd: cd integration-tests/chaos && DETACH_RUNNER=false go test -v -test.run ^TestAutomationChaos$ -test.parallel=20 -timeout 60m -count=1 -json pyroscope_env: ci-automation-on-demand-chaos test_inputs: test_suite: chaos diff --git a/.github/workflows/run-automation-ondemand-e2e-tests.yml b/.github/workflows/run-automation-ondemand-e2e-tests.yml index 7bf4691ecc5..8dac3c56994 100644 --- a/.github/workflows/run-automation-ondemand-e2e-tests.yml +++ b/.github/workflows/run-automation-ondemand-e2e-tests.yml @@ -116,10 +116,18 @@ jobs: # Run reorg tests if enabled if [[ "${{ github.event.inputs.enableReorg }}" == 'true' ]]; then cat >> test_list.yaml < Date: Wed, 7 Aug 2024 07:42:01 -0600 Subject: [PATCH 3/9] bump solana commit (#14062) --- core/scripts/go.mod | 2 +- core/scripts/go.sum | 4 ++-- go.mod | 2 +- go.sum | 4 ++-- integration-tests/go.mod | 2 +- integration-tests/go.sum | 4 ++-- integration-tests/load/go.mod | 2 +- integration-tests/load/go.sum | 4 ++-- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/core/scripts/go.mod b/core/scripts/go.mod index 45b5ee59059..94504897ab0 100644 --- a/core/scripts/go.mod +++ b/core/scripts/go.mod @@ -273,7 +273,7 @@ require ( github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240710121324-3ed288aa9b45 // indirect github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240801131703-fd75761c982f // indirect github.com/smartcontractkit/chainlink-feeds v0.0.0-20240710170203-5b41615da827 // indirect - github.com/smartcontractkit/chainlink-solana v1.1.0 // indirect + github.com/smartcontractkit/chainlink-solana v1.1.1-0.20240806154405-8e5684f98564 // indirect github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240709043547-03612098f799 // indirect github.com/smartcontractkit/tdh2/go/ocr2/decryptionplugin v0.0.0-20230906073235-9e478e5e19f1 // indirect github.com/smartcontractkit/tdh2/go/tdh2 v0.0.0-20230906073235-9e478e5e19f1 // indirect diff --git a/core/scripts/go.sum b/core/scripts/go.sum index dff6f3f356a..f770498cff8 100644 --- a/core/scripts/go.sum +++ b/core/scripts/go.sum @@ -1192,8 +1192,8 @@ github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240801131703-fd75761 github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240801131703-fd75761c982f/go.mod h1:V/86loaFSH0dqqUEHqyXVbyNqDRSjvcf9BRomWFTljU= github.com/smartcontractkit/chainlink-feeds v0.0.0-20240710170203-5b41615da827 h1:BCHu4pNP6arrcHLEWx61XjLaonOd2coQNyL0NTUcaMc= github.com/smartcontractkit/chainlink-feeds v0.0.0-20240710170203-5b41615da827/go.mod h1:OPX+wC2TWQsyLNpR7daMt2vMpmsNcoBxbZyGTHr6tiA= -github.com/smartcontractkit/chainlink-solana v1.1.0 h1:+xBeVqx2x0Sx3CBbF8RLSblczsxJDYTkta8h7i8+23I= -github.com/smartcontractkit/chainlink-solana v1.1.0/go.mod h1:Ml88TJTwZCj6yHDkAEN/EhxVutzSlk+kDZgfibRIqF0= +github.com/smartcontractkit/chainlink-solana v1.1.1-0.20240806154405-8e5684f98564 h1:8ZzsGNhqYxmQ/QMO1fuXO7u9Vpl9YUvPJK+td/ZaBJA= +github.com/smartcontractkit/chainlink-solana v1.1.1-0.20240806154405-8e5684f98564/go.mod h1:Ml88TJTwZCj6yHDkAEN/EhxVutzSlk+kDZgfibRIqF0= github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240709043547-03612098f799 h1:HyLTySm7BR+oNfZqDTkVJ25wnmcTtxBBD31UkFL+kEM= github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240709043547-03612098f799/go.mod h1:UVFRacRkP7O7TQAzFmR52v5mUlxf+G1ovMlCQAB/cHU= github.com/smartcontractkit/go-plugin v0.0.0-20240208201424-b3b91517de16 h1:TFe+FvzxClblt6qRfqEhUfa4kFQx5UobuoFGO2W4mMo= diff --git a/go.mod b/go.mod index 78ec7d29ee1..2179ffc2d21 100644 --- a/go.mod +++ b/go.mod @@ -78,7 +78,7 @@ require ( github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240710121324-3ed288aa9b45 github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240801131703-fd75761c982f github.com/smartcontractkit/chainlink-feeds v0.0.0-20240710170203-5b41615da827 - github.com/smartcontractkit/chainlink-solana v1.1.0 + github.com/smartcontractkit/chainlink-solana v1.1.1-0.20240806154405-8e5684f98564 github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240709043547-03612098f799 github.com/smartcontractkit/libocr v0.0.0-20240717100443-f6226e09bee7 github.com/smartcontractkit/tdh2/go/ocr2/decryptionplugin v0.0.0-20230906073235-9e478e5e19f1 diff --git a/go.sum b/go.sum index f5ef0f91e70..b953f315e92 100644 --- a/go.sum +++ b/go.sum @@ -1147,8 +1147,8 @@ github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240801131703-fd75761 github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240801131703-fd75761c982f/go.mod h1:V/86loaFSH0dqqUEHqyXVbyNqDRSjvcf9BRomWFTljU= github.com/smartcontractkit/chainlink-feeds v0.0.0-20240710170203-5b41615da827 h1:BCHu4pNP6arrcHLEWx61XjLaonOd2coQNyL0NTUcaMc= github.com/smartcontractkit/chainlink-feeds v0.0.0-20240710170203-5b41615da827/go.mod h1:OPX+wC2TWQsyLNpR7daMt2vMpmsNcoBxbZyGTHr6tiA= -github.com/smartcontractkit/chainlink-solana v1.1.0 h1:+xBeVqx2x0Sx3CBbF8RLSblczsxJDYTkta8h7i8+23I= -github.com/smartcontractkit/chainlink-solana v1.1.0/go.mod h1:Ml88TJTwZCj6yHDkAEN/EhxVutzSlk+kDZgfibRIqF0= +github.com/smartcontractkit/chainlink-solana v1.1.1-0.20240806154405-8e5684f98564 h1:8ZzsGNhqYxmQ/QMO1fuXO7u9Vpl9YUvPJK+td/ZaBJA= +github.com/smartcontractkit/chainlink-solana v1.1.1-0.20240806154405-8e5684f98564/go.mod h1:Ml88TJTwZCj6yHDkAEN/EhxVutzSlk+kDZgfibRIqF0= github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240709043547-03612098f799 h1:HyLTySm7BR+oNfZqDTkVJ25wnmcTtxBBD31UkFL+kEM= github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240709043547-03612098f799/go.mod h1:UVFRacRkP7O7TQAzFmR52v5mUlxf+G1ovMlCQAB/cHU= github.com/smartcontractkit/go-plugin v0.0.0-20240208201424-b3b91517de16 h1:TFe+FvzxClblt6qRfqEhUfa4kFQx5UobuoFGO2W4mMo= diff --git a/integration-tests/go.mod b/integration-tests/go.mod index a648e46e9f0..ff60a8f78b3 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -380,7 +380,7 @@ require ( github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240710121324-3ed288aa9b45 // indirect github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240801131703-fd75761c982f // indirect github.com/smartcontractkit/chainlink-feeds v0.0.0-20240710170203-5b41615da827 // indirect - github.com/smartcontractkit/chainlink-solana v1.1.0 // indirect + github.com/smartcontractkit/chainlink-solana v1.1.1-0.20240806154405-8e5684f98564 // indirect github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240709043547-03612098f799 // indirect github.com/smartcontractkit/tdh2/go/ocr2/decryptionplugin v0.0.0-20230906073235-9e478e5e19f1 // indirect github.com/smartcontractkit/tdh2/go/tdh2 v0.0.0-20230906073235-9e478e5e19f1 // indirect diff --git a/integration-tests/go.sum b/integration-tests/go.sum index 03e4a9082ff..5d15dfd92f6 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -1496,8 +1496,8 @@ github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240801131703-fd75761 github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240801131703-fd75761c982f/go.mod h1:V/86loaFSH0dqqUEHqyXVbyNqDRSjvcf9BRomWFTljU= github.com/smartcontractkit/chainlink-feeds v0.0.0-20240710170203-5b41615da827 h1:BCHu4pNP6arrcHLEWx61XjLaonOd2coQNyL0NTUcaMc= github.com/smartcontractkit/chainlink-feeds v0.0.0-20240710170203-5b41615da827/go.mod h1:OPX+wC2TWQsyLNpR7daMt2vMpmsNcoBxbZyGTHr6tiA= -github.com/smartcontractkit/chainlink-solana v1.1.0 h1:+xBeVqx2x0Sx3CBbF8RLSblczsxJDYTkta8h7i8+23I= -github.com/smartcontractkit/chainlink-solana v1.1.0/go.mod h1:Ml88TJTwZCj6yHDkAEN/EhxVutzSlk+kDZgfibRIqF0= +github.com/smartcontractkit/chainlink-solana v1.1.1-0.20240806154405-8e5684f98564 h1:8ZzsGNhqYxmQ/QMO1fuXO7u9Vpl9YUvPJK+td/ZaBJA= +github.com/smartcontractkit/chainlink-solana v1.1.1-0.20240806154405-8e5684f98564/go.mod h1:Ml88TJTwZCj6yHDkAEN/EhxVutzSlk+kDZgfibRIqF0= github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240709043547-03612098f799 h1:HyLTySm7BR+oNfZqDTkVJ25wnmcTtxBBD31UkFL+kEM= github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240709043547-03612098f799/go.mod h1:UVFRacRkP7O7TQAzFmR52v5mUlxf+G1ovMlCQAB/cHU= github.com/smartcontractkit/chainlink-testing-framework v1.34.2 h1:YL3ft7KJB7SAopdmJeyeR4/kv0j4jOdagNihXq8OZ38= diff --git a/integration-tests/load/go.mod b/integration-tests/load/go.mod index 1aa754f8cfa..c464231c745 100644 --- a/integration-tests/load/go.mod +++ b/integration-tests/load/go.mod @@ -372,7 +372,7 @@ require ( github.com/smartcontractkit/chain-selectors v1.0.10 // indirect github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240801131703-fd75761c982f // indirect github.com/smartcontractkit/chainlink-feeds v0.0.0-20240710170203-5b41615da827 // indirect - github.com/smartcontractkit/chainlink-solana v1.1.0 // indirect + github.com/smartcontractkit/chainlink-solana v1.1.1-0.20240806154405-8e5684f98564 // indirect github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240709043547-03612098f799 // indirect github.com/smartcontractkit/chainlink-testing-framework/grafana v0.0.0-20240405215812-5a72bc9af239 // indirect github.com/smartcontractkit/havoc/k8schaos v0.0.0-20240409145249-e78d20847e37 // indirect diff --git a/integration-tests/load/go.sum b/integration-tests/load/go.sum index 698623c50f1..d1d6f3a4d52 100644 --- a/integration-tests/load/go.sum +++ b/integration-tests/load/go.sum @@ -1478,8 +1478,8 @@ github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240801131703-fd75761 github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240801131703-fd75761c982f/go.mod h1:V/86loaFSH0dqqUEHqyXVbyNqDRSjvcf9BRomWFTljU= github.com/smartcontractkit/chainlink-feeds v0.0.0-20240710170203-5b41615da827 h1:BCHu4pNP6arrcHLEWx61XjLaonOd2coQNyL0NTUcaMc= github.com/smartcontractkit/chainlink-feeds v0.0.0-20240710170203-5b41615da827/go.mod h1:OPX+wC2TWQsyLNpR7daMt2vMpmsNcoBxbZyGTHr6tiA= -github.com/smartcontractkit/chainlink-solana v1.1.0 h1:+xBeVqx2x0Sx3CBbF8RLSblczsxJDYTkta8h7i8+23I= -github.com/smartcontractkit/chainlink-solana v1.1.0/go.mod h1:Ml88TJTwZCj6yHDkAEN/EhxVutzSlk+kDZgfibRIqF0= +github.com/smartcontractkit/chainlink-solana v1.1.1-0.20240806154405-8e5684f98564 h1:8ZzsGNhqYxmQ/QMO1fuXO7u9Vpl9YUvPJK+td/ZaBJA= +github.com/smartcontractkit/chainlink-solana v1.1.1-0.20240806154405-8e5684f98564/go.mod h1:Ml88TJTwZCj6yHDkAEN/EhxVutzSlk+kDZgfibRIqF0= github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240709043547-03612098f799 h1:HyLTySm7BR+oNfZqDTkVJ25wnmcTtxBBD31UkFL+kEM= github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240709043547-03612098f799/go.mod h1:UVFRacRkP7O7TQAzFmR52v5mUlxf+G1ovMlCQAB/cHU= github.com/smartcontractkit/chainlink-testing-framework v1.34.2 h1:YL3ft7KJB7SAopdmJeyeR4/kv0j4jOdagNihXq8OZ38= From 5a1dd1f74007f365bc52e323e5d6a3a638e6d7ed Mon Sep 17 00:00:00 2001 From: Akshay Aggarwal Date: Wed, 7 Aug 2024 15:49:50 +0100 Subject: [PATCH 4/9] Update log trigger default values (#14051) --- .../ocr2keeper/evmregistry/v21/logprovider/factory.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/logprovider/factory.go b/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/logprovider/factory.go index 7ec65ff4740..25cc5e939ba 100644 --- a/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/logprovider/factory.go +++ b/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/logprovider/factory.go @@ -74,7 +74,7 @@ func (o *LogTriggersOptions) Defaults(finalityDepth int64) { func (o *LogTriggersOptions) defaultBlockRate() uint32 { switch o.chainID.Int64() { - case 42161, 421613, 421614: // Arbitrum + case 42161, 421613, 421614: // Arbitrum, Arb Goerli, Arb Sepolia return 2 default: return 1 @@ -83,10 +83,10 @@ func (o *LogTriggersOptions) defaultBlockRate() uint32 { func (o *LogTriggersOptions) defaultLogLimit() uint32 { switch o.chainID.Int64() { - case 1, 4, 5, 42, 11155111: // Eth + case 1, 4, 5, 42, 11155111: // Eth, Rinkeby, Goerli, Kovan, Sepolia return 20 - case 10, 420, 56, 97, 137, 80001, 43113, 43114, 8453, 84531: // Optimism, BSC, Polygon, Avax, Base - return 5 + case 10, 420, 11155420, 56, 97, 137, 80001, 80002, 43114, 43113, 8453, 84531, 84532: // Optimism, OP Goerli, OP Sepolia, BSC, BSC Test, Polygon, Mumbai, Amoy, Avax, Avax Fuji, Base, Base Goerli, Base Sepolia + return 4 default: return 1 } From e500c1a471c4e9bb66a89ff27a763a83824f767c Mon Sep 17 00:00:00 2001 From: frank zhu Date: Wed, 7 Aug 2024 10:02:24 -0500 Subject: [PATCH 5/9] chore: update dependabot config gomod (#14063) --- .github/dependabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 19e008c8ce4..cea4f07b90d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,7 +4,7 @@ updates: directory: "/" schedule: interval: monthly - open-pull-requests-limit: 10 + open-pull-requests-limit: 0 ignore: # Old versions are pinned for libocr. - dependency-name: github.com/libp2p/go-libp2p-core From 215277f9e041d18dc5686c697e6959d5edaaf346 Mon Sep 17 00:00:00 2001 From: FelixFan1992 Date: Wed, 7 Aug 2024 11:21:31 -0400 Subject: [PATCH 6/9] auto-10161: replicate v2_3 to v2_3_zksync (#14035) * auto-10161: replicate v2_3 to v2_3_zksync * update * small fixes * add an zksync automation forwarder * fix linter * update * update * lint --- contracts/.changeset/loud-lobsters-guess.md | 5 + contracts/.solhintignore | 1 + .../native_solc_compile_all_automation | 2 +- .../automation/ZKSyncAutomationForwarder.sol | 92 ++ .../v0.8/automation/test/{v2_3 => }/WETH9.sol | 0 .../v0.8/automation/test/v2_3/BaseTest.t.sol | 4 +- .../ZKSyncAutomationRegistry2_3.sol | 391 ++++++ .../ZKSyncAutomationRegistryBase2_3.sol | 1216 +++++++++++++++++ .../ZKSyncAutomationRegistryLogicA2_3.sol | 283 ++++ .../ZKSyncAutomationRegistryLogicB2_3.sol | 449 ++++++ .../ZKSyncAutomationRegistryLogicC2_3.sol | 638 +++++++++ .../automation/AutomationRegistry2_3.test.ts | 1 - contracts/test/v0.8/automation/helpers.ts | 4 +- 13 files changed, 3080 insertions(+), 6 deletions(-) create mode 100644 contracts/.changeset/loud-lobsters-guess.md create mode 100644 contracts/src/v0.8/automation/ZKSyncAutomationForwarder.sol rename contracts/src/v0.8/automation/test/{v2_3 => }/WETH9.sol (100%) create mode 100644 contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistry2_3.sol create mode 100644 contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryBase2_3.sol create mode 100644 contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryLogicA2_3.sol create mode 100644 contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryLogicB2_3.sol create mode 100644 contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryLogicC2_3.sol diff --git a/contracts/.changeset/loud-lobsters-guess.md b/contracts/.changeset/loud-lobsters-guess.md new file mode 100644 index 00000000000..e470267e4e4 --- /dev/null +++ b/contracts/.changeset/loud-lobsters-guess.md @@ -0,0 +1,5 @@ +--- +'@chainlink/contracts': patch +--- + +auto: create a replication from v2_3 to v2_3_zksync diff --git a/contracts/.solhintignore b/contracts/.solhintignore index bad1935442b..55d195c3059 100644 --- a/contracts/.solhintignore +++ b/contracts/.solhintignore @@ -18,6 +18,7 @@ ./src/v0.8/automation/libraries/internal/Cron.sol ./src/v0.8/automation/AutomationForwarder.sol ./src/v0.8/automation/AutomationForwarderLogic.sol +./src/v0.8/automation/ZKSyncAutomationForwarder.sol ./src/v0.8/automation/interfaces/v2_2/IAutomationRegistryMaster.sol ./src/v0.8/automation/interfaces/v2_3/IAutomationRegistryMaster2_3.sol diff --git a/contracts/scripts/native_solc_compile_all_automation b/contracts/scripts/native_solc_compile_all_automation index f144e4f7dc8..29326a15c05 100755 --- a/contracts/scripts/native_solc_compile_all_automation +++ b/contracts/scripts/native_solc_compile_all_automation @@ -108,4 +108,4 @@ compileContract automation/v2_3/AutomationUtils2_3.sol compileContract automation/interfaces/v2_3/IAutomationRegistryMaster2_3.sol compileContract automation/testhelpers/MockETHUSDAggregator.sol -compileContract automation/test/v2_3/WETH9.sol +compileContract automation/test/WETH9.sol diff --git a/contracts/src/v0.8/automation/ZKSyncAutomationForwarder.sol b/contracts/src/v0.8/automation/ZKSyncAutomationForwarder.sol new file mode 100644 index 00000000000..cfbff1365e1 --- /dev/null +++ b/contracts/src/v0.8/automation/ZKSyncAutomationForwarder.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.16; + +import {IAutomationRegistryConsumer} from "./interfaces/IAutomationRegistryConsumer.sol"; + +uint256 constant PERFORM_GAS_CUSHION = 5_000; + +/** + * @title AutomationForwarder is a relayer that sits between the registry and the customer's target contract + * @dev The purpose of the forwarder is to give customers a consistent address to authorize against, + * which stays consistent between migrations. The Forwarder also exposes the registry address, so that users who + * want to programmatically interact with the registry (ie top up funds) can do so. + */ +contract ZKSyncAutomationForwarder { + /// @notice the user's target contract address + address private immutable i_target; + + /// @notice the shared logic address + address private immutable i_logic; + + IAutomationRegistryConsumer private s_registry; + + constructor(address target, address registry, address logic) { + s_registry = IAutomationRegistryConsumer(registry); + i_target = target; + i_logic = logic; + } + + /** + * @notice forward is called by the registry and forwards the call to the target + * @param gasAmount is the amount of gas to use in the call + * @param data is the 4 bytes function selector + arbitrary function data + * @return success indicating whether the target call succeeded or failed + */ + function forward(uint256 gasAmount, bytes memory data) external returns (bool success, uint256 gasUsed) { + if (msg.sender != address(s_registry)) revert(); + address target = i_target; + gasUsed = gasleft(); + assembly { + let g := gas() + // Compute g -= PERFORM_GAS_CUSHION and check for underflow + if lt(g, PERFORM_GAS_CUSHION) { + revert(0, 0) + } + g := sub(g, PERFORM_GAS_CUSHION) + // if g - g//64 <= gasAmount, revert + // (we subtract g//64 because of EIP-150) + if iszero(gt(sub(g, div(g, 64)), gasAmount)) { + revert(0, 0) + } + // solidity calls check that a contract actually exists at the destination, so we do the same + if iszero(extcodesize(target)) { + revert(0, 0) + } + // call with exact gas + success := call(gasAmount, target, 0, add(data, 0x20), mload(data), 0, 0) + } + gasUsed = gasUsed - gasleft(); + return (success, gasUsed); + } + + function getTarget() external view returns (address) { + return i_target; + } + + fallback() external { + // copy to memory for assembly access + address logic = i_logic; + // copied directly from OZ's Proxy contract + assembly { + // Copy msg.data. We take full control of memory in this inline assembly + // block because it will not return to Solidity code. We overwrite the + // Solidity scratch pad at memory position 0. + calldatacopy(0, 0, calldatasize()) + + // out and outsize are 0 because we don't know the size yet. + let result := delegatecall(gas(), logic, 0, calldatasize(), 0, 0) + + // Copy the returned data. + returndatacopy(0, 0, returndatasize()) + + switch result + // delegatecall returns 0 on error. + case 0 { + revert(0, returndatasize()) + } + default { + return(0, returndatasize()) + } + } + } +} diff --git a/contracts/src/v0.8/automation/test/v2_3/WETH9.sol b/contracts/src/v0.8/automation/test/WETH9.sol similarity index 100% rename from contracts/src/v0.8/automation/test/v2_3/WETH9.sol rename to contracts/src/v0.8/automation/test/WETH9.sol diff --git a/contracts/src/v0.8/automation/test/v2_3/BaseTest.t.sol b/contracts/src/v0.8/automation/test/v2_3/BaseTest.t.sol index 9016f52c55d..9e46e7bb40d 100644 --- a/contracts/src/v0.8/automation/test/v2_3/BaseTest.t.sol +++ b/contracts/src/v0.8/automation/test/v2_3/BaseTest.t.sol @@ -20,14 +20,14 @@ import {ChainModuleBase} from "../../chains/ChainModuleBase.sol"; import {IERC20Metadata as IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import {MockUpkeep} from "../../mocks/MockUpkeep.sol"; import {IWrappedNative} from "../../interfaces/v2_3/IWrappedNative.sol"; -import {WETH9} from "./WETH9.sol"; +import {WETH9} from "../WETH9.sol"; /** * @title BaseTest provides basic test setup procedures and dependencies for use by other * unit tests */ contract BaseTest is Test { - // test state (not exposed to derrived tests) + // test state (not exposed to derived tests) uint256 private nonce; // constants diff --git a/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistry2_3.sol b/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistry2_3.sol new file mode 100644 index 00000000000..027fe59aca7 --- /dev/null +++ b/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistry2_3.sol @@ -0,0 +1,391 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.19; + +import {EnumerableSet} from "../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/structs/EnumerableSet.sol"; +import {Address} from "../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/Address.sol"; +import {ZKSyncAutomationRegistryBase2_3} from "./ZKSyncAutomationRegistryBase2_3.sol"; +import {ZKSyncAutomationRegistryLogicA2_3} from "./ZKSyncAutomationRegistryLogicA2_3.sol"; +import {ZKSyncAutomationRegistryLogicC2_3} from "./ZKSyncAutomationRegistryLogicC2_3.sol"; +import {Chainable} from "../Chainable.sol"; +import {OCR2Abstract} from "../../shared/ocr2/OCR2Abstract.sol"; +import {IERC20Metadata as IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +/** + * @notice Registry for adding work for Chainlink nodes to perform on client + * contracts. Clients must support the AutomationCompatibleInterface interface. + */ +contract ZKSyncAutomationRegistry2_3 is ZKSyncAutomationRegistryBase2_3, OCR2Abstract, Chainable { + using Address for address; + using EnumerableSet for EnumerableSet.UintSet; + using EnumerableSet for EnumerableSet.AddressSet; + + /** + * @notice versions: + * AutomationRegistry 2.3.0: supports native and ERC20 billing + * changes flat fee to USD-denominated + * adds support for custom billing overrides + * AutomationRegistry 2.2.0: moves chain-specific integration code into a separate module + * KeeperRegistry 2.1.0: introduces support for log triggers + * removes the need for "wrapped perform data" + * KeeperRegistry 2.0.2: pass revert bytes as performData when target contract reverts + * fixes issue with arbitrum block number + * does an early return in case of stale report instead of revert + * KeeperRegistry 2.0.1: implements workaround for buggy migrate function in 1.X + * KeeperRegistry 2.0.0: implement OCR interface + * KeeperRegistry 1.3.0: split contract into Proxy and Logic + * account for Arbitrum and Optimism L1 gas fee + * allow users to configure upkeeps + * KeeperRegistry 1.2.0: allow funding within performUpkeep + * allow configurable registry maxPerformGas + * add function to let admin change upkeep gas limit + * add minUpkeepSpend requirement + * upgrade to solidity v0.8 + * KeeperRegistry 1.1.0: added flatFeeMicroLink + * KeeperRegistry 1.0.0: initial release + */ + string public constant override typeAndVersion = "AutomationRegistry 2.3.0"; + + /** + * @param logicA the address of the first logic contract + * @dev we cast the contract to logicC in order to call logicC functions (via fallback) + */ + constructor( + ZKSyncAutomationRegistryLogicA2_3 logicA + ) + ZKSyncAutomationRegistryBase2_3( + ZKSyncAutomationRegistryLogicC2_3(address(logicA)).getLinkAddress(), + ZKSyncAutomationRegistryLogicC2_3(address(logicA)).getLinkUSDFeedAddress(), + ZKSyncAutomationRegistryLogicC2_3(address(logicA)).getNativeUSDFeedAddress(), + ZKSyncAutomationRegistryLogicC2_3(address(logicA)).getFastGasFeedAddress(), + ZKSyncAutomationRegistryLogicC2_3(address(logicA)).getAutomationForwarderLogic(), + ZKSyncAutomationRegistryLogicC2_3(address(logicA)).getAllowedReadOnlyAddress(), + ZKSyncAutomationRegistryLogicC2_3(address(logicA)).getPayoutMode(), + ZKSyncAutomationRegistryLogicC2_3(address(logicA)).getWrappedNativeTokenAddress() + ) + Chainable(address(logicA)) + {} + + /** + * @notice holds the variables used in the transmit function, necessary to avoid stack too deep errors + */ + struct TransmitVars { + uint16 numUpkeepsPassedChecks; + uint96 totalReimbursement; + uint96 totalPremium; + uint256 totalCalldataWeight; + } + + // ================================================================ + // | HOT PATH ACTIONS | + // ================================================================ + + /** + * @inheritdoc OCR2Abstract + */ + function transmit( + bytes32[3] calldata reportContext, + bytes calldata rawReport, + bytes32[] calldata rs, + bytes32[] calldata ss, + bytes32 rawVs + ) external override { + uint256 gasOverhead = gasleft(); + // use this msg.data length check to ensure no extra data is included in the call + // 4 is first 4 bytes of the keccak-256 hash of the function signature. ss.length == rs.length so use one of them + // 4 + (32 * 3) + (rawReport.length + 32 + 32) + (32 * rs.length + 32 + 32) + (32 * ss.length + 32 + 32) + 32 + uint256 requiredLength = 324 + rawReport.length + 64 * rs.length; + if (msg.data.length != requiredLength) revert InvalidDataLength(); + HotVars memory hotVars = s_hotVars; + + if (hotVars.paused) revert RegistryPaused(); + if (!s_transmitters[msg.sender].active) revert OnlyActiveTransmitters(); + + // Verify signatures + if (s_latestConfigDigest != reportContext[0]) revert ConfigDigestMismatch(); + if (rs.length != hotVars.f + 1 || rs.length != ss.length) revert IncorrectNumberOfSignatures(); + _verifyReportSignature(reportContext, rawReport, rs, ss, rawVs); + + Report memory report = _decodeReport(rawReport); + + uint40 epochAndRound = uint40(uint256(reportContext[1])); + uint32 epoch = uint32(epochAndRound >> 8); + + _handleReport(hotVars, report, gasOverhead); + + if (epoch > hotVars.latestEpoch) { + s_hotVars.latestEpoch = epoch; + } + } + + /** + * @notice handles the report by performing the upkeeps and updating the state + * @param hotVars the hot variables of the registry + * @param report the report to be handled (already verified and decoded) + * @param gasOverhead the running tally of gas overhead to be split across the upkeeps + * @dev had to split this function from transmit() to avoid stack too deep errors + * @dev all other internal / private functions are generally defined in the Base contract + * we leave this here because it is essentially a continuation of the transmit() function, + */ + function _handleReport(HotVars memory hotVars, Report memory report, uint256 gasOverhead) private { + UpkeepTransmitInfo[] memory upkeepTransmitInfo = new UpkeepTransmitInfo[](report.upkeepIds.length); + TransmitVars memory transmitVars = TransmitVars({ + numUpkeepsPassedChecks: 0, + totalCalldataWeight: 0, + totalReimbursement: 0, + totalPremium: 0 + }); + + uint256 blocknumber = hotVars.chainModule.blockNumber(); + uint256 l1Fee = hotVars.chainModule.getCurrentL1Fee(); + + for (uint256 i = 0; i < report.upkeepIds.length; i++) { + upkeepTransmitInfo[i].upkeep = s_upkeep[report.upkeepIds[i]]; + upkeepTransmitInfo[i].triggerType = _getTriggerType(report.upkeepIds[i]); + + (upkeepTransmitInfo[i].earlyChecksPassed, upkeepTransmitInfo[i].dedupID) = _prePerformChecks( + report.upkeepIds[i], + blocknumber, + report.triggers[i], + upkeepTransmitInfo[i], + hotVars + ); + + if (upkeepTransmitInfo[i].earlyChecksPassed) { + transmitVars.numUpkeepsPassedChecks += 1; + } else { + continue; + } + + // Actually perform the target upkeep + (upkeepTransmitInfo[i].performSuccess, upkeepTransmitInfo[i].gasUsed) = _performUpkeep( + upkeepTransmitInfo[i].upkeep.forwarder, + report.gasLimits[i], + report.performDatas[i] + ); + + // To split L1 fee across the upkeeps, assign a weight to this upkeep based on the length + // of the perform data and calldata overhead + upkeepTransmitInfo[i].calldataWeight = + report.performDatas[i].length + + TRANSMIT_CALLDATA_FIXED_BYTES_OVERHEAD + + (TRANSMIT_CALLDATA_PER_SIGNER_BYTES_OVERHEAD * (hotVars.f + 1)); + transmitVars.totalCalldataWeight += upkeepTransmitInfo[i].calldataWeight; + + // Deduct the gasUsed by upkeep from the overhead tally - upkeeps pay for their own gas individually + gasOverhead -= upkeepTransmitInfo[i].gasUsed; + + // Store last perform block number / deduping key for upkeep + _updateTriggerMarker(report.upkeepIds[i], blocknumber, upkeepTransmitInfo[i]); + } + // No upkeeps to be performed in this report + if (transmitVars.numUpkeepsPassedChecks == 0) { + return; + } + + // This is the overall gas overhead that will be split across performed upkeeps + // Take upper bound of 16 gas per callData bytes + gasOverhead = (gasOverhead - gasleft()) + (16 * msg.data.length) + ACCOUNTING_FIXED_GAS_OVERHEAD; + gasOverhead = gasOverhead / transmitVars.numUpkeepsPassedChecks + ACCOUNTING_PER_UPKEEP_GAS_OVERHEAD; + + { + BillingTokenPaymentParams memory billingTokenParams; + uint256 nativeUSD = _getNativeUSD(hotVars); + for (uint256 i = 0; i < report.upkeepIds.length; i++) { + if (upkeepTransmitInfo[i].earlyChecksPassed) { + if (i == 0 || upkeepTransmitInfo[i].upkeep.billingToken != upkeepTransmitInfo[i - 1].upkeep.billingToken) { + billingTokenParams = _getBillingTokenPaymentParams(hotVars, upkeepTransmitInfo[i].upkeep.billingToken); + } + PaymentReceipt memory receipt = _handlePayment( + hotVars, + PaymentParams({ + gasLimit: upkeepTransmitInfo[i].gasUsed, + gasOverhead: gasOverhead, + l1CostWei: (l1Fee * upkeepTransmitInfo[i].calldataWeight) / transmitVars.totalCalldataWeight, + fastGasWei: report.fastGasWei, + linkUSD: report.linkUSD, + nativeUSD: nativeUSD, + billingToken: upkeepTransmitInfo[i].upkeep.billingToken, + billingTokenParams: billingTokenParams, + isTransaction: true + }), + report.upkeepIds[i], + upkeepTransmitInfo[i].upkeep + ); + transmitVars.totalPremium += receipt.premiumInJuels; + transmitVars.totalReimbursement += receipt.gasReimbursementInJuels; + + emit UpkeepPerformed( + report.upkeepIds[i], + upkeepTransmitInfo[i].performSuccess, + receipt.gasChargeInBillingToken + receipt.premiumInBillingToken, + upkeepTransmitInfo[i].gasUsed, + gasOverhead, + report.triggers[i] + ); + } + } + } + // record payments to NOPs, all in LINK + s_transmitters[msg.sender].balance += transmitVars.totalReimbursement; + s_hotVars.totalPremium += transmitVars.totalPremium; + s_reserveAmounts[IERC20(address(i_link))] += transmitVars.totalReimbursement + transmitVars.totalPremium; + } + + // ================================================================ + // | OCR2ABSTRACT | + // ================================================================ + + /** + * @inheritdoc OCR2Abstract + * @dev prefer the type-safe version of setConfig (below) whenever possible. The OnchainConfig could differ between registry versions + * @dev this function takes up precious space on the root contract, but must be implemented to conform to the OCR2Abstract interface + */ + function setConfig( + address[] memory signers, + address[] memory transmitters, + uint8 f, + bytes memory onchainConfigBytes, + uint64 offchainConfigVersion, + bytes memory offchainConfig + ) external override { + (OnchainConfig memory config, IERC20[] memory billingTokens, BillingConfig[] memory billingConfigs) = abi.decode( + onchainConfigBytes, + (OnchainConfig, IERC20[], BillingConfig[]) + ); + + setConfigTypeSafe( + signers, + transmitters, + f, + config, + offchainConfigVersion, + offchainConfig, + billingTokens, + billingConfigs + ); + } + + /** + * @notice sets the configuration for the registry + * @param signers the list of permitted signers + * @param transmitters the list of permitted transmitters + * @param f the maximum tolerance for faulty nodes + * @param onchainConfig configuration values that are used on-chain + * @param offchainConfigVersion the version of the offchainConfig + * @param offchainConfig configuration values that are used off-chain + * @param billingTokens the list of valid billing tokens + * @param billingConfigs the configurations for each billing token + */ + function setConfigTypeSafe( + address[] memory signers, + address[] memory transmitters, + uint8 f, + OnchainConfig memory onchainConfig, + uint64 offchainConfigVersion, + bytes memory offchainConfig, + IERC20[] memory billingTokens, + BillingConfig[] memory billingConfigs + ) public onlyOwner { + if (signers.length > MAX_NUM_ORACLES) revert TooManyOracles(); + if (f == 0) revert IncorrectNumberOfFaultyOracles(); + if (signers.length != transmitters.length || signers.length <= 3 * f) revert IncorrectNumberOfSigners(); + if (billingTokens.length != billingConfigs.length) revert ParameterLengthError(); + // set billing config for tokens + _setBillingConfig(billingTokens, billingConfigs); + + _updateTransmitters(signers, transmitters); + + s_hotVars = HotVars({ + f: f, + stalenessSeconds: onchainConfig.stalenessSeconds, + gasCeilingMultiplier: onchainConfig.gasCeilingMultiplier, + paused: s_hotVars.paused, + reentrancyGuard: s_hotVars.reentrancyGuard, + totalPremium: s_hotVars.totalPremium, + latestEpoch: 0, // DON restarts epoch + reorgProtectionEnabled: onchainConfig.reorgProtectionEnabled, + chainModule: onchainConfig.chainModule + }); + + uint32 previousConfigBlockNumber = s_storage.latestConfigBlockNumber; + uint32 newLatestConfigBlockNumber = uint32(onchainConfig.chainModule.blockNumber()); + uint32 newConfigCount = s_storage.configCount + 1; + + s_storage = Storage({ + checkGasLimit: onchainConfig.checkGasLimit, + maxPerformGas: onchainConfig.maxPerformGas, + transcoder: onchainConfig.transcoder, + maxCheckDataSize: onchainConfig.maxCheckDataSize, + maxPerformDataSize: onchainConfig.maxPerformDataSize, + maxRevertDataSize: onchainConfig.maxRevertDataSize, + upkeepPrivilegeManager: onchainConfig.upkeepPrivilegeManager, + financeAdmin: onchainConfig.financeAdmin, + nonce: s_storage.nonce, + configCount: newConfigCount, + latestConfigBlockNumber: newLatestConfigBlockNumber + }); + s_fallbackGasPrice = onchainConfig.fallbackGasPrice; + s_fallbackLinkPrice = onchainConfig.fallbackLinkPrice; + s_fallbackNativePrice = onchainConfig.fallbackNativePrice; + + bytes memory onchainConfigBytes = abi.encode(onchainConfig); + + s_latestConfigDigest = _configDigestFromConfigData( + block.chainid, + address(this), + s_storage.configCount, + signers, + transmitters, + f, + onchainConfigBytes, + offchainConfigVersion, + offchainConfig + ); + + for (uint256 idx = s_registrars.length(); idx > 0; idx--) { + s_registrars.remove(s_registrars.at(idx - 1)); + } + + for (uint256 idx = 0; idx < onchainConfig.registrars.length; idx++) { + s_registrars.add(onchainConfig.registrars[idx]); + } + + emit ConfigSet( + previousConfigBlockNumber, + s_latestConfigDigest, + s_storage.configCount, + signers, + transmitters, + f, + onchainConfigBytes, + offchainConfigVersion, + offchainConfig + ); + } + + /** + * @inheritdoc OCR2Abstract + * @dev this function takes up precious space on the root contract, but must be implemented to conform to the OCR2Abstract interface + */ + function latestConfigDetails() + external + view + override + returns (uint32 configCount, uint32 blockNumber, bytes32 configDigest) + { + return (s_storage.configCount, s_storage.latestConfigBlockNumber, s_latestConfigDigest); + } + + /** + * @inheritdoc OCR2Abstract + * @dev this function takes up precious space on the root contract, but must be implemented to conform to the OCR2Abstract interface + */ + function latestConfigDigestAndEpoch() + external + view + override + returns (bool scanLogs, bytes32 configDigest, uint32 epoch) + { + return (false, s_latestConfigDigest, s_hotVars.latestEpoch); + } +} diff --git a/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryBase2_3.sol b/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryBase2_3.sol new file mode 100644 index 00000000000..524ecacc826 --- /dev/null +++ b/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryBase2_3.sol @@ -0,0 +1,1216 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.19; + +import {EnumerableSet} from "../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/structs/EnumerableSet.sol"; +import {Address} from "../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/Address.sol"; +import {StreamsLookupCompatibleInterface} from "../interfaces/StreamsLookupCompatibleInterface.sol"; +import {ILogAutomation, Log} from "../interfaces/ILogAutomation.sol"; +import {IAutomationForwarder} from "../interfaces/IAutomationForwarder.sol"; +import {ConfirmedOwner} from "../../shared/access/ConfirmedOwner.sol"; +import {AggregatorV3Interface} from "../../shared/interfaces/AggregatorV3Interface.sol"; +import {LinkTokenInterface} from "../../shared/interfaces/LinkTokenInterface.sol"; +import {KeeperCompatibleInterface} from "../interfaces/KeeperCompatibleInterface.sol"; +import {IChainModule} from "../interfaces/IChainModule.sol"; +import {IERC20Metadata as IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {SafeCast} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/math/SafeCast.sol"; +import {IWrappedNative} from "../interfaces/v2_3/IWrappedNative.sol"; + +/** + * @notice Base Keeper Registry contract, contains shared logic between + * AutomationRegistry and AutomationRegistryLogic + * @dev all errors, events, and internal functions should live here + */ +// solhint-disable-next-line max-states-count +abstract contract ZKSyncAutomationRegistryBase2_3 is ConfirmedOwner { + using Address for address; + using EnumerableSet for EnumerableSet.UintSet; + using EnumerableSet for EnumerableSet.AddressSet; + + address internal constant ZERO_ADDRESS = address(0); + address internal constant IGNORE_ADDRESS = 0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF; + bytes4 internal constant CHECK_SELECTOR = KeeperCompatibleInterface.checkUpkeep.selector; + bytes4 internal constant PERFORM_SELECTOR = KeeperCompatibleInterface.performUpkeep.selector; + bytes4 internal constant CHECK_CALLBACK_SELECTOR = StreamsLookupCompatibleInterface.checkCallback.selector; + bytes4 internal constant CHECK_LOG_SELECTOR = ILogAutomation.checkLog.selector; + uint256 internal constant PERFORM_GAS_MIN = 2_300; + uint256 internal constant CANCELLATION_DELAY = 50; + uint256 internal constant PERFORM_GAS_CUSHION = 5_000; + uint256 internal constant PPB_BASE = 1_000_000_000; + uint32 internal constant UINT32_MAX = type(uint32).max; + // The first byte of the mask can be 0, because we only ever have 31 oracles + uint256 internal constant ORACLE_MASK = 0x0001010101010101010101010101010101010101010101010101010101010101; + uint8 internal constant UPKEEP_VERSION_BASE = 4; + + // Next block of constants are only used in maxPayment estimation during checkUpkeep simulation + // These values are calibrated using hardhat tests which simulate various cases and verify that + // the variables result in accurate estimation + uint256 internal constant REGISTRY_CONDITIONAL_OVERHEAD = 98_200; // Fixed gas overhead for conditional upkeeps + uint256 internal constant REGISTRY_LOG_OVERHEAD = 122_500; // Fixed gas overhead for log upkeeps + uint256 internal constant REGISTRY_PER_SIGNER_GAS_OVERHEAD = 5_600; // Value scales with f + uint256 internal constant REGISTRY_PER_PERFORM_BYTE_GAS_OVERHEAD = 24; // Per perform data byte overhead + + // The overhead (in bytes) in addition to perform data for upkeep sent in calldata + // This includes overhead for all struct encoding as well as report signatures + // There is a fixed component and a per signer component. This is calculated exactly by doing abi encoding + uint256 internal constant TRANSMIT_CALLDATA_FIXED_BYTES_OVERHEAD = 932; + uint256 internal constant TRANSMIT_CALLDATA_PER_SIGNER_BYTES_OVERHEAD = 64; + + // Next block of constants are used in actual payment calculation. We calculate the exact gas used within the + // tx itself, but since payment processing itself takes gas, and it needs the overhead as input, we use fixed constants + // to account for gas used in payment processing. These values are calibrated using hardhat tests which simulates various cases and verifies that + // the variables result in accurate estimation + uint256 internal constant ACCOUNTING_FIXED_GAS_OVERHEAD = 51_200; // Fixed overhead per tx + uint256 internal constant ACCOUNTING_PER_UPKEEP_GAS_OVERHEAD = 14_200; // Overhead per upkeep performed in batch + + LinkTokenInterface internal immutable i_link; + AggregatorV3Interface internal immutable i_linkUSDFeed; + AggregatorV3Interface internal immutable i_nativeUSDFeed; + AggregatorV3Interface internal immutable i_fastGasFeed; + address internal immutable i_automationForwarderLogic; + address internal immutable i_allowedReadOnlyAddress; + IWrappedNative internal immutable i_wrappedNativeToken; + + /** + * @dev - The storage is gas optimised for one and only one function - transmit. All the storage accessed in transmit + * is stored compactly. Rest of the storage layout is not of much concern as transmit is the only hot path + */ + + // Upkeep storage + EnumerableSet.UintSet internal s_upkeepIDs; + mapping(uint256 => Upkeep) internal s_upkeep; // accessed during transmit + mapping(uint256 => address) internal s_upkeepAdmin; + mapping(uint256 => address) internal s_proposedAdmin; + mapping(uint256 => bytes) internal s_checkData; + mapping(bytes32 => bool) internal s_dedupKeys; + // Registry config and state + EnumerableSet.AddressSet internal s_registrars; + mapping(address => Transmitter) internal s_transmitters; + mapping(address => Signer) internal s_signers; + address[] internal s_signersList; // s_signersList contains the signing address of each oracle + address[] internal s_transmittersList; // s_transmittersList contains the transmission address of each oracle + EnumerableSet.AddressSet internal s_deactivatedTransmitters; + mapping(address => address) internal s_transmitterPayees; // s_payees contains the mapping from transmitter to payee. + mapping(address => address) internal s_proposedPayee; // proposed payee for a transmitter + bytes32 internal s_latestConfigDigest; // Read on transmit path in case of signature verification + HotVars internal s_hotVars; // Mixture of config and state, used in transmit + Storage internal s_storage; // Mixture of config and state, not used in transmit + uint256 internal s_fallbackGasPrice; + uint256 internal s_fallbackLinkPrice; + uint256 internal s_fallbackNativePrice; + mapping(address => MigrationPermission) internal s_peerRegistryMigrationPermission; // Permissions for migration to and fro + mapping(uint256 => bytes) internal s_upkeepTriggerConfig; // upkeep triggers + mapping(uint256 => bytes) internal s_upkeepOffchainConfig; // general config set by users for each upkeep + mapping(uint256 => bytes) internal s_upkeepPrivilegeConfig; // general config set by an administrative role for an upkeep + mapping(address => bytes) internal s_adminPrivilegeConfig; // general config set by an administrative role for an admin + // billing + mapping(IERC20 billingToken => uint256 reserveAmount) internal s_reserveAmounts; // unspent user deposits + unwithdrawn NOP payments + mapping(IERC20 billingToken => BillingConfig billingConfig) internal s_billingConfigs; // billing configurations for different tokens + mapping(uint256 upkeepID => BillingOverrides billingOverrides) internal s_billingOverrides; // billing overrides for specific upkeeps + IERC20[] internal s_billingTokens; // list of billing tokens + PayoutMode internal s_payoutMode; + + error ArrayHasNoEntries(); + error CannotCancel(); + error CheckDataExceedsLimit(); + error ConfigDigestMismatch(); + error DuplicateEntry(); + error DuplicateSigners(); + error GasLimitCanOnlyIncrease(); + error GasLimitOutsideRange(); + error IncorrectNumberOfFaultyOracles(); + error IncorrectNumberOfSignatures(); + error IncorrectNumberOfSigners(); + error IndexOutOfRange(); + error InsufficientBalance(uint256 available, uint256 requested); + error InsufficientLinkLiquidity(); + error InvalidDataLength(); + error InvalidFeed(); + error InvalidTrigger(); + error InvalidPayee(); + error InvalidRecipient(); + error InvalidReport(); + error InvalidSigner(); + error InvalidToken(); + error InvalidTransmitter(); + error InvalidTriggerType(); + error MigrationNotPermitted(); + error MustSettleOffchain(); + error MustSettleOnchain(); + error NotAContract(); + error OnlyActiveSigners(); + error OnlyActiveTransmitters(); + error OnlyCallableByAdmin(); + error OnlyCallableByLINKToken(); + error OnlyCallableByOwnerOrAdmin(); + error OnlyCallableByOwnerOrRegistrar(); + error OnlyCallableByPayee(); + error OnlyCallableByProposedAdmin(); + error OnlyCallableByProposedPayee(); + error OnlyCallableByUpkeepPrivilegeManager(); + error OnlyFinanceAdmin(); + error OnlyPausedUpkeep(); + error OnlySimulatedBackend(); + error OnlyUnpausedUpkeep(); + error ParameterLengthError(); + error ReentrantCall(); + error RegistryPaused(); + error RepeatedSigner(); + error RepeatedTransmitter(); + error TargetCheckReverted(bytes reason); + error TooManyOracles(); + error TranscoderNotSet(); + error TransferFailed(); + error UpkeepAlreadyExists(); + error UpkeepCancelled(); + error UpkeepNotCanceled(); + error UpkeepNotNeeded(); + error ValueNotChanged(); + error ZeroAddressNotAllowed(); + + enum MigrationPermission { + NONE, + OUTGOING, + INCOMING, + BIDIRECTIONAL + } + + enum Trigger { + CONDITION, + LOG + } + + enum UpkeepFailureReason { + NONE, + UPKEEP_CANCELLED, + UPKEEP_PAUSED, + TARGET_CHECK_REVERTED, + UPKEEP_NOT_NEEDED, + PERFORM_DATA_EXCEEDS_LIMIT, + INSUFFICIENT_BALANCE, + CALLBACK_REVERTED, + REVERT_DATA_EXCEEDS_LIMIT, + REGISTRY_PAUSED + } + + enum PayoutMode { + ON_CHAIN, + OFF_CHAIN + } + + /** + * @notice OnchainConfig of the registry + * @dev used only in setConfig() + * @member checkGasLimit gas limit when checking for upkeep + * @member stalenessSeconds number of seconds that is allowed for feed data to + * be stale before switching to the fallback pricing + * @member gasCeilingMultiplier multiplier to apply to the fast gas feed price + * when calculating the payment ceiling for keepers + * @member maxPerformGas max performGas allowed for an upkeep on this registry + * @member maxCheckDataSize max length of checkData bytes + * @member maxPerformDataSize max length of performData bytes + * @member maxRevertDataSize max length of revertData bytes + * @member fallbackGasPrice gas price used if the gas price feed is stale + * @member fallbackLinkPrice LINK price used if the LINK price feed is stale + * @member transcoder address of the transcoder contract + * @member registrars addresses of the registrar contracts + * @member upkeepPrivilegeManager address which can set privilege for upkeeps + * @member reorgProtectionEnabled if this registry enables re-org protection checks + * @member chainModule the chain specific module + */ + struct OnchainConfig { + uint32 checkGasLimit; + uint32 maxPerformGas; + uint32 maxCheckDataSize; + address transcoder; + // 1 word full + bool reorgProtectionEnabled; + uint24 stalenessSeconds; + uint32 maxPerformDataSize; + uint32 maxRevertDataSize; + address upkeepPrivilegeManager; + // 2 words full + uint16 gasCeilingMultiplier; + address financeAdmin; + // 3 words + uint256 fallbackGasPrice; + uint256 fallbackLinkPrice; + uint256 fallbackNativePrice; + address[] registrars; + IChainModule chainModule; + } + + /** + * @notice relevant state of an upkeep which is used in transmit function + * @member paused if this upkeep has been paused + * @member overridesEnabled if this upkeep has overrides enabled + * @member performGas the gas limit of upkeep execution + * @member maxValidBlocknumber until which block this upkeep is valid + * @member forwarder the forwarder contract to use for this upkeep + * @member amountSpent the amount this upkeep has spent, in the upkeep's billing token + * @member balance the balance of this upkeep + * @member lastPerformedBlockNumber the last block number when this upkeep was performed + */ + struct Upkeep { + bool paused; + bool overridesEnabled; + uint32 performGas; + uint32 maxValidBlocknumber; + IAutomationForwarder forwarder; + // 2 bytes left in 1st EVM word - read in transmit path + uint128 amountSpent; + uint96 balance; + uint32 lastPerformedBlockNumber; + // 0 bytes left in 2nd EVM word - written in transmit path + IERC20 billingToken; + // 12 bytes left in 3rd EVM word - read in transmit path + } + + /// @dev Config + State storage struct which is on hot transmit path + struct HotVars { + uint96 totalPremium; // ─────────╮ total historical payment to oracles for premium + uint32 latestEpoch; // │ latest epoch for which a report was transmitted + uint24 stalenessSeconds; // │ Staleness tolerance for feeds + uint16 gasCeilingMultiplier; // │ multiplier on top of fast gas feed for upper bound + uint8 f; // │ maximum number of faulty oracles + bool paused; // │ pause switch for all upkeeps in the registry + bool reentrancyGuard; // | guard against reentrancy + bool reorgProtectionEnabled; // ─╯ if this registry should enable the re-org protection mechanism + IChainModule chainModule; // the interface of chain specific module + } + + /// @dev Config + State storage struct which is not on hot transmit path + struct Storage { + address transcoder; // Address of transcoder contract used in migrations + uint32 checkGasLimit; // Gas limit allowed in checkUpkeep + uint32 maxPerformGas; // Max gas an upkeep can use on this registry + uint32 nonce; // Nonce for each upkeep created + // 1 EVM word full + address upkeepPrivilegeManager; // address which can set privilege for upkeeps + uint32 configCount; // incremented each time a new config is posted, The count is incorporated into the config digest to prevent replay attacks. + uint32 latestConfigBlockNumber; // makes it easier for offchain systems to extract config from logs + uint32 maxCheckDataSize; // max length of checkData bytes + // 2 EVM word full + address financeAdmin; // address which can withdraw funds from the contract + uint32 maxPerformDataSize; // max length of performData bytes + uint32 maxRevertDataSize; // max length of revertData bytes + // 4 bytes left in 3rd EVM word + } + + /// @dev Report transmitted by OCR to transmit function + struct Report { + uint256 fastGasWei; + uint256 linkUSD; + uint256[] upkeepIds; + uint256[] gasLimits; + bytes[] triggers; + bytes[] performDatas; + } + + /** + * @dev This struct is used to maintain run time information about an upkeep in transmit function + * @member upkeep the upkeep struct + * @member earlyChecksPassed whether the upkeep passed early checks before perform + * @member performSuccess whether the perform was successful + * @member triggerType the type of trigger + * @member gasUsed gasUsed by this upkeep in perform + * @member calldataWeight weight assigned to this upkeep for its contribution to calldata. It is used to split L1 fee + * @member dedupID unique ID used to dedup an upkeep/trigger combo + */ + struct UpkeepTransmitInfo { + Upkeep upkeep; + bool earlyChecksPassed; + bool performSuccess; + Trigger triggerType; + uint256 gasUsed; + uint256 calldataWeight; + bytes32 dedupID; + } + + /** + * @notice holds information about a transmiter / node in the DON + * @member active can this transmitter submit reports + * @member index of oracle in s_signersList/s_transmittersList + * @member balance a node's balance in LINK + * @member lastCollected the total balance at which the node last withdrew + * @dev uint96 is safe for balance / last collected because transmitters are only ever paid in LINK + */ + struct Transmitter { + bool active; + uint8 index; + uint96 balance; + uint96 lastCollected; + } + + struct TransmitterPayeeInfo { + address transmitterAddress; + address payeeAddress; + } + + struct Signer { + bool active; + // Index of oracle in s_signersList/s_transmittersList + uint8 index; + } + + /** + * @notice the trigger structure conditional trigger type + */ + struct ConditionalTrigger { + uint32 blockNum; + bytes32 blockHash; + } + + /** + * @notice the trigger structure of log upkeeps + * @dev NOTE that blockNum / blockHash describe the block used for the callback, + * not necessarily the block number that the log was emitted in!!!! + */ + struct LogTrigger { + bytes32 logBlockHash; + bytes32 txHash; + uint32 logIndex; + uint32 blockNum; + bytes32 blockHash; + } + + /** + * @notice the billing config of a token + * @dev this is a storage struct + */ + // solhint-disable-next-line gas-struct-packing + struct BillingConfig { + uint32 gasFeePPB; + uint24 flatFeeMilliCents; // min fee is $0.00001, max fee is $167 + AggregatorV3Interface priceFeed; + uint8 decimals; + // 1st word, read in calculating BillingTokenPaymentParams + uint256 fallbackPrice; + // 2nd word only read if stale + uint96 minSpend; + // 3rd word only read during cancellation + } + + /** + * @notice override-able billing params of a billing token + */ + struct BillingOverrides { + uint32 gasFeePPB; + uint24 flatFeeMilliCents; + } + + /** + * @notice pricing params for a billing token + * @dev this is a memory-only struct, so struct packing is less important + */ + struct BillingTokenPaymentParams { + uint8 decimals; + uint32 gasFeePPB; + uint24 flatFeeMilliCents; + uint256 priceUSD; + } + + /** + * @notice struct containing price & payment information used in calculating payment amount + * @member gasLimit the amount of gas used + * @member gasOverhead the amount of gas overhead + * @member l1CostWei the amount to be charged for L1 fee in wei + * @member fastGasWei the fast gas price + * @member linkUSD the exchange ratio between LINK and USD + * @member nativeUSD the exchange ratio between the chain's native token and USD + * @member billingToken the billing token + * @member billingTokenParams the payment params specific to a particular payment token + * @member isTransaction is this an eth_call or a transaction + */ + struct PaymentParams { + uint256 gasLimit; + uint256 gasOverhead; + uint256 l1CostWei; + uint256 fastGasWei; + uint256 linkUSD; + uint256 nativeUSD; + IERC20 billingToken; + BillingTokenPaymentParams billingTokenParams; + bool isTransaction; + } + + /** + * @notice struct containing receipt information about a payment or cost estimation + * @member gasChargeInBillingToken the amount to charge a user for gas spent using the billing token's native decimals + * @member premiumInBillingToken the premium charged to the user, shared between all nodes, using the billing token's native decimals + * @member gasReimbursementInJuels the amount to reimburse a node for gas spent + * @member premiumInJuels the premium paid to NOPs, shared between all nodes + */ + // solhint-disable-next-line gas-struct-packing + struct PaymentReceipt { + uint96 gasChargeInBillingToken; + uint96 premiumInBillingToken; + // one word ends + uint96 gasReimbursementInJuels; + uint96 premiumInJuels; + // second word ends + IERC20 billingToken; + uint96 linkUSD; + // third word ends + uint96 nativeUSD; + uint96 billingUSD; + // fourth word ends + } + + event AdminPrivilegeConfigSet(address indexed admin, bytes privilegeConfig); + event BillingConfigOverridden(uint256 indexed id, BillingOverrides overrides); + event BillingConfigOverrideRemoved(uint256 indexed id); + event BillingConfigSet(IERC20 indexed token, BillingConfig config); + event CancelledUpkeepReport(uint256 indexed id, bytes trigger); + event ChainSpecificModuleUpdated(address newModule); + event DedupKeyAdded(bytes32 indexed dedupKey); + event FeesWithdrawn(address indexed assetAddress, address indexed recipient, uint256 amount); + event FundsAdded(uint256 indexed id, address indexed from, uint96 amount); + event FundsWithdrawn(uint256 indexed id, uint256 amount, address to); + event InsufficientFundsUpkeepReport(uint256 indexed id, bytes trigger); + event NOPsSettledOffchain(address[] payees, uint256[] payments); + event Paused(address account); + event PayeesUpdated(address[] transmitters, address[] payees); + event PayeeshipTransferRequested(address indexed transmitter, address indexed from, address indexed to); + event PayeeshipTransferred(address indexed transmitter, address indexed from, address indexed to); + event PaymentWithdrawn(address indexed transmitter, uint256 indexed amount, address indexed to, address payee); + event ReorgedUpkeepReport(uint256 indexed id, bytes trigger); + event StaleUpkeepReport(uint256 indexed id, bytes trigger); + event UpkeepAdminTransferred(uint256 indexed id, address indexed from, address indexed to); + event UpkeepAdminTransferRequested(uint256 indexed id, address indexed from, address indexed to); + event UpkeepCanceled(uint256 indexed id, uint64 indexed atBlockHeight); + event UpkeepCheckDataSet(uint256 indexed id, bytes newCheckData); + event UpkeepGasLimitSet(uint256 indexed id, uint96 gasLimit); + event UpkeepMigrated(uint256 indexed id, uint256 remainingBalance, address destination); + event UpkeepOffchainConfigSet(uint256 indexed id, bytes offchainConfig); + event UpkeepPaused(uint256 indexed id); + event UpkeepPerformed( + uint256 indexed id, + bool indexed success, + uint96 totalPayment, + uint256 gasUsed, + uint256 gasOverhead, + bytes trigger + ); + event UpkeepCharged(uint256 indexed id, PaymentReceipt receipt); + event UpkeepPrivilegeConfigSet(uint256 indexed id, bytes privilegeConfig); + event UpkeepReceived(uint256 indexed id, uint256 startingBalance, address importedFrom); + event UpkeepRegistered(uint256 indexed id, uint32 performGas, address admin); + event UpkeepTriggerConfigSet(uint256 indexed id, bytes triggerConfig); + event UpkeepUnpaused(uint256 indexed id); + event Unpaused(address account); + + /** + * @param link address of the LINK Token + * @param linkUSDFeed address of the LINK/USD price feed + * @param nativeUSDFeed address of the Native/USD price feed + * @param fastGasFeed address of the Fast Gas price feed + * @param automationForwarderLogic the address of automation forwarder logic + * @param allowedReadOnlyAddress the address of the allowed read only address + * @param payoutMode the payout mode + */ + constructor( + address link, + address linkUSDFeed, + address nativeUSDFeed, + address fastGasFeed, + address automationForwarderLogic, + address allowedReadOnlyAddress, + PayoutMode payoutMode, + address wrappedNativeTokenAddress + ) ConfirmedOwner(msg.sender) { + i_link = LinkTokenInterface(link); + i_linkUSDFeed = AggregatorV3Interface(linkUSDFeed); + i_nativeUSDFeed = AggregatorV3Interface(nativeUSDFeed); + i_fastGasFeed = AggregatorV3Interface(fastGasFeed); + i_automationForwarderLogic = automationForwarderLogic; + i_allowedReadOnlyAddress = allowedReadOnlyAddress; + s_payoutMode = payoutMode; + i_wrappedNativeToken = IWrappedNative(wrappedNativeTokenAddress); + if (i_linkUSDFeed.decimals() != i_nativeUSDFeed.decimals()) { + revert InvalidFeed(); + } + } + + // ================================================================ + // | INTERNAL FUNCTIONS ONLY | + // ================================================================ + + /** + * @dev creates a new upkeep with the given fields + * @param id the id of the upkeep + * @param upkeep the upkeep to create + * @param admin address to cancel upkeep and withdraw remaining funds + * @param checkData data which is passed to user's checkUpkeep + * @param triggerConfig the trigger config for this upkeep + * @param offchainConfig the off-chain config of this upkeep + */ + function _createUpkeep( + uint256 id, + Upkeep memory upkeep, + address admin, + bytes memory checkData, + bytes memory triggerConfig, + bytes memory offchainConfig + ) internal { + if (s_hotVars.paused) revert RegistryPaused(); + if (checkData.length > s_storage.maxCheckDataSize) revert CheckDataExceedsLimit(); + if (upkeep.performGas < PERFORM_GAS_MIN || upkeep.performGas > s_storage.maxPerformGas) + revert GasLimitOutsideRange(); + if (address(s_upkeep[id].forwarder) != address(0)) revert UpkeepAlreadyExists(); + if (address(s_billingConfigs[upkeep.billingToken].priceFeed) == address(0)) revert InvalidToken(); + s_upkeep[id] = upkeep; + s_upkeepAdmin[id] = admin; + s_checkData[id] = checkData; + s_reserveAmounts[upkeep.billingToken] = s_reserveAmounts[upkeep.billingToken] + upkeep.balance; + s_upkeepTriggerConfig[id] = triggerConfig; + s_upkeepOffchainConfig[id] = offchainConfig; + s_upkeepIDs.add(id); + } + + /** + * @dev creates an ID for the upkeep based on the upkeep's type + * @dev the format of the ID looks like this: + * ****00000000000X**************** + * 4 bytes of entropy + * 11 bytes of zeros + * 1 identifying byte for the trigger type + * 16 bytes of entropy + * @dev this maintains the same level of entropy as eth addresses, so IDs will still be unique + * @dev we add the "identifying" part in the middle so that it is mostly hidden from users who usually only + * see the first 4 and last 4 hex values ex 0x1234...ABCD + */ + function _createID(Trigger triggerType) internal view returns (uint256) { + bytes1 empty; + IChainModule chainModule = s_hotVars.chainModule; + bytes memory idBytes = abi.encodePacked( + keccak256(abi.encode(chainModule.blockHash((chainModule.blockNumber() - 1)), address(this), s_storage.nonce)) + ); + for (uint256 idx = 4; idx < 15; idx++) { + idBytes[idx] = empty; + } + idBytes[15] = bytes1(uint8(triggerType)); + return uint256(bytes32(idBytes)); + } + + /** + * @dev retrieves feed data for fast gas/native and link/native prices. if the feed + * data is stale it uses the configured fallback price. Once a price is picked + * for gas it takes the min of gas price in the transaction or the fast gas + * price in order to reduce costs for the upkeep clients. + */ + function _getFeedData( + HotVars memory hotVars + ) internal view returns (uint256 gasWei, uint256 linkUSD, uint256 nativeUSD) { + uint32 stalenessSeconds = hotVars.stalenessSeconds; + bool staleFallback = stalenessSeconds > 0; + uint256 timestamp; + int256 feedValue; + (, feedValue, , timestamp, ) = i_fastGasFeed.latestRoundData(); + if ( + feedValue <= 0 || block.timestamp < timestamp || (staleFallback && stalenessSeconds < block.timestamp - timestamp) + ) { + gasWei = s_fallbackGasPrice; + } else { + gasWei = uint256(feedValue); + } + (, feedValue, , timestamp, ) = i_linkUSDFeed.latestRoundData(); + if ( + feedValue <= 0 || block.timestamp < timestamp || (staleFallback && stalenessSeconds < block.timestamp - timestamp) + ) { + linkUSD = s_fallbackLinkPrice; + } else { + linkUSD = uint256(feedValue); + } + return (gasWei, linkUSD, _getNativeUSD(hotVars)); + } + + /** + * @dev this price has it's own getter for use in the transmit() hot path + * in the future, all price data should be included in the report instead of + * getting read during execution + */ + function _getNativeUSD(HotVars memory hotVars) internal view returns (uint256) { + (, int256 feedValue, , uint256 timestamp, ) = i_nativeUSDFeed.latestRoundData(); + if ( + feedValue <= 0 || + block.timestamp < timestamp || + (hotVars.stalenessSeconds > 0 && hotVars.stalenessSeconds < block.timestamp - timestamp) + ) { + return s_fallbackNativePrice; + } else { + return uint256(feedValue); + } + } + + /** + * @dev gets the price and billing params for a specific billing token + */ + function _getBillingTokenPaymentParams( + HotVars memory hotVars, + IERC20 billingToken + ) internal view returns (BillingTokenPaymentParams memory paymentParams) { + BillingConfig storage config = s_billingConfigs[billingToken]; + paymentParams.flatFeeMilliCents = config.flatFeeMilliCents; + paymentParams.gasFeePPB = config.gasFeePPB; + paymentParams.decimals = config.decimals; + (, int256 feedValue, , uint256 timestamp, ) = config.priceFeed.latestRoundData(); + if ( + feedValue <= 0 || + block.timestamp < timestamp || + (hotVars.stalenessSeconds > 0 && hotVars.stalenessSeconds < block.timestamp - timestamp) + ) { + paymentParams.priceUSD = config.fallbackPrice; + } else { + paymentParams.priceUSD = uint256(feedValue); + } + return paymentParams; + } + + /** + * @param hotVars the hot path variables + * @param paymentParams the pricing data and gas usage data + * @return receipt the receipt of payment with pricing breakdown + * @dev use of PaymentParams struct is necessary to avoid stack too deep errors + * @dev calculates LINK paid for gas spent plus a configure premium percentage + * @dev 1 USD = 1e18 attoUSD + * @dev 1 USD = 1e26 hexaicosaUSD (had to borrow this prefix from geometry because there is no metric prefix for 1e-26) + * @dev 1 millicent = 1e-5 USD = 1e13 attoUSD + */ + function _calculatePaymentAmount( + HotVars memory hotVars, + PaymentParams memory paymentParams + ) internal view returns (PaymentReceipt memory receipt) { + uint256 decimals = paymentParams.billingTokenParams.decimals; + uint256 gasWei = paymentParams.fastGasWei * hotVars.gasCeilingMultiplier; + // in case it's actual execution use actual gas price, capped by fastGasWei * gasCeilingMultiplier + if (paymentParams.isTransaction && tx.gasprice < gasWei) { + gasWei = tx.gasprice; + } + + // scaling factor is based on decimals of billing token, and applies to premium and gasCharge + uint256 numeratorScalingFactor = decimals > 18 ? 10 ** (decimals - 18) : 1; + uint256 denominatorScalingFactor = decimals < 18 ? 10 ** (18 - decimals) : 1; + + // gas calculation + uint256 gasPaymentHexaicosaUSD = (gasWei * + (paymentParams.gasLimit + paymentParams.gasOverhead) + + paymentParams.l1CostWei) * paymentParams.nativeUSD; // gasPaymentHexaicosaUSD has an extra 8 zeros because of decimals on nativeUSD feed + // gasChargeInBillingToken is scaled by the billing token's decimals. Round up to ensure a minimum billing token is charged for gas + receipt.gasChargeInBillingToken = SafeCast.toUint96( + ((gasPaymentHexaicosaUSD * numeratorScalingFactor) + + (paymentParams.billingTokenParams.priceUSD * denominatorScalingFactor - 1)) / + (paymentParams.billingTokenParams.priceUSD * denominatorScalingFactor) + ); + // 18 decimals: 26 decimals / 8 decimals + receipt.gasReimbursementInJuels = SafeCast.toUint96(gasPaymentHexaicosaUSD / paymentParams.linkUSD); + + // premium calculation + uint256 flatFeeHexaicosaUSD = uint256(paymentParams.billingTokenParams.flatFeeMilliCents) * 1e21; // 1e13 for milliCents to attoUSD and 1e8 for attoUSD to hexaicosaUSD + uint256 premiumHexaicosaUSD = ((((gasWei * paymentParams.gasLimit) + paymentParams.l1CostWei) * + paymentParams.billingTokenParams.gasFeePPB * + paymentParams.nativeUSD) / 1e9) + flatFeeHexaicosaUSD; + // premium is scaled by the billing token's decimals. Round up to ensure at least minimum charge + receipt.premiumInBillingToken = SafeCast.toUint96( + ((premiumHexaicosaUSD * numeratorScalingFactor) + + (paymentParams.billingTokenParams.priceUSD * denominatorScalingFactor - 1)) / + (paymentParams.billingTokenParams.priceUSD * denominatorScalingFactor) + ); + receipt.premiumInJuels = SafeCast.toUint96(premiumHexaicosaUSD / paymentParams.linkUSD); + + receipt.billingToken = paymentParams.billingToken; + receipt.linkUSD = SafeCast.toUint96(paymentParams.linkUSD); + receipt.nativeUSD = SafeCast.toUint96(paymentParams.nativeUSD); + receipt.billingUSD = SafeCast.toUint96(paymentParams.billingTokenParams.priceUSD); + + return receipt; + } + + /** + * @dev calculates the max payment for an upkeep. Called during checkUpkeep simulation and assumes + * maximum gas overhead, L1 fee + */ + function _getMaxPayment( + uint256 upkeepId, + HotVars memory hotVars, + Trigger triggerType, + uint32 performGas, + uint256 fastGasWei, + uint256 linkUSD, + uint256 nativeUSD, + IERC20 billingToken + ) internal view returns (uint96) { + uint256 maxL1Fee; + uint256 maxGasOverhead; + + { + if (triggerType == Trigger.CONDITION) { + maxGasOverhead = REGISTRY_CONDITIONAL_OVERHEAD; + } else if (triggerType == Trigger.LOG) { + maxGasOverhead = REGISTRY_LOG_OVERHEAD; + } else { + revert InvalidTriggerType(); + } + uint256 maxCalldataSize = s_storage.maxPerformDataSize + + TRANSMIT_CALLDATA_FIXED_BYTES_OVERHEAD + + (TRANSMIT_CALLDATA_PER_SIGNER_BYTES_OVERHEAD * (hotVars.f + 1)); + (uint256 chainModuleFixedOverhead, uint256 chainModulePerByteOverhead) = s_hotVars.chainModule.getGasOverhead(); + maxGasOverhead += + (REGISTRY_PER_SIGNER_GAS_OVERHEAD * (hotVars.f + 1)) + + ((REGISTRY_PER_PERFORM_BYTE_GAS_OVERHEAD + chainModulePerByteOverhead) * maxCalldataSize) + + chainModuleFixedOverhead; + maxL1Fee = hotVars.gasCeilingMultiplier * hotVars.chainModule.getMaxL1Fee(maxCalldataSize); + } + + BillingTokenPaymentParams memory paymentParams = _getBillingTokenPaymentParams(hotVars, billingToken); + if (s_upkeep[upkeepId].overridesEnabled) { + BillingOverrides memory billingOverrides = s_billingOverrides[upkeepId]; + // use the overridden configs + paymentParams.gasFeePPB = billingOverrides.gasFeePPB; + paymentParams.flatFeeMilliCents = billingOverrides.flatFeeMilliCents; + } + + PaymentReceipt memory receipt = _calculatePaymentAmount( + hotVars, + PaymentParams({ + gasLimit: performGas, + gasOverhead: maxGasOverhead, + l1CostWei: maxL1Fee, + fastGasWei: fastGasWei, + linkUSD: linkUSD, + nativeUSD: nativeUSD, + billingToken: billingToken, + billingTokenParams: paymentParams, + isTransaction: false + }) + ); + + return receipt.gasChargeInBillingToken + receipt.premiumInBillingToken; + } + + /** + * @dev move a transmitter's balance from total pool to withdrawable balance + */ + function _updateTransmitterBalanceFromPool( + address transmitterAddress, + uint96 totalPremium, + uint96 payeeCount + ) internal returns (uint96) { + Transmitter memory transmitter = s_transmitters[transmitterAddress]; + + if (transmitter.active) { + uint96 uncollected = totalPremium - transmitter.lastCollected; + uint96 due = uncollected / payeeCount; + transmitter.balance += due; + transmitter.lastCollected += due * payeeCount; + s_transmitters[transmitterAddress] = transmitter; + } + + return transmitter.balance; + } + + /** + * @dev gets the trigger type from an upkeepID (trigger type is encoded in the middle of the ID) + */ + function _getTriggerType(uint256 upkeepId) internal pure returns (Trigger) { + bytes32 rawID = bytes32(upkeepId); + bytes1 empty = bytes1(0); + for (uint256 idx = 4; idx < 15; idx++) { + if (rawID[idx] != empty) { + // old IDs that were created before this standard and migrated to this registry + return Trigger.CONDITION; + } + } + return Trigger(uint8(rawID[15])); + } + + function _checkPayload( + uint256 upkeepId, + Trigger triggerType, + bytes memory triggerData + ) internal view returns (bytes memory) { + if (triggerType == Trigger.CONDITION) { + return abi.encodeWithSelector(CHECK_SELECTOR, s_checkData[upkeepId]); + } else if (triggerType == Trigger.LOG) { + Log memory log = abi.decode(triggerData, (Log)); + return abi.encodeWithSelector(CHECK_LOG_SELECTOR, log, s_checkData[upkeepId]); + } + revert InvalidTriggerType(); + } + + /** + * @dev _decodeReport decodes a serialized report into a Report struct + */ + function _decodeReport(bytes calldata rawReport) internal pure returns (Report memory) { + Report memory report = abi.decode(rawReport, (Report)); + uint256 expectedLength = report.upkeepIds.length; + if ( + report.gasLimits.length != expectedLength || + report.triggers.length != expectedLength || + report.performDatas.length != expectedLength + ) { + revert InvalidReport(); + } + return report; + } + + /** + * @dev Does some early sanity checks before actually performing an upkeep + * @return bool whether the upkeep should be performed + * @return bytes32 dedupID for preventing duplicate performances of this trigger + */ + function _prePerformChecks( + uint256 upkeepId, + uint256 blocknumber, + bytes memory rawTrigger, + UpkeepTransmitInfo memory transmitInfo, + HotVars memory hotVars + ) internal returns (bool, bytes32) { + bytes32 dedupID; + if (transmitInfo.triggerType == Trigger.CONDITION) { + if (!_validateConditionalTrigger(upkeepId, blocknumber, rawTrigger, transmitInfo, hotVars)) + return (false, dedupID); + } else if (transmitInfo.triggerType == Trigger.LOG) { + bool valid; + (valid, dedupID) = _validateLogTrigger(upkeepId, blocknumber, rawTrigger, hotVars); + if (!valid) return (false, dedupID); + } else { + revert InvalidTriggerType(); + } + if (transmitInfo.upkeep.maxValidBlocknumber <= blocknumber) { + // Can happen when an upkeep got cancelled after report was generated. + // However we have a CANCELLATION_DELAY of 50 blocks so shouldn't happen in practice + emit CancelledUpkeepReport(upkeepId, rawTrigger); + return (false, dedupID); + } + return (true, dedupID); + } + + /** + * @dev Does some early sanity checks before actually performing an upkeep + */ + function _validateConditionalTrigger( + uint256 upkeepId, + uint256 blocknumber, + bytes memory rawTrigger, + UpkeepTransmitInfo memory transmitInfo, + HotVars memory hotVars + ) internal returns (bool) { + ConditionalTrigger memory trigger = abi.decode(rawTrigger, (ConditionalTrigger)); + if (trigger.blockNum < transmitInfo.upkeep.lastPerformedBlockNumber) { + // Can happen when another report performed this upkeep after this report was generated + emit StaleUpkeepReport(upkeepId, rawTrigger); + return false; + } + if ( + (hotVars.reorgProtectionEnabled && + (trigger.blockHash != bytes32("") && hotVars.chainModule.blockHash(trigger.blockNum) != trigger.blockHash)) || + trigger.blockNum >= blocknumber + ) { + // There are two cases of reorged report + // 1. trigger block number is in future: this is an edge case during extreme deep reorgs of chain + // which is always protected against + // 2. blockHash at trigger block number was same as trigger time. This is an optional check which is + // applied if DON sends non empty trigger.blockHash. Note: It only works for last 256 blocks on chain + // when it is sent + emit ReorgedUpkeepReport(upkeepId, rawTrigger); + return false; + } + return true; + } + + function _validateLogTrigger( + uint256 upkeepId, + uint256 blocknumber, + bytes memory rawTrigger, + HotVars memory hotVars + ) internal returns (bool, bytes32) { + LogTrigger memory trigger = abi.decode(rawTrigger, (LogTrigger)); + bytes32 dedupID = keccak256(abi.encodePacked(upkeepId, trigger.logBlockHash, trigger.txHash, trigger.logIndex)); + if ( + (hotVars.reorgProtectionEnabled && + (trigger.blockHash != bytes32("") && hotVars.chainModule.blockHash(trigger.blockNum) != trigger.blockHash)) || + trigger.blockNum >= blocknumber + ) { + // Reorg protection is same as conditional trigger upkeeps + emit ReorgedUpkeepReport(upkeepId, rawTrigger); + return (false, dedupID); + } + if (s_dedupKeys[dedupID]) { + emit StaleUpkeepReport(upkeepId, rawTrigger); + return (false, dedupID); + } + return (true, dedupID); + } + + /** + * @dev Verify signatures attached to report + */ + function _verifyReportSignature( + bytes32[3] calldata reportContext, + bytes calldata report, + bytes32[] calldata rs, + bytes32[] calldata ss, + bytes32 rawVs + ) internal view { + bytes32 h = keccak256(abi.encode(keccak256(report), reportContext)); + // i-th byte counts number of sigs made by i-th signer + uint256 signedCount = 0; + + Signer memory signer; + address signerAddress; + for (uint256 i = 0; i < rs.length; i++) { + signerAddress = ecrecover(h, uint8(rawVs[i]) + 27, rs[i], ss[i]); + signer = s_signers[signerAddress]; + if (!signer.active) revert OnlyActiveSigners(); + unchecked { + signedCount += 1 << (8 * signer.index); + } + } + + if (signedCount & ORACLE_MASK != signedCount) revert DuplicateSigners(); + } + + /** + * @dev updates a storage marker for this upkeep to prevent duplicate and out of order performances + * @dev for conditional triggers we set the latest block number, for log triggers we store a dedupID + */ + function _updateTriggerMarker( + uint256 upkeepID, + uint256 blocknumber, + UpkeepTransmitInfo memory upkeepTransmitInfo + ) internal { + if (upkeepTransmitInfo.triggerType == Trigger.CONDITION) { + s_upkeep[upkeepID].lastPerformedBlockNumber = uint32(blocknumber); + } else if (upkeepTransmitInfo.triggerType == Trigger.LOG) { + s_dedupKeys[upkeepTransmitInfo.dedupID] = true; + emit DedupKeyAdded(upkeepTransmitInfo.dedupID); + } + } + + /** + * @dev calls the Upkeep target with the performData param passed in by the + * transmitter and the exact gas required by the Upkeep + */ + function _performUpkeep( + IAutomationForwarder forwarder, + uint256 performGas, + bytes memory performData + ) internal nonReentrant returns (bool success, uint256 gasUsed) { + performData = abi.encodeWithSelector(PERFORM_SELECTOR, performData); + return forwarder.forward(performGas, performData); + } + + /** + * @dev handles the payment processing after an upkeep has been performed. + * Deducts an upkeep's balance and increases the amount spent. + */ + function _handlePayment( + HotVars memory hotVars, + PaymentParams memory paymentParams, + uint256 upkeepId, + Upkeep memory upkeep + ) internal returns (PaymentReceipt memory) { + if (upkeep.overridesEnabled) { + BillingOverrides memory billingOverrides = s_billingOverrides[upkeepId]; + // use the overridden configs + paymentParams.billingTokenParams.gasFeePPB = billingOverrides.gasFeePPB; + paymentParams.billingTokenParams.flatFeeMilliCents = billingOverrides.flatFeeMilliCents; + } + + PaymentReceipt memory receipt = _calculatePaymentAmount(hotVars, paymentParams); + + // balance is in the token's native decimals + uint96 balance = upkeep.balance; + // payment is in the token's native decimals + uint96 payment = receipt.gasChargeInBillingToken + receipt.premiumInBillingToken; + + // scaling factors to adjust decimals between billing token and LINK + uint256 decimals = paymentParams.billingTokenParams.decimals; + uint256 scalingFactor1 = decimals < 18 ? 10 ** (18 - decimals) : 1; + uint256 scalingFactor2 = decimals > 18 ? 10 ** (decimals - 18) : 1; + + // this shouldn't happen, but in rare edge cases, we charge the full balance in case the user + // can't cover the amount owed + if (balance < receipt.gasChargeInBillingToken) { + // if the user can't cover the gas fee, then direct all of the payment to the transmitter and distribute no premium to the DON + payment = balance; + receipt.gasReimbursementInJuels = SafeCast.toUint96( + (balance * paymentParams.billingTokenParams.priceUSD * scalingFactor1) / + (paymentParams.linkUSD * scalingFactor2) + ); + receipt.premiumInJuels = 0; + receipt.premiumInBillingToken = 0; + receipt.gasChargeInBillingToken = balance; + } else if (balance < payment) { + // if the user can cover the gas fee, but not the premium, then reduce the premium + payment = balance; + receipt.premiumInJuels = SafeCast.toUint96( + ((balance * paymentParams.billingTokenParams.priceUSD * scalingFactor1) / + (paymentParams.linkUSD * scalingFactor2)) - receipt.gasReimbursementInJuels + ); + // round up + receipt.premiumInBillingToken = SafeCast.toUint96( + ((receipt.premiumInJuels * paymentParams.linkUSD * scalingFactor2) + + (paymentParams.billingTokenParams.priceUSD * scalingFactor1 - 1)) / + (paymentParams.billingTokenParams.priceUSD * scalingFactor1) + ); + } + + s_upkeep[upkeepId].balance -= payment; + s_upkeep[upkeepId].amountSpent += payment; + s_reserveAmounts[paymentParams.billingToken] -= payment; + + emit UpkeepCharged(upkeepId, receipt); + return receipt; + } + + /** + * @dev ensures the upkeep is not cancelled and the caller is the upkeep admin + */ + function _requireAdminAndNotCancelled(uint256 upkeepId) internal view { + if (msg.sender != s_upkeepAdmin[upkeepId]) revert OnlyCallableByAdmin(); + if (s_upkeep[upkeepId].maxValidBlocknumber != UINT32_MAX) revert UpkeepCancelled(); + } + + /** + * @dev replicates Open Zeppelin's ReentrancyGuard but optimized to fit our storage + */ + modifier nonReentrant() { + if (s_hotVars.reentrancyGuard) revert ReentrantCall(); + s_hotVars.reentrancyGuard = true; + _; + s_hotVars.reentrancyGuard = false; + } + + /** + * @notice only allows a pre-configured address to initiate offchain read + */ + function _preventExecution() internal view { + // solhint-disable-next-line avoid-tx-origin + if (tx.origin != i_allowedReadOnlyAddress) { + revert OnlySimulatedBackend(); + } + } + + /** + * @notice only allows finance admin to call the function + */ + function _onlyFinanceAdminAllowed() internal view { + if (msg.sender != s_storage.financeAdmin) { + revert OnlyFinanceAdmin(); + } + } + + /** + * @notice only allows privilege manager to call the function + */ + function _onlyPrivilegeManagerAllowed() internal view { + if (msg.sender != s_storage.upkeepPrivilegeManager) { + revert OnlyCallableByUpkeepPrivilegeManager(); + } + } + + /** + * @notice sets billing configuration for a token + * @param billingTokens the addresses of tokens + * @param billingConfigs the configs for tokens + */ + function _setBillingConfig(IERC20[] memory billingTokens, BillingConfig[] memory billingConfigs) internal { + // Clear existing data + for (uint256 i = 0; i < s_billingTokens.length; i++) { + delete s_billingConfigs[s_billingTokens[i]]; + } + delete s_billingTokens; + + PayoutMode mode = s_payoutMode; + for (uint256 i = 0; i < billingTokens.length; i++) { + IERC20 token = billingTokens[i]; + BillingConfig memory config = billingConfigs[i]; + + // most ERC20 tokens are 18 decimals, priceFeed must be 8 decimals + if (config.decimals != token.decimals() || config.priceFeed.decimals() != 8) { + revert InvalidToken(); + } + + // if LINK is a billing option, payout mode must be ON_CHAIN + if (address(token) == address(i_link) && mode == PayoutMode.OFF_CHAIN) { + revert InvalidToken(); + } + if (address(token) == ZERO_ADDRESS || address(config.priceFeed) == ZERO_ADDRESS) { + revert ZeroAddressNotAllowed(); + } + + // if this is a new token, add it to tokens list. Otherwise revert + if (address(s_billingConfigs[token].priceFeed) != ZERO_ADDRESS) { + revert DuplicateEntry(); + } + s_billingTokens.push(token); + + // update the billing config for an existing token or add a new one + s_billingConfigs[token] = config; + + emit BillingConfigSet(token, config); + } + } + + /** + * @notice updates the signers and transmitters lists + */ + function _updateTransmitters(address[] memory signers, address[] memory transmitters) internal { + uint96 transmittersListLength = uint96(s_transmittersList.length); + uint96 totalPremium = s_hotVars.totalPremium; + + // move all pooled payments out of the pool to each transmitter's balance + for (uint256 i = 0; i < s_transmittersList.length; i++) { + _updateTransmitterBalanceFromPool(s_transmittersList[i], totalPremium, transmittersListLength); + } + + // remove any old signer/transmitter addresses + address transmitterAddress; + PayoutMode mode = s_payoutMode; + for (uint256 i = 0; i < s_transmittersList.length; i++) { + transmitterAddress = s_transmittersList[i]; + delete s_signers[s_signersList[i]]; + // Do not delete the whole transmitter struct as it has balance information stored + s_transmitters[transmitterAddress].active = false; + if (mode == PayoutMode.OFF_CHAIN && s_transmitters[transmitterAddress].balance > 0) { + s_deactivatedTransmitters.add(transmitterAddress); + } + } + delete s_signersList; + delete s_transmittersList; + + // add new signer/transmitter addresses + Transmitter memory transmitter; + for (uint256 i = 0; i < signers.length; i++) { + if (s_signers[signers[i]].active) revert RepeatedSigner(); + if (signers[i] == ZERO_ADDRESS) revert InvalidSigner(); + s_signers[signers[i]] = Signer({active: true, index: uint8(i)}); + + transmitterAddress = transmitters[i]; + if (transmitterAddress == ZERO_ADDRESS) revert InvalidTransmitter(); + transmitter = s_transmitters[transmitterAddress]; + if (transmitter.active) revert RepeatedTransmitter(); + transmitter.active = true; + transmitter.index = uint8(i); + // new transmitters start afresh from current totalPremium + // some spare change of premium from previous pool will be forfeited + transmitter.lastCollected = s_hotVars.totalPremium; + s_transmitters[transmitterAddress] = transmitter; + if (mode == PayoutMode.OFF_CHAIN) { + s_deactivatedTransmitters.remove(transmitterAddress); + } + } + + s_signersList = signers; + s_transmittersList = transmitters; + } + + /** + * @notice returns the size of the LINK liquidity pool + # @dev LINK max supply < 2^96, so casting to int256 is safe + */ + function _linkAvailableForPayment() internal view returns (int256) { + return int256(i_link.balanceOf(address(this))) - int256(s_reserveAmounts[IERC20(address(i_link))]); + } +} diff --git a/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryLogicA2_3.sol b/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryLogicA2_3.sol new file mode 100644 index 00000000000..64d697c70f9 --- /dev/null +++ b/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryLogicA2_3.sol @@ -0,0 +1,283 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.19; + +import {EnumerableSet} from "../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/structs/EnumerableSet.sol"; +import {Address} from "../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/Address.sol"; +import {ZKSyncAutomationRegistryBase2_3} from "./ZKSyncAutomationRegistryBase2_3.sol"; +import {ZKSyncAutomationRegistryLogicC2_3} from "./ZKSyncAutomationRegistryLogicC2_3.sol"; +import {ZKSyncAutomationRegistryLogicB2_3} from "./ZKSyncAutomationRegistryLogicB2_3.sol"; +import {Chainable} from "../Chainable.sol"; +import {ZKSyncAutomationForwarder} from "../ZKSyncAutomationForwarder.sol"; +import {IAutomationForwarder} from "../interfaces/IAutomationForwarder.sol"; +import {UpkeepTranscoderInterfaceV2} from "../interfaces/UpkeepTranscoderInterfaceV2.sol"; +import {MigratableKeeperRegistryInterfaceV2} from "../interfaces/MigratableKeeperRegistryInterfaceV2.sol"; +import {IERC20Metadata as IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {SafeERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC677Receiver} from "../../shared/interfaces/IERC677Receiver.sol"; + +/** + * @notice Logic contract, works in tandem with AutomationRegistry as a proxy + */ +contract ZKSyncAutomationRegistryLogicA2_3 is ZKSyncAutomationRegistryBase2_3, Chainable, IERC677Receiver { + using Address for address; + using EnumerableSet for EnumerableSet.UintSet; + using EnumerableSet for EnumerableSet.AddressSet; + using SafeERC20 for IERC20; + + /** + * @param logicB the address of the second logic contract + * @dev we cast the contract to logicC in order to call logicC functions (via fallback) + */ + constructor( + ZKSyncAutomationRegistryLogicB2_3 logicB + ) + ZKSyncAutomationRegistryBase2_3( + ZKSyncAutomationRegistryLogicC2_3(address(logicB)).getLinkAddress(), + ZKSyncAutomationRegistryLogicC2_3(address(logicB)).getLinkUSDFeedAddress(), + ZKSyncAutomationRegistryLogicC2_3(address(logicB)).getNativeUSDFeedAddress(), + ZKSyncAutomationRegistryLogicC2_3(address(logicB)).getFastGasFeedAddress(), + ZKSyncAutomationRegistryLogicC2_3(address(logicB)).getAutomationForwarderLogic(), + ZKSyncAutomationRegistryLogicC2_3(address(logicB)).getAllowedReadOnlyAddress(), + ZKSyncAutomationRegistryLogicC2_3(address(logicB)).getPayoutMode(), + ZKSyncAutomationRegistryLogicC2_3(address(logicB)).getWrappedNativeTokenAddress() + ) + Chainable(address(logicB)) + {} + + /** + * @notice uses LINK's transferAndCall to LINK and add funding to an upkeep + * @dev safe to cast uint256 to uint96 as total LINK supply is under UINT96MAX + * @param sender the account which transferred the funds + * @param amount number of LINK transfer + */ + function onTokenTransfer(address sender, uint256 amount, bytes calldata data) external override { + if (msg.sender != address(i_link)) revert OnlyCallableByLINKToken(); + if (data.length != 32) revert InvalidDataLength(); + uint256 id = abi.decode(data, (uint256)); + if (s_upkeep[id].maxValidBlocknumber != UINT32_MAX) revert UpkeepCancelled(); + if (address(s_upkeep[id].billingToken) != address(i_link)) revert InvalidToken(); + s_upkeep[id].balance = s_upkeep[id].balance + uint96(amount); + s_reserveAmounts[IERC20(address(i_link))] = s_reserveAmounts[IERC20(address(i_link))] + amount; + emit FundsAdded(id, sender, uint96(amount)); + } + + // ================================================================ + // | UPKEEP MANAGEMENT | + // ================================================================ + + /** + * @notice adds a new upkeep + * @param target address to perform upkeep on + * @param gasLimit amount of gas to provide the target contract when + * performing upkeep + * @param admin address to cancel upkeep and withdraw remaining funds + * @param triggerType the trigger for the upkeep + * @param billingToken the billing token for the upkeep + * @param checkData data passed to the contract when checking for upkeep + * @param triggerConfig the config for the trigger + * @param offchainConfig arbitrary offchain config for the upkeep + */ + function registerUpkeep( + address target, + uint32 gasLimit, + address admin, + Trigger triggerType, + IERC20 billingToken, + bytes calldata checkData, + bytes memory triggerConfig, + bytes memory offchainConfig + ) public returns (uint256 id) { + if (msg.sender != owner() && !s_registrars.contains(msg.sender)) revert OnlyCallableByOwnerOrRegistrar(); + if (!target.isContract()) revert NotAContract(); + id = _createID(triggerType); + IAutomationForwarder forwarder = IAutomationForwarder( + address(new ZKSyncAutomationForwarder(target, address(this), i_automationForwarderLogic)) + ); + _createUpkeep( + id, + Upkeep({ + overridesEnabled: false, + performGas: gasLimit, + balance: 0, + maxValidBlocknumber: UINT32_MAX, + lastPerformedBlockNumber: 0, + amountSpent: 0, + paused: false, + forwarder: forwarder, + billingToken: billingToken + }), + admin, + checkData, + triggerConfig, + offchainConfig + ); + s_storage.nonce++; + emit UpkeepRegistered(id, gasLimit, admin); + emit UpkeepCheckDataSet(id, checkData); + emit UpkeepTriggerConfigSet(id, triggerConfig); + emit UpkeepOffchainConfigSet(id, offchainConfig); + return (id); + } + + /** + * @notice cancels an upkeep + * @param id the upkeepID to cancel + * @dev if a user cancels an upkeep, their funds are locked for CANCELLATION_DELAY blocks to + * allow any pending performUpkeep txs time to get confirmed + */ + function cancelUpkeep(uint256 id) external { + Upkeep memory upkeep = s_upkeep[id]; + bool isOwner = msg.sender == owner(); + uint96 minSpend = s_billingConfigs[upkeep.billingToken].minSpend; + + uint256 height = s_hotVars.chainModule.blockNumber(); + if (upkeep.maxValidBlocknumber == 0) revert CannotCancel(); + if (upkeep.maxValidBlocknumber != UINT32_MAX) revert UpkeepCancelled(); + if (!isOwner && msg.sender != s_upkeepAdmin[id]) revert OnlyCallableByOwnerOrAdmin(); + + if (!isOwner) { + height = height + CANCELLATION_DELAY; + } + s_upkeep[id].maxValidBlocknumber = uint32(height); + s_upkeepIDs.remove(id); + + // charge the cancellation fee if the minSpend is not met + uint96 cancellationFee = 0; + // cancellationFee is min(max(minSpend - amountSpent, 0), amountLeft) + if (upkeep.amountSpent < minSpend) { + cancellationFee = minSpend - uint96(upkeep.amountSpent); + if (cancellationFee > upkeep.balance) { + cancellationFee = upkeep.balance; + } + } + s_upkeep[id].balance = upkeep.balance - cancellationFee; + s_reserveAmounts[upkeep.billingToken] = s_reserveAmounts[upkeep.billingToken] - cancellationFee; + + emit UpkeepCanceled(id, uint64(height)); + } + + /** + * @notice migrates upkeeps from one registry to another. + * @param ids the upkeepIDs to migrate + * @param destination the destination registry address + * @dev a transcoder must be set in order to enable migration + * @dev migration permissions must be set on *both* sending and receiving registries + * @dev only an upkeep admin can migrate their upkeeps + * @dev this function is most gas-efficient if upkeepIDs are sorted by billing token + * @dev s_billingOverrides and s_upkeepPrivilegeConfig are not migrated in this function + */ + function migrateUpkeeps(uint256[] calldata ids, address destination) external { + if ( + s_peerRegistryMigrationPermission[destination] != MigrationPermission.OUTGOING && + s_peerRegistryMigrationPermission[destination] != MigrationPermission.BIDIRECTIONAL + ) revert MigrationNotPermitted(); + if (s_storage.transcoder == ZERO_ADDRESS) revert TranscoderNotSet(); + if (ids.length == 0) revert ArrayHasNoEntries(); + + IERC20 billingToken; + uint256 balanceToTransfer; + uint256 id; + Upkeep memory upkeep; + address[] memory admins = new address[](ids.length); + Upkeep[] memory upkeeps = new Upkeep[](ids.length); + bytes[] memory checkDatas = new bytes[](ids.length); + bytes[] memory triggerConfigs = new bytes[](ids.length); + bytes[] memory offchainConfigs = new bytes[](ids.length); + + for (uint256 idx = 0; idx < ids.length; idx++) { + id = ids[idx]; + upkeep = s_upkeep[id]; + + if (idx == 0) { + billingToken = upkeep.billingToken; + balanceToTransfer = upkeep.balance; + } + + // if we encounter a new billing token, send the sum from the last billing token to the destination registry + if (upkeep.billingToken != billingToken) { + s_reserveAmounts[billingToken] = s_reserveAmounts[billingToken] - balanceToTransfer; + billingToken.safeTransfer(destination, balanceToTransfer); + billingToken = upkeep.billingToken; + balanceToTransfer = upkeep.balance; + } else if (idx != 0) { + balanceToTransfer += upkeep.balance; + } + + _requireAdminAndNotCancelled(id); + upkeep.forwarder.updateRegistry(destination); + + upkeeps[idx] = upkeep; + admins[idx] = s_upkeepAdmin[id]; + checkDatas[idx] = s_checkData[id]; + triggerConfigs[idx] = s_upkeepTriggerConfig[id]; + offchainConfigs[idx] = s_upkeepOffchainConfig[id]; + delete s_upkeep[id]; + delete s_checkData[id]; + delete s_upkeepTriggerConfig[id]; + delete s_upkeepOffchainConfig[id]; + // nullify existing proposed admin change if an upkeep is being migrated + delete s_proposedAdmin[id]; + delete s_upkeepAdmin[id]; + s_upkeepIDs.remove(id); + emit UpkeepMigrated(id, upkeep.balance, destination); + } + // always transfer the rolling sum in the end + s_reserveAmounts[billingToken] = s_reserveAmounts[billingToken] - balanceToTransfer; + billingToken.safeTransfer(destination, balanceToTransfer); + + bytes memory encodedUpkeeps = abi.encode( + ids, + upkeeps, + new address[](ids.length), + admins, + checkDatas, + triggerConfigs, + offchainConfigs + ); + MigratableKeeperRegistryInterfaceV2(destination).receiveUpkeeps( + UpkeepTranscoderInterfaceV2(s_storage.transcoder).transcodeUpkeeps( + UPKEEP_VERSION_BASE, + MigratableKeeperRegistryInterfaceV2(destination).upkeepVersion(), + encodedUpkeeps + ) + ); + } + + /** + * @notice received upkeeps migrated from another registry + * @param encodedUpkeeps the raw upkeep data to import + * @dev this function is never called directly, it is only called by another registry's migrate function + * @dev s_billingOverrides and s_upkeepPrivilegeConfig are not handled in this function + */ + function receiveUpkeeps(bytes calldata encodedUpkeeps) external { + if ( + s_peerRegistryMigrationPermission[msg.sender] != MigrationPermission.INCOMING && + s_peerRegistryMigrationPermission[msg.sender] != MigrationPermission.BIDIRECTIONAL + ) revert MigrationNotPermitted(); + ( + uint256[] memory ids, + Upkeep[] memory upkeeps, + address[] memory targets, + address[] memory upkeepAdmins, + bytes[] memory checkDatas, + bytes[] memory triggerConfigs, + bytes[] memory offchainConfigs + ) = abi.decode(encodedUpkeeps, (uint256[], Upkeep[], address[], address[], bytes[], bytes[], bytes[])); + for (uint256 idx = 0; idx < ids.length; idx++) { + if (address(upkeeps[idx].forwarder) == ZERO_ADDRESS) { + upkeeps[idx].forwarder = IAutomationForwarder( + address(new ZKSyncAutomationForwarder(targets[idx], address(this), i_automationForwarderLogic)) + ); + } + _createUpkeep( + ids[idx], + upkeeps[idx], + upkeepAdmins[idx], + checkDatas[idx], + triggerConfigs[idx], + offchainConfigs[idx] + ); + emit UpkeepReceived(ids[idx], upkeeps[idx].balance, msg.sender); + } + } +} diff --git a/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryLogicB2_3.sol b/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryLogicB2_3.sol new file mode 100644 index 00000000000..55af99fde87 --- /dev/null +++ b/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryLogicB2_3.sol @@ -0,0 +1,449 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.19; + +import {ZKSyncAutomationRegistryBase2_3} from "./ZKSyncAutomationRegistryBase2_3.sol"; +import {EnumerableSet} from "../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/structs/EnumerableSet.sol"; +import {Address} from "../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/Address.sol"; +import {ZKSyncAutomationRegistryLogicC2_3} from "./ZKSyncAutomationRegistryLogicC2_3.sol"; +import {Chainable} from "../Chainable.sol"; +import {IERC20Metadata as IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {SafeERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; +import {SafeCast} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/math/SafeCast.sol"; + +contract ZKSyncAutomationRegistryLogicB2_3 is ZKSyncAutomationRegistryBase2_3, Chainable { + using Address for address; + using EnumerableSet for EnumerableSet.UintSet; + using EnumerableSet for EnumerableSet.AddressSet; + using SafeERC20 for IERC20; + + /** + * @param logicC the address of the third logic contract + */ + constructor( + ZKSyncAutomationRegistryLogicC2_3 logicC + ) + ZKSyncAutomationRegistryBase2_3( + logicC.getLinkAddress(), + logicC.getLinkUSDFeedAddress(), + logicC.getNativeUSDFeedAddress(), + logicC.getFastGasFeedAddress(), + logicC.getAutomationForwarderLogic(), + logicC.getAllowedReadOnlyAddress(), + logicC.getPayoutMode(), + logicC.getWrappedNativeTokenAddress() + ) + Chainable(address(logicC)) + {} + + // ================================================================ + // | PIPELINE FUNCTIONS | + // ================================================================ + + /** + * @notice called by the automation DON to check if work is needed + * @param id the upkeep ID to check for work needed + * @param triggerData extra contextual data about the trigger (not used in all code paths) + * @dev this one of the core functions called in the hot path + * @dev there is a 2nd checkUpkeep function (below) that is being maintained for backwards compatibility + * @dev there is an incongruency on what gets returned during failure modes + * ex sometimes we include price data, sometimes we omit it depending on the failure + */ + function checkUpkeep( + uint256 id, + bytes memory triggerData + ) + public + returns ( + bool upkeepNeeded, + bytes memory performData, + UpkeepFailureReason upkeepFailureReason, + uint256 gasUsed, + uint256 gasLimit, + uint256 fastGasWei, + uint256 linkUSD + ) + { + _preventExecution(); + + Trigger triggerType = _getTriggerType(id); + HotVars memory hotVars = s_hotVars; + Upkeep memory upkeep = s_upkeep[id]; + + { + uint256 nativeUSD; + uint96 maxPayment; + if (hotVars.paused) return (false, bytes(""), UpkeepFailureReason.REGISTRY_PAUSED, 0, upkeep.performGas, 0, 0); + if (upkeep.maxValidBlocknumber != UINT32_MAX) + return (false, bytes(""), UpkeepFailureReason.UPKEEP_CANCELLED, 0, upkeep.performGas, 0, 0); + if (upkeep.paused) return (false, bytes(""), UpkeepFailureReason.UPKEEP_PAUSED, 0, upkeep.performGas, 0, 0); + (fastGasWei, linkUSD, nativeUSD) = _getFeedData(hotVars); + maxPayment = _getMaxPayment( + id, + hotVars, + triggerType, + upkeep.performGas, + fastGasWei, + linkUSD, + nativeUSD, + upkeep.billingToken + ); + if (upkeep.balance < maxPayment) { + return (false, bytes(""), UpkeepFailureReason.INSUFFICIENT_BALANCE, 0, upkeep.performGas, 0, 0); + } + } + + bytes memory callData = _checkPayload(id, triggerType, triggerData); + + gasUsed = gasleft(); + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory result) = upkeep.forwarder.getTarget().call{gas: s_storage.checkGasLimit}(callData); + gasUsed = gasUsed - gasleft(); + + if (!success) { + // User's target check reverted. We capture the revert data here and pass it within performData + if (result.length > s_storage.maxRevertDataSize) { + return ( + false, + bytes(""), + UpkeepFailureReason.REVERT_DATA_EXCEEDS_LIMIT, + gasUsed, + upkeep.performGas, + fastGasWei, + linkUSD + ); + } + return ( + upkeepNeeded, + result, + UpkeepFailureReason.TARGET_CHECK_REVERTED, + gasUsed, + upkeep.performGas, + fastGasWei, + linkUSD + ); + } + + (upkeepNeeded, performData) = abi.decode(result, (bool, bytes)); + if (!upkeepNeeded) + return (false, bytes(""), UpkeepFailureReason.UPKEEP_NOT_NEEDED, gasUsed, upkeep.performGas, fastGasWei, linkUSD); + + if (performData.length > s_storage.maxPerformDataSize) + return ( + false, + bytes(""), + UpkeepFailureReason.PERFORM_DATA_EXCEEDS_LIMIT, + gasUsed, + upkeep.performGas, + fastGasWei, + linkUSD + ); + + return (upkeepNeeded, performData, upkeepFailureReason, gasUsed, upkeep.performGas, fastGasWei, linkUSD); + } + + /** + * @notice see other checkUpkeep function for description + * @dev this function may be deprecated in a future version of chainlink automation + */ + function checkUpkeep( + uint256 id + ) + external + returns ( + bool upkeepNeeded, + bytes memory performData, + UpkeepFailureReason upkeepFailureReason, + uint256 gasUsed, + uint256 gasLimit, + uint256 fastGasWei, + uint256 linkUSD + ) + { + return checkUpkeep(id, bytes("")); + } + + /** + * @dev checkCallback is used specifically for automation data streams lookups (see StreamsLookupCompatibleInterface.sol) + * @param id the upkeepID to execute a callback for + * @param values the values returned from the data streams lookup + * @param extraData the user-provided extra context data + */ + function checkCallback( + uint256 id, + bytes[] memory values, + bytes calldata extraData + ) + external + returns (bool upkeepNeeded, bytes memory performData, UpkeepFailureReason upkeepFailureReason, uint256 gasUsed) + { + bytes memory payload = abi.encodeWithSelector(CHECK_CALLBACK_SELECTOR, values, extraData); + return executeCallback(id, payload); + } + + /** + * @notice this is a generic callback executor that forwards a call to a user's contract with the configured + * gas limit + * @param id the upkeepID to execute a callback for + * @param payload the data (including function selector) to call on the upkeep target contract + */ + function executeCallback( + uint256 id, + bytes memory payload + ) + public + returns (bool upkeepNeeded, bytes memory performData, UpkeepFailureReason upkeepFailureReason, uint256 gasUsed) + { + _preventExecution(); + + Upkeep memory upkeep = s_upkeep[id]; + gasUsed = gasleft(); + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory result) = upkeep.forwarder.getTarget().call{gas: s_storage.checkGasLimit}(payload); + gasUsed = gasUsed - gasleft(); + if (!success) { + return (false, bytes(""), UpkeepFailureReason.CALLBACK_REVERTED, gasUsed); + } + (upkeepNeeded, performData) = abi.decode(result, (bool, bytes)); + if (!upkeepNeeded) { + return (false, bytes(""), UpkeepFailureReason.UPKEEP_NOT_NEEDED, gasUsed); + } + if (performData.length > s_storage.maxPerformDataSize) { + return (false, bytes(""), UpkeepFailureReason.PERFORM_DATA_EXCEEDS_LIMIT, gasUsed); + } + return (upkeepNeeded, performData, upkeepFailureReason, gasUsed); + } + + /** + * @notice simulates the upkeep with the perform data returned from checkUpkeep + * @param id identifier of the upkeep to execute the data with. + * @param performData calldata parameter to be passed to the target upkeep. + * @return success whether the call reverted or not + * @return gasUsed the amount of gas the target contract consumed + */ + function simulatePerformUpkeep( + uint256 id, + bytes calldata performData + ) external returns (bool success, uint256 gasUsed) { + _preventExecution(); + + if (s_hotVars.paused) revert RegistryPaused(); + Upkeep memory upkeep = s_upkeep[id]; + (success, gasUsed) = _performUpkeep(upkeep.forwarder, upkeep.performGas, performData); + return (success, gasUsed); + } + + // ================================================================ + // | UPKEEP MANAGEMENT | + // ================================================================ + + /** + * @notice adds fund to an upkeep + * @param id the upkeepID + * @param amount the amount of funds to add, in the upkeep's billing token + */ + function addFunds(uint256 id, uint96 amount) external payable { + Upkeep memory upkeep = s_upkeep[id]; + if (upkeep.maxValidBlocknumber != UINT32_MAX) revert UpkeepCancelled(); + + if (msg.value != 0) { + if (upkeep.billingToken != IERC20(i_wrappedNativeToken)) { + revert InvalidToken(); + } + amount = SafeCast.toUint96(msg.value); + } + + s_upkeep[id].balance = upkeep.balance + amount; + s_reserveAmounts[upkeep.billingToken] = s_reserveAmounts[upkeep.billingToken] + amount; + + if (msg.value == 0) { + // ERC20 payment + upkeep.billingToken.safeTransferFrom(msg.sender, address(this), amount); + } else { + // native payment + i_wrappedNativeToken.deposit{value: amount}(); + } + + emit FundsAdded(id, msg.sender, amount); + } + + /** + * @notice overrides the billing config for an upkeep + * @param id the upkeepID + * @param billingOverrides the override-able billing config + */ + function setBillingOverrides(uint256 id, BillingOverrides calldata billingOverrides) external { + _onlyPrivilegeManagerAllowed(); + if (s_upkeep[id].maxValidBlocknumber != UINT32_MAX) revert UpkeepCancelled(); + + s_upkeep[id].overridesEnabled = true; + s_billingOverrides[id] = billingOverrides; + emit BillingConfigOverridden(id, billingOverrides); + } + + /** + * @notice remove the overridden billing config for an upkeep + * @param id the upkeepID + */ + function removeBillingOverrides(uint256 id) external { + _onlyPrivilegeManagerAllowed(); + + s_upkeep[id].overridesEnabled = false; + delete s_billingOverrides[id]; + emit BillingConfigOverrideRemoved(id); + } + + /** + * @notice transfers the address of an admin for an upkeep + */ + function transferUpkeepAdmin(uint256 id, address proposed) external { + _requireAdminAndNotCancelled(id); + if (proposed == msg.sender) revert ValueNotChanged(); + + if (s_proposedAdmin[id] != proposed) { + s_proposedAdmin[id] = proposed; + emit UpkeepAdminTransferRequested(id, msg.sender, proposed); + } + } + + /** + * @notice accepts the transfer of an upkeep admin + */ + function acceptUpkeepAdmin(uint256 id) external { + Upkeep memory upkeep = s_upkeep[id]; + if (upkeep.maxValidBlocknumber != UINT32_MAX) revert UpkeepCancelled(); + if (s_proposedAdmin[id] != msg.sender) revert OnlyCallableByProposedAdmin(); + address past = s_upkeepAdmin[id]; + s_upkeepAdmin[id] = msg.sender; + s_proposedAdmin[id] = ZERO_ADDRESS; + + emit UpkeepAdminTransferred(id, past, msg.sender); + } + + /** + * @notice pauses an upkeep - an upkeep will be neither checked nor performed while paused + */ + function pauseUpkeep(uint256 id) external { + _requireAdminAndNotCancelled(id); + Upkeep memory upkeep = s_upkeep[id]; + if (upkeep.paused) revert OnlyUnpausedUpkeep(); + s_upkeep[id].paused = true; + s_upkeepIDs.remove(id); + emit UpkeepPaused(id); + } + + /** + * @notice unpauses an upkeep + */ + function unpauseUpkeep(uint256 id) external { + _requireAdminAndNotCancelled(id); + Upkeep memory upkeep = s_upkeep[id]; + if (!upkeep.paused) revert OnlyPausedUpkeep(); + s_upkeep[id].paused = false; + s_upkeepIDs.add(id); + emit UpkeepUnpaused(id); + } + + /** + * @notice updates the checkData for an upkeep + */ + function setUpkeepCheckData(uint256 id, bytes calldata newCheckData) external { + _requireAdminAndNotCancelled(id); + if (newCheckData.length > s_storage.maxCheckDataSize) revert CheckDataExceedsLimit(); + s_checkData[id] = newCheckData; + emit UpkeepCheckDataSet(id, newCheckData); + } + + /** + * @notice updates the gas limit for an upkeep + */ + function setUpkeepGasLimit(uint256 id, uint32 gasLimit) external { + if (gasLimit < PERFORM_GAS_MIN || gasLimit > s_storage.maxPerformGas) revert GasLimitOutsideRange(); + _requireAdminAndNotCancelled(id); + s_upkeep[id].performGas = gasLimit; + + emit UpkeepGasLimitSet(id, gasLimit); + } + + /** + * @notice updates the offchain config for an upkeep + */ + function setUpkeepOffchainConfig(uint256 id, bytes calldata config) external { + _requireAdminAndNotCancelled(id); + s_upkeepOffchainConfig[id] = config; + emit UpkeepOffchainConfigSet(id, config); + } + + /** + * @notice sets the upkeep trigger config + * @param id the upkeepID to change the trigger for + * @param triggerConfig the new trigger config + */ + function setUpkeepTriggerConfig(uint256 id, bytes calldata triggerConfig) external { + _requireAdminAndNotCancelled(id); + s_upkeepTriggerConfig[id] = triggerConfig; + emit UpkeepTriggerConfigSet(id, triggerConfig); + } + + /** + * @notice withdraws an upkeep's funds from an upkeep + * @dev note that an upkeep must be cancelled first!! + */ + function withdrawFunds(uint256 id, address to) external nonReentrant { + if (to == ZERO_ADDRESS) revert InvalidRecipient(); + Upkeep memory upkeep = s_upkeep[id]; + if (s_upkeepAdmin[id] != msg.sender) revert OnlyCallableByAdmin(); + if (upkeep.maxValidBlocknumber > s_hotVars.chainModule.blockNumber()) revert UpkeepNotCanceled(); + uint96 amountToWithdraw = s_upkeep[id].balance; + s_reserveAmounts[upkeep.billingToken] = s_reserveAmounts[upkeep.billingToken] - amountToWithdraw; + s_upkeep[id].balance = 0; + upkeep.billingToken.safeTransfer(to, amountToWithdraw); + emit FundsWithdrawn(id, amountToWithdraw, to); + } + + // ================================================================ + // | FINANCE ACTIONS | + // ================================================================ + + /** + * @notice withdraws excess LINK from the liquidity pool + * @param to the address to send the fees to + * @param amount the amount to withdraw + */ + function withdrawLink(address to, uint256 amount) external { + _onlyFinanceAdminAllowed(); + if (to == ZERO_ADDRESS) revert InvalidRecipient(); + + int256 available = _linkAvailableForPayment(); + if (available < 0) { + revert InsufficientBalance(0, amount); + } else if (amount > uint256(available)) { + revert InsufficientBalance(uint256(available), amount); + } + + bool transferStatus = i_link.transfer(to, amount); + if (!transferStatus) { + revert TransferFailed(); + } + emit FeesWithdrawn(address(i_link), to, amount); + } + + /** + * @notice withdraws non-LINK fees earned by the contract + * @param asset the asset to withdraw + * @param to the address to send the fees to + * @param amount the amount to withdraw + * @dev in ON_CHAIN mode, we prevent withdrawing non-LINK fees unless there is sufficient LINK liquidity + * to cover all outstanding debts on the registry + */ + function withdrawERC20Fees(IERC20 asset, address to, uint256 amount) external { + _onlyFinanceAdminAllowed(); + if (to == ZERO_ADDRESS) revert InvalidRecipient(); + if (address(asset) == address(i_link)) revert InvalidToken(); + if (_linkAvailableForPayment() < 0 && s_payoutMode == PayoutMode.ON_CHAIN) revert InsufficientLinkLiquidity(); + uint256 available = asset.balanceOf(address(this)) - s_reserveAmounts[asset]; + if (amount > available) revert InsufficientBalance(available, amount); + + asset.safeTransfer(to, amount); + emit FeesWithdrawn(address(asset), to, amount); + } +} diff --git a/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryLogicC2_3.sol b/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryLogicC2_3.sol new file mode 100644 index 00000000000..61d0eecfbaf --- /dev/null +++ b/contracts/src/v0.8/automation/v2_3_zksync/ZKSyncAutomationRegistryLogicC2_3.sol @@ -0,0 +1,638 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.19; + +import {ZKSyncAutomationRegistryBase2_3} from "./ZKSyncAutomationRegistryBase2_3.sol"; +import {EnumerableSet} from "../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/structs/EnumerableSet.sol"; +import {Address} from "../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/Address.sol"; +import {IAutomationForwarder} from "../interfaces/IAutomationForwarder.sol"; +import {IChainModule} from "../interfaces/IChainModule.sol"; +import {IERC20Metadata as IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {IAutomationV21PlusCommon} from "../interfaces/IAutomationV21PlusCommon.sol"; + +contract ZKSyncAutomationRegistryLogicC2_3 is ZKSyncAutomationRegistryBase2_3 { + using Address for address; + using EnumerableSet for EnumerableSet.UintSet; + using EnumerableSet for EnumerableSet.AddressSet; + + /** + * @dev see AutomationRegistry master contract for constructor description + */ + constructor( + address link, + address linkUSDFeed, + address nativeUSDFeed, + address fastGasFeed, + address automationForwarderLogic, + address allowedReadOnlyAddress, + PayoutMode payoutMode, + address wrappedNativeTokenAddress + ) + ZKSyncAutomationRegistryBase2_3( + link, + linkUSDFeed, + nativeUSDFeed, + fastGasFeed, + automationForwarderLogic, + allowedReadOnlyAddress, + payoutMode, + wrappedNativeTokenAddress + ) + {} + + // ================================================================ + // | NODE ACTIONS | + // ================================================================ + + /** + * @notice transfers the address of payee for a transmitter + */ + function transferPayeeship(address transmitter, address proposed) external { + if (s_transmitterPayees[transmitter] != msg.sender) revert OnlyCallableByPayee(); + if (proposed == msg.sender) revert ValueNotChanged(); + + if (s_proposedPayee[transmitter] != proposed) { + s_proposedPayee[transmitter] = proposed; + emit PayeeshipTransferRequested(transmitter, msg.sender, proposed); + } + } + + /** + * @notice accepts the transfer of the payee + */ + function acceptPayeeship(address transmitter) external { + if (s_proposedPayee[transmitter] != msg.sender) revert OnlyCallableByProposedPayee(); + address past = s_transmitterPayees[transmitter]; + s_transmitterPayees[transmitter] = msg.sender; + s_proposedPayee[transmitter] = ZERO_ADDRESS; + + emit PayeeshipTransferred(transmitter, past, msg.sender); + } + + /** + * @notice this is for NOPs to withdraw LINK received as payment for work performed + */ + function withdrawPayment(address from, address to) external { + if (to == ZERO_ADDRESS) revert InvalidRecipient(); + if (s_payoutMode == PayoutMode.OFF_CHAIN) revert MustSettleOffchain(); + if (s_transmitterPayees[from] != msg.sender) revert OnlyCallableByPayee(); + uint96 balance = _updateTransmitterBalanceFromPool(from, s_hotVars.totalPremium, uint96(s_transmittersList.length)); + s_transmitters[from].balance = 0; + s_reserveAmounts[IERC20(address(i_link))] = s_reserveAmounts[IERC20(address(i_link))] - balance; + bool transferStatus = i_link.transfer(to, balance); + if (!transferStatus) { + revert TransferFailed(); + } + emit PaymentWithdrawn(from, balance, to, msg.sender); + } + + // ================================================================ + // | OWNER / MANAGER ACTIONS | + // ================================================================ + + /** + * @notice sets the privilege config for an upkeep + */ + function setUpkeepPrivilegeConfig(uint256 upkeepId, bytes calldata newPrivilegeConfig) external { + _onlyPrivilegeManagerAllowed(); + s_upkeepPrivilegeConfig[upkeepId] = newPrivilegeConfig; + emit UpkeepPrivilegeConfigSet(upkeepId, newPrivilegeConfig); + } + + /** + * @notice this is used by the owner to set the initial payees for newly added transmitters. The owner is not allowed to change payees for existing transmitters. + * @dev the IGNORE_ADDRESS is a "helper" that makes it easier to construct a list of payees when you only care about setting the payee for a small number of transmitters. + */ + function setPayees(address[] calldata payees) external onlyOwner { + if (s_transmittersList.length != payees.length) revert ParameterLengthError(); + for (uint256 i = 0; i < s_transmittersList.length; i++) { + address transmitter = s_transmittersList[i]; + address oldPayee = s_transmitterPayees[transmitter]; + address newPayee = payees[i]; + + if ( + (newPayee == ZERO_ADDRESS) || (oldPayee != ZERO_ADDRESS && oldPayee != newPayee && newPayee != IGNORE_ADDRESS) + ) { + revert InvalidPayee(); + } + + if (newPayee != IGNORE_ADDRESS) { + s_transmitterPayees[transmitter] = newPayee; + } + } + emit PayeesUpdated(s_transmittersList, payees); + } + + /** + * @notice sets the migration permission for a peer registry + * @dev this must be done before upkeeps can be migrated to/from another registry + */ + function setPeerRegistryMigrationPermission(address peer, MigrationPermission permission) external onlyOwner { + s_peerRegistryMigrationPermission[peer] = permission; + } + + /** + * @notice pauses the entire registry + */ + function pause() external onlyOwner { + s_hotVars.paused = true; + emit Paused(msg.sender); + } + + /** + * @notice unpauses the entire registry + */ + function unpause() external onlyOwner { + s_hotVars.paused = false; + emit Unpaused(msg.sender); + } + + /** + * @notice sets a generic bytes field used to indicate the privilege that this admin address had + * @param admin the address to set privilege for + * @param newPrivilegeConfig the privileges that this admin has + */ + function setAdminPrivilegeConfig(address admin, bytes calldata newPrivilegeConfig) external { + _onlyPrivilegeManagerAllowed(); + s_adminPrivilegeConfig[admin] = newPrivilegeConfig; + emit AdminPrivilegeConfigSet(admin, newPrivilegeConfig); + } + + /** + * @notice settles NOPs' LINK payment offchain + */ + function settleNOPsOffchain() external { + _onlyFinanceAdminAllowed(); + if (s_payoutMode == PayoutMode.ON_CHAIN) revert MustSettleOnchain(); + + uint96 totalPremium = s_hotVars.totalPremium; + uint256 activeTransmittersLength = s_transmittersList.length; + uint256 deactivatedTransmittersLength = s_deactivatedTransmitters.length(); + uint256 length = activeTransmittersLength + deactivatedTransmittersLength; + uint256[] memory payments = new uint256[](length); + address[] memory payees = new address[](length); + + for (uint256 i = 0; i < activeTransmittersLength; i++) { + address transmitterAddr = s_transmittersList[i]; + uint96 balance = _updateTransmitterBalanceFromPool( + transmitterAddr, + totalPremium, + uint96(activeTransmittersLength) + ); + + payments[i] = balance; + payees[i] = s_transmitterPayees[transmitterAddr]; + s_transmitters[transmitterAddr].balance = 0; + } + + for (uint256 i = 0; i < deactivatedTransmittersLength; i++) { + address deactivatedAddr = s_deactivatedTransmitters.at(i); + Transmitter memory transmitter = s_transmitters[deactivatedAddr]; + + payees[i + activeTransmittersLength] = s_transmitterPayees[deactivatedAddr]; + payments[i + activeTransmittersLength] = transmitter.balance; + s_transmitters[deactivatedAddr].balance = 0; + } + + // reserve amount of LINK is reset to 0 since no user deposits of LINK are expected in offchain mode + s_reserveAmounts[IERC20(address(i_link))] = 0; + + for (uint256 idx = s_deactivatedTransmitters.length(); idx > 0; idx--) { + s_deactivatedTransmitters.remove(s_deactivatedTransmitters.at(idx - 1)); + } + + emit NOPsSettledOffchain(payees, payments); + } + + /** + * @notice disables offchain payment for NOPs + */ + function disableOffchainPayments() external onlyOwner { + s_payoutMode = PayoutMode.ON_CHAIN; + } + + // ================================================================ + // | GETTERS | + // ================================================================ + + function getConditionalGasOverhead() external pure returns (uint256) { + return REGISTRY_CONDITIONAL_OVERHEAD; + } + + function getLogGasOverhead() external pure returns (uint256) { + return REGISTRY_LOG_OVERHEAD; + } + + function getPerPerformByteGasOverhead() external pure returns (uint256) { + return REGISTRY_PER_PERFORM_BYTE_GAS_OVERHEAD; + } + + function getPerSignerGasOverhead() external pure returns (uint256) { + return REGISTRY_PER_SIGNER_GAS_OVERHEAD; + } + + function getTransmitCalldataFixedBytesOverhead() external pure returns (uint256) { + return TRANSMIT_CALLDATA_FIXED_BYTES_OVERHEAD; + } + + function getTransmitCalldataPerSignerBytesOverhead() external pure returns (uint256) { + return TRANSMIT_CALLDATA_PER_SIGNER_BYTES_OVERHEAD; + } + + function getCancellationDelay() external pure returns (uint256) { + return CANCELLATION_DELAY; + } + + function getLinkAddress() external view returns (address) { + return address(i_link); + } + + function getLinkUSDFeedAddress() external view returns (address) { + return address(i_linkUSDFeed); + } + + function getNativeUSDFeedAddress() external view returns (address) { + return address(i_nativeUSDFeed); + } + + function getFastGasFeedAddress() external view returns (address) { + return address(i_fastGasFeed); + } + + function getAutomationForwarderLogic() external view returns (address) { + return i_automationForwarderLogic; + } + + function getAllowedReadOnlyAddress() external view returns (address) { + return i_allowedReadOnlyAddress; + } + + function getWrappedNativeTokenAddress() external view returns (address) { + return address(i_wrappedNativeToken); + } + + function getBillingToken(uint256 upkeepID) external view returns (IERC20) { + return s_upkeep[upkeepID].billingToken; + } + + function getBillingTokens() external view returns (IERC20[] memory) { + return s_billingTokens; + } + + function supportsBillingToken(IERC20 token) external view returns (bool) { + return address(s_billingConfigs[token].priceFeed) != address(0); + } + + function getBillingTokenConfig(IERC20 token) external view returns (BillingConfig memory) { + return s_billingConfigs[token]; + } + + function getBillingOverridesEnabled(uint256 upkeepID) external view returns (bool) { + return s_upkeep[upkeepID].overridesEnabled; + } + + function getPayoutMode() external view returns (PayoutMode) { + return s_payoutMode; + } + + function upkeepVersion() public pure returns (uint8) { + return UPKEEP_VERSION_BASE; + } + + /** + * @notice gets the number of upkeeps on the registry + */ + function getNumUpkeeps() external view returns (uint256) { + return s_upkeepIDs.length(); + } + + /** + * @notice read all of the details about an upkeep + * @dev this function may be deprecated in a future version of automation in favor of individual + * getters for each field + */ + function getUpkeep(uint256 id) external view returns (IAutomationV21PlusCommon.UpkeepInfoLegacy memory upkeepInfo) { + Upkeep memory reg = s_upkeep[id]; + address target = address(reg.forwarder) == address(0) ? address(0) : reg.forwarder.getTarget(); + upkeepInfo = IAutomationV21PlusCommon.UpkeepInfoLegacy({ + target: target, + performGas: reg.performGas, + checkData: s_checkData[id], + balance: reg.balance, + admin: s_upkeepAdmin[id], + maxValidBlocknumber: reg.maxValidBlocknumber, + lastPerformedBlockNumber: reg.lastPerformedBlockNumber, + amountSpent: uint96(reg.amountSpent), // force casting to uint96 for backwards compatibility. Not an issue if it overflows. + paused: reg.paused, + offchainConfig: s_upkeepOffchainConfig[id] + }); + return upkeepInfo; + } + + /** + * @notice retrieve active upkeep IDs. Active upkeep is defined as an upkeep which is not paused and not canceled. + * @param startIndex starting index in list + * @param maxCount max count to retrieve (0 = unlimited) + * @dev the order of IDs in the list is **not guaranteed**, therefore, if making successive calls, one + * should consider keeping the blockheight constant to ensure a holistic picture of the contract state + */ + function getActiveUpkeepIDs(uint256 startIndex, uint256 maxCount) external view returns (uint256[] memory) { + uint256 numUpkeeps = s_upkeepIDs.length(); + if (startIndex >= numUpkeeps) revert IndexOutOfRange(); + uint256 endIndex = startIndex + maxCount; + endIndex = endIndex > numUpkeeps || maxCount == 0 ? numUpkeeps : endIndex; + uint256[] memory ids = new uint256[](endIndex - startIndex); + for (uint256 idx = 0; idx < ids.length; idx++) { + ids[idx] = s_upkeepIDs.at(idx + startIndex); + } + return ids; + } + + /** + * @notice returns the upkeep's trigger type + */ + function getTriggerType(uint256 upkeepId) external pure returns (Trigger) { + return _getTriggerType(upkeepId); + } + + /** + * @notice returns the trigger config for an upkeeep + */ + function getUpkeepTriggerConfig(uint256 upkeepId) public view returns (bytes memory) { + return s_upkeepTriggerConfig[upkeepId]; + } + + /** + * @notice read the current info about any transmitter address + */ + function getTransmitterInfo( + address query + ) external view returns (bool active, uint8 index, uint96 balance, uint96 lastCollected, address payee) { + Transmitter memory transmitter = s_transmitters[query]; + + uint96 pooledShare = 0; + if (transmitter.active) { + uint96 totalDifference = s_hotVars.totalPremium - transmitter.lastCollected; + pooledShare = totalDifference / uint96(s_transmittersList.length); + } + + return ( + transmitter.active, + transmitter.index, + (transmitter.balance + pooledShare), + transmitter.lastCollected, + s_transmitterPayees[query] + ); + } + + /** + * @notice read the current info about any signer address + */ + function getSignerInfo(address query) external view returns (bool active, uint8 index) { + Signer memory signer = s_signers[query]; + return (signer.active, signer.index); + } + + /** + * @notice read the current on-chain config of the registry + * @dev this function will change between versions, it should never be used where + * backwards compatibility matters! + */ + function getConfig() external view returns (OnchainConfig memory) { + return + OnchainConfig({ + checkGasLimit: s_storage.checkGasLimit, + stalenessSeconds: s_hotVars.stalenessSeconds, + gasCeilingMultiplier: s_hotVars.gasCeilingMultiplier, + maxPerformGas: s_storage.maxPerformGas, + maxCheckDataSize: s_storage.maxCheckDataSize, + maxPerformDataSize: s_storage.maxPerformDataSize, + maxRevertDataSize: s_storage.maxRevertDataSize, + fallbackGasPrice: s_fallbackGasPrice, + fallbackLinkPrice: s_fallbackLinkPrice, + fallbackNativePrice: s_fallbackNativePrice, + transcoder: s_storage.transcoder, + registrars: s_registrars.values(), + upkeepPrivilegeManager: s_storage.upkeepPrivilegeManager, + chainModule: s_hotVars.chainModule, + reorgProtectionEnabled: s_hotVars.reorgProtectionEnabled, + financeAdmin: s_storage.financeAdmin + }); + } + + /** + * @notice read the current state of the registry + * @dev this function is deprecated + */ + function getState() + external + view + returns ( + IAutomationV21PlusCommon.StateLegacy memory state, + IAutomationV21PlusCommon.OnchainConfigLegacy memory config, + address[] memory signers, + address[] memory transmitters, + uint8 f + ) + { + state = IAutomationV21PlusCommon.StateLegacy({ + nonce: s_storage.nonce, + ownerLinkBalance: 0, // deprecated + expectedLinkBalance: 0, // deprecated + totalPremium: s_hotVars.totalPremium, + numUpkeeps: s_upkeepIDs.length(), + configCount: s_storage.configCount, + latestConfigBlockNumber: s_storage.latestConfigBlockNumber, + latestConfigDigest: s_latestConfigDigest, + latestEpoch: s_hotVars.latestEpoch, + paused: s_hotVars.paused + }); + + config = IAutomationV21PlusCommon.OnchainConfigLegacy({ + paymentPremiumPPB: 0, // deprecated + flatFeeMicroLink: 0, // deprecated + checkGasLimit: s_storage.checkGasLimit, + stalenessSeconds: s_hotVars.stalenessSeconds, + gasCeilingMultiplier: s_hotVars.gasCeilingMultiplier, + minUpkeepSpend: 0, // deprecated + maxPerformGas: s_storage.maxPerformGas, + maxCheckDataSize: s_storage.maxCheckDataSize, + maxPerformDataSize: s_storage.maxPerformDataSize, + maxRevertDataSize: s_storage.maxRevertDataSize, + fallbackGasPrice: s_fallbackGasPrice, + fallbackLinkPrice: s_fallbackLinkPrice, + transcoder: s_storage.transcoder, + registrars: s_registrars.values(), + upkeepPrivilegeManager: s_storage.upkeepPrivilegeManager + }); + + return (state, config, s_signersList, s_transmittersList, s_hotVars.f); + } + + /** + * @notice read the Storage data + * @dev this function signature will change with each version of automation + * this should not be treated as a stable function + */ + function getStorage() external view returns (Storage memory) { + return s_storage; + } + + /** + * @notice read the HotVars data + * @dev this function signature will change with each version of automation + * this should not be treated as a stable function + */ + function getHotVars() external view returns (HotVars memory) { + return s_hotVars; + } + + /** + * @notice get the chain module + */ + function getChainModule() external view returns (IChainModule chainModule) { + return s_hotVars.chainModule; + } + + /** + * @notice if this registry has reorg protection enabled + */ + function getReorgProtectionEnabled() external view returns (bool reorgProtectionEnabled) { + return s_hotVars.reorgProtectionEnabled; + } + + /** + * @notice calculates the minimum balance required for an upkeep to remain eligible + * @param id the upkeep id to calculate minimum balance for + */ + function getBalance(uint256 id) external view returns (uint96 balance) { + return s_upkeep[id].balance; + } + + /** + * @notice calculates the minimum balance required for an upkeep to remain eligible + * @param id the upkeep id to calculate minimum balance for + */ + function getMinBalance(uint256 id) external view returns (uint96) { + return getMinBalanceForUpkeep(id); + } + + /** + * @notice calculates the minimum balance required for an upkeep to remain eligible + * @param id the upkeep id to calculate minimum balance for + * @dev this will be deprecated in a future version in favor of getMinBalance + */ + function getMinBalanceForUpkeep(uint256 id) public view returns (uint96 minBalance) { + Upkeep memory upkeep = s_upkeep[id]; + return getMaxPaymentForGas(id, _getTriggerType(id), upkeep.performGas, upkeep.billingToken); + } + + /** + * @notice calculates the maximum payment for a given gas limit + * @param gasLimit the gas to calculate payment for + */ + function getMaxPaymentForGas( + uint256 id, + Trigger triggerType, + uint32 gasLimit, + IERC20 billingToken + ) public view returns (uint96 maxPayment) { + HotVars memory hotVars = s_hotVars; + (uint256 fastGasWei, uint256 linkUSD, uint256 nativeUSD) = _getFeedData(hotVars); + return _getMaxPayment(id, hotVars, triggerType, gasLimit, fastGasWei, linkUSD, nativeUSD, billingToken); + } + + /** + * @notice retrieves the migration permission for a peer registry + */ + function getPeerRegistryMigrationPermission(address peer) external view returns (MigrationPermission) { + return s_peerRegistryMigrationPermission[peer]; + } + + /** + * @notice returns the upkeep privilege config + */ + function getUpkeepPrivilegeConfig(uint256 upkeepId) external view returns (bytes memory) { + return s_upkeepPrivilegeConfig[upkeepId]; + } + + /** + * @notice returns the admin's privilege config + */ + function getAdminPrivilegeConfig(address admin) external view returns (bytes memory) { + return s_adminPrivilegeConfig[admin]; + } + + /** + * @notice returns the upkeep's forwarder contract + */ + function getForwarder(uint256 upkeepID) external view returns (IAutomationForwarder) { + return s_upkeep[upkeepID].forwarder; + } + + /** + * @notice returns if the dedupKey exists or not + */ + function hasDedupKey(bytes32 dedupKey) external view returns (bool) { + return s_dedupKeys[dedupKey]; + } + + /** + * @notice returns the fallback native price + */ + function getFallbackNativePrice() external view returns (uint256) { + return s_fallbackNativePrice; + } + + /** + * @notice returns the amount of a particular token that is reserved as + * user deposits / NOP payments + */ + function getReserveAmount(IERC20 billingToken) external view returns (uint256) { + return s_reserveAmounts[billingToken]; + } + + /** + * @notice returns the amount of a particular token that is withdraw-able by finance admin + */ + function getAvailableERC20ForPayment(IERC20 billingToken) external view returns (uint256) { + return billingToken.balanceOf(address(this)) - s_reserveAmounts[IERC20(address(billingToken))]; + } + + /** + * @notice returns the size of the LINK liquidity pool + */ + function linkAvailableForPayment() public view returns (int256) { + return _linkAvailableForPayment(); + } + + /** + * @notice returns the BillingOverrides config for a given upkeep + */ + function getBillingOverrides(uint256 upkeepID) external view returns (BillingOverrides memory) { + return s_billingOverrides[upkeepID]; + } + + /** + * @notice returns the BillingConfig for a given billing token, this includes decimals and price feed etc + */ + function getBillingConfig(IERC20 billingToken) external view returns (BillingConfig memory) { + return s_billingConfigs[billingToken]; + } + + /** + * @notice returns all active transmitters with their associated payees + */ + function getTransmittersWithPayees() external view returns (TransmitterPayeeInfo[] memory) { + uint256 transmitterCount = s_transmittersList.length; + TransmitterPayeeInfo[] memory transmitters = new TransmitterPayeeInfo[](transmitterCount); + + for (uint256 i = 0; i < transmitterCount; i++) { + address transmitterAddress = s_transmittersList[i]; + address payeeAddress = s_transmitterPayees[transmitterAddress]; + + transmitters[i] = TransmitterPayeeInfo(transmitterAddress, payeeAddress); + } + + return transmitters; + } +} diff --git a/contracts/test/v0.8/automation/AutomationRegistry2_3.test.ts b/contracts/test/v0.8/automation/AutomationRegistry2_3.test.ts index 9a572269695..f993271fbbc 100644 --- a/contracts/test/v0.8/automation/AutomationRegistry2_3.test.ts +++ b/contracts/test/v0.8/automation/AutomationRegistry2_3.test.ts @@ -25,7 +25,6 @@ import { ChainModuleBase__factory as ChainModuleBaseFactory } from '../../../typ import { ArbitrumModule__factory as ArbitrumModuleFactory } from '../../../typechain/factories/ArbitrumModule__factory' import { OptimismModule__factory as OptimismModuleFactory } from '../../../typechain/factories/OptimismModule__factory' import { ILogAutomation__factory as ILogAutomationactory } from '../../../typechain/factories/ILogAutomation__factory' -import { IAutomationForwarder__factory as IAutomationForwarderFactory } from '../../../typechain/factories/IAutomationForwarder__factory' import { MockArbSys__factory as MockArbSysFactory } from '../../../typechain/factories/MockArbSys__factory' import { AutomationCompatibleUtils } from '../../../typechain/AutomationCompatibleUtils' import { MockArbGasInfo } from '../../../typechain/MockArbGasInfo' diff --git a/contracts/test/v0.8/automation/helpers.ts b/contracts/test/v0.8/automation/helpers.ts index 5a95fb482cd..b2cdfb4efd9 100644 --- a/contracts/test/v0.8/automation/helpers.ts +++ b/contracts/test/v0.8/automation/helpers.ts @@ -170,10 +170,10 @@ export const deployRegistry23 = async ( link: Parameters[0], linkUSD: Parameters[1], nativeUSD: Parameters[2], - fastgas: Parameters[2], + fastgas: Parameters[3], allowedReadOnlyAddress: Parameters< AutomationRegistryLogicC2_3Factory['deploy'] - >[3], + >[5], payoutMode: Parameters[6], wrappedNativeTokenAddress: Parameters< AutomationRegistryLogicC2_3Factory['deploy'] From 477c8ce4b5b0a33a1645c15027bce3d23cff8c44 Mon Sep 17 00:00:00 2001 From: Jordan Krage Date: Wed, 7 Aug 2024 17:38:04 +0200 Subject: [PATCH 7/9] enable gomods (#14042) --- GNUmakefile | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index 3cba1738d5d..3b781a665d2 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -27,12 +27,8 @@ gomod: ## Ensure chainlink's go dependencies are installed. go mod download .PHONY: gomodtidy -gomodtidy: ## Run go mod tidy on all modules. - go mod tidy - cd ./core/scripts && go mod tidy - cd ./integration-tests && go mod tidy - cd ./integration-tests/load && go mod tidy - cd ./dashboard-lib && go mod tidy +gomodtidy: gomods ## Run go mod tidy on all modules. + gomods tidy .PHONY: docs docs: ## Install and run pkgsite to view Go docs @@ -89,12 +85,8 @@ abigen: ## Build & install abigen. ./tools/bin/build_abigen .PHONY: generate -generate: abigen codecgen mockery protoc ## Execute all go:generate commands. - go generate -x ./... - cd ./core/scripts && go generate -x ./... - cd ./integration-tests && go generate -x ./... - cd ./integration-tests/load && go generate -x ./... - cd ./dashboard-lib && go generate -x ./... +generate: abigen codecgen mockery protoc gomods ## Execute all go:generate commands. + gomods -w go generate -x ./... mockery .PHONY: rm-mocked @@ -136,7 +128,7 @@ presubmit: ## Format go files and imports. .PHONY: gomods gomods: ## Install gomods - go install github.com/jmank88/gomods@v0.1.1 + go install github.com/jmank88/gomods@v0.1.3 .PHONY: mockery mockery: $(mockery) ## Install mockery. From 499a67705ac7ea525685c4a064ff4aa52b08fa44 Mon Sep 17 00:00:00 2001 From: Ryan Hall Date: Wed, 7 Aug 2024 12:51:02 -0400 Subject: [PATCH 8/9] add OZ 5.0.2 contracts (#14065) --- contracts/.changeset/mean-zoos-fly.md | 5 + .../v5.0.2/contracts/access/AccessControl.sol | 209 +++ .../contracts/access/IAccessControl.sol | 98 ++ .../v5.0.2/contracts/interfaces/IERC165.sol | 6 + .../v5.0.2/contracts/interfaces/IERC20.sol | 6 + .../v5.0.2/contracts/interfaces/IERC5267.sol | 28 + .../contracts/interfaces/draft-IERC6093.sol | 161 +++ .../v5.0.2/contracts/token/ERC20/ERC20.sol | 316 +++++ .../v5.0.2/contracts/token/ERC20/IERC20.sol | 79 ++ .../token/ERC20/extensions/ERC20Burnable.sol | 39 + .../token/ERC20/extensions/IERC20Metadata.sol | 26 + .../token/ERC20/extensions/IERC20Permit.sol | 90 ++ .../contracts/token/ERC20/utils/SafeERC20.sol | 118 ++ .../v5.0.2/contracts/utils/Address.sol | 159 +++ .../v5.0.2/contracts/utils/Context.sol | 28 + .../v5.0.2/contracts/utils/Pausable.sol | 119 ++ .../v5.0.2/contracts/utils/ShortStrings.sol | 123 ++ .../v5.0.2/contracts/utils/StorageSlot.sol | 135 ++ .../v5.0.2/contracts/utils/Strings.sol | 94 ++ .../contracts/utils/cryptography/ECDSA.sol | 174 +++ .../contracts/utils/cryptography/EIP712.sol | 160 +++ .../utils/cryptography/MessageHashUtils.sol | 86 ++ .../contracts/utils/introspection/ERC165.sol | 27 + .../utils/introspection/ERC165Checker.sol | 124 ++ .../contracts/utils/introspection/IERC165.sol | 25 + .../v5.0.2/contracts/utils/math/Math.sol | 415 ++++++ .../v5.0.2/contracts/utils/math/SafeCast.sol | 1153 +++++++++++++++++ .../contracts/utils/math/SignedMath.sol | 43 + .../contracts/utils/structs/EnumerableMap.sol | 533 ++++++++ .../contracts/utils/structs/EnumerableSet.sol | 378 ++++++ 30 files changed, 4957 insertions(+) create mode 100644 contracts/.changeset/mean-zoos-fly.md create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/access/AccessControl.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/access/IAccessControl.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/IERC165.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/IERC20.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/IERC5267.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/draft-IERC6093.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/ERC20.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/IERC20.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/extensions/ERC20Burnable.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/extensions/IERC20Metadata.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/extensions/IERC20Permit.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/utils/SafeERC20.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Address.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Context.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Pausable.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/ShortStrings.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/StorageSlot.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Strings.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/cryptography/ECDSA.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/cryptography/EIP712.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/cryptography/MessageHashUtils.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/ERC165.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/ERC165Checker.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/IERC165.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/math/Math.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/math/SafeCast.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/math/SignedMath.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/structs/EnumerableMap.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/structs/EnumerableSet.sol diff --git a/contracts/.changeset/mean-zoos-fly.md b/contracts/.changeset/mean-zoos-fly.md new file mode 100644 index 00000000000..72eb98198d0 --- /dev/null +++ b/contracts/.changeset/mean-zoos-fly.md @@ -0,0 +1,5 @@ +--- +'@chainlink/contracts': patch +--- + +add OZ v0.5 contracts diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/access/AccessControl.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/access/AccessControl.sol new file mode 100644 index 00000000000..3e3341e9cfd --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/access/AccessControl.sol @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (access/AccessControl.sol) + +pragma solidity ^0.8.20; + +import {IAccessControl} from "./IAccessControl.sol"; +import {Context} from "../utils/Context.sol"; +import {ERC165} from "../utils/introspection/ERC165.sol"; + +/** + * @dev Contract module that allows children to implement role-based access + * control mechanisms. This is a lightweight version that doesn't allow enumerating role + * members except through off-chain means by accessing the contract event logs. Some + * applications may benefit from on-chain enumerability, for those cases see + * {AccessControlEnumerable}. + * + * Roles are referred to by their `bytes32` identifier. These should be exposed + * in the external API and be unique. The best way to achieve this is by + * using `public constant` hash digests: + * + * ```solidity + * bytes32 public constant MY_ROLE = keccak256("MY_ROLE"); + * ``` + * + * Roles can be used to represent a set of permissions. To restrict access to a + * function call, use {hasRole}: + * + * ```solidity + * function foo() public { + * require(hasRole(MY_ROLE, msg.sender)); + * ... + * } + * ``` + * + * Roles can be granted and revoked dynamically via the {grantRole} and + * {revokeRole} functions. Each role has an associated admin role, and only + * accounts that have a role's admin role can call {grantRole} and {revokeRole}. + * + * By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means + * that only accounts with this role will be able to grant or revoke other + * roles. More complex role relationships can be created by using + * {_setRoleAdmin}. + * + * WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to + * grant and revoke this role. Extra precautions should be taken to secure + * accounts that have been granted it. We recommend using {AccessControlDefaultAdminRules} + * to enforce additional security measures for this role. + */ +abstract contract AccessControl is Context, IAccessControl, ERC165 { + struct RoleData { + mapping(address account => bool) hasRole; + bytes32 adminRole; + } + + mapping(bytes32 role => RoleData) private _roles; + + bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00; + + /** + * @dev Modifier that checks that an account has a specific role. Reverts + * with an {AccessControlUnauthorizedAccount} error including the required role. + */ + modifier onlyRole(bytes32 role) { + _checkRole(role); + _; + } + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return interfaceId == type(IAccessControl).interfaceId || super.supportsInterface(interfaceId); + } + + /** + * @dev Returns `true` if `account` has been granted `role`. + */ + function hasRole(bytes32 role, address account) public view virtual returns (bool) { + return _roles[role].hasRole[account]; + } + + /** + * @dev Reverts with an {AccessControlUnauthorizedAccount} error if `_msgSender()` + * is missing `role`. Overriding this function changes the behavior of the {onlyRole} modifier. + */ + function _checkRole(bytes32 role) internal view virtual { + _checkRole(role, _msgSender()); + } + + /** + * @dev Reverts with an {AccessControlUnauthorizedAccount} error if `account` + * is missing `role`. + */ + function _checkRole(bytes32 role, address account) internal view virtual { + if (!hasRole(role, account)) { + revert AccessControlUnauthorizedAccount(account, role); + } + } + + /** + * @dev Returns the admin role that controls `role`. See {grantRole} and + * {revokeRole}. + * + * To change a role's admin, use {_setRoleAdmin}. + */ + function getRoleAdmin(bytes32 role) public view virtual returns (bytes32) { + return _roles[role].adminRole; + } + + /** + * @dev Grants `role` to `account`. + * + * If `account` had not been already granted `role`, emits a {RoleGranted} + * event. + * + * Requirements: + * + * - the caller must have ``role``'s admin role. + * + * May emit a {RoleGranted} event. + */ + function grantRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) { + _grantRole(role, account); + } + + /** + * @dev Revokes `role` from `account`. + * + * If `account` had been granted `role`, emits a {RoleRevoked} event. + * + * Requirements: + * + * - the caller must have ``role``'s admin role. + * + * May emit a {RoleRevoked} event. + */ + function revokeRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) { + _revokeRole(role, account); + } + + /** + * @dev Revokes `role` from the calling account. + * + * Roles are often managed via {grantRole} and {revokeRole}: this function's + * purpose is to provide a mechanism for accounts to lose their privileges + * if they are compromised (such as when a trusted device is misplaced). + * + * If the calling account had been revoked `role`, emits a {RoleRevoked} + * event. + * + * Requirements: + * + * - the caller must be `callerConfirmation`. + * + * May emit a {RoleRevoked} event. + */ + function renounceRole(bytes32 role, address callerConfirmation) public virtual { + if (callerConfirmation != _msgSender()) { + revert AccessControlBadConfirmation(); + } + + _revokeRole(role, callerConfirmation); + } + + /** + * @dev Sets `adminRole` as ``role``'s admin role. + * + * Emits a {RoleAdminChanged} event. + */ + function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual { + bytes32 previousAdminRole = getRoleAdmin(role); + _roles[role].adminRole = adminRole; + emit RoleAdminChanged(role, previousAdminRole, adminRole); + } + + /** + * @dev Attempts to grant `role` to `account` and returns a boolean indicating if `role` was granted. + * + * Internal function without access restriction. + * + * May emit a {RoleGranted} event. + */ + function _grantRole(bytes32 role, address account) internal virtual returns (bool) { + if (!hasRole(role, account)) { + _roles[role].hasRole[account] = true; + emit RoleGranted(role, account, _msgSender()); + return true; + } else { + return false; + } + } + + /** + * @dev Attempts to revoke `role` to `account` and returns a boolean indicating if `role` was revoked. + * + * Internal function without access restriction. + * + * May emit a {RoleRevoked} event. + */ + function _revokeRole(bytes32 role, address account) internal virtual returns (bool) { + if (hasRole(role, account)) { + _roles[role].hasRole[account] = false; + emit RoleRevoked(role, account, _msgSender()); + return true; + } else { + return false; + } + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/access/IAccessControl.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/access/IAccessControl.sol new file mode 100644 index 00000000000..2ac89ca7356 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/access/IAccessControl.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (access/IAccessControl.sol) + +pragma solidity ^0.8.20; + +/** + * @dev External interface of AccessControl declared to support ERC165 detection. + */ +interface IAccessControl { + /** + * @dev The `account` is missing a role. + */ + error AccessControlUnauthorizedAccount(address account, bytes32 neededRole); + + /** + * @dev The caller of a function is not the expected one. + * + * NOTE: Don't confuse with {AccessControlUnauthorizedAccount}. + */ + error AccessControlBadConfirmation(); + + /** + * @dev Emitted when `newAdminRole` is set as ``role``'s admin role, replacing `previousAdminRole` + * + * `DEFAULT_ADMIN_ROLE` is the starting admin for all roles, despite + * {RoleAdminChanged} not being emitted signaling this. + */ + event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole); + + /** + * @dev Emitted when `account` is granted `role`. + * + * `sender` is the account that originated the contract call, an admin role + * bearer except when using {AccessControl-_setupRole}. + */ + event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); + + /** + * @dev Emitted when `account` is revoked `role`. + * + * `sender` is the account that originated the contract call: + * - if using `revokeRole`, it is the admin role bearer + * - if using `renounceRole`, it is the role bearer (i.e. `account`) + */ + event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender); + + /** + * @dev Returns `true` if `account` has been granted `role`. + */ + function hasRole(bytes32 role, address account) external view returns (bool); + + /** + * @dev Returns the admin role that controls `role`. See {grantRole} and + * {revokeRole}. + * + * To change a role's admin, use {AccessControl-_setRoleAdmin}. + */ + function getRoleAdmin(bytes32 role) external view returns (bytes32); + + /** + * @dev Grants `role` to `account`. + * + * If `account` had not been already granted `role`, emits a {RoleGranted} + * event. + * + * Requirements: + * + * - the caller must have ``role``'s admin role. + */ + function grantRole(bytes32 role, address account) external; + + /** + * @dev Revokes `role` from `account`. + * + * If `account` had been granted `role`, emits a {RoleRevoked} event. + * + * Requirements: + * + * - the caller must have ``role``'s admin role. + */ + function revokeRole(bytes32 role, address account) external; + + /** + * @dev Revokes `role` from the calling account. + * + * Roles are often managed via {grantRole} and {revokeRole}: this function's + * purpose is to provide a mechanism for accounts to lose their privileges + * if they are compromised (such as when a trusted device is misplaced). + * + * If the calling account had been granted `role`, emits a {RoleRevoked} + * event. + * + * Requirements: + * + * - the caller must be `callerConfirmation`. + */ + function renounceRole(bytes32 role, address callerConfirmation) external; +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/IERC165.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/IERC165.sol new file mode 100644 index 00000000000..944dd0d5912 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/IERC165.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC165.sol) + +pragma solidity ^0.8.20; + +import {IERC165} from "../utils/introspection/IERC165.sol"; diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/IERC20.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/IERC20.sol new file mode 100644 index 00000000000..21d5a413275 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/IERC20.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC20.sol) + +pragma solidity ^0.8.20; + +import {IERC20} from "../token/ERC20/IERC20.sol"; diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/IERC5267.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/IERC5267.sol new file mode 100644 index 00000000000..47a9fd58855 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/IERC5267.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC5267.sol) + +pragma solidity ^0.8.20; + +interface IERC5267 { + /** + * @dev MAY be emitted to signal that the domain could have changed. + */ + event EIP712DomainChanged(); + + /** + * @dev returns the fields and values that describe the domain separator used by this contract for EIP-712 + * signature. + */ + function eip712Domain() + external + view + returns ( + bytes1 fields, + string memory name, + string memory version, + uint256 chainId, + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ); +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/draft-IERC6093.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/draft-IERC6093.sol new file mode 100644 index 00000000000..f6990e607c9 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/interfaces/draft-IERC6093.sol @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/draft-IERC6093.sol) +pragma solidity ^0.8.20; + +/** + * @dev Standard ERC20 Errors + * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC20 tokens. + */ +interface IERC20Errors { + /** + * @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + * @param balance Current balance for the interacting account. + * @param needed Minimum amount required to perform a transfer. + */ + error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed); + + /** + * @dev Indicates a failure with the token `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + */ + error ERC20InvalidSender(address sender); + + /** + * @dev Indicates a failure with the token `receiver`. Used in transfers. + * @param receiver Address to which tokens are being transferred. + */ + error ERC20InvalidReceiver(address receiver); + + /** + * @dev Indicates a failure with the `spender`’s `allowance`. Used in transfers. + * @param spender Address that may be allowed to operate on tokens without being their owner. + * @param allowance Amount of tokens a `spender` is allowed to operate with. + * @param needed Minimum amount required to perform a transfer. + */ + error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed); + + /** + * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals. + * @param approver Address initiating an approval operation. + */ + error ERC20InvalidApprover(address approver); + + /** + * @dev Indicates a failure with the `spender` to be approved. Used in approvals. + * @param spender Address that may be allowed to operate on tokens without being their owner. + */ + error ERC20InvalidSpender(address spender); +} + +/** + * @dev Standard ERC721 Errors + * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC721 tokens. + */ +interface IERC721Errors { + /** + * @dev Indicates that an address can't be an owner. For example, `address(0)` is a forbidden owner in EIP-20. + * Used in balance queries. + * @param owner Address of the current owner of a token. + */ + error ERC721InvalidOwner(address owner); + + /** + * @dev Indicates a `tokenId` whose `owner` is the zero address. + * @param tokenId Identifier number of a token. + */ + error ERC721NonexistentToken(uint256 tokenId); + + /** + * @dev Indicates an error related to the ownership over a particular token. Used in transfers. + * @param sender Address whose tokens are being transferred. + * @param tokenId Identifier number of a token. + * @param owner Address of the current owner of a token. + */ + error ERC721IncorrectOwner(address sender, uint256 tokenId, address owner); + + /** + * @dev Indicates a failure with the token `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + */ + error ERC721InvalidSender(address sender); + + /** + * @dev Indicates a failure with the token `receiver`. Used in transfers. + * @param receiver Address to which tokens are being transferred. + */ + error ERC721InvalidReceiver(address receiver); + + /** + * @dev Indicates a failure with the `operator`’s approval. Used in transfers. + * @param operator Address that may be allowed to operate on tokens without being their owner. + * @param tokenId Identifier number of a token. + */ + error ERC721InsufficientApproval(address operator, uint256 tokenId); + + /** + * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals. + * @param approver Address initiating an approval operation. + */ + error ERC721InvalidApprover(address approver); + + /** + * @dev Indicates a failure with the `operator` to be approved. Used in approvals. + * @param operator Address that may be allowed to operate on tokens without being their owner. + */ + error ERC721InvalidOperator(address operator); +} + +/** + * @dev Standard ERC1155 Errors + * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC1155 tokens. + */ +interface IERC1155Errors { + /** + * @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + * @param balance Current balance for the interacting account. + * @param needed Minimum amount required to perform a transfer. + * @param tokenId Identifier number of a token. + */ + error ERC1155InsufficientBalance(address sender, uint256 balance, uint256 needed, uint256 tokenId); + + /** + * @dev Indicates a failure with the token `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + */ + error ERC1155InvalidSender(address sender); + + /** + * @dev Indicates a failure with the token `receiver`. Used in transfers. + * @param receiver Address to which tokens are being transferred. + */ + error ERC1155InvalidReceiver(address receiver); + + /** + * @dev Indicates a failure with the `operator`’s approval. Used in transfers. + * @param operator Address that may be allowed to operate on tokens without being their owner. + * @param owner Address of the current owner of a token. + */ + error ERC1155MissingApprovalForAll(address operator, address owner); + + /** + * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals. + * @param approver Address initiating an approval operation. + */ + error ERC1155InvalidApprover(address approver); + + /** + * @dev Indicates a failure with the `operator` to be approved. Used in approvals. + * @param operator Address that may be allowed to operate on tokens without being their owner. + */ + error ERC1155InvalidOperator(address operator); + + /** + * @dev Indicates an array length mismatch between ids and values in a safeBatchTransferFrom operation. + * Used in batch transfers. + * @param idsLength Length of the array of token identifiers + * @param valuesLength Length of the array of token amounts + */ + error ERC1155InvalidArrayLength(uint256 idsLength, uint256 valuesLength); +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/ERC20.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/ERC20.sol new file mode 100644 index 00000000000..1fde5279d00 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/ERC20.sol @@ -0,0 +1,316 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/ERC20.sol) + +pragma solidity ^0.8.20; + +import {IERC20} from "./IERC20.sol"; +import {IERC20Metadata} from "./extensions/IERC20Metadata.sol"; +import {Context} from "../../utils/Context.sol"; +import {IERC20Errors} from "../../interfaces/draft-IERC6093.sol"; + +/** + * @dev Implementation of the {IERC20} interface. + * + * This implementation is agnostic to the way tokens are created. This means + * that a supply mechanism has to be added in a derived contract using {_mint}. + * + * TIP: For a detailed writeup see our guide + * https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How + * to implement supply mechanisms]. + * + * The default value of {decimals} is 18. To change this, you should override + * this function so it returns a different value. + * + * We have followed general OpenZeppelin Contracts guidelines: functions revert + * instead returning `false` on failure. This behavior is nonetheless + * conventional and does not conflict with the expectations of ERC20 + * applications. + * + * Additionally, an {Approval} event is emitted on calls to {transferFrom}. + * This allows applications to reconstruct the allowance for all accounts just + * by listening to said events. Other implementations of the EIP may not emit + * these events, as it isn't required by the specification. + */ +abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors { + mapping(address account => uint256) private _balances; + + mapping(address account => mapping(address spender => uint256)) private _allowances; + + uint256 private _totalSupply; + + string private _name; + string private _symbol; + + /** + * @dev Sets the values for {name} and {symbol}. + * + * All two of these values are immutable: they can only be set once during + * construction. + */ + constructor(string memory name_, string memory symbol_) { + _name = name_; + _symbol = symbol_; + } + + /** + * @dev Returns the name of the token. + */ + function name() public view virtual returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view virtual returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5.05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. This is the default value returned by this function, unless + * it's overridden. + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() public view virtual returns (uint8) { + return 18; + } + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public view virtual returns (uint256) { + return _totalSupply; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf(address account) public view virtual returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - the caller must have a balance of at least `value`. + */ + function transfer(address to, uint256 value) public virtual returns (bool) { + address owner = _msgSender(); + _transfer(owner, to, value); + return true; + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance(address owner, address spender) public view virtual returns (uint256) { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * NOTE: If `value` is the maximum `uint256`, the allowance is not updated on + * `transferFrom`. This is semantically equivalent to an infinite approval. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 value) public virtual returns (bool) { + address owner = _msgSender(); + _approve(owner, spender, value); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}. + * + * NOTE: Does not update the allowance if the current allowance + * is the maximum `uint256`. + * + * Requirements: + * + * - `from` and `to` cannot be the zero address. + * - `from` must have a balance of at least `value`. + * - the caller must have allowance for ``from``'s tokens of at least + * `value`. + */ + function transferFrom(address from, address to, uint256 value) public virtual returns (bool) { + address spender = _msgSender(); + _spendAllowance(from, spender, value); + _transfer(from, to, value); + return true; + } + + /** + * @dev Moves a `value` amount of tokens from `from` to `to`. + * + * This internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * NOTE: This function is not virtual, {_update} should be overridden instead. + */ + function _transfer(address from, address to, uint256 value) internal { + if (from == address(0)) { + revert ERC20InvalidSender(address(0)); + } + if (to == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + _update(from, to, value); + } + + /** + * @dev Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from` + * (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding + * this function. + * + * Emits a {Transfer} event. + */ + function _update(address from, address to, uint256 value) internal virtual { + if (from == address(0)) { + // Overflow check required: The rest of the code assumes that totalSupply never overflows + _totalSupply += value; + } else { + uint256 fromBalance = _balances[from]; + if (fromBalance < value) { + revert ERC20InsufficientBalance(from, fromBalance, value); + } + unchecked { + // Overflow not possible: value <= fromBalance <= totalSupply. + _balances[from] = fromBalance - value; + } + } + + if (to == address(0)) { + unchecked { + // Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply. + _totalSupply -= value; + } + } else { + unchecked { + // Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256. + _balances[to] += value; + } + } + + emit Transfer(from, to, value); + } + + /** + * @dev Creates a `value` amount of tokens and assigns them to `account`, by transferring it from address(0). + * Relies on the `_update` mechanism + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * NOTE: This function is not virtual, {_update} should be overridden instead. + */ + function _mint(address account, uint256 value) internal { + if (account == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + _update(address(0), account, value); + } + + /** + * @dev Destroys a `value` amount of tokens from `account`, lowering the total supply. + * Relies on the `_update` mechanism. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * NOTE: This function is not virtual, {_update} should be overridden instead + */ + function _burn(address account, uint256 value) internal { + if (account == address(0)) { + revert ERC20InvalidSender(address(0)); + } + _update(account, address(0), value); + } + + /** + * @dev Sets `value` as the allowance of `spender` over the `owner` s tokens. + * + * This internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + * + * Overrides to this logic should be done to the variant with an additional `bool emitEvent` argument. + */ + function _approve(address owner, address spender, uint256 value) internal { + _approve(owner, spender, value, true); + } + + /** + * @dev Variant of {_approve} with an optional flag to enable or disable the {Approval} event. + * + * By default (when calling {_approve}) the flag is set to true. On the other hand, approval changes made by + * `_spendAllowance` during the `transferFrom` operation set the flag to false. This saves gas by not emitting any + * `Approval` event during `transferFrom` operations. + * + * Anyone who wishes to continue emitting `Approval` events on the`transferFrom` operation can force the flag to + * true using the following override: + * ``` + * function _approve(address owner, address spender, uint256 value, bool) internal virtual override { + * super._approve(owner, spender, value, true); + * } + * ``` + * + * Requirements are the same as {_approve}. + */ + function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual { + if (owner == address(0)) { + revert ERC20InvalidApprover(address(0)); + } + if (spender == address(0)) { + revert ERC20InvalidSpender(address(0)); + } + _allowances[owner][spender] = value; + if (emitEvent) { + emit Approval(owner, spender, value); + } + } + + /** + * @dev Updates `owner` s allowance for `spender` based on spent `value`. + * + * Does not update the allowance value in case of infinite allowance. + * Revert if not enough allowance is available. + * + * Does not emit an {Approval} event. + */ + function _spendAllowance(address owner, address spender, uint256 value) internal virtual { + uint256 currentAllowance = allowance(owner, spender); + if (currentAllowance != type(uint256).max) { + if (currentAllowance < value) { + revert ERC20InsufficientAllowance(spender, currentAllowance, value); + } + unchecked { + _approve(owner, spender, currentAllowance - value, false); + } + } + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/IERC20.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/IERC20.sol new file mode 100644 index 00000000000..db01cf4c751 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/IERC20.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/IERC20.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20 { + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); + + /** + * @dev Returns the value of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the value of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves a `value` amount of tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 value) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets a `value` amount of tokens as the allowance of `spender` over the + * caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 value) external returns (bool); + + /** + * @dev Moves a `value` amount of tokens from `from` to `to` using the + * allowance mechanism. `value` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom(address from, address to, uint256 value) external returns (bool); +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/extensions/ERC20Burnable.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/extensions/ERC20Burnable.sol new file mode 100644 index 00000000000..4d482d8ec83 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/extensions/ERC20Burnable.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/ERC20Burnable.sol) + +pragma solidity ^0.8.20; + +import {ERC20} from "../ERC20.sol"; +import {Context} from "../../../utils/Context.sol"; + +/** + * @dev Extension of {ERC20} that allows token holders to destroy both their own + * tokens and those that they have an allowance for, in a way that can be + * recognized off-chain (via event analysis). + */ +abstract contract ERC20Burnable is Context, ERC20 { + /** + * @dev Destroys a `value` amount of tokens from the caller. + * + * See {ERC20-_burn}. + */ + function burn(uint256 value) public virtual { + _burn(_msgSender(), value); + } + + /** + * @dev Destroys a `value` amount of tokens from `account`, deducting from + * the caller's allowance. + * + * See {ERC20-_burn} and {ERC20-allowance}. + * + * Requirements: + * + * - the caller must have allowance for ``accounts``'s tokens of at least + * `value`. + */ + function burnFrom(address account, uint256 value) public virtual { + _spendAllowance(account, _msgSender(), value); + _burn(account, value); + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/extensions/IERC20Metadata.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/extensions/IERC20Metadata.sol new file mode 100644 index 00000000000..1a38cba3e06 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/extensions/IERC20Metadata.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/IERC20Metadata.sol) + +pragma solidity ^0.8.20; + +import {IERC20} from "../IERC20.sol"; + +/** + * @dev Interface for the optional metadata functions from the ERC20 standard. + */ +interface IERC20Metadata is IERC20 { + /** + * @dev Returns the name of the token. + */ + function name() external view returns (string memory); + + /** + * @dev Returns the symbol of the token. + */ + function symbol() external view returns (string memory); + + /** + * @dev Returns the decimals places of the token. + */ + function decimals() external view returns (uint8); +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/extensions/IERC20Permit.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/extensions/IERC20Permit.sol new file mode 100644 index 00000000000..5af48101ab8 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/extensions/IERC20Permit.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/IERC20Permit.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Interface of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC20 allowance (see {IERC20-allowance}) by + * presenting a message signed by the account. By not relying on {IERC20-approve}, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + * + * ==== Security Considerations + * + * There are two important considerations concerning the use of `permit`. The first is that a valid permit signature + * expresses an allowance, and it should not be assumed to convey additional meaning. In particular, it should not be + * considered as an intention to spend the allowance in any specific way. The second is that because permits have + * built-in replay protection and can be submitted by anyone, they can be frontrun. A protocol that uses permits should + * take this into consideration and allow a `permit` call to fail. Combining these two aspects, a pattern that may be + * generally recommended is: + * + * ```solidity + * function doThingWithPermit(..., uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public { + * try token.permit(msg.sender, address(this), value, deadline, v, r, s) {} catch {} + * doThing(..., value); + * } + * + * function doThing(..., uint256 value) public { + * token.safeTransferFrom(msg.sender, address(this), value); + * ... + * } + * ``` + * + * Observe that: 1) `msg.sender` is used as the owner, leaving no ambiguity as to the signer intent, and 2) the use of + * `try/catch` allows the permit to fail and makes the code tolerant to frontrunning. (See also + * {SafeERC20-safeTransferFrom}). + * + * Additionally, note that smart contract wallets (such as Argent or Safe) are not able to produce permit signatures, so + * contracts should have entry points that don't rely on permit. + */ +interface IERC20Permit { + /** + * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, + * given ``owner``'s signed approval. + * + * IMPORTANT: The same issues {IERC20-approve} has related to transaction + * ordering also apply here. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `deadline` must be a timestamp in the future. + * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` + * over the EIP712-formatted function arguments. + * - the signature must use ``owner``'s current nonce (see {nonces}). + * + * For more information on the signature format, see the + * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP + * section]. + * + * CAUTION: See Security Considerations above. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + /** + * @dev Returns the current nonce for `owner`. This value must be + * included whenever a signature is generated for {permit}. + * + * Every successful call to {permit} increases ``owner``'s nonce by one. This + * prevents a signature from being used multiple times. + */ + function nonces(address owner) external view returns (uint256); + + /** + * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32); +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/utils/SafeERC20.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/utils/SafeERC20.sol new file mode 100644 index 00000000000..bb65709b46b --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/utils/SafeERC20.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/utils/SafeERC20.sol) + +pragma solidity ^0.8.20; + +import {IERC20} from "../IERC20.sol"; +import {IERC20Permit} from "../extensions/IERC20Permit.sol"; +import {Address} from "../../../utils/Address.sol"; + +/** + * @title SafeERC20 + * @dev Wrappers around ERC20 operations that throw on failure (when the token + * contract returns false). Tokens that return no value (and instead revert or + * throw on failure) are also supported, non-reverting calls are assumed to be + * successful. + * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract, + * which allows you to call the safe operations as `token.safeTransfer(...)`, etc. + */ +library SafeERC20 { + using Address for address; + + /** + * @dev An operation with an ERC20 token failed. + */ + error SafeERC20FailedOperation(address token); + + /** + * @dev Indicates a failed `decreaseAllowance` request. + */ + error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease); + + /** + * @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value, + * non-reverting calls are assumed to be successful. + */ + function safeTransfer(IERC20 token, address to, uint256 value) internal { + _callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value))); + } + + /** + * @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the + * calling contract. If `token` returns no value, non-reverting calls are assumed to be successful. + */ + function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { + _callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value))); + } + + /** + * @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value, + * non-reverting calls are assumed to be successful. + */ + function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal { + uint256 oldAllowance = token.allowance(address(this), spender); + forceApprove(token, spender, oldAllowance + value); + } + + /** + * @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no + * value, non-reverting calls are assumed to be successful. + */ + function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal { + unchecked { + uint256 currentAllowance = token.allowance(address(this), spender); + if (currentAllowance < requestedDecrease) { + revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease); + } + forceApprove(token, spender, currentAllowance - requestedDecrease); + } + } + + /** + * @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value, + * non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval + * to be set to zero before setting it to a non-zero value, such as USDT. + */ + function forceApprove(IERC20 token, address spender, uint256 value) internal { + bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value)); + + if (!_callOptionalReturnBool(token, approvalCall)) { + _callOptionalReturn(token, abi.encodeCall(token.approve, (spender, 0))); + _callOptionalReturn(token, approvalCall); + } + } + + /** + * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement + * on the return value: the return value is optional (but if data is returned, it must not be false). + * @param token The token targeted by the call. + * @param data The call data (encoded using abi.encode or one of its variants). + */ + function _callOptionalReturn(IERC20 token, bytes memory data) private { + // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since + // we're implementing it ourselves. We use {Address-functionCall} to perform this call, which verifies that + // the target address contains contract code and also asserts for success in the low-level call. + + bytes memory returndata = address(token).functionCall(data); + if (returndata.length != 0 && !abi.decode(returndata, (bool))) { + revert SafeERC20FailedOperation(address(token)); + } + } + + /** + * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement + * on the return value: the return value is optional (but if data is returned, it must not be false). + * @param token The token targeted by the call. + * @param data The call data (encoded using abi.encode or one of its variants). + * + * This is a variant of {_callOptionalReturn} that silents catches all reverts and returns a bool instead. + */ + function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) { + // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since + // we're implementing it ourselves. We cannot use {Address-functionCall} here since this should return false + // and not revert is the subcall reverts. + + (bool success, bytes memory returndata) = address(token).call(data); + return success && (returndata.length == 0 || abi.decode(returndata, (bool))) && address(token).code.length > 0; + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Address.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Address.sol new file mode 100644 index 00000000000..b7e3059529a --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Address.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/Address.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Collection of functions related to the address type + */ +library Address { + /** + * @dev The ETH balance of the account is not enough to perform the operation. + */ + error AddressInsufficientBalance(address account); + + /** + * @dev There's no code at `target` (it is not a contract). + */ + error AddressEmptyCode(address target); + + /** + * @dev A call to an address target failed. The target may have reverted. + */ + error FailedInnerCall(); + + /** + * @dev Replacement for Solidity's `transfer`: sends `amount` wei to + * `recipient`, forwarding all available gas and reverting on errors. + * + * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost + * of certain opcodes, possibly making contracts go over the 2300 gas limit + * imposed by `transfer`, making them unable to receive funds via + * `transfer`. {sendValue} removes this limitation. + * + * https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/[Learn more]. + * + * IMPORTANT: because control is transferred to `recipient`, care must be + * taken to not create reentrancy vulnerabilities. Consider using + * {ReentrancyGuard} or the + * https://solidity.readthedocs.io/en/v0.8.20/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. + */ + function sendValue(address payable recipient, uint256 amount) internal { + if (address(this).balance < amount) { + revert AddressInsufficientBalance(address(this)); + } + + (bool success, ) = recipient.call{value: amount}(""); + if (!success) { + revert FailedInnerCall(); + } + } + + /** + * @dev Performs a Solidity function call using a low level `call`. A + * plain `call` is an unsafe replacement for a function call: use this + * function instead. + * + * If `target` reverts with a revert reason or custom error, it is bubbled + * up by this function (like regular Solidity function calls). However, if + * the call reverted with no returned reason, this function reverts with a + * {FailedInnerCall} error. + * + * Returns the raw returned data. To convert to the expected return value, + * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. + * + * Requirements: + * + * - `target` must be a contract. + * - calling `target` with `data` must not revert. + */ + function functionCall(address target, bytes memory data) internal returns (bytes memory) { + return functionCallWithValue(target, data, 0); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but also transferring `value` wei to `target`. + * + * Requirements: + * + * - the calling contract must have an ETH balance of at least `value`. + * - the called Solidity function must be `payable`. + */ + function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) { + if (address(this).balance < value) { + revert AddressInsufficientBalance(address(this)); + } + (bool success, bytes memory returndata) = target.call{value: value}(data); + return verifyCallResultFromTarget(target, success, returndata); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a static call. + */ + function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) { + (bool success, bytes memory returndata) = target.staticcall(data); + return verifyCallResultFromTarget(target, success, returndata); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a delegate call. + */ + function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { + (bool success, bytes memory returndata) = target.delegatecall(data); + return verifyCallResultFromTarget(target, success, returndata); + } + + /** + * @dev Tool to verify that a low level call to smart-contract was successful, and reverts if the target + * was not a contract or bubbling up the revert reason (falling back to {FailedInnerCall}) in case of an + * unsuccessful call. + */ + function verifyCallResultFromTarget( + address target, + bool success, + bytes memory returndata + ) internal view returns (bytes memory) { + if (!success) { + _revert(returndata); + } else { + // only check if target is a contract if the call was successful and the return data is empty + // otherwise we already know that it was a contract + if (returndata.length == 0 && target.code.length == 0) { + revert AddressEmptyCode(target); + } + return returndata; + } + } + + /** + * @dev Tool to verify that a low level call was successful, and reverts if it wasn't, either by bubbling the + * revert reason or with a default {FailedInnerCall} error. + */ + function verifyCallResult(bool success, bytes memory returndata) internal pure returns (bytes memory) { + if (!success) { + _revert(returndata); + } else { + return returndata; + } + } + + /** + * @dev Reverts with returndata if present. Otherwise reverts with {FailedInnerCall}. + */ + function _revert(bytes memory returndata) private pure { + // Look for revert reason and bubble it up if present + if (returndata.length > 0) { + // The easiest way to bubble the revert reason is using memory via assembly + /// @solidity memory-safe-assembly + assembly { + let returndata_size := mload(returndata) + revert(add(32, returndata), returndata_size) + } + } else { + revert FailedInnerCall(); + } + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Context.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Context.sol new file mode 100644 index 00000000000..4e535fe03c2 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Context.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +abstract contract Context { + function _msgSender() internal view virtual returns (address) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + return msg.data; + } + + function _contextSuffixLength() internal view virtual returns (uint256) { + return 0; + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Pausable.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Pausable.sol new file mode 100644 index 00000000000..312f1cb90fe --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Pausable.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/Pausable.sol) + +pragma solidity ^0.8.20; + +import {Context} from "../utils/Context.sol"; + +/** + * @dev Contract module which allows children to implement an emergency stop + * mechanism that can be triggered by an authorized account. + * + * This module is used through inheritance. It will make available the + * modifiers `whenNotPaused` and `whenPaused`, which can be applied to + * the functions of your contract. Note that they will not be pausable by + * simply including this module, only once the modifiers are put in place. + */ +abstract contract Pausable is Context { + bool private _paused; + + /** + * @dev Emitted when the pause is triggered by `account`. + */ + event Paused(address account); + + /** + * @dev Emitted when the pause is lifted by `account`. + */ + event Unpaused(address account); + + /** + * @dev The operation failed because the contract is paused. + */ + error EnforcedPause(); + + /** + * @dev The operation failed because the contract is not paused. + */ + error ExpectedPause(); + + /** + * @dev Initializes the contract in unpaused state. + */ + constructor() { + _paused = false; + } + + /** + * @dev Modifier to make a function callable only when the contract is not paused. + * + * Requirements: + * + * - The contract must not be paused. + */ + modifier whenNotPaused() { + _requireNotPaused(); + _; + } + + /** + * @dev Modifier to make a function callable only when the contract is paused. + * + * Requirements: + * + * - The contract must be paused. + */ + modifier whenPaused() { + _requirePaused(); + _; + } + + /** + * @dev Returns true if the contract is paused, and false otherwise. + */ + function paused() public view virtual returns (bool) { + return _paused; + } + + /** + * @dev Throws if the contract is paused. + */ + function _requireNotPaused() internal view virtual { + if (paused()) { + revert EnforcedPause(); + } + } + + /** + * @dev Throws if the contract is not paused. + */ + function _requirePaused() internal view virtual { + if (!paused()) { + revert ExpectedPause(); + } + } + + /** + * @dev Triggers stopped state. + * + * Requirements: + * + * - The contract must not be paused. + */ + function _pause() internal virtual whenNotPaused { + _paused = true; + emit Paused(_msgSender()); + } + + /** + * @dev Returns to normal state. + * + * Requirements: + * + * - The contract must be paused. + */ + function _unpause() internal virtual whenPaused { + _paused = false; + emit Unpaused(_msgSender()); + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/ShortStrings.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/ShortStrings.sol new file mode 100644 index 00000000000..fdfe774d635 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/ShortStrings.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/ShortStrings.sol) + +pragma solidity ^0.8.20; + +import {StorageSlot} from "./StorageSlot.sol"; + +// | string | 0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA | +// | length | 0x BB | +type ShortString is bytes32; + +/** + * @dev This library provides functions to convert short memory strings + * into a `ShortString` type that can be used as an immutable variable. + * + * Strings of arbitrary length can be optimized using this library if + * they are short enough (up to 31 bytes) by packing them with their + * length (1 byte) in a single EVM word (32 bytes). Additionally, a + * fallback mechanism can be used for every other case. + * + * Usage example: + * + * ```solidity + * contract Named { + * using ShortStrings for *; + * + * ShortString private immutable _name; + * string private _nameFallback; + * + * constructor(string memory contractName) { + * _name = contractName.toShortStringWithFallback(_nameFallback); + * } + * + * function name() external view returns (string memory) { + * return _name.toStringWithFallback(_nameFallback); + * } + * } + * ``` + */ +library ShortStrings { + // Used as an identifier for strings longer than 31 bytes. + bytes32 private constant FALLBACK_SENTINEL = 0x00000000000000000000000000000000000000000000000000000000000000FF; + + error StringTooLong(string str); + error InvalidShortString(); + + /** + * @dev Encode a string of at most 31 chars into a `ShortString`. + * + * This will trigger a `StringTooLong` error is the input string is too long. + */ + function toShortString(string memory str) internal pure returns (ShortString) { + bytes memory bstr = bytes(str); + if (bstr.length > 31) { + revert StringTooLong(str); + } + return ShortString.wrap(bytes32(uint256(bytes32(bstr)) | bstr.length)); + } + + /** + * @dev Decode a `ShortString` back to a "normal" string. + */ + function toString(ShortString sstr) internal pure returns (string memory) { + uint256 len = byteLength(sstr); + // using `new string(len)` would work locally but is not memory safe. + string memory str = new string(32); + /// @solidity memory-safe-assembly + assembly { + mstore(str, len) + mstore(add(str, 0x20), sstr) + } + return str; + } + + /** + * @dev Return the length of a `ShortString`. + */ + function byteLength(ShortString sstr) internal pure returns (uint256) { + uint256 result = uint256(ShortString.unwrap(sstr)) & 0xFF; + if (result > 31) { + revert InvalidShortString(); + } + return result; + } + + /** + * @dev Encode a string into a `ShortString`, or write it to storage if it is too long. + */ + function toShortStringWithFallback(string memory value, string storage store) internal returns (ShortString) { + if (bytes(value).length < 32) { + return toShortString(value); + } else { + StorageSlot.getStringSlot(store).value = value; + return ShortString.wrap(FALLBACK_SENTINEL); + } + } + + /** + * @dev Decode a string that was encoded to `ShortString` or written to storage using {setWithFallback}. + */ + function toStringWithFallback(ShortString value, string storage store) internal pure returns (string memory) { + if (ShortString.unwrap(value) != FALLBACK_SENTINEL) { + return toString(value); + } else { + return store; + } + } + + /** + * @dev Return the length of a string that was encoded to `ShortString` or written to storage using + * {setWithFallback}. + * + * WARNING: This will return the "byte length" of the string. This may not reflect the actual length in terms of + * actual characters as the UTF-8 encoding of a single character can span over multiple bytes. + */ + function byteLengthWithFallback(ShortString value, string storage store) internal view returns (uint256) { + if (ShortString.unwrap(value) != FALLBACK_SENTINEL) { + return byteLength(value); + } else { + return bytes(store).length; + } + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/StorageSlot.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/StorageSlot.sol new file mode 100644 index 00000000000..08418327a59 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/StorageSlot.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/StorageSlot.sol) +// This file was procedurally generated from scripts/generate/templates/StorageSlot.js. + +pragma solidity ^0.8.20; + +/** + * @dev Library for reading and writing primitive types to specific storage slots. + * + * Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts. + * This library helps with reading and writing to such slots without the need for inline assembly. + * + * The functions in this library return Slot structs that contain a `value` member that can be used to read or write. + * + * Example usage to set ERC1967 implementation slot: + * ```solidity + * contract ERC1967 { + * bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + * + * function _getImplementation() internal view returns (address) { + * return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; + * } + * + * function _setImplementation(address newImplementation) internal { + * require(newImplementation.code.length > 0); + * StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; + * } + * } + * ``` + */ +library StorageSlot { + struct AddressSlot { + address value; + } + + struct BooleanSlot { + bool value; + } + + struct Bytes32Slot { + bytes32 value; + } + + struct Uint256Slot { + uint256 value; + } + + struct StringSlot { + string value; + } + + struct BytesSlot { + bytes value; + } + + /** + * @dev Returns an `AddressSlot` with member `value` located at `slot`. + */ + function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `BooleanSlot` with member `value` located at `slot`. + */ + function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `Bytes32Slot` with member `value` located at `slot`. + */ + function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `Uint256Slot` with member `value` located at `slot`. + */ + function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `StringSlot` with member `value` located at `slot`. + */ + function getStringSlot(bytes32 slot) internal pure returns (StringSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `StringSlot` representation of the string storage pointer `store`. + */ + function getStringSlot(string storage store) internal pure returns (StringSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := store.slot + } + } + + /** + * @dev Returns an `BytesSlot` with member `value` located at `slot`. + */ + function getBytesSlot(bytes32 slot) internal pure returns (BytesSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `BytesSlot` representation of the bytes storage pointer `store`. + */ + function getBytesSlot(bytes storage store) internal pure returns (BytesSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := store.slot + } + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Strings.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Strings.sol new file mode 100644 index 00000000000..b2c0a40fb2a --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Strings.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/Strings.sol) + +pragma solidity ^0.8.20; + +import {Math} from "./math/Math.sol"; +import {SignedMath} from "./math/SignedMath.sol"; + +/** + * @dev String operations. + */ +library Strings { + bytes16 private constant HEX_DIGITS = "0123456789abcdef"; + uint8 private constant ADDRESS_LENGTH = 20; + + /** + * @dev The `value` string doesn't fit in the specified `length`. + */ + error StringsInsufficientHexLength(uint256 value, uint256 length); + + /** + * @dev Converts a `uint256` to its ASCII `string` decimal representation. + */ + function toString(uint256 value) internal pure returns (string memory) { + unchecked { + uint256 length = Math.log10(value) + 1; + string memory buffer = new string(length); + uint256 ptr; + /// @solidity memory-safe-assembly + assembly { + ptr := add(buffer, add(32, length)) + } + while (true) { + ptr--; + /// @solidity memory-safe-assembly + assembly { + mstore8(ptr, byte(mod(value, 10), HEX_DIGITS)) + } + value /= 10; + if (value == 0) break; + } + return buffer; + } + } + + /** + * @dev Converts a `int256` to its ASCII `string` decimal representation. + */ + function toStringSigned(int256 value) internal pure returns (string memory) { + return string.concat(value < 0 ? "-" : "", toString(SignedMath.abs(value))); + } + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation. + */ + function toHexString(uint256 value) internal pure returns (string memory) { + unchecked { + return toHexString(value, Math.log256(value) + 1); + } + } + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length. + */ + function toHexString(uint256 value, uint256 length) internal pure returns (string memory) { + uint256 localValue = value; + bytes memory buffer = new bytes(2 * length + 2); + buffer[0] = "0"; + buffer[1] = "x"; + for (uint256 i = 2 * length + 1; i > 1; --i) { + buffer[i] = HEX_DIGITS[localValue & 0xf]; + localValue >>= 4; + } + if (localValue != 0) { + revert StringsInsufficientHexLength(value, length); + } + return string(buffer); + } + + /** + * @dev Converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal + * representation. + */ + function toHexString(address addr) internal pure returns (string memory) { + return toHexString(uint256(uint160(addr)), ADDRESS_LENGTH); + } + + /** + * @dev Returns true if the two strings are equal. + */ + function equal(string memory a, string memory b) internal pure returns (bool) { + return bytes(a).length == bytes(b).length && keccak256(bytes(a)) == keccak256(bytes(b)); + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/cryptography/ECDSA.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/cryptography/ECDSA.sol new file mode 100644 index 00000000000..04b3e5e0646 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/cryptography/ECDSA.sol @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/ECDSA.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations. + * + * These functions can be used to verify that a message was signed by the holder + * of the private keys of a given address. + */ +library ECDSA { + enum RecoverError { + NoError, + InvalidSignature, + InvalidSignatureLength, + InvalidSignatureS + } + + /** + * @dev The signature derives the `address(0)`. + */ + error ECDSAInvalidSignature(); + + /** + * @dev The signature has an invalid length. + */ + error ECDSAInvalidSignatureLength(uint256 length); + + /** + * @dev The signature has an S value that is in the upper half order. + */ + error ECDSAInvalidSignatureS(bytes32 s); + + /** + * @dev Returns the address that signed a hashed message (`hash`) with `signature` or an error. This will not + * return address(0) without also returning an error description. Errors are documented using an enum (error type) + * and a bytes32 providing additional information about the error. + * + * If no error is returned, then the address can be used for verification purposes. + * + * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures: + * this function rejects them by requiring the `s` value to be in the lower + * half order, and the `v` value to be either 27 or 28. + * + * IMPORTANT: `hash` _must_ be the result of a hash operation for the + * verification to be secure: it is possible to craft signatures that + * recover to arbitrary addresses for non-hashed data. A safe way to ensure + * this is by receiving a hash of the original message (which may otherwise + * be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it. + * + * Documentation for signature generation: + * - with https://web3js.readthedocs.io/en/v1.3.4/web3-eth-accounts.html#sign[Web3.js] + * - with https://docs.ethers.io/v5/api/signer/#Signer-signMessage[ethers] + */ + function tryRecover(bytes32 hash, bytes memory signature) internal pure returns (address, RecoverError, bytes32) { + if (signature.length == 65) { + bytes32 r; + bytes32 s; + uint8 v; + // ecrecover takes the signature parameters, and the only way to get them + // currently is to use assembly. + /// @solidity memory-safe-assembly + assembly { + r := mload(add(signature, 0x20)) + s := mload(add(signature, 0x40)) + v := byte(0, mload(add(signature, 0x60))) + } + return tryRecover(hash, v, r, s); + } else { + return (address(0), RecoverError.InvalidSignatureLength, bytes32(signature.length)); + } + } + + /** + * @dev Returns the address that signed a hashed message (`hash`) with + * `signature`. This address can then be used for verification purposes. + * + * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures: + * this function rejects them by requiring the `s` value to be in the lower + * half order, and the `v` value to be either 27 or 28. + * + * IMPORTANT: `hash` _must_ be the result of a hash operation for the + * verification to be secure: it is possible to craft signatures that + * recover to arbitrary addresses for non-hashed data. A safe way to ensure + * this is by receiving a hash of the original message (which may otherwise + * be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it. + */ + function recover(bytes32 hash, bytes memory signature) internal pure returns (address) { + (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, signature); + _throwError(error, errorArg); + return recovered; + } + + /** + * @dev Overload of {ECDSA-tryRecover} that receives the `r` and `vs` short-signature fields separately. + * + * See https://eips.ethereum.org/EIPS/eip-2098[EIP-2098 short signatures] + */ + function tryRecover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address, RecoverError, bytes32) { + unchecked { + bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); + // We do not check for an overflow here since the shift operation results in 0 or 1. + uint8 v = uint8((uint256(vs) >> 255) + 27); + return tryRecover(hash, v, r, s); + } + } + + /** + * @dev Overload of {ECDSA-recover} that receives the `r and `vs` short-signature fields separately. + */ + function recover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address) { + (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, r, vs); + _throwError(error, errorArg); + return recovered; + } + + /** + * @dev Overload of {ECDSA-tryRecover} that receives the `v`, + * `r` and `s` signature fields separately. + */ + function tryRecover( + bytes32 hash, + uint8 v, + bytes32 r, + bytes32 s + ) internal pure returns (address, RecoverError, bytes32) { + // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature + // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines + // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most + // signatures from current libraries generate a unique signature with an s-value in the lower half order. + // + // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value + // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or + // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept + // these malleable signatures as well. + if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { + return (address(0), RecoverError.InvalidSignatureS, s); + } + + // If the signature is valid (and not malleable), return the signer address + address signer = ecrecover(hash, v, r, s); + if (signer == address(0)) { + return (address(0), RecoverError.InvalidSignature, bytes32(0)); + } + + return (signer, RecoverError.NoError, bytes32(0)); + } + + /** + * @dev Overload of {ECDSA-recover} that receives the `v`, + * `r` and `s` signature fields separately. + */ + function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) { + (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, v, r, s); + _throwError(error, errorArg); + return recovered; + } + + /** + * @dev Optionally reverts with the corresponding custom error according to the `error` argument provided. + */ + function _throwError(RecoverError error, bytes32 errorArg) private pure { + if (error == RecoverError.NoError) { + return; // no error: do nothing + } else if (error == RecoverError.InvalidSignature) { + revert ECDSAInvalidSignature(); + } else if (error == RecoverError.InvalidSignatureLength) { + revert ECDSAInvalidSignatureLength(uint256(errorArg)); + } else if (error == RecoverError.InvalidSignatureS) { + revert ECDSAInvalidSignatureS(errorArg); + } + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/cryptography/EIP712.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/cryptography/EIP712.sol new file mode 100644 index 00000000000..8e548cdd8f0 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/cryptography/EIP712.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/EIP712.sol) + +pragma solidity ^0.8.20; + +import {MessageHashUtils} from "./MessageHashUtils.sol"; +import {ShortStrings, ShortString} from "../ShortStrings.sol"; +import {IERC5267} from "../../interfaces/IERC5267.sol"; + +/** + * @dev https://eips.ethereum.org/EIPS/eip-712[EIP 712] is a standard for hashing and signing of typed structured data. + * + * The encoding scheme specified in the EIP requires a domain separator and a hash of the typed structured data, whose + * encoding is very generic and therefore its implementation in Solidity is not feasible, thus this contract + * does not implement the encoding itself. Protocols need to implement the type-specific encoding they need in order to + * produce the hash of their typed data using a combination of `abi.encode` and `keccak256`. + * + * This contract implements the EIP 712 domain separator ({_domainSeparatorV4}) that is used as part of the encoding + * scheme, and the final step of the encoding to obtain the message digest that is then signed via ECDSA + * ({_hashTypedDataV4}). + * + * The implementation of the domain separator was designed to be as efficient as possible while still properly updating + * the chain id to protect against replay attacks on an eventual fork of the chain. + * + * NOTE: This contract implements the version of the encoding known as "v4", as implemented by the JSON RPC method + * https://docs.metamask.io/guide/signing-data.html[`eth_signTypedDataV4` in MetaMask]. + * + * NOTE: In the upgradeable version of this contract, the cached values will correspond to the address, and the domain + * separator of the implementation contract. This will cause the {_domainSeparatorV4} function to always rebuild the + * separator from the immutable values, which is cheaper than accessing a cached version in cold storage. + * + * @custom:oz-upgrades-unsafe-allow state-variable-immutable + */ +abstract contract EIP712 is IERC5267 { + using ShortStrings for *; + + bytes32 private constant TYPE_HASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + + // Cache the domain separator as an immutable value, but also store the chain id that it corresponds to, in order to + // invalidate the cached domain separator if the chain id changes. + bytes32 private immutable _cachedDomainSeparator; + uint256 private immutable _cachedChainId; + address private immutable _cachedThis; + + bytes32 private immutable _hashedName; + bytes32 private immutable _hashedVersion; + + ShortString private immutable _name; + ShortString private immutable _version; + string private _nameFallback; + string private _versionFallback; + + /** + * @dev Initializes the domain separator and parameter caches. + * + * The meaning of `name` and `version` is specified in + * https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator[EIP 712]: + * + * - `name`: the user readable name of the signing domain, i.e. the name of the DApp or the protocol. + * - `version`: the current major version of the signing domain. + * + * NOTE: These parameters cannot be changed except through a xref:learn::upgrading-smart-contracts.adoc[smart + * contract upgrade]. + */ + constructor(string memory name, string memory version) { + _name = name.toShortStringWithFallback(_nameFallback); + _version = version.toShortStringWithFallback(_versionFallback); + _hashedName = keccak256(bytes(name)); + _hashedVersion = keccak256(bytes(version)); + + _cachedChainId = block.chainid; + _cachedDomainSeparator = _buildDomainSeparator(); + _cachedThis = address(this); + } + + /** + * @dev Returns the domain separator for the current chain. + */ + function _domainSeparatorV4() internal view returns (bytes32) { + if (address(this) == _cachedThis && block.chainid == _cachedChainId) { + return _cachedDomainSeparator; + } else { + return _buildDomainSeparator(); + } + } + + function _buildDomainSeparator() private view returns (bytes32) { + return keccak256(abi.encode(TYPE_HASH, _hashedName, _hashedVersion, block.chainid, address(this))); + } + + /** + * @dev Given an already https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct[hashed struct], this + * function returns the hash of the fully encoded EIP712 message for this domain. + * + * This hash can be used together with {ECDSA-recover} to obtain the signer of a message. For example: + * + * ```solidity + * bytes32 digest = _hashTypedDataV4(keccak256(abi.encode( + * keccak256("Mail(address to,string contents)"), + * mailTo, + * keccak256(bytes(mailContents)) + * ))); + * address signer = ECDSA.recover(digest, signature); + * ``` + */ + function _hashTypedDataV4(bytes32 structHash) internal view virtual returns (bytes32) { + return MessageHashUtils.toTypedDataHash(_domainSeparatorV4(), structHash); + } + + /** + * @dev See {IERC-5267}. + */ + function eip712Domain() + public + view + virtual + returns ( + bytes1 fields, + string memory name, + string memory version, + uint256 chainId, + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ) + { + return ( + hex"0f", // 01111 + _EIP712Name(), + _EIP712Version(), + block.chainid, + address(this), + bytes32(0), + new uint256[](0) + ); + } + + /** + * @dev The name parameter for the EIP712 domain. + * + * NOTE: By default this function reads _name which is an immutable value. + * It only reads from storage if necessary (in case the value is too large to fit in a ShortString). + */ + // solhint-disable-next-line func-name-mixedcase + function _EIP712Name() internal view returns (string memory) { + return _name.toStringWithFallback(_nameFallback); + } + + /** + * @dev The version parameter for the EIP712 domain. + * + * NOTE: By default this function reads _version which is an immutable value. + * It only reads from storage if necessary (in case the value is too large to fit in a ShortString). + */ + // solhint-disable-next-line func-name-mixedcase + function _EIP712Version() internal view returns (string memory) { + return _version.toStringWithFallback(_versionFallback); + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/cryptography/MessageHashUtils.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/cryptography/MessageHashUtils.sol new file mode 100644 index 00000000000..8836693e79b --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/cryptography/MessageHashUtils.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/MessageHashUtils.sol) + +pragma solidity ^0.8.20; + +import {Strings} from "../Strings.sol"; + +/** + * @dev Signature message hash utilities for producing digests to be consumed by {ECDSA} recovery or signing. + * + * The library provides methods for generating a hash of a message that conforms to the + * https://eips.ethereum.org/EIPS/eip-191[EIP 191] and https://eips.ethereum.org/EIPS/eip-712[EIP 712] + * specifications. + */ +library MessageHashUtils { + /** + * @dev Returns the keccak256 digest of an EIP-191 signed data with version + * `0x45` (`personal_sign` messages). + * + * The digest is calculated by prefixing a bytes32 `messageHash` with + * `"\x19Ethereum Signed Message:\n32"` and hashing the result. It corresponds with the + * hash signed when using the https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] JSON-RPC method. + * + * NOTE: The `messageHash` parameter is intended to be the result of hashing a raw message with + * keccak256, although any bytes32 value can be safely used because the final digest will + * be re-hashed. + * + * See {ECDSA-recover}. + */ + function toEthSignedMessageHash(bytes32 messageHash) internal pure returns (bytes32 digest) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, "\x19Ethereum Signed Message:\n32") // 32 is the bytes-length of messageHash + mstore(0x1c, messageHash) // 0x1c (28) is the length of the prefix + digest := keccak256(0x00, 0x3c) // 0x3c is the length of the prefix (0x1c) + messageHash (0x20) + } + } + + /** + * @dev Returns the keccak256 digest of an EIP-191 signed data with version + * `0x45` (`personal_sign` messages). + * + * The digest is calculated by prefixing an arbitrary `message` with + * `"\x19Ethereum Signed Message:\n" + len(message)` and hashing the result. It corresponds with the + * hash signed when using the https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] JSON-RPC method. + * + * See {ECDSA-recover}. + */ + function toEthSignedMessageHash(bytes memory message) internal pure returns (bytes32) { + return + keccak256(bytes.concat("\x19Ethereum Signed Message:\n", bytes(Strings.toString(message.length)), message)); + } + + /** + * @dev Returns the keccak256 digest of an EIP-191 signed data with version + * `0x00` (data with intended validator). + * + * The digest is calculated by prefixing an arbitrary `data` with `"\x19\x00"` and the intended + * `validator` address. Then hashing the result. + * + * See {ECDSA-recover}. + */ + function toDataWithIntendedValidatorHash(address validator, bytes memory data) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(hex"19_00", validator, data)); + } + + /** + * @dev Returns the keccak256 digest of an EIP-712 typed data (EIP-191 version `0x01`). + * + * The digest is calculated from a `domainSeparator` and a `structHash`, by prefixing them with + * `\x19\x01` and hashing the result. It corresponds to the hash signed by the + * https://eips.ethereum.org/EIPS/eip-712[`eth_signTypedData`] JSON-RPC method as part of EIP-712. + * + * See {ECDSA-recover}. + */ + function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32 digest) { + /// @solidity memory-safe-assembly + assembly { + let ptr := mload(0x40) + mstore(ptr, hex"19_01") + mstore(add(ptr, 0x02), domainSeparator) + mstore(add(ptr, 0x22), structHash) + digest := keccak256(ptr, 0x42) + } + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/ERC165.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/ERC165.sol new file mode 100644 index 00000000000..1e77b60d739 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/ERC165.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/introspection/ERC165.sol) + +pragma solidity ^0.8.20; + +import {IERC165} from "./IERC165.sol"; + +/** + * @dev Implementation of the {IERC165} interface. + * + * Contracts that want to implement ERC165 should inherit from this contract and override {supportsInterface} to check + * for the additional interface id that will be supported. For example: + * + * ```solidity + * function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + * return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId); + * } + * ``` + */ +abstract contract ERC165 is IERC165 { + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { + return interfaceId == type(IERC165).interfaceId; + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/ERC165Checker.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/ERC165Checker.sol new file mode 100644 index 00000000000..7b52241446d --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/ERC165Checker.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/introspection/ERC165Checker.sol) + +pragma solidity ^0.8.20; + +import {IERC165} from "./IERC165.sol"; + +/** + * @dev Library used to query support of an interface declared via {IERC165}. + * + * Note that these functions return the actual result of the query: they do not + * `revert` if an interface is not supported. It is up to the caller to decide + * what to do in these cases. + */ +library ERC165Checker { + // As per the EIP-165 spec, no interface should ever match 0xffffffff + bytes4 private constant INTERFACE_ID_INVALID = 0xffffffff; + + /** + * @dev Returns true if `account` supports the {IERC165} interface. + */ + function supportsERC165(address account) internal view returns (bool) { + // Any contract that implements ERC165 must explicitly indicate support of + // InterfaceId_ERC165 and explicitly indicate non-support of InterfaceId_Invalid + return + supportsERC165InterfaceUnchecked(account, type(IERC165).interfaceId) && + !supportsERC165InterfaceUnchecked(account, INTERFACE_ID_INVALID); + } + + /** + * @dev Returns true if `account` supports the interface defined by + * `interfaceId`. Support for {IERC165} itself is queried automatically. + * + * See {IERC165-supportsInterface}. + */ + function supportsInterface(address account, bytes4 interfaceId) internal view returns (bool) { + // query support of both ERC165 as per the spec and support of _interfaceId + return supportsERC165(account) && supportsERC165InterfaceUnchecked(account, interfaceId); + } + + /** + * @dev Returns a boolean array where each value corresponds to the + * interfaces passed in and whether they're supported or not. This allows + * you to batch check interfaces for a contract where your expectation + * is that some interfaces may not be supported. + * + * See {IERC165-supportsInterface}. + */ + function getSupportedInterfaces( + address account, + bytes4[] memory interfaceIds + ) internal view returns (bool[] memory) { + // an array of booleans corresponding to interfaceIds and whether they're supported or not + bool[] memory interfaceIdsSupported = new bool[](interfaceIds.length); + + // query support of ERC165 itself + if (supportsERC165(account)) { + // query support of each interface in interfaceIds + for (uint256 i = 0; i < interfaceIds.length; i++) { + interfaceIdsSupported[i] = supportsERC165InterfaceUnchecked(account, interfaceIds[i]); + } + } + + return interfaceIdsSupported; + } + + /** + * @dev Returns true if `account` supports all the interfaces defined in + * `interfaceIds`. Support for {IERC165} itself is queried automatically. + * + * Batch-querying can lead to gas savings by skipping repeated checks for + * {IERC165} support. + * + * See {IERC165-supportsInterface}. + */ + function supportsAllInterfaces(address account, bytes4[] memory interfaceIds) internal view returns (bool) { + // query support of ERC165 itself + if (!supportsERC165(account)) { + return false; + } + + // query support of each interface in interfaceIds + for (uint256 i = 0; i < interfaceIds.length; i++) { + if (!supportsERC165InterfaceUnchecked(account, interfaceIds[i])) { + return false; + } + } + + // all interfaces supported + return true; + } + + /** + * @notice Query if a contract implements an interface, does not check ERC165 support + * @param account The address of the contract to query for support of an interface + * @param interfaceId The interface identifier, as specified in ERC-165 + * @return true if the contract at account indicates support of the interface with + * identifier interfaceId, false otherwise + * @dev Assumes that account contains a contract that supports ERC165, otherwise + * the behavior of this method is undefined. This precondition can be checked + * with {supportsERC165}. + * + * Some precompiled contracts will falsely indicate support for a given interface, so caution + * should be exercised when using this function. + * + * Interface identification is specified in ERC-165. + */ + function supportsERC165InterfaceUnchecked(address account, bytes4 interfaceId) internal view returns (bool) { + // prepare call + bytes memory encodedParams = abi.encodeCall(IERC165.supportsInterface, (interfaceId)); + + // perform static call + bool success; + uint256 returnSize; + uint256 returnValue; + assembly { + success := staticcall(30000, account, add(encodedParams, 0x20), mload(encodedParams), 0x00, 0x20) + returnSize := returndatasize() + returnValue := mload(0x00) + } + + return success && returnSize >= 0x20 && returnValue > 0; + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/IERC165.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/IERC165.sol new file mode 100644 index 00000000000..c09f31fe128 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/IERC165.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/introspection/IERC165.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Interface of the ERC165 standard, as defined in the + * https://eips.ethereum.org/EIPS/eip-165[EIP]. + * + * Implementers can declare support of contract interfaces, which can then be + * queried by others ({ERC165Checker}). + * + * For an implementation, see {ERC165}. + */ +interface IERC165 { + /** + * @dev Returns true if this contract implements the interface defined by + * `interfaceId`. See the corresponding + * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] + * to learn more about how these ids are created. + * + * This function call must use less than 30 000 gas. + */ + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/math/Math.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/math/Math.sol new file mode 100644 index 00000000000..9681524529b --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/math/Math.sol @@ -0,0 +1,415 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/math/Math.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Standard math utilities missing in the Solidity language. + */ +library Math { + /** + * @dev Muldiv operation overflow. + */ + error MathOverflowedMulDiv(); + + enum Rounding { + Floor, // Toward negative infinity + Ceil, // Toward positive infinity + Trunc, // Toward zero + Expand // Away from zero + } + + /** + * @dev Returns the addition of two unsigned integers, with an overflow flag. + */ + function tryAdd(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + uint256 c = a + b; + if (c < a) return (false, 0); + return (true, c); + } + } + + /** + * @dev Returns the subtraction of two unsigned integers, with an overflow flag. + */ + function trySub(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + if (b > a) return (false, 0); + return (true, a - b); + } + } + + /** + * @dev Returns the multiplication of two unsigned integers, with an overflow flag. + */ + function tryMul(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 + if (a == 0) return (true, 0); + uint256 c = a * b; + if (c / a != b) return (false, 0); + return (true, c); + } + } + + /** + * @dev Returns the division of two unsigned integers, with a division by zero flag. + */ + function tryDiv(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + if (b == 0) return (false, 0); + return (true, a / b); + } + } + + /** + * @dev Returns the remainder of dividing two unsigned integers, with a division by zero flag. + */ + function tryMod(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + if (b == 0) return (false, 0); + return (true, a % b); + } + } + + /** + * @dev Returns the largest of two numbers. + */ + function max(uint256 a, uint256 b) internal pure returns (uint256) { + return a > b ? a : b; + } + + /** + * @dev Returns the smallest of two numbers. + */ + function min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } + + /** + * @dev Returns the average of two numbers. The result is rounded towards + * zero. + */ + function average(uint256 a, uint256 b) internal pure returns (uint256) { + // (a + b) / 2 can overflow. + return (a & b) + (a ^ b) / 2; + } + + /** + * @dev Returns the ceiling of the division of two numbers. + * + * This differs from standard division with `/` in that it rounds towards infinity instead + * of rounding towards zero. + */ + function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) { + if (b == 0) { + // Guarantee the same behavior as in a regular Solidity division. + return a / b; + } + + // (a + b - 1) / b can overflow on addition, so we distribute. + return a == 0 ? 0 : (a - 1) / b + 1; + } + + /** + * @notice Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or + * denominator == 0. + * @dev Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv) with further edits by + * Uniswap Labs also under MIT license. + */ + function mulDiv(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 result) { + unchecked { + // 512-bit multiply [prod1 prod0] = x * y. Compute the product mod 2^256 and mod 2^256 - 1, then use + // use the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256 + // variables such that product = prod1 * 2^256 + prod0. + uint256 prod0 = x * y; // Least significant 256 bits of the product + uint256 prod1; // Most significant 256 bits of the product + assembly { + let mm := mulmod(x, y, not(0)) + prod1 := sub(sub(mm, prod0), lt(mm, prod0)) + } + + // Handle non-overflow cases, 256 by 256 division. + if (prod1 == 0) { + // Solidity will revert if denominator == 0, unlike the div opcode on its own. + // The surrounding unchecked block does not change this fact. + // See https://docs.soliditylang.org/en/latest/control-structures.html#checked-or-unchecked-arithmetic. + return prod0 / denominator; + } + + // Make sure the result is less than 2^256. Also prevents denominator == 0. + if (denominator <= prod1) { + revert MathOverflowedMulDiv(); + } + + /////////////////////////////////////////////// + // 512 by 256 division. + /////////////////////////////////////////////// + + // Make division exact by subtracting the remainder from [prod1 prod0]. + uint256 remainder; + assembly { + // Compute remainder using mulmod. + remainder := mulmod(x, y, denominator) + + // Subtract 256 bit number from 512 bit number. + prod1 := sub(prod1, gt(remainder, prod0)) + prod0 := sub(prod0, remainder) + } + + // Factor powers of two out of denominator and compute largest power of two divisor of denominator. + // Always >= 1. See https://cs.stackexchange.com/q/138556/92363. + + uint256 twos = denominator & (0 - denominator); + assembly { + // Divide denominator by twos. + denominator := div(denominator, twos) + + // Divide [prod1 prod0] by twos. + prod0 := div(prod0, twos) + + // Flip twos such that it is 2^256 / twos. If twos is zero, then it becomes one. + twos := add(div(sub(0, twos), twos), 1) + } + + // Shift in bits from prod1 into prod0. + prod0 |= prod1 * twos; + + // Invert denominator mod 2^256. Now that denominator is an odd number, it has an inverse modulo 2^256 such + // that denominator * inv = 1 mod 2^256. Compute the inverse by starting with a seed that is correct for + // four bits. That is, denominator * inv = 1 mod 2^4. + uint256 inverse = (3 * denominator) ^ 2; + + // Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also + // works in modular arithmetic, doubling the correct bits in each step. + inverse *= 2 - denominator * inverse; // inverse mod 2^8 + inverse *= 2 - denominator * inverse; // inverse mod 2^16 + inverse *= 2 - denominator * inverse; // inverse mod 2^32 + inverse *= 2 - denominator * inverse; // inverse mod 2^64 + inverse *= 2 - denominator * inverse; // inverse mod 2^128 + inverse *= 2 - denominator * inverse; // inverse mod 2^256 + + // Because the division is now exact we can divide by multiplying with the modular inverse of denominator. + // This will give us the correct result modulo 2^256. Since the preconditions guarantee that the outcome is + // less than 2^256, this is the final result. We don't need to compute the high bits of the result and prod1 + // is no longer required. + result = prod0 * inverse; + return result; + } + } + + /** + * @notice Calculates x * y / denominator with full precision, following the selected rounding direction. + */ + function mulDiv(uint256 x, uint256 y, uint256 denominator, Rounding rounding) internal pure returns (uint256) { + uint256 result = mulDiv(x, y, denominator); + if (unsignedRoundsUp(rounding) && mulmod(x, y, denominator) > 0) { + result += 1; + } + return result; + } + + /** + * @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded + * towards zero. + * + * Inspired by Henry S. Warren, Jr.'s "Hacker's Delight" (Chapter 11). + */ + function sqrt(uint256 a) internal pure returns (uint256) { + if (a == 0) { + return 0; + } + + // For our first guess, we get the biggest power of 2 which is smaller than the square root of the target. + // + // We know that the "msb" (most significant bit) of our target number `a` is a power of 2 such that we have + // `msb(a) <= a < 2*msb(a)`. This value can be written `msb(a)=2**k` with `k=log2(a)`. + // + // This can be rewritten `2**log2(a) <= a < 2**(log2(a) + 1)` + // → `sqrt(2**k) <= sqrt(a) < sqrt(2**(k+1))` + // → `2**(k/2) <= sqrt(a) < 2**((k+1)/2) <= 2**(k/2 + 1)` + // + // Consequently, `2**(log2(a) / 2)` is a good first approximation of `sqrt(a)` with at least 1 correct bit. + uint256 result = 1 << (log2(a) >> 1); + + // At this point `result` is an estimation with one bit of precision. We know the true value is a uint128, + // since it is the square root of a uint256. Newton's method converges quadratically (precision doubles at + // every iteration). We thus need at most 7 iteration to turn our partial result with one bit of precision + // into the expected uint128 result. + unchecked { + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + return min(result, a / result); + } + } + + /** + * @notice Calculates sqrt(a), following the selected rounding direction. + */ + function sqrt(uint256 a, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = sqrt(a); + return result + (unsignedRoundsUp(rounding) && result * result < a ? 1 : 0); + } + } + + /** + * @dev Return the log in base 2 of a positive value rounded towards zero. + * Returns 0 if given 0. + */ + function log2(uint256 value) internal pure returns (uint256) { + uint256 result = 0; + unchecked { + if (value >> 128 > 0) { + value >>= 128; + result += 128; + } + if (value >> 64 > 0) { + value >>= 64; + result += 64; + } + if (value >> 32 > 0) { + value >>= 32; + result += 32; + } + if (value >> 16 > 0) { + value >>= 16; + result += 16; + } + if (value >> 8 > 0) { + value >>= 8; + result += 8; + } + if (value >> 4 > 0) { + value >>= 4; + result += 4; + } + if (value >> 2 > 0) { + value >>= 2; + result += 2; + } + if (value >> 1 > 0) { + result += 1; + } + } + return result; + } + + /** + * @dev Return the log in base 2, following the selected rounding direction, of a positive value. + * Returns 0 if given 0. + */ + function log2(uint256 value, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = log2(value); + return result + (unsignedRoundsUp(rounding) && 1 << result < value ? 1 : 0); + } + } + + /** + * @dev Return the log in base 10 of a positive value rounded towards zero. + * Returns 0 if given 0. + */ + function log10(uint256 value) internal pure returns (uint256) { + uint256 result = 0; + unchecked { + if (value >= 10 ** 64) { + value /= 10 ** 64; + result += 64; + } + if (value >= 10 ** 32) { + value /= 10 ** 32; + result += 32; + } + if (value >= 10 ** 16) { + value /= 10 ** 16; + result += 16; + } + if (value >= 10 ** 8) { + value /= 10 ** 8; + result += 8; + } + if (value >= 10 ** 4) { + value /= 10 ** 4; + result += 4; + } + if (value >= 10 ** 2) { + value /= 10 ** 2; + result += 2; + } + if (value >= 10 ** 1) { + result += 1; + } + } + return result; + } + + /** + * @dev Return the log in base 10, following the selected rounding direction, of a positive value. + * Returns 0 if given 0. + */ + function log10(uint256 value, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = log10(value); + return result + (unsignedRoundsUp(rounding) && 10 ** result < value ? 1 : 0); + } + } + + /** + * @dev Return the log in base 256 of a positive value rounded towards zero. + * Returns 0 if given 0. + * + * Adding one to the result gives the number of pairs of hex symbols needed to represent `value` as a hex string. + */ + function log256(uint256 value) internal pure returns (uint256) { + uint256 result = 0; + unchecked { + if (value >> 128 > 0) { + value >>= 128; + result += 16; + } + if (value >> 64 > 0) { + value >>= 64; + result += 8; + } + if (value >> 32 > 0) { + value >>= 32; + result += 4; + } + if (value >> 16 > 0) { + value >>= 16; + result += 2; + } + if (value >> 8 > 0) { + result += 1; + } + } + return result; + } + + /** + * @dev Return the log in base 256, following the selected rounding direction, of a positive value. + * Returns 0 if given 0. + */ + function log256(uint256 value, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = log256(value); + return result + (unsignedRoundsUp(rounding) && 1 << (result << 3) < value ? 1 : 0); + } + } + + /** + * @dev Returns whether a provided rounding mode is considered rounding up for unsigned integers. + */ + function unsignedRoundsUp(Rounding rounding) internal pure returns (bool) { + return uint8(rounding) % 2 == 1; + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/math/SafeCast.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/math/SafeCast.sol new file mode 100644 index 00000000000..0ed458b43c2 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/math/SafeCast.sol @@ -0,0 +1,1153 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/math/SafeCast.sol) +// This file was procedurally generated from scripts/generate/templates/SafeCast.js. + +pragma solidity ^0.8.20; + +/** + * @dev Wrappers over Solidity's uintXX/intXX casting operators with added overflow + * checks. + * + * Downcasting from uint256/int256 in Solidity does not revert on overflow. This can + * easily result in undesired exploitation or bugs, since developers usually + * assume that overflows raise errors. `SafeCast` restores this intuition by + * reverting the transaction when such an operation overflows. + * + * Using this library instead of the unchecked operations eliminates an entire + * class of bugs, so it's recommended to use it always. + */ +library SafeCast { + /** + * @dev Value doesn't fit in an uint of `bits` size. + */ + error SafeCastOverflowedUintDowncast(uint8 bits, uint256 value); + + /** + * @dev An int value doesn't fit in an uint of `bits` size. + */ + error SafeCastOverflowedIntToUint(int256 value); + + /** + * @dev Value doesn't fit in an int of `bits` size. + */ + error SafeCastOverflowedIntDowncast(uint8 bits, int256 value); + + /** + * @dev An uint value doesn't fit in an int of `bits` size. + */ + error SafeCastOverflowedUintToInt(uint256 value); + + /** + * @dev Returns the downcasted uint248 from uint256, reverting on + * overflow (when the input is greater than largest uint248). + * + * Counterpart to Solidity's `uint248` operator. + * + * Requirements: + * + * - input must fit into 248 bits + */ + function toUint248(uint256 value) internal pure returns (uint248) { + if (value > type(uint248).max) { + revert SafeCastOverflowedUintDowncast(248, value); + } + return uint248(value); + } + + /** + * @dev Returns the downcasted uint240 from uint256, reverting on + * overflow (when the input is greater than largest uint240). + * + * Counterpart to Solidity's `uint240` operator. + * + * Requirements: + * + * - input must fit into 240 bits + */ + function toUint240(uint256 value) internal pure returns (uint240) { + if (value > type(uint240).max) { + revert SafeCastOverflowedUintDowncast(240, value); + } + return uint240(value); + } + + /** + * @dev Returns the downcasted uint232 from uint256, reverting on + * overflow (when the input is greater than largest uint232). + * + * Counterpart to Solidity's `uint232` operator. + * + * Requirements: + * + * - input must fit into 232 bits + */ + function toUint232(uint256 value) internal pure returns (uint232) { + if (value > type(uint232).max) { + revert SafeCastOverflowedUintDowncast(232, value); + } + return uint232(value); + } + + /** + * @dev Returns the downcasted uint224 from uint256, reverting on + * overflow (when the input is greater than largest uint224). + * + * Counterpart to Solidity's `uint224` operator. + * + * Requirements: + * + * - input must fit into 224 bits + */ + function toUint224(uint256 value) internal pure returns (uint224) { + if (value > type(uint224).max) { + revert SafeCastOverflowedUintDowncast(224, value); + } + return uint224(value); + } + + /** + * @dev Returns the downcasted uint216 from uint256, reverting on + * overflow (when the input is greater than largest uint216). + * + * Counterpart to Solidity's `uint216` operator. + * + * Requirements: + * + * - input must fit into 216 bits + */ + function toUint216(uint256 value) internal pure returns (uint216) { + if (value > type(uint216).max) { + revert SafeCastOverflowedUintDowncast(216, value); + } + return uint216(value); + } + + /** + * @dev Returns the downcasted uint208 from uint256, reverting on + * overflow (when the input is greater than largest uint208). + * + * Counterpart to Solidity's `uint208` operator. + * + * Requirements: + * + * - input must fit into 208 bits + */ + function toUint208(uint256 value) internal pure returns (uint208) { + if (value > type(uint208).max) { + revert SafeCastOverflowedUintDowncast(208, value); + } + return uint208(value); + } + + /** + * @dev Returns the downcasted uint200 from uint256, reverting on + * overflow (when the input is greater than largest uint200). + * + * Counterpart to Solidity's `uint200` operator. + * + * Requirements: + * + * - input must fit into 200 bits + */ + function toUint200(uint256 value) internal pure returns (uint200) { + if (value > type(uint200).max) { + revert SafeCastOverflowedUintDowncast(200, value); + } + return uint200(value); + } + + /** + * @dev Returns the downcasted uint192 from uint256, reverting on + * overflow (when the input is greater than largest uint192). + * + * Counterpart to Solidity's `uint192` operator. + * + * Requirements: + * + * - input must fit into 192 bits + */ + function toUint192(uint256 value) internal pure returns (uint192) { + if (value > type(uint192).max) { + revert SafeCastOverflowedUintDowncast(192, value); + } + return uint192(value); + } + + /** + * @dev Returns the downcasted uint184 from uint256, reverting on + * overflow (when the input is greater than largest uint184). + * + * Counterpart to Solidity's `uint184` operator. + * + * Requirements: + * + * - input must fit into 184 bits + */ + function toUint184(uint256 value) internal pure returns (uint184) { + if (value > type(uint184).max) { + revert SafeCastOverflowedUintDowncast(184, value); + } + return uint184(value); + } + + /** + * @dev Returns the downcasted uint176 from uint256, reverting on + * overflow (when the input is greater than largest uint176). + * + * Counterpart to Solidity's `uint176` operator. + * + * Requirements: + * + * - input must fit into 176 bits + */ + function toUint176(uint256 value) internal pure returns (uint176) { + if (value > type(uint176).max) { + revert SafeCastOverflowedUintDowncast(176, value); + } + return uint176(value); + } + + /** + * @dev Returns the downcasted uint168 from uint256, reverting on + * overflow (when the input is greater than largest uint168). + * + * Counterpart to Solidity's `uint168` operator. + * + * Requirements: + * + * - input must fit into 168 bits + */ + function toUint168(uint256 value) internal pure returns (uint168) { + if (value > type(uint168).max) { + revert SafeCastOverflowedUintDowncast(168, value); + } + return uint168(value); + } + + /** + * @dev Returns the downcasted uint160 from uint256, reverting on + * overflow (when the input is greater than largest uint160). + * + * Counterpart to Solidity's `uint160` operator. + * + * Requirements: + * + * - input must fit into 160 bits + */ + function toUint160(uint256 value) internal pure returns (uint160) { + if (value > type(uint160).max) { + revert SafeCastOverflowedUintDowncast(160, value); + } + return uint160(value); + } + + /** + * @dev Returns the downcasted uint152 from uint256, reverting on + * overflow (when the input is greater than largest uint152). + * + * Counterpart to Solidity's `uint152` operator. + * + * Requirements: + * + * - input must fit into 152 bits + */ + function toUint152(uint256 value) internal pure returns (uint152) { + if (value > type(uint152).max) { + revert SafeCastOverflowedUintDowncast(152, value); + } + return uint152(value); + } + + /** + * @dev Returns the downcasted uint144 from uint256, reverting on + * overflow (when the input is greater than largest uint144). + * + * Counterpart to Solidity's `uint144` operator. + * + * Requirements: + * + * - input must fit into 144 bits + */ + function toUint144(uint256 value) internal pure returns (uint144) { + if (value > type(uint144).max) { + revert SafeCastOverflowedUintDowncast(144, value); + } + return uint144(value); + } + + /** + * @dev Returns the downcasted uint136 from uint256, reverting on + * overflow (when the input is greater than largest uint136). + * + * Counterpart to Solidity's `uint136` operator. + * + * Requirements: + * + * - input must fit into 136 bits + */ + function toUint136(uint256 value) internal pure returns (uint136) { + if (value > type(uint136).max) { + revert SafeCastOverflowedUintDowncast(136, value); + } + return uint136(value); + } + + /** + * @dev Returns the downcasted uint128 from uint256, reverting on + * overflow (when the input is greater than largest uint128). + * + * Counterpart to Solidity's `uint128` operator. + * + * Requirements: + * + * - input must fit into 128 bits + */ + function toUint128(uint256 value) internal pure returns (uint128) { + if (value > type(uint128).max) { + revert SafeCastOverflowedUintDowncast(128, value); + } + return uint128(value); + } + + /** + * @dev Returns the downcasted uint120 from uint256, reverting on + * overflow (when the input is greater than largest uint120). + * + * Counterpart to Solidity's `uint120` operator. + * + * Requirements: + * + * - input must fit into 120 bits + */ + function toUint120(uint256 value) internal pure returns (uint120) { + if (value > type(uint120).max) { + revert SafeCastOverflowedUintDowncast(120, value); + } + return uint120(value); + } + + /** + * @dev Returns the downcasted uint112 from uint256, reverting on + * overflow (when the input is greater than largest uint112). + * + * Counterpart to Solidity's `uint112` operator. + * + * Requirements: + * + * - input must fit into 112 bits + */ + function toUint112(uint256 value) internal pure returns (uint112) { + if (value > type(uint112).max) { + revert SafeCastOverflowedUintDowncast(112, value); + } + return uint112(value); + } + + /** + * @dev Returns the downcasted uint104 from uint256, reverting on + * overflow (when the input is greater than largest uint104). + * + * Counterpart to Solidity's `uint104` operator. + * + * Requirements: + * + * - input must fit into 104 bits + */ + function toUint104(uint256 value) internal pure returns (uint104) { + if (value > type(uint104).max) { + revert SafeCastOverflowedUintDowncast(104, value); + } + return uint104(value); + } + + /** + * @dev Returns the downcasted uint96 from uint256, reverting on + * overflow (when the input is greater than largest uint96). + * + * Counterpart to Solidity's `uint96` operator. + * + * Requirements: + * + * - input must fit into 96 bits + */ + function toUint96(uint256 value) internal pure returns (uint96) { + if (value > type(uint96).max) { + revert SafeCastOverflowedUintDowncast(96, value); + } + return uint96(value); + } + + /** + * @dev Returns the downcasted uint88 from uint256, reverting on + * overflow (when the input is greater than largest uint88). + * + * Counterpart to Solidity's `uint88` operator. + * + * Requirements: + * + * - input must fit into 88 bits + */ + function toUint88(uint256 value) internal pure returns (uint88) { + if (value > type(uint88).max) { + revert SafeCastOverflowedUintDowncast(88, value); + } + return uint88(value); + } + + /** + * @dev Returns the downcasted uint80 from uint256, reverting on + * overflow (when the input is greater than largest uint80). + * + * Counterpart to Solidity's `uint80` operator. + * + * Requirements: + * + * - input must fit into 80 bits + */ + function toUint80(uint256 value) internal pure returns (uint80) { + if (value > type(uint80).max) { + revert SafeCastOverflowedUintDowncast(80, value); + } + return uint80(value); + } + + /** + * @dev Returns the downcasted uint72 from uint256, reverting on + * overflow (when the input is greater than largest uint72). + * + * Counterpart to Solidity's `uint72` operator. + * + * Requirements: + * + * - input must fit into 72 bits + */ + function toUint72(uint256 value) internal pure returns (uint72) { + if (value > type(uint72).max) { + revert SafeCastOverflowedUintDowncast(72, value); + } + return uint72(value); + } + + /** + * @dev Returns the downcasted uint64 from uint256, reverting on + * overflow (when the input is greater than largest uint64). + * + * Counterpart to Solidity's `uint64` operator. + * + * Requirements: + * + * - input must fit into 64 bits + */ + function toUint64(uint256 value) internal pure returns (uint64) { + if (value > type(uint64).max) { + revert SafeCastOverflowedUintDowncast(64, value); + } + return uint64(value); + } + + /** + * @dev Returns the downcasted uint56 from uint256, reverting on + * overflow (when the input is greater than largest uint56). + * + * Counterpart to Solidity's `uint56` operator. + * + * Requirements: + * + * - input must fit into 56 bits + */ + function toUint56(uint256 value) internal pure returns (uint56) { + if (value > type(uint56).max) { + revert SafeCastOverflowedUintDowncast(56, value); + } + return uint56(value); + } + + /** + * @dev Returns the downcasted uint48 from uint256, reverting on + * overflow (when the input is greater than largest uint48). + * + * Counterpart to Solidity's `uint48` operator. + * + * Requirements: + * + * - input must fit into 48 bits + */ + function toUint48(uint256 value) internal pure returns (uint48) { + if (value > type(uint48).max) { + revert SafeCastOverflowedUintDowncast(48, value); + } + return uint48(value); + } + + /** + * @dev Returns the downcasted uint40 from uint256, reverting on + * overflow (when the input is greater than largest uint40). + * + * Counterpart to Solidity's `uint40` operator. + * + * Requirements: + * + * - input must fit into 40 bits + */ + function toUint40(uint256 value) internal pure returns (uint40) { + if (value > type(uint40).max) { + revert SafeCastOverflowedUintDowncast(40, value); + } + return uint40(value); + } + + /** + * @dev Returns the downcasted uint32 from uint256, reverting on + * overflow (when the input is greater than largest uint32). + * + * Counterpart to Solidity's `uint32` operator. + * + * Requirements: + * + * - input must fit into 32 bits + */ + function toUint32(uint256 value) internal pure returns (uint32) { + if (value > type(uint32).max) { + revert SafeCastOverflowedUintDowncast(32, value); + } + return uint32(value); + } + + /** + * @dev Returns the downcasted uint24 from uint256, reverting on + * overflow (when the input is greater than largest uint24). + * + * Counterpart to Solidity's `uint24` operator. + * + * Requirements: + * + * - input must fit into 24 bits + */ + function toUint24(uint256 value) internal pure returns (uint24) { + if (value > type(uint24).max) { + revert SafeCastOverflowedUintDowncast(24, value); + } + return uint24(value); + } + + /** + * @dev Returns the downcasted uint16 from uint256, reverting on + * overflow (when the input is greater than largest uint16). + * + * Counterpart to Solidity's `uint16` operator. + * + * Requirements: + * + * - input must fit into 16 bits + */ + function toUint16(uint256 value) internal pure returns (uint16) { + if (value > type(uint16).max) { + revert SafeCastOverflowedUintDowncast(16, value); + } + return uint16(value); + } + + /** + * @dev Returns the downcasted uint8 from uint256, reverting on + * overflow (when the input is greater than largest uint8). + * + * Counterpart to Solidity's `uint8` operator. + * + * Requirements: + * + * - input must fit into 8 bits + */ + function toUint8(uint256 value) internal pure returns (uint8) { + if (value > type(uint8).max) { + revert SafeCastOverflowedUintDowncast(8, value); + } + return uint8(value); + } + + /** + * @dev Converts a signed int256 into an unsigned uint256. + * + * Requirements: + * + * - input must be greater than or equal to 0. + */ + function toUint256(int256 value) internal pure returns (uint256) { + if (value < 0) { + revert SafeCastOverflowedIntToUint(value); + } + return uint256(value); + } + + /** + * @dev Returns the downcasted int248 from int256, reverting on + * overflow (when the input is less than smallest int248 or + * greater than largest int248). + * + * Counterpart to Solidity's `int248` operator. + * + * Requirements: + * + * - input must fit into 248 bits + */ + function toInt248(int256 value) internal pure returns (int248 downcasted) { + downcasted = int248(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(248, value); + } + } + + /** + * @dev Returns the downcasted int240 from int256, reverting on + * overflow (when the input is less than smallest int240 or + * greater than largest int240). + * + * Counterpart to Solidity's `int240` operator. + * + * Requirements: + * + * - input must fit into 240 bits + */ + function toInt240(int256 value) internal pure returns (int240 downcasted) { + downcasted = int240(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(240, value); + } + } + + /** + * @dev Returns the downcasted int232 from int256, reverting on + * overflow (when the input is less than smallest int232 or + * greater than largest int232). + * + * Counterpart to Solidity's `int232` operator. + * + * Requirements: + * + * - input must fit into 232 bits + */ + function toInt232(int256 value) internal pure returns (int232 downcasted) { + downcasted = int232(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(232, value); + } + } + + /** + * @dev Returns the downcasted int224 from int256, reverting on + * overflow (when the input is less than smallest int224 or + * greater than largest int224). + * + * Counterpart to Solidity's `int224` operator. + * + * Requirements: + * + * - input must fit into 224 bits + */ + function toInt224(int256 value) internal pure returns (int224 downcasted) { + downcasted = int224(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(224, value); + } + } + + /** + * @dev Returns the downcasted int216 from int256, reverting on + * overflow (when the input is less than smallest int216 or + * greater than largest int216). + * + * Counterpart to Solidity's `int216` operator. + * + * Requirements: + * + * - input must fit into 216 bits + */ + function toInt216(int256 value) internal pure returns (int216 downcasted) { + downcasted = int216(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(216, value); + } + } + + /** + * @dev Returns the downcasted int208 from int256, reverting on + * overflow (when the input is less than smallest int208 or + * greater than largest int208). + * + * Counterpart to Solidity's `int208` operator. + * + * Requirements: + * + * - input must fit into 208 bits + */ + function toInt208(int256 value) internal pure returns (int208 downcasted) { + downcasted = int208(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(208, value); + } + } + + /** + * @dev Returns the downcasted int200 from int256, reverting on + * overflow (when the input is less than smallest int200 or + * greater than largest int200). + * + * Counterpart to Solidity's `int200` operator. + * + * Requirements: + * + * - input must fit into 200 bits + */ + function toInt200(int256 value) internal pure returns (int200 downcasted) { + downcasted = int200(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(200, value); + } + } + + /** + * @dev Returns the downcasted int192 from int256, reverting on + * overflow (when the input is less than smallest int192 or + * greater than largest int192). + * + * Counterpart to Solidity's `int192` operator. + * + * Requirements: + * + * - input must fit into 192 bits + */ + function toInt192(int256 value) internal pure returns (int192 downcasted) { + downcasted = int192(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(192, value); + } + } + + /** + * @dev Returns the downcasted int184 from int256, reverting on + * overflow (when the input is less than smallest int184 or + * greater than largest int184). + * + * Counterpart to Solidity's `int184` operator. + * + * Requirements: + * + * - input must fit into 184 bits + */ + function toInt184(int256 value) internal pure returns (int184 downcasted) { + downcasted = int184(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(184, value); + } + } + + /** + * @dev Returns the downcasted int176 from int256, reverting on + * overflow (when the input is less than smallest int176 or + * greater than largest int176). + * + * Counterpart to Solidity's `int176` operator. + * + * Requirements: + * + * - input must fit into 176 bits + */ + function toInt176(int256 value) internal pure returns (int176 downcasted) { + downcasted = int176(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(176, value); + } + } + + /** + * @dev Returns the downcasted int168 from int256, reverting on + * overflow (when the input is less than smallest int168 or + * greater than largest int168). + * + * Counterpart to Solidity's `int168` operator. + * + * Requirements: + * + * - input must fit into 168 bits + */ + function toInt168(int256 value) internal pure returns (int168 downcasted) { + downcasted = int168(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(168, value); + } + } + + /** + * @dev Returns the downcasted int160 from int256, reverting on + * overflow (when the input is less than smallest int160 or + * greater than largest int160). + * + * Counterpart to Solidity's `int160` operator. + * + * Requirements: + * + * - input must fit into 160 bits + */ + function toInt160(int256 value) internal pure returns (int160 downcasted) { + downcasted = int160(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(160, value); + } + } + + /** + * @dev Returns the downcasted int152 from int256, reverting on + * overflow (when the input is less than smallest int152 or + * greater than largest int152). + * + * Counterpart to Solidity's `int152` operator. + * + * Requirements: + * + * - input must fit into 152 bits + */ + function toInt152(int256 value) internal pure returns (int152 downcasted) { + downcasted = int152(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(152, value); + } + } + + /** + * @dev Returns the downcasted int144 from int256, reverting on + * overflow (when the input is less than smallest int144 or + * greater than largest int144). + * + * Counterpart to Solidity's `int144` operator. + * + * Requirements: + * + * - input must fit into 144 bits + */ + function toInt144(int256 value) internal pure returns (int144 downcasted) { + downcasted = int144(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(144, value); + } + } + + /** + * @dev Returns the downcasted int136 from int256, reverting on + * overflow (when the input is less than smallest int136 or + * greater than largest int136). + * + * Counterpart to Solidity's `int136` operator. + * + * Requirements: + * + * - input must fit into 136 bits + */ + function toInt136(int256 value) internal pure returns (int136 downcasted) { + downcasted = int136(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(136, value); + } + } + + /** + * @dev Returns the downcasted int128 from int256, reverting on + * overflow (when the input is less than smallest int128 or + * greater than largest int128). + * + * Counterpart to Solidity's `int128` operator. + * + * Requirements: + * + * - input must fit into 128 bits + */ + function toInt128(int256 value) internal pure returns (int128 downcasted) { + downcasted = int128(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(128, value); + } + } + + /** + * @dev Returns the downcasted int120 from int256, reverting on + * overflow (when the input is less than smallest int120 or + * greater than largest int120). + * + * Counterpart to Solidity's `int120` operator. + * + * Requirements: + * + * - input must fit into 120 bits + */ + function toInt120(int256 value) internal pure returns (int120 downcasted) { + downcasted = int120(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(120, value); + } + } + + /** + * @dev Returns the downcasted int112 from int256, reverting on + * overflow (when the input is less than smallest int112 or + * greater than largest int112). + * + * Counterpart to Solidity's `int112` operator. + * + * Requirements: + * + * - input must fit into 112 bits + */ + function toInt112(int256 value) internal pure returns (int112 downcasted) { + downcasted = int112(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(112, value); + } + } + + /** + * @dev Returns the downcasted int104 from int256, reverting on + * overflow (when the input is less than smallest int104 or + * greater than largest int104). + * + * Counterpart to Solidity's `int104` operator. + * + * Requirements: + * + * - input must fit into 104 bits + */ + function toInt104(int256 value) internal pure returns (int104 downcasted) { + downcasted = int104(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(104, value); + } + } + + /** + * @dev Returns the downcasted int96 from int256, reverting on + * overflow (when the input is less than smallest int96 or + * greater than largest int96). + * + * Counterpart to Solidity's `int96` operator. + * + * Requirements: + * + * - input must fit into 96 bits + */ + function toInt96(int256 value) internal pure returns (int96 downcasted) { + downcasted = int96(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(96, value); + } + } + + /** + * @dev Returns the downcasted int88 from int256, reverting on + * overflow (when the input is less than smallest int88 or + * greater than largest int88). + * + * Counterpart to Solidity's `int88` operator. + * + * Requirements: + * + * - input must fit into 88 bits + */ + function toInt88(int256 value) internal pure returns (int88 downcasted) { + downcasted = int88(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(88, value); + } + } + + /** + * @dev Returns the downcasted int80 from int256, reverting on + * overflow (when the input is less than smallest int80 or + * greater than largest int80). + * + * Counterpart to Solidity's `int80` operator. + * + * Requirements: + * + * - input must fit into 80 bits + */ + function toInt80(int256 value) internal pure returns (int80 downcasted) { + downcasted = int80(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(80, value); + } + } + + /** + * @dev Returns the downcasted int72 from int256, reverting on + * overflow (when the input is less than smallest int72 or + * greater than largest int72). + * + * Counterpart to Solidity's `int72` operator. + * + * Requirements: + * + * - input must fit into 72 bits + */ + function toInt72(int256 value) internal pure returns (int72 downcasted) { + downcasted = int72(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(72, value); + } + } + + /** + * @dev Returns the downcasted int64 from int256, reverting on + * overflow (when the input is less than smallest int64 or + * greater than largest int64). + * + * Counterpart to Solidity's `int64` operator. + * + * Requirements: + * + * - input must fit into 64 bits + */ + function toInt64(int256 value) internal pure returns (int64 downcasted) { + downcasted = int64(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(64, value); + } + } + + /** + * @dev Returns the downcasted int56 from int256, reverting on + * overflow (when the input is less than smallest int56 or + * greater than largest int56). + * + * Counterpart to Solidity's `int56` operator. + * + * Requirements: + * + * - input must fit into 56 bits + */ + function toInt56(int256 value) internal pure returns (int56 downcasted) { + downcasted = int56(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(56, value); + } + } + + /** + * @dev Returns the downcasted int48 from int256, reverting on + * overflow (when the input is less than smallest int48 or + * greater than largest int48). + * + * Counterpart to Solidity's `int48` operator. + * + * Requirements: + * + * - input must fit into 48 bits + */ + function toInt48(int256 value) internal pure returns (int48 downcasted) { + downcasted = int48(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(48, value); + } + } + + /** + * @dev Returns the downcasted int40 from int256, reverting on + * overflow (when the input is less than smallest int40 or + * greater than largest int40). + * + * Counterpart to Solidity's `int40` operator. + * + * Requirements: + * + * - input must fit into 40 bits + */ + function toInt40(int256 value) internal pure returns (int40 downcasted) { + downcasted = int40(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(40, value); + } + } + + /** + * @dev Returns the downcasted int32 from int256, reverting on + * overflow (when the input is less than smallest int32 or + * greater than largest int32). + * + * Counterpart to Solidity's `int32` operator. + * + * Requirements: + * + * - input must fit into 32 bits + */ + function toInt32(int256 value) internal pure returns (int32 downcasted) { + downcasted = int32(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(32, value); + } + } + + /** + * @dev Returns the downcasted int24 from int256, reverting on + * overflow (when the input is less than smallest int24 or + * greater than largest int24). + * + * Counterpart to Solidity's `int24` operator. + * + * Requirements: + * + * - input must fit into 24 bits + */ + function toInt24(int256 value) internal pure returns (int24 downcasted) { + downcasted = int24(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(24, value); + } + } + + /** + * @dev Returns the downcasted int16 from int256, reverting on + * overflow (when the input is less than smallest int16 or + * greater than largest int16). + * + * Counterpart to Solidity's `int16` operator. + * + * Requirements: + * + * - input must fit into 16 bits + */ + function toInt16(int256 value) internal pure returns (int16 downcasted) { + downcasted = int16(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(16, value); + } + } + + /** + * @dev Returns the downcasted int8 from int256, reverting on + * overflow (when the input is less than smallest int8 or + * greater than largest int8). + * + * Counterpart to Solidity's `int8` operator. + * + * Requirements: + * + * - input must fit into 8 bits + */ + function toInt8(int256 value) internal pure returns (int8 downcasted) { + downcasted = int8(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(8, value); + } + } + + /** + * @dev Converts an unsigned uint256 into a signed int256. + * + * Requirements: + * + * - input must be less than or equal to maxInt256. + */ + function toInt256(uint256 value) internal pure returns (int256) { + // Note: Unsafe cast below is okay because `type(int256).max` is guaranteed to be positive + if (value > uint256(type(int256).max)) { + revert SafeCastOverflowedUintToInt(value); + } + return int256(value); + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/math/SignedMath.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/math/SignedMath.sol new file mode 100644 index 00000000000..66a61516292 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/math/SignedMath.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/math/SignedMath.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Standard signed math utilities missing in the Solidity language. + */ +library SignedMath { + /** + * @dev Returns the largest of two signed numbers. + */ + function max(int256 a, int256 b) internal pure returns (int256) { + return a > b ? a : b; + } + + /** + * @dev Returns the smallest of two signed numbers. + */ + function min(int256 a, int256 b) internal pure returns (int256) { + return a < b ? a : b; + } + + /** + * @dev Returns the average of two signed numbers without overflow. + * The result is rounded towards zero. + */ + function average(int256 a, int256 b) internal pure returns (int256) { + // Formula from the book "Hacker's Delight" + int256 x = (a & b) + ((a ^ b) >> 1); + return x + (int256(uint256(x) >> 255) & (a ^ b)); + } + + /** + * @dev Returns the absolute unsigned value of a signed value. + */ + function abs(int256 n) internal pure returns (uint256) { + unchecked { + // must be unchecked in order to support `n = type(int256).min` + return uint256(n >= 0 ? n : -n); + } + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/structs/EnumerableMap.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/structs/EnumerableMap.sol new file mode 100644 index 00000000000..929ae7c536e --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/structs/EnumerableMap.sol @@ -0,0 +1,533 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/structs/EnumerableMap.sol) +// This file was procedurally generated from scripts/generate/templates/EnumerableMap.js. + +pragma solidity ^0.8.20; + +import {EnumerableSet} from "./EnumerableSet.sol"; + +/** + * @dev Library for managing an enumerable variant of Solidity's + * https://solidity.readthedocs.io/en/latest/types.html#mapping-types[`mapping`] + * type. + * + * Maps have the following properties: + * + * - Entries are added, removed, and checked for existence in constant time + * (O(1)). + * - Entries are enumerated in O(n). No guarantees are made on the ordering. + * + * ```solidity + * contract Example { + * // Add the library methods + * using EnumerableMap for EnumerableMap.UintToAddressMap; + * + * // Declare a set state variable + * EnumerableMap.UintToAddressMap private myMap; + * } + * ``` + * + * The following map types are supported: + * + * - `uint256 -> address` (`UintToAddressMap`) since v3.0.0 + * - `address -> uint256` (`AddressToUintMap`) since v4.6.0 + * - `bytes32 -> bytes32` (`Bytes32ToBytes32Map`) since v4.6.0 + * - `uint256 -> uint256` (`UintToUintMap`) since v4.7.0 + * - `bytes32 -> uint256` (`Bytes32ToUintMap`) since v4.7.0 + * + * [WARNING] + * ==== + * Trying to delete such a structure from storage will likely result in data corruption, rendering the structure + * unusable. + * See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info. + * + * In order to clean an EnumerableMap, you can either remove all elements one by one or create a fresh instance using an + * array of EnumerableMap. + * ==== + */ +library EnumerableMap { + using EnumerableSet for EnumerableSet.Bytes32Set; + + // To implement this library for multiple types with as little code repetition as possible, we write it in + // terms of a generic Map type with bytes32 keys and values. The Map implementation uses private functions, + // and user-facing implementations such as `UintToAddressMap` are just wrappers around the underlying Map. + // This means that we can only create new EnumerableMaps for types that fit in bytes32. + + /** + * @dev Query for a nonexistent map key. + */ + error EnumerableMapNonexistentKey(bytes32 key); + + struct Bytes32ToBytes32Map { + // Storage of keys + EnumerableSet.Bytes32Set _keys; + mapping(bytes32 key => bytes32) _values; + } + + /** + * @dev Adds a key-value pair to a map, or updates the value for an existing + * key. O(1). + * + * Returns true if the key was added to the map, that is if it was not + * already present. + */ + function set(Bytes32ToBytes32Map storage map, bytes32 key, bytes32 value) internal returns (bool) { + map._values[key] = value; + return map._keys.add(key); + } + + /** + * @dev Removes a key-value pair from a map. O(1). + * + * Returns true if the key was removed from the map, that is if it was present. + */ + function remove(Bytes32ToBytes32Map storage map, bytes32 key) internal returns (bool) { + delete map._values[key]; + return map._keys.remove(key); + } + + /** + * @dev Returns true if the key is in the map. O(1). + */ + function contains(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bool) { + return map._keys.contains(key); + } + + /** + * @dev Returns the number of key-value pairs in the map. O(1). + */ + function length(Bytes32ToBytes32Map storage map) internal view returns (uint256) { + return map._keys.length(); + } + + /** + * @dev Returns the key-value pair stored at position `index` in the map. O(1). + * + * Note that there are no guarantees on the ordering of entries inside the + * array, and it may change when more entries are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(Bytes32ToBytes32Map storage map, uint256 index) internal view returns (bytes32, bytes32) { + bytes32 key = map._keys.at(index); + return (key, map._values[key]); + } + + /** + * @dev Tries to returns the value associated with `key`. O(1). + * Does not revert if `key` is not in the map. + */ + function tryGet(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bool, bytes32) { + bytes32 value = map._values[key]; + if (value == bytes32(0)) { + return (contains(map, key), bytes32(0)); + } else { + return (true, value); + } + } + + /** + * @dev Returns the value associated with `key`. O(1). + * + * Requirements: + * + * - `key` must be in the map. + */ + function get(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bytes32) { + bytes32 value = map._values[key]; + if (value == 0 && !contains(map, key)) { + revert EnumerableMapNonexistentKey(key); + } + return value; + } + + /** + * @dev Return the an array containing all the keys + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the map grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function keys(Bytes32ToBytes32Map storage map) internal view returns (bytes32[] memory) { + return map._keys.values(); + } + + // UintToUintMap + + struct UintToUintMap { + Bytes32ToBytes32Map _inner; + } + + /** + * @dev Adds a key-value pair to a map, or updates the value for an existing + * key. O(1). + * + * Returns true if the key was added to the map, that is if it was not + * already present. + */ + function set(UintToUintMap storage map, uint256 key, uint256 value) internal returns (bool) { + return set(map._inner, bytes32(key), bytes32(value)); + } + + /** + * @dev Removes a value from a map. O(1). + * + * Returns true if the key was removed from the map, that is if it was present. + */ + function remove(UintToUintMap storage map, uint256 key) internal returns (bool) { + return remove(map._inner, bytes32(key)); + } + + /** + * @dev Returns true if the key is in the map. O(1). + */ + function contains(UintToUintMap storage map, uint256 key) internal view returns (bool) { + return contains(map._inner, bytes32(key)); + } + + /** + * @dev Returns the number of elements in the map. O(1). + */ + function length(UintToUintMap storage map) internal view returns (uint256) { + return length(map._inner); + } + + /** + * @dev Returns the element stored at position `index` in the map. O(1). + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(UintToUintMap storage map, uint256 index) internal view returns (uint256, uint256) { + (bytes32 key, bytes32 value) = at(map._inner, index); + return (uint256(key), uint256(value)); + } + + /** + * @dev Tries to returns the value associated with `key`. O(1). + * Does not revert if `key` is not in the map. + */ + function tryGet(UintToUintMap storage map, uint256 key) internal view returns (bool, uint256) { + (bool success, bytes32 value) = tryGet(map._inner, bytes32(key)); + return (success, uint256(value)); + } + + /** + * @dev Returns the value associated with `key`. O(1). + * + * Requirements: + * + * - `key` must be in the map. + */ + function get(UintToUintMap storage map, uint256 key) internal view returns (uint256) { + return uint256(get(map._inner, bytes32(key))); + } + + /** + * @dev Return the an array containing all the keys + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the map grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function keys(UintToUintMap storage map) internal view returns (uint256[] memory) { + bytes32[] memory store = keys(map._inner); + uint256[] memory result; + + /// @solidity memory-safe-assembly + assembly { + result := store + } + + return result; + } + + // UintToAddressMap + + struct UintToAddressMap { + Bytes32ToBytes32Map _inner; + } + + /** + * @dev Adds a key-value pair to a map, or updates the value for an existing + * key. O(1). + * + * Returns true if the key was added to the map, that is if it was not + * already present. + */ + function set(UintToAddressMap storage map, uint256 key, address value) internal returns (bool) { + return set(map._inner, bytes32(key), bytes32(uint256(uint160(value)))); + } + + /** + * @dev Removes a value from a map. O(1). + * + * Returns true if the key was removed from the map, that is if it was present. + */ + function remove(UintToAddressMap storage map, uint256 key) internal returns (bool) { + return remove(map._inner, bytes32(key)); + } + + /** + * @dev Returns true if the key is in the map. O(1). + */ + function contains(UintToAddressMap storage map, uint256 key) internal view returns (bool) { + return contains(map._inner, bytes32(key)); + } + + /** + * @dev Returns the number of elements in the map. O(1). + */ + function length(UintToAddressMap storage map) internal view returns (uint256) { + return length(map._inner); + } + + /** + * @dev Returns the element stored at position `index` in the map. O(1). + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(UintToAddressMap storage map, uint256 index) internal view returns (uint256, address) { + (bytes32 key, bytes32 value) = at(map._inner, index); + return (uint256(key), address(uint160(uint256(value)))); + } + + /** + * @dev Tries to returns the value associated with `key`. O(1). + * Does not revert if `key` is not in the map. + */ + function tryGet(UintToAddressMap storage map, uint256 key) internal view returns (bool, address) { + (bool success, bytes32 value) = tryGet(map._inner, bytes32(key)); + return (success, address(uint160(uint256(value)))); + } + + /** + * @dev Returns the value associated with `key`. O(1). + * + * Requirements: + * + * - `key` must be in the map. + */ + function get(UintToAddressMap storage map, uint256 key) internal view returns (address) { + return address(uint160(uint256(get(map._inner, bytes32(key))))); + } + + /** + * @dev Return the an array containing all the keys + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the map grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function keys(UintToAddressMap storage map) internal view returns (uint256[] memory) { + bytes32[] memory store = keys(map._inner); + uint256[] memory result; + + /// @solidity memory-safe-assembly + assembly { + result := store + } + + return result; + } + + // AddressToUintMap + + struct AddressToUintMap { + Bytes32ToBytes32Map _inner; + } + + /** + * @dev Adds a key-value pair to a map, or updates the value for an existing + * key. O(1). + * + * Returns true if the key was added to the map, that is if it was not + * already present. + */ + function set(AddressToUintMap storage map, address key, uint256 value) internal returns (bool) { + return set(map._inner, bytes32(uint256(uint160(key))), bytes32(value)); + } + + /** + * @dev Removes a value from a map. O(1). + * + * Returns true if the key was removed from the map, that is if it was present. + */ + function remove(AddressToUintMap storage map, address key) internal returns (bool) { + return remove(map._inner, bytes32(uint256(uint160(key)))); + } + + /** + * @dev Returns true if the key is in the map. O(1). + */ + function contains(AddressToUintMap storage map, address key) internal view returns (bool) { + return contains(map._inner, bytes32(uint256(uint160(key)))); + } + + /** + * @dev Returns the number of elements in the map. O(1). + */ + function length(AddressToUintMap storage map) internal view returns (uint256) { + return length(map._inner); + } + + /** + * @dev Returns the element stored at position `index` in the map. O(1). + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(AddressToUintMap storage map, uint256 index) internal view returns (address, uint256) { + (bytes32 key, bytes32 value) = at(map._inner, index); + return (address(uint160(uint256(key))), uint256(value)); + } + + /** + * @dev Tries to returns the value associated with `key`. O(1). + * Does not revert if `key` is not in the map. + */ + function tryGet(AddressToUintMap storage map, address key) internal view returns (bool, uint256) { + (bool success, bytes32 value) = tryGet(map._inner, bytes32(uint256(uint160(key)))); + return (success, uint256(value)); + } + + /** + * @dev Returns the value associated with `key`. O(1). + * + * Requirements: + * + * - `key` must be in the map. + */ + function get(AddressToUintMap storage map, address key) internal view returns (uint256) { + return uint256(get(map._inner, bytes32(uint256(uint160(key))))); + } + + /** + * @dev Return the an array containing all the keys + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the map grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function keys(AddressToUintMap storage map) internal view returns (address[] memory) { + bytes32[] memory store = keys(map._inner); + address[] memory result; + + /// @solidity memory-safe-assembly + assembly { + result := store + } + + return result; + } + + // Bytes32ToUintMap + + struct Bytes32ToUintMap { + Bytes32ToBytes32Map _inner; + } + + /** + * @dev Adds a key-value pair to a map, or updates the value for an existing + * key. O(1). + * + * Returns true if the key was added to the map, that is if it was not + * already present. + */ + function set(Bytes32ToUintMap storage map, bytes32 key, uint256 value) internal returns (bool) { + return set(map._inner, key, bytes32(value)); + } + + /** + * @dev Removes a value from a map. O(1). + * + * Returns true if the key was removed from the map, that is if it was present. + */ + function remove(Bytes32ToUintMap storage map, bytes32 key) internal returns (bool) { + return remove(map._inner, key); + } + + /** + * @dev Returns true if the key is in the map. O(1). + */ + function contains(Bytes32ToUintMap storage map, bytes32 key) internal view returns (bool) { + return contains(map._inner, key); + } + + /** + * @dev Returns the number of elements in the map. O(1). + */ + function length(Bytes32ToUintMap storage map) internal view returns (uint256) { + return length(map._inner); + } + + /** + * @dev Returns the element stored at position `index` in the map. O(1). + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(Bytes32ToUintMap storage map, uint256 index) internal view returns (bytes32, uint256) { + (bytes32 key, bytes32 value) = at(map._inner, index); + return (key, uint256(value)); + } + + /** + * @dev Tries to returns the value associated with `key`. O(1). + * Does not revert if `key` is not in the map. + */ + function tryGet(Bytes32ToUintMap storage map, bytes32 key) internal view returns (bool, uint256) { + (bool success, bytes32 value) = tryGet(map._inner, key); + return (success, uint256(value)); + } + + /** + * @dev Returns the value associated with `key`. O(1). + * + * Requirements: + * + * - `key` must be in the map. + */ + function get(Bytes32ToUintMap storage map, bytes32 key) internal view returns (uint256) { + return uint256(get(map._inner, key)); + } + + /** + * @dev Return the an array containing all the keys + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the map grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function keys(Bytes32ToUintMap storage map) internal view returns (bytes32[] memory) { + bytes32[] memory store = keys(map._inner); + bytes32[] memory result; + + /// @solidity memory-safe-assembly + assembly { + result := store + } + + return result; + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/structs/EnumerableSet.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/structs/EnumerableSet.sol new file mode 100644 index 00000000000..4c7fc5e1d76 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/structs/EnumerableSet.sol @@ -0,0 +1,378 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/structs/EnumerableSet.sol) +// This file was procedurally generated from scripts/generate/templates/EnumerableSet.js. + +pragma solidity ^0.8.20; + +/** + * @dev Library for managing + * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive + * types. + * + * Sets have the following properties: + * + * - Elements are added, removed, and checked for existence in constant time + * (O(1)). + * - Elements are enumerated in O(n). No guarantees are made on the ordering. + * + * ```solidity + * contract Example { + * // Add the library methods + * using EnumerableSet for EnumerableSet.AddressSet; + * + * // Declare a set state variable + * EnumerableSet.AddressSet private mySet; + * } + * ``` + * + * As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`) + * and `uint256` (`UintSet`) are supported. + * + * [WARNING] + * ==== + * Trying to delete such a structure from storage will likely result in data corruption, rendering the structure + * unusable. + * See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info. + * + * In order to clean an EnumerableSet, you can either remove all elements one by one or create a fresh instance using an + * array of EnumerableSet. + * ==== + */ +library EnumerableSet { + // To implement this library for multiple types with as little code + // repetition as possible, we write it in terms of a generic Set type with + // bytes32 values. + // The Set implementation uses private functions, and user-facing + // implementations (such as AddressSet) are just wrappers around the + // underlying Set. + // This means that we can only create new EnumerableSets for types that fit + // in bytes32. + + struct Set { + // Storage of set values + bytes32[] _values; + // Position is the index of the value in the `values` array plus 1. + // Position 0 is used to mean a value is not in the set. + mapping(bytes32 value => uint256) _positions; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function _add(Set storage set, bytes32 value) private returns (bool) { + if (!_contains(set, value)) { + set._values.push(value); + // The value is stored at length-1, but we add 1 to all indexes + // and use 0 as a sentinel value + set._positions[value] = set._values.length; + return true; + } else { + return false; + } + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function _remove(Set storage set, bytes32 value) private returns (bool) { + // We cache the value's position to prevent multiple reads from the same storage slot + uint256 position = set._positions[value]; + + if (position != 0) { + // Equivalent to contains(set, value) + // To delete an element from the _values array in O(1), we swap the element to delete with the last one in + // the array, and then remove the last element (sometimes called as 'swap and pop'). + // This modifies the order of the array, as noted in {at}. + + uint256 valueIndex = position - 1; + uint256 lastIndex = set._values.length - 1; + + if (valueIndex != lastIndex) { + bytes32 lastValue = set._values[lastIndex]; + + // Move the lastValue to the index where the value to delete is + set._values[valueIndex] = lastValue; + // Update the tracked position of the lastValue (that was just moved) + set._positions[lastValue] = position; + } + + // Delete the slot where the moved value was stored + set._values.pop(); + + // Delete the tracked position for the deleted slot + delete set._positions[value]; + + return true; + } else { + return false; + } + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function _contains(Set storage set, bytes32 value) private view returns (bool) { + return set._positions[value] != 0; + } + + /** + * @dev Returns the number of values on the set. O(1). + */ + function _length(Set storage set) private view returns (uint256) { + return set._values.length; + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function _at(Set storage set, uint256 index) private view returns (bytes32) { + return set._values[index]; + } + + /** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function _values(Set storage set) private view returns (bytes32[] memory) { + return set._values; + } + + // Bytes32Set + + struct Bytes32Set { + Set _inner; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function add(Bytes32Set storage set, bytes32 value) internal returns (bool) { + return _add(set._inner, value); + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) { + return _remove(set._inner, value); + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) { + return _contains(set._inner, value); + } + + /** + * @dev Returns the number of values in the set. O(1). + */ + function length(Bytes32Set storage set) internal view returns (uint256) { + return _length(set._inner); + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) { + return _at(set._inner, index); + } + + /** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function values(Bytes32Set storage set) internal view returns (bytes32[] memory) { + bytes32[] memory store = _values(set._inner); + bytes32[] memory result; + + /// @solidity memory-safe-assembly + assembly { + result := store + } + + return result; + } + + // AddressSet + + struct AddressSet { + Set _inner; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function add(AddressSet storage set, address value) internal returns (bool) { + return _add(set._inner, bytes32(uint256(uint160(value)))); + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function remove(AddressSet storage set, address value) internal returns (bool) { + return _remove(set._inner, bytes32(uint256(uint160(value)))); + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function contains(AddressSet storage set, address value) internal view returns (bool) { + return _contains(set._inner, bytes32(uint256(uint160(value)))); + } + + /** + * @dev Returns the number of values in the set. O(1). + */ + function length(AddressSet storage set) internal view returns (uint256) { + return _length(set._inner); + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(AddressSet storage set, uint256 index) internal view returns (address) { + return address(uint160(uint256(_at(set._inner, index)))); + } + + /** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function values(AddressSet storage set) internal view returns (address[] memory) { + bytes32[] memory store = _values(set._inner); + address[] memory result; + + /// @solidity memory-safe-assembly + assembly { + result := store + } + + return result; + } + + // UintSet + + struct UintSet { + Set _inner; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function add(UintSet storage set, uint256 value) internal returns (bool) { + return _add(set._inner, bytes32(value)); + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function remove(UintSet storage set, uint256 value) internal returns (bool) { + return _remove(set._inner, bytes32(value)); + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function contains(UintSet storage set, uint256 value) internal view returns (bool) { + return _contains(set._inner, bytes32(value)); + } + + /** + * @dev Returns the number of values in the set. O(1). + */ + function length(UintSet storage set) internal view returns (uint256) { + return _length(set._inner); + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(UintSet storage set, uint256 index) internal view returns (uint256) { + return uint256(_at(set._inner, index)); + } + + /** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function values(UintSet storage set) internal view returns (uint256[] memory) { + bytes32[] memory store = _values(set._inner); + uint256[] memory result; + + /// @solidity memory-safe-assembly + assembly { + result := store + } + + return result; + } +} From 6ab3eb5b67739ff88d3c4cf8ea125fd8273bc2b1 Mon Sep 17 00:00:00 2001 From: "Abdelrahman Soliman (Boda)" <2677789+asoliman92@users.noreply.github.com> Date: Wed, 7 Aug 2024 22:32:07 +0400 Subject: [PATCH 9/9] [CCIP Merge] Capabilities [CCIP-2943] (#14068) * [CCIP Merge] Add ccip capabilities directory [CCIP-2943] (#14044) * Add ccip capabilities directory * [CCIP Merge] Capabilities fix [CCIP-2943] (#14048) * Fix compilation for launcher, diff Make application.go ready for adding more fixes * Fix launcher tests * ks-409 fix the mock trigger to ensure events are sent (#14047) * Add ccip to job orm * Add capabilities directory under BUSL license * Prep to instantiate separate registrysyncer for CCIP * Move registrySyncer creation into ccip delegate * [chore] Change registrysyncer config to bytes * Fix launcher diff tests after changing structs in syncer * Fix linting * MAke simulated backend client work with chains other than 1337 * core/capabilities/ccip: use OCR offchain config (#1264) We want to define and use the appropriate OCR offchain config for each plugin. Requires https://github.com/smartcontractkit/chainlink-ccip/pull/36/ * Cleaning up * Add capabilities types to mockery --------- Co-authored-by: Cedric Cordenier Co-authored-by: Matthew Pendrey Co-authored-by: Makram * make modgraph * Add changeset * Fix test with new TxMgr constructor --------- Co-authored-by: Cedric Cordenier Co-authored-by: Matthew Pendrey Co-authored-by: Makram --- .changeset/eight-radios-hear.md | 5 + .mockery.yaml | 4 + LICENSE | 2 +- .../ccip/ccip_integration_tests/.gitignore | 1 + .../ccipreader/ccipreader_test.go | 411 +++++++ .../chainreader/Makefile | 12 + .../chainreader/chainreader_test.go | 273 +++++ .../chainreader/mycontract.go | 519 ++++++++ .../chainreader/mycontract.sol | 31 + .../ccip/ccip_integration_tests/helpers.go | 938 +++++++++++++++ .../ccip_integration_tests/home_chain_test.go | 103 ++ .../integrationhelpers/integration_helpers.go | 304 +++++ .../ccip_integration_tests/ocr3_node_test.go | 281 +++++ .../ccip_integration_tests/ocr_node_helper.go | 316 +++++ .../ccip_integration_tests/ping_pong_test.go | 95 ++ core/capabilities/ccip/ccipevm/commitcodec.go | 138 +++ .../ccip/ccipevm/commitcodec_test.go | 135 +++ .../capabilities/ccip/ccipevm/executecodec.go | 181 +++ .../ccip/ccipevm/executecodec_test.go | 174 +++ core/capabilities/ccip/ccipevm/helpers.go | 33 + .../capabilities/ccip/ccipevm/helpers_test.go | 41 + core/capabilities/ccip/ccipevm/msghasher.go | 127 ++ .../ccip/ccipevm/msghasher_test.go | 189 +++ core/capabilities/ccip/common/common.go | 23 + core/capabilities/ccip/common/common_test.go | 51 + .../ccip/configs/evm/chain_writer.go | 75 ++ .../ccip/configs/evm/contract_reader.go | 219 ++++ core/capabilities/ccip/delegate.go | 321 +++++ core/capabilities/ccip/delegate_test.go | 1 + core/capabilities/ccip/launcher/README.md | 69 ++ core/capabilities/ccip/launcher/bluegreen.go | 178 +++ .../ccip/launcher/bluegreen_test.go | 1043 +++++++++++++++++ .../launcher/ccip_capability_launcher.png | Bin 0 -> 253433 bytes .../launcher/ccip_config_state_machine.png | Bin 0 -> 96958 bytes core/capabilities/ccip/launcher/diff.go | 141 +++ core/capabilities/ccip/launcher/diff_test.go | 352 ++++++ .../ccip/launcher/integration_test.go | 120 ++ core/capabilities/ccip/launcher/launcher.go | 432 +++++++ .../ccip/launcher/launcher_test.go | 472 ++++++++ .../ccip/launcher/test_helpers.go | 56 + .../ccip/ocrimpls/config_digester.go | 23 + .../ccip/ocrimpls/config_tracker.go | 77 ++ .../ccip/ocrimpls/contract_transmitter.go | 188 +++ .../ocrimpls/contract_transmitter_test.go | 691 +++++++++++ core/capabilities/ccip/ocrimpls/keyring.go | 61 + .../ccip/oraclecreator/inprocess.go | 371 ++++++ .../ccip/oraclecreator/inprocess_test.go | 239 ++++ .../ccip/types/mocks/ccip_oracle.go | 122 ++ .../ccip/types/mocks/home_chain_reader.go | 129 ++ .../ccip/types/mocks/oracle_creator.go | 152 +++ core/capabilities/ccip/types/types.go | 46 + core/capabilities/ccip/validate/validate.go | 94 ++ .../ccip/validate/validate_test.go | 58 + core/capabilities/launcher.go | 56 +- core/capabilities/launcher_test.go | 29 +- core/capabilities/registry.go | 10 +- .../evm/client/simulated_backend_client.go | 13 +- core/scripts/go.mod | 5 +- core/scripts/go.sum | 10 +- core/services/chainlink/application.go | 87 +- core/services/job/models.go | 50 + core/services/job/orm.go | 64 +- core/services/pipeline/common.go | 1 + .../services/registrysyncer/local_registry.go | 19 +- core/services/registrysyncer/syncer.go | 57 +- core/services/registrysyncer/syncer_test.go | 44 +- core/services/synchronization/common.go | 4 + core/services/workflows/engine_test.go | 26 +- core/web/presenters/job.go | 23 + core/web/presenters/job_test.go | 98 +- go.md | 4 + go.mod | 5 +- go.sum | 10 +- integration-tests/go.mod | 5 +- integration-tests/go.sum | 10 +- integration-tests/load/go.mod | 5 +- integration-tests/load/go.sum | 10 +- 77 files changed, 10565 insertions(+), 197 deletions(-) create mode 100644 .changeset/eight-radios-hear.md create mode 100644 core/capabilities/ccip/ccip_integration_tests/.gitignore create mode 100644 core/capabilities/ccip/ccip_integration_tests/ccipreader/ccipreader_test.go create mode 100644 core/capabilities/ccip/ccip_integration_tests/chainreader/Makefile create mode 100644 core/capabilities/ccip/ccip_integration_tests/chainreader/chainreader_test.go create mode 100644 core/capabilities/ccip/ccip_integration_tests/chainreader/mycontract.go create mode 100644 core/capabilities/ccip/ccip_integration_tests/chainreader/mycontract.sol create mode 100644 core/capabilities/ccip/ccip_integration_tests/helpers.go create mode 100644 core/capabilities/ccip/ccip_integration_tests/home_chain_test.go create mode 100644 core/capabilities/ccip/ccip_integration_tests/integrationhelpers/integration_helpers.go create mode 100644 core/capabilities/ccip/ccip_integration_tests/ocr3_node_test.go create mode 100644 core/capabilities/ccip/ccip_integration_tests/ocr_node_helper.go create mode 100644 core/capabilities/ccip/ccip_integration_tests/ping_pong_test.go create mode 100644 core/capabilities/ccip/ccipevm/commitcodec.go create mode 100644 core/capabilities/ccip/ccipevm/commitcodec_test.go create mode 100644 core/capabilities/ccip/ccipevm/executecodec.go create mode 100644 core/capabilities/ccip/ccipevm/executecodec_test.go create mode 100644 core/capabilities/ccip/ccipevm/helpers.go create mode 100644 core/capabilities/ccip/ccipevm/helpers_test.go create mode 100644 core/capabilities/ccip/ccipevm/msghasher.go create mode 100644 core/capabilities/ccip/ccipevm/msghasher_test.go create mode 100644 core/capabilities/ccip/common/common.go create mode 100644 core/capabilities/ccip/common/common_test.go create mode 100644 core/capabilities/ccip/configs/evm/chain_writer.go create mode 100644 core/capabilities/ccip/configs/evm/contract_reader.go create mode 100644 core/capabilities/ccip/delegate.go create mode 100644 core/capabilities/ccip/delegate_test.go create mode 100644 core/capabilities/ccip/launcher/README.md create mode 100644 core/capabilities/ccip/launcher/bluegreen.go create mode 100644 core/capabilities/ccip/launcher/bluegreen_test.go create mode 100644 core/capabilities/ccip/launcher/ccip_capability_launcher.png create mode 100644 core/capabilities/ccip/launcher/ccip_config_state_machine.png create mode 100644 core/capabilities/ccip/launcher/diff.go create mode 100644 core/capabilities/ccip/launcher/diff_test.go create mode 100644 core/capabilities/ccip/launcher/integration_test.go create mode 100644 core/capabilities/ccip/launcher/launcher.go create mode 100644 core/capabilities/ccip/launcher/launcher_test.go create mode 100644 core/capabilities/ccip/launcher/test_helpers.go create mode 100644 core/capabilities/ccip/ocrimpls/config_digester.go create mode 100644 core/capabilities/ccip/ocrimpls/config_tracker.go create mode 100644 core/capabilities/ccip/ocrimpls/contract_transmitter.go create mode 100644 core/capabilities/ccip/ocrimpls/contract_transmitter_test.go create mode 100644 core/capabilities/ccip/ocrimpls/keyring.go create mode 100644 core/capabilities/ccip/oraclecreator/inprocess.go create mode 100644 core/capabilities/ccip/oraclecreator/inprocess_test.go create mode 100644 core/capabilities/ccip/types/mocks/ccip_oracle.go create mode 100644 core/capabilities/ccip/types/mocks/home_chain_reader.go create mode 100644 core/capabilities/ccip/types/mocks/oracle_creator.go create mode 100644 core/capabilities/ccip/types/types.go create mode 100644 core/capabilities/ccip/validate/validate.go create mode 100644 core/capabilities/ccip/validate/validate_test.go diff --git a/.changeset/eight-radios-hear.md b/.changeset/eight-radios-hear.md new file mode 100644 index 00000000000..b422f378326 --- /dev/null +++ b/.changeset/eight-radios-hear.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +#added merging core/capabilities/ccip from https://github.com/smartcontractkit/ccip diff --git a/.mockery.yaml b/.mockery.yaml index 8fab61a5b9d..abb3105b136 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -43,6 +43,10 @@ packages: github.com/smartcontractkit/chainlink/v2/core/bridges: interfaces: ORM: + github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types: + interfaces: + CCIPOracle: + OracleCreator: github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/types: interfaces: Dispatcher: diff --git a/LICENSE b/LICENSE index 4a10bfc38b0..3af9faa6c6f 100644 --- a/LICENSE +++ b/LICENSE @@ -24,7 +24,7 @@ THE SOFTWARE. *All content residing under (1) “/contracts/src/v0.8/ccip”; (2) -“/core/gethwrappers/ccip”; (3) “/core/services/ocr2/plugins/ccip” are licensed +“/core/gethwrappers/ccip”; (3) “/core/services/ocr2/plugins/ccip”; (4) "/core/capabilities/ccip" are licensed under “Business Source License 1.1” with a Change Date of May 23, 2027 and Change License to “MIT License” diff --git a/core/capabilities/ccip/ccip_integration_tests/.gitignore b/core/capabilities/ccip/ccip_integration_tests/.gitignore new file mode 100644 index 00000000000..567609b1234 --- /dev/null +++ b/core/capabilities/ccip/ccip_integration_tests/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/core/capabilities/ccip/ccip_integration_tests/ccipreader/ccipreader_test.go b/core/capabilities/ccip/ccip_integration_tests/ccipreader/ccipreader_test.go new file mode 100644 index 00000000000..66c47f4741f --- /dev/null +++ b/core/capabilities/ccip/ccip_integration_tests/ccipreader/ccipreader_test.go @@ -0,0 +1,411 @@ +package ccipreader + +import ( + "context" + "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/crypto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + "golang.org/x/exp/maps" + + "github.com/smartcontractkit/chainlink-common/pkg/types" + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + + "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/chains/evm/utils" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/ccip_reader_tester" + "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/relay/evm" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" + + "github.com/smartcontractkit/chainlink-ccip/pkg/consts" + "github.com/smartcontractkit/chainlink-ccip/pkg/contractreader" + ccipreaderpkg "github.com/smartcontractkit/chainlink-ccip/pkg/reader" + "github.com/smartcontractkit/chainlink-ccip/plugintypes" +) + +const ( + chainS1 = cciptypes.ChainSelector(1) + chainS2 = cciptypes.ChainSelector(2) + chainS3 = cciptypes.ChainSelector(3) + chainD = cciptypes.ChainSelector(4) +) + +func TestCCIPReader_CommitReportsGTETimestamp(t *testing.T) { + ctx := testutils.Context(t) + + cfg := evmtypes.ChainReaderConfig{ + Contracts: map[string]evmtypes.ChainContractReader{ + consts.ContractNameOffRamp: { + ContractPollingFilter: evmtypes.ContractPollingFilter{ + GenericEventNames: []string{consts.EventNameCommitReportAccepted}, + }, + ContractABI: ccip_reader_tester.CCIPReaderTesterABI, + Configs: map[string]*evmtypes.ChainReaderDefinition{ + consts.EventNameCommitReportAccepted: { + ChainSpecificName: consts.EventNameCommitReportAccepted, + ReadType: evmtypes.Event, + }, + }, + }, + }, + } + + s := testSetup(ctx, t, chainD, chainD, nil, cfg) + + tokenA := common.HexToAddress("123") + const numReports = 5 + + for i := uint8(0); i < numReports; i++ { + _, err := s.contract.EmitCommitReportAccepted(s.auth, ccip_reader_tester.EVM2EVMMultiOffRampCommitReport{ + PriceUpdates: ccip_reader_tester.InternalPriceUpdates{ + TokenPriceUpdates: []ccip_reader_tester.InternalTokenPriceUpdate{ + { + SourceToken: tokenA, + UsdPerToken: big.NewInt(1000), + }, + }, + GasPriceUpdates: []ccip_reader_tester.InternalGasPriceUpdate{ + { + DestChainSelector: uint64(chainD), + UsdPerUnitGas: big.NewInt(90), + }, + }, + }, + MerkleRoots: []ccip_reader_tester.EVM2EVMMultiOffRampMerkleRoot{ + { + SourceChainSelector: uint64(chainS1), + Interval: ccip_reader_tester.EVM2EVMMultiOffRampInterval{ + Min: 10, + Max: 20, + }, + MerkleRoot: [32]byte{i + 1}, + }, + }, + }) + assert.NoError(t, err) + s.sb.Commit() + } + + var reports []plugintypes.CommitPluginReportWithMeta + var err error + require.Eventually(t, func() bool { + reports, err = s.reader.CommitReportsGTETimestamp( + ctx, + chainD, + time.Unix(30, 0), // Skips first report, simulated backend report timestamps are [20, 30, 40, ...] + 10, + ) + require.NoError(t, err) + return len(reports) == numReports-1 + }, testutils.WaitTimeout(t), 50*time.Millisecond) + + assert.Len(t, reports[0].Report.MerkleRoots, 1) + assert.Equal(t, chainS1, reports[0].Report.MerkleRoots[0].ChainSel) + assert.Equal(t, cciptypes.SeqNum(10), reports[0].Report.MerkleRoots[0].SeqNumsRange.Start()) + assert.Equal(t, cciptypes.SeqNum(20), reports[0].Report.MerkleRoots[0].SeqNumsRange.End()) + assert.Equal(t, "0x0200000000000000000000000000000000000000000000000000000000000000", + reports[0].Report.MerkleRoots[0].MerkleRoot.String()) + + assert.Equal(t, tokenA.String(), string(reports[0].Report.PriceUpdates.TokenPriceUpdates[0].TokenID)) + assert.Equal(t, uint64(1000), reports[0].Report.PriceUpdates.TokenPriceUpdates[0].Price.Uint64()) + + assert.Equal(t, chainD, reports[0].Report.PriceUpdates.GasPriceUpdates[0].ChainSel) + assert.Equal(t, uint64(90), reports[0].Report.PriceUpdates.GasPriceUpdates[0].GasPrice.Uint64()) +} + +func TestCCIPReader_ExecutedMessageRanges(t *testing.T) { + ctx := testutils.Context(t) + cfg := evmtypes.ChainReaderConfig{ + Contracts: map[string]evmtypes.ChainContractReader{ + consts.ContractNameOffRamp: { + ContractPollingFilter: evmtypes.ContractPollingFilter{ + GenericEventNames: []string{consts.EventNameExecutionStateChanged}, + }, + ContractABI: ccip_reader_tester.CCIPReaderTesterABI, + Configs: map[string]*evmtypes.ChainReaderDefinition{ + consts.EventNameExecutionStateChanged: { + ChainSpecificName: consts.EventNameExecutionStateChanged, + ReadType: evmtypes.Event, + }, + }, + }, + }, + } + + s := testSetup(ctx, t, chainD, chainD, nil, cfg) + + _, err := s.contract.EmitExecutionStateChanged( + s.auth, + uint64(chainS1), + 14, + cciptypes.Bytes32{1, 0, 0, 1}, + 1, + []byte{1, 2, 3, 4}, + ) + assert.NoError(t, err) + s.sb.Commit() + + _, err = s.contract.EmitExecutionStateChanged( + s.auth, + uint64(chainS1), + 15, + cciptypes.Bytes32{1, 0, 0, 2}, + 1, + []byte{1, 2, 3, 4, 5}, + ) + assert.NoError(t, err) + s.sb.Commit() + + // Need to replay as sometimes the logs are not picked up by the log poller (?) + // Maybe another situation where chain reader doesn't register filters as expected. + require.NoError(t, s.lp.Replay(ctx, 1)) + + var executedRanges []cciptypes.SeqNumRange + require.Eventually(t, func() bool { + executedRanges, err = s.reader.ExecutedMessageRanges( + ctx, + chainS1, + chainD, + cciptypes.NewSeqNumRange(14, 15), + ) + require.NoError(t, err) + return len(executedRanges) == 2 + }, testutils.WaitTimeout(t), 50*time.Millisecond) + + assert.Equal(t, cciptypes.SeqNum(14), executedRanges[0].Start()) + assert.Equal(t, cciptypes.SeqNum(14), executedRanges[0].End()) + + assert.Equal(t, cciptypes.SeqNum(15), executedRanges[1].Start()) + assert.Equal(t, cciptypes.SeqNum(15), executedRanges[1].End()) +} + +func TestCCIPReader_MsgsBetweenSeqNums(t *testing.T) { + ctx := testutils.Context(t) + + cfg := evmtypes.ChainReaderConfig{ + Contracts: map[string]evmtypes.ChainContractReader{ + consts.ContractNameOnRamp: { + ContractPollingFilter: evmtypes.ContractPollingFilter{ + GenericEventNames: []string{consts.EventNameCCIPSendRequested}, + }, + ContractABI: ccip_reader_tester.CCIPReaderTesterABI, + Configs: map[string]*evmtypes.ChainReaderDefinition{ + consts.EventNameCCIPSendRequested: { + ChainSpecificName: consts.EventNameCCIPSendRequested, + ReadType: evmtypes.Event, + }, + }, + }, + }, + } + + s := testSetup(ctx, t, chainS1, chainD, nil, cfg) + + _, err := s.contract.EmitCCIPSendRequested(s.auth, uint64(chainD), ccip_reader_tester.InternalEVM2AnyRampMessage{ + Header: ccip_reader_tester.InternalRampMessageHeader{ + MessageId: [32]byte{1, 0, 0, 0, 0}, + SourceChainSelector: uint64(chainS1), + DestChainSelector: uint64(chainD), + SequenceNumber: 10, + }, + Sender: utils.RandomAddress(), + Data: make([]byte, 0), + Receiver: utils.RandomAddress().Bytes(), + ExtraArgs: make([]byte, 0), + FeeToken: utils.RandomAddress(), + FeeTokenAmount: big.NewInt(0), + TokenAmounts: make([]ccip_reader_tester.InternalRampTokenAmount, 0), + }) + assert.NoError(t, err) + + _, err = s.contract.EmitCCIPSendRequested(s.auth, uint64(chainD), ccip_reader_tester.InternalEVM2AnyRampMessage{ + Header: ccip_reader_tester.InternalRampMessageHeader{ + MessageId: [32]byte{1, 0, 0, 0, 1}, + SourceChainSelector: uint64(chainS1), + DestChainSelector: uint64(chainD), + SequenceNumber: 15, + }, + Sender: utils.RandomAddress(), + Data: make([]byte, 0), + Receiver: utils.RandomAddress().Bytes(), + ExtraArgs: make([]byte, 0), + FeeToken: utils.RandomAddress(), + FeeTokenAmount: big.NewInt(0), + TokenAmounts: make([]ccip_reader_tester.InternalRampTokenAmount, 0), + }) + assert.NoError(t, err) + + s.sb.Commit() + + var msgs []cciptypes.Message + require.Eventually(t, func() bool { + msgs, err = s.reader.MsgsBetweenSeqNums( + ctx, + chainS1, + cciptypes.NewSeqNumRange(5, 20), + ) + require.NoError(t, err) + return len(msgs) == 2 + }, 10*time.Second, 100*time.Millisecond) + + require.Len(t, msgs, 2) + require.Equal(t, cciptypes.SeqNum(10), msgs[0].Header.SequenceNumber) + require.Equal(t, cciptypes.SeqNum(15), msgs[1].Header.SequenceNumber) + for _, msg := range msgs { + require.Equal(t, chainS1, msg.Header.SourceChainSelector) + require.Equal(t, chainD, msg.Header.DestChainSelector) + } +} + +func TestCCIPReader_NextSeqNum(t *testing.T) { + ctx := testutils.Context(t) + + onChainSeqNums := map[cciptypes.ChainSelector]cciptypes.SeqNum{ + chainS1: 10, + chainS2: 20, + chainS3: 30, + } + + cfg := evmtypes.ChainReaderConfig{ + Contracts: map[string]evmtypes.ChainContractReader{ + consts.ContractNameOffRamp: { + ContractABI: ccip_reader_tester.CCIPReaderTesterABI, + Configs: map[string]*evmtypes.ChainReaderDefinition{ + consts.MethodNameGetSourceChainConfig: { + ChainSpecificName: "getSourceChainConfig", + ReadType: evmtypes.Method, + }, + }, + }, + }, + } + + s := testSetup(ctx, t, chainD, chainD, onChainSeqNums, cfg) + + seqNums, err := s.reader.NextSeqNum(ctx, []cciptypes.ChainSelector{chainS1, chainS2, chainS3}) + assert.NoError(t, err) + assert.Len(t, seqNums, 3) + assert.Equal(t, cciptypes.SeqNum(10), seqNums[0]) + assert.Equal(t, cciptypes.SeqNum(20), seqNums[1]) + assert.Equal(t, cciptypes.SeqNum(30), seqNums[2]) +} + +func testSetup(ctx context.Context, t *testing.T, readerChain, destChain cciptypes.ChainSelector, onChainSeqNums map[cciptypes.ChainSelector]cciptypes.SeqNum, cfg evmtypes.ChainReaderConfig) *testSetupData { + const chainID = 1337 + + // Generate a new key pair for the simulated account + privateKey, err := crypto.GenerateKey() + assert.NoError(t, err) + // Set up the genesis account with balance + blnc, ok := big.NewInt(0).SetString("999999999999999999999999999999999999", 10) + assert.True(t, ok) + alloc := map[common.Address]core.GenesisAccount{crypto.PubkeyToAddress(privateKey.PublicKey): {Balance: blnc}} + simulatedBackend := backends.NewSimulatedBackend(alloc, 0) + // Create a transactor + + auth, err := bind.NewKeyedTransactorWithChainID(privateKey, big.NewInt(chainID)) + assert.NoError(t, err) + auth.GasLimit = uint64(0) + + // Deploy the contract + address, _, _, err := ccip_reader_tester.DeployCCIPReaderTester(auth, simulatedBackend) + assert.NoError(t, err) + simulatedBackend.Commit() + + // Setup contract client + contract, err := ccip_reader_tester.NewCCIPReaderTester(address, simulatedBackend) + assert.NoError(t, err) + + lggr := logger.TestLogger(t) + lggr.SetLogLevel(zapcore.ErrorLevel) + db := pgtest.NewSqlxDB(t) + lpOpts := logpoller.Opts{ + PollPeriod: time.Millisecond, + FinalityDepth: 0, + BackfillBatchSize: 10, + RpcBatchSize: 10, + KeepFinalizedBlocksDepth: 100000, + } + cl := client.NewSimulatedBackendClient(t, simulatedBackend, big.NewInt(0).SetUint64(uint64(readerChain))) + headTracker := headtracker.NewSimulatedHeadTracker(cl, lpOpts.UseFinalityTag, lpOpts.FinalityDepth) + lp := logpoller.NewLogPoller(logpoller.NewORM(big.NewInt(0).SetUint64(uint64(readerChain)), db, lggr), + cl, + lggr, + headTracker, + lpOpts, + ) + assert.NoError(t, lp.Start(ctx)) + + for sourceChain, seqNum := range onChainSeqNums { + _, err1 := contract.SetSourceChainConfig(auth, uint64(sourceChain), ccip_reader_tester.EVM2EVMMultiOffRampSourceChainConfig{ + IsEnabled: true, + MinSeqNr: uint64(seqNum), + }) + assert.NoError(t, err1) + simulatedBackend.Commit() + scc, err1 := contract.GetSourceChainConfig(&bind.CallOpts{Context: ctx}, uint64(sourceChain)) + assert.NoError(t, err1) + assert.Equal(t, seqNum, cciptypes.SeqNum(scc.MinSeqNr)) + } + + contractNames := maps.Keys(cfg.Contracts) + assert.Len(t, contractNames, 1, "test setup assumes there is only one contract") + + cr, err := evm.NewChainReaderService(ctx, lggr, lp, headTracker, cl, cfg) + require.NoError(t, err) + + extendedCr := contractreader.NewExtendedContractReader(cr) + err = extendedCr.Bind(ctx, []types.BoundContract{ + { + Address: address.String(), + Name: contractNames[0], + }, + }) + require.NoError(t, err) + + err = cr.Start(ctx) + require.NoError(t, err) + + contractReaders := map[cciptypes.ChainSelector]contractreader.Extended{readerChain: extendedCr} + contractWriters := make(map[cciptypes.ChainSelector]types.ChainWriter) + reader := ccipreaderpkg.NewCCIPReaderWithExtendedContractReaders(lggr, contractReaders, contractWriters, destChain) + + t.Cleanup(func() { + require.NoError(t, cr.Close()) + require.NoError(t, lp.Close()) + require.NoError(t, db.Close()) + }) + + return &testSetupData{ + contractAddr: address, + contract: contract, + sb: simulatedBackend, + auth: auth, + lp: lp, + cl: cl, + reader: reader, + } +} + +type testSetupData struct { + contractAddr common.Address + contract *ccip_reader_tester.CCIPReaderTester + sb *backends.SimulatedBackend + auth *bind.TransactOpts + lp logpoller.LogPoller + cl client.Client + reader ccipreaderpkg.CCIPReader +} diff --git a/core/capabilities/ccip/ccip_integration_tests/chainreader/Makefile b/core/capabilities/ccip/ccip_integration_tests/chainreader/Makefile new file mode 100644 index 00000000000..e9c88564e69 --- /dev/null +++ b/core/capabilities/ccip/ccip_integration_tests/chainreader/Makefile @@ -0,0 +1,12 @@ + +# IMPORTANT: If you encounter any issues try using solc 0.8.18 and abigen 1.14.5 + +.PHONY: build +build: + rm -rf build/ + solc --evm-version paris --abi --bin mycontract.sol -o build + abigen --abi build/mycontract_sol_SimpleContract.abi --bin build/mycontract_sol_SimpleContract.bin --pkg=chainreader --out=mycontract.go + +.PHONY: test +test: build + go test -v --tags "playground" ./... diff --git a/core/capabilities/ccip/ccip_integration_tests/chainreader/chainreader_test.go b/core/capabilities/ccip/ccip_integration_tests/chainreader/chainreader_test.go new file mode 100644 index 00000000000..52a3de0dae9 --- /dev/null +++ b/core/capabilities/ccip/ccip_integration_tests/chainreader/chainreader_test.go @@ -0,0 +1,273 @@ +//go:build playground +// +build playground + +package chainreader + +import ( + "context" + _ "embed" + "math/big" + "strconv" + "sync" + "testing" + "time" + + "github.com/ethereum/go-ethereum" + "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/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/assert" + + "github.com/smartcontractkit/chainlink-common/pkg/codec" + types2 "github.com/smartcontractkit/chainlink-common/pkg/types" + query2 "github.com/smartcontractkit/chainlink-common/pkg/types/query" + "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/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" + logger2 "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" +) + +const chainID = 1337 + +type testSetupData struct { + contractAddr common.Address + contract *Chainreader + sb *backends.SimulatedBackend + auth *bind.TransactOpts +} + +func TestChainReader(t *testing.T) { + ctx := testutils.Context(t) + lggr := logger2.NullLogger + d := testSetup(t, ctx) + + db := pgtest.NewSqlxDB(t) + lpOpts := logpoller.Opts{ + PollPeriod: time.Millisecond, + FinalityDepth: 0, + BackfillBatchSize: 10, + RpcBatchSize: 10, + KeepFinalizedBlocksDepth: 100000, + } + cl := client.NewSimulatedBackendClient(t, d.sb, big.NewInt(chainID)) + headTracker := headtracker.NewSimulatedHeadTracker(cl, lpOpts.UseFinalityTag, lpOpts.FinalityDepth) + lp := logpoller.NewLogPoller(logpoller.NewORM(big.NewInt(chainID), db, lggr), + cl, + lggr, + headTracker, + lpOpts, + ) + assert.NoError(t, lp.Start(ctx)) + + const ( + ContractNameAlias = "myCoolContract" + + FnAliasGetCount = "myCoolFunction" + FnGetCount = "getEventCount" + + FnAliasGetNumbers = "GetNumbers" + FnGetNumbers = "getNumbers" + + FnAliasGetPerson = "GetPerson" + FnGetPerson = "getPerson" + + EventNameAlias = "myCoolEvent" + EventName = "SimpleEvent" + ) + + // Initialize chainReader + cfg := evmtypes.ChainReaderConfig{ + Contracts: map[string]evmtypes.ChainContractReader{ + ContractNameAlias: { + ContractPollingFilter: evmtypes.ContractPollingFilter{ + GenericEventNames: []string{EventNameAlias}, + }, + ContractABI: ChainreaderMetaData.ABI, + Configs: map[string]*evmtypes.ChainReaderDefinition{ + EventNameAlias: { + ChainSpecificName: EventName, + ReadType: evmtypes.Event, + ConfidenceConfirmations: map[string]int{"0.0": 0, "1.0": 0}, + }, + FnAliasGetCount: { + ChainSpecificName: FnGetCount, + }, + FnAliasGetNumbers: { + ChainSpecificName: FnGetNumbers, + OutputModifications: codec.ModifiersConfig{}, + }, + FnAliasGetPerson: { + ChainSpecificName: FnGetPerson, + OutputModifications: codec.ModifiersConfig{ + &codec.RenameModifierConfig{ + Fields: map[string]string{"Name": "NameField"}, // solidity name -> go struct name + }, + }, + }, + }, + }, + }, + } + + cr, err := evm.NewChainReaderService(ctx, lggr, lp, cl, cfg) + assert.NoError(t, err) + err = cr.Bind(ctx, []types2.BoundContract{ + { + Address: d.contractAddr.String(), + Name: ContractNameAlias, + Pending: false, + }, + }) + assert.NoError(t, err) + + err = cr.Start(ctx) + assert.NoError(t, err) + for { + if err := cr.Ready(); err == nil { + break + } + } + + emitEvents(t, d, ctx) // Calls the contract to emit events + + // (hack) Sometimes LP logs are missing, commit several times and wait few seconds to make it work. + for i := 0; i < 100; i++ { + d.sb.Commit() + } + time.Sleep(5 * time.Second) + + t.Run("simple contract read", func(t *testing.T) { + var cnt big.Int + err = cr.GetLatestValue(ctx, ContractNameAlias, FnAliasGetCount, map[string]interface{}{}, &cnt) + assert.NoError(t, err) + assert.Equal(t, int64(10), cnt.Int64()) + }) + + t.Run("read array", func(t *testing.T) { + var nums []big.Int + err = cr.GetLatestValue(ctx, ContractNameAlias, FnAliasGetNumbers, map[string]interface{}{}, &nums) + assert.NoError(t, err) + assert.Len(t, nums, 10) + for i := 1; i <= 10; i++ { + assert.Equal(t, int64(i), nums[i-1].Int64()) + } + }) + + t.Run("read struct", func(t *testing.T) { + person := struct { + NameField string + Age *big.Int // WARN: specifying a wrong data type e.g. int instead of *big.Int fails silently with a default value of 0 + }{} + err = cr.GetLatestValue(ctx, ContractNameAlias, FnAliasGetPerson, map[string]interface{}{}, &person) + assert.Equal(t, "Dim", person.NameField) + assert.Equal(t, int64(18), person.Age.Int64()) + }) + + t.Run("read events", func(t *testing.T) { + var myDataType *big.Int + seq, err := cr.QueryKey( + ctx, + ContractNameAlias, + query2.KeyFilter{ + Key: EventNameAlias, + Expressions: []query2.Expression{}, + }, + query2.LimitAndSort{}, + myDataType, + ) + assert.NoError(t, err) + assert.Equal(t, 10, len(seq), "expected 10 events from chain reader") + for _, v := range seq { + // TODO: for some reason log poller does not populate event data + blockNum, err := strconv.ParseUint(v.Identifier, 10, 64) + assert.NoError(t, err) + assert.Positive(t, blockNum) + t.Logf("(chain reader) got event: (data=%v) (hash=%x)", v.Data, v.Hash) + } + }) +} + +func testSetup(t *testing.T, ctx context.Context) *testSetupData { + // Generate a new key pair for the simulated account + privateKey, err := crypto.GenerateKey() + assert.NoError(t, err) + // Set up the genesis account with balance + blnc, ok := big.NewInt(0).SetString("999999999999999999999999999999999999", 10) + assert.True(t, ok) + alloc := map[common.Address]core.GenesisAccount{crypto.PubkeyToAddress(privateKey.PublicKey): {Balance: blnc}} + simulatedBackend := backends.NewSimulatedBackend(alloc, 0) + // Create a transactor + + auth, err := bind.NewKeyedTransactorWithChainID(privateKey, big.NewInt(chainID)) + assert.NoError(t, err) + auth.GasLimit = uint64(0) + + // Deploy the contract + address, tx, _, err := DeployChainreader(auth, simulatedBackend) + assert.NoError(t, err) + simulatedBackend.Commit() + t.Logf("contract deployed: addr=%s tx=%s", address.Hex(), tx.Hash()) + + // Setup contract client + contract, err := NewChainreader(address, simulatedBackend) + assert.NoError(t, err) + + return &testSetupData{ + contractAddr: address, + contract: contract, + sb: simulatedBackend, + auth: auth, + } +} + +func emitEvents(t *testing.T, d *testSetupData, ctx context.Context) { + var wg sync.WaitGroup + wg.Add(2) + + // Start emitting events + go func() { + defer wg.Done() + for i := 0; i < 10; i++ { + _, err := d.contract.EmitEvent(d.auth) + assert.NoError(t, err) + d.sb.Commit() + } + }() + + // Listen events using go-ethereum lib + go func() { + query := ethereum.FilterQuery{ + FromBlock: big.NewInt(0), + Addresses: []common.Address{d.contractAddr}, + } + logs := make(chan types.Log) + sub, err := d.sb.SubscribeFilterLogs(ctx, query, logs) + assert.NoError(t, err) + + numLogs := 0 + defer wg.Done() + for { + // Wait for the events + select { + case err := <-sub.Err(): + assert.NoError(t, err, "got an unexpected error") + case vLog := <-logs: + assert.Equal(t, d.contractAddr, vLog.Address, "got an unexpected address") + t.Logf("(geth) got new log (cnt=%d) (data=%x) (topics=%s)", numLogs, vLog.Data, vLog.Topics) + numLogs++ + if numLogs == 10 { + return + } + } + } + }() + + wg.Wait() // wait for all the events to be consumed +} diff --git a/core/capabilities/ccip/ccip_integration_tests/chainreader/mycontract.go b/core/capabilities/ccip/ccip_integration_tests/chainreader/mycontract.go new file mode 100644 index 00000000000..c7d480eed46 --- /dev/null +++ b/core/capabilities/ccip/ccip_integration_tests/chainreader/mycontract.go @@ -0,0 +1,519 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package chainreader + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// SimpleContractPerson is an auto generated low-level Go binding around an user-defined struct. +type SimpleContractPerson struct { + Name string + Age *big.Int +} + +// ChainreaderMetaData contains all meta data concerning the Chainreader contract. +var ChainreaderMetaData = &bind.MetaData{ + ABI: "[{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"SimpleEvent\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"emitEvent\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"eventCount\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getEventCount\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getNumbers\",\"outputs\":[{\"internalType\":\"uint256[]\",\"name\":\"\",\"type\":\"uint256[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getPerson\",\"outputs\":[{\"components\":[{\"internalType\":\"string\",\"name\":\"name\",\"type\":\"string\"},{\"internalType\":\"uint256\",\"name\":\"age\",\"type\":\"uint256\"}],\"internalType\":\"structSimpleContract.Person\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"numbers\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]", + Bin: "0x608060405234801561001057600080fd5b506105a1806100206000396000f3fe608060405234801561001057600080fd5b50600436106100625760003560e01c806371be2e4a146100675780637b0cb8391461008557806389f915f61461008f5780638ec4dc95146100ad578063d39fa233146100cb578063d9e48f5c146100fb575b600080fd5b61006f610119565b60405161007c91906102ac565b60405180910390f35b61008d61011f565b005b61009761019c565b6040516100a49190610385565b60405180910390f35b6100b56101f4565b6040516100c29190610474565b60405180910390f35b6100e560048036038101906100e091906104c7565b61024c565b6040516100f291906102ac565b60405180910390f35b610103610270565b60405161011091906102ac565b60405180910390f35b60005481565b60008081548092919061013190610523565b9190505550600160005490806001815401808255809150506001900390600052602060002001600090919091909150557f12d199749b3f4c44df8d9386c63d725b7756ec47204f3aa0bf05ea832f89effb60005460405161019291906102ac565b60405180910390a1565b606060018054806020026020016040519081016040528092919081815260200182805480156101ea57602002820191906000526020600020905b8154815260200190600101908083116101d6575b5050505050905090565b6101fc610279565b60405180604001604052806040518060400160405280600381526020017f44696d000000000000000000000000000000000000000000000000000000000081525081526020016012815250905090565b6001818154811061025c57600080fd5b906000526020600020016000915090505481565b60008054905090565b604051806040016040528060608152602001600081525090565b6000819050919050565b6102a681610293565b82525050565b60006020820190506102c1600083018461029d565b92915050565b600081519050919050565b600082825260208201905092915050565b6000819050602082019050919050565b6102fc81610293565b82525050565b600061030e83836102f3565b60208301905092915050565b6000602082019050919050565b6000610332826102c7565b61033c81856102d2565b9350610347836102e3565b8060005b8381101561037857815161035f8882610302565b975061036a8361031a565b92505060018101905061034b565b5085935050505092915050565b6000602082019050818103600083015261039f8184610327565b905092915050565b600081519050919050565b600082825260208201905092915050565b60005b838110156103e15780820151818401526020810190506103c6565b60008484015250505050565b6000601f19601f8301169050919050565b6000610409826103a7565b61041381856103b2565b93506104238185602086016103c3565b61042c816103ed565b840191505092915050565b6000604083016000830151848203600086015261045482826103fe565b915050602083015161046960208601826102f3565b508091505092915050565b6000602082019050818103600083015261048e8184610437565b905092915050565b600080fd5b6104a481610293565b81146104af57600080fd5b50565b6000813590506104c18161049b565b92915050565b6000602082840312156104dd576104dc610496565b5b60006104eb848285016104b2565b91505092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600061052e82610293565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036105605761055f6104f4565b5b60018201905091905056fea2646970667358221220f7986dc9efbc0d9ef58e2925ffddc62ea13a6bab8b3a2c03ad2d85d50653129664736f6c63430008120033", +} + +// ChainreaderABI is the input ABI used to generate the binding from. +// Deprecated: Use ChainreaderMetaData.ABI instead. +var ChainreaderABI = ChainreaderMetaData.ABI + +// ChainreaderBin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use ChainreaderMetaData.Bin instead. +var ChainreaderBin = ChainreaderMetaData.Bin + +// DeployChainreader deploys a new Ethereum contract, binding an instance of Chainreader to it. +func DeployChainreader(auth *bind.TransactOpts, backend bind.ContractBackend) (common.Address, *types.Transaction, *Chainreader, error) { + parsed, err := ChainreaderMetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(ChainreaderBin), backend) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &Chainreader{ChainreaderCaller: ChainreaderCaller{contract: contract}, ChainreaderTransactor: ChainreaderTransactor{contract: contract}, ChainreaderFilterer: ChainreaderFilterer{contract: contract}}, nil +} + +// Chainreader is an auto generated Go binding around an Ethereum contract. +type Chainreader struct { + ChainreaderCaller // Read-only binding to the contract + ChainreaderTransactor // Write-only binding to the contract + ChainreaderFilterer // Log filterer for contract events +} + +// ChainreaderCaller is an auto generated read-only Go binding around an Ethereum contract. +type ChainreaderCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ChainreaderTransactor is an auto generated write-only Go binding around an Ethereum contract. +type ChainreaderTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ChainreaderFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type ChainreaderFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ChainreaderSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type ChainreaderSession struct { + Contract *Chainreader // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// ChainreaderCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type ChainreaderCallerSession struct { + Contract *ChainreaderCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// ChainreaderTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type ChainreaderTransactorSession struct { + Contract *ChainreaderTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// ChainreaderRaw is an auto generated low-level Go binding around an Ethereum contract. +type ChainreaderRaw struct { + Contract *Chainreader // Generic contract binding to access the raw methods on +} + +// ChainreaderCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type ChainreaderCallerRaw struct { + Contract *ChainreaderCaller // Generic read-only contract binding to access the raw methods on +} + +// ChainreaderTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type ChainreaderTransactorRaw struct { + Contract *ChainreaderTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewChainreader creates a new instance of Chainreader, bound to a specific deployed contract. +func NewChainreader(address common.Address, backend bind.ContractBackend) (*Chainreader, error) { + contract, err := bindChainreader(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &Chainreader{ChainreaderCaller: ChainreaderCaller{contract: contract}, ChainreaderTransactor: ChainreaderTransactor{contract: contract}, ChainreaderFilterer: ChainreaderFilterer{contract: contract}}, nil +} + +// NewChainreaderCaller creates a new read-only instance of Chainreader, bound to a specific deployed contract. +func NewChainreaderCaller(address common.Address, caller bind.ContractCaller) (*ChainreaderCaller, error) { + contract, err := bindChainreader(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &ChainreaderCaller{contract: contract}, nil +} + +// NewChainreaderTransactor creates a new write-only instance of Chainreader, bound to a specific deployed contract. +func NewChainreaderTransactor(address common.Address, transactor bind.ContractTransactor) (*ChainreaderTransactor, error) { + contract, err := bindChainreader(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &ChainreaderTransactor{contract: contract}, nil +} + +// NewChainreaderFilterer creates a new log filterer instance of Chainreader, bound to a specific deployed contract. +func NewChainreaderFilterer(address common.Address, filterer bind.ContractFilterer) (*ChainreaderFilterer, error) { + contract, err := bindChainreader(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &ChainreaderFilterer{contract: contract}, nil +} + +// bindChainreader binds a generic wrapper to an already deployed contract. +func bindChainreader(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := ChainreaderMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Chainreader *ChainreaderRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Chainreader.Contract.ChainreaderCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Chainreader *ChainreaderRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Chainreader.Contract.ChainreaderTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Chainreader *ChainreaderRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Chainreader.Contract.ChainreaderTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Chainreader *ChainreaderCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Chainreader.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Chainreader *ChainreaderTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Chainreader.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Chainreader *ChainreaderTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Chainreader.Contract.contract.Transact(opts, method, params...) +} + +// EventCount is a free data retrieval call binding the contract method 0x71be2e4a. +// +// Solidity: function eventCount() view returns(uint256) +func (_Chainreader *ChainreaderCaller) EventCount(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Chainreader.contract.Call(opts, &out, "eventCount") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// EventCount is a free data retrieval call binding the contract method 0x71be2e4a. +// +// Solidity: function eventCount() view returns(uint256) +func (_Chainreader *ChainreaderSession) EventCount() (*big.Int, error) { + return _Chainreader.Contract.EventCount(&_Chainreader.CallOpts) +} + +// EventCount is a free data retrieval call binding the contract method 0x71be2e4a. +// +// Solidity: function eventCount() view returns(uint256) +func (_Chainreader *ChainreaderCallerSession) EventCount() (*big.Int, error) { + return _Chainreader.Contract.EventCount(&_Chainreader.CallOpts) +} + +// GetEventCount is a free data retrieval call binding the contract method 0xd9e48f5c. +// +// Solidity: function getEventCount() view returns(uint256) +func (_Chainreader *ChainreaderCaller) GetEventCount(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Chainreader.contract.Call(opts, &out, "getEventCount") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// GetEventCount is a free data retrieval call binding the contract method 0xd9e48f5c. +// +// Solidity: function getEventCount() view returns(uint256) +func (_Chainreader *ChainreaderSession) GetEventCount() (*big.Int, error) { + return _Chainreader.Contract.GetEventCount(&_Chainreader.CallOpts) +} + +// GetEventCount is a free data retrieval call binding the contract method 0xd9e48f5c. +// +// Solidity: function getEventCount() view returns(uint256) +func (_Chainreader *ChainreaderCallerSession) GetEventCount() (*big.Int, error) { + return _Chainreader.Contract.GetEventCount(&_Chainreader.CallOpts) +} + +// GetNumbers is a free data retrieval call binding the contract method 0x89f915f6. +// +// Solidity: function getNumbers() view returns(uint256[]) +func (_Chainreader *ChainreaderCaller) GetNumbers(opts *bind.CallOpts) ([]*big.Int, error) { + var out []interface{} + err := _Chainreader.contract.Call(opts, &out, "getNumbers") + + if err != nil { + return *new([]*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new([]*big.Int)).(*[]*big.Int) + + return out0, err + +} + +// GetNumbers is a free data retrieval call binding the contract method 0x89f915f6. +// +// Solidity: function getNumbers() view returns(uint256[]) +func (_Chainreader *ChainreaderSession) GetNumbers() ([]*big.Int, error) { + return _Chainreader.Contract.GetNumbers(&_Chainreader.CallOpts) +} + +// GetNumbers is a free data retrieval call binding the contract method 0x89f915f6. +// +// Solidity: function getNumbers() view returns(uint256[]) +func (_Chainreader *ChainreaderCallerSession) GetNumbers() ([]*big.Int, error) { + return _Chainreader.Contract.GetNumbers(&_Chainreader.CallOpts) +} + +// GetPerson is a free data retrieval call binding the contract method 0x8ec4dc95. +// +// Solidity: function getPerson() pure returns((string,uint256)) +func (_Chainreader *ChainreaderCaller) GetPerson(opts *bind.CallOpts) (SimpleContractPerson, error) { + var out []interface{} + err := _Chainreader.contract.Call(opts, &out, "getPerson") + + if err != nil { + return *new(SimpleContractPerson), err + } + + out0 := *abi.ConvertType(out[0], new(SimpleContractPerson)).(*SimpleContractPerson) + + return out0, err + +} + +// GetPerson is a free data retrieval call binding the contract method 0x8ec4dc95. +// +// Solidity: function getPerson() pure returns((string,uint256)) +func (_Chainreader *ChainreaderSession) GetPerson() (SimpleContractPerson, error) { + return _Chainreader.Contract.GetPerson(&_Chainreader.CallOpts) +} + +// GetPerson is a free data retrieval call binding the contract method 0x8ec4dc95. +// +// Solidity: function getPerson() pure returns((string,uint256)) +func (_Chainreader *ChainreaderCallerSession) GetPerson() (SimpleContractPerson, error) { + return _Chainreader.Contract.GetPerson(&_Chainreader.CallOpts) +} + +// Numbers is a free data retrieval call binding the contract method 0xd39fa233. +// +// Solidity: function numbers(uint256 ) view returns(uint256) +func (_Chainreader *ChainreaderCaller) Numbers(opts *bind.CallOpts, arg0 *big.Int) (*big.Int, error) { + var out []interface{} + err := _Chainreader.contract.Call(opts, &out, "numbers", arg0) + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// Numbers is a free data retrieval call binding the contract method 0xd39fa233. +// +// Solidity: function numbers(uint256 ) view returns(uint256) +func (_Chainreader *ChainreaderSession) Numbers(arg0 *big.Int) (*big.Int, error) { + return _Chainreader.Contract.Numbers(&_Chainreader.CallOpts, arg0) +} + +// Numbers is a free data retrieval call binding the contract method 0xd39fa233. +// +// Solidity: function numbers(uint256 ) view returns(uint256) +func (_Chainreader *ChainreaderCallerSession) Numbers(arg0 *big.Int) (*big.Int, error) { + return _Chainreader.Contract.Numbers(&_Chainreader.CallOpts, arg0) +} + +// EmitEvent is a paid mutator transaction binding the contract method 0x7b0cb839. +// +// Solidity: function emitEvent() returns() +func (_Chainreader *ChainreaderTransactor) EmitEvent(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Chainreader.contract.Transact(opts, "emitEvent") +} + +// EmitEvent is a paid mutator transaction binding the contract method 0x7b0cb839. +// +// Solidity: function emitEvent() returns() +func (_Chainreader *ChainreaderSession) EmitEvent() (*types.Transaction, error) { + return _Chainreader.Contract.EmitEvent(&_Chainreader.TransactOpts) +} + +// EmitEvent is a paid mutator transaction binding the contract method 0x7b0cb839. +// +// Solidity: function emitEvent() returns() +func (_Chainreader *ChainreaderTransactorSession) EmitEvent() (*types.Transaction, error) { + return _Chainreader.Contract.EmitEvent(&_Chainreader.TransactOpts) +} + +// ChainreaderSimpleEventIterator is returned from FilterSimpleEvent and is used to iterate over the raw logs and unpacked data for SimpleEvent events raised by the Chainreader contract. +type ChainreaderSimpleEventIterator struct { + Event *ChainreaderSimpleEvent // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *ChainreaderSimpleEventIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(ChainreaderSimpleEvent) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(ChainreaderSimpleEvent) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *ChainreaderSimpleEventIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *ChainreaderSimpleEventIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// ChainreaderSimpleEvent represents a SimpleEvent event raised by the Chainreader contract. +type ChainreaderSimpleEvent struct { + Value *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterSimpleEvent is a free log retrieval operation binding the contract event 0x12d199749b3f4c44df8d9386c63d725b7756ec47204f3aa0bf05ea832f89effb. +// +// Solidity: event SimpleEvent(uint256 value) +func (_Chainreader *ChainreaderFilterer) FilterSimpleEvent(opts *bind.FilterOpts) (*ChainreaderSimpleEventIterator, error) { + + logs, sub, err := _Chainreader.contract.FilterLogs(opts, "SimpleEvent") + if err != nil { + return nil, err + } + return &ChainreaderSimpleEventIterator{contract: _Chainreader.contract, event: "SimpleEvent", logs: logs, sub: sub}, nil +} + +// WatchSimpleEvent is a free log subscription operation binding the contract event 0x12d199749b3f4c44df8d9386c63d725b7756ec47204f3aa0bf05ea832f89effb. +// +// Solidity: event SimpleEvent(uint256 value) +func (_Chainreader *ChainreaderFilterer) WatchSimpleEvent(opts *bind.WatchOpts, sink chan<- *ChainreaderSimpleEvent) (event.Subscription, error) { + + logs, sub, err := _Chainreader.contract.WatchLogs(opts, "SimpleEvent") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(ChainreaderSimpleEvent) + if err := _Chainreader.contract.UnpackLog(event, "SimpleEvent", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseSimpleEvent is a log parse operation binding the contract event 0x12d199749b3f4c44df8d9386c63d725b7756ec47204f3aa0bf05ea832f89effb. +// +// Solidity: event SimpleEvent(uint256 value) +func (_Chainreader *ChainreaderFilterer) ParseSimpleEvent(log types.Log) (*ChainreaderSimpleEvent, error) { + event := new(ChainreaderSimpleEvent) + if err := _Chainreader.contract.UnpackLog(event, "SimpleEvent", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/core/capabilities/ccip/ccip_integration_tests/chainreader/mycontract.sol b/core/capabilities/ccip/ccip_integration_tests/chainreader/mycontract.sol new file mode 100644 index 00000000000..0fae1f4baac --- /dev/null +++ b/core/capabilities/ccip/ccip_integration_tests/chainreader/mycontract.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.18; + +contract SimpleContract { + event SimpleEvent(uint256 value); + uint256 public eventCount; + uint[] public numbers; + + struct Person { + string name; + uint age; + } + + function emitEvent() public { + eventCount++; + numbers.push(eventCount); + emit SimpleEvent(eventCount); + } + + function getEventCount() public view returns (uint256) { + return eventCount; + } + + function getNumbers() public view returns (uint256[] memory) { + return numbers; + } + + function getPerson() public pure returns (Person memory) { + return Person("Dim", 18); + } +} diff --git a/core/capabilities/ccip/ccip_integration_tests/helpers.go b/core/capabilities/ccip/ccip_integration_tests/helpers.go new file mode 100644 index 00000000000..7606c8bbebc --- /dev/null +++ b/core/capabilities/ccip/ccip_integration_tests/helpers.go @@ -0,0 +1,938 @@ +package ccip_integration_tests + +import ( + "bytes" + "encoding/hex" + "math/big" + "sort" + "testing" + "time" + + "github.com/smartcontractkit/chainlink-ccip/chainconfig" + "github.com/smartcontractkit/chainlink-ccip/pluginconfig" + commonconfig "github.com/smartcontractkit/chainlink-common/pkg/config" + "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ccip_integration_tests/integrationhelpers" + cctypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" + + "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" + + confighelper2 "github.com/smartcontractkit/libocr/offchainreporting2plus/confighelper" + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3confighelper" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/arm_proxy_contract" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/ccip_config" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_multi_offramp" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_multi_onramp" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/maybe_revert_message_receiver" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/mock_arm_contract" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/nonce_manager" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/ocr3_config_encoder" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/price_registry" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/router" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/token_admin_registry" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/weth9" + kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/shared/generated/link_token" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + + chainsel "github.com/smartcontractkit/chain-selectors" + + "github.com/stretchr/testify/require" +) + +var ( + homeChainID = chainsel.GETH_TESTNET.EvmChainID + ccipSendRequestedTopic = evm_2_evm_multi_onramp.EVM2EVMMultiOnRampCCIPSendRequested{}.Topic() + commitReportAcceptedTopic = evm_2_evm_multi_offramp.EVM2EVMMultiOffRampCommitReportAccepted{}.Topic() + executionStateChangedTopic = evm_2_evm_multi_offramp.EVM2EVMMultiOffRampExecutionStateChanged{}.Topic() +) + +const ( + CapabilityLabelledName = "ccip" + CapabilityVersion = "v1.0.0" + NodeOperatorID = 1 + + // These constants drive what is set in the plugin offchain configs. + FirstBlockAge = 8 * time.Hour + RemoteGasPriceBatchWriteFrequency = 30 * time.Minute + BatchGasLimit = 6_500_000 + RelativeBoostPerWaitHour = 1.5 + InflightCacheExpiry = 10 * time.Minute + RootSnoozeTime = 30 * time.Minute + BatchingStrategyID = 0 + DeltaProgress = 30 * time.Second + DeltaResend = 10 * time.Second + DeltaInitial = 20 * time.Second + DeltaRound = 2 * time.Second + DeltaGrace = 2 * time.Second + DeltaCertifiedCommitRequest = 10 * time.Second + DeltaStage = 10 * time.Second + Rmax = 3 + MaxDurationQuery = 50 * time.Millisecond + MaxDurationObservation = 5 * time.Second + MaxDurationShouldAcceptAttestedReport = 10 * time.Second + MaxDurationShouldTransmitAcceptedReport = 10 * time.Second +) + +func e18Mult(amount uint64) *big.Int { + return new(big.Int).Mul(uBigInt(amount), uBigInt(1e18)) +} + +func uBigInt(i uint64) *big.Int { + return new(big.Int).SetUint64(i) +} + +type homeChain struct { + backend *backends.SimulatedBackend + owner *bind.TransactOpts + chainID uint64 + capabilityRegistry *kcr.CapabilitiesRegistry + ccipConfig *ccip_config.CCIPConfig +} + +type onchainUniverse struct { + backend *backends.SimulatedBackend + owner *bind.TransactOpts + chainID uint64 + linkToken *link_token.LinkToken + weth *weth9.WETH9 + router *router.Router + rmnProxy *arm_proxy_contract.ARMProxyContract + rmn *mock_arm_contract.MockARMContract + onramp *evm_2_evm_multi_onramp.EVM2EVMMultiOnRamp + offramp *evm_2_evm_multi_offramp.EVM2EVMMultiOffRamp + priceRegistry *price_registry.PriceRegistry + tokenAdminRegistry *token_admin_registry.TokenAdminRegistry + nonceManager *nonce_manager.NonceManager + receiver *maybe_revert_message_receiver.MaybeRevertMessageReceiver +} + +type requestData struct { + destChainSelector uint64 + receiverAddress common.Address + data []byte +} + +func (u *onchainUniverse) SendCCIPRequests(t *testing.T, requestDatas []requestData) { + for _, reqData := range requestDatas { + msg := router.ClientEVM2AnyMessage{ + Receiver: common.LeftPadBytes(reqData.receiverAddress.Bytes(), 32), + Data: reqData.data, + TokenAmounts: nil, // TODO: no tokens for now + FeeToken: u.weth.Address(), + ExtraArgs: nil, // TODO: no extra args for now, falls back to default + } + fee, err := u.router.GetFee(&bind.CallOpts{Context: testutils.Context(t)}, reqData.destChainSelector, msg) + require.NoError(t, err) + _, err = u.weth.Deposit(&bind.TransactOpts{ + From: u.owner.From, + Signer: u.owner.Signer, + Value: fee, + }) + require.NoError(t, err) + u.backend.Commit() + _, err = u.weth.Approve(u.owner, u.router.Address(), fee) + require.NoError(t, err) + u.backend.Commit() + + t.Logf("Sending CCIP request from chain %d (selector %d) to chain selector %d", + u.chainID, getSelector(u.chainID), reqData.destChainSelector) + _, err = u.router.CcipSend(u.owner, reqData.destChainSelector, msg) + require.NoError(t, err) + u.backend.Commit() + } +} + +type chainBase struct { + backend *backends.SimulatedBackend + owner *bind.TransactOpts +} + +// createUniverses does the following: +// 1. Creates 1 home chain and `numChains`-1 non-home chains +// 2. Sets up home chain with the capability registry and the CCIP config contract +// 2. Deploys the CCIP contracts to all chains. +// 3. Sets up the initial configurations for the contracts on all chains. +// 4. Wires the chains together. +// +// Conceptually one universe is ONE chain with all the contracts deployed on it and all the dependencies initialized. +func createUniverses( + t *testing.T, + numChains int, +) (homeChainUni homeChain, universes map[uint64]onchainUniverse) { + chains := createChains(t, numChains) + + homeChainBase, ok := chains[homeChainID] + require.True(t, ok, "home chain backend not available") + // Set up home chain first + homeChainUniverse := setupHomeChain(t, homeChainBase.owner, homeChainBase.backend) + + // deploy the ccip contracts on all chains + universes = make(map[uint64]onchainUniverse) + for chainID, base := range chains { + owner := base.owner + backend := base.backend + // deploy the CCIP contracts + linkToken := deployLinkToken(t, owner, backend, chainID) + rmn := deployMockARMContract(t, owner, backend, chainID) + rmnProxy := deployARMProxyContract(t, owner, backend, rmn.Address(), chainID) + weth := deployWETHContract(t, owner, backend, chainID) + rout := deployRouter(t, owner, backend, weth.Address(), rmnProxy.Address(), chainID) + priceRegistry := deployPriceRegistry(t, owner, backend, linkToken.Address(), weth.Address(), big.NewInt(1e18), chainID) + tokenAdminRegistry := deployTokenAdminRegistry(t, owner, backend, chainID) + nonceManager := deployNonceManager(t, owner, backend, chainID) + + // ====================================================================== + // OnRamp + // ====================================================================== + onRampAddr, _, _, err := evm_2_evm_multi_onramp.DeployEVM2EVMMultiOnRamp( + owner, + backend, + evm_2_evm_multi_onramp.EVM2EVMMultiOnRampStaticConfig{ + ChainSelector: getSelector(chainID), + RmnProxy: rmnProxy.Address(), + NonceManager: nonceManager.Address(), + TokenAdminRegistry: tokenAdminRegistry.Address(), + }, + evm_2_evm_multi_onramp.EVM2EVMMultiOnRampDynamicConfig{ + Router: rout.Address(), + PriceRegistry: priceRegistry.Address(), + // `withdrawFeeTokens` onRamp function is not part of the message flow + // so we can set this to any address + FeeAggregator: testutils.NewAddress(), + }, + ) + require.NoErrorf(t, err, "failed to deploy onramp on chain id %d", chainID) + backend.Commit() + onramp, err := evm_2_evm_multi_onramp.NewEVM2EVMMultiOnRamp(onRampAddr, backend) + require.NoError(t, err) + + // ====================================================================== + // OffRamp + // ====================================================================== + offrampAddr, _, _, err := evm_2_evm_multi_offramp.DeployEVM2EVMMultiOffRamp( + owner, + backend, + evm_2_evm_multi_offramp.EVM2EVMMultiOffRampStaticConfig{ + ChainSelector: getSelector(chainID), + RmnProxy: rmnProxy.Address(), + TokenAdminRegistry: tokenAdminRegistry.Address(), + NonceManager: nonceManager.Address(), + }, + evm_2_evm_multi_offramp.EVM2EVMMultiOffRampDynamicConfig{ + Router: rout.Address(), + PriceRegistry: priceRegistry.Address(), + }, + // Source chain configs will be set up later once we have all chains + []evm_2_evm_multi_offramp.EVM2EVMMultiOffRampSourceChainConfigArgs{}, + ) + require.NoErrorf(t, err, "failed to deploy offramp on chain id %d", chainID) + backend.Commit() + offramp, err := evm_2_evm_multi_offramp.NewEVM2EVMMultiOffRamp(offrampAddr, backend) + require.NoError(t, err) + + receiverAddress, _, _, err := maybe_revert_message_receiver.DeployMaybeRevertMessageReceiver( + owner, + backend, + false, + ) + require.NoError(t, err, "failed to deploy MaybeRevertMessageReceiver on chain id %d", chainID) + backend.Commit() + receiver, err := maybe_revert_message_receiver.NewMaybeRevertMessageReceiver(receiverAddress, backend) + require.NoError(t, err) + + universe := onchainUniverse{ + backend: backend, + owner: owner, + chainID: chainID, + linkToken: linkToken, + weth: weth, + router: rout, + rmnProxy: rmnProxy, + rmn: rmn, + onramp: onramp, + offramp: offramp, + priceRegistry: priceRegistry, + tokenAdminRegistry: tokenAdminRegistry, + nonceManager: nonceManager, + receiver: receiver, + } + // Set up the initial configurations for the contracts + setupUniverseBasics(t, universe) + + universes[chainID] = universe + } + + // Once we have all chains created and contracts deployed, we can set up the initial configurations and wire chains together + connectUniverses(t, universes) + + // print out all contract addresses for debugging purposes + for chainID, uni := range universes { + t.Logf("Chain ID: %d\n Chain Selector: %d\n LinkToken: %s\n WETH: %s\n Router: %s\n RMNProxy: %s\n RMN: %s\n OnRamp: %s\n OffRamp: %s\n PriceRegistry: %s\n TokenAdminRegistry: %s\n NonceManager: %s\n", + chainID, + getSelector(chainID), + uni.linkToken.Address().Hex(), + uni.weth.Address().Hex(), + uni.router.Address().Hex(), + uni.rmnProxy.Address().Hex(), + uni.rmn.Address().Hex(), + uni.onramp.Address().Hex(), + uni.offramp.Address().Hex(), + uni.priceRegistry.Address().Hex(), + uni.tokenAdminRegistry.Address().Hex(), + uni.nonceManager.Address().Hex(), + ) + } + + // print out topic hashes of relevant events for debugging purposes + t.Logf("Topic hash of CommitReportAccepted: %s", commitReportAcceptedTopic.Hex()) + t.Logf("Topic hash of ExecutionStateChanged: %s", executionStateChangedTopic.Hex()) + t.Logf("Topic hash of CCIPSendRequested: %s", ccipSendRequestedTopic.Hex()) + + return homeChainUniverse, universes +} + +// Creates 1 home chain and `numChains`-1 non-home chains +func createChains(t *testing.T, numChains int) map[uint64]chainBase { + chains := make(map[uint64]chainBase) + + homeChainOwner := testutils.MustNewSimTransactor(t) + homeChainBackend := backends.NewSimulatedBackend(core.GenesisAlloc{ + homeChainOwner.From: core.GenesisAccount{ + Balance: assets.Ether(10_000).ToInt(), + }, + }, 30e6) + tweakChainTimestamp(t, homeChainBackend, FirstBlockAge) + + chains[homeChainID] = chainBase{ + owner: homeChainOwner, + backend: homeChainBackend, + } + + for chainID := chainsel.TEST_90000001.EvmChainID; len(chains) < numChains && chainID < chainsel.TEST_90000020.EvmChainID; chainID++ { + owner := testutils.MustNewSimTransactor(t) + backend := backends.NewSimulatedBackend(core.GenesisAlloc{ + owner.From: core.GenesisAccount{ + Balance: assets.Ether(10_000).ToInt(), + }, + }, 30e6) + + tweakChainTimestamp(t, backend, FirstBlockAge) + + chains[chainID] = chainBase{ + owner: owner, + backend: backend, + } + } + + return chains +} + +// CCIP relies on block timestamps, but SimulatedBackend uses by default clock starting from 1970-01-01 +// This trick is used to move the clock closer to the current time. We set first block to be X hours ago. +// Tests create plenty of transactions so this number can't be too low, every new block mined will tick the clock, +// if you mine more than "X hours" transactions, SimulatedBackend will panic because generated timestamps will be in the future. +func tweakChainTimestamp(t *testing.T, backend *backends.SimulatedBackend, tweak time.Duration) { + blockTime := time.Unix(int64(backend.Blockchain().CurrentHeader().Time), 0) + sinceBlockTime := time.Since(blockTime) + diff := sinceBlockTime - tweak + err := backend.AdjustTime(diff) + require.NoError(t, err, "unable to adjust time on simulated chain") + backend.Commit() + backend.Commit() +} + +func setupHomeChain(t *testing.T, owner *bind.TransactOpts, backend *backends.SimulatedBackend) homeChain { + // deploy the capability registry on the home chain + crAddress, _, _, err := kcr.DeployCapabilitiesRegistry(owner, backend) + require.NoError(t, err, "failed to deploy capability registry on home chain") + backend.Commit() + + capabilityRegistry, err := kcr.NewCapabilitiesRegistry(crAddress, backend) + require.NoError(t, err) + + ccAddress, _, _, err := ccip_config.DeployCCIPConfig(owner, backend, crAddress) + require.NoError(t, err) + backend.Commit() + + capabilityConfig, err := ccip_config.NewCCIPConfig(ccAddress, backend) + require.NoError(t, err) + + _, err = capabilityRegistry.AddCapabilities(owner, []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: CapabilityLabelledName, + Version: CapabilityVersion, + CapabilityType: 2, // consensus. not used (?) + ResponseType: 0, // report. not used (?) + ConfigurationContract: ccAddress, + }, + }) + require.NoError(t, err, "failed to add capabilities to the capability registry") + backend.Commit() + + // Add NodeOperator, for simplicity we'll add one NodeOperator only + // First NodeOperator will have NodeOperatorId = 1 + _, err = capabilityRegistry.AddNodeOperators(owner, []kcr.CapabilitiesRegistryNodeOperator{ + { + Admin: owner.From, + Name: "NodeOperator", + }, + }) + require.NoError(t, err, "failed to add node operator to the capability registry") + backend.Commit() + + return homeChain{ + backend: backend, + owner: owner, + chainID: homeChainID, + capabilityRegistry: capabilityRegistry, + ccipConfig: capabilityConfig, + } +} + +func sortP2PIDS(p2pIDs [][32]byte) { + sort.Slice(p2pIDs, func(i, j int) bool { + return bytes.Compare(p2pIDs[i][:], p2pIDs[j][:]) < 0 + }) +} + +func (h *homeChain) AddNodes( + t *testing.T, + p2pIDs [][32]byte, + capabilityIDs [][32]byte, +) { + // Need to sort, otherwise _checkIsValidUniqueSubset onChain will fail + sortP2PIDS(p2pIDs) + var nodeParams []kcr.CapabilitiesRegistryNodeParams + for _, p2pID := range p2pIDs { + nodeParam := kcr.CapabilitiesRegistryNodeParams{ + NodeOperatorId: NodeOperatorID, + Signer: p2pID, // Not used in tests + P2pId: p2pID, + HashedCapabilityIds: capabilityIDs, + } + nodeParams = append(nodeParams, nodeParam) + } + _, err := h.capabilityRegistry.AddNodes(h.owner, nodeParams) + require.NoError(t, err, "failed to add node operator oracles") + h.backend.Commit() +} + +func AddChainConfig( + t *testing.T, + h homeChain, + chainSelector uint64, + p2pIDs [][32]byte, + f uint8, +) ccip_config.CCIPConfigTypesChainConfigInfo { + // Need to sort, otherwise _checkIsValidUniqueSubset onChain will fail + sortP2PIDS(p2pIDs) + // First Add ChainConfig that includes all p2pIDs as readers + encodedExtraChainConfig, err := chainconfig.EncodeChainConfig(chainconfig.ChainConfig{ + GasPriceDeviationPPB: ccipocr3.NewBigIntFromInt64(1000), + DAGasPriceDeviationPPB: ccipocr3.NewBigIntFromInt64(0), + FinalityDepth: 10, + OptimisticConfirmations: 1, + }) + require.NoError(t, err) + chainConfig := integrationhelpers.SetupConfigInfo(chainSelector, p2pIDs, f, encodedExtraChainConfig) + inputConfig := []ccip_config.CCIPConfigTypesChainConfigInfo{ + chainConfig, + } + _, err = h.ccipConfig.ApplyChainConfigUpdates(h.owner, nil, inputConfig) + require.NoError(t, err) + h.backend.Commit() + return chainConfig +} + +func (h *homeChain) AddDON( + t *testing.T, + ccipCapabilityID [32]byte, + chainSelector uint64, + uni onchainUniverse, + f uint8, + bootstrapP2PID [32]byte, + p2pIDs [][32]byte, + oracles []confighelper2.OracleIdentityExtra, +) { + // Get OCR3 Config from helper + var schedule []int + for range oracles { + schedule = append(schedule, 1) + } + + tabi, err := ocr3_config_encoder.IOCR3ConfigEncoderMetaData.GetAbi() + require.NoError(t, err) + + // Add DON on capability registry contract + var ocr3Configs []ocr3_config_encoder.CCIPConfigTypesOCR3Config + for _, pluginType := range []cctypes.PluginType{cctypes.PluginTypeCCIPCommit, cctypes.PluginTypeCCIPExec} { + var encodedOffchainConfig []byte + var err2 error + if pluginType == cctypes.PluginTypeCCIPCommit { + encodedOffchainConfig, err2 = pluginconfig.EncodeCommitOffchainConfig(pluginconfig.CommitOffchainConfig{ + RemoteGasPriceBatchWriteFrequency: *commonconfig.MustNewDuration(RemoteGasPriceBatchWriteFrequency), + // TODO: implement token price writes + // TokenPriceBatchWriteFrequency: *commonconfig.MustNewDuration(tokenPriceBatchWriteFrequency), + }) + require.NoError(t, err2) + } else { + encodedOffchainConfig, err2 = pluginconfig.EncodeExecuteOffchainConfig(pluginconfig.ExecuteOffchainConfig{ + BatchGasLimit: BatchGasLimit, + RelativeBoostPerWaitHour: RelativeBoostPerWaitHour, + MessageVisibilityInterval: *commonconfig.MustNewDuration(FirstBlockAge), + InflightCacheExpiry: *commonconfig.MustNewDuration(InflightCacheExpiry), + RootSnoozeTime: *commonconfig.MustNewDuration(RootSnoozeTime), + BatchingStrategyID: BatchingStrategyID, + }) + require.NoError(t, err2) + } + signers, transmitters, configF, _, offchainConfigVersion, offchainConfig, err2 := ocr3confighelper.ContractSetConfigArgsForTests( + DeltaProgress, + DeltaResend, + DeltaInitial, + DeltaRound, + DeltaGrace, + DeltaCertifiedCommitRequest, + DeltaStage, + Rmax, + schedule, + oracles, + encodedOffchainConfig, + MaxDurationQuery, + MaxDurationObservation, + MaxDurationShouldAcceptAttestedReport, + MaxDurationShouldTransmitAcceptedReport, + int(f), + []byte{}, // empty OnChainConfig + ) + require.NoError(t, err2, "failed to create contract config") + + signersBytes := make([][]byte, len(signers)) + for i, signer := range signers { + signersBytes[i] = signer + } + + transmittersBytes := make([][]byte, len(transmitters)) + for i, transmitter := range transmitters { + // anotherErr because linting doesn't want to shadow err + parsed, anotherErr := common.ParseHexOrString(string(transmitter)) + require.NoError(t, anotherErr) + transmittersBytes[i] = parsed + } + + ocr3Configs = append(ocr3Configs, ocr3_config_encoder.CCIPConfigTypesOCR3Config{ + PluginType: uint8(pluginType), + ChainSelector: chainSelector, + F: configF, + OffchainConfigVersion: offchainConfigVersion, + OfframpAddress: uni.offramp.Address().Bytes(), + BootstrapP2PIds: [][32]byte{bootstrapP2PID}, + P2pIds: p2pIDs, + Signers: signersBytes, + Transmitters: transmittersBytes, + OffchainConfig: offchainConfig, + }) + } + + encodedCall, err := tabi.Pack("exposeOCR3Config", ocr3Configs) + require.NoError(t, err) + + // Trim first four bytes to remove function selector. + encodedConfigs := encodedCall[4:] + + // commit so that we have an empty block to filter events from + h.backend.Commit() + + _, err = h.capabilityRegistry.AddDON(h.owner, p2pIDs, []kcr.CapabilitiesRegistryCapabilityConfiguration{ + { + CapabilityId: ccipCapabilityID, + Config: encodedConfigs, + }, + }, false, false, f) + require.NoError(t, err) + h.backend.Commit() + + endBlock := h.backend.Blockchain().CurrentBlock().Number.Uint64() + iter, err := h.capabilityRegistry.FilterConfigSet(&bind.FilterOpts{ + Start: h.backend.Blockchain().CurrentBlock().Number.Uint64() - 1, + End: &endBlock, + }) + require.NoError(t, err, "failed to filter config set events") + var donID uint32 + for iter.Next() { + donID = iter.Event.DonId + break + } + require.NotZero(t, donID, "failed to get donID from config set event") + + var signerAddresses []common.Address + for _, oracle := range oracles { + signerAddresses = append(signerAddresses, common.BytesToAddress(oracle.OnchainPublicKey)) + } + + var transmitterAddresses []common.Address + for _, oracle := range oracles { + transmitterAddresses = append(transmitterAddresses, common.HexToAddress(string(oracle.TransmitAccount))) + } + + // get the config digest from the ccip config contract and set config on the offramp. + var offrampOCR3Configs []evm_2_evm_multi_offramp.MultiOCR3BaseOCRConfigArgs + for _, pluginType := range []cctypes.PluginType{cctypes.PluginTypeCCIPCommit, cctypes.PluginTypeCCIPExec} { + ocrConfig, err1 := h.ccipConfig.GetOCRConfig(&bind.CallOpts{ + Context: testutils.Context(t), + }, donID, uint8(pluginType)) + require.NoError(t, err1, "failed to get OCR3 config from ccip config contract") + require.Len(t, ocrConfig, 1, "expected exactly one OCR3 config") + offrampOCR3Configs = append(offrampOCR3Configs, evm_2_evm_multi_offramp.MultiOCR3BaseOCRConfigArgs{ + ConfigDigest: ocrConfig[0].ConfigDigest, + OcrPluginType: uint8(pluginType), + F: f, + IsSignatureVerificationEnabled: pluginType == cctypes.PluginTypeCCIPCommit, + Signers: signerAddresses, + Transmitters: transmitterAddresses, + }) + } + + uni.backend.Commit() + + _, err = uni.offramp.SetOCR3Configs(uni.owner, offrampOCR3Configs) + require.NoError(t, err, "failed to set ocr3 configs on offramp") + uni.backend.Commit() + + for _, pluginType := range []cctypes.PluginType{cctypes.PluginTypeCCIPCommit, cctypes.PluginTypeCCIPExec} { + ocrConfig, err := uni.offramp.LatestConfigDetails(&bind.CallOpts{ + Context: testutils.Context(t), + }, uint8(pluginType)) + require.NoError(t, err, "failed to get latest commit OCR3 config") + require.Equalf(t, offrampOCR3Configs[pluginType].ConfigDigest, ocrConfig.ConfigInfo.ConfigDigest, "%s OCR3 config digest mismatch", pluginType.String()) + require.Equalf(t, offrampOCR3Configs[pluginType].F, ocrConfig.ConfigInfo.F, "%s OCR3 config F mismatch", pluginType.String()) + require.Equalf(t, offrampOCR3Configs[pluginType].IsSignatureVerificationEnabled, ocrConfig.ConfigInfo.IsSignatureVerificationEnabled, "%s OCR3 config signature verification mismatch", pluginType.String()) + if pluginType == cctypes.PluginTypeCCIPCommit { + // only commit will set signers, exec doesn't need them. + require.Equalf(t, offrampOCR3Configs[pluginType].Signers, ocrConfig.Signers, "%s OCR3 config signers mismatch", pluginType.String()) + } + require.Equalf(t, offrampOCR3Configs[pluginType].Transmitters, ocrConfig.Transmitters, "%s OCR3 config transmitters mismatch", pluginType.String()) + } + + t.Logf("set ocr3 config on the offramp, signers: %+v, transmitters: %+v", signerAddresses, transmitterAddresses) +} + +func connectUniverses( + t *testing.T, + universes map[uint64]onchainUniverse, +) { + for _, uni := range universes { + wireRouter(t, uni, universes) + wirePriceRegistry(t, uni, universes) + wireOffRamp(t, uni, universes) + initRemoteChainsGasPrices(t, uni, universes) + } +} + +// setupUniverseBasics sets up the initial configurations for the CCIP contracts on a single chain. +// 1. Mint 1000 LINK to the owner +// 2. Set the price registry with local token prices +// 3. Authorize the onRamp and offRamp on the nonce manager +func setupUniverseBasics(t *testing.T, uni onchainUniverse) { + // ============================================================================= + // Universe specific updates/configs + // These updates are specific to each universe and are set up here + // These updates don't depend on other chains + // ============================================================================= + owner := uni.owner + // ============================================================================= + // Mint 1000 LINK to owner + // ============================================================================= + _, err := uni.linkToken.GrantMintRole(owner, owner.From) + require.NoError(t, err) + _, err = uni.linkToken.Mint(owner, owner.From, e18Mult(1000)) + require.NoError(t, err) + uni.backend.Commit() + + // ============================================================================= + // Price updates for tokens + // These are the prices of the fee tokens of local chain in USD + // ============================================================================= + tokenPriceUpdates := []price_registry.InternalTokenPriceUpdate{ + { + SourceToken: uni.linkToken.Address(), + UsdPerToken: e18Mult(20), + }, + { + SourceToken: uni.weth.Address(), + UsdPerToken: e18Mult(4000), + }, + } + _, err = uni.priceRegistry.UpdatePrices(owner, price_registry.InternalPriceUpdates{ + TokenPriceUpdates: tokenPriceUpdates, + }) + require.NoErrorf(t, err, "failed to update prices in price registry on chain id %d", uni.chainID) + uni.backend.Commit() + + _, err = uni.priceRegistry.ApplyAuthorizedCallerUpdates(owner, price_registry.AuthorizedCallersAuthorizedCallerArgs{ + AddedCallers: []common.Address{ + uni.offramp.Address(), + }, + }) + require.NoError(t, err, "failed to authorize offramp on price registry") + uni.backend.Commit() + + // ============================================================================= + // Authorize OnRamp & OffRamp on NonceManager + // Otherwise the onramp will not be able to call the nonceManager to get next Nonce + // ============================================================================= + authorizedCallersAuthorizedCallerArgs := nonce_manager.AuthorizedCallersAuthorizedCallerArgs{ + AddedCallers: []common.Address{ + uni.onramp.Address(), + uni.offramp.Address(), + }, + } + _, err = uni.nonceManager.ApplyAuthorizedCallerUpdates(owner, authorizedCallersAuthorizedCallerArgs) + require.NoError(t, err) + uni.backend.Commit() +} + +// As we can't change router contract. The contract was expecting onRamp and offRamp per lane and not per chain +// In the new architecture we have only one onRamp and one offRamp per chain. +// hence we add the mapping for all remote chains to the onRamp/offRamp contract of the local chain +func wireRouter(t *testing.T, uni onchainUniverse, universes map[uint64]onchainUniverse) { + owner := uni.owner + var ( + routerOnrampUpdates []router.RouterOnRamp + routerOfframpUpdates []router.RouterOffRamp + ) + for remoteChainID := range universes { + if remoteChainID == uni.chainID { + continue + } + routerOnrampUpdates = append(routerOnrampUpdates, router.RouterOnRamp{ + DestChainSelector: getSelector(remoteChainID), + OnRamp: uni.onramp.Address(), + }) + routerOfframpUpdates = append(routerOfframpUpdates, router.RouterOffRamp{ + SourceChainSelector: getSelector(remoteChainID), + OffRamp: uni.offramp.Address(), + }) + } + _, err := uni.router.ApplyRampUpdates(owner, routerOnrampUpdates, []router.RouterOffRamp{}, routerOfframpUpdates) + require.NoErrorf(t, err, "failed to apply ramp updates on router on chain id %d", uni.chainID) + uni.backend.Commit() +} + +// Setting OnRampDestChainConfigs +func wirePriceRegistry(t *testing.T, uni onchainUniverse, universes map[uint64]onchainUniverse) { + owner := uni.owner + var priceRegistryDestChainConfigArgs []price_registry.PriceRegistryDestChainConfigArgs + for remoteChainID := range universes { + if remoteChainID == uni.chainID { + continue + } + priceRegistryDestChainConfigArgs = append(priceRegistryDestChainConfigArgs, price_registry.PriceRegistryDestChainConfigArgs{ + DestChainSelector: getSelector(remoteChainID), + DestChainConfig: defaultPriceRegistryDestChainConfig(t), + }) + } + _, err := uni.priceRegistry.ApplyDestChainConfigUpdates(owner, priceRegistryDestChainConfigArgs) + require.NoErrorf(t, err, "failed to apply dest chain config updates on price registry on chain id %d", uni.chainID) + uni.backend.Commit() +} + +// Setting OffRampSourceChainConfigs +func wireOffRamp(t *testing.T, uni onchainUniverse, universes map[uint64]onchainUniverse) { + owner := uni.owner + var offrampSourceChainConfigArgs []evm_2_evm_multi_offramp.EVM2EVMMultiOffRampSourceChainConfigArgs + for remoteChainID, remoteUniverse := range universes { + if remoteChainID == uni.chainID { + continue + } + offrampSourceChainConfigArgs = append(offrampSourceChainConfigArgs, evm_2_evm_multi_offramp.EVM2EVMMultiOffRampSourceChainConfigArgs{ + SourceChainSelector: getSelector(remoteChainID), // for each destination chain, add a source chain config + IsEnabled: true, + OnRamp: remoteUniverse.onramp.Address().Bytes(), + }) + } + _, err := uni.offramp.ApplySourceChainConfigUpdates(owner, offrampSourceChainConfigArgs) + require.NoErrorf(t, err, "failed to apply source chain config updates on offramp on chain id %d", uni.chainID) + uni.backend.Commit() + for remoteChainID, remoteUniverse := range universes { + if remoteChainID == uni.chainID { + continue + } + sourceCfg, err2 := uni.offramp.GetSourceChainConfig(&bind.CallOpts{}, getSelector(remoteChainID)) + require.NoError(t, err2) + require.True(t, sourceCfg.IsEnabled, "source chain config should be enabled") + require.Equal(t, remoteUniverse.onramp.Address(), common.BytesToAddress(sourceCfg.OnRamp), "source chain config onRamp address mismatch") + } +} + +func getSelector(chainID uint64) uint64 { + selector, err := chainsel.SelectorFromChainId(chainID) + if err != nil { + panic(err) + } + return selector +} + +// initRemoteChainsGasPrices sets the gas prices for all chains except the local chain in the local price registry +func initRemoteChainsGasPrices(t *testing.T, uni onchainUniverse, universes map[uint64]onchainUniverse) { + var gasPriceUpdates []price_registry.InternalGasPriceUpdate + for remoteChainID := range universes { + if remoteChainID == uni.chainID { + continue + } + gasPriceUpdates = append(gasPriceUpdates, + price_registry.InternalGasPriceUpdate{ + DestChainSelector: getSelector(remoteChainID), + UsdPerUnitGas: big.NewInt(2e12), + }, + ) + } + _, err := uni.priceRegistry.UpdatePrices(uni.owner, price_registry.InternalPriceUpdates{ + GasPriceUpdates: gasPriceUpdates, + }) + require.NoError(t, err) +} + +func defaultPriceRegistryDestChainConfig(t *testing.T) price_registry.PriceRegistryDestChainConfig { + // https://github.com/smartcontractkit/ccip/blob/c4856b64bd766f1ddbaf5d13b42d3c4b12efde3a/contracts/src/v0.8/ccip/libraries/Internal.sol#L337-L337 + /* + ```Solidity + // bytes4(keccak256("CCIP ChainFamilySelector EVM")) + bytes4 public constant CHAIN_FAMILY_SELECTOR_EVM = 0x2812d52c; + ``` + */ + evmFamilySelector, err := hex.DecodeString("2812d52c") + require.NoError(t, err) + return price_registry.PriceRegistryDestChainConfig{ + IsEnabled: true, + MaxNumberOfTokensPerMsg: 10, + MaxDataBytes: 256, + MaxPerMsgGasLimit: 3_000_000, + DestGasOverhead: 50_000, + DefaultTokenFeeUSDCents: 1, + DestGasPerPayloadByte: 10, + DestDataAvailabilityOverheadGas: 0, + DestGasPerDataAvailabilityByte: 100, + DestDataAvailabilityMultiplierBps: 1, + DefaultTokenDestGasOverhead: 125_000, + DefaultTokenDestBytesOverhead: 32, + DefaultTxGasLimit: 200_000, + GasMultiplierWeiPerEth: 1, + NetworkFeeUSDCents: 1, + ChainFamilySelector: [4]byte(evmFamilySelector), + } +} + +func deployLinkToken(t *testing.T, owner *bind.TransactOpts, backend *backends.SimulatedBackend, chainID uint64) *link_token.LinkToken { + linkAddr, _, _, err := link_token.DeployLinkToken(owner, backend) + require.NoErrorf(t, err, "failed to deploy link token on chain id %d", chainID) + backend.Commit() + linkToken, err := link_token.NewLinkToken(linkAddr, backend) + require.NoError(t, err) + return linkToken +} + +func deployMockARMContract(t *testing.T, owner *bind.TransactOpts, backend *backends.SimulatedBackend, chainID uint64) *mock_arm_contract.MockARMContract { + rmnAddr, _, _, err := mock_arm_contract.DeployMockARMContract(owner, backend) + require.NoErrorf(t, err, "failed to deploy mock arm on chain id %d", chainID) + backend.Commit() + rmn, err := mock_arm_contract.NewMockARMContract(rmnAddr, backend) + require.NoError(t, err) + return rmn +} + +func deployARMProxyContract(t *testing.T, owner *bind.TransactOpts, backend *backends.SimulatedBackend, rmnAddr common.Address, chainID uint64) *arm_proxy_contract.ARMProxyContract { + rmnProxyAddr, _, _, err := arm_proxy_contract.DeployARMProxyContract(owner, backend, rmnAddr) + require.NoErrorf(t, err, "failed to deploy arm proxy on chain id %d", chainID) + backend.Commit() + rmnProxy, err := arm_proxy_contract.NewARMProxyContract(rmnProxyAddr, backend) + require.NoError(t, err) + return rmnProxy +} + +func deployWETHContract(t *testing.T, owner *bind.TransactOpts, backend *backends.SimulatedBackend, chainID uint64) *weth9.WETH9 { + wethAddr, _, _, err := weth9.DeployWETH9(owner, backend) + require.NoErrorf(t, err, "failed to deploy weth contract on chain id %d", chainID) + backend.Commit() + weth, err := weth9.NewWETH9(wethAddr, backend) + require.NoError(t, err) + return weth +} + +func deployRouter(t *testing.T, owner *bind.TransactOpts, backend *backends.SimulatedBackend, wethAddr, rmnProxyAddr common.Address, chainID uint64) *router.Router { + routerAddr, _, _, err := router.DeployRouter(owner, backend, wethAddr, rmnProxyAddr) + require.NoErrorf(t, err, "failed to deploy router on chain id %d", chainID) + backend.Commit() + rout, err := router.NewRouter(routerAddr, backend) + require.NoError(t, err) + return rout +} + +func deployPriceRegistry( + t *testing.T, + owner *bind.TransactOpts, + backend *backends.SimulatedBackend, + linkAddr, + wethAddr common.Address, + maxFeeJuelsPerMsg *big.Int, + chainID uint64, +) *price_registry.PriceRegistry { + priceRegistryAddr, _, _, err := price_registry.DeployPriceRegistry( + owner, + backend, + price_registry.PriceRegistryStaticConfig{ + MaxFeeJuelsPerMsg: maxFeeJuelsPerMsg, + LinkToken: linkAddr, + StalenessThreshold: 24 * 60 * 60, // 24 hours + }, + []common.Address{ + owner.From, // owner can update prices in this test + }, // price updaters, will be set to offramp later + []common.Address{linkAddr, wethAddr}, // fee tokens + // empty for now, need to fill in when testing token transfers + []price_registry.PriceRegistryTokenPriceFeedUpdate{}, + // empty for now, need to fill in when testing token transfers + []price_registry.PriceRegistryTokenTransferFeeConfigArgs{}, + []price_registry.PriceRegistryPremiumMultiplierWeiPerEthArgs{ + { + PremiumMultiplierWeiPerEth: 9e17, // 0.9 ETH + Token: linkAddr, + }, + { + PremiumMultiplierWeiPerEth: 1e18, + Token: wethAddr, + }, + }, + // Destination chain configs will be set up later once we have all chains + []price_registry.PriceRegistryDestChainConfigArgs{}, + ) + require.NoErrorf(t, err, "failed to deploy price registry on chain id %d", chainID) + backend.Commit() + priceRegistry, err := price_registry.NewPriceRegistry(priceRegistryAddr, backend) + require.NoError(t, err) + return priceRegistry +} + +func deployTokenAdminRegistry(t *testing.T, owner *bind.TransactOpts, backend *backends.SimulatedBackend, chainID uint64) *token_admin_registry.TokenAdminRegistry { + tarAddr, _, _, err := token_admin_registry.DeployTokenAdminRegistry(owner, backend) + require.NoErrorf(t, err, "failed to deploy token admin registry on chain id %d", chainID) + backend.Commit() + tokenAdminRegistry, err := token_admin_registry.NewTokenAdminRegistry(tarAddr, backend) + require.NoError(t, err) + return tokenAdminRegistry +} + +func deployNonceManager(t *testing.T, owner *bind.TransactOpts, backend *backends.SimulatedBackend, chainID uint64) *nonce_manager.NonceManager { + nonceManagerAddr, _, _, err := nonce_manager.DeployNonceManager(owner, backend, []common.Address{owner.From}) + require.NoErrorf(t, err, "failed to deploy nonce_manager on chain id %d", chainID) + backend.Commit() + nonceManager, err := nonce_manager.NewNonceManager(nonceManagerAddr, backend) + require.NoError(t, err) + return nonceManager +} diff --git a/core/capabilities/ccip/ccip_integration_tests/home_chain_test.go b/core/capabilities/ccip/ccip_integration_tests/home_chain_test.go new file mode 100644 index 00000000000..c78fd37b809 --- /dev/null +++ b/core/capabilities/ccip/ccip_integration_tests/home_chain_test.go @@ -0,0 +1,103 @@ +package ccip_integration_tests + +import ( + "testing" + "time" + + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ccip_integration_tests/integrationhelpers" + + mapset "github.com/deckarep/golang-set/v2" + "github.com/onsi/gomega" + + libocrtypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/chainlink-ccip/chainconfig" + ccipreader "github.com/smartcontractkit/chainlink-ccip/pkg/reader" + + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + + "github.com/stretchr/testify/require" + + capcfg "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/ccip_config" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/logger" +) + +func TestHomeChainReader(t *testing.T) { + ctx := testutils.Context(t) + lggr := logger.TestLogger(t) + uni := integrationhelpers.NewTestUniverse(ctx, t, lggr) + // We need 3*f + 1 p2pIDs to have enough nodes to bootstrap + var arr []int64 + n := int(integrationhelpers.FChainA*3 + 1) + for i := 0; i <= n; i++ { + arr = append(arr, int64(i)) + } + p2pIDs := integrationhelpers.P2pIDsFromInts(arr) + uni.AddCapability(p2pIDs) + //==============================Apply configs to Capability Contract================================= + encodedChainConfig, err := chainconfig.EncodeChainConfig(chainconfig.ChainConfig{ + GasPriceDeviationPPB: cciptypes.NewBigIntFromInt64(1000), + DAGasPriceDeviationPPB: cciptypes.NewBigIntFromInt64(1_000_000), + FinalityDepth: -1, + OptimisticConfirmations: 1, + }) + require.NoError(t, err) + chainAConf := integrationhelpers.SetupConfigInfo(integrationhelpers.ChainA, p2pIDs, integrationhelpers.FChainA, encodedChainConfig) + chainBConf := integrationhelpers.SetupConfigInfo(integrationhelpers.ChainB, p2pIDs[1:], integrationhelpers.FChainB, encodedChainConfig) + chainCConf := integrationhelpers.SetupConfigInfo(integrationhelpers.ChainC, p2pIDs[2:], integrationhelpers.FChainC, encodedChainConfig) + inputConfig := []capcfg.CCIPConfigTypesChainConfigInfo{ + chainAConf, + chainBConf, + chainCConf, + } + _, err = uni.CcipCfg.ApplyChainConfigUpdates(uni.Transactor, nil, inputConfig) + require.NoError(t, err) + uni.Backend.Commit() + //================================Setup HomeChainReader=============================== + + pollDuration := time.Second + homeChain := uni.HomeChainReader + + gomega.NewWithT(t).Eventually(func() bool { + configs, _ := homeChain.GetAllChainConfigs() + return configs != nil + }, testutils.WaitTimeout(t), pollDuration*5).Should(gomega.BeTrue()) + + t.Logf("homchain reader is ready") + //================================Test HomeChain Reader=============================== + expectedChainConfigs := map[cciptypes.ChainSelector]ccipreader.ChainConfig{} + for _, c := range inputConfig { + expectedChainConfigs[cciptypes.ChainSelector(c.ChainSelector)] = ccipreader.ChainConfig{ + FChain: int(c.ChainConfig.FChain), + SupportedNodes: toPeerIDs(c.ChainConfig.Readers), + Config: mustDecodeChainConfig(t, c.ChainConfig.Config), + } + } + configs, err := homeChain.GetAllChainConfigs() + require.NoError(t, err) + require.Equal(t, expectedChainConfigs, configs) + //=================================Remove ChainC from OnChainConfig========================================= + _, err = uni.CcipCfg.ApplyChainConfigUpdates(uni.Transactor, []uint64{integrationhelpers.ChainC}, nil) + require.NoError(t, err) + uni.Backend.Commit() + time.Sleep(pollDuration * 5) // Wait for the chain reader to update + configs, err = homeChain.GetAllChainConfigs() + require.NoError(t, err) + delete(expectedChainConfigs, cciptypes.ChainSelector(integrationhelpers.ChainC)) + require.Equal(t, expectedChainConfigs, configs) +} + +func toPeerIDs(readers [][32]byte) mapset.Set[libocrtypes.PeerID] { + peerIDs := mapset.NewSet[libocrtypes.PeerID]() + for _, r := range readers { + peerIDs.Add(r) + } + return peerIDs +} + +func mustDecodeChainConfig(t *testing.T, encodedChainConfig []byte) chainconfig.ChainConfig { + chainConfig, err := chainconfig.DecodeChainConfig(encodedChainConfig) + require.NoError(t, err) + return chainConfig +} diff --git a/core/capabilities/ccip/ccip_integration_tests/integrationhelpers/integration_helpers.go b/core/capabilities/ccip/ccip_integration_tests/integrationhelpers/integration_helpers.go new file mode 100644 index 00000000000..7520b126336 --- /dev/null +++ b/core/capabilities/ccip/ccip_integration_tests/integrationhelpers/integration_helpers.go @@ -0,0 +1,304 @@ +package integrationhelpers + +import ( + "context" + "encoding/json" + "fmt" + "math/big" + "sort" + "testing" + "time" + + configsevm "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/configs/evm" + cctypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" + + "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/smartcontractkit/chainlink-ccip/pkg/consts" + ccipreader "github.com/smartcontractkit/chainlink-ccip/pkg/reader" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/headtracker" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/ccip_config" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/ocr3_config_encoder" + kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" + + "github.com/smartcontractkit/chainlink-common/pkg/types" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" + evmrelaytypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" +) + +const chainID = 1337 + +func NewReader( + t *testing.T, + logPoller logpoller.LogPoller, + headTracker logpoller.HeadTracker, + client client.Client, + address common.Address, + chainReaderConfig evmrelaytypes.ChainReaderConfig, +) types.ContractReader { + cr, err := evm.NewChainReaderService(testutils.Context(t), logger.TestLogger(t), logPoller, headTracker, client, chainReaderConfig) + require.NoError(t, err) + err = cr.Bind(testutils.Context(t), []types.BoundContract{ + { + Address: address.String(), + Name: consts.ContractNameCCIPConfig, + }, + }) + require.NoError(t, err) + require.NoError(t, cr.Start(testutils.Context(t))) + for { + if err := cr.Ready(); err == nil { + break + } + } + + return cr +} + +const ( + ChainA uint64 = 1 + FChainA uint8 = 1 + + ChainB uint64 = 2 + FChainB uint8 = 2 + + ChainC uint64 = 3 + FChainC uint8 = 3 + + CcipCapabilityLabelledName = "ccip" + CcipCapabilityVersion = "v1.0" +) + +var CapabilityID = fmt.Sprintf("%s@%s", CcipCapabilityLabelledName, CcipCapabilityVersion) + +type TestUniverse struct { + Transactor *bind.TransactOpts + Backend *backends.SimulatedBackend + CapReg *kcr.CapabilitiesRegistry + CcipCfg *ccip_config.CCIPConfig + TestingT *testing.T + LogPoller logpoller.LogPoller + HeadTracker logpoller.HeadTracker + SimClient client.Client + HomeChainReader ccipreader.HomeChain +} + +func NewTestUniverse(ctx context.Context, t *testing.T, lggr logger.Logger) TestUniverse { + transactor := testutils.MustNewSimTransactor(t) + backend := backends.NewSimulatedBackend(core.GenesisAlloc{ + transactor.From: {Balance: assets.Ether(1000).ToInt()}, + }, 30e6) + + crAddress, _, _, err := kcr.DeployCapabilitiesRegistry(transactor, backend) + require.NoError(t, err) + backend.Commit() + + capReg, err := kcr.NewCapabilitiesRegistry(crAddress, backend) + require.NoError(t, err) + + ccAddress, _, _, err := ccip_config.DeployCCIPConfig(transactor, backend, crAddress) + require.NoError(t, err) + backend.Commit() + + cc, err := ccip_config.NewCCIPConfig(ccAddress, backend) + require.NoError(t, err) + + db := pgtest.NewSqlxDB(t) + lpOpts := logpoller.Opts{ + PollPeriod: time.Millisecond, + FinalityDepth: 0, + BackfillBatchSize: 10, + RpcBatchSize: 10, + KeepFinalizedBlocksDepth: 100000, + } + cl := client.NewSimulatedBackendClient(t, backend, big.NewInt(chainID)) + headTracker := headtracker.NewSimulatedHeadTracker(cl, lpOpts.UseFinalityTag, lpOpts.FinalityDepth) + if lpOpts.PollPeriod == 0 { + lpOpts.PollPeriod = 1 * time.Hour + } + lp := logpoller.NewLogPoller(logpoller.NewORM(big.NewInt(chainID), db, lggr), cl, logger.NullLogger, headTracker, lpOpts) + require.NoError(t, lp.Start(ctx)) + t.Cleanup(func() { require.NoError(t, lp.Close()) }) + + hcr := NewHomeChainReader(t, lp, headTracker, cl, ccAddress) + return TestUniverse{ + Transactor: transactor, + Backend: backend, + CapReg: capReg, + CcipCfg: cc, + TestingT: t, + SimClient: cl, + LogPoller: lp, + HeadTracker: headTracker, + HomeChainReader: hcr, + } +} + +func (t TestUniverse) NewContractReader(ctx context.Context, cfg []byte) (types.ContractReader, error) { + var config evmrelaytypes.ChainReaderConfig + err := json.Unmarshal(cfg, &config) + require.NoError(t.TestingT, err) + return evm.NewChainReaderService(ctx, logger.TestLogger(t.TestingT), t.LogPoller, t.HeadTracker, t.SimClient, config) +} + +func P2pIDsFromInts(ints []int64) [][32]byte { + var p2pIDs [][32]byte + for _, i := range ints { + p2pID := p2pkey.MustNewV2XXXTestingOnly(big.NewInt(i)).PeerID() + p2pIDs = append(p2pIDs, p2pID) + } + sort.Slice(p2pIDs, func(i, j int) bool { + for k := 0; k < 32; k++ { + if p2pIDs[i][k] < p2pIDs[j][k] { + return true + } else if p2pIDs[i][k] > p2pIDs[j][k] { + return false + } + } + return false + }) + return p2pIDs +} + +func (t *TestUniverse) AddCapability(p2pIDs [][32]byte) { + _, err := t.CapReg.AddCapabilities(t.Transactor, []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: CcipCapabilityLabelledName, + Version: CcipCapabilityVersion, + CapabilityType: 0, + ResponseType: 0, + ConfigurationContract: t.CcipCfg.Address(), + }, + }) + require.NoError(t.TestingT, err, "failed to add capability to registry") + t.Backend.Commit() + + ccipCapabilityID, err := t.CapReg.GetHashedCapabilityId(nil, CcipCapabilityLabelledName, CcipCapabilityVersion) + require.NoError(t.TestingT, err) + + for i := 0; i < len(p2pIDs); i++ { + _, err = t.CapReg.AddNodeOperators(t.Transactor, []kcr.CapabilitiesRegistryNodeOperator{ + { + Admin: t.Transactor.From, + Name: fmt.Sprintf("nop-%d", i), + }, + }) + require.NoError(t.TestingT, err) + t.Backend.Commit() + + // get the node operator id from the event + it, err := t.CapReg.FilterNodeOperatorAdded(nil, nil, nil) + require.NoError(t.TestingT, err) + var nodeOperatorID uint32 + for it.Next() { + if it.Event.Name == fmt.Sprintf("nop-%d", i) { + nodeOperatorID = it.Event.NodeOperatorId + break + } + } + require.NotZero(t.TestingT, nodeOperatorID) + + _, err = t.CapReg.AddNodes(t.Transactor, []kcr.CapabilitiesRegistryNodeParams{ + { + NodeOperatorId: nodeOperatorID, + Signer: testutils.Random32Byte(), + P2pId: p2pIDs[i], + HashedCapabilityIds: [][32]byte{ccipCapabilityID}, + }, + }) + require.NoError(t.TestingT, err) + t.Backend.Commit() + + // verify that the node was added successfully + nodeInfo, err := t.CapReg.GetNode(nil, p2pIDs[i]) + require.NoError(t.TestingT, err) + + require.Equal(t.TestingT, nodeOperatorID, nodeInfo.NodeOperatorId) + require.Equal(t.TestingT, p2pIDs[i][:], nodeInfo.P2pId[:]) + } +} + +func NewHomeChainReader(t *testing.T, logPoller logpoller.LogPoller, headTracker logpoller.HeadTracker, client client.Client, ccAddress common.Address) ccipreader.HomeChain { + cr := NewReader(t, logPoller, headTracker, client, ccAddress, configsevm.HomeChainReaderConfigRaw()) + + hcr := ccipreader.NewHomeChainReader(cr, logger.TestLogger(t), 500*time.Millisecond) + require.NoError(t, hcr.Start(testutils.Context(t))) + t.Cleanup(func() { require.NoError(t, hcr.Close()) }) + + return hcr +} + +func (t *TestUniverse) AddDONToRegistry( + ccipCapabilityID [32]byte, + chainSelector uint64, + f uint8, + bootstrapP2PID [32]byte, + p2pIDs [][32]byte, +) { + tabi, err := ocr3_config_encoder.IOCR3ConfigEncoderMetaData.GetAbi() + require.NoError(t.TestingT, err) + + var ( + signers [][]byte + transmitters [][]byte + ) + for range p2pIDs { + signers = append(signers, testutils.NewAddress().Bytes()) + transmitters = append(transmitters, testutils.NewAddress().Bytes()) + } + + var ocr3Configs []ocr3_config_encoder.CCIPConfigTypesOCR3Config + for _, pluginType := range []cctypes.PluginType{cctypes.PluginTypeCCIPCommit, cctypes.PluginTypeCCIPExec} { + ocr3Configs = append(ocr3Configs, ocr3_config_encoder.CCIPConfigTypesOCR3Config{ + PluginType: uint8(pluginType), + ChainSelector: chainSelector, + F: f, + OffchainConfigVersion: 30, + OfframpAddress: testutils.NewAddress().Bytes(), + BootstrapP2PIds: [][32]byte{bootstrapP2PID}, + P2pIds: p2pIDs, + Signers: signers, + Transmitters: transmitters, + OffchainConfig: []byte("offchain config"), + }) + } + + encodedCall, err := tabi.Pack("exposeOCR3Config", ocr3Configs) + require.NoError(t.TestingT, err) + + // Trim first four bytes to remove function selector. + encodedConfigs := encodedCall[4:] + + _, err = t.CapReg.AddDON(t.Transactor, p2pIDs, []kcr.CapabilitiesRegistryCapabilityConfiguration{ + { + CapabilityId: ccipCapabilityID, + Config: encodedConfigs, + }, + }, false, false, f) + require.NoError(t.TestingT, err) + t.Backend.Commit() +} + +func SetupConfigInfo(chainSelector uint64, readers [][32]byte, fChain uint8, cfg []byte) ccip_config.CCIPConfigTypesChainConfigInfo { + return ccip_config.CCIPConfigTypesChainConfigInfo{ + ChainSelector: chainSelector, + ChainConfig: ccip_config.CCIPConfigTypesChainConfig{ + Readers: readers, + FChain: fChain, + Config: cfg, + }, + } +} diff --git a/core/capabilities/ccip/ccip_integration_tests/ocr3_node_test.go b/core/capabilities/ccip/ccip_integration_tests/ocr3_node_test.go new file mode 100644 index 00000000000..8cafb901724 --- /dev/null +++ b/core/capabilities/ccip/ccip_integration_tests/ocr3_node_test.go @@ -0,0 +1,281 @@ +package ccip_integration_tests + +import ( + "fmt" + "math/big" + "sync" + "testing" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/hashicorp/consul/sdk/freeport" + "go.uber.org/zap/zapcore" + + "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_multi_offramp" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" + + confighelper2 "github.com/smartcontractkit/libocr/offchainreporting2plus/confighelper" + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + + "github.com/stretchr/testify/require" +) + +const STATE_SUCCESS = uint8(2) + +/* +* If you want to debug, set log level to info and use the following commands for easier logs filtering. +* +* // Run the test and redirect logs to logs.txt +* go test -v -run "^TestIntegration_OCR3Nodes" ./core/capabilities/ccip/ccip_integration_tests 2>&1 > logs.txt +* +* // Reads logs.txt as a stream and apply filters using grep +* tail -fn0 logs.txt | grep "CCIPExecPlugin" + */ +func TestIntegration_OCR3Nodes(t *testing.T) { + const ( + numChains = 3 // number of chains that this test will run on + numNodes = 4 // number of OCR3 nodes, test assumes that every node supports every chain + + simulatedBackendBlockTime = 900 * time.Millisecond // Simulated backend blocks committing interval + oraclesBootWaitTime = 30 * time.Second // Time to wait for oracles to come up (HACK) + fChain = 1 // fChain value for all the chains + oracleLogLevel = zapcore.InfoLevel // Log level for the oracle / plugins. + ) + + t.Logf("creating %d universes", numChains) + homeChainUni, universes := createUniverses(t, numChains) + + var ( + oracles = make(map[uint64][]confighelper2.OracleIdentityExtra) + apps []chainlink.Application + nodes []*ocr3Node + p2pIDs [][32]byte + + // The bootstrap node will be: nodes[0] + bootstrapPort int + bootstrapP2PID p2pkey.PeerID + ) + + ports := freeport.GetN(t, numNodes) + ctx := testutils.Context(t) + callCtx := &bind.CallOpts{Context: ctx} + + for i := 0; i < numNodes; i++ { + t.Logf("Setting up ocr3 node:%d at port:%d", i, ports[i]) + node := setupNodeOCR3(t, ports[i], universes, homeChainUni, oracleLogLevel) + + for chainID, transmitter := range node.transmitters { + identity := confighelper2.OracleIdentityExtra{ + OracleIdentity: confighelper2.OracleIdentity{ + OnchainPublicKey: node.keybundle.PublicKey(), // Different for each chain + TransmitAccount: ocrtypes.Account(transmitter.Hex()), + OffchainPublicKey: node.keybundle.OffchainPublicKey(), // Same for each family + PeerID: node.peerID, + }, + ConfigEncryptionPublicKey: node.keybundle.ConfigEncryptionPublicKey(), // Different for each chain + } + oracles[chainID] = append(oracles[chainID], identity) + } + + apps = append(apps, node.app) + nodes = append(nodes, node) + + peerID, err := p2pkey.MakePeerID(node.peerID) + require.NoError(t, err) + p2pIDs = append(p2pIDs, peerID) + } + + bootstrapPort = ports[0] + bootstrapP2PID = p2pIDs[0] + bootstrapAddr := fmt.Sprintf("127.0.0.1:%d", bootstrapPort) + t.Logf("[bootstrap node] peerID:%s p2pID:%d address:%s", nodes[0].peerID, bootstrapP2PID, bootstrapAddr) + + // Start committing periodically in the background for all the chains + tick := time.NewTicker(simulatedBackendBlockTime) + defer tick.Stop() + commitBlocksBackground(t, universes, tick) + + ccipCapabilityID, err := homeChainUni.capabilityRegistry.GetHashedCapabilityId( + callCtx, CapabilityLabelledName, CapabilityVersion) + require.NoError(t, err, "failed to get hashed capability id for ccip") + require.NotEqual(t, [32]byte{}, ccipCapabilityID, "ccip capability id is empty") + + // Need to Add nodes and assign capabilities to them before creating DONS + homeChainUni.AddNodes(t, p2pIDs, [][32]byte{ccipCapabilityID}) + + for _, uni := range universes { + t.Logf("Adding chainconfig for chain %d", uni.chainID) + AddChainConfig(t, homeChainUni, getSelector(uni.chainID), p2pIDs, fChain) + } + + cfgs, err := homeChainUni.ccipConfig.GetAllChainConfigs(callCtx) + require.NoError(t, err) + require.Len(t, cfgs, numChains) + + // Create a DON for each chain + for _, uni := range universes { + // Add nodes and give them the capability + t.Log("Adding DON for universe: ", uni.chainID) + chainSelector := getSelector(uni.chainID) + homeChainUni.AddDON( + t, + ccipCapabilityID, + chainSelector, + uni, + fChain, + bootstrapP2PID, + p2pIDs, + oracles[uni.chainID], + ) + } + + t.Log("Creating ocr3 jobs, starting oracles") + for i := 0; i < len(nodes); i++ { + err1 := nodes[i].app.Start(ctx) + require.NoError(t, err1) + tApp := apps[i] + t.Cleanup(func() { require.NoError(t, tApp.Stop()) }) + + jb := mustGetJobSpec(t, bootstrapP2PID, bootstrapPort, nodes[i].peerID, nodes[i].keybundle.ID()) + require.NoErrorf(t, tApp.AddJobV2(ctx, &jb), "Wasn't able to create ccip job for node %d", i) + } + + t.Logf("Sending ccip requests from each chain to all other chains") + for _, uni := range universes { + requests := genRequestData(uni.chainID, universes) + uni.SendCCIPRequests(t, requests) + } + + // Wait for the oracles to come up. + // TODO: We need some data driven way to do this e.g. wait until LP filters to be registered. + time.Sleep(oraclesBootWaitTime) + + // Replay the log poller on all the chains so that the logs are in the db. + // otherwise the plugins won't pick them up. + for _, node := range nodes { + for chainID := range universes { + t.Logf("Replaying logs for chain %d from block %d", chainID, 1) + require.NoError(t, node.app.ReplayFromBlock(big.NewInt(int64(chainID)), 1, false), "failed to replay logs") + } + } + + // with only one request sent from each chain to each other chain, + // and with sequence numbers on incrementing by 1 on a per-dest chain + // basis, we expect the min sequence number to be 1 on all chains. + expectedSeqNrRange := ccipocr3.NewSeqNumRange(1, 1) + var wg sync.WaitGroup + for _, uni := range universes { + for remoteSelector := range universes { + if remoteSelector == uni.chainID { + continue + } + wg.Add(1) + go func(uni onchainUniverse, remoteSelector uint64) { + defer wg.Done() + waitForCommitWithInterval(t, uni, getSelector(remoteSelector), expectedSeqNrRange) + }(uni, remoteSelector) + } + } + + start := time.Now() + wg.Wait() + t.Logf("All chains received the expected commit report in %s", time.Since(start)) + + // with only one request sent from each chain to each other chain, + // all ExecutionStateChanged events should have the sequence number 1. + expectedSeqNr := uint64(1) + for _, uni := range universes { + for remoteSelector := range universes { + if remoteSelector == uni.chainID { + continue + } + wg.Add(1) + go func(uni onchainUniverse, remoteSelector uint64) { + defer wg.Done() + waitForExecWithSeqNr(t, uni, getSelector(remoteSelector), expectedSeqNr) + }(uni, remoteSelector) + } + } + + start = time.Now() + wg.Wait() + t.Logf("All chains received the expected ExecutionStateChanged event in %s", time.Since(start)) +} + +func genRequestData(chainID uint64, universes map[uint64]onchainUniverse) []requestData { + var res []requestData + for destChainID, destUni := range universes { + if destChainID == chainID { + continue + } + res = append(res, requestData{ + destChainSelector: getSelector(destChainID), + receiverAddress: destUni.receiver.Address(), + data: []byte(fmt.Sprintf("msg from chain %d to chain %d", chainID, destChainID)), + }) + } + return res +} + +func waitForCommitWithInterval( + t *testing.T, + uni onchainUniverse, + expectedSourceChainSelector uint64, + expectedSeqNumRange ccipocr3.SeqNumRange, +) { + sink := make(chan *evm_2_evm_multi_offramp.EVM2EVMMultiOffRampCommitReportAccepted) + subscription, err := uni.offramp.WatchCommitReportAccepted(&bind.WatchOpts{ + Context: testutils.Context(t), + }, sink) + require.NoError(t, err) + + for { + select { + case <-time.After(10 * time.Second): + t.Logf("Waiting for commit report on chain id %d (selector %d) from source selector %d expected seq nr range %s", + uni.chainID, getSelector(uni.chainID), expectedSourceChainSelector, expectedSeqNumRange.String()) + case subErr := <-subscription.Err(): + t.Fatalf("Subscription error: %+v", subErr) + case report := <-sink: + if len(report.Report.MerkleRoots) > 0 { + // Check the interval of sequence numbers and make sure it matches + // the expected range. + for _, mr := range report.Report.MerkleRoots { + if mr.SourceChainSelector == expectedSourceChainSelector && + uint64(expectedSeqNumRange.Start()) == mr.Interval.Min && + uint64(expectedSeqNumRange.End()) == mr.Interval.Max { + t.Logf("Received commit report on chain id %d (selector %d) from source selector %d expected seq nr range %s", + uni.chainID, getSelector(uni.chainID), expectedSourceChainSelector, expectedSeqNumRange.String()) + return + } + } + } + } + } +} + +func waitForExecWithSeqNr(t *testing.T, uni onchainUniverse, expectedSourceChainSelector, expectedSeqNr uint64) { + for { + scc, err := uni.offramp.GetSourceChainConfig(nil, expectedSourceChainSelector) + require.NoError(t, err) + t.Logf("Waiting for ExecutionStateChanged on chain %d (selector %d) from chain %d with expected sequence number %d, current onchain minSeqNr: %d", + uni.chainID, getSelector(uni.chainID), expectedSourceChainSelector, expectedSeqNr, scc.MinSeqNr) + iter, err := uni.offramp.FilterExecutionStateChanged(nil, []uint64{expectedSourceChainSelector}, []uint64{expectedSeqNr}, nil) + require.NoError(t, err) + var count int + for iter.Next() { + if iter.Event.SequenceNumber == expectedSeqNr && iter.Event.SourceChainSelector == expectedSourceChainSelector { + count++ + } + } + if count == 1 { + t.Logf("Received ExecutionStateChanged on chain %d (selector %d) from chain %d with expected sequence number %d", + uni.chainID, getSelector(uni.chainID), expectedSourceChainSelector, expectedSeqNr) + return + } + time.Sleep(5 * time.Second) + } +} diff --git a/core/capabilities/ccip/ccip_integration_tests/ocr_node_helper.go b/core/capabilities/ccip/ccip_integration_tests/ocr_node_helper.go new file mode 100644 index 00000000000..75b0e0ee947 --- /dev/null +++ b/core/capabilities/ccip/ccip_integration_tests/ocr_node_helper.go @@ -0,0 +1,316 @@ +package ccip_integration_tests + +import ( + "context" + "fmt" + "math/big" + "net/http" + "strconv" + "sync" + "testing" + "time" + + coretypes "github.com/smartcontractkit/chainlink-common/pkg/types/core/mocks" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/validate" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" + "github.com/ethereum/go-ethereum/common" + gethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/jmoiron/sqlx" + + "github.com/smartcontractkit/chainlink/v2/core/services/job" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" + + "github.com/smartcontractkit/chainlink-common/pkg/config" + "github.com/smartcontractkit/chainlink-common/pkg/loop" + "github.com/smartcontractkit/chainlink-common/pkg/utils/mailbox" + "github.com/smartcontractkit/chainlink/v2/core/services/relay" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + v2toml "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/toml" + evmutils "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big" + "github.com/smartcontractkit/chainlink/v2/core/chains/legacyevm" + configv2 "github.com/smartcontractkit/chainlink/v2/core/config/toml" + "github.com/smartcontractkit/chainlink/v2/core/internal/cltest/heavyweight" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/logger/audit" + "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/chaintype" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ocr2key" + "github.com/smartcontractkit/chainlink/v2/core/utils" + "github.com/smartcontractkit/chainlink/v2/plugins" + + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" +) + +type ocr3Node struct { + app chainlink.Application + peerID string + transmitters map[uint64]common.Address + keybundle ocr2key.KeyBundle + db *sqlx.DB +} + +// setupNodeOCR3 creates a chainlink node and any associated keys in order to run +// ccip. +func setupNodeOCR3( + t *testing.T, + port int, + universes map[uint64]onchainUniverse, + homeChainUniverse homeChain, + logLevel zapcore.Level, +) *ocr3Node { + // Do not want to load fixtures as they contain a dummy chainID. + cfg, db := heavyweight.FullTestDBNoFixturesV2(t, func(c *chainlink.Config, s *chainlink.Secrets) { + c.Insecure.OCRDevelopmentMode = ptr(true) // Disables ocr spec validation so we can have fast polling for the test. + + c.Feature.LogPoller = ptr(true) + + // P2P V2 configs. + c.P2P.V2.Enabled = ptr(true) + c.P2P.V2.DeltaDial = config.MustNewDuration(500 * time.Millisecond) + c.P2P.V2.DeltaReconcile = config.MustNewDuration(5 * time.Second) + c.P2P.V2.ListenAddresses = &[]string{fmt.Sprintf("127.0.0.1:%d", port)} + + // Enable Capabilities, This is a pre-requisite for registrySyncer to work. + c.Capabilities.ExternalRegistry.NetworkID = ptr(relay.NetworkEVM) + c.Capabilities.ExternalRegistry.ChainID = ptr(strconv.FormatUint(homeChainUniverse.chainID, 10)) + c.Capabilities.ExternalRegistry.Address = ptr(homeChainUniverse.capabilityRegistry.Address().String()) + + // OCR configs + c.OCR.Enabled = ptr(false) + c.OCR.DefaultTransactionQueueDepth = ptr(uint32(200)) + c.OCR2.Enabled = ptr(true) + c.OCR2.ContractPollInterval = config.MustNewDuration(5 * time.Second) + + c.Log.Level = ptr(configv2.LogLevel(logLevel)) + + var chains v2toml.EVMConfigs + for chainID := range universes { + chains = append(chains, createConfigV2Chain(uBigInt(chainID))) + } + c.EVM = chains + }) + + lggr := logger.TestLogger(t) + lggr.SetLogLevel(logLevel) + ctx := testutils.Context(t) + clients := make(map[uint64]client.Client) + + for chainID, uni := range universes { + clients[chainID] = client.NewSimulatedBackendClient(t, uni.backend, uBigInt(chainID)) + } + + master := keystore.New(db, utils.FastScryptParams, lggr) + + kStore := KeystoreSim{ + eks: &EthKeystoreSim{ + Eth: master.Eth(), + t: t, + }, + csa: master.CSA(), + } + mailMon := mailbox.NewMonitor("ccip", lggr.Named("mailbox")) + evmOpts := chainlink.EVMFactoryConfig{ + ChainOpts: legacyevm.ChainOpts{ + AppConfig: cfg, + GenEthClient: func(i *big.Int) client.Client { + client, ok := clients[i.Uint64()] + if !ok { + t.Fatal("no backend for chainID", i) + } + return client + }, + MailMon: mailMon, + DS: db, + }, + CSAETHKeystore: kStore, + } + relayerFactory := chainlink.RelayerFactory{ + Logger: lggr, + LoopRegistry: plugins.NewLoopRegistry(lggr.Named("LoopRegistry"), cfg.Tracing()), + GRPCOpts: loop.GRPCOpts{}, + CapabilitiesRegistry: coretypes.NewCapabilitiesRegistry(t), + } + initOps := []chainlink.CoreRelayerChainInitFunc{chainlink.InitEVM(testutils.Context(t), relayerFactory, evmOpts)} + rci, err := chainlink.NewCoreRelayerChainInteroperators(initOps...) + require.NoError(t, err) + + app, err := chainlink.NewApplication(chainlink.ApplicationOpts{ + Config: cfg, + DS: db, + KeyStore: master, + RelayerChainInteroperators: rci, + Logger: lggr, + ExternalInitiatorManager: nil, + CloseLogger: lggr.Sync, + UnrestrictedHTTPClient: &http.Client{}, + RestrictedHTTPClient: &http.Client{}, + AuditLogger: audit.NoopLogger, + MailMon: mailMon, + LoopRegistry: plugins.NewLoopRegistry(lggr, cfg.Tracing()), + }) + require.NoError(t, err) + require.NoError(t, app.GetKeyStore().Unlock(ctx, "password")) + _, err = app.GetKeyStore().P2P().Create(ctx) + require.NoError(t, err) + + p2pIDs, err := app.GetKeyStore().P2P().GetAll() + require.NoError(t, err) + require.Len(t, p2pIDs, 1) + peerID := p2pIDs[0].PeerID() + // create a transmitter for each chain + transmitters := make(map[uint64]common.Address) + for chainID, uni := range universes { + backend := uni.backend + owner := uni.owner + cID := uBigInt(chainID) + addrs, err2 := app.GetKeyStore().Eth().EnabledAddressesForChain(testutils.Context(t), cID) + require.NoError(t, err2) + if len(addrs) == 1 { + // just fund the address + fundAddress(t, owner, addrs[0], assets.Ether(10).ToInt(), backend) + transmitters[chainID] = addrs[0] + } else { + // create key and fund it + _, err3 := app.GetKeyStore().Eth().Create(testutils.Context(t), cID) + require.NoError(t, err3, "failed to create key for chain", chainID) + sendingKeys, err3 := app.GetKeyStore().Eth().EnabledAddressesForChain(testutils.Context(t), cID) + require.NoError(t, err3) + require.Len(t, sendingKeys, 1) + fundAddress(t, owner, sendingKeys[0], assets.Ether(10).ToInt(), backend) + transmitters[chainID] = sendingKeys[0] + } + } + require.Len(t, transmitters, len(universes)) + + keybundle, err := app.GetKeyStore().OCR2().Create(ctx, chaintype.EVM) + require.NoError(t, err) + + t.Cleanup(func() { + require.NoError(t, db.Close()) + }) + + return &ocr3Node{ + // can't use this app because it doesn't have the right toml config + // missing bootstrapp + app: app, + peerID: peerID.Raw(), + transmitters: transmitters, + keybundle: keybundle, + db: db, + } +} + +func ptr[T any](v T) *T { return &v } + +var _ keystore.Eth = &EthKeystoreSim{} + +type EthKeystoreSim struct { + keystore.Eth + t *testing.T +} + +// override +func (e *EthKeystoreSim) SignTx(ctx context.Context, address common.Address, tx *gethtypes.Transaction, chainID *big.Int) (*gethtypes.Transaction, error) { + // always sign with chain id 1337 for the simulated backend + return e.Eth.SignTx(ctx, address, tx, big.NewInt(1337)) +} + +type KeystoreSim struct { + eks keystore.Eth + csa keystore.CSA +} + +func (e KeystoreSim) Eth() keystore.Eth { + return e.eks +} + +func (e KeystoreSim) CSA() keystore.CSA { + return e.csa +} + +func fundAddress(t *testing.T, from *bind.TransactOpts, to common.Address, amount *big.Int, backend *backends.SimulatedBackend) { + nonce, err := backend.PendingNonceAt(testutils.Context(t), from.From) + require.NoError(t, err) + gp, err := backend.SuggestGasPrice(testutils.Context(t)) + require.NoError(t, err) + rawTx := gethtypes.NewTx(&gethtypes.LegacyTx{ + Nonce: nonce, + GasPrice: gp, + Gas: 21000, + To: &to, + Value: amount, + }) + signedTx, err := from.Signer(from.From, rawTx) + require.NoError(t, err) + err = backend.SendTransaction(testutils.Context(t), signedTx) + require.NoError(t, err) + backend.Commit() +} + +func createConfigV2Chain(chainID *big.Int) *v2toml.EVMConfig { + chain := v2toml.Defaults((*evmutils.Big)(chainID)) + chain.GasEstimator.LimitDefault = ptr(uint64(5e6)) + chain.LogPollInterval = config.MustNewDuration(100 * time.Millisecond) + chain.Transactions.ForwardersEnabled = ptr(false) + chain.FinalityDepth = ptr(uint32(2)) + return &v2toml.EVMConfig{ + ChainID: (*evmutils.Big)(chainID), + Enabled: ptr(true), + Chain: chain, + Nodes: v2toml.EVMNodes{&v2toml.Node{}}, + } +} + +// Commit blocks periodically in the background for all chains +func commitBlocksBackground(t *testing.T, universes map[uint64]onchainUniverse, tick *time.Ticker) { + t.Log("starting ticker to commit blocks") + tickCtx, tickCancel := context.WithCancel(testutils.Context(t)) + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + for { + select { + case <-tick.C: + for _, uni := range universes { + uni.backend.Commit() + } + case <-tickCtx.Done(): + return + } + } + }() + t.Cleanup(func() { + tickCancel() + wg.Wait() + }) +} + +// p2pKeyID: nodes p2p id +// ocrKeyBundleID: nodes ocr key bundle id +func mustGetJobSpec(t *testing.T, bootstrapP2PID p2pkey.PeerID, bootstrapPort int, p2pKeyID string, ocrKeyBundleID string) job.Job { + specArgs := validate.SpecArgs{ + P2PV2Bootstrappers: []string{ + fmt.Sprintf("%s@127.0.0.1:%d", bootstrapP2PID.Raw(), bootstrapPort), + }, + CapabilityVersion: CapabilityVersion, + CapabilityLabelledName: CapabilityLabelledName, + OCRKeyBundleIDs: map[string]string{ + relay.NetworkEVM: ocrKeyBundleID, + }, + P2PKeyID: p2pKeyID, + PluginConfig: map[string]any{}, + } + specToml, err := validate.NewCCIPSpecToml(specArgs) + require.NoError(t, err) + jb, err := validate.ValidatedCCIPSpec(specToml) + require.NoError(t, err) + return jb +} diff --git a/core/capabilities/ccip/ccip_integration_tests/ping_pong_test.go b/core/capabilities/ccip/ccip_integration_tests/ping_pong_test.go new file mode 100644 index 00000000000..8a65ff5167d --- /dev/null +++ b/core/capabilities/ccip/ccip_integration_tests/ping_pong_test.go @@ -0,0 +1,95 @@ +package ccip_integration_tests + +import ( + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + gethcommon "github.com/ethereum/go-ethereum/common" + + "github.com/stretchr/testify/require" + + "golang.org/x/exp/maps" + + pp "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/ping_pong_demo" +) + +/* +* Test is setting up 3 chains (let's call them A, B, C), each chain deploys and starts 2 ping pong contracts for the other 2. +* A ---deploy+start---> (pingPongB, pingPongC) +* B ---deploy+start---> (pingPongA, pingPongC) +* C ---deploy+start---> (pingPongA, pingPongB) +* and then checks that each ping pong contract emitted `CCIPSendRequested` event from the expected source to destination. +* Test fails if any wiring between contracts is not correct. + */ +func TestPingPong(t *testing.T) { + _, universes := createUniverses(t, 3) + pingPongs := initializePingPongContracts(t, universes) + for chainID, universe := range universes { + for otherChain, pingPong := range pingPongs[chainID] { + t.Log("PingPong From: ", chainID, " To: ", otherChain) + _, err := pingPong.StartPingPong(universe.owner) + require.NoError(t, err) + universe.backend.Commit() + + logIter, err := universe.onramp.FilterCCIPSendRequested(&bind.FilterOpts{Start: 0}, nil) + require.NoError(t, err) + // Iterate until latest event + for logIter.Next() { + } + log := logIter.Event + require.Equal(t, getSelector(otherChain), log.DestChainSelector) + require.Equal(t, pingPong.Address(), log.Message.Sender) + chainPingPongAddr := pingPongs[otherChain][chainID].Address().Bytes() + // With chain agnostic addresses we need to pad the address to the correct length if the receiver is zero prefixed + paddedAddr := gethcommon.LeftPadBytes(chainPingPongAddr, len(log.Message.Receiver)) + require.Equal(t, paddedAddr, log.Message.Receiver) + } + } +} + +// InitializeContracts initializes ping pong contracts on all chains and +// connects them all to each other. +func initializePingPongContracts( + t *testing.T, + chainUniverses map[uint64]onchainUniverse, +) map[uint64]map[uint64]*pp.PingPongDemo { + pingPongs := make(map[uint64]map[uint64]*pp.PingPongDemo) + chainIDs := maps.Keys(chainUniverses) + // For each chain initialize N ping pong contracts, where N is the (number of chains - 1) + for chainID, universe := range chainUniverses { + pingPongs[chainID] = make(map[uint64]*pp.PingPongDemo) + for _, chainToConnect := range chainIDs { + if chainToConnect == chainID { + continue // don't connect chain to itself + } + backend := universe.backend + owner := universe.owner + pingPongAddr, _, _, err := pp.DeployPingPongDemo(owner, backend, universe.router.Address(), universe.linkToken.Address()) + require.NoError(t, err) + backend.Commit() + pingPong, err := pp.NewPingPongDemo(pingPongAddr, backend) + require.NoError(t, err) + backend.Commit() + // Fund the ping pong contract with LINK + _, err = universe.linkToken.Transfer(owner, pingPong.Address(), e18Mult(10)) + backend.Commit() + require.NoError(t, err) + pingPongs[chainID][chainToConnect] = pingPong + } + } + + // Set up each ping pong contract to its counterpart on the other chain + for chainID, universe := range chainUniverses { + for chainToConnect, pingPong := range pingPongs[chainID] { + _, err := pingPong.SetCounterpart( + universe.owner, + getSelector(chainUniverses[chainToConnect].chainID), + // This is the address of the ping pong contract on the other chain + pingPongs[chainToConnect][chainID].Address(), + ) + require.NoError(t, err) + universe.backend.Commit() + } + } + return pingPongs +} diff --git a/core/capabilities/ccip/ccipevm/commitcodec.go b/core/capabilities/ccip/ccipevm/commitcodec.go new file mode 100644 index 00000000000..928cecd0a41 --- /dev/null +++ b/core/capabilities/ccip/ccipevm/commitcodec.go @@ -0,0 +1,138 @@ +package ccipevm + +import ( + "context" + "fmt" + "math/big" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_multi_offramp" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" +) + +// CommitPluginCodecV1 is a codec for encoding and decoding commit plugin reports. +// Compatible with: +// - "EVM2EVMMultiOffRamp 1.6.0-dev" +type CommitPluginCodecV1 struct { + commitReportAcceptedEventInputs abi.Arguments +} + +func NewCommitPluginCodecV1() *CommitPluginCodecV1 { + abiParsed, err := abi.JSON(strings.NewReader(evm_2_evm_multi_offramp.EVM2EVMMultiOffRampABI)) + if err != nil { + panic(fmt.Errorf("parse multi offramp abi: %s", err)) + } + eventInputs := abihelpers.MustGetEventInputs("CommitReportAccepted", abiParsed) + return &CommitPluginCodecV1{commitReportAcceptedEventInputs: eventInputs} +} + +func (c *CommitPluginCodecV1) Encode(ctx context.Context, report cciptypes.CommitPluginReport) ([]byte, error) { + merkleRoots := make([]evm_2_evm_multi_offramp.EVM2EVMMultiOffRampMerkleRoot, 0, len(report.MerkleRoots)) + for _, root := range report.MerkleRoots { + merkleRoots = append(merkleRoots, evm_2_evm_multi_offramp.EVM2EVMMultiOffRampMerkleRoot{ + SourceChainSelector: uint64(root.ChainSel), + Interval: evm_2_evm_multi_offramp.EVM2EVMMultiOffRampInterval{ + Min: uint64(root.SeqNumsRange.Start()), + Max: uint64(root.SeqNumsRange.End()), + }, + MerkleRoot: root.MerkleRoot, + }) + } + + tokenPriceUpdates := make([]evm_2_evm_multi_offramp.InternalTokenPriceUpdate, 0, len(report.PriceUpdates.TokenPriceUpdates)) + for _, update := range report.PriceUpdates.TokenPriceUpdates { + if !common.IsHexAddress(string(update.TokenID)) { + return nil, fmt.Errorf("invalid token address: %s", update.TokenID) + } + if update.Price.IsEmpty() { + return nil, fmt.Errorf("empty price for token: %s", update.TokenID) + } + tokenPriceUpdates = append(tokenPriceUpdates, evm_2_evm_multi_offramp.InternalTokenPriceUpdate{ + SourceToken: common.HexToAddress(string(update.TokenID)), + UsdPerToken: update.Price.Int, + }) + } + + gasPriceUpdates := make([]evm_2_evm_multi_offramp.InternalGasPriceUpdate, 0, len(report.PriceUpdates.GasPriceUpdates)) + for _, update := range report.PriceUpdates.GasPriceUpdates { + if update.GasPrice.IsEmpty() { + return nil, fmt.Errorf("empty gas price for chain: %d", update.ChainSel) + } + + gasPriceUpdates = append(gasPriceUpdates, evm_2_evm_multi_offramp.InternalGasPriceUpdate{ + DestChainSelector: uint64(update.ChainSel), + UsdPerUnitGas: update.GasPrice.Int, + }) + } + + evmReport := evm_2_evm_multi_offramp.EVM2EVMMultiOffRampCommitReport{ + PriceUpdates: evm_2_evm_multi_offramp.InternalPriceUpdates{ + TokenPriceUpdates: tokenPriceUpdates, + GasPriceUpdates: gasPriceUpdates, + }, + MerkleRoots: merkleRoots, + } + + return c.commitReportAcceptedEventInputs.PackValues([]interface{}{evmReport}) +} + +func (c *CommitPluginCodecV1) Decode(ctx context.Context, bytes []byte) (cciptypes.CommitPluginReport, error) { + unpacked, err := c.commitReportAcceptedEventInputs.Unpack(bytes) + if err != nil { + return cciptypes.CommitPluginReport{}, err + } + if len(unpacked) != 1 { + return cciptypes.CommitPluginReport{}, fmt.Errorf("expected 1 argument, got %d", len(unpacked)) + } + + commitReportRaw := abi.ConvertType(unpacked[0], new(evm_2_evm_multi_offramp.EVM2EVMMultiOffRampCommitReport)) + commitReport, is := commitReportRaw.(*evm_2_evm_multi_offramp.EVM2EVMMultiOffRampCommitReport) + if !is { + return cciptypes.CommitPluginReport{}, + fmt.Errorf("expected EVM2EVMMultiOffRampCommitReport, got %T", unpacked[0]) + } + + merkleRoots := make([]cciptypes.MerkleRootChain, 0, len(commitReport.MerkleRoots)) + for _, root := range commitReport.MerkleRoots { + merkleRoots = append(merkleRoots, cciptypes.MerkleRootChain{ + ChainSel: cciptypes.ChainSelector(root.SourceChainSelector), + SeqNumsRange: cciptypes.NewSeqNumRange( + cciptypes.SeqNum(root.Interval.Min), + cciptypes.SeqNum(root.Interval.Max), + ), + MerkleRoot: root.MerkleRoot, + }) + } + + tokenPriceUpdates := make([]cciptypes.TokenPrice, 0, len(commitReport.PriceUpdates.TokenPriceUpdates)) + for _, update := range commitReport.PriceUpdates.TokenPriceUpdates { + tokenPriceUpdates = append(tokenPriceUpdates, cciptypes.TokenPrice{ + TokenID: types.Account(update.SourceToken.String()), + Price: cciptypes.NewBigInt(big.NewInt(0).Set(update.UsdPerToken)), + }) + } + + gasPriceUpdates := make([]cciptypes.GasPriceChain, 0, len(commitReport.PriceUpdates.GasPriceUpdates)) + for _, update := range commitReport.PriceUpdates.GasPriceUpdates { + gasPriceUpdates = append(gasPriceUpdates, cciptypes.GasPriceChain{ + GasPrice: cciptypes.NewBigInt(big.NewInt(0).Set(update.UsdPerUnitGas)), + ChainSel: cciptypes.ChainSelector(update.DestChainSelector), + }) + } + + return cciptypes.CommitPluginReport{ + MerkleRoots: merkleRoots, + PriceUpdates: cciptypes.PriceUpdates{ + TokenPriceUpdates: tokenPriceUpdates, + GasPriceUpdates: gasPriceUpdates, + }, + }, nil +} + +// Ensure CommitPluginCodec implements the CommitPluginCodec interface +var _ cciptypes.CommitPluginCodec = (*CommitPluginCodecV1)(nil) diff --git a/core/capabilities/ccip/ccipevm/commitcodec_test.go b/core/capabilities/ccip/ccipevm/commitcodec_test.go new file mode 100644 index 00000000000..737f7be1d6e --- /dev/null +++ b/core/capabilities/ccip/ccipevm/commitcodec_test.go @@ -0,0 +1,135 @@ +package ccipevm + +import ( + "math/big" + "math/rand" + "testing" + + "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" +) + +var randomCommitReport = func() cciptypes.CommitPluginReport { + return cciptypes.CommitPluginReport{ + MerkleRoots: []cciptypes.MerkleRootChain{ + { + ChainSel: cciptypes.ChainSelector(rand.Uint64()), + SeqNumsRange: cciptypes.NewSeqNumRange( + cciptypes.SeqNum(rand.Uint64()), + cciptypes.SeqNum(rand.Uint64()), + ), + MerkleRoot: utils.RandomBytes32(), + }, + { + ChainSel: cciptypes.ChainSelector(rand.Uint64()), + SeqNumsRange: cciptypes.NewSeqNumRange( + cciptypes.SeqNum(rand.Uint64()), + cciptypes.SeqNum(rand.Uint64()), + ), + MerkleRoot: utils.RandomBytes32(), + }, + }, + PriceUpdates: cciptypes.PriceUpdates{ + TokenPriceUpdates: []cciptypes.TokenPrice{ + { + TokenID: types.Account(utils.RandomAddress().String()), + Price: cciptypes.NewBigInt(utils.RandUint256()), + }, + }, + GasPriceUpdates: []cciptypes.GasPriceChain{ + {GasPrice: cciptypes.NewBigInt(utils.RandUint256()), ChainSel: cciptypes.ChainSelector(rand.Uint64())}, + {GasPrice: cciptypes.NewBigInt(utils.RandUint256()), ChainSel: cciptypes.ChainSelector(rand.Uint64())}, + {GasPrice: cciptypes.NewBigInt(utils.RandUint256()), ChainSel: cciptypes.ChainSelector(rand.Uint64())}, + }, + }, + } +} + +func TestCommitPluginCodecV1(t *testing.T) { + testCases := []struct { + name string + report func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport + expErr bool + }{ + { + name: "base report", + report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport { + return report + }, + }, + { + name: "empty token address", + report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport { + report.PriceUpdates.TokenPriceUpdates[0].TokenID = "" + return report + }, + expErr: true, + }, + { + name: "empty merkle root", + report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport { + report.MerkleRoots[0].MerkleRoot = cciptypes.Bytes32{} + return report + }, + }, + { + name: "zero token price", + report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport { + report.PriceUpdates.TokenPriceUpdates[0].Price = cciptypes.NewBigInt(big.NewInt(0)) + return report + }, + }, + { + name: "zero gas price", + report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport { + report.PriceUpdates.GasPriceUpdates[0].GasPrice = cciptypes.NewBigInt(big.NewInt(0)) + return report + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + report := tc.report(randomCommitReport()) + commitCodec := NewCommitPluginCodecV1() + ctx := testutils.Context(t) + encodedReport, err := commitCodec.Encode(ctx, report) + if tc.expErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + decodedReport, err := commitCodec.Decode(ctx, encodedReport) + require.NoError(t, err) + require.Equal(t, report, decodedReport) + }) + } +} + +func BenchmarkCommitPluginCodecV1_Encode(b *testing.B) { + commitCodec := NewCommitPluginCodecV1() + ctx := testutils.Context(b) + + rep := randomCommitReport() + for i := 0; i < b.N; i++ { + _, err := commitCodec.Encode(ctx, rep) + require.NoError(b, err) + } +} + +func BenchmarkCommitPluginCodecV1_Decode(b *testing.B) { + commitCodec := NewCommitPluginCodecV1() + ctx := testutils.Context(b) + encodedReport, err := commitCodec.Encode(ctx, randomCommitReport()) + require.NoError(b, err) + + for i := 0; i < b.N; i++ { + _, err := commitCodec.Decode(ctx, encodedReport) + require.NoError(b, err) + } +} diff --git a/core/capabilities/ccip/ccipevm/executecodec.go b/core/capabilities/ccip/ccipevm/executecodec.go new file mode 100644 index 00000000000..a64c775112c --- /dev/null +++ b/core/capabilities/ccip/ccipevm/executecodec.go @@ -0,0 +1,181 @@ +package ccipevm + +import ( + "context" + "fmt" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_multi_offramp" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" +) + +// ExecutePluginCodecV1 is a codec for encoding and decoding execute plugin reports. +// Compatible with: +// - "EVM2EVMMultiOffRamp 1.6.0-dev" +type ExecutePluginCodecV1 struct { + executeReportMethodInputs abi.Arguments +} + +func NewExecutePluginCodecV1() *ExecutePluginCodecV1 { + abiParsed, err := abi.JSON(strings.NewReader(evm_2_evm_multi_offramp.EVM2EVMMultiOffRampABI)) + if err != nil { + panic(fmt.Errorf("parse multi offramp abi: %s", err)) + } + methodInputs := abihelpers.MustGetMethodInputs("manuallyExecute", abiParsed) + if len(methodInputs) == 0 { + panic("no inputs found for method: manuallyExecute") + } + + return &ExecutePluginCodecV1{ + executeReportMethodInputs: methodInputs[:1], + } +} + +func (e *ExecutePluginCodecV1) Encode(ctx context.Context, report cciptypes.ExecutePluginReport) ([]byte, error) { + evmReport := make([]evm_2_evm_multi_offramp.InternalExecutionReportSingleChain, 0, len(report.ChainReports)) + + for _, chainReport := range report.ChainReports { + if chainReport.ProofFlagBits.IsEmpty() { + return nil, fmt.Errorf("proof flag bits are empty") + } + + evmProofs := make([][32]byte, 0, len(chainReport.Proofs)) + for _, proof := range chainReport.Proofs { + evmProofs = append(evmProofs, proof) + } + + evmMessages := make([]evm_2_evm_multi_offramp.InternalAny2EVMRampMessage, 0, len(chainReport.Messages)) + for _, message := range chainReport.Messages { + receiver := common.BytesToAddress(message.Receiver) + + tokenAmounts := make([]evm_2_evm_multi_offramp.InternalRampTokenAmount, 0, len(message.TokenAmounts)) + for _, tokenAmount := range message.TokenAmounts { + if tokenAmount.Amount.IsEmpty() { + return nil, fmt.Errorf("empty amount for token: %s", tokenAmount.DestTokenAddress) + } + + tokenAmounts = append(tokenAmounts, evm_2_evm_multi_offramp.InternalRampTokenAmount{ + SourcePoolAddress: tokenAmount.SourcePoolAddress, + DestTokenAddress: tokenAmount.DestTokenAddress, + ExtraData: tokenAmount.ExtraData, + Amount: tokenAmount.Amount.Int, + }) + } + + gasLimit, err := decodeExtraArgsV1V2(message.ExtraArgs) + if err != nil { + return nil, fmt.Errorf("decode extra args to get gas limit: %w", err) + } + + evmMessages = append(evmMessages, evm_2_evm_multi_offramp.InternalAny2EVMRampMessage{ + Header: evm_2_evm_multi_offramp.InternalRampMessageHeader{ + MessageId: message.Header.MessageID, + SourceChainSelector: uint64(message.Header.SourceChainSelector), + DestChainSelector: uint64(message.Header.DestChainSelector), + SequenceNumber: uint64(message.Header.SequenceNumber), + Nonce: message.Header.Nonce, + }, + Sender: message.Sender, + Data: message.Data, + Receiver: receiver, + GasLimit: gasLimit, + TokenAmounts: tokenAmounts, + }) + } + + evmChainReport := evm_2_evm_multi_offramp.InternalExecutionReportSingleChain{ + SourceChainSelector: uint64(chainReport.SourceChainSelector), + Messages: evmMessages, + OffchainTokenData: chainReport.OffchainTokenData, + Proofs: evmProofs, + ProofFlagBits: chainReport.ProofFlagBits.Int, + } + evmReport = append(evmReport, evmChainReport) + } + + return e.executeReportMethodInputs.PackValues([]interface{}{&evmReport}) +} + +func (e *ExecutePluginCodecV1) Decode(ctx context.Context, encodedReport []byte) (cciptypes.ExecutePluginReport, error) { + unpacked, err := e.executeReportMethodInputs.Unpack(encodedReport) + if err != nil { + return cciptypes.ExecutePluginReport{}, fmt.Errorf("unpack encoded report: %w", err) + } + if len(unpacked) != 1 { + return cciptypes.ExecutePluginReport{}, fmt.Errorf("unpacked report is empty") + } + + evmReportRaw := abi.ConvertType(unpacked[0], new([]evm_2_evm_multi_offramp.InternalExecutionReportSingleChain)) + evmReportPtr, is := evmReportRaw.(*[]evm_2_evm_multi_offramp.InternalExecutionReportSingleChain) + if !is { + return cciptypes.ExecutePluginReport{}, fmt.Errorf("got an unexpected report type %T", unpacked[0]) + } + if evmReportPtr == nil { + return cciptypes.ExecutePluginReport{}, fmt.Errorf("evm report is nil") + } + + evmReport := *evmReportPtr + executeReport := cciptypes.ExecutePluginReport{ + ChainReports: make([]cciptypes.ExecutePluginReportSingleChain, 0, len(evmReport)), + } + + for _, evmChainReport := range evmReport { + proofs := make([]cciptypes.Bytes32, 0, len(evmChainReport.Proofs)) + for _, proof := range evmChainReport.Proofs { + proofs = append(proofs, proof) + } + + messages := make([]cciptypes.Message, 0, len(evmChainReport.Messages)) + for _, evmMessage := range evmChainReport.Messages { + tokenAmounts := make([]cciptypes.RampTokenAmount, 0, len(evmMessage.TokenAmounts)) + for _, tokenAmount := range evmMessage.TokenAmounts { + tokenAmounts = append(tokenAmounts, cciptypes.RampTokenAmount{ + SourcePoolAddress: tokenAmount.SourcePoolAddress, + DestTokenAddress: tokenAmount.DestTokenAddress, + ExtraData: tokenAmount.ExtraData, + Amount: cciptypes.NewBigInt(tokenAmount.Amount), + }) + } + + message := cciptypes.Message{ + Header: cciptypes.RampMessageHeader{ + MessageID: evmMessage.Header.MessageId, + SourceChainSelector: cciptypes.ChainSelector(evmMessage.Header.SourceChainSelector), + DestChainSelector: cciptypes.ChainSelector(evmMessage.Header.DestChainSelector), + SequenceNumber: cciptypes.SeqNum(evmMessage.Header.SequenceNumber), + Nonce: evmMessage.Header.Nonce, + MsgHash: cciptypes.Bytes32{}, // <-- todo: info not available, but not required atm + OnRamp: cciptypes.Bytes{}, // <-- todo: info not available, but not required atm + }, + Sender: evmMessage.Sender, + Data: evmMessage.Data, + Receiver: evmMessage.Receiver.Bytes(), + ExtraArgs: cciptypes.Bytes{}, // <-- todo: info not available, but not required atm + FeeToken: cciptypes.Bytes{}, // <-- todo: info not available, but not required atm + FeeTokenAmount: cciptypes.BigInt{}, // <-- todo: info not available, but not required atm + TokenAmounts: tokenAmounts, + } + messages = append(messages, message) + } + + chainReport := cciptypes.ExecutePluginReportSingleChain{ + SourceChainSelector: cciptypes.ChainSelector(evmChainReport.SourceChainSelector), + Messages: messages, + OffchainTokenData: evmChainReport.OffchainTokenData, + Proofs: proofs, + ProofFlagBits: cciptypes.NewBigInt(evmChainReport.ProofFlagBits), + } + + executeReport.ChainReports = append(executeReport.ChainReports, chainReport) + } + + return executeReport, nil +} + +// Ensure ExecutePluginCodec implements the ExecutePluginCodec interface +var _ cciptypes.ExecutePluginCodec = (*ExecutePluginCodecV1)(nil) diff --git a/core/capabilities/ccip/ccipevm/executecodec_test.go b/core/capabilities/ccip/ccipevm/executecodec_test.go new file mode 100644 index 00000000000..4f207fdb0e2 --- /dev/null +++ b/core/capabilities/ccip/ccipevm/executecodec_test.go @@ -0,0 +1,174 @@ +package ccipevm + +import ( + "math/rand" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" + "github.com/ethereum/go-ethereum/core" + + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/message_hasher" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/report_codec" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var randomExecuteReport = func(t *testing.T, d *testSetupData) cciptypes.ExecutePluginReport { + const numChainReports = 10 + const msgsPerReport = 10 + const numTokensPerMsg = 3 + + chainReports := make([]cciptypes.ExecutePluginReportSingleChain, numChainReports) + for i := 0; i < numChainReports; i++ { + reportMessages := make([]cciptypes.Message, msgsPerReport) + for j := 0; j < msgsPerReport; j++ { + data, err := cciptypes.NewBytesFromString(utils.RandomAddress().String()) + assert.NoError(t, err) + + tokenAmounts := make([]cciptypes.RampTokenAmount, numTokensPerMsg) + for z := 0; z < numTokensPerMsg; z++ { + tokenAmounts[z] = cciptypes.RampTokenAmount{ + SourcePoolAddress: utils.RandomAddress().Bytes(), + DestTokenAddress: utils.RandomAddress().Bytes(), + ExtraData: data, + Amount: cciptypes.NewBigInt(utils.RandUint256()), + } + } + + extraArgs, err := d.contract.EncodeEVMExtraArgsV1(nil, message_hasher.ClientEVMExtraArgsV1{ + GasLimit: utils.RandUint256(), + }) + assert.NoError(t, err) + + reportMessages[j] = cciptypes.Message{ + Header: cciptypes.RampMessageHeader{ + MessageID: utils.RandomBytes32(), + SourceChainSelector: cciptypes.ChainSelector(rand.Uint64()), + DestChainSelector: cciptypes.ChainSelector(rand.Uint64()), + SequenceNumber: cciptypes.SeqNum(rand.Uint64()), + Nonce: rand.Uint64(), + MsgHash: utils.RandomBytes32(), + OnRamp: utils.RandomAddress().Bytes(), + }, + Sender: utils.RandomAddress().Bytes(), + Data: data, + Receiver: utils.RandomAddress().Bytes(), + ExtraArgs: extraArgs, + FeeToken: utils.RandomAddress().Bytes(), + FeeTokenAmount: cciptypes.NewBigInt(utils.RandUint256()), + TokenAmounts: tokenAmounts, + } + } + + tokenData := make([][][]byte, numTokensPerMsg) + for j := 0; j < numTokensPerMsg; j++ { + tokenData[j] = [][]byte{{0x1}, {0x2, 0x3}} + } + + chainReports[i] = cciptypes.ExecutePluginReportSingleChain{ + SourceChainSelector: cciptypes.ChainSelector(rand.Uint64()), + Messages: reportMessages, + OffchainTokenData: tokenData, + Proofs: []cciptypes.Bytes32{utils.RandomBytes32(), utils.RandomBytes32()}, + ProofFlagBits: cciptypes.NewBigInt(utils.RandUint256()), + } + } + + return cciptypes.ExecutePluginReport{ChainReports: chainReports} +} + +func TestExecutePluginCodecV1(t *testing.T) { + d := testSetup(t) + + testCases := []struct { + name string + report func(report cciptypes.ExecutePluginReport) cciptypes.ExecutePluginReport + expErr bool + }{ + { + name: "base report", + report: func(report cciptypes.ExecutePluginReport) cciptypes.ExecutePluginReport { return report }, + expErr: false, + }, + { + name: "reports have empty msgs", + report: func(report cciptypes.ExecutePluginReport) cciptypes.ExecutePluginReport { + report.ChainReports[0].Messages = []cciptypes.Message{} + report.ChainReports[4].Messages = []cciptypes.Message{} + return report + }, + expErr: false, + }, + { + name: "reports have empty offchain token data", + report: func(report cciptypes.ExecutePluginReport) cciptypes.ExecutePluginReport { + report.ChainReports[0].OffchainTokenData = [][][]byte{} + report.ChainReports[4].OffchainTokenData[1] = [][]byte{} + return report + }, + expErr: false, + }, + } + + ctx := testutils.Context(t) + + // Deploy the contract + transactor := testutils.MustNewSimTransactor(t) + simulatedBackend := backends.NewSimulatedBackend(core.GenesisAlloc{ + transactor.From: {Balance: assets.Ether(1000).ToInt()}, + }, 30e6) + address, _, _, err := report_codec.DeployReportCodec(transactor, simulatedBackend) + require.NoError(t, err) + simulatedBackend.Commit() + contract, err := report_codec.NewReportCodec(address, simulatedBackend) + require.NoError(t, err) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + codec := NewExecutePluginCodecV1() + report := tc.report(randomExecuteReport(t, d)) + bytes, err := codec.Encode(ctx, report) + if tc.expErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + + testSetup(t) + + // ignore msg hash in comparison + for i := range report.ChainReports { + for j := range report.ChainReports[i].Messages { + report.ChainReports[i].Messages[j].Header.MsgHash = cciptypes.Bytes32{} + report.ChainReports[i].Messages[j].Header.OnRamp = cciptypes.Bytes{} + report.ChainReports[i].Messages[j].FeeToken = cciptypes.Bytes{} + report.ChainReports[i].Messages[j].ExtraArgs = cciptypes.Bytes{} + report.ChainReports[i].Messages[j].FeeTokenAmount = cciptypes.BigInt{} + } + } + + // decode using the contract + contractDecodedReport, err := contract.DecodeExecuteReport(&bind.CallOpts{Context: ctx}, bytes) + assert.NoError(t, err) + assert.Equal(t, len(report.ChainReports), len(contractDecodedReport)) + for i, expReport := range report.ChainReports { + actReport := contractDecodedReport[i] + assert.Equal(t, expReport.OffchainTokenData, actReport.OffchainTokenData) + assert.Equal(t, len(expReport.Messages), len(actReport.Messages)) + assert.Equal(t, uint64(expReport.SourceChainSelector), actReport.SourceChainSelector) + } + + // decode using the codec + codecDecoded, err := codec.Decode(ctx, bytes) + assert.NoError(t, err) + assert.Equal(t, report, codecDecoded) + }) + } +} diff --git a/core/capabilities/ccip/ccipevm/helpers.go b/core/capabilities/ccip/ccipevm/helpers.go new file mode 100644 index 00000000000..ee83230a4ce --- /dev/null +++ b/core/capabilities/ccip/ccipevm/helpers.go @@ -0,0 +1,33 @@ +package ccipevm + +import ( + "bytes" + "fmt" + "math/big" +) + +func decodeExtraArgsV1V2(extraArgs []byte) (gasLimit *big.Int, err error) { + if len(extraArgs) < 4 { + return nil, fmt.Errorf("extra args too short: %d, should be at least 4 (i.e the extraArgs tag)", len(extraArgs)) + } + + var method string + if bytes.Equal(extraArgs[:4], evmExtraArgsV1Tag) { + method = "decodeEVMExtraArgsV1" + } else if bytes.Equal(extraArgs[:4], evmExtraArgsV2Tag) { + method = "decodeEVMExtraArgsV2" + } else { + return nil, fmt.Errorf("unknown extra args tag: %x", extraArgs) + } + ifaces, err := messageHasherABI.Methods[method].Inputs.UnpackValues(extraArgs[4:]) + if err != nil { + return nil, fmt.Errorf("abi decode extra args v1: %w", err) + } + // gas limit is always the first argument, and allow OOO isn't set explicitly + // on the message. + _, ok := ifaces[0].(*big.Int) + if !ok { + return nil, fmt.Errorf("expected *big.Int, got %T", ifaces[0]) + } + return ifaces[0].(*big.Int), nil +} diff --git a/core/capabilities/ccip/ccipevm/helpers_test.go b/core/capabilities/ccip/ccipevm/helpers_test.go new file mode 100644 index 00000000000..95a5d4439bb --- /dev/null +++ b/core/capabilities/ccip/ccipevm/helpers_test.go @@ -0,0 +1,41 @@ +package ccipevm + +import ( + "math/big" + "math/rand" + "testing" + + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/message_hasher" + + "github.com/stretchr/testify/require" +) + +func Test_decodeExtraArgs(t *testing.T) { + d := testSetup(t) + gasLimit := big.NewInt(rand.Int63()) + + t.Run("v1", func(t *testing.T) { + encoded, err := d.contract.EncodeEVMExtraArgsV1(nil, message_hasher.ClientEVMExtraArgsV1{ + GasLimit: gasLimit, + }) + require.NoError(t, err) + + decodedGasLimit, err := decodeExtraArgsV1V2(encoded) + require.NoError(t, err) + + require.Equal(t, gasLimit, decodedGasLimit) + }) + + t.Run("v2", func(t *testing.T) { + encoded, err := d.contract.EncodeEVMExtraArgsV2(nil, message_hasher.ClientEVMExtraArgsV2{ + GasLimit: gasLimit, + AllowOutOfOrderExecution: true, + }) + require.NoError(t, err) + + decodedGasLimit, err := decodeExtraArgsV1V2(encoded) + require.NoError(t, err) + + require.Equal(t, gasLimit, decodedGasLimit) + }) +} diff --git a/core/capabilities/ccip/ccipevm/msghasher.go b/core/capabilities/ccip/ccipevm/msghasher.go new file mode 100644 index 00000000000..0df0a8254ac --- /dev/null +++ b/core/capabilities/ccip/ccipevm/msghasher.go @@ -0,0 +1,127 @@ +package ccipevm + +import ( + "context" + "fmt" + + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/message_hasher" +) + +var ( + // bytes32 internal constant LEAF_DOMAIN_SEPARATOR = 0x0000000000000000000000000000000000000000000000000000000000000000; + leafDomainSeparator = [32]byte{} + + // bytes32 internal constant ANY_2_EVM_MESSAGE_HASH = keccak256("Any2EVMMessageHashV1"); + ANY_2_EVM_MESSAGE_HASH = utils.Keccak256Fixed([]byte("Any2EVMMessageHashV1")) + + messageHasherABI = types.MustGetABI(message_hasher.MessageHasherABI) + + // bytes4 public constant EVM_EXTRA_ARGS_V1_TAG = 0x97a657c9; + evmExtraArgsV1Tag = hexutil.MustDecode("0x97a657c9") + + // bytes4 public constant EVM_EXTRA_ARGS_V2_TAG = 0x181dcf10; + evmExtraArgsV2Tag = hexutil.MustDecode("0x181dcf10") +) + +// MessageHasherV1 implements the MessageHasher interface. +// Compatible with: +// - "EVM2EVMMultiOnRamp 1.6.0-dev" +type MessageHasherV1 struct{} + +func NewMessageHasherV1() *MessageHasherV1 { + return &MessageHasherV1{} +} + +// Hash implements the MessageHasher interface. +// It constructs all of the inputs to the final keccak256 hash in Internal._hash(Any2EVMRampMessage). +// The main structure of the hash is as follows: +/* + keccak256( + leafDomainSeparator, + keccak256(any_2_evm_message_hash, header.sourceChainSelector, header.destinationChainSelector, onRamp), + keccak256(fixedSizeMessageFields), + keccak256(messageData), + keccak256(encodedRampTokenAmounts), + ) +*/ +func (h *MessageHasherV1) Hash(_ context.Context, msg cciptypes.Message) (cciptypes.Bytes32, error) { + var rampTokenAmounts []message_hasher.InternalRampTokenAmount + for _, rta := range msg.TokenAmounts { + rampTokenAmounts = append(rampTokenAmounts, message_hasher.InternalRampTokenAmount{ + SourcePoolAddress: rta.SourcePoolAddress, + DestTokenAddress: rta.DestTokenAddress, + ExtraData: rta.ExtraData, + Amount: rta.Amount.Int, + }) + } + encodedRampTokenAmounts, err := abiEncode("encodeTokenAmountsHashPreimage", rampTokenAmounts) + if err != nil { + return [32]byte{}, fmt.Errorf("abi encode token amounts: %w", err) + } + + metaDataHashInput, err := abiEncode( + "encodeMetadataHashPreimage", + ANY_2_EVM_MESSAGE_HASH, + uint64(msg.Header.SourceChainSelector), + uint64(msg.Header.DestChainSelector), + []byte(msg.Header.OnRamp), + ) + if err != nil { + return [32]byte{}, fmt.Errorf("abi encode metadata hash input: %w", err) + } + + // Need to decode the extra args to get the gas limit. + // TODO: we assume that extra args is always abi-encoded for now, but we need + // to decode according to source chain selector family. We should add a family + // lookup API to the chain-selectors library. + gasLimit, err := decodeExtraArgsV1V2(msg.ExtraArgs) + if err != nil { + return [32]byte{}, fmt.Errorf("decode extra args: %w", err) + } + + fixedSizeFieldsEncoded, err := abiEncode( + "encodeFixedSizeFieldsHashPreimage", + msg.Header.MessageID, + []byte(msg.Sender), + common.BytesToAddress(msg.Receiver), + uint64(msg.Header.SequenceNumber), + gasLimit, + msg.Header.Nonce, + ) + if err != nil { + return [32]byte{}, fmt.Errorf("abi encode fixed size values: %w", err) + } + + packedValues, err := abiEncode( + "encodeFinalHashPreimage", + leafDomainSeparator, + utils.Keccak256Fixed(metaDataHashInput), + utils.Keccak256Fixed(fixedSizeFieldsEncoded), + utils.Keccak256Fixed(msg.Data), + utils.Keccak256Fixed(encodedRampTokenAmounts), + ) + if err != nil { + return [32]byte{}, fmt.Errorf("abi encode packed values: %w", err) + } + + return utils.Keccak256Fixed(packedValues), nil +} + +func abiEncode(method string, values ...interface{}) ([]byte, error) { + res, err := messageHasherABI.Pack(method, values...) + if err != nil { + return nil, err + } + // trim the method selector. + return res[4:], nil +} + +// Interface compliance check +var _ cciptypes.MessageHasher = (*MessageHasherV1)(nil) diff --git a/core/capabilities/ccip/ccipevm/msghasher_test.go b/core/capabilities/ccip/ccipevm/msghasher_test.go new file mode 100644 index 00000000000..911a10b26a5 --- /dev/null +++ b/core/capabilities/ccip/ccipevm/msghasher_test.go @@ -0,0 +1,189 @@ +package ccipevm + +import ( + "context" + cryptorand "crypto/rand" + "fmt" + "math/big" + "math/rand" + "strings" + "testing" + + "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/stretchr/testify/require" + + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/message_hasher" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" +) + +// NOTE: these test cases are only EVM <-> EVM. +// Update these cases once we have non-EVM examples. +func TestMessageHasher_EVM2EVM(t *testing.T) { + ctx := testutils.Context(t) + d := testSetup(t) + + testCases := []evmExtraArgs{ + {version: "v1", gasLimit: big.NewInt(rand.Int63())}, + {version: "v2", gasLimit: big.NewInt(rand.Int63()), allowOOO: false}, + {version: "v2", gasLimit: big.NewInt(rand.Int63()), allowOOO: true}, + } + for i, tc := range testCases { + t.Run(fmt.Sprintf("tc_%d", i), func(tt *testing.T) { + testHasherEVM2EVM(ctx, tt, d, tc) + }) + } +} + +func testHasherEVM2EVM(ctx context.Context, t *testing.T, d *testSetupData, evmExtraArgs evmExtraArgs) { + ccipMsg := createEVM2EVMMessage(t, d.contract, evmExtraArgs) + + var tokenAmounts []message_hasher.InternalRampTokenAmount + for _, rta := range ccipMsg.TokenAmounts { + tokenAmounts = append(tokenAmounts, message_hasher.InternalRampTokenAmount{ + SourcePoolAddress: rta.SourcePoolAddress, + DestTokenAddress: rta.DestTokenAddress, + ExtraData: rta.ExtraData[:], + Amount: rta.Amount.Int, + }) + } + evmMsg := message_hasher.InternalAny2EVMRampMessage{ + Header: message_hasher.InternalRampMessageHeader{ + MessageId: ccipMsg.Header.MessageID, + SourceChainSelector: uint64(ccipMsg.Header.SourceChainSelector), + DestChainSelector: uint64(ccipMsg.Header.DestChainSelector), + SequenceNumber: uint64(ccipMsg.Header.SequenceNumber), + Nonce: ccipMsg.Header.Nonce, + }, + Sender: ccipMsg.Sender, + Receiver: common.BytesToAddress(ccipMsg.Receiver), + GasLimit: evmExtraArgs.gasLimit, + Data: ccipMsg.Data, + TokenAmounts: tokenAmounts, + } + + expectedHash, err := d.contract.Hash(&bind.CallOpts{Context: ctx}, evmMsg, ccipMsg.Header.OnRamp) + require.NoError(t, err) + + evmMsgHasher := NewMessageHasherV1() + actualHash, err := evmMsgHasher.Hash(ctx, ccipMsg) + require.NoError(t, err) + + require.Equal(t, fmt.Sprintf("%x", expectedHash), strings.TrimPrefix(actualHash.String(), "0x")) +} + +type evmExtraArgs struct { + version string + gasLimit *big.Int + allowOOO bool +} + +func createEVM2EVMMessage(t *testing.T, messageHasher *message_hasher.MessageHasher, evmExtraArgs evmExtraArgs) cciptypes.Message { + messageID := utils.RandomBytes32() + + sourceTokenData := make([]byte, rand.Intn(2048)) + _, err := cryptorand.Read(sourceTokenData) + require.NoError(t, err) + + sourceChain := rand.Uint64() + seqNum := rand.Uint64() + nonce := rand.Uint64() + destChain := rand.Uint64() + + var extraArgsBytes []byte + if evmExtraArgs.version == "v1" { + extraArgsBytes, err = messageHasher.EncodeEVMExtraArgsV1(nil, message_hasher.ClientEVMExtraArgsV1{ + GasLimit: evmExtraArgs.gasLimit, + }) + require.NoError(t, err) + } else if evmExtraArgs.version == "v2" { + extraArgsBytes, err = messageHasher.EncodeEVMExtraArgsV2(nil, message_hasher.ClientEVMExtraArgsV2{ + GasLimit: evmExtraArgs.gasLimit, + AllowOutOfOrderExecution: evmExtraArgs.allowOOO, + }) + require.NoError(t, err) + } else { + require.FailNowf(t, "unknown extra args version", "version: %s", evmExtraArgs.version) + } + + messageData := make([]byte, rand.Intn(2048)) + _, err = cryptorand.Read(messageData) + require.NoError(t, err) + + numTokens := rand.Intn(10) + var sourceTokenDatas [][]byte + for i := 0; i < numTokens; i++ { + sourceTokenDatas = append(sourceTokenDatas, sourceTokenData) + } + + var tokenAmounts []cciptypes.RampTokenAmount + for i := 0; i < len(sourceTokenDatas); i++ { + extraData := utils.RandomBytes32() + tokenAmounts = append(tokenAmounts, cciptypes.RampTokenAmount{ + SourcePoolAddress: abiEncodedAddress(t), + DestTokenAddress: abiEncodedAddress(t), + ExtraData: extraData[:], + Amount: cciptypes.NewBigInt(big.NewInt(0).SetUint64(rand.Uint64())), + }) + } + + return cciptypes.Message{ + Header: cciptypes.RampMessageHeader{ + MessageID: messageID, + SourceChainSelector: cciptypes.ChainSelector(sourceChain), + DestChainSelector: cciptypes.ChainSelector(destChain), + SequenceNumber: cciptypes.SeqNum(seqNum), + Nonce: nonce, + OnRamp: abiEncodedAddress(t), + }, + Sender: abiEncodedAddress(t), + Receiver: abiEncodedAddress(t), + Data: messageData, + TokenAmounts: tokenAmounts, + FeeToken: abiEncodedAddress(t), + FeeTokenAmount: cciptypes.NewBigInt(big.NewInt(0).SetUint64(rand.Uint64())), + ExtraArgs: extraArgsBytes, + } +} + +func abiEncodedAddress(t *testing.T) []byte { + addr := utils.RandomAddress() + encoded, err := utils.ABIEncode(`[{"type": "address"}]`, addr) + require.NoError(t, err) + return encoded +} + +type testSetupData struct { + contractAddr common.Address + contract *message_hasher.MessageHasher + sb *backends.SimulatedBackend + auth *bind.TransactOpts +} + +func testSetup(t *testing.T) *testSetupData { + transactor := testutils.MustNewSimTransactor(t) + simulatedBackend := backends.NewSimulatedBackend(core.GenesisAlloc{ + transactor.From: {Balance: assets.Ether(1000).ToInt()}, + }, 30e6) + + // Deploy the contract + address, _, _, err := message_hasher.DeployMessageHasher(transactor, simulatedBackend) + require.NoError(t, err) + simulatedBackend.Commit() + + // Setup contract client + contract, err := message_hasher.NewMessageHasher(address, simulatedBackend) + require.NoError(t, err) + + return &testSetupData{ + contractAddr: address, + contract: contract, + sb: simulatedBackend, + auth: transactor, + } +} diff --git a/core/capabilities/ccip/common/common.go b/core/capabilities/ccip/common/common.go new file mode 100644 index 00000000000..6409345ed93 --- /dev/null +++ b/core/capabilities/ccip/common/common.go @@ -0,0 +1,23 @@ +package common + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/crypto" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" +) + +// HashedCapabilityID returns the hashed capability id in a manner equivalent to the capability registry. +func HashedCapabilityID(capabilityLabelledName, capabilityVersion string) (r [32]byte, err error) { + // TODO: investigate how to avoid parsing the ABI everytime. + tabi := `[{"type": "string"}, {"type": "string"}]` + abiEncoded, err := utils.ABIEncode(tabi, capabilityLabelledName, capabilityVersion) + if err != nil { + return r, fmt.Errorf("failed to ABI encode capability version and labelled name: %w", err) + } + + h := crypto.Keccak256(abiEncoded) + copy(r[:], h) + return r, nil +} diff --git a/core/capabilities/ccip/common/common_test.go b/core/capabilities/ccip/common/common_test.go new file mode 100644 index 00000000000..a7484a83ad9 --- /dev/null +++ b/core/capabilities/ccip/common/common_test.go @@ -0,0 +1,51 @@ +package common_test + +import ( + "testing" + + capcommon "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/common" + + "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" +) + +func Test_HashedCapabilityId(t *testing.T) { + transactor := testutils.MustNewSimTransactor(t) + sb := backends.NewSimulatedBackend(core.GenesisAlloc{ + transactor.From: {Balance: assets.Ether(1000).ToInt()}, + }, 30e6) + + crAddress, _, _, err := kcr.DeployCapabilitiesRegistry(transactor, sb) + require.NoError(t, err) + sb.Commit() + + cr, err := kcr.NewCapabilitiesRegistry(crAddress, sb) + require.NoError(t, err) + + // add a capability, ignore cap config for simplicity. + _, err = cr.AddCapabilities(transactor, []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: "ccip", + Version: "v1.0.0", + CapabilityType: 0, + ResponseType: 0, + ConfigurationContract: common.Address{}, + }, + }) + require.NoError(t, err) + sb.Commit() + + hidExpected, err := cr.GetHashedCapabilityId(nil, "ccip", "v1.0.0") + require.NoError(t, err) + + hid, err := capcommon.HashedCapabilityID("ccip", "v1.0.0") + require.NoError(t, err) + + require.Equal(t, hidExpected, hid) +} diff --git a/core/capabilities/ccip/configs/evm/chain_writer.go b/core/capabilities/ccip/configs/evm/chain_writer.go new file mode 100644 index 00000000000..6d3b73c6f5c --- /dev/null +++ b/core/capabilities/ccip/configs/evm/chain_writer.go @@ -0,0 +1,75 @@ +package evm + +import ( + "encoding/json" + "fmt" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink-ccip/pkg/consts" + "github.com/smartcontractkit/chainlink/v2/common/txmgr" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_multi_offramp" + evmrelaytypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" +) + +var ( + offrampABI = evmtypes.MustGetABI(evm_2_evm_multi_offramp.EVM2EVMMultiOffRampABI) +) + +func MustChainWriterConfig( + fromAddress common.Address, + maxGasPrice *assets.Wei, + commitGasLimit, + execBatchGasLimit uint64, +) []byte { + rawConfig := ChainWriterConfigRaw(fromAddress, maxGasPrice, commitGasLimit, execBatchGasLimit) + encoded, err := json.Marshal(rawConfig) + if err != nil { + panic(fmt.Errorf("failed to marshal ChainWriterConfig: %w", err)) + } + + return encoded +} + +// ChainWriterConfigRaw returns a ChainWriterConfig that can be used to transmit commit and execute reports. +func ChainWriterConfigRaw( + fromAddress common.Address, + maxGasPrice *assets.Wei, + commitGasLimit, + execBatchGasLimit uint64, +) evmrelaytypes.ChainWriterConfig { + return evmrelaytypes.ChainWriterConfig{ + Contracts: map[string]*evmrelaytypes.ContractConfig{ + consts.ContractNameOffRamp: { + ContractABI: evm_2_evm_multi_offramp.EVM2EVMMultiOffRampABI, + Configs: map[string]*evmrelaytypes.ChainWriterDefinition{ + consts.MethodCommit: { + ChainSpecificName: mustGetMethodName("commit", offrampABI), + FromAddress: fromAddress, + GasLimit: commitGasLimit, + }, + consts.MethodExecute: { + ChainSpecificName: mustGetMethodName("execute", offrampABI), + FromAddress: fromAddress, + GasLimit: execBatchGasLimit, + }, + }, + }, + }, + SendStrategy: txmgr.NewSendEveryStrategy(), + MaxGasPrice: maxGasPrice, + } +} + +// mustGetMethodName panics if the method name is not found in the provided ABI. +func mustGetMethodName(name string, tabi abi.ABI) (methodName string) { + m, ok := tabi.Methods[name] + if !ok { + panic(fmt.Sprintf("missing method %s in the abi", name)) + } + return m.Name +} diff --git a/core/capabilities/ccip/configs/evm/contract_reader.go b/core/capabilities/ccip/configs/evm/contract_reader.go new file mode 100644 index 00000000000..085729690d5 --- /dev/null +++ b/core/capabilities/ccip/configs/evm/contract_reader.go @@ -0,0 +1,219 @@ +package evm + +import ( + "encoding/json" + "fmt" + + "github.com/ethereum/go-ethereum/accounts/abi" + + "github.com/smartcontractkit/chainlink-ccip/pkg/consts" + + evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/ccip_config" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_multi_offramp" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_multi_onramp" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/price_registry" + kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" + evmrelaytypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" +) + +var ( + onrampABI = evmtypes.MustGetABI(evm_2_evm_multi_onramp.EVM2EVMMultiOnRampABI) + capabilitiesRegsitryABI = evmtypes.MustGetABI(kcr.CapabilitiesRegistryABI) + ccipConfigABI = evmtypes.MustGetABI(ccip_config.CCIPConfigABI) + priceRegistryABI = evmtypes.MustGetABI(price_registry.PriceRegistryABI) +) + +// MustSourceReaderConfig returns a ChainReaderConfig that can be used to read from the onramp. +// The configuration is marshaled into JSON so that it can be passed to the relayer NewContractReader() method. +func MustSourceReaderConfig() []byte { + rawConfig := SourceReaderConfig() + encoded, err := json.Marshal(rawConfig) + if err != nil { + panic(fmt.Errorf("failed to marshal ChainReaderConfig into JSON: %w", err)) + } + + return encoded +} + +// MustDestReaderConfig returns a ChainReaderConfig that can be used to read from the offramp. +// The configuration is marshaled into JSON so that it can be passed to the relayer NewContractReader() method. +func MustDestReaderConfig() []byte { + rawConfig := DestReaderConfig() + encoded, err := json.Marshal(rawConfig) + if err != nil { + panic(fmt.Errorf("failed to marshal ChainReaderConfig into JSON: %w", err)) + } + + return encoded +} + +// DestReaderConfig returns a ChainReaderConfig that can be used to read from the offramp. +func DestReaderConfig() evmrelaytypes.ChainReaderConfig { + return evmrelaytypes.ChainReaderConfig{ + Contracts: map[string]evmrelaytypes.ChainContractReader{ + consts.ContractNameOffRamp: { + ContractABI: evm_2_evm_multi_offramp.EVM2EVMMultiOffRampABI, + ContractPollingFilter: evmrelaytypes.ContractPollingFilter{ + GenericEventNames: []string{ + mustGetEventName(consts.EventNameExecutionStateChanged, offrampABI), + mustGetEventName(consts.EventNameCommitReportAccepted, offrampABI), + }, + }, + Configs: map[string]*evmrelaytypes.ChainReaderDefinition{ + consts.MethodNameGetExecutionState: { + ChainSpecificName: mustGetMethodName("getExecutionState", offrampABI), + ReadType: evmrelaytypes.Method, + }, + consts.MethodNameGetMerkleRoot: { + ChainSpecificName: mustGetMethodName("getMerkleRoot", offrampABI), + ReadType: evmrelaytypes.Method, + }, + consts.MethodNameIsBlessed: { + ChainSpecificName: mustGetMethodName("isBlessed", offrampABI), + ReadType: evmrelaytypes.Method, + }, + consts.MethodNameGetLatestPriceSequenceNumber: { + ChainSpecificName: mustGetMethodName("getLatestPriceSequenceNumber", offrampABI), + ReadType: evmrelaytypes.Method, + }, + consts.MethodNameOfframpGetStaticConfig: { + ChainSpecificName: mustGetMethodName("getStaticConfig", offrampABI), + ReadType: evmrelaytypes.Method, + }, + consts.MethodNameOfframpGetDynamicConfig: { + ChainSpecificName: mustGetMethodName("getDynamicConfig", offrampABI), + ReadType: evmrelaytypes.Method, + }, + consts.MethodNameGetSourceChainConfig: { + ChainSpecificName: mustGetMethodName("getSourceChainConfig", offrampABI), + ReadType: evmrelaytypes.Method, + }, + consts.EventNameCommitReportAccepted: { + ChainSpecificName: mustGetEventName(consts.EventNameCommitReportAccepted, offrampABI), + ReadType: evmrelaytypes.Event, + }, + consts.EventNameExecutionStateChanged: { + ChainSpecificName: mustGetEventName(consts.EventNameExecutionStateChanged, offrampABI), + ReadType: evmrelaytypes.Event, + }, + }, + }, + }, + } +} + +// SourceReaderConfig returns a ChainReaderConfig that can be used to read from the onramp. +func SourceReaderConfig() evmrelaytypes.ChainReaderConfig { + return evmrelaytypes.ChainReaderConfig{ + Contracts: map[string]evmrelaytypes.ChainContractReader{ + consts.ContractNameOnRamp: { + ContractABI: evm_2_evm_multi_onramp.EVM2EVMMultiOnRampABI, + ContractPollingFilter: evmrelaytypes.ContractPollingFilter{ + GenericEventNames: []string{ + mustGetEventName(consts.EventNameCCIPSendRequested, onrampABI), + }, + }, + Configs: map[string]*evmrelaytypes.ChainReaderDefinition{ + // all "{external|public} view" functions in the onramp except for getFee and getPoolBySourceToken are here. + // getFee is not expected to get called offchain and is only called by end-user contracts. + consts.MethodNameGetExpectedNextSequenceNumber: { + ChainSpecificName: mustGetMethodName("getExpectedNextSequenceNumber", onrampABI), + ReadType: evmrelaytypes.Method, + }, + consts.MethodNameOnrampGetStaticConfig: { + ChainSpecificName: mustGetMethodName("getStaticConfig", onrampABI), + ReadType: evmrelaytypes.Method, + }, + consts.MethodNameOnrampGetDynamicConfig: { + ChainSpecificName: mustGetMethodName("getDynamicConfig", onrampABI), + ReadType: evmrelaytypes.Method, + }, + consts.EventNameCCIPSendRequested: { + ChainSpecificName: mustGetEventName(consts.EventNameCCIPSendRequested, onrampABI), + ReadType: evmrelaytypes.Event, + EventDefinitions: &evmrelaytypes.EventDefinitions{ + GenericDataWordNames: map[string]uint8{ + consts.EventAttributeSequenceNumber: 5, + }, + }, + }, + }, + }, + consts.ContractNamePriceRegistry: { + ContractABI: price_registry.PriceRegistryABI, + Configs: map[string]*evmrelaytypes.ChainReaderDefinition{ + // TODO: update with the consts from https://github.com/smartcontractkit/chainlink-ccip/pull/39 + // in a followup. + "GetStaticConfig": { + ChainSpecificName: mustGetMethodName("getStaticConfig", priceRegistryABI), + ReadType: evmrelaytypes.Method, + }, + "GetDestChainConfig": { + ChainSpecificName: mustGetMethodName("getDestChainConfig", priceRegistryABI), + ReadType: evmrelaytypes.Method, + }, + "GetPremiumMultiplierWeiPerEth": { + ChainSpecificName: mustGetMethodName("getPremiumMultiplierWeiPerEth", priceRegistryABI), + ReadType: evmrelaytypes.Method, + }, + "GetTokenTransferFeeConfig": { + ChainSpecificName: mustGetMethodName("getTokenTransferFeeConfig", priceRegistryABI), + ReadType: evmrelaytypes.Method, + }, + "ProcessMessageArgs": { + ChainSpecificName: mustGetMethodName("processMessageArgs", priceRegistryABI), + ReadType: evmrelaytypes.Method, + }, + "ValidatePoolReturnData": { + ChainSpecificName: mustGetMethodName("validatePoolReturnData", priceRegistryABI), + ReadType: evmrelaytypes.Method, + }, + "GetValidatedTokenPrice": { + ChainSpecificName: mustGetMethodName("getValidatedTokenPrice", priceRegistryABI), + ReadType: evmrelaytypes.Method, + }, + "GetFeeTokens": { + ChainSpecificName: mustGetMethodName("getFeeTokens", priceRegistryABI), + ReadType: evmrelaytypes.Method, + }, + }, + }, + }, + } +} + +// HomeChainReaderConfigRaw returns a ChainReaderConfig that can be used to read from the home chain. +func HomeChainReaderConfigRaw() evmrelaytypes.ChainReaderConfig { + return evmrelaytypes.ChainReaderConfig{ + Contracts: map[string]evmrelaytypes.ChainContractReader{ + consts.ContractNameCapabilitiesRegistry: { + ContractABI: kcr.CapabilitiesRegistryABI, + Configs: map[string]*evmrelaytypes.ChainReaderDefinition{ + consts.MethodNameGetCapability: { + ChainSpecificName: mustGetMethodName("getCapability", capabilitiesRegsitryABI), + }, + }, + }, + consts.ContractNameCCIPConfig: { + ContractABI: ccip_config.CCIPConfigABI, + Configs: map[string]*evmrelaytypes.ChainReaderDefinition{ + consts.MethodNameGetAllChainConfigs: { + ChainSpecificName: mustGetMethodName("getAllChainConfigs", ccipConfigABI), + }, + consts.MethodNameGetOCRConfig: { + ChainSpecificName: mustGetMethodName("getOCRConfig", ccipConfigABI), + }, + }, + }, + }, + } +} + +func mustGetEventName(event string, tabi abi.ABI) string { + e, ok := tabi.Events[event] + if !ok { + panic(fmt.Sprintf("missing event %s in onrampABI", event)) + } + return e.Name +} diff --git a/core/capabilities/ccip/delegate.go b/core/capabilities/ccip/delegate.go new file mode 100644 index 00000000000..c9974d62e99 --- /dev/null +++ b/core/capabilities/ccip/delegate.go @@ -0,0 +1,321 @@ +package ccip + +import ( + "context" + "fmt" + "time" + + "github.com/smartcontractkit/chainlink-common/pkg/loop" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/common" + configsevm "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/configs/evm" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/launcher" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/oraclecreator" + "github.com/smartcontractkit/chainlink/v2/core/services/ocrcommon" + p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" + "github.com/smartcontractkit/chainlink/v2/core/services/registrysyncer" + + ragep2ptypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/chainlink-ccip/pkg/consts" + ccipreaderpkg "github.com/smartcontractkit/chainlink-ccip/pkg/reader" + "github.com/smartcontractkit/chainlink-common/pkg/sqlutil" + "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives" + + "github.com/smartcontractkit/chainlink/v2/core/chains/legacyevm" + "github.com/smartcontractkit/chainlink/v2/core/config" + kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/job" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ocr2key" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2" + "github.com/smartcontractkit/chainlink/v2/core/services/pipeline" + "github.com/smartcontractkit/chainlink/v2/core/services/relay" + "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" + "github.com/smartcontractkit/chainlink/v2/core/services/telemetry" + "github.com/smartcontractkit/chainlink/v2/plugins" +) + +type RelayGetter interface { + Get(types.RelayID) (loop.Relayer, error) + GetIDToRelayerMap() (map[types.RelayID]loop.Relayer, error) +} + +type Delegate struct { + lggr logger.Logger + registrarConfig plugins.RegistrarConfig + pipelineRunner pipeline.Runner + chains legacyevm.LegacyChainContainer + relayers RelayGetter + keystore keystore.Master + ds sqlutil.DataSource + peerWrapper *ocrcommon.SingletonPeerWrapper + monitoringEndpointGen telemetry.MonitoringEndpointGenerator + capabilityConfig config.Capabilities + + isNewlyCreatedJob bool +} + +func NewDelegate( + lggr logger.Logger, + registrarConfig plugins.RegistrarConfig, + pipelineRunner pipeline.Runner, + chains legacyevm.LegacyChainContainer, + relayers RelayGetter, + keystore keystore.Master, + ds sqlutil.DataSource, + peerWrapper *ocrcommon.SingletonPeerWrapper, + monitoringEndpointGen telemetry.MonitoringEndpointGenerator, + capabilityConfig config.Capabilities, +) *Delegate { + return &Delegate{ + lggr: lggr, + registrarConfig: registrarConfig, + pipelineRunner: pipelineRunner, + chains: chains, + relayers: relayers, + ds: ds, + keystore: keystore, + peerWrapper: peerWrapper, + monitoringEndpointGen: monitoringEndpointGen, + capabilityConfig: capabilityConfig, + } +} + +func (d *Delegate) JobType() job.Type { + return job.CCIP +} + +func (d *Delegate) BeforeJobCreated(job.Job) { + // This is only called first time the job is created + d.isNewlyCreatedJob = true +} + +func (d *Delegate) ServicesForSpec(ctx context.Context, spec job.Job) (services []job.ServiceCtx, err error) { + // In general there should only be one P2P key but the node may have multiple. + // The job spec should specify the correct P2P key to use. + peerID, err := p2pkey.MakePeerID(spec.CCIPSpec.P2PKeyID) + if err != nil { + return nil, fmt.Errorf("failed to make peer ID from provided spec p2p id (%s): %w", spec.CCIPSpec.P2PKeyID, err) + } + + p2pID, err := d.keystore.P2P().Get(peerID) + if err != nil { + return nil, fmt.Errorf("failed to get all p2p keys: %w", err) + } + + cfg := d.capabilityConfig + rid := cfg.ExternalRegistry().RelayID() + relayer, err := d.relayers.Get(rid) + if err != nil { + return nil, fmt.Errorf("could not fetch relayer %s configured for capabilities registry: %w", rid, err) + } + registrySyncer, err := registrysyncer.New( + d.lggr, + func() (p2ptypes.PeerID, error) { + return p2ptypes.PeerID(p2pID.PeerID()), nil + }, + relayer, + cfg.ExternalRegistry().Address(), + ) + if err != nil { + return nil, fmt.Errorf("could not configure syncer: %w", err) + } + + ocrKeys, err := d.getOCRKeys(spec.CCIPSpec.OCRKeyBundleIDs) + if err != nil { + return nil, err + } + + transmitterKeys, err := d.getTransmitterKeys(ctx, d.chains) + if err != nil { + return nil, err + } + + bootstrapperLocators, err := ocrcommon.ParseBootstrapPeers(spec.CCIPSpec.P2PV2Bootstrappers) + if err != nil { + return nil, fmt.Errorf("failed to parse bootstrapper locators: %w", err) + } + + // NOTE: we can use the same DB for all plugin instances, + // since all queries are scoped by config digest. + ocrDB := ocr2.NewDB(d.ds, spec.ID, 0, d.lggr) + + homeChainContractReader, err := d.getHomeChainContractReader( + ctx, + d.chains, + spec.CCIPSpec.CapabilityLabelledName, + spec.CCIPSpec.CapabilityVersion) + if err != nil { + return nil, fmt.Errorf("failed to get home chain contract reader: %w", err) + } + + hcr := ccipreaderpkg.NewHomeChainReader( + homeChainContractReader, + d.lggr.Named("HomeChainReader"), + 100*time.Millisecond, + ) + + oracleCreator := oraclecreator.New( + ocrKeys, + transmitterKeys, + d.chains, + d.peerWrapper, + spec.ExternalJobID, + spec.ID, + d.isNewlyCreatedJob, + spec.CCIPSpec.PluginConfig, + ocrDB, + d.lggr, + d.monitoringEndpointGen, + bootstrapperLocators, + hcr, + ) + + capabilityID := fmt.Sprintf("%s@%s", spec.CCIPSpec.CapabilityLabelledName, spec.CCIPSpec.CapabilityVersion) + capLauncher := launcher.New( + capabilityID, + ragep2ptypes.PeerID(p2pID.PeerID()), + d.lggr, + hcr, + oracleCreator, + 12*time.Second, + ) + + // register the capability launcher with the registry syncer + registrySyncer.AddLauncher(capLauncher) + + return []job.ServiceCtx{ + registrySyncer, + hcr, + capLauncher, + }, nil +} + +func (d *Delegate) AfterJobCreated(spec job.Job) {} + +func (d *Delegate) BeforeJobDeleted(spec job.Job) {} + +func (d *Delegate) OnDeleteJob(ctx context.Context, spec job.Job) error { + // TODO: shut down needed services? + return nil +} + +func (d *Delegate) getOCRKeys(ocrKeyBundleIDs job.JSONConfig) (map[string]ocr2key.KeyBundle, error) { + ocrKeys := make(map[string]ocr2key.KeyBundle) + for networkType, bundleIDRaw := range ocrKeyBundleIDs { + if networkType != relay.NetworkEVM { + return nil, fmt.Errorf("unsupported chain type: %s", networkType) + } + + bundleID, ok := bundleIDRaw.(string) + if !ok { + return nil, fmt.Errorf("OCRKeyBundleIDs must be a map of chain types to OCR key bundle IDs, got: %T", bundleIDRaw) + } + + bundle, err2 := d.keystore.OCR2().Get(bundleID) + if err2 != nil { + return nil, fmt.Errorf("OCR key bundle with ID %s not found: %w", bundleID, err2) + } + + ocrKeys[networkType] = bundle + } + return ocrKeys, nil +} + +func (d *Delegate) getTransmitterKeys(ctx context.Context, chains legacyevm.LegacyChainContainer) (map[types.RelayID][]string, error) { + transmitterKeys := make(map[types.RelayID][]string) + for _, chain := range chains.Slice() { + relayID := types.NewRelayID(relay.NetworkEVM, chain.ID().String()) + ethKeys, err2 := d.keystore.Eth().EnabledAddressesForChain(ctx, chain.ID()) + if err2 != nil { + return nil, fmt.Errorf("error getting enabled addresses for chain: %s %w", chain.ID().String(), err2) + } + + transmitterKeys[relayID] = func() (r []string) { + for _, key := range ethKeys { + r = append(r, key.Hex()) + } + return + }() + } + return transmitterKeys, nil +} + +func (d *Delegate) getHomeChainContractReader( + ctx context.Context, + chains legacyevm.LegacyChainContainer, + capabilityLabelledName, + capabilityVersion string, +) (types.ContractReader, error) { + // home chain is where the capability registry is deployed, + // which should be set correctly in toml config. + homeChainRelayID := d.capabilityConfig.ExternalRegistry().RelayID() + homeChain, err := chains.Get(homeChainRelayID.ChainID) + if err != nil { + return nil, fmt.Errorf("home chain relayer not found, chain id: %s, err: %w", homeChainRelayID.String(), err) + } + + reader, err := evm.NewChainReaderService( + context.Background(), + d.lggr, + homeChain.LogPoller(), + homeChain.HeadTracker(), + homeChain.Client(), + configsevm.HomeChainReaderConfigRaw(), + ) + if err != nil { + return nil, fmt.Errorf("failed to create home chain contract reader: %w", err) + } + + reader, err = bindReader(ctx, reader, d.capabilityConfig.ExternalRegistry().Address(), capabilityLabelledName, capabilityVersion) + if err != nil { + return nil, fmt.Errorf("failed to bind home chain contract reader: %w", err) + } + + return reader, nil +} + +func bindReader(ctx context.Context, + reader types.ContractReader, + capRegAddress, + capabilityLabelledName, + capabilityVersion string) (types.ContractReader, error) { + err := reader.Bind(ctx, []types.BoundContract{ + { + Address: capRegAddress, + Name: consts.ContractNameCapabilitiesRegistry, + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to bind home chain contract reader: %w", err) + } + + hid, err := common.HashedCapabilityID(capabilityLabelledName, capabilityVersion) + if err != nil { + return nil, fmt.Errorf("failed to hash capability id: %w", err) + } + + var ccipCapabilityInfo kcr.CapabilitiesRegistryCapabilityInfo + err = reader.GetLatestValue(ctx, consts.ContractNameCapabilitiesRegistry, consts.MethodNameGetCapability, primitives.Unconfirmed, map[string]any{ + "hashedId": hid, + }, &ccipCapabilityInfo) + if err != nil { + return nil, fmt.Errorf("failed to get CCIP capability info from chain reader: %w", err) + } + + // bind the ccip capability configuration contract + err = reader.Bind(ctx, []types.BoundContract{ + { + Address: ccipCapabilityInfo.ConfigurationContract.String(), + Name: consts.ContractNameCCIPConfig, + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to bind CCIP capability configuration contract: %w", err) + } + + return reader, nil +} diff --git a/core/capabilities/ccip/delegate_test.go b/core/capabilities/ccip/delegate_test.go new file mode 100644 index 00000000000..dd8a5124b57 --- /dev/null +++ b/core/capabilities/ccip/delegate_test.go @@ -0,0 +1 @@ +package ccip diff --git a/core/capabilities/ccip/launcher/README.md b/core/capabilities/ccip/launcher/README.md new file mode 100644 index 00000000000..41fbecfdbd8 --- /dev/null +++ b/core/capabilities/ccip/launcher/README.md @@ -0,0 +1,69 @@ +# CCIP Capability Launcher + +The CCIP capability launcher is responsible for listening to +[Capabilities Registry](../../../../contracts/src/v0.8/keystone/CapabilitiesRegistry.sol) (CR) updates +for the particular CCIP capability (labelled name, version) pair and reacting to them. In +particular, there are three kinds of events that would affect a particular capability: + +1. DON Creation: when `addDON` is called on the CR, the capabilities of this new DON are specified. +If CCIP is one of those capabilities, the launcher will launch a commit and an execution plugin +with the OCR configuration specified in the DON creation process. See +[Types.sol](../../../../contracts/src/v0.8/ccip/capability/libraries/Types.sol) for more details +on what the OCR configuration contains. +2. DON update: when `updateDON` is called on the CR, capabilities of the DON can be updated. In the +CCIP use case specifically, `updateDON` is used to update OCR configuration of that DON. Updates +follow the blue/green deployment pattern (explained in detail below with a state diagram). In this +scenario the launcher must either launch brand new instances of the commit and execution plugins +(in the event a green deployment is made) or promote the currently running green instance to be +the blue instance. +3. DON deletion: when `deleteDON` is called on the CR, the launcher must shut down all running plugins +related to that DON. When a DON is deleted it effectively means that it should no longer function. +DON deletion is permanent. + +## Architecture Diagram + +![CCIP Capability Launcher](ccip_capability_launcher.png) + +The above diagram shows how the CCIP capability launcher interacts with the rest of the components +in the CCIP system. + +The CCIP capability job, which is created on the Chainlink node, will spin up the CCIP capability +launcher alongside the home chain reader, which reads the [CCIPConfig.sol](../../../../contracts/src/v0.8/ccip/capability/CCIPConfig.sol) +contract deployed on the home chain (typically Ethereum Mainnet, though could be "any chain" in theory). + +Injected into the launcher is the [OracleCreator](../types/types.go) object which knows how to spin up CCIP +oracles (both bootstrap and plugin oracles). This is used by the launcher at the appropriate time in order +to create oracle instances but not start them right away. + +After all the required oracles have been created, the launcher will start and shut them down as required +in order to match the configuration that was posted on-chain in the CR and the CCIPConfig.sol contract. + + +## Config State Diagram + +![CCIP Config State Machine](ccip_config_state_machine.png) + +CCIP's blue/green deployment paradigm is intentionally kept as simple as possible. + +Every CCIP DON starts in the `Init` state. Upon DON creation, which must provide a valid OCR +configuration, the CCIP DON will move into the `Running` state. In this state, the DON is +presumed to be fully functional from a configuration standpoint. + +When we want to update configuration, we propose a new configuration to the CR that consists of +an array of two OCR configurations: + +1. The first element of the array is the current OCR configuration that is running (termed "blue"). +2. The second element of the array is the future OCR configuration that we want to run (termed "green"). + +Various checks are done on-chain in order to validate this particular state transition, in particular, +related to config counts. Doing this will move the state of the configuration to the `Staging` state. + +In the `Staging` state, there are effectively four plugins running - one (commit, execution) pair for the +blue configuration, and one (commit, execution) pair for the green configuration. However, only the blue +configuration will actually be writing on-chain, where as the green configuration will be "dry running", +i.e doing everything except transmitting. + +This allows us to test out new configurations without committing to them immediately. + +Finally, from the `Staging` state, there is only one transition, which is to promote the green configuration +to be the new blue configuration, and go back into the `Running` state. diff --git a/core/capabilities/ccip/launcher/bluegreen.go b/core/capabilities/ccip/launcher/bluegreen.go new file mode 100644 index 00000000000..62458466291 --- /dev/null +++ b/core/capabilities/ccip/launcher/bluegreen.go @@ -0,0 +1,178 @@ +package launcher + +import ( + "fmt" + + cctypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" + + "go.uber.org/multierr" + + ccipreaderpkg "github.com/smartcontractkit/chainlink-ccip/pkg/reader" +) + +// blueGreenDeployment represents a blue-green deployment of OCR instances. +type blueGreenDeployment struct { + // blue is the blue OCR instance. + // blue must always be present. + blue cctypes.CCIPOracle + + // bootstrapBlue is the bootstrap node of the blue OCR instance. + // Only a subset of the DON will be running bootstrap instances, + // so this may be nil. + bootstrapBlue cctypes.CCIPOracle + + // green is the green OCR instance. + // green may or may not be present. + // green must never be present if blue is not present. + // TODO: should we enforce this invariant somehow? + green cctypes.CCIPOracle + + // bootstrapGreen is the bootstrap node of the green OCR instance. + // Only a subset of the DON will be running bootstrap instances, + // so this may be nil, even when green is not nil. + bootstrapGreen cctypes.CCIPOracle +} + +// ccipDeployment represents blue-green deployments of both commit and exec +// OCR instances. +type ccipDeployment struct { + commit blueGreenDeployment + exec blueGreenDeployment +} + +// Close shuts down all OCR instances in the deployment. +func (c *ccipDeployment) Close() error { + var err error + + // shutdown blue commit instances. + err = multierr.Append(err, c.commit.blue.Close()) + if c.commit.bootstrapBlue != nil { + err = multierr.Append(err, c.commit.bootstrapBlue.Close()) + } + + // shutdown green commit instances. + if c.commit.green != nil { + err = multierr.Append(err, c.commit.green.Close()) + } + if c.commit.bootstrapGreen != nil { + err = multierr.Append(err, c.commit.bootstrapGreen.Close()) + } + + // shutdown blue exec instances. + err = multierr.Append(err, c.exec.blue.Close()) + if c.exec.bootstrapBlue != nil { + err = multierr.Append(err, c.exec.bootstrapBlue.Close()) + } + + // shutdown green exec instances. + if c.exec.green != nil { + err = multierr.Append(err, c.exec.green.Close()) + } + if c.exec.bootstrapGreen != nil { + err = multierr.Append(err, c.exec.bootstrapGreen.Close()) + } + + return err +} + +// StartBlue starts the blue OCR instances. +func (c *ccipDeployment) StartBlue() error { + var err error + + err = multierr.Append(err, c.commit.blue.Start()) + if c.commit.bootstrapBlue != nil { + err = multierr.Append(err, c.commit.bootstrapBlue.Start()) + } + err = multierr.Append(err, c.exec.blue.Start()) + if c.exec.bootstrapBlue != nil { + err = multierr.Append(err, c.exec.bootstrapBlue.Start()) + } + + return err +} + +// CloseBlue shuts down the blue OCR instances. +func (c *ccipDeployment) CloseBlue() error { + var err error + + err = multierr.Append(err, c.commit.blue.Close()) + if c.commit.bootstrapBlue != nil { + err = multierr.Append(err, c.commit.bootstrapBlue.Close()) + } + err = multierr.Append(err, c.exec.blue.Close()) + if c.exec.bootstrapBlue != nil { + err = multierr.Append(err, c.exec.bootstrapBlue.Close()) + } + + return err +} + +// HandleBlueGreen handles the blue-green deployment transition. +// prevDeployment is the previous deployment state. +// there are two possible cases: +// +// 1. both blue and green are present in prevDeployment, but only blue is present in c. +// this is a promotion of green to blue, so we need to shut down the blue deployment +// and make green the new blue. In this case green is already running, so there's no +// need to start it. However, we need to shut down the blue deployment. +// +// 2. only blue is present in prevDeployment, both blue and green are present in c. +// In this case, blue is already running, so there's no need to start it. We need to +// start green. +func (c *ccipDeployment) HandleBlueGreen(prevDeployment *ccipDeployment) error { + if prevDeployment == nil { + return fmt.Errorf("previous deployment is nil") + } + + var err error + if prevDeployment.commit.green != nil && c.commit.green == nil { + err = multierr.Append(err, prevDeployment.commit.blue.Close()) + if prevDeployment.commit.bootstrapBlue != nil { + err = multierr.Append(err, prevDeployment.commit.bootstrapBlue.Close()) + } + } else if prevDeployment.commit.green == nil && c.commit.green != nil { + err = multierr.Append(err, c.commit.green.Start()) + if c.commit.bootstrapGreen != nil { + err = multierr.Append(err, c.commit.bootstrapGreen.Start()) + } + } else { + return fmt.Errorf("invalid blue-green deployment transition") + } + + if prevDeployment.exec.green != nil && c.exec.green == nil { + err = multierr.Append(err, prevDeployment.exec.blue.Close()) + if prevDeployment.exec.bootstrapBlue != nil { + err = multierr.Append(err, prevDeployment.exec.bootstrapBlue.Close()) + } + } else if prevDeployment.exec.green == nil && c.exec.green != nil { + err = multierr.Append(err, c.exec.green.Start()) + if c.exec.bootstrapGreen != nil { + err = multierr.Append(err, c.exec.bootstrapGreen.Start()) + } + } else { + return fmt.Errorf("invalid blue-green deployment transition") + } + + return err +} + +// HasGreenInstance returns true if the deployment has a green instance for the +// given plugin type. +func (c *ccipDeployment) HasGreenInstance(pluginType cctypes.PluginType) bool { + switch pluginType { + case cctypes.PluginTypeCCIPCommit: + return c.commit.green != nil + case cctypes.PluginTypeCCIPExec: + return c.exec.green != nil + default: + return false + } +} + +func isNewGreenInstance(pluginType cctypes.PluginType, ocrConfigs []ccipreaderpkg.OCR3ConfigWithMeta, prevDeployment ccipDeployment) bool { + return len(ocrConfigs) == 2 && !prevDeployment.HasGreenInstance(pluginType) +} + +func isPromotion(pluginType cctypes.PluginType, ocrConfigs []ccipreaderpkg.OCR3ConfigWithMeta, prevDeployment ccipDeployment) bool { + return len(ocrConfigs) == 1 && prevDeployment.HasGreenInstance(pluginType) +} diff --git a/core/capabilities/ccip/launcher/bluegreen_test.go b/core/capabilities/ccip/launcher/bluegreen_test.go new file mode 100644 index 00000000000..9fd71a0cb44 --- /dev/null +++ b/core/capabilities/ccip/launcher/bluegreen_test.go @@ -0,0 +1,1043 @@ +package launcher + +import ( + "errors" + "testing" + + cctypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" + mocktypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types/mocks" + + "github.com/stretchr/testify/require" + + ccipreaderpkg "github.com/smartcontractkit/chainlink-ccip/pkg/reader" +) + +func Test_ccipDeployment_Close(t *testing.T) { + type args struct { + commitBlue *mocktypes.CCIPOracle + commitBlueBootstrap *mocktypes.CCIPOracle + commitGreen *mocktypes.CCIPOracle + commitGreenBootstrap *mocktypes.CCIPOracle + execBlue *mocktypes.CCIPOracle + execBlueBootstrap *mocktypes.CCIPOracle + execGreen *mocktypes.CCIPOracle + execGreenBootstrap *mocktypes.CCIPOracle + } + tests := []struct { + name string + args args + expect func(t *testing.T, args args) + asserts func(t *testing.T, args args) + wantErr bool + }{ + { + name: "no errors, blue only", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitGreen: nil, + commitGreenBootstrap: nil, + execBlue: mocktypes.NewCCIPOracle(t), + execGreen: nil, + execGreenBootstrap: nil, + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Close").Return(nil).Once() + args.execBlue.On("Close").Return(nil).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + }, + wantErr: false, + }, + { + name: "no errors, blue and green", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitGreen: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + execGreen: mocktypes.NewCCIPOracle(t), + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Close").Return(nil).Once() + args.commitGreen.On("Close").Return(nil).Once() + args.execBlue.On("Close").Return(nil).Once() + args.execGreen.On("Close").Return(nil).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.commitGreen.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + args.execGreen.AssertExpectations(t) + }, + wantErr: false, + }, + { + name: "error on commit blue", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitGreen: nil, + execBlue: mocktypes.NewCCIPOracle(t), + execGreen: nil, + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Close").Return(errors.New("failed")).Once() + args.execBlue.On("Close").Return(nil).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + }, + wantErr: true, + }, + { + name: "bootstrap blue also closed", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + execBlueBootstrap: mocktypes.NewCCIPOracle(t), + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Close").Return(nil).Once() + args.commitBlueBootstrap.On("Close").Return(nil).Once() + args.execBlue.On("Close").Return(nil).Once() + args.execBlueBootstrap.On("Close").Return(nil).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.commitBlueBootstrap.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + args.execBlueBootstrap.AssertExpectations(t) + }, + wantErr: false, + }, + { + name: "bootstrap green also closed", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: mocktypes.NewCCIPOracle(t), + commitGreen: mocktypes.NewCCIPOracle(t), + commitGreenBootstrap: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + execBlueBootstrap: mocktypes.NewCCIPOracle(t), + execGreen: mocktypes.NewCCIPOracle(t), + execGreenBootstrap: mocktypes.NewCCIPOracle(t), + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Close").Return(nil).Once() + args.commitBlueBootstrap.On("Close").Return(nil).Once() + args.commitGreen.On("Close").Return(nil).Once() + args.commitGreenBootstrap.On("Close").Return(nil).Once() + args.execBlue.On("Close").Return(nil).Once() + args.execBlueBootstrap.On("Close").Return(nil).Once() + args.execGreen.On("Close").Return(nil).Once() + args.execGreenBootstrap.On("Close").Return(nil).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.commitBlueBootstrap.AssertExpectations(t) + args.commitGreen.AssertExpectations(t) + args.commitGreenBootstrap.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + args.execBlueBootstrap.AssertExpectations(t) + args.execGreen.AssertExpectations(t) + args.execGreenBootstrap.AssertExpectations(t) + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &ccipDeployment{ + commit: blueGreenDeployment{ + blue: tt.args.commitBlue, + }, + exec: blueGreenDeployment{ + blue: tt.args.execBlue, + }, + } + if tt.args.commitGreen != nil { + c.commit.green = tt.args.commitGreen + } + if tt.args.commitBlueBootstrap != nil { + c.commit.bootstrapBlue = tt.args.commitBlueBootstrap + } + if tt.args.commitGreenBootstrap != nil { + c.commit.bootstrapGreen = tt.args.commitGreenBootstrap + } + + if tt.args.execGreen != nil { + c.exec.green = tt.args.execGreen + } + if tt.args.execBlueBootstrap != nil { + c.exec.bootstrapBlue = tt.args.execBlueBootstrap + } + if tt.args.execGreenBootstrap != nil { + c.exec.bootstrapGreen = tt.args.execGreenBootstrap + } + + tt.expect(t, tt.args) + defer tt.asserts(t, tt.args) + err := c.Close() + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func Test_ccipDeployment_StartBlue(t *testing.T) { + type args struct { + commitBlue *mocktypes.CCIPOracle + commitBlueBootstrap *mocktypes.CCIPOracle + execBlue *mocktypes.CCIPOracle + execBlueBootstrap *mocktypes.CCIPOracle + } + tests := []struct { + name string + args args + expect func(t *testing.T, args args) + asserts func(t *testing.T, args args) + wantErr bool + }{ + { + name: "no errors, no bootstrap", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: nil, + execBlueBootstrap: nil, + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Start").Return(nil).Once() + args.execBlue.On("Start").Return(nil).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + }, + wantErr: false, + }, + { + name: "no errors, with bootstrap", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: mocktypes.NewCCIPOracle(t), + execBlueBootstrap: mocktypes.NewCCIPOracle(t), + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Start").Return(nil).Once() + args.commitBlueBootstrap.On("Start").Return(nil).Once() + args.execBlue.On("Start").Return(nil).Once() + args.execBlueBootstrap.On("Start").Return(nil).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.commitBlueBootstrap.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + args.execBlueBootstrap.AssertExpectations(t) + }, + wantErr: false, + }, + { + name: "error on commit blue", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: nil, + execBlueBootstrap: nil, + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Start").Return(errors.New("failed")).Once() + args.execBlue.On("Start").Return(nil).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + }, + wantErr: true, + }, + { + name: "error on exec blue", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: nil, + execBlueBootstrap: nil, + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Start").Return(nil).Once() + args.execBlue.On("Start").Return(errors.New("failed")).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + }, + wantErr: true, + }, + { + name: "error on commit blue bootstrap", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: mocktypes.NewCCIPOracle(t), + execBlueBootstrap: nil, + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Start").Return(nil).Once() + args.commitBlueBootstrap.On("Start").Return(errors.New("failed")).Once() + args.execBlue.On("Start").Return(nil).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.commitBlueBootstrap.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + }, + wantErr: true, + }, + { + name: "error on exec blue bootstrap", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: nil, + execBlueBootstrap: mocktypes.NewCCIPOracle(t), + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Start").Return(nil).Once() + args.execBlue.On("Start").Return(nil).Once() + args.execBlueBootstrap.On("Start").Return(errors.New("failed")).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + args.execBlueBootstrap.AssertExpectations(t) + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &ccipDeployment{ + commit: blueGreenDeployment{ + blue: tt.args.commitBlue, + }, + exec: blueGreenDeployment{ + blue: tt.args.execBlue, + }, + } + if tt.args.commitBlueBootstrap != nil { + c.commit.bootstrapBlue = tt.args.commitBlueBootstrap + } + if tt.args.execBlueBootstrap != nil { + c.exec.bootstrapBlue = tt.args.execBlueBootstrap + } + + tt.expect(t, tt.args) + defer tt.asserts(t, tt.args) + err := c.StartBlue() + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func Test_ccipDeployment_CloseBlue(t *testing.T) { + type args struct { + commitBlue *mocktypes.CCIPOracle + commitBlueBootstrap *mocktypes.CCIPOracle + execBlue *mocktypes.CCIPOracle + execBlueBootstrap *mocktypes.CCIPOracle + } + tests := []struct { + name string + args args + expect func(t *testing.T, args args) + asserts func(t *testing.T, args args) + wantErr bool + }{ + { + name: "no errors, no bootstrap", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: nil, + execBlueBootstrap: nil, + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Close").Return(nil).Once() + args.execBlue.On("Close").Return(nil).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + }, + wantErr: false, + }, + { + name: "no errors, with bootstrap", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: mocktypes.NewCCIPOracle(t), + execBlueBootstrap: mocktypes.NewCCIPOracle(t), + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Close").Return(nil).Once() + args.commitBlueBootstrap.On("Close").Return(nil).Once() + args.execBlue.On("Close").Return(nil).Once() + args.execBlueBootstrap.On("Close").Return(nil).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.commitBlueBootstrap.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + args.execBlueBootstrap.AssertExpectations(t) + }, + wantErr: false, + }, + { + name: "error on commit blue", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: nil, + execBlueBootstrap: nil, + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Close").Return(errors.New("failed")).Once() + args.execBlue.On("Close").Return(nil).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + }, + wantErr: true, + }, + { + name: "error on exec blue", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: nil, + execBlueBootstrap: nil, + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Close").Return(nil).Once() + args.execBlue.On("Close").Return(errors.New("failed")).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + }, + wantErr: true, + }, + { + name: "error on commit blue bootstrap", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: mocktypes.NewCCIPOracle(t), + execBlueBootstrap: nil, + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Close").Return(nil).Once() + args.commitBlueBootstrap.On("Close").Return(errors.New("failed")).Once() + args.execBlue.On("Close").Return(nil).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.commitBlueBootstrap.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + }, + wantErr: true, + }, + { + name: "error on exec blue bootstrap", + args: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: nil, + execBlueBootstrap: mocktypes.NewCCIPOracle(t), + }, + expect: func(t *testing.T, args args) { + args.commitBlue.On("Close").Return(nil).Once() + args.execBlue.On("Close").Return(nil).Once() + args.execBlueBootstrap.On("Close").Return(errors.New("failed")).Once() + }, + asserts: func(t *testing.T, args args) { + args.commitBlue.AssertExpectations(t) + args.execBlue.AssertExpectations(t) + args.execBlueBootstrap.AssertExpectations(t) + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &ccipDeployment{ + commit: blueGreenDeployment{ + blue: tt.args.commitBlue, + }, + exec: blueGreenDeployment{ + blue: tt.args.execBlue, + }, + } + if tt.args.commitBlueBootstrap != nil { + c.commit.bootstrapBlue = tt.args.commitBlueBootstrap + } + if tt.args.execBlueBootstrap != nil { + c.exec.bootstrapBlue = tt.args.execBlueBootstrap + } + + tt.expect(t, tt.args) + defer tt.asserts(t, tt.args) + err := c.CloseBlue() + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func Test_ccipDeployment_HandleBlueGreen_PrevDeploymentNil(t *testing.T) { + require.Error(t, (&ccipDeployment{}).HandleBlueGreen(nil)) +} + +func Test_ccipDeployment_HandleBlueGreen(t *testing.T) { + type args struct { + commitBlue *mocktypes.CCIPOracle + commitBlueBootstrap *mocktypes.CCIPOracle + commitGreen *mocktypes.CCIPOracle + commitGreenBootstrap *mocktypes.CCIPOracle + execBlue *mocktypes.CCIPOracle + execBlueBootstrap *mocktypes.CCIPOracle + execGreen *mocktypes.CCIPOracle + execGreenBootstrap *mocktypes.CCIPOracle + } + tests := []struct { + name string + argsPrevDeployment args + argsFutureDeployment args + expect func(t *testing.T, args args, argsPrevDeployment args) + asserts func(t *testing.T, args args, argsPrevDeployment args) + wantErr bool + }{ + { + name: "promotion blue to green, no bootstrap", + argsPrevDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitGreen: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + execGreen: mocktypes.NewCCIPOracle(t), + }, + argsFutureDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitGreen: nil, + execBlue: mocktypes.NewCCIPOracle(t), + execGreen: nil, + }, + expect: func(t *testing.T, args args, argsPrevDeployment args) { + argsPrevDeployment.commitBlue.On("Close").Return(nil).Once() + argsPrevDeployment.execBlue.On("Close").Return(nil).Once() + }, + asserts: func(t *testing.T, args args, argsPrevDeployment args) { + argsPrevDeployment.commitBlue.AssertExpectations(t) + argsPrevDeployment.execBlue.AssertExpectations(t) + }, + wantErr: false, + }, + { + name: "promotion blue to green, with bootstrap", + argsPrevDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: mocktypes.NewCCIPOracle(t), + commitGreen: mocktypes.NewCCIPOracle(t), + commitGreenBootstrap: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + execBlueBootstrap: mocktypes.NewCCIPOracle(t), + execGreen: mocktypes.NewCCIPOracle(t), + execGreenBootstrap: mocktypes.NewCCIPOracle(t), + }, + argsFutureDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: mocktypes.NewCCIPOracle(t), + commitGreen: nil, + commitGreenBootstrap: nil, + execBlue: mocktypes.NewCCIPOracle(t), + execBlueBootstrap: mocktypes.NewCCIPOracle(t), + execGreen: nil, + execGreenBootstrap: nil, + }, + expect: func(t *testing.T, args args, argsPrevDeployment args) { + argsPrevDeployment.commitBlue.On("Close").Return(nil).Once() + argsPrevDeployment.commitBlueBootstrap.On("Close").Return(nil).Once() + argsPrevDeployment.execBlue.On("Close").Return(nil).Once() + argsPrevDeployment.execBlueBootstrap.On("Close").Return(nil).Once() + }, + asserts: func(t *testing.T, args args, argsPrevDeployment args) { + argsPrevDeployment.commitBlue.AssertExpectations(t) + argsPrevDeployment.commitBlueBootstrap.AssertExpectations(t) + argsPrevDeployment.execBlue.AssertExpectations(t) + argsPrevDeployment.execBlueBootstrap.AssertExpectations(t) + }, + wantErr: false, + }, + { + name: "new green deployment, no bootstrap", + argsPrevDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitGreen: nil, + execBlue: mocktypes.NewCCIPOracle(t), + execGreen: nil, + }, + argsFutureDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitGreen: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + execGreen: mocktypes.NewCCIPOracle(t), + }, + expect: func(t *testing.T, args args, argsPrevDeployment args) { + args.commitGreen.On("Start").Return(nil).Once() + args.execGreen.On("Start").Return(nil).Once() + }, + asserts: func(t *testing.T, args args, argsPrevDeployment args) { + args.commitGreen.AssertExpectations(t) + args.execGreen.AssertExpectations(t) + }, + wantErr: false, + }, + { + name: "new green deployment, with bootstrap", + argsPrevDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: mocktypes.NewCCIPOracle(t), + commitGreen: nil, + commitGreenBootstrap: nil, + execBlue: mocktypes.NewCCIPOracle(t), + execBlueBootstrap: mocktypes.NewCCIPOracle(t), + execGreen: nil, + execGreenBootstrap: nil, + }, + argsFutureDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: mocktypes.NewCCIPOracle(t), + commitGreen: mocktypes.NewCCIPOracle(t), + commitGreenBootstrap: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + execBlueBootstrap: mocktypes.NewCCIPOracle(t), + execGreen: mocktypes.NewCCIPOracle(t), + execGreenBootstrap: mocktypes.NewCCIPOracle(t), + }, + expect: func(t *testing.T, args args, argsPrevDeployment args) { + args.commitGreen.On("Start").Return(nil).Once() + args.commitGreenBootstrap.On("Start").Return(nil).Once() + args.execGreen.On("Start").Return(nil).Once() + args.execGreenBootstrap.On("Start").Return(nil).Once() + }, + asserts: func(t *testing.T, args args, argsPrevDeployment args) { + args.commitGreen.AssertExpectations(t) + args.commitGreenBootstrap.AssertExpectations(t) + args.execGreen.AssertExpectations(t) + args.execGreenBootstrap.AssertExpectations(t) + }, + wantErr: false, + }, + { + name: "error on commit green start", + argsPrevDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitGreen: nil, + execBlue: mocktypes.NewCCIPOracle(t), + execGreen: nil, + }, + argsFutureDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitGreen: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + execGreen: mocktypes.NewCCIPOracle(t), + }, + expect: func(t *testing.T, args args, argsPrevDeployment args) { + args.commitGreen.On("Start").Return(errors.New("failed")).Once() + args.execGreen.On("Start").Return(nil).Once() + }, + asserts: func(t *testing.T, args args, argsPrevDeployment args) { + args.commitGreen.AssertExpectations(t) + args.execGreen.AssertExpectations(t) + }, + wantErr: true, + }, + { + name: "error on exec green start", + argsPrevDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitGreen: nil, + execBlue: mocktypes.NewCCIPOracle(t), + execGreen: nil, + }, + argsFutureDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitGreen: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + execGreen: mocktypes.NewCCIPOracle(t), + }, + expect: func(t *testing.T, args args, argsPrevDeployment args) { + args.commitGreen.On("Start").Return(nil).Once() + args.execGreen.On("Start").Return(errors.New("failed")).Once() + }, + asserts: func(t *testing.T, args args, argsPrevDeployment args) { + args.commitGreen.AssertExpectations(t) + args.execGreen.AssertExpectations(t) + }, + wantErr: true, + }, + { + name: "error on commit green bootstrap start", + argsPrevDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: mocktypes.NewCCIPOracle(t), + commitGreen: nil, + commitGreenBootstrap: nil, + execBlue: mocktypes.NewCCIPOracle(t), + execBlueBootstrap: mocktypes.NewCCIPOracle(t), + execGreen: nil, + execGreenBootstrap: nil, + }, + argsFutureDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitBlueBootstrap: mocktypes.NewCCIPOracle(t), + commitGreen: mocktypes.NewCCIPOracle(t), + commitGreenBootstrap: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + execBlueBootstrap: mocktypes.NewCCIPOracle(t), + execGreen: mocktypes.NewCCIPOracle(t), + execGreenBootstrap: mocktypes.NewCCIPOracle(t), + }, + expect: func(t *testing.T, args args, argsPrevDeployment args) { + args.commitGreen.On("Start").Return(nil).Once() + args.commitGreenBootstrap.On("Start").Return(errors.New("failed")).Once() + args.execGreen.On("Start").Return(nil).Once() + args.execGreenBootstrap.On("Start").Return(nil).Once() + }, + asserts: func(t *testing.T, args args, argsPrevDeployment args) { + args.commitGreen.AssertExpectations(t) + args.commitGreenBootstrap.AssertExpectations(t) + args.execGreen.AssertExpectations(t) + args.execGreenBootstrap.AssertExpectations(t) + }, + wantErr: true, + }, + { + name: "invalid blue-green deployment transition commit: both prev and future deployment have green", + argsPrevDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitGreen: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + execGreen: mocktypes.NewCCIPOracle(t), + }, + argsFutureDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitGreen: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + execGreen: mocktypes.NewCCIPOracle(t), + }, + expect: func(t *testing.T, args args, argsPrevDeployment args) {}, + asserts: func(t *testing.T, args args, argsPrevDeployment args) {}, + wantErr: true, + }, + { + name: "invalid blue-green deployment transition exec: both prev and future deployment have green", + argsPrevDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitGreen: nil, + execBlue: mocktypes.NewCCIPOracle(t), + execGreen: mocktypes.NewCCIPOracle(t), + }, + argsFutureDeployment: args{ + commitBlue: mocktypes.NewCCIPOracle(t), + commitGreen: mocktypes.NewCCIPOracle(t), + execBlue: mocktypes.NewCCIPOracle(t), + execGreen: mocktypes.NewCCIPOracle(t), + }, + expect: func(t *testing.T, args args, argsPrevDeployment args) { + args.commitGreen.On("Start").Return(nil).Once() + }, + asserts: func(t *testing.T, args args, argsPrevDeployment args) { + args.commitGreen.AssertExpectations(t) + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + futDeployment := &ccipDeployment{ + commit: blueGreenDeployment{ + blue: tt.argsFutureDeployment.commitBlue, + }, + exec: blueGreenDeployment{ + blue: tt.argsFutureDeployment.execBlue, + }, + } + if tt.argsFutureDeployment.commitGreen != nil { + futDeployment.commit.green = tt.argsFutureDeployment.commitGreen + } + if tt.argsFutureDeployment.commitBlueBootstrap != nil { + futDeployment.commit.bootstrapBlue = tt.argsFutureDeployment.commitBlueBootstrap + } + if tt.argsFutureDeployment.commitGreenBootstrap != nil { + futDeployment.commit.bootstrapGreen = tt.argsFutureDeployment.commitGreenBootstrap + } + if tt.argsFutureDeployment.execGreen != nil { + futDeployment.exec.green = tt.argsFutureDeployment.execGreen + } + if tt.argsFutureDeployment.execBlueBootstrap != nil { + futDeployment.exec.bootstrapBlue = tt.argsFutureDeployment.execBlueBootstrap + } + if tt.argsFutureDeployment.execGreenBootstrap != nil { + futDeployment.exec.bootstrapGreen = tt.argsFutureDeployment.execGreenBootstrap + } + + prevDeployment := &ccipDeployment{ + commit: blueGreenDeployment{ + blue: tt.argsPrevDeployment.commitBlue, + }, + exec: blueGreenDeployment{ + blue: tt.argsPrevDeployment.execBlue, + }, + } + if tt.argsPrevDeployment.commitGreen != nil { + prevDeployment.commit.green = tt.argsPrevDeployment.commitGreen + } + if tt.argsPrevDeployment.commitBlueBootstrap != nil { + prevDeployment.commit.bootstrapBlue = tt.argsPrevDeployment.commitBlueBootstrap + } + if tt.argsPrevDeployment.commitGreenBootstrap != nil { + prevDeployment.commit.bootstrapGreen = tt.argsPrevDeployment.commitGreenBootstrap + } + if tt.argsPrevDeployment.execGreen != nil { + prevDeployment.exec.green = tt.argsPrevDeployment.execGreen + } + if tt.argsPrevDeployment.execBlueBootstrap != nil { + prevDeployment.exec.bootstrapBlue = tt.argsPrevDeployment.execBlueBootstrap + } + if tt.argsPrevDeployment.execGreenBootstrap != nil { + prevDeployment.exec.bootstrapGreen = tt.argsPrevDeployment.execGreenBootstrap + } + + tt.expect(t, tt.argsFutureDeployment, tt.argsPrevDeployment) + defer tt.asserts(t, tt.argsFutureDeployment, tt.argsPrevDeployment) + err := futDeployment.HandleBlueGreen(prevDeployment) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func Test_isNewGreenInstance(t *testing.T) { + type args struct { + pluginType cctypes.PluginType + ocrConfigs []ccipreaderpkg.OCR3ConfigWithMeta + prevDeployment ccipDeployment + } + tests := []struct { + name string + args args + want bool + }{ + { + "prev deployment only blue", + args{ + pluginType: cctypes.PluginTypeCCIPCommit, + ocrConfigs: []ccipreaderpkg.OCR3ConfigWithMeta{ + {}, {}, + }, + prevDeployment: ccipDeployment{ + commit: blueGreenDeployment{ + blue: mocktypes.NewCCIPOracle(t), + }, + }, + }, + true, + }, + { + "green -> blue promotion", + args{ + pluginType: cctypes.PluginTypeCCIPCommit, + ocrConfigs: []ccipreaderpkg.OCR3ConfigWithMeta{ + {}, + }, + prevDeployment: ccipDeployment{ + commit: blueGreenDeployment{ + blue: mocktypes.NewCCIPOracle(t), + green: mocktypes.NewCCIPOracle(t), + }, + }, + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := isNewGreenInstance(tt.args.pluginType, tt.args.ocrConfigs, tt.args.prevDeployment) + require.Equal(t, tt.want, got) + }) + } +} + +func Test_isPromotion(t *testing.T) { + type args struct { + pluginType cctypes.PluginType + ocrConfigs []ccipreaderpkg.OCR3ConfigWithMeta + prevDeployment ccipDeployment + } + tests := []struct { + name string + args args + want bool + }{ + { + "prev deployment only blue", + args{ + pluginType: cctypes.PluginTypeCCIPCommit, + ocrConfigs: []ccipreaderpkg.OCR3ConfigWithMeta{ + {}, {}, + }, + prevDeployment: ccipDeployment{ + commit: blueGreenDeployment{ + blue: mocktypes.NewCCIPOracle(t), + }, + }, + }, + false, + }, + { + "green -> blue promotion", + args{ + pluginType: cctypes.PluginTypeCCIPCommit, + ocrConfigs: []ccipreaderpkg.OCR3ConfigWithMeta{ + {}, + }, + prevDeployment: ccipDeployment{ + commit: blueGreenDeployment{ + blue: mocktypes.NewCCIPOracle(t), + green: mocktypes.NewCCIPOracle(t), + }, + }, + }, + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isPromotion(tt.args.pluginType, tt.args.ocrConfigs, tt.args.prevDeployment); got != tt.want { + t.Errorf("isPromotion() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_ccipDeployment_HasGreenInstance(t *testing.T) { + type fields struct { + commit blueGreenDeployment + exec blueGreenDeployment + } + type args struct { + pluginType cctypes.PluginType + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + "commit green present", + fields{ + commit: blueGreenDeployment{ + blue: mocktypes.NewCCIPOracle(t), + green: mocktypes.NewCCIPOracle(t), + }, + }, + args{ + pluginType: cctypes.PluginTypeCCIPCommit, + }, + true, + }, + { + "commit green not present", + fields{ + commit: blueGreenDeployment{ + blue: mocktypes.NewCCIPOracle(t), + }, + }, + args{ + pluginType: cctypes.PluginTypeCCIPCommit, + }, + false, + }, + { + "exec green present", + fields{ + exec: blueGreenDeployment{ + blue: mocktypes.NewCCIPOracle(t), + green: mocktypes.NewCCIPOracle(t), + }, + }, + args{ + pluginType: cctypes.PluginTypeCCIPExec, + }, + true, + }, + { + "exec green not present", + fields{ + exec: blueGreenDeployment{ + blue: mocktypes.NewCCIPOracle(t), + }, + }, + args{ + pluginType: cctypes.PluginTypeCCIPExec, + }, + false, + }, + { + "invalid plugin type", + fields{}, + args{ + pluginType: cctypes.PluginType(100), + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &ccipDeployment{} + if tt.fields.commit.blue != nil { + c.commit.blue = tt.fields.commit.blue + } + if tt.fields.commit.green != nil { + c.commit.green = tt.fields.commit.green + } + if tt.fields.exec.blue != nil { + c.exec.blue = tt.fields.exec.blue + } + if tt.fields.exec.green != nil { + c.exec.green = tt.fields.exec.green + } + got := c.HasGreenInstance(tt.args.pluginType) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/core/capabilities/ccip/launcher/ccip_capability_launcher.png b/core/capabilities/ccip/launcher/ccip_capability_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..5e90d5ff7daa3643fde8185f49b4c83840b2c298 GIT binary patch literal 253433 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

}`U=fGbo%nS)w* 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%0DbN!YhqTG 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„JG$8DMFvpn~5IY)uJ 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

q)*yZZGJ|G0u*#`zvW(_ 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

esQpm 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

j#E!8a{;^ic**L3%PFs%{p91&%QJl)>04h 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 q^%lt6$0)pb>!L#|YvY`t>=p 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 literal 0 HcmV?d00001 diff --git a/core/capabilities/ccip/launcher/diff.go b/core/capabilities/ccip/launcher/diff.go new file mode 100644 index 00000000000..e631ea9fc78 --- /dev/null +++ b/core/capabilities/ccip/launcher/diff.go @@ -0,0 +1,141 @@ +package launcher + +import ( + "fmt" + + ragep2ptypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/chainlink/v2/core/services/registrysyncer" +) + +// diffResult contains the added, removed and updated CCIP DONs. +// It is determined by using the `diff` function below. +type diffResult struct { + added map[registrysyncer.DonID]registrysyncer.DON + removed map[registrysyncer.DonID]registrysyncer.DON + updated map[registrysyncer.DonID]registrysyncer.DON +} + +// diff compares the old and new state and returns the added, removed and updated CCIP DONs. +func diff( + capabilityID string, + oldState, + newState registrysyncer.LocalRegistry, +) (diffResult, error) { + ccipCapability, err := checkCapabilityPresence(capabilityID, newState) + if err != nil { + return diffResult{}, fmt.Errorf("failed to check capability presence: %w", err) + } + + newCCIPDONs, err := filterCCIPDONs(ccipCapability, newState) + if err != nil { + return diffResult{}, fmt.Errorf("failed to filter CCIP DONs from new state: %w", err) + } + + currCCIPDONs, err := filterCCIPDONs(ccipCapability, oldState) + if err != nil { + return diffResult{}, fmt.Errorf("failed to filter CCIP DONs from old state: %w", err) + } + + // compare curr with new and launch or update OCR instances as needed + diffRes, err := compareDONs(currCCIPDONs, newCCIPDONs) + if err != nil { + return diffResult{}, fmt.Errorf("failed to compare CCIP DONs: %w", err) + } + + return diffRes, nil +} + +// compareDONs compares the current and new CCIP DONs and returns the added, removed and updated DONs. +func compareDONs( + currCCIPDONs, + newCCIPDONs map[registrysyncer.DonID]registrysyncer.DON, +) ( + dr diffResult, + err error, +) { + added := make(map[registrysyncer.DonID]registrysyncer.DON) + removed := make(map[registrysyncer.DonID]registrysyncer.DON) + updated := make(map[registrysyncer.DonID]registrysyncer.DON) + + for id, don := range newCCIPDONs { + if currDONState, ok := currCCIPDONs[id]; !ok { + // Not in current state, so mark as added. + added[id] = don + } else { + // If its in the current state and the config count for the DON has changed, mark as updated. + // Since the registry returns the full state we need to compare the config count. + if don.ConfigVersion > currDONState.ConfigVersion { + updated[id] = don + } + } + } + + for id, don := range currCCIPDONs { + if _, ok := newCCIPDONs[id]; !ok { + // In current state but not in latest registry state, so should remove. + removed[id] = don + } + } + + return diffResult{ + added: added, + removed: removed, + updated: updated, + }, nil +} + +// filterCCIPDONs filters the CCIP DONs from the given state. +func filterCCIPDONs( + ccipCapability registrysyncer.Capability, + state registrysyncer.LocalRegistry, +) (map[registrysyncer.DonID]registrysyncer.DON, error) { + ccipDONs := make(map[registrysyncer.DonID]registrysyncer.DON) + for _, don := range state.IDsToDONs { + _, ok := don.CapabilityConfigurations[ccipCapability.ID] + if ok { + ccipDONs[registrysyncer.DonID(don.ID)] = don + } + } + + return ccipDONs, nil +} + +// checkCapabilityPresence checks if the capability with the given capabilityID +// is present in the given capability registry state. +func checkCapabilityPresence( + capabilityID string, + state registrysyncer.LocalRegistry, +) (registrysyncer.Capability, error) { + // Sanity check to make sure the capability registry has the capability we are looking for. + ccipCapability, ok := state.IDsToCapabilities[capabilityID] + if !ok { + return registrysyncer.Capability{}, + fmt.Errorf("failed to find capability with capabilityID %s in capability registry state", capabilityID) + } + + return ccipCapability, nil +} + +// isMemberOfDON returns true if and only if the given p2pID is a member of the given DON. +func isMemberOfDON(don registrysyncer.DON, p2pID ragep2ptypes.PeerID) bool { + for _, node := range don.Members { + if node == p2pID { + return true + } + } + return false +} + +// isMemberOfBootstrapSubcommittee returns true if and only if the given p2pID is a member of the given bootstrap subcommittee. +func isMemberOfBootstrapSubcommittee( + bootstrapP2PIDs [][32]byte, + p2pID ragep2ptypes.PeerID, +) bool { + for _, bootstrapID := range bootstrapP2PIDs { + if bootstrapID == p2pID { + return true + } + } + return false +} diff --git a/core/capabilities/ccip/launcher/diff_test.go b/core/capabilities/ccip/launcher/diff_test.go new file mode 100644 index 00000000000..f3dd327fe91 --- /dev/null +++ b/core/capabilities/ccip/launcher/diff_test.go @@ -0,0 +1,352 @@ +package launcher + +import ( + "math/big" + "reflect" + "testing" + + ragep2ptypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/stretchr/testify/require" + + kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" + "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" + "github.com/smartcontractkit/chainlink/v2/core/services/registrysyncer" +) + +func Test_diff(t *testing.T) { + type args struct { + capabilityID string + oldState registrysyncer.LocalRegistry + newState registrysyncer.LocalRegistry + } + tests := []struct { + name string + args args + want diffResult + wantErr bool + }{ + { + name: "no diff", + args: args{ + capabilityID: defaultCapability.ID, + oldState: registrysyncer.LocalRegistry{ + IDsToCapabilities: map[string]registrysyncer.Capability{ + defaultCapability.ID: defaultCapability, + }, + IDsToDONs: map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + IDsToNodes: map[types.PeerID]kcr.CapabilitiesRegistryNodeInfo{}, + }, + newState: registrysyncer.LocalRegistry{ + IDsToCapabilities: map[string]registrysyncer.Capability{ + defaultCapability.ID: defaultCapability, + }, + IDsToDONs: map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + IDsToNodes: map[types.PeerID]kcr.CapabilitiesRegistryNodeInfo{}, + }, + }, + want: diffResult{ + added: map[registrysyncer.DonID]registrysyncer.DON{}, + removed: map[registrysyncer.DonID]registrysyncer.DON{}, + updated: map[registrysyncer.DonID]registrysyncer.DON{}, + }, + }, + { + "capability not present", + args{ + capabilityID: defaultCapability.ID, + oldState: registrysyncer.LocalRegistry{ + IDsToCapabilities: map[string]registrysyncer.Capability{ + newCapability.ID: newCapability, + }, + }, + newState: registrysyncer.LocalRegistry{ + IDsToCapabilities: map[string]registrysyncer.Capability{ + newCapability.ID: newCapability, + }, + }, + }, + diffResult{}, + true, + }, + { + "diff present, new don", + args{ + capabilityID: defaultCapability.ID, + oldState: registrysyncer.LocalRegistry{ + IDsToCapabilities: map[string]registrysyncer.Capability{ + defaultCapability.ID: defaultCapability, + }, + IDsToDONs: map[registrysyncer.DonID]registrysyncer.DON{}, + }, + newState: registrysyncer.LocalRegistry{ + IDsToCapabilities: map[string]registrysyncer.Capability{ + defaultCapability.ID: defaultCapability, + }, + IDsToDONs: map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + }, + }, + diffResult{ + added: map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + removed: map[registrysyncer.DonID]registrysyncer.DON{}, + updated: map[registrysyncer.DonID]registrysyncer.DON{}, + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := diff(tt.args.capabilityID, tt.args.oldState, tt.args.newState) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.want, got) + } + }) + } +} + +func Test_compareDONs(t *testing.T) { + type args struct { + currCCIPDONs map[registrysyncer.DonID]registrysyncer.DON + newCCIPDONs map[registrysyncer.DonID]registrysyncer.DON + } + tests := []struct { + name string + args args + wantAdded map[registrysyncer.DonID]registrysyncer.DON + wantRemoved map[registrysyncer.DonID]registrysyncer.DON + wantUpdated map[registrysyncer.DonID]registrysyncer.DON + wantErr bool + }{ + { + "added dons", + args{ + currCCIPDONs: map[registrysyncer.DonID]registrysyncer.DON{}, + newCCIPDONs: map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + }, + map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + map[registrysyncer.DonID]registrysyncer.DON{}, + map[registrysyncer.DonID]registrysyncer.DON{}, + false, + }, + { + "removed dons", + args{ + currCCIPDONs: map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + newCCIPDONs: map[registrysyncer.DonID]registrysyncer.DON{}, + }, + map[registrysyncer.DonID]registrysyncer.DON{}, + map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + map[registrysyncer.DonID]registrysyncer.DON{}, + false, + }, + { + "updated dons", + args{ + currCCIPDONs: map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + newCCIPDONs: map[registrysyncer.DonID]registrysyncer.DON{ + 1: { + DON: getDON(defaultRegistryDon.ID, defaultRegistryDon.Members, defaultRegistryDon.ConfigVersion+1), + CapabilityConfigurations: defaultCapCfgs, + }, + }, + }, + map[registrysyncer.DonID]registrysyncer.DON{}, + map[registrysyncer.DonID]registrysyncer.DON{}, + map[registrysyncer.DonID]registrysyncer.DON{ + 1: { + DON: getDON(defaultRegistryDon.ID, defaultRegistryDon.Members, defaultRegistryDon.ConfigVersion+1), + CapabilityConfigurations: defaultCapCfgs, + }, + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dr, err := compareDONs(tt.args.currCCIPDONs, tt.args.newCCIPDONs) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.wantAdded, dr.added) + require.Equal(t, tt.wantRemoved, dr.removed) + require.Equal(t, tt.wantUpdated, dr.updated) + } + }) + } +} + +func Test_filterCCIPDONs(t *testing.T) { + type args struct { + ccipCapability registrysyncer.Capability + state registrysyncer.LocalRegistry + } + tests := []struct { + name string + args args + want map[registrysyncer.DonID]registrysyncer.DON + wantErr bool + }{ + { + "one ccip don", + args{ + ccipCapability: defaultCapability, + state: registrysyncer.LocalRegistry{ + IDsToDONs: map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + }, + }, + map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + false, + }, + { + "no ccip dons - different capability", + args{ + ccipCapability: newCapability, + state: registrysyncer.LocalRegistry{ + IDsToDONs: map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + }, + }, + map[registrysyncer.DonID]registrysyncer.DON{}, + false, + }, + { + "don with multiple capabilities, one of them ccip", + args{ + ccipCapability: defaultCapability, + state: registrysyncer.LocalRegistry{ + IDsToDONs: map[registrysyncer.DonID]registrysyncer.DON{ + 1: { + DON: getDON(1, []ragep2ptypes.PeerID{p2pID1}, 0), + CapabilityConfigurations: map[string]registrysyncer.CapabilityConfiguration{ + defaultCapability.ID: {}, + newCapability.ID: {}, + }, + }, + }, + }, + }, + map[registrysyncer.DonID]registrysyncer.DON{ + 1: { + DON: getDON(1, []ragep2ptypes.PeerID{p2pID1}, 0), + CapabilityConfigurations: map[string]registrysyncer.CapabilityConfiguration{ + defaultCapability.ID: {}, + newCapability.ID: {}, + }, + }, + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := filterCCIPDONs(tt.args.ccipCapability, tt.args.state) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.want, got) + } + }) + } +} + +func Test_checkCapabilityPresence(t *testing.T) { + type args struct { + capabilityID string + state registrysyncer.LocalRegistry + } + tests := []struct { + name string + args args + want registrysyncer.Capability + wantErr bool + }{ + { + "in registry state", + args{ + capabilityID: defaultCapability.ID, + state: registrysyncer.LocalRegistry{ + IDsToCapabilities: map[string]registrysyncer.Capability{ + defaultCapability.ID: defaultCapability, + }, + }, + }, + defaultCapability, + false, + }, + { + "not in registry state", + args{ + capabilityID: defaultCapability.ID, + state: registrysyncer.LocalRegistry{ + IDsToCapabilities: map[string]registrysyncer.Capability{ + newCapability.ID: newCapability, + }, + }, + }, + registrysyncer.Capability{}, + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := checkCapabilityPresence(tt.args.capabilityID, tt.args.state) + if (err != nil) != tt.wantErr { + t.Errorf("checkCapabilityPresence() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("checkCapabilityPresence() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_isMemberOfDON(t *testing.T) { + var p2pIDs []ragep2ptypes.PeerID + for i := range [4]struct{}{} { + p2pIDs = append(p2pIDs, ragep2ptypes.PeerID(p2pkey.MustNewV2XXXTestingOnly(big.NewInt(int64(i+1))).PeerID())) + } + don := registrysyncer.DON{ + DON: getDON(1, p2pIDs, 0), + } + require.True(t, isMemberOfDON(don, ragep2ptypes.PeerID(p2pkey.MustNewV2XXXTestingOnly(big.NewInt(1)).PeerID()))) + require.False(t, isMemberOfDON(don, ragep2ptypes.PeerID(p2pkey.MustNewV2XXXTestingOnly(big.NewInt(5)).PeerID()))) +} + +func Test_isMemberOfBootstrapSubcommittee(t *testing.T) { + var bootstrapKeys [][32]byte + for i := range [4]struct{}{} { + bootstrapKeys = append(bootstrapKeys, p2pkey.MustNewV2XXXTestingOnly(big.NewInt(int64(i+1))).PeerID()) + } + require.True(t, isMemberOfBootstrapSubcommittee(bootstrapKeys, p2pID1)) + require.False(t, isMemberOfBootstrapSubcommittee(bootstrapKeys, getP2PID(5))) +} diff --git a/core/capabilities/ccip/launcher/integration_test.go b/core/capabilities/ccip/launcher/integration_test.go new file mode 100644 index 00000000000..db3daf4d9b9 --- /dev/null +++ b/core/capabilities/ccip/launcher/integration_test.go @@ -0,0 +1,120 @@ +package launcher + +import ( + "testing" + "time" + + it "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ccip_integration_tests/integrationhelpers" + cctypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" + p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" + + "github.com/onsi/gomega" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/ccip_config" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/registrysyncer" +) + +func TestIntegration_Launcher(t *testing.T) { + ctx := testutils.Context(t) + lggr := logger.TestLogger(t) + uni := it.NewTestUniverse(ctx, t, lggr) + // We need 3*f + 1 p2pIDs to have enough nodes to bootstrap + var arr []int64 + n := int(it.FChainA*3 + 1) + for i := 0; i <= n; i++ { + arr = append(arr, int64(i)) + } + p2pIDs := it.P2pIDsFromInts(arr) + uni.AddCapability(p2pIDs) + + regSyncer, err := registrysyncer.New(lggr, + func() (p2ptypes.PeerID, error) { + return p2pIDs[0], nil + }, + uni, + uni.CapReg.Address().String(), + ) + require.NoError(t, err) + + hcr := uni.HomeChainReader + + launcher := New( + it.CapabilityID, + p2pIDs[0], + logger.TestLogger(t), + hcr, + &oracleCreatorPrints{ + t: t, + }, + 1*time.Second, + ) + regSyncer.AddLauncher(launcher) + + require.NoError(t, launcher.Start(ctx)) + require.NoError(t, regSyncer.Start(ctx)) + t.Cleanup(func() { require.NoError(t, regSyncer.Close()) }) + t.Cleanup(func() { require.NoError(t, launcher.Close()) }) + + chainAConf := it.SetupConfigInfo(it.ChainA, p2pIDs, it.FChainA, []byte("ChainA")) + chainBConf := it.SetupConfigInfo(it.ChainB, p2pIDs[1:], it.FChainB, []byte("ChainB")) + chainCConf := it.SetupConfigInfo(it.ChainC, p2pIDs[2:], it.FChainC, []byte("ChainC")) + inputConfig := []ccip_config.CCIPConfigTypesChainConfigInfo{ + chainAConf, + chainBConf, + chainCConf, + } + _, err = uni.CcipCfg.ApplyChainConfigUpdates(uni.Transactor, nil, inputConfig) + require.NoError(t, err) + uni.Backend.Commit() + + ccipCapabilityID, err := uni.CapReg.GetHashedCapabilityId(nil, it.CcipCapabilityLabelledName, it.CcipCapabilityVersion) + require.NoError(t, err) + + uni.AddDONToRegistry( + ccipCapabilityID, + it.ChainA, + it.FChainA, + p2pIDs[1], + p2pIDs) + + gomega.NewWithT(t).Eventually(func() bool { + return len(launcher.runningDONIDs()) == 1 + }, testutils.WaitTimeout(t), testutils.TestInterval).Should(gomega.BeTrue()) +} + +type oraclePrints struct { + t *testing.T + pluginType cctypes.PluginType + config cctypes.OCR3ConfigWithMeta + isBootstrap bool +} + +func (o *oraclePrints) Start() error { + o.t.Logf("Starting oracle (pluginType: %s, isBootstrap: %t) with config %+v\n", o.pluginType, o.isBootstrap, o.config) + return nil +} + +func (o *oraclePrints) Close() error { + o.t.Logf("Closing oracle (pluginType: %s, isBootstrap: %t) with config %+v\n", o.pluginType, o.isBootstrap, o.config) + return nil +} + +type oracleCreatorPrints struct { + t *testing.T +} + +func (o *oracleCreatorPrints) CreatePluginOracle(pluginType cctypes.PluginType, config cctypes.OCR3ConfigWithMeta) (cctypes.CCIPOracle, error) { + o.t.Logf("Creating plugin oracle (pluginType: %s) with config %+v\n", pluginType, config) + return &oraclePrints{pluginType: pluginType, config: config, t: o.t}, nil +} + +func (o *oracleCreatorPrints) CreateBootstrapOracle(config cctypes.OCR3ConfigWithMeta) (cctypes.CCIPOracle, error) { + o.t.Logf("Creating bootstrap oracle with config %+v\n", config) + return &oraclePrints{pluginType: cctypes.PluginTypeCCIPCommit, config: config, isBootstrap: true, t: o.t}, nil +} + +var _ cctypes.OracleCreator = &oracleCreatorPrints{} +var _ cctypes.CCIPOracle = &oraclePrints{} diff --git a/core/capabilities/ccip/launcher/launcher.go b/core/capabilities/ccip/launcher/launcher.go new file mode 100644 index 00000000000..2dc1a1954f5 --- /dev/null +++ b/core/capabilities/ccip/launcher/launcher.go @@ -0,0 +1,432 @@ +package launcher + +import ( + "context" + "fmt" + "sync" + "time" + + cctypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/job" + p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" + "github.com/smartcontractkit/chainlink/v2/core/services/registrysyncer" + + "go.uber.org/multierr" + + ragep2ptypes "github.com/smartcontractkit/libocr/ragep2p/types" + + ccipreader "github.com/smartcontractkit/chainlink-ccip/pkg/reader" + + "github.com/smartcontractkit/chainlink-common/pkg/services" + + kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" +) + +var ( + _ job.ServiceCtx = (*launcher)(nil) + _ registrysyncer.Launcher = (*launcher)(nil) +) + +func New( + capabilityID string, + p2pID ragep2ptypes.PeerID, + lggr logger.Logger, + homeChainReader ccipreader.HomeChain, + oracleCreator cctypes.OracleCreator, + tickInterval time.Duration, +) *launcher { + return &launcher{ + p2pID: p2pID, + capabilityID: capabilityID, + lggr: lggr, + homeChainReader: homeChainReader, + regState: registrysyncer.LocalRegistry{ + IDsToDONs: make(map[registrysyncer.DonID]registrysyncer.DON), + IDsToNodes: make(map[p2ptypes.PeerID]kcr.CapabilitiesRegistryNodeInfo), + IDsToCapabilities: make(map[string]registrysyncer.Capability), + }, + oracleCreator: oracleCreator, + dons: make(map[registrysyncer.DonID]*ccipDeployment), + tickInterval: tickInterval, + } +} + +// launcher manages the lifecycles of the CCIP capability on all chains. +type launcher struct { + services.StateMachine + + capabilityID string + p2pID ragep2ptypes.PeerID + lggr logger.Logger + homeChainReader ccipreader.HomeChain + stopChan chan struct{} + // latestState is the latest capability registry state received from the syncer. + latestState registrysyncer.LocalRegistry + // regState is the latest capability registry state that we have successfully processed. + regState registrysyncer.LocalRegistry + oracleCreator cctypes.OracleCreator + lock sync.RWMutex + wg sync.WaitGroup + tickInterval time.Duration + + // dons is a map of CCIP DON IDs to the OCR instances that are running on them. + // we can have up to two OCR instances per CCIP plugin, since we are running two plugins, + // thats four OCR instances per CCIP DON maximum. + dons map[registrysyncer.DonID]*ccipDeployment +} + +// Launch implements registrysyncer.Launcher. +func (l *launcher) Launch(ctx context.Context, state *registrysyncer.LocalRegistry) error { + l.lock.Lock() + defer l.lock.Unlock() + l.lggr.Debugw("Received new state from syncer", "dons", state.IDsToDONs) + l.latestState = *state + return nil +} + +func (l *launcher) getLatestState() registrysyncer.LocalRegistry { + l.lock.RLock() + defer l.lock.RUnlock() + return l.latestState +} + +func (l *launcher) runningDONIDs() []registrysyncer.DonID { + l.lock.RLock() + defer l.lock.RUnlock() + var runningDONs []registrysyncer.DonID + for id := range l.dons { + runningDONs = append(runningDONs, id) + } + return runningDONs +} + +// Close implements job.ServiceCtx. +func (l *launcher) Close() error { + return l.StateMachine.StopOnce("launcher", func() error { + // shut down the monitor goroutine. + close(l.stopChan) + l.wg.Wait() + + // shut down all running oracles. + var err error + for _, ceDep := range l.dons { + err = multierr.Append(err, ceDep.Close()) + } + + return err + }) +} + +// Start implements job.ServiceCtx. +func (l *launcher) Start(context.Context) error { + return l.StartOnce("launcher", func() error { + l.stopChan = make(chan struct{}) + l.wg.Add(1) + go l.monitor() + return nil + }) +} + +func (l *launcher) monitor() { + defer l.wg.Done() + ticker := time.NewTicker(l.tickInterval) + for { + select { + case <-l.stopChan: + return + case <-ticker.C: + if err := l.tick(); err != nil { + l.lggr.Errorw("Failed to tick", "err", err) + } + } + } +} + +func (l *launcher) tick() error { + // Ensure that the home chain reader is healthy. + // For new jobs it may be possible that the home chain reader is not yet ready + // so we won't be able to fetch configs and start any OCR instances. + if ready := l.homeChainReader.Ready(); ready != nil { + return fmt.Errorf("home chain reader is not ready: %w", ready) + } + + // Fetch the latest state from the capability registry and determine if we need to + // launch or update any OCR instances. + latestState := l.getLatestState() + + diffRes, err := diff(l.capabilityID, l.regState, latestState) + if err != nil { + return fmt.Errorf("failed to diff capability registry states: %w", err) + } + + err = l.processDiff(diffRes) + if err != nil { + return fmt.Errorf("failed to process diff: %w", err) + } + + return nil +} + +// processDiff processes the diff between the current and latest capability registry states. +// for any added OCR instances, it will launch them. +// for any removed OCR instances, it will shut them down. +// for any updated OCR instances, it will restart them with the new configuration. +func (l *launcher) processDiff(diff diffResult) error { + err := l.processRemoved(diff.removed) + err = multierr.Append(err, l.processAdded(diff.added)) + err = multierr.Append(err, l.processUpdate(diff.updated)) + + return err +} + +func (l *launcher) processUpdate(updated map[registrysyncer.DonID]registrysyncer.DON) error { + l.lock.Lock() + defer l.lock.Unlock() + + for donID, don := range updated { + prevDeployment, ok := l.dons[registrysyncer.DonID(don.ID)] + if !ok { + return fmt.Errorf("invariant violation: expected to find CCIP DON %d in the map of running deployments", don.ID) + } + + futDeployment, err := updateDON( + l.lggr, + l.p2pID, + l.homeChainReader, + l.oracleCreator, + *prevDeployment, + don, + ) + if err != nil { + return err + } + if err := futDeployment.HandleBlueGreen(prevDeployment); err != nil { + // TODO: how to handle a failed blue-green deployment? + return fmt.Errorf("failed to handle blue-green deployment for CCIP DON %d: %w", donID, err) + } + + // update state. + l.dons[donID] = futDeployment + // update the state with the latest config. + // this way if one of the starts errors, we don't retry all of them. + l.regState.IDsToDONs[donID] = updated[donID] + } + + return nil +} + +func (l *launcher) processAdded(added map[registrysyncer.DonID]registrysyncer.DON) error { + l.lock.Lock() + defer l.lock.Unlock() + + for donID, don := range added { + dep, err := createDON( + l.lggr, + l.p2pID, + l.homeChainReader, + l.oracleCreator, + don, + ) + if err != nil { + return err + } + if dep == nil { + // not a member of this DON. + continue + } + + if err := dep.StartBlue(); err != nil { + if shutdownErr := dep.CloseBlue(); shutdownErr != nil { + l.lggr.Errorw("Failed to shutdown blue instance after failed start", "donId", donID, "err", shutdownErr) + } + return fmt.Errorf("failed to start oracles for CCIP DON %d: %w", donID, err) + } + + // update state. + l.dons[donID] = dep + // update the state with the latest config. + // this way if one of the starts errors, we don't retry all of them. + l.regState.IDsToDONs[donID] = added[donID] + } + + return nil +} + +func (l *launcher) processRemoved(removed map[registrysyncer.DonID]registrysyncer.DON) error { + l.lock.Lock() + defer l.lock.Unlock() + + for id := range removed { + ceDep, ok := l.dons[id] + if !ok { + // not running this particular DON. + continue + } + + if err := ceDep.Close(); err != nil { + return fmt.Errorf("failed to shutdown oracles for CCIP DON %d: %w", id, err) + } + + // after a successful shutdown we can safely remove the DON deployment from the map. + delete(l.dons, id) + delete(l.regState.IDsToDONs, id) + } + + return nil +} + +// updateDON is a pure function that handles the case where a DON in the capability registry +// has received a new configuration. +// It returns a new ccipDeployment that can then be used to perform the blue-green deployment, +// based on the previous deployment. +func updateDON( + lggr logger.Logger, + p2pID ragep2ptypes.PeerID, + homeChainReader ccipreader.HomeChain, + oracleCreator cctypes.OracleCreator, + prevDeployment ccipDeployment, + don registrysyncer.DON, +) (futDeployment *ccipDeployment, err error) { + if !isMemberOfDON(don, p2pID) { + lggr.Infow("Not a member of this DON, skipping", "donId", don.ID, "p2pId", p2pID.String()) + return nil, nil + } + + // this should be a retryable error. + commitOCRConfigs, err := homeChainReader.GetOCRConfigs(context.Background(), don.ID, uint8(cctypes.PluginTypeCCIPCommit)) + if err != nil { + return nil, fmt.Errorf("failed to fetch OCR configs for CCIP commit plugin (don id: %d) from home chain config contract: %w", + don.ID, err) + } + + execOCRConfigs, err := homeChainReader.GetOCRConfigs(context.Background(), don.ID, uint8(cctypes.PluginTypeCCIPExec)) + if err != nil { + return nil, fmt.Errorf("failed to fetch OCR configs for CCIP exec plugin (don id: %d) from home chain config contract: %w", + don.ID, err) + } + + commitBgd, err := createFutureBlueGreenDeployment(prevDeployment, commitOCRConfigs, oracleCreator, cctypes.PluginTypeCCIPCommit) + if err != nil { + return nil, fmt.Errorf("failed to create future blue-green deployment for CCIP commit plugin: %w, don id: %d", err, don.ID) + } + + execBgd, err := createFutureBlueGreenDeployment(prevDeployment, execOCRConfigs, oracleCreator, cctypes.PluginTypeCCIPExec) + if err != nil { + return nil, fmt.Errorf("failed to create future blue-green deployment for CCIP exec plugin: %w, don id: %d", err, don.ID) + } + + return &ccipDeployment{ + commit: commitBgd, + exec: execBgd, + }, nil +} + +// valid cases: +// a) len(ocrConfigs) == 2 && !prevDeployment.HasGreenInstance(pluginType): this is a new green instance. +// b) len(ocrConfigs) == 1 && prevDeployment.HasGreenInstance(): this is a promotion of green->blue. +// All other cases are invalid. This is enforced in the ccip config contract. +func createFutureBlueGreenDeployment( + prevDeployment ccipDeployment, + ocrConfigs []ccipreader.OCR3ConfigWithMeta, + oracleCreator cctypes.OracleCreator, + pluginType cctypes.PluginType, +) (blueGreenDeployment, error) { + var deployment blueGreenDeployment + if isNewGreenInstance(pluginType, ocrConfigs, prevDeployment) { + // this is a new green instance. + greenOracle, err := oracleCreator.CreatePluginOracle(pluginType, cctypes.OCR3ConfigWithMeta(ocrConfigs[1])) + if err != nil { + return blueGreenDeployment{}, fmt.Errorf("failed to create CCIP commit oracle: %w", err) + } + + deployment.blue = prevDeployment.commit.blue + deployment.green = greenOracle + } else if isPromotion(pluginType, ocrConfigs, prevDeployment) { + // this is a promotion of green->blue. + deployment.blue = prevDeployment.commit.green + } else { + return blueGreenDeployment{}, fmt.Errorf("invariant violation: expected 1 or 2 OCR configs for CCIP plugin (type: %d), got %d", pluginType, len(ocrConfigs)) + } + + return deployment, nil +} + +// createDON is a pure function that handles the case where a new DON is added to the capability registry. +// It returns a new ccipDeployment that can then be used to start the blue instance. +func createDON( + lggr logger.Logger, + p2pID ragep2ptypes.PeerID, + homeChainReader ccipreader.HomeChain, + oracleCreator cctypes.OracleCreator, + don registrysyncer.DON, +) (*ccipDeployment, error) { + if !isMemberOfDON(don, p2pID) { + lggr.Infow("Not a member of this DON, skipping", "donId", don.ID, "p2pId", p2pID.String()) + return nil, nil + } + + // this should be a retryable error. + commitOCRConfigs, err := homeChainReader.GetOCRConfigs(context.Background(), don.ID, uint8(cctypes.PluginTypeCCIPCommit)) + if err != nil { + return nil, fmt.Errorf("failed to fetch OCR configs for CCIP commit plugin (don id: %d) from home chain config contract: %w", + don.ID, err) + } + + execOCRConfigs, err := homeChainReader.GetOCRConfigs(context.Background(), don.ID, uint8(cctypes.PluginTypeCCIPExec)) + if err != nil { + return nil, fmt.Errorf("failed to fetch OCR configs for CCIP exec plugin (don id: %d) from home chain config contract: %w", + don.ID, err) + } + + // upon creation we should only have one OCR config per plugin type. + if len(commitOCRConfigs) != 1 { + return nil, fmt.Errorf("expected exactly one OCR config for CCIP commit plugin (don id: %d), got %d", don.ID, len(commitOCRConfigs)) + } + + if len(execOCRConfigs) != 1 { + return nil, fmt.Errorf("expected exactly one OCR config for CCIP exec plugin (don id: %d), got %d", don.ID, len(execOCRConfigs)) + } + + commitOracle, commitBootstrap, err := createOracle(p2pID, oracleCreator, cctypes.PluginTypeCCIPCommit, commitOCRConfigs) + if err != nil { + return nil, fmt.Errorf("failed to create CCIP commit oracle: %w", err) + } + + execOracle, execBootstrap, err := createOracle(p2pID, oracleCreator, cctypes.PluginTypeCCIPExec, execOCRConfigs) + if err != nil { + return nil, fmt.Errorf("failed to create CCIP exec oracle: %w", err) + } + + return &ccipDeployment{ + commit: blueGreenDeployment{ + blue: commitOracle, + bootstrapBlue: commitBootstrap, + }, + exec: blueGreenDeployment{ + blue: execOracle, + bootstrapBlue: execBootstrap, + }, + }, nil +} + +func createOracle( + p2pID ragep2ptypes.PeerID, + oracleCreator cctypes.OracleCreator, + pluginType cctypes.PluginType, + ocrConfigs []ccipreader.OCR3ConfigWithMeta, +) (pluginOracle, bootstrapOracle cctypes.CCIPOracle, err error) { + pluginOracle, err = oracleCreator.CreatePluginOracle(pluginType, cctypes.OCR3ConfigWithMeta(ocrConfigs[0])) + if err != nil { + return nil, nil, fmt.Errorf("failed to create CCIP plugin oracle (plugintype: %d): %w", pluginType, err) + } + + if isMemberOfBootstrapSubcommittee(ocrConfigs[0].Config.BootstrapP2PIds, p2pID) { + bootstrapOracle, err = oracleCreator.CreateBootstrapOracle(cctypes.OCR3ConfigWithMeta(ocrConfigs[0])) + if err != nil { + return nil, nil, fmt.Errorf("failed to create CCIP bootstrap oracle (plugintype: %d): %w", pluginType, err) + } + } + + return pluginOracle, bootstrapOracle, nil +} diff --git a/core/capabilities/ccip/launcher/launcher_test.go b/core/capabilities/ccip/launcher/launcher_test.go new file mode 100644 index 00000000000..242dd0be248 --- /dev/null +++ b/core/capabilities/ccip/launcher/launcher_test.go @@ -0,0 +1,472 @@ +package launcher + +import ( + "errors" + "math/big" + "reflect" + "testing" + + cctypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types/mocks" + + ragep2ptypes "github.com/smartcontractkit/libocr/ragep2p/types" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + ccipreaderpkg "github.com/smartcontractkit/chainlink-ccip/pkg/reader" + + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" + "github.com/smartcontractkit/chainlink/v2/core/services/registrysyncer" +) + +func Test_createOracle(t *testing.T) { + var p2pKeys []ragep2ptypes.PeerID + for i := 0; i < 3; i++ { + p2pKeys = append(p2pKeys, ragep2ptypes.PeerID(p2pkey.MustNewV2XXXTestingOnly(big.NewInt(int64(i+1))).PeerID())) + } + myP2PKey := p2pKeys[0] + type args struct { + p2pID ragep2ptypes.PeerID + oracleCreator *mocks.OracleCreator + pluginType cctypes.PluginType + ocrConfigs []ccipreaderpkg.OCR3ConfigWithMeta + } + tests := []struct { + name string + args args + expect func(t *testing.T, args args, oracleCreator *mocks.OracleCreator) + wantErr bool + }{ + { + "success, no bootstrap", + args{ + myP2PKey, + mocks.NewOracleCreator(t), + cctypes.PluginTypeCCIPCommit, + []ccipreaderpkg.OCR3ConfigWithMeta{ + { + Config: ccipreaderpkg.OCR3Config{}, + ConfigCount: 1, + ConfigDigest: testutils.Random32Byte(), + }, + }, + }, + func(t *testing.T, args args, oracleCreator *mocks.OracleCreator) { + oracleCreator. + On("CreatePluginOracle", cctypes.PluginTypeCCIPCommit, cctypes.OCR3ConfigWithMeta(args.ocrConfigs[0])). + Return(mocks.NewCCIPOracle(t), nil) + }, + false, + }, + { + "success, with bootstrap", + args{ + myP2PKey, + mocks.NewOracleCreator(t), + cctypes.PluginTypeCCIPCommit, + []ccipreaderpkg.OCR3ConfigWithMeta{ + { + Config: ccipreaderpkg.OCR3Config{ + BootstrapP2PIds: [][32]byte{myP2PKey}, + }, + ConfigCount: 1, + ConfigDigest: testutils.Random32Byte(), + }, + }, + }, + func(t *testing.T, args args, oracleCreator *mocks.OracleCreator) { + oracleCreator. + On("CreatePluginOracle", cctypes.PluginTypeCCIPCommit, cctypes.OCR3ConfigWithMeta(args.ocrConfigs[0])). + Return(mocks.NewCCIPOracle(t), nil) + oracleCreator. + On("CreateBootstrapOracle", cctypes.OCR3ConfigWithMeta(args.ocrConfigs[0])). + Return(mocks.NewCCIPOracle(t), nil) + }, + false, + }, + { + "error creating plugin oracle", + args{ + myP2PKey, + mocks.NewOracleCreator(t), + cctypes.PluginTypeCCIPCommit, + []ccipreaderpkg.OCR3ConfigWithMeta{ + { + Config: ccipreaderpkg.OCR3Config{}, + ConfigCount: 1, + ConfigDigest: testutils.Random32Byte(), + }, + }, + }, + func(t *testing.T, args args, oracleCreator *mocks.OracleCreator) { + oracleCreator. + On("CreatePluginOracle", cctypes.PluginTypeCCIPCommit, cctypes.OCR3ConfigWithMeta(args.ocrConfigs[0])). + Return(nil, errors.New("error creating oracle")) + }, + true, + }, + { + "error creating bootstrap oracle", + args{ + myP2PKey, + mocks.NewOracleCreator(t), + cctypes.PluginTypeCCIPCommit, + []ccipreaderpkg.OCR3ConfigWithMeta{ + { + Config: ccipreaderpkg.OCR3Config{ + BootstrapP2PIds: [][32]byte{myP2PKey}, + }, + ConfigCount: 1, + ConfigDigest: testutils.Random32Byte(), + }, + }, + }, + func(t *testing.T, args args, oracleCreator *mocks.OracleCreator) { + oracleCreator. + On("CreatePluginOracle", cctypes.PluginTypeCCIPCommit, cctypes.OCR3ConfigWithMeta(args.ocrConfigs[0])). + Return(mocks.NewCCIPOracle(t), nil) + oracleCreator. + On("CreateBootstrapOracle", cctypes.OCR3ConfigWithMeta(args.ocrConfigs[0])). + Return(nil, errors.New("error creating oracle")) + }, + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.expect(t, tt.args, tt.args.oracleCreator) + _, _, err := createOracle(tt.args.p2pID, tt.args.oracleCreator, tt.args.pluginType, tt.args.ocrConfigs) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func Test_createDON(t *testing.T) { + type args struct { + lggr logger.Logger + p2pID ragep2ptypes.PeerID + homeChainReader *mocks.HomeChainReader + oracleCreator *mocks.OracleCreator + don registrysyncer.DON + } + tests := []struct { + name string + args args + expect func(t *testing.T, args args, oracleCreator *mocks.OracleCreator, homeChainReader *mocks.HomeChainReader) + wantErr bool + }{ + { + "not a member of the DON", + args{ + logger.TestLogger(t), + p2pID1, + mocks.NewHomeChainReader(t), + mocks.NewOracleCreator(t), + registrysyncer.DON{ + DON: getDON(2, []ragep2ptypes.PeerID{p2pID2}, 0), + CapabilityConfigurations: defaultCapCfgs, + }, + }, + func(t *testing.T, args args, oracleCreator *mocks.OracleCreator, homeChainReader *mocks.HomeChainReader) { + }, + false, + }, + { + "success, no bootstrap", + args{ + logger.TestLogger(t), + p2pID1, + mocks.NewHomeChainReader(t), + mocks.NewOracleCreator(t), + defaultRegistryDon, + }, + func(t *testing.T, args args, oracleCreator *mocks.OracleCreator, homeChainReader *mocks.HomeChainReader) { + homeChainReader. + On("GetOCRConfigs", mock.Anything, uint32(1), uint8(cctypes.PluginTypeCCIPCommit)). + Return([]ccipreaderpkg.OCR3ConfigWithMeta{{}}, nil) + homeChainReader. + On("GetOCRConfigs", mock.Anything, uint32(1), uint8(cctypes.PluginTypeCCIPExec)). + Return([]ccipreaderpkg.OCR3ConfigWithMeta{{}}, nil) + oracleCreator. + On("CreatePluginOracle", cctypes.PluginTypeCCIPCommit, mock.Anything). + Return(mocks.NewCCIPOracle(t), nil) + oracleCreator. + On("CreatePluginOracle", cctypes.PluginTypeCCIPExec, mock.Anything). + Return(mocks.NewCCIPOracle(t), nil) + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.expect != nil { + tt.expect(t, tt.args, tt.args.oracleCreator, tt.args.homeChainReader) + } + _, err := createDON(tt.args.lggr, tt.args.p2pID, tt.args.homeChainReader, tt.args.oracleCreator, tt.args.don) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func Test_createFutureBlueGreenDeployment(t *testing.T) { + type args struct { + prevDeployment ccipDeployment + ocrConfigs []ccipreaderpkg.OCR3ConfigWithMeta + oracleCreator *mocks.OracleCreator + pluginType cctypes.PluginType + } + tests := []struct { + name string + args args + want blueGreenDeployment + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := createFutureBlueGreenDeployment(tt.args.prevDeployment, tt.args.ocrConfigs, tt.args.oracleCreator, tt.args.pluginType) + if (err != nil) != tt.wantErr { + t.Errorf("createFutureBlueGreenDeployment() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("createFutureBlueGreenDeployment() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_updateDON(t *testing.T) { + type args struct { + lggr logger.Logger + p2pID ragep2ptypes.PeerID + homeChainReader *mocks.HomeChainReader + oracleCreator *mocks.OracleCreator + prevDeployment ccipDeployment + don registrysyncer.DON + } + tests := []struct { + name string + args args + wantFutDeployment *ccipDeployment + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotFutDeployment, err := updateDON(tt.args.lggr, tt.args.p2pID, tt.args.homeChainReader, tt.args.oracleCreator, tt.args.prevDeployment, tt.args.don) + if (err != nil) != tt.wantErr { + t.Errorf("updateDON() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(gotFutDeployment, tt.wantFutDeployment) { + t.Errorf("updateDON() = %v, want %v", gotFutDeployment, tt.wantFutDeployment) + } + }) + } +} + +func Test_launcher_processDiff(t *testing.T) { + type fields struct { + lggr logger.Logger + p2pID ragep2ptypes.PeerID + homeChainReader *mocks.HomeChainReader + oracleCreator *mocks.OracleCreator + dons map[registrysyncer.DonID]*ccipDeployment + regState registrysyncer.LocalRegistry + } + type args struct { + diff diffResult + } + tests := []struct { + name string + fields fields + args args + assert func(t *testing.T, l *launcher) + wantErr bool + }{ + { + "don removed success", + fields{ + dons: map[registrysyncer.DonID]*ccipDeployment{ + 1: { + commit: blueGreenDeployment{ + blue: newMock(t, + func(t *testing.T) *mocks.CCIPOracle { return mocks.NewCCIPOracle(t) }, + func(m *mocks.CCIPOracle) { + m.On("Close").Return(nil) + }), + }, + exec: blueGreenDeployment{ + blue: newMock(t, + func(t *testing.T) *mocks.CCIPOracle { return mocks.NewCCIPOracle(t) }, + func(m *mocks.CCIPOracle) { + m.On("Close").Return(nil) + }), + }, + }, + }, + regState: registrysyncer.LocalRegistry{ + IDsToDONs: map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + }, + }, + args{ + diff: diffResult{ + removed: map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + }, + }, + func(t *testing.T, l *launcher) { + require.Len(t, l.dons, 0) + require.Len(t, l.regState.IDsToDONs, 0) + }, + false, + }, + { + "don added success", + fields{ + lggr: logger.TestLogger(t), + p2pID: p2pID1, + homeChainReader: newMock(t, func(t *testing.T) *mocks.HomeChainReader { + return mocks.NewHomeChainReader(t) + }, func(m *mocks.HomeChainReader) { + m.On("GetOCRConfigs", mock.Anything, uint32(1), uint8(cctypes.PluginTypeCCIPCommit)). + Return([]ccipreaderpkg.OCR3ConfigWithMeta{{}}, nil) + m.On("GetOCRConfigs", mock.Anything, uint32(1), uint8(cctypes.PluginTypeCCIPExec)). + Return([]ccipreaderpkg.OCR3ConfigWithMeta{{}}, nil) + }), + oracleCreator: newMock(t, func(t *testing.T) *mocks.OracleCreator { + return mocks.NewOracleCreator(t) + }, func(m *mocks.OracleCreator) { + commitOracle := mocks.NewCCIPOracle(t) + commitOracle.On("Start").Return(nil) + execOracle := mocks.NewCCIPOracle(t) + execOracle.On("Start").Return(nil) + m.On("CreatePluginOracle", cctypes.PluginTypeCCIPCommit, mock.Anything). + Return(commitOracle, nil) + m.On("CreatePluginOracle", cctypes.PluginTypeCCIPExec, mock.Anything). + Return(execOracle, nil) + }), + dons: map[registrysyncer.DonID]*ccipDeployment{}, + regState: registrysyncer.LocalRegistry{ + IDsToDONs: map[registrysyncer.DonID]registrysyncer.DON{}, + }, + }, + args{ + diff: diffResult{ + added: map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + }, + }, + func(t *testing.T, l *launcher) { + require.Len(t, l.dons, 1) + require.Len(t, l.regState.IDsToDONs, 1) + }, + false, + }, + { + "don updated new green instance success", + fields{ + lggr: logger.TestLogger(t), + p2pID: p2pID1, + homeChainReader: newMock(t, func(t *testing.T) *mocks.HomeChainReader { + return mocks.NewHomeChainReader(t) + }, func(m *mocks.HomeChainReader) { + m.On("GetOCRConfigs", mock.Anything, uint32(1), uint8(cctypes.PluginTypeCCIPCommit)). + Return([]ccipreaderpkg.OCR3ConfigWithMeta{{}, {}}, nil) + m.On("GetOCRConfigs", mock.Anything, uint32(1), uint8(cctypes.PluginTypeCCIPExec)). + Return([]ccipreaderpkg.OCR3ConfigWithMeta{{}, {}}, nil) + }), + oracleCreator: newMock(t, func(t *testing.T) *mocks.OracleCreator { + return mocks.NewOracleCreator(t) + }, func(m *mocks.OracleCreator) { + commitOracle := mocks.NewCCIPOracle(t) + commitOracle.On("Start").Return(nil) + execOracle := mocks.NewCCIPOracle(t) + execOracle.On("Start").Return(nil) + m.On("CreatePluginOracle", cctypes.PluginTypeCCIPCommit, mock.Anything). + Return(commitOracle, nil) + m.On("CreatePluginOracle", cctypes.PluginTypeCCIPExec, mock.Anything). + Return(execOracle, nil) + }), + dons: map[registrysyncer.DonID]*ccipDeployment{ + 1: { + commit: blueGreenDeployment{ + blue: newMock(t, func(t *testing.T) *mocks.CCIPOracle { + return mocks.NewCCIPOracle(t) + }, func(m *mocks.CCIPOracle) {}), + }, + exec: blueGreenDeployment{ + blue: newMock(t, func(t *testing.T) *mocks.CCIPOracle { + return mocks.NewCCIPOracle(t) + }, func(m *mocks.CCIPOracle) {}), + }, + }, + }, + regState: registrysyncer.LocalRegistry{ + IDsToDONs: map[registrysyncer.DonID]registrysyncer.DON{ + 1: defaultRegistryDon, + }, + }, + }, + args{ + diff: diffResult{ + updated: map[registrysyncer.DonID]registrysyncer.DON{ + 1: { + // new Node in Don: p2pID2 + DON: getDON(1, []ragep2ptypes.PeerID{p2pID1, p2pID2}, 0), + CapabilityConfigurations: defaultCapCfgs, + }, + }, + }, + }, + func(t *testing.T, l *launcher) { + require.Len(t, l.dons, 1) + require.Len(t, l.regState.IDsToDONs, 1) + require.Len(t, l.regState.IDsToDONs[1].Members, 2) + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := &launcher{ + dons: tt.fields.dons, + regState: tt.fields.regState, + p2pID: tt.fields.p2pID, + lggr: tt.fields.lggr, + homeChainReader: tt.fields.homeChainReader, + oracleCreator: tt.fields.oracleCreator, + } + err := l.processDiff(tt.args.diff) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + tt.assert(t, l) + }) + } +} + +func newMock[T any](t *testing.T, newer func(t *testing.T) T, expect func(m T)) T { + o := newer(t) + expect(o) + return o +} diff --git a/core/capabilities/ccip/launcher/test_helpers.go b/core/capabilities/ccip/launcher/test_helpers.go new file mode 100644 index 00000000000..a2ebf3fdba9 --- /dev/null +++ b/core/capabilities/ccip/launcher/test_helpers.go @@ -0,0 +1,56 @@ +package launcher + +import ( + "fmt" + "math/big" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" + "github.com/smartcontractkit/chainlink/v2/core/services/registrysyncer" + + ragep2ptypes "github.com/smartcontractkit/libocr/ragep2p/types" +) + +const ( + ccipCapVersion = "v1.0.0" + ccipCapNewVersion = "v1.1.0" + ccipCapName = "ccip" +) + +var ( + defaultCapability = getCapability(ccipCapName, ccipCapVersion) + newCapability = getCapability(ccipCapName, ccipCapNewVersion) + p2pID1 = getP2PID(1) + p2pID2 = getP2PID(2) + defaultCapCfgs = map[string]registrysyncer.CapabilityConfiguration{ + defaultCapability.ID: registrysyncer.CapabilityConfiguration{}, + } + defaultRegistryDon = registrysyncer.DON{ + DON: getDON(1, []ragep2ptypes.PeerID{p2pID1}, 0), + CapabilityConfigurations: defaultCapCfgs, + } +) + +func getP2PID(id uint32) ragep2ptypes.PeerID { + return ragep2ptypes.PeerID(p2pkey.MustNewV2XXXTestingOnly(big.NewInt(int64(id))).PeerID()) +} + +func getCapability(ccipCapName, ccipCapVersion string) registrysyncer.Capability { + id := fmt.Sprintf("%s@%s", ccipCapName, ccipCapVersion) + return registrysyncer.Capability{ + CapabilityType: capabilities.CapabilityTypeTarget, + ID: id, + } +} + +func getDON(id uint32, members []ragep2ptypes.PeerID, cfgVersion uint32) capabilities.DON { + return capabilities.DON{ + ID: id, + ConfigVersion: cfgVersion, + F: uint8(1), + IsPublic: true, + AcceptsWorkflows: true, + Members: members, + } +} diff --git a/core/capabilities/ccip/ocrimpls/config_digester.go b/core/capabilities/ccip/ocrimpls/config_digester.go new file mode 100644 index 00000000000..ef0c5e7ca32 --- /dev/null +++ b/core/capabilities/ccip/ocrimpls/config_digester.go @@ -0,0 +1,23 @@ +package ocrimpls + +import "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + +type configDigester struct { + d types.ConfigDigest +} + +func NewConfigDigester(d types.ConfigDigest) *configDigester { + return &configDigester{d: d} +} + +// ConfigDigest implements types.OffchainConfigDigester. +func (c *configDigester) ConfigDigest(types.ContractConfig) (types.ConfigDigest, error) { + return c.d, nil +} + +// ConfigDigestPrefix implements types.OffchainConfigDigester. +func (c *configDigester) ConfigDigestPrefix() (types.ConfigDigestPrefix, error) { + return types.ConfigDigestPrefixCCIPMultiRole, nil +} + +var _ types.OffchainConfigDigester = (*configDigester)(nil) diff --git a/core/capabilities/ccip/ocrimpls/config_tracker.go b/core/capabilities/ccip/ocrimpls/config_tracker.go new file mode 100644 index 00000000000..3a6a27fa40c --- /dev/null +++ b/core/capabilities/ccip/ocrimpls/config_tracker.go @@ -0,0 +1,77 @@ +package ocrimpls + +import ( + "context" + + cctypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" + + gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3confighelper" + "github.com/smartcontractkit/libocr/offchainreporting2plus/types" +) + +type configTracker struct { + cfg cctypes.OCR3ConfigWithMeta +} + +func NewConfigTracker(cfg cctypes.OCR3ConfigWithMeta) *configTracker { + return &configTracker{cfg: cfg} +} + +// LatestBlockHeight implements types.ContractConfigTracker. +func (c *configTracker) LatestBlockHeight(ctx context.Context) (blockHeight uint64, err error) { + return 0, nil +} + +// LatestConfig implements types.ContractConfigTracker. +func (c *configTracker) LatestConfig(ctx context.Context, changedInBlock uint64) (types.ContractConfig, error) { + return c.contractConfig(), nil +} + +// LatestConfigDetails implements types.ContractConfigTracker. +func (c *configTracker) LatestConfigDetails(ctx context.Context) (changedInBlock uint64, configDigest types.ConfigDigest, err error) { + return 0, c.cfg.ConfigDigest, nil +} + +// Notify implements types.ContractConfigTracker. +func (c *configTracker) Notify() <-chan struct{} { + return nil +} + +func (c *configTracker) contractConfig() types.ContractConfig { + return types.ContractConfig{ + ConfigDigest: c.cfg.ConfigDigest, + ConfigCount: c.cfg.ConfigCount, + Signers: toOnchainPublicKeys(c.cfg.Config.Signers), + Transmitters: toOCRAccounts(c.cfg.Config.Transmitters), + F: c.cfg.Config.F, + OnchainConfig: []byte{}, + OffchainConfigVersion: c.cfg.Config.OffchainConfigVersion, + OffchainConfig: c.cfg.Config.OffchainConfig, + } +} + +// PublicConfig returns the OCR configuration as a PublicConfig so that we can +// access ReportingPluginConfig and other fields prior to launching the plugins. +func (c *configTracker) PublicConfig() (ocr3confighelper.PublicConfig, error) { + return ocr3confighelper.PublicConfigFromContractConfig(false, c.contractConfig()) +} + +func toOnchainPublicKeys(signers [][]byte) []types.OnchainPublicKey { + keys := make([]types.OnchainPublicKey, len(signers)) + for i, signer := range signers { + keys[i] = types.OnchainPublicKey(signer) + } + return keys +} + +func toOCRAccounts(transmitters [][]byte) []types.Account { + accounts := make([]types.Account, len(transmitters)) + for i, transmitter := range transmitters { + // TODO: string-encode the transmitter appropriately to the dest chain family. + accounts[i] = types.Account(gethcommon.BytesToAddress(transmitter).Hex()) + } + return accounts +} + +var _ types.ContractConfigTracker = (*configTracker)(nil) diff --git a/core/capabilities/ccip/ocrimpls/contract_transmitter.go b/core/capabilities/ccip/ocrimpls/contract_transmitter.go new file mode 100644 index 00000000000..fd8e206d0e3 --- /dev/null +++ b/core/capabilities/ccip/ocrimpls/contract_transmitter.go @@ -0,0 +1,188 @@ +package ocrimpls + +import ( + "context" + "errors" + "fmt" + "math/big" + + "github.com/google/uuid" + "github.com/smartcontractkit/libocr/offchainreporting2/chains/evmutil" + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + + "github.com/smartcontractkit/chainlink-ccip/pkg/consts" + commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" +) + +type ToCalldataFunc func(rawReportCtx [3][32]byte, report []byte, rs, ss [][32]byte, vs [32]byte) any + +func ToCommitCalldata(rawReportCtx [3][32]byte, report []byte, rs, ss [][32]byte, vs [32]byte) any { + // Note that the name of the struct field is very important, since the encoder used + // by the chainwriter uses mapstructure, which will use the struct field name to map + // to the argument name in the function call. + // If, for whatever reason, we want to change the field name, make sure to add a `mapstructure:""` tag + // for that field. + return struct { + ReportContext [3][32]byte + Report []byte + Rs [][32]byte + Ss [][32]byte + RawVs [32]byte + }{ + ReportContext: rawReportCtx, + Report: report, + Rs: rs, + Ss: ss, + RawVs: vs, + } +} + +func ToExecCalldata(rawReportCtx [3][32]byte, report []byte, _, _ [][32]byte, _ [32]byte) any { + // Note that the name of the struct field is very important, since the encoder used + // by the chainwriter uses mapstructure, which will use the struct field name to map + // to the argument name in the function call. + // If, for whatever reason, we want to change the field name, make sure to add a `mapstructure:""` tag + // for that field. + return struct { + ReportContext [3][32]byte + Report []byte + }{ + ReportContext: rawReportCtx, + Report: report, + } +} + +var _ ocr3types.ContractTransmitter[[]byte] = &commitTransmitter[[]byte]{} + +type commitTransmitter[RI any] struct { + cw commontypes.ChainWriter + fromAccount ocrtypes.Account + contractName string + method string + offrampAddress string + toCalldataFn ToCalldataFunc +} + +func XXXNewContractTransmitterTestsOnly[RI any]( + cw commontypes.ChainWriter, + fromAccount ocrtypes.Account, + contractName string, + method string, + offrampAddress string, + toCalldataFn ToCalldataFunc, +) ocr3types.ContractTransmitter[RI] { + return &commitTransmitter[RI]{ + cw: cw, + fromAccount: fromAccount, + contractName: contractName, + method: method, + offrampAddress: offrampAddress, + toCalldataFn: toCalldataFn, + } +} + +func NewCommitContractTransmitter[RI any]( + cw commontypes.ChainWriter, + fromAccount ocrtypes.Account, + offrampAddress string, +) ocr3types.ContractTransmitter[RI] { + return &commitTransmitter[RI]{ + cw: cw, + fromAccount: fromAccount, + contractName: consts.ContractNameOffRamp, + method: consts.MethodCommit, + offrampAddress: offrampAddress, + toCalldataFn: ToCommitCalldata, + } +} + +func NewExecContractTransmitter[RI any]( + cw commontypes.ChainWriter, + fromAccount ocrtypes.Account, + offrampAddress string, +) ocr3types.ContractTransmitter[RI] { + return &commitTransmitter[RI]{ + cw: cw, + fromAccount: fromAccount, + contractName: consts.ContractNameOffRamp, + method: consts.MethodExecute, + offrampAddress: offrampAddress, + toCalldataFn: ToExecCalldata, + } +} + +// FromAccount implements ocr3types.ContractTransmitter. +func (c *commitTransmitter[RI]) FromAccount() (ocrtypes.Account, error) { + return c.fromAccount, nil +} + +// Transmit implements ocr3types.ContractTransmitter. +func (c *commitTransmitter[RI]) Transmit( + ctx context.Context, + configDigest ocrtypes.ConfigDigest, + seqNr uint64, + reportWithInfo ocr3types.ReportWithInfo[RI], + sigs []ocrtypes.AttributedOnchainSignature, +) error { + var rs [][32]byte + var ss [][32]byte + var vs [32]byte + if len(sigs) > 32 { + return errors.New("too many signatures, maximum is 32") + } + for i, as := range sigs { + r, s, v, err := evmutil.SplitSignature(as.Signature) + if err != nil { + return fmt.Errorf("failed to split signature: %w", err) + } + rs = append(rs, r) + ss = append(ss, s) + vs[i] = v + } + + // report ctx for OCR3 consists of the following + // reportContext[0]: ConfigDigest + // reportContext[1]: 24 byte padding, 8 byte sequence number + // reportContext[2]: unused + // convert seqNum, which is a uint64, into a uint32 epoch and uint8 round + // while this does truncate the sequence number, it is not a problem because + // it still gives us 2^40 - 1 possible sequence numbers. + // assuming a sequence number is generated every second, this gives us + // 1099511627775 seconds, or approximately 34,865 years, before we run out + // of sequence numbers. + epoch, round := uint64ToUint32AndUint8(seqNr) + rawReportCtx := evmutil.RawReportContext(ocrtypes.ReportContext{ + ReportTimestamp: ocrtypes.ReportTimestamp{ + ConfigDigest: configDigest, + Epoch: epoch, + Round: round, + }, + // ExtraData not used in OCR3 + }) + + if c.toCalldataFn == nil { + return errors.New("toCalldataFn is nil") + } + + // chain writer takes in the raw calldata and packs it on its own. + args := c.toCalldataFn(rawReportCtx, reportWithInfo.Report, rs, ss, vs) + + // TODO: no meta fields yet, what should we add? + // probably whats in the info part of the report? + meta := commontypes.TxMeta{} + txID, err := uuid.NewRandom() // NOTE: CW expects us to generate an ID, rather than return one + if err != nil { + return fmt.Errorf("failed to generate UUID: %w", err) + } + zero := big.NewInt(0) + if err := c.cw.SubmitTransaction(ctx, c.contractName, c.method, args, fmt.Sprintf("%s-%s-%s", c.contractName, c.offrampAddress, txID.String()), c.offrampAddress, &meta, zero); err != nil { + return fmt.Errorf("failed to submit transaction thru chainwriter: %w", err) + } + + return nil +} + +func uint64ToUint32AndUint8(x uint64) (uint32, uint8) { + return uint32(x >> 32), uint8(x) +} diff --git a/core/capabilities/ccip/ocrimpls/contract_transmitter_test.go b/core/capabilities/ccip/ocrimpls/contract_transmitter_test.go new file mode 100644 index 00000000000..871afbb6697 --- /dev/null +++ b/core/capabilities/ccip/ocrimpls/contract_transmitter_test.go @@ -0,0 +1,691 @@ +package ocrimpls_test + +import ( + "crypto/rand" + "math/big" + "net/url" + "testing" + "time" + + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ocrimpls" + cctypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" + + "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/common/hexutil" + "github.com/ethereum/go-ethereum/core" + "github.com/jmoiron/sqlx" + "github.com/onsi/gomega" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/libocr/commontypes" + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + + "github.com/smartcontractkit/chainlink-common/pkg/utils/mailbox" + txmgrcommon "github.com/smartcontractkit/chainlink/v2/common/txmgr" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + evmconfig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/chaintype" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/headtracker" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/keystore" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" + "github.com/smartcontractkit/chainlink/v2/core/config" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/multi_ocr3_helper" + "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" + kschaintype "github.com/smartcontractkit/chainlink/v2/core/services/keystore/chaintype" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ocr2key" + "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" + evmrelaytypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" +) + +func Test_ContractTransmitter_TransmitWithoutSignatures(t *testing.T) { + type testCase struct { + name string + pluginType uint8 + withSigs bool + expectedSigsEnabled bool + report []byte + } + + testCases := []testCase{ + { + "empty report with sigs", + uint8(cctypes.PluginTypeCCIPCommit), + true, + true, + []byte{}, + }, + { + "empty report without sigs", + uint8(cctypes.PluginTypeCCIPExec), + false, + false, + []byte{}, + }, + { + "report with data with sigs", + uint8(cctypes.PluginTypeCCIPCommit), + true, + true, + randomReport(t, 96), + }, + { + "report with data without sigs", + uint8(cctypes.PluginTypeCCIPExec), + false, + false, + randomReport(t, 96), + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc := tc + t.Parallel() + testTransmitter(t, tc.pluginType, tc.withSigs, tc.expectedSigsEnabled, tc.report) + }) + } +} + +func testTransmitter( + t *testing.T, + pluginType uint8, + withSigs bool, + expectedSigsEnabled bool, + report []byte, +) { + uni := newTestUniverse[[]byte](t, nil) + + c, err := uni.wrapper.LatestConfigDetails(nil, pluginType) + require.NoError(t, err, "failed to get latest config details") + configDigest := c.ConfigInfo.ConfigDigest + require.Equal(t, expectedSigsEnabled, c.ConfigInfo.IsSignatureVerificationEnabled, "signature verification enabled setting not correct") + + // set the plugin type on the helper so it fetches the right config info. + // the important aspect is whether signatures should be enabled or not. + _, err = uni.wrapper.SetTransmitOcrPluginType(uni.deployer, pluginType) + require.NoError(t, err, "failed to set plugin type") + uni.backend.Commit() + + // create attributed sigs + // only need f+1 which is 2 in this case + rwi := ocr3types.ReportWithInfo[[]byte]{ + Report: report, + Info: []byte{}, + } + seqNr := uint64(1) + attributedSigs := uni.SignReport(t, configDigest, rwi, seqNr) + + account, err := uni.transmitterWithSigs.FromAccount() + require.NoError(t, err, "failed to get from account") + require.Equal(t, ocrtypes.Account(uni.transmitters[0].Hex()), account, "from account mismatch") + if withSigs { + err = uni.transmitterWithSigs.Transmit(testutils.Context(t), configDigest, seqNr, rwi, attributedSigs) + } else { + err = uni.transmitterWithoutSigs.Transmit(testutils.Context(t), configDigest, seqNr, rwi, attributedSigs) + } + require.NoError(t, err, "failed to transmit") + uni.backend.Commit() + + var txStatus uint64 + gomega.NewWithT(t).Eventually(func() bool { + uni.backend.Commit() + rows, err := uni.db.QueryContext(testutils.Context(t), `SELECT hash FROM evm.tx_attempts LIMIT 1`) + require.NoError(t, err, "failed to query txes") + defer rows.Close() + var txHash []byte + for rows.Next() { + require.NoError(t, rows.Scan(&txHash), "failed to scan") + } + t.Log("txHash:", txHash) + receipt, err := uni.simClient.TransactionReceipt(testutils.Context(t), common.BytesToHash(txHash)) + if err != nil { + t.Log("tx not found yet:", hexutil.Encode(txHash)) + return false + } + t.Log("tx found:", hexutil.Encode(txHash), "status:", receipt.Status) + txStatus = receipt.Status + return true + }, testutils.WaitTimeout(t), 1*time.Second).Should(gomega.BeTrue()) + + // wait for receipt to be written to the db + gomega.NewWithT(t).Eventually(func() bool { + rows, err := uni.db.QueryContext(testutils.Context(t), `SELECT count(*) as cnt FROM evm.receipts LIMIT 1`) + require.NoError(t, err, "failed to query receipts") + defer rows.Close() + var count int + for rows.Next() { + require.NoError(t, rows.Scan(&count), "failed to scan") + } + return count == 1 + }, testutils.WaitTimeout(t), 2*time.Second).Should(gomega.BeTrue()) + + require.Equal(t, uint64(1), txStatus, "tx status should be success") + + // check that the event was emitted + events := uni.TransmittedEvents(t) + require.Len(t, events, 1, "expected 1 event") + require.Equal(t, configDigest, events[0].ConfigDigest, "config digest mismatch") + require.Equal(t, seqNr, events[0].SequenceNumber, "seq num mismatch") +} + +type testUniverse[RI any] struct { + simClient *client.SimulatedBackendClient + backend *backends.SimulatedBackend + deployer *bind.TransactOpts + transmitters []common.Address + signers []common.Address + wrapper *multi_ocr3_helper.MultiOCR3Helper + transmitterWithSigs ocr3types.ContractTransmitter[RI] + transmitterWithoutSigs ocr3types.ContractTransmitter[RI] + keyrings []ocr3types.OnchainKeyring[RI] + f uint8 + db *sqlx.DB + txm txmgr.TxManager + gasEstimator gas.EvmFeeEstimator +} + +type keyringsAndSigners[RI any] struct { + keyrings []ocr3types.OnchainKeyring[RI] + signers []common.Address +} + +func newTestUniverse[RI any](t *testing.T, ks *keyringsAndSigners[RI]) *testUniverse[RI] { + t.Helper() + + db := pgtest.NewSqlxDB(t) + owner := testutils.MustNewSimTransactor(t) + + // create many transmitters but only need to fund one, rest are to get + // setOCR3Config to pass. + keyStore := cltest.NewKeyStore(t, db) + var transmitters []common.Address + for i := 0; i < 4; i++ { + key, err := keyStore.Eth().Create(testutils.Context(t), big.NewInt(1337)) + require.NoError(t, err, "failed to create key") + transmitters = append(transmitters, key.Address) + } + + backend := backends.NewSimulatedBackend(core.GenesisAlloc{ + owner.From: core.GenesisAccount{ + Balance: assets.Ether(1000).ToInt(), + }, + transmitters[0]: core.GenesisAccount{ + Balance: assets.Ether(1000).ToInt(), + }, + }, 30e6) + + ocr3HelperAddr, _, _, err := multi_ocr3_helper.DeployMultiOCR3Helper(owner, backend) + require.NoError(t, err) + backend.Commit() + wrapper, err := multi_ocr3_helper.NewMultiOCR3Helper(ocr3HelperAddr, backend) + require.NoError(t, err) + + // create the oracle identities for setConfig + // need to create at least 4 identities otherwise setConfig will fail + var ( + keyrings []ocr3types.OnchainKeyring[RI] + signers []common.Address + ) + if ks != nil { + keyrings = ks.keyrings + signers = ks.signers + } else { + for i := 0; i < 4; i++ { + kb, err2 := ocr2key.New(kschaintype.EVM) + require.NoError(t, err2, "failed to create key") + kr := ocrimpls.NewOnchainKeyring[RI](kb, logger.TestLogger(t)) + signers = append(signers, common.BytesToAddress(kr.PublicKey())) + keyrings = append(keyrings, kr) + } + } + f := uint8(1) + commitConfigDigest := testutils.Random32Byte() + execConfigDigest := testutils.Random32Byte() + _, err = wrapper.SetOCR3Configs( + owner, + []multi_ocr3_helper.MultiOCR3BaseOCRConfigArgs{ + { + ConfigDigest: commitConfigDigest, + OcrPluginType: uint8(cctypes.PluginTypeCCIPCommit), + F: f, + IsSignatureVerificationEnabled: true, + Signers: signers, + Transmitters: []common.Address{ + transmitters[0], + transmitters[1], + transmitters[2], + transmitters[3], + }, + }, + { + ConfigDigest: execConfigDigest, + OcrPluginType: uint8(cctypes.PluginTypeCCIPExec), + F: f, + IsSignatureVerificationEnabled: false, + Signers: signers, + Transmitters: []common.Address{ + transmitters[0], + transmitters[1], + transmitters[2], + transmitters[3], + }, + }, + }, + ) + require.NoError(t, err) + backend.Commit() + + commitConfig, err := wrapper.LatestConfigDetails(nil, uint8(cctypes.PluginTypeCCIPCommit)) + require.NoError(t, err, "failed to get latest commit config") + require.Equal(t, commitConfigDigest, commitConfig.ConfigInfo.ConfigDigest, "commit config digest mismatch") + execConfig, err := wrapper.LatestConfigDetails(nil, uint8(cctypes.PluginTypeCCIPExec)) + require.NoError(t, err, "failed to get latest exec config") + require.Equal(t, execConfigDigest, execConfig.ConfigInfo.ConfigDigest, "exec config digest mismatch") + + simClient := client.NewSimulatedBackendClient(t, backend, testutils.SimulatedChainID) + + // create the chain writer service + txm, gasEstimator := makeTestEvmTxm(t, db, simClient, keyStore.Eth()) + require.NoError(t, txm.Start(testutils.Context(t)), "failed to start tx manager") + t.Cleanup(func() { require.NoError(t, txm.Close()) }) + + chainWriter, err := evm.NewChainWriterService( + logger.TestLogger(t), + simClient, + txm, + gasEstimator, + chainWriterConfigRaw(transmitters[0], assets.GWei(1))) + require.NoError(t, err, "failed to create chain writer") + require.NoError(t, chainWriter.Start(testutils.Context(t)), "failed to start chain writer") + t.Cleanup(func() { require.NoError(t, chainWriter.Close()) }) + + transmitterWithSigs := ocrimpls.XXXNewContractTransmitterTestsOnly[RI]( + chainWriter, + ocrtypes.Account(transmitters[0].Hex()), + contractName, + methodTransmitWithSignatures, + ocr3HelperAddr.Hex(), + ocrimpls.ToCommitCalldata, + ) + transmitterWithoutSigs := ocrimpls.XXXNewContractTransmitterTestsOnly[RI]( + chainWriter, + ocrtypes.Account(transmitters[0].Hex()), + contractName, + methodTransmitWithoutSignatures, + ocr3HelperAddr.Hex(), + ocrimpls.ToExecCalldata, + ) + + return &testUniverse[RI]{ + simClient: simClient, + backend: backend, + deployer: owner, + transmitters: transmitters, + signers: signers, + wrapper: wrapper, + transmitterWithSigs: transmitterWithSigs, + transmitterWithoutSigs: transmitterWithoutSigs, + keyrings: keyrings, + f: f, + db: db, + txm: txm, + gasEstimator: gasEstimator, + } +} + +func (uni testUniverse[RI]) SignReport(t *testing.T, configDigest ocrtypes.ConfigDigest, rwi ocr3types.ReportWithInfo[RI], seqNum uint64) []ocrtypes.AttributedOnchainSignature { + var attributedSigs []ocrtypes.AttributedOnchainSignature + for i := uint8(0); i < uni.f+1; i++ { + t.Log("signing report with", hexutil.Encode(uni.keyrings[i].PublicKey())) + sig, err := uni.keyrings[i].Sign(configDigest, seqNum, rwi) + require.NoError(t, err, "failed to sign report") + attributedSigs = append(attributedSigs, ocrtypes.AttributedOnchainSignature{ + Signature: sig, + Signer: commontypes.OracleID(i), + }) + } + return attributedSigs +} + +func (uni testUniverse[RI]) TransmittedEvents(t *testing.T) []*multi_ocr3_helper.MultiOCR3HelperTransmitted { + iter, err := uni.wrapper.FilterTransmitted(&bind.FilterOpts{ + Start: 0, + }, nil) + require.NoError(t, err, "failed to create filter iterator") + var events []*multi_ocr3_helper.MultiOCR3HelperTransmitted + for iter.Next() { + event := iter.Event + events = append(events, event) + } + return events +} + +func randomReport(t *testing.T, len int) []byte { + report := make([]byte, len) + _, err := rand.Reader.Read(report) + require.NoError(t, err, "failed to read random bytes") + return report +} + +const ( + contractName = "MultiOCR3Helper" + methodTransmitWithSignatures = "TransmitWithSignatures" + methodTransmitWithoutSignatures = "TransmitWithoutSignatures" +) + +func chainWriterConfigRaw(fromAddress common.Address, maxGasPrice *assets.Wei) evmrelaytypes.ChainWriterConfig { + return evmrelaytypes.ChainWriterConfig{ + Contracts: map[string]*evmrelaytypes.ContractConfig{ + contractName: { + ContractABI: multi_ocr3_helper.MultiOCR3HelperABI, + Configs: map[string]*evmrelaytypes.ChainWriterDefinition{ + methodTransmitWithSignatures: { + ChainSpecificName: "transmitWithSignatures", + GasLimit: 1e6, + FromAddress: fromAddress, + }, + methodTransmitWithoutSignatures: { + ChainSpecificName: "transmitWithoutSignatures", + GasLimit: 1e6, + FromAddress: fromAddress, + }, + }, + }, + }, + SendStrategy: txmgrcommon.NewSendEveryStrategy(), + MaxGasPrice: maxGasPrice, + } +} + +func makeTestEvmTxm( + t *testing.T, + db *sqlx.DB, + ethClient client.Client, + keyStore keystore.Eth) (txmgr.TxManager, gas.EvmFeeEstimator) { + config, dbConfig, evmConfig := MakeTestConfigs(t) + + estimator, err := gas.NewEstimator(logger.TestLogger(t), ethClient, config, evmConfig.GasEstimator()) + require.NoError(t, err, "failed to create gas estimator") + + lggr := logger.TestLogger(t) + lpOpts := logpoller.Opts{ + PollPeriod: 100 * time.Millisecond, + FinalityDepth: 2, + BackfillBatchSize: 3, + RpcBatchSize: 2, + KeepFinalizedBlocksDepth: 1000, + } + + chainID := big.NewInt(1337) + headSaver := headtracker.NewHeadSaver( + logger.NullLogger, + headtracker.NewORM(*chainID, db), + evmConfig, + evmConfig.HeadTrackerConfig, + ) + + broadcaster := headtracker.NewHeadBroadcaster(logger.NullLogger) + require.NoError(t, broadcaster.Start(testutils.Context(t)), "failed to start head broadcaster") + t.Cleanup(func() { require.NoError(t, broadcaster.Close()) }) + + ht := headtracker.NewHeadTracker( + logger.NullLogger, + ethClient, + evmConfig, + evmConfig.HeadTrackerConfig, + broadcaster, + headSaver, + mailbox.NewMonitor("contract_transmitter_test", logger.NullLogger), + ) + require.NoError(t, ht.Start(testutils.Context(t)), "failed to start head tracker") + t.Cleanup(func() { require.NoError(t, ht.Close()) }) + + lp := logpoller.NewLogPoller(logpoller.NewORM(testutils.FixtureChainID, db, logger.NullLogger), + ethClient, logger.NullLogger, ht, lpOpts) + require.NoError(t, lp.Start(testutils.Context(t)), "failed to start log poller") + t.Cleanup(func() { require.NoError(t, lp.Close()) }) + + // logic for building components (from evm/evm_txm.go) ------- + lggr.Infow("Initializing EVM transaction manager", + "bumpTxDepth", evmConfig.GasEstimator().BumpTxDepth(), + "maxInFlightTransactions", config.EvmConfig.Transactions().MaxInFlight(), + "maxQueuedTransactions", config.EvmConfig.Transactions().MaxQueued(), + "nonceAutoSync", evmConfig.NonceAutoSync(), + "limitDefault", evmConfig.GasEstimator().LimitDefault(), + ) + + txm, err := txmgr.NewTxm( + db, + config, + config.EvmConfig.GasEstimator(), + config.EvmConfig.Transactions(), + nil, + dbConfig, + dbConfig.Listener(), + ethClient, + lggr, + lp, + keyStore, + estimator, + ht) + require.NoError(t, err, "can't create tx manager") + + _, unsub := broadcaster.Subscribe(txm) + t.Cleanup(unsub) + + return txm, estimator +} + +// Code below copied/pasted and slightly modified in order to work from core/chains/evm/txmgr/test_helpers.go. + +func ptr[T any](t T) *T { return &t } + +type TestDatabaseConfig struct { + config.Database + defaultQueryTimeout time.Duration +} + +func (d *TestDatabaseConfig) DefaultQueryTimeout() time.Duration { + return d.defaultQueryTimeout +} + +func (d *TestDatabaseConfig) LogSQL() bool { + return false +} + +type TestListenerConfig struct { + config.Listener +} + +func (l *TestListenerConfig) FallbackPollInterval() time.Duration { + return 1 * time.Minute +} + +func (d *TestDatabaseConfig) Listener() config.Listener { + return &TestListenerConfig{} +} + +type TestHeadTrackerConfig struct{} + +// FinalityTagBypass implements config.HeadTracker. +func (t *TestHeadTrackerConfig) FinalityTagBypass() bool { + return false +} + +// HistoryDepth implements config.HeadTracker. +func (t *TestHeadTrackerConfig) HistoryDepth() uint32 { + return 50 +} + +// MaxAllowedFinalityDepth implements config.HeadTracker. +func (t *TestHeadTrackerConfig) MaxAllowedFinalityDepth() uint32 { + return 100 +} + +// MaxBufferSize implements config.HeadTracker. +func (t *TestHeadTrackerConfig) MaxBufferSize() uint32 { + return 100 +} + +// SamplingInterval implements config.HeadTracker. +func (t *TestHeadTrackerConfig) SamplingInterval() time.Duration { + return 1 * time.Second +} + +var _ evmconfig.HeadTracker = (*TestHeadTrackerConfig)(nil) + +type TestEvmConfig struct { + evmconfig.EVM + HeadTrackerConfig evmconfig.HeadTracker + MaxInFlight uint32 + ReaperInterval time.Duration + ReaperThreshold time.Duration + ResendAfterThreshold time.Duration + BumpThreshold uint64 + MaxQueued uint64 + Enabled bool + Threshold uint32 + MinAttempts uint32 + DetectionApiUrl *url.URL +} + +func (e *TestEvmConfig) FinalityTagEnabled() bool { + return false +} + +func (e *TestEvmConfig) FinalityDepth() uint32 { + return 42 +} + +func (e *TestEvmConfig) FinalizedBlockOffset() uint32 { + return 42 +} + +func (e *TestEvmConfig) BlockEmissionIdleWarningThreshold() time.Duration { + return 10 * time.Second +} + +func (e *TestEvmConfig) Transactions() evmconfig.Transactions { + return &transactionsConfig{e: e, autoPurge: &autoPurgeConfig{}} +} + +func (e *TestEvmConfig) NonceAutoSync() bool { return true } + +func (e *TestEvmConfig) ChainType() chaintype.ChainType { return "" } + +type TestGasEstimatorConfig struct { + bumpThreshold uint64 +} + +func (g *TestGasEstimatorConfig) BlockHistory() evmconfig.BlockHistory { + return &TestBlockHistoryConfig{} +} + +func (g *TestGasEstimatorConfig) EIP1559DynamicFees() bool { return false } +func (g *TestGasEstimatorConfig) LimitDefault() uint64 { return 1e6 } +func (g *TestGasEstimatorConfig) BumpPercent() uint16 { return 2 } +func (g *TestGasEstimatorConfig) BumpThreshold() uint64 { return g.bumpThreshold } +func (g *TestGasEstimatorConfig) BumpMin() *assets.Wei { return assets.GWei(1) } +func (g *TestGasEstimatorConfig) FeeCapDefault() *assets.Wei { return assets.GWei(1) } +func (g *TestGasEstimatorConfig) PriceDefault() *assets.Wei { return assets.GWei(1) } +func (g *TestGasEstimatorConfig) TipCapDefault() *assets.Wei { return assets.GWei(1) } +func (g *TestGasEstimatorConfig) TipCapMin() *assets.Wei { return assets.GWei(1) } +func (g *TestGasEstimatorConfig) LimitMax() uint64 { return 0 } +func (g *TestGasEstimatorConfig) LimitMultiplier() float32 { return 1 } +func (g *TestGasEstimatorConfig) BumpTxDepth() uint32 { return 42 } +func (g *TestGasEstimatorConfig) LimitTransfer() uint64 { return 42 } +func (g *TestGasEstimatorConfig) PriceMax() *assets.Wei { return assets.GWei(1) } +func (g *TestGasEstimatorConfig) PriceMin() *assets.Wei { return assets.GWei(1) } +func (g *TestGasEstimatorConfig) Mode() string { return "FixedPrice" } +func (g *TestGasEstimatorConfig) LimitJobType() evmconfig.LimitJobType { + return &TestLimitJobTypeConfig{} +} +func (g *TestGasEstimatorConfig) PriceMaxKey(addr common.Address) *assets.Wei { + return assets.GWei(1) +} + +func (e *TestEvmConfig) GasEstimator() evmconfig.GasEstimator { + return &TestGasEstimatorConfig{bumpThreshold: e.BumpThreshold} +} + +type TestLimitJobTypeConfig struct { +} + +func (l *TestLimitJobTypeConfig) OCR() *uint32 { return ptr(uint32(0)) } +func (l *TestLimitJobTypeConfig) OCR2() *uint32 { return ptr(uint32(0)) } +func (l *TestLimitJobTypeConfig) DR() *uint32 { return ptr(uint32(0)) } +func (l *TestLimitJobTypeConfig) FM() *uint32 { return ptr(uint32(0)) } +func (l *TestLimitJobTypeConfig) Keeper() *uint32 { return ptr(uint32(0)) } +func (l *TestLimitJobTypeConfig) VRF() *uint32 { return ptr(uint32(0)) } + +type TestBlockHistoryConfig struct { + evmconfig.BlockHistory +} + +func (b *TestBlockHistoryConfig) BatchSize() uint32 { return 42 } +func (b *TestBlockHistoryConfig) BlockDelay() uint16 { return 42 } +func (b *TestBlockHistoryConfig) BlockHistorySize() uint16 { return 42 } +func (b *TestBlockHistoryConfig) EIP1559FeeCapBufferBlocks() uint16 { return 42 } +func (b *TestBlockHistoryConfig) TransactionPercentile() uint16 { return 42 } + +type transactionsConfig struct { + evmconfig.Transactions + e *TestEvmConfig + autoPurge evmconfig.AutoPurgeConfig +} + +func (*transactionsConfig) ForwardersEnabled() bool { return false } +func (t *transactionsConfig) MaxInFlight() uint32 { return t.e.MaxInFlight } +func (t *transactionsConfig) MaxQueued() uint64 { return t.e.MaxQueued } +func (t *transactionsConfig) ReaperInterval() time.Duration { return t.e.ReaperInterval } +func (t *transactionsConfig) ReaperThreshold() time.Duration { return t.e.ReaperThreshold } +func (t *transactionsConfig) ResendAfterThreshold() time.Duration { return t.e.ResendAfterThreshold } +func (t *transactionsConfig) AutoPurge() evmconfig.AutoPurgeConfig { return t.autoPurge } + +type autoPurgeConfig struct { + evmconfig.AutoPurgeConfig +} + +func (a *autoPurgeConfig) Enabled() bool { return false } + +type MockConfig struct { + EvmConfig *TestEvmConfig + RpcDefaultBatchSize uint32 + finalityDepth uint32 + finalityTagEnabled bool +} + +func (c *MockConfig) EVM() evmconfig.EVM { + return c.EvmConfig +} + +func (c *MockConfig) NonceAutoSync() bool { return true } +func (c *MockConfig) ChainType() chaintype.ChainType { return "" } +func (c *MockConfig) FinalityDepth() uint32 { return c.finalityDepth } +func (c *MockConfig) SetFinalityDepth(fd uint32) { c.finalityDepth = fd } +func (c *MockConfig) FinalityTagEnabled() bool { return c.finalityTagEnabled } +func (c *MockConfig) RPCDefaultBatchSize() uint32 { return c.RpcDefaultBatchSize } + +func MakeTestConfigs(t *testing.T) (*MockConfig, *TestDatabaseConfig, *TestEvmConfig) { + db := &TestDatabaseConfig{defaultQueryTimeout: utils.DefaultQueryTimeout} + ec := &TestEvmConfig{ + HeadTrackerConfig: &TestHeadTrackerConfig{}, + BumpThreshold: 42, + MaxInFlight: uint32(42), + MaxQueued: uint64(0), + ReaperInterval: time.Duration(0), + ReaperThreshold: time.Duration(0), + } + config := &MockConfig{EvmConfig: ec} + return config, db, ec +} diff --git a/core/capabilities/ccip/ocrimpls/keyring.go b/core/capabilities/ccip/ocrimpls/keyring.go new file mode 100644 index 00000000000..4b15c75b09a --- /dev/null +++ b/core/capabilities/ccip/ocrimpls/keyring.go @@ -0,0 +1,61 @@ +package ocrimpls + +import ( + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" + "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + + "github.com/smartcontractkit/chainlink/v2/core/logger" +) + +var _ ocr3types.OnchainKeyring[[]byte] = &ocr3Keyring[[]byte]{} + +type ocr3Keyring[RI any] struct { + core types.OnchainKeyring + lggr logger.Logger +} + +func NewOnchainKeyring[RI any](keyring types.OnchainKeyring, lggr logger.Logger) *ocr3Keyring[RI] { + return &ocr3Keyring[RI]{ + core: keyring, + lggr: lggr.Named("OCR3Keyring"), + } +} + +func (w *ocr3Keyring[RI]) PublicKey() types.OnchainPublicKey { + return w.core.PublicKey() +} + +func (w *ocr3Keyring[RI]) MaxSignatureLength() int { + return w.core.MaxSignatureLength() +} + +func (w *ocr3Keyring[RI]) Sign(configDigest types.ConfigDigest, seqNr uint64, r ocr3types.ReportWithInfo[RI]) (signature []byte, err error) { + epoch, round := uint64ToUint32AndUint8(seqNr) + rCtx := types.ReportContext{ + ReportTimestamp: types.ReportTimestamp{ + ConfigDigest: configDigest, + Epoch: epoch, + Round: round, + }, + } + + w.lggr.Debugw("signing report", "configDigest", configDigest.Hex(), "seqNr", seqNr, "report", hexutil.Encode(r.Report)) + + return w.core.Sign(rCtx, r.Report) +} + +func (w *ocr3Keyring[RI]) Verify(key types.OnchainPublicKey, configDigest types.ConfigDigest, seqNr uint64, r ocr3types.ReportWithInfo[RI], signature []byte) bool { + epoch, round := uint64ToUint32AndUint8(seqNr) + rCtx := types.ReportContext{ + ReportTimestamp: types.ReportTimestamp{ + ConfigDigest: configDigest, + Epoch: epoch, + Round: round, + }, + } + + w.lggr.Debugw("verifying report", "configDigest", configDigest.Hex(), "seqNr", seqNr, "report", hexutil.Encode(r.Report)) + + return w.core.Verify(key, rCtx, r.Report, signature) +} diff --git a/core/capabilities/ccip/oraclecreator/inprocess.go b/core/capabilities/ccip/oraclecreator/inprocess.go new file mode 100644 index 00000000000..6616d356756 --- /dev/null +++ b/core/capabilities/ccip/oraclecreator/inprocess.go @@ -0,0 +1,371 @@ +package oraclecreator + +import ( + "context" + "fmt" + "time" + + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ccipevm" + evmconfig "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/configs/evm" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ocrimpls" + cctypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/google/uuid" + "github.com/prometheus/client_golang/prometheus" + + chainsel "github.com/smartcontractkit/chain-selectors" + + "github.com/smartcontractkit/chainlink-ccip/pkg/consts" + "github.com/smartcontractkit/chainlink-ccip/pluginconfig" + + "github.com/smartcontractkit/libocr/commontypes" + libocr3 "github.com/smartcontractkit/libocr/offchainreporting2plus" + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + + commitocr3 "github.com/smartcontractkit/chainlink-ccip/commit" + execocr3 "github.com/smartcontractkit/chainlink-ccip/execute" + ccipreaderpkg "github.com/smartcontractkit/chainlink-ccip/pkg/reader" + + "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink/v2/core/chains/legacyevm" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/job" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/chaintype" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ocr2key" + "github.com/smartcontractkit/chainlink/v2/core/services/ocrcommon" + "github.com/smartcontractkit/chainlink/v2/core/services/relay" + "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" + evmrelaytypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" + "github.com/smartcontractkit/chainlink/v2/core/services/synchronization" + "github.com/smartcontractkit/chainlink/v2/core/services/telemetry" +) + +var _ cctypes.OracleCreator = &inprocessOracleCreator{} + +const ( + defaultCommitGasLimit = 500_000 +) + +// inprocessOracleCreator creates oracles that reference plugins running +// in the same process as the chainlink node, i.e not LOOPPs. +type inprocessOracleCreator struct { + ocrKeyBundles map[string]ocr2key.KeyBundle + transmitters map[types.RelayID][]string + chains legacyevm.LegacyChainContainer + peerWrapper *ocrcommon.SingletonPeerWrapper + externalJobID uuid.UUID + jobID int32 + isNewlyCreatedJob bool + pluginConfig job.JSONConfig + db ocr3types.Database + lggr logger.Logger + monitoringEndpointGen telemetry.MonitoringEndpointGenerator + bootstrapperLocators []commontypes.BootstrapperLocator + homeChainReader ccipreaderpkg.HomeChain +} + +func New( + ocrKeyBundles map[string]ocr2key.KeyBundle, + transmitters map[types.RelayID][]string, + chains legacyevm.LegacyChainContainer, + peerWrapper *ocrcommon.SingletonPeerWrapper, + externalJobID uuid.UUID, + jobID int32, + isNewlyCreatedJob bool, + pluginConfig job.JSONConfig, + db ocr3types.Database, + lggr logger.Logger, + monitoringEndpointGen telemetry.MonitoringEndpointGenerator, + bootstrapperLocators []commontypes.BootstrapperLocator, + homeChainReader ccipreaderpkg.HomeChain, +) cctypes.OracleCreator { + return &inprocessOracleCreator{ + ocrKeyBundles: ocrKeyBundles, + transmitters: transmitters, + chains: chains, + peerWrapper: peerWrapper, + externalJobID: externalJobID, + jobID: jobID, + isNewlyCreatedJob: isNewlyCreatedJob, + pluginConfig: pluginConfig, + db: db, + lggr: lggr, + monitoringEndpointGen: monitoringEndpointGen, + bootstrapperLocators: bootstrapperLocators, + homeChainReader: homeChainReader, + } +} + +// CreateBootstrapOracle implements types.OracleCreator. +func (i *inprocessOracleCreator) CreateBootstrapOracle(config cctypes.OCR3ConfigWithMeta) (cctypes.CCIPOracle, error) { + // Assuming that the chain selector is referring to an evm chain for now. + // TODO: add an api that returns chain family. + chainID, err := chainsel.ChainIdFromSelector(uint64(config.Config.ChainSelector)) + if err != nil { + return nil, fmt.Errorf("failed to get chain ID from selector: %w", err) + } + + destChainFamily := chaintype.EVM + destRelayID := types.NewRelayID(string(destChainFamily), fmt.Sprintf("%d", chainID)) + + bootstrapperArgs := libocr3.BootstrapperArgs{ + BootstrapperFactory: i.peerWrapper.Peer2, + V2Bootstrappers: i.bootstrapperLocators, + ContractConfigTracker: ocrimpls.NewConfigTracker(config), + Database: i.db, + LocalConfig: defaultLocalConfig(), + Logger: ocrcommon.NewOCRWrapper( + i.lggr. + Named("CCIPBootstrap"). + Named(destRelayID.String()). + Named(config.Config.ChainSelector.String()). + Named(hexutil.Encode(config.Config.OfframpAddress)), + false, /* traceLogging */ + func(ctx context.Context, msg string) {}), + MonitoringEndpoint: i.monitoringEndpointGen.GenMonitoringEndpoint( + string(destChainFamily), + destRelayID.ChainID, + hexutil.Encode(config.Config.OfframpAddress), + synchronization.OCR3CCIPBootstrap, + ), + OffchainConfigDigester: ocrimpls.NewConfigDigester(config.ConfigDigest), + } + bootstrapper, err := libocr3.NewBootstrapper(bootstrapperArgs) + if err != nil { + return nil, err + } + return bootstrapper, nil +} + +// CreatePluginOracle implements types.OracleCreator. +func (i *inprocessOracleCreator) CreatePluginOracle(pluginType cctypes.PluginType, config cctypes.OCR3ConfigWithMeta) (cctypes.CCIPOracle, error) { + // Assuming that the chain selector is referring to an evm chain for now. + // TODO: add an api that returns chain family. + destChainID, err := chainsel.ChainIdFromSelector(uint64(config.Config.ChainSelector)) + if err != nil { + return nil, fmt.Errorf("failed to get chain ID from selector %d: %w", config.Config.ChainSelector, err) + } + destChainFamily := relay.NetworkEVM + destRelayID := types.NewRelayID(destChainFamily, fmt.Sprintf("%d", destChainID)) + + configTracker := ocrimpls.NewConfigTracker(config) + publicConfig, err := configTracker.PublicConfig() + if err != nil { + return nil, fmt.Errorf("failed to get public config from OCR config: %w", err) + } + var execBatchGasLimit uint64 + if pluginType == cctypes.PluginTypeCCIPExec { + execOffchainConfig, err2 := pluginconfig.DecodeExecuteOffchainConfig(publicConfig.ReportingPluginConfig) + if err2 != nil { + return nil, fmt.Errorf("failed to decode execute offchain config: %w, raw: %s", + err2, string(publicConfig.ReportingPluginConfig)) + } + if execOffchainConfig.BatchGasLimit == 0 && destChainFamily == relay.NetworkEVM { + return nil, fmt.Errorf("BatchGasLimit not set in execute offchain config, must be > 0") + } + execBatchGasLimit = execOffchainConfig.BatchGasLimit + } + + // this is so that we can use the msg hasher and report encoder from that dest chain relayer's provider. + contractReaders := make(map[cciptypes.ChainSelector]types.ContractReader) + chainWriters := make(map[cciptypes.ChainSelector]types.ChainWriter) + for _, chain := range i.chains.Slice() { + var chainReaderConfig evmrelaytypes.ChainReaderConfig + if chain.ID().Uint64() == destChainID { + chainReaderConfig = evmconfig.DestReaderConfig() + } else { + chainReaderConfig = evmconfig.SourceReaderConfig() + } + cr, err2 := evm.NewChainReaderService( + context.Background(), + i.lggr. + Named("EVMChainReaderService"). + Named(chain.ID().String()). + Named(pluginType.String()), + chain.LogPoller(), + chain.HeadTracker(), + chain.Client(), + chainReaderConfig, + ) + if err2 != nil { + return nil, fmt.Errorf("failed to create contract reader for chain %s: %w", chain.ID(), err2) + } + + if chain.ID().Uint64() == destChainID { + // bind the chain reader to the dest chain's offramp. + offrampAddressHex := common.BytesToAddress(config.Config.OfframpAddress).Hex() + err3 := cr.Bind(context.Background(), []types.BoundContract{ + { + Address: offrampAddressHex, + Name: consts.ContractNameOffRamp, + }, + }) + if err3 != nil { + return nil, fmt.Errorf("failed to bind chain reader for dest chain %s's offramp at %s: %w", chain.ID(), offrampAddressHex, err3) + } + } + + // TODO: figure out shutdown. + // maybe from the plugin directly? + err2 = cr.Start(context.Background()) + if err2 != nil { + return nil, fmt.Errorf("failed to start contract reader for chain %s: %w", chain.ID(), err2) + } + + // Even though we only write to the dest chain, we need to create chain writers for all chains + // we know about in order to post gas prices on the dest. + var fromAddress common.Address + transmitter, ok := i.transmitters[types.NewRelayID(relay.NetworkEVM, chain.ID().String())] + if ok { + fromAddress = common.HexToAddress(transmitter[0]) + } + cw, err2 := evm.NewChainWriterService( + i.lggr.Named("EVMChainWriterService"). + Named(chain.ID().String()). + Named(pluginType.String()), + chain.Client(), + chain.TxManager(), + chain.GasEstimator(), + evmconfig.ChainWriterConfigRaw( + fromAddress, + chain.Config().EVM().GasEstimator().PriceMaxKey(fromAddress), + defaultCommitGasLimit, + execBatchGasLimit, + ), + ) + if err2 != nil { + return nil, fmt.Errorf("failed to create chain writer for chain %s: %w", chain.ID(), err2) + } + + // TODO: figure out shutdown. + // maybe from the plugin directly? + err2 = cw.Start(context.Background()) + if err2 != nil { + return nil, fmt.Errorf("failed to start chain writer for chain %s: %w", chain.ID(), err2) + } + + chainSelector, ok := chainsel.EvmChainIdToChainSelector()[chain.ID().Uint64()] + if !ok { + return nil, fmt.Errorf("failed to get chain selector from chain ID %s", chain.ID()) + } + + contractReaders[cciptypes.ChainSelector(chainSelector)] = cr + chainWriters[cciptypes.ChainSelector(chainSelector)] = cw + } + + // build the onchain keyring. it will be the signing key for the destination chain family. + keybundle, ok := i.ocrKeyBundles[destChainFamily] + if !ok { + return nil, fmt.Errorf("no OCR key bundle found for chain family %s, forgot to create one?", destChainFamily) + } + onchainKeyring := ocrimpls.NewOnchainKeyring[[]byte](keybundle, i.lggr) + + // build the contract transmitter + // assume that we are using the first account in the keybundle as the from account + // and that we are able to transmit to the dest chain. + // TODO: revisit this in the future, since not all oracles will be able to transmit to the dest chain. + destChainWriter, ok := chainWriters[config.Config.ChainSelector] + if !ok { + return nil, fmt.Errorf("no chain writer found for dest chain selector %d, can't create contract transmitter", + config.Config.ChainSelector) + } + destFromAccounts, ok := i.transmitters[destRelayID] + if !ok { + return nil, fmt.Errorf("no transmitter found for dest relay ID %s, can't create contract transmitter", destRelayID) + } + + // TODO: Extract the correct transmitter address from the destsFromAccount + var factory ocr3types.ReportingPluginFactory[[]byte] + var transmitter ocr3types.ContractTransmitter[[]byte] + if config.Config.PluginType == uint8(cctypes.PluginTypeCCIPCommit) { + factory = commitocr3.NewPluginFactory( + i.lggr. + Named("CCIPCommitPlugin"). + Named(destRelayID.String()). + Named(fmt.Sprintf("%d", config.Config.ChainSelector)). + Named(hexutil.Encode(config.Config.OfframpAddress)), + ccipreaderpkg.OCR3ConfigWithMeta(config), + ccipevm.NewCommitPluginCodecV1(), + ccipevm.NewMessageHasherV1(), + i.homeChainReader, + contractReaders, + chainWriters, + ) + transmitter = ocrimpls.NewCommitContractTransmitter[[]byte](destChainWriter, + ocrtypes.Account(destFromAccounts[0]), + hexutil.Encode(config.Config.OfframpAddress), // TODO: this works for evm only, how about non-evm? + ) + } else if config.Config.PluginType == uint8(cctypes.PluginTypeCCIPExec) { + factory = execocr3.NewPluginFactory( + i.lggr. + Named("CCIPExecPlugin"). + Named(destRelayID.String()). + Named(hexutil.Encode(config.Config.OfframpAddress)), + ccipreaderpkg.OCR3ConfigWithMeta(config), + ccipevm.NewExecutePluginCodecV1(), + ccipevm.NewMessageHasherV1(), + i.homeChainReader, + contractReaders, + chainWriters, + ) + transmitter = ocrimpls.NewExecContractTransmitter[[]byte](destChainWriter, + ocrtypes.Account(destFromAccounts[0]), + hexutil.Encode(config.Config.OfframpAddress), // TODO: this works for evm only, how about non-evm? + ) + } else { + return nil, fmt.Errorf("unsupported plugin type %d", config.Config.PluginType) + } + + oracleArgs := libocr3.OCR3OracleArgs[[]byte]{ + BinaryNetworkEndpointFactory: i.peerWrapper.Peer2, + Database: i.db, + V2Bootstrappers: i.bootstrapperLocators, + ContractConfigTracker: configTracker, + ContractTransmitter: transmitter, + LocalConfig: defaultLocalConfig(), + Logger: ocrcommon.NewOCRWrapper( + i.lggr. + Named(fmt.Sprintf("CCIP%sOCR3", pluginType.String())). + Named(destRelayID.String()). + Named(hexutil.Encode(config.Config.OfframpAddress)), + false, + func(ctx context.Context, msg string) {}), + MetricsRegisterer: prometheus.WrapRegistererWith(map[string]string{"name": fmt.Sprintf("commit-%d", config.Config.ChainSelector)}, prometheus.DefaultRegisterer), + MonitoringEndpoint: i.monitoringEndpointGen.GenMonitoringEndpoint( + destChainFamily, + destRelayID.ChainID, + string(config.Config.OfframpAddress), + synchronization.OCR3CCIPCommit, + ), + OffchainConfigDigester: ocrimpls.NewConfigDigester(config.ConfigDigest), + OffchainKeyring: keybundle, + OnchainKeyring: onchainKeyring, + ReportingPluginFactory: factory, + } + oracle, err := libocr3.NewOracle(oracleArgs) + if err != nil { + return nil, err + } + return oracle, nil +} + +func defaultLocalConfig() ocrtypes.LocalConfig { + return ocrtypes.LocalConfig{ + BlockchainTimeout: 10 * time.Second, + // Config tracking is handled by the launcher, since we're doing blue-green + // deployments we're not going to be using OCR's built-in config switching, + // which always shuts down the previous instance. + ContractConfigConfirmations: 1, + SkipContractConfigConfirmations: true, + ContractConfigTrackerPollInterval: 10 * time.Second, + ContractTransmitterTransmitTimeout: 10 * time.Second, + DatabaseTimeout: 10 * time.Second, + MinOCR2MaxDurationQuery: 1 * time.Second, + DevelopmentMode: "false", + } +} diff --git a/core/capabilities/ccip/oraclecreator/inprocess_test.go b/core/capabilities/ccip/oraclecreator/inprocess_test.go new file mode 100644 index 00000000000..639f01e62e3 --- /dev/null +++ b/core/capabilities/ccip/oraclecreator/inprocess_test.go @@ -0,0 +1,239 @@ +package oraclecreator_test + +import ( + "fmt" + "testing" + "time" + + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/oraclecreator" + cctypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/google/uuid" + "github.com/hashicorp/consul/sdk/freeport" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/guregu/null.v4" + + chainsel "github.com/smartcontractkit/chain-selectors" + "github.com/smartcontractkit/libocr/offchainreporting2/types" + confighelper2 "github.com/smartcontractkit/libocr/offchainreporting2plus/confighelper" + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3confighelper" + + "github.com/smartcontractkit/chainlink-ccip/pkg/reader" + "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + + "github.com/smartcontractkit/libocr/commontypes" + + "github.com/smartcontractkit/chainlink/v2/core/bridges" + "github.com/smartcontractkit/chainlink/v2/core/internal/cltest" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/configtest" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" + "github.com/smartcontractkit/chainlink/v2/core/services/job" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/chaintype" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2" + ocr2validate "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/validate" + "github.com/smartcontractkit/chainlink/v2/core/services/ocrcommon" + "github.com/smartcontractkit/chainlink/v2/core/services/pipeline" + "github.com/smartcontractkit/chainlink/v2/core/services/synchronization" + "github.com/smartcontractkit/chainlink/v2/core/services/telemetry" + "github.com/smartcontractkit/chainlink/v2/core/testdata/testspecs" + "github.com/smartcontractkit/chainlink/v2/core/utils" +) + +func TestOracleCreator_CreateBootstrap(t *testing.T) { + db := pgtest.NewSqlxDB(t) + + keyStore := keystore.New(db, utils.DefaultScryptParams, logger.NullLogger) + require.NoError(t, keyStore.Unlock(testutils.Context(t), cltest.Password), "unable to unlock keystore") + p2pKey, err := keyStore.P2P().Create(testutils.Context(t)) + require.NoError(t, err) + peerID := p2pKey.PeerID() + listenPort := freeport.GetOne(t) + generalConfig := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { + c.P2P.PeerID = ptr(peerID) + c.P2P.TraceLogging = ptr(false) + c.P2P.V2.Enabled = ptr(true) + c.P2P.V2.ListenAddresses = ptr([]string{fmt.Sprintf("127.0.0.1:%d", listenPort)}) + + c.OCR2.Enabled = ptr(true) + }) + peerWrapper := ocrcommon.NewSingletonPeerWrapper(keyStore, generalConfig.P2P(), generalConfig.OCR(), db, logger.NullLogger) + require.NoError(t, peerWrapper.Start(testutils.Context(t))) + t.Cleanup(func() { assert.NoError(t, peerWrapper.Close()) }) + + // NOTE: this is a bit of a hack to get the OCR2 job created in order to use the ocr db + // the ocr2_contract_configs table has a foreign key constraint on ocr2_oracle_spec_id + // which is passed into ocr2.NewDB. + pipelineORM := pipeline.NewORM(db, + logger.NullLogger, generalConfig.JobPipeline().MaxSuccessfulRuns()) + bridgesORM := bridges.NewORM(db) + + jobORM := job.NewORM(db, pipelineORM, bridgesORM, keyStore, logger.TestLogger(t)) + t.Cleanup(func() { assert.NoError(t, jobORM.Close()) }) + + jb, err := ocr2validate.ValidatedOracleSpecToml(testutils.Context(t), generalConfig.OCR2(), generalConfig.Insecure(), testspecs.GetOCR2EVMSpecMinimal(), nil) + require.NoError(t, err) + const juelsPerFeeCoinSource = ` + ds [type=http method=GET url="https://chain.link/ETH-USD"]; + ds_parse [type=jsonparse path="data.price" separator="."]; + ds_multiply [type=multiply times=100]; + ds -> ds_parse -> ds_multiply;` + + _, address := cltest.MustInsertRandomKey(t, keyStore.Eth()) + jb.Name = null.StringFrom("Job 1") + jb.OCR2OracleSpec.TransmitterID = null.StringFrom(address.String()) + jb.OCR2OracleSpec.PluginConfig["juelsPerFeeCoinSource"] = juelsPerFeeCoinSource + + err = jobORM.CreateJob(testutils.Context(t), &jb) + require.NoError(t, err) + + cltest.AssertCount(t, db, "ocr2_oracle_specs", 1) + cltest.AssertCount(t, db, "jobs", 1) + + var oracleSpecID int32 + err = db.Get(&oracleSpecID, "SELECT id FROM ocr2_oracle_specs LIMIT 1") + require.NoError(t, err) + + ocrdb := ocr2.NewDB(db, oracleSpecID, 0, logger.NullLogger) + + oc := oraclecreator.New( + nil, + nil, + nil, + peerWrapper, + uuid.Max, + 0, + false, + nil, + ocrdb, + logger.TestLogger(t), + &mockEndpointGen{}, + []commontypes.BootstrapperLocator{}, + nil, + ) + + chainSelector := chainsel.GETH_TESTNET.Selector + oracles, offchainConfig := ocrOffchainConfig(t, keyStore) + bootstrapP2PID, err := p2pkey.MakePeerID(oracles[0].PeerID) + require.NoError(t, err) + transmitters := func() [][]byte { + var transmitters [][]byte + for _, o := range oracles { + transmitters = append(transmitters, hexutil.MustDecode(string(o.TransmitAccount))) + } + return transmitters + }() + configDigest := ccipConfigDigest() + bootstrap, err := oc.CreateBootstrapOracle(cctypes.OCR3ConfigWithMeta{ + ConfigDigest: configDigest, + ConfigCount: 1, + Config: reader.OCR3Config{ + ChainSelector: ccipocr3.ChainSelector(chainSelector), + OfframpAddress: testutils.NewAddress().Bytes(), + PluginType: uint8(cctypes.PluginTypeCCIPCommit), + F: 1, + OffchainConfigVersion: 30, + BootstrapP2PIds: [][32]byte{bootstrapP2PID}, + P2PIds: func() [][32]byte { + var ids [][32]byte + for _, o := range oracles { + id, err2 := p2pkey.MakePeerID(o.PeerID) + require.NoError(t, err2) + ids = append(ids, id) + } + return ids + }(), + Signers: func() [][]byte { + var signers [][]byte + for _, o := range oracles { + signers = append(signers, o.OnchainPublicKey) + } + return signers + }(), + Transmitters: transmitters, + OffchainConfig: offchainConfig, + }, + }) + require.NoError(t, err) + require.NoError(t, bootstrap.Start()) + t.Cleanup(func() { assert.NoError(t, bootstrap.Close()) }) + + tests.AssertEventually(t, func() bool { + c, err := ocrdb.ReadConfig(testutils.Context(t)) + require.NoError(t, err) + return c.ConfigDigest == configDigest + }) +} + +func ccipConfigDigest() [32]byte { + rand32Bytes := testutils.Random32Byte() + // overwrite first four bytes to be 0x000a, to match the prefix in libocr. + rand32Bytes[0] = 0x00 + rand32Bytes[1] = 0x0a + return rand32Bytes +} + +type mockEndpointGen struct{} + +func (m *mockEndpointGen) GenMonitoringEndpoint(network string, chainID string, contractID string, telemType synchronization.TelemetryType) commontypes.MonitoringEndpoint { + return &telemetry.NoopAgent{} +} + +func ptr[T any](b T) *T { + return &b +} + +func ocrOffchainConfig(t *testing.T, ks keystore.Master) (oracles []confighelper2.OracleIdentityExtra, offchainConfig []byte) { + for i := 0; i < 4; i++ { + kb, err := ks.OCR2().Create(testutils.Context(t), chaintype.EVM) + require.NoError(t, err) + p2pKey, err := ks.P2P().Create(testutils.Context(t)) + require.NoError(t, err) + ethKey, err := ks.Eth().Create(testutils.Context(t)) + require.NoError(t, err) + oracles = append(oracles, confighelper2.OracleIdentityExtra{ + OracleIdentity: confighelper2.OracleIdentity{ + OffchainPublicKey: kb.OffchainPublicKey(), + OnchainPublicKey: types.OnchainPublicKey(kb.OnChainPublicKey()), + PeerID: p2pKey.ID(), + TransmitAccount: types.Account(ethKey.Address.Hex()), + }, + ConfigEncryptionPublicKey: kb.ConfigEncryptionPublicKey(), + }) + } + var schedule []int + for range oracles { + schedule = append(schedule, 1) + } + offchainConfig, onchainConfig := []byte{}, []byte{} + f := uint8(1) + + _, _, _, _, _, offchainConfig, err := ocr3confighelper.ContractSetConfigArgsForTests( + 30*time.Second, // deltaProgress + 10*time.Second, // deltaResend + 20*time.Second, // deltaInitial + 2*time.Second, // deltaRound + 20*time.Second, // deltaGrace + 10*time.Second, // deltaCertifiedCommitRequest + 10*time.Second, // deltaStage + 3, // rmax + schedule, + oracles, + offchainConfig, + 50*time.Millisecond, // maxDurationQuery + 5*time.Second, // maxDurationObservation + 10*time.Second, // maxDurationShouldAcceptAttestedReport + 10*time.Second, // maxDurationShouldTransmitAcceptedReport + int(f), + onchainConfig) + require.NoError(t, err, "failed to create contract config") + + return oracles, offchainConfig +} diff --git a/core/capabilities/ccip/types/mocks/ccip_oracle.go b/core/capabilities/ccip/types/mocks/ccip_oracle.go new file mode 100644 index 00000000000..c849b3d9414 --- /dev/null +++ b/core/capabilities/ccip/types/mocks/ccip_oracle.go @@ -0,0 +1,122 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// CCIPOracle is an autogenerated mock type for the CCIPOracle type +type CCIPOracle struct { + mock.Mock +} + +type CCIPOracle_Expecter struct { + mock *mock.Mock +} + +func (_m *CCIPOracle) EXPECT() *CCIPOracle_Expecter { + return &CCIPOracle_Expecter{mock: &_m.Mock} +} + +// Close provides a mock function with given fields: +func (_m *CCIPOracle) Close() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Close") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CCIPOracle_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' +type CCIPOracle_Close_Call struct { + *mock.Call +} + +// Close is a helper method to define mock.On call +func (_e *CCIPOracle_Expecter) Close() *CCIPOracle_Close_Call { + return &CCIPOracle_Close_Call{Call: _e.mock.On("Close")} +} + +func (_c *CCIPOracle_Close_Call) Run(run func()) *CCIPOracle_Close_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CCIPOracle_Close_Call) Return(_a0 error) *CCIPOracle_Close_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *CCIPOracle_Close_Call) RunAndReturn(run func() error) *CCIPOracle_Close_Call { + _c.Call.Return(run) + return _c +} + +// Start provides a mock function with given fields: +func (_m *CCIPOracle) Start() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Start") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CCIPOracle_Start_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Start' +type CCIPOracle_Start_Call struct { + *mock.Call +} + +// Start is a helper method to define mock.On call +func (_e *CCIPOracle_Expecter) Start() *CCIPOracle_Start_Call { + return &CCIPOracle_Start_Call{Call: _e.mock.On("Start")} +} + +func (_c *CCIPOracle_Start_Call) Run(run func()) *CCIPOracle_Start_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CCIPOracle_Start_Call) Return(_a0 error) *CCIPOracle_Start_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *CCIPOracle_Start_Call) RunAndReturn(run func() error) *CCIPOracle_Start_Call { + _c.Call.Return(run) + return _c +} + +// NewCCIPOracle creates a new instance of CCIPOracle. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewCCIPOracle(t interface { + mock.TestingT + Cleanup(func()) +}) *CCIPOracle { + mock := &CCIPOracle{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/capabilities/ccip/types/mocks/home_chain_reader.go b/core/capabilities/ccip/types/mocks/home_chain_reader.go new file mode 100644 index 00000000000..a5a581a1d2d --- /dev/null +++ b/core/capabilities/ccip/types/mocks/home_chain_reader.go @@ -0,0 +1,129 @@ +package mocks + +import ( + "context" + + mapset "github.com/deckarep/golang-set/v2" + "github.com/stretchr/testify/mock" + + ccipreaderpkg "github.com/smartcontractkit/chainlink-ccip/pkg/reader" + + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + + "github.com/smartcontractkit/libocr/ragep2p/types" +) + +var _ ccipreaderpkg.HomeChain = (*HomeChainReader)(nil) + +type HomeChainReader struct { + mock.Mock +} + +func (_m *HomeChainReader) GetChainConfig(chainSelector cciptypes.ChainSelector) (ccipreaderpkg.ChainConfig, error) { + //TODO implement me + panic("implement me") +} + +func (_m *HomeChainReader) GetAllChainConfigs() (map[cciptypes.ChainSelector]ccipreaderpkg.ChainConfig, error) { + //TODO implement me + panic("implement me") +} + +func (_m *HomeChainReader) GetSupportedChainsForPeer(id types.PeerID) (mapset.Set[cciptypes.ChainSelector], error) { + //TODO implement me + panic("implement me") +} + +func (_m *HomeChainReader) GetKnownCCIPChains() (mapset.Set[cciptypes.ChainSelector], error) { + //TODO implement me + panic("implement me") +} + +func (_m *HomeChainReader) GetFChain() (map[cciptypes.ChainSelector]int, error) { + //TODO implement me + panic("implement me") +} + +func (_m *HomeChainReader) Start(ctx context.Context) error { + //TODO implement me + panic("implement me") +} + +func (_m *HomeChainReader) Close() error { + //TODO implement me + panic("implement me") +} + +func (_m *HomeChainReader) HealthReport() map[string]error { + //TODO implement me + panic("implement me") +} + +func (_m *HomeChainReader) Name() string { + //TODO implement me + panic("implement me") +} + +// GetOCRConfigs provides a mock function with given fields: ctx, donID, pluginType +func (_m *HomeChainReader) GetOCRConfigs(ctx context.Context, donID uint32, pluginType uint8) ([]ccipreaderpkg.OCR3ConfigWithMeta, error) { + ret := _m.Called(ctx, donID, pluginType) + + if len(ret) == 0 { + panic("no return value specified for GetOCRConfigs") + } + + var r0 []ccipreaderpkg.OCR3ConfigWithMeta + var r1 error + if rf, ok := ret.Get(0).(func(ctx context.Context, donID uint32, pluginType uint8) ([]ccipreaderpkg.OCR3ConfigWithMeta, error)); ok { + return rf(ctx, donID, pluginType) + } + if rf, ok := ret.Get(0).(func(ctx context.Context, donID uint32, pluginType uint8) []ccipreaderpkg.OCR3ConfigWithMeta); ok { + r0 = rf(ctx, donID, pluginType) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]ccipreaderpkg.OCR3ConfigWithMeta) + } + } + + if rf, ok := ret.Get(1).(func(ctx context.Context, donID uint32, pluginType uint8) error); ok { + r1 = rf(ctx, donID, pluginType) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +func (_m *HomeChainReader) Ready() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Ready") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewHomeChainReader creates a new instance of HomeChainReader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewHomeChainReader(t interface { + mock.TestingT + Cleanup(func()) +}) *HomeChainReader { + mock := &HomeChainReader{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/capabilities/ccip/types/mocks/oracle_creator.go b/core/capabilities/ccip/types/mocks/oracle_creator.go new file mode 100644 index 00000000000..d83ad042bfe --- /dev/null +++ b/core/capabilities/ccip/types/mocks/oracle_creator.go @@ -0,0 +1,152 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +package mocks + +import ( + types "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" + mock "github.com/stretchr/testify/mock" +) + +// OracleCreator is an autogenerated mock type for the OracleCreator type +type OracleCreator struct { + mock.Mock +} + +type OracleCreator_Expecter struct { + mock *mock.Mock +} + +func (_m *OracleCreator) EXPECT() *OracleCreator_Expecter { + return &OracleCreator_Expecter{mock: &_m.Mock} +} + +// CreateBootstrapOracle provides a mock function with given fields: config +func (_m *OracleCreator) CreateBootstrapOracle(config types.OCR3ConfigWithMeta) (types.CCIPOracle, error) { + ret := _m.Called(config) + + if len(ret) == 0 { + panic("no return value specified for CreateBootstrapOracle") + } + + var r0 types.CCIPOracle + var r1 error + if rf, ok := ret.Get(0).(func(types.OCR3ConfigWithMeta) (types.CCIPOracle, error)); ok { + return rf(config) + } + if rf, ok := ret.Get(0).(func(types.OCR3ConfigWithMeta) types.CCIPOracle); ok { + r0 = rf(config) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(types.CCIPOracle) + } + } + + if rf, ok := ret.Get(1).(func(types.OCR3ConfigWithMeta) error); ok { + r1 = rf(config) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// OracleCreator_CreateBootstrapOracle_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateBootstrapOracle' +type OracleCreator_CreateBootstrapOracle_Call struct { + *mock.Call +} + +// CreateBootstrapOracle is a helper method to define mock.On call +// - config types.OCR3ConfigWithMeta +func (_e *OracleCreator_Expecter) CreateBootstrapOracle(config interface{}) *OracleCreator_CreateBootstrapOracle_Call { + return &OracleCreator_CreateBootstrapOracle_Call{Call: _e.mock.On("CreateBootstrapOracle", config)} +} + +func (_c *OracleCreator_CreateBootstrapOracle_Call) Run(run func(config types.OCR3ConfigWithMeta)) *OracleCreator_CreateBootstrapOracle_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(types.OCR3ConfigWithMeta)) + }) + return _c +} + +func (_c *OracleCreator_CreateBootstrapOracle_Call) Return(_a0 types.CCIPOracle, _a1 error) *OracleCreator_CreateBootstrapOracle_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *OracleCreator_CreateBootstrapOracle_Call) RunAndReturn(run func(types.OCR3ConfigWithMeta) (types.CCIPOracle, error)) *OracleCreator_CreateBootstrapOracle_Call { + _c.Call.Return(run) + return _c +} + +// CreatePluginOracle provides a mock function with given fields: pluginType, config +func (_m *OracleCreator) CreatePluginOracle(pluginType types.PluginType, config types.OCR3ConfigWithMeta) (types.CCIPOracle, error) { + ret := _m.Called(pluginType, config) + + if len(ret) == 0 { + panic("no return value specified for CreatePluginOracle") + } + + var r0 types.CCIPOracle + var r1 error + if rf, ok := ret.Get(0).(func(types.PluginType, types.OCR3ConfigWithMeta) (types.CCIPOracle, error)); ok { + return rf(pluginType, config) + } + if rf, ok := ret.Get(0).(func(types.PluginType, types.OCR3ConfigWithMeta) types.CCIPOracle); ok { + r0 = rf(pluginType, config) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(types.CCIPOracle) + } + } + + if rf, ok := ret.Get(1).(func(types.PluginType, types.OCR3ConfigWithMeta) error); ok { + r1 = rf(pluginType, config) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// OracleCreator_CreatePluginOracle_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreatePluginOracle' +type OracleCreator_CreatePluginOracle_Call struct { + *mock.Call +} + +// CreatePluginOracle is a helper method to define mock.On call +// - pluginType types.PluginType +// - config types.OCR3ConfigWithMeta +func (_e *OracleCreator_Expecter) CreatePluginOracle(pluginType interface{}, config interface{}) *OracleCreator_CreatePluginOracle_Call { + return &OracleCreator_CreatePluginOracle_Call{Call: _e.mock.On("CreatePluginOracle", pluginType, config)} +} + +func (_c *OracleCreator_CreatePluginOracle_Call) Run(run func(pluginType types.PluginType, config types.OCR3ConfigWithMeta)) *OracleCreator_CreatePluginOracle_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(types.PluginType), args[1].(types.OCR3ConfigWithMeta)) + }) + return _c +} + +func (_c *OracleCreator_CreatePluginOracle_Call) Return(_a0 types.CCIPOracle, _a1 error) *OracleCreator_CreatePluginOracle_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *OracleCreator_CreatePluginOracle_Call) RunAndReturn(run func(types.PluginType, types.OCR3ConfigWithMeta) (types.CCIPOracle, error)) *OracleCreator_CreatePluginOracle_Call { + _c.Call.Return(run) + return _c +} + +// NewOracleCreator creates a new instance of OracleCreator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewOracleCreator(t interface { + mock.TestingT + Cleanup(func()) +}) *OracleCreator { + mock := &OracleCreator{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/capabilities/ccip/types/types.go b/core/capabilities/ccip/types/types.go new file mode 100644 index 00000000000..952b8fe4465 --- /dev/null +++ b/core/capabilities/ccip/types/types.go @@ -0,0 +1,46 @@ +package types + +import ( + ccipreaderpkg "github.com/smartcontractkit/chainlink-ccip/pkg/reader" +) + +// OCR3ConfigWithMeta is a type alias in order to generate correct mocks for the OracleCreator interface. +type OCR3ConfigWithMeta ccipreaderpkg.OCR3ConfigWithMeta + +// PluginType represents the type of CCIP plugin. +// It mirrors the OCRPluginType in Internal.sol. +type PluginType uint8 + +const ( + PluginTypeCCIPCommit PluginType = 0 + PluginTypeCCIPExec PluginType = 1 +) + +func (pt PluginType) String() string { + switch pt { + case PluginTypeCCIPCommit: + return "CCIPCommit" + case PluginTypeCCIPExec: + return "CCIPExec" + default: + return "Unknown" + } +} + +// CCIPOracle represents either a CCIP commit or exec oracle or a bootstrap node. +type CCIPOracle interface { + Close() error + Start() error +} + +// OracleCreator is an interface for creating CCIP oracles. +// Whether the oracle uses a LOOPP or not is an implementation detail. +type OracleCreator interface { + // CreatePlugin creates a new oracle that will run either the commit or exec ccip plugin. + // The oracle must be returned unstarted. + CreatePluginOracle(pluginType PluginType, config OCR3ConfigWithMeta) (CCIPOracle, error) + + // CreateBootstrapOracle creates a new bootstrap node with the given OCR config. + // The oracle must be returned unstarted. + CreateBootstrapOracle(config OCR3ConfigWithMeta) (CCIPOracle, error) +} diff --git a/core/capabilities/ccip/validate/validate.go b/core/capabilities/ccip/validate/validate.go new file mode 100644 index 00000000000..04f4f4a4959 --- /dev/null +++ b/core/capabilities/ccip/validate/validate.go @@ -0,0 +1,94 @@ +package validate + +import ( + "fmt" + + "github.com/google/uuid" + "github.com/pelletier/go-toml" + + "github.com/smartcontractkit/chainlink/v2/core/services/job" + "github.com/smartcontractkit/chainlink/v2/core/services/ocrcommon" +) + +// ValidatedCCIPSpec validates the given toml string as a CCIP spec. +func ValidatedCCIPSpec(tomlString string) (jb job.Job, err error) { + var spec job.CCIPSpec + tree, err := toml.Load(tomlString) + if err != nil { + return job.Job{}, fmt.Errorf("toml error on load: %w", err) + } + // Note this validates all the fields which implement an UnmarshalText + err = tree.Unmarshal(&spec) + if err != nil { + return job.Job{}, fmt.Errorf("toml unmarshal error on spec: %w", err) + } + err = tree.Unmarshal(&jb) + if err != nil { + return job.Job{}, fmt.Errorf("toml unmarshal error on job: %w", err) + } + jb.CCIPSpec = &spec + + if jb.Type != job.CCIP { + return job.Job{}, fmt.Errorf("the only supported type is currently 'ccip', got %s", jb.Type) + } + if jb.CCIPSpec.CapabilityLabelledName == "" { + return job.Job{}, fmt.Errorf("capabilityLabelledName must be set") + } + if jb.CCIPSpec.CapabilityVersion == "" { + return job.Job{}, fmt.Errorf("capabilityVersion must be set") + } + if jb.CCIPSpec.P2PKeyID == "" { + return job.Job{}, fmt.Errorf("p2pKeyID must be set") + } + if len(jb.CCIPSpec.P2PV2Bootstrappers) == 0 { + return job.Job{}, fmt.Errorf("p2pV2Bootstrappers must be set") + } + + // ensure that the P2PV2Bootstrappers is in the right format. + for _, bootstrapperLocator := range jb.CCIPSpec.P2PV2Bootstrappers { + // needs to be of the form @: + _, err := ocrcommon.ParseBootstrapPeers([]string{bootstrapperLocator}) + if err != nil { + return job.Job{}, fmt.Errorf("p2p v2 bootstrapper locator %s is not in the correct format: %w", bootstrapperLocator, err) + } + } + + return jb, nil +} + +type SpecArgs struct { + P2PV2Bootstrappers []string `toml:"p2pV2Bootstrappers"` + CapabilityVersion string `toml:"capabilityVersion"` + CapabilityLabelledName string `toml:"capabilityLabelledName"` + OCRKeyBundleIDs map[string]string `toml:"ocrKeyBundleIDs"` + P2PKeyID string `toml:"p2pKeyID"` + RelayConfigs map[string]any `toml:"relayConfigs"` + PluginConfig map[string]any `toml:"pluginConfig"` +} + +// NewCCIPSpecToml creates a new CCIP spec in toml format from the given spec args. +func NewCCIPSpecToml(spec SpecArgs) (string, error) { + type fullSpec struct { + SpecArgs + Type string `toml:"type"` + SchemaVersion uint64 `toml:"schemaVersion"` + Name string `toml:"name"` + ExternalJobID string `toml:"externalJobID"` + } + extJobID, err := uuid.NewRandom() + if err != nil { + return "", fmt.Errorf("failed to generate external job id: %w", err) + } + marshaled, err := toml.Marshal(fullSpec{ + SpecArgs: spec, + Type: "ccip", + SchemaVersion: 1, + Name: fmt.Sprintf("%s-%s", "ccip", extJobID.String()), + ExternalJobID: extJobID.String(), + }) + if err != nil { + return "", fmt.Errorf("failed to marshal spec into toml: %w", err) + } + + return string(marshaled), nil +} diff --git a/core/capabilities/ccip/validate/validate_test.go b/core/capabilities/ccip/validate/validate_test.go new file mode 100644 index 00000000000..97958f4cf9d --- /dev/null +++ b/core/capabilities/ccip/validate/validate_test.go @@ -0,0 +1,58 @@ +package validate_test + +import ( + "testing" + + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/validate" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/v2/core/services/job" +) + +func TestNewCCIPSpecToml(t *testing.T) { + tests := []struct { + name string + specArgs validate.SpecArgs + want string + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := validate.NewCCIPSpecToml(tt.specArgs) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.want, got) + } + }) + } +} + +func TestValidatedCCIPSpec(t *testing.T) { + type args struct { + tomlString string + } + tests := []struct { + name string + args args + wantJb job.Job + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotJb, err := validate.ValidatedCCIPSpec(tt.args.tomlString) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.wantJb, gotJb) + } + }) + } +} diff --git a/core/capabilities/launcher.go b/core/capabilities/launcher.go index b30477e4c83..3fc321087b8 100644 --- a/core/capabilities/launcher.go +++ b/core/capabilities/launcher.go @@ -7,13 +7,17 @@ import ( "strings" "time" + "google.golang.org/protobuf/proto" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/capabilities/triggers" "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink-common/pkg/values" "github.com/smartcontractkit/libocr/ragep2p" ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote" "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/target" remotetypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/types" @@ -46,6 +50,42 @@ type launcher struct { subServices []services.Service } +func unmarshalCapabilityConfig(data []byte) (capabilities.CapabilityConfiguration, error) { + cconf := &capabilitiespb.CapabilityConfig{} + err := proto.Unmarshal(data, cconf) + if err != nil { + return capabilities.CapabilityConfiguration{}, err + } + + var remoteTriggerConfig *capabilities.RemoteTriggerConfig + var remoteTargetConfig *capabilities.RemoteTargetConfig + + switch cconf.GetRemoteConfig().(type) { + case *capabilitiespb.CapabilityConfig_RemoteTriggerConfig: + prtc := cconf.GetRemoteTriggerConfig() + remoteTriggerConfig = &capabilities.RemoteTriggerConfig{} + remoteTriggerConfig.RegistrationRefresh = prtc.RegistrationRefresh.AsDuration() + remoteTriggerConfig.RegistrationExpiry = prtc.RegistrationExpiry.AsDuration() + remoteTriggerConfig.MinResponsesToAggregate = prtc.MinResponsesToAggregate + remoteTriggerConfig.MessageExpiry = prtc.MessageExpiry.AsDuration() + case *capabilitiespb.CapabilityConfig_RemoteTargetConfig: + prtc := cconf.GetRemoteTargetConfig() + remoteTargetConfig = &capabilities.RemoteTargetConfig{} + remoteTargetConfig.RequestHashExcludedAttributes = prtc.RequestHashExcludedAttributes + } + + dc, err := values.FromMapValueProto(cconf.DefaultConfig) + if err != nil { + return capabilities.CapabilityConfiguration{}, err + } + + return capabilities.CapabilityConfiguration{ + DefaultConfig: dc, + RemoteTriggerConfig: remoteTriggerConfig, + RemoteTargetConfig: remoteTargetConfig, + }, nil +} + func NewLauncher( lggr logger.Logger, peerWrapper p2ptypes.PeerWrapper, @@ -196,6 +236,11 @@ func (w *launcher) addRemoteCapabilities(ctx context.Context, myDON registrysync return fmt.Errorf("could not find capability matching id %s", cid) } + capabilityConfig, err := unmarshalCapabilityConfig(c.Config) + if err != nil { + return fmt.Errorf("could not unmarshal capability config for id %s", cid) + } + switch capability.CapabilityType { case capabilities.CapabilityTypeTrigger: newTriggerFn := func(info capabilities.CapabilityInfo) (capabilityService, error) { @@ -224,7 +269,7 @@ func (w *launcher) addRemoteCapabilities(ctx context.Context, myDON registrysync // When this is solved, we can move to a generic aggregator // and remove this. triggerCap := remote.NewTriggerSubscriber( - c.RemoteTriggerConfig, + capabilityConfig.RemoteTriggerConfig, info, remoteDON.DON, myDON.DON, @@ -333,11 +378,16 @@ func (w *launcher) exposeCapabilities(ctx context.Context, myPeerID p2ptypes.Pee return fmt.Errorf("could not find capability matching id %s", cid) } + capabilityConfig, err := unmarshalCapabilityConfig(c.Config) + if err != nil { + return fmt.Errorf("could not unmarshal capability config for id %s", cid) + } + switch capability.CapabilityType { case capabilities.CapabilityTypeTrigger: newTriggerPublisher := func(capability capabilities.BaseCapability, info capabilities.CapabilityInfo) (receiverService, error) { publisher := remote.NewTriggerPublisher( - c.RemoteTriggerConfig, + capabilityConfig.RemoteTriggerConfig, capability.(capabilities.TriggerCapability), info, don.DON, @@ -359,7 +409,7 @@ func (w *launcher) exposeCapabilities(ctx context.Context, myPeerID p2ptypes.Pee case capabilities.CapabilityTypeTarget: newTargetServer := func(capability capabilities.BaseCapability, info capabilities.CapabilityInfo) (receiverService, error) { return target.NewServer( - c.RemoteTargetConfig, + capabilityConfig.RemoteTargetConfig, myPeerID, capability.(capabilities.TargetCapability), info, diff --git a/core/capabilities/launcher_test.go b/core/capabilities/launcher_test.go index 82b03edcecb..8bca3be0db1 100644 --- a/core/capabilities/launcher_test.go +++ b/core/capabilities/launcher_test.go @@ -4,14 +4,18 @@ import ( "context" "crypto/rand" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/durationpb" ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote" remoteMocks "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/types/mocks" @@ -121,7 +125,7 @@ func TestLauncher_WiresUpExternalCapabilities(t *testing.T) { AcceptsWorkflows: true, Members: nodes, }, - CapabilityConfigurations: map[string]capabilities.CapabilityConfiguration{ + CapabilityConfigurations: map[string]registrysyncer.CapabilityConfiguration{ fullTriggerCapID: {}, fullTargetID: {}, }, @@ -223,7 +227,7 @@ func TestSyncer_IgnoresCapabilitiesForPrivateDON(t *testing.T) { AcceptsWorkflows: true, Members: nodes, }, - CapabilityConfigurations: map[string]capabilities.CapabilityConfiguration{ + CapabilityConfigurations: map[string]registrysyncer.CapabilityConfiguration{ triggerID: {}, targetID: {}, }, @@ -326,6 +330,15 @@ func TestLauncher_WiresUpClientsForPublicWorkflowDON(t *testing.T) { rtc := &capabilities.RemoteTriggerConfig{} rtc.ApplyDefaults() + cfg, err := proto.Marshal(&capabilitiespb.CapabilityConfig{ + RemoteConfig: &capabilitiespb.CapabilityConfig_RemoteTriggerConfig{ + RemoteTriggerConfig: &capabilitiespb.RemoteTriggerConfig{ + RegistrationRefresh: durationpb.New(1 * time.Second), + }, + }, + }) + require.NoError(t, err) + state := ®istrysyncer.LocalRegistry{ IDsToDONs: map[registrysyncer.DonID]registrysyncer.DON{ registrysyncer.DonID(dID): { @@ -347,12 +360,12 @@ func TestLauncher_WiresUpClientsForPublicWorkflowDON(t *testing.T) { AcceptsWorkflows: false, Members: capabilityDonNodes, }, - CapabilityConfigurations: map[string]capabilities.CapabilityConfiguration{ + CapabilityConfigurations: map[string]registrysyncer.CapabilityConfiguration{ fullTriggerCapID: { - RemoteTriggerConfig: rtc, + Config: cfg, }, fullTargetID: { - RemoteTriggerConfig: rtc, + Config: cfg, }, }, }, @@ -496,7 +509,7 @@ func TestLauncher_WiresUpClientsForPublicWorkflowDONButIgnoresPrivateCapabilitie AcceptsWorkflows: false, Members: capabilityDonNodes, }, - CapabilityConfigurations: map[string]capabilities.CapabilityConfiguration{ + CapabilityConfigurations: map[string]registrysyncer.CapabilityConfiguration{ fullTriggerCapID: {}, }, }, @@ -509,7 +522,7 @@ func TestLauncher_WiresUpClientsForPublicWorkflowDONButIgnoresPrivateCapabilitie AcceptsWorkflows: false, Members: capabilityDonNodes, }, - CapabilityConfigurations: map[string]capabilities.CapabilityConfiguration{ + CapabilityConfigurations: map[string]registrysyncer.CapabilityConfiguration{ fullTargetID: {}, }, }, @@ -653,7 +666,7 @@ func TestLauncher_SucceedsEvenIfDispatcherAlreadyHasReceiver(t *testing.T) { AcceptsWorkflows: false, Members: capabilityDonNodes, }, - CapabilityConfigurations: map[string]capabilities.CapabilityConfiguration{ + CapabilityConfigurations: map[string]registrysyncer.CapabilityConfiguration{ fullTriggerCapID: {}, }, }, diff --git a/core/capabilities/registry.go b/core/capabilities/registry.go index d6891c81ab9..4da51a27b6b 100644 --- a/core/capabilities/registry.go +++ b/core/capabilities/registry.go @@ -8,6 +8,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/registrysyncer" ) var ( @@ -16,7 +17,7 @@ var ( type metadataRegistry interface { LocalNode(ctx context.Context) (capabilities.Node, error) - ConfigForCapability(ctx context.Context, capabilityID string, donID uint32) (capabilities.CapabilityConfiguration, error) + ConfigForCapability(ctx context.Context, capabilityID string, donID uint32) (registrysyncer.CapabilityConfiguration, error) } // Registry is a struct for the registry of capabilities. @@ -43,7 +44,12 @@ func (r *Registry) ConfigForCapability(ctx context.Context, capabilityID string, return capabilities.CapabilityConfiguration{}, errors.New("metadataRegistry information not available") } - return r.metadataRegistry.ConfigForCapability(ctx, capabilityID, donID) + cfc, err := r.metadataRegistry.ConfigForCapability(ctx, capabilityID, donID) + if err != nil { + return capabilities.CapabilityConfiguration{}, err + } + + return unmarshalCapabilityConfig(cfc.Config) } // SetLocalRegistry sets a local copy of the offchain registry for the registry to use. diff --git a/core/chains/evm/client/simulated_backend_client.go b/core/chains/evm/client/simulated_backend_client.go index 6bcc1f36960..7dfd39f444c 100644 --- a/core/chains/evm/client/simulated_backend_client.go +++ b/core/chains/evm/client/simulated_backend_client.go @@ -360,9 +360,18 @@ func (c *SimulatedBackendClient) SendTransactionReturnCode(ctx context.Context, // SendTransaction sends a transaction. func (c *SimulatedBackendClient) SendTransaction(ctx context.Context, tx *types.Transaction) error { - sender, err := types.Sender(types.NewLondonSigner(c.chainId), tx) + var ( + sender common.Address + err error + ) + // try to recover the sender from the transaction using the configured chain id + // first. if that fails, try again with the simulated chain id (1337) + sender, err = types.Sender(types.NewLondonSigner(c.chainId), tx) if err != nil { - logger.Test(c.t).Panic(fmt.Errorf("invalid transaction: %v (tx: %#v)", err, tx)) + sender, err = types.Sender(types.NewLondonSigner(big.NewInt(1337)), tx) + if err != nil { + logger.Test(c.t).Panic(fmt.Errorf("invalid transaction: %v (tx: %#v)", err, tx)) + } } pendingNonce, err := c.b.PendingNonceAt(ctx, sender) if err != nil { diff --git a/core/scripts/go.mod b/core/scripts/go.mod index 94504897ab0..cd136127431 100644 --- a/core/scripts/go.mod +++ b/core/scripts/go.mod @@ -102,7 +102,7 @@ require ( github.com/danieljoos/wincred v1.1.2 // indirect github.com/danielkov/gin-helmet v0.0.0-20171108135313-1387e224435e // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/deckarep/golang-set/v2 v2.3.0 // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/dfuse-io/logging v0.0.0-20210109005628-b97a57253f70 // indirect github.com/dgraph-io/badger/v2 v2.2007.4 // indirect @@ -270,6 +270,7 @@ require ( github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/shirou/gopsutil/v3 v3.24.3 // indirect github.com/smartcontractkit/chain-selectors v1.0.10 // indirect + github.com/smartcontractkit/chainlink-ccip v0.0.0-20240806144315-04ac101e9c95 // indirect github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240710121324-3ed288aa9b45 // indirect github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240801131703-fd75761c982f // indirect github.com/smartcontractkit/chainlink-feeds v0.0.0-20240710170203-5b41615da827 // indirect @@ -330,7 +331,7 @@ require ( go.uber.org/zap v1.27.0 // indirect golang.org/x/arch v0.8.0 // indirect golang.org/x/crypto v0.25.0 // indirect - golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/mod v0.19.0 // indirect golang.org/x/net v0.27.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect diff --git a/core/scripts/go.sum b/core/scripts/go.sum index f770498cff8..d8ca90e8b47 100644 --- a/core/scripts/go.sum +++ b/core/scripts/go.sum @@ -330,8 +330,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/deckarep/golang-set/v2 v2.3.0 h1:qs18EKUfHm2X9fA50Mr/M5hccg2tNnVqsiBImnyDs0g= -github.com/deckarep/golang-set/v2 v2.3.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= @@ -1184,6 +1184,8 @@ github.com/smartcontractkit/chain-selectors v1.0.10 h1:t9kJeE6B6G+hKD0GYR4kGJSCq github.com/smartcontractkit/chain-selectors v1.0.10/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= github.com/smartcontractkit/chainlink-automation v1.0.4 h1:iyW181JjKHLNMnDleI8umfIfVVlwC7+n5izbLSFgjw8= github.com/smartcontractkit/chainlink-automation v1.0.4/go.mod h1:u4NbPZKJ5XiayfKHD/v3z3iflQWqvtdhj13jVZXj/cM= +github.com/smartcontractkit/chainlink-ccip v0.0.0-20240806144315-04ac101e9c95 h1:LAgJTg9Yr/uCo2g7Krp88Dco2U45Y6sbJVl8uKoLkys= +github.com/smartcontractkit/chainlink-ccip v0.0.0-20240806144315-04ac101e9c95/go.mod h1:/ZWraCBaDDgaIN1prixYcbVvIk/6HeED9+8zbWQ+TMo= github.com/smartcontractkit/chainlink-common v0.2.2-0.20240805160614-501c4f40b98c h1:3apUsez/6Pkp1ckXzSwIhzPRuWjDGjzMjKapEKi0Fcw= github.com/smartcontractkit/chainlink-common v0.2.2-0.20240805160614-501c4f40b98c/go.mod h1:Jg1sCTsbxg76YByI8ifpFby3FvVqISStHT8ypy9ocmY= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240710121324-3ed288aa9b45 h1:NBQLtqk8zsyY4qTJs+NElI3aDFTcAo83JHvqD04EvB0= @@ -1484,8 +1486,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 h1:wDLEX9a7YQoKdKNQt88rtydkqDxeGaBUTnIYc3iG/mA= -golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/core/services/chainlink/application.go b/core/services/chainlink/application.go index c23ec08a692..6a381b1ffa8 100644 --- a/core/services/chainlink/application.go +++ b/core/services/chainlink/application.go @@ -23,15 +23,14 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/utils" "github.com/smartcontractkit/chainlink-common/pkg/utils/jsonserializable" "github.com/smartcontractkit/chainlink-common/pkg/utils/mailbox" - "github.com/smartcontractkit/chainlink/v2/core/capabilities" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" - "github.com/smartcontractkit/chainlink/v2/core/services/standardcapabilities" - "github.com/smartcontractkit/chainlink/v2/core/static" "github.com/smartcontractkit/chainlink/v2/core/bridges" "github.com/smartcontractkit/chainlink/v2/core/build" + "github.com/smartcontractkit/chainlink/v2/core/capabilities" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip" "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote" remotetypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/types" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr" evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" evmutils "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" @@ -61,6 +60,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/registrysyncer" "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/mercury" "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/mercury/wsrpc" + "github.com/smartcontractkit/chainlink/v2/core/services/standardcapabilities" "github.com/smartcontractkit/chainlink/v2/core/services/streams" "github.com/smartcontractkit/chainlink/v2/core/services/telemetry" "github.com/smartcontractkit/chainlink/v2/core/services/vrf" @@ -70,6 +70,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/sessions" "github.com/smartcontractkit/chainlink/v2/core/sessions/ldapauth" "github.com/smartcontractkit/chainlink/v2/core/sessions/localauth" + "github.com/smartcontractkit/chainlink/v2/core/static" "github.com/smartcontractkit/chainlink/v2/plugins" ) @@ -212,41 +213,51 @@ func NewApplication(opts ApplicationOpts) (Application, error) { externalPeer := externalp2p.NewExternalPeerWrapper(keyStore.P2P(), cfg.Capabilities().Peering(), opts.DS, globalLogger) signer := externalPeer externalPeerWrapper = externalPeer - dispatcher = remote.NewDispatcher(externalPeerWrapper, signer, opts.CapabilitiesRegistry, globalLogger) - srvcs = append(srvcs, externalPeerWrapper) // peer wrapper must be started before dispatcher - srvcs = append(srvcs, dispatcher) - } else { // tests only + remoteDispatcher := remote.NewDispatcher(externalPeerWrapper, signer, opts.CapabilitiesRegistry, globalLogger) + srvcs = append(srvcs, remoteDispatcher) + + dispatcher = remoteDispatcher + } else { dispatcher = opts.CapabilitiesDispatcher externalPeerWrapper = opts.CapabilitiesPeerWrapper - srvcs = append(srvcs, externalPeerWrapper) } - rid := cfg.Capabilities().ExternalRegistry().RelayID() - registryAddress := cfg.Capabilities().ExternalRegistry().Address() - relayer, err := relayerChainInterops.Get(rid) - if err != nil { - return nil, fmt.Errorf("could not fetch relayer %s configured for capabilities registry: %w", rid, err) - } + srvcs = append(srvcs, externalPeerWrapper, dispatcher) - registrySyncer, err := registrysyncer.New( - globalLogger, - externalPeerWrapper, - relayer, - registryAddress, - ) - if err != nil { - return nil, fmt.Errorf("could not configure syncer: %w", err) - } + if cfg.Capabilities().ExternalRegistry().Address() != "" { + rid := cfg.Capabilities().ExternalRegistry().RelayID() + registryAddress := cfg.Capabilities().ExternalRegistry().Address() + relayer, err := relayerChainInterops.Get(rid) + if err != nil { + return nil, fmt.Errorf("could not fetch relayer %s configured for capabilities registry: %w", rid, err) + } + registrySyncer, err := registrysyncer.New( + globalLogger, + func() (p2ptypes.PeerID, error) { + p := externalPeerWrapper.GetPeer() + if p == nil { + return p2ptypes.PeerID{}, errors.New("could not get peer") + } - wfLauncher := capabilities.NewLauncher( - globalLogger, - externalPeerWrapper, - dispatcher, - opts.CapabilitiesRegistry, - ) - registrySyncer.AddLauncher(wfLauncher) + return p.ID(), nil + }, + relayer, + registryAddress, + ) + if err != nil { + return nil, fmt.Errorf("could not configure syncer: %w", err) + } + + wfLauncher := capabilities.NewLauncher( + globalLogger, + externalPeerWrapper, + dispatcher, + opts.CapabilitiesRegistry, + ) + registrySyncer.AddLauncher(wfLauncher) - srvcs = append(srvcs, dispatcher, wfLauncher, registrySyncer) + srvcs = append(srvcs, wfLauncher, registrySyncer) + } } // LOOPs can be created as options, in the case of LOOP relayers, or @@ -512,6 +523,18 @@ func NewApplication(opts ApplicationOpts) (Application, error) { cfg.Insecure(), opts.RelayerChainInteroperators, ) + delegates[job.CCIP] = ccip.NewDelegate( + globalLogger, + loopRegistrarConfig, + pipelineRunner, + opts.RelayerChainInteroperators.LegacyEVMChains(), + relayerChainInterops, + opts.KeyStore, + opts.DS, + peerWrapper, + telemetryManager, + cfg.Capabilities(), + ) } else { globalLogger.Debug("Off-chain reporting v2 disabled") } diff --git a/core/services/job/models.go b/core/services/job/models.go index 2f864efe300..1c46d08c59c 100644 --- a/core/services/job/models.go +++ b/core/services/job/models.go @@ -38,6 +38,7 @@ const ( BlockhashStore Type = (Type)(pipeline.BlockhashStoreJobType) Bootstrap Type = (Type)(pipeline.BootstrapJobType) Cron Type = (Type)(pipeline.CronJobType) + CCIP Type = (Type)(pipeline.CCIPJobType) DirectRequest Type = (Type)(pipeline.DirectRequestJobType) FluxMonitor Type = (Type)(pipeline.FluxMonitorJobType) Gateway Type = (Type)(pipeline.GatewayJobType) @@ -78,6 +79,7 @@ var ( BlockhashStore: false, Bootstrap: false, Cron: true, + CCIP: false, DirectRequest: true, FluxMonitor: true, Gateway: false, @@ -97,6 +99,7 @@ var ( BlockhashStore: false, Bootstrap: false, Cron: true, + CCIP: false, DirectRequest: true, FluxMonitor: false, Gateway: false, @@ -116,6 +119,7 @@ var ( BlockhashStore: 1, Bootstrap: 1, Cron: 1, + CCIP: 1, DirectRequest: 1, FluxMonitor: 1, Gateway: 1, @@ -176,6 +180,7 @@ type Job struct { StandardCapabilitiesSpecID *int32 StandardCapabilitiesSpec *StandardCapabilitiesSpec CCIPSpecID *int32 + CCIPSpec *CCIPSpec CCIPBootstrapSpecID *int32 JobSpecErrors []SpecError Type Type `toml:"type"` @@ -910,3 +915,48 @@ func (w *StandardCapabilitiesSpec) SetID(value string) error { w.ID = int32(ID) return nil } + +type CCIPSpec struct { + ID int32 + CreatedAt time.Time `toml:"-"` + UpdatedAt time.Time `toml:"-"` + + // P2PV2Bootstrappers is a list of "peer_id@ip_address:port" strings that are used to + // identify the bootstrap nodes of the P2P network. + // These bootstrappers will be used to bootstrap all CCIP DONs. + P2PV2Bootstrappers pq.StringArray `toml:"p2pV2Bootstrappers" db:"p2pv2_bootstrappers"` + + // CapabilityVersion is the semantic version of the CCIP capability. + // This capability version must exist in the onchain capability registry. + CapabilityVersion string `toml:"capabilityVersion" db:"capability_version"` + + // CapabilityLabelledName is the labelled name of the CCIP capability. + // Corresponds to the labelled name of the capability in the onchain capability registry. + CapabilityLabelledName string `toml:"capabilityLabelledName" db:"capability_labelled_name"` + + // OCRKeyBundleIDs is a mapping from chain type to OCR key bundle ID. + // These are explicitly specified here so that we don't run into strange errors auto-detecting + // the valid bundle, since nops can create as many bundles as they want. + // This will most likely never change for a particular CCIP capability version, + // since new chain families will likely require a new capability version. + // {"evm": "evm_key_bundle_id", "solana": "solana_key_bundle_id", ... } + OCRKeyBundleIDs JSONConfig `toml:"ocrKeyBundleIDs" db:"ocr_key_bundle_ids"` + + // RelayConfigs consists of relay specific configuration. + // Chain reader configurations are stored here, and are defined on a chain family basis, e.g + // we will have one chain reader config for EVM, one for solana, starknet, etc. + // Chain writer configurations are also stored here, and are also defined on a chain family basis, + // e.g we will have one chain writer config for EVM, one for solana, starknet, etc. + // See tests for examples of relay configs in TOML. + // { "evm": {"chainReader": {...}, "chainWriter": {...}}, "solana": {...}, ... } + // see core/services/relay/evm/types/types.go for EVM configs. + RelayConfigs JSONConfig `toml:"relayConfigs" db:"relay_configs"` + + // P2PKeyID is the ID of the P2P key of the node. + // This must be present in the capability registry otherwise the job will not start correctly. + P2PKeyID string `toml:"p2pKeyID" db:"p2p_key_id"` + + // PluginConfig contains plugin-specific config, like token price pipelines + // and RMN network info for offchain blessing. + PluginConfig JSONConfig `toml:"pluginConfig"` +} diff --git a/core/services/job/orm.go b/core/services/job/orm.go index d13decc7208..ac3bb655306 100644 --- a/core/services/job/orm.go +++ b/core/services/job/orm.go @@ -425,7 +425,34 @@ func (o *orm) CreateJob(ctx context.Context, jb *Job) error { return errors.Wrap(err, "failed to create StandardCapabilities for jobSpec") } jb.StandardCapabilitiesSpecID = &specID - + case CCIP: + sql := `INSERT INTO ccip_specs ( + capability_version, + capability_labelled_name, + ocr_key_bundle_ids, + p2p_key_id, + p2pv2_bootstrappers, + relay_configs, + plugin_config, + created_at, + updated_at + ) VALUES ( + :capability_version, + :capability_labelled_name, + :ocr_key_bundle_ids, + :p2p_key_id, + :p2pv2_bootstrappers, + :relay_configs, + :plugin_config, + NOW(), + NOW() + ) + RETURNING id;` + specID, err := tx.prepareQuerySpecID(ctx, sql, jb.CCIPSpec) + if err != nil { + return errors.Wrap(err, "failed to create CCIPSpec for jobSpec") + } + jb.CCIPSpecID = &specID default: o.lggr.Panicf("Unsupported jb.Type: %v", jb.Type) } @@ -643,19 +670,19 @@ func (o *orm) InsertJob(ctx context.Context, job *Job) error { // if job has id, emplace otherwise insert with a new id. if job.ID == 0 { query = `INSERT INTO jobs (name, stream_id, schema_version, type, max_task_duration, ocr_oracle_spec_id, ocr2_oracle_spec_id, direct_request_spec_id, flux_monitor_spec_id, - keeper_spec_id, cron_spec_id, vrf_spec_id, webhook_spec_id, blockhash_store_spec_id, bootstrap_spec_id, block_header_feeder_spec_id, gateway_spec_id, - legacy_gas_station_server_spec_id, legacy_gas_station_sidecar_spec_id, workflow_spec_id, standard_capabilities_spec_id, external_job_id, gas_limit, forwarding_allowed, created_at) + keeper_spec_id, cron_spec_id, vrf_spec_id, webhook_spec_id, blockhash_store_spec_id, bootstrap_spec_id, block_header_feeder_spec_id, gateway_spec_id, + legacy_gas_station_server_spec_id, legacy_gas_station_sidecar_spec_id, workflow_spec_id, standard_capabilities_spec_id, ccip_spec_id, external_job_id, gas_limit, forwarding_allowed, created_at) VALUES (:name, :stream_id, :schema_version, :type, :max_task_duration, :ocr_oracle_spec_id, :ocr2_oracle_spec_id, :direct_request_spec_id, :flux_monitor_spec_id, - :keeper_spec_id, :cron_spec_id, :vrf_spec_id, :webhook_spec_id, :blockhash_store_spec_id, :bootstrap_spec_id, :block_header_feeder_spec_id, :gateway_spec_id, - :legacy_gas_station_server_spec_id, :legacy_gas_station_sidecar_spec_id, :workflow_spec_id, :standard_capabilities_spec_id, :external_job_id, :gas_limit, :forwarding_allowed, NOW()) + :keeper_spec_id, :cron_spec_id, :vrf_spec_id, :webhook_spec_id, :blockhash_store_spec_id, :bootstrap_spec_id, :block_header_feeder_spec_id, :gateway_spec_id, + :legacy_gas_station_server_spec_id, :legacy_gas_station_sidecar_spec_id, :workflow_spec_id, :standard_capabilities_spec_id, :ccip_spec_id, :external_job_id, :gas_limit, :forwarding_allowed, NOW()) RETURNING *;` } else { query = `INSERT INTO jobs (id, name, stream_id, schema_version, type, max_task_duration, ocr_oracle_spec_id, ocr2_oracle_spec_id, direct_request_spec_id, flux_monitor_spec_id, - keeper_spec_id, cron_spec_id, vrf_spec_id, webhook_spec_id, blockhash_store_spec_id, bootstrap_spec_id, block_header_feeder_spec_id, gateway_spec_id, - legacy_gas_station_server_spec_id, legacy_gas_station_sidecar_spec_id, workflow_spec_id, standard_capabilities_spec_id, external_job_id, gas_limit, forwarding_allowed, created_at) + keeper_spec_id, cron_spec_id, vrf_spec_id, webhook_spec_id, blockhash_store_spec_id, bootstrap_spec_id, block_header_feeder_spec_id, gateway_spec_id, + legacy_gas_station_server_spec_id, legacy_gas_station_sidecar_spec_id, workflow_spec_id, standard_capabilities_spec_id, ccip_spec_id, external_job_id, gas_limit, forwarding_allowed, created_at) VALUES (:id, :name, :stream_id, :schema_version, :type, :max_task_duration, :ocr_oracle_spec_id, :ocr2_oracle_spec_id, :direct_request_spec_id, :flux_monitor_spec_id, - :keeper_spec_id, :cron_spec_id, :vrf_spec_id, :webhook_spec_id, :blockhash_store_spec_id, :bootstrap_spec_id, :block_header_feeder_spec_id, :gateway_spec_id, - :legacy_gas_station_server_spec_id, :legacy_gas_station_sidecar_spec_id, :workflow_spec_id, :standard_capabilities_spec_id, :external_job_id, :gas_limit, :forwarding_allowed, NOW()) + :keeper_spec_id, :cron_spec_id, :vrf_spec_id, :webhook_spec_id, :blockhash_store_spec_id, :bootstrap_spec_id, :block_header_feeder_spec_id, :gateway_spec_id, + :legacy_gas_station_server_spec_id, :legacy_gas_station_sidecar_spec_id, :workflow_spec_id, :standard_capabilities_spec_id, :ccip_spec_id, :external_job_id, :gas_limit, :forwarding_allowed, NOW()) RETURNING *;` } query, args, err := tx.ds.BindNamed(query, job) @@ -699,7 +726,8 @@ func (o *orm) DeleteJob(ctx context.Context, id int32) error { block_header_feeder_spec_id, gateway_spec_id, workflow_spec_id, - standard_capabilities_spec_id + standard_capabilities_spec_id, + ccip_spec_id ), deleted_oracle_specs AS ( DELETE FROM ocr_oracle_specs WHERE id IN (SELECT ocr_oracle_spec_id FROM deleted_jobs) @@ -742,7 +770,10 @@ func (o *orm) DeleteJob(ctx context.Context, id int32) error { ), deleted_standardcapabilities_specs AS ( DELETE FROM standardcapabilities_specs WHERE id in (SELECT standard_capabilities_spec_id FROM deleted_jobs) - ), + ), + deleted_ccip_specs AS ( + DELETE FROM ccip_specs WHERE id in (SELECT ccip_spec_id FROM deleted_jobs) + ), deleted_job_pipeline_specs AS ( DELETE FROM job_pipeline_specs WHERE job_id IN (SELECT id FROM deleted_jobs) RETURNING pipeline_spec_id ) @@ -816,7 +847,7 @@ func (o *orm) FindJobs(ctx context.Context, offset, limit int) (jobs []Job, coun return fmt.Errorf("failed to query jobs count: %w", err) } - sql = `SELECT jobs.*, job_pipeline_specs.pipeline_spec_id as pipeline_spec_id + sql = `SELECT jobs.*, job_pipeline_specs.pipeline_spec_id as pipeline_spec_id FROM jobs JOIN job_pipeline_specs ON (jobs.id = job_pipeline_specs.job_id) ORDER BY jobs.created_at DESC, jobs.id DESC OFFSET $1 LIMIT $2;` @@ -1030,8 +1061,8 @@ func (o *orm) findJob(ctx context.Context, jb *Job, col string, arg interface{}) } func (o *orm) FindJobIDsWithBridge(ctx context.Context, name string) (jids []int32, err error) { - query := `SELECT - jobs.id, pipeline_specs.dot_dag_source + query := `SELECT + jobs.id, pipeline_specs.dot_dag_source FROM jobs JOIN job_pipeline_specs ON job_pipeline_specs.job_id = jobs.id JOIN pipeline_specs ON pipeline_specs.id = job_pipeline_specs.pipeline_spec_id @@ -1078,7 +1109,7 @@ func (o *orm) FindJobIDsWithBridge(ctx context.Context, name string) (jids []int func (o *orm) FindJobIDByWorkflow(ctx context.Context, spec WorkflowSpec) (jobID int32, err error) { stmt := ` SELECT jobs.id FROM jobs -INNER JOIN workflow_specs ws on jobs.workflow_spec_id = ws.id AND ws.workflow_owner = $1 AND ws.workflow_name = $2 +INNER JOIN workflow_specs ws on jobs.workflow_spec_id = ws.id AND ws.workflow_owner = $1 AND ws.workflow_name = $2 ` err = o.ds.GetContext(ctx, &jobID, stmt, spec.WorkflowOwner, spec.WorkflowName) if err != nil { @@ -1391,6 +1422,7 @@ func (o *orm) loadAllJobTypes(ctx context.Context, job *Job) error { o.loadJobType(ctx, job, "GatewaySpec", "gateway_specs", job.GatewaySpecID), o.loadJobType(ctx, job, "WorkflowSpec", "workflow_specs", job.WorkflowSpecID), o.loadJobType(ctx, job, "StandardCapabilitiesSpec", "standardcapabilities_specs", job.StandardCapabilitiesSpecID), + o.loadJobType(ctx, job, "CCIPSpec", "ccip_specs", job.CCIPSpecID), ) } @@ -1428,7 +1460,7 @@ func (o *orm) loadJobPipelineSpec(ctx context.Context, job *Job, id *int32) erro ctx, pipelineSpecRow, `SELECT pipeline_specs.*, job_pipeline_specs.job_id as job_id - FROM pipeline_specs + FROM pipeline_specs JOIN job_pipeline_specs ON(pipeline_specs.id = job_pipeline_specs.pipeline_spec_id) WHERE job_pipeline_specs.job_id = $1 AND job_pipeline_specs.pipeline_spec_id = $2`, job.ID, *id, diff --git a/core/services/pipeline/common.go b/core/services/pipeline/common.go index 763e50546fd..1b36c8a664b 100644 --- a/core/services/pipeline/common.go +++ b/core/services/pipeline/common.go @@ -29,6 +29,7 @@ const ( BlockhashStoreJobType string = "blockhashstore" BootstrapJobType string = "bootstrap" CronJobType string = "cron" + CCIPJobType string = "ccip" DirectRequestJobType string = "directrequest" FluxMonitorJobType string = "fluxmonitor" GatewayJobType string = "gateway" diff --git a/core/services/registrysyncer/local_registry.go b/core/services/registrysyncer/local_registry.go index 4e4a632bf87..8a0e471ccda 100644 --- a/core/services/registrysyncer/local_registry.go +++ b/core/services/registrysyncer/local_registry.go @@ -16,7 +16,11 @@ type DonID uint32 type DON struct { capabilities.DON - CapabilityConfigurations map[string]capabilities.CapabilityConfiguration + CapabilityConfigurations map[string]CapabilityConfiguration +} + +type CapabilityConfiguration struct { + Config []byte } type Capability struct { @@ -26,7 +30,7 @@ type Capability struct { type LocalRegistry struct { lggr logger.Logger - peerWrapper p2ptypes.PeerWrapper + getPeerID func() (p2ptypes.PeerID, error) IDsToDONs map[DonID]DON IDsToNodes map[p2ptypes.PeerID]kcr.CapabilitiesRegistryNodeInfo IDsToCapabilities map[string]Capability @@ -36,12 +40,11 @@ func (l *LocalRegistry) LocalNode(ctx context.Context) (capabilities.Node, error // Load the current nodes PeerWrapper, this gets us the current node's // PeerID, allowing us to contextualize registry information in terms of DON ownership // (eg. get my current DON configuration, etc). - if l.peerWrapper.GetPeer() == nil { + pid, err := l.getPeerID() + if err != nil { return capabilities.Node{}, errors.New("unable to get local node: peerWrapper hasn't started yet") } - pid := l.peerWrapper.GetPeer().ID() - var workflowDON capabilities.DON capabilityDONs := []capabilities.DON{} for _, d := range l.IDsToDONs { @@ -70,15 +73,15 @@ func (l *LocalRegistry) LocalNode(ctx context.Context) (capabilities.Node, error }, nil } -func (l *LocalRegistry) ConfigForCapability(ctx context.Context, capabilityID string, donID uint32) (capabilities.CapabilityConfiguration, error) { +func (l *LocalRegistry) ConfigForCapability(ctx context.Context, capabilityID string, donID uint32) (CapabilityConfiguration, error) { d, ok := l.IDsToDONs[DonID(donID)] if !ok { - return capabilities.CapabilityConfiguration{}, fmt.Errorf("could not find don %d", donID) + return CapabilityConfiguration{}, fmt.Errorf("could not find don %d", donID) } cc, ok := d.CapabilityConfigurations[capabilityID] if !ok { - return capabilities.CapabilityConfiguration{}, fmt.Errorf("could not find capability configuration for capability %s and donID %d", capabilityID, donID) + return CapabilityConfiguration{}, fmt.Errorf("could not find capability configuration for capability %s and donID %d", capabilityID, donID) } return cc, nil diff --git a/core/services/registrysyncer/syncer.go b/core/services/registrysyncer/syncer.go index 9675d86dc86..83f77e46d35 100644 --- a/core/services/registrysyncer/syncer.go +++ b/core/services/registrysyncer/syncer.go @@ -7,14 +7,10 @@ import ( "sync" "time" - "google.golang.org/protobuf/proto" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives" - "github.com/smartcontractkit/chainlink-common/pkg/values" kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" "github.com/smartcontractkit/chainlink/v2/core/logger" @@ -39,7 +35,7 @@ type registrySyncer struct { initReader func(ctx context.Context, lggr logger.Logger, relayer contractReaderFactory, registryAddress string) (types.ContractReader, error) relayer contractReaderFactory registryAddress string - peerWrapper p2ptypes.PeerWrapper + getPeerID func() (p2ptypes.PeerID, error) wg sync.WaitGroup lggr logger.Logger @@ -55,7 +51,7 @@ var ( // New instantiates a new RegistrySyncer func New( lggr logger.Logger, - peerWrapper p2ptypes.PeerWrapper, + getPeerID func() (p2ptypes.PeerID, error), relayer contractReaderFactory, registryAddress string, ) (*registrySyncer, error) { @@ -66,7 +62,7 @@ func New( relayer: relayer, registryAddress: registryAddress, initReader: newReader, - peerWrapper: peerWrapper, + getPeerID: getPeerID, }, nil } @@ -158,42 +154,6 @@ func (s *registrySyncer) syncLoop() { } } -func unmarshalCapabilityConfig(data []byte) (capabilities.CapabilityConfiguration, error) { - cconf := &capabilitiespb.CapabilityConfig{} - err := proto.Unmarshal(data, cconf) - if err != nil { - return capabilities.CapabilityConfiguration{}, err - } - - var remoteTriggerConfig *capabilities.RemoteTriggerConfig - var remoteTargetConfig *capabilities.RemoteTargetConfig - - switch cconf.GetRemoteConfig().(type) { - case *capabilitiespb.CapabilityConfig_RemoteTriggerConfig: - prtc := cconf.GetRemoteTriggerConfig() - remoteTriggerConfig = &capabilities.RemoteTriggerConfig{} - remoteTriggerConfig.RegistrationRefresh = prtc.RegistrationRefresh.AsDuration() - remoteTriggerConfig.RegistrationExpiry = prtc.RegistrationExpiry.AsDuration() - remoteTriggerConfig.MinResponsesToAggregate = prtc.MinResponsesToAggregate - remoteTriggerConfig.MessageExpiry = prtc.MessageExpiry.AsDuration() - case *capabilitiespb.CapabilityConfig_RemoteTargetConfig: - prtc := cconf.GetRemoteTargetConfig() - remoteTargetConfig = &capabilities.RemoteTargetConfig{} - remoteTargetConfig.RequestHashExcludedAttributes = prtc.RequestHashExcludedAttributes - } - - dc, err := values.FromMapValueProto(cconf.DefaultConfig) - if err != nil { - return capabilities.CapabilityConfiguration{}, err - } - - return capabilities.CapabilityConfiguration{ - DefaultConfig: dc, - RemoteTriggerConfig: remoteTriggerConfig, - RemoteTargetConfig: remoteTargetConfig, - }, nil -} - func (s *registrySyncer) localRegistry(ctx context.Context) (*LocalRegistry, error) { caps := []kcr.CapabilitiesRegistryCapabilityInfo{} err := s.reader.GetLatestValue(ctx, "CapabilitiesRegistry", "getCapabilities", primitives.Unconfirmed, nil, &caps) @@ -221,19 +181,16 @@ func (s *registrySyncer) localRegistry(ctx context.Context) (*LocalRegistry, err idsToDONs := map[DonID]DON{} for _, d := range dons { - cc := map[string]capabilities.CapabilityConfiguration{} + cc := map[string]CapabilityConfiguration{} for _, dc := range d.CapabilityConfigurations { cid, ok := hashedIDsToCapabilityIDs[dc.CapabilityId] if !ok { return nil, fmt.Errorf("invariant violation: could not find full ID for hashed ID %s", dc.CapabilityId) } - cconf, innerErr := unmarshalCapabilityConfig(dc.Config) - if innerErr != nil { - return nil, innerErr + cc[cid] = CapabilityConfiguration{ + Config: dc.Config, } - - cc[cid] = cconf } idsToDONs[DonID(d.Id)] = DON{ @@ -255,7 +212,7 @@ func (s *registrySyncer) localRegistry(ctx context.Context) (*LocalRegistry, err return &LocalRegistry{ lggr: s.lggr, - peerWrapper: s.peerWrapper, + getPeerID: s.getPeerID, IDsToDONs: idsToDONs, IDsToCapabilities: idsToCapabilities, IDsToNodes: idsToNodes, diff --git a/core/services/registrysyncer/syncer_test.go b/core/services/registrysyncer/syncer_test.go index c13cc904909..cd8776d882c 100644 --- a/core/services/registrysyncer/syncer_test.go +++ b/core/services/registrysyncer/syncer_test.go @@ -33,7 +33,6 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" "github.com/smartcontractkit/chainlink/v2/core/logger" p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" - "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types/mocks" "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" evmrelaytypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" ) @@ -237,9 +236,8 @@ func TestReader_Integration(t *testing.T) { require.NoError(t, err) - wrapper := mocks.NewPeerWrapper(t) factory := newContractReaderFactory(t, sim) - syncer, err := New(logger.TestLogger(t), wrapper, factory, regAddress.Hex()) + syncer, err := New(logger.TestLogger(t), func() (p2ptypes.PeerID, error) { return p2ptypes.PeerID{}, nil }, factory, regAddress.Hex()) require.NoError(t, err) l := &launcher{} @@ -257,29 +255,17 @@ func TestReader_Integration(t *testing.T) { }, gotCap) assert.Len(t, s.IDsToDONs, 1) - rtc := &capabilities.RemoteTriggerConfig{ - RegistrationRefresh: 20 * time.Second, - MinResponsesToAggregate: 2, - RegistrationExpiry: 60 * time.Second, - MessageExpiry: 120 * time.Second, - } - expectedDON := DON{ - DON: capabilities.DON{ - ID: 1, - ConfigVersion: 1, - IsPublic: true, - AcceptsWorkflows: true, - F: 1, - Members: toPeerIDs(nodeSet), - }, - CapabilityConfigurations: map[string]capabilities.CapabilityConfiguration{ - cid: { - DefaultConfig: values.EmptyMap(), - RemoteTriggerConfig: rtc, - }, - }, + expectedDON := capabilities.DON{ + ID: 1, + ConfigVersion: 1, + IsPublic: true, + AcceptsWorkflows: true, + F: 1, + Members: toPeerIDs(nodeSet), } - assert.Equal(t, expectedDON, s.IDsToDONs[1]) + gotDon := s.IDsToDONs[1] + assert.Equal(t, expectedDON, gotDon.DON) + assert.Equal(t, configb, gotDon.CapabilityConfigurations[cid].Config) nodesInfo := []kcr.CapabilitiesRegistryNodeInfo{ { @@ -329,10 +315,6 @@ func TestSyncer_LocalNode(t *testing.T) { var pid p2ptypes.PeerID err := pid.UnmarshalText([]byte("12D3KooWBCF1XT5Wi8FzfgNCqRL76Swv8TRU3TiD4QiJm8NMNX7N")) require.NoError(t, err) - peer := mocks.NewPeer(t) - peer.On("ID").Return(pid) - wrapper := mocks.NewPeerWrapper(t) - wrapper.On("GetPeer").Return(peer) workflowDonNodes := []p2ptypes.PeerID{ pid, @@ -346,8 +328,8 @@ func TestSyncer_LocalNode(t *testing.T) { // which exposes the streams-trigger and write_chain capabilities. // We expect receivers to be wired up and both capabilities to be added to the registry. localRegistry := LocalRegistry{ - lggr: lggr, - peerWrapper: wrapper, + lggr: lggr, + getPeerID: func() (p2ptypes.PeerID, error) { return pid, nil }, IDsToDONs: map[DonID]DON{ DonID(dID): { DON: capabilities.DON{ diff --git a/core/services/synchronization/common.go b/core/services/synchronization/common.go index bfb9fba6de6..394830a76af 100644 --- a/core/services/synchronization/common.go +++ b/core/services/synchronization/common.go @@ -24,6 +24,10 @@ const ( OCR3Mercury TelemetryType = "ocr3-mercury" AutomationCustom TelemetryType = "automation-custom" OCR3Automation TelemetryType = "ocr3-automation" + OCR3Rebalancer TelemetryType = "ocr3-rebalancer" + OCR3CCIPCommit TelemetryType = "ocr3-ccip-commit" + OCR3CCIPExec TelemetryType = "ocr3-ccip-exec" + OCR3CCIPBootstrap TelemetryType = "ocr3-bootstrap" ) type TelemPayload struct { diff --git a/core/services/workflows/engine_test.go b/core/services/workflows/engine_test.go index 3af87284131..0a38bf719b2 100644 --- a/core/services/workflows/engine_test.go +++ b/core/services/workflows/engine_test.go @@ -11,8 +11,10 @@ import ( "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" "github.com/smartcontractkit/chainlink-common/pkg/services/servicetest" "github.com/smartcontractkit/chainlink-common/pkg/values" "github.com/smartcontractkit/chainlink-common/pkg/workflows" @@ -22,6 +24,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/job" p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" + "github.com/smartcontractkit/chainlink/v2/core/services/registrysyncer" "github.com/smartcontractkit/chainlink/v2/core/services/workflows/store" ) @@ -101,7 +104,7 @@ func newTestDBStore(t *testing.T, clock clockwork.Clock) store.Store { type testConfigProvider struct { localNode func(ctx context.Context) (capabilities.Node, error) - configForCapability func(ctx context.Context, capabilityID string, donID uint32) (capabilities.CapabilityConfiguration, error) + configForCapability func(ctx context.Context, capabilityID string, donID uint32) (registrysyncer.CapabilityConfiguration, error) } func (t testConfigProvider) LocalNode(ctx context.Context) (capabilities.Node, error) { @@ -118,12 +121,12 @@ func (t testConfigProvider) LocalNode(ctx context.Context) (capabilities.Node, e }, nil } -func (t testConfigProvider) ConfigForCapability(ctx context.Context, capabilityID string, donID uint32) (capabilities.CapabilityConfiguration, error) { +func (t testConfigProvider) ConfigForCapability(ctx context.Context, capabilityID string, donID uint32) (registrysyncer.CapabilityConfiguration, error) { if t.configForCapability != nil { return t.configForCapability(ctx, capabilityID, donID) } - return capabilities.CapabilityConfiguration{}, nil + return registrysyncer.CapabilityConfiguration{}, nil } // newTestEngine creates a new engine with some test defaults. @@ -1028,11 +1031,9 @@ func TestEngine_MergesWorkflowConfigAndCRConfig(t *testing.T) { simpleWorkflow, ) reg.SetLocalRegistry(testConfigProvider{ - configForCapability: func(ctx context.Context, capabilityID string, donID uint32) (capabilities.CapabilityConfiguration, error) { + configForCapability: func(ctx context.Context, capabilityID string, donID uint32) (registrysyncer.CapabilityConfiguration, error) { if capabilityID != writeID { - return capabilities.CapabilityConfiguration{ - DefaultConfig: values.EmptyMap(), - }, nil + return registrysyncer.CapabilityConfiguration{}, nil } cm, err := values.WrapMap(map[string]any{ @@ -1040,12 +1041,15 @@ func TestEngine_MergesWorkflowConfigAndCRConfig(t *testing.T) { "schedule": "allAtOnce", }) if err != nil { - return capabilities.CapabilityConfiguration{}, err + return registrysyncer.CapabilityConfiguration{}, err } - return capabilities.CapabilityConfiguration{ - DefaultConfig: cm, - }, nil + cb, err := proto.Marshal(&capabilitiespb.CapabilityConfig{ + DefaultConfig: values.ProtoMap(cm), + }) + return registrysyncer.CapabilityConfiguration{ + Config: cb, + }, err }, }) diff --git a/core/web/presenters/job.go b/core/web/presenters/job.go index ad6bf617a82..bb518650516 100644 --- a/core/web/presenters/job.go +++ b/core/web/presenters/job.go @@ -468,6 +468,26 @@ func NewStandardCapabilitiesSpec(spec *job.StandardCapabilitiesSpec) *StandardCa } } +type CCIPSpec struct { + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + CapabilityVersion string `json:"capabilityVersion"` + CapabilityLabelledName string `json:"capabilityLabelledName"` + OCRKeyBundleIDs map[string]interface{} `json:"ocrKeyBundleIDs"` + P2PKeyID string `json:"p2pKeyID"` +} + +func NewCCIPSpec(spec *job.CCIPSpec) *CCIPSpec { + return &CCIPSpec{ + CreatedAt: spec.CreatedAt, + UpdatedAt: spec.UpdatedAt, + CapabilityVersion: spec.CapabilityVersion, + CapabilityLabelledName: spec.CapabilityLabelledName, + OCRKeyBundleIDs: spec.OCRKeyBundleIDs, + P2PKeyID: spec.P2PKeyID, + } +} + // JobError represents errors on the job type JobError struct { ID int64 `json:"id"` @@ -512,6 +532,7 @@ type JobResource struct { GatewaySpec *GatewaySpec `json:"gatewaySpec"` WorkflowSpec *WorkflowSpec `json:"workflowSpec"` StandardCapabilitiesSpec *StandardCapabilitiesSpec `json:"standardCapabilitiesSpec"` + CCIPSpec *CCIPSpec `json:"ccipSpec"` PipelineSpec PipelineSpec `json:"pipelineSpec"` Errors []JobError `json:"errors"` } @@ -562,6 +583,8 @@ func NewJobResource(j job.Job) *JobResource { resource.WorkflowSpec = NewWorkflowSpec(j.WorkflowSpec) case job.StandardCapabilities: resource.StandardCapabilitiesSpec = NewStandardCapabilitiesSpec(j.StandardCapabilitiesSpec) + case job.CCIP: + resource.CCIPSpec = NewCCIPSpec(j.CCIPSpec) case job.LegacyGasStationServer, job.LegacyGasStationSidecar: // unsupported } diff --git a/core/web/presenters/job_test.go b/core/web/presenters/job_test.go index 5de71f918e3..75697c6e068 100644 --- a/core/web/presenters/job_test.go +++ b/core/web/presenters/job_test.go @@ -130,6 +130,7 @@ func TestJob(t *testing.T) { "bootstrapSpec": null, "gatewaySpec": null, "standardCapabilitiesSpec": null, + "ccipSpec": null, "errors": [] } } @@ -208,6 +209,7 @@ func TestJob(t *testing.T) { "bootstrapSpec": null, "gatewaySpec": null, "standardCapabilitiesSpec": null, + "ccipSpec": null, "errors": [] } } @@ -296,6 +298,7 @@ func TestJob(t *testing.T) { "bootstrapSpec": null, "gatewaySpec": null, "standardCapabilitiesSpec": null, + "ccipSpec": null, "errors": [] } } @@ -361,6 +364,7 @@ func TestJob(t *testing.T) { "bootstrapSpec": null, "gatewaySpec": null, "standardCapabilitiesSpec": null, + "ccipSpec": null, "errors": [] } } @@ -423,6 +427,7 @@ func TestJob(t *testing.T) { "bootstrapSpec": null, "gatewaySpec": null, "standardCapabilitiesSpec": null, + "ccipSpec": null, "errors": [] } } @@ -481,6 +486,7 @@ func TestJob(t *testing.T) { "bootstrapSpec": null, "gatewaySpec": null, "standardCapabilitiesSpec": null, + "ccipSpec": null, "errors": [] } } @@ -566,7 +572,9 @@ func TestJob(t *testing.T) { "dotDagSource": "" }, "gatewaySpec": null, - "standardCapabilitiesSpec": null, + "standardCapabilitiesSpec": null, + "standardCapabilitiesSpec": null, + "ccipSpec": null, "errors": [] } } @@ -649,6 +657,7 @@ func TestJob(t *testing.T) { }, "gatewaySpec": null, "standardCapabilitiesSpec": null, + "ccipSpec": null, "errors": [] } } @@ -731,6 +740,7 @@ func TestJob(t *testing.T) { }, "gatewaySpec": null, "standardCapabilitiesSpec": null, + "ccipSpec": null, "errors": [] } } @@ -780,14 +790,14 @@ func TestJob(t *testing.T) { "blockhashStoreSpec": null, "blockHeaderFeederSpec": null, "bootstrapSpec": { - "blockchainTimeout":"0s", - "contractConfigConfirmations":0, - "contractConfigTrackerPollInterval":"0s", - "contractConfigTrackerSubscribeInterval":"0s", - "contractID":"0x16988483b46e695f6c8D58e6e1461DC703e008e1", - "createdAt":"0001-01-01T00:00:00Z", - "relay":"evm", - "relayConfig":{"chainID":1337}, + "blockchainTimeout":"0s", + "contractConfigConfirmations":0, + "contractConfigTrackerPollInterval":"0s", + "contractConfigTrackerSubscribeInterval":"0s", + "contractID":"0x16988483b46e695f6c8D58e6e1461DC703e008e1", + "createdAt":"0001-01-01T00:00:00Z", + "relay":"evm", + "relayConfig":{"chainID":1337}, "updatedAt":"0001-01-01T00:00:00Z" }, "pipelineSpec": { @@ -797,6 +807,7 @@ func TestJob(t *testing.T) { }, "gatewaySpec": null, "standardCapabilitiesSpec": null, + "ccipSpec": null, "errors": [] } } @@ -855,6 +866,7 @@ func TestJob(t *testing.T) { "updatedAt":"0001-01-01T00:00:00Z" }, "standardCapabilitiesSpec": null, + "ccipSpec": null, "pipelineSpec": { "id": 1, "jobID": 0, @@ -919,6 +931,7 @@ func TestJob(t *testing.T) { "bootstrapSpec": null, "gatewaySpec": null, "standardCapabilitiesSpec": null, + "ccipSpec": null, "pipelineSpec": { "id": 1, "jobID": 0, @@ -979,6 +992,72 @@ func TestJob(t *testing.T) { "createdAt":"0001-01-01T00:00:00Z", "updatedAt":"0001-01-01T00:00:00Z" }, + "ccipSpec": null, + "pipelineSpec": { + "id": 1, + "jobID": 0, + "dotDagSource": "" + }, + "errors": [] + } + } + }`, + }, + { + name: "ccip spec", + job: job.Job{ + ID: 1, + CCIPSpec: &job.CCIPSpec{ + ID: 3, + CreatedAt: timestamp, + UpdatedAt: timestamp, + CapabilityVersion: "4.5.9", + CapabilityLabelledName: "ccip", + }, + PipelineSpec: &pipeline.Spec{ + ID: 1, + DotDagSource: "", + }, + ExternalJobID: uuid.MustParse("0eec7e1d-d0d2-476c-a1a8-72dfb6633f46"), + Type: job.CCIP, + SchemaVersion: 1, + Name: null.StringFrom("ccip test"), + }, + want: ` + { + "data": { + "type": "jobs", + "id": "1", + "attributes": { + "name": "ccip test", + "type": "ccip", + "schemaVersion": 1, + "maxTaskDuration": "0s", + "externalJobID": "0eec7e1d-d0d2-476c-a1a8-72dfb6633f46", + "directRequestSpec": null, + "fluxMonitorSpec": null, + "gasLimit": null, + "forwardingAllowed": false, + "cronSpec": null, + "offChainReportingOracleSpec": null, + "offChainReporting2OracleSpec": null, + "keeperSpec": null, + "vrfSpec": null, + "webhookSpec": null, + "workflowSpec": null, + "blockhashStoreSpec": null, + "blockHeaderFeederSpec": null, + "bootstrapSpec": null, + "gatewaySpec": null, + "standardCapabilitiesSpec": null, + "ccipSpec": { + "capabilityVersion":"4.5.9", + "capabilityLabelledName":"ccip", + "ocrKeyBundleIDs": null, + "p2pKeyID": "", + "createdAt":"2000-01-01T00:00:00Z", + "updatedAt":"2000-01-01T00:00:00Z" + }, "pipelineSpec": { "id": 1, "jobID": 0, @@ -1058,6 +1137,7 @@ func TestJob(t *testing.T) { "bootstrapSpec": null, "gatewaySpec": null, "standardCapabilitiesSpec": null, + "ccipSpec": null, "errors": [{ "id": 200, "description": "some error", diff --git a/go.md b/go.md index d9ed0d0a660..697d6b52cea 100644 --- a/go.md +++ b/go.md @@ -28,6 +28,8 @@ flowchart LR click chain-selectors href "https://github.com/smartcontractkit/chain-selectors" chainlink/v2 --> chainlink-automation click chainlink-automation href "https://github.com/smartcontractkit/chainlink-automation" + chainlink/v2 --> chainlink-ccip + click chainlink-ccip href "https://github.com/smartcontractkit/chainlink-ccip" chainlink/v2 --> chainlink-common click chainlink-common href "https://github.com/smartcontractkit/chainlink-common" chainlink/v2 --> chainlink-cosmos @@ -50,6 +52,8 @@ flowchart LR click wsrpc href "https://github.com/smartcontractkit/wsrpc" chainlink-automation --> chainlink-common chainlink-automation --> libocr + chainlink-ccip --> chainlink-common + chainlink-ccip --> libocr chainlink-common --> libocr chainlink-cosmos --> chainlink-common chainlink-cosmos --> libocr diff --git a/go.mod b/go.mod index 2179ffc2d21..f89bfe7def5 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/cometbft/cometbft v0.37.2 github.com/cosmos/cosmos-sdk v0.47.4 github.com/danielkov/gin-helmet v0.0.0-20171108135313-1387e224435e - github.com/deckarep/golang-set/v2 v2.3.0 + github.com/deckarep/golang-set/v2 v2.6.0 github.com/dominikbraun/graph v0.23.0 github.com/esote/minmaxheap v1.0.0 github.com/ethereum/go-ethereum v1.13.8 @@ -74,6 +74,7 @@ require ( github.com/shopspring/decimal v1.4.0 github.com/smartcontractkit/chain-selectors v1.0.10 github.com/smartcontractkit/chainlink-automation v1.0.4 + github.com/smartcontractkit/chainlink-ccip v0.0.0-20240806144315-04ac101e9c95 github.com/smartcontractkit/chainlink-common v0.2.2-0.20240805160614-501c4f40b98c github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240710121324-3ed288aa9b45 github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240801131703-fd75761c982f @@ -102,7 +103,7 @@ require ( go.uber.org/multierr v1.11.0 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.25.0 - golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 golang.org/x/mod v0.19.0 golang.org/x/net v0.27.0 golang.org/x/sync v0.7.0 diff --git a/go.sum b/go.sum index b953f315e92..9679a2da3af 100644 --- a/go.sum +++ b/go.sum @@ -315,8 +315,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/deckarep/golang-set/v2 v2.3.0 h1:qs18EKUfHm2X9fA50Mr/M5hccg2tNnVqsiBImnyDs0g= -github.com/deckarep/golang-set/v2 v2.3.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= @@ -1139,6 +1139,8 @@ github.com/smartcontractkit/chain-selectors v1.0.10 h1:t9kJeE6B6G+hKD0GYR4kGJSCq github.com/smartcontractkit/chain-selectors v1.0.10/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= github.com/smartcontractkit/chainlink-automation v1.0.4 h1:iyW181JjKHLNMnDleI8umfIfVVlwC7+n5izbLSFgjw8= github.com/smartcontractkit/chainlink-automation v1.0.4/go.mod h1:u4NbPZKJ5XiayfKHD/v3z3iflQWqvtdhj13jVZXj/cM= +github.com/smartcontractkit/chainlink-ccip v0.0.0-20240806144315-04ac101e9c95 h1:LAgJTg9Yr/uCo2g7Krp88Dco2U45Y6sbJVl8uKoLkys= +github.com/smartcontractkit/chainlink-ccip v0.0.0-20240806144315-04ac101e9c95/go.mod h1:/ZWraCBaDDgaIN1prixYcbVvIk/6HeED9+8zbWQ+TMo= github.com/smartcontractkit/chainlink-common v0.2.2-0.20240805160614-501c4f40b98c h1:3apUsez/6Pkp1ckXzSwIhzPRuWjDGjzMjKapEKi0Fcw= github.com/smartcontractkit/chainlink-common v0.2.2-0.20240805160614-501c4f40b98c/go.mod h1:Jg1sCTsbxg76YByI8ifpFby3FvVqISStHT8ypy9ocmY= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240710121324-3ed288aa9b45 h1:NBQLtqk8zsyY4qTJs+NElI3aDFTcAo83JHvqD04EvB0= @@ -1436,8 +1438,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 h1:wDLEX9a7YQoKdKNQt88rtydkqDxeGaBUTnIYc3iG/mA= -golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/integration-tests/go.mod b/integration-tests/go.mod index ff60a8f78b3..7be6ea209f1 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -143,7 +143,7 @@ require ( github.com/crate-crypto/go-kzg-4844 v0.7.0 // indirect github.com/danieljoos/wincred v1.1.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/deckarep/golang-set/v2 v2.3.0 // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/dennwc/varint v1.0.0 // indirect github.com/dfuse-io/logging v0.0.0-20210109005628-b97a57253f70 // indirect @@ -377,6 +377,7 @@ require ( github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/smartcontractkit/chain-selectors v1.0.10 // indirect + github.com/smartcontractkit/chainlink-ccip v0.0.0-20240806144315-04ac101e9c95 // indirect github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240710121324-3ed288aa9b45 // indirect github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240801131703-fd75761c982f // indirect github.com/smartcontractkit/chainlink-feeds v0.0.0-20240710170203-5b41615da827 // indirect @@ -448,7 +449,7 @@ require ( go4.org/netipx v0.0.0-20230125063823-8449b0a6169f // indirect golang.org/x/arch v0.8.0 // indirect golang.org/x/crypto v0.25.0 // indirect - golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/mod v0.19.0 // indirect golang.org/x/net v0.27.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect diff --git a/integration-tests/go.sum b/integration-tests/go.sum index 5d15dfd92f6..372a5ee0145 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -415,8 +415,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/deckarep/golang-set/v2 v2.3.0 h1:qs18EKUfHm2X9fA50Mr/M5hccg2tNnVqsiBImnyDs0g= -github.com/deckarep/golang-set/v2 v2.3.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= @@ -1488,6 +1488,8 @@ github.com/smartcontractkit/chain-selectors v1.0.10 h1:t9kJeE6B6G+hKD0GYR4kGJSCq github.com/smartcontractkit/chain-selectors v1.0.10/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= github.com/smartcontractkit/chainlink-automation v1.0.4 h1:iyW181JjKHLNMnDleI8umfIfVVlwC7+n5izbLSFgjw8= github.com/smartcontractkit/chainlink-automation v1.0.4/go.mod h1:u4NbPZKJ5XiayfKHD/v3z3iflQWqvtdhj13jVZXj/cM= +github.com/smartcontractkit/chainlink-ccip v0.0.0-20240806144315-04ac101e9c95 h1:LAgJTg9Yr/uCo2g7Krp88Dco2U45Y6sbJVl8uKoLkys= +github.com/smartcontractkit/chainlink-ccip v0.0.0-20240806144315-04ac101e9c95/go.mod h1:/ZWraCBaDDgaIN1prixYcbVvIk/6HeED9+8zbWQ+TMo= github.com/smartcontractkit/chainlink-common v0.2.2-0.20240805160614-501c4f40b98c h1:3apUsez/6Pkp1ckXzSwIhzPRuWjDGjzMjKapEKi0Fcw= github.com/smartcontractkit/chainlink-common v0.2.2-0.20240805160614-501c4f40b98c/go.mod h1:Jg1sCTsbxg76YByI8ifpFby3FvVqISStHT8ypy9ocmY= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240710121324-3ed288aa9b45 h1:NBQLtqk8zsyY4qTJs+NElI3aDFTcAo83JHvqD04EvB0= @@ -1836,8 +1838,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 h1:wDLEX9a7YQoKdKNQt88rtydkqDxeGaBUTnIYc3iG/mA= -golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/integration-tests/load/go.mod b/integration-tests/load/go.mod index c464231c745..11893540a39 100644 --- a/integration-tests/load/go.mod +++ b/integration-tests/load/go.mod @@ -39,6 +39,7 @@ require ( github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/smartcontractkit/chainlink-ccip v0.0.0-20240806144315-04ac101e9c95 // indirect github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240710121324-3ed288aa9b45 // indirect k8s.io/apimachinery v0.30.2 // indirect ) @@ -131,7 +132,7 @@ require ( github.com/crate-crypto/go-kzg-4844 v0.7.0 // indirect github.com/danieljoos/wincred v1.1.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/deckarep/golang-set/v2 v2.3.0 // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/dennwc/varint v1.0.0 // indirect github.com/dfuse-io/logging v0.0.0-20210109005628-b97a57253f70 // indirect @@ -445,7 +446,7 @@ require ( go4.org/netipx v0.0.0-20230125063823-8449b0a6169f // indirect golang.org/x/arch v0.8.0 // indirect golang.org/x/crypto v0.25.0 // indirect - golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/mod v0.19.0 // indirect golang.org/x/net v0.27.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect diff --git a/integration-tests/load/go.sum b/integration-tests/load/go.sum index d1d6f3a4d52..97a9dfc8ec7 100644 --- a/integration-tests/load/go.sum +++ b/integration-tests/load/go.sum @@ -405,8 +405,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/deckarep/golang-set/v2 v2.3.0 h1:qs18EKUfHm2X9fA50Mr/M5hccg2tNnVqsiBImnyDs0g= -github.com/deckarep/golang-set/v2 v2.3.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= @@ -1470,6 +1470,8 @@ github.com/smartcontractkit/chain-selectors v1.0.10 h1:t9kJeE6B6G+hKD0GYR4kGJSCq github.com/smartcontractkit/chain-selectors v1.0.10/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= github.com/smartcontractkit/chainlink-automation v1.0.4 h1:iyW181JjKHLNMnDleI8umfIfVVlwC7+n5izbLSFgjw8= github.com/smartcontractkit/chainlink-automation v1.0.4/go.mod h1:u4NbPZKJ5XiayfKHD/v3z3iflQWqvtdhj13jVZXj/cM= +github.com/smartcontractkit/chainlink-ccip v0.0.0-20240806144315-04ac101e9c95 h1:LAgJTg9Yr/uCo2g7Krp88Dco2U45Y6sbJVl8uKoLkys= +github.com/smartcontractkit/chainlink-ccip v0.0.0-20240806144315-04ac101e9c95/go.mod h1:/ZWraCBaDDgaIN1prixYcbVvIk/6HeED9+8zbWQ+TMo= github.com/smartcontractkit/chainlink-common v0.2.2-0.20240805160614-501c4f40b98c h1:3apUsez/6Pkp1ckXzSwIhzPRuWjDGjzMjKapEKi0Fcw= github.com/smartcontractkit/chainlink-common v0.2.2-0.20240805160614-501c4f40b98c/go.mod h1:Jg1sCTsbxg76YByI8ifpFby3FvVqISStHT8ypy9ocmY= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240710121324-3ed288aa9b45 h1:NBQLtqk8zsyY4qTJs+NElI3aDFTcAo83JHvqD04EvB0= @@ -1818,8 +1820,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 h1:wDLEX9a7YQoKdKNQt88rtydkqDxeGaBUTnIYc3iG/mA= -golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=