diff --git a/ledger/eval/eval.go b/ledger/eval/eval.go index c3eececc88..ba57ed1348 100644 --- a/ledger/eval/eval.go +++ b/ledger/eval/eval.go @@ -1632,13 +1632,15 @@ func (eval *BlockEvaluator) proposerPayout() (basics.MicroAlgos, error) { } // generateKnockOfflineAccountsList creates the lists of expired or absent -// participation accounts by traversing over the modified accounts in the state -// deltas and testing if any of them needs to be reset/suspended. Expiration -// takes precedence - if an account is expired, it should be knocked offline and -// key material deleted. If it is only suspended, the key material will remain. +// participation accounts to be suspended. It examines the accounts that appear +// in the current block and high-stake accounts being tracked for state +// proofs. Expiration takes precedence - if an account is expired, it should be +// knocked offline and key material deleted. If it is only suspended, the key +// material will remain. // -// Different ndoes may propose different list of addresses based on node state. -// Block validators only check whether ExpiredParticipationAccounts or +// Different nodes may propose different list of addresses based on node state, +// the protocol does not enforce which accounts must appear. Block validators +// only check whether ExpiredParticipationAccounts or // AbsentParticipationAccounts meet the criteria for expiration or suspension, // not whether the lists are complete. // @@ -1740,7 +1742,7 @@ func (eval *BlockEvaluator) generateKnockOfflineAccountsList(participating []bas updates.ExpiredParticipationAccounts, accountAddr, ) - continue // if marking expired, do not also suspend + continue // if marking expired, do not consider suspension } } @@ -1750,7 +1752,7 @@ func (eval *BlockEvaluator) generateKnockOfflineAccountsList(participating []bas continue // no more room (don't break the loop, since we may have more expiries) } - if acctData.Status == basics.Online { + if acctData.Status == basics.Online && acctData.IncentiveEligible { lastSeen := max(acctData.LastProposed, acctData.LastHeartbeat) oad, lErr := eval.state.lookupAgreement(accountAddr) if lErr != nil { @@ -1882,6 +1884,9 @@ func (eval *BlockEvaluator) validateAbsentOnlineAccounts() error { if acctData.MicroAlgos.IsZero() { return fmt.Errorf("proposed absent account %v with zero algos", accountAddr) } + if !acctData.IncentiveEligible { + return fmt.Errorf("proposed absent account %v not IncentiveEligible", accountAddr) + } oad, lErr := eval.state.lookupAgreement(accountAddr) if lErr != nil { diff --git a/ledger/eval/eval_test.go b/ledger/eval/eval_test.go index 6ff50de2cd..494341e859 100644 --- a/ledger/eval/eval_test.go +++ b/ledger/eval/eval_test.go @@ -1336,6 +1336,7 @@ func TestAbsenteeChecks(t *testing.T) { crypto.RandBytes(tmp.StateProofID[:]) crypto.RandBytes(tmp.SelectionID[:]) crypto.RandBytes(tmp.VoteID[:]) + tmp.IncentiveEligible = true // make suspendable tmp.VoteFirstValid = 1 tmp.VoteLastValid = 1500 // large enough to avoid EXPIRATION, so we can see SUSPENSION switch i { @@ -1345,16 +1346,25 @@ func TestAbsenteeChecks(t *testing.T) { tmp.LastProposed = 1 // we want addrs[2] to be suspended earlier than others case 3: tmp.LastProposed = 1 // we want addrs[3] to be a proposer, and never suspend itself + case 5: + tmp.LastHeartbeat = 1 // like addr[1] but !IncentiveEligible, no suspend + tmp.IncentiveEligible = false + case 6: + tmp.LastProposed = 1 // like addr[2] but !IncentiveEligible, no suspend + tmp.IncentiveEligible = false default: - if i < 10 { // make the other 8 genesis wallets unsuspendable - if i%2 == 0 { + if i < 10 { // make 0,3,4,7,8,9 unsuspendable + switch i % 3 { + case 0: tmp.LastProposed = 1200 - } else { + case 1: tmp.LastHeartbeat = 1200 + case 2: + tmp.IncentiveEligible = false } } else { - // ensure non-zero balance for new accounts, but a small balance - // so they will not be absent, just challenged. + // ensure non-zero balance for the 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 } @@ -1385,6 +1395,7 @@ func TestAbsenteeChecks(t *testing.T) { switch vb.Block().Round() { case 202: // 2 out of 10 genesis accounts are now absent + require.Len(t, vb.Block().AbsentParticipationAccounts, 2, addrs) require.Contains(t, vb.Block().AbsentParticipationAccounts, addrs[1]) require.Contains(t, vb.Block().AbsentParticipationAccounts, addrs[2]) case 1000: