Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

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

Draft
wants to merge 19 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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

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

func (l *localLedger) GetIncentiveKickoffCandidates(basics.Round, config.ConsensusParams, uint64) (data map[basics.Address]basics.OnlineAccountData, err error) {
cce marked this conversation as resolved.
Show resolved Hide resolved
return nil, nil
}

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

func (dl *dryrunLedger) 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
Expand Down
3 changes: 3 additions & 0 deletions data/basics/userBalance.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,10 @@ type VotingData struct {
type OnlineAccountData struct {
MicroAlgosWithRewards MicroAlgos
VotingData

IncentiveEligible bool
LastProposed Round
LastHeartbeat Round
}

// AccountData contains the data associated with a given address.
Expand Down
4 changes: 4 additions & 0 deletions ledger/acctdeltas.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

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

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

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

func (ml *emptyLedger) 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
}
Expand Down
5 changes: 5 additions & 0 deletions ledger/eval/cow.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type roundCowParent interface {
// lookup retrieves agreement data about an address, querying the ledger if necessary.
lookupAgreement(basics.Address) (basics.OnlineAccountData, error)
onlineStake() (basics.MicroAlgos, error)
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.
Expand Down Expand Up @@ -192,6 +193,10 @@ func (cb *roundCowState) lookupAgreement(addr basics.Address) (data basics.Onlin
return cb.lookupParent.lookupAgreement(addr)
}

func (cb *roundCowState) 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 {
Expand Down
4 changes: 4 additions & 0 deletions ledger/eval/cow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ func (ml *mockLedger) onlineStake() (basics.MicroAlgos, error) {
return basics.Algos(55_555), nil
}

func (ml *mockLedger) 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: &params}, ok, nil // XXX make a copy?
Expand Down
64 changes: 55 additions & 9 deletions ledger/eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type LedgerForCowBase interface {
CheckDup(config.ConsensusParams, basics.Round, basics.Round, basics.Round, transactions.Txid, ledgercore.Txlease) error
LookupWithoutRewards(basics.Round, basics.Address) (ledgercore.AccountData, basics.Round, error)
LookupAgreement(basics.Round, basics.Address) (basics.OnlineAccountData, error)
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)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
jannotti marked this conversation as resolved.
Show resolved Hide resolved
LastProposed: acctData.LastProposed,
LastHeartbeat: acctData.LastHeartbeat,
MicroAlgosWithRewards: acctData.RewardedMicroAlgos,
cce marked this conversation as resolved.
Show resolved Hide resolved
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).
Expand All @@ -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) ||
Copy link
Contributor Author

@cce cce Aug 9, 2024

Choose a reason for hiding this comment

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

@jannotti in your earlier version, you were able to use the (ledgercore.AccountData).MicroAlgos value returned from eval.state.mods.Accts.GetData here without calling AccountData.WithUpdatedRewards() because you knew all accounts in mods would be guaranteed to already have their rewards updated?

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't recall thinking it through that way, but it seems true.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is it worth checking rewardsLevel or anything to make sure they were applied? I imagine all accounts touched should end up having them applied them but maybe there are some that don't?

Copy link
Contributor

Choose a reason for hiding this comment

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

We should do a little further checking. I think any account considered touched here will also be updated. I think for weird cases like an axfer from A to B, only A is touched (since its algo balance changes when paying fee). Similar stuff can happen with accounts in app calls. Probably/hopefully eval.state.modifiedAccounts() are exactly the ones that have truly been touched and will therefore have updated balances.

failsChallenge(ch, accountAddr, lastSeen) {
updates.AbsentParticipationAccounts = append(
updates.AbsentParticipationAccounts,
Expand All @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions ledger/eval/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
}
Expand Down
13 changes: 12 additions & 1 deletion ledger/eval/prefetcher/prefetcher_alignment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,16 +119,23 @@ func (l *prefetcherAlignmentTestLedger) LookupWithoutRewards(_ basics.Round, add
}
return ledgercore.AccountData{}, 0, nil
}

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

func (l *prefetcherAlignmentTestLedger) OnlineCirculation(rnd, voteRnd basics.Round) (basics.MicroAlgos, error) {
return basics.MicroAlgos{}, errors.New("not implemented")
panic("not implemented")
cce marked this conversation as resolved.
Show resolved Hide resolved
}

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 {
Expand All @@ -144,6 +151,7 @@ func (l *prefetcherAlignmentTestLedger) LookupApplication(rnd basics.Round, addr

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

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

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

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

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

func (l *prefetcherAlignmentTestLedger) GenesisHash() crypto.Digest {
return crypto.Digest{}
}
Expand Down
28 changes: 27 additions & 1 deletion ledger/ledger.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ type Ledger struct {
notifier blockNotifier
metrics metricsTracker
spVerification spVerificationTracker
topOnlineCache topOnlineCache

trackers trackerRegistry
trackerMu deadlock.RWMutex
Expand Down Expand Up @@ -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) {
Expand Down
9 changes: 7 additions & 2 deletions ledger/ledgercore/onlineacct.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
cce marked this conversation as resolved.
Show resolved Hide resolved
}
6 changes: 6 additions & 0 deletions ledger/store/trackerdb/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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,
}
}

Expand Down
Loading
Loading