Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add rpc for generating rewards claim proofs #172

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion cmd/debugger/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/Layr-Labs/sidecar/pkg/indexer"
"github.com/Layr-Labs/sidecar/pkg/pipeline"
"github.com/Layr-Labs/sidecar/pkg/postgres"
"github.com/Layr-Labs/sidecar/pkg/proofs"
"github.com/Layr-Labs/sidecar/pkg/rewards"
"github.com/Layr-Labs/sidecar/pkg/rewards/stakerOperators"
"github.com/Layr-Labs/sidecar/pkg/rewardsCalculatorQueue"
Expand Down Expand Up @@ -95,12 +96,14 @@ func main() {

rcq := rewardsCalculatorQueue.NewRewardsCalculatorQueue(rc, l)

rps := proofs.NewRewardsProofsStore(rc, l)

p := pipeline.NewPipeline(fetchr, idxr, mds, sm, rc, rcq, cfg, sdc, l)

// Create new sidecar instance
sidecar := sidecar.NewSidecar(&sidecar.SidecarConfig{
GenesisBlockNumber: cfg.GetGenesisBlockNumber(),
}, cfg, mds, p, sm, rc, rcq, l, client)
}, cfg, mds, p, sm, rc, rcq, rps, l, client)

// RPC channel to notify the RPC server to shutdown gracefully
rpcChannel := make(chan bool)
Expand Down
5 changes: 4 additions & 1 deletion cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/Layr-Labs/sidecar/pkg/indexer"
"github.com/Layr-Labs/sidecar/pkg/pipeline"
"github.com/Layr-Labs/sidecar/pkg/postgres"
"github.com/Layr-Labs/sidecar/pkg/proofs"
"github.com/Layr-Labs/sidecar/pkg/rewards"
"github.com/Layr-Labs/sidecar/pkg/rewards/stakerOperators"
"github.com/Layr-Labs/sidecar/pkg/rewardsCalculatorQueue"
Expand Down Expand Up @@ -111,14 +112,16 @@ var runCmd = &cobra.Command{

rcq := rewardsCalculatorQueue.NewRewardsCalculatorQueue(rc, l)

rps := proofs.NewRewardsProofsStore(rc, l)

go rcq.Process()

p := pipeline.NewPipeline(fetchr, idxr, mds, sm, rc, rcq, cfg, sdc, l)

// Create new sidecar instance
sidecar := sidecar.NewSidecar(&sidecar.SidecarConfig{
GenesisBlockNumber: cfg.GetGenesisBlockNumber(),
}, cfg, mds, p, sm, rc, rcq, l, client)
}, cfg, mds, p, sm, rc, rcq, rps, l, client)

