From 21db44da28636a0133ee4e4d1a9415c84fec2ec8 Mon Sep 17 00:00:00 2001 From: chris erway Date: Wed, 24 Jul 2024 21:31:24 -0400 Subject: [PATCH 01/16] periodically track top N online accounts in Ledger, and use when building AbsentParticipationAccounts --- Makefile | 3 + cmd/tealdbg/localLedger.go | 4 ++ daemon/algod/api/server/v2/dryrun.go | 4 ++ data/basics/userBalance.go | 3 + ledger/acctdeltas.go | 4 ++ ledger/acctonline.go | 5 -- ledger/eval/appcow_test.go | 4 ++ ledger/eval/cow.go | 5 ++ ledger/eval/cow_test.go | 4 ++ ledger/eval/eval.go | 64 ++++++++++++++++--- ledger/eval/eval_test.go | 8 +++ .../prefetcher/prefetcher_alignment_test.go | 13 +++- ledger/ledger.go | 28 +++++++- ledger/ledgercore/onlineacct.go | 9 ++- ledger/store/trackerdb/data.go | 6 ++ ledger/toponline.go | 59 +++++++++++++++++ ledger/tracker.go | 6 +- 17 files changed, 210 insertions(+), 19 deletions(-) create mode 100644 ledger/toponline.go diff --git a/Makefile b/Makefile index 5204f24345..e9b1330b94 100644 --- a/Makefile +++ b/Makefile @@ -285,6 +285,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 diff --git a/cmd/tealdbg/localLedger.go b/cmd/tealdbg/localLedger.go index d495fbb328..91dd5f9985 100644 --- a/cmd/tealdbg/localLedger.go +++ b/cmd/tealdbg/localLedger.go @@ -359,6 +359,10 @@ func (l *localLedger) LookupAgreement(rnd basics.Round, addr basics.Address) (ba }, nil } +func (l *localLedger) GetIncentiveKickoffCandidates(basics.Round, config.ConsensusParams, uint64) (data map[basics.Address]basics.OnlineAccountData, err 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 diff --git a/daemon/algod/api/server/v2/dryrun.go b/daemon/algod/api/server/v2/dryrun.go index d3924eaf1d..941634b355 100644 --- a/daemon/algod/api/server/v2/dryrun.go +++ b/daemon/algod/api/server/v2/dryrun.go @@ -329,6 +329,10 @@ func (dl *dryrunLedger) LookupAgreement(rnd basics.Round, addr basics.Address) ( }, nil } +func (dl *dryrunLedger) GetIncentiveKickoffCandidates(basics.Round, config.ConsensusParams, uint64) (data map[basics.Address]basics.OnlineAccountData, err 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 diff --git a/data/basics/userBalance.go b/data/basics/userBalance.go index d8f86aea54..81eab80032 100644 --- a/data/basics/userBalance.go +++ b/data/basics/userBalance.go @@ -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. diff --git a/ledger/acctdeltas.go b/ledger/acctdeltas.go index ad0be650b7..39fdc19ccc 100644 --- a/ledger/acctdeltas.go +++ b/ledger/acctdeltas.go @@ -690,7 +690,11 @@ func accountDataToOnline(address basics.Address, ad *ledgercore.AccountData, pro NormalizedOnlineBalance: ad.NormalizedOnlineBalance(proto), VoteFirstValid: ad.VoteFirstValid, VoteLastValid: ad.VoteLastValid, + VoteID: ad.VoteID, StateProofID: ad.StateProofID, + LastProposed: ad.LastProposed, + LastHeartbeat: ad.LastHeartbeat, + IncentiveEligible: ad.IncentiveEligible, } } diff --git a/ledger/acctonline.go b/ledger/acctonline.go index 0db04e92ad..f82da850d1 100644 --- a/ledger/acctonline.go +++ b/ledger/acctonline.go @@ -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 { diff --git a/ledger/eval/appcow_test.go b/ledger/eval/appcow_test.go index 6f5e39b305..9687ab2166 100644 --- a/ledger/eval/appcow_test.go +++ b/ledger/eval/appcow_test.go @@ -56,6 +56,10 @@ func (ml *emptyLedger) onlineStake() (basics.MicroAlgos, error) { return basics.MicroAlgos{}, nil } +func (ml *emptyLedger) incentiveCandidates(uint64) (data map[basics.Address]basics.OnlineAccountData, err 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 } diff --git a/ledger/eval/cow.go b/ledger/eval/cow.go index 9511af7ce7..1fc280e4d0 100644 --- a/ledger/eval/cow.go +++ b/ledger/eval/cow.go @@ -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) + incentiveCandidates(rewardsLevel uint64) (data map[basics.Address]basics.OnlineAccountData, err error) // 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. @@ -192,6 +193,10 @@ func (cb *roundCowState) lookupAgreement(addr basics.Address) (data basics.Onlin return cb.lookupParent.lookupAgreement(addr) } +func (cb *roundCowState) incentiveCandidates(rewardsLevel uint64) (data map[basics.Address]basics.OnlineAccountData, err error) { + return cb.lookupParent.incentiveCandidates(rewardsLevel) +} + 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 { diff --git a/ledger/eval/cow_test.go b/ledger/eval/cow_test.go index 138e2562ad..5837eb38ed 100644 --- a/ledger/eval/cow_test.go +++ b/ledger/eval/cow_test.go @@ -73,6 +73,10 @@ func (ml *mockLedger) onlineStake() (basics.MicroAlgos, error) { return basics.Algos(55_555), nil } +func (ml *mockLedger) incentiveCandidates(uint64) (data map[basics.Address]basics.OnlineAccountData, err 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: ¶ms}, ok, nil // XXX make a copy? diff --git a/ledger/eval/eval.go b/ledger/eval/eval.go index 859b62922f..c86ab089c4 100644 --- a/ledger/eval/eval.go +++ b/ledger/eval/eval.go @@ -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) + GetIncentiveKickoffCandidates(basics.Round, config.ConsensusParams, uint64) (data map[basics.Address]basics.OnlineAccountData, err error) 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) @@ -237,6 +238,10 @@ func (x *roundCowBase) lookupAgreement(addr basics.Address) (basics.OnlineAccoun return ad, err } +func (x *roundCowBase) incentiveCandidates(rewardsLevel uint64) (data map[basics.Address]basics.OnlineAccountData, err error) { + return x.l.GetIncentiveKickoffCandidates(x.rnd, x.proto, rewardsLevel) +} + // 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) { @@ -1620,12 +1625,61 @@ func (eval *BlockEvaluator) generateKnockOfflineAccountsList() { ch := activeChallenge(&eval.proto, uint64(eval.Round()), eval.state) + // Make a set of candidate addresses to check for expired or 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 + } + candidates := 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. + incentiveCandidates, err := eval.state.incentiveCandidates(eval.state.rewardsLevel()) + if err != nil { + // Log an error and keep going; generating lists of absent and expired + // accounts is not required by block validation rules. + logging.Base().Warnf("error fetching incentiveCandidates: %v", err) + incentiveCandidates = nil + } + for accountAddr, acctData := range incentiveCandidates { + // acctData is from previous block: doesn't include any updates in mods + candidates[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 incentiveCandidates() list, if they were modified in the current block. + candidates[accountAddr] = candidateData{ + VoteLastValid: acctData.VoteLastValid, + VoteID: acctData.VoteID, + Status: acctData.Status, + LastProposed: acctData.LastProposed, + LastHeartbeat: acctData.LastHeartbeat, + MicroAlgosWithRewards: acctData.RewardedMicroAlgos, + IncentiveEligible: acctData.IncentiveEligible, + } + } + // Now, check these candidate accounts to see if they are expired or absent. + for accountAddr, acctData := range candidates { // Regardless of being online or suspended, if voting data exists, the // account can be expired to remove it. This means an offline account // can be expired (because it was already suspended). @@ -1647,7 +1701,7 @@ func (eval *BlockEvaluator) generateKnockOfflineAccountsList() { 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) || failsChallenge(ch, accountAddr, lastSeen) { updates.AbsentParticipationAccounts = append( updates.AbsentParticipationAccounts, @@ -1658,14 +1712,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. diff --git a/ledger/eval/eval_test.go b/ledger/eval/eval_test.go index 77a477b3c0..e9cc1ce6b8 100644 --- a/ledger/eval/eval_test.go +++ b/ledger/eval/eval_test.go @@ -793,6 +793,10 @@ func (ledger *evalTestLedger) LookupAgreement(rnd basics.Round, addr basics.Addr return convertToOnline(ad), err } +func (ledger *evalTestLedger) GetIncentiveKickoffCandidates(basics.Round, config.ConsensusParams, uint64) (data map[basics.Address]basics.OnlineAccountData, err error) { + return nil, 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 @@ -1025,6 +1029,10 @@ func (l *testCowBaseLedger) LookupAgreement(rnd basics.Round, addr basics.Addres return basics.OnlineAccountData{}, errors.New("not implemented") } +func (l *testCowBaseLedger) GetIncentiveKickoffCandidates(basics.Round, config.ConsensusParams, uint64) (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") } diff --git a/ledger/eval/prefetcher/prefetcher_alignment_test.go b/ledger/eval/prefetcher/prefetcher_alignment_test.go index 734d84a661..715814ac9b 100644 --- a/ledger/eval/prefetcher/prefetcher_alignment_test.go +++ b/ledger/eval/prefetcher/prefetcher_alignment_test.go @@ -119,6 +119,7 @@ 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 @@ -126,9 +127,15 @@ func (l *prefetcherAlignmentTestLedger) LookupAgreement(_ basics.Round, addr bas // 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") + panic("not implemented") +} + +func (l *prefetcherAlignmentTestLedger) GetIncentiveKickoffCandidates(basics.Round, config.ConsensusParams, uint64) (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 { @@ -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 { @@ -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 { @@ -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{} } diff --git a/ledger/ledger.go b/ledger/ledger.go index 7459c23037..25df6ce9c1 100644 --- a/ledger/ledger.go +++ b/ledger/ledger.go @@ -89,6 +89,7 @@ type Ledger struct { notifier blockNotifier metrics metricsTracker spVerification spVerificationTracker + topOnlineCache topOnlineCache trackers trackerRegistry trackerMu deadlock.RWMutex @@ -635,10 +636,35 @@ 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 } +// GetIncentiveKickoffCandidates retrieves a list of online accounts who may not have +// proposed or sent a heartbeat recently. +func (l *Ledger) GetIncentiveKickoffCandidates(rnd basics.Round, proto config.ConsensusParams, rewardsLevel uint64) (map[basics.Address]basics.OnlineAccountData, error) { + l.trackerMu.RLock() + defer l.trackerMu.RUnlock() + + // get cached list of top N addresses + addrs, err := l.topOnlineCache.topN(&l.acctsOnline, rnd, proto, rewardsLevel) + if err != nil { + return nil, err + } + + // fetch data for this round from online account cache. These accounts should all + // be in cache, as long as topOnlineCacheSize < onlineAccountsCacheMaxSize. + ret := make(map[basics.Address]basics.OnlineAccountData) + for _, addr := range addrs { + 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) { diff --git a/ledger/ledgercore/onlineacct.go b/ledger/ledgercore/onlineacct.go index 8a6b771aad..6a5d35848c 100644 --- a/ledger/ledgercore/onlineacct.go +++ b/ledger/ledgercore/onlineacct.go @@ -17,14 +17,15 @@ package ledgercore import ( + "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/crypto/merklesignature" "github.com/algorand/go-algorand/data/basics" ) // 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. +// a state proof. It is also used to track incentives participants. type OnlineAccount struct { // These are a subset of the fields from the corresponding AccountData. Address basics.Address @@ -33,5 +34,9 @@ type OnlineAccount struct { NormalizedOnlineBalance uint64 VoteFirstValid basics.Round VoteLastValid basics.Round + VoteID crypto.OneTimeSignatureVerifier StateProofID merklesignature.Commitment + LastProposed basics.Round + LastHeartbeat basics.Round + IncentiveEligible bool } diff --git a/ledger/store/trackerdb/data.go b/ledger/store/trackerdb/data.go index 8e69f2fc69..708c2803c4 100644 --- a/ledger/store/trackerdb/data.go +++ b/ledger/store/trackerdb/data.go @@ -152,6 +152,8 @@ type BaseOnlineAccountData struct { BaseVotingData + LastProposed basics.Round `codec:"V"` + LastHeartbeat basics.Round `codec:"W"` IncentiveEligible bool `codec:"X"` MicroAlgos basics.MicroAlgos `codec:"Y"` RewardsBase uint64 `codec:"Z"` @@ -469,7 +471,11 @@ func (bo *BaseOnlineAccountData) GetOnlineAccount(addr basics.Address, normBalan NormalizedOnlineBalance: normBalance, VoteFirstValid: bo.VoteFirstValid, VoteLastValid: bo.VoteLastValid, + VoteID: bo.VoteID, StateProofID: bo.StateProofID, + LastHeartbeat: bo.LastHeartbeat, + LastProposed: bo.LastProposed, + IncentiveEligible: bo.IncentiveEligible, } } diff --git a/ledger/toponline.go b/ledger/toponline.go new file mode 100644 index 0000000000..38501777aa --- /dev/null +++ b/ledger/toponline.go @@ -0,0 +1,59 @@ +// Copyright (C) 2019-2024 Algorand, Inc. +// This file is part of go-algorand +// +// go-algorand is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// go-algorand is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with go-algorand. If not, see . + +package ledger + +import ( + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/ledger/ledgercore" +) + +// topOnlineCache caches a list of top N online accounts, for use in tracking incentive +// participants. The list of addresses may be stale up to topOnlineCacheMaxAge rounds. +type topOnlineCache struct { + lastQuery basics.Round // the round when the last top N online query was made + topAccts []basics.Address +} + +const topOnlineCacheMaxAge = 256 +const topOnlineCacheSize = 1000 + +func (t *topOnlineCache) clear() { + t.lastQuery = 0 + t.topAccts = nil +} + +func (t *topOnlineCache) topN(l ledgercore.OnlineAccountsFetcher, rnd basics.Round, currentProto config.ConsensusParams, rewardsLevel uint64) ([]basics.Address, error) { + if rnd < t.lastQuery { + // requesting rnd before latest; clear state + t.clear() + } + if rnd.SubSaturate(t.lastQuery) >= topOnlineCacheMaxAge { + // topOnlineCacheMaxAge has passed, update cache + data, _, err := l.TopOnlineAccounts(rnd, rnd, topOnlineCacheSize, ¤tProto, rewardsLevel) + if err != nil { + return nil, err + } + t.topAccts = make([]basics.Address, len(data)) + for i := range data { + t.topAccts[i] = data[i].Address + } + t.lastQuery = rnd + } + // return cached list of top N accounts + return t.topAccts, nil +} diff --git a/ledger/tracker.go b/ledger/tracker.go index 97098a572f..e1876b3a7a 100644 --- a/ledger/tracker.go +++ b/ledger/tracker.go @@ -912,7 +912,11 @@ func (aul *accountUpdatesLedgerEvaluator) LookupWithoutRewards(rnd basics.Round, } func (aul *accountUpdatesLedgerEvaluator) LookupAgreement(rnd basics.Round, addr basics.Address) (basics.OnlineAccountData, error) { - return aul.ao.LookupOnlineAccountData(rnd, addr) + return aul.ao.lookupOnlineAccountData(rnd, addr) +} + +func (aul *accountUpdatesLedgerEvaluator) GetIncentiveKickoffCandidates(basics.Round, config.ConsensusParams, uint64) (data map[basics.Address]basics.OnlineAccountData, err error) { + return nil, nil } func (aul *accountUpdatesLedgerEvaluator) OnlineCirculation(rnd basics.Round, voteRnd basics.Round) (basics.MicroAlgos, error) { From e9685153fa1d96c89105f4a2f2938453363134be Mon Sep 17 00:00:00 2001 From: chris erway Date: Fri, 26 Jul 2024 13:53:43 -0400 Subject: [PATCH 02/16] run make msgp --- ledger/store/trackerdb/msgp_gen.go | 66 +++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/ledger/store/trackerdb/msgp_gen.go b/ledger/store/trackerdb/msgp_gen.go index 465248e93d..98f35bf519 100644 --- a/ledger/store/trackerdb/msgp_gen.go +++ b/ledger/store/trackerdb/msgp_gen.go @@ -749,8 +749,8 @@ func BaseAccountDataMaxSize() (s int) { func (z *BaseOnlineAccountData) MarshalMsg(b []byte) (o []byte) { o = msgp.Require(b, z.Msgsize()) // omitempty: check for empty values - zb0001Len := uint32(9) - var zb0001Mask uint16 /* 11 bits */ + zb0001Len := uint32(11) + var zb0001Mask uint16 /* 13 bits */ if (*z).BaseVotingData.VoteID.MsgIsZero() { zb0001Len-- zb0001Mask |= 0x1 @@ -775,18 +775,26 @@ func (z *BaseOnlineAccountData) MarshalMsg(b []byte) (o []byte) { zb0001Len-- zb0001Mask |= 0x20 } - if (*z).IncentiveEligible == false { + if (*z).LastProposed.MsgIsZero() { zb0001Len-- zb0001Mask |= 0x40 } - if (*z).MicroAlgos.MsgIsZero() { + if (*z).LastHeartbeat.MsgIsZero() { zb0001Len-- zb0001Mask |= 0x80 } - if (*z).RewardsBase == 0 { + if (*z).IncentiveEligible == false { zb0001Len-- zb0001Mask |= 0x100 } + if (*z).MicroAlgos.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x200 + } + if (*z).RewardsBase == 0 { + zb0001Len-- + zb0001Mask |= 0x400 + } // variable map header, size zb0001Len o = append(o, 0x80|uint8(zb0001Len)) if zb0001Len != 0 { @@ -821,16 +829,26 @@ func (z *BaseOnlineAccountData) MarshalMsg(b []byte) (o []byte) { o = (*z).BaseVotingData.StateProofID.MarshalMsg(o) } if (zb0001Mask & 0x40) == 0 { // if not empty + // string "V" + o = append(o, 0xa1, 0x56) + o = (*z).LastProposed.MarshalMsg(o) + } + if (zb0001Mask & 0x80) == 0 { // if not empty + // string "W" + o = append(o, 0xa1, 0x57) + o = (*z).LastHeartbeat.MarshalMsg(o) + } + if (zb0001Mask & 0x100) == 0 { // if not empty // string "X" o = append(o, 0xa1, 0x58) o = msgp.AppendBool(o, (*z).IncentiveEligible) } - if (zb0001Mask & 0x80) == 0 { // if not empty + if (zb0001Mask & 0x200) == 0 { // if not empty // string "Y" o = append(o, 0xa1, 0x59) o = (*z).MicroAlgos.MarshalMsg(o) } - if (zb0001Mask & 0x100) == 0 { // if not empty + if (zb0001Mask & 0x400) == 0 { // if not empty // string "Z" o = append(o, 0xa1, 0x5a) o = msgp.AppendUint64(o, (*z).RewardsBase) @@ -910,6 +928,22 @@ func (z *BaseOnlineAccountData) UnmarshalMsgWithState(bts []byte, st msgp.Unmars return } } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).LastProposed.UnmarshalMsgWithState(bts, st) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "LastProposed") + return + } + } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).LastHeartbeat.UnmarshalMsgWithState(bts, st) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "LastHeartbeat") + return + } + } if zb0001 > 0 { zb0001-- (*z).IncentiveEligible, bts, err = msgp.ReadBoolBytes(bts) @@ -993,6 +1027,18 @@ func (z *BaseOnlineAccountData) UnmarshalMsgWithState(bts []byte, st msgp.Unmars err = msgp.WrapError(err, "StateProofID") return } + case "V": + bts, err = (*z).LastProposed.UnmarshalMsgWithState(bts, st) + if err != nil { + err = msgp.WrapError(err, "LastProposed") + return + } + case "W": + bts, err = (*z).LastHeartbeat.UnmarshalMsgWithState(bts, st) + if err != nil { + err = msgp.WrapError(err, "LastHeartbeat") + return + } case "X": (*z).IncentiveEligible, bts, err = msgp.ReadBoolBytes(bts) if err != nil { @@ -1034,18 +1080,18 @@ func (_ *BaseOnlineAccountData) CanUnmarshalMsg(z interface{}) bool { // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message func (z *BaseOnlineAccountData) Msgsize() (s int) { - s = 1 + 2 + (*z).BaseVotingData.VoteID.Msgsize() + 2 + (*z).BaseVotingData.SelectionID.Msgsize() + 2 + (*z).BaseVotingData.VoteFirstValid.Msgsize() + 2 + (*z).BaseVotingData.VoteLastValid.Msgsize() + 2 + msgp.Uint64Size + 2 + (*z).BaseVotingData.StateProofID.Msgsize() + 2 + msgp.BoolSize + 2 + (*z).MicroAlgos.Msgsize() + 2 + msgp.Uint64Size + s = 1 + 2 + (*z).BaseVotingData.VoteID.Msgsize() + 2 + (*z).BaseVotingData.SelectionID.Msgsize() + 2 + (*z).BaseVotingData.VoteFirstValid.Msgsize() + 2 + (*z).BaseVotingData.VoteLastValid.Msgsize() + 2 + msgp.Uint64Size + 2 + (*z).BaseVotingData.StateProofID.Msgsize() + 2 + (*z).LastProposed.Msgsize() + 2 + (*z).LastHeartbeat.Msgsize() + 2 + msgp.BoolSize + 2 + (*z).MicroAlgos.Msgsize() + 2 + msgp.Uint64Size return } // MsgIsZero returns whether this is a zero value func (z *BaseOnlineAccountData) MsgIsZero() bool { - return ((*z).BaseVotingData.VoteID.MsgIsZero()) && ((*z).BaseVotingData.SelectionID.MsgIsZero()) && ((*z).BaseVotingData.VoteFirstValid.MsgIsZero()) && ((*z).BaseVotingData.VoteLastValid.MsgIsZero()) && ((*z).BaseVotingData.VoteKeyDilution == 0) && ((*z).BaseVotingData.StateProofID.MsgIsZero()) && ((*z).IncentiveEligible == false) && ((*z).MicroAlgos.MsgIsZero()) && ((*z).RewardsBase == 0) + return ((*z).BaseVotingData.VoteID.MsgIsZero()) && ((*z).BaseVotingData.SelectionID.MsgIsZero()) && ((*z).BaseVotingData.VoteFirstValid.MsgIsZero()) && ((*z).BaseVotingData.VoteLastValid.MsgIsZero()) && ((*z).BaseVotingData.VoteKeyDilution == 0) && ((*z).BaseVotingData.StateProofID.MsgIsZero()) && ((*z).LastProposed.MsgIsZero()) && ((*z).LastHeartbeat.MsgIsZero()) && ((*z).IncentiveEligible == false) && ((*z).MicroAlgos.MsgIsZero()) && ((*z).RewardsBase == 0) } // MaxSize returns a maximum valid message size for this message type func BaseOnlineAccountDataMaxSize() (s int) { - s = 1 + 2 + crypto.OneTimeSignatureVerifierMaxSize() + 2 + crypto.VRFVerifierMaxSize() + 2 + basics.RoundMaxSize() + 2 + basics.RoundMaxSize() + 2 + msgp.Uint64Size + 2 + merklesignature.CommitmentMaxSize() + 2 + msgp.BoolSize + 2 + basics.MicroAlgosMaxSize() + 2 + msgp.Uint64Size + s = 1 + 2 + crypto.OneTimeSignatureVerifierMaxSize() + 2 + crypto.VRFVerifierMaxSize() + 2 + basics.RoundMaxSize() + 2 + basics.RoundMaxSize() + 2 + msgp.Uint64Size + 2 + merklesignature.CommitmentMaxSize() + 2 + basics.RoundMaxSize() + 2 + basics.RoundMaxSize() + 2 + msgp.BoolSize + 2 + basics.MicroAlgosMaxSize() + 2 + msgp.Uint64Size return } From 8587b28d27fc426276352942b1d82f125a5e15cf Mon Sep 17 00:00:00 2001 From: cce <51567+cce@users.noreply.github.com> Date: Fri, 9 Aug 2024 10:15:03 -0400 Subject: [PATCH 03/16] update votersTracker --- ledger/ledger.go | 16 ++++++++++------ ledger/ledgercore/votersForRound.go | 8 ++++++++ ledger/voters.go | 25 ++++++++++++++++++++++++- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/ledger/ledger.go b/ledger/ledger.go index 25df6ce9c1..40fda149e3 100644 --- a/ledger/ledger.go +++ b/ledger/ledger.go @@ -646,16 +646,20 @@ func (l *Ledger) GetIncentiveKickoffCandidates(rnd basics.Round, proto config.Co l.trackerMu.RLock() defer l.trackerMu.RUnlock() - // get cached list of top N addresses - addrs, err := l.topOnlineCache.topN(&l.acctsOnline, rnd, proto, rewardsLevel) - if err != nil { - return nil, err + // 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() + rnd, voters := l.acctsOnline.voters.LatestCompletedVotersUpTo(rnd) + if voters == nil { // no cached voters found < rnd + return nil, nil } - // fetch data for this round from online account cache. These accounts should all + // fetch fresh data up to this round from online account cache. These accounts should all // be in cache, as long as topOnlineCacheSize < onlineAccountsCacheMaxSize. ret := make(map[basics.Address]basics.OnlineAccountData) - for _, addr := range addrs { + for addr := range voters.AddrToPos { data, err := l.acctsOnline.lookupOnlineAccountData(rnd, addr) if err != nil { continue // skip missing / not online accounts diff --git a/ledger/ledgercore/votersForRound.go b/ledger/ledgercore/votersForRound.go index 7ab103dcd1..957ec08a52 100644 --- a/ledger/ledgercore/votersForRound.go +++ b/ledger/ledgercore/votersForRound.go @@ -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 +} diff --git a/ledger/voters.go b/ledger/voters.go index 63e0722a6f..05d9b5f7e5 100644 --- a/ledger/voters.go +++ b/ledger/voters.go @@ -291,7 +291,30 @@ func (vt *votersTracker) lowestRound(base basics.Round) basics.Round { return minRound } -// VotersForStateProof returns the top online participants from round r. +// LatestCompletedVotersUpTo returns the highest round <= r for which information about the top online +// participants has already been collected, and the completed VotersForRound for that round. +// If none is found, it returns 0, nil. Unlike VotersForStateProof, this function does not wait. +func (vt *votersTracker) LatestCompletedVotersUpTo(r basics.Round) (basics.Round, *ledgercore.VotersForRound) { + vt.votersMu.RLock() + defer vt.votersMu.RUnlock() + + var latestRound basics.Round + var latestVoters *ledgercore.VotersForRound + + for round, voters := range vt.votersForRoundCache { + if round <= r && round > latestRound { + if completed, err := voters.Completed(); completed && err != nil { + latestRound = round + latestVoters = voters + } + } + } + + return latestRound, latestVoters +} + +// VotersForStateProof returns the top online participants from round r. If this data is still being +// constructed in another goroutine, this function will wait until it is ready. func (vt *votersTracker) VotersForStateProof(r basics.Round) (*ledgercore.VotersForRound, error) { tr, exists := vt.getVoters(r) if !exists { From 38d4b8ded4a1363ccd278821f0d1bfa4998ac626 Mon Sep 17 00:00:00 2001 From: cce <51567+cce@users.noreply.github.com> Date: Fri, 9 Aug 2024 10:23:40 -0400 Subject: [PATCH 04/16] CR fixes --- cmd/tealdbg/localLedger.go | 2 +- daemon/algod/api/server/v2/dryrun.go | 2 +- ledger/eval/appcow_test.go | 2 +- ledger/eval/cow.go | 4 ++-- ledger/eval/cow_test.go | 2 +- ledger/eval/eval.go | 4 ++-- ledger/eval/eval_test.go | 2 +- ledger/eval/prefetcher/prefetcher_alignment_test.go | 2 +- ledger/tracker.go | 2 +- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/cmd/tealdbg/localLedger.go b/cmd/tealdbg/localLedger.go index 91dd5f9985..40586153e2 100644 --- a/cmd/tealdbg/localLedger.go +++ b/cmd/tealdbg/localLedger.go @@ -359,7 +359,7 @@ func (l *localLedger) LookupAgreement(rnd basics.Round, addr basics.Address) (ba }, nil } -func (l *localLedger) GetIncentiveKickoffCandidates(basics.Round, config.ConsensusParams, uint64) (data map[basics.Address]basics.OnlineAccountData, err error) { +func (l *localLedger) GetIncentiveKickoffCandidates(basics.Round, config.ConsensusParams, uint64) (map[basics.Address]basics.OnlineAccountData, error) { return nil, nil } diff --git a/daemon/algod/api/server/v2/dryrun.go b/daemon/algod/api/server/v2/dryrun.go index 941634b355..4492ee2ee6 100644 --- a/daemon/algod/api/server/v2/dryrun.go +++ b/daemon/algod/api/server/v2/dryrun.go @@ -329,7 +329,7 @@ func (dl *dryrunLedger) LookupAgreement(rnd basics.Round, addr basics.Address) ( }, nil } -func (dl *dryrunLedger) GetIncentiveKickoffCandidates(basics.Round, config.ConsensusParams, uint64) (data map[basics.Address]basics.OnlineAccountData, err error) { +func (dl *dryrunLedger) GetIncentiveKickoffCandidates(basics.Round, config.ConsensusParams, uint64) (map[basics.Address]basics.OnlineAccountData, error) { return nil, nil } diff --git a/ledger/eval/appcow_test.go b/ledger/eval/appcow_test.go index 9687ab2166..f3a9ae1045 100644 --- a/ledger/eval/appcow_test.go +++ b/ledger/eval/appcow_test.go @@ -56,7 +56,7 @@ func (ml *emptyLedger) onlineStake() (basics.MicroAlgos, error) { return basics.MicroAlgos{}, nil } -func (ml *emptyLedger) incentiveCandidates(uint64) (data map[basics.Address]basics.OnlineAccountData, err error) { +func (ml *emptyLedger) incentiveCandidates(uint64) (map[basics.Address]basics.OnlineAccountData, error) { return nil, nil } diff --git a/ledger/eval/cow.go b/ledger/eval/cow.go index 1fc280e4d0..de9c08a5fc 100644 --- a/ledger/eval/cow.go +++ b/ledger/eval/cow.go @@ -47,7 +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) - incentiveCandidates(rewardsLevel uint64) (data map[basics.Address]basics.OnlineAccountData, err error) + incentiveCandidates(rewardsLevel uint64) (map[basics.Address]basics.OnlineAccountData, error) // 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. @@ -193,7 +193,7 @@ func (cb *roundCowState) lookupAgreement(addr basics.Address) (data basics.Onlin return cb.lookupParent.lookupAgreement(addr) } -func (cb *roundCowState) incentiveCandidates(rewardsLevel uint64) (data map[basics.Address]basics.OnlineAccountData, err error) { +func (cb *roundCowState) incentiveCandidates(rewardsLevel uint64) (map[basics.Address]basics.OnlineAccountData, error) { return cb.lookupParent.incentiveCandidates(rewardsLevel) } diff --git a/ledger/eval/cow_test.go b/ledger/eval/cow_test.go index 5837eb38ed..98107e0ad1 100644 --- a/ledger/eval/cow_test.go +++ b/ledger/eval/cow_test.go @@ -73,7 +73,7 @@ func (ml *mockLedger) onlineStake() (basics.MicroAlgos, error) { return basics.Algos(55_555), nil } -func (ml *mockLedger) incentiveCandidates(uint64) (data map[basics.Address]basics.OnlineAccountData, err error) { +func (ml *mockLedger) incentiveCandidates(uint64) (map[basics.Address]basics.OnlineAccountData, error) { return nil, nil } diff --git a/ledger/eval/eval.go b/ledger/eval/eval.go index c86ab089c4..75e2aa5e0c 100644 --- a/ledger/eval/eval.go +++ b/ledger/eval/eval.go @@ -48,7 +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) - GetIncentiveKickoffCandidates(basics.Round, config.ConsensusParams, uint64) (data map[basics.Address]basics.OnlineAccountData, err error) + GetIncentiveKickoffCandidates(basics.Round, config.ConsensusParams, uint64) (map[basics.Address]basics.OnlineAccountData, error) 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) @@ -238,7 +238,7 @@ func (x *roundCowBase) lookupAgreement(addr basics.Address) (basics.OnlineAccoun return ad, err } -func (x *roundCowBase) incentiveCandidates(rewardsLevel uint64) (data map[basics.Address]basics.OnlineAccountData, err error) { +func (x *roundCowBase) incentiveCandidates(rewardsLevel uint64) (map[basics.Address]basics.OnlineAccountData, error) { return x.l.GetIncentiveKickoffCandidates(x.rnd, x.proto, rewardsLevel) } diff --git a/ledger/eval/eval_test.go b/ledger/eval/eval_test.go index e9cc1ce6b8..4193a71b2e 100644 --- a/ledger/eval/eval_test.go +++ b/ledger/eval/eval_test.go @@ -793,7 +793,7 @@ func (ledger *evalTestLedger) LookupAgreement(rnd basics.Round, addr basics.Addr return convertToOnline(ad), err } -func (ledger *evalTestLedger) GetIncentiveKickoffCandidates(basics.Round, config.ConsensusParams, uint64) (data map[basics.Address]basics.OnlineAccountData, err error) { +func (ledger *evalTestLedger) GetIncentiveKickoffCandidates(basics.Round, config.ConsensusParams, uint64) (map[basics.Address]basics.OnlineAccountData, error) { return nil, nil } diff --git a/ledger/eval/prefetcher/prefetcher_alignment_test.go b/ledger/eval/prefetcher/prefetcher_alignment_test.go index 715814ac9b..b9c0047436 100644 --- a/ledger/eval/prefetcher/prefetcher_alignment_test.go +++ b/ledger/eval/prefetcher/prefetcher_alignment_test.go @@ -129,7 +129,7 @@ func (l *prefetcherAlignmentTestLedger) LookupAgreement(_ basics.Round, addr bas } func (l *prefetcherAlignmentTestLedger) OnlineCirculation(rnd, voteRnd basics.Round) (basics.MicroAlgos, error) { - panic("not implemented") + return basics.MicroAlgos{}, errors.New("not implemented") } func (l *prefetcherAlignmentTestLedger) GetIncentiveKickoffCandidates(basics.Round, config.ConsensusParams, uint64) (map[basics.Address]basics.OnlineAccountData, error) { diff --git a/ledger/tracker.go b/ledger/tracker.go index e1876b3a7a..9e3b65b2c4 100644 --- a/ledger/tracker.go +++ b/ledger/tracker.go @@ -915,7 +915,7 @@ func (aul *accountUpdatesLedgerEvaluator) LookupAgreement(rnd basics.Round, addr return aul.ao.lookupOnlineAccountData(rnd, addr) } -func (aul *accountUpdatesLedgerEvaluator) GetIncentiveKickoffCandidates(basics.Round, config.ConsensusParams, uint64) (data map[basics.Address]basics.OnlineAccountData, err error) { +func (aul *accountUpdatesLedgerEvaluator) GetIncentiveKickoffCandidates(basics.Round, config.ConsensusParams, uint64) (map[basics.Address]basics.OnlineAccountData, error) { return nil, nil } From 9740ddc735d1df656d22d48352624202a1162966 Mon Sep 17 00:00:00 2001 From: cce <51567+cce@users.noreply.github.com> Date: Fri, 9 Aug 2024 10:58:02 -0400 Subject: [PATCH 05/16] rename and simplify --- cmd/tealdbg/localLedger.go | 2 +- daemon/algod/api/server/v2/dryrun.go | 2 +- ledger/acctdeltas.go | 4 -- ledger/eval/appcow_test.go | 2 +- ledger/eval/cow.go | 6 +- ledger/eval/cow_test.go | 2 +- ledger/eval/eval.go | 50 ++++++++-------- ledger/eval/eval_test.go | 4 +- .../prefetcher/prefetcher_alignment_test.go | 2 +- ledger/ledger.go | 7 +-- ledger/ledgercore/onlineacct.go | 7 +-- ledger/store/trackerdb/data.go | 4 -- ledger/toponline.go | 59 ------------------- ledger/tracker.go | 2 +- 14 files changed, 41 insertions(+), 112 deletions(-) delete mode 100644 ledger/toponline.go diff --git a/cmd/tealdbg/localLedger.go b/cmd/tealdbg/localLedger.go index 40586153e2..16f28fd904 100644 --- a/cmd/tealdbg/localLedger.go +++ b/cmd/tealdbg/localLedger.go @@ -359,7 +359,7 @@ func (l *localLedger) LookupAgreement(rnd basics.Round, addr basics.Address) (ba }, nil } -func (l *localLedger) GetIncentiveKickoffCandidates(basics.Round, config.ConsensusParams, uint64) (map[basics.Address]basics.OnlineAccountData, error) { +func (l *localLedger) GetKnockOfflineCandidates(basics.Round, config.ConsensusParams) (map[basics.Address]basics.OnlineAccountData, error) { return nil, nil } diff --git a/daemon/algod/api/server/v2/dryrun.go b/daemon/algod/api/server/v2/dryrun.go index 4492ee2ee6..25b3365f4a 100644 --- a/daemon/algod/api/server/v2/dryrun.go +++ b/daemon/algod/api/server/v2/dryrun.go @@ -329,7 +329,7 @@ func (dl *dryrunLedger) LookupAgreement(rnd basics.Round, addr basics.Address) ( }, nil } -func (dl *dryrunLedger) GetIncentiveKickoffCandidates(basics.Round, config.ConsensusParams, uint64) (map[basics.Address]basics.OnlineAccountData, error) { +func (dl *dryrunLedger) GetKnockOfflineCandidates(basics.Round, config.ConsensusParams) (map[basics.Address]basics.OnlineAccountData, error) { return nil, nil } diff --git a/ledger/acctdeltas.go b/ledger/acctdeltas.go index 39fdc19ccc..ad0be650b7 100644 --- a/ledger/acctdeltas.go +++ b/ledger/acctdeltas.go @@ -690,11 +690,7 @@ func accountDataToOnline(address basics.Address, ad *ledgercore.AccountData, pro NormalizedOnlineBalance: ad.NormalizedOnlineBalance(proto), VoteFirstValid: ad.VoteFirstValid, VoteLastValid: ad.VoteLastValid, - VoteID: ad.VoteID, StateProofID: ad.StateProofID, - LastProposed: ad.LastProposed, - LastHeartbeat: ad.LastHeartbeat, - IncentiveEligible: ad.IncentiveEligible, } } diff --git a/ledger/eval/appcow_test.go b/ledger/eval/appcow_test.go index f3a9ae1045..54aea0ece4 100644 --- a/ledger/eval/appcow_test.go +++ b/ledger/eval/appcow_test.go @@ -56,7 +56,7 @@ func (ml *emptyLedger) onlineStake() (basics.MicroAlgos, error) { return basics.MicroAlgos{}, nil } -func (ml *emptyLedger) incentiveCandidates(uint64) (map[basics.Address]basics.OnlineAccountData, error) { +func (ml *emptyLedger) knockOfflineCandidates() (map[basics.Address]basics.OnlineAccountData, error) { return nil, nil } diff --git a/ledger/eval/cow.go b/ledger/eval/cow.go index de9c08a5fc..c628b15224 100644 --- a/ledger/eval/cow.go +++ b/ledger/eval/cow.go @@ -47,7 +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) - incentiveCandidates(rewardsLevel uint64) (map[basics.Address]basics.OnlineAccountData, error) + knockOfflineCandidates() (map[basics.Address]basics.OnlineAccountData, error) // 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. @@ -193,8 +193,8 @@ func (cb *roundCowState) lookupAgreement(addr basics.Address) (data basics.Onlin return cb.lookupParent.lookupAgreement(addr) } -func (cb *roundCowState) incentiveCandidates(rewardsLevel uint64) (map[basics.Address]basics.OnlineAccountData, error) { - return cb.lookupParent.incentiveCandidates(rewardsLevel) +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) { diff --git a/ledger/eval/cow_test.go b/ledger/eval/cow_test.go index 98107e0ad1..2192272bf1 100644 --- a/ledger/eval/cow_test.go +++ b/ledger/eval/cow_test.go @@ -73,7 +73,7 @@ func (ml *mockLedger) onlineStake() (basics.MicroAlgos, error) { return basics.Algos(55_555), nil } -func (ml *mockLedger) incentiveCandidates(uint64) (map[basics.Address]basics.OnlineAccountData, error) { +func (ml *mockLedger) knockOfflineCandidates() (map[basics.Address]basics.OnlineAccountData, error) { return nil, nil } diff --git a/ledger/eval/eval.go b/ledger/eval/eval.go index 75e2aa5e0c..bfb9e86a0b 100644 --- a/ledger/eval/eval.go +++ b/ledger/eval/eval.go @@ -48,7 +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) - GetIncentiveKickoffCandidates(basics.Round, config.ConsensusParams, uint64) (map[basics.Address]basics.OnlineAccountData, error) + GetKnockOfflineCandidates(basics.Round, config.ConsensusParams) (map[basics.Address]basics.OnlineAccountData, error) 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) @@ -238,8 +238,8 @@ func (x *roundCowBase) lookupAgreement(addr basics.Address) (basics.OnlineAccoun return ad, err } -func (x *roundCowBase) incentiveCandidates(rewardsLevel uint64) (map[basics.Address]basics.OnlineAccountData, error) { - return x.l.GetIncentiveKickoffCandidates(x.rnd, x.proto, rewardsLevel) +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 @@ -1616,14 +1616,14 @@ 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 expired or absentee status. type candidateData struct { @@ -1639,23 +1639,25 @@ func (eval *BlockEvaluator) generateKnockOfflineAccountsList() { // First, ask the ledger for the top N online accounts, with their latest // online account data, current up to the previous round. - incentiveCandidates, err := eval.state.incentiveCandidates(eval.state.rewardsLevel()) - if err != nil { - // Log an error and keep going; generating lists of absent and expired - // accounts is not required by block validation rules. - logging.Base().Warnf("error fetching incentiveCandidates: %v", err) - incentiveCandidates = nil - } - for accountAddr, acctData := range incentiveCandidates { - // acctData is from previous block: doesn't include any updates in mods - candidates[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, + if maxSuspensions > 0 { + knockOfflineCandidates, err := eval.state.knockOfflineCandidates() + if err != nil { + // Log an error and keep going; generating lists of absent and expired + // 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 + candidates[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, + } } } @@ -1666,14 +1668,14 @@ func (eval *BlockEvaluator) generateKnockOfflineAccountsList() { if !found { continue } - // This will overwrite data from the incentiveCandidates() list, if they were modified in the current block. + // This will overwrite data from the knockOfflineCandidates() list, if they were modified in the current block. candidates[accountAddr] = candidateData{ VoteLastValid: acctData.VoteLastValid, VoteID: acctData.VoteID, Status: acctData.Status, LastProposed: acctData.LastProposed, LastHeartbeat: acctData.LastHeartbeat, - MicroAlgosWithRewards: acctData.RewardedMicroAlgos, + MicroAlgosWithRewards: acctData.WithUpdatedRewards(eval.proto, eval.state.rewardsLevel()).MicroAlgos, IncentiveEligible: acctData.IncentiveEligible, } } diff --git a/ledger/eval/eval_test.go b/ledger/eval/eval_test.go index 4193a71b2e..3424629d73 100644 --- a/ledger/eval/eval_test.go +++ b/ledger/eval/eval_test.go @@ -793,7 +793,7 @@ func (ledger *evalTestLedger) LookupAgreement(rnd basics.Round, addr basics.Addr return convertToOnline(ad), err } -func (ledger *evalTestLedger) GetIncentiveKickoffCandidates(basics.Round, config.ConsensusParams, uint64) (map[basics.Address]basics.OnlineAccountData, error) { +func (ledger *evalTestLedger) GetKnockOfflineCandidates(basics.Round, config.ConsensusParams) (map[basics.Address]basics.OnlineAccountData, error) { return nil, nil } @@ -1029,7 +1029,7 @@ func (l *testCowBaseLedger) LookupAgreement(rnd basics.Round, addr basics.Addres return basics.OnlineAccountData{}, errors.New("not implemented") } -func (l *testCowBaseLedger) GetIncentiveKickoffCandidates(basics.Round, config.ConsensusParams, uint64) (map[basics.Address]basics.OnlineAccountData, error) { +func (l *testCowBaseLedger) GetKnockOfflineCandidates(basics.Round, config.ConsensusParams) (map[basics.Address]basics.OnlineAccountData, error) { return nil, errors.New("not implemented") } diff --git a/ledger/eval/prefetcher/prefetcher_alignment_test.go b/ledger/eval/prefetcher/prefetcher_alignment_test.go index b9c0047436..5f61f4938f 100644 --- a/ledger/eval/prefetcher/prefetcher_alignment_test.go +++ b/ledger/eval/prefetcher/prefetcher_alignment_test.go @@ -132,7 +132,7 @@ func (l *prefetcherAlignmentTestLedger) OnlineCirculation(rnd, voteRnd basics.Ro return basics.MicroAlgos{}, errors.New("not implemented") } -func (l *prefetcherAlignmentTestLedger) GetIncentiveKickoffCandidates(basics.Round, config.ConsensusParams, uint64) (map[basics.Address]basics.OnlineAccountData, error) { +func (l *prefetcherAlignmentTestLedger) GetKnockOfflineCandidates(basics.Round, config.ConsensusParams) (map[basics.Address]basics.OnlineAccountData, error) { return nil, errors.New("not implemented") } diff --git a/ledger/ledger.go b/ledger/ledger.go index 40fda149e3..7ba3dc50d2 100644 --- a/ledger/ledger.go +++ b/ledger/ledger.go @@ -89,7 +89,6 @@ type Ledger struct { notifier blockNotifier metrics metricsTracker spVerification spVerificationTracker - topOnlineCache topOnlineCache trackers trackerRegistry trackerMu deadlock.RWMutex @@ -640,9 +639,9 @@ func (l *Ledger) LookupAgreement(rnd basics.Round, addr basics.Address) (basics. return data, err } -// GetIncentiveKickoffCandidates retrieves a list of online accounts who may not have +// GetKnockOfflineCandidates retrieves a list of online accounts who may not have // proposed or sent a heartbeat recently. -func (l *Ledger) GetIncentiveKickoffCandidates(rnd basics.Round, proto config.ConsensusParams, rewardsLevel uint64) (map[basics.Address]basics.OnlineAccountData, error) { +func (l *Ledger) GetKnockOfflineCandidates(rnd basics.Round, proto config.ConsensusParams) (map[basics.Address]basics.OnlineAccountData, error) { l.trackerMu.RLock() defer l.trackerMu.RUnlock() @@ -657,7 +656,7 @@ func (l *Ledger) GetIncentiveKickoffCandidates(rnd basics.Round, proto config.Co } // fetch fresh data up to this round from online account cache. These accounts should all - // be in cache, as long as topOnlineCacheSize < onlineAccountsCacheMaxSize. + // be in cache, as long as proto.StateProofTopVoters < onlineAccountsCacheMaxSize. ret := make(map[basics.Address]basics.OnlineAccountData) for addr := range voters.AddrToPos { data, err := l.acctsOnline.lookupOnlineAccountData(rnd, addr) diff --git a/ledger/ledgercore/onlineacct.go b/ledger/ledgercore/onlineacct.go index 6a5d35848c..f5b29c789e 100644 --- a/ledger/ledgercore/onlineacct.go +++ b/ledger/ledgercore/onlineacct.go @@ -17,7 +17,6 @@ package ledgercore import ( - "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/crypto/merklesignature" "github.com/algorand/go-algorand/data/basics" ) @@ -25,7 +24,7 @@ import ( // An OnlineAccount corresponds to an account whose AccountData.Status // is Online. This is used for a Merkle tree commitment of online // accounts, which is subsequently used to validate participants for -// a state proof. It is also used to track incentives participants. +// a state proof. type OnlineAccount struct { // These are a subset of the fields from the corresponding AccountData. Address basics.Address @@ -34,9 +33,5 @@ type OnlineAccount struct { NormalizedOnlineBalance uint64 VoteFirstValid basics.Round VoteLastValid basics.Round - VoteID crypto.OneTimeSignatureVerifier StateProofID merklesignature.Commitment - LastProposed basics.Round - LastHeartbeat basics.Round - IncentiveEligible bool } diff --git a/ledger/store/trackerdb/data.go b/ledger/store/trackerdb/data.go index 708c2803c4..bca30d85e6 100644 --- a/ledger/store/trackerdb/data.go +++ b/ledger/store/trackerdb/data.go @@ -471,11 +471,7 @@ func (bo *BaseOnlineAccountData) GetOnlineAccount(addr basics.Address, normBalan NormalizedOnlineBalance: normBalance, VoteFirstValid: bo.VoteFirstValid, VoteLastValid: bo.VoteLastValid, - VoteID: bo.VoteID, StateProofID: bo.StateProofID, - LastHeartbeat: bo.LastHeartbeat, - LastProposed: bo.LastProposed, - IncentiveEligible: bo.IncentiveEligible, } } diff --git a/ledger/toponline.go b/ledger/toponline.go deleted file mode 100644 index 38501777aa..0000000000 --- a/ledger/toponline.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (C) 2019-2024 Algorand, Inc. -// This file is part of go-algorand -// -// go-algorand is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// go-algorand is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with go-algorand. If not, see . - -package ledger - -import ( - "github.com/algorand/go-algorand/config" - "github.com/algorand/go-algorand/data/basics" - "github.com/algorand/go-algorand/ledger/ledgercore" -) - -// topOnlineCache caches a list of top N online accounts, for use in tracking incentive -// participants. The list of addresses may be stale up to topOnlineCacheMaxAge rounds. -type topOnlineCache struct { - lastQuery basics.Round // the round when the last top N online query was made - topAccts []basics.Address -} - -const topOnlineCacheMaxAge = 256 -const topOnlineCacheSize = 1000 - -func (t *topOnlineCache) clear() { - t.lastQuery = 0 - t.topAccts = nil -} - -func (t *topOnlineCache) topN(l ledgercore.OnlineAccountsFetcher, rnd basics.Round, currentProto config.ConsensusParams, rewardsLevel uint64) ([]basics.Address, error) { - if rnd < t.lastQuery { - // requesting rnd before latest; clear state - t.clear() - } - if rnd.SubSaturate(t.lastQuery) >= topOnlineCacheMaxAge { - // topOnlineCacheMaxAge has passed, update cache - data, _, err := l.TopOnlineAccounts(rnd, rnd, topOnlineCacheSize, ¤tProto, rewardsLevel) - if err != nil { - return nil, err - } - t.topAccts = make([]basics.Address, len(data)) - for i := range data { - t.topAccts[i] = data[i].Address - } - t.lastQuery = rnd - } - // return cached list of top N accounts - return t.topAccts, nil -} diff --git a/ledger/tracker.go b/ledger/tracker.go index 9e3b65b2c4..eb5f32e233 100644 --- a/ledger/tracker.go +++ b/ledger/tracker.go @@ -915,7 +915,7 @@ func (aul *accountUpdatesLedgerEvaluator) LookupAgreement(rnd basics.Round, addr return aul.ao.lookupOnlineAccountData(rnd, addr) } -func (aul *accountUpdatesLedgerEvaluator) GetIncentiveKickoffCandidates(basics.Round, config.ConsensusParams, uint64) (map[basics.Address]basics.OnlineAccountData, error) { +func (aul *accountUpdatesLedgerEvaluator) GetKnockOfflineCandidates(basics.Round, config.ConsensusParams) (map[basics.Address]basics.OnlineAccountData, error) { return nil, nil } From b8b96733447b069dd3462517cd5fd602e1858008 Mon Sep 17 00:00:00 2001 From: cce <51567+cce@users.noreply.github.com> Date: Fri, 9 Aug 2024 11:06:11 -0400 Subject: [PATCH 06/16] add LastProposed/LastHeartbeat to BaseOnlineAccountData handling --- ledger/store/trackerdb/data.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ledger/store/trackerdb/data.go b/ledger/store/trackerdb/data.go index bca30d85e6..d93355faa4 100644 --- a/ledger/store/trackerdb/data.go +++ b/ledger/store/trackerdb/data.go @@ -492,6 +492,8 @@ func (bo *BaseOnlineAccountData) GetOnlineAccountData(proto config.ConsensusPara VoteLastValid: bo.VoteLastValid, VoteKeyDilution: bo.VoteKeyDilution, }, + LastProposed: bo.LastProposed, + LastHeartbeat: bo.LastHeartbeat, IncentiveEligible: bo.IncentiveEligible, } } @@ -508,6 +510,8 @@ func (bo *BaseOnlineAccountData) SetCoreAccountData(ad *ledgercore.AccountData) // These are updated by the evaluator when accounts are touched bo.MicroAlgos = ad.MicroAlgos bo.RewardsBase = ad.RewardsBase + bo.LastHeartbeat = ad.LastHeartbeat + bo.LastProposed = ad.LastProposed bo.IncentiveEligible = ad.IncentiveEligible } From 0b7fbadc810d55b17428495c1230541e9660abc0 Mon Sep 17 00:00:00 2001 From: cce <51567+cce@users.noreply.github.com> Date: Fri, 9 Aug 2024 11:43:53 -0400 Subject: [PATCH 07/16] fill out LastHeartbeat/LastProposed from #5965 --- ledger/ledgercore/accountdata.go | 2 ++ ledger/store/trackerdb/data.go | 11 +++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/ledger/ledgercore/accountdata.go b/ledger/ledgercore/accountdata.go index 081fbffde6..5b17730122 100644 --- a/ledger/ledgercore/accountdata.go +++ b/ledger/ledgercore/accountdata.go @@ -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, } } diff --git a/ledger/store/trackerdb/data.go b/ledger/store/trackerdb/data.go index d93355faa4..1649d1f82d 100644 --- a/ledger/store/trackerdb/data.go +++ b/ledger/store/trackerdb/data.go @@ -458,7 +458,10 @@ func (bo *BaseOnlineAccountData) IsVotingEmpty() bool { func (bo *BaseOnlineAccountData) IsEmpty() bool { return bo.IsVotingEmpty() && bo.MicroAlgos.Raw == 0 && - bo.RewardsBase == 0 && !bo.IncentiveEligible + bo.RewardsBase == 0 && + bo.LastHeartbeat == 0 && + bo.LastProposed == 0 && + !bo.IncentiveEligible } // GetOnlineAccount returns ledgercore.OnlineAccount for top online accounts / voters @@ -492,9 +495,9 @@ func (bo *BaseOnlineAccountData) GetOnlineAccountData(proto config.ConsensusPara VoteLastValid: bo.VoteLastValid, VoteKeyDilution: bo.VoteKeyDilution, }, + IncentiveEligible: bo.IncentiveEligible, LastProposed: bo.LastProposed, LastHeartbeat: bo.LastHeartbeat, - IncentiveEligible: bo.IncentiveEligible, } } @@ -510,9 +513,9 @@ func (bo *BaseOnlineAccountData) SetCoreAccountData(ad *ledgercore.AccountData) // These are updated by the evaluator when accounts are touched bo.MicroAlgos = ad.MicroAlgos bo.RewardsBase = ad.RewardsBase - bo.LastHeartbeat = ad.LastHeartbeat - bo.LastProposed = ad.LastProposed bo.IncentiveEligible = ad.IncentiveEligible + bo.LastProposed = ad.LastProposed + bo.LastHeartbeat = ad.LastHeartbeat } // MakeResourcesData returns a new empty instance of resourcesData. From b2f1130aafd85e355576e403180dd4180f0e786a Mon Sep 17 00:00:00 2001 From: cce <51567+cce@users.noreply.github.com> Date: Fri, 9 Aug 2024 13:06:18 -0400 Subject: [PATCH 08/16] fix remaining tests --- ledger/store/trackerdb/data_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ledger/store/trackerdb/data_test.go b/ledger/store/trackerdb/data_test.go index edc0d0dc9e..b256fa4e76 100644 --- a/ledger/store/trackerdb/data_test.go +++ b/ledger/store/trackerdb/data_test.go @@ -1152,7 +1152,7 @@ func TestBaseOnlineAccountDataIsEmpty(t *testing.T) { structureTesting := func(t *testing.T) { encoding, err := json.Marshal(&empty) zeros32 := "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0" - expectedEncoding := `{"VoteID":[` + zeros32 + `],"SelectionID":[` + zeros32 + `],"VoteFirstValid":0,"VoteLastValid":0,"VoteKeyDilution":0,"StateProofID":[` + zeros32 + `,` + zeros32 + `],"IncentiveEligible":false,"MicroAlgos":{"Raw":0},"RewardsBase":0}` + expectedEncoding := `{"VoteID":[` + zeros32 + `],"SelectionID":[` + zeros32 + `],"VoteFirstValid":0,"VoteLastValid":0,"VoteKeyDilution":0,"StateProofID":[` + zeros32 + `,` + zeros32 + `],"LastProposed":0,"LastHeartbeat":0,"IncentiveEligible":false,"MicroAlgos":{"Raw":0},"RewardsBase":0}` require.NoError(t, err) require.Equal(t, expectedEncoding, string(encoding)) } @@ -1249,7 +1249,7 @@ func TestBaseOnlineAccountDataReflect(t *testing.T) { partitiontest.PartitionTest(t) t.Parallel() - require.Equal(t, 5, reflect.TypeOf(BaseOnlineAccountData{}).NumField(), "update all getters and setters for baseOnlineAccountData and change the field count") + require.Equal(t, 7, reflect.TypeOf(BaseOnlineAccountData{}).NumField(), "update all getters and setters for baseOnlineAccountData and change the field count") } func TestBaseVotingDataReflect(t *testing.T) { From 5242990044add03aba86a68a9a8d388976d59ef1 Mon Sep 17 00:00:00 2001 From: cce <51567+cce@users.noreply.github.com> Date: Fri, 9 Aug 2024 14:40:52 -0400 Subject: [PATCH 09/16] fix missing AccountData => OnlineAccountData transformation function break TestAbsenteeChecks --- data/basics/userBalance.go | 2 ++ ledger/eval/eval.go | 23 +++++++++++++++-------- ledger/eval/eval_test.go | 11 +++++++++-- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/data/basics/userBalance.go b/data/basics/userBalance.go index 81eab80032..4db69bf22a 100644 --- a/data/basics/userBalance.go +++ b/data/basics/userBalance.go @@ -564,6 +564,8 @@ func (u AccountData) OnlineAccountData() OnlineAccountData { VoteKeyDilution: u.VoteKeyDilution, }, IncentiveEligible: u.IncentiveEligible, + LastProposed: u.LastProposed, + LastHeartbeat: u.LastHeartbeat, } } diff --git a/ledger/eval/eval.go b/ledger/eval/eval.go index bfb9e86a0b..15a43c7ba9 100644 --- a/ledger/eval/eval.go +++ b/ledger/eval/eval.go @@ -1625,7 +1625,7 @@ func (eval *BlockEvaluator) generateKnockOfflineAccountsList() { ch := activeChallenge(&eval.proto, uint64(current), eval.state) - // Make a set of candidate addresses to check for expired or absentee status. + // Make a set of candidate addresses to check for absentee status. type candidateData struct { VoteLastValid basics.Round VoteID crypto.OneTimeSignatureVerifier @@ -1635,7 +1635,7 @@ func (eval *BlockEvaluator) generateKnockOfflineAccountsList() { MicroAlgosWithRewards basics.MicroAlgos IncentiveEligible bool // currently unused below, but may be needed in the future } - candidates := make(map[basics.Address]candidateData) + 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. @@ -1649,7 +1649,7 @@ func (eval *BlockEvaluator) generateKnockOfflineAccountsList() { } for accountAddr, acctData := range knockOfflineCandidates { // acctData is from previous block: doesn't include any updates in mods - candidates[accountAddr] = candidateData{ + absentCandidates[accountAddr] = candidateData{ VoteLastValid: acctData.VoteLastValid, VoteID: acctData.VoteID, Status: basics.Online, // from lookupOnlineAccountData, which only returns online accounts @@ -1669,7 +1669,7 @@ func (eval *BlockEvaluator) generateKnockOfflineAccountsList() { continue } // This will overwrite data from the knockOfflineCandidates() list, if they were modified in the current block. - candidates[accountAddr] = candidateData{ + absentCandidates[accountAddr] = candidateData{ VoteLastValid: acctData.VoteLastValid, VoteID: acctData.VoteID, Status: acctData.Status, @@ -1678,10 +1678,7 @@ func (eval *BlockEvaluator) generateKnockOfflineAccountsList() { MicroAlgosWithRewards: acctData.WithUpdatedRewards(eval.proto, eval.state.rewardsLevel()).MicroAlgos, IncentiveEligible: acctData.IncentiveEligible, } - } - // Now, check these candidate accounts to see if they are expired or absent. - for accountAddr, acctData := range candidates { // Regardless of being online or suspended, if voting data exists, the // account can be expired to remove it. This means an offline account // can be expired (because it was already suspended). @@ -1693,9 +1690,16 @@ 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) @@ -1869,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) + } lastSeen := max(acctData.LastProposed, acctData.LastHeartbeat) if isAbsent(eval.state.prevTotals.Online.Money, acctData.MicroAlgos, lastSeen, eval.Round()) { diff --git a/ledger/eval/eval_test.go b/ledger/eval/eval_test.go index 3424629d73..d2942d1ce5 100644 --- a/ledger/eval/eval_test.go +++ b/ledger/eval/eval_test.go @@ -793,8 +793,15 @@ func (ledger *evalTestLedger) LookupAgreement(rnd basics.Round, addr basics.Addr return convertToOnline(ad), err } -func (ledger *evalTestLedger) GetKnockOfflineCandidates(basics.Round, config.ConsensusParams) (map[basics.Address]basics.OnlineAccountData, error) { - return nil, nil +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. From be464cf5a5ca0b1b4242a788bed6c90a792295e2 Mon Sep 17 00:00:00 2001 From: cce <51567+cce@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:08:57 -0400 Subject: [PATCH 10/16] Update ledger/ledger.go Co-authored-by: John Jannotti --- ledger/ledger.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ledger/ledger.go b/ledger/ledger.go index 8276f9cc90..8fdd4d5e46 100644 --- a/ledger/ledger.go +++ b/ledger/ledger.go @@ -642,8 +642,8 @@ func (l *Ledger) LookupAgreement(rnd basics.Round, addr basics.Address) (basics. return data, err } -// GetKnockOfflineCandidates retrieves a list of online accounts who may not have -// proposed or sent a heartbeat recently. +// 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() From 97d0bcfbbad77774c67e286a64e6a0efc3aa5ef4 Mon Sep 17 00:00:00 2001 From: cce <51567+cce@users.noreply.github.com> Date: Wed, 21 Aug 2024 11:29:32 -0400 Subject: [PATCH 11/16] Update ledger/ledger.go --- ledger/ledger.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ledger/ledger.go b/ledger/ledger.go index 8fdd4d5e46..c45dcdedf9 100644 --- a/ledger/ledger.go +++ b/ledger/ledger.go @@ -653,7 +653,7 @@ func (l *Ledger) GetKnockOfflineCandidates(rnd basics.Round, proto config.Consen return nil, nil } // get latest state proof voters information, up to rnd, without calling cond.Wait() - rnd, voters := l.acctsOnline.voters.LatestCompletedVotersUpTo(rnd) + _, voters := l.acctsOnline.voters.LatestCompletedVotersUpTo(rnd) if voters == nil { // no cached voters found < rnd return nil, nil } From 01b150ae250467c280b3bc097eeede352341291e Mon Sep 17 00:00:00 2001 From: cce <51567+cce@users.noreply.github.com> Date: Wed, 18 Sep 2024 11:24:04 -0400 Subject: [PATCH 12/16] update TestAbsenteeChecks --- ledger/eval/eval.go | 2 +- ledger/eval/eval_test.go | 51 +++++++++++++++++++++++++++++++--------- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/ledger/eval/eval.go b/ledger/eval/eval.go index 15a43c7ba9..e13217a020 100644 --- a/ledger/eval/eval.go +++ b/ledger/eval/eval.go @@ -1698,7 +1698,7 @@ func (eval *BlockEvaluator) generateKnockOfflineAccountsList() { // 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 + continue // don't check accounts that are being closed } if len(updates.AbsentParticipationAccounts) >= maxSuspensions { diff --git a/ledger/eval/eval_test.go b/ledger/eval/eval_test.go index d2942d1ce5..a624a7412a 100644 --- a/ledger/eval/eval_test.go +++ b/ledger/eval/eval_test.go @@ -1371,12 +1371,24 @@ func TestAbsenteeChecks(t *testing.T) { crypto.RandBytes(tmp.VoteID[:]) tmp.VoteFirstValid = 1 tmp.VoteLastValid = 1500 // large enough to avoid EXPIRATION, so we can see SUSPENSION - tmp.LastHeartbeat = 1 // non-zero allows suspensions switch i { case 1: - tmp.LastHeartbeat = 1150 // lie here so that addr[1] won't be suspended + tmp.LastHeartbeat = 1 // we want addr[1] to be suspended earlier than others case 2: - tmp.LastProposed = 1150 // lie here so that addr[2] won't be suspended + tmp.LastProposed = 1 // we want addr[1] to be suspended earlier than others + default: + if i < 10 { // make the other 8 genesis wallets unsuspendable + if i%2 == 0 { + tmp.LastProposed = 1200 + } else { + tmp.LastHeartbeat = 1200 + } + } else { + // ensure non-zero balance for new accounts, but a small balance + // so they will not be absent, just challenged. + tmp.MicroAlgos = basics.MicroAlgos{Raw: 1_000_000} + tmp.LastHeartbeat = 1 // non-zero allows suspensions + } } genesisInitState.Accounts[addr] = tmp @@ -1393,13 +1405,28 @@ func TestAbsenteeChecks(t *testing.T) { blkEval, err := l.StartEvaluator(newBlock.BlockHeader, 0, 0, nil) require.NoError(t, err) - // Advance the evaluator, watching for lack of suspensions since we don't - // suspend until a txn with a suspendable account appears + // Advance the evaluator, watching for suspensions as they appear challenge := byte(0) - for i := uint64(0); i < uint64(1210); i++ { // A bit past one grace period (200) past challenge at 1000. + for i := uint64(0); i < uint64(1200); i++ { // Just before first suspension at 1171 vb := l.endBlock(t, blkEval) blkEval = l.nextBlock(t) - require.Zero(t, vb.Block().AbsentParticipationAccounts) + // make map of addrs in AbsentParticipationAccounts + absentAddrs := make(map[basics.Address]struct{}) + for _, addr := range vb.Block().AbsentParticipationAccounts { + absentAddrs[addr] = struct{}{} + } + // get indexes of addrs in AbsentParticipationAccounts + for j, addr := range addrs { + if _, isAbsent := absentAddrs[addr]; isAbsent { + t.Logf("round %d: addr %d %s is absent", vb.Block().Round(), j, addr) + } + } + if vb.Block().Round() == 102 { // 2 out of 10 genesis accounts are absent + require.Contains(t, vb.Block().AbsentParticipationAccounts, addrs[1]) + require.Contains(t, vb.Block().AbsentParticipationAccounts, addrs[2]) + } else { + require.Zero(t, vb.Block().AbsentParticipationAccounts, "round %v", vb.Block().Round()) + } if vb.Block().Round() == 1000 { challenge = vb.Block().BlockHeader.Seed[0] } @@ -1443,15 +1470,17 @@ func TestAbsenteeChecks(t *testing.T) { // fake agreement's setting of header fields so later validates work validatedBlock := ledgercore.MakeValidatedBlock(unfinishedBlock.UnfinishedBlock().WithProposer(committee.Seed{}, testPoolAddr, true), unfinishedBlock.UnfinishedDeltas()) + t.Logf("round %d: absent %v", validatedBlock.Block().Round(), validatedBlock.Block().AbsentParticipationAccounts) require.Zero(t, validatedBlock.Block().ExpiredParticipationAccounts) - require.Contains(t, validatedBlock.Block().AbsentParticipationAccounts, addrs[0], addrs[0].String()) - require.NotContains(t, validatedBlock.Block().AbsentParticipationAccounts, addrs[1], addrs[1].String()) - require.NotContains(t, validatedBlock.Block().AbsentParticipationAccounts, addrs[2], addrs[2].String()) // Of the 32 extra accounts, make sure only the one matching the challenge is suspended require.Contains(t, validatedBlock.Block().AbsentParticipationAccounts, challenged, challenged.String()) for i := byte(0); i < 32; i++ { if i == challenge>>3 { + rnd := validatedBlock.Block().Round() + ad := basics.Address{i << 3, 0xaa} + t.Logf("extra account %d %s is challenged, balance rnd %d %d", i, ad, + rnd, l.roundBalances[rnd][ad].MicroAlgos.Raw) require.Equal(t, basics.Address{i << 3, 0xaa}, challenged) continue } @@ -1474,7 +1503,7 @@ func TestAbsenteeChecks(t *testing.T) { // Introduce an address that shouldn't be suspended badBlock := goodBlock - badBlock.AbsentParticipationAccounts = append(badBlock.AbsentParticipationAccounts, addrs[1]) + badBlock.AbsentParticipationAccounts = append(badBlock.AbsentParticipationAccounts, addrs[3]) _, err = Eval(context.Background(), l, badBlock, true, verify.GetMockedCache(true), nil, l.tracer) require.ErrorContains(t, err, "not absent") From c24e8099614b7a6e0bdc3e147a7c06fccaf4daa9 Mon Sep 17 00:00:00 2001 From: cce <51567+cce@users.noreply.github.com> Date: Wed, 18 Sep 2024 13:37:51 -0400 Subject: [PATCH 13/16] also consider candidates for ExpiredParticipationAccounts --- ledger/eval/eval.go | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/ledger/eval/eval.go b/ledger/eval/eval.go index e13217a020..b532806c42 100644 --- a/ledger/eval/eval.go +++ b/ledger/eval/eval.go @@ -1625,7 +1625,7 @@ func (eval *BlockEvaluator) generateKnockOfflineAccountsList() { ch := activeChallenge(&eval.proto, uint64(current), eval.state) - // Make a set of candidate addresses to check for absentee status. + // Make a set of candidate addresses to check for expired or absentee status. type candidateData struct { VoteLastValid basics.Round VoteID crypto.OneTimeSignatureVerifier @@ -1635,7 +1635,7 @@ func (eval *BlockEvaluator) generateKnockOfflineAccountsList() { MicroAlgosWithRewards basics.MicroAlgos IncentiveEligible bool // currently unused below, but may be needed in the future } - absentCandidates := make(map[basics.Address]candidateData) + candidates := 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. @@ -1649,7 +1649,7 @@ func (eval *BlockEvaluator) generateKnockOfflineAccountsList() { } for accountAddr, acctData := range knockOfflineCandidates { // acctData is from previous block: doesn't include any updates in mods - absentCandidates[accountAddr] = candidateData{ + candidates[accountAddr] = candidateData{ VoteLastValid: acctData.VoteLastValid, VoteID: acctData.VoteID, Status: basics.Online, // from lookupOnlineAccountData, which only returns online accounts @@ -1669,7 +1669,7 @@ func (eval *BlockEvaluator) generateKnockOfflineAccountsList() { continue } // This will overwrite data from the knockOfflineCandidates() list, if they were modified in the current block. - absentCandidates[accountAddr] = candidateData{ + candidates[accountAddr] = candidateData{ VoteLastValid: acctData.VoteLastValid, VoteID: acctData.VoteID, Status: acctData.Status, @@ -1678,7 +1678,15 @@ func (eval *BlockEvaluator) generateKnockOfflineAccountsList() { MicroAlgosWithRewards: acctData.WithUpdatedRewards(eval.proto, eval.state.rewardsLevel()).MicroAlgos, IncentiveEligible: acctData.IncentiveEligible, } + } + + // Now, check these candidate accounts to see if they are expired or absent. + for accountAddr, acctData := range candidates { + if acctData.MicroAlgosWithRewards.IsZero() { + continue // don't check accounts that are being closed + } + // Expired check: are this account's voting keys no longer valid? // Regardless of being online or suspended, if voting data exists, the // account can be expired to remove it. This means an offline account // can be expired (because it was already suspended). @@ -1690,17 +1698,12 @@ func (eval *BlockEvaluator) generateKnockOfflineAccountsList() { updates.ExpiredParticipationAccounts, accountAddr, ) - delete(absentCandidates, accountAddr) // if marking expired, do not also suspend + continue // 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 // don't check accounts that are being closed - } + // Absent check: has it been too long since the last heartbeat/proposal, or + // has this online account failed a challenge? if len(updates.AbsentParticipationAccounts) >= maxSuspensions { continue // no more room (don't break the loop, since we may have more expiries) } From c252d95c2e78053f38d8b2b93ce4429b3d3c9b97 Mon Sep 17 00:00:00 2001 From: cce <51567+cce@users.noreply.github.com> Date: Wed, 18 Sep 2024 13:45:08 -0400 Subject: [PATCH 14/16] update TestExpiredAccountGeneration --- ledger/eval/eval_test.go | 59 ++++++++++------------------------------ 1 file changed, 15 insertions(+), 44 deletions(-) diff --git a/ledger/eval/eval_test.go b/ledger/eval/eval_test.go index a624a7412a..bafa061164 100644 --- a/ledger/eval/eval_test.go +++ b/ledger/eval/eval_test.go @@ -1266,7 +1266,7 @@ func TestExpiredAccountGenerationWithDiskFailure(t *testing.T) { partitiontest.PartitionTest(t) t.Parallel() - genesisInitState, addrs, keys := ledgertesting.GenesisWithProto(10, protocol.ConsensusFuture) + genesisInitState, addrs, _ := ledgertesting.GenesisWithProto(10, protocol.ConsensusFuture) sendAddr := addrs[0] recvAddr := addrs[1] @@ -1312,26 +1312,6 @@ func TestExpiredAccountGenerationWithDiskFailure(t *testing.T) { eval = l.nextBlock(t) } - genHash := l.GenesisHash() - txn := transactions.Transaction{ - Type: protocol.PaymentTx, - Header: transactions.Header{ - Sender: sendAddr, - Fee: minFee, - FirstValid: newBlock.Round(), - LastValid: eval.Round(), - GenesisHash: genHash, - }, - PaymentTxnFields: transactions.PaymentTxnFields{ - Receiver: recvAddr, - Amount: basics.MicroAlgos{Raw: 100}, - }, - } - - st := txn.Sign(keys[0]) - err = eval.Transaction(st, transactions.ApplyData{}) - require.NoError(t, err) - eval.validate = true eval.generate = false @@ -1541,7 +1521,7 @@ func TestExpiredAccountGeneration(t *testing.T) { partitiontest.PartitionTest(t) t.Parallel() - genesisInitState, addrs, keys := ledgertesting.GenesisWithProto(10, protocol.ConsensusFuture) + genesisInitState, addrs, _ := ledgertesting.GenesisWithProto(10, protocol.ConsensusFuture) sendAddr := addrs[0] recvAddr := addrs[1] @@ -1550,7 +1530,7 @@ func TestExpiredAccountGeneration(t *testing.T) { recvAddrLastValidRound := basics.Round(2) // the target round we want to advance the evaluator to - targetRound := basics.Round(4) + targetRound := basics.Round(2) // Set all to online except the sending address for _, addr := range addrs { @@ -1592,31 +1572,22 @@ func TestExpiredAccountGeneration(t *testing.T) { // Advance the evaluator a couple rounds... for i := uint64(0); i < uint64(targetRound); i++ { - l.endBlock(t, eval) + vb := l.endBlock(t, eval) eval = l.nextBlock(t) - } - require.Greater(t, uint64(eval.Round()), uint64(recvAddrLastValidRound)) - - genHash := l.GenesisHash() - txn := transactions.Transaction{ - Type: protocol.PaymentTx, - Header: transactions.Header{ - Sender: sendAddr, - Fee: minFee, - FirstValid: newBlock.Round(), - LastValid: eval.Round(), - GenesisHash: genHash, - }, - PaymentTxnFields: transactions.PaymentTxnFields{ - Receiver: recvAddr, - Amount: basics.MicroAlgos{Raw: 100}, - }, + expiredAddrs := make(map[basics.Address]struct{}) + for _, addr := range vb.Block().ExpiredParticipationAccounts { + expiredAddrs[addr] = struct{}{} + } + // get indexes of addrs in ExpiredParticipationAccounts + for j, addr := range addrs { + if _, isExpired := expiredAddrs[addr]; isExpired { + t.Logf("round %d: addr %d %s is expired", vb.Block().Round(), j, addr) + } + } } - st := txn.Sign(keys[0]) - err = eval.Transaction(st, transactions.ApplyData{}) - require.NoError(t, err) + require.Greater(t, uint64(eval.Round()), uint64(recvAddrLastValidRound)) // Make sure we validate our block as well eval.validate = true From bb82a972931dac9a66253bce383862da616608ec Mon Sep 17 00:00:00 2001 From: cce <51567+cce@users.noreply.github.com> Date: Thu, 19 Sep 2024 13:37:34 -0400 Subject: [PATCH 15/16] Fix TestAbsentTracking --- ledger/eval_simple_test.go | 114 ++++++++++++++++++++++++++++++------- ledger/voters.go | 2 +- 2 files changed, 96 insertions(+), 20 deletions(-) diff --git a/ledger/eval_simple_test.go b/ledger/eval_simple_test.go index 972821c26c..5e1903c2e7 100644 --- a/ledger/eval_simple_test.go +++ b/ledger/eval_simple_test.go @@ -412,11 +412,52 @@ func TestAbsentTracking(t *testing.T) { int 0; voter_params_get VoterIncentiveEligible; itob; log; itob; log; int 1` + addrIndexes := make(map[basics.Address]int) + for i, addr := range addrs { + addrIndexes[addr] = i + } + prettyAddrs := func(inAddrs []basics.Address) []string { + ret := make([]string, len(inAddrs)) + for i, addr := range inAddrs { + if idx, ok := addrIndexes[addr]; ok { + ret[i] = fmt.Sprintf("addrs[%d]", idx) + } else { + ret[i] = addr.String() + } + } + return ret + } + + printAbsent := func(vb *ledgercore.ValidatedBlock) { + t.Helper() + absent := vb.Block().AbsentParticipationAccounts + expired := vb.Block().ExpiredParticipationAccounts + if len(expired) > 0 || len(absent) > 0 { + t.Logf("rnd %d: expired %d, absent %d (exp %v abs %v)", vb.Block().Round(), + len(expired), len(absent), prettyAddrs(expired), prettyAddrs(absent)) + } + } + checkingBegins := 40 - ledgertesting.TestConsensusRange(t, checkingBegins, 0, func(t *testing.T, ver int, cv protocol.ConsensusVersion, cfg config.Local) { + runTest := func(t *testing.T, cv protocol.ConsensusVersion, cfg config.Local, beforeSPInterval bool) { dl := NewDoubleLedger(t, genBalances, cv, cfg) defer dl.Close() + const spIntervalRounds = 240 + + baseRound := basics.Round(0) + if !beforeSPInterval { + var vb *ledgercore.ValidatedBlock + for i := 0; i < spIntervalRounds; i++ { // run up to block 240 (state proof interval) + vb = dl.fullBlock() + printAbsent(vb) + require.Empty(t, vb.Block().AbsentParticipationAccounts) + require.Empty(t, vb.Block().ExpiredParticipationAccounts) + } + require.Equal(t, basics.Round(spIntervalRounds), vb.Block().Round()) + baseRound += spIntervalRounds + } + // we use stakeChecker for testing `voter_params_get` on suspended accounts stib := dl.txn(&txntest.Txn{ // #1 Type: "appl", @@ -456,13 +497,14 @@ func TestAbsentTracking(t *testing.T) { // have addrs[1] go online explicitly, which makes it eligible for suspension. // use a large fee, so we can see IncentiveEligible change - dl.txn(&txntest.Txn{ // #2 + vb := dl.fullBlock(&txntest.Txn{ // #2 Type: "keyreg", Fee: 10_000_000, Sender: addrs[1], VotePK: [32]byte{1}, SelectionPK: [32]byte{1}, }) + require.Equal(t, baseRound+basics.Round(2), vb.Block().Round()) // as configured above, only the first two accounts should be online require.True(t, lookup(t, dl.generator, addrs[0]).Status == basics.Online) @@ -480,7 +522,8 @@ func TestAbsentTracking(t *testing.T) { require.True(t, lookup(t, dl.generator, addrs[1]).IncentiveEligible) require.False(t, lookup(t, dl.generator, addrs[2]).IncentiveEligible) - vb := dl.fullBlock() // #6 + vb = dl.fullBlock() // #6 + printAbsent(vb) totals, err := dl.generator.Totals(vb.Block().Round()) require.NoError(t, err) require.NotZero(t, totals.Online.Money.Raw) @@ -494,7 +537,7 @@ func TestAbsentTracking(t *testing.T) { Receiver: addrs[2], Amount: 100_000, }) - dl.endBlock(proposer) // #7 + printAbsent(dl.endBlock(proposer)) // #7 prp := lookup(t, dl.validator, proposer) require.Equal(t, prp.LastProposed, dl.validator.Latest()) @@ -508,7 +551,7 @@ func TestAbsentTracking(t *testing.T) { require.Equal(t, totals.Online.Money.Raw-100_000-1000, newtotals.Online.Money.Raw) totals = newtotals - dl.fullBlock() + printAbsent(dl.fullBlock()) // addrs[2] was already offline dl.txns(&txntest.Txn{Type: "keyreg", Sender: addrs[2]}) // OFFLINE keyreg #9 @@ -524,12 +567,13 @@ func TestAbsentTracking(t *testing.T) { require.Zero(t, regger.LastHeartbeat) // ONLINE keyreg without extra fee - dl.txns(&txntest.Txn{ + vb = dl.fullBlock(&txntest.Txn{ Type: "keyreg", Sender: addrs[2], VotePK: [32]byte{1}, SelectionPK: [32]byte{1}, }) // #10 + printAbsent(vb) // online totals have grown, addr[2] was added newtotals, err = dl.generator.Totals(dl.generator.Latest()) require.NoError(t, err) @@ -555,14 +599,15 @@ func TestAbsentTracking(t *testing.T) { VotePK: [32]byte{1}, SelectionPK: [32]byte{1}, }) // #14 + printAbsent(vb) twoEligible := vb.Block().Round() - require.EqualValues(t, 14, twoEligible) // sanity check + require.EqualValues(t, baseRound+14, twoEligible) // sanity check regger = lookup(t, dl.validator, addrs[2]) require.True(t, regger.IncentiveEligible) for i := 0; i < 5; i++ { - dl.fullBlock() // #15-19 + printAbsent(dl.fullBlock()) // #15-19 require.True(t, lookup(t, dl.generator, addrs[0]).Status == basics.Online) require.True(t, lookup(t, dl.generator, addrs[1]).Status == basics.Online) require.True(t, lookup(t, dl.generator, addrs[2]).Status == basics.Online) @@ -574,7 +619,7 @@ func TestAbsentTracking(t *testing.T) { require.True(t, lookup(t, dl.generator, addrs[2]).Status == basics.Online) for i := 0; i < 30; i++ { - dl.fullBlock() // #20-49 + printAbsent(dl.fullBlock()) // #20-49 } // addrs 0-2 all have about 1/3 of stake, so seemingly (see next block @@ -582,7 +627,11 @@ func TestAbsentTracking(t *testing.T) { // about 35. But, since blocks are empty, nobody's suspendible account // is noticed. require.Equal(t, basics.Online, lookup(t, dl.generator, addrs[0]).Status) - require.Equal(t, basics.Online, lookup(t, dl.generator, addrs[1]).Status) + if beforeSPInterval { + require.Equal(t, basics.Online, lookup(t, dl.generator, addrs[1]).Status) + } else { + require.Equal(t, basics.Offline, lookup(t, dl.generator, addrs[1]).Status) + } require.Equal(t, basics.Online, lookup(t, dl.generator, addrs[2]).Status) require.True(t, lookup(t, dl.generator, addrs[2]).IncentiveEligible) @@ -594,21 +643,26 @@ func TestAbsentTracking(t *testing.T) { Receiver: addrs[0], Amount: 0, }) // #50 + printAbsent(vb) require.Equal(t, vb.Block().AbsentParticipationAccounts, []basics.Address{addrs[2]}) twoPaysZero := vb.Block().Round() - require.EqualValues(t, 50, twoPaysZero) + require.EqualValues(t, baseRound+50, twoPaysZero) // addr[0] has never proposed or heartbeat so it is not considered absent require.Equal(t, basics.Online, lookup(t, dl.generator, addrs[0]).Status) // addr[1] still hasn't been "noticed" - require.Equal(t, basics.Online, lookup(t, dl.generator, addrs[1]).Status) + if beforeSPInterval { + require.Equal(t, basics.Online, lookup(t, dl.generator, addrs[1]).Status) + } else { + require.Equal(t, basics.Offline, lookup(t, dl.generator, addrs[1]).Status) + } require.Equal(t, basics.Offline, lookup(t, dl.generator, addrs[2]).Status) require.False(t, lookup(t, dl.generator, addrs[2]).IncentiveEligible) // separate the payments by a few blocks so it will be easier to test // when the changes go into effect for i := 0; i < 4; i++ { - dl.fullBlock() // #51-54 + printAbsent(dl.fullBlock()) // #51-54 } // now, when 2 pays 1, 1 gets suspended (unlike 0, we had 1 keyreg early on, so LastHeartbeat>0) vb = dl.fullBlock(&txntest.Txn{ @@ -617,9 +671,14 @@ func TestAbsentTracking(t *testing.T) { Receiver: addrs[1], Amount: 0, }) // #55 + printAbsent(vb) twoPaysOne := vb.Block().Round() - require.EqualValues(t, 55, twoPaysOne) - require.Equal(t, vb.Block().AbsentParticipationAccounts, []basics.Address{addrs[1]}) + require.EqualValues(t, baseRound+55, twoPaysOne) + if beforeSPInterval { + require.Equal(t, vb.Block().AbsentParticipationAccounts, []basics.Address{addrs[1]}) + } else { + require.Empty(t, vb.Block().AbsentParticipationAccounts) + } require.Equal(t, basics.Online, lookup(t, dl.generator, addrs[0]).Status) require.Equal(t, basics.Offline, lookup(t, dl.generator, addrs[1]).Status) require.False(t, lookup(t, dl.generator, addrs[1]).IncentiveEligible) @@ -628,7 +687,7 @@ func TestAbsentTracking(t *testing.T) { // now, addrs[2] proposes, so it gets back online, but stays ineligible dl.proposer = addrs[2] - dl.fullBlock() + printAbsent(dl.fullBlock()) require.Equal(t, basics.Online, lookup(t, dl.generator, addrs[2]).Status) require.False(t, lookup(t, dl.generator, addrs[2]).IncentiveEligible) @@ -651,7 +710,11 @@ func TestAbsentTracking(t *testing.T) { // in second block, the checkstate app was created checkState(addrs[1], true, false, 833_333_333_333_333) // 322 // addr[1] spent 10A on a fee in rnd 3, so online stake and eligibility adjusted in 323 - checkState(addrs[1], true, true, 833_333_323_333_333) // 323 + if beforeSPInterval { + checkState(addrs[1], true, true, 833_333_323_333_333) // 323 + } else { + checkState(addrs[1], true, false, 833_333_333_333_333) // 323 + } for rnd := dl.fullBlock().Block().Round(); rnd < 320+twoEligible-1; rnd = dl.fullBlock().Block().Round() { } @@ -671,10 +734,23 @@ func TestAbsentTracking(t *testing.T) { // after doing a keyreg, became susceptible to suspension for rnd := dl.fullBlock().Block().Round(); rnd < 320+twoPaysOne-1; rnd = dl.fullBlock().Block().Round() { } - checkState(addrs[1], true, true, 833_333_323_230_333) // still online, balance irrelevant + if beforeSPInterval { + checkState(addrs[1], true, true, 833_333_323_230_333) // still online, balance irrelevant + } else { + checkState(addrs[1], false, false, 0) // suspended already + } // 1 was noticed & suspended after being paid by 2, so eligible and amount go to 0 checkState(addrs[1], false, false, 0) - }) + } + + testBeforeAndAfterSPInterval := func(t *testing.T, ver int, cv protocol.ConsensusVersion, cfg config.Local) { + // before the first state proof interval (240 rounds), no cached voters data is available, so only accounts + // noticed in blocks will be suspended. + t.Run("beforeSPInterval", func(t *testing.T) { runTest(t, cv, cfg, false) }) + t.Run("afterSPInterval", func(t *testing.T) { runTest(t, cv, cfg, true) }) + } + + ledgertesting.TestConsensusRange(t, checkingBegins, 0, testBeforeAndAfterSPInterval) } // TestAbsenteeChallenges ensures that online accounts that don't (do) respond diff --git a/ledger/voters.go b/ledger/voters.go index 05d9b5f7e5..49d7adf457 100644 --- a/ledger/voters.go +++ b/ledger/voters.go @@ -303,7 +303,7 @@ func (vt *votersTracker) LatestCompletedVotersUpTo(r basics.Round) (basics.Round for round, voters := range vt.votersForRoundCache { if round <= r && round > latestRound { - if completed, err := voters.Completed(); completed && err != nil { + if completed, err := voters.Completed(); completed && err == nil { latestRound = round latestVoters = voters } From 04c0a844790bf717d43e0e828fe71322dc412608 Mon Sep 17 00:00:00 2001 From: cce <51567+cce@users.noreply.github.com> Date: Mon, 23 Sep 2024 22:27:39 -0400 Subject: [PATCH 16/16] add TestLatestCompletedVotersUpTo --- ledger/voters_test.go | 82 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/ledger/voters_test.go b/ledger/voters_test.go index a4913c4999..083492c610 100644 --- a/ledger/voters_test.go +++ b/ledger/voters_test.go @@ -17,6 +17,7 @@ package ledger import ( + "fmt" "testing" "github.com/algorand/go-algorand/config" @@ -273,3 +274,84 @@ func TestTopNAccountsThatHaveNoMssKeys(t *testing.T) { a.Equal(merklesignature.NoKeysCommitment, top.Participants[j].PK.Commitment) } } + +// implements ledgercore.OnlineAccountsFetcher +type testOnlineAccountsFetcher struct { + topAccts []*ledgercore.OnlineAccount + totalStake basics.MicroAlgos + err error +} + +func (o testOnlineAccountsFetcher) TopOnlineAccounts(rnd basics.Round, voteRnd basics.Round, n uint64, params *config.ConsensusParams, rewardsLevel uint64) (topOnlineAccounts []*ledgercore.OnlineAccount, totalOnlineStake basics.MicroAlgos, err error) { + return o.topAccts, o.totalStake, o.err +} + +func TestLatestCompletedVotersUpToWithError(t *testing.T) { + partitiontest.PartitionTest(t) + a := require.New(t) + + // Set up mock ledger with initial data + accts := []map[basics.Address]basics.AccountData{makeRandomOnlineAccounts(20)} + ml := makeMockLedgerForTracker(t, true, 1, protocol.ConsensusCurrentVersion, accts) + defer ml.Close() + + conf := config.GetDefaultLocal() + _, ao := newAcctUpdates(t, ml, conf) + + // Add several blocks + for i := uint64(1); i < 10; i++ { + addRandomBlock(t, ml) + } + commitAll(t, ml) + + // Populate votersForRoundCache with test data + for r := basics.Round(1); r <= 9; r += 2 { // simulate every odd round + vr := ledgercore.MakeVotersForRound() + if r%4 == 1 { // Simulate an error for rounds 1, 5, and 9 + vr.BroadcastError(fmt.Errorf("error loading data for round %d", r)) + } else { + // Simulate a successful load of voter data + hdr := bookkeeping.BlockHeader{Round: r} + oaf := testOnlineAccountsFetcher{nil, basics.MicroAlgos{Raw: 1_000_000}, nil} + require.NoError(t, vr.LoadTree(oaf, hdr)) + } + + ao.voters.setVoters(r, vr) + } + + // LastCompletedVotersUpTo retrieves the highest round less than or equal to + // the requested round where data is complete, ignoring rounds with errors. + for _, tc := range []struct { + reqRound, retRound uint64 + completed bool + }{ + {0, 0, false}, + {1, 0, false}, + {2, 0, false}, // requested 2, no completed rounds <= 2 + {3, 3, true}, + {4, 3, true}, + {5, 3, true}, // requested 5, got 3 (round 5 had error) + {6, 3, true}, + {7, 7, true}, // requested 7, got 7 (last completed <= 8) + {8, 7, true}, // requested 8, got 7 (last completed <= 8) + {9, 7, true}, // requested 9, got 7 (err at 9) + {10, 7, true}, + {11, 7, true}, + } { + completedRound, voters := ao.voters.LatestCompletedVotersUpTo(basics.Round(tc.reqRound)) + a.Equal(completedRound, basics.Round(tc.retRound)) // No completed rounds before 2 + a.Equal(voters != nil, tc.completed) + } + + // Test with errors in all rounds + ao.voters.votersForRoundCache = make(map[basics.Round]*ledgercore.VotersForRound) // reset map + for r := basics.Round(1); r <= 9; r += 2 { + vr := ledgercore.MakeVotersForRound() + vr.BroadcastError(fmt.Errorf("error loading data for round %d", r)) + ao.voters.setVoters(r, vr) + } + + completedRound, voters := ao.voters.LatestCompletedVotersUpTo(basics.Round(9)) + a.Equal(basics.Round(0), completedRound) // No completed rounds due to errors + a.Nil(voters) +}