@@ -106,8 +106,11 @@ func Test_OperatorAllocationSnapshots(t *testing.T) {
106106 assert .Nil (t , res .Error )
107107
108108 // Generate snapshots
109- calculator := NewRewardsCalculator (grm , nil , cfg , l , sink , stakerOperators .NewStakerOperatorStore (grm , l ))
110- err := calculator .GenerateAndInsertOperatorAllocationSnapshots (snapshotDate )
109+ sog := stakerOperators .NewStakerOperatorGenerator (grm , l , cfg )
110+ calculator , err := NewRewardsCalculator (cfg , grm , nil , sog , sink , l )
111+ assert .Nil (t , err )
112+
113+ err = calculator .GenerateAndInsertOperatorAllocationSnapshots (snapshotDate )
111114 assert .Nil (t , err )
112115
113116 // Verify snapshots were created
@@ -121,19 +124,317 @@ func Test_OperatorAllocationSnapshots(t *testing.T) {
121124 assert .True (t , len (snapshots ) > 0 , "Expected snapshots to be generated" )
122125
123126 // Verify rounding behavior:
124- // - First allocation (1000) at block 100 (2024-11-14 10:00) should have snapshot starting 2024-11-15
125- // - Increase (1500) at block 101 (2024-11-14 15:00) should have snapshot starting 2024-11-15 (next day after 2024-11-14)
126- // - Decrease (500) at block 102 (2024-11-15 12:00) should have snapshot starting 2024-11-15 (same day)
127+ // - Block 100 (2024-11-14 10:00, mag 1000) and block 101 (2024-11-14 15:00, mag 1500) both on 2024-11-14
128+ // - SQL takes latest record per day: block 101 with magnitude 1500
129+ // - Since 1500 > 1000 (increase), rounds up to 2024-11-15
130+ // - Block 102 (2024-11-15 12:00, mag 500): decrease from 1500 to 500, rounds down to 2024-11-15
131+ // - Final allocation: 500 (the latest/current magnitude after all operations)
132+
133+ // Find the snapshot with magnitude 1500 (latest allocation on 2024-11-14)
134+ var mag1500Count int64
135+ res = grm .Raw (`
136+ SELECT COUNT(*) FROM operator_allocation_snapshots
137+ WHERE operator = ? AND avs = ? AND strategy = ? AND operator_set_id = ?
138+ AND magnitude = '1500' AND snapshot = ?
139+ ` , "0xoperator1" , "0xavs1" , "0xstrategy1" , 1 , "2024-11-15" ).Scan (& mag1500Count )
140+ assert .Nil (t , res .Error )
141+ assert .True (t , mag1500Count > 0 , "Expected magnitude 1500 to have snapshot on 2024-11-15 (latest record on 2024-11-14, rounded up)" )
142+ })
143+
144+ t .Run ("Basic allocation round up test" , func (t * testing.T ) {
145+ // Create test block
146+ blockTime := time .Date (2024 , 12 , 1 , 14 , 30 , 0 , 0 , time .UTC )
147+ blockNum := uint64 (200 )
148+ res := grm .Exec (`
149+ INSERT INTO blocks (number, hash, block_time, block_date, state_root, created_at, updated_at)
150+ VALUES (?, ?, ?, ?, '', NOW(), NOW())
151+ ` , blockNum , fmt .Sprintf ("hash_%d" , blockNum ), blockTime , blockTime .Format ("2006-01-02" ))
152+ assert .Nil (t , res .Error )
153+
154+ // First allocation should round up to next day (2024-12-02)
155+ res = grm .Exec (`
156+ INSERT INTO operator_allocations (operator, avs, strategy, operator_set_id, magnitude, effective_block, block_number, transaction_hash, log_index, created_at, updated_at)
157+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
158+ ` , "0xoperator2" , "0xavs2" , "0xstrategy2" , 2 , "2000" , blockNum , blockNum , "tx_200" , 1 )
159+ assert .Nil (t , res .Error )
160+
161+ sog := stakerOperators .NewStakerOperatorGenerator (grm , l , cfg )
162+ calculator , err := NewRewardsCalculator (cfg , grm , nil , sog , sink , l )
163+ assert .Nil (t , err )
164+
165+ err = calculator .GenerateAndInsertOperatorAllocationSnapshots ("2024-12-05" )
166+ assert .Nil (t , err )
127167
128- // Find the snapshot with magnitude 1000
129- var mag1000Count int64
168+ // Verify first allocation rounds up to 2024-12-02
169+ var count int64
170+ res = grm .Raw (`
171+ SELECT COUNT(*) FROM operator_allocation_snapshots
172+ WHERE operator = ? AND avs = ? AND strategy = ? AND operator_set_id = ?
173+ AND magnitude = '2000' AND snapshot = ?
174+ ` , "0xoperator2" , "0xavs2" , "0xstrategy2" , 2 , "2024-12-02" ).Scan (& count )
175+ assert .Nil (t , res .Error )
176+ assert .True (t , count > 0 , "Expected first allocation to round up to 2024-12-02" )
177+ })
178+
179+ t .Run ("Multiple allocations same day uses latest" , func (t * testing.T ) {
180+ // Create blocks - all on same day
181+ baseDate := time .Date (2024 , 12 , 10 , 0 , 0 , 0 , 0 , time .UTC )
182+ blocks := []struct {
183+ number uint64
184+ hour int
185+ magnitude string
186+ }{
187+ {300 , 8 , "3000" }, // 08:00
188+ {301 , 12 , "3500" }, // 12:00
189+ {302 , 18 , "4000" }, // 18:00 - latest, should be used
190+ }
191+
192+ for _ , b := range blocks {
193+ blockTime := baseDate .Add (time .Duration (b .hour ) * time .Hour )
194+ res := grm .Exec (`
195+ INSERT INTO blocks (number, hash, block_time, block_date, state_root, created_at, updated_at)
196+ VALUES (?, ?, ?, ?, '', NOW(), NOW())
197+ ` , b .number , fmt .Sprintf ("hash_%d" , b .number ), blockTime , blockTime .Format ("2006-01-02" ))
198+ assert .Nil (t , res .Error )
199+
200+ res = grm .Exec (`
201+ INSERT INTO operator_allocations (operator, avs, strategy, operator_set_id, magnitude, effective_block, block_number, transaction_hash, log_index, created_at, updated_at)
202+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
203+ ` , "0xoperator3" , "0xavs3" , "0xstrategy3" , 3 , b .magnitude , b .number , b .number , fmt .Sprintf ("tx_%d" , b .number ), 1 )
204+ assert .Nil (t , res .Error )
205+ }
206+
207+ sog := stakerOperators .NewStakerOperatorGenerator (grm , l , cfg )
208+ calculator , err := NewRewardsCalculator (cfg , grm , nil , sog , sink , l )
209+ assert .Nil (t , err )
210+
211+ err = calculator .GenerateAndInsertOperatorAllocationSnapshots ("2024-12-15" )
212+ assert .Nil (t , err )
213+
214+ // Verify only the latest allocation (4000) is used, rounded up to 2024-12-11
215+ var count int64
216+ res := grm .Raw (`
217+ SELECT COUNT(*) FROM operator_allocation_snapshots
218+ WHERE operator = ? AND avs = ? AND strategy = ? AND operator_set_id = ?
219+ AND magnitude = '4000' AND snapshot = ?
220+ ` , "0xoperator3" , "0xavs3" , "0xstrategy3" , 3 , "2024-12-11" ).Scan (& count )
221+ assert .Nil (t , res .Error )
222+ assert .True (t , count > 0 , "Expected latest allocation (4000) to be used, rounded up to 2024-12-11" )
223+
224+ // Verify earlier allocations are not present
225+ res = grm .Raw (`
226+ SELECT COUNT(*) FROM operator_allocation_snapshots
227+ WHERE operator = ? AND avs = ? AND strategy = ? AND operator_set_id = ?
228+ AND magnitude IN ('3000', '3500')
229+ ` , "0xoperator3" , "0xavs3" , "0xstrategy3" , 3 ).Scan (& count )
230+ assert .Nil (t , res .Error )
231+ assert .Equal (t , int64 (0 ), count , "Expected earlier allocations to not be present" )
232+ })
233+
234+ t .Run ("Allocate and deallocate same day uses latest" , func (t * testing.T ) {
235+ // Create blocks on same day
236+ baseDate := time .Date (2024 , 12 , 20 , 0 , 0 , 0 , 0 , time .UTC )
237+
238+ // Allocate at 10:00
239+ block1Time := baseDate .Add (10 * time .Hour )
240+ block1Num := uint64 (400 )
241+ res := grm .Exec (`
242+ INSERT INTO blocks (number, hash, block_time, block_date, state_root, created_at, updated_at)
243+ VALUES (?, ?, ?, ?, '', NOW(), NOW())
244+ ` , block1Num , fmt .Sprintf ("hash_%d" , block1Num ), block1Time , block1Time .Format ("2006-01-02" ))
245+ assert .Nil (t , res .Error )
246+
247+ res = grm .Exec (`
248+ INSERT INTO operator_allocations (operator, avs, strategy, operator_set_id, magnitude, effective_block, block_number, transaction_hash, log_index, created_at, updated_at)
249+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
250+ ` , "0xoperator4" , "0xavs4" , "0xstrategy4" , 4 , "5000" , block1Num , block1Num , "tx_400" , 1 )
251+ assert .Nil (t , res .Error )
252+
253+ // Deallocate at 16:00 (same day)
254+ block2Time := baseDate .Add (16 * time .Hour )
255+ block2Num := uint64 (401 )
256+ res = grm .Exec (`
257+ INSERT INTO blocks (number, hash, block_time, block_date, state_root, created_at, updated_at)
258+ VALUES (?, ?, ?, ?, '', NOW(), NOW())
259+ ` , block2Num , fmt .Sprintf ("hash_%d" , block2Num ), block2Time , block2Time .Format ("2006-01-02" ))
260+ assert .Nil (t , res .Error )
261+
262+ res = grm .Exec (`
263+ INSERT INTO operator_allocations (operator, avs, strategy, operator_set_id, magnitude, effective_block, block_number, transaction_hash, log_index, created_at, updated_at)
264+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
265+ ` , "0xoperator4" , "0xavs4" , "0xstrategy4" , 4 , "1000" , block2Num , block2Num , "tx_401" , 1 )
266+ assert .Nil (t , res .Error )
267+
268+ sog := stakerOperators .NewStakerOperatorGenerator (grm , l , cfg )
269+ calculator , err := NewRewardsCalculator (cfg , grm , nil , sog , sink , l )
270+ assert .Nil (t , err )
271+
272+ err = calculator .GenerateAndInsertOperatorAllocationSnapshots ("2024-12-25" )
273+ assert .Nil (t , err )
274+
275+ // Verify the latest record (deallocation to 1000) is used
276+ // Since it's a decrease, it should round down to 2024-12-20
277+ var count int64
130278 res = grm .Raw (`
131279 SELECT COUNT(*) FROM operator_allocation_snapshots
132280 WHERE operator = ? AND avs = ? AND strategy = ? AND operator_set_id = ?
133281 AND magnitude = '1000' AND snapshot = ?
134- ` , "0xoperator1" , "0xavs1" , "0xstrategy1" , 1 , "2024-11-15" ).Scan (& mag1000Count )
282+ ` , "0xoperator4" , "0xavs4" , "0xstrategy4" , 4 , "2024-12-20" ).Scan (& count )
283+ assert .Nil (t , res .Error )
284+ assert .True (t , count > 0 , "Expected latest deallocation (1000) to be used, rounded down to 2024-12-20" )
285+ })
286+
287+ t .Run ("Allocate day 1, deallocate to 0 day 2 at 12pm - 0 days counted" , func (t * testing.T ) {
288+ // Allocate on day 1
289+ day1 := time .Date (2025 , 1 , 10 , 10 , 0 , 0 , 0 , time .UTC )
290+ block1Num := uint64 (500 )
291+ res := grm .Exec (`
292+ INSERT INTO blocks (number, hash, block_time, block_date, state_root, created_at, updated_at)
293+ VALUES (?, ?, ?, ?, '', NOW(), NOW())
294+ ` , block1Num , fmt .Sprintf ("hash_%d" , block1Num ), day1 , day1 .Format ("2006-01-02" ))
295+ assert .Nil (t , res .Error )
296+
297+ res = grm .Exec (`
298+ INSERT INTO operator_allocations (operator, avs, strategy, operator_set_id, magnitude, effective_block, block_number, transaction_hash, log_index, created_at, updated_at)
299+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
300+ ` , "0xoperator5" , "0xavs5" , "0xstrategy5" , 5 , "6000" , block1Num , block1Num , "tx_500" , 1 )
301+ assert .Nil (t , res .Error )
302+
303+ // Deallocate to 0 on day 2 at 12pm (noon)
304+ day2Noon := time .Date (2025 , 1 , 11 , 12 , 0 , 0 , 0 , time .UTC )
305+ block2Num := uint64 (501 )
306+ res = grm .Exec (`
307+ INSERT INTO blocks (number, hash, block_time, block_date, state_root, created_at, updated_at)
308+ VALUES (?, ?, ?, ?, '', NOW(), NOW())
309+ ` , block2Num , fmt .Sprintf ("hash_%d" , block2Num ), day2Noon , day2Noon .Format ("2006-01-02" ))
310+ assert .Nil (t , res .Error )
311+
312+ res = grm .Exec (`
313+ INSERT INTO operator_allocations (operator, avs, strategy, operator_set_id, magnitude, effective_block, block_number, transaction_hash, log_index, created_at, updated_at)
314+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
315+ ` , "0xoperator5" , "0xavs5" , "0xstrategy5" , 5 , "0" , block2Num , block2Num , "tx_501" , 1 )
316+ assert .Nil (t , res .Error )
317+
318+ sog := stakerOperators .NewStakerOperatorGenerator (grm , l , cfg )
319+ calculator , err := NewRewardsCalculator (cfg , grm , nil , sog , sink , l )
320+ assert .Nil (t , err )
321+
322+ err = calculator .GenerateAndInsertOperatorAllocationSnapshots ("2025-01-15" )
323+ assert .Nil (t , err )
324+
325+ // Allocation on day 1 (2025-01-10) rounds up to 2025-01-11
326+ // Deallocation on day 2 (2025-01-11) rounds down to 2025-01-11
327+ // So the range is [2025-01-11, 2025-01-11) which generates no days
328+ var count int64
329+ res = grm .Raw (`
330+ SELECT COUNT(*) FROM operator_allocation_snapshots
331+ WHERE operator = ? AND avs = ? AND strategy = ? AND operator_set_id = ?
332+ ` , "0xoperator5" , "0xavs5" , "0xstrategy5" , 5 ).Scan (& count )
333+ assert .Nil (t , res .Error )
334+ assert .Equal (t , int64 (0 ), count , "Expected 0 days to be counted (allocation rounds up to day 2, deallocation rounds down to day 2)" )
335+ })
336+
337+ t .Run ("Allocate day 1, deallocate to 0 day 3 at 12pm - 1 day counted" , func (t * testing.T ) {
338+ // Allocate on day 1
339+ day1 := time .Date (2025 , 1 , 20 , 10 , 0 , 0 , 0 , time .UTC )
340+ block1Num := uint64 (600 )
341+ res := grm .Exec (`
342+ INSERT INTO blocks (number, hash, block_time, block_date, state_root, created_at, updated_at)
343+ VALUES (?, ?, ?, ?, '', NOW(), NOW())
344+ ` , block1Num , fmt .Sprintf ("hash_%d" , block1Num ), day1 , day1 .Format ("2006-01-02" ))
345+ assert .Nil (t , res .Error )
346+
347+ res = grm .Exec (`
348+ INSERT INTO operator_allocations (operator, avs, strategy, operator_set_id, magnitude, effective_block, block_number, transaction_hash, log_index, created_at, updated_at)
349+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
350+ ` , "0xoperator6" , "0xavs6" , "0xstrategy6" , 6 , "7000" , block1Num , block1Num , "tx_600" , 1 )
351+ assert .Nil (t , res .Error )
352+
353+ // Deallocate to 0 on day 3 at 12pm (noon)
354+ day3Noon := time .Date (2025 , 1 , 22 , 12 , 0 , 0 , 0 , time .UTC )
355+ block2Num := uint64 (601 )
356+ res = grm .Exec (`
357+ INSERT INTO blocks (number, hash, block_time, block_date, state_root, created_at, updated_at)
358+ VALUES (?, ?, ?, ?, '', NOW(), NOW())
359+ ` , block2Num , fmt .Sprintf ("hash_%d" , block2Num ), day3Noon , day3Noon .Format ("2006-01-02" ))
360+ assert .Nil (t , res .Error )
361+
362+ res = grm .Exec (`
363+ INSERT INTO operator_allocations (operator, avs, strategy, operator_set_id, magnitude, effective_block, block_number, transaction_hash, log_index, created_at, updated_at)
364+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
365+ ` , "0xoperator6" , "0xavs6" , "0xstrategy6" , 6 , "0" , block2Num , block2Num , "tx_601" , 1 )
366+ assert .Nil (t , res .Error )
367+
368+ sog := stakerOperators .NewStakerOperatorGenerator (grm , l , cfg )
369+ calculator , err := NewRewardsCalculator (cfg , grm , nil , sog , sink , l )
370+ assert .Nil (t , err )
371+
372+ err = calculator .GenerateAndInsertOperatorAllocationSnapshots ("2025-01-25" )
373+ assert .Nil (t , err )
374+
375+ // Allocation on day 1 (2025-01-20) rounds up to 2025-01-21
376+ // Deallocation on day 3 (2025-01-22) rounds down to 2025-01-22
377+ // So the range is [2025-01-21, 2025-01-22) which generates 1 day (2025-01-21)
378+ var count int64
379+ res = grm .Raw (`
380+ SELECT COUNT(*) FROM operator_allocation_snapshots
381+ WHERE operator = ? AND avs = ? AND strategy = ? AND operator_set_id = ?
382+ ` , "0xoperator6" , "0xavs6" , "0xstrategy6" , 6 ).Scan (& count )
383+ assert .Nil (t , res .Error )
384+ assert .Equal (t , int64 (1 ), count , "Expected 1 day to be counted (2025-01-21)" )
385+
386+ // Verify the snapshot is on 2025-01-21 with magnitude 7000
387+ res = grm .Raw (`
388+ SELECT COUNT(*) FROM operator_allocation_snapshots
389+ WHERE operator = ? AND avs = ? AND strategy = ? AND operator_set_id = ?
390+ AND magnitude = '7000' AND snapshot = ?
391+ ` , "0xoperator6" , "0xavs6" , "0xstrategy6" , 6 , "2025-01-21" ).Scan (& count )
392+ assert .Nil (t , res .Error )
393+ assert .Equal (t , int64 (1 ), count , "Expected snapshot on 2025-01-21 with magnitude 7000" )
394+ })
395+
396+ t .Run ("Future allocation - event at block 1, effective at block 10" , func (t * testing.T ) {
397+ // Event emitted at block 1 (day 1)
398+ day1 := time .Date (2025 , 2 , 1 , 10 , 0 , 0 , 0 , time .UTC )
399+ block1Num := uint64 (700 )
400+ res := grm .Exec (`
401+ INSERT INTO blocks (number, hash, block_time, block_date, state_root, created_at, updated_at)
402+ VALUES (?, ?, ?, ?, '', NOW(), NOW())
403+ ` , block1Num , fmt .Sprintf ("hash_%d" , block1Num ), day1 , day1 .Format ("2006-01-02" ))
404+ assert .Nil (t , res .Error )
405+
406+ // Effective block 10 (day 5)
407+ day5 := time .Date (2025 , 2 , 5 , 14 , 0 , 0 , 0 , time .UTC )
408+ block10Num := uint64 (710 )
409+ res = grm .Exec (`
410+ INSERT INTO blocks (number, hash, block_time, block_date, state_root, created_at, updated_at)
411+ VALUES (?, ?, ?, ?, '', NOW(), NOW())
412+ ` , block10Num , fmt .Sprintf ("hash_%d" , block10Num ), day5 , day5 .Format ("2006-01-02" ))
413+ assert .Nil (t , res .Error )
414+
415+ // Allocation event at block 1, effective at block 10
416+ res = grm .Exec (`
417+ INSERT INTO operator_allocations (operator, avs, strategy, operator_set_id, magnitude, effective_block, block_number, transaction_hash, log_index, created_at, updated_at)
418+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
419+ ` , "0xoperator7" , "0xavs7" , "0xstrategy7" , 7 , "8000" , block10Num , block1Num , "tx_700" , 1 )
420+ assert .Nil (t , res .Error )
421+
422+ sog := stakerOperators .NewStakerOperatorGenerator (grm , l , cfg )
423+ calculator , err := NewRewardsCalculator (cfg , grm , nil , sog , sink , l )
424+ assert .Nil (t , err )
425+
426+ err = calculator .GenerateAndInsertOperatorAllocationSnapshots ("2025-02-10" )
427+ assert .Nil (t , err )
428+
429+ // Allocation should use effective block's day (2025-02-05), round up to 2025-02-06
430+ var count int64
431+ res = grm .Raw (`
432+ SELECT COUNT(*) FROM operator_allocation_snapshots
433+ WHERE operator = ? AND avs = ? AND strategy = ? AND operator_set_id = ?
434+ AND magnitude = '8000' AND snapshot = ?
435+ ` , "0xoperator7" , "0xavs7" , "0xstrategy7" , 7 , "2025-02-06" ).Scan (& count )
135436 assert .Nil (t , res .Error )
136- assert .True (t , mag1000Count > 0 , "Expected magnitude 1000 to have snapshot on 2024-11-15 (rounded up) " )
437+ assert .True (t , count > 0 , "Expected allocation to round up based on effective block (2025-02-05) to 2025-02-06, not emission block " )
137438 })
138439
139440 t .Cleanup (func () {
0 commit comments