diff --git a/data/transactions/logic/README.md b/data/transactions/logic/README.md index e577ea93f9..b7e4a519af 100644 --- a/data/transactions/logic/README.md +++ b/data/transactions/logic/README.md @@ -383,6 +383,7 @@ Some of these have immediate data in the byte or bytes after the opcode. | `intc_2` | constant 2 from intcblock | | `intc_3` | constant 3 from intcblock | | `pushint uint` | immediate UINT | +| `pushints uint ...` | push sequence of immediate uints to stack in the order they appear (first uint being deepest) | | `bytecblock bytes ...` | prepare block of byte-array constants for use by bytec | | `bytec i` | Ith constant from bytecblock | | `bytec_0` | constant 0 from bytecblock | @@ -390,6 +391,7 @@ Some of these have immediate data in the byte or bytes after the opcode. | `bytec_2` | constant 2 from bytecblock | | `bytec_3` | constant 3 from bytecblock | | `pushbytes bytes` | immediate BYTES | +| `pushbytess bytes ...` | push sequences of immediate byte arrays to stack (first byte array being deepest) | | `bzero` | zero filled byte-array of length A | | `arg n` | Nth LogicSig argument | | `arg_0` | LogicSig argument 0 | @@ -601,6 +603,7 @@ Account fields used in the `acct_params_get` opcode. | `proto a r` | Prepare top call frame for a retsub that will assume A args and R return values. | | `retsub` | pop the top instruction from the call stack and branch to it | | `switch target ...` | branch to the Ath label. Continue at following instruction if index A exceeds the number of labels. | +| `match target ...` | given match cases from A[1] to A[N], branch to the Ith label where A[I] = B. Continue to the following instruction if no matches are found. | ### State Access diff --git a/data/transactions/logic/TEAL_opcodes.md b/data/transactions/logic/TEAL_opcodes.md index d093c08230..eb2016cd3a 100644 --- a/data/transactions/logic/TEAL_opcodes.md +++ b/data/transactions/logic/TEAL_opcodes.md @@ -242,7 +242,7 @@ The notation J,K indicates that two uint64 values J and K are interpreted as a u ## intcblock uint ... -- Opcode: 0x20 {varuint length} [{varuint value}, ...] +- Opcode: 0x20 {varuint count} [{varuint value}, ...] - Stack: ... → ... - prepare block of uint64 constants for use by intc @@ -280,7 +280,7 @@ The notation J,K indicates that two uint64 values J and K are interpreted as a u ## bytecblock bytes ... -- Opcode: 0x26 {varuint length} [({varuint value length} bytes), ...] +- Opcode: 0x26 {varuint count} [({varuint value length} bytes), ...] - Stack: ... → ... - prepare block of byte-array constants for use by bytec @@ -1048,6 +1048,24 @@ pushbytes args are not added to the bytecblock during assembly processes pushint args are not added to the intcblock during assembly processes +## pushbytess bytes ... + +- Opcode: 0x82 {varuint count} [({varuint value length} bytes), ...] +- Stack: ... → ..., [N items] +- push sequences of immediate byte arrays to stack (first byte array being deepest) +- Availability: v8 + +pushbytess args are not added to the bytecblock during assembly processes + +## pushints uint ... + +- Opcode: 0x83 {varuint count} [{varuint value}, ...] +- Stack: ... → ..., [N items] +- push sequence of immediate uints to stack in the order they appear (first uint being deepest) +- Availability: v8 + +pushints args are not added to the intcblock during assembly processes + ## ed25519verify_bare - Opcode: 0x84 @@ -1104,6 +1122,15 @@ Fails unless the last instruction executed was a `callsub`. - branch to the Ath label. Continue at following instruction if index A exceeds the number of labels. - Availability: v8 +## match target ... + +- Opcode: 0x8e {uint8 branch count} [{int16 branch offset, big-endian}, ...] +- Stack: ..., [A1, A2, ..., AN], B → ... +- given match cases from A[1] to A[N], branch to the Ith label where A[I] = B. Continue to the following instruction if no matches are found. +- Availability: v8 + +`match` consumes N+1 values from the stack. Let the top stack value be B. The following N values represent an ordered list of match cases/constants (A), where the first value (A[0]) is the deepest in the stack. The immediate arguments are an ordered list of N labels (T). `match` will branch to target T[I], where A[I] = B. If there are no matches then execution continues on to the next instruction. + ## shl - Opcode: 0x90 diff --git a/data/transactions/logic/assembler.go b/data/transactions/logic/assembler.go index fd3c79bf44..e97b7f2cf3 100644 --- a/data/transactions/logic/assembler.go +++ b/data/transactions/logic/assembler.go @@ -583,6 +583,13 @@ func asmPushInt(ops *OpStream, spec *OpSpec, args []string) error { ops.pending.Write(scratch[:vlen]) return nil } + +func asmPushInts(ops *OpStream, spec *OpSpec, args []string) error { + ops.pending.WriteByte(spec.Opcode) + _, err := asmIntImmArgs(ops, args) + return err +} + func asmPushBytes(ops *OpStream, spec *OpSpec, args []string) error { if len(args) == 0 { return ops.errorf("%s operation needs byte literal argument", spec.Name) @@ -602,6 +609,12 @@ func asmPushBytes(ops *OpStream, spec *OpSpec, args []string) error { return nil } +func asmPushBytess(ops *OpStream, spec *OpSpec, args []string) error { + ops.pending.WriteByte(spec.Opcode) + _, err := asmByteImmArgs(ops, args) + return err +} + func base32DecodeAnyPadding(x string) (val []byte, err error) { val, err = base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(x) if err != nil { @@ -812,8 +825,7 @@ func asmMethod(ops *OpStream, spec *OpSpec, args []string) error { return ops.error("Unable to parse method signature") } -func asmIntCBlock(ops *OpStream, spec *OpSpec, args []string) error { - ops.pending.WriteByte(spec.Opcode) +func asmIntImmArgs(ops *OpStream, args []string) ([]uint64, error) { ivals := make([]uint64, len(args)) var scratch [binary.MaxVarintLen64]byte l := binary.PutUvarint(scratch[:], uint64(len(args))) @@ -825,9 +837,17 @@ func asmIntCBlock(ops *OpStream, spec *OpSpec, args []string) error { } l = binary.PutUvarint(scratch[:], cu) ops.pending.Write(scratch[:l]) - if !ops.known.deadcode { - ivals[i] = cu - } + ivals[i] = cu + } + + return ivals, nil +} + +func asmIntCBlock(ops *OpStream, spec *OpSpec, args []string) error { + ops.pending.WriteByte(spec.Opcode) + ivals, err := asmIntImmArgs(ops, args) + if err != nil { + return err } if !ops.known.deadcode { // If we previously processed an `int`, we thought we could insert our @@ -843,8 +863,7 @@ func asmIntCBlock(ops *OpStream, spec *OpSpec, args []string) error { return nil } -func asmByteCBlock(ops *OpStream, spec *OpSpec, args []string) error { - ops.pending.WriteByte(spec.Opcode) +func asmByteImmArgs(ops *OpStream, args []string) ([][]byte, error) { bvals := make([][]byte, 0, len(args)) rest := args for len(rest) > 0 { @@ -854,7 +873,7 @@ func asmByteCBlock(ops *OpStream, spec *OpSpec, args []string) error { // intcblock, but parseBinaryArgs would have // to return a useful consumed value even in // the face of errors. Hard. - return ops.error(err) + return nil, ops.error(err) } bvals = append(bvals, val) rest = rest[consumed:] @@ -867,6 +886,17 @@ func asmByteCBlock(ops *OpStream, spec *OpSpec, args []string) error { ops.pending.Write(scratch[:l]) ops.pending.Write(bv) } + + return bvals, nil +} + +func asmByteCBlock(ops *OpStream, spec *OpSpec, args []string) error { + ops.pending.WriteByte(spec.Opcode) + bvals, err := asmByteImmArgs(ops, args) + if err != nil { + return err + } + if !ops.known.deadcode { // If we previously processed a pseudo `byte`, we thought we could // insert our own bytecblock, but now we see a manual one. @@ -1454,6 +1484,24 @@ func typeDupN(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes, err return nil, copies, nil } +func typePushBytess(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes, error) { + types := make(StackTypes, len(args)) + for i := range types { + types[i] = StackBytes + } + + return nil, types, nil +} + +func typePushInts(pgm *ProgramKnowledge, args []string) (StackTypes, StackTypes, error) { + types := make(StackTypes, len(args)) + for i := range types { + types[i] = StackUint64 + } + + return nil, types, nil +} + func joinIntsOnOr(singularTerminator string, list ...int) string { if len(list) == 1 { switch list[0] { @@ -2519,7 +2567,7 @@ func disassemble(dis *disassembleState, spec *OpSpec) (string, error) { out += fmt.Sprintf("0x%s // %s", hex.EncodeToString(constant), guessByteFormat(constant)) pc = int(end) case immInts: - intc, nextpc, err := parseIntcblock(dis.program, pc) + intc, nextpc, err := parseIntImmArgs(dis.program, pc) if err != nil { return "", err } @@ -2533,7 +2581,7 @@ func disassemble(dis *disassembleState, spec *OpSpec) (string, error) { } pc = nextpc case immBytess: - bytec, nextpc, err := parseBytecBlock(dis.program, pc) + bytec, nextpc, err := parseByteImmArgs(dis.program, pc) if err != nil { return "", err } @@ -2590,13 +2638,13 @@ func disassemble(dis *disassembleState, spec *OpSpec) (string, error) { return out, nil } -var errShortIntcblock = errors.New("intcblock ran past end of program") -var errTooManyIntc = errors.New("intcblock with too many items") +var errShortIntImmArgs = errors.New("const int list ran past end of program") +var errTooManyIntc = errors.New("const int list with too many items") -func parseIntcblock(program []byte, pos int) (intc []uint64, nextpc int, err error) { +func parseIntImmArgs(program []byte, pos int) (intc []uint64, nextpc int, err error) { numInts, bytesUsed := binary.Uvarint(program[pos:]) if bytesUsed <= 0 { - err = fmt.Errorf("could not decode intcblock size at pc=%d", pos) + err = fmt.Errorf("could not decode length of int list at pc=%d", pos) return } pos += bytesUsed @@ -2607,7 +2655,7 @@ func parseIntcblock(program []byte, pos int) (intc []uint64, nextpc int, err err intc = make([]uint64, numInts) for i := uint64(0); i < numInts; i++ { if pos >= len(program) { - err = errShortIntcblock + err = errShortIntImmArgs return } intc[i], bytesUsed = binary.Uvarint(program[pos:]) @@ -2621,38 +2669,19 @@ func parseIntcblock(program []byte, pos int) (intc []uint64, nextpc int, err err return } -func checkIntConstBlock(cx *EvalContext) error { - pos := cx.pc + 1 - numInts, bytesUsed := binary.Uvarint(cx.program[pos:]) - if bytesUsed <= 0 { - return fmt.Errorf("could not decode intcblock size at pc=%d", pos) - } - pos += bytesUsed - if numInts > uint64(len(cx.program)) { - return errTooManyIntc - } - //intc = make([]uint64, numInts) - for i := uint64(0); i < numInts; i++ { - if pos >= len(cx.program) { - return errShortIntcblock - } - _, bytesUsed = binary.Uvarint(cx.program[pos:]) - if bytesUsed <= 0 { - return fmt.Errorf("could not decode int const[%d] at pc=%d", i, pos) - } - pos += bytesUsed - } - cx.nextpc = pos - return nil +func checkIntImmArgs(cx *EvalContext) error { + var err error + _, cx.nextpc, err = parseIntImmArgs(cx.program, cx.pc+1) + return err } -var errShortBytecblock = errors.New("bytecblock ran past end of program") -var errTooManyItems = errors.New("bytecblock with too many items") +var errShortByteImmArgs = errors.New("const bytes list ran past end of program") +var errTooManyItems = errors.New("const bytes list with too many items") -func parseBytecBlock(program []byte, pos int) (bytec [][]byte, nextpc int, err error) { +func parseByteImmArgs(program []byte, pos int) (bytec [][]byte, nextpc int, err error) { numItems, bytesUsed := binary.Uvarint(program[pos:]) if bytesUsed <= 0 { - err = fmt.Errorf("could not decode bytecblock size at pc=%d", pos) + err = fmt.Errorf("could not decode length of bytes list at pc=%d", pos) return } pos += bytesUsed @@ -2663,7 +2692,7 @@ func parseBytecBlock(program []byte, pos int) (bytec [][]byte, nextpc int, err e bytec = make([][]byte, numItems) for i := uint64(0); i < numItems; i++ { if pos >= len(program) { - err = errShortBytecblock + err = errShortByteImmArgs return } itemLen, bytesUsed := binary.Uvarint(program[pos:]) @@ -2673,12 +2702,12 @@ func parseBytecBlock(program []byte, pos int) (bytec [][]byte, nextpc int, err e } pos += bytesUsed if pos >= len(program) { - err = errShortBytecblock + err = errShortByteImmArgs return } end := uint64(pos) + itemLen if end > uint64(len(program)) || end < uint64(pos) { - err = errShortBytecblock + err = errShortByteImmArgs return } bytec[i] = program[pos : pos+int(itemLen)] @@ -2688,38 +2717,10 @@ func parseBytecBlock(program []byte, pos int) (bytec [][]byte, nextpc int, err e return } -func checkByteConstBlock(cx *EvalContext) error { - pos := cx.pc + 1 - numItems, bytesUsed := binary.Uvarint(cx.program[pos:]) - if bytesUsed <= 0 { - return fmt.Errorf("could not decode bytecblock size at pc=%d", pos) - } - pos += bytesUsed - if numItems > uint64(len(cx.program)) { - return errTooManyItems - } - //bytec = make([][]byte, numItems) - for i := uint64(0); i < numItems; i++ { - if pos >= len(cx.program) { - return errShortBytecblock - } - itemLen, bytesUsed := binary.Uvarint(cx.program[pos:]) - if bytesUsed <= 0 { - return fmt.Errorf("could not decode []byte const[%d] at pc=%d", i, pos) - } - pos += bytesUsed - if pos >= len(cx.program) { - return errShortBytecblock - } - end := uint64(pos) + itemLen - if end > uint64(len(cx.program)) || end < uint64(pos) { - return errShortBytecblock - } - //bytec[i] = program[pos : pos+int(itemLen)] - pos += int(itemLen) - } - cx.nextpc = pos - return nil +func checkByteImmArgs(cx *EvalContext) error { + var err error + _, cx.nextpc, err = parseByteImmArgs(cx.program, cx.pc+1) + return err } func parseSwitch(program []byte, pos int) (targets []int, nextpc int, err error) { diff --git a/data/transactions/logic/assembler_test.go b/data/transactions/logic/assembler_test.go index 1adf9b4508..5f500b99a2 100644 --- a/data/transactions/logic/assembler_test.go +++ b/data/transactions/logic/assembler_test.go @@ -407,7 +407,15 @@ switch_label1: pushint 1 ` -const v8Nonsense = v7Nonsense + switchNonsense + frameNonsense +const matchNonsense = ` +match_label0: +pushints 1 2 1 +match match_label0 match_label1 +match_label1: +pushbytess "1" "2" "1" +` + +const v8Nonsense = v7Nonsense + switchNonsense + frameNonsense + matchNonsense const v9Nonsense = v8Nonsense + pairingNonsense @@ -419,8 +427,9 @@ const v7Compiled = v6Compiled + "5e005f018120af060180070123456789abcd49490501988 randomnessCompiled + "800243218001775c0280018881015d" const switchCompiled = "81018d02fff800008101" +const matchCompiled = "83030102018e02fff500008203013101320131" -const v8Compiled = v7Compiled + switchCompiled + frameCompiled +const v8Compiled = v7Compiled + switchCompiled + frameCompiled + matchCompiled const v9Compiled = v7Compiled + pairingCompiled @@ -2347,13 +2356,13 @@ func TestErrShortBytecblock(t *testing.T) { text := `intcblock 0x1234567812345678 0x1234567812345671 0x1234567812345672 0x1234567812345673 4 5 6 7 8` ops := testProg(t, text, 1) - _, _, err := parseIntcblock(ops.Program, 1) - require.Equal(t, err, errShortIntcblock) + _, _, err := parseIntImmArgs(ops.Program, 1) + require.Equal(t, err, errShortIntImmArgs) var cx EvalContext cx.program = ops.Program - err = checkIntConstBlock(&cx) - require.Equal(t, err, errShortIntcblock) + err = checkIntImmArgs(&cx) + require.Equal(t, err, errShortIntImmArgs) } func TestMethodWarning(t *testing.T) { @@ -2890,3 +2899,119 @@ int 1 ` testProg(t, source, AssemblerMaxVersion) } + +func TestAssembleMatch(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + // fail when target doesn't correspond to existing label + source := ` + pushints 1 1 1 + match label1 label2 + label1: + ` + 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 +match +int 1 +`, AssemblerMaxVersion) + + // confirm arg limit + source = ` + pushints 1 2 1 + match label1 label2 + label1: + label2: + ` + ops := testProg(t, source, AssemblerMaxVersion) + require.Len(t, ops.Program, 12) // ver (1) + pushints (5) + opcode (1) + length (1) + labels (2*2) + + // confirm byte array args are assembled successfully + source = ` + pushbytess "1" "2" "1" + match label1 label2 + label1: + label2: + ` + testProg(t, source, AssemblerMaxVersion) + + var labels []string + for i := 0; i < 255; i++ { + labels = append(labels, fmt.Sprintf("label%d", i)) + } + + // test that 255 labels is ok + source = fmt.Sprintf(` + pushint 1 + match %s + %s + `, strings.Join(labels, " "), strings.Join(labels, ":\n")+":\n") + ops = testProg(t, source, AssemblerMaxVersion) + require.Len(t, ops.Program, 515) // ver (1) + pushint (2) + opcode (1) + length (1) + labels (2*255) + + // 256 is too many + source = fmt.Sprintf(` + pushint 1 + match %s extra + %s + `, strings.Join(labels, " "), strings.Join(labels, ":\n")+":\n") + testProg(t, source, AssemblerMaxVersion, Expect{3, "match cannot take more than 255 labels"}) + + // allow duplicate label reference + source = ` + pushint 1 + match label1 label1 + label1: + ` + testProg(t, source, AssemblerMaxVersion) +} + +func TestAssemblePushConsts(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + // allow empty const int list + source := `pushints` + testProg(t, source, AssemblerMaxVersion) + + // allow empty const bytes list + source = `pushbytess` + testProg(t, source, AssemblerMaxVersion) + + // basic test + source = `pushints 1 2 3` + ops := testProg(t, source, AssemblerMaxVersion) + require.Len(t, ops.Program, 6) // ver (1) + pushints (5) + source = `pushbytess "1" "2" "33"` + ops = testProg(t, source, AssemblerMaxVersion) + require.Len(t, ops.Program, 10) // ver (1) + pushbytess (9) + + // 256 increases size of encoded length to two bytes + valsStr := make([]string, 256) + for i := range valsStr { + valsStr[i] = fmt.Sprintf("%d", 1) + } + source = fmt.Sprintf(`pushints %s`, strings.Join(valsStr, " ")) + ops = testProg(t, source, AssemblerMaxVersion) + require.Len(t, ops.Program, 260) // ver (1) + opcode (1) + len (2) + ints (256) + + for i := range valsStr { + valsStr[i] = fmt.Sprintf("\"%d\"", 1) + } + source = fmt.Sprintf(`pushbytess %s`, strings.Join(valsStr, " ")) + ops = testProg(t, source, AssemblerMaxVersion) + require.Len(t, ops.Program, 516) // ver (1) + opcode (1) + len (2) + bytess (512) + + // enforce correct types + source = `pushints "1" "2" "3"` + testProg(t, source, AssemblerMaxVersion, Expect{1, `strconv.ParseUint: parsing "\"1\"": invalid syntax`}) + source = `pushbytess 1 2 3` + testProg(t, source, AssemblerMaxVersion, Expect{1, "byte arg did not parse: 1"}) + source = `pushints 6 4; concat` + testProg(t, source, AssemblerMaxVersion, Expect{1, "concat arg 1 wanted type []byte got uint64"}) + source = `pushbytess "x" "y"; +` + testProg(t, source, AssemblerMaxVersion, Expect{1, "+ arg 1 wanted type uint64 got []byte"}) +} diff --git a/data/transactions/logic/doc.go b/data/transactions/logic/doc.go index a12149bcf3..9097475607 100644 --- a/data/transactions/logic/doc.go +++ b/data/transactions/logic/doc.go @@ -75,6 +75,7 @@ var opDocByName = map[string]string{ "intc_2": "constant 2 from intcblock", "intc_3": "constant 3 from intcblock", "pushint": "immediate UINT", + "pushints": "push sequence of immediate uints to stack in the order they appear (first uint being deepest)", "bytecblock": "prepare block of byte-array constants for use by bytec", "bytec": "Ith constant from bytecblock", "bytec_0": "constant 0 from bytecblock", @@ -82,6 +83,7 @@ var opDocByName = map[string]string{ "bytec_2": "constant 2 from bytecblock", "bytec_3": "constant 3 from bytecblock", "pushbytes": "immediate BYTES", + "pushbytess": "push sequences of immediate byte arrays to stack (first byte array being deepest)", "bzero": "zero filled byte-array of length A", "arg": "Nth LogicSig argument", @@ -197,6 +199,7 @@ var opDocByName = map[string]string{ "block": "field F of block A. Fail unless A falls between txn.LastValid-1002 and txn.FirstValid (exclusive)", "switch": "branch to the Ath label. Continue at following instruction if index A exceeds the number of labels.", + "match": "given match cases from A[1] to A[N], branch to the Ith label where A[I] = B. Continue to the following instruction if no matches are found.", "proto": "Prepare top call frame for a retsub that will assume A args and R return values.", "frame_dig": "Nth (signed) value from the frame pointer.", @@ -210,12 +213,14 @@ func OpDoc(opName string) string { } var opcodeImmediateNotes = map[string]string{ - "intcblock": "{varuint length} [{varuint value}, ...]", + "intcblock": "{varuint count} [{varuint value}, ...]", "intc": "{uint8 int constant index}", "pushint": "{varuint int}", - "bytecblock": "{varuint length} [({varuint value length} bytes), ...]", + "pushints": "{varuint count} [{varuint value}, ...]", + "bytecblock": "{varuint count} [({varuint value length} bytes), ...]", "bytec": "{uint8 byte constant index}", "pushbytes": "{varuint length} {bytes}", + "pushbytess": "{varuint count} [({varuint value length} bytes), ...]", "arg": "{uint8 arg index N}", "global": "{uint8 global field index}", @@ -273,6 +278,7 @@ var opcodeImmediateNotes = map[string]string{ "block": "{uint8 block field}", "switch": "{uint8 branch count} [{int16 branch offset, big-endian}, ...]", + "match": "{uint8 branch count} [{int16 branch offset, big-endian}, ...]", "proto": "{uint8 arguments} {uint8 return values}", "frame_dig": "{int8 frame slot}", @@ -318,7 +324,9 @@ var opDocExtras = map[string]string{ "btoi": "`btoi` fails if the input is longer than 8 bytes.", "concat": "`concat` fails if the result would be greater than 4096 bytes.", "pushbytes": "pushbytes args are not added to the bytecblock during assembly processes", + "pushbytess": "pushbytess args are not added to the bytecblock during assembly processes", "pushint": "pushint args are not added to the intcblock during assembly processes", + "pushints": "pushints args are not added to the intcblock during assembly processes", "getbit": "see explanation of bit ordering in setbit", "setbit": "When A is a uint64, index 0 is the least significant bit. Setting bit 3 to 1 on the integer 0 yields 8, or 2^3. When A is a byte array, index 0 is the leftmost bit of the leftmost byte. Setting bits 0 through 11 to 1 in a 4-byte-array of 0s yields the byte array 0xfff00000. Setting bit 3 to 1 on the 1-byte-array 0x00 yields the byte array 0x10.", "balance": "params: Txn.Accounts offset (or, since v4, an _available_ account address), _available_ application id (or, since v4, a Txn.ForeignApps offset). Return: value.", @@ -342,6 +350,7 @@ var opDocExtras = map[string]string{ "base64_decode": "*Warning*: Usage should be restricted to very rare use cases. In almost all cases, smart contracts should directly handle non-encoded byte-strings. This opcode should only be used in cases where base64 is the only available option, e.g. interoperability with a third-party that only signs base64 strings.\n\n Decodes A using the base64 encoding E. Specify the encoding with an immediate arg either as URL and Filename Safe (`URLEncoding`) or Standard (`StdEncoding`). See [RFC 4648 sections 4 and 5](https://rfc-editor.org/rfc/rfc4648.html#section-4). It is assumed that the encoding ends with the exact number of `=` padding characters as required by the RFC. When padding occurs, any unused pad bits in the encoding must be set to zero or the decoding will fail. The special cases of `\\n` and `\\r` are allowed but completely ignored. An error will result when attempting to decode a string with a character that is not in the encoding alphabet or not one of `=`, `\\r`, or `\\n`.", "json_ref": "*Warning*: Usage should be restricted to very rare use cases, as JSON decoding is expensive and quite limited. In addition, JSON objects are large and not optimized for size.\n\nAlmost all smart contracts should use simpler and smaller methods (such as the [ABI](https://arc.algorand.foundation/ARCs/arc-0004). This opcode should only be used in cases where JSON is only available option, e.g. when a third-party only signs JSON.", "proto": "Fails unless the last instruction executed was a `callsub`.", + "match": "`match` consumes N+1 values from the stack. Let the top stack value be B. The following N values represent an ordered list of match cases/constants (A), where the first value (A[0]) is the deepest in the stack. The immediate arguments are an ordered list of N labels (T). `match` will branch to target T[I], where A[I] = B. If there are no matches then execution continues on to the next instruction.", } // OpDocExtra returns extra documentation text about an op @@ -357,8 +366,8 @@ var OpGroups = map[string][]string{ "Byte Array Manipulation": {"substring", "substring3", "extract", "extract3", "extract_uint16", "extract_uint32", "extract_uint64", "replace2", "replace3", "base64_decode", "json_ref"}, "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", "popn", "dup", "dup2", "dupn", "dig", "bury", "cover", "uncover", "frame_dig", "frame_bury", "swap", "select", "assert", "callsub", "proto", "retsub", "switch"}, + "Loading Values": {"intcblock", "intc", "intc_0", "intc_1", "intc_2", "intc_3", "pushint", "pushints", "bytecblock", "bytec", "bytec_0", "bytec_1", "bytec_2", "bytec_3", "pushbytes", "pushbytess", "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", "popn", "dup", "dup2", "dupn", "dig", "bury", "cover", "uncover", "frame_dig", "frame_bury", "swap", "select", "assert", "callsub", "proto", "retsub", "switch", "match"}, "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 1e78d69605..25b683de30 100644 --- a/data/transactions/logic/eval.go +++ b/data/transactions/logic/eval.go @@ -1126,6 +1126,15 @@ func (cx *EvalContext) checkStep() (int, error) { return opcost, nil } +func (cx *EvalContext) ensureStackCap(targetCap int) { + if cap(cx.stack) < targetCap { + // Let's grow all at once, plus a little slack. + newStack := make([]stackValue, len(cx.stack), targetCap+4) + copy(newStack, cx.stack) + cx.stack = newStack + } +} + func opErr(cx *EvalContext) error { return errors.New("err opcode executed") } @@ -1855,7 +1864,7 @@ func opBytesZero(cx *EvalContext) error { func opIntConstBlock(cx *EvalContext) error { var err error - cx.intc, cx.nextpc, err = parseIntcblock(cx.program, cx.pc+1) + cx.intc, cx.nextpc, err = parseIntImmArgs(cx.program, cx.pc+1) return err } @@ -1895,9 +1904,24 @@ func opPushInt(cx *EvalContext) error { return nil } +func opPushInts(cx *EvalContext) error { + intc, nextpc, err := parseIntImmArgs(cx.program, cx.pc+1) + if err != nil { + return err + } + finalLen := len(cx.stack) + len(intc) + cx.ensureStackCap(finalLen) + for _, cint := range intc { + sv := stackValue{Uint: cint} + cx.stack = append(cx.stack, sv) + } + cx.nextpc = nextpc + return nil +} + func opByteConstBlock(cx *EvalContext) error { var err error - cx.bytec, cx.nextpc, err = parseBytecBlock(cx.program, cx.pc+1) + cx.bytec, cx.nextpc, err = parseByteImmArgs(cx.program, cx.pc+1) return err } @@ -1942,6 +1966,21 @@ func opPushBytes(cx *EvalContext) error { return nil } +func opPushBytess(cx *EvalContext) error { + cbytess, nextpc, err := parseByteImmArgs(cx.program, cx.pc+1) + if err != nil { + return err + } + finalLen := len(cx.stack) + len(cbytess) + cx.ensureStackCap(finalLen) + for _, cbytes := range cbytess { + sv := stackValue{Bytes: cbytes} + cx.stack = append(cx.stack, sv) + } + cx.nextpc = nextpc + return nil +} + func opArgN(cx *EvalContext, n uint64) error { if n >= uint64(len(cx.txn.Lsig.Args)) { return fmt.Errorf("cannot load arg[%d] of %d", n, len(cx.txn.Lsig.Args)) @@ -2119,6 +2158,44 @@ func opSwitch(cx *EvalContext) error { return nil } +func opMatch(cx *EvalContext) error { + n := int(cx.program[cx.pc+1]) + // stack contains the n sized match list and the single match value + if n+1 > len(cx.stack) { + return fmt.Errorf("match expects %d stack args while stack only contains %d", n+1, len(cx.stack)) + } + + last := len(cx.stack) - 1 + matchVal := cx.stack[last] + cx.stack = cx.stack[:last] + + argBase := len(cx.stack) - n + matchList := cx.stack[argBase:] + cx.stack = cx.stack[:argBase] + + matchedIdx := n + for i, stackArg := range matchList { + if stackArg.argType() != matchVal.argType() { + continue + } + + if matchVal.argType() == StackBytes && bytes.Equal(matchVal.Bytes, stackArg.Bytes) { + matchedIdx = i + break + } else if matchVal.argType() == StackUint64 && matchVal.Uint == stackArg.Uint { + matchedIdx = i + break + } + } + + target, err := switchTarget(cx, uint64(matchedIdx)) + if err != nil { + return err + } + cx.nextpc = target + return nil +} + const protoByte = 0x8a func opCallSub(cx *EvalContext) error { diff --git a/data/transactions/logic/eval_test.go b/data/transactions/logic/eval_test.go index 130167bf02..f3db7a7705 100644 --- a/data/transactions/logic/eval_test.go +++ b/data/transactions/logic/eval_test.go @@ -3020,7 +3020,7 @@ func TestShortBytecblock(t *testing.T) { program := fullops.Program[:i] t.Run(hex.EncodeToString(program), func(t *testing.T) { testLogicBytes(t, program, defaultEvalParams(nil), - "bytecblock", "bytecblock") + "bytes list", "bytes list") }) } }) @@ -3041,7 +3041,7 @@ func TestShortBytecblock2(t *testing.T) { t.Run(src, func(t *testing.T) { program, err := hex.DecodeString(src) require.NoError(t, err) - testLogicBytes(t, program, defaultEvalParams(nil), "bytecblock", "bytecblock") + testLogicBytes(t, program, defaultEvalParams(nil), "const bytes list", "const bytes list") }) } } @@ -5691,3 +5691,203 @@ int 88 switch done1 done2; done1: ; done2: ; `, 8) } + +func TestMatch(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + // take the 0th label with int cases + testAccepts(t, ` +int 99 +int 100 +int 99 +match zero one +err +zero: int 1; return +one: int 0; +`, 8) + + // take the 0th label with bytes cases + testAccepts(t, ` +byte "0" +byte "1" +byte "0" +match zero one +err +zero: int 1; return +one: int 0; +`, 8) + + // take the 1th label with int cases + testRejects(t, ` +int 99 +int 100 +int 100 +match zero one +err +zero: int 1; return +one: int 0; +`, 8) + + // take the 1th label with bytes cases + testRejects(t, ` +byte "0" +byte "1" +byte "1" +match zero one +err +zero: int 1; return +one: int 0; +`, 8) + + // same, but jumping to end of program + testAccepts(t, ` +int 1; int 99; int 100; int 100 +match zero one +zero: err +one: +`, 8) + + // no match + testAccepts(t, ` +int 99 +int 100 +int 101 +match 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 99 +start: +int 1 ++ +int 100 +int 101 +dig 2 +match start end +err +end: +int 101 +== +assert +int 1 +`, 8) + + // 0 labels are allowed, but weird! + testAccepts(t, ` +int 0 +match +int 1 +`, 8) + + testPanics(t, notrack("match; int 1"), 8) + + // make the match the final instruction + testAccepts(t, ` +int 1 +int 100 +int 99 +int 100 +match done1 done2; done1: ; done2: ; +`, 8) + + // make the switch the final instruction, and don't match + testAccepts(t, ` +int 1 +int 1 +int 2 +int 88 +match done1 done2; done1: ; done2: ; +`, 8) + + // allow mixed types for match cases + testAccepts(t, ` +int 1 +int 100 +byte "101" +byte "101" +match done1 done2; done1: ; done2: ; +`, 8) + + testAccepts(t, ` +byte "0" +int 1 +byte "0" +match zero one +err +zero: int 1; return +one: int 0; +`, 8) + + testAccepts(t, ` +byte "0" +int 1 +int 1 +match zero one +err +one: int 1; return +zero: int 0; +`, 8) + + testAccepts(t, ` +byte "0" +byte "1" +int 1 +match zero one +int 1; return +zero: int 0; +one: int 0; +`, 8) +} + +func TestPushConsts(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + testAccepts(t, ` +pushints 1 2 +int 2 +== +assert +int 1 +== +assert +int 1 +`, 8) + + testAccepts(t, ` +pushbytess "1" "2" +byte "2" +== +assert +byte "1" +== +assert +int 1 +`, 8) + + valsStr := make([]string, 256) + for i := range valsStr { + valsStr[i] = fmt.Sprintf("%d", i) + } + source := fmt.Sprintf(`pushints %s`, strings.Join(valsStr, " ")) + testAccepts(t, source+` +popn 255 +pop +int 1 +`, 8) + + for i := range valsStr { + valsStr[i] = fmt.Sprintf("\"%d\"", i) + } + source = fmt.Sprintf(`pushbytess %s`, strings.Join(valsStr, " ")) + testAccepts(t, source+` +popn 255 +pop +int 1 +`, 8) +} diff --git a/data/transactions/logic/frames.go b/data/transactions/logic/frames.go index e145ac8fce..1acc0c3c2b 100644 --- a/data/transactions/logic/frames.go +++ b/data/transactions/logic/frames.go @@ -114,12 +114,7 @@ func opDupN(cx *EvalContext) error { n := int(cx.program[cx.pc+1]) finalLen := len(cx.stack) + n - if cap(cx.stack) < finalLen { - // Let's grow all at once, plus a little slack. - newStack := make([]stackValue, len(cx.stack), finalLen+4) - copy(newStack, cx.stack) - cx.stack = newStack - } + cx.ensureStackCap(finalLen) for i := 0; i < n; i++ { // There will be enough room that this will not allocate cx.stack = append(cx.stack, cx.stack[last]) diff --git a/data/transactions/logic/langspec.json b/data/transactions/logic/langspec.json index f4742f1b7f..74bfcb27ca 100644 --- a/data/transactions/logic/langspec.json +++ b/data/transactions/logic/langspec.json @@ -370,7 +370,7 @@ "Size": 0, "Doc": "prepare block of uint64 constants for use by intc", "DocExtra": "`intcblock` loads following program bytes into an array of integer constants in the evaluator. These integer constants can be referred to by `intc` and `intc_*` which will push the value onto the stack. Subsequent calls to `intcblock` reset and replace the integer constants available to the script.", - "ImmediateNote": "{varuint length} [{varuint value}, ...]", + "ImmediateNote": "{varuint count} [{varuint value}, ...]", "Groups": [ "Loading Values" ] @@ -432,7 +432,7 @@ "Size": 0, "Doc": "prepare block of byte-array constants for use by bytec", "DocExtra": "`bytecblock` loads the following program bytes into an array of byte-array constants in the evaluator. These constants can be referred to by `bytec` and `bytec_*` which will push the value onto the stack. Subsequent calls to `bytecblock` reset and replace the bytes constants available to the script.", - "ImmediateNote": "{varuint length} [({varuint value length} bytes), ...]", + "ImmediateNote": "{varuint count} [({varuint value length} bytes), ...]", "Groups": [ "Loading Values" ] @@ -1576,6 +1576,28 @@ "Loading Values" ] }, + { + "Opcode": 130, + "Name": "pushbytess", + "Size": 0, + "Doc": "push sequences of immediate byte arrays to stack (first byte array being deepest)", + "DocExtra": "pushbytess args are not added to the bytecblock during assembly processes", + "ImmediateNote": "{varuint count} [({varuint value length} bytes), ...]", + "Groups": [ + "Loading Values" + ] + }, + { + "Opcode": 131, + "Name": "pushints", + "Size": 0, + "Doc": "push sequence of immediate uints to stack in the order they appear (first uint being deepest)", + "DocExtra": "pushints args are not added to the intcblock during assembly processes", + "ImmediateNote": "{varuint count} [{varuint value}, ...]", + "Groups": [ + "Loading Values" + ] + }, { "Opcode": 132, "Name": "ed25519verify_bare", @@ -1652,6 +1674,17 @@ "Flow Control" ] }, + { + "Opcode": 142, + "Name": "match", + "Size": 0, + "Doc": "given match cases from A[1] to A[N], branch to the Ith label where A[I] = B. Continue to the following instruction if no matches are found.", + "DocExtra": "`match` consumes N+1 values from the stack. Let the top stack value be B. The following N values represent an ordered list of match cases/constants (A), where the first value (A[0]) is the deepest in the stack. The immediate arguments are an ordered list of N labels (T). `match` will branch to target T[I], where A[I] = B. If there are no matches then execution continues on to the next instruction.", + "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 1efab93be3..d54de6d494 100644 --- a/data/transactions/logic/opcodes.go +++ b/data/transactions/logic/opcodes.go @@ -448,13 +448,13 @@ var OpSpecs = []OpSpec{ {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)}, + {0x20, "intcblock", opIntConstBlock, proto(":"), 1, constants(asmIntCBlock, checkIntImmArgs, "uint ...", immInts)}, {0x21, "intc", opIntConstLoad, proto(":i"), 1, immediates("i").assembler(asmIntC)}, {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)}, + {0x26, "bytecblock", opByteConstBlock, proto(":"), 1, constants(asmByteCBlock, checkByteImmArgs, "bytes ...", immBytess)}, {0x27, "bytec", opByteConstLoad, proto(":b"), 1, immediates("i").assembler(asmByteC)}, {0x28, "bytec_0", opByteConst0, proto(":b"), 1, detDefault()}, {0x29, "bytec_1", opByteConst1, proto(":b"), 1, detDefault()}, @@ -552,6 +552,8 @@ var OpSpecs = []OpSpec{ // Immediate bytes and ints. Smaller code size for single use of constant. {0x80, "pushbytes", opPushBytes, proto(":b"), 3, constants(asmPushBytes, opPushBytes, "bytes", immBytes)}, {0x81, "pushint", opPushInt, proto(":i"), 3, constants(asmPushInt, opPushInt, "uint", immInt)}, + {0x82, "pushbytess", opPushBytess, proto(":", "", "[N items]"), 8, constants(asmPushBytess, checkByteImmArgs, "bytes ...", immBytess).typed(typePushBytess).trust()}, + {0x83, "pushints", opPushInts, proto(":", "", "[N items]"), 8, constants(asmPushInts, checkIntImmArgs, "uint ...", immInts).typed(typePushInts).trust()}, {0x84, "ed25519verify_bare", opEd25519VerifyBare, proto("bbb:i"), 7, costly(1900)}, @@ -563,7 +565,7 @@ var OpSpecs = []OpSpec{ {0x8b, "frame_dig", opFrameDig, proto(":a"), fpVersion, immKinded(immInt8, "i").typed(typeFrameDig)}, {0x8c, "frame_bury", opFrameBury, proto("a:"), fpVersion, immKinded(immInt8, "i").typed(typeFrameBury)}, {0x8d, "switch", opSwitch, proto("i:"), 8, detSwitch()}, - // 0x8e will likely be a switch on pairs of values/targets, called `match` + {0x8e, "match", opMatch, proto(":", "[A1, A2, ..., AN], B", ""), 8, detSwitch().trust()}, // More math {0x90, "shl", opShiftLeft, proto("ii:i"), 4, detDefault()}, diff --git a/data/transactions/logic/teal.tmLanguage.json b/data/transactions/logic/teal.tmLanguage.json index 7a299a9624..81a4101aa7 100644 --- a/data/transactions/logic/teal.tmLanguage.json +++ b/data/transactions/logic/teal.tmLanguage.json @@ -64,11 +64,11 @@ }, { "name": "keyword.control.teal", - "match": "^(assert|b|bnz|bury|bz|callsub|cover|dig|dup|dup2|dupn|err|frame_bury|frame_dig|pop|popn|proto|retsub|return|select|swap|switch|uncover)\\b" + "match": "^(assert|b|bnz|bury|bz|callsub|cover|dig|dup|dup2|dupn|err|frame_bury|frame_dig|match|pop|popn|proto|retsub|return|select|swap|switch|uncover)\\b" }, { "name": "keyword.other.teal", - "match": "^(int|byte|addr|arg|arg_0|arg_1|arg_2|arg_3|args|bytec|bytec_0|bytec_1|bytec_2|bytec_3|bytecblock|bzero|gaid|gaids|gload|gloads|gloadss|global|gtxn|gtxna|gtxnas|gtxns|gtxnsa|gtxnsas|intc|intc_0|intc_1|intc_2|intc_3|intcblock|load|loads|pushbytes|pushint|store|stores|txn|txna|txnas)\\b" + "match": "^(int|byte|addr|arg|arg_0|arg_1|arg_2|arg_3|args|bytec|bytec_0|bytec_1|bytec_2|bytec_3|bytecblock|bzero|gaid|gaids|gload|gloads|gloadss|global|gtxn|gtxna|gtxnas|gtxns|gtxnsa|gtxnsas|intc|intc_0|intc_1|intc_2|intc_3|intcblock|load|loads|pushbytes|pushbytess|pushint|pushints|store|stores|txn|txna|txnas)\\b" }, { "name": "keyword.other.unit.teal",