Repository: algorand/pyteal

Issue:
Add a way to version to PyTeal programs
## Summary
For security reasons, PyTeal programs should be written for a specific version of TEAL. This is because new TEAL versions can introduce new fields and transaction types that are not accessible from older versions and which can be dangerous. For example, TEAL v2 introduced rekeying, so to secure all TEAL v1 programs, the network automatically rejects transactions that involve rekeying and TEAL v1 programs.

When future versions of TEAL are released, PyTeal will be updated to support them, so the package should provide some way to version programs. This will guard against a program being compiled to a later version of TEAL than the program author intended.

Thanks to @fabrice102 for bringing this up.

## Scope
Some ideas about how this could be achieved:
* Add a version parameter to the `compileTeal` method.
* Add a `Version` object to the AST to declare support versions inside of a program's AST.

## Urgency
This should be implemented before PyTeal supports TEAL v3.

Discussion:

Patch:
diff --git a/pyteal/ast/binaryexpr.py b/pyteal/ast/binaryexpr.py
index 5e2e01e..7664a4f 100644
--- a/pyteal/ast/binaryexpr.py
+++ b/pyteal/ast/binaryexpr.py
@@ -17,7 +17,7 @@ class BinaryExpr(Expr):
         return TealBlock.FromOp(TealOp(self.op), self.argLeft, self.argRight)
     
     def __str__(self):
-        return "({} {} {})".format(self.op.value, self.argLeft, self.argRight)
+        return "({} {} {})".format(self.op, self.argLeft, self.argRight)
     
     def type_of(self):
         return self.outputType
diff --git a/pyteal/ast/naryexpr.py b/pyteal/ast/naryexpr.py
index 50da97a..1a4bb92 100644
--- a/pyteal/ast/naryexpr.py
+++ b/pyteal/ast/naryexpr.py
@@ -39,7 +39,7 @@ class NaryExpr(Expr):
         return start, end
 
     def __str__(self):
-        ret_str = "(" + self.op.value,
+        ret_str = "(" + str(self.op),
         for a in self.args:
             ret_str += " " + a.__str__()
         ret_str += ")"
diff --git a/pyteal/ast/tmpl.py b/pyteal/ast/tmpl.py
index 91f956e..635be0d 100644
--- a/pyteal/ast/tmpl.py
+++ b/pyteal/ast/tmpl.py
@@ -13,7 +13,7 @@ class Tmpl(LeafExpr):
         self.name = name
 
     def __str__(self):
-        return "(Tmpl {} {})".format(self.op.value, self.name)
+        return "(Tmpl {} {})".format(self.op, self.name)
 
     def __teal__(self):
         op = TealOp(self.op, self.name)
diff --git a/pyteal/ast/unaryexpr.py b/pyteal/ast/unaryexpr.py
index e9879f8..faa171b 100644
--- a/pyteal/ast/unaryexpr.py
+++ b/pyteal/ast/unaryexpr.py
@@ -15,7 +15,7 @@ class UnaryExpr(Expr):
         return TealBlock.FromOp(TealOp(self.op), self.arg)
 
     def __str__(self):
-        return "({} {})".format(self.op.value, self.arg)
+        return "({} {})".format(self.op, self.arg)
 
     def type_of(self):
         return self.outputType
diff --git a/pyteal/compiler.py b/pyteal/compiler.py
index 91643d0..12cb64b 100644
--- a/pyteal/compiler.py
+++ b/pyteal/compiler.py
@@ -6,6 +6,10 @@ from .ir import Op, Mode, TealComponent, TealOp, TealLabel, TealBlock, TealSimpl
 from .errors import TealInputError, TealInternalError
 from .config import NUM_SLOTS
 
+MAX_TEAL_VERSION = 2
+MIN_TEAL_VERSION = 2
+DEFAULT_TEAL_VERSION = 2
+
 def sortBlocks(start: TealBlock) -> List[TealBlock]:
     """Topologically sort the graph which starts with the input TealBlock.
 
@@ -95,6 +99,22 @@ def flattenBlocks(blocks: List[TealBlock]) -> List[TealComponent]:
 
     return teal
 
+def verifyOpsForVersion(teal: List[TealComponent], version: int):
+    """Verify that all TEAL operations are allowed in the specified version.
+
+    Args:
+        teal: Code to check.
+        mode: The version to check against.
+
+    Raises:
+        TealInputError: if teal contains an operation not allowed in version.
+    """
+    for stmt in teal:
+        if isinstance(stmt, TealOp):
+            op = stmt.getOp()
+            if op.min_version > version:
+                raise TealInputError("Op not supported in TEAL version {}: {}".format(version, op))
+
 def verifyOpsForMode(teal: List[TealComponent], mode: Mode):
     """Verify that all TEAL operations are allowed in mode.
 