// RPC channel to notify the RPC server to shutdown gracefully
rpcChannel := make(chan bool)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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 v1.0.0-rc.1.0.20241206171209-6484d433345c
github.com/Layr-Labs/protocol-apis v1.0.0-rc.1.0.20250103191019-e175af17c3df
github.com/ethereum/go-ethereum v1.14.9
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1
github.com/google/uuid v1.6.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ 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 v1.0.0-rc.1.0.20241206171209-6484d433345c h1:m9xRDJR9ItBQmiOBhsx64Bemi4W5/2pkXru75evpEGI=
github.com/Layr-Labs/protocol-apis v1.0.0-rc.1.0.20241206171209-6484d433345c/go.mod h1:prNA2/mLO5vpMZ2q78Nsn0m97wm28uiRnwO+/yOxigk=
github.com/Layr-Labs/protocol-apis v1.0.0-rc.1.0.20250103191019-e175af17c3df h1:KdwnQU0REOWN9lcNS/Hoh3gLgAw0E/8EHCR7h8xL3us=
github.com/Layr-Labs/protocol-apis v1.0.0-rc.1.0.20250103191019-e175af17c3df/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=
Expand Down
2 changes: 1 addition & 1 deletion pkg/pipeline/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ func (p *Pipeline) RunForFetchedBlock(ctx context.Context, block *fetcher.Fetche
zap.String("cutoffDate", cutoffDate),
zap.Uint64("blockNumber", blockNumber),
)
accountTree, _, err := p.rewardsCalculator.MerkelizeRewardsForSnapshot(rewardsCalculationEnd)
accountTree, _, _, err := p.rewardsCalculator.MerkelizeRewardsForSnapshot(rewardsCalculationEnd)
if err != nil {
p.Logger.Sugar().Errorw("Failed to merkelize rewards for snapshot date",
zap.String("cutoffDate", cutoffDate), zap.Error(err),
Expand Down
121 changes: 121 additions & 0 deletions pkg/proofs/rewardsProofs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package proofs

import (
rewardsCoordinator "github.com/Layr-Labs/eigenlayer-contracts/pkg/bindings/IRewardsCoordinator"
"github.com/Layr-Labs/eigenlayer-rewards-proofs/pkg/claimgen"
"github.com/Layr-Labs/eigenlayer-rewards-proofs/pkg/distribution"
"github.com/Layr-Labs/sidecar/pkg/rewards"
"github.com/Layr-Labs/sidecar/pkg/utils"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/wealdtech/go-merkletree/v2"
"go.uber.org/zap"
)

type RewardsProofsStore struct {
rewardsCalculator *rewards.RewardsCalculator
logger *zap.Logger
rewardsData map[string]*ProofData
}

type ProofData struct {
SnapshotDate string
AccountTree *merkletree.MerkleTree
TokenTree map[gethcommon.Address]*merkletree.MerkleTree
Distribution *distribution.Distribution
}

func NewRewardsProofsStore(
rc *rewards.RewardsCalculator,
l *zap.Logger,
) *RewardsProofsStore {
return &RewardsProofsStore{
rewardsCalculator: rc,
logger: l,
rewardsData: make(map[string]*ProofData),
}
}

func (rps *RewardsProofsStore) getRewardsDataForSnapshot(snapshot string) (*ProofData, error) {
data, ok := rps.rewardsData[snapshot]
if !ok {
accountTree, tokenTree, distro, err := rps.rewardsCalculator.MerkelizeRewardsForSnapshot(snapshot)
if err != nil {
rps.logger.Sugar().Errorw("Failed to fetch rewards for snapshot",
zap.String("snapshot", snapshot),
zap.Error(err),
)
return nil, err
}

data = &ProofData{
SnapshotDate: snapshot,
AccountTree: accountTree,
TokenTree: tokenTree,
Distribution: distro,
}
rps.rewardsData[snapshot] = data
}
return data, nil
}

func (rps *RewardsProofsStore) GenerateRewardsClaimProof(earnerAddress string, tokenAddresses []string, snapshotDate string) (
[]byte,
*rewardsCoordinator.IRewardsCoordinatorRewardsMerkleClaim,
error,
) {
if snapshotDate == "" {
snapshotDate = "latest"
}

distributionRoot, err := rps.rewardsCalculator.FindClaimableDistributionRoot(snapshotDate)
if err != nil {
rps.logger.Sugar().Errorf("Failed to find most claimable distribution root", zap.Error(err))
return nil, nil, err
}
if snapshotDate == "latest" {
snapshotDate = distributionRoot.GetSnapshotDate()
}

// Make sure rewards have been generated for this snapshot.
// Any snapshot that is >= the provided date is valid since we'll select only data up
// to the snapshot/cutoff date
generatedSnapshot, err := rps.rewardsCalculator.GetGeneratedRewardsForSnapshotDate(snapshotDate)
if err != nil {
rps.logger.Sugar().Errorf("Failed to get generated rewards for snapshot date", zap.Error(err))
return nil, nil, err
}
rps.logger.Sugar().Infow("Using snapshot for rewards proof",
zap.String("requestedSnapshot", snapshotDate),
zap.String("snapshot", generatedSnapshot.SnapshotDate),
)

proofData, err := rps.getRewardsDataForSnapshot(snapshotDate)
if err != nil {
rps.logger.Sugar().Error("Failed to get rewards data for snapshot",
zap.String("snapshot", snapshotDate),
zap.Error(err),
)
return nil, nil, err
}

tokens := utils.Map(tokenAddresses, func(addr string, i uint64) gethcommon.Address {
return gethcommon.HexToAddress(addr)
})
earner := gethcommon.HexToAddress(earnerAddress)
rootIndex := distributionRoot.RootIndex

claim, err := claimgen.GetProofForEarner(
proofData.Distribution,
uint32(rootIndex),
proofData.AccountTree,
proofData.TokenTree,
earner,
tokens,
)
if err != nil {
rps.logger.Sugar().Error("Failed to generate claim proof for earner", zap.Error(err))
return nil, nil, err
}

return proofData.AccountTree.Root(), claim, nil
}
87 changes: 81 additions & 6 deletions pkg/rewards/rewards.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"github.com/Layr-Labs/eigenlayer-rewards-proofs/pkg/distribution"
"github.com/Layr-Labs/sidecar/pkg/eigenState/types"
"github.com/Layr-Labs/sidecar/pkg/rewards/stakerOperators"
"github.com/Layr-Labs/sidecar/pkg/rewardsUtils"
"github.com/Layr-Labs/sidecar/pkg/storage"
Expand Down Expand Up @@ -185,10 +186,15 @@ func (rc *RewardsCalculator) GetRewardSnapshotStatus(snapshotDate string) (*stor
return r, nil
}

func (rc *RewardsCalculator) MerkelizeRewardsForSnapshot(snapshotDate string) (*merkletree.MerkleTree, map[gethcommon.Address]*merkletree.MerkleTree, error) {
rewards, err := rc.fetchRewardsForSnapshot(snapshotDate)
func (rc *RewardsCalculator) MerkelizeRewardsForSnapshot(snapshotDate string) (
*merkletree.MerkleTree,
map[gethcommon.Address]*merkletree.MerkleTree,
*distribution.Distribution,
error,
) {
rewards, err := rc.FetchRewardsForSnapshot(snapshotDate)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}

distro := distribution.NewDistribution()
Expand All @@ -206,12 +212,12 @@ func (rc *RewardsCalculator) MerkelizeRewardsForSnapshot(snapshotDate string) (*

if err := distro.LoadLines(earnerLines); err != nil {
rc.logger.Error("Failed to load lines", zap.Error(err))
return nil, nil, err
return nil, nil, nil, err
}

accountTree, tokenTree, err := distro.Merklize()

return accountTree, tokenTree, err
return accountTree, tokenTree, distro, err
}

func (rc *RewardsCalculator) GetMaxSnapshotDateForCutoffDate(cutoffDate string) (string, error) {
Expand Down Expand Up @@ -480,7 +486,7 @@ type Reward struct {
CumulativeAmount string
}

func (rc *RewardsCalculator) fetchRewardsForSnapshot(snapshotDate string) ([]*Reward, error) {
func (rc *RewardsCalculator) FetchRewardsForSnapshot(snapshotDate string) ([]*Reward, error) {
var goldRows []*Reward
query, err := rewardsUtils.RenderQueryTemplate(`
select
Expand Down Expand Up @@ -668,3 +674,72 @@ func (rc *RewardsCalculator) generateAndInsertFromQuery(
rc.logger,
)
}

func (rc *RewardsCalculator) FindClaimableDistributionRoot(snapshotDate string) (*types.SubmittedDistributionRoot, error) {
if snapshotDate == "" {
snapshotDate = "latest"
}
query := `
select
*
from submitted_distribution_roots as sdr
left join disabled_distribution_roots as ddr on (sdr.root_index = ddr.root_index)
where
ddr.root_index is null
{{ if eq .snapshotDate "latest" }}
and activated_at >= now()
{{ else }}
and activated_at >= '{{.snapshotDate}}'::timestamp(6)
{{ end }}
order by root_index desc
limit 1
`
renderedQuery, err := rewardsUtils.RenderQueryTemplate(query, map[string]string{"snapshotDate": snapshotDate})
if err != nil {
rc.logger.Sugar().Errorw("Failed to render query template", "error", err)
return nil, err
}

var submittedDistributionRoot *types.SubmittedDistributionRoot
res := rc.grm.Raw(renderedQuery).Scan(&submittedDistributionRoot)
if res.Error != nil {
if errors.Is(res.Error, gorm.ErrRecordNotFound) {
rc.logger.Sugar().Errorw("No active distribution root found for snapshot",
zap.String("snapshotDate", snapshotDate),
zap.Error(res.Error),
)
return nil, res.Error
}
rc.logger.Sugar().Errorw("Failed to find most recent claimable distribution root", "error", res.Error)
return nil, res.Error
}

return submittedDistributionRoot, nil
}

func (rc *RewardsCalculator) GetGeneratedRewardsForSnapshotDate(snapshotDate string) (*storage.GeneratedRewardsSnapshots, error) {
query, err := rewardsUtils.RenderQueryTemplate(`
select
*
from generated_rewards_snapshots as grs
where
status = 'complete'
{{if ne .snapshotDate "latest"}}
and grs.snapshot_date::timestamp(6) >= '{{.snapshotDate}}'::timestamp(6)
{{end}}
limit 1
`, map[string]string{"snapshotDate": snapshotDate})

if err != nil {
rc.logger.Sugar().Errorw("Failed to render query template", "error", err)
return nil, err
}

var generatedRewardsSnapshot *storage.GeneratedRewardsSnapshots
res := rc.grm.Raw(query).Scan(&generatedRewardsSnapshot)
if res.Error != nil {
rc.logger.Sugar().Errorw("Failed to get generated rewards snapshots", "error", res.Error)
return nil, res.Error
}
return generatedRewardsSnapshot, nil
}
2 changes: 1 addition & 1 deletion pkg/rewards/rewards_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ func Test_Rewards(t *testing.T) {
err = rc.sog.GenerateStakerOperatorsTable(snapshotDate)
assert.Nil(t, err)

accountTree, _, err := rc.MerkelizeRewardsForSnapshot(snapshotDate)
accountTree, _, _, err := rc.MerkelizeRewardsForSnapshot(snapshotDate)
assert.Nil(t, err)

root := utils.ConvertBytesToString(accountTree.Root())
Expand Down
51 changes: 51 additions & 0 deletions pkg/rpcServer/proofsHandlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package rpcServer

import (
"context"
"github.com/Layr-Labs/eigenlayer-rewards-proofs/pkg/claimgen"
sidecarV1 "github.com/Layr-Labs/protocol-apis/gen/protos/eigenlayer/sidecar/v1"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

func convertClaimProofToRPCResponse(solidityProof *claimgen.IRewardsCoordinatorRewardsMerkleClaimStrings) *sidecarV1.Proof {
tokenLeaves := make([]*sidecarV1.TokenLeaf, 0)

for _, l := range solidityProof.TokenLeaves {
tokenLeaves = append(tokenLeaves, &sidecarV1.TokenLeaf{
Token: l.Token.String(),
CumulativeEarnings: l.CumulativeEarnings,
})
}

return &sidecarV1.Proof{
Root: solidityProof.Root,
RootIndex: solidityProof.RootIndex,
EarnerIndex: solidityProof.EarnerIndex,
EarnerTreeProof: solidityProof.EarnerTreeProof,
EarnerLeaf: &sidecarV1.EarnerLeaf{
Earner: solidityProof.EarnerLeaf.Earner.String(),
EarnerTokenRoot: solidityProof.EarnerLeaf.EarnerTokenRoot,
},
LeafIndices: solidityProof.TokenIndices,
TokenTreeProofs: solidityProof.TokenTreeProofs,
TokenLeaves: tokenLeaves,
}
}

func (rpc *RpcServer) GenerateClaimProof(ctx context.Context, req *sidecarV1.GenerateClaimProofRequest) (*sidecarV1.GenerateClaimProofResponse, error) {
earner := req.GetEarnerAddress()
tokens := req.GetTokens()
snapshotDate := req.GetSnapshot()

root, claim, err := rpc.rewardsProofs.GenerateRewardsClaimProof(earner, tokens, snapshotDate)
if err != nil {
return nil, status.Errorf(codes.Internal, "Failed to generate claim proof %s", err.Error())
}

solidityClaim := claimgen.FormatProofForSolidity(root, claim)

return &sidecarV1.GenerateClaimProofResponse{
Proof: convertClaimProofToRPCResponse(solidityClaim),
}, nil
}
Loading
Loading