From 01fa31c97fa7deaca484de497818f9b6cf11a20c Mon Sep 17 00:00:00 2001 From: Sean McGary Date: Tue, 3 Dec 2024 16:04:47 -0600 Subject: [PATCH] feat: add RPC to generate staker-operators data for previously calculated rewards --- go.mod | 2 +- go.sum | 6 ++++ pkg/rewards/rewards.go | 59 +++++++++++++++++++++++++++++--- pkg/rewards/rewards_test.go | 2 +- pkg/rpcServer/rewardsHandlers.go | 19 ++++++++++ 5 files changed, 82 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index b6730670..b0709e19 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/DataDog/datadog-go/v5 v5.5.0 github.com/Layr-Labs/eigenlayer-contracts v0.4.1-holesky-pepe.0.20240813143901-00fc4b95e9c1 github.com/Layr-Labs/eigenlayer-rewards-proofs v0.2.13 - github.com/Layr-Labs/protocol-apis v0.1.0-beta.3.0.20241122223729-1734c60ac737 + github.com/Layr-Labs/protocol-apis v0.1.0-beta.3.0.20241203214053-44b580f4ea84 github.com/ethereum/go-ethereum v1.14.9 github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 github.com/google/uuid v1.6.0 diff --git a/go.sum b/go.sum index 26aa70a3..cd10e56d 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,12 @@ github.com/Layr-Labs/eigenlayer-rewards-proofs v0.2.13 h1:Blb4AE+jC/vddV71w4/MQA github.com/Layr-Labs/eigenlayer-rewards-proofs v0.2.13/go.mod h1:PD/HoyzZjxDw1tAcZw3yD0yGddo+yhmwQAi+lk298r4= github.com/Layr-Labs/protocol-apis v0.1.0-beta.3.0.20241122223729-1734c60ac737 h1:I/0YAw2ue150YuLNavErIQ4t7yoTDuH3nqZkOS7RTjg= github.com/Layr-Labs/protocol-apis v0.1.0-beta.3.0.20241122223729-1734c60ac737/go.mod h1:prNA2/mLO5vpMZ2q78Nsn0m97wm28uiRnwO+/yOxigk= +github.com/Layr-Labs/protocol-apis v0.1.0-beta.3.0.20241203213718-bda8083a30b9 h1:tvBtFPGqMw1Hwp++cGP/otFTC2OQrJMtVjkH/vNbttE= +github.com/Layr-Labs/protocol-apis v0.1.0-beta.3.0.20241203213718-bda8083a30b9/go.mod h1:prNA2/mLO5vpMZ2q78Nsn0m97wm28uiRnwO+/yOxigk= +github.com/Layr-Labs/protocol-apis v0.1.0-beta.3.0.20241203213920-e35bed7723dc h1:P7S3ijgAQ4Xdpdocfl7rGgaTsPAurHsSCYHfhWm9au4= +github.com/Layr-Labs/protocol-apis v0.1.0-beta.3.0.20241203213920-e35bed7723dc/go.mod h1:prNA2/mLO5vpMZ2q78Nsn0m97wm28uiRnwO+/yOxigk= +github.com/Layr-Labs/protocol-apis v0.1.0-beta.3.0.20241203214053-44b580f4ea84 h1:BLF8GHMmXSC1YtjlqBRC7pVBDVK0QwEJpwXYjZO7t1w= +github.com/Layr-Labs/protocol-apis v0.1.0-beta.3.0.20241203214053-44b580f4ea84/go.mod h1:prNA2/mLO5vpMZ2q78Nsn0m97wm28uiRnwO+/yOxigk= github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= diff --git a/pkg/rewards/rewards.go b/pkg/rewards/rewards.go index f80e31e0..212b419a 100644 --- a/pkg/rewards/rewards.go +++ b/pkg/rewards/rewards.go @@ -58,9 +58,9 @@ func (rc *RewardsCalculator) releaseGenerationLock() { rc.isGenerating.Store(false) } -type RewardsCalculationInProgressError struct{} +type ErrRewardsCalculationInProgress struct{} -func (e *RewardsCalculationInProgressError) Error() string { +func (e *ErrRewardsCalculationInProgress) Error() string { return "rewards calculation already in progress" } @@ -71,7 +71,7 @@ func (e *RewardsCalculationInProgressError) Error() string { // If there is no previous DistributionRoot, the rewards are calculated from EigenLayer Genesis. func (rc *RewardsCalculator) calculateRewardsForSnapshotDate(snapshotDate string) error { if rc.GetIsGenerating() { - err := &RewardsCalculationInProgressError{} + err := &ErrRewardsCalculationInProgress{} rc.logger.Sugar().Infow(err.Error()) return err } @@ -143,7 +143,7 @@ func (rc *RewardsCalculator) CalculateRewardsForSnapshotDate(snapshotDate string go func() { for { err := rc.calculateRewardsForSnapshotDate(snapshotDate) - if errors.Is(err, &RewardsCalculationInProgressError{}) { + if errors.Is(err, &ErrRewardsCalculationInProgress{}) { rc.logger.Sugar().Infow("Rewards calculation already in progress, sleeping", zap.String("snapshotDate", snapshotDate)) time.Sleep(1 * time.Minute) } else { @@ -239,6 +239,57 @@ func (rc *RewardsCalculator) GetMaxSnapshotDateForCutoffDate(cutoffDate string) return maxSnapshotStr, nil } +// GenerateStakerOperatorsTableForPastSnapshot generates the staker operators table for a past snapshot date, OR +// generates the rewards and the related staker-operator table data if the snapshot is greater than the latest snapshot. +func (rc *RewardsCalculator) GenerateStakerOperatorsTableForPastSnapshot(cutoffDate string) error { + // find the first snapshot that is >= to the provided cutoff date + var generatedSnapshot storage.GeneratedRewardsSnapshots + query := `select * from generated_rewards_snapshots where snapshot_date >= ? order by snapshot_date asc limit 1` + res := rc.grm.Raw(query, cutoffDate).Scan(&generatedSnapshot) + if res.Error != nil && errors.Is(res.Error, gorm.ErrRecordNotFound) { + rc.logger.Sugar().Errorw("Failed to get generated snapshot", "error", res.Error) + return res.Error + } + if res.RowsAffected == 0 || errors.Is(res.Error, gorm.ErrRecordNotFound) { + rc.logger.Sugar().Infow("No snapshot found for cutoff date, rewards need to be calculated", "cutoffDate", cutoffDate) + return rc.CalculateRewardsForSnapshotDate(cutoffDate) + } + + // since rewards are already calculated and the corresponding tables are tied to the snapshot date, + // we need to use the snapshot date from the generated snapshot to generate the staker operators table. + // + // Since this date is larger, and the insert into the staker-operators table discards duplicates, + // this should be safe to do. + cutoffDate = generatedSnapshot.SnapshotDate + + // Since this was a previous calculation, we have the date-suffixed gold tables, but not necessarily the snapshot tables. + // In order for our calculations to work, we need to generate the snapshot tables for the cutoff date. + // + // First check to see if there is already a rewards generation in progress. If there is, return an error and let the caller try again. + if rc.GetIsGenerating() { + err := &ErrRewardsCalculationInProgress{} + rc.logger.Sugar().Infow(err.Error()) + return err + } + + // Acquire the generation lock and proceed with generating snapshot tables and then the staker operators table. + rc.acquireGenerationLock() + defer rc.releaseGenerationLock() + + rc.logger.Sugar().Infow("Acquired rewards generation lock", "cutoffDate", cutoffDate) + + if err := rc.generateSnapshotData(cutoffDate); err != nil { + rc.logger.Sugar().Errorw("Failed to generate snapshot data", "error", err) + return err + } + + if err := rc.sog.GenerateStakerOperatorsTable(cutoffDate); err != nil { + rc.logger.Sugar().Errorw("Failed to generate staker operators table", "error", err) + return err + } + return nil +} + type Reward struct { Earner string Token string diff --git a/pkg/rewards/rewards_test.go b/pkg/rewards/rewards_test.go index d58d4314..eba13663 100644 --- a/pkg/rewards/rewards_test.go +++ b/pkg/rewards/rewards_test.go @@ -388,7 +388,7 @@ func Test_RewardsCalculatorLock(t *testing.T) { time.Sleep(1 * time.Second) t.Logf("Attempting to calculate second rewards for snapshot date: 2024-08-02") err = rc.calculateRewardsForSnapshotDate("2024-08-02") - assert.True(t, errors.Is(err, &RewardsCalculationInProgressError{})) + assert.True(t, errors.Is(err, &ErrRewardsCalculationInProgress{})) t.Cleanup(func() { postgres.TeardownTestDatabase(dbFileName, cfg, grm, l) diff --git a/pkg/rpcServer/rewardsHandlers.go b/pkg/rpcServer/rewardsHandlers.go index c0f41fde..c1c3d0df 100644 --- a/pkg/rpcServer/rewardsHandlers.go +++ b/pkg/rpcServer/rewardsHandlers.go @@ -2,7 +2,9 @@ package rpcServer import ( "context" + "errors" sidecarV1 "github.com/Layr-Labs/protocol-apis/gen/protos/eigenlayer/sidecar/v1" + "github.com/Layr-Labs/sidecar/pkg/rewards" "github.com/Layr-Labs/sidecar/pkg/utils" "go.uber.org/zap" "google.golang.org/grpc/codes" @@ -78,6 +80,23 @@ func (rpc *RpcServer) GenerateRewardsRoot(ctx context.Context, req *sidecarV1.Ge }, nil } +func (rpc *RpcServer) GenerateStakerOperators(ctx context.Context, req *sidecarV1.GenerateStakerOperatorsRequest) (*sidecarV1.GenerateStakerOperatorsResponse, error) { + cutoffDate := req.GetCutoffDate() + + if cutoffDate == "" { + return nil, status.Error(codes.InvalidArgument, "snapshot date is required") + } + + err := rpc.rewardsCalculator.GenerateStakerOperatorsTableForPastSnapshot(cutoffDate) + if err != nil { + if errors.Is(err, &rewards.ErrRewardsCalculationInProgress{}) { + return nil, status.Error(codes.FailedPrecondition, err.Error()) + } + return nil, status.Error(codes.Internal, err.Error()) + } + return &sidecarV1.GenerateStakerOperatorsResponse{}, nil +} + func (rpc *RpcServer) GetRewardsForSnapshot(ctx context.Context, req *sidecarV1.GetRewardsForSnapshotRequest) (*sidecarV1.GetRewardsForSnapshotResponse, error) { return nil, status.Error(codes.Unimplemented, "method GetRewardsForSnapshot not implemented") }