diff --git a/pkg/eigenState/eigenState.go b/pkg/eigenState/eigenState.go index 0d441810..fdca74d4 100644 --- a/pkg/eigenState/eigenState.go +++ b/pkg/eigenState/eigenState.go @@ -6,8 +6,12 @@ import ( "github.com/Layr-Labs/sidecar/pkg/eigenState/defaultOperatorSplits" "github.com/Layr-Labs/sidecar/pkg/eigenState/disabledDistributionRoots" "github.com/Layr-Labs/sidecar/pkg/eigenState/operatorAVSSplits" + "github.com/Layr-Labs/sidecar/pkg/eigenState/operatorDirectedOperatorSetRewardSubmissions" "github.com/Layr-Labs/sidecar/pkg/eigenState/operatorDirectedRewardSubmissions" "github.com/Layr-Labs/sidecar/pkg/eigenState/operatorPISplits" + "github.com/Layr-Labs/sidecar/pkg/eigenState/operatorSetOperatorRegistrations" + "github.com/Layr-Labs/sidecar/pkg/eigenState/operatorSetSplits" + "github.com/Layr-Labs/sidecar/pkg/eigenState/operatorSetStrategyRegistrations" "github.com/Layr-Labs/sidecar/pkg/eigenState/operatorShares" "github.com/Layr-Labs/sidecar/pkg/eigenState/rewardSubmissions" "github.com/Layr-Labs/sidecar/pkg/eigenState/stakerDelegations" @@ -68,5 +72,21 @@ func LoadEigenStateModels( l.Sugar().Errorw("Failed to create DefaultOperatorSplitModel", zap.Error(err)) return err } + if _, err := operatorDirectedOperatorSetRewardSubmissions.NewOperatorDirectedOperatorSetRewardSubmissionsModel(sm, grm, l, cfg); err != nil { + l.Sugar().Errorw("Failed to create OperatorDirectedOperatorSetRewardSubmissionsModel", zap.Error(err)) + return err + } + if _, err := operatorSetSplits.NewOperatorSetSplitModel(sm, grm, l, cfg); err != nil { + l.Sugar().Errorw("Failed to create OperatorSetSplitModel", zap.Error(err)) + return err + } + if _, err := operatorSetOperatorRegistrations.NewOperatorSetOperatorRegistrationModel(sm, grm, l, cfg); err != nil { + l.Sugar().Errorw("Failed to create OperatorSetOperatorRegistrationModel", zap.Error(err)) + return err + } + if _, err := operatorSetStrategyRegistrations.NewOperatorSetStrategyRegistrationModel(sm, grm, l, cfg); err != nil { + l.Sugar().Errorw("Failed to create OperatorSetStrategyRegistrationModel", zap.Error(err)) + return err + } return nil } diff --git a/pkg/eigenState/operatorDirectedOperatorSetRewardSubmissions/operatorDirectedOperatorSetRewardSubmissions.go b/pkg/eigenState/operatorDirectedOperatorSetRewardSubmissions/operatorDirectedOperatorSetRewardSubmissions.go new file mode 100644 index 00000000..4097c61a --- /dev/null +++ b/pkg/eigenState/operatorDirectedOperatorSetRewardSubmissions/operatorDirectedOperatorSetRewardSubmissions.go @@ -0,0 +1,439 @@ +package operatorDirectedOperatorSetRewardSubmissions + +import ( + "encoding/json" + "fmt" + "math/big" + "slices" + "sort" + "strings" + "time" + + "github.com/Layr-Labs/sidecar/internal/config" + "github.com/Layr-Labs/sidecar/pkg/eigenState/base" + "github.com/Layr-Labs/sidecar/pkg/eigenState/stateManager" + "github.com/Layr-Labs/sidecar/pkg/eigenState/types" + "github.com/Layr-Labs/sidecar/pkg/storage" + "github.com/Layr-Labs/sidecar/pkg/types/numbers" + "go.uber.org/zap" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +type OperatorDirectedOperatorSetRewardSubmission struct { + Avs string + OperatorSetId uint64 + RewardHash string + Token string + Operator string + OperatorIndex uint64 + Amount string + Strategy string + StrategyIndex uint64 + Multiplier string + StartTimestamp *time.Time + EndTimestamp *time.Time + Duration uint64 + Description string + BlockNumber uint64 + TransactionHash string + LogIndex uint64 +} + +type OperatorDirectedOperatorSetRewardSubmissionsModel struct { + base.BaseEigenState + StateTransitions types.StateTransitions[[]*OperatorDirectedOperatorSetRewardSubmission] + DB *gorm.DB + Network config.Network + Environment config.Environment + logger *zap.Logger + globalConfig *config.Config + + // Accumulates state changes for SlotIds, grouped by block number + stateAccumulator map[uint64]map[types.SlotID]*OperatorDirectedOperatorSetRewardSubmission + committedState map[uint64][]*OperatorDirectedOperatorSetRewardSubmission +} + +func NewOperatorDirectedOperatorSetRewardSubmissionsModel( + esm *stateManager.EigenStateManager, + grm *gorm.DB, + logger *zap.Logger, + globalConfig *config.Config, +) (*OperatorDirectedOperatorSetRewardSubmissionsModel, error) { + model := &OperatorDirectedOperatorSetRewardSubmissionsModel{ + BaseEigenState: base.BaseEigenState{ + Logger: logger, + }, + DB: grm, + logger: logger, + globalConfig: globalConfig, + stateAccumulator: make(map[uint64]map[types.SlotID]*OperatorDirectedOperatorSetRewardSubmission), + committedState: make(map[uint64][]*OperatorDirectedOperatorSetRewardSubmission), + } + + esm.RegisterState(model, 11) + return model, nil +} + +func (od *OperatorDirectedOperatorSetRewardSubmissionsModel) GetModelName() string { + return "OperatorDirectedOperatorSetRewardSubmissionsModel" +} + +func (od *OperatorDirectedOperatorSetRewardSubmissionsModel) NewSlotID( + transactionHash string, + logIndex uint64, + rewardHash string, + strategyIndex uint64, + operatorIndex uint64, +) (types.SlotID, error) { + return base.NewSlotIDWithSuffix(transactionHash, logIndex, fmt.Sprintf("%s_%016x_%016x", rewardHash, strategyIndex, operatorIndex)), nil +} + +type operatorDirectedRewardData struct { + StrategiesAndMultipliers []struct { + Strategy string `json:"strategy"` + Multiplier json.Number `json:"multiplier"` + } `json:"strategiesAndMultipliers"` + Token string `json:"token"` + OperatorRewards []struct { + Operator string `json:"operator"` + Amount json.Number `json:"amount"` + } `json:"operatorRewards"` + StartTimestamp uint64 `json:"startTimestamp"` + Duration uint64 `json:"duration"` + Description string `json:"description"` +} + +type OperatorSet struct { + Avs string `json:"avs"` + Id uint64 `json:"id"` +} + +type operatorDirectedRewardSubmissionOutputData struct { + OperatorSet *OperatorSet `json:"operatorSet"` + SubmissionNonce json.Number `json:"submissionNonce"` + OperatorDirectedRewardsSubmission *operatorDirectedRewardData `json:"operatorDirectedRewardsSubmission"` +} + +func parseRewardSubmissionOutputData(outputDataStr string) (*operatorDirectedRewardSubmissionOutputData, error) { + outputData := &operatorDirectedRewardSubmissionOutputData{} + decoder := json.NewDecoder(strings.NewReader(outputDataStr)) + decoder.UseNumber() + + err := decoder.Decode(&outputData) + if err != nil { + return nil, err + } + + return outputData, err +} + +func (od *OperatorDirectedOperatorSetRewardSubmissionsModel) handleOperatorDirectedOperatorSetRewardSubmissionCreatedEvent(log *storage.TransactionLog) ([]*OperatorDirectedOperatorSetRewardSubmission, error) { + arguments, err := od.ParseLogArguments(log) + if err != nil { + return nil, err + } + + outputData, err := parseRewardSubmissionOutputData(log.OutputData) + if err != nil { + return nil, err + } + outputRewardData := outputData.OperatorDirectedRewardsSubmission + + rewardSubmissions := make([]*OperatorDirectedOperatorSetRewardSubmission, 0) + + for i, strategyAndMultiplier := range outputRewardData.StrategiesAndMultipliers { + startTimestamp := time.Unix(int64(outputRewardData.StartTimestamp), 0) + endTimestamp := startTimestamp.Add(time.Duration(outputRewardData.Duration) * time.Second) + + multiplierBig, success := numbers.NewBig257().SetString(strategyAndMultiplier.Multiplier.String(), 10) + if !success { + return nil, fmt.Errorf("Failed to parse multiplier to Big257: %s", strategyAndMultiplier.Multiplier.String()) + } + + for j, operatorReward := range outputRewardData.OperatorRewards { + amountBig, success := numbers.NewBig257().SetString(operatorReward.Amount.String(), 10) + if !success { + return nil, fmt.Errorf("Failed to parse amount to Big257: %s", operatorReward.Amount.String()) + } + + rewardSubmission := &OperatorDirectedOperatorSetRewardSubmission{ + Avs: strings.ToLower(outputData.OperatorSet.Avs), + OperatorSetId: uint64(outputData.OperatorSet.Id), + RewardHash: strings.ToLower(arguments[1].Value.(string)), + Token: strings.ToLower(outputRewardData.Token), + Operator: strings.ToLower(operatorReward.Operator), + OperatorIndex: uint64(j), + Amount: amountBig.String(), + Strategy: strings.ToLower(strategyAndMultiplier.Strategy), + StrategyIndex: uint64(i), + Multiplier: multiplierBig.String(), + StartTimestamp: &startTimestamp, + EndTimestamp: &endTimestamp, + Duration: outputRewardData.Duration, + Description: outputRewardData.Description, + BlockNumber: log.BlockNumber, + TransactionHash: log.TransactionHash, + LogIndex: log.LogIndex, + } + + rewardSubmissions = append(rewardSubmissions, rewardSubmission) + } + } + + return rewardSubmissions, nil +} + +func (od *OperatorDirectedOperatorSetRewardSubmissionsModel) GetStateTransitions() (types.StateTransitions[[]*OperatorDirectedOperatorSetRewardSubmission], []uint64) { + stateChanges := make(types.StateTransitions[[]*OperatorDirectedOperatorSetRewardSubmission]) + + stateChanges[0] = func(log *storage.TransactionLog) ([]*OperatorDirectedOperatorSetRewardSubmission, error) { + rewardSubmissions, err := od.handleOperatorDirectedOperatorSetRewardSubmissionCreatedEvent(log) + if err != nil { + return nil, err + } + + for _, rewardSubmission := range rewardSubmissions { + slotId, err := od.NewSlotID( + rewardSubmission.TransactionHash, + rewardSubmission.LogIndex, + rewardSubmission.RewardHash, + rewardSubmission.StrategyIndex, + rewardSubmission.OperatorIndex, + ) + if err != nil { + od.logger.Sugar().Errorw("Failed to create slot ID", + zap.String("transactionHash", log.TransactionHash), + zap.Uint64("logIndex", log.LogIndex), + zap.String("rewardHash", rewardSubmission.RewardHash), + zap.Uint64("strategyIndex", rewardSubmission.StrategyIndex), + zap.Uint64("operatorIndex", rewardSubmission.OperatorIndex), + zap.Error(err), + ) + return nil, err + } + + _, ok := od.stateAccumulator[log.BlockNumber][slotId] + if ok { + err := fmt.Errorf("Duplicate operator directed operator set reward submission submitted for slot %s at block %d", slotId, log.BlockNumber) + od.logger.Sugar().Errorw("Duplicate operator directed operator set reward submission submitted", zap.Error(err)) + return nil, err + } + + od.stateAccumulator[log.BlockNumber][slotId] = rewardSubmission + } + + return rewardSubmissions, 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 (od *OperatorDirectedOperatorSetRewardSubmissionsModel) getContractAddressesForEnvironment() map[string][]string { + contracts := od.globalConfig.GetContractsMapForChain() + return map[string][]string{ + contracts.RewardsCoordinator: { + "OperatorDirectedOperatorSetRewardsSubmissionCreated", + }, + } +} + +func (od *OperatorDirectedOperatorSetRewardSubmissionsModel) IsInterestingLog(log *storage.TransactionLog) bool { + addresses := od.getContractAddressesForEnvironment() + return od.BaseEigenState.IsInterestingLog(addresses, log) +} + +func (od *OperatorDirectedOperatorSetRewardSubmissionsModel) SetupStateForBlock(blockNumber uint64) error { + od.stateAccumulator[blockNumber] = make(map[types.SlotID]*OperatorDirectedOperatorSetRewardSubmission) + od.committedState[blockNumber] = make([]*OperatorDirectedOperatorSetRewardSubmission, 0) + return nil +} + +func (od *OperatorDirectedOperatorSetRewardSubmissionsModel) CleanupProcessedStateForBlock(blockNumber uint64) error { + delete(od.stateAccumulator, blockNumber) + delete(od.committedState, blockNumber) + return nil +} + +func (od *OperatorDirectedOperatorSetRewardSubmissionsModel) HandleStateChange(log *storage.TransactionLog) (interface{}, error) { + stateChanges, sortedBlockNumbers := od.GetStateTransitions() + + for _, blockNumber := range sortedBlockNumbers { + if log.BlockNumber >= blockNumber { + od.logger.Sugar().Debugw("Handling state change", zap.Uint64("blockNumber", log.BlockNumber)) + + change, err := stateChanges[blockNumber](log) + if err != nil { + return nil, err + } + if change == nil { + return nil, nil + } + return change, nil + } + } + return nil, nil +} + +// prepareState prepares the state for commit by adding the new state to the existing state. +func (od *OperatorDirectedOperatorSetRewardSubmissionsModel) prepareState(blockNumber uint64) ([]*OperatorDirectedOperatorSetRewardSubmission, error) { + accumulatedState, ok := od.stateAccumulator[blockNumber] + if !ok { + err := fmt.Errorf("No accumulated state found for block %d", blockNumber) + od.logger.Sugar().Errorw(err.Error(), zap.Error(err), zap.Uint64("blockNumber", blockNumber)) + return nil, err + } + + recordsToInsert := make([]*OperatorDirectedOperatorSetRewardSubmission, 0) + for _, submission := range accumulatedState { + recordsToInsert = append(recordsToInsert, submission) + } + return recordsToInsert, nil +} + +// CommitFinalState commits the final state for the given block number. +func (od *OperatorDirectedOperatorSetRewardSubmissionsModel) CommitFinalState(blockNumber uint64) error { + recordsToInsert, err := od.prepareState(blockNumber) + if err != nil { + return err + } + + if len(recordsToInsert) > 0 { + for _, record := range recordsToInsert { + res := od.DB.Model(&OperatorDirectedOperatorSetRewardSubmission{}).Clauses(clause.Returning{}).Create(&record) + if res.Error != nil { + od.logger.Sugar().Errorw("Failed to insert records", zap.Error(res.Error)) + return res.Error + } + } + } + od.committedState[blockNumber] = recordsToInsert + return nil +} + +// GenerateStateRoot generates the state root for the given block number using the results of the state changes. +func (od *OperatorDirectedOperatorSetRewardSubmissionsModel) GenerateStateRoot(blockNumber uint64) ([]byte, error) { + inserts, err := od.prepareState(blockNumber) + if err != nil { + return nil, err + } + + inputs, err := od.sortValuesForMerkleTree(inserts) + if err != nil { + return nil, err + } + + if len(inputs) == 0 { + return nil, nil + } + + fullTree, err := od.MerkleizeEigenState(blockNumber, inputs) + if err != nil { + od.logger.Sugar().Errorw("Failed to create merkle tree", + zap.Error(err), + zap.Uint64("blockNumber", blockNumber), + zap.Any("inputs", inputs), + ) + return nil, err + } + return fullTree.Root(), nil +} + +func (od *OperatorDirectedOperatorSetRewardSubmissionsModel) GetCommittedState(blockNumber uint64) ([]interface{}, error) { + records, ok := od.committedState[blockNumber] + if !ok { + err := fmt.Errorf("No committed state found for block %d", blockNumber) + od.logger.Sugar().Errorw(err.Error(), zap.Error(err), zap.Uint64("blockNumber", blockNumber)) + return nil, err + } + return base.CastCommittedStateToInterface(records), nil +} + +func (od *OperatorDirectedOperatorSetRewardSubmissionsModel) formatMerkleLeafValue( + rewardHash string, + strategy string, + multiplier string, + operator string, + amount string, +) (string, error) { + // Multiplier is a uint96 in the contracts, which translates to 24 hex characters + // Amount is a uint256 in the contracts, which translates to 64 hex characters + multiplierBig, success := new(big.Int).SetString(multiplier, 10) + if !success { + return "", fmt.Errorf("failed to parse multiplier to BigInt: %s", multiplier) + } + + amountBig, success := new(big.Int).SetString(amount, 10) + if !success { + return "", fmt.Errorf("failed to parse amount to BigInt: %s", amount) + } + + return fmt.Sprintf("%s_%s_%024x_%s_%064x", rewardHash, strategy, multiplierBig, operator, amountBig), nil +} + +func (od *OperatorDirectedOperatorSetRewardSubmissionsModel) sortValuesForMerkleTree(submissions []*OperatorDirectedOperatorSetRewardSubmission) ([]*base.MerkleTreeInput, error) { + inputs := make([]*base.MerkleTreeInput, 0) + for _, submission := range submissions { + slotID, err := od.NewSlotID( + submission.TransactionHash, + submission.LogIndex, + submission.RewardHash, + submission.StrategyIndex, + submission.OperatorIndex, + ) + if err != nil { + od.logger.Sugar().Errorw("Failed to create slot ID", + zap.String("transactionHash", submission.TransactionHash), + zap.Uint64("logIndex", submission.LogIndex), + zap.String("rewardHash", submission.RewardHash), + zap.Uint64("strategyIndex", submission.StrategyIndex), + zap.Uint64("operatorIndex", submission.OperatorIndex), + zap.Error(err), + ) + return nil, err + } + + value, err := od.formatMerkleLeafValue( + submission.RewardHash, + submission.Strategy, + submission.Multiplier, + submission.Operator, + submission.Amount, + ) + if err != nil { + od.Logger.Sugar().Errorw("Failed to format merkle leaf value", + zap.Error(err), + zap.String("rewardHash", submission.RewardHash), + zap.String("strategy", submission.Strategy), + zap.String("multiplier", submission.Multiplier), + zap.String("operator", submission.Operator), + zap.String("amount", submission.Amount), + ) + return nil, err + } + inputs = append(inputs, &base.MerkleTreeInput{ + SlotID: slotID, + Value: []byte(value), + }) + } + + slices.SortFunc(inputs, func(i, j *base.MerkleTreeInput) int { + return strings.Compare(string(i.SlotID), string(j.SlotID)) + }) + + return inputs, nil +} + +func (od *OperatorDirectedOperatorSetRewardSubmissionsModel) DeleteState(startBlockNumber uint64, endBlockNumber uint64) error { + return od.BaseEigenState.DeleteState("operator_directed_operator_set_reward_submissions", startBlockNumber, endBlockNumber, od.DB) +} diff --git a/pkg/eigenState/operatorDirectedOperatorSetRewardSubmissions/operatorDirectedOperatorSetRewardSubmissions_test.go b/pkg/eigenState/operatorDirectedOperatorSetRewardSubmissions/operatorDirectedOperatorSetRewardSubmissions_test.go new file mode 100644 index 00000000..665471dc --- /dev/null +++ b/pkg/eigenState/operatorDirectedOperatorSetRewardSubmissions/operatorDirectedOperatorSetRewardSubmissions_test.go @@ -0,0 +1,156 @@ +package operatorDirectedOperatorSetRewardSubmissions + +import ( + "math/big" + "os" + "strings" + "testing" + "time" + + "github.com/Layr-Labs/sidecar/pkg/postgres" + "github.com/Layr-Labs/sidecar/pkg/storage" + + "github.com/Layr-Labs/sidecar/internal/config" + "github.com/Layr-Labs/sidecar/internal/logger" + "github.com/Layr-Labs/sidecar/internal/tests" + "github.com/Layr-Labs/sidecar/pkg/eigenState/stateManager" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + "gorm.io/gorm" +) + +func setup() ( + string, + *gorm.DB, + *zap.Logger, + *config.Config, + error, +) { + cfg := config.NewConfig() + cfg.Debug = os.Getenv(config.Debug) == "true" + cfg.DatabaseConfig = *tests.GetDbConfigFromEnv() + + l, _ := logger.NewLogger(&logger.LoggerConfig{Debug: cfg.Debug}) + + dbname, _, grm, err := postgres.GetTestPostgresDatabase(cfg.DatabaseConfig, cfg, l) + if err != nil { + return dbname, nil, nil, nil, err + } + + return dbname, grm, l, cfg, nil +} + +func createBlock(model *OperatorDirectedOperatorSetRewardSubmissionsModel, blockNumber uint64) error { + block := &storage.Block{ + Number: blockNumber, + Hash: "some hash", + BlockTime: time.Now().Add(time.Hour * time.Duration(blockNumber)), + } + res := model.DB.Model(&storage.Block{}).Create(block) + if res.Error != nil { + return res.Error + } + return nil +} + +func Test_OperatorDirectedOperatorSetRewardSubmissions(t *testing.T) { + dbName, grm, l, cfg, err := setup() + + if err != nil { + t.Fatal(err) + } + + t.Run("Test each event type", func(t *testing.T) { + esm := stateManager.NewEigenStateManager(l, grm) + + model, err := NewOperatorDirectedOperatorSetRewardSubmissionsModel(esm, grm, l, cfg) + + submissionCounter := 0 + + t.Run("Handle an operator directed operator set reward submission", func(t *testing.T) { + blockNumber := uint64(102) + + if err := createBlock(model, blockNumber); err != nil { + t.Fatal(err) + } + + log := &storage.TransactionLog{ + TransactionHash: "some hash", + TransactionIndex: big.NewInt(100).Uint64(), + BlockNumber: blockNumber, + Address: cfg.GetContractsMapForChain().RewardsCoordinator, + Arguments: `[{"Name": "caller", "Type": "address", "Value": "0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101", "Indexed": true}, {"Name": "operatorDirectedRewardsSubmissionHash", "Type": "bytes32", "Value": "0x7402669fb2c8a0cfe8108acb8a0070257c77ec6906ecb07d97c38e8a5ddc66a9", "Indexed": true}, {"Name": "operatorSet", "Type": "tuple", "Value": {"avs": "0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101", "id": 1}, "Indexed": false}, {"Name": "submissionNonce", "Type": "uint256", "Value": 0, "Indexed": false}, {"Name": "operatorDirectedRewardsSubmission", "Type": "((address,uint96)[],address,(address,uint256)[],uint32,uint32,string)", "Value": null, "Indexed": false}]`, + EventName: "OperatorDirectedOperatorSetRewardsSubmissionCreated", + LogIndex: big.NewInt(12).Uint64(), + OutputData: `{"operatorSet": {"avs": "0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101", "id": 1}, "submissionNonce": 0, "operatorDirectedRewardsSubmission": {"token": "0x0ddd9dc88e638aef6a8e42d0c98aaa6a48a98d24", "operatorRewards": [{"operator": "0x9401E5E6564DB35C0f86573a9828DF69Fc778aF1", "amount": 30000000000000000000000}, {"operator": "0xF50Cba7a66b5E615587157e43286DaA7aF94009e", "amount": 40000000000000000000000}], "duration": 2419200, "startTimestamp": 1725494400, "strategiesAndMultipliers": [{"strategy": "0x5074dfd18e9498d9e006fb8d4f3fecdc9af90a2c", "multiplier": 1000000000000000000}, {"strategy": "0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc", "multiplier": 2000000000000000000}], "description": "test reward submission"}}`, + } + + err = model.SetupStateForBlock(blockNumber) + assert.Nil(t, err) + + isInteresting := model.IsInterestingLog(log) + assert.True(t, isInteresting) + + change, err := model.HandleStateChange(log) + assert.Nil(t, err) + assert.NotNil(t, change) + + strategiesAndMultipliers := []struct { + Strategy string + Multiplier string + }{ + {"0x5074dfd18e9498d9e006fb8d4f3fecdc9af90a2c", "1000000000000000000"}, + {"0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc", "2000000000000000000"}, + } + + operatorRewards := []struct { + Operator string + Amount string + }{ + {"0x9401E5E6564DB35C0f86573a9828DF69Fc778aF1", "30000000000000000000000"}, + {"0xF50Cba7a66b5E615587157e43286DaA7aF94009e", "40000000000000000000000"}, + } + + typedChange := change.([]*OperatorDirectedOperatorSetRewardSubmission) + assert.Equal(t, len(strategiesAndMultipliers)*len(operatorRewards), len(typedChange)) + + for _, submission := range typedChange { + assert.Equal(t, strings.ToLower("0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101"), strings.ToLower(submission.Avs)) + assert.Equal(t, uint64(1), submission.OperatorSetId) + assert.Equal(t, strings.ToLower("0x0ddd9dc88e638aef6a8e42d0c98aaa6a48a98d24"), strings.ToLower(submission.Token)) + assert.Equal(t, strings.ToLower("0x7402669fb2c8a0cfe8108acb8a0070257c77ec6906ecb07d97c38e8a5ddc66a9"), strings.ToLower(submission.RewardHash)) + assert.Equal(t, uint64(2419200), submission.Duration) + assert.Equal(t, int64(1725494400), submission.StartTimestamp.Unix()) + assert.Equal(t, int64(2419200+1725494400), submission.EndTimestamp.Unix()) + + assert.Equal(t, strings.ToLower(strategiesAndMultipliers[submission.StrategyIndex].Strategy), strings.ToLower(submission.Strategy)) + assert.Equal(t, strategiesAndMultipliers[submission.StrategyIndex].Multiplier, submission.Multiplier) + + assert.Equal(t, strings.ToLower(operatorRewards[submission.OperatorIndex].Operator), strings.ToLower(submission.Operator)) + assert.Equal(t, operatorRewards[submission.OperatorIndex].Amount, submission.Amount) + + assert.Equal(t, "test reward submission", submission.Description) + } + + err = model.CommitFinalState(blockNumber) + assert.Nil(t, err) + + rewards := make([]*OperatorDirectedOperatorSetRewardSubmission, 0) + query := `select * from operator_directed_operator_set_reward_submissions where block_number = ?` + res := model.DB.Raw(query, blockNumber).Scan(&rewards) + assert.Nil(t, res.Error) + assert.Equal(t, len(strategiesAndMultipliers)*len(operatorRewards), len(rewards)) + + submissionCounter += len(strategiesAndMultipliers) * len(operatorRewards) + + stateRoot, err := model.GenerateStateRoot(blockNumber) + assert.Nil(t, err) + assert.NotNil(t, stateRoot) + assert.True(t, len(stateRoot) > 0) + }) + }) + + t.Cleanup(func() { + postgres.TeardownTestDatabase(dbName, cfg, grm, l) + }) +} diff --git a/pkg/eigenState/operatorSetOperatorRegistrations/operatorSetOperatorRegistrations.go b/pkg/eigenState/operatorSetOperatorRegistrations/operatorSetOperatorRegistrations.go new file mode 100644 index 00000000..44886aa8 --- /dev/null +++ b/pkg/eigenState/operatorSetOperatorRegistrations/operatorSetOperatorRegistrations.go @@ -0,0 +1,321 @@ +package operatorSetOperatorRegistrations + +import ( + "encoding/json" + "fmt" + "slices" + "sort" + "strings" + + "github.com/Layr-Labs/sidecar/internal/config" + "github.com/Layr-Labs/sidecar/pkg/eigenState/base" + "github.com/Layr-Labs/sidecar/pkg/eigenState/stateManager" + "github.com/Layr-Labs/sidecar/pkg/eigenState/types" + "github.com/Layr-Labs/sidecar/pkg/storage" + "go.uber.org/zap" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +type OperatorSetOperatorRegistration struct { + Operator string + Avs string + OperatorSetId uint64 + IsActive bool + BlockNumber uint64 + TransactionHash string + LogIndex uint64 +} + +type OperatorSetOperatorRegistrationModel struct { + base.BaseEigenState + StateTransitions types.StateTransitions[[]*OperatorSetOperatorRegistration] + DB *gorm.DB + Network config.Network + Environment config.Environment + logger *zap.Logger + globalConfig *config.Config + + // Accumulates state changes for SlotIds, grouped by block number + stateAccumulator map[uint64]map[types.SlotID]*OperatorSetOperatorRegistration + committedState map[uint64][]*OperatorSetOperatorRegistration +} + +func NewOperatorSetOperatorRegistrationModel( + esm *stateManager.EigenStateManager, + grm *gorm.DB, + logger *zap.Logger, + globalConfig *config.Config, +) (*OperatorSetOperatorRegistrationModel, error) { + model := &OperatorSetOperatorRegistrationModel{ + BaseEigenState: base.BaseEigenState{ + Logger: logger, + }, + DB: grm, + logger: logger, + globalConfig: globalConfig, + stateAccumulator: make(map[uint64]map[types.SlotID]*OperatorSetOperatorRegistration), + committedState: make(map[uint64][]*OperatorSetOperatorRegistration), + } + + esm.RegisterState(model, 13) + return model, nil +} + +func (osor *OperatorSetOperatorRegistrationModel) GetModelName() string { + return "OperatorSetOperatorRegistrationModel" +} + +type operatorSetOperatorRegistrationOutputData struct { + OperatorSet *OperatorSet `json:"operatorSet"` +} + +type OperatorSet struct { + Avs string `json:"avs"` + Id uint64 `json:"id"` +} + +func parseOperatorSetOperatorRegistrationOutputData(outputDataStr string) (*operatorSetOperatorRegistrationOutputData, error) { + outputData := &operatorSetOperatorRegistrationOutputData{} + decoder := json.NewDecoder(strings.NewReader(outputDataStr)) + decoder.UseNumber() + + err := decoder.Decode(&outputData) + if err != nil { + return nil, err + } + + return outputData, err +} + +func (osor *OperatorSetOperatorRegistrationModel) handleOperatorSetOperatorRegistrationEvent(log *storage.TransactionLog) (*OperatorSetOperatorRegistration, error) { + arguments, err := osor.ParseLogArguments(log) + if err != nil { + return nil, err + } + + outputData, err := parseOperatorSetOperatorRegistrationOutputData(log.OutputData) + if err != nil { + return nil, err + } + + isActive := true // default to true for "OperatorAddedToOperatorSet" event + if log.EventName == "OperatorRemovedFromOperatorSet" { + isActive = false + } + + operatorRegistration := &OperatorSetOperatorRegistration{ + Operator: strings.ToLower(arguments[0].Value.(string)), + Avs: outputData.OperatorSet.Avs, + OperatorSetId: outputData.OperatorSet.Id, + IsActive: isActive, + BlockNumber: log.BlockNumber, + TransactionHash: log.TransactionHash, + LogIndex: log.LogIndex, + } + + return operatorRegistration, nil +} + +func (osor *OperatorSetOperatorRegistrationModel) GetStateTransitions() (types.StateTransitions[*OperatorSetOperatorRegistration], []uint64) { + stateChanges := make(types.StateTransitions[*OperatorSetOperatorRegistration]) + + stateChanges[0] = func(log *storage.TransactionLog) (*OperatorSetOperatorRegistration, error) { + operatorSetOperatorRegistration, err := osor.handleOperatorSetOperatorRegistrationEvent(log) + if err != nil { + return nil, err + } + + slotId := base.NewSlotID(operatorSetOperatorRegistration.TransactionHash, operatorSetOperatorRegistration.LogIndex) + + _, ok := osor.stateAccumulator[log.BlockNumber][slotId] + if ok { + err := fmt.Errorf("Duplicate operator set operator registration submitted for slot %s at block %d", slotId, log.BlockNumber) + osor.logger.Sugar().Errorw("Duplicate operator set operator registration submitted", zap.Error(err)) + return nil, err + } + + osor.stateAccumulator[log.BlockNumber][slotId] = operatorSetOperatorRegistration + + return operatorSetOperatorRegistration, 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 (osor *OperatorSetOperatorRegistrationModel) getContractAddressesForEnvironment() map[string][]string { + contracts := osor.globalConfig.GetContractsMapForChain() + return map[string][]string{ + contracts.AllocationManager: { + "OperatorAddedToOperatorSet", + "OperatorRemovedFromOperatorSet", + }, + } +} + +func (osor *OperatorSetOperatorRegistrationModel) IsInterestingLog(log *storage.TransactionLog) bool { + addresses := osor.getContractAddressesForEnvironment() + return osor.BaseEigenState.IsInterestingLog(addresses, log) +} + +func (osor *OperatorSetOperatorRegistrationModel) SetupStateForBlock(blockNumber uint64) error { + osor.stateAccumulator[blockNumber] = make(map[types.SlotID]*OperatorSetOperatorRegistration) + osor.committedState[blockNumber] = make([]*OperatorSetOperatorRegistration, 0) + return nil +} + +func (osor *OperatorSetOperatorRegistrationModel) CleanupProcessedStateForBlock(blockNumber uint64) error { + delete(osor.stateAccumulator, blockNumber) + delete(osor.committedState, blockNumber) + return nil +} + +func (osor *OperatorSetOperatorRegistrationModel) HandleStateChange(log *storage.TransactionLog) (interface{}, error) { + stateChanges, sortedBlockNumbers := osor.GetStateTransitions() + + for _, blockNumber := range sortedBlockNumbers { + if log.BlockNumber >= blockNumber { + osor.logger.Sugar().Debugw("Handling state change", zap.Uint64("blockNumber", log.BlockNumber)) + + change, err := stateChanges[blockNumber](log) + if err != nil { + return nil, err + } + if change == nil { + return nil, nil + } + return change, nil + } + } + return nil, nil +} + +// prepareState prepares the state for commit by adding the new state to the existing state. +func (osor *OperatorSetOperatorRegistrationModel) prepareState(blockNumber uint64) ([]*OperatorSetOperatorRegistration, error) { + accumulatedState, ok := osor.stateAccumulator[blockNumber] + if !ok { + err := fmt.Errorf("No accumulated state found for block %d", blockNumber) + osor.logger.Sugar().Errorw(err.Error(), zap.Error(err), zap.Uint64("blockNumber", blockNumber)) + return nil, err + } + + recordsToInsert := make([]*OperatorSetOperatorRegistration, 0) + for _, split := range accumulatedState { + recordsToInsert = append(recordsToInsert, split) + } + return recordsToInsert, nil +} + +// CommitFinalState commits the final state for the given block number. +func (osor *OperatorSetOperatorRegistrationModel) CommitFinalState(blockNumber uint64) error { + recordsToInsert, err := osor.prepareState(blockNumber) + if err != nil { + return err + } + + if len(recordsToInsert) > 0 { + for _, record := range recordsToInsert { + res := osor.DB.Model(&OperatorSetOperatorRegistration{}).Clauses(clause.Returning{}).Create(&record) + if res.Error != nil { + osor.logger.Sugar().Errorw("Failed to insert records", zap.Error(res.Error)) + return res.Error + } + } + } + osor.committedState[blockNumber] = recordsToInsert + return nil +} + +// GenerateStateRoot generates the state root for the given block number using the results of the state changes. +func (osor *OperatorSetOperatorRegistrationModel) GenerateStateRoot(blockNumber uint64) ([]byte, error) { + inserts, err := osor.prepareState(blockNumber) + if err != nil { + return nil, err + } + + inputs, err := osor.sortValuesForMerkleTree(inserts) + if err != nil { + return nil, err + } + + if len(inputs) == 0 { + return nil, nil + } + + fullTree, err := osor.MerkleizeEigenState(blockNumber, inputs) + if err != nil { + osor.logger.Sugar().Errorw("Failed to create merkle tree", + zap.Error(err), + zap.Uint64("blockNumber", blockNumber), + zap.Any("inputs", inputs), + ) + return nil, err + } + return fullTree.Root(), nil +} + +func (osor *OperatorSetOperatorRegistrationModel) GetCommittedState(blockNumber uint64) ([]interface{}, error) { + records, ok := osor.committedState[blockNumber] + if !ok { + err := fmt.Errorf("No committed state found for block %d", blockNumber) + osor.logger.Sugar().Errorw(err.Error(), zap.Error(err), zap.Uint64("blockNumber", blockNumber)) + return nil, err + } + return base.CastCommittedStateToInterface(records), nil +} + +func (osor *OperatorSetOperatorRegistrationModel) formatMerkleLeafValue( + operator string, + avs string, + operatorSetId uint64, + isActive bool, +) (string, error) { + return fmt.Sprintf("%s_%s_%016x_%t", operator, avs, operatorSetId, isActive), nil +} + +func (osor *OperatorSetOperatorRegistrationModel) sortValuesForMerkleTree(operatorRegistrations []*OperatorSetOperatorRegistration) ([]*base.MerkleTreeInput, error) { + inputs := make([]*base.MerkleTreeInput, 0) + for _, operatorRegistration := range operatorRegistrations { + slotID := base.NewSlotID(operatorRegistration.TransactionHash, operatorRegistration.LogIndex) + value, err := osor.formatMerkleLeafValue( + operatorRegistration.Operator, + operatorRegistration.Avs, + operatorRegistration.OperatorSetId, + operatorRegistration.IsActive, + ) + if err != nil { + osor.logger.Sugar().Errorw("Failed to format merkle leaf value", + zap.Error(err), + zap.String("operator", operatorRegistration.Operator), + zap.String("avs", operatorRegistration.Avs), + zap.Uint64("operatorSetId", operatorRegistration.OperatorSetId), + zap.Bool("isActive", operatorRegistration.IsActive), + ) + return nil, err + } + inputs = append(inputs, &base.MerkleTreeInput{ + SlotID: slotID, + Value: []byte(value), + }) + } + + slices.SortFunc(inputs, func(i, j *base.MerkleTreeInput) int { + return strings.Compare(string(i.SlotID), string(j.SlotID)) + }) + + return inputs, nil +} + +func (osor *OperatorSetOperatorRegistrationModel) DeleteState(startBlockNumber uint64, endBlockNumber uint64) error { + return osor.BaseEigenState.DeleteState("operator_set_operator_registrations", startBlockNumber, endBlockNumber, osor.DB) +} diff --git a/pkg/eigenState/operatorSetOperatorRegistrations/operatorSetOperatorRegistrations_test.go b/pkg/eigenState/operatorSetOperatorRegistrations/operatorSetOperatorRegistrations_test.go new file mode 100644 index 00000000..15827fc7 --- /dev/null +++ b/pkg/eigenState/operatorSetOperatorRegistrations/operatorSetOperatorRegistrations_test.go @@ -0,0 +1,200 @@ +package operatorSetOperatorRegistrations + +import ( + "fmt" + "math/big" + "os" + "strings" + "testing" + "time" + + "github.com/Layr-Labs/sidecar/pkg/postgres" + "github.com/Layr-Labs/sidecar/pkg/storage" + + "github.com/Layr-Labs/sidecar/internal/config" + "github.com/Layr-Labs/sidecar/internal/logger" + "github.com/Layr-Labs/sidecar/internal/tests" + "github.com/Layr-Labs/sidecar/pkg/eigenState/stateManager" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + "gorm.io/gorm" +) + +func setup() ( + string, + *gorm.DB, + *zap.Logger, + *config.Config, + error, +) { + cfg := config.NewConfig() + cfg.Debug = os.Getenv(config.Debug) == "true" + cfg.DatabaseConfig = *tests.GetDbConfigFromEnv() + + l, _ := logger.NewLogger(&logger.LoggerConfig{Debug: cfg.Debug}) + + dbname, _, grm, err := postgres.GetTestPostgresDatabase(cfg.DatabaseConfig, cfg, l) + if err != nil { + return dbname, nil, nil, nil, err + } + + return dbname, grm, l, cfg, nil +} + +func teardown(model *OperatorSetOperatorRegistrationModel) { + queries := []string{ + `truncate table operator_set_operator_registrations`, + `truncate table blocks cascade`, + } + for _, query := range queries { + res := model.DB.Exec(query) + if res.Error != nil { + fmt.Printf("Failed to run query: %v\n", res.Error) + } + } +} + +func createBlock(model *OperatorSetOperatorRegistrationModel, blockNumber uint64) error { + block := &storage.Block{ + Number: blockNumber, + Hash: "some hash", + BlockTime: time.Now().Add(time.Hour * time.Duration(blockNumber)), + } + res := model.DB.Model(&storage.Block{}).Create(block) + if res.Error != nil { + return res.Error + } + return nil +} + +func Test_OperatorSetOperatorRegistration(t *testing.T) { + dbName, grm, l, cfg, err := setup() + + if err != nil { + t.Fatal(err) + } + + t.Run("Test each event type", func(t *testing.T) { + esm := stateManager.NewEigenStateManager(l, grm) + + model, err := NewOperatorSetOperatorRegistrationModel(esm, grm, l, cfg) + + t.Run("Handle an operator added to an operator set", func(t *testing.T) { + blockNumber := uint64(102) + + if err := createBlock(model, blockNumber); err != nil { + t.Fatal(err) + } + + log := &storage.TransactionLog{ + TransactionHash: "some hash", + TransactionIndex: big.NewInt(100).Uint64(), + BlockNumber: blockNumber, + Address: cfg.GetContractsMapForChain().AllocationManager, + Arguments: `[{"Name": "operator", "Type": "address", "Value": "0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101", "Indexed": true}, {"Name": "operatorSet", "Type": "tuple", "Value": {"avs": "0x9401E5E6564DB35C0f86573a9828DF69Fc778aF1", "id": 1}, "Indexed": false}]`, + EventName: "OperatorAddedToOperatorSet", + LogIndex: big.NewInt(12).Uint64(), + OutputData: `{"operatorSet": {"avs": "0x9401E5E6564DB35C0f86573a9828DF69Fc778aF1", "id": 1}}`, + } + + err = model.SetupStateForBlock(blockNumber) + assert.Nil(t, err) + + isInteresting := model.IsInterestingLog(log) + assert.True(t, isInteresting) + + change, err := model.HandleStateChange(log) + assert.Nil(t, err) + assert.NotNil(t, change) + + operatorRegistration := change.(*OperatorSetOperatorRegistration) + + assert.Equal(t, strings.ToLower("0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101"), strings.ToLower(operatorRegistration.Operator)) + assert.Equal(t, strings.ToLower("0x9401E5E6564DB35C0f86573a9828DF69Fc778aF1"), strings.ToLower(operatorRegistration.Avs)) + assert.Equal(t, uint64(1), operatorRegistration.OperatorSetId) + assert.Equal(t, true, operatorRegistration.IsActive) + + err = model.CommitFinalState(blockNumber) + assert.Nil(t, err) + + operatorRegistrations := make([]*OperatorSetOperatorRegistration, 0) + query := `select * from operator_set_operator_registrations where block_number = ?` + res := model.DB.Raw(query, blockNumber).Scan(&operatorRegistrations) + assert.Nil(t, res.Error) + assert.Equal(t, 1, len(operatorRegistrations)) + + stateRoot, err := model.GenerateStateRoot(blockNumber) + assert.Nil(t, err) + assert.NotNil(t, stateRoot) + assert.True(t, len(stateRoot) > 0) + + t.Cleanup(func() { + teardown(model) + }) + }) + + t.Run("Handle an operator removed from an operator set", func(t *testing.T) { + blockNumber := uint64(103) + + if err := createBlock(model, blockNumber); err != nil { + t.Fatal(err) + } + + log := &storage.TransactionLog{ + TransactionHash: "some hash", + TransactionIndex: big.NewInt(100).Uint64(), + BlockNumber: blockNumber, + Address: cfg.GetContractsMapForChain().AllocationManager, + Arguments: `[{"Name": "operator", "Type": "address", "Value": "0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101", "Indexed": true}, {"Name": "operatorSet", "Type": "tuple", "Value": {"avs": "0x9401E5E6564DB35C0f86573a9828DF69Fc778aF1", "id": 1}, "Indexed": false}]`, + EventName: "OperatorRemovedFromOperatorSet", + LogIndex: big.NewInt(12).Uint64(), + OutputData: `{"operatorSet": {"avs": "0x9401E5E6564DB35C0f86573a9828DF69Fc778aF1", "id": 1}}`, + } + + err = model.SetupStateForBlock(blockNumber) + assert.Nil(t, err) + + isInteresting := model.IsInterestingLog(log) + assert.True(t, isInteresting) + + change, err := model.HandleStateChange(log) + assert.Nil(t, err) + assert.NotNil(t, change) + + operatorRegistration := change.(*OperatorSetOperatorRegistration) + + assert.Equal(t, strings.ToLower("0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101"), strings.ToLower(operatorRegistration.Operator)) + assert.Equal(t, strings.ToLower("0x9401E5E6564DB35C0f86573a9828DF69Fc778aF1"), strings.ToLower(operatorRegistration.Avs)) + assert.Equal(t, uint64(1), operatorRegistration.OperatorSetId) + assert.Equal(t, false, operatorRegistration.IsActive) + + err = model.CommitFinalState(blockNumber) + assert.Nil(t, err) + + operatorRegistrations := make([]*OperatorSetOperatorRegistration, 0) + query := `select * from operator_set_operator_registrations where block_number = ?` + res := model.DB.Raw(query, blockNumber).Scan(&operatorRegistrations) + assert.Nil(t, res.Error) + assert.Equal(t, 1, len(operatorRegistrations)) + + fmt.Printf("operatorRegistrations: %+v\n", operatorRegistrations[0]) + + stateRoot, err := model.GenerateStateRoot(blockNumber) + assert.Nil(t, err) + assert.NotNil(t, stateRoot) + assert.True(t, len(stateRoot) > 0) + + t.Cleanup(func() { + teardown(model) + }) + }) + + t.Cleanup(func() { + teardown(model) + }) + }) + + t.Cleanup(func() { + postgres.TeardownTestDatabase(dbName, cfg, grm, l) + }) +} diff --git a/pkg/eigenState/operatorSetSplits/operatorSetSplits.go b/pkg/eigenState/operatorSetSplits/operatorSetSplits.go new file mode 100644 index 00000000..a3ad405b --- /dev/null +++ b/pkg/eigenState/operatorSetSplits/operatorSetSplits.go @@ -0,0 +1,331 @@ +package operatorSetSplits + +import ( + "encoding/json" + "fmt" + "slices" + "sort" + "strings" + "time" + + "github.com/Layr-Labs/sidecar/internal/config" + "github.com/Layr-Labs/sidecar/pkg/eigenState/base" + "github.com/Layr-Labs/sidecar/pkg/eigenState/stateManager" + "github.com/Layr-Labs/sidecar/pkg/eigenState/types" + "github.com/Layr-Labs/sidecar/pkg/storage" + "go.uber.org/zap" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +type OperatorSetSplit struct { + Operator string + Avs string + OperatorSetId uint64 + ActivatedAt *time.Time + OldOperatorSetSplitBips uint64 + NewOperatorSetSplitBips uint64 + BlockNumber uint64 + TransactionHash string + LogIndex uint64 +} + +type OperatorSetSplitModel struct { + base.BaseEigenState + StateTransitions types.StateTransitions[[]*OperatorSetSplit] + DB *gorm.DB + Network config.Network + Environment config.Environment + logger *zap.Logger + globalConfig *config.Config + + // Accumulates state changes for SlotIds, grouped by block number + stateAccumulator map[uint64]map[types.SlotID]*OperatorSetSplit + committedState map[uint64][]*OperatorSetSplit +} + +func NewOperatorSetSplitModel( + esm *stateManager.EigenStateManager, + grm *gorm.DB, + logger *zap.Logger, + globalConfig *config.Config, +) (*OperatorSetSplitModel, error) { + model := &OperatorSetSplitModel{ + BaseEigenState: base.BaseEigenState{ + Logger: logger, + }, + DB: grm, + logger: logger, + globalConfig: globalConfig, + stateAccumulator: make(map[uint64]map[types.SlotID]*OperatorSetSplit), + committedState: make(map[uint64][]*OperatorSetSplit), + } + + esm.RegisterState(model, 12) + return model, nil +} + +func (oss *OperatorSetSplitModel) GetModelName() string { + return "OperatorSetSplitModel" +} + +type operatorSetSplitOutputData struct { + OperatorSet *OperatorSet `json:"operatorSet"` + ActivatedAt uint64 `json:"activatedAt"` + OldOperatorSetSplitBips uint64 `json:"oldOperatorSetSplitBips"` + NewOperatorSetSplitBips uint64 `json:"newOperatorSetSplitBips"` +} + +type OperatorSet struct { + Avs string `json:"avs"` + Id uint64 `json:"id"` +} + +func parseOperatorSetSplitOutputData(outputDataStr string) (*operatorSetSplitOutputData, error) { + outputData := &operatorSetSplitOutputData{} + decoder := json.NewDecoder(strings.NewReader(outputDataStr)) + decoder.UseNumber() + + err := decoder.Decode(&outputData) + if err != nil { + return nil, err + } + + return outputData, err +} + +func (oss *OperatorSetSplitModel) handleOperatorSetSplitBipsSetEvent(log *storage.TransactionLog) (*OperatorSetSplit, error) { + arguments, err := oss.ParseLogArguments(log) + if err != nil { + return nil, err + } + + outputData, err := parseOperatorSetSplitOutputData(log.OutputData) + if err != nil { + return nil, err + } + + activatedAt := time.Unix(int64(outputData.ActivatedAt), 0) + + split := &OperatorSetSplit{ + Operator: strings.ToLower(arguments[1].Value.(string)), + Avs: strings.ToLower(outputData.OperatorSet.Avs), + OperatorSetId: uint64(outputData.OperatorSet.Id), + ActivatedAt: &activatedAt, + OldOperatorSetSplitBips: outputData.OldOperatorSetSplitBips, + NewOperatorSetSplitBips: outputData.NewOperatorSetSplitBips, + BlockNumber: log.BlockNumber, + TransactionHash: log.TransactionHash, + LogIndex: log.LogIndex, + } + + return split, nil +} + +func (oss *OperatorSetSplitModel) GetStateTransitions() (types.StateTransitions[*OperatorSetSplit], []uint64) { + stateChanges := make(types.StateTransitions[*OperatorSetSplit]) + + stateChanges[0] = func(log *storage.TransactionLog) (*OperatorSetSplit, error) { + operatorSetSplit, err := oss.handleOperatorSetSplitBipsSetEvent(log) + if err != nil { + return nil, err + } + + slotId := base.NewSlotID(operatorSetSplit.TransactionHash, operatorSetSplit.LogIndex) + + _, ok := oss.stateAccumulator[log.BlockNumber][slotId] + if ok { + err := fmt.Errorf("Duplicate operator set split submitted for slot %s at block %d", slotId, log.BlockNumber) + oss.logger.Sugar().Errorw("Duplicate operator set split submitted", zap.Error(err)) + return nil, err + } + + oss.stateAccumulator[log.BlockNumber][slotId] = operatorSetSplit + + return operatorSetSplit, 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 (oss *OperatorSetSplitModel) getContractAddressesForEnvironment() map[string][]string { + contracts := oss.globalConfig.GetContractsMapForChain() + return map[string][]string{ + contracts.RewardsCoordinator: { + "OperatorSetSplitBipsSet", + }, + } +} + +func (oss *OperatorSetSplitModel) IsInterestingLog(log *storage.TransactionLog) bool { + addresses := oss.getContractAddressesForEnvironment() + return oss.BaseEigenState.IsInterestingLog(addresses, log) +} + +func (oss *OperatorSetSplitModel) SetupStateForBlock(blockNumber uint64) error { + oss.stateAccumulator[blockNumber] = make(map[types.SlotID]*OperatorSetSplit) + oss.committedState[blockNumber] = make([]*OperatorSetSplit, 0) + return nil +} + +func (oss *OperatorSetSplitModel) CleanupProcessedStateForBlock(blockNumber uint64) error { + delete(oss.stateAccumulator, blockNumber) + delete(oss.committedState, blockNumber) + return nil +} + +func (oss *OperatorSetSplitModel) HandleStateChange(log *storage.TransactionLog) (interface{}, error) { + stateChanges, sortedBlockNumbers := oss.GetStateTransitions() + + for _, blockNumber := range sortedBlockNumbers { + if log.BlockNumber >= blockNumber { + oss.logger.Sugar().Debugw("Handling state change", zap.Uint64("blockNumber", log.BlockNumber)) + + change, err := stateChanges[blockNumber](log) + if err != nil { + return nil, err + } + if change == nil { + return nil, nil + } + return change, nil + } + } + return nil, nil +} + +// prepareState prepares the state for commit by adding the new state to the existing state. +func (oss *OperatorSetSplitModel) prepareState(blockNumber uint64) ([]*OperatorSetSplit, error) { + accumulatedState, ok := oss.stateAccumulator[blockNumber] + if !ok { + err := fmt.Errorf("No accumulated state found for block %d", blockNumber) + oss.logger.Sugar().Errorw(err.Error(), zap.Error(err), zap.Uint64("blockNumber", blockNumber)) + return nil, err + } + + recordsToInsert := make([]*OperatorSetSplit, 0) + for _, split := range accumulatedState { + recordsToInsert = append(recordsToInsert, split) + } + return recordsToInsert, nil +} + +// CommitFinalState commits the final state for the given block number. +func (oss *OperatorSetSplitModel) CommitFinalState(blockNumber uint64) error { + recordsToInsert, err := oss.prepareState(blockNumber) + if err != nil { + return err + } + + if len(recordsToInsert) > 0 { + for _, record := range recordsToInsert { + res := oss.DB.Model(&OperatorSetSplit{}).Clauses(clause.Returning{}).Create(&record) + if res.Error != nil { + oss.logger.Sugar().Errorw("Failed to insert records", zap.Error(res.Error)) + return res.Error + } + } + } + oss.committedState[blockNumber] = recordsToInsert + return nil +} + +// GenerateStateRoot generates the state root for the given block number using the results of the state changes. +func (oss *OperatorSetSplitModel) GenerateStateRoot(blockNumber uint64) ([]byte, error) { + inserts, err := oss.prepareState(blockNumber) + if err != nil { + return nil, err + } + + inputs, err := oss.sortValuesForMerkleTree(inserts) + if err != nil { + return nil, err + } + + if len(inputs) == 0 { + return nil, nil + } + + fullTree, err := oss.MerkleizeEigenState(blockNumber, inputs) + if err != nil { + oss.logger.Sugar().Errorw("Failed to create merkle tree", + zap.Error(err), + zap.Uint64("blockNumber", blockNumber), + zap.Any("inputs", inputs), + ) + return nil, err + } + return fullTree.Root(), nil +} + +func (oss *OperatorSetSplitModel) GetCommittedState(blockNumber uint64) ([]interface{}, error) { + records, ok := oss.committedState[blockNumber] + if !ok { + err := fmt.Errorf("No committed state found for block %d", blockNumber) + oss.logger.Sugar().Errorw(err.Error(), zap.Error(err), zap.Uint64("blockNumber", blockNumber)) + return nil, err + } + return base.CastCommittedStateToInterface(records), nil +} + +func (oss *OperatorSetSplitModel) formatMerkleLeafValue( + operator string, + avs string, + operatorSetId uint64, + activatedAt *time.Time, + oldOperatorSetSplitBips uint64, + newOperatorSetSplitBips uint64, +) (string, error) { + return fmt.Sprintf("%s_%s_%016x_%016x_%016x_%016x", operator, avs, operatorSetId, activatedAt.Unix(), oldOperatorSetSplitBips, newOperatorSetSplitBips), nil +} + +func (oss *OperatorSetSplitModel) sortValuesForMerkleTree(splits []*OperatorSetSplit) ([]*base.MerkleTreeInput, error) { + inputs := make([]*base.MerkleTreeInput, 0) + for _, split := range splits { + slotID := base.NewSlotID(split.TransactionHash, split.LogIndex) + value, err := oss.formatMerkleLeafValue( + split.Operator, + split.Avs, + split.OperatorSetId, + split.ActivatedAt, + split.OldOperatorSetSplitBips, + split.NewOperatorSetSplitBips, + ) + if err != nil { + oss.logger.Sugar().Errorw("Failed to format merkle leaf value", + zap.Error(err), + zap.String("operator", split.Operator), + zap.String("avs", split.Avs), + zap.Uint64("operatorSetId", split.OperatorSetId), + zap.Time("activatedAt", *split.ActivatedAt), + zap.Uint64("oldOperatorSetSplitBips", split.OldOperatorSetSplitBips), + zap.Uint64("newOperatorSetSplitBips", split.NewOperatorSetSplitBips), + ) + return nil, err + } + inputs = append(inputs, &base.MerkleTreeInput{ + SlotID: slotID, + Value: []byte(value), + }) + } + + slices.SortFunc(inputs, func(i, j *base.MerkleTreeInput) int { + return strings.Compare(string(i.SlotID), string(j.SlotID)) + }) + + return inputs, nil +} + +func (oss *OperatorSetSplitModel) DeleteState(startBlockNumber uint64, endBlockNumber uint64) error { + return oss.BaseEigenState.DeleteState("operator_set_splits", startBlockNumber, endBlockNumber, oss.DB) +} diff --git a/pkg/eigenState/operatorSetSplits/operatorSetSplits_test.go b/pkg/eigenState/operatorSetSplits/operatorSetSplits_test.go new file mode 100644 index 00000000..b862c827 --- /dev/null +++ b/pkg/eigenState/operatorSetSplits/operatorSetSplits_test.go @@ -0,0 +1,124 @@ +package operatorSetSplits + +import ( + "math/big" + "os" + "strings" + "testing" + "time" + + "github.com/Layr-Labs/sidecar/pkg/postgres" + "github.com/Layr-Labs/sidecar/pkg/storage" + + "github.com/Layr-Labs/sidecar/internal/config" + "github.com/Layr-Labs/sidecar/internal/logger" + "github.com/Layr-Labs/sidecar/internal/tests" + "github.com/Layr-Labs/sidecar/pkg/eigenState/stateManager" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + "gorm.io/gorm" +) + +func setup() ( + string, + *gorm.DB, + *zap.Logger, + *config.Config, + error, +) { + cfg := config.NewConfig() + cfg.Debug = os.Getenv(config.Debug) == "true" + cfg.DatabaseConfig = *tests.GetDbConfigFromEnv() + + l, _ := logger.NewLogger(&logger.LoggerConfig{Debug: cfg.Debug}) + + dbname, _, grm, err := postgres.GetTestPostgresDatabase(cfg.DatabaseConfig, cfg, l) + if err != nil { + return dbname, nil, nil, nil, err + } + + return dbname, grm, l, cfg, nil +} + +func createBlock(model *OperatorSetSplitModel, blockNumber uint64) error { + block := &storage.Block{ + Number: blockNumber, + Hash: "some hash", + BlockTime: time.Now().Add(time.Hour * time.Duration(blockNumber)), + } + res := model.DB.Model(&storage.Block{}).Create(block) + if res.Error != nil { + return res.Error + } + return nil +} + +func Test_OperatorSetSplit(t *testing.T) { + dbName, grm, l, cfg, err := setup() + + if err != nil { + t.Fatal(err) + } + + t.Run("Test each event type", func(t *testing.T) { + esm := stateManager.NewEigenStateManager(l, grm) + + model, err := NewOperatorSetSplitModel(esm, grm, l, cfg) + + t.Run("Handle an operator set split", func(t *testing.T) { + blockNumber := uint64(102) + + if err := createBlock(model, blockNumber); err != nil { + t.Fatal(err) + } + + log := &storage.TransactionLog{ + TransactionHash: "some hash", + TransactionIndex: big.NewInt(100).Uint64(), + BlockNumber: blockNumber, + Address: cfg.GetContractsMapForChain().RewardsCoordinator, + Arguments: `[{"Name": "caller", "Type": "address", "Value": "0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101", "Indexed": true}, {"Name": "operator", "Type": "address", "Value": "0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101", "Indexed": true}, {"Name": "operatorSet", "Type": "tuple", "Value": {"avs": "0x9401E5E6564DB35C0f86573a9828DF69Fc778aF1", "id": 1}, "Indexed": false}, {"Name": "activatedAt", "Type": "uint32", "Value": 1725494400, "Indexed": false}, {"Name": "oldOperatorSetSplitBips", "Type": "uint16", "Value": 1000, "Indexed": false}, {"Name": "newOperatorSetSplitBips", "Type": "uint16", "Value": 2000, "Indexed": false}]`, + EventName: "OperatorSetSplitBipsSet", + LogIndex: big.NewInt(12).Uint64(), + OutputData: `{"operatorSet": {"avs": "0x9401E5E6564DB35C0f86573a9828DF69Fc778aF1", "id": 1}, "activatedAt": 1725494400, "oldOperatorSetSplitBips": 1000, "newOperatorSetSplitBips": 2000}`, + } + + err = model.SetupStateForBlock(blockNumber) + assert.Nil(t, err) + + isInteresting := model.IsInterestingLog(log) + assert.True(t, isInteresting) + + change, err := model.HandleStateChange(log) + assert.Nil(t, err) + assert.NotNil(t, change) + + split := change.(*OperatorSetSplit) + + assert.Equal(t, strings.ToLower("0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101"), strings.ToLower(split.Operator)) + assert.Equal(t, strings.ToLower("0x9401E5E6564DB35C0f86573a9828DF69Fc778aF1"), strings.ToLower(split.Avs)) + assert.Equal(t, uint64(1), split.OperatorSetId) + assert.Equal(t, int64(1725494400), split.ActivatedAt.Unix()) + assert.Equal(t, uint64(1000), split.OldOperatorSetSplitBips) + assert.Equal(t, uint64(2000), split.NewOperatorSetSplitBips) + + err = model.CommitFinalState(blockNumber) + assert.Nil(t, err) + + splits := make([]*OperatorSetSplit, 0) + query := `select * from operator_set_splits where block_number = ?` + res := model.DB.Raw(query, blockNumber).Scan(&splits) + assert.Nil(t, res.Error) + assert.Equal(t, 1, len(splits)) + + stateRoot, err := model.GenerateStateRoot(blockNumber) + assert.Nil(t, err) + assert.NotNil(t, stateRoot) + assert.True(t, len(stateRoot) > 0) + }) + }) + + t.Cleanup(func() { + postgres.TeardownTestDatabase(dbName, cfg, grm, l) + }) +} diff --git a/pkg/eigenState/operatorSetStrategyRegistrations/operatorSetStrategyRegistrations.go b/pkg/eigenState/operatorSetStrategyRegistrations/operatorSetStrategyRegistrations.go new file mode 100644 index 00000000..b3af5a61 --- /dev/null +++ b/pkg/eigenState/operatorSetStrategyRegistrations/operatorSetStrategyRegistrations.go @@ -0,0 +1,317 @@ +package operatorSetStrategyRegistrations + +import ( + "encoding/json" + "fmt" + "slices" + "sort" + "strings" + + "github.com/Layr-Labs/sidecar/internal/config" + "github.com/Layr-Labs/sidecar/pkg/eigenState/base" + "github.com/Layr-Labs/sidecar/pkg/eigenState/stateManager" + "github.com/Layr-Labs/sidecar/pkg/eigenState/types" + "github.com/Layr-Labs/sidecar/pkg/storage" + "go.uber.org/zap" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +type OperatorSetStrategyRegistration struct { + Strategy string + Avs string + OperatorSetId uint64 + IsActive bool + BlockNumber uint64 + TransactionHash string + LogIndex uint64 +} + +type OperatorSetStrategyRegistrationModel struct { + base.BaseEigenState + StateTransitions types.StateTransitions[[]*OperatorSetStrategyRegistration] + DB *gorm.DB + Network config.Network + Environment config.Environment + logger *zap.Logger + globalConfig *config.Config + + // Accumulates state changes for SlotIds, grouped by block number + stateAccumulator map[uint64]map[types.SlotID]*OperatorSetStrategyRegistration + committedState map[uint64][]*OperatorSetStrategyRegistration +} + +func NewOperatorSetStrategyRegistrationModel( + esm *stateManager.EigenStateManager, + grm *gorm.DB, + logger *zap.Logger, + globalConfig *config.Config, +) (*OperatorSetStrategyRegistrationModel, error) { + model := &OperatorSetStrategyRegistrationModel{ + BaseEigenState: base.BaseEigenState{ + Logger: logger, + }, + DB: grm, + logger: logger, + globalConfig: globalConfig, + stateAccumulator: make(map[uint64]map[types.SlotID]*OperatorSetStrategyRegistration), + committedState: make(map[uint64][]*OperatorSetStrategyRegistration), + } + + esm.RegisterState(model, 14) + return model, nil +} + +func (ossr *OperatorSetStrategyRegistrationModel) GetModelName() string { + return "OperatorSetStrategyRegistrationModel" +} + +type operatorSetStrategyRegistrationOutputData struct { + OperatorSet *OperatorSet `json:"operatorSet"` + Strategy string `json:"strategy"` +} + +type OperatorSet struct { + Avs string `json:"avs"` + Id uint64 `json:"id"` +} + +func parseOperatorSetStrategyRegistrationOutputData(outputDataStr string) (*operatorSetStrategyRegistrationOutputData, error) { + outputData := &operatorSetStrategyRegistrationOutputData{} + decoder := json.NewDecoder(strings.NewReader(outputDataStr)) + decoder.UseNumber() + + err := decoder.Decode(&outputData) + if err != nil { + return nil, err + } + + return outputData, err +} + +func (ossr *OperatorSetStrategyRegistrationModel) handleOperatorSetStrategyRegistrationEvent(log *storage.TransactionLog) (*OperatorSetStrategyRegistration, error) { + outputData, err := parseOperatorSetStrategyRegistrationOutputData(log.OutputData) + if err != nil { + return nil, err + } + + isActive := true // default to true for "StrategyAddedToOperatorSet" event + if log.EventName == "StrategyRemovedFromOperatorSet" { + isActive = false + } + + strategyRegistration := &OperatorSetStrategyRegistration{ + Strategy: outputData.Strategy, + Avs: outputData.OperatorSet.Avs, + OperatorSetId: outputData.OperatorSet.Id, + IsActive: isActive, + BlockNumber: log.BlockNumber, + TransactionHash: log.TransactionHash, + LogIndex: log.LogIndex, + } + + return strategyRegistration, nil +} + +func (ossr *OperatorSetStrategyRegistrationModel) GetStateTransitions() (types.StateTransitions[*OperatorSetStrategyRegistration], []uint64) { + stateChanges := make(types.StateTransitions[*OperatorSetStrategyRegistration]) + + stateChanges[0] = func(log *storage.TransactionLog) (*OperatorSetStrategyRegistration, error) { + operatorSetStrategyRegistration, err := ossr.handleOperatorSetStrategyRegistrationEvent(log) + if err != nil { + return nil, err + } + + slotId := base.NewSlotID(operatorSetStrategyRegistration.TransactionHash, operatorSetStrategyRegistration.LogIndex) + + _, ok := ossr.stateAccumulator[log.BlockNumber][slotId] + if ok { + err := fmt.Errorf("Duplicate operator set strategy registration submitted for slot %s at block %d", slotId, log.BlockNumber) + ossr.logger.Sugar().Errorw("Duplicate operator set strategy registration submitted", zap.Error(err)) + return nil, err + } + + ossr.stateAccumulator[log.BlockNumber][slotId] = operatorSetStrategyRegistration + + return operatorSetStrategyRegistration, 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 (ossr *OperatorSetStrategyRegistrationModel) getContractAddressesForEnvironment() map[string][]string { + contracts := ossr.globalConfig.GetContractsMapForChain() + return map[string][]string{ + contracts.AllocationManager: { + "StrategyAddedToOperatorSet", + "StrategyRemovedFromOperatorSet", + }, + } +} + +func (ossr *OperatorSetStrategyRegistrationModel) IsInterestingLog(log *storage.TransactionLog) bool { + addresses := ossr.getContractAddressesForEnvironment() + return ossr.BaseEigenState.IsInterestingLog(addresses, log) +} + +func (ossr *OperatorSetStrategyRegistrationModel) SetupStateForBlock(blockNumber uint64) error { + ossr.stateAccumulator[blockNumber] = make(map[types.SlotID]*OperatorSetStrategyRegistration) + ossr.committedState[blockNumber] = make([]*OperatorSetStrategyRegistration, 0) + return nil +} + +func (ossr *OperatorSetStrategyRegistrationModel) CleanupProcessedStateForBlock(blockNumber uint64) error { + delete(ossr.stateAccumulator, blockNumber) + delete(ossr.committedState, blockNumber) + return nil +} + +func (ossr *OperatorSetStrategyRegistrationModel) HandleStateChange(log *storage.TransactionLog) (interface{}, error) { + stateChanges, sortedBlockNumbers := ossr.GetStateTransitions() + + for _, blockNumber := range sortedBlockNumbers { + if log.BlockNumber >= blockNumber { + ossr.logger.Sugar().Debugw("Handling state change", zap.Uint64("blockNumber", log.BlockNumber)) + + change, err := stateChanges[blockNumber](log) + if err != nil { + return nil, err + } + if change == nil { + return nil, nil + } + return change, nil + } + } + return nil, nil +} + +// prepareState prepares the state for commit by adding the new state to the existing state. +func (ossr *OperatorSetStrategyRegistrationModel) prepareState(blockNumber uint64) ([]*OperatorSetStrategyRegistration, error) { + accumulatedState, ok := ossr.stateAccumulator[blockNumber] + if !ok { + err := fmt.Errorf("No accumulated state found for block %d", blockNumber) + ossr.logger.Sugar().Errorw(err.Error(), zap.Error(err), zap.Uint64("blockNumber", blockNumber)) + return nil, err + } + + recordsToInsert := make([]*OperatorSetStrategyRegistration, 0) + for _, split := range accumulatedState { + recordsToInsert = append(recordsToInsert, split) + } + return recordsToInsert, nil +} + +// CommitFinalState commits the final state for the given block number. +func (ossr *OperatorSetStrategyRegistrationModel) CommitFinalState(blockNumber uint64) error { + recordsToInsert, err := ossr.prepareState(blockNumber) + if err != nil { + return err + } + + if len(recordsToInsert) > 0 { + for _, record := range recordsToInsert { + res := ossr.DB.Model(&OperatorSetStrategyRegistration{}).Clauses(clause.Returning{}).Create(&record) + if res.Error != nil { + ossr.logger.Sugar().Errorw("Failed to insert records", zap.Error(res.Error)) + return res.Error + } + } + } + ossr.committedState[blockNumber] = recordsToInsert + return nil +} + +// GenerateStateRoot generates the state root for the given block number using the results of the state changes. +func (ossr *OperatorSetStrategyRegistrationModel) GenerateStateRoot(blockNumber uint64) ([]byte, error) { + inserts, err := ossr.prepareState(blockNumber) + if err != nil { + return nil, err + } + + inputs, err := ossr.sortValuesForMerkleTree(inserts) + if err != nil { + return nil, err + } + + if len(inputs) == 0 { + return nil, nil + } + + fullTree, err := ossr.MerkleizeEigenState(blockNumber, inputs) + if err != nil { + ossr.logger.Sugar().Errorw("Failed to create merkle tree", + zap.Error(err), + zap.Uint64("blockNumber", blockNumber), + zap.Any("inputs", inputs), + ) + return nil, err + } + return fullTree.Root(), nil +} + +func (ossr *OperatorSetStrategyRegistrationModel) GetCommittedState(blockNumber uint64) ([]interface{}, error) { + records, ok := ossr.committedState[blockNumber] + if !ok { + err := fmt.Errorf("No committed state found for block %d", blockNumber) + ossr.logger.Sugar().Errorw(err.Error(), zap.Error(err), zap.Uint64("blockNumber", blockNumber)) + return nil, err + } + return base.CastCommittedStateToInterface(records), nil +} + +func (ossr *OperatorSetStrategyRegistrationModel) formatMerkleLeafValue( + strategy string, + avs string, + operatorSetId uint64, + isActive bool, +) (string, error) { + return fmt.Sprintf("%s_%s_%016x_%t", strategy, avs, operatorSetId, isActive), nil +} + +func (ossr *OperatorSetStrategyRegistrationModel) sortValuesForMerkleTree(strategyRegistrations []*OperatorSetStrategyRegistration) ([]*base.MerkleTreeInput, error) { + inputs := make([]*base.MerkleTreeInput, 0) + for _, strategyRegistration := range strategyRegistrations { + slotID := base.NewSlotID(strategyRegistration.TransactionHash, strategyRegistration.LogIndex) + value, err := ossr.formatMerkleLeafValue( + strategyRegistration.Strategy, + strategyRegistration.Avs, + strategyRegistration.OperatorSetId, + strategyRegistration.IsActive, + ) + if err != nil { + ossr.logger.Sugar().Errorw("Failed to format merkle leaf value", + zap.Error(err), + zap.String("strategy", strategyRegistration.Strategy), + zap.String("avs", strategyRegistration.Avs), + zap.Uint64("operatorSetId", strategyRegistration.OperatorSetId), + zap.Bool("isActive", strategyRegistration.IsActive), + ) + return nil, err + } + inputs = append(inputs, &base.MerkleTreeInput{ + SlotID: slotID, + Value: []byte(value), + }) + } + + slices.SortFunc(inputs, func(i, j *base.MerkleTreeInput) int { + return strings.Compare(string(i.SlotID), string(j.SlotID)) + }) + + return inputs, nil +} + +func (ossr *OperatorSetStrategyRegistrationModel) DeleteState(startBlockNumber uint64, endBlockNumber uint64) error { + return ossr.BaseEigenState.DeleteState("operator_set_strategy_registrations", startBlockNumber, endBlockNumber, ossr.DB) +} diff --git a/pkg/eigenState/operatorSetStrategyRegistrations/operatorSetStrategyRegistrations_test.go b/pkg/eigenState/operatorSetStrategyRegistrations/operatorSetStrategyRegistrations_test.go new file mode 100644 index 00000000..0343c693 --- /dev/null +++ b/pkg/eigenState/operatorSetStrategyRegistrations/operatorSetStrategyRegistrations_test.go @@ -0,0 +1,198 @@ +package operatorSetStrategyRegistrations + +import ( + "fmt" + "math/big" + "os" + "strings" + "testing" + "time" + + "github.com/Layr-Labs/sidecar/pkg/postgres" + "github.com/Layr-Labs/sidecar/pkg/storage" + + "github.com/Layr-Labs/sidecar/internal/config" + "github.com/Layr-Labs/sidecar/internal/logger" + "github.com/Layr-Labs/sidecar/internal/tests" + "github.com/Layr-Labs/sidecar/pkg/eigenState/stateManager" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + "gorm.io/gorm" +) + +func setup() ( + string, + *gorm.DB, + *zap.Logger, + *config.Config, + error, +) { + cfg := config.NewConfig() + cfg.Debug = os.Getenv(config.Debug) == "true" + cfg.DatabaseConfig = *tests.GetDbConfigFromEnv() + + l, _ := logger.NewLogger(&logger.LoggerConfig{Debug: cfg.Debug}) + + dbname, _, grm, err := postgres.GetTestPostgresDatabase(cfg.DatabaseConfig, cfg, l) + if err != nil { + return dbname, nil, nil, nil, err + } + + return dbname, grm, l, cfg, nil +} + +func teardown(model *OperatorSetStrategyRegistrationModel) { + queries := []string{ + `truncate table operator_set_strategy_registrations`, + `truncate table blocks cascade`, + } + for _, query := range queries { + res := model.DB.Exec(query) + if res.Error != nil { + fmt.Printf("Failed to run query: %v\n", res.Error) + } + } +} + +func createBlock(model *OperatorSetStrategyRegistrationModel, blockNumber uint64) error { + block := &storage.Block{ + Number: blockNumber, + Hash: "some hash", + BlockTime: time.Now().Add(time.Hour * time.Duration(blockNumber)), + } + res := model.DB.Model(&storage.Block{}).Create(block) + if res.Error != nil { + return res.Error + } + return nil +} + +func Test_OperatorSetStrategyRegistration(t *testing.T) { + dbName, grm, l, cfg, err := setup() + + if err != nil { + t.Fatal(err) + } + + t.Run("Test each event type", func(t *testing.T) { + esm := stateManager.NewEigenStateManager(l, grm) + + model, err := NewOperatorSetStrategyRegistrationModel(esm, grm, l, cfg) + + t.Run("Handle a strategy added to an operator set", func(t *testing.T) { + blockNumber := uint64(102) + + if err := createBlock(model, blockNumber); err != nil { + t.Fatal(err) + } + + log := &storage.TransactionLog{ + TransactionHash: "some hash", + TransactionIndex: big.NewInt(100).Uint64(), + BlockNumber: blockNumber, + Address: cfg.GetContractsMapForChain().AllocationManager, + Arguments: `[{"Name": "operatorSet", "Type": "tuple", "Value": {"avs": "0x9401E5E6564DB35C0f86573a9828DF69Fc778aF1", "id": 1}, "Indexed": false}, {"Name": "strategy", "Type": "address", "Value": "0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101", "Indexed": false}]`, + EventName: "StrategyAddedToOperatorSet", + LogIndex: big.NewInt(12).Uint64(), + OutputData: `{"operatorSet": {"avs": "0x9401E5E6564DB35C0f86573a9828DF69Fc778aF1", "id": 1}, "strategy": "0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101"}`, + } + + err = model.SetupStateForBlock(blockNumber) + assert.Nil(t, err) + + isInteresting := model.IsInterestingLog(log) + assert.True(t, isInteresting) + + change, err := model.HandleStateChange(log) + assert.Nil(t, err) + assert.NotNil(t, change) + + strategyRegistration := change.(*OperatorSetStrategyRegistration) + + assert.Equal(t, strings.ToLower("0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101"), strings.ToLower(strategyRegistration.Strategy)) + assert.Equal(t, strings.ToLower("0x9401E5E6564DB35C0f86573a9828DF69Fc778aF1"), strings.ToLower(strategyRegistration.Avs)) + assert.Equal(t, uint64(1), strategyRegistration.OperatorSetId) + assert.Equal(t, true, strategyRegistration.IsActive) + + err = model.CommitFinalState(blockNumber) + assert.Nil(t, err) + + strategyRegistrations := make([]*OperatorSetStrategyRegistration, 0) + query := `select * from operator_set_strategy_registrations where block_number = ?` + res := model.DB.Raw(query, blockNumber).Scan(&strategyRegistrations) + assert.Nil(t, res.Error) + assert.Equal(t, 1, len(strategyRegistrations)) + + stateRoot, err := model.GenerateStateRoot(blockNumber) + assert.Nil(t, err) + assert.NotNil(t, stateRoot) + assert.True(t, len(stateRoot) > 0) + + t.Cleanup(func() { + teardown(model) + }) + }) + + t.Run("Handle a strategy removed from an operator set", func(t *testing.T) { + blockNumber := uint64(103) + + if err := createBlock(model, blockNumber); err != nil { + t.Fatal(err) + } + + log := &storage.TransactionLog{ + TransactionHash: "some hash", + TransactionIndex: big.NewInt(100).Uint64(), + BlockNumber: blockNumber, + Address: cfg.GetContractsMapForChain().AllocationManager, + Arguments: `[{"Name": "operatorSet", "Type": "tuple", "Value": {"avs": "0x9401E5E6564DB35C0f86573a9828DF69Fc778aF1", "id": 1}, "Indexed": false}, {"Name": "strategy", "Type": "address", "Value": "0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101", "Indexed": false}]`, + EventName: "StrategyRemovedFromOperatorSet", + LogIndex: big.NewInt(12).Uint64(), + OutputData: `{"operatorSet": {"avs": "0x9401E5E6564DB35C0f86573a9828DF69Fc778aF1", "id": 1}, "strategy": "0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101"}`, + } + + err = model.SetupStateForBlock(blockNumber) + assert.Nil(t, err) + + isInteresting := model.IsInterestingLog(log) + assert.True(t, isInteresting) + + change, err := model.HandleStateChange(log) + assert.Nil(t, err) + assert.NotNil(t, change) + + strategyRegistration := change.(*OperatorSetStrategyRegistration) + + assert.Equal(t, strings.ToLower("0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101"), strings.ToLower(strategyRegistration.Strategy)) + assert.Equal(t, strings.ToLower("0x9401E5E6564DB35C0f86573a9828DF69Fc778aF1"), strings.ToLower(strategyRegistration.Avs)) + assert.Equal(t, uint64(1), strategyRegistration.OperatorSetId) + assert.Equal(t, false, strategyRegistration.IsActive) + + err = model.CommitFinalState(blockNumber) + assert.Nil(t, err) + + strategyRegistrations := make([]*OperatorSetStrategyRegistration, 0) + query := `select * from operator_set_strategy_registrations where block_number = ?` + res := model.DB.Raw(query, blockNumber).Scan(&strategyRegistrations) + assert.Nil(t, res.Error) + assert.Equal(t, 1, len(strategyRegistrations)) + + stateRoot, err := model.GenerateStateRoot(blockNumber) + assert.Nil(t, err) + assert.NotNil(t, stateRoot) + assert.True(t, len(stateRoot) > 0) + + t.Cleanup(func() { + teardown(model) + }) + }) + + t.Cleanup(func() { + teardown(model) + }) + }) + + t.Cleanup(func() { + postgres.TeardownTestDatabase(dbName, cfg, grm, l) + }) +} diff --git a/pkg/postgres/migrations/202501241322_operatorDirectedOperatorSetRewardSubmissions/up.go b/pkg/postgres/migrations/202501241322_operatorDirectedOperatorSetRewardSubmissions/up.go new file mode 100644 index 00000000..cde2ab59 --- /dev/null +++ b/pkg/postgres/migrations/202501241322_operatorDirectedOperatorSetRewardSubmissions/up.go @@ -0,0 +1,45 @@ +package _202501241322_operatorDirectedOperatorSetRewardSubmissions + +import ( + "database/sql" + + "github.com/Layr-Labs/sidecar/internal/config" + "gorm.io/gorm" +) + +type Migration struct { +} + +func (m *Migration) Up(db *sql.DB, grm *gorm.DB, cfg *config.Config) error { + query := ` + create table if not exists operator_directed_operator_set_reward_submissions ( + avs varchar not null, + operator_set_id bigint not null, + reward_hash varchar not null, + token varchar not null, + operator varchar not null, + operator_index integer not null, + amount numeric not null, + strategy varchar not null, + strategy_index integer not null, + multiplier numeric(78) not null, + start_timestamp timestamp(6) not null, + end_timestamp timestamp(6) not null, + duration bigint not null, + description varchar not null, + block_number bigint not null, + transaction_hash varchar not null, + log_index bigint not null, + unique(transaction_hash, log_index, block_number, reward_hash, strategy_index, operator_index), + CONSTRAINT operator_directed_operator_set_reward_submissions_block_number_fkey FOREIGN KEY (block_number) REFERENCES blocks(number) ON DELETE CASCADE + ); + ` + if err := grm.Exec(query).Error; err != nil { + return err + } + return nil +} + +func (m *Migration) GetName() string { + return "202501241322_operatorDirectedOperatorSetRewardSubmissions" +} diff --git a/pkg/postgres/migrations/202501241533_operatorSetSplits/up.go b/pkg/postgres/migrations/202501241533_operatorSetSplits/up.go new file mode 100644 index 00000000..8bd45f24 --- /dev/null +++ b/pkg/postgres/migrations/202501241533_operatorSetSplits/up.go @@ -0,0 +1,37 @@ +package _202501241533_operatorSetSplits + +import ( + "database/sql" + + "github.com/Layr-Labs/sidecar/internal/config" + "gorm.io/gorm" +) + +type Migration struct { +} + +func (m *Migration) Up(db *sql.DB, grm *gorm.DB, cfg *config.Config) error { + query := ` + create table if not exists operator_set_splits ( + operator varchar not null, + avs varchar not null, + operator_set_id bigint not null, + activated_at timestamp(6) not null, + old_operator_set_split_bips integer not null, + new_operator_set_split_bips integer not null, + block_number bigint not null, + transaction_hash varchar not null, + log_index bigint not null, + unique(transaction_hash, log_index, block_number), + CONSTRAINT operator_set_splits_block_number_fkey FOREIGN KEY (block_number) REFERENCES blocks(number) ON DELETE CASCADE + ); + ` + if err := grm.Exec(query).Error; err != nil { + return err + } + return nil +} + +func (m *Migration) GetName() string { + return "202501241533_operatorSetSplits" +} diff --git a/pkg/postgres/migrations/202501271727_operatorSetOperatorRegistrations/up.go b/pkg/postgres/migrations/202501271727_operatorSetOperatorRegistrations/up.go new file mode 100644 index 00000000..0f551cda --- /dev/null +++ b/pkg/postgres/migrations/202501271727_operatorSetOperatorRegistrations/up.go @@ -0,0 +1,35 @@ +package _202501271727_operatorSetOperatorRegistrations + +import ( + "database/sql" + + "github.com/Layr-Labs/sidecar/internal/config" + "gorm.io/gorm" +) + +type Migration struct { +} + +func (m *Migration) Up(db *sql.DB, grm *gorm.DB, cfg *config.Config) error { + query := ` + create table if not exists operator_set_operator_registrations ( + operator varchar not null, + avs varchar not null, + operator_set_id bigint not null, + is_active boolean not null, + block_number bigint not null, + transaction_hash varchar not null, + log_index bigint not null, + unique(transaction_hash, log_index, block_number), + CONSTRAINT operator_set_operator_registrations_block_number_fkey FOREIGN KEY (block_number) REFERENCES blocks(number) ON DELETE CASCADE + ); + ` + if err := grm.Exec(query).Error; err != nil { + return err + } + return nil +} + +func (m *Migration) GetName() string { + return "202501271727_operatorSetOperatorRegistrations" +} diff --git a/pkg/postgres/migrations/202501281806_operatorSetStrategyRegistrations/up.go b/pkg/postgres/migrations/202501281806_operatorSetStrategyRegistrations/up.go new file mode 100644 index 00000000..d6472664 --- /dev/null +++ b/pkg/postgres/migrations/202501281806_operatorSetStrategyRegistrations/up.go @@ -0,0 +1,35 @@ +package _202501281806_operatorSetStrategyRegistrations + +import ( + "database/sql" + + "github.com/Layr-Labs/sidecar/internal/config" + "gorm.io/gorm" +) + +type Migration struct { +} + +func (m *Migration) Up(db *sql.DB, grm *gorm.DB, cfg *config.Config) error { + query := ` + create table if not exists operator_set_strategy_registrations ( + strategy varchar not null, + avs varchar not null, + operator_set_id bigint not null, + is_active boolean not null, + block_number bigint not null, + transaction_hash varchar not null, + log_index bigint not null, + unique(transaction_hash, log_index, block_number), + CONSTRAINT operator_set_strategy_registrations_block_number_fkey FOREIGN KEY (block_number) REFERENCES blocks(number) ON DELETE CASCADE + ); + ` + if err := grm.Exec(query).Error; err != nil { + return err + } + return nil +} + +func (m *Migration) GetName() string { + return "202501281806_operatorSetStrategyRegistrations" +} diff --git a/pkg/postgres/migrations/migrator.go b/pkg/postgres/migrations/migrator.go index f5a40eab..c1ca9305 100644 --- a/pkg/postgres/migrations/migrator.go +++ b/pkg/postgres/migrations/migrator.go @@ -50,6 +50,10 @@ import ( _202501061422_defaultOperatorSplits "github.com/Layr-Labs/sidecar/pkg/postgres/migrations/202501061422_defaultOperatorSplits" _202501071401_defaultOperatorSplitSnapshots "github.com/Layr-Labs/sidecar/pkg/postgres/migrations/202501071401_defaultOperatorSplitSnapshots" _202501151039_rewardsClaimed "github.com/Layr-Labs/sidecar/pkg/postgres/migrations/202501151039_rewardsClaimed" + _202501241322_operatorDirectedOperatorSetRewardSubmissions "github.com/Layr-Labs/sidecar/pkg/postgres/migrations/202501241322_operatorDirectedOperatorSetRewardSubmissions" + _202501241533_operatorSetSplits "github.com/Layr-Labs/sidecar/pkg/postgres/migrations/202501241533_operatorSetSplits" + _202501271727_operatorSetOperatorRegistrations "github.com/Layr-Labs/sidecar/pkg/postgres/migrations/202501271727_operatorSetOperatorRegistrations" + _202501281806_operatorSetStrategyRegistrations "github.com/Layr-Labs/sidecar/pkg/postgres/migrations/202501281806_operatorSetStrategyRegistrations" "go.uber.org/zap" "gorm.io/gorm" ) @@ -136,6 +140,10 @@ func (m *Migrator) MigrateAll() error { &_202501061422_defaultOperatorSplits.Migration{}, &_202501071401_defaultOperatorSplitSnapshots.Migration{}, &_202501241111_addIndexesForRpcFunctions.Migration{}, + &_202501241322_operatorDirectedOperatorSetRewardSubmissions.Migration{}, + &_202501241533_operatorSetSplits.Migration{}, + &_202501271727_operatorSetOperatorRegistrations.Migration{}, + &_202501281806_operatorSetStrategyRegistrations.Migration{}, } for _, migration := range migrations {