├── .gitignore ├── README.md ├── block.js ├── config.js ├── contracts ├── ArrayLib.sol ├── Migrations.sol ├── MinHeapLib.sol ├── PlasmaChainManager.sol └── RLP.sol ├── geth.js ├── main.js ├── merkle.js ├── migrations ├── 1_initial_migration.js └── 2_deploy_contracts.js ├── package-lock.json ├── package.json ├── test ├── TestMinHeapLib.sol ├── TestPlasmaManager.sol ├── integration.js └── plasma_chain_manager.js ├── transaction.js ├── truffle-config.js ├── truffle.js └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .idea 4 | build/ 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple plasma implementation 2 | 3 | Plasma consists of three major parts. 4 | 1. Plasma chain: A simple proof-of-authority chain where the actual transactions take place. 5 | 2. Plasma contract: A smart contract deployed on root chain which handles the deposits and withdrawals for the child chain (plasma chain). 6 | 3. Ethereum blockchain: The root chain which only records the block headers of the plasma chain. 7 | 8 | The complete cycle of interacting with plasma is made up of three stages. 9 | 10 | ### Deposit 11 | 12 | Participants deposit to the plasma contract on the root chain. Then the operator at the plasma chain will construct a deposit transaction according to the contract event when building a new block. 13 | 14 | ### Transact 15 | 16 | Participants could transact with each other on the plasma chain without notifying the root chain. Only when every block is created by the operator, it will submit the block header to the plasma contract on the root chain. 17 | 18 | ### Withdraw 19 | 20 | A withdrawal is initiated by calling the plasma contract. After creating a withdrawal, user needs to wait 7 days for other participants to challenge it. If anyone could prove the given withdrawal that has been spent later on the plasma chain, the withdrawal will be canceled. Otherwise, after 7 days and without any other withdrawals with higher priority, user could withdraw his funds back to the root chain. 21 | 22 | ## Prerequisite 23 | 24 | 1. [Truffle](http://truffleframework.com/): An Ethereum development framework which helps us compiling, deploying, and interacting with smart contract. 25 | 2. Testrpc: A test Ethereum RPC client for fast development. Here, we use [ganache-cli](https://github.com/trufflesuite/ganache-cli). If you prefer a GUI version, you could replace it with [ganache](http://truffleframework.com/ganache/). Note that you could also launch an Ethereum private chain (geth) to replace testrpc. 26 | 27 | ## Run 28 | 1. Install dependency 29 | ``` 30 | npm install 31 | ``` 32 | 2. Run ganache 33 | ``` 34 | ganache-cli 35 | ``` 36 | Testrpc will generate ten default accounts for us. For convenience, you could specify a HD wallet mnemonic to get fixed addresses. For example: 37 | ``` 38 | ganache-cli -m pink two example move shop length clean crop cheese tent strike field 39 | ``` 40 | The corresponding initial addresses are: 41 | ``` 42 | (0) 0x0bf5f0f213b0b752858e9352fd6081f5d730dc17 43 | (1) 0x87dbd8ab1bd9d4fce07db12743594a5f456435ff 44 | (2) 0x3b0ba3134ac12cc065d4dba498a60cba5ef16098 45 | (3) 0x6c7f749d0e21aa6478af8e7adc362a8bf76be826 46 | (4) 0x9a404f89ad853e592d7b48242b4745c17a8ee852 47 | (5) 0x34d5e94fc3e7ecac3859a03176cb57534f30b71c 48 | (6) 0x8c0e1d37680a03eeb4c85880ffe1edf61ffa76f4 49 | (7) 0x6017db4acdfed284da94485d715a0f758048ac0b 50 | (8) 0x239ddceb1d7cebf07435a52f569b86f310af9cad 51 | (9) 0x1166d1f78f44c79e8e3f0d74419940e521790d7c 52 | ``` 53 | 3. Compile contracts 54 | ``` 55 | truffle compile 56 | ``` 57 | 4. Deploy contracts 58 | ``` 59 | truffle migrate 60 | ``` 61 | If you need to deploy contracts on this testrpc again, don't forget to add the `--reset` argument. 62 | 63 | 5. Set the contract configuration (config.js). 64 | 1. After deploying contracts, fill in the `PlasmaChainManager` contract address. 65 | 2. Choose one of the initial addresses as the operator address, for example, `0x87dbd8ab1bd9d4fce07db12743594a5f456435ff`. 66 | 6. Run the plasma chain. 67 | ``` 68 | npm start 69 | ``` 70 | or 71 | ``` 72 | node main.js [options] 73 | ``` 74 | the available options are: 75 | 76 | |Option|Description| 77 | |---|---| 78 | |--port|Specify HTTP API port, default 3001| 79 | |--contract|Specify contract address, otherwise use value in config.js| 80 | |--operator|Specify operator address, otherwise use value in config.js| 81 | 82 | ## Testing 83 | 84 | 1. Run ganache 85 | ``` 86 | ganache-cli 87 | ``` 88 | 89 | 2. Run tests 90 | ``` 91 | truffle test 92 | ``` 93 | 94 | ## HTTP API 95 | ### Block related 96 | #### Get blockchain 97 | Get the whole blockchain. 98 | ##### Parameter 99 | None 100 | ##### Sample 101 | ``` 102 | curl http://localhost:3001/blocks 103 | ``` 104 | #### Mine blocks 105 | Miner mines a new block. 106 | ##### Parameter 107 | None 108 | ##### Sample 109 | ``` 110 | curl -X POST http://localhost:3001/mineBlock 111 | ``` 112 | 113 | ### Transaction related 114 | #### Create a transaction 115 | Create a transaction to other participants. User could specify at most two UTXOs to spend. Also note that the units used in field `amount` is ether. 116 | ##### Parameter 117 | |Name|Type|Required|Description| 118 | |---|---|---|---| 119 | |from|Address|Yes|Transfer funds from whom| 120 | |to|Address|Yes|Transfer funds to whom| 121 | |amount|Decimal|Yes|How much ether (in ether)| 122 | ##### Sample 123 | ``` 124 | curl -H "Content-type:application/json" --data '{"from": "0x6C7f749d0E21aA6478aF8e7Adc362a8bF76Be826", "to": "0x3B0bA3134Ac12Cc065d4dBa498a60cba5Ef16098", "amount": 2}' http://localhost:3001/transact 125 | ``` 126 | 127 | ### Deposit related 128 | #### Deposit 129 | Deposit funds to Plasma smart contract. 130 | ##### Parameter 131 | |Name|Type|Required|Description| 132 | |---|---|---|---| 133 | |address|Address|Yes|Deposit from whom| 134 | |amount|Integer|Yes|How much funds to deposit| 135 | ##### Sample 136 | ``` 137 | curl -H "Content-type:application/json" --data '{"address": "0x6C7f749d0E21aA6478aF8e7Adc362a8bF76Be826", "amount": 4}' http://localhost:3001/deposit 138 | ``` 139 | 140 | ### Withdrawal related 141 | #### Create withdrawal 142 | Create a new withdrawal. 143 | ##### Parameter 144 | |Name|Type|Required|Description| 145 | |---|---|---|---| 146 | |blkNum|Integer|Yes|The position of the UTXO user wants to withdraw| 147 | |txIndex|Integer|Yes|The position of the UTXO user wants to withdraw| 148 | |oIndex|Integer|Yes|The position of the UTXO user wants to withdraw| 149 | |from|Address|Yes|The owner of the UTXO| 150 | ##### Sample 151 | ``` 152 | curl -H "Content-type:application/json" --data '{"blkNum": 3, "txIndex": 1, "oIndex": 0, "from": "0x6C7f749d0E21aA6478aF8e7Adc362a8bF76Be826"}' http://localhost:3001/withdraw/create 153 | ``` 154 | #### Challenge withdrawal 155 | Create a withdrawal challenge. 156 | ##### Parameter 157 | |Name|Type|Required|Description| 158 | |---|---|---|---| 159 | |withdrawalId|Integer|Yes|The withdrawal ID user wants to challenge| 160 | |blkNum|Integer|Yes|The position of the UTXO user wants to challenge| 161 | |txIndex|Integer|Yes|The position of the UTXO user wants to challenge| 162 | |oIndex|Integer|Yes|The position of the UTXO user wants to challenge| 163 | |from|Address|Yes|The owner of the UTXO| 164 | ``` 165 | curl -H "Content-type:application/json" --data '{"withdrawalId": 4000000000, "blkNum": 4, "txIndex": 2, "oIndex": 1, "from": "0x6C7f749d0E21aA6478aF8e7Adc362a8bF76Be826"}' http://localhost:3001/withdraw/challenge 166 | ``` 167 | #### Finalize withdrawal 168 | Finalize withdrawals manually. 169 | ##### Parameter 170 | |Name|Type|Required|Description| 171 | |---|---|---|---| 172 | |from|Address|Yes|Who initiates the withdrawal finalization| 173 | ##### Sample 174 | ``` 175 | curl -H "Content-type:application/json" --data '{"from": "0x6C7f749d0E21aA6478aF8e7Adc362a8bF76Be826"}' http://localhost:3001/withdraw/finalize 176 | ``` 177 | -------------------------------------------------------------------------------- /block.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const crypto = require('crypto'); 4 | 5 | const tx = require("./transaction"); 6 | const utils = require("./utils"); 7 | 8 | const Merkle = require("./merkle"); 9 | 10 | class Block { 11 | constructor(blockNumber, previousHash, transactions) { 12 | let data = []; 13 | transactions.forEach(tx => data.push(tx.toString(true))); 14 | 15 | this.blockHeader = new BlockHeader(blockNumber, previousHash, data); 16 | this.transactions = transactions; 17 | } 18 | 19 | get hash() { 20 | return crypto.createHash('sha256').update(this.toString()).digest('hex'); 21 | } 22 | 23 | toString() { 24 | let txsHex = ""; 25 | this.transactions.forEach(tx => txsHex += tx); 26 | return this.blockHeader.toString(true) + txsHex; 27 | } 28 | 29 | printBlock() { 30 | return { 31 | 'blockNumber': this.blockHeader.blockNumber, 32 | 'previousHash': this.blockHeader.previousHash, 33 | 'merkleRoot': this.blockHeader.merkleRoot, 34 | 'signature': this.blockHeader.sigR + this.blockHeader.sigS + this.blockHeader.sigV, 35 | 'transactions': this.transactions.filter(tx => tx.length > 0) 36 | }; 37 | } 38 | } 39 | 40 | class BlockHeader { 41 | constructor(blockNumber, previousHash, data) { 42 | this.blockNumber = blockNumber; // 32 bytes 43 | this.previousHash = previousHash; // 32 bytes 44 | if (blockNumber == 0) { 45 | this.merkle = null; 46 | this.merkleRoot = ""; 47 | } else { 48 | this.merkle = new Merkle(data); 49 | this.merkle.makeTree(); 50 | this.merkleRoot = utils.bufferToHex(this.merkle.getRoot(), false); // 32 bytes 51 | } 52 | this.sigR = ''; // 32 bytes 53 | this.sigS = ''; // 32 bytes 54 | this.sigV = ''; // 1 byte 55 | } 56 | 57 | setSignature(signature) { 58 | let sig = utils.removeHexPrefix(signature); 59 | let sigR = sig.substring(0, 64); 60 | let sigS = sig.substring(64, 128); 61 | let sigV = parseInt(sig.substring(128, 130), 16); 62 | if (sigV < 27) { 63 | sigV += 27; 64 | } 65 | this.sigR = sigR; 66 | this.sigS = sigS; 67 | this.sigV = sigV.toString(16).padStart(2, "0"); 68 | } 69 | 70 | toString(includingSig) { 71 | let blkNumHexString = this.blockNumber.toString(16).padStart(64, "0"); 72 | let rawBlockHeader = blkNumHexString + this.previousHash + this.merkleRoot; 73 | if (includingSig) { 74 | rawBlockHeader += this.sigR + this.sigS + this.sigV; 75 | } 76 | return rawBlockHeader; 77 | } 78 | } 79 | 80 | const getGenesisBlock = () => { 81 | // Create a hard coded genesis block. 82 | return new Block(0, '46182d20ccd7006058f3e801a1ff3de78b740b557bba686ced70f8e3d8a009a6', []); 83 | }; 84 | 85 | let blockchain = [getGenesisBlock()]; 86 | 87 | const generateNextBlock = async (geth) => { 88 | let previousBlock = getLatestBlock(); 89 | let previousHash = previousBlock.hash; 90 | let nextIndex = previousBlock.blockHeader.blockNumber + 1; 91 | 92 | // Query contract past event for deposits / withdrawals and collect transactions. 93 | let deposits = await geth.getDeposits(nextIndex - 1); 94 | let withdrawals = await geth.getWithdrawals(nextIndex - 1); 95 | let transactions = await tx.collectTransactions(nextIndex, deposits, withdrawals); 96 | let newBlock = new Block(nextIndex, previousHash, transactions); 97 | 98 | // Operator signs the new block. 99 | let messageToSign = utils.addHexPrefix(newBlock.blockHeader.toString(false)); 100 | let signature = await geth.signBlock(messageToSign); 101 | newBlock.blockHeader.setSignature(signature); 102 | 103 | // Submit the block header to plasma contract. 104 | let hexPrefixHeader = utils.addHexPrefix(newBlock.blockHeader.toString(true)); 105 | await geth.submitBlockHeader(hexPrefixHeader); 106 | 107 | // Add the new block to blockchain. 108 | console.log('New block added.'); 109 | console.log(newBlock.printBlock()); 110 | blockchain.push(newBlock); 111 | 112 | return newBlock; 113 | }; 114 | 115 | const getTransactionProofInBlock = (blockNumber, txIndex) => { 116 | let block = getBlock(blockNumber); 117 | let tx = utils.addHexPrefix(block.transactions[txIndex]); 118 | let proof = utils.bufferToHex(Buffer.concat(block.blockHeader.merkle.getProof(txIndex)), true); 119 | return { 120 | root: block.blockHeader.merkleRoot, 121 | tx: tx, 122 | proof: proof 123 | }; 124 | }; 125 | 126 | const getLatestBlock = () => blockchain[blockchain.length - 1]; 127 | const getBlocks = () => blockchain; 128 | const getBlock = (index) => blockchain[index]; 129 | 130 | module.exports = {getLatestBlock, getBlocks, generateNextBlock, 131 | getTransactionProofInBlock}; 132 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | get plasmaContractAddress() { 3 | return ""; 4 | }, 5 | get plasmaOperatorAddress() { 6 | return ""; 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /contracts/ArrayLib.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.19; 2 | 3 | library ArrayLib { 4 | function remove(uint256[] storage _array, uint256 _value) 5 | internal 6 | returns (bool success) 7 | { 8 | int256 index = indexOf(_array, _value); 9 | if (index == -1) { 10 | return false; 11 | } 12 | uint256 lastElement = _array[_array.length - 1]; 13 | _array[uint256(index)] = lastElement; 14 | 15 | delete _array[_array.length - 1]; 16 | _array.length -= 1; 17 | return true; 18 | } 19 | 20 | function indexOf(uint256[] _array, uint256 _value) 21 | internal 22 | pure 23 | returns(int256 index) 24 | { 25 | for (uint256 i = 0; i < _array.length; i++) { 26 | if (_array[i] == _value) { 27 | return int256(i); 28 | } 29 | } 30 | return -1; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /contracts/MinHeapLib.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.19; 2 | 3 | library MinHeapLib { 4 | struct Heap { 5 | uint256[] data; 6 | } 7 | 8 | function add(Heap storage _heap, uint256 value) internal { 9 | uint index = _heap.data.length; 10 | _heap.data.length += 1; 11 | _heap.data[index] = value; 12 | 13 | // Fix the min heap if it is violated. 14 | while (index != 0 && _heap.data[index] < _heap.data[(index - 1) / 2]) { 15 | uint256 temp = _heap.data[index]; 16 | _heap.data[index] = _heap.data[(index - 1) / 2]; 17 | _heap.data[(index - 1) / 2] = temp; 18 | index = (index - 1) / 2; 19 | } 20 | } 21 | 22 | function peek(Heap storage _heap) view internal returns (uint256 value) { 23 | require(_heap.data.length > 0); 24 | return _heap.data[0]; 25 | } 26 | 27 | function pop(Heap storage _heap) internal returns (uint256 value) { 28 | require(_heap.data.length > 0); 29 | uint256 root = _heap.data[0]; 30 | _heap.data[0] = _heap.data[_heap.data.length - 1]; 31 | _heap.data.length -= 1; 32 | heapify(_heap, 0); 33 | return root; 34 | } 35 | 36 | function heapify(Heap storage _heap, uint i) internal { 37 | uint left = 2 * i + 1; 38 | uint right = 2 * i + 2; 39 | uint smallest = i; 40 | if (left < _heap.data.length && _heap.data[left] < _heap.data[i]) { 41 | smallest = left; 42 | } 43 | if (right < _heap.data.length && _heap.data[right] < _heap.data[smallest]) { 44 | smallest = right; 45 | } 46 | if (smallest != i) { 47 | uint256 temp = _heap.data[i]; 48 | _heap.data[i] = _heap.data[smallest]; 49 | _heap.data[smallest] = temp; 50 | heapify(_heap, smallest); 51 | } 52 | } 53 | 54 | function isEmpty(Heap storage _heap) view internal returns (bool empty) { 55 | return _heap.data.length == 0; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /contracts/PlasmaChainManager.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.19; 2 | import './RLP.sol'; 3 | import './MinHeapLib.sol'; 4 | import './ArrayLib.sol'; 5 | 6 | contract PlasmaChainManager { 7 | using ArrayLib for uint256[]; 8 | using RLP for bytes; 9 | using RLP for RLP.RLPItem; 10 | using RLP for RLP.Iterator; 11 | using MinHeapLib for MinHeapLib.Heap; 12 | 13 | bytes constant PersonalMessagePrefixBytes = "\x19Ethereum Signed Message:\n96"; 14 | uint32 constant blockHeaderLength = 161; 15 | 16 | uint256 exitAgeOffset; 17 | uint256 exitWaitOffset; 18 | 19 | struct BlockHeader { 20 | uint256 blockNumber; 21 | bytes32 previousHash; 22 | bytes32 merkleRoot; 23 | bytes32 r; 24 | bytes32 s; 25 | uint8 v; 26 | uint256 timeSubmitted; 27 | } 28 | 29 | struct DepositRecord { 30 | uint256 blockNumber; 31 | uint256 txIndex; 32 | address depositor; 33 | uint256 amount; 34 | uint256 timeCreated; 35 | } 36 | 37 | struct WithdrawRecord { 38 | uint256 blockNumber; 39 | uint256 txIndex; 40 | uint256 oIndex; 41 | address beneficiary; 42 | uint256 amount; 43 | uint256 priority; 44 | } 45 | 46 | address public owner; 47 | uint256 public lastBlockNumber; 48 | uint256 public txCounter; 49 | mapping(uint256 => BlockHeader) public headers; 50 | mapping(address => DepositRecord[]) public depositRecords; 51 | mapping(uint256 => uint256[]) public withdrawalIds; 52 | mapping(uint256 => WithdrawRecord) public withdrawRecords; 53 | MinHeapLib.Heap exits; 54 | 55 | function PlasmaChainManager(uint256 exitAge, uint256 exitWait) public { 56 | owner = msg.sender; 57 | lastBlockNumber = 0; 58 | txCounter = 0; 59 | exitAgeOffset = exitAge; 60 | exitWaitOffset = exitWait; 61 | } 62 | 63 | event HeaderSubmittedEvent(address signer, uint32 blockNumber); 64 | 65 | function submitBlockHeader(bytes header) public returns (bool success) { 66 | require(header.length == blockHeaderLength); 67 | 68 | bytes32 blockNumber; 69 | bytes32 previousHash; 70 | bytes32 merkleRoot; 71 | bytes32 sigR; 72 | bytes32 sigS; 73 | bytes1 sigV; 74 | assembly { 75 | let data := add(header, 0x20) 76 | blockNumber := mload(data) 77 | previousHash := mload(add(data, 32)) 78 | merkleRoot := mload(add(data, 64)) 79 | sigR := mload(add(data, 96)) 80 | sigS := mload(add(data, 128)) 81 | sigV := mload(add(data, 160)) 82 | if lt(sigV, 27) { sigV := add(sigV, 27) } 83 | } 84 | 85 | // Check the block number. 86 | require(uint8(blockNumber) == lastBlockNumber + 1); 87 | 88 | // Check the signature. 89 | bytes32 blockHash = keccak256(PersonalMessagePrefixBytes, blockNumber, 90 | previousHash, merkleRoot); 91 | address signer = ecrecover(blockHash, uint8(sigV), sigR, sigS); 92 | require(msg.sender == signer); 93 | 94 | // Append the new header. 95 | BlockHeader memory newHeader = BlockHeader({ 96 | blockNumber: uint8(blockNumber), 97 | previousHash: previousHash, 98 | merkleRoot: merkleRoot, 99 | r: sigR, 100 | s: sigS, 101 | v: uint8(sigV), 102 | timeSubmitted: now 103 | }); 104 | headers[uint8(blockNumber)] = newHeader; 105 | 106 | // Increment the block number by 1 and reset the transaction counter. 107 | lastBlockNumber += 1; 108 | txCounter = 0; 109 | 110 | HeaderSubmittedEvent(signer, uint8(blockNumber)); 111 | return true; 112 | } 113 | 114 | event DepositEvent(address from, uint256 amount, 115 | uint256 indexed blockNumber, uint256 txIndex); 116 | 117 | function deposit() payable public returns (bool success) { 118 | DepositRecord memory newDeposit = DepositRecord({ 119 | blockNumber: lastBlockNumber, 120 | txIndex: txCounter, 121 | depositor: msg.sender, 122 | amount: msg.value, 123 | timeCreated: now 124 | }); 125 | depositRecords[msg.sender].push(newDeposit); 126 | txCounter += 1; 127 | DepositEvent(msg.sender, msg.value, newDeposit.blockNumber, 128 | newDeposit.txIndex); 129 | return true; 130 | } 131 | 132 | event WithdrawalStartedEvent(uint256 withdrawalId); 133 | 134 | function startWithdrawal( 135 | uint256 blockNumber, 136 | uint256 txIndex, 137 | uint256 oIndex, 138 | bytes targetTx, 139 | bytes proof 140 | ) 141 | public 142 | returns (uint256 withdrawalId) 143 | { 144 | BlockHeader memory header = headers[blockNumber]; 145 | require(header.blockNumber > 0); 146 | 147 | var txList = targetTx.toRLPItem().toList(); 148 | require(txList.length == 13); 149 | 150 | // Check if the target transaction is in the block. 151 | require(isValidProof(header.merkleRoot, targetTx, proof)); 152 | 153 | // Check if the transaction owner is the sender. 154 | address txOwner = txList[6 + 2 * oIndex].toAddress(); 155 | require(txOwner == msg.sender); 156 | 157 | // Generate a new withdrawal ID. 158 | uint256 priority = max(header.timeSubmitted, now - exitAgeOffset); 159 | withdrawalId = blockNumber * 1000000 + txIndex * 1000 + oIndex; 160 | WithdrawRecord storage record = withdrawRecords[withdrawalId]; 161 | require(record.blockNumber == 0); 162 | 163 | // Construct a new withdrawal. 164 | record.blockNumber = blockNumber; 165 | record.txIndex = txIndex; 166 | record.oIndex = oIndex; 167 | record.beneficiary = txOwner; 168 | record.amount = txList[7 + 2 * oIndex].toUint(); 169 | record.priority = priority; 170 | 171 | exits.add(priority); 172 | withdrawalIds[priority].push(withdrawalId); 173 | 174 | WithdrawalStartedEvent(withdrawalId); 175 | return withdrawalId; 176 | } 177 | 178 | event WithdrawalChallengedEvent(uint256 withdrawalId); 179 | 180 | function challengeWithdrawal( 181 | uint256 withdrawalId, 182 | uint256 blockNumber, 183 | uint256 txIndex, 184 | uint256 oIndex, 185 | bytes targetTx, 186 | bytes proof 187 | ) 188 | public 189 | returns (bool success) 190 | { 191 | BlockHeader memory header = headers[blockNumber]; 192 | require(header.blockNumber > 0); 193 | 194 | var txList = targetTx.toRLPItem().toList(); 195 | require(txList.length == 13); 196 | 197 | // Check if the transaction is in the block. 198 | require(isValidProof(header.merkleRoot, targetTx, proof)); 199 | 200 | // Check if the withdrawal exists. 201 | WithdrawRecord memory record = withdrawRecords[withdrawalId]; 202 | require(record.blockNumber > 0); 203 | 204 | // The transaction spends the given withdrawal on plasma chain. 205 | if (isWithdrawalSpent(targetTx, record)) { 206 | withdrawalIds[record.priority].remove(withdrawalId); 207 | delete withdrawRecords[withdrawalId]; 208 | 209 | WithdrawalChallengedEvent(withdrawalId); 210 | return true; 211 | } 212 | 213 | return false; 214 | } 215 | 216 | event WithdrawalCompleteEvent(uint256 indexed blockNumber, 217 | uint256 exitBlockNumber, uint256 exitTxIndex, uint256 exitOIndex); 218 | 219 | function finalizeWithdrawal() public returns (bool success) { 220 | while (!exits.isEmpty() && now > exits.peek() + exitWaitOffset) { 221 | uint256 priority = exits.pop(); 222 | for (uint256 i = 0; i < withdrawalIds[priority].length; i++) { 223 | uint256 index = withdrawalIds[priority][i]; 224 | WithdrawRecord memory record = withdrawRecords[index]; 225 | record.beneficiary.transfer(record.amount); 226 | 227 | WithdrawalCompleteEvent(lastBlockNumber, record.blockNumber, 228 | record.txIndex, record.oIndex); 229 | delete withdrawRecords[index]; 230 | } 231 | delete withdrawalIds[priority]; 232 | } 233 | return true; 234 | } 235 | 236 | function isValidProof(bytes32 root, bytes target, bytes proof) 237 | pure 238 | internal 239 | returns (bool valid) 240 | { 241 | bytes32 hash = keccak256(target); 242 | for (uint i = 32; i < proof.length; i += 33) { 243 | bytes1 flag; 244 | bytes32 sibling; 245 | assembly { 246 | flag := mload(add(proof, i)) 247 | sibling := mload(add(add(proof, i), 1)) 248 | } 249 | if (flag == 0) { 250 | hash = keccak256(sibling, hash); 251 | } else if (flag == 1) { 252 | hash = keccak256(hash, sibling); 253 | } 254 | } 255 | return hash == root; 256 | } 257 | 258 | function max(uint256 a, uint256 b) pure internal returns (uint256 result) { 259 | return (a > b) ? a : b; 260 | } 261 | 262 | function isWithdrawalSpent(bytes targetTx, WithdrawRecord record) 263 | view 264 | internal 265 | returns (bool spent) 266 | { 267 | var txList = targetTx.toRLPItem().toList(); 268 | require(txList.length == 13); 269 | 270 | // Check two inputs individually if it spent the given withdrawal. 271 | for (uint256 i = 0; i < 2; i++) { 272 | if (!txList[3 * i].isEmpty()) { 273 | uint256 blockNumber = txList[3 * i].toUint(); 274 | // RLP will encode integer 0 to 0x80 just like empty content... 275 | uint256 txIndex = txList[3 * i + 1].isEmpty() ? 0 : txList[3 * i + 1].toUint(); 276 | uint256 oIndex = txList[3 * i + 2].isEmpty() ? 0 : txList[3 * i + 2].toUint(); 277 | if (record.blockNumber == blockNumber && 278 | record.txIndex == txIndex && 279 | record.oIndex == oIndex) { 280 | return true; 281 | } 282 | } 283 | } 284 | return false; 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /contracts/RLP.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.19; 2 | 3 | /** 4 | * @title RLPReader 5 | * 6 | * RLPReader is used to read and parse RLP encoded data in memory. 7 | * 8 | * @author Andreas Olofsson (androlo1980@gmail.com) 9 | */ 10 | library RLP { 11 | 12 | uint constant DATA_SHORT_START = 0x80; 13 | uint constant DATA_LONG_START = 0xB8; 14 | uint constant LIST_SHORT_START = 0xC0; 15 | uint constant LIST_LONG_START = 0xF8; 16 | 17 | uint constant DATA_LONG_OFFSET = 0xB7; 18 | uint constant LIST_LONG_OFFSET = 0xF7; 19 | 20 | 21 | struct RLPItem { 22 | uint _unsafe_memPtr; // Pointer to the RLP-encoded bytes. 23 | uint _unsafe_length; // Number of bytes. This is the full length of the string. 24 | } 25 | 26 | struct Iterator { 27 | RLPItem _unsafe_item; // Item that's being iterated over. 28 | uint _unsafe_nextPtr; // Position of the next item in the list. 29 | } 30 | 31 | /* Iterator */ 32 | 33 | function next(Iterator memory self) internal constant returns (RLPItem memory subItem) { 34 | if(hasNext(self)) { 35 | var ptr = self._unsafe_nextPtr; 36 | var itemLength = _itemLength(ptr); 37 | subItem._unsafe_memPtr = ptr; 38 | subItem._unsafe_length = itemLength; 39 | self._unsafe_nextPtr = ptr + itemLength; 40 | } 41 | else 42 | throw; 43 | } 44 | 45 | function next(Iterator memory self, bool strict) internal constant returns (RLPItem memory subItem) { 46 | subItem = next(self); 47 | if(strict && !_validate(subItem)) 48 | throw; 49 | return; 50 | } 51 | 52 | function hasNext(Iterator memory self) internal constant returns (bool) { 53 | var item = self._unsafe_item; 54 | return self._unsafe_nextPtr < item._unsafe_memPtr + item._unsafe_length; 55 | } 56 | 57 | /* RLPItem */ 58 | 59 | /// @dev Creates an RLPItem from an array of RLP encoded bytes. 60 | /// @param self The RLP encoded bytes. 61 | /// @return An RLPItem 62 | function toRLPItem(bytes memory self) internal constant returns (RLPItem memory) { 63 | uint len = self.length; 64 | if (len == 0) { 65 | return RLPItem(0, 0); 66 | } 67 | uint memPtr; 68 | assembly { 69 | memPtr := add(self, 0x20) 70 | } 71 | return RLPItem(memPtr, len); 72 | } 73 | 74 | /// @dev Creates an RLPItem from an array of RLP encoded bytes. 75 | /// @param self The RLP encoded bytes. 76 | /// @param strict Will throw if the data is not RLP encoded. 77 | /// @return An RLPItem 78 | function toRLPItem(bytes memory self, bool strict) internal constant returns (RLPItem memory) { 79 | var item = toRLPItem(self); 80 | if(strict) { 81 | uint len = self.length; 82 | if(_payloadOffset(item) > len) 83 | throw; 84 | if(_itemLength(item._unsafe_memPtr) != len) 85 | throw; 86 | if(!_validate(item)) 87 | throw; 88 | } 89 | return item; 90 | } 91 | 92 | /// @dev Check if the RLP item is null. 93 | /// @param self The RLP item. 94 | /// @return 'true' if the item is null. 95 | function isNull(RLPItem memory self) internal constant returns (bool ret) { 96 | return self._unsafe_length == 0; 97 | } 98 | 99 | /// @dev Check if the RLP item is a list. 100 | /// @param self The RLP item. 101 | /// @return 'true' if the item is a list. 102 | function isList(RLPItem memory self) internal constant returns (bool ret) { 103 | if (self._unsafe_length == 0) 104 | return false; 105 | uint memPtr = self._unsafe_memPtr; 106 | assembly { 107 | ret := iszero(lt(byte(0, mload(memPtr)), 0xC0)) 108 | } 109 | } 110 | 111 | /// @dev Check if the RLP item is data. 112 | /// @param self The RLP item. 113 | /// @return 'true' if the item is data. 114 | function isData(RLPItem memory self) internal constant returns (bool ret) { 115 | if (self._unsafe_length == 0) 116 | return false; 117 | uint memPtr = self._unsafe_memPtr; 118 | assembly { 119 | ret := lt(byte(0, mload(memPtr)), 0xC0) 120 | } 121 | } 122 | 123 | /// @dev Check if the RLP item is empty (string or list). 124 | /// @param self The RLP item. 125 | /// @return 'true' if the item is null. 126 | function isEmpty(RLPItem memory self) internal constant returns (bool ret) { 127 | if(isNull(self)) 128 | return false; 129 | uint b0; 130 | uint memPtr = self._unsafe_memPtr; 131 | assembly { 132 | b0 := byte(0, mload(memPtr)) 133 | } 134 | return (b0 == DATA_SHORT_START || b0 == LIST_SHORT_START); 135 | } 136 | 137 | /// @dev Get the number of items in an RLP encoded list. 138 | /// @param self The RLP item. 139 | /// @return The number of items. 140 | function items(RLPItem memory self) internal constant returns (uint) { 141 | if (!isList(self)) 142 | return 0; 143 | uint b0; 144 | uint memPtr = self._unsafe_memPtr; 145 | assembly { 146 | b0 := byte(0, mload(memPtr)) 147 | } 148 | uint pos = memPtr + _payloadOffset(self); 149 | uint last = memPtr + self._unsafe_length - 1; 150 | uint itms; 151 | while(pos <= last) { 152 | pos += _itemLength(pos); 153 | itms++; 154 | } 155 | return itms; 156 | } 157 | 158 | /// @dev Create an iterator. 159 | /// @param self The RLP item. 160 | /// @return An 'Iterator' over the item. 161 | function iterator(RLPItem memory self) internal constant returns (Iterator memory it) { 162 | if (!isList(self)) 163 | throw; 164 | uint ptr = self._unsafe_memPtr + _payloadOffset(self); 165 | it._unsafe_item = self; 166 | it._unsafe_nextPtr = ptr; 167 | } 168 | 169 | /// @dev Return the RLP encoded bytes. 170 | /// @param self The RLPItem. 171 | /// @return The bytes. 172 | function toBytes(RLPItem memory self) internal constant returns (bytes memory bts) { 173 | var len = self._unsafe_length; 174 | if (len == 0) 175 | return; 176 | bts = new bytes(len); 177 | _copyToBytes(self._unsafe_memPtr, bts, len); 178 | } 179 | 180 | /// @dev Decode an RLPItem into bytes. This will not work if the 181 | /// RLPItem is a list. 182 | /// @param self The RLPItem. 183 | /// @return The decoded string. 184 | function toData(RLPItem memory self) internal constant returns (bytes memory bts) { 185 | if(!isData(self)) 186 | throw; 187 | var (rStartPos, len) = _decode(self); 188 | bts = new bytes(len); 189 | _copyToBytes(rStartPos, bts, len); 190 | } 191 | 192 | /// @dev Get the list of sub-items from an RLP encoded list. 193 | /// Warning: This is inefficient, as it requires that the list is read twice. 194 | /// @param self The RLP item. 195 | /// @return Array of RLPItems. 196 | function toList(RLPItem memory self) internal constant returns (RLPItem[] memory list) { 197 | if(!isList(self)) 198 | throw; 199 | var numItems = items(self); 200 | list = new RLPItem[](numItems); 201 | var it = iterator(self); 202 | uint idx; 203 | while(hasNext(it)) { 204 | list[idx] = next(it); 205 | idx++; 206 | } 207 | } 208 | 209 | /// @dev Decode an RLPItem into an ascii string. This will not work if the 210 | /// RLPItem is a list. 211 | /// @param self The RLPItem. 212 | /// @return The decoded string. 213 | function toAscii(RLPItem memory self) internal constant returns (string memory str) { 214 | if(!isData(self)) 215 | throw; 216 | var (rStartPos, len) = _decode(self); 217 | bytes memory bts = new bytes(len); 218 | _copyToBytes(rStartPos, bts, len); 219 | str = string(bts); 220 | } 221 | 222 | /// @dev Decode an RLPItem into a uint. This will not work if the 223 | /// RLPItem is a list. 224 | /// @param self The RLPItem. 225 | /// @return The decoded string. 226 | function toUint(RLPItem memory self) internal constant returns (uint data) { 227 | if(!isData(self)) 228 | throw; 229 | var (rStartPos, len) = _decode(self); 230 | if (len > 32 || len == 0) 231 | throw; 232 | assembly { 233 | data := div(mload(rStartPos), exp(256, sub(32, len))) 234 | } 235 | } 236 | 237 | /// @dev Decode an RLPItem into a boolean. This will not work if the 238 | /// RLPItem is a list. 239 | /// @param self The RLPItem. 240 | /// @return The decoded string. 241 | function toBool(RLPItem memory self) internal constant returns (bool data) { 242 | if(!isData(self)) 243 | throw; 244 | var (rStartPos, len) = _decode(self); 245 | if (len != 1) 246 | throw; 247 | uint temp; 248 | assembly { 249 | temp := byte(0, mload(rStartPos)) 250 | } 251 | if (temp > 1) 252 | throw; 253 | return temp == 1 ? true : false; 254 | } 255 | 256 | /// @dev Decode an RLPItem into a byte. This will not work if the 257 | /// RLPItem is a list. 258 | /// @param self The RLPItem. 259 | /// @return The decoded string. 260 | function toByte(RLPItem memory self) internal constant returns (byte data) { 261 | if(!isData(self)) 262 | throw; 263 | var (rStartPos, len) = _decode(self); 264 | if (len != 1) 265 | throw; 266 | uint temp; 267 | assembly { 268 | temp := byte(0, mload(rStartPos)) 269 | } 270 | return byte(temp); 271 | } 272 | 273 | /// @dev Decode an RLPItem into an int. This will not work if the 274 | /// RLPItem is a list. 275 | /// @param self The RLPItem. 276 | /// @return The decoded string. 277 | function toInt(RLPItem memory self) internal constant returns (int data) { 278 | return int(toUint(self)); 279 | } 280 | 281 | /// @dev Decode an RLPItem into a bytes32. This will not work if the 282 | /// RLPItem is a list. 283 | /// @param self The RLPItem. 284 | /// @return The decoded string. 285 | function toBytes32(RLPItem memory self) internal constant returns (bytes32 data) { 286 | return bytes32(toUint(self)); 287 | } 288 | 289 | /// @dev Decode an RLPItem into an address. This will not work if the 290 | /// RLPItem is a list. 291 | /// @param self The RLPItem. 292 | /// @return The decoded string. 293 | function toAddress(RLPItem memory self) internal constant returns (address data) { 294 | if(!isData(self)) 295 | throw; 296 | var (rStartPos, len) = _decode(self); 297 | if (len != 20) 298 | throw; 299 | assembly { 300 | data := div(mload(rStartPos), exp(256, 12)) 301 | } 302 | } 303 | 304 | // Get the payload offset. 305 | function _payloadOffset(RLPItem memory self) private constant returns (uint) { 306 | if(self._unsafe_length == 0) 307 | return 0; 308 | uint b0; 309 | uint memPtr = self._unsafe_memPtr; 310 | assembly { 311 | b0 := byte(0, mload(memPtr)) 312 | } 313 | if(b0 < DATA_SHORT_START) 314 | return 0; 315 | if(b0 < DATA_LONG_START || (b0 >= LIST_SHORT_START && b0 < LIST_LONG_START)) 316 | return 1; 317 | if(b0 < LIST_SHORT_START) 318 | return b0 - DATA_LONG_OFFSET + 1; 319 | return b0 - LIST_LONG_OFFSET + 1; 320 | } 321 | 322 | // Get the full length of an RLP item. 323 | function _itemLength(uint memPtr) private constant returns (uint len) { 324 | uint b0; 325 | assembly { 326 | b0 := byte(0, mload(memPtr)) 327 | } 328 | if (b0 < DATA_SHORT_START) 329 | len = 1; 330 | else if (b0 < DATA_LONG_START) 331 | len = b0 - DATA_SHORT_START + 1; 332 | else if (b0 < LIST_SHORT_START) { 333 | assembly { 334 | let bLen := sub(b0, 0xB7) // bytes length (DATA_LONG_OFFSET) 335 | let dLen := div(mload(add(memPtr, 1)), exp(256, sub(32, bLen))) // data length 336 | len := add(1, add(bLen, dLen)) // total length 337 | } 338 | } 339 | else if (b0 < LIST_LONG_START) 340 | len = b0 - LIST_SHORT_START + 1; 341 | else { 342 | assembly { 343 | let bLen := sub(b0, 0xF7) // bytes length (LIST_LONG_OFFSET) 344 | let dLen := div(mload(add(memPtr, 1)), exp(256, sub(32, bLen))) // data length 345 | len := add(1, add(bLen, dLen)) // total length 346 | } 347 | } 348 | } 349 | 350 | // Get start position and length of the data. 351 | function _decode(RLPItem memory self) private constant returns (uint memPtr, uint len) { 352 | if(!isData(self)) 353 | throw; 354 | uint b0; 355 | uint start = self._unsafe_memPtr; 356 | assembly { 357 | b0 := byte(0, mload(start)) 358 | } 359 | if (b0 < DATA_SHORT_START) { 360 | memPtr = start; 361 | len = 1; 362 | return; 363 | } 364 | if (b0 < DATA_LONG_START) { 365 | len = self._unsafe_length - 1; 366 | memPtr = start + 1; 367 | } else { 368 | uint bLen; 369 | assembly { 370 | bLen := sub(b0, 0xB7) // DATA_LONG_OFFSET 371 | } 372 | len = self._unsafe_length - 1 - bLen; 373 | memPtr = start + bLen + 1; 374 | } 375 | return; 376 | } 377 | 378 | // Assumes that enough memory has been allocated to store in target. 379 | function _copyToBytes(uint btsPtr, bytes memory tgt, uint btsLen) private constant { 380 | // Exploiting the fact that 'tgt' was the last thing to be allocated, 381 | // we can write entire words, and just overwrite any excess. 382 | assembly { 383 | { 384 | let i := 0 // Start at arr + 0x20 385 | let words := div(add(btsLen, 31), 32) 386 | let rOffset := btsPtr 387 | let wOffset := add(tgt, 0x20) 388 | tag_loop: 389 | jumpi(end, eq(i, words)) 390 | { 391 | let offset := mul(i, 0x20) 392 | mstore(add(wOffset, offset), mload(add(rOffset, offset))) 393 | i := add(i, 1) 394 | } 395 | jump(tag_loop) 396 | end: 397 | mstore(add(tgt, add(0x20, mload(tgt))), 0) 398 | } 399 | } 400 | } 401 | 402 | // Check that an RLP item is valid. 403 | function _validate(RLPItem memory self) private constant returns (bool ret) { 404 | // Check that RLP is well-formed. 405 | uint b0; 406 | uint b1; 407 | uint memPtr = self._unsafe_memPtr; 408 | assembly { 409 | b0 := byte(0, mload(memPtr)) 410 | b1 := byte(1, mload(memPtr)) 411 | } 412 | if(b0 == DATA_SHORT_START + 1 && b1 < DATA_SHORT_START) 413 | return false; 414 | return true; 415 | } 416 | } 417 | -------------------------------------------------------------------------------- /geth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Web3 = require("web3"); 4 | 5 | const utils = require("./utils"); 6 | const artifacts = require("./build/contracts/PlasmaChainManager.json"); 7 | 8 | const provider = new Web3.providers.HttpProvider('http://localhost:8545'); 9 | const web3 = new Web3(provider); 10 | 11 | var plasmaContract, plasmaOperator; 12 | 13 | const init = (contractAddress, operatorAddress) => { 14 | plasmaContract = new web3.eth.Contract(artifacts.abi, contractAddress, {gas: 1000000}); 15 | plasmaOperator = operatorAddress; 16 | } 17 | 18 | const submitBlockHeader = async (header) => { 19 | let result = await plasmaContract.methods.submitBlockHeader(header).send({ 20 | from: plasmaOperator, gas: 300000 21 | }); 22 | let ev = result.events.HeaderSubmittedEvent.returnValues; 23 | console.log(ev); 24 | }; 25 | 26 | const signBlock = async (message) => { 27 | return await web3.eth.sign(message, plasmaOperator); 28 | }; 29 | 30 | const signTransaction = async (message, address) => { 31 | return await web3.eth.sign(message, address); 32 | }; 33 | 34 | const isValidSignature = async (message, signature, address) => { 35 | const hash = await web3.eth.accounts.hashMessage(message); 36 | const signer = await web3.eth.accounts.recover(hash, signature); 37 | return utils.removeHexPrefix(address.toLowerCase()) == utils.removeHexPrefix(signer.toLowerCase()); 38 | }; 39 | 40 | const deposit = async (address, amount) => { 41 | amount = utils.etherToWei(amount); 42 | const result = await plasmaContract.methods.deposit().send({ 43 | from: address, value: amount, gas: 300000 44 | }); 45 | console.log(result); 46 | }; 47 | 48 | const getDeposits = async (blockNumber) => { 49 | let depositEvents = await plasmaContract.getPastEvents('DepositEvent', { 50 | filter: {blockNumber: blockNumber.toString()}, 51 | fromBlock: 0, 52 | toBlock: 'latest' 53 | }); 54 | 55 | let deposits = []; 56 | depositEvents.forEach(ev => deposits.push(ev.returnValues)); 57 | deposits.sort((d1, d2) => (d1.ctr - d2.ctr)); 58 | return deposits; 59 | } 60 | 61 | const startWithdrawal = async (blkNum, txIndex, oIndex, targetTx, proof, from) => { 62 | let result = await plasmaContract.methods.startWithdrawal(blkNum, txIndex, oIndex, targetTx, proof).send({ 63 | from: from, gas: 300000 64 | }); 65 | let ev = result.events.WithdrawalStartedEvent.returnValues; 66 | console.log(ev); 67 | return ev.withdrawalId; 68 | }; 69 | 70 | const challengeWithdrawal = async (withdrawalId, blkNum, txIndex, oIndex, targetTx, proof, from) => { 71 | let result = await plasmaContract.methods.challengeWithdrawal(withdrawalId, blkNum, txIndex, oIndex, targetTx, proof).send({ 72 | from: from, gas: 300000 73 | }); 74 | console.log(result); 75 | }; 76 | 77 | const finalizeWithdrawal = async (from) => { 78 | let result = await plasmaContract.methods.finalizeWithdrawal().send({ 79 | from: from, gas: 300000 80 | }); 81 | if (result.events.WithdrawalCompleteEvent) { 82 | console.log(result.events.WithdrawalCompleteEvent.returnValues); 83 | } 84 | }; 85 | 86 | const getWithdrawals = async (blockNumber) => { 87 | let withdrawalEvents = await plasmaContract.getPastEvents('WithdrawalCompleteEvent', { 88 | filter: {blockNumber: blockNumber.toString()}, 89 | fromBlock: 0, 90 | toBlock: 'latest' 91 | }); 92 | 93 | return withdrawalEvents.map(ev => ev.returnValues); 94 | }; 95 | 96 | module.exports = {init, signBlock, signTransaction, submitBlockHeader, deposit, 97 | getDeposits, isValidSignature, startWithdrawal, challengeWithdrawal, 98 | finalizeWithdrawal, getWithdrawals}; 99 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require("express"); 4 | const bodyParser = require('body-parser'); 5 | const minimist = require('minimist'); 6 | 7 | const block = require("./block"); 8 | const tx = require("./transaction"); 9 | const config = require("./config"); 10 | const geth = require("./geth"); 11 | 12 | const initHttpServer = (http_port) => { 13 | const app = express(); 14 | app.use(bodyParser.json()); 15 | 16 | // Block related 17 | app.get('/blocks', (req, res) => { 18 | res.send(JSON.stringify(block.getBlocks().map(b => b.printBlock()))); 19 | }); 20 | app.post('/mineBlock', async (req, res) => { 21 | try { 22 | const newBlock = await block.generateNextBlock(geth); 23 | res.send(JSON.stringify(newBlock.printBlock())); 24 | } catch (e) { 25 | res.send(JSON.stringify(e)); 26 | } 27 | }); 28 | 29 | // Transaction related 30 | app.post('/transact', async (req, res) => { 31 | try { 32 | const rawTx = await tx.createTransaction(req.body, geth); 33 | console.log('New transaction created: ' + JSON.stringify(rawTx)); 34 | res.send(JSON.stringify(rawTx.toString(true))); 35 | } catch (e) { 36 | res.send(JSON.stringify(e)); 37 | } 38 | }); 39 | 40 | // Deposit related 41 | app.post('/deposit', async (req, res) => { 42 | await geth.deposit(req.body.address, req.body.amount); 43 | res.send(); 44 | }); 45 | 46 | // Withdrawal related 47 | app.post('/withdraw/create', async (req, res) => { 48 | const p = block.getTransactionProofInBlock(req.body.blkNum, 49 | req.body.txIndex); 50 | const withdrawalId = await geth.startWithdrawal(req.body.blkNum, 51 | req.body.txIndex, req.body.oIndex, p.tx, p.proof, req.body.from); 52 | res.send(withdrawalId); 53 | }); 54 | app.post('/withdraw/challenge', async (req, res) => { 55 | const p = block.getTransactionProofInBlock(req.body.blkNum, 56 | req.body.txIndex); 57 | await geth.challengeWithdrawal(req.body.withdrawalId, req.body.blkNum, 58 | req.body.txIndex, req.body.oIndex, p.tx, p.proof, req.body.from); 59 | res.send(); 60 | }); 61 | app.post('/withdraw/finalize', async (req, res) => { 62 | await geth.finalizeWithdrawal(req.body.from); 63 | res.send(); 64 | }); 65 | 66 | // Debug function 67 | app.get('/utxo', (req, res) => { 68 | res.send(tx.getUTXO()); 69 | }); 70 | app.get('/pool', (req, res) => { 71 | res.send(tx.getPool()); 72 | }); 73 | 74 | app.listen(http_port, () => console.log('Listening http on port: ' + http_port)); 75 | }; 76 | 77 | const main = () => { 78 | const argv = minimist(process.argv.slice(2), { string: ["port", "contract", "operator"]}); 79 | const http_port = argv.port || 3001; 80 | const contract_address = argv.contract || config.plasmaContractAddress; 81 | const operator_address = argv.operator || config.plasmaOperatorAddress; 82 | 83 | geth.init(contract_address, operator_address); 84 | initHttpServer(http_port); 85 | }; 86 | 87 | main(); 88 | -------------------------------------------------------------------------------- /merkle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const createKeccakHash = require('keccak'); 4 | 5 | class Merkle { 6 | constructor(data) { 7 | this.isReady = false; 8 | this.leaves = data.map(str => this._hash(this._getBuffer(str))); 9 | this.levels = []; 10 | } 11 | 12 | makeTree() { 13 | this.isReady = false; 14 | this.levels.unshift(this.leaves); 15 | while (this.levels[0].length > 1) { 16 | this.levels.unshift(this._getNextLevel()); 17 | } 18 | this.isReady = true; 19 | } 20 | 21 | getRoot() { 22 | return this.isReady ? this.levels[0][0] : null; 23 | } 24 | 25 | getProof(index) { 26 | let proof = []; 27 | for (let i = this.levels.length - 1; i > 0; i--) { 28 | let isRightNode = index % 2; 29 | let siblingIndex = isRightNode ? (index - 1) : (index + 1); 30 | proof.push(new Buffer(isRightNode ? [0x00] : [0x01])); 31 | proof.push(this.levels[i][siblingIndex]); 32 | index = Math.floor(index / 2); 33 | } 34 | return proof; 35 | } 36 | 37 | _hash(value) { 38 | return createKeccakHash('keccak256').update(value).digest(); 39 | } 40 | 41 | _getBuffer(value) { 42 | return new Buffer(value, 'hex'); 43 | } 44 | 45 | _getNextLevel() { 46 | let nodes = []; 47 | for (let i = 0; i < this.levels[0].length - 1; i += 2) { 48 | let left = this.levels[0][i]; 49 | let right = this.levels[0][i + 1]; 50 | nodes.push(this._hash(Buffer.concat([left, right]))); 51 | } 52 | return nodes; 53 | } 54 | } 55 | 56 | module.exports = Merkle; 57 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | var Migrations = artifacts.require("./Migrations.sol"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | const PlasmaChainManager = artifacts.require("./PlasmaChainManager"); 2 | const minHeapLib = artifacts.require("./MinHeapLib"); 3 | const arrayLib = artifacts.require("./ArrayLib"); 4 | const RLP = artifacts.require("./RLP"); 5 | 6 | module.exports = function(deployer) { 7 | deployer.deploy(minHeapLib); 8 | deployer.deploy(arrayLib); 9 | deployer.deploy(RLP); 10 | deployer.link(minHeapLib, PlasmaChainManager); 11 | deployer.link(arrayLib, PlasmaChainManager); 12 | deployer.link(RLP, PlasmaChainManager); 13 | deployer.deploy(PlasmaChainManager, 7 * 86400, 14 * 86400); 14 | }; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plasma", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "start": "node main.js" 7 | }, 8 | "dependencies": { 9 | "body-parser": "^1.15.2", 10 | "express": "~4.11.1", 11 | "ganache-cli": "^6.0.3", 12 | "keccak": "^1.4.0", 13 | "minimist": "^1.2.0", 14 | "rlp": "^2.0.0", 15 | "truffle": "^4.0.6", 16 | "web3": "^1.0.0-beta.27" 17 | }, 18 | "engines": { 19 | "node": ">=9.8.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/TestMinHeapLib.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.18; 2 | 3 | import "truffle/Assert.sol"; 4 | import "truffle/DeployedAddresses.sol"; 5 | import "../contracts/MinHeapLib.sol"; 6 | 7 | contract TestMinHeapLib { 8 | using MinHeapLib for MinHeapLib.Heap; 9 | 10 | MinHeapLib.Heap heap; 11 | 12 | function initHeap() internal { 13 | heap.data.length = 0; 14 | } 15 | 16 | function testAdd() public { 17 | initHeap(); 18 | heap.add(5); 19 | heap.add(3); 20 | heap.add(4); 21 | heap.add(1); 22 | heap.add(2); 23 | Assert.equal(heap.data[0], uint(1), "top of heap data should be 1"); 24 | Assert.equal(heap.data.length, uint(5), "heap should have 5 elements"); 25 | } 26 | 27 | function testPeek() public { 28 | initHeap(); 29 | heap.data = [1, 2, 3]; 30 | 31 | Assert.equal(heap.peek(), uint(1), "heap.peek() should be 1"); 32 | } 33 | 34 | function testPop() public { 35 | initHeap(); 36 | heap.data = [1, 2, 3]; 37 | 38 | Assert.equal(heap.pop(), uint(1), "should pop 1"); 39 | Assert.equal(heap.pop(), uint(2), "should pop 2"); 40 | Assert.equal(heap.pop(), uint(3), "should pop 3"); 41 | Assert.isTrue(heap.isEmpty(), "heap should be empty"); 42 | } 43 | 44 | function testSimpleIntegration() public { 45 | initHeap(); 46 | uint TEST_NUM = 10; 47 | uint i; 48 | 49 | for(i = TEST_NUM ; i > 0 ; i--) { 50 | heap.add(i); 51 | Assert.equal(heap.peek(), i, "should have correct peek while adding"); 52 | } 53 | 54 | for(i = 1 ; i <= TEST_NUM ; i++) { 55 | Assert.equal(heap.pop(), i, "should pop correctly"); 56 | } 57 | 58 | Assert.isTrue(heap.isEmpty(), "heap should be empty"); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/TestPlasmaManager.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.18; 2 | 3 | import "truffle/Assert.sol"; 4 | import "truffle/DeployedAddresses.sol"; 5 | import "../contracts/PlasmaChainManager.sol"; 6 | 7 | contract TestPlasmaManager { 8 | function testTrue() public { 9 | Assert.equal(uint(1), uint(1), "plzzzz"); 10 | } 11 | 12 | function testContractInitState() public { 13 | PlasmaChainManager plasma = PlasmaChainManager(DeployedAddresses.PlasmaChainManager()); 14 | Assert.equal(plasma.lastBlockNumber(), uint(0), "lastBlockNumber should be initiated with 0"); 15 | Assert.equal(plasma.txCounter(), uint(0), "txCounter should be initiated with 0"); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/integration.js: -------------------------------------------------------------------------------- 1 | const http = require("http"); 2 | const spawn = require('child_process').spawn; 3 | 4 | const PlasmaChainManager = artifacts.require("./PlasmaChainManager"); 5 | 6 | var runServer = (contract, operator) => { 7 | return new Promise(function(resolve, reject) { 8 | var server = spawn('node', ['main.js', '--contract', contract, '--operator', operator]); 9 | 10 | server.stdout.on('data', (data) => { 11 | resolve(server); 12 | }); 13 | }); 14 | }; 15 | 16 | var request = (method, path, data) => { 17 | return new Promise(function(resolve, reject) { 18 | var options = { 19 | hostname: 'localhost', 20 | port: 3001, 21 | method: method, 22 | path: path, 23 | }; 24 | 25 | var req = http.request(options, (res) => { 26 | var buf = ''; 27 | res.on('data', (data) => { 28 | buf += data; 29 | }); 30 | res.on('end', () => { 31 | if (buf) { 32 | resolve(JSON.parse(buf)); 33 | } else { 34 | resolve(null); 35 | } 36 | }); 37 | }); 38 | 39 | if (data) { 40 | req.setHeader('Content-Type', 'application/json'); 41 | req.write(data); 42 | } 43 | 44 | req.end(); 45 | }); 46 | }; 47 | 48 | var sleep = (ms) => { 49 | return new Promise(function(resolve, reject) { 50 | setTimeout(resolve, ms); 51 | }); 52 | }; 53 | 54 | contract('PlasmaChainManager', function(accounts) { 55 | it("deposit", async () => { 56 | var plasmaChainManager = await PlasmaChainManager.new(100, 200); 57 | var contractAddress = plasmaChainManager.address; 58 | var operatorAddress = accounts[0]; 59 | var server = await runServer(contractAddress, operatorAddress); 60 | 61 | try { 62 | await request('POST', '/deposit', JSON.stringify({address: accounts[0], amount: 0.2})); 63 | await request('POST', '/mineBlock'); 64 | var utxo = await request('GET', '/utxo'); 65 | assert.equal(utxo[0].denom, 200000000000000000); 66 | } catch(e) { 67 | throw e; 68 | } finally { 69 | server.kill(); 70 | } 71 | }); 72 | 73 | it("transact", async () => { 74 | var plasmaChainManager = await PlasmaChainManager.new(100, 200); 75 | var contractAddress = plasmaChainManager.address; 76 | var operatorAddress = accounts[0]; 77 | var server = await runServer(contractAddress, operatorAddress); 78 | 79 | try { 80 | await request('POST', '/deposit', JSON.stringify({address: accounts[1], amount: 0.5})); 81 | await request('POST', '/mineBlock'); 82 | await request('POST', '/transact', JSON.stringify({from: accounts[1], to: accounts[2], amount: 0.3})); 83 | await request('POST', '/mineBlock'); 84 | var utxo = await request('GET', '/utxo'); 85 | 86 | for (i = 0; i < utxo.length; i++) { 87 | if (utxo[i].owner === accounts[1]) { 88 | assert.equal(utxo[i].denom, 190000000000000000); 89 | } else if (utxo[i].owner === accounts[2]) { 90 | assert.equal(utxo[i].denom, 300000000000000000); 91 | } 92 | } 93 | } catch(e) { 94 | throw e; 95 | } finally { 96 | server.kill(); 97 | } 98 | }); 99 | 100 | it("withdraw", async () => { 101 | var plasmaChainManager = await PlasmaChainManager.new(0, 0); 102 | var contractAddress = plasmaChainManager.address; 103 | var operatorAddress = accounts[0]; 104 | var server = await runServer(contractAddress, operatorAddress); 105 | 106 | try { 107 | await request('POST', '/deposit', JSON.stringify({address: accounts[1], amount: 0.5})); 108 | await request('POST', '/mineBlock'); 109 | var utxo = await request('GET', '/utxo'); 110 | assert.equal(utxo.length, 1); 111 | var withdrawId = await request('POST', '/withdraw/create', JSON.stringify({from: accounts[1], blkNum: utxo[0].blkNum, txIndex: utxo[0].txIndex, oIndex: utxo[0].oIndex})); 112 | await sleep(1000); 113 | await request('POST', '/withdraw/finalize', JSON.stringify({from: accounts[1]})); 114 | await request('POST', '/mineBlock'); 115 | var utxo2 = await request('GET', '/utxo'); 116 | assert.equal(utxo2.length, 0); 117 | } catch(e) { 118 | throw e; 119 | } finally { 120 | server.kill(); 121 | } 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /test/plasma_chain_manager.js: -------------------------------------------------------------------------------- 1 | const PlasmaChainManager = artifacts.require("./PlasmaChainManager"); 2 | 3 | contract('PlasmaChainManager', function(accounts) { 4 | it("should assert true", function(done) { 5 | var plasmaChainManager = PlasmaChainManager.deployed(); 6 | assert.isTrue(true); 7 | done(); 8 | }); 9 | 10 | it("should deposit successfully", async () => { 11 | const plasmaChainManager = await PlasmaChainManager.deployed(); 12 | const oldTxCounter = (await plasmaChainManager.txCounter()).toNumber(); 13 | 14 | await plasmaChainManager.deposit({value: web3.toWei(1, "ether"), from: accounts[0]}); 15 | const newTxCounter = (await plasmaChainManager.txCounter()).toNumber(); 16 | assert.equal(newTxCounter, oldTxCounter + 1); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /transaction.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const createKeccakHash = require('keccak'); 4 | const RLP = require('rlp'); 5 | 6 | const utils = require("./utils"); 7 | 8 | class Transaction { 9 | constructor(blkNum1, txIndex1, oIndex1, sig1, 10 | blkNum2, txIndex2, oIndex2, sig2, 11 | newOwner1, denom1, newOwner2, denom2, fee, type) { 12 | // first input 13 | this.blkNum1 = blkNum1; 14 | this.txIndex1 = txIndex1; 15 | this.oIndex1 = oIndex1; 16 | this.sig1 = sig1; 17 | 18 | // second input 19 | this.blkNum2 = blkNum2; 20 | this.txIndex2 = txIndex2; 21 | this.oIndex2 = oIndex2; 22 | this.sig2 = sig2; 23 | 24 | // outputs 25 | this.newOwner1 = newOwner1; 26 | this.denom1 = denom1; 27 | this.newOwner2 = newOwner2; 28 | this.denom2 = denom2; 29 | 30 | this.fee = fee; 31 | this.type = type; 32 | } 33 | 34 | encode(includingSig) { 35 | let data = [ 36 | this.blkNum1, this.txIndex1, this.oIndex1, 37 | this.blkNum2, this.txIndex2, this.oIndex2, 38 | this.newOwner1, this.denom1, this.newOwner2, this.denom2, this.fee 39 | ]; 40 | if (includingSig) { 41 | data.push(this.sig1); 42 | data.push(this.sig2); 43 | } 44 | return RLP.encode(data); 45 | } 46 | 47 | toString(includingSig) { 48 | return utils.bufferToHex(this.encode(includingSig), false); 49 | } 50 | 51 | setSignature(sig) { 52 | this.sig1 = sig; 53 | if (this.blkNum2 !== 0) { 54 | this.sig2 = sig; 55 | } 56 | } 57 | } 58 | 59 | class UTXO { 60 | constructor(blkNum, txIndex, oIndex, owner, denom) { 61 | this.blkNum = blkNum; 62 | this.txIndex = txIndex; 63 | this.oIndex = oIndex; 64 | this.owner = owner; 65 | this.denom = denom; 66 | } 67 | } 68 | 69 | const TxType = { 70 | NORMAL: 0, 71 | DEPOSIT: 1, 72 | WITHDRAW: 2, 73 | MERGE: 3 74 | }; 75 | 76 | let txPool = []; 77 | let utxo = []; 78 | 79 | const createDepositTransactions = (deposits) => { 80 | return deposits.map(deposit => { 81 | const owner = deposit.from; 82 | const amount = parseInt(deposit.amount); 83 | return new Transaction(0, 0, 0, 0, 0, 0, 0, 0, 84 | owner, amount, 0, 0, 0, TxType.DEPOSIT); 85 | }); 86 | }; 87 | 88 | const createWithdrawalTransactions = (withdrawals) => { 89 | return withdrawals.map(withdrawal => { 90 | const blkNum = parseInt(withdrawal.exitBlockNumber); 91 | const txIndex = parseInt(withdrawal.exitTxIndex); 92 | const oIndex = parseInt(withdrawal.exitOIndex); 93 | return new Transaction(blkNum, txIndex, oIndex, 0, 0, 0, 0, 0, 94 | 0, 0, 0, 0, 0, TxType.WITHDRAW); 95 | }); 96 | }; 97 | 98 | const createMergeTransaction = (owner) => { 99 | const indexes = getTwoUTXOsByAddress(owner); 100 | if (indexes[0] !== -1 && indexes[1] !== -1) { 101 | const utxoA = utxo[indexes[0]]; 102 | const utxoB = utxo[indexes[1]]; 103 | return new Transaction(utxoA.blkNum, utxoA.txIndex, utxoA.oIndex, 0, 104 | utxoB.blkNum, utxoB.txIndex, utxoB.oIndex, 0, 105 | owner, utxoA.denom + utxoB.denom, 0, 0, 0, 106 | TxType.MERGE); 107 | } else { 108 | return null; 109 | } 110 | }; 111 | 112 | const createTransaction = async (data, geth) => { 113 | let index = getUTXOByAddress(data.from); 114 | if (index === -1) { 115 | throw 'No asset found'; 116 | } 117 | let blkNum1 = utxo[index].blkNum; 118 | let txIndex1 = utxo[index].txIndex; 119 | let oIndex1 = utxo[index].oIndex; 120 | 121 | let newOwner1 = data.to; 122 | let denom1 = utils.etherToWei(data.amount); 123 | let fee = utils.etherToWei(0.01); // hard-coded fee to 0.01 124 | if (utxo[index].denom < denom1 + fee) { 125 | throw 'Insufficient funds'; 126 | } 127 | let remain = utxo[index].denom - denom1 - fee; 128 | let newOwner2 = (remain > 0) ? data.from : 0; 129 | let denom2 = remain; 130 | 131 | let tx = new Transaction( 132 | blkNum1, txIndex1, oIndex1, 0, 0, 0, 0, 0, 133 | newOwner1, denom1, newOwner2, denom2, fee, TxType.NORMAL); 134 | let signature = await geth.signTransaction(tx.toString(false), data.from); 135 | tx.setSignature(signature); 136 | 137 | if (await isValidTransaction(tx, geth)) { 138 | spendUTXO(tx); 139 | txPool.push(tx); 140 | } else { 141 | throw 'Invalid transaction'; 142 | } 143 | return tx; 144 | } 145 | 146 | const getUTXOByAddress = (owner, start = 0) => { 147 | for (let i = start; i < utxo.length; i++) { 148 | if (utxo[i].owner.toLowerCase() === owner.toLowerCase()) { 149 | return i; 150 | } 151 | } 152 | return -1; 153 | }; 154 | 155 | const getTwoUTXOsByAddress = (owner) => { 156 | let index1 = getUTXOByAddress(owner); 157 | let index2 = index1 !== -1 ? getUTXOByAddress(owner, index1 + 1) : -1; 158 | return [index1, index2]; 159 | }; 160 | 161 | const getUTXOByIndex = (blkNum, txIndex, oIndex) => { 162 | for (let i = 0; i < utxo.length; i++) { 163 | if (utxo[i].blkNum === blkNum && 164 | utxo[i].txIndex === txIndex && 165 | utxo[i].oIndex === oIndex) { 166 | return i; 167 | } 168 | } 169 | return -1; 170 | }; 171 | 172 | const isValidTransaction = async (tx, geth) => { 173 | if (tx.type !== TxType.NORMAL) { 174 | return true; 175 | } 176 | 177 | let denom = 0; 178 | if (tx.blkNum1 !== 0) { 179 | let message = tx.toString(false); 180 | let index = getUTXOByIndex(tx.blkNum1, tx.txIndex1, tx.oIndex1); 181 | if (index !== -1 && 182 | await geth.isValidSignature(message, tx.sig1, utxo[index].owner)) { 183 | denom += utxo[index].denom; 184 | } else { 185 | return false; 186 | } 187 | } 188 | if (tx.blkNum2 !== 0) { 189 | let message = tx.toString(false); 190 | let index = getUTXOByIndex(tx.blkNum2, tx.txIndex2, tx.oIndex2); 191 | if (index !== -1 || 192 | await geth.isValidSignature(message, tx.sig2, utxo[index].owner)) { 193 | denom += utxo[index].denom; 194 | } else { 195 | return false; 196 | } 197 | } 198 | return denom === tx.denom1 + tx.denom2 + tx.fee; 199 | }; 200 | 201 | const spendUTXO = (tx) => { 202 | if (tx.blkNum1 !== 0) { 203 | const index = getUTXOByIndex(tx.blkNum1, tx.txIndex1, tx.oIndex1); 204 | if (index !== -1) { 205 | utxo.splice(index, 1); 206 | } 207 | } 208 | if (tx.blkNum2 !== 0) { 209 | const index = getUTXOByIndex(tx.blkNum2, tx.txIndex2, tx.oIndex2); 210 | if (index !== -1) { 211 | utxo.splice(index, 1); 212 | } 213 | } 214 | }; 215 | 216 | const createUTXO = (blockNumber, tx, txIndex) => { 217 | if (tx.newOwner1 !== 0 && tx.denom1 !== 0) { 218 | utxo.push(new UTXO(blockNumber, txIndex, 0, tx.newOwner1, tx.denom1)); 219 | } 220 | if (tx.newOwner2 !== 0 && tx.denom2 !== 0) { 221 | utxo.push(new UTXO(blockNumber, txIndex, 1, tx.newOwner2, tx.denom2)); 222 | } 223 | }; 224 | 225 | const collectTransactions = async (blockNumber, deposits, withdrawals) => { 226 | const txs = []; 227 | 228 | if (deposits.length > 0) { 229 | console.log('Deposit transactions found.'); 230 | console.log(deposits); 231 | const depositTxs = await createDepositTransactions(deposits); 232 | for (let i = 0; i < depositTxs.length; i++) { 233 | const tx = depositTxs[i]; 234 | createUTXO(blockNumber, tx, txs.length); 235 | txs.push(tx.toString(true)); 236 | 237 | const mergeTx = await createMergeTransaction(tx.newOwner1); 238 | if (mergeTx !== null) { 239 | spendUTXO(mergeTx); 240 | createUTXO(blockNumber, mergeTx, txs.length); 241 | txs.push(mergeTx.toString(true)); 242 | } 243 | } 244 | } 245 | 246 | if (withdrawals.length > 0) { 247 | console.log('Withdrawals detected.'); 248 | console.log(withdrawals); 249 | const withdrawalTxs = await createWithdrawalTransactions(withdrawals); 250 | for (let i = 0; i < withdrawalTxs.length; i++) { 251 | const tx = withdrawalTxs[i]; 252 | spendUTXO(tx); 253 | txs.push(tx.toString(true)); 254 | } 255 | } 256 | 257 | for (let i = 0; i < txPool.length; i++) { 258 | const tx = txPool[i]; 259 | createUTXO(blockNumber, tx, txs.length); 260 | txs.push(tx.toString(true)); 261 | txPool.splice(i, 1); 262 | 263 | const mergeTx1 = await createMergeTransaction(tx.newOwner1); 264 | if (mergeTx1 !== null) { 265 | spendUTXO(mergeTx1); 266 | createUTXO(blockNumber, mergeTx1, txs.length); 267 | txs.push(mergeTx1.toString(true)); 268 | } 269 | const mergeTx2 = await createMergeTransaction(tx.newOwner2); 270 | if (mergeTx2 !== null) { 271 | spendUTXO(mergeTx2); 272 | createUTXO(blockNumber, mergeTx2, txs.length); 273 | txs.push(mergeTx2.toString(true)); 274 | } 275 | 276 | // Limit transactions per block to power of 2 on purpose for the 277 | // convenience of building Merkle tree. 278 | if (txs.length >= 256) { 279 | break; 280 | } 281 | } 282 | 283 | // Fill empty string if transactions are less than 256. 284 | const len = txs.length; 285 | for (let i = len; i < 256; i++) { 286 | txs.push(""); 287 | } 288 | 289 | return txs; 290 | }; 291 | 292 | const getUTXO = () => { 293 | return utxo; 294 | }; 295 | 296 | const getPool = () => { 297 | return txPool; 298 | }; 299 | 300 | module.exports = {createTransaction, collectTransactions, getUTXO, getPool}; 301 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // See 3 | // to customize your Truffle configuration! 4 | }; 5 | -------------------------------------------------------------------------------- /truffle.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | networks: { 3 | development: { 4 | host: "localhost", 5 | port: 8545, 6 | gas: 6721975, 7 | network_id: "*", // Match any network id 8 | } 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Web3 = require("web3"); 4 | 5 | const addHexPrefix = (msg) => { 6 | return '0x' + msg; 7 | }; 8 | 9 | const removeHexPrefix = (msg) => { 10 | if (Web3.utils.isHexStrict(msg)) { 11 | return msg.slice(2); 12 | } else { 13 | return msg; 14 | } 15 | }; 16 | 17 | const bufferToHex = (buf, withPrefix) => { 18 | if (withPrefix) { 19 | return addHexPrefix(buf.toString('hex')); 20 | } else { 21 | return buf.toString('hex'); 22 | } 23 | }; 24 | 25 | const weiToEther = (data) => { 26 | return data / 1000000000000000000; 27 | }; 28 | 29 | const etherToWei = (data) => { 30 | return data * 1000000000000000000; 31 | }; 32 | 33 | module.exports = {addHexPrefix, removeHexPrefix, bufferToHex, weiToEther, 34 | etherToWei}; 35 | --------------------------------------------------------------------------------