diff --git a/data/transactions/logic/README.md b/data/transactions/logic/README.md index 2fda903c98..e577ea93f9 100644 --- a/data/transactions/logic/README.md +++ b/data/transactions/logic/README.md @@ -584,15 +584,21 @@ Account fields used in the `acct_params_get` opcode. | `b target` | branch unconditionally to TARGET | | `return` | use A as success value; end | | `pop` | discard A | +| `popn n` | Remove N values from the top of the stack | | `dup` | duplicate A | | `dup2` | duplicate A and B | +| `dupn n` | duplicate A, N times | | `dig n` | Nth value from the top of the stack. dig 0 is equivalent to dup | +| `bury n` | Replace the Nth value from the top of the stack. bury 0 fails. | | `cover n` | remove top of stack, and place it deeper in the stack such that N elements are above it. Fails if stack depth <= N. | | `uncover n` | remove the value at depth N in the stack and shift above items down so the Nth deep value is on top of the stack. Fails if stack depth <= N. | +| `frame_dig i` | Nth (signed) value from the frame pointer. | +| `frame_bury i` | Replace the Nth (signed) value from the frame pointer in the stack | | `swap` | swaps A and B on stack | | `select` | selects one of two values based on top-of-stack: B if C != 0, else A | | `assert` | immediately fail unless A is a non-zero number | | `callsub target` | branch unconditionally to TARGET, saving the next instruction on the call stack | +| `proto a r` | Prepare top call frame for a retsub that will assume A args and R return values. | | `retsub` | pop the top instruction from the call stack and branch to it | | `switch target ...` | branch to the Ath label. Continue at following instruction if index A exceeds the number of labels. | diff --git a/data/transactions/logic/TEAL_opcodes.md b/data/transactions/logic/TEAL_opcodes.md index 05cb20b96d..d093c08230 100644 --- a/data/transactions/logic/TEAL_opcodes.md +++ b/data/transactions/logic/TEAL_opcodes.md @@ -610,6 +610,27 @@ See `bnz` for details on how branches work. `b` always jumps to the offset. - immediately fail unless A is a non-zero number - Availability: v3 +## bury n + +- Opcode: 0x45 {uint8 depth} +- Stack: ..., A → ... +- Replace the Nth value from the top of the stack. bury 0 fails. +- Availability: v8 + +## popn n + +- Opcode: 0x46 {uint8 stack depth} +- Stack: ..., [N items] → ... +- Remove N values from the top of the stack +- Availability: v8 + +## dupn n + +- Opcode: 0x47 {uint8 copy count} +- Stack: ..., A → ..., A, [N copies of A] +- duplicate A, N times +- Availability: v8 + ## pop - Opcode: 0x48 @@ -790,7 +811,7 @@ When A is a uint64, index 0 is the least significant bit. Setting bit 3 to 1 on ## json_ref r -- Opcode: 0x5f {string return type} +- Opcode: 0x5f {uint8 return type} - Stack: ..., A: []byte, B: []byte → ..., any - key B's value, of type R, from a [valid](jsonspec.md) utf-8 encoded json object A - **Cost**: 25 + 2 per 7 bytes of A @@ -1042,7 +1063,7 @@ pushint args are not added to the intcblock during assembly processes - branch unconditionally to TARGET, saving the next instruction on the call stack - Availability: v4 -The call stack is separate from the data stack. Only `callsub` and `retsub` manipulate it. +The call stack is separate from the data stack. Only `callsub`, `retsub`, and `proto` manipulate it. ## retsub @@ -1051,11 +1072,34 @@ The call stack is separate from the data stack. Only `callsub` and `retsub` mani - pop the top instruction from the call stack and branch to it - Availability: v4 -The call stack is separate from the data stack. Only `callsub` and `retsub` manipulate it. +If the current frame was prepared by `proto A R`, `retsub` will remove the 'A' arguments from the stack, move the `R` return values down, and pop any stack locations above the relocated return values. + +## proto a r + +- Opcode: 0x8a {uint8 arguments} {uint8 return values} +- Stack: ... → ... +- Prepare top call frame for a retsub that will assume A args and R return values. +- Availability: v8 + +Fails unless the last instruction executed was a `callsub`. + +## frame_dig i + +- Opcode: 0x8b {int8 frame slot} +- Stack: ... → ..., any +- Nth (signed) value from the frame pointer. +- Availability: v8 + +## frame_bury i + +- Opcode: 0x8c {int8 frame slot} +- Stack: ..., A → ... +- Replace the Nth (signed) value from the frame pointer in the stack +- Availability: v8 ## switch target ... -- Opcode: 0x8a {uint8 branch count} [{int16 branch offset, big-endian}, ...] +- Opcode: 0x8d {uint8 branch count} [{int16 branch offset, big-endian}, ...] - Stack: ..., A: uint64 → ... - branch to the Ath label. Continue at following instruction if index A exceeds the number of labels. - Availability: v8 diff --git a/data/transactions/logic/assembler.go b/data/transactions/logic/assembler.go index 47d5da1a15..fd3c79bf44 100644 --- a/data/transactions/logic/assembler.go +++ b/data/transactions/logic/assembler.go @@ -266,6 +266,7 @@ func newOpStream(version uint64) OpStream { OffsetToLine: make(map[int]int), typeTracking: true, Version: version, + known: ProgramKnowledge{fp: -1}, } for i := range o.known.scratchSpace { @@ -283,7 +284,7 @@ type ProgramKnowledge struct { // Return.Types. If `deadcode` is true, `stack` should be empty. stack StackTypes - // bottom is the type given out when known is empty. It is StackNone at + // bottom is the type given out when `stack` is empty. It is StackNone at // program start, so, for example, a `+` opcode at the start of a program // fails. But when a label or callsub is encountered, `stack` is truncated // and `bottom` becomes StackAny, because we don't track program state @@ -295,9 +296,24 @@ type ProgramKnowledge struct { // errors should be reported. deadcode bool + // fp is the frame pointer, if known/usable, or -1 if not. When + // encountering a `proto`, `stack` is grown to fit `args`, and this `fp` is + // set to the top of those args. This may not be the "real" fp when the + // program is actually evaluated, but it is good enough for frame_{dig/bury} + // to work from there. + fp int + scratchSpace [256]StackType } +func (pgm *ProgramKnowledge) top() (StackType, bool) { + if len(pgm.stack) == 0 { + return pgm.bottom, pgm.bottom != StackNone + } + last := len(pgm.stack) - 1 + return pgm.stack[last], true +} + func (pgm *ProgramKnowledge) pop() StackType { if len(pgm.stack) == 0 { return pgm.bottom @@ -328,6 +344,7 @@ func (pgm *ProgramKnowledge) label() { func (pgm *ProgramKnowledge) reset() { pgm.stack = nil pgm.bottom = StackAny + pgm.fp = -1 pgm.deadcode = false for i := range pgm.scratchSpace { pgm.scratchSpace[i] = StackAny @@ -354,7 +371,7 @@ func (ops *OpStream) referToLabel(pc int, label string, offsetPosition int) { ops.labelReferences = append(ops.labelReferences, labelReference{ops.sourceLine, pc, label, offsetPosition}) } -type refineFunc func(pgm *ProgramKnowledge, immediates []string) (StackTypes, StackTypes) +type refineFunc func(pgm *ProgramKnowledge, immediates []string) (StackTypes, StackTypes, error) // returns allows opcodes like `txn` to be specific about their return value // types, based on the field requested, rather than use Any as specified by @@ -533,7 +550,7 @@ func asmIntC(ops *OpStream, spec *OpSpec, args []string) error { if len(args) != 1 { return ops.error("intc operation needs one argument") } - constIndex, err := simpleImm(args[0], "constant") + constIndex, err := byteImm(args[0], "constant") if err != nil { return ops.error(err) } @@ -544,7 +561,7 @@ func asmByteC(ops *OpStream, spec *OpSpec, args []string) error { if len(args) != 1 { return ops.error("bytec operation needs one argument") } - constIndex, err := simpleImm(args[0], "constant") + constIndex, err := byteImm(args[0], "constant") if err != nil { return ops.error(err) } @@ -881,7 +898,7 @@ func asmArg(ops *OpStream, spec *OpSpec, args []string) error { if len(args) != 1 { return ops.error("arg operation needs one argument") } - val, err := simpleImm(args[0], "argument") + val, err := byteImm(args[0], "argument") if err != nil { return ops.error(err) } @@ -946,7 +963,7 @@ func asmSubstring(ops *OpStream, spec *OpSpec, args []string) error { return nil } -func simpleImm(value string, label string) (byte, error) { +func byteImm(value string, label string) (byte, error) { res, err := strconv.ParseUint(value, 0, 64) if err != nil { return 0, fmt.Errorf("unable to parse %s %#v as integer", label, value) @@ -957,6 +974,14 @@ func simpleImm(value string, label string) (byte, error) { return byte(res), err } +func int8Imm(value string, label string) (byte, error) { + res, err := strconv.ParseInt(value, 10, 8) + if err != nil { + return 0, fmt.Errorf("unable to parse %s %#v as int8", label, value) + } + return byte(res), err +} + func asmItxn(ops *OpStream, spec *OpSpec, args []string) error { if len(args) == 1 { return asmDefault(ops, spec, args) @@ -1020,7 +1045,7 @@ func asmDefault(ops *OpStream, spec *OpSpec, args []string) error { if imm.Group != nil { fs, ok := imm.Group.SpecByName(args[i]) if !ok { - _, err := simpleImm(args[i], "") + _, err := byteImm(args[i], "") if err == nil { // User supplied a uint, so we see if any of the other immediates take uints for j, otherImm := range spec.OpDetails.Immediates { @@ -1063,7 +1088,7 @@ func asmDefault(ops *OpStream, spec *OpSpec, args []string) error { ops.pending.WriteByte(fs.Field()) } else { // simple immediate that must be a number from 0-255 - val, err := simpleImm(args[i], imm.Name) + val, err := byteImm(args[i], imm.Name) if err != nil { if strings.Contains(err.Error(), "unable to parse") { // Perhaps the field works in a different order @@ -1086,6 +1111,12 @@ func asmDefault(ops *OpStream, spec *OpSpec, args []string) error { } ops.pending.WriteByte(val) } + case immInt8: + val, err := int8Imm(args[i], imm.Name) + if err != nil { + return ops.errorf("%s %w", spec.Name, err) + } + ops.pending.WriteByte(val) default: return ops.errorf("unable to assemble immKind %d", imm.kind) } @@ -1093,72 +1124,169 @@ func asmDefault(ops *OpStream, spec *OpSpec, args []string) error { return nil } -// Interprets the arg at index argIndex as byte-long immediate -func getByteImm(args []string, argIndex int) (byte, bool) { +// getImm interprets the arg at index argIndex as an immediate +func getImm(args []string, argIndex int) (int, bool) { if len(args) <= argIndex { return 0, false } - n, err := strconv.ParseUint(args[argIndex], 0, 8) + // We want to parse anything from -128 up to 255. So allow 9 bits. + // Normal assembly checking will catch signed as byte, vice versa + n, err := strconv.ParseInt(args[argIndex], 0, 9) if err != nil { return 0, false } - return byte(n), true + return int(n), true } -func typeSwap(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) { - topTwo := StackTypes{StackAny, StackAny} +func anyTypes(n int) StackTypes { + as := make(StackTypes, n) + for i := range as { + as[i] = StackAny + } + return as +} + +func typeSwap(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes, error) { + swapped := StackTypes{StackAny, StackAny} top := len(pgm.stack) - 1 if top >= 0 { - topTwo[1] = pgm.stack[top] + swapped[0] = pgm.stack[top] if top >= 1 { - topTwo[0] = pgm.stack[top-1] + swapped[1] = pgm.stack[top-1] } } - reversed := StackTypes{topTwo[1], topTwo[0]} - return nil, reversed + return nil, swapped, nil } -func typeDig(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) { - n, ok := getByteImm(args, 0) +func typeDig(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes, error) { + n, ok := getImm(args, 0) if !ok { - return nil, nil - } - depth := int(n) + 1 - anys := make(StackTypes, depth) - returns := make(StackTypes, depth+1) - for i := range anys { - anys[i] = StackAny - returns[i] = StackAny + return nil, nil, nil } - returns[depth] = StackAny + depth := n + 1 + returns := anyTypes(depth + 1) idx := len(pgm.stack) - depth if idx >= 0 { + // We return exactly what on the stack... + copy(returns[:], pgm.stack[idx:]) + // plus a repeat of what was at idx returns[len(returns)-1] = pgm.stack[idx] - for i := idx; i < len(pgm.stack); i++ { - returns[i-idx] = pgm.stack[i] + } + return anyTypes(depth), returns, nil +} + +func typeBury(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes, error) { + n, ok := getImm(args, 0) + if !ok { + return nil, nil, nil + } + if n == 0 { + return nil, nil, errors.New("bury 0 always fails") + } + + top := len(pgm.stack) - 1 + typ, ok := pgm.top() + if !ok { + return nil, nil, nil // Will error because bury demands a stack arg + } + + idx := top - n + if idx < 0 { + if pgm.bottom == StackNone { + // By demanding n+1 elements, we'll trigger an error + return anyTypes(n + 1), nil, nil } + // We're going to bury below the tracked portion of the stack, so there's + // nothing to update. + return nil, nil, nil + } + + returns := make(StackTypes, n) + copy(returns, pgm.stack[idx:]) // Won't have room to copy the top type + returns[0] = typ // Replace the bottom with the top type + return pgm.stack[idx:], returns, nil +} + +func typeFrameDig(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes, error) { + n, ok := getImm(args, 0) + if !ok { + return nil, nil, nil + } + // If we have no frame pointer, we can't do better than "any" + if pgm.fp == -1 { + return nil, nil, nil + } + + // If we do have a framepointer, we can try to get the type + idx := pgm.fp + n + if idx < 0 { + return nil, nil, fmt.Errorf("frame_dig %d in sub with %d args", n, pgm.fp) } - return anys, returns + if idx >= len(pgm.stack) { + return nil, nil, fmt.Errorf("frame_dig above stack") + } + return nil, StackTypes{pgm.stack[idx]}, nil } -func typeEquals(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) { +func typeFrameBury(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes, error) { + n, ok := getImm(args, 0) + if !ok { + return nil, nil, nil + } + + top := len(pgm.stack) - 1 + typ, ok := pgm.top() + if !ok { + return nil, nil, nil // Will error because fbury demands a stack arg + } + + // If we have no frame pointer, we have to wipe out any belief that the + // stack contains anything but the supplied type. + if pgm.fp == -1 { + // Perhaps it would be cleaner to build up the args, return slices to + // cause this, rather than manipulate the pgm.stack directly. + for i := range pgm.stack { + if pgm.stack[i] != typ { + pgm.stack[i] = StackAny + } + } + return nil, nil, nil + } + + // If we do have a framepointer, we can try to update the typestack + idx := pgm.fp + n + if idx < 0 { + return nil, nil, fmt.Errorf("frame_bury %d in sub with %d args", n, pgm.fp) + } + if idx >= top { + return nil, nil, fmt.Errorf("frame_bury above stack") + } + depth := top - idx + + returns := make(StackTypes, depth) + copy(returns, pgm.stack[idx:]) // Won't have room to copy the top type + returns[0] = typ // Replace the bottom with the top type + return pgm.stack[idx:], returns, nil +} + +func typeEquals(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes, error) { top := len(pgm.stack) - 1 if top >= 0 { //Require arg0 and arg1 to have same type - return StackTypes{pgm.stack[top], pgm.stack[top]}, nil + return StackTypes{pgm.stack[top], pgm.stack[top]}, nil, nil } - return nil, nil + return nil, nil, nil } -func typeDup(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) { +func typeDup(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes, error) { top := len(pgm.stack) - 1 if top >= 0 { - return StackTypes{pgm.stack[top]}, StackTypes{pgm.stack[top], pgm.stack[top]} + return nil, StackTypes{pgm.stack[top], pgm.stack[top]}, nil } - return nil, nil + return nil, nil, nil } -func typeDupTwo(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) { +func typeDupTwo(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes, error) { topTwo := StackTypes{StackAny, StackAny} top := len(pgm.stack) - 1 if top >= 0 { @@ -1167,41 +1295,35 @@ func typeDupTwo(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) { topTwo[0] = pgm.stack[top-1] } } - return nil, append(topTwo, topTwo...) + return nil, append(topTwo, topTwo...), nil } -func typeSelect(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) { +func typeSelect(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes, error) { top := len(pgm.stack) - 1 if top >= 2 { if pgm.stack[top-1] == pgm.stack[top-2] { - return nil, StackTypes{pgm.stack[top-1]} + return nil, StackTypes{pgm.stack[top-1]}, nil } } - return nil, nil + return nil, nil, nil } -func typeSetBit(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) { +func typeSetBit(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes, error) { top := len(pgm.stack) - 1 if top >= 2 { - return nil, StackTypes{pgm.stack[top-2]} + return nil, StackTypes{pgm.stack[top-2]}, nil } - return nil, nil + return nil, nil, nil } -func typeCover(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) { - n, ok := getByteImm(args, 0) +func typeCover(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes, error) { + n, ok := getImm(args, 0) if !ok { - return nil, nil + return nil, nil, nil } depth := int(n) + 1 - anys := make(StackTypes, depth) - for i := range anys { - anys[i] = StackAny - } - returns := make(StackTypes, depth) - for i := range returns { - returns[i] = StackAny - } + returns := anyTypes(depth) + idx := len(pgm.stack) - depth // This rotates all the types if idx is >= 0. But there's a potential // improvement: when pgm.bottom is StackAny, and the cover is going "under" @@ -1213,23 +1335,16 @@ func typeCover(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) { returns[i-idx+1] = pgm.stack[i] } } - return anys, returns + return anyTypes(depth), returns, nil } -func typeUncover(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) { - n, ok := getByteImm(args, 0) +func typeUncover(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes, error) { + n, ok := getImm(args, 0) if !ok { - return nil, nil - } - depth := int(n) + 1 - anys := make(StackTypes, depth) - for i := range anys { - anys[i] = StackAny - } - returns := make(StackTypes, depth) - for i := range returns { - returns[i] = StackAny + return nil, nil, nil } + depth := n + 1 + returns := anyTypes(depth) idx := len(pgm.stack) - depth // See precision comment in typeCover if idx >= 0 { @@ -1238,36 +1353,36 @@ func typeUncover(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) returns[i-idx-1] = pgm.stack[i] } } - return anys, returns + return anyTypes(depth), returns, nil } -func typeTxField(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) { +func typeTxField(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes, error) { if len(args) != 1 { - return nil, nil + return nil, nil, nil } fs, ok := txnFieldSpecByName[args[0]] if !ok { - return nil, nil + return nil, nil, nil } - return StackTypes{fs.ftype}, nil + return StackTypes{fs.ftype}, nil, nil } -func typeStore(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) { - scratchIndex, ok := getByteImm(args, 0) +func typeStore(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes, error) { + scratchIndex, ok := getImm(args, 0) if !ok { - return nil, nil + return nil, nil, nil } top := len(pgm.stack) - 1 if top >= 0 { pgm.scratchSpace[scratchIndex] = pgm.stack[top] } - return nil, nil + return nil, nil, nil } -func typeStores(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) { +func typeStores(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes, error) { top := len(pgm.stack) - 1 if top < 0 { - return nil, nil + return nil, nil, nil } for i := range pgm.scratchSpace { // We can't know what slot stacktop is being stored in, but we can at least keep the slots that are the same type as stacktop @@ -1275,26 +1390,68 @@ func typeStores(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) { pgm.scratchSpace[i] = StackAny } } - return nil, nil + return nil, nil, nil } -func typeLoad(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) { - scratchIndex, ok := getByteImm(args, 0) +func typeLoad(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes, error) { + scratchIndex, ok := getImm(args, 0) if !ok { - return nil, nil + return nil, nil, nil + } + return nil, StackTypes{pgm.scratchSpace[scratchIndex]}, nil +} + +func typeProto(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes, error) { + a, aok := getImm(args, 0) + _, rok := getImm(args, 1) + if !aok || !rok { + return nil, nil, nil + } + + if len(pgm.stack) != 0 || pgm.bottom != StackAny { + return nil, nil, fmt.Errorf("proto must be unreachable from previous PC") } - return nil, StackTypes{pgm.scratchSpace[scratchIndex]} + pgm.stack = anyTypes(a) + pgm.fp = a + return nil, nil, nil } -func typeLoads(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes) { +func typeLoads(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes, error) { scratchType := pgm.scratchSpace[0] for _, item := range pgm.scratchSpace { // If all the scratch slots are one type, then we can say we are loading that type if item != scratchType { - return nil, nil + return nil, nil, nil } } - return nil, StackTypes{scratchType} + return nil, StackTypes{scratchType}, nil +} + +func typePopN(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes, error) { + n, ok := getImm(args, 0) + if !ok { + return nil, nil, nil + } + return anyTypes(n), nil, nil +} + +func typeDupN(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes, error) { + n, ok := getImm(args, 0) + if !ok { + return nil, nil, nil + } + top := len(pgm.stack) - 1 + if top < 0 { + return nil, nil, nil + } + + // `dupn 3` ends up with 4 copies of ToS on top + copies := make(StackTypes, n+1) + for i := range copies { + copies[i] = pgm.stack[top] + } + + return nil, copies, nil } func joinIntsOnOr(singularTerminator string, list ...int) string { @@ -1591,9 +1748,9 @@ func (ops *OpStream) trace(format string, args ...interface{}) { fmt.Fprintf(ops.Trace, format, args...) } -func (ops *OpStream) typeError(err error) { +func (ops *OpStream) typeErrorf(format string, args ...interface{}) { if ops.typeTracking { - ops.error(err) + ops.errorf(format, args...) } } @@ -1605,9 +1762,8 @@ func (ops *OpStream) trackStack(args StackTypes, returns StackTypes, instruction } argcount := len(args) if argcount > len(ops.known.stack) && ops.known.bottom == StackNone { - err := fmt.Errorf("%s expects %d stack arguments but stack height is %d", + ops.typeErrorf("%s expects %d stack arguments but stack height is %d", strings.Join(instruction, " "), argcount, len(ops.known.stack)) - ops.typeError(err) } else { firstPop := true for i := argcount - 1; i >= 0; i-- { @@ -1620,9 +1776,8 @@ func (ops *OpStream) trackStack(args StackTypes, returns StackTypes, instruction ops.trace(", %s", argType) } if !typecheck(argType, stype) { - err := fmt.Errorf("%s arg %d wanted type %s got %s", + ops.typeErrorf("%s arg %d wanted type %s got %s", strings.Join(instruction, " "), i, argType, stype) - ops.typeError(err) } } if !firstPop { @@ -1706,7 +1861,10 @@ func (ops *OpStream) assemble(text string) error { } args, returns := spec.Arg.Types, spec.Return.Types if spec.refine != nil { - nargs, nreturns := spec.refine(&ops.known, current[1:]) + nargs, nreturns, err := spec.refine(&ops.known, current[1:]) + if err != nil { + ops.typeErrorf("%w", err) + } if nargs != nil { args = nargs } @@ -2293,7 +2451,7 @@ func disassemble(dis *disassembleState, spec *OpSpec) (string, error) { for _, imm := range spec.OpDetails.Immediates { out += " " switch imm.kind { - case immByte: + case immByte, immInt8: if pc >= len(dis.program) { return "", fmt.Errorf("program end while reading immediate %s for %s", imm.Name, spec.Name) @@ -2309,7 +2467,11 @@ func disassemble(dis *disassembleState, spec *OpSpec) (string, error) { } out += name } else { - out += fmt.Sprintf("%d", b) + if imm.kind == immByte { + out += fmt.Sprintf("%d", b) + } else if imm.kind == immInt8 { + out += fmt.Sprintf("%d", int8(b)) + } } if spec.Name == "intc" && int(b) < len(dis.intc) { out += fmt.Sprintf(" // %d", dis.intc[b]) diff --git a/data/transactions/logic/assembler_test.go b/data/transactions/logic/assembler_test.go index e614a722c6..1adf9b4508 100644 --- a/data/transactions/logic/assembler_test.go +++ b/data/transactions/logic/assembler_test.go @@ -20,6 +20,7 @@ import ( "bytes" "encoding/hex" "fmt" + "regexp" "strings" "testing" @@ -32,7 +33,8 @@ import ( ) // used by TestAssemble and others, see UPDATE PROCEDURE in TestAssemble() -const v1Nonsense = `err +const v1Nonsense = ` +err global MinTxnFee global MinBalance global MaxTxnLife @@ -118,6 +120,8 @@ intc 1 intc 1 ! % +| +& ^ ~ byte 0x4242 @@ -343,6 +347,7 @@ byte 0x0123456789abcd dup dup ecdsa_pk_recover Secp256k1 +itxn Sender itxna Logs 3 ` @@ -402,20 +407,20 @@ switch_label1: pushint 1 ` -const v8Nonsense = v7Nonsense + switchNonsense +const v8Nonsense = v7Nonsense + switchNonsense + frameNonsense const v9Nonsense = v8Nonsense + pairingNonsense -const v6Compiled = "2004010002b7a60c26050242420c68656c6c6f20776f726c6421070123456789abcd208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d047465737400320032013202320380021234292929292b0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e01022581f8acd19181cf959a1281f8acd19181cf951a81f8acd19181cf1581f8acd191810f082209240a220b230c240d250e230f23102311231223132314181b1c28171615400003290349483403350222231d4a484848482b50512a632223524100034200004322602261222704634848222862482864286548482228246628226723286828692322700048482371004848361c0037001a0031183119311b311d311e311f312023221e312131223123312431253126312731283129312a312b312c312d312e312f447825225314225427042455220824564c4d4b0222382124391c0081e80780046a6f686e2281d00f23241f880003420001892224902291922494249593a0a1a2a3a4a5a6a7a8a9aaabacadae24af3a00003b003c003d816472064e014f012a57000823810858235b235a2359b03139330039b1b200b322c01a23c1001a2323c21a23c3233e233f8120af06002a494905002a49490700b53a03b6b7043cb8033a0c2349c42a9631007300810881088120978101c53a8101c6003a" +const v6Compiled = "2004010002b7a60c26050242420c68656c6c6f20776f726c6421070123456789abcd208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d047465737400320032013202320380021234292929292b0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e01022581f8acd19181cf959a1281f8acd19181cf951a81f8acd19181cf1581f8acd191810f082209240a220b230c240d250e230f2310231123122313231418191a1b1c28171615400003290349483403350222231d4a484848482b50512a632223524100034200004322602261222704634848222862482864286548482228246628226723286828692322700048482371004848361c0037001a0031183119311b311d311e311f312023221e312131223123312431253126312731283129312a312b312c312d312e312f447825225314225427042455220824564c4d4b0222382124391c0081e80780046a6f686e2281d00f23241f880003420001892224902291922494249593a0a1a2a3a4a5a6a7a8a9aaabacadae24af3a00003b003c003d816472064e014f012a57000823810858235b235a2359b03139330039b1b200b322c01a23c1001a2323c21a23c3233e233f8120af06002a494905002a49490700b400b53a03b6b7043cb8033a0c2349c42a9631007300810881088120978101c53a8101c6003a" const randomnessCompiled = "81ffff03d101d000" const v7Compiled = v6Compiled + "5e005f018120af060180070123456789abcd49490501988003012345494984" + randomnessCompiled + "800243218001775c0280018881015d" -const switchCompiled = "81018a02fff800008101" +const switchCompiled = "81018d02fff800008101" -const v8Compiled = v7Compiled + switchCompiled +const v8Compiled = v7Compiled + switchCompiled + frameCompiled const v9Compiled = v7Compiled + pairingCompiled @@ -432,11 +437,11 @@ var nonsense = map[uint64]string{ } var compiled = map[uint64]string{ - 1: "012008b7a60cf8acd19181cf959a12f8acd19181cf951af8acd19181cf15f8acd191810f01020026050212340c68656c6c6f20776f726c6421208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d024242047465737400320032013202320328292929292a0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e0102222324252104082209240a220b230c240d250e230f23102311231223132314181b1c2b1716154000032903494", - 2: "022008b7a60cf8acd19181cf959a12f8acd19181cf951af8acd19181cf15f8acd191810f01020026050212340c68656c6c6f20776f726c6421208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d024242047465737400320032013202320328292929292a0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e0102222324252104082209240a220b230c240d250e230f23102311231223132314181b1c2b171615400003290349483403350222231d4a484848482a50512a63222352410003420000432105602105612105270463484821052b62482b642b65484821052b2106662b21056721072b682b692107210570004848210771004848361c0037001a0031183119311b311d311e311f3120210721051e312131223123312431253126312731283129312a312b312c312d312e312f", - 3: "032008b7a60cf8acd19181cf959a12f8acd19181cf951af8acd19181cf15f8acd191810f01020026050212340c68656c6c6f20776f726c6421208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d024242047465737400320032013202320328292929292a0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e0102222324252104082209240a220b230c240d250e230f23102311231223132314181b1c2b171615400003290349483403350222231d4a484848482a50512a63222352410003420000432105602105612105270463484821052b62482b642b65484821052b2106662b21056721072b682b692107210570004848210771004848361c0037001a0031183119311b311d311e311f3120210721051e312131223123312431253126312731283129312a312b312c312d312e312f4478222105531421055427042106552105082106564c4d4b02210538212106391c0081e80780046a6f686e", - 4: "042004010200b7a60c26040242420c68656c6c6f20776f726c6421208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d047465737400320032013202320380021234292929292a0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e01022581f8acd19181cf959a1281f8acd19181cf951a81f8acd19181cf1581f8acd191810f082209240a220b230c240d250e230f23102311231223132314181b1c28171615400003290349483403350222231d4a484848482a50512a632223524100034200004322602261222b634848222862482864286548482228236628226724286828692422700048482471004848361c0037001a0031183119311b311d311e311f312024221e312131223123312431253126312731283129312a312b312c312d312e312f44782522531422542b2355220823564c4d4b0222382123391c0081e80780046a6f686e2281d00f24231f880003420001892223902291922394239593a0a1a2a3a4a5a6a7a8a9aaabacadae23af3a00003b003c003d8164", - 5: "052004010002b7a60c26050242420c68656c6c6f20776f726c6421070123456789abcd208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d047465737400320032013202320380021234292929292b0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e01022581f8acd19181cf959a1281f8acd19181cf951a81f8acd19181cf1581f8acd191810f082209240a220b230c240d250e230f23102311231223132314181b1c28171615400003290349483403350222231d4a484848482b50512a632223524100034200004322602261222704634848222862482864286548482228246628226723286828692322700048482371004848361c0037001a0031183119311b311d311e311f312023221e312131223123312431253126312731283129312a312b312c312d312e312f447825225314225427042455220824564c4d4b0222382124391c0081e80780046a6f686e2281d00f23241f880003420001892224902291922494249593a0a1a2a3a4a5a6a7a8a9aaabacadae24af3a00003b003c003d816472064e014f012a57000823810858235b235a2359b03139330039b1b200b322c01a23c1001a2323c21a23c3233e233f8120af06002a494905002a49490700b53a03", + 1: "012008b7a60cf8acd19181cf959a12f8acd19181cf951af8acd19181cf15f8acd191810f01020026050212340c68656c6c6f20776f726c6421208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d024242047465737400320032013202320328292929292a0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e0102222324252104082209240a220b230c240d250e230f2310231123122313231418191a1b1c2b1716154000032903494", + 2: "022008b7a60cf8acd19181cf959a12f8acd19181cf951af8acd19181cf15f8acd191810f01020026050212340c68656c6c6f20776f726c6421208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d024242047465737400320032013202320328292929292a0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e0102222324252104082209240a220b230c240d250e230f2310231123122313231418191a1b1c2b171615400003290349483403350222231d4a484848482a50512a63222352410003420000432105602105612105270463484821052b62482b642b65484821052b2106662b21056721072b682b692107210570004848210771004848361c0037001a0031183119311b311d311e311f3120210721051e312131223123312431253126312731283129312a312b312c312d312e312f", + 3: "032008b7a60cf8acd19181cf959a12f8acd19181cf951af8acd19181cf15f8acd191810f01020026050212340c68656c6c6f20776f726c6421208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d024242047465737400320032013202320328292929292a0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e0102222324252104082209240a220b230c240d250e230f2310231123122313231418191a1b1c2b171615400003290349483403350222231d4a484848482a50512a63222352410003420000432105602105612105270463484821052b62482b642b65484821052b2106662b21056721072b682b692107210570004848210771004848361c0037001a0031183119311b311d311e311f3120210721051e312131223123312431253126312731283129312a312b312c312d312e312f4478222105531421055427042106552105082106564c4d4b02210538212106391c0081e80780046a6f686e", + 4: "042004010200b7a60c26040242420c68656c6c6f20776f726c6421208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d047465737400320032013202320380021234292929292a0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e01022581f8acd19181cf959a1281f8acd19181cf951a81f8acd19181cf1581f8acd191810f082209240a220b230c240d250e230f2310231123122313231418191a1b1c28171615400003290349483403350222231d4a484848482a50512a632223524100034200004322602261222b634848222862482864286548482228236628226724286828692422700048482471004848361c0037001a0031183119311b311d311e311f312024221e312131223123312431253126312731283129312a312b312c312d312e312f44782522531422542b2355220823564c4d4b0222382123391c0081e80780046a6f686e2281d00f24231f880003420001892223902291922394239593a0a1a2a3a4a5a6a7a8a9aaabacadae23af3a00003b003c003d8164", + 5: "052004010002b7a60c26050242420c68656c6c6f20776f726c6421070123456789abcd208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d047465737400320032013202320380021234292929292b0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e01022581f8acd19181cf959a1281f8acd19181cf951a81f8acd19181cf1581f8acd191810f082209240a220b230c240d250e230f2310231123122313231418191a1b1c28171615400003290349483403350222231d4a484848482b50512a632223524100034200004322602261222704634848222862482864286548482228246628226723286828692322700048482371004848361c0037001a0031183119311b311d311e311f312023221e312131223123312431253126312731283129312a312b312c312d312e312f447825225314225427042455220824564c4d4b0222382124391c0081e80780046a6f686e2281d00f23241f880003420001892224902291922494249593a0a1a2a3a4a5a6a7a8a9aaabacadae24af3a00003b003c003d816472064e014f012a57000823810858235b235a2359b03139330039b1b200b322c01a23c1001a2323c21a23c3233e233f8120af06002a494905002a49490700b400b53a03", 6: "06" + v6Compiled, 7: "07" + v7Compiled, 8: "08" + v8Compiled, @@ -469,8 +474,10 @@ func TestAssemble(t *testing.T) { for v := uint64(2); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { for _, spec := range OpSpecs { - // Make sure our nonsense covers the ops - if !strings.Contains(nonsense[v], spec.Name) && + // Make sure our nonsense covers the ops. + hasOp, err := regexp.MatchString("\\s"+regexp.QuoteMeta(spec.Name)+"\\s", nonsense[v]) + require.NoError(t, err) + if !hasOp && !pseudoOp(spec.Name) && spec.Version <= v { t.Errorf("v%d nonsense test should contain op %v", v, spec.Name) } @@ -481,6 +488,7 @@ func TestAssemble(t *testing.T) { // time. we must assemble to the same bytes // this month that we did last month. expectedBytes, _ := hex.DecodeString(compiled[v]) + require.NotEmpty(t, expectedBytes) // the hex is for convenience if the program has been changed. the // hex string can be copy pasted back in as a new expected result. require.Equal(t, expectedBytes, ops.Program, hex.EncodeToString(ops.Program)) @@ -2460,6 +2468,29 @@ func TestDigAsm(t *testing.T) { } +func TestBuryAsm(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + testProg(t, "int 1; bury; +", AssemblerMaxVersion, Expect{1, "bury expects 1 immediate..."}) + testProg(t, "int 1; bury junk; +", AssemblerMaxVersion, Expect{1, "bury unable to parse..."}) + + testProg(t, "int 1; byte 0x1234; int 2; bury 1; +", AssemblerMaxVersion) // the 2 replaces the byte string + testProg(t, "int 2; int 2; byte 0x1234; bury 1; +", AssemblerMaxVersion, + Expect{1, "+ arg 1..."}) + testProg(t, "byte 0x32; byte 0x1234; int 2; bury 3; +", AssemblerMaxVersion, + Expect{1, "bury 3 expects 4..."}) + testProg(t, "int 1; byte 0x1234; int 2; bury 12; +", AssemblerMaxVersion, + Expect{1, "bury 12 expects 13..."}) + + // We do not lose track of the ints between ToS and bury index + testProg(t, "int 0; int 1; int 2; int 4; bury 3; concat", AssemblerMaxVersion, + Expect{1, "concat arg 1 wanted type []byte..."}) + + // Even when we are burying into unknown (seems repetitive, but is an easy bug) + testProg(t, "int 0; int 0; b LABEL; LABEL: int 1; int 2; int 4; bury 4; concat", AssemblerMaxVersion, + Expect{1, "concat arg 1 wanted type []byte..."}) +} + func TestEqualsTypeCheck(t *testing.T) { partitiontest.PartitionTest(t) t.Parallel() @@ -2521,6 +2552,31 @@ func TestScratchTypeCheck(t *testing.T) { testProg(t, "callsub A; int 1; store 0; load 0; btoi; return; A: retsub", AssemblerMaxVersion, Expect{1, "btoi arg 0..."}) } +// TestProtoAsm confirms that the assembler will yell at you if you are +// clearly dipping into the arguments when using `proto`. You should be using +// `frame_dig`. +func TestProtoAsm(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + testProg(t, "proto 0 0", AssemblerMaxVersion, Expect{1, "proto must be unreachable..."}) + testProg(t, notrack("proto 0 0"), AssemblerMaxVersion) + testProg(t, "b a; int 1; a: proto 0 0", AssemblerMaxVersion) // we could flag a `b` to `proto` + + testProg(t, ` + int 10 + int 20 + callsub main + int 1 + return +main: + proto 2 1 + + // This consumes the top arg. We complain. + dup; dup // Even though the dup;dup restores it, so it _evals_ fine. + retsub +`, AssemblerMaxVersion) + +} + func TestCoverAsm(t *testing.T) { partitiontest.PartitionTest(t) t.Parallel() @@ -2529,6 +2585,7 @@ func TestCoverAsm(t *testing.T) { testProg(t, `int 4; byte "john"; int 5; cover 2; +`, AssemblerMaxVersion, Expect{1, "+ arg 1..."}) testProg(t, `int 4; cover junk`, AssemblerMaxVersion, Expect{1, "cover unable to parse n ..."}) + testProg(t, notrack(`int 4; int 5; cover 0`), AssemblerMaxVersion) } func TestUncoverAsm(t *testing.T) { diff --git a/data/transactions/logic/debugger.go b/data/transactions/logic/debugger.go index cae8f7111f..a2a0453c48 100644 --- a/data/transactions/logic/debugger.go +++ b/data/transactions/logic/debugger.go @@ -202,12 +202,12 @@ func valueDeltaToValueDelta(vd *basics.ValueDelta) basics.ValueDelta { // parseCallStack initializes an array of CallFrame objects from the raw // callstack. -func (d *DebugState) parseCallstack(callstack []int) []CallFrame { +func (d *DebugState) parseCallstack(callstack []frame) []CallFrame { callFrames := make([]CallFrame, 0) lines := strings.Split(d.Disassembly, "\n") - for _, pc := range callstack { + for _, fr := range callstack { // The callsub is pc - 3 from the callstack pc - callsubLineNum := d.PCToLine(pc - 3) + callsubLineNum := d.PCToLine(fr.retpc - 3) callSubLine := strings.Fields(lines[callsubLineNum]) label := "" if callSubLine[0] == "callsub" { diff --git a/data/transactions/logic/debugger_test.go b/data/transactions/logic/debugger_test.go index 060b953fcd..f33e8ae5cc 100644 --- a/data/transactions/logic/debugger_test.go +++ b/data/transactions/logic/debugger_test.go @@ -202,7 +202,7 @@ func TestParseCallstack(t *testing.T) { Disassembly: testCallStackProgram, PCOffset: []PCOffset{{PC: 1, Offset: 18}, {PC: 4, Offset: 30}, {PC: 7, Offset: 45}, {PC: 8, Offset: 65}, {PC: 11, Offset: 88}}, } - callstack := []int{4, 8} + callstack := []frame{{retpc: 4}, {retpc: 8}} cfs := dState.parseCallstack(callstack) require.Equal(t, expectedCallFrames, cfs) diff --git a/data/transactions/logic/doc.go b/data/transactions/logic/doc.go index bfcf927f0a..a12149bcf3 100644 --- a/data/transactions/logic/doc.go +++ b/data/transactions/logic/doc.go @@ -126,7 +126,9 @@ var opDocByName = map[string]string{ "pop": "discard A", "dup": "duplicate A", "dup2": "duplicate A and B", + "dupn": "duplicate A, N times", "dig": "Nth value from the top of the stack. dig 0 is equivalent to dup", + "bury": "Replace the Nth value from the top of the stack. bury 0 fails.", "cover": "remove top of stack, and place it deeper in the stack such that N elements are above it. Fails if stack depth <= N.", "uncover": "remove the value at depth N in the stack and shift above items down so the Nth deep value is on top of the stack. Fails if stack depth <= N.", "swap": "swaps A and B on stack", @@ -195,6 +197,11 @@ var opDocByName = map[string]string{ "block": "field F of block A. Fail unless A falls between txn.LastValid-1002 and txn.FirstValid (exclusive)", "switch": "branch to the Ath label. Continue at following instruction if index A exceeds the number of labels.", + + "proto": "Prepare top call frame for a retsub that will assume A args and R return values.", + "frame_dig": "Nth (signed) value from the frame pointer.", + "frame_bury": "Replace the Nth (signed) value from the frame pointer in the stack", + "popn": "Remove N values from the top of the stack", } // OpDoc returns a description of the op @@ -238,6 +245,7 @@ var opcodeImmediateNotes = map[string]string{ "extract": "{uint8 start position} {uint8 length}", "replace2": "{uint8 start position}", "dig": "{uint8 depth}", + "bury": "{uint8 depth}", "cover": "{uint8 depth}", "uncover": "{uint8 depth}", @@ -259,12 +267,18 @@ var opcodeImmediateNotes = map[string]string{ "ecdsa_pk_recover": "{uint8 curve index}", "base64_decode": "{uint8 encoding index}", - "json_ref": "{string return type}", + "json_ref": "{uint8 return type}", "vrf_verify": "{uint8 parameters index}", "block": "{uint8 block field}", "switch": "{uint8 branch count} [{int16 branch offset, big-endian}, ...]", + + "proto": "{uint8 arguments} {uint8 return values}", + "frame_dig": "{int8 frame slot}", + "frame_bury": "{int8 frame slot}", + "popn": "{uint8 stack depth}", + "dupn": "{uint8 copy count}", } // OpImmediateNote returns a short string about immediate data which follows the op byte @@ -285,8 +299,8 @@ var opDocExtras = map[string]string{ "bnz": "The `bnz` instruction opcode 0x40 is followed by two immediate data bytes which are a high byte first and low byte second which together form a 16 bit offset which the instruction may branch to. For a bnz instruction at `pc`, if the last element of the stack is not zero then branch to instruction at `pc + 3 + N`, else proceed to next instruction at `pc + 3`. Branch targets must be aligned instructions. (e.g. Branching to the second byte of a 2 byte op will be rejected.) Starting at v4, the offset is treated as a signed 16 bit integer allowing for backward branches and looping. In prior version (v1 to v3), branch offsets are limited to forward branches only, 0-0x7fff.\n\nAt v2 it became allowed to branch to the end of the program exactly after the last instruction: bnz to byte N (with 0-indexing) was illegal for a TEAL program with N bytes before v2, and is legal after it. This change eliminates the need for a last instruction of no-op as a branch target at the end. (Branching beyond the end--in other words, to a byte larger than N--is still illegal and will cause the program to fail.)", "bz": "See `bnz` for details on how branches work. `bz` inverts the behavior of `bnz`.", "b": "See `bnz` for details on how branches work. `b` always jumps to the offset.", - "callsub": "The call stack is separate from the data stack. Only `callsub` and `retsub` manipulate it.", - "retsub": "The call stack is separate from the data stack. Only `callsub` and `retsub` manipulate it.", + "callsub": "The call stack is separate from the data stack. Only `callsub`, `retsub`, and `proto` manipulate it.", + "retsub": "If the current frame was prepared by `proto A R`, `retsub` will remove the 'A' arguments from the stack, move the `R` return values down, and pop any stack locations above the relocated return values.", "intcblock": "`intcblock` loads following program bytes into an array of integer constants in the evaluator. These integer constants can be referred to by `intc` and `intc_*` which will push the value onto the stack. Subsequent calls to `intcblock` reset and replace the integer constants available to the script.", "bytecblock": "`bytecblock` loads the following program bytes into an array of byte-array constants in the evaluator. These constants can be referred to by `bytec` and `bytec_*` which will push the value onto the stack. Subsequent calls to `bytecblock` reset and replace the bytes constants available to the script.", "*": "Overflow is an error condition which halts execution and fails the transaction. Full precision is available from `mulw`.", @@ -327,6 +341,7 @@ var opDocExtras = map[string]string{ "itxn_submit": "`itxn_submit` resets the current transaction so that it can not be resubmitted. A new `itxn_begin` is required to prepare another inner transaction.", "base64_decode": "*Warning*: Usage should be restricted to very rare use cases. In almost all cases, smart contracts should directly handle non-encoded byte-strings. This opcode should only be used in cases where base64 is the only available option, e.g. interoperability with a third-party that only signs base64 strings.\n\n Decodes A using the base64 encoding E. Specify the encoding with an immediate arg either as URL and Filename Safe (`URLEncoding`) or Standard (`StdEncoding`). See [RFC 4648 sections 4 and 5](https://rfc-editor.org/rfc/rfc4648.html#section-4). It is assumed that the encoding ends with the exact number of `=` padding characters as required by the RFC. When padding occurs, any unused pad bits in the encoding must be set to zero or the decoding will fail. The special cases of `\\n` and `\\r` are allowed but completely ignored. An error will result when attempting to decode a string with a character that is not in the encoding alphabet or not one of `=`, `\\r`, or `\\n`.", "json_ref": "*Warning*: Usage should be restricted to very rare use cases, as JSON decoding is expensive and quite limited. In addition, JSON objects are large and not optimized for size.\n\nAlmost all smart contracts should use simpler and smaller methods (such as the [ABI](https://arc.algorand.foundation/ARCs/arc-0004). This opcode should only be used in cases where JSON is only available option, e.g. when a third-party only signs JSON.", + "proto": "Fails unless the last instruction executed was a `callsub`.", } // OpDocExtra returns extra documentation text about an op @@ -343,7 +358,7 @@ var OpGroups = map[string][]string{ "Byte Array Arithmetic": {"b+", "b-", "b/", "b*", "b<", "b>", "b<=", "b>=", "b==", "b!=", "b%", "bsqrt"}, "Byte Array Logic": {"b|", "b&", "b^", "b~"}, "Loading Values": {"intcblock", "intc", "intc_0", "intc_1", "intc_2", "intc_3", "pushint", "bytecblock", "bytec", "bytec_0", "bytec_1", "bytec_2", "bytec_3", "pushbytes", "bzero", "arg", "arg_0", "arg_1", "arg_2", "arg_3", "args", "txn", "gtxn", "txna", "txnas", "gtxna", "gtxnas", "gtxns", "gtxnsa", "gtxnsas", "global", "load", "loads", "store", "stores", "gload", "gloads", "gloadss", "gaid", "gaids"}, - "Flow Control": {"err", "bnz", "bz", "b", "return", "pop", "dup", "dup2", "dig", "cover", "uncover", "swap", "select", "assert", "callsub", "retsub", "switch"}, + "Flow Control": {"err", "bnz", "bz", "b", "return", "pop", "popn", "dup", "dup2", "dupn", "dig", "bury", "cover", "uncover", "frame_dig", "frame_bury", "swap", "select", "assert", "callsub", "proto", "retsub", "switch"}, "State Access": {"balance", "min_balance", "app_opted_in", "app_local_get", "app_local_get_ex", "app_global_get", "app_global_get_ex", "app_local_put", "app_global_put", "app_local_del", "app_global_del", "asset_holding_get", "asset_params_get", "app_params_get", "acct_params_get", "log", "block"}, "Inner Transactions": {"itxn_begin", "itxn_next", "itxn_field", "itxn_submit", "itxn", "itxna", "itxnas", "gitxn", "gitxna", "gitxnas"}, } diff --git a/data/transactions/logic/doc_test.go b/data/transactions/logic/doc_test.go index f270d91629..e95293106d 100644 --- a/data/transactions/logic/doc_test.go +++ b/data/transactions/logic/doc_test.go @@ -105,14 +105,29 @@ func TestAllImmediatesDocumented(t *testing.T) { partitiontest.PartitionTest(t) for _, op := range OpSpecs { - count := len(op.OpDetails.Immediates) + count := len(op.Immediates) note := OpImmediateNote(op.Name) - if count == 1 && op.OpDetails.Immediates[0].kind >= immBytes { + if count == 1 && op.Immediates[0].kind >= immBytes { // More elaborate than can be checked by easy count. assert.NotEmpty(t, note) continue } assert.Equal(t, count, strings.Count(note, "{"), "opcodeImmediateNotes for %s is wrong", op.Name) + assert.Equal(t, count, strings.Count(note, "}"), "opcodeImmediateNotes for %s is wrong", op.Name) + for _, imm := range op.Immediates { + switch imm.kind { + case immByte: + require.True(t, strings.HasPrefix(note, "{uint8 "), "%v %v", op.Name, note) + case immInt8: + require.True(t, strings.HasPrefix(note, "{int8 "), "%v %v", op.Name, note) + case immLabel: + require.True(t, strings.HasPrefix(note, "{int16 "), "%v %v", op.Name, note) + case immInt: + require.True(t, strings.HasPrefix(note, "{varuint "), "%v %v", op.Name, note) + } + close := strings.Index(note, "}") + note = strings.TrimPrefix(note[close+1:], " ") + } } } diff --git a/data/transactions/logic/eval.go b/data/transactions/logic/eval.go index 5e4cf4c377..1e78d69605 100644 --- a/data/transactions/logic/eval.go +++ b/data/transactions/logic/eval.go @@ -472,6 +472,15 @@ func (ep *EvalParams) RecordAD(gi int, ad transactions.ApplyData) { } } +type frame struct { + retpc int + height int + + clear bool // perform "shift and clear" in retsub + args int + returns int +} + type scratchSpace [256]stackValue // EvalContext is the execution context of AVM bytecode. It contains the full @@ -497,8 +506,9 @@ type EvalContext struct { // keeping the running changes, the debugger can be changed to display them // as the app runs. - stack []stackValue - callstack []int + stack []stackValue + callstack []frame + fromCallsub bool appID basics.AppIndex program []byte @@ -980,7 +990,7 @@ func (cx *EvalContext) step() error { preheight := len(cx.stack) err := spec.op(cx) - if err == nil { + if err == nil && !spec.trusted { postheight := len(cx.stack) if postheight-preheight != len(spec.Return.Types)-len(spec.Arg.Types) && !spec.AlwaysExits() { return fmt.Errorf("%s changed stack height improperly %d != %d", @@ -2109,9 +2119,26 @@ func opSwitch(cx *EvalContext) error { return nil } +const protoByte = 0x8a + func opCallSub(cx *EvalContext) error { - cx.callstack = append(cx.callstack, cx.pc+3) - return opB(cx) + cx.callstack = append(cx.callstack, frame{ + retpc: cx.pc + 3, // retpc is pc _after_ the callsub + height: len(cx.stack), + }) + err := opB(cx) + + /* We only set fromCallSub if we know we're jumping to a proto. In opProto, + we confirm we came directly from callsub by checking (and resetting) the + flag. This is really a little handshake between callsub and proto. Done + this way, we don't have to waste time clearing the fromCallsub flag in + every instruction, only in proto since we know we're going there next. + */ + + if cx.nextpc < len(cx.program) && cx.program[cx.nextpc] == protoByte { + cx.fromCallsub = true + } + return err } func opRetSub(cx *EvalContext) error { @@ -2119,9 +2146,26 @@ func opRetSub(cx *EvalContext) error { if top < 0 { return errors.New("retsub with empty callstack") } - target := cx.callstack[top] + frame := cx.callstack[top] + if frame.clear { // A `proto` was issued in the subroutine, so retsub cleans up. + expect := frame.height + frame.returns + if len(cx.stack) < expect { // Check general error case first, only diffentiate when error is assured + switch { + case len(cx.stack) < frame.height: + return fmt.Errorf("retsub executed with stack below frame. Did you pop args?") + case len(cx.stack) == frame.height: + return fmt.Errorf("retsub executed with no return values on stack. proto declared %d", frame.returns) + default: + return fmt.Errorf("retsub executed with %d return values on stack. proto declared %d", + len(cx.stack)-frame.height, frame.returns) + } + } + argstart := frame.height - frame.args + copy(cx.stack[argstart:], cx.stack[frame.height:expect]) + cx.stack = cx.stack[:argstart+frame.returns] + } cx.callstack = cx.callstack[:top] - cx.nextpc = target + cx.nextpc = frame.retpc return nil } diff --git a/data/transactions/logic/evalStateful_test.go b/data/transactions/logic/evalStateful_test.go index 1652702dfb..f5c87abb3c 100644 --- a/data/transactions/logic/evalStateful_test.go +++ b/data/transactions/logic/evalStateful_test.go @@ -2374,6 +2374,9 @@ func TestReturnTypes(t *testing.T) { "json_ref": `: byte "{\"k\": 7}"; byte "k"; json_ref JSONUint64`, "block": "block BlkSeed", + + "proto": "callsub p; p: proto 0 3", + "bury": ": int 1; int 2; int 3; bury 2; pop; pop;", } /* Make sure the specialCmd tests the opcode in question */ @@ -2399,6 +2402,9 @@ func TestReturnTypes(t *testing.T) { "bn256_add": true, "bn256_scalar_mul": true, "bn256_pairing": true, + + "frame_dig": true, // would need a "proto" subroutine + "frame_bury": true, // would need a "proto" subroutine } byName := OpsByName[LogicVersion] @@ -2408,7 +2414,7 @@ func TestReturnTypes(t *testing.T) { if (m & spec.Modes) == 0 { continue } - if skipCmd[name] { + if skipCmd[name] || spec.trusted { continue } t.Run(fmt.Sprintf("mode=%s,opcode=%s", m, name), func(t *testing.T) { @@ -2426,6 +2432,8 @@ func TestReturnTypes(t *testing.T) { switch imm.kind { case immByte: cmd += " 0" + case immInt8: + cmd += " -2" case immInt: cmd += " 10" case immInts: diff --git a/data/transactions/logic/eval_test.go b/data/transactions/logic/eval_test.go index 64c6c04808..130167bf02 100644 --- a/data/transactions/logic/eval_test.go +++ b/data/transactions/logic/eval_test.go @@ -1077,7 +1077,7 @@ const globalV7TestProgram = globalV6TestProgram + ` ` const globalV8TestProgram = globalV7TestProgram + ` -// No new globals in v7 +// No new globals in v8 ` func TestGlobal(t *testing.T) { @@ -1575,7 +1575,8 @@ assert int 1 ` -const testTxnProgramTextV8 = testTxnProgramTextV7 +const testTxnProgramTextV8 = testTxnProgramTextV7 + ` +` func makeSampleTxn() transactions.SignedTxn { var txn transactions.SignedTxn @@ -3597,6 +3598,51 @@ func BenchmarkUintCmp(b *testing.B) { }) } } + +func BenchmarkDupnProto(b *testing.B) { + benches := [][]string{ + {"dupn1", ` + b main +f: + proto 1 1 + byte "repeat" + dupn 0 // return 1 string + retsub +main: + int 777; dupn 0; // start with 1 int on stack +`, "callsub f", "len"}, + {"dupn10", ` + b main +f: + proto 10 10 + byte "repeat" + dupn 9 // return 10 strings + retsub +main: + int 777; dupn 9; // start with 10 ints on stack +`, "callsub f", strings.Repeat("pop;", 9) + "len"}, + {"dupn100", ` + b main +f: + proto 100 100 + byte "repeat" + dupn 99 // return 100 strings + retsub +main: + int 777; dupn 99; // start with 100 ints on stack +`, "callsub f", strings.Repeat("pop;", 99) + "len"}, + {"dp1", "int 777", "dupn 1; popn 1", ""}, + {"dp10", "int 777", "dupn 10; popn 10", ""}, + {"dp100", "int 777", "dupn 100; popn 100", ""}, + } + for _, bench := range benches { + b.Run(bench[0], func(b *testing.B) { + b.ReportAllocs() + benchmarkOperation(b, bench[1], bench[2], bench[3]) + }) + } +} + func BenchmarkByteLogic(b *testing.B) { benches := [][]string{ {"b&", "", "byte 0x012345678901feab; byte 0x01ffffffffffffff; b&; pop", "int 1"}, @@ -4204,7 +4250,7 @@ func notrack(program string) string { return pragma + program } -type evalTester func(pass bool, err error) bool +type evalTester func(t *testing.T, pass bool, err error) bool func testEvaluation(t *testing.T, program string, introduced uint64, tester evalTester) error { t.Helper() @@ -4234,7 +4280,7 @@ func testEvaluation(t *testing.T, program string, introduced uint64, tester eval require.NoError(t, err) ep = defaultEvalParamsWithVersion(&txn, lv) pass, err := EvalSignature(0, ep) - ok := tester(pass, err) + ok := tester(t, pass, err) if !ok { t.Log(ep.Trace.String()) t.Log(err) @@ -4254,22 +4300,36 @@ func testEvaluation(t *testing.T, program string, introduced uint64, tester eval func testAccepts(t *testing.T, program string, introduced uint64) { t.Helper() - testEvaluation(t, program, introduced, func(pass bool, err error) bool { + testEvaluation(t, program, introduced, func(t *testing.T, pass bool, err error) bool { return pass && err == nil }) } func testRejects(t *testing.T, program string, introduced uint64) { t.Helper() - testEvaluation(t, program, introduced, func(pass bool, err error) bool { + testEvaluation(t, program, introduced, func(t *testing.T, pass bool, err error) bool { // Returned False, but didn't panic return !pass && err == nil }) } -func testPanics(t *testing.T, program string, introduced uint64) error { +func testPanics(t *testing.T, program string, introduced uint64, pattern ...string) error { t.Helper() - return testEvaluation(t, program, introduced, func(pass bool, err error) bool { + return testEvaluation(t, program, introduced, func(t *testing.T, pass bool, err error) bool { + t.Helper() // TEAL panic! not just reject at exit - return !pass && err != nil + if pass { + return false + } + if err == nil { + t.Log("program rejected rather panicked") + return false + } + for _, p := range pattern { + if !strings.Contains(err.Error(), p) { + t.Log(err, "does not contain", p) + return false + } + } + return true }) } @@ -4389,6 +4449,29 @@ func TestDig(t *testing.T) { testPanics(t, notrack("int 3; int 2; int 1; dig 11; int 2; ==; return"), 3) } +func TestBury(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + // bury 0 panics + source := "int 3; int 2; int 7; bury 0; int 1; return" + testProg(t, source, 8, Expect{1, "bury 0 always fails"}) + testPanics(t, notrack("int 3; int 2; int 7; bury 0; int 1; return"), 8, "bury outside stack") + + // bury 1 pops the ToS and replaces the thing "1 down", which becomes the new ToS + testAccepts(t, "int 3; int 2; int 7; bury 1; int 7; ==; assert; int 3; ==", 8) + + // bury 2 + testAccepts(t, `int 3; int 2; int 7; + bury 2; + int 2; ==; assert + int 7; ==; +`, 8) + + // bury too deep + testPanics(t, notrack("int 3; int 2; int 7; bury 3; int 1; return"), 8, "bury outside stack") +} + func TestCover(t *testing.T) { partitiontest.PartitionTest(t) t.Parallel() diff --git a/data/transactions/logic/frames.go b/data/transactions/logic/frames.go new file mode 100644 index 0000000000..e145ac8fce --- /dev/null +++ b/data/transactions/logic/frames.go @@ -0,0 +1,128 @@ +// Copyright (C) 2019-2022 Algorand, Inc. +// This file is part of go-algorand +// +// go-algorand is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// go-algorand is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with go-algorand. If not, see . + +package logic + +import ( + "errors" + "fmt" +) + +func opProto(cx *EvalContext) error { + if !cx.fromCallsub { + return fmt.Errorf("proto was executed without a callsub") + } + cx.fromCallsub = false + nargs := int(cx.program[cx.pc+1]) + if nargs > len(cx.stack) { + return fmt.Errorf("callsub to proto that requires %d args with stack height %d", nargs, len(cx.stack)) + } + top := len(cx.callstack) - 1 + cx.callstack[top].clear = true + cx.callstack[top].args = nargs + cx.callstack[top].returns = int(cx.program[cx.pc+2]) + return nil +} + +func opFrameDig(cx *EvalContext) error { + i := int8(cx.program[cx.pc+1]) + + top := len(cx.callstack) - 1 + if top < 0 { + return errors.New("frame_dig with empty callstack") + } + + frame := cx.callstack[top] + // If proto was used, don't allow `frame_dig` to go below specified args + if frame.clear && -int(i) > frame.args { + return fmt.Errorf("frame_dig %d in sub with %d args", i, frame.args) + } + idx := frame.height + int(i) + if idx >= len(cx.stack) { + return errors.New("frame_dig above stack") + } + if idx < 0 { + return errors.New("frame_dig below stack") + } + + cx.stack = append(cx.stack, cx.stack[idx]) + return nil +} +func opFrameBury(cx *EvalContext) error { + last := len(cx.stack) - 1 // value + i := int8(cx.program[cx.pc+1]) + + top := len(cx.callstack) - 1 + if top < 0 { + return errors.New("frame_bury with empty callstack") + } + + frame := cx.callstack[top] + // If proto was used, don't allow `frame_bury` to go below specified args + if frame.clear && -int(i) > frame.args { + return fmt.Errorf("frame_bury %d in sub with %d args", i, frame.args) + } + idx := frame.height + int(i) + if idx >= last { + return errors.New("frame_bury above stack") + } + if idx < 0 { + return errors.New("frame_bury below stack") + } + cx.stack[idx] = cx.stack[last] + cx.stack = cx.stack[:last] // pop value + return nil +} +func opBury(cx *EvalContext) error { + last := len(cx.stack) - 1 // value + i := int(cx.program[cx.pc+1]) + + idx := last - i + if idx < 0 || idx == last { + return errors.New("bury outside stack") + } + cx.stack[idx] = cx.stack[last] + cx.stack = cx.stack[:last] // pop value + return nil +} + +func opPopN(cx *EvalContext) error { + n := cx.program[cx.pc+1] + top := len(cx.stack) - int(n) + if top < 0 { + return fmt.Errorf("popn %d while stack contains %d", n, len(cx.stack)) + } + cx.stack = cx.stack[:top] // pop value + return nil +} + +func opDupN(cx *EvalContext) error { + last := len(cx.stack) - 1 // value + + n := int(cx.program[cx.pc+1]) + finalLen := len(cx.stack) + n + if cap(cx.stack) < finalLen { + // Let's grow all at once, plus a little slack. + newStack := make([]stackValue, len(cx.stack), finalLen+4) + copy(newStack, cx.stack) + cx.stack = newStack + } + for i := 0; i < n; i++ { + // There will be enough room that this will not allocate + cx.stack = append(cx.stack, cx.stack[last]) + } + return nil +} diff --git a/data/transactions/logic/frames_test.go b/data/transactions/logic/frames_test.go new file mode 100644 index 0000000000..f1c1780c3e --- /dev/null +++ b/data/transactions/logic/frames_test.go @@ -0,0 +1,496 @@ +// Copyright (C) 2019-2022 Algorand, Inc. +// This file is part of go-algorand +// +// go-algorand is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// go-algorand is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with go-algorand. If not, see . + +package logic + +import ( + "testing" + + "github.com/algorand/go-algorand/test/partitiontest" +) + +const frameNonsense = ` + return // proto subs must appear in deadcode + double: + proto 1 2 + frame_dig -1 + int 2 + * + frame_bury 0 + retsub + pushint 2 + popn 1 + dupn 4 + bury 9 +` + +const frameCompiled = "438a01028bff240b8c00898102460147044509" + +func TestDupPopN(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + // These two are equally dumbs uses of popn, and should perhaps be banned + testAccepts(t, "int 1; popn 0", fpVersion) + testAccepts(t, "int 1; dup; popn 1;", fpVersion) + + testAccepts(t, "int 1; int 1; int 1; popn 2", fpVersion) + testAccepts(t, "int 1; int 0; popn 1", fpVersion) + testPanics(t, "int 1; int 0; popn 2", fpVersion) + testProg(t, "int 1; int 0; popn 3", LogicVersion, Expect{1, "popn 3 expects 3..."}) + testPanics(t, notrack("int 1; int 0; popn 3"), fpVersion) + + testAccepts(t, `int 7; dupn 250; dupn 250; dupn 250; dupn 249; + popn 250; popn 250; popn 250; popn 249; int 7; ==`, + fpVersion) + // We could detect this in assembler if we checked pgm.stack > maxStackDepth + // at each step. But it seems vanishly unlikely to have a detetectable + // instance of this bug in real code. + testPanics(t, `int 1; dupn 250; dupn 250; dupn 250; dupn 250 + popn 250; popn 250; popn 250; popn 250; !`, + fpVersion, "stack overflow") +} + +func TestDupPopNTyping(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + testProg(t, "int 8; dupn 2; +; pop", LogicVersion) + testProg(t, "int 8; dupn 2; concat; pop", LogicVersion, Expect{1, "...wanted type []byte..."}) + + testProg(t, "popn 1", LogicVersion, Expect{1, "...expects 1 stack argument..."}) +} + +func TestSimpleFrame(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + testAccepts(t, ` + int 3 + int 4 + callsub hyp + int 5 + == + return + hyp: + proto 2 1 + dupn 1 // room for the return value + frame_dig -1 + frame_dig -1 + * + frame_dig -2 + frame_dig -2 + * + + + sqrt + frame_bury 0 // place return value + retsub +`, fpVersion) +} + +func TestProtoChecks(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + // We normally report a non-deadcode `proto` at assembly time. But it still + // must fail at evaluation. + testPanics(t, notrack("proto 0 0; int 1"), fpVersion, "proto was executed without a callsub") + testAccepts(t, "callsub a; a: proto 0 0; int 1", fpVersion) + + // the assembler could detect this, since we know stack height, but it's + // rare to KNOW the height, and hard to get the knowledge to the right place + testPanics(t, ` + callsub toodeep +toodeep: + proto 10 1 + int 1 + return +`, fpVersion, "callsub to proto that requires 10 args") + + // the assembler could detect this, since sub is one basic block + testPanics(t, ` + int 5; int 10; callsub eatsargs + int 1; return +eatsargs: + proto 2 1 + + + retsub +`, fpVersion, "retsub executed with stack below frame") + + // the assembler could detect this, since sub is one basic block + testPanics(t, ` + int 5; int 10; callsub donothing + int 1; return +donothing: // does not leave return value above args + proto 2 1 + retsub +`, fpVersion, "retsub executed with no return values on stack") + + // the assembler could detect this, since sub is one basic block + testPanics(t, ` + int 5; int 10; callsub only1 + int 1; return +only1: // leaves only 1 return val + proto 2 2 + dup2; + + retsub +`, fpVersion, "retsub executed with 1 return values on stack") + + testAccepts(t, ` + int 5; int 10; callsub fine + int 1; return +fine: + proto 2 2 + dup2 + retsub +`, fpVersion) + + testAccepts(t, ` + int 5; int 10; callsub extra + int 1; return +extra: + proto 2 2 + dup2; dup2 + retsub +`, fpVersion) + + // the assembler could potentially complain about the stack going below fp, + // since the sub is one basic block. + testAccepts(t, ` + int 10 + int 20 + callsub main + int 1; return +main: + proto 2 1 + + // This consumes the top arg. We could complain in assembly if checked stack height against pgm.fp + dup; dup // But the dup;dup restores it, so it _evals_ fine. + retsub +`, AssemblerMaxVersion) + +} + +func TestVoidSub(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + testAccepts(t, ` + b main + a: proto 0 0 + int 4 // junk local should get cleared + retsub + main: callsub a + int 1 // would fail because of two stack items unless 4 cleared +`, fpVersion) + + testPanics(t, ` + b main + a: int 4 // junk local should not get cleared (no "proto") + retsub + main: callsub a + int 1 // fails because two items on stack +`, 4) // test back to retsub introduction +} + +func TestForgetReturn(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + testAccepts(t, ` + b main + a: proto 0 1 + int 5 // Just placing on stack is a fine way to return + retsub + main: callsub a + int 5 + == +`, fpVersion) + + testPanics(t, ` + b main + a: proto 0 1 + // Oops. No return value + retsub + main: callsub a + ! +`, fpVersion, "retsub executed with no return values on stack") + + testPanics(t, ` + b main + a: proto 0 3 + int 1; int 2 // only 2. need 3 + retsub + main: callsub a + ! +`, fpVersion, "retsub executed with 2 return values on stack") + + // Extra is fine. They are "locals", and they are cleared + testAccepts(t, ` + b main + a: proto 0 3 + int 7; dupn 3 // height grows by 4. only needed 3 + retsub + main: callsub a // returns 3 7s + +; +; int 21; == +`, fpVersion) +} + +func TestFrameAccess(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + testAccepts(t, ` + b main + add: proto 2 1 + frame_dig -1 + frame_dig -2 + + + retsub + main: int 8 + int 2 + callsub add + int 10 + == +`, fpVersion) + + testAccepts(t, ` + b main + ijsum: + proto 2 1 + int 0; int 0 // room for sum and one "local", a loop variable + + frame_dig -2 // first arg + frame_bury 1 // initialize loop var + loop: + // test for loop exit + frame_dig 1 // loop var + frame_dig -1 // second arg + > + bnz break + + // add loop var into sum + frame_dig 1 + frame_dig 0 // the sum, to be returned + + + frame_bury 0 + + // inc the loop var + frame_dig 1 + int 1 + + + frame_bury 1 + b loop + break: + retsub // sum is sitting in frame_dig 0, which will end up ToS + + main: int 2 + int 8 + callsub ijsum + int 35 // 2+3+4+5+6+7+8 + == +`, fpVersion) + + testPanics(t, notrack(` + b main + add: proto 2 1 + frame_dig -1 + frame_dig -3 + + + retsub + main: int 8 + int 2 + callsub add + int 10 + == +`), fpVersion, "frame_dig -3 in sub with 2") + + testPanics(t, notrack(` + b main + add: proto 2 1 + frame_dig -1 + int 5 + frame_bury -3 + + + retsub + main: int 8 + int 2 + callsub add + int 10 + == +`), fpVersion, "frame_bury -3 in sub with 2") + + source := ` + b main + add: proto 2 1 + frame_dig 0 // return slot. but wasn't allocated + retsub + main: int 8 + int 2 + callsub add + int 1 + return +` + testProg(t, source, fpVersion, Expect{4, "frame_dig above stack"}) + testPanics(t, notrack(source), fpVersion, "frame_dig above stack") + + source = ` + b main + add: proto 2 1 + int 0; dupn 2 // allocate return slot plus two locals + frame_dig 3 // but look beyond + retsub + main: int 8 + int 2 + callsub add + int 1 + return +` + testProg(t, source, fpVersion, Expect{5, "frame_dig above stack"}) + testPanics(t, notrack(source), fpVersion, "frame_dig above stack") + + // Note that at the moment of frame_bury, the stack IS big enough, because + // the 4 would essentially be written over itself. But because frame_bury + // pops, we consider this to be beyond the stack. + source = ` + b main + add: proto 2 1 + int 0; dupn 2 // allocate return slot plus two locals + int 4 + frame_bury 3 // but put "beyond" + retsub + main: int 8 + int 2 + callsub add + int 1 + return +` + testProg(t, source, fpVersion, Expect{6, "frame_bury above stack"}) + testPanics(t, notrack(source), fpVersion, "frame_bury above stack") +} + +func TestFrameAccesAtStart(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + testPanics(t, "frame_dig 1", fpVersion, "frame_dig with empty callstack") + testPanics(t, "int 7; frame_bury 1", fpVersion, "frame_bury with empty callstack") +} + +func TestFrameAccessAboveStack(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + source := ` + int 1 + callsub main +main: + proto 1 1 + pop // argument popped + frame_dig -1 // but then frame_dig used to get at it +` + testProg(t, source, fpVersion, Expect{7, "frame_dig above stack"}) + testPanics(t, notrack(source), fpVersion, "frame_dig above stack") + + testAccepts(t, ` + int 2 + callsub main + int 1; ==; return +main: + proto 1 1 + int 7 + frame_dig 0; int 7; ==; + frame_bury 0; + retsub +`, fpVersion) + + // almost the same but try to use a "local" slot without pushing first + source = ` + int 2 + callsub main + int 1; ==; return +main: + proto 1 1 + int 7 + frame_dig 1; int 7; ==; + frame_bury 1; + retsub +` + testProg(t, source, fpVersion, Expect{8, "frame_dig above stack"}) + testPanics(t, notrack(source), fpVersion) +} + +func TestFrameAccessBelowStack(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + source := ` + int 1 + callsub main +main: + proto 1 1 + frame_dig -10 // digging down below arguments +` + testProg(t, source, fpVersion, Expect{6, "frame_dig -10 in sub with 1 arg..."}) + testPanics(t, notrack(source), fpVersion, "frame_dig -10 in sub with 1 arg") + + testPanics(t, ` + int 1 + callsub main +main: + frame_dig -10 // digging down below arguments +`, fpVersion, "frame_dig below stack") + + source = ` + int 1 + callsub main +main: + proto 1 15 + frame_bury -10 // burying down below arguments +` + testProg(t, source, fpVersion, Expect{6, "frame_bury -10 in sub with 1 arg..."}) + testPanics(t, notrack(source), fpVersion, "frame_bury -10 in sub with 1 arg") + + // Without `proto`, frame_bury can't be checked by assembler, but still panics + source = ` + int 1 + callsub main +main: + frame_bury -10 // burying down below arguments +` + testPanics(t, source, fpVersion, "frame_bury below stack") + +} + +// TestDirectDig is an example of using dig instead of frame_dig, notice that +// the offset needs to account for the added stack height of second call. +func TestDirectDig(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + source := ` + int 3 + int 5 + callsub double_both + + + int 16; ==; return +double_both: + proto 2 2 + dig 1; int 2; * // dig for first arg + dig 1; int 2; * // dig for second + retsub +` + testProg(t, source, fpVersion) + testAccepts(t, source, fpVersion) +} diff --git a/data/transactions/logic/langspec.json b/data/transactions/logic/langspec.json index 58f3ded24a..f4742f1b7f 100644 --- a/data/transactions/logic/langspec.json +++ b/data/transactions/logic/langspec.json @@ -1028,6 +1028,38 @@ "Flow Control" ] }, + { + "Opcode": 69, + "Name": "bury", + "Args": ".", + "Size": 2, + "Doc": "Replace the Nth value from the top of the stack. bury 0 fails.", + "ImmediateNote": "{uint8 depth}", + "Groups": [ + "Flow Control" + ] + }, + { + "Opcode": 70, + "Name": "popn", + "Size": 2, + "Doc": "Remove N values from the top of the stack", + "ImmediateNote": "{uint8 stack depth}", + "Groups": [ + "Flow Control" + ] + }, + { + "Opcode": 71, + "Name": "dupn", + "Args": ".", + "Size": 2, + "Doc": "duplicate A, N times", + "ImmediateNote": "{uint8 copy count}", + "Groups": [ + "Flow Control" + ] + }, { "Opcode": 72, "Name": "pop", @@ -1299,7 +1331,7 @@ "Size": 2, "Doc": "key B's value, of type R, from a [valid](jsonspec.md) utf-8 encoded json object A", "DocExtra": "*Warning*: Usage should be restricted to very rare use cases, as JSON decoding is expensive and quite limited. In addition, JSON objects are large and not optimized for size.\n\nAlmost all smart contracts should use simpler and smaller methods (such as the [ABI](https://arc.algorand.foundation/ARCs/arc-0004). This opcode should only be used in cases where JSON is only available option, e.g. when a third-party only signs JSON.", - "ImmediateNote": "{string return type}", + "ImmediateNote": "{uint8 return type}", "Groups": [ "Byte Array Manipulation" ] @@ -1560,7 +1592,7 @@ "Name": "callsub", "Size": 3, "Doc": "branch unconditionally to TARGET, saving the next instruction on the call stack", - "DocExtra": "The call stack is separate from the data stack. Only `callsub` and `retsub` manipulate it.", + "DocExtra": "The call stack is separate from the data stack. Only `callsub`, `retsub`, and `proto` manipulate it.", "ImmediateNote": "{int16 branch offset, big-endian}", "Groups": [ "Flow Control" @@ -1571,13 +1603,46 @@ "Name": "retsub", "Size": 1, "Doc": "pop the top instruction from the call stack and branch to it", - "DocExtra": "The call stack is separate from the data stack. Only `callsub` and `retsub` manipulate it.", + "DocExtra": "If the current frame was prepared by `proto A R`, `retsub` will remove the 'A' arguments from the stack, move the `R` return values down, and pop any stack locations above the relocated return values.", "Groups": [ "Flow Control" ] }, { "Opcode": 138, + "Name": "proto", + "Size": 3, + "Doc": "Prepare top call frame for a retsub that will assume A args and R return values.", + "DocExtra": "Fails unless the last instruction executed was a `callsub`.", + "ImmediateNote": "{uint8 arguments} {uint8 return values}", + "Groups": [ + "Flow Control" + ] + }, + { + "Opcode": 139, + "Name": "frame_dig", + "Returns": ".", + "Size": 2, + "Doc": "Nth (signed) value from the frame pointer.", + "ImmediateNote": "{int8 frame slot}", + "Groups": [ + "Flow Control" + ] + }, + { + "Opcode": 140, + "Name": "frame_bury", + "Args": ".", + "Size": 2, + "Doc": "Replace the Nth (signed) value from the frame pointer in the stack", + "ImmediateNote": "{int8 frame slot}", + "Groups": [ + "Flow Control" + ] + }, + { + "Opcode": 141, "Name": "switch", "Args": "U", "Size": 0, diff --git a/data/transactions/logic/opcodes.go b/data/transactions/logic/opcodes.go index 1e3dcfc5b6..1efab93be3 100644 --- a/data/transactions/logic/opcodes.go +++ b/data/transactions/logic/opcodes.go @@ -64,6 +64,7 @@ const appAddressAvailableVersion = 7 const fidoVersion = 7 // base64, json, secp256r1 const randomnessVersion = 7 // vrf_verify, block +const fpVersion = 8 // changes for frame pointers and simpler function discipline // EXPERIMENTAL. These should be revisited whenever a new LogicSigVersion is // moved from vFuture to a new consensus version. If they remain unready, bump @@ -122,6 +123,8 @@ type OpDetails struct { FullCost linearCost // if non-zero, the cost of the opcode, no immediates matter Size int // if non-zero, the known size of opcode. if 0, check() determines. Immediates []immediate // details of each immediate arg to opcode + + trusted bool // if `trusted`, don't check stack effects. they are more complicated than simply checking the opcode prototype. } func (d *OpDetails) docCost(argLen int) string { @@ -168,11 +171,11 @@ func (d *OpDetails) Cost(program []byte, pc int, stack []stackValue) int { } func detDefault() OpDetails { - return OpDetails{asmDefault, nil, nil, modeAny, linearCost{baseCost: 1}, 1, nil} + return OpDetails{asmDefault, nil, nil, modeAny, linearCost{baseCost: 1}, 1, nil, false} } func constants(asm asmFunc, checker checkFunc, name string, kind immKind) OpDetails { - return OpDetails{asm, checker, nil, modeAny, linearCost{baseCost: 1}, 0, []immediate{imm(name, kind)}} + return OpDetails{asm, checker, nil, modeAny, linearCost{baseCost: 1}, 0, []immediate{imm(name, kind)}, false} } func detBranch() OpDetails { @@ -200,9 +203,8 @@ func assembler(asm asmFunc) OpDetails { } func (d OpDetails) assembler(asm asmFunc) OpDetails { - clone := d - clone.asm = asm - return clone + d.asm = asm + return d } func costly(cost int) OpDetails { @@ -212,9 +214,8 @@ func costly(cost int) OpDetails { } func (d OpDetails) costs(cost int) OpDetails { - clone := d - clone.FullCost = linearCost{baseCost: cost} - return clone + d.FullCost = linearCost{baseCost: cost} + return d } func only(m runMode) OpDetails { @@ -224,29 +225,41 @@ func only(m runMode) OpDetails { } func (d OpDetails) only(m runMode) OpDetails { - clone := d - clone.Modes = m - return clone + d.Modes = m + return d } func (d OpDetails) costByLength(initial, perChunk, chunkSize, depth int) OpDetails { - clone := d - clone.FullCost = costByLength(initial, perChunk, chunkSize, depth).FullCost - return clone + d.FullCost = costByLength(initial, perChunk, chunkSize, depth).FullCost + return d } func immediates(names ...string) OpDetails { + return immKinded(immByte, names...) +} + +func (d OpDetails) trust() OpDetails { + d.trusted = true + return d +} + +func immKinded(kind immKind, names ...string) OpDetails { d := detDefault() d.Size = len(names) + 1 d.Immediates = make([]immediate, len(names)) for i, name := range names { - d.Immediates[i] = imm(name, immByte) + d.Immediates[i] = imm(name, kind) } return d } -func stacky(typer refineFunc, imms ...string) OpDetails { - d := immediates(imms...) +func typed(typer refineFunc) OpDetails { + d := detDefault() + d.refine = typer + return d +} + +func (d OpDetails) typed(typer refineFunc) OpDetails { d.refine = typer return d } @@ -292,6 +305,7 @@ type immKind byte const ( immByte immKind = iota + immInt8 immLabel immInt immBytes @@ -419,8 +433,8 @@ var OpSpecs = []OpSpec{ {0x0f, ">=", opGe, proto("ii:i"), 1, detDefault()}, {0x10, "&&", opAnd, proto("ii:i"), 1, detDefault()}, {0x11, "||", opOr, proto("ii:i"), 1, detDefault()}, - {0x12, "==", opEq, proto("aa:i"), 1, stacky(typeEquals)}, - {0x13, "!=", opNeq, proto("aa:i"), 1, stacky(typeEquals)}, + {0x12, "==", opEq, proto("aa:i"), 1, typed(typeEquals)}, + {0x13, "!=", opNeq, proto("aa:i"), 1, typed(typeEquals)}, {0x14, "!", opNot, proto("i:i"), 1, detDefault()}, {0x15, "len", opLen, proto("b:i"), 1, detDefault()}, {0x16, "itob", opItob, proto("i:b"), 1, detDefault()}, @@ -456,8 +470,8 @@ var OpSpecs = []OpSpec{ {0x31, "txn", opTxn, proto(":a"), 1, field("f", &TxnScalarFields)}, {0x32, "global", opGlobal, proto(":a"), 1, field("f", &GlobalFields)}, {0x33, "gtxn", opGtxn, proto(":a"), 1, immediates("t", "f").field("f", &TxnScalarFields)}, - {0x34, "load", opLoad, proto(":a"), 1, stacky(typeLoad, "i")}, - {0x35, "store", opStore, proto("a:"), 1, stacky(typeStore, "i")}, + {0x34, "load", opLoad, proto(":a"), 1, immediates("i").typed(typeLoad)}, + {0x35, "store", opStore, proto("a:"), 1, immediates("i").typed(typeStore)}, {0x36, "txna", opTxna, proto(":a"), 2, immediates("f", "i").field("f", &TxnArrayFields)}, {0x37, "gtxna", opGtxna, proto(":a"), 2, immediates("t", "f", "i").field("f", &TxnArrayFields)}, // Like gtxn, but gets txn index from stack, rather than immediate arg @@ -471,31 +485,32 @@ var OpSpecs = []OpSpec{ {0x3d, "gaids", opGaids, proto("i:i"), 4, only(modeApp)}, // Like load/store, but scratch slot taken from TOS instead of immediate - {0x3e, "loads", opLoads, proto("i:a"), 5, stacky(typeLoads)}, - {0x3f, "stores", opStores, proto("ia:"), 5, stacky(typeStores)}, + {0x3e, "loads", opLoads, proto("i:a"), 5, typed(typeLoads)}, + {0x3f, "stores", opStores, proto("ia:"), 5, typed(typeStores)}, {0x40, "bnz", opBnz, proto("i:"), 1, detBranch()}, {0x41, "bz", opBz, proto("i:"), 2, detBranch()}, {0x42, "b", opB, proto(":"), 2, detBranch()}, {0x43, "return", opReturn, proto("i:x"), 2, detDefault()}, {0x44, "assert", opAssert, proto("i:"), 3, detDefault()}, + {0x45, "bury", opBury, proto("a:"), fpVersion, immediates("n").typed(typeBury)}, + {0x46, "popn", opPopN, proto(":", "[N items]", ""), fpVersion, immediates("n").typed(typePopN).trust()}, + {0x47, "dupn", opDupN, proto("a:", "", "A, [N copies of A]"), fpVersion, immediates("n").typed(typeDupN).trust()}, {0x48, "pop", opPop, proto("a:"), 1, detDefault()}, - {0x49, "dup", opDup, proto("a:aa", "A, A"), 1, stacky(typeDup)}, - {0x4a, "dup2", opDup2, proto("aa:aaaa", "A, B, A, B"), 2, stacky(typeDupTwo)}, - // There must be at least one thing on the stack for dig, but - // it would be nice if we did better checking than that. - {0x4b, "dig", opDig, proto("a:aa", "A, [N items]", "A, [N items], A"), 3, stacky(typeDig, "n")}, - {0x4c, "swap", opSwap, proto("aa:aa", "B, A"), 3, stacky(typeSwap)}, - {0x4d, "select", opSelect, proto("aai:a", "A or B"), 3, stacky(typeSelect)}, - {0x4e, "cover", opCover, proto("a:a", "[N items], A", "A, [N items]"), 5, stacky(typeCover, "n")}, - {0x4f, "uncover", opUncover, proto("a:a", "A, [N items]", "[N items], A"), 5, stacky(typeUncover, "n")}, + {0x49, "dup", opDup, proto("a:aa", "A, A"), 1, typed(typeDup)}, + {0x4a, "dup2", opDup2, proto("aa:aaaa", "A, B, A, B"), 2, typed(typeDupTwo)}, + {0x4b, "dig", opDig, proto("a:aa", "A, [N items]", "A, [N items], A"), 3, immediates("n").typed(typeDig)}, + {0x4c, "swap", opSwap, proto("aa:aa", "B, A"), 3, typed(typeSwap)}, + {0x4d, "select", opSelect, proto("aai:a", "A or B"), 3, typed(typeSelect)}, + {0x4e, "cover", opCover, proto("a:a", "[N items], A", "A, [N items]"), 5, immediates("n").typed(typeCover)}, + {0x4f, "uncover", opUncover, proto("a:a", "A, [N items]", "[N items], A"), 5, immediates("n").typed(typeUncover)}, // byteslice processing / StringOps {0x50, "concat", opConcat, proto("bb:b"), 2, detDefault()}, {0x51, "substring", opSubstring, proto("b:b"), 2, immediates("s", "e").assembler(asmSubstring)}, {0x52, "substring3", opSubstring3, proto("bii:b"), 2, detDefault()}, {0x53, "getbit", opGetBit, proto("ai:i"), 3, detDefault()}, - {0x54, "setbit", opSetBit, proto("aii:a"), 3, stacky(typeSetBit)}, + {0x54, "setbit", opSetBit, proto("aii:a"), 3, typed(typeSetBit)}, {0x55, "getbyte", opGetByte, proto("bi:i"), 3, detDefault()}, {0x56, "setbyte", opSetByte, proto("bii:b"), 3, detDefault()}, {0x57, "extract", opExtract, proto("b:b"), 5, immediates("s", "l")}, @@ -505,7 +520,6 @@ var OpSpecs = []OpSpec{ {0x5b, "extract_uint64", opExtract64Bits, proto("bi:i"), 5, detDefault()}, {0x5c, "replace2", opReplace2, proto("bb:b"), 7, immediates("s")}, {0x5d, "replace3", opReplace3, proto("bib:b"), 7, detDefault()}, - {0x5e, "base64_decode", opBase64Decode, proto("b:b"), fidoVersion, field("e", &Base64Encodings).costByLength(1, 1, 16, 0)}, {0x5f, "json_ref", opJSONRef, proto("bb:a"), fidoVersion, field("r", &JSONRefTypes).costByLength(25, 2, 7, 1)}, @@ -543,9 +557,13 @@ var OpSpecs = []OpSpec{ // "Function oriented" {0x88, "callsub", opCallSub, proto(":"), 4, detBranch()}, - {0x89, "retsub", opRetSub, proto(":"), 4, detDefault()}, - {0x8a, "switch", opSwitch, proto("i:"), 8, detSwitch()}, - // 0x8b will likely be a switch on pairs of values/targets + {0x89, "retsub", opRetSub, proto(":"), 4, detDefault().trust()}, + // protoByte is a named constant because opCallSub needs to know it. + {protoByte, "proto", opProto, proto(":"), fpVersion, immediates("a", "r").typed(typeProto)}, + {0x8b, "frame_dig", opFrameDig, proto(":a"), fpVersion, immKinded(immInt8, "i").typed(typeFrameDig)}, + {0x8c, "frame_bury", opFrameBury, proto("a:"), fpVersion, immKinded(immInt8, "i").typed(typeFrameBury)}, + {0x8d, "switch", opSwitch, proto("i:"), 8, detSwitch()}, + // 0x8e will likely be a switch on pairs of values/targets, called `match` // More math {0x90, "shl", opShiftLeft, proto("ii:i"), 4, detDefault()}, @@ -586,7 +604,7 @@ var OpSpecs = []OpSpec{ // AVM "effects" {0xb0, "log", opLog, proto("b:"), 5, only(modeApp)}, {0xb1, "itxn_begin", opTxBegin, proto(":"), 5, only(modeApp)}, - {0xb2, "itxn_field", opItxnField, proto("a:"), 5, stacky(typeTxField, "f").field("f", &TxnFields).only(modeApp).assembler(asmItxnField)}, + {0xb2, "itxn_field", opItxnField, proto("a:"), 5, immediates("f").typed(typeTxField).field("f", &TxnFields).only(modeApp).assembler(asmItxnField)}, {0xb3, "itxn_submit", opItxnSubmit, proto(":"), 5, only(modeApp)}, {0xb4, "itxn", opItxn, proto(":a"), 5, field("f", &TxnScalarFields).only(modeApp).assembler(asmItxn)}, {0xb5, "itxna", opItxna, proto(":a"), 5, immediates("f", "i").field("f", &TxnArrayFields).only(modeApp)}, diff --git a/data/transactions/logic/teal.tmLanguage.json b/data/transactions/logic/teal.tmLanguage.json index 863d0a0c43..7a299a9624 100644 --- a/data/transactions/logic/teal.tmLanguage.json +++ b/data/transactions/logic/teal.tmLanguage.json @@ -64,7 +64,7 @@ }, { "name": "keyword.control.teal", - "match": "^(assert|b|bnz|bz|callsub|cover|dig|dup|dup2|err|pop|retsub|return|select|swap|switch|uncover)\\b" + "match": "^(assert|b|bnz|bury|bz|callsub|cover|dig|dup|dup2|dupn|err|frame_bury|frame_dig|pop|popn|proto|retsub|return|select|swap|switch|uncover)\\b" }, { "name": "keyword.other.teal",