diff options
Diffstat (limited to 'ledger/eval_test.go')
-rw-r--r-- | ledger/eval_test.go | 1934 |
1 files changed, 0 insertions, 1934 deletions
diff --git a/ledger/eval_test.go b/ledger/eval_test.go deleted file mode 100644 index 07ba2d099..000000000 --- a/ledger/eval_test.go +++ /dev/null @@ -1,1934 +0,0 @@ -// 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 ledger - -import ( - "context" - "errors" - "fmt" - "math/rand" - "os" - "path/filepath" - "reflect" - "runtime/pprof" - "strings" - "sync" - "testing" - "time" - - "github.com/algorand/go-deadlock" - "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/logic" - "github.com/algorand/go-algorand/data/txntest" - "github.com/algorand/go-algorand/ledger/ledgercore" - "github.com/algorand/go-algorand/logging" - "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 TestBlockEvaluator(t *testing.T) { - partitiontest.PartitionTest(t) - - genesisInitState, addrs, keys := genesis(10) - - dbName := fmt.Sprintf("%s.%d", t.Name(), crypto.RandUint64()) - const inMem = true - cfg := config.GetDefaultLocal() - cfg.Archival = true - l, err := OpenLedger(logging.Base(), dbName, inMem, genesisInitState, cfg) - require.NoError(t, err) - defer l.Close() - - newBlock := bookkeeping.MakeBlock(genesisInitState.Block.BlockHeader) - eval, err := l.StartEvaluator(newBlock.BlockHeader, 0, 0) - require.Equal(t, eval.specials.FeeSink, testSinkAddr) - require.NoError(t, err) - - genHash := genesisInitState.Block.BlockHeader.GenesisHash - txn := transactions.Transaction{ - Type: protocol.PaymentTx, - Header: transactions.Header{ - Sender: addrs[0], - Fee: minFee, - FirstValid: newBlock.Round(), - LastValid: newBlock.Round(), - GenesisHash: genHash, - }, - PaymentTxnFields: transactions.PaymentTxnFields{ - Receiver: addrs[1], - Amount: basics.MicroAlgos{Raw: 100}, - }, - } - - // Correct signature should work - st := txn.Sign(keys[0]) - err = eval.Transaction(st, transactions.ApplyData{}) - require.NoError(t, err) - - // Broken signature should fail - stbad := st - st.Sig[2] ^= 8 - txgroup := []transactions.SignedTxn{stbad} - err = eval.TestTransactionGroup(txgroup) - require.Error(t, err) - - // Repeat should fail - txgroup = []transactions.SignedTxn{st} - err = eval.TestTransactionGroup(txgroup) - require.Error(t, err) - err = eval.Transaction(st, transactions.ApplyData{}) - require.Error(t, err) - - // out of range should fail - btxn := txn - btxn.FirstValid++ - btxn.LastValid += 2 - st = btxn.Sign(keys[0]) - txgroup = []transactions.SignedTxn{st} - err = eval.TestTransactionGroup(txgroup) - require.Error(t, err) - err = eval.Transaction(st, transactions.ApplyData{}) - require.Error(t, err) - - // bogus group should fail - btxn = txn - btxn.Group[1] = 1 - st = btxn.Sign(keys[0]) - txgroup = []transactions.SignedTxn{st} - err = eval.TestTransactionGroup(txgroup) - require.Error(t, err) - err = eval.Transaction(st, transactions.ApplyData{}) - require.Error(t, err) - - // mixed fields should fail - btxn = txn - btxn.XferAsset = 3 - st = btxn.Sign(keys[0]) - txgroup = []transactions.SignedTxn{st} - err = eval.TestTransactionGroup(txgroup) - require.Error(t, err) - // We don't test eval.Transaction() here because it doesn't check txn.WellFormed(), instead relying on that to have already been checked by the transaction pool. - // err = eval.Transaction(st, transactions.ApplyData{}) - // require.Error(t, err) - - selfTxn := transactions.Transaction{ - Type: protocol.PaymentTx, - Header: transactions.Header{ - Sender: addrs[2], - Fee: minFee, - FirstValid: newBlock.Round(), - LastValid: newBlock.Round(), - GenesisHash: genHash, - }, - PaymentTxnFields: transactions.PaymentTxnFields{ - Receiver: addrs[2], - Amount: basics.MicroAlgos{Raw: 100}, - }, - } - stxn := selfTxn.Sign(keys[2]) - - // TestTransactionGroup() and Transaction() should have the same outcome, but work slightly different code paths. - txgroup = []transactions.SignedTxn{stxn} - err = eval.TestTransactionGroup(txgroup) - require.NoError(t, err) - - err = eval.Transaction(stxn, transactions.ApplyData{}) - require.NoError(t, err) - - t3 := txn - t3.Amount.Raw++ - t4 := selfTxn - t4.Amount.Raw++ - - // a group without .Group should fail - s3 := t3.Sign(keys[0]) - s4 := t4.Sign(keys[2]) - txgroup = []transactions.SignedTxn{s3, s4} - err = eval.TestTransactionGroup(txgroup) - require.Error(t, err) - txgroupad := transactions.WrapSignedTxnsWithAD(txgroup) - err = eval.TransactionGroup(txgroupad) - require.Error(t, err) - - // Test a group that should work - var group transactions.TxGroup - group.TxGroupHashes = []crypto.Digest{crypto.HashObj(t3), crypto.HashObj(t4)} - t3.Group = crypto.HashObj(group) - t4.Group = t3.Group - s3 = t3.Sign(keys[0]) - s4 = t4.Sign(keys[2]) - txgroup = []transactions.SignedTxn{s3, s4} - err = eval.TestTransactionGroup(txgroup) - require.NoError(t, err) - - // disagreement on Group id should fail - t4bad := t4 - t4bad.Group[3] ^= 3 - s4bad := t4bad.Sign(keys[2]) - txgroup = []transactions.SignedTxn{s3, s4bad} - err = eval.TestTransactionGroup(txgroup) - require.Error(t, err) - txgroupad = transactions.WrapSignedTxnsWithAD(txgroup) - err = eval.TransactionGroup(txgroupad) - require.Error(t, err) - - // missing part of the group should fail - txgroup = []transactions.SignedTxn{s3} - err = eval.TestTransactionGroup(txgroup) - require.Error(t, err) - - validatedBlock, err := eval.GenerateBlock() - require.NoError(t, err) - - accts := genesisInitState.Accounts - bal0 := accts[addrs[0]] - bal1 := accts[addrs[1]] - bal2 := accts[addrs[2]] - - l.AddValidatedBlock(*validatedBlock, agreement.Certificate{}) - - bal0new, err := l.Lookup(newBlock.Round(), addrs[0]) - require.NoError(t, err) - bal1new, err := l.Lookup(newBlock.Round(), addrs[1]) - require.NoError(t, err) - bal2new, err := l.Lookup(newBlock.Round(), addrs[2]) - require.NoError(t, err) - - require.Equal(t, bal0new.MicroAlgos.Raw, bal0.MicroAlgos.Raw-minFee.Raw-100) - require.Equal(t, bal1new.MicroAlgos.Raw, bal1.MicroAlgos.Raw+100) - require.Equal(t, bal2new.MicroAlgos.Raw, bal2.MicroAlgos.Raw-minFee.Raw) -} - -func TestRekeying(t *testing.T) { - partitiontest.PartitionTest(t) - - // Pretend rekeying is supported - actual := config.Consensus[protocol.ConsensusCurrentVersion] - pretend := actual - pretend.SupportRekeying = true - config.Consensus[protocol.ConsensusCurrentVersion] = pretend - defer func() { - config.Consensus[protocol.ConsensusCurrentVersion] = actual - }() - - // Bring up a ledger - genesisInitState, addrs, keys := genesis(10) - dbName := fmt.Sprintf("%s.%d", t.Name(), crypto.RandUint64()) - const inMem = true - cfg := config.GetDefaultLocal() - cfg.Archival = true - l, err := OpenLedger(logging.Base(), dbName, inMem, genesisInitState, cfg) - require.NoError(t, err) - defer l.Close() - - // Make a new block - nextRound := l.Latest() + basics.Round(1) - genHash := genesisInitState.Block.BlockHeader.GenesisHash - - // Test plan - // Syntax: [A -> B][C, D] means transaction from A that rekeys to B with authaddr C and actual sig from D - makeTxn := func(sender, rekeyto, authaddr basics.Address, signer *crypto.SignatureSecrets, uniq uint8) transactions.SignedTxn { - txn := transactions.Transaction{ - Type: protocol.PaymentTx, - Header: transactions.Header{ - Sender: sender, - Fee: minFee, - FirstValid: nextRound, - LastValid: nextRound, - GenesisHash: genHash, - RekeyTo: rekeyto, - Note: []byte{uniq}, - }, - PaymentTxnFields: transactions.PaymentTxnFields{ - Receiver: sender, - }, - } - sig := signer.Sign(txn) - return transactions.SignedTxn{Txn: txn, Sig: sig, AuthAddr: authaddr} - } - - tryBlock := func(stxns []transactions.SignedTxn) error { - // We'll make a block using the evaluator. - // When generating a block, the evaluator doesn't check transaction sigs -- it assumes the transaction pool already did that. - // So the ValidatedBlock that comes out isn't necessarily actually a valid block. We'll call Validate ourselves. - - newBlock := bookkeeping.MakeBlock(genesisInitState.Block.BlockHeader) - eval, err := l.StartEvaluator(newBlock.BlockHeader, 0, 0) - require.NoError(t, err) - - for _, stxn := range stxns { - err = eval.Transaction(stxn, transactions.ApplyData{}) - if err != nil { - return err - } - } - validatedBlock, err := eval.GenerateBlock() - if err != nil { - return err - } - - backlogPool := execpool.MakeBacklog(nil, 0, execpool.LowPriority, nil) - defer backlogPool.Shutdown() - _, err = l.Validate(context.Background(), validatedBlock.Block(), backlogPool) - return err - } - - // Preamble transactions, which all of the blocks in this test will start with - // [A -> 0][0,A] (normal transaction) - // [A -> B][0,A] (rekey) - txn0 := makeTxn(addrs[0], basics.Address{}, basics.Address{}, keys[0], 0) // Normal transaction - txn1 := makeTxn(addrs[0], addrs[1], basics.Address{}, keys[0], 1) // Rekey transaction - - // Test 1: Do only good things - // (preamble) - // [A -> 0][B,B] (normal transaction using new key) - // [A -> A][B,B] (rekey back to A, transaction still signed by B) - // [A -> 0][0,A] (normal transaction again) - test1txns := []transactions.SignedTxn{ - txn0, txn1, // (preamble) - makeTxn(addrs[0], basics.Address{}, addrs[1], keys[1], 2), // [A -> 0][B,B] - makeTxn(addrs[0], addrs[0], addrs[1], keys[1], 3), // [A -> A][B,B] - makeTxn(addrs[0], basics.Address{}, basics.Address{}, keys[0], 4), // [A -> 0][0,A] - } - err = tryBlock(test1txns) - require.NoError(t, err) - - // Test 2: Use old key after rekeying - // (preamble) - // [A -> A][0,A] (rekey back to A, but signed by A instead of B) - test2txns := []transactions.SignedTxn{ - txn0, txn1, // (preamble) - makeTxn(addrs[0], addrs[0], basics.Address{}, keys[0], 2), // [A -> A][0,A] - } - err = tryBlock(test2txns) - require.Error(t, err) - - // TODO: More tests -} - -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 testLedgerCleanup(l *Ledger, dbName string, inMem bool) { - l.Close() - if !inMem { - hits, err := filepath.Glob(dbName + "*.sqlite") - if err != nil { - return - } - for _, fname := range hits { - os.Remove(fname) - } - } -} - -func testEvalAppGroup(t *testing.T, schema basics.StateSchema) (*BlockEvaluator, basics.Address, error) { - genesisInitState, addrs, keys := genesis(10) - - dbName := fmt.Sprintf("%s.%d", t.Name(), crypto.RandUint64()) - const inMem = true - cfg := config.GetDefaultLocal() - l, err := OpenLedger(logging.Base(), dbName, inMem, genesisInitState, cfg) - require.NoError(t, err) - defer l.Close() - - newBlock := bookkeeping.MakeBlock(genesisInitState.Block.BlockHeader) - eval, err := l.StartEvaluator(newBlock.BlockHeader, 0, 0) - require.NoError(t, err) - eval.validate = true - eval.generate = false - - ops, err := logic.AssembleString(`#pragma version 2 - txn ApplicationID - bz create - byte "caller" - txn Sender - app_global_put - b ok -create: - byte "creator" - txn Sender - app_global_put -ok: - int 1`) - require.NoError(t, err, ops.Errors) - approval := ops.Program - ops, err = logic.AssembleString("#pragma version 2\nint 1") - require.NoError(t, err) - clear := ops.Program - - genHash := genesisInitState.Block.BlockHeader.GenesisHash - header := transactions.Header{ - Sender: addrs[0], - Fee: minFee, - FirstValid: newBlock.Round(), - LastValid: newBlock.Round(), - GenesisHash: genHash, - } - appcall1 := transactions.Transaction{ - Type: protocol.ApplicationCallTx, - Header: header, - ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ - GlobalStateSchema: schema, - ApprovalProgram: approval, - ClearStateProgram: clear, - }, - } - - appcall2 := transactions.Transaction{ - Type: protocol.ApplicationCallTx, - Header: header, - ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ - ApplicationID: 1, - }, - } - - var group transactions.TxGroup - group.TxGroupHashes = []crypto.Digest{crypto.HashObj(appcall1), crypto.HashObj(appcall2)} - appcall1.Group = crypto.HashObj(group) - appcall2.Group = crypto.HashObj(group) - stxn1 := appcall1.Sign(keys[0]) - stxn2 := appcall2.Sign(keys[0]) - - g := []transactions.SignedTxnWithAD{ - { - SignedTxn: stxn1, - ApplyData: transactions.ApplyData{ - EvalDelta: transactions.EvalDelta{GlobalDelta: map[string]basics.ValueDelta{ - "creator": {Action: basics.SetBytesAction, Bytes: string(addrs[0][:])}}, - }, - ApplicationID: 1, - }, - }, - { - SignedTxn: stxn2, - ApplyData: transactions.ApplyData{ - EvalDelta: transactions.EvalDelta{GlobalDelta: map[string]basics.ValueDelta{ - "caller": {Action: basics.SetBytesAction, Bytes: string(addrs[0][:])}}, - }}, - }, - } - txgroup := []transactions.SignedTxn{stxn1, stxn2} - err = eval.TestTransactionGroup(txgroup) - if err != nil { - return eval, addrs[0], err - } - err = eval.transactionGroup(g) - return eval, addrs[0], err -} - -// TestEvalAppStateCountsWithTxnGroup ensures txns in a group can't violate app state schema limits -// the test ensures that -// commitToParent -> applyChild copies child's cow state usage counts into parent -// and the usage counts correctly propagated from parent cow to child cow and back -func TestEvalAppStateCountsWithTxnGroup(t *testing.T) { - partitiontest.PartitionTest(t) - - _, _, err := testEvalAppGroup(t, basics.StateSchema{NumByteSlice: 1}) - require.Error(t, err) - require.Contains(t, err.Error(), "store bytes count 2 exceeds schema bytes count 1") -} - -// TestEvalAppAllocStateWithTxnGroup ensures roundCowState.deltas and applyStorageDelta -// produce correct results when a txn group has storage allocate and storage update actions -func TestEvalAppAllocStateWithTxnGroup(t *testing.T) { - partitiontest.PartitionTest(t) - - eval, addr, err := testEvalAppGroup(t, basics.StateSchema{NumByteSlice: 2}) - require.NoError(t, err) - deltas := eval.state.deltas() - ad, _ := deltas.Accts.Get(addr) - state := ad.AppParams[1].GlobalState - require.Equal(t, basics.TealValue{Type: basics.TealBytesType, Bytes: string(addr[:])}, state["caller"]) - require.Equal(t, basics.TealValue{Type: basics.TealBytesType, Bytes: string(addr[:])}, state["creator"]) -} - -func testEvalAppPoolingGroup(t *testing.T, schema basics.StateSchema, approvalProgram string, consensusVersion protocol.ConsensusVersion) error { - genBalances, addrs, _ := newTestGenesis() - l := newTestLedger(t, genBalances) - defer l.Close() - - eval := l.nextBlock(t) - eval.validate = true - eval.generate = false - - eval.proto = config.Consensus[consensusVersion] - - appcall1 := txntest.Txn{ - Sender: addrs[0], - Type: protocol.ApplicationCallTx, - GlobalStateSchema: schema, - ApprovalProgram: approvalProgram, - } - - appcall2 := txntest.Txn{ - Sender: addrs[0], - Type: protocol.ApplicationCallTx, - ApplicationID: basics.AppIndex(1), - } - - appcall3 := txntest.Txn{ - Sender: addrs[1], - Type: protocol.ApplicationCallTx, - ApplicationID: basics.AppIndex(1), - } - - return eval.txgroup(t, &appcall1, &appcall2, &appcall3) -} - -// TestEvalAppPooledBudgetWithTxnGroup ensures 3 app call txns can successfully pool -// budgets in a group txn and return an error if the budget is exceeded -func TestEvalAppPooledBudgetWithTxnGroup(t *testing.T) { - partitiontest.PartitionTest(t) - - source := func(n int, m int) string { - return "#pragma version 4\nbyte 0x1337BEEF\n" + strings.Repeat("keccak256\n", n) + - strings.Repeat("substring 0 4\n", m) + "pop\nint 1\n" - } - - params := []protocol.ConsensusVersion{ - protocol.ConsensusV29, - protocol.ConsensusFuture, - } - - cases := []struct { - prog string - isSuccessV29 bool - isSuccessVFuture bool - expectedErrorV29 string - expectedErrorVFuture string - }{ - {source(5, 47), true, true, - "", - ""}, - {source(5, 48), false, true, - "pc=157 dynamic cost budget exceeded, executing pushint: remaining budget is 700 but program cost was 701", - ""}, - {source(16, 17), false, true, - "pc= 12 dynamic cost budget exceeded, executing keccak256: remaining budget is 700 but program cost was 781", - ""}, - {source(16, 18), false, false, - "pc= 12 dynamic cost budget exceeded, executing keccak256: remaining budget is 700 but program cost was 781", - "pc= 78 dynamic cost budget exceeded, executing pushint: remaining budget is 2100 but program cost was 2101"}, - } - - for i, param := range params { - for j, testCase := range cases { - t.Run(fmt.Sprintf("i=%d,j=%d", i, j), func(t *testing.T) { - err := testEvalAppPoolingGroup(t, basics.StateSchema{NumByteSlice: 3}, testCase.prog, param) - if !testCase.isSuccessV29 && reflect.DeepEqual(param, protocol.ConsensusV29) { - require.Error(t, err) - require.Contains(t, err.Error(), testCase.expectedErrorV29) - } else if !testCase.isSuccessVFuture && reflect.DeepEqual(param, protocol.ConsensusFuture) { - require.Error(t, err) - require.Contains(t, err.Error(), testCase.expectedErrorVFuture) - } - }) - } - } -} - -// BenchTxnGenerator generates transactions as long as asked for -type BenchTxnGenerator interface { - // Prepare should be used for making pre-benchmark ledger initialization - // like accounts funding, assets or apps creation - Prepare(tb testing.TB, addrs []basics.Address, keys []*crypto.SignatureSecrets, rnd basics.Round, gh crypto.Digest) ([]transactions.SignedTxn, int) - // Txn generates a single transaction - Txn(tb testing.TB, addrs []basics.Address, keys []*crypto.SignatureSecrets, rnd basics.Round, gh crypto.Digest) transactions.SignedTxn -} - -// BenchPaymentTxnGenerator generates payment transactions -type BenchPaymentTxnGenerator struct { - counter int -} - -func (g *BenchPaymentTxnGenerator) Prepare(tb testing.TB, addrs []basics.Address, keys []*crypto.SignatureSecrets, rnd basics.Round, gh crypto.Digest) ([]transactions.SignedTxn, int) { - return nil, 0 -} - -func (g *BenchPaymentTxnGenerator) Txn(tb testing.TB, addrs []basics.Address, keys []*crypto.SignatureSecrets, rnd basics.Round, gh crypto.Digest) transactions.SignedTxn { - sender := g.counter % len(addrs) - receiver := (g.counter + 1) % len(addrs) - // The following would create more random selection of accounts, and prevent a cache of half of the accounts.. - // iDigest := crypto.Hash([]byte{byte(i), byte(i >> 8), byte(i >> 16), byte(i >> 24)}) - // sender := (uint64(iDigest[0]) + uint64(iDigest[1])*256 + uint64(iDigest[2])*256*256) % uint64(len(addrs)) - // receiver := (uint64(iDigest[4]) + uint64(iDigest[5])*256 + uint64(iDigest[6])*256*256) % uint64(len(addrs)) - - txn := transactions.Transaction{ - Type: protocol.PaymentTx, - Header: transactions.Header{ - Sender: addrs[sender], - Fee: minFee, - FirstValid: rnd, - LastValid: rnd, - GenesisHash: gh, - }, - PaymentTxnFields: transactions.PaymentTxnFields{ - Receiver: addrs[receiver], - Amount: basics.MicroAlgos{Raw: 100}, - }, - } - stxn := txn.Sign(keys[sender]) - g.counter++ - return stxn -} - -// BenchAppTxnGenerator generates app opt in transactions -type BenchAppOptInsTxnGenerator struct { - NumApps int - Proto protocol.ConsensusVersion - Program []byte - OptedInAccts []basics.Address - OptedInAcctsIndices []int -} - -func (g *BenchAppOptInsTxnGenerator) Prepare(tb testing.TB, addrs []basics.Address, keys []*crypto.SignatureSecrets, rnd basics.Round, gh crypto.Digest) ([]transactions.SignedTxn, int) { - maxLocalSchemaEntries := config.Consensus[g.Proto].MaxLocalSchemaEntries - maxAppsOptedIn := config.Consensus[g.Proto].MaxAppsOptedIn - - // this function might create too much transaction even to fit into a single block - // estimate number of smaller blocks needed in order to set LastValid properly - const numAccts = 10000 - const maxTxnPerBlock = 10000 - expectedTxnNum := g.NumApps + numAccts*maxAppsOptedIn - expectedNumOfBlocks := expectedTxnNum/maxTxnPerBlock + 1 - - createTxns := make([]transactions.SignedTxn, 0, g.NumApps) - for i := 0; i < g.NumApps; i++ { - creatorIdx := rand.Intn(len(addrs)) - creator := addrs[creatorIdx] - txn := transactions.Transaction{ - Type: protocol.ApplicationCallTx, - Header: transactions.Header{ - Sender: creator, - Fee: minFee, - FirstValid: rnd, - LastValid: rnd + basics.Round(expectedNumOfBlocks), - GenesisHash: gh, - Note: randomNote(), - }, - ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ - ApprovalProgram: g.Program, - ClearStateProgram: []byte{0x02, 0x20, 0x01, 0x01, 0x22}, - LocalStateSchema: basics.StateSchema{NumByteSlice: maxLocalSchemaEntries}, - }, - } - stxn := txn.Sign(keys[creatorIdx]) - createTxns = append(createTxns, stxn) - } - - appsOptedIn := make(map[basics.Address]map[basics.AppIndex]struct{}, numAccts) - - optInTxns := make([]transactions.SignedTxn, 0, numAccts*maxAppsOptedIn) - - for i := 0; i < numAccts; i++ { - var senderIdx int - var sender basics.Address - for { - senderIdx = rand.Intn(len(addrs)) - sender = addrs[senderIdx] - if len(appsOptedIn[sender]) < maxAppsOptedIn { - appsOptedIn[sender] = make(map[basics.AppIndex]struct{}, maxAppsOptedIn) - break - } - } - g.OptedInAccts = append(g.OptedInAccts, sender) - g.OptedInAcctsIndices = append(g.OptedInAcctsIndices, senderIdx) - - acctOptIns := appsOptedIn[sender] - for j := 0; j < maxAppsOptedIn; j++ { - var appIdx basics.AppIndex - for { - appIdx = basics.AppIndex(rand.Intn(g.NumApps) + 1) - if _, ok := acctOptIns[appIdx]; !ok { - acctOptIns[appIdx] = struct{}{} - break - } - } - - txn := transactions.Transaction{ - Type: protocol.ApplicationCallTx, - Header: transactions.Header{ - Sender: sender, - Fee: minFee, - FirstValid: rnd, - LastValid: rnd + basics.Round(expectedNumOfBlocks), - GenesisHash: gh, - }, - ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ - ApplicationID: basics.AppIndex(appIdx), - OnCompletion: transactions.OptInOC, - }, - } - stxn := txn.Sign(keys[senderIdx]) - optInTxns = append(optInTxns, stxn) - } - appsOptedIn[sender] = acctOptIns - } - - return append(createTxns, optInTxns...), maxTxnPerBlock -} - -func (g *BenchAppOptInsTxnGenerator) Txn(tb testing.TB, addrs []basics.Address, keys []*crypto.SignatureSecrets, rnd basics.Round, gh crypto.Digest) transactions.SignedTxn { - idx := rand.Intn(len(g.OptedInAcctsIndices)) - senderIdx := g.OptedInAcctsIndices[idx] - sender := addrs[senderIdx] - receiverIdx := rand.Intn(len(addrs)) - - txn := transactions.Transaction{ - Type: protocol.PaymentTx, - Header: transactions.Header{ - Sender: sender, - Fee: minFee, - FirstValid: rnd, - LastValid: rnd, - GenesisHash: gh, - Note: randomNote(), - }, - PaymentTxnFields: transactions.PaymentTxnFields{ - Receiver: addrs[receiverIdx], - Amount: basics.MicroAlgos{Raw: 100}, - }, - } - stxn := txn.Sign(keys[senderIdx]) - return stxn -} - -func BenchmarkBlockEvaluatorRAMCrypto(b *testing.B) { - g := BenchPaymentTxnGenerator{} - benchmarkBlockEvaluator(b, true, true, protocol.ConsensusCurrentVersion, &g) -} -func BenchmarkBlockEvaluatorRAMNoCrypto(b *testing.B) { - g := BenchPaymentTxnGenerator{} - benchmarkBlockEvaluator(b, true, false, protocol.ConsensusCurrentVersion, &g) -} -func BenchmarkBlockEvaluatorDiskCrypto(b *testing.B) { - g := BenchPaymentTxnGenerator{} - benchmarkBlockEvaluator(b, false, true, protocol.ConsensusCurrentVersion, &g) -} -func BenchmarkBlockEvaluatorDiskNoCrypto(b *testing.B) { - g := BenchPaymentTxnGenerator{} - benchmarkBlockEvaluator(b, false, false, protocol.ConsensusCurrentVersion, &g) -} - -func BenchmarkBlockEvaluatorDiskAppOptIns(b *testing.B) { - g := BenchAppOptInsTxnGenerator{ - NumApps: 500, - Proto: protocol.ConsensusFuture, - Program: []byte{0x02, 0x20, 0x01, 0x01, 0x22}, - } - benchmarkBlockEvaluator(b, false, false, protocol.ConsensusFuture, &g) -} - -func BenchmarkBlockEvaluatorDiskFullAppOptIns(b *testing.B) { - // program sets all 16 available keys of len 64 bytes to same values of 64 bytes - source := `#pragma version 5 - txn OnCompletion - int OptIn - == - bz done - int 0 - store 0 // save loop var -loop: - int 0 // acct index - byte "012345678901234567890123456789012345678901234567890123456789ABC0" - int 63 - load 0 // loop var - int 0x41 - + - setbyte // str[63] = chr(i + 'A') - dup // value is the same as key - app_local_put - load 0 // loop var - int 1 - + - dup - store 0 // save loop var - int 16 - < - bnz loop -done: - int 1 -` - ops, err := logic.AssembleString(source) - require.NoError(b, err) - prog := ops.Program - g := BenchAppOptInsTxnGenerator{ - NumApps: 500, - Proto: protocol.ConsensusFuture, - Program: prog, - } - benchmarkBlockEvaluator(b, false, false, protocol.ConsensusFuture, &g) -} - -// this variant focuses on benchmarking ledger.go `eval()`, the rest is setup, it runs eval() b.N times. -func benchmarkBlockEvaluator(b *testing.B, inMem bool, withCrypto bool, proto protocol.ConsensusVersion, txnSource BenchTxnGenerator) { - deadlockDisable := deadlock.Opts.Disable - deadlock.Opts.Disable = true - defer func() { deadlock.Opts.Disable = deadlockDisable }() - start := time.Now() - genesisInitState, addrs, keys := genesisWithProto(100000, proto) - dbName := fmt.Sprintf("%s.%d", b.Name(), crypto.RandUint64()) - cparams := config.Consensus[genesisInitState.Block.CurrentProtocol] - cparams.MaxTxnBytesPerBlock = 1000000000 // very big, no limit - config.Consensus[protocol.ConsensusVersion(dbName)] = cparams - genesisInitState.Block.CurrentProtocol = protocol.ConsensusVersion(dbName) - cfg := config.GetDefaultLocal() - cfg.Archival = true - l, err := OpenLedger(logging.Base(), dbName, inMem, genesisInitState, cfg) - require.NoError(b, err) - defer testLedgerCleanup(l, dbName, inMem) - - dbName2 := dbName + "_2" - l2, err := OpenLedger(logging.Base(), dbName2, inMem, genesisInitState, cfg) - require.NoError(b, err) - defer testLedgerCleanup(l2, dbName2, inMem) - - bepprof := os.Getenv("BLOCK_EVAL_PPROF") - if len(bepprof) > 0 { - profpath := dbName + "_cpuprof" - profout, err := os.Create(profpath) - if err != nil { - b.Fatal(err) - return - } - b.Logf("%s: cpu profile for b.N=%d", profpath, b.N) - pprof.StartCPUProfile(profout) - defer func() { - pprof.StopCPUProfile() - profout.Close() - }() - } - - newBlock := bookkeeping.MakeBlock(genesisInitState.Block.BlockHeader) - bev, err := l.StartEvaluator(newBlock.BlockHeader, 0, 0) - require.NoError(b, err) - - genHash := genesisInitState.Block.BlockHeader.GenesisHash - - backlogPool := execpool.MakeBacklog(nil, 0, execpool.LowPriority, nil) - defer backlogPool.Shutdown() - - // apply initialization transations if any - initSignedTxns, maxTxnPerBlock := txnSource.Prepare(b, addrs, keys, newBlock.Round(), genHash) - if len(initSignedTxns) > 0 { - // all init transactions need to be written to ledger before reopening and benchmarking - for _, l := range []*Ledger{l, l2} { - l.accts.ctxCancel() // force commitSyncer to exit - - // wait commitSyncer to exit - // the test calls commitRound directly and does not need commitSyncer/committedUpTo - select { - case <-l.accts.commitSyncerClosed: - break - } - } - - var numBlocks uint64 = 0 - var validatedBlock *ValidatedBlock - - // there are might more transactions than MaxTxnBytesPerBlock allows - // so make smaller blocks to fit - for i, stxn := range initSignedTxns { - err = bev.Transaction(stxn, transactions.ApplyData{}) - require.NoError(b, err) - if maxTxnPerBlock > 0 && i%maxTxnPerBlock == 0 || i == len(initSignedTxns)-1 { - validatedBlock, err = bev.GenerateBlock() - require.NoError(b, err) - for _, l := range []*Ledger{l, l2} { - err = l.AddValidatedBlock(*validatedBlock, agreement.Certificate{}) - require.NoError(b, err) - } - newBlock = bookkeeping.MakeBlock(validatedBlock.blk.BlockHeader) - bev, err = l.StartEvaluator(newBlock.BlockHeader, 0, 0) - require.NoError(b, err) - numBlocks++ - } - } - - // wait until everying is written and then reload ledgers in order - // to start reading accounts from DB and not from caches/deltas - var wg sync.WaitGroup - for _, l := range []*Ledger{l, l2} { - wg.Add(1) - // committing might take a long time, do it parallel - go func(l *Ledger) { - l.accts.accountsWriting.Add(1) - l.accts.commitRound(numBlocks, 0, 0) - l.accts.accountsWriting.Wait() - l.reloadLedger() - wg.Done() - }(l) - } - wg.Wait() - - newBlock = bookkeeping.MakeBlock(validatedBlock.blk.BlockHeader) - bev, err = l.StartEvaluator(newBlock.BlockHeader, 0, 0) - require.NoError(b, err) - } - - setupDone := time.Now() - setupTime := setupDone.Sub(start) - b.Logf("BenchmarkBlockEvaluator setup time %s", setupTime.String()) - - // test speed of block building - numTxns := 50000 - - for i := 0; i < numTxns; i++ { - stxn := txnSource.Txn(b, addrs, keys, newBlock.Round(), genHash) - err = bev.Transaction(stxn, transactions.ApplyData{}) - require.NoError(b, err) - } - - validatedBlock, err := bev.GenerateBlock() - require.NoError(b, err) - - blockBuildDone := time.Now() - blockBuildTime := blockBuildDone.Sub(setupDone) - b.ReportMetric(float64(blockBuildTime)/float64(numTxns), "ns/block_build_tx") - - err = l.AddValidatedBlock(*validatedBlock, agreement.Certificate{}) - require.NoError(b, err) - - avbDone := time.Now() - avbTime := avbDone.Sub(blockBuildDone) - b.ReportMetric(float64(avbTime)/float64(numTxns), "ns/AddValidatedBlock_tx") - - // test speed of block validation - // This should be the same as the eval line in ledger.go AddBlock() - // This is pulled out to isolate eval() time from db ops of AddValidatedBlock() - b.ResetTimer() - for i := 0; i < b.N; i++ { - if withCrypto { - _, err = l2.Validate(context.Background(), validatedBlock.blk, backlogPool) - } else { - _, err = eval(context.Background(), l2, validatedBlock.blk, false, nil, nil) - } - require.NoError(b, err) - } - - abDone := time.Now() - abTime := abDone.Sub(avbDone) - b.ReportMetric(float64(abTime)/float64(numTxns*b.N), "ns/eval_validate_tx") - - b.StopTimer() -} - -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 := 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 := genesis(10) - genesisInitState.Block.BlockHeader.GenesisHash = testnetGenesisHash - genesisInitState.Block.BlockHeader.GenesisID = "testnet" - genesisInitState.GenesisHash = testnetGenesisHash - - // for addr, adata := range genesisInitState.Accounts { - // t.Logf("%s: %+v", addr.String(), adata) - // } - rewardPoolBalance := genesisInitState.Accounts[testPoolAddr] - nextPoolBalance := rewardPoolBalance.MicroAlgos.Raw + poolBonus - - dbName := fmt.Sprintf("%s.%d", t.Name(), crypto.RandUint64()) - const inMem = true - cfg := config.GetDefaultLocal() - cfg.Archival = true - l, err := OpenLedger(logging.Base(), dbName, inMem, genesisInitState, cfg) - require.NoError(t, err) - defer l.Close() - - 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) -} - -// Test that ModifiedAssetHoldings in StateDelta is set correctly. -func TestModifiedAssetHoldings(t *testing.T) { - partitiontest.PartitionTest(t) - - genBalances, addrs, _ := newTestGenesis() - l := newTestLedger(t, genBalances) - defer l.Close() - - const assetid basics.AssetIndex = 1 - - createTxn := txntest.Txn{ - Type: "acfg", - Sender: addrs[0], - Fee: 2000, - AssetParams: basics.AssetParams{ - Total: 3, - Decimals: 0, - Manager: addrs[0], - Reserve: addrs[0], - Freeze: addrs[0], - Clawback: addrs[0], - }, - } - - optInTxn := txntest.Txn{ - Type: "axfer", - Sender: addrs[1], - Fee: 2000, - XferAsset: assetid, - AssetAmount: 0, - AssetReceiver: addrs[1], - } - - eval := l.nextBlock(t) - eval.txns(t, &createTxn, &optInTxn) - vb := l.endBlock(t, eval) - - { - aa := ledgercore.AccountAsset{ - Address: addrs[0], - Asset: assetid, - } - created, ok := vb.delta.ModifiedAssetHoldings[aa] - require.True(t, ok) - assert.True(t, created) - } - { - aa := ledgercore.AccountAsset{ - Address: addrs[1], - Asset: assetid, - } - created, ok := vb.delta.ModifiedAssetHoldings[aa] - require.True(t, ok) - assert.True(t, created) - } - - optOutTxn := txntest.Txn{ - Type: "axfer", - Sender: addrs[1], - Fee: 1000, - XferAsset: assetid, - AssetReceiver: addrs[0], - AssetCloseTo: addrs[0], - } - - closeTxn := txntest.Txn{ - Type: "acfg", - Sender: addrs[0], - Fee: 1000, - ConfigAsset: assetid, - } - - eval = l.nextBlock(t) - eval.txns(t, &optOutTxn, &closeTxn) - vb = l.endBlock(t, eval) - - { - aa := ledgercore.AccountAsset{ - Address: addrs[0], - Asset: assetid, - } - created, ok := vb.delta.ModifiedAssetHoldings[aa] - require.True(t, ok) - assert.False(t, created) - } - { - aa := ledgercore.AccountAsset{ - Address: addrs[1], - Asset: assetid, - } - created, ok := vb.delta.ModifiedAssetHoldings[aa] - require.True(t, ok) - assert.False(t, created) - } -} - -// 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 -} - -// 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) *Ledger { - l, _, _ := newTestLedgerImpl(t, balances, true) - return l -} - -func newTestLedgerOnDisk(t testing.TB, balances bookkeeping.GenesisBalances) (*Ledger, string, bookkeeping.Block) { - return newTestLedgerImpl(t, balances, false) -} - -func newTestLedgerImpl(t testing.TB, balances bookkeeping.GenesisBalances, inMem bool) (*Ledger, string, bookkeeping.Block) { - var genHash crypto.Digest - crypto.RandBytes(genHash[:]) - genBlock, err := bookkeeping.MakeGenesisBlock(protocol.ConsensusFuture, - balances, "test", genHash) - require.False(t, genBlock.FeeSink.IsZero()) - require.False(t, genBlock.RewardsPool.IsZero()) - dbName := fmt.Sprintf("%s.%d", t.Name(), crypto.RandUint64()) - cfg := config.GetDefaultLocal() - cfg.Archival = true - l, err := OpenLedger(logging.Base(), dbName, inMem, InitState{ - Block: genBlock, - Accounts: balances.Balances, - GenesisHash: genHash, - }, cfg) - require.NoError(t, err) - return l, dbName, genBlock -} - -// nextBlock begins evaluation of a new block, after ledger creation or endBlock() -func (ledger *Ledger) 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 *Ledger) endBlock(t testing.TB, eval *BlockEvaluator) *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 *Ledger) 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 *Ledger) 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 *Ledger) 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 *Ledger) 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) -} - -func (eval *BlockEvaluator) fillDefaults(txn *txntest.Txn) { - if txn.GenesisHash.IsZero() { - txn.GenesisHash = eval.genesisHash - } - if txn.FirstValid == 0 { - txn.FirstValid = eval.Round() - } - txn.FillDefaults(eval.proto) -} - -func (eval *BlockEvaluator) txn(t testing.TB, txn *txntest.Txn, problem ...string) { - t.Helper() - eval.fillDefaults(txn) - stxn := txn.SignedTxn() - err := eval.testTransaction(stxn, eval.state.child(1)) - if err != nil { - if len(problem) == 1 { - require.Contains(t, err.Error(), problem[0]) - } else { - require.NoError(t, err) // Will obviously fail - } - return - } - err = eval.Transaction(stxn, transactions.ApplyData{}) - if err != nil { - if len(problem) == 1 { - require.Contains(t, err.Error(), problem[0]) - } else { - require.NoError(t, err) // Will obviously fail - } - return - } - require.Len(t, problem, 0) -} - -func (eval *BlockEvaluator) txns(t testing.TB, txns ...*txntest.Txn) { - t.Helper() - for _, txn := range txns { - eval.txn(t, txn) - } -} - -func (eval *BlockEvaluator) txgroup(t testing.TB, txns ...*txntest.Txn) error { - t.Helper() - for _, txn := range txns { - eval.fillDefaults(txn) - } - txgroup := txntest.SignedTxns(txns...) - - err := eval.TestTransactionGroup(txgroup) - if err != nil { - return err - } - - err = eval.transactionGroup(transactions.WrapSignedTxnsWithAD(txgroup)) - return err -} - -func TestRewardsInAD(t *testing.T) { - partitiontest.PartitionTest(t) - - genBalances, addrs, _ := newTestGenesis() - l := newTestLedger(t, genBalances) - defer l.Close() - - payTxn := txntest.Txn{Type: "pay", Sender: addrs[0], Receiver: addrs[1]} - - // Build up Residue in RewardsState so it's ready to pay - for i := 1; i < 10; i++ { - eval := l.nextBlock(t) - l.endBlock(t, eval) - } - - eval := l.nextBlock(t) - eval.txn(t, &payTxn) - payInBlock := eval.block.Payset[0] - require.Greater(t, payInBlock.ApplyData.SenderRewards.Raw, uint64(1000)) - require.Greater(t, payInBlock.ApplyData.ReceiverRewards.Raw, uint64(1000)) - require.Equal(t, payInBlock.ApplyData.SenderRewards, payInBlock.ApplyData.ReceiverRewards) - l.endBlock(t, eval) -} - -func TestMinBalanceChanges(t *testing.T) { - partitiontest.PartitionTest(t) - - genBalances, addrs, _ := newTestGenesis() - l := newTestLedger(t, genBalances) - defer l.Close() - - createTxn := txntest.Txn{ - Type: "acfg", - Sender: addrs[0], - AssetParams: basics.AssetParams{ - Total: 3, - Manager: addrs[1], - Reserve: addrs[2], - Freeze: addrs[3], - Clawback: addrs[4], - }, - } - - const expectedID basics.AssetIndex = 1 - optInTxn := txntest.Txn{ - Type: "axfer", - Sender: addrs[5], - XferAsset: expectedID, - AssetReceiver: addrs[5], - } - - ad0init := l.lookup(t, addrs[0]) - ad5init := l.lookup(t, addrs[5]) - - eval := l.nextBlock(t) - eval.txns(t, &createTxn, &optInTxn) - l.endBlock(t, eval) - - ad0new := l.lookup(t, addrs[0]) - ad5new := l.lookup(t, addrs[5]) - - proto := config.Consensus[eval.block.BlockHeader.CurrentProtocol] - // Check balance and min balance requirement changes - require.Equal(t, ad0init.MicroAlgos.Raw, ad0new.MicroAlgos.Raw+1000) // fee - require.Equal(t, ad0init.MinBalance(&proto).Raw, ad0new.MinBalance(&proto).Raw-100000) // create - require.Equal(t, ad5init.MicroAlgos.Raw, ad5new.MicroAlgos.Raw+1000) // fee - require.Equal(t, ad5init.MinBalance(&proto).Raw, ad5new.MinBalance(&proto).Raw-100000) // optin - - optOutTxn := txntest.Txn{ - Type: "axfer", - Sender: addrs[5], - XferAsset: expectedID, - AssetReceiver: addrs[0], - AssetCloseTo: addrs[0], - } - - closeTxn := txntest.Txn{ - Type: "acfg", - Sender: addrs[1], // The manager, not the creator - ConfigAsset: expectedID, - } - - eval = l.nextBlock(t) - eval.txns(t, &optOutTxn, &closeTxn) - l.endBlock(t, eval) - - ad0final := l.lookup(t, addrs[0]) - ad5final := l.lookup(t, addrs[5]) - // Check we got our balance "back" - require.Equal(t, ad0final.MinBalance(&proto), ad0init.MinBalance(&proto)) - require.Equal(t, ad5final.MinBalance(&proto), ad5init.MinBalance(&proto)) -} - -// Test that ModifiedAppLocalStates in StateDelta is set correctly. -func TestModifiedAppLocalStates(t *testing.T) { - partitiontest.PartitionTest(t) - - genBalances, addrs, _ := newTestGenesis() - l := newTestLedger(t, genBalances) - defer l.Close() - - const appid basics.AppIndex = 1 - - createTxn := txntest.Txn{ - Type: "appl", - Sender: addrs[0], - ApprovalProgram: "int 1", - } - - optInTxn := txntest.Txn{ - Type: "appl", - Sender: addrs[1], - ApplicationID: appid, - OnCompletion: transactions.OptInOC, - } - - eval := l.nextBlock(t) - eval.txns(t, &createTxn, &optInTxn) - vb := l.endBlock(t, eval) - - assert.Len(t, vb.delta.ModifiedAppLocalStates, 1) - { - aa := ledgercore.AccountApp{ - Address: addrs[1], - App: appid, - } - created, ok := vb.delta.ModifiedAppLocalStates[aa] - require.True(t, ok) - assert.True(t, created) - } - - optOutTxn := txntest.Txn{ - Type: "appl", - Sender: addrs[1], - ApplicationID: appid, - OnCompletion: transactions.CloseOutOC, - } - - closeTxn := txntest.Txn{ - Type: "appl", - Sender: addrs[0], - ApplicationID: appid, - OnCompletion: transactions.DeleteApplicationOC, - } - - eval = l.nextBlock(t) - eval.txns(t, &optOutTxn, &closeTxn) - vb = l.endBlock(t, eval) - - assert.Len(t, vb.delta.ModifiedAppLocalStates, 1) - { - aa := ledgercore.AccountApp{ - Address: addrs[1], - App: appid, - } - created, ok := vb.delta.ModifiedAppLocalStates[aa] - require.True(t, ok) - assert.False(t, created) - } -} - -// TestAppInsMinBalance checks that accounts with MaxAppsOptedIn are accepted by block evaluator -// and do not cause any MaximumMinimumBalance problems -func TestAppInsMinBalance(t *testing.T) { - partitiontest.PartitionTest(t) - - genBalances, addrs, _ := newTestGenesis() - l := newTestLedger(t, genBalances) - defer l.Close() - - const appid basics.AppIndex = 1 - - maxAppsOptedIn := config.Consensus[protocol.ConsensusFuture].MaxAppsOptedIn - require.Greater(t, maxAppsOptedIn, 0) - maxAppsCreated := config.Consensus[protocol.ConsensusFuture].MaxAppsCreated - require.Greater(t, maxAppsCreated, 0) - maxLocalSchemaEntries := config.Consensus[protocol.ConsensusFuture].MaxLocalSchemaEntries - require.Greater(t, maxLocalSchemaEntries, uint64(0)) - - txnsCreate := make([]*txntest.Txn, 0, maxAppsOptedIn) - txnsOptIn := make([]*txntest.Txn, 0, maxAppsOptedIn) - appsCreated := make(map[basics.Address]int, len(addrs)-1) - - acctIdx := 0 - for i := 0; i < maxAppsOptedIn; i++ { - creator := addrs[acctIdx] - createTxn := txntest.Txn{ - Type: protocol.ApplicationCallTx, - Sender: creator, - ApprovalProgram: "int 1", - LocalStateSchema: basics.StateSchema{NumByteSlice: maxLocalSchemaEntries}, - Note: randomNote(), - } - txnsCreate = append(txnsCreate, &createTxn) - count := appsCreated[creator] - count++ - appsCreated[creator] = count - if count == maxAppsCreated { - acctIdx++ - } - - optInTxn := txntest.Txn{ - Type: protocol.ApplicationCallTx, - Sender: addrs[9], - ApplicationID: appid + basics.AppIndex(i), - OnCompletion: transactions.OptInOC, - } - txnsOptIn = append(txnsOptIn, &optInTxn) - } - - eval := l.nextBlock(t) - txns := append(txnsCreate, txnsOptIn...) - eval.txns(t, txns...) - vb := l.endBlock(t, eval) - assert.Len(t, vb.delta.ModifiedAppLocalStates, 50) -} - -// TestGhostTransactions confirms that accounts that don't even exist -// can be the Sender in some situations. If some other transaction -// covers the fee, and the transaction itself does not require an -// asset or a min balance, it's fine. -func TestGhostTransactions(t *testing.T) { - t.Skip("Behavior should be changed so test passes.") - - /* - I think we have a behavior we should fix. I’m going to call these - transactions where the Sender has no account and the fee=0 “ghost” - transactions. In a ghost transaction, we still call balances.Move to - “pay” the fee. Further, Move does not short-circuit a Move of 0 (for - good reason, allowing compounding). Therefore, in Move, we do rewards - processing on the “ghost” account. That causes us to want to write a - new accountdata for them. But if we do that, the minimum balance - checker will catch it, and kill the transaction because the ghost isn’t - allowed to have a balance of 0. I don’t think we can short-circuit - Move(0) because a zero pay is a known way to get your rewards - actualized. Instead, I advocate that we short-circuit the call to Move - for 0 fees. - - // move fee to pool - if !tx.Fee.IsZero() { - err = balances.Move(tx.Sender, eval.specials.FeeSink, tx.Fee, &ad.SenderRewards, nil) - if err != nil { - return - } - } - - I think this must be controlled by consensus upgrade, but I would love - to be told I’m wrong. The other option is to outlaw these - transactions, but even that requires changing code if we want to be - exactly correct, because they are currently allowed when there are no - rewards to get paid out (as would happen in a new network, or if we - stop participation rewards - notice that this test only fails on the - 4th attempt, once rewards have accumulated). - - Will suggested that we could treat Ghost accounts as non-partipating. - Maybe that would allow the Move code to avoid trying to update - accountdata. - */ - - partitiontest.PartitionTest(t) - - genBalances, addrs, _ := newTestGenesis() - l := newTestLedger(t, genBalances) - defer l.Close() - - asaIndex := basics.AssetIndex(1) - - asa := txntest.Txn{ - Type: "acfg", - Sender: addrs[0], - AssetParams: basics.AssetParams{ - Total: 1000000, - Decimals: 3, - UnitName: "oz", - AssetName: "Gold", - URL: "https://gold.rush/", - Clawback: basics.Address{0x0c, 0x0b, 0x0a, 0x0c}, - Freeze: basics.Address{0x0f, 0x0e, 0xe, 0xe}, - Manager: basics.Address{0x0a, 0x0a, 0xe}, - }, - } - - eval := l.nextBlock(t) - eval.txn(t, &asa) - l.endBlock(t, eval) - - benefactor := txntest.Txn{ - Type: "pay", - Sender: addrs[0], - Receiver: addrs[0], - Fee: 2000, - } - - ghost := basics.Address{0x01} - ephemeral := []txntest.Txn{ - { - Type: "pay", - Amount: 0, - Sender: ghost, - Receiver: ghost, - Fee: 0, - }, - { - Type: "axfer", - AssetAmount: 0, - Sender: ghost, - AssetReceiver: basics.Address{0x02}, - XferAsset: basics.AssetIndex(1), - Fee: 0, - }, - { - Type: "axfer", - AssetAmount: 0, - Sender: basics.Address{0x0c, 0x0b, 0x0a, 0x0c}, - AssetReceiver: addrs[0], - AssetSender: addrs[1], - XferAsset: asaIndex, - Fee: 0, - }, - { - Type: "afrz", - Sender: basics.Address{0x0f, 0x0e, 0xe, 0xe}, - FreezeAccount: addrs[0], // creator, therefore is opted in - FreezeAsset: asaIndex, - AssetFrozen: true, - Fee: 0, - }, - { - Type: "afrz", - Sender: basics.Address{0x0f, 0x0e, 0xe, 0xe}, - FreezeAccount: addrs[0], // creator, therefore is opted in - FreezeAsset: asaIndex, - AssetFrozen: false, - Fee: 0, - }, - } - - for i, e := range ephemeral { - eval = l.nextBlock(t) - err := eval.txgroup(t, &benefactor, &e) - require.NoError(t, err, "i=%d %s", i, e.Type) - l.endBlock(t, eval) - } -} - -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, 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) - } - } -} |