diff --git a/data/transactions/logic/ledger_test.go b/data/transactions/logic/ledger_test.go index 3dcead5e51..b16694d48c 100644 --- a/data/transactions/logic/ledger_test.go +++ b/data/transactions/logic/ledger_test.go @@ -46,9 +46,14 @@ import ( ) type balanceRecord struct { - addr basics.Address - auth basics.Address - balance uint64 + addr basics.Address + auth basics.Address + balance uint64 + voting basics.VotingData + + proposed basics.Round // The last round that this account proposed the accepted block + heartbeat basics.Round // The last round that this account sent a heartbeat to show it was online. + locals map[basics.AppIndex]basics.TealKeyValue holdings map[basics.AssetIndex]basics.AssetHolding mods map[basics.AppIndex]map[string]basics.ValueDelta @@ -119,6 +124,18 @@ func (l *Ledger) NewAccount(addr basics.Address, balance uint64) { l.balances[addr] = newBalanceRecord(addr, balance) } +// NewVoting sets VoteID on the account. Could expand to set other voting data +// if that became useful in tests. +func (l *Ledger) NewVoting(addr basics.Address, voteID crypto.OneTimeSignatureVerifier) { + br, ok := l.balances[addr] + if !ok { + br = newBalanceRecord(addr, 0) + } + br.voting.VoteID = voteID + br.voting.VoteKeyDilution = 10_000 + l.balances[addr] = br +} + // NewApp add a new AVM app to the Ledger. In most uses, it only sets up the id // and schema but no code, as testing will want to try many different code // sequences. @@ -312,7 +329,11 @@ func (l *Ledger) AccountData(addr basics.Address) (ledgercore.AccountData, error TotalBoxes: uint64(boxesTotal), TotalBoxBytes: uint64(boxBytesTotal), + + LastProposed: br.proposed, + LastHeartbeat: br.heartbeat, }, + VotingData: br.voting, }, nil } @@ -952,6 +973,8 @@ func (l *Ledger) Get(addr basics.Address, withPendingRewards bool) (basics.Accou Assets: map[basics.AssetIndex]basics.AssetHolding{}, AppLocalStates: map[basics.AppIndex]basics.AppLocalState{}, AppParams: map[basics.AppIndex]basics.AppParams{}, + LastProposed: br.proposed, + LastHeartbeat: br.heartbeat, }, nil } diff --git a/heartbeat/abstractions.go b/heartbeat/abstractions.go index 52206a0ef7..9ccecb6fb9 100644 --- a/heartbeat/abstractions.go +++ b/heartbeat/abstractions.go @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2023 Algorand, Inc. +// 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 @@ -30,8 +30,8 @@ type txnBroadcaster interface { BroadcastInternalSignedTxGroup([]transactions.SignedTxn) error } -// ledger represents the aspects of the "real" Ledger that heartbeat needs. -// to interact with. +// ledger represents the aspects of the "real" Ledger that the heartbeat service +// needs to interact with type ledger interface { // LastRound tells the round is ready for checking LastRound() basics.Round diff --git a/heartbeat/service.go b/heartbeat/service.go index 8f9775cc50..48d99cb959 100644 --- a/heartbeat/service.go +++ b/heartbeat/service.go @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2023 Algorand, Inc. +// 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 diff --git a/heartbeat/service_test.go b/heartbeat/service_test.go index aef54dd29f..2246bde174 100644 --- a/heartbeat/service_test.go +++ b/heartbeat/service_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2023 Algorand, Inc. +// 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 @@ -21,6 +21,7 @@ import ( "testing" "time" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/data/account" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" @@ -69,6 +70,7 @@ type mockedLedger struct { waiters map[basics.Round]chan struct{} history []table version protocol.ConsensusVersion + hdrs map[basics.Round]bookkeeping.BlockHeader } func newMockedLedger() mockedLedger { @@ -110,12 +112,25 @@ func (l *mockedLedger) WaitMem(r basics.Round) chan struct{} { // BlockHdr allows the service access to consensus values func (l *mockedLedger) BlockHdr(r basics.Round) (bookkeeping.BlockHeader, error) { + if r > l.LastRound() { + return bookkeeping.BlockHeader{}, fmt.Errorf("%d is beyond current block (%d)", r, l.LastRound()) + } + if hdr, ok := l.hdrs[r]; ok { + return hdr, nil + } + // just return a simple hdr var hdr bookkeeping.BlockHeader hdr.Round = r hdr.CurrentProtocol = l.version return hdr, nil } +// addHeader places a block header into the ledger's history. It is used to make +// challenges occur as we'd like. +func (l *mockedLedger) addHeader(hdr bookkeeping.BlockHeader) { + l.hdrs[hdr.Round] = hdr +} + func (l *mockedLedger) addBlock(delta table) error { l.mu.Lock() defer l.mu.Unlock() @@ -197,7 +212,7 @@ func makeBlock(r basics.Round) bookkeeping.Block { } } -func TestHeartBeatOnlyWhenSuspended(t *testing.T) { +func TestHeartBeatOnlyWhenChallenged(t *testing.T) { partitiontest.PartitionTest(t) t.Parallel() @@ -208,15 +223,8 @@ func TestHeartBeatOnlyWhenSuspended(t *testing.T) { s := NewService(accts, &ledger, &sink, logging.TestingLog(t)) s.Start() - // ensure Donor can pay - a.NoError(ledger.addBlock(table{Donor: ledgercore.AccountData{ - AccountBaseData: ledgercore.AccountBaseData{ - MicroAlgos: basics.MicroAlgos{Raw: 1_000_000}, - }, - }})) - a.Empty(sink) - - joe := basics.Address{1, 1} + // joe is a simple, non-online account, service will not heartbeat + joe := basics.Address{0xcc} // 0xcc will matter when we set the challenge accts.add(joe) acct := ledgercore.AccountData{} @@ -225,123 +233,30 @@ func TestHeartBeatOnlyWhenSuspended(t *testing.T) { ledger.waitFor(s, a) a.Empty(sink) + // now joe is online, but not challenged, so no heartbeat acct.Status = basics.Online a.NoError(ledger.addBlock(table{joe: acct})) a.Empty(sink) - acct.Status = basics.Suspended + // now we have to make it seem like joe has been challenged. We obtain the + // payout rules to find the first challenge round, skip forward to it, then + // go forward half a grace period. Only then should the service heartbeat + hdr, err := ledger.BlockHdr(ledger.LastRound()) + a.NoError(err) + rules := config.Consensus[hdr.CurrentProtocol].Payouts + for ledger.LastRound() < basics.Round(rules.ChallengeInterval) { + a.NoError(ledger.addBlock(table{})) + ledger.waitFor(s, a) + a.Empty(sink) + } a.NoError(ledger.addBlock(table{joe: acct})) ledger.waitFor(s, a) - a.Len(sink, 1) // only one heartbeat so far - a.Len(sink[0], 1) // will probably end up being 3 to pay for `heartbeat` opcode + a.Len(sink, 1) // only one heartbeat so far + a.Len(sink[0], 1) + a.Equal(sink[0][0].Txn.Type, protocol.HeartbeatTx) + a.Equal(sink[0][0].Txn.HeartbeatAddress, joe) s.Stop() } - -func TestHeartBeatOnlyWhenDonorFunded(t *testing.T) { - partitiontest.PartitionTest(t) - t.Parallel() - - a := require.New(t) - sink := txnSink{} - accts := &mockParticipants{} - ledger := newMockedLedger() - s := NewService(accts, &ledger, &sink, logging.TestingLog(t)) - s.Start() - - joe := basics.Address{1, 1} - accts.add(joe) - - acct := ledgercore.AccountData{} - - a.NoError(ledger.addBlock(table{joe: acct})) - a.Empty(sink) - - acct.Status = basics.Suspended - - a.NoError(ledger.addBlock(table{joe: acct})) - ledger.waitFor(s, a) - a.Empty(sink) // no funded donor, no heartbeat - - // Donor exists, has enough for fee, but not enough when MBR is considered - a.NoError(ledger.addBlock(table{Donor: ledgercore.AccountData{ - AccountBaseData: ledgercore.AccountBaseData{ - MicroAlgos: basics.MicroAlgos{Raw: 100_000}, - }, - }})) - a.NoError(ledger.addBlock(table{joe: acct})) - ledger.waitFor(s, a) - a.Empty(sink) - - a.NoError(ledger.addBlock(table{Donor: ledgercore.AccountData{ - AccountBaseData: ledgercore.AccountBaseData{ - MicroAlgos: basics.MicroAlgos{Raw: 200_000}, - }, - }})) - ledger.waitFor(s, a) - a.Len(sink, 1) // only one heartbeat so far - a.Len(sink[0], 1) // will probably end up being 3 to pay for `heartbeat` opcode - s.Stop() -} - -func TestHeartBeatForm(t *testing.T) { - partitiontest.PartitionTest(t) - t.Parallel() - - a := require.New(t) - sink := txnSink{} - accts := &mockParticipants{} - ledger := newMockedLedger() - s := NewService(accts, &ledger, &sink, logging.TestingLog(t)) - s.Start() - - joe := basics.Address{1, 1} - accts.add(joe) - - // Fund the donor, suspend joe - a.NoError(ledger.addBlock(table{ - Donor: ledgercore.AccountData{ - AccountBaseData: ledgercore.AccountBaseData{ - MicroAlgos: basics.MicroAlgos{Raw: 200_000}, - }, - }, - joe: ledgercore.AccountData{ - AccountBaseData: ledgercore.AccountBaseData{ - Status: basics.Suspended, - MicroAlgos: basics.MicroAlgos{Raw: 2_000_000}, - }, - }, - })) - ledger.waitFor(s, a) - a.Len(sink, 1) // only one heartbeat so far - a.Len(sink[0], 1) // will probably end up being 3 to pay for `heartbeat` opcode - - grp := sink[0] - require.Equal(t, grp[0].Txn.Sender, Donor) - require.Equal(t, grp[0].Lsig, transactions.LogicSig{Logic: DonorByteCode}) - - a.NoError(ledger.addBlock(nil)) - ledger.waitFor(s, a) - a.Len(sink, 2) // still suspended, another heartbeat - inc := sink[0] - inc[0].Txn.FirstValid++ - inc[0].Txn.LastValid++ - a.Equal(inc, sink[1]) - - // mark joe online again - a.NoError(ledger.addBlock(table{ - joe: ledgercore.AccountData{ - AccountBaseData: ledgercore.AccountBaseData{ - Status: basics.Online, - MicroAlgos: basics.MicroAlgos{Raw: 2_000_000}, - }, - }, - })) - ledger.waitFor(s, a) - a.Len(sink, 2) // no further heartbeat - - s.Stop() - -}