Beispiel #1
0
    def __init__(
        self,
        value: Union[str, int],
        args: List["LLLnode"] = None,
        typ: NodeType = None,
        location: str = None,
        pos: Optional[Tuple[int, int]] = None,
        annotation: Optional[str] = None,
        mutable: bool = True,
        add_gas_estimate: int = 0,
        valency: Optional[int] = None,
        encoding: Encoding = Encoding.VYPER,
    ):
        if args is None:
            args = []

        self.value = value
        self.args = args
        # TODO remove this sanity check once mypy is more thorough
        assert isinstance(typ, NodeType) or typ is None, repr(typ)
        self.typ = typ
        self.location = location
        self.pos = pos
        self.annotation = annotation
        self.mutable = mutable
        self.add_gas_estimate = add_gas_estimate
        self.encoding = encoding
        self.as_hex = AS_HEX_DEFAULT

        # Optional annotation properties for gas estimation
        self.total_gas = None
        self.func_name = None

        def _check(condition, err):
            if not condition:
                raise CompilerPanic(str(err))

        _check(self.value is not None, "None is not allowed as LLLnode value")

        # Determine this node's valency (1 if it pushes a value on the stack,
        # 0 otherwise) and checks to make sure the number and valencies of
        # children are correct. Also, find an upper bound on gas consumption
        # Numbers
        if isinstance(self.value, int):
            _check(len(self.args) == 0, "int can't have arguments")
            self.valency = 1
            self.gas = 5
        elif isinstance(self.value, str):
            # Opcodes and pseudo-opcodes (e.g. clamp)
            if self.value.upper() in get_lll_opcodes():
                _, ins, outs, gas = get_lll_opcodes()[self.value.upper()]
                self.valency = outs
                _check(
                    len(self.args) == ins,
                    f"Number of arguments mismatched: {self.value} {self.args}",
                )
                # We add 2 per stack height at push time and take it back
                # at pop time; this makes `break` easier to handle
                self.gas = gas + 2 * (outs - ins)
                for arg in self.args:
                    # pop and pass are used to push/pop values on the stack to be
                    # consumed for internal functions, therefore we whitelist this as a zero valency
                    # allowed argument.
                    zero_valency_whitelist = {"pass", "pop"}
                    _check(
                        arg.valency == 1
                        or arg.value in zero_valency_whitelist,
                        f"invalid argument to `{self.value}`: {arg}",
                    )
                    self.gas += arg.gas
                # Dynamic gas cost: 8 gas for each byte of logging data
                if self.value.upper()[0:3] == "LOG" and isinstance(
                        self.args[1].value, int):
                    self.gas += self.args[1].value * 8
                # Dynamic gas cost: non-zero-valued call
                if self.value.upper() == "CALL" and self.args[2].value != 0:
                    self.gas += 34000
                # Dynamic gas cost: filling sstore (ie. not clearing)
                elif self.value.upper(
                ) == "SSTORE" and self.args[1].value != 0:
                    self.gas += 15000
                # Dynamic gas cost: calldatacopy
                elif self.value.upper() in ("CALLDATACOPY", "CODECOPY",
                                            "EXTCODECOPY"):
                    size = 34000
                    size_arg_index = 3 if self.value.upper(
                    ) == "EXTCODECOPY" else 2
                    size_arg = self.args[size_arg_index]
                    if isinstance(size_arg.value, int):
                        size = size_arg.value
                    self.gas += ceil32(size) // 32 * 3
                # Gas limits in call
                if self.value.upper() == "CALL" and isinstance(
                        self.args[0].value, int):
                    self.gas += self.args[0].value
            # If statements
            elif self.value == "if":
                if len(self.args) == 3:
                    self.gas = self.args[0].gas + max(self.args[1].gas,
                                                      self.args[2].gas) + 3
                if len(self.args) == 2:
                    self.gas = self.args[0].gas + self.args[1].gas + 17
                _check(
                    self.args[0].valency > 0,
                    f"zerovalent argument as a test to an if statement: {self.args[0]}",
                )
                _check(
                    len(self.args) in (2, 3),
                    "if statement can only have 2 or 3 arguments")
                self.valency = self.args[1].valency
            # With statements: with <var> <initial> <statement>
            elif self.value == "with":
                _check(len(self.args) == 3, self)
                _check(
                    len(self.args[0].args) == 0
                    and isinstance(self.args[0].value, str),
                    f"first argument to with statement must be a variable name: {self.args[0]}",
                )
                _check(
                    self.args[1].valency == 1 or self.args[1].value == "pass",
                    f"zerovalent argument to with statement: {self.args[1]}",
                )
                self.valency = self.args[2].valency
                self.gas = sum([arg.gas for arg in self.args]) + 5
            # Repeat statements: repeat <index_name> <startval> <rounds> <rounds_bound> <body>
            elif self.value == "repeat":
                _check(
                    len(self.args) == 5,
                    "repeat(index_name, startval, rounds, rounds_bound, body)")

                counter_ptr = self.args[0]
                start = self.args[1]
                repeat_count = self.args[2]
                repeat_bound = self.args[3]
                body = self.args[4]

                _check(
                    isinstance(repeat_bound.value, int)
                    and repeat_bound.value > 0,
                    f"repeat bound must be a compile-time positive integer: {self.args[2]}",
                )
                _check(repeat_count.valency == 1, repeat_count)
                _check(counter_ptr.valency == 1, counter_ptr)
                _check(start.valency == 1, start)

                self.valency = 0

                self.gas = counter_ptr.gas + start.gas
                self.gas += 3  # gas for repeat_bound
                int_bound = int(repeat_bound.value)
                self.gas += int_bound * (body.gas + 50) + 30

                if repeat_count != repeat_bound:
                    # gas for assert(repeat_count <= repeat_bound)
                    self.gas += 18

            # Seq statements: seq <statement> <statement> ...
            elif self.value == "seq":
                self.valency = self.args[-1].valency if self.args else 0
                self.gas = sum([arg.gas for arg in self.args]) + 30

            # GOTO is a jump with args
            # e.g. (goto my_label x y z) will push x y and z onto the stack,
            # then JUMP to my_label.
            elif self.value in ("goto", "exit_to"):
                for arg in self.args:
                    _check(
                        arg.valency == 1 or arg.value == "pass",
                        f"zerovalent argument to goto {arg}",
                    )

                self.valency = 0
                self.gas = sum([arg.gas for arg in self.args])
            elif self.value == "label":
                if not self.args[1].value == "var_list":
                    raise CodegenPanic(
                        f"2nd argument to label must be var_list, {self}")
                self.valency = 0
                self.gas = 1 + sum(t.gas for t in self.args)
            # var_list names a variable number stack variables
            elif self.value == "var_list":
                for arg in self.args:
                    if not isinstance(arg.value, str) or len(arg.args) > 0:
                        raise CodegenPanic(
                            f"var_list only takes strings: {self.args}")
                self.valency = 0
                self.gas = 0

            # Multi statements: multi <expr> <expr> ...
            elif self.value == "multi":
                for arg in self.args:
                    _check(
                        arg.valency > 0,
                        f"Multi expects all children to not be zerovalent: {arg}"
                    )
                self.valency = sum([arg.valency for arg in self.args])
                self.gas = sum([arg.gas for arg in self.args])
            elif self.value == "deploy":
                self.valency = 0
                self.gas = NullAttractor()  # unknown
            # Stack variables
            else:
                self.valency = 1
                self.gas = 3
        elif self.value is None:
            self.valency = 1
            # None LLLnodes always get compiled into something else, e.g.
            # mzero or PUSH1 0, and the gas will get re-estimated then.
            self.gas = 3
        else:
            raise CompilerPanic(
                f"Invalid value for LLL AST node: {self.value}")
        assert isinstance(self.args, list)

        if valency is not None:
            self.valency = valency

        self.gas += self.add_gas_estimate
