@@ -3442,3 +3442,160 @@ func TestEncodedBaseResourceSize(t *testing.T) {
3442
3442
require .Less (t , len (encodedAsset ), len (encodedApp ))
3443
3443
require .GreaterOrEqual (t , MaxEncodedBaseResourceDataSize , len (encodedApp ))
3444
3444
}
3445
+
3446
+ // TestOnlineAccountsExceedOfflineRows checks for extra rows for offline accounts in online accounts table:
3447
+ // 1. Account is online
3448
+ // 2. Account goes offline and recorded in baseOnlineAccounts cache
3449
+ // 3. Many (>320 normally) rounds later, account gets deleted by prunning
3450
+ // 4. Account updated with a transfer
3451
+ // 5. Since it is still in baseOnlineAccounts, it fetched as offline and a new offline row is inserted
3452
+ // ==> 5 <== could lead to a ghost row in online accounts table that:
3453
+ // - are not needed but still correct
3454
+ // - make catchpoint generation inconsistent across nodes since it content depends on dynamic baseOnlineAccounts cache.
3455
+ //
3456
+ // 6. A similar behavior is exposed when there are multiple offline updates in a batch with the same result
3457
+ // of extra unnesesary rows in the online accounts table.
3458
+ func TestOnlineAccountsExceedOfflineRows (t * testing.T ) {
3459
+ partitiontest .PartitionTest (t )
3460
+ t .Parallel ()
3461
+
3462
+ dbs , _ := storetesting .DbOpenTest (t , true )
3463
+ storetesting .SetDbLogging (t , dbs )
3464
+ defer dbs .Close ()
3465
+
3466
+ tx , err := dbs .Wdb .Handle .Begin ()
3467
+ require .NoError (t , err )
3468
+ defer tx .Rollback ()
3469
+
3470
+ proto := config .Consensus [protocol .ConsensusCurrentVersion ]
3471
+
3472
+ var accts map [basics.Address ]basics.AccountData
3473
+ sqlitedriver .AccountsInitTest (t , tx , accts , protocol .ConsensusCurrentVersion )
3474
+
3475
+ addrA := ledgertesting .RandomAddress ()
3476
+
3477
+ // acct A is new, offline and then online => exercise new entry for account
3478
+ deltaA := onlineAccountDelta {
3479
+ address : addrA ,
3480
+ newAcct : []trackerdb.BaseOnlineAccountData {
3481
+ {
3482
+ MicroAlgos : basics.MicroAlgos {Raw : 100_000_000 },
3483
+ BaseVotingData : trackerdb.BaseVotingData {VoteFirstValid : 1 , VoteLastValid : 5 },
3484
+ },
3485
+ {
3486
+ MicroAlgos : basics.MicroAlgos {Raw : 100_000_000 },
3487
+ },
3488
+ },
3489
+ updRound : []uint64 {1 , 2 },
3490
+ newStatus : []basics.Status {basics .Online , basics .Offline },
3491
+ }
3492
+ updates := compactOnlineAccountDeltas {}
3493
+ updates .deltas = append (updates .deltas , deltaA )
3494
+ writer , err := sqlitedriver .MakeOnlineAccountsSQLWriter (tx , updates .len () > 0 )
3495
+ require .NoError (t , err )
3496
+ defer writer .Close ()
3497
+
3498
+ lastUpdateRound := basics .Round (2 )
3499
+ updated , err := onlineAccountsNewRoundImpl (writer , updates , proto , lastUpdateRound )
3500
+ require .NoError (t , err )
3501
+ require .Len (t , updated , 2 )
3502
+
3503
+ var baseOnlineAccounts lruOnlineAccounts
3504
+ baseOnlineAccounts .init (logging .TestingLog (t ), 1000 , 800 )
3505
+ for _ , persistedAcct := range updated {
3506
+ baseOnlineAccounts .write (persistedAcct )
3507
+ }
3508
+
3509
+ // make sure baseOnlineAccounts has the entry
3510
+ entry , has := baseOnlineAccounts .read (addrA )
3511
+ require .True (t , has )
3512
+ require .True (t , entry .AccountData .IsVotingEmpty ())
3513
+ require .Equal (t , basics .Round (2 ), entry .UpdRound )
3514
+
3515
+ queries , err := sqlitedriver .OnlineAccountsInitDbQueries (tx )
3516
+ require .NoError (t , err )
3517
+
3518
+ // make sure both rows are in the db
3519
+ history , _ , err := queries .LookupOnlineHistory (addrA )
3520
+ require .NoError (t , err )
3521
+ require .Len (t , history , 2 )
3522
+ // ASC ordered by updRound
3523
+ require .False (t , history [0 ].AccountData .IsVotingEmpty ())
3524
+ require .Equal (t , basics .Round (1 ), history [0 ].UpdRound )
3525
+ require .True (t , history [1 ].AccountData .IsVotingEmpty ())
3526
+ require .Equal (t , basics .Round (2 ), history [1 ].UpdRound )
3527
+
3528
+ // test case 1
3529
+ // simulate compact online delta construction with baseOnlineAccounts use
3530
+ acctDelta := ledgercore.AccountDeltas {}
3531
+ ad := ledgercore.AccountData {
3532
+ AccountBaseData : ledgercore.AccountBaseData {
3533
+ Status : basics .Offline ,
3534
+ MicroAlgos : basics.MicroAlgos {Raw : 100_000_000 - 1 },
3535
+ },
3536
+ }
3537
+ acctDelta .Upsert (addrA , ad )
3538
+ deltas := []ledgercore.AccountDeltas {acctDelta }
3539
+ updates = makeCompactOnlineAccountDeltas (deltas , 3 , baseOnlineAccounts )
3540
+
3541
+ // make sure old is filled from baseOnlineAccounts
3542
+ require .Empty (t , updates .misses )
3543
+ require .Len (t , updates .deltas , 1 )
3544
+ require .NotEmpty (t , updates .deltas [0 ].oldAcct )
3545
+ require .True (t , updates .deltas [0 ].oldAcct .AccountData .IsVotingEmpty ())
3546
+ require .Equal (t , 1 , updates .deltas [0 ].nOnlineAcctDeltas )
3547
+ require .Equal (t , basics .Offline , updates .deltas [0 ].newStatus [0 ])
3548
+ require .True (t , updates .deltas [0 ].newAcct [0 ].IsVotingEmpty ())
3549
+
3550
+ // insert and make sure no new rows are inserted
3551
+ lastUpdateRound = basics .Round (3 )
3552
+ updated , err = onlineAccountsNewRoundImpl (writer , updates , proto , lastUpdateRound )
3553
+ require .NoError (t , err )
3554
+ require .Len (t , updated , 0 )
3555
+
3556
+ history , _ , err = queries .LookupOnlineHistory (addrA )
3557
+ require .NoError (t , err )
3558
+ require .Len (t , history , 2 )
3559
+
3560
+ // test case 2
3561
+ // multiple offline entries in a single batch
3562
+
3563
+ addrB := ledgertesting .RandomAddress ()
3564
+
3565
+ // acct A is new, offline and then online => exercise new entry for account
3566
+ deltaB := onlineAccountDelta {
3567
+ address : addrB ,
3568
+ newAcct : []trackerdb.BaseOnlineAccountData {
3569
+ {
3570
+ MicroAlgos : basics.MicroAlgos {Raw : 100_000_000 },
3571
+ BaseVotingData : trackerdb.BaseVotingData {VoteFirstValid : 1 , VoteLastValid : 5 },
3572
+ },
3573
+ {
3574
+ MicroAlgos : basics.MicroAlgos {Raw : 100_000_000 },
3575
+ },
3576
+ {
3577
+ MicroAlgos : basics.MicroAlgos {Raw : 100_000_000 - 1 },
3578
+ },
3579
+ },
3580
+ updRound : []uint64 {4 , 5 , 6 },
3581
+ newStatus : []basics.Status {basics .Online , basics .Offline , basics .Offline },
3582
+ }
3583
+ updates = compactOnlineAccountDeltas {}
3584
+ updates .deltas = append (updates .deltas , deltaB )
3585
+
3586
+ lastUpdateRound = basics .Round (4 )
3587
+ updated , err = onlineAccountsNewRoundImpl (writer , updates , proto , lastUpdateRound )
3588
+ require .NoError (t , err )
3589
+ require .Len (t , updated , 2 ) // 3rd update is ignored
3590
+
3591
+ // make sure the last offline entry is ignored
3592
+ history , _ , err = queries .LookupOnlineHistory (addrB )
3593
+ require .NoError (t , err )
3594
+ require .Len (t , history , 2 )
3595
+
3596
+ // ASC ordered by updRound
3597
+ require .False (t , history [0 ].AccountData .IsVotingEmpty ())
3598
+ require .Equal (t , basics .Round (4 ), history [0 ].UpdRound )
3599
+ require .True (t , history [1 ].AccountData .IsVotingEmpty ())
3600
+ require .Equal (t , basics .Round (5 ), history [1 ].UpdRound )
3601
+ }
0 commit comments