diff options
Diffstat (limited to 'data/transactions/logic/eval.go')
-rw-r--r-- | data/transactions/logic/eval.go | 1564 |
1 files changed, 996 insertions, 568 deletions
diff --git a/data/transactions/logic/eval.go b/data/transactions/logic/eval.go index 0157e724d..e3d39535d 100644 --- a/data/transactions/logic/eval.go +++ b/data/transactions/logic/eval.go @@ -25,7 +25,6 @@ import ( "encoding/hex" "errors" "fmt" - "io" "math" "math/big" "math/bits" @@ -49,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 @@ -116,6 +115,16 @@ func (sv *stackValue) uint() (uint64, error) { return sv.Uint, nil } +func (sv *stackValue) uintMaxed(max uint64) (uint64, error) { + if sv.Bytes != nil { + return 0, fmt.Errorf("%#v is not a uint64", sv.Bytes) + } + if sv.Uint > max { + return 0, fmt.Errorf("%d is larger than max=%d", sv.Uint, max) + } + return sv.Uint, nil +} + func (sv *stackValue) bool() (bool, error) { u64, err := sv.uint() if err != nil { @@ -158,8 +167,10 @@ func stackValueFromTealValue(tv *basics.TealValue) (sv stackValue, err error) { // newly-introduced transaction fields from breaking assumptions made by older // versions of TEAL. If one of the transactions in a group will execute a TEAL // program whose version predates a given field, that field must not be set -// anywhere in the transaction group, or the group will be rejected. -func ComputeMinTealVersion(group []transactions.SignedTxn) uint64 { +// anywhere in the transaction group, or the group will be rejected. In +// addition, inner app calls must not call teal from before inner app calls were +// introduced. +func ComputeMinTealVersion(group []transactions.SignedTxnWithAD, inner bool) uint64 { var minVersion uint64 for _, txn := range group { if !txn.Txn.RekeyTo.IsZero() { @@ -172,6 +183,11 @@ func ComputeMinTealVersion(group []transactions.SignedTxn) uint64 { minVersion = appsEnabledVersion } } + if inner { + if minVersion < innerAppsEnabledVersion { + minVersion = innerAppsEnabledVersion + } + } } return minVersion } @@ -194,65 +210,38 @@ type LedgerForLogic interface { AssetHolding(addr basics.Address, assetIdx basics.AssetIndex) (basics.AssetHolding, error) AssetParams(aidx basics.AssetIndex) (basics.AssetParams, basics.Address, error) AppParams(aidx basics.AppIndex) (basics.AppParams, basics.Address, error) - ApplicationID() basics.AppIndex 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, key string, value basics.TealValue, accountIdx uint64) error - DelLocal(addr basics.Address, key string, accountIdx uint64) error + SetLocal(addr basics.Address, appIdx basics.AppIndex, key string, value basics.TealValue, accountIdx uint64) error + DelLocal(addr basics.Address, appIdx basics.AppIndex, key string, accountIdx uint64) error GetGlobal(appIdx basics.AppIndex, key string) (value basics.TealValue, exists bool, err error) - SetGlobal(key string, value basics.TealValue) error - DelGlobal(key string) error - - GetDelta(txn *transactions.Transaction) (evalDelta transactions.EvalDelta, err error) - - Perform(txn *transactions.Transaction, spec transactions.SpecialAddresses) (transactions.ApplyData, error) -} - -// 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 -} + SetGlobal(appIdx basics.AppIndex, key string, value basics.TealValue) error + DelGlobal(appIdx basics.AppIndex, key string) error -// 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() + Perform(gi int, ep *EvalParams) error + Counter() uint64 } -// 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. type EvalParams struct { - // the transaction being evaluated - Txn *transactions.SignedTxn - Proto *config.ConsensusParams - Trace io.Writer - - TxnGroup []transactions.SignedTxn + Trace *strings.Builder - // GroupIndex should point to Txn within TxnGroup - GroupIndex uint64 + TxnGroup []transactions.SignedTxnWithAD - PastSideEffects []EvalSideEffects + pastScratch []*scratchSpace - Logger logging.Logger + logger logging.Logger Ledger LedgerForLogic @@ -264,19 +253,121 @@ 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 is spent by application - // actions. Shared value across a group's txns, so that it - // can be updated. nil is interpretted 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 - // determines eval mode: runModeSignature or runModeApplication - runModeFlags runMode - - // Total pool of app call budget in a group transaction + // 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 inner pooling enabled) + pooledAllowedInners *int + + // 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 +} + +func copyWithClearAD(txgroup []transactions.SignedTxnWithAD) []transactions.SignedTxnWithAD { + copy := make([]transactions.SignedTxnWithAD, len(txgroup)) + for i := range txgroup { + copy[i].SignedTxn = txgroup[i].SignedTxn + // leave copy[i].ApplyData clear + } + return copy +} + +// 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++ + } + } + + minTealVersion := ComputeMinTealVersion(txgroup, false) + + var pooledApplicationBudget *uint64 + var pooledAllowedInners *int + + credit, _ := transactions.FeeCredit(txgroup, proto.MinTxnFee) + + if proto.EnableAppCostPooling { + pooledApplicationBudget = new(uint64) + *pooledApplicationBudget = uint64(apps * proto.MaxAppProgramCost) + } + + if proto.EnableInnerTransactionPooling { + pooledAllowedInners = new(int) + *pooledAllowedInners = proto.MaxTxGroupSize * proto.MaxInnerTransactions + } + + return &EvalParams{ + TxnGroup: copyWithClearAD(txgroup), + Proto: proto, + Specials: specials, + pastScratch: make([]*scratchSpace, len(txgroup)), + MinTealVersion: &minTealVersion, + FeeCredit: &credit, + PooledApplicationBudget: pooledApplicationBudget, + pooledAllowedInners: pooledAllowedInners, + created: &resources{}, + appAddrCache: make(map[basics.AppIndex]basics.Address), + } +} + +// NewInnerEvalParams creates an EvalParams to be used while evaluating an inner group txgroup +func NewInnerEvalParams(txg []transactions.SignedTxn, caller *EvalContext) *EvalParams { + txgroup := transactions.WrapSignedTxnsWithAD(txg) + + minTealVersion := ComputeMinTealVersion(txgroup, true) + // Can't happen now, since innerAppsEnabledVersion > than any minimum + // imposed otherwise. But is correct to check. + if minTealVersion < *caller.MinTealVersion { + minTealVersion = *caller.MinTealVersion + } + credit, _ := transactions.FeeCredit(txgroup, caller.Proto.MinTxnFee) + *caller.FeeCredit = basics.AddSaturate(*caller.FeeCredit, credit) + + if caller.Proto.EnableAppCostPooling { + for _, tx := range txgroup { + if tx.Txn.Type == protocol.ApplicationCallTx { + *caller.PooledApplicationBudget += uint64(caller.Proto.MaxAppProgramCost) + } + } + } + + ep := &EvalParams{ + Proto: caller.Proto, + TxnGroup: copyWithClearAD(txgroup), + pastScratch: make([]*scratchSpace, len(txgroup)), + MinTealVersion: &minTealVersion, + FeeCredit: caller.FeeCredit, + Specials: caller.Specials, + PooledApplicationBudget: caller.PooledApplicationBudget, + pooledAllowedInners: caller.pooledAllowedInners, + Ledger: caller.Ledger, + created: caller.created, + appAddrCache: caller.appAddrCache, + caller: caller, + } + return ep } type opEvalFunc func(cx *EvalContext) @@ -312,35 +403,55 @@ func (r runMode) String() string { return "Unknown" } -func (ep EvalParams) budget() int { - if ep.runModeFlags == runModeSignature { - return int(ep.Proto.LogicSigMaxCost) - } - if ep.Proto.EnableAppCostPooling && ep.PooledApplicationBudget != nil { - return int(*ep.PooledApplicationBudget) +func (ep EvalParams) log() logging.Logger { + if ep.logger != nil { + return ep.logger } - return ep.Proto.MaxAppProgramCost + return logging.Base() } -func (ep EvalParams) log() logging.Logger { - if ep.Logger != nil { - return ep.Logger +// 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) } - return logging.Base() } -type scratchSpace = [256]stackValue +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 has been done, like log message and inner -// transactions. +// 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 +// has done, like log messages and inner transactions. type EvalContext struct { - EvalParams + *EvalParams + + // determines eval mode: runModeSignature or runModeApplication + runModeFlags runMode + + // the index of the transaction being evaluated + GroupIndex int + // the transaction being evaluated (initialized from GroupIndex + ep.TxnGroup) + Txn *transactions.SignedTxnWithAD + + // Txn.EvalDelta maintains a summary of changes as we go. We used to + // compute this from the ledger after a full eval. But now apps can call + // apps. When they do, all of the changes accumulate into the parent's + // ledger, but Txn.EvalDelta should only have the changes from *this* + // call. (The changes caused by children are deeper inside - in the + // EvalDeltas of the InnerTxns inside this EvalDelta) Nice bonus - by + // keeping the running changes, the debugger can be changed to display them + // as the app runs. stack []stackValue callstack []int + appID basics.AppIndex program []byte pc int nextpc int @@ -351,12 +462,8 @@ type EvalContext struct { scratch scratchSpace subtxns []transactions.SignedTxn // place to build for itxn_submit - // Previous transactions Performed() and their effects - InnerTxns []transactions.SignedTxnWithAD - - cost int // cost incurred so far - Logs []string - logSize int // total log size so far + cost int // cost incurred so far + logSize int // total log size so far // Set of PC values that branches we've seen so far might // go. So, if checkStep() skips one, that branch is trying to @@ -369,8 +476,6 @@ type EvalContext struct { instructionStarts map[int]bool programHashCached crypto.Digest - txidCache map[uint64]transactions.Txid - appAddrCache map[basics.AppIndex]basics.Address // Stores state & disassembly for the optional debugger debugState DebugState @@ -408,6 +513,15 @@ func (st StackType) String() string { return "internal error, unknown type" } +// Typed tells whether the StackType is a specific concrete type. +func (st StackType) Typed() bool { + switch st { + case StackUint64, StackBytes: + return true + } + return false +} + func (sts StackTypes) plus(other StackTypes) StackTypes { return append(sts, other...) } @@ -425,41 +539,51 @@ func (pe PanicError) Error() string { var errLogicSigNotSupported = errors.New("LogicSig not supported") var errTooManyArgs = errors.New("LogicSig has too many arguments") -// EvalStatefulCx executes stateful TEAL program -func EvalStatefulCx(program []byte, params EvalParams) (bool, *EvalContext, error) { - var cx EvalContext - cx.EvalParams = params - cx.runModeFlags = runModeApplication +// EvalContract executes stateful TEAL program as the gi'th transaction in params +func EvalContract(program []byte, gi int, aid basics.AppIndex, params *EvalParams) (bool, *EvalContext, error) { + if params.Ledger == nil { + return false, nil, errors.New("no ledger in contract eval") + } + cx := EvalContext{ + EvalParams: params, + runModeFlags: runModeApplication, + GroupIndex: gi, + Txn: ¶ms.TxnGroup[gi], + appID: aid, + } pass, err := eval(program, &cx) - // The following two updates show a need for something like a - // GroupEvalContext, as we are currently tucking things into the - // EvalParams so that they are available to later calls. - - // update pooled budget - if cx.Proto.EnableAppCostPooling && cx.PooledApplicationBudget != nil { - // if eval passes, then budget is always greater than cost, so should not have underflow + // update pooled budget (shouldn't overflow, but being careful anyway) + if cx.PooledApplicationBudget != nil { *cx.PooledApplicationBudget = basics.SubSaturate(*cx.PooledApplicationBudget, uint64(cx.cost)) } + // update allowed inner transactions (shouldn't overflow, but it's an int, so safe anyway) + if cx.pooledAllowedInners != nil { + *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 } -// EvalStateful is a lighter weight interface that doesn't return the EvalContext -func EvalStateful(program []byte, params EvalParams) (bool, error) { - pass, _, err := EvalStatefulCx(program, params) +// EvalApp is a lighter weight interface that doesn't return the EvalContext +func EvalApp(program []byte, gi int, aid basics.AppIndex, params *EvalParams) (bool, error) { + pass, _, err := EvalContract(program, gi, aid, params) return pass, err } -// Eval checks to see if a transaction passes logic +// EvalSignature evaluates the logicsig of the ith transaction in params. // A program passes successfully if it finishes with one int element on the stack that is non-zero. -func Eval(program []byte, params EvalParams) (pass bool, err error) { - var cx EvalContext - cx.EvalParams = params - cx.runModeFlags = runModeSignature - return eval(program, &cx) +func EvalSignature(gi int, params *EvalParams) (pass bool, err error) { + cx := EvalContext{ + EvalParams: params, + runModeFlags: runModeSignature, + GroupIndex: gi, + Txn: ¶ms.TxnGroup[gi], + } + return eval(cx.Txn.Lsig.Logic, &cx) } // eval implementation @@ -471,10 +595,8 @@ func eval(program []byte, cx *EvalContext) (pass bool, err error) { stlen := runtime.Stack(buf, false) pass = false errstr := string(buf[:stlen]) - if cx.EvalParams.Trace != nil { - if sb, ok := cx.EvalParams.Trace.(*strings.Builder); ok { - errstr += sb.String() - } + if cx.Trace != nil { + errstr += cx.Trace.String() } err = PanicError{x, errstr} cx.EvalParams.log().Errorf("recovered panic in Eval: %w", err) @@ -495,44 +617,23 @@ func eval(program []byte, cx *EvalContext) (pass bool, err error) { err = errLogicSigNotSupported return } - if cx.EvalParams.Txn.Lsig.Args != nil && len(cx.EvalParams.Txn.Lsig.Args) > transactions.EvalMaxArgs { + if cx.Txn.Lsig.Args != nil && len(cx.Txn.Lsig.Args) > transactions.EvalMaxArgs { err = errTooManyArgs return } - if len(program) == 0 { - cx.err = errors.New("invalid program (empty)") - return false, cx.err - } - version, vlen := binary.Uvarint(program) - if vlen <= 0 { - cx.err = errors.New("invalid version") - return false, cx.err - } - if version > EvalMaxVersion { - cx.err = fmt.Errorf("program version %d greater than max supported version %d", version, EvalMaxVersion) - return false, cx.err - } - if version > cx.EvalParams.Proto.LogicSigVersion { - cx.err = fmt.Errorf("program version %d greater than protocol supported version %d", version, cx.EvalParams.Proto.LogicSigVersion) - return false, cx.err - } - - var minVersion uint64 - if cx.EvalParams.MinTealVersion == nil { - minVersion = ComputeMinTealVersion(cx.EvalParams.TxnGroup) - } else { - minVersion = *cx.EvalParams.MinTealVersion - } - if version < minVersion { - cx.err = fmt.Errorf("program version must be >= %d for this transaction group, but have version %d", minVersion, version) - return false, cx.err + version, vlen, err := versionCheck(program, cx.EvalParams) + if err != nil { + cx.err = err + return false, err } cx.version = version cx.pc = vlen cx.stack = make([]stackValue, 0, 10) cx.program = program + cx.Txn.EvalDelta.GlobalDelta = basics.StateDelta{} + cx.Txn.EvalDelta.LocalDeltas = make(map[uint64]basics.StateDelta) if cx.Debugger != nil { cx.debugState = makeDebugState(cx) @@ -573,34 +674,29 @@ func eval(program []byte, cx *EvalContext) (pass bool, err error) { return cx.stack[0].Uint != 0, nil } -// CheckStateful should be faster than EvalStateful. It can perform +// CheckContract should be faster than EvalContract. It can perform // static checks and reject programs that are invalid. Prior to v4, // these static checks include a cost estimate that must be low enough // (controlled by params.Proto). -func CheckStateful(program []byte, params EvalParams) error { - params.runModeFlags = runModeApplication - return check(program, params) +func CheckContract(program []byte, params *EvalParams) error { + return check(program, params, runModeApplication) } -// Check should be faster than Eval. It can perform static checks and -// reject programs that are invalid. Prior to v4, these static checks -// include a cost estimate that must be low enough (controlled by -// params.Proto). -func Check(program []byte, params EvalParams) error { - params.runModeFlags = runModeSignature - return check(program, params) +// CheckSignature should be faster than EvalSignature. It can perform static +// checks and reject programs that are invalid. Prior to v4, these static checks +// include a cost estimate that must be low enough (controlled by params.Proto). +func CheckSignature(gi int, params *EvalParams) error { + return check(params.TxnGroup[gi].Lsig.Logic, params, runModeSignature) } -func check(program []byte, params EvalParams) (err error) { +func check(program []byte, params *EvalParams, mode runMode) (err error) { defer func() { if x := recover(); x != nil { buf := make([]byte, 16*1024) stlen := runtime.Stack(buf, false) errstr := string(buf[:stlen]) if params.Trace != nil { - if sb, ok := params.Trace.(*strings.Builder); ok { - errstr += sb.String() - } + errstr += params.Trace.String() } err = PanicError{x, errstr} params.log().Errorf("recovered panic in Check: %s", err) @@ -609,36 +705,22 @@ func check(program []byte, params EvalParams) (err error) { if (params.Proto == nil) || (params.Proto.LogicSigVersion == 0) { return errLogicSigNotSupported } - version, vlen := binary.Uvarint(program) - if vlen <= 0 { - return errors.New("invalid version") - } - if version > EvalMaxVersion { - return fmt.Errorf("program version %d greater than max supported version %d", version, EvalMaxVersion) - } - if version > params.Proto.LogicSigVersion { - return fmt.Errorf("program version %d greater than protocol supported version %d", version, params.Proto.LogicSigVersion) - } - var minVersion uint64 - if params.MinTealVersion == nil { - minVersion = ComputeMinTealVersion(params.TxnGroup) - } else { - minVersion = *params.MinTealVersion - } - if version < minVersion { - return fmt.Errorf("program version must be >= %d for this transaction group, but have version %d", minVersion, version) + version, vlen, err := versionCheck(program, params) + if err != nil { + return err } var cx EvalContext cx.version = version cx.pc = vlen cx.EvalParams = params + cx.runModeFlags = mode cx.program = program cx.branchTargets = make(map[int]bool) cx.instructionStarts = make(map[int]bool) - maxCost := params.budget() + maxCost := cx.budget() if version >= backBranchEnabledVersion { maxCost = math.MaxInt32 } @@ -664,6 +746,31 @@ func check(program []byte, params EvalParams) (err error) { return nil } +func versionCheck(program []byte, params *EvalParams) (uint64, int, error) { + if len(program) == 0 { + return 0, 0, errors.New("invalid program (empty)") + } + version, vlen := binary.Uvarint(program) + if vlen <= 0 { + return 0, 0, errors.New("invalid version") + } + if version > EvalMaxVersion { + return 0, 0, fmt.Errorf("program version %d greater than max supported version %d", version, EvalMaxVersion) + } + if version > params.Proto.LogicSigVersion { + return 0, 0, fmt.Errorf("program version %d greater than protocol supported version %d", version, params.Proto.LogicSigVersion) + } + + if params.MinTealVersion == nil { + minVersion := ComputeMinTealVersion(params.TxnGroup, params.caller != nil) + params.MinTealVersion = &minVersion + } + if version < *params.MinTealVersion { + return 0, 0, fmt.Errorf("program version must be >= %d for this transaction group, but have version %d", *params.MinTealVersion, version) + } + return version, vlen, nil +} + func opCompat(expected, got StackType) bool { if expected == StackAny { return true @@ -685,9 +792,26 @@ func boolToUint(x bool) uint64 { return 0 } -// MaxStackDepth should move to consensus params +// MaxStackDepth should not change unless gated by a teal version change / consensus upgrade. const MaxStackDepth = 1000 +func (cx *EvalContext) budget() int { + if cx.runModeFlags == runModeSignature { + return int(cx.Proto.LogicSigMaxCost) + } + if cx.Proto.EnableAppCostPooling && cx.PooledApplicationBudget != nil { + return int(*cx.PooledApplicationBudget) + } + return cx.Proto.MaxAppProgramCost +} + +func (cx *EvalContext) allowedInners() int { + if cx.Proto.EnableInnerTransactionPooling && cx.pooledAllowedInners != nil { + return *cx.pooledAllowedInners + } + return cx.Proto.MaxInnerTransactions +} + func (cx *EvalContext) step() { opcode := cx.program[cx.pc] spec := &opsByOpcode[cx.version][opcode] @@ -1035,11 +1159,7 @@ func opLt(cx *EvalContext) { last := len(cx.stack) - 1 prev := last - 1 cond := cx.stack[prev].Uint < cx.stack[last].Uint - if cond { - cx.stack[prev].Uint = 1 - } else { - cx.stack[prev].Uint = 0 - } + cx.stack[prev].Uint = boolToUint(cond) cx.stack = cx.stack[:last] } @@ -1062,11 +1182,7 @@ func opAnd(cx *EvalContext) { last := len(cx.stack) - 1 prev := last - 1 cond := (cx.stack[prev].Uint != 0) && (cx.stack[last].Uint != 0) - if cond { - cx.stack[prev].Uint = 1 - } else { - cx.stack[prev].Uint = 0 - } + cx.stack[prev].Uint = boolToUint(cond) cx.stack = cx.stack[:last] } @@ -1074,11 +1190,7 @@ func opOr(cx *EvalContext) { last := len(cx.stack) - 1 prev := last - 1 cond := (cx.stack[prev].Uint != 0) || (cx.stack[last].Uint != 0) - if cond { - cx.stack[prev].Uint = 1 - } else { - cx.stack[prev].Uint = 0 - } + cx.stack[prev].Uint = boolToUint(cond) cx.stack = cx.stack[:last] } @@ -1097,11 +1209,7 @@ func opEq(cx *EvalContext) { } else { cond = cx.stack[prev].Uint == cx.stack[last].Uint } - if cond { - cx.stack[prev].Uint = 1 - } else { - cx.stack[prev].Uint = 0 - } + cx.stack[prev].Uint = boolToUint(cond) cx.stack[prev].Bytes = nil cx.stack = cx.stack[:last] } @@ -1114,11 +1222,7 @@ func opNeq(cx *EvalContext) { func opNot(cx *EvalContext) { last := len(cx.stack) - 1 cond := cx.stack[last].Uint == 0 - if cond { - cx.stack[last].Uint = 1 - } else { - cx.stack[last].Uint = 0 - } + cx.stack[last].Uint = boolToUint(cond) } func opLen(cx *EvalContext) { @@ -1385,6 +1489,19 @@ func opBytesMul(cx *EvalContext) { opBytesBinOp(cx, result, result.Mul) } +func opBytesSqrt(cx *EvalContext) { + last := len(cx.stack) - 1 + + if len(cx.stack[last].Bytes) > MaxByteMathSize { + cx.err = errors.New("math attempted on large byte-array") + return + } + + val := new(big.Int).SetBytes(cx.stack[last].Bytes) + val.Sqrt(val) + cx.stack[last].Bytes = val.Bytes() +} + func opBytesLt(cx *EvalContext) { last := len(cx.stack) - 1 prev := last - 1 @@ -1397,11 +1514,7 @@ func opBytesLt(cx *EvalContext) { rhs := new(big.Int).SetBytes(cx.stack[last].Bytes) lhs := new(big.Int).SetBytes(cx.stack[prev].Bytes) cx.stack[prev].Bytes = nil - if lhs.Cmp(rhs) < 0 { - cx.stack[prev].Uint = 1 - } else { - cx.stack[prev].Uint = 0 - } + cx.stack[prev].Uint = boolToUint(lhs.Cmp(rhs) < 0) cx.stack = cx.stack[:last] } @@ -1432,11 +1545,7 @@ func opBytesEq(cx *EvalContext) { rhs := new(big.Int).SetBytes(cx.stack[last].Bytes) lhs := new(big.Int).SetBytes(cx.stack[prev].Bytes) cx.stack[prev].Bytes = nil - if lhs.Cmp(rhs) == 0 { - cx.stack[prev].Uint = 1 - } else { - cx.stack[prev].Uint = 0 - } + cx.stack[prev].Uint = boolToUint(lhs.Cmp(rhs) == 0) cx.stack = cx.stack[:last] } @@ -1884,62 +1993,58 @@ func TxnFieldToTealValue(txn *transactions.Transaction, groupIndex int, field Tx if groupIndex < 0 { return basics.TealValue{}, fmt.Errorf("negative groupIndex %d", groupIndex) } - cx := EvalContext{EvalParams: EvalParams{GroupIndex: uint64(groupIndex)}} + cx := EvalContext{ + GroupIndex: groupIndex, + Txn: &transactions.SignedTxnWithAD{SignedTxn: transactions.SignedTxn{Txn: *txn}}, + } fs := txnFieldSpecByField[field] - sv, err := cx.txnFieldToStack(txn, fs, arrayFieldIdx, uint64(groupIndex)) + sv, err := cx.txnFieldToStack(cx.Txn, &fs, arrayFieldIdx, groupIndex, false) return sv.toTealValue(), err } -func (cx *EvalContext) getTxID(txn *transactions.Transaction, groupIndex uint64) transactions.Txid { +func (cx *EvalContext) getTxID(txn *transactions.Transaction, groupIndex int) transactions.Txid { + if cx.EvalParams == nil { // Special case, called through TxnFieldToTealValue. No EvalParams, no caching. + return txn.ID() + } + // Initialize txidCache if necessary - if cx.txidCache == nil { - cx.txidCache = make(map[uint64]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 { - txid = txn.ID() - cx.txidCache[groupIndex] = txid + if cx.caller != nil { + innerOffset := len(cx.caller.Txn.EvalDelta.InnerTxns) + txid = txn.InnerID(cx.caller.Txn.ID(), innerOffset+groupIndex) + } else { + txid = txn.ID() + } + cx.EvalParams.txidCache[groupIndex] = txid } return txid } -func (cx *EvalContext) itxnFieldToStack(itxn *transactions.SignedTxnWithAD, fs txnFieldSpec, arrayFieldIdx uint64) (sv stackValue, err error) { +func (cx *EvalContext) txnFieldToStack(stxn *transactions.SignedTxnWithAD, fs *txnFieldSpec, arrayFieldIdx uint64, groupIndex int, inner bool) (sv stackValue, err error) { if fs.effects { - switch fs.field { - case Logs: - if arrayFieldIdx >= uint64(len(itxn.EvalDelta.Logs)) { - err = fmt.Errorf("invalid Logs index %d", arrayFieldIdx) - return - } - sv.Bytes = nilToEmpty([]byte(itxn.EvalDelta.Logs[arrayFieldIdx])) - case NumLogs: - sv.Uint = uint64(len(itxn.EvalDelta.Logs)) - case CreatedAssetID: - sv.Uint = uint64(itxn.ApplyData.ConfigAsset) - case CreatedApplicationID: - sv.Uint = uint64(itxn.ApplyData.ApplicationID) - default: - err = fmt.Errorf("invalid txn field %d", fs.field) + if cx.runModeFlags == runModeSignature { + return sv, fmt.Errorf("txn[%s] not allowed in current mode", fs.field) + } + if cx.version < txnEffectsVersion && !inner { + return sv, errors.New("Unable to obtain effects from top-level transactions") } - return - } - - if fs.field == GroupIndex || fs.field == TxID { - err = fmt.Errorf("illegal field for inner transaction %s", fs.field) - } else { - sv, err = cx.txnFieldToStack(&itxn.Txn, fs, arrayFieldIdx, 0) } - return -} - -func (cx *EvalContext) txnFieldToStack(txn *transactions.Transaction, fs txnFieldSpec, arrayFieldIdx uint64, groupIndex uint64) (sv stackValue, err error) { - if fs.effects { - return sv, errors.New("Unable to obtain effects from top-level transactions") + if inner { + // Before we had inner apps, we did not allow these, since we had no inner groups. + if cx.version < innerAppsEnabledVersion && (fs.field == GroupIndex || fs.field == TxID) { + err = fmt.Errorf("illegal field for inner transaction %s", fs.field) + return + } } err = nil + txn := &stxn.SignedTxn.Txn switch fs.field { case Sender: sv.Bytes = txn.Sender[:] @@ -1984,7 +2089,7 @@ func (cx *EvalContext) txnFieldToStack(txn *transactions.Transaction, fs txnFiel case AssetCloseTo: sv.Bytes = txn.AssetCloseTo[:] case GroupIndex: - sv.Uint = groupIndex + sv.Uint = uint64(groupIndex) case TxID: txid := cx.getTxID(txn, groupIndex) sv.Bytes = txid[:] @@ -2089,31 +2194,53 @@ func (cx *EvalContext) txnFieldToStack(txn *transactions.Transaction, fs txnFiel sv.Uint = boolToUint(txn.AssetFrozen) case ExtraProgramPages: sv.Uint = uint64(txn.ExtraProgramPages) + + case Logs: + if arrayFieldIdx >= uint64(len(stxn.EvalDelta.Logs)) { + err = fmt.Errorf("invalid Logs index %d", arrayFieldIdx) + return + } + sv.Bytes = nilToEmpty([]byte(stxn.EvalDelta.Logs[arrayFieldIdx])) + case NumLogs: + sv.Uint = uint64(len(stxn.EvalDelta.Logs)) + case CreatedAssetID: + sv.Uint = uint64(stxn.ApplyData.ConfigAsset) + case CreatedApplicationID: + sv.Uint = uint64(stxn.ApplyData.ApplicationID) + default: - err = fmt.Errorf("invalid txn field %d", fs.field) + err = fmt.Errorf("invalid txn field %s", fs.field) return } - txnFieldType := TxnFieldTypes[fs.field] - if !typecheck(txnFieldType, sv.argType()) { - err = fmt.Errorf("%s expected field type is %s but got %s", fs.field.String(), txnFieldType.String(), sv.argType().String()) + if !typecheck(fs.ftype, sv.argType()) { + err = fmt.Errorf("%s expected field type is %s but got %s", fs.field, fs.ftype, sv.argType()) } return } -func opTxn(cx *EvalContext) { - field := TxnField(cx.program[cx.pc+1]) +func (cx *EvalContext) fetchField(field TxnField, expectArray bool) (*txnFieldSpec, error) { fs, ok := txnFieldSpecByField[field] if !ok || fs.version > cx.version { - cx.err = fmt.Errorf("invalid txn field %d", field) - return + return nil, fmt.Errorf("invalid txn field %d", field) } - _, ok = txnaFieldSpecByField[field] - if ok { - cx.err = fmt.Errorf("invalid txn field %d", field) + if expectArray != fs.array { + if expectArray { + return nil, fmt.Errorf("unsupported array field %d", field) + } + return nil, fmt.Errorf("invalid txn field %d", field) + } + return &fs, nil +} + +func opTxn(cx *EvalContext) { + fs, err := cx.fetchField(TxnField(cx.program[cx.pc+1]), false) + if err != nil { + cx.err = err return } - sv, err := cx.txnFieldToStack(&cx.Txn.Txn, fs, 0, cx.GroupIndex) + + sv, err := cx.txnFieldToStack(cx.Txn, fs, 0, cx.GroupIndex, false) if err != nil { cx.err = err return @@ -2122,19 +2249,14 @@ func opTxn(cx *EvalContext) { } func opTxna(cx *EvalContext) { - field := TxnField(cx.program[cx.pc+1]) - fs, ok := txnFieldSpecByField[field] - if !ok || fs.version > cx.version { - cx.err = fmt.Errorf("invalid txn field %d", field) - return - } - _, ok = txnaFieldSpecByField[field] - if !ok { - cx.err = fmt.Errorf("txna unsupported field %d", field) + fs, err := cx.fetchField(TxnField(cx.program[cx.pc+1]), true) + if err != nil { + cx.err = err return } + arrayFieldIdx := uint64(cx.program[cx.pc+2]) - sv, err := cx.txnFieldToStack(&cx.Txn.Txn, fs, arrayFieldIdx, cx.GroupIndex) + sv, err := cx.txnFieldToStack(cx.Txn, fs, arrayFieldIdx, cx.GroupIndex, false) if err != nil { cx.err = err return @@ -2143,21 +2265,15 @@ func opTxna(cx *EvalContext) { } func opTxnas(cx *EvalContext) { - last := len(cx.stack) - 1 - - field := TxnField(cx.program[cx.pc+1]) - fs, ok := txnFieldSpecByField[field] - if !ok || fs.version > cx.version { - cx.err = fmt.Errorf("invalid txn field %d", field) - return - } - _, ok = txnaFieldSpecByField[field] - if !ok { - cx.err = fmt.Errorf("txnas unsupported field %d", field) + fs, err := cx.fetchField(TxnField(cx.program[cx.pc+1]), true) + if err != nil { + cx.err = err return } + + last := len(cx.stack) - 1 arrayFieldIdx := cx.stack[last].Uint - sv, err := cx.txnFieldToStack(&cx.Txn.Txn, fs, arrayFieldIdx, cx.GroupIndex) + sv, err := cx.txnFieldToStack(cx.Txn, fs, arrayFieldIdx, cx.GroupIndex, false) if err != nil { cx.err = err return @@ -2166,58 +2282,40 @@ func opTxnas(cx *EvalContext) { } func opGtxn(cx *EvalContext) { - gtxid := cx.program[cx.pc+1] - if int(gtxid) >= len(cx.TxnGroup) { - cx.err = fmt.Errorf("gtxn lookup TxnGroup[%d] but it only has %d", gtxid, len(cx.TxnGroup)) + gi := cx.program[cx.pc+1] + if int(gi) >= len(cx.TxnGroup) { + cx.err = fmt.Errorf("gtxn lookup TxnGroup[%d] but it only has %d", gi, len(cx.TxnGroup)) return } - tx := &cx.TxnGroup[gtxid].Txn - field := TxnField(cx.program[cx.pc+2]) - fs, ok := txnFieldSpecByField[field] - if !ok || fs.version > cx.version { - cx.err = fmt.Errorf("invalid txn field %d", field) + fs, err := cx.fetchField(TxnField(cx.program[cx.pc+2]), false) + if err != nil { + cx.err = err return } - _, ok = txnaFieldSpecByField[field] - if ok { - cx.err = fmt.Errorf("invalid txn field %d", field) + + tx := &cx.TxnGroup[gi] + sv, err := cx.txnFieldToStack(tx, fs, 0, int(gi), false) + if err != nil { + cx.err = err return } - var sv stackValue - var err error - if field == GroupIndex { - // GroupIndex; asking this when we just specified it is _dumb_, but oh well - sv.Uint = uint64(gtxid) - } else { - sv, err = cx.txnFieldToStack(tx, fs, 0, uint64(gtxid)) - if err != nil { - cx.err = err - return - } - } cx.stack = append(cx.stack, sv) } func opGtxna(cx *EvalContext) { - gtxid := int(uint(cx.program[cx.pc+1])) - if gtxid >= len(cx.TxnGroup) { - cx.err = fmt.Errorf("gtxna lookup TxnGroup[%d] but it only has %d", gtxid, len(cx.TxnGroup)) + gi := cx.program[cx.pc+1] + if int(gi) >= len(cx.TxnGroup) { + cx.err = fmt.Errorf("gtxna lookup TxnGroup[%d] but it only has %d", gi, len(cx.TxnGroup)) return } - tx := &cx.TxnGroup[gtxid].Txn - field := TxnField(cx.program[cx.pc+2]) - fs, ok := txnFieldSpecByField[field] - if !ok || fs.version > cx.version { - cx.err = fmt.Errorf("invalid txn field %d", field) - return - } - _, ok = txnaFieldSpecByField[field] - if !ok { - cx.err = fmt.Errorf("gtxna unsupported field %d", field) + fs, err := cx.fetchField(TxnField(cx.program[cx.pc+2]), true) + if err != nil { + cx.err = err return } arrayFieldIdx := uint64(cx.program[cx.pc+3]) - sv, err := cx.txnFieldToStack(tx, fs, arrayFieldIdx, uint64(gtxid)) + tx := &cx.TxnGroup[gi] + sv, err := cx.txnFieldToStack(tx, fs, arrayFieldIdx, int(gi), false) if err != nil { cx.err = err return @@ -2228,25 +2326,19 @@ func opGtxna(cx *EvalContext) { func opGtxnas(cx *EvalContext) { last := len(cx.stack) - 1 - gtxid := cx.program[cx.pc+1] - if int(gtxid) >= len(cx.TxnGroup) { - cx.err = fmt.Errorf("gtxnas lookup TxnGroup[%d] but it only has %d", gtxid, len(cx.TxnGroup)) + gi := int(cx.program[cx.pc+1]) + if int(gi) >= len(cx.TxnGroup) { + cx.err = fmt.Errorf("gtxnas lookup TxnGroup[%d] but it only has %d", gi, len(cx.TxnGroup)) return } - tx := &cx.TxnGroup[gtxid].Txn - field := TxnField(cx.program[cx.pc+2]) - fs, ok := txnFieldSpecByField[field] - if !ok || fs.version > cx.version { - cx.err = fmt.Errorf("invalid txn field %d", field) - return - } - _, ok = txnaFieldSpecByField[field] - if !ok { - cx.err = fmt.Errorf("gtxnas unsupported field %d", field) + fs, err := cx.fetchField(TxnField(cx.program[cx.pc+2]), true) + if err != nil { + cx.err = err return } arrayFieldIdx := cx.stack[last].Uint - sv, err := cx.txnFieldToStack(tx, fs, arrayFieldIdx, uint64(gtxid)) + tx := &cx.TxnGroup[gi] + sv, err := cx.txnFieldToStack(tx, fs, arrayFieldIdx, gi, false) if err != nil { cx.err = err return @@ -2256,59 +2348,40 @@ func opGtxnas(cx *EvalContext) { func opGtxns(cx *EvalContext) { last := len(cx.stack) - 1 - gtxid := cx.stack[last].Uint - if gtxid >= uint64(len(cx.TxnGroup)) { - cx.err = fmt.Errorf("gtxns lookup TxnGroup[%d] but it only has %d", gtxid, len(cx.TxnGroup)) + gi := cx.stack[last].Uint + if gi >= uint64(len(cx.TxnGroup)) { + cx.err = fmt.Errorf("gtxns lookup TxnGroup[%d] but it only has %d", gi, len(cx.TxnGroup)) return } - tx := &cx.TxnGroup[gtxid].Txn - field := TxnField(cx.program[cx.pc+1]) - fs, ok := txnFieldSpecByField[field] - if !ok || fs.version > cx.version { - cx.err = fmt.Errorf("invalid txn field %d", field) + fs, err := cx.fetchField(TxnField(cx.program[cx.pc+1]), false) + if err != nil { + cx.err = err return } - _, ok = txnaFieldSpecByField[field] - if ok { - cx.err = fmt.Errorf("invalid txn field %d", field) + tx := &cx.TxnGroup[gi] + sv, err := cx.txnFieldToStack(tx, fs, 0, int(gi), false) + if err != nil { + cx.err = err return } - var sv stackValue - var err error - if field == GroupIndex { - // GroupIndex; asking this when we just specified it is _dumb_, but oh well - sv.Uint = gtxid - } else { - sv, err = cx.txnFieldToStack(tx, fs, 0, gtxid) - if err != nil { - cx.err = err - return - } - } cx.stack[last] = sv } func opGtxnsa(cx *EvalContext) { last := len(cx.stack) - 1 - gtxid := cx.stack[last].Uint - if gtxid >= uint64(len(cx.TxnGroup)) { - cx.err = fmt.Errorf("gtxnsa lookup TxnGroup[%d] but it only has %d", gtxid, len(cx.TxnGroup)) - return - } - tx := &cx.TxnGroup[gtxid].Txn - field := TxnField(cx.program[cx.pc+1]) - fs, ok := txnFieldSpecByField[field] - if !ok || fs.version > cx.version { - cx.err = fmt.Errorf("invalid txn field %d", field) + gi := cx.stack[last].Uint + if gi >= uint64(len(cx.TxnGroup)) { + cx.err = fmt.Errorf("gtxnsa lookup TxnGroup[%d] but it only has %d", gi, len(cx.TxnGroup)) return } - _, ok = txnaFieldSpecByField[field] - if !ok { - cx.err = fmt.Errorf("gtxnsa unsupported field %d", field) + fs, err := cx.fetchField(TxnField(cx.program[cx.pc+1]), true) + if err != nil { + cx.err = err return } arrayFieldIdx := uint64(cx.program[cx.pc+2]) - sv, err := cx.txnFieldToStack(tx, fs, arrayFieldIdx, gtxid) + tx := &cx.TxnGroup[gi] + sv, err := cx.txnFieldToStack(tx, fs, arrayFieldIdx, int(gi), false) if err != nil { cx.err = err return @@ -2320,25 +2393,19 @@ func opGtxnsas(cx *EvalContext) { last := len(cx.stack) - 1 prev := last - 1 - gtxid := cx.stack[prev].Uint - if gtxid >= uint64(len(cx.TxnGroup)) { - cx.err = fmt.Errorf("gtxnsas lookup TxnGroup[%d] but it only has %d", gtxid, len(cx.TxnGroup)) + gi := cx.stack[prev].Uint + if gi >= uint64(len(cx.TxnGroup)) { + cx.err = fmt.Errorf("gtxnsas lookup TxnGroup[%d] but it only has %d", gi, len(cx.TxnGroup)) return } - tx := &cx.TxnGroup[gtxid].Txn - field := TxnField(cx.program[cx.pc+1]) - fs, ok := txnFieldSpecByField[field] - if !ok || fs.version > cx.version { - cx.err = fmt.Errorf("invalid txn field %d", field) - return - } - _, ok = txnaFieldSpecByField[field] - if !ok { - cx.err = fmt.Errorf("gtxnsas unsupported field %d", field) + fs, err := cx.fetchField(TxnField(cx.program[cx.pc+1]), true) + if err != nil { + cx.err = err return } arrayFieldIdx := cx.stack[last].Uint - sv, err := cx.txnFieldToStack(tx, fs, arrayFieldIdx, gtxid) + tx := &cx.TxnGroup[gi] + sv, err := cx.txnFieldToStack(tx, fs, arrayFieldIdx, int(gi), false) if err != nil { cx.err = err return @@ -2348,25 +2415,41 @@ func opGtxnsas(cx *EvalContext) { } func opItxn(cx *EvalContext) { - field := TxnField(cx.program[cx.pc+1]) - fs, ok := txnFieldSpecByField[field] - if !ok || fs.version > cx.version { - cx.err = fmt.Errorf("invalid itxn field %d", field) + fs, err := cx.fetchField(TxnField(cx.program[cx.pc+1]), false) + if err != nil { + cx.err = err return } - _, ok = txnaFieldSpecByField[field] - if ok { - cx.err = fmt.Errorf("invalid itxn field %d", field) + + if len(cx.Txn.EvalDelta.InnerTxns) == 0 { + cx.err = fmt.Errorf("no inner transaction available %d", fs.field) return } - if len(cx.InnerTxns) == 0 { - cx.err = fmt.Errorf("no inner transaction available %d", field) + itxn := &cx.Txn.EvalDelta.InnerTxns[len(cx.Txn.EvalDelta.InnerTxns)-1] + sv, err := cx.txnFieldToStack(itxn, fs, 0, 0, true) + if err != nil { + cx.err = err return } + cx.stack = append(cx.stack, sv) +} - itxn := &cx.InnerTxns[len(cx.InnerTxns)-1] - sv, err := cx.itxnFieldToStack(itxn, fs, 0) +func opItxna(cx *EvalContext) { + fs, err := cx.fetchField(TxnField(cx.program[cx.pc+1]), true) + if err != nil { + cx.err = err + return + } + arrayFieldIdx := uint64(cx.program[cx.pc+2]) + + if len(cx.Txn.EvalDelta.InnerTxns) == 0 { + cx.err = fmt.Errorf("no inner transaction available %d", fs.field) + return + } + + itxn := &cx.Txn.EvalDelta.InnerTxns[len(cx.Txn.EvalDelta.InnerTxns)-1] + sv, err := cx.txnFieldToStack(itxn, fs, arrayFieldIdx, 0, true) if err != nil { cx.err = err return @@ -2374,27 +2457,66 @@ func opItxn(cx *EvalContext) { cx.stack = append(cx.stack, sv) } -func opItxna(cx *EvalContext) { - field := TxnField(cx.program[cx.pc+1]) - fs, ok := txnFieldSpecByField[field] - if !ok || fs.version > cx.version { - cx.err = fmt.Errorf("invalid itxn field %d", field) +func (cx *EvalContext) getLastInnerGroup() []transactions.SignedTxnWithAD { + inners := cx.Txn.EvalDelta.InnerTxns + // If there are no inners yet, return empty slice, which will result in error + if len(inners) == 0 { + return inners + } + gid := inners[len(inners)-1].Txn.Group + // If last inner was a singleton, return it as a slice. + if gid.IsZero() { + return inners[len(inners)-1:] + } + // Look back for the first non-matching inner (by group) to find beginning + for i := len(inners) - 2; i >= 0; i-- { + if inners[i].Txn.Group != gid { + return inners[i+1:] + } + } + // All have the same (non-zero) group. Return all + return inners +} + +func opGitxn(cx *EvalContext) { + lastInnerGroup := cx.getLastInnerGroup() + gi := cx.program[cx.pc+1] + if int(gi) >= len(lastInnerGroup) { + cx.err = fmt.Errorf("gitxn %d ... but last group has %d", gi, len(lastInnerGroup)) return } - _, ok = txnaFieldSpecByField[field] - if !ok { - cx.err = fmt.Errorf("itxna unsupported field %d", field) + itxn := &lastInnerGroup[gi] + + fs, err := cx.fetchField(TxnField(cx.program[cx.pc+2]), false) + if err != nil { + cx.err = err return } - arrayFieldIdx := uint64(cx.program[cx.pc+2]) - if len(cx.InnerTxns) == 0 { - cx.err = fmt.Errorf("no inner transaction available %d", field) + sv, err := cx.txnFieldToStack(itxn, fs, 0, int(gi), true) + if err != nil { + cx.err = err + return + } + cx.stack = append(cx.stack, sv) +} + +func opGitxna(cx *EvalContext) { + lastInnerGroup := cx.getLastInnerGroup() + gi := int(uint(cx.program[cx.pc+1])) + if gi >= len(lastInnerGroup) { + cx.err = fmt.Errorf("gitxna %d ... but last group has %d", gi, len(lastInnerGroup)) return } + itxn := &lastInnerGroup[gi] - itxn := &cx.InnerTxns[len(cx.InnerTxns)-1] - sv, err := cx.itxnFieldToStack(itxn, fs, arrayFieldIdx) + fs, err := cx.fetchField(TxnField(cx.program[cx.pc+2]), true) + if err != nil { + cx.err = err + return + } + arrayFieldIdx := uint64(cx.program[cx.pc+3]) + sv, err := cx.txnFieldToStack(itxn, fs, arrayFieldIdx, gi, true) if err != nil { cx.err = err return @@ -2402,8 +2524,8 @@ func opItxna(cx *EvalContext) { cx.stack = append(cx.stack, sv) } -func opGaidImpl(cx *EvalContext, groupIdx uint64, opName string) (sv stackValue, err error) { - if groupIdx >= uint64(len(cx.TxnGroup)) { +func opGaidImpl(cx *EvalContext, groupIdx int, opName string) (sv stackValue, err error) { + 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 groupIdx > cx.GroupIndex { @@ -2430,8 +2552,8 @@ func opGaidImpl(cx *EvalContext, groupIdx uint64, opName string) (sv stackValue, } func opGaid(cx *EvalContext) { - groupIdx := cx.program[cx.pc+1] - sv, err := opGaidImpl(cx, uint64(groupIdx), "gaid") + groupIdx := int(cx.program[cx.pc+1]) + sv, err := opGaidImpl(cx, groupIdx, "gaid") if err != nil { cx.err = err return @@ -2442,8 +2564,12 @@ func opGaid(cx *EvalContext) { func opGaids(cx *EvalContext) { last := len(cx.stack) - 1 - groupIdx := cx.stack[last].Uint - sv, err := opGaidImpl(cx, groupIdx, "gaids") + gi := cx.stack[last].Uint + if gi >= uint64(len(cx.TxnGroup)) { + cx.err = fmt.Errorf("gaids lookup TxnGroup[%d] but it only has %d", gi, len(cx.TxnGroup)) + return + } + sv, err := opGaidImpl(cx, int(gi), "gaids") if err != nil { cx.err = err return @@ -2473,61 +2599,41 @@ func (cx *EvalContext) getLatestTimestamp() (timestamp uint64, err error) { return uint64(ts), nil } -func (cx *EvalContext) getApplicationID() (uint64, error) { - if cx.Ledger == nil { - return 0, fmt.Errorf("ledger not available") - } - return uint64(cx.Ledger.ApplicationID()), nil -} - -func (cx *EvalContext) getApplicationAddress() (basics.Address, error) { - if cx.Ledger == nil { - return basics.Address{}, fmt.Errorf("ledger not available") - } - - // Initialize appAddrCache if necessary - if cx.appAddrCache == nil { - cx.appAddrCache = make(map[basics.AppIndex]basics.Address) - } - - appID := cx.Ledger.ApplicationID() - // Hashes are expensive, so we cache computed app addrs - appAddr, ok := cx.appAddrCache[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 = appID.Address() - cx.appAddrCache[appID] = appAddr + appAddr = app.Address() + cx.appAddrCache[app] = appAddr } - return appAddr, nil + return appAddr } -func (cx *EvalContext) getCreatableID(groupIndex uint64) (cid uint64, err error) { - if cx.Ledger == nil { - err = fmt.Errorf("ledger not available") - return +func (cx *EvalContext) getCreatableID(groupIndex int) (cid uint64, err error) { + if aid := cx.TxnGroup[groupIndex].ApplyData.ConfigAsset; aid != 0 { + return uint64(aid), nil } - gi := int(groupIndex) - if gi < 0 { - return 0, fmt.Errorf("groupIndex %d too high", groupIndex) + if aid := cx.TxnGroup[groupIndex].ApplyData.ApplicationID; aid != 0 { + return uint64(aid), nil } - return uint64(cx.Ledger.GetCreatableID(gi)), nil + return 0, fmt.Errorf("Index %d did not create anything", groupIndex) } func (cx *EvalContext) getCreatorAddress() ([]byte, error) { if cx.Ledger == nil { return nil, fmt.Errorf("ledger not available") } - _, creator, err := cx.Ledger.AppParams(cx.Ledger.ApplicationID()) + _, creator, err := cx.Ledger.AppParams(cx.appID) if err != nil { return nil, fmt.Errorf("No params for current app") } return creator[:], nil } -func (cx *EvalContext) getGroupID() []byte { - return cx.Txn.Txn.Group[:] -} - var zeroAddress basics.Address func (cx *EvalContext) globalFieldToValue(fs globalFieldSpec) (sv stackValue, err error) { @@ -2549,15 +2655,29 @@ func (cx *EvalContext) globalFieldToValue(fs globalFieldSpec) (sv stackValue, er case LatestTimestamp: sv.Uint, err = cx.getLatestTimestamp() case CurrentApplicationID: - sv.Uint, err = cx.getApplicationID() + sv.Uint = uint64(cx.appID) case CurrentApplicationAddress: - var addr basics.Address - addr, err = cx.getApplicationAddress() + addr := cx.getApplicationAddress(cx.appID) sv.Bytes = addr[:] case CreatorAddress: sv.Bytes, err = cx.getCreatorAddress() case GroupID: - sv.Bytes = cx.getGroupID() + sv.Bytes = cx.Txn.Txn.Group[:] + case OpcodeBudget: + sv.Uint = uint64(cx.budget() - cx.cost) + case CallerApplicationID: + if cx.caller != nil { + sv.Uint = uint64(cx.caller.appID) + } else { + sv.Uint = 0 + } + case CallerApplicationAddress: + if cx.caller != nil { + addr := cx.caller.getApplicationAddress(cx.caller.appID) + sv.Bytes = addr[:] + } else { + sv.Bytes = zeroAddress[:] + } default: err = fmt.Errorf("invalid global field %d", fs.field) } @@ -2573,11 +2693,11 @@ func opGlobal(cx *EvalContext) { globalField := GlobalField(cx.program[cx.pc+1]) fs, ok := globalFieldSpecByField[globalField] if !ok || fs.version > cx.version { - cx.err = fmt.Errorf("invalid global field %d", globalField) + cx.err = fmt.Errorf("invalid global field %s", globalField) return } if (cx.runModeFlags & fs.mode) == 0 { - cx.err = fmt.Errorf("global[%d] not allowed in current mode", globalField) + cx.err = fmt.Errorf("global[%s] not allowed in current mode", globalField) return } @@ -2631,11 +2751,7 @@ func opEd25519verify(cx *EvalContext) { copy(sig[:], cx.stack[prev].Bytes) msg := Msg{ProgramHash: cx.programHash(), Data: cx.stack[pprev].Bytes} - if sv.Verify(msg, sig) { - cx.stack[pprev].Uint = 1 - } else { - cx.stack[pprev].Uint = 0 - } + cx.stack[pprev].Uint = boolToUint(sv.Verify(msg, sig)) cx.stack[pprev].Bytes = nil cx.stack = cx.stack[:prev] } @@ -2690,11 +2806,7 @@ func opEcdsaVerify(cx *EvalContext) { result := secp256k1.VerifySignature(pubkey, msg, signature) - if result { - cx.stack[fifth].Uint = 1 - } else { - cx.stack[fifth].Uint = 0 - } + cx.stack[fifth].Uint = boolToUint(result) cx.stack[fifth].Bytes = nil cx.stack = cx.stack[:fourth] } @@ -2832,30 +2944,29 @@ func opStores(cx *EvalContext) { cx.stack = cx.stack[:prev] } -func opGloadImpl(cx *EvalContext, groupIdx uint64, scratchIdx byte, opName string) (scratchValue stackValue, err error) { - if groupIdx >= uint64(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 +func opGloadImpl(cx *EvalContext, groupIdx int, scratchIdx byte, opName string) (stackValue, error) { + var none stackValue + if groupIdx >= len(cx.TxnGroup) { + 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) { - groupIdx := uint64(cx.program[cx.pc+1]) + groupIdx := int(cx.program[cx.pc+1]) scratchIdx := cx.program[cx.pc+2] scratchValue, err := opGloadImpl(cx, groupIdx, scratchIdx, "gload") if err != nil { @@ -2868,9 +2979,13 @@ func opGload(cx *EvalContext) { func opGloads(cx *EvalContext) { last := len(cx.stack) - 1 - groupIdx := cx.stack[last].Uint + gi := cx.stack[last].Uint + if gi >= uint64(len(cx.TxnGroup)) { + cx.err = fmt.Errorf("gloads lookup TxnGroup[%d] but it only has %d", gi, len(cx.TxnGroup)) + return + } scratchIdx := cx.program[cx.pc+1] - scratchValue, err := opGloadImpl(cx, groupIdx, scratchIdx, "gloads") + scratchValue, err := opGloadImpl(cx, int(gi), scratchIdx, "gloads") if err != nil { cx.err = err return @@ -2879,6 +2994,30 @@ func opGloads(cx *EvalContext) { cx.stack[last] = scratchValue } +func opGloadss(cx *EvalContext) { + last := len(cx.stack) - 1 + prev := last - 1 + + gi := cx.stack[prev].Uint + if gi >= uint64(len(cx.TxnGroup)) { + cx.err = fmt.Errorf("gloads lookup TxnGroup[%d] but it only has %d", gi, len(cx.TxnGroup)) + return + } + scratchIdx := cx.stack[last].Uint + if scratchIdx >= 256 { + cx.err = fmt.Errorf("gloadss scratch index >= 256 (%d)", scratchIdx) + return + } + scratchValue, err := opGloadImpl(cx, int(gi), byte(scratchIdx), "gloadss") + if err != nil { + cx.err = err + return + } + + cx.stack[prev] = scratchValue + cx.stack = cx.stack[:last] +} + func opConcat(cx *EvalContext) { last := len(cx.stack) - 1 prev := last - 1 @@ -3122,14 +3261,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) @@ -3141,17 +3285,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 { @@ -3224,12 +3391,8 @@ func opAppOptedIn(cx *EvalContext) { return } + cx.stack[prev].Uint = boolToUint(optedIn) cx.stack[prev].Bytes = nil - if optedIn { - cx.stack[prev].Uint = 1 - } else { - cx.stack[prev].Uint = 0 - } cx.stack = cx.stack[:last] } @@ -3372,11 +3535,28 @@ func opAppLocalPut(cx *EvalContext) { return } - addr, accountIdx, err := cx.accountReference(cx.stack[pprev]) - if err == nil { - err = cx.Ledger.SetLocal(addr, key, sv.toTealValue(), accountIdx) + addr, accountIdx, err := cx.mutableAccountReference(cx.stack[pprev]) + if err != nil { + cx.err = err + return } + // if writing the same value, do nothing, matching ledger behavior with + // previous BuildEvalDelta mechanism + etv, ok, err := cx.Ledger.GetLocal(addr, cx.appID, key, accountIdx) + if err != nil { + cx.err = err + return + } + + tv := sv.toTealValue() + if !ok || tv != etv { + if _, ok := cx.Txn.EvalDelta.LocalDeltas[accountIdx]; !ok { + cx.Txn.EvalDelta.LocalDeltas[accountIdx] = basics.StateDelta{} + } + cx.Txn.EvalDelta.LocalDeltas[accountIdx][key] = tv.ToValueDelta() + } + err = cx.Ledger.SetLocal(addr, cx.appID, key, tv, accountIdx) if err != nil { cx.err = err return @@ -3397,7 +3577,19 @@ func opAppGlobalPut(cx *EvalContext) { return } - err := cx.Ledger.SetGlobal(key, sv.toTealValue()) + // if writing the same value, do nothing, matching ledger behavior with + // previous BuildEvalDelta mechanism + etv, ok, err := cx.Ledger.GetGlobal(cx.appID, key) + if err != nil { + cx.err = err + return + } + tv := sv.toTealValue() + if !ok || tv != etv { + cx.Txn.EvalDelta.GlobalDelta[key] = tv.ToValueDelta() + } + + err = cx.Ledger.SetGlobal(cx.appID, key, tv) if err != nil { cx.err = err return @@ -3417,9 +3609,15 @@ func opAppLocalDel(cx *EvalContext) { return } - addr, accountIdx, err := cx.accountReference(cx.stack[prev]) + addr, accountIdx, err := cx.mutableAccountReference(cx.stack[prev]) if err == nil { - err = cx.Ledger.DelLocal(addr, key, accountIdx) + if _, ok := cx.Txn.EvalDelta.LocalDeltas[accountIdx]; !ok { + cx.Txn.EvalDelta.LocalDeltas[accountIdx] = basics.StateDelta{} + } + cx.Txn.EvalDelta.LocalDeltas[accountIdx][key] = basics.ValueDelta{ + Action: basics.DeleteAction, + } + err = cx.Ledger.DelLocal(addr, cx.appID, key, accountIdx) } if err != nil { cx.err = err @@ -3439,7 +3637,10 @@ func opAppGlobalDel(cx *EvalContext) { return } - err := cx.Ledger.DelGlobal(key) + cx.Txn.EvalDelta.GlobalDelta[key] = basics.ValueDelta{ + Action: basics.DeleteAction, + } + err := cx.Ledger.DelGlobal(cx.appID, key) if err != nil { cx.err = err return @@ -3457,7 +3658,7 @@ func opAppGlobalDel(cx *EvalContext) { func appReference(cx *EvalContext, ref uint64, foreign bool) (basics.AppIndex, error) { if cx.version >= directRefEnabledVersion { if ref == 0 { - return cx.Ledger.ApplicationID(), nil + return cx.appID, nil } if ref <= uint64(len(cx.Txn.Txn.ForeignApps)) { return basics.AppIndex(cx.Txn.Txn.ForeignApps[ref-1]), nil @@ -3467,22 +3668,29 @@ func appReference(cx *EvalContext, ref uint64, foreign bool) (basics.AppIndex, e 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 the - // == 0 test, above, but it must come after the check - // for being below len(ForeignApps) - if ref == uint64(cx.Ledger.ApplicationID()) { - return cx.Ledger.ApplicationID(), 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 + // the == 0 test, above, but it must come after the check for being + // below len(ForeignApps) + if ref == uint64(cx.appID) { + return cx.appID, nil } } else { // Old rules + if ref == 0 { + return cx.appID, nil + } if foreign { // In old versions, a foreign reference must be an index in ForeignAssets or 0 - if ref == 0 { - return cx.Ledger.ApplicationID(), nil - } if ref <= uint64(len(cx.Txn.Txn.ForeignApps)) { return basics.AppIndex(cx.Txn.Txn.ForeignApps[ref-1]), nil } @@ -3505,6 +3713,14 @@ func asaReference(cx *EvalContext, ref uint64, foreign bool) (basics.AssetIndex, 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 { @@ -3648,10 +3864,66 @@ func opAppParamsGet(cx *EvalContext) { cx.stack = append(cx.stack, stackValue{Uint: exist}) } +func opAcctParamsGet(cx *EvalContext) { + last := len(cx.stack) - 1 // acct + + if cx.Ledger == nil { + cx.err = fmt.Errorf("ledger not available") + return + } + + addr, _, err := cx.accountReference(cx.stack[last]) + if err != nil { + cx.err = err + return + } + + paramField := AcctParamsField(cx.program[cx.pc+1]) + fs, ok := acctParamsFieldSpecByField[paramField] + if !ok || fs.version > cx.version { + cx.err = fmt.Errorf("invalid acct_params_get field %d", paramField) + return + } + + bal, err := cx.Ledger.Balance(addr) + if err != nil { + cx.err = err + return + } + exist := boolToUint(bal.Raw > 0) + + var value stackValue + + switch fs.field { + case AcctBalance: + value.Uint = bal.Raw + case AcctMinBalance: + mbal, err := cx.Ledger.MinBalance(addr, cx.Proto) + if err != nil { + cx.err = err + return + } + value.Uint = mbal.Raw + case AcctAuthAddr: + auth, err := cx.Ledger.Authorizer(addr) + if err != nil { + cx.err = err + return + } + if auth == addr { + value.Bytes = zeroAddress[:] + } else { + value.Bytes = auth[:] + } + } + cx.stack[last] = value + cx.stack = append(cx.stack, stackValue{Uint: exist}) +} + func opLog(cx *EvalContext) { last := len(cx.stack) - 1 - if len(cx.Logs) == MaxLogCalls { + if len(cx.Txn.EvalDelta.Logs) == MaxLogCalls { cx.err = fmt.Errorf("too many log calls in program. up to %d is allowed", MaxLogCalls) return } @@ -3661,38 +3933,32 @@ func opLog(cx *EvalContext) { cx.err = fmt.Errorf("program logs too large. %d bytes > %d bytes limit", cx.logSize, MaxLogSize) return } - cx.Logs = append(cx.Logs, string(log.Bytes)) + cx.Txn.EvalDelta.Logs = append(cx.Txn.EvalDelta.Logs, string(log.Bytes)) cx.stack = cx.stack[:last] } func authorizedSender(cx *EvalContext, addr basics.Address) bool { - appAddr, err := cx.getApplicationAddress() - if err != nil { - return false - } authorizer, err := cx.Ledger.Authorizer(addr) if err != nil { return false } - return appAddr == authorizer + return cx.getApplicationAddress(cx.appID) == authorizer } // addInnerTxn appends a fresh SignedTxn to subtxns, populated with reasonable // defaults. func addInnerTxn(cx *EvalContext) error { - addr, err := cx.getApplicationAddress() - if err != nil { - return err - } + addr := cx.getApplicationAddress(cx.appID) // For compatibility with v5, in which failures only occurred in the submit, - // we only fail here if we are OVER the MaxInnerTransactions limit. Thus - // this allows construction of one more Inner than is actually allowed, and - // will fail in submit. (But we do want the check here, so this can't become - // unbounded.) The MaxTxGroupSize check can be, and is, precise. - if len(cx.InnerTxns)+len(cx.subtxns) > cx.Proto.MaxInnerTransactions || - len(cx.subtxns) >= cx.Proto.MaxTxGroupSize { - return errors.New("attempt to create too many inner transactions") + // we only fail here if we are already over the max inner limit. Thus this + // allows construction of one more Inner than is actually allowed, and will + // fail in submit. (But we do want the check here, so this can't become + // unbounded.) The MaxTxGroupSize check can be, and is, precise. (That is, + // if we are at max group size, we can panic now, since we are trying to add + // too many) + if len(cx.Txn.EvalDelta.InnerTxns)+len(cx.subtxns) > cx.allowedInners() || len(cx.subtxns) >= cx.Proto.MaxTxGroupSize { + return fmt.Errorf("too many inner transactions %d", len(cx.Txn.EvalDelta.InnerTxns)+len(cx.subtxns)) } stxn := transactions.SignedTxn{} @@ -3757,25 +4023,67 @@ 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 } + 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) } -func (cx *EvalContext) stackIntoTxnField(sv stackValue, fs txnFieldSpec, txn *transactions.Transaction) (err error) { +// availableApp is used instead of appReference for more recent (stateful) +// opcodes that don't need (or want!) to allow low numbers to represent the app +// at that index in ForeignApps array. +func (cx *EvalContext) availableApp(sv stackValue) (basics.AppIndex, error) { + uint, err := sv.uint() + if err != nil { + 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 + } + + return 0, fmt.Errorf("invalid App reference %d", aid) +} + +func (cx *EvalContext) stackIntoTxnField(sv stackValue, fs *txnFieldSpec, txn *transactions.Transaction) (err error) { switch fs.field { case Type: if sv.Bytes == nil { - err = fmt.Errorf("Type arg not a byte array") - return + return fmt.Errorf("Type arg not a byte array") } txType := string(sv.Bytes) ver, ok := innerTxnTypes[txType] @@ -3810,11 +4118,10 @@ func (cx *EvalContext) stackIntoTxnField(sv stackValue, fs txnFieldSpec, txn *tr // round, and separation by MaxLifetime (check lifetime in submit, not here) case Note: if len(sv.Bytes) > cx.Proto.MaxTxnNoteBytes { - err = fmt.Errorf("%s may not exceed %d bytes", fs.field, cx.Proto.MaxTxnNoteBytes) - } else { - txn.Note = make([]byte, len(sv.Bytes)) - copy(txn.Note[:], sv.Bytes) + return fmt.Errorf("%s may not exceed %d bytes", fs.field, cx.Proto.MaxTxnNoteBytes) } + txn.Note = make([]byte, len(sv.Bytes)) + copy(txn.Note, sv.Bytes) // GenesisID, GenesisHash unsettable: surely makes no sense // Group unsettable: Can't make groups from AVM (yet?) // Lease unsettable: This seems potentially useful. @@ -3825,16 +4132,14 @@ func (cx *EvalContext) stackIntoTxnField(sv stackValue, fs txnFieldSpec, txn *tr // KeyReg case VotePK: if len(sv.Bytes) != 32 { - err = fmt.Errorf("%s must be 32 bytes", fs.field) - } else { - copy(txn.VotePK[:], sv.Bytes) + return fmt.Errorf("%s must be 32 bytes", fs.field) } + copy(txn.VotePK[:], sv.Bytes) case SelectionPK: if len(sv.Bytes) != 32 { - err = fmt.Errorf("%s must be 32 bytes", fs.field) - } else { - copy(txn.SelectionPK[:], sv.Bytes) + return fmt.Errorf("%s must be 32 bytes", fs.field) } + copy(txn.SelectionPK[:], sv.Bytes) case VoteFirst: var round uint64 round, err = sv.uint() @@ -3891,10 +4196,9 @@ func (cx *EvalContext) stackIntoTxnField(sv stackValue, fs txnFieldSpec, txn *tr txn.AssetParams.URL, err = sv.string(cx.Proto.MaxAssetURLBytes) case ConfigAssetMetadataHash: if len(sv.Bytes) != 32 { - err = fmt.Errorf("%s must be 32 bytes", fs.field) - } else { - copy(txn.AssetParams.MetadataHash[:], sv.Bytes) + return fmt.Errorf("%s must be 32 bytes", fs.field) } + copy(txn.AssetParams.MetadataHash[:], sv.Bytes) case ConfigAssetManager: txn.AssetParams.Manager, err = sv.address() case ConfigAssetReserve: @@ -3911,10 +4215,96 @@ func (cx *EvalContext) stackIntoTxnField(sv stackValue, fs txnFieldSpec, txn *tr case FreezeAssetFrozen: txn.AssetFrozen, err = sv.bool() - // appl needs to wait. Can't call AVM from AVM. - + // ApplicationCall + case ApplicationID: + txn.ApplicationID, err = cx.availableApp(sv) + case OnCompletion: + var onc uint64 + onc, err = sv.uintMaxed(uint64(transactions.DeleteApplicationOC)) + txn.OnCompletion = transactions.OnCompletion(onc) + case ApplicationArgs: + if sv.Bytes == nil { + return fmt.Errorf("ApplicationArg is not a byte array") + } + total := len(sv.Bytes) + for _, arg := range txn.ApplicationArgs { + total += len(arg) + } + if total > cx.Proto.MaxAppTotalArgLen { + return errors.New("total application args length too long") + } + if len(txn.ApplicationArgs) >= cx.Proto.MaxAppArgs { + return errors.New("too many application args") + } + new := make([]byte, len(sv.Bytes)) + copy(new, sv.Bytes) + txn.ApplicationArgs = append(txn.ApplicationArgs, new) + case Accounts: + var new basics.Address + new, err = cx.availableAccount(sv) + if err != nil { + return + } + if len(txn.Accounts) >= cx.Proto.MaxAppTxnAccounts { + return errors.New("too many foreign accounts") + } + txn.Accounts = append(txn.Accounts, new) + case ApprovalProgram: + maxPossible := cx.Proto.MaxAppProgramLen * (1 + cx.Proto.MaxExtraAppProgramPages) + if len(sv.Bytes) > maxPossible { + return fmt.Errorf("%s may not exceed %d bytes", fs.field, maxPossible) + } + txn.ApprovalProgram = make([]byte, len(sv.Bytes)) + copy(txn.ApprovalProgram, sv.Bytes) + case ClearStateProgram: + maxPossible := cx.Proto.MaxAppProgramLen * (1 + cx.Proto.MaxExtraAppProgramPages) + if len(sv.Bytes) > maxPossible { + return fmt.Errorf("%s may not exceed %d bytes", fs.field, maxPossible) + } + txn.ClearStateProgram = make([]byte, len(sv.Bytes)) + copy(txn.ClearStateProgram, sv.Bytes) + case Assets: + var new basics.AssetIndex + new, err = cx.availableAsset(sv) + if err != nil { + return + } + if len(txn.ForeignAssets) >= cx.Proto.MaxAppTxnForeignAssets { + return errors.New("too many foreign assets") + } + txn.ForeignAssets = append(txn.ForeignAssets, new) + case Applications: + var new basics.AppIndex + new, err = cx.availableApp(sv) + if err != nil { + return + } + if len(txn.ForeignApps) >= cx.Proto.MaxAppTxnForeignApps { + return errors.New("too many foreign apps") + } + txn.ForeignApps = append(txn.ForeignApps, new) + case GlobalNumUint: + txn.GlobalStateSchema.NumUint, err = + sv.uintMaxed(cx.Proto.MaxGlobalSchemaEntries) + case GlobalNumByteSlice: + txn.GlobalStateSchema.NumByteSlice, err = + sv.uintMaxed(cx.Proto.MaxGlobalSchemaEntries) + case LocalNumUint: + txn.LocalStateSchema.NumUint, err = + sv.uintMaxed(cx.Proto.MaxLocalSchemaEntries) + case LocalNumByteSlice: + txn.LocalStateSchema.NumByteSlice, err = + sv.uintMaxed(cx.Proto.MaxLocalSchemaEntries) + case ExtraProgramPages: + var epp uint64 + epp, err = + sv.uintMaxed(uint64(cx.Proto.MaxExtraAppProgramPages)) + if err != nil { + return + } + txn.ExtraProgramPages = uint32(epp) default: - return fmt.Errorf("invalid itxn_field %s", fs.field) + err = fmt.Errorf("invalid itxn_field %s", fs.field) } return } @@ -3933,7 +4323,7 @@ func opTxField(cx *EvalContext) { return } sv := cx.stack[last] - cx.err = cx.stackIntoTxnField(sv, fs, &cx.subtxns[itx].Txn) + cx.err = cx.stackIntoTxnField(sv, &fs, &cx.subtxns[itx].Txn) cx.stack = cx.stack[:last] // pop } @@ -3943,10 +4333,11 @@ func opTxSubmit(cx *EvalContext) { return } - // Should never trigger, since itxn_next checks these too. - if len(cx.InnerTxns)+len(cx.subtxns) > cx.Proto.MaxInnerTransactions || - len(cx.subtxns) > cx.Proto.MaxTxGroupSize { - cx.err = errors.New("too many inner transactions") + // Should rarely trigger, since itxn_next checks these too. (but that check + // must be imperfect, see its comment) In contrast to that check, subtxns is + // already populated here. + if len(cx.Txn.EvalDelta.InnerTxns)+len(cx.subtxns) > cx.allowedInners() || len(cx.subtxns) > cx.Proto.MaxTxGroupSize { + cx.err = fmt.Errorf("too many inner transactions %d", len(cx.Txn.EvalDelta.InnerTxns)+len(cx.subtxns)) return } @@ -3977,6 +4368,14 @@ func opTxSubmit(cx *EvalContext) { *cx.FeeCredit = basics.AddSaturate(*cx.FeeCredit, overpay) } + // All subtxns will have zero'd GroupID since GroupID can't be set in + // AVM. (no need to blank it out before hashing for TxID) + var group transactions.TxGroup + var parent transactions.Txid + isGroup := len(cx.subtxns) > 1 + if isGroup { + parent = cx.Txn.ID() + } for itx := range cx.subtxns { // The goal is to follow the same invariants used by the // transaction pool. Namely that any transaction that makes it @@ -3988,23 +4387,52 @@ func opTxSubmit(cx *EvalContext) { } // Recall that WellFormed does not care about individual - // transaction fees because of fee pooling. So we check below. + // transaction fees because of fee pooling. Checked above. cx.err = cx.subtxns[itx].Txn.WellFormed(*cx.Specials, *cx.Proto) if cx.err != nil { return } - ad, err := cx.Ledger.Perform(&cx.subtxns[itx].Txn, *cx.Specials) + // Disallow re-entrancy + if cx.subtxns[itx].Txn.Type == protocol.ApplicationCallTx { + if cx.appID == cx.subtxns[itx].Txn.ApplicationID { + cx.err = fmt.Errorf("attempt to self-call") + return + } + for parent := cx.caller; parent != nil; parent = parent.caller { + if parent.appID == cx.subtxns[itx].Txn.ApplicationID { + cx.err = fmt.Errorf("attempt to re-enter %d", parent.appID) + return + } + } + } + + if isGroup { + innerOffset := len(cx.Txn.EvalDelta.InnerTxns) + group.TxGroupHashes = append(group.TxGroupHashes, + crypto.Digest(cx.subtxns[itx].Txn.InnerID(parent, innerOffset))) + } + } + + if isGroup { + groupID := crypto.HashObj(group) + for itx := range cx.subtxns { + cx.subtxns[itx].Txn.Group = groupID + } + } + + ep := NewInnerEvalParams(cx.subtxns, cx) + for i := range ep.TxnGroup { + err := cx.Ledger.Perform(i, ep) if err != nil { cx.err = err return } - - cx.InnerTxns = append(cx.InnerTxns, transactions.SignedTxnWithAD{ - SignedTxn: cx.subtxns[itx], - ApplyData: ad, - }) + // 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 } @@ -4038,7 +4466,7 @@ func (cx *EvalContext) PcDetails() (pc int, dis string) { func base64Decode(encoded []byte, encoding *base64.Encoding) ([]byte, error) { decoded := make([]byte, encoding.DecodedLen(len(encoded))) - n, err := encoding.Strict().Decode(decoded, encoded) + n, err := encoding.Decode(decoded, encoded) if err != nil { return decoded[:0], err } |