├── .babelrc ├── .circleci └── config.yml ├── .codecov.yml ├── .develatus-apparatus.js ├── .eslintrc ├── .gitattributes ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE.md ├── .gitignore ├── .solcover.js ├── .solhint.json ├── .solhintignore ├── LICENSE ├── README.md ├── challenger ├── .gitignore ├── ExecutionPoker.js ├── LICENSE ├── README.md ├── bin.js ├── cliArgs.js ├── enforcerMock.json ├── index.js ├── package.json └── yarn.lock ├── contracts ├── EVMCode.slb ├── EVMConstants.sol ├── EVMMemory.slb ├── EVMRuntime.sol ├── EVMStack.slb ├── EVMTokenBag.slb ├── EVMUtils.slb ├── Enforcer.sol ├── EthereumRuntime.sol ├── HydratedRuntime.sol ├── MemOps.slb ├── Merkelizer.slb ├── PlasmaVerifier.sol ├── Verifier.sol ├── interfaces │ ├── IEnforcer.sol │ └── IVerifier.sol └── mocks │ ├── EVMCodeMock.sol │ ├── EVMStackMock.sol │ ├── EnforcerProxyMock.sol │ ├── IERC165.sol │ ├── IERC20.sol │ ├── IERC721.sol │ ├── SpendingConditionMock.sol │ ├── TestMemOps.sol │ └── VerifierMock.sol ├── docs ├── Architecture.md ├── Merkelizer.md └── SystemDescriptionDocument.md ├── package.json ├── scripts ├── compile.js ├── genesis.json ├── geth.js ├── start_geth.sh ├── test.sh └── test_geth.sh ├── test ├── .eslintrc ├── contracts │ ├── PlasmaVerifier.js │ ├── dispute.js │ ├── enforcer.js │ ├── ethereumRuntime.js │ ├── evmCode.test.js │ ├── evmStack.test.js │ ├── onChain.test.js │ ├── testMemOps.js │ └── verifier.js ├── fixtures │ ├── dispute.js │ ├── onChain.js │ ├── runtime.js │ └── runtimeGasUsed.js ├── helpers │ ├── assertInvalid.js │ ├── assertRevert.js │ ├── tokenBag.js │ └── utils.js └── utils │ ├── FragmentTree.js │ ├── PrimeTester.js │ └── stepper.js ├── tools ├── deploy.js ├── executionPoker.js └── verifyDeployment.js ├── truffle-config.js ├── utils ├── AbstractMerkleTree.js ├── EVMRuntime.js ├── EthereumRuntimeAdapter.js ├── ExecutionPoker.js ├── FragmentTree.js ├── HydratedRuntime.js ├── Merkelizer.js ├── Opcodes.js ├── ProofHelper.js ├── RangeProofHelper.js ├── constants.js ├── index.js └── precompiled │ ├── 01-ecrecover.js │ ├── 02-sha256.js │ ├── 03-ripemd160.js │ ├── 04-identity.js │ ├── 05-modexp.js │ ├── 06-ecadd.js │ ├── 07-ecmul.js │ └── 08-ecpairing.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "targets": { 5 | "node": "10.14.2", 6 | } 7 | }], 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | defaults: &defaults 4 | working_directory: ~/solEVM-enforcer 5 | docker: 6 | - image: circleci/node:lts 7 | 8 | restore_node_modules: &restore_node_modules 9 | restore_cache: 10 | name: 'restore_cache' 11 | keys: 12 | - v1-node-{{ .Branch }}-{{ checksum "package.json" }} 13 | - v1-node-{{ checksum "package.json" }} 14 | 15 | jobs: 16 | setup: 17 | <<: *defaults 18 | steps: 19 | - checkout 20 | - *restore_node_modules 21 | - run: 22 | name: 'setup workspace' 23 | command: | 24 | yarn 25 | yarn compile:contracts 26 | wget 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.8.22-7fa3509e.tar.gz' -O - | tar -xz; mv geth-*/geth . 27 | - save_cache: 28 | name: 'save_cache' 29 | key: v1-node-{{ .Branch }}-{{ checksum "package.json" }} 30 | paths: 31 | - node_modules/ 32 | - persist_to_workspace: 33 | root: ~/ 34 | paths: 35 | - solEVM-enforcer 36 | 37 | lint: 38 | <<: *defaults 39 | steps: 40 | - attach_workspace: 41 | at: ~/ 42 | - *restore_node_modules 43 | - run: 44 | name: 'lint:all' 45 | command: yarn lint:all 46 | 47 | contracts: 48 | <<: *defaults 49 | steps: 50 | - attach_workspace: 51 | at: ~/ 52 | - *restore_node_modules 53 | - run: 54 | name: 'test:contracts' 55 | command: yarn test:contracts 56 | 57 | utils: 58 | <<: *defaults 59 | steps: 60 | - attach_workspace: 61 | at: ~/ 62 | - *restore_node_modules 63 | - run: 64 | name: 'test:utils' 65 | command: yarn test:utils 66 | 67 | coverage: 68 | <<: *defaults 69 | steps: 70 | - attach_workspace: 71 | at: ~/ 72 | - *restore_node_modules 73 | - run: 74 | name: 'Solidity Coverage' 75 | command: | 76 | sudo yarn global add codecov 77 | PATH=`pwd`:$PATH yarn coverage 78 | codecov -f coverage-report.json 79 | 80 | contracts_geth: 81 | <<: *defaults 82 | steps: 83 | - attach_workspace: 84 | at: ~/ 85 | - *restore_node_modules 86 | - run: 87 | name: 'test:contracts with geth' 88 | command: PATH=`pwd`:$PATH ./scripts/test_geth.sh test/contracts/* 89 | 90 | execution_poker: 91 | <<: *defaults 92 | steps: 93 | - attach_workspace: 94 | at: ~/ 95 | - *restore_node_modules 96 | - run: 97 | name: 'test ExecutionPoker cli tool' 98 | command: yarn compile:contracts && ./tools/executionPoker.js build/contracts/SpendingConditionMock.json test 0xD8992E08C1Fb17775deF02e15B620A006c4726db [] [] 99 | 100 | workflows: 101 | version: 2 102 | setup-lint-test: 103 | jobs: 104 | - setup 105 | - lint: 106 | requires: 107 | - setup 108 | - utils: 109 | requires: 110 | - setup 111 | - coverage: 112 | requires: 113 | - setup 114 | - execution_poker: 115 | requires: 116 | - setup 117 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "contracts/mocks" 3 | - "contracts/interfaces" 4 | -------------------------------------------------------------------------------- /.develatus-apparatus.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testCommand: 'yarn mocha --timeout 120000 test/contracts/*.js', 3 | artifactsPath: 'build/contracts', 4 | proxyPort: 8333, 5 | rpcUrl: 'http://localhost:8222', 6 | }; 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 2018 4 | }, 5 | "env": { 6 | "es6": true, 7 | "browser" : true, 8 | "node" : true, 9 | "mocha" : true, 10 | "jest" : true 11 | }, 12 | "globals" : { 13 | "artifacts": false, 14 | "contract": false, 15 | "assert": false, 16 | "web3": false 17 | }, 18 | "rules": { 19 | // Strict mode 20 | "strict": ["warn", "global"], 21 | 22 | // Code style 23 | "indent": [2, 2], 24 | "quotes": [2, "single"], 25 | "semi": ["error", "always"], 26 | "space-before-function-paren": ["error", "always"], 27 | "no-use-before-define": 0, 28 | "eqeqeq": [2, "smart"], 29 | "dot-notation": [2, {"allowKeywords": true, "allowPattern": ""}], 30 | "no-redeclare": [2, {"builtinGlobals": true}], 31 | "no-trailing-spaces": [2, { "skipBlankLines": true }], 32 | "eol-last": 0, 33 | "comma-spacing": [2, {"before": false, "after": true}], 34 | "camelcase": [2, {"properties": "always"}], 35 | "no-mixed-spaces-and-tabs": [2, "smart-tabs"], 36 | "comma-dangle": [1, "always-multiline"], 37 | "no-dupe-args": 2, 38 | "no-dupe-keys": 2, 39 | "no-debugger": 0, 40 | "no-undef": 2, 41 | "object-curly-spacing": [2, "always"], 42 | "max-len": [2, 120, 2], 43 | "generator-star-spacing": ["error", "before"], 44 | "promise/avoid-new": 0, 45 | "promise/always-return": 0 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.slb linguist-language=Solidity 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # github: Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 2 | # patreon: Replace with a single Patreon username 3 | # open_collective: Replace with a single Open Collective username 4 | # ko_fi: Replace with a single Ko-fi username 5 | # tidelift: Replace with a single Tidelift platform-name/package-name e.g., npm/babel 6 | custom: https://gitcoin.co/grants/66/leapdao-building-a-home-for-dapps-on-layer-2 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 31 | # Bounty 32 | 33 | ## Scope 34 | - 35 | - 36 | - 37 | 38 | ## Deliverables 39 | 40 | ## Gain for the project 41 | 42 | ## Roles 43 | bounty gardener: name / share 44 | bounty worker: name / share 45 | bounty reviewer: name / share 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | allFiredEvents 13 | scTopics 14 | 15 | # node-waf configuration 16 | .lock-wscript 17 | 18 | # Dependency directory 19 | node_modules 20 | 21 | # Debug log from npm 22 | npm-debug.log 23 | package-lock.json 24 | 25 | # local env variables 26 | .env 27 | 28 | # truffle build directory 29 | build/ 30 | 31 | # lol macs 32 | .DS_Store/ 33 | 34 | .vscode/ -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | compileCommand: '../scripts/compile.js', 3 | testCommand: 'COVERAGE=1 ../node_modules/.bin/mocha --timeout 120000 test/contracts/*', 4 | norpc: true, 5 | deepSkip: true, 6 | skipFiles: ['mocks/', 'PlasmaVerifier.sol'], 7 | }; 8 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:default", 3 | "rules": { 4 | "no-complex-fallback": "warn", 5 | "indent": [ 6 | "error", 7 | 4 8 | ], 9 | "quotes": [ 10 | "error", 11 | "double" 12 | ], 13 | "max-line-length": [ 14 | "error", 15 | 140 16 | ], 17 | "no-inline-assembly": false, 18 | "compiler-fixed": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.solhintignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EVM Enforcer [![Build Status](https://circleci.com/gh/leapdao/solEVM-enforcer.svg?style=svg)](https://circleci.com/gh/leapdao/solEVM-enforcer) 2 | 3 | ![EVM Enforcer](https://i.imgur.com/V9EGql2.png) 4 | 5 | The **EVM Enforcer** is a computation verification engine that allows on-chain enforcement of off-chain EVM execution. 6 | 7 | ![Process Diagram](https://i.imgur.com/o1FRMqp.png) 8 | 9 | Anyone who solved some off-chain execution can register a result with the *Enforcer contract*. A challenge period starts during which challengers can open disputes over the presented results. Disputes are delegated to the *Verifier contract* where solver and challenger play an interactive execution verification game. If the challenger wins, the execution results are deleted and the solver looses a bond. If the solver is able to defend the execution results throughout the challenge period, the result is accepted. 10 | 11 | 12 | ## FAQ 13 | 14 | - **What is the relationship to solEVM?** 15 | The EVM enforcer is built on [Andreas Olofsson](https://github.com/androlo)'s [solEVM](https://github.com/Ohalo-Ltd/solevm). Thank you Andreas :clap: ! 16 | - **How is this different than Truebit?** 17 | [Truebit](http://truebit.io)'s interactive computation verification game uses a [WebAssembly](https://webassembly.org/) VM. We allow to run EVM bytecode. 18 | - **What do you use this for?** 19 | We use this to enforce the correctness of transaction in [Plasma Leap](https://ethresear.ch/t/plasma-leap-a-state-enabled-computing-model-for-plasma/3539). 20 | 21 | 22 | ## Contributions 23 | 24 | You are very welcome to contribute by: 25 | - Picking `beginner-friendly` issues from the backlog. 26 | - Joining the [Discord channel](https://discord.gg/7bfD6eB) to ask questions. 27 | - Participating in the [bi-weekly hangouts](https://hackmd.io/Kn0hwBA7Tvm6mfacCH1rIw?both). 28 | 29 | Please make sure to read the contribution guidelines. 30 | 31 | 32 | ## Setup 33 | 34 | You can run the tests like this: 35 | 36 | ``` 37 | yarn 38 | yarn test 39 | ``` 40 | 41 | ### Deployment 42 | 43 | `tools/deploy.js` - is a little deployment helper script. 44 | 45 | Additionally to the deployment variables you also have to provide the network (the network name in truffle-config). 46 | If the RPC provider does not support signing, you have to supply either `privKey|PRIV_KEY` or `mnemonic|MNEMONIC` 47 | as environment variables to this script. 48 | 49 | You can run it like this: 50 | ``` 51 | network=geth verifierTimeout=1800 taskPeriod=43200 challengePeriod=21600 bondAmount=10000000000000000 maxExecutionDepth=10 ./tools/deploy.js 52 | ``` 53 | 54 | ## Runtime - EVMRuntime.sol 55 | 56 | The runtime contract is `EVMRuntime.sol`. The contract is designed with extensibility in mind. 57 | The most basic contract which makes use of it is [EthereumRuntime.sol](https://github.com/leapdao/solEVM-enforcer/blob/master/contracts/EVMRuntime.sol), 58 | the contract has an `execute` function which is used to run code. 59 | 60 | Other contracts which makes use of the `EVMRuntime` are: 61 | - [Verifier.sol](https://github.com/leapdao/solEVM-enforcer/blob/master/contracts/Verifier.sol) 62 | 63 | 64 | ## Off-chain Interpreter & Merkelizer | Based on ethereumjs-vm :clap: 65 | 66 | It also exists a corresponding runtime implementation on the JS side. 67 | [You can take a look at the on-chain Verifier unit test on how it's used.](https://github.com/leapdao/solEVM-enforcer/blob/master/test/verifier.js) 68 | 69 | [The off-chain Interpreter mimics the on-chain Stepper](https://github.com/leapdao/solEVM-enforcer/blob/master/utils/EVMRuntime.js) 70 | and together with the [Merkelizer](https://github.com/leapdao/solEVM-enforcer/blob/master/utils/Merkelizer.js), 71 | creates a Merkle Root of the individual execution steps (before and after each opcode) given `code`, `data` and other runtime properties. 72 | 73 | For the curious, you can checkout the on-chain [Verifier](https://github.com/leapdao/solEVM-enforcer/blob/master/contracts/Verifier.sol). 74 | 75 | 76 | #### Work In Progress 77 | 78 | The design decision that the `EVMRuntime.sol` will be the base 'class' for contracts needing a runtime environment is final, 79 | though the whole interface design is not final yet. 80 | 81 | It is planned that the `struct EVM` will hold a pointer to memory where users can point to their custom data / structure, 82 | this property gives the most flexibility for developers working with the `EVMRuntime` without hitting contract size-, maximal stack depth or other limitations. 83 | 84 | 85 | ### Blockchain 86 | 87 | There are no blocks, so `BLOCKHASH` will always return `0`. The only blockchain related parameters that can be set are those in the context object. 88 | -------------------------------------------------------------------------------- /challenger/.gitignore: -------------------------------------------------------------------------------- 1 | .cache -------------------------------------------------------------------------------- /challenger/README.md: -------------------------------------------------------------------------------- 1 | # solEVM-enforcer service 2 | 3 | To solve and challenge 4 | 5 | ## Use 6 | 7 | `solevm-enforcer [ARGS]` 8 | 9 | ### Arguments 10 | 11 | - `--enforcerAddr` — enforcer contract address 12 | - `--walletPriv` — private key 13 | - `--ethProvider` — ethereum JSON-rpc endpoint (e.g. infura) 14 | -------------------------------------------------------------------------------- /challenger/bin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | require('./build/index.js'); 5 | -------------------------------------------------------------------------------- /challenger/cliArgs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const dashdash = require('dashdash'); 4 | 5 | const options = [ 6 | { 7 | names: ['help', 'h'], 8 | type: 'bool', 9 | help: 'Print this help', 10 | }, 11 | { 12 | names: ['version'], 13 | type: 'bool', 14 | help: 'Print version', 15 | }, 16 | { 17 | names: ['enforcerAddr'], 18 | type: 'string', 19 | env: 'ENFORCER_ADDR', 20 | help: 'Enforcer contract address', 21 | }, 22 | { 23 | names: ['walletPriv'], 24 | type: 'string', 25 | env: 'WALLET_PRIV', 26 | help: 'Private key for signing transactions', 27 | }, 28 | { 29 | names: ['ethProvider'], 30 | type: 'string', 31 | env: 'ETH_PROVIDER', 32 | help: 'Ethereum JSON RPC url', 33 | }, 34 | { 35 | names: ['delay'], 36 | type: 'number', 37 | env: 'DELAY', 38 | help: 'Events handling delay (for temp setup)', 39 | default: 0, 40 | }, 41 | { 42 | names: ['stupid'], 43 | type: 'bool', 44 | env: 'STUPID', 45 | help: 'Enables wrong results producing', 46 | default: false, 47 | }, 48 | ]; 49 | 50 | const parser = dashdash.createParser({ options }); 51 | 52 | /** 53 | * @type {{ 54 | * ethProvider: string; 55 | * walletPriv: string; 56 | * enforcerAddr: string; 57 | * delay: number; 58 | * stupid: boolean; 59 | * }} 60 | */ 61 | const cliArgs = parser.parse(process.argv); 62 | 63 | if (cliArgs.help) { 64 | console.log('Usage:'); 65 | console.log(parser.help({ includeEnv: true }).trimRight()); 66 | process.exit(0); 67 | } 68 | 69 | if (cliArgs.version) { 70 | console.log(`v${require('./package.json').version}`); // eslint-disable-line 71 | process.exit(0); 72 | } 73 | 74 | if (!cliArgs.enforcerAddr) { 75 | console.log('enforcerAddr is required. See --help for reference'); 76 | process.exit(0); 77 | } 78 | 79 | // ToDo: do not use plain private key here 80 | // it will be saved in terminal history 81 | // Much better would be to use a file with private key 82 | if (!cliArgs.walletPriv) { 83 | console.log('walletPriv is required. See --help for reference'); 84 | process.exit(0); 85 | } 86 | 87 | if (!cliArgs.ethProvider) { 88 | console.log('ethProvider is required. See --help for reference'); 89 | process.exit(0); 90 | } 91 | 92 | module.exports = cliArgs; -------------------------------------------------------------------------------- /challenger/enforcerMock.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { 6 | "indexed": false, 7 | "name": "taskHash", 8 | "type": "bytes32" 9 | }, 10 | { 11 | "components": [ 12 | { 13 | "name": "origin", 14 | "type": "address" 15 | }, 16 | { 17 | "name": "target", 18 | "type": "address" 19 | }, 20 | { 21 | "name": "blockHash", 22 | "type": "bytes32" 23 | }, 24 | { 25 | "name": "blockNumber", 26 | "type": "uint256" 27 | }, 28 | { 29 | "name": "time", 30 | "type": "uint256" 31 | }, 32 | { 33 | "name": "txGasLimit", 34 | "type": "uint256" 35 | }, 36 | { 37 | "name": "customEnvironmentHash", 38 | "type": "bytes32" 39 | }, 40 | { 41 | "name": "codeHash", 42 | "type": "bytes32" 43 | }, 44 | { 45 | "name": "dataHash", 46 | "type": "bytes32" 47 | } 48 | ], 49 | "indexed": false, 50 | "name": "parameters", 51 | "type": "tuple" 52 | }, 53 | { 54 | "indexed": false, 55 | "name": "callData", 56 | "type": "bytes" 57 | } 58 | ], 59 | "name": "Requested", 60 | "type": "event" 61 | }, 62 | { 63 | "anonymous": false, 64 | "inputs": [ 65 | { 66 | "indexed": true, 67 | "name": "taskHash", 68 | "type": "bytes32" 69 | }, 70 | { 71 | "indexed": true, 72 | "name": "solverPathRoot", 73 | "type": "bytes32" 74 | }, 75 | { 76 | "indexed": false, 77 | "name": "executionDepth", 78 | "type": "uint256" 79 | }, 80 | { 81 | "indexed": false, 82 | "name": "result", 83 | "type": "bytes" 84 | } 85 | ], 86 | "name": "Registered", 87 | "type": "event" 88 | }, 89 | { 90 | "constant": false, 91 | "inputs": [ 92 | { 93 | "name": "_taskHash", 94 | "type": "bytes32" 95 | }, 96 | { 97 | "name": "_pathRoot", 98 | "type": "bytes32" 99 | }, 100 | { 101 | "name": "result", 102 | "type": "bytes" 103 | } 104 | ], 105 | "name": "registerResult", 106 | "outputs": [], 107 | "payable": false, 108 | "stateMutability": "nonpayable", 109 | "type": "function" 110 | }, 111 | { 112 | "constant": false, 113 | "inputs": [ 114 | { 115 | "name": "_taskHash", 116 | "type": "bytes32" 117 | } 118 | ], 119 | "name": "finalizeTask", 120 | "outputs": [], 121 | "payable": false, 122 | "stateMutability": "nonpayable", 123 | "type": "function" 124 | }, 125 | { 126 | "constant": false, 127 | "inputs": [ 128 | { 129 | "components": [ 130 | { 131 | "name": "origin", 132 | "type": "address" 133 | }, 134 | { 135 | "name": "target", 136 | "type": "address" 137 | }, 138 | { 139 | "name": "blockHash", 140 | "type": "bytes32" 141 | }, 142 | { 143 | "name": "blockNumber", 144 | "type": "uint256" 145 | }, 146 | { 147 | "name": "time", 148 | "type": "uint256" 149 | }, 150 | { 151 | "name": "txGasLimit", 152 | "type": "uint256" 153 | }, 154 | { 155 | "name": "customEnvironmentHash", 156 | "type": "bytes32" 157 | }, 158 | { 159 | "name": "codeHash", 160 | "type": "bytes32" 161 | }, 162 | { 163 | "name": "dataHash", 164 | "type": "bytes32" 165 | } 166 | ], 167 | "name": "_params", 168 | "type": "tuple" 169 | }, 170 | { 171 | "name": "_data", 172 | "type": "bytes" 173 | } 174 | ], 175 | "name": "request", 176 | "outputs": [ 177 | { 178 | "name": "", 179 | "type": "bytes32" 180 | } 181 | ], 182 | "payable": false, 183 | "stateMutability": "nonpayable", 184 | "type": "function" 185 | }, 186 | { 187 | "constant": true, 188 | "inputs": [ 189 | { 190 | "name": "_taskHash", 191 | "type": "bytes32" 192 | } 193 | ], 194 | "name": "getStatus", 195 | "outputs": [ 196 | { 197 | "name": "", 198 | "type": "uint256" 199 | }, 200 | { 201 | "name": "", 202 | "type": "bytes32[]" 203 | }, 204 | { 205 | "name": "", 206 | "type": "bytes32[]" 207 | } 208 | ], 209 | "payable": false, 210 | "stateMutability": "view", 211 | "type": "function" 212 | } 213 | ] 214 | -------------------------------------------------------------------------------- /challenger/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ethers = require('ethers'); 4 | const BigNumber = require('bignumber.js'); 5 | const { ExecutionPoker, executionId } = require('./ExecutionPoker'); 6 | 7 | const cliArgs = require('./cliArgs'); 8 | 9 | let Enforcer; 10 | let Verifier; 11 | try { 12 | Enforcer = require('../build/contracts/Enforcer.json'); 13 | Verifier = require('../build/contracts/Verifier.json'); 14 | } catch (e) { 15 | console.error('Please run `npm run compile:contracts` first. 😉'); 16 | process.exit(1); 17 | } 18 | 19 | const fromWei = (wei) => { 20 | const dec = new BigNumber(10).pow(18); 21 | return new BigNumber(wei).div(dec).toString(); 22 | }; 23 | 24 | (async () => { 25 | const provider = new ethers.providers.JsonRpcProvider(cliArgs.ethProvider); 26 | const wallet = new ethers.Wallet(cliArgs.walletPriv, provider); 27 | const enforcer = new ethers.Contract(cliArgs.enforcerAddr, Enforcer.abi, provider); 28 | const verifierAddr = await enforcer.verifier(); 29 | const balance = await wallet.getBalance(); 30 | console.log(`Wallet: ${wallet.address} (${fromWei(balance)} ETH)`); 31 | console.log(`Enforcer: ${cliArgs.enforcerAddr}`); 32 | console.log(`Verfier: ${verifierAddr}`); 33 | const verifier = new ethers.Contract(verifierAddr, Verifier.abi, provider); 34 | 35 | new ExecutionPoker(db, enforcer, verifier, wallet, 3000000, 'challenger'); // eslint-disable-line 36 | })(); 37 | 38 | function onException (e) { 39 | console.error(e); 40 | process.exit(1); 41 | } 42 | 43 | process.on('uncaughtException', onException); 44 | process.on('unhandledRejection', onException); 45 | -------------------------------------------------------------------------------- /challenger/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solevm-enforcer", 3 | "version": "1.0.3", 4 | "main": "build/index.js", 5 | "license": "MPL-2.0", 6 | "dependencies": { 7 | "bignumber.js": "^9.0.0", 8 | "dashdash": "^1.14.1", 9 | "ethereumjs-util": "^6.1.0", 10 | "ethereumjs-vm": "https://github.com/pinkiebell/ethereumjs-vm.git#release/v2.6.0-r6", 11 | "ethers": "^4.0.32" 12 | }, 13 | "devDependencies": { 14 | "@babel/core": "^7.0.0-0", 15 | "@babel/plugin-syntax-async-generators": "^7.2.0", 16 | "@babel/preset-env": "^7.5.4", 17 | "parcel-bundler": "^1.12.3" 18 | }, 19 | "scripts": { 20 | "build": "pushd .. && yarn compile:contracts && popd && parcel build --target=node -d build index.js" 21 | }, 22 | "bin": { 23 | "solevm-enforcer": "./bin.js" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /contracts/EVMCode.slb: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.2; 2 | pragma experimental ABIEncoderV2; 3 | 4 | 5 | library EVMCode { 6 | 7 | struct Code { 8 | address codeAddress; 9 | uint length; 10 | DataNode root; 11 | } 12 | 13 | struct DataNode { 14 | uint pos; 15 | uint value; 16 | uint next; 17 | } 18 | 19 | function nextDataNode(DataNode memory node) internal pure returns (DataNode memory next) { 20 | if (node.next == 0) { 21 | return DataNode(uint(-1), 0, 0); 22 | } 23 | 24 | assembly { 25 | next := mload(add(node, 0x40)) 26 | } 27 | } 28 | 29 | function fromArray(bytes32[] memory codes, uint length) internal pure returns (Code memory code) { 30 | require(codes.length >= 2, "Empty code array"); 31 | 32 | code.length = length; 33 | code.root = DataNode(uint(codes[0]), uint(codes[1]), 0); 34 | 35 | DataNode memory current = code.root; 36 | for (uint i = 2; i < codes.length; i += 2) { 37 | uint pos = uint(codes[i]); 38 | uint val = uint(codes[i + 1]); 39 | require(pos > uint(codes[i - 2]), "wrong code order"); 40 | require(pos <= (length >> 5), "incorrect code length"); 41 | 42 | DataNode memory next = DataNode(pos, val, 0); 43 | assembly { 44 | mstore(add(current, 0x40), next) 45 | } 46 | current = next; 47 | } 48 | } 49 | 50 | function fromAddress(address codeAddress) internal view returns (Code memory code) { 51 | code.codeAddress = codeAddress; 52 | uint codeSize; 53 | assembly { 54 | codeSize := extcodesize(codeAddress) 55 | } 56 | 57 | code.length = codeSize; 58 | } 59 | 60 | function findFragment(Code memory self, uint wordPos) internal view returns (DataNode memory res) { 61 | DataNode memory current = self.root; 62 | 63 | while (current.pos != uint(-1)) { 64 | if (current.pos == wordPos) { 65 | return current; 66 | } 67 | current = nextDataNode(current); 68 | } 69 | 70 | return current; 71 | } 72 | 73 | /** 74 | * @dev return the opcode at position if known. Otherwise return 0 75 | * 76 | */ 77 | function getOpcodeAt(Code memory self, uint pos) internal view returns (uint8 opcode) { 78 | address codeContractAddress = self.codeAddress; 79 | 80 | if (pos >= self.length) { 81 | return 0; 82 | } 83 | 84 | if (codeContractAddress == address(0)) { 85 | uint wordPos = pos >> 5; 86 | DataNode memory fragment = findFragment(self, wordPos); 87 | 88 | require(fragment.pos != uint(-1), "Opcode missing"); 89 | // 0x00000000 11111111 22222222 ... (256 bit) 90 | // to get a byte at position x, we need to shift all these byte at position > x + 8 91 | // x = 0 -> shift 248 = (32 - 1) * 8 92 | // x = 1 -> shift 242 = (32 - 2) * 8 93 | // x = 31 -> shift 0 = (32 - 32) * 8 94 | opcode = uint8(fragment.value >> ((31 - (pos % 32)) * 8)); 95 | } else { 96 | assembly { 97 | extcodecopy(codeContractAddress, 31, pos, 1) 98 | opcode := mload(0) 99 | } 100 | } 101 | } 102 | 103 | /** 104 | * @dev return code as bytes array, from a position for a number of bytes 105 | * revert if the known code cannot fulfill the requirement 106 | */ 107 | // solhint-disable-next-line function-max-lines 108 | function toBytes(Code memory self, uint pos, uint numBytes) internal view returns (bytes memory bts) { 109 | address codeContractAddress = self.codeAddress; 110 | assembly { 111 | bts := mload(0x40) 112 | // padding up to word size 113 | mstore(0x40, add(bts, and(add(add(numBytes, 0x20), 0x1f), not(0x1f)))) 114 | mstore(bts, numBytes) 115 | } 116 | 117 | if (codeContractAddress == address(0)) { 118 | uint wordPos = pos >> 5; 119 | DataNode memory fragment = findFragment(self, wordPos); 120 | 121 | if (pos < self.length) { 122 | require(fragment.pos != uint(-1), "Code not found"); 123 | } 124 | 125 | uint copiedLen = 0; 126 | uint prevPos; 127 | // copy first fragment, which do not fit in whole word 128 | if (pos % 32 != 0) { 129 | assembly { 130 | // store value at fragmentPos to bts, shift left to correct for pos 131 | mstore(add(bts, 0x20), shl(mul(mod(pos, 0x20), 0x08), mload(add(fragment, 0x20)))) 132 | } 133 | prevPos = fragment.pos; 134 | fragment = nextDataNode(fragment); 135 | copiedLen += 32 - pos % 32; 136 | } 137 | 138 | // copy the rest 139 | while (copiedLen < numBytes) { 140 | if (pos + copiedLen >= self.length) { 141 | // we are done, everything else is zero by default 142 | break; 143 | } 144 | 145 | require(fragment.pos == 0 || 146 | fragment.pos == prevPos + 1, "Known code not enough"); 147 | 148 | assembly { 149 | mstore(add(bts, add(copiedLen, 0x20)), mload(add(fragment, 0x20))) 150 | } 151 | 152 | prevPos = fragment.pos; 153 | fragment = nextDataNode(fragment); 154 | copiedLen += 32; 155 | } 156 | } else { 157 | assembly { 158 | extcodecopy(codeContractAddress, add(bts, 0x20), pos, numBytes) 159 | } 160 | } 161 | } 162 | 163 | function toUint(Code memory self, uint pos, uint numBytes) internal view returns (uint data) { 164 | // if pos + numBytes > self.length, we get zeroes. 165 | // this is the behaviour we want 166 | assert(32 >= numBytes && numBytes > 0); 167 | 168 | if (pos >= self.length) { 169 | return 0; 170 | } 171 | 172 | address codeContractAddress = self.codeAddress; 173 | 174 | if (codeContractAddress == address(0)) { 175 | // 2 cases: 176 | // - return data fit in a fragment 177 | // - return data span 2 fragments 178 | uint wordPos = pos >> 5; 179 | DataNode memory fragment = findFragment(self, wordPos); 180 | 181 | require(fragment.pos != uint(-1), "Code not found"); 182 | 183 | // only need to retrieve 32 bytes 184 | if (fragment.pos == ((pos + numBytes - 1) >> 5)) { 185 | // retrieve the word which contains the required data 186 | // shift left to strip unnecessary data on the left 187 | uint temp = fragment.value << ((pos % 32) * 8); 188 | // then shift right to strip unnecessary data on the right 189 | return temp >> ((32 - numBytes) * 8); 190 | } 191 | 192 | // require fetching an additional 32 bytes 193 | DataNode memory fragmentNext = nextDataNode(fragment); 194 | require(fragmentNext.pos == fragment.pos + 1, "Code not enough"); 195 | // the left part should be the rightmost part of the first word 196 | // to retrieve: shift left to strip, then shift back to correct position in numBytes 197 | uint left = (fragment.value << ((pos % 32) * 8)) >> ((32 - numBytes) * 8); 198 | // the right part should be the leftmost part of the second word 199 | // to retrieve: shift all the way to the right 200 | // 64 - numBytes - (pos % 32) = 32 - (numBytes - (32 - (pos % 32))) = word_length - (required_length - (left_path_length)) 201 | // numBytes + (pos % 32) >= 32, if not, then it requires only 1 byte 202 | uint right = (fragmentNext.value >> (64 - numBytes - (pos % 32)) * 8); 203 | data = left | right; 204 | } else { 205 | assembly { 206 | extcodecopy(codeContractAddress, 0, pos, numBytes) 207 | data := mload(0) 208 | } 209 | data = data >> 8 * (32 - numBytes); 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /contracts/EVMStack.slb: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.2; 2 | pragma experimental ABIEncoderV2; 3 | 4 | import {MemOps} from "./MemOps.slb"; 5 | 6 | 7 | library EVMStack { 8 | 9 | // TODO: make that programmable? 10 | // possible could look like: 11 | // EVMRuntime:defaultStackSize() < could be overwritten 12 | uint constant internal INITIAL_CAP = 64; 13 | uint constant internal MAX_SIZE = 1024; 14 | 15 | struct Stack { 16 | uint size; 17 | uint cap; 18 | uint dataPtr; 19 | } 20 | 21 | // Re-sizes and re-allocates the stack. 22 | function expandCapacity(Stack memory self, uint capIncrease) internal pure { 23 | assert(capIncrease != 0); 24 | assert(self.cap + capIncrease <= MAX_SIZE); 25 | self.cap += capIncrease; 26 | uint src = self.dataPtr; 27 | uint dest = MemOps.allocate32(self.cap); 28 | MemOps.memcopy32(src, dest, self.size); 29 | self.dataPtr = dest; 30 | } 31 | 32 | function newStack() internal pure returns (Stack memory stk) { 33 | stk.size = 0; 34 | stk.cap = INITIAL_CAP; 35 | stk.dataPtr = MemOps.allocate32(INITIAL_CAP); 36 | } 37 | 38 | function fromArray(bytes32[] memory sArr) internal pure returns (Stack memory stk) { 39 | stk.size = sArr.length; 40 | stk.cap = sArr.length + INITIAL_CAP; 41 | stk.dataPtr = MemOps.allocate32(sArr.length + INITIAL_CAP); 42 | for (uint i = 0; i < sArr.length; i++) { 43 | bytes32 val = sArr[i]; 44 | uint slot = stk.dataPtr + 32*i; 45 | assembly { 46 | mstore(slot, val) 47 | } 48 | } 49 | } 50 | 51 | function toArray(Stack memory self) internal pure returns (bytes32[] memory arr) { 52 | if (self.size == 0) { 53 | return arr; 54 | } 55 | arr = new bytes32[](self.size); 56 | uint dest; 57 | assembly { 58 | dest := add(arr, 0x20) 59 | } 60 | MemOps.memcopy32(self.dataPtr, dest, self.size); 61 | } 62 | 63 | function push(Stack memory self, uint val) internal pure { 64 | assert(self.size < MAX_SIZE); 65 | if (self.size == self.cap) { 66 | expandCapacity(self, 32); 67 | } 68 | uint slot = self.dataPtr + 32*self.size++; 69 | assembly { 70 | mstore(slot, val) 71 | } 72 | } 73 | 74 | function pop(Stack memory self) internal pure returns (uint) { 75 | assert(self.size > 0); 76 | uint data; 77 | uint slot = self.dataPtr + --self.size*32; 78 | assembly { 79 | data := mload(slot) 80 | mstore(slot, 0) // remove? 81 | } 82 | return data; 83 | } 84 | 85 | function dup(Stack memory self, uint n) internal pure { 86 | assert(self.size < MAX_SIZE); 87 | assert(1 <= n && n <= 16); 88 | assert(n <= self.size); 89 | uint data; 90 | uint slot1 = self.dataPtr + (self.size - n)*32; 91 | assembly { 92 | data := mload(slot1) 93 | } 94 | if (self.size == self.cap) { 95 | expandCapacity(self, 32); 96 | } 97 | uint slot2 = self.dataPtr + 32*self.size++; 98 | assembly { 99 | mstore(slot2, data) 100 | } 101 | } 102 | 103 | function swap(Stack memory self, uint n) internal pure { 104 | assert(1 <= n && n <= 16); 105 | assert(n < self.size); 106 | uint slot1 = self.dataPtr + (self.size - 1)*32; 107 | uint slot2 = self.dataPtr + (self.size - n - 1)*32; 108 | assembly { 109 | let data1 := mload(slot1) 110 | mstore(slot1, mload(slot2)) 111 | mstore(slot2, data1) 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /contracts/EVMTokenBag.slb: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.2; 2 | pragma experimental ABIEncoderV2; 3 | 4 | 5 | library EVMTokenBag { 6 | 7 | uint8 constant internal ERC20TYPE = 0; 8 | uint8 constant internal ERC721TYPE = 1; 9 | uint8 constant internal ERC1948TYPE = 2; 10 | 11 | struct Output { 12 | address owner; 13 | uint valueOrId; 14 | bytes32 data; 15 | address color; 16 | uint8 tokenType; 17 | } 18 | 19 | struct TokenBag { 20 | Output[16] bag; 21 | } 22 | 23 | function balanceOf( 24 | TokenBag memory self, 25 | address color, 26 | address owner 27 | ) internal pure returns (uint value) { 28 | Output memory query; 29 | query.owner = owner; 30 | query.color = color; 31 | (query,) = findToken2(self, query, 9); 32 | value = query.valueOrId; 33 | } 34 | 35 | function readData( 36 | TokenBag memory self, 37 | address color, 38 | uint id 39 | ) internal pure returns (bytes32 data) { 40 | Output memory query; 41 | query.valueOrId = id; 42 | query.color = color; 43 | (query,) = findToken2(self, query, 10); 44 | data = query.data; 45 | } 46 | 47 | function ownerOf( 48 | TokenBag memory self, 49 | address color, 50 | uint id 51 | ) internal pure returns (address owner) { 52 | Output memory query; 53 | query.valueOrId = id; 54 | query.color = color; 55 | (query,) = findToken2(self, query, 10); 56 | owner = query.owner; 57 | } 58 | 59 | function writeData( 60 | TokenBag memory self, 61 | address color, 62 | uint256 tokenId, 63 | bytes32 newData 64 | ) internal pure { 65 | Output memory query; 66 | bool success; 67 | query.valueOrId = tokenId; 68 | query.color = color; 69 | (query, success) = findToken2(self, query, 10); 70 | if (success) { 71 | query.data = newData; 72 | } 73 | } 74 | 75 | function transferFrom( 76 | TokenBag memory self, 77 | address color, 78 | address from, 79 | address to, 80 | uint valueOrId 81 | ) internal pure returns (bool) { 82 | if (to == address(0)) return false; 83 | (Output memory source, bool success) = findToken(self, color, from, valueOrId); 84 | // sender's token/tokens don't exist 85 | if (!success) return false; 86 | if (source.tokenType == ERC20TYPE) { 87 | Output memory dest; 88 | (dest, success) = findToken(self, color, to, valueOrId); 89 | if (!success) { 90 | (dest, success) = findToken(self, address(0), address(0), valueOrId); 91 | } 92 | // no slot found for output token 93 | if (!success) return false; 94 | // sender does not have enough tokens to send and/or underflow 95 | if (source.valueOrId < valueOrId) return false; 96 | // overflow 97 | if (valueOrId + dest.valueOrId < valueOrId) return false; 98 | 99 | dest.owner = to; 100 | dest.color = color; 101 | dest.valueOrId += valueOrId; 102 | source.valueOrId -= valueOrId; 103 | 104 | return true; 105 | } else if (source.tokenType == ERC721TYPE || source.tokenType == ERC1948TYPE) { 106 | source.owner = to; 107 | return true; 108 | } else { 109 | return false; 110 | } 111 | } 112 | 113 | function findToken( 114 | TokenBag memory self, 115 | address color, 116 | address owner, 117 | uint valueOrId 118 | ) internal pure returns (Output memory, bool) { 119 | Output memory token; 120 | bool success; 121 | bytes1 selector = 0x1f; 122 | for (uint i = 0; i < self.bag.length; i++) { 123 | if (self.bag[i].owner == owner && self.bag[i].color == color) { 124 | if (self.bag[i].tokenType == ERC20TYPE) { 125 | token = self.bag[i]; 126 | success = true; 127 | break; 128 | } else { 129 | if (self.bag[i].valueOrId == valueOrId) { 130 | token = self.bag[i]; 131 | success = true; 132 | break; 133 | } 134 | } 135 | } 136 | } 137 | return (token, success); 138 | } 139 | 140 | /* This is a generalized query function on the token bag. */ 141 | /* Eventually replace findToken with this? This is a larger and more expensive */ 142 | /* function, but I think clearer. */ 143 | /* selector: take the fields in Output that are relevant to the query and put 1 in that place. */ 144 | /* example: I care about owner (position 0) and color (position 3), so 01001 = 9 */ 145 | function findToken2( 146 | TokenBag memory self, 147 | Output memory query, 148 | uint8 selector 149 | ) internal pure returns (Output memory, bool) { 150 | Output memory token; 151 | bool success; 152 | Output memory current; 153 | for (uint i = 0; i < self.bag.length; i++) { 154 | current = self.bag[i]; 155 | bool isMatch = 156 | (!(0x1 & selector != 0) || ((0x1 & selector != 0) && current.owner == query.owner)) && 157 | (!(0x2 & selector != 0) || ((0x2 & selector != 0) && current.valueOrId == query.valueOrId)) && 158 | (!(0x4 & selector != 0) || ((0x4 & selector != 0) && current.data == query.data)) && 159 | (!(0x8 & selector != 0) || ((0x8 & selector != 0) && current.color == query.color)) && 160 | (!(0x10 & selector != 0) || ((0x10 & selector != 0) && current.tokenType == query.tokenType)); 161 | if (isMatch) { 162 | token = current; 163 | success = true; 164 | break; 165 | } 166 | } 167 | return (token, success); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /contracts/EVMUtils.slb: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.2; 2 | pragma experimental ABIEncoderV2; 3 | 4 | 5 | library EVMUtils { 6 | 7 | function toUint(bytes memory bts, uint addr, uint numBytes) internal pure returns (uint data) { 8 | assert(32 >= numBytes && numBytes > 0 && addr + numBytes <= bts.length); 9 | assembly { 10 | data := mload(add(add(bts, 0x20), addr)) 11 | } 12 | data = data >> 8 * (32 - numBytes); 13 | } 14 | 15 | function fromUint(uint x) internal pure returns (bytes memory bts) { 16 | bts = new bytes(32); 17 | assembly { 18 | mstore(add(bts, 0x20), x) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /contracts/EthereumRuntime.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.2; 2 | pragma experimental ABIEncoderV2; 3 | 4 | 5 | import { EVMCode } from "./EVMCode.slb"; 6 | import { EVMStack } from "./EVMStack.slb"; 7 | import { EVMMemory } from "./EVMMemory.slb"; 8 | import { EVMTokenBag } from "./EVMTokenBag.slb"; 9 | import { HydratedRuntime } from "./HydratedRuntime.sol"; 10 | 11 | 12 | contract EthereumRuntime is HydratedRuntime { 13 | 14 | struct EVMPreimage { 15 | address code; 16 | bytes data; 17 | uint pc; 18 | uint8 errno; 19 | uint gasRemaining; 20 | uint stepCount; 21 | bytes32[] stack; 22 | bytes32[] mem; 23 | bytes returnData; 24 | EVMTokenBag.TokenBag tokenBag; 25 | } 26 | 27 | struct EVMResult { 28 | uint gas; 29 | bytes data; 30 | bytes returnData; 31 | uint8 errno; 32 | bytes32[] mem; 33 | bytes32[] stack; 34 | uint pc; 35 | bytes32 hashValue; 36 | EVMTokenBag.TokenBag tokenBag; 37 | } 38 | 39 | // Init EVM with given stack and memory and execute from the given opcode 40 | // solhint-disable-next-line function-max-lines 41 | function execute(EVMPreimage memory img) public returns (EVMResult memory) { 42 | // solhint-disable-next-line avoid-low-level-calls 43 | EVM memory evm; 44 | 45 | initHydratedState(evm); 46 | 47 | evm.data = img.data; 48 | evm.gas = img.gasRemaining; 49 | 50 | evm.caller = DEFAULT_CALLER; 51 | evm.target = DEFAULT_CONTRACT_ADDRESS; 52 | evm.returnData = img.returnData; 53 | evm.errno = img.errno; 54 | 55 | evm.code = EVMCode.fromAddress(img.code); 56 | evm.stack = EVMStack.fromArray(img.stack); 57 | evm.mem = EVMMemory.fromArray(img.mem); 58 | 59 | evm.tokenBag = img.tokenBag; 60 | 61 | _run(evm, img.pc, img.stepCount); 62 | 63 | bytes32 hashValue = stateHash(evm); 64 | EVMResult memory resultState; 65 | 66 | resultState.gas = evm.gas; 67 | resultState.data = evm.data; 68 | resultState.returnData = evm.returnData; 69 | resultState.errno = evm.errno; 70 | resultState.mem = EVMMemory.toArray(evm.mem); 71 | resultState.stack = EVMStack.toArray(evm.stack); 72 | resultState.pc = evm.pc; 73 | resultState.hashValue = hashValue; 74 | resultState.tokenBag = evm.tokenBag; 75 | 76 | return resultState; 77 | } 78 | 79 | function stateHash(EVM memory evm) internal view returns (bytes32) { 80 | bytes32 dataHash = keccak256(abi.encodePacked( 81 | evm.gas, 82 | evm.code.toBytes(0, evm.code.length), 83 | evm.data, 84 | evm.returnData, 85 | evm.errno 86 | )); 87 | 88 | HydratedState memory hydratedState = getHydratedState(evm); 89 | 90 | bytes32 hashValue = keccak256(abi.encodePacked( 91 | dataHash, 92 | hydratedState.stackHash, 93 | hydratedState.memHash, 94 | evm.mem.size, 95 | evm.stack.size, 96 | evm.pc, 97 | evm.caller, 98 | evm.target, 99 | evm.blockNumber, 100 | evm.blockTime, 101 | evm.blockHash 102 | )); 103 | 104 | return hashValue; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /contracts/HydratedRuntime.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.2; 2 | pragma experimental ABIEncoderV2; 3 | 4 | import "./EVMRuntime.sol"; 5 | 6 | 7 | contract HydratedRuntime is EVMRuntime { 8 | 9 | struct HydratedState { 10 | bytes32 stackHash; 11 | bytes32 memHash; 12 | } 13 | 14 | function initHydratedState(EVM memory evm) internal pure returns (HydratedState memory hydratedState) { 15 | uint ptr; 16 | 17 | assembly { 18 | ptr := hydratedState 19 | } 20 | 21 | evm.customDataPtr = ptr; 22 | } 23 | 24 | function getHydratedState(EVM memory evm) internal pure returns (HydratedState memory) { 25 | HydratedState memory res; 26 | uint ptr = evm.customDataPtr; 27 | 28 | assembly { 29 | res := ptr 30 | } 31 | 32 | return res; 33 | } 34 | 35 | function updateHydratedState(EVM memory evm) internal pure { 36 | // TODO: 37 | // gather all proofs here 38 | // How we compute the proofs below is not final yet. 39 | // Update memory proofs setup to use slot, value, once off-chain support lands. 40 | HydratedState memory hydratedState = getHydratedState(evm); 41 | 42 | bytes32 hash = hydratedState.stackHash; 43 | uint ptr = evm.stack.dataPtr; 44 | 45 | for (uint i = 0; i < evm.stack.size; i++) { 46 | assembly { 47 | mstore(0, hash) 48 | mstore(0x20, mload(add(ptr, mul(i, 0x20)))) 49 | hash := keccak256(0, 0x40) 50 | } 51 | } 52 | hydratedState.stackHash = hash; 53 | 54 | bytes32[] memory mem = evm.mem.toArray(); 55 | if (mem.length > 0) { 56 | hydratedState.memHash = keccak256(abi.encodePacked(mem)); 57 | } 58 | } 59 | 60 | function _run(EVM memory evm, uint pc, uint pcStepCount) internal { 61 | super._run(evm, pc, pcStepCount); 62 | updateHydratedState(evm); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /contracts/MemOps.slb: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.2; 2 | pragma experimental ABIEncoderV2; 3 | 4 | 5 | /* 6 | Operations to do with EVM memory. This library uses 0x40 as free memory pointer just as LLL and 7 | Solidity, but only for storage; it always reads the value from MSIZE. Allocation will allocate new 8 | memory from free memory by expanding it and then updating the free memory pointer with the new address. 9 | */ 10 | library MemOps { 11 | 12 | uint constant internal WORD_SIZE = 32; 13 | 14 | /* 15 | Allocates 'words' words of memory. 16 | */ 17 | function allocate32(uint words) internal pure returns (uint addr) { 18 | uint numBytes = words * WORD_SIZE; 19 | assembly { 20 | // free memory address 0x40 - constants doesn't work in inline assembly. 21 | addr := mload(0x40) 22 | // addr is a free memory pointer 23 | mstore(0x40, add(addr, numBytes)) 24 | } 25 | } 26 | 27 | // Copies 'words*32' bytes from 'srcPtr' to 'destPtr'. 28 | // NOTE: This function does not check if memory is allocated, it only copies the bytes. 29 | function memcopy32(uint srcPtr, uint destPtr, uint words) internal pure { 30 | // Copy word-length chunks. 31 | for (uint i = 0; i < words; i++) { 32 | uint mp = i * WORD_SIZE; 33 | assembly { 34 | mstore(add(destPtr, mp), mload(add(srcPtr, mp))) 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /contracts/Merkelizer.slb: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.2; 2 | pragma experimental ABIEncoderV2; 3 | 4 | 5 | library Merkelizer { 6 | uint constant internal DEFAULT_GAS = 0x0fffffffffffff; 7 | 8 | struct ExecutionState { 9 | bytes data; 10 | bytes32[] stack; 11 | bytes32[] mem; 12 | bytes32 customEnvironmentHash; 13 | bytes returnData; 14 | uint pc; 15 | uint gasRemaining; 16 | uint stackSize; 17 | uint memSize; 18 | } 19 | 20 | function memHash(bytes32[] memory _mem) internal pure returns (bytes32) { 21 | return keccak256(abi.encodePacked(_mem)); 22 | } 23 | 24 | function dataHash(bytes memory _data) internal pure returns (bytes32) { 25 | return keccak256(abi.encodePacked(_data)); 26 | } 27 | 28 | function stackHash(ExecutionState memory self, bytes32 _sibling) internal pure returns (bytes32) { 29 | bytes32 hash = _sibling; 30 | bytes32[] memory stack = self.stack; 31 | 32 | for (uint i = 0; i < stack.length; i++) { 33 | assembly { 34 | mstore(0, hash) 35 | mstore(0x20, mload(add(add(stack, 0x20), mul(i, 0x20)))) 36 | hash := keccak256(0, 0x40) 37 | } 38 | } 39 | 40 | return hash; 41 | } 42 | 43 | function stateHash( 44 | ExecutionState memory self, 45 | bytes32 _stackHash, 46 | bytes32 _memHash, 47 | bytes32 _dataHash 48 | ) internal pure returns (bytes32) { 49 | if (_memHash == 0) { 50 | _memHash = memHash(self.mem); 51 | } 52 | 53 | if (_dataHash == 0) { 54 | _dataHash = dataHash(self.data); 55 | } 56 | 57 | bytes32 preHash = keccak256( 58 | abi.encodePacked( 59 | _stackHash, 60 | _memHash, 61 | _dataHash, 62 | self.customEnvironmentHash, 63 | self.pc, 64 | self.gasRemaining, 65 | self.stackSize, 66 | self.memSize 67 | ) 68 | ); 69 | 70 | // Question: before we *eventually* implement `FragmentTree` for `returnData`, 71 | // should we also hash the bytelength from `returnData`. 72 | // This is probably not needed because the array would be too large anyway to verify on-chain 73 | // for a possible hash-collision 74 | return keccak256(abi.encodePacked(preHash, self.returnData)); 75 | } 76 | 77 | function initialStateHash(bytes32 dataHash, bytes32 customEnvironmentHash) internal pure returns (bytes32) { 78 | ExecutionState memory e; 79 | e.gasRemaining = DEFAULT_GAS; 80 | e.customEnvironmentHash = customEnvironmentHash; 81 | 82 | return stateHash(e, 0, 0, dataHash); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /contracts/PlasmaVerifier.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.2; 2 | pragma experimental ABIEncoderV2; 3 | 4 | import "./Verifier.sol"; 5 | 6 | 7 | contract PlasmaVerifier is Verifier { 8 | bytes4 constant internal FUNC_SIG_BALANCE_OF = hex'70a08231'; 9 | bytes4 constant internal FUNC_SIG_TRANSFER = hex'a9059cbb'; 10 | 11 | constructor(uint256 timeout) public Verifier(timeout) { 12 | } 13 | 14 | event ResultEvent(uint errno, uint gas, bytes returnData); 15 | event Transfer(address indexed from, address indexed to, uint256 value); 16 | event BalanceEvent(address tokenAddr, address owner); 17 | 18 | event InfoEvent( 19 | address caller, 20 | address currentTarget, 21 | address target, 22 | bytes4 functionSig, 23 | address inputcaller, 24 | address spendingAddr 25 | ); 26 | 27 | struct Input { 28 | address caller; 29 | address spendingCondition; 30 | bytes callData; 31 | } 32 | 33 | function testRun(Input memory input) public { 34 | EVM memory evm; 35 | HydratedState memory hydratedState = initHydratedState(evm); 36 | evm.data = input.callData; 37 | evm.gas = 0xffffffff; 38 | evm.code = EVMCode.fromAddress(input.spendingCondition); 39 | evm.stack = EVMStack.newStack(); 40 | evm.mem = EVMMemory.newMemory(); 41 | 42 | _run(evm, 0, 0); 43 | 44 | emit ResultEvent(evm.errno, evm.gas, evm.returnData); 45 | } 46 | 47 | function handleCALL(EVM memory state) internal { 48 | uint gas = state.stack.pop(); 49 | address target = address(state.stack.pop()); 50 | uint value = state.stack.pop(); 51 | uint inOffset = state.stack.pop(); 52 | uint inSize = state.stack.pop(); 53 | uint retOffset = state.stack.pop(); 54 | uint retSize = state.stack.pop(); 55 | 56 | bytes memory returnData = handleCall(state, target, inOffset, inSize); 57 | state.returnData = returnData; 58 | 59 | if (returnData.length != 0) { 60 | state.mem.storeBytesAndPadWithZeroes(returnData, 0, retOffset, retSize); 61 | } 62 | } 63 | 64 | function handleSTATICCALL(EVM memory state) internal { 65 | uint gas = state.stack.pop(); 66 | address target = address(state.stack.pop()); 67 | uint inOffset = state.stack.pop(); 68 | uint inSize = state.stack.pop(); 69 | uint retOffset = state.stack.pop(); 70 | uint retSize = state.stack.pop(); 71 | 72 | bytes memory returnData = handleCall(state, target, inOffset, inSize); 73 | state.returnData = returnData; 74 | 75 | if (returnData.length != 0) { 76 | state.mem.storeBytesAndPadWithZeroes(returnData, 0, retOffset, retSize); 77 | } 78 | } 79 | 80 | // solhint-disable-next-line function-max-lines 81 | function handleCall(EVM memory state, address target, uint inOffset, uint inSize) internal returns (bytes memory) { 82 | bytes4 functionSig = bytes4(bytes32(state.mem.load(inOffset))); 83 | 84 | Input memory input; 85 | assembly { 86 | let ptr := mload(0x40) 87 | mstore(0x40, add(ptr, 64)) 88 | calldatacopy(ptr, add(4, 0x20), 64) 89 | input := ptr 90 | } 91 | 92 | emit InfoEvent( 93 | state.caller, 94 | state.target, 95 | target, 96 | functionSig, 97 | input.caller, 98 | input.spendingCondition 99 | ); 100 | 101 | // TODO: do real checks 102 | 103 | if (functionSig == FUNC_SIG_BALANCE_OF) { 104 | // do this shit only this way 105 | address balanceOf = address(uint160(state.mem.load(inOffset + 4))); 106 | 107 | emit BalanceEvent(target, balanceOf); 108 | state.stack.push(1); 109 | 110 | return abi.encode(["uint256"], [0xffff]); 111 | } 112 | 113 | if (functionSig == FUNC_SIG_TRANSFER) { 114 | address to = address(uint160(state.mem.load(inOffset + 4))); 115 | uint value = state.mem.load(inOffset + 24); 116 | // TODO: 117 | // if the function is `transfer()`, then the `from` would be the signer of the inputs (recovered from r, v, s) 118 | // requirements: 119 | // - token address must be in input 120 | // - must have enough value 121 | // - must check color 122 | // - if ok : insert output : else invalid execution? 123 | 124 | state.stack.push(1); 125 | return abi.encode(["bool"], [true]); 126 | } 127 | 128 | // TODO: ERC721, ERC1948, allowance stuff 129 | 130 | // invalid - call fails 131 | state.stack.push(0); 132 | return ""; 133 | } 134 | 135 | /* 136 | * This is used to by solc to check if a address is indeed a contract (has code). 137 | */ 138 | function handleEXTCODESIZE(EVM memory state) internal { 139 | // TODO: check the address 140 | state.stack.pop(); 141 | 142 | // return non-zero length to signal ok 143 | state.stack.push(1); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /contracts/interfaces/IEnforcer.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.2; 2 | pragma experimental ABIEncoderV2; 3 | 4 | import "./IVerifier.sol"; 5 | 6 | 7 | contract IEnforcer { 8 | /// @notice Structure for the V-Game EVM environment. 9 | struct EVMParameters { 10 | // Transaction sender 11 | address origin; 12 | // address of the current contract / execution context 13 | address target; 14 | // blockHash 15 | bytes32 blockHash; 16 | // blockNumber 17 | uint256 blockNumber; 18 | // timestamp of the current block in seconds since the epoch 19 | uint256 time; 20 | // tx gas limit 21 | uint256 txGasLimit; 22 | // customEnvironmentHash - for custom implementations like Plasma Exit 23 | bytes32 customEnvironmentHash; 24 | // codeHash / dataHash should be the root hash of the given merkle tree 25 | // Except that codeHash could also be the contract address (addr + right-padded with zeros to 32 bytes) 26 | bytes32 codeHash; 27 | bytes32 dataHash; 28 | } 29 | 30 | struct Task { 31 | uint256 startTime; 32 | bytes32[] executions; 33 | } 34 | 35 | struct ExecutionResult { 36 | uint256 startTime; 37 | bytes32 taskHash; 38 | bytes32 solverPathRoot; 39 | bytes32 resultHash; 40 | uint256 executionDepth; 41 | address solver; 42 | } 43 | 44 | uint256 public taskPeriod; 45 | uint256 public challengePeriod; 46 | uint256 public maxExecutionDepth; 47 | uint256 public bondAmount; 48 | IVerifier public verifier; 49 | 50 | mapping(address => uint256) public bonds; 51 | mapping(bytes32 => Task) public tasks; 52 | mapping(bytes32 => ExecutionResult) public executions; 53 | 54 | event Requested(bytes32 taskHash, EVMParameters parameters, bytes callData); 55 | 56 | event Registered( 57 | bytes32 indexed taskHash, 58 | bytes32 indexed solverPathRoot, 59 | uint256 executionDepth, 60 | bytes result 61 | ); 62 | 63 | event DisputeInitialised(bytes32 indexed disputeId, bytes32 indexed executionId); 64 | event Slashed(bytes32 indexed executionId, address indexed _address); 65 | 66 | function parameterHash(EVMParameters memory _parameters) public pure returns (bytes32) { 67 | return keccak256( 68 | abi.encodePacked( 69 | _parameters.target, 70 | _parameters.blockHash, 71 | _parameters.blockNumber, 72 | _parameters.time, 73 | _parameters.txGasLimit, 74 | _parameters.customEnvironmentHash, 75 | _parameters.codeHash, 76 | _parameters.dataHash 77 | ) 78 | ); 79 | } 80 | 81 | /// @notice request a new task 82 | /// @dev if `_parameters.dataHash` is zero and `callData.length` over zero 83 | /// then `_parameters.dataHash` will be recalculated 84 | /// @return bytes32 taskHash 85 | function request(EVMParameters memory _parameters, bytes memory callData) public returns (bytes32); 86 | 87 | /// @notice register 88 | function register( 89 | bytes32 _taskHash, 90 | bytes32 _solverPathRoot, 91 | bytes32[] memory _resultProof, 92 | bytes memory _result 93 | ) public payable; 94 | 95 | /// @notice dispute is called by challenger to start a new dispute 96 | /// assumed that challenger's execution tree is of the same depth as solver's. 97 | /// @dev In case challenger's tree is shallower, he should use node with zero hash to make it deeper. 98 | /// In case challenger's tree is deeper, he should submit only the left subtree with the same depth with solver's. 99 | function dispute( 100 | bytes32 _solverPathRoot, 101 | bytes32 _challengerPathRoot, 102 | EVMParameters memory _parameters 103 | ) public payable; 104 | 105 | /// @notice receive result from Verifier contract. 106 | /// only callable from `verifier` 107 | function result(bytes32 _executionId, bool solverWon, address challenger) public; 108 | 109 | /// @notice check execution results and `taskPeriod` 110 | /// @return endTime, pathRoots, resultHashes 111 | function getStatus(bytes32 _taskHash) public view returns (uint256, bytes32[] memory, bytes32[] memory); 112 | } 113 | -------------------------------------------------------------------------------- /contracts/interfaces/IVerifier.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.2; 2 | pragma experimental ABIEncoderV2; 3 | 4 | import "./IEnforcer.sol"; 5 | 6 | 7 | contract IVerifier { 8 | struct ComputationPath { 9 | bytes32 left; 10 | bytes32 right; 11 | } 12 | 13 | // 256x32 bytes as the memory limit 14 | uint constant internal MAX_MEM_WORD_COUNT = 256; 15 | 16 | uint8 constant internal SOLVER_RESPONDED = 1 << 0; 17 | uint8 constant internal CHALLENGER_RESPONDED = 1 << 1; 18 | uint8 constant internal SOLVER_VERIFIED = 1 << 2; 19 | uint8 constant internal CHALLENGER_VERIFIED = 1 << 3; 20 | uint8 constant internal START_OF_EXECUTION = 1 << 4; 21 | uint8 constant internal END_OF_EXECUTION = 1 << 5; 22 | uint8 constant internal INITIAL_STATE = START_OF_EXECUTION | END_OF_EXECUTION; 23 | 24 | struct Dispute { 25 | bytes32 executionId; 26 | bytes32 initialStateHash; 27 | bytes32 codeHash; 28 | address challengerAddr; 29 | 30 | bytes32 solverPath; 31 | bytes32 challengerPath; 32 | uint256 treeDepth; 33 | bytes32 witness; 34 | 35 | ComputationPath solver; 36 | ComputationPath challenger; 37 | 38 | uint8 state; 39 | 40 | uint256 timeout; // in seconds 41 | } 42 | 43 | event DisputeNewRound(bytes32 indexed disputeId, uint256 timeout, bytes32 solverPath, bytes32 challengerPath); 44 | 45 | uint256 public timeoutDuration; 46 | 47 | IEnforcer public enforcer; 48 | 49 | mapping (bytes32 => Dispute) public disputes; 50 | 51 | function initGame( 52 | bytes32 executionId, 53 | bytes32 solverHashRoot, 54 | bytes32 challengerHashRoot, 55 | uint256 executionDepth, 56 | // optional for implementors 57 | bytes32 customEnvironmentHash, 58 | // TODO: should be the bytes32 root hash later on 59 | bytes32 codeHash, 60 | bytes32 dataHash, 61 | address challenger 62 | ) public returns (bytes32 disputeId); 63 | } 64 | -------------------------------------------------------------------------------- /contracts/mocks/EVMCodeMock.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.2; 2 | pragma experimental ABIEncoderV2; 3 | 4 | import "../EVMCode.slb"; 5 | 6 | 7 | contract EVMCodeMock { 8 | using EVMCode for EVMCode.Code; 9 | 10 | function testFindFragment(bytes32[] memory rawCodes, uint codeLength, uint pos) 11 | public view returns (EVMCode.DataNode memory res) { 12 | EVMCode.Code memory code = EVMCode.fromArray(rawCodes, codeLength); 13 | res = code.findFragment(pos); 14 | } 15 | 16 | function testToUint(bytes32[] memory rawCodes, uint codeLength, uint pos, uint length) 17 | public view returns (uint res) { 18 | EVMCode.Code memory code = EVMCode.fromArray(rawCodes, codeLength); 19 | res = code.toUint(pos, length); 20 | } 21 | 22 | function testToBytes(bytes32[] memory rawCodes, uint codeLength, uint pos, uint length) 23 | public view returns (bytes memory res) { 24 | EVMCode.Code memory code = EVMCode.fromArray(rawCodes, codeLength); 25 | res = code.toBytes(pos, length); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /contracts/mocks/EVMStackMock.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.2; 2 | pragma experimental ABIEncoderV2; 3 | 4 | import "../EVMStack.slb"; 5 | import "../MemOps.slb"; 6 | 7 | 8 | contract EVMStackMock { 9 | using EVMStack for EVMStack.Stack; 10 | 11 | function dupThrowsNTooSmall() public { 12 | EVMStack.Stack memory stack = EVMStack.newStack(); 13 | stack.dup(0); 14 | } 15 | 16 | function dupThrowsNTooLarge() public { 17 | EVMStack.Stack memory stack = EVMStack.newStack(); 18 | stack.dup(17); 19 | } 20 | 21 | function dupThrowsUnderflow() public { 22 | EVMStack.Stack memory stack = EVMStack.newStack(); 23 | stack.dup(1); 24 | } 25 | 26 | function popThrowsUnderflow() public { 27 | EVMStack.Stack memory stack = EVMStack.newStack(); 28 | stack.pop(); 29 | } 30 | 31 | function pushThrowsOverflow() public { 32 | EVMStack.Stack memory stack = EVMStack.newStack(); 33 | stack.size = 1024; 34 | stack.push(1); 35 | } 36 | 37 | function dupThrowsOverflow() public { 38 | EVMStack.Stack memory stack = EVMStack.newStack(); 39 | stack.push(1); 40 | stack.size = 1024; 41 | stack.dup(1); 42 | } 43 | 44 | function testCreate() public { 45 | EVMStack.Stack memory stack = EVMStack.newStack(); 46 | uint fPtr; 47 | 48 | assembly { 49 | fPtr := mload(0x40) 50 | } 51 | 52 | require(stack.dataPtr == (fPtr - (stack.cap * 32)), "stack.dataPtr"); 53 | require(stack.size == 0, "stack.size"); 54 | require(stack.cap == 64, "stack.cap"); 55 | } 56 | 57 | function testPush() public { 58 | uint DATA = 1234; 59 | EVMStack.Stack memory stack = EVMStack.newStack(); 60 | stack.push(DATA); 61 | uint val; 62 | uint pos = stack.dataPtr; 63 | assembly { 64 | val := mload(pos) 65 | } 66 | require(val == DATA, "val == DATA"); 67 | require(stack.size == 1, "stack.size"); 68 | require(stack.cap == 64, "stack.cap"); 69 | } 70 | 71 | function testPop() public { 72 | EVMStack.Stack memory stack = EVMStack.newStack(); 73 | stack.push(1); 74 | uint data = stack.pop(); 75 | require(stack.size == 0, "stack.size"); 76 | require(data == 1, "data"); 77 | uint dataA; 78 | uint slot = stack.dataPtr; 79 | assembly { 80 | dataA := mload(slot) 81 | } 82 | require(dataA == 0, "dataA"); 83 | } 84 | 85 | function testDup1() public { 86 | EVMStack.Stack memory stack = EVMStack.newStack(); 87 | stack.push(1); 88 | stack.dup(1); 89 | require(stack.size == 2, "stack.size"); 90 | require(stack.pop() == 1, "pop 1"); 91 | require(stack.pop() == 1, "pop 2"); 92 | } 93 | 94 | function testDup16() public { 95 | EVMStack.Stack memory stack = EVMStack.newStack(); 96 | for (uint i = 0; i < 16; i++) { 97 | stack.push(i + 1); 98 | } 99 | stack.dup(16); 100 | require(stack.size == 17, "stack.size"); 101 | require(stack.pop() == 1, "stack.pop"); 102 | for (uint i = 0; i < 16; i++) { 103 | require(stack.pop() == (16 - i), "stack.pop loop"); 104 | } 105 | } 106 | 107 | function testSwap1() public { 108 | EVMStack.Stack memory stack = EVMStack.newStack(); 109 | for (uint i = 0; i < 17; i++) { 110 | stack.push(i + 1); 111 | } 112 | stack.swap(16); 113 | require(stack.size == 17, "stack.size"); 114 | require(stack.pop() == 1, "stack.pop"); 115 | 116 | for (uint i = 0; i < 15; i++) { 117 | stack.pop(); 118 | } 119 | require(stack.pop() == 17, "stack.pop 2"); 120 | } 121 | 122 | function testSwap16() public { 123 | EVMStack.Stack memory stack = EVMStack.newStack(); 124 | stack.push(1); 125 | stack.push(2); 126 | stack.swap(1); 127 | require(stack.size == 2, "stack.size 2"); 128 | require(stack.pop() == 1, "pop 1"); 129 | require(stack.pop() == 2, "pop 2"); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /contracts/mocks/EnforcerProxyMock.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.2; 2 | pragma experimental ABIEncoderV2; 3 | 4 | import "./../interfaces/IEnforcer.sol"; 5 | 6 | contract EnforcerProxyMock { 7 | IEnforcer enforcer; 8 | 9 | event StatusEvent(uint256, bytes32[], bytes32[]); 10 | 11 | constructor(address _enforcer) public { 12 | enforcer = IEnforcer(_enforcer); 13 | } 14 | 15 | function getStatus(bytes32 _taskHash) public { 16 | (uint256 a, bytes32[] memory b, bytes32[] memory c) = enforcer.getStatus(_taskHash); 17 | emit StatusEvent(a, b, c); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /contracts/mocks/IERC165.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | /** 4 | * @title IERC165 5 | * @dev https://github.com/ethereum/EIPs/blob/master/EIPS/eip-165.md 6 | */ 7 | interface IERC165 { 8 | /** 9 | * @notice Query if a contract implements an interface 10 | * @param interfaceId The interface identifier, as specified in ERC-165 11 | * @dev Interface identification is specified in ERC-165. This function 12 | * uses less than 30,000 gas. 13 | */ 14 | function supportsInterface(bytes4 interfaceId) external view returns (bool); 15 | } 16 | -------------------------------------------------------------------------------- /contracts/mocks/IERC20.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | /** 4 | * @title ERC20 interface 5 | * @dev see https://github.com/ethereum/EIPs/issues/20 6 | */ 7 | interface IERC20 { 8 | function transfer(address to, uint256 value) external returns (bool); 9 | 10 | function approve(address spender, uint256 value) external returns (bool); 11 | 12 | function transferFrom(address from, address to, uint256 value) external returns (bool); 13 | 14 | function totalSupply() external view returns (uint256); 15 | 16 | function balanceOf(address who) external view returns (uint256); 17 | 18 | function allowance(address owner, address spender) external view returns (uint256); 19 | 20 | event Transfer(address indexed from, address indexed to, uint256 value); 21 | 22 | event Approval(address indexed owner, address indexed spender, uint256 value); 23 | } 24 | -------------------------------------------------------------------------------- /contracts/mocks/IERC721.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | import "./IERC165.sol"; 4 | 5 | /** 6 | * @title ERC721 Non-Fungible Token Standard basic interface 7 | * @dev see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md 8 | */ 9 | contract IERC721 is IERC165 { 10 | event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); 11 | event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); 12 | event ApprovalForAll(address indexed owner, address indexed operator, bool approved); 13 | 14 | function balanceOf(address owner) public view returns (uint256 balance); 15 | function ownerOf(uint256 tokenId) public view returns (address owner); 16 | 17 | function approve(address to, uint256 tokenId) public; 18 | function getApproved(uint256 tokenId) public view returns (address operator); 19 | 20 | function setApprovalForAll(address operator, bool _approved) public; 21 | function isApprovedForAll(address owner, address operator) public view returns (bool); 22 | 23 | function transferFrom(address from, address to, uint256 tokenId) public; 24 | function safeTransferFrom(address from, address to, uint256 tokenId) public; 25 | 26 | function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public; 27 | } 28 | -------------------------------------------------------------------------------- /contracts/mocks/SpendingConditionMock.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.2; 2 | pragma experimental ABIEncoderV2; 3 | 4 | import "./IERC20.sol"; 5 | import "./IERC721.sol"; 6 | 7 | 8 | contract SpendingConditionMock { 9 | address constant SPENDER_ADDR = 0xF3beAC30C498D9E26865F34fCAa57dBB935b0D74; 10 | 11 | function test(address tokenAddr, address[] memory receivers, uint[] memory amounts) public { 12 | IERC20 token = IERC20(tokenAddr); 13 | IERC721 nft = IERC721(tokenAddr); 14 | 15 | for (uint i = 0; i < receivers.length; i++) { 16 | token.transfer(receivers[i], amounts[i]); 17 | token.balanceOf(receivers[i]); 18 | // transferFrom(from, to, tokenid) 19 | nft.balanceOf(receivers[i]); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /contracts/mocks/TestMemOps.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.2; 2 | pragma experimental ABIEncoderV2; 3 | 4 | 5 | import { MemOps } from "../MemOps.slb"; 6 | 7 | 8 | contract TestMemOps { 9 | 10 | uint constant internal WORD_SIZE = 32; 11 | 12 | function testAllocate32() public { 13 | uint NUM_WORDS = 55; 14 | uint fMem; 15 | assembly { 16 | fMem := mload(0x40) 17 | } 18 | MemOps.allocate32(NUM_WORDS); 19 | uint fMem2; 20 | assembly { 21 | fMem2 := mload(0x40) 22 | } 23 | require(fMem2 == fMem + NUM_WORDS*WORD_SIZE, "Free Memory Pointer after alocate32"); 24 | } 25 | 26 | function testMemcopy32() public { 27 | uint NUM_WORDS = 20; 28 | uint srcPtr = MemOps.allocate32(NUM_WORDS); 29 | 30 | for (uint i = 0; i < NUM_WORDS; i++) { 31 | uint pos = srcPtr + i*WORD_SIZE; 32 | assembly { 33 | mstore(pos, add(i, 1)) 34 | } 35 | } 36 | 37 | uint destPtr; 38 | assembly { 39 | destPtr := mload(0x40) 40 | mstore(0x40, add(destPtr, NUM_WORDS)) 41 | } 42 | MemOps.memcopy32(srcPtr, destPtr, NUM_WORDS); 43 | 44 | for (uint i = 0; i < NUM_WORDS; i++) { 45 | uint pos = srcPtr + i*WORD_SIZE; 46 | uint val = 0; 47 | assembly { 48 | val := mload(pos) 49 | } 50 | require(val == i + 1, "memcopy32"); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /contracts/mocks/VerifierMock.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.2; 2 | pragma experimental ABIEncoderV2; 3 | 4 | import "../Enforcer.sol"; 5 | 6 | 7 | contract VerifierMock { 8 | Enforcer enforcer; 9 | uint256 public timeoutDuration; 10 | 11 | constructor(uint256 timeout) public { 12 | timeoutDuration = timeout; 13 | } 14 | 15 | function setEnforcer(address _enforcer) public { 16 | enforcer = Enforcer(_enforcer); 17 | } 18 | 19 | function initGame( 20 | bytes32 executionId, 21 | bytes32 solverHashRoot, 22 | bytes32 challengerHashRoot, 23 | uint256 executionDepth, 24 | // optional for implementors 25 | bytes32 customEnvironmentHash, 26 | // TODO: should be the bytes32 root hash later on 27 | bytes32 codeHash, 28 | bytes32 dataHash, 29 | address challenger 30 | ) public returns (bytes32 disputeId) { 31 | // do nothing 32 | } 33 | 34 | function submitResult(bytes32 executionId, bool solverWon, address challenger) public { 35 | enforcer.result(executionId, solverWon, challenger); 36 | } 37 | 38 | function dummy() public {} 39 | } 40 | 41 | -------------------------------------------------------------------------------- /docs/Architecture.md: -------------------------------------------------------------------------------- 1 | # EVM Fraud Proof Architecture 2 | 3 | The work builds on [solEVM](https://github.com/Ohalo-Ltd/solevm), an EVM runtime written in Solidity. This document will describe how the Solidy EVM Runtime can be extended to provide the 3 function that are necessary to do on-chain computation verification: 4 | 5 | 1. Run a program and export state at specific step (number of instructions executed). 6 | 2. Initiate the VM at any specific step with state exported in 1. 7 | 3. Once initiated as in 2., run the next OP-code and export state again as in 1. 8 | 9 | 10 | These 3 functions together will allow to run any EVM code step by step, while producing the same results as a continuous run of an EVM. We will compress the exported state of the EVM through a deterministic hashing function. This hash will give us the follow two abilities: 11 | 12 | - Ability to compare state of EVM at different states of the binary search (Truebit protocol) 13 | - Ability to verify that EVM has been set up with correct parameters in state 2 14 | 15 | ## Execution Modes 16 | 17 | We distinguish two modes of operation for the implementation we create: 18 | 19 | **Off-chain Mode** - This mode is used to initiate the VM with a computation task and instruct it to run until a specific instruction count. The VM should then stop and export the state (function 1. above). For this mode the VM can be invoked with a static call. 20 | 21 | **On-chain Mode** - When the VM is initiated with an intermediate state generated by a run of the off-chain VM and instructed to run only the next OP-code. This invocation needs to fit into an on-chain transaction. 22 | 23 | The OP-codes are organized in groups based on which elements of the VM they access: 24 | 25 | **Stack-only type OP-codes:** 26 | ADD, MUL, SUB, DIV, SDIV, MOD, SMOD, ADDMOD, MULMOD, EXP, SIGNEXTEND, SHL, SHR, SAR, LT, GT, SLT, SGT, EQ, ISZERO, AND, OR, XOR, NOT, BYTE, POP, DUP, SWAP, 27 | 28 | 29 | **Context and stack type OP-codes:** 30 | ADDRESS, BALANCE, ORIGIN, CALLER, CALLVALUE, GASPRICE, BLOCKHASH, COINBASE, TIMESTAMP, BLOCK_NUMBER, DIFFICULTY, GASLIMIT, JUMP, JUMPI, PC, GAS, RETURNDATASIZE 31 | 32 | **Code and stack type OP-codes:** 33 | CODELOAD, CODESIZE, PUSH1 - PUSH32, 34 | 35 | **Data and stack type OP-codes:** 36 | CALLDATALOAD, CALLDATASIZE 37 | <--- (!!! at this point we should be able to prove basic contracts !!!) ---> 38 | 39 | **Memory and Stack type:** 40 | MLOAD, MSTORE, MSTORE8, MSIZE 41 | 42 | **Storage and Stack type OP-codes:** 43 | SSTORE, SLOAD 44 | <--- (!!! prove basic contracts with memory and storage !!!) ---> 45 | 46 | **Context, stack and memory type OP-codes:** 47 | LOG 48 | 49 | **Data, stack and memory type OP-codes:** 50 | CALLDATACOPY 51 | 52 | **Code, stack and memory type OP-codes:** 53 | CODECOPY 54 | 55 | **Return, Stack and Memory type OP-codes:** 56 | RETURN, REVENT, RETURNDATACOPY 57 | <--- (!!! most self-contained contract provable !!!) ---> 58 | 59 | **Extra difficult type:** 60 | EXTCODECOPY, EXTCODESIZE, CREATE, CALL, DELEGATECALL, STATICCALL, SELFDESTRUCT, PRECOMPILES 61 | 62 | the complexity of opcodes differs based on the VM elements they need to access and the complexity of the access (read/write full word vs. read/write dynamic array or struct): 63 | 64 | a) OP-codes that access (stack and (context or code or data)) 65 | b) OP-codes that access memory and storage 66 | c) OP-codes that access (stack and memory and (context or code or data)) 67 | d) OP-codes that modify global state in a complex way like call, create, selfdestroy 68 | 69 | ## Milestones 70 | 71 | The work is split into two milestones in terms of proof size: 72 | 73 | 1. Simple full state initialization for on-chain VM - in this implementation the different VM elements are initiated with full arrays of data for the different elements like stack, memory, and storage. 74 | 2. Compressed proof-based initialization (hydrated state) for on-chain VM - in this implementation only the data which is needed by the next opcode is provided to the VM. Other data is represented by intermediate hashes of the merkle proof. -------------------------------------------------------------------------------- /docs/Merkelizer.md: -------------------------------------------------------------------------------- 1 | # Merkelizer Explained 2 | 3 | Our execution steps: 4 | [executionState 0, executionState 1, executionState 2, ...] 5 | 6 | For each `executionState`, we have a `stateHash` which references back 7 | to the previous `executionState`. This is done via compact proofs. 8 | 9 | 10 | Let's construct our tree with 6 executionStates; aka 'execution steps'; 11 | So we end up with [stateHash0, stateHash1, stateHash2, stateHash3, stateHash4, stateHash5] 12 | Note: 13 | We assume power of 2 here, so we pad with an empty state and the hash of such a empty state consists of zeros 0x000000.... 14 | 15 | If a prevLeaf doesn't exist (starting at 0), that leaf will have no `left` part, 16 | and the `right` part going to be the `initialStateHash`, 17 | aka; the known state before the first execution (code, callData, gasRemaining). 18 | 19 | `prevLeaf = { right: initialState, hash: initialStateHash }` 20 | ``` 21 | [ 22 | prevLeaf = { left: prevLeaf.right.hash, right: stateHash0, hash: hash(left, right) }, 23 | prevLeaf = { left: prevLeaf.right.hash, right: stateHash1, hash: hash(left, right) }, 24 | prevLeaf = { left: prevLeaf.right.hash, right: stateHash2, hash: hash(left, right) }, 25 | prevLeaf = { left: prevLeaf.right.hash, right: stateHash3, hash: hash(left, right) }, 26 | prevLeaf = { left: prevLeaf.right.hash, right: stateHash4, hash: hash(left, right) }, 27 | prevLeaf = { left: prevLeaf.right.hash, right: stateHash5 , hash: hash(left, right) }, 28 | ] 29 | ``` 30 | Fine, now we can build our tree until we are left with one node (=root) 31 | 32 | solver: 33 | below: the nodes we just constructed above 34 | ``` 35 | [ a192 (l:initialState r:ef8a) ] [ e18a (l:ef8a r:77d9) ] [ d87e (l:77d9 r:d5b2) ] [ 3639 (l:d5b2 r:b2b2) ] [ 02ed (l:b2b2 r:ddf7) ] [ df38 (l:ddf7 r:67c9) ] 36 | [ 8b76 (l:a192 r:e18a) ] [ 8a08 (l:d87e r:3639) ] [ 49f9 (l:02ed r:df38) ] [ 0000 (l:0000 r:0000) < padding] 37 | [ 3c2e (l:8b76 r:8a08) ] [ 080d (l:49f9 r:0000) ] 38 | [ 3f61 (l:3c2e r:080d) ] < root node 39 | ``` 40 | challenger: 41 | below: the nodes we just constructed above 42 | ``` 43 | [ a192 (l:initialState r:ef8a) ] [ e18a (l:ef8a r:77d9) ] [ d87e (l:77d9 r:d5b2) ] [ 3639 (l:d5b2 r:b2b2) ] [ e6a7 (l:b2b2 r:78bd) ] [ a7ea (l:78bd r:67c9) ] 44 | [ 8b76 (l:a192 r:e18a) ] [ 8a08 (l:d87e r:3639) ] [ cf2d (l:e6a7 r:a7ea) ] [ 0000 (l:0000 r:0000) < padding] 45 | [ 3c2e (l:8b76 r:8a08) ] [ ebab (l:cf2d r:0000) ] 46 | [ 3d4a (l:3c2e r:ebab) ] < root node 47 | ``` 48 | 49 | Note: In this example the Solver is wrong. 50 | 51 | Let's walk the path from root to leaves; 52 | We always compare `left`, `right` from solver and challenger; 53 | Both of them need to follow the path they do *not* agree on. 54 | If they both have different `left` and `right` hashes, they should default to/follow left. 55 | 56 | Note: Solver & Challenger can submit out of order in each 'round', but they have to wait for each other to go onto the next 'level'. 57 | 58 | The goal here is to find the first disagreement. 59 | 60 | Let's go: 61 | solver goes right from 3f61 to 080d 62 | challenger goes right from 3d4a to ebab 63 | 64 | solver goes left from 080d to 49f9 65 | challenger goes left from ebab to cf2d 66 | 67 | solver goes left from 49f9 to 02ed 68 | challenger goes left from cf2d to e6a7 69 | **They reached their leaves** 70 | 71 | Seems like they disagree on stateHash4(solver=ddf7, challenger=78bd) and they agree on stateHash3(b2b2). 72 | Because we construct our leaf nodes with `left: prevLeaf.right.hash` / the previous the execution, 73 | we end up with the knowledge of the point disagreement and a stateHash they agree on (left). 74 | 75 | Note: 76 | In the event that they do not agree on any execution step, we enforce the left-most `stateHash` = `initialStateHash`. 77 | That way we have known starting point. 78 | 79 | -------------------------------------------------------------------------------- /docs/SystemDescriptionDocument.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | The goal of this project is to verify off-chain EVM computation on-chain. 3 | In our particular case, it is used to correctly exit spending conditions from a side-chain to the main-chain 4 | and also a means of a PoS mechanism to secure the side-chain. 5 | 6 | Executing a program consists of many intermediate steps and in turn, 7 | the EVM consists of various elements which change their data after each single step (opcode) of the program. 8 | These intermediate states can be compared in a deterministic manner, by hashing the state of the EVM’s elements into a single state hash. 9 | This makes it possible to construct a merkle tree from all those state hashes, each one of them representing a single execution step. 10 | 11 | To find the first divergence of the execution (the first mismatch), we start a round-based path walking, 12 | starting from the merkle root until we reach the leaves. 13 | 14 | For more information on how this works, take a look at [Merkelizer.md](./Merkelizer.md). 15 | 16 | ## Example 17 | Let us assume a solver registers an execution result given the `code` of the program, inputs (`callData`) and the merkle root hash of the execution. 18 | A Verifier can now execute this program and compare if both merkle root hashes are the same. 19 | If the root hash are the same then both have the same result, if not, the Verifier can challenge the solver. 20 | 21 | 22 | # Actors 23 | 24 | ## Solver 25 | The solver registers execution result with the `Enforcer` contract. 26 | This is a requirement if the solver wants to exit some tokens under a spending condition to the main-chain. 27 | 28 | ## Verifier / Challenger 29 | Potential verifiers can watch execution registrations and validate those off-chain. 30 | If the off-chain result does not match the on-chain result, the validator is free to open a dispute, becoming a challenger. 31 | 32 | 33 | # Incentivisation - Bonds 34 | 35 | ## Solver 36 | The solver is incentified because this is a requirement to exit the spending condition to main-chain. 37 | For every registered execution, the solver also needs to attach it with a bond. 38 | If the solver gets challenged and wins, solver owns the bond of the challenger. 39 | (Challenger's bond is slashed) 40 | 41 | If the solver's execution result does not get challenged in a given challenge period, the execution is assumed as valid. 42 | In the other case, if the solver does stop playing the verification game and the game is reaching the challenge period, 43 | the solver will automatically lose (`claimTimeout`). 44 | 45 | ## Verifier 46 | The verifier is incentified for the very fact that proving a execution wrong, the verifier gets the bond of the solver. 47 | (Solver's bond is slashed) 48 | 49 | If the challenger opens a dispute and stops playing the verification game or committed a wrong result, 50 | the solver will win this dispute if the challenge reaches the challenge period (`claimTimeout`) and 51 | gets the bond of the verifier (Verifier's bond is slashed). 52 | 53 | ## Notes 54 | Anyone who is aware of the computation path can play a particular challenge. 55 | That means it is not pegged to any address, except for the bonds. 56 | 57 | 58 | # Contracts / Classes - Inheritance 59 | 60 | ``` 61 | -------------------------------------------------------------------------------- 62 | { Accounts Constants Memory Stack Utils Code Logs Storage } 63 | | 64 | EVMRuntime 65 | | 66 | HydratedRuntime - Supporting proofs 67 | | 68 | Interceptor - opcode handlers overwritten to support SpendingConditions 69 | | 70 | Verifier - Verification Game 71 | 72 | Enforcer - Execution registration and handling of bonds 73 | 74 | -------------------------------------------------------------------------------- 75 | ``` 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solEVM-enforcer", 3 | "version": "0.1.0", 4 | "description": "The EVM Enforcer is a computation verification engine that allows on-chain enforcement of off-chain EVM execution.", 5 | "author": "LeapDAO", 6 | "license": "MPL-2.0", 7 | "keywords": [ 8 | "evm", 9 | "solidity", 10 | "truebit" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/leapdao/solEVM-enforcer.git" 15 | }, 16 | "main": "utils/index.js", 17 | "scripts": { 18 | "test": "yarn test:contracts && yarn test:utils", 19 | "test:contracts": "yarn compile:contracts && scripts/test.sh test/contracts/*", 20 | "test:utils": "mocha test/utils/", 21 | "lint": "eslint .", 22 | "lint:fix": "eslint . --fix", 23 | "lint:sol": "solhint contracts/**/*{.sol,.slb}", 24 | "lint:all": "yarn lint && yarn lint:sol", 25 | "lint:all:fix": "yarn lint:fix && yarn lint:sol", 26 | "compile:contracts": "scripts/compile.js", 27 | "coverage": "scripts/start_geth.sh && RPC_PORT=8333 yarn develatus-apparatus" 28 | }, 29 | "devDependencies": { 30 | "develatus-apparatus": "https://github.com/pinkiebell/develatus-apparatus.git#0.1.1", 31 | "eslint": "=6.0.1", 32 | "ganache-cli": "=6.4.0", 33 | "mocha": "=6.1.4", 34 | "solc": "=0.5.2", 35 | "solhint": "=2.1.0" 36 | }, 37 | "dependencies": { 38 | "ethereumjs-util": "=6.1.0", 39 | "ethers": "=4.0.33", 40 | "keccak": "=2.0.0", 41 | "rustbn.js": "=0.2.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /scripts/compile.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const fs = require('fs'); 6 | const { basename } = require('path'); 7 | const solc = require('solc'); 8 | 9 | const contractsDir = 'contracts'; 10 | const files = fs.readdirSync(contractsDir); 11 | const sources = {}; 12 | let outputDir = ''; 13 | 14 | ['build/', 'contracts'].forEach( 15 | function (dir) { 16 | outputDir += dir; 17 | if (!fs.existsSync(outputDir)) { 18 | fs.mkdirSync(outputDir); 19 | } 20 | } 21 | ); 22 | 23 | while (files.length) { 24 | const file = files.pop(); 25 | const path = `${contractsDir}/${file}`; 26 | const stat = fs.statSync(path); 27 | 28 | if (stat.isFile() && (file.endsWith('.sol') || file.endsWith('.slb') || file.endsWith('.yul'))) { 29 | const source = fs.readFileSync(path).toString(); 30 | sources[path] = { content: source }; 31 | process.stdout.write(`> Compiling ${path}\n`); 32 | } 33 | 34 | if (stat.isDirectory()) { 35 | fs.readdirSync(path).forEach( 36 | function (p) { 37 | files.push(`${file}/${p}`); 38 | } 39 | ); 40 | } 41 | } 42 | 43 | const compilerInput = { 44 | language: 'Solidity', 45 | sources: sources, 46 | settings: { 47 | evmVersion: 'constantinople', 48 | optimizer: { 49 | enabled: true, 50 | runs: 2, 51 | }, 52 | outputSelection: { 53 | '*': { 54 | '': [ 55 | 'legacyAST', 56 | 'ast', 57 | ], 58 | '*': [ 59 | 'abi', 60 | 'metadata', 61 | 'evm.bytecode.object', 62 | 'evm.bytecode.sourceMap', 63 | 'evm.deployedBytecode.object', 64 | 'evm.deployedBytecode.sourceMap', 65 | 'userdoc', 66 | 'devdoc', 67 | ], 68 | }, 69 | }, 70 | }, 71 | }; 72 | 73 | const output = JSON.parse(solc.compile(JSON.stringify(compilerInput))); 74 | 75 | if (output.errors) { 76 | output.errors.forEach((obj) => process.stderr.write(obj.formattedMessage)); 77 | } 78 | 79 | if (!output.contracts) { 80 | process.exit(1); 81 | } 82 | 83 | for (const file in output.contracts) { 84 | const contract = output.contracts[file]; 85 | const sourceObj = output.sources[file]; 86 | const source = sources[file].content; 87 | 88 | for (const contractName in contract) { 89 | const obj = contract[contractName]; 90 | 91 | obj.id = sourceObj.id; 92 | obj.ast = sourceObj.ast; 93 | obj.legacyAST = sourceObj.legacyAST; 94 | obj.source = source; 95 | 96 | const evm = obj.evm; 97 | delete obj.evm; 98 | 99 | obj.contractName = contractName; 100 | obj.bytecode = `0x${evm.bytecode.object}`; 101 | obj.sourceMap = evm.bytecode.sourceMap; 102 | obj.deployedBytecode = `0x${evm.deployedBytecode.object}`; 103 | obj.deployedSourceMap = evm.deployedBytecode.sourceMap; 104 | 105 | const artifactPath = `${outputDir}/${contractName}.json`; 106 | 107 | fs.writeFileSync(artifactPath, JSON.stringify(obj, null, 2)); 108 | process.stdout.write(`> Artifact for ${contractName} written to ${artifactPath}\n`); 109 | } 110 | } 111 | 112 | process.stdout.write(`> Compiled successfully using solc ${solc.version()}\n`); 113 | -------------------------------------------------------------------------------- /scripts/geth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var personal = personal; 4 | 5 | if (personal.listAccounts.length === 0) { 6 | var k = '2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b750120'; 7 | 8 | for (var i = 0; i < 5; i++) { 9 | try { 10 | personal.importRawKey(k + i.toString(), ''); 11 | } catch (e) { 12 | } 13 | } 14 | } 15 | 16 | personal.listAccounts.forEach( 17 | function (e) { 18 | personal.unlockAccount(e, '', ~0 >>> 0); 19 | } 20 | ); 21 | -------------------------------------------------------------------------------- /scripts/start_geth.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Exit script as soon as a command fails. 4 | set -o errexit 5 | 6 | geth_port=8222 7 | geth=$(which geth) 8 | 9 | geth_running() { 10 | nc -z localhost "$geth_port" 11 | } 12 | 13 | start_geth() { 14 | rm -rf /tmp/geth 15 | 16 | args=( 17 | "--datadir=/tmp/geth" 18 | "--networkid=99" 19 | "--maxpeers=0" 20 | "--nodiscover" 21 | "--nousb" 22 | "--targetgaslimit=0xfffffffffffff" 23 | "--gasprice=0x01" 24 | "--rpc" 25 | "--rpcport="$geth_port"" 26 | "--rpcapi=eth,net,web3,debug" 27 | ) 28 | $geth "${args[@]}" init scripts/genesis.json 29 | $geth "${args[@]}" js scripts/geth.js 30 | $geth "${args[@]}" --mine &> /dev/null & 31 | 32 | geth_pid=$! 33 | 34 | while true; do 35 | echo 'waiting for geth' 36 | if [ -e /tmp/geth/geth.ipc ]; then 37 | break 38 | fi 39 | sleep 1 40 | done 41 | $geth "${args[@]}" --exec 'loadScript("scripts/geth.js")' attach 42 | } 43 | 44 | if geth_running; then 45 | echo "Using existing instance" 46 | else 47 | echo "Starting new instance" 48 | start_geth 49 | fi 50 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Exit script as soon as a command fails. 4 | set -o errexit 5 | 6 | ganache_port=8111 7 | 8 | ganache_running() { 9 | nc -z localhost "$ganache_port" 10 | } 11 | 12 | start_ganache() { 13 | # We define 10 accounts with balance 1M ether, needed for high-value tests. 14 | local accounts=( 15 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501200,1000000000000000000000000" 16 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501201,1000000000000000000000000" 17 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501202,1000000000000000000000000" 18 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501203,1000000000000000000000000" 19 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501204,1000000000000000000000000" 20 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501205,1000000000000000000000000" 21 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501206,1000000000000000000000000" 22 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501207,1000000000000000000000000" 23 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501208,1000000000000000000000000" 24 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501209,1000000000000000000000000" 25 | ) 26 | 27 | node_modules/.bin/ganache-cli --gasPrice 0x01 --gasLimit 0xfffffffffffff --hardfork petersburg --port "$ganache_port" "${accounts[@]}" > /dev/null & 28 | 29 | ganache_pid=$! 30 | } 31 | 32 | if ganache_running; then 33 | echo "Using existing ganache instance" 34 | else 35 | echo "Starting our own ganache instance" 36 | start_ganache 37 | sleep 3 38 | fi 39 | 40 | if [ "$SOLC_NIGHTLY" = true ]; then 41 | echo "Downloading solc nightly" 42 | wget -q https://raw.githubusercontent.com/ethereum/solc-bin/gh-pages/bin/soljson-nightly.js -O /tmp/soljson.js && find . -name soljson.js -exec cp /tmp/soljson.js {} \; 43 | fi 44 | 45 | RPC_PORT=$ganache_port yarn mocha --timeout 60000 "$@" 46 | -------------------------------------------------------------------------------- /scripts/test_geth.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Exit script as soon as a command fails. 4 | set -o errexit 5 | 6 | source "$(dirname $0)/start_geth.sh" 7 | RPC_PORT=$geth_port yarn mocha --timeout 60000 "$@" 8 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-unused-expressions": 0 4 | } 5 | } -------------------------------------------------------------------------------- /test/contracts/PlasmaVerifier.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { wallets, deployContract, txOverrides, opcodeNames } = require('./../helpers/utils'); 4 | 5 | const SpendingConditionMock = require('./../../build/contracts/SpendingConditionMock.json'); 6 | const Interceptor = require('./../../build/contracts/PlasmaVerifier.json'); 7 | 8 | describe('PlasmaVerifier', () => { 9 | // skip test if we do coverage 10 | if (process.env.COVERAGE) { 11 | return; 12 | } 13 | 14 | const receivers = [ 15 | '0x1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a', 16 | '0x1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b', 17 | ]; 18 | const txAmounts = [ 19 | 0xffff, 20 | 0xfafa, 21 | ]; 22 | let tokenContract; 23 | let spendingCondition; 24 | let interceptor; 25 | 26 | before(async () => { 27 | tokenContract = { address: '0xFc99E0a69A2D7F99153A7F07F7907Fb30F24FDB7' }; 28 | spendingCondition = await deployContract(SpendingConditionMock); 29 | interceptor = await deployContract(Interceptor, 1000000); 30 | }); 31 | 32 | it('should pass with SpendingConditionMock', async () => { 33 | let tx; 34 | let data = spendingCondition.interface.functions.test.encode( 35 | [tokenContract.address, receivers, txAmounts] 36 | ); 37 | 38 | tx = await interceptor.testRun( 39 | { 40 | caller: wallets[0].address, 41 | spendingCondition: spendingCondition.address, 42 | callData: data, 43 | }, 44 | txOverrides 45 | ); 46 | tx = await tx.wait(); 47 | 48 | tx.events.forEach( 49 | (ele) => { 50 | if (ele.args.pc !== undefined) { 51 | let hexStr = ele.args.opcode.toString(16); 52 | if (hexStr.length === 1) { 53 | hexStr = '0' + hexStr; 54 | } 55 | console.log(ele.args.stepRun + ' pc=' + ele.args.pc + 'opcode=' + opcodeNames[hexStr]); 56 | return; 57 | } 58 | 59 | console.log(ele.args); 60 | } 61 | ); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/contracts/dispute.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ethers = require('ethers'); 4 | const debug = require('debug')('vgame-test'); 5 | const assert = require('assert'); 6 | 7 | const { Merkelizer, ExecutionPoker, Constants, FragmentTree } = require('./../../utils'); 8 | const disputeFixtures = require('./../fixtures/dispute'); 9 | const { onchainWait, deployContract, deployCode, wallets, provider } = require('./../helpers/utils'); 10 | 11 | const Verifier = require('./../../build/contracts/Verifier.json'); 12 | const Enforcer = require('./../../build/contracts/Enforcer.json'); 13 | 14 | const SOLVER_VERIFIED = (1 << 2); 15 | 16 | const EVMParameters = { 17 | origin: '0xa1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1', 18 | target: '0xfeefeefeefeefeefeefeefeefeefeefeefeefee0', 19 | blockHash: '0xdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdc', 20 | blockNumber: 123, 21 | time: 1560775755, 22 | txGasLimit: 0xffffffffff, 23 | customEnvironmentHash: '0x0000000000000000000000000000000000000000000000000000000000000000', 24 | codeHash: '0x0000000000000000000000000000000000000000000000000000000000000000', 25 | dataHash: '0x0000000000000000000000000000000000000000000000000000000000000000', 26 | }; 27 | 28 | class MyExecutionPoker extends ExecutionPoker { 29 | async submitRound (disputeId) { 30 | try { 31 | await super.submitRound(disputeId); 32 | } catch (e) { 33 | // ignore 34 | } 35 | } 36 | 37 | async submitProof (disputeId, computationPath) { 38 | try { 39 | await super.submitProof(disputeId, computationPath); 40 | } catch (e) { 41 | // ignore 42 | } 43 | 44 | this.afterSubmitProof(disputeId); 45 | } 46 | 47 | async computeCall (evmParams, invalidateLastStep) { 48 | return { steps: this._steps, merkle: this._merkle, codeFragmentTree: this._codeFragmentTree }; 49 | } 50 | 51 | async requestExecution (code, callData, doDeployCode) { 52 | EVMParameters.blockNumber++; 53 | 54 | let codeHash; 55 | 56 | if (doDeployCode) { 57 | const codeContract = await deployCode(code); 58 | codeHash = `0x${codeContract.address.replace('0x', '').toLowerCase().padEnd(64, '0')}`; 59 | } else { 60 | this._codeFragmentTree = new FragmentTree().run(code.join('')); 61 | codeHash = this._codeFragmentTree.root.hash; 62 | } 63 | 64 | const dataHash = Merkelizer.dataHash(callData); 65 | const evmParams = Object.assign(EVMParameters, { codeHash, dataHash }); 66 | 67 | return super.requestExecution(evmParams, callData); 68 | } 69 | 70 | log (...args) { 71 | debug(...args); 72 | } 73 | } 74 | 75 | async function doGame ({ verifier, code, callData, execPokerSolver, execPokerChallenger, doDeployCode }) { 76 | return new Promise( 77 | (resolve, reject) => { 78 | let resolved = false; 79 | let state = 0; 80 | 81 | async function decide (disputeId) { 82 | state++; 83 | if (state !== 2) { 84 | return; 85 | } 86 | 87 | const dispute = await verifier.disputes(disputeId); 88 | let winner = 'challenger'; 89 | if ((dispute.state & SOLVER_VERIFIED) !== 0) { 90 | winner = 'solver'; 91 | } 92 | if (!resolved) { 93 | resolved = true; 94 | resolve(winner); 95 | } 96 | } 97 | 98 | execPokerSolver.afterSubmitProof = async (disputeId) => { 99 | decide(disputeId); 100 | }; 101 | execPokerChallenger.afterSubmitProof = async (disputeId) => { 102 | decide(disputeId); 103 | }; 104 | execPokerSolver.onSlashed = () => { 105 | if (!resolved) { 106 | resolved = true; 107 | resolve('challenger'); 108 | } 109 | }; 110 | execPokerChallenger.onSlashed = () => { 111 | if (!resolved) { 112 | resolved = true; 113 | resolve('solver'); 114 | } 115 | }; 116 | 117 | execPokerSolver.requestExecution(code, callData, doDeployCode); 118 | } 119 | ); 120 | } 121 | 122 | describe('Verifier', function () { 123 | let enforcer; 124 | let verifier; 125 | let execPokerSolver; 126 | let execPokerChallenger; 127 | 128 | before(async () => { 129 | const taskPeriod = 1000000; 130 | const challengePeriod = 1000; 131 | const timeoutDuration = 10; 132 | const bondAmount = 1; 133 | const maxExecutionDepth = 10; 134 | 135 | verifier = await deployContract(Verifier, timeoutDuration); 136 | enforcer = await deployContract( 137 | Enforcer, verifier.address, taskPeriod, challengePeriod, bondAmount, maxExecutionDepth 138 | ); 139 | 140 | let tx = await verifier.setEnforcer(enforcer.address); 141 | 142 | await tx.wait(); 143 | 144 | // yes, we need a new provider. Otherwise we get duplicate events... 145 | const solverWallet = wallets[2].connect(new ethers.providers.JsonRpcProvider(provider.connection.url)); 146 | const challengerWallet = wallets[3].connect(new ethers.providers.JsonRpcProvider(provider.connection.url)); 147 | // let's be faster (events) 148 | solverWallet.provider.pollingInterval = 30; 149 | challengerWallet.provider.pollingInterval = 30; 150 | 151 | execPokerSolver = new MyExecutionPoker( 152 | enforcer, 153 | verifier, 154 | solverWallet, 155 | Constants.GAS_LIMIT, 156 | 'solver' 157 | ); 158 | after(function (done) { 159 | done(); 160 | process.exit(0); 161 | }); 162 | 163 | execPokerChallenger = new MyExecutionPoker( 164 | enforcer, 165 | verifier, 166 | challengerWallet, 167 | Constants.GAS_LIMIT, 168 | 'challenger' 169 | ); 170 | execPokerChallenger.alwaysChallenge = true; 171 | }); 172 | 173 | describe('with contract bytecode deployed', () => { 174 | disputeFixtures( 175 | async (code, callData, solverMerkle, challengerMerkle, expectedWinner) => { 176 | // use merkle tree from fixture 177 | execPokerSolver._merkle = solverMerkle; 178 | execPokerSolver._steps = []; 179 | execPokerChallenger._merkle = challengerMerkle; 180 | execPokerChallenger._steps = []; 181 | 182 | const winner = await doGame( 183 | { verifier, code, callData, execPokerSolver, execPokerChallenger, doDeployCode: true } 184 | ); 185 | assert.equal(winner, expectedWinner, 'winner should match fixture'); 186 | await onchainWait(10); 187 | } 188 | ); 189 | }); 190 | 191 | describe('without contract bytecode deployed', () => { 192 | disputeFixtures( 193 | async (code, callData, solverMerkle, challengerMerkle, expectedWinner) => { 194 | // use merkle tree from fixture 195 | execPokerSolver._merkle = solverMerkle; 196 | execPokerSolver._steps = []; 197 | execPokerChallenger._merkle = challengerMerkle; 198 | execPokerChallenger._steps = []; 199 | 200 | const winner = await doGame( 201 | { verifier, code, callData, execPokerSolver, execPokerChallenger } 202 | ); 203 | assert.equal(winner, expectedWinner, 'winner should match fixture'); 204 | await onchainWait(10); 205 | } 206 | ); 207 | }); 208 | }); 209 | -------------------------------------------------------------------------------- /test/contracts/ethereumRuntime.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const assert = require('assert'); 5 | 6 | const { getCode, deployContract, deployCode } = 7 | require('./../helpers/utils'); 8 | const { assertTokenBagEqual } = require('./../helpers/tokenBag.js'); 9 | const fixtures = require('./../fixtures/runtime'); 10 | const runtimeGasUsed = require('./../fixtures/runtimeGasUsed'); 11 | const Runtime = require('./../../utils/EthereumRuntimeAdapter'); 12 | const OP = require('./../../utils/constants'); 13 | 14 | const { PUSH1, BLOCK_GAS_LIMIT } = OP; 15 | 16 | const EthereumRuntime = require('./../../build/contracts/EthereumRuntime.json'); 17 | 18 | describe('Runtime', function () { 19 | let rt; 20 | 21 | before(async () => { 22 | rt = new Runtime(await deployContract(EthereumRuntime)); 23 | }); 24 | 25 | describe('executeAndStop', () => { 26 | it('should allow to run a specific number of steps', async () => { 27 | // codepointers: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C 28 | // execution order: 0, 1, 2, 8, 9, A, B, 3, 4, 5, 6, 7, C 29 | const code = [ 30 | OP.PUSH1, '08', OP.JUMP, // jump to 0x08 31 | OP.JUMPDEST, OP.GASLIMIT, OP.PUSH1, '0C', OP.JUMP, // 0x03. Jump to 0x0c 32 | OP.JUMPDEST, OP.PUSH1, '03', OP.JUMP, // 0x08. Jump to 0x03 33 | OP.JUMPDEST, // 0x0c 34 | ]; 35 | const data = '0x'; 36 | const codeContract = await deployCode(code); 37 | const executeStep = async (stepCount) => 38 | (await rt.execute( 39 | { 40 | code: codeContract.address, 41 | data: data, 42 | pc: 0, 43 | stepCount: stepCount, 44 | } 45 | )).pc; 46 | 47 | assert.equal(await executeStep(1), 2, 'should be at 2 JUMP'); 48 | assert.equal(await executeStep(2), 8, 'should be at 8 JUMPDEST'); 49 | assert.equal(await executeStep(3), 9, 'should be at 9 PUSH1'); 50 | assert.equal(await executeStep(4), 11, 'should be at 11 JUMP'); 51 | assert.equal(await executeStep(5), 3, 'should be at 3 JUMPDEST'); 52 | }); 53 | }); 54 | 55 | describe('initAndExecute', () => { 56 | it('can continue from non-zero program counter', async () => { 57 | const code = [PUSH1, '03', PUSH1, '05', OP.ADD]; 58 | const codeContract = await deployCode(code); 59 | const res = await rt.execute( 60 | { 61 | code: codeContract.address, 62 | pc: 0, 63 | stepCount: 2, 64 | } 65 | ); 66 | const { stack } = await rt.execute( 67 | { 68 | code: codeContract.address, 69 | pc: 4, 70 | stepCount: 0, 71 | stack: res.stack, 72 | mem: res.mem, 73 | } 74 | ); 75 | assert.deepEqual(stack, ['0x0000000000000000000000000000000000000000000000000000000000000008']); 76 | }); 77 | 78 | const gasUsedValues = []; 79 | let totalGasUsed = 0; 80 | let totalGasUsedBaseline = 0; 81 | 82 | fixtures.forEach(async (fixture, index) => { 83 | const { code, pc, opcodeUnderTest } = getCode(fixture); 84 | const testName = fixture.description || opcodeUnderTest; 85 | 86 | it(testName, async () => { 87 | const stack = fixture.stack || []; 88 | const mem = fixture.memory || []; 89 | const data = fixture.data || '0x'; 90 | const tokenBag = fixture.tokenBag || undefined; 91 | const gasLimit = fixture.gasLimit || BLOCK_GAS_LIMIT; 92 | const gasRemaining = typeof fixture.gasRemaining !== 'undefined' ? fixture.gasRemaining : gasLimit; 93 | const codeContract = await deployCode(code); 94 | const args = { 95 | code: codeContract.address, 96 | data, 97 | pc, 98 | gasLimit, 99 | gasRemaining, 100 | stack, 101 | mem, 102 | tokenBag, 103 | }; 104 | const res = await rt.execute(args); 105 | 106 | const gasUsed = (await (await rt.execute(args, true)).wait()).gasUsed.toNumber(); 107 | 108 | totalGasUsed += gasUsed; 109 | gasUsedValues[index] = gasUsed; 110 | console.log(testName, 'gasUsed', gasUsed); 111 | 112 | const gasUsedBaseline = runtimeGasUsed[index]; 113 | 114 | if (gasUsedBaseline !== undefined) { 115 | // The max increase in gas usage 116 | const maxAllowedDiff = 500000; 117 | 118 | // Skip gas accounting if we do coverage. 119 | // Ther other hack is for ganache. It has wrong gas accounting with some precompiles 🤦 120 | if (process.env.COVERAGE || gasUsed >= 0xf810000000000) { 121 | console.log( 122 | `Skipping gas accounting for ${testName} because of broken gas accounting (ganache) or coverage` 123 | ); 124 | } else { 125 | totalGasUsedBaseline += gasUsedBaseline; 126 | 127 | assert.ok( 128 | gasUsed <= (gasUsedBaseline + maxAllowedDiff), 129 | `gasUsed(${gasUsed}) should be not more than baseline(${gasUsedBaseline}) + ${maxAllowedDiff}` 130 | ); 131 | } 132 | } else { 133 | console.log(`*** No gasUsed-baseline for ${testName} ***`); 134 | } 135 | 136 | if (fixture.result.stack) { 137 | assert.deepEqual(res.stack, fixture.result.stack, 'stack'); 138 | } 139 | if (fixture.result.memory) { 140 | assert.deepEqual(res.mem, fixture.result.memory, 'memory'); 141 | } 142 | if (fixture.result.tokenBag) { 143 | assertTokenBagEqual(fixture.result.tokenBag, res.tokenBag); 144 | } 145 | if (fixture.result.pc !== undefined) { 146 | assert.equal(res.pc.toNumber(), fixture.result.pc, 'pc'); 147 | } 148 | if (fixture.result.gasUsed !== undefined) { 149 | assert.equal(gasRemaining - parseInt(res.gas), fixture.result.gasUsed, 'gasUsed'); 150 | } 151 | if (fixture.result.errno !== undefined) { 152 | assert.equal(res.errno, fixture.result.errno, 'errno'); 153 | } 154 | 155 | // test for OUT OF GAS 156 | if (fixture.result.gasUsed !== undefined && fixture.result.gasUsed > 0) { 157 | const oogArgs = { 158 | ...args, 159 | gasRemaining: fixture.result.gasUsed - 1, 160 | }; 161 | const oogState = await rt.execute(oogArgs); 162 | // there are 2 cases here: 163 | // - out of gas because of the call 164 | // - out of gas and cannot make the call 165 | if ([OP.CALL, OP.STATICCALL, OP.CALLCODE, OP.DELEGATECALL].includes(code[0]) && oogState.errno === 0) { 166 | assert.equal(oogState.stack[0], 0); 167 | } else { 168 | assert.equal(oogState.errno, OP.ERROR_OUT_OF_GAS, 'Not out of gas'); 169 | } 170 | } 171 | 172 | if (index + 1 === fixtures.length) { 173 | console.log(`totalGasUsed new: ${totalGasUsed} old: ${totalGasUsedBaseline}`); 174 | 175 | if (totalGasUsed < totalGasUsedBaseline || fixtures.length !== runtimeGasUsed.length) { 176 | const path = './test/fixtures/runtimeGasUsed.js'; 177 | 178 | console.log(`*** New fixtures or low gas usage record. Writing results to ${path}. ***`); 179 | fs.writeFileSync(path, `'use strict';\nmodule.exports = ${JSON.stringify(gasUsedValues, null, 2)};`); 180 | } 181 | } 182 | }); 183 | }); 184 | }); 185 | 186 | it('should have enough gas', async function () { 187 | const code = [PUSH1, '03', PUSH1, '05', OP.ADD]; 188 | const data = '0x'; 189 | const gasLimit = 9; 190 | const codeContract = await deployCode(code); 191 | const res = await rt.execute({ code: codeContract.address, data, gasLimit }); 192 | // should have zero gas left 193 | assert.equal(res.gas, 0); 194 | }); 195 | 196 | it('should run out of gas', async function () { 197 | const code = [PUSH1, '03', PUSH1, '05', OP.ADD]; 198 | const data = '0x'; 199 | const gasLimit = 8; 200 | const codeContract = await deployCode(code); 201 | const res = await rt.execute({ code: codeContract.address, data, gasLimit }); 202 | // 13 = out of gas 203 | assert.equal(res.errno, 13); 204 | }); 205 | 206 | it('(OP.STATICCALL) should run out of gas', async function () { 207 | const code = [ 208 | // gas 209 | PUSH1, 'ff', 210 | // targetAddr 211 | PUSH1, '00', 212 | // value 213 | PUSH1, '00', 214 | // inOffset 215 | PUSH1, '00', 216 | // inSize 217 | PUSH1, '00', 218 | // retOffset 219 | PUSH1, '00', 220 | // retSize 221 | PUSH1, '00', 222 | OP.STATICCALL, 223 | ]; 224 | const data = '0x'; 225 | const gasLimit = 200; 226 | const codeContract = await deployCode(code); 227 | const res = await rt.execute({ code: codeContract.address, data, gasLimit }); 228 | // 13 = out of gas 229 | assert.equal(res.errno, 13); 230 | }); 231 | 232 | it('(OP.STATICCALL) should not run out of gas', async function () { 233 | const code = [ 234 | // gas 235 | PUSH1, 'ff', 236 | // targetAddr 237 | PUSH1, '00', 238 | // value 239 | PUSH1, '00', 240 | // inOffset 241 | PUSH1, '00', 242 | // inSize 243 | PUSH1, '00', 244 | // retOffset 245 | PUSH1, '00', 246 | // retSize 247 | PUSH1, '00', 248 | OP.STATICCALL, 249 | ]; 250 | const data = '0x'; 251 | const gasLimit = 2000; 252 | const codeContract = await deployCode(code); 253 | const res = await rt.execute({ code: codeContract.address, data, gasLimit }); 254 | assert.equal(res.errno, 0); 255 | }); 256 | 257 | it('should stack overflow', async function () { 258 | const code = [OP.PUSH1, '00']; 259 | const stack = Array(1024).fill(OP.ZERO_HASH); 260 | const codeContract = await deployCode(code); 261 | const res = await rt.execute({ code: codeContract.address, stack }); 262 | assert.equal(res.errno, OP.ERROR_STACK_OVERFLOW); 263 | }); 264 | 265 | it('Limited gas', async () => { 266 | let code = [OP.PUSH1, '00']; 267 | let codeContract = await deployCode(code); 268 | const res = await rt.execute( 269 | { 270 | code: codeContract.address, 271 | gasRemaining: 2, 272 | gasLimit: 1, 273 | } 274 | ); 275 | assert.equal(res.errno, OP.ERROR_OUT_OF_GAS); 276 | }); 277 | }); 278 | -------------------------------------------------------------------------------- /test/contracts/evmCode.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { deployContract } = require('./../helpers/utils'); 4 | const BN = require('ethers').utils.BigNumber; 5 | const EVMCodeMock = require('./../../build/contracts/EVMCodeMock.json'); 6 | const assert = require('assert'); 7 | 8 | describe('TestEVMCode', function () { 9 | let evmCode; 10 | 11 | before(async () => { 12 | evmCode = await deployContract(EVMCodeMock); 13 | }); 14 | 15 | const rawCodes = [ 16 | // pos 17 | '0x0000000000000000000000000000000000000000000000000000000000000000', 18 | // val 19 | '0x7f10111213141516171819202122232425262728293031323334353637383940', 20 | // pos 21 | '0x0000000000000000000000000000000000000000000000000000000000000001', 22 | // val 23 | '0x4100000000000000000000000000000000000000000000000000000000000000', 24 | // pos 25 | '0x0000000000000000000000000000000000000000000000000000000000000003', 26 | // val 27 | '0x3000000000000000000000000000000000000000000000000000000000000000', 28 | ]; 29 | 30 | it('test findFragment #1', async function () { 31 | let res = await evmCode.testFindFragment( 32 | rawCodes.slice(0, 4), 33 | 64, 34 | 0 35 | ); 36 | assert.equal(res.pos, 0); 37 | assert(res.value.eq(new BN('0x7f10111213141516171819202122232425262728293031323334353637383940'))); 38 | }); 39 | 40 | it('test findFragment #2', async function () { 41 | let res = await evmCode.testFindFragment( 42 | rawCodes.slice(0, 4), 43 | 64, 44 | 1 45 | ); 46 | assert.equal(res.pos, 1); 47 | assert(res.value.eq(new BN('0x4100000000000000000000000000000000000000000000000000000000000000'))); 48 | }); 49 | 50 | it('test findFragment #2', async function () { 51 | let res = await evmCode.testFindFragment( 52 | rawCodes, 53 | 96, 54 | 3 55 | ); 56 | assert.equal(res.pos, 3); 57 | assert(res.value.eq(new BN('0x3000000000000000000000000000000000000000000000000000000000000000'))); 58 | }); 59 | 60 | it('test to uint #1', async function () { 61 | let res = await evmCode.testToUint( 62 | rawCodes.slice(0, 4), 63 | 33, 64 | 1, 65 | 32 66 | ); 67 | assert(res.eq('0x1011121314151617181920212223242526272829303132333435363738394041')); 68 | }); 69 | 70 | it('test to uint #2', async function () { 71 | let res = await evmCode.testToUint( 72 | rawCodes.slice(0, 4), 73 | 33, 74 | 32, 75 | 1 76 | ); 77 | assert(res.eq('0x41')); 78 | }); 79 | 80 | it('test to bytes #1', async function () { 81 | let res = await evmCode.testToBytes( 82 | rawCodes.slice(0, 4), 83 | 33, 84 | 1, 85 | 1 86 | ); 87 | assert.equal(res, '0x10'); 88 | }); 89 | 90 | it('test to bytes #2', async function () { 91 | let res = await evmCode.testToBytes( 92 | rawCodes.slice(0, 4), 93 | 33, 94 | 32, 95 | 1 96 | ); 97 | assert.equal(res, '0x41'); 98 | }); 99 | 100 | it('test to bytes #3', async function () { 101 | let res = await evmCode.testToBytes( 102 | rawCodes.slice(0, 4), 103 | 33, 104 | 0, 105 | 33 106 | ); 107 | assert.equal(res, '0x7f1011121314151617181920212223242526272829303132333435363738394041'); 108 | }); 109 | 110 | it('test to bytes #4', async function () { 111 | let res = await evmCode.testToBytes( 112 | rawCodes.slice(0, 4), 113 | 33, 114 | 31, 115 | 2 116 | ); 117 | assert.equal(res, '0x4041'); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /test/contracts/evmStack.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { deployContract } = require('./../helpers/utils'); 4 | 5 | const EVMStackMock = require('./../../build/contracts/EVMStackMock.json'); 6 | const assert = require('assert'); 7 | 8 | describe('TestEVMStack', function () { 9 | let stack; 10 | 11 | before(async () => { 12 | stack = await deployContract(EVMStackMock); 13 | }); 14 | 15 | it('dupThrowsNTooSmall', async function () { 16 | await assert.rejects(stack.dupThrowsNTooSmall()); 17 | }); 18 | 19 | it('dupThrowsNTooLarge', async function () { 20 | await assert.rejects(stack.dupThrowsNTooLarge()); 21 | }); 22 | 23 | it('dupThrowsUnderflow', async function () { 24 | await assert.rejects(stack.dupThrowsUnderflow()); 25 | }); 26 | 27 | it('popThrowsUnderflow', async function () { 28 | await assert.rejects(stack.popThrowsUnderflow()); 29 | }); 30 | 31 | it('pushThrowsOverflow', async function () { 32 | await assert.rejects(stack.pushThrowsOverflow()); 33 | }); 34 | 35 | it('dupThrowsOverflow', async function () { 36 | await assert.rejects(stack.dupThrowsOverflow()); 37 | }); 38 | 39 | // if those calls fail, they will throw 40 | it('testCreate', async function () { 41 | await stack.testCreate(); 42 | }); 43 | 44 | it('testPush', async function () { 45 | await stack.testPush(); 46 | }); 47 | 48 | it('testPop', async function () { 49 | await stack.testPop(); 50 | }); 51 | 52 | it('testDup1', async function () { 53 | await stack.testDup1(); 54 | }); 55 | 56 | it('testDup16', async function () { 57 | await stack.testDup16(); 58 | }); 59 | 60 | it('testSwap1', async function () { 61 | await stack.testSwap1(); 62 | }); 63 | 64 | it('testSwap16', async function () { 65 | await stack.testSwap16(); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /test/contracts/onChain.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | const { getCodeWithStep, deployContract, deployCode, toBN } = require('./../helpers/utils'); 6 | const onChainFixtures = require('./../fixtures/onChain'); 7 | const Runtime = require('./../../utils/EthereumRuntimeAdapter'); 8 | 9 | const OP = require('./../../utils/constants'); 10 | const EthereumRuntime = require('./../../build/contracts/EthereumRuntime.json'); 11 | 12 | describe('Runtime', function () { 13 | let rt; 14 | 15 | before(async () => { 16 | rt = new Runtime(await deployContract(EthereumRuntime)); 17 | }); 18 | 19 | describe('execute - stop - execute one step - compare', () => { 20 | onChainFixtures.forEach(fixture => { 21 | const { code, step, opcodeUnderTest } = getCodeWithStep(fixture); 22 | const data = fixture.data || '0x'; 23 | let gasCost; 24 | 25 | it(opcodeUnderTest, async () => { 26 | const codeContract = await deployCode(code); 27 | // 1. export the state right before the target opcode (this supposed to be off-chain) 28 | const beforeState = await rt.execute( 29 | { 30 | code: codeContract.address, 31 | data, 32 | pc: 0, 33 | stepCount: step, 34 | } 35 | ); 36 | 37 | // 2. export state right after the target opcode (this supposed to be off-chain) 38 | const afterState = await rt.execute( 39 | { 40 | code: codeContract.address, 41 | data, 42 | pc: 0, 43 | stepCount: step + 1, 44 | } 45 | ); 46 | 47 | // 3. init with beforeState and execute just one step (target opcode) (this supposed to be on-chain) 48 | const onChainState = await rt.execute( 49 | { 50 | code: codeContract.address, 51 | data, 52 | pc: beforeState.pc, 53 | stepCount: 1, 54 | gasRemaining: beforeState.gas, 55 | stack: beforeState.stack, 56 | mem: beforeState.mem, 57 | } 58 | ); 59 | 60 | // 4. check that on-chain state is the same as off-chain 61 | // checking hashValue is enough to say that states are same 62 | assert.equal(onChainState.hashValue, afterState.hashValue, 'State Hash'); 63 | 64 | // 5. run again with limited gas 65 | if (onChainState.gas === beforeState.gas) { 66 | // skip test out of gas if already an error or cost nothing 67 | return; 68 | } 69 | gasCost = onChainState.gas; 70 | let limitedGas = toBN(OP.BLOCK_GAS_LIMIT) - gasCost - 1; 71 | if (limitedGas < 0) limitedGas = 0; 72 | 73 | const oogState = await rt.execute( 74 | { 75 | code: codeContract.address, 76 | data, 77 | pc: 0, 78 | stepCount: 0, 79 | gasRemaining: limitedGas, 80 | } 81 | ); 82 | assert.equal(oogState.errno, OP.ERROR_OUT_OF_GAS, 'Not out of gas'); 83 | }); 84 | }); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /test/contracts/testMemOps.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { deployContract } = require('./../helpers/utils'); 4 | 5 | const TestMemOps = require('./../../build/contracts/TestMemOps.json'); 6 | 7 | describe('TestMemOps', function () { 8 | let contract; 9 | 10 | before(async () => { 11 | contract = await deployContract(TestMemOps); 12 | }); 13 | 14 | // if those calls fail, they will throw 15 | it('testAllocate32', async function () { 16 | await (await contract.testAllocate32()).wait(); 17 | }); 18 | 19 | it('testMemcopy32', async function () { 20 | await (await contract.testMemcopy32()).wait(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/fixtures/runtimeGasUsed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = [ 3 | 98856, 4 | 98890, 5 | 98920, 6 | 98954, 7 | 98922, 8 | 99018, 9 | 99050, 10 | 99779, 11 | 99811, 12 | 99472, 13 | 99682, 14 | 99892, 15 | 100102, 16 | 105982, 17 | 99178, 18 | 100160, 19 | 99240, 20 | 99272, 21 | 99304, 22 | 99336, 23 | 98678, 24 | 99400, 25 | 99432, 26 | 99464, 27 | 100790, 28 | 99528, 29 | 99560, 30 | 99592, 31 | 99560, 32 | 100036, 33 | 100656, 34 | 114973, 35 | 115005, 36 | 114973, 37 | 115069, 38 | 115037, 39 | 115133, 40 | 115165, 41 | 115197, 42 | 115229, 43 | 115261, 44 | 115293, 45 | 115325, 46 | 115293, 47 | 115389, 48 | 115421, 49 | 101630, 50 | 102550, 51 | 103534, 52 | 104518, 53 | 105566, 54 | 106550, 55 | 107535, 56 | 108519, 57 | 109503, 58 | 110488, 59 | 111408, 60 | 112393, 61 | 113441, 62 | 114426, 63 | 115411, 64 | 116396, 65 | 99880, 66 | 98359, 67 | 98423, 68 | 98455, 69 | 98348, 70 | 97747, 71 | 99433, 72 | 97907, 73 | 98860, 74 | 98892, 75 | 98067, 76 | 98099, 77 | 99273, 78 | 99746, 79 | 116900, 80 | 100504, 81 | 99472, 82 | 113108, 83 | 98703, 84 | 98549, 85 | 106194, 86 | 100227, 87 | 100227, 88 | 100227, 89 | 100227, 90 | 100227, 91 | 100227, 92 | 100227, 93 | 101575, 94 | 101639, 95 | 101639, 96 | 100227, 97 | 100227, 98 | 100227, 99 | 100227, 100 | 100227, 101 | 100227, 102 | 100227, 103 | 100227, 104 | 100227, 105 | 100227, 106 | 101639, 107 | 101647, 108 | 100227, 109 | 100235, 110 | 100227, 111 | 100227, 112 | 100235, 113 | 100227, 114 | 101735, 115 | 100227, 116 | 100323, 117 | 100334, 118 | 99783, 119 | 99756, 120 | 98892, 121 | 103125, 122 | 98848, 123 | 102027, 124 | 99630, 125 | 101104, 126 | 99598, 127 | 101685, 128 | 105098, 129 | 102081, 130 | 102958, 131 | 99786, 132 | 100319, 133 | 99400, 134 | 101860, 135 | 102908, 136 | 103892, 137 | 104940, 138 | 106052, 139 | 101189, 140 | 100691, 141 | 101349, 142 | 100900, 143 | 100254, 144 | 99818, 145 | 100137, 146 | 98666, 147 | 103770, 148 | 107085, 149 | 121745, 150 | 119279, 151 | 120118, 152 | 171158, 153 | 209570, 154 | 126525, 155 | 130232, 156 | 110140, 157 | 102128, 158 | 108546, 159 | 107035, 160 | 106323, 161 | 104624, 162 | 111271, 163 | 112949, 164 | 117800, 165 | 134710, 166 | 113043, 167 | 115007, 168 | 131740, 169 | 113069, 170 | 115444, 171 | 132285, 172 | 113095, 173 | 113459, 174 | 130272, 175 | 114803, 176 | 114574, 177 | 103691, 178 | 102899, 179 | 106623, 180 | 101599, 181 | 96047, 182 | 100968, 183 | 100840, 184 | 109168, 185 | 142757, 186 | 140826, 187 | 146758, 188 | 149648, 189 | 4363961650737897, 190 | 185563, 191 | 4363961650736453, 192 | 321797, 193 | 411047, 194 | 4363961650712767, 195 | 125198, 196 | 130716, 197 | 130475, 198 | 131402, 199 | 142858, 200 | 149738, 201 | 145539, 202 | 145269, 203 | 145751, 204 | 104119 205 | ]; -------------------------------------------------------------------------------- /test/helpers/assertInvalid.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async promise => { 4 | try { 5 | await promise; 6 | assert.fail('Expected invalid not received'); 7 | } catch (error) { 8 | const revertFound = error.message.search('invalid opcode') >= 0; 9 | assert(revertFound, `Expected "invalid opcode", got ${error} instead`); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /test/helpers/assertRevert.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const Utils = require('./utils'); 5 | 6 | module.exports = async (promise, message) => { 7 | try { 8 | let tx = await promise; 9 | // geth doesn't fail immediately 10 | await tx.wait(); 11 | } catch (error) { 12 | // support for the unspecific error 'transaction failed' of geth 13 | const revertFound = error.message.search(/(revert|transaction failed|always failing transaction)/) >= 0; 14 | assert(revertFound, `Expected "revert", got ${error} instead`); 15 | const client = await Utils.client(); 16 | // Geth does not return full error message 17 | if (message && !client.startsWith('Geth')) { 18 | const messageFound = error.message.search(message) >= 0; 19 | assert(messageFound, `Expect ${message}, got ${error} instead`); 20 | } 21 | return; 22 | } 23 | assert.fail('Expected revert not received'); 24 | }; 25 | -------------------------------------------------------------------------------- /test/helpers/tokenBag.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const { utils } = require('ethers'); 5 | 6 | const { BN } = require('ethereumjs-util'); 7 | 8 | const { 9 | BLOCK_GAS_LIMIT, 10 | ZERO_ADDRESS, 11 | ZERO_HASH, 12 | ERC20TYPE, 13 | ERC1948TYPE, 14 | ERC721TYPE, 15 | } = require('../../utils/constants'); 16 | 17 | const TokenBagHelpers = {}; 18 | 19 | TokenBagHelpers.assertTokenBagEqual = (expected, actual) => { 20 | expected.bag.forEach((expectedOutput, i) => { 21 | const actualOutput = actual.bag[i]; 22 | assert.equal( 23 | expectedOutput.owner.replace('0x', ''), 24 | actualOutput.owner.toLowerCase().replace('0x', '') 25 | ); 26 | assert.equal(expectedOutput.color, actualOutput.color.toLowerCase()); 27 | assert.equal(expectedOutput.data, actualOutput.data); 28 | assert.equal(expectedOutput.tokenType, actualOutput.tokenType); 29 | if (actualOutput.valueOrId.toHexString) { 30 | assert.equal( 31 | expectedOutput.valueOrId, 32 | utils.hexZeroPad(actualOutput.valueOrId.toHexString(), 32) 33 | ); 34 | } else { 35 | assert.equal( 36 | expectedOutput.valueOrId, 37 | actualOutput.valueOrId 38 | ); 39 | } 40 | }); 41 | }; 42 | 43 | TokenBagHelpers.emptyOutput = () => { 44 | return { 45 | owner: ZERO_ADDRESS, 46 | valueOrId: 0x0, 47 | data: ZERO_HASH, 48 | color: ZERO_ADDRESS, 49 | tokenType: ERC20TYPE, 50 | }; 51 | }; 52 | 53 | TokenBagHelpers.emptyTokenBag = () => { 54 | return promote({ bag: Array.apply(null, Array(16)).map(TokenBagHelpers.emptyOutput) }); 55 | }; 56 | 57 | TokenBagHelpers.padTokenBag = (tokenBag) => { 58 | while(tokenBag.length < 16) { 59 | tokenBag.push(TokenBagHelpers.emptyOutput()); 60 | } 61 | return promote({ bag: tokenBag }); 62 | }; 63 | 64 | function promote (tokenBag) { 65 | 66 | const bn = (hex) => { 67 | return new BN(hex.replace('0x', ''), 16); 68 | }; 69 | 70 | function balanceOf (color, address) { 71 | return this.findToken({ 72 | color: color, 73 | owner: address, 74 | }).valueOrId || 0; 75 | } 76 | 77 | function readData (color, tokenId) { 78 | return this.findToken({ 79 | color: color, 80 | valueOrId: tokenId, 81 | }).data || ZERO_HASH; 82 | } 83 | 84 | function transferFrom (color, from, to, valueOrId) { 85 | if (to === ZERO_ADDRESS) return false; 86 | const source = this.findToken({ 87 | color: color, 88 | owner: from, 89 | }); 90 | if (!source) return false; 91 | if (source.tokenType === ERC20TYPE) { 92 | let dest; 93 | dest = this.findToken({ 94 | color: color, 95 | owner: to, 96 | }); 97 | if (!dest) { 98 | dest = this.findToken({ 99 | color: ZERO_ADDRESS, 100 | owner: ZERO_ADDRESS, 101 | }); 102 | } 103 | if (!dest) return false; 104 | if (bn(source.valueOrId).lt(bn(valueOrId))) return false; 105 | if (bn(valueOrId).add(bn(dest.valueOrId)).lt(bn(valueOrId))) return false; 106 | 107 | dest.owner = to; 108 | dest.color = color; 109 | source.valueOrId = '0x' + bn(source.valueOrId).sub(bn(valueOrId)).toString(16, 64); 110 | dest.valueOrId = '0x' + bn(dest.valueOrId).add(bn(valueOrId)).toString(16, 64); 111 | 112 | return true; 113 | } else if (source.tokenType === ERC1948TYPE || source.tokenType === ERC721TYPE) { 114 | source.owner = to; 115 | return true; 116 | } else { 117 | return false; 118 | } 119 | } 120 | 121 | function ownerOf (color, tokenId) { 122 | return this.findToken({ 123 | color: color, 124 | valueOrId: tokenId, 125 | }).owner || ZERO_ADDRESS; 126 | } 127 | 128 | function writeData (color, tokenId, newData) { 129 | const token = this.findToken({ 130 | color: color, 131 | valueOrId: tokenId, 132 | }); 133 | if (token) { 134 | token.data = newData; 135 | } 136 | return true; 137 | } 138 | 139 | function findToken (query) { 140 | const tokens = this.bag.filter(o => { 141 | return Object.entries(query).reduce((acc, [key, value]) => acc && (o[key] === value), true); 142 | }); 143 | return tokens.length === 0 ? false : tokens[0]; 144 | } 145 | 146 | tokenBag.balanceOf = balanceOf; 147 | tokenBag.readData = readData; 148 | tokenBag.ownerOf = ownerOf; 149 | tokenBag.findToken = findToken; 150 | tokenBag.writeData = writeData; 151 | tokenBag.transferFrom = transferFrom; 152 | return tokenBag; 153 | } 154 | 155 | module.exports = TokenBagHelpers; 156 | -------------------------------------------------------------------------------- /test/helpers/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const OP = require('./../../utils/constants'); 4 | // TODO make a util contract 5 | const ethers = require('ethers'); 6 | const { PUSH1 } = OP; 7 | 8 | const Utils = {}; 9 | 10 | Utils.toBN = require('ethers').utils.bigNumberify; 11 | 12 | Utils.toBytes32 = require('ethers').utils.formatBytes32String; 13 | 14 | Utils.toNum = arr => arr.map(e => e.toNumber()); 15 | 16 | Utils.toStr = arr => arr.map(e => e.toString()); 17 | 18 | Utils.toHex = arr => arr.map(e => e.toString(16)); 19 | 20 | Utils.leftPad = (n, width) => { 21 | n = '' + n; 22 | return n.length >= width ? n : new Array(width - n.length + 1).join(0) + n; 23 | }; 24 | 25 | Utils.pushRange = (from, to) => Array.from( 26 | { length: (to - from + 1) * 2 }, 27 | (_, i) => i % 2 === 0 ? PUSH1 : Utils.leftPad(Math.floor((i / 2) + from), 2) 28 | ); 29 | 30 | Utils.range = (from, to) => Array.from({ length: to - from + 1 }, (x, i) => (i + from).toString()); 31 | 32 | Utils.hexRange = (from, to) => Utils.toBN('0x' + Utils.range(from, to).join('')).toString(); 33 | 34 | Utils.opcodeNames = Object.keys(OP).reduce((s, k) => { s[OP[k]] = k; return s; }, {}); 35 | 36 | Utils.getCodeWithStep = (fixture) => { 37 | let code; 38 | if (!fixture.join) { 39 | code = fixture.code || []; 40 | if (!code.join) { // wrap single opcode 41 | code = [code]; 42 | } 43 | } else { 44 | code = fixture; 45 | } 46 | 47 | const codeSize = code.length; 48 | const pc = fixture.pc !== undefined ? fixture.pc : codeSize - 1; 49 | const opcodeUnderTest = Utils.opcodeNames[code[pc]]; 50 | const step = fixture.step !== undefined ? fixture.step : 0; 51 | return { code, step, opcodeUnderTest }; 52 | }; 53 | 54 | Utils.getCode = (fixture) => { 55 | let code; 56 | if (!fixture.join) { 57 | code = fixture.code || []; 58 | if (!code.join) { // wrap single opcode 59 | code = [code]; 60 | } 61 | } else { 62 | code = fixture; 63 | } 64 | 65 | const codeSize = code.length; 66 | const pc = fixture.pc !== undefined ? fixture.pc : codeSize - 1; 67 | const opcodeUnderTest = Utils.opcodeNames[code[pc]]; 68 | return { code, codeSize, pc: ~~pc, opcodeUnderTest }; 69 | }; 70 | 71 | Utils.provider = new ethers.providers.JsonRpcProvider(`http://localhost:${process.env.RPC_PORT}`); 72 | Utils.provider.pollingInterval = 30; 73 | 74 | Utils.client = async () => { 75 | if (Utils.clientName === undefined) { 76 | Utils.clientName = await Utils.provider.send('web3_clientVersion', []); 77 | } 78 | return Utils.clientName; 79 | }; 80 | 81 | Utils.txOverrides = { 82 | gasLimit: 0xfffffffffffff, 83 | gasPrice: 0x01, 84 | }; 85 | 86 | Utils.deployContract = async function (truffleContract, ...args) { 87 | // wait for RPC 88 | while (true) { 89 | try { 90 | await Utils.provider.getBlockNumber(); 91 | break; 92 | } catch (e) { 93 | // ignore 94 | } 95 | await new Promise((resolve) => setTimeout(resolve, 100)); 96 | } 97 | 98 | let _factory = new ethers.ContractFactory( 99 | truffleContract.abi, 100 | truffleContract.bytecode, 101 | Utils.wallets[0] 102 | ); 103 | const contract = await _factory.deploy(...args, Utils.txOverrides); 104 | 105 | await contract.deployed(); 106 | return contract; 107 | }; 108 | 109 | Utils.deployCode = async function (code) { 110 | let codeLen = (code.join('').length / 2).toString(16); 111 | 112 | if (codeLen.length % 2 === 1) { 113 | codeLen = '0' + codeLen; 114 | } 115 | 116 | let codeOffset = (10 + codeLen.length / 2).toString(16); 117 | if (codeOffset.length % 2 === 1) { 118 | codeOffset = '0' + codeOffset; 119 | } 120 | let codeCopy = [ 121 | OP[`PUSH${codeLen.length / 2}`], codeLen, 122 | OP.DUP1, 123 | OP.PUSH1, codeOffset, 124 | OP.PUSH1, '00', 125 | OP.CODECOPY, 126 | OP.PUSH1, '00', 127 | OP.RETURN, 128 | ]; 129 | const obj = { 130 | abi: [], 131 | bytecode: '0x' + codeCopy.join('') + code.join(''), 132 | }; 133 | 134 | return Utils.deployContract(obj); 135 | }; 136 | 137 | Utils.wallets = []; 138 | 139 | for (var i = 0; i < 10; i++) { 140 | const privateKey = '0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b750120' + i; 141 | const wallet = new ethers.Wallet(privateKey, Utils.provider); 142 | Utils.wallets.push(wallet); 143 | } 144 | 145 | Utils.onchainWait = async function onchainWait (t) { 146 | for (let i = 0; i < t; i++) { 147 | let tx = await Utils.wallets[0].sendTransaction({ to: Utils.wallets[0].address }); 148 | tx = await tx.wait(); 149 | } 150 | }; 151 | 152 | Utils.sleep = async function sleep (seconds) { 153 | seconds = seconds + 1; 154 | await new Promise((resolve) => setTimeout(resolve, seconds * 1000)); 155 | }; 156 | 157 | module.exports = Utils; 158 | -------------------------------------------------------------------------------- /test/utils/FragmentTree.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const { randomFillSync } = require('crypto'); 5 | 6 | const { FragmentTree } = require('./../../utils'); 7 | 8 | function randomString (size) { 9 | return `0x${randomFillSync(Buffer.alloc(size)).toString('hex')}`; 10 | } 11 | 12 | describe('FragmentTree', function () { 13 | for (let i = 1; i < 1024; i++) { 14 | it(`Loop #${i}`, () => { 15 | const bytecode = randomString(i); 16 | const byteLength = (bytecode.length - 2) / 2; 17 | const tree = new FragmentTree().run(bytecode); 18 | const slots = ~~((i + 31) / 32); 19 | 20 | for (let x = 0; x < slots; x++) { 21 | const leaf = tree.leaves[x]; 22 | const startOffset = (x * 64) + 2; 23 | const value = bytecode.substring(startOffset, startOffset + 64).padEnd(64, '0'); 24 | 25 | assert.equal(leaf.slot, x, 'slot should match'); 26 | assert.equal(leaf.byteLength, byteLength, 'byteLength should match'); 27 | assert.equal(leaf.value, `0x${value}`, 'value should match'); 28 | } 29 | 30 | assert.equal(tree.leaves.length, slots + slots % 2, 'number of leaves should match'); 31 | 32 | const proof = tree.calculateProof(slots - 1); 33 | assert.ok(tree.verifyProof(tree.leaves[slots - 1], proof), 'verifyProof'); 34 | }); 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /test/utils/PrimeTester.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const { HydratedRuntime, Constants } = require('./../../utils/'); 5 | 6 | /* eslint-disable max-len */ 7 | // the really long-running task 8 | const data = 9 | '0x686109bb0000000000000000000000000000000000003ffffffffffffeffffffffffffff0000000000000000000000000000000000000000000000000000000000003039'; 10 | 11 | //const data = 12 | // '0x686109bb00000000000000000003c06a5bdb1dd7dcf5ca96b974ab2b4ae19f9972ca019d000000000000000000000000000000000000000000000000000000000000007b'; 13 | 14 | // PrimeTester contract 15 | const code = '0x608060405234801561001057600080fd5b50600436106100625760003560e01c80630c78932d1461006757806327e916c31461009357806333f1600a1461009b578063686109bb146100a35780638be1baee146100c6578063df5c127e146100ce575b600080fd5b61006f6100d6565b6040518082600481111561007f57fe5b60ff16815260200191505060405180910390f35b61006f6100db565b61006f6100e0565b61006f600480360360408110156100b957600080fd5b50803590602001356100e5565b61006f6101cf565b61006f6101d4565b600281565b600081565b600381565b600060288315806100f65750836001145b806101015750836004145b156101105760009150506101c9565b836002148061011f5750836003145b1561012e5760049150506101c9565b60001984015b6001811615156101465760011c610134565b6000610151856101d9565b905060005b838110156101c05760008061016c858a86610202565b909250905080151561018757600296505050505050506101c9565b81151561019d57600096505050505050506101c9565b50506158f991909102600781901c18620118c702600b81901c1890600101610156565b50600393505050505b92915050565b600481565b600181565b604080516020808201849052825180830382018152918301909252805191012060001c5b919050565b60008060018086161461021157fe5b846001850381151561021f57fe5b061561022757fe5b6000856001860381151561023757fe5b0490506102438161037d565b151561024b57fe5b600181101561025657fe5b600485101561026c576000809250925050610375565b6000600486038581151561027c57fe5b066002019050600061028f82898961039c565b945090508315156102a95760008094509450505050610375565b80600114806102ba57506001870381145b156102ce5760018094509450505050610375565b60001987018814610369576102e38182610454565b945090508315156102fd5760008094509450505050610375565b868181151561030857fe5b069050610316886002610454565b90985093508315156103315760008094509450505050610375565b806001141561034a576000600194509450505050610375565b600187038114156103645760018094509450505050610375565b6102ce565b60006001945094505050505b935093915050565b600081151561038e575060006101fd565b5060001981018116156101fd565b6000808215156103b157506000905080610375565b600083868115156103be57fe5b069550600192505b600085111561044757600185161561040a576103e28387610454565b90935090508015156103fb575060009150819050610375565b838381151561040657fe5b0692505b6104148687610454565b909650905080151561042d575060009150819050610375565b838681151561043857fe5b069550600185901c94506103c6565b6001915050935093915050565b60008083151561046a5750600090506001610492565b83830283858281151561047957fe5b04141561048b57915060019050610492565b9150600090505b925092905056fea165627a7a72305820cf95159c84ed2253d37b8acf6ea2b6a17f8f0117b0fa754ffeec62324898a8940029'; 16 | /* eslint-enable max-len */ 17 | 18 | describe('PrimeTester', function () { 19 | this.timeout(180000); 20 | 21 | it('should not go out of memory', async () => { 22 | const stepper = new HydratedRuntime(); 23 | const steps = await stepper.run({ code, data }); 24 | const res = steps[steps.length - 1]; 25 | 26 | console.log({ executionSteps: steps.length }); 27 | assert.equal(Constants.BLOCK_GAS_LIMIT - res.gasRemaining, 2458780); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/utils/stepper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { getCode } = require('./../helpers/utils'); 4 | const { Constants, HydratedRuntime } = require('./../../utils/'); 5 | const fixtures = require('./../fixtures/runtime'); 6 | const { assertTokenBagEqual } = require('../helpers/tokenBag.js'); 7 | 8 | const assert = require('assert'); 9 | 10 | describe('JS Stepper', function () { 11 | describe('fixtures', async () => { 12 | fixtures.forEach(fixture => { 13 | const { pc, opcodeUnderTest } = getCode(fixture); 14 | 15 | it(fixture.description || opcodeUnderTest, async () => { 16 | const stepper = new HydratedRuntime(); 17 | const code = typeof fixture.code === 'object' ? fixture.code : [fixture.code]; 18 | const stack = fixture.stack || []; 19 | const mem = fixture.memory || ''; 20 | const data = fixture.data || ''; 21 | const tokenBag = fixture.tokenBag || false; 22 | const gasLimit = fixture.gasLimit || Constants.BLOCK_GAS_LIMIT; 23 | const blockGasLimit = fixture.gasLimit || Constants.BLOCK_GAS_LIMIT; 24 | const gasRemaining = typeof fixture.gasRemaining !== 'undefined' ? fixture.gasRemaining : gasLimit; 25 | const args = { 26 | code, 27 | data, 28 | stack, 29 | mem, 30 | gasLimit, 31 | blockGasLimit, 32 | gasRemaining, 33 | pc, 34 | tokenBag, 35 | }; 36 | const steps = await stepper.run(args); 37 | const res = steps[steps.length - 1]; 38 | 39 | if (fixture.result.stack) { 40 | assert.deepEqual(res.stack, fixture.result.stack, 'stack'); 41 | } 42 | if (fixture.result.memory) { 43 | assert.deepEqual(res.mem, fixture.result.memory, 'mem'); 44 | } 45 | if (fixture.result.tokenBag) { 46 | assertTokenBagEqual(res.tokenBag, fixture.result.tokenBag); 47 | } 48 | if (fixture.result.gasUsed !== undefined) { 49 | assert.equal(gasRemaining - res.gasRemaining, fixture.result.gasUsed, 'gasUsed'); 50 | } 51 | if (fixture.result.pc !== undefined) { 52 | assert.equal(res.pc, fixture.result.pc, 'pc'); 53 | } 54 | if (fixture.result.errno !== undefined) { 55 | assert.equal(res.errno, fixture.result.errno, 'errno'); 56 | } 57 | }); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /tools/deploy.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const ethers = require('ethers'); 6 | 7 | const Verifier = require('../build/contracts/Verifier.json'); 8 | const Enforcer = require('../build/contracts/Enforcer.json'); 9 | 10 | async function deployContract (wallet, artifact, ...args) { 11 | const _factory = new ethers.ContractFactory( 12 | artifact.abi, 13 | artifact.bytecode, 14 | wallet 15 | ); 16 | const contract = await _factory.deploy(...args); 17 | const tx = await contract.deployTransaction.wait(); 18 | 19 | console.log(`\n 20 | Contract: ${artifact.contractName} 21 | Address: ${contract.address} 22 | Transaction Hash: ${tx.transactionHash} 23 | Deployer: ${tx.from} 24 | Gas used: ${tx.cumulativeGasUsed.toString()} 25 | Gas fee in Ether: ${ethers.utils.formatUnits(contract.deployTransaction.gasPrice.mul(tx.cumulativeGasUsed), 'ether')} 26 | `); 27 | 28 | return contract; 29 | }; 30 | 31 | /// Additionally to the deployment variables 32 | /// you also have to provide the network (the network name in truffle-config). 33 | /// 34 | /// If the RPC provider does not support signing, 35 | /// you have to supply either `privKey|PRIV_KEY` or `mnemonic|MNEMONIC` 36 | /// as environment variables to this script. 37 | (async function () { 38 | if (!process.env.network) { 39 | console.error('Please pass `network=` to this script.'); 40 | process.exit(1); 41 | } 42 | 43 | const truffleConfig = require('../truffle-config'); 44 | const network = truffleConfig.networks[process.env.network]; 45 | const mnemonic = process.env.mnemonic || process.env.MNEMONIC; 46 | const privKey = process.env.privKey || process.env.PRIV_KEY; 47 | const provider = new ethers.providers.JsonRpcProvider(network.url); 48 | const txOverrides = { 49 | gasLimit: network.gas, 50 | gasPrice: network.gasPrice, 51 | }; 52 | 53 | let wallet; 54 | 55 | if (privKey) { 56 | wallet = new ethers.Wallet(privKey, provider); 57 | } else if (mnemonic) { 58 | wallet = ethers.Wallet.fromMnemonic(mnemonic).connect(provider); 59 | } else { 60 | wallet = provider.getSigner(); 61 | } 62 | 63 | const deployVars = { 64 | verifierTimeout: process.env.verifierTimeout, 65 | taskPeriod: process.env.taskPeriod, 66 | challengePeriod: process.env.challengePeriod, 67 | bondAmount: process.env.bondAmount, 68 | maxExecutionDepth: process.env.maxExecutionDepth, 69 | }; 70 | 71 | console.log('Deployment variables:'); 72 | for (let key in deployVars) { 73 | if (!deployVars[key]) { 74 | console.error(`${key} not defined via environment`); 75 | process.exit(1); 76 | } 77 | console.log(` ${key} = ${deployVars[key]}`); 78 | } 79 | 80 | const verifier = await deployContract( 81 | wallet, 82 | Verifier, 83 | deployVars.verifierTimeout, 84 | txOverrides 85 | ); 86 | const enforcer = await deployContract( 87 | wallet, 88 | Enforcer, 89 | verifier.address, 90 | deployVars.taskPeriod, 91 | deployVars.challengePeriod, 92 | deployVars.bondAmount, 93 | deployVars.maxExecutionDepth, 94 | txOverrides 95 | ); 96 | 97 | console.log('verifier.setEnforcer', enforcer.address); 98 | await (await verifier.setEnforcer(enforcer.address, txOverrides)).wait(); 99 | })(); 100 | -------------------------------------------------------------------------------- /tools/executionPoker.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | const { ExecutionPoker, Merkelizer } = require('./../utils'); 5 | 6 | const fs = require('fs'); 7 | const ethers = require('ethers'); 8 | const ganache = require('ganache-cli'); 9 | 10 | const GAS_LIMIT = 0xfffffffffffff; 11 | const EVMParameters = { 12 | origin: '0xa1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1', 13 | target: '0xfeefeefeefeefeefeefeefeefeefeefeefeefee0', 14 | blockHash: '0xdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdc', 15 | blockNumber: 123, 16 | time: 1560775755, 17 | txGasLimit: 0xffffffffff, 18 | customEnvironmentHash: '0x0000000000000000000000000000000000000000000000000000000000000000', 19 | codeHash: '0x0000000000000000000000000000000000000000000000000000000000000000', 20 | dataHash: '0x0000000000000000000000000000000000000000000000000000000000000000', 21 | }; 22 | 23 | class MyExecutionPoker extends ExecutionPoker { 24 | onSlashed (execId) { 25 | this.log(`got slashed, executionId(${execId})`); 26 | // we are done 27 | process.exit(0); 28 | } 29 | 30 | async requestExecution (contractAddr, callData) { 31 | const codeHash = `0x${contractAddr.replace('0x', '').toLowerCase().padEnd(64, '0')}`; 32 | const dataHash = Merkelizer.dataHash(callData); 33 | const evmParams = Object.assign(EVMParameters, { codeHash, dataHash }); 34 | 35 | return super.requestExecution(evmParams, callData); 36 | } 37 | 38 | async submitProof (disputeId, computationPath) { 39 | try { 40 | await super.submitProof(disputeId, computationPath); 41 | } catch (e) { 42 | // ignore for unit test 43 | } 44 | } 45 | 46 | async computeCall (evmParams) { 47 | const res = await super.computeCall(evmParams); 48 | 49 | if (this.logTag === 'solver') { 50 | this.log('making one leaf invalid'); 51 | 52 | const leaf = res.merkle.leaves[0]; 53 | leaf.right.executionState.gasRemaining = 2222; 54 | leaf.right.hash = Merkelizer.stateHash( 55 | leaf.right.executionState, 56 | leaf.right.stackHash, 57 | leaf.right.memHash, 58 | evmParams.dataHash, 59 | evmParams.customEnvironmentHash 60 | ); 61 | leaf.hash = Merkelizer.hash(leaf.left.hash, leaf.right.hash); 62 | res.merkle.recal(0); 63 | } 64 | 65 | return res; 66 | } 67 | } 68 | 69 | async function deployContract (truffleContract, wallet, ...args) { 70 | const _factory = new ethers.ContractFactory( 71 | truffleContract.abi, 72 | truffleContract.bytecode, 73 | wallet 74 | ); 75 | const contract = await _factory.deploy(...args, { gasLimit: GAS_LIMIT }); 76 | 77 | await contract.deployed(); 78 | 79 | return contract; 80 | } 81 | 82 | async function main () { 83 | let Verifier; 84 | let Enforcer; 85 | 86 | try { 87 | Verifier = require('./../build/contracts/Verifier.json'); 88 | Enforcer = require('./../build/contracts/Enforcer.json'); 89 | } catch (e) { 90 | console.error('Please run `npm run compile:contracts` first. 😉'); 91 | return; 92 | } 93 | 94 | if (process.argv.length < 3) { 95 | console.log( 96 | 'Usage: pathToContract.json functionName functionArgs\n' + 97 | 'Example:' + 98 | '\n\tbuild/contracts/SpendingConditionMock.json test 0xD8992E08C1Fb17775deF02e15B620A006c4726db [] []' 99 | ); 100 | 101 | process.exit(0); 102 | } 103 | 104 | let deployerWallet = ethers.Wallet.createRandom(); 105 | let solverWallet = ethers.Wallet.createRandom(); 106 | let challengerWallet = ethers.Wallet.createRandom(); 107 | 108 | const accounts = [ 109 | { secretKey: deployerWallet.privateKey, balance: ethers.constants.MaxUint256 }, 110 | { secretKey: solverWallet.privateKey, balance: ethers.constants.MaxUint256 }, 111 | { secretKey: challengerWallet.privateKey, balance: ethers.constants.MaxUint256 }, 112 | ]; 113 | 114 | const provider = ganache.provider({ locked: false, accounts: accounts, gasLimit: GAS_LIMIT }); 115 | global.provider = provider; 116 | 117 | // yes, we need a new Web3Provider. Otherwise we get duplicate events -.- 118 | deployerWallet = deployerWallet.connect(new ethers.providers.Web3Provider(provider)); 119 | solverWallet = solverWallet.connect(new ethers.providers.Web3Provider(provider)); 120 | challengerWallet = challengerWallet.connect(new ethers.providers.Web3Provider(provider)); 121 | 122 | // faster unit tests :) 123 | solverWallet.provider.pollingInterval = 30; 124 | challengerWallet.provider.pollingInterval = 30; 125 | 126 | const timeout = 10; 127 | const taskPeriod = 100000; 128 | const challengePeriod = 10000; 129 | const bondAmount = 1; 130 | const maxExecutionDepth = 10; 131 | 132 | console.log( 133 | `Deploying Verifier & Enforcer\n\ 134 | \tTimeout: ${timeout}\n\tChallengePeriod: ${challengePeriod}\n\tBond amount: ${bondAmount}` 135 | ); 136 | 137 | const verifier = await deployContract(Verifier, deployerWallet, timeout); 138 | const enforcer = await deployContract( 139 | Enforcer, 140 | deployerWallet, 141 | verifier.address, 142 | taskPeriod, 143 | challengePeriod, 144 | bondAmount, 145 | maxExecutionDepth 146 | ); 147 | 148 | let tx = await verifier.setEnforcer(enforcer.address); 149 | 150 | await tx.wait(); 151 | 152 | console.log(`Verifier contract: ${verifier.address}`); 153 | console.log(`Enforcer contract: ${enforcer.address}`); 154 | 155 | console.log(`Solver wallet: ${solverWallet.address}`); 156 | console.log(`Challenger wallet: ${challengerWallet.address}`); 157 | 158 | // challenger 159 | // eslint-disable-next-line no-new 160 | new MyExecutionPoker( 161 | enforcer, 162 | verifier, 163 | challengerWallet, 164 | GAS_LIMIT, 165 | 'challenger' 166 | ); 167 | 168 | fs.realpath(process.argv[2], async function (err, path) { 169 | if (err) { 170 | onException(err); 171 | return; 172 | } 173 | 174 | const args = process.argv.slice(3, process.argv.length); 175 | const functionName = args.shift(); 176 | const functionArgs = []; 177 | 178 | while (args.length) { 179 | const e = preParse(args.shift()); 180 | functionArgs.push(JSON.parse(e)); 181 | } 182 | 183 | const contr = require(path); 184 | const target = await deployContract(contr, solverWallet); 185 | 186 | if (!target.interface.functions[functionName]) { 187 | console.log('available functions'); 188 | console.log(target.functions); 189 | process.exit(0); 190 | } 191 | 192 | console.log('function arguments for', functionName, '\n', functionArgs); 193 | 194 | const data = target.interface.functions[functionName].encode(functionArgs); 195 | 196 | console.log('callData', data); 197 | 198 | const execPoker = new MyExecutionPoker( 199 | enforcer, 200 | verifier, 201 | solverWallet, 202 | GAS_LIMIT, 203 | 'solver' 204 | ); 205 | // will kick solver and later the challenger :) 206 | execPoker.requestExecution(target.address, data); 207 | }); 208 | } 209 | 210 | function preParse (str) { 211 | const len = str.length; 212 | let res = ''; 213 | let openToken = false; 214 | 215 | for (let i = 0; i < len; i++) { 216 | let v = str[i]; 217 | 218 | if (openToken && (v === ',' || v === ']' || v === ' ')) { 219 | res += '"'; 220 | openToken = false; 221 | } 222 | 223 | if (v === '0' && str[i + 1] === 'x') { 224 | res += '"'; 225 | openToken = true; 226 | } 227 | 228 | res += v; 229 | } 230 | 231 | if (openToken) { 232 | res += '"'; 233 | } 234 | 235 | return res; 236 | } 237 | 238 | function onException (e) { 239 | console.error(e); 240 | process.exit(1); 241 | } 242 | 243 | process.on('uncaughtException', onException); 244 | process.on('unhandledRejection', onException); 245 | 246 | main(); 247 | -------------------------------------------------------------------------------- /tools/verifyDeployment.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const provider = require('ethers').getDefaultProvider(process.env.network); 6 | 7 | (async function () { 8 | const verifierCode = await provider.getCode(process.env.verifier); 9 | const enforcerCode = await provider.getCode(process.env.enforcer); 10 | 11 | const verifierRefCode = require('./../build/contracts/Verifier.json').deployedBytecode; 12 | const enforcerRefCode = require('./../build/contracts/Enforcer.json').deployedBytecode; 13 | 14 | console.log('Verifier bytecode is the same:', verifierRefCode === verifierCode); 15 | console.log('Enforcer bytecode is the same:', enforcerRefCode === enforcerCode); 16 | })(); 17 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | compilers: { 5 | solc: { 6 | version: '0.5.2', 7 | settings: { 8 | optimizer: { 9 | enabled: true, 10 | runs: 2, 11 | }, 12 | evmVersion: 'constantinople', 13 | }, 14 | }, 15 | }, 16 | networks: { 17 | goerli: { 18 | url: 'https://goerli.infura.io/v3/' + process.env.INFURA_API_KEY, 19 | network_id: '5', // eslint-disable-line camelcase 20 | gas: 6465030, 21 | gasPrice: 10000000000, 22 | }, 23 | ganache: { 24 | url: 'http://localhost:8111', 25 | network_id: '*', // eslint-disable-line camelcase 26 | gas: 7000000, 27 | gasPrice: 1, 28 | }, 29 | geth: { 30 | url: 'http://localhost:8222', 31 | network_id: '*', // eslint-disable-line camelcase 32 | gas: 7000000, 33 | gasPrice: 1, 34 | }, 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /utils/AbstractMerkleTree.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ethers = require('ethers'); 4 | const { ZERO_HASH } = require('./constants'); 5 | 6 | module.exports = class AbstractMerkleTree { 7 | static hash (left, right) { 8 | return ethers.utils.solidityKeccak256( 9 | ['bytes32', 'bytes32'], 10 | [left, right] 11 | ); 12 | } 13 | 14 | static zero () { 15 | return { 16 | left: { 17 | hash: ZERO_HASH, 18 | }, 19 | right: { 20 | hash: ZERO_HASH, 21 | }, 22 | hash: ZERO_HASH, 23 | }; 24 | } 25 | 26 | constructor () { 27 | this.tree = [[]]; 28 | } 29 | 30 | get leaves () { 31 | return this.tree[0]; 32 | } 33 | 34 | get root () { 35 | return this.tree[this.tree.length - 1][0]; 36 | } 37 | 38 | get depth () { 39 | // we also count leaves 40 | return this.tree.length; 41 | } 42 | 43 | getNode (hash) { 44 | let len = this.tree.length; 45 | 46 | while (len--) { 47 | let x = this.tree[len]; 48 | 49 | let iLen = x.length; 50 | while (iLen--) { 51 | let y = x[iLen]; 52 | if (y.hash === hash) { 53 | return y; 54 | } 55 | } 56 | } 57 | 58 | return null; 59 | } 60 | 61 | getPair (leftHash, rightHash) { 62 | let len = this.tree.length; 63 | 64 | while (len--) { 65 | let x = this.tree[len]; 66 | 67 | let iLen = x.length; 68 | while (iLen--) { 69 | let y = x[iLen]; 70 | if (y.left.hash === leftHash && y.right.hash === rightHash) { 71 | return y; 72 | } 73 | } 74 | } 75 | 76 | return null; 77 | } 78 | 79 | recal (baseLevel) { 80 | if (baseLevel === undefined) { 81 | baseLevel = 0; 82 | } 83 | let level = baseLevel + 1; 84 | // clear everything from level and above 85 | this.tree = this.tree.slice(0, level); 86 | while (true) { 87 | let last = this.tree[level - 1]; 88 | let cur = []; 89 | 90 | if (last.length <= 1 && level > 1) { 91 | // done 92 | break; 93 | } 94 | 95 | let len = last.length; 96 | for (let i = 0; i < len; i += 2) { 97 | let left = last[i]; 98 | let right = last[i + 1]; 99 | 100 | if (!right) { 101 | right = this.constructor.zero(); 102 | last.push(right); 103 | } 104 | 105 | cur.push( 106 | { 107 | left: left, 108 | right: right, 109 | hash: this.constructor.hash(left.hash, right.hash), 110 | } 111 | ); 112 | } 113 | 114 | this.tree.push(cur); 115 | level++; 116 | } 117 | } 118 | 119 | printTree () { 120 | let res = ''; 121 | 122 | for (let i = 0; i < this.tree.length; i++) { 123 | const row = this.tree[i]; 124 | 125 | res += `level ${i}: `; 126 | for (let y = 0; y < row.length; y++) { 127 | const e = row[y]; 128 | const h = e.hash.substring(2, 6); 129 | const hl = e.left ? e.left.hash.substring(2, 6) : '?'; 130 | const hr = e.right ? e.right.hash.substring(2, 6) : '?'; 131 | 132 | res += ` [ ${h} (l:${hl} r:${hr}) ] `; 133 | } 134 | 135 | res += '\n'; 136 | } 137 | 138 | return res; 139 | } 140 | }; 141 | -------------------------------------------------------------------------------- /utils/EthereumRuntimeAdapter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { BLOCK_GAS_LIMIT, ZERO_ADDRESS, ZERO_HASH } = require('./constants'); 4 | const ethers = require('ethers'); 5 | const { emptyTokenBag } = require('../test/helpers/tokenBag.js'); 6 | 7 | 8 | module.exports = class EthereumRuntimeAdapter { 9 | constructor (runtimeContract) { 10 | // explicit mark it as view, so we can just call the execute function 11 | // TODO: ethers.js should provide the option to explicitly call a function 12 | // https://github.com/ethers-io/ethers.js/issues/395 13 | 14 | // Need to copy (read-only) 15 | let abi = JSON.parse(JSON.stringify(runtimeContract.interface.abi)); 16 | 17 | abi[0].constant = true; 18 | abi[0].stateMutability = 'view'; 19 | 20 | this.runtimeContract = new ethers.Contract( 21 | runtimeContract.address, 22 | abi, 23 | runtimeContract.provider 24 | ); 25 | // this is used to derive the gas usage (payable) 26 | this.payableRuntimeContract = runtimeContract; 27 | } 28 | 29 | execute ( 30 | { code, data, pc, stepCount, gasRemaining, gasLimit, stack, mem, tokenBag }, 31 | payable 32 | ) { 33 | return (payable ? this.payableRuntimeContract.execute : this.runtimeContract.execute)( 34 | { 35 | code: code || '0x', 36 | data: data || '0x', 37 | pc: pc | 0, 38 | errno: 0, 39 | stepCount: stepCount | 0, 40 | gasRemaining: gasRemaining || gasLimit || BLOCK_GAS_LIMIT, 41 | gasLimit: gasLimit || BLOCK_GAS_LIMIT, 42 | stack: stack || [], 43 | mem: mem || [], 44 | tokenBag: tokenBag || emptyTokenBag(), 45 | returnData: '0x', 46 | } 47 | ); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /utils/ExecutionPoker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ethers = require('ethers'); 4 | 5 | const HydratedRuntime = require('./HydratedRuntime.js'); 6 | const Merkelizer = require('./Merkelizer.js'); 7 | const ProofHelper = require('./ProofHelper.js'); 8 | const FragmentTree = require('./FragmentTree'); 9 | const { ZERO_HASH } = require('./constants.js'); 10 | 11 | module.exports = class ExecutionPoker { 12 | constructor (enforcer, verifier, wallet, gasLimit = 0xfffffffffffff, logTag = 'unkn') { 13 | this.enforcer = enforcer.connect(wallet); 14 | this.verifier = verifier.connect(wallet); 15 | this.wallet = wallet; 16 | this.gasLimit = gasLimit; 17 | this.logTag = logTag; 18 | // TODO: this needs to be garbage collected 19 | this.taskParams = {}; 20 | this.taskCallData = {}; 21 | this.disputes = {}; 22 | this.solutions = {}; 23 | 24 | this.enforcer.on( 25 | this.enforcer.filters.Requested(), 26 | async (taskHash, parameters, callData, tx) => { 27 | const params = { 28 | origin: parameters[0], 29 | target: parameters[1], 30 | blockHash: parameters[2], 31 | blockNumber: parameters[3], 32 | time: parameters[4], 33 | txGasLimit: parameters[5], 34 | customEnvironmentHash: parameters[6], 35 | codeHash: parameters[7], 36 | dataHash: parameters[8], 37 | }; 38 | this.taskParams[taskHash] = params; 39 | this.taskCallData[params.dataHash] = callData; 40 | 41 | const receipt = await tx.getTransactionReceipt(); 42 | 43 | if (receipt.from === this.wallet.address) { 44 | this.log('task request', { taskHash, params }); 45 | this.registerExecution(taskHash, params); 46 | } else { 47 | this.log('task requested', { taskHash, params }); 48 | } 49 | } 50 | ); 51 | 52 | this.enforcer.on( 53 | this.enforcer.filters.Registered(), 54 | async (taskHash, solverPathRoot, executionDepth, resultBytes, tx) => { 55 | const receipt = await tx.getTransactionReceipt(); 56 | 57 | if (receipt.from === this.wallet.address) { 58 | this.log('execution result registered', taskHash); 59 | } else { 60 | this.validateExecution(taskHash, solverPathRoot, executionDepth, resultBytes); 61 | } 62 | } 63 | ); 64 | 65 | this.enforcer.on( 66 | this.enforcer.filters.Slashed(), 67 | (execId, addr, tx) => { 68 | if (addr === this.wallet.address) { 69 | this.onSlashed(execId); 70 | } 71 | } 72 | ); 73 | 74 | this.enforcer.on( 75 | this.enforcer.filters.DisputeInitialised(), 76 | (disputeId, execId, tx) => { 77 | let sol = this.solutions[execId]; 78 | 79 | if (sol) { 80 | this.log('new dispute for', execId); 81 | this.initDispute(disputeId, sol); 82 | } 83 | } 84 | ); 85 | 86 | this.verifier.on( 87 | this.verifier.filters.DisputeNewRound(), 88 | (disputeId, timeout, solverPath, challengerPath, tx) => { 89 | let o = this.disputes[disputeId]; 90 | 91 | if (o) { 92 | this.log(`dispute(${disputeId}) new round`); 93 | this.submitRound(disputeId); 94 | } 95 | } 96 | ); 97 | } 98 | 99 | onSlashed (execId) { 100 | } 101 | 102 | log (...args) { 103 | console.log(this.logTag, ':', ...args); 104 | } 105 | 106 | async requestExecution (evmParameter, callData) { 107 | let tx = await this.enforcer.request(evmParameter, callData); 108 | 109 | tx = await tx.wait(); 110 | 111 | const taskHash = tx.events[0].args.taskHash; 112 | 113 | return { taskHash, evmParameter }; 114 | } 115 | 116 | async registerExecution (taskHash, evmParams) { 117 | const res = await this.computeCall(evmParams); 118 | const { resultProof, returnData } = res.merkle.computeResultProof(); 119 | const bondAmount = await this.enforcer.bondAmount(); 120 | 121 | this.log('registering execution:', res.steps.length, 'steps'); 122 | 123 | // paranoia, better safe than sorry 124 | if (!res.merkle.verifyResultProof(resultProof, returnData, res.merkle.root.hash)) { 125 | throw new Error('Computed resultProof is invalid. Please file a bug report.'); 126 | } 127 | 128 | let tx = await this.enforcer.register( 129 | taskHash, 130 | res.merkle.root.hash, 131 | resultProof, 132 | returnData, 133 | { value: bondAmount } 134 | ); 135 | 136 | tx = await tx.wait(); 137 | 138 | const evt = tx.events[0].args; 139 | const executionId = ethers.utils.solidityKeccak256( 140 | ['bytes32', 'bytes32'], 141 | [taskHash, evt.solverPathRoot] 142 | ); 143 | 144 | this.solutions[executionId] = res; 145 | } 146 | 147 | async validateExecution (taskHash, solverHash, executionDepth, resultBytes) { 148 | const executionId = ethers.utils.solidityKeccak256( 149 | ['bytes32', 'bytes32'], 150 | [taskHash, solverHash] 151 | ); 152 | this.log('validating execution result', executionId); 153 | 154 | // TODO: MerkleTree resizing 155 | // check execution length and resize tree if necessary 156 | const taskParams = this.taskParams[taskHash]; 157 | const res = await this.computeCall(taskParams); 158 | 159 | // TODO: handle the bigger case too 160 | if (executionDepth < res.merkle.depth) { 161 | // scale down 162 | res.merkle.tree[0] = res.merkle.tree[0].slice(0, 2 ** (executionDepth.toNumber() - 1)); 163 | // recalculate tree 164 | res.merkle.recal(); 165 | } 166 | 167 | const challengerHash = res.merkle.root.hash; 168 | 169 | this.log('solverHash', solverHash); 170 | this.log('challengerHash', challengerHash); 171 | 172 | if (solverHash !== challengerHash || this.alwaysChallenge) { 173 | const bondAmount = await this.enforcer.bondAmount(); 174 | 175 | let tx = await this.enforcer.dispute( 176 | solverHash, 177 | challengerHash, 178 | taskParams, 179 | { value: bondAmount, gasLimit: this.gasLimit } 180 | ); 181 | 182 | tx = await tx.wait(); 183 | 184 | let disputeId = tx.events[0].topics[1]; 185 | 186 | this.initDispute(disputeId, res); 187 | return; 188 | } 189 | 190 | this.log('same execution result'); 191 | } 192 | 193 | initDispute (disputeId, res) { 194 | this.log('initDispute', disputeId); 195 | 196 | let obj = { 197 | merkle: res.merkle, 198 | depth: res.merkle.depth, 199 | computationPath: res.merkle.root, 200 | codeFragmentTree: res.codeFragmentTree, 201 | }; 202 | 203 | this.disputes[disputeId] = obj; 204 | 205 | this.submitRound(disputeId); 206 | } 207 | 208 | async submitRound (disputeId) { 209 | const obj = this.disputes[disputeId]; 210 | 211 | if (obj.computationPath.isLeaf) { 212 | this.log('reached leaves'); 213 | this.log('submitting for l=' + 214 | obj.computationPath.left.hash + ' r=' + obj.computationPath.right.hash); 215 | 216 | await this.submitProof(disputeId, obj); 217 | return; 218 | } 219 | 220 | const dispute = await this.verifier.disputes(disputeId); 221 | const targetPath = this.wallet.address === dispute.challengerAddr ? dispute.challengerPath : dispute.solverPath; 222 | const path = this.wallet.address === dispute.challengerAddr ? dispute.challenger : dispute.solver; 223 | const nextPath = obj.merkle.getNode(targetPath); 224 | 225 | if (!nextPath) { 226 | this.log('submission already made by another party'); 227 | obj.computationPath = obj.merkle.getPair(path.left, path.right); 228 | return; 229 | } 230 | 231 | if (obj.computationPath.left.hash === targetPath) { 232 | this.log('goes left from ' + 233 | obj.computationPath.hash.substring(2, 6) + ' to ' + 234 | obj.computationPath.left.hash.substring(2, 6) 235 | ); 236 | } else if (obj.computationPath.right.hash === targetPath) { 237 | this.log('goes right from ' + 238 | obj.computationPath.hash.substring(2, 6) + ' to ' + 239 | obj.computationPath.right.hash.substring(2, 6) 240 | ); 241 | } 242 | 243 | obj.computationPath = nextPath; 244 | 245 | let witnessPath; 246 | 247 | if (dispute.witness !== ZERO_HASH) { 248 | const path = obj.merkle.getNode(dispute.witness); 249 | 250 | witnessPath = { left: path.left.hash, right: path.right.hash }; 251 | } else { 252 | witnessPath = { left: ZERO_HASH, right: ZERO_HASH }; 253 | } 254 | 255 | let tx = await this.verifier.respond( 256 | disputeId, 257 | { 258 | left: obj.computationPath.left.hash, 259 | right: obj.computationPath.right.hash, 260 | }, 261 | witnessPath, 262 | { gasLimit: this.gasLimit } 263 | ); 264 | 265 | tx = await tx.wait(); 266 | 267 | this.log('gas used', tx.gasUsed.toString(), tx.hash); 268 | } 269 | 270 | async submitProof (disputeId, disputeObj) { 271 | const args = ProofHelper.constructProof(disputeObj.computationPath, disputeObj); 272 | 273 | this.log('submitting proof - proofs', args.proofs); 274 | this.log('submitting proof - executionState', args.executionInput); 275 | 276 | let tx = await this.verifier.submitProof( 277 | disputeId, 278 | args.proofs, 279 | args.executionInput, 280 | { gasLimit: this.gasLimit } 281 | ); 282 | 283 | tx = await tx.wait(); 284 | 285 | this.log('submitting proof - gas used', tx.gasUsed.toString()); 286 | 287 | return tx; 288 | } 289 | 290 | async computeCall (evmParams) { 291 | let bytecode = await this.getCodeForParams(evmParams); 292 | let data = await this.getDataForParams(evmParams); 293 | let code = []; 294 | let len = bytecode.length; 295 | 296 | for (let i = 2; i < len;) { 297 | code.push(bytecode.substring(i, i += 2)); 298 | } 299 | 300 | let codeFragmentTree; 301 | // code is not on chain-🍕 302 | if (!evmParams.codeHash.endsWith('000000000000000000000000')) { 303 | codeFragmentTree = new FragmentTree().run(bytecode); 304 | } 305 | 306 | const runtime = new HydratedRuntime(); 307 | const steps = await runtime.run({ code, data }); 308 | const merkle = new Merkelizer().run(steps, bytecode, data, evmParams.customEnvironmentHash); 309 | 310 | return { steps, merkle, codeFragmentTree }; 311 | } 312 | 313 | async getCodeForParams (evmParams) { 314 | const addr = evmParams.codeHash.substring(0, 42); 315 | return this.wallet.provider.getCode(addr); 316 | } 317 | 318 | async getDataForParams (evmParams) { 319 | return this.taskCallData[evmParams.dataHash]; 320 | } 321 | }; 322 | -------------------------------------------------------------------------------- /utils/FragmentTree.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ethers = require('ethers'); 4 | 5 | const AbstractMerkleTree = require('./AbstractMerkleTree'); 6 | 7 | // We should support compressed proofs in the future, 8 | // means re-using hashes if we construct a proof for more than 1 slot. 9 | module.exports = class FragmentTree extends AbstractMerkleTree { 10 | /// @dev return hash proof for `slot`, 11 | /// `slot` is position in `this.leaves`. 12 | /// @return proof - array of 32 bytes hex-string (hashes) 13 | calculateProof (slot) { 14 | const proof = []; 15 | const len = this.depth - 1; 16 | 17 | for (let i = 0; i < len; i++) { 18 | proof.push(this.tree[i][slot ^ 1].hash); 19 | slot >>= 1; 20 | } 21 | return proof; 22 | } 23 | 24 | /// @dev verify if given `proofs` and `leaf` match `this.root.hash` 25 | verifyProof (leaf, proofs) { 26 | const len = proofs.length; 27 | let hash = leaf.hash; 28 | let slot = leaf.slot; 29 | 30 | for (let i = 0; i < len; i++) { 31 | const proof = proofs[i]; 32 | 33 | if (slot % 2 === 0) { 34 | hash = this.constructor.hash(hash, proof); 35 | } else { 36 | hash = this.constructor.hash(proof, hash); 37 | } 38 | slot >>= 1; 39 | } 40 | 41 | return hash === this.root.hash; 42 | } 43 | 44 | /// @notice build the tree for the given hex-string `data`. 45 | /// @dev `data` will be splited into fragments and padded to one word (32 bytes). 46 | run (data) { 47 | data = data.replace('0x', ''); 48 | 49 | this.tree = [[]]; 50 | 51 | const leaves = this.tree[0]; 52 | const byteLength = data.length / 2; 53 | const len = data.length; 54 | let slot = 0; 55 | 56 | for (let i = 0; i < len;) { 57 | const value = '0x' + data.substring(i, i += 64).padEnd(64, '0'); 58 | const hash = ethers.utils.solidityKeccak256( 59 | ['bytes32', 'uint256', 'uint256'], 60 | [value, slot, byteLength] 61 | ); 62 | 63 | slot = leaves.push({ hash, value, slot, byteLength }); 64 | } 65 | 66 | this.recal(0); 67 | 68 | return this; 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /utils/HydratedRuntime.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const OP = require('./constants'); 4 | const EVMRuntime = require('./EVMRuntime'); 5 | const RangeProofHelper = require('./RangeProofHelper'); 6 | const Merkelizer = require('./Merkelizer'); 7 | 8 | const toHex = arr => arr.map(e => '0x' + e.toString(16).padStart(64, '0')); 9 | 10 | const OP_SWAP1 = parseInt(OP.SWAP1, 16); 11 | const OP_SWAP16 = parseInt(OP.SWAP16, 16); 12 | const OP_DUP1 = parseInt(OP.DUP1, 16); 13 | const OP_DUP16 = parseInt(OP.DUP16, 16); 14 | 15 | module.exports = class HydratedRuntime extends EVMRuntime { 16 | async initRunState (obj) { 17 | const runState = await super.initRunState(obj); 18 | const stack = toHex(runState.stack); 19 | 20 | runState.steps = []; 21 | runState.prevStack = stack; 22 | runState.stackHashes = Merkelizer.stackHashes(stack); 23 | 24 | runState.memProof = new RangeProofHelper(runState.memory); 25 | runState.callDataProof = new RangeProofHelper(runState.callData); 26 | runState.codeProof = new RangeProofHelper(runState.code); 27 | runState.callData = runState.callDataProof.proxy; 28 | runState.memory = runState.memProof.proxy; 29 | runState.code = runState.codeProof.proxy; 30 | 31 | return runState; 32 | } 33 | 34 | 35 | async run (args) { 36 | const runState = await super.run(args); 37 | 38 | // a temporay hack for our unit tests :/ 39 | if (runState.steps.length > 0) { 40 | runState.steps[runState.steps.length - 1].stack = toHex(runState.stack); 41 | } 42 | 43 | return runState.steps; 44 | } 45 | 46 | async runNextStep (runState) { 47 | let pc = runState.programCounter; 48 | const callDataProof = runState.callDataProof; 49 | const codeProof = runState.codeProof; 50 | const gasLeft = runState.gasLeft.addn(0); 51 | 52 | callDataProof.reset(); 53 | codeProof.reset(); 54 | 55 | await super.runNextStep(runState); 56 | 57 | const opcode = runState.opCode; 58 | const returnData = '0x' + (runState.returnValue ? runState.returnValue.toString('hex') : ''); 59 | 60 | // if we have no errors and opcode is not RETURN or STOP, update pc 61 | if (runState.errno === 0 && (opcode !== 0xf3 && opcode !== 0x00)) { 62 | pc = runState.programCounter; 63 | } 64 | 65 | const codeReads = codeProof.readAndWrites.filter( 66 | function (val) { 67 | return val < runState.code.length; 68 | } 69 | ).sort( 70 | function (a, b) { 71 | if (a < b) { 72 | return -1; 73 | } 74 | if (a > b) { 75 | return 1; 76 | } 77 | return 0; 78 | } 79 | ); 80 | 81 | const step = { 82 | callDataReadLow: callDataProof.readLow, 83 | callDataReadHigh: callDataProof.readHigh, 84 | codeReads: codeReads, 85 | returnData: returnData, 86 | pc: pc, 87 | errno: runState.errno, 88 | gasRemaining: runState.gasLeft.toNumber(), 89 | tokenBag: runState.tokenBag, 90 | }; 91 | 92 | this.calculateMemProof(runState, step); 93 | this.calculateStackProof(runState, step); 94 | 95 | runState.steps.push(step); 96 | } 97 | 98 | calculateMemProof (runState, step) { 99 | const memProof = runState.memProof; 100 | const prevMem = runState.prevMem; 101 | const memSize = runState.memoryWordCount.toNumber(); 102 | 103 | // serialize the memory if it changed 104 | if (memProof.readHigh !== -1 || memProof.writeHigh !== -1 || !prevMem || prevMem.length !== memSize) { 105 | const mem = []; 106 | const memStore = runState.memProof.data; 107 | 108 | let i = 0; 109 | while (i < memStore.length) { 110 | const hexVal = Buffer.from(memStore.slice(i, i += 32)).toString('hex'); 111 | mem.push('0x' + hexVal.padEnd(64, '0')); 112 | } 113 | // fill the remaing zero slots 114 | while (mem.length < memSize) { 115 | mem.push(OP.ZERO_HASH); 116 | } 117 | step.mem = mem; 118 | runState.prevMem = mem; 119 | } else { 120 | step.mem = prevMem; 121 | } 122 | 123 | step.memReadLow = memProof.readLow; 124 | step.memReadHigh = memProof.readHigh; 125 | step.memWriteLow = memProof.writeLow; 126 | step.memWriteHigh = memProof.writeHigh; 127 | 128 | memProof.reset(); 129 | } 130 | 131 | calculateStackProof (runState, step) { 132 | const opcode = runState.opCode; 133 | let stackIn = runState.stackIn | 0; 134 | 135 | if (opcode >= OP_SWAP1 && opcode <= OP_SWAP16) { 136 | stackIn = (16 - (OP_SWAP16 - opcode)) * 2; 137 | } 138 | 139 | if (opcode >= OP_DUP1 && opcode <= OP_DUP16) { 140 | stackIn = 16 - (OP_DUP16 - opcode); 141 | } 142 | 143 | // can happen on error - clip here 144 | if (stackIn > runState.prevStack.length) { 145 | stackIn = runState.prevStack.length; 146 | } 147 | 148 | // if stack changed 149 | if (stackIn !== 0 || runState.prevStack.length !== runState.stack.length) { 150 | // elements needed 151 | step.compactStack = new Array(stackIn); 152 | 153 | // remove the number of 'consumed' elements - if any 154 | while (stackIn--) { 155 | step.compactStack[stackIn] = runState.prevStack.pop(); 156 | runState.stackHashes.pop(); 157 | } 158 | 159 | // add the new/changed elements - if any 160 | const newElements = []; 161 | for (let i = runState.prevStack.length; i < runState.stack.length; i++) { 162 | let val = '0x' + runState.stack[i].toString(16).padStart(64, '0'); 163 | runState.prevStack.push(val); 164 | newElements.push(val); 165 | } 166 | step.compactStackHash = runState.stackHashes[runState.stackHashes.length - 1]; 167 | 168 | const partialHashes = Merkelizer.stackHashes(newElements, step.compactStackHash); 169 | // first element of partialHash is alread in the list 170 | runState.stackHashes = runState.stackHashes.concat(partialHashes.slice(1, partialHashes.length)); 171 | } else { 172 | step.compactStackHash = runState.stackHashes[runState.stackHashes.length - 1]; 173 | step.compactStack = []; 174 | } 175 | 176 | step.stackHash = runState.stackHashes[runState.stackHashes.length - 1]; 177 | step.stackSize = runState.stack.length; 178 | } 179 | 180 | async handleJUMP (runState) { 181 | await super.handleJUMP(runState); 182 | 183 | runState.codeProof.readAndWrites.push(runState.programCounter); 184 | } 185 | 186 | async handleJUMPI (runState) { 187 | await super.handleJUMPI(runState); 188 | 189 | runState.codeProof.readAndWrites.push(runState.programCounter); 190 | } 191 | }; 192 | -------------------------------------------------------------------------------- /utils/Merkelizer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ethers = require('ethers'); 4 | const createKeccakHash = require('keccak'); 5 | 6 | const AbstractMerkleTree = require('./AbstractMerkleTree'); 7 | const { ZERO_HASH } = require('./constants'); 8 | 9 | module.exports = class Merkelizer extends AbstractMerkleTree { 10 | /// @notice If the first (left-most) hash is not the same as this, 11 | /// then the solution from that player is invalid. 12 | static initialStateHash (code, callData, customEnvironmentHash) { 13 | const DEFAULT_GAS = 0x0fffffffffffff; 14 | const res = { 15 | executionState: { 16 | code: code, 17 | data: callData, 18 | compactStack: [], 19 | stack: [], 20 | mem: [], 21 | returnData: '0x', 22 | pc: 0, 23 | errno: 0, 24 | gasRemaining: DEFAULT_GAS, 25 | stackSize: 0, 26 | memSize: 0, 27 | customEnvironmentHash: customEnvironmentHash, 28 | stackHash: this.stackHash([]), 29 | memHash: this.memHash([]), 30 | dataHash: this.dataHash(callData), 31 | }, 32 | }; 33 | 34 | res.hash = this.stateHash(res.executionState); 35 | 36 | return res; 37 | } 38 | 39 | static stackHashes (stack, sibling) { 40 | const res = sibling || ZERO_HASH; 41 | const hashes = [res]; 42 | const len = stack.length; 43 | 44 | if (len === 0) { 45 | return hashes; 46 | } 47 | 48 | // could be further improved by a custom keccak implementation 49 | const hash = createKeccakHash('keccak256'); 50 | 51 | hash.update(Buffer.from(res.replace('0x', ''), 'hex')); 52 | for (let i = 0; i < len; i++) { 53 | const val = Buffer.from(stack[i].replace('0x', ''), 'hex'); 54 | 55 | hash.update(val); 56 | 57 | if (i !== len - 1) { 58 | const buf = hash.digest(); 59 | hashes.push(`0x${buf.toString('hex')}`); 60 | hash._finalized = false; 61 | hash.update(buf); 62 | } 63 | } 64 | 65 | hashes.push(`0x${hash.digest().toString('hex')}`); 66 | return hashes; 67 | } 68 | 69 | static stackHash (stack, sibling) { 70 | const res = this.stackHashes(stack, sibling); 71 | return res[res.length - 1]; 72 | } 73 | 74 | static memHash (mem) { 75 | const len = mem.length; 76 | const hash = createKeccakHash('keccak256'); 77 | 78 | for (let i = 0; i < len; i++) { 79 | hash.update(Buffer.from(mem[i].replace('0x', ''), 'hex')); 80 | } 81 | 82 | return `0x${hash.digest().toString('hex')}`; 83 | } 84 | 85 | static dataHash (data) { 86 | return ethers.utils.solidityKeccak256( 87 | ['bytes'], 88 | [data] 89 | ); 90 | } 91 | 92 | static preStateHash (execution) { 93 | return ethers.utils.solidityKeccak256( 94 | [ 95 | 'bytes32', 96 | 'bytes32', 97 | 'bytes32', 98 | 'bytes32', 99 | 'uint256', 100 | 'uint256', 101 | 'uint256', 102 | 'uint256', 103 | ], 104 | [ 105 | execution.stackHash, 106 | execution.memHash, 107 | execution.dataHash, 108 | execution.customEnvironmentHash, 109 | execution.pc, 110 | execution.gasRemaining, 111 | execution.stackSize, 112 | execution.memSize, 113 | ] 114 | ); 115 | } 116 | 117 | static stateHash (execution) { 118 | // TODO: compact returnData 119 | 120 | return ethers.utils.solidityKeccak256( 121 | [ 122 | 'bytes32', 123 | 'bytes', 124 | ], 125 | [ 126 | this.preStateHash(execution), 127 | execution.returnData, 128 | ] 129 | ); 130 | } 131 | 132 | run (executions, code, callData, customEnvironmentHash) { 133 | if (!executions || !executions.length) { 134 | throw new Error('You need to pass at least one execution step'); 135 | } 136 | customEnvironmentHash = customEnvironmentHash || ZERO_HASH; 137 | 138 | this.tree = [[]]; 139 | 140 | const initialState = this.constructor.initialStateHash(code, callData, customEnvironmentHash); 141 | const leaves = this.tree[0]; 142 | const callDataHash = this.constructor.dataHash(callData); 143 | 144 | let prevLeaf = { right: initialState }; 145 | let len = executions.length; 146 | let memHash; 147 | 148 | for (let i = 0; i < len; i++) { 149 | const exec = executions[i]; 150 | const stackHash = exec.stackHash; 151 | 152 | // memory is changed if either written to or if it was expanded 153 | let memoryChanged = exec.memWriteLow !== -1; 154 | if (!memoryChanged && prevLeaf.right.executionState) { 155 | memoryChanged = prevLeaf.right.executionState.memSize !== exec.memSize; 156 | } 157 | 158 | if (!memHash || memoryChanged) { 159 | memHash = this.constructor.memHash(exec.mem); 160 | } 161 | 162 | // convenience 163 | exec.memSize = exec.mem.length; 164 | exec.data = callData; 165 | exec.dataHash = callDataHash; 166 | exec.memHash = memHash; 167 | // TODO: the runtime should ultimately support and supply that 168 | exec.customEnvironmentHash = customEnvironmentHash; 169 | 170 | const hash = this.constructor.stateHash(exec); 171 | const llen = leaves.push( 172 | { 173 | left: prevLeaf.right, 174 | right: { 175 | hash: hash, 176 | stackHash, 177 | memHash, 178 | executionState: executions[i], 179 | }, 180 | hash: this.constructor.hash(prevLeaf.right.hash, hash), 181 | isLeaf: true, 182 | isFirstExecutionStep: i === 0, 183 | } 184 | ); 185 | 186 | prevLeaf = leaves[llen - 1]; 187 | } 188 | 189 | this.recal(0); 190 | 191 | return this; 192 | } 193 | 194 | /// @notice Calculates a proof for `returnData` of the last execution step. 195 | /// @return Array 196 | computeResultProof () { 197 | const resultProof = []; 198 | 199 | let returnData; 200 | let node = this.root; 201 | while (true) { 202 | let hash = node.right.hash; 203 | 204 | if (node.isLeaf) { 205 | if (node.right.hash === ZERO_HASH) { 206 | const preHash = this.constructor.preStateHash(node.left.executionState); 207 | 208 | returnData = node.left.executionState.returnData; 209 | resultProof.push(preHash); 210 | resultProof.push(node.right.hash); 211 | } else { 212 | const preHash = this.constructor.preStateHash(node.right.executionState); 213 | 214 | returnData = node.right.executionState.returnData; 215 | resultProof.push(node.left.hash); 216 | resultProof.push(preHash); 217 | } 218 | break; 219 | } 220 | 221 | resultProof.push(node.left.hash); 222 | resultProof.push(node.right.hash); 223 | 224 | if (hash === ZERO_HASH) { 225 | hash = node.left.hash; 226 | } 227 | node = this.getNode(hash); 228 | } 229 | 230 | return { resultProof, returnData }; 231 | } 232 | 233 | /// @notice Verifies a proof from `computeResultProof`. 234 | /// @return `true` if correct, else `false` 235 | verifyResultProof (resultProof, returnData, rootHash) { 236 | const len = resultProof.length; 237 | 238 | if (len < 2 || (len % 2) !== 0) { 239 | return false; 240 | } 241 | 242 | // save those temporarily 243 | let tmpLeft = resultProof[len - 2]; 244 | let tmpRight = resultProof[len - 1]; 245 | if (tmpRight === ZERO_HASH) { 246 | resultProof[len - 2] = 247 | ethers.utils.solidityKeccak256(['bytes32', 'bytes'], [tmpLeft, returnData]); 248 | } else { 249 | resultProof[len - 1] = 250 | ethers.utils.solidityKeccak256(['bytes32', 'bytes'], [tmpRight, returnData]); 251 | } 252 | 253 | let valid = true; 254 | let parentHash = rootHash; 255 | for (let i = 0; i < len; i += 2) { 256 | const left = resultProof[i]; 257 | const right = resultProof[i + 1]; 258 | const hash = this.constructor.hash(left, right); 259 | 260 | if (hash !== parentHash) { 261 | valid = false; 262 | break; 263 | } 264 | 265 | if (right === ZERO_HASH) { 266 | parentHash = left; 267 | } else { 268 | parentHash = right; 269 | } 270 | } 271 | 272 | // restore the values we swapped above 273 | resultProof[len - 2] = tmpLeft; 274 | resultProof[len - 1] = tmpRight; 275 | 276 | return valid; 277 | } 278 | }; 279 | -------------------------------------------------------------------------------- /utils/Opcodes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | // 0x0 range - arithmetic ops 5 | // name, baseCost, off stack, on stack 6 | 0x00: ['STOP', 0, 0, 0], 7 | 0x01: ['ADD', 3, 2, 1], 8 | 0x02: ['MUL', 5, 2, 1], 9 | 0x03: ['SUB', 3, 2, 1], 10 | 0x04: ['DIV', 5, 2, 1], 11 | 0x05: ['SDIV', 5, 2, 1], 12 | 0x06: ['MOD', 5, 2, 1], 13 | 0x07: ['SMOD', 5, 2, 1], 14 | 0x08: ['ADDMOD', 8, 3, 1], 15 | 0x09: ['MULMOD', 8, 3, 1], 16 | 0x0a: ['EXP', 10, 2, 1], 17 | 0x0b: ['SIGNEXTEND', 5, 2, 1], 18 | 19 | // 0x10 range - bit ops 20 | 0x10: ['LT', 3, 2, 1], 21 | 0x11: ['GT', 3, 2, 1], 22 | 0x12: ['SLT', 3, 2, 1], 23 | 0x13: ['SGT', 3, 2, 1], 24 | 0x14: ['EQ', 3, 2, 1], 25 | 0x15: ['ISZERO', 3, 1, 1], 26 | 0x16: ['AND', 3, 2, 1], 27 | 0x17: ['OR', 3, 2, 1], 28 | 0x18: ['XOR', 3, 2, 1], 29 | 0x19: ['NOT', 3, 1, 1], 30 | 0x1a: ['BYTE', 3, 2, 1], 31 | 0x1b: ['SHL', 3, 2, 1], 32 | 0x1c: ['SHR', 3, 2, 1], 33 | 0x1d: ['SAR', 3, 2, 1], 34 | 35 | // 0x20 range - crypto 36 | 0x20: ['SHA3', 30, 2, 1], 37 | 38 | // 0x30 range - closure state 39 | 0x30: ['ADDRESS', 2, 0, 1], 40 | 0x31: ['BALANCE', 400, 1, 1], 41 | 0x32: ['ORIGIN', 2, 0, 1], 42 | 0x33: ['CALLER', 2, 0, 1], 43 | 0x34: ['CALLVALUE', 2, 0, 1], 44 | 0x35: ['CALLDATALOAD', 3, 1, 1], 45 | 0x36: ['CALLDATASIZE', 2, 0, 1], 46 | 0x37: ['CALLDATACOPY', 3, 3, 0], 47 | 0x38: ['CODESIZE', 2, 0, 1], 48 | 0x39: ['CODECOPY', 3, 3, 0], 49 | 0x3a: ['GASPRICE', 2, 0, 1], 50 | 0x3b: ['EXTCODESIZE', 700, 1, 1], 51 | 0x3c: ['EXTCODECOPY', 700, 4, 0], 52 | 0x3d: ['RETURNDATASIZE', 2, 0, 1], 53 | 0x3e: ['RETURNDATACOPY', 3, 3, 0], 54 | 0x3f: ['EXTCODEHASH', 400, 1, 1], 55 | 56 | // '0x40' range - block operations 57 | 0x40: ['BLOCKHASH', 20, 1, 1], 58 | 0x41: ['COINBASE', 2, 0, 1], 59 | 0x42: ['TIMESTAMP', 2, 0, 1], 60 | 0x43: ['NUMBER', 2, 0, 1], 61 | 0x44: ['DIFFICULTY', 2, 0, 1], 62 | 0x45: ['GASLIMIT', 2, 0, 1], 63 | 64 | // 0x50 range - 'storage' and execution 65 | 0x50: ['POP', 2, 1, 0], 66 | 0x51: ['MLOAD', 3, 1, 1], 67 | 0x52: ['MSTORE', 3, 2, 0], 68 | 0x53: ['MSTORE8', 3, 2, 0], 69 | 0x54: ['SLOAD', 200, 1, 1], 70 | 0x55: ['SSTORE', 0, 2, 0], 71 | 0x56: ['JUMP', 8, 1, 0], 72 | 0x57: ['JUMPI', 10, 2, 0], 73 | 0x58: ['PC', 2, 0, 1], 74 | 0x59: ['MSIZE', 2, 0, 1], 75 | 0x5a: ['GAS', 2, 0, 1], 76 | 0x5b: ['JUMPDEST', 1, 0, 0], 77 | 78 | // 0x60, range 79 | 0x60: ['PUSH', 3, 0, 1], 80 | 0x61: ['PUSH', 3, 0, 1], 81 | 0x62: ['PUSH', 3, 0, 1], 82 | 0x63: ['PUSH', 3, 0, 1], 83 | 0x64: ['PUSH', 3, 0, 1], 84 | 0x65: ['PUSH', 3, 0, 1], 85 | 0x66: ['PUSH', 3, 0, 1], 86 | 0x67: ['PUSH', 3, 0, 1], 87 | 0x68: ['PUSH', 3, 0, 1], 88 | 0x69: ['PUSH', 3, 0, 1], 89 | 0x6a: ['PUSH', 3, 0, 1], 90 | 0x6b: ['PUSH', 3, 0, 1], 91 | 0x6c: ['PUSH', 3, 0, 1], 92 | 0x6d: ['PUSH', 3, 0, 1], 93 | 0x6e: ['PUSH', 3, 0, 1], 94 | 0x6f: ['PUSH', 3, 0, 1], 95 | 0x70: ['PUSH', 3, 0, 1], 96 | 0x71: ['PUSH', 3, 0, 1], 97 | 0x72: ['PUSH', 3, 0, 1], 98 | 0x73: ['PUSH', 3, 0, 1], 99 | 0x74: ['PUSH', 3, 0, 1], 100 | 0x75: ['PUSH', 3, 0, 1], 101 | 0x76: ['PUSH', 3, 0, 1], 102 | 0x77: ['PUSH', 3, 0, 1], 103 | 0x78: ['PUSH', 3, 0, 1], 104 | 0x79: ['PUSH', 3, 0, 1], 105 | 0x7a: ['PUSH', 3, 0, 1], 106 | 0x7b: ['PUSH', 3, 0, 1], 107 | 0x7c: ['PUSH', 3, 0, 1], 108 | 0x7d: ['PUSH', 3, 0, 1], 109 | 0x7e: ['PUSH', 3, 0, 1], 110 | 0x7f: ['PUSH', 3, 0, 1], 111 | 112 | 0x80: ['DUP', 3, 0, 1], 113 | 0x81: ['DUP', 3, 0, 1], 114 | 0x82: ['DUP', 3, 0, 1], 115 | 0x83: ['DUP', 3, 0, 1], 116 | 0x84: ['DUP', 3, 0, 1], 117 | 0x85: ['DUP', 3, 0, 1], 118 | 0x86: ['DUP', 3, 0, 1], 119 | 0x87: ['DUP', 3, 0, 1], 120 | 0x88: ['DUP', 3, 0, 1], 121 | 0x89: ['DUP', 3, 0, 1], 122 | 0x8a: ['DUP', 3, 0, 1], 123 | 0x8b: ['DUP', 3, 0, 1], 124 | 0x8c: ['DUP', 3, 0, 1], 125 | 0x8d: ['DUP', 3, 0, 1], 126 | 0x8e: ['DUP', 3, 0, 1], 127 | 0x8f: ['DUP', 3, 0, 1], 128 | 129 | 0x90: ['SWAP', 3, 0, 0], 130 | 0x91: ['SWAP', 3, 0, 0], 131 | 0x92: ['SWAP', 3, 0, 0], 132 | 0x93: ['SWAP', 3, 0, 0], 133 | 0x94: ['SWAP', 3, 0, 0], 134 | 0x95: ['SWAP', 3, 0, 0], 135 | 0x96: ['SWAP', 3, 0, 0], 136 | 0x97: ['SWAP', 3, 0, 0], 137 | 0x98: ['SWAP', 3, 0, 0], 138 | 0x99: ['SWAP', 3, 0, 0], 139 | 0x9a: ['SWAP', 3, 0, 0], 140 | 0x9b: ['SWAP', 3, 0, 0], 141 | 0x9c: ['SWAP', 3, 0, 0], 142 | 0x9d: ['SWAP', 3, 0, 0], 143 | 0x9e: ['SWAP', 3, 0, 0], 144 | 0x9f: ['SWAP', 3, 0, 0], 145 | 146 | 0xa0: ['LOG', 375, 2, 0], 147 | 0xa1: ['LOG', 375, 3, 0], 148 | 0xa2: ['LOG', 375, 4, 0], 149 | 0xa3: ['LOG', 375, 5, 0], 150 | 0xa4: ['LOG', 375, 6, 0], 151 | 152 | // '0xf0' range - closures 153 | 0xf0: ['CREATE', 32000, 3, 1], 154 | 0xf1: ['CALL', 700, 7, 1], 155 | 0xf2: ['CALLCODE', 700, 7, 1], 156 | 0xf3: ['RETURN', 0, 2, 0], 157 | 0xf4: ['DELEGATECALL', 700, 6, 1], 158 | 0xf5: ['CREATE2', 32000, 4, 1], 159 | 0xfa: ['STATICCALL', 700, 6, 1], 160 | 0xfd: ['REVERT', 0, 2, 0], 161 | 162 | // '0x70', range - other 163 | 0xfe: ['INVALID', 0, 0, 0], 164 | 0xff: ['SELFDESTRUCT', 5000, 1, 0], 165 | }; 166 | -------------------------------------------------------------------------------- /utils/ProofHelper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Merkelizer = require('./Merkelizer'); 4 | 5 | const { ZERO_HASH } = require('./constants'); 6 | 7 | module.exports = class ProofHelper { 8 | static constructProof (computationPath, { merkle, codeFragmentTree } = {}) { 9 | const prevOutput = computationPath.left.executionState; 10 | const execState = computationPath.right.executionState; 11 | let isMemoryRequired = false; 12 | if (execState.memReadHigh !== -1 || execState.memWriteHigh !== -1) { 13 | isMemoryRequired = true; 14 | } 15 | let isCallDataRequired = false; 16 | if (execState.callDataReadHigh !== -1 || execState.callDataWriteHigh !== -1) { 17 | isCallDataRequired = true; 18 | } 19 | 20 | const proofs = { 21 | stackHash: execState.compactStackHash, 22 | memHash: isMemoryRequired ? ZERO_HASH : Merkelizer.memHash(prevOutput.mem), 23 | dataHash: isCallDataRequired ? ZERO_HASH : Merkelizer.dataHash(prevOutput.data), 24 | codeByteLength: 0, 25 | codeFragments: [], 26 | codeProof: [], 27 | }; 28 | 29 | if (codeFragmentTree) { 30 | const leaves = codeFragmentTree.leaves; 31 | const neededSlots = []; 32 | 33 | // convert to 32 byte-sized words 34 | execState.codeReads.forEach( 35 | function (val) { 36 | val = val >> 5; 37 | if (neededSlots.indexOf(val) === -1) { 38 | neededSlots.push(val); 39 | } 40 | } 41 | ); 42 | 43 | for (let i = 0; i < neededSlots.length; i++) { 44 | const slot = neededSlots[i]; 45 | const leaf = leaves[slot]; 46 | 47 | if (leaf.hash === ZERO_HASH) { 48 | continue; 49 | } 50 | // panic, just in case 51 | if (leaf.slot !== slot) { 52 | throw new Error('FragmentTree for contract code is not sorted'); 53 | } 54 | 55 | proofs.codeFragments.push('0x' + leaf.slot.toString(16).padStart(64, '0')); 56 | proofs.codeFragments.push(leaf.value); 57 | 58 | const proof = codeFragmentTree.calculateProof(leaf.slot); 59 | // paranoia 60 | if (!codeFragmentTree.verifyProof(leaf, proof)) { 61 | throw new Error(`Can not verify proof for ${leaf}`); 62 | } 63 | 64 | proofs.codeProof = proofs.codeProof.concat(proof); 65 | proofs.codeByteLength = leaf.byteLength; 66 | } 67 | } 68 | 69 | return { 70 | proofs, 71 | executionInput: { 72 | data: isCallDataRequired ? prevOutput.data : '0x', 73 | stack: execState.compactStack, 74 | mem: isMemoryRequired ? prevOutput.mem : [], 75 | customEnvironmentHash: prevOutput.customEnvironmentHash, 76 | returnData: prevOutput.returnData, 77 | pc: prevOutput.pc, 78 | gasRemaining: prevOutput.gasRemaining, 79 | stackSize: prevOutput.stackSize, 80 | memSize: prevOutput.memSize, 81 | }, 82 | }; 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /utils/RangeProofHelper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = class RangeProofHelper { 4 | constructor (mem) { 5 | const self = this; 6 | 7 | const handler = { 8 | get: function (obj, prop) { 9 | if (prop === 'slice') { 10 | return this.slice; 11 | } 12 | 13 | const x = parseInt(prop); 14 | if (Number.isInteger(x)) { 15 | if (x < self.readLow || self.readLow === -1) { 16 | self.readLow = x; 17 | } 18 | if (x > self.readHigh || self.readHigh === -1) { 19 | self.readHigh = x; 20 | } 21 | if (self.readAndWrites.indexOf(x) === -1) { 22 | self.readAndWrites.push(x); 23 | } 24 | } 25 | 26 | return obj[prop]; 27 | }, 28 | 29 | set: function (obj, prop, val) { 30 | obj[prop] = val; 31 | 32 | const x = parseInt(prop); 33 | if (Number.isInteger(x)) { 34 | if (x < self.writeLow || self.writeLow === -1) { 35 | self.writeLow = x; 36 | } 37 | if (x > self.writeHigh || self.writeHigh === -1) { 38 | self.writeHigh = x; 39 | } 40 | if (self.readAndWrites.indexOf(x) === -1) { 41 | self.readAndWrites.push(x); 42 | } 43 | } 44 | 45 | return true; 46 | }, 47 | 48 | slice: function (a, b) { 49 | for (let i = a; i < b; i++) { 50 | if (self.readAndWrites.indexOf(i) === -1) { 51 | self.readAndWrites.push(i); 52 | } 53 | } 54 | if (a < self.readLow || self.readLow === -1) { 55 | self.readLow = a; 56 | } 57 | if (b > self.readHigh || self.readHigh === -1) { 58 | self.readHigh = b; 59 | } 60 | return mem.slice(a, b); 61 | }, 62 | }; 63 | 64 | this.data = mem; 65 | this.proxy = new Proxy(mem, handler); 66 | this.reset(); 67 | } 68 | 69 | reset () { 70 | this.readLow = -1; 71 | this.readHigh = -1; 72 | this.writeLow = -1; 73 | this.writeHigh = -1; 74 | this.readAndWrites = []; 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /utils/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | DEFAULT_CALLER: 'cd1722f2947def4cf144679da39c4c32bdc35681', 5 | DEFAULT_CONTRACT_ADDRESS: '0f572e5295c57f15886f9b263e2f6d2d6c7b5ec6', 6 | GAS_LIMIT: 0x0fffffffffffff, 7 | // Errors 8 | 9 | NO_ERROR: 0, 10 | ERROR_STACK_OVERFLOW: 0x01, 11 | ERROR_STACK_UNDERFLOW: 0x02, 12 | ERROR_INDEX_OOB: 0x03, 13 | ERROR_INVALID_OPCODE: 0x04, 14 | ERROR_INVALID_JUMP_DESTINATION: 0x05, 15 | ERROR_INSTRUCTION_NOT_SUPPORTED: 0x06, 16 | ERROR_STATE_REVERTED: 0x07, 17 | ERROR_INSUFFICIENT_FUNDS: 0x08, 18 | ERROR_CONTRACT_CREATION_COLLISION: 0x09, 19 | ERROR_MAX_CODE_SIZE_EXCEEDED: 0x0a, 20 | ERROR_ILLEGAL_WRITE_OPERATION: 0x0b, 21 | ERROR_PRECOMPILE_NOT_IMPLEMENTED: 0x0c, 22 | ERROR_OUT_OF_GAS: 0x0d, 23 | ERROR_INTERNAL: 0xff, 24 | 25 | // Ethereum opcodes 26 | 27 | // Stop and arithmetic ops 28 | STOP: '00', 29 | ADD: '01', 30 | MUL: '02', 31 | SUB: '03', 32 | DIV: '04', 33 | SDIV: '05', 34 | MOD: '06', 35 | SMOD: '07', 36 | ADDMOD: '08', 37 | MULMOD: '09', 38 | EXP: '0a', 39 | SIGNEXTEND: '0b', 40 | 41 | // Comparison & bitwise logic 42 | LT: '10', 43 | GT: '11', 44 | SLT: '12', 45 | SGT: '13', 46 | EQ: '14', 47 | ISZERO: '15', 48 | AND: '16', 49 | OR: '17', 50 | XOR: '18', 51 | NOT: '19', 52 | BYTE: '1a', 53 | SHL: '1b', 54 | SHR: '1c', 55 | SAR: '1d', 56 | // SHA3 57 | SHA3: '20', 58 | 59 | // Environmental information 60 | ADDRESS: '30', 61 | BALANCE: '31', 62 | ORIGIN: '32', 63 | CALLER: '33', 64 | CALLVALUE: '34', 65 | CALLDATALOAD: '35', 66 | CALLDATASIZE: '36', 67 | CALLDATACOPY: '37', 68 | CODESIZE: '38', 69 | CODECOPY: '39', 70 | GASPRICE: '3a', 71 | EXTCODESIZE: '3b', 72 | EXTCODECOPY: '3c', 73 | RETURNDATASIZE: '3d', 74 | RETURNDATACOPY: '3e', 75 | EXTCODEHASH: '3f', 76 | 77 | // Block information 78 | BLOCKHASH: '40', 79 | COINBASE: '41', 80 | TIMESTAMP: '42', 81 | NUMBER: '43', 82 | DIFFICULTY: '44', 83 | GASLIMIT: '45', 84 | 85 | // Stack, Memory, Storage and Flow Operations 86 | POP: '50', 87 | MLOAD: '51', 88 | MSTORE: '52', 89 | MSTORE8: '53', 90 | SLOAD: '54', 91 | SSTORE: '55', 92 | JUMP: '56', 93 | JUMPI: '57', 94 | PC: '58', 95 | MSIZE: '59', 96 | GAS: '5a', 97 | JUMPDEST: '5b', 98 | 99 | // Push operations 100 | PUSH1: '60', 101 | PUSH2: '61', 102 | PUSH3: '62', 103 | PUSH4: '63', 104 | PUSH5: '64', 105 | PUSH6: '65', 106 | PUSH7: '66', 107 | PUSH8: '67', 108 | PUSH9: '68', 109 | PUSH10: '69', 110 | PUSH11: '6a', 111 | PUSH12: '6b', 112 | PUSH13: '6c', 113 | PUSH14: '6d', 114 | PUSH15: '6e', 115 | PUSH16: '6f', 116 | PUSH17: '70', 117 | PUSH18: '71', 118 | PUSH19: '72', 119 | PUSH20: '73', 120 | PUSH21: '74', 121 | PUSH22: '75', 122 | PUSH23: '76', 123 | PUSH24: '77', 124 | PUSH25: '78', 125 | PUSH26: '79', 126 | PUSH27: '7a', 127 | PUSH28: '7b', 128 | PUSH29: '7c', 129 | PUSH30: '7d', 130 | PUSH31: '7e', 131 | PUSH32: '7f', 132 | 133 | // Duplication operations 134 | DUP1: '80', 135 | DUP2: '81', 136 | DUP3: '82', 137 | DUP4: '83', 138 | DUP5: '84', 139 | DUP6: '85', 140 | DUP7: '86', 141 | DUP8: '87', 142 | DUP9: '88', 143 | DUP10: '89', 144 | DUP11: '8a', 145 | DUP12: '8b', 146 | DUP13: '8c', 147 | DUP14: '8d', 148 | DUP15: '8e', 149 | DUP16: '8f', 150 | 151 | // Exchange operations 152 | SWAP1: '90', 153 | SWAP2: '91', 154 | SWAP3: '92', 155 | SWAP4: '93', 156 | SWAP5: '94', 157 | SWAP6: '95', 158 | SWAP7: '96', 159 | SWAP8: '97', 160 | SWAP9: '98', 161 | SWAP10: '99', 162 | SWAP11: '9a', 163 | SWAP12: '9b', 164 | SWAP13: '9c', 165 | SWAP14: '9d', 166 | SWAP15: '9e', 167 | SWAP16: '9f', 168 | 169 | // Logging operations 170 | LOG0: 'a0', 171 | LOG1: 'a1', 172 | LOG2: 'a2', 173 | LOG3: 'a3', 174 | LOG4: 'a4', 175 | 176 | // System operations 177 | CREATE: 'f0', 178 | CALL: 'f1', 179 | CALLCODE: 'f2', 180 | RETURN: 'f3', 181 | DELEGATECALL: 'f4', 182 | CREATE2: 'f5', 183 | STATICCALL: 'fa', 184 | REVERT: 'fd', 185 | INVALID: 'fe', 186 | SELFDESTRUCT: 'ff', 187 | 188 | BLOCK_GAS_LIMIT: '0x0fffffffffffff', 189 | 190 | ZERO_HASH: '0x0000000000000000000000000000000000000000000000000000000000000000', 191 | ZERO_ADDRESS: '0x0000000000000000000000000000000000000000', 192 | 193 | FUNCSIG_TRANSFER: '0xa9059cbb', 194 | FUNCSIG_BALANCEOF: '0x70a08231', 195 | FUNCSIG_READATA: '0x37ebbc03', 196 | FUNCSIG_TRANSFERFROM: '0x23b872dd', 197 | FUNCSIG_WRITEDATA: '0xa983d43f', 198 | FUNCSIG_OWNEROF: '0x6352211e', 199 | 200 | CALLISH_SUCCESS: 1, 201 | CALLISH_FAIL: 0, 202 | 203 | ERC20TYPE: 0, 204 | ERC721TYPE: 1, 205 | ERC1948TYPE: 2, 206 | }; 207 | -------------------------------------------------------------------------------- /utils/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Constants = require('./constants.js'); 4 | const AbstractMerkleTree = require('./AbstractMerkleTree.js'); 5 | const EVMRuntime = require('./EVMRuntime.js'); 6 | const HydratedRuntime = require('./HydratedRuntime.js'); 7 | const Merkelizer = require('./Merkelizer.js'); 8 | const ProofHelper = require('./ProofHelper.js'); 9 | const ExecutionPoker = require('./ExecutionPoker.js'); 10 | const FragmentTree = require('./FragmentTree.js'); 11 | 12 | module.exports = { 13 | Constants, 14 | AbstractMerkleTree, 15 | EVMRuntime, 16 | HydratedRuntime, 17 | Merkelizer, 18 | ProofHelper, 19 | ExecutionPoker, 20 | FragmentTree, 21 | }; 22 | -------------------------------------------------------------------------------- /utils/precompiled/01-ecrecover.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const utils = require('ethereumjs-util'); 4 | const BN = utils.BN; 5 | 6 | module.exports = function (gasLimit, data) { 7 | const results = {}; 8 | 9 | results.gasUsed = new BN(3000); 10 | 11 | if (gasLimit.lt(results.gasUsed)) { 12 | results.returnValue = Buffer.alloc(0); 13 | results.gasUsed = gasLimit; 14 | results.exception = 0; 15 | return results; 16 | } 17 | 18 | const paddedData = utils.setLengthRight(data, 128); 19 | const msgHash = paddedData.slice(0, 32); 20 | const v = paddedData.slice(32, 64); 21 | const r = paddedData.slice(64, 96); 22 | const s = paddedData.slice(96, 128); 23 | 24 | let publicKey; 25 | try { 26 | publicKey = utils.ecrecover(msgHash, new BN(v).toNumber(), r, s); 27 | } catch (e) { 28 | results.returnValue = Buffer.alloc(0); 29 | results.exception = 1; 30 | return results; 31 | } 32 | 33 | results.returnValue = utils.setLengthLeft(utils.publicToAddress(publicKey), 32); 34 | results.exception = 1; 35 | 36 | return results; 37 | }; 38 | -------------------------------------------------------------------------------- /utils/precompiled/02-sha256.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const utils = require('ethereumjs-util'); 4 | const BN = utils.BN; 5 | 6 | module.exports = function (gasLimit, data) { 7 | const results = {}; 8 | 9 | results.gasUsed = new BN(60); 10 | results.gasUsed.iadd(new BN(12).imuln(Math.ceil(data.length / 32))); 11 | 12 | if (gasLimit.lt(results.gasUsed)) { 13 | results.returnValue = Buffer.alloc(0); 14 | results.gasUsed = gasLimit; 15 | results.exception = 0; 16 | return results; 17 | } 18 | 19 | results.returnValue = utils.sha256(data); 20 | results.exception = 1; 21 | 22 | return results; 23 | }; 24 | -------------------------------------------------------------------------------- /utils/precompiled/03-ripemd160.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const utils = require('ethereumjs-util'); 4 | const BN = utils.BN; 5 | 6 | module.exports = function (gasLimit, data) { 7 | const results = {}; 8 | 9 | results.gasUsed = new BN(600); 10 | results.gasUsed.iadd(new BN(120).imuln(Math.ceil(data.length / 32))); 11 | 12 | if (gasLimit.lt(results.gasUsed)) { 13 | results.returnValue = Buffer.alloc(0); 14 | results.gasUsed = gasLimit; 15 | results.exception = 0; 16 | return results; 17 | } 18 | 19 | results.returnValue = utils.ripemd160(data, true); 20 | results.exception = 1; 21 | 22 | return results; 23 | }; 24 | -------------------------------------------------------------------------------- /utils/precompiled/04-identity.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const utils = require('ethereumjs-util'); 4 | const BN = utils.BN; 5 | 6 | module.exports = function (gasLimit, data) { 7 | const results = {}; 8 | 9 | results.gasUsed = new BN(15); 10 | results.gasUsed.iadd(new BN(3).imuln(Math.ceil(data.length / 32))); 11 | 12 | if (gasLimit.lt(results.gasUsed)) { 13 | results.returnValue = Buffer.alloc(0); 14 | results.gasUsed = gasLimit; 15 | results.exception = 0; 16 | return results; 17 | } 18 | 19 | results.returnValue = data; 20 | results.exception = 1; 21 | 22 | return results; 23 | }; 24 | -------------------------------------------------------------------------------- /utils/precompiled/05-modexp.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const utils = require('ethereumjs-util'); 4 | const BN = utils.BN; 5 | 6 | function multComplexity (x) { 7 | if (x.lten(64)) { 8 | return x.sqr(); 9 | } 10 | 11 | if (x.lten(1024)) { 12 | // return Math.floor(Math.pow(x, 2) / 4) + 96 * x - 3072 13 | const fac1 = x.sqr().divn(4); 14 | const fac2 = x.muln(96); 15 | 16 | return fac1.add(fac2).subn(3072); 17 | } 18 | 19 | // return Math.floor(Math.pow(x, 2) / 16) + 480 * x - 199680 20 | const fac1 = x.sqr().divn(16); 21 | const fac2 = x.muln(480); 22 | 23 | return fac1.add(fac2).subn(199680); 24 | } 25 | 26 | function getAdjustedExponentLength (data) { 27 | let expBytesStart; 28 | try { 29 | const baseLen = new BN(data.slice(0, 32)).toNumber(); 30 | 31 | expBytesStart = 96 + baseLen; 32 | // 96 for base length, 33 | // then exponent length, 34 | // and modulus length, 35 | // then baseLen for the base data, 36 | // then exponent bytes start 37 | } catch (e) { 38 | expBytesStart = Number.MAX_SAFE_INTEGER - 32; 39 | } 40 | const expLen = new BN(data.slice(32, 64)); 41 | let firstExpBytes = Buffer.from(data.slice(expBytesStart, expBytesStart + 32)); // first word of the exponent data 42 | firstExpBytes = utils.setLengthRight(firstExpBytes, 32); // reading past the data reads virtual zeros 43 | firstExpBytes = new BN(firstExpBytes); 44 | let max32expLen = 0; 45 | 46 | if (expLen.ltn(32)) { 47 | max32expLen = 32 - expLen.toNumber(); 48 | } 49 | firstExpBytes = firstExpBytes.shrn(8 * Math.max(max32expLen, 0)); 50 | 51 | let bitLen = -1; 52 | while (firstExpBytes.gtn(0)) { 53 | bitLen = bitLen + 1; 54 | firstExpBytes = firstExpBytes.ushrn(1); 55 | } 56 | 57 | let expLenMinus32OrZero = expLen.subn(32); 58 | if (expLenMinus32OrZero.ltn(0)) { 59 | expLenMinus32OrZero = new BN(0); 60 | } 61 | 62 | const eightTimesExpLenMinus32OrZero = expLenMinus32OrZero.muln(8); 63 | let adjustedExpLen = eightTimesExpLenMinus32OrZero; 64 | if (bitLen > 0) { 65 | adjustedExpLen.iaddn(bitLen); 66 | } 67 | 68 | return adjustedExpLen; 69 | } 70 | 71 | function expmod (B, E, M) { 72 | if (E.isZero()) { 73 | return new BN(1).mod(M); 74 | } 75 | 76 | // Red asserts M > 1 77 | if (M.lten(1)) { 78 | return new BN(0); 79 | } 80 | 81 | const red = BN.red(M); 82 | const redB = B.toRed(red); 83 | const res = redB.redPow(E); 84 | return res.fromRed(); 85 | } 86 | 87 | function getOOGResults (gasLimit, results) { 88 | results.returnValue = Buffer.alloc(0); 89 | results.gasUsed = gasLimit; 90 | results.exception = 0; 91 | return results; 92 | } 93 | 94 | module.exports = function (gasLimit, data) { 95 | const results = {}; 96 | 97 | let adjustedELen = getAdjustedExponentLength(data); 98 | if (adjustedELen.ltn(1)) { 99 | adjustedELen = new BN(1); 100 | } 101 | 102 | const bLen = new BN(data.slice(0, 32)); 103 | const eLen = new BN(data.slice(32, 64)); 104 | let mLen = new BN(data.slice(64, 96)); 105 | let maxLen = bLen; 106 | 107 | if (maxLen.lt(mLen)) { 108 | maxLen = mLen; 109 | } 110 | 111 | const Gquaddivisor = 20; 112 | const gasUsed = adjustedELen.mul(multComplexity(maxLen)).divn(Gquaddivisor); 113 | 114 | if (gasLimit.lt(gasUsed)) { 115 | return getOOGResults(gasLimit, results); 116 | } 117 | 118 | results.gasUsed = gasUsed; 119 | 120 | if (bLen.isZero()) { 121 | results.returnValue = new BN(0).toArrayLike(Buffer, 'be', 1); 122 | results.exception = 1; 123 | return results; 124 | } 125 | 126 | if (mLen.isZero()) { 127 | results.returnValue = Buffer.alloc(0); 128 | results.exception = 1; 129 | return results; 130 | } 131 | 132 | const maxInt = new BN(Number.MAX_SAFE_INTEGER); 133 | const maxSize = new BN(2147483647); 134 | 135 | if (bLen.gt(maxSize) || eLen.gt(maxSize) || mLen.gt(maxSize)) { 136 | return getOOGResults(gasLimit, results); 137 | } 138 | 139 | const bStart = new BN(96); 140 | const bEnd = bStart.add(bLen); 141 | const eStart = bEnd; 142 | const eEnd = eStart.add(eLen); 143 | const mStart = eEnd; 144 | const mEnd = mStart.add(mLen); 145 | 146 | if (mEnd.gt(maxInt)) { 147 | return getOOGResults(gasLimit, results); 148 | } 149 | 150 | mLen = mLen.toNumber(); 151 | 152 | const B = new BN(utils.setLengthRight(data.slice(bStart.toNumber(), bEnd.toNumber()), bLen.toNumber())); 153 | const E = new BN(utils.setLengthRight(data.slice(eStart.toNumber(), eEnd.toNumber()), eLen.toNumber())); 154 | const M = new BN(utils.setLengthRight(data.slice(mStart.toNumber(), mEnd.toNumber()), mLen)); 155 | 156 | let R; 157 | if (M.isZero()) { 158 | R = new BN(0); 159 | } else { 160 | R = expmod(B, E, M); 161 | } 162 | 163 | results.returnValue = R.toArrayLike(Buffer, 'be', mLen); 164 | results.exception = 1; 165 | 166 | return results; 167 | }; 168 | -------------------------------------------------------------------------------- /utils/precompiled/06-ecadd.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const utils = require('ethereumjs-util'); 4 | const BN = utils.BN; 5 | const bn128 = require('rustbn.js'); 6 | 7 | module.exports = function (gasLimit, data) { 8 | const results = {}; 9 | 10 | results.gasUsed = new BN(500); 11 | if (gasLimit.lt(results.gasUsed)) { 12 | results.returnValue = Buffer.alloc(0); 13 | results.exception = 0; 14 | results.gasUsed = new BN(gasLimit); 15 | return results; 16 | } 17 | 18 | const returnData = bn128.add(data); 19 | 20 | // check ecadd success or failure by comparing the output length 21 | if (returnData.length !== 64) { 22 | results.returnValue = Buffer.alloc(0); 23 | results.exception = 0; 24 | results.gasUsed = new BN(gasLimit); 25 | } else { 26 | results.returnValue = returnData; 27 | results.exception = 1; 28 | } 29 | 30 | return results; 31 | }; 32 | -------------------------------------------------------------------------------- /utils/precompiled/07-ecmul.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const utils = require('ethereumjs-util'); 4 | const BN = utils.BN; 5 | 6 | const bn128 = require('rustbn.js'); 7 | 8 | module.exports = function (gasLimit, data) { 9 | const results = {}; 10 | 11 | results.gasUsed = new BN(40000); 12 | 13 | if (gasLimit.lt(results.gasUsed)) { 14 | results.returnValue = Buffer.alloc(0); 15 | results.exception = 0; 16 | results.gasUsed = new BN(gasLimit); 17 | return results; 18 | } 19 | 20 | const returnData = bn128.mul(data); 21 | 22 | // check ecmul success or failure by comparing the output length 23 | if (returnData.length !== 64) { 24 | results.returnValue = Buffer.alloc(0); 25 | results.exception = 0; 26 | results.gasUsed = new BN(gasLimit); 27 | } else { 28 | results.returnValue = returnData; 29 | results.exception = 1; 30 | } 31 | 32 | return results; 33 | }; 34 | -------------------------------------------------------------------------------- /utils/precompiled/08-ecpairing.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const utils = require('ethereumjs-util'); 4 | const BN = utils.BN; 5 | const bn128 = require('rustbn.js'); 6 | 7 | module.exports = function (gasLimit, data) { 8 | const results = {}; 9 | // no need to care about non-divisible-by-192, because bn128.pairing will properly fail in that case 10 | const inputDataSize = Math.floor(data.length / 192); 11 | 12 | const gascost = 100000 + (inputDataSize * 80000); 13 | results.gasUsed = new BN(gascost); 14 | 15 | if (gasLimit.ltn(gascost)) { 16 | results.returnValue = Buffer.alloc(0); 17 | results.gasUsed = gasLimit; 18 | results.exception = 0; 19 | return results; 20 | } 21 | 22 | const returnData = bn128.pairing(data); 23 | 24 | // check ecpairing success or failure by comparing the output length 25 | if (returnData.length !== 32) { 26 | results.returnValue = Buffer.alloc(0); 27 | results.gasUsed = gasLimit; 28 | results.exception = 0; 29 | } else { 30 | results.returnValue = returnData; 31 | results.exception = 1; 32 | } 33 | 34 | return results; 35 | }; 36 | --------------------------------------------------------------------------------