diff --git a/cmd/goal/clerk.go b/cmd/goal/clerk.go index 3bf35f795a..e064a5b48e 100644 --- a/cmd/goal/clerk.go +++ b/cmd/goal/clerk.go @@ -1095,7 +1095,7 @@ var dryrunCmd = &cobra.Command{ if uint64(txn.Lsig.Len()) > params.LogicSigMaxSize { reportErrorf("program size too large: %d > %d", len(txn.Lsig.Logic), params.LogicSigMaxSize) } - ep := &logic.EvalParams{Proto: ¶ms, TxnGroup: txgroup} + ep := logic.NewEvalParams(txgroup, ¶ms, nil) err := logic.CheckSignature(i, ep) if err != nil { reportErrorf("program failed Check: %s", err) diff --git a/cmd/tealdbg/debugger_test.go b/cmd/tealdbg/debugger_test.go index 2b13ea4a65..bf9dcd7c93 100644 --- a/cmd/tealdbg/debugger_test.go +++ b/cmd/tealdbg/debugger_test.go @@ -100,11 +100,8 @@ func TestDebuggerSimple(t *testing.T) { da := makeTestDbgAdapter(t) debugger.AddAdapter(da) - ep := &logic.EvalParams{ - Proto: &proto, - Debugger: debugger, - TxnGroup: make([]transactions.SignedTxnWithAD, 1), - } + ep := logic.NewEvalParams(make([]transactions.SignedTxnWithAD, 1), &proto, nil) + ep.Debugger = debugger source := `int 0 int 1 diff --git a/cmd/tealdbg/local.go b/cmd/tealdbg/local.go index 53dafc8b68..3afb4bcdd7 100644 --- a/cmd/tealdbg/local.go +++ b/cmd/tealdbg/local.go @@ -205,17 +205,16 @@ const ( // evaluation is a description of a single debugger run type evaluation struct { - program []byte - source string - offsetToLine map[int]int - name string - groupIndex uint64 - pastSideEffects []logic.EvalSideEffects - mode modeType - aidx basics.AppIndex - ba apply.Balances - result evalResult - states AppState + program []byte + source string + offsetToLine map[int]int + name string + groupIndex uint64 + mode modeType + aidx basics.AppIndex + ba apply.Balances + result evalResult + states AppState } func (e *evaluation) eval(gi int, ep *logic.EvalParams) (pass bool, err error) { @@ -341,17 +340,6 @@ func (r *LocalRunner) Setup(dp *DebugParams) (err error) { dp.LatestTimestamp = int64(ddr.LatestTimestamp) } - if dp.PastSideEffects == nil { - dp.PastSideEffects = logic.MakePastSideEffects(len(r.txnGroup)) - } else if len(dp.PastSideEffects) != len(r.txnGroup) { - err = fmt.Errorf( - "invalid past side effects slice with length %d should match group length of %d txns", - len(dp.PastSideEffects), - len(r.txnGroup), - ) - return - } - // if program(s) specified then run from it if len(dp.ProgramBlobs) > 0 { if len(r.txnGroup) == 1 && dp.GroupIndex != 0 { @@ -389,7 +377,6 @@ func (r *LocalRunner) Setup(dp *DebugParams) (err error) { } } r.runs[i].groupIndex = uint64(dp.GroupIndex) - r.runs[i].pastSideEffects = dp.PastSideEffects r.runs[i].name = dp.ProgramNames[i] var mode modeType @@ -452,13 +439,12 @@ func (r *LocalRunner) Setup(dp *DebugParams) (err error) { return } run := evaluation{ - program: stxn.Txn.ApprovalProgram, - groupIndex: uint64(gi), - pastSideEffects: dp.PastSideEffects, - mode: modeStateful, - aidx: appIdx, - ba: b, - states: states, + program: stxn.Txn.ApprovalProgram, + groupIndex: uint64(gi), + mode: modeStateful, + aidx: appIdx, + ba: b, + states: states, } r.runs = append(r.runs, run) } @@ -488,13 +474,12 @@ func (r *LocalRunner) Setup(dp *DebugParams) (err error) { return } run := evaluation{ - program: program, - groupIndex: uint64(gi), - pastSideEffects: dp.PastSideEffects, - mode: modeStateful, - aidx: appIdx, - ba: b, - states: states, + program: program, + groupIndex: uint64(gi), + mode: modeStateful, + aidx: appIdx, + ba: b, + states: states, } r.runs = append(r.runs, run) found = true @@ -526,30 +511,9 @@ func (r *LocalRunner) RunAll() error { txngroup := transactions.WrapSignedTxnsWithAD(r.txnGroup) failed := 0 start := time.Now() - pooledApplicationBudget := uint64(0) - credit, _ := transactions.FeeCredit(txngroup, r.proto.MinTxnFee) - // ignore error since fees are not important for debugging in most cases - - var pastSideEffects []logic.EvalSideEffects - for _, run := range r.runs { - if run.mode == modeStateful { - if r.proto.EnableAppCostPooling { - pooledApplicationBudget += uint64(r.proto.MaxAppProgramCost) - } else { - pooledApplicationBudget = uint64(r.proto.MaxAppProgramCost) - } - pastSideEffects = run.pastSideEffects - } - } - ep := &logic.EvalParams{ - Proto: &r.proto, - Debugger: r.debugger, - TxnGroup: txngroup, - PastSideEffects: pastSideEffects, - Specials: &transactions.SpecialAddresses{}, - FeeCredit: &credit, - PooledApplicationBudget: &pooledApplicationBudget, - } + + ep := logic.NewEvalParams(txngroup, &r.proto, &transactions.SpecialAddresses{}) + ep.Debugger = r.debugger var last error for i := range r.runs { @@ -576,27 +540,8 @@ func (r *LocalRunner) Run() (bool, error) { } txngroup := transactions.WrapSignedTxnsWithAD(r.txnGroup) - pooledApplicationBudget := uint64(0) - credit, _ := transactions.FeeCredit(txngroup, r.proto.MinTxnFee) - // ignore error since fees are not important for debugging in most cases - - for _, run := range r.runs { - if run.mode == modeStateful { - if r.proto.EnableAppCostPooling { - pooledApplicationBudget += uint64(r.proto.MaxAppProgramCost) - } else { - pooledApplicationBudget = uint64(r.proto.MaxAppProgramCost) - } - } - } - ep := &logic.EvalParams{ - Proto: &r.proto, - TxnGroup: txngroup, - PastSideEffects: r.runs[0].pastSideEffects, // Looks strange, but all runs have same pastSideEffects - Specials: &transactions.SpecialAddresses{}, - FeeCredit: &credit, - PooledApplicationBudget: &pooledApplicationBudget, - } + + ep := logic.NewEvalParams(txngroup, &r.proto, &transactions.SpecialAddresses{}) run := r.runs[0] // Workaround for Go's nil/empty interfaces nil check after nil assignment, i.e. diff --git a/cmd/tealdbg/localLedger_test.go b/cmd/tealdbg/localLedger_test.go index 3e43586a32..c21d5a1cbb 100644 --- a/cmd/tealdbg/localLedger_test.go +++ b/cmd/tealdbg/localLedger_test.go @@ -90,6 +90,7 @@ int 2 // make transaction group: app call + sample payment txn := transactions.SignedTxn{ Txn: transactions.Transaction{ + Type: protocol.ApplicationCallTx, Header: transactions.Header{ Sender: addr, Fee: basics.MicroAlgos{Raw: 100}, @@ -109,11 +110,7 @@ int 2 a.NoError(err) proto := config.Consensus[protocol.ConsensusCurrentVersion] - ep := &logic.EvalParams{ - Proto: &proto, - TxnGroup: []transactions.SignedTxnWithAD{transactions.SignedTxnWithAD{SignedTxn: txn}}, - PastSideEffects: logic.MakePastSideEffects(1), - } + ep := logic.NewEvalParams([]transactions.SignedTxnWithAD{{SignedTxn: txn}}, &proto, &transactions.SpecialAddresses{}) pass, delta, err := ba.StatefulEval(0, ep, appIdx, program) a.NoError(err) a.True(pass) diff --git a/cmd/tealdbg/local_test.go b/cmd/tealdbg/local_test.go index d3884f8469..4387480a4c 100644 --- a/cmd/tealdbg/local_test.go +++ b/cmd/tealdbg/local_test.go @@ -316,6 +316,7 @@ func TestDebugEnvironment(t *testing.T) { // make transaction group: app call + sample payment txn := transactions.SignedTxn{ Txn: transactions.Transaction{ + Type: protocol.ApplicationCallTx, Header: transactions.Header{ Sender: sender, Fee: basics.MicroAlgos{Raw: 1000}, @@ -523,7 +524,7 @@ func TestDebugFromPrograms(t *testing.T) { partitiontest.PartitionTest(t) a := require.New(t) - txnBlob := []byte("[" + strings.Join([]string{string(txnSample), txnSample}, ",") + "]") + txnBlob := []byte("[" + strings.Join([]string{txnSample, txnSample}, ",") + "]") l := LocalRunner{} dp := DebugParams{ @@ -602,7 +603,7 @@ func TestRunMode(t *testing.T) { partitiontest.PartitionTest(t) a := require.New(t) - txnBlob := []byte("[" + strings.Join([]string{string(txnSample), txnSample}, ",") + "]") + txnBlob := []byte("[" + strings.Join([]string{txnSample, txnSample}, ",") + "]") l := LocalRunner{} // check run mode auto on stateful code diff --git a/cmd/tealdbg/server.go b/cmd/tealdbg/server.go index 43335921fc..f207285948 100644 --- a/cmd/tealdbg/server.go +++ b/cmd/tealdbg/server.go @@ -25,7 +25,6 @@ import ( "strings" "time" - "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/websocket" "github.com/gorilla/mux" ) @@ -73,7 +72,6 @@ type DebugParams struct { Proto string TxnBlob []byte GroupIndex int - PastSideEffects []logic.EvalSideEffects BalanceBlob []byte DdrBlob []byte IndexerURL string diff --git a/cmd/tealdbg/server_test.go b/cmd/tealdbg/server_test.go index 232c6a6f4c..2e09add32d 100644 --- a/cmd/tealdbg/server_test.go +++ b/cmd/tealdbg/server_test.go @@ -131,7 +131,7 @@ func TestServerRemote(t *testing.T) { func TestServerLocal(t *testing.T) { partitiontest.PartitionTest(t) - txnBlob := []byte("[" + strings.Join([]string{string(txnSample), txnSample}, ",") + "]") + txnBlob := []byte("[" + strings.Join([]string{txnSample, txnSample}, ",") + "]") dp := DebugParams{ ProgramNames: []string{"test"}, ProgramBlobs: [][]byte{{2, 0x20, 1, 1, 0x22}}, // version, intcb, int 1 diff --git a/daemon/algod/api/server/v2/dryrun.go b/daemon/algod/api/server/v2/dryrun.go index f663606e5c..872e8830fb 100644 --- a/daemon/algod/api/server/v2/dryrun.go +++ b/daemon/algod/api/server/v2/dryrun.go @@ -366,6 +366,10 @@ func doDryrunRequest(dr *DryrunRequest, response *generated.DryrunResponse) { return } proto := config.Consensus[protocol.ConsensusVersion(dr.ProtocolVersion)] + txgroup := transactions.WrapSignedTxnsWithAD(dr.Txns) + specials := transactions.SpecialAddresses{} + ep := logic.NewEvalParams(txgroup, &proto, &specials) + origEnableAppCostPooling := proto.EnableAppCostPooling // Enable EnableAppCostPooling so that dryrun // 1) can determine cost 2) reports actual cost for large programs that fail @@ -381,18 +385,10 @@ func doDryrunRequest(dr *DryrunRequest, response *generated.DryrunResponse) { allowedBudget += uint64(proto.MaxAppProgramCost) } } + ep.PooledApplicationBudget = &pooledAppBudget response.Txns = make([]generated.DryrunTxnResult, len(dr.Txns)) - txgroup := transactions.WrapSignedTxnsWithAD(dr.Txns) - pse := logic.MakePastSideEffects(len(dr.Txns)) for ti, stxn := range dr.Txns { - ep := &logic.EvalParams{ - Proto: &proto, - TxnGroup: txgroup, - PastSideEffects: pse, - PooledApplicationBudget: &pooledAppBudget, - Specials: &transactions.SpecialAddresses{}, - } var result generated.DryrunTxnResult if len(stxn.Lsig.Logic) > 0 { var debug dryrunDebugReceiver diff --git a/data/transactions/logic/blackbox_test.go b/data/transactions/logic/blackbox_test.go index 79e57a74cf..8a095c634a 100644 --- a/data/transactions/logic/blackbox_test.go +++ b/data/transactions/logic/blackbox_test.go @@ -80,22 +80,14 @@ func TestNewAppEvalParams(t *testing.T) { for i, param := range params { for j, testCase := range cases { t.Run(fmt.Sprintf("i=%d,j=%d", i, j), func(t *testing.T) { - ep := logic.NewAppEvalParams(testCase.group, ¶m, nil, 0) - - // Ensure non app calls have a nil evaluator, and that non-nil - // evaluators point to the right transactions and values - if testCase.numAppCalls > 0 { - require.NotNil(t, ep) - require.NotNil(t, ep.PastSideEffects) - require.Equal(t, ep.TxnGroup, testCase.group) - require.Equal(t, *ep.Proto, param) - if reflect.DeepEqual(param, config.Consensus[protocol.ConsensusV29]) { - require.Nil(t, ep.PooledApplicationBudget) - } else if reflect.DeepEqual(param, config.Consensus[protocol.ConsensusFuture]) { - require.Equal(t, *ep.PooledApplicationBudget, uint64(param.MaxAppProgramCost*testCase.numAppCalls)) - } - } else { - require.Nil(t, ep) + ep := logic.NewEvalParams(testCase.group, ¶m, nil) + require.NotNil(t, ep) + require.Equal(t, ep.TxnGroup, testCase.group) + require.Equal(t, *ep.Proto, param) + if reflect.DeepEqual(param, config.Consensus[protocol.ConsensusV29]) { + require.Nil(t, ep.PooledApplicationBudget) + } else if reflect.DeepEqual(param, config.Consensus[protocol.ConsensusFuture]) { + require.Equal(t, *ep.PooledApplicationBudget, uint64(param.MaxAppProgramCost*testCase.numAppCalls)) } }) } diff --git a/data/transactions/logic/debugger.go b/data/transactions/logic/debugger.go index 0e1d937744..1c1985bdaf 100644 --- a/data/transactions/logic/debugger.go +++ b/data/transactions/logic/debugger.go @@ -105,6 +105,10 @@ func makeDebugState(cx *EvalContext) DebugState { globals := make([]basics.TealValue, len(globalFieldSpecs)) for _, fs := range globalFieldSpecs { + // Don't try to grab app only fields when evaluating a signature + if (cx.runModeFlags&runModeSignature) != 0 && fs.mode == runModeApplication { + continue + } sv, err := cx.globalFieldToValue(fs) if err != nil { sv = stackValue{Bytes: []byte(err.Error())} diff --git a/data/transactions/logic/eval.go b/data/transactions/logic/eval.go index 749942a36a..f3e717324d 100644 --- a/data/transactions/logic/eval.go +++ b/data/transactions/logic/eval.go @@ -48,7 +48,7 @@ const EvalMaxVersion = LogicVersion // The constants below control TEAL opcodes evaluation and MAY NOT be changed // without moving them into consensus parameters. -// MaxStringSize is the limit of byte strings created by `concat` +// MaxStringSize is the limit of byte string length in an AVM value const MaxStringSize = 4096 // MaxByteMathSize is the limit of byte strings supplied as input to byte math opcodes @@ -211,7 +211,6 @@ type LedgerForLogic interface { AssetParams(aidx basics.AssetIndex) (basics.AssetParams, basics.Address, error) AppParams(aidx basics.AppIndex) (basics.AppParams, basics.Address, error) OptedIn(addr basics.Address, appIdx basics.AppIndex) (bool, error) - GetCreatableID(groupIdx int) basics.CreatableIndex GetLocal(addr basics.Address, appIdx basics.AppIndex, key string, accountIdx uint64) (value basics.TealValue, exists bool, err error) SetLocal(addr basics.Address, appIdx basics.AppIndex, key string, value basics.TealValue, accountIdx uint64) error @@ -225,29 +224,11 @@ type LedgerForLogic interface { Counter() uint64 } -// EvalSideEffects contains data returned from evaluation -type EvalSideEffects struct { - scratchSpace scratchSpace -} - -// MakePastSideEffects allocates and initializes a slice of EvalSideEffects of length `size` -func MakePastSideEffects(size int) (pastSideEffects []EvalSideEffects) { - pastSideEffects = make([]EvalSideEffects, size) - for j := range pastSideEffects { - pastSideEffects[j] = EvalSideEffects{} - } - return -} - -// getScratchValue loads and clones a stackValue -// The value is cloned so the original bytes are protected from changes -func (se *EvalSideEffects) getScratchValue(scratchPos uint8) stackValue { - return se.scratchSpace[scratchPos].clone() -} - -// setScratchSpace stores the scratch space -func (se *EvalSideEffects) setScratchSpace(scratch scratchSpace) { - se.scratchSpace = scratch +// resources contains a list of apps and assets. It's used to track the apps and +// assets created by a txgroup, for "free" access. +type resources struct { + asas []basics.AssetIndex + apps []basics.AppIndex } // EvalParams contains data that comes into condition evaluation. @@ -258,9 +239,9 @@ type EvalParams struct { TxnGroup []transactions.SignedTxnWithAD - PastSideEffects []EvalSideEffects + pastScratch []*scratchSpace - Logger logging.Logger + logger logging.Logger Ledger LedgerForLogic @@ -272,22 +253,31 @@ type EvalParams struct { // MinTealVersion is nil, we will compute it ourselves MinTealVersion *uint64 - // Amount "overpaid" by the top-level transactions of the group. Often 0. - // When positive, it can be spent by inner transactions. Shared across a - // group's txns, so that it can be updated. nil is interpreted as 0. + // Amount "overpaid" by the transactions of the group. Often 0. When + // positive, it can be spent by inner transactions. Shared across a group's + // txns, so that it can be updated (including upward, by overpaying inner + // transactions). nil is treated as 0 (used before fee pooling is enabled). FeeCredit *uint64 Specials *transactions.SpecialAddresses - // Total pool of app call budget in a group transaction (nil before pooling enabled) + // Total pool of app call budget in a group transaction (nil before budget pooling enabled) PooledApplicationBudget *uint64 - // Total allowable inner txns in a group transaction (nil before pooling enabled) + // Total allowable inner txns in a group transaction (nil before inner pooling enabled) pooledAllowedInners *int - // If non-zero, a txn counter value after which assets and apps should be - // allowed w/o foreign array. Left zero until createdResourcesVersion. - initialCounter uint64 + // created contains resources that may be used for "created" - they need not be in + // a foreign array. They remain empty until createdResourcesVersion. + created *resources + + // Caching these here means the hashes can be shared across the TxnGroup + // (and inners, because the cache is shared with the inner EvalParams) + appAddrCache map[basics.AppIndex]basics.Address + + // Cache the txid hashing, but do *not* share this into inner EvalParams, as + // the key is just the index in the txgroup. + txidCache map[int]transactions.Txid // The calling context, if this is an inner app call caller *EvalContext @@ -302,20 +292,16 @@ func copyWithClearAD(txgroup []transactions.SignedTxnWithAD) []transactions.Sign return copy } -// NewAppEvalParams creates an EvalParams to be used while evaluating apps for a top-level txgroup -func NewAppEvalParams(txgroup []transactions.SignedTxnWithAD, proto *config.ConsensusParams, specials *transactions.SpecialAddresses, counter uint64) *EvalParams { - minTealVersion := ComputeMinTealVersion(txgroup, false) - +// NewEvalParams creates an EvalParams to use while evaluating a top-level txgroup +func NewEvalParams(txgroup []transactions.SignedTxnWithAD, proto *config.ConsensusParams, specials *transactions.SpecialAddresses) *EvalParams { apps := 0 for _, tx := range txgroup { if tx.Txn.Type == protocol.ApplicationCallTx { apps++ } } - if apps == 0 { - // As an optimization, do no work if there will be no apps to evaluate in this txgroup - return nil - } + + minTealVersion := ComputeMinTealVersion(txgroup, false) var pooledApplicationBudget *uint64 var pooledAllowedInners *int @@ -332,22 +318,18 @@ func NewAppEvalParams(txgroup []transactions.SignedTxnWithAD, proto *config.Cons *pooledAllowedInners = apps * proto.MaxInnerTransactions } - if counter == 0 || proto.LogicSigVersion < createdResourcesVersion { - counter = math.MaxUint64 - } - - ep := &EvalParams{ - Proto: proto, + return &EvalParams{ TxnGroup: copyWithClearAD(txgroup), - PastSideEffects: MakePastSideEffects(len(txgroup)), + Proto: proto, + Specials: specials, + pastScratch: make([]*scratchSpace, len(txgroup)), MinTealVersion: &minTealVersion, FeeCredit: &credit, - Specials: specials, PooledApplicationBudget: pooledApplicationBudget, pooledAllowedInners: pooledAllowedInners, - initialCounter: counter, + created: &resources{}, + appAddrCache: make(map[basics.AppIndex]basics.Address), } - return ep } // NewInnerEvalParams creates an EvalParams to be used while evaluating an inner group txgroup @@ -374,14 +356,15 @@ func NewInnerEvalParams(txg []transactions.SignedTxn, caller *EvalContext) *Eval ep := &EvalParams{ Proto: caller.Proto, TxnGroup: copyWithClearAD(txgroup), - PastSideEffects: MakePastSideEffects(len(txgroup)), + pastScratch: make([]*scratchSpace, len(txgroup)), MinTealVersion: &minTealVersion, FeeCredit: caller.FeeCredit, Specials: caller.Specials, PooledApplicationBudget: caller.PooledApplicationBudget, pooledAllowedInners: caller.pooledAllowedInners, Ledger: caller.Ledger, - initialCounter: caller.initialCounter, + created: caller.created, + appAddrCache: caller.appAddrCache, caller: caller, } return ep @@ -421,13 +404,26 @@ func (r runMode) String() string { } func (ep EvalParams) log() logging.Logger { - if ep.Logger != nil { - return ep.Logger + if ep.logger != nil { + return ep.logger } return logging.Base() } -type scratchSpace = [256]stackValue +// RecordAD notes ApplyData information that was derived outside of the logic +// package. For example, after a acfg transaction is processed, the AD created +// by the acfg is added to the EvalParams this way. +func (ep *EvalParams) RecordAD(gi int, ad transactions.ApplyData) { + ep.TxnGroup[gi].ApplyData = ad + if aid := ad.ConfigAsset; aid != 0 { + ep.created.asas = append(ep.created.asas, aid) + } + if aid := ad.ApplicationID; aid != 0 { + ep.created.apps = append(ep.created.apps, aid) + } +} + +type scratchSpace [256]stackValue // EvalContext is the execution context of AVM bytecode. It contains the full // state of the running program, and tracks some of the things that the program @@ -480,8 +476,6 @@ type EvalContext struct { instructionStarts map[int]bool programHashCached crypto.Digest - txidCache map[int]transactions.Txid - appAddrCache map[basics.AppIndex]basics.Address // Stores state & disassembly for the optional debugger debugState DebugState @@ -559,7 +553,8 @@ func EvalContract(program []byte, gi int, aid basics.AppIndex, params *EvalParam *cx.pooledAllowedInners -= len(cx.Txn.EvalDelta.InnerTxns) } // update side effects - cx.PastSideEffects[cx.GroupIndex].setScratchSpace(cx.scratch) + cx.pastScratch[cx.GroupIndex] = &scratchSpace{} + *cx.pastScratch[cx.GroupIndex] = cx.scratch return pass, &cx, err } @@ -2019,12 +2014,12 @@ func (cx *EvalContext) getTxID(txn *transactions.Transaction, groupIndex int) tr } // Initialize txidCache if necessary - if cx.txidCache == nil { - cx.txidCache = make(map[int]transactions.Txid, len(cx.TxnGroup)) + if cx.EvalParams.txidCache == nil { + cx.EvalParams.txidCache = make(map[int]transactions.Txid, len(cx.TxnGroup)) } // Hashes are expensive, so we cache computed TxIDs - txid, ok := cx.txidCache[groupIndex] + txid, ok := cx.EvalParams.txidCache[groupIndex] if !ok { if cx.caller != nil { innerOffset := len(cx.caller.Txn.EvalDelta.InnerTxns) @@ -2032,7 +2027,7 @@ func (cx *EvalContext) getTxID(txn *transactions.Transaction, groupIndex int) tr } else { txid = txn.ID() } - cx.txidCache[groupIndex] = txid + cx.EvalParams.txidCache[groupIndex] = txid } return txid @@ -2611,27 +2606,28 @@ func (cx *EvalContext) getLatestTimestamp() (timestamp uint64, err error) { return uint64(ts), nil } -func (cx *EvalContext) getApplicationAddress() basics.Address { - // Initialize appAddrCache if necessary - if cx.appAddrCache == nil { - cx.appAddrCache = make(map[basics.AppIndex]basics.Address) - } - - appAddr, ok := cx.appAddrCache[cx.appID] +// getApplicationAddress memoizes app.Address() across a tx group's evaluation +func (cx *EvalContext) getApplicationAddress(app basics.AppIndex) basics.Address { + /* Do not instantiate the cache here, that would mask a programming error. + The cache must be instantiated at EvalParams construction time, so that + proper sharing with inner EvalParams can work. */ + appAddr, ok := cx.appAddrCache[app] if !ok { - appAddr = cx.appID.Address() - cx.appAddrCache[cx.appID] = appAddr + appAddr = app.Address() + cx.appAddrCache[app] = appAddr } return appAddr } func (cx *EvalContext) getCreatableID(groupIndex int) (cid uint64, err error) { - if cx.Ledger == nil { - err = fmt.Errorf("ledger not available") - return + if aid := cx.TxnGroup[groupIndex].ApplyData.ConfigAsset; aid != 0 { + return uint64(aid), nil } - return uint64(cx.Ledger.GetCreatableID(groupIndex)), nil + if aid := cx.TxnGroup[groupIndex].ApplyData.ApplicationID; aid != 0 { + return uint64(aid), nil + } + return 0, fmt.Errorf("Index %d did not create anything", groupIndex) } func (cx *EvalContext) getCreatorAddress() ([]byte, error) { @@ -2668,8 +2664,7 @@ func (cx *EvalContext) globalFieldToValue(fs globalFieldSpec) (sv stackValue, er case CurrentApplicationID: sv.Uint = uint64(cx.appID) case CurrentApplicationAddress: - var addr basics.Address - addr = cx.getApplicationAddress() + addr := cx.getApplicationAddress(cx.appID) sv.Bytes = addr[:] case CreatorAddress: sv.Bytes, err = cx.getCreatorAddress() @@ -2685,8 +2680,7 @@ func (cx *EvalContext) globalFieldToValue(fs globalFieldSpec) (sv stackValue, er } case CallerApplicationAddress: if cx.caller != nil { - var addr basics.Address - addr = cx.caller.getApplicationAddress() + addr := cx.caller.getApplicationAddress(cx.caller.appID) sv.Bytes = addr[:] } else { sv.Bytes = zeroAddress[:] @@ -2965,26 +2959,25 @@ func opStores(cx *EvalContext) { cx.stack = cx.stack[:prev] } -func opGloadImpl(cx *EvalContext, groupIdx int, scratchIdx byte, opName string) (scratchValue stackValue, err error) { +func opGloadImpl(cx *EvalContext, groupIdx int, scratchIdx byte, opName string) (stackValue, error) { + var none stackValue if groupIdx >= len(cx.TxnGroup) { - err = fmt.Errorf("%s lookup TxnGroup[%d] but it only has %d", opName, groupIdx, len(cx.TxnGroup)) - return - } else if int(scratchIdx) >= len(cx.scratch) { - err = fmt.Errorf("invalid Scratch index %d", scratchIdx) - return - } else if txn := cx.TxnGroup[groupIdx].Txn; txn.Type != protocol.ApplicationCallTx { - err = fmt.Errorf("can't use %s on non-app call txn with index %d", opName, groupIdx) - return - } else if groupIdx == cx.GroupIndex { - err = fmt.Errorf("can't use %s on self, use load instead", opName) - return - } else if groupIdx > cx.GroupIndex { - err = fmt.Errorf("%s can't get future scratch space from txn with index %d", opName, groupIdx) - return + return none, fmt.Errorf("%s lookup TxnGroup[%d] but it only has %d", opName, groupIdx, len(cx.TxnGroup)) + } + if int(scratchIdx) >= len(cx.scratch) { + return none, fmt.Errorf("invalid Scratch index %d", scratchIdx) + } + if cx.TxnGroup[groupIdx].Txn.Type != protocol.ApplicationCallTx { + return none, fmt.Errorf("can't use %s on non-app call txn with index %d", opName, groupIdx) + } + if groupIdx == cx.GroupIndex { + return none, fmt.Errorf("can't use %s on self, use load instead", opName) + } + if groupIdx > cx.GroupIndex { + return none, fmt.Errorf("%s can't get future scratch space from txn with index %d", opName, groupIdx) } - scratchValue = cx.PastSideEffects[groupIdx].getScratchValue(scratchIdx) - return + return cx.pastScratch[groupIdx][scratchIdx], nil } func opGload(cx *EvalContext) { @@ -3283,14 +3276,19 @@ func opExtract64Bits(cx *EvalContext) { opExtractNBytes(cx, 8) // extract 8 bytes } -// accountReference yields the address and Accounts offset designated -// by a stackValue. If the stackValue is the app account, it need not -// be in the Accounts array, therefore len(Accounts) + 1 is returned -// as the index. This unusual convention is based on the existing -// convention that 0 is the sender, 1-len(Accounts) are indexes into -// Accounts array, and so len+1 is the next available value. This -// will allow encoding into EvalDelta efficiently when it becomes -// necessary (when apps change local state on their own account). +// accountReference yields the address and Accounts offset designated by a +// stackValue. If the stackValue is the app account or an account of an app in +// created.apps, and it is not be in the Accounts array, then len(Accounts) + 1 +// is returned as the index. This would let us catch the mistake if the index is +// used for set/del. If the txn somehow "psychically" predicted the address, and +// therefore it IS in txn.Accounts, then happy day, we can set/del it. Return +// the proper index. + +// If we ever want apps to be able to change local state on these accounts +// (which includes this app's own account!), we will need a change to +// EvalDelta's on disk format, so that the addr can be encoded explicitly rather +// than by index into txn.Accounts. + func (cx *EvalContext) accountReference(account stackValue) (basics.Address, uint64, error) { if account.argType() == StackUint64 { addr, err := cx.Txn.Txn.AddressByIndex(account.Uint, cx.Txn.Txn.Sender) @@ -3302,17 +3300,40 @@ func (cx *EvalContext) accountReference(account stackValue) (basics.Address, uin } idx, err := cx.Txn.Txn.IndexByAddress(addr, cx.Txn.Txn.Sender) + invalidIndex := uint64(len(cx.Txn.Txn.Accounts) + 1) + // Allow an address for an app that was created in group + if err != nil && cx.version >= createdResourcesVersion { + for _, appID := range cx.created.apps { + createdAddress := cx.getApplicationAddress(appID) + if addr == createdAddress { + return addr, invalidIndex, nil + } + } + } + + // this app's address is also allowed if err != nil { - // Application address is acceptable. index is meaningless though - appAddr := cx.getApplicationAddress() + appAddr := cx.getApplicationAddress(cx.appID) if appAddr == addr { - return addr, uint64(len(cx.Txn.Txn.Accounts) + 1), nil + return addr, invalidIndex, nil } } return addr, idx, err } +func (cx *EvalContext) mutableAccountReference(account stackValue) (basics.Address, uint64, error) { + addr, accountIdx, err := cx.accountReference(account) + if err == nil && accountIdx > uint64(len(cx.Txn.Txn.Accounts)) { + // There was no error, but accountReference has signaled that accountIdx + // is not for mutable ops (because it can't encode it in EvalDelta) + // This also tells us that account.address() will work. + addr, _ := account.address() + err = fmt.Errorf("invalid Account reference for mutation %s", addr) + } + return addr, accountIdx, err +} + type opQuery func(basics.Address, *config.ConsensusParams) (basics.MicroAlgos, error) func opBalanceQuery(cx *EvalContext, query opQuery, item string) error { @@ -3533,7 +3554,7 @@ func opAppLocalPut(cx *EvalContext) { return } - addr, accountIdx, err := cx.accountReference(cx.stack[pprev]) + addr, accountIdx, err := cx.mutableAccountReference(cx.stack[pprev]) if err != nil { cx.err = err return @@ -3607,7 +3628,7 @@ func opAppLocalDel(cx *EvalContext) { return } - addr, accountIdx, err := cx.accountReference(cx.stack[prev]) + addr, accountIdx, err := cx.mutableAccountReference(cx.stack[prev]) if err == nil { if _, ok := cx.Txn.EvalDelta.LocalDeltas[accountIdx]; !ok { cx.Txn.EvalDelta.LocalDeltas[accountIdx] = basics.StateDelta{} @@ -3666,6 +3687,14 @@ func appReference(cx *EvalContext, ref uint64, foreign bool) (basics.AppIndex, e return appID, nil } } + // or was created in group + if cx.version >= createdResourcesVersion { + for _, appID := range cx.created.apps { + if appID == basics.AppIndex(ref) { + return appID, nil + } + } + } // It should be legal to use your own app id, which can't be in // ForeignApps during creation, because it is unknown then. But it can // be discovered in the app code. It's tempting to combine this with @@ -3698,14 +3727,19 @@ func asaReference(cx *EvalContext, ref uint64, foreign bool) (basics.AssetIndex, if ref < uint64(len(cx.Txn.Txn.ForeignAssets)) { return basics.AssetIndex(cx.Txn.Txn.ForeignAssets[ref]), nil } - if ref >= cx.initialCounter { - return basics.AssetIndex(ref), nil - } for _, assetID := range cx.Txn.Txn.ForeignAssets { if assetID == basics.AssetIndex(ref) { return assetID, nil } } + // or was created in group + if cx.version >= createdResourcesVersion { + for _, assetID := range cx.created.asas { + if assetID == basics.AssetIndex(ref) { + return assetID, nil + } + } + } } else { // Old rules if foreign { @@ -3871,13 +3905,13 @@ func authorizedSender(cx *EvalContext, addr basics.Address) bool { if err != nil { return false } - return cx.getApplicationAddress() == authorizer + return cx.getApplicationAddress(cx.appID) == authorizer } // addInnerTxn appends a fresh SignedTxn to subtxns, populated with reasonable // defaults. func addInnerTxn(cx *EvalContext) error { - addr := cx.getApplicationAddress() + addr := cx.getApplicationAddress(cx.appID) // For compatibility with v5, in which failures only occurred in the submit, // we only fail here if we are already over the max inner limit. Thus this @@ -3952,19 +3986,27 @@ func (cx *EvalContext) availableAccount(sv stackValue) (basics.Address, error) { // don't need (or want!) to allow low numbers to represent the asset at that // index in ForeignAssets array. func (cx *EvalContext) availableAsset(sv stackValue) (basics.AssetIndex, error) { - aid, err := sv.uint() + uint, err := sv.uint() if err != nil { return basics.AssetIndex(0), err } - if aid >= cx.initialCounter { - return basics.AssetIndex(aid), nil - } + aid := basics.AssetIndex(uint) + // Ensure that aid is in Foreign Assets for _, assetID := range cx.Txn.Txn.ForeignAssets { - if assetID == basics.AssetIndex(aid) { - return basics.AssetIndex(aid), nil + if assetID == aid { + return aid, nil } } + // or was created in group + if cx.version >= createdResourcesVersion { + for _, assetID := range cx.created.asas { + if assetID == aid { + return aid, nil + } + } + } + return basics.AssetIndex(0), fmt.Errorf("invalid Asset reference %d", aid) } @@ -3977,12 +4019,21 @@ func (cx *EvalContext) availableApp(sv stackValue) (basics.AppIndex, error) { return basics.AppIndex(0), err } aid := basics.AppIndex(uint) + // Ensure that aid is in Foreign Apps for _, appID := range cx.Txn.Txn.ForeignApps { if appID == aid { return aid, nil } } + // or was created in group + if cx.version >= createdResourcesVersion { + for _, appID := range cx.created.apps { + if appID == aid { + return aid, nil + } + } + } // Or, it can be the current app if cx.appID == aid { return aid, nil @@ -4340,6 +4391,9 @@ func opTxSubmit(cx *EvalContext) { cx.err = err return } + // This is mostly a no-op, because Perform does its work "in-place", but + // RecordAD has some further responsibilities. + ep.RecordAD(i, ep.TxnGroup[i].ApplyData) } cx.Txn.EvalDelta.InnerTxns = append(cx.Txn.EvalDelta.InnerTxns, ep.TxnGroup...) cx.subtxns = nil diff --git a/data/transactions/logic/evalAppTxn_test.go b/data/transactions/logic/evalAppTxn_test.go index 13f7759c45..3a568f8d23 100644 --- a/data/transactions/logic/evalAppTxn_test.go +++ b/data/transactions/logic/evalAppTxn_test.go @@ -1315,7 +1315,7 @@ int 222; itxn_field ApplicationID itxn_submit ` txg := []transactions.SignedTxnWithAD{tx} - ep := NewAppEvalParams(txg, MakeTestProto(), &transactions.SpecialAddresses{}, 0) + ep := NewEvalParams(txg, MakeTestProto(), &transactions.SpecialAddresses{}) ep.Ledger = ledger TestApp(t, callpay3+"int 1", ep, "insufficient balance") // inner contract needs money @@ -1344,7 +1344,7 @@ func TestCreateAndUse(t *testing.T) { int 1 ` - // First testing in axfer + // First testing use in axfer ep, tx, ledger := MakeSampleEnv() ledger.NewApp(tx.Receiver, 888, basics.AppParams{}) ledger.NewAccount(appAddr(888), 4*MakeTestProto().MinTxnFee) @@ -1384,7 +1384,7 @@ func TestCreateAndUse(t *testing.T) { int 1 ` - // Now as in asset balance opcode + // Now test use in asset balance opcode ep, tx, ledger = MakeSampleEnv() ledger.NewApp(tx.Receiver, 888, basics.AppParams{}) ledger.NewAccount(appAddr(888), 4*MakeTestProto().MinTxnFee) @@ -1409,7 +1409,7 @@ func TestCreateAndUse(t *testing.T) { int 1 ` - // Now as ForeigAsset + // Now as ForeignAsset ep, tx, ledger = MakeSampleEnv() ledger.NewApp(tx.Receiver, 888, basics.AppParams{}) ledger.NewApp(tx.Receiver, 888, basics.AppParams{}) @@ -1422,3 +1422,179 @@ func TestCreateAndUse(t *testing.T) { // ep.Proto = MakeTestProtoV(CreatedResourcesVersion - 1) // TestApp(t, appcall, ep, "invalid Asset reference") } + +// 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 fmt.Sprintf(`txn ApplicationID + bz end + %s + end: int 1`, source) +} + +func hexProgram(t *testing.T, source string) string { + return "0x" + hex.EncodeToString(TestProg(t, source, AssemblerMaxVersion).Program) +} + +// TestCreateAndUseApp checks that an app can be created in an inner txn, and then +// the address for it can be looked up. +func TestCreateUseApp(t *testing.T) { + pay5back := main(` +itxn_begin +int pay; itxn_field TypeEnum +txn Sender; itxn_field Receiver +int 5; itxn_field Amount +itxn_submit +int 1 +`) + + createAndUse := ` + itxn_begin + int appl; itxn_field TypeEnum + byte ` + hexProgram(t, pay5back) + `; itxn_field ApprovalProgram; + itxn_submit + + itxn CreatedApplicationID; app_params_get AppAddress; assert + addr ` + appAddr(5000).String() + ` + == +` + + ep, tx, ledger := MakeSampleEnv() + ledger.NewApp(tx.Receiver, 888, basics.AppParams{}) + ledger.NewAccount(appAddr(888), 1*MakeTestProto().MinTxnFee) + TestApp(t, createAndUse, ep) + // Again, can't test if this (properly) fails in previous version, because + // we can't even create apps this way in previous version. +} + +// TestCreateAndPay checks that an app can be created in an inner app, and then +// a pay can be done to the app's account. This was not allowed until v6, +// because of the strict adherence to the foreign-accounts rules. +func TestCreateAndPay(t *testing.T) { + pay5back := main(` +itxn_begin +int pay; itxn_field TypeEnum +txn Sender; itxn_field Receiver +int 5; itxn_field Amount +itxn_submit +int 1 +`) + + createAndPay := ` + itxn_begin + int appl; itxn_field TypeEnum + ` + fmt.Sprintf("byte %s; itxn_field ApprovalProgram;", hexProgram(t, pay5back)) + ` + itxn_submit + + itxn_begin + int pay; itxn_field TypeEnum + itxn CreatedApplicationID; app_params_get AppAddress; assert; itxn_field Receiver + int 10; itxn_field Amount + itxn_submit + + int 1 +` + + ep, tx, ledger := MakeSampleEnv() + ledger.NewApp(tx.Receiver, 888, basics.AppParams{}) + ledger.NewAccount(appAddr(888), 10*MakeTestProto().MinTxnFee) + TestApp(t, createAndPay, ep) + + // This test is impossible because CreatedResourcesVersion is also when + // inner txns could make apps. + // ep.Proto = MakeTestProtoV(CreatedResourcesVersion - 1) + // TestApp(t, createAndPay, ep, "invalid Address reference") +} + +// TestInnerGaid ensures there's no confusion over the tracking of ids +// across multiple inner transaction groups +func TestInnerGaid(t *testing.T) { + ep, tx, ledger := MakeSampleEnv() + ep.Proto.MaxInnerTransactions = 100 + // App to log the aid of slot[apparg[0]] + logGaid := TestProg(t, `txn ApplicationArgs 0; btoi; gaids; itob; log; int 1`, AssemblerMaxVersion) + ledger.NewApp(tx.Receiver, 222, basics.AppParams{ + ApprovalProgram: logGaid.Program, + }) + + ledger.NewApp(tx.Receiver, 888, basics.AppParams{}) + ledger.NewAccount(appAddr(888), 50_000) + tx.ForeignApps = []basics.AppIndex{basics.AppIndex(222)} + TestApp(t, `itxn_begin +int acfg; itxn_field TypeEnum +itxn_next +int pay; itxn_field TypeEnum +txn Sender; itxn_field Receiver +itxn_next +int appl; itxn_field TypeEnum +int 222; itxn_field ApplicationID +int 0; itob; itxn_field ApplicationArgs +itxn_submit +itxn Logs 0 +btoi +int 5000 +== +assert + +// Swap the pay and acfg, ensure gaid 1 works instead +itxn_begin +int pay; itxn_field TypeEnum +txn Sender; itxn_field Receiver +itxn_next +int acfg; itxn_field TypeEnum +itxn_next +int appl; itxn_field TypeEnum +int 222; itxn_field ApplicationID +int 1; itob; itxn_field ApplicationArgs +itxn_submit +itxn Logs 0 +btoi +int 5001 +== +assert + + +int 1 +`, ep) + + // Nearly identical, but ensures that gaid 0 FAILS in the second group + TestApp(t, `itxn_begin +int acfg; itxn_field TypeEnum +itxn_next +int pay; itxn_field TypeEnum +txn Sender; itxn_field Receiver +itxn_next +int appl; itxn_field TypeEnum +int 222; itxn_field ApplicationID +int 0; itob; itxn_field ApplicationArgs +itxn_submit +itxn Logs 0 +btoi +int 5000 +== +assert + +// Swap the pay and acfg, ensure gaid 1 works instead +itxn_begin +int pay; itxn_field TypeEnum +txn Sender; itxn_field Receiver +itxn_next +int acfg; itxn_field TypeEnum +itxn_next +int appl; itxn_field TypeEnum +int 222; itxn_field ApplicationID +int 0; itob; itxn_field ApplicationArgs +itxn_submit +itxn Logs 0 +btoi +int 5001 +== +assert + + +int 1 +`, ep, "assert failed") + +} diff --git a/data/transactions/logic/evalStateful_test.go b/data/transactions/logic/evalStateful_test.go index 5266e123cb..972e6f37f6 100644 --- a/data/transactions/logic/evalStateful_test.go +++ b/data/transactions/logic/evalStateful_test.go @@ -336,7 +336,7 @@ func testApps(t *testing.T, programs []string, txgroup []transactions.SignedTxn, codes[i] = testProg(t, program, version).Program } } - ep := NewAppEvalParams(transactions.WrapSignedTxnsWithAD(txgroup), makeTestProtoV(version), &transactions.SpecialAddresses{}, 0) + ep := NewEvalParams(transactions.WrapSignedTxnsWithAD(txgroup), makeTestProtoV(version), &transactions.SpecialAddresses{}) ep.Ledger = ledger testAppsBytes(t, codes, ep, expected...) } @@ -2191,6 +2191,7 @@ func TestReturnTypes(t *testing.T) { []byte("aoeu2"), []byte("aoeu3"), } + ep.pastScratch[0] = &scratchSpace{} // for gload ledger.NewAccount(tx.Sender, 1) params := basics.AssetParams{ Total: 1000, @@ -2206,7 +2207,6 @@ func TestReturnTypes(t *testing.T) { } ledger.NewAsset(tx.Sender, 1, params) ledger.NewApp(tx.Sender, 1, basics.AppParams{}) - ledger.SetTrackedCreatable(0, basics.CreatableLocator{Index: 1}) ledger.NewAccount(tx.Receiver, 1000000) ledger.NewLocals(tx.Receiver, 1) key, err := hex.DecodeString("33343536") @@ -2299,11 +2299,11 @@ func TestReturnTypes(t *testing.T) { cx.runModeFlags = m cx.appID = 1 - // These two are silly, but one test needs a higher gi, and - // another needs to work on the args that were put in txn[0]. - // This convinces them both to work. Revisit. + // These set conditions for some ops that examine the group. + // This convinces them all to work. Revisit. cx.Txn = &ep.TxnGroup[0] cx.GroupIndex = 1 + cx.TxnGroup[0].ConfigAsset = 100 eval(ops.Program, &cx) @@ -2412,8 +2412,7 @@ func TestAppInfo(t *testing.T) { } func TestBudget(t *testing.T) { - ep, tx, ledger := makeSampleEnv() - ledger.NewApp(tx.Receiver, 888, basics.AppParams{}) + ep := defaultEvalParams(nil) source := ` global OpcodeBudget int 699 @@ -2425,3 +2424,40 @@ int 695 ` testApp(t, source, ep) } + +func TestSelfMutate(t *testing.T) { + ep, _, ledger := makeSampleEnv() + + /* In order to test the added protection of mutableAccountReference, we're + going to set up a ledger in which an app account is opted into + itself. That was impossible before v6, and indeed we did not have the + extra mutable reference check then. */ + ledger.NewLocals(basics.AppIndex(888).Address(), 888) + ledger.NewLocal(basics.AppIndex(888).Address(), 888, "hey", + basics.TealValue{Type: basics.TealUintType, Uint: 77}) + + source := ` +global CurrentApplicationAddress +byte "hey" +int 42 +app_local_put +` + testApp(t, source, ep, "invalid Account reference for mutation") + + source = ` +global CurrentApplicationAddress +byte "hey" +app_local_del +` + testApp(t, source, ep, "invalid Account reference for mutation") + + /* But let's just check normal access is working properly. */ + source = ` +global CurrentApplicationAddress +byte "hey" +app_local_get +int 77 +== +` + testApp(t, source, ep) +} diff --git a/data/transactions/logic/eval_test.go b/data/transactions/logic/eval_test.go index be4a4c04c0..9f30fda9a8 100644 --- a/data/transactions/logic/eval_test.go +++ b/data/transactions/logic/eval_test.go @@ -21,7 +21,6 @@ import ( "encoding/binary" "encoding/hex" "fmt" - "math" "strconv" "strings" "testing" @@ -130,9 +129,9 @@ func defaultEvalParamsWithVersion(txn *transactions.SignedTxn, version uint64) * return ep } -// reset zeros out the ApplyDatas in the underlying TxnGroup. This is in -// *_test.go because no real code should ever need this. EvalParams should be -// created to evaluate a group, and then thrown away. +// reset puts an ep back into its original state. This is in *_test.go because +// no real code should ever need this. EvalParams should be created to evaluate +// a group, and then thrown away. func (ep *EvalParams) reset() { if ep.Proto.EnableAppCostPooling { budget := uint64(ep.Proto.MaxAppProgramCost) @@ -142,19 +141,13 @@ func (ep *EvalParams) reset() { inners := ep.Proto.MaxInnerTransactions ep.pooledAllowedInners = &inners } - ep.PastSideEffects = MakePastSideEffects(ep.Proto.MaxTxGroupSize) + ep.pastScratch = make([]*scratchSpace, ep.Proto.MaxTxGroupSize) for i := range ep.TxnGroup { ep.TxnGroup[i].ApplyData = transactions.ApplyData{} } - if ep.Proto.LogicSigVersion < createdResourcesVersion { - ep.initialCounter = math.MaxUint64 - } else { - if ep.Ledger != nil { - ep.initialCounter = ep.Ledger.Counter() - } else { - ep.initialCounter = firstTestID - } - } + ep.created = &resources{} + ep.appAddrCache = make(map[basics.AppIndex]basics.Address) + ep.Trace = &strings.Builder{} } func TestTooManyArgs(t *testing.T) { @@ -1622,12 +1615,7 @@ func TestGaid(t *testing.T) { partitiontest.PartitionTest(t) t.Parallel() - checkCreatableIDProg := ` -gaid 0 -int 100 -== -` - ops := testProg(t, checkCreatableIDProg, 4) + check0 := testProg(t, "gaid 0; int 100; ==", 4) txn := makeSampleTxn() txn.Txn.Type = protocol.ApplicationCallTx txgroup := make([]transactions.SignedTxn, 3) @@ -1635,12 +1623,17 @@ int 100 targetTxn := makeSampleTxn() targetTxn.Txn.Type = protocol.AssetConfigTx txgroup[0] = targetTxn - ledger := MakeLedger(nil) - ledger.SetTrackedCreatable(0, basics.CreatableLocator{Index: 100}) ep := defaultEvalParams(nil) - ep.Ledger = ledger ep.TxnGroup = transactions.WrapSignedTxnsWithAD(txgroup) - pass, err := EvalApp(ops.Program, 1, 0, ep) + ep.Ledger = MakeLedger(nil) + + // should fail when no creatable was created + _, err := EvalApp(check0.Program, 1, 0, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "the txn did not create anything") + + ep.TxnGroup[0].ApplyData.ConfigAsset = 100 + pass, err := EvalApp(check0.Program, 1, 0, ep) if !pass || err != nil { t.Log(ep.Trace.String()) } @@ -1648,35 +1641,22 @@ int 100 require.True(t, pass) // should fail when accessing future transaction in group - futureCreatableIDProg := ` -gaid 2 -int 0 -> -` - - ops = testProg(t, futureCreatableIDProg, 4) - _, err = EvalApp(ops.Program, 1, 0, ep) + check2 := testProg(t, "gaid 2; int 0; >", 4) + _, err = EvalApp(check2.Program, 1, 0, ep) require.Error(t, err) require.Contains(t, err.Error(), "gaid can't get creatable ID of txn ahead of the current one") // should fail when accessing self - ops = testProg(t, checkCreatableIDProg, 4) - _, err = EvalApp(ops.Program, 0, 0, ep) + _, err = EvalApp(check0.Program, 0, 0, ep) require.Error(t, err) require.Contains(t, err.Error(), "gaid is only for accessing creatable IDs of previous txns") // should fail on non-creatable ep.TxnGroup[0].Txn.Type = protocol.PaymentTx - _, err = EvalApp(ops.Program, 1, 0, ep) + _, err = EvalApp(check0.Program, 1, 0, ep) require.Error(t, err) require.Contains(t, err.Error(), "can't use gaid on txn that is not an app call nor an asset config txn") ep.TxnGroup[0].Txn.Type = protocol.AssetConfigTx - - // should fail when no creatable was created - ledger.SetTrackedCreatable(0, basics.CreatableLocator{}) - _, err = EvalApp(ops.Program, 1, 0, ep) - require.Error(t, err) - require.Contains(t, err.Error(), "the txn did not create anything") } func TestGtxn(t *testing.T) { @@ -2460,9 +2440,9 @@ int 1`, } ep := &EvalParams{ - Proto: makeTestProto(), - TxnGroup: txgroup, - PastSideEffects: MakePastSideEffects(2), + Proto: makeTestProto(), + TxnGroup: txgroup, + pastScratch: make([]*scratchSpace, 2), } switch failCase.runMode { @@ -2825,7 +2805,7 @@ func TestPanic(t *testing.T) { } } params := defaultEvalParams(nil) - params.Logger = log + params.logger = log params.TxnGroup[0].Lsig.Logic = ops.Program err := CheckSignature(0, params) require.Error(t, err) @@ -2839,7 +2819,7 @@ func TestPanic(t *testing.T) { var txn transactions.SignedTxn txn.Lsig.Logic = ops.Program params = defaultEvalParams(&txn) - params.Logger = log + params.logger = log pass, err := EvalSignature(0, params) if pass { t.Log(hex.EncodeToString(ops.Program)) diff --git a/data/transactions/logic/ledger_test.go b/data/transactions/logic/ledger_test.go index 0769419d55..ede07b6023 100644 --- a/data/transactions/logic/ledger_test.go +++ b/data/transactions/logic/ledger_test.go @@ -61,12 +61,11 @@ type asaParams struct { // Ledger is a fake ledger that is "good enough" to reasonably test AVM programs. type Ledger struct { - balances map[basics.Address]balanceRecord - applications map[basics.AppIndex]appParams - assets map[basics.AssetIndex]asaParams - trackedCreatables map[int]basics.CreatableIndex - mods map[basics.AppIndex]map[string]basics.ValueDelta - rnd basics.Round + balances map[basics.Address]balanceRecord + applications map[basics.AppIndex]appParams + assets map[basics.AssetIndex]asaParams + mods map[basics.AppIndex]map[string]basics.ValueDelta + rnd basics.Round } // MakeLedger constructs a Ledger with the given balances. @@ -78,7 +77,6 @@ func MakeLedger(balances map[basics.Address]uint64) *Ledger { } l.applications = make(map[basics.AppIndex]appParams) l.assets = make(map[basics.AssetIndex]asaParams) - l.trackedCreatables = make(map[int]basics.CreatableIndex) l.mods = make(map[basics.AppIndex]map[string]basics.ValueDelta) return l } @@ -430,18 +428,6 @@ func (l *Ledger) OptedIn(addr basics.Address, appIdx basics.AppIndex) (bool, err return ok, nil } -// SetTrackedCreatable remembers that the given cl "happened" in txn -// groupIdx of the group, for use by GetCreatableID. -func (l *Ledger) SetTrackedCreatable(groupIdx int, cl basics.CreatableLocator) { - l.trackedCreatables[groupIdx] = cl.Index -} - -// GetCreatableID returns the creatable constructed in a given transaction -// slot. For the test ledger, that's been set up by SetTrackedCreatable -func (l *Ledger) GetCreatableID(groupIdx int) basics.CreatableIndex { - return l.trackedCreatables[groupIdx] -} - // AssetHolding gives the amount of an ASA held by an account, or // error if the account is not opted into the asset. func (l *Ledger) AssetHolding(addr basics.Address, assetID basics.AssetIndex) (basics.AssetHolding, error) { diff --git a/ledger/apply/application.go b/ledger/apply/application.go index a178723e38..ee59b82080 100644 --- a/ledger/apply/application.go +++ b/ledger/apply/application.go @@ -342,11 +342,7 @@ func ApplicationCall(ac transactions.ApplicationCallTxnFields, header transactio if err != nil { return } - // No separate config for activating storage in AD because - // inner transactions can't be turned on without this change. - if balances.ConsensusParams().MaxInnerTransactions > 0 { - ad.ApplicationID = appIdx - } + ad.ApplicationID = appIdx } // Fetch the application parameters, if they exist diff --git a/ledger/apply/asset.go b/ledger/apply/asset.go index 0796cb8f7f..9aaacde4d5 100644 --- a/ledger/apply/asset.go +++ b/ledger/apply/asset.go @@ -100,12 +100,7 @@ func AssetConfig(cc transactions.AssetConfigTxnFields, header transactions.Heade return err } - // Record the index used. No separate config for activating - // storage in AD because inner transactions can't be turned on - // without this change. - if balances.ConsensusParams().MaxInnerTransactions > 0 { - ad.ConfigAsset = newidx - } + ad.ConfigAsset = newidx // Tell the cow what asset we created err = balances.AllocateAsset(header.Sender, newidx, true) diff --git a/ledger/internal/appcow.go b/ledger/internal/appcow.go index 012278bf30..ee6e424951 100644 --- a/ledger/internal/appcow.go +++ b/ledger/internal/appcow.go @@ -256,8 +256,6 @@ func (cb *roundCowState) AllocateApp(addr basics.Address, aidx basics.AppIndex, cb.mods.ModifiedAppLocalStates[aa] = true } - cb.trackCreatable(basics.CreatableIndex(aidx)) - return nil } diff --git a/ledger/internal/applications.go b/ledger/internal/applications.go index 962e8a18c4..5bccc88c41 100644 --- a/ledger/internal/applications.go +++ b/ledger/internal/applications.go @@ -33,7 +33,6 @@ type logicLedger struct { type cowForLogicLedger interface { Get(addr basics.Address, withPendingRewards bool) (basics.AccountData, error) - GetCreatableID(groupIdx int) basics.CreatableIndex GetCreator(cidx basics.CreatableIndex, ctype basics.CreatableType) (basics.Address, bool, error) GetKey(addr basics.Address, aidx basics.AppIndex, global bool, key string, accountIdx uint64) (basics.TealValue, bool, error) BuildEvalDelta(aidx basics.AppIndex, txn *transactions.Transaction) (transactions.EvalDelta, error) @@ -84,10 +83,6 @@ func (al *logicLedger) Authorizer(addr basics.Address) (basics.Address, error) { return addr, nil } -func (al *logicLedger) GetCreatableID(groupIdx int) basics.CreatableIndex { - return al.cow.GetCreatableID(groupIdx) -} - func (al *logicLedger) AssetHolding(addr basics.Address, assetIdx basics.AssetIndex) (basics.AssetHolding, error) { // Fetch the requested balance record record, err := al.cow.Get(addr, false) diff --git a/ledger/internal/applications_test.go b/ledger/internal/applications_test.go index cc10166976..63a71d9c4c 100644 --- a/ledger/internal/applications_test.go +++ b/ledger/internal/applications_test.go @@ -43,7 +43,6 @@ type mockCowForLogicLedger struct { cr map[creatableLocator]basics.Address brs map[basics.Address]basics.AccountData stores map[storeLocator]basics.TealKeyValue - tcs map[int]basics.CreatableIndex txc uint64 } @@ -55,10 +54,6 @@ func (c *mockCowForLogicLedger) Get(addr basics.Address, withPendingRewards bool return br, nil } -func (c *mockCowForLogicLedger) GetCreatableID(groupIdx int) basics.CreatableIndex { - return c.tcs[groupIdx] -} - func (c *mockCowForLogicLedger) GetCreator(cidx basics.CreatableIndex, ctype basics.CreatableType) (basics.Address, bool, error) { addr, found := c.cr[creatableLocator{cidx, ctype}] return addr, found, nil diff --git a/ledger/internal/assetcow.go b/ledger/internal/assetcow.go index b28d09a7f4..f56af1bdb7 100644 --- a/ledger/internal/assetcow.go +++ b/ledger/internal/assetcow.go @@ -28,8 +28,6 @@ func (cs *roundCowState) AllocateAsset(addr basics.Address, index basics.AssetIn Creator: addr, Created: true, } - - cs.trackCreatable(basics.CreatableIndex(index)) } else { aa := ledgercore.AccountAsset{ Address: addr, diff --git a/ledger/internal/cow.go b/ledger/internal/cow.go index 4bf546de9c..43614a5808 100644 --- a/ledger/internal/cow.go +++ b/ledger/internal/cow.go @@ -74,11 +74,6 @@ type roundCowState struct { // cache mainaining accountIdx used in getKey for local keys access compatibilityGetKeyCache map[basics.Address]map[storagePtr]uint64 - // index of a txn within a group; used in conjunction with trackedCreatables - groupIdx int - // track creatables created during each transaction in the round - trackedCreatables map[int]basics.CreatableIndex - // prevTotals contains the accounts totals for the previous round. It's being used to calculate the totals for the new round // so that we could perform the validation test on these to ensure the block evaluator generate a valid changeset. prevTotals ledgercore.AccountTotals @@ -86,13 +81,12 @@ type roundCowState struct { func makeRoundCowState(b roundCowParent, hdr bookkeeping.BlockHeader, proto config.ConsensusParams, prevTimestamp int64, prevTotals ledgercore.AccountTotals, hint int) *roundCowState { cb := roundCowState{ - lookupParent: b, - commitParent: nil, - proto: proto, - mods: ledgercore.MakeStateDelta(&hdr, prevTimestamp, hint, 0), - sdeltas: make(map[basics.Address]map[storagePtr]*storageDelta), - trackedCreatables: make(map[int]basics.CreatableIndex), - prevTotals: prevTotals, + lookupParent: b, + commitParent: nil, + proto: proto, + mods: ledgercore.MakeStateDelta(&hdr, prevTimestamp, hint, 0), + sdeltas: make(map[basics.Address]map[storagePtr]*storageDelta), + prevTotals: prevTotals, } // compatibilityMode retains producing application' eval deltas under the following rule: @@ -149,10 +143,6 @@ func (cb *roundCowState) prevTimestamp() int64 { return cb.mods.PrevTimestamp } -func (cb *roundCowState) getCreatableIndex(groupIdx int) basics.CreatableIndex { - return cb.trackedCreatables[groupIdx] -} - func (cb *roundCowState) getCreator(cidx basics.CreatableIndex, ctype basics.CreatableType) (creator basics.Address, ok bool, err error) { delta, ok := cb.mods.Creatables[cidx] if ok { @@ -204,10 +194,6 @@ func (cb *roundCowState) blockHdr(r basics.Round) (bookkeeping.BlockHeader, erro return cb.lookupParent.blockHdr(r) } -func (cb *roundCowState) trackCreatable(creatableIndex basics.CreatableIndex) { - cb.trackedCreatables[cb.groupIdx] = creatableIndex -} - func (cb *roundCowState) incTxnCount() { cb.txnCount++ } @@ -233,12 +219,6 @@ func (cb *roundCowState) child(hint int) *roundCowState { sdeltas: make(map[basics.Address]map[storagePtr]*storageDelta), } - // clone tracked creatables - ch.trackedCreatables = make(map[int]basics.CreatableIndex) - for i, tc := range cb.trackedCreatables { - ch.trackedCreatables[i] = tc - } - if cb.compatibilityMode { ch.compatibilityMode = cb.compatibilityMode ch.compatibilityGetKeyCache = make(map[basics.Address]map[storagePtr]uint64) @@ -246,11 +226,6 @@ func (cb *roundCowState) child(hint int) *roundCowState { return &ch } -// setGroupIdx sets this transaction's index within its group -func (cb *roundCowState) setGroupIdx(txnIdx int) { - cb.groupIdx = txnIdx -} - func (cb *roundCowState) commitToParent() { cb.commitParent.mods.Accts.MergeAccounts(cb.mods.Accts) diff --git a/ledger/internal/eval.go b/ledger/internal/eval.go index 225ae227aa..c0c3e392a9 100644 --- a/ledger/internal/eval.go +++ b/ledger/internal/eval.go @@ -286,10 +286,6 @@ func (cs *roundCowState) Get(addr basics.Address, withPendingRewards bool) (basi return acct, nil } -func (cs *roundCowState) GetCreatableID(groupIdx int) basics.CreatableIndex { - return cs.getCreatableIndex(groupIdx) -} - func (cs *roundCowState) GetCreator(cidx basics.CreatableIndex, ctype basics.CreatableType) (basics.Address, bool, error) { return cs.getCreator(cidx, ctype) } @@ -744,14 +740,13 @@ func (eval *BlockEvaluator) transactionGroup(txgroup []transactions.SignedTxnWit var groupTxBytes int cow := eval.state.child(len(txgroup)) - evalParams := logic.NewAppEvalParams(txgroup, &eval.proto, &eval.specials, cow.txnCounter()) + evalParams := logic.NewEvalParams(txgroup, &eval.proto, &eval.specials) // Evaluate each transaction in the group txibs = make([]transactions.SignedTxnInBlock, 0, len(txgroup)) for gi, txad := range txgroup { var txib transactions.SignedTxnInBlock - cow.setGroupIdx(gi) err := eval.transaction(txad.SignedTxn, evalParams, gi, txad.ApplyData, cow, &txib) if err != nil { return err @@ -968,6 +963,11 @@ func (eval *BlockEvaluator) applyTransaction(tx transactions.Transaction, balanc err = fmt.Errorf("Unknown transaction type %v", tx.Type) } + // Record first, so that details can all be used in logic evaluation, even + // if cleared below. For example, `gaid`, introduced in v28 is now + // implemented in terms of the AD fields introduced in v30. + evalParams.RecordAD(gi, ad) + // If the protocol does not support rewards in ApplyData, // clear them out. if !params.RewardsInApplyData { @@ -975,8 +975,13 @@ func (eval *BlockEvaluator) applyTransaction(tx transactions.Transaction, balanc ad.ReceiverRewards = basics.MicroAlgos{} ad.CloseRewards = basics.MicroAlgos{} } - if evalParams != nil { - evalParams.TxnGroup[gi].ApplyData = ad + + // No separate config for activating these AD fields because inner + // transactions require their presence, so the consensus update to add + // inners also stores these IDs. + if params.MaxInnerTransactions == 0 { + ad.ApplicationID = 0 + ad.ConfigAsset = 0 } return