Skip to content

Commit f51270b

Browse files
committed
create operator_allocation_snapshots table for rounding logic
1 parent 68e1b83 commit f51270b

File tree

5 files changed

+149
-152
lines changed

5 files changed

+149
-152
lines changed

pkg/eigenState/operatorAllocations/operatorAllocations.go

Lines changed: 0 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"slices"
88
"sort"
99
"strings"
10-
"time"
1110

1211
"github.com/Layr-Labs/sidecar/internal/config"
1312
"github.com/Layr-Labs/sidecar/pkg/eigenState/base"
@@ -28,7 +27,6 @@ type OperatorAllocation struct {
2827
BlockNumber uint64
2928
TransactionHash string
3029
LogIndex uint64
31-
EffectiveDate string // Rounded date when allocation takes effect (YYYY-MM-DD)
3230
}
3331

3432
type OperatorAllocationModel struct {
@@ -121,27 +119,6 @@ func (oa *OperatorAllocationModel) handleOperatorAllocationCreatedEvent(log *sto
121119
LogIndex: log.LogIndex,
122120
}
123121

124-
// Sabine fork: Apply rounding logic and populate date fields
125-
isSabineForkActive, err := oa.IsActiveForSabineForkBlockHeight(log.BlockNumber)
126-
if err != nil {
127-
return nil, err
128-
}
129-
if isSabineForkActive {
130-
effectiveDateStr, err := oa.calculateAllocationDates(
131-
log.BlockNumber,
132-
magnitude,
133-
strings.ToLower(outputData.Operator),
134-
strings.ToLower(outputData.OperatorSet.Avs),
135-
strings.ToLower(outputData.Strategy),
136-
outputData.OperatorSet.Id,
137-
)
138-
if err != nil {
139-
return nil, err
140-
}
141-
142-
split.EffectiveDate = effectiveDateStr
143-
}
144-
145122
return split, nil
146123
}
147124

@@ -364,122 +341,3 @@ func (oa *OperatorAllocationModel) IsActiveForBlockHeight(blockHeight uint64) (b
364341
return blockHeight >= forks[config.RewardsFork_Brazos].BlockNumber, nil
365342
}
366343

