diff options
Diffstat (limited to 'data/transactions/logic/eval.go')
-rw-r--r-- | data/transactions/logic/eval.go | 171 |
1 files changed, 139 insertions, 32 deletions
diff --git a/data/transactions/logic/eval.go b/data/transactions/logic/eval.go index 836b8cb85..210d7a316 100644 --- a/data/transactions/logic/eval.go +++ b/data/transactions/logic/eval.go @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2023 Algorand, Inc. +// Copyright (C) 2019-2024 Algorand, Inc. // This file is part of go-algorand // // go-algorand is free software: you can redistribute it and/or modify @@ -28,6 +28,7 @@ import ( "math/big" "math/bits" "runtime" + "strconv" "strings" "golang.org/x/exp/slices" @@ -66,6 +67,11 @@ var maxAppCallDepth = 8 // maxStackDepth should not change unless controlled by an AVM version change const maxStackDepth = 1000 +// maxTxGroupSize is the same as config.MaxTxGroupSize, but is a constant so +// that we can declare an array of this size. A unit test confirms that they +// match. +const maxTxGroupSize = 16 + // stackValue is the type for the operand stack. // Each stackValue is either a valid []byte value or a uint64 value. // If (.Bytes != nil) the stackValue is a []byte value, otherwise uint64 value. @@ -102,6 +108,17 @@ func (sv stackValue) String() string { return fmt.Sprintf("%d 0x%x", sv.Uint, sv.Uint) } +func (sv stackValue) asAny() any { + if sv.Bytes != nil { + return sv.Bytes + } + return sv.Uint +} + +func (sv stackValue) isEmpty() bool { + return sv.Bytes == nil && sv.Uint == 0 +} + func (sv stackValue) address() (addr basics.Address, err error) { if len(sv.Bytes) != len(addr) { return basics.Address{}, errors.New("not an address") @@ -301,7 +318,7 @@ type EvalParams struct { TxnGroup []transactions.SignedTxnWithAD - pastScratch []*scratchSpace + pastScratch [maxTxGroupSize]*scratchSpace logger logging.Logger @@ -457,7 +474,6 @@ func NewAppEvalParams(txgroup []transactions.SignedTxnWithAD, proto *config.Cons TxnGroup: copyWithClearAD(txgroup), Proto: proto, Specials: specials, - pastScratch: make([]*scratchSpace, len(txgroup)), minAvmVersion: computeMinAvmVersion(txgroup), FeeCredit: credit, PooledApplicationBudget: pooledApplicationBudget, @@ -525,7 +541,6 @@ func NewInnerEvalParams(txg []transactions.SignedTxnWithAD, caller *EvalContext) Proto: caller.Proto, Trace: caller.Trace, TxnGroup: txg, - pastScratch: make([]*scratchSpace, len(txg)), logger: caller.logger, SigLedger: caller.SigLedger, Ledger: caller.Ledger, @@ -784,7 +799,7 @@ var ( // AllStackTypes is a map of all the stack types we recognize // so that we can iterate over them in doc prep // and use them for opcode proto shorthand - AllStackTypes = map[rune]StackType{ + AllStackTypes = map[byte]StackType{ 'a': StackAny, 'b': StackBytes, 'i': StackUint64, @@ -792,9 +807,6 @@ var ( 'A': StackAddress, 'I': StackBigInt, 'T': StackBoolean, - '3': StackBytes32, - '6': StackBytes64, - '8': StackBytes80, 'M': StackMethodSelector, 'K': StackStateKey, 'N': StackBoxName, @@ -879,11 +891,11 @@ func (st StackType) widened() StackType { } } -func (st StackType) constant() (uint64, bool) { - if st.Bound[0] == st.Bound[1] { - return st.Bound[0], true +func (st StackType) constInt() (uint64, bool) { + if st.AVMType != avmUint64 || st.Bound[0] != st.Bound[1] { + return 0, false } - return 0, false + return st.Bound[0], true } // overlaps checks if there is enough overlap @@ -932,13 +944,31 @@ func parseStackTypes(spec string) StackTypes { if spec == "" { return nil } - types := make(StackTypes, len(spec)) - for i, letter := range spec { + types := make(StackTypes, 0, len(spec)) + for i := 0; i < len(spec); i++ { + letter := spec[i] + if letter == '{' { + if types[len(types)-1] != StackBytes { + panic("{ after non-bytes " + spec) + } + end := strings.IndexByte(spec[i:], '}') + if end == -1 { + panic("No } after b{ " + spec) + } + size, err := strconv.Atoi(spec[i+1 : i+end]) + if err != nil { + panic("b{} does not contain a number " + spec) + } + // replace the generic type with the constrained type + types[len(types)-1] = NewStackType(avmBytes, static(uint64(size)), fmt.Sprintf("[%d]byte", size)) + i += end + continue + } st, ok := AllStackTypes[letter] if !ok { panic(spec) } - types[i] = st + types = append(types, st) } return types } @@ -968,10 +998,9 @@ var errTooManyArgs = errors.New("LogicSig has too many arguments") // EvalError indicates AVM evaluation failure type EvalError struct { - Err error - details string - groupIndex int - logicsig bool + Err error + details string + logicsig bool } // Error satisfies builtin interface `error` @@ -992,6 +1021,78 @@ func (err EvalError) Unwrap() error { return err.Err } +func (cx *EvalContext) evalError(err error) error { + pc, det := cx.pcDetails() + details := fmt.Sprintf("pc=%d, opcodes=%s", pc, det) + + err = basics.Annotate(err, + "pc", pc, + "group-index", cx.groupIndex, + "eval-states", cx.evalStates()) + if cx.runMode == ModeApp { + details = fmt.Sprintf("app=%d, %s", cx.appID, details) + err = basics.Annotate(err, "app-index", cx.appID) + } + + return EvalError{err, details, cx.runMode == ModeSig} +} + +type evalState struct { + Scratch []any `json:"scratch,omitempty"` + Stack []any `json:"stack,omitempty"` + Logs [][]byte `json:"logs,omitempty"` +} + +func (cx *EvalContext) evalStates() []evalState { + states := make([]evalState, cx.groupIndex+1) + for i := 0; i <= cx.groupIndex; i++ { + var scratch []stackValue + if cx.pastScratch[i] != nil { + scratch = (*cx.pastScratch[i])[:] + } + lastNonZero := -1 + scratchAsAny := make([]any, len(scratch)) + for s, sv := range scratch { + if !sv.isEmpty() { + lastNonZero = s + } + scratchAsAny[s] = sv.asAny() + } + if lastNonZero == -1 { + scratchAsAny = nil + } else { + scratchAsAny = scratchAsAny[:lastNonZero+1] + } + + // Only the current program's stack is still available. So perhaps it + // should be located outside of the evalState, with the PC. + var stack []any + if cx.groupIndex == i { + stack = convertSlice(cx.Stack, func(sv stackValue) any { + return sv.asAny() + }) + } + + states[i] = evalState{ + Scratch: scratchAsAny, + Stack: stack, + Logs: convertSlice(cx.TxnGroup[i].EvalDelta.Logs, func(s string) []byte { return []byte(s) }), + } + } + return states +} + +func convertSlice[X any, Y any](input []X, fn func(X) Y) []Y { + if input == nil { + return nil + } + output := make([]Y, len(input)) + for i := range input { + output[i] = fn(input[i]) + } + return output +} + // EvalContract executes stateful 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 { @@ -1013,6 +1114,10 @@ func EvalContract(program []byte, gi int, aid basics.AppIndex, params *EvalParam txn: ¶ms.TxnGroup[gi], appID: aid, } + // Save scratch for `gload`. We used to copy, but cx.scratch is quite large, + // about 8k, and caused measurable CPU and memory demands. Of course, these + // should never be changed by later transactions. + cx.pastScratch[cx.groupIndex] = &cx.Scratch if cx.Proto.IsolateClearState && cx.txn.Txn.OnCompletion == transactions.ClearStateOC { if cx.PooledApplicationBudget != nil && *cx.PooledApplicationBudget < cx.Proto.MaxAppProgramCost { @@ -1070,7 +1175,13 @@ func EvalContract(program []byte, gi int, aid basics.AppIndex, params *EvalParam if used > cx.ioBudget { err = fmt.Errorf("box read budget (%d) exceeded", cx.ioBudget) if !cx.Proto.EnableBareBudgetError { - err = EvalError{err, "", gi, false} + // We return an EvalError here because we used to do + // that. It is wrong, and means that there could be a + // ClearState call in an old block that failed on read + // quota, but we allowed to execute anyway. If testnet and + // mainnet have no such transactions, we can remove + // EnableBareBudgetError and this code. + err = EvalError{err, "", false} } return false, nil, err } @@ -1084,20 +1195,13 @@ func EvalContract(program []byte, gi int, aid basics.AppIndex, params *EvalParam } pass, err := eval(program, &cx) if err != nil { - pc, det := cx.pcDetails() - details := fmt.Sprintf("pc=%d, opcodes=%s", pc, det) - err = EvalError{err, details, gi, false} + err = cx.evalError(err) } if cx.Trace != nil && cx.caller != nil { fmt.Fprintf(cx.Trace, "--- exit %d accept=%t\n", aid, pass) } - // Save scratch for `gload`. We used to copy, but cx.scratch is quite large, - // about 8k, and caused measurable CPU and memory demands. Of course, these - // should never be changed by later transactions. - cx.pastScratch[cx.groupIndex] = &cx.Scratch - return pass, &cx, err } @@ -1123,12 +1227,15 @@ func EvalSignatureFull(gi int, params *EvalParams) (bool, *EvalContext, error) { groupIndex: gi, txn: ¶ms.TxnGroup[gi], } + // Save scratch. `gload*` opcodes are not currently allowed in ModeSig + // (though it seems we could allow them, with access to LogicSig scratch + // values). But error returns and potentially debug code might like to + // return them. + cx.pastScratch[cx.groupIndex] = &cx.Scratch pass, err := eval(cx.txn.Lsig.Logic, &cx) if err != nil { - pc, det := cx.pcDetails() - details := fmt.Sprintf("pc=%d, opcodes=%s", pc, det) - err = EvalError{err, details, gi, true} + err = cx.evalError(err) } return pass, &cx, err @@ -5539,7 +5646,7 @@ func opItxnSubmit(cx *EvalContext) (err error) { } if err != nil { - return err + return basics.Wrap(err, fmt.Sprintf("inner tx %d failed: %s", i, err.Error()), "inner") } // This is mostly a no-op, because Perform does its work "in-place", but |