@@ -109,14 +129,17 @@ def verifyOpsForMode(teal: List[TealComponent], mode: Mode):
         if isinstance(stmt, TealOp):
             op = stmt.getOp()
             if not op.mode & mode:
-                raise TealInputError("Op not supported in {} mode: {}".format(mode.name, op.value))
+                raise TealInputError("Op not supported in {} mode: {}".format(mode.name, op))
 
-def compileTeal(ast: Expr, mode: Mode) -> str:
+def compileTeal(ast: Expr, mode: Mode, version: int = DEFAULT_TEAL_VERSION) -> str:
     """Compile a PyTeal expression into TEAL assembly.
 
     Args:
         ast: The PyTeal expression to assemble.
         mode: The mode of the program to assemble. Must be Signature or Application.
+        version (optional): The TEAL version used to assemble the program. This will determine which
+        expressions and fields are able to be used in the program and how expressions compile to
+        TEAL opcodes. Defaults to 2 if not included.
 
     Returns:
         A TEAL assembly program compiled from the input expression.
@@ -124,6 +147,9 @@ def compileTeal(ast: Expr, mode: Mode) -> str:
     Raises:
         TealInputError: if an operation in ast is not supported by the supplied mode.
     """
+    if not (MIN_TEAL_VERSION <= version <= MAX_TEAL_VERSION):
+        raise TealInputError("Unsupported TEAL version: {}. Excepted a number in the range [{}, {}]".format(version, MIN_TEAL_VERSION, MAX_TEAL_VERSION))
+
     start, _ = ast.__teal__()
     start.addIncoming()
     start.validate()
@@ -134,6 +160,7 @@ def compileTeal(ast: Expr, mode: Mode) -> str:
     order = sortBlocks(start)
     teal = flattenBlocks(order)
 
+    verifyOpsForVersion(teal, version)
     verifyOpsForMode(teal, mode)
 
     slots = set()
diff --git a/pyteal/ir/ops.py b/pyteal/ir/ops.py
index 241eab1..cf26d77 100644
--- a/pyteal/ir/ops.py
+++ b/pyteal/ir/ops.py
@@ -1,3 +1,4 @@
+from typing import NamedTuple
 from enum import Enum, Flag, auto
 
 class Mode(Flag):
@@ -8,77 +9,84 @@ class Mode(Flag):
 
 Mode.__module__ = "pyteal"
 
+OpType = NamedTuple('OpType', [('value', str), ('mode', Mode), ('min_version', int)])
+
 class Op(Enum):
     """Enum of program opcodes."""
 
-    err = "err", Mode.Signature | Mode.Application
-    sha256 = "sha256", Mode.Signature | Mode.Application
-    keccak256 = "keccak256", Mode.Signature | Mode.Application
-    sha512_256 = "sha512_256", Mode.Signature | Mode.Application
-    ed25519verify = "ed25519verify", Mode.Signature
-    add = "+", Mode.Signature | Mode.Application
-    minus = "-", Mode.Signature | Mode.Application
-    div = "/", Mode.Signature | Mode.Application
-    mul = "*", Mode.Signature | Mode.Application
-    lt = "<", Mode.Signature | Mode.Application
-    gt = ">", Mode.Signature | Mode.Application
-    le = "<=", Mode.Signature | Mode.Application
-    ge = ">=", Mode.Signature | Mode.Application
-    logic_and = "&&", Mode.Signature | Mode.Application
-    logic_or = "||", Mode.Signature | Mode.Application
-    eq = "==", Mode.Signature | Mode.Application
-    neq = "!=", Mode.Signature | Mode.Application
-    logic_not = "!", Mode.Signature | Mode.Application
-    len = "len", Mode.Signature | Mode.Application
-    itob = "itob", Mode.Signature | Mode.Application
-    btoi = "btoi", Mode.Signature | Mode.Application
-    mod = "%", Mode.Signature | Mode.Application
-    bitwise_or = "|", Mode.Signature | Mode.Application
-    bitwise_and = "&", Mode.Signature | Mode.Application
-    bitwise_xor = "^", Mode.Signature | Mode.Application
-    bitwise_not = "~", Mode.Signature | Mode.Application
-    mulw = "mulw", Mode.Signature | Mode.Application
-    addw = "addw", Mode.Signature | Mode.Application
-    int = "int", Mode.Signature | Mode.Application
-    byte = "byte", Mode.Signature | Mode.Application
-    addr = "addr", Mode.Signature | Mode.Application
-    arg = "arg", Mode.Signature
-    txn = "txn", Mode.Signature | Mode.Application
-    global_ = "global", Mode.Signature | Mode.Application
-    gtxn = "gtxn", Mode.Signature | Mode.Application
-    load = "load", Mode.Signature | Mode.Application
-    store = "store", Mode.Signature | Mode.Application
-    txna = "txna", Mode.Signature | Mode.Application
-    gtxna = "gtxna", Mode.Signature | Mode.Application
-    bnz = "bnz", Mode.Signature | Mode.Application
-    bz = "bz", Mode.Signature | Mode.Application
-    b = "b", Mode.Signature | Mode.Application
-    return_ = "return", Mode.Signature | Mode.Application
-    pop = "pop", Mode.Signature | Mode.Application
-    dup = "dup", Mode.Signature | Mode.Application
-    dup2 = "dup2", Mode.Signature | Mode.Application
-    concat = "concat", Mode.Signature | Mode.Application
-    substring = "substring", Mode.Signature | Mode.Application
-    substring3 = "substring3", Mode.Signature | Mode.Application
-    balance = "balance", Mode.Application
-    app_opted_in = "app_opted_in", Mode.Application
-    app_local_get = "app_local_get", Mode.Application
-    app_local_get_ex = "app_local_get_ex", Mode.Application
-    app_global_get = "app_global_get", Mode.Application
-    app_global_get_ex = "app_global_get_ex", Mode.Application
-    app_local_put = "app_local_put", Mode.Application
-    app_global_put = "app_global_put", Mode.Application
-    app_local_del = "app_local_del", Mode.Application
-    app_global_del = "app_global_del", Mode.Application
-    asset_holding_get = "asset_holding_get", Mode.Application
-    asset_params_get = "asset_params_get", Mode.Application
+    def __str__(self) -> str:
+        return self.value.value
+
+    @property
+    def mode(self) -> Mode:
+        """Get the modes where this op is available."""
+        return self.value.mode
 
