@@ -77,8 +77,6 @@ type catchpointFileWriter struct {
77
77
kvDone bool
78
78
onlineAccountRows trackerdb.TableIterator [* encoded.OnlineAccountRecordV6 ]
79
79
onlineAccountsDone bool
80
- onlineAccountPrev basics.Address
81
- onlineAccountPrevRound basics.Round
82
80
onlineRoundParamsRows trackerdb.TableIterator [* encoded.OnlineRoundParamsRecordV6 ]
83
81
onlineRoundParamsDone bool
84
82
}
@@ -390,7 +388,7 @@ func (cw *catchpointFileWriter) readDatabaseStep(ctx context.Context) error {
390
388
// Create the OnlineAccounts iterator JIT
391
389
if cw .onlineAccountRows == nil {
392
390
// MakeOrderedOnlineAccountsIter orders by (address, updateRound).
393
- rows , err := cw .tx .MakeOrderedOnlineAccountsIter (ctx , false , cw .onlineExcludeBefore )
391
+ rows , err := makeCatchpointOrderedOnlineAccountsIterFactory ( cw .tx .MakeOrderedOnlineAccountsIter , cw . accountsRound , cw . params ) (ctx , false , cw .onlineExcludeBefore )
394
392
if err != nil {
395
393
return err
396
394
}
@@ -403,70 +401,6 @@ func (cw *catchpointFileWriter) readDatabaseStep(ctx context.Context) error {
403
401
if err != nil {
404
402
return err
405
403
}
406
- // We set UpdateRound to 0 here, so that all nodes generating catchpoints will have the
407
- // verification hash for the onlineaccounts table data (which is used to calculate the
408
- // catchpoint label). Depending on the history of an online account, nodes may not have
409
- // the same updateRound column value for the oldest "horizon" row for that address,
410
- // depending on whether the node caught up from genesis, or restored from a
411
- // catchpoint. This does not have any impact on the correctness of online account
412
- // lookups, but is due to changes in the database schema over time:
413
- //
414
- // 1. For nodes that have been online for a long time, the unlimited assets release
415
- // (v3.5.1, PR #3652) introduced a BaseAccountData type with an UpdateRound field,
416
- // consensus-flagged to be zero until EnableAccountDataResourceSeparation was enabled
417
- // in consensus v32. So accounts that have been inactive since before consensus v32
418
- // will continue to have a zero UpdateRound, until a transaction updates the
419
- // account. This behavior is consistent for all nodes and validated by the merkle trie
420
- // generated each catchpoint round.
421
- //
422
- // 2. The onlineaccounts table, introduced later in v3.9.2 (PR #4003), uses a
423
- // migration to populate the onlineaccounts table by selecting all online accounts
424
- // from the accounts table. This migration copies the BaseAccountData.UpdateRound
425
- // field, along with voting data, to set the initial values of the onlineaccounts
426
- // table for each address. After that, the onlineaccounts table's updateRound column
427
- // would only be updated if voting data changed -- so certain transactions like
428
- // receiving a pay txn of 0 algos, or receiving an asset transfer, etc, would not
429
- // result in a new onlineaccounts row with a new updateRound (unless it triggered a
430
- // balance or voting data change). This criteria is implemented in
431
- // onlineAccountsNewRound in acctdeltas.go, separate from accountsNewRound &
432
- // makeCompactAccountDeltas, which set the account table's UpdateRound value.
433
- //
434
- // 3. Node operators using fast catchup to restore from a catchpoint file version V6
435
- // or V7 (used before v4.0.1 and consensus v40, which added the
436
- // EnableCatchpointsWithOnlineAccounts flag) initialize the onlineaccounts table by
437
- // first restoring the accounts table from the snapshot, then running the same
438
- // migration introduced in (2), where updateRound (and account data) comes from
439
- // BaseAccountData. This means catchpoint file writers and fast catchup users could
440
- // see some addresses have a horizon row with an updateRound that was set to zero
441
- // (case 1), or the round of the last account data change (case 2). Since v4.0.1,
442
- // catchpoint file version V8 includes the onlineaccounts and onlineroundparams tables
443
- // in snapshots, to support the voter_params_get and online_stake opcodes (PR #6177).
444
- //
445
- // 4. However, a node catching up from scratch without using fast catchup, running
446
- // v3.9.2 or later, must track the online account history to verify block certificates
447
- // as it validates each block in turn. It sets updateRound based on observing all
448
- // account voting data changes starting from round 0, whether or not
449
- // EnableAccountDataResourceSeparation is set. These nodes will have horizon rows for
450
- // addresses with updateRound set to the round of the last actual voting data change,
451
- // not zero (case 1) or the round of the last account data change (case 2).
452
- //
453
-
454
- // Is the updateRound for this row beyond the lookback horizon (R-320)?
455
- if oa .UpdateRound < catchpointLookbackHorizonForNextRound (cw .accountsRound , cw .params ) {
456
- // Is this the first (and thus oldest) row for this address?
457
- if cw .onlineAccountPrev .IsZero () || cw .onlineAccountPrev != oa .Address {
458
- // Then set it to 0.
459
- oa .UpdateRound = 0
460
- }
461
-
462
- // This case should never happen: there should only be one horizon row per account.
463
- if ! cw .onlineAccountPrev .IsZero () && cw .onlineAccountPrev == oa .Address {
464
- return fmt .Errorf ("bad online account data: multiple horizon rows for %s, prev updround %d cur updround %d" , oa .Address , cw .onlineAccountPrevRound , oa .UpdateRound )
465
- }
466
- }
467
-
468
- cw .onlineAccountPrev = oa .Address
469
- cw .onlineAccountPrevRound = oa .UpdateRound
470
404
onlineAccts = append (onlineAccts , * oa )
471
405
if len (onlineAccts ) == BalancesPerCatchpointFileChunk {
472
406
break
@@ -540,3 +474,108 @@ func hasContextDeadlineExceeded(ctx context.Context) (contextExceeded bool, cont
540
474
func catchpointLookbackHorizonForNextRound (rnd basics.Round , params config.ConsensusParams ) basics.Round {
541
475
return (rnd + 1 ).SubSaturate (basics .Round (params .MaxBalLookback ))
542
476
}
477
+
478
+ type catchpointOnlineAccountsIterWrapper struct {
479
+ iter trackerdb.TableIterator [* encoded.OnlineAccountRecordV6 ]
480
+ onlineAccountPrev basics.Address
481
+ onlineAccountPrevRound basics.Round
482
+ accountsRound basics.Round
483
+ params config.ConsensusParams
484
+ }
485
+
486
+ // makeCatchpointOrderedOnlineAccountsIter wraps the MakeOrderedOnlineAccountsIter iterator to deterministically set
487
+ // the UpdateRound number to zero for online accounts beyond the "horizon" of online history of 320 rounds (defined by
488
+ // MaxBalLookback).
489
+ func makeCatchpointOrderedOnlineAccountsIterFactory (
490
+ iterFactory func (context.Context , bool , basics.Round ) (trackerdb.TableIterator [* encoded.OnlineAccountRecordV6 ], error ),
491
+ accountsRound basics.Round ,
492
+ params config.ConsensusParams ,
493
+ ) (
494
+ wrappedIterFactory func (context.Context , bool , basics.Round ) (trackerdb.TableIterator [* encoded.OnlineAccountRecordV6 ], error ),
495
+ ) {
496
+ // return an iterFactory that wraps the provided iterFactory
497
+ return func (ctx context.Context , useStaging bool , excludeBefore basics.Round ) (trackerdb.TableIterator [* encoded.OnlineAccountRecordV6 ], error ) {
498
+ iter , err := iterFactory (ctx , useStaging , excludeBefore )
499
+ if err != nil {
500
+ return nil , err
501
+ }
502
+ return & catchpointOnlineAccountsIterWrapper {
503
+ iter : iter ,
504
+ accountsRound : accountsRound ,
505
+ params : params ,
506
+ }, nil
507
+ }
508
+ }
509
+
510
+ func (i * catchpointOnlineAccountsIterWrapper ) Next () bool { return i .iter .Next () }
511
+ func (i * catchpointOnlineAccountsIterWrapper ) Close () { i .iter .Close () }
512
+ func (i * catchpointOnlineAccountsIterWrapper ) GetItem () (* encoded.OnlineAccountRecordV6 , error ) {
513
+ oa , err := i .iter .GetItem ()
514
+ if err != nil {
515
+ return nil , err
516
+ }
517
+ // We set UpdateRound to 0 here, so that all nodes generating catchpoints will have the
518
+ // verification hash for the onlineaccounts table data (which is used to calculate the
519
+ // catchpoint label). Depending on the history of an online account, nodes may not have
520
+ // the same updateRound column value for the oldest "horizon" row for that address,
521
+ // depending on whether the node caught up from genesis, or restored from a
522
+ // catchpoint. This does not have any impact on the correctness of online account
523
+ // lookups, but is due to changes in the database schema over time:
524
+ //
525
+ // 1. For nodes that have been online for a long time, the unlimited assets release
526
+ // (v3.5.1, PR #3652) introduced a BaseAccountData type with an UpdateRound field,
527
+ // consensus-flagged to be zero until EnableAccountDataResourceSeparation was enabled
528
+ // in consensus v32. So accounts that have been inactive since before consensus v32
529
+ // will continue to have a zero UpdateRound, until a transaction updates the
530
+ // account. This behavior is consistent for all nodes and validated by the merkle trie
531
+ // generated each catchpoint round.
532
+ //
533
+ // 2. The onlineaccounts table, introduced later in v3.9.2 (PR #4003), uses a
534
+ // migration to populate the onlineaccounts table by selecting all online accounts
535
+ // from the accounts table. This migration copies the BaseAccountData.UpdateRound
536
+ // field, along with voting data, to set the initial values of the onlineaccounts
537
+ // table for each address. After that, the onlineaccounts table's updateRound column
538
+ // would only be updated if voting data changed -- so certain transactions like
539
+ // receiving a pay txn of 0 algos, or receiving an asset transfer, etc, would not
540
+ // result in a new onlineaccounts row with a new updateRound (unless it triggered a
541
+ // balance or voting data change). This criteria is implemented in
542
+ // onlineAccountsNewRound in acctdeltas.go, separate from accountsNewRound &
543
+ // makeCompactAccountDeltas, which set the account table's UpdateRound value.
544
+ //
545
+ // 3. Node operators using fast catchup to restore from a catchpoint file version V6
546
+ // or V7 (used before v4.0.1 and consensus v40, which added the
547
+ // EnableCatchpointsWithOnlineAccounts flag) initialize the onlineaccounts table by
548
+ // first restoring the accounts table from the snapshot, then running the same
549
+ // migration introduced in (2), where updateRound (and account data) comes from
550
+ // BaseAccountData. This means catchpoint file writers and fast catchup users could
551
+ // see some addresses have a horizon row with an updateRound that was set to zero
552
+ // (case 1), or the round of the last account data change (case 2). Since v4.0.1,
553
+ // catchpoint file version V8 includes the onlineaccounts and onlineroundparams tables
554
+ // in snapshots, to support the voter_params_get and online_stake opcodes (PR #6177).
555
+ //
556
+ // 4. However, a node catching up from scratch without using fast catchup, running
557
+ // v3.9.2 or later, must track the online account history to verify block certificates
558
+ // as it validates each block in turn. It sets updateRound based on observing all
559
+ // account voting data changes starting from round 0, whether or not
560
+ // EnableAccountDataResourceSeparation is set. These nodes will have horizon rows for
561
+ // addresses with updateRound set to the round of the last actual voting data change,
562
+ // not zero (case 1) or the round of the last account data change (case 2).
563
+ //
564
+ // Is the updateRound for this row beyond the lookback horizon (R-320)?
565
+ if oa .UpdateRound < catchpointLookbackHorizonForNextRound (i .accountsRound , i .params ) {
566
+ // Is this the first (and thus oldest) row for this address?
567
+ if i .onlineAccountPrev .IsZero () || i .onlineAccountPrev != oa .Address {
568
+ // Then set it to 0.
569
+ oa .UpdateRound = 0
570
+ }
571
+
572
+ // This case should never happen: there should only be one horizon row per account.
573
+ if ! i .onlineAccountPrev .IsZero () && i .onlineAccountPrev == oa .Address {
574
+ return nil , fmt .Errorf ("bad online account data: multiple horizon rows for %s, prev updround %d cur updround %d" , oa .Address , i .onlineAccountPrevRound , oa .UpdateRound )
575
+ }
576
+ }
577
+
578
+ i .onlineAccountPrev = oa .Address
579
+ i .onlineAccountPrevRound = oa .UpdateRound
580
+ return oa , nil
581
+ }
0 commit comments