Skip to content

Commit

Permalink
Validate proposed absents, and suspend them.
Browse files Browse the repository at this point in the history
First cut at computing the expected interval. Uses 10x the expected
interval, but calculating the interval is currently wonky.  Uses the
last round's total online stake, but this round's acct stake.  Also
ignores rewards.
  • Loading branch information
jannotti committed Oct 19, 2023
1 parent 5c52d1e commit f71bd0f
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 41 deletions.
50 changes: 28 additions & 22 deletions ledger/eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -1321,13 +1321,17 @@ func (eval *BlockEvaluator) endOfBlock() error {
}
}

err := eval.validateExpiredOnlineAccounts()
if err != nil {
if err := eval.validateExpiredOnlineAccounts(); err != nil {
return err
}
if err := eval.resetExpiredOnlineAccountsParticipationKeys(); err != nil {
return err
}

err = eval.resetExpiredOnlineAccountsParticipationKeys()
if err != nil {
if err := eval.validateAbsentOnlineAccounts(); err != nil {
return err
}
if err := eval.suspendAbsentAccounts(); err != nil {
return err
}

Expand Down Expand Up @@ -1399,8 +1403,7 @@ func (eval *BlockEvaluator) endOfBlock() error {
}
}

err = eval.state.CalculateTotals()
if err != nil {
if err := eval.state.CalculateTotals(); err != nil {
return err
}

Expand Down Expand Up @@ -1449,15 +1452,8 @@ func (eval *BlockEvaluator) generateKnockOfflineAccountsList() {
}

if acctDelta.Status == basics.Online {
// TODO: 1000 rounds is obviously a placeholder. Probably needs to
// be something like 10x the "expected" interval. Further
// complications: (1) At keyreg, LastProposed=0 (or is old). (2)
// Accounts should be allowed to "heartbeat" to stay online. (3)
// "Expected" interval varies with balance and onlinestake.
allowableLag := basics.Round(1000)
absent := acctDelta.LastProposed+allowableLag < currentRound &&
acctDelta.LastHeartbeat+allowableLag < currentRound
if absent &&
lastSeen := max(acctDelta.LastHeartbeat, acctDelta.LastHeartbeat)
if isAbsent(eval.state.prevTotals.Online.Money, acctDelta.MicroAlgos, lastSeen, currentRound) &&
len(updates.AbsentParticipationAccounts) < expectedMaxNumberOfAbsentAccounts {
updates.AbsentParticipationAccounts = append(
updates.AbsentParticipationAccounts,
Expand All @@ -1468,6 +1464,21 @@ 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
}

func isAbsent(totalOnlineStake basics.MicroAlgos, acctStake basics.MicroAlgos, lastSeen basics.Round, current basics.Round) bool {
// See if the account has exceeded 10x their expected observation interval.
allowableLag := basics.Round(10 * totalOnlineStake.Raw / acctStake.Raw)
fmt.Printf("%d / %d -> %d \n", acctStake, totalOnlineStake, allowableLag)
return lastSeen+allowableLag < current
}

// validateExpiredOnlineAccounts tests the expired online accounts specified in ExpiredParticipationAccounts, and verify
// that they have all expired and need to be reset.
func (eval *BlockEvaluator) validateExpiredOnlineAccounts() error {
Expand Down Expand Up @@ -1553,13 +1564,8 @@ func (eval *BlockEvaluator) validateAbsentOnlineAccounts() error {
return fmt.Errorf("proposed absent acct %v was not online but %v", accountAddr, acctData.Status)
}

// TODO: 1000 rounds is obviously a placeholder. Probably needs to be
// something like 10x the "expected" interval. Further complications:
// (1) At first keyreg, LastProposed=0. (2) Accounts should be allowed
// to "heartbeat" to stay online.
allowableLag := basics.Round(1000)
if acctData.LastProposed+allowableLag >= currentRound ||
acctData.LastHeartbeat+allowableLag >= currentRound {
lastSeen := max(acctData.LastHeartbeat, acctData.LastHeartbeat)
if !isAbsent(eval.state.prevTotals.Online.Money, acctData.MicroAlgos, lastSeen, currentRound) {
return fmt.Errorf("proposed absent account %v is not absent in %d, %d",
accountAddr, acctData.LastProposed, acctData.LastHeartbeat)
}
Expand Down
88 changes: 74 additions & 14 deletions ledger/eval_simple_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,26 +278,42 @@ func TestAbsentTracking(t *testing.T) {
partitiontest.PartitionTest(t)
t.Parallel()

genBalances, addrs, _ := ledgertesting.NewTestGenesis()
genBalances, addrs, _ := ledgertesting.NewTestGenesis(func(cfg *ledgertesting.GenesisCfg) {
cfg.OnlineCount = 2 // So we know proposer should propose every 2 rounds, on average
})
// Absentee checking begins in v39. Start checking in v38 to test that is unchanged.
ledgertesting.TestConsensusRange(t, 38, 0, func(t *testing.T, ver int, cv protocol.ConsensusVersion, cfg config.Local) {
dl := NewDoubleLedger(t, genBalances, cv, cfg)
defer dl.Close()

totals, err := dl.generator.Totals(0)
require.NoError(t, err)
require.NotZero(t, totals.Online.Money.Raw)
for i, addr := range addrs {
fmt.Printf("addrs[%d] == %v\n", i, addr)
}
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.False(t, lookup(t, dl.generator, addrs[2]).Status == basics.Online)

dl.fullBlock()

pay := txntest.Txn{
proposer := addrs[7]
dl.beginBlock()
dl.txns(&txntest.Txn{
Type: "pay",
Sender: addrs[1],
Receiver: addrs[2],
Amount: 100000,
}

proposer := addrs[7]
dl.beginBlock()
dl.txns(&pay)
Amount: 100_000,
})
dl.endBlock(proposer)

newtotals, err := dl.generator.Totals(dl.generator.Latest())
require.NoError(t, err)
// payment and fee left the online account
require.Equal(t, totals.Online.Money.Raw-100_000-1000, newtotals.Online.Money.Raw)
totals = newtotals

dl.fullBlock()

prp := lookup(t, dl.validator, proposer)
Expand All @@ -313,8 +329,13 @@ func TestAbsentTracking(t *testing.T) {
require.Zero(t, prp.LastHeartbeat)
}

dl.txns(&txntest.Txn{Type: "keyreg", Sender: addrs[1]}) // OFFLINE keyreg
regger := lookup(t, dl.validator, addrs[1])
// addrs[2] was already offline
dl.txns(&txntest.Txn{Type: "keyreg", Sender: addrs[2]}) // OFFLINE keyreg
regger := lookup(t, dl.validator, addrs[2])

newtotals, err = dl.generator.Totals(dl.generator.Latest())
require.NoError(t, err)
require.Equal(t, totals.Online.Money.Raw, newtotals.Online.Money.Raw)

// offline transaction records nothing
require.Zero(t, regger.LastProposed)
Expand All @@ -323,19 +344,58 @@ func TestAbsentTracking(t *testing.T) {
// ONLINE keyreg
dl.txns(&txntest.Txn{
Type: "keyreg",
Sender: addrs[1],
Sender: addrs[2],
VotePK: [32]byte{1},
SelectionPK: [32]byte{1},
})
regger = lookup(t, dl.validator, addrs[1])
newtotals, err = dl.generator.Totals(dl.generator.Latest())
require.NoError(t, err)
require.Greater(t, newtotals.Online.Money.Raw, totals.Online.Money.Raw)

regger = lookup(t, dl.validator, addrs[2])
require.Zero(t, regger.LastProposed)
require.True(t, regger.Status == basics.Online)

if ver >= 39 {
require.Zero(t, regger.LastProposed)
require.NotZero(t, regger.LastHeartbeat) // online keyreg caused update
} else {
require.Zero(t, regger.LastProposed)
require.Zero(t, regger.LastHeartbeat)
}

for i := 0; i < 5; i++ {
dl.fullBlock()
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)
}

// all are still online after a few blocks
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)

for i := 0; i < 30; i++ {
dl.fullBlock()
}

// addrs 0-2 all have about 1/3 of stake, so become eligible for
// suspension after 30 rounds. We're at about 35. But, since blocks are
// empty, nobody's susspendible account is noticed.
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)

// when 2 pays 0, they both get noticed and get suspended
dl.txns(&txntest.Txn{
Type: "pay",
Sender: addrs[2],
Receiver: addrs[0],
Amount: 0,
})
require.Equal(t, ver < 39, lookup(t, dl.generator, addrs[0]).Status == basics.Online)
require.True(t, lookup(t, dl.generator, addrs[1]).Status == basics.Online)
require.Equal(t, ver < 39, lookup(t, dl.generator, addrs[2]).Status == basics.Online)

})
}

Expand Down
20 changes: 15 additions & 5 deletions ledger/testing/testGenesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,26 @@ import (
"github.com/algorand/go-algorand/protocol"
)

// testGenesisCfg provides a configuration object for NewTestGenesis.
type testGenesisCfg struct {
// GenesisCfg provides a configuration object for NewTestGenesis.
type GenesisCfg struct {
rewardsPoolAmount basics.MicroAlgos
OnlineCount int
}

// TestGenesisOption provides functional options for testGenesisCfg.
type TestGenesisOption func(*testGenesisCfg)
type TestGenesisOption func(*GenesisCfg)

// TestGenesisRewardsPoolSize configures the rewards pool size in the genesis block.
func TestGenesisRewardsPoolSize(amount basics.MicroAlgos) TestGenesisOption {
return func(cfg *testGenesisCfg) { cfg.rewardsPoolAmount = amount }
return func(cfg *GenesisCfg) { cfg.rewardsPoolAmount = amount }
}

// NewTestGenesis creates a bunch of accounts, splits up 10B algos
// between them and the rewardspool and feesink, and gives out the
// addresses and secrets it creates to enable tests. For special
// scenarios, manipulate these return values before using newTestLedger.
func NewTestGenesis(opts ...TestGenesisOption) (bookkeeping.GenesisBalances, []basics.Address, []*crypto.SignatureSecrets) {
var cfg testGenesisCfg
var cfg GenesisCfg
for _, opt := range opts {
opt(&cfg)
}
Expand Down Expand Up @@ -75,6 +76,15 @@ func NewTestGenesis(opts ...TestGenesisOption) (bookkeeping.GenesisBalances, []b

adata := basics.AccountData{
MicroAlgos: basics.MicroAlgos{Raw: amount},
Status: basics.Offline,
}
if i < cfg.OnlineCount {
adata.Status = basics.Online
adata.VoteFirstValid = 0
adata.VoteLastValid = 1_000_000
crypto.RandBytes(adata.VoteID[:])
crypto.RandBytes(adata.SelectionID[:])
crypto.RandBytes(adata.StateProofID[:])
}
accts[addrs[i]] = adata
}
Expand Down

0 comments on commit f71bd0f

Please sign in to comment.