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 - validator redirects #5

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
/.tmp
.env
.DS_Store
node_modules
node_modules
.idea/
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,30 @@ mechanics:
# Redirect rewards to different addresses. The left-hand side is the owner address,
# and the right-hand side is the reward recipient address. Do not specify deployer
# addresses on the left-hand side, only owner addresses.
reward_redirects:
owner_redirects:
"0x1234567890abcdef1234567890abcdef12345678": "0x1234567890abcdef1234567890abcdef12345678"
# Redirect rewards to different addresses by validator public key. The left-hand side is the validator public key,
# and the right-hand side is the reward recipient address.
validator_redirects:
"0x1234500012345000123450001234500012345000123450001234500012345000123450001234500012345000123450001234": "0x1234567890abcdef1234567890abcdef12345678"

# Alternatively, you can specify redirects using external CSV files:
# - You cannot use both `owner_redirects` and `owner_redirects_file` simultaneously. Choose one method.
# - You cannot use both `validator_redirects` and `validator_redirects_file` simultaneously. Choose one method.
# - Each file must have a header row with "from" and "to" as column names.

# For owner redirects, the "from" column contains owner addresses, and the "to" column contains recipient addresses.
# Example of owner_redirects_file content:
# from,to
# 0x1234567890abcdef1234567890abcdef12345678,0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef
# 0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef,0x1234567890abcdef1234567890abcdef12345678
owner_redirects_file: owner_redirects_2023_11.csv
# For validator redirects, the "from" column contains validator public keys, and the "to" column contains recipient addresses.
# Example of validator_redirects_file content:
# from,to
# 0x1234500012345000123450001234500012345000123450001234500012345000123450001234500012345000123450001234,0x1234567890abcdef1234567890abcdef12345678
# 0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef,0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef
validator_redirects_file: validator_redirects_2023_11.csv

rounds:
- period: 2023-07 # Designated period (year-month)
Expand Down
213 changes: 177 additions & 36 deletions cmd/ssv-rewards/calc.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@ import (
"time"

"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/bloxapp/ssv-rewards/pkg/models"
"github.com/bloxapp/ssv-rewards/pkg/precise"
"github.com/bloxapp/ssv-rewards/pkg/rewards"
"github.com/bloxapp/ssv/networkconfig"
"github.com/gocarina/gocsv"
"github.com/volatiletech/sqlboiler/v4/boil"
"github.com/volatiletech/sqlboiler/v4/queries"
"go.uber.org/zap"
"golang.org/x/exp/maps"

"github.com/bloxapp/ssv-rewards/pkg/models"
"github.com/bloxapp/ssv-rewards/pkg/precise"
"github.com/bloxapp/ssv-rewards/pkg/rewards"
)

