diff options
Diffstat (limited to 'ledger/internal/apptxn_test.go')
-rw-r--r-- | ledger/internal/apptxn_test.go | 2441 |
1 files changed, 2441 insertions, 0 deletions
diff --git a/ledger/internal/apptxn_test.go b/ledger/internal/apptxn_test.go new file mode 100644 index 000000000..fed5a4f06 --- /dev/null +++ b/ledger/internal/apptxn_test.go @@ -0,0 +1,2441 @@ +// Copyright (C) 2019-2022 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_test + +import ( + "encoding/hex" + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/bookkeeping" + "github.com/algorand/go-algorand/data/transactions/logic" + "github.com/algorand/go-algorand/data/txntest" + "github.com/algorand/go-algorand/ledger" + "github.com/algorand/go-algorand/ledger/ledgercore" + ledgertesting "github.com/algorand/go-algorand/ledger/testing" + "github.com/algorand/go-algorand/logging" + "github.com/algorand/go-algorand/protocol" + "github.com/algorand/go-algorand/test/partitiontest" +) + +// main wraps up some TEAL source in a header and footer so that it is +// an app that does nothing at create time, but otherwise runs source, +// then approves, if the source avoids panicing and leaves the stack +// empty. +func main(source string) string { + return strings.Replace(fmt.Sprintf(`txn ApplicationID + bz end + %s + end: int 1`, source), ";", "\n", -1) +} + +// TestPayAction ensures a pay in teal affects balances +func TestPayAction(t *testing.T) { + partitiontest.PartitionTest(t) + + genesisInitState, addrs, _ := ledgertesting.Genesis(10) + + l, err := ledger.OpenLedger(logging.TestingLog(t), "", true, genesisInitState, config.GetDefaultLocal()) + require.NoError(t, err) + defer l.Close() + + create := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` + itxn_begin + int pay + itxn_field TypeEnum + int 5000 + itxn_field Amount + txn Accounts 1 + itxn_field Receiver + itxn_submit +`), + } + + ai := basics.AppIndex(1) + fund := txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: ai.Address(), + Amount: 200000, // account min balance, plus fees + } + + payout1 := txntest.Txn{ + Type: "appl", + Sender: addrs[1], + ApplicationID: ai, + Accounts: []basics.Address{addrs[1]}, // pay self + } + + eval := nextBlock(t, l, true, nil) + txns(t, l, eval, &create, &fund, &payout1) + vb := endBlock(t, l, eval) + + // AD contains expected appIndex + require.Equal(t, ai, vb.Block().Payset[0].ApplyData.ApplicationID) + + ad0 := micros(t, l, addrs[0]) + ad1 := micros(t, l, addrs[1]) + app := micros(t, l, ai.Address()) + + genAccounts := genesisInitState.Accounts + // create(1000) and fund(1000 + 200000) + require.Equal(t, uint64(202000), genAccounts[addrs[0]].MicroAlgos.Raw-ad0) + // paid 5000, but 1000 fee + require.Equal(t, uint64(4000), ad1-genAccounts[addrs[1]].MicroAlgos.Raw) + // app still has 194000 (paid out 5000, and paid fee to do it) + require.Equal(t, uint64(194000), app) + + // Build up Residue in RewardsState so it's ready to pay + for i := 1; i < 10; i++ { + eval = nextBlock(t, l, true, nil) + endBlock(t, l, eval) + } + + eval = nextBlock(t, l, true, nil) + payout2 := txntest.Txn{ + Type: "appl", + Sender: addrs[1], + ApplicationID: ai, + Accounts: []basics.Address{addrs[2]}, // pay other + } + txn(t, l, eval, &payout2) + // confirm that modifiedAccounts can see account in inner txn + vb = endBlock(t, l, eval) + + deltas := vb.Delta() + require.Contains(t, deltas.Accts.ModifiedAccounts(), addrs[2]) + + payInBlock := vb.Block().Payset[0] + rewards := payInBlock.ApplyData.SenderRewards.Raw + require.Greater(t, rewards, uint64(2000)) // some biggish number + inners := payInBlock.ApplyData.EvalDelta.InnerTxns + require.Len(t, inners, 1) + + // addr[2] is going to get the same rewards as addr[1], who + // originally sent the top-level txn. Both had their algo balance + // touched and has very nearly the same balance. + require.Equal(t, rewards, inners[0].ReceiverRewards.Raw) + // app gets none, because it has less than 1A + require.Equal(t, uint64(0), inners[0].SenderRewards.Raw) + + ad1 = micros(t, l, addrs[1]) + ad2 := micros(t, l, addrs[2]) + app = micros(t, l, ai.Address()) + + // paid 5000, in first payout (only), but paid 1000 fee in each payout txn + require.Equal(t, rewards+3000, ad1-genAccounts[addrs[1]].MicroAlgos.Raw) + // app still has 188000 (paid out 10000, and paid 2k fees to do it) + // no rewards because owns less than an algo + require.Equal(t, uint64(200000)-10000-2000, app) + + // paid 5000 by payout2, never paid any fees, got same rewards + require.Equal(t, rewards+uint64(5000), ad2-genAccounts[addrs[2]].MicroAlgos.Raw) + + // Now fund the app account much more, so we can confirm it gets rewards. + tenkalgos := txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: ai.Address(), + Amount: 10 * 1000 * 1000000, // account min balance, plus fees + } + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, &tenkalgos) + endBlock(t, l, eval) + beforepay := micros(t, l, ai.Address()) + + // Build up Residue in RewardsState so it's ready to pay again + for i := 1; i < 10; i++ { + eval = nextBlock(t, l, true, nil) + endBlock(t, l, eval) + } + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, payout2.Noted("2")) + vb = endBlock(t, l, eval) + + afterpay := micros(t, l, ai.Address()) + + payInBlock = vb.Block().Payset[0] + inners = payInBlock.ApplyData.EvalDelta.InnerTxns + require.Len(t, inners, 1) + + appreward := inners[0].SenderRewards.Raw + require.Greater(t, appreward, uint64(1000)) + + require.Equal(t, beforepay+appreward-5000-1000, afterpay) +} + +// TestAxferAction ensures axfers in teal have the intended effects +func TestAxferAction(t *testing.T) { + partitiontest.PartitionTest(t) + + genesisInitState, addrs, _ := ledgertesting.Genesis(10) + + l, err := ledger.OpenLedger(logging.TestingLog(t), "", true, genesisInitState, config.GetDefaultLocal()) + require.NoError(t, err) + defer l.Close() + + asa := txntest.Txn{ + Type: "acfg", + Sender: addrs[0], + AssetParams: basics.AssetParams{ + Total: 1000000, + Decimals: 3, + UnitName: "oz", + AssetName: "Gold", + URL: "https://gold.rush/", + }, + } + + app := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` + itxn_begin + int axfer + itxn_field TypeEnum + txn Assets 0 + itxn_field XferAsset + + txn ApplicationArgs 0 + byte "optin" + == + bz withdraw + // let AssetAmount default to 0 + global CurrentApplicationAddress + itxn_field AssetReceiver + b submit +withdraw: + txn ApplicationArgs 0 + byte "close" + == + bz noclose + txn Accounts 1 + itxn_field AssetCloseTo + b skipamount +noclose: int 10000 + itxn_field AssetAmount +skipamount: + txn Accounts 1 + itxn_field AssetReceiver +submit: itxn_submit +`), + } + + eval := nextBlock(t, l, true, nil) + txns(t, l, eval, &asa, &app) + vb := endBlock(t, l, eval) + + asaIndex := basics.AssetIndex(1) + require.Equal(t, asaIndex, vb.Block().Payset[0].ApplyData.ConfigAsset) + appIndex := basics.AppIndex(2) + require.Equal(t, appIndex, vb.Block().Payset[1].ApplyData.ApplicationID) + + fund := txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: appIndex.Address(), + Amount: 300000, // account min balance, optin min balance, plus fees + // stay under 1M, to avoid rewards complications + } + + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, &fund) + endBlock(t, l, eval) + + fundgold := txntest.Txn{ + Type: "axfer", + Sender: addrs[0], + XferAsset: asaIndex, + AssetReceiver: appIndex.Address(), + AssetAmount: 20000, + } + + // Fail, because app account is not opted in. + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, &fundgold, fmt.Sprintf("asset %d missing", asaIndex)) + endBlock(t, l, eval) + + amount, in := holding(t, l, appIndex.Address(), asaIndex) + require.False(t, in) + require.Equal(t, amount, uint64(0)) + + optin := txntest.Txn{ + Type: "appl", + ApplicationID: appIndex, + Sender: addrs[0], + ApplicationArgs: [][]byte{[]byte("optin")}, + ForeignAssets: []basics.AssetIndex{asaIndex}, + } + + // Tell the app to opt itself in. + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, &optin) + endBlock(t, l, eval) + + amount, in = holding(t, l, appIndex.Address(), asaIndex) + require.True(t, in) + require.Equal(t, amount, uint64(0)) + + // Now, suceed, because opted in. + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, &fundgold) + endBlock(t, l, eval) + + amount, in = holding(t, l, appIndex.Address(), asaIndex) + require.True(t, in) + require.Equal(t, amount, uint64(20000)) + + withdraw := txntest.Txn{ + Type: "appl", + ApplicationID: appIndex, + Sender: addrs[0], + ApplicationArgs: [][]byte{[]byte("withdraw")}, + ForeignAssets: []basics.AssetIndex{asaIndex}, + Accounts: []basics.Address{addrs[0]}, + } + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, &withdraw) + endBlock(t, l, eval) + + amount, in = holding(t, l, appIndex.Address(), asaIndex) + require.True(t, in) + require.Equal(t, amount, uint64(10000)) + + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, withdraw.Noted("2")) + endBlock(t, l, eval) + + amount, in = holding(t, l, appIndex.Address(), asaIndex) + require.True(t, in) // Zero left, but still opted in + require.Equal(t, amount, uint64(0)) + + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, withdraw.Noted("3"), "underflow on subtracting") + endBlock(t, l, eval) + + amount, in = holding(t, l, appIndex.Address(), asaIndex) + require.True(t, in) // Zero left, but still opted in + require.Equal(t, amount, uint64(0)) + + close := txntest.Txn{ + Type: "appl", + ApplicationID: appIndex, + Sender: addrs[0], + ApplicationArgs: [][]byte{[]byte("close")}, + ForeignAssets: []basics.AssetIndex{asaIndex}, + Accounts: []basics.Address{addrs[0]}, + } + + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, &close) + endBlock(t, l, eval) + + amount, in = holding(t, l, appIndex.Address(), asaIndex) + require.False(t, in) // Zero left, not opted in + require.Equal(t, amount, uint64(0)) + + // Now, fail again, opted out + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, fundgold.Noted("2"), fmt.Sprintf("asset %d missing", asaIndex)) + endBlock(t, l, eval) + + // Do it all again, so we can test closeTo when we have a non-zero balance + // Tell the app to opt itself in. + eval = nextBlock(t, l, true, nil) + txns(t, l, eval, optin.Noted("a"), fundgold.Noted("a")) + endBlock(t, l, eval) + + amount, _ = holding(t, l, appIndex.Address(), asaIndex) + require.Equal(t, uint64(20000), amount) + left, _ := holding(t, l, addrs[0], asaIndex) + + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, close.Noted("a")) + endBlock(t, l, eval) + + amount, _ = holding(t, l, appIndex.Address(), asaIndex) + require.Equal(t, uint64(0), amount) + back, _ := holding(t, l, addrs[0], asaIndex) + require.Equal(t, uint64(20000), back-left) +} + +func newTestLedger(t testing.TB, balances bookkeeping.GenesisBalances) *ledger.Ledger { + return newTestLedgerWithConsensusVersion(t, balances, protocol.ConsensusFuture) +} + +func newTestLedgerWithConsensusVersion(t testing.TB, balances bookkeeping.GenesisBalances, cv protocol.ConsensusVersion) *ledger.Ledger { + var genHash crypto.Digest + crypto.RandBytes(genHash[:]) + genBlock, err := bookkeeping.MakeGenesisBlock(cv, balances, "test", genHash) + require.NoError(t, err) + 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 := ledger.OpenLedger(logging.Base(), dbName, true, ledgercore.InitState{ + Block: genBlock, + Accounts: balances.Balances, + GenesisHash: genHash, + }, cfg) + require.NoError(t, err) + return l +} + +// TestClawbackAction ensures an app address can act as clawback address. +func TestClawbackAction(t *testing.T) { + partitiontest.PartitionTest(t) + + genBalances, addrs, _ := ledgertesting.NewTestGenesis() + l := newTestLedger(t, genBalances) + defer l.Close() + + asaIndex := basics.AssetIndex(1) + appIndex := basics.AppIndex(2) + + asa := txntest.Txn{ + Type: "acfg", + Sender: addrs[0], + AssetParams: basics.AssetParams{ + Total: 1000000, + Decimals: 3, + UnitName: "oz", + AssetName: "Gold", + URL: "https://gold.rush/", + Clawback: appIndex.Address(), + }, + } + + app := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` + itxn_begin + + int axfer + itxn_field TypeEnum + + txn Assets 0 + itxn_field XferAsset + + txn Accounts 1 + itxn_field AssetSender + + txn Accounts 2 + itxn_field AssetReceiver + + int 1000 + itxn_field AssetAmount + + itxn_submit +`), + } + + optin := txntest.Txn{ + Type: "axfer", + Sender: addrs[1], + AssetReceiver: addrs[1], + XferAsset: asaIndex, + } + eval := nextBlock(t, l, true, nil) + txns(t, l, eval, &asa, &app, &optin) + vb := endBlock(t, l, eval) + + require.Equal(t, asaIndex, vb.Block().Payset[0].ApplyData.ConfigAsset) + require.Equal(t, appIndex, vb.Block().Payset[1].ApplyData.ApplicationID) + + bystander := addrs[2] // Has no authority of its own + overpay := txntest.Txn{ + Type: "pay", + Sender: bystander, + Receiver: bystander, + Fee: 2000, // Overpay fee so that app account can be unfunded + } + clawmove := txntest.Txn{ + Type: "appl", + Sender: bystander, + ApplicationID: appIndex, + ForeignAssets: []basics.AssetIndex{asaIndex}, + Accounts: []basics.Address{addrs[0], addrs[1]}, + } + eval = nextBlock(t, l, true, nil) + err := txgroup(t, l, eval, &overpay, &clawmove) + require.NoError(t, err) + endBlock(t, l, eval) + + amount, _ := holding(t, l, addrs[1], asaIndex) + require.Equal(t, amount, uint64(1000)) +} + +// TestRekeyAction ensures an app can transact for a rekeyed account +func TestRekeyAction(t *testing.T) { + partitiontest.PartitionTest(t) + + genBalances, addrs, _ := ledgertesting.NewTestGenesis() + l := newTestLedger(t, genBalances) + defer l.Close() + + appIndex := basics.AppIndex(1) + ezpayer := txntest.Txn{ + Type: "appl", + Sender: addrs[5], + ApprovalProgram: main(` + itxn_begin + int pay + itxn_field TypeEnum + int 5000 + itxn_field Amount + txn Accounts 1 + itxn_field Sender + txn Accounts 2 + itxn_field Receiver + txn NumAccounts + int 3 + == + bz skipclose + txn Accounts 3 + itxn_field CloseRemainderTo +skipclose: + itxn_submit +`), + } + + rekey := txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: addrs[0], + RekeyTo: appIndex.Address(), + } + + eval := nextBlock(t, l, true, nil) + txns(t, l, eval, &ezpayer, &rekey) + endBlock(t, l, eval) + + useacct := txntest.Txn{ + Type: "appl", + Sender: addrs[1], + ApplicationID: appIndex, + Accounts: []basics.Address{addrs[0], addrs[2]}, // pay 2 from 0 (which was rekeyed) + } + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, &useacct) + endBlock(t, l, eval) + + // App was never funded (didn't spend from it's own acct) + require.Equal(t, uint64(0), micros(t, l, basics.AppIndex(1).Address())) + // addrs[2] got paid + require.Equal(t, uint64(5000), micros(t, l, addrs[2])-micros(t, l, addrs[6])) + // addrs[0] paid 5k + rekey fee + inner txn fee + require.Equal(t, uint64(7000), micros(t, l, addrs[6])-micros(t, l, addrs[0])) + + baduse := txntest.Txn{ + Type: "appl", + Sender: addrs[1], + ApplicationID: appIndex, + Accounts: []basics.Address{addrs[2], addrs[0]}, // pay 0 from 2 + } + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, &baduse, "unauthorized") + endBlock(t, l, eval) + + // Now, we close addrs[0], which wipes its rekey status. Reopen + // it, and make sure the app can't spend. + + close := txntest.Txn{ + Type: "appl", + Sender: addrs[1], + ApplicationID: appIndex, + Accounts: []basics.Address{addrs[0], addrs[2], addrs[3]}, // close to 3 + } + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, &close) + endBlock(t, l, eval) + + require.Equal(t, uint64(0), micros(t, l, addrs[0])) + + payback := txntest.Txn{ + Type: "pay", + Sender: addrs[3], + Receiver: addrs[0], + Amount: 10_000_000, + } + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, &payback) + endBlock(t, l, eval) + + require.Equal(t, uint64(10_000_000), micros(t, l, addrs[0])) + + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, useacct.Noted("2"), "unauthorized") + endBlock(t, l, eval) +} + +// TestRekeyActionCloseAccount ensures closing and reopening a rekeyed account in a single app call +// properly removes the app as an authorizer for the account +func TestRekeyActionCloseAccount(t *testing.T) { + partitiontest.PartitionTest(t) + + genBalances, addrs, _ := ledgertesting.NewTestGenesis() + l := newTestLedger(t, genBalances) + defer l.Close() + + appIndex := basics.AppIndex(1) + create := txntest.Txn{ + Type: "appl", + Sender: addrs[5], + ApprovalProgram: main(` + // close account 1 + itxn_begin + int pay + itxn_field TypeEnum + txn Accounts 1 + itxn_field Sender + txn Accounts 2 + itxn_field CloseRemainderTo + itxn_submit + + // reopen account 1 + itxn_begin + int pay + itxn_field TypeEnum + int 5000 + itxn_field Amount + txn Accounts 1 + itxn_field Receiver + itxn_submit + // send from account 1 again (should fail because closing an account erases rekeying) + itxn_begin + int pay + itxn_field TypeEnum + int 1 + itxn_field Amount + txn Accounts 1 + itxn_field Sender + txn Accounts 2 + itxn_field Receiver + itxn_submit +`), + } + + rekey := txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: addrs[0], + RekeyTo: appIndex.Address(), + } + + fund := txntest.Txn{ + Type: "pay", + Sender: addrs[1], + Receiver: appIndex.Address(), + Amount: 1_000_000, + } + + eval := nextBlock(t, l, true, nil) + txns(t, l, eval, &create, &rekey, &fund) + endBlock(t, l, eval) + + useacct := txntest.Txn{ + Type: "appl", + Sender: addrs[1], + ApplicationID: appIndex, + Accounts: []basics.Address{addrs[0], addrs[2]}, + } + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, &useacct, "unauthorized") + endBlock(t, l, eval) +} + +// TestDuplicatePayAction shows two pays with same parameters can be done as inner tarnsactions +func TestDuplicatePayAction(t *testing.T) { + partitiontest.PartitionTest(t) + + genBalances, addrs, _ := ledgertesting.NewTestGenesis() + l := newTestLedger(t, genBalances) + defer l.Close() + + appIndex := basics.AppIndex(1) + create := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` + itxn_begin + int pay + itxn_field TypeEnum + int 5000 + itxn_field Amount + txn Accounts 1 + itxn_field Receiver + itxn_submit + itxn_begin + int pay + itxn_field TypeEnum + int 5000 + itxn_field Amount + txn Accounts 1 + itxn_field Receiver + itxn_submit +`), + } + + fund := txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: appIndex.Address(), + Amount: 200000, // account min balance, plus fees + } + + paytwice := txntest.Txn{ + Type: "appl", + Sender: addrs[1], + ApplicationID: appIndex, + Accounts: []basics.Address{addrs[1]}, // pay self + } + + eval := nextBlock(t, l, true, nil) + txns(t, l, eval, &create, &fund, &paytwice, create.Noted("in same block")) + vb := endBlock(t, l, eval) + + require.Equal(t, appIndex, vb.Block().Payset[0].ApplyData.ApplicationID) + require.Equal(t, 4, len(vb.Block().Payset)) + // create=1, fund=2, payTwice=3,4,5 + require.Equal(t, basics.AppIndex(6), vb.Block().Payset[3].ApplyData.ApplicationID) + + ad0 := micros(t, l, addrs[0]) + ad1 := micros(t, l, addrs[1]) + app := micros(t, l, appIndex.Address()) + + // create(1000) and fund(1000 + 200000), extra create (1000) + require.Equal(t, 203000, int(genBalances.Balances[addrs[0]].MicroAlgos.Raw-ad0)) + // paid 10000, but 1000 fee on tx + require.Equal(t, 9000, int(ad1-genBalances.Balances[addrs[1]].MicroAlgos.Raw)) + // app still has 188000 (paid out 10000, and paid 2 x fee to do it) + require.Equal(t, 188000, int(app)) + + // Now create another app, and see if it gets the index we expect. + eval = nextBlock(t, l, true, nil) + txns(t, l, eval, create.Noted("again")) + vb = endBlock(t, l, eval) + + // create=1, fund=2, payTwice=3,4,5, insameblock=6 + require.Equal(t, basics.AppIndex(7), vb.Block().Payset[0].ApplyData.ApplicationID) +} + +// TestInnerTxCount ensures that inner transactions increment the TxnCounter +func TestInnerTxnCount(t *testing.T) { + partitiontest.PartitionTest(t) + + genBalances, addrs, _ := ledgertesting.NewTestGenesis() + l := newTestLedger(t, genBalances) + defer l.Close() + + create := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` + itxn_begin + int pay + itxn_field TypeEnum + int 5000 + itxn_field Amount + txn Accounts 1 + itxn_field Receiver + itxn_submit +`), + } + + fund := txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: basics.AppIndex(1).Address(), + Amount: 200000, // account min balance, plus fees + } + + payout1 := txntest.Txn{ + Type: "appl", + Sender: addrs[1], + ApplicationID: basics.AppIndex(1), + Accounts: []basics.Address{addrs[1]}, // pay self + } + + eval := nextBlock(t, l, true, nil) + txns(t, l, eval, &create, &fund) + vb := endBlock(t, l, eval) + require.Equal(t, 2, int(vb.Block().TxnCounter)) + + eval = nextBlock(t, l, true, nil) + txns(t, l, eval, &payout1) + vb = endBlock(t, l, eval) + require.Equal(t, 4, int(vb.Block().TxnCounter)) +} + +// TestAcfgAction ensures assets can be created and configured in teal +func TestAcfgAction(t *testing.T) { + partitiontest.PartitionTest(t) + + genBalances, addrs, _ := ledgertesting.NewTestGenesis() + l := newTestLedger(t, genBalances) + defer l.Close() + + appIndex := basics.AppIndex(1) + app := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` + itxn_begin + int acfg + itxn_field TypeEnum + + txn ApplicationArgs 0 + byte "create" + == + bz manager + int 1000000 + itxn_field ConfigAssetTotal + int 3 + itxn_field ConfigAssetDecimals + byte "oz" + itxn_field ConfigAssetUnitName + byte "Gold" + itxn_field ConfigAssetName + byte "https://gold.rush/" + itxn_field ConfigAssetURL + + global CurrentApplicationAddress + dup + dup2 + itxn_field ConfigAssetManager + itxn_field ConfigAssetReserve + itxn_field ConfigAssetFreeze + itxn_field ConfigAssetClawback + b submit +manager: + // Put the current values in the itxn + txn Assets 0 + asset_params_get AssetManager + assert // exists + itxn_field ConfigAssetManager + + txn Assets 0 + asset_params_get AssetReserve + assert // exists + itxn_field ConfigAssetReserve + + txn Assets 0 + asset_params_get AssetFreeze + assert // exists + itxn_field ConfigAssetFreeze + + txn Assets 0 + asset_params_get AssetClawback + assert // exists + itxn_field ConfigAssetClawback + + + txn ApplicationArgs 0 + byte "manager" + == + bz reserve + txn Assets 0 + itxn_field ConfigAsset + txn ApplicationArgs 1 + itxn_field ConfigAssetManager + b submit +reserve: + txn ApplicationArgs 0 + byte "reserve" + == + bz freeze + txn Assets 0 + itxn_field ConfigAsset + txn ApplicationArgs 1 + itxn_field ConfigAssetReserve + b submit +freeze: + txn ApplicationArgs 0 + byte "freeze" + == + bz clawback + txn Assets 0 + itxn_field ConfigAsset + txn ApplicationArgs 1 + itxn_field ConfigAssetFreeze + b submit +clawback: + txn ApplicationArgs 0 + byte "clawback" + == + bz error + txn Assets 0 + itxn_field ConfigAsset + txn ApplicationArgs 1 + itxn_field ConfigAssetClawback + b submit +error: err +submit: itxn_submit +`), + } + + fund := txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: appIndex.Address(), + Amount: 200_000, // exactly account min balance + one asset + } + + eval := nextBlock(t, l, true, nil) + txns(t, l, eval, &app, &fund) + endBlock(t, l, eval) + + createAsa := txntest.Txn{ + Type: "appl", + Sender: addrs[1], + ApplicationID: appIndex, + ApplicationArgs: [][]byte{[]byte("create")}, + } + + eval = nextBlock(t, l, true, nil) + // Can't create an asset if you have exactly 200,000 and need to pay fee + txn(t, l, eval, &createAsa, "balance 199000 below min 200000") + // fund it some more and try again + txns(t, l, eval, fund.Noted("more!"), &createAsa) + vb := endBlock(t, l, eval) + + asaIndex := vb.Block().Payset[1].EvalDelta.InnerTxns[0].ConfigAsset + require.Equal(t, basics.AssetIndex(5), asaIndex) + + asaParams, err := asaParams(t, l, basics.AssetIndex(5)) + require.NoError(t, err) + + require.Equal(t, 1_000_000, int(asaParams.Total)) + require.Equal(t, 3, int(asaParams.Decimals)) + require.Equal(t, "oz", asaParams.UnitName) + require.Equal(t, "Gold", asaParams.AssetName) + require.Equal(t, "https://gold.rush/", asaParams.URL) + + require.Equal(t, appIndex.Address(), asaParams.Manager) + + for _, a := range []string{"reserve", "freeze", "clawback", "manager"} { + check := txntest.Txn{ + Type: "appl", + Sender: addrs[1], + ApplicationID: appIndex, + ApplicationArgs: [][]byte{[]byte(a), []byte("junkjunkjunkjunkjunkjunkjunkjunk")}, + ForeignAssets: []basics.AssetIndex{asaIndex}, + } + eval = nextBlock(t, l, true, nil) + t.Log(a) + txn(t, l, eval, &check) + endBlock(t, l, eval) + } + // Not the manager anymore so this won't work + nodice := txntest.Txn{ + Type: "appl", + Sender: addrs[1], + ApplicationID: appIndex, + ApplicationArgs: [][]byte{[]byte("freeze"), []byte("junkjunkjunkjunkjunkjunkjunkjunk")}, + ForeignAssets: []basics.AssetIndex{asaIndex}, + } + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, &nodice, "this transaction should be issued by the manager") + endBlock(t, l, eval) + +} + +// TestAsaDuringInit ensures an ASA can be made while initilizing an +// app. In practice, this is impossible, because you would not be +// able to prefund the account - you don't know the app id. But here +// we can know, so it helps exercise txncounter changes. +func TestAsaDuringInit(t *testing.T) { + partitiontest.PartitionTest(t) + + genBalances, addrs, _ := ledgertesting.NewTestGenesis() + l := newTestLedger(t, genBalances) + defer l.Close() + + appIndex := basics.AppIndex(2) + prefund := txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: appIndex.Address(), + Amount: 300000, // plenty for min balances, fees + } + + app := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: ` + itxn_begin + int acfg + itxn_field TypeEnum + int 1000000 + itxn_field ConfigAssetTotal + byte "oz" + itxn_field ConfigAssetUnitName + byte "Gold" + itxn_field ConfigAssetName + itxn_submit + itxn CreatedAssetID + int 3 + == + assert + itxn CreatedApplicationID + int 0 + == + assert + itxn NumLogs + int 0 + == +`, + } + + eval := nextBlock(t, l, true, nil) + txns(t, l, eval, &prefund, &app) + vb := endBlock(t, l, eval) + + require.Equal(t, appIndex, vb.Block().Payset[1].ApplicationID) + + asaIndex := vb.Block().Payset[1].EvalDelta.InnerTxns[0].ConfigAsset + require.Equal(t, basics.AssetIndex(3), asaIndex) +} + +func TestRekey(t *testing.T) { + partitiontest.PartitionTest(t) + + genBalances, addrs, _ := ledgertesting.NewTestGenesis() + l := newTestLedger(t, genBalances) + defer l.Close() + + app := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` + itxn_begin + int pay + itxn_field TypeEnum + int 1 + itxn_field Amount + global CurrentApplicationAddress + itxn_field Receiver + int 31 + bzero + byte 0x01 + concat + itxn_field RekeyTo + itxn_submit +`), + } + + eval := nextBlock(t, l, true, nil) + txns(t, l, eval, &app) + vb := endBlock(t, l, eval) + appIndex := vb.Block().Payset[0].ApplicationID + require.Equal(t, basics.AppIndex(1), appIndex) + + fund := txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: appIndex.Address(), + Amount: 1_000_000, + } + rekey := txntest.Txn{ + Type: "appl", + Sender: addrs[1], + ApplicationID: appIndex, + } + eval = nextBlock(t, l, true, nil) + txns(t, l, eval, &fund, &rekey) + txn(t, l, eval, rekey.Noted("2"), "unauthorized") + endBlock(t, l, eval) + +} + +func TestNote(t *testing.T) { + partitiontest.PartitionTest(t) + + genBalances, addrs, _ := ledgertesting.NewTestGenesis() + l := newTestLedger(t, genBalances) + defer l.Close() + + app := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` + itxn_begin + int pay + itxn_field TypeEnum + int 0 + itxn_field Amount + global CurrentApplicationAddress + itxn_field Receiver + byte "abcdefghijklmnopqrstuvwxyz01234567890" + itxn_field Note + itxn_submit +`), + } + + eval := nextBlock(t, l, true, nil) + txns(t, l, eval, &app) + vb := endBlock(t, l, eval) + appIndex := vb.Block().Payset[0].ApplicationID + require.Equal(t, basics.AppIndex(1), appIndex) + + fund := txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: appIndex.Address(), + Amount: 1_000_000, + } + note := txntest.Txn{ + Type: "appl", + Sender: addrs[1], + ApplicationID: appIndex, + } + eval = nextBlock(t, l, true, nil) + txns(t, l, eval, &fund, ¬e) + vb = endBlock(t, l, eval) + alphabet := vb.Block().Payset[1].EvalDelta.InnerTxns[0].Txn.Note + require.Equal(t, "abcdefghijklmnopqrstuvwxyz01234567890", string(alphabet)) +} + +func TestKeyreg(t *testing.T) { + partitiontest.PartitionTest(t) + + genBalances, addrs, _ := ledgertesting.NewTestGenesis() + l := newTestLedger(t, genBalances) + defer l.Close() + + app := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` + txn ApplicationArgs 0 + byte "pay" + == + bz nonpart + itxn_begin + int pay + itxn_field TypeEnum + int 1 + itxn_field Amount + txn Sender + itxn_field Receiver + itxn_submit + int 1 + return +nonpart: + itxn_begin + int keyreg + itxn_field TypeEnum + int 1 + itxn_field Nonparticipation + itxn_submit +`), + } + + // Create the app + eval := nextBlock(t, l, true, nil) + txns(t, l, eval, &app) + vb := endBlock(t, l, eval) + appIndex := vb.Block().Payset[0].ApplicationID + require.Equal(t, basics.AppIndex(1), appIndex) + + // Give the app a lot of money + fund := txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: appIndex.Address(), + Amount: 1_000_000_000, + } + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, &fund) + endBlock(t, l, eval) + + require.Equal(t, 1_000_000_000, int(micros(t, l, appIndex.Address()))) + + // Build up Residue in RewardsState so it's ready to pay + for i := 1; i < 10; i++ { + eval := nextBlock(t, l, true, nil) + endBlock(t, l, eval) + } + + // pay a little + pay := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApplicationID: appIndex, + ApplicationArgs: [][]byte{[]byte("pay")}, + } + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, &pay) + endBlock(t, l, eval) + // 2000 was earned in rewards (- 1000 fee, -1 pay) + require.Equal(t, 1_000_000_999, int(micros(t, l, appIndex.Address()))) + + // Go nonpart + nonpart := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApplicationID: appIndex, + ApplicationArgs: [][]byte{[]byte("nonpart")}, + } + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, &nonpart) + endBlock(t, l, eval) + require.Equal(t, 999_999_999, int(micros(t, l, appIndex.Address()))) + + // Build up Residue in RewardsState so it's ready to pay AGAIN + // But expect no rewards + for i := 1; i < 100; i++ { + eval := nextBlock(t, l, true, nil) + endBlock(t, l, eval) + } + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, pay.Noted("again")) + txn(t, l, eval, nonpart.Noted("again"), "cannot change online/offline") + endBlock(t, l, eval) + // Paid fee + 1. Did not get rewards + require.Equal(t, 999_998_998, int(micros(t, l, appIndex.Address()))) +} + +func TestInnerAppCall(t *testing.T) { + partitiontest.PartitionTest(t) + + genBalances, addrs, _ := ledgertesting.NewTestGenesis() + l := newTestLedger(t, genBalances) + defer l.Close() + + app0 := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` + itxn_begin + int pay + itxn_field TypeEnum + int 1 + itxn_field Amount + txn Sender + itxn_field Receiver + itxn_submit +`), + } + eval := nextBlock(t, l, true, nil) + txn(t, l, eval, &app0) + vb := endBlock(t, l, eval) + index0 := vb.Block().Payset[0].ApplicationID + + app1 := txntest.Txn{ + Type: "appl", + Sender: addrs[1], + ApprovalProgram: main(` + itxn_begin + int appl + itxn_field TypeEnum + txn Applications 1 + itxn_field ApplicationID + itxn_submit +`), + } + + eval = nextBlock(t, l, true, nil) + txns(t, l, eval, &app1) + vb = endBlock(t, l, eval) + index1 := vb.Block().Payset[0].ApplicationID + + fund0 := txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: index0.Address(), + Amount: 1_000_000_000, + } + fund1 := fund0 + fund1.Receiver = index1.Address() + + call1 := txntest.Txn{ + Type: "appl", + Sender: addrs[2], + ApplicationID: index1, + ForeignApps: []basics.AppIndex{index0}, + } + eval = nextBlock(t, l, true, nil) + txns(t, l, eval, &fund0, &fund1, &call1) + endBlock(t, l, eval) + +} + +// TestInnerAppManipulate ensures that apps called from inner transactions make +// the changes expected when invoked. +func TestInnerAppManipulate(t *testing.T) { + partitiontest.PartitionTest(t) + + genBalances, addrs, _ := ledgertesting.NewTestGenesis() + l := newTestLedger(t, genBalances) + defer l.Close() + + calleeIndex := basics.AppIndex(1) + callee := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + // This app set a global key arg[1] to arg[2] or get arg[1] and log it + ApprovalProgram: main(` + txn ApplicationArgs 0 + byte "set" + == + bz next1 + txn ApplicationArgs 1 + txn ApplicationArgs 2 + app_global_put + b end +next1: + txn ApplicationArgs 0 + byte "get" + == + bz next2 + txn ApplicationArgs 1 + app_global_get + log // Fails if key didn't exist, b/c TOS = 0 + b end +next2: + err +`), + GlobalStateSchema: basics.StateSchema{ + NumByteSlice: 1, + }, + } + fund := txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: calleeIndex.Address(), + Amount: 1_000_000, + } + eval := nextBlock(t, l, true, nil) + txns(t, l, eval, &callee, &fund) + vb := endBlock(t, l, eval) + require.Equal(t, calleeIndex, vb.Block().Payset[0].ApplicationID) + + callerIndex := basics.AppIndex(3) + caller := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` + itxn_begin + int appl + itxn_field TypeEnum + txn Applications 1 + itxn_field ApplicationID + byte "set" + itxn_field ApplicationArgs + byte "X" + itxn_field ApplicationArgs + byte "A" + itxn_field ApplicationArgs + itxn_submit + itxn NumLogs + int 0 + == + assert + b end +`), + } + fund.Receiver = callerIndex.Address() + + eval = nextBlock(t, l, true, nil) + txns(t, l, eval, &caller, &fund) + vb = endBlock(t, l, eval) + require.Equal(t, callerIndex, vb.Block().Payset[0].ApplicationID) + + call := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApplicationID: callerIndex, + ForeignApps: []basics.AppIndex{calleeIndex}, + } + eval = nextBlock(t, l, true, nil) + txns(t, l, eval, &call) + vb = endBlock(t, l, eval) + tib := vb.Block().Payset[0] + // No changes in the top-level EvalDelta + require.Empty(t, tib.EvalDelta.GlobalDelta) + require.Empty(t, tib.EvalDelta.LocalDeltas) + + inner := tib.EvalDelta.InnerTxns[0] + require.Empty(t, inner.EvalDelta.LocalDeltas) + + require.Len(t, inner.EvalDelta.GlobalDelta, 1) + require.Equal(t, basics.ValueDelta{ + Action: basics.SetBytesAction, + Bytes: "A", + }, inner.EvalDelta.GlobalDelta["X"]) +} + +// TestCreateAndUse checks that an ASA can be created in an early tx, and then +// used in a later app call tx (in the same group). This was not allowed until +// v6, because of the strict adherence to the foreign-arrays rules. +func TestCreateAndUse(t *testing.T) { + genBalances, addrs, _ := ledgertesting.NewTestGenesis() + l := newTestLedger(t, genBalances) + defer l.Close() + + createapp := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` + itxn_begin + int axfer; itxn_field TypeEnum + int 0; itxn_field Amount + gaid 0; itxn_field XferAsset + global CurrentApplicationAddress; itxn_field Sender + global CurrentApplicationAddress; itxn_field AssetReceiver + itxn_submit +`), + } + appIndex := basics.AppIndex(1) + + fund := txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: appIndex.Address(), + Amount: 1_000_000, + } + + createasa := txntest.Txn{ + Type: "acfg", + Sender: addrs[0], + AssetParams: basics.AssetParams{ + Total: 1000000, + Decimals: 3, + UnitName: "oz", + AssetName: "Gold", + URL: "https://gold.rush/", + }, + } + asaIndex := basics.AssetIndex(3) + + use := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApplicationID: basics.AppIndex(1), + // The point of this test is to show the following (psychic) setting is unnecessary. + //ForeignAssets: []basics.AssetIndex{asaIndex}, + } + + eval := nextBlock(t, l, true, nil) + txn(t, l, eval, &createapp) + txn(t, l, eval, &fund) + err := txgroup(t, l, eval, &createasa, &use) + require.NoError(t, err) + vb := endBlock(t, l, eval) + + require.Equal(t, appIndex, vb.Block().Payset[0].ApplyData.ApplicationID) + require.Equal(t, asaIndex, vb.Block().Payset[2].ApplyData.ConfigAsset) +} + +func TestGtxnEffects(t *testing.T) { + genBalances, addrs, _ := ledgertesting.NewTestGenesis() + l := newTestLedger(t, genBalances) + defer l.Close() + + createapp := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` + gtxn 0 CreatedAssetID + int 3 + == + assert +`), + } + appIndex := basics.AppIndex(1) + + fund := txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: appIndex.Address(), + Amount: 1_000_000, + } + + eval := nextBlock(t, l, true, nil) + txns(t, l, eval, &createapp, &fund) + + createasa := txntest.Txn{ + Type: "acfg", + Sender: addrs[0], + AssetParams: basics.AssetParams{ + Total: 1000000, + Decimals: 3, + UnitName: "oz", + AssetName: "Gold", + URL: "https://gold.rush/", + }, + } + asaIndex := basics.AssetIndex(3) + + see := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApplicationID: basics.AppIndex(1), + } + + err := txgroup(t, l, eval, &createasa, &see) + require.NoError(t, err) + vb := endBlock(t, l, eval) + + require.Equal(t, appIndex, vb.Block().Payset[0].ApplyData.ApplicationID) + require.Equal(t, asaIndex, vb.Block().Payset[2].ApplyData.ConfigAsset) +} + +func TestBasicReentry(t *testing.T) { + partitiontest.PartitionTest(t) + + genBalances, addrs, _ := ledgertesting.NewTestGenesis() + l := newTestLedger(t, genBalances) + defer l.Close() + + app0 := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` + itxn_begin + int appl + itxn_field TypeEnum + txn Applications 1 + itxn_field ApplicationID + itxn_submit +`), + } + eval := nextBlock(t, l, true, nil) + txn(t, l, eval, &app0) + vb := endBlock(t, l, eval) + index0 := vb.Block().Payset[0].ApplicationID + + call1 := txntest.Txn{ + Type: "appl", + Sender: addrs[2], + ApplicationID: index0, + ForeignApps: []basics.AppIndex{index0}, + } + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, &call1, "self-call") + endBlock(t, l, eval) +} + +func TestIndirectReentry(t *testing.T) { + partitiontest.PartitionTest(t) + + genBalances, addrs, _ := ledgertesting.NewTestGenesis() + l := newTestLedger(t, genBalances) + defer l.Close() + + app0 := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` + itxn_begin + int appl + itxn_field TypeEnum + txn Applications 1 + itxn_field ApplicationID + txn Applications 2 + itxn_field Applications + itxn_submit +`), + } + eval := nextBlock(t, l, true, nil) + txn(t, l, eval, &app0) + vb := endBlock(t, l, eval) + index0 := vb.Block().Payset[0].ApplicationID + + fund := txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: index0.Address(), + Amount: 1_000_000, + } + + app1 := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` + itxn_begin + int appl + itxn_field TypeEnum + txn Applications 1 + itxn_field ApplicationID + itxn_submit +`), + } + eval = nextBlock(t, l, true, nil) + txns(t, l, eval, &app1, &fund) + vb = endBlock(t, l, eval) + index1 := vb.Block().Payset[0].ApplicationID + + call1 := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApplicationID: index0, + ForeignApps: []basics.AppIndex{index1, index0}, + } + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, &call1, "attempt to re-enter") + endBlock(t, l, eval) +} + +// This tests a valid form of reentry (which may not be the correct word here). +// When A calls B then returns to A then A calls C which calls B, the execution +// should not produce an error because B doesn't occur in the call stack twice. +func TestValidAppReentry(t *testing.T) { + partitiontest.PartitionTest(t) + + genBalances, addrs, _ := ledgertesting.NewTestGenesis() + l := newTestLedger(t, genBalances) + defer l.Close() + + app0 := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` + itxn_begin + int appl + itxn_field TypeEnum + txn Applications 2 + itxn_field ApplicationID + itxn_submit + + itxn_begin + int appl + itxn_field TypeEnum + txn Applications 1 + itxn_field ApplicationID + txn Applications 2 + itxn_field Applications + itxn_submit +`), + } + eval := nextBlock(t, l, true, nil) + txn(t, l, eval, &app0) + vb := endBlock(t, l, eval) + index0 := vb.Block().Payset[0].ApplicationID + + fund0 := txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: index0.Address(), + Amount: 1_000_000, + } + + app1 := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` + int 3 + int 3 + == + assert +`), + } + eval = nextBlock(t, l, true, nil) + txns(t, l, eval, &app1, &fund0) + vb = endBlock(t, l, eval) + index1 := vb.Block().Payset[0].ApplicationID + + app2 := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` + itxn_begin + int appl + itxn_field TypeEnum + txn Applications 1 + itxn_field ApplicationID + itxn_submit +`), + } + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, &app2) + vb = endBlock(t, l, eval) + index2 := vb.Block().Payset[0].ApplicationID + + fund2 := txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: index2.Address(), + Amount: 1_000_000, + } + + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, &fund2) + _ = endBlock(t, l, eval) + + call1 := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApplicationID: index0, + ForeignApps: []basics.AppIndex{index2, index1, index0}, + } + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, &call1) + endBlock(t, l, eval) +} + +func TestMaxInnerTxFanOut(t *testing.T) { + partitiontest.PartitionTest(t) + + genBalances, addrs, _ := ledgertesting.NewTestGenesis() + l := newTestLedger(t, genBalances) + defer l.Close() + + app0 := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` + itxn_begin + int appl + itxn_field TypeEnum + txn Applications 1 + itxn_field ApplicationID + itxn_submit + + itxn_begin + int appl + itxn_field TypeEnum + txn Applications 1 + itxn_field ApplicationID + itxn_submit + + itxn_begin + int appl + itxn_field TypeEnum + txn Applications 1 + itxn_field ApplicationID + itxn_submit + + itxn_begin + int appl + itxn_field TypeEnum + txn Applications 1 + itxn_field ApplicationID + itxn_submit + + itxn_begin + int appl + itxn_field TypeEnum + txn Applications 1 + itxn_field ApplicationID + itxn_submit + + itxn_begin + int appl + itxn_field TypeEnum + txn Applications 1 + itxn_field ApplicationID + itxn_submit + + itxn_begin + int appl + itxn_field TypeEnum + txn Applications 1 + itxn_field ApplicationID + itxn_submit + + itxn_begin + int appl + itxn_field TypeEnum + txn Applications 1 + itxn_field ApplicationID + itxn_submit + + itxn_begin + int appl + itxn_field TypeEnum + txn Applications 1 + itxn_field ApplicationID + itxn_submit + + itxn_begin + int appl + itxn_field TypeEnum + txn Applications 1 + itxn_field ApplicationID + itxn_submit + + itxn_begin + int appl + itxn_field TypeEnum + txn Applications 1 + itxn_field ApplicationID + itxn_submit + + itxn_begin + int appl + itxn_field TypeEnum + txn Applications 1 + itxn_field ApplicationID + itxn_submit + + itxn_begin + int appl + itxn_field TypeEnum + txn Applications 1 + itxn_field ApplicationID + itxn_submit + + itxn_begin + int appl + itxn_field TypeEnum + txn Applications 1 + itxn_field ApplicationID + itxn_submit + + itxn_begin + int appl + itxn_field TypeEnum + txn Applications 1 + itxn_field ApplicationID + itxn_submit + + itxn_begin + int appl + itxn_field TypeEnum + txn Applications 1 + itxn_field ApplicationID + itxn_submit +`), + } + eval := nextBlock(t, l, true, nil) + txn(t, l, eval, &app0) + vb := endBlock(t, l, eval) + index0 := vb.Block().Payset[0].ApplicationID + + fund0 := txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: index0.Address(), + Amount: 1_000_000, + } + + app1 := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` + int 3 + int 3 + == + assert +`), + } + eval = nextBlock(t, l, true, nil) + txns(t, l, eval, &app1, &fund0) + vb = endBlock(t, l, eval) + index1 := vb.Block().Payset[0].ApplicationID + + callTxGroup := make([]txntest.Txn, 16) + for i := 0; i < 16; i++ { + callTxGroup[i] = txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApplicationID: index0, + ForeignApps: []basics.AppIndex{index1}, + } + } + eval = nextBlock(t, l, true, nil) + err := txgroup(t, l, eval, &callTxGroup[0], &callTxGroup[1], &callTxGroup[2], &callTxGroup[3], &callTxGroup[4], &callTxGroup[5], &callTxGroup[6], &callTxGroup[7], &callTxGroup[8], &callTxGroup[9], &callTxGroup[10], &callTxGroup[11], &callTxGroup[12], &callTxGroup[13], &callTxGroup[14], &callTxGroup[15]) + require.NoError(t, err) + + endBlock(t, l, eval) +} + +func TestExceedMaxInnerTxFanOut(t *testing.T) { + partitiontest.PartitionTest(t) + + genBalances, addrs, _ := ledgertesting.NewTestGenesis() + l := newTestLedger(t, genBalances) + defer l.Close() + + var program string + for i := 0; i < 17; i++ { + program += ` + itxn_begin + int appl + itxn_field TypeEnum + txn Applications 1 + itxn_field ApplicationID + itxn_submit +` + } + + // 17 inner txns + app0 := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(program), + } + eval := nextBlock(t, l, true, nil) + txn(t, l, eval, &app0) + vb := endBlock(t, l, eval) + index0 := vb.Block().Payset[0].ApplicationID + + fund0 := txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: index0.Address(), + Amount: 1_000_000, + } + + app1 := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` + int 3 + int 3 + == + assert +`), + } + eval = nextBlock(t, l, true, nil) + txns(t, l, eval, &app1, &fund0) + vb = endBlock(t, l, eval) + index1 := vb.Block().Payset[0].ApplicationID + + callTxGroup := make([]txntest.Txn, 16) + for i := 0; i < 16; i++ { + callTxGroup[i] = txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApplicationID: index0, + ForeignApps: []basics.AppIndex{index1}, + } + } + eval = nextBlock(t, l, true, nil) + err := txgroup(t, l, eval, &callTxGroup[0], &callTxGroup[1], &callTxGroup[2], &callTxGroup[3], &callTxGroup[4], &callTxGroup[5], &callTxGroup[6], &callTxGroup[7], &callTxGroup[8], &callTxGroup[9], &callTxGroup[10], &callTxGroup[11], &callTxGroup[12], &callTxGroup[13], &callTxGroup[14], &callTxGroup[15]) + require.Error(t, err) + + endBlock(t, l, eval) +} + +func TestMaxInnerTxForSingleAppCall(t *testing.T) { + partitiontest.PartitionTest(t) + + genBalances, addrs, _ := ledgertesting.NewTestGenesis() + l := newTestLedger(t, genBalances) + defer l.Close() + + program := ` +int 1 +loop: +itxn_begin + int appl + itxn_field TypeEnum + txn Applications 1 + itxn_field ApplicationID +itxn_submit +int 1 ++ +dup +int 256 +<= +bnz loop +int 257 +== +assert +` + + // 256 inner txns + app0 := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(program), + } + eval := nextBlock(t, l, true, nil) + txn(t, l, eval, &app0) + vb := endBlock(t, l, eval) + index0 := vb.Block().Payset[0].ApplicationID + + fund0 := txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: index0.Address(), + Amount: 1_000_000, + } + + app1 := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` + int 3 + int 3 + == + assert +`), + } + eval = nextBlock(t, l, true, nil) + txns(t, l, eval, &app1, &fund0) + vb = endBlock(t, l, eval) + index1 := vb.Block().Payset[0].ApplicationID + + callTxGroup := make([]txntest.Txn, 16) + callTxGroup[0] = txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApplicationID: index0, + ForeignApps: []basics.AppIndex{index1}, + } + for i := 1; i < 16; i++ { + callTxGroup[i] = txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApplicationID: index1, + ForeignApps: []basics.AppIndex{}, + } + } + eval = nextBlock(t, l, true, nil) + err := txgroup(t, l, eval, &callTxGroup[0], &callTxGroup[1], &callTxGroup[2], &callTxGroup[3], &callTxGroup[4], &callTxGroup[5], &callTxGroup[6], &callTxGroup[7], &callTxGroup[8], &callTxGroup[9], &callTxGroup[10], &callTxGroup[11], &callTxGroup[12], &callTxGroup[13], &callTxGroup[14], &callTxGroup[15]) + require.NoError(t, err) + + endBlock(t, l, eval) +} + +func TestExceedMaxInnerTxForSingleAppCall(t *testing.T) { + partitiontest.PartitionTest(t) + + genBalances, addrs, _ := ledgertesting.NewTestGenesis() + l := newTestLedger(t, genBalances) + defer l.Close() + + program := ` +int 1 +loop: +itxn_begin + int appl + itxn_field TypeEnum + txn Applications 1 + itxn_field ApplicationID +itxn_submit +int 1 ++ +dup +int 257 +<= +bnz loop +int 258 +== +assert +` + + // 257 inner txns + app0 := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(program), + } + eval := nextBlock(t, l, true, nil) + txn(t, l, eval, &app0) + vb := endBlock(t, l, eval) + index0 := vb.Block().Payset[0].ApplicationID + + fund0 := txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: index0.Address(), + Amount: 1_000_000, + } + + app1 := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` + int 3 + int 3 + == + assert +`), + } + eval = nextBlock(t, l, true, nil) + txns(t, l, eval, &app1, &fund0) + vb = endBlock(t, l, eval) + index1 := vb.Block().Payset[0].ApplicationID + + callTxGroup := make([]txntest.Txn, 16) + callTxGroup[0] = txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApplicationID: index0, + ForeignApps: []basics.AppIndex{index1}, + } + for i := 1; i < 16; i++ { + callTxGroup[i] = txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApplicationID: index1, + ForeignApps: []basics.AppIndex{}, + } + } + eval = nextBlock(t, l, true, nil) + err := txgroup(t, l, eval, &callTxGroup[0], &callTxGroup[1], &callTxGroup[2], &callTxGroup[3], &callTxGroup[4], &callTxGroup[5], &callTxGroup[6], &callTxGroup[7], &callTxGroup[8], &callTxGroup[9], &callTxGroup[10], &callTxGroup[11], &callTxGroup[12], &callTxGroup[13], &callTxGroup[14], &callTxGroup[15]) + require.Error(t, err) + + endBlock(t, l, eval) +} + +func TestAbortWhenInnerAppCallFails(t *testing.T) { + partitiontest.PartitionTest(t) + + genBalances, addrs, _ := ledgertesting.NewTestGenesis() + l := newTestLedger(t, genBalances) + defer l.Close() + + app0 := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` +itxn_begin + int appl + itxn_field TypeEnum + txn Applications 1 + itxn_field ApplicationID +itxn_submit +int 1 +int 1 +== +assert +`), + } + eval := nextBlock(t, l, true, nil) + txn(t, l, eval, &app0) + vb := endBlock(t, l, eval) + index0 := vb.Block().Payset[0].ApplicationID + + fund0 := txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: index0.Address(), + Amount: 1_000_000, + } + + app1 := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` + int 3 + int 2 + == + assert +`), + } + eval = nextBlock(t, l, true, nil) + txns(t, l, eval, &app1, &fund0) + vb = endBlock(t, l, eval) + index1 := vb.Block().Payset[0].ApplicationID + + callTx := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApplicationID: index0, + ForeignApps: []basics.AppIndex{index1}, + } + + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, &callTx, "logic eval error") + endBlock(t, l, eval) +} + +func TestCreatedAppsAreAccessible(t *testing.T) { + partitiontest.PartitionTest(t) + + genBalances, addrs, _ := ledgertesting.NewTestGenesis() + l := newTestLedger(t, genBalances) + defer l.Close() + + ops, err := logic.AssembleStringWithVersion("int 1\nint 1\nassert", logic.AssemblerMaxVersion) + require.NoError(t, err) + program := "byte 0x" + hex.EncodeToString(ops.Program) + + createapp := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` + itxn_begin + int appl; itxn_field TypeEnum + ` + program + `; itxn_field ApprovalProgram + ` + program + `; itxn_field ClearStateProgram + int 1; itxn_field GlobalNumUint + int 2; itxn_field LocalNumByteSlice + int 3; itxn_field LocalNumUint + itxn_submit`), + } + + eval := nextBlock(t, l, true, nil) + txn(t, l, eval, &createapp) + vb := endBlock(t, l, eval) + index0 := vb.Block().Payset[0].ApplicationID + + fund0 := txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: index0.Address(), + Amount: 1_000_000, + } + + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, &fund0) + endBlock(t, l, eval) + + callTx := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApplicationID: index0, + ForeignApps: []basics.AppIndex{}, + } + + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, &callTx) + endBlock(t, l, eval) + index1 := basics.AppIndex(1) + + callTx = txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApplicationID: index1, + ForeignApps: []basics.AppIndex{}, + } + + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, &callTx) + endBlock(t, l, eval) +} + +func TestInvalidAppsNotAccessible(t *testing.T) { + partitiontest.PartitionTest(t) + + genBalances, addrs, _ := ledgertesting.NewTestGenesis() + l := newTestLedger(t, genBalances) + defer l.Close() + + app0 := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` +itxn_begin + int appl + itxn_field TypeEnum + int 2 + itxn_field ApplicationID +itxn_submit`), + } + eval := nextBlock(t, l, true, nil) + txn(t, l, eval, &app0) + vb := endBlock(t, l, eval) + index0 := vb.Block().Payset[0].ApplicationID + + fund0 := txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: index0.Address(), + Amount: 1_000_000, + } + + app1 := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` +int 2 +int 2 +== +assert +`), + } + eval = nextBlock(t, l, true, nil) + txns(t, l, eval, &app1, &fund0) + endBlock(t, l, eval) + + callTx := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApplicationID: index0, + ForeignApps: []basics.AppIndex{}, + } + + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, &callTx, "invalid App reference 2") + endBlock(t, l, eval) +} + +func TestInvalidAssetsNotAccessible(t *testing.T) { + partitiontest.PartitionTest(t) + + genBalances, addrs, _ := ledgertesting.NewTestGenesis() + l := newTestLedger(t, genBalances) + defer l.Close() + + createapp := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: main(` + itxn_begin + int axfer; itxn_field TypeEnum + int 0; itxn_field Amount + int 3; itxn_field XferAsset + global CurrentApplicationAddress; itxn_field Sender + global CurrentApplicationAddress; itxn_field AssetReceiver + itxn_submit +`), + } + appIndex := basics.AppIndex(1) + + fund := txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: appIndex.Address(), + Amount: 1_000_000, + } + + createasa := txntest.Txn{ + Type: "acfg", + Sender: addrs[0], + AssetParams: basics.AssetParams{ + Total: 1000000, + Decimals: 3, + UnitName: "oz", + AssetName: "Gold", + URL: "https://gold.rush/", + }, + } + + eval := nextBlock(t, l, true, nil) + txns(t, l, eval, &createapp, &fund, &createasa) + endBlock(t, l, eval) + + use := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApplicationID: basics.AppIndex(1), + } + + eval = nextBlock(t, l, true, nil) + txn(t, l, eval, &use, "invalid Asset reference 3") + endBlock(t, l, eval) +} + +func executeMegaContract(b *testing.B) { + genBalances, addrs, _ := ledgertesting.NewTestGenesis() + + vTest := config.Consensus[protocol.ConsensusFuture] + vTest.MaxAppProgramCost = 20000 + var cv protocol.ConsensusVersion = "temp test" + config.Consensus[cv] = vTest + + l := newTestLedgerWithConsensusVersion(b, genBalances, cv) + defer l.Close() + defer delete(config.Consensus, cv) + + // app must use maximum memory then recursively create a new app with the same approval program. + // recursion is terminated when a depth of 256 is reached + // fill scratch space + // fill stack + depth := 255 + createapp := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: ` + int 0 + loop: + dup + int 4096 + bzero + stores + int 1 + + + dup + int 256 + < + bnz loop + pop + int 0 + loop2: + int 4096 + bzero + swap + int 1 + + + dup + int 994 + <= + bnz loop2 + txna ApplicationArgs 0 + btoi + int 1 + - + dup + int 0 + <= + bnz done + itxn_begin + itob + itxn_field ApplicationArgs + int appl + itxn_field TypeEnum + txn ApprovalProgram + itxn_field ApprovalProgram + txn ClearStateProgram + itxn_field ClearStateProgram + itxn_submit + done: + int 1 + return`, + ApplicationArgs: [][]byte{{byte(depth)}}, + ExtraProgramPages: 3, + } + + funds := make([]*txntest.Txn, 256) + for i := 257; i <= 2*256; i++ { + funds[i-257] = &txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: basics.AppIndex(i).Address(), + Amount: 1_000_000, + } + } + + eval := nextBlock(b, l, true, nil) + txns(b, l, eval, funds...) + endBlock(b, l, eval) + + app1 := txntest.Txn{ + Type: "appl", + Sender: addrs[0], + ApprovalProgram: `int 1`, + } + + eval = nextBlock(b, l, true, nil) + err := txgroup(b, l, eval, &createapp, &app1, &app1, &app1, &app1, &app1, &app1) + require.NoError(b, err) + endBlock(b, l, eval) +} + +func BenchmarkMaximumCallStackDepth(b *testing.B) { + for i := 0; i < b.N; i++ { + executeMegaContract(b) + } +} |