├── parity ├── pw └── boot.js ├── .gitattributes ├── .gitignore ├── truffle-config.js ├── secretsTEMPLATE.json ├── ethpm.json ├── contracts ├── Migrations.sol ├── RLPEncode.sol ├── MerklePatriciaProof.sol ├── BytesLib.sol ├── RLP.sol └── Bridge.sol ├── truffle.js ├── LICENSE ├── test ├── util │ ├── val.js │ ├── sync.js │ ├── merkle.js │ ├── receiptProof.js │ ├── blocks.js │ └── txProof.js └── bridge.js ├── package.json ├── migrations └── 1_initial_migration.js ├── scripts └── sendTokens.js └── README.md /parity/pw: -------------------------------------------------------------------------------- 1 | password 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | parity/chains 2 | build 3 | installed_contracts 4 | secrets.json 5 | node_modules 6 | lastBlock 7 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // See 3 | // to customize your Truffle configuration! 4 | }; 5 | -------------------------------------------------------------------------------- /secretsTEMPLATE.json: -------------------------------------------------------------------------------- 1 | { 2 | "mnemonic": "public okay smoke segment forum front animal extra appear online before various cook test arrow", 3 | "hdPath": "m/44'/60'/0'/0/" 4 | } -------------------------------------------------------------------------------- /ethpm.json: -------------------------------------------------------------------------------- 1 | { 2 | "package_name": "trustless-bridge", 3 | "version": "0.1.0", 4 | "description": "Connect assets on any two EVM chains with a bridge.", 5 | "authors": [ 6 | "Alex Miller " 7 | ], 8 | "keywords": [ 9 | "ethereum", 10 | "bridge", 11 | "evm" 12 | ], 13 | "dependencies": { 14 | "tokens": "^1.0.0" 15 | }, 16 | "license": "MIT" 17 | } 18 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.17; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public last_completed_migration; 6 | 7 | modifier restricted() { 8 | if (msg.sender == owner) _; 9 | } 10 | 11 | function Migrations() public { 12 | owner = msg.sender; 13 | } 14 | 15 | function setCompleted(uint completed) public restricted { 16 | last_completed_migration = completed; 17 | } 18 | 19 | function upgrade(address new_address) public restricted { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(last_completed_migration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /truffle.js: -------------------------------------------------------------------------------- 1 | const HDWalletProvider = require('truffle-hdwallet-provider'); 2 | const secrets = require('./secrets.json'); 3 | const bip39 = require('bip39'); 4 | const hdkey = require('ethereumjs-wallet/hdkey'); 5 | const hdwallet = hdkey.fromMasterSeed(bip39.mnemonicToSeed(secrets.mnemonic)); 6 | const node = hdwallet.derivePath(secrets.hdPath + '0'); 7 | const addr = node.getWallet().getAddressString(); 8 | 9 | module.exports = { 10 | networks: { 11 | development: { 12 | name: "Dev", 13 | host: 'localhost', 14 | port: 7545, 15 | network_id: '*', // Match any network id 16 | gas: 20000000 // https://github.com/trufflesuite/truffle/issues/825#issuecomment-369189238 17 | }, 18 | developmentB: { 19 | name: "devB", 20 | host: 'localhost', 21 | port: 8545, 22 | network_id: '*', 23 | gas: 20000000 24 | }, 25 | /*ropsten: { 26 | provider: new HDWalletProvider(secrets.mnemonic, 'https://ropsten.infura.io/'), 27 | network_id: 3, // official id of the ropsten network 28 | },*/ 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Grid+ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/util/val.js: -------------------------------------------------------------------------------- 1 | // Functions for the validators (signatures, messaging) 2 | const ethutil = require('ethereumjs-util'); 3 | const leftPad = require('left-pad'); 4 | const sha3 = require('solidity-sha3').default; 5 | 6 | // Get the formatted message for signing. Should be replicable in solidity 7 | function getMsg(headerRoot, chain, start, end) { 8 | const solStart = leftPad(start.toString(16), 64, '0'); 9 | const solEnd = leftPad(end.toString(16), 64, '0'); 10 | const msg = `0x${headerRoot.slice(2)}${chain.slice(2)}${solStart}${solEnd}`; 11 | return sha3(msg); 12 | } 13 | 14 | // Get signature on a piece of data 15 | function sign(msg, wallet) { 16 | const msgBuf = Buffer.from(msg.slice(2), 'hex'); 17 | const pkey = Buffer.from(wallet[1].slice(2), 'hex'); 18 | const sigTmp = ethutil.ecsign(msgBuf, pkey); 19 | const newSig = { 20 | r: leftPad(sigTmp.r.toString('hex'), 64, '0'), 21 | s: leftPad(sigTmp.s.toString('hex'), 64, '0'), 22 | v: leftPad(sigTmp.v.toString(16), 64, '0'), 23 | }; 24 | return newSig; 25 | } 26 | 27 | function formatSigs(sigs) { 28 | let data = '0x'; 29 | sigs.forEach((sig) => { 30 | const tmp = `${sig.r}${sig.s}${sig.v}`; 31 | data += tmp; 32 | }) 33 | return data; 34 | } 35 | 36 | exports.formatSigs = formatSigs; 37 | exports.getMsg = getMsg; 38 | exports.sign = sign; 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trustless-relay", 3 | "version": "1.0.0", 4 | "description": "Contracts for trustless relay", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "parity": "node parity/boot.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/alex-miller-0/trustless-relay.git" 15 | }, 16 | "keywords": [ 17 | "Ethereum", 18 | "Blockchain", 19 | "Scaling" 20 | ], 21 | "author": "Alex Miller", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/alex-miller-0/trustless-relay/issues" 25 | }, 26 | "homepage": "https://github.com/alex-miller-0/trustless-relay#readme", 27 | "dependencies": { 28 | "async": "^2.6.0", 29 | "big-number": "^0.4.0", 30 | "bip39": "^2.5.0", 31 | "bluebird": "^3.5.1", 32 | "eth-proof": "^0.1.3", 33 | "ethereumjs-block": "^1.7.0", 34 | "ethereumjs-tx": "^1.3.3", 35 | "ethereumjs-util": "^5.1.3", 36 | "ethereumjs-wallet": "^0.6.0", 37 | "js-sha3": "^0.7.0", 38 | "jsonfile": "^4.0.0", 39 | "left-pad": "^1.2.0", 40 | "merkle-patricia-tree": "^2.3.0", 41 | "merkle-tools": "^1.4.0", 42 | "mtree": "^1.0.0", 43 | "rlp": "^2.0.0", 44 | "solidity-sha3": "^0.4.1", 45 | "truffle-hdwallet-provider": "0.0.3", 46 | "web3": "^1.0.0-beta.27", 47 | "yargs": "^11.0.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/util/sync.js: -------------------------------------------------------------------------------- 1 | const Promise = require('bluebird').Promise; 2 | const blocks = require('./blocks.js'); 3 | const fs = require('fs'); 4 | const path = `${process.cwd()}/lastBlock`; 5 | 6 | exports.fsSync = fsSync 7 | exports.syncChain = syncChain; 8 | 9 | 10 | function syncChain(web3, lastBlockNumber=null, lastHeader=null, w=false) { 11 | return new Promise((resolve, reject) => { 12 | let targetBlock; 13 | if (lastBlockNumber == 0) { lastBlockNumber = null; } 14 | web3.eth.getBlockNumber() 15 | .then((n) => { 16 | targetBlock = n; 17 | return blocks.getHeaders(lastBlockNumber, n, web3, [lastHeader]) 18 | }) 19 | .then((headers) => { 20 | const lastHeader = headers[headers.length - 1] || 0; 21 | if (w) { fs.writeFileSync(path, `${lastHeader},${targetBlock}`); } 22 | return resolve([lastHeader, targetBlock]); 23 | }) 24 | .catch((err) => { return reject(err); }) 25 | }) 26 | } 27 | 28 | function fsSync(web3) { 29 | return new Promise((resolve, reject) => { 30 | let targetBlock 31 | if (fs.existsSync(path)) { 32 | const f = fs.readFileSync(path).toString('utf8'); 33 | return resolve(f.split(',')); 34 | } else { 35 | web3.eth.getBlockNumber() 36 | .then((n) => { 37 | targetBlock = n; 38 | return blocks.getHeaders(0, n, web3) 39 | }) 40 | .then((headers) => { 41 | const lastHeader = headers[headers.length - 1]; 42 | if (path) { fs.writeFileSync(path, `${lastHeader},${targetBlock}`); } 43 | return resolve([lastHeader, targetBlock]); 44 | }) 45 | .catch((err) => { return reject(err); }) 46 | } 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | var Migrations = artifacts.require("./Migrations.sol"); 2 | var Bridge = artifacts.require('./Bridge.sol'); 3 | var MerkleLib = artifacts.require('./MerklePatriciaProof.sol'); 4 | var RLP = artifacts.require('./RLP.sol'); 5 | var EIP20 = artifacts.require('EIP20.sol'); 6 | 7 | const argv = require('yargs') 8 | .usage('Usage: [options]') 9 | .command('--live', 'Deploy a specific bridge with a stake token') 10 | .command('--token', 'Staking token to deploy on the bridge. If none is provided, deploy a new token.') 11 | .argv; 12 | 13 | // seems redundant, argv.live unused (e.g. does NOT deploy a specic bridge as mentioned above) 14 | if (argv.live) { 15 | module.exports = function(deployer) { 16 | deployer.deploy(Migrations); 17 | deployer.deploy(MerkleLib); 18 | deployer.link(MerkleLib, Bridge); 19 | deployer.deploy(RLP); 20 | deployer.link(RLP, MerkleLib); 21 | if (argv.token && argv.token != '') { 22 | deployer.deploy(Bridge, argv.token); 23 | } else { 24 | deployer.deploy(EIP20, 1000000000000, 'Staking', 0, 'STK') 25 | .then(() => { return deployer.deploy(Bridge, EIP20.address); }) 26 | } 27 | } 28 | } else { 29 | // TODO: code is neraly redundant 30 | module.exports = function(deployer) { 31 | deployer.deploy(Migrations); 32 | deployer.deploy(MerkleLib); 33 | deployer.link(MerkleLib, Bridge); 34 | deployer.deploy(RLP); 35 | deployer.link(RLP, MerkleLib); 36 | // do not call without constructor param, since truffle 4.0.6 this throws an error 37 | deployer.deploy(EIP20, 1000000000000, 'Staking', 0, 'STK') 38 | .then(() => { return deployer.deploy(Bridge, EIP20.address); }) 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /test/util/merkle.js: -------------------------------------------------------------------------------- 1 | // Merkle trees, proofs, headers 2 | // Solidity hashes things strangely, so I'm building my own Merkle tools 3 | const sha3 = require('solidity-sha3').default; 4 | 5 | exports.buildTree = buildTree; 6 | exports.checkProof = checkProof; 7 | exports.getProof = getProof; 8 | exports.getProofStr = getProofStr; 9 | // Builds a 2D array Merkle tree with: 10 | // layers[0] = leaves 11 | // layers[N] = root 12 | function buildTree(nodes, layers=[]) { 13 | layers.push(nodes); 14 | if (nodes.length < 2) { return layers; } 15 | let newNodes = []; 16 | for (let i = 0; i < nodes.length - 1; i += 2) { 17 | newNodes.push(hash(nodes[i], nodes[i+1])); 18 | } 19 | return buildTree(newNodes, layers); 20 | } 21 | 22 | // Form a Merkle proof on a 2D array Merkle tree. 23 | // If the tree is formed incorrectly, this will return null (indicates root 24 | // does not match up) 25 | function getProof(i, tree) { 26 | let proof = []; 27 | let currentHash; 28 | for (let L = 0; L < tree.length - 1; L ++) { 29 | // If this index is on the left, return true 30 | const partnerIsRight = i % 2 == 0; 31 | let partner; 32 | let _proof = [ partnerIsRight ]; 33 | // Get partner node 34 | if (partnerIsRight) { 35 | partner = tree[L][i + 1]; 36 | currentHash = hash(tree[L][i], partner); 37 | } else { 38 | partner = tree[L][i - 1]; 39 | currentHash = hash(partner, tree[L][i]); 40 | } 41 | _proof.push(partner); 42 | proof.push(_proof); 43 | i = Math.floor(i / 2); 44 | } 45 | if (currentHash != tree[tree.length - 1]) { return null; } 46 | else { return proof; } 47 | } 48 | 49 | function checkProof(leaf, proof, targetHash) { 50 | let currentHash = leaf; 51 | proof.forEach((partner) => { 52 | if (partner[0] == true) { 53 | // partner node is on the right 54 | currentHash = hash(currentHash, partner[1]); 55 | } else { 56 | currentHash = hash(partner[1], currentHash); 57 | } 58 | }) 59 | return currentHash == targetHash; 60 | } 61 | 62 | // Convert the proof to a string consumable by solidity function 63 | // Is of form 0x${partnerIsRight_i}${hash_i} 64 | function getProofStr(proof) { 65 | let proofStr = '0x'; 66 | proof.forEach((p) => { 67 | proofStr += p[0] == true ? '01' : '00'; 68 | proofStr += p[1].slice(2); 69 | }) 70 | return proofStr; 71 | } 72 | 73 | 74 | function hash(left, right) { 75 | return sha3(`0x${left.slice(2)}${right.slice(2)}`); 76 | } 77 | -------------------------------------------------------------------------------- /contracts/RLPEncode.sol: -------------------------------------------------------------------------------- 1 | // Library for RLP encoding a list of bytes arrays. 2 | // Modeled after ethereumjs/rlp (https://github.com/ethereumjs/rlp) 3 | // [Very] modified version of Sam Mayo's library. 4 | pragma solidity ^0.4.18; 5 | import "./BytesLib.sol"; 6 | 7 | library RLPEncode { 8 | 9 | // Encode an item (bytes) 10 | function encodeItem(bytes memory self) internal constant returns (bytes) { 11 | bytes memory encoded; 12 | if(self.length == 1 && uint(self[0]) < 0x80) { 13 | encoded = new bytes(1); 14 | encoded = self; 15 | } else { 16 | encoded = BytesLib.concat(encodeLength(self.length, 128), self); 17 | } 18 | return encoded; 19 | } 20 | 21 | // Encode a list of items 22 | function encodeList(bytes[] memory self) internal constant returns (bytes) { 23 | bytes memory encoded; 24 | for (uint i=0; i < self.length; i++) { 25 | encoded = BytesLib.concat(encoded, encodeItem(self[i])); 26 | } 27 | return BytesLib.concat(encodeLength(encoded.length, 192), encoded); 28 | } 29 | 30 | // Hack to encode nested lists. If you have a list as an item passed here, included 31 | // pass = true in that index. E.g. 32 | // [item, list, item] --> pass = [false, true, false] 33 | function encodeListWithPasses(bytes[] memory self, bool[] pass) internal constant returns (bytes) { 34 | bytes memory encoded; 35 | for (uint i=0; i < self.length; i++) { 36 | if (pass[i] == true) { 37 | encoded = BytesLib.concat(encoded, self[i]); 38 | } else { 39 | encoded = BytesLib.concat(encoded, encodeItem(self[i])); 40 | } 41 | } 42 | return BytesLib.concat(encodeLength(encoded.length, 192), encoded); 43 | } 44 | 45 | // Generate the prefix for an item or the entire list based on RLP spec 46 | function encodeLength(uint256 L, uint256 offset) internal constant returns (bytes) { 47 | if (L < 56) { 48 | bytes memory prefix = new bytes(1); 49 | prefix[0] = byte(L + offset); 50 | return prefix; 51 | } else { 52 | // lenLen is the length of the hex representation of the data length 53 | uint lenLen; 54 | uint i = 0x1; 55 | while(L/i != 0) { 56 | lenLen++; 57 | i *= 0x100; 58 | } 59 | bytes memory prefix0 = getLengthBytes(offset + 55 + lenLen); 60 | bytes memory prefix1 = getLengthBytes(L); 61 | return BytesLib.concat(prefix0, prefix1); 62 | } 63 | } 64 | 65 | function getLengthBytes(uint256 x) constant returns (bytes b) { 66 | // Figure out if we need 1 or two bytes to express the length. 67 | // 1 byte gets us to max 255 68 | // 2 bytes gets us to max 65535 (no payloads will be larger than this) 69 | uint256 nBytes = 1; 70 | if (x > 255) { nBytes = 2; } 71 | b = new bytes(nBytes); 72 | // Encode the length and return it 73 | for (uint i = 0; i < nBytes; i++) { 74 | b[i] = byte(uint8(x / (2**(8*(nBytes - 1 - i))))); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /scripts/sendTokens.js: -------------------------------------------------------------------------------- 1 | const bip39 = require('bip39'); 2 | const hdkey = require('ethereumjs-wallet/hdkey'); 3 | const secrets = require('../secrets.json'); 4 | const Web3 = require('web3'); 5 | const leftPad = require('left-pad'); 6 | 7 | const argv = require('yargs') 8 | .usage('Usage: [options]') 9 | .command('--to', 'Address to send tokens to, 0x prefixed') 10 | .command('--from', 'Address to send tokens from. Must be in set of accounts (--accounts to list)') 11 | .alias('-f', '--from') 12 | .command('--accounts', 'List accounts that can send tokens') 13 | .alias('-a', '--accounts') 14 | .command('--token', 'Address of token to send') 15 | .command('--number', 'Number of tokens to send (default 1)') 16 | .alias('-n', '--number') 17 | .command('--host', 'Host of the chain to query (default localhost:8545)') 18 | .alias('-h', '--host') 19 | .argv; 20 | 21 | const host = argv.host ? argv.host : 'http://localhost:7545'; 22 | console.log('host', host) 23 | const provider = new Web3.providers.HttpProvider(host); 24 | const web3 = new Web3(provider); 25 | 26 | // Send some tokens from the main account to the desired recipient 27 | function generateFirstWallets(n, _wallets, hdPathIndex) { 28 | const hdwallet = hdkey.fromMasterSeed(bip39.mnemonicToSeed(secrets.mnemonic)); 29 | const node = hdwallet.derivePath(secrets.hdPath + hdPathIndex.toString()); 30 | const secretKey = node.getWallet().getPrivateKeyString(); 31 | const addr = node.getWallet().getAddressString(); 32 | _wallets.push([addr, secretKey]); 33 | const nextHDPathIndex = hdPathIndex + 1; 34 | if (nextHDPathIndex >= n) { 35 | return _wallets; 36 | } 37 | return generateFirstWallets(n, _wallets, nextHDPathIndex); 38 | } 39 | 40 | // First 4 default accounts (wallets[0] is not in the set) 41 | const wallets = generateFirstWallets(5, [], 0); 42 | if (argv.accounts) { 43 | console.log('Listing accounts you can send tokens from') 44 | wallets.forEach((w, i) => { console.log(`[${i}]\t${w[0]}`)}) 45 | } else { 46 | if (!argv.to) { console.log('You must specify who to send to (--to)'); } 47 | if (!argv.from) { argv.from = wallets[1][0]; } 48 | const n = argv.number ? argv.number : 1; 49 | const gas = 100000; 50 | let tx = { 51 | from: argv.from, 52 | gas 53 | } 54 | if (!argv.token) { 55 | tx.value = parseInt(n); 56 | tx.to = argv.to; 57 | } else { 58 | tx.data = `0xa9059cbb${leftPad(argv.to.slice(2), 64, '0')}${leftPad(n.toString(16), 64, '0')}`; 59 | tx.to = argv.token; 60 | } 61 | web3.eth.sendTransaction(tx, (err, res) => { 62 | if (err) { console.log('Error sending token: ', err); } 63 | else { 64 | web3.eth.getTransactionReceipt(res, (err, receipt) => { 65 | if (err) { console.log(`Error getting transaction receipt: ${err}`); } 66 | else if (argv.token && receipt.logs.length < 1) { console.log('Error sending transaction. Are you sure you have enough tokens to send?'); } 67 | else { console.log(`${n} tokens (${argv.token ? argv.token : 'ether'}) successfully sent to ${argv.to}`)} 68 | }) 69 | } 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /test/util/receiptProof.js: -------------------------------------------------------------------------------- 1 | // Merkle-Patricia proof for a transaction receipt. 2 | // Modified from eth-proof 3 | const Promise = require('bluebird').Promise; 4 | const Trie = require('merkle-patricia-tree'); 5 | const rlp = require('rlp'); 6 | const async = require('async'); 7 | const EthereumBlock = require('ethereumjs-block/from-rpc'); 8 | 9 | exports.buildProof = buildProof; 10 | exports.encodeLogs = encodeLogs; 11 | 12 | function buildProof(receipt, block, web3) { 13 | return new Promise((resolve, reject) => { 14 | var receiptsTrie = new Trie(); 15 | Promise.map(block.transactions, (siblingTxHash) => { 16 | return web3.eth.getTransactionReceipt(siblingTxHash) 17 | }) 18 | .map((siblingReceipt) => { 19 | putReceipt(siblingReceipt, receiptsTrie, () => { 20 | return; 21 | }); 22 | }) 23 | .then(() => { 24 | receiptsTrie.findPath(rlp.encode(receipt.transactionIndex), (e,rawReceiptNode,remainder,stack) => { 25 | var prf = { 26 | blockHash: Buffer.from(receipt.blockHash.slice(2),'hex'), 27 | header: getRawHeader(block), 28 | parentNodes: rawStack(stack), 29 | path: rlp.encode(receipt.transactionIndex), 30 | value: rlp.decode(rawReceiptNode.value) 31 | } 32 | return resolve(prf) 33 | }) 34 | }) 35 | .catch((err) => { return reject(err); }); 36 | }) 37 | } 38 | 39 | var putReceipt = (siblingReceipt, receiptsTrie, cb2) => {//need siblings to rebuild trie 40 | var path = siblingReceipt.transactionIndex 41 | var cummulativeGas = numToBuf(siblingReceipt.cumulativeGasUsed) 42 | var bloomFilter = strToBuf(siblingReceipt.logsBloom) 43 | var setOfLogs = encodeLogs(siblingReceipt.logs) 44 | var rawReceipt; 45 | if (siblingReceipt.status !== undefined && siblingReceipt.status != null) { 46 | var status = strToBuf(siblingReceipt.status); 47 | rawReceipt = rlp.encode([status, cummulativeGas, bloomFilter, setOfLogs]); 48 | } else { 49 | var postTransactionState = strToBuf(siblingReceipt.root) 50 | rawReceipt = rlp.encode([postTransactionState, cummulativeGas, bloomFilter, setOfLogs]) 51 | } 52 | 53 | receiptsTrie.put(rlp.encode(path), rawReceipt, function (error) { 54 | error != null ? cb2(error, null) : cb2(error, true) 55 | }) 56 | } 57 | function encodeLogs(input) { 58 | var logs = [] 59 | for (var i = 0; i < input.length; i++) { 60 | var address = strToBuf(input[i].address); 61 | var topics = input[i].topics.map(strToBuf) 62 | var data = Buffer.from(input[i].data.slice(2),'hex') 63 | logs.push([address, topics, data]) 64 | } 65 | return logs 66 | } 67 | var rawStack = (input) => { 68 | output = [] 69 | for (var i = 0; i < input.length; i++) { 70 | output.push(input[i].raw) 71 | } 72 | return output 73 | } 74 | var getRawHeader = (_block) => { 75 | if(typeof _block.difficulty != 'string'){ 76 | _block.difficulty = '0x' + _block.difficulty.toString(16) 77 | } 78 | var block = new EthereumBlock(_block) 79 | return block.header.raw 80 | } 81 | var squanchTx = (tx) => { 82 | tx.gasPrice = '0x' + tx.gasPrice.toString(16) 83 | tx.value = '0x' + tx.value.toString(16) 84 | return tx; 85 | } 86 | var numToBuf = (input)=>{ return Buffer.from(byteable(input.toString(16)), "hex") } 87 | var byteable = (input)=>{ return input.length % 2 == 0 ? input : "0" + input } 88 | var strToBuf = (input)=>{ 89 | if(input.slice(0,2) == "0x"){ 90 | return Buffer.from(byteable(input.slice(2)), "hex") 91 | }else{ 92 | return Buffer.from(byteable(input), "hex") 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /test/util/blocks.js: -------------------------------------------------------------------------------- 1 | // Functions for forming proofs from blocks. These are modified block headers 2 | // which only contain the following data: 3 | // 1. Previous modified block header 4 | // 2. Block number 5 | // 3. Timestamp 6 | // 4. Transaction root hash 7 | // These data are hashed as tightly packed hex arguments in 256 bit words 8 | // in the above order. 9 | // NOTE: This means the verification is: block.modHeader == prevBlock.modHeader 10 | // This verification is done in solidity, so it is ignored 11 | const sha3 = require('solidity-sha3').default; 12 | const leftPad = require('left-pad'); 13 | 14 | // Get a range of headers. More efficient than pulling them individually. 15 | // NOTE: This is kinda cheating, since it references the genesis block as the 16 | // "previousHeader" for all ranges. That's fine for thse test cases, but will 17 | // not fly for production systems. PreviousHeaders should be saved in persistant 18 | // storage 19 | function getHeaders(start, end, web3, headers=[], i=null, parentRes=null) { 20 | return new Promise((resolve, reject) => { 21 | let lastBlock = 1; 22 | let lastHeader = null; 23 | if (!i) { i = start; } 24 | else { lastBlock = i - 1; } 25 | if (headers.length > 0) { lastHeader = headers[headers.length - 1]; } 26 | 27 | if (!parentRes) { parentRes = resolve; } 28 | if (end <= start || !end) { return resolve([]); } 29 | if (i == end + 1) { return parentRes(headers); } 30 | else { 31 | return getHeader(i, web3, lastHeader) 32 | .then((header) => { 33 | headers.push(header); 34 | i++; 35 | return getHeaders(start, end, web3, headers, i, parentRes); 36 | }) 37 | .catch((err) => { return reject(err); }) 38 | } 39 | }) 40 | } 41 | 42 | // Get a modified header for block N. This requires we look through the entire 43 | // history to modify headers 44 | function getHeader(N, web3, lastHeader) { 45 | return new Promise((resolve, reject) => { 46 | web3.eth.getBlock(N) 47 | .then((block) => { 48 | const header = hashHeader(block, lastHeader, N==0); 49 | return resolve(header); 50 | }) 51 | .catch((err) => { return reject(err); }) 52 | }); 53 | } 54 | 55 | // Get the root 56 | function getRoot(headers) { 57 | let nodes = headers; 58 | if (!isPowTwo(headers.length)) { return null; } 59 | while (nodes.length > 1) { 60 | let tmpNodes = []; 61 | for (let i = 0; i < nodes.length / 2; i++) { 62 | tmpNodes.push(sha3(nodes[i], nodes[i + 1])); 63 | } 64 | nodes = tmpNodes; 65 | } 66 | return nodes[0]; 67 | } 68 | 69 | function forceMine(n, account, web3, i=0, outerResolve=null, outerReject=null) { 70 | return new Promise((resolve, reject) => { 71 | if (i == 0) { outerResolve = resolve; outerReject = reject; } 72 | if (i == n) { return outerResolve(true); } 73 | else { 74 | web3.eth.sendTransaction({ from: account, to: account, value: 1}) 75 | .then(() => { forceMine(n, account, web3, i+1, outerResolve, outerReject); }) 76 | .catch((err) => { return outerReject(err); }) 77 | } 78 | }) 79 | } 80 | 81 | // Return the most recent power of two 82 | function getLastPowTwo(n) { 83 | return Math.pow(2, Math.floor(Math.log(n) / Math.log(2))) 84 | } 85 | 86 | // Return the next power of two 87 | function getNextPowTwo(n) { 88 | return Math.pow(2, Math.ceil(Math.log(n) / Math.log(2))) 89 | } 90 | 91 | 92 | function hashHeader(block, prevHeader, genesis=false) { 93 | const n = leftPad(parseInt(block.number).toString(16), 64, '0'); 94 | const ts = leftPad(parseInt(block.timestamp).toString(16), 64, '0'); 95 | let str; 96 | if (genesis) { 97 | const emptyHeader = leftPad(0, 64, '0'); 98 | const genesisN = leftPad(1, 64, '0'); 99 | str = `0x${emptyHeader}${ts}${genesisN}${block.transactionsRoot.slice(2)}${block.receiptsRoot.slice(2)}`; 100 | } 101 | else { 102 | str = `0x${prevHeader.slice(2)}${ts}${n}${block.transactionsRoot.slice(2)}${block.receiptsRoot.slice(2)}`; 103 | } 104 | return sha3(str); 105 | } 106 | 107 | function isPowTwo(n) { 108 | n = Math.floor(n); 109 | if (n == 0) return false; 110 | while (n != 1) { 111 | if (n % 2 != 0) return 0; 112 | n = Math.floor(n / 2); 113 | } 114 | return true; 115 | } 116 | 117 | exports.hashHeader = hashHeader; 118 | exports.getLastPowTwo = getLastPowTwo; 119 | exports.getNextPowTwo = getNextPowTwo; 120 | exports.getHeader = getHeader; 121 | exports.getHeaders = getHeaders; 122 | exports.getRoot = getRoot; 123 | exports.forceMine = forceMine; 124 | exports.isPowTwo = isPowTwo; 125 | -------------------------------------------------------------------------------- /contracts/MerklePatriciaProof.sol: -------------------------------------------------------------------------------- 1 | /* 2 | * @title MerklePatriciaVerifier 3 | * @author Sam Mayo (sammayo888@gmail.com) 4 | * 5 | * @dev Library for verifing merkle patricia proofs. 6 | */ 7 | 8 | import "./RLP.sol"; 9 | 10 | library MerklePatriciaProof { 11 | /* 12 | * @dev Verifies a merkle patricia proof. 13 | * @param value The terminating value in the trie. 14 | * @param encodedPath The path in the trie leading to value. 15 | * @param rlpParentNodes The rlp encoded stack of nodes. 16 | * @param root The root hash of the trie. 17 | * @return The boolean validity of the proof. 18 | */ 19 | function verify(bytes value, bytes encodedPath, bytes rlpParentNodes, bytes32 root) internal constant returns (bool) { 20 | RLP.RLPItem memory item = RLP.toRLPItem(rlpParentNodes); 21 | RLP.RLPItem[] memory parentNodes = RLP.toList(item); 22 | 23 | bytes memory currentNode; 24 | RLP.RLPItem[] memory currentNodeList; 25 | 26 | bytes32 nodeKey = root; 27 | uint pathPtr = 0; 28 | 29 | bytes memory path = _getNibbleArray(encodedPath); 30 | if(path.length == 0) {return false;} 31 | 32 | for (uint i=0; i path.length) {return false;} 34 | 35 | currentNode = RLP.toBytes(parentNodes[i]); 36 | if(nodeKey != keccak256(currentNode)) {return false;} 37 | currentNodeList = RLP.toList(parentNodes[i]); 38 | 39 | if(currentNodeList.length == 17) { 40 | if(pathPtr == path.length) { 41 | if(keccak256(RLP.toBytes(currentNodeList[16])) == keccak256(value)) { 42 | return true; 43 | } else { 44 | return false; 45 | } 46 | } 47 | 48 | uint8 nextPathNibble = uint8(path[pathPtr]); 49 | if(nextPathNibble > 16) {return false;} 50 | nodeKey = RLP.toBytes32(currentNodeList[nextPathNibble]); 51 | pathPtr += 1; 52 | } else if(currentNodeList.length == 2) { 53 | pathPtr += _nibblesToTraverse(RLP.toData(currentNodeList[0]), path, pathPtr); 54 | 55 | if(pathPtr == path.length) {//leaf node 56 | if(keccak256(RLP.toData(currentNodeList[1])) == keccak256(value)) { 57 | return true; 58 | } else { 59 | return false; 60 | } 61 | } 62 | //extension node 63 | if(_nibblesToTraverse(RLP.toData(currentNodeList[0]), path, pathPtr) == 0) { 64 | return false; 65 | } 66 | 67 | nodeKey = RLP.toBytes32(currentNodeList[1]); 68 | } else { 69 | return false; 70 | } 71 | } 72 | } 73 | 74 | function _nibblesToTraverse(bytes encodedPartialPath, bytes path, uint pathPtr) private constant returns (uint) { 75 | uint len; 76 | // encodedPartialPath has elements that are each two hex characters (1 byte), but partialPath 77 | // and slicedPath have elements that are each one hex character (1 nibble) 78 | bytes memory partialPath = _getNibbleArray(encodedPartialPath); 79 | bytes memory slicedPath = new bytes(partialPath.length); 80 | 81 | // pathPtr counts nibbles in path 82 | // partialPath.length is a number of nibbles 83 | for(uint i=pathPtr; i0) { 100 | uint8 offset; 101 | uint8 hpNibble = uint8(_getNthNibbleOfBytes(0,b)); 102 | if(hpNibble == 1 || hpNibble == 3) { 103 | nibbles = new bytes(b.length*2-1); 104 | byte oddNibble = _getNthNibbleOfBytes(1,b); 105 | nibbles[0] = oddNibble; 106 | offset = 1; 107 | } else { 108 | nibbles = new bytes(b.length*2-2); 109 | offset = 0; 110 | } 111 | 112 | for(uint i=offset; i { 22 | let txTrie = new Trie(); 23 | async.map(block.transactions, (siblingTx, cb) => { 24 | let path = rlp.encode(siblingTx.transactionIndex); 25 | const signedSiblingTx = new EthereumTx(squanchTx(siblingTx)); 26 | const rawSignedSiblingTx = signedSiblingTx.serialize(); 27 | txTrie.put(path, rawSignedSiblingTx, (err) => { 28 | if (err) { cb(err, null); } 29 | cb(null, true); 30 | }) 31 | }, (err, r) => { 32 | if (err) { return reject(err); } 33 | txTrie.findPath(rlp.encode(tx.transactionIndex), (err, rawTxNode, reminder, stack) => { 34 | const prf = { 35 | blockHash: Buffer.from(tx.blockHash.slice(2), 'hex'), 36 | header: getRawHeader(block), 37 | parentNodes: rawStack(stack), 38 | path: rlp.encode(tx.transactionIndex), 39 | value: rlp.decode(rawTxNode.value), 40 | } 41 | return resolve(prf) 42 | }) 43 | }) 44 | }) 45 | } 46 | 47 | // From eth-proof (VerifyProof.trieValue) 48 | // Checks that the path of the tx (value) is correct 49 | // `value` is rlp decoded 50 | // `i` is the index of the root in the header: 4 = tx, 5 = receipt 51 | function verify(proof, j) { 52 | const path = proof.path.toString('hex'); 53 | const value = proof.value; 54 | const parentNodes = proof.parentNodes; 55 | const header = proof.header; 56 | const blockHash = proof.blockHash; 57 | const txRoot = header[j]; // txRoot is the 4th item in the header Array 58 | try{ 59 | var currentNode; 60 | var len = parentNodes.length; 61 | var rlpTxFromPrf = parentNodes[len - 1][parentNodes[len - 1].length - 1]; 62 | var nodeKey = txRoot; 63 | var pathPtr = 0; 64 | for (var i = 0 ; i < len ; i++) { 65 | currentNode = parentNodes[i]; 66 | const encodedNode = Buffer.from(sha3(rlp.encode(currentNode)),'hex'); 67 | if(!nodeKey.equals(encodedNode)){ 68 | return false; 69 | } 70 | if(pathPtr > path.length){ 71 | return false 72 | } 73 | switch(currentNode.length){ 74 | case 17://branch node 75 | if(pathPtr == path.length){ 76 | if(currentNode[16] == rlp.encode(value)){ 77 | return true; 78 | }else{ 79 | return false 80 | } 81 | } 82 | nodeKey = currentNode[parseInt(path[pathPtr],16)] //must == sha3(rlp.encode(currentNode[path[pathptr]])) 83 | pathPtr += 1 84 | break; 85 | case 2: 86 | pathPtr += nibblesToTraverse(currentNode[0].toString('hex'), path, pathPtr) 87 | if(pathPtr == path.length){//leaf node 88 | if(currentNode[1].equals(rlp.encode(value))){ 89 | return true 90 | }else{ 91 | return false 92 | } 93 | }else{//extension node 94 | nodeKey = currentNode[1] 95 | } 96 | break; 97 | default: 98 | console.log("all nodes must be length 17 or 2"); 99 | return false 100 | } 101 | } 102 | }catch(e){ console.log(e); return false } 103 | return false 104 | } 105 | 106 | var nibblesToTraverse = (encodedPartialPath, path, pathPtr) => { 107 | if(encodedPartialPath[0] == 0 || encodedPartialPath[0] == 2){ 108 | var partialPath = encodedPartialPath.slice(2) 109 | }else{ 110 | var partialPath = encodedPartialPath.slice(1) 111 | } 112 | 113 | if(partialPath == path.slice(pathPtr, pathPtr + partialPath.length)){ 114 | return partialPath.length 115 | }else{ 116 | throw new Error("path was wrong") 117 | } 118 | } 119 | 120 | var getRawHeader = (_block) => { 121 | if(typeof _block.difficulty != 'string'){ 122 | _block.difficulty = '0x' + _block.difficulty.toString(16) 123 | } 124 | var block = new EthereumBlock(_block) 125 | return block.header.raw 126 | } 127 | 128 | var squanchTx = (tx) => { 129 | tx.gas = '0x' + parseInt(tx.gas).toString(16); 130 | tx.gasPrice = '0x' + parseInt(tx.gasPrice).toString(16); 131 | tx.value = '0x' + parseInt(tx.value).toString(16) || '0'; 132 | tx.data = tx.input; 133 | return tx; 134 | } 135 | 136 | var rawStack = (input) => { 137 | output = [] 138 | for (var i = 0; i < input.length; i++) { 139 | output.push(input[i].raw) 140 | } 141 | return output 142 | } 143 | -------------------------------------------------------------------------------- /contracts/BytesLib.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.18; 2 | 3 | 4 | library BytesLib { 5 | function concat(bytes memory _preBytes, bytes memory _postBytes) internal pure returns (bytes) { 6 | bytes memory tempBytes; 7 | 8 | assembly { 9 | // Get a location of some free memory and store it in tempBytes as 10 | // Solidity does for memory variables. 11 | tempBytes := mload(0x40) 12 | 13 | // Store the length of the first bytes array at the beginning of 14 | // the memory for tempBytes. 15 | let length := mload(_preBytes) 16 | mstore(tempBytes, length) 17 | 18 | // Maintain a memory counter for the current write location in the 19 | // temp bytes array by adding the 32 bytes for the array length to 20 | // the starting location. 21 | let mc := add(tempBytes, 0x20) 22 | // Stop copying when the memory counter reaches the length of the 23 | // first bytes array. 24 | let end := add(mc, length) 25 | 26 | for { 27 | // Initialize a copy counter to the start of the _preBytes data, 28 | // 32 bytes into its memory. 29 | let cc := add(_preBytes, 0x20) 30 | } lt(mc, end) { 31 | // Increase both counters by 32 bytes each iteration. 32 | mc := add(mc, 0x20) 33 | cc := add(cc, 0x20) 34 | } { 35 | // Write the _preBytes data into the tempBytes memory 32 bytes 36 | // at a time. 37 | mstore(mc, mload(cc)) 38 | } 39 | 40 | // Add the length of _postBytes to the current length of tempBytes 41 | // and store it as the new length in the first 32 bytes of the 42 | // tempBytes memory. 43 | length := mload(_postBytes) 44 | mstore(tempBytes, add(length, mload(tempBytes))) 45 | 46 | // Move the memory counter back from a multiple of 0x20 to the 47 | // actual end of the _preBytes data. 48 | mc := end 49 | // Stop copying when the memory counter reaches the new combined 50 | // length of the arrays. 51 | end := add(mc, length) 52 | 53 | for { 54 | let cc := add(_postBytes, 0x20) 55 | } lt(mc, end) { 56 | mc := add(mc, 0x20) 57 | cc := add(cc, 0x20) 58 | } { 59 | mstore(mc, mload(cc)) 60 | } 61 | 62 | // Update the free-memory pointer by padding our last write location 63 | // to 32 bytes: add 31 bytes to the end of tempBytes to move to the 64 | // next 32 byte block, then round down to the nearest multiple of 65 | // 32. If the sum of the length of the two arrays is zero then add 66 | // one before rounding down to leave a blank 32 bytes (the length block with 0). 67 | mstore(0x40, and( 68 | add(add(end, iszero(add(length, mload(_preBytes)))), 31), 69 | not(31) // Round down to the nearest 32 bytes. 70 | )) 71 | } 72 | 73 | return tempBytes; 74 | } 75 | 76 | function slice(bytes _bytes, uint _start, uint _length) internal pure returns (bytes) { 77 | require(_bytes.length >= (_start + _length)); 78 | 79 | bytes memory tempBytes; 80 | 81 | assembly { 82 | switch iszero(_length) 83 | case 0 { 84 | // Get a location of some free memory and store it in tempBytes as 85 | // Solidity does for memory variables. 86 | tempBytes := mload(0x40) 87 | 88 | // The first word of the slice result is potentially a partial 89 | // word read from the original array. To read it, we calculate 90 | // the length of that partial word and start copying that many 91 | // bytes into the array. The first word we copy will start with 92 | // data we don't care about, but the last `lengthmod` bytes will 93 | // land at the beginning of the contents of the new array. When 94 | // we're done copying, we overwrite the full first word with 95 | // the actual length of the slice. 96 | let lengthmod := and(_length, 31) 97 | 98 | // The multiplication in the next line is necessary 99 | // because when slicing multiples of 32 bytes (lengthmod == 0) 100 | // the following copy loop was copying the origin's length 101 | // and then ending prematurely not copying everything it should. 102 | let mc := add(add(tempBytes, lengthmod), mul(0x20, iszero(lengthmod))) 103 | let end := add(mc, _length) 104 | 105 | for { 106 | // The multiplication in the next line has the same exact purpose 107 | // as the one above. 108 | let cc := add(add(add(_bytes, lengthmod), mul(0x20, iszero(lengthmod))), _start) 109 | } lt(mc, end) { 110 | mc := add(mc, 0x20) 111 | cc := add(cc, 0x20) 112 | } { 113 | mstore(mc, mload(cc)) 114 | } 115 | 116 | mstore(tempBytes, _length) 117 | 118 | //update free-memory pointer 119 | //allocating the array padded to 32 bytes like the compiler does now 120 | mstore(0x40, and(add(mc, 31), not(31))) 121 | } 122 | //if we want a zero-length slice let's just return a zero-length array 123 | default { 124 | tempBytes := mload(0x40) 125 | 126 | mstore(0x40, add(tempBytes, 0x20)) 127 | } 128 | } 129 | 130 | return tempBytes; 131 | } 132 | 133 | // Pad a bytes array to 32 bytes 134 | function leftPad(bytes _bytes) internal pure returns (bytes) { 135 | bytes memory newBytes = new bytes(32 - _bytes.length); 136 | return concat(newBytes, _bytes); 137 | } 138 | 139 | function toBytes32(bytes b) internal pure returns (bytes32) { 140 | bytes32 out; 141 | for (uint i = 0; i < 32; i++) { 142 | out |= bytes32(b[i] & 0xFF) >> (i * 8); 143 | } 144 | return out; 145 | } 146 | 147 | function fromBytes32(bytes32 x) internal constant returns (bytes) { 148 | bytes memory b = new bytes(32); 149 | for (uint i = 0; i < 32; i++) { 150 | b[i] = byte(uint8(uint(x) / (2**(8*(19 - i))))); 151 | } 152 | return b; 153 | } 154 | 155 | function toUint(bytes _bytes, uint _start) internal pure returns (uint256) { 156 | require(_bytes.length >= (_start + 32)); 157 | uint256 tempUint; 158 | assembly { 159 | tempUint := mload(add(add(_bytes, 0x20), _start)) 160 | } 161 | return tempUint; 162 | } 163 | 164 | function toAddress(bytes _bytes, uint _start) internal pure returns (address) { 165 | require(_bytes.length >= (_start + 20)); 166 | address tempAddress; 167 | 168 | assembly { 169 | tempAddress := div(mload(add(add(_bytes, 0x20), _start)), 0x1000000000000000000000000) 170 | } 171 | 172 | return tempAddress; 173 | } 174 | 175 | } 176 | -------------------------------------------------------------------------------- /parity/boot.js: -------------------------------------------------------------------------------- 1 | // Boot a parity PoA chain (single node) with one or more specified ports 2 | const Promise = require('bluebird').Promise; 3 | const secrets = require('../secrets.json'); 4 | const bip39 = require('bip39'); 5 | const hdkey = require('ethereumjs-wallet/hdkey'); 6 | const ethWallet = require('ethereumjs-wallet'); 7 | const fs = require('fs'); 8 | const jsonfile = require('jsonfile'); 9 | const spawn = require('child_process').spawn; 10 | const execFile = require('child_process').execFile; 11 | const exec = require('child_process').exec; 12 | 13 | // The password that will be used for accounts (these are temporary accounts on 14 | // private chains) 15 | const password = 'password'; 16 | // Create a directory for the poa chain data if it doesn't exist 17 | const DATA_DIR = `${process.cwd()}/parity/chains`; 18 | if(fs.existsSync(DATA_DIR)) { rmrfDirSync(DATA_DIR) }; 19 | fs.mkdirSync(DATA_DIR); 20 | // Get rid of the networks file - we will be updating it 21 | const networksF = `${process.cwd()}/networks.json`; 22 | if(fs.existsSync(networksF)) { 23 | fs.unlinkSync(networksF); 24 | }; 25 | // Create a bunch of config filges given ports specified in the script arguments 26 | const ports = process.argv.slice(2) 27 | 28 | // Pull wallets out of the secret mnemonic 29 | const wallets = generateFirstWallets(5, [], 0); 30 | let keystores = []; 31 | let addrs = []; 32 | 33 | // ============================================================================ 34 | // MAIN FUNCTION 35 | // ============================================================================ 36 | Promise.map(ports, (_port, i) => { 37 | const port = parseInt(_port); 38 | const PATH = `${DATA_DIR}/${port}`; 39 | const chainName = `LocalPoA_${port}`; 40 | if(!fs.existsSync(PATH)) { 41 | fs.mkdirSync(PATH); 42 | fs.mkdirSync(`${PATH}/keys`); 43 | fs.mkdirSync(`${PATH}/keys/${chainName}`); 44 | } 45 | 46 | 47 | Promise.map(wallets, (wallet) => { 48 | addrs.push(wallet[0]); 49 | const keystore = ethWallet.fromPrivateKey(Buffer.from(wallet[1].slice(2), 'hex')); 50 | const keystore2 = keystore.toV3String(password); 51 | keystores.push(keystore); 52 | fs.writeFileSync(`${PATH}/keys/${chainName}/${wallet[0]}`, keystore2); 53 | return; 54 | }) 55 | .then(() => { 56 | let tmpConfig = genConfig(chainName, port); 57 | addrs.forEach((addr) => { 58 | tmpConfig.accounts[addr] = { "balance": "1000000000000000000000" }; 59 | }); 60 | jsonfile.writeFile(`${PATH}/config.json`, tmpConfig, { spaces: 2 }, () => { 61 | 62 | // ------------------------------------------------------------ 63 | // https://github.com/GridPlus/cryptobridge-contracts/issues/13 64 | let pwfile = `${DATA_DIR}/../pw` 65 | 66 | exec(`parity account new --chain ${PATH}/config.json --keys-path ${PATH}/keys --password ${pwfile}`, (err, stdout, stderr) => { 67 | if (err) { 68 | console.error(`exec error: ${err}`); 69 | return; 70 | } 71 | console.log(`${chainName} Account: ${stdout}`); 72 | }); 73 | // ------------------------------------------------------------- 74 | 75 | // NOTE: I had to add a timeout because there was a race condition. 76 | // 300ms seems to work but if you're getting errors try increasing it. 77 | setTimeout(() => { 78 | // // Get address from the new wallet 79 | jsonfile.readFile(`${PATH}/config.json`, (err, file) => { 80 | // Add signer to it 81 | const fnames = fs.readdirSync(`${PATH}/keys/${chainName}`); 82 | let fname; 83 | fnames.forEach((f) => { 84 | if (f.substring(0, 5) == 'UTC--') { fname = f; } 85 | }); 86 | const _k = fs.readFileSync(`${PATH}/keys/${chainName}/${fname}`); 87 | const k = JSON.parse(_k); 88 | const signer = `0x${k.address}`; 89 | let config = file; 90 | config.accounts[signer] = { "balance": "1000000000000000000000" }; 91 | jsonfile.writeFile(`${PATH}/config.json`, config, { spaces: 2}, () => { 92 | // Spawn the parity process 93 | const access = fs.createWriteStream(`${PATH}/log`, { flags: 'a' }); 94 | const error = fs.createWriteStream(`${PATH}/error.log`, { flags: 'a' }); 95 | // Allow web sockets (for listening on events) 96 | const wsPort = String(port + 1); 97 | 98 | // Set up parity config 99 | let args = ['--chain', `${PATH}/config.json`, '-d', `${PATH}/data`, 100 | '--jsonrpc-port', String(port), '--ws-port', wsPort, '--port', String(port+2), 101 | '--ui-port', String(port+3), 102 | '--jsonrpc-apis', 'web3,eth,net,personal,parity,parity_set,traces,rpc,parity_accounts', 103 | '--author', signer, '--engine-signer', signer, '--reseal-on-txs', 'all', '--force-sealing', 104 | '--rpccorsdomain', '*', '--jsonrpc-interface', 'all', '--reseal-max-period', '0', '--reseal-min-period', '0', 105 | '--jsonrpc-hosts', 'all', '--keys-path', `${PATH}/keys`, '--no-persistent-txqueue']; 106 | 107 | // Unlock signer AND first 5 addresses from seed phrase 108 | let unlock = signer; 109 | addrs.forEach((addr) => { unlock += `,${addr}`; }) 110 | let pwfile = `${DATA_DIR}/../pw` 111 | args.push('--unlock'); 112 | args.push(unlock); 113 | args.push('--password'); 114 | args.push(pwfile); 115 | 116 | // https://github.com/GridPlus/cryptobridge-contracts/issues/15 117 | // args.push('--no-ipc'); 118 | 119 | const parity = spawn('parity', args, { stdio: 'pipe', cwd: PATH }); 120 | parity.stdout.pipe(access); 121 | parity.stderr.pipe(error); 122 | parity.on('close', () => { 123 | setTimeout(() => { 124 | console.log(new Date(), `Parity killed (RPC port ${port})`); 125 | }, 500); 126 | }); 127 | 128 | console.log(`${new Date()} Parity PoA chain #${i} started. RPC port=${port} WS port=${wsPort}`); 129 | }) 130 | }); 131 | }, 500) 132 | 133 | }); 134 | }) 135 | }) 136 | 137 | 138 | function rmrfDirSync(path) { 139 | if (fs.existsSync(path)) { 140 | fs.readdirSync(path).forEach(function(file, index){ 141 | var curPath = path + "/" + file; 142 | if (fs.lstatSync(curPath).isDirectory()) { // recurse 143 | rmrfDirSync(curPath); 144 | } else { // delete file 145 | fs.unlinkSync(curPath); 146 | } 147 | }); 148 | fs.rmdirSync(path); 149 | } 150 | }; 151 | 152 | function genConfig(name, port) { 153 | const config = { 154 | name: name, 155 | engine: { 156 | instantSeal: null 157 | }, 158 | params: { 159 | gasLimitBoundDivisor: "0x400", 160 | maximumExtraDataSize: "0x20", 161 | minGasLimit: "0x1312d00", 162 | networkID: `0x${port.toString(16)}`, 163 | "eip140Transition": "0x0", 164 | "eip211Transition": "0x0", 165 | "eip214Transition": "0x0", 166 | "eip658Transition": "0x0" 167 | }, 168 | "genesis": { 169 | "seal": { 170 | "generic": "0x0" 171 | }, 172 | "difficulty": "0x20000", 173 | "author": "0x0000000000000000000000000000000000000000", 174 | "timestamp": "0x00", 175 | "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", 176 | "extraData": "0x", 177 | "gasLimit": "0x1312d00" 178 | }, 179 | accounts: { 180 | "0x0000000000000000000000000000000000000001": { "balance": "1", "builtin": { "name": "ecrecover", "pricing": { "linear": { "base": 3000, "word": 0 } } } }, 181 | "0x0000000000000000000000000000000000000002": { "balance": "1", "builtin": { "name": "sha256", "pricing": { "linear": { "base": 60, "word": 12 } } } }, 182 | "0x0000000000000000000000000000000000000003": { "balance": "1", "builtin": { "name": "ripemd160", "pricing": { "linear": { "base": 600, "word": 120 } } } }, 183 | "0x0000000000000000000000000000000000000004": { "balance": "1", "builtin": { "name": "identity", "pricing": { "linear": { "base": 15, "word": 3 } } } }, 184 | "0x0000000000000000000000000000000000000005": { "builtin": { "name": "modexp", "activate_at": "0x0", "pricing": { "modexp": { "divisor": 20 } } } }, 185 | "0x0000000000000000000000000000000000000006": { "builtin": { "name": "alt_bn128_add", "activate_at": "0x0", "pricing": { "linear": { "base": 500, "word": 0 } } } }, 186 | "0x0000000000000000000000000000000000000007": { "builtin": { "name": "alt_bn128_mul", "activate_at": "0x0", "pricing": { "linear": { "base": 40000, "word": 0 } } } }, 187 | "0x0000000000000000000000000000000000000008": { "builtin": { "name": "alt_bn128_pairing", "activate_at": "0x0", "pricing": { "alt_bn128_pairing": { "base": 100000, "pair": 80000 } } } } 188 | } 189 | }; 190 | return config; 191 | } 192 | 193 | function generateFirstWallets(n, _wallets, hdPathIndex) { 194 | const hdwallet = hdkey.fromMasterSeed(bip39.mnemonicToSeed(secrets.mnemonic)); 195 | const node = hdwallet.derivePath(secrets.hdPath + hdPathIndex.toString()); 196 | const secretKey = node.getWallet().getPrivateKeyString(); 197 | const addr = node.getWallet().getAddressString(); 198 | _wallets.push([addr, secretKey]); 199 | const nextHDPathIndex = hdPathIndex + 1; 200 | if (nextHDPathIndex >= n) { 201 | return _wallets; 202 | } 203 | return generateFirstWallets(n, _wallets, nextHDPathIndex); 204 | } 205 | -------------------------------------------------------------------------------- /contracts/RLP.sol: -------------------------------------------------------------------------------- 1 | /** 2 | * @title RLPReader 3 | * 4 | * RLPReader is used to read and parse RLP encoded data in memory. 5 | * 6 | * @author Andreas Olofsson (androlo1980@gmail.com) 7 | */ 8 | library RLP { 9 | 10 | uint constant DATA_SHORT_START = 0x80; 11 | uint constant DATA_LONG_START = 0xB8; 12 | uint constant LIST_SHORT_START = 0xC0; 13 | uint constant LIST_LONG_START = 0xF8; 14 | 15 | uint constant DATA_LONG_OFFSET = 0xB7; 16 | uint constant LIST_LONG_OFFSET = 0xF7; 17 | 18 | 19 | struct RLPItem { 20 | uint _unsafe_memPtr; // Pointer to the RLP-encoded bytes. 21 | uint _unsafe_length; // Number of bytes. This is the full length of the string. 22 | } 23 | 24 | struct Iterator { 25 | RLPItem _unsafe_item; // Item that's being iterated over. 26 | uint _unsafe_nextPtr; // Position of the next item in the list. 27 | } 28 | 29 | /* Iterator */ 30 | 31 | function next(Iterator memory self) internal constant returns (RLPItem memory subItem) { 32 | if(hasNext(self)) { 33 | var ptr = self._unsafe_nextPtr; 34 | var itemLength = _itemLength(ptr); 35 | subItem._unsafe_memPtr = ptr; 36 | subItem._unsafe_length = itemLength; 37 | self._unsafe_nextPtr = ptr + itemLength; 38 | } 39 | else 40 | throw; 41 | } 42 | 43 | function next(Iterator memory self, bool strict) internal constant returns (RLPItem memory subItem) { 44 | subItem = next(self); 45 | if(strict && !_validate(subItem)) 46 | throw; 47 | return; 48 | } 49 | 50 | function hasNext(Iterator memory self) internal constant returns (bool) { 51 | var item = self._unsafe_item; 52 | return self._unsafe_nextPtr < item._unsafe_memPtr + item._unsafe_length; 53 | } 54 | 55 | /* RLPItem */ 56 | 57 | /// @dev Creates an RLPItem from an array of RLP encoded bytes. 58 | /// @param self The RLP encoded bytes. 59 | /// @return An RLPItem 60 | function toRLPItem(bytes memory self) internal constant returns (RLPItem memory) { 61 | uint len = self.length; 62 | if (len == 0) { 63 | return RLPItem(0, 0); 64 | } 65 | uint memPtr; 66 | assembly { 67 | memPtr := add(self, 0x20) 68 | } 69 | return RLPItem(memPtr, len); 70 | } 71 | 72 | /// @dev Creates an RLPItem from an array of RLP encoded bytes. 73 | /// @param self The RLP encoded bytes. 74 | /// @param strict Will throw if the data is not RLP encoded. 75 | /// @return An RLPItem 76 | function toRLPItem(bytes memory self, bool strict) internal constant returns (RLPItem memory) { 77 | var item = toRLPItem(self); 78 | if(strict) { 79 | uint len = self.length; 80 | if(_payloadOffset(item) > len) 81 | throw; 82 | if(_itemLength(item._unsafe_memPtr) != len) 83 | throw; 84 | if(!_validate(item)) 85 | throw; 86 | } 87 | return item; 88 | } 89 | 90 | /// @dev Check if the RLP item is null. 91 | /// @param self The RLP item. 92 | /// @return 'true' if the item is null. 93 | function isNull(RLPItem memory self) internal constant returns (bool ret) { 94 | return self._unsafe_length == 0; 95 | } 96 | 97 | /// @dev Check if the RLP item is a list. 98 | /// @param self The RLP item. 99 | /// @return 'true' if the item is a list. 100 | function isList(RLPItem memory self) internal constant returns (bool ret) { 101 | if (self._unsafe_length == 0) 102 | return false; 103 | uint memPtr = self._unsafe_memPtr; 104 | assembly { 105 | ret := iszero(lt(byte(0, mload(memPtr)), 0xC0)) 106 | } 107 | } 108 | 109 | /// @dev Check if the RLP item is data. 110 | /// @param self The RLP item. 111 | /// @return 'true' if the item is data. 112 | function isData(RLPItem memory self) internal constant returns (bool ret) { 113 | if (self._unsafe_length == 0) 114 | return false; 115 | uint memPtr = self._unsafe_memPtr; 116 | assembly { 117 | ret := lt(byte(0, mload(memPtr)), 0xC0) 118 | } 119 | } 120 | 121 | /// @dev Check if the RLP item is empty (string or list). 122 | /// @param self The RLP item. 123 | /// @return 'true' if the item is null. 124 | function isEmpty(RLPItem memory self) internal constant returns (bool ret) { 125 | if(isNull(self)) 126 | return false; 127 | uint b0; 128 | uint memPtr = self._unsafe_memPtr; 129 | assembly { 130 | b0 := byte(0, mload(memPtr)) 131 | } 132 | return (b0 == DATA_SHORT_START || b0 == LIST_SHORT_START); 133 | } 134 | 135 | /// @dev Get the number of items in an RLP encoded list. 136 | /// @param self The RLP item. 137 | /// @return The number of items. 138 | function items(RLPItem memory self) internal constant returns (uint) { 139 | if (!isList(self)) 140 | return 0; 141 | uint b0; 142 | uint memPtr = self._unsafe_memPtr; 143 | assembly { 144 | b0 := byte(0, mload(memPtr)) 145 | } 146 | uint pos = memPtr + _payloadOffset(self); 147 | uint last = memPtr + self._unsafe_length - 1; 148 | uint itms; 149 | while(pos <= last) { 150 | pos += _itemLength(pos); 151 | itms++; 152 | } 153 | return itms; 154 | } 155 | 156 | /// @dev Create an iterator. 157 | /// @param self The RLP item. 158 | /// @return An 'Iterator' over the item. 159 | function iterator(RLPItem memory self) internal constant returns (Iterator memory it) { 160 | if (!isList(self)) 161 | throw; 162 | uint ptr = self._unsafe_memPtr + _payloadOffset(self); 163 | it._unsafe_item = self; 164 | it._unsafe_nextPtr = ptr; 165 | } 166 | 167 | /// @dev Return the RLP encoded bytes. 168 | /// @param self The RLPItem. 169 | /// @return The bytes. 170 | function toBytes(RLPItem memory self) internal constant returns (bytes memory bts) { 171 | var len = self._unsafe_length; 172 | if (len == 0) 173 | return; 174 | bts = new bytes(len); 175 | _copyToBytes(self._unsafe_memPtr, bts, len); 176 | } 177 | 178 | /// @dev Decode an RLPItem into bytes. This will not work if the 179 | /// RLPItem is a list. 180 | /// @param self The RLPItem. 181 | /// @return The decoded string. 182 | function toData(RLPItem memory self) internal constant returns (bytes memory bts) { 183 | if(!isData(self)) 184 | throw; 185 | var (rStartPos, len) = _decode(self); 186 | bts = new bytes(len); 187 | _copyToBytes(rStartPos, bts, len); 188 | } 189 | 190 | /// @dev Get the list of sub-items from an RLP encoded list. 191 | /// Warning: This is inefficient, as it requires that the list is read twice. 192 | /// @param self The RLP item. 193 | /// @return Array of RLPItems. 194 | function toList(RLPItem memory self) internal constant returns (RLPItem[] memory list) { 195 | if(!isList(self)) 196 | throw; 197 | var numItems = items(self); 198 | list = new RLPItem[](numItems); 199 | var it = iterator(self); 200 | uint idx; 201 | while(hasNext(it)) { 202 | list[idx] = next(it); 203 | idx++; 204 | } 205 | } 206 | 207 | /// @dev Decode an RLPItem into an ascii string. This will not work if the 208 | /// RLPItem is a list. 209 | /// @param self The RLPItem. 210 | /// @return The decoded string. 211 | function toAscii(RLPItem memory self) internal constant returns (string memory str) { 212 | if(!isData(self)) 213 | throw; 214 | var (rStartPos, len) = _decode(self); 215 | bytes memory bts = new bytes(len); 216 | _copyToBytes(rStartPos, bts, len); 217 | str = string(bts); 218 | } 219 | 220 | /// @dev Decode an RLPItem into a uint. This will not work if the 221 | /// RLPItem is a list. 222 | /// @param self The RLPItem. 223 | /// @return The decoded string. 224 | function toUint(RLPItem memory self) internal constant returns (uint data) { 225 | if(!isData(self)) 226 | throw; 227 | var (rStartPos, len) = _decode(self); 228 | if (len > 32 || len == 0) 229 | throw; 230 | assembly { 231 | data := div(mload(rStartPos), exp(256, sub(32, len))) 232 | } 233 | } 234 | 235 | /// @dev Decode an RLPItem into a boolean. This will not work if the 236 | /// RLPItem is a list. 237 | /// @param self The RLPItem. 238 | /// @return The decoded string. 239 | function toBool(RLPItem memory self) internal constant returns (bool data) { 240 | if(!isData(self)) 241 | throw; 242 | var (rStartPos, len) = _decode(self); 243 | if (len != 1) 244 | throw; 245 | uint temp; 246 | assembly { 247 | temp := byte(0, mload(rStartPos)) 248 | } 249 | if (temp > 1) 250 | throw; 251 | return temp == 1 ? true : false; 252 | } 253 | 254 | /// @dev Decode an RLPItem into a byte. This will not work if the 255 | /// RLPItem is a list. 256 | /// @param self The RLPItem. 257 | /// @return The decoded string. 258 | function toByte(RLPItem memory self) internal constant returns (byte data) { 259 | if(!isData(self)) 260 | throw; 261 | var (rStartPos, len) = _decode(self); 262 | if (len != 1) 263 | throw; 264 | uint temp; 265 | assembly { 266 | temp := byte(0, mload(rStartPos)) 267 | } 268 | return byte(temp); 269 | } 270 | 271 | /// @dev Decode an RLPItem into an int. This will not work if the 272 | /// RLPItem is a list. 273 | /// @param self The RLPItem. 274 | /// @return The decoded string. 275 | function toInt(RLPItem memory self) internal constant returns (int data) { 276 | return int(toUint(self)); 277 | } 278 | 279 | /// @dev Decode an RLPItem into a bytes32. This will not work if the 280 | /// RLPItem is a list. 281 | /// @param self The RLPItem. 282 | /// @return The decoded string. 283 | function toBytes32(RLPItem memory self) internal constant returns (bytes32 data) { 284 | return bytes32(toUint(self)); 285 | } 286 | 287 | /// @dev Decode an RLPItem into an address. This will not work if the 288 | /// RLPItem is a list. 289 | /// @param self The RLPItem. 290 | /// @return The decoded string. 291 | function toAddress(RLPItem memory self) internal constant returns (address data) { 292 | if(!isData(self)) 293 | throw; 294 | var (rStartPos, len) = _decode(self); 295 | if (len != 20) 296 | throw; 297 | assembly { 298 | data := div(mload(rStartPos), exp(256, 12)) 299 | } 300 | } 301 | 302 | // Get the payload offset. 303 | function _payloadOffset(RLPItem memory self) private constant returns (uint) { 304 | if(self._unsafe_length == 0) 305 | return 0; 306 | uint b0; 307 | uint memPtr = self._unsafe_memPtr; 308 | assembly { 309 | b0 := byte(0, mload(memPtr)) 310 | } 311 | if(b0 < DATA_SHORT_START) 312 | return 0; 313 | if(b0 < DATA_LONG_START || (b0 >= LIST_SHORT_START && b0 < LIST_LONG_START)) 314 | return 1; 315 | if(b0 < LIST_SHORT_START) 316 | return b0 - DATA_LONG_OFFSET + 1; 317 | return b0 - LIST_LONG_OFFSET + 1; 318 | } 319 | 320 | // Get the full length of an RLP item. 321 | function _itemLength(uint memPtr) private constant returns (uint len) { 322 | uint b0; 323 | assembly { 324 | b0 := byte(0, mload(memPtr)) 325 | } 326 | if (b0 < DATA_SHORT_START) 327 | len = 1; 328 | else if (b0 < DATA_LONG_START) 329 | len = b0 - DATA_SHORT_START + 1; 330 | else if (b0 < LIST_SHORT_START) { 331 | assembly { 332 | let bLen := sub(b0, 0xB7) // bytes length (DATA_LONG_OFFSET) 333 | let dLen := div(mload(add(memPtr, 1)), exp(256, sub(32, bLen))) // data length 334 | len := add(1, add(bLen, dLen)) // total length 335 | } 336 | } 337 | else if (b0 < LIST_LONG_START) 338 | len = b0 - LIST_SHORT_START + 1; 339 | else { 340 | assembly { 341 | let bLen := sub(b0, 0xF7) // bytes length (LIST_LONG_OFFSET) 342 | let dLen := div(mload(add(memPtr, 1)), exp(256, sub(32, bLen))) // data length 343 | len := add(1, add(bLen, dLen)) // total length 344 | } 345 | } 346 | } 347 | 348 | // Get start position and length of the data. 349 | function _decode(RLPItem memory self) private constant returns (uint memPtr, uint len) { 350 | if(!isData(self)) 351 | throw; 352 | uint b0; 353 | uint start = self._unsafe_memPtr; 354 | assembly { 355 | b0 := byte(0, mload(start)) 356 | } 357 | if (b0 < DATA_SHORT_START) { 358 | memPtr = start; 359 | len = 1; 360 | return; 361 | } 362 | if (b0 < DATA_LONG_START) { 363 | len = self._unsafe_length - 1; 364 | memPtr = start + 1; 365 | } else { 366 | uint bLen; 367 | assembly { 368 | bLen := sub(b0, 0xB7) // DATA_LONG_OFFSET 369 | } 370 | len = self._unsafe_length - 1 - bLen; 371 | memPtr = start + bLen + 1; 372 | } 373 | return; 374 | } 375 | 376 | // Assumes that enough memory has been allocated to store in target. 377 | function _copyToBytes(uint btsPtr, bytes memory tgt, uint btsLen) private constant { 378 | // Exploiting the fact that 'tgt' was the last thing to be allocated, 379 | // we can write entire words, and just overwrite any excess. 380 | assembly { 381 | { 382 | let i := 0 // Start at arr + 0x20 383 | let words := div(add(btsLen, 31), 32) 384 | let rOffset := btsPtr 385 | let wOffset := add(tgt, 0x20) 386 | tag_loop: 387 | jumpi(end, eq(i, words)) 388 | { 389 | let offset := mul(i, 0x20) 390 | mstore(add(wOffset, offset), mload(add(rOffset, offset))) 391 | i := add(i, 1) 392 | } 393 | jump(tag_loop) 394 | end: 395 | mstore(add(tgt, add(0x20, mload(tgt))), 0) 396 | } 397 | } 398 | } 399 | 400 | // Check that an RLP item is valid. 401 | function _validate(RLPItem memory self) private constant returns (bool ret) { 402 | // Check that RLP is well-formed. 403 | uint b0; 404 | uint b1; 405 | uint memPtr = self._unsafe_memPtr; 406 | assembly { 407 | b0 := byte(0, mload(memPtr)) 408 | b1 := byte(1, mload(memPtr)) 409 | } 410 | if(b0 == DATA_SHORT_START + 1 && b1 < DATA_SHORT_START) 411 | return false; 412 | return true; 413 | } 414 | } 415 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cryptobridge Contracts 2 | 3 | ## This module was experimental and has been deprecated in favor of superior Plasma designs within the Ethereum community 4 | 5 | **WARNING:** 6 | This package is functional, but is unaudited and in still in development. It should not be used in production systems with large amounts of value. 7 | 8 | This repo implements the trustless EVM bridge (now termed "cryptobridge") contract. For more background on the concept, see [this article](https://blog.gridplus.io/efficiently-bridging-evm-blockchains-8421504e9ced). The bridges are maintained by networks of participants running the [cryptobridge client](https://github.com/GridPlus/cryptobridge-client). 9 | 10 | ![Bridge](https://upload.wikimedia.org/wikipedia/commons/thumb/d/da/360_Bridge_at_night%2C_2008.jpg/800px-360_Bridge_at_night%2C_2008.jpg) 11 | 12 | # Bridge Basics 13 | 14 | A bridge exists as two contracts (`Bridge.sol`) on two separate EVM-based blockchains. A set of participants may stake (`Bridge.stake()`) a specified token (`Bridge.stakeToken()`) to enter the pool of proposer candidates. Every time a piece of data is submitted to a bridge, a new proposer is chosen pseudorandomly (`Bridge.getProposer()`) with probability proportional to the participant's stake. 15 | 16 | This proposer listens to the bridged blockchain and collects block headers until he/she is ready to submit data to the bridge contract on his/her origin blockchain (i.e. where he/she is currently the proposer). At such a time, the proposer packages the block headers into a Merkle root (note: for now, the number of headers being packaged must be a power of two) and includes the block number of the last packaged header (the starting block number is assumed to be 1 greater than the last checkpointed header in the previous header root saved to the Bridge). 17 | 18 | With data in hand, the proposer passes `root, chainAddr, startBlock, endBlock` to the other staking participants, who are currently validators. Note that `chainAddr` corresponds to the address of the Bridge contract on the blockchain being bridged. If this root is consistent with the one the validators compute, they will sign the following hash: `keccak256(root, chainAddr, startBlock, endBlock)`, where arguments are tightly packed as they would be in Solidity (i.e. with numbers being left-padded to 32 bytes). 19 | 20 | Once enough validators sign off (at least `Bridge.validatorThreshold()`), the proposer may submit the data to the bridge via `Bridge.proposeRoot()`. Assuming the signatures are correct, the proposer will be rewarded based on the current reward (`Bridge.reward()`). This is parameterized by `Bridge.updateReward()` and is a function of the number of blocks elapsed since the last root was checkpointed. This allows the proposer to wait until it is profitable to checkpoint the data (e.g. to wait out periods of high gas prices). Note that there is also a cutoff number of blocks, after which anyone may proposer a header with signatures and receive the reward. In future versions, this cutoff can be made into a random range to avoid proposers from waiting too long. 21 | 22 | # APIs 23 | 24 | The following is a set of APIs for the end user, stakers/proposer, and admin. If you would like to get started installing and testing this package, please skip to *Installation and Setup* 25 | 26 | ## User API 27 | 28 | Users may deposit tokens on the bridge contract in their blockchain and withdraw them from the corresponding bridge contract in the destination blockchain. Since the proposer only relays a single Merkle root hash, the user has to prove a few things from several pieces of data. 29 | 30 | ### deposit (token, toChain, amount) 31 | 32 | ``` 33 | Function: deposit 34 | Purpose: Deposit tokens so that they can be withdrawn on another chain. 35 | Arguments: 36 | * token (address: the address of the token being deposited) 37 | * toChain (address: the address of the corresponding bridged blockchain, i.e where the coins will be withdrawn) 38 | * amount (uint256: amount to deposit) 39 | ``` 40 | 41 | #### Notes: 42 | 43 | * This is done on the "origin" chain by the user. The user must give an allowance to the bridge contract ahead of time. 44 | 45 | * NOTE: `Bridge.sol` v0.1 does not accept deposits or withdrawals of ether and is only compatable with ERC20 tokens. In future version, ether will be included as an allowable deposit or withdrawal token. 46 | 47 | ### prepWithdraw ( v, [r, s, txRoot], addrs, amount, path, parentNodes, netVersion, rlpDepositTxData, rlpWithdrawTxData) 48 | 49 | ``` 50 | Function: prepWithdraw 51 | Purpose: Step 1 of withdrawal. Initialize a withdrawal and prove a transaction. Save the transaction root and other data. 52 | Arguments: 53 | * v (bytes: value of v received from transaction receipt in origin chain, see note below) 54 | * [r, s, txRoot] (1) 55 | * addrs (address[3]: [fromChain, depositToken, withdrawToken]. fromChain = address of origin chain bridge contract, depositToken = address of token deposited in the origin chain, withdrawToken = address of mapped token in this chain) 56 | * amount (bytes: amount deposited in origin chain, hex integer, atomic units) 57 | * path (bytes: path of deposit transaction in the transactions Merkle-Patricia tree) 58 | * parentNodes (bytes: concatenated list of parent nodes in the transaction Merkle-Patricia tree) 59 | * netVersion (bytes: version of the origin chain, only needed if v is EIP155 form, can be called from web3.version.network) 60 | * rlpDepositTxData (rlp binary encoded Deposit transaction data) 61 | * rlpWithdrawTxData (rlp binary encoded Withdraw transaction data) 62 | ``` 63 | 64 | (1) [r, s, txRoot] 65 | * r (bytes32: value of r from the deposit transaction) 66 | * s (bytes32: value of s from the deposit transaction) 67 | * txRoot (bytes32: transactionsRoot from block in which the deposit was made on the origin chain) 68 | 69 | JavaScript code Example 70 | 71 | ```js 72 | // Make the transaction 73 | const prepWithdraw = await BridgeA.prepWithdraw( 74 | deposit.v, 75 | [deposit.r, deposit.s, depositBlock.transactionsRoot], 76 | [BridgeB.options.address, tokenB.options.address, tokenA.address], 77 | 5, 78 | path, 79 | parentNodes, 80 | version, 81 | rlpDepositTxData.toString('binary'), 82 | rlpWithdrawTxData.toString('binary'), 83 | { from: wallets[2][0], gas: 500000 } 84 | ); 85 | ``` 86 | 87 | For more details on how to setup the transaction, see `test/bridge.js`. 88 | 89 | #### Notes: 90 | 91 | * EIP155 changed `v` from `27`/`28` to `netVersion * 2 + 35`/`netVersion * 2 + 36`. Bridge maintainers who publish data should indicate which version is being used. v0.1 of `Bridge.sol` supports both. Parity treats EIP155 as the official `v` value and labels the previous version as `standardV`. 92 | * `path` and `parentNodes` are produced by [eth-proof](https://github.com/zmitton/eth-proof). For more information, please see that library. 93 | * All bytes arguments are unpadded, e.g. 0x02 would represent the number 2 (with one byte). 94 | 95 | ### proveReceipt (logs, cumulativeGas, logsBloom, receiptsRoot, path, parentNodes) 96 | 97 | ``` 98 | Function: proveReceipt 99 | Purpose: Step 2 of withdrawal. Prove a receipt and save the receipts root to an existing pending withdrawal. 100 | Arguments: 101 | * logs (bytes: encoded logs, see below) 102 | * cumulativeGas (bytes: amount of gas used after this transaction completed in the block, hex integer) 103 | * logsBloom (bytes: raw data from deposit transaction receipt) 104 | * receiptsRoot (bytes32: root of the receipts in the deposit's block) 105 | * path (bytes: path of the receipt in the receipt Merkle-Patricia tree) 106 | * parentNodes (bytes: concatenated list of parent nodes in the receipt Merkle-Patricia tree) 107 | ``` 108 | 109 | #### Notes: 110 | * `logs` are encoded as a concatenated list of bytes: 111 | 112 | ``` 113 | [ [addrs[0], [ topics[0], topics[1], topics[2]], data[0] ], [addrs[1], [ topics[3], topics[4], topics[5], topics[6] ], data[1] ] ] 114 | ``` 115 | 116 | This is a fixed size because there are two events emitted: `Transfer` (ERC20) and `Deposit` (Bridge). `topics` correspond to the indexed log parameters in the order the appear in the contract's definition. `data` are the unindexed arguments. `addrs` correspond to the address of the contract that emitted the log (regardless of which blockchain it is deployed on). To see this encoding in action, see `encodeLogs()` in `test/util/receiptProof.js`. Note that the array returned by `encodeLogs()` must have each item encoded to hex and concatenated before sending the whole payload to `proveReceipt()`. 117 | 118 | ### withdraw (blockNum, timestamp, prevHeader, rootN, proof) 119 | 120 | ``` 121 | Function: withdraw 122 | Purpose: Step 3 of withdrawal. Prove block header and receive tokens. 123 | Arguments: 124 | * blockNum (uint256: block number the block containing the deposit on the bridged blockchain) 125 | * timestamp (uint256: timestamp on the block containing the deposit on the bridged blockchain) 126 | * prevHeader (bytes32: the previous modified block header (NOT Ethereum block header), see note below for formatting) 127 | * rootN (uint256: index of the header root corresponding to the origin chain) 128 | * proof (bytes: concatenated Merkle proof, see note below for formatting) 129 | ``` 130 | 131 | #### Notes: 132 | 133 | * Headers in this system are modified and only contain the following data: 134 | - Previous [modified] header (`bytes32(0)` if this is the genesis block) 135 | - Timestamp (from block) 136 | - Block number 137 | - Transactions root 138 | - Receipts root 139 | 140 | * A normal Merkle proof is used for headers rather than a Merkle-Patricia tree. It is formatted as: 141 | 142 | ``` 143 | partnerIsRight_i, partner_i], ... 144 | ``` 145 | 146 | Where `partnerIsRight_i = 0x01` for `true` and `0x00` for false. For more details on implementation, see `test/util/merkle.js`. Note that the original leaf is *not* included in this proof. 147 | 148 | 149 | ### getTokenMapping (chain, token) constant 150 | 151 | ``` 152 | Function: getTokenMapping 153 | Purpose: Get the token associated with your token from another chain. This will be your withdrawal token if you deposit the other one. 154 | Arguments: 155 | * chain (address: the bridge contract on the origin chain where you would deposit your tokens) 156 | * token (address: the token you would deposit) 157 | Returns: 158 | * address: the token you will receive as a withdrawal on this chain if you deposit your token on your chain 159 | ``` 160 | 161 | ### getLastBlockNum (fromChain) constant 162 | 163 | ``` 164 | Function: getLastBlockNum 165 | Purpose: Find the most recent block on the given chain that has been included in a proposed header root. If you have a deposit in a block less than or equal to this one on the provided chain, you may begin the withdrawal process. 166 | Arguments: 167 | * fromChain (address: the bridge contract on the origin chain) 168 | Returns: 169 | * uint256: last block on the origin chain that was relayed to this chain 170 | ``` 171 | 172 | ## Staker API 173 | 174 | Any participant may join a staking pool, but future versions may give the option to whitelist a set of participants. 175 | 176 | ### stake (amount) 177 | 178 | ``` 179 | Function: stake 180 | Purpose: Join a staking pool or add to your stake in the pre-determined stakeToken. 181 | Arguments: 182 | * amount (uint256: atomic units of staking token to add to the pool. This will credit your account with more stake. 183 | ``` 184 | 185 | #### Notes: 186 | 187 | * In v0.1, once a participant has added stake, the proposer is subject to change, even for the current header root. This is to incentivize proposers to submit roots more quickly. 188 | 189 | ### destake (amount) 190 | 191 | ``` 192 | Function: destake 193 | Purpose: Remove stake from a poo in the pre-determined stakeToken. 194 | Arguments: 195 | * amount (uint256: atomic units of staking token to remove from the pool) 196 | ``` 197 | 198 | #### Notes: 199 | 200 | * As with staking, in v0.1 this potentially changes the current proposer's identity. If you are the proposer, it is something to be aware of. 201 | * In v0.1, there is currently no lock-up period, though one will likely be added in the future 202 | * If the participant destakes the total amount currently staked, he/she will be removed from the pool entirely. 203 | 204 | ### proposeRoot (headerRoot, chainId, end, sigs) 205 | 206 | ``` 207 | Function: proposeRoot 208 | Purpose: May only be called by elected proposer, submit a headerRoot and validator signatures and receive a reward in return. 209 | Arguments: 210 | * headerRoot (bytes32: the Merkle root of the modified block headers since the last block checkpointed. See withdraw() notes on block header formatting. Ordering in Merkle tree is based on block number) 211 | * chainId (address: location of bridge contract on connected chain) 212 | * end (uint256: last block number in the header Merkle tree corresponding to the root being submited) 213 | * sigs (bytes: concatenated list of signatures of form 'r,s,v'. See notes on `prepWithdraw()` for instructions on formatting `v`) 214 | ``` 215 | 216 | #### Notes: 217 | 218 | * The Merkle tree must begin with the block *after* the last block checkpoined in the previous Merkle root corresponding to this `chainId`. Although no explicit contract checks exist to ensure this range is a power of two, it is enforcced in the included test cases and is recommended. 219 | 220 | ### getProposer () constant 221 | 222 | ``` 223 | Function: getProposer 224 | Purpose: Get the current proposer for all chains. 225 | ``` 226 | 227 | #### Notes: 228 | 229 | * In the future, stakers will be able to enroll in watching specific chains and only be elected to those chains. For simplicity, in v0.1 each proposer is proposer of all bridged chains at the same time and may only publish one at a time before a new proposer is selected. This is really designed to only relay one chain at a time. 230 | * The proposer is selected pseudorandomly based on the `epochSeed`, which is updated when a root is proposed. 231 | 232 | ## Admin API 233 | 234 | Admin functionality is key to running a clean bridge. In v0.1, there is only one admin - the user who deploys the contract. In the future, this role can be delegated to the stakers or an elected representative. 235 | 236 | ### Bridge (token) 237 | 238 | ``` 239 | Function: default function 240 | Purpose: Set admin and staking token 241 | Arguments: 242 | * token (address: the staking token. Once set, this cannot be changed!) 243 | ``` 244 | 245 | ### addToken (newToken, origToken, fromChain) onlyAdmin 246 | 247 | ``` 248 | Function: addToken 249 | Purpose: Create a token and move all units to this bridge contract, then associate to a token on an existing bridge. 250 | Arguments: 251 | * newToken (address: token on this blockchain to map) 252 | * origToken (address: token on fromChain to map) 253 | * fromChain (address: bridge contract on the bridged blockchain) 254 | ``` 255 | 256 | #### Notes: 257 | 258 | * This function exists primarily to add trust to an admin's job of creating token mappings. It emits a separate event, so users can be sure the token was created correctly and all units were moved to the bridge contract (where the admin cannot withdraw them). 259 | * This function is meant for the destination chain (e.g. a sidechain), where an asset must be replicated and mapped to an existing asset (e.g. on the mainnet). 260 | 261 | ### associateToken (newToken, origToken, toChain) onlyAdmin 262 | 263 | ``` 264 | Function: associateToken 265 | Purpose: Associate an existing token to a replicated token. 266 | Arguments: 267 | * newToken (address: newly replicated token on bridged blockchain) 268 | * origtoken (address: token on this blockchain to map) 269 | * fromChain (address: bridge contract on blockchain housing the replicated token) 270 | ``` 271 | 272 | #### Notes: 273 | 274 | * This function complements `addToken`, which would be called on a sidechain. This function would be called on e.g. the mainnet. This exists because *both sides* of the bridge need to have the same token mapping (mirrored, of course). 275 | 276 | ### updateValidatorThreshold (newThreshold) onlyAdmin 277 | 278 | ``` 279 | Function: updateValidatorThreshold 280 | Purpose: Change the number of validators required to propose a root 281 | Arguments: 282 | * newThreshold (uint256: new number of validators needed to propose a root) 283 | ``` 284 | 285 | #### Notes: 286 | 287 | * This function may be deprecated after v0.1 288 | 289 | ### updateReward (base, a, max) 290 | 291 | ``` 292 | Function: updateReward 293 | Purpose: Change the reward issued to the proposer 294 | Arguments: 295 | * base (uint256: minimum number of tokens rewarded for proposing a root) 296 | * a (uint256: number of tokens per additional block in the range of the root tree) 297 | * max (uint256: maximum number of tokens rewarded for proposing a root) 298 | ``` 299 | 300 | #### Notes: 301 | 302 | * Based on the slope of this reward curve (`a`), the maximum may be reached more quickly with a change. 303 | * Only the admin may call this function for now, but that may be changed in future versions 304 | 305 | # Installation and Setup 306 | 307 | ## EthPM 308 | 309 | This package is not yet installable via EthPM. 310 | 311 | ## Setup and Testing 312 | 313 | In order to run tests against the contract, execute the following commands, which should be self-explaining 314 | 315 | ```sh 316 | git clone https://github.com/GridPlus/cryptobridge-contracts.git 317 | cd cry*ts 318 | npm install 319 | cp secretsTEMPLATE.json secrets.json 320 | npm install -g truffle 321 | truffle install tokens 322 | ``` 323 | 324 | Important: If you are a network participant, then you have to replace the seed-phrase within secrets.json with your own unique seed-phrase. For a simple isolated test-run, secrets.json can remain as it is. 325 | 326 | ## Starting Test Networks 327 | 328 | The convenience script `parity/boot.js` boots multiple parity instances with one command. All instances will have instant sealing. Unfortunately, this will be a lot slower than using TestRPC/Ganache (1). 329 | 330 | ``` 331 | npm run parity 7545 8545 332 | ``` 333 | 334 | ## Testing 335 | 336 | Start the tests via truffle (which launches `truffle.js` first, then `test/bridge.js`) 337 | 338 | ``` 339 | truffle compile 340 | truffle test 341 | ``` 342 | 343 | Further testing runs (with no contract changes) only require `truffle test`. 344 | 345 | For the case you ran into problems, cleanup the build directory with `rm -rf build` (or `rmdir /S /Q build`) before running `truffle compile && truffle test`. 346 | 347 | ## Sending Tokens 348 | 349 | A convenience script is included to allow you to send tokens to a recipient once your `secrets.json` file is set up. If you'd like to see which options you may use, run: 350 | 351 | ``` 352 | node scripts/sendTokens.js --help 353 | ``` 354 | 355 | Here is an example using the default network (`localhost:7545`): 356 | 357 | ``` 358 | node scripts/sendTokens.js --token 0x87a464eb78986993a16bbeff76e1e3f0cd181060 --to 0xd1aa9d98da70774190b6a80fbead38b1e4e07928 --number 100 359 | ``` 360 | 361 | ## (1) TestRPC/Ganache 362 | 363 | Unfortunately TestRPC/Ganache are incompatible with these tests because they do not provide `v`, `r`, `s` signature parameters for transactions (see [issue](https://github.com/trufflesuite/ganache/issues/294)). 364 | -------------------------------------------------------------------------------- /contracts/Bridge.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.18; 2 | 3 | import "./MerklePatriciaProof.sol"; 4 | import './RLPEncode.sol'; 5 | import "./BytesLib.sol"; 6 | import "tokens/contracts/eip20/EIP20.sol"; 7 | 8 | contract Bridge { 9 | //helpers 10 | function toBytes(address a) constant returns (bytes b) { 11 | assembly { 12 | let m := mload(0x40) 13 | mstore(add(m, 20), xor(0x140000000000000000000000000000000000000000, a)) 14 | mstore(0x40, add(m, 52)) 15 | b := m 16 | } 17 | } 18 | 19 | function toBytes(uint256 x) returns (bytes b) { 20 | b = new bytes(32); 21 | assembly { mstore(add(b, 32), x) } 22 | } 23 | 24 | function encodeAddress(address a) returns(bytes) { 25 | return BytesLib.concat(new bytes(12) , toBytes(a)); 26 | } 27 | 28 | // =========================================================================== 29 | // GLOBAL VARIABLES 30 | // =========================================================================== 31 | 32 | // This maps the start block and end block for a given chain to an epoch 33 | // index (i) and provides the root. 34 | event RootStorage(address indexed chain, uint256 indexed start, 35 | uint256 indexed end, bytes32 headerRoot, uint256 i, address proposer); 36 | event Deposit(address indexed user, address indexed toChain, 37 | address indexed depositToken, address fromChain, uint256 amount); 38 | event Withdraw(address indexed user, address indexed fromChain, 39 | address indexed withdrawToken, uint256 amount); 40 | event TokenAdded(address indexed fromChain, address indexed origToken, 41 | address indexed newToken); 42 | event TokenAssociated(address indexed toChain, address indexed fromToken, 43 | address indexed toToken); 44 | 45 | 46 | // Admin has the ability to add tokens to the bridge 47 | address public admin; 48 | 49 | // The reward function, which is of form (reward = base + a*n) 50 | // where n is the number of blocks proposed in the header (end-start) 51 | struct Reward { 52 | uint256 base; 53 | uint256 a; 54 | } 55 | Reward reward; 56 | uint256 public maxReward; 57 | 58 | // Reward for successfully contesting a headerRoot 59 | uint256 public bountyWei; 60 | 61 | // The randomness seed of the epoch. This is used to determine the proposer 62 | // and the validator pool 63 | bytes32 public epochSeed = keccak256(block.difficulty + block.number + now); 64 | 65 | // Global pool of stakers - indexed by address leading to stake size 66 | struct Stake { 67 | uint256 amount; 68 | address staker; 69 | } 70 | mapping(address => uint256) stakers; 71 | Stake[] stakes; 72 | uint256 public stakeSum; 73 | address public stakeToken; 74 | uint256 public validatorThreshold = 0; 75 | 76 | // Pending withdrawals. The user prepares a withdrawal with tx data and then 77 | // releases it with a withdraw. It can be overwritten by the user and gets wiped 78 | // upon withdrawal. 79 | struct Withdrawal { 80 | address withdrawToken; // Token to withdraw (i.e. the one mapped to deposit) 81 | address fromChain; 82 | uint256 amount; // Number of atomic units to withdraw 83 | bytes32 txRoot; // Transactions root for the block housing this tx 84 | bytes32 txHash; // Hash of this tx 85 | bytes32 receiptsRoot; // Receipts root for the block housing this tx 86 | } 87 | mapping(address => Withdrawal) pendingWithdrawals; 88 | 89 | // The root of a Merkle tree made of consecutive block headers. 90 | // These are indexed by the chainId of the Bridge contract on the 91 | // sidechain. This also serves as the identity of the chain itself. 92 | // The associatin between address-id and chain-id is stored off-chain but it 93 | // must be 1:1 and unique. 94 | mapping(address => bytes32[]) roots; 95 | 96 | // Tracking the last block for each bridge network 97 | mapping(address => uint256) lastBlock; 98 | 99 | // Tokens need to be associated between chains. For now, only the admin can 100 | // create and map tokens on the sidechain to tokens on the main chain 101 | // fromChainId => (oldTokenAddr => newTokenAddr) 102 | mapping(address => mapping(address => address)) tokens; 103 | 104 | // =========================================================================== 105 | // STAKER FUNCTIONS 106 | // =========================================================================== 107 | 108 | // Stake a specified quantity of the staking token 109 | function stake(uint256 amount) public { 110 | EIP20 t = EIP20(stakeToken); 111 | t.transferFrom(msg.sender, address(this), amount); 112 | // We can't have a 0-length stakes array 113 | if (stakers[msg.sender] == 0) { 114 | // If the staker is new 115 | Stake memory s; 116 | s.amount = amount; 117 | s.staker = msg.sender; 118 | if (stakes.length == 0) { stakes.push(s); } 119 | stakes.push(s); 120 | stakers[msg.sender] = stakes.length - 1; 121 | } else { 122 | // Otherwise we can just add to the stake 123 | stakes[stakers[msg.sender]].amount += amount; 124 | } 125 | stakeSum += amount; 126 | } 127 | 128 | // Remove stake 129 | // TODO: This can probably be rolled into stake() 130 | function destake(uint256 amount) public { 131 | assert(stakers[msg.sender] != 0); 132 | assert(amount <= stakes[stakers[msg.sender]].amount); 133 | //stakeSum -= amount; 134 | stakes[stakers[msg.sender]].amount -= amount; 135 | stakeSum -= amount; 136 | EIP20 t = EIP20(stakeToken); 137 | t.transfer(msg.sender, amount); 138 | if (stakes[stakers[msg.sender]].amount == 0) { 139 | delete stakes[stakers[msg.sender]]; 140 | } 141 | } 142 | 143 | // Save a hash to an append-only array of rootHashes associated with the 144 | // given origin chain address-id. 145 | // TODO: This can be turned into a loop so the proposer can submit roots 146 | // from multiple bridged chains. The reward will be increased with more roots. 147 | function proposeRoot(bytes32 headerRoot, address chainId, uint256 end, bytes sigs) 148 | public { 149 | // Make sure we are adding blocks 150 | assert(end > lastBlock[chainId] + 1); 151 | // Make sure enough validators sign off on the proposed header root 152 | assert(checkSignatures(headerRoot, chainId, lastBlock[chainId] + 1, end, sigs) >= validatorThreshold); 153 | // Add the header root 154 | roots[chainId].push(headerRoot); 155 | // Calculate the reward and issue it 156 | uint256 r = reward.base + reward.a * (end - lastBlock[chainId]); 157 | // If we exceed the max reward, anyone can propose the header root 158 | if (r > maxReward) { 159 | r = maxReward; 160 | } else { 161 | assert(msg.sender == getProposer()); 162 | } 163 | msg.sender.transfer(r); 164 | epochSeed = keccak256(block.difficulty + block.number + now); 165 | RootStorage(chainId, lastBlock[chainId] + 1, end, headerRoot, roots[chainId].length - 1, msg.sender); 166 | lastBlock[chainId] = end; 167 | } 168 | 169 | // =========================================================================== 170 | // ADMIN FUNCTIONS 171 | // =========================================================================== 172 | 173 | // Create a token and map it to an existing one on the origin chain 174 | function addToken(address newToken, address origToken, address fromChain) 175 | public payable onlyAdmin() { 176 | // Ether is represented as address(1). We don't need to map the entire supply 177 | // because actors need ether to do anything on this chain. We'll assume 178 | // the accounting is managed off-chain. 179 | if (newToken != address(1)) { 180 | // Adding ERC20 tokens is stricter. We need to map the total supply. 181 | assert(newToken != address(0)); 182 | EIP20 t = EIP20(newToken); 183 | t.transferFrom(msg.sender, address(this), t.totalSupply()); 184 | tokens[fromChain][origToken] = newToken; 185 | } 186 | TokenAdded(fromChain, origToken, newToken); 187 | } 188 | 189 | // Forward association. Map an existing token to a replciated one on the 190 | // destination chain. 191 | // oldToken is on this chain; newToken is on toChain 192 | function associateToken(address newToken, address origToken, address toChain) 193 | public onlyAdmin() { 194 | tokens[toChain][newToken] = origToken; 195 | TokenAssociated(toChain, origToken, newToken); 196 | } 197 | 198 | // Change the number of validators required to allow a passed header root 199 | function updateValidatorThreshold(uint256 newThreshold) public onlyAdmin() { 200 | validatorThreshold = newThreshold; 201 | } 202 | 203 | // The admin can update the reward at any time. 204 | // TODO: We may want to block this during the current epoch, which would require 205 | // we keep a "reward cache" of some kind. 206 | function updateReward(uint256 base, uint256 a, uint256 max) public onlyAdmin() { 207 | reward.base = base; 208 | reward.a = a; 209 | maxReward = max; 210 | } 211 | 212 | // =========================================================================== 213 | // USER FUNCTIONS 214 | // =========================================================================== 215 | 216 | // Any user may make a deposit bound for a particular chainId (address of 217 | // bridge on the destination chain). 218 | // Only tokens for now, but ether may be allowed later. 219 | function deposit(address token, address toChain, uint256 amount) public { 220 | EIP20 t = EIP20(token); 221 | t.transferFrom(msg.sender, address(this), amount); 222 | Deposit(msg.sender, toChain, token, address(this), amount); 223 | } 224 | 225 | // -------------------------------------------------------------------------- 226 | // Witddraw 227 | // -------------------------------------------------------------------------- 228 | 229 | function getStandardV(bytes v, uint256 netVersion) internal constant returns (uint8) { 230 | if (netVersion > 0) { 231 | return uint8(BytesLib.toUint(BytesLib.leftPad(v), 0) - (netVersion * 2) - 8); 232 | } else { 233 | return uint8(BytesLib.toUint(BytesLib.leftPad(v), 0)); 234 | } 235 | } 236 | 237 | // The user who wishes to make a withdrawal sets the transaction here. 238 | // This must correspond to `deposit()` on the fromChain 239 | // The txRoot can be passed in, but it needs to be correct for the second part 240 | // of this process where the user proves the transaction root goes in the 241 | // block header. 242 | // 243 | // addrs = [fromChain, depositToken, withdrawToken] 244 | // 245 | // netVersion is for EIP155 - v = netVersion*2 + 35 or netVersion*2 + 36 246 | // This can be found in a web3 console with web3.version.network. Parity 247 | // also serves it in the transaction log under `chainId` 248 | 249 | function prepWithdraw( 250 | bytes v, 251 | bytes32[3] b32p, // r=0, s=1, txRoot=2 252 | //bytes32 r, 253 | //bytes32 s, 254 | address[3] addrs, 255 | uint256 amount, 256 | //bytes32 txRoot, 257 | bytes path, 258 | bytes parentNodes, 259 | bytes netVersion, 260 | bytes rlpDepositTx, 261 | bytes rlpWithdrawTx 262 | ) 263 | public 264 | { 265 | 266 | // LAZ: 267 | // further reduction of parameters: find method to fetch values directly 268 | // from the rlp-bytes. Reduction of this function is possible, but it cannot 269 | // be merged with proveReceipt, see comments there. 270 | 271 | //assert(tokens[addrs[0]][addrs[3]] == addrs[1]); 272 | // Form the transaction data. 273 | 274 | // Make sure this transaction is the value on the path via a MerklePatricia proof 275 | assert(MerklePatriciaProof.verify(rlpDepositTx, path, parentNodes, b32p[2]) == true); 276 | 277 | // Ensure v,r,s belong to msg.sender 278 | // We want standardV as either 27 or 28 279 | uint8 standardV = getStandardV(v, BytesLib.toUint(BytesLib.leftPad(netVersion), 0)); 280 | assert(msg.sender == ecrecover(keccak256(rlpWithdrawTx), standardV, b32p[0], b32p[1])); 281 | 282 | Withdrawal memory w; 283 | w.withdrawToken = addrs[2]; 284 | w.fromChain = addrs[0]; 285 | w.amount = amount; 286 | w.txRoot = b32p[2]; 287 | w.txHash = keccak256(rlpWithdrawTx); 288 | pendingWithdrawals[msg.sender] = w; 289 | } 290 | 291 | // -------------------------------------------------------------------------- 292 | 293 | // Prove the receipt included in the tx forms the receipt root for the block 294 | // If the proof works, save the receipt root 295 | // Two logs are emitted. Token transfer has 3 topics, Deposit has 4 topics. 296 | // Topics are only for the indexed fields. 297 | // 298 | // --------------------------------------------------------------------------- 299 | // logs format (NOTE: this is falttened as input!): 300 | // [ [addrs[0], [ topics[0], topics[1], topics[2]], data[0] ], 301 | // [addrs[1], [ topics[3], topics[4], topics[5], topics[6] ], data[1] ] ] 302 | // where addrs = [token, bridgeB] 303 | // -------------------------------------------------------------------------- 304 | // data[2] is the receiptsRoot for the block 305 | //function proveReceipt(bytes cumulativeGas, bytes logsBloom, address[2] addrs, 306 | //bytes32[3] data, bytes32[7] topics, bytes path, bytes parentNodes) 307 | function proveReceipt( 308 | bytes logs, 309 | bytes cumulativeGas, 310 | bytes logsBloom, 311 | bytes32 receiptsRoot, 312 | bytes path, 313 | bytes parentNodes 314 | ) 315 | public 316 | { 317 | // LAZ: 318 | // even one(!) more parameter leads to "CompilerError: Stack too deep, 319 | // try removing local variables.". Applying Packing/Slicing has its 320 | // limits, as it has stack-cost, too (and needs sometimes local vars, 321 | // as it does below). 322 | // Suggestion (essentially a must): 323 | // Overall rewrite of proveReceipt, to use other techniques/concepts, 324 | // thus stack-cost is reduced significantly. After this, at minimum 325 | // one of the 2 other functions (prep / withdraw) will be mergable. 326 | 327 | // Make sure the user has a pending withdrawal 328 | //assert(pendingWithdrawals[msg.sender].txRoot != bytes32(0)); 329 | 330 | // Encdode the logs. This is of form: 331 | // [ [addrs[0], [ topics[0], topics[1], topics[2]], data[0] ], 332 | // [addrs[1], [ topics[3], topics[4], topics[5], topics[6] ], data[1] ] ] 333 | bytes[] memory log0 = new bytes[](3); 334 | bytes[] memory topics0 = new bytes[](3); 335 | log0[0] = BytesLib.slice(logs, 0, 20); 336 | topics0[0] = BytesLib.slice(logs, 20, 32); 337 | topics0[1] = BytesLib.slice(logs, 52, 32); 338 | topics0[2] = BytesLib.slice(logs, 84, 32); 339 | log0[1] = RLPEncode.encodeList(topics0); 340 | log0[2] = BytesLib.slice(logs, 116, 32); 341 | 342 | bytes[] memory log1 = new bytes[](3); 343 | bytes[] memory topics1 = new bytes[](4); 344 | log1[0] = BytesLib.slice(logs, 148, 20); 345 | topics1[0] = BytesLib.slice(logs, 168, 32); 346 | topics1[1] = BytesLib.slice(logs, 200, 32); 347 | topics1[2] = BytesLib.slice(logs, 232, 32); 348 | topics1[3] = BytesLib.slice(logs, 264, 32); 349 | log1[1] = RLPEncode.encodeList(topics1); 350 | log1[2] = BytesLib.slice(logs, 296, 64); // this is two 32 byte words 351 | 352 | // We need to hack around the RLPEncode library for the topics, which are 353 | // nested lists 354 | bool[] memory passes = new bool[](4); 355 | passes[0] = false; 356 | passes[1] = true; 357 | passes[2] = false; 358 | bytes[] memory allLogs = new bytes[](2); 359 | allLogs[0] = RLPEncode.encodeListWithPasses(log0, passes); 360 | allLogs[1] = RLPEncode.encodeListWithPasses(log1, passes); 361 | passes[0] = true; 362 | 363 | // Finally, we can encode the receipt 364 | bytes[] memory receipt = new bytes[](4); 365 | receipt[0] = hex"01"; 366 | receipt[1] = cumulativeGas; 367 | receipt[2] = logsBloom; 368 | receipt[3] = RLPEncode.encodeListWithPasses(allLogs, passes); 369 | passes[0] = false; 370 | passes[1] = false; 371 | passes[3] = true; 372 | 373 | // Check that the sender made this transaction 374 | assert(BytesLib.toAddress(topics0[1], 12) == msg.sender); 375 | assert(BytesLib.toAddress(topics1[1], 12) == msg.sender); 376 | 377 | // Check the amount 378 | assert(BytesLib.toUint(log0[2], 0) == pendingWithdrawals[msg.sender].amount); 379 | assert(BytesLib.toUint(log1[2], 32) == pendingWithdrawals[msg.sender].amount); 380 | 381 | // Check that this is the right destination 382 | assert(BytesLib.toAddress(topics1[2], 12) == address(this)); 383 | 384 | // Check that it's coming from the right place 385 | assert(BytesLib.toAddress(log1[0], 0) == pendingWithdrawals[msg.sender].fromChain); 386 | 387 | // Check the token 388 | assert(tokens[pendingWithdrawals[msg.sender].fromChain][BytesLib.toAddress(log0[0], 0)] == pendingWithdrawals[msg.sender].withdrawToken); 389 | 390 | // TODO: There may be more checks for other parts of the logs, but this covers 391 | // the basic stuff 392 | 393 | assert(MerklePatriciaProof.verify(RLPEncode.encodeListWithPasses(receipt, passes), 394 | path, parentNodes, receiptsRoot) == true); 395 | pendingWithdrawals[msg.sender].receiptsRoot = receiptsRoot; 396 | 397 | // LAZ: 398 | // draft for inclusion of final withdraw function call 399 | // Part 3 of withdrawal. At this point, the user has proven transaction and 400 | // receipt. Now the user needs to prove the header. 401 | 402 | // Those lines cannot be merged, as they depend on parameters 403 | //Withdrawal memory w = pendingWithdrawals[msg.sender]; 404 | //bytes32 leaf = keccak256(prevHeader, timestamp, blockNum, w.txRoot, w.receiptsRoot); 405 | //assert(merkleProof(leaf, roots[w.fromChain][rootN], proof) == true); 406 | 407 | // Those lines could be enabled (this results in around 370k gas, but without 408 | // the prooves above) 409 | //EIP20 t = EIP20(pendingWithdrawals[msg.sender].withdrawToken); 410 | //t.transfer(msg.sender, pendingWithdrawals[msg.sender].amount); 411 | //Withdraw(msg.sender, pendingWithdrawals[msg.sender].fromChain, pendingWithdrawals[msg.sender].withdrawToken, pendingWithdrawals[msg.sender].amount); 412 | //delete pendingWithdrawals[msg.sender]; 413 | } 414 | 415 | // -------------------------------------------------------------------------- 416 | 417 | // Part 3 of withdrawal. At this point, the user has proven transaction and 418 | // receipt. Now the user needs to prove the header. 419 | function withdraw( 420 | uint256 blockNum, 421 | uint256 timestamp, 422 | bytes32 prevHeader, 423 | uint rootN, 424 | bytes proof 425 | ) 426 | public 427 | { 428 | Withdrawal memory w = pendingWithdrawals[msg.sender]; 429 | bytes32 leaf = keccak256(prevHeader, timestamp, blockNum, w.txRoot, w.receiptsRoot); 430 | assert(merkleProof(leaf, roots[w.fromChain][rootN], proof) == true); 431 | EIP20 t = EIP20(w.withdrawToken); 432 | t.transfer(msg.sender, w.amount); 433 | Withdraw(msg.sender, w.fromChain, w.withdrawToken, w.amount); 434 | delete pendingWithdrawals[msg.sender]; 435 | } 436 | 437 | // -------------------------------------------------------------------------- 438 | // END WITHDRAW 439 | // -------------------------------------------------------------------------- 440 | 441 | function getPendingToken(address user) public constant returns (address) { 442 | return pendingWithdrawals[user].withdrawToken; 443 | } 444 | 445 | function getPendingAmount(address user) public constant returns (uint256) { 446 | return pendingWithdrawals[user].amount; 447 | } 448 | 449 | function getPendingFromChain(address user) public constant returns (address) { 450 | return pendingWithdrawals[user].fromChain; 451 | } 452 | 453 | function getReward(uint end, address chainId) public constant returns (uint256) { 454 | uint256 r = reward.base + reward.a * (end - lastBlock[chainId]); 455 | // If we exceed the max reward, anyone can propose the header root 456 | if (r > maxReward) { r = maxReward; } 457 | return r; 458 | } 459 | 460 | // =========================================================================== 461 | // UTILITY FUNCTIONS 462 | // =========================================================================== 463 | 464 | 465 | // Check a series of signatures against staker addresses. If there are enough 466 | // signatures (>= validatorThreshold), return true 467 | // NOTE: For the first version, any staker will work. For the future, we should 468 | // select a subset of validators from the staker pool. 469 | function checkSignatures(bytes32 root, address chain, uint256 start, uint256 end, bytes sigs) 470 | public constant returns (uint256) { 471 | bytes32 h = keccak256(root, chain, start, end); 472 | uint256 passed; 473 | address[] memory passing = new address[](sigs.length / 96); 474 | // signs are chunked in 65 bytes -> [r, s, v] 475 | for (uint64 i = 0; i < sigs.length; i += 96) { 476 | bytes32 r = BytesLib.toBytes32(BytesLib.slice(sigs, i, 32)); 477 | bytes32 s = BytesLib.toBytes32(BytesLib.slice(sigs, i + 32, 32)); 478 | uint8 v = uint8(BytesLib.toUint(sigs, i + 64)); 479 | address valTmp = ecrecover(h, v, r, s); 480 | bool noPass = false; 481 | // Unfortunately we need to loop through the cache to make sure there are 482 | // no signature duplicates. This is the most efficient way to do it since 483 | // storage costs too much.s 484 | // Make sure this address is a staker and NOT the proposer 485 | if (stakes[stakers[valTmp]].amount > 0 && valTmp != getProposer()) { 486 | for (uint64 j = 0; j < i / 96; j += 1) { 487 | if (passing[j] == valTmp) { noPass = true; } 488 | } 489 | } 490 | if (noPass == false) { 491 | passing[(i / 96)] = valTmp; 492 | passed ++; 493 | } 494 | } 495 | return passed; 496 | } 497 | 498 | function getStake(address a) public constant returns (uint256) { 499 | return stakes[stakers[a]].amount; 500 | } 501 | 502 | function getStakeIndex(address a) public constant returns (uint256) { 503 | return stakers[a]; 504 | } 505 | 506 | function getLastBlock(address fromChain) public constant returns (uint256) { 507 | return lastBlock[fromChain]; 508 | } 509 | 510 | // Sample a proposer. Likelihood of being chosen is proportional to stake size. 511 | // NOTE: This current design assumes 512 | function getProposer() public constant returns (address) { 513 | // Convert the seed to an index 514 | uint256 target = uint256(epochSeed) % stakeSum; 515 | // Index of stakes 516 | uint64 i = 1; 517 | // Total stake 518 | uint256 sum = 0; 519 | while (sum < target) { 520 | sum += stakes[i].amount; 521 | i += 1; 522 | } 523 | // Winner winner chicken dinner 524 | return stakes[i - 1].staker; 525 | } 526 | 527 | function getTokenMapping(address chain, address token) 528 | public constant returns (address) { 529 | return tokens[chain][token]; 530 | } 531 | 532 | // Get 32 bytes and cast to byes32 533 | function getBytes32(uint64 start, bytes data) pure returns (bytes32) { 534 | bytes32[1] memory newData; 535 | assembly { 536 | mstore(newData, mload(add(start, add(data, 0x32)))) 537 | } 538 | return newData[0]; 539 | } 540 | 541 | // Get 32 bytes and cast to uint256 542 | function getUint256(uint64 start, bytes data) pure returns (uint256) { 543 | uint256[1] memory newData; 544 | assembly { 545 | mstore(newData, mload(add(start, add(data, 0x32)))) 546 | } 547 | return newData[0]; 548 | } 549 | 550 | // Get 8 bytes and cast to uint64 551 | function getUint64(uint64 start, bytes data) pure returns (uint64) { 552 | return uint64(getUint256(start, data)); 553 | } 554 | 555 | // 'proof' is a concatenated set of [right][hash], i.e. 33 byte chunks 556 | function merkleProof(bytes32 leaf, bytes32 targetHash, bytes proof) private constant returns (bool) { 557 | bytes32 currentHash = leaf; 558 | for (uint i = 0; i < proof.length / 33; i++) { 559 | if (BytesLib.slice(proof, i*33, 1)[0] == 1) { 560 | currentHash = keccak256(currentHash, BytesLib.slice(proof, i * 33 + 1, 32)); 561 | } else { 562 | currentHash = keccak256(BytesLib.slice(proof, i * 33 + 1, 32), currentHash); 563 | } 564 | } 565 | return currentHash == targetHash; 566 | } 567 | 568 | // Staking token can only be set at instantiation! 569 | function Bridge(address token) { 570 | admin = msg.sender; 571 | stakeToken = token; 572 | } 573 | 574 | modifier onlyAdmin() { 575 | require(msg.sender == admin); 576 | _; 577 | } 578 | 579 | function() public payable {} 580 | 581 | } 582 | -------------------------------------------------------------------------------- /test/bridge.js: -------------------------------------------------------------------------------- 1 | const Promise = require('bluebird').Promise; 2 | const bip39 = require('bip39'); 3 | const hdkey = require('ethereumjs-wallet/hdkey'); 4 | const leftPad = require('left-pad'); 5 | const secrets = require('../secrets.json'); 6 | const sha3 = require('solidity-sha3').default; 7 | const util = require('ethereumjs-util'); 8 | const truffleConf = require('../truffle.js').networks; 9 | const Web3 = require('web3'); 10 | const EthProof = require('eth-proof'); 11 | const txProof = require('./util/txProof.js'); 12 | const rProof = require('./util/receiptProof.js'); 13 | const rlp = require('rlp'); 14 | const blocks = require('./util/blocks.js'); 15 | const val = require('./util/val.js'); 16 | const Token = artifacts.require('EIP20.sol'); // EPM package 17 | const Bridge = artifacts.require('./Bridge.sol'); 18 | const EthereumTx = require('ethereumjs-tx'); 19 | const EthUtil = require('ethereumjs-util'); 20 | const BN = require('big-number'); 21 | const merkle = require('./util/merkle.js'); 22 | const sync = require('./util/sync.js'); 23 | 24 | // Need two of these 25 | const _providerA = `http://${truffleConf.development.host}:${truffleConf.development.port}`; 26 | const providerA = new Web3.providers.HttpProvider(_providerA); 27 | const web3A = new Web3(providerA); 28 | const epA = new EthProof(providerA); 29 | const _providerB = `http://${truffleConf.developmentB.host}:${truffleConf.developmentB.port}`; 30 | const providerB = new Web3.providers.HttpProvider(_providerB); 31 | const web3B = new Web3(providerB); 32 | const epB = new EthProof(providerB); 33 | 34 | // ABI and bytes for interacting with web3B 35 | const BridgeABI = require('../build/contracts/Bridge.json').abi; 36 | const BridgeBytes = require('../build/contracts/Bridge.json').bytecode; 37 | const tokenABI = require('../build/contracts/EIP20.json').abi; 38 | const tokenBytes = require('../build/contracts/EIP20.json').bytecode; 39 | const merkleLibBytes = require('../build/contracts/MerklePatriciaProof.json').bytecode; 40 | 41 | // Global variables (will be references throughout the tests) 42 | let wallets; 43 | let stakingToken; 44 | let tokenA; 45 | let tokenB; 46 | let BridgeA; 47 | let BridgeB; 48 | let merkleLibBAddr; 49 | let deposit; 50 | let depositBlock; 51 | let depositBlockSlim; // Contains only tx hashes 52 | let depositHeader; 53 | let depositReceipt; 54 | let headers; 55 | let headerRoot; 56 | let sigs = []; 57 | let gasPrice = 10 ** 9; 58 | let RootStorageIndex; // This is needed for the final withdrawal 59 | let startingHeader; 60 | let startingBlockNumber; 61 | let treeHeaders; 62 | // Parameters that can be changed throughout the process 63 | let proposer; 64 | 65 | // left-pad half-bytes 66 | function ensureByte(s) { 67 | if (s.substr(0, 2) == '0x') { s = s.slice(2); } 68 | if (s.length % 2 == 0) { return `0x${s}`; } 69 | else { return `0x0${s}`; } 70 | } 71 | 72 | contract('Bridge', (accounts) => { 73 | assert(accounts.length > 0); 74 | function isEVMException(err) { 75 | return err.toString().includes('VM Exception') || err.toString().includes('StatusError'); 76 | } 77 | 78 | function generateFirstWallets(n, _wallets, hdPathIndex) { 79 | const hdwallet = hdkey.fromMasterSeed(bip39.mnemonicToSeed(secrets.mnemonic)); 80 | const node = hdwallet.derivePath(secrets.hdPath + hdPathIndex.toString()); 81 | const secretKey = node.getWallet().getPrivateKeyString(); 82 | const addr = node.getWallet().getAddressString(); 83 | _wallets.push([addr, secretKey]); 84 | const nextHDPathIndex = hdPathIndex + 1; 85 | if (nextHDPathIndex >= n) { 86 | return _wallets; 87 | } 88 | return generateFirstWallets(n, _wallets, nextHDPathIndex); 89 | } 90 | 91 | async function saveDummyCheckpoints(ends, start, i=0) { 92 | if (i == ends.length) { return true; } 93 | else { 94 | let end = ends[i] 95 | // Sign and store 96 | let signers = []; 97 | const headerRoot = sha3('dummy'); 98 | const msg = val.getMsg(headerRoot, BridgeB.options.address, parseInt(start), end); 99 | let sigs = []; 100 | // wallets[j+1] = accounts[j] and we're looking for accounts 1-4 101 | const proposer = await BridgeA.getProposer(); 102 | for (let j = 0; j < 4; j++) { 103 | if (wallets[j+1][0] != proposer) { 104 | sigs.push(val.sign(msg, wallets[j+1])); 105 | signers.push(wallets[j+1][0]); 106 | } 107 | } 108 | const sigData = val.formatSigs(sigs) 109 | const proposeRoot = await BridgeA.proposeRoot(headerRoot, BridgeB.options.address, end, sigData, 110 | { from: proposer, gas: 500000, gasPrice: gasPrice }); 111 | console.log(`Propose root gas usage (${start} - ${end}): ${proposeRoot.receipt.gasUsed}`); 112 | start = end + 1; 113 | i ++; 114 | return saveDummyCheckpoints(ends, start, i); 115 | } 116 | } 117 | 118 | before (async () => { 119 | const syncData = await sync.fsSync(web3B); 120 | startingHeader = syncData[0]; 121 | startingBlockNumber = syncData[1]; 122 | }); 123 | 124 | describe('Wallets', () => { 125 | it('Should create wallets for first 5 accounts', async () => { 126 | wallets = generateFirstWallets(5, [], 0); 127 | assert(wallets.length == 5); 128 | assert(wallets[1][0] == accounts[0]); 129 | }) 130 | }) 131 | 132 | describe('Admin: Bridge setup', () => { 133 | it('Should create a token on chain A and give it out to accounts 1-3', async () => { 134 | stakingToken = await Token.new(1000, 'Staking', 0, 'STK', { from: accounts[0] }); 135 | console.log('Staking token A: ', stakingToken.address); 136 | // Need to stake from wallets rather than accounts since validators sigs 137 | // will come from wallets. 138 | // For some reason the wallet indexing doesn't seem to match up to the 139 | // account indexing (the first wallet is different, but also the ordering 140 | // of subsequent ones seems different) 141 | // wallets[1] == accounts[0] 142 | await stakingToken.transfer(wallets[1][0], 100); 143 | await stakingToken.transfer(wallets[2][0], 100); 144 | await stakingToken.transfer(wallets[3][0], 100); 145 | await stakingToken.transfer(wallets[4][0], 100); 146 | }); 147 | 148 | it('Should create the Bridge with the token as the staking token', async () => { 149 | BridgeA = await Bridge.new(stakingToken.address, { from: accounts[0] }); 150 | const admin = await BridgeA.admin(); 151 | assert(admin == accounts[0]); 152 | console.log(`Bridge on ${_providerA}: ${BridgeA.address}`) 153 | }); 154 | 155 | // NOTE: Even though I'm getting values of 3 for checkSignatures, it's still 156 | // failing validation checks. I'm really not sure why, but I've knocked it 157 | // down to 2 and it works fine. 158 | // TODO: Investigate this further 159 | it('Should set the validator threshold to 2', async () => { 160 | await BridgeA.updateValidatorThreshold(2); 161 | const thresh = await BridgeA.validatorThreshold(); 162 | assert(parseInt(thresh) === 2); 163 | }) 164 | 165 | it('Should give a small amount of ether to the Bridge', async () => { 166 | await web3A.eth.sendTransaction({ 167 | to: BridgeA.address, 168 | value: 10 ** 17, 169 | from: accounts[0] 170 | }); 171 | }); 172 | 173 | it('Should set the reward parameters of the Bridge', async () => { 174 | const BASE = 10 ** 16; 175 | await BridgeA.updateReward(BASE, 0, BASE, { from: accounts[0] }); 176 | const maxReward = await BridgeA.maxReward(); 177 | assert(maxReward == BASE); 178 | }); 179 | 180 | it('Should stake via wallets[1]', async () => { 181 | const user = wallets[1][0]; 182 | const amount = 1; 183 | await stakingToken.approve(BridgeA.address, amount, { from: user }); 184 | await BridgeA.stake(amount, { from: user }); 185 | const stakeSumTmp = await BridgeA.stakeSum(); 186 | console.log('stakeSumTmp', parseInt(stakeSumTmp), 'amount', amount); 187 | assert(parseInt(stakeSumTmp) === amount); 188 | const currentStake = await BridgeA.getStake(user); 189 | console.log('currentStake', currentStake) 190 | assert(parseInt(currentStake) === amount); 191 | }) 192 | 193 | it('Should stake via wallets[2]', async () => { 194 | const user = wallets[2][0]; 195 | const amount = 1; 196 | await stakingToken.approve(BridgeA.address, amount, { from: user }); 197 | await BridgeA.stake(amount, { from: user }); 198 | const stakeSumTmp = await BridgeA.stakeSum(); 199 | assert(parseInt(stakeSumTmp) === 2); 200 | const currentStake = await BridgeA.getStake(user); 201 | assert(parseInt(currentStake) === amount); 202 | }); 203 | 204 | it('Should stake via wallets[3]', async () => { 205 | const user = wallets[3][0]; 206 | const amount = 10; 207 | await stakingToken.approve(BridgeA.address, amount, { from: user }); 208 | await BridgeA.stake(amount, { from: user }); 209 | const stakeSumTmp = await BridgeA.stakeSum(); 210 | assert(parseInt(stakeSumTmp) === 12); 211 | const currentStake = await BridgeA.getStake(user); 212 | assert(parseInt(currentStake) === amount); 213 | }); 214 | 215 | it('Should stake via wallets[4]', async () => { 216 | const user = wallets[4][0]; 217 | const amount = 100; 218 | await stakingToken.approve(BridgeA.address, amount, { from: user }); 219 | await BridgeA.stake(amount, { from: user }); 220 | const stakeSumTmp = await BridgeA.stakeSum(); 221 | assert(parseInt(stakeSumTmp) === 112); 222 | const currentStake = await BridgeA.getStake(user); 223 | assert(parseInt(currentStake) === amount); 224 | }); 225 | 226 | it('Should destake a small amount from wallets[4]', async () => { 227 | await BridgeA.destake(1, { from: wallets[4][0] }); 228 | const stakeSumTmp = await BridgeA.stakeSum(); 229 | assert(parseInt(stakeSumTmp) === 111); 230 | }) 231 | 232 | it('Should get the proposer and make sure it is a staker', async () => { 233 | const seed = await BridgeA.epochSeed(); 234 | let stakeSum = await BridgeA.stakeSum(); 235 | stakeSum = parseInt(stakeSum); 236 | proposer = await BridgeA.getProposer(); 237 | assert(proposer === wallets[1][0] || proposer === wallets[2][0] || proposer === wallets[3][0] || proposer === wallets[4][0]); 238 | }); 239 | 240 | it('Should deploy MerkleLib and use it to deploy a Bridge on chain B', async () => { 241 | // Deploy the library 242 | const libReceipt = await web3B.eth.sendTransaction({ 243 | from: accounts[0], 244 | data: merkleLibBytes, 245 | gas: 3000000, 246 | }); 247 | merkleLibBAddr = libReceipt.contractAddress.slice(2).toLowerCase(); 248 | const BridgeBytesB = BridgeBytes.replace(/_+MerkleLib_+/g, merkleLibBAddr); 249 | const txReceipt = await web3B.eth.sendTransaction({ 250 | from: accounts[0], 251 | data: BridgeBytesB, 252 | gas: 7000000, 253 | }); 254 | assert(txReceipt.blockNumber >= 0); 255 | BridgeB = await new web3B.eth.Contract(BridgeABI, txReceipt.contractAddress); 256 | assert(txReceipt.contractAddress === BridgeB.options.address); 257 | BridgeB.setProvider(providerB); 258 | console.log(`Bridge on ${_providerB}: ${BridgeB.options.address}`) 259 | }); 260 | }); 261 | 262 | describe('Admin: Token mapping', () => { 263 | it('Should create a new token (token B) on chain B', async () => { 264 | const tokenBTmp = await new web3B.eth.Contract(tokenABI); 265 | await tokenBTmp.deploy({ 266 | data: tokenBytes, 267 | arguments: [1000, 'TokenB', 0, 'TKB'] 268 | }) 269 | .send({ from: accounts[0], gas: 3000000 }) 270 | .then((tkb) => { 271 | tokenB = tkb; 272 | assert(tkb.options.address != null); 273 | tokenB.setProvider(providerB); 274 | }) 275 | }); 276 | 277 | it('Should create a new token (token A) on chain A', async () => { 278 | tokenA = await Token.new(1000, 'TokenA', 0, 'TKA', { from: accounts[0] }); 279 | }); 280 | 281 | it('Should fail to map tokenB because it has not been given allowance', async () => { 282 | try { 283 | await BridgeA.addToken(tokenA.address, tokenB.options.address, BridgeB.options.address) 284 | } catch (err) { 285 | assert(isEVMException(err) === true); 286 | } 287 | }); 288 | 289 | it('Should map token on chain A to the one on chain B', async () => { 290 | await tokenA.approve(BridgeA.address, 1000); 291 | await BridgeA.addToken(tokenA.address, tokenB.options.address, BridgeB.options.address) 292 | const tkB = await BridgeA.getTokenMapping(BridgeB.options.address, tokenB.options.address); 293 | assert(tkB.toLowerCase() == tokenA.address.toLowerCase()); 294 | }); 295 | 296 | it('Should map token on chainB to the one on chain A', async () => { 297 | await BridgeB.methods.associateToken(tokenA.address, tokenB.options.address, BridgeA.address) 298 | .send({ from: accounts[0] }); 299 | const associatedToken = await BridgeB.methods.getTokenMapping(BridgeA.address, tokenA.address).call(); 300 | assert(associatedToken.toLowerCase() == tokenB.options.address.toLowerCase()); 301 | }) 302 | 303 | it('Should ensure the Bridge on chain A has all of the mapped token', async () => { 304 | const supply = await tokenA.totalSupply(); 305 | const held = await tokenA.balanceOf(BridgeA.address); 306 | assert(parseInt(supply) === parseInt(held)); 307 | }); 308 | 309 | it('Should give 5 token B to wallets[1]', async () => { 310 | const r = await tokenB.methods.transfer(wallets[2][0], 5).send({ from: accounts[0] }) 311 | const balance = await tokenB.methods.balanceOf(wallets[2][0]).call(); 312 | assert(parseInt(balance) === 5); 313 | }); 314 | }); 315 | 316 | describe('Stakers: Bridge backlog', () => { 317 | let ends = []; 318 | let sigData = []; 319 | const headerRoot = sha3('fake'); // We can fake this one since there are no deposits 320 | 321 | it('Should get a set of end points to Bridge (powers of two)', async () => { 322 | let _ends = []; 323 | // Get to the latest block with a set of end points 324 | const latestBlock = await web3B.eth.getBlockNumber(); 325 | const lastBlock = await BridgeA.getLastBlock(BridgeB.options.address); 326 | 327 | // Get the set of end points that we will want to checkpoint. There should 328 | // only be a handful since they get exponentially smaller 329 | _ends.push(blocks.getLastPowTwo(parseInt(latestBlock))); 330 | let endsSum = _ends[0]; 331 | while (blocks.getLastPowTwo(latestBlock - endsSum) > 1) { 332 | const nextEnd = blocks.getLastPowTwo(latestBlock - endsSum); 333 | _ends.push(nextEnd); 334 | endsSum += nextEnd; 335 | } 336 | let endsSum2 = 0; 337 | _ends.forEach((end, i) => { 338 | ends.push(parseInt(end) + parseInt(lastBlock) + endsSum2); 339 | endsSum2 += end; 340 | }) 341 | }); 342 | 343 | it('Should go through end points and get signatures', async () => { 344 | const _lastBlock = await BridgeA.getLastBlock(BridgeB.options.address); 345 | const saved = await saveDummyCheckpoints(ends, _lastBlock + 1); 346 | assert(saved === true); 347 | }); 348 | }); 349 | 350 | describe('User: Deposit tokens on chain B', () => { 351 | it('Should deposit 5 token B to the Bridge on chain B', async () => { 352 | await tokenB.methods.approve(BridgeB.options.address, 5).send({ from: wallets[2][0] }) 353 | const allowance = await tokenB.methods.allowance(accounts[1], BridgeB.options.address).call(); 354 | const _deposit = await BridgeB.methods.deposit(tokenB.options.address, BridgeA.address, 5) 355 | .send({ from: wallets[2][0], gas: 500000 }) 356 | const balance = await tokenB.methods.balanceOf(accounts[1]).call(); 357 | deposit = await web3B.eth.getTransaction(_deposit.transactionHash); 358 | depositBlock = await web3B.eth.getBlock(_deposit.blockHash, true); 359 | depositBlockSlim = await web3B.eth.getBlock(_deposit.blockHash, false); 360 | depositReceipt = await web3B.eth.getTransactionReceipt(_deposit.transactionHash); 361 | }); 362 | }) 363 | 364 | describe('Stakers: Bridge blocks', () => { 365 | let end; 366 | let sigData; 367 | let lastBlock 368 | 369 | it('Should fast forward blockchain to next power of two', async() => { 370 | lastBlock = await BridgeA.getLastBlock(BridgeB.options.address); 371 | lastBlock = parseInt(lastBlock); 372 | const currentBlock = await web3B.eth.getBlockNumber(); 373 | let diff = currentBlock - lastBlock; 374 | let toMine = blocks.getNextPowTwo(diff) - diff; 375 | if (blocks.isPowTwo(diff)) { toMine = 0; } 376 | await blocks.forceMine(toMine, accounts[0], web3B) 377 | end = await web3B.eth.getBlockNumber(); 378 | assert(blocks.isPowTwo(end - lastBlock) === true); 379 | }); 380 | 381 | it('Should get tree headers', async () => { 382 | const currentBlockNumber = await web3B.eth.getBlockNumber(); 383 | assert(currentBlockNumber > startingBlockNumber, 'Your chain is oversynced. Did you delete your "lastBlock" file?') 384 | const allHeaders = await blocks.getHeaders(startingBlockNumber, currentBlockNumber, web3B, [startingHeader]); 385 | treeHeaders = allHeaders.slice(allHeaders.length - (currentBlockNumber - lastBlock)); 386 | }); 387 | 388 | it('Should form a Merkle tree from the last block headers, get signatures, and submit to chain A', async () => { 389 | depositHeader = treeHeaders[depositBlock.number - (lastBlock + 1)]; 390 | tree = merkle.buildTree(treeHeaders); 391 | assert(tree[tree.length - 1].length == 1, 'Merkle tree formatted incorrectly - no root.'); 392 | headerRoot = tree[tree.length - 1][0]; 393 | }); 394 | 395 | it('Should get signatures from stakers for proposed header root and check them', async () => { 396 | // Sign and store 397 | let signers = []; 398 | const msg = val.getMsg(headerRoot, BridgeB.options.address, lastBlock + 1, end); 399 | let sigs = []; 400 | proposer = await BridgeA.getProposer(); 401 | // wallets[i+1] = accounts[i] and we're looking for accounts 1-4 402 | for (let i = 0; i < 4; i++) { 403 | if (wallets[i+1][0] != proposer) { 404 | sigs.push(val.sign(msg, wallets[i+1])); 405 | signers.push(wallets[i+1][0]); 406 | } 407 | } 408 | sigData = val.formatSigs(sigs); 409 | const checkSignatures = await BridgeA.checkSignatures(headerRoot, BridgeB.options.address, lastBlock + 1, end, sigData); 410 | const bountyStart = await web3A.eth.getBalance(BridgeA.address); 411 | const proposerStart = await web3A.eth.getBalance(proposer); 412 | const reward = await BridgeA.getReward(end, BridgeB.options.address); 413 | 414 | // TODO: Add fast-forward function to get to a specified block 415 | // TODO: Fast-forward to the next 2^N block 416 | const proposeRoot = await BridgeA.proposeRoot(headerRoot, BridgeB.options.address, end, sigData, 417 | { from: proposer, gas: 500000, gasPrice: gasPrice }); 418 | RootStorageIndex = parseInt(proposeRoot.logs[0].args.i); 419 | console.log('Propose root gas usage: ', proposeRoot.receipt.gasUsed); 420 | const bountyEnd = await web3A.eth.getBalance(BridgeA.address); 421 | const proposerEnd = await web3A.eth.getBalance(proposer); 422 | const gasCost = proposeRoot.receipt.gasUsed * gasPrice; 423 | const diffBounty = bountyStart - bountyEnd; 424 | assert(diffBounty === parseInt(reward)); 425 | assert(parseInt(BN(proposerEnd).plus(gasCost).minus(proposerStart)) === parseInt(reward)); 426 | }); 427 | }) 428 | 429 | describe('User: Withdraw tokens on chain A', () => { 430 | 431 | it('Should check that the deposit txParams hash was signed by wallets[2]', async () => { 432 | const unsignedDeposit = deposit; 433 | unsignedDeposit.value = ''; 434 | unsignedDeposit.gasPrice = parseInt(deposit.gasPrice); 435 | const ethtx = new EthereumTx(unsignedDeposit); 436 | const ethtxhash = ethtx.hash(false); 437 | const signingV = parseInt(deposit.standardV) + 27; 438 | 439 | const signerPub = EthUtil.ecrecover(ethtxhash, signingV, deposit.r, deposit.s) 440 | const signer = EthUtil.pubToAddress(signerPub).toString('hex'); 441 | assert(`0x${signer}` === wallets[2][0]); 442 | }); 443 | 444 | 445 | it('Should prepare the withdrawal with the transaction data (wallets[2])', async () => { 446 | const proof = await txProof.build(deposit, depositBlock); 447 | const path = ensureByte(rlp.encode(proof.path).toString('hex')); 448 | const parentNodes = ensureByte(rlp.encode(proof.parentNodes).toString('hex')); 449 | 450 | const nonce = ensureByte(`0x${parseInt(deposit.nonce).toString(16)}`); 451 | const gasPrice = ensureByte(`0x${parseInt(deposit.gasPrice).toString(16)}`); 452 | const gas = ensureByte(`0x${parseInt(deposit.gas).toString(16)}`); 453 | 454 | // Make sure we are RLP encoding the transaction correctly. `encoded` corresponds 455 | // to what Solidity calculates. 456 | const rlpDepositTxHex = rlp.encode([ 457 | nonce, 458 | gasPrice, 459 | gas, 460 | BridgeB.options.address, 461 | '', 462 | deposit.input, 463 | deposit.v, 464 | deposit.r, 465 | deposit.s 466 | ]).toString('hex'); 467 | 468 | const rlpDepositTxProof = rlp.encode(proof.value).toString('hex'); 469 | assert(rlpDepositTxHex == rlpDepositTxProof, 'Tx RLP encoding incorrect'); 470 | 471 | // Check the proof in JS first 472 | const checkpoint = txProof.verify(proof, 4); 473 | assert(checkpoint === true); 474 | 475 | // Get the network version 476 | const version = parseInt(deposit.chainId); 477 | 478 | const rlpDepositTxData = rlp.encode(proof.value); 479 | const rlpWithdrawTxData = rlp.encode([ 480 | nonce, 481 | gasPrice, 482 | gas, 483 | BridgeB.options.address, 484 | '', 485 | deposit.input, 486 | version, 487 | "", 488 | "" 489 | ]); 490 | 491 | // Make the transaction 492 | const prepWithdraw = await BridgeA.prepWithdraw( 493 | deposit.v, 494 | [deposit.r, deposit.s, depositBlock.transactionsRoot], 495 | [BridgeB.options.address, tokenB.options.address, tokenA.address], 496 | 5, 497 | path, 498 | parentNodes, 499 | version, 500 | //LAZ: passing as 'hex' results in ascii beeing received in contract 501 | rlpDepositTxData.toString('binary'), 502 | rlpWithdrawTxData.toString('binary'), 503 | { from: wallets[2][0], gas: 500000 } 504 | ); 505 | 506 | console.log('prepWithdraw gas usage:', prepWithdraw.receipt.gasUsed); 507 | assert(prepWithdraw.receipt.gasUsed < 500000); 508 | }) 509 | 510 | //LAZ: use wallets[2][0] instead of accounts[2], see problems described above 511 | it('Should check the pending withdrawal fields', async () => { 512 | const pendingToken = await BridgeA.getPendingToken( wallets[2][0] ); 513 | assert(pendingToken.toLowerCase() == tokenA.address.toLowerCase()); 514 | const pendingFromChain = await BridgeA.getPendingFromChain( wallets[2][0] ); 515 | assert(pendingFromChain.toLowerCase() == BridgeB.options.address.toLowerCase()); 516 | }) 517 | 518 | it('Should prove the state root', async () => { 519 | // Get the receipt proof 520 | const receiptProof = await rProof.buildProof(depositReceipt, depositBlockSlim, web3B); 521 | const path = ensureByte(rlp.encode(receiptProof.path).toString('hex')); 522 | const parentNodes = ensureByte(rlp.encode(receiptProof.parentNodes).toString('hex')); 523 | 524 | const checkpoint2 = txProof.verify(receiptProof, 5); 525 | const encodedLogs = rProof.encodeLogs(depositReceipt.logs); 526 | const encodedReceiptTest = rlp.encode([depositReceipt.status, depositReceipt.cumulativeGasUsed, 527 | depositReceipt.logsBloom, encodedLogs]); 528 | const encodedReceiptValue = rlp.encode(receiptProof.value); 529 | 530 | assert(encodedReceiptTest.equals(encodedReceiptValue) == true); 531 | let addrs = [encodedLogs[0][0], encodedLogs[1][0]]; 532 | let topics = [encodedLogs[0][1], encodedLogs[1][1]]; 533 | let data = [encodedLogs[0][2], encodedLogs[1][2]]; 534 | 535 | let logsCat = `0x${addrs[0].toString('hex')}${topics[0][0].toString('hex')}` 536 | logsCat += `${topics[0][1].toString('hex')}${topics[0][2].toString('hex')}` 537 | logsCat += `${data[0].toString('hex')}${addrs[1].toString('hex')}${topics[1][0].toString('hex')}` 538 | logsCat += `${topics[1][1].toString('hex')}${topics[1][2].toString('hex')}` 539 | logsCat += `${topics[1][3].toString('hex')}${data[1].toString('hex')}`; 540 | 541 | const proveReceipt = await BridgeA.proveReceipt( 542 | logsCat, 543 | depositReceipt.cumulativeGasUsed, 544 | depositReceipt.logsBloom, 545 | depositBlock.receiptsRoot, 546 | path, 547 | parentNodes, 548 | { from: wallets[2][0], gas: 500000 } 549 | ) 550 | 551 | console.log('proveReceipt gas usage:', proveReceipt.receipt.gasUsed); 552 | }); 553 | 554 | it('Should submit the required data and make the withdrawal', async () => { 555 | let hI; 556 | treeHeaders.forEach((header, i) => { 557 | if (header == depositHeader) { hI = i; } 558 | }) 559 | // This should always be greater than zero since the prior two withdrawal 560 | // calls were made after the deposit 561 | assert(hI > 0); 562 | const prevHeader = treeHeaders[hI - 1]; 563 | const proof = merkle.getProof(hI, tree); 564 | const proofStr = merkle.getProofStr(proof); 565 | const validProof = merkle.checkProof(depositHeader, proof, tree[tree.length - 1][0]) 566 | assert(validProof === true); 567 | const withdrawal = await BridgeA.withdraw( 568 | depositBlock.number, 569 | depositBlock.timestamp, 570 | prevHeader, 571 | RootStorageIndex, 572 | proofStr, 573 | { from: wallets[2][0], gas: 500000} 574 | ); 575 | 576 | 577 | assert(withdrawal.receipt.gasUsed < 500000); 578 | console.log('Withdrawal gas usage: ', withdrawal.receipt.gasUsed); 579 | }); 580 | 581 | }); 582 | 583 | }); 584 | --------------------------------------------------------------------------------