├── .gitignore ├── obf ├── __init__.py ├── node_shim.py ├── brainfuck.py └── _cli.py ├── LICENSE ├── setup.py └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Distribution / packaging 7 | .Python 8 | build/ 9 | develop-eggs/ 10 | dist/ 11 | downloads/ 12 | eggs/ 13 | .eggs/ 14 | lib/ 15 | lib64/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | wheels/ 20 | *.egg-info/ 21 | .installed.cfg 22 | *.egg 23 | MANIFEST 24 | 25 | htmlcov/ 26 | .coverage 27 | .coverage.* 28 | *.log 29 | *.out 30 | 31 | # Environments 32 | .env 33 | .venv 34 | env/ 35 | venv/ 36 | ENV/ 37 | env.bak/ 38 | venv.bak/ 39 | 40 | # mypy 41 | .mypy_cache/ 42 | 43 | *.json 44 | -------------------------------------------------------------------------------- /obf/__init__.py: -------------------------------------------------------------------------------- 1 | try: 2 | from Crypto.Hash import keccak 3 | 4 | def keccak_256(x): return keccak.new(digest_bits=256, data=x).digest() 5 | except ImportError: 6 | import sha3 as _sha3 7 | 8 | def keccak_256(x): return _sha3.keccak_256(x).digest() 9 | 10 | import remerkleable.settings as remerkleable_settings 11 | 12 | 13 | def merkle_hash(left: bytes, right: bytes) -> bytes: 14 | return keccak_256(left + right) 15 | 16 | 17 | # The EVM is big-endian, it will be easier to implement a verifier in the EVM if we use big-endian integers, 18 | # even though SSZ spec is little-endian. 19 | remerkleable_settings.ENDIANNESS = 'big' 20 | 21 | # Keccak-256 is cheaper in the EVM than calling a sha-256 precompile. 22 | remerkleable_settings.merkle_hash = merkle_hash 23 | 24 | # re-initialize the zero-hashes we use to pad list trees, to use the new hash func 25 | remerkleable_settings.init_zero_hashes() 26 | 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 @protolambda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open("README.md", "rt", encoding="utf8") as f: 4 | readme = f.read() 5 | 6 | setup( 7 | name="optimistic-brainfuck", 8 | description="Optimistic brainfuck rollup fraud proof generator", 9 | version="0.0.1", 10 | long_description=readme, 11 | long_description_content_type="text/markdown", 12 | author="protolambda", 13 | author_email="proto+pip@protolambda.com", 14 | url="https://github.com/protolambda/optimistic-brainfuck", 15 | python_requires=">=3.8, <4", 16 | license="MIT", 17 | packages=find_packages(), 18 | py_modules=["obf"], 19 | tests_require=[], 20 | extras_require={ 21 | "testing": ["pytest"], 22 | "linting": ["flake8", "mypy"], 23 | }, 24 | install_requires=[ 25 | "remerkleable==0.1.24", 26 | "Click", 27 | # one of the two for keccak256 28 | "pysha3", 29 | "pycryptodome>=3.3.1" 30 | ], 31 | include_package_data=True, 32 | entry_points={ 33 | 'console_scripts': [ 34 | 'obf = obf._cli:cli', 35 | ], 36 | }, 37 | keywords=["optimistic", "rollup", "optimism", "fraud-proof", "brainfuck", "ethereum"], 38 | classifiers=[ 39 | "Development Status :: 4 - Beta", 40 | "Intended Audience :: Developers", 41 | "License :: OSI Approved :: MIT License", 42 | "Natural Language :: English", 43 | "Programming Language :: Python :: 3", 44 | "Programming Language :: Python :: 3.8", 45 | "Programming Language :: Python :: Implementation :: PyPy", 46 | "Operating System :: OS Independent", 47 | ], 48 | ) 49 | -------------------------------------------------------------------------------- /obf/node_shim.py: -------------------------------------------------------------------------------- 1 | from typing import Generator 2 | from remerkleable.tree import Node, PairNode, Gindex 3 | 4 | 5 | class ShimNode(PairNode): 6 | __slots__ = ('_touched_left', '_touched_right') 7 | 8 | _touched_left: bool 9 | _touched_right: bool 10 | 11 | def __init__(self, left: Node, right: Node): 12 | self.reset_shim() 13 | 14 | if not left.is_leaf(): 15 | if isinstance(left, ShimNode): 16 | left.reset_shim() 17 | else: 18 | left_root = left.merkle_root() # preserve hash cache 19 | left = ShimNode(left.get_left(), left.get_right()) 20 | left._root = left_root 21 | 22 | if not right.is_leaf(): 23 | if isinstance(right, ShimNode): 24 | right.reset_shim() 25 | else: 26 | right_root = right.merkle_root() # preserve hash cache 27 | right = ShimNode(right.get_left(), right.get_right()) 28 | right._root = right_root 29 | 30 | super(ShimNode, self).__init__(left, right) 31 | 32 | @staticmethod 33 | def shim(node: Node) -> Node: 34 | if isinstance(node, ShimNode): 35 | node.reset_shim() 36 | return node 37 | if node.is_leaf(): 38 | return node 39 | # calc root first, cached hashed can then hydrate the shim 40 | node_root = node.merkle_root() 41 | sh = ShimNode(node.get_left(), node.get_right()) 42 | sh._root = node_root 43 | return sh 44 | 45 | def get_touched_gindices(self, g: int = 1) -> Generator[Gindex, None, None]: 46 | if self._touched_left: 47 | if isinstance(self.left, ShimNode): 48 | yield from self.left.get_touched_gindices(g*2) 49 | else: 50 | yield g*2 51 | else: 52 | yield g*2 53 | if self._touched_right: 54 | if isinstance(self.right, ShimNode): 55 | yield from self.right.get_touched_gindices(g*2+1) 56 | else: 57 | yield g*2+1 58 | else: 59 | yield g*2+1 60 | 61 | def reset_shim(self) -> None: 62 | self._touched_left = False 63 | self._touched_right = False 64 | 65 | def get_left(self) -> Node: 66 | self._touched_left = True 67 | return super().get_left() 68 | 69 | def get_right(self) -> Node: 70 | self._touched_right = True 71 | return super().get_right() 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Optimistic Brainfuck 2 | 3 | Ever wanted to run [Brainfuck](https://esolangs.org/wiki/Brainfuck) on ethereum? Don't ask, now you can! And at a fraction of the cost, thanks to optimistic rollup tech! 4 | 5 | If you can plug in Brainfuck, you can plug in **anything**. [EVM is a work in progress](https://github.com/protolambda/macula). 6 | 7 | ## State 8 | 9 | State: 10 | - There are 256 brainfuck contract slots 11 | - Contracts can only be created via a L1 deposit, with a fee (not implemented) 12 | - Memory cells and pointer are persisted per contract, essentially cheap and easy to navigate storage 13 | - Regular transactions are input data to the contract specified by the transaction, it's up to the contract to read and process it 14 | - The l1 sender is always put in the first 20 input bytes, so the contract can trust the user (compare it against its memory) 15 | - Contract program counter always starts at 0 16 | - Execution stops as soon as the contract outputs a `0x00` (success, changes are persisted). 17 | Higher codes are used as error codes (reverts to pre-state memory and ptr), e.g. stack-overflow, out-of-gas, etc. 18 | `0xff` is reserved as placeholder during execution. 19 | 20 | Gas: a transaction gets 1000 + 128 times the gas based on its payload length, to loop around and do fun things. 21 | 1 gas is 1 brainfuck opcode. No gas is returned on exit. These numbers can be tuned. 22 | 23 | 24 | ## Running 25 | 26 | Quick install in encapsulated environment: 27 | ```shell 28 | python -m venv venv 29 | source venv/bin/activate 30 | pip install -e . 31 | ``` 32 | 33 | Get a genesis state: 34 | ```shell 35 | # create a state with example contract 36 | obf init-state state.json 37 | ``` 38 | Output: 39 | ```json 40 | { 41 | "contracts": { 42 | "0": { 43 | "code": ",,,,,,,,,,,,,,,,,,,,,[>+++++++<-]", 44 | "ptr": 0, 45 | "cells": [ 46 | 0 47 | ] 48 | } 49 | } 50 | } 51 | ``` 52 | This is a simple contract that skips over the standard sender address data (first 20 bytes), and multiplies the first byte with 7. 53 | 54 | 55 | ```shell 56 | # call the default 0 contract with some input data, and a dummy 0xaa.... address 57 | obf transition state.json state_out.json '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 0 '0x03' 58 | ``` 59 | 60 | This produces `state_out.json`: 61 | ```json 62 | { 63 | "contracts": { 64 | "0": { 65 | "code": ",,,,,,,,,,,,,,,,,,,,,[>+++++++<-]", 66 | "cells": [ 67 | 0, 68 | 21 69 | ], 70 | "ptr": 0 71 | } 72 | } 73 | } 74 | ``` 75 | 76 | Now say some malicious sequencer committed to a different state of this contract, what happens? 77 | 1. Any honest user sees the mismatch with their local transition 78 | 2. Generate a fraud proof witness 79 | 3. They open a challenge on-chain 80 | 4. They do an interactive game to find the first differing step 81 | 5. They extract the witness for this particular step from the fraud proof data 82 | 6. They submit it, to finish the on-chain work, showing that indeed the sequencer was claiming a different result 83 | than could be computed with a tiny step on-chain, on top of the previous undisputed step (base case is just loading the transaction into a step). 84 | 85 | Generate a fraud proof: 86 | ```shell 87 | obf gen state.json proof.json '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 0 '0x03' 88 | ``` 89 | 90 | Output: 91 | ```js 92 | { 93 | "nodes": { /* key -> [left node, right node] */}, 94 | "step_roots": [ /* merkle roots of each step, as well as the final output, to play dispute game on */], 95 | "access": [ /* per step, a list of 32-byte encoded generalized indices, to point which nodes are relevant to the step */] 96 | } 97 | ``` 98 | 99 | Build a witness for a specific step, e.g. step 53: 100 | ```shell 101 | step-witness proof.json step53.json 53 102 | ``` 103 | 104 | ```js 105 | { 106 | "node_by_gindex": { 107 | "0x0000000000000000000000000000000000000000000000000000000000000008": "0x0000000000000433000000000000000000000000000000000000000000000000", 108 | "0x0000000000000000000000000000000000000000000000000000000000000009": "0x0000001d00000000000000000000000000000000000000000000000000000000", 109 | // some more gindex -> value nodes 110 | }, 111 | "pre_root": "0x3ea782a870598661a410b833761ab5483002362cc4ce077ab96bf5e038be394a", 112 | "post_root": "0x438d23b78af4c6701d00630bb716c6dfdab5390ce0a5425fe5419f0cd0242184", 113 | "step": 53 114 | } 115 | ``` 116 | 117 | And now the last part: format the witness as a call to the L1 executor contract, to finish the game with. 118 | This prototype does not have a solidity implementation of the verification (yet? next project maybe), but it does have a python one: 119 | ```shell 120 | obf verify step53.json "0x438d23b78af4c6701d00630bb716c6dfdab5390ce0a5425fe5419f0cd0242184" 121 | ``` 122 | ``` 123 | parsing fraud proof 124 | verifying fraud proof 125 | transaction was effective, post contract root: 0x438d23b78af4c6701d00630bb716c6dfdab5390ce0a5425fe5419f0cd0242184 126 | root matches, no fraud 127 | ``` 128 | 129 | Or with a slightly different root (thus wrong, like a malicious actor might try): 130 | ```shell 131 | obf verify step53.json "0x438d23b78af4c6701d00630bb716c6dfdab5390ce0a5425fe5419f0cd0242183" 132 | ``` 133 | ``` 134 | parsing fraud proof 135 | verifying fraud proof 136 | transaction was effective, post contract root: 0x438d23b78af4c6701d00630bb716c6dfdab5390ce0a5425fe5419f0cd0242184 137 | root did not match, fraud detected! 138 | ``` 139 | 140 | ## License 141 | 142 | MIT, see [`LICENSE`](./LICENSE) file. 143 | -------------------------------------------------------------------------------- /obf/brainfuck.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | from typing import TypeVar, Type, Callable 3 | from remerkleable.complex import Container, List 4 | from remerkleable.basic import uint64, uint32, uint8 5 | from remerkleable.bitfields import Bitlist 6 | from remerkleable.byte_arrays import ByteVector 7 | 8 | 9 | class Address(ByteVector[20]): 10 | pass 11 | 12 | 13 | # Brainfuck rules in rollup TLDR: 14 | # 15 | # bits brainfuck meaning 16 | # 000 > Move the pointer to the right 17 | # 001 < Move the pointer to the left 18 | # 010 + Increment the memory cell at the pointer 19 | # 011 - Decrement the memory cell at the pointer 20 | # 100 . Output the character signified by the cell at the pointer 21 | # 101 , Input a character and store it in the cell at the pointer 22 | # 110 [ Jump past the matching ] if the cell at the pointer is 0 23 | # 111 ] Jump back to the matching [ if the cell at the pointer is nonzero 24 | # 25 | 26 | GAS_FREE_STIPEND = 1000 27 | L1_CALLDATA_TO_L2_GAS_MULTIPLIER = 128 28 | 29 | # 64 KiB per transaction 30 | MAX_CODE_SIZE = 64 * 1024 31 | 32 | # Note: we fail transactions that go out of bounds, we do not wrap around 33 | # 128 KiB memory per transaction 34 | MAX_CELL_COUNT = 128 * 1024 35 | 36 | # 64 KiB per payload 37 | MAX_PAYLOAD_DATA = 64 * 1024 38 | 39 | # maximum amount of yet unmatched '[' at any time 40 | MAX_STACK_DEPTH = 1024 41 | 42 | MAX_CONTRACTS = 256 43 | 44 | brainfuck_chars = ['>', '<', '+', '-', '.', ',', '[', ']'] 45 | 46 | class ExitCodes(IntEnum): 47 | OK = 0 48 | StackOverflow = 1 49 | StackUnderflow = 2 50 | NegativePtr = 3 51 | PtrTooHigh = 4 52 | OutOfGas = 5 53 | 54 | 55 | class OpCode(IntEnum): 56 | MOVE_RIGHT = 0b000 57 | MOVE_LEFT = 0b001 58 | INCR_CELL = 0b010 59 | DECR_CELL = 0b011 60 | GET_CELL = 0b100 61 | PUT_CELL = 0b101 62 | JUMP_COND = 0b110 63 | JUMP_BACK = 0b111 64 | 65 | def character(self) -> str: 66 | return brainfuck_chars[self] 67 | 68 | def __str__(self): 69 | return self.character() 70 | 71 | 72 | V = TypeVar('V') 73 | 74 | 75 | # 3 bits per brainfuck opcode, utilize all that data! 76 | class Code(Bitlist[MAX_CODE_SIZE]): 77 | @classmethod 78 | def from_pretty_str(cls: Type[V], v: str) -> V: 79 | ops = [brainfuck_chars.index(c) for c in v] 80 | bits = [] 81 | for op in ops: 82 | for j in range(2, -1, -1): 83 | bits.append(op & (1 << j) != 0) 84 | return cls(bits) 85 | 86 | def to_pretty_str(self) -> str: 87 | bits = list(self) # faster 88 | ops = [((int(bits[j]) << 2) | (int(bits[j+1]) << 1) | int(bits[j+2])) for j in range(0, len(bits), 3)] 89 | return ''.join(brainfuck_chars[op] for op in ops) 90 | 91 | def op_count(self) -> uint32: 92 | return len(self) // 3 93 | 94 | def get_op(self, i: uint32) -> OpCode: 95 | i *= 3 96 | a, b, c = self[i], self[i + 1], self[i + 2] 97 | op = (int(a) << 2) | (int(b) << 1) | int(c) 98 | return OpCode(op) 99 | 100 | 101 | class Cells(List[uint8, MAX_CELL_COUNT]): 102 | pass 103 | 104 | 105 | class PayloadData(List[uint8, MAX_PAYLOAD_DATA]): 106 | pass 107 | 108 | 109 | class Contract(Container): 110 | code: Code 111 | cells: Cells 112 | ptr: uint32 113 | # the pc is always reset to 0 in each transaction 114 | 115 | 116 | class Step(Container): 117 | # a transaction spends gas, to constrain computation (infinite loops) 118 | # gas is "free", just based on L1 calldata cost 119 | gas: uint64 120 | 121 | # Program counter, pointing to current op code 122 | pc: uint32 123 | # keeps track of the pc of every past opening bracket '[', to return to later 124 | stack: List[uint32, MAX_STACK_DEPTH] 125 | # any opcode, except [ and ] is ignored until this indent is back to 0 126 | indent: uint32 127 | 128 | contract: Contract 129 | 130 | input_read: uint32 131 | input_data: PayloadData 132 | 133 | result_code: uint8 134 | 135 | 136 | def parse_tx(contract: Contract, sender: Address, payload: bytes) -> Step: 137 | gas = uint64(GAS_FREE_STIPEND + len(payload) * L1_CALLDATA_TO_L2_GAS_MULTIPLIER) 138 | return Step( 139 | gas=gas, 140 | pc=0, 141 | stack=[], 142 | contract=contract, 143 | input_read=0, 144 | input_data=PayloadData(list(sender)+list(payload)), 145 | result_code=0xff, # unused value to start with, either 0 or 1 at the end 146 | ) 147 | 148 | 149 | def next_step(last: Step) -> Step: 150 | next = last.copy() 151 | pc = last.pc 152 | 153 | size = last.contract.code.op_count() 154 | if pc >= size: 155 | next.result_code = ExitCodes.OK 156 | return next 157 | 158 | # count 1 gas for this operation 159 | if last.gas == 0: 160 | next.result_code = ExitCodes.OutOfGas 161 | return next 162 | 163 | next.gas -= 1 164 | 165 | # run the operation 166 | op = last.contract.code.get_op(pc) 167 | 168 | # print("----") 169 | # print("op", op) 170 | # print("ptr", last.contract.ptr) 171 | # print("stack", list(last.stack)) 172 | # print("cells", list(last.contract.cells)) 173 | # print("indent", last.indent) 174 | 175 | if last.indent > 0: 176 | if op == OpCode.JUMP_COND: 177 | if last.indent > MAX_STACK_DEPTH: 178 | next.result_code = ExitCodes.StackOverflow 179 | return next 180 | next.indent += 1 181 | next.pc += 1 182 | return next 183 | elif op == OpCode.JUMP_BACK: 184 | next.indent -= 1 185 | next.pc += 1 186 | return next 187 | else: 188 | next.pc += 1 189 | return next 190 | 191 | if op == OpCode.MOVE_RIGHT: 192 | if last.contract.ptr < MAX_CELL_COUNT - 1: 193 | if last.contract.ptr + 1 >= len(last.contract.cells): # dynamically grow the cells data 194 | next.contract.cells.append(uint8(0)) 195 | next.contract.ptr += 1 196 | next.pc += 1 197 | return next 198 | else: 199 | next.result_code = ExitCodes.PtrTooHigh 200 | return next 201 | elif op == OpCode.MOVE_LEFT: 202 | if last.contract.ptr != 0: 203 | next.contract.ptr -= 1 204 | next.pc += 1 205 | return next 206 | else: 207 | next.result_code = ExitCodes.NegativePtr 208 | return next 209 | elif op == OpCode.INCR_CELL: 210 | cell_value = last.contract.cells[last.contract.ptr] 211 | next.contract.cells[last.contract.ptr] = (int(cell_value) + 1) % 256 # we want over/underflow here 212 | next.pc += 1 213 | return next 214 | elif op == OpCode.DECR_CELL: 215 | cell_value = last.contract.cells[last.contract.ptr] 216 | next.contract.cells[last.contract.ptr] = (int(cell_value) + 256 - 1) % 256 # we want over/underflow here 217 | next.pc += 1 218 | return next 219 | elif op == OpCode.GET_CELL: 220 | cell_value = last.contract.cells[last.contract.ptr] 221 | if cell_value == 0 or cell_value == 1: 222 | next.result_code = cell_value 223 | return next 224 | else: 225 | # ignore the value, continue 226 | next.pc += 1 227 | return next 228 | elif op == OpCode.PUT_CELL: 229 | if last.input_read < len(last.input_data): 230 | new_cell_value = last.input_data[last.input_read] 231 | else: 232 | new_cell_value = 0 233 | next.contract.cells[last.contract.ptr] = new_cell_value 234 | next.input_read += 1 235 | next.pc += 1 236 | return next 237 | elif op == OpCode.JUMP_COND: 238 | cell_value = last.contract.cells[last.contract.ptr] 239 | if cell_value == 0: 240 | # we want to skip to matching ], we do this by tracking indentation 241 | next.pc += 1 242 | next.indent += 1 243 | return next 244 | else: 245 | if len(last.stack) == MAX_STACK_DEPTH: 246 | next.result_code = ExitCodes.StackOverflow 247 | return next 248 | next.stack.append(pc) 249 | next.pc += 1 250 | return next 251 | elif op == OpCode.JUMP_BACK: 252 | if len(last.stack) == 0: 253 | next.result_code = ExitCodes.StackUnderflow 254 | return next 255 | back_pc = last.stack[len(last.stack) - 1] 256 | next.stack.pop() 257 | next.pc = back_pc 258 | return next 259 | else: 260 | raise Exception(f"opcode parsing broken, unrecognized op: {op}") 261 | -------------------------------------------------------------------------------- /obf/_cli.py: -------------------------------------------------------------------------------- 1 | import click 2 | from typing import TextIO 3 | from .node_shim import ShimNode 4 | from .brainfuck import Step, next_step, parse_tx, Address, Contract, ExitCodes, Code 5 | from remerkleable.tree import PairNode, RootNode, Node 6 | import json 7 | import sys 8 | 9 | 10 | def encode_hex(v: bytes) -> str: 11 | return '0x' + v.hex() 12 | 13 | 14 | def decode_hex(v: str) -> bytes: 15 | if v.startswith('0x'): 16 | v = v[2:] 17 | return bytes.fromhex(v) 18 | 19 | 20 | @click.group() 21 | def cli(): 22 | """Optimistic Brainfuck - experiment to run brainfuck on an optimistic rollup on ethereum 23 | Contribute here: https://github.com/protolambda/optimistic-brainfuck 24 | """ 25 | 26 | 27 | # TODO: when to shut down the tracer, just in case of unexpected loop? 28 | SANITY_LIMIT = 10000 29 | 30 | # Enables us to save the contract with brainfuck characters, instead of more compressed form 31 | def contract_parse_code(obj): 32 | obj['code'] = Code.from_pretty_str(obj['code']).to_obj() 33 | return obj 34 | 35 | def contract_pretty_code(obj): 36 | obj['code'] = Code.from_obj(obj['code']).to_pretty_str() 37 | return obj 38 | 39 | 40 | @cli.command() 41 | @click.argument('state', type=click.File('wt')) 42 | def init_state(state: TextIO): 43 | """Initialize STATE 44 | 45 | STATE path to world state, will be JSON 46 | """ 47 | state.write(json.dumps({ 48 | "contracts": { 49 | "0": { 50 | "code": ",,,,,,,,,,,,,,,,,,,,,[>+++++++<-]", # skips the 20 byte address, and then multiplies the first input byte with 7, and stores result in second cell 51 | "ptr": 0, 52 | "cells": [0] 53 | } 54 | }, # 256 contract slots, starting with none 55 | }, indent=' ')) 56 | 57 | 58 | @cli.command() 59 | @click.argument('input', type=click.File('rt')) 60 | @click.argument('output', type=click.File('wt')) 61 | @click.argument('sender', type=click.STRING) 62 | @click.argument('contract', type=click.INT) 63 | @click.argument('tx', type=click.STRING) 64 | def transition(input: TextIO, output: TextIO, sender: str, contract: int, tx: str): 65 | """Transition full transaction TX, read INPUT state and write OUTPUT state 66 | 67 | INPUT file/input to current brainfuck world state, encoded in JSON 68 | 69 | OUTPUT where the updated state is written to 70 | 71 | SENDER sender address, 20 bytes, hex encoded and 0x prefix 72 | 73 | CONTRACT the ID (one byte) to call 74 | 75 | TX brainfuck tx payload 76 | """ 77 | click.echo("decoding transaction: "+tx) 78 | tx_bytes = decode_hex(tx) 79 | 80 | state_parsed = json.loads(input.read()) 81 | 82 | contract_inst = Contract.from_obj(contract_parse_code(state_parsed['contracts'][str(contract)])) 83 | 84 | click.echo("loading first step...") 85 | step = parse_tx(contract_inst, Address(decode_hex(sender)), tx_bytes) 86 | click.echo("selected brainfuck contract %d" % contract) 87 | 88 | click.echo("updating state by just generating and applying all fraud proof steps...") 89 | n = 0 90 | while True: 91 | click.echo("\rProcessing step %d" % n, nl=False) 92 | n += 1 93 | if n >= SANITY_LIMIT: 94 | raise Exception("Oh no! So many steps! What happened?") 95 | 96 | step = next_step(step) 97 | if step.result_code != 0xff: # have we finished yet? 98 | break 99 | print() # new line after \r loop 100 | 101 | if step.result_code == 0: 102 | click.echo("success transaction") 103 | # success, write back new contract state 104 | state_parsed['contracts'][str(contract)] = contract_pretty_code(step.contract.to_obj()) 105 | output.write(json.dumps(state_parsed, indent=' ')) 106 | else: 107 | click.echo(f"failed transaction, no state changes, exit code: {str(ExitCodes(step.result_code))}") 108 | 109 | 110 | @cli.command() 111 | @click.argument('state', type=click.File('rt')) 112 | @click.argument('output', type=click.File('wt')) 113 | @click.argument('sender', type=click.STRING) 114 | @click.argument('contract', type=click.INT) 115 | @click.argument('tx', type=click.STRING) 116 | def gen(state: TextIO, output: TextIO, sender: str, contract: int, tx: str): 117 | """Generate a fraud proof for the given transaction TX, applied on top of STATE 118 | 119 | STATE file/input to current brainfuck world state, encoded in JSON. 120 | 121 | OUTPUT the file/output to write the proof to. 122 | 123 | SENDER sender address, 20 bytes, hex encoded and 0x prefix 124 | 125 | CONTRACT the ID (one byte) to call 126 | 127 | TX brainfuck tx payload 128 | """ 129 | 130 | click.echo("decoding transaction: "+tx) 131 | if tx.startswith("0x"): 132 | tx = tx[2:] 133 | tx_bytes = bytes.fromhex(tx) 134 | 135 | state_parsed = json.loads(state.read()) 136 | 137 | contract_inst = Contract.from_obj(contract_parse_code(state_parsed['contracts'][str(contract)])) 138 | 139 | click.echo("loading first step...") 140 | init_step = parse_tx(contract_inst, Address(decode_hex(sender)), tx_bytes) 141 | 142 | click.echo("selected brainfuck contract %d" % contract) 143 | 144 | steps = [Step(backing=ShimNode.shim(init_step.get_backing()))] 145 | access_trace = [] 146 | 147 | def reset_shims(): 148 | for step in steps: 149 | backing: ShimNode = step.get_backing() 150 | backing.reset_shim() 151 | 152 | def capture_access(last: Step): 153 | shim: ShimNode = last.get_backing() 154 | access_list = list(shim.get_touched_gindices(g=1)) 155 | access_trace.append(access_list) 156 | 157 | click.echo("running step by step proof generator...") 158 | n = 0 159 | while True: 160 | click.echo("\rProcessing step %d" % n, nl=False) 161 | n += 1 162 | 163 | if n >= SANITY_LIMIT: 164 | raise Exception("Oh no! So many steps! What happened?") 165 | 166 | reset_shims() 167 | last = steps[-1] 168 | next = next_step(last) 169 | capture_access(last) 170 | steps.append(Step(backing=ShimNode.shim(next.get_backing()))) 171 | if next.result_code != 0xff: # have we finished yet? 172 | break 173 | print() # new line after \r loop 174 | 175 | binary_nodes = dict() 176 | 177 | def store_tree(b: PairNode): 178 | left, right = b.get_left(), b.get_right() 179 | # The merkle-roots are cached, this is fine 180 | binary_nodes[encode_hex(b.merkle_root())] = [encode_hex(left.merkle_root()), encode_hex(right.merkle_root())] 181 | if not left.is_leaf(): 182 | store_tree(left) 183 | if not right.is_leaf(): 184 | store_tree(right) 185 | 186 | # store all data relevant to all steps 187 | for step in steps: 188 | store_tree(step.get_backing()) 189 | 190 | output.write(json.dumps({ 191 | 'nodes': binary_nodes, 192 | 'step_roots': [encode_hex(step.hash_tree_root()) for step in steps], 193 | 'access': [ 194 | # not that this array is 1 shorter, the last step (post output) has no access data 195 | [encode_hex(gi.to_bytes(length=32, byteorder='big')) for gi in acc_li] for acc_li in access_trace 196 | ], 197 | }, indent=' ')) 198 | 199 | click.echo("done!") 200 | 201 | 202 | @cli.command() 203 | @click.argument('input', type=click.File('rt')) 204 | @click.argument('output', type=click.File('wt')) 205 | @click.argument('step', type=click.INT) 206 | def step_witness(input: TextIO, output: TextIO, step: int): 207 | """Compute the witness data for a single step by index, using the full trace witness. 208 | 209 | INPUT File/input to read fraud proof from. 210 | 211 | OUTPUT File/output to write witness of selected step to. 212 | 213 | STEP index of step to generate witness data for. 214 | """ 215 | obj = json.loads(input.read()) 216 | 217 | nodes = obj['nodes'] 218 | 219 | def retrieve_node_by_gindex(i: int, root: str) -> str: 220 | if i == 1: 221 | return root 222 | 223 | if root not in nodes: 224 | raise Exception("this should be 1") 225 | 226 | pivot = 1 << (i.bit_length() - 2) 227 | go_right = i & pivot != 0 228 | # mask out the top bit, and set the new top bit 229 | child = (i | pivot) - (pivot << 1) 230 | left, right = nodes[root] 231 | if go_right: 232 | return retrieve_node_by_gindex(child, right) 233 | else: 234 | return retrieve_node_by_gindex(child, left) 235 | 236 | pre_root = obj['step_roots'][step] 237 | post_root = obj['step_roots'][step+1] 238 | 239 | contents = {g: retrieve_node_by_gindex(int.from_bytes(decode_hex(g), byteorder='big'), pre_root) 240 | for g in obj['access'][step]} 241 | 242 | output.write(json.dumps({ 243 | 'node_by_gindex': contents, 244 | 'pre_root': pre_root, 245 | 'post_root': post_root, 246 | 'step': step, 247 | }, indent=' ')) 248 | 249 | 250 | @cli.command() 251 | @click.argument('input', type=click.File('rt')) 252 | @click.argument('claimed_post_root', type=click.STRING) 253 | def verify(input: TextIO, claimed_post_root: str): 254 | """Verify the execution of a step 255 | \f 256 | by providing the witness data and computing the step output 257 | 258 | CLAIMED_POST_ROOT hex encoded, 0x prefixed, root of contract state 259 | that is expected after progressing one step further 260 | """ 261 | obj = json.loads(input.read()) 262 | 263 | click.echo('parsing fraud proof') 264 | 265 | # parse all gindices and node contents 266 | node_by_gindex = { 267 | int.from_bytes(decode_hex(g), byteorder='big'): decode_hex(node) for g, node in obj['node_by_gindex'].items() 268 | } 269 | 270 | # Take all those witness nodes by their position, and construct a tree that we can use as backing. 271 | # Any other node 272 | def construct_backing(g: int) -> Node: 273 | if g > 2**60: 274 | raise Exception("didn't expect backing branches this deep! witness data must be missing") 275 | if g not in node_by_gindex: 276 | left = construct_backing(g*2) 277 | right = construct_backing(g*2+1) 278 | return PairNode(left, right) 279 | else: 280 | return RootNode(node_by_gindex[g]) 281 | 282 | # start at the root, find all sub-node 283 | partial_backing = construct_backing(1) 284 | 285 | click.echo('verifying fraud proof') 286 | 287 | pre = Step.view_from_backing(partial_backing) 288 | post = next_step(pre) 289 | 290 | expected_root = decode_hex(claimed_post_root) 291 | if post.result_code == 0 or post.result_code == 0xff: 292 | # success, state may have changed, check it 293 | got_root = post.hash_tree_root() 294 | click.echo("transaction was effective, post contract root: "+encode_hex(got_root)) 295 | else: 296 | # no success, check that we were expecting the pre-state root as output (i.e. no changes) 297 | got_root = post.hash_tree_root() 298 | click.echo(f"transaction reverted ({str(ExitCodes(post.result_code))}), expecting pre-state contract root," 299 | " to indicate no change was made: "+encode_hex(got_root)) 300 | if got_root != expected_root: 301 | click.echo("root did not match, fraud detected!") 302 | sys.exit(1) 303 | else: 304 | click.echo("root matches, no fraud") 305 | sys.exit(0) 306 | 307 | 308 | if __name__ == '__main__': 309 | cli() 310 | --------------------------------------------------------------------------------