diff --git a/rolling-shutter/chainobserver/db/keyper/keyper.sqlc.gen.go b/rolling-shutter/chainobserver/db/keyper/keyper.sqlc.gen.go index fea52e4a4..fd0f0fe68 100644 --- a/rolling-shutter/chainobserver/db/keyper/keyper.sqlc.gen.go +++ b/rolling-shutter/chainobserver/db/keyper/keyper.sqlc.gen.go @@ -43,6 +43,31 @@ func (q *Queries) GetKeyperSetByKeyperConfigIndex(ctx context.Context, keyperCon return i, err } +const getKeyperSetIndices = `-- name: GetKeyperSetIndices :many +SELECT keyper_config_index FROM keyper_set +ORDER BY keyper_config_index ASC +` + +func (q *Queries) GetKeyperSetIndices(ctx context.Context) ([]int64, error) { + rows, err := q.db.Query(ctx, getKeyperSetIndices) + if err != nil { + return nil, err + } + defer rows.Close() + var items []int64 + for rows.Next() { + var keyper_config_index int64 + if err := rows.Scan(&keyper_config_index); err != nil { + return nil, err + } + items = append(items, keyper_config_index) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getKeyperSets = `-- name: GetKeyperSets :many SELECT keyper_config_index, activation_block_number, keypers, threshold FROM keyper_set ORDER BY activation_block_number ASC diff --git a/rolling-shutter/chainobserver/db/keyper/sql/queries/keyper.sql b/rolling-shutter/chainobserver/db/keyper/sql/queries/keyper.sql index 90b386425..fd822cdd0 100644 --- a/rolling-shutter/chainobserver/db/keyper/sql/queries/keyper.sql +++ b/rolling-shutter/chainobserver/db/keyper/sql/queries/keyper.sql @@ -18,4 +18,8 @@ ORDER BY activation_block_number DESC LIMIT 1; -- name: GetKeyperSets :many SELECT * FROM keyper_set -ORDER BY activation_block_number ASC; \ No newline at end of file +ORDER BY activation_block_number ASC; + +-- name: GetKeyperSetIndices :many +SELECT keyper_config_index FROM keyper_set +ORDER BY keyper_config_index ASC; \ No newline at end of file diff --git a/rolling-shutter/keyperimpl/gnosis/keyper.go b/rolling-shutter/keyperimpl/gnosis/keyper.go index 70d8fbd8f..1ab0876b3 100644 --- a/rolling-shutter/keyperimpl/gnosis/keyper.go +++ b/rolling-shutter/keyperimpl/gnosis/keyper.go @@ -13,6 +13,7 @@ import ( sequencerBindings "github.com/shutter-network/gnosh-contracts/gnoshcontracts/sequencer" validatorRegistryBindings "github.com/shutter-network/gnosh-contracts/gnoshcontracts/validatorregistry" + obskeyper "github.com/shutter-network/rolling-shutter/rolling-shutter/chainobserver/db/keyper" "github.com/shutter-network/rolling-shutter/rolling-shutter/eonkeypublisher" "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper" "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/epochkghandler" @@ -109,6 +110,10 @@ func (kpr *Keyper) Start(ctx context.Context, runner service.Runner) error { return errors.Wrap(err, "can't instantiate keyper core") } + ksIndices, err := obskeyper.New(kpr.dbpool).GetKeyperSetIndices(ctx) + if err != nil { + return errors.Wrap(err, "failed to load keyper set indices") + } kpr.chainSyncClient, err = chainsync.NewClient( ctx, chainsync.WithClientURL(kpr.config.Gnosis.Node.EthereumURL), @@ -118,6 +123,7 @@ func (kpr *Keyper) Start(ctx context.Context, runner service.Runner) error { chainsync.WithSyncNewKeyperSet(kpr.channelNewKeyperSet), chainsync.WithPrivateKey(kpr.config.Gnosis.Node.PrivateKey.Key), chainsync.WithLogger(gethLog.NewLogger(slog.Default().Handler())), + chainsync.WithKnownKeyperSetIndices(ksIndices), ) if err != nil { return err diff --git a/rolling-shutter/keyperimpl/optimism/keyper.go b/rolling-shutter/keyperimpl/optimism/keyper.go index 07ec6191e..235a69cb8 100644 --- a/rolling-shutter/keyperimpl/optimism/keyper.go +++ b/rolling-shutter/keyperimpl/optimism/keyper.go @@ -78,6 +78,10 @@ func (kpr *Keyper) Start(ctx context.Context, runner service.Runner) error { if err != nil { return errors.Wrap(err, "can't instantiate keyper core") } + ksIndices, err := obskeyper.New(kpr.dbpool).GetKeyperSetIndices(ctx) + if err != nil { + return errors.Wrap(err, "failed to load keyper set indices") + } // TODO: wrap the logger and pass in kpr.l2Client, err = chainsync.NewClient( ctx, @@ -85,6 +89,7 @@ func (kpr *Keyper) Start(ctx context.Context, runner service.Runner) error { chainsync.WithSyncNewBlock(kpr.newBlock), chainsync.WithSyncNewKeyperSet(kpr.newKeyperSet), chainsync.WithPrivateKey(kpr.config.Optimism.PrivateKey.Key), + chainsync.WithKnownKeyperSetIndices(ksIndices), ) if err != nil { return err diff --git a/rolling-shutter/keyperimpl/primev/keyper.go b/rolling-shutter/keyperimpl/primev/keyper.go index 609a35672..dedded73d 100644 --- a/rolling-shutter/keyperimpl/primev/keyper.go +++ b/rolling-shutter/keyperimpl/primev/keyper.go @@ -11,6 +11,7 @@ import ( providerregistry "github.com/primev/mev-commit/contracts-abi/clients/ProviderRegistry" "github.com/rs/zerolog/log" + obskeyper "github.com/shutter-network/rolling-shutter/rolling-shutter/chainobserver/db/keyper" "github.com/shutter-network/rolling-shutter/rolling-shutter/eonkeypublisher" "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper" "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/epochkghandler" @@ -77,6 +78,10 @@ func (k *Keyper) Start(ctx context.Context, runner service.Runner) error { return errors.Wrap(err, "can't instantiate keyper core") } + ksIndices, err := obskeyper.New(k.dbpool).GetKeyperSetIndices(ctx) + if err != nil { + return errors.Wrap(err, "failed to load keyper set indices") + } k.chainSyncClient, err = chainsync.NewClient( ctx, chainsync.WithClientURL(k.config.Chain.Node.EthereumURL), @@ -86,6 +91,7 @@ func (k *Keyper) Start(ctx context.Context, runner service.Runner) error { chainsync.WithSyncNewBlock(k.channelNewBlock), chainsync.WithPrivateKey(k.config.Chain.Node.PrivateKey.Key), chainsync.WithLogger(gethLog.NewLogger(slog.Default().Handler())), + chainsync.WithKnownKeyperSetIndices(ksIndices), ) if err != nil { return err diff --git a/rolling-shutter/keyperimpl/shutterservice/keyper.go b/rolling-shutter/keyperimpl/shutterservice/keyper.go index 4d5960c41..73d93dabb 100644 --- a/rolling-shutter/keyperimpl/shutterservice/keyper.go +++ b/rolling-shutter/keyperimpl/shutterservice/keyper.go @@ -14,6 +14,7 @@ import ( triggerRegistryV1Bindings "github.com/shutter-network/contracts/v2/bindings/shuttereventtriggerregistryv1" registryBindings "github.com/shutter-network/contracts/v2/bindings/shutterregistry" + obskeyper "github.com/shutter-network/rolling-shutter/rolling-shutter/chainobserver/db/keyper" "github.com/shutter-network/rolling-shutter/rolling-shutter/eonkeypublisher" "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper" "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/epochkghandler" @@ -83,6 +84,10 @@ func (kpr *Keyper) Start(ctx context.Context, runner service.Runner) error { if err != nil { return errors.Wrap(err, "can't instantiate keyper core") } + ksIndices, err := obskeyper.New(kpr.dbpool).GetKeyperSetIndices(ctx) + if err != nil { + return errors.Wrap(err, "failed to load keyper set indices") + } kpr.chainSyncClient, err = chainsync.NewClient( ctx, chainsync.WithClientURL(kpr.config.Chain.Node.EthereumURL), @@ -92,6 +97,7 @@ func (kpr *Keyper) Start(ctx context.Context, runner service.Runner) error { chainsync.WithSyncNewKeyperSet(kpr.channelNewKeyperSet), chainsync.WithPrivateKey(kpr.config.Chain.Node.PrivateKey.Key), chainsync.WithLogger(gethLog.NewLogger(slog.Default().Handler())), + chainsync.WithKnownKeyperSetIndices(ksIndices), ) if err != nil { return err diff --git a/rolling-shutter/medley/chainsync/options.go b/rolling-shutter/medley/chainsync/options.go index 580876243..46a861117 100644 --- a/rolling-shutter/medley/chainsync/options.go +++ b/rolling-shutter/medley/chainsync/options.go @@ -30,10 +30,11 @@ type options struct { syncStart *number.BlockNumber privKey *ecdsa.PrivateKey - handlerShutterState event.ShutterStateHandler - handlerKeyperSet event.KeyperSetHandler - handlerEonPublicKey event.EonPublicKeyHandler - handlerBlock event.BlockHandler + handlerShutterState event.ShutterStateHandler + handlerKeyperSet event.KeyperSetHandler + handlerEonPublicKey event.EonPublicKeyHandler + handlerBlock event.BlockHandler + knownKeyperSetIndices []int64 } func (o *options) verify() error { @@ -85,11 +86,12 @@ func (o *options) apply(ctx context.Context, c *Client) error { return err } c.kssync = &syncer.KeyperSetSyncer{ - Client: client, - Contract: c.KeyperSetManager, - Log: c.log, - StartBlock: o.syncStart, - Handler: o.handlerKeyperSet, + Client: client, + Contract: c.KeyperSetManager, + Log: c.log, + StartBlock: o.syncStart, + Handler: o.handlerKeyperSet, + KnownIndices: o.knownKeyperSetIndices, } if o.handlerKeyperSet != nil { c.services = append(c.services, c.kssync) @@ -214,6 +216,13 @@ func WithSyncNewKeyperSet(handler event.KeyperSetHandler) Option { } } +func WithKnownKeyperSetIndices(indices []int64) Option { + return func(o *options) error { + o.knownKeyperSetIndices = indices + return nil + } +} + func WithSyncNewBlock(handler event.BlockHandler) Option { return func(o *options) error { o.handlerBlock = handler diff --git a/rolling-shutter/medley/chainsync/syncer/keyperset.go b/rolling-shutter/medley/chainsync/syncer/keyperset.go index 9daada16e..e0832f02e 100644 --- a/rolling-shutter/medley/chainsync/syncer/keyperset.go +++ b/rolling-shutter/medley/chainsync/syncer/keyperset.go @@ -22,11 +22,12 @@ func makeCallError(attrName string, err error) error { const channelSize = 10 type KeyperSetSyncer struct { - Client client.Client - Contract *bindings.KeyperSetManager - Log log.Logger - StartBlock *number.BlockNumber - Handler event.KeyperSetHandler + Client client.Client + Contract *bindings.KeyperSetManager + Log log.Logger + StartBlock *number.BlockNumber + Handler event.KeyperSetHandler + KnownIndices []int64 keyperAddedCh chan *bindings.KeyperSetManagerKeyperSetAdded } @@ -81,6 +82,20 @@ func (s *KeyperSetSyncer) Start(ctx context.Context, runner service.Runner) erro return nil } +func missingIndices(known []int64, total uint64) []uint64 { + knownSet := make(map[uint64]struct{}, len(known)) + for _, k := range known { + knownSet[uint64(k)] = struct{}{} //nolint:gosec + } + var result []uint64 + for i := uint64(0); i < total; i++ { + if _, ok := knownSet[i]; !ok { + result = append(result, i) + } + } + return result +} + func (s *KeyperSetSyncer) getInitialKeyperSets(ctx context.Context) ([]*event.KeyperSet, error) { opts := &bind.CallOpts{ Context: ctx, @@ -89,34 +104,20 @@ func (s *KeyperSetSyncer) getInitialKeyperSets(ctx context.Context) ([]*event.Ke if err := guardCallOpts(opts, false); err != nil { return nil, err } - bn := s.StartBlock.ToUInt64Ptr() - if bn == nil { - // this should not be the case - return nil, errors.New("start block is 'latest'") - } - - initialKeyperSets := []*event.KeyperSet{} - // this blocknumber specifies the argument to the contract - // getter - ks, err := s.GetKeyperSetForBlock(ctx, opts, s.StartBlock) - if err != nil { - return nil, err - } - initialKeyperSets = append(initialKeyperSets, ks) numKS, err := s.Contract.GetNumKeyperSets(opts) if err != nil { return nil, err } - for i := ks.Eon + 1; i < numKS; i++ { - ks, err = s.GetKeyperSetByIndex(ctx, opts, i) + var initialKeyperSets []*event.KeyperSet + for _, i := range missingIndices(s.KnownIndices, numKS) { + ks, err := s.GetKeyperSetByIndex(ctx, opts, i) if err != nil { return nil, err } initialKeyperSets = append(initialKeyperSets, ks) } - return initialKeyperSets, nil } diff --git a/rolling-shutter/medley/chainsync/syncer/keyperset_test.go b/rolling-shutter/medley/chainsync/syncer/keyperset_test.go new file mode 100644 index 000000000..ca4e881d0 --- /dev/null +++ b/rolling-shutter/medley/chainsync/syncer/keyperset_test.go @@ -0,0 +1,76 @@ +package syncer + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +func TestMissingIndices(t *testing.T) { + tests := []struct { + name string + known []int64 + total uint64 + want []uint64 + }{ + { + name: "nothing known, fetch all", + known: nil, + total: 4, + want: []uint64{0, 1, 2, 3}, + }, + { + name: "nothing known, empty contract", + known: nil, + total: 0, + want: nil, + }, + { + name: "all known", + known: []int64{0, 1, 2, 3}, + total: 4, + want: nil, + }, + { + name: "gap at the start", + known: []int64{3, 4, 5}, + total: 6, + want: []uint64{0, 1, 2}, + }, + { + name: "gap at the end", + known: []int64{0, 1, 2}, + total: 6, + want: []uint64{3, 4, 5}, + }, + { + name: "gap in the middle", + known: []int64{0, 1, 5, 6}, + total: 7, + want: []uint64{2, 3, 4}, + }, + { + name: "known indices beyond total are ignored", + known: []int64{0, 1, 10, 20}, + total: 4, + want: []uint64{2, 3}, + }, + { + name: "duplicate known indices", + known: []int64{1, 1, 2, 2}, + total: 4, + want: []uint64{0, 3}, + }, + { + name: "unordered known indices", + known: []int64{4, 1, 3}, + total: 5, + want: []uint64{0, 2}, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert.DeepEqual(t, missingIndices(tc.known, tc.total), tc.want) + }) + } +}