Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

incentives: cache top online accounts and use when building AbsentParticipationAccounts #6085

Open
wants to merge 25 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
21db44d
periodically track top N online accounts in Ledger, and use when buil…
cce Jul 25, 2024
e968515
run make msgp
cce Jul 26, 2024
8587b28
update votersTracker
cce Aug 9, 2024
38d4b8d
CR fixes
cce Aug 9, 2024
9740ddc
rename and simplify
cce Aug 9, 2024
b8b9673
add LastProposed/LastHeartbeat to BaseOnlineAccountData handling
cce Aug 9, 2024
0b7fbad
fill out LastHeartbeat/LastProposed from #5965
cce Aug 9, 2024
b2f1130
fix remaining tests
cce Aug 9, 2024
5242990
fix missing AccountData => OnlineAccountData transformation function …
cce Aug 9, 2024
0baf81f
Merge remote-tracking branch 'upstream/master' into track-incentive-c…
cce Aug 10, 2024
be464cf
Update ledger/ledger.go
cce Aug 20, 2024
97d0bcf
Update ledger/ledger.go
cce Aug 21, 2024
55d5068
Merge remote-tracking branch 'upstream/master' into track-incentive-c…
cce Aug 28, 2024
01b150a
update TestAbsenteeChecks
cce Sep 18, 2024
0f954d1
Merge remote-tracking branch 'upstream/master' into track-incentive-c…
cce Sep 18, 2024
c24e809
also consider candidates for ExpiredParticipationAccounts
cce Sep 18, 2024
c252d95
update TestExpiredAccountGeneration
cce Sep 18, 2024
bb82a97
Fix TestAbsentTracking
cce Sep 19, 2024
04c0a84
add TestLatestCompletedVotersUpTo
cce Sep 24, 2024
c41a49a
don't propose to suspend yourself, and update TestAbsenteeChallenges
cce Oct 4, 2024
e476730
Update TestExpiredAccountGeneration
cce Oct 4, 2024
9115aae
update TestAbsenteeChecks for proposer change
cce Oct 4, 2024
23b9996
update TestTotalWeightChanges and update comment on generateKnockOffl…
cce Oct 4, 2024
9bc39aa
update TestEvalFunctionForExpiredAccounts
cce Oct 4, 2024
c558d59
update TestOnlineActModel*
cce Oct 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,9 @@ $(GOPATH1)/bin/%:
test: build
$(GOTESTCOMMAND) $(GOTAGS) -race $(UNIT_TEST_SOURCES) -timeout 1h -coverprofile=coverage.txt -covermode=atomic

testc:
echo $(UNIT_TEST_SOURCES) | xargs -P8 -n1 go test -c

benchcheck: build
$(GOTESTCOMMAND) $(GOTAGS) -race $(UNIT_TEST_SOURCES) -run ^NOTHING -bench Benchmark -benchtime 1x -timeout 1h

Expand Down
4 changes: 4 additions & 0 deletions cmd/tealdbg/localLedger.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,10 @@ func (l *localLedger) LookupAgreement(rnd basics.Round, addr basics.Address) (ba
}, nil
}

func (l *localLedger) GetKnockOfflineCandidates(basics.Round, config.ConsensusParams) (map[basics.Address]basics.OnlineAccountData, error) {
return nil, nil
}

func (l *localLedger) OnlineCirculation(rnd basics.Round, voteRound basics.Round) (basics.MicroAlgos, error) {
// A constant is fine for tealdbg
return basics.Algos(1_000_000_000), nil // 1B
Expand Down
4 changes: 4 additions & 0 deletions daemon/algod/api/server/v2/dryrun.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,10 @@ func (dl *dryrunLedger) LookupAgreement(rnd basics.Round, addr basics.Address) (
}, nil
}

func (dl *dryrunLedger) GetKnockOfflineCandidates(basics.Round, config.ConsensusParams) (map[basics.Address]basics.OnlineAccountData, error) {
return nil, nil
}

