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
def _codecopy_gas_bound(num_bytes): return GAS_CODECOPY_WORD * ceil32(num_bytes) // 32
def _calldatacopy_gas_bound(num_bytes): return GAS_CALLDATACOPY_WORD * ceil32(num_bytes) // 32
def _identity_gas_bound(num_bytes): return GAS_IDENTITY + GAS_IDENTITYWORD * (ceil32(num_bytes) // 32)
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))))
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))))