367-
func (oa *OperatorAllocationModel) IsActiveForSabineForkBlockHeight(blockHeight uint64) (bool, error) {
368-
forks, err := oa.globalConfig.GetRewardsSqlForkDates()
369-
if err != nil {
370-
oa.logger.Sugar().Errorw("Failed to get rewards sql fork dates", zap.Error(err))
371-
return false, err
372-
}
373-
374-
return blockHeight >= forks[config.RewardsFork_Sabine].BlockNumber, nil
375-
}
376-
377-
// calculateAllocationDates calculates the effective date for an allocation
378-
// This encapsulates the logic for querying block data and applying rounding rules
379-
// Block timestamp can be derived from block_number FK to blocks table
380-
func (oa *OperatorAllocationModel) calculateAllocationDates(
381-
blockNumber uint64,
382-
magnitude *big.Int,
383-
operator string,
384-
avs string,
385-
strategy string,
386-
operatorSetId uint64,
387-
) (effectiveDateStr string, err error) {
388-
// 1. Get block timestamp from blocks table
389-
var block storage.Block
390-
result := oa.DB.Where("number = ?", blockNumber).First(&block)
391-
if result.Error != nil {
392-
oa.logger.Sugar().Errorw("Failed to query block timestamp",
393-
zap.Error(result.Error),
394-
zap.Uint64("blockNumber", blockNumber),
395-
)
396-
return "", fmt.Errorf("failed to query block timestamp: %w", result.Error)
397-
}
398-
399-
// 2. Get previous allocation magnitude for comparison
400-
previousMagnitude, err := oa.getPreviousAllocationMagnitude(
401-
operator,
402-
avs,
403-
strategy,
404-
operatorSetId,
405-
blockNumber,
406-
)
407-
if err != nil {
408-
oa.logger.Sugar().Errorw("Failed to get previous allocation magnitude",
409-
zap.Error(err),
410-
zap.Uint64("blockNumber", blockNumber),
411-
zap.String("operator", operator),
412-
zap.String("avs", avs),
413-
zap.String("strategy", strategy),
414-
zap.Uint64("operatorSetId", operatorSetId),
415-
)
416-
return "", fmt.Errorf("failed to get previous allocation magnitude: %w", err)
417-
}
418-
419-
// 3. Determine effective date using rounding rules
420-
effectiveDate := oa.determineEffectiveDate(block.BlockTime, magnitude, previousMagnitude)
421-
422-
// 4. Format and return date string
423-
effectiveDateStr = effectiveDate.Format("2006-01-02")
424-
425-
return effectiveDateStr, nil
426-
}
427-
428-
// getPreviousAllocationMagnitude retrieves the most recent allocation magnitude
429-
// for the given operator-avs-strategy combination before the specified block number
430-
func (oa *OperatorAllocationModel) getPreviousAllocationMagnitude(
431-
operator string,
432-
avs string,
433-
strategy string,
434-
operatorSetId uint64,
435-
currentBlockNumber uint64,
436-
) (*big.Int, error) {
437-
var previousAllocation OperatorAllocation
438-
439-
result := oa.DB.
440-
Where("operator = ?", operator).
441-
Where("avs = ?", avs).
442-
Where("strategy = ?", strategy).
443-
Where("operator_set_id = ?", operatorSetId).
444-
Where("block_number < ?", currentBlockNumber).
445-
Order("block_number DESC, log_index DESC").
446-
Limit(1).
447-
First(&previousAllocation)
448-
449-
if result.Error != nil {
450-
if result.Error == gorm.ErrRecordNotFound {
451-
// No previous allocation found, return 0
452-
return big.NewInt(0), nil
453-
}
454-
return nil, result.Error
455-
}
456-
457-
magnitude, success := new(big.Int).SetString(previousAllocation.Magnitude, 10)
458-
if !success {
459-
return nil, fmt.Errorf("failed to parse previous magnitude: %s", previousAllocation.Magnitude)
460-
}
461-
462-
return magnitude, nil
463-
}
464-
465-
// determineEffectiveDate determines the effective date for an allocation based on rounding rules
466-
// - Allocation (increase): Round UP to next day
467-
// - Deallocation (decrease): Round DOWN to current day
468-
func (oa *OperatorAllocationModel) determineEffectiveDate(
469-
blockTimestamp time.Time,
470-
newMagnitude *big.Int,
471-
previousMagnitude *big.Int,
472-
) time.Time {
473-
blockTimestamp = blockTimestamp.UTC()
474-
comparison := newMagnitude.Cmp(previousMagnitude)
475-
476-
year, month, day := blockTimestamp.Date()
477-
midnightUTC := time.Date(year, month, day, 0, 0, 0, 0, time.UTC)
478-
479-
if comparison > 0 {
480-
// Allocation (increase) - always round up to next day
481-
return midnightUTC.Add(24 * time.Hour)
482-
}
483-
// Deallocation (decrease or no change) - round down to current day
484-
return midnightUTC
485-
}

