summaryrefslogtreecommitdiff
path: root/data/transactions/logic/eval.go
diff options
context:
space:
mode:
Diffstat (limited to 'data/transactions/logic/eval.go')
-rw-r--r--data/transactions/logic/eval.go171
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: &params.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: &params.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