diff --git a/data/basics/userBalance.go b/data/basics/userBalance.go index cec1534df2..f10c326354 100644 --- a/data/basics/userBalance.go +++ b/data/basics/userBalance.go @@ -343,20 +343,6 @@ type CreatableLocator struct { Index CreatableIndex } -// HoldingLocator stores enough information to name a "Holding" the balance of -// an ASA for a particular account. -type HoldingLocator struct { - Address Address - Index AssetIndex -} - -// LocalsLocator stores enough information to find the Local State that an -// account has with a particular app. -type LocalsLocator struct { - Address Address - Index AppIndex -} - // AssetHolding describes an asset held by an account. type AssetHolding struct { _struct struct{} `codec:",omitempty,omitemptyarray"` diff --git a/data/transactions/logic/blackbox_test.go b/data/transactions/logic/blackbox_test.go index 865ffad1ad..79029f2c38 100644 --- a/data/transactions/logic/blackbox_test.go +++ b/data/transactions/logic/blackbox_test.go @@ -97,248 +97,3 @@ func TestNewAppEvalParams(t *testing.T) { } } } - -// TestAppSharing confirms that as of v9, assets can be accessed across -// groups, but that before then, they could not. -func TestAppSharing(t *testing.T) { - partitiontest.PartitionTest(t) - t.Parallel() - - // Create some sample transactions. The main reason this a blackbox test - // (_test package) is to have access to txntest. - appl0 := txntest.Txn{ - Type: protocol.ApplicationCallTx, - Sender: basics.Address{1, 2, 3, 4}, - ForeignApps: []basics.AppIndex{500}, - } - - appl1 := txntest.Txn{ - Type: protocol.ApplicationCallTx, - Sender: basics.Address{4, 3, 2, 1}, - } - - appl2 := txntest.Txn{ - Type: protocol.ApplicationCallTx, - Sender: basics.Address{1, 2, 3, 4}, - } - - getSchema := ` -int 500 -app_params_get AppGlobalNumByteSlice -!; assert; pop; int 1 -` - sources := []string{getSchema, getSchema} - // In v8, the first tx can read app params of 500, because it's in its - // foreign array, but the second can't - logic.TestApps(t, sources, txntest.SignedTxns(&appl0, &appl1), 8, nil, - logic.NewExpect(1, "invalid App reference 500")) - // In v9, the second can, because the first can. - logic.TestApps(t, sources, txntest.SignedTxns(&appl0, &appl1), 9, nil) - - getLocalEx := ` -int 0 // Sender -int 500 -byte "some-key" -app_local_get_ex -pop; pop; int 1 -` - - sources = []string{getLocalEx, getLocalEx} - // In contrast, here there's no help from v9, because the second tx is - // reading the locals for a different account. - - // app_local_get* requires the address and the app exist, else the program fails - logic.TestApps(t, sources, txntest.SignedTxns(&appl0, &appl1), 8, nil, - logic.NewExpect(0, "no account")) - - _, _, ledger := logic.MakeSampleEnv() - ledger.NewAccount(appl0.Sender, 100_000) - ledger.NewApp(appl0.Sender, 500, basics.AppParams{}) - ledger.NewLocals(appl0.Sender, 500) // opt in - // Now txn0 passes, but txn1 has an error because it can't see app 500 - logic.TestApps(t, sources, txntest.SignedTxns(&appl0, &appl1), 9, ledger, - logic.NewExpect(1, "invalid Local State access")) - - // But it's ok in appl2, because appl2 uses the same Sender, even though the - // foreign-app is not repeated in appl2 so the holding being accessed is is - // the one from tx0. - logic.TestApps(t, sources, txntest.SignedTxns(&appl0, &appl2), 9, ledger) -} - -// TestAssetSharing confirms that as of v9, assets can be accessed across -// groups, but that before then, they could not. -func TestAssetSharing(t *testing.T) { - partitiontest.PartitionTest(t) - t.Parallel() - - // Create some sample transactions. The main reason this a blackbox test - // (_test package) is to have access to txntest. - appl0 := txntest.Txn{ - Type: protocol.ApplicationCallTx, - Sender: basics.Address{1, 2, 3, 4}, - ForeignAssets: []basics.AssetIndex{400}, - } - - appl1 := txntest.Txn{ - Type: protocol.ApplicationCallTx, - Sender: basics.Address{4, 3, 2, 1}, - } - - appl2 := txntest.Txn{ - Type: protocol.ApplicationCallTx, - Sender: basics.Address{1, 2, 3, 4}, - } - - getTotal := ` -int 400 -asset_params_get AssetTotal -pop; pop; int 1 -` - sources := []string{getTotal, getTotal} - // In v8, the first tx can read asset 400, because it's in its foreign arry, - // but the second can't - logic.TestApps(t, sources, txntest.SignedTxns(&appl0, &appl1), 8, nil, - logic.NewExpect(1, "invalid Asset reference 400")) - // In v9, the second can, because the first can. - logic.TestApps(t, sources, txntest.SignedTxns(&appl0, &appl1), 9, nil) - - getBalance := ` -int 0 -int 400 -asset_holding_get AssetBalance -pop; pop; int 1 -` - - sources = []string{getBalance, getBalance} - // In contrast, here there's no help from v9, because the second tx is - // reading a holding for a different account. - logic.TestApps(t, sources, txntest.SignedTxns(&appl0, &appl1), 8, nil, - logic.NewExpect(1, "invalid Asset reference 400")) - logic.TestApps(t, sources, txntest.SignedTxns(&appl0, &appl1), 9, nil, - logic.NewExpect(1, "invalid Holding access")) - // But it's ok in appl2, because the same account is used, even though the - // foreign-asset is not repeated in appl2. - logic.TestApps(t, sources, txntest.SignedTxns(&appl0, &appl2), 9, nil) -} - -// TestOtherTxSharing tests resource sharing across other kinds of transactions besides appl. -func TestOtherTxSharing(t *testing.T) { - partitiontest.PartitionTest(t) - t.Parallel() - - _, _, ledger := logic.MakeSampleEnv() - - senderAcct := basics.Address{1, 2, 3, 4, 5, 6, 1} - ledger.NewAccount(senderAcct, 2001) - senderBalance := "txn ApplicationArgs 0; balance; int 2001; ==" - - receiverAcct := basics.Address{1, 2, 3, 4, 5, 6, 2} - ledger.NewAccount(receiverAcct, 2002) - receiverBalance := "txn ApplicationArgs 0; balance; int 2002; ==" - - otherAcct := basics.Address{1, 2, 3, 4, 5, 6, 3} - ledger.NewAccount(otherAcct, 2003) - otherBalance := "txn ApplicationArgs 0; balance; int 2003; ==" - - appl := txntest.Txn{ - Type: protocol.ApplicationCallTx, - ApplicationArgs: [][]byte{senderAcct[:]}, - } - - keyreg := txntest.Txn{ - Type: protocol.KeyRegistrationTx, - Sender: senderAcct, - } - pay := txntest.Txn{ - Type: protocol.PaymentTx, - Sender: senderAcct, - Receiver: receiverAcct, - } - acfg := txntest.Txn{ - Type: protocol.AssetConfigTx, - Sender: senderAcct, - } - axfer := txntest.Txn{ - Type: protocol.AssetTransferTx, - XferAsset: 100, // must be < 256, later code assumes it fits in a byte - Sender: senderAcct, - AssetReceiver: receiverAcct, - AssetSender: otherAcct, - } - afrz := txntest.Txn{ - Type: protocol.AssetFreezeTx, - Sender: senderAcct, - FreezeAccount: otherAcct, - } - - sources := []string{"", senderBalance} - rsources := []string{senderBalance, ""} - for _, send := range []txntest.Txn{keyreg, pay, acfg, axfer, afrz} { - logic.TestApps(t, sources, txntest.SignedTxns(&send, &appl), 9, ledger) - logic.TestApps(t, rsources, txntest.SignedTxns(&appl, &send), 9, ledger) - - logic.TestApps(t, sources, txntest.SignedTxns(&send, &appl), 8, ledger, - logic.NewExpect(1, "invalid Account reference")) - logic.TestApps(t, rsources, txntest.SignedTxns(&appl, &send), 8, ledger, - logic.NewExpect(0, "invalid Account reference")) - } - - holdingAccess := ` - txn ApplicationArgs 0 - txn ApplicationArgs 1; btoi - asset_holding_get AssetBalance - pop; pop -` - sources = []string{"", holdingAccess} - rsources = []string{holdingAccess, ""} - - t.Run("keyreg", func(t *testing.T) { - appl.ApplicationArgs = [][]byte{senderAcct[:], {200}} - logic.TestApps(t, sources, txntest.SignedTxns(&keyreg, &appl), 9, ledger, - logic.NewExpect(1, "invalid Asset reference 200")) - withRef := appl - withRef.ForeignAssets = []basics.AssetIndex{200} - logic.TestApps(t, sources, txntest.SignedTxns(&keyreg, &withRef), 9, ledger, - logic.NewExpect(1, "invalid Holding access "+senderAcct.String())) - }) - t.Run("pay", func(t *testing.T) { - // The receiver is available for algo balance reading - appl.ApplicationArgs = [][]byte{receiverAcct[:]} - logic.TestApps(t, []string{"", receiverBalance}, txntest.SignedTxns(&pay, &appl), 9, ledger) - - // The other account is not (it's not even in the pay txn) - appl.ApplicationArgs = [][]byte{otherAcct[:]} - logic.TestApps(t, []string{"", otherBalance}, txntest.SignedTxns(&pay, &appl), 9, ledger, - logic.NewExpect(1, "invalid Account reference "+otherAcct.String())) - - // The other account becomes accessible because used in CloseRemainderTo - withClose := pay - withClose.CloseRemainderTo = otherAcct - logic.TestApps(t, []string{"", otherBalance}, txntest.SignedTxns(&withClose, &appl), 9, ledger) - }) - - t.Run("acfg", func(t *testing.T) { - }) - - t.Run("axfer", func(t *testing.T) { - // The receiver is NOT available for algo balance reading (only the holding for the asa) - appl.ApplicationArgs = [][]byte{receiverAcct[:]} - logic.TestApps(t, []string{"", receiverBalance}, txntest.SignedTxns(&axfer, &appl), 9, ledger, - logic.NewExpect(1, "invalid Account reference "+receiverAcct.String())) - - appl.ApplicationArgs = [][]byte{receiverAcct[:], {byte(axfer.XferAsset)}} - /* - logic.TestApps(t, []string{"", holdingAccess}, txntest.SignedTxns(&axfer, &appl), 9, ledger) - - // The other account becomes accessible because used in CloseRemainderTo (for asa, not algo) - withClose := axfer - withClose.AssetCloseTo = otherAcct - logic.TestApps(t, []string{"", otherBalance}, txntest.SignedTxns(&withClose, &appl), 9, ledger, - logic.NewExpect(1, "bad")) - logic.TestApps(t, []string{"", holdingAccess}, txntest.SignedTxns(&withClose, &appl), 9, ledger) - */ - }) - - t.Run("afrz", func(t *testing.T) { - }) -} diff --git a/data/transactions/logic/eval.go b/data/transactions/logic/eval.go index 27d85344be..82c97f0b39 100644 --- a/data/transactions/logic/eval.go +++ b/data/transactions/logic/eval.go @@ -380,26 +380,12 @@ func (ep *EvalParams) computeAvailability() *resources { sharedAccounts: make(map[basics.Address]struct{}), sharedAsas: make(map[basics.AssetIndex]struct{}), sharedApps: make(map[basics.AppIndex]struct{}), - sharedHoldings: make(map[basics.HoldingLocator]struct{}), - sharedLocals: make(map[basics.LocalsLocator]struct{}), + sharedHoldings: make(map[ledgercore.AccountAsset]struct{}), + sharedLocals: make(map[ledgercore.AccountApp]struct{}), boxes: make(map[boxRef]bool), } for i := range ep.TxnGroup { - tx := &ep.TxnGroup[i] - switch tx.Txn.Type { - case protocol.PaymentTx: - available.fillPayment(&tx.Txn.Header, &tx.Txn.PaymentTxnFields) - case protocol.KeyRegistrationTx: - available.fillKeyRegistration(&tx.Txn.Header) - case protocol.AssetConfigTx: - available.fillAssetConfig(&tx.Txn.Header, &tx.Txn.AssetConfigTxnFields) - case protocol.AssetTransferTx: - available.fillAssetTransfer(&tx.Txn.Header, &tx.Txn.AssetTransferTxnFields) - case protocol.AssetFreezeTx: - available.fillAssetFreeze(&tx.Txn.Header, &tx.Txn.AssetFreezeTxnFields) - case protocol.ApplicationCallTx: - available.fillApplicationCall(ep, &tx.Txn.Header, &tx.Txn.ApplicationCallTxnFields) - } + available.fill(&ep.TxnGroup[i].Txn, ep) } return available } @@ -4433,22 +4419,59 @@ func (cx *EvalContext) assetReference(ref uint64, foreign bool) (basics.AssetInd } -func (cx *EvalContext) holdingReference(account stackValue, ref uint64) (addr basics.Address, asset basics.AssetIndex, err error) { - addr, _, err = cx.accountReference(account) - if err != nil { - return +func (cx *EvalContext) holdingReference(account stackValue, ref uint64) (basics.Address, basics.AssetIndex, error) { + if cx.version >= resourceSharingVersion { + var addr basics.Address + var err error + if account.Bytes != nil { + addr, err = account.address() + } else { + addr, err = cx.txn.Txn.AddressByIndex(account.Uint, cx.txn.Txn.Sender) + } + if err != nil { + return basics.Address{}, basics.AssetIndex(0), err + } + aid := basics.AssetIndex(ref) + if cx.availableHolding(addr, aid) { + return addr, aid, nil + } + if ref < uint64(len(cx.txn.Txn.ForeignAssets)) { + aid := cx.txn.Txn.ForeignAssets[ref] + if cx.availableHolding(addr, aid) { + return addr, aid, nil + } + } + + // Do some extra lookups to give a more concise err. Whenever a holding + // is available, its account and asset must be as well (but not vice + // versa, anymore). So, if (only) one of them is not available, yell + // about it, specifically. + + _, _, acctErr := cx.accountReference(account) + _, assetErr := cx.assetReference(ref, false) + switch { + case acctErr != nil && assetErr == nil: + err = acctErr + case acctErr == nil && assetErr != nil: + err = assetErr + default: + err = fmt.Errorf("invalid Holding access %s x %d", addr, aid) + } + + return basics.Address{}, basics.AssetIndex(0), err } - asset, err = cx.assetReference(ref, false) + + // Pre group resource sharing, the rule is just that account and asset are + // each available. + addr, _, err := cx.accountReference(account) if err != nil { - return + return basics.Address{}, basics.AssetIndex(0), err } - - if !cx.availableHolding(addr, asset) { - err = fmt.Errorf("invalid Holding access %s x %d", addr, asset) - return + asset, err := cx.assetReference(ref, false) + if err != nil { + return basics.Address{}, basics.AssetIndex(0), err } - - return + return addr, asset, nil } func opAssetHoldingGet(cx *EvalContext) error { @@ -4803,15 +4826,9 @@ func (cx *EvalContext) availableApp(aid basics.AppIndex) bool { return false } -// availableHolding is an additional check, required since -// resourceSharingVersion. It will work in previous versions too, since -// cx.available will be populated to make it work. But it must protect for < -// directRefEnabledVersion, because unnamed holdings could be read then. +// availableHolding checks if a holding is available under the txgroup sharing rules func (cx *EvalContext) availableHolding(addr basics.Address, ai basics.AssetIndex) bool { - if cx.version < directRefEnabledVersion { - return true - } - if _, ok := cx.available.sharedHoldings[basics.HoldingLocator{Address: addr, Index: ai}]; ok { + if _, ok := cx.available.sharedHoldings[ledgercore.AccountAsset{Address: addr, Asset: ai}]; ok { return true } // All holdings of created assets are available @@ -4831,7 +4848,7 @@ func (cx *EvalContext) availableLocals(addr basics.Address, ai basics.AppIndex) if cx.version < directRefEnabledVersion { return true } - if _, ok := cx.available.sharedLocals[basics.LocalsLocator{Address: addr, Index: ai}]; ok { + if _, ok := cx.available.sharedLocals[ledgercore.AccountApp{Address: addr, App: ai}]; ok { return true } // All locals of created apps are available diff --git a/data/transactions/logic/evalStateful_test.go b/data/transactions/logic/evalStateful_test.go index 4c95d9e603..24adde370e 100644 --- a/data/transactions/logic/evalStateful_test.go +++ b/data/transactions/logic/evalStateful_test.go @@ -101,13 +101,13 @@ func (r *resources) String() string { if len(r.sharedHoldings) > 0 { fmt.Fprintf(&sb, "sharedHoldings:\n") for hl := range r.sharedHoldings { - fmt.Fprintf(&sb, " %s x %d\n", hl.Address, hl.Index) + fmt.Fprintf(&sb, " %s x %d\n", hl.Address, hl.Asset) } } if len(r.sharedLocals) > 0 { fmt.Fprintf(&sb, "sharedLocals:\n") for hl := range r.sharedLocals { - fmt.Fprintf(&sb, " %s x %d\n", hl.Address, hl.Index) + fmt.Fprintf(&sb, " %s x %d\n", hl.Address, hl.App) } } @@ -411,18 +411,22 @@ func testApps(t *testing.T, programs []string, txgroup []transactions.SignedTxn, func testAppsBytes(t *testing.T, programs [][]byte, ep *EvalParams, expected ...Expect) { t.Helper() - require.Equal(t, len(programs), len(ep.TxnGroup)) + require.LessOrEqual(t, len(programs), len(ep.TxnGroup)) for i := range ep.TxnGroup { - if programs[i] != nil { + program := ep.TxnGroup[i].Txn.ApprovalProgram + if len(programs) > i && programs[i] != nil { + program = programs[i] + } + if program != nil { appID := ep.TxnGroup[i].Txn.ApplicationID if appID == 0 { appID = basics.AppIndex(888) } if len(expected) > 0 && expected[0].l == i { - testAppFull(t, programs[i], i, appID, ep, expected[0].s) + testAppFull(t, program, i, appID, ep, expected[0].s) break // Stop after first failure } else { - testAppFull(t, programs[i], i, appID, ep) + testAppFull(t, program, i, appID, ep) } } } diff --git a/data/transactions/logic/resources.go b/data/transactions/logic/resources.go index e8578b1c37..3658ab1fcb 100644 --- a/data/transactions/logic/resources.go +++ b/data/transactions/logic/resources.go @@ -19,6 +19,8 @@ package logic import ( "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/ledger/ledgercore" + "github.com/algorand/go-algorand/protocol" ) // resources contains a catalog of available resources. It's used to track the @@ -40,8 +42,8 @@ type resources struct { // We need to carefully track the "cross-product" availability, because if // tx0 mentions an account A, and tx1 mentions an ASA X, that does _not_ // make the holding AX available - sharedHoldings map[basics.HoldingLocator]struct{} - sharedLocals map[basics.LocalsLocator]struct{} + sharedHoldings map[ledgercore.AccountAsset]struct{} + sharedLocals map[ledgercore.AccountApp]struct{} // boxes are all of the top-level box refs from the txgroup. Most are added // during NewEvalParams(). refs using 0 on an appl create are resolved and @@ -56,17 +58,41 @@ type resources struct { } func (r *resources) shareHolding(addr basics.Address, id basics.AssetIndex) { - r.sharedHoldings[basics.HoldingLocator{Address: addr, Index: id}] = struct{}{} + r.sharedHoldings[ledgercore.AccountAsset{Address: addr, Asset: id}] = struct{}{} +} + +func (r *resources) shareAccountAndHolding(addr basics.Address, id basics.AssetIndex) { + r.sharedAccounts[addr] = struct{}{} + if id != 0 { + r.sharedHoldings[ledgercore.AccountAsset{Address: addr, Asset: id}] = struct{}{} + } } func (r *resources) shareLocal(addr basics.Address, id basics.AppIndex) { - r.sharedLocals[basics.LocalsLocator{Address: addr, Index: id}] = struct{}{} + r.sharedLocals[ledgercore.AccountApp{Address: addr, App: id}] = struct{}{} } -// In the fill routines, we pass the header and the fields in separately, even +// In the fill* routines, we pass the header and the fields in separately, even // though they are pointers into the same structure. That prevents dumb attempts // to use other fields from the transaction. +func (r *resources) fill(tx *transactions.Transaction, ep *EvalParams) { + switch tx.Type { + case protocol.PaymentTx: + r.fillPayment(&tx.Header, &tx.PaymentTxnFields) + case protocol.KeyRegistrationTx: + r.fillKeyRegistration(&tx.Header) + case protocol.AssetConfigTx: + r.fillAssetConfig(&tx.Header, &tx.AssetConfigTxnFields) + case protocol.AssetTransferTx: + r.fillAssetTransfer(&tx.Header, &tx.AssetTransferTxnFields) + case protocol.AssetFreezeTx: + r.fillAssetFreeze(&tx.Header, &tx.AssetFreezeTxnFields) + case protocol.ApplicationCallTx: + r.fillApplicationCall(ep, &tx.Header, &tx.ApplicationCallTxnFields) + } +} + func (r *resources) fillKeyRegistration(hdr *transactions.Header) { r.sharedAccounts[hdr.Sender] = struct{}{} } @@ -80,26 +106,25 @@ func (r *resources) fillPayment(hdr *transactions.Header, tx *transactions.Payme } func (r *resources) fillAssetConfig(hdr *transactions.Header, tx *transactions.AssetConfigTxnFields) { - r.sharedAccounts[hdr.Sender] = struct{}{} + r.shareAccountAndHolding(hdr.Sender, tx.ConfigAsset) if id := tx.ConfigAsset; id != 0 { r.sharedAsas[id] = struct{}{} - r.shareHolding(hdr.Sender, id) } // We don't need to read the special addresses, so they don't go in. } func (r *resources) fillAssetTransfer(hdr *transactions.Header, tx *transactions.AssetTransferTxnFields) { - r.sharedAccounts[hdr.Sender] = struct{}{} id := tx.XferAsset r.sharedAsas[id] = struct{}{} - r.shareHolding(hdr.Sender, id) + r.shareAccountAndHolding(hdr.Sender, id) + r.shareAccountAndHolding(tx.AssetReceiver, id) if !tx.AssetSender.IsZero() { - r.shareHolding(tx.AssetSender, id) + r.shareAccountAndHolding(tx.AssetSender, id) } - r.shareHolding(tx.AssetReceiver, id) + if !tx.AssetCloseTo.IsZero() { - r.shareHolding(tx.AssetCloseTo, id) + r.shareAccountAndHolding(tx.AssetCloseTo, id) } } @@ -107,7 +132,7 @@ func (r *resources) fillAssetFreeze(hdr *transactions.Header, tx *transactions.A r.sharedAccounts[hdr.Sender] = struct{}{} id := tx.FreezeAsset r.sharedAsas[id] = struct{}{} - r.shareHolding(tx.FreezeAccount, id) + r.shareAccountAndHolding(tx.FreezeAccount, id) } func (r *resources) fillApplicationCall(ep *EvalParams, hdr *transactions.Header, tx *transactions.ApplicationCallTxnFields) { diff --git a/data/transactions/logic/resources_test.go b/data/transactions/logic/resources_test.go new file mode 100644 index 0000000000..313125857b --- /dev/null +++ b/data/transactions/logic/resources_test.go @@ -0,0 +1,416 @@ +// Copyright (C) 2019-2023 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 . + +package logic_test + +import ( + "fmt" + "testing" + + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/transactions/logic" + "github.com/algorand/go-algorand/data/txntest" + "github.com/algorand/go-algorand/protocol" + "github.com/algorand/go-algorand/test/partitiontest" +) + +// TestAppSharing confirms that as of v9, assets can be accessed across +// groups, but that before then, they could not. +func TestAppSharing(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + // Create some sample transactions. The main reason this a blackbox test + // (_test package) is to have access to txntest. + appl0 := txntest.Txn{ + Type: protocol.ApplicationCallTx, + Sender: basics.Address{1, 2, 3, 4}, + ForeignApps: []basics.AppIndex{500}, + } + + appl1 := txntest.Txn{ + Type: protocol.ApplicationCallTx, + Sender: basics.Address{4, 3, 2, 1}, + } + + appl2 := txntest.Txn{ + Type: protocol.ApplicationCallTx, + Sender: basics.Address{1, 2, 3, 4}, + } + + getSchema := ` +int 500 +app_params_get AppGlobalNumByteSlice +!; assert; pop; int 1 +` + sources := []string{getSchema, getSchema} + // In v8, the first tx can read app params of 500, because it's in its + // foreign array, but the second can't + logic.TestApps(t, sources, txntest.SignedTxns(&appl0, &appl1), 8, nil, + logic.NewExpect(1, "invalid App reference 500")) + // In v9, the second can, because the first can. + logic.TestApps(t, sources, txntest.SignedTxns(&appl0, &appl1), 9, nil) + + getLocalEx := ` +int 0 // Sender +int 500 +byte "some-key" +app_local_get_ex +pop; pop; int 1 +` + + sources = []string{getLocalEx, getLocalEx} + // In contrast, here there's no help from v9, because the second tx is + // reading the locals for a different account. + + // app_local_get* requires the address and the app exist, else the program fails + logic.TestApps(t, sources, txntest.SignedTxns(&appl0, &appl1), 8, nil, + logic.NewExpect(0, "no account")) + + _, _, ledger := logic.MakeSampleEnv() + ledger.NewAccount(appl0.Sender, 100_000) + ledger.NewApp(appl0.Sender, 500, basics.AppParams{}) + ledger.NewLocals(appl0.Sender, 500) // opt in + // Now txn0 passes, but txn1 has an error because it can't see app 500 + logic.TestApps(t, sources, txntest.SignedTxns(&appl0, &appl1), 9, ledger, + logic.NewExpect(1, "invalid Local State access")) + + // But it's ok in appl2, because appl2 uses the same Sender, even though the + // foreign-app is not repeated in appl2 so the holding being accessed is is + // the one from tx0. + logic.TestApps(t, sources, txntest.SignedTxns(&appl0, &appl2), 9, ledger) +} + +// TestAssetSharing confirms that as of v9, assets can be accessed across +// groups, but that before then, they could not. +func TestAssetSharing(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + // Create some sample transactions. The main reason this a blackbox test + // (_test package) is to have access to txntest. + appl0 := txntest.Txn{ + Type: protocol.ApplicationCallTx, + Sender: basics.Address{1, 2, 3, 4}, + ForeignAssets: []basics.AssetIndex{400}, + } + + appl1 := txntest.Txn{ + Type: protocol.ApplicationCallTx, + Sender: basics.Address{4, 3, 2, 1}, + } + + appl2 := txntest.Txn{ + Type: protocol.ApplicationCallTx, + Sender: basics.Address{1, 2, 3, 4}, + } + + getTotal := ` +int 400 +asset_params_get AssetTotal +pop; pop; int 1 +` + sources := []string{getTotal, getTotal} + // In v8, the first tx can read asset 400, because it's in its foreign arry, + // but the second can't + logic.TestApps(t, sources, txntest.SignedTxns(&appl0, &appl1), 8, nil, + logic.NewExpect(1, "invalid Asset reference 400")) + // In v9, the second can, because the first can. + logic.TestApps(t, sources, txntest.SignedTxns(&appl0, &appl1), 9, nil) + + getBalance := ` +int 0 +int 400 +asset_holding_get AssetBalance +pop; pop; int 1 +` + + sources = []string{getBalance, getBalance} + // In contrast, here there's no help from v9, because the second tx is + // reading a holding for a different account. + logic.TestApps(t, sources, txntest.SignedTxns(&appl0, &appl1), 8, nil, + logic.NewExpect(1, "invalid Asset reference 400")) + logic.TestApps(t, sources, txntest.SignedTxns(&appl0, &appl1), 9, nil, + logic.NewExpect(1, "invalid Holding access")) + // But it's ok in appl2, because the same account is used, even though the + // foreign-asset is not repeated in appl2. + logic.TestApps(t, sources, txntest.SignedTxns(&appl0, &appl2), 9, nil) +} + +// TestOtherTxSharing tests resource sharing across other kinds of transactions besides appl. +func TestOtherTxSharing(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + _, _, ledger := logic.MakeSampleEnv() + + senderAcct := basics.Address{1, 2, 3, 4, 5, 6, 1} + ledger.NewAccount(senderAcct, 2001) + senderBalance := "txn ApplicationArgs 0; balance; int 2001; ==" + + receiverAcct := basics.Address{1, 2, 3, 4, 5, 6, 2} + ledger.NewAccount(receiverAcct, 2002) + receiverBalance := "txn ApplicationArgs 0; balance; int 2002; ==" + + otherAcct := basics.Address{1, 2, 3, 4, 5, 6, 3} + ledger.NewAccount(otherAcct, 2003) + otherBalance := "txn ApplicationArgs 0; balance; int 2003; ==" + + appl := txntest.Txn{ + Type: protocol.ApplicationCallTx, + Sender: basics.Address{5, 5, 5, 5}, // different from all other accounts used + ApplicationArgs: [][]byte{senderAcct[:]}, + } + + keyreg := txntest.Txn{ + Type: protocol.KeyRegistrationTx, + Sender: senderAcct, + } + pay := txntest.Txn{ + Type: protocol.PaymentTx, + Sender: senderAcct, + Receiver: receiverAcct, + } + acfg := txntest.Txn{ + Type: protocol.AssetConfigTx, + Sender: senderAcct, + AssetParams: basics.AssetParams{ + Manager: otherAcct, // other is here to show they _don't_ become available + Reserve: otherAcct, + Freeze: otherAcct, + Clawback: otherAcct, + }, + } + axfer := txntest.Txn{ + Type: protocol.AssetTransferTx, + XferAsset: 100, // must be < 256, later code assumes it fits in a byte + Sender: senderAcct, + AssetReceiver: receiverAcct, + AssetSender: otherAcct, + } + afrz := txntest.Txn{ + Type: protocol.AssetFreezeTx, + FreezeAsset: 200, // must be < 256, later code assumes it fits in a byte + Sender: senderAcct, + FreezeAccount: otherAcct, + } + + sources := []string{"", senderBalance} + rsources := []string{senderBalance, ""} + for _, send := range []txntest.Txn{keyreg, pay, acfg, axfer, afrz} { + logic.TestApps(t, sources, txntest.SignedTxns(&send, &appl), 9, ledger) + logic.TestApps(t, rsources, txntest.SignedTxns(&appl, &send), 9, ledger) + + logic.TestApps(t, sources, txntest.SignedTxns(&send, &appl), 8, ledger, + logic.NewExpect(1, "invalid Account reference")) + logic.TestApps(t, rsources, txntest.SignedTxns(&appl, &send), 8, ledger, + logic.NewExpect(0, "invalid Account reference")) + } + + holdingAccess := ` + txn ApplicationArgs 0 + txn ApplicationArgs 1; btoi + asset_holding_get AssetBalance + pop; pop; int 1 +` + sources = []string{"", holdingAccess} + rsources = []string{holdingAccess, ""} + + t.Run("keyreg", func(t *testing.T) { + appl.ApplicationArgs = [][]byte{senderAcct[:], {200}} + logic.TestApps(t, sources, txntest.SignedTxns(&keyreg, &appl), 9, ledger, + logic.NewExpect(1, "invalid Asset reference 200")) + withRef := appl + withRef.ForeignAssets = []basics.AssetIndex{200} + logic.TestApps(t, sources, txntest.SignedTxns(&keyreg, &withRef), 9, ledger, + logic.NewExpect(1, "invalid Holding access "+senderAcct.String())) + }) + t.Run("pay", func(t *testing.T) { + // The receiver is available for algo balance reading + appl.ApplicationArgs = [][]byte{receiverAcct[:]} + logic.TestApps(t, []string{"", receiverBalance}, txntest.SignedTxns(&pay, &appl), 9, ledger) + + // The other account is not (it's not even in the pay txn) + appl.ApplicationArgs = [][]byte{otherAcct[:]} + logic.TestApps(t, []string{"", otherBalance}, txntest.SignedTxns(&pay, &appl), 9, ledger, + logic.NewExpect(1, "invalid Account reference "+otherAcct.String())) + + // The other account becomes accessible because used in CloseRemainderTo + withClose := pay + withClose.CloseRemainderTo = otherAcct + logic.TestApps(t, []string{"", otherBalance}, txntest.SignedTxns(&withClose, &appl), 9, ledger) + }) + + t.Run("acfg", func(t *testing.T) { + // The other account is not available even though it's all the extra addresses + appl.ApplicationArgs = [][]byte{otherAcct[:]} + logic.TestApps(t, []string{"", otherBalance}, txntest.SignedTxns(&acfg, &appl), 9, ledger, + logic.NewExpect(1, "invalid Account reference "+otherAcct.String())) + }) + + t.Run("axfer", func(t *testing.T) { + // The receiver is also available for algo balance reading + appl.ApplicationArgs = [][]byte{receiverAcct[:]} + logic.TestApps(t, []string{"", receiverBalance}, txntest.SignedTxns(&axfer, &appl), 9, ledger) + + // as is the "other" (AssetSender) + appl.ApplicationArgs = [][]byte{otherAcct[:]} + logic.TestApps(t, []string{"", otherBalance}, txntest.SignedTxns(&axfer, &appl), 9, ledger) + + // receiver holding is available + appl.ApplicationArgs = [][]byte{receiverAcct[:], {byte(axfer.XferAsset)}} + logic.TestApps(t, []string{"", holdingAccess}, txntest.SignedTxns(&axfer, &appl), 9, ledger) + + // The other account becomes accessible because used in CloseRemainderTo + // (for asa and algo) + withClose := axfer + withClose.AssetCloseTo = otherAcct + appl.ApplicationArgs = [][]byte{otherAcct[:], {byte(axfer.XferAsset)}} + logic.TestApps(t, []string{"", otherBalance}, txntest.SignedTxns(&withClose, &appl), 9, ledger) + logic.TestApps(t, []string{"", holdingAccess}, txntest.SignedTxns(&withClose, &appl), 9, ledger) + }) + + t.Run("afrz", func(t *testing.T) { + // The other account is available (for algo and asset) + appl.ApplicationArgs = [][]byte{otherAcct[:], {byte(afrz.FreezeAsset)}} + logic.TestApps(t, []string{"", otherBalance}, txntest.SignedTxns(&afrz, &appl), 9, ledger) + logic.TestApps(t, []string{"", holdingAccess}, txntest.SignedTxns(&afrz, &appl), 9, ledger) + }) +} + +// TestSharedInnerTxns checks how inner txns access resources. +func TestSharedInnerTxns(t *testing.T) { + _, _, ledger := logic.MakeSampleEnv() + + const asa1 = 201 + const asa2 = 202 + + senderAcct := basics.Address{1, 2, 3, 4, 5, 6, 1} + ledger.NewAccount(senderAcct, 2001) + ledger.NewHolding(senderAcct, asa1, 1, false) + + receiverAcct := basics.Address{1, 2, 3, 4, 5, 6, 2} + ledger.NewAccount(receiverAcct, 2002) + ledger.NewHolding(receiverAcct, asa1, 1, false) + + otherAcct := basics.Address{1, 2, 3, 4, 5, 6, 3} + ledger.NewAccount(otherAcct, 2003) + ledger.NewHolding(otherAcct, asa1, 1, false) + + unusedAcct := basics.Address{1, 2, 3, 4, 5, 6, 4} + + payToArg := ` +itxn_begin + int pay; itxn_field TypeEnum + int 100; itxn_field Amount + txn ApplicationArgs 0; itxn_field Receiver +itxn_submit +int 1 +` + axferToArgs := ` +itxn_begin + int axfer; itxn_field TypeEnum + int 2; itxn_field AssetAmount + txn ApplicationArgs 0; itxn_field AssetReceiver + txn ApplicationArgs 1; btoi; itxn_field XferAsset +itxn_submit +int 1 +` + + appl := txntest.Txn{ + Type: protocol.ApplicationCallTx, + Sender: basics.Address{5, 5, 5, 5}, // different from all other accounts used + } + // App will do a lot of txns. Start well funded. + ledger.NewAccount(basics.AppIndex(888).Address(), 1_000_000) + // And needs some ASAs for inner axfer testing + ledger.NewHolding(basics.AppIndex(888).Address(), asa1, 1_000_000, false) + + t.Run("keyreg", func(t *testing.T) { + keyreg := txntest.Txn{ + Type: protocol.KeyRegistrationTx, + Sender: senderAcct, + } + + // appl has no foreign ref to senderAcct, but can still inner pay it + appl.ApplicationArgs = [][]byte{senderAcct[:]} + logic.TestApps(t, []string{"", payToArg}, txntest.SignedTxns(&keyreg, &appl), 9, ledger) + logic.TestApps(t, []string{"", payToArg}, txntest.SignedTxns(&keyreg, &appl), 8, ledger, + logic.NewExpect(1, "invalid Account reference "+senderAcct.String())) + + // confirm you can't just pay _anybody_. receiverAcct is not in use at all. + appl.ApplicationArgs = [][]byte{receiverAcct[:]} + logic.TestApps(t, []string{"", payToArg}, txntest.SignedTxns(&keyreg, &appl), 9, ledger, + logic.NewExpect(1, "invalid Account reference "+receiverAcct.String())) + }) + + t.Run("pay", func(t *testing.T) { + pay := txntest.Txn{ + Type: protocol.PaymentTx, + Sender: senderAcct, + Receiver: receiverAcct, + } + + // appl has no foreign ref to senderAcct or receiverAcct, but can still inner pay them + appl.ApplicationArgs = [][]byte{senderAcct[:]} + logic.TestApps(t, []string{"", payToArg}, txntest.SignedTxns(&pay, &appl), 9, ledger) + logic.TestApps(t, []string{"", payToArg}, txntest.SignedTxns(&pay, &appl), 8, ledger, + logic.NewExpect(1, "invalid Account reference "+senderAcct.String())) + + appl.ApplicationArgs = [][]byte{receiverAcct[:]} + logic.TestApps(t, []string{"", payToArg}, txntest.SignedTxns(&pay, &appl), 9, ledger) + logic.TestApps(t, []string{"", payToArg}, txntest.SignedTxns(&pay, &appl), 8, ledger, + logic.NewExpect(1, "invalid Account reference "+receiverAcct.String())) + + // confirm you can't just pay _anybody_. otherAcct is not in use at all. + appl.ApplicationArgs = [][]byte{otherAcct[:]} + logic.TestApps(t, []string{"", payToArg}, txntest.SignedTxns(&pay, &appl), 9, ledger, + logic.NewExpect(1, "invalid Account reference "+otherAcct.String())) + }) + + t.Run("axfer", func(t *testing.T) { + axfer := txntest.Txn{ + Type: protocol.AssetTransferTx, + XferAsset: asa1, + Sender: senderAcct, + AssetReceiver: receiverAcct, + AssetSender: otherAcct, + } + + // appl can pay or axfer to the sender + appl.ApplicationArgs = [][]byte{senderAcct[:], {asa1}} + logic.TestApps(t, []string{"", payToArg}, txntest.SignedTxns(&axfer, &appl), 9, ledger) + logic.TestApps(t, []string{"", axferToArgs}, txntest.SignedTxns(&axfer, &appl), 9, ledger) + // and to the receiver + appl.ApplicationArgs = [][]byte{receiverAcct[:], {asa1}} + logic.TestApps(t, []string{payToArg}, txntest.SignedTxns(&appl, &axfer), 9, ledger) + logic.TestApps(t, []string{axferToArgs}, txntest.SignedTxns(&appl, &axfer), 9, ledger) + // and to the clawback + appl.ApplicationArgs = [][]byte{otherAcct[:], {asa1}} + logic.TestApps(t, []string{"", payToArg}, txntest.SignedTxns(&axfer, &appl), 9, ledger) + logic.TestApps(t, []string{"", axferToArgs}, txntest.SignedTxns(&axfer, &appl), 9, ledger) + + // but can't axfer a different asset + appl.ApplicationArgs = [][]byte{senderAcct[:], {asa2}} + logic.TestApps(t, []string{"", axferToArgs}, txntest.SignedTxns(&axfer, &appl), 9, ledger, + logic.NewExpect(1, fmt.Sprintf("invalid Asset reference %d", asa2))) + // or correct asset to an unknown address + appl.ApplicationArgs = [][]byte{unusedAcct[:], {asa1}} + logic.TestApps(t, []string{"", axferToArgs}, txntest.SignedTxns(&axfer, &appl), 9, ledger, + logic.NewExpect(1, "invalid Account reference")) + }) + +}