pkg/postgres/migrations/202511141700_withdrawalQueueAndAllocationRounding/up.go

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ func (m *Migration) Up(db *sql.DB, grm *gorm.DB, cfg *config.Config) error {
1717
// =============================================================================
1818

1919
// Add completion tracking (timestamps can be derived from blocks table via FK)
20-
`alter table queued_slashing_withdrawals add column if not exists completed boolean default false`,
2120
`alter table queued_slashing_withdrawals add column if not exists completion_block_number bigint`,
2221

2322
// Add FK constraint for completion block
@@ -28,22 +27,29 @@ func (m *Migration) Up(db *sql.DB, grm *gorm.DB, cfg *config.Config) error {
2827
// =============================================================================
2928

3029
// Index for finding active (non-completed) withdrawals
31-
`create index if not exists idx_queued_withdrawals_active on queued_slashing_withdrawals(staker, operator, completed) where completed = false`,
30+
`create index if not exists idx_queued_withdrawals_active on queued_slashing_withdrawals(staker, operator) where completion_block_number is null`,
3231

3332
// Index for completion queries
34-
`create index if not exists idx_queued_withdrawals_completed on queued_slashing_withdrawals(completion_block_number) where completed = true`,
33+
`create index if not exists idx_queued_withdrawals_completed on queued_slashing_withdrawals(completion_block_number) where completion_block_number is not null`,
3534

3635
// =============================================================================
37-
// PART 3: Update operator_allocations table for rounding logic
36+
// PART 3: Create operator_allocation_snapshots table for rewards calculation
3837
// =============================================================================
3938

40-
// Add effective_date column for allocation/deallocation rounding
41-
// This is a computed value based on magnitude changes (round UP for increases, DOWN for decreases)
42-
// block_timestamp can be derived from block_number FK to blocks table
43-
`alter table operator_allocations add column if not exists effective_date date`,
39+
// Create snapshot table following the same pattern as staker_share_snapshots
40+
// This table will be populated during rewards calculation with rounding logic applied
41+
`CREATE TABLE IF NOT EXISTS operator_allocation_snapshots (
42+
operator varchar not null,
43+
avs varchar not null,
44+
strategy varchar not null,
45+
operator_set_id bigint not null,
46+
magnitude numeric not null,
47+
snapshot date not null
48+
)`,
4449

45-
// Create index for effective_date queries
46-
`create index if not exists idx_operator_allocations_effective_date on operator_allocations(operator, avs, strategy, effective_date)`,
50+
// Create indexes for efficient snapshot queries
51+
`create index if not exists idx_operator_allocation_snapshots_operator_avs_strategy_snapshot on operator_allocation_snapshots (operator, avs, strategy, snapshot)`,
52+
`create index if not exists idx_operator_allocation_snapshots_strategy_snapshot on operator_allocation_snapshots (strategy, snapshot)`,
4753
}
4854

4955
for _, query := range queries {
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package rewards
2+
3+
import (
4+
"github.com/Layr-Labs/sidecar/pkg/rewardsUtils"
5+
"go.uber.org/zap"
6+
)
7+
8+
const operatorAllocationSnapshotsQuery = `
9+
insert into operator_allocation_snapshots(operator, avs, strategy, operator_set_id, magnitude, snapshot)
10+
WITH ranked_allocation_records as (
11+
SELECT *,
12+
ROW_NUMBER() OVER (PARTITION BY operator, avs, strategy, operator_set_id, cast(block_time AS DATE) ORDER BY block_time DESC, log_index DESC) AS rn
13+
FROM operator_allocations oa
14+
INNER JOIN blocks b ON oa.block_number = b.number
15+
WHERE b.block_time < TIMESTAMP '{{.cutoffDate}}'
16+
),
17+
-- Get the latest record for each day
18+
daily_records as (
19+
SELECT
20+
operator,
21+
avs,
22+
strategy,
23+
operator_set_id,
24+
magnitude,
25+
block_time,
26+
block_number,
27+
log_index
28+
FROM ranked_allocation_records
29+
WHERE rn = 1
30+
),
31+
-- Compare each record with the previous record to determine if it's an increase or decrease
32+
records_with_comparison as (
33+
SELECT
34+
operator,
35+
avs,
36+
strategy,
37+
operator_set_id,
38+
magnitude,
39+
block_time,
40+
LAG(magnitude) OVER (PARTITION BY operator, avs, strategy, operator_set_id ORDER BY block_time, block_number, log_index) as previous_magnitude,
41+
-- Allocation (increase): Round UP to next day
42+
-- Deallocation (decrease or no change): Round DOWN to current day
43+
CASE
44+
WHEN LAG(magnitude) OVER (PARTITION BY operator, avs, strategy, operator_set_id ORDER BY block_time, block_number, log_index) IS NULL THEN
45+
-- First allocation: round down to current day (conservative default)
46+
date_trunc('day', block_time)
47+
WHEN magnitude > LAG(magnitude) OVER (PARTITION BY operator, avs, strategy, operator_set_id ORDER BY block_time, block_number, log_index) THEN
48+
-- Increase: round up to next day
49+
date_trunc('day', block_time) + INTERVAL '1' day
50+
ELSE
51+
-- Decrease or no change: round down to current day
52+
date_trunc('day', block_time)
53+
END AS snapshot_time
54+
FROM daily_records
55+
),
56+
-- Get the range for each operator, avs, strategy, operator_set_id combination
57+
allocation_windows as (
58+
SELECT
59+
operator,
60+
avs,
61+
strategy,
62+
operator_set_id,
63+
magnitude,
64+
snapshot_time as start_time,
65+
CASE
66+
-- If the range does not have the end, use the current timestamp truncated to 0 UTC
67+
WHEN LEAD(snapshot_time) OVER (PARTITION BY operator, avs, strategy, operator_set_id ORDER BY snapshot_time) is null THEN date_trunc('day', TIMESTAMP '{{.cutoffDate}}')
68+
ELSE LEAD(snapshot_time) OVER (PARTITION BY operator, avs, strategy, operator_set_id ORDER BY snapshot_time)
69+
END AS end_time
70+
FROM records_with_comparison
71+
),
72+
cleaned_records as (
73+
SELECT * FROM allocation_windows
74+
WHERE start_time < end_time
75+
)
76+
SELECT
77+
operator,
78+
avs,
79+
strategy,
80+
operator_set_id,
81+
magnitude,
82+
cast(day AS DATE) AS snapshot
83+
FROM
84+
cleaned_records
85+
CROSS JOIN
86+
generate_series(DATE(start_time), DATE(end_time) - interval '1' day, interval '1' day) AS day
87+
on conflict do nothing;
88+
`
89+
90+
func (r *RewardsCalculator) GenerateAndInsertOperatorAllocationSnapshots(snapshotDate string) error {
91+
query, err := rewardsUtils.RenderQueryTemplate(operatorAllocationSnapshotsQuery, map[string]interface{}{
92+
"cutoffDate": snapshotDate,
93+
})
94+
if err != nil {
95+
r.logger.Sugar().Errorw("Failed to render query template", "error", err)
96+
return err
97+
}
98+
99+
res := r.grm.Exec(query)
100+
if res.Error != nil {
101+
r.logger.Sugar().Errorw("Failed to generate operator_allocation_snapshots",
102+
zap.String("snapshotDate", snapshotDate),
103+
zap.Error(res.Error),
104+
)
105+
return res.Error
106+
}
107+
return nil
108+
}
109+
110+
func (r *RewardsCalculator) ListOperatorAllocationSnapshots() ([]*OperatorAllocationSnapshot, error) {
111+
var snapshots []*OperatorAllocationSnapshot
112+
res := r.grm.Model(&OperatorAllocationSnapshot{}).Find(&snapshots)
113+
if res.Error != nil {
114+
r.logger.Sugar().Errorw("Failed to list operator allocation snapshots", "error", res.Error)
115+
return nil, res.Error
116+
}
117+
return snapshots, nil
118+
}

pkg/rewards/rewards.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,12 @@ func (rc *RewardsCalculator) generateSnapshotData(snapshotDate string) error {
667667
}
668668
rc.logger.Sugar().Debugw("Generated operator share snapshots")
669669

670+
if err = rc.GenerateAndInsertOperatorAllocationSnapshots(snapshotDate); err != nil {
671+
rc.logger.Sugar().Errorw("Failed to generate operator allocation snapshots", "error", err)
672+
return err
673+
}
674+
rc.logger.Sugar().Debugw("Generated operator allocation snapshots")
675+
670676
if err = rc.GenerateAndInsertStakerShareSnapshots(snapshotDate); err != nil {
671677
rc.logger.Sugar().Errorw("Failed to generate staker share snapshots", "error", err)
672678
return err

pkg/rewards/tables.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,12 @@ type OperatorDirectedOperatorSetRewards struct {
153153
BlockTime time.Time
154154
BlockDate string
155155
}
156+
157+
type OperatorAllocationSnapshot struct {
158+
Operator string
159+
Avs string
160+
Strategy string
161+
OperatorSetId uint64
162+
Magnitude string
163+
Snapshot time.Time
164+
}

0 commit comments

Comments
 (0)