├── .eslintrc ├── .prettierrc ├── test ├── test-block-manager │ ├── tx-log │ │ ├── 000000000000 │ │ └── test-block-0001.bin │ ├── dummy-tx-utils.js │ ├── test-leveldb-sum.js │ └── test-block-store.js ├── config-test.json ├── mock-accounts.js ├── performance-test-signing-and-recovering.js ├── test-utils.js ├── test-state-manager │ ├── load-test-state.js │ └── test-state.js ├── test-integration.js └── test-api.js ├── index.js ├── config.json ├── src ├── run-server.js ├── event-watcher.js ├── constants.js ├── state-manager │ ├── app.js │ └── state.js ├── mock-node.js ├── block-manager │ ├── app.js │ ├── leveldb-sum-tree.js │ └── block-store.js ├── utils.js ├── server.js └── eth-service.js ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── CONTRIBUTING.md ├── LICENSE.txt ├── bin ├── plasma-chain-start.js ├── plasma-chain-db.js ├── utils.js ├── plasma-chain-list.js ├── plasma-chain.js ├── plasma-chain-account.js ├── plasma-chain-testSwarm.js └── plasma-chain-deploy.js ├── .gitignore ├── README.md └── package.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier"], 3 | "rules": { 4 | "prettier/prettier": "error" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true, 6 | "arrowParens": "always" 7 | } 8 | -------------------------------------------------------------------------------- /test/test-block-manager/tx-log/000000000000: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plasma-group/plasma-chain-operator/HEAD/test/test-block-manager/tx-log/000000000000 -------------------------------------------------------------------------------- /test/test-block-manager/tx-log/test-block-0001.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plasma-group/plasma-chain-operator/HEAD/test/test-block-manager/tx-log/test-block-0001.bin -------------------------------------------------------------------------------- /test/config-test.json: -------------------------------------------------------------------------------- 1 | { 2 | "dbDir": "./operator-db-test/", 3 | "plasmaChainName": "One TEST Chain To Rule Them All", 4 | "operatorIpAddress": "0.0.0.0", 5 | "port": "3000", 6 | "finalityDepth": "0" 7 | } 8 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const Server = require('./src/server.js').Server 2 | const ethService = require('./src/eth-service.js') 3 | const readConfigFile = require('./src/utils.js').readConfigFile 4 | 5 | const server = new Server() 6 | 7 | module.exports = { 8 | startup: server.startup, 9 | ethService, 10 | readConfigFile 11 | } 12 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "dbDir": "./operator-db", 3 | "plasmaRegistryAddress": "0x18d8BD44a01fb8D5f295a2B3Ab15789F26385df7", 4 | "plasmaChainName": "I <3 Plasma", 5 | "operatorIpAddress": "0.0.0.0", 6 | "port": "3000", 7 | "finalityDepth": "1", 8 | "blockTimeInSeconds": "30", 9 | "ethPollInterval": "3000", 10 | "web3HttpProvider": "https://rinkeby.infura.io/v3/fce31f1fb2d54caa9b31ed7d28437fa5" 11 | } 12 | -------------------------------------------------------------------------------- /src/run-server.js: -------------------------------------------------------------------------------- 1 | const Server = require('./server.js').Server 2 | const readConfigFile = require('./utils.js').readConfigFile 3 | const path = require('path') 4 | 5 | const configFile = process.env.CONFIG 6 | ? process.env.CONFIG 7 | : path.join(__dirname, '../config.json') 8 | const config = readConfigFile(configFile) 9 | 10 | const server = new Server() 11 | 12 | async function startup () { 13 | await server.startup(config) 14 | } 15 | startup() 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of the bug. 12 | 13 | **Steps to reproduce** 14 | Steps to reproduce the behavior: 15 | 1. 16 | 2. 17 | 3. 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Specifications** 26 | - Version: 27 | - OS: 28 | - Node Version: 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /test/mock-accounts.js: -------------------------------------------------------------------------------- 1 | const Web3 = require('web3') 2 | 3 | // Generate a bunch of private keys 4 | const privateKeys = [] 5 | for (let i = 0; i < 100; i++) { 6 | privateKeys.push(Web3.utils.sha3(i.toString())) 7 | } 8 | // Generate a bunch of accounts 9 | const accounts = [] 10 | for (const privateKey of privateKeys) { 11 | accounts.push(new Web3().eth.accounts.privateKeyToAccount(privateKey)) 12 | } 13 | 14 | // Generate a sample transaction 15 | const sampleTr = { 16 | sender: accounts[0].address, 17 | recipient: accounts[1].address, 18 | type: new Web3.utils.BN('0'), 19 | start: new Web3.utils.BN('10'), 20 | offset: new Web3.utils.BN('1'), 21 | block: new Web3.utils.BN('0'), 22 | } 23 | 24 | const sig = { 25 | v: new Web3.utils.BN('0'), 26 | r: new Web3.utils.BN('0'), 27 | s: new Web3.utils.BN('0'), 28 | } 29 | 30 | const testTx = { 31 | transferRecords: [sampleTr], 32 | signatures: [sig], 33 | } 34 | 35 | module.exports = { 36 | accounts, 37 | testTx, 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2018-2019 Plasma Group 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /bin/plasma-chain-start.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const path = require('path') 3 | const program = require('commander') 4 | const colors = require('colors') // eslint-disable-line no-unused-vars 5 | const appRoot = require('../src/utils.js').appRoot 6 | const getAccount = require('./utils.js').getAccount 7 | const Server = require(appRoot + '/src/server.js').Server 8 | const readConfigFile = require(appRoot + '/src/utils.js').readConfigFile 9 | 10 | const server = new Server() 11 | 12 | program 13 | .command('*') 14 | .description('starts the operator using the first account') 15 | .action(async (none, cmd) => { 16 | const account = await getAccount() 17 | if (account === null) { 18 | return 19 | } 20 | // Start the server! 21 | const configFile = process.env.CONFIG 22 | ? process.env.CONFIG 23 | : path.join(appRoot.toString(), 'config.json') 24 | const config = readConfigFile(configFile) 25 | config.privateKey = account.privateKey 26 | await server.startup(config) 27 | }) 28 | 29 | program.parse(process.argv) 30 | -------------------------------------------------------------------------------- /test/performance-test-signing-and-recovering.js: -------------------------------------------------------------------------------- 1 | function runTest() { 2 | // eslint-disable-line no-unused-vars 3 | const Web3 = require('web3') 4 | const web3 = new Web3('ws://localhost:8546') 5 | const testPrivateKey = 6 | '0x55d5a0faa78131c313390567a8e6efc2a8df714f187549331794f6d805de03db' 7 | 8 | const myAcct = web3.eth.accounts.privateKeyToAccount(testPrivateKey) 9 | 10 | // Generate messages to sign 11 | const msgs = [] 12 | for (let i = 0; i < 1000; i++) { 13 | msgs.push(web3.eth.accounts.hashMessage(i.toString())) 14 | } 15 | 16 | const signingStartTime = +new Date() 17 | const signedMsgs = [] 18 | // Sign messages 19 | for (let msg of msgs) { 20 | signedMsgs.push(myAcct.sign(msg)) 21 | } 22 | const signingEndTime = +new Date() 23 | console.log('Time to sign 1000 txs: ' + (signingEndTime - signingStartTime)) 24 | 25 | console.log(signedMsgs[0]) 26 | 27 | const recoveringStartTime = +new Date() 28 | // Recover messages 29 | for (let signedMsg of signedMsgs) { 30 | web3.eth.accounts.recover(signedMsg) 31 | } 32 | const recoveringEndTime = +new Date() 33 | console.log( 34 | 'Time to recover 1000 txs: ' + (recoveringEndTime - recoveringStartTime) 35 | ) 36 | } 37 | 38 | // runTest() 39 | // On my 3 year old laptop I got: 40 | // Time to sign 1000 txs: 0.896 41 | // Time to recover 1000 txs: 1.888 42 | -------------------------------------------------------------------------------- /bin/plasma-chain-db.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const program = require('commander') 3 | const fs = require('fs') 4 | const path = require('path') 5 | const colors = require('colors') // eslint-disable-line no-unused-vars 6 | const readConfigFile = require('../src/utils.js').readConfigFile 7 | const configFile = process.env.CONFIG 8 | ? process.env.CONFIG 9 | : path.join(__dirname, '..', 'config.json') 10 | const config = readConfigFile(configFile) 11 | program 12 | .command('kill') 13 | .description('Destroy operator database contents (dangerous)') 14 | .action(async () => { 15 | if (!fs.existsSync(config.dbDir)) { 16 | console.error('Operator database does not exist. Exiting.'.red) 17 | } else { 18 | try { 19 | rimraf(config.dbDir) 20 | console.log('Operator database successfully deleted'.green) 21 | } catch (e) { 22 | console.log('Error deleting databse:'.red, e) 23 | } 24 | process.exit() 25 | } 26 | }) 27 | 28 | program.parse(process.argv) 29 | 30 | if (program.args.length === 1) { 31 | console.log( 32 | 'Command not found. Try `' + 33 | 'operator help db'.yellow + 34 | '` for more options' 35 | ) 36 | } 37 | 38 | function rimraf(dir_path) { 39 | fs.readdirSync(dir_path).forEach(function(entry) { 40 | var entry_path = path.join(dir_path, entry) 41 | if (fs.lstatSync(entry_path).isDirectory()) { 42 | rimraf(entry_path) 43 | } else { 44 | fs.unlinkSync(entry_path) 45 | } 46 | }) 47 | fs.rmdirSync(dir_path) 48 | } 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Database 2 | operator-db 3 | operator-db-test 4 | history-db 5 | state-db 6 | operator-keystore 7 | 8 | # Vim 9 | *.swp 10 | 11 | # Logs 12 | logs 13 | *.log 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | 18 | # Runtime data 19 | pids 20 | *.pid 21 | *.seed 22 | *.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | lib-cov 26 | 27 | # Coverage directory used by tools like istanbul 28 | coverage 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # TypeScript v1 declaration files 50 | typings/ 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Optional REPL history 59 | .node_repl_history 60 | 61 | # Output of 'npm pack' 62 | *.tgz 63 | 64 | # Yarn Integrity file 65 | .yarn-integrity 66 | 67 | # dotenv environment variables file 68 | .env 69 | .env.test 70 | 71 | # parcel-bundler cache (https://parceljs.org/) 72 | .cache 73 | 74 | # next.js build output 75 | .next 76 | 77 | # nuxt.js build output 78 | .nuxt 79 | 80 | # vuepress build output 81 | .vuepress/dist 82 | 83 | # Serverless directories 84 | .serverless/ 85 | 86 | # FuseBox cache 87 | .fusebox/ 88 | 89 | # DynamoDB Local files 90 | .dynamodb/ 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deploy your own Plasma Chain 2 | 3 | ## Prerequisites 4 | - `node.js` -- version 11.6.0 5 | - `git` -- version 2.17.1 6 | - Build essentials or similar (specifically `g++` -- version 7.3.1) 7 | 8 | ## Setup 9 | To deploy a new Plasma Chain, use the following commands: 10 | ``` 11 | 0) $ npm install plasma-chain [-g] # install the plasma chain operator. Global flag is optional, if you don't 12 | use global, just replace all of the following commands with `npm run plasma-chain [command]`. If you can't install 13 | globally without `sudo` then just use local! 14 | 15 | 1) $ plasma-chain account new # create a new account 16 | 17 | 2) # On Rinkeby testnet, send your new Operator address ~0.5 ETH. 18 | You can use a faucet to get test ETH for free here: https://faucet.rinkeby.io/ 19 | 20 | 2.5) $ plasma-chain list # list all the plasma chains which others have deployed to the Plasma Network Registry 21 | 22 | 3) $ plasma-chain deploy # deploy a new Plasma Chain. 23 | Note you will be prompted for a unique Plasma Chain name & IP address. 24 | If you are running on your laptop, just set the IP to `0.0.0.0` as you probably don't 25 | want to reveal your IP to the public. However, if you are running in a data center and would 26 | like to accept Plasma transactions & serve a block explorer to the public, go ahead and set an IP. 27 | 28 | 4) $ plasma-chain start # start your new Plasma Chain 29 | You can also view your local block explorer at http:127.0.0.1:8000 30 | 31 | 5) [optional] 32 | Open a new terminal. In this new terminal use the following command: 33 | $ plasma-chain testSwarm # spam your Plasma Chain with tons of test transactions 😁 34 | 35 | ``` 36 | 37 | ### To open your port, you may need to forward traffic from port 80 to port 3000 38 | See: https://coderwall.com/p/plejka/forward-port-80-to-port-3000 39 | -------------------------------------------------------------------------------- /bin/utils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const colors = require('colors') // eslint-disable-line no-unused-vars 4 | const inquirer = require('inquirer') 5 | const appRoot = require('../src/utils.js').appRoot 6 | const Web3 = require('web3') 7 | const KEYSTORE_DIR = require('../src/constants.js').KEYSTORE_DIR 8 | 9 | const web3 = new Web3() 10 | const keystoreDirectory = path.join(appRoot.toString(), KEYSTORE_DIR) 11 | 12 | async function getAccount() { 13 | if (!fs.existsSync(keystoreDirectory)) { 14 | fs.mkdirSync(keystoreDirectory) 15 | } 16 | const accounts = fs.readdirSync(keystoreDirectory) 17 | if (!accounts.length) { 18 | console.log( 19 | 'ERROR:'.red, 20 | 'No account found! Create a new account with `plasma-chain account new`' 21 | ) 22 | return 23 | } 24 | // Check if the account is plaintext 25 | let account = JSON.parse( 26 | fs.readFileSync(path.join(keystoreDirectory, accounts[0])) 27 | ) 28 | if (account.privateKey === undefined) { 29 | // Unlock account 30 | account = await _unlockAccount(account) 31 | if (account === null) { 32 | console.log('Max password attempts reached. Exiting!'.yellow) 33 | return null 34 | } 35 | } 36 | return account 37 | } 38 | 39 | async function _unlockAccount(encryptedAccount) { 40 | let account 41 | for (let i = 0; i < 3; i++) { 42 | const response = await inquirer.prompt([ 43 | { 44 | type: 'password', 45 | name: 'password', 46 | message: 'Passphrase:', 47 | }, 48 | ]) 49 | try { 50 | account = web3.eth.accounts.wallet.decrypt( 51 | [encryptedAccount], 52 | response.password 53 | )['0'] 54 | } catch (err) { 55 | account = null 56 | console.log('Wrong password'.red, 'Please try again', '<3'.red) 57 | } 58 | if (account !== null) { 59 | return account 60 | } 61 | } 62 | return account 63 | } 64 | 65 | module.exports = { 66 | getAccount, 67 | } 68 | -------------------------------------------------------------------------------- /src/event-watcher.js: -------------------------------------------------------------------------------- 1 | const log = require('debug')('info:event-watcher') 2 | 3 | class EventWatcher { 4 | constructor(web3, contract, topic, finalityDepth, pollInterval) { 5 | this.web3 = web3 6 | this.contract = contract 7 | this.topic = topic 8 | this.finalityDepth = finalityDepth 9 | this.pollInterval = pollInterval 10 | this.lastLoggedBlock = 0 11 | } 12 | 13 | subscribe(callback) { 14 | if (this.pollInterval !== undefined) { 15 | this.subscribePolling(callback) 16 | } else { 17 | this.subscribeWebSockets(callback) 18 | } 19 | } 20 | 21 | subscribePolling(callback) { 22 | log('Subscribing to topic by polling') 23 | // Poll for the most recent events every $pollInterval milliseconds 24 | setInterval(async () => { 25 | const block = await this.web3.eth.getBlockNumber() 26 | if (block === this.lastLoggedBlock) { 27 | return 28 | } 29 | log('New block event triggered! Last logged block:', this.lastLoggedBlock) 30 | // Get most recent events 31 | this.contract.getPastEvents( 32 | this.topic, 33 | { 34 | fromBlock: this.lastLoggedBlock + 1, 35 | toBlock: block - this.finalityDepth, 36 | }, 37 | callback 38 | ) 39 | this.lastLoggedBlock = block - this.finalityDepth 40 | }, this.pollInterval) 41 | } 42 | 43 | subscribeWebSockets(callback) { 44 | log('Subscribing to topic with websockets') 45 | this.web3.eth.subscribe('newBlockHeaders', (err, block) => { 46 | if (err) { 47 | throw err 48 | } 49 | log('New block event triggered! Last logged block:', this.lastLoggedBlock) 50 | // Get most recent events 51 | this.contract.getPastEvents( 52 | this.topic, 53 | { 54 | fromBlock: this.lastLoggedBlock + 1, 55 | toBlock: block.number - this.finalityDepth, 56 | }, 57 | callback 58 | ) 59 | this.lastLoggedBlock = block.number - this.finalityDepth 60 | }) 61 | } 62 | } 63 | 64 | module.exports = EventWatcher 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plasma-chain", 3 | "version": "0.0.17", 4 | "description": "Plasma operator & simple REST server", 5 | "main": "index.js", 6 | "scripts": { 7 | "plasma-chain": "./bin/plasma-chain.js", 8 | "clean": "rm -rf db", 9 | "interactive": "node", 10 | "test": "env CONFIG='./config-test.json' NODE_ENV='test' mocha --timeout 200000 --recursive", 11 | "test-debug": "env CONFIG='./config-test.json' NODE_ENV='test' mocha debug --timeout 200000 --recursive", 12 | "lint": "prettier --check '{src,test,bin}/**/*.js'", 13 | "fix": "prettier --write '{src,test,bin}/**/*.js'" 14 | }, 15 | "husky": { 16 | "hooks": { 17 | "pre-commit": "lint-staged" 18 | } 19 | }, 20 | "lint-staged": { 21 | "src/**/*.{js,json,css,md}": [ 22 | "prettier --write", 23 | "git add" 24 | ] 25 | }, 26 | "bin": { 27 | "plasma-chain": "./bin/plasma-chain.js" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "git+https://github.com/plasma-group/plasma-chain-operator.git" 32 | }, 33 | "keywords": [ 34 | "plasma", 35 | "chain", 36 | "operator" 37 | ], 38 | "author": "Plasma Group", 39 | "license": "MIT", 40 | "bugs": { 41 | "url": "https://github.com/plasma-group/plasma-chain-operator/issues" 42 | }, 43 | "homepage": "https://github.com/plasma-group/plasma-chain-operator#readme", 44 | "devDependencies": { 45 | "chai-http": "^4.2.0", 46 | "eslint": "^5.10.0", 47 | "eslint-config-standard": "^12.0.0", 48 | "eslint-plugin-import": "^2.14.0", 49 | "eslint-plugin-node": "^8.0.0", 50 | "eslint-plugin-promise": "^4.0.1", 51 | "eslint-plugin-standard": "^4.0.0", 52 | "husky": "^1.3.1", 53 | "lint-staged": "^8.1.5", 54 | "mocha": "^5.2.0", 55 | "prettier": "^1.16.4" 56 | }, 57 | "dependencies": { 58 | "axios": "^0.18.0", 59 | "body-parser": "^1.18.3", 60 | "chai": "^4.2.0", 61 | "colors": "^1.3.3", 62 | "commander": "^2.19.0", 63 | "cors": "^2.8.5", 64 | "debug": "^4.1.1", 65 | "decimal.js-light": "^2.5.0", 66 | "express": "^4.16.4", 67 | "fs-extra": "^7.0.1", 68 | "inquirer": "^6.2.1", 69 | "leveldown": "^4.0.1", 70 | "levelup": "^3.1.1", 71 | "lodash": "^4.17.11", 72 | "plasma-contracts": "^0.0.4-beta.2", 73 | "plasma-utils": "^0.0.4-beta.2", 74 | "web3": "1.0.0-beta.37" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/test-block-manager/dummy-tx-utils.js: -------------------------------------------------------------------------------- 1 | const models = require('plasma-utils').serialization.models 2 | const Transaction = models.SignedTransaction 3 | const BN = require('web3').utils.BN 4 | 5 | const int32ToHex = (x) => { 6 | x &= 0xffffffff 7 | let hex = x.toString(16).toUpperCase() 8 | return ('0000000000000000' + hex).slice(-16) 9 | } 10 | 11 | /** 12 | * Returns a list of `n` sequential transactions. 13 | * @param {*} n Number of sequential transactions to return. 14 | * @return {*} A list of sequential transactions. 15 | */ 16 | const getSequentialTxs = (n, size, blockNumber) => { 17 | if (blockNumber === undefined) { 18 | blockNumber = 1 19 | } 20 | let txs = [] 21 | 22 | for (let i = 0; i < n; i++) { 23 | txs[i] = new Transaction({ 24 | block: blockNumber, 25 | transfers: [ 26 | { 27 | sender: '0x000000000000000f000000000000000000000000', // random fs here because contract crashes on decoding bytes20 of all zeros to address 28 | recipient: '0x000000000000f000000000000000000000000000', 29 | token: 0, 30 | start: i * size, 31 | end: (i + 1) * size, 32 | }, 33 | ], 34 | signatures: [ 35 | { 36 | v: '1b', 37 | r: 'd693b532a80fed6392b428604171fb32fdbf953728a3a7ecc7d4062b1652c042', 38 | s: '24e9c602ac800b983b035700a14b23f78a253ab762deab5dc27e3555a750b354', 39 | }, 40 | ], 41 | }) 42 | } 43 | 44 | return txs 45 | } 46 | 47 | function genRandomTX(blockNum, senderAddress, recipientAddress, numTransfers) { 48 | let randomTransfers = [] 49 | for (let i = 0; i < numTransfers; i++) { 50 | // fuzz a random encoding to test decoding with 51 | let randomVals = '' 52 | for (let i = 0; i < 28; i++) { 53 | // random start, end, type = 12+12+4 bytes 54 | const randHex = Math.floor(Math.random() * 256) 55 | randomVals += new BN(randHex, 10).toString(16, 2) 56 | } 57 | randomTransfers += 58 | senderAddress.slice(2) + recipientAddress.slice(2) + randomVals 59 | // can't have invalid addresses so ignore this partthe 33rd byte is the numTransfers which isn't random--it's 4 60 | } 61 | return ( 62 | new BN(blockNum).toString(16, 8) + 63 | new BN(numTransfers).toString(16, 2) + 64 | randomTransfers 65 | ) 66 | } 67 | 68 | module.exports = { 69 | int32ToHex: int32ToHex, 70 | getSequentialTxs: getSequentialTxs, 71 | genRandomTX: genRandomTX, 72 | } 73 | -------------------------------------------------------------------------------- /bin/plasma-chain-list.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const path = require('path') 3 | const program = require('commander') 4 | const colors = require('colors') // eslint-disable-line no-unused-vars 5 | const getAccount = require('./utils.js').getAccount 6 | const plasmaRegistryCompiled = require('plasma-contracts') 7 | .plasmaRegistryCompiled 8 | const Web3 = require('web3') 9 | const readConfigFile = require('../src/utils.js').readConfigFile 10 | 11 | program 12 | .description('lists all Plasma chains in the current registry') 13 | .option( 14 | '-n, --newRegistry', 15 | 'Deploy a new Plasma Network in addition to a Plasma Chain' 16 | ) 17 | .action(async (none, cmd) => { 18 | const account = await getAccount() 19 | if (account === null) { 20 | return 21 | } 22 | // Get the config 23 | const configFile = process.env.CONFIG 24 | ? process.env.CONFIG 25 | : path.join(__dirname, '..', 'config.json') 26 | console.log('Reading config file from:', configFile) 27 | const config = readConfigFile(configFile) 28 | if (config.plasmaRegistryAddress === undefined) { 29 | console.log('Error:'.red, 'No Plasma Registry specified!') 30 | return 31 | } 32 | const web3 = new Web3() 33 | web3.setProvider(new Web3.providers.HttpProvider(config.web3HttpProvider)) 34 | const plasmaRegistryCt = new web3.eth.Contract( 35 | plasmaRegistryCompiled.abi, 36 | config.plasmaRegistryAddress 37 | ) 38 | plasmaRegistryCt.getPastEvents( 39 | 'NewPlasmaChain', 40 | { 41 | fromBlock: 0, 42 | toBlock: 'latest', 43 | }, 44 | (err, res) => { 45 | if (err) { 46 | console.log('Error! Try checking your network') 47 | console.log(err) 48 | } 49 | console.log( 50 | '~~~~~~~~~~~~~~~~~~~'.rainbow, 51 | '\nList of Plasma Chains Deployed to the registry:'.white.bold, 52 | config.plasmaRegistryAddress.white.bold 53 | ) 54 | for (const ethEvent of res) { 55 | const plasmaChainAddress = ethEvent.returnValues['0'] 56 | const operatorAddress = ethEvent.returnValues['0'] 57 | const name = Buffer.from( 58 | ethEvent.returnValues['2'].slice(2), 59 | 'hex' 60 | ).toString('utf8') 61 | const ip = Buffer.from( 62 | ethEvent.returnValues['3'].slice(2), 63 | 'hex' 64 | ).toString('utf8') 65 | console.log(` 66 | ${'~~~~~~~~~~~~~~~~~~~'.rainbow} 67 | Chain Name: ${name.white.bold} 68 | Operator Address: ${operatorAddress.white.bold} 69 | Plasma Chain Address: ${plasmaChainAddress.white.bold} 70 | Chain IP: ${ip.white.bold} 71 | `) 72 | } 73 | } 74 | ) 75 | }) 76 | 77 | program.parse(process.argv) 78 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | // Constants 2 | const INIT_METHOD = 'init' 3 | const DEPOSIT_METHOD = 'deposit' 4 | const ADD_TX_METHOD = 'addTransaction' 5 | const GET_TXS_METHOD = 'getTransactions' 6 | const GET_HISTORY_PROOF = 'getHistoryProof' 7 | const NEW_BLOCK_METHOD = 'newBlock' 8 | const GET_RECENT_TXS_METHOD = 'getRecentTransactions' 9 | const GET_BLOCK_NUMBER_METHOD = 'getBlockNumber' 10 | const GET_BLOCK_TXS_METHOD = 'getBlockTransactions' 11 | const GET_BLOCK_METADATA_METHOD = 'getBlockMetadata' 12 | const GET_TX_FROM_HASH_METHOD = 'getTxFromHash' 13 | const GET_ETH_INFO_METHOD = 'getEthInfo' 14 | const ADDRESS_BYTE_SIZE = 20 15 | const START_BYTE_SIZE = 12 16 | const TYPE_BYTE_SIZE = 4 17 | const COIN_ID_BYTE_SIZE = START_BYTE_SIZE + TYPE_BYTE_SIZE 18 | const BLOCKNUMBER_BYTE_SIZE = 4 19 | const DEPOSIT_SENDER = '0x0000000000000000000000000000000000000000' 20 | // For now, include a constant which defines the total size of a transaction 21 | const TRANSFER_BYTE_SIZE = 22 | ADDRESS_BYTE_SIZE * 2 + TYPE_BYTE_SIZE + START_BYTE_SIZE * 2 23 | const SIGNATURE_BYTE_SIZE = 1 + 32 * 2 24 | // DB Prefixes 25 | // State Manager 26 | const COIN_ID_PREFIX = Buffer.from([128]) 27 | const ADDRESS_PREFIX = Buffer.from([127]) 28 | const DEPOSIT_PREFIX = Buffer.from([126]) 29 | // Block Manager 30 | const BLOCK_TX_PREFIX = Buffer.from([255]) 31 | const BLOCK_DEPOSIT_PREFIX = Buffer.from([254]) 32 | const BLOCK_INDEX_PREFIX = Buffer.from([253]) 33 | const BLOCK_ROOT_HASH_PREFIX = Buffer.from([252]) 34 | const NUM_LEVELS_PREFIX = Buffer.from([251]) // The number of levels in a particular block 35 | const NODE_DB_PREFIX = Buffer.from([250]) 36 | const BLOCK_NUM_TXS_PREFIX = Buffer.from([249]) 37 | const BLOCK_TIMESTAMP_PREFIX = Buffer.from([248]) 38 | const HASH_TO_TX_PREFIX = Buffer.from([247]) 39 | // DB 40 | const ETH_DB_FILENAME = 'eth-config.json' 41 | const TEST_DB_DIR = './operator-db-test/' 42 | const KEYSTORE_DIR = 'operator-keystore' 43 | 44 | const STATE_METHODS = [ 45 | DEPOSIT_METHOD, 46 | ADD_TX_METHOD, 47 | NEW_BLOCK_METHOD, 48 | GET_BLOCK_NUMBER_METHOD, 49 | GET_TXS_METHOD, 50 | GET_RECENT_TXS_METHOD 51 | ] 52 | 53 | const BLOCK_METHODS = [ 54 | GET_HISTORY_PROOF, 55 | GET_BLOCK_METADATA_METHOD, 56 | GET_TX_FROM_HASH_METHOD, 57 | GET_BLOCK_TXS_METHOD 58 | ] 59 | 60 | module.exports = { 61 | STATE_METHODS, 62 | BLOCK_METHODS, 63 | 64 | INIT_METHOD, 65 | DEPOSIT_METHOD, 66 | NEW_BLOCK_METHOD, 67 | ADD_TX_METHOD, 68 | GET_TXS_METHOD, 69 | GET_BLOCK_NUMBER_METHOD, 70 | GET_RECENT_TXS_METHOD, 71 | GET_BLOCK_TXS_METHOD, 72 | GET_BLOCK_METADATA_METHOD, 73 | GET_TX_FROM_HASH_METHOD, 74 | GET_ETH_INFO_METHOD, 75 | GET_HISTORY_PROOF, 76 | START_BYTE_SIZE, 77 | TYPE_BYTE_SIZE, 78 | COIN_ID_BYTE_SIZE, 79 | BLOCKNUMBER_BYTE_SIZE, 80 | TRANSFER_BYTE_SIZE, 81 | DEPOSIT_SENDER, 82 | COIN_ID_PREFIX, 83 | ADDRESS_PREFIX, 84 | DEPOSIT_PREFIX, 85 | BLOCK_TX_PREFIX, 86 | BLOCK_DEPOSIT_PREFIX, 87 | BLOCK_ROOT_HASH_PREFIX, 88 | BLOCK_NUM_TXS_PREFIX, 89 | BLOCK_TIMESTAMP_PREFIX, 90 | BLOCK_INDEX_PREFIX, 91 | NUM_LEVELS_PREFIX, 92 | NODE_DB_PREFIX, 93 | HASH_TO_TX_PREFIX, 94 | SIGNATURE_BYTE_SIZE, 95 | ETH_DB_FILENAME, 96 | KEYSTORE_DIR, 97 | TEST_DB_DIR, 98 | } 99 | -------------------------------------------------------------------------------- /bin/plasma-chain.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const program = require('commander') 3 | const colors = require('colors') // eslint-disable-line no-unused-vars 4 | const appRoot = require('../src/utils.js').appRoot 5 | const readConfigFile = require('../src/utils.js').readConfigFile 6 | const path = require('path') 7 | 8 | const configFile = process.env.CONFIG 9 | ? process.env.CONFIG 10 | : path.join(appRoot.toString(), 'config.json') 11 | const config = readConfigFile(configFile) 12 | const isRinkeby = config.web3HttpProvider.includes('rinkeby') 13 | 14 | const rinkebyStep2 = `# On Rinkeby testnet, send your new Operator address ~0.5 ETH. 15 | You can use a faucet to get test ETH for free here: ${ 16 | 'https://faucet.rinkeby.io/'.green 17 | }` 18 | const rinkebyStep3 = `${ 19 | '$ plasma-chain deploy'.green 20 | } # deploys a new Plasma Chain. 21 | Note you will be prompted for a unique Plasma Chain name & IP address. 22 | If you are running on your laptop, just set the IP to \`0.0.0.0\` as you probably don't 23 | want to reveal your IP to the public. However, if you are running in a data center and would 24 | like to accept Plasma transactions & serve a block explorer to the public, go ahead and set an IP.` 25 | const customStep2 = 26 | '# On your Ethereum node, send your new Operator address ~0.5 ETH' 27 | const customStep3 = `${ 28 | '$ plasma-chain deploy [-n]'.green 29 | } # deploys a new Plasma Chain. If you want 30 | to also deploy a new Plasma Network Registry, use the \`-n\` flag. Note you will be prompted for a unique 31 | Plasma Chain name & IP address (public to the Ethereum chain you deploy to)` 32 | 33 | const introText = ` 34 | ${'~~~~~~~~~plasma~~~~~~~~~chain~~~~~~~~~operator~~~~~~~~~'.rainbow} 35 | ${'Deploy a new Plasma Chain in just a couple commands.'.white.bold} 🤞😁 36 | 37 | Note that Plasma Chains require a constant connection to the Ethereum network. 38 | You can set your Ethereum node in your config file located: ${ 39 | configFile.toString().yellow 40 | } 41 | (All configs & DB files are located in this directory--I promise I won't pollute your home directory!) 42 | Right now your Web3 HTTP provider is set to: ${config.web3HttpProvider.blue} 43 | 44 | To deploy a new Plasma Chain, use the following commands: 45 | 46 | 1) ${'$ plasma-chain account new'.green} # create a new account 47 | 48 | 2) ${isRinkeby ? rinkebyStep2 : customStep2} 49 | 50 | 3) ${isRinkeby ? rinkebyStep3 : customStep3} 51 | 52 | 4) ${'$ plasma-chain start'.green} # start your new Plasma Chain 53 | You can also view your local block explorer at http:127.0.0.1:8000 54 | 55 | [optional] 56 | 5) ${ 57 | '$ plasma-chain testSwarm'.green 58 | } # spam your Plasma Chain with tons of test transactions 😁 59 | ${'WARNING: This is experimental software--use it carefully!'.yellow} 60 | 61 | ${'<3'.red} 62 | ` 63 | 64 | program 65 | .version('0.0.1') 66 | .description(introText) 67 | .command('deploy', 'deploy a new plasma chain') 68 | .command('start', 'start the operator') 69 | .command('account [cmd]', 'manage accounts') 70 | .command('db [cmd]', 'manage local database') 71 | .command('testSwarm', 'start a swarm of test nodes') 72 | .command('list', 'list all Plasma Chains on the Registry') 73 | .action((command) => {}) 74 | .parse(process.argv) 75 | -------------------------------------------------------------------------------- /test/test-utils.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | const chai = require('chai') 4 | const expect = chai.expect 5 | const utils = require('../src/utils.js') 6 | const BN = require('web3').utils.BN 7 | 8 | function bn(number) { 9 | return new BN(number) 10 | } 11 | 12 | describe('utils', function() { 13 | describe('addRange', () => { 14 | it('adds ranges and merges if possible', async () => { 15 | const addRange = utils.addRange 16 | const rangeList = [ 17 | bn(0), 18 | bn(2), 19 | bn(6), 20 | bn(11), 21 | bn(15), 22 | bn(18), 23 | bn(20), 24 | bn(21), 25 | ] 26 | addRange(rangeList, bn(5), bn(6)) 27 | expect(rangeList).to.deep.equal([ 28 | bn(0), 29 | bn(2), 30 | bn(5), 31 | bn(11), 32 | bn(15), 33 | bn(18), 34 | bn(20), 35 | bn(21), 36 | ]) 37 | addRange(rangeList, bn(3), bn(4)) 38 | expect(rangeList).to.deep.equal([ 39 | bn(0), 40 | bn(2), 41 | bn(3), 42 | bn(4), 43 | bn(5), 44 | bn(11), 45 | bn(15), 46 | bn(18), 47 | bn(20), 48 | bn(21), 49 | ]) 50 | addRange(rangeList, bn(2), bn(3)) 51 | expect(rangeList).to.deep.equal([ 52 | bn(0), 53 | bn(4), 54 | bn(5), 55 | bn(11), 56 | bn(15), 57 | bn(18), 58 | bn(20), 59 | bn(21), 60 | ]) 61 | addRange(rangeList, bn(4), bn(5)) 62 | expect(rangeList).to.deep.equal([ 63 | bn(0), 64 | bn(11), 65 | bn(15), 66 | bn(18), 67 | bn(20), 68 | bn(21), 69 | ]) 70 | addRange(rangeList, bn(18), bn(20)) 71 | expect(rangeList).to.deep.equal([bn(0), bn(11), bn(15), bn(21)]) 72 | addRange(rangeList, bn(11), bn(15)) 73 | expect(rangeList).to.deep.equal([bn(0), bn(21)]) 74 | }) 75 | }) 76 | 77 | describe('subtractRange', () => { 78 | it('removes ranges & splits them up if needed', async () => { 79 | const subtractRange = utils.subtractRange 80 | const rangeList = [ 81 | bn(0), 82 | bn(4), 83 | bn(6), 84 | bn(11), 85 | bn(15), 86 | bn(18), 87 | bn(18), 88 | bn(19), 89 | ] 90 | subtractRange(rangeList, bn(0), bn(4)) 91 | expect(rangeList).to.deep.equal([ 92 | bn(6), 93 | bn(11), 94 | bn(15), 95 | bn(18), 96 | bn(18), 97 | bn(19), 98 | ]) 99 | subtractRange(rangeList, bn(18), bn(19)) 100 | expect(rangeList).to.deep.equal([bn(6), bn(11), bn(15), bn(18)]) 101 | subtractRange(rangeList, bn(7), bn(8)) 102 | expect(rangeList).to.deep.equal([ 103 | bn(6), 104 | bn(7), 105 | bn(8), 106 | bn(11), 107 | bn(15), 108 | bn(18), 109 | ]) 110 | subtractRange(rangeList, bn(15), bn(18)) 111 | expect(rangeList).to.deep.equal([bn(6), bn(7), bn(8), bn(11)]) 112 | subtractRange(rangeList, bn(6), bn(7)) 113 | expect(rangeList).to.deep.equal([bn(8), bn(11)]) 114 | subtractRange(rangeList, bn(9), bn(10)) 115 | expect(rangeList).to.deep.equal([bn(8), bn(9), bn(10), bn(11)]) 116 | subtractRange(rangeList, bn(8), bn(9)) 117 | expect(rangeList).to.deep.equal([bn(10), bn(11)]) 118 | subtractRange(rangeList, bn(10), bn(11)) 119 | expect(rangeList).to.deep.equal([]) 120 | }) 121 | }) 122 | }) 123 | -------------------------------------------------------------------------------- /bin/plasma-chain-account.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const fs = require('fs') 3 | const path = require('path') 4 | const program = require('commander') 5 | const colors = require('colors') // eslint-disable-line no-unused-vars 6 | const inquirer = require('inquirer') 7 | const appRoot = require('../src/utils.js').appRoot 8 | const Web3 = require('web3') 9 | const KEYSTORE_DIR = require('../src/constants.js').KEYSTORE_DIR 10 | 11 | const web3 = new Web3() 12 | const keystoreDirectory = path.join(appRoot.toString(), KEYSTORE_DIR) 13 | 14 | program 15 | .command('new') 16 | .description('creates a new account') 17 | .option('-p, --plaintext', 'Store the private key in plaintext') 18 | .action(async (none, cmd) => { 19 | if (!fs.existsSync(keystoreDirectory)) { 20 | fs.mkdirSync(keystoreDirectory) 21 | } 22 | // Check that there are no accounts already -- no reason to have more than one operator account 23 | const accounts = fs.readdirSync(keystoreDirectory) 24 | if (accounts.length) { 25 | console.log( 26 | 'Account already created! Try starting the operator with `operator start`' 27 | ) 28 | return 29 | } 30 | // There are no accounts so create one! 31 | const newAccount = await createAccount(cmd.plaintext) 32 | if (newAccount === undefined) { 33 | return 34 | } 35 | console.log('Created new account with address:', newAccount.address.green) 36 | const keystorePath = path.join( 37 | keystoreDirectory, 38 | new Date().toISOString() + '--' + newAccount.address 39 | ) 40 | console.log('Saving account to:', keystorePath.yellow) 41 | fs.writeFileSync(keystorePath, newAccount.keystoreFile) 42 | // Create new password file 43 | }) 44 | 45 | program 46 | .command('list') 47 | .description('list all accounts') 48 | .action((none, cmd) => { 49 | let counter = 0 50 | if (!fs.existsSync(keystoreDirectory)) { 51 | console.log('No accounts found!') 52 | return 53 | } 54 | fs.readdirSync(keystoreDirectory).forEach((file) => { 55 | console.log('Account #' + counter++ + ':', file.split('--')[1]) 56 | }) 57 | }) 58 | 59 | async function createAccount(isPlaintext) { 60 | if (isPlaintext) { 61 | const newAccount = web3.eth.accounts.create() 62 | return { 63 | address: newAccount.address, 64 | keystoreFile: JSON.stringify(newAccount), 65 | } 66 | } 67 | // If it's not plaintext, we need to prompt for password 68 | console.log( 69 | 'Your new account is locked with a password. Please give a password. Do not forget this password.' 70 | ) 71 | const response = await inquirer.prompt([ 72 | { 73 | type: 'password', 74 | name: 'password', 75 | message: 'Passphrase:', 76 | }, 77 | { 78 | type: 'password', 79 | name: 'retypePassword', 80 | message: 'Repeat passphrase:', 81 | }, 82 | ]) 83 | if (response.password !== response.retypePassword) { 84 | console.log('Passwords do not match! Try again...'.red) 85 | return 86 | } 87 | const newAccount = web3.eth.accounts.create() 88 | const encryptedAccount = newAccount.encrypt(response.password) 89 | return { 90 | address: newAccount.address, 91 | keystoreFile: JSON.stringify(encryptedAccount), 92 | } 93 | } 94 | 95 | program.parse(process.argv) 96 | 97 | if (program.args.length === 1) { 98 | console.log( 99 | 'Command not found. Try `' + 100 | 'operator help account'.yellow + 101 | '` for more options' 102 | ) 103 | } 104 | -------------------------------------------------------------------------------- /bin/plasma-chain-testSwarm.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const program = require('commander') 3 | const colors = require('colors') // eslint-disable-line no-unused-vars 4 | const constants = require('../src/constants.js') 5 | const UnsignedTransaction = require('plasma-utils').serialization.models 6 | .UnsignedTransaction 7 | const Web3 = require('web3') 8 | const BN = require('web3').utils.BN 9 | const axios = require('axios') 10 | const accounts = require('../test/mock-accounts.js').accounts 11 | const MockNode = require('../src/mock-node.js') 12 | 13 | const timeout = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) 14 | 15 | // Get the config 16 | const http = axios.create({ 17 | baseURL: 'http://localhost:3000', 18 | }) 19 | 20 | let idCounter 21 | let totalDeposits = {} 22 | 23 | // Operator object wrapper to query api 24 | const operator = { 25 | addTransaction: async (tx) => { 26 | const encodedTx = tx.encoded 27 | let txResponse 28 | txResponse = await http.post('/api', { 29 | method: constants.ADD_TX_METHOD, 30 | jsonrpc: '2.0', 31 | id: idCounter++, 32 | params: [encodedTx], 33 | }) 34 | return txResponse.data 35 | }, 36 | addDeposit: async (recipient, token, amount) => { 37 | // First calculate start and end from token amount 38 | const tokenTypeKey = token.toString() 39 | if (totalDeposits[tokenTypeKey] === undefined) { 40 | totalDeposits[tokenTypeKey] = new BN(0) 41 | } 42 | const start = new BN(totalDeposits[tokenTypeKey]) 43 | totalDeposits[tokenTypeKey] = new BN( 44 | totalDeposits[tokenTypeKey].add(amount) 45 | ) 46 | const end = new BN(totalDeposits[tokenTypeKey]) 47 | let txResponse 48 | txResponse = await http.post('/api', { 49 | method: constants.DEPOSIT_METHOD, 50 | jsonrpc: '2.0', 51 | id: idCounter++, 52 | params: { 53 | recipient: Web3.utils.bytesToHex(recipient), 54 | token: token.toString(16), 55 | start: start.toString(16), 56 | end: end.toString(16), 57 | }, 58 | }) 59 | return new UnsignedTransaction(txResponse.data.deposit) 60 | }, 61 | getBlockNumber: async () => { 62 | const response = await http.post('/api', { 63 | method: constants.GET_BLOCK_NUMBER_METHOD, 64 | jsonrpc: '2.0', 65 | id: idCounter++, 66 | params: [], 67 | }) 68 | console.log( 69 | 'Sending transactions for block:', 70 | new BN(response.data.result, 10).toString(10).green 71 | ) 72 | return new BN(response.data.result, 10) 73 | }, 74 | } 75 | 76 | program 77 | .command('*') 78 | .description('starts a swarm of test nodes for load testing') 79 | .action(async (none, cmd) => { 80 | const nodes = [] 81 | for (const acct of accounts) { 82 | nodes.push(new MockNode(operator, acct, nodes)) 83 | } 84 | // Add deposits -- it will be a random token above token type 1000 to avoid overlap with real deposits... 85 | const depositType = new BN(999 + Math.floor(Math.random() * 10000)) 86 | const depositAmount = new BN(10000000, 'hex') 87 | for (const node of nodes) { 88 | await node.deposit(depositType, depositAmount) 89 | } 90 | const startTime = +new Date() 91 | // Transact on a looonng loooop! 92 | for (let i = 0; i < 100000; i++) { 93 | // Get the current block number 94 | const blockNumber = await operator.getBlockNumber() 95 | const promises = [] 96 | for (const node of nodes) { 97 | promises.push(node.sendRandomTransaction(blockNumber, 2, true)) 98 | } 99 | await Promise.all(promises) 100 | console.log('sleeping...') 101 | await timeout(Math.floor(Math.random() * 1000) + 10) 102 | } 103 | console.log('Total time:', +new Date() - startTime) 104 | }) 105 | 106 | program.parse(process.argv) 107 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure that tests pass and code is lint free: `npm test` 11 | 2. Update the README.md if any changes invalidate its current content. 12 | 3. Include any tests for new functionality. 13 | 4. Reference any revelant issues in your PR comment. 14 | 15 | ## Code of Conduct 16 | 17 | ### Our Pledge 18 | 19 | In the interest of fostering an open and welcoming environment, we as 20 | contributors and maintainers pledge to making participation in our project and 21 | our community a harassment-free experience for everyone, regardless of age, body 22 | size, disability, ethnicity, gender identity and expression, level of experience, 23 | nationality, personal appearance, race, religion, or sexual identity and 24 | orientation. 25 | 26 | ### Our Standards 27 | 28 | Examples of behavior that contributes to creating a positive environment 29 | include: 30 | 31 | * Using welcoming and inclusive language 32 | * Being respectful of differing viewpoints and experiences 33 | * Gracefully accepting constructive criticism 34 | * Focusing on what is best for the community 35 | * Showing empathy towards other community members 36 | 37 | Examples of unacceptable behavior by participants include: 38 | 39 | * The use of sexualized language or imagery and unwelcome sexual attention or 40 | advances 41 | * Trolling, insulting/derogatory comments, and personal or political attacks 42 | * Public or private harassment 43 | * Publishing others' private information, such as a physical or electronic 44 | address, without explicit permission 45 | * Other conduct which could reasonably be considered inappropriate in a 46 | professional setting 47 | 48 | ### Our Responsibilities 49 | 50 | Project maintainers are responsible for clarifying the standards of acceptable 51 | behavior and are expected to take appropriate and fair corrective action in 52 | response to any instances of unacceptable behavior. 53 | 54 | Project maintainers have the right and responsibility to remove, edit, or 55 | reject comments, commits, code, wiki edits, issues, and other contributions 56 | that are not aligned to this Code of Conduct, or to ban temporarily or 57 | permanently any contributor for other behaviors that they deem inappropriate, 58 | threatening, offensive, or harmful. 59 | 60 | ### Scope 61 | 62 | This Code of Conduct applies both within project spaces and in public spaces 63 | when an individual is representing the project or its community. Examples of 64 | representing a project or community include using an official project e-mail 65 | address, posting via an official social media account, or acting as an appointed 66 | representative at an online or offline event. Representation of a project may be 67 | further defined and clarified by project maintainers. 68 | 69 | ### Enforcement 70 | 71 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 72 | reported by contacting the project team at contributing@plasma.group. All 73 | complaints will be reviewed and investigated and will result in a response that 74 | is deemed necessary and appropriate to the circumstances. The project team is 75 | obligated to maintain confidentiality with regard to the reporter of an incident. 76 | Further details of specific enforcement policies may be posted separately. 77 | 78 | Project maintainers who do not follow or enforce the Code of Conduct in good 79 | faith may face temporary or permanent repercussions as determined by other 80 | members of the project's leadership. 81 | 82 | ### Attribution 83 | 84 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 85 | available at [http://contributor-covenant.org/version/1/4][version] and from the [Angular Seed Contributing Guide][angular-contrib]. 86 | 87 | [homepage]: http://contributor-covenant.org 88 | [version]: http://contributor-covenant.org/version/1/4/ 89 | [angular-contrib]: https://github.com/mgechev/angular-seed/blob/master/.github/CONTRIBUTING.md -------------------------------------------------------------------------------- /test/test-state-manager/load-test-state.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | const fs = require('fs') 4 | const chai = require('chai') 5 | const accounts = require('../mock-accounts.js').accounts 6 | const web3 = require('web3') 7 | const BN = web3.utils.BN 8 | const State = require('../../src/state-manager/state.js') 9 | const levelup = require('levelup') 10 | const leveldown = require('leveldown') 11 | const log = require('debug')('test:info:load-test-state') 12 | const MockNode = require('../../src/mock-node.js') 13 | const TEST_DB_DIR = require('../../src/constants.js').TEST_DB_DIR 14 | 15 | const expect = chai.expect // eslint-disable-line no-unused-vars 16 | 17 | let state 18 | let totalDeposits 19 | 20 | const operator = { 21 | addDeposit: (address, tokenType, amount) => { 22 | const tokenTypeKey = tokenType.toString() 23 | if (totalDeposits[tokenTypeKey] === undefined) { 24 | log('Adding new token type to our total deposits store') 25 | totalDeposits[tokenTypeKey] = new BN(0) 26 | } 27 | const start = new BN(totalDeposits[tokenTypeKey]) 28 | totalDeposits[tokenTypeKey] = new BN( 29 | totalDeposits[tokenTypeKey].add(amount) 30 | ) 31 | const end = new BN(totalDeposits[tokenTypeKey]) 32 | return state.addDeposit(address, tokenType, start, end) 33 | }, 34 | addTransaction: (tx) => { 35 | return state.addTransaction(tx) 36 | }, 37 | getBlockNumber: async () => { 38 | return state.blockNumber 39 | }, 40 | } 41 | 42 | describe('State', function() { 43 | let db 44 | const startNewDB = async () => { 45 | const dbDir = TEST_DB_DIR 46 | if (!fs.existsSync(dbDir)) { 47 | log('Creating a new db directory because it does not exist') 48 | fs.mkdirSync(dbDir) 49 | } 50 | db = levelup(leveldown(dbDir + +new Date())) 51 | // Create a new tx-log dir for this test 52 | const txLogDirectory = dbDir + +new Date() + '-tx-log/' 53 | fs.mkdirSync(txLogDirectory) 54 | // Create state object 55 | state = new State(db, txLogDirectory, () => true) 56 | totalDeposits = {} 57 | await state.init() 58 | } 59 | beforeEach(startNewDB) 60 | 61 | describe('Mock node swarm', () => { 62 | it('accepts many deposits from the mock node swarm', (done) => { 63 | // const accts = accounts.slice(0, 2) 64 | const depositType = new BN(0) 65 | const depositAmount = new BN(256) 66 | const nodes = [] 67 | for (const acct of accounts) { 68 | nodes.push(new MockNode(operator, acct, nodes)) 69 | } 70 | const depositPromises = [] 71 | // Add deposits from 100 different accounts 72 | for (const node of nodes) { 73 | depositPromises.push(node.deposit(depositType, depositAmount)) 74 | } 75 | Promise.all(depositPromises).then((res) => { 76 | // Send txs! 77 | mineAndLoopSendRandomTxs(100, operator, nodes).then(() => { 78 | // 10000 79 | done() 80 | }) 81 | }) 82 | }) 83 | 84 | it('should work with one massive block', (done) => { 85 | // const accts = accounts.slice(0, 2) 86 | const depositType = new BN(1) 87 | const depositAmount = new BN('1000000') 88 | const nodes = [] 89 | for (const acct of accounts) { 90 | // for (const acct of accts) { 91 | nodes.push(new MockNode(operator, acct, nodes)) 92 | } 93 | // Add deposits from 100 different accounts 94 | const depositPromises = [] 95 | for (const node of nodes) { 96 | depositPromises.push(node.deposit(depositType, depositAmount)) 97 | } 98 | Promise.all(depositPromises) 99 | .catch((err) => { 100 | console.log(err) 101 | }) 102 | .then((res) => { 103 | // For some number of rounds, have every node send a random transaction 104 | loopSendRandomTxs(100, operator, nodes).then(() => { 105 | done() 106 | }) 107 | }) 108 | }) 109 | }) 110 | }) 111 | 112 | async function mineAndLoopSendRandomTxs(numTimes, operator, nodes) { 113 | for (let i = 0; i < numTimes; i++) { 114 | const blockNumber = await operator.getBlockNumber() 115 | await sendRandomTransactions(operator, nodes, blockNumber) 116 | log('Starting new block...') 117 | await state.startNewBlock() 118 | log('Sending new txs for block number:', blockNumber.toString()) 119 | for (const node of nodes) { 120 | node.processPendingRanges() 121 | } 122 | } 123 | } 124 | 125 | async function loopSendRandomTxs(numTimes, operator, nodes) { 126 | const blockNumber = await operator.getBlockNumber() 127 | for (let i = 0; i < numTimes; i++) { 128 | await sendRandomTransactions(operator, nodes, blockNumber, 1, 10) 129 | } 130 | await state.startNewBlock() 131 | } 132 | 133 | function sendRandomTransactions(operator, nodes, blockNumber, rounds, maxSize) { 134 | if (rounds === undefined) rounds = 1 135 | const randomTxPromises = [] 136 | for (let i = 0; i < rounds; i++) { 137 | for (const node of nodes) { 138 | randomTxPromises.push(node.sendRandomTransaction(blockNumber, maxSize)) 139 | } 140 | log('Starting round:', i) 141 | } 142 | // log('promises:', randomTxPromises) 143 | return Promise.all(randomTxPromises) 144 | } 145 | -------------------------------------------------------------------------------- /src/state-manager/app.js: -------------------------------------------------------------------------------- 1 | const levelup = require('levelup') 2 | const leveldown = require('leveldown') 3 | const State = require('./state.js') 4 | const Web3 = require('web3') 5 | const constants = require('../constants.js') 6 | const BN = Web3.utils.BN 7 | const log = require('debug')('info:state-app') 8 | const error = require('debug')('ERROR:state-app') 9 | const models = require('plasma-utils').serialization.models 10 | const SignedTransaction = models.SignedTransaction 11 | 12 | // Create global state object 13 | let state 14 | 15 | async function startup(options) { 16 | const db = levelup(leveldown(options.stateDBDir)) 17 | state = new State(db, options.txLogDir) 18 | await state.init() 19 | } 20 | 21 | process.on('message', async (m) => { 22 | log( 23 | 'INCOMING request with method:', 24 | m.message.method, 25 | 'and rpcID:', 26 | m.message.id 27 | ) 28 | // ******* INIT ******* // 29 | if (m.message.method === constants.INIT_METHOD) { 30 | await startup(m.message.params) 31 | process.send({ ipcID: m.ipcID, message: { startup: 'SUCCESS' } }) 32 | return 33 | // ******* NEW_BLOCK ******* // 34 | } else if (m.message.method === constants.NEW_BLOCK_METHOD) { 35 | let response 36 | try { 37 | const blockNumber = await state.startNewBlock() 38 | response = { newBlockNumber: blockNumber.toString() } 39 | } catch (err) { 40 | error( 41 | 'Error in new block!\nrpcID:', 42 | m.message.id, 43 | '\nError message:', 44 | err, 45 | '\n' 46 | ) 47 | response = { error: err } 48 | } 49 | log('OUTGOING new block success with rpcID:', m.message.id) 50 | process.send({ ipcID: m.ipcID, message: { result: response } }) 51 | return 52 | // ******* GET_BLOCK_NUMBER ******* // 53 | } else if (m.message.method === constants.GET_BLOCK_NUMBER_METHOD) { 54 | const blockNumber = state.blockNumber 55 | log('OUTGOING new block success with rpcID:', m.message.id) 56 | process.send({ 57 | ipcID: m.ipcID, 58 | message: { result: blockNumber.toString() }, 59 | }) 60 | return 61 | // ******* DEPOSIT ******* // 62 | } else if (m.message.method === constants.DEPOSIT_METHOD) { 63 | const deposit = await newDepositCallback(null, { 64 | recipient: Buffer.from(Web3.utils.hexToBytes(m.message.params.recipient)), 65 | token: new BN(m.message.params.token, 16), 66 | start: new BN(m.message.params.start, 16), 67 | end: new BN(m.message.params.end, 16), 68 | }) 69 | log('OUTGOING new deposit with rpcID:', m.message.id) 70 | process.send({ ipcID: m.ipcID, message: { deposit } }) 71 | return 72 | // ******* ADD_TX ******* // 73 | } else if (m.message.method === constants.ADD_TX_METHOD) { 74 | const tx = new SignedTransaction(m.message.params[0]) 75 | let txResponse 76 | try { 77 | const addTxResult = await state.addTransaction(tx) 78 | txResponse = { result: addTxResult } 79 | } catch (err) { 80 | error( 81 | 'Error in adding transaction!\nrpcID:', 82 | m.message.id, 83 | '\nError message:', 84 | err, 85 | '\n' 86 | ) 87 | txResponse = { error: err } 88 | } 89 | log('OUTGOING addTransaction response with rpcID:', m.message.id) 90 | process.send({ ipcID: m.ipcID, message: txResponse }) 91 | return 92 | // ******* GET_TXS ******* // 93 | } else if (m.message.method === constants.GET_TXS_METHOD) { 94 | let response 95 | try { 96 | const [address, startBlock, endBlock] = m.message.params 97 | const getTxResult = Array.from( 98 | await state.getTransactions(address, startBlock, endBlock) 99 | ) 100 | response = { result: getTxResult } 101 | } catch (err) { 102 | error( 103 | 'Error in getting past transactions!\nrpcID:', 104 | m.message.id, 105 | '\nError message:', 106 | err, 107 | '\n' 108 | ) 109 | response = { error: err } 110 | } 111 | log('OUTGOING getTransactions response with rpcID:', m.message.id) 112 | process.send({ ipcID: m.ipcID, message: response }) 113 | return 114 | // ******* GET_RECENT_TXS ******* // 115 | } else if (m.message.method === constants.GET_RECENT_TXS_METHOD) { 116 | let response 117 | try { 118 | const [start, end] = m.message.params 119 | const recentTransactions = await state.getRecentTransactions(start, end) 120 | response = { result: recentTransactions } 121 | } catch (err) { 122 | error( 123 | 'Error getting recent transactions!\nrpcID:', 124 | m.message.id, 125 | '\nError message:', 126 | err, 127 | '\n' 128 | ) 129 | response = { error: err } 130 | } 131 | log('OUTGOING getRecentTransactions response with rpcID:', m.message.id) 132 | process.send({ ipcID: m.ipcID, message: response }) 133 | return 134 | } 135 | process.send({ 136 | ipcID: m.ipcID, 137 | message: { error: 'RPC method not recognized!' }, 138 | }) 139 | error('RPC method', m.message.method, 'not recognized!') 140 | }) 141 | 142 | async function newDepositCallback(err, deposit) { 143 | if (err) { 144 | throw err 145 | } 146 | return state.addDeposit( 147 | deposit.recipient, 148 | deposit.token, 149 | deposit.start, 150 | deposit.end 151 | ) 152 | } 153 | -------------------------------------------------------------------------------- /bin/plasma-chain-deploy.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const path = require('path') 3 | const fs = require('fs') 4 | const program = require('commander') 5 | const colors = require('colors') // eslint-disable-line no-unused-vars 6 | const inquirer = require('inquirer') 7 | const getAccount = require('./utils.js').getAccount 8 | const ethService = require('../src/eth-service.js') 9 | const readConfigFile = require('../src/utils.js').readConfigFile 10 | const ETH_DB_FILENAME = require('../src/constants.js').ETH_DB_FILENAME 11 | 12 | function loadEthDB(config) { 13 | const ethDBPath = path.join(config.ethDBDir, ETH_DB_FILENAME) 14 | let ethDB = {} 15 | if (fs.existsSync(ethDBPath)) { 16 | // Load the db if it exists 17 | ethDB = JSON.parse(fs.readFileSync(ethDBPath, 'utf8')) 18 | } 19 | if (config.plasmaRegistryAddress !== undefined) { 20 | ethDB.plasmaRegistryAddress = config.plasmaRegistryAddress 21 | } 22 | return ethDB 23 | } 24 | 25 | function writeEthDB(config, ethDB) { 26 | if (!fs.existsSync(config.dbDir)) { 27 | fs.mkdirSync(config.dbDir, { recursive: true }) 28 | fs.mkdirSync(config.ethDBDir) 29 | } 30 | fs.writeFileSync( 31 | path.join(config.ethDBDir, ETH_DB_FILENAME), 32 | JSON.stringify(ethDB) 33 | ) 34 | } 35 | 36 | program 37 | .description('starts the operator using the first account') 38 | .option( 39 | '-n, --newRegistry', 40 | 'Deploy a new Plasma Network in addition to a Plasma Chain' 41 | ) 42 | .option( 43 | '--force', 44 | 'Force deployment of new Plasma chain even if we already have one. Note this will overwrite your current Plasma Chain address' 45 | ) 46 | .action(async (none, cmd) => { 47 | const account = await getAccount() 48 | if (account === null || account === undefined) { 49 | return 50 | } 51 | // Get the config 52 | const configFile = process.env.CONFIG 53 | ? process.env.CONFIG 54 | : path.join(__dirname, '..', 'config.json') 55 | console.log('Reading config file from:', configFile) 56 | const config = readConfigFile(configFile) 57 | // Check if we have already deployed a plasma chain 58 | const ethDB = loadEthDB(config) 59 | if (cmd.force === true) { 60 | console.log('\nForcing new Plasma Chain deployment...') 61 | delete ethDB.plasmaChainAddress 62 | writeEthDB(config, ethDB) 63 | } 64 | if (ethDB.plasmaChainAddress !== undefined) { 65 | console.log( 66 | '\nWARNING:'.yellow, 67 | 'Plasma Chain already deployed at:'.yellow, 68 | ethDB.plasmaChainAddress.yellow 69 | ) 70 | console.log( 71 | 'If you want to deploy another chain try ', 72 | '`', 73 | 'plasma-chain deploy --force'.white.bold, 74 | '`' 75 | ) 76 | return 77 | } 78 | // Ask for a Plasma Chain name and ip address 79 | const chainMetadata = await setChainMetadata(config) 80 | console.log('Chain metadata:', chainMetadata) 81 | // Add the chain metadata to the config 82 | config.plasmaChainName = chainMetadata.chainName 83 | config.operatorIpAddress = chainMetadata.ipAddress 84 | // Deploy a new Plasma Chain 85 | config.privateKey = account.privateKey 86 | if (cmd.newRegistry === true) { 87 | config.plasmaRegistryAddress = 'DEPLOY' 88 | } 89 | await ethService.startup(config) 90 | }) 91 | 92 | async function setChainMetadata(config) { 93 | console.log( 94 | '\n~~~~~~~~~plasma~~~~~~~~~chain~~~~~~~~~deployment~~~~~~~~~'.rainbow 95 | ) 96 | console.log( 97 | "\nBefore we deploy your new Plasma Chain, I'll need to ask a couple questions." 98 | .white 99 | ) 100 | const plasmaChainMetadata = {} 101 | // Get the Plasma Chain name 102 | const chainName = await getPlasmaChainName(config) 103 | Object.assign(plasmaChainMetadata, chainName) 104 | // Set the hostname 105 | console.log( 106 | '\nWhat is your IP address? Or a domain name that points to your IP.\n' 107 | .white, 108 | 'WARNING:'.yellow, 109 | 'This IP address will be posted to the Ethereum blockchain.'.white 110 | ) 111 | const hostResponse = await inquirer.prompt([ 112 | { 113 | type: 'input', 114 | name: 'ipAddress', 115 | default: 116 | config.operatorIpAddress === undefined ? '' : config.operatorIpAddress, 117 | message: 'Your IP address or hostname:', 118 | }, 119 | ]) 120 | Object.assign(plasmaChainMetadata, hostResponse) 121 | return plasmaChainMetadata 122 | } 123 | 124 | async function getPlasmaChainName(config) { 125 | console.log( 126 | '\nWhat is the name of your new Plasma Chain?\nThis will be displayed in the' 127 | .white, 128 | 'Plasma Network Registry'.green, 129 | '--'.white, 130 | 'You can view all registered Plasma Chains with'.white, 131 | '`plasma-chain list`'.white.bold 132 | ) 133 | const chainNameResponse = await inquirer.prompt([ 134 | { 135 | type: 'input', 136 | name: 'chainName', 137 | message: "Your Plasma Chain's Name:", 138 | default: 139 | config.plasmaChainName === undefined ? '' : config.plasmaChainName, 140 | validate: (input) => { 141 | if (input.length < 3) { 142 | console.log('Error!'.red, 'Plasma Chain Name is too short!') 143 | return false 144 | } else if (Buffer.from(input, 'utf8').length > 32) { 145 | console.log('Error!'.red, 'Plasma Chain Name is too long!') 146 | return false 147 | } 148 | return true 149 | }, 150 | }, 151 | ]) 152 | return chainNameResponse 153 | } 154 | 155 | program.parse(process.argv) 156 | -------------------------------------------------------------------------------- /test/test-block-manager/test-leveldb-sum.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | const fs = require('fs') 4 | const chai = require('chai') 5 | const log = require('debug')('test:info:test-block-store') 6 | const levelup = require('levelup') 7 | const leveldown = require('leveldown') 8 | const BlockStore = require('../../src/block-manager/block-store.js') 9 | const LevelDBSumTree = require('../../src/block-manager/leveldb-sum-tree.js') 10 | const models = require('plasma-utils').serialization.models 11 | const Transfer = models.Transfer 12 | const Signature = models.Signature 13 | const SignedTransaction = models.SignedTransaction 14 | const BN = require('bn.js') 15 | const dummyTxs = require('./dummy-tx-utils') 16 | const TEST_DB_DIR = require('../../src/constants.js').TEST_DB_DIR 17 | 18 | const expect = chai.expect 19 | 20 | const tr1 = new Transfer({ 21 | sender: '0x43aaDF3d5b44290385fe4193A1b13f15eF3A4FD5', 22 | recipient: '0xa12bcf1159aa01c739269391ae2d0be4037259f3', 23 | token: 0, 24 | start: 2, 25 | end: 3, 26 | }) 27 | const tr2 = new Transfer({ 28 | sender: '0xEA674fdDe714fd979de3EdF0F56AA9716B898ec8', 29 | recipient: '0xa12bcf1159aa01c739269391ae2d0be4037259f4', 30 | token: 0, 31 | start: 6, 32 | end: 7, 33 | }) 34 | const tr3 = new Transfer({ 35 | sender: '0xEA674fdDe714fd979de3EdF0F56AA9716B898ec8', 36 | recipient: '0xa12bcf1159aa01c739269391ae2d0be4037259f4', 37 | token: 1, 38 | start: 100, 39 | end: 108, 40 | }) 41 | const sig = new Signature({ 42 | v: '0a', 43 | r: 'd693b532a80fed6392b428604171fb32fdbf953728a3a7ecc7d4062b1652c042', 44 | s: 'd693b532a80fed6392b428604171fb32fdbf953728a3a7ecc7d4062b1652c042', 45 | }) 46 | const TX1 = new SignedTransaction({ 47 | block: new BN(4), 48 | transfers: [tr1], 49 | signatures: [sig], 50 | }) 51 | const TX2 = new SignedTransaction({ 52 | block: new BN(5), 53 | transfers: [tr2], 54 | signatures: [sig], 55 | }) 56 | const TX3 = new SignedTransaction({ 57 | block: new BN(5), 58 | transfers: [tr3], 59 | signatures: [sig], 60 | }) 61 | TX1.TRIndex = TX2.TRIndex = TX3.TRIndex = 0 62 | 63 | function getTxBundle(txs) { 64 | const txBundle = [] 65 | for (const tx of txs) { 66 | txBundle.push([tx, Buffer.from(tx.encoded, 'hex')]) 67 | } 68 | return txBundle 69 | } 70 | 71 | describe('LevelDBSumTree', function() { 72 | let db 73 | let blockStore 74 | beforeEach(async () => { 75 | const rootDBDir = TEST_DB_DIR 76 | if (!fs.existsSync(rootDBDir)) { 77 | log('Creating a new db directory because it does not exist') 78 | fs.mkdirSync(rootDBDir) 79 | } 80 | const dbDir = rootDBDir + 'block-db-' + +new Date() 81 | db = levelup(leveldown(dbDir)) 82 | // Create a new tx-log dir for this test 83 | const txLogDirectory = './test/test-block-manager/tx-log/' 84 | // fs.mkdirSync(txLogDirectory) 85 | // Create state object 86 | blockStore = new BlockStore(db, txLogDirectory) 87 | }) 88 | 89 | it('should return 0x0000000 as blockhash if the block is empty', async () => { 90 | // Ingest the required data to begin processing the block 91 | const blockNumber = Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) 92 | // Put a fake entry in the db to find 93 | await blockStore.db.put( 94 | Buffer.from([ 95 | 255, 96 | 255, 97 | 255, 98 | 255, 99 | 255, 100 | 255, 101 | 255, 102 | 255, 103 | 255, 104 | 255, 105 | 255, 106 | 255, 107 | 255, 108 | 255, 109 | 255, 110 | 255, 111 | ]), 112 | 'this is a fake value' 113 | ) 114 | // Create a new tree based on block 0's transactions 115 | const sumTree = new LevelDBSumTree(blockStore.db) 116 | await sumTree.parseLeaves(blockNumber) 117 | const root = await sumTree.generateLevel(blockNumber, 0) 118 | expect(new BN(root).eq(new BN(0))).to.equal(true) 119 | }) 120 | 121 | it('should return 0x0000000 as blockhash even if the entire DB is empty', async () => { 122 | // Ingest the required data to begin processing the block 123 | const blockNumber = Buffer.from([0, 0, 0, 0]) 124 | // Create a new tree based on block 0's transactions 125 | const sumTree = new LevelDBSumTree(blockStore.db) 126 | await sumTree.parseLeaves(blockNumber) 127 | const root = await sumTree.generateLevel(blockNumber, 0) 128 | expect(new BN(root).eq(new BN(0))).to.equal(true) 129 | }) 130 | 131 | it('should generate an odd tree w/ multiple types correctly', async () => { 132 | // Ingest the required data to begin processing the block 133 | const TXs = [TX1, TX2, TX3] 134 | const txBundle = getTxBundle(TXs) 135 | const blockNumber = Buffer.from([0, 0, 0, 0]) 136 | blockStore.storeTransactions(blockNumber, txBundle) 137 | await Promise.all(blockStore.batchPromises) 138 | // Create a new tree based on block 0's transactions 139 | const sumTree = new LevelDBSumTree(blockStore.db) 140 | await sumTree.parseLeaves(blockNumber) 141 | await sumTree.generateLevel(blockNumber, 0) 142 | }) 143 | 144 | it('should succeed in generating a tree of x ordered transactions', async () => { 145 | const TXs = dummyTxs.getSequentialTxs(1000, 10) 146 | const txBundle = getTxBundle(TXs) 147 | const blockNumber = Buffer.from([0, 0, 0, 0]) 148 | blockStore.storeTransactions(blockNumber, txBundle) 149 | await Promise.all(blockStore.batchPromises) 150 | // TODO: Optimize this so that we don't spend so long hashing 151 | const sumTree = new LevelDBSumTree(blockStore.db) 152 | await sumTree.generateTree(blockNumber) 153 | }) 154 | }) 155 | -------------------------------------------------------------------------------- /src/mock-node.js: -------------------------------------------------------------------------------- 1 | const Web3 = require('web3') 2 | const BN = Web3.utils.BN 3 | const utils = require('../src/utils.js') 4 | const TYPE_BYTE_SIZE = require('../src/constants.js').TYPE_BYTE_SIZE 5 | const models = require('plasma-utils').serialization.models 6 | const Signature = require('plasma-utils').serialization.models.Signature 7 | const UnsignedTransaction = require('plasma-utils').serialization.models 8 | .UnsignedTransaction 9 | const SignedTransaction = models.SignedTransaction 10 | const log = require('debug')('info:node') 11 | 12 | const fakeSig = { 13 | // Used when isSigned is set to false 14 | v: 'ff', 15 | r: '0000000000000000000000000000000000000000000000000000000000000000', 16 | s: '0000000000000000000000000000000000000000000000000000000000000000', 17 | } 18 | 19 | class MockNode { 20 | constructor(operator, account, peerList) { 21 | this.operator = operator 22 | this.account = account 23 | this.peerList = peerList 24 | this.ranges = [] 25 | this.pendingRanges = [] 26 | } 27 | 28 | processPendingRanges() { 29 | for (const pr of this.pendingRanges) { 30 | utils.addRange(this.ranges, pr[0], pr[1]) 31 | } 32 | this.pendingRanges = [] 33 | } 34 | 35 | async deposit(coinType, amount) { 36 | const encodedDeposit = await this.operator.addDeposit( 37 | Buffer.from(Web3.utils.hexToBytes(this.account.address)), 38 | coinType, 39 | amount 40 | ) 41 | const deposit = new UnsignedTransaction(encodedDeposit).transfers[0] 42 | const start = new BN(utils.getCoinId(deposit.token, deposit.start)) 43 | const end = new BN(utils.getCoinId(deposit.token, deposit.end)) 44 | log( 45 | this.account.address, 46 | 'adding range from deposit with start:', 47 | start.toString('hex'), 48 | '- end:', 49 | end.toString('hex') 50 | ) 51 | utils.addRange(this.ranges, new BN(start), new BN(end)) 52 | } 53 | 54 | getRandomSubrange(startBound, endBound, maxSize) { 55 | const totalSize = endBound.sub(startBound).toNumber() 56 | const startOffset = Math.floor(Math.random() * totalSize) 57 | const endOffset = Math.floor(Math.random() * (totalSize - startOffset)) 58 | const start = startBound.add(new BN(startOffset)) 59 | const end = endBound.sub(new BN(endOffset)) 60 | return [start, end] 61 | } 62 | 63 | async sendRandomTransaction(blockNumber, maxSize, isSigned) { 64 | if (this.ranges.length === 0) { 65 | log('got no money to send!') 66 | return 67 | } 68 | let startIndex = Math.floor(Math.random() * (this.ranges.length / 2)) 69 | startIndex -= startIndex % 2 70 | const startBoundId = this.ranges[startIndex] 71 | const endBoundId = this.ranges[startIndex + 1] 72 | // Come up with a random range within some bounds 73 | const startBound = new BN( 74 | startBoundId.toArrayLike(Buffer, 'big', 16).slice(TYPE_BYTE_SIZE) 75 | ) 76 | const endBound = new BN( 77 | endBoundId.toArrayLike(Buffer, 'big', 16).slice(TYPE_BYTE_SIZE) 78 | ) 79 | // Get the actual thing 80 | let start, end 81 | if (maxSize === undefined) { 82 | ;[start, end] = this.getRandomSubrange(startBound, endBound) 83 | } else { 84 | start = startBound 85 | end = startBound.add(new BN(Math.floor(Math.random()) * maxSize + 1)) 86 | } 87 | const type = new BN( 88 | startBoundId.toArrayLike(Buffer, 'big', 16).slice(0, TYPE_BYTE_SIZE) 89 | ) 90 | const startId = new BN(utils.getCoinId(type, start)) 91 | const endId = new BN(utils.getCoinId(type, end)) 92 | // Get a random recipient that isn't us 93 | let recipient = this.peerList[ 94 | Math.floor(Math.random() * this.peerList.length) 95 | ] 96 | while (recipient === this) { 97 | recipient = this.peerList[ 98 | Math.floor(Math.random() * this.peerList.length) 99 | ] 100 | } 101 | const tx = this.makeTx( 102 | { 103 | sender: this.account.address, 104 | recipient: recipient.account.address, 105 | token: type, 106 | start, 107 | end, 108 | }, 109 | blockNumber, 110 | isSigned 111 | ) 112 | // Add transaction 113 | const txResult = await this.operator.addTransaction(tx) 114 | if (txResult.error !== undefined) { 115 | // This means we got an error! Probably need to update the block number 116 | log('Error in transaction! We may need to update the block number...') 117 | return false 118 | } 119 | // Update ranges 120 | log( 121 | this.account.address, 122 | 'trying to send a transaction with', 123 | 'start:', 124 | new BN(startId).toString('hex'), 125 | '-- end', 126 | new BN(endId).toString('hex') 127 | ) 128 | // TODO: Move this over to the range manager code in `core` 129 | try { 130 | utils.subtractRange(this.ranges, startId, endId) 131 | } catch (err) { 132 | console.log('WARNING: squashing subtract range error') 133 | return 134 | // throw err 135 | } 136 | recipient.pendingRanges.push([new BN(startId), new BN(endId)]) 137 | log('sent a transaction!') 138 | } 139 | 140 | makeTx(tr, block, isSigned) { 141 | let sig 142 | if (isSigned) { 143 | const txHash = new UnsignedTransaction({ block, transfers: [tr] }).hash 144 | const encodedSig = this.account.sign(txHash) 145 | sig = new Signature(encodedSig) 146 | } else { 147 | sig = fakeSig 148 | } 149 | const tx = new SignedTransaction({ 150 | transfers: [tr], 151 | signatures: [sig], 152 | block: block, 153 | }) 154 | return tx 155 | } 156 | } 157 | 158 | module.exports = MockNode 159 | -------------------------------------------------------------------------------- /test/test-integration.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | const chai = require('chai') 4 | const chaiHttp = require('chai-http') 5 | const Server = require('../src/server').Server 6 | const constants = require('../src/constants.js') 7 | const accounts = require('./mock-accounts.js').accounts 8 | const BN = require('web3').utils.BN 9 | const log = require('debug')('test:info:test-integration') 10 | const MockNode = require('../src/mock-node.js') 11 | const EthService = require('../src/eth-service.js') 12 | const readConfigFile = require('../src/utils.js').readConfigFile 13 | const path = require('path') 14 | const UnsignedTransaction = require('plasma-utils').serialization.models 15 | .UnsignedTransaction 16 | const DEPOSIT_SENDER = '0x0000000000000000000000000000000000000000' 17 | 18 | const server = new Server() 19 | const timeout = ms => new Promise(resolve => setTimeout(resolve, ms)) 20 | 21 | chai.use(chaiHttp) 22 | 23 | let idCounter = 0 24 | 25 | // Operator object wrapper to query api 26 | const operator = { 27 | addTransaction: (tx) => { 28 | const encodedTx = tx.encoded 29 | return new Promise((resolve, reject) => { 30 | chai 31 | .request(server.app) 32 | .post('/api') 33 | .send({ 34 | method: constants.ADD_TX_METHOD, 35 | jsonrpc: '2.0', 36 | id: idCounter++, 37 | params: [encodedTx], 38 | }) 39 | .end((err, res) => { 40 | if (err) { 41 | throw err 42 | } 43 | log('Resolve add tx') 44 | // Parse the response to return what the mock node expects 45 | const txResponse = res.body 46 | // Return the deposit 47 | resolve(txResponse) 48 | }) 49 | }) 50 | }, 51 | addDeposit: async (recipient, type, amount) => { 52 | // Construct deposit transaction 53 | // TODO: change this to actually use the right type and amount 54 | const reciept = await EthService.plasmaChain.methods.depositETH().send({ 55 | from: EthService.web3.utils.bytesToHex(recipient), 56 | value: new BN('100000000', 'hex').toString(), 57 | gas: 3500000, 58 | gasPrice: '300000', 59 | }) 60 | const depositEvent = reciept.events.DepositEvent.returnValues 61 | const tokenType = new BN(depositEvent.tokenType, 10) 62 | const start = new BN(depositEvent.untypedStart, 10) 63 | const end = new BN(depositEvent.untypedEnd, 10) 64 | const deposit = new UnsignedTransaction({ 65 | block: depositEvent.block, 66 | transfers: [ 67 | { 68 | sender: DEPOSIT_SENDER, 69 | recipient: depositEvent.depositer, 70 | tokenType, 71 | start, 72 | end, 73 | }, 74 | ], 75 | }) 76 | return deposit 77 | }, 78 | startNewBlock: () => { 79 | return new Promise((resolve, reject) => { 80 | chai 81 | .request(server.app) 82 | .post('/api') 83 | .send({ 84 | method: constants.NEW_BLOCK_METHOD, 85 | jsonrpc: '2.0', 86 | id: idCounter++, 87 | params: {}, 88 | }) 89 | .end((err, res) => { 90 | if (err) { 91 | throw err 92 | } 93 | log('Resolve new block') 94 | resolve(res.body) 95 | }) 96 | }) 97 | }, 98 | getBlockNumber: () => { 99 | return new Promise((resolve, reject) => { 100 | chai 101 | .request(server.app) 102 | .post('/api') 103 | .send({ 104 | method: constants.GET_BLOCK_NUMBER_METHOD, 105 | jsonrpc: '2.0', 106 | id: idCounter++, 107 | params: {}, 108 | }) 109 | .end((err, res) => { 110 | if (err) { 111 | throw err 112 | } 113 | log('Resolve get block number') 114 | resolve(new BN(res.body.result, 10)) 115 | }) 116 | }) 117 | }, 118 | } 119 | 120 | describe('Server', function() { 121 | before(async () => { 122 | // Startup with test config file 123 | const configFile = path.join(__dirname, 'config-test.json') 124 | const config = readConfigFile(configFile, 'test') 125 | 126 | // Server is already started in Server api test 127 | server.started = true 128 | await server.safeStartup(config) 129 | }) 130 | it('Nodes are able to deposit and send transactions', (done) => { 131 | // const accts = accounts.slice(0, 5) 132 | const nodes = [] 133 | // for (const acct of accts) { 134 | for (const acct of accounts) { 135 | nodes.push(new MockNode(operator, acct, nodes)) 136 | } 137 | bigIntegrationTest(nodes, operator).then(() => { 138 | done() 139 | }) 140 | }) 141 | }) 142 | 143 | async function bigIntegrationTest(nodes, operator) { 144 | // Add deposits from 100 different accounts 145 | const depositType = new BN(0) 146 | const depositAmount = new BN(10000, 'hex') 147 | for (const node of nodes) { 148 | await node.deposit(depositType, depositAmount) 149 | log('Submitted deposit') 150 | } 151 | // Now mine and send random transactions 152 | await mineAndLoopSendRandomTxs(5, operator, nodes) 153 | } 154 | 155 | async function mineAndLoopSendRandomTxs(numTimes, operator, nodes) { 156 | for (let i = 0; i < numTimes; i++) { 157 | const blockNumber = await operator.getBlockNumber() 158 | // Send a bunch of transactions 159 | for (const node of nodes) { 160 | await node.sendRandomTransaction(blockNumber, 1024) 161 | } 162 | // Start a new block 163 | log('Starting new block...') 164 | await operator.startNewBlock() 165 | log( 166 | 'Waiting before sending transactions to block:', 167 | blockNumber.toString() + '...' 168 | ) 169 | await timeout(500) 170 | log('Sending new txs for block number:', blockNumber.toString()) 171 | for (const node of nodes) { 172 | node.processPendingRanges() 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/block-manager/app.js: -------------------------------------------------------------------------------- 1 | const log = require('debug')('info:block-app') 2 | const error = require('debug')('ERROR:block-manager-app') 3 | const fs = require('fs') 4 | const levelup = require('levelup') 5 | const leveldown = require('leveldown') 6 | const BlockStore = require('./block-store.js') 7 | const SignedTransaction = require('plasma-utils').serialization.models 8 | .SignedTransaction 9 | const constants = require('../constants.js') 10 | const getDepositTransaction = require('../utils.js').getDepositTransaction 11 | const BN = require('web3').utils.BN 12 | 13 | // Create global state object 14 | let blockStore 15 | 16 | async function startup(options) { 17 | log('Starting block manager') 18 | const db = levelup(leveldown(options.blockDBDir)) 19 | blockStore = new BlockStore(db, options.txLogDir) 20 | // Add a watcher which watches the tx-log for new files and calls `blockStore.addBlock()` with the new block file every time one is added. 21 | fs.watch( 22 | options.txLogDir, 23 | { encoding: 'utf8' }, 24 | async (eventType, filename) => { 25 | if (!filename || filename === 'tmp-tx-log.bin') { 26 | log('Directory change but filename is', filename) 27 | return 28 | } 29 | log('Adding new block:', filename) 30 | try { 31 | const newBlock = await blockStore.addBlock(filename) 32 | log('Successfully added block:', filename) 33 | process.send({ 34 | ipcID: -1, 35 | message: { 36 | rootHash: Buffer.from(newBlock.rootHash, 'hex').toString('hex'), 37 | }, 38 | }) 39 | } catch (err) { 40 | log('FAILED to add block:', filename, 'with error:', err.toString()) 41 | throw err 42 | } 43 | } 44 | ) 45 | } 46 | 47 | process.on('message', async (m) => { 48 | log('Block manager got request:', m.message) 49 | if (m.message.method === constants.INIT_METHOD) { 50 | await startup(m.message.params) 51 | process.send({ ipcID: m.ipcID, message: { startup: 'SUCCESS' } }) 52 | return 53 | // ******* NEW_BLOCK ******* // 54 | } else if (m.message.method === constants.NEW_BLOCK_METHOD) { 55 | const isSuccessfullyStarted = await blockStore.ingestBlock(m.message) 56 | if (!isSuccessfullyStarted) { 57 | process.send({ ipcID: m.ipcID, message: 'FAIL' }) 58 | return 59 | } else throw new Error('BlockStore failed to ingest block!') 60 | // ******* DEPOSIT ******* // 61 | } else if (m.message.method === constants.DEPOSIT_METHOD) { 62 | let addDepositRes 63 | try { 64 | // owner, token, start, end, block 65 | const depositTx = getDepositTransaction( 66 | m.message.params.recipient, 67 | new BN(m.message.params.token, 16), 68 | new BN(m.message.params.start, 16), 69 | new BN(m.message.params.end, 16) 70 | ) 71 | addDepositRes = await blockStore.addDeposit(depositTx) 72 | } catch (err) { 73 | error( 74 | 'Error in adding deposit!\nrpcID:', 75 | m.message.id, 76 | '\nError message:', 77 | err.toString(), 78 | '\n' 79 | ) 80 | addDepositRes = { error: err } 81 | } 82 | process.send({ ipcID: m.ipcID, message: { addDepositRes } }) 83 | return 84 | // ******* GET_HISTORY_PROOF ******* // 85 | } else if (m.message.method === constants.GET_HISTORY_PROOF) { 86 | const startBlockBN = new BN(m.message.params[0], 'hex') 87 | const endBlockBN = new BN(m.message.params[1], 'hex') 88 | const transaction = new SignedTransaction(m.message.params[2]) 89 | let response 90 | try { 91 | const txsAndProofs = await blockStore.getTxHistory( 92 | startBlockBN, 93 | endBlockBN, 94 | transaction 95 | ) 96 | response = { result: txsAndProofs } 97 | } catch (err) { 98 | console.error( 99 | 'Error in getting history proof!\nrpcID:', 100 | m.message.id, 101 | '\nError message:', 102 | err.toString(), 103 | '\n' 104 | ) 105 | response = { error: err } 106 | } 107 | log('OUTGOING getHistoryProof with rpcID:', m.message.id) 108 | process.send({ ipcID: m.ipcID, message: response }) 109 | return 110 | // ******* GET_BLOCK_TXS ******* // 111 | } else if (m.message.method === constants.GET_BLOCK_TXS_METHOD) { 112 | const blockNum = new BN(m.message.params[0], 'hex') 113 | let token 114 | if (m.message.params[1] === 'none') { 115 | token = 'none' 116 | } else { 117 | token = new BN(m.message.params[1], 'hex') 118 | } 119 | const start = new BN(m.message.params[2], 'hex') 120 | const maxEnd = new BN('ffffffffffffffffffffffffffffffff', 'hex') // max end 121 | let response 122 | try { 123 | const txs = await blockStore.getTransactions( 124 | blockNum, 125 | blockNum, 126 | token, 127 | start, 128 | maxEnd, 129 | 20 130 | ) 131 | response = { result: txs } 132 | } catch (err) { 133 | response = { error: err.toString() } 134 | } 135 | process.send({ ipcID: m.ipcID, message: response }) 136 | return 137 | // ******* GET_BLOCK_METADATA ******* // 138 | } else if (m.message.method === constants.GET_BLOCK_METADATA_METHOD) { 139 | const startBlock = new BN(m.message.params[0], 'hex') 140 | // If end block is undefined, set them equal 141 | let endBlock 142 | if (m.message.params[1] !== undefined) { 143 | endBlock = new BN(m.message.params[1], 'hex') 144 | } else { 145 | endBlock = startBlock 146 | } 147 | let response 148 | try { 149 | const blocks = await blockStore.getBlockMetadata(startBlock, endBlock) 150 | response = { result: blocks } 151 | } catch (err) { 152 | response = { error: err.toString() } 153 | } 154 | process.send({ ipcID: m.ipcID, message: response }) 155 | return 156 | // ******* GET_TX_FROM_HASH ******* // 157 | } else if (m.message.method === constants.GET_TX_FROM_HASH_METHOD) { 158 | const txHash = Buffer.from(m.message.params[0], 'hex') 159 | // If end block is undefined, set them equal 160 | let response 161 | try { 162 | const txEncoding = await blockStore.getTxFromHash(txHash) 163 | const tx = new SignedTransaction(txEncoding) 164 | response = { result: tx } 165 | } catch (err) { 166 | response = { error: err.toString() } 167 | } 168 | log('Sending a response!', response) 169 | process.send({ ipcID: m.ipcID, message: response }) 170 | return 171 | } 172 | process.send({ 173 | ipcID: m.ipcID, 174 | message: { error: 'RPC method not recognized!' }, 175 | }) 176 | error('RPC method', m.message.method, 'not recognized!') 177 | }) 178 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const START_BYTE_SIZE = require('./constants.js').START_BYTE_SIZE 3 | const TYPE_BYTE_SIZE = require('./constants.js').TYPE_BYTE_SIZE 4 | const BLOCK_TX_PREFIX = require('./constants.js').BLOCK_TX_PREFIX 5 | const TEST_DB_DIR = require('./constants.js').TEST_DB_DIR 6 | const DEPOSIT_SENDER = require('./constants.js').DEPOSIT_SENDER 7 | const soliditySha3 = require('web3').utils.soliditySha3 8 | const UnsignedTransaction = require('plasma-utils').serialization.models 9 | .UnsignedTransaction 10 | const fs = require('fs') 11 | const _ = require('lodash') 12 | 13 | const appRoot = path.join(__dirname, '..') 14 | 15 | function addRange(rangeList, start, end, numSize) { 16 | if (numSize === undefined) { 17 | // Default to 16 18 | numSize = 16 19 | } 20 | // Find leftRange (a range which ends at the start of our tx) and right_range (a range which starts at the end of our tx) 21 | let leftRange, rightRange 22 | let insertionPoint = _.sortedIndexBy(rangeList, start, (n) => 23 | n.toString(16, numSize) 24 | ) 25 | // If the insertion point found an end poisition equal to our start, change it to the next index (find insertion on the right side) 26 | if ( 27 | insertionPoint > 0 && 28 | insertionPoint < rangeList.length && 29 | insertionPoint % 2 === 1 && 30 | rangeList[insertionPoint].eq(start) 31 | ) { 32 | insertionPoint++ 33 | } 34 | if (insertionPoint > 0 && rangeList[insertionPoint - 1].eq(start)) { 35 | leftRange = insertionPoint - 2 36 | } 37 | if (insertionPoint < rangeList.length && rangeList[insertionPoint].eq(end)) { 38 | rightRange = insertionPoint 39 | } 40 | // Set the start and end of our new range based on the deleted ranges 41 | if (leftRange !== undefined) { 42 | start = rangeList[leftRange] 43 | } 44 | if (rightRange !== undefined) { 45 | end = rangeList[rightRange + 1] 46 | } 47 | // Delete the leftRange and rightRange if we found them 48 | if (leftRange !== undefined && rightRange !== undefined) { 49 | rangeList.splice(leftRange + 1, 2) 50 | return 51 | } else if (leftRange !== undefined) { 52 | rangeList.splice(leftRange, 2) 53 | insertionPoint -= 2 54 | } else if (rightRange !== undefined) { 55 | rangeList.splice(rightRange, 2) 56 | } 57 | rangeList.splice(insertionPoint, 0, start) 58 | rangeList.splice(insertionPoint + 1, 0, end) 59 | } 60 | 61 | function subtractRange(rangeList, start, end) { 62 | let affectedRange 63 | let arStart 64 | let arEnd 65 | for (let i = 0; i < rangeList.length; i += 2) { 66 | arStart = rangeList[i] 67 | arEnd = rangeList[i + 1] 68 | if (arStart.lte(start) && end.lte(arEnd)) { 69 | affectedRange = i 70 | break 71 | } 72 | } 73 | if (affectedRange === undefined) { 74 | throw new Error('No affected range found! Must be an invalid subtraction') 75 | } 76 | // Remove the effected range 77 | rangeList.splice(affectedRange, 2) 78 | // Create new sub-ranges based on what we deleted 79 | if (!arStart.eq(start)) { 80 | // # rangeList += [arStart, start - 1] 81 | rangeList.splice(affectedRange, 0, arStart) 82 | rangeList.splice(affectedRange + 1, 0, start) 83 | affectedRange += 2 84 | } 85 | if (!arEnd.eq(end)) { 86 | // # rangeList += [end + 1, arEnd] 87 | rangeList.splice(affectedRange, 0, end) 88 | rangeList.splice(affectedRange + 1, 0, arEnd) 89 | } 90 | } 91 | 92 | function getCoinId(type, start) { 93 | const buffers = [ 94 | type.toArrayLike(Buffer, 'big', TYPE_BYTE_SIZE), 95 | start.toArrayLike(Buffer, 'big', START_BYTE_SIZE), 96 | ] 97 | return Buffer.concat(buffers) 98 | } 99 | 100 | // Create a defer function which will allow us to add our promise to the messageQueue 101 | function defer() { 102 | const deferred = { 103 | promise: null, 104 | resolve: null, 105 | reject: null, 106 | } 107 | deferred.promise = new Promise((resolve, reject) => { 108 | deferred.resolve = resolve 109 | deferred.reject = reject 110 | }) 111 | return deferred 112 | } 113 | 114 | function jsonrpc(method, params) { 115 | return { 116 | jsonrpc: '2.0', 117 | method, 118 | params, 119 | } 120 | } 121 | 122 | // Promisify the it.next(cb) function 123 | function itNext(it) { 124 | return new Promise((resolve, reject) => { 125 | it.next((err, key, value) => { 126 | if (err) { 127 | reject(err) 128 | } 129 | resolve({ key, value }) 130 | }) 131 | }) 132 | } 133 | 134 | // Promisify the it.end(cb) function 135 | function itEnd(it) { 136 | return new Promise((resolve, reject) => { 137 | it.end((err) => { 138 | if (err) { 139 | reject(err) 140 | } 141 | resolve() 142 | }) 143 | }) 144 | } 145 | 146 | function makeBlockTxKey(blockNumber, type, start) { 147 | return Buffer.concat([BLOCK_TX_PREFIX, blockNumber, getCoinId(type, start)]) 148 | } 149 | 150 | function readConfigFile(configFilePath, mode) { 151 | const config = JSON.parse(fs.readFileSync(configFilePath, 'utf8')) 152 | setConfigDefaults(config, mode) 153 | return config 154 | } 155 | 156 | function setConfigDefaults(config, mode) { 157 | const setIfUndefined = (config, key, value) => { 158 | if (config[key] === undefined) { 159 | config[key] = value 160 | } 161 | } 162 | if (mode === 'test') { 163 | config.dbDir = _generateNewDbTestDir() 164 | } 165 | config.dbDir = path.join(appRoot.toString(), config.dbDir) 166 | // Set db sub directories defaults if they don't exist 167 | Object.assign(config, { 168 | txLogDir: config.dbDir + '/tx-log/', 169 | stateDBDir: config.dbDir + '/state-db/', 170 | blockDBDir: config.dbDir + '/block-db/', 171 | ethDBDir: config.dbDir + '/eth-db/', 172 | }) 173 | } 174 | 175 | function _generateNewDbTestDir() { 176 | return TEST_DB_DIR + +new Date() 177 | } 178 | 179 | function sha3(value) { 180 | // Requires '0x' + becuase web3 only interprets strings as bytes if they start with 0x 181 | const hashString = '0x' + value.toString('hex') 182 | const solidityHash = soliditySha3(hashString) 183 | return Buffer.from(solidityHash.slice(2), 'hex') // Slice 2 to remove the dumb 0x 184 | } 185 | 186 | function getDepositTransaction(owner, token, start, end, block) { 187 | const tx = new UnsignedTransaction({ 188 | block, 189 | transfers: [ 190 | { sender: DEPOSIT_SENDER, recipient: owner, token, start, end }, 191 | ], 192 | }) 193 | tx.tr = tx.transfers[0] 194 | return tx 195 | } 196 | 197 | module.exports = { 198 | addRange, 199 | subtractRange, 200 | defer, 201 | jsonrpc, 202 | itNext, 203 | itEnd, 204 | getCoinId, 205 | readConfigFile, 206 | sha3, 207 | appRoot, 208 | getDepositTransaction, 209 | makeBlockTxKey, 210 | } 211 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs') 3 | const cp = require('child_process') 4 | const constants = require('./constants.js') 5 | const defer = require('./utils.js').defer 6 | const jsonrpc = require('./utils.js').jsonrpc 7 | const express = require('express') 8 | const cors = require('cors') 9 | const bodyParser = require('body-parser') 10 | const log = require('debug')('info:api-app') 11 | const EthService = require('./eth-service.js') 12 | const BN = require('web3').utils.BN 13 | const models = require('plasma-utils').serialization.models 14 | const SignedTransaction = models.SignedTransaction 15 | const debug = require('debug') 16 | 17 | if (process.env.DEBUG === undefined) { 18 | // If no logging is enabled, set these as defaults 19 | debug.enable('info:state,info:block-store,info:leveldb-sum-tree') 20 | } 21 | 22 | class Server { 23 | constructor () { 24 | // Setup simple message queue 25 | this.messageQueue = {} 26 | this.messageCount = 0 27 | 28 | // Set up child processes 29 | this.stateManager = {} 30 | this.blockManager = {} 31 | this.started = false 32 | this.alreadyStartedError = new Error('Operator already started!') 33 | } 34 | 35 | sendMessage (process, message) { 36 | const deferred = defer() 37 | process.send({ 38 | ipcID: this.messageCount, 39 | message 40 | }) 41 | this.messageQueue[this.messageCount] = { resolve: deferred.resolve } 42 | this.messageCount++ 43 | return deferred.promise 44 | } 45 | 46 | resolveMessage (m) { 47 | log('Resolving message with ipcID', m.ipcID) 48 | if (m.ipcID === -1) { 49 | // If this is a message directly from a child, it must be the root hash from the block-store 50 | log('Got new block root:', m.message.rootHash, '- submitting to Ethereum') 51 | EthService.submitRootHash(m.message.rootHash) 52 | return 53 | } 54 | this.messageQueue[m.ipcID].resolve(m) 55 | delete this.messageQueue[m.ipcID] 56 | } 57 | 58 | async startup (config) { 59 | // Set up express 60 | this.app = express() 61 | this.app.use(bodyParser.json()) 62 | this.app.use(cors()) 63 | if (this.started) { 64 | throw this.alreadyStartedError 65 | } 66 | // Make a new db directory if it doesn't exist. 67 | if (!fs.existsSync(config.dbDir)) { 68 | log('Creating a new db directory because it does not exist') 69 | fs.mkdirSync(config.dbDir, { recursive: true }) 70 | fs.mkdirSync(config.ethDBDir) 71 | } 72 | try { 73 | // Setup web3 74 | await EthService.startup(config) 75 | // Setup our child processes -- stateManager & blockManager 76 | this.stateManager = cp.fork(path.join(__dirname, '/state-manager/app.js')) 77 | this.blockManager = cp.fork(path.join(__dirname, '/block-manager/app.js')) 78 | // Child processes need to have the context of 'this' passed in with bind 79 | this.stateManager.on('message', this.resolveMessage.bind(this)) 80 | this.blockManager.on('message', this.resolveMessage.bind(this)) 81 | // Now send an init message 82 | await this.sendMessage(this.stateManager, jsonrpc(constants.INIT_METHOD, { 83 | stateDBDir: config.stateDBDir, 84 | txLogDir: config.txLogDir 85 | })) 86 | await this.sendMessage(this.blockManager, jsonrpc(constants.INIT_METHOD, { 87 | blockDBDir: config.blockDBDir, 88 | txLogDir: config.txLogDir 89 | })) 90 | // Set up the eth event watchers 91 | log('Registering Ethereum event watcher for `DepositEvent`') 92 | EthService.eventWatchers['DepositEvent'].subscribe(this.submitDeposits) 93 | // Set up auto new block creator 94 | if (config.blockTimeInSeconds !== undefined) { 95 | const blockTimeInMiliseconds = parseInt(config.blockTimeInSeconds) * 1000 96 | setTimeout(() => this.newBlockTrigger(blockTimeInMiliseconds), blockTimeInMiliseconds) 97 | } 98 | } catch (err) { 99 | throw err 100 | } 101 | log('Finished sub process startup') 102 | this.app.post('/api', this.handleTx.bind(this)) 103 | this.app.listen(config.port, '0.0.0.0', () => { 104 | console.log('\x1b[36m%s\x1b[0m', `Operator listening on port ${config.port}!`) 105 | }) 106 | this.started = true 107 | } 108 | 109 | async safeStartup (config) { 110 | try { 111 | await this.startup(config) 112 | } catch (err) { 113 | if (err !== this.alreadyStartedError) { 114 | // If this error is anything other than an already started error, throw it 115 | throw err 116 | } 117 | log('Startup has already been run... skipping...') 118 | } 119 | } 120 | 121 | async submitDeposits (err, depositEvents) { 122 | if (err) { 123 | throw err 124 | } 125 | for (const e of depositEvents) { 126 | // Decode the event... 127 | const depositEvent = e.returnValues 128 | log('Detected deposit event with start:', depositEvent.untypedStart, '- & end:', depositEvent.untypedEnd, 'and id:', e.id) 129 | const recipient = depositEvent.depositer 130 | const token = new BN(depositEvent.tokenType, 10).toString('hex') 131 | const start = new BN(depositEvent.untypedStart, 10).toString('hex') 132 | const end = new BN(depositEvent.untypedEnd, 10).toString('hex') 133 | // Send the deposit to the state manager 134 | await this.sendMessage(this.stateManager, jsonrpc(constants.DEPOSIT_METHOD, { 135 | id: e.id, 136 | recipient, 137 | token, 138 | start, 139 | end 140 | })) 141 | // Send the deposit to the block manager 142 | await this.sendMessage(this.blockManager, jsonrpc(constants.DEPOSIT_METHOD, { 143 | id: e.id, 144 | recipient, 145 | token, 146 | start, 147 | end 148 | })) 149 | } 150 | } 151 | 152 | async newBlockTrigger (blockTime) { 153 | const newBlockReq = { 154 | method: constants.NEW_BLOCK_METHOD 155 | } 156 | const response = await this.sendMessage(this.stateManager, newBlockReq) 157 | if (response.error === undefined) { 158 | log('New block created with blockNumber:', response.message.newBlockNumber) 159 | } else { 160 | log('Block is empty--skipping new block') 161 | } 162 | setTimeout(() => this.newBlockTrigger(blockTime), blockTime) 163 | } 164 | 165 | handleTx (req, res) { 166 | log('INCOMING RPC request with method:', req.body.method, 'and rpcID:', req.body.id) 167 | if (constants.STATE_METHODS.includes(req.body.method)) { 168 | if (req.body.method === constants.ADD_TX_METHOD) { 169 | // For performance, check sigs here 170 | try { 171 | const tx = new SignedTransaction(req.body.params[0]) 172 | if (tx.checkSigs() === false) { 173 | throw new Error('Invalid signature on tx!') 174 | } 175 | } catch (err) { 176 | throw new Error(err) 177 | } 178 | } 179 | this.sendMessage(this.stateManager, req.body).then((response) => { 180 | log('OUTGOING response to RPC request with method:', req.body.method, 'and rpcID:', req.body.id) 181 | res.send(response.message) 182 | }) 183 | } else if (constants.BLOCK_METHODS.includes(req.body.method)) { 184 | this.sendMessage(this.blockManager, req.body).then((response) => { 185 | log('OUTGOING response to RPC request with method:', req.body.method, 'and rpcID:', req.body.id) 186 | res.send(response.message) 187 | }) 188 | } else if (req.body.method === constants.GET_ETH_INFO_METHOD) { 189 | res.send({ result: { 190 | operatorAddress: EthService.operatorAddress, 191 | plasmaRegistryAddress: EthService.ethDB.plasmaRegistryAddress, 192 | plasmaChainAddress: EthService.ethDB.plasmaChainAddress 193 | } }) 194 | } 195 | } 196 | } 197 | 198 | module.exports = { 199 | Server 200 | } 201 | -------------------------------------------------------------------------------- /test/test-api.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | const chai = require('chai') 4 | const chaiHttp = require('chai-http') 5 | const Server = require('../src/server').Server 6 | const web3 = require('web3') 7 | const constants = require('../src/constants.js') 8 | const accounts = require('./mock-accounts.js').accounts 9 | const BN = require('web3').utils.BN 10 | const log = require('debug')('test:info:test-api') 11 | const MockNode = require('../src/mock-node.js') 12 | const readConfigFile = require('../src/utils.js').readConfigFile 13 | const path = require('path') 14 | 15 | const server = new Server() 16 | const expect = chai.expect 17 | 18 | chai.use(chaiHttp) 19 | 20 | let idCounter = 0 21 | let totalDeposits = new BN(0) 22 | 23 | // Operator object wrapper to query api 24 | const operator = { 25 | addTransaction: (tx) => { 26 | const encodedTx = tx.encoded 27 | return new Promise((resolve, reject) => { 28 | chai 29 | .request(server.app) 30 | .post('/api') 31 | .send({ 32 | method: constants.ADD_TX_METHOD, 33 | jsonrpc: '2.0', 34 | id: idCounter++, 35 | params: [encodedTx], 36 | }) 37 | .end((err, res) => { 38 | if (err) { 39 | throw err 40 | } 41 | log('Resolve add tx') 42 | // Parse the response to return what the mock node expects 43 | const txResponse = res.body 44 | // Return the deposit 45 | resolve(txResponse) 46 | }) 47 | }) 48 | }, 49 | // Add deposit will deposit coins for an ID that no one cares about. 50 | // This is to simulate throughput without mainchain transactions. 51 | addDeposit: (recipient, token, amount) => { 52 | return new Promise((resolve, reject) => { 53 | const start = new BN(totalDeposits) 54 | totalDeposits = new BN(totalDeposits.add(amount)) 55 | const end = new BN(totalDeposits) 56 | totalDeposits = totalDeposits.add(amount) 57 | chai 58 | .request(server.app) 59 | .post('/api') 60 | .send({ 61 | method: constants.DEPOSIT_METHOD, 62 | jsonrpc: '2.0', 63 | id: idCounter++, 64 | params: { 65 | recipient: web3.utils.bytesToHex(recipient), 66 | token: token.toString(16), 67 | start: start.toString(16), 68 | end: end.toString(16), 69 | }, 70 | }) 71 | .end((err, res) => { 72 | if (err) { 73 | throw err 74 | } 75 | // Return the deposit 76 | resolve(res.body.deposit) 77 | }) 78 | }) 79 | }, 80 | startNewBlock: () => { 81 | return new Promise((resolve, reject) => { 82 | chai 83 | .request(server.app) 84 | .post('/api') 85 | .send({ 86 | method: constants.NEW_BLOCK_METHOD, 87 | jsonrpc: '2.0', 88 | id: idCounter++, 89 | params: {}, 90 | }) 91 | .end((err, res) => { 92 | if (err) { 93 | throw err 94 | } 95 | log('Resolve new block') 96 | resolve(res.body) 97 | }) 98 | }) 99 | }, 100 | getBlockNumber: () => { 101 | return new Promise((resolve, reject) => { 102 | chai 103 | .request(server.app) 104 | .post('/api') 105 | .send({ 106 | method: constants.GET_BLOCK_NUMBER_METHOD, 107 | jsonrpc: '2.0', 108 | id: idCounter++, 109 | params: {}, 110 | }) 111 | .end((err, res) => { 112 | if (err) { 113 | throw err 114 | } 115 | log('Resolve get block number') 116 | resolve(new BN(res.body.result, 10)) 117 | }) 118 | }) 119 | }, 120 | } 121 | 122 | describe('Server api', function() { 123 | before(async () => { 124 | // Startup with test config file 125 | const configFile = path.join(__dirname, 'config-test.json') 126 | const config = readConfigFile(configFile, 'test') 127 | await server.safeStartup(config) 128 | }) 129 | 130 | beforeEach(() => { 131 | totalDeposits = new BN(0) 132 | }) 133 | 134 | describe('/api', function() { 135 | it('responds with status 200 on deposit', function(done) { 136 | chai 137 | .request(server.app) 138 | .post('/api') 139 | .send({ 140 | method: constants.DEPOSIT_METHOD, 141 | jsonrpc: '2.0', 142 | params: { 143 | recipient: accounts[0].address, 144 | token: new BN(999).toString(16), 145 | start: new BN(0).toString(16), 146 | end: new BN(10).toString(16), 147 | }, 148 | }) 149 | .end((err, res) => { 150 | log(err) 151 | expect(res).to.have.status(200) 152 | done() 153 | }) 154 | }) 155 | it('responds with status 200 for many deposits', function(done) { 156 | const promises = [] 157 | for (let i = 0; i < 100; i++) { 158 | promises.push( 159 | chai 160 | .request(server.app) 161 | .post('/api') 162 | .send({ 163 | method: constants.DEPOSIT_METHOD, 164 | jsonrpc: '2.0', 165 | params: { 166 | recipient: accounts[0].address, 167 | token: new BN(999).toString(16), 168 | start: new BN(i * 10).toString(16), 169 | end: new BN((i + 1) * 10).toString(16), 170 | }, 171 | }) 172 | ) 173 | } 174 | Promise.all(promises).then((res) => { 175 | log('Completed: responds with status 200 for many requests') 176 | done() 177 | }) 178 | }) 179 | 180 | it('Nodes are able to deposit and send transactions using the api', (done) => { 181 | const nodes = [] 182 | for (const acct of accounts) { 183 | nodes.push(new MockNode(operator, acct, nodes)) 184 | } 185 | runDepositAndSendTxTest(nodes, operator).then((res) => { 186 | done() 187 | }) 188 | }) 189 | }) 190 | }) 191 | 192 | // Use a function outside of the main test because mocha doens't play as nicely with async functions--the tests don't end consistently 193 | async function runDepositAndSendTxTest(nodes, operator) { 194 | const depositType = new BN(0) 195 | const depositAmount = new BN(10000) 196 | // Add deposits from 100 different accounts 197 | for (const node of nodes) { 198 | await node.deposit(depositType, depositAmount) 199 | } 200 | await mineAndLoopSendRandomTxs(5, operator, nodes) 201 | } 202 | 203 | async function mineAndLoopSendRandomTxs(numTimes, operator, nodes) { 204 | for (let i = 0; i < numTimes; i++) { 205 | let blockNumber = await operator.getBlockNumber() 206 | try { 207 | await sendRandomTransactions(operator, nodes, blockNumber) 208 | } catch (err) { 209 | if ( 210 | err 211 | .toString() 212 | .contains('No affected range found! Must be an invalid subtraction') 213 | ) { 214 | console.log('ERROR:', err) 215 | } 216 | console.log( 217 | 'Squashing for now... this might be a problem with the range manager which I need to sort out anyway...' 218 | ) 219 | } 220 | log('Starting new block...') 221 | await operator.startNewBlock() 222 | blockNumber = await operator.getBlockNumber() 223 | log('Sending new txs for block number:', blockNumber.toString()) 224 | for (const node of nodes) { 225 | node.processPendingRanges() 226 | } 227 | } 228 | } 229 | 230 | let randomTxPromises 231 | let promisesAndTestIds = [] 232 | 233 | function sendRandomTransactions(operator, nodes, blockNumber, rounds, maxSize) { 234 | if (rounds === undefined) rounds = 1 235 | randomTxPromises = [] 236 | for (let i = 0; i < rounds; i++) { 237 | for (const node of nodes) { 238 | randomTxPromises.push(node.sendRandomTransaction(blockNumber, maxSize)) 239 | promisesAndTestIds.push({ 240 | promise: randomTxPromises[randomTxPromises.length - 1], 241 | id: idCounter, 242 | }) 243 | } 244 | } 245 | Promise.all(randomTxPromises).then(() => { 246 | promisesAndTestIds = [] 247 | }) 248 | return Promise.all(randomTxPromises) 249 | } 250 | -------------------------------------------------------------------------------- /test/test-state-manager/test-state.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | const log = require('debug')('test:info:test-state') 4 | const fs = require('fs') 5 | const chai = require('chai') 6 | const chaiHttp = require('chai-http') 7 | const accounts = require('../mock-accounts.js').accounts 8 | const web3 = require('web3') 9 | const BN = web3.utils.BN 10 | const State = require('../../src/state-manager/state.js') 11 | const levelup = require('levelup') 12 | const leveldown = require('leveldown') 13 | const models = require('plasma-utils').serialization.models 14 | const getRandomTx = require('plasma-utils').utils.getRandomTx 15 | const Transfer = models.Transfer 16 | const Signature = models.Signature 17 | const SignedTransaction = models.SignedTransaction 18 | const UnsignedTransaction = models.UnsignedTransaction 19 | const TEST_DB_DIR = require('../../src/constants.js').TEST_DB_DIR 20 | 21 | const expect = chai.expect 22 | 23 | chai.use(chaiHttp) 24 | 25 | const fakeSig = { 26 | v: '1b', 27 | r: '0000000000000000000000000000000000000000000000000000000000000000', 28 | s: '0000000000000000000000000000000000000000000000000000000000000000', 29 | } 30 | 31 | let state 32 | let totalDeposits 33 | 34 | const operator = { 35 | addDeposit: (address, tokenType, amount) => { 36 | const tokenTypeKey = tokenType.toString() 37 | if (totalDeposits[tokenTypeKey] === undefined) { 38 | log('Adding new token type to our total deposits store') 39 | totalDeposits[tokenTypeKey] = new BN(0) 40 | } 41 | const start = new BN(totalDeposits[tokenTypeKey]) 42 | totalDeposits[tokenTypeKey] = new BN( 43 | totalDeposits[tokenTypeKey].add(amount) 44 | ) 45 | const end = new BN(totalDeposits[tokenTypeKey]) 46 | return state.addDeposit(address, tokenType, start, end) 47 | }, 48 | addTransaction: (tx) => { 49 | return state.addTransaction(tx) 50 | }, 51 | } 52 | 53 | function makeTx(rawTrs, rawSigs, block) { 54 | const trs = [] 55 | const sigs = [] 56 | for (let i = 0; i < rawTrs.length; i++) { 57 | trs.push(new Transfer(rawTrs[i])) 58 | sigs.push(new Signature(rawSigs[i])) 59 | } 60 | const tx = new SignedTransaction({ 61 | transfers: trs, 62 | signatures: sigs, 63 | block: block, 64 | }) 65 | return tx 66 | } 67 | 68 | describe('State', function() { 69 | let db 70 | const startNewDB = async () => { 71 | const dbDir = TEST_DB_DIR 72 | if (!fs.existsSync(dbDir)) { 73 | log('Creating a new db directory because it does not exist') 74 | fs.mkdirSync(dbDir) 75 | } 76 | db = levelup(leveldown(dbDir + +new Date())) 77 | // Create a new tx-log dir for this test 78 | const txLogDirectory = dbDir + +new Date() + '-tx-log/' 79 | fs.mkdirSync(txLogDirectory) 80 | // Create state object 81 | state = new State(db, txLogDirectory, () => true) 82 | totalDeposits = {} 83 | await state.init() 84 | } 85 | beforeEach(startNewDB) 86 | 87 | describe('addDeposit', () => { 88 | it('adds the deposit in the expected location', async () => { 89 | const addr0 = Buffer.from(web3.utils.hexToBytes(accounts[0].address)) 90 | const tokenType = new BN(999) 91 | const depositAmount = new BN(10) 92 | // Add a deposit 93 | await operator.addDeposit(addr0, tokenType, depositAmount) 94 | // Check that our account has a record 95 | const transactions = await state.getTransactions(accounts[0].address) 96 | // Get the first record of the set 97 | const depositTransfer = new UnsignedTransaction( 98 | transactions 99 | .values() 100 | .next() 101 | .value.toString('hex') 102 | ).transfers[0] 103 | expect(depositTransfer.token.toString()).to.equal('999') 104 | expect(depositTransfer.start.toString()).to.equal('0') 105 | expect(depositTransfer.end.toString()).to.equal('10') 106 | }) 107 | }) 108 | 109 | describe('startNewBlock', () => { 110 | it('increments the current blockNumber', async () => { 111 | const addr0 = Buffer.from(web3.utils.hexToBytes(accounts[0].address)) 112 | const ethType = new BN(0) 113 | const depositAmount = new BN(10) 114 | // Add a deposit 115 | await operator.addDeposit(addr0, ethType, depositAmount) 116 | 117 | // Add a tx to the block before starting a new one 118 | const tx = makeTx( 119 | [ 120 | { 121 | sender: accounts[0].address, 122 | recipient: accounts[1].address, 123 | token: ethType, 124 | start: 0, 125 | end: 5, 126 | }, 127 | ], 128 | [fakeSig], 129 | 1 130 | ) 131 | await state.addTransaction(tx) 132 | 133 | // Increment the blockNumber 134 | await state.startNewBlock() 135 | expect(state.blockNumber).to.deep.equal(new BN(2)) 136 | }) 137 | }) 138 | 139 | describe('getAffectedRanges', () => { 140 | it('should be correct', async () => { 141 | const addr0 = Buffer.from(web3.utils.hexToBytes(accounts[0].address)) 142 | const ethType = new BN(0) 143 | const depositAmount = new BN(16) 144 | // Now add a bunch of deposits. 145 | for (let i = 0; i < 20; i++) { 146 | await operator.addDeposit(addr0, ethType, depositAmount) 147 | } 148 | const test = await state._getAffectedRanges( 149 | new BN(0), 150 | new BN(0), 151 | new BN(50) 152 | ) 153 | log(test) 154 | }) 155 | }) 156 | 157 | describe('addTransaction', () => { 158 | it('should return false if the block already contains a transfer for the range', async () => { 159 | // Add deposits for us to later send 160 | await operator.addDeposit( 161 | Buffer.from(web3.utils.hexToBytes(accounts[0].address)), 162 | new BN(0), 163 | new BN(10) 164 | ) 165 | // Create a transfer record which touches the same range which we just deposited 166 | const tx = makeTx( 167 | [ 168 | { 169 | sender: accounts[0].address, 170 | recipient: accounts[1].address, 171 | token: 1, 172 | start: 0, 173 | end: 12, 174 | }, 175 | ], 176 | [fakeSig], 177 | 1 178 | ) 179 | try { 180 | await state.addTransaction(tx) 181 | throw new Error('Expect to fail') 182 | } catch (err) {} 183 | }) 184 | 185 | it('should return false if the transfer ranges overlap', async () => { 186 | const ethType = new BN(0) 187 | const depositAmount = new BN(10) 188 | // Add deposits for us to later send 189 | await operator.addDeposit( 190 | Buffer.from(web3.utils.hexToBytes(accounts[0].address)), 191 | ethType, 192 | depositAmount 193 | ) 194 | // Create some transfer records & trList 195 | const tx = makeTx( 196 | [ 197 | { 198 | sender: accounts[0].address, 199 | recipient: accounts[1].address, 200 | token: ethType, 201 | start: 0, 202 | end: 8, 203 | }, 204 | { 205 | sender: accounts[0].address, 206 | recipient: accounts[1].address, 207 | token: ethType, 208 | start: 3, 209 | end: 7, 210 | }, 211 | ], 212 | [fakeSig, fakeSig], 213 | 1 214 | ) 215 | try { 216 | await state.addTransaction(tx) 217 | } catch (err) { 218 | return 219 | } 220 | throw new Error('This should have failed!') 221 | }) 222 | 223 | it('should handle multisends', async () => { 224 | const ethType = new BN(0) 225 | const depositAmount = new BN(10) 226 | // Add deposits for us to later send 227 | await operator.addDeposit( 228 | Buffer.from(web3.utils.hexToBytes(accounts[0].address)), 229 | ethType, 230 | depositAmount 231 | ) 232 | await operator.addDeposit( 233 | Buffer.from(web3.utils.hexToBytes(accounts[0].address)), 234 | ethType, 235 | depositAmount 236 | ) 237 | await operator.addDeposit( 238 | Buffer.from(web3.utils.hexToBytes(accounts[1].address)), 239 | ethType, 240 | depositAmount 241 | ) 242 | await operator.addDeposit( 243 | Buffer.from(web3.utils.hexToBytes(accounts[1].address)), 244 | ethType, 245 | depositAmount 246 | ) 247 | // Create some transfer records & trList 248 | const tx = makeTx( 249 | [ 250 | { 251 | sender: accounts[0].address, 252 | recipient: accounts[1].address, 253 | token: ethType, 254 | start: 0, 255 | end: 12, 256 | }, 257 | { 258 | sender: accounts[1].address, 259 | recipient: accounts[0].address, 260 | token: ethType, 261 | start: 35, 262 | end: 40, 263 | }, 264 | ], 265 | [fakeSig, fakeSig], 266 | 1 267 | ) 268 | // Should not throw an error 269 | const result = await state.addTransaction(tx) 270 | expect(result).to.not.equal(undefined) 271 | }) 272 | }) 273 | 274 | describe('getOwnedRanges', () => { 275 | it('should return the proper number of ranges', async () => { 276 | const ethType = new BN(0) 277 | const depositAmount = new BN(10) 278 | // Add 100 deposits of value 10 from 100 different accounts 279 | for (let i = 0; i < 5; i++) { 280 | await operator.addDeposit( 281 | Buffer.from(web3.utils.hexToBytes(accounts[0].address)), 282 | ethType, 283 | depositAmount 284 | ) 285 | } 286 | const ownedRanges = await state.getOwnedRanges(accounts[0].address) 287 | expect(ownedRanges.length).to.equal(5) 288 | }) 289 | }) 290 | }) 291 | -------------------------------------------------------------------------------- /test/test-block-manager/test-block-store.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | const fs = require('fs') 4 | const chai = require('chai') 5 | const log = require('debug')('test:info:test-block-store') 6 | const levelup = require('levelup') 7 | const leveldown = require('leveldown') 8 | const BlockStore = require('../../src/block-manager/block-store.js') 9 | const BN = require('web3').utils.BN 10 | const dummyTxs = require('./dummy-tx-utils') 11 | const EthService = require('../../src/eth-service.js') 12 | const readConfigFile = require('../../src/utils.js').readConfigFile 13 | const path = require('path') 14 | const BLOCKNUMBER_BYTE_SIZE = require('../../src/constants.js') 15 | .BLOCKNUMBER_BYTE_SIZE 16 | // const constants = require('../../src/constants.js') 17 | const models = require('plasma-utils').serialization.models 18 | const PlasmaMerkleSumTree = require('plasma-utils').PlasmaMerkleSumTree 19 | const UnsignedTransaction = models.UnsignedTransaction 20 | const TransferProof = models.TransferProof 21 | const getDepositTransaction = require('../../src/utils.js') 22 | .getDepositTransaction 23 | 24 | const expect = chai.expect 25 | 26 | function getTxBundle(txs) { 27 | const txBundle = [] 28 | for (const tx of txs) { 29 | txBundle.push([tx, Buffer.from(tx.encoded, 'hex')]) 30 | } 31 | return txBundle 32 | } 33 | 34 | function getUnsignedTransaction(tx) { 35 | const unsignedTx = new UnsignedTransaction({ 36 | block: tx.block, 37 | transfers: tx.transfers, 38 | }) 39 | return unsignedTx 40 | } 41 | 42 | function getHexStringProof(proof) { 43 | // TODO: Remove this and instead support buffers by default 44 | let inclusionProof = [] 45 | for (const sibling of proof) { 46 | inclusionProof.push(sibling.toString('hex')) 47 | } 48 | return inclusionProof 49 | } 50 | 51 | describe('BlockStore', function() { 52 | let db 53 | let web3 54 | let plasmaChain 55 | let blockStore 56 | let config 57 | 58 | beforeEach(async () => { 59 | // Startup with test config file 60 | const configFile = path.join(__dirname, '..', 'config-test.json') 61 | config = readConfigFile(configFile, 'test') 62 | // Create dbDir and ethDB dir directory 63 | if (!fs.existsSync(config.dbDir)) { 64 | log('Creating a new db directory because it does not exist') 65 | fs.mkdirSync(config.dbDir) 66 | fs.mkdirSync(config.ethDBDir) 67 | fs.mkdirSync(config.txLogDir) 68 | } 69 | // Copy a sample tx log to the dbDir 70 | const TEST_BLOCK_FILENAME = 'test-block-0001.bin' 71 | fs.copyFileSync( 72 | path.join(__dirname, 'tx-log', TEST_BLOCK_FILENAME), 73 | config.txLogDir + '0001' 74 | ) 75 | // Start up a new chain 76 | await EthService.startup(config) 77 | web3 = EthService.web3 78 | plasmaChain = EthService.plasmaChain 79 | db = levelup(leveldown(config.blockDBDir)) 80 | blockStore = new BlockStore(db, config.txLogDir) 81 | }) 82 | 83 | it('ingests a block without fail', async () => { 84 | await blockStore.addBlock('0001') 85 | // await blockStore.ingestBlock('00000000000000000000000000000002') 86 | expect(blockStore).to.not.equal(undefined) 87 | }) 88 | 89 | // it('gets range correctly', async () => { 90 | // const TXs = dummyTxs.genNSequentialTransactionsSpacedByOne(100) 91 | // const txBundle = getTxBundle(TXs) 92 | // const blockNumber = Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 1]) 93 | // blockStore.storeTransactions(blockNumber, txBundle) 94 | // const res = await blockStore.getTransactionsAt(blockNumber, new BN(0), new BN(1), new BN(4)) 95 | // // Should print out the ranges 1, 2, 3 96 | // for (let r of res) { log(r) } 97 | // console.log('') 98 | // }) 99 | 100 | it.skip('gets transaction leaves over a number of blocks correctly', async () => { 101 | // add some blocks 102 | for (let i = 1; i < 4; i++) { 103 | const TXs = dummyTxs.getSequentialTxs(100, 1) 104 | const txBundle = getTxBundle(TXs) 105 | const blockNumber = new BN(i).toArrayLike( 106 | Buffer, 107 | 'big', 108 | BLOCKNUMBER_BYTE_SIZE 109 | ) 110 | blockStore.storeTransactions(blockNumber, txBundle) 111 | blockStore.blockNumberBN = blockStore.blockNumberBN.add(new BN(1)) 112 | } 113 | // begin test 114 | const rangeSinceBlockZero = await blockStore.getTransactions( 115 | new BN(1), 116 | blockStore.blockNumberBN, 117 | new BN(0), 118 | new BN(1), 119 | new BN(2) 120 | ) 121 | for (const range of rangeSinceBlockZero) { 122 | for (const r of range) { 123 | log(r) 124 | } 125 | } 126 | }) 127 | 128 | it('generates history proofs correctly', async () => { 129 | const numTxs = 50 130 | const txAmt = 2 131 | // add some blocks 132 | const roots = [] 133 | for (let i = 1; i < 3; i++) { 134 | const TXs = dummyTxs.getSequentialTxs(numTxs, txAmt, i) 135 | const txBundle = getTxBundle(TXs) 136 | const blockNumber = new BN(i).toArrayLike( 137 | Buffer, 138 | 'big', 139 | BLOCKNUMBER_BYTE_SIZE 140 | ) 141 | // Store the transactions 142 | blockStore.storeTransactions(blockNumber, txBundle) 143 | await Promise.all(blockStore.batchPromises) 144 | // Generate a new block using these transactions 145 | const rootHash = await blockStore.sumTree.generateTree(blockNumber) 146 | blockStore.blockNumberBN = blockStore.blockNumberBN.add(new BN(1)) 147 | // Submit the block to the Plasma Chain contract 148 | const reciept = await plasmaChain.methods 149 | .submitBlock(rootHash) 150 | .send({ gas: 400000 }) 151 | expect(reciept.events.SubmitBlockEvent.returnValues['0']).to.equal( 152 | blockStore.blockNumberBN.toString() 153 | ) 154 | expect(reciept.events.SubmitBlockEvent.returnValues['1']).to.equal( 155 | '0x' + Buffer.from(rootHash).toString('hex') 156 | ) 157 | roots.push('0x' + Buffer.from(rootHash).toString('hex')) 158 | } 159 | log('Roots:', roots) 160 | // Check proofs for the first block 161 | const start = new BN(3) 162 | const end = new BN(50) 163 | // Get the tx proofs for the range 164 | const txsAndProofs = await blockStore.getTxsWithProofs( 165 | new BN(1), 166 | new BN(1), 167 | new BN(0), 168 | start, 169 | end 170 | ) 171 | log(txsAndProofs) 172 | // await verifyTxsAndProofs(txsAndProofs, web3, plasmaChain, roots) 173 | }) 174 | 175 | it.skip('generates history proofs correctly when given a particular transaction', async () => { 176 | const numTxs = 50 177 | const txAmt = 2 178 | // First add some deposits 179 | const depositTxs = dummyTxs.getSequentialTxs(numTxs, txAmt, 0) 180 | for (let i = 0; i < depositTxs.length; i++) { 181 | const tr = depositTxs[i].transfers[0] 182 | depositTxs[i] = getDepositTransaction( 183 | tr.recipient, 184 | tr.token, 185 | tr.start, 186 | tr.end, 187 | new BN(i) 188 | ) 189 | await blockStore.addDeposit(depositTxs[i]) 190 | } 191 | // add some blocks 192 | const roots = [] 193 | let TXs 194 | for (let i = 1; i < 100; i++) { 195 | TXs = dummyTxs.getSequentialTxs(i, 2, i) 196 | const txBundle = getTxBundle(TXs) 197 | const blockNumber = new BN(i).toArrayLike( 198 | Buffer, 199 | 'big', 200 | BLOCKNUMBER_BYTE_SIZE 201 | ) 202 | // Store the transactions 203 | blockStore.storeTransactions(blockNumber, txBundle) 204 | await Promise.all(blockStore.batchPromises) 205 | // Generate a new block using these transactions 206 | const rootHash = await blockStore.sumTree.generateTree(blockNumber) 207 | blockStore.blockNumberBN = blockStore.blockNumberBN.add(new BN(1)) 208 | // Submit the block to the Plasma Chain contract 209 | const reciept = await plasmaChain.methods 210 | .submitBlock(rootHash) 211 | .send({ gas: 400000 }) 212 | expect(reciept.events.SubmitBlockEvent.returnValues['0']).to.equal( 213 | blockStore.blockNumberBN.toString() 214 | ) 215 | expect(reciept.events.SubmitBlockEvent.returnValues['1']).to.equal( 216 | '0x' + Buffer.from(rootHash).toString('hex') 217 | ) 218 | roots.push('0x' + Buffer.from(rootHash).toString('hex')) 219 | // Check that the tree root from utils matches the tree root from the sumTree 220 | const plasmaUtilsTree = new PlasmaMerkleSumTree( 221 | txBundle.map((value) => value[0]) 222 | ) 223 | expect(plasmaUtilsTree.root().hash).to.equal( 224 | Buffer.from(rootHash).toString('hex') 225 | ) 226 | } 227 | log('Roots:', roots) 228 | // Check proofs for our transaction. This tx will not be real but instead span over a bunch of txs 229 | let proofTx = dummyTxs.getSequentialTxs(1, 20, 99)[0] 230 | // Get the tx proofs for the range 231 | const txsAndProofs = await blockStore.getTxHistory( 232 | new BN(1), 233 | new BN(99), 234 | proofTx 235 | ) 236 | await verifyTxsAndProofs( 237 | txsAndProofs.transactionHistory, 238 | web3, 239 | plasmaChain, 240 | roots 241 | ) 242 | }) 243 | }) 244 | 245 | async function verifyTxsAndProofs(txsAndProofs, web3, plasmaChain, roots) { 246 | // For all blocks... 247 | for (const blockNumber of Object.keys(txsAndProofs)) { 248 | // For all transactions in each block... 249 | for (const txAndProof of txsAndProofs[blockNumber]) { 250 | const transaction = txAndProof.transaction 251 | const unsignedTx = getUnsignedTransaction(transaction) 252 | // For all transfers in each transaction in each block.... 253 | for (const [ 254 | i, 255 | trProofDecoded, 256 | ] of txAndProof.transactionProof.transferProofs.entries()) { 257 | // Actually check inclusion. LOL 258 | trProofDecoded.inclusionProof = getHexStringProof( 259 | trProofDecoded.inclusionProof 260 | ) 261 | const transferProof = new TransferProof(trProofDecoded) 262 | for (const hash of transferProof.inclusionProof) { 263 | log('Inclusion Proof:', hash.toString('hex')) 264 | } 265 | const res = await plasmaChain.methods 266 | .checkTransferProofAndGetBounds( 267 | web3.utils.soliditySha3('0x' + unsignedTx.encoded), 268 | unsignedTx.block.toString(), 269 | '0x' + transferProof.encoded 270 | ) 271 | .call({ gas: 5000000 }) // This should not revert! 272 | log('Result:', res) 273 | // Check the Plasma Utils transfer checker 274 | const result = PlasmaMerkleSumTree.checkTransferProof( 275 | unsignedTx, 276 | i, 277 | transferProof, 278 | roots[0].slice(2) + 'ffffffffffffffffffffffffffffffff' 279 | ) 280 | expect(result).to.not.equal(false) 281 | } 282 | // Check transaction proof in utils... TODO: Assert that it's true 283 | // PlasmaMerkleSumTree.checkTransactionProof(transaction, anotherTxProof, roots[0].slice(2) + 'ffffffffffffffffffffffffffffffff') 284 | } 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /src/eth-service.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs') 3 | const colors = require('colors') // eslint-disable-line no-unused-vars 4 | const plasmaChainCompiled = require('plasma-contracts').plasmaChainCompiled 5 | const plasmaRegistryCompiled = require('plasma-contracts') 6 | .plasmaRegistryCompiled 7 | const serializerCompiled = require('plasma-contracts').serializerCompiled 8 | const Web3 = require('web3') 9 | const ganache = require('ganache-cli') 10 | const log = require('debug')('info:eth') 11 | const ETH_DB_FILENAME = require('./constants.js').ETH_DB_FILENAME 12 | const EventWatcher = require('./event-watcher.js') 13 | 14 | const DEPLOY_REGISTRY = 'DEPLOY' 15 | 16 | // ES short for EthService 17 | // `web3` & `plasmaChain` start as uninitialized because the startup script must be run before we can interact meaningfully with our node 18 | const UNINITIALIZED = 'UNINITIALIZED' 19 | const es = { 20 | web3: UNINITIALIZED, 21 | ganacheServer: UNINITIALIZED, 22 | plasmaChain: UNINITIALIZED, 23 | operatorAddress: UNINITIALIZED, 24 | eventWatchers: UNINITIALIZED, 25 | ethDB: {}, 26 | } 27 | 28 | // Startup function called to initialize everything 29 | async function startup(config) { 30 | es.web3 = new Web3() 31 | // Initalize our wallet 32 | initializeWallet(es.web3, config.privateKey) 33 | // Load the ethDB database 34 | es.ethDB = loadEthDB(config) 35 | // Check if we are in test mode 36 | if (process.env.NODE_ENV === 'test') { 37 | await initializeTestingEnv(config) 38 | } else { 39 | await initializeProdEnv(config) 40 | } 41 | // Create our plasma chain es.web3 object, this will point to an existing Ethereum smart contract 42 | es.plasmaChain = new es.web3.eth.Contract( 43 | plasmaChainCompiled.abi, 44 | es.ethDB.plasmaChainAddress, 45 | { from: es.operatorAddress } 46 | ) 47 | // Load our event handlers 48 | es.eventWatchers = _getEventWatchers(config) 49 | console.log('Plasma Registry address:', es.ethDB.plasmaRegistryAddress.yellow) 50 | console.log('Plasma Chain address:', es.ethDB.plasmaChainAddress.yellow) 51 | } 52 | 53 | function _getEventWatchers(config) { 54 | const eventWatchers = {} 55 | for (const ethEvent of Object.keys(es.plasmaChain.events)) { 56 | if (ethEvent.slice(0, 2) === '0x' || ethEvent.includes('(')) { 57 | // Filter out all of the events that are not of the form `DepositEvent(address,uint256)` 58 | continue 59 | } 60 | log('Creating event watcher for event:', ethEvent) 61 | eventWatchers[ethEvent] = new EventWatcher( 62 | es.web3, 63 | es.plasmaChain, 64 | ethEvent, 65 | config.finalityDepth, 66 | config.ethPollInterval 67 | ) 68 | } 69 | return eventWatchers 70 | } 71 | 72 | function loadEthDB(config) { 73 | const ethDBPath = path.join(config.ethDBDir, ETH_DB_FILENAME) 74 | let ethDB = {} 75 | if (fs.existsSync(ethDBPath)) { 76 | // Load the db if it exists 77 | ethDB = JSON.parse(fs.readFileSync(ethDBPath, 'utf8')) 78 | } 79 | if (config.plasmaRegistryAddress !== undefined) { 80 | ethDB.plasmaRegistryAddress = config.plasmaRegistryAddress 81 | } 82 | return ethDB 83 | } 84 | 85 | function writeEthDB(config, ethDB) { 86 | if (!fs.existsSync(config.dbDir)) { 87 | log('Creating a new db directory because it does not exist') 88 | fs.mkdirSync(config.dbDir, { recursive: true }) 89 | fs.mkdirSync(config.ethDBDir) 90 | } 91 | fs.writeFileSync( 92 | path.join(config.ethDBDir, ETH_DB_FILENAME), 93 | JSON.stringify(ethDB) 94 | ) 95 | } 96 | 97 | function initializeWallet(web3, privateKey) { 98 | if (privateKey !== undefined) { 99 | es.web3.eth.accounts.wallet.add(privateKey) 100 | } else if (process.env.NODE_ENV === 'test') { 101 | _addTestWalletsToWeb3(es.web3) // If we are in test mode & there is no private key, add some fake private keys 102 | } else { 103 | throw new Error('No private key specified!') 104 | } 105 | es.operatorAddress = es.web3.eth.accounts.wallet[0].address 106 | console.log('Operator address:', es.operatorAddress.yellow) 107 | } 108 | 109 | async function initializeTestingEnv(config) { 110 | // First get our es.web3 object which we will use. This comes with some wallets that have $ in them 111 | await _setupGanache(es.web3, config) 112 | // Deploy a new Plasma Registry 113 | await deployNewPlasmaRegistry(config) 114 | // Deploy our new Plasma Chain and save it in a file 115 | es.ethDB.plasmaChainAddress = await deployNewPlasmaChain(es.web3, config) 116 | console.log( 117 | 'Testing mode enabled so deployed a new Plasma Registry & Plasma Chain' 118 | ) 119 | writeEthDB(config, es.ethDB) 120 | } 121 | 122 | async function deployNewPlasmaRegistry(config) { 123 | // Deploy a new PlasmaRegistry. This requires first deploying a dummy Plasma Chain 124 | // We have the compiled contracts, let's create objects for them... 125 | const plasmaSerializerCt = new es.web3.eth.Contract( 126 | serializerCompiled.abi, 127 | es.operatorAddress, 128 | { from: es.operatorAddress, gas: 7000000, gasPrice: '5000000000' } 129 | ) 130 | const plasmaChainCt = new es.web3.eth.Contract( 131 | plasmaChainCompiled.abi, 132 | es.operatorAddress, 133 | { from: es.operatorAddress, gas: 7000000, gasPrice: '5000000000' } 134 | ) 135 | const plasmaRegistryCt = new es.web3.eth.Contract( 136 | plasmaRegistryCompiled.abi, 137 | es.operatorAddress, 138 | { from: es.operatorAddress, gas: 7000000, gasPrice: '5000000000' } 139 | ) 140 | const serializer = await plasmaSerializerCt 141 | .deploy({ data: serializerCompiled.bytecode }) 142 | .send() 143 | // To set up the Plasma Network, we need to first deploy a Plasma Chain contract 144 | const plasmaChain = await plasmaChainCt 145 | .deploy({ data: plasmaChainCompiled.bytecode }) 146 | .send() 147 | // Finally deploy the Plasma Registry and save the address in our ethDB 148 | const plasmaRegistry = await plasmaRegistryCt 149 | .deploy({ data: plasmaRegistryCompiled.bytecode }) 150 | .send() 151 | es.ethDB.plasmaRegistryAddress = plasmaRegistry.options.address 152 | writeEthDB(config, es.ethDB) 153 | log('Deployed a Plasma Registry at', es.ethDB.plasmaRegistryAddress) 154 | // Initialize the registry 155 | await plasmaRegistry.methods 156 | .initializeRegistry(plasmaChain.options.address, serializer.options.address) 157 | .send() 158 | } 159 | 160 | async function initializeProdEnv(config) { 161 | if (config.web3HttpProvider === undefined) { 162 | throw new Error('Web3 provider undefined!') 163 | } 164 | es.web3.setProvider(new Web3.providers.HttpProvider(config.web3HttpProvider)) 165 | // Check if we need to deploy a new Plasma registry. 166 | if (es.ethDB.plasmaRegistryAddress === DEPLOY_REGISTRY) { 167 | console.log('Deploying new registry...This could take some time.'.green) 168 | if ( 169 | config.web3HttpProvider !== undefined && 170 | config.web3HttpProvider.includes('rinkeby') 171 | ) { 172 | console.log( 173 | 'View transaction progress on Etherscan:', 174 | 'https://rinkeby.etherscan.io/address/'.blue + es.operatorAddress.blue 175 | ) 176 | } 177 | await deployNewPlasmaRegistry(config) 178 | log('New registry at address:', es.ethDB.plasmaRegistryAddress) 179 | es.ethDB.plasmaChainAddress = undefined 180 | } 181 | // Check if we need to deploy a new Plasma Chain 182 | if (es.ethDB.plasmaChainAddress === undefined) { 183 | // Check that the plasma registry was deployed 184 | const plasmaRegistryCode = await es.web3.eth.getCode( 185 | es.ethDB.plasmaRegistryAddress 186 | ) 187 | if (plasmaRegistryCode === '0x') { 188 | throw new Error( 189 | 'No plasma registry found at address: ' + es.ethDB.plasmaRegistryAddress 190 | ) 191 | } 192 | // Deploy a new Plasma Chain and save it in a file 193 | es.ethDB.plasmaChainAddress = await deployNewPlasmaChain(es.web3, config) 194 | console.log( 195 | 'No Plasma Chain contract detected! Deploying a new one...'.green 196 | ) 197 | log('Deployed Plasma Chain to address:', es.ethDB.plasmaChainAddress) 198 | writeEthDB(config, es.ethDB) 199 | } else { 200 | console.log( 201 | 'Plasma Chain contract already deployed. Skipping deployment...'.green 202 | ) 203 | } 204 | } 205 | 206 | async function deployNewPlasmaChain(web3, config) { 207 | // We have the compiled contracts, let's create objects for them... 208 | const plasmaRegistry = new web3.eth.Contract( 209 | plasmaRegistryCompiled.abi, 210 | es.ethDB.plasmaRegistryAddress 211 | ) 212 | let createPChainReciept 213 | try { 214 | console.log('Deploying new Plasma Chain... this could take a while') 215 | if ( 216 | config.web3HttpProvider !== undefined && 217 | config.web3HttpProvider.includes('rinkeby') 218 | ) { 219 | console.log( 220 | 'View transaction progress on Etherscan:', 221 | 'https://rinkeby.etherscan.io/address/'.blue + es.operatorAddress.blue 222 | ) 223 | } 224 | createPChainReciept = await plasmaRegistry.methods 225 | .createPlasmaChain( 226 | es.operatorAddress, 227 | Buffer.from(config.plasmaChainName), 228 | Buffer.from(config.operatorIpAddress) 229 | ) 230 | .send({ from: es.operatorAddress, gas: 800000, gasPrice: '5000000000' }) 231 | } catch (err) { 232 | console.log('ERROR DEPLOYING CHAIN\n'.red) 233 | if (err.toString().includes('gas * price')) { 234 | console.log( 235 | 'You do not have enough ETH to pay for the deployment.\nGet some using a faucet (https://faucet.rinkeby.io/)' 236 | ) 237 | } else { 238 | console.log( 239 | "Your Plasma Chain name may be taken! Try another--they're filling up quick!" 240 | ) 241 | } 242 | console.log('\n', err) 243 | process.exit(1) 244 | throw err 245 | } 246 | const newPlasmaChainAddress = 247 | createPChainReciept.events.NewPlasmaChain.returnValues['0'] 248 | log('Deployed a Plasma Chain at', newPlasmaChainAddress) 249 | return newPlasmaChainAddress 250 | } 251 | 252 | function _addTestWalletsToWeb3(web3) { 253 | log('Filling wallet with test private keys') 254 | for (let i = 0; i < 100; i++) { 255 | web3.eth.accounts.wallet.add(web3.utils.sha3(i.toString())) 256 | } 257 | } 258 | 259 | async function _startGanacheServer(server, port) { 260 | return new Promise((resolve) => { 261 | server.listen(port, resolve) 262 | }) 263 | } 264 | 265 | async function _setupGanache(web3, config) { 266 | const ganacheAccounts = [] 267 | for (let i = 0; i < web3.eth.accounts.wallet.length; i++) { 268 | ganacheAccounts.push({ 269 | balance: '0x100000000000000000000', 270 | secretKey: web3.eth.accounts.wallet[i].privateKey, 271 | }) 272 | } 273 | // For all provider options, see: https://github.com/trufflesuite/ganache-cli#library 274 | const providerOptions = { 275 | accounts: ganacheAccounts, 276 | gasLimit: '0x7A1200', 277 | locked: false, 278 | logger: { log }, 279 | } 280 | // If we are given an HttpProvider, use a ganache server instead of as a local library 281 | if (config.testHttpProviderPort !== undefined) { 282 | es.ganacheServer = ganache.server(providerOptions) 283 | await _startGanacheServer(es.ganacheServer, config.testHttpProviderPort) 284 | web3.setProvider( 285 | new Web3.providers.HttpProvider( 286 | 'http://localhost:' + config.testHttpProviderPort 287 | ) 288 | ) 289 | } else { 290 | // No port given, so run as local library 291 | web3.setProvider(ganache.provider(providerOptions)) 292 | web3.currentProvider.setMaxListeners(300) // TODO: Remove this as it is squashing errors. See https://github.com/ethereum/web3.js/issues/1648 293 | } 294 | } 295 | 296 | let numRoots = 0 297 | let currentRootNum = 0 298 | const timeout = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) 299 | async function submitRootHash(rootHash) { 300 | const rootNum = numRoots++ 301 | while (currentRootNum !== rootNum) { 302 | // eslint-disable-line no-unmodified-loop-condition 303 | log( 304 | 'Multiple roots attempting to be submitted at the same time. Waiting until other submit root has finished' 305 | ) 306 | await timeout(100) 307 | } 308 | const reciept = await es.plasmaChain.methods 309 | .submitBlock('0x' + rootHash) 310 | .send({ gas: 400000, gasPrice: '5000000000' }) 311 | currentRootNum++ 312 | log( 313 | 'Ethereum reciept for block number:', 314 | reciept.events.SubmitBlockEvent.returnValues['0'], 315 | 'wish root hash:', 316 | reciept.events.SubmitBlockEvent.returnValues['1'] 317 | ) 318 | } 319 | 320 | // Set functions which we will export as well 321 | es.startup = startup 322 | es.submitRootHash = submitRootHash 323 | 324 | module.exports = es 325 | -------------------------------------------------------------------------------- /src/block-manager/leveldb-sum-tree.js: -------------------------------------------------------------------------------- 1 | const log = require('debug')('info:leveldb-sum-tree') 2 | const web3 = require('web3') 3 | const BN = web3.utils.BN 4 | const COIN_ID_BYTE_SIZE = require('../constants.js').COIN_ID_BYTE_SIZE 5 | const BLOCK_TX_PREFIX = require('../constants.js').BLOCK_TX_PREFIX 6 | const BLOCK_INDEX_PREFIX = require('../constants.js').BLOCK_INDEX_PREFIX 7 | const NUM_LEVELS_PREFIX = require('../constants.js').BLOCK_TX_PREFIX 8 | const NODE_DB_PREFIX = require('../constants.js').NODE_DB_PREFIX 9 | const HASH_TO_TX_PREFIX = require('../constants.js').HASH_TO_TX_PREFIX 10 | const models = require('plasma-utils').serialization.models 11 | const SignedTransaction = models.SignedTransaction 12 | const UnsignedTransaction = models.UnsignedTransaction 13 | const Transfer = models.Transfer 14 | const itNext = require('../utils.js').itNext 15 | const itEnd = require('../utils.js').itEnd 16 | const sha3 = require('../utils.js').sha3 17 | const Decimal = require('decimal.js-light') 18 | 19 | const INDEX_BYTES_SIZE = 4 20 | 21 | function coinIdToBuffer(coinId) { 22 | return coinId.toArrayLike(Buffer, 'big', COIN_ID_BYTE_SIZE) 23 | } 24 | 25 | class LevelDBSumTree { 26 | constructor(db) { 27 | this.db = db 28 | } 29 | 30 | async generateTree(blockNumber) { 31 | const numLeaves = await this.parseLeaves(blockNumber) 32 | let heightOfTree 33 | if (numLeaves === undefined) { 34 | heightOfTree = 0 35 | } else { 36 | // TODO: Replace this and instead detect heightOfTree in generateLevel 37 | heightOfTree = Math.ceil( 38 | new Decimal(numLeaves.toString(10)).log(2).toNumber() 39 | ) 40 | } 41 | const rootHash = await this.generateLevel(blockNumber, 0, heightOfTree) 42 | log( 43 | 'Generating tree for block:', 44 | blockNumber.toString('hex'), 45 | 'with root:', 46 | Buffer.from(rootHash).toString('hex') 47 | ) 48 | return rootHash 49 | } 50 | 51 | getTransactionFromLeaf(value) { 52 | const index = value[0] 53 | const encoding = value.slice(1) 54 | const transaction = new SignedTransaction(encoding.toString('hex')) 55 | transaction.trIndex = index 56 | transaction.encoding = encoding 57 | return transaction 58 | } 59 | 60 | getUnsignedTransaction(tx) { 61 | const unsignedTx = new UnsignedTransaction({ 62 | block: tx.block, 63 | transfers: tx.transfers, 64 | }) 65 | return unsignedTx 66 | } 67 | 68 | /** 69 | * Parses the leaves to generate the zero'th level of our sum tree 70 | */ 71 | async parseLeaves(blockNumber) { 72 | const self = this 73 | return new Promise(async (resolve, reject) => { 74 | // Helper functions for getting properties of our transactions 75 | const getTr = (tx) => new Transfer(tx.transfers[tx.trIndex]) 76 | const typedStart = (tr) => 77 | new BN(tr.token.toString(16, 8) + tr.start.toString(16, 24), 16) 78 | // Store the min and max values which can exist for any range. This will be used as the bounds of our stream 79 | const minStart = Buffer.from('0'.repeat(COIN_ID_BYTE_SIZE * 2), 'hex') 80 | const maxEnd = Buffer.from('f'.repeat(COIN_ID_BYTE_SIZE * 2), 'hex') 81 | // Store the prefix which all our transactions should have 82 | const blockTxPrefix = Buffer.concat([BLOCK_TX_PREFIX, blockNumber]) 83 | // We need special logic to handle the first leaf / transaction. Because of this, look it up independently. 84 | const firstLeaf = await this.getNearest( 85 | Buffer.concat([blockTxPrefix, minStart]) 86 | ) 87 | // Check if this block is empty -- if the nearest 88 | if ( 89 | firstLeaf.key === undefined || 90 | !firstLeaf.key.slice(0, blockTxPrefix.length).equals(blockTxPrefix) 91 | ) { 92 | // This block appears to be empty! Return early 93 | resolve() 94 | return 95 | } 96 | const firstTransaction = this.getTransactionFromLeaf(firstLeaf.value) 97 | // Now set the prev tx's sum start to *zero* instead of what it normally is--the previous transaction's start 98 | let previousTransaction = firstTransaction 99 | previousTransaction.sumStart = new BN(0) 100 | let previousTxIndex = new BN(0) 101 | // Read all remaining leaves, computing hash and setting sum value 102 | const firstTxStart = coinIdToBuffer(typedStart(getTr(firstTransaction))) // Store the first start as we will use it for our next seek 103 | this.db 104 | .createReadStream({ 105 | gt: Buffer.concat([BLOCK_TX_PREFIX, blockNumber, firstTxStart]), 106 | lt: Buffer.concat([BLOCK_TX_PREFIX, blockNumber, maxEnd]), 107 | }) 108 | .on('data', function(data) { 109 | const transaction = self.getTransactionFromLeaf(data.value) 110 | transaction.sumStart = typedStart(getTr(transaction)) 111 | const range = coinIdToBuffer( 112 | transaction.sumStart.sub(previousTransaction.sumStart) 113 | ) 114 | const prevTxEncodedBuf = Buffer.from( 115 | self.getUnsignedTransaction(previousTransaction).encoded, 116 | 'hex' 117 | ) 118 | const prevTxHash = sha3(prevTxEncodedBuf) 119 | self.writeNode(blockNumber, 0, previousTxIndex, prevTxHash, range) 120 | self.writeTrToIndex( 121 | blockNumber, 122 | Buffer.from(getTr(previousTransaction).encoded, 'hex'), 123 | previousTxIndex 124 | ) 125 | self.writeHashToTransaction(prevTxHash, prevTxEncodedBuf) 126 | previousTxIndex = previousTxIndex.add(new BN(1)) 127 | previousTransaction = transaction 128 | }) 129 | .on('end', function(data) { 130 | const range = coinIdToBuffer( 131 | new BN(maxEnd).sub(previousTransaction.sumStart) 132 | ) 133 | const prevTxEncodedBuf = Buffer.from( 134 | self.getUnsignedTransaction(previousTransaction).encoded, 135 | 'hex' 136 | ) 137 | const prevTxHash = sha3(prevTxEncodedBuf) 138 | self.writeNode(blockNumber, 0, previousTxIndex, prevTxHash, range) 139 | self.writeTrToIndex( 140 | blockNumber, 141 | Buffer.from(getTr(previousTransaction).encoded, 'hex'), 142 | previousTxIndex 143 | ) 144 | self.writeHashToTransaction(prevTxHash, prevTxEncodedBuf) 145 | // Return the total number of leaves 146 | resolve(previousTxIndex.addn(1)) 147 | }) 148 | .on('error', function(err) { 149 | reject(err) 150 | }) 151 | }) 152 | } 153 | 154 | async getNearest(key) { 155 | const it = this.db.iterator({ 156 | gte: key, 157 | limit: 1, 158 | }) 159 | const result = await itNext(it) 160 | await itEnd(it) 161 | return result 162 | } 163 | 164 | async getHeight(blockNumber) { 165 | const height = await this.get( 166 | Buffer.concat([blockNumber, Buffer.from('height')]) 167 | ) 168 | return height 169 | } 170 | 171 | async getNode(blockNumber, level, index) { 172 | const node = await this.db.get(this.makeNodeKey(blockNumber, level, index)) 173 | return node 174 | } 175 | 176 | async getIndex(blockNumber, trEncoding) { 177 | const index = await this.db.get( 178 | this.makeTrToIndexKey(blockNumber, trEncoding) 179 | ) 180 | return index 181 | } 182 | 183 | parseNodeValue(value) { 184 | return { 185 | hash: value.slice(0, 32), 186 | sum: new BN(value.slice(32)), 187 | } 188 | } 189 | 190 | makeNodeKey(blockNumber, level, index) { 191 | return Buffer.concat([ 192 | Buffer.from(NODE_DB_PREFIX), 193 | blockNumber, 194 | this.makeIndexId(level, index), 195 | ]) 196 | } 197 | 198 | makeTrToIndexKey(blockNumber, trEncoding) { 199 | return Buffer.concat([BLOCK_INDEX_PREFIX, blockNumber, trEncoding]) 200 | } 201 | 202 | async writeNode(blockNumber, level, index, hash, sum) { 203 | const newNodeKey = this.makeNodeKey(blockNumber, level, index) 204 | log( 205 | 'Writing new node\nKey:', 206 | newNodeKey.toString('hex'), 207 | '\nValue:', 208 | Buffer.concat([Buffer.from(hash), sum]).toString('hex') 209 | ) 210 | await this.db.put(newNodeKey, Buffer.concat([Buffer.from(hash), sum])) 211 | } 212 | 213 | async writeHashToTransaction(txHash, prevTxEncodedBuf) { 214 | const newHashToTransactionKey = Buffer.concat([ 215 | Buffer.from(HASH_TO_TX_PREFIX), 216 | txHash, 217 | ]) 218 | log( 219 | 'Writing hash -> encoding\nKey:', 220 | newHashToTransactionKey.toString('hex'), 221 | '\nValue:', 222 | prevTxEncodedBuf.toString('hex') 223 | ) 224 | await this.db.put(newHashToTransactionKey, prevTxEncodedBuf) 225 | } 226 | 227 | async writeTrToIndex(blockNumber, trEncoding, index) { 228 | const newTrKey = this.makeTrToIndexKey(blockNumber, trEncoding) 229 | const indexBuff = index.toArrayLike(Buffer, 'big', INDEX_BYTES_SIZE) 230 | log( 231 | 'Writing new tr -> index\nKey:', 232 | newTrKey.toString('hex'), 233 | '\nValue:', 234 | indexBuff.toString('hex') 235 | ) 236 | await this.db.put(newTrKey, indexBuff) 237 | } 238 | 239 | async writeNumLevels(blockNumber, numLevels) { 240 | log( 241 | 'Writing num levels for block:', 242 | Buffer.concat([NUM_LEVELS_PREFIX, blockNumber]), 243 | '\nWith value:', 244 | Buffer.from([numLevels]) 245 | ) 246 | await this.db.put( 247 | Buffer.concat([NUM_LEVELS_PREFIX, blockNumber]), 248 | Buffer.from([numLevels]) 249 | ) 250 | } 251 | 252 | async getNumLevels(blockNumber) { 253 | const numLevels = await this.db.get( 254 | Buffer.concat([NUM_LEVELS_PREFIX, blockNumber]) 255 | ) 256 | return new BN(numLevels) 257 | } 258 | 259 | makeIndexId(level, index) { 260 | return Buffer.concat([ 261 | Buffer.from([level]), 262 | index.toArrayLike(Buffer, 'big', INDEX_BYTES_SIZE), 263 | ]) 264 | } 265 | 266 | emptyNode() { 267 | const emptyHash = Buffer.from('0'.repeat(64), 'hex') 268 | const emptySum = new BN(0) 269 | return { 270 | hash: emptyHash, 271 | sum: emptySum, 272 | } 273 | } 274 | 275 | getParent(left, right) { 276 | const leftSum = left.sum.toArrayLike(Buffer, 'big', 16) 277 | const rightSum = right.sum.toArrayLike(Buffer, 'big', 16) 278 | const parentHash = sha3( 279 | Buffer.concat([left.hash, leftSum, right.hash, rightSum]) 280 | ) 281 | return { 282 | hash: parentHash, 283 | sum: left.sum 284 | .add(right.sum) 285 | .toArrayLike(Buffer, 'big', COIN_ID_BYTE_SIZE), 286 | } 287 | } 288 | 289 | async generateLevel(blockNumber, level, height) { 290 | log( 291 | 'Starting to generate level:', 292 | level, 293 | 'for block:', 294 | blockNumber.toString('hex') 295 | ) 296 | const self = this 297 | const parentLevel = level + 1 298 | // Check that there is at least one node at this level--if not it might be an empty block 299 | try { 300 | await this.getNode(blockNumber, level, new BN(0)) 301 | } catch (err) { 302 | // No node found! Is this an empty block? 303 | return this.emptyNode().hash 304 | } 305 | return new Promise((resolve, reject) => { 306 | // Create readstream for all nodes at the previous level 307 | const maxEnd = new BN('f'.repeat(INDEX_BYTES_SIZE * 2), 16) 308 | const readStream = this.db.createReadStream({ 309 | gte: this.makeNodeKey(blockNumber, level, new BN(0)), 310 | lte: this.makeNodeKey(blockNumber, level, maxEnd), 311 | }) 312 | // Go through every node at this level and build the next level's nodes 313 | let leftChild = null 314 | let numChildren = new BN(0) 315 | let parentIndex = new BN(0) 316 | let parentNode 317 | readStream 318 | .on('data', (data) => { 319 | log('Processing child:', data.key.toString('hex')) 320 | numChildren = numChildren.add(new BN(1)) 321 | // If this is the left child store it and move on 322 | if (leftChild === null) { 323 | leftChild = this.parseNodeValue(data.value) 324 | return 325 | } 326 | // Now we have the left and right children. Let's hash and compute the next sum 327 | const rightChild = this.parseNodeValue(data.value) 328 | parentNode = this.getParent(leftChild, rightChild) 329 | self.writeNode( 330 | blockNumber, 331 | parentLevel, 332 | parentIndex, 333 | parentNode.hash, 334 | parentNode.sum 335 | ) 336 | parentIndex = parentIndex.add(new BN(1)) 337 | leftChild = null 338 | }) 339 | .on('end', async () => { 340 | // If level equals height, we have reached the root node. 341 | if (level === height) { 342 | log( 343 | 'Returning root hash:', 344 | Buffer.from(leftChild.hash).toString('hex') 345 | ) 346 | await self.writeNumLevels(blockNumber, level) 347 | resolve(leftChild.hash) 348 | return 349 | } 350 | // Check if we ended on an element that wasn't a right node. If so fill it in with a blank node 351 | if (leftChild !== null) { 352 | log('Filling in an odd length level with a zero node') 353 | const rightChild = this.emptyNode() 354 | const parentNode = this.getParent(leftChild, rightChild) 355 | self.writeNode( 356 | blockNumber, 357 | parentLevel, 358 | parentIndex, 359 | parentNode.hash, 360 | parentNode.sum 361 | ) 362 | } 363 | resolve(await self.generateLevel(blockNumber, parentLevel, height)) 364 | }) 365 | .on('error', (err) => { 366 | reject(err) 367 | }) 368 | }) 369 | } 370 | } 371 | 372 | module.exports = LevelDBSumTree 373 | -------------------------------------------------------------------------------- /src/state-manager/state.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | const Web3 = require('web3') 3 | const BN = Web3.utils.BN 4 | const log = require('debug')('info:state') 5 | const models = require('plasma-utils').serialization.models 6 | const UnsignedTransaction = models.UnsignedTransaction 7 | const SignedTransaction = models.SignedTransaction 8 | const itNext = require('../utils.js').itNext 9 | const itEnd = require('../utils.js').itEnd 10 | const getDepositTransaction = require('../utils.js').getDepositTransaction 11 | const getCoinId = require('../utils.js').getCoinId 12 | const colors = require('colors') // eslint-disable-line no-unused-vars 13 | 14 | const COIN_ID_PREFIX = require('../constants.js').COIN_ID_PREFIX 15 | const ADDRESS_PREFIX = require('../constants.js').ADDRESS_PREFIX 16 | const DEPOSIT_PREFIX = require('../constants.js').DEPOSIT_PREFIX 17 | const START_BYTE_SIZE = require('../constants.js').START_BYTE_SIZE 18 | const TYPE_BYTE_SIZE = require('../constants.js').TYPE_BYTE_SIZE 19 | const DEPOSIT_SENDER = require('../constants.js').DEPOSIT_SENDER 20 | const BLOCKNUMBER_BYTE_SIZE = require('../constants.js').BLOCKNUMBER_BYTE_SIZE 21 | 22 | const DEPOSIT_TX_LENGTH = 73 23 | const RECENT_TX_CACHE_SIZE = 30 24 | 25 | // ************* HELPER FUNCTIONS ************* // 26 | const timeout = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) 27 | const timeoutAmt = () => 0 28 | // const timeoutAmt = () => Math.floor(Math.random() * 2) 29 | 30 | function decodeTransaction(encoding) { 31 | let tx 32 | if (encoding.length === DEPOSIT_TX_LENGTH) { 33 | tx = new UnsignedTransaction(encoding.toString('hex')) 34 | } else { 35 | tx = new SignedTransaction(encoding.toString('hex')) 36 | } 37 | tx.tr = tx.transfers[0] 38 | return tx 39 | } 40 | 41 | function getAddressToCoinKey(address, token, end) { 42 | const buffers = [ 43 | ADDRESS_PREFIX, 44 | address, 45 | token.toArrayLike(Buffer, 'big', TYPE_BYTE_SIZE), 46 | end.toArrayLike(Buffer, 'big', START_BYTE_SIZE), 47 | ] 48 | return Buffer.concat(buffers) 49 | } 50 | 51 | function getCoinToTxKey(token, start) { 52 | const buffers = [ 53 | COIN_ID_PREFIX, 54 | token.toArrayLike(Buffer, 'big', TYPE_BYTE_SIZE), 55 | start.toArrayLike(Buffer, 'big', START_BYTE_SIZE), 56 | ] 57 | return Buffer.concat(buffers) 58 | } 59 | 60 | class State { 61 | constructor(db, txLogDirectory) { 62 | this.db = db 63 | this.txLogDirectory = txLogDirectory 64 | this.tmpTxLogFile = this.txLogDirectory + 'tmp-tx-log.bin' 65 | this.lock = {} 66 | this.recentTransactions = [] 67 | this.isCurrentBlockEmpty = true 68 | } 69 | 70 | async init() { 71 | // Get the current block number 72 | try { 73 | const blockNumberBuff = await this.db.get('blockNumber') 74 | this.blockNumber = new BN(blockNumberBuff) 75 | log('Block number found! Starting at: ' + this.blockNumber) 76 | } catch (err) { 77 | if (err.notFound) { 78 | log('No blockNumber found! Starting from block 1.') 79 | this.blockNumber = new BN(1) 80 | await this.db.put( 81 | Buffer.from('blockNumber'), 82 | this.blockNumber.toArrayLike(Buffer, 'big', BLOCKNUMBER_BYTE_SIZE) 83 | ) 84 | } else { 85 | throw err 86 | } 87 | } 88 | // Make a new tx-log directory if it doesn't exist. 89 | if (!fs.existsSync(this.txLogDirectory)) { 90 | log('Creating a new tx-log directory') 91 | fs.mkdirSync(this.txLogDirectory) 92 | } 93 | // Open a write stream for our tx log 94 | if (fs.existsSync(this.tmpTxLogFile)) { 95 | console.log( 96 | 'WARNING:'.yellow, 97 | `Partially complete transaction log detected. 98 | Starting from where we left off but note that for extra security you may want to 99 | start from scratch & reingest only the finalized blocks in the transaction log.` 100 | ) 101 | } 102 | this.writeStream = fs.createWriteStream(this.tmpTxLogFile, { flags: 'a' }) 103 | } 104 | 105 | async startNewBlock() { 106 | // Check that this isn't an empty block 107 | if (this.isCurrentBlockEmpty) { 108 | throw new Error('Block is empty! Cannot start new block.') 109 | } 110 | if (this.lock.all === true) { 111 | throw new Error( 112 | 'Attempting to start a new block when a global lock is already active' 113 | ) 114 | } 115 | // Start a global lock as we increment the block number. Note that we will have to wait until all other locks are released 116 | this.lock.all = true 117 | // Wait until all other locks are released 118 | while (Object.keys(this.lock).length !== 1) { 119 | log('Waiting to acquire global lock') 120 | await timeout(timeoutAmt()) 121 | } 122 | // Everything should be locked now that we have a `lock.all` activated. Time to increment the blockNumber 123 | this.blockNumber = this.blockNumber.add(new BN(1)) 124 | // Create a new block 125 | await this.db.put( 126 | Buffer.from('blockNumber'), 127 | this.blockNumber.toArrayLike(Buffer, 'big', BLOCKNUMBER_BYTE_SIZE) 128 | ) 129 | // Start a new tx log 130 | this.writeStream.end() 131 | const txLogPath = 132 | this.txLogDirectory + 133 | this.blockNumber.subn(1).toString(10, BLOCKNUMBER_BYTE_SIZE * 2) 134 | await fs.rename(this.tmpTxLogFile, txLogPath) 135 | this.writeStream = fs.createWriteStream(this.tmpTxLogFile, { flags: 'a' }) 136 | // Set empty block flag 137 | this.isCurrentBlockEmpty = true 138 | // Release our lock 139 | delete this.lock.all 140 | log('#### Started new Block #', this.blockNumber.toString()) 141 | return this.blockNumber 142 | } 143 | 144 | attemptAcquireLocks(k) { 145 | const keywords = k.slice() // Make a copy of the array to make sure we don't pollute anything when we add the `all` keyword 146 | log('Attempting to acquire lock for:', keywords) 147 | keywords.push('all') 148 | if ( 149 | keywords.some((val) => { 150 | return this.lock.hasOwnProperty(val) 151 | }) 152 | ) { 153 | log('Failed') 154 | // Failed to acquire locks 155 | return false 156 | } 157 | // Acquire locks 158 | for (let i = 0; i < keywords.length - 1; i++) { 159 | this.lock[keywords[i]] = true 160 | } 161 | return true 162 | } 163 | 164 | releaseLocks(keywords) { 165 | // Pop off our lock queue 166 | for (const keyword of keywords) { 167 | delete this.lock[keyword] 168 | } 169 | } 170 | 171 | async addDeposit(recipient, token, start, end) { 172 | // Check that we haven't already recorded this deposit 173 | try { 174 | await this.db.get(Buffer.concat([DEPOSIT_PREFIX, getCoinId(token, end)])) 175 | log( 176 | 'Deposit already recorded with token type:', 177 | token.toString('hex'), 178 | ', start:', 179 | start.toString('hex'), 180 | 'and end:', 181 | end.toString('hex') 182 | ) 183 | return 184 | } catch (err) {} 185 | while (!this.attemptAcquireLocks([token.toString(16)])) { 186 | // Wait before attempting again 187 | await timeout(timeoutAmt()) 188 | } 189 | const deposit = getDepositTransaction( 190 | Web3.utils.bytesToHex(recipient), 191 | token, 192 | start, 193 | end, 194 | this.blockNumber 195 | ) 196 | const depositEncoded = deposit.encoded 197 | try { 198 | // Put the new owned coin range and the new total deposits 199 | const ops = [ 200 | { 201 | type: 'put', 202 | key: Buffer.concat([DEPOSIT_PREFIX, getCoinId(token, end)]), 203 | value: Buffer.from([1]), 204 | }, 205 | { 206 | type: 'put', 207 | key: getAddressToCoinKey(recipient, token, deposit.tr.end), 208 | value: Buffer.from(depositEncoded, 'hex'), 209 | }, 210 | { 211 | type: 'put', 212 | key: getCoinToTxKey(token, deposit.tr.end), 213 | value: Buffer.from(depositEncoded, 'hex'), 214 | }, 215 | ] 216 | await this.db.batch(ops) 217 | } catch (err) { 218 | throw err 219 | } 220 | this.releaseLocks([recipient, token.toString(16)]) 221 | log( 222 | 'Added deposit with token type:', 223 | token.toString('hex'), 224 | ', start:', 225 | start.toString('hex'), 226 | 'and end:', 227 | end.toString('hex') 228 | ) 229 | return depositEncoded 230 | } 231 | 232 | validateTransaction(tx) { 233 | // Make sure the block number is correct 234 | if (!tx.block.eq(this.blockNumber)) { 235 | throw new Error('Transfer record blockNumber mismatch!') 236 | } 237 | // Check that no ranges overlap... we may want to disable this 238 | for (const [i, tr] of tx.transfers.entries()) { 239 | // Check that none of the other transfer records overlap 240 | for (let j = 0; j < tx.transfers.length; j++) { 241 | if ( 242 | j !== i && 243 | tx.transfers[j].start.lt(tr.end) && 244 | tx.transfers[j].end.gt(tr.start) 245 | ) { 246 | throw new Error('Transfer record ranges overlap!') 247 | } 248 | } 249 | } 250 | } 251 | 252 | async getTransactionLock(tx) { 253 | const senders = tx.transfers.map((transfer) => transfer.sender) 254 | while (!this.attemptAcquireLocks(senders)) { 255 | // Wait before attempting again 256 | await timeout(timeoutAmt()) 257 | } 258 | } 259 | 260 | async releaseTransactionLock(tx) { 261 | const senders = tx.transfers.map((transfer) => transfer.sender) 262 | this.releaseLocks(senders) 263 | } 264 | 265 | validateAffectedRanges(tx) { 266 | // For all affected ranges, check all affected ranges are owned by the correct sender and blockNumber 267 | for (const tr of tx.transfers) { 268 | for (const ar of tr.affectedRanges) { 269 | if ( 270 | tr.sender.toLowerCase() !== ar.decodedTx.tr.recipient.toLowerCase() 271 | ) { 272 | throw new Error( 273 | 'Affected range check failed! Transfer record sender = ' + 274 | tr.sender.toLowerCase() + 275 | 'and the affected range recipient = ' + 276 | ar.decodedTx.tr.recipient.toLowerCase() 277 | ) 278 | } 279 | if ( 280 | ar.decodedTx.block.eq(this.blockNumber) && 281 | ar.decodedTx.tr.sender !== DEPOSIT_SENDER 282 | ) { 283 | throw new Error( 284 | 'Affected range check failed! Affected range block = ' + 285 | ar.decodedTx.block.toString() + 286 | ' and this block = ' + 287 | this.blockNumber.toString() 288 | ) 289 | } 290 | } 291 | } 292 | } 293 | 294 | async writeTransactionToDB(tx) { 295 | let dbBatch = [] 296 | for (const tr of tx.transfers) { 297 | // For every transfer, get all of the DB operations we need to perform 298 | let batchOps 299 | batchOps = await this.getTransferBatchOps(tx, tr, tr.affectedRanges) 300 | dbBatch = dbBatch.concat(batchOps) 301 | } 302 | const txEncoding = tx.encoded 303 | // Write the transaction to the DB and tx log 304 | await this.db.batch(dbBatch) 305 | this.writeStream.write(Buffer.from(txEncoding, 'hex')) 306 | // And add to our recent transaction cache 307 | this.addRecentTransaction(txEncoding) 308 | } 309 | 310 | async getTransferBatchOps(transaction, transfer, affectedRanges) { 311 | const dbBatch = [] 312 | // Begin by queuing up the deletion of all affected ranges. 313 | for (const arEntry of affectedRanges) { 314 | const arRecipient = Buffer.from( 315 | Web3.utils.hexToBytes(arEntry.decodedTx.tr.recipient) 316 | ) 317 | dbBatch.push({ type: 'del', key: arEntry.key }) 318 | dbBatch.push({ 319 | type: 'del', 320 | key: Buffer.concat([ADDRESS_PREFIX, arRecipient, arEntry.key.slice(1)]), 321 | }) // Delete the address -> end mapping 322 | } 323 | // Now add back the ranges which were not entirely covered by this transfer. 324 | let arEntry = affectedRanges[0] 325 | let ar = arEntry.decodedTx 326 | if (!ar.tr.start.eq(transfer.start)) { 327 | // Reduce the first affected range's end position. Eg: ##### becomes ###$$ 328 | const arRecipient = Buffer.from(Web3.utils.hexToBytes(ar.tr.recipient)) 329 | ar.tr.end = transfer.start 330 | // Get the affectedTransaction so that when we create the new address->coin mapping we preserve the transaction 331 | const affectedTransaction = await this.db.get( 332 | Buffer.concat([ADDRESS_PREFIX, arRecipient, arEntry.key.slice(1)]) 333 | ) 334 | dbBatch.push({ 335 | type: 'put', 336 | key: getCoinToTxKey(ar.tr.token, ar.tr.end), 337 | value: Buffer.from(ar.encoded, 'hex'), 338 | }) 339 | dbBatch.push({ 340 | type: 'put', 341 | key: getAddressToCoinKey(arRecipient, ar.tr.token, ar.tr.end), 342 | value: affectedTransaction, 343 | }) 344 | } 345 | arEntry = affectedRanges[affectedRanges.length - 1] 346 | ar = arEntry.decodedTx 347 | if (!ar.tr.end.eq(transfer.end)) { 348 | // Increase the last affected range's start position. Eg: ##### becomes $$### 349 | const arRecipient = Buffer.from(Web3.utils.hexToBytes(ar.tr.recipient)) 350 | ar.tr.start = transfer.end 351 | // Get the affectedTransaction so that when we create the new address->coin mapping we preserve the transaction 352 | const affectedTransaction = await this.db.get( 353 | Buffer.concat([ADDRESS_PREFIX, arRecipient, arEntry.key.slice(1)]) 354 | ) 355 | dbBatch.push({ 356 | type: 'put', 357 | key: affectedRanges[affectedRanges.length - 1].key, 358 | value: Buffer.from(ar.encoded, 'hex'), 359 | }) 360 | dbBatch.push({ 361 | type: 'put', 362 | key: Buffer.concat([ 363 | ADDRESS_PREFIX, 364 | arRecipient, 365 | affectedRanges[affectedRanges.length - 1].key.slice(1), 366 | ]), 367 | value: affectedTransaction, 368 | }) 369 | } 370 | // Add our new transfer record 371 | const trRecipient = Buffer.from(Web3.utils.hexToBytes(transfer.recipient)) 372 | const transferAsTx = new UnsignedTransaction({ 373 | transfers: [transfer], 374 | block: transaction.block, 375 | }) 376 | dbBatch.push({ 377 | type: 'put', 378 | key: getCoinToTxKey(transfer.token, transfer.end), 379 | value: Buffer.from(transferAsTx.encoded, 'hex'), 380 | }) 381 | dbBatch.push({ 382 | type: 'put', 383 | key: getAddressToCoinKey(trRecipient, transfer.token, transfer.end), 384 | value: Buffer.from(transaction.encoded, 'hex'), 385 | }) 386 | // And finally apply the batch operations 387 | return dbBatch 388 | } 389 | 390 | async addTransaction(tx) { 391 | // Check that the transaction is well formatted 392 | this.validateTransaction(tx) 393 | // Acquire lock on all of the transfer record senders 394 | await this.getTransactionLock(tx) 395 | log('Attempting to add transaction from:', tx.transfers[0].sender) 396 | try { 397 | // Get the ranges which the transaction affects and attach them to the transaction object 398 | await this.addAffectedRangesToTx(tx) 399 | // Check that all of the affected ranges are valid 400 | await this.validateAffectedRanges(tx) 401 | // All checks have passed, now write to the DB 402 | await this.writeTransactionToDB(tx) 403 | } catch (err) { 404 | this.releaseTransactionLock(tx) 405 | throw err 406 | } 407 | this.releaseTransactionLock(tx) 408 | log('Added transaction from:', tx.transfers[0].recipient) 409 | this.addRecentTransaction(tx) 410 | // Record that this block is not empty 411 | this.isCurrentBlockEmpty = false 412 | return new SignedTransaction(tx).encoded 413 | } 414 | 415 | isCorrectTokenType(tokenType, coinID) { 416 | const prefixAndType = Buffer.concat([ 417 | COIN_ID_PREFIX, 418 | tokenType.toArrayLike(Buffer, 'big', TYPE_BYTE_SIZE), 419 | ]) 420 | return Buffer.compare(prefixAndType, coinID.slice(0, 5)) === 0 421 | } 422 | 423 | async addAffectedRangesToTx(tx) { 424 | for (const [i, tr] of tx.transfers.entries()) { 425 | const affectedRange = await this._getAffectedRanges( 426 | tr.token, 427 | tr.start, 428 | tr.end 429 | ) 430 | if (affectedRange.length === 0) { 431 | // If there are no affected ranges then this transfer must be invalid 432 | throw new Error('No affected ranges!') 433 | } 434 | for (let i = 0; i < affectedRange.length; i++) { 435 | affectedRange[i].decodedTx = decodeTransaction(affectedRange[i].value) 436 | } 437 | tx.transfers[i].affectedRanges = affectedRange 438 | } 439 | } 440 | 441 | async _getAffectedRanges(token, start, end) { 442 | // TODO: Handle results which are undefined 443 | const it = this.db.iterator({ 444 | gt: getCoinToTxKey(new BN(token), new BN(start)), 445 | }) 446 | const affectedRanges = [] 447 | let result = await itNext(it) 448 | // Check that the prefix & token type match the transfer we are looking for 449 | if (!this.isCorrectTokenType(token, result.key)) { 450 | await itEnd(it) 451 | return [] 452 | } 453 | affectedRanges.push(result) 454 | while ( 455 | this.isCorrectTokenType(token, result.key) && 456 | Buffer.compare( 457 | result.key.slice(5), 458 | end.toArrayLike(Buffer, 'big', START_BYTE_SIZE) 459 | ) < 0 460 | ) { 461 | result = await itNext(it) 462 | affectedRanges.push(result) 463 | } 464 | await itEnd(it) 465 | return affectedRanges 466 | } 467 | 468 | async getOwnedRanges(address) { 469 | while (!this.attemptAcquireLocks([address])) { 470 | // Wait before attempting again 471 | await timeout(timeoutAmt()) 472 | } 473 | // Get the ranges 474 | const addressBuffer = Buffer.from(Web3.utils.hexToBytes(address)) 475 | const it = this.db.iterator({ 476 | gt: getAddressToCoinKey(addressBuffer, new BN(0), new BN(0)), 477 | }) 478 | const ownedRanges = [] 479 | let result = await itNext(it) 480 | while ( 481 | result.key && 482 | Buffer.compare(addressBuffer, result.key.slice(1, 21)) === 0 483 | ) { 484 | ownedRanges.push(result) 485 | result = await itNext(it) 486 | } 487 | await itEnd(it) 488 | this.releaseLocks([address]) 489 | return ownedRanges 490 | } 491 | 492 | getRecentTransactions(start, end) { 493 | if (start === undefined) start = 0 494 | if (end === undefined) start = this.recentTransactions.length 495 | return this.recentTransactions.slice(start, end) 496 | } 497 | 498 | addRecentTransaction(encodedTx) { 499 | this.recentTransactions.unshift(encodedTx) 500 | if (this.recentTransactions.length > RECENT_TX_CACHE_SIZE) { 501 | this.recentTransactions.pop() 502 | } 503 | } 504 | 505 | async getTransactions(address, startBlock, endBlock) { 506 | const ownedRanges = await this.getOwnedRanges(address) 507 | const transactions = new Set() 508 | for (const range of ownedRanges) { 509 | const serialized = new UnsignedTransaction(range.value) 510 | if (serialized.block < startBlock || serialized > endBlock) { 511 | continue 512 | } 513 | 514 | transactions.add(range.value) 515 | } 516 | return transactions 517 | } 518 | } 519 | 520 | module.exports = State 521 | -------------------------------------------------------------------------------- /src/block-manager/block-store.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const log = require('debug')('info:block-store') 3 | const BN = require('web3').utils.BN 4 | const makeBlockTxKey = require('../utils.js').makeBlockTxKey 5 | const LevelDBSumTree = require('./leveldb-sum-tree.js') 6 | const models = require('plasma-utils').serialization.models 7 | const UnsignedTransaction = models.UnsignedTransaction 8 | const SignedTransaction = models.SignedTransaction 9 | const Transfer = models.Transfer 10 | const TransactionProof = models.TransactionProof 11 | const BLOCK_TX_PREFIX = require('../constants.js').BLOCK_TX_PREFIX 12 | const BLOCK_DEPOSIT_PREFIX = require('../constants.js').BLOCK_DEPOSIT_PREFIX 13 | const BLOCK_ROOT_HASH_PREFIX = require('../constants.js').BLOCK_ROOT_HASH_PREFIX 14 | const BLOCK_NUM_TXS_PREFIX = require('../constants.js').BLOCK_NUM_TXS_PREFIX 15 | const BLOCK_TIMESTAMP_PREFIX = require('../constants.js').BLOCK_TIMESTAMP_PREFIX 16 | const HASH_TO_TX_PREFIX = require('../constants.js').HASH_TO_TX_PREFIX 17 | const BLOCKNUMBER_BYTE_SIZE = require('../constants.js').BLOCKNUMBER_BYTE_SIZE 18 | const TRANSFER_BYTE_SIZE = require('../constants.js').TRANSFER_BYTE_SIZE 19 | const SIGNATURE_BYTE_SIZE = require('../constants.js').SIGNATURE_BYTE_SIZE 20 | const itNext = require('../utils.js').itNext 21 | const itEnd = require('../utils.js').itEnd 22 | const defer = require('../utils.js').defer 23 | const getCoinId = require('../utils.js').getCoinId 24 | 25 | /* ******** HELPER FUNCTIONS ********** */ 26 | function getHexStringProof(proof) { 27 | // TODO: Remove this and instead support buffers by default 28 | let inclusionProof = [] 29 | for (const sibling of proof) { 30 | inclusionProof.push( 31 | sibling.hash.toString('hex') + sibling.sum.toString('hex', 32) 32 | ) 33 | } 34 | return inclusionProof 35 | } 36 | 37 | function makeDepositKey(type, start) { 38 | return Buffer.concat([BLOCK_DEPOSIT_PREFIX, getCoinId(type, start)]) 39 | } 40 | 41 | class BlockStore { 42 | constructor(db, txLogDir) { 43 | log('Creating new block store') 44 | this.db = db 45 | this.sumTree = new LevelDBSumTree(this.db) 46 | this.txLogDir = txLogDir 47 | this.partialChunk = null 48 | this.batchPromises = [] 49 | this.blockNumberBN = new BN(0) // Set block number to be -1 so that the first block is block 0 50 | this.newBlockQueue = [] 51 | this.lastBlockNumTxs = new BN(0) 52 | } 53 | 54 | async addBlock(txLogFile) { 55 | log('Adding new block:', txLogFile) 56 | const deferred = defer() 57 | this.newBlockQueue.push({ txLogFile, resolve: deferred.resolve }) 58 | if (this.newBlockQueue.length === 1) { 59 | this._processNewBlockQueue() 60 | } 61 | return deferred.promise 62 | } 63 | 64 | async addDeposit(depositTx) { 65 | log( 66 | 'Adding new deposit for', 67 | depositTx.tr.recipient, 68 | 'with start:', 69 | depositTx.tr.start.toString(16), 70 | 'and end:', 71 | depositTx.tr.end.toString(16) 72 | ) 73 | // Store the deposit in our database in a similar way that we store normal transactions, by start position 74 | await this.db.put( 75 | makeDepositKey(depositTx.tr.token, depositTx.tr.start), 76 | Buffer.from(depositTx.encoded, 'hex') 77 | ) 78 | } 79 | 80 | async getRootHash(blockNumberBN) { 81 | const blockNumber = blockNumberBN.toArrayLike( 82 | Buffer, 83 | 'big', 84 | BLOCKNUMBER_BYTE_SIZE 85 | ) 86 | const rootHash = await this.db.get( 87 | Buffer.concat([BLOCK_ROOT_HASH_PREFIX, blockNumber]) 88 | ) 89 | return rootHash 90 | } 91 | 92 | async getBlockMetadata(startBlockBN, endBlockBN) { 93 | let blockNumberBN = new BN(startBlockBN) 94 | const metadata = [] 95 | for ( 96 | blockNumberBN; 97 | blockNumberBN.lte(endBlockBN); 98 | blockNumberBN = blockNumberBN.addn(1) 99 | ) { 100 | const blockNumber = blockNumberBN.toArrayLike( 101 | Buffer, 102 | 'big', 103 | BLOCKNUMBER_BYTE_SIZE 104 | ) 105 | try { 106 | const rootHash = await this.db.get( 107 | Buffer.concat([BLOCK_ROOT_HASH_PREFIX, blockNumber]) 108 | ) 109 | const timestamp = await this.db.get( 110 | Buffer.concat([BLOCK_TIMESTAMP_PREFIX, blockNumber]) 111 | ) 112 | const numTxs = await this.db.get( 113 | Buffer.concat([BLOCK_NUM_TXS_PREFIX, blockNumber]) 114 | ) 115 | metadata.push({ 116 | blockNumber, 117 | rootHash, 118 | timestamp, 119 | numTxs, 120 | }) 121 | } catch (err) {} 122 | } 123 | return metadata 124 | } 125 | 126 | async getTxFromHash(txHash) { 127 | let txEncoding 128 | try { 129 | txEncoding = await this.db.get(Buffer.concat([HASH_TO_TX_PREFIX, txHash])) 130 | } catch (err) { 131 | return { error: 'Tx hash: ' + txHash.toString('hex') + ' not found!' } 132 | } 133 | return txEncoding 134 | } 135 | 136 | async _processNewBlockQueue() { 137 | let numBlocksProcessed 138 | for ( 139 | numBlocksProcessed = 0; 140 | numBlocksProcessed < this.newBlockQueue.length; 141 | numBlocksProcessed++ 142 | ) { 143 | this.newBlockQueue[ 144 | numBlocksProcessed 145 | ].newBlock = await this._processBlock( 146 | this.newBlockQueue[numBlocksProcessed].txLogFile 147 | ) 148 | } 149 | const processedBlocks = this.newBlockQueue.splice(0, numBlocksProcessed) 150 | for (const processedBlock of processedBlocks) { 151 | processedBlock.resolve(processedBlock.newBlock) 152 | } 153 | } 154 | 155 | async _processBlock(txLogFile) { 156 | const blockNumberBN = new BN(txLogFile) 157 | const blockNumber = blockNumberBN.toArrayLike( 158 | Buffer, 159 | 'big', 160 | BLOCKNUMBER_BYTE_SIZE 161 | ) 162 | if (!this.blockNumberBN.add(new BN(1)).eq(blockNumberBN)) { 163 | // TODO: After launch figure out why I was even storing block number in the first place....? Either store in DB or remove it. 164 | log( 165 | 'Warning: Expected block number to be ' + 166 | this.blockNumberBN.add(new BN(1)).toString() + 167 | ' not ' + 168 | blockNumberBN.toString() 169 | ) 170 | } 171 | await this.ingestBlock(blockNumber, this.txLogDir + txLogFile) 172 | const rootHash = await this.sumTree.generateTree(blockNumber) 173 | // Set block metadata for the block explorer api 174 | this.db.put(Buffer.concat([BLOCK_ROOT_HASH_PREFIX, blockNumber]), rootHash) 175 | this.db.put( 176 | Buffer.concat([BLOCK_TIMESTAMP_PREFIX, blockNumber]), 177 | new BN(Date.now()).toArrayLike(Buffer, 'big') 178 | ) 179 | this.db.put( 180 | Buffer.concat([BLOCK_NUM_TXS_PREFIX, blockNumber]), 181 | this.lastBlockNumTxs.toArrayLike(Buffer, 'big') 182 | ) // lastBlockNumTxs computed during ingestion 183 | this.blockNumberBN = this.blockNumberBN.addn(1) 184 | log( 185 | 'Adding block number:', 186 | blockNumberBN.toString(), 187 | 'with root hash:', 188 | Buffer.from(rootHash).toString('hex') 189 | ) 190 | return { 191 | blockNumber, 192 | rootHash, 193 | } 194 | } 195 | 196 | /* 197 | * History proof logic 198 | */ 199 | async getLeavesAt(blockNumber, token, start, end, maxLeaves) { 200 | let startKey 201 | let endKey 202 | if (token === 'none') { 203 | startKey = start.toArrayLike(Buffer, 'big', 16) 204 | endKey = end.toArrayLike(Buffer, 'big', 16) 205 | } else { 206 | startKey = makeBlockTxKey(blockNumber, token, start) 207 | endKey = makeBlockTxKey(blockNumber, token, end) 208 | } 209 | if (maxLeaves === undefined) { 210 | maxLeaves = 50000 211 | } 212 | const it = this.db.iterator({ 213 | lt: endKey, 214 | reverse: true, 215 | }) 216 | let result = await this._getNextBlockTx(it) 217 | const ranges = [result] 218 | // Make sure that we returned values that we expect 219 | while (result.key > startKey) { 220 | result = await this._getNextBlockTx(it) 221 | ranges.push(result) 222 | if (ranges.length > maxLeaves) { 223 | await itEnd(it) 224 | return ranges 225 | } 226 | } 227 | await itEnd(it) 228 | return ranges 229 | } 230 | 231 | async _getNextBlockTx(it) { 232 | const result = await itNext(it) 233 | if (result.key === undefined) { 234 | await itEnd(it) 235 | throw new Error('getLeavesAt iterator returned undefined!') 236 | } 237 | if (result.key[0] !== BLOCK_TX_PREFIX[0]) { 238 | await itEnd(it) 239 | throw new Error('Expected BLOCK_TX_PREFIX instead of ' + result.key[0]) 240 | } 241 | return result 242 | } 243 | 244 | async getDepositsAt(token, start, end) { 245 | const startKey = makeDepositKey(token, start) 246 | const endKey = makeDepositKey(token, end) 247 | const it = this.db.iterator({ 248 | lt: endKey, 249 | reverse: true, 250 | }) 251 | let [result, deposit] = await this._getNextDeposit(it) 252 | const deposits = [deposit] 253 | let earliestBlock = new BN(deposit.block) 254 | // Make sure that we returned values that we expect 255 | while (result.key > startKey) { 256 | ;[result, deposit] = await this._getNextDeposit(it) 257 | deposits.push(deposit) 258 | if (earliestBlock.gt(deposit.block)) { 259 | earliestBlock = new BN(deposit.block) 260 | } 261 | } 262 | await itEnd(it) 263 | return [deposits, earliestBlock] 264 | } 265 | 266 | async _getNextDeposit(it) { 267 | const result = await itNext(it) 268 | if (result.key === undefined) { 269 | await itEnd(it) 270 | throw new Error('getDepositsAt iterator returned undefined!') 271 | } 272 | if (result.key[0] !== BLOCK_DEPOSIT_PREFIX[0]) { 273 | await itEnd(it) 274 | throw new Error( 275 | 'Expected BLOCK_DEPOSIT_PREFIX instead of ' + result.key[0] 276 | ) 277 | } 278 | const deposit = new UnsignedTransaction(result.value.toString('hex')) 279 | return [result, deposit] 280 | } 281 | 282 | async getTransactions( 283 | startBlockNumberBN, 284 | endBlockNumberBN, 285 | token, 286 | start, 287 | end, 288 | maxTransactions 289 | ) { 290 | let blockNumberBN = startBlockNumberBN 291 | const relevantTransactions = [] 292 | while (blockNumberBN.lte(endBlockNumberBN)) { 293 | const blockNumberKey = blockNumberBN.toArrayLike( 294 | Buffer, 295 | 'big', 296 | BLOCKNUMBER_BYTE_SIZE 297 | ) 298 | const ranges = await this.getLeavesAt( 299 | blockNumberKey, 300 | token, 301 | start, 302 | end, 303 | maxTransactions 304 | ) 305 | for (const r of ranges) { 306 | const tx = await this.sumTree.getTransactionFromLeaf(r.value) 307 | relevantTransactions.push(tx) 308 | } 309 | if ( 310 | maxTransactions !== undefined && 311 | relevantTransactions.length > maxTransactions 312 | ) { 313 | return relevantTransactions 314 | } 315 | blockNumberBN = blockNumberBN.add(new BN(1)) 316 | } 317 | return relevantTransactions 318 | } 319 | 320 | async getTxsWithProofsFor(blockNumber, token, start, end) { 321 | const numLevels = await this.sumTree.getNumLevels(blockNumber) 322 | const leaves = await this.getLeavesAt(blockNumber, token, start, end) 323 | const txProofs = [] 324 | for (const leaf of leaves) { 325 | const transaction = this.sumTree.getTransactionFromLeaf(leaf.value) 326 | const transactionProof = await this.getTransactionInclusionProof( 327 | transaction, 328 | blockNumber, 329 | numLevels 330 | ) 331 | txProofs.push({ 332 | transaction, 333 | transactionProof, 334 | }) 335 | } 336 | return txProofs 337 | } 338 | 339 | async getTxsWithProofs( 340 | startBlockNumberBN, 341 | endBlockNumberBN, 342 | token, 343 | start, 344 | end 345 | ) { 346 | let blockNumberBN = startBlockNumberBN 347 | const transactionProofs = {} 348 | while (blockNumberBN.lte(endBlockNumberBN)) { 349 | const blockNumberKey = blockNumberBN.toArrayLike( 350 | Buffer, 351 | 'big', 352 | BLOCKNUMBER_BYTE_SIZE 353 | ) 354 | const proofs = await this.getTxsWithProofsFor( 355 | blockNumberKey, 356 | token, 357 | start, 358 | end 359 | ) 360 | transactionProofs[blockNumberBN.toString()] = proofs 361 | blockNumberBN = blockNumberBN.add(new BN(1)) 362 | } 363 | return transactionProofs 364 | } 365 | 366 | async getTxHistory(startBlockNumberBN, endBlockNumberBN, transaction) { 367 | let blockNumberBN = startBlockNumberBN.eqn(0) 368 | ? new BN(1) 369 | : startBlockNumberBN 370 | // First get all of the deposits for each transaction 371 | let deposits = [] 372 | const earliestBlocks = [] 373 | for (const transfer of transaction.transfers) { 374 | const [transferDeposits, earliestBlock] = await this.getDepositsAt( 375 | transfer.token, 376 | transfer.start, 377 | transfer.end 378 | ) 379 | // Get the earliest deposit & set the transfer `earliestDeposit` field to it 380 | deposits = deposits.concat(transferDeposits) 381 | earliestBlocks.push(earliestBlock) 382 | } 383 | const transactionHistory = {} 384 | while (blockNumberBN.lte(endBlockNumberBN)) { 385 | const rootHash = await this.getRootHash(blockNumberBN) 386 | if (new BN(rootHash).eq(new BN(0))) { 387 | blockNumberBN = blockNumberBN.addn(1) 388 | continue 389 | } 390 | 391 | let proofs = [] 392 | for (const [i, transfer] of transaction.transfers.entries()) { 393 | if (blockNumberBN.lte(earliestBlocks[i])) { 394 | log('Reached deposit end for transfer', i) 395 | // Stop retrieving proofs if we have reached the block which got the deposit for the transaction 396 | continue 397 | } 398 | // For each one of the transfer records get the proofs 399 | const blockNumberKey = blockNumberBN.toArrayLike( 400 | Buffer, 401 | 'big', 402 | BLOCKNUMBER_BYTE_SIZE 403 | ) 404 | const rangeTxProof = await this.getTxsWithProofsFor( 405 | blockNumberKey, 406 | transfer.token, 407 | transfer.start, 408 | transfer.end 409 | ) 410 | proofs = proofs.concat(rangeTxProof) 411 | } 412 | transactionHistory[blockNumberBN.toString()] = proofs 413 | blockNumberBN = blockNumberBN.add(new BN(1)) 414 | } 415 | return { 416 | deposits, 417 | transactionHistory, 418 | } 419 | } 420 | 421 | async getTransactionInclusionProof(transaction, blockNumber, numLevels) { 422 | const getTr = (tx, trIndex) => new Transfer(tx.transfers[trIndex]) 423 | const transferProofs = [] 424 | // For all transfers in our transaction, get transfer proof 425 | for (let i = 0; i < transaction.transfers.length; i++) { 426 | // First we need the index in the merkle sum tree of this leaf 427 | const trEncoding = Buffer.from(getTr(transaction, i).encoded, 'hex') 428 | const leafIndex = await this.sumTree.getIndex(blockNumber, trEncoding) 429 | // Now get the transfer inclusion proof 430 | const inclusionProof = await this.getTransferInclusionProof( 431 | blockNumber, 432 | numLevels, 433 | new BN(leafIndex) 434 | ) 435 | const trProof = { 436 | parsedSum: new BN(inclusionProof.includedNode.sum), 437 | transaction: transaction, 438 | leafIndex, 439 | signature: transaction.signatures[i], 440 | inclusionProof: getHexStringProof(inclusionProof.proof), 441 | } 442 | // Add it to our transaction proof 443 | transferProofs.push(trProof) 444 | } 445 | return new TransactionProof({ transferProofs }) 446 | } 447 | 448 | async getTransferInclusionProof(blockNumber, numLevels, index) { 449 | const proof = [] 450 | 451 | // Included node 452 | const includedNodeValue = await this.sumTree.getNode(blockNumber, 0, index) 453 | const includedNode = this.sumTree.parseNodeValue(includedNodeValue) 454 | log( 455 | 'Included node hash:', 456 | includedNode.hash.toString('hex'), 457 | '--sum:', 458 | includedNode.sum.toString(16) 459 | ) 460 | 461 | let parentIndex 462 | let nodeValue 463 | let node 464 | let siblingIndex = index.addn(index.mod(new BN(2)).eq(new BN(0)) ? 1 : -1) 465 | for (let i = 0; i < numLevels; i++) { 466 | try { 467 | nodeValue = await this.sumTree.getNode(blockNumber, i, siblingIndex) 468 | node = this.sumTree.parseNodeValue(nodeValue) 469 | } catch (err) { 470 | if (err.type === 'NotFoundError') { 471 | log('Node not found in block tree! Treating it as an empty leaf...') 472 | nodeValue = undefined 473 | node = this.sumTree.emptyNode() 474 | } else throw err 475 | } 476 | proof.push({ 477 | hash: node.hash, 478 | sum: node.sum, 479 | }) 480 | 481 | // Figure out the parent and then figure out the parent's sibling. 482 | parentIndex = siblingIndex.eq(new BN(0)) 483 | ? new BN(0) 484 | : siblingIndex.divn(2) 485 | siblingIndex = parentIndex.addn( 486 | parentIndex.mod(new BN(2)).eq(new BN(0)) ? 1 : -1 487 | ) 488 | } 489 | return { 490 | includedNode, 491 | proof, 492 | } 493 | } 494 | 495 | /* 496 | * Block ingestion logic 497 | */ 498 | ingestBlock(blockNumber, txLogFilePath) { 499 | const self = this 500 | log('Generating new block for block:', blockNumber) 501 | // First set the total number of txs for the latest block to zero. This just metadata for the block 502 | this.lastBlockNumTxs = new BN(0) 503 | const readStream = fs.createReadStream(txLogFilePath) 504 | let chunkNumber = 0 505 | readStream.on('data', function(chunk) { 506 | log('Read chunk of size:', chunk.length) 507 | self.parseTxBinary(blockNumber, chunk, chunkNumber) 508 | chunkNumber++ 509 | }) 510 | // Return a promise which resolves once the entire file has been read 511 | return new Promise((resolve, reject) => { 512 | readStream.on('end', (res) => { 513 | log('Finished reading all chunks') 514 | Promise.all(this.batchPromises).then(() => { 515 | log('Finished ingesting & sorting all chunks') 516 | resolve() 517 | }) 518 | }) 519 | }) 520 | } 521 | 522 | readNextTransaction(cursor, chunk) { 523 | const numElements = new BN(chunk.slice(cursor + 4, cursor + 5)).toNumber() 524 | const transferSize = numElements * TRANSFER_BYTE_SIZE 525 | const signatureSize = numElements * SIGNATURE_BYTE_SIZE 526 | const txSize = BLOCKNUMBER_BYTE_SIZE + transferSize + signatureSize + 2 // We have two length identifiers, so plus 2 527 | // Check if this transaction is the very last in our chunk 528 | if (cursor + txSize > chunk.length) { 529 | // Set partial tx 530 | this.partialChunk = chunk.slice(cursor) 531 | return [null] 532 | } 533 | const txStart = cursor 534 | const txEnd = txStart + txSize 535 | // Make the transaction object 536 | const nextTransaction = new SignedTransaction( 537 | chunk.slice(txStart, txEnd).toString('hex') 538 | ) 539 | log('Read new transaction') 540 | this.lastBlockNumTxs = this.lastBlockNumTxs.addn(1) // Increment the total number of txs which we've added 541 | return [cursor + txSize, nextTransaction, chunk.slice(cursor, txEnd)] 542 | } 543 | 544 | parseTxBinary(blockNumber, chunk, chunkNumber) { 545 | if (this.partialChunk != null) { 546 | chunk = Buffer.concat([this.partialChunk, chunk]) 547 | } 548 | const txBundle = [] 549 | let [cursor, nextTx, nextTxEncoding] = this.readNextTransaction(0, chunk) 550 | while (cursor !== null) { 551 | txBundle.push([nextTx, nextTxEncoding]) 552 | ;[cursor, nextTx, nextTxEncoding] = this.readNextTransaction( 553 | cursor, 554 | chunk 555 | ) 556 | } 557 | 558 | // Leftmost transfer of the first transaction in a block MUST start at zero. 559 | // We should probably get rid of this later on. 560 | if (chunkNumber === 0 && txBundle.length > 0) { 561 | const leftmost = txBundle[0][0].transfers 562 | .sort((a, b) => { 563 | return a.token.sub(b.token) 564 | }) 565 | .reduce((prev, curr) => { 566 | return prev.start.lt(curr.start) ? prev : curr 567 | }) 568 | leftmost.token = new BN(0) 569 | leftmost.start = new BN(0) 570 | } 571 | 572 | this.storeTransactions(blockNumber, txBundle) 573 | } 574 | 575 | storeTransactions(blockNumber, txBundle) { 576 | // Ingest these transactions, into levelDB as `blocknum + typedStart + 577 | const dbBatch = [] 578 | for (const tx of txBundle) { 579 | for (const [i, tr] of tx[0].transfers.entries()) { 580 | log('Storing tx at:', makeBlockTxKey(blockNumber, tr.token, tr.start)) 581 | dbBatch.push({ 582 | type: 'put', 583 | key: makeBlockTxKey(blockNumber, tr.token, tr.start), 584 | value: Buffer.concat([Buffer.from([i]), Buffer.from(tx[1])]), // Store as index of the TR & then transaction 585 | }) 586 | } 587 | } 588 | this.batchPromises.push(this.db.batch(dbBatch)) 589 | } 590 | } 591 | 592 | module.exports = BlockStore 593 | --------------------------------------------------------------------------------