-    def __new__(cls, value: str, mode: Mode):
-        obj = object.__new__(cls)
-        obj._value_ = value
-        return obj
+    @property
+    def min_version(self) -> int:
+        """Get the minimum version where this op is available."""
+        return self.value.min_version
 
-    def __init__(self, value: str, mode: Mode):
-        self.mode = mode
+    err               = OpType("err",               Mode.Signature | Mode.Application, 2)
+    sha256            = OpType("sha256",            Mode.Signature | Mode.Application, 2)
+    keccak256         = OpType("keccak256",         Mode.Signature | Mode.Application, 2)
+    sha512_256        = OpType("sha512_256",        Mode.Signature | Mode.Application, 2)
+    ed25519verify     = OpType("ed25519verify",     Mode.Signature,                    2)
+    add               = OpType("+",                 Mode.Signature | Mode.Application, 2)
+    minus             = OpType("-",                 Mode.Signature | Mode.Application, 2)
+    div               = OpType("/",                 Mode.Signature | Mode.Application, 2)
+    mul               = OpType("*",                 Mode.Signature | Mode.Application, 2)
+    lt                = OpType("<",                 Mode.Signature | Mode.Application, 2)
+    gt                = OpType(">",                 Mode.Signature | Mode.Application, 2)
+    le                = OpType("<=",                Mode.Signature | Mode.Application, 2)
+    ge                = OpType(">=",                Mode.Signature | Mode.Application, 2)
+    logic_and         = OpType("&&",                Mode.Signature | Mode.Application, 2)
+    logic_or          = OpType("||",                Mode.Signature | Mode.Application, 2)
+    eq                = OpType("==",                Mode.Signature | Mode.Application, 2)
+    neq               = OpType("!=",                Mode.Signature | Mode.Application, 2)
+    logic_not         = OpType("!",                 Mode.Signature | Mode.Application, 2)
+    len               = OpType("len",               Mode.Signature | Mode.Application, 2)
+    itob              = OpType("itob",              Mode.Signature | Mode.Application, 2)
+    btoi              = OpType("btoi",              Mode.Signature | Mode.Application, 2)
+    mod               = OpType("%",                 Mode.Signature | Mode.Application, 2)
+    bitwise_or        = OpType("|",                 Mode.Signature | Mode.Application, 2)
+    bitwise_and       = OpType("&",                 Mode.Signature | Mode.Application, 2)
+    bitwise_xor       = OpType("^",                 Mode.Signature | Mode.Application, 2)
+    bitwise_not       = OpType("~",                 Mode.Signature | Mode.Application, 2)
+    mulw              = OpType("mulw",              Mode.Signature | Mode.Application, 2)
+    addw              = OpType("addw",              Mode.Signature | Mode.Application, 2)
+    int               = OpType("int",               Mode.Signature | Mode.Application, 2)
+    byte              = OpType("byte",              Mode.Signature | Mode.Application, 2)
+    addr              = OpType("addr",              Mode.Signature | Mode.Application, 2)
+    arg               = OpType("arg",               Mode.Signature,                    2)
+    txn               = OpType("txn",               Mode.Signature | Mode.Application, 2)
+    global_           = OpType("global",            Mode.Signature | Mode.Application, 2)
+    gtxn              = OpType("gtxn",              Mode.Signature | Mode.Application, 2)
+    load              = OpType("load",              Mode.Signature | Mode.Application, 2)
+    store             = OpType("store",             Mode.Signature | Mode.Application, 2)
+    txna              = OpType("txna",              Mode.Signature | Mode.Application, 2)
+    gtxna             = OpType("gtxna",             Mode.Signature | Mode.Application, 2)
+    bnz               = OpType("bnz",               Mode.Signature | Mode.Application, 2)
+    bz                = OpType("bz",                Mode.Signature | Mode.Application, 2)
+    b                 = OpType("b",                 Mode.Signature | Mode.Application, 2)
+    return_           = OpType("return",            Mode.Signature | Mode.Application, 2)
+    pop               = OpType("pop",               Mode.Signature | Mode.Application, 2)
+    dup               = OpType("dup",               Mode.Signature | Mode.Application, 2)
+    dup2              = OpType("dup2",              Mode.Signature | Mode.Application, 2)
+    concat            = OpType("concat",            Mode.Signature | Mode.Application, 2)
+    substring         = OpType("substring",         Mode.Signature | Mode.Application, 2)
+    substring3        = OpType("substring3",        Mode.Signature | Mode.Application, 2)
+    balance           = OpType("balance",           Mode.Application,                  2)
+    app_opted_in      = OpType("app_opted_in",      Mode.Application,                  2)
+    app_local_get     = OpType("app_local_get",     Mode.Application,                  2)
+    app_local_get_ex  = OpType("app_local_get_ex",  Mode.Application,                  2)
+    app_global_get    = OpType("app_global_get",    Mode.Application,                  2)
+    app_global_get_ex = OpType("app_global_get_ex", Mode.Application,                  2)
+    app_local_put     = OpType("app_local_put",     Mode.Application,                  2)
+    app_global_put    = OpType("app_global_put",    Mode.Application,                  2)
+    app_local_del     = OpType("app_local_del",     Mode.Application,                  2)
+    app_global_del    = OpType("app_global_del",    Mode.Application,                  2)
+    asset_holding_get = OpType("asset_holding_get", Mode.Application,                  2)
+    asset_params_get  = OpType("asset_params_get",  Mode.Application,                  2)
 
 Op.__module__ = "pyteal"