func (dl *dryrunLedger) OnlineCirculation(rnd basics.Round, voteRnd basics.Round) (basics.MicroAlgos, error) {
// dryrun doesn't support setting the global online stake, so we'll just return a constant
return basics.Algos(1_000_000_000), nil // 1B
Expand Down
5 changes: 5 additions & 0 deletions data/basics/userBalance.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,10 @@ type VotingData struct {
type OnlineAccountData struct {
MicroAlgosWithRewards MicroAlgos
VotingData

IncentiveEligible bool
LastProposed Round
LastHeartbeat Round
}

// AccountData contains the data associated with a given address.
Expand Down Expand Up @@ -561,6 +564,8 @@ func (u AccountData) OnlineAccountData() OnlineAccountData {
VoteKeyDilution: u.VoteKeyDilution,
},
IncentiveEligible: u.IncentiveEligible,
LastProposed: u.LastProposed,
LastHeartbeat: u.LastHeartbeat,
}
}

Expand Down
5 changes: 0 additions & 5 deletions ledger/acctonline.go
Original file line number Diff line number Diff line change
Expand Up @@ -622,11 +622,6 @@ func (ao *onlineAccounts) onlineTotals(rnd basics.Round) (basics.MicroAlgos, pro
return basics.MicroAlgos{Raw: onlineRoundParams.OnlineSupply}, onlineRoundParams.CurrentProtocol, nil
}

// LookupOnlineAccountData returns the online account data for a given address at a given round.
func (ao *onlineAccounts) LookupOnlineAccountData(rnd basics.Round, addr basics.Address) (data basics.OnlineAccountData, err error) {
return ao.lookupOnlineAccountData(rnd, addr)
}