Beispiel #2
0
def _codecopy_gas_bound(num_bytes):
    return GAS_CODECOPY_WORD * ceil32(num_bytes) // 32
Beispiel #3
0
def _calldatacopy_gas_bound(num_bytes):
    return GAS_CALLDATACOPY_WORD * ceil32(num_bytes) // 32
Beispiel #4
0
def _identity_gas_bound(num_bytes):
    return GAS_IDENTITY + GAS_IDENTITYWORD * (ceil32(num_bytes) // 32)
Beispiel #5
0
def copy_bytes(dst, src, length, length_bound):
    annotation = f"copy up to {length_bound} bytes from {src} to {dst}"

    src = IRnode.from_list(src)
    dst = IRnode.from_list(dst)
    length = IRnode.from_list(length)

    with src.cache_when_complex("src") as (
            b1, src), length.cache_when_complex("copy_bytes_count") as (
                b2, length), dst.cache_when_complex("dst") as (b3, dst):

        assert isinstance(length_bound, int) and length_bound >= 0

        # correctness: do not clobber dst
        if length_bound == 0:
            return IRnode.from_list(["seq"], annotation=annotation)
        # performance: if we know that length is 0, do not copy anything
        if length.value == 0:
            return IRnode.from_list(["seq"], annotation=annotation)

        assert src.is_pointer and dst.is_pointer

        # fast code for common case where num bytes is small
        # TODO expand this for more cases where num words is less than ~8
        if length_bound <= 32:
            copy_op = STORE(dst, LOAD(src))
            ret = IRnode.from_list(copy_op, annotation=annotation)
            return b1.resolve(b2.resolve(b3.resolve(ret)))

        if dst.location == MEMORY and src.location in (MEMORY, CALLDATA, DATA):
            # special cases: batch copy to memory
            # TODO: iloadbytes
            if src.location == MEMORY:
                copy_op = ["staticcall", "gas", 4, src, length, dst, length]
                gas_bound = _identity_gas_bound(length_bound)
            elif src.location == CALLDATA:
                copy_op = ["calldatacopy", dst, src, length]
                gas_bound = _calldatacopy_gas_bound(length_bound)
            elif src.location == DATA:
                copy_op = ["dloadbytes", dst, src, length]
                # note: dloadbytes compiles to CODECOPY
                gas_bound = _codecopy_gas_bound(length_bound)

            ret = IRnode.from_list(copy_op,
                                   annotation=annotation,
                                   add_gas_estimate=gas_bound)
            return b1.resolve(b2.resolve(b3.resolve(ret)))

        if dst.location == IMMUTABLES and src.location in (MEMORY, DATA):
            # TODO istorebytes-from-mem, istorebytes-from-calldata(?)
            # compile to identity, CODECOPY respectively.
            pass

        # general case, copy word-for-word
        # pseudocode for our approach (memory-storage as example):
        # for i in range(len, bound=MAX_LEN):
        #   sstore(_dst + i, mload(src + i * 32))
        i = IRnode.from_list(_freshname("copy_bytes_ix"), typ="uint256")

        # optimized form of (div (ceil32 len) 32)
        n = ["div", ["add", 31, length], 32]
        n_bound = ceil32(length_bound) // 32

        dst_i = add_ofst(dst, _mul(i, dst.location.word_scale))
        src_i = add_ofst(src, _mul(i, src.location.word_scale))

        copy_one_word = STORE(dst_i, LOAD(src_i))

        main_loop = ["repeat", i, 0, n, n_bound, copy_one_word]

        return b1.resolve(
            b2.resolve(
                b3.resolve(IRnode.from_list(main_loop,
                                            annotation=annotation))))
Beispiel #6
0
def copy_bytes(dst, src, length, length_bound, pos=None):
    annotation = f"copy_bytes from {src} to {dst}"

    src = LLLnode.from_list(src)
    dst = LLLnode.from_list(dst)
    length = LLLnode.from_list(length)

    with src.cache_when_complex("src") as (
            b1, src), length.cache_when_complex("copy_word_count") as (
                b2,
                length,
            ), dst.cache_when_complex("dst") as (b3, dst):

        # fast code for common case where num bytes is small
        # TODO expand this for more cases where num words is less than ~8
        if length_bound <= 32:
            copy_op = [
                store_op(dst.location), dst, [load_op(src.location), src]
            ]
            ret = LLLnode.from_list(copy_op, annotation=annotation)
            return b1.resolve(b2.resolve(b3.resolve(ret)))

        if dst.location == "memory" and src.location in ("memory", "calldata",
                                                         "code"):
            # special cases: batch copy to memory
            if src.location == "memory":
                copy_op = ["staticcall", "gas", 4, src, length, dst, length]
                gas_bound = _identity_gas_bound(length_bound)
            elif src.location == "calldata":
                copy_op = ["calldatacopy", dst, src, length]
                gas_bound = _calldatacopy_gas_bound(length_bound)
            elif src.location == "code":
                copy_op = ["codecopy", dst, src, length]
                gas_bound = _codecopy_gas_bound(length_bound)

            ret = LLLnode.from_list(copy_op,
                                    annotation=annotation,
                                    add_gas_estimate=gas_bound)
            return b1.resolve(b2.resolve(b3.resolve(ret)))

        # general case, copy word-for-word
        # pseudocode for our approach (memory-storage as example):
        # for i in range(len, bound=MAX_LEN):
        #   sstore(_dst + i, mload(src + i * 32))
        # TODO should use something like
        # for i in range(len, bound=MAX_LEN):
        #   _dst += 1
        #   src += 32
        #   sstore(_dst, mload(src))

        i = LLLnode.from_list(_freshname("copy_bytes_ix"), typ="uint256")

        if src.location in ("memory", "calldata", "code"):
            loader = [load_op(src.location), ["add", src, _mul(32, i)]]
        elif src.location == "storage":
            loader = [load_op(src.location), ["add", src, i]]
        else:
            raise CompilerPanic(
                f"Unsupported location: {src.location}")  # pragma: notest

        if dst.location == "memory":
            setter = ["mstore", ["add", dst, _mul(32, i)], loader]
        elif dst.location == "storage":
            setter = ["sstore", ["add", dst, i], loader]
        else:
            raise CompilerPanic(
                f"Unsupported location: {dst.location}")  # pragma: notest

        n = ["div", ["ceil32", length], 32]
        n_bound = ceil32(length_bound) // 32

        main_loop = ["repeat", i, 0, n, n_bound, setter]

        return b1.resolve(
            b2.resolve(
                b3.resolve(
                    LLLnode.from_list(main_loop,
                                      annotation=annotation,
                                      pos=pos))))