type CalcCmd struct {
Expand Down Expand Up @@ -102,6 +103,22 @@ func (c *CalcCmd) Run(
return fmt.Errorf("failed to write rewards.json: %w", err)
}

// Export redirects from files.
for _, mechanics := range c.plan.Mechanics {
if mechanics.OwnerRedirectsFile != "" {
filePath := filepath.Join(inputsDir, filepath.Base(mechanics.OwnerRedirectsFile))
if err := exportRedirectsToCSV(mechanics.OwnerRedirects, filePath); err != nil {
return fmt.Errorf("failed to export owner redirects for period %s: %w", mechanics.Since, err)
}
}
if mechanics.ValidatorRedirectsFile != "" {
filePath := filepath.Join(inputsDir, filepath.Base(mechanics.ValidatorRedirectsFile))
if err := exportRedirectsToCSV(mechanics.ValidatorRedirects, filePath); err != nil {
return fmt.Errorf("failed to export validator redirects for period %s: %w", mechanics.Since, err)
}
}
}

// Calculate rewards.
if err := c.run(ctx, logger, tmpDir); err != nil {
return fmt.Errorf("failed to calculate rewards: %w", err)
Expand Down Expand Up @@ -341,11 +358,12 @@ func (c *CalcCmd) run(ctx context.Context, logger *zap.Logger, dir string) error
}

type ValidatorParticipation struct {
OwnerAddress string
PublicKey string
ActiveDays int
Reward *precise.ETH `boil:"-"`
reward *big.Int `boil:"-"`
OwnerAddress string
RecipientAddress string
PublicKey string
ActiveDays int
Reward *precise.ETH `boil:"-"`
reward *big.Int `boil:"-"`
}

type ValidatorParticipationRound struct {
Expand All @@ -357,14 +375,55 @@ func (c *CalcCmd) validatorParticipations(
ctx context.Context,
period rewards.Period,
) ([]*ValidatorParticipation, error) {
var rewards []*ValidatorParticipation
return rewards, queries.Raw(
"SELECT * FROM active_days_by_validator($1, $2, $3, $4)",
// Retrieve mechanics for the given period
mechanics, err := c.plan.Mechanics.At(period)
if err != nil {
return nil, fmt.Errorf("failed to get mechanics for period %s: %w", period, err)
}

// Prepare redirections for the current mechanics
ownerRedirectsSupport, validatorRedirectsSupport, err := c.prepareRedirections(ctx, mechanics)
if err != nil {
return nil, fmt.Errorf("failed to prepare redirections for period %s: %w", period, err)
}

var participations []*ValidatorParticipation
gnosisSafeSupport := mechanics.Features.Enabled(rewards.FeatureGnosisSafe)
return participations, queries.Raw(
"SELECT * FROM active_days_by_validator($1, $2, $3, $4, $5, $6, $7, $8)",
c.PerformanceProvider,
c.plan.Criteria.MinAttestationsPerDay,
c.plan.Criteria.MinDecidedsPerDay,
time.Time(period),
).Bind(ctx, c.db, &rewards)
nil, // to_period can be nil for single-period queries
gnosisSafeSupport,
ownerRedirectsSupport,
validatorRedirectsSupport,
).Bind(ctx, c.db, &participations)
}

func (c *CalcCmd) prepareRedirections(
ctx context.Context,
mechanics *rewards.Mechanics,
) (bool, bool, error) {
// Check and populate Owner Redirects
ownerRedirectsSupport := len(mechanics.OwnerRedirects) > 0
if ownerRedirectsSupport {
if err := c.populateOwnerRedirectsTable(ctx, mechanics.OwnerRedirects); err != nil {
return false, false, fmt.Errorf("failed to populate owner redirects: %w", err)
}
}

// Check and populate Validator Redirects
validatorRedirectsSupport := len(mechanics.ValidatorRedirects) > 0
if validatorRedirectsSupport {
if err := c.populateValidatorRedirectsTable(ctx, mechanics.ValidatorRedirects); err != nil {
return false, false, fmt.Errorf("failed to populate validator redirects: %w", err)
}
}

// Return whether redirects are supported
return ownerRedirectsSupport, validatorRedirectsSupport, nil
}

type OwnerParticipation struct {
Expand Down Expand Up @@ -411,52 +470,52 @@ func (c *CalcCmd) recipientParticipations(
ctx context.Context,
period rewards.Period,
) ([]*RecipientParticipation, error) {
// Retrieve mechanics for the given period
mechanics, err := c.plan.Mechanics.At(period)
if err != nil {
return nil, fmt.Errorf("failed to get mechanics: %w", err)
}
gnosisSafeSupport := mechanics.Features.Enabled(rewards.FeatureGnosisSafe)
rewardRedirectsSupport := len(mechanics.RewardRedirects) > 0

if rewardRedirectsSupport {
err := c.populateRewardRedirectsTable(ctx, mechanics.RewardRedirects)
if err != nil {
return nil, fmt.Errorf("failed to populate reward redirects table: %w", err)
}
// Prepare redirections for the current mechanics
ownerRedirectsSupport, validatorRedirectsSupport, err := c.prepareRedirections(ctx, mechanics)
if err != nil {
return nil, fmt.Errorf("failed to prepare redirections for period %s: %w", period, err)
}

var rewards []*RecipientParticipation
return rewards, queries.Raw(
"SELECT * FROM active_days_by_recipient($1, $2, $3, $4, $5, $6, $7)",
var participations []*RecipientParticipation
gnosisSafeSupport := mechanics.Features.Enabled(rewards.FeatureGnosisSafe)
return participations, queries.Raw(
"SELECT * FROM active_days_by_recipient($1, $2, $3, $4, $5, $6, $7, $8)",
c.PerformanceProvider,
c.plan.Criteria.MinAttestationsPerDay,
c.plan.Criteria.MinDecidedsPerDay,
time.Time(period),
nil,
gnosisSafeSupport,
rewardRedirectsSupport,
).Bind(ctx, c.db, &rewards)
ownerRedirectsSupport,
validatorRedirectsSupport,
).Bind(ctx, c.db, &participations)
}

func (c *CalcCmd) populateRewardRedirectsTable(
func (c *CalcCmd) populateOwnerRedirectsTable(
ctx context.Context,
redirects rewards.Redirects,
redirects rewards.OwnerRedirects,
) error {
// Truncate the reward_redirects table.
// Truncate the owner_redirects table.
_, err := queries.Raw(
"TRUNCATE TABLE "+models.TableNames.RewardRedirects,
"TRUNCATE TABLE "+models.TableNames.OwnerRedirects,
).ExecContext(ctx, c.db)
if err != nil {
return fmt.Errorf("failed to truncate reward_redirects: %w", err)
return fmt.Errorf("failed to truncate owner_redirects: %w", err)
}

// Verify that the table is empty.
count, err := models.RewardRedirects().Count(ctx, c.db)
count, err := models.OwnerRedirects().Count(ctx, c.db)
if err != nil {
return fmt.Errorf("failed to count reward_redirects: %w", err)
return fmt.Errorf("failed to count owner_redirects: %w", err)
}
if count != 0 {
return fmt.Errorf("reward_redirects table was not truncated")
return fmt.Errorf("owner_redirects table was not truncated")
}

// Populate with given redirects.
Expand All @@ -466,7 +525,7 @@ func (c *CalcCmd) populateRewardRedirectsTable(
}
defer tx.Rollback()
for from, to := range redirects {
model := models.RewardRedirect{
model := models.OwnerRedirect{
FromAddress: from.String(),
ToAddress: to.String(),
}
Expand All @@ -479,12 +538,64 @@ func (c *CalcCmd) populateRewardRedirectsTable(
}

// Verify that the table is populated.
count, err = models.RewardRedirects().Count(ctx, c.db)
count, err = models.OwnerRedirects().Count(ctx, c.db)
if err != nil {
return fmt.Errorf("failed to count reward_redirects: %w", err)
return fmt.Errorf("failed to count owner_redirects: %w", err)
}
if int(count) != len(redirects) {
return fmt.Errorf("reward_redirects table was not populated")
return fmt.Errorf("owner_redirects table was not populated")
}

return nil
}

func (c *CalcCmd) populateValidatorRedirectsTable(
ctx context.Context,
redirects rewards.ValidatorRedirects,
) error {
// Truncate the validator_redirects table.
_, err := queries.Raw(
"TRUNCATE TABLE "+models.TableNames.ValidatorRedirects,
).ExecContext(ctx, c.db)
if err != nil {
return fmt.Errorf("failed to truncate validator_redirects: %w", err)
}

// Verify that the table is empty.
count, err := models.ValidatorRedirects().Count(ctx, c.db)
if err != nil {
return fmt.Errorf("failed to count validator_redirects: %w", err)
}
if count != 0 {
return fmt.Errorf("validator_redirects table was not truncated")
}

// Populate with given redirects.
tx, err := c.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback()
y0sher marked this conversation as resolved.
Show resolved Hide resolved
for pubkey, to := range redirects {
model := models.ValidatorRedirect{
PublicKey: pubkey.String(),
ToAddress: to.String(),
}
if err := model.Insert(ctx, tx, boil.Infer()); err != nil {
return fmt.Errorf("failed to insert rewards_redirect: %w", err)
}
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("failed to commit transaction: %w", err)
}

// Verify that the table is populated.
count, err = models.ValidatorRedirects().Count(ctx, c.db)
if err != nil {
return fmt.Errorf("failed to count validator_redirects: %w", err)
}
if int(count) != len(redirects) {
return fmt.Errorf("validator_redirects table was not populated")
}

return nil
Expand Down Expand Up @@ -561,3 +672,33 @@ func exportCSV(data any, fileName string) error {
}
return nil
}

func exportRedirectsToCSV(redirects interface{}, fileName string) error {
type RedirectRow struct {
From string `csv:"from"`
To string `csv:"to"`
}

var rows []RedirectRow

switch r := redirects.(type) {
case rewards.OwnerRedirects:
for from, to := range r {
rows = append(rows, RedirectRow{
From: from.String(),
To: to.String(),
})
}
case rewards.ValidatorRedirects:
for from, to := range r {
rows = append(rows, RedirectRow{
From: from.String(),
To: to.String(),
})
}
default:
return fmt.Errorf("unsupported redirects type: %T", redirects)
}

return exportCSV(rows, fileName)
}
6 changes: 4 additions & 2 deletions pkg/models/boil_table_names.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading