@@ -3442,3 +3442,160 @@ func TestEncodedBaseResourceSize(t *testing.T) {
34423442 require .Less (t , len (encodedAsset ), len (encodedApp ))
34433443 require .GreaterOrEqual (t , MaxEncodedBaseResourceDataSize , len (encodedApp ))
34443444}
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