Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AVM: Make apps and app accounts available while creation is "pending" #5425

Merged
merged 2 commits into from
May 30, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
43 changes: 27 additions & 16 deletions data/transactions/logic/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -539,10 +539,16 @@ func (ep *EvalParams) RecordAD(gi int, ad transactions.ApplyData) {
}
ep.TxnGroup[gi].ApplyData = ad
if aid := ad.ConfigAsset; aid != 0 {
ep.available.createdAsas = append(ep.available.createdAsas, aid)
if ep.available.createdAsas == nil {
ep.available.createdAsas = make(map[basics.AssetIndex]struct{})
}
ep.available.createdAsas[aid] = struct{}{}
bbroder-algo marked this conversation as resolved.
Show resolved Hide resolved
}
if aid := ad.ApplicationID; aid != 0 {
ep.available.createdApps = append(ep.available.createdApps, aid)
if ep.available.createdApps == nil {
ep.available.createdApps = make(map[basics.AppIndex]struct{})
}
ep.available.createdApps[aid] = struct{}{}
}
}

Expand Down Expand Up @@ -954,14 +960,21 @@ func EvalContract(program []byte, gi int, aid basics.AppIndex, params *EvalParam
}
}

// If this is a creation, make any "0 index" box refs available now that we
// have an appID.
// If this is a creation...
if cx.txn.Txn.ApplicationID == 0 {
// make any "0 index" box refs available now that we have an appID.
for _, br := range cx.txn.Txn.Boxes {
if br.Index == 0 {
cx.EvalParams.available.boxes[boxRef{cx.appID, string(br.Name)}] = false
}
}
// and add the appID to `createdApps`
if cx.EvalParams.Proto.LogicSigVersion >= sharedResourcesVersion {
if cx.EvalParams.available.createdApps == nil {
cx.EvalParams.available.createdApps = make(map[basics.AppIndex]struct{})
}
cx.EvalParams.available.createdApps[cx.appID] = struct{}{}
}
}