// roundOffset calculates the offset of the given round compared to the current dbRound. Requires that the lock would be taken.
func (ao *onlineAccounts) roundOffset(rnd basics.Round) (offset uint64, err error) {
if rnd < ao.cachedDBRoundOnline {
Expand Down
4 changes: 4 additions & 0 deletions ledger/eval/appcow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ func (ml *emptyLedger) onlineStake() (basics.MicroAlgos, error) {
return basics.MicroAlgos{}, nil
}

func (ml *emptyLedger) knockOfflineCandidates() (map[basics.Address]basics.OnlineAccountData, error) {
return nil, nil
}

func (ml *emptyLedger) lookupAppParams(addr basics.Address, aidx basics.AppIndex, cacheOnly bool) (ledgercore.AppParamsDelta, bool, error) {
return ledgercore.AppParamsDelta{}, true, nil
}
Expand Down
5 changes: 5 additions & 0 deletions ledger/eval/cow.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type roundCowParent interface {
// lookup retrieves agreement data about an address, querying the ledger if necessary.
lookupAgreement(basics.Address) (basics.OnlineAccountData, error)
onlineStake() (basics.MicroAlgos, error)
knockOfflineCandidates() (map[basics.Address]basics.OnlineAccountData, error)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a NIT: should we actually call this top online accounts or similar naming? It's very clear from comments that's what we are requesting, more a debate over if the name should be based on what it's sourced from vs the use-case we have for this atm.

Copy link
Contributor Author

@cce cce Oct 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a potentially stale list of top online accounts, if new accounts appeared online in the last 256 rounds (since the last state proof) they wouldn't appear. So the word "candidates" was intended to make it seem a little less definitive that this was the complete list of top online accounts for the round... but happy to pick any other name, I wasn't particularly happy with this name.

This is already being used in a method JJ called "generateKnockOfflineAccountsList" in #5757 which is where the "knockOffline" part came from.


// lookupAppParams, lookupAssetParams, lookupAppLocalState, and lookupAssetHolding retrieve data for a given address and ID.
// If cacheOnly is set, the ledger DB will not be queried, and only the cache will be consulted.
Expand Down Expand Up @@ -192,6 +193,10 @@ func (cb *roundCowState) lookupAgreement(addr basics.Address) (data basics.Onlin
return cb.lookupParent.lookupAgreement(addr)
}

func (cb *roundCowState) knockOfflineCandidates() (map[basics.Address]basics.OnlineAccountData, error) {
return cb.lookupParent.knockOfflineCandidates()
}

func (cb *roundCowState) lookupAppParams(addr basics.Address, aidx basics.AppIndex, cacheOnly bool) (ledgercore.AppParamsDelta, bool, error) {
params, ok := cb.mods.Accts.GetAppParams(addr, aidx)
if ok {
Expand Down
4 changes: 4 additions & 0 deletions ledger/eval/cow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ func (ml *mockLedger) onlineStake() (basics.MicroAlgos, error) {
return basics.Algos(55_555), nil
}

func (ml *mockLedger) knockOfflineCandidates() (map[basics.Address]basics.OnlineAccountData, error) {
return nil, nil
}

func (ml *mockLedger) lookupAppParams(addr basics.Address, aidx basics.AppIndex, cacheOnly bool) (ledgercore.AppParamsDelta, bool, error) {
params, ok := ml.balanceMap[addr].AppParams[aidx]
return ledgercore.AppParamsDelta{Params: &params}, ok, nil // XXX make a copy?
Expand Down
79 changes: 67 additions & 12 deletions ledger/eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type LedgerForCowBase interface {
CheckDup(config.ConsensusParams, basics.Round, basics.Round, basics.Round, transactions.Txid, ledgercore.Txlease) error
LookupWithoutRewards(basics.Round, basics.Address) (ledgercore.AccountData, basics.Round, error)
LookupAgreement(basics.Round, basics.Address) (basics.OnlineAccountData, error)
GetKnockOfflineCandidates(basics.Round, config.ConsensusParams) (map[basics.Address]basics.OnlineAccountData, error)
jannotti marked this conversation as resolved.
Show resolved Hide resolved
LookupAsset(basics.Round, basics.Address, basics.AssetIndex) (ledgercore.AssetResource, error)
LookupApplication(basics.Round, basics.Address, basics.AppIndex) (ledgercore.AppResource, error)
LookupKv(basics.Round, string) ([]byte, error)
Expand Down Expand Up @@ -237,6 +238,10 @@ func (x *roundCowBase) lookupAgreement(addr basics.Address) (basics.OnlineAccoun
return ad, err
}

func (x *roundCowBase) knockOfflineCandidates() (map[basics.Address]basics.OnlineAccountData, error) {
return x.l.GetKnockOfflineCandidates(x.rnd, x.proto)
}

// onlineStake returns the total online stake as of the start of the round. It
// caches the result to prevent repeated calls to the ledger.
func (x *roundCowBase) onlineStake() (basics.MicroAlgos, error) {
Expand Down Expand Up @@ -1611,20 +1616,68 @@ func (eval *BlockEvaluator) generateKnockOfflineAccountsList() {
if !eval.generate {
return
}
current := eval.Round()

current := eval.Round()
maxExpirations := eval.proto.MaxProposedExpiredOnlineAccounts
maxSuspensions := eval.proto.Payouts.MaxMarkAbsent

updates := &eval.block.ParticipationUpdates

ch := activeChallenge(&eval.proto, uint64(eval.Round()), eval.state)
ch := activeChallenge(&eval.proto, uint64(current), eval.state)

// Make a set of candidate addresses to check for absentee status.
type candidateData struct {
VoteLastValid basics.Round
VoteID crypto.OneTimeSignatureVerifier
Status basics.Status
LastProposed basics.Round
LastHeartbeat basics.Round
MicroAlgosWithRewards basics.MicroAlgos
IncentiveEligible bool // currently unused below, but may be needed in the future
}
absentCandidates := make(map[basics.Address]candidateData)

// First, ask the ledger for the top N online accounts, with their latest
// online account data, current up to the previous round.
if maxSuspensions > 0 {
knockOfflineCandidates, err := eval.state.knockOfflineCandidates()
cce marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
// Log an error and keep going; generating lists of absent and expired
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this implies some nodes can "choose" not to search for absent/expired accounts.

// accounts is not required by block validation rules.
logging.Base().Warnf("error fetching knockOfflineCandidates: %v", err)
knockOfflineCandidates = nil
}
for accountAddr, acctData := range knockOfflineCandidates {
// acctData is from previous block: doesn't include any updates in mods
absentCandidates[accountAddr] = candidateData{
VoteLastValid: acctData.VoteLastValid,
VoteID: acctData.VoteID,
Status: basics.Online, // from lookupOnlineAccountData, which only returns online accounts
LastProposed: acctData.LastProposed,
LastHeartbeat: acctData.LastHeartbeat,
MicroAlgosWithRewards: acctData.MicroAlgosWithRewards,
IncentiveEligible: acctData.IncentiveEligible,
}
}
}

// Then add any accounts modified in this block, with their state at the
// end of the round.
for _, accountAddr := range eval.state.modifiedAccounts() {
acctData, found := eval.state.mods.Accts.GetData(accountAddr)
if !found {
continue
}
// This will overwrite data from the knockOfflineCandidates() list, if they were modified in the current block.
absentCandidates[accountAddr] = candidateData{
VoteLastValid: acctData.VoteLastValid,
VoteID: acctData.VoteID,
Status: acctData.Status,
jannotti marked this conversation as resolved.
Show resolved Hide resolved
LastProposed: acctData.LastProposed,
LastHeartbeat: acctData.LastHeartbeat,
MicroAlgosWithRewards: acctData.WithUpdatedRewards(eval.proto, eval.state.rewardsLevel()).MicroAlgos,
IncentiveEligible: acctData.IncentiveEligible,
}

// Regardless of being online or suspended, if voting data exists, the
// account can be expired to remove it. This means an offline account
Expand All @@ -1637,17 +1690,24 @@ func (eval *BlockEvaluator) generateKnockOfflineAccountsList() {
updates.ExpiredParticipationAccounts,
accountAddr,
)
continue // if marking expired, do not also suspend
delete(absentCandidates, accountAddr) // if marking expired, do not also suspend
}
}
}

// Now, check these candidate accounts to see if they are expired or absent.
for accountAddr, acctData := range absentCandidates {
if acctData.MicroAlgosWithRewards.IsZero() {
continue // should only happen in tests; prevents panic in isAbsent
}

if len(updates.AbsentParticipationAccounts) >= maxSuspensions {
continue // no more room (don't break the loop, since we may have more expiries)
}

if acctData.Status == basics.Online {
lastSeen := max(acctData.LastProposed, acctData.LastHeartbeat)
if isAbsent(eval.state.prevTotals.Online.Money, acctData.MicroAlgos, lastSeen, current) ||
if isAbsent(eval.state.prevTotals.Online.Money, acctData.MicroAlgosWithRewards, lastSeen, current) ||
jannotti marked this conversation as resolved.
Show resolved Hide resolved
gmalouf marked this conversation as resolved.
Show resolved Hide resolved
failsChallenge(ch, accountAddr, lastSeen) {
updates.AbsentParticipationAccounts = append(
updates.AbsentParticipationAccounts,
Expand All @@ -1658,14 +1718,6 @@ func (eval *BlockEvaluator) generateKnockOfflineAccountsList() {
}
}

// delete me in Go 1.21
func max(a, b basics.Round) basics.Round {
if a > b {
return a
}
return b
}

// bitsMatch checks if the first n bits of two byte slices match. Written to
// work on arbitrary slices, but we expect that n is small. Only user today
// calls with n=5.
Expand Down Expand Up @@ -1821,6 +1873,9 @@ func (eval *BlockEvaluator) validateAbsentOnlineAccounts() error {
if acctData.Status != basics.Online {
return fmt.Errorf("proposed absent account %v was %v, not Online", accountAddr, acctData.Status)
}
if acctData.MicroAlgos.IsZero() {
return fmt.Errorf("proposed absent account %v with zero algos", accountAddr)
}
jannotti marked this conversation as resolved.
Show resolved Hide resolved

lastSeen := max(acctData.LastProposed, acctData.LastHeartbeat)
if isAbsent(eval.state.prevTotals.Online.Money, acctData.MicroAlgos, lastSeen, eval.Round()) {
Expand Down
15 changes: 15 additions & 0 deletions ledger/eval/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,17 @@ func (ledger *evalTestLedger) LookupAgreement(rnd basics.Round, addr basics.Addr
return convertToOnline(ad), err
}

func (ledger *evalTestLedger) GetKnockOfflineCandidates(rnd basics.Round, _ config.ConsensusParams) (map[basics.Address]basics.OnlineAccountData, error) {
// simulate by returning all online accounts known by the test ledger
ret := make(map[basics.Address]basics.OnlineAccountData)
for addr, data := range ledger.roundBalances[rnd] {
if data.Status == basics.Online && !data.MicroAlgos.IsZero() {
ret[addr] = data.OnlineAccountData()
}
}
return ret, nil
}

// OnlineCirculation just returns a deterministic value for a given round.
func (ledger *evalTestLedger) OnlineCirculation(rnd, voteRound basics.Round) (basics.MicroAlgos, error) {
return basics.MicroAlgos{Raw: uint64(rnd) * 1_000_000}, nil
Expand Down Expand Up @@ -1025,6 +1036,10 @@ func (l *testCowBaseLedger) LookupAgreement(rnd basics.Round, addr basics.Addres
return basics.OnlineAccountData{}, errors.New("not implemented")
}

func (l *testCowBaseLedger) GetKnockOfflineCandidates(basics.Round, config.ConsensusParams) (map[basics.Address]basics.OnlineAccountData, error) {
return nil, errors.New("not implemented")
}

func (l *testCowBaseLedger) OnlineCirculation(rnd, voteRnd basics.Round) (basics.MicroAlgos, error) {
return basics.MicroAlgos{}, errors.New("not implemented")
}
Expand Down
11 changes: 11 additions & 0 deletions ledger/eval/prefetcher/prefetcher_alignment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,16 +119,23 @@ func (l *prefetcherAlignmentTestLedger) LookupWithoutRewards(_ basics.Round, add
}
return ledgercore.AccountData{}, 0, nil
}

func (l *prefetcherAlignmentTestLedger) LookupAgreement(_ basics.Round, addr basics.Address) (basics.OnlineAccountData, error) {
// prefetch alignment tests do not check for prefetching of online account data
// because it's quite different and can only occur in AVM opcodes, which
// aren't handled anyway (just as we don't know if a holding or app local
// will be accessed in AVM.)
return basics.OnlineAccountData{}, errors.New("not implemented")
}

func (l *prefetcherAlignmentTestLedger) OnlineCirculation(rnd, voteRnd basics.Round) (basics.MicroAlgos, error) {
return basics.MicroAlgos{}, errors.New("not implemented")
}

func (l *prefetcherAlignmentTestLedger) GetKnockOfflineCandidates(basics.Round, config.ConsensusParams) (map[basics.Address]basics.OnlineAccountData, error) {
return nil, errors.New("not implemented")
}

func (l *prefetcherAlignmentTestLedger) LookupApplication(rnd basics.Round, addr basics.Address, aidx basics.AppIndex) (ledgercore.AppResource, error) {
l.mu.Lock()
if l.requestedApps == nil {
Expand All @@ -144,6 +151,7 @@ func (l *prefetcherAlignmentTestLedger) LookupApplication(rnd basics.Round, addr

return l.apps[addr][aidx], nil
}

func (l *prefetcherAlignmentTestLedger) LookupAsset(rnd basics.Round, addr basics.Address, aidx basics.AssetIndex) (ledgercore.AssetResource, error) {
l.mu.Lock()
if l.requestedAssets == nil {
Expand All @@ -159,9 +167,11 @@ func (l *prefetcherAlignmentTestLedger) LookupAsset(rnd basics.Round, addr basic

return l.assets[addr][aidx], nil
}

func (l *prefetcherAlignmentTestLedger) LookupKv(rnd basics.Round, key string) ([]byte, error) {
panic("not implemented")
}

func (l *prefetcherAlignmentTestLedger) GetCreatorForRound(_ basics.Round, cidx basics.CreatableIndex, ctype basics.CreatableType) (basics.Address, bool, error) {
l.mu.Lock()
if l.requestedCreators == nil {
Expand All @@ -175,6 +185,7 @@ func (l *prefetcherAlignmentTestLedger) GetCreatorForRound(_ basics.Round, cidx
}
return basics.Address{}, false, nil
}

func (l *prefetcherAlignmentTestLedger) GenesisHash() crypto.Digest {
return crypto.Digest{}
}
Expand Down
31 changes: 30 additions & 1 deletion ledger/ledger.go
Original file line number Diff line number Diff line change
Expand Up @@ -638,10 +638,39 @@ func (l *Ledger) LookupAgreement(rnd basics.Round, addr basics.Address) (basics.
defer l.trackerMu.RUnlock()

// Intentionally apply (pending) rewards up to rnd.
data, err := l.acctsOnline.LookupOnlineAccountData(rnd, addr)
data, err := l.acctsOnline.lookupOnlineAccountData(rnd, addr)
return data, err
}

// GetKnockOfflineCandidates retrieves a list of online accounts who will be
// checked to a recent proposal or heartbeat. Large accounts are the ones worth checking.
func (l *Ledger) GetKnockOfflineCandidates(rnd basics.Round, proto config.ConsensusParams) (map[basics.Address]basics.OnlineAccountData, error) {
l.trackerMu.RLock()
defer l.trackerMu.RUnlock()

// get state proof worker's most recent list for top N addresses
if proto.StateProofInterval == 0 {
return nil, nil
}
// get latest state proof voters information, up to rnd, without calling cond.Wait()
_, voters := l.acctsOnline.voters.LatestCompletedVotersUpTo(rnd)
if voters == nil { // no cached voters found < rnd
return nil, nil
}

// fetch fresh data up to this round from online account cache. These accounts should all
// be in cache, as long as proto.StateProofTopVoters < onlineAccountsCacheMaxSize.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like a condition to call out in the consensus file.

ret := make(map[basics.Address]basics.OnlineAccountData)
for addr := range voters.AddrToPos {
data, err := l.acctsOnline.lookupOnlineAccountData(rnd, addr)
if err != nil {
continue // skip missing / not online accounts
}
ret[addr] = data
}
return ret, nil
}

// LookupWithoutRewards is like Lookup but does not apply pending rewards up
// to the requested round rnd.
func (l *Ledger) LookupWithoutRewards(rnd basics.Round, addr basics.Address) (ledgercore.AccountData, basics.Round, error) {
Expand Down
2 changes: 2 additions & 0 deletions ledger/ledgercore/accountdata.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ func (u AccountData) OnlineAccountData(proto config.ConsensusParams, rewardsLeve
MicroAlgosWithRewards: microAlgos,
VotingData: u.VotingData,
IncentiveEligible: u.IncentiveEligible,
LastProposed: u.LastProposed,
LastHeartbeat: u.LastHeartbeat,
}
}

Expand Down
2 changes: 1 addition & 1 deletion ledger/ledgercore/onlineacct.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import (
)

// An OnlineAccount corresponds to an account whose AccountData.Status
// is Online. This is used for a Merkle tree commitment of online
// is Online. This is used for a Merkle tree commitment of online
// accounts, which is subsequently used to validate participants for
// a state proof.
type OnlineAccount struct {
Expand Down
8 changes: 8 additions & 0 deletions ledger/ledgercore/votersForRound.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,11 @@ func (tr *VotersForRound) Wait() error {
}
return nil
}

// Completed returns true if the tree has finished being constructed.
// If there was an error constructing the tree, the error is also returned.
func (tr *VotersForRound) Completed() (bool, error) {
tr.mu.Lock()
defer tr.mu.Unlock()
return tr.Tree != nil || tr.loadTreeError != nil, tr.loadTreeError
}
Loading
Loading