-
Notifications
You must be signed in to change notification settings - Fork 0
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: utilize split in existing rewards calculation #117
base: release/rewards-v2
Are you sure you want to change the base?
Changes from 7 commits
7c732ed
383c832
c571d20
6c3355a
5e83b2a
2dadcf2
d60cdac
3af7b0e
0eb7412
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package _202411221331_operatorAVSSplitSnapshots | ||
|
||
import ( | ||
"database/sql" | ||
|
||
"gorm.io/gorm" | ||
) | ||
|
||
type Migration struct { | ||
} | ||
|
||
func (m *Migration) Up(db *sql.DB, grm *gorm.DB) error { | ||
queries := []string{ | ||
`CREATE TABLE IF NOT EXISTS operator_avs_split_snapshots ( | ||
operator varchar not null, | ||
avs varchar not null, | ||
split integer not null, | ||
snapshot date not null | ||
)`, | ||
} | ||
for _, query := range queries { | ||
if _, err := db.Exec(query); err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
func (m *Migration) GetName() string { | ||
return "202411221331_operatorAVSSplitSnapshots" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package _202411221331_operatorPISplitSnapshots | ||
|
||
import ( | ||
"database/sql" | ||
|
||
"gorm.io/gorm" | ||
) | ||
|
||
type Migration struct { | ||
} | ||
|
||
func (m *Migration) Up(db *sql.DB, grm *gorm.DB) error { | ||
queries := []string{ | ||
`CREATE TABLE IF NOT EXISTS operator_pi_split_snapshots ( | ||
operator varchar not null, | ||
split integer not null, | ||
snapshot date not null | ||
)`, | ||
} | ||
for _, query := range queries { | ||
if _, err := db.Exec(query); err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
func (m *Migration) GetName() string { | ||
return "202411221331_operatorPISplitSnapshots" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ package rewards | |
|
||
import ( | ||
"database/sql" | ||
|
||
"github.com/Layr-Labs/sidecar/internal/config" | ||
"go.uber.org/zap" | ||
) | ||
|
@@ -106,12 +107,23 @@ staker_operator_total_tokens AS ( | |
FLOOR(staker_proportion * tokens_per_day_decimal) as total_staker_operator_payout | ||
FROM staker_proportion | ||
), | ||
-- Calculate the token breakdown for each (staker, operator) pair | ||
-- Include the operator_pi_split_snapshots table | ||
operator_pi_splits_cte AS ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: we don't use cte in naming anywhere There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did that to prevent a conflict with the existing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure why this is needed at all. Why not just reference operator_pi_split_snapshots where you need it? You don’t really gain any advantage by selecting just a few columns from it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah good point. I don't know why I did it that way. Fixed in 0eb7412 |
||
SELECT | ||
operator, | ||
snapshot, | ||
split | ||
FROM operator_pi_split_snapshots | ||
), | ||
-- Calculate the token breakdown for each (staker, operator) pair with dynamic split logic | ||
-- If no split is found, default to 1000 (10%) | ||
token_breakdowns AS ( | ||
SELECT *, | ||
floor(total_staker_operator_payout * 0.10) as operator_tokens, | ||
total_staker_operator_payout - floor(total_staker_operator_payout * 0.10) as staker_tokens | ||
FROM staker_operator_total_tokens | ||
SELECT sot.*, | ||
floor(total_staker_operator_payout * COALESCE(ops.split, 1000) / 10000.0) as operator_tokens, | ||
total_staker_operator_payout - floor(total_staker_operator_payout * COALESCE(ops.split, 1000) / 10000.0) as staker_tokens | ||
FROM staker_operator_total_tokens sot | ||
LEFT JOIN operator_pi_splits_cte ops | ||
ON sot.operator = ops.operator AND sot.snapshot = ops.snapshot | ||
) | ||
SELECT * from token_breakdowns | ||
ORDER BY reward_hash, snapshot, staker, operator | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
package rewards | ||
|
||
const operatorAvsSplitSnapshotQuery = ` | ||
WITH operator_avs_splits_with_block_info as ( | ||
select | ||
oas.operator, | ||
oas.avs, | ||
oas.activated_at::timestamp(6) as activated_at, | ||
oas.new_operator_avs_split_bips as split, | ||
oas.block_number, | ||
oas.log_index, | ||
b.block_time::timestamp(6) as block_time | ||
from operator_avs_splits as oas | ||
left join blocks as b on (b.number = oas.block_number) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know this is my fault for doing it elsewhere, but this should probably be just a |
||
where activated_at < TIMESTAMP '{{.cutoffDate}}' | ||
), | ||
-- Rank the records for each combination of (operator, avs, activation date) by activation time, block time and log index | ||
ranked_operator_avs_split_records as ( | ||
SELECT *, | ||
ROW_NUMBER() OVER (PARTITION BY operator, avs, cast(activated_at AS DATE) ORDER BY activated_at DESC, block_time DESC, log_index DESC) AS rn | ||
FROM operator_avs_splits_with_block_info | ||
), | ||
-- Get the latest record for each day & round up to the snapshot day | ||
snapshotted_records as ( | ||
SELECT | ||
operator, | ||
avs, | ||
split, | ||
block_time, | ||
date_trunc('day', activated_at) + INTERVAL '1' day AS snapshot_time | ||
from ranked_operator_avs_split_records | ||
where rn = 1 | ||
), | ||
-- Get the range for each operator, avs pairing | ||
operator_avs_split_windows as ( | ||
SELECT | ||
operator, avs, split, snapshot_time as start_time, | ||
CASE | ||
-- If the range does not have the end, use the current timestamp truncated to 0 UTC | ||
WHEN LEAD(snapshot_time) OVER (PARTITION BY operator, avs ORDER BY snapshot_time) is null THEN date_trunc('day', TIMESTAMP '{{.cutoffDate}}') | ||
ELSE LEAD(snapshot_time) OVER (PARTITION BY operator, avs ORDER BY snapshot_time) | ||
END AS end_time | ||
FROM snapshotted_records | ||
), | ||
-- Clean up any records where start_time >= end_time | ||
cleaned_records as ( | ||
SELECT * FROM operator_avs_split_windows | ||
WHERE start_time < end_time | ||
), | ||
-- Generate a snapshot for each day in the range | ||
final_results as ( | ||
SELECT | ||
operator, | ||
avs, | ||
split, | ||
d AS snapshot | ||
FROM | ||
cleaned_records | ||
CROSS JOIN | ||
generate_series(DATE(start_time), DATE(end_time) - interval '1' day, interval '1' day) AS d | ||
) | ||
select * from final_results | ||
` | ||
|
||
func (r *RewardsCalculator) GenerateAndInsertOperatorAvsSplitSnapshots(snapshotDate string) error { | ||
tableName := "operator_avs_split_snapshots" | ||
|
||
query, err := renderQueryTemplate(operatorAvsSplitSnapshotQuery, map[string]string{ | ||
"cutoffDate": snapshotDate, | ||
}) | ||
if err != nil { | ||
r.logger.Sugar().Errorw("Failed to render query template", "error", err) | ||
return err | ||
} | ||
|
||
err = r.generateAndInsertFromQuery(tableName, query, nil) | ||
if err != nil { | ||
r.logger.Sugar().Errorw("Failed to generate operator_avs_split_snapshots", "error", err) | ||
return err | ||
} | ||
return nil | ||
} | ||
|
||
func (r *RewardsCalculator) ListOperatorAvsSplitSnapshots() ([]*OperatorAVSSplitSnapshots, error) { | ||
var snapshots []*OperatorAVSSplitSnapshots | ||
res := r.grm.Model(&OperatorAVSSplitSnapshots{}).Find(&snapshots) | ||
if res.Error != nil { | ||
r.logger.Sugar().Errorw("Failed to list operator avs split snapshots", "error", res.Error) | ||
return nil, res.Error | ||
} | ||
return snapshots, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
package rewards | ||
|
||
const operatorPISplitSnapshotQuery = ` | ||
WITH operator_pi_splits_with_block_info as ( | ||
select | ||
ops.operator, | ||
ops.activated_at::timestamp(6) as activated_at, | ||
ops.new_operator_avs_split_bips as split, | ||
ops.block_number, | ||
ops.log_index, | ||
b.block_time::timestamp(6) as block_time | ||
from operator_pi_splits as ops | ||
left join blocks as b on (b.number = ops.block_number) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as the comment above; should probably make this an inner join |
||
where activated_at < TIMESTAMP '{{.cutoffDate}}' | ||
), | ||
-- Rank the records for each combination of (operator, activation date) by activation time, block time and log index | ||
ranked_operator_pi_split_records as ( | ||
SELECT *, | ||
ROW_NUMBER() OVER (PARTITION BY operator, cast(activated_at AS DATE) ORDER BY activated_at DESC, block_time DESC, log_index DESC) AS rn | ||
FROM operator_pi_splits_with_block_info | ||
), | ||
-- Get the latest record for each day & round up to the snapshot day | ||
snapshotted_records as ( | ||
SELECT | ||
operator, | ||
split, | ||
block_time, | ||
date_trunc('day', activated_at) + INTERVAL '1' day AS snapshot_time | ||
from ranked_operator_pi_split_records | ||
where rn = 1 | ||
), | ||
-- Get the range for each operator | ||
operator_pi_split_windows as ( | ||
SELECT | ||
operator, split, snapshot_time as start_time, | ||
CASE | ||
-- If the range does not have the end, use the current timestamp truncated to 0 UTC | ||
WHEN LEAD(snapshot_time) OVER (PARTITION BY operator ORDER BY snapshot_time) is null THEN date_trunc('day', TIMESTAMP '{{.cutoffDate}}') | ||
ELSE LEAD(snapshot_time) OVER (PARTITION BY operator ORDER BY snapshot_time) | ||
END AS end_time | ||
FROM snapshotted_records | ||
), | ||
-- Clean up any records where start_time >= end_time | ||
cleaned_records as ( | ||
SELECT * FROM operator_pi_split_windows | ||
WHERE start_time < end_time | ||
), | ||
-- Generate a snapshot for each day in the range | ||
final_results as ( | ||
SELECT | ||
operator, | ||
split, | ||
d AS snapshot | ||
FROM | ||
cleaned_records | ||
CROSS JOIN | ||
generate_series(DATE(start_time), DATE(end_time) - interval '1' day, interval '1' day) AS d | ||
) | ||
select * from final_results | ||
` | ||
|
||
func (r *RewardsCalculator) GenerateAndInsertOperatorPISplitSnapshots(snapshotDate string) error { | ||
tableName := "operator_pi_split_snapshots" | ||
|
||
query, err := renderQueryTemplate(operatorPISplitSnapshotQuery, map[string]string{ | ||
"cutoffDate": snapshotDate, | ||
}) | ||
if err != nil { | ||
r.logger.Sugar().Errorw("Failed to render query template", "error", err) | ||
return err | ||
} | ||
|
||
err = r.generateAndInsertFromQuery(tableName, query, nil) | ||
if err != nil { | ||
r.logger.Sugar().Errorw("Failed to generate operator_pi_split_snapshots", "error", err) | ||
return err | ||
} | ||
return nil | ||
} | ||
|
||
func (r *RewardsCalculator) ListOperatorPISplitSnapshots() ([]*OperatorPISplitSnapshots, error) { | ||
var snapshots []*OperatorPISplitSnapshots | ||
res := r.grm.Model(&OperatorPISplitSnapshots{}).Find(&snapshots) | ||
if res.Error != nil { | ||
r.logger.Sugar().Errorw("Failed to list operator pi split snapshots", "error", res.Error) | ||
return nil, res.Error | ||
} | ||
return snapshots, nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we know what type the output of
COALESCE(oas.split, 1000) / 10000.0
is? We want thetotal_staker_operator_payout * split
to be anumeric * numeric
and not anumeric * float
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean the existing
0.10
is a float / double. So I've stuck with that. I believe the floor does round it down to a whole number.