From 758050896746755d65f37320a636acebeaceabab Mon Sep 17 00:00:00 2001 From: Sean McGary Date: Thu, 5 Sep 2024 22:28:09 -0500 Subject: [PATCH] Add support for staker delegations --- .../eigenState/avsOperators/avsOperators.go | 8 +- internal/eigenState/base/baseEigenState.go | 12 + .../operatorShares/operatorShares.go | 8 +- .../stakerDelegations/stakerDelegations.go | 360 ++++++++++++++++++ .../stakerDelegations_test.go | 207 ++++++++++ .../202408200934_eigenlayerStateTables/up.go | 23 +- .../202409052151_stakerDelegations/up.go | 46 +++ internal/postgres/migrations/migrator.go | 2 + internal/storage/tables.go | 42 -- sql/schema.sql | 73 ++++ 10 files changed, 703 insertions(+), 78 deletions(-) create mode 100644 internal/eigenState/stakerDelegations/stakerDelegations.go create mode 100644 internal/eigenState/stakerDelegations/stakerDelegations_test.go create mode 100644 internal/postgres/migrations/202409052151_stakerDelegations/up.go diff --git a/internal/eigenState/avsOperators/avsOperators.go b/internal/eigenState/avsOperators/avsOperators.go index ed83c66d..edd8361a 100644 --- a/internal/eigenState/avsOperators/avsOperators.go +++ b/internal/eigenState/avsOperators/avsOperators.go @@ -152,13 +152,7 @@ func (a *AvsOperators) getContractAddressesForEnvironment() map[string][]string // Given a log, determine if it is interesting to the state model func (a *AvsOperators) IsInterestingLog(log *storage.TransactionLog) bool { addresses := a.getContractAddressesForEnvironment() - logAddress := strings.ToLower(log.Address) - if eventNames, ok := addresses[logAddress]; ok { - if slices.Contains(eventNames, log.EventName) { - return true - } - } - return false + return a.BaseEigenState.IsInterestingLog(addresses, log) } // Handle the state change for the given log diff --git a/internal/eigenState/base/baseEigenState.go b/internal/eigenState/base/baseEigenState.go index da85b722..41eea18c 100644 --- a/internal/eigenState/base/baseEigenState.go +++ b/internal/eigenState/base/baseEigenState.go @@ -6,6 +6,8 @@ import ( "github.com/Layr-Labs/sidecar/internal/parser" "github.com/Layr-Labs/sidecar/internal/storage" "go.uber.org/zap" + "slices" + "strings" ) type BaseEigenState struct { @@ -49,3 +51,13 @@ func (b *BaseEigenState) InitializeMerkleTreeBaseStateWithBlock(blockNumber uint []byte(fmt.Sprintf("%d", blockNumber)), } } + +func (b *BaseEigenState) IsInterestingLog(contractsEvents map[string][]string, log *storage.TransactionLog) bool { + logAddress := strings.ToLower(log.Address) + if eventNames, ok := contractsEvents[logAddress]; ok { + if slices.Contains(eventNames, log.EventName) { + return true + } + } + return false +} diff --git a/internal/eigenState/operatorShares/operatorShares.go b/internal/eigenState/operatorShares/operatorShares.go index 1a29713b..49b6ba16 100644 --- a/internal/eigenState/operatorShares/operatorShares.go +++ b/internal/eigenState/operatorShares/operatorShares.go @@ -152,13 +152,7 @@ func (osm *OperatorSharesModel) getContractAddressesForEnvironment() map[string] func (osm *OperatorSharesModel) IsInterestingLog(log *storage.TransactionLog) bool { addresses := osm.getContractAddressesForEnvironment() - logAddress := strings.ToLower(log.Address) - if eventNames, ok := addresses[logAddress]; ok { - if slices.Contains(eventNames, log.EventName) { - return true - } - } - return false + return osm.BaseEigenState.IsInterestingLog(addresses, log) } func (osm *OperatorSharesModel) HandleStateChange(log *storage.TransactionLog) (interface{}, error) { diff --git a/internal/eigenState/stakerDelegations/stakerDelegations.go b/internal/eigenState/stakerDelegations/stakerDelegations.go new file mode 100644 index 00000000..141a7f24 --- /dev/null +++ b/internal/eigenState/stakerDelegations/stakerDelegations.go @@ -0,0 +1,360 @@ +package stakerDelegations + +import ( + "database/sql" + "fmt" + "github.com/Layr-Labs/sidecar/internal/config" + "github.com/Layr-Labs/sidecar/internal/eigenState/base" + "github.com/Layr-Labs/sidecar/internal/eigenState/stateManager" + "github.com/Layr-Labs/sidecar/internal/eigenState/types" + "github.com/Layr-Labs/sidecar/internal/storage" + "github.com/Layr-Labs/sidecar/internal/utils" + "github.com/wealdtech/go-merkletree/v2" + "github.com/wealdtech/go-merkletree/v2/keccak256" + orderedmap "github.com/wk8/go-ordered-map/v2" + "go.uber.org/zap" + "gorm.io/gorm" + "gorm.io/gorm/clause" + "slices" + "sort" + "strings" + "time" +) + +type DelegatedStakers struct { + Staker string + Operator string + BlockNumber uint64 + CreatedAt time.Time +} + +type StakerDelegationChange struct { + Staker string + Operator string + Delegated bool + TransactionHash string + TransactionIndex uint64 + LogIndex uint64 + BlockNumber uint64 + CreatedAt time.Time +} + +type StakerDelegationsModel struct { + base.BaseEigenState + StateTransitions types.StateTransitions[StakerDelegationChange] + Db *gorm.DB + Network config.Network + Environment config.Environment + logger *zap.Logger + globalConfig *config.Config +} + +type DelegatedStakersDiff struct { + Staker string + Operator string + Delegated bool + BlockNumber uint64 +} + +func NewStakerDelegationsModel( + esm *stateManager.EigenStateManager, + grm *gorm.DB, + Network config.Network, + Environment config.Environment, + logger *zap.Logger, + globalConfig *config.Config, +) (*StakerDelegationsModel, error) { + model := &StakerDelegationsModel{ + BaseEigenState: base.BaseEigenState{ + Logger: logger, + }, + Db: grm, + Network: Network, + Environment: Environment, + logger: logger, + globalConfig: globalConfig, + } + + esm.RegisterState(model, 2) + return model, nil +} + +func (s *StakerDelegationsModel) GetModelName() string { + return "StakerDelegationsModel" +} + +func (s *StakerDelegationsModel) GetStateTransitions() (types.StateTransitions[StakerDelegationChange], []uint64) { + stateChanges := make(types.StateTransitions[StakerDelegationChange]) + + stateChanges[0] = func(log *storage.TransactionLog) (*StakerDelegationChange, error) { + arguments, err := s.ParseLogArguments(log) + if err != nil { + return nil, err + } + + delegated := true + if log.EventName == "StakerUndelegated" { + delegated = false + } + + change := &StakerDelegationChange{ + Staker: arguments[0].Value.(string), + Operator: arguments[1].Value.(string), + Delegated: delegated, + TransactionHash: log.TransactionHash, + TransactionIndex: log.TransactionIndex, + LogIndex: log.LogIndex, + BlockNumber: log.BlockNumber, + CreatedAt: log.CreatedAt, + } + return change, nil + } + + // Create an ordered list of block numbers + blockNumbers := make([]uint64, 0) + for blockNumber, _ := range stateChanges { + blockNumbers = append(blockNumbers, blockNumber) + } + sort.Slice(blockNumbers, func(i, j int) bool { + return blockNumbers[i] < blockNumbers[j] + }) + slices.Reverse(blockNumbers) + + return stateChanges, blockNumbers +} + +func (s *StakerDelegationsModel) getContractAddressesForEnvironment() map[string][]string { + contracts := s.globalConfig.GetContractsMapForEnvAndNetwork() + return map[string][]string{ + contracts.DelegationManager: []string{ + "StakerUndelegated", + "StakerDelegated", + }, + } +} + +func (s *StakerDelegationsModel) IsInterestingLog(log *storage.TransactionLog) bool { + addresses := s.getContractAddressesForEnvironment() + return s.BaseEigenState.IsInterestingLog(addresses, log) +} + +func (s *StakerDelegationsModel) HandleStateChange(log *storage.TransactionLog) (interface{}, error) { + stateChanges, sortedBlockNumbers := s.GetStateTransitions() + + for _, blockNumber := range sortedBlockNumbers { + if log.BlockNumber >= blockNumber { + s.logger.Sugar().Debugw("Handling state change", zap.Uint64("blockNumber", blockNumber)) + + change, err := stateChanges[blockNumber](log) + if err != nil { + return nil, err + } + + if change != nil { + wroteChange, err := s.writeStateChange(change) + if err != nil { + return wroteChange, err + } + return wroteChange, nil + } + } + } + return nil, nil +} + +func (s *StakerDelegationsModel) writeStateChange(change *StakerDelegationChange) (interface{}, error) { + s.logger.Sugar().Debugw("Writing state change", zap.Any("change", change)) + res := s.Db.Model(&StakerDelegationChange{}).Clauses(clause.Returning{}).Create(change) + if res.Error != nil { + s.logger.Error("Failed to insert into avs_operator_changes", zap.Error(res.Error)) + return change, res.Error + } + return change, nil +} + +func (s *StakerDelegationsModel) WriteFinalState(blockNumber uint64) error { + query := ` + with new_changes as ( + select + staker, + operator, + block_number, + max(transaction_index) as transaction_index, + max(log_index) as log_index + from staker_delegation_changes + where block_number = @currentBlock + group by 1, 2, 3 + ), + unique_delegations as ( + select + nc.staker, + nc.operator, + sdc.log_index, + sdc.delegated, + nc.block_number + from new_changes as nc + left join staker_delegation_changes as sdc on ( + sdc.staker = nc.staker + and sdc.operator = nc.operator + and sdc.log_index = nc.log_index + and sdc.transaction_index = nc.transaction_index + and sdc.block_number = nc.block_number + ) + ), + undelegations as ( + select + concat(staker, '_', operator) as staker_operator + from unique_delegations + where delegated = false + ), + carryover as ( + select + rao.staker, + rao.operator, + @currentBlock as block_number + from delegated_stakers as rao + where + rao.block_number = @previousBlock + and concat(rao.staker, '_', rao.operator) not in (select staker_operator from undelegations) + ), + final_state as ( + (select staker, operator, block_number::bigint from carryover) + union all + (select staker, operator, block_number::bigint from unique_delegations where delegated = true) + ) + insert into delegated_stakers (staker, operator, block_number) + select staker, operator, block_number from final_state + ` + + res := s.Db.Exec(query, + sql.Named("currentBlock", blockNumber), + sql.Named("previousBlock", blockNumber-1), + ) + if res.Error != nil { + s.logger.Sugar().Errorw("Failed to insert into operator_shares", zap.Error(res.Error)) + return res.Error + } + return nil +} +func (s *StakerDelegationsModel) getDifferenceInStates(blockNumber uint64) ([]DelegatedStakersDiff, error) { + query := ` + with new_states as ( + select + staker, + operator, + block_number, + true as delegated + from delegated_stakers + where block_number = @currentBlock + ), + previous_states as ( + select + staker, + operator, + block_number, + true as delegated + from delegated_stakers + where block_number = @previousBlock + ), + undelegated as ( + (select staker, operator, delegated from previous_states) + except + (select staker, operator, delegated from new_states) + ), + new_delegated as ( + (select staker, operator, delegated from new_states) + except + (select staker, operator, delegated from previous_states) + ) + select staker, operator, false as delegated from undelegated + union all + select staker, operator, true as delegated from new_delegated; + ` + results := make([]DelegatedStakersDiff, 0) + res := s.Db.Model(&DelegatedStakersDiff{}). + Raw(query, + sql.Named("currentBlock", blockNumber), + sql.Named("previousBlock", blockNumber-1), + ). + Scan(&results) + + if res.Error != nil { + s.logger.Sugar().Errorw("Failed to fetch delegated_stakers", zap.Error(res.Error)) + return nil, res.Error + } + return results, nil +} + +func (s *StakerDelegationsModel) GenerateStateRoot(blockNumber uint64) (types.StateRoot, error) { + results, err := s.getDifferenceInStates(blockNumber) + if err != nil { + return "", err + } + + fullTree, err := s.merkelizeState(blockNumber, results) + if err != nil { + return "", err + } + return types.StateRoot(utils.ConvertBytesToString(fullTree.Root())), nil +} + +func (s *StakerDelegationsModel) merkelizeState(blockNumber uint64, delegatedStakers []DelegatedStakersDiff) (*merkletree.MerkleTree, error) { + // Operator -> staker:delegated + om := orderedmap.New[string, *orderedmap.OrderedMap[string, bool]]() + + for _, result := range delegatedStakers { + existingOperator, found := om.Get(result.Operator) + if !found { + existingOperator = orderedmap.New[string, bool]() + om.Set(result.Operator, existingOperator) + + prev := om.GetPair(result.Operator).Prev() + if prev != nil && strings.Compare(prev.Key, result.Operator) >= 0 { + om.Delete(result.Operator) + return nil, fmt.Errorf("operators not in order") + } + } + existingOperator.Set(result.Staker, result.Delegated) + + prev := existingOperator.GetPair(result.Staker).Prev() + if prev != nil && strings.Compare(prev.Key, result.Staker) >= 0 { + existingOperator.Delete(result.Staker) + return nil, fmt.Errorf("stakers not in order") + } + } + + operatorLeaves := s.InitializeMerkleTreeBaseStateWithBlock(blockNumber) + + for op := om.Oldest(); op != nil; op = op.Next() { + + stakerLeafs := make([][]byte, 0) + for staker := op.Value.Oldest(); staker != nil; staker = staker.Next() { + operatorAddr := staker.Key + delegated := staker.Value + stakerLeafs = append(stakerLeafs, encodeStakerLeaf(operatorAddr, delegated)) + } + + avsTree, err := merkletree.NewTree( + merkletree.WithData(stakerLeafs), + merkletree.WithHashType(keccak256.New()), + ) + if err != nil { + return nil, err + } + + operatorLeaves = append(operatorLeaves, encodeOperatorLeaf(op.Key, avsTree.Root())) + } + + return merkletree.NewTree( + merkletree.WithData(operatorLeaves), + merkletree.WithHashType(keccak256.New()), + ) +} + +func encodeStakerLeaf(staker string, delegated bool) []byte { + return []byte(fmt.Sprintf("%s:%t", staker, delegated)) +} + +func encodeOperatorLeaf(operator string, operatorStakersRoot []byte) []byte { + return append([]byte(operator), operatorStakersRoot[:]...) +} diff --git a/internal/eigenState/stakerDelegations/stakerDelegations_test.go b/internal/eigenState/stakerDelegations/stakerDelegations_test.go new file mode 100644 index 00000000..134e7f50 --- /dev/null +++ b/internal/eigenState/stakerDelegations/stakerDelegations_test.go @@ -0,0 +1,207 @@ +package stakerDelegations + +import ( + "database/sql" + "github.com/Layr-Labs/sidecar/internal/config" + "github.com/Layr-Labs/sidecar/internal/eigenState/stateManager" + "github.com/Layr-Labs/sidecar/internal/logger" + "github.com/Layr-Labs/sidecar/internal/storage" + "github.com/Layr-Labs/sidecar/internal/tests" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + "gorm.io/gorm" + "testing" + "time" +) + +func setup() ( + *config.Config, + *gorm.DB, + *zap.Logger, + error, +) { + cfg := tests.GetConfig() + l, _ := logger.NewLogger(&logger.LoggerConfig{Debug: cfg.Debug}) + + _, grm, err := tests.GetDatabaseConnection(cfg) + + return cfg, grm, l, err +} + +func teardown(model *StakerDelegationsModel) { + model.Db.Exec("truncate table staker_delegation_changes cascade") + model.Db.Exec("truncate table delegated_stakers cascade") +} + +func Test_DelegatedStakersState(t *testing.T) { + cfg, grm, l, err := setup() + + if err != nil { + t.Fatal(err) + } + + t.Run("Should create a new StakerDelegationsModel", func(t *testing.T) { + esm := stateManager.NewEigenStateManager(l) + model, err := NewStakerDelegationsModel(esm, grm, cfg.Network, cfg.Environment, l, cfg) + assert.Nil(t, err) + assert.NotNil(t, model) + }) + t.Run("Should register StakerDelegationsModel", func(t *testing.T) { + esm := stateManager.NewEigenStateManager(l) + blockNumber := uint64(200) + log := storage.TransactionLog{ + TransactionHash: "some hash", + TransactionIndex: 100, + BlockNumber: blockNumber, + BlockSequenceId: 300, + Address: cfg.GetContractsMapForEnvAndNetwork().DelegationManager, + Arguments: `[{"Value": "0x5fc1b61816ddeb33b65a02a42b29059ecd3a20e9" }, { "Value": "0x5accc90436492f24e6af278569691e2c942a676d" }]`, + EventName: "StakerDelegated", + LogIndex: 400, + OutputData: `{}`, + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + DeletedAt: time.Time{}, + } + + model, err := NewStakerDelegationsModel(esm, grm, cfg.Network, cfg.Environment, l, cfg) + + assert.Equal(t, true, model.IsInterestingLog(&log)) + + res, err := model.HandleStateChange(&log) + assert.Nil(t, err) + assert.NotNil(t, res) + + teardown(model) + }) + t.Run("Should register StakerDelegationsModel and generate the table for the block", func(t *testing.T) { + esm := stateManager.NewEigenStateManager(l) + blockNumber := uint64(200) + + log := storage.TransactionLog{ + TransactionHash: "some hash", + TransactionIndex: 100, + BlockNumber: blockNumber, + BlockSequenceId: 300, + Address: cfg.GetContractsMapForEnvAndNetwork().DelegationManager, + Arguments: `[{"Value": "0x5fc1b61816ddeb33b65a02a42b29059ecd3a20e9" }, { "Value": "0x5accc90436492f24e6af278569691e2c942a676d" }]`, + EventName: "StakerDelegated", + LogIndex: 400, + OutputData: `{}`, + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + DeletedAt: time.Time{}, + } + + model, err := NewStakerDelegationsModel(esm, grm, cfg.Network, cfg.Environment, l, cfg) + assert.Nil(t, err) + + assert.Equal(t, true, model.IsInterestingLog(&log)) + + stateChange, err := model.HandleStateChange(&log) + assert.Nil(t, err) + assert.NotNil(t, stateChange) + + err = model.WriteFinalState(blockNumber) + assert.Nil(t, err) + + states := []DelegatedStakers{} + statesRes := model.Db. + Model(&DelegatedStakers{}). + Raw("select * from delegated_stakers where block_number = @blockNumber", sql.Named("blockNumber", blockNumber)). + Scan(&states) + + if statesRes.Error != nil { + t.Fatalf("Failed to fetch delegated_stakers: %v", statesRes.Error) + } + assert.Equal(t, 1, len(states)) + + stateRoot, err := model.GenerateStateRoot(blockNumber) + assert.Nil(t, err) + assert.True(t, len(stateRoot) > 0) + + teardown(model) + }) + t.Run("Should correctly generate state across multiple blocks", func(t *testing.T) { + esm := stateManager.NewEigenStateManager(l) + blocks := []uint64{ + 300, + 301, + } + + logs := []*storage.TransactionLog{ + &storage.TransactionLog{ + TransactionHash: "some hash", + TransactionIndex: 100, + BlockNumber: blocks[0], + BlockSequenceId: 300, + Address: cfg.GetContractsMapForEnvAndNetwork().DelegationManager, + Arguments: `[{"Value": "0x5fc1b61816ddeb33b65a02a42b29059ecd3a20e9" }, { "Value": "0x5accc90436492f24e6af278569691e2c942a676d" }]`, + EventName: "StakerDelegated", + LogIndex: 400, + OutputData: `{}`, + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + DeletedAt: time.Time{}, + }, + &storage.TransactionLog{ + TransactionHash: "some hash", + TransactionIndex: 100, + BlockNumber: blocks[1], + BlockSequenceId: 300, + Address: cfg.GetContractsMapForEnvAndNetwork().DelegationManager, + Arguments: `[{"Value": "0x5fc1b61816ddeb33b65a02a42b29059ecd3a20e9" }, { "Value": "0x5accc90436492f24e6af278569691e2c942a676d" }]`, + EventName: "StakerUndelegated", + LogIndex: 400, + OutputData: `{}`, + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + DeletedAt: time.Time{}, + }, + } + + model, err := NewStakerDelegationsModel(esm, grm, cfg.Network, cfg.Environment, l, cfg) + assert.Nil(t, err) + + for _, log := range logs { + assert.True(t, model.IsInterestingLog(log)) + + stateChange, err := model.HandleStateChange(log) + assert.Nil(t, err) + assert.NotNil(t, stateChange) + + err = model.WriteFinalState(log.BlockNumber) + assert.Nil(t, err) + + states := []DelegatedStakers{} + statesRes := model.Db. + Model(&DelegatedStakers{}). + Raw("select * from delegated_stakers where block_number = @blockNumber", sql.Named("blockNumber", log.BlockNumber)). + Scan(&states) + + if statesRes.Error != nil { + t.Fatalf("Failed to fetch delegated_stakers: %v", statesRes.Error) + } + + if log.BlockNumber == blocks[0] { + assert.Equal(t, 1, len(states)) + diffs, err := model.getDifferenceInStates(log.BlockNumber) + assert.Nil(t, err) + assert.Equal(t, 1, len(diffs)) + assert.Equal(t, true, diffs[0].Delegated) + } else if log.BlockNumber == blocks[1] { + assert.Equal(t, 0, len(states)) + diffs, err := model.getDifferenceInStates(log.BlockNumber) + assert.Nil(t, err) + assert.Equal(t, 1, len(diffs)) + assert.Equal(t, false, diffs[0].Delegated) + } + + stateRoot, err := model.GenerateStateRoot(log.BlockNumber) + assert.Nil(t, err) + assert.True(t, len(stateRoot) > 0) + } + + teardown(model) + }) +} diff --git a/internal/postgres/migrations/202408200934_eigenlayerStateTables/up.go b/internal/postgres/migrations/202408200934_eigenlayerStateTables/up.go index c31d66fc..5fefbe2b 100644 --- a/internal/postgres/migrations/202408200934_eigenlayerStateTables/up.go +++ b/internal/postgres/migrations/202408200934_eigenlayerStateTables/up.go @@ -50,19 +50,6 @@ func (m *Migration) Up(db *sql.DB, grm *gorm.DB) error { `, `create index if not exists idx_staker_share_changes_staker_strat on staker_share_changes (staker, strategy)`, `create index if not exists idx_staker_share_changes_block on staker_share_changes (block_number)`, - `create table if not exists staker_delegation_changes ( - id serial primary key, - staker varchar, - operator varchar, - delegated boolean, - transaction_hash varchar, - log_index bigint, - block_number bigint, - created_at timestamp with time zone default current_timestamp - ); - `, - `create index if not exists idx_staker_delegation_changes_staker_operator on staker_delegation_changes (staker, operator)`, - `create index if not exists idx_staker_delegation_changes_block on staker_delegation_changes (block_number)`, `create table if not exists active_reward_submissions ( id serial primary key, avs varchar, @@ -114,15 +101,7 @@ func (m *Migration) Up(db *sql.DB, grm *gorm.DB) error { `, `create index if not exists idx_staker_shares_staker_strategy on staker_shares (staker, strategy)`, `create index if not exists idx_staker_shares_block on staker_shares (block_number)`, - `create table if not exists delegated_stakers ( - staker varchar, - operator varchar, - block_number bigint, - created_at timestamp with time zone default current_timestamp, - unique(staker, operator, block_number) - )`, - `create index if not exists idx_delegated_stakers_staker_operator on delegated_stakers (staker, operator)`, - `create index if not exists idx_delegated_stakers_block on delegated_stakers (block_number)`, + `create table if not exists active_rewards ( avs varchar, reward_hash varchar, diff --git a/internal/postgres/migrations/202409052151_stakerDelegations/up.go b/internal/postgres/migrations/202409052151_stakerDelegations/up.go new file mode 100644 index 00000000..180a010f --- /dev/null +++ b/internal/postgres/migrations/202409052151_stakerDelegations/up.go @@ -0,0 +1,46 @@ +package _202409052151_stakerDelegations + +import ( + "database/sql" + "gorm.io/gorm" +) + +type Migration struct { +} + +func (m *Migration) Up(db *sql.DB, grm *gorm.DB) error { + queries := []string{ + `create table if not exists staker_delegation_changes ( + staker varchar, + operator varchar, + delegated boolean, + transaction_hash varchar, + log_index bigint, + transaction_index bigint, + block_number bigint, + created_at timestamp with time zone default current_timestamp, + unique(staker, operator, log_index, block_number) + );`, + `create index if not exists idx_staker_delegation_changes_staker_operator on staker_delegation_changes (staker, operator)`, + `create index if not exists idx_staker_delegation_changes_block on staker_delegation_changes (block_number)`, + `create table if not exists delegated_stakers ( + staker varchar, + operator varchar, + block_number bigint, + created_at timestamp with time zone default current_timestamp, + unique(staker, operator, block_number) + )`, + `create index if not exists idx_delegated_stakers_staker_operator on delegated_stakers (staker, operator)`, + `create index if not exists idx_delegated_stakers_block on delegated_stakers (block_number)`, + } + for _, query := range queries { + if _, err := db.Exec(query); err != nil { + return err + } + } + return nil +} + +func (m *Migration) GetName() string { + return "202409052151_stakerDelegations" +} diff --git a/internal/postgres/migrations/migrator.go b/internal/postgres/migrations/migrator.go index 1a939f94..c32f0142 100644 --- a/internal/postgres/migrations/migrator.go +++ b/internal/postgres/migrations/migrator.go @@ -27,6 +27,7 @@ import ( _202407121407_updateProxyContractIndex "github.com/Layr-Labs/sidecar/internal/postgres/migrations/202407121407_updateProxyContractIndex" _202408200934_eigenlayerStateTables "github.com/Layr-Labs/sidecar/internal/postgres/migrations/202408200934_eigenlayerStateTables" _202409051720_operatorShareChanges "github.com/Layr-Labs/sidecar/internal/postgres/migrations/202409051720_operatorShareChanges" + _202409052151_stakerDelegations "github.com/Layr-Labs/sidecar/internal/postgres/migrations/202409052151_stakerDelegations" "go.uber.org/zap" "gorm.io/gorm" "time" @@ -78,6 +79,7 @@ func (m *Migrator) MigrateAll() error { &_202407121407_updateProxyContractIndex.Migration{}, &_202408200934_eigenlayerStateTables.Migration{}, &_202409051720_operatorShareChanges.Migration{}, + &_202409052151_stakerDelegations.Migration{}, } for _, migration := range migrations { diff --git a/internal/storage/tables.go b/internal/storage/tables.go index 0f87e6aa..3f6377d6 100644 --- a/internal/storage/tables.go +++ b/internal/storage/tables.go @@ -34,30 +34,6 @@ type StakerShareChanges struct { CreatedAt time.Time } -/* -create table if not exists staker_delegation_changes ( - - id serial primary key, - staker varchar, - operator varchar, - transaction_hash varchar, - log_index bigint, - block_number bigint - created_at timestamp with time zone - -); -*/ -type StakerDelegationChanges struct { - Id uint64 `gorm:"type:serial"` - Staker string - Operator string - Delegated bool - TransactionHash string - LogIndex uint64 - BlockNumber uint64 - CreatedAt time.Time -} - /* create table if not exists active_reward_submissions ( @@ -160,24 +136,6 @@ type StakerShares struct { CreatedAt time.Time } -/* -create table if not exists delegated_stakers ( - - staker varchar, - operator varchar, - block_number bigint, - created_at timestamp with time zone - unique idx_uniq_delegated_stakers_block (staker, operator, block_number) - -) -*/ -type DelegatedStakers struct { - Staker string - Operator string - BlockNumber uint64 - CreatedAt time.Time -} - /* create table if not exists active_rewards ( diff --git a/sql/schema.sql b/sql/schema.sql index b5353100..d5e7945a 100644 --- a/sql/schema.sql +++ b/sql/schema.sql @@ -139,6 +139,18 @@ CREATE SEQUENCE public.contracts_id_seq ALTER SEQUENCE public.contracts_id_seq OWNED BY public.contracts.id; +-- +-- Name: delegated_stakers; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.delegated_stakers ( + staker character varying, + operator character varying, + block_number bigint, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP +); + + -- -- Name: migrations; Type: TABLE; Schema: public; Owner: - -- @@ -264,6 +276,22 @@ CREATE TABLE public.registered_avs_operators ( ); +-- +-- Name: staker_delegation_changes; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.staker_delegation_changes ( + staker character varying, + operator character varying, + delegated boolean, + transaction_hash character varying, + log_index bigint, + transaction_index bigint, + block_number bigint, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP +); + + -- -- Name: transaction_logs; Type: TABLE; Schema: public; Owner: - -- @@ -385,6 +413,14 @@ ALTER TABLE ONLY public.contracts ADD CONSTRAINT contracts_pkey PRIMARY KEY (contract_address); +-- +-- Name: delegated_stakers delegated_stakers_staker_operator_block_number_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.delegated_stakers + ADD CONSTRAINT delegated_stakers_staker_operator_block_number_key UNIQUE (staker, operator, block_number); + + -- -- Name: migrations migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -425,6 +461,14 @@ ALTER TABLE ONLY public.registered_avs_operators ADD CONSTRAINT registered_avs_operators_operator_avs_block_number_key UNIQUE (operator, avs, block_number); +-- +-- Name: staker_delegation_changes staker_delegation_changes_staker_operator_log_index_block_n_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.staker_delegation_changes + ADD CONSTRAINT staker_delegation_changes_staker_operator_log_index_block_n_key UNIQUE (staker, operator, log_index, block_number); + + -- -- Name: transactions transactions_transaction_hash_sequence_id_key; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -462,6 +506,20 @@ CREATE INDEX idx_avs_operator_changes_block ON public.avs_operator_changes USING CREATE INDEX idx_bytecode_hash ON public.contracts USING btree (bytecode_hash); +-- +-- Name: idx_delegated_stakers_block; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_delegated_stakers_block ON public.delegated_stakers USING btree (block_number); + + +-- +-- Name: idx_delegated_stakers_staker_operator; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_delegated_stakers_staker_operator ON public.delegated_stakers USING btree (staker, operator); + + -- -- Name: idx_operator_share_changes_block; Type: INDEX; Schema: public; Owner: - -- @@ -518,6 +576,20 @@ CREATE INDEX idx_registered_avs_operators_avs_operator ON public.registered_avs_ CREATE INDEX idx_registered_avs_operators_block ON public.registered_avs_operators USING btree (block_number); +-- +-- Name: idx_staker_delegation_changes_block; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_staker_delegation_changes_block ON public.staker_delegation_changes USING btree (block_number); + + +-- +-- Name: idx_staker_delegation_changes_staker_operator; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_staker_delegation_changes_staker_operator ON public.staker_delegation_changes USING btree (staker, operator); + + -- -- Name: idx_transaciton_logs_block_number; Type: INDEX; Schema: public; Owner: - -- @@ -677,6 +749,7 @@ INSERT INTO public.migrations VALUES ('202407111116_addAvsDirectoryAddress', '20 INSERT INTO public.migrations VALUES ('202407121407_updateProxyContractIndex', '2024-07-24 09:13:18.240594-05', NULL); INSERT INTO public.migrations VALUES ('202408200934_eigenlayerStateTables', '2024-09-05 16:16:40.950631-05', NULL); INSERT INTO public.migrations VALUES ('202409051720_operatorShareChanges', '2024-09-05 19:14:07.595987-05', NULL); +INSERT INTO public.migrations VALUES ('202409052151_stakerDelegations', '2024-09-05 22:24:34.016039-05', NULL); --