summaryrefslogtreecommitdiff
path: root/ledger/internal/eval_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'ledger/internal/eval_test.go')
-rw-r--r--ledger/internal/eval_test.go1030
1 files changed, 1030 insertions, 0 deletions
diff --git a/ledger/internal/eval_test.go b/ledger/internal/eval_test.go
new file mode 100644
index 000000000..c3bae4613
--- /dev/null
+++ b/ledger/internal/eval_test.go
@@ -0,0 +1,1030 @@
+// Copyright (C) 2019-2021 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 <https://www.gnu.org/licenses/>.
+
+package internal
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "math/rand"
+ "reflect"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/algorand/go-algorand/agreement"
+ "github.com/algorand/go-algorand/config"
+ "github.com/algorand/go-algorand/crypto"
+ "github.com/algorand/go-algorand/crypto/compactcert"
+ "github.com/algorand/go-algorand/data/basics"
+ "github.com/algorand/go-algorand/data/bookkeeping"
+ "github.com/algorand/go-algorand/data/transactions"
+ "github.com/algorand/go-algorand/data/transactions/verify"
+ "github.com/algorand/go-algorand/data/txntest"
+ "github.com/algorand/go-algorand/ledger/ledgercore"
+ ledgertesting "github.com/algorand/go-algorand/ledger/testing"
+ "github.com/algorand/go-algorand/protocol"
+ "github.com/algorand/go-algorand/test/partitiontest"
+ "github.com/algorand/go-algorand/util/execpool"
+)
+
+var testPoolAddr = basics.Address{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
+var testSinkAddr = basics.Address{0x2c, 0x2a, 0x6c, 0xe9, 0xa9, 0xa7, 0xc2, 0x8c, 0x22, 0x95, 0xfd, 0x32, 0x4f, 0x77, 0xa5, 0x4, 0x8b, 0x42, 0xc2, 0xb7, 0xa8, 0x54, 0x84, 0xb6, 0x80, 0xb1, 0xe1, 0x3d, 0x59, 0x9b, 0xeb, 0x36}
+var minFee basics.MicroAlgos
+
+func init() {
+ params := config.Consensus[protocol.ConsensusCurrentVersion]
+ minFee = basics.MicroAlgos{Raw: params.MinTxnFee}
+}
+
+func TestBlockEvaluatorFeeSink(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ genesisInitState, _, _ := ledgertesting.Genesis(10)
+
+ genesisBalances := bookkeeping.GenesisBalances{
+ Balances: genesisInitState.Accounts,
+ FeeSink: testSinkAddr,
+ RewardsPool: testPoolAddr,
+ Timestamp: 0,
+ }
+ l := newTestLedger(t, genesisBalances)
+
+ genesisBlockHeader, err := l.BlockHdr(basics.Round(0))
+ newBlock := bookkeeping.MakeBlock(genesisBlockHeader)
+ eval, err := l.StartEvaluator(newBlock.BlockHeader, 0, 0)
+ require.NoError(t, err)
+ require.Equal(t, eval.specials.FeeSink, testSinkAddr)
+}
+
+func TestPrepareEvalParams(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ eval := BlockEvaluator{
+ prevHeader: bookkeeping.BlockHeader{
+ TimeStamp: 1234,
+ Round: 2345,
+ },
+ }
+
+ params := []config.ConsensusParams{
+ {Application: true, MaxAppProgramCost: 700},
+ config.Consensus[protocol.ConsensusV29],
+ config.Consensus[protocol.ConsensusFuture],
+ }
+
+ // Create some sample transactions
+ payment := txntest.Txn{
+ Type: protocol.PaymentTx,
+ Sender: basics.Address{1, 2, 3, 4},
+ Receiver: basics.Address{4, 3, 2, 1},
+ Amount: 100,
+ }.SignedTxnWithAD()
+
+ appcall1 := txntest.Txn{
+ Type: protocol.ApplicationCallTx,
+ Sender: basics.Address{1, 2, 3, 4},
+ ApplicationID: basics.AppIndex(1),
+ }.SignedTxnWithAD()
+
+ appcall2 := appcall1
+ appcall2.SignedTxn.Txn.ApplicationCallTxnFields.ApplicationID = basics.AppIndex(2)
+
+ type evalTestCase struct {
+ group []transactions.SignedTxnWithAD
+
+ // indicates if prepareAppEvaluators should return a non-nil
+ // appTealEvaluator for the txn at index i
+ expected []bool
+
+ numAppCalls int
+ // Used for checking transitive pointer equality in app calls
+ // If there are no app calls in the group, it is set to -1
+ firstAppCallIndex int
+ }
+
+ // Create some groups with these transactions
+ cases := []evalTestCase{
+ {[]transactions.SignedTxnWithAD{payment}, []bool{false}, 0, -1},
+ {[]transactions.SignedTxnWithAD{appcall1}, []bool{true}, 1, 0},
+ {[]transactions.SignedTxnWithAD{payment, payment}, []bool{false, false}, 0, -1},
+ {[]transactions.SignedTxnWithAD{appcall1, payment}, []bool{true, false}, 1, 0},
+ {[]transactions.SignedTxnWithAD{payment, appcall1}, []bool{false, true}, 1, 1},
+ {[]transactions.SignedTxnWithAD{appcall1, appcall2}, []bool{true, true}, 2, 0},
+ {[]transactions.SignedTxnWithAD{appcall1, appcall2, appcall1}, []bool{true, true, true}, 3, 0},
+ {[]transactions.SignedTxnWithAD{payment, appcall1, payment}, []bool{false, true, false}, 1, 1},
+ {[]transactions.SignedTxnWithAD{appcall1, payment, appcall2}, []bool{true, false, true}, 2, 0},
+ }
+
+ for i, param := range params {
+ for j, testCase := range cases {
+ t.Run(fmt.Sprintf("i=%d,j=%d", i, j), func(t *testing.T) {
+ eval.proto = param
+ res := eval.prepareEvalParams(testCase.group)
+ require.Equal(t, len(res), len(testCase.group))
+
+ // Compute the expected transaction group without ApplyData for
+ // the test case
+ expGroupNoAD := make([]transactions.SignedTxn, len(testCase.group))
+ for k := range testCase.group {
+ expGroupNoAD[k] = testCase.group[k].SignedTxn
+ }
+
+ // Ensure non app calls have a nil evaluator, and that non-nil
+ // evaluators point to the right transactions and values
+ for k, present := range testCase.expected {
+ if present {
+ require.NotNil(t, res[k])
+ require.NotNil(t, res[k].PastSideEffects)
+ require.Equal(t, res[k].GroupIndex, uint64(k))
+ require.Equal(t, res[k].TxnGroup, expGroupNoAD)
+ require.Equal(t, *res[k].Proto, eval.proto)
+ require.Equal(t, *res[k].Txn, testCase.group[k].SignedTxn)
+ require.Equal(t, res[k].MinTealVersion, res[testCase.firstAppCallIndex].MinTealVersion)
+ require.Equal(t, res[k].PooledApplicationBudget, res[testCase.firstAppCallIndex].PooledApplicationBudget)
+ if reflect.DeepEqual(param, config.Consensus[protocol.ConsensusV29]) {
+ require.Equal(t, *res[k].PooledApplicationBudget, uint64(eval.proto.MaxAppProgramCost))
+ } else if reflect.DeepEqual(param, config.Consensus[protocol.ConsensusFuture]) {
+ require.Equal(t, *res[k].PooledApplicationBudget, uint64(eval.proto.MaxAppProgramCost*testCase.numAppCalls))
+ }
+ } else {
+ require.Nil(t, res[k])
+ }
+ }
+ })
+ }
+ }
+}
+
+func TestCowCompactCert(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ var certRnd basics.Round
+ var certType protocol.CompactCertType
+ var cert compactcert.Cert
+ var atRound basics.Round
+ var validate bool
+ accts0 := ledgertesting.RandomAccounts(20, true)
+ blocks := make(map[basics.Round]bookkeeping.BlockHeader)
+ blockErr := make(map[basics.Round]error)
+ ml := mockLedger{balanceMap: accts0, blocks: blocks, blockErr: blockErr}
+ c0 := makeRoundCowState(
+ &ml, bookkeeping.BlockHeader{}, config.Consensus[protocol.ConsensusCurrentVersion],
+ 0, ledgercore.AccountTotals{}, 0)
+
+ certType = protocol.CompactCertType(1234) // bad cert type
+ err := c0.compactCert(certRnd, certType, cert, atRound, validate)
+ require.Error(t, err)
+
+ // no certRnd block
+ certType = protocol.CompactCertBasic
+ noBlockErr := errors.New("no block")
+ blockErr[3] = noBlockErr
+ certRnd = 3
+ err = c0.compactCert(certRnd, certType, cert, atRound, validate)
+ require.Error(t, err)
+
+ // no votersRnd block
+ // this is slightly a mess of things that don't quite line up with likely usage
+ validate = true
+ var certHdr bookkeeping.BlockHeader
+ certHdr.CurrentProtocol = "TestCowCompactCert"
+ certHdr.Round = 1
+ proto := config.Consensus[certHdr.CurrentProtocol]
+ proto.CompactCertRounds = 2
+ config.Consensus[certHdr.CurrentProtocol] = proto
+ blocks[certHdr.Round] = certHdr
+
+ certHdr.Round = 15
+ blocks[certHdr.Round] = certHdr
+ certRnd = certHdr.Round
+ blockErr[13] = noBlockErr
+ err = c0.compactCert(certRnd, certType, cert, atRound, validate)
+ require.Error(t, err)
+
+ // validate fail
+ certHdr.Round = 1
+ certRnd = certHdr.Round
+ err = c0.compactCert(certRnd, certType, cert, atRound, validate)
+ require.Error(t, err)
+
+ // fall through to no err
+ validate = false
+ err = c0.compactCert(certRnd, certType, cert, atRound, validate)
+ require.NoError(t, err)
+
+ // 100% coverage
+}
+
+// a couple trivial tests that don't need setup
+// see TestBlockEvaluator for more
+func TestTestTransactionGroup(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ var txgroup []transactions.SignedTxn
+ eval := BlockEvaluator{}
+ err := eval.TestTransactionGroup(txgroup)
+ require.NoError(t, err) // nothing to do, no problem
+
+ eval.proto = config.Consensus[protocol.ConsensusCurrentVersion]
+ txgroup = make([]transactions.SignedTxn, eval.proto.MaxTxGroupSize+1)
+ err = eval.TestTransactionGroup(txgroup)
+ require.Error(t, err) // too many
+}
+
+// test BlockEvaluator.transactionGroup()
+// some trivial checks that require no setup
+func TestPrivateTransactionGroup(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ var txgroup []transactions.SignedTxnWithAD
+ eval := BlockEvaluator{}
+ err := eval.TransactionGroup(txgroup)
+ require.NoError(t, err) // nothing to do, no problem
+
+ eval.proto = config.Consensus[protocol.ConsensusCurrentVersion]
+ txgroup = make([]transactions.SignedTxnWithAD, eval.proto.MaxTxGroupSize+1)
+ err = eval.TransactionGroup(txgroup)
+ require.Error(t, err) // too many
+}
+
+// BlockEvaluator.workaroundOverspentRewards() fixed a couple issues on testnet.
+// This is now part of history and has to be re-created when running catchup on testnet. So, test to ensure it keeps happenning.
+func TestTestnetFixup(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ eval := &BlockEvaluator{}
+ var rewardPoolBalance basics.AccountData
+ rewardPoolBalance.MicroAlgos.Raw = 1234
+ var headerRound basics.Round
+ testnetGenesisHash, _ := crypto.DigestFromString("JBR3KGFEWPEE5SAQ6IWU6EEBZMHXD4CZU6WCBXWGF57XBZIJHIRA")
+
+ // not a fixup round, no change
+ headerRound = 1
+ poolOld, err := eval.workaroundOverspentRewards(rewardPoolBalance, headerRound)
+ require.Equal(t, rewardPoolBalance, poolOld)
+ require.NoError(t, err)
+
+ eval.genesisHash = testnetGenesisHash
+ eval.genesisHash[3]++
+
+ specialRounds := []basics.Round{1499995, 2926564}
+ for _, headerRound = range specialRounds {
+ poolOld, err = eval.workaroundOverspentRewards(rewardPoolBalance, headerRound)
+ require.Equal(t, rewardPoolBalance, poolOld)
+ require.NoError(t, err)
+ }
+
+ for _, headerRound = range specialRounds {
+ testnetFixupExecution(t, headerRound, 20000000000)
+ }
+ // do all the setup and do nothing for not a special round
+ testnetFixupExecution(t, specialRounds[0]+1, 0)
+}
+
+func testnetFixupExecution(t *testing.T, headerRound basics.Round, poolBonus uint64) {
+ testnetGenesisHash, _ := crypto.DigestFromString("JBR3KGFEWPEE5SAQ6IWU6EEBZMHXD4CZU6WCBXWGF57XBZIJHIRA")
+ // big setup so we can move some algos
+ // boilerplate like TestBlockEvaluator, but pretend to be testnet
+ genesisInitState, addrs, keys := ledgertesting.Genesis(10)
+ genesisInitState.Block.BlockHeader.GenesisHash = testnetGenesisHash
+ genesisInitState.Block.BlockHeader.GenesisID = "testnet"
+ genesisInitState.GenesisHash = testnetGenesisHash
+
+ rewardPoolBalance := genesisInitState.Accounts[testPoolAddr]
+ nextPoolBalance := rewardPoolBalance.MicroAlgos.Raw + poolBonus
+
+ l := newTestLedger(t, bookkeeping.GenesisBalances{
+ Balances: genesisInitState.Accounts,
+ FeeSink: testSinkAddr,
+ RewardsPool: testPoolAddr,
+ })
+ l.blocks[0] = genesisInitState.Block
+ l.genesisHash = genesisInitState.GenesisHash
+
+ newBlock := bookkeeping.MakeBlock(genesisInitState.Block.BlockHeader)
+ eval, err := l.StartEvaluator(newBlock.BlockHeader, 0, 0)
+ require.NoError(t, err)
+
+ // won't work before funding bank
+ if poolBonus > 0 {
+ _, err = eval.workaroundOverspentRewards(rewardPoolBalance, headerRound)
+ require.Error(t, err)
+ }
+
+ bankAddr, _ := basics.UnmarshalChecksumAddress("GD64YIY3TWGDMCNPP553DZPPR6LDUSFQOIJVFDPPXWEG3FVOJCCDBBHU5A")
+
+ // put some algos in the bank so that fixup can pull from this account
+ txn := transactions.Transaction{
+ Type: protocol.PaymentTx,
+ Header: transactions.Header{
+ Sender: addrs[0],
+ Fee: minFee,
+ FirstValid: newBlock.Round(),
+ LastValid: newBlock.Round(),
+ GenesisHash: testnetGenesisHash,
+ },
+ PaymentTxnFields: transactions.PaymentTxnFields{
+ Receiver: bankAddr,
+ Amount: basics.MicroAlgos{Raw: 20000000000 * 10},
+ },
+ }
+ st := txn.Sign(keys[0])
+ err = eval.Transaction(st, transactions.ApplyData{})
+ require.NoError(t, err)
+
+ poolOld, err := eval.workaroundOverspentRewards(rewardPoolBalance, headerRound)
+ require.Equal(t, nextPoolBalance, poolOld.MicroAlgos.Raw)
+ require.NoError(t, err)
+}
+
+// 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() (bookkeeping.GenesisBalances, []basics.Address, []*crypto.SignatureSecrets) {
+ // irrelevant, but deterministic
+ sink, err := basics.UnmarshalChecksumAddress("YTPRLJ2KK2JRFSZZNAF57F3K5Y2KCG36FZ5OSYLW776JJGAUW5JXJBBD7Q")
+ if err != nil {
+ panic(err)
+ }
+ rewards, err := basics.UnmarshalChecksumAddress("242H5OXHUEBYCGGWB3CQ6AZAMQB5TMCWJGHCGQOZPEIVQJKOO7NZXUXDQA")
+ if err != nil {
+ panic(err)
+ }
+
+ const count = 10
+ addrs := make([]basics.Address, count)
+ secrets := make([]*crypto.SignatureSecrets, count)
+ accts := make(map[basics.Address]basics.AccountData)
+
+ // 10 billion microalgos, across N accounts and pool and sink
+ amount := 10 * 1000000000 * 1000000 / uint64(count+2)
+
+ for i := 0; i < count; i++ {
+ // Create deterministic addresses, so that output stays the same, run to run.
+ var seed crypto.Seed
+ seed[0] = byte(i)
+ secrets[i] = crypto.GenerateSignatureSecrets(seed)
+ addrs[i] = basics.Address(secrets[i].SignatureVerifier)
+
+ adata := basics.AccountData{
+ MicroAlgos: basics.MicroAlgos{Raw: amount},
+ }
+ accts[addrs[i]] = adata
+ }
+
+ accts[sink] = basics.AccountData{
+ MicroAlgos: basics.MicroAlgos{Raw: amount},
+ Status: basics.NotParticipating,
+ }
+
+ accts[rewards] = basics.AccountData{
+ MicroAlgos: basics.MicroAlgos{Raw: amount},
+ }
+
+ genBalances := bookkeeping.MakeGenesisBalances(accts, sink, rewards)
+
+ return genBalances, addrs, secrets
+}
+
+type evalTestLedger struct {
+ blocks map[basics.Round]bookkeeping.Block
+ roundBalances map[basics.Round]map[basics.Address]basics.AccountData
+ genesisHash crypto.Digest
+ feeSink basics.Address
+ rewardsPool basics.Address
+ latestTotals ledgercore.AccountTotals
+}
+
+// newTestLedger creates a in memory Ledger that is as realistic as
+// possible. It has Rewards and FeeSink properly configured.
+func newTestLedger(t testing.TB, balances bookkeeping.GenesisBalances) *evalTestLedger {
+ l := &evalTestLedger{
+ blocks: make(map[basics.Round]bookkeeping.Block),
+ roundBalances: make(map[basics.Round]map[basics.Address]basics.AccountData),
+ feeSink: balances.FeeSink,
+ rewardsPool: balances.RewardsPool,
+ }
+
+ crypto.RandBytes(l.genesisHash[:])
+ genBlock, err := bookkeeping.MakeGenesisBlock(protocol.ConsensusFuture,
+ balances, "test", l.genesisHash)
+ require.NoError(t, err)
+ l.roundBalances[0] = balances.Balances
+ l.blocks[0] = genBlock
+
+ // calculate the accounts totals.
+ var ot basics.OverflowTracker
+ proto := config.Consensus[protocol.ConsensusCurrentVersion]
+ for _, acctData := range balances.Balances {
+ l.latestTotals.AddAccount(proto, acctData, &ot)
+ }
+
+ require.False(t, genBlock.FeeSink.IsZero())
+ require.False(t, genBlock.RewardsPool.IsZero())
+ return l
+}
+
+// Validate uses the ledger to validate block blk as a candidate next block.
+// It returns an error if blk is not the expected next block, or if blk is
+// not a valid block (e.g., it has duplicate transactions, overspends some
+// account, etc).
+func (ledger *evalTestLedger) Validate(ctx context.Context, blk bookkeeping.Block, executionPool execpool.BacklogPool) (*ledgercore.ValidatedBlock, error) {
+ verifiedTxnCache := verify.MakeVerifiedTransactionCache(config.GetDefaultLocal().VerifiedTranscationsCacheSize)
+
+ delta, err := Eval(ctx, ledger, blk, true, verifiedTxnCache, executionPool)
+ if err != nil {
+ return nil, err
+ }
+
+ vb := ledgercore.MakeValidatedBlock(blk, delta)
+ return &vb, nil
+}
+
+// StartEvaluator creates a BlockEvaluator, given a ledger and a block header
+// of the block that the caller is planning to evaluate. If the length of the
+// payset being evaluated is known in advance, a paysetHint >= 0 can be
+// passed, avoiding unnecessary payset slice growth.
+func (ledger *evalTestLedger) StartEvaluator(hdr bookkeeping.BlockHeader, paysetHint, maxTxnBytesPerBlock int) (*BlockEvaluator, error) {
+ return StartEvaluator(ledger, hdr,
+ EvaluatorOptions{
+ PaysetHint: paysetHint,
+ Validate: true,
+ Generate: true,
+ MaxTxnBytesPerBlock: maxTxnBytesPerBlock,
+ })
+}
+
+// GetCreatorForRound takes a CreatableIndex and a CreatableType and tries to
+// look up a creator address, setting ok to false if the query succeeded but no
+// creator was found.
+func (ledger *evalTestLedger) GetCreatorForRound(rnd basics.Round, cidx basics.CreatableIndex, ctype basics.CreatableType) (creator basics.Address, ok bool, err error) {
+ balances := ledger.roundBalances[rnd]
+ for addr, balance := range balances {
+ if _, has := balance.AssetParams[basics.AssetIndex(cidx)]; has {
+ return addr, true, nil
+ }
+ if _, has := balance.AppParams[basics.AppIndex(cidx)]; has {
+ return addr, true, nil
+ }
+ }
+ return basics.Address{}, false, nil
+}
+
+// LatestTotals returns the totals of all accounts for the most recent round, as well as the round number.
+func (ledger *evalTestLedger) LatestTotals() (basics.Round, ledgercore.AccountTotals, error) {
+ return basics.Round(len(ledger.blocks)).SubSaturate(1), ledger.latestTotals, nil
+}
+
+// LookupWithoutRewards is like Lookup but does not apply pending rewards up
+// to the requested round rnd.
+func (ledger *evalTestLedger) LookupWithoutRewards(rnd basics.Round, addr basics.Address) (basics.AccountData, basics.Round, error) {
+ return ledger.roundBalances[rnd][addr], rnd, nil
+}
+
+// GenesisHash returns the genesis hash for this ledger.
+func (ledger *evalTestLedger) GenesisHash() crypto.Digest {
+ return ledger.genesisHash
+}
+
+// Latest returns the latest known block round added to the ledger.
+func (ledger *evalTestLedger) Latest() basics.Round {
+ return basics.Round(len(ledger.blocks)).SubSaturate(1)
+}
+
+// AddValidatedBlock adds a new block to the ledger, after the block has
+// been validated by calling Ledger.Validate(). This saves the cost of
+// having to re-compute the effect of the block on the ledger state, if
+// the block has previously been validated. Otherwise, AddValidatedBlock
+// behaves like AddBlock.
+func (ledger *evalTestLedger) AddValidatedBlock(vb ledgercore.ValidatedBlock, cert agreement.Certificate) error {
+ blk := vb.Block()
+ ledger.blocks[blk.Round()] = blk
+ newBalances := make(map[basics.Address]basics.AccountData)
+
+ // copy the previous balances.
+ for k, v := range ledger.roundBalances[vb.Block().Round()-1] {
+ newBalances[k] = v
+ }
+ // update
+ deltas := vb.Delta()
+ for _, addr := range deltas.Accts.ModifiedAccounts() {
+ accountData, _ := deltas.Accts.Get(addr)
+ newBalances[addr] = accountData
+ }
+ ledger.roundBalances[vb.Block().Round()] = newBalances
+ ledger.latestTotals = vb.Delta().Totals
+ return nil
+}
+
+// Lookup uses the accounts tracker to return the account state for a
+// given account in a particular round. The account values reflect
+// the changes of all blocks up to and including rnd.
+func (ledger *evalTestLedger) Lookup(rnd basics.Round, addr basics.Address) (basics.AccountData, error) {
+ balances, has := ledger.roundBalances[rnd]
+ if !has {
+ return basics.AccountData{}, errors.New("invalid round specified")
+ }
+
+ return balances[addr], nil
+}
+func (ledger *evalTestLedger) BlockHdr(rnd basics.Round) (bookkeeping.BlockHeader, error) {
+ block, has := ledger.blocks[rnd]
+ if !has {
+ return bookkeeping.BlockHeader{}, errors.New("invalid round specified")
+ }
+ return block.BlockHeader, nil
+}
+
+func (ledger *evalTestLedger) CompactCertVoters(rnd basics.Round) (*ledgercore.VotersForRound, error) {
+ return nil, errors.New("untested code path")
+}
+
+// GetCreator is like GetCreatorForRound, but for the latest round and race-free
+// with respect to ledger.Latest()
+func (ledger *evalTestLedger) GetCreator(cidx basics.CreatableIndex, ctype basics.CreatableType) (basics.Address, bool, error) {
+ latestRound := ledger.Latest()
+ return ledger.GetCreatorForRound(latestRound, cidx, ctype)
+}
+
+func (ledger *evalTestLedger) CheckDup(currentProto config.ConsensusParams, current basics.Round, firstValid basics.Round, lastValid basics.Round, txid transactions.Txid, txl ledgercore.Txlease) error {
+ for _, block := range ledger.blocks {
+ for _, txn := range block.Payset {
+ if lastValid != txn.Txn.LastValid {
+ continue
+ }
+ currentTxid := txn.Txn.ID()
+ if bytes.Equal(txid[:], currentTxid[:]) {
+ return &ledgercore.TransactionInLedgerError{Txid: txid}
+ }
+ }
+ }
+ // todo - support leases.
+ return nil
+}
+
+// nextBlock begins evaluation of a new block, after ledger creation or endBlock()
+func (ledger *evalTestLedger) nextBlock(t testing.TB) *BlockEvaluator {
+ rnd := ledger.Latest()
+ hdr, err := ledger.BlockHdr(rnd)
+ require.NoError(t, err)
+
+ nextHdr := bookkeeping.MakeBlock(hdr).BlockHeader
+ eval, err := ledger.StartEvaluator(nextHdr, 0, 0)
+ require.NoError(t, err)
+ return eval
+}
+
+// endBlock completes the block being created, returns the ValidatedBlock for inspection
+func (ledger *evalTestLedger) endBlock(t testing.TB, eval *BlockEvaluator) *ledgercore.ValidatedBlock {
+ validatedBlock, err := eval.GenerateBlock()
+ require.NoError(t, err)
+ err = ledger.AddValidatedBlock(*validatedBlock, agreement.Certificate{})
+ require.NoError(t, err)
+ return validatedBlock
+}
+
+// lookup gets the current accountdata for an address
+func (ledger *evalTestLedger) lookup(t testing.TB, addr basics.Address) basics.AccountData {
+ rnd := ledger.Latest()
+ ad, err := ledger.Lookup(rnd, addr)
+ require.NoError(t, err)
+ return ad
+}
+
+// micros gets the current microAlgo balance for an address
+func (ledger *evalTestLedger) micros(t testing.TB, addr basics.Address) uint64 {
+ return ledger.lookup(t, addr).MicroAlgos.Raw
+}
+
+// asa gets the current balance and optin status for some asa for an address
+func (ledger *evalTestLedger) asa(t testing.TB, addr basics.Address, asset basics.AssetIndex) (uint64, bool) {
+ if holding, ok := ledger.lookup(t, addr).Assets[asset]; ok {
+ return holding.Amount, true
+ }
+ return 0, false
+}
+
+// asaParams gets the asset params for a given asa index
+func (ledger *evalTestLedger) asaParams(t testing.TB, asset basics.AssetIndex) (basics.AssetParams, error) {
+ creator, ok, err := ledger.GetCreator(basics.CreatableIndex(asset), basics.AssetCreatable)
+ if err != nil {
+ return basics.AssetParams{}, err
+ }
+ if !ok {
+ return basics.AssetParams{}, fmt.Errorf("no asset (%d)", asset)
+ }
+ if params, ok := ledger.lookup(t, creator).AssetParams[asset]; ok {
+ return params, nil
+ }
+ return basics.AssetParams{}, fmt.Errorf("bad lookup (%d)", asset)
+}
+
+type getCreatorForRoundResult struct {
+ address basics.Address
+ exists bool
+}
+
+type testCowBaseLedger struct {
+ creators []getCreatorForRoundResult
+}
+
+func (l *testCowBaseLedger) BlockHdr(basics.Round) (bookkeeping.BlockHeader, error) {
+ return bookkeeping.BlockHeader{}, errors.New("not implemented")
+}
+
+func (l *testCowBaseLedger) CheckDup(config.ConsensusParams, basics.Round, basics.Round, basics.Round, transactions.Txid, ledgercore.Txlease) error {
+ return errors.New("not implemented")
+}
+
+func (l *testCowBaseLedger) LookupWithoutRewards(basics.Round, basics.Address) (basics.AccountData, basics.Round, error) {
+ return basics.AccountData{}, basics.Round(0), errors.New("not implemented")
+}
+
+func (l *testCowBaseLedger) GetCreatorForRound(_ basics.Round, cindex basics.CreatableIndex, ctype basics.CreatableType) (basics.Address, bool, error) {
+ res := l.creators[0]
+ l.creators = l.creators[1:]
+ return res.address, res.exists, nil
+}
+
+func TestCowBaseCreatorsCache(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ addresses := make([]basics.Address, 3)
+ for i := 0; i < len(addresses); i++ {
+ _, err := rand.Read(addresses[i][:])
+ require.NoError(t, err)
+ }
+
+ creators := []getCreatorForRoundResult{
+ {address: addresses[0], exists: true},
+ {address: basics.Address{}, exists: false},
+ {address: addresses[1], exists: true},
+ {address: basics.Address{}, exists: false},
+ }
+ l := testCowBaseLedger{
+ creators: creators,
+ }
+
+ base := roundCowBase{
+ l: &l,
+ creators: map[creatable]foundAddress{},
+ }
+
+ cindex := []basics.CreatableIndex{9, 10, 9, 10}
+ ctype := []basics.CreatableType{
+ basics.AssetCreatable,
+ basics.AssetCreatable,
+ basics.AppCreatable,
+ basics.AppCreatable,
+ }
+ for i := 0; i < 2; i++ {
+ for j, expected := range creators {
+ address, exists, err := base.getCreator(cindex[j], ctype[j])
+ require.NoError(t, err)
+
+ assert.Equal(t, expected.address, address)
+ assert.Equal(t, expected.exists, exists)
+ }
+ }
+}
+
+// TestEvalFunctionForExpiredAccounts tests that the eval function will correctly mark accounts as offline
+func TestEvalFunctionForExpiredAccounts(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ genesisInitState, addrs, keys := ledgertesting.GenesisWithProto(10, protocol.ConsensusFuture)
+
+ sendAddr := addrs[0]
+ recvAddr := addrs[1]
+
+ // the last round that the recvAddr is valid for
+ recvAddrLastValidRound := basics.Round(2)
+
+ // the target round we want to advance the evaluator to
+ targetRound := basics.Round(4)
+
+ // Set all to online except the sending address
+ for _, addr := range addrs {
+ if addr == sendAddr {
+ continue
+ }
+ tmp := genesisInitState.Accounts[addr]
+ tmp.Status = basics.Online
+ genesisInitState.Accounts[addr] = tmp
+ }
+
+ // Choose recvAddr to have a last valid round less than genesis block round
+ {
+ tmp := genesisInitState.Accounts[recvAddr]
+ tmp.VoteLastValid = recvAddrLastValidRound
+ genesisInitState.Accounts[recvAddr] = tmp
+ }
+
+ genesisBalances := bookkeeping.GenesisBalances{
+ Balances: genesisInitState.Accounts,
+ FeeSink: testSinkAddr,
+ RewardsPool: testPoolAddr,
+ Timestamp: 0,
+ }
+ l := newTestLedger(t, genesisBalances)
+
+ newBlock := bookkeeping.MakeBlock(l.blocks[0].BlockHeader)
+
+ blkEval, err := l.StartEvaluator(newBlock.BlockHeader, 0, 0)
+ require.NoError(t, err)
+
+ // Advance the evaluator a couple rounds...
+ for i := uint64(0); i < uint64(targetRound); i++ {
+ l.endBlock(t, blkEval)
+ blkEval = l.nextBlock(t)
+ }
+
+ require.Greater(t, uint64(blkEval.Round()), uint64(recvAddrLastValidRound))
+
+ genHash := l.GenesisHash()
+ txn := transactions.Transaction{
+ Type: protocol.PaymentTx,
+ Header: transactions.Header{
+ Sender: sendAddr,
+ Fee: minFee,
+ FirstValid: newBlock.Round(),
+ LastValid: blkEval.Round(),
+ GenesisHash: genHash,
+ },
+ PaymentTxnFields: transactions.PaymentTxnFields{
+ Receiver: recvAddr,
+ Amount: basics.MicroAlgos{Raw: 100},
+ },
+ }
+
+ st := txn.Sign(keys[0])
+ err = blkEval.Transaction(st, transactions.ApplyData{})
+ require.NoError(t, err)
+
+ // Make sure we validate our block as well
+ blkEval.validate = true
+
+ validatedBlock, err := blkEval.GenerateBlock()
+ require.NoError(t, err)
+
+ _, err = Eval(context.Background(), l, validatedBlock.Block(), false, nil, nil)
+ require.NoError(t, err)
+
+ badBlock := *validatedBlock
+
+ // First validate that bad block is fine if we dont touch it...
+ _, err = Eval(context.Background(), l, badBlock.Block(), true, verify.GetMockedCache(true), nil)
+ require.NoError(t, err)
+
+ badBlock = *validatedBlock
+
+ // Introduce an unknown address to introduce an error
+ badBlockObj := badBlock.Block()
+ badBlockObj.ExpiredParticipationAccounts = append(badBlockObj.ExpiredParticipationAccounts, basics.Address{1})
+ badBlock = ledgercore.MakeValidatedBlock(badBlockObj, badBlock.Delta())
+
+ _, err = Eval(context.Background(), l, badBlock.Block(), true, verify.GetMockedCache(true), nil)
+ require.Error(t, err)
+
+ badBlock = *validatedBlock
+
+ addressToCopy := badBlock.Block().ExpiredParticipationAccounts[0]
+
+ // Add more than the expected number of accounts
+ badBlockObj = badBlock.Block()
+ for i := 0; i < blkEval.proto.MaxProposedExpiredOnlineAccounts+1; i++ {
+ badBlockObj.ExpiredParticipationAccounts = append(badBlockObj.ExpiredParticipationAccounts, addressToCopy)
+ }
+ badBlock = ledgercore.MakeValidatedBlock(badBlockObj, badBlock.Delta())
+
+ _, err = Eval(context.Background(), l, badBlock.Block(), true, verify.GetMockedCache(true), nil)
+ require.Error(t, err)
+
+ badBlock = *validatedBlock
+
+ // Duplicate an address
+ badBlockObj = badBlock.Block()
+ badBlockObj.ExpiredParticipationAccounts = append(badBlockObj.ExpiredParticipationAccounts, badBlockObj.ExpiredParticipationAccounts[0])
+ badBlock = ledgercore.MakeValidatedBlock(badBlockObj, badBlock.Delta())
+
+ _, err = Eval(context.Background(), l, badBlock.Block(), true, verify.GetMockedCache(true), nil)
+ require.Error(t, err)
+
+ badBlock = *validatedBlock
+ // sanity check that bad block is being actually copied and not just the pointer
+ _, err = Eval(context.Background(), l, badBlock.Block(), true, verify.GetMockedCache(true), nil)
+ require.NoError(t, err)
+
+}
+
+type failRoundCowParent struct {
+ roundCowBase
+}
+
+func (p *failRoundCowParent) lookup(basics.Address) (basics.AccountData, error) {
+ return basics.AccountData{}, fmt.Errorf("disk I/O fail (on purpose)")
+}
+
+// TestExpiredAccountGenerationWithDiskFailure tests edge cases where disk failures can lead to ledger look up failures
+func TestExpiredAccountGenerationWithDiskFailure(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ genesisInitState, addrs, keys := ledgertesting.GenesisWithProto(10, protocol.ConsensusFuture)
+
+ sendAddr := addrs[0]
+ recvAddr := addrs[1]
+
+ // the last round that the recvAddr is valid for
+ recvAddrLastValidRound := basics.Round(10)
+
+ // the target round we want to advance the evaluator to
+ targetRound := basics.Round(4)
+
+ // Set all to online except the sending address
+ for _, addr := range addrs {
+ if addr == sendAddr {
+ continue
+ }
+ tmp := genesisInitState.Accounts[addr]
+ tmp.Status = basics.Online
+ genesisInitState.Accounts[addr] = tmp
+ }
+
+ // Choose recvAddr to have a last valid round less than genesis block round
+ {
+ tmp := genesisInitState.Accounts[recvAddr]
+ tmp.VoteLastValid = recvAddrLastValidRound
+ genesisInitState.Accounts[recvAddr] = tmp
+ }
+
+ l := newTestLedger(t, bookkeeping.GenesisBalances{
+ Balances: genesisInitState.Accounts,
+ FeeSink: testSinkAddr,
+ RewardsPool: testPoolAddr,
+ Timestamp: 0,
+ })
+
+ newBlock := bookkeeping.MakeBlock(l.blocks[0].BlockHeader)
+
+ eval, err := l.StartEvaluator(newBlock.BlockHeader, 0, 0)
+ require.NoError(t, err)
+
+ // Advance the evaluator a couple rounds...
+ for i := uint64(0); i < uint64(targetRound); i++ {
+ l.endBlock(t, eval)
+ 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
+
+ eval.block.ExpiredParticipationAccounts = append(eval.block.ExpiredParticipationAccounts, recvAddr)
+
+ err = eval.endOfBlock()
+ require.Error(t, err)
+
+ eval.block.ExpiredParticipationAccounts = []basics.Address{
+ basics.Address{},
+ }
+ eval.state.mods.Accts = ledgercore.AccountDeltas{}
+ eval.state.lookupParent = &failRoundCowParent{}
+ err = eval.endOfBlock()
+ require.Error(t, err)
+
+ err = eval.resetExpiredOnlineAccountsParticipationKeys()
+ require.Error(t, err)
+
+}
+
+// TestExpiredAccountGeneration test that expired accounts are added to a block header and validated
+func TestExpiredAccountGeneration(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ genesisInitState, addrs, keys := ledgertesting.GenesisWithProto(10, protocol.ConsensusFuture)
+
+ sendAddr := addrs[0]
+ recvAddr := addrs[1]
+
+ // the last round that the recvAddr is valid for
+ recvAddrLastValidRound := basics.Round(2)
+
+ // the target round we want to advance the evaluator to
+ targetRound := basics.Round(4)
+
+ // Set all to online except the sending address
+ for _, addr := range addrs {
+ if addr == sendAddr {
+ continue
+ }
+ tmp := genesisInitState.Accounts[addr]
+ tmp.Status = basics.Online
+ genesisInitState.Accounts[addr] = tmp
+ }
+
+ // Choose recvAddr to have a last valid round less than genesis block round
+ {
+ tmp := genesisInitState.Accounts[recvAddr]
+ tmp.VoteLastValid = recvAddrLastValidRound
+ genesisInitState.Accounts[recvAddr] = tmp
+ }
+
+ l := newTestLedger(t, bookkeeping.GenesisBalances{
+ Balances: genesisInitState.Accounts,
+ FeeSink: testSinkAddr,
+ RewardsPool: testPoolAddr,
+ Timestamp: 0,
+ })
+
+ newBlock := bookkeeping.MakeBlock(l.blocks[0].BlockHeader)
+
+ eval, err := l.StartEvaluator(newBlock.BlockHeader, 0, 0)
+ require.NoError(t, err)
+
+ // Advance the evaluator a couple rounds...
+ for i := uint64(0); i < uint64(targetRound); i++ {
+ 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},
+ },
+ }
+
+ st := txn.Sign(keys[0])
+ err = eval.Transaction(st, transactions.ApplyData{})
+ require.NoError(t, err)
+
+ // Make sure we validate our block as well
+ eval.validate = true
+
+ validatedBlock, err := eval.GenerateBlock()
+ require.NoError(t, err)
+
+ listOfExpiredAccounts := validatedBlock.Block().ParticipationUpdates.ExpiredParticipationAccounts
+
+ require.Equal(t, 1, len(listOfExpiredAccounts))
+ expiredAccount := listOfExpiredAccounts[0]
+ require.Equal(t, expiredAccount, recvAddr)
+
+ recvAcct, err := eval.state.lookup(recvAddr)
+ require.NoError(t, err)
+ require.Equal(t, recvAcct.Status, basics.Offline)
+ require.Equal(t, recvAcct.VoteFirstValid, basics.Round(0))
+ require.Equal(t, recvAcct.VoteLastValid, basics.Round(0))
+ require.Equal(t, recvAcct.VoteKeyDilution, uint64(0))
+ require.Equal(t, recvAcct.VoteID, crypto.OneTimeSignatureVerifier{})
+ require.Equal(t, recvAcct.SelectionID, crypto.VRFVerifier{})
+
+}