// Check the I/O budget for reading if this is the first top-level app call
Expand Down Expand Up @@ -4248,13 +4261,15 @@ func opExtract64Bits(cx *EvalContext) error {
// assignAccount is used to convert a stackValue into a 32-byte account value,
// enforcing any "availability" restrictions in force.
func (cx *EvalContext) assignAccount(sv stackValue) (basics.Address, error) {
_, err := sv.address()
addr, err := sv.address()
if err != nil {
return basics.Address{}, err
}

addr, _, err := cx.accountReference(sv)
return addr, err
if cx.availableAccount(addr) {
return addr, nil
}
return basics.Address{}, fmt.Errorf("invalid Account reference %s", addr)
}

// accountReference yields the address and Accounts offset designated by a
Expand Down Expand Up @@ -4323,7 +4338,7 @@ func (cx *EvalContext) availableAccount(addr basics.Address) bool {

// Allow an address for an app that was created in group
if cx.version >= createdResourcesVersion {
for _, appID := range cx.available.createdApps {
for appID := range cx.available.createdApps {
createdAddress := cx.getApplicationAddress(appID)
if addr == createdAddress {
return true
Expand Down Expand Up @@ -5199,10 +5214,8 @@ func (cx *EvalContext) availableAsset(aid basics.AssetIndex) bool {
}
// or was created in group
if cx.version >= createdResourcesVersion {
for _, assetID := range cx.available.createdAsas {
if assetID == aid {
return true
}
if _, ok := cx.available.createdAsas[aid]; ok {
return true
}
}

Expand Down Expand Up @@ -5241,10 +5254,8 @@ func (cx *EvalContext) availableApp(aid basics.AppIndex) bool {
}
// or was created in group
if cx.version >= createdResourcesVersion {
for _, appID := range cx.available.createdApps {
if appID == aid {
return true
}
if _, ok := cx.available.createdApps[aid]; ok {
return true
}
}
// Or, it can be the current app
Expand Down
41 changes: 23 additions & 18 deletions data/transactions/logic/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ import (
type resources struct {
// These resources were created previously in the group, so they can be used
// by later transactions.
createdAsas []basics.AssetIndex
createdApps []basics.AppIndex
createdAsas map[basics.AssetIndex]struct{}
createdApps map[basics.AppIndex]struct{}

// These resources have been used by some txn in the group, so they are
// available. These maps track the availability of the basic objects (often
Expand Down Expand Up @@ -101,6 +101,16 @@ func (r *resources) fill(tx *transactions.Transaction, ep *EvalParams) {
}

func (cx *EvalContext) allows(tx *transactions.Transaction, calleeVer uint64) error {
// if the caller is pre-sharing, it can't prepare transactions with
// resources that are not available, so `tx` is surely legal.
if cx.version < sharedResourcesVersion {
// this is important, not just an optimization, because a pre-sharing
// creation txn has access to the app and app account it is currently
// creating (and therefore can pass that access down), but cx.available
// doesn't track that properly until v9's protocol upgrade. See
jasonpaulos marked this conversation as resolved.
Show resolved Hide resolved
// TestInnerAppCreateAndOptin for an example.
return nil
}
switch tx.Type {
case protocol.PaymentTx, protocol.KeyRegistrationTx, protocol.AssetConfigTx:
// these transactions don't touch cross-product resources, so no error is possible
Expand All @@ -110,7 +120,7 @@ func (cx *EvalContext) allows(tx *transactions.Transaction, calleeVer uint64) er
case protocol.AssetFreezeTx:
return cx.allowsAssetFreeze(&tx.Header, &tx.AssetFreezeTxnFields)
case protocol.ApplicationCallTx:
return cx.allowsApplicationCall(&tx.Header, &tx.ApplicationCallTxnFields, cx.version, calleeVer)
return cx.allowsApplicationCall(&tx.Header, &tx.ApplicationCallTxnFields, calleeVer)
default:
return fmt.Errorf("unknown inner transaction type %s", tx.Type)
}
Expand Down Expand Up @@ -158,13 +168,11 @@ func (cx *EvalContext) allowsHolding(addr basics.Address, ai basics.AssetIndex)
return true
}
// If an ASA was created in this group, then allow holding access for any allowed account.
for _, created := range r.createdAsas {
if created == ai {
return cx.availableAccount(addr)
}
if _, ok := r.createdAsas[ai]; ok {
return cx.availableAccount(addr)
}
// If the address was "created" by making its app in this group, then allow for available assets.
for _, created := range r.createdApps {
for created := range r.createdApps {
if cx.getApplicationAddress(created) == addr {
return cx.availableAsset(ai)
}
Expand All @@ -184,17 +192,15 @@ func (cx *EvalContext) allowsLocals(addr basics.Address, ai basics.AppIndex) boo
return true
}
// All locals of created apps are available
for _, created := range r.createdApps {
if created == ai {
return cx.availableAccount(addr)
}
if _, ok := r.createdApps[ai]; ok {
return cx.availableAccount(addr)
}
if cx.txn.Txn.ApplicationID == 0 && cx.appID == ai {
return cx.availableAccount(addr)
}

// All locals of created app accounts are available
for _, created := range r.createdApps {
for created := range r.createdApps {
if cx.getApplicationAddress(created) == addr {
return cx.availableApp(ai)
}
Expand Down Expand Up @@ -315,11 +321,10 @@ func (r *resources) fillApplicationCall(ep *EvalParams, hdr *transactions.Header
}
}

func (cx *EvalContext) allowsApplicationCall(hdr *transactions.Header, tx *transactions.ApplicationCallTxnFields, callerVer, calleeVer uint64) error {
// If an old (pre resource sharing) app is being called from an app that has
// resource sharing enabled, we need to confirm that no new "cross-product"
// resources have become available.
if callerVer < sharedResourcesVersion || calleeVer >= sharedResourcesVersion {
func (cx *EvalContext) allowsApplicationCall(hdr *transactions.Header, tx *transactions.ApplicationCallTxnFields, calleeVer uint64) error {
// If the callee is at least sharedResourcesVersion, then it will check
// availability properly itself.
if calleeVer >= sharedResourcesVersion {
bbroder-algo marked this conversation as resolved.
Show resolved Hide resolved
return nil
}

Expand Down
115 changes: 115 additions & 0 deletions ledger/apptxn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,121 @@ func TestInnerRekey(t *testing.T) {
})
}

// TestInnerAppCreateAndOptin tests a weird way to create an app and opt it into
// an ASA all from one top-level transaction. Part of the trick is to use an
// inner helper app. The app being created rekeys itself to the inner app,
// which funds the outer app and opts it into the ASA. It could have worked
// differently - the inner app could have just funded the outer app, and then
// the outer app could have opted-in. But this technique tests something
// interesting, that the inner app can perform an opt-in on the outer app, which
// tests that the newly created app's holdings are available. In practice, the
// helper shold rekey it back, but we don't bother here.
func TestInnerAppCreateAndOptin(t *testing.T) {
partitiontest.PartitionTest(t)
t.Parallel()

genBalances, addrs, _ := ledgertesting.NewTestGenesis()

// v31 allows inner appl and inner rekey
ledgertesting.TestConsensusRange(t, 31, 0, func(t *testing.T, ver int, cv protocol.ConsensusVersion, cfg config.Local) {
dl := NewDoubleLedger(t, genBalances, cv, cfg)
defer dl.Close()

createasa := txntest.Txn{
Type: "acfg",
Sender: addrs[0],
AssetParams: basics.AssetParams{Total: 2, UnitName: "$"},
}
asaID := dl.txn(&createasa).ApplyData.ConfigAsset
require.NotZero(t, asaID)

// helper app, is called during the creation of an app. When such an
// app is created, it rekeys itself to this helper and calls it. The
// helpers opts the caller into an ASA, and funds the MBR the caller
// needs for that optin.
helper := dl.fundedApp(addrs[0], 1_000_000,
main(`
itxn_begin
int axfer; itxn_field TypeEnum
int `+strconv.Itoa(int(asaID))+`; itxn_field XferAsset
txn Sender; itxn_field Sender // call as the caller! (works because of rekey by caller)
txn Sender; itxn_field AssetReceiver // 0 to self == opt-in
itxn_next
int pay; itxn_field TypeEnum // pay 200kmAlgo to the caller, for MBR
int 200000; itxn_field Amount
txn Sender; itxn_field Receiver
itxn_submit
`))
// Don't use `main` here, we want to do the work during creation. Rekey
// to the helper and invoke it, trusting it to opt us into the ASA.
createapp := txntest.Txn{
Type: "appl",
Sender: addrs[0],
Fee: 3 * 1000, // to pay for self, call to helper, and helper's axfer
ApprovalProgram: `
jasonpaulos marked this conversation as resolved.
Show resolved Hide resolved
itxn_begin
int appl; itxn_field TypeEnum
addr ` + helper.Address().String() + `; itxn_field RekeyTo
int ` + strconv.Itoa(int(helper)) + `; itxn_field ApplicationID
txn Assets 0; itxn_field Assets
itxn_submit
int 1
`,
ForeignApps: []basics.AppIndex{helper},
ForeignAssets: []basics.AssetIndex{asaID},
}
appID := dl.txn(&createapp).ApplyData.ApplicationID
require.NotZero(t, appID)
})
}

// TestParentGlobals tests that a newly created app can call an inner app, and
// the inner app will have access to the parent globals, even if the originally
// created app ID isn't passed down, because the rule is that "pending" created
// apps are available, starting from v38
func TestParentGlobals(t *testing.T) {
partitiontest.PartitionTest(t)
t.Parallel()

genBalances, addrs, _ := ledgertesting.NewTestGenesis()

// v38 allows parent access, but we start with v31 to make sure we don't mistakenly change it
ledgertesting.TestConsensusRange(t, 31, 0, func(t *testing.T, ver int, cv protocol.ConsensusVersion, cfg config.Local) {
dl := NewDoubleLedger(t, genBalances, cv, cfg)
defer dl.Close()

// helper app, is called during the creation of an app. this app tries
// to access its parent's globals, by using `global CallerApplicationID`
helper := dl.fundedApp(addrs[0], 1_000_000,
main(`
global CallerApplicationID
byte "X"
app_global_get_ex; pop; pop; // we only care that it didn't panic
`))
// Don't use `main` here, we want to do the work during creation.
createapp := txntest.Txn{
Type: "appl",
Sender: addrs[0],
Fee: 2 * 1000, // to pay for self and call to helper
ApprovalProgram: `
itxn_begin
int appl; itxn_field TypeEnum
int ` + strconv.Itoa(int(helper)) + `; itxn_field ApplicationID
itxn_submit
int 1
`,
ForeignApps: []basics.AppIndex{helper},
}
if ver >= 38 {
appID := dl.txn(&createapp).ApplyData.ApplicationID
require.NotZero(t, appID)
} else {
dl.txn(&createapp, "unavailable App")
}

})
}

func TestNote(t *testing.T) {
partitiontest.PartitionTest(t)
t.Parallel()
Expand Down
5 changes: 4 additions & 1 deletion ledger/double_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ func (dl *DoubleLedger) txn(tx *txntest.Txn, problem ...string) (stib *transacti
dl.eval = nil
} else {
vb := dl.endBlock()
stib = &vb.Block().Payset[0]
// It should have a stib, but don't panic here because of an earlier problem.
if len(vb.Block().Payset) > 0 {
stib = &vb.Block().Payset[0]
}
}
}()
}
Expand Down