From bdc77fe278ab4bc37d266f57339b0e19ed84fb22 Mon Sep 17 00:00:00 2001 From: algoidurovic <91566643+algoidurovic@users.noreply.github.com> Date: Fri, 19 Aug 2022 15:05:18 -0400 Subject: [PATCH 01/19] typo fix --- data/transactions/logic/assembler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/transactions/logic/assembler.go b/data/transactions/logic/assembler.go index 6a7144d9bc..586f67000e 100644 --- a/data/transactions/logic/assembler.go +++ b/data/transactions/logic/assembler.go @@ -474,7 +474,7 @@ func asmInt(ops *OpStream, spec *OpSpec, args []string) error { ops.Uint(i) return nil } - // check OnCompetion constants + // check OnCompletion constants oc, isOCStr := onCompletionMap[args[0]] if isOCStr { ops.Uint(oc) From 5683c57b2da3d9d53f253b1412f6d023e5c14144 Mon Sep 17 00:00:00 2001 From: algoidurovic <91566643+algoidurovic@users.noreply.github.com> Date: Wed, 24 Aug 2022 15:21:38 -0400 Subject: [PATCH 02/19] initial implementation --- data/transactions/logic/assembler.go | 47 ++++++++++++++++----- data/transactions/logic/eval.go | 62 ++++++++++++++++++++++++++++ data/transactions/logic/opcodes.go | 13 ++++++ 3 files changed, 112 insertions(+), 10 deletions(-) diff --git a/data/transactions/logic/assembler.go b/data/transactions/logic/assembler.go index 586f67000e..551fc69004 100644 --- a/data/transactions/logic/assembler.go +++ b/data/transactions/logic/assembler.go @@ -48,10 +48,14 @@ type Writer interface { type labelReference struct { sourceLine int - // position of the opcode start that refers to the label + // position of the label reference position int label string + + // starting and ending positions of the opcode containing the label reference. + // the ending position is exclusive: follows [l, r) convention + offsetPosition int } type constReference interface { @@ -344,8 +348,8 @@ func (ops *OpStream) recordSourceLine() { } // referToLabel records an opcode label reference to resolve later -func (ops *OpStream) referToLabel(pc int, label string) { - ops.labelReferences = append(ops.labelReferences, labelReference{ops.sourceLine, pc, label}) +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) @@ -827,14 +831,38 @@ func asmBranch(ops *OpStream, spec *OpSpec, args []string) error { return ops.error("branch operation needs label argument") } - ops.referToLabel(ops.pending.Len(), args[0]) ops.pending.WriteByte(spec.Opcode) + ops.referToLabel(ops.pending.Len(), args[0], ops.pending.Len()+3) // zero bytes will get replaced with actual offset in resolveLabels() ops.pending.WriteByte(0) ops.pending.WriteByte(0) return nil } +func asmSwitch(ops *OpStream, spec *OpSpec, args []string) error { + numOffsets, err := strconv.ParseUint(args[0], 0, 64) + if err != nil { + return err + } + + if len(args)-1 != int(numOffsets) { + return ops.errorf("switch operation requires %d labels but contains %d", numOffsets, len(args)-1) + } + + ops.pending.WriteByte(spec.Opcode) + var scratch [binary.MaxVarintLen64]byte + vlen := binary.PutUvarint(scratch[:], numOffsets) + ops.pending.Write(scratch[:vlen]) + opEndPos := ops.pending.Len() + 2*int(numOffsets) + for i := 1; i <= int(numOffsets); i++ { + ops.referToLabel(ops.pending.Len(), args[i], opEndPos) + // zero bytes will get replaced with actual offset in resolveLabels() + ops.pending.WriteByte(0) + ops.pending.WriteByte(0) + } + return nil +} + func asmSubstring(ops *OpStream, spec *OpSpec, args []string) error { err := asmDefault(ops, spec, args) if err != nil { @@ -1738,19 +1766,18 @@ func (ops *OpStream) resolveLabels() { reported[lr.label] = true continue } - // all branch instructions (currently) are opcode byte and 2 offset bytes, and the destination is relative to the next pc as if the branch was a no-op - naturalPc := lr.position + 3 - if ops.Version < backBranchEnabledVersion && dest < naturalPc { + + if ops.Version < backBranchEnabledVersion && dest < lr.offsetPosition { ops.errorf("label %#v is a back reference, back jump support was introduced in v4", lr.label) continue } - jump := dest - naturalPc + jump := dest - lr.offsetPosition if jump > 0x7fff { ops.errorf("label %#v is too far away", lr.label) continue } - raw[lr.position+1] = uint8(jump >> 8) - raw[lr.position+2] = uint8(jump & 0x0ff) + raw[lr.position] = uint8(jump >> 8) + raw[lr.position+1] = uint8(jump & 0x0ff) } ops.pending = *bytes.NewBuffer(raw) ops.sourceLine = saved diff --git a/data/transactions/logic/eval.go b/data/transactions/logic/eval.go index b648ff7781..9745d697be 100644 --- a/data/transactions/logic/eval.go +++ b/data/transactions/logic/eval.go @@ -1985,6 +1985,28 @@ func branchTarget(cx *EvalContext) (int, error) { return target, nil } +func branchSwitch(cx *EvalContext, branchIdx uint64) (int, uint64, error) { + numOffsets, bytesUsed := binary.Uvarint(cx.program[cx.pc+1:]) + if numOffsets == 0 { + return 0, 0, fmt.Errorf("number of offsets must be greater than 0") + } + if branchIdx >= numOffsets { + return 0, 0, fmt.Errorf("provided branch index %d exceeds max offset index %d", branchIdx, numOffsets-1) + } + + pos := uint64(cx.pc+1+bytesUsed) + (2 * branchIdx) + offset := int16(uint16(cx.program[pos])<<8 | uint16(cx.program[pos+1])) + target := (cx.pc + 1 + bytesUsed) + 2*int(numOffsets) + int(offset) + + // branching to exactly the end of the program (target == len(cx.program)), the next pc after the last instruction, + // is okay and ends normally + if target > len(cx.program) || target < 0 { + return 0, 0, fmt.Errorf("branch target %d outside of program", target) + } + + return target, numOffsets, nil +} + // checks any branch that is {op} {int16 be offset} func checkBranch(cx *EvalContext) error { target, err := branchTarget(cx) @@ -2000,6 +2022,34 @@ func checkBranch(cx *EvalContext) error { cx.branchTargets[target] = true return nil } + +// checks any branch that is {op} {int16 be offset} +func checkSwitch(cx *EvalContext) error { + // first call to get the number of offsets, 0 is a safe choice because there must exist at least one label + _, numOffsets, err := branchSwitch(cx, 0) + if err != nil { + return err + } + + _, bytesUsed := binary.Uvarint(cx.program[cx.pc+1:]) + opSize := 1 + bytesUsed + 2*int(numOffsets) + for branchIdx := uint64(0); branchIdx < numOffsets; branchIdx++ { + target, _, err := branchSwitch(cx, branchIdx) + if err != nil { + return err + } + + if target < cx.pc+opSize { + // If a branch goes backwards, we should have already noted that an instruction began at that location. + if _, ok := cx.instructionStarts[target]; !ok { + return fmt.Errorf("back branch target %d is not an aligned instruction", target) + } + } + cx.branchTargets[target] = true + } + return nil +} + func opBnz(cx *EvalContext) error { last := len(cx.stack) - 1 cx.nextpc = cx.pc + 3 @@ -2039,6 +2089,18 @@ func opB(cx *EvalContext) error { return nil } +func opSwitchInt(cx *EvalContext) error { + last := len(cx.stack) - 1 + branchIdx := cx.stack[last].Uint + cx.stack = cx.stack[:last] + target, _, err := branchSwitch(cx, branchIdx) + if err != nil { + return err + } + cx.nextpc = target + return nil +} + func opCallSub(cx *EvalContext) error { cx.callstack = append(cx.callstack, cx.pc+3) return opB(cx) diff --git a/data/transactions/logic/opcodes.go b/data/transactions/logic/opcodes.go index dc5627422e..49ca47b90b 100644 --- a/data/transactions/logic/opcodes.go +++ b/data/transactions/logic/opcodes.go @@ -184,6 +184,15 @@ func opBranch() OpDetails { return d } +func opSwitch() OpDetails { + d := opDefault() + d.asm = asmSwitch + d.check = checkSwitch + d.Size = 0 + d.Immediates = []immediate{imm("uint", immInt), imm("target ...", immLabels)} + return d +} + func assembler(asm asmFunc) OpDetails { d := opDefault() d.asm = asm @@ -288,6 +297,7 @@ const ( immBytes immInts immBytess // "ss" not a typo. Multiple "bytes" + immLabels ) type immediate struct { @@ -596,6 +606,9 @@ var OpSpecs = []OpSpec{ // randomness support {0xd0, "vrf_verify", opVrfVerify, proto("bbb:bi"), randomnessVersion, field("s", &VrfStandards).costs(5700)}, {0xd1, "block", opBlock, proto("i:a"), randomnessVersion, field("f", &BlockFields)}, + + // switch on value + {0xe0, "switchi", opSwitchInt, proto("i:"), 8, opSwitch()}, } type sortByOpcode []OpSpec From 80e1b1225fef85fc4befb58baf60a122ac1d8fc7 Mon Sep 17 00:00:00 2001 From: algoidurovic <91566643+algoidurovic@users.noreply.github.com> Date: Mon, 29 Aug 2022 11:41:03 -0400 Subject: [PATCH 03/19] documentation --- data/transactions/logic/README.md | 1 + data/transactions/logic/TEAL_opcodes.md | 8 ++++++++ data/transactions/logic/doc.go | 2 ++ data/transactions/logic/eval.go | 8 ++++---- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/data/transactions/logic/README.md b/data/transactions/logic/README.md index 6c68a8a7c9..5c2f5715a2 100644 --- a/data/transactions/logic/README.md +++ b/data/transactions/logic/README.md @@ -594,6 +594,7 @@ Account fields used in the `acct_params_get` opcode. | `assert` | immediately fail unless A is a non-zero number | | `callsub target` | branch unconditionally to TARGET, saving the next instruction on the call stack | | `retsub` | pop the top instruction from the call stack and branch to it | +| `switchi n target[0] ... target[n-1]` | branch unconditionally to TARGET[A] | ### State Access diff --git a/data/transactions/logic/TEAL_opcodes.md b/data/transactions/logic/TEAL_opcodes.md index 6c5d679108..2342ae0395 100644 --- a/data/transactions/logic/TEAL_opcodes.md +++ b/data/transactions/logic/TEAL_opcodes.md @@ -1411,3 +1411,11 @@ The notation A,B indicates that A and B are interpreted as a uint128 value, with | 0 | BlkSeed | []byte | | | 1 | BlkTimestamp | uint64 | | +## switchi n target[0] ... target[n-1] + +- Opcode: 0xe0 {varuint length} [{int16 branch offset big-endian}, ...] +- Stack: ..., A: uint64 → ... +- branch unconditionally to TARGET[A]. Fail if A is less than `n-1` +- Availability: v8 + +The `switchi` instruction opcode 0xe0 is followed `n`, the number of targets, each of which are encoded as 2 byte values indicating the position of the target label relative to the end of the `switchi` instruction. 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. diff --git a/data/transactions/logic/doc.go b/data/transactions/logic/doc.go index 003c60a611..8f3a3bc2ff 100644 --- a/data/transactions/logic/doc.go +++ b/data/transactions/logic/doc.go @@ -193,6 +193,8 @@ var opDocByName = map[string]string{ "vrf_verify": "Verify the proof B of message A against pubkey C. Returns vrf output and verification flag.", "block": "field F of block A. Fail unless A falls between txn.LastValid-1002 and txn.FirstValid (exclusive)", + + "switchi": "branch to target at index A. Fail if index A is out of bound.", } // OpDoc returns a description of the op diff --git a/data/transactions/logic/eval.go b/data/transactions/logic/eval.go index 9745d697be..e5095ef73f 100644 --- a/data/transactions/logic/eval.go +++ b/data/transactions/logic/eval.go @@ -1985,7 +1985,7 @@ func branchTarget(cx *EvalContext) (int, error) { return target, nil } -func branchSwitch(cx *EvalContext, branchIdx uint64) (int, uint64, error) { +func switchTarget(cx *EvalContext, branchIdx uint64) (int, uint64, error) { numOffsets, bytesUsed := binary.Uvarint(cx.program[cx.pc+1:]) if numOffsets == 0 { return 0, 0, fmt.Errorf("number of offsets must be greater than 0") @@ -2026,7 +2026,7 @@ func checkBranch(cx *EvalContext) error { // checks any branch that is {op} {int16 be offset} func checkSwitch(cx *EvalContext) error { // first call to get the number of offsets, 0 is a safe choice because there must exist at least one label - _, numOffsets, err := branchSwitch(cx, 0) + _, numOffsets, err := switchTarget(cx, 0) if err != nil { return err } @@ -2034,7 +2034,7 @@ func checkSwitch(cx *EvalContext) error { _, bytesUsed := binary.Uvarint(cx.program[cx.pc+1:]) opSize := 1 + bytesUsed + 2*int(numOffsets) for branchIdx := uint64(0); branchIdx < numOffsets; branchIdx++ { - target, _, err := branchSwitch(cx, branchIdx) + target, _, err := switchTarget(cx, branchIdx) if err != nil { return err } @@ -2093,7 +2093,7 @@ func opSwitchInt(cx *EvalContext) error { last := len(cx.stack) - 1 branchIdx := cx.stack[last].Uint cx.stack = cx.stack[:last] - target, _, err := branchSwitch(cx, branchIdx) + target, _, err := switchTarget(cx, branchIdx) if err != nil { return err } From 8ebb577e3deb995bb4d000a951bc771fa48a3757 Mon Sep 17 00:00:00 2001 From: algoidurovic <91566643+algoidurovic@users.noreply.github.com> Date: Wed, 7 Sep 2022 14:41:31 -0400 Subject: [PATCH 04/19] respond to feedback --- data/transactions/logic/assembler.go | 7 +- data/transactions/logic/eval.go | 17 ++-- data/transactions/logic/opcodes.go | 142 +++++++++++++-------------- 3 files changed, 86 insertions(+), 80 deletions(-) diff --git a/data/transactions/logic/assembler.go b/data/transactions/logic/assembler.go index 551fc69004..8e8f0e0830 100644 --- a/data/transactions/logic/assembler.go +++ b/data/transactions/logic/assembler.go @@ -53,8 +53,7 @@ type labelReference struct { label string - // starting and ending positions of the opcode containing the label reference. - // the ending position is exclusive: follows [l, r) convention + // ending positions of the opcode containing the label reference. offsetPosition int } @@ -831,8 +830,8 @@ func asmBranch(ops *OpStream, spec *OpSpec, args []string) error { return ops.error("branch operation needs label argument") } + ops.referToLabel(ops.pending.Len(), args[0], ops.pending.Len()+spec.Size) ops.pending.WriteByte(spec.Opcode) - ops.referToLabel(ops.pending.Len(), args[0], ops.pending.Len()+3) // zero bytes will get replaced with actual offset in resolveLabels() ops.pending.WriteByte(0) ops.pending.WriteByte(0) @@ -1767,6 +1766,8 @@ func (ops *OpStream) resolveLabels() { continue } + // All branch targets are encoded as 2 offset bytes. The destination is relative to the end of the + // instruction they appear in, which is available in lr.offsetPostion if ops.Version < backBranchEnabledVersion && dest < lr.offsetPosition { ops.errorf("label %#v is a back reference, back jump support was introduced in v4", lr.label) continue diff --git a/data/transactions/logic/eval.go b/data/transactions/logic/eval.go index e5095ef73f..eaed8e11b6 100644 --- a/data/transactions/logic/eval.go +++ b/data/transactions/logic/eval.go @@ -1987,16 +1987,21 @@ func branchTarget(cx *EvalContext) (int, error) { func switchTarget(cx *EvalContext, branchIdx uint64) (int, uint64, error) { numOffsets, bytesUsed := binary.Uvarint(cx.program[cx.pc+1:]) - if numOffsets == 0 { - return 0, 0, fmt.Errorf("number of offsets must be greater than 0") + if numOffsets <= 0 { + return 0, 0, fmt.Errorf("could not decode switch label count at pc=%d", cx.pc+1) } if branchIdx >= numOffsets { return 0, 0, fmt.Errorf("provided branch index %d exceeds max offset index %d", branchIdx, numOffsets-1) } - pos := uint64(cx.pc+1+bytesUsed) + (2 * branchIdx) + end := cx.pc + 1 + bytesUsed // end of opcode + number of offsets, beginning of offset list + pos := uint64(end) + (2 * branchIdx) // position of referenced offset: each offset is 2 bytes + if pos >= uint64(len(cx.program)-1) { + return 0, 0, fmt.Errorf("invalid byte code: expected offset value but reached end of program") + } + offset := int16(uint16(cx.program[pos])<<8 | uint16(cx.program[pos+1])) - target := (cx.pc + 1 + bytesUsed) + 2*int(numOffsets) + int(offset) + target := end + 2*int(numOffsets) + int(offset) // offset is applied to the end of this opcode // branching to exactly the end of the program (target == len(cx.program)), the next pc after the last instruction, // is okay and ends normally @@ -2023,7 +2028,7 @@ func checkBranch(cx *EvalContext) error { return nil } -// checks any branch that is {op} {int16 be offset} +// checks any switch that is {op} {varuint offset index} [{int16 offset}...] func checkSwitch(cx *EvalContext) error { // first call to get the number of offsets, 0 is a safe choice because there must exist at least one label _, numOffsets, err := switchTarget(cx, 0) @@ -2031,7 +2036,7 @@ func checkSwitch(cx *EvalContext) error { return err } - _, bytesUsed := binary.Uvarint(cx.program[cx.pc+1:]) + _, bytesUsed := binary.Uvarint(cx.program[cx.pc+1:]) // decoding the value will work because switchTarget() above already checked opSize := 1 + bytesUsed + 2*int(numOffsets) for branchIdx := uint64(0); branchIdx < numOffsets; branchIdx++ { target, _, err := switchTarget(cx, branchIdx) diff --git a/data/transactions/logic/opcodes.go b/data/transactions/logic/opcodes.go index 49ca47b90b..a9a7c03d6d 100644 --- a/data/transactions/logic/opcodes.go +++ b/data/transactions/logic/opcodes.go @@ -167,7 +167,7 @@ func (d *OpDetails) Cost(program []byte, pc int, stack []stackValue) int { return cost } -func opDefault() OpDetails { +func detDefault() OpDetails { return OpDetails{asmDefault, nil, nil, modeAny, linearCost{baseCost: 1}, 1, nil} } @@ -175,8 +175,8 @@ func constants(asm asmFunc, checker checkFunc, name string, kind immKind) OpDeta return OpDetails{asm, checker, nil, modeAny, linearCost{baseCost: 1}, 0, []immediate{imm(name, kind)}} } -func opBranch() OpDetails { - d := opDefault() +func detBranch() OpDetails { + d := detDefault() d.asm = asmBranch d.check = checkBranch d.Size = 3 @@ -184,8 +184,8 @@ func opBranch() OpDetails { return d } -func opSwitch() OpDetails { - d := opDefault() +func detSwitch() OpDetails { + d := detDefault() d.asm = asmSwitch d.check = checkSwitch d.Size = 0 @@ -194,7 +194,7 @@ func opSwitch() OpDetails { } func assembler(asm asmFunc) OpDetails { - d := opDefault() + d := detDefault() d.asm = asm return d } @@ -206,7 +206,7 @@ func (d OpDetails) assembler(asm asmFunc) OpDetails { } func costly(cost int) OpDetails { - d := opDefault() + d := detDefault() d.FullCost.baseCost = cost return d } @@ -218,7 +218,7 @@ func (d OpDetails) costs(cost int) OpDetails { } func only(m runMode) OpDetails { - d := opDefault() + d := detDefault() d.Modes = m return d } @@ -236,7 +236,7 @@ func (d OpDetails) costByLength(initial, perChunk, chunkSize, depth int) OpDetai } func immediates(names ...string) OpDetails { - d := opDefault() + d := detDefault() d.Size = len(names) + 1 d.Immediates = make([]immediate, len(names)) for i, name := range names { @@ -282,7 +282,7 @@ func costByLength(initial, perChunk, chunkSize, depth int) OpDetails { if initial < 1 || perChunk <= 0 || chunkSize < 1 || chunkSize > maxStringSize { panic("bad cost configuration") } - d := opDefault() + d := detDefault() d.FullCost = linearCost{initial, perChunk, chunkSize, depth} return d } @@ -379,7 +379,7 @@ func (spec *OpSpec) deadens() bool { // Note: assembly can specialize an Any return type if known at // assembly-time, with ops.returns() var OpSpecs = []OpSpec{ - {0x00, "err", opErr, proto(":x"), 1, opDefault()}, + {0x00, "err", opErr, proto(":x"), 1, detDefault()}, {0x01, "sha256", opSHA256, proto("b:b"), 1, costly(7)}, {0x02, "keccak256", opKeccak256, proto("b:b"), 1, costly(26)}, {0x03, "sha512_256", opSHA512_256, proto("b:b"), 1, costly(9)}, @@ -409,43 +409,43 @@ var OpSpecs = []OpSpec{ {0x06, "ecdsa_pk_decompress", opEcdsaPkDecompress, proto("b:bb"), 5, costByField("v", &EcdsaCurves, ecdsaDecompressCosts)}, {0x07, "ecdsa_pk_recover", opEcdsaPkRecover, proto("bibb:bb"), 5, field("v", &EcdsaCurves).costs(2000)}, - {0x08, "+", opPlus, proto("ii:i"), 1, opDefault()}, - {0x09, "-", opMinus, proto("ii:i"), 1, opDefault()}, - {0x0a, "/", opDiv, proto("ii:i"), 1, opDefault()}, - {0x0b, "*", opMul, proto("ii:i"), 1, opDefault()}, - {0x0c, "<", opLt, proto("ii:i"), 1, opDefault()}, - {0x0d, ">", opGt, proto("ii:i"), 1, opDefault()}, - {0x0e, "<=", opLe, proto("ii:i"), 1, opDefault()}, - {0x0f, ">=", opGe, proto("ii:i"), 1, opDefault()}, - {0x10, "&&", opAnd, proto("ii:i"), 1, opDefault()}, - {0x11, "||", opOr, proto("ii:i"), 1, opDefault()}, + {0x08, "+", opPlus, proto("ii:i"), 1, detDefault()}, + {0x09, "-", opMinus, proto("ii:i"), 1, detDefault()}, + {0x0a, "/", opDiv, proto("ii:i"), 1, detDefault()}, + {0x0b, "*", opMul, proto("ii:i"), 1, detDefault()}, + {0x0c, "<", opLt, proto("ii:i"), 1, detDefault()}, + {0x0d, ">", opGt, proto("ii:i"), 1, detDefault()}, + {0x0e, "<=", opLe, proto("ii:i"), 1, detDefault()}, + {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)}, - {0x14, "!", opNot, proto("i:i"), 1, opDefault()}, - {0x15, "len", opLen, proto("b:i"), 1, opDefault()}, - {0x16, "itob", opItob, proto("i:b"), 1, opDefault()}, - {0x17, "btoi", opBtoi, proto("b:i"), 1, opDefault()}, - {0x18, "%", opModulo, proto("ii:i"), 1, opDefault()}, - {0x19, "|", opBitOr, proto("ii:i"), 1, opDefault()}, - {0x1a, "&", opBitAnd, proto("ii:i"), 1, opDefault()}, - {0x1b, "^", opBitXor, proto("ii:i"), 1, opDefault()}, - {0x1c, "~", opBitNot, proto("i:i"), 1, opDefault()}, - {0x1d, "mulw", opMulw, proto("ii:ii"), 1, opDefault()}, - {0x1e, "addw", opAddw, proto("ii:ii"), 2, opDefault()}, + {0x14, "!", opNot, proto("i:i"), 1, detDefault()}, + {0x15, "len", opLen, proto("b:i"), 1, detDefault()}, + {0x16, "itob", opItob, proto("i:b"), 1, detDefault()}, + {0x17, "btoi", opBtoi, proto("b:i"), 1, detDefault()}, + {0x18, "%", opModulo, proto("ii:i"), 1, detDefault()}, + {0x19, "|", opBitOr, proto("ii:i"), 1, detDefault()}, + {0x1a, "&", opBitAnd, proto("ii:i"), 1, detDefault()}, + {0x1b, "^", opBitXor, proto("ii:i"), 1, detDefault()}, + {0x1c, "~", opBitNot, proto("i:i"), 1, detDefault()}, + {0x1d, "mulw", opMulw, proto("ii:ii"), 1, detDefault()}, + {0x1e, "addw", opAddw, proto("ii:ii"), 2, detDefault()}, {0x1f, "divmodw", opDivModw, proto("iiii:iiii"), 4, costly(20)}, {0x20, "intcblock", opIntConstBlock, proto(":"), 1, constants(asmIntCBlock, checkIntConstBlock, "uint ...", immInts)}, {0x21, "intc", opIntConstLoad, proto(":i"), 1, immediates("i").assembler(asmIntC)}, - {0x22, "intc_0", opIntConst0, proto(":i"), 1, opDefault()}, - {0x23, "intc_1", opIntConst1, proto(":i"), 1, opDefault()}, - {0x24, "intc_2", opIntConst2, proto(":i"), 1, opDefault()}, - {0x25, "intc_3", opIntConst3, proto(":i"), 1, opDefault()}, + {0x22, "intc_0", opIntConst0, proto(":i"), 1, detDefault()}, + {0x23, "intc_1", opIntConst1, proto(":i"), 1, detDefault()}, + {0x24, "intc_2", opIntConst2, proto(":i"), 1, detDefault()}, + {0x25, "intc_3", opIntConst3, proto(":i"), 1, detDefault()}, {0x26, "bytecblock", opByteConstBlock, proto(":"), 1, constants(asmByteCBlock, checkByteConstBlock, "bytes ...", immBytess)}, {0x27, "bytec", opByteConstLoad, proto(":b"), 1, immediates("i").assembler(asmByteC)}, - {0x28, "bytec_0", opByteConst0, proto(":b"), 1, opDefault()}, - {0x29, "bytec_1", opByteConst1, proto(":b"), 1, opDefault()}, - {0x2a, "bytec_2", opByteConst2, proto(":b"), 1, opDefault()}, - {0x2b, "bytec_3", opByteConst3, proto(":b"), 1, opDefault()}, + {0x28, "bytec_0", opByteConst0, proto(":b"), 1, detDefault()}, + {0x29, "bytec_1", opByteConst1, proto(":b"), 1, detDefault()}, + {0x2a, "bytec_2", opByteConst2, proto(":b"), 1, detDefault()}, + {0x2b, "bytec_3", opByteConst3, proto(":b"), 1, detDefault()}, {0x2c, "arg", opArg, proto(":b"), 1, immediates("n").only(modeSig).assembler(asmArg)}, {0x2d, "arg_0", opArg0, proto(":b"), 1, only(modeSig)}, {0x2e, "arg_1", opArg1, proto(":b"), 1, only(modeSig)}, @@ -474,12 +474,12 @@ var OpSpecs = []OpSpec{ {0x3e, "loads", opLoads, proto("i:a"), 5, stacky(typeLoads)}, {0x3f, "stores", opStores, proto("ia:"), 5, stacky(typeStores)}, - {0x40, "bnz", opBnz, proto("i:"), 1, opBranch()}, - {0x41, "bz", opBz, proto("i:"), 2, opBranch()}, - {0x42, "b", opB, proto(":"), 2, opBranch()}, - {0x43, "return", opReturn, proto("i:x"), 2, opDefault()}, - {0x44, "assert", opAssert, proto("i:"), 3, opDefault()}, - {0x48, "pop", opPop, proto("a:"), 1, opDefault()}, + {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()}, + {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 @@ -491,20 +491,20 @@ var OpSpecs = []OpSpec{ {0x4f, "uncover", opUncover, proto("a:a", "A, [N items]", "[N items], A"), 5, stacky(typeUncover, "n")}, // byteslice processing / StringOps - {0x50, "concat", opConcat, proto("bb:b"), 2, opDefault()}, + {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, opDefault()}, - {0x53, "getbit", opGetBit, proto("ai:i"), 3, opDefault()}, + {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)}, - {0x55, "getbyte", opGetByte, proto("bi:i"), 3, opDefault()}, - {0x56, "setbyte", opSetByte, proto("bii:b"), 3, opDefault()}, + {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")}, - {0x58, "extract3", opExtract3, proto("bii:b"), 5, opDefault()}, - {0x59, "extract_uint16", opExtract16Bits, proto("bi:i"), 5, opDefault()}, - {0x5a, "extract_uint32", opExtract32Bits, proto("bi:i"), 5, opDefault()}, - {0x5b, "extract_uint64", opExtract64Bits, proto("bi:i"), 5, opDefault()}, + {0x58, "extract3", opExtract3, proto("bii:b"), 5, detDefault()}, + {0x59, "extract_uint16", opExtract16Bits, proto("bi:i"), 5, detDefault()}, + {0x5a, "extract_uint32", opExtract32Bits, proto("bi:i"), 5, detDefault()}, + {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, opDefault()}, + {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)}, @@ -542,19 +542,19 @@ var OpSpecs = []OpSpec{ {0x84, "ed25519verify_bare", opEd25519VerifyBare, proto("bbb:i"), 7, costly(1900)}, // "Function oriented" - {0x88, "callsub", opCallSub, proto(":"), 4, opBranch()}, - {0x89, "retsub", opRetSub, proto(":"), 4, opDefault()}, + {0x88, "callsub", opCallSub, proto(":"), 4, detBranch()}, + {0x89, "retsub", opRetSub, proto(":"), 4, detDefault()}, // Leave a little room for indirect function calls, or similar // More math - {0x90, "shl", opShiftLeft, proto("ii:i"), 4, opDefault()}, - {0x91, "shr", opShiftRight, proto("ii:i"), 4, opDefault()}, + {0x90, "shl", opShiftLeft, proto("ii:i"), 4, detDefault()}, + {0x91, "shr", opShiftRight, proto("ii:i"), 4, detDefault()}, {0x92, "sqrt", opSqrt, proto("i:i"), 4, costly(4)}, - {0x93, "bitlen", opBitLen, proto("a:i"), 4, opDefault()}, - {0x94, "exp", opExp, proto("ii:i"), 4, opDefault()}, + {0x93, "bitlen", opBitLen, proto("a:i"), 4, detDefault()}, + {0x94, "exp", opExp, proto("ii:i"), 4, detDefault()}, {0x95, "expw", opExpw, proto("ii:ii"), 4, costly(10)}, {0x96, "bsqrt", opBytesSqrt, proto("b:b"), 6, costly(40)}, - {0x97, "divw", opDivw, proto("iii:i"), 6, opDefault()}, + {0x97, "divw", opDivw, proto("iii:i"), 6, detDefault()}, {0x98, "sha3_256", opSHA3_256, proto("b:b"), 7, costly(130)}, /* Will end up following keccak256 - {0x98, "sha3_256", opSHA3_256, proto("b:b"), unlimitedStorage, costByLength(58, 4, 8)},}, @@ -570,18 +570,18 @@ var OpSpecs = []OpSpec{ {0xa1, "b-", opBytesMinus, proto("bb:b"), 4, costly(10)}, {0xa2, "b/", opBytesDiv, proto("bb:b"), 4, costly(20)}, {0xa3, "b*", opBytesMul, proto("bb:b"), 4, costly(20)}, - {0xa4, "b<", opBytesLt, proto("bb:i"), 4, opDefault()}, - {0xa5, "b>", opBytesGt, proto("bb:i"), 4, opDefault()}, - {0xa6, "b<=", opBytesLe, proto("bb:i"), 4, opDefault()}, - {0xa7, "b>=", opBytesGe, proto("bb:i"), 4, opDefault()}, - {0xa8, "b==", opBytesEq, proto("bb:i"), 4, opDefault()}, - {0xa9, "b!=", opBytesNeq, proto("bb:i"), 4, opDefault()}, + {0xa4, "b<", opBytesLt, proto("bb:i"), 4, detDefault()}, + {0xa5, "b>", opBytesGt, proto("bb:i"), 4, detDefault()}, + {0xa6, "b<=", opBytesLe, proto("bb:i"), 4, detDefault()}, + {0xa7, "b>=", opBytesGe, proto("bb:i"), 4, detDefault()}, + {0xa8, "b==", opBytesEq, proto("bb:i"), 4, detDefault()}, + {0xa9, "b!=", opBytesNeq, proto("bb:i"), 4, detDefault()}, {0xaa, "b%", opBytesModulo, proto("bb:b"), 4, costly(20)}, {0xab, "b|", opBytesBitOr, proto("bb:b"), 4, costly(6)}, {0xac, "b&", opBytesBitAnd, proto("bb:b"), 4, costly(6)}, {0xad, "b^", opBytesBitXor, proto("bb:b"), 4, costly(6)}, {0xae, "b~", opBytesBitNot, proto("b:b"), 4, costly(4)}, - {0xaf, "bzero", opBytesZero, proto("i:b"), 4, opDefault()}, + {0xaf, "bzero", opBytesZero, proto("i:b"), 4, detDefault()}, // AVM "effects" {0xb0, "log", opLog, proto("b:"), 5, only(modeApp)}, @@ -608,7 +608,7 @@ var OpSpecs = []OpSpec{ {0xd1, "block", opBlock, proto("i:a"), randomnessVersion, field("f", &BlockFields)}, // switch on value - {0xe0, "switchi", opSwitchInt, proto("i:"), 8, opSwitch()}, + {0xe0, "switchi", opSwitchInt, proto("i:"), 8, detSwitch()}, } type sortByOpcode []OpSpec From 011495d08eda4fc01ecaac5d3b3868a213310489 Mon Sep 17 00:00:00 2001 From: algoidurovic <91566643+algoidurovic@users.noreply.github.com> Date: Thu, 8 Sep 2022 14:01:54 -0400 Subject: [PATCH 05/19] fix errors --- data/transactions/logic/TEAL_opcodes.md | 2 +- data/transactions/logic/assembler.go | 58 ++++++++++++++++++-- data/transactions/logic/assembler_test.go | 11 +++- data/transactions/logic/evalStateful_test.go | 2 + data/transactions/logic/opcodes.go | 2 +- 5 files changed, 67 insertions(+), 8 deletions(-) diff --git a/data/transactions/logic/TEAL_opcodes.md b/data/transactions/logic/TEAL_opcodes.md index 2342ae0395..74e18330c9 100644 --- a/data/transactions/logic/TEAL_opcodes.md +++ b/data/transactions/logic/TEAL_opcodes.md @@ -1418,4 +1418,4 @@ The notation A,B indicates that A and B are interpreted as a uint128 value, with - branch unconditionally to TARGET[A]. Fail if A is less than `n-1` - Availability: v8 -The `switchi` instruction opcode 0xe0 is followed `n`, the number of targets, each of which are encoded as 2 byte values indicating the position of the target label relative to the end of the `switchi` instruction. 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. +The `switchi` instruction opcode 0xe0 is followed by `n`, the number of targets, each of which are encoded as 2 byte values indicating the position of the target label relative to the end of the `switchi` instruction (i.e. the offset). The last element on the stack represents the index of the target to branch to. If the index is greater than or equal to n, the evaluation will fail. Otherwise, the program will branch to `pc + 1 + sizeof(n) + 2 * n + target[index]`. Branch targets must be aligned instructions. (e.g. Branching to the second byte of a 2 byte op will be rejected.) \ No newline at end of file diff --git a/data/transactions/logic/assembler.go b/data/transactions/logic/assembler.go index 8e8f0e0830..5f01a963ce 100644 --- a/data/transactions/logic/assembler.go +++ b/data/transactions/logic/assembler.go @@ -830,7 +830,7 @@ func asmBranch(ops *OpStream, spec *OpSpec, args []string) error { return ops.error("branch operation needs label argument") } - ops.referToLabel(ops.pending.Len(), args[0], ops.pending.Len()+spec.Size) + ops.referToLabel(ops.pending.Len()+1, args[0], ops.pending.Len()+spec.Size) ops.pending.WriteByte(spec.Opcode) // zero bytes will get replaced with actual offset in resolveLabels() ops.pending.WriteByte(0) @@ -2012,6 +2012,7 @@ func (ops *OpStream) optimizeConstants(refs []constReference, constBlock []inter for i := range ops.labelReferences { if ops.labelReferences[i].position > position { ops.labelReferences[i].position += positionDelta + ops.labelReferences[i].offsetPosition += positionDelta } } @@ -2248,11 +2249,8 @@ func disassemble(dis *disassembleState, spec *OpSpec) (string, error) { pc++ case immLabel: - offset := (uint(dis.program[pc]) << 8) | uint(dis.program[pc+1]) + offset := int16((uint(dis.program[pc]) << 8) | uint(dis.program[pc+1])) target := int(offset) + pc + 2 - if target > 0xffff { - target -= 0x10000 - } var label string if dis.numericTargets { label = fmt.Sprintf("%d", target) @@ -2314,6 +2312,31 @@ func disassemble(dis *disassembleState, spec *OpSpec) (string, error) { out += fmt.Sprintf("0x%s", hex.EncodeToString(bv)) } pc = nextpc + case immLabels: + targets, nextpc, err := parseSwitch(dis.program, pc) + if err != nil { + return "", err + } + + var labels []string + for _, target := range targets { + var label string + if dis.numericTargets { + label = fmt.Sprintf("%d", target) + } else { + if known, ok := dis.pendingLabels[target]; ok { + label = known + } else { + dis.labelCount++ + label = fmt.Sprintf("label%d", dis.labelCount) + dis.putLabel(label, target) + } + } + labels = append(labels, label) + } + out += strconv.Itoa(len(targets)) + " " + out += strings.Join(labels, " ") + pc = nextpc default: return "", fmt.Errorf("unknown immKind %d", imm.kind) } @@ -2467,6 +2490,31 @@ func checkByteConstBlock(cx *EvalContext) error { return nil } +func parseSwitch(program []byte, pos int) (targets []int, nextpc int, err error) { + numOffsets, bytesUsed := binary.Uvarint(program[pos:]) + if bytesUsed <= 0 { + err = fmt.Errorf("could not decode switch target list size at pc=%d", pos) + return + } + pc := pos + pos += bytesUsed + if numOffsets > uint64(len(program)) { + err = errTooManyItems + return + } + + end := pos + int(2*numOffsets) // end of op: offset is applied to this position + for i := 0; i < int(numOffsets); i++ { + offset := int16(uint16(program[pos])<<8 | uint16(program[pos+1])) + target := int(offset) + int(end) + targets = append(targets, target) + pos += 2 + fmt.Println(fmt.Sprintf("%d %d %d", pc, pos, target)) + } + nextpc = pos + return +} + func allPrintableASCII(bytes []byte) bool { for _, b := range bytes { if b < 32 || b > 126 { diff --git a/data/transactions/logic/assembler_test.go b/data/transactions/logic/assembler_test.go index d0bcff2954..d3116224f8 100644 --- a/data/transactions/logic/assembler_test.go +++ b/data/transactions/logic/assembler_test.go @@ -393,7 +393,13 @@ pushint 1 replace3 ` -const v8Nonsense = v7Nonsense + pairingNonsense +const v8Nonsense = v7Nonsense + pairingNonsense + ` +switch_label0: +pushint 1 +switchi 2 switch_label0 switch_label1 +switch_label1: +pushint 1 +` const v6Compiled = "2004010002b7a60c26050242420c68656c6c6f20776f726c6421070123456789abcd208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d047465737400320032013202320380021234292929292b0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e01022581f8acd19181cf959a1281f8acd19181cf951a81f8acd19181cf1581f8acd191810f082209240a220b230c240d250e230f23102311231223132314181b1c28171615400003290349483403350222231d4a484848482b50512a632223524100034200004322602261222704634848222862482864286548482228246628226723286828692322700048482371004848361c0037001a0031183119311b311d311e311f312023221e312131223123312431253126312731283129312a312b312c312d312e312f447825225314225427042455220824564c4d4b0222382124391c0081e80780046a6f686e2281d00f23241f880003420001892224902291922494249593a0a1a2a3a4a5a6a7a8a9aaabacadae24af3a00003b003c003d816472064e014f012a57000823810858235b235a2359b03139330039b1b200b322c01a23c1001a2323c21a23c3233e233f8120af06002a494905002a49490700b53a03b6b7043cb8033a0c2349c42a9631007300810881088120978101c53a8101c6003a" @@ -467,6 +473,8 @@ func TestAssemble(t *testing.T) { expectedBytes, _ := hex.DecodeString(compiled[v]) // 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. + fmt.Println(hex.EncodeToString(ops.Program)) + fmt.Println(compiled[v]) require.Equal(t, expectedBytes, ops.Program, hex.EncodeToString(ops.Program)) }) } @@ -1431,6 +1439,7 @@ txna ClearStateProgramPages 0 } ops := testProg(t, text, AssemblerMaxVersion) t2, err := Disassemble(ops.Program) + fmt.Println(t2) require.Equal(t, text, t2) require.NoError(t, err) } diff --git a/data/transactions/logic/evalStateful_test.go b/data/transactions/logic/evalStateful_test.go index e513172519..1652702dfb 100644 --- a/data/transactions/logic/evalStateful_test.go +++ b/data/transactions/logic/evalStateful_test.go @@ -2436,6 +2436,8 @@ func TestReturnTypes(t *testing.T) { cmd += " 0x12 0x34 0x56" case immLabel: cmd += " done; done: ;" + case immLabels: + cmd += " done1 done2; done1: ; done2: ;" default: require.Fail(t, "bad immediate", "%s", imm) } diff --git a/data/transactions/logic/opcodes.go b/data/transactions/logic/opcodes.go index a9a7c03d6d..1cc731a66a 100644 --- a/data/transactions/logic/opcodes.go +++ b/data/transactions/logic/opcodes.go @@ -189,7 +189,7 @@ func detSwitch() OpDetails { d.asm = asmSwitch d.check = checkSwitch d.Size = 0 - d.Immediates = []immediate{imm("uint", immInt), imm("target ...", immLabels)} + d.Immediates = []immediate{imm("target ...", immLabels)} return d } From 2ac78901ee34887a741b7287276f7fa92ceb83b1 Mon Sep 17 00:00:00 2001 From: algoidurovic <91566643+algoidurovic@users.noreply.github.com> Date: Thu, 8 Sep 2022 15:26:27 -0400 Subject: [PATCH 06/19] add tests --- data/transactions/logic/assembler_test.go | 4 +-- data/transactions/logic/doc.go | 6 +++-- data/transactions/logic/eval.go | 3 +++ data/transactions/logic/eval_test.go | 30 +++++++++++++++++++++++ 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/data/transactions/logic/assembler_test.go b/data/transactions/logic/assembler_test.go index d3116224f8..c3bcfd7187 100644 --- a/data/transactions/logic/assembler_test.go +++ b/data/transactions/logic/assembler_test.go @@ -408,7 +408,7 @@ const randomnessCompiled = "81ffff03d101d000" const v7Compiled = v6Compiled + "5e005f018120af060180070123456789abcd49490501988003012345494984" + randomnessCompiled + "800243218001775c0280018881015d" -const v8Compiled = v7Compiled + pairingCompiled +const v8Compiled = v7Compiled + pairingCompiled + "8101e002fff800008101" var nonsense = map[uint64]string{ 1: v1Nonsense, @@ -473,8 +473,6 @@ func TestAssemble(t *testing.T) { expectedBytes, _ := hex.DecodeString(compiled[v]) // 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. - fmt.Println(hex.EncodeToString(ops.Program)) - fmt.Println(compiled[v]) require.Equal(t, expectedBytes, ops.Program, hex.EncodeToString(ops.Program)) }) } diff --git a/data/transactions/logic/doc.go b/data/transactions/logic/doc.go index 8f3a3bc2ff..add9d23368 100644 --- a/data/transactions/logic/doc.go +++ b/data/transactions/logic/doc.go @@ -194,7 +194,7 @@ var opDocByName = map[string]string{ "vrf_verify": "Verify the proof B of message A against pubkey C. Returns vrf output and verification flag.", "block": "field F of block A. Fail unless A falls between txn.LastValid-1002 and txn.FirstValid (exclusive)", - "switchi": "branch to target at index A. Fail if index A is out of bound.", + "switchi": "branch to target at index A. Fail if index A is out of bounds.", } // OpDoc returns a description of the op @@ -263,6 +263,8 @@ var opcodeImmediateNotes = map[string]string{ "vrf_verify": "{uint8 parameters index}", "block": "{uint8 block field}", + + "switchi": "{varuint length} [{int16 branch offset, big-endian}, ...]", } // OpImmediateNote returns a short string about immediate data which follows the op byte @@ -341,7 +343,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"}, + "Flow Control": {"err", "bnz", "bz", "b", "return", "pop", "dup", "dup2", "dig", "cover", "uncover", "swap", "select", "assert", "callsub", "retsub", "switchi"}, "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/eval.go b/data/transactions/logic/eval.go index eaed8e11b6..2f2a4a839c 100644 --- a/data/transactions/logic/eval.go +++ b/data/transactions/logic/eval.go @@ -2052,6 +2052,9 @@ func checkSwitch(cx *EvalContext) error { } cx.branchTargets[target] = true } + + // this opcode's size is dynamic so nextpc must be set here + cx.nextpc = cx.pc + opSize return nil } diff --git a/data/transactions/logic/eval_test.go b/data/transactions/logic/eval_test.go index 19970acfb3..4335701b76 100644 --- a/data/transactions/logic/eval_test.go +++ b/data/transactions/logic/eval_test.go @@ -5515,3 +5515,33 @@ func TestTypeComplaints(t *testing.T) { testProg(t, "err; store 0", AssemblerMaxVersion) testProg(t, "int 1; return; store 0", AssemblerMaxVersion) } + +func TestSwitchInt(t *testing.T) { + partitiontest.PartitionTest(t) + + t.Parallel() + testAccepts(t, ` +int 0 +start: +int 1 ++ +dup +int 1 +- +switchi 2 start end +err +end: +pop +int 1 +`, 8) + + // test code fails when target index is out of bounds + testPanics(t, ` +int 2 +switchi 2 start end +err +start: +end: +int 1 +`, 8) +} From e35f14a623cd8201ea8a6654f0bd50b4abd1f8b7 Mon Sep 17 00:00:00 2001 From: algoidurovic <91566643+algoidurovic@users.noreply.github.com> Date: Thu, 8 Sep 2022 17:24:38 -0400 Subject: [PATCH 07/19] assemble test --- data/transactions/logic/assembler_test.go | 66 +++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/data/transactions/logic/assembler_test.go b/data/transactions/logic/assembler_test.go index c3bcfd7187..c08a0fe32f 100644 --- a/data/transactions/logic/assembler_test.go +++ b/data/transactions/logic/assembler_test.go @@ -17,6 +17,7 @@ package logic import ( + "encoding/binary" "encoding/hex" "fmt" "strings" @@ -2644,3 +2645,68 @@ func TestSemiColon(t *testing.T) { `byte "test;this";;;pop;`, ) } + +func TestAssembleSwitch(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + // fail when wrong number of targets are present + source := ` + pushint 1 + switchi 2 label1 label2 label3 + label1: + label2: + label3: + ` + testProg(t, source, 8, NewExpect(3, "switch operation requires 2 labels but contains 3")) + + // fail when target doesn't correspond to existing label + source = ` + pushint 1 + switchi 2 label1 label2 + label1: + ` + testProg(t, source, 8, NewExpect(3, "reference to undefined label \"label2\"")) + + // confirm size of varuint list size + source = ` + pushint 1 + switchi 2 label1 label2 + label1: + label2: + ` + ops, err := AssembleStringWithVersion(source, 8) + require.NoError(t, err) + val, bytesUsed := binary.Uvarint(ops.Program[4:]) + require.Equal(t, uint64(2), val) + require.Equal(t, 1, bytesUsed) + + var labelReferences []string + for i := 0; i < (1 << 9); i++ { + labelReferences = append(labelReferences, fmt.Sprintf("label%d", i)) + } + + var labels []string + for i := 0; i < (1 << 9); i++ { + labels = append(labels, fmt.Sprintf("label%d:", i)) + } + + source = fmt.Sprintf(` + pushint 1 + switchi %d %s + %s + `, (1 << 9), strings.Join(labelReferences, " "), strings.Join(labels, "\n")) + ops, err = AssembleStringWithVersion(source, 8) + require.NoError(t, err) + val, bytesUsed = binary.Uvarint(ops.Program[4:]) + require.Equal(t, uint64(1<<9), val) + require.Equal(t, 2, bytesUsed) + + // allow duplicate label reference + source = ` + pushint 1 + switchi 2 label1 label1 + label1: + ` + testProg(t, source, 8) +} From bf2c849ae32b58df621847cce91e8e21a48471d0 Mon Sep 17 00:00:00 2001 From: algoidurovic <91566643+algoidurovic@users.noreply.github.com> Date: Fri, 9 Sep 2022 11:16:07 -0400 Subject: [PATCH 08/19] fix doc formatting and failing test --- data/transactions/logic/TEAL_opcodes.md | 2 +- data/transactions/logic/evalStateful_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/transactions/logic/TEAL_opcodes.md b/data/transactions/logic/TEAL_opcodes.md index 74e18330c9..5a74c625e2 100644 --- a/data/transactions/logic/TEAL_opcodes.md +++ b/data/transactions/logic/TEAL_opcodes.md @@ -1418,4 +1418,4 @@ The notation A,B indicates that A and B are interpreted as a uint128 value, with - branch unconditionally to TARGET[A]. Fail if A is less than `n-1` - Availability: v8 -The `switchi` instruction opcode 0xe0 is followed by `n`, the number of targets, each of which are encoded as 2 byte values indicating the position of the target label relative to the end of the `switchi` instruction (i.e. the offset). The last element on the stack represents the index of the target to branch to. If the index is greater than or equal to n, the evaluation will fail. Otherwise, the program will branch to `pc + 1 + sizeof(n) + 2 * n + target[index]`. Branch targets must be aligned instructions. (e.g. Branching to the second byte of a 2 byte op will be rejected.) \ No newline at end of file +The `switchi` instruction opcode 0xe0 is followed by `n`, the number of targets, each of which are encoded as 2 byte values indicating the position of the target label relative to the end of the `switchi` instruction (i.e. the offset). The last element on the stack represents the index of the target to branch to. If the index is greater than or equal to n, the evaluation will fail. Otherwise, the program will branch to `pc + 1 + sizeof(n) + 2 * n + target[index]`. Branch targets must be aligned instructions. (e.g. Branching to the second byte of a 2 byte op will be rejected.) diff --git a/data/transactions/logic/evalStateful_test.go b/data/transactions/logic/evalStateful_test.go index 1652702dfb..07be787c01 100644 --- a/data/transactions/logic/evalStateful_test.go +++ b/data/transactions/logic/evalStateful_test.go @@ -2437,7 +2437,7 @@ func TestReturnTypes(t *testing.T) { case immLabel: cmd += " done; done: ;" case immLabels: - cmd += " done1 done2; done1: ; done2: ;" + cmd += " 2 done1 done2; done1: ; done2: ;" default: require.Fail(t, "bad immediate", "%s", imm) } From 7e6014f9fde433c4ff00c5cf4e958e22f75489c8 Mon Sep 17 00:00:00 2001 From: algoidurovic <91566643+algoidurovic@users.noreply.github.com> Date: Fri, 9 Sep 2022 11:52:19 -0400 Subject: [PATCH 09/19] run make for doc generation, and extract offset decoding into dedicated function --- data/transactions/logic/README.md | 1 - data/transactions/logic/TEAL_opcodes.md | 8 -------- data/transactions/logic/assembler.go | 8 +++----- data/transactions/logic/eval.go | 17 +++++++++++------ data/transactions/logic/teal.tmLanguage.json | 2 +- 5 files changed, 15 insertions(+), 21 deletions(-) diff --git a/data/transactions/logic/README.md b/data/transactions/logic/README.md index 5c2f5715a2..6c68a8a7c9 100644 --- a/data/transactions/logic/README.md +++ b/data/transactions/logic/README.md @@ -594,7 +594,6 @@ Account fields used in the `acct_params_get` opcode. | `assert` | immediately fail unless A is a non-zero number | | `callsub target` | branch unconditionally to TARGET, saving the next instruction on the call stack | | `retsub` | pop the top instruction from the call stack and branch to it | -| `switchi n target[0] ... target[n-1]` | branch unconditionally to TARGET[A] | ### State Access diff --git a/data/transactions/logic/TEAL_opcodes.md b/data/transactions/logic/TEAL_opcodes.md index 5a74c625e2..6c5d679108 100644 --- a/data/transactions/logic/TEAL_opcodes.md +++ b/data/transactions/logic/TEAL_opcodes.md @@ -1411,11 +1411,3 @@ The notation A,B indicates that A and B are interpreted as a uint128 value, with | 0 | BlkSeed | []byte | | | 1 | BlkTimestamp | uint64 | | -## switchi n target[0] ... target[n-1] - -- Opcode: 0xe0 {varuint length} [{int16 branch offset big-endian}, ...] -- Stack: ..., A: uint64 → ... -- branch unconditionally to TARGET[A]. Fail if A is less than `n-1` -- Availability: v8 - -The `switchi` instruction opcode 0xe0 is followed by `n`, the number of targets, each of which are encoded as 2 byte values indicating the position of the target label relative to the end of the `switchi` instruction (i.e. the offset). The last element on the stack represents the index of the target to branch to. If the index is greater than or equal to n, the evaluation will fail. Otherwise, the program will branch to `pc + 1 + sizeof(n) + 2 * n + target[index]`. Branch targets must be aligned instructions. (e.g. Branching to the second byte of a 2 byte op will be rejected.) diff --git a/data/transactions/logic/assembler.go b/data/transactions/logic/assembler.go index 3db1cd870b..9d4242e417 100644 --- a/data/transactions/logic/assembler.go +++ b/data/transactions/logic/assembler.go @@ -2326,8 +2326,8 @@ func disassemble(dis *disassembleState, spec *OpSpec) (string, error) { pc++ case immLabel: - offset := int16((uint(dis.program[pc]) << 8) | uint(dis.program[pc+1])) - target := int(offset) + pc + 2 + offset := decodeBranchOffset(dis.program, pc) + target := offset + pc + 2 var label string if dis.numericTargets { label = fmt.Sprintf("%d", target) @@ -2573,7 +2573,6 @@ func parseSwitch(program []byte, pos int) (targets []int, nextpc int, err error) err = fmt.Errorf("could not decode switch target list size at pc=%d", pos) return } - pc := pos pos += bytesUsed if numOffsets > uint64(len(program)) { err = errTooManyItems @@ -2582,11 +2581,10 @@ func parseSwitch(program []byte, pos int) (targets []int, nextpc int, err error) end := pos + int(2*numOffsets) // end of op: offset is applied to this position for i := 0; i < int(numOffsets); i++ { - offset := int16(uint16(program[pos])<<8 | uint16(program[pos+1])) + offset := decodeBranchOffset(program, pos) target := int(offset) + int(end) targets = append(targets, target) pos += 2 - fmt.Println(fmt.Sprintf("%d %d %d", pc, pos, target)) } nextpc = pos return diff --git a/data/transactions/logic/eval.go b/data/transactions/logic/eval.go index 6682ba9c96..5b8ab61920 100644 --- a/data/transactions/logic/eval.go +++ b/data/transactions/logic/eval.go @@ -1965,12 +1965,17 @@ func opArgs(cx *EvalContext) error { return opArgN(cx, n) } +func decodeBranchOffset(program []byte, pos int) int { + // tricky casting to preserve signed value + return int(int16(program[pos])<<8 | int16(program[pos+1])) +} + func branchTarget(cx *EvalContext) (int, error) { - offset := int16(uint16(cx.program[cx.pc+1])<<8 | uint16(cx.program[cx.pc+2])) + offset := decodeBranchOffset(cx.program, cx.pc+1) if offset < 0 && cx.version < backBranchEnabledVersion { return 0, fmt.Errorf("negative branch offset %x", offset) } - target := cx.pc + 3 + int(offset) + target := cx.pc + 3 + offset var branchTooFar bool if cx.version >= 2 { // branching to exactly the end of the program (target == len(cx.program)), the next pc after the last instruction, is okay and ends normally @@ -1994,13 +1999,13 @@ func switchTarget(cx *EvalContext, branchIdx uint64) (int, uint64, error) { return 0, 0, fmt.Errorf("provided branch index %d exceeds max offset index %d", branchIdx, numOffsets-1) } - end := cx.pc + 1 + bytesUsed // end of opcode + number of offsets, beginning of offset list - pos := uint64(end) + (2 * branchIdx) // position of referenced offset: each offset is 2 bytes - if pos >= uint64(len(cx.program)-1) { + end := cx.pc + 1 + bytesUsed // end of opcode + number of offsets, beginning of offset list + pos := end + int(2*branchIdx) // position of referenced offset: each offset is 2 bytes + if pos >= len(cx.program)-1 { return 0, 0, fmt.Errorf("invalid byte code: expected offset value but reached end of program") } - offset := int16(uint16(cx.program[pos])<<8 | uint16(cx.program[pos+1])) + offset := decodeBranchOffset(cx.program, pos) target := end + 2*int(numOffsets) + int(offset) // offset is applied to the end of this opcode // branching to exactly the end of the program (target == len(cx.program)), the next pc after the last instruction, diff --git a/data/transactions/logic/teal.tmLanguage.json b/data/transactions/logic/teal.tmLanguage.json index 127f129619..1f2925f983 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|uncover)\\b" + "match": "^(assert|b|bnz|bz|callsub|cover|dig|dup|dup2|err|pop|retsub|return|select|swap|switchi|uncover)\\b" }, { "name": "keyword.other.teal", From b9b417a204e5284ac216584ce191d8564b0ecf42 Mon Sep 17 00:00:00 2001 From: algoidurovic <91566643+algoidurovic@users.noreply.github.com> Date: Fri, 9 Sep 2022 17:26:11 -0400 Subject: [PATCH 10/19] touch up --- data/transactions/logic/assembler.go | 17 ++++----------- data/transactions/logic/assembler_test.go | 25 +++++++---------------- data/transactions/logic/doc.go | 1 + data/transactions/logic/eval_test.go | 4 ++-- 4 files changed, 14 insertions(+), 33 deletions(-) diff --git a/data/transactions/logic/assembler.go b/data/transactions/logic/assembler.go index 9d4242e417..9b32925c2b 100644 --- a/data/transactions/logic/assembler.go +++ b/data/transactions/logic/assembler.go @@ -915,22 +915,14 @@ func asmBranch(ops *OpStream, spec *OpSpec, args []string) error { } func asmSwitch(ops *OpStream, spec *OpSpec, args []string) error { - numOffsets, err := strconv.ParseUint(args[0], 0, 64) - if err != nil { - return err - } - - if len(args)-1 != int(numOffsets) { - return ops.errorf("switch operation requires %d labels but contains %d", numOffsets, len(args)-1) - } - + numOffsets := uint64(len(args)) ops.pending.WriteByte(spec.Opcode) var scratch [binary.MaxVarintLen64]byte - vlen := binary.PutUvarint(scratch[:], numOffsets) + vlen := binary.PutUvarint(scratch[:], uint64(len(args))) ops.pending.Write(scratch[:vlen]) opEndPos := ops.pending.Len() + 2*int(numOffsets) - for i := 1; i <= int(numOffsets); i++ { - ops.referToLabel(ops.pending.Len(), args[i], opEndPos) + for _, arg := range args { + ops.referToLabel(ops.pending.Len(), arg, opEndPos) // zero bytes will get replaced with actual offset in resolveLabels() ops.pending.WriteByte(0) ops.pending.WriteByte(0) @@ -2411,7 +2403,6 @@ func disassemble(dis *disassembleState, spec *OpSpec) (string, error) { } labels = append(labels, label) } - out += strconv.Itoa(len(targets)) + " " out += strings.Join(labels, " ") pc = nextpc default: diff --git a/data/transactions/logic/assembler_test.go b/data/transactions/logic/assembler_test.go index bf918f5f42..cfbec0e15b 100644 --- a/data/transactions/logic/assembler_test.go +++ b/data/transactions/logic/assembler_test.go @@ -398,7 +398,7 @@ replace3 const v8Nonsense = v7Nonsense + pairingNonsense + ` switch_label0: pushint 1 -switchi 2 switch_label0 switch_label1 +switchi switch_label0 switch_label1 switch_label1: pushint 1 ` @@ -1558,7 +1558,6 @@ txna ClearStateProgramPages 0 } ops := testProg(t, text, AssemblerMaxVersion) t2, err := Disassemble(ops.Program) - fmt.Println(t2) require.Equal(t, text, t2) require.NoError(t, err) } @@ -2772,20 +2771,10 @@ func TestAssembleSwitch(t *testing.T) { partitiontest.PartitionTest(t) t.Parallel() - // fail when wrong number of targets are present - source := ` - pushint 1 - switchi 2 label1 label2 label3 - label1: - label2: - label3: - ` - testProg(t, source, 8, NewExpect(3, "switch operation requires 2 labels but contains 3")) - // fail when target doesn't correspond to existing label - source = ` + source := ` pushint 1 - switchi 2 label1 label2 + switchi label1 label2 label1: ` testProg(t, source, 8, NewExpect(3, "reference to undefined label \"label2\"")) @@ -2793,7 +2782,7 @@ func TestAssembleSwitch(t *testing.T) { // confirm size of varuint list size source = ` pushint 1 - switchi 2 label1 label2 + switchi label1 label2 label1: label2: ` @@ -2815,9 +2804,9 @@ func TestAssembleSwitch(t *testing.T) { source = fmt.Sprintf(` pushint 1 - switchi %d %s + switchi %s %s - `, (1 << 9), strings.Join(labelReferences, " "), strings.Join(labels, "\n")) + `, strings.Join(labelReferences, " "), strings.Join(labels, "\n")) ops, err = AssembleStringWithVersion(source, 8) require.NoError(t, err) val, bytesUsed = binary.Uvarint(ops.Program[4:]) @@ -2827,7 +2816,7 @@ func TestAssembleSwitch(t *testing.T) { // allow duplicate label reference source = ` pushint 1 - switchi 2 label1 label1 + switchi label1 label1 label1: ` testProg(t, source, 8) diff --git a/data/transactions/logic/doc.go b/data/transactions/logic/doc.go index add9d23368..848fb9f26f 100644 --- a/data/transactions/logic/doc.go +++ b/data/transactions/logic/doc.go @@ -327,6 +327,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.", + "switchi": "The `switchi` instruction opcode 0xe0 is followed by `n`, the number of targets, each of which are encoded as 2 byte values indicating the position of the target label relative to the end of the `switchi` instruction (i.e. the offset). The last element on the stack represents the index of the target to branch to. If the index is greater than or equal to n, the evaluation will fail. Otherwise, the program will branch to `pc + 1 + sizeof(n) + 2 * n + target[index]`. Branch targets must be aligned instructions. (e.g. Branching to the second byte of a 2 byte op will be rejected.)", } // OpDocExtra returns extra documentation text about an op diff --git a/data/transactions/logic/eval_test.go b/data/transactions/logic/eval_test.go index 32d05a8402..f3164ec575 100644 --- a/data/transactions/logic/eval_test.go +++ b/data/transactions/logic/eval_test.go @@ -5540,7 +5540,7 @@ int 1 dup int 1 - -switchi 2 start end +switchi start end err end: pop @@ -5550,7 +5550,7 @@ int 1 // test code fails when target index is out of bounds testPanics(t, ` int 2 -switchi 2 start end +switchi start end err start: end: From 933df408444e51260e6aa85798974e743ba29804 Mon Sep 17 00:00:00 2001 From: algoidurovic <91566643+algoidurovic@users.noreply.github.com> Date: Fri, 9 Sep 2022 17:49:14 -0400 Subject: [PATCH 11/19] update tests --- data/transactions/logic/evalStateful_test.go | 2 +- data/transactions/logic/eval_test.go | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/data/transactions/logic/evalStateful_test.go b/data/transactions/logic/evalStateful_test.go index 07be787c01..1652702dfb 100644 --- a/data/transactions/logic/evalStateful_test.go +++ b/data/transactions/logic/evalStateful_test.go @@ -2437,7 +2437,7 @@ func TestReturnTypes(t *testing.T) { case immLabel: cmd += " done; done: ;" case immLabels: - cmd += " 2 done1 done2; done1: ; done2: ;" + cmd += " done1 done2; done1: ; done2: ;" default: require.Fail(t, "bad immediate", "%s", imm) } diff --git a/data/transactions/logic/eval_test.go b/data/transactions/logic/eval_test.go index f3164ec575..06ec2bb775 100644 --- a/data/transactions/logic/eval_test.go +++ b/data/transactions/logic/eval_test.go @@ -5543,7 +5543,9 @@ int 1 switchi start end err end: -pop +int 2 +== +assert int 1 `, 8) @@ -5551,7 +5553,6 @@ int 1 testPanics(t, ` int 2 switchi start end -err start: end: int 1 From 9d1f0dec2889f433b8e59748428ccda6534ce950 Mon Sep 17 00:00:00 2001 From: John Jannotti Date: Wed, 14 Sep 2022 21:21:16 -0400 Subject: [PATCH 12/19] Allow fall-through in switch Moved opcode number to be by other "strange jumps" Added simple tests for normal cases, and for fallthrough Added some overflow protection from handcrafted bytecode by limiting switch labels to 2^16. --- data/transactions/logic/assembler.go | 7 ++-- data/transactions/logic/assembler_test.go | 32 ++++++++++----- data/transactions/logic/eval.go | 26 ++++++------ data/transactions/logic/eval_test.go | 50 +++++++++++++++++++---- data/transactions/logic/opcodes.go | 7 +--- 5 files changed, 85 insertions(+), 37 deletions(-) diff --git a/data/transactions/logic/assembler.go b/data/transactions/logic/assembler.go index 9b32925c2b..53cc64053b 100644 --- a/data/transactions/logic/assembler.go +++ b/data/transactions/logic/assembler.go @@ -27,6 +27,7 @@ import ( "errors" "fmt" "io" + "math" "sort" "strconv" "strings" @@ -2565,15 +2566,15 @@ func parseSwitch(program []byte, pos int) (targets []int, nextpc int, err error) return } pos += bytesUsed - if numOffsets > uint64(len(program)) { - err = errTooManyItems + if numOffsets > math.MaxUint16 { + err = errors.New("switch with too many labels") return } end := pos + int(2*numOffsets) // end of op: offset is applied to this position for i := 0; i < int(numOffsets); i++ { offset := decodeBranchOffset(program, pos) - target := int(offset) + int(end) + target := end + offset targets = append(targets, target) pos += 2 } diff --git a/data/transactions/logic/assembler_test.go b/data/transactions/logic/assembler_test.go index cfbec0e15b..a4bc01738c 100644 --- a/data/transactions/logic/assembler_test.go +++ b/data/transactions/logic/assembler_test.go @@ -410,7 +410,7 @@ const randomnessCompiled = "81ffff03d101d000" const v7Compiled = v6Compiled + "5e005f018120af060180070123456789abcd49490501988003012345494984" + randomnessCompiled + "800243218001775c0280018881015d" -const v8Compiled = v7Compiled + pairingCompiled + "8101e002fff800008101" +const v8Compiled = v7Compiled + pairingCompiled + "81018a02fff800008101" var nonsense = map[uint64]string{ 1: v1Nonsense, @@ -517,16 +517,21 @@ type Expect struct { s string } -func testMatch(t testing.TB, actual, expected string) bool { +func testMatch(t testing.TB, actual, expected string) (ok bool) { + defer func() { + if !ok { + t.Logf("'%s' does not match '%s'", actual, expected) + } + }() t.Helper() if strings.HasPrefix(expected, "...") && strings.HasSuffix(expected, "...") { - return assert.Contains(t, actual, expected[3:len(expected)-3]) + return strings.Contains(actual, expected[3:len(expected)-3]) } else if strings.HasPrefix(expected, "...") { - return assert.Contains(t, actual+"^", expected[3:]+"^") + return strings.Contains(actual+"^", expected[3:]+"^") } else if strings.HasSuffix(expected, "...") { - return assert.Contains(t, "^"+actual, "^"+expected[:len(expected)-3]) + return strings.Contains("^"+actual, "^"+expected[:len(expected)-3]) } else { - return assert.Equal(t, expected, actual) + return expected == actual } } @@ -597,13 +602,13 @@ func testProg(t testing.TB, source string, ver uint64, expected ...Expect) *OpSt errors := ops.Errors for _, exp := range expected { if exp.l == 0 { - // line 0 means: "must match all" + // line 0 means: "must match some line" require.Len(t, expected, 1) - fail := false + fail := true for _, err := range errors { msg := err.Unwrap().Error() - if !testMatch(t, msg, exp.s) { - fail = true + if testMatch(t, msg, exp.s) { + fail = false } } if fail { @@ -2779,6 +2784,13 @@ func TestAssembleSwitch(t *testing.T) { ` testProg(t, source, 8, NewExpect(3, "reference to undefined label \"label2\"")) + // No labels is pretty degenerate, but ok, I suppose. It's just a no-op + testProg(t, ` +int 0 +switchi +int 1 +`, 8) + // confirm size of varuint list size source = ` pushint 1 diff --git a/data/transactions/logic/eval.go b/data/transactions/logic/eval.go index 5b8ab61920..fa808ab012 100644 --- a/data/transactions/logic/eval.go +++ b/data/transactions/logic/eval.go @@ -1992,21 +1992,24 @@ func branchTarget(cx *EvalContext) (int, error) { func switchTarget(cx *EvalContext, branchIdx uint64) (int, uint64, error) { numOffsets, bytesUsed := binary.Uvarint(cx.program[cx.pc+1:]) - if numOffsets <= 0 { + if bytesUsed <= 0 || numOffsets > math.MaxUint16 { return 0, 0, fmt.Errorf("could not decode switch label count at pc=%d", cx.pc+1) } + + end := cx.pc + 1 + bytesUsed // end of opcode + number of offsets, beginning of offset list + eoi := end + 2*int(numOffsets) // end of instruction if branchIdx >= numOffsets { - return 0, 0, fmt.Errorf("provided branch index %d exceeds max offset index %d", branchIdx, numOffsets-1) + return eoi, numOffsets, nil } - end := cx.pc + 1 + bytesUsed // end of opcode + number of offsets, beginning of offset list + // 2*branchIdx will be no where near overflow b/c branchIdx < numOffsets < math.MaxUint16 pos := end + int(2*branchIdx) // position of referenced offset: each offset is 2 bytes - if pos >= len(cx.program)-1 { - return 0, 0, fmt.Errorf("invalid byte code: expected offset value but reached end of program") + if pos >= len(cx.program)-1 { // Need two bytes to decode an offset + return 0, 0, fmt.Errorf("offset position %d is beyond program", pos) } offset := decodeBranchOffset(cx.program, pos) - target := end + 2*int(numOffsets) + int(offset) // offset is applied to the end of this opcode + target := eoi + offset // offset is applied to the end of this opcode // branching to exactly the end of the program (target == len(cx.program)), the next pc after the last instruction, // is okay and ends normally @@ -2035,21 +2038,20 @@ func checkBranch(cx *EvalContext) error { // checks any switch that is {op} {varuint offset index} [{int16 offset}...] func checkSwitch(cx *EvalContext) error { - // first call to get the number of offsets, 0 is a safe choice because there must exist at least one label - _, numOffsets, err := switchTarget(cx, 0) + // call with too big of an index. the end-of-instruction PC is returned because + // that's where an out of range branchIdx goes + eoi, numOffsets, err := switchTarget(cx, math.MaxUint64) if err != nil { return err } - _, bytesUsed := binary.Uvarint(cx.program[cx.pc+1:]) // decoding the value will work because switchTarget() above already checked - opSize := 1 + bytesUsed + 2*int(numOffsets) for branchIdx := uint64(0); branchIdx < numOffsets; branchIdx++ { target, _, err := switchTarget(cx, branchIdx) if err != nil { return err } - if target < cx.pc+opSize { + if target < eoi { // If a branch goes backwards, we should have already noted that an instruction began at that location. if _, ok := cx.instructionStarts[target]; !ok { return fmt.Errorf("back branch target %d is not an aligned instruction", target) @@ -2059,7 +2061,7 @@ func checkSwitch(cx *EvalContext) error { } // this opcode's size is dynamic so nextpc must be set here - cx.nextpc = cx.pc + opSize + cx.nextpc = eoi return nil } diff --git a/data/transactions/logic/eval_test.go b/data/transactions/logic/eval_test.go index 06ec2bb775..ac41772e00 100644 --- a/data/transactions/logic/eval_test.go +++ b/data/transactions/logic/eval_test.go @@ -5530,8 +5530,44 @@ func TestTypeComplaints(t *testing.T) { func TestSwitchInt(t *testing.T) { partitiontest.PartitionTest(t) - t.Parallel() + + // take the 0th label + testAccepts(t, ` +int 0 +switchi zero one +err +zero: int 1; return +one: int 0; +`, 8) + + // take the 1th label + testRejects(t, ` +int 1 +switchi zero one +err +zero: int 1; return +one: int 0; +`, 8) + + // same, but jumping to end of program + testAccepts(t, ` +int 1; dup +switchi zero one +zero: err +one: +`, 8) + + // no match + testAccepts(t, ` +int 2 +switchi zero one +int 1; return // falls through to here +zero: int 0; return +one: int 0; return +`, 8) + + // jump forward and backward testAccepts(t, ` int 0 start: @@ -5549,12 +5585,12 @@ assert int 1 `, 8) - // test code fails when target index is out of bounds - testPanics(t, ` -int 2 -switchi start end -start: -end: + // 0 labels are allowed, but weird! + testAccepts(t, ` +int 0 +switchi int 1 `, 8) + + testPanics(t, notrack("switchi; int 1"), 8) } diff --git a/data/transactions/logic/opcodes.go b/data/transactions/logic/opcodes.go index 1cc731a66a..50802b307f 100644 --- a/data/transactions/logic/opcodes.go +++ b/data/transactions/logic/opcodes.go @@ -544,7 +544,8 @@ var OpSpecs = []OpSpec{ // "Function oriented" {0x88, "callsub", opCallSub, proto(":"), 4, detBranch()}, {0x89, "retsub", opRetSub, proto(":"), 4, detDefault()}, - // Leave a little room for indirect function calls, or similar + {0x8a, "switchi", opSwitchInt, proto("i:"), 8, detSwitch()}, + // 0x8b will likely be a switch on pairs of values/targets // More math {0x90, "shl", opShiftLeft, proto("ii:i"), 4, detDefault()}, @@ -563,7 +564,6 @@ var OpSpecs = []OpSpec{ {0x99, "bn256_add", opBn256Add, proto("bb:b"), pairingVersion, costly(70)}, {0x9a, "bn256_scalar_mul", opBn256ScalarMul, proto("bb:b"), pairingVersion, costly(970)}, {0x9b, "bn256_pairing", opBn256Pairing, proto("bb:i"), pairingVersion, costly(8700)}, - // leave room here for eip-2537 style opcodes // Byteslice math. {0xa0, "b+", opBytesPlus, proto("bb:b"), 4, costly(10)}, @@ -606,9 +606,6 @@ var OpSpecs = []OpSpec{ // randomness support {0xd0, "vrf_verify", opVrfVerify, proto("bbb:bi"), randomnessVersion, field("s", &VrfStandards).costs(5700)}, {0xd1, "block", opBlock, proto("i:a"), randomnessVersion, field("f", &BlockFields)}, - - // switch on value - {0xe0, "switchi", opSwitchInt, proto("i:"), 8, detSwitch()}, } type sortByOpcode []OpSpec From c0fa16ca2bb2aa67e43507653a1c5ef8e4b9afa3 Mon Sep 17 00:00:00 2001 From: michaeldiamant Date: Thu, 15 Sep 2022 10:37:56 -0400 Subject: [PATCH 13/19] Replace hard-coded version with AssemblerMaxVersion in assembler_test.go --- data/transactions/logic/assembler_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/data/transactions/logic/assembler_test.go b/data/transactions/logic/assembler_test.go index a4bc01738c..c68fe7be77 100644 --- a/data/transactions/logic/assembler_test.go +++ b/data/transactions/logic/assembler_test.go @@ -2782,14 +2782,14 @@ func TestAssembleSwitch(t *testing.T) { switchi label1 label2 label1: ` - testProg(t, source, 8, NewExpect(3, "reference to undefined label \"label2\"")) + testProg(t, source, AssemblerMaxVersion, NewExpect(3, "reference to undefined label \"label2\"")) // No labels is pretty degenerate, but ok, I suppose. It's just a no-op testProg(t, ` int 0 switchi int 1 -`, 8) +`, AssemblerMaxVersion) // confirm size of varuint list size source = ` @@ -2798,7 +2798,7 @@ int 1 label1: label2: ` - ops, err := AssembleStringWithVersion(source, 8) + ops, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) val, bytesUsed := binary.Uvarint(ops.Program[4:]) require.Equal(t, uint64(2), val) @@ -2819,7 +2819,7 @@ int 1 switchi %s %s `, strings.Join(labelReferences, " "), strings.Join(labels, "\n")) - ops, err = AssembleStringWithVersion(source, 8) + ops, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) val, bytesUsed = binary.Uvarint(ops.Program[4:]) require.Equal(t, uint64(1<<9), val) @@ -2831,5 +2831,5 @@ int 1 switchi label1 label1 label1: ` - testProg(t, source, 8) + testProg(t, source, AssemblerMaxVersion) } From 76b87f51082335a137823a80621a51c2f6a0d62b Mon Sep 17 00:00:00 2001 From: michaeldiamant Date: Thu, 15 Sep 2022 10:44:48 -0400 Subject: [PATCH 14/19] Update docs for switchi fall-through behavior --- data/transactions/logic/doc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/transactions/logic/doc.go b/data/transactions/logic/doc.go index 848fb9f26f..379d746b4c 100644 --- a/data/transactions/logic/doc.go +++ b/data/transactions/logic/doc.go @@ -327,7 +327,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.", - "switchi": "The `switchi` instruction opcode 0xe0 is followed by `n`, the number of targets, each of which are encoded as 2 byte values indicating the position of the target label relative to the end of the `switchi` instruction (i.e. the offset). The last element on the stack represents the index of the target to branch to. If the index is greater than or equal to n, the evaluation will fail. Otherwise, the program will branch to `pc + 1 + sizeof(n) + 2 * n + target[index]`. Branch targets must be aligned instructions. (e.g. Branching to the second byte of a 2 byte op will be rejected.)", + "switchi": "The `switchi` instruction opcode is followed by `n`, the number of targets, each of which are encoded as 2 byte values indicating the position of the target label relative to the end of the `switchi` instruction (i.e. the offset). The last element on the stack represents the index of the target to branch to. If the index is greater than or equal to n, then evaluation falls through to the next instruction. Otherwise, the program will branch to `pc + 1 + sizeof(n) + 2 * n + target[index]`. Branch targets must be aligned instructions. (e.g. Branching to the second byte of a 2 byte op will be rejected.)", } // OpDocExtra returns extra documentation text about an op From d78965e04d54f368e79fb82ce88b041c066e959d Mon Sep 17 00:00:00 2001 From: michaeldiamant Date: Thu, 15 Sep 2022 11:14:33 -0400 Subject: [PATCH 15/19] Add switchi assembly test case --- data/transactions/logic/assembler_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/data/transactions/logic/assembler_test.go b/data/transactions/logic/assembler_test.go index c68fe7be77..54ad11a006 100644 --- a/data/transactions/logic/assembler_test.go +++ b/data/transactions/logic/assembler_test.go @@ -2784,6 +2784,13 @@ func TestAssembleSwitch(t *testing.T) { ` testProg(t, source, AssemblerMaxVersion, NewExpect(3, "reference to undefined label \"label2\"")) + // fail when target index != uint64 + testProg(t, ` + byte "fail" + switchi label1 + labe11: + `, AssemblerMaxVersion, Expect{3, "switchi label1 arg 0 wanted type uint64..."}) + // No labels is pretty degenerate, but ok, I suppose. It's just a no-op testProg(t, ` int 0 From c066ff29baec7d5347891e7264388111d15fbd80 Mon Sep 17 00:00:00 2001 From: John Jannotti Date: Thu, 15 Sep 2022 14:44:48 -0400 Subject: [PATCH 16/19] Changing encoding to use a single byte for label count --- data/transactions/logic/assembler.go | 9 +-- data/transactions/logic/assembler_test.go | 86 ++++++++++------------- data/transactions/logic/doc.go | 5 +- data/transactions/logic/eval.go | 45 +++++------- data/transactions/logic/eval_test.go | 14 ++++ 5 files changed, 76 insertions(+), 83 deletions(-) diff --git a/data/transactions/logic/assembler.go b/data/transactions/logic/assembler.go index 53cc64053b..e30663e105 100644 --- a/data/transactions/logic/assembler.go +++ b/data/transactions/logic/assembler.go @@ -916,11 +916,12 @@ func asmBranch(ops *OpStream, spec *OpSpec, args []string) error { } func asmSwitch(ops *OpStream, spec *OpSpec, args []string) error { - numOffsets := uint64(len(args)) + if len(args) > 256 { + return ops.errorf("%s cannot take more than 256 labels", spec.Name) + } + numOffsets := len(args) ops.pending.WriteByte(spec.Opcode) - var scratch [binary.MaxVarintLen64]byte - vlen := binary.PutUvarint(scratch[:], uint64(len(args))) - ops.pending.Write(scratch[:vlen]) + ops.pending.WriteByte(byte(numOffsets)) opEndPos := ops.pending.Len() + 2*int(numOffsets) for _, arg := range args { ops.referToLabel(ops.pending.Len(), arg, opEndPos) diff --git a/data/transactions/logic/assembler_test.go b/data/transactions/logic/assembler_test.go index 54ad11a006..843fd70944 100644 --- a/data/transactions/logic/assembler_test.go +++ b/data/transactions/logic/assembler_test.go @@ -18,7 +18,6 @@ package logic import ( "bytes" - "encoding/binary" "encoding/hex" "fmt" "strings" @@ -395,7 +394,7 @@ pushint 1 replace3 ` -const v8Nonsense = v7Nonsense + pairingNonsense + ` +const switchNonsense = ` switch_label0: pushint 1 switchi switch_label0 switch_label1 @@ -403,6 +402,8 @@ switch_label1: pushint 1 ` +const v8Nonsense = v7Nonsense + pairingNonsense + switchNonsense + const v6Compiled = "2004010002b7a60c26050242420c68656c6c6f20776f726c6421070123456789abcd208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d047465737400320032013202320380021234292929292b0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e01022581f8acd19181cf959a1281f8acd19181cf951a81f8acd19181cf1581f8acd191810f082209240a220b230c240d250e230f23102311231223132314181b1c28171615400003290349483403350222231d4a484848482b50512a632223524100034200004322602261222704634848222862482864286548482228246628226723286828692322700048482371004848361c0037001a0031183119311b311d311e311f312023221e312131223123312431253126312731283129312a312b312c312d312e312f447825225314225427042455220824564c4d4b0222382124391c0081e80780046a6f686e2281d00f23241f880003420001892224902291922494249593a0a1a2a3a4a5a6a7a8a9aaabacadae24af3a00003b003c003d816472064e014f012a57000823810858235b235a2359b03139330039b1b200b322c01a23c1001a2323c21a23c3233e233f8120af06002a494905002a49490700b53a03b6b7043cb8033a0c2349c42a9631007300810881088120978101c53a8101c6003a" const randomnessCompiled = "81ffff03d101d000" @@ -1971,8 +1972,7 @@ intc_0 // 1 bnz label1 label1: `, v) - ops, err := AssembleStringWithVersion(source, v) - require.NoError(t, err) + ops := testProg(t, source, v) dis, err := Disassemble(ops.Program) require.NoError(t, err) require.Equal(t, source, dis) @@ -2085,8 +2085,7 @@ func TestHasStatefulOps(t *testing.T) { t.Parallel() source := "int 1" - ops, err := AssembleStringWithVersion(source, AssemblerMaxVersion) - require.NoError(t, err) + ops := testProg(t, source, AssemblerMaxVersion) has, err := HasStatefulOps(ops.Program) require.NoError(t, err) require.False(t, has) @@ -2096,8 +2095,7 @@ int 1 app_opted_in err ` - ops, err = AssembleStringWithVersion(source, AssemblerMaxVersion) - require.NoError(t, err) + ops = testProg(t, source, AssemblerMaxVersion) has, err = HasStatefulOps(ops.Program) require.NoError(t, err) require.True(t, has) @@ -2274,46 +2272,38 @@ func TestAssemblePragmaVersion(t *testing.T) { text := `#pragma version 1 int 1 ` - ops, err := AssembleStringWithVersion(text, 1) - require.NoError(t, err) - ops1, err := AssembleStringWithVersion("int 1", 1) - require.NoError(t, err) + ops := testProg(t, text, 1) + ops1 := testProg(t, "int 1", 1) require.Equal(t, ops1.Program, ops.Program) testProg(t, text, 0, Expect{1, "version mismatch..."}) testProg(t, text, 2, Expect{1, "version mismatch..."}) testProg(t, text, assemblerNoVersion) - ops, err = AssembleStringWithVersion(text, assemblerNoVersion) - require.NoError(t, err) + ops = testProg(t, text, assemblerNoVersion) require.Equal(t, ops1.Program, ops.Program) text = `#pragma version 2 int 1 ` - ops, err = AssembleStringWithVersion(text, 2) - require.NoError(t, err) - ops2, err := AssembleStringWithVersion("int 1", 2) - require.NoError(t, err) + ops = testProg(t, text, 2) + ops2 := testProg(t, "int 1", 2) require.Equal(t, ops2.Program, ops.Program) testProg(t, text, 0, Expect{1, "version mismatch..."}) testProg(t, text, 1, Expect{1, "version mismatch..."}) - ops, err = AssembleStringWithVersion(text, assemblerNoVersion) - require.NoError(t, err) + ops = testProg(t, text, assemblerNoVersion) require.Equal(t, ops2.Program, ops.Program) // check if no version it defaults to v1 text = `byte "test" len ` - ops, err = AssembleStringWithVersion(text, assemblerNoVersion) - require.NoError(t, err) - ops1, err = AssembleStringWithVersion(text, 1) + ops = testProg(t, text, assemblerNoVersion) + ops1 = testProg(t, text, 1) require.Equal(t, ops1.Program, ops.Program) - require.NoError(t, err) - ops2, err = AssembleString(text) + ops2, err := AssembleString(text) require.NoError(t, err) require.Equal(t, ops2.Program, ops.Program) @@ -2341,9 +2331,8 @@ func TestErrShortBytecblock(t *testing.T) { t.Parallel() text := `intcblock 0x1234567812345678 0x1234567812345671 0x1234567812345672 0x1234567812345673 4 5 6 7 8` - ops, err := AssembleStringWithVersion(text, 1) - require.NoError(t, err) - _, _, err = parseIntcblock(ops.Program, 1) + ops := testProg(t, text, 1) + _, _, err := parseIntcblock(ops.Program, 1) require.Equal(t, err, errShortIntcblock) var cx EvalContext @@ -2385,8 +2374,7 @@ func TestMethodWarning(t *testing.T) { for _, test := range tests { for v := uint64(1); v <= AssemblerMaxVersion; v++ { src := fmt.Sprintf("method \"%s\"\nint 1", test.method) - ops, err := AssembleStringWithVersion(src, v) - require.NoError(t, err) + ops := testProg(t, src, v) if test.pass { require.Len(t, ops.Warnings, 0) @@ -2689,7 +2677,7 @@ func TestMergeProtos(t *testing.T) { func TestGetSpec(t *testing.T) { partitiontest.PartitionTest(t) t.Parallel() - ops, _ := AssembleStringWithVersion("int 1", AssemblerMaxVersion) + ops := testProg(t, "int 1", AssemblerMaxVersion) ops.versionedPseudoOps["dummyPseudo"] = make(map[int]OpSpec) ops.versionedPseudoOps["dummyPseudo"][1] = OpSpec{Name: "b:", Version: AssemblerMaxVersion, Proto: proto("b:")} ops.versionedPseudoOps["dummyPseudo"][2] = OpSpec{Name: ":", Version: AssemblerMaxVersion} @@ -2798,39 +2786,37 @@ switchi int 1 `, AssemblerMaxVersion) - // confirm size of varuint list size + // confirm arg limit source = ` pushint 1 switchi label1 label2 label1: label2: ` - ops, err := AssembleStringWithVersion(source, AssemblerMaxVersion) - require.NoError(t, err) - val, bytesUsed := binary.Uvarint(ops.Program[4:]) - require.Equal(t, uint64(2), val) - require.Equal(t, 1, bytesUsed) - - var labelReferences []string - for i := 0; i < (1 << 9); i++ { - labelReferences = append(labelReferences, fmt.Sprintf("label%d", i)) - } + ops := testProg(t, source, AssemblerMaxVersion) + require.Len(t, ops.Program, 9) // ver (1) + pushint (2) + opcode (1) + length (1) + labels (2*2) var labels []string - for i := 0; i < (1 << 9); i++ { - labels = append(labels, fmt.Sprintf("label%d:", i)) + for i := 0; i < 256; i++ { + labels = append(labels, fmt.Sprintf("label%d", i)) } + // test that 256 labels is ok source = fmt.Sprintf(` pushint 1 switchi %s %s - `, strings.Join(labelReferences, " "), strings.Join(labels, "\n")) - ops, err = AssembleStringWithVersion(source, AssemblerMaxVersion) - require.NoError(t, err) - val, bytesUsed = binary.Uvarint(ops.Program[4:]) - require.Equal(t, uint64(1<<9), val) - require.Equal(t, 2, bytesUsed) + `, strings.Join(labels, " "), strings.Join(labels, ":\n")+":\n") + ops = testProg(t, source, AssemblerMaxVersion) + require.Len(t, ops.Program, 517) // ver (1) + pushint (2) + opcode (1) + length (1) + labels (2*256) + + // 257 is too many + source = fmt.Sprintf(` + pushint 1 + switchi %s extra + %s + `, strings.Join(labels, " "), strings.Join(labels, ":\n")+":\n") + ops = testProg(t, source, AssemblerMaxVersion, Expect{3, "switchi cannot take more than 256 labels"}) // allow duplicate label reference source = ` diff --git a/data/transactions/logic/doc.go b/data/transactions/logic/doc.go index 379d746b4c..cbb754f3ae 100644 --- a/data/transactions/logic/doc.go +++ b/data/transactions/logic/doc.go @@ -194,7 +194,7 @@ var opDocByName = map[string]string{ "vrf_verify": "Verify the proof B of message A against pubkey C. Returns vrf output and verification flag.", "block": "field F of block A. Fail unless A falls between txn.LastValid-1002 and txn.FirstValid (exclusive)", - "switchi": "branch to target at index A. Fail if index A is out of bounds.", + "switchi": "branch to the Ath label. Continue at following instruction if index A exceeds the number of labels.", } // OpDoc returns a description of the op @@ -264,7 +264,7 @@ var opcodeImmediateNotes = map[string]string{ "vrf_verify": "{uint8 parameters index}", "block": "{uint8 block field}", - "switchi": "{varuint length} [{int16 branch offset, big-endian}, ...]", + "switchi": "{uint8 branch count} [{int16 branch offset, big-endian}, ...]", } // OpImmediateNote returns a short string about immediate data which follows the op byte @@ -327,7 +327,6 @@ 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.", - "switchi": "The `switchi` instruction opcode is followed by `n`, the number of targets, each of which are encoded as 2 byte values indicating the position of the target label relative to the end of the `switchi` instruction (i.e. the offset). The last element on the stack represents the index of the target to branch to. If the index is greater than or equal to n, then evaluation falls through to the next instruction. Otherwise, the program will branch to `pc + 1 + sizeof(n) + 2 * n + target[index]`. Branch targets must be aligned instructions. (e.g. Branching to the second byte of a 2 byte op will be rejected.)", } // OpDocExtra returns extra documentation text about an op diff --git a/data/transactions/logic/eval.go b/data/transactions/logic/eval.go index fa808ab012..d2ef4405f1 100644 --- a/data/transactions/logic/eval.go +++ b/data/transactions/logic/eval.go @@ -1990,34 +1990,30 @@ func branchTarget(cx *EvalContext) (int, error) { return target, nil } -func switchTarget(cx *EvalContext, branchIdx uint64) (int, uint64, error) { - numOffsets, bytesUsed := binary.Uvarint(cx.program[cx.pc+1:]) - if bytesUsed <= 0 || numOffsets > math.MaxUint16 { - return 0, 0, fmt.Errorf("could not decode switch label count at pc=%d", cx.pc+1) - } +func switchTarget(cx *EvalContext, branchIdx uint64) (int, error) { + numOffsets := cx.program[cx.pc+1] - end := cx.pc + 1 + bytesUsed // end of opcode + number of offsets, beginning of offset list + end := cx.pc + 2 // end of opcode + number of offsets, beginning of offset list eoi := end + 2*int(numOffsets) // end of instruction - if branchIdx >= numOffsets { - return eoi, numOffsets, nil + + if eoi > len(cx.program) { // eoi will equal len(p) if switch is last instruction + return 0, fmt.Errorf("switch claims to extend beyond program") } - // 2*branchIdx will be no where near overflow b/c branchIdx < numOffsets < math.MaxUint16 - pos := end + int(2*branchIdx) // position of referenced offset: each offset is 2 bytes - if pos >= len(cx.program)-1 { // Need two bytes to decode an offset - return 0, 0, fmt.Errorf("offset position %d is beyond program", pos) + offset := 0 + if branchIdx < uint64(numOffsets) { + pos := end + int(2*branchIdx) // position of referenced offset: each offset is 2 bytes + offset = decodeBranchOffset(cx.program, pos) } - offset := decodeBranchOffset(cx.program, pos) - target := eoi + offset // offset is applied to the end of this opcode + target := eoi + offset // branching to exactly the end of the program (target == len(cx.program)), the next pc after the last instruction, // is okay and ends normally if target > len(cx.program) || target < 0 { - return 0, 0, fmt.Errorf("branch target %d outside of program", target) + return 0, fmt.Errorf("branch target %d outside of program", target) } - - return target, numOffsets, nil + return target, nil } // checks any branch that is {op} {int16 be offset} @@ -2036,17 +2032,13 @@ func checkBranch(cx *EvalContext) error { return nil } -// checks any switch that is {op} {varuint offset index} [{int16 offset}...] +// checks switch is encoded properly (and calculates nextpc) func checkSwitch(cx *EvalContext) error { - // call with too big of an index. the end-of-instruction PC is returned because - // that's where an out of range branchIdx goes - eoi, numOffsets, err := switchTarget(cx, math.MaxUint64) - if err != nil { - return err - } + numOffsets := uint64(cx.program[cx.pc+1]) + eoi := cx.pc + 2 + int(2*numOffsets) for branchIdx := uint64(0); branchIdx < numOffsets; branchIdx++ { - target, _, err := switchTarget(cx, branchIdx) + target, err := switchTarget(cx, branchIdx) if err != nil { return err } @@ -2107,8 +2099,9 @@ func opB(cx *EvalContext) error { func opSwitchInt(cx *EvalContext) error { last := len(cx.stack) - 1 branchIdx := cx.stack[last].Uint + cx.stack = cx.stack[:last] - target, _, err := switchTarget(cx, branchIdx) + target, err := switchTarget(cx, branchIdx) if err != nil { return err } diff --git a/data/transactions/logic/eval_test.go b/data/transactions/logic/eval_test.go index ac41772e00..26e8a8c718 100644 --- a/data/transactions/logic/eval_test.go +++ b/data/transactions/logic/eval_test.go @@ -5593,4 +5593,18 @@ int 1 `, 8) testPanics(t, notrack("switchi; int 1"), 8) + + // make the switch the final instruction + testAccepts(t, ` +int 1 +int 0 +switchi done1 done2; done1: ; done2: ; +`, 8) + + // make the switch the final instruction, and don't match + testAccepts(t, ` +int 1 +int 88 +switchi done1 done2; done1: ; done2: ; +`, 8) } From d705b0c1d12a53587190a1e763d064d8f647138e Mon Sep 17 00:00:00 2001 From: John Jannotti Date: Thu, 15 Sep 2022 16:56:06 -0400 Subject: [PATCH 17/19] Fix switch disassembly Made some changes to cast the label count (numOffsets) to `int` sooner. My bug was multiplying numOffsets by 2 while it was still a byte and _then_ casting to int. It had already wrapped. --- data/transactions/logic/assembler.go | 25 ++++++++--------------- data/transactions/logic/assembler_test.go | 10 ++++----- data/transactions/logic/eval.go | 14 ++++++------- 3 files changed, 20 insertions(+), 29 deletions(-) diff --git a/data/transactions/logic/assembler.go b/data/transactions/logic/assembler.go index e30663e105..47d5da1a15 100644 --- a/data/transactions/logic/assembler.go +++ b/data/transactions/logic/assembler.go @@ -916,13 +916,13 @@ func asmBranch(ops *OpStream, spec *OpSpec, args []string) error { } func asmSwitch(ops *OpStream, spec *OpSpec, args []string) error { - if len(args) > 256 { - return ops.errorf("%s cannot take more than 256 labels", spec.Name) - } numOffsets := len(args) + if numOffsets > math.MaxUint8 { + return ops.errorf("%s cannot take more than 255 labels", spec.Name) + } ops.pending.WriteByte(spec.Opcode) ops.pending.WriteByte(byte(numOffsets)) - opEndPos := ops.pending.Len() + 2*int(numOffsets) + opEndPos := ops.pending.Len() + 2*numOffsets for _, arg := range args { ops.referToLabel(ops.pending.Len(), arg, opEndPos) // zero bytes will get replaced with actual offset in resolveLabels() @@ -2561,19 +2561,10 @@ func checkByteConstBlock(cx *EvalContext) error { } func parseSwitch(program []byte, pos int) (targets []int, nextpc int, err error) { - numOffsets, bytesUsed := binary.Uvarint(program[pos:]) - if bytesUsed <= 0 { - err = fmt.Errorf("could not decode switch target list size at pc=%d", pos) - return - } - pos += bytesUsed - if numOffsets > math.MaxUint16 { - err = errors.New("switch with too many labels") - return - } - - end := pos + int(2*numOffsets) // end of op: offset is applied to this position - for i := 0; i < int(numOffsets); i++ { + numOffsets := int(program[pos]) + pos++ + end := pos + 2*numOffsets // end of op: offset is applied to this position + for i := 0; i < numOffsets; i++ { offset := decodeBranchOffset(program, pos) target := end + offset targets = append(targets, target) diff --git a/data/transactions/logic/assembler_test.go b/data/transactions/logic/assembler_test.go index 843fd70944..5f8e9baa8c 100644 --- a/data/transactions/logic/assembler_test.go +++ b/data/transactions/logic/assembler_test.go @@ -2797,26 +2797,26 @@ int 1 require.Len(t, ops.Program, 9) // ver (1) + pushint (2) + opcode (1) + length (1) + labels (2*2) var labels []string - for i := 0; i < 256; i++ { + for i := 0; i < 255; i++ { labels = append(labels, fmt.Sprintf("label%d", i)) } - // test that 256 labels is ok + // test that 255 labels is ok source = fmt.Sprintf(` pushint 1 switchi %s %s `, strings.Join(labels, " "), strings.Join(labels, ":\n")+":\n") ops = testProg(t, source, AssemblerMaxVersion) - require.Len(t, ops.Program, 517) // ver (1) + pushint (2) + opcode (1) + length (1) + labels (2*256) + require.Len(t, ops.Program, 515) // ver (1) + pushint (2) + opcode (1) + length (1) + labels (2*255) - // 257 is too many + // 256 is too many source = fmt.Sprintf(` pushint 1 switchi %s extra %s `, strings.Join(labels, " "), strings.Join(labels, ":\n")+":\n") - ops = testProg(t, source, AssemblerMaxVersion, Expect{3, "switchi cannot take more than 256 labels"}) + ops = testProg(t, source, AssemblerMaxVersion, Expect{3, "switchi cannot take more than 255 labels"}) // allow duplicate label reference source = ` diff --git a/data/transactions/logic/eval.go b/data/transactions/logic/eval.go index d2ef4405f1..f87b844240 100644 --- a/data/transactions/logic/eval.go +++ b/data/transactions/logic/eval.go @@ -1991,10 +1991,10 @@ func branchTarget(cx *EvalContext) (int, error) { } func switchTarget(cx *EvalContext, branchIdx uint64) (int, error) { - numOffsets := cx.program[cx.pc+1] + numOffsets := int(cx.program[cx.pc+1]) - end := cx.pc + 2 // end of opcode + number of offsets, beginning of offset list - eoi := end + 2*int(numOffsets) // end of instruction + end := cx.pc + 2 // end of opcode + number of offsets, beginning of offset list + eoi := end + 2*numOffsets // end of instruction if eoi > len(cx.program) { // eoi will equal len(p) if switch is last instruction return 0, fmt.Errorf("switch claims to extend beyond program") @@ -2034,11 +2034,11 @@ func checkBranch(cx *EvalContext) error { // checks switch is encoded properly (and calculates nextpc) func checkSwitch(cx *EvalContext) error { - numOffsets := uint64(cx.program[cx.pc+1]) - eoi := cx.pc + 2 + int(2*numOffsets) + numOffsets := int(cx.program[cx.pc+1]) + eoi := cx.pc + 2 + 2*numOffsets - for branchIdx := uint64(0); branchIdx < numOffsets; branchIdx++ { - target, err := switchTarget(cx, branchIdx) + for branchIdx := 0; branchIdx < numOffsets; branchIdx++ { + target, err := switchTarget(cx, uint64(branchIdx)) if err != nil { return err } From 928966ae526af6b4dc14d3c27db8f55fdf624e33 Mon Sep 17 00:00:00 2001 From: John Jannotti Date: Fri, 16 Sep 2022 11:32:43 -0400 Subject: [PATCH 18/19] docVersion and bump pairing ops --- cmd/opdoc/opdoc.go | 2 +- data/transactions/logic/README.md | 1 + data/transactions/logic/TEAL_opcodes.md | 7 +++++++ data/transactions/logic/assembler_test.go | 11 +++++++++-- data/transactions/logic/langspec.json | 13 ++++++++++++- data/transactions/logic/opcodes.go | 2 +- 6 files changed, 31 insertions(+), 5 deletions(-) diff --git a/cmd/opdoc/opdoc.go b/cmd/opdoc/opdoc.go index 94a394bbca..226d87a78b 100644 --- a/cmd/opdoc/opdoc.go +++ b/cmd/opdoc/opdoc.go @@ -28,7 +28,7 @@ import ( "github.com/algorand/go-algorand/protocol" ) -var docVersion = 7 +var docVersion = 8 func opGroupMarkdownTable(names []string, out io.Writer) { fmt.Fprint(out, `| Opcode | Description | diff --git a/data/transactions/logic/README.md b/data/transactions/logic/README.md index 6c68a8a7c9..521f9b57b7 100644 --- a/data/transactions/logic/README.md +++ b/data/transactions/logic/README.md @@ -594,6 +594,7 @@ Account fields used in the `acct_params_get` opcode. | `assert` | immediately fail unless A is a non-zero number | | `callsub target` | branch unconditionally to TARGET, saving the next instruction on the call stack | | `retsub` | pop the top instruction from the call stack and branch to it | +| `switchi target ...` | branch to the Ath label. Continue at following instruction if index A exceeds the number of labels. | ### State Access diff --git a/data/transactions/logic/TEAL_opcodes.md b/data/transactions/logic/TEAL_opcodes.md index 6c5d679108..45cba65d05 100644 --- a/data/transactions/logic/TEAL_opcodes.md +++ b/data/transactions/logic/TEAL_opcodes.md @@ -1053,6 +1053,13 @@ The call stack is separate from the data stack. Only `callsub` and `retsub` mani The call stack is separate from the data stack. Only `callsub` and `retsub` manipulate it. +## switchi target ... + +- Opcode: 0x8a {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 + ## shl - Opcode: 0x90 diff --git a/data/transactions/logic/assembler_test.go b/data/transactions/logic/assembler_test.go index 5f8e9baa8c..8a2928731b 100644 --- a/data/transactions/logic/assembler_test.go +++ b/data/transactions/logic/assembler_test.go @@ -402,7 +402,9 @@ switch_label1: pushint 1 ` -const v8Nonsense = v7Nonsense + pairingNonsense + switchNonsense +const v8Nonsense = v7Nonsense + switchNonsense + +const v9Nonsense = v8Nonsense + pairingNonsense const v6Compiled = "2004010002b7a60c26050242420c68656c6c6f20776f726c6421070123456789abcd208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d047465737400320032013202320380021234292929292b0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e01022581f8acd19181cf959a1281f8acd19181cf951a81f8acd19181cf1581f8acd191810f082209240a220b230c240d250e230f23102311231223132314181b1c28171615400003290349483403350222231d4a484848482b50512a632223524100034200004322602261222704634848222862482864286548482228246628226723286828692322700048482371004848361c0037001a0031183119311b311d311e311f312023221e312131223123312431253126312731283129312a312b312c312d312e312f447825225314225427042455220824564c4d4b0222382124391c0081e80780046a6f686e2281d00f23241f880003420001892224902291922494249593a0a1a2a3a4a5a6a7a8a9aaabacadae24af3a00003b003c003d816472064e014f012a57000823810858235b235a2359b03139330039b1b200b322c01a23c1001a2323c21a23c3233e233f8120af06002a494905002a49490700b53a03b6b7043cb8033a0c2349c42a9631007300810881088120978101c53a8101c6003a" @@ -411,7 +413,11 @@ const randomnessCompiled = "81ffff03d101d000" const v7Compiled = v6Compiled + "5e005f018120af060180070123456789abcd49490501988003012345494984" + randomnessCompiled + "800243218001775c0280018881015d" -const v8Compiled = v7Compiled + pairingCompiled + "81018a02fff800008101" +const switchCompiled = "81018a02fff800008101" + +const v8Compiled = v7Compiled + switchCompiled + +const v9Compiled = v7Compiled + pairingCompiled var nonsense = map[uint64]string{ 1: v1Nonsense, @@ -422,6 +428,7 @@ var nonsense = map[uint64]string{ 6: v6Nonsense, 7: v7Nonsense, 8: v8Nonsense, + 9: v9Nonsense, } var compiled = map[uint64]string{ diff --git a/data/transactions/logic/langspec.json b/data/transactions/logic/langspec.json index ff1a681667..5db44dfefb 100644 --- a/data/transactions/logic/langspec.json +++ b/data/transactions/logic/langspec.json @@ -1,5 +1,5 @@ { - "EvalMaxVersion": 7, + "EvalMaxVersion": 8, "LogicSigVersion": 7, "Ops": [ { @@ -1576,6 +1576,17 @@ "Flow Control" ] }, + { + "Opcode": 138, + "Name": "switchi", + "Args": "U", + "Size": 0, + "Doc": "branch to the Ath label. Continue at following instruction if index A exceeds the number of labels.", + "ImmediateNote": "{uint8 branch count} [{int16 branch offset, big-endian}, ...]", + "Groups": [ + "Flow Control" + ] + }, { "Opcode": 144, "Name": "shl", diff --git a/data/transactions/logic/opcodes.go b/data/transactions/logic/opcodes.go index 50802b307f..ec88ea5ca5 100644 --- a/data/transactions/logic/opcodes.go +++ b/data/transactions/logic/opcodes.go @@ -68,7 +68,7 @@ const randomnessVersion = 7 // vrf_verify, block // EXPERIMENTAL. These should be revisited whenever a new LogicSigVersion is // moved from vFuture to a new consensus version. If they remain unready, bump // their version, and fixup TestAssemble() in assembler_test.go. -const pairingVersion = 8 // bn256 opcodes. will add bls12-381, and unify the available opcodes. +const pairingVersion = 9 // bn256 opcodes. will add bls12-381, and unify the available opcodes. type linearCost struct { baseCost int From 96fe7c1102b99876be92b5e1e68750386c90171b Mon Sep 17 00:00:00 2001 From: John Jannotti Date: Fri, 16 Sep 2022 13:03:36 -0400 Subject: [PATCH 19/19] change name to switch. we've agreed next one would be `match` --- data/transactions/logic/README.md | 2 +- data/transactions/logic/TEAL_opcodes.md | 2 +- data/transactions/logic/assembler_test.go | 20 ++++++++++---------- data/transactions/logic/doc.go | 6 +++--- data/transactions/logic/eval.go | 2 +- data/transactions/logic/eval_test.go | 18 +++++++++--------- data/transactions/logic/langspec.json | 2 +- data/transactions/logic/opcodes.go | 2 +- data/transactions/logic/teal.tmLanguage.json | 2 +- 9 files changed, 28 insertions(+), 28 deletions(-) diff --git a/data/transactions/logic/README.md b/data/transactions/logic/README.md index 521f9b57b7..2fda903c98 100644 --- a/data/transactions/logic/README.md +++ b/data/transactions/logic/README.md @@ -594,7 +594,7 @@ Account fields used in the `acct_params_get` opcode. | `assert` | immediately fail unless A is a non-zero number | | `callsub target` | branch unconditionally to TARGET, saving the next instruction on the call stack | | `retsub` | pop the top instruction from the call stack and branch to it | -| `switchi target ...` | branch to the Ath label. Continue at following instruction if index A exceeds the number of labels. | +| `switch target ...` | branch to the Ath label. Continue at following instruction if index A exceeds the number of labels. | ### State Access diff --git a/data/transactions/logic/TEAL_opcodes.md b/data/transactions/logic/TEAL_opcodes.md index 45cba65d05..05cb20b96d 100644 --- a/data/transactions/logic/TEAL_opcodes.md +++ b/data/transactions/logic/TEAL_opcodes.md @@ -1053,7 +1053,7 @@ The call stack is separate from the data stack. Only `callsub` and `retsub` mani The call stack is separate from the data stack. Only `callsub` and `retsub` manipulate it. -## switchi target ... +## switch target ... - Opcode: 0x8a {uint8 branch count} [{int16 branch offset, big-endian}, ...] - Stack: ..., A: uint64 → ... diff --git a/data/transactions/logic/assembler_test.go b/data/transactions/logic/assembler_test.go index 8a2928731b..e614a722c6 100644 --- a/data/transactions/logic/assembler_test.go +++ b/data/transactions/logic/assembler_test.go @@ -397,7 +397,7 @@ replace3 const switchNonsense = ` switch_label0: pushint 1 -switchi switch_label0 switch_label1 +switch switch_label0 switch_label1 switch_label1: pushint 1 ` @@ -2774,7 +2774,7 @@ func TestAssembleSwitch(t *testing.T) { // fail when target doesn't correspond to existing label source := ` pushint 1 - switchi label1 label2 + switch label1 label2 label1: ` testProg(t, source, AssemblerMaxVersion, NewExpect(3, "reference to undefined label \"label2\"")) @@ -2782,21 +2782,21 @@ func TestAssembleSwitch(t *testing.T) { // fail when target index != uint64 testProg(t, ` byte "fail" - switchi label1 + switch label1 labe11: - `, AssemblerMaxVersion, Expect{3, "switchi label1 arg 0 wanted type uint64..."}) + `, AssemblerMaxVersion, Expect{3, "switch label1 arg 0 wanted type uint64..."}) // No labels is pretty degenerate, but ok, I suppose. It's just a no-op testProg(t, ` int 0 -switchi +switch int 1 `, AssemblerMaxVersion) // confirm arg limit source = ` pushint 1 - switchi label1 label2 + switch label1 label2 label1: label2: ` @@ -2811,7 +2811,7 @@ int 1 // test that 255 labels is ok source = fmt.Sprintf(` pushint 1 - switchi %s + switch %s %s `, strings.Join(labels, " "), strings.Join(labels, ":\n")+":\n") ops = testProg(t, source, AssemblerMaxVersion) @@ -2820,15 +2820,15 @@ int 1 // 256 is too many source = fmt.Sprintf(` pushint 1 - switchi %s extra + switch %s extra %s `, strings.Join(labels, " "), strings.Join(labels, ":\n")+":\n") - ops = testProg(t, source, AssemblerMaxVersion, Expect{3, "switchi cannot take more than 255 labels"}) + ops = testProg(t, source, AssemblerMaxVersion, Expect{3, "switch cannot take more than 255 labels"}) // allow duplicate label reference source = ` pushint 1 - switchi label1 label1 + switch label1 label1 label1: ` testProg(t, source, AssemblerMaxVersion) diff --git a/data/transactions/logic/doc.go b/data/transactions/logic/doc.go index cbb754f3ae..bfcf927f0a 100644 --- a/data/transactions/logic/doc.go +++ b/data/transactions/logic/doc.go @@ -194,7 +194,7 @@ var opDocByName = map[string]string{ "vrf_verify": "Verify the proof B of message A against pubkey C. Returns vrf output and verification flag.", "block": "field F of block A. Fail unless A falls between txn.LastValid-1002 and txn.FirstValid (exclusive)", - "switchi": "branch to the Ath label. Continue at following instruction if index A exceeds the number of labels.", + "switch": "branch to the Ath label. Continue at following instruction if index A exceeds the number of labels.", } // OpDoc returns a description of the op @@ -264,7 +264,7 @@ var opcodeImmediateNotes = map[string]string{ "vrf_verify": "{uint8 parameters index}", "block": "{uint8 block field}", - "switchi": "{uint8 branch count} [{int16 branch offset, big-endian}, ...]", + "switch": "{uint8 branch count} [{int16 branch offset, big-endian}, ...]", } // OpImmediateNote returns a short string about immediate data which follows the op byte @@ -343,7 +343,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", "switchi"}, + "Flow Control": {"err", "bnz", "bz", "b", "return", "pop", "dup", "dup2", "dig", "cover", "uncover", "swap", "select", "assert", "callsub", "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/eval.go b/data/transactions/logic/eval.go index f87b844240..5e4cf4c377 100644 --- a/data/transactions/logic/eval.go +++ b/data/transactions/logic/eval.go @@ -2096,7 +2096,7 @@ func opB(cx *EvalContext) error { return nil } -func opSwitchInt(cx *EvalContext) error { +func opSwitch(cx *EvalContext) error { last := len(cx.stack) - 1 branchIdx := cx.stack[last].Uint diff --git a/data/transactions/logic/eval_test.go b/data/transactions/logic/eval_test.go index 26e8a8c718..64c6c04808 100644 --- a/data/transactions/logic/eval_test.go +++ b/data/transactions/logic/eval_test.go @@ -5535,7 +5535,7 @@ func TestSwitchInt(t *testing.T) { // take the 0th label testAccepts(t, ` int 0 -switchi zero one +switch zero one err zero: int 1; return one: int 0; @@ -5544,7 +5544,7 @@ one: int 0; // take the 1th label testRejects(t, ` int 1 -switchi zero one +switch zero one err zero: int 1; return one: int 0; @@ -5553,7 +5553,7 @@ one: int 0; // same, but jumping to end of program testAccepts(t, ` int 1; dup -switchi zero one +switch zero one zero: err one: `, 8) @@ -5561,7 +5561,7 @@ one: // no match testAccepts(t, ` int 2 -switchi zero one +switch zero one int 1; return // falls through to here zero: int 0; return one: int 0; return @@ -5576,7 +5576,7 @@ int 1 dup int 1 - -switchi start end +switch start end err end: int 2 @@ -5588,23 +5588,23 @@ int 1 // 0 labels are allowed, but weird! testAccepts(t, ` int 0 -switchi +switch int 1 `, 8) - testPanics(t, notrack("switchi; int 1"), 8) + testPanics(t, notrack("switch; int 1"), 8) // make the switch the final instruction testAccepts(t, ` int 1 int 0 -switchi done1 done2; done1: ; done2: ; +switch done1 done2; done1: ; done2: ; `, 8) // make the switch the final instruction, and don't match testAccepts(t, ` int 1 int 88 -switchi done1 done2; done1: ; done2: ; +switch done1 done2; done1: ; done2: ; `, 8) } diff --git a/data/transactions/logic/langspec.json b/data/transactions/logic/langspec.json index 5db44dfefb..58f3ded24a 100644 --- a/data/transactions/logic/langspec.json +++ b/data/transactions/logic/langspec.json @@ -1578,7 +1578,7 @@ }, { "Opcode": 138, - "Name": "switchi", + "Name": "switch", "Args": "U", "Size": 0, "Doc": "branch to the Ath label. Continue at following instruction if index A exceeds the number of labels.", diff --git a/data/transactions/logic/opcodes.go b/data/transactions/logic/opcodes.go index ec88ea5ca5..1e3dcfc5b6 100644 --- a/data/transactions/logic/opcodes.go +++ b/data/transactions/logic/opcodes.go @@ -544,7 +544,7 @@ var OpSpecs = []OpSpec{ // "Function oriented" {0x88, "callsub", opCallSub, proto(":"), 4, detBranch()}, {0x89, "retsub", opRetSub, proto(":"), 4, detDefault()}, - {0x8a, "switchi", opSwitchInt, proto("i:"), 8, detSwitch()}, + {0x8a, "switch", opSwitch, proto("i:"), 8, detSwitch()}, // 0x8b will likely be a switch on pairs of values/targets // More math diff --git a/data/transactions/logic/teal.tmLanguage.json b/data/transactions/logic/teal.tmLanguage.json index 1f2925f983..863d0a0c43 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|switchi|uncover)\\b" + "match": "^(assert|b|bnz|bz|callsub|cover|dig|dup|dup2|err|pop|retsub|return|select|swap|switch|uncover)\\b" }, { "name": "keyword.other.teal",