diff --git a/pyteal/ir/tealop.py b/pyteal/ir/tealop.py
index 83aa8cf..27c062d 100644
--- a/pyteal/ir/tealop.py
+++ b/pyteal/ir/tealop.py
@@ -26,7 +26,7 @@ class TealOp(TealComponent):
 
     def assemble(self) -> str:
         from ..ast import ScratchSlot
-        parts = [self.op.value]
+        parts = [str(self.op)]
         for arg in self.args:
             if isinstance(arg, ScratchSlot):
                 raise TealInternalError("Slot not assigned: {}".format(arg))
@@ -39,7 +39,7 @@ class TealOp(TealComponent):
         return " ".join(parts)
     
     def __repr__(self) -> str:
-        args = [self.op.__str__()]
+        args = [str(self.op)]
         for a in self.args:
             args.append(repr(a))
 


Test patch:
diff --git a/pyteal/compiler_test.py b/pyteal/compiler_test.py
index cbc6780..52b6c1a 100644
--- a/pyteal/compiler_test.py
+++ b/pyteal/compiler_test.py
@@ -344,3 +344,22 @@ app_global_get
 
     with pytest.raises(TealInputError):
         compileTeal(expr, Mode.Signature)
+
+def test_compile_version():
+    expr = Int(1)
+
+    with pytest.raises(TealInputError):
+        compileTeal(expr, Mode.Signature, 1)
+    
+    expected_version_2 = """
+#pragma version 2
+int 1
+""".strip()
+    actual_version_2 = compileTeal(expr, Mode.Signature, 2)
+    assert actual_version_2 == expected_version_2
+
+    actual_default = compileTeal(expr, Mode.Signature)
+    assert actual_default == expected_version_2
+
+    with pytest.raises(TealInputError):
+        compileTeal(expr, Mode.Signature, 3)