├── .gitignore ├── .gitmodules ├── README.md ├── build ├── .keep └── abi ├── contracts ├── Bits.sol ├── BuyoutsProcessor.sol ├── ByteSlice.sol ├── Conversion.sol ├── LimboExitGame.sol ├── Migrations.sol ├── PlasmaBlockStorage.sol ├── PlasmaChallenges.sol ├── PlasmaParent.sol ├── PlasmaTransactionLibrary.sol ├── PriorityQueue.sol ├── RLP.sol ├── SafeMath.sol ├── Structures.sol └── TXTester.sol ├── helpers ├── assertHelpers.js ├── details.js ├── expectThrow.js ├── increaseTime.js └── start.sh ├── migrations ├── 1_initial_migration.js └── 2_deploy_plasma.js ├── package-lock.json ├── package.json ├── test ├── blockSubmissionFunctions.js ├── buyout.js ├── createBlock.js ├── createTransaction.js ├── deploy.js ├── depositWithdrawFunctions.js ├── exitFunctions.js ├── fullExitProcedure.js ├── keys.js ├── limboExitFunctions.js ├── priorityQueue.js ├── testInvalidBlockChallenges.js ├── transactionSerialization.js └── utils.js └── truffle.js /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | /build/contracts 3 | node_modules 4 | .vscode 5 | .DS_Store 6 | .idea 7 | !/build/.keep 8 | !/build/abi 9 | build/details* 10 | .node-* 11 | .node* -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib"] 2 | path = lib 3 | url = https://github.com/matterinc/PlasmaTransactionJS.git 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 |
5 | The Matter Plasma Implementation 6 |
7 |

8 |

9 | Contract • 10 | TX & Block RLP • 11 | API • 12 | JS Lib • 13 | Swift Lib • 14 | Block Explorer • 15 | Web App • 16 | iOS App 17 |

18 | 19 | # Plasma Parent Contract 20 | 21 | 22 | 23 | - [Description](#description) 24 | - [Transaction structure](#transaction-structure) 25 | * [Input](#input) 26 | * [Output](#output) 27 | * [Transaction](#transaction) 28 | * [Signed transaction](#signed-transaction) 29 | - [Block structure](#block-structure) 30 | * [Block header](#block-header) 31 | * [Block](#block) 32 | - [This contract differs from Minimal Viable Plasma in the following:](#this-contract-differs-from-minimal-viable-plasma-in-the-following) 33 | - [Implemented functionality:](#implemented-functionality) 34 | - [List of intended challenges and tests](#list-of-intended-challenges-and-tests) 35 | - [Getting started](#getting-started) 36 | * [Download dependecies:](#download-dependecies) 37 | - [Contribution](#contribution) 38 | - [Authors](#authors) 39 | - [Further work](#further-work) 40 | - [License](#license) 41 | 42 | 43 | 44 | ## Description 45 | 46 | This contract is used to maintain the integrity and correctness of Plasma by providing deposits, exits and limbo exits mechanism. It's based on a construction called More Viable Plasma, where in case of exit a priority of the transaction in the exit queue (so, priority of all outputs of this transaction) is assigned as the "age of the yongest input". In case of this implementation age is determined as a big-endian number made from byte concatenation of `BlockNumber|TransactionNumber|OutputNumber` of the UTXO being spent by the input. So age is first determined by smalled block number, then smaller transaction number in block, and then smaller output number in transaction. Priority is determined as the largest age and lower is better. Priority for exit purposes is capped and can not be better (smaller) than the age of block at `-1 week` timestamp. 47 | 48 | To demonstrate why such priority is enough for proper limbo exit game (when block withholding happens and it's unknown what's happening in Plasma, so exits are done without a Merkle proof of inclusion in block) on can imagine a following situation: 49 | - An operator does something malicious in block number `N` (like spend someone's else UTXO without knowing a private key) and withholds a block, but still publishes a header 50 | - An operator can not start an exit directly from the block number `N` as he will be challenged by demonstrating a mismatch between transaction input and original UTXOs 51 | - So an operator will have to make another block `N+1` (and withhold it) to start exits from it. There he puts a transaction that references an invalid transaction that was produced in block number `N` 52 | - In this case a priority of malicious exiting transaction will be based on a block number `N` 53 | - Any valid transaction from users (that can not be trivially challenged, since full information for a blocks `< N` is available) would have it's inputs from block with a number ` input 173 | - [x] should allow successful exit 174 | - [x] Challenges 175 | - [x] Transaction in block references the future 176 | - [x] Transaction is malformed (balance breaking) 177 | - [x] Double spend 178 | - [x] Spend without owner signature 179 | - [x] UTXO amount is not equal to input amount 180 | - [x] UTXO was successfully withdrawn and than spent in Plasma 181 | 182 | ## Getting started 183 | 184 | ### Download dependecies: 185 | 186 | ``` 187 | git submodule init 188 | git submodule update --recursive 189 | ``` 190 | 191 | ## Contribution 192 | 193 | Everyone is welcome to spot mistakes in the logic of this contract as number of provided functions is substantial. If you find a potential error or security loophole (one that would allow Plasma operator or user to break the normal operation and not being caught) - please open an issue. 194 | 195 | ## Authors 196 | 197 | Alex Vlasov, [@shamatar](https://github.com/shamatar), alex.m.vlasov@gmail.com 198 | 199 | ## Further work 200 | 201 | Make optimizations for reduced gas costs for most of the functions. 202 | 203 | ## License 204 | 205 | All source code and information in this repository is available under the Apache License 2.0 license. 206 | -------------------------------------------------------------------------------- /build/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matter-labs-archive/PlasmaContract/a1e96489a2b2c05c43a4d614ff40177e9ed03e42/build/.keep -------------------------------------------------------------------------------- /build/abi: -------------------------------------------------------------------------------- 1 | {"abi":[{"constant":true,"inputs":[],"name":"buyoutProcessorContract","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"WithdrawCollateral","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"plasmaErrorFound","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"lastValidBlock","outputs":[{"name":"","type":"uint32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"DepositWithdrawDelay","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"ExitDelay","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"blockStorage","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"operatorsBond","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"bytes22"}],"name":"succesfulExits","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"limboExitContract","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"depositRecords","outputs":[{"name":"from","type":"address"},{"name":"status","type":"uint8"},{"name":"hasCollateral","type":"bool"},{"name":"amount","type":"uint256"},{"name":"withdrawStartedAt","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"DepositWithdrawCollateral","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"bytes22"}],"name":"exitBuyoutOffers","outputs":[{"name":"amount","type":"uint256"},{"name":"from","type":"address"},{"name":"accepted","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"challengesContract","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"bytes22"}],"name":"exitRecords","outputs":[{"name":"transactionRef","type":"bytes32"},{"name":"amount","type":"uint256"},{"name":"owner","type":"address"},{"name":"timePublished","type":"uint64"},{"name":"blockNumber","type":"uint32"},{"name":"transactionNumber","type":"uint32"},{"name":"outputNumber","type":"uint8"},{"name":"isValid","type":"bool"},{"name":"isLimbo","type":"bool"},{"name":"priority","type":"uint72"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"},{"name":"","type":"uint256"}],"name":"allDepositRecordsForUser","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"depositCounter","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"},{"name":"","type":"uint256"}],"name":"allExitsForUser","outputs":[{"name":"","type":"bytes22"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"LimboChallangesDelay","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"exitQueue","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"_priorityQueue","type":"address"},{"name":"_blockStorage","type":"address"}],"payable":true,"stateMutability":"payable","type":"constructor"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_lastValidBlockNumber","type":"uint256"}],"name":"ErrorFoundEvent","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_from","type":"address"},{"indexed":true,"name":"_amount","type":"uint256"},{"indexed":true,"name":"_depositIndex","type":"uint256"}],"name":"DepositEvent","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_depositIndex","type":"uint256"}],"name":"DepositWithdrawStartedEvent","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_depositIndex","type":"uint256"}],"name":"DepositWithdrawChallengedEvent","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_depositIndex","type":"uint256"}],"name":"DepositWithdrawCompletedEvent","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_hash","type":"bytes32"},{"indexed":false,"name":"_data","type":"bytes"}],"name":"TransactionPublished","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_partialHash","type":"bytes22"}],"name":"ExitRecordCreated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_partialHash","type":"bytes22"}],"name":"ExitChallenged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_index","type":"uint64"}],"name":"TransactionIsPublished","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_from","type":"address"},{"indexed":false,"name":"_priority","type":"uint72"},{"indexed":true,"name":"_index","type":"uint72"},{"indexed":true,"name":"_partialHash","type":"bytes22"}],"name":"ExitStartedEvent","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_from","type":"address"},{"indexed":true,"name":"_priority","type":"uint72"},{"indexed":true,"name":"_partialHash","type":"bytes22"}],"name":"LimboExitStartedEvent","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_partialHash","type":"bytes22"},{"indexed":true,"name":"_from","type":"address"},{"indexed":false,"name":"_challengeNumber","type":"uint8"},{"indexed":false,"name":"_inputNumber","type":"uint8"}],"name":"LimboExitChallengePublished","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_partialHash","type":"bytes22"},{"indexed":true,"name":"_from","type":"address"},{"indexed":true,"name":"_buyoutAmount","type":"uint256"}],"name":"ExitBuyoutOffered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_partialHash","type":"bytes22"},{"indexed":true,"name":"_from","type":"address"}],"name":"ExitBuyoutAccepted","type":"event"},{"constant":false,"inputs":[{"name":"_op","type":"address"},{"name":"_status","type":"uint256"}],"name":"setOperator","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_buyouts","type":"address"}],"name":"allowDeposits","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_challenger","type":"address"}],"name":"allowChallenges","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_limboExiter","type":"address"}],"name":"allowLimboExits","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_headers","type":"bytes"}],"name":"submitBlockHeaders","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"lastBlockNumber","outputs":[{"name":"blockNumber","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"hashOfLastSubmittedBlock","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"incrementWeekOldCounter","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_plasmaBlockNumber","type":"uint32"},{"name":"_outputNumber","type":"uint8"},{"name":"_plasmaTransaction","type":"bytes"},{"name":"_merkleProof","type":"bytes"}],"name":"startExit","outputs":[{"name":"success","type":"bool"}],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[{"name":"_exitRecordHash","type":"bytes22"},{"name":"_plasmaBlockNumber","type":"uint32"},{"name":"_plasmaTransaction","type":"bytes"},{"name":"_merkleProof","type":"bytes"},{"name":"_inputNumber","type":"uint8"}],"name":"challengeNormalExitByShowingExitBeingSpent","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_originalTransaction","type":"bytes"},{"name":"_originalInputNumber","type":"uint8"},{"name":"_exitRecordHash","type":"bytes22"},{"name":"_plasmaBlockNumber","type":"uint32"},{"name":"_plasmaTransaction","type":"bytes"},{"name":"_merkleProof","type":"bytes"},{"name":"_inputNumber","type":"uint8"}],"name":"challengeNormalExitByShowingAnInputDoubleSpend","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_originalTransaction","type":"bytes"},{"name":"_originalInputNumber","type":"uint8"},{"name":"_exitRecordHash","type":"bytes22"},{"name":"_plasmaBlockNumber","type":"uint32"},{"name":"_plasmaTransaction","type":"bytes"},{"name":"_merkleProof","type":"bytes"},{"name":"_outputNumber","type":"uint8"}],"name":"challengeNormalExitByShowingMismatchedInput","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_numOfExits","type":"uint256"}],"name":"finalizeExits","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_plasmaTransaction","type":"bytes"}],"name":"isWellFormedTransaction","outputs":[{"name":"isWellFormed","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"deposit","outputs":[{"name":"success","type":"bool"}],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[{"name":"_for","type":"address"}],"name":"depositFor","outputs":[{"name":"success","type":"bool"}],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[{"name":"_index","type":"bytes22"},{"name":"_beneficiary","type":"address"}],"name":"offerOutputBuyout","outputs":[{"name":"success","type":"bool"}],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[{"name":"_index","type":"bytes22"}],"name":"acceptBuyoutOffer","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_index","type":"bytes22"}],"name":"returnExpiredBuyoutOffer","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_index","type":"bytes22"},{"name":"_amount","type":"uint256"},{"name":"_beneficiary","type":"address"},{"name":"v","type":"uint8"},{"name":"r","type":"bytes32"},{"name":"s","type":"bytes32"}],"name":"publishPreacceptedBuyout","outputs":[{"name":"success","type":"bool"}],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[{"name":"depositIndex","type":"uint256"}],"name":"startDepositWithdraw","outputs":[{"name":"success","type":"bool"}],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[{"name":"depositIndex","type":"uint256"}],"name":"finalizeDepositWithdraw","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"depositIndex","type":"uint256"},{"name":"_plasmaBlockNumber","type":"uint32"},{"name":"_plasmaTransaction","type":"bytes"},{"name":"_merkleProof","type":"bytes"}],"name":"challengeDepositWithdraw","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_plasmaBlockNumber1","type":"uint32"},{"name":"_inputNumber1","type":"uint8"},{"name":"_plasmaTransaction1","type":"bytes"},{"name":"_merkleProof1","type":"bytes"},{"name":"_plasmaBlockNumber2","type":"uint32"},{"name":"_inputNumber2","type":"uint8"},{"name":"_plasmaTransaction2","type":"bytes"},{"name":"_merkleProof2","type":"bytes"}],"name":"proveDoubleSpend","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_plasmaBlockNumber","type":"uint32"},{"name":"_plasmaTransaction","type":"bytes"},{"name":"_merkleProof","type":"bytes"},{"name":"_originatingPlasmaTransaction","type":"bytes"},{"name":"_originatingMerkleProof","type":"bytes"},{"name":"_inputNumber","type":"uint8"}],"name":"proveSpendAndWithdraw","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_plasmaBlockNumber","type":"uint32"},{"name":"_plasmaTransaction","type":"bytes"},{"name":"_merkleProof","type":"bytes"}],"name":"proveInvalidDeposit","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_plasmaBlockNumber1","type":"uint32"},{"name":"_plasmaTransaction1","type":"bytes"},{"name":"_merkleProof1","type":"bytes"},{"name":"_plasmaBlockNumber2","type":"uint32"},{"name":"_plasmaTransaction2","type":"bytes"},{"name":"_merkleProof2","type":"bytes"}],"name":"proveDoubleFunding","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_plasmaBlockNumber","type":"uint32"},{"name":"_plasmaInputNumberInTx","type":"uint8"},{"name":"_plasmaTransaction","type":"bytes"},{"name":"_merkleProof","type":"bytes"}],"name":"proveReferencingInvalidBlock","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_plasmaBlockNumber","type":"uint32"},{"name":"_plasmaTransaction","type":"bytes"},{"name":"_merkleProof","type":"bytes"},{"name":"_originatingPlasmaBlockNumber","type":"uint32"},{"name":"_originatingPlasmaTransaction","type":"bytes"},{"name":"_originatingMerkleProof","type":"bytes"},{"name":"_inputOfInterest","type":"uint256"}],"name":"proveBalanceOrOwnershipBreakingBetweenInputAndOutput","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"}]} -------------------------------------------------------------------------------- /contracts/Bits.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.25; 2 | 3 | 4 | library SmallBits { 5 | 6 | uint8 constant internal ONE = uint8(1); 7 | uint8 constant internal ONES = uint8(~0); 8 | 9 | // Sets the bit at the given 'index' in 'self' to '1'. 10 | // Returns the modified value. 11 | function setBit(uint8 self, uint8 index) internal pure returns (uint8) { 12 | return self | ONE << index; 13 | } 14 | 15 | // Sets the bit at the given 'index' in 'self' to '0'. 16 | // Returns the modified value. 17 | function clearBit(uint8 self, uint8 index) internal pure returns (uint8) { 18 | return self & ~(ONE << index); 19 | } 20 | 21 | // Sets the bit at the given 'index' in 'self' to: 22 | // '1' - if the bit is '0' 23 | // '0' - if the bit is '1' 24 | // Returns the modified value. 25 | function toggleBit(uint self, uint8 index) internal pure returns (uint) { 26 | return self ^ ONE << index; 27 | } 28 | 29 | // Get the value of the bit at the given 'index' in 'self'. 30 | function bit(uint self, uint8 index) internal pure returns (uint8) { 31 | return uint8(self >> index & 1); 32 | } 33 | 34 | // Check if the bit at the given 'index' in 'self' is set. 35 | // Returns: 36 | // 'true' - if the value of the bit is '1' 37 | // 'false' - if the value of the bit is '0' 38 | function bitSet(uint self, uint8 index) internal pure returns (bool) { 39 | return self >> index & 1 == 1; 40 | } 41 | 42 | // Checks if the bit at the given 'index' in 'self' is equal to the corresponding 43 | // bit in 'other'. 44 | // Returns: 45 | // 'true' - if both bits are '0' or both bits are '1' 46 | // 'false' - otherwise 47 | function bitEqual(uint self, uint other, uint8 index) internal pure returns (bool) { 48 | return (self ^ other) >> index & 1 == 0; 49 | } 50 | 51 | // Get the bitwise NOT of the bit at the given 'index' in 'self'. 52 | function bitNot(uint self, uint8 index) internal pure returns (uint8) { 53 | return uint8(1 - (self >> index & 1)); 54 | } 55 | 56 | // Computes the bitwise AND of the bit at the given 'index' in 'self', and the 57 | // corresponding bit in 'other', and returns the value. 58 | function bitAnd(uint self, uint other, uint8 index) internal pure returns (uint8) { 59 | return uint8((self & other) >> index & 1); 60 | } 61 | 62 | // Computes the bitwise OR of the bit at the given 'index' in 'self', and the 63 | // corresponding bit in 'other', and returns the value. 64 | function bitOr(uint self, uint other, uint8 index) internal pure returns (uint8) { 65 | return uint8((self | other) >> index & 1); 66 | } 67 | 68 | // Computes the bitwise XOR of the bit at the given 'index' in 'self', and the 69 | // corresponding bit in 'other', and returns the value. 70 | function bitXor(uint self, uint other, uint8 index) internal pure returns (uint8) { 71 | return uint8((self ^ other) >> index & 1); 72 | } 73 | 74 | // Gets 'numBits' consecutive bits from 'self', starting from the bit at 'startIndex'. 75 | // Returns the bits as a 'uint'. 76 | // Requires that: 77 | // - '0 < numBits <= 256' 78 | // - 'startIndex < 256' 79 | // - 'numBits + startIndex <= 256' 80 | function bits(uint self, uint8 startIndex, uint16 numBits) internal pure returns (uint) { 81 | require(0 < numBits && startIndex < 256 && startIndex + numBits <= 256); 82 | return self >> startIndex & ONES >> 256 - numBits; 83 | } 84 | 85 | // Computes the index of the highest bit set in 'self'. 86 | // Returns the highest bit set as an 'uint8'. 87 | // Requires that 'self != 0'. 88 | function highestBitSet(uint self) internal pure returns (uint8 highest) { 89 | require(self != 0); 90 | uint val = self; 91 | for (uint8 i = 128; i >= 1; i >>= 1) { 92 | if (val & (ONE << i) - 1 << i != 0) { 93 | highest += i; 94 | val >>= i; 95 | } 96 | } 97 | } 98 | 99 | // Computes the index of the lowest bit set in 'self'. 100 | // Returns the lowest bit set as an 'uint8'. 101 | // Requires that 'self != 0'. 102 | function lowestBitSet(uint self) internal pure returns (uint8 lowest) { 103 | require(self != 0); 104 | uint val = self; 105 | for (uint8 i = 128; i >= 1; i >>= 1) { 106 | if (val & (ONE << i) - 1 == 0) { 107 | lowest += i; 108 | val >>= i; 109 | } 110 | } 111 | } 112 | 113 | } -------------------------------------------------------------------------------- /contracts/BuyoutsProcessor.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.25; 2 | 3 | import {PlasmaTransactionLibrary} from "./PlasmaTransactionLibrary.sol"; 4 | import {PlasmaBlockStorageInterface} from "./PlasmaBlockStorage.sol"; 5 | import {PriorityQueueInterface} from "./PriorityQueue.sol"; 6 | import {StructuresLibrary} from "./Structures.sol"; 7 | import {SafeMath} from "./SafeMath.sol"; 8 | 9 | contract PlasmaBuyoutProcessor { 10 | // begining of storage declaration 11 | 12 | bool public plasmaErrorFound; 13 | uint32 public lastValidBlock; 14 | uint256 public operatorsBond; 15 | 16 | PriorityQueueInterface public exitQueue; 17 | PlasmaBlockStorageInterface public blockStorage; 18 | address public challengesContract; 19 | address public limboExitContract; 20 | address public buyoutProcessorContract; 21 | address public owner = msg.sender; 22 | 23 | uint256 public depositCounter; 24 | 25 | uint256 public constant DepositWithdrawCollateral = 50000000000000000; 26 | uint256 public constant WithdrawCollateral = 50000000000000000; 27 | uint256 public constant DepositWithdrawDelay = (72 hours); 28 | uint256 public constant LimboChallangesDelay = (72 hours); 29 | uint256 public constant ExitDelay = (168 hours); 30 | 31 | uint256 constant TxTypeNull = 0; 32 | uint256 constant TxTypeSplit = 1; 33 | uint256 constant TxTypeMerge = 2; 34 | uint256 constant TxTypeFund = 4; 35 | 36 | // deposits 37 | 38 | uint8 constant DepositStatusNoRecord = 0; // no deposit 39 | uint8 constant DepositStatusDeposited = 1; // deposit has happened 40 | uint8 constant DepositStatusWithdrawStarted = 2; // user withdraws a deposit 41 | uint8 constant DepositStatusWithdrawCompleted = 3; // used has withdrawn a deposit 42 | uint8 constant DepositStatusDepositConfirmed = 4; // a transaction with a deposit was posted 43 | 44 | struct DepositRecord { 45 | address from; 46 | uint8 status; 47 | bool hasCollateral; 48 | uint256 amount; 49 | uint256 withdrawStartedAt; 50 | } 51 | 52 | mapping(uint256 => DepositRecord) public depositRecords; 53 | mapping(address => uint256[]) public allDepositRecordsForUser; 54 | 55 | struct ExitBuyoutOffer { 56 | uint256 amount; 57 | address from; 58 | bool accepted; 59 | } 60 | 61 | mapping(address => bytes22[]) public allExitsForUser; 62 | mapping(bytes22 => ExitBuyoutOffer) public exitBuyoutOffers; 63 | 64 | mapping(bytes22 => StructuresLibrary.ExitRecord) public exitRecords; 65 | mapping(bytes22 => StructuresLibrary.LimboData) limboExitsData; 66 | mapping(bytes22 => bool) public succesfulExits; 67 | 68 | event ErrorFoundEvent(uint256 indexed _lastValidBlockNumber); 69 | 70 | event DepositEvent(address indexed _from, uint256 indexed _amount, uint256 indexed _depositIndex); 71 | event DepositWithdrawStartedEvent(uint256 indexed _depositIndex); 72 | event DepositWithdrawChallengedEvent(uint256 indexed _depositIndex); 73 | event DepositWithdrawCompletedEvent(uint256 indexed _depositIndex); 74 | 75 | event TransactionPublished(bytes32 indexed _hash, bytes _data); 76 | event ExitRecordCreated(bytes22 indexed _partialHash); 77 | event ExitChallenged(bytes22 indexed _partialHash); 78 | event TransactionIsPublished(uint64 indexed _index); 79 | event ExitStartedEvent(address indexed _from, uint72 _priority, uint72 indexed _index, bytes22 indexed _partialHash); 80 | 81 | event LimboExitStartedEvent(address indexed _from, uint72 indexed _priority, bytes22 indexed _partialHash); 82 | event LimboExitChallengePublished(bytes22 indexed _partialHash, address indexed _from, uint8 _challengeNumber, uint8 _inputNumber); 83 | event ExitBuyoutOffered(bytes22 indexed _partialHash, address indexed _from, uint256 indexed _buyoutAmount); 84 | event ExitBuyoutAccepted(bytes22 indexed _partialHash, address indexed _from); 85 | // end of storage declarations --------------------------- 86 | 87 | constructor() public { 88 | } 89 | // ---------------------------------- 90 | 91 | // Deposit related functions 92 | 93 | function deposit() payable public returns (bool success) { 94 | return depositFor(msg.sender); 95 | } 96 | 97 | function depositFor(address _for) payable public returns (bool success) { 98 | require(msg.value > 0); 99 | require(!plasmaErrorFound); 100 | uint256 size; 101 | assembly { 102 | size := extcodesize(_for) 103 | } 104 | if (size > 0) { 105 | revert("No deposits to the contracts!"); 106 | } 107 | uint256 depositIndex = depositCounter; 108 | DepositRecord storage record = depositRecords[depositIndex]; 109 | require(record.status == DepositStatusNoRecord); 110 | record.from = _for; 111 | record.amount = msg.value; 112 | record.status = DepositStatusDeposited; 113 | depositCounter = depositCounter + 1; 114 | emit DepositEvent(_for, msg.value, depositIndex); 115 | allDepositRecordsForUser[_for].push(depositIndex); 116 | return true; 117 | } 118 | 119 | function offerOutputBuyout(bytes22 _index, address _beneficiary) public payable returns (bool success) { 120 | require(msg.value > 0); 121 | require(_beneficiary != address(0)); 122 | StructuresLibrary.ExitRecord storage exitRecord = exitRecords[_index]; 123 | require(exitRecord.isValid); 124 | ExitBuyoutOffer storage offer = exitBuyoutOffers[_index]; 125 | emit ExitBuyoutOffered(_index, _beneficiary, msg.value); 126 | require(!offer.accepted); 127 | address oldFrom = offer.from; 128 | uint256 oldAmount = offer.amount; 129 | require(msg.value > oldAmount); 130 | offer.from = _beneficiary; 131 | offer.amount = msg.value; 132 | if (oldFrom != address(0)) { 133 | oldFrom.transfer(oldAmount); 134 | } 135 | return true; 136 | } 137 | 138 | function acceptBuyoutOffer(bytes22 _index) public returns (bool success) { 139 | StructuresLibrary.ExitRecord storage exitRecord = exitRecords[_index]; 140 | // this simple require solves accepting the already exited transaciton and validity :) 141 | require(exitRecord.isValid); 142 | ExitBuyoutOffer storage offer = exitBuyoutOffers[_index]; 143 | require(offer.from != address(0)); 144 | require(!offer.accepted); 145 | address oldBeneficiary = exitRecord.owner; 146 | uint256 offerAmount = offer.amount; 147 | offer.accepted = true; 148 | emit ExitBuyoutAccepted(_index, offer.from); 149 | oldBeneficiary.transfer(offerAmount); 150 | return true; 151 | } 152 | 153 | function returnExpiredBuyoutOffer(bytes22 _index) public returns (bool success) { 154 | ExitBuyoutOffer storage offer = exitBuyoutOffers[_index]; 155 | require(!offer.accepted); 156 | // require(record.status != WithdrawStatusStarted || (block.timestamp >= record.timestamp + WithdrawDelay)); 157 | address oldFrom = offer.from; 158 | uint256 oldAmount = offer.amount; 159 | require(msg.sender == oldFrom); 160 | delete exitBuyoutOffers[_index]; 161 | oldFrom.transfer(oldAmount); 162 | return true; 163 | } 164 | 165 | function publishPreacceptedBuyout( 166 | bytes22 _index, 167 | uint256 _amount, 168 | address _beneficiary, 169 | uint8 v, 170 | bytes32 r, 171 | bytes32 s 172 | ) public payable returns (bool success) { 173 | StructuresLibrary.ExitRecord storage exitRecord = exitRecords[_index]; 174 | require(exitRecord.isValid, "Exit should be valid to accept a buyout"); 175 | ExitBuyoutOffer storage offer = exitBuyoutOffers[_index]; 176 | require(!offer.accepted, "Offer should not be prefiously accepted"); 177 | bytes memory PersonalMessagePrefixBytes = "\x19Ethereum Signed Message:\n74"; // 22 of index + 32 of amount + 20 of address 178 | address signer = ecrecover(keccak256(abi.encodePacked(PersonalMessagePrefixBytes, _index, _amount, _beneficiary)), v, r, s); 179 | require(signer == exitRecord.owner, "Acceptance signer should be record owner"); 180 | require(msg.value >= _amount, "Need to send at least the agreed amount"); 181 | address oldFrom = offer.from; 182 | uint256 oldAmount = offer.amount; 183 | require(msg.value > oldAmount, "Should send more than any previous offer"); 184 | offer.from = _beneficiary; 185 | offer.amount = msg.value; 186 | if (oldFrom != address(0)) { 187 | oldFrom.transfer(oldAmount); 188 | } 189 | offer.accepted = true; 190 | exitRecord.owner.transfer(_amount); 191 | emit ExitBuyoutAccepted(_index, _beneficiary); 192 | return true; 193 | } 194 | 195 | // ---------------------------------- 196 | 197 | function() external payable{ 198 | address callee = challengesContract; 199 | assembly { 200 | let memoryPointer := mload(0x40) 201 | calldatacopy(memoryPointer, 0, calldatasize) 202 | let newFreeMemoryPointer := add(memoryPointer, calldatasize) 203 | mstore(0x40, newFreeMemoryPointer) 204 | let retVal := delegatecall(sub(gas, 2000), callee, memoryPointer, calldatasize, newFreeMemoryPointer, 0x40) 205 | let retDataSize := returndatasize 206 | returndatacopy(newFreeMemoryPointer, 0, retDataSize) 207 | switch retVal case 0 { revert(0,0) } default { return(newFreeMemoryPointer, retDataSize) } 208 | } 209 | } 210 | 211 | } 212 | -------------------------------------------------------------------------------- /contracts/ByteSlice.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.25; 2 | 3 | library ByteSlice { 4 | 5 | struct Slice { 6 | uint256 _unsafe_memPtr; // Memory address of the first byte. 7 | uint256 _unsafe_length; // Length. 8 | } 9 | 10 | /// @dev Converts bytes to a slice. 11 | /// @param self The bytes. 12 | /// @return A slice. 13 | function slice(bytes memory self) internal pure returns (Slice memory newSlice) { 14 | assembly { 15 | let length := mload(self) 16 | let memPtr := add(self, 0x20) 17 | mstore(newSlice, mul(memPtr, iszero(iszero(length)))) 18 | mstore(add(newSlice, 0x20), length) 19 | } 20 | } 21 | 22 | // /// @dev Converts bytes to a slice from the given starting position. 23 | // /// 'startpos' <= 'len(slice)' 24 | // /// @param self The bytes. 25 | // /// @param startpos The starting position. 26 | // /// @return A slice. 27 | // function slice(bytes memory self, uint256 startpos) internal pure returns (Slice memory) { 28 | // return slice(slice(self), startpos); 29 | // } 30 | 31 | // /// @dev Converts bytes to a slice from the given starting position. 32 | // /// -len(slice) <= 'startpos' <= 'len(slice)' 33 | // /// @param self The bytes. 34 | // /// @param startpos The starting position. 35 | // /// @return A slice. 36 | // function slice(bytes memory self, int startpos) internal pure returns (Slice memory) { 37 | // return slice(slice(self), startpos); 38 | // } 39 | 40 | // /// @dev Converts bytes to a slice from the given starting-position, and end-position. 41 | // /// 'startpos <= len(slice) and startpos <= endpos' 42 | // /// 'endpos <= len(slice)' 43 | // /// @param self The bytes. 44 | // /// @param startpos The starting position. 45 | // /// @param endpos The end position. 46 | // /// @return A slice. 47 | // function slice(bytes memory self, uint256 startpos, uint256 endpos) internal pure returns (Slice memory) { 48 | // return slice(slice(self), startpos, endpos); 49 | // } 50 | 51 | // /// @dev Converts bytes to a slice from the given starting-position, and end-position. 52 | // /// Warning: higher cost then using unsigned integers. 53 | // /// @param self The bytes. 54 | // /// @param startpos The starting position. 55 | // /// @param endpos The end position. 56 | // /// @return A slice. 57 | // function slice(bytes memory self, int startpos, int endpos) internal pure returns (Slice memory) { 58 | // return slice(slice(self), startpos, endpos); 59 | // } 60 | 61 | /// @dev Get the length of the slice (in bytes). 62 | /// @param self The slice. 63 | /// @return the length. 64 | function len(Slice memory self) internal pure returns (uint256) { 65 | return self._unsafe_length; 66 | } 67 | 68 | // /// @dev Returns the byte from the backing array at a given index. 69 | // /// The function will throw unless 'index < len(slice)' 70 | // /// @param self The slice. 71 | // /// @param index The index. 72 | // /// @return The byte at that index. 73 | // function at(Slice memory self, uint256 index) internal pure returns (byte b) { 74 | // if (index >= self._unsafe_length) 75 | // revert(); 76 | // uint256 bb; 77 | // assembly { 78 | // // Get byte at index, and format to 'byte' variable. 79 | // bb := byte(0, mload(add(mload(self), index))) 80 | // } 81 | // b = byte(bb); 82 | // } 83 | 84 | // /// @dev Returns the byte from the backing array at a given index. 85 | // /// The function will throw unless '-len(self) <= index < len(self)'. 86 | // /// @param self The slice. 87 | // /// @param index The index. 88 | // /// @return The byte at that index. 89 | // function at(Slice memory self, int index) internal pure returns (byte b) { 90 | // if (index >= 0) 91 | // return at(self, uint256(index)); 92 | // uint256 iAbs = uint256(-index); 93 | // if (iAbs > self._unsafe_length) 94 | // revert(); 95 | // return at(self, self._unsafe_length - iAbs); 96 | // } 97 | 98 | // /// @dev Set the byte at the given index. 99 | // /// The function will throw unless 'index < len(slice)' 100 | // /// @param self The slice. 101 | // /// @param index The index. 102 | // /// @return The byte at that index. 103 | // function set(Slice memory self, uint256 index, byte b) internal pure { 104 | // if (index >= self._unsafe_length) 105 | // revert(); 106 | // assembly { 107 | // mstore8(add(mload(self), index), byte(0, b)) 108 | // } 109 | // } 110 | 111 | // /// @dev Set the byte at the given index. 112 | // /// The function will throw unless '-len(self) <= index < len(self)'. 113 | // /// @param self The slice. 114 | // /// @param index The index. 115 | // /// @return The byte at that index. 116 | // function set(Slice memory self, int index, byte b) internal pure { 117 | // if (index >= 0) 118 | // return set(self, uint256(index), b); 119 | // uint256 iAbs = uint256(-index); 120 | // if (iAbs > self._unsafe_length) 121 | // revert(); 122 | // return set(self, self._unsafe_length - iAbs, b); 123 | // } 124 | 125 | /// @dev Creates a copy of the slice. 126 | /// @param self The slice. 127 | /// @return the new reference. 128 | function slice(Slice memory self) internal pure returns (Slice memory newSlice) { 129 | newSlice._unsafe_memPtr = self._unsafe_memPtr; 130 | newSlice._unsafe_length = self._unsafe_length; 131 | } 132 | 133 | // /// @dev Create a new slice from the given starting position. 134 | // /// 'startpos' <= 'len(slice)' 135 | // /// @param self The slice. 136 | // /// @param startpos The starting position. 137 | // /// @return The new slice. 138 | // function slice(Slice memory self, uint256 startpos) internal pure returns (Slice memory newSlice) { 139 | // uint256 length = self._unsafe_length; 140 | // if (startpos > length) 141 | // revert(); 142 | // assembly { 143 | // length := sub(length, startpos) 144 | // let newMemPtr := mul(add(mload(self), startpos), iszero(iszero(length))) 145 | // mstore(newSlice, newMemPtr) 146 | // mstore(add(newSlice, 0x20), length) 147 | // } 148 | // } 149 | 150 | // /// @dev Create a new slice from the given starting position. 151 | // /// -len(slice) <= 'startpos' <= 'len(slice)' 152 | // /// @param self The slice. 153 | // /// @param startpos The starting position. 154 | // /// @return The new slice. 155 | // function slice(Slice memory self, int startpos) internal pure returns (Slice memory newSlice) { 156 | // uint256 startpos_; 157 | // uint256 length = self._unsafe_length; 158 | // if (startpos >= 0) { 159 | // startpos_ = uint256(startpos); 160 | // if (startpos_ > length) 161 | // revert(); 162 | // } else { 163 | // startpos_ = uint256(-startpos); 164 | // if (startpos_ > length) 165 | // revert(); 166 | // startpos_ = length - startpos_; 167 | // } 168 | // assembly { 169 | // length := sub(length, startpos_) 170 | // let newMemPtr := mul(add(mload(self), startpos_), iszero(iszero(length))) 171 | // mstore(newSlice, newMemPtr) 172 | // mstore(add(newSlice, 0x20), length) 173 | // } 174 | // } 175 | 176 | /// @dev Create a new slice from a given slice, starting-position, and end-position. 177 | /// 'startpos <= len(slice) and startpos <= endpos' 178 | /// 'endpos <= len(slice)' 179 | /// @param self The slice. 180 | /// @param startpos The starting position. 181 | /// @param endpos The end position. 182 | /// @return the new slice. 183 | function slice(Slice memory self, uint256 startpos, uint256 endpos) internal pure returns (Slice memory newSlice) { 184 | uint256 length = self._unsafe_length; 185 | if (startpos > length || endpos > length || startpos > endpos) 186 | revert(); 187 | assembly { 188 | length := sub(endpos, startpos) 189 | let newMemPtr := mul(add(mload(self), startpos), iszero(iszero(length))) 190 | mstore(newSlice, newMemPtr) 191 | mstore(add(newSlice, 0x20), length) 192 | } 193 | } 194 | 195 | /// Same as new(Slice memory, uint256, uint256) but allows for negative indices. 196 | /// Warning: higher cost then using unsigned integers. 197 | /// @param self The slice. 198 | /// @param startpos The starting position. 199 | /// @param endpos The end position. 200 | /// @return The new slice. 201 | function slice(Slice memory self, int startpos, int endpos) internal pure returns (Slice memory newSlice) { 202 | // Don't allow slice on bytes of length 0. 203 | uint256 startpos_; 204 | uint256 endpos_; 205 | uint256 length = self._unsafe_length; 206 | if (startpos < 0) { 207 | startpos_ = uint256(-startpos); 208 | if (startpos_ > length) 209 | revert(); 210 | startpos_ = length - startpos_; 211 | } 212 | else { 213 | startpos_ = uint256(startpos); 214 | if (startpos_ > length) 215 | revert(); 216 | } 217 | if (endpos < 0) { 218 | endpos_ = uint256(-endpos); 219 | if (endpos_ > length) 220 | revert(); 221 | endpos_ = length - endpos_; 222 | } 223 | else { 224 | endpos_ = uint256(endpos); 225 | if (endpos_ > length) 226 | revert(); 227 | } 228 | if(startpos_ > endpos_) 229 | revert(); 230 | assembly { 231 | length := sub(endpos_, startpos_) 232 | let newMemPtr := mul(add(mload(self), startpos_), iszero(iszero(length))) 233 | mstore(newSlice, newMemPtr) 234 | mstore(add(newSlice, 0x20), length) 235 | } 236 | } 237 | 238 | /// @dev Creates a 'bytes memory' variable from a slice, copying the data. 239 | /// Bytes are copied from the memory address 'self._unsafe_memPtr'. 240 | /// The number of bytes copied is 'self._unsafe_length'. 241 | /// @param self The slice. 242 | /// @return The bytes variable. 243 | function toBytes(Slice memory self) internal constant returns (bytes memory bts) { 244 | uint256 length = self._unsafe_length; 245 | if (length == 0) 246 | return; 247 | uint256 memPtr = self._unsafe_memPtr; 248 | bts = new bytes(length); 249 | // We can do word-by-word copying since 'bts' was the last thing to be 250 | // allocated. Just overwrite any excess bytes at the end with zeroes. 251 | assembly { 252 | let i := 0 253 | let btsOffset := add(bts, 0x20) 254 | let words := div(add(length, 31), 32) 255 | tag_loop: 256 | jumpi(end, gt(i, words)) 257 | { 258 | let offset := mul(i, 32) 259 | mstore(add(btsOffset, offset), mload(add(memPtr, offset))) 260 | i := add(i, 1) 261 | } 262 | jump(tag_loop) 263 | end: 264 | mstore(add(add(bts, length), 0x20), 0) 265 | } 266 | } 267 | 268 | /// @dev Creates an ascii-encoded 'string' variable from a slice, copying the data. 269 | /// Bytes are copied from the memory address 'self._unsafe_memPtr'. 270 | /// The number of bytes copied is 'self._unsafe_length'. 271 | /// @param self The slice. 272 | /// @return The bytes variable. 273 | function toAscii(Slice memory self) internal view returns (string memory str) { 274 | return string(toBytes(self)); 275 | } 276 | 277 | /// @dev Check if two slices are equal. 278 | /// @param self The slice. 279 | /// @param other The other slice. 280 | /// @return True if both slices point to the same memory address, and has the same length. 281 | function equals(Slice memory self, Slice memory other) internal pure returns (bool) { 282 | return ( 283 | self._unsafe_length == other._unsafe_length && 284 | self._unsafe_memPtr == other._unsafe_memPtr 285 | ); 286 | } 287 | 288 | function toUint(Slice memory self) internal pure returns (uint256 data) { 289 | uint256 sliceLength = self._unsafe_length; 290 | uint256 rStartPos = self._unsafe_memPtr; 291 | if (sliceLength > 32 || sliceLength == 0) 292 | revert(); 293 | assembly { 294 | data := div(mload(rStartPos), exp(256, sub(32, sliceLength))) 295 | } 296 | } 297 | 298 | function toBytes32(Slice memory self) internal pure returns (bytes32 data) { 299 | uint256 sliceLength = self._unsafe_length; 300 | uint256 rStartPos = self._unsafe_memPtr; 301 | if (sliceLength != 32) 302 | revert(); 303 | assembly { 304 | data := mload(rStartPos) 305 | } 306 | } 307 | 308 | } -------------------------------------------------------------------------------- /contracts/Conversion.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.25; 2 | 3 | library Conversion { 4 | 5 | function uintToBytes(uint256 self) internal pure returns (bytes memory s) { 6 | uint256 maxlength = 100; 7 | bytes memory reversed = new bytes(maxlength); 8 | uint256 i = 0; 9 | while (self != 0) { 10 | uint256 remainder = self % 10; 11 | self = self / 10; 12 | reversed[i++] = byte(48 + remainder); 13 | } 14 | s = new bytes(i); 15 | for (uint256 j = 0; j < i; j++) { 16 | s[j] = reversed[i - 1 - j]; 17 | } 18 | return s; 19 | } 20 | } -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.25; 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 | constructor() 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/PlasmaBlockStorage.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.25; 2 | 3 | import {Conversion} from "./Conversion.sol"; 4 | import {ByteSlice} from "./ByteSlice.sol"; 5 | 6 | interface PlasmaBlockStorageInterface { 7 | function isOperator(address _operator) external view returns (bool); 8 | function canSignBlocks(address _operator) external view returns (bool); 9 | function setOperator(address _op, uint256 _status) external returns (bool success); 10 | function lastBlockNumber() external view returns(uint256); 11 | function hashOfLastSubmittedBlock() external view returns(bytes32); 12 | function weekOldBlockNumber() external view returns(uint256); 13 | function submitBlockHeaders(bytes _headers) external returns (bool success); 14 | // function storeBlock(uint256 _blockNumber, uint256 _numberOfTransactions, bytes32 _merkleRoot) external returns (bool success); 15 | // function storeBlocks(uint256[] _blockNumbers, uint256[] _numbersOfTransactions, bytes32[] _merkleRoots) external returns (bool success); 16 | function getBlockInformation(uint32 _blockNumber) external view returns (uint256 submittedAt, uint32 numberOfTransactions, bytes32 merkleRoot); 17 | function getMerkleRoot(uint32 _blockNumber) external view returns (bytes32 merkleRoot); 18 | function incrementWeekOldCounter() external; 19 | function getSubmissionTime(uint32 _blockNumber) external view returns (uint256 submittedAt); 20 | function getNumberOfTransactions(uint32 _blockNumber) external view returns (uint32 numberOfTransaction); 21 | } 22 | 23 | contract PlasmaBlockStorage { 24 | using ByteSlice for bytes; 25 | using ByteSlice for ByteSlice.Slice; 26 | using Conversion for uint256; 27 | address public owner; 28 | 29 | mapping(address => OperatorStatus) public operators; 30 | enum OperatorStatus {Null, CanSignTXes, CanSignBlocks} 31 | 32 | uint256 public lastBlockNumber; 33 | uint256 public weekOldBlockNumber; 34 | uint256 public blockHeaderLength = 137; 35 | bytes32 public hashOfLastSubmittedBlock = keccak256(abi.encodePacked(PersonalMessagePrefixBytes,"6","Matter")); 36 | 37 | uint256 constant SignatureLength = 65; 38 | uint256 constant BlockNumberLength = 4; 39 | uint256 constant TxNumberLength = 4; 40 | uint256 constant TxTypeLength = 1; 41 | uint256 constant TxOutputNumberLength = 1; 42 | uint256 constant PreviousHashLength = 32; 43 | uint256 constant MerkleRootHashLength = 32; 44 | bytes constant PersonalMessagePrefixBytes = "\x19Ethereum Signed Message:\n"; 45 | uint256 constant PreviousBlockPersonalHashLength = BlockNumberLength + 46 | TxNumberLength + 47 | PreviousHashLength + 48 | MerkleRootHashLength + 49 | SignatureLength; 50 | uint256 constant NewBlockPersonalHashLength = BlockNumberLength + 51 | TxNumberLength + 52 | PreviousHashLength + 53 | MerkleRootHashLength; 54 | 55 | struct BlockInformation { 56 | uint32 numberOfTransactions; 57 | uint64 submittedAt; 58 | bytes32 merkleRootHash; 59 | } 60 | 61 | mapping (uint256 => BlockInformation) public blocks; 62 | event BlockHeaderSubmitted(uint256 indexed _blockNumber, bytes32 indexed _merkleRoot); 63 | 64 | constructor() public { 65 | owner = msg.sender; 66 | blocks[weekOldBlockNumber].submittedAt = uint64(block.timestamp); 67 | } 68 | 69 | modifier onlyOwner() { 70 | require(msg.sender == owner); 71 | _; 72 | } 73 | 74 | function setOwner(address _newOwner) onlyOwner public { 75 | require(_newOwner != address(0)); 76 | owner = _newOwner; 77 | } 78 | 79 | function setOperator(address _op, uint256 _status) public returns (bool success) { 80 | require(msg.sender == owner); 81 | OperatorStatus stat = operators[_op]; 82 | OperatorStatus newStat = OperatorStatus(_status); 83 | if (stat == OperatorStatus.Null) { 84 | operators[_op] = newStat; 85 | return true; 86 | } else if (stat == OperatorStatus.CanSignTXes) { 87 | require(newStat == OperatorStatus.CanSignBlocks); 88 | operators[_op] = newStat; 89 | return true; 90 | } else if (stat == OperatorStatus.CanSignBlocks) { 91 | require(newStat == OperatorStatus.CanSignTXes); 92 | operators[_op] = newStat; 93 | return true; 94 | } 95 | revert(); 96 | } 97 | 98 | function isOperator(address _operator) public view returns (bool) { 99 | OperatorStatus stat = operators[_operator]; 100 | return stat != OperatorStatus.Null; 101 | } 102 | 103 | function canSignBlocks(address _operator) public view returns (bool) { 104 | OperatorStatus stat = operators[_operator]; 105 | return stat == OperatorStatus.CanSignBlocks; 106 | } 107 | 108 | function incrementWeekOldCounter() onlyOwner public { 109 | uint256 ts = block.timestamp - (1 weeks); 110 | uint256 localCounter = weekOldBlockNumber; 111 | while (uint256(blocks[localCounter].submittedAt) <= ts) { 112 | if (blocks[localCounter].submittedAt == 0) { 113 | break; 114 | } 115 | localCounter++; 116 | } 117 | if (localCounter != weekOldBlockNumber) { 118 | weekOldBlockNumber = localCounter - 1; 119 | } 120 | } 121 | 122 | function submitBlockHeaders(bytes _headers) onlyOwner public returns (bool success) { 123 | require(_headers.length % blockHeaderLength == 0); 124 | ByteSlice.Slice memory slice = _headers.slice(); 125 | ByteSlice.Slice memory reusableSlice; 126 | uint256[] memory reusableSpace = new uint256[](5); 127 | bytes32 lastBlockHash = hashOfLastSubmittedBlock; 128 | uint256 _lastBlockNumber = lastBlockNumber; 129 | for (uint256 i = 0; i < _headers.length/blockHeaderLength; i++) { 130 | reusableSlice = slice.slice(i*blockHeaderLength, (i+1)*blockHeaderLength); 131 | reusableSpace[0] = 0; 132 | reusableSpace[1] = BlockNumberLength; 133 | reusableSpace[2] = reusableSlice.slice(reusableSpace[0],reusableSpace[1]).toUint(); //blockNumber 134 | require(reusableSpace[2] == _lastBlockNumber+1+i); 135 | reusableSpace[0] = reusableSpace[1]; 136 | reusableSpace[1] += TxNumberLength; 137 | reusableSpace[3] = reusableSlice.slice(reusableSpace[0],reusableSpace[1]).toUint(); //numberOfTransactions 138 | reusableSpace[0] = reusableSpace[1]; 139 | reusableSpace[1] += PreviousHashLength; 140 | bytes32 previousBlockHash = reusableSlice.slice(reusableSpace[0],reusableSpace[1]).toBytes32(); 141 | require(previousBlockHash == lastBlockHash); 142 | reusableSpace[0] = reusableSpace[1]; 143 | reusableSpace[1] += MerkleRootHashLength; 144 | bytes32 merkleRootHash = reusableSlice.slice(reusableSpace[0],reusableSpace[1]).toBytes32(); 145 | reusableSpace[0] = reusableSpace[1]; 146 | reusableSpace[1] += 1; 147 | reusableSpace[4] = reusableSlice.slice(reusableSpace[0],reusableSpace[1]).toUint(); 148 | if (reusableSpace[4] < 27) { 149 | reusableSpace[4] = reusableSpace[4]+27; 150 | } 151 | reusableSpace[0] = reusableSpace[1]; 152 | reusableSpace[1] += 32; 153 | bytes32 r = reusableSlice.slice(reusableSpace[0],reusableSpace[1]).toBytes32(); 154 | reusableSpace[0] = reusableSpace[1]; 155 | reusableSpace[1] += 32; 156 | bytes32 s = reusableSlice.slice(reusableSpace[0],reusableSpace[1]).toBytes32(); 157 | bytes32 newBlockHash = keccak256(abi.encodePacked(PersonalMessagePrefixBytes, NewBlockPersonalHashLength.uintToBytes(), uint32(reusableSpace[2]), uint32(reusableSpace[3]), previousBlockHash, merkleRootHash)); 158 | address signer = ecrecover(newBlockHash, uint8(reusableSpace[4]), r, s); 159 | require(canSignBlocks(signer)); 160 | lastBlockHash = keccak256(abi.encodePacked(PersonalMessagePrefixBytes, PreviousBlockPersonalHashLength.uintToBytes(), reusableSlice.toBytes())); 161 | storeBlock(reusableSpace[2], reusableSpace[3], merkleRootHash, i); 162 | } 163 | hashOfLastSubmittedBlock = lastBlockHash; 164 | return true; 165 | } 166 | 167 | 168 | function storeBlock(uint256 _blockNumber, uint256 _numberOfTransactions, bytes32 _merkleRoot, uint256 _timeOffset) internal returns (bool success) { 169 | incrementWeekOldCounter(); 170 | require(_blockNumber == lastBlockNumber + 1); 171 | BlockInformation storage newBlockInformation = blocks[_blockNumber]; 172 | newBlockInformation.merkleRootHash = _merkleRoot; 173 | newBlockInformation.submittedAt = uint64(block.timestamp + _timeOffset); 174 | newBlockInformation.numberOfTransactions = uint32(_numberOfTransactions); 175 | lastBlockNumber = _blockNumber; 176 | emit BlockHeaderSubmitted(_blockNumber, _merkleRoot); 177 | return true; 178 | } 179 | 180 | // function storeBlocks(uint256[] _blockNumbers, uint256[] _numbersOfTransactions, bytes32[] _merkleRoots) public returns (bool success) { 181 | // require(_blockNumbers.length == _merkleRoots.length); 182 | // require(_blockNumbers.length == _numbersOfTransactions.length); 183 | // require(_blockNumbers.length != 0); 184 | // incrementWeekOldCounter(); 185 | 186 | // uint256 currentCounter = lastBlockNumber; 187 | // for (uint256 i = 0; i < _blockNumbers.length; i++) { 188 | // require(_blockNumbers[i] == currentCounter + 1); 189 | // currentCounter = _blockNumbers[i]; 190 | // BlockInformation storage newBlockInformation = blocks[currentCounter]; 191 | // newBlockInformation.merkleRootHash = _merkleRoots[i]; 192 | // newBlockInformation.submittedAt = uint192(block.timestamp + i); 193 | // newBlockInformation.numberOfTransactions = uint32(_numbersOfTransactions[i]); 194 | // emit BlockHeaderSubmitted(_blockNumbers[i], _merkleRoots[i]); 195 | // } 196 | // lastBlockNumber = currentCounter; 197 | // return true; 198 | // } 199 | 200 | function getBlockInformation(uint32 _blockNumber) public view returns (uint256 submittedAt, uint32 numberOfTransactions, bytes32 merkleRoot) { 201 | BlockInformation storage blockInformation = blocks[uint256(_blockNumber)]; 202 | return (blockInformation.submittedAt, blockInformation.numberOfTransactions, blockInformation.merkleRootHash); 203 | } 204 | 205 | function getMerkleRoot(uint32 _blockNumber) public view returns (bytes32 merkleRoot) { 206 | BlockInformation storage blockInformation = blocks[uint256(_blockNumber)]; 207 | return blockInformation.merkleRootHash; 208 | } 209 | 210 | function getSubmissionTime(uint32 _blockNumber) public view returns (uint256 submittedAt) { 211 | BlockInformation storage blockInformation = blocks[uint256(_blockNumber)]; 212 | return uint256(blockInformation.submittedAt); 213 | } 214 | 215 | function getNumberOfTransactions(uint32 _blockNumber) public view returns (uint32 numberOfTransaction) { 216 | BlockInformation storage blockInformation = blocks[uint256(_blockNumber)]; 217 | return blockInformation.numberOfTransactions; 218 | } 219 | } -------------------------------------------------------------------------------- /contracts/PlasmaTransactionLibrary.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.25; 2 | 3 | import {RLP} from "./RLP.sol"; 4 | import {Conversion} from "./Conversion.sol"; 5 | 6 | library PlasmaTransactionLibrary { 7 | 8 | using RLP for RLP.RLPItem; 9 | using RLP for RLP.Iterator; 10 | using RLP for bytes; 11 | using Conversion for uint256; 12 | 13 | uint256 public constant BlockNumberLength = 4; 14 | uint256 public constant TxNumberLength = 4; 15 | uint256 public constant TxTypeLength = 1; 16 | uint256 public constant TxOutputNumberLength = 1; 17 | 18 | uint256 constant TxTypeNull = 0; 19 | uint256 constant TxTypeSplit = 1; 20 | uint256 constant TxTypeMerge = 2; 21 | uint256 constant TxTypeFund = 4; 22 | 23 | struct TransactionInput { 24 | uint32 blockNumber; 25 | uint32 txNumberInBlock; 26 | uint8 outputNumberInTX; 27 | uint256 amount; 28 | } 29 | 30 | struct TransactionOutput { 31 | address recipient; 32 | uint8 outputNumberInTX; 33 | uint256 amount; 34 | } 35 | 36 | struct PlasmaTransaction { 37 | uint32 txNumberInBlock; 38 | uint8 txType; 39 | TransactionInput[] inputs; 40 | TransactionOutput[] outputs; 41 | address sender; 42 | bool isWellFormed; 43 | } 44 | 45 | bytes constant PersonalMessagePrefixBytes = "\x19Ethereum Signed Message:\n"; 46 | 47 | function createPersonalMessageTypeHash(bytes memory message) internal pure returns (bytes32 msgHash) { 48 | bytes memory lengthBytes = message.length.uintToBytes(); 49 | return keccak256(abi.encodePacked(PersonalMessagePrefixBytes, lengthBytes, message)); 50 | } 51 | 52 | function checkForInclusionIntoBlock(bytes32 _merkleRoot, bytes _plasmaTransaction, bytes _merkleProof) internal pure returns (bool included, uint256 txNumber) { 53 | (included, txNumber) = checkProof(_merkleRoot, _plasmaTransaction, _merkleProof, true); 54 | return (included, txNumber); 55 | } 56 | 57 | function checkProof(bytes32 root, bytes data, bytes proof, bool convertToMessageHash) internal pure returns (bool included, uint256 txNumber) { 58 | bytes32 h; 59 | if (convertToMessageHash) { 60 | h = createPersonalMessageTypeHash(data); 61 | } else { 62 | h = keccak256(data); 63 | } 64 | bytes32 elProvided; 65 | uint8 rightElementProvided; 66 | uint32 loc; 67 | uint32 elLoc; 68 | uint256 transactionNumber; 69 | uint256 treeLevel; 70 | for (uint32 i = 32; i <= uint32(proof.length); i += 33) { 71 | assembly { 72 | loc := proof 73 | elLoc := add(loc, add(i, 1)) 74 | elProvided := mload(elLoc) 75 | } 76 | rightElementProvided = uint8(bytes1(0xff)&proof[i-32]); 77 | if (rightElementProvided > 0) { 78 | h = keccak256(abi.encodePacked(h, elProvided)); 79 | } else { 80 | transactionNumber += 1 << treeLevel; 81 | h = keccak256(abi.encodePacked(elProvided, h)); 82 | } 83 | treeLevel++; 84 | } 85 | if (h == root) { 86 | return (true, transactionNumber); 87 | } 88 | return (false, 0); 89 | } 90 | 91 | // function plasmaTransactionFromBytes(bytes _rawTX) internal view returns (PlasmaTransaction memory TX) { 92 | // RLP.RLPItem memory item = _rawTX.toRLPItem(); 93 | // if (!item._validate()) { 94 | // return constructEmptyTransaction(); 95 | // } 96 | // if (!item.isList()) { 97 | // return constructEmptyTransaction(); 98 | // } 99 | // uint256 numItems = item.items(); 100 | // if (numItems != 2) { 101 | // return constructEmptyTransaction(); 102 | // } 103 | // RLP.Iterator memory iter = item.iterator(); 104 | // item = iter.next(); 105 | // (uint256 numInBlock, bool valid) = item.toUint(TxNumberLength); 106 | // if (!valid) { 107 | // return constructEmptyTransaction(); 108 | // } 109 | // item = iter.next(); 110 | // TX = signedPlasmaTransactionFromRLPItem(item); 111 | // if (!TX.isWellFormed) { 112 | // return constructEmptyTransaction(); 113 | // } 114 | // TX.txNumberInBlock = uint32(numInBlock); 115 | // return TX; 116 | // } 117 | 118 | function signedPlasmaTransactionFromBytes(bytes _rawLimboTX) internal view returns (PlasmaTransaction memory TX) { 119 | RLP.RLPItem memory item = _rawLimboTX.toRLPItem(); 120 | if (!item._validate()) { 121 | return constructEmptyTransaction(); 122 | } 123 | TX = signedPlasmaTransactionFromRLPItem(item); 124 | if (!TX.isWellFormed) { 125 | return constructEmptyTransaction(); 126 | } 127 | return TX; 128 | } 129 | 130 | function signedPlasmaTransactionFromRLPItem(RLP.RLPItem memory _item) internal view returns (PlasmaTransaction memory TX) { 131 | if (!_item.isList()) { 132 | return constructEmptyTransaction(); 133 | } 134 | uint256 numItems = _item.items(); 135 | if (numItems != 4) { 136 | return constructEmptyTransaction(); 137 | } 138 | RLP.Iterator memory iter = _item.iterator(); 139 | RLP.RLPItem memory item = iter.next(); 140 | bytes memory rawSignedPart = item.toBytes(); 141 | bytes32 persMessageHashWithoutNumber = createPersonalMessageTypeHash(rawSignedPart); 142 | TX = plasmaTransactionFromRLPItem(item); 143 | if (!TX.isWellFormed) { 144 | return constructEmptyTransaction(); 145 | } 146 | item = iter.next(); 147 | (uint256 v_tmp, bool valid) = item.toUint(1); 148 | uint8 v = uint8(v_tmp); 149 | if (!valid) { 150 | return constructEmptyTransaction(); 151 | } 152 | item = iter.next(); 153 | bytes32 r; 154 | (r, valid) = item.toBytes32(); 155 | if (!valid) { 156 | return constructEmptyTransaction(); 157 | } 158 | item = iter.next(); 159 | bytes32 s; 160 | (s, valid) = item.toBytes32(); 161 | if (!valid) { 162 | return constructEmptyTransaction(); 163 | } 164 | TX.sender = ecrecover(persMessageHashWithoutNumber, v, r, s); 165 | if (TX.sender == address(0)) { 166 | return constructEmptyTransaction(); 167 | } 168 | return TX; 169 | } 170 | 171 | function plasmaTransactionFromRLPItem(RLP.RLPItem memory _item) internal pure returns (PlasmaTransaction memory TX) { 172 | if (!_item.isList()) { 173 | return constructEmptyTransaction(); 174 | } 175 | uint256 numItems = _item.items(); 176 | if (numItems != 3) { 177 | return constructEmptyTransaction(); 178 | } 179 | RLP.Iterator memory iter = _item.iterator(); 180 | if (!iter.hasNext()) { 181 | return constructEmptyTransaction(); 182 | } 183 | RLP.RLPItem memory item = iter.next(); // transaction type 184 | bool reusableValidFlag = false; 185 | uint256[] memory reusableSpace = new uint256[](7); 186 | (reusableSpace[0], reusableValidFlag) = item.toUint(TxTypeLength); //hardcode can be used 187 | uint256 txType = reusableSpace[0]; 188 | if (!(txType == TxTypeFund || txType == TxTypeSplit || txType == TxTypeMerge) ) { 189 | return constructEmptyTransaction(); 190 | } 191 | if (!reusableValidFlag) { 192 | return constructEmptyTransaction(); 193 | } 194 | item = iter.next(); 195 | if (!item.isList()) { 196 | return constructEmptyTransaction(); 197 | } 198 | numItems = item.items(); 199 | if (numItems == 0) { 200 | return constructEmptyTransaction(); 201 | } 202 | RLP.Iterator memory reusableIterator = item.iterator(); 203 | RLP.Iterator memory reusableIteratorPerItem; 204 | TransactionInput[] memory inputs = new TransactionInput[](numItems); 205 | reusableSpace[1] = 0; 206 | while (reusableIterator.hasNext()) { // go over the inputs 207 | item = reusableIterator.next(); 208 | if (!item.isList()) { 209 | return constructEmptyTransaction(); 210 | } 211 | numItems = item.items(); 212 | if (numItems != 4) { 213 | return constructEmptyTransaction(); 214 | } 215 | reusableIteratorPerItem = item.iterator(); 216 | (reusableSpace[2], reusableValidFlag) = reusableIteratorPerItem.next().toUint(BlockNumberLength); // block number 217 | if (!reusableValidFlag) { 218 | return constructEmptyTransaction(); 219 | } 220 | (reusableSpace[3], reusableValidFlag) = reusableIteratorPerItem.next().toUint(TxNumberLength); // tx number in block 221 | if (!reusableValidFlag) { 222 | return constructEmptyTransaction(); 223 | } 224 | (reusableSpace[4], reusableValidFlag) = reusableIteratorPerItem.next().toUint(TxOutputNumberLength); //tx output number in tx 225 | if (!reusableValidFlag) { 226 | return constructEmptyTransaction(); 227 | } 228 | (reusableSpace[5], reusableValidFlag) = reusableIteratorPerItem.next().toUint(32); //tx amount 229 | if (!reusableValidFlag) { 230 | return constructEmptyTransaction(); 231 | } 232 | TransactionInput memory input = TransactionInput({ 233 | blockNumber: uint32(reusableSpace[2]), 234 | txNumberInBlock: uint32(reusableSpace[3]), 235 | outputNumberInTX: uint8(reusableSpace[4]), 236 | amount: reusableSpace[5] 237 | }); 238 | inputs[reusableSpace[1]] = input; 239 | reusableSpace[1]++; 240 | } // now we have completed parsing all the inputs 241 | if (!iter.hasNext()) { 242 | return constructEmptyTransaction(); 243 | } 244 | item = iter.next(); 245 | if(!item.isList()){ 246 | return constructEmptyTransaction(); 247 | } 248 | reusableIterator = item.iterator(); 249 | numItems = item.items(); 250 | if (numItems == 0) { 251 | return constructEmptyTransaction(); 252 | } 253 | TransactionOutput[] memory outputs = new TransactionOutput[](numItems); 254 | reusableSpace[1] = 0; 255 | address reusableRecipient; 256 | while (reusableIterator.hasNext()) { // go over outputs 257 | item = reusableIterator.next(); 258 | if (!item.isList()) { 259 | return constructEmptyTransaction(); 260 | } 261 | numItems = item.items(); 262 | if (numItems != 3) { 263 | return constructEmptyTransaction(); 264 | } 265 | reusableIteratorPerItem = item.iterator(); 266 | if (!reusableIteratorPerItem.hasNext()) { 267 | return constructEmptyTransaction(); 268 | } 269 | (reusableSpace[2], reusableValidFlag) = reusableIteratorPerItem.next().toUint(TxOutputNumberLength); // output numbber 270 | if (!reusableValidFlag) { 271 | return constructEmptyTransaction(); 272 | } 273 | (reusableRecipient, reusableValidFlag) = reusableIteratorPerItem.next().toAddress(); // recipient 274 | if (!reusableValidFlag) { 275 | return constructEmptyTransaction(); 276 | } 277 | (reusableSpace[3], reusableValidFlag) = reusableIteratorPerItem.next().toUint(32); //amount 278 | if (!reusableValidFlag) { 279 | return constructEmptyTransaction(); 280 | } 281 | TransactionOutput memory output = TransactionOutput({ 282 | outputNumberInTX: uint8(reusableSpace[2]), 283 | recipient: reusableRecipient, 284 | amount: reusableSpace[3] 285 | }); 286 | outputs[reusableSpace[1]] = output; 287 | reusableSpace[1]++; 288 | } 289 | TX = PlasmaTransaction({ 290 | txNumberInBlock: 0, 291 | txType: uint8(reusableSpace[0]), 292 | inputs: inputs, 293 | outputs: outputs, 294 | sender: address(0), 295 | isWellFormed: true 296 | }); 297 | return TX; 298 | } 299 | 300 | function makeTransactionIndex(uint32 _blockNumber, uint32 _txNumberInBlock) internal pure returns (uint64 index) { 301 | index += (uint64(_blockNumber) << (TxNumberLength*8)); 302 | index += uint64(_txNumberInBlock); 303 | return index; 304 | } 305 | 306 | function parseTransactionIndex(uint64 _index) internal pure returns (uint32 blockNumber, uint32 txNumberInBlock) { 307 | uint64 idx = _index; 308 | txNumberInBlock = uint32(idx % (uint64(1) << TxNumberLength*8)); 309 | idx = idx >> (TxNumberLength*8); 310 | blockNumber = uint32(idx); 311 | return (blockNumber, txNumberInBlock); 312 | } 313 | 314 | function makeInputOrOutputIndex(uint32 _blockNumber, uint32 _txNumberInBlock, uint8 _outputNumberInTX) internal pure returns (uint72 index) { 315 | index += ( uint72(_blockNumber) << ((TxNumberLength + TxOutputNumberLength)*8) ); 316 | index += ( uint72(_txNumberInBlock) << (TxOutputNumberLength*8) ); 317 | index += uint72(_outputNumberInTX); 318 | return index; 319 | } 320 | 321 | function parseInputOrOutputIndex(uint72 _index) internal pure returns (uint32 blockNumber, uint32 txNumberInBlock, uint8 outputNumber) { 322 | uint256 idx = _index; 323 | outputNumber = uint8(idx % (uint72(1) << TxOutputNumberLength*8)); 324 | idx = idx >> (TxOutputNumberLength*8); 325 | txNumberInBlock = uint32(idx % (uint72(1) << TxNumberLength*8)); 326 | idx = idx >> (TxNumberLength*8); 327 | blockNumber = uint32(idx % (uint72(1) << BlockNumberLength*8)); 328 | return (blockNumber, txNumberInBlock, outputNumber); 329 | } 330 | 331 | function constructEmptyTransaction() internal pure returns (PlasmaTransaction memory TX) { 332 | TransactionInput[] memory inputs = new TransactionInput[](0); 333 | TransactionOutput[] memory outputs = new TransactionOutput[](0); 334 | TX = PlasmaTransaction({ 335 | txNumberInBlock: 0, 336 | txType: 0, 337 | sender: address(0), 338 | inputs: inputs, 339 | outputs: outputs, 340 | isWellFormed: false 341 | }); 342 | } 343 | } -------------------------------------------------------------------------------- /contracts/PriorityQueue.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.25; 2 | 3 | // original source from https://github.com/DavidKnott 4 | // https://github.com/omisego/plasma-mvp/blob/master/plasma/root_chain/contracts/RootChain/RootChain.sol 5 | 6 | import {SafeMath} from "./SafeMath.sol"; 7 | 8 | interface PriorityQueueInterface { 9 | function insert(uint72 _priority, bytes22 _partialHash) external; 10 | function minChild(uint256 i) view external returns (uint256); 11 | function getMin() external view returns (bytes22 partialHash); 12 | function delMin() external returns (bytes22 partialHash); 13 | function currentSize() external returns (uint256); 14 | } 15 | 16 | contract PriorityQueue { 17 | using SafeMath for uint256; 18 | /* 19 | * Modifiers 20 | */ 21 | modifier onlyOwner() { 22 | require(msg.sender == owner); 23 | _; 24 | } 25 | 26 | function setOwner (address _newOwner) onlyOwner public { 27 | require(_newOwner != address(0)); 28 | owner = _newOwner; 29 | } 30 | 31 | /* 32 | * Storage 33 | */ 34 | 35 | struct QueueItem { 36 | uint72 priority; 37 | bytes22 partialHash; 38 | // 31 bytes 39 | } 40 | 41 | address public owner; 42 | QueueItem[] public heapList; 43 | uint256 public currentSize; 44 | 45 | constructor () public 46 | { 47 | owner = msg.sender; 48 | QueueItem memory item = QueueItem({ 49 | priority: 0, 50 | partialHash: bytes22(0) 51 | }); 52 | heapList.push(item); 53 | currentSize = 0; 54 | } 55 | 56 | function insert(uint72 _priority, bytes22 _index) 57 | public 58 | onlyOwner 59 | { 60 | heapList.push(QueueItem({ 61 | priority: _priority, 62 | partialHash: _index 63 | })); 64 | currentSize = currentSize.add(1); 65 | percUp(currentSize); 66 | } 67 | 68 | function minChild(uint256 i) 69 | public 70 | view 71 | returns (uint256) 72 | { 73 | if (i.mul(2).add(1) > currentSize) { 74 | return i.mul(2); 75 | } else { 76 | if (heapList[i.mul(2)].priority < heapList[i.mul(2).add(1)].priority) { 77 | return i.mul(2); 78 | } else { 79 | return i.mul(2).add(1); 80 | } 81 | } 82 | } 83 | 84 | function getMin() 85 | public 86 | view 87 | returns (bytes22 index) 88 | { 89 | return heapList[1].partialHash; 90 | } 91 | 92 | function delMin() 93 | public 94 | onlyOwner 95 | returns (bytes22 partialHash) 96 | { 97 | require(currentSize > 0); 98 | partialHash = heapList[1].partialHash; 99 | heapList[1] = heapList[currentSize]; 100 | delete heapList[currentSize]; 101 | currentSize = currentSize.sub(1); 102 | percDown(1); 103 | return partialHash; 104 | } 105 | 106 | function percUp(uint256 j) 107 | private 108 | { 109 | uint256 i = j; 110 | QueueItem memory tmp; 111 | while (i.div(2) > 0) { 112 | if (heapList[i].priority < heapList[i.div(2)].priority) { 113 | tmp = heapList[i.div(2)]; 114 | heapList[i.div(2)] = heapList[i]; 115 | heapList[i] = tmp; 116 | } 117 | i = i.div(2); 118 | } 119 | } 120 | 121 | function percDown(uint256 j) 122 | private 123 | { 124 | uint256 i = j; 125 | QueueItem memory tmp; 126 | while (i.mul(2) <= currentSize) { 127 | uint256 mc = minChild(i); 128 | if (heapList[i].priority > heapList[mc].priority) { 129 | tmp = heapList[i]; 130 | heapList[i] = heapList[mc]; 131 | heapList[mc] = tmp; 132 | } 133 | i = mc; 134 | } 135 | } 136 | } -------------------------------------------------------------------------------- /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 | * updated by 8 | * @author Alex Vlasov (alex.m.vlasov@gmail.com) 9 | */ 10 | pragma solidity ^0.4.25; 11 | 12 | library RLP { 13 | 14 | uint256 constant DATA_SHORT_START = 0x80; 15 | uint256 constant DATA_LONG_START = 0xB8; 16 | uint256 constant LIST_SHORT_START = 0xC0; 17 | uint256 constant LIST_LONG_START = 0xF8; 18 | 19 | uint256 constant DATA_LONG_OFFSET = 0xB7; 20 | uint256 constant LIST_LONG_OFFSET = 0xF7; 21 | 22 | 23 | struct RLPItem { 24 | uint256 _unsafe_memPtr; // Pointer to the RLP-encoded bytes. 25 | uint256 _unsafe_length; // Number of bytes. This is the full length of the string. 26 | } 27 | 28 | struct Iterator { 29 | RLPItem _unsafe_item; // Item that's being iterated over. 30 | uint256 _unsafe_nextPtr; // Position of the next item in the list. 31 | } 32 | 33 | /* Iterator */ 34 | 35 | function next(Iterator memory self) internal pure returns (RLPItem memory subItem) { 36 | if (hasNext(self)) { 37 | uint256 ptr = self._unsafe_nextPtr; 38 | uint256 itemLength = _itemLength(ptr); 39 | subItem._unsafe_memPtr = ptr; 40 | subItem._unsafe_length = itemLength; 41 | self._unsafe_nextPtr = ptr + itemLength; 42 | } 43 | else 44 | revert(); 45 | } 46 | 47 | function next(Iterator memory self, bool strict) internal pure returns (RLPItem memory subItem) { 48 | subItem = next(self); 49 | if (strict && !_validate(subItem)) 50 | revert(); 51 | return; 52 | } 53 | 54 | function hasNext(Iterator memory self) internal pure returns (bool) { 55 | RLPItem memory item = self._unsafe_item; 56 | return self._unsafe_nextPtr < item._unsafe_memPtr + item._unsafe_length; 57 | } 58 | 59 | /* RLPItem */ 60 | 61 | /// @dev Creates an RLPItem from an array of RLP encoded bytes. 62 | /// @param self The RLP encoded bytes. 63 | /// @return An RLPItem 64 | function toRLPItem(bytes memory self) internal pure returns (RLPItem memory) { 65 | uint256 len = self.length; 66 | if (len == 0) { 67 | return RLPItem(0, 0); 68 | } 69 | uint256 memPtr; 70 | assembly { 71 | memPtr := add(self, 0x20) 72 | } 73 | return RLPItem(memPtr, len); 74 | } 75 | 76 | /// @dev Creates an RLPItem from an array of RLP encoded bytes. 77 | /// @param self The RLP encoded bytes. 78 | /// @param strict Will revert() if the data is not RLP encoded. 79 | /// @return An RLPItem 80 | function toRLPItem(bytes memory self, bool strict) internal pure returns (RLPItem memory) { 81 | RLPItem memory item = toRLPItem(self); 82 | if (strict) { 83 | uint256 len = self.length; 84 | if (_payloadOffset(item) > len) 85 | revert(); 86 | if (_itemLength(item._unsafe_memPtr) != len) 87 | revert(); 88 | if (!_validate(item)) 89 | revert(); 90 | } 91 | return item; 92 | } 93 | 94 | /// @dev Check if the RLP item is null. 95 | /// @param self The RLP item. 96 | /// @return 'true' if the item is null. 97 | function isNull(RLPItem memory self) internal pure returns (bool ret) { 98 | return self._unsafe_length == 0; 99 | } 100 | 101 | /// @dev Check if the RLP item is a list. 102 | /// @param self The RLP item. 103 | /// @return 'true' if the item is a list. 104 | function isList(RLPItem memory self) internal pure returns (bool ret) { 105 | if (self._unsafe_length == 0) 106 | return false; 107 | uint256 memPtr = self._unsafe_memPtr; 108 | assembly { 109 | ret := iszero(lt(byte(0, mload(memPtr)), 0xC0)) 110 | } 111 | } 112 | 113 | /// @dev Check if the RLP item is data. 114 | /// @param self The RLP item. 115 | /// @return 'true' if the item is data. 116 | function isData(RLPItem memory self) internal pure returns (bool ret) { 117 | if (self._unsafe_length == 0) 118 | return false; 119 | uint256 memPtr = self._unsafe_memPtr; 120 | assembly { 121 | ret := lt(byte(0, mload(memPtr)), 0xC0) 122 | } 123 | } 124 | 125 | /// @dev Check if the RLP item is empty (string or list). 126 | /// @param self The RLP item. 127 | /// @return 'true' if the item is null. 128 | function isEmpty(RLPItem memory self) internal pure returns (bool ret) { 129 | if (isNull(self)) 130 | return false; 131 | uint256 b0; 132 | uint256 memPtr = self._unsafe_memPtr; 133 | assembly { 134 | b0 := byte(0, mload(memPtr)) 135 | } 136 | return (b0 == DATA_SHORT_START || b0 == LIST_SHORT_START); 137 | } 138 | 139 | /// @dev Get the number of items in an RLP encoded list. 140 | /// @param self The RLP item. 141 | /// @return The number of items. 142 | function items(RLPItem memory self) internal pure returns (uint) { 143 | if (!isList(self)) 144 | return 0; 145 | uint256 b0; 146 | uint256 memPtr = self._unsafe_memPtr; 147 | assembly { 148 | b0 := byte(0, mload(memPtr)) 149 | } 150 | uint256 pos = memPtr + _payloadOffset(self); 151 | uint256 last = memPtr + self._unsafe_length - 1; 152 | uint256 itms; 153 | while(pos <= last) { 154 | pos += _itemLength(pos); 155 | itms++; 156 | } 157 | return itms; 158 | } 159 | 160 | /// @dev Create an iterator. 161 | /// @param self The RLP item. 162 | /// @return An 'Iterator' over the item. 163 | function iterator(RLPItem memory self) internal pure returns (Iterator memory it) { 164 | if (!isList(self)) 165 | return; 166 | // revert(); 167 | uint256 ptr = self._unsafe_memPtr + _payloadOffset(self); 168 | it._unsafe_item = self; 169 | it._unsafe_nextPtr = ptr; 170 | } 171 | 172 | /// @dev Return the RLP encoded bytes. 173 | /// @param self The RLPItem. 174 | /// @return The bytes. 175 | function toBytes(RLPItem memory self) internal view returns (bytes memory bts) { 176 | uint256 len = self._unsafe_length; 177 | if (len == 0) 178 | return; 179 | bts = new bytes(len); 180 | _copyToBytes(self._unsafe_memPtr, bts, len); 181 | } 182 | 183 | /// @dev Decode an RLPItem into bytes. This will not work if the 184 | /// RLPItem is a list. 185 | /// @param self The RLPItem. 186 | /// @return The decoded string. 187 | function toData(RLPItem memory self) internal view returns (bytes memory bts) { 188 | if (!isData(self)) 189 | return; 190 | (uint256 rStartPos, uint256 len) = _decode(self); 191 | bts = new bytes(len); 192 | _copyToBytes(rStartPos, bts, len); 193 | } 194 | 195 | /// @dev Get the list of sub-items from an RLP encoded list. 196 | /// Warning: This is inefficient, as it requires that the list is read twice. 197 | /// @param self The RLP item. 198 | /// @return Array of RLPItems. 199 | function toList(RLPItem memory self) internal pure returns (RLPItem[] memory list) { 200 | if (!isList(self)) 201 | return; 202 | // revert(); 203 | uint256 numItems = items(self); 204 | list = new RLPItem[](numItems); 205 | Iterator memory it = iterator(self); 206 | uint256 idx; 207 | while(hasNext(it)) { 208 | list[idx] = next(it); 209 | idx++; 210 | } 211 | } 212 | 213 | /// @dev Decode an RLPItem into an ascii string. This will not work if the 214 | /// RLPItem is a list. 215 | /// @param self The RLPItem. 216 | /// @return The decoded string. 217 | function toAscii(RLPItem memory self) internal view returns (string memory str) { 218 | if (!isData(self)) 219 | return; 220 | // revert(); 221 | (uint256 rStartPos, uint256 len) = _decode(self); 222 | bytes memory bts = new bytes(len); 223 | _copyToBytes(rStartPos, bts, len); 224 | str = string(bts); 225 | } 226 | 227 | /// @dev Decode an RLPItem into a uint. This will not work if the 228 | /// RLPItem is a list. 229 | /// @param self The RLPItem. 230 | /// @return The decoded string. 231 | function toUint(RLPItem memory self, uint256 maxLength) internal pure returns (uint256 data, bool valid) { 232 | if (!isData(self)) { 233 | return (0, false); 234 | } 235 | (uint256 rStartPos, uint256 len) = _decode(self); 236 | if (len > 32 || len == 0 || len > maxLength) { 237 | return (0, false); 238 | } 239 | assembly { 240 | data := div(mload(rStartPos), exp(256, sub(32, len))) 241 | } 242 | return (data, true); 243 | } 244 | 245 | /// @dev Decode an RLPItem into a boolean. This will not work if the 246 | /// RLPItem is a list. 247 | /// @param self The RLPItem. 248 | /// @return The decoded string. 249 | function toBool(RLPItem memory self) internal pure returns (bool data, bool valid) { 250 | if (!isData(self)) 251 | return; 252 | (uint256 rStartPos, uint256 len) = _decode(self); 253 | if (len != 1) 254 | return; 255 | uint256 temp; 256 | assembly { 257 | temp := byte(0, mload(rStartPos)) 258 | } 259 | if (temp > 1) { 260 | return; 261 | } else if (temp == 1) { 262 | data = true; 263 | } else { 264 | data = false; 265 | } 266 | return (data, true); 267 | } 268 | 269 | /// @dev Decode an RLPItem into a byte. This will not work if the 270 | /// RLPItem is a list. 271 | /// @param self The RLPItem. 272 | /// @return The decoded string. 273 | function toByte(RLPItem memory self) internal pure returns (byte data, bool valid) { 274 | if (!isData(self)) 275 | return; 276 | (uint256 rStartPos, uint256 len) = _decode(self); 277 | if (len != 1) 278 | return; 279 | uint256 temp; 280 | assembly { 281 | temp := byte(0, mload(rStartPos)) 282 | } 283 | return (byte(temp), true); 284 | } 285 | 286 | /// @dev Decode an RLPItem into an int. This will not work if the 287 | /// RLPItem is a list. 288 | /// @param self The RLPItem. 289 | /// @return The decoded string. 290 | function toInt(RLPItem memory self, uint256 maxLength) internal pure returns (int256 data, bool valid) { 291 | (uint256 num, bool val) = toUint(self, maxLength); 292 | if (!val) { 293 | return; 294 | } 295 | return (int256(num), val); 296 | } 297 | 298 | /// @dev Decode an RLPItem into a bytes32. This will not work if the 299 | /// RLPItem is a list. 300 | /// @param self The RLPItem. 301 | /// @return The decoded string. 302 | function toBytes32(RLPItem memory self) internal pure returns (bytes32 data, bool valid) { 303 | (uint256 num, bool val) = toUint(self, 32); 304 | if (!val) { 305 | return; 306 | } 307 | return (bytes32(num), val); 308 | } 309 | 310 | /// @dev Decode an RLPItem into an address. This will not work if the 311 | /// RLPItem is a list. 312 | /// @param self The RLPItem. 313 | /// @return The decoded string. 314 | function toAddress(RLPItem memory self) internal pure returns (address data, bool valid) { 315 | if (!isData(self)) 316 | return; 317 | (uint256 rStartPos, uint256 len) = _decode(self); 318 | if (len != 20) 319 | return; 320 | assembly { 321 | data := div(mload(rStartPos), exp(256, 12)) 322 | } 323 | return (data, true); 324 | } 325 | 326 | // Get the payload offset. 327 | function _payloadOffset(RLPItem memory self) private pure returns (uint) { 328 | if (self._unsafe_length == 0) 329 | return 0; 330 | uint256 b0; 331 | uint256 memPtr = self._unsafe_memPtr; 332 | assembly { 333 | b0 := byte(0, mload(memPtr)) 334 | } 335 | if (b0 < DATA_SHORT_START) 336 | return 0; 337 | if (b0 < DATA_LONG_START || (b0 >= LIST_SHORT_START && b0 < LIST_LONG_START)) 338 | return 1; 339 | if (b0 < LIST_SHORT_START) 340 | return b0 - DATA_LONG_OFFSET + 1; 341 | return b0 - LIST_LONG_OFFSET + 1; 342 | } 343 | 344 | // Get the full length of an RLP item. 345 | function _itemLength(uint256 memPtr) private pure returns (uint256 len) { 346 | uint256 b0; 347 | assembly { 348 | b0 := byte(0, mload(memPtr)) 349 | } 350 | if (b0 < DATA_SHORT_START) 351 | len = 1; 352 | else if (b0 < DATA_LONG_START) 353 | len = b0 - DATA_SHORT_START + 1; 354 | else if (b0 < LIST_SHORT_START) { 355 | assembly { 356 | let bLen := sub(b0, 0xB7) // bytes length (DATA_LONG_OFFSET) 357 | let dLen := div(mload(add(memPtr, 1)), exp(256, sub(32, bLen))) // data length 358 | len := add(1, add(bLen, dLen)) // total length 359 | } 360 | } 361 | else if (b0 < LIST_LONG_START) 362 | len = b0 - LIST_SHORT_START + 1; 363 | else { 364 | assembly { 365 | let bLen := sub(b0, 0xF7) // bytes length (LIST_LONG_OFFSET) 366 | let dLen := div(mload(add(memPtr, 1)), exp(256, sub(32, bLen))) // data length 367 | len := add(1, add(bLen, dLen)) // total length 368 | } 369 | } 370 | } 371 | 372 | // Get start position and length of the data. 373 | function _decode(RLPItem memory self) private pure returns (uint256 memPtr, uint256 len) { 374 | if (!isData(self)) 375 | return; 376 | uint256 b0; 377 | uint256 start = self._unsafe_memPtr; 378 | assembly { 379 | b0 := byte(0, mload(start)) 380 | } 381 | if (b0 < DATA_SHORT_START) { 382 | memPtr = start; 383 | len = 1; 384 | return; 385 | } 386 | if (b0 < DATA_LONG_START) { 387 | len = self._unsafe_length - 1; 388 | memPtr = start + 1; 389 | } else { 390 | uint256 bLen; 391 | assembly { 392 | bLen := sub(b0, 0xB7) // DATA_LONG_OFFSET 393 | } 394 | len = self._unsafe_length - 1 - bLen; 395 | memPtr = start + bLen + 1; 396 | } 397 | return; 398 | } 399 | 400 | // Assumes that enough memory has been allocated to store in target. 401 | function _copyToBytes(uint256 btsPtr, bytes memory tgt, uint256 btsLen) private view { 402 | // Exploiting the fact that 'tgt' was the last thing to be allocated, 403 | // we can write entire words, and just overwrite any excess. 404 | assembly { 405 | { 406 | let i := 0 // Start at arr + 0x20 407 | let words := div(add(btsLen, 31), 32) 408 | let rOffset := btsPtr 409 | let wOffset := add(tgt, 0x20) 410 | tag_loop: 411 | jumpi(end, eq(i, words)) 412 | { 413 | let offset := mul(i, 0x20) 414 | mstore(add(wOffset, offset), mload(add(rOffset, offset))) 415 | i := add(i, 1) 416 | } 417 | jump(tag_loop) 418 | end: 419 | mstore(add(tgt, add(0x20, mload(tgt))), 0) 420 | } 421 | } 422 | } 423 | 424 | // Check that an RLP item is valid. 425 | function _validate(RLPItem memory self) internal pure returns (bool ret) { 426 | // Check that RLP is well-formed. 427 | uint256 b0; 428 | uint256 b1; 429 | uint256 memPtr = self._unsafe_memPtr; 430 | assembly { 431 | b0 := byte(0, mload(memPtr)) 432 | b1 := byte(1, mload(memPtr)) 433 | } 434 | if (b0 == DATA_SHORT_START + 1 && b1 < DATA_SHORT_START) 435 | return false; 436 | return true; 437 | } 438 | } -------------------------------------------------------------------------------- /contracts/SafeMath.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.25; 2 | 3 | library SafeMath { 4 | function mul(uint256 a, uint256 b) internal pure returns (uint256) { 5 | uint256 c = a * b; 6 | assert(a == 0 || c / a == b); 7 | return c; 8 | } 9 | 10 | function div(uint256 a, uint256 b) internal pure returns (uint256) { 11 | uint256 c = a / b; 12 | return c; 13 | } 14 | 15 | function sub(uint256 a, uint256 b) internal pure returns (uint256) { 16 | assert(b <= a); 17 | return a - b; 18 | } 19 | 20 | function add(uint256 a, uint256 b) internal pure returns (uint256) { 21 | uint256 c = a + b; 22 | assert(c >= a); 23 | return c; 24 | } 25 | } -------------------------------------------------------------------------------- /contracts/Structures.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.25; 2 | 3 | library StructuresLibrary { 4 | struct ExitRecord { 5 | bytes32 transactionRef; 6 | //32 bytes 7 | uint256 amount; 8 | // 64 bytes 9 | address owner; 10 | uint64 timePublished; 11 | uint32 blockNumber; 12 | // 96 bytes 13 | uint32 transactionNumber; 14 | uint8 outputNumber; 15 | bool isValid; 16 | bool isLimbo; 17 | // 96 + 7 bytes 18 | uint72 priority; 19 | // 96 + 16 bytes 20 | } 21 | 22 | struct LimboData { 23 | LimboInputChallenge[] inputChallenges; 24 | LimboOutput[] outputs; 25 | } 26 | 27 | struct LimboInputChallenge { 28 | address from; 29 | uint8 inputNumber; 30 | bool resolved; 31 | } 32 | 33 | struct LimboOutput { 34 | uint256 amount; 35 | address owner; 36 | bool isPegged; 37 | } 38 | 39 | function getCompactExitRecordCommitment(ExitRecord self) internal pure returns(bytes22 commitment) { 40 | return bytes22(keccak256(abi.encodePacked(self.transactionRef, self.blockNumber, self.transactionNumber, self.outputNumber, self.isLimbo))); 41 | } 42 | 43 | function isFullyResolved(LimboData storage self) internal view returns (bool resolved) { 44 | for (uint8 i = 0; i < self.inputChallenges.length; i++) { 45 | if (self.inputChallenges[i].resolved == false) { 46 | return false; 47 | } 48 | } 49 | return true; 50 | } 51 | 52 | function addOutput(LimboData storage self, address _owner, uint256 _amount, bool _pegged) internal { 53 | LimboOutput memory newOutput; 54 | newOutput.owner = _owner; 55 | newOutput.amount = _amount; 56 | newOutput.isPegged = _pegged; 57 | self.outputs.push(newOutput); 58 | } 59 | } -------------------------------------------------------------------------------- /contracts/TXTester.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.25; 2 | 3 | import {PlasmaTransactionLibrary} from "./PlasmaTransactionLibrary.sol"; 4 | 5 | contract TXTester { 6 | constructor() public { 7 | 8 | } 9 | 10 | function parseFromBlock(bytes _plasmaTransaction, bytes _merkleProof, bytes32 _merkleRoot) public view returns (uint32 txNum, uint8 txType, uint numIns, uint numOuts, address sender, bool isWellFormed ) { 11 | PlasmaTransactionLibrary.PlasmaTransaction memory TX = PlasmaTransactionLibrary.signedPlasmaTransactionFromBytes(_plasmaTransaction); 12 | (bool included, uint256 transactionNumber) = PlasmaTransactionLibrary.checkForInclusionIntoBlock(_merkleRoot, _plasmaTransaction, _merkleProof); 13 | require(included); 14 | TX.txNumberInBlock = uint32(transactionNumber); 15 | return (TX.txNumberInBlock, TX.txType, TX.inputs.length, TX.outputs.length, TX.sender, TX.isWellFormed); 16 | } 17 | 18 | function parseFromBlockLimited(bytes _plasmaTransaction, bytes _merkleProof, bytes32 _merkleRoot) public pure returns (bool included, uint256 transactionNumber) { 19 | (included, transactionNumber) = PlasmaTransactionLibrary.checkForInclusionIntoBlock(_merkleRoot, _plasmaTransaction, _merkleProof); 20 | return; 21 | } 22 | 23 | function parseTransaction(bytes _plasmaTransaction) public view returns (uint32 txNum, uint8 txType, uint numIns, uint numOuts, address sender, bool isWellFormed ) { 24 | PlasmaTransactionLibrary.PlasmaTransaction memory TX = PlasmaTransactionLibrary.signedPlasmaTransactionFromBytes(_plasmaTransaction); 25 | return (TX.txNumberInBlock, TX.txType, TX.inputs.length, TX.outputs.length, TX.sender, TX.isWellFormed); 26 | } 27 | 28 | function getInputInfo(bytes _plasmaTransaction, uint8 _inputNumber) public view returns (uint32 blockNumber, uint32 txNumberInBlock, uint8 outputNumberInTx, uint amount) { 29 | PlasmaTransactionLibrary.PlasmaTransaction memory TX = PlasmaTransactionLibrary.signedPlasmaTransactionFromBytes(_plasmaTransaction); 30 | PlasmaTransactionLibrary.TransactionInput memory input = TX.inputs[_inputNumber]; 31 | return (input.blockNumber, input.txNumberInBlock, input.outputNumberInTX, input.amount); 32 | } 33 | 34 | function getOutputInfo(bytes _plasmaTransaction, uint8 _outputNumber) public view returns (uint8 outputNumberInTx, address recipient, uint amount) { 35 | PlasmaTransactionLibrary.PlasmaTransaction memory TX = PlasmaTransactionLibrary.signedPlasmaTransactionFromBytes(_plasmaTransaction); 36 | PlasmaTransactionLibrary.TransactionOutput memory output = TX.outputs[_outputNumber]; 37 | return (output.outputNumberInTX, output.recipient, output.amount); 38 | } 39 | } -------------------------------------------------------------------------------- /helpers/assertHelpers.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | init: function() { 4 | if (assert !== undefined) { 5 | assert.web3Events = function(observedTransactionResult, expectedEvents, message) { 6 | let entries = observedTransactionResult.logs.map(function(logEntry) { 7 | return { 8 | event: logEntry.event, 9 | args: Object.keys(logEntry.args).reduce(function(previous, current) { 10 | previous[current] = 11 | (typeof logEntry.args[current].toNumber === 'function') 12 | ? logEntry.args[current].toNumber() 13 | : logEntry.args[current]; 14 | return previous; 15 | }, {}) 16 | } 17 | }); 18 | assert.deepEqual(entries, expectedEvents, message); 19 | }; 20 | 21 | assert.web3Event = function(observedTransactionResult, expectedEvent, message) { 22 | assert.web3Events(observedTransactionResult, [expectedEvent], message); 23 | }; 24 | } 25 | 26 | if (expect !== undefined) { 27 | expect.web3Events = function(observedTransactionResult, expectedEvents, message) { 28 | let entries = observedTransactionResult.logs.map(function(logEntry) { 29 | return { 30 | event: logEntry.event, 31 | args: Object.keys(logEntry.args).reduce(function(previous, current) { 32 | previous[current] = 33 | (typeof logEntry.args[current].toNumber === 'function') 34 | ? logEntry.args[current].toNumber() 35 | : logEntry.args[current]; 36 | return previous; 37 | }, {}) 38 | } 39 | }); 40 | // expect({a: 1}).to.deep.equal({a: 1}); 41 | // expect.deepEqual(entries, expectedEvents, message); 42 | expect(entries).to.deep.equal(expectedEvents); 43 | }; 44 | 45 | expect.web3Event = function(observedTransactionResult, expectedEvent, message) { 46 | expect.web3Events(observedTransactionResult, [expectedEvent], message); 47 | }; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /helpers/details.js: -------------------------------------------------------------------------------- 1 | let fs = require('fs'); 2 | let contract = require("../build/contracts/PlasmaParent.json"); 3 | let details = {error: false, address: contract.networks[4].address, abi: contract.abi}; 4 | fs.writeFile("../build/details", JSON.stringify(details), err => { 5 | if (err) throw err; 6 | console.log('Complete. Contract address: ' + details.address); 7 | } 8 | ); 9 | -------------------------------------------------------------------------------- /helpers/expectThrow.js: -------------------------------------------------------------------------------- 1 | module.exports = async function expectThrow(promise) { 2 | try { 3 | await promise; 4 | } catch (error) { 5 | // TODO: Check jump destination to destinguish between a throw 6 | // and an actual invalid jump. 7 | const invalidJump = error.message.search('invalid JUMP') >= 0; 8 | // TODO: When we contract A calls contract B, and B throws, instead 9 | // of an 'invalid jump', we get an 'out of gas' error. How do 10 | // we distinguish this from an actual out of gas event? (The 11 | // testrpc log actually show an 'invalid jump' event.) 12 | const outOfGas = error.message.search('out of gas') >= 0; 13 | // General revert 14 | const revert = error.message.search("VM Exception while processing transaction: revert") >= 0; 15 | assert( 16 | invalidJump || outOfGas || revert, 17 | "Expected throw, got '" + error + "' instead", 18 | ); 19 | return; 20 | } 21 | assert.fail('Expected throw not received'); 22 | }; -------------------------------------------------------------------------------- /helpers/increaseTime.js: -------------------------------------------------------------------------------- 1 | const increaseTime = async function(addSeconds) { 2 | await web3.currentProvider.send({jsonrpc: "2.0", method: "evm_increaseTime", params: [addSeconds], id: 0}) 3 | await web3.currentProvider.send({jsonrpc: "2.0", method: "evm_mine", params: [], id: 1}) 4 | } 5 | 6 | const increaseBlockCounter = async function(addBlocks) { 7 | for (let i = 0; i < addBlocks; i++) { 8 | await web3.currentProvider.send({jsonrpc: "2.0", method: "evm_mine", params: [], id: i}) 9 | } 10 | } 11 | 12 | module.exports.increaseTime = {increaseTime, increaseBlockCounter}; -------------------------------------------------------------------------------- /helpers/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ganache-cli --gasLimit=7e7 --defaultBalanceEther=10000 -m 'candy maple cake sugar pudding cream honey rich smooth crumble sweet treat' 4 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | var Migrations = artifacts.require("Migrations.sol"); 2 | 3 | module.exports = function(deployer, network, accounts) { 4 | return; 5 | deployer.deploy(Migrations); 6 | }; 7 | -------------------------------------------------------------------------------- /migrations/2_deploy_plasma.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const PlasmaParent = artifacts.require('PlasmaParent'); 3 | const PriorityQueue = artifacts.require('PriorityQueue'); 4 | const BlockStorage = artifacts.require("PlasmaBlockStorage"); 5 | const Challenger = artifacts.require("PlasmaChallenges"); 6 | const BuyoutProcessor = artifacts.require("PlasmaBuyoutProcessor"); 7 | // const LimboExitGame = artifacts.require("PlasmaLimboExitGame"); 8 | const assert = require('assert'); 9 | const _ = require('lodash'); 10 | 11 | var nonce; 12 | 13 | function sleep(ms) { 14 | return new Promise(resolve => setTimeout(resolve, ms)); 15 | } 16 | 17 | let blockSignerAddress = "0x627306090abab3a6e1400e9345bc60c78a8bef57" 18 | let operatorsCollateral = "1000000000000000000" // 1 ETH 19 | 20 | module.exports = function(deployer, network, accounts) { 21 | 22 | async function waitForNonceUpdate() { 23 | console.log("Waiting for nonce update") 24 | await sleep(60000) 25 | let newNonce = await getNonce(operator) 26 | while (newNonce <= nonce) { 27 | await sleep(60000) 28 | newNonce = await getNonce(operator) 29 | } 30 | } 31 | 32 | function getNonce(account) { 33 | return new Promise(function(resolve, reject) { 34 | web3.eth.getTransactionCount(account, function(error, nonce) { 35 | if (error !== null) { 36 | reject(error) 37 | } 38 | resolve(nonce) 39 | }) 40 | }); 41 | } 42 | 43 | const operator = accounts[0]; 44 | try { 45 | let env = process.env; 46 | if (env.NODE_ENV !== 'production') { 47 | require('dotenv').load(); 48 | } 49 | const blockSignerAddressCandidate = env.BLOCK_SIGNER_ADDRESS 50 | if (blockSignerAddressCandidate !== undefined && blockSignerAddressCandidate !== "") { 51 | blockSignerAddress = blockSignerAddressCandidate; 52 | } 53 | } 54 | catch(error) { 55 | console.log(error) 56 | } 57 | console.log("Block signer = " + blockSignerAddress); 58 | console.log("Operator's bond = " + operatorsCollateral + " wei"); 59 | 60 | (async () => { 61 | nonce = await getNonce(operator); 62 | await deployer.deploy(BlockStorage, {from: operator, gas: 2500000}); 63 | let storage = await BlockStorage.deployed(); 64 | console.log("Storage was deployed at " + storage.address); 65 | await waitForNonceUpdate(); 66 | 67 | await deployer.deploy(PriorityQueue, {from: operator, gas: 1500000}); 68 | let queue = await PriorityQueue.deployed(); 69 | console.log("Queue was deployed at " + queue.address); 70 | await waitForNonceUpdate(); 71 | 72 | await deployer.deploy(PlasmaParent, queue.address, storage.address, {from: operator, value: operatorsCollateral}); 73 | let parent = await PlasmaParent.deployed(); 74 | console.log("Parent was deployed at " + parent.address); 75 | await waitForNonceUpdate(); 76 | 77 | // let contractBalance = await web3.eth.getBalance(parent.address); 78 | // assert(contractBalance.toString(10) === operatorsCollateral); 79 | 80 | await storage.setOwner(parent.address, {from: operator, gas: 50000}); 81 | console.log("Has set storage owner") 82 | await waitForNonceUpdate(); 83 | 84 | await queue.setOwner(parent.address, {from: operator, gas: 50000}); 85 | console.log("Has set queue owner") 86 | await waitForNonceUpdate(); 87 | 88 | await deployer.deploy(BuyoutProcessor, {from: operator, gas: 2500000}); 89 | let buyoutProcessor = await BuyoutProcessor.deployed(); 90 | console.log("Buyout processor was deployed at " + buyoutProcessor.address); 91 | await waitForNonceUpdate(); 92 | 93 | await deployer.deploy(Challenger, {from: operator, gas: 5500000}); 94 | let challenger = await Challenger.deployed(); 95 | console.log("Challenge processor was deployed at " + challenger.address); 96 | await waitForNonceUpdate(); 97 | 98 | // await deployer.deploy(LimboExitGame, {from: operator}); 99 | // let limboExitGame = await LimboExitGame.deployed(); 100 | 101 | await parent.allowDeposits(buyoutProcessor.address, {from: operator, gas: 50000}) 102 | await waitForNonceUpdate(); 103 | 104 | await parent.allowChallenges(challenger.address, {from: operator, gas: 50000}); 105 | await waitForNonceUpdate(); 106 | 107 | // await parent.allowLimboExits(limboExitGame.address, {from: operator}) 108 | // await waitForNonceUpdate(); 109 | 110 | await parent.setOperator(blockSignerAddress, 2, {from: operator, gas: 50000}); 111 | await waitForNonceUpdate(); 112 | 113 | const canSignBlocks = await storage.canSignBlocks(blockSignerAddress); 114 | assert(canSignBlocks); 115 | 116 | const buyoutProcessorAddress = await parent.buyoutProcessorContract(); 117 | assert(buyoutProcessorAddress === buyoutProcessor.address); 118 | 119 | const challengesAddress = await parent.challengesContract(); 120 | assert(challengesAddress === challenger.address); 121 | 122 | // const limboExitsAddress = await parent.limboExitContract(); 123 | // assert(limboExitsAddress === limboExitGame.address); 124 | 125 | let parentAbi = parent.abi; 126 | let buyoutAbi = buyoutProcessor.abi; 127 | let challengerAbi = challenger.abi; 128 | // let limboExitAbi = limboExitGame.abi; 129 | 130 | 131 | const mergedABI = _.uniqBy([...parentAbi, ...buyoutAbi, ...challengerAbi], a => a.name || a.type); 132 | 133 | // const mergedABI = _.uniqBy([...parentAbi, ...buyoutAbi, ...challengerAbi, ...limboExitAbi], a => a.name || a.type); 134 | // due to async contract address is not saved in not saved in json by truffle 135 | // so we need to generate details file from within migration 136 | let details = {error: false, address: parent.address, abi: mergedABI}; 137 | fs.writeFileSync("build/details" + network, JSON.stringify(details)); 138 | let abiOnly = {abi: mergedABI} 139 | fs.writeFileSync("build/abi", JSON.stringify(abiOnly)); 140 | if (fs.existsSync("/data/shared")) { 141 | fs.writeFileSync("/data/shared/details", JSON.stringify(details)); 142 | fs.writeFileSync("/data/shared/abi", JSON.stringify(abiOnly)); 143 | } 144 | console.log('Complete. Contract address: ' + parent.address); 145 | })(); 146 | }; 147 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PlasmaParentContract", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "deploy-rinkeby": "truffle migrate --network rinkeby", 6 | "test": "node $NODE_DEBUG_OPTION node_modules/.bin/truffle test" 7 | }, 8 | "dependencies": { 9 | "dotenv": "^5.0.1", 10 | "ethereumjs-util": "^5.2.0", 11 | "is-buffer": "^2.0.2", 12 | "js-sha3": "^0.7.0", 13 | "lodash": "^4.17.10", 14 | "mocha": "^5.2.0", 15 | "truffle": "^4.1.3", 16 | "truffle-core": "^4.1.3", 17 | "truffle-hdwallet-provider": "0.0.3", 18 | "truffle-wallet-provider": "0.0.5", 19 | "web3": "^1.0.0-beta.36" 20 | }, 21 | "devDependencies": { 22 | "truffle-artifactor": "^3.0.7", 23 | "truffle-contract": "^3.0.6", 24 | "truffle-test-utils": "0.1.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/buyout.js: -------------------------------------------------------------------------------- 1 | const util = require("util"); 2 | const ethUtil = require('ethereumjs-util') 3 | const BN = ethUtil.BN; 4 | const t = require('truffle-test-utils') 5 | t.init() 6 | const expectThrow = require("../helpers/expectThrow"); 7 | const {addresses, keys} = require("./keys.js"); 8 | const {createTransaction, parseTransactionIndex} = require("./createTransaction"); 9 | const {createBlock, createMerkleTree} = require("./createBlock"); 10 | const testUtils = require('./utils'); 11 | const deploy = require("./deploy"); 12 | 13 | const increaseTime = async function(addSeconds) { 14 | await web3.currentProvider.send({jsonrpc: "2.0", method: "evm_increaseTime", params: [addSeconds], id: 0}) 15 | await web3.currentProvider.send({jsonrpc: "2.0", method: "evm_mine", params: [], id: 1}) 16 | } 17 | 18 | const { 19 | TxTypeFund, 20 | TxTypeMerge, 21 | TxTypeSplit} = require("../lib/Tx/RLPtx.js"); 22 | 23 | contract('Plasma buyout procedure', async (accounts) => { 24 | 25 | const operatorAddress = accounts[0]; 26 | const operatorKey = keys[0]; 27 | 28 | let queue; 29 | let plasma; 30 | let storage; 31 | let firstHash; 32 | 33 | const operator = accounts[0]; 34 | 35 | const alice = addresses[2]; 36 | const aliceKey = keys[2]; 37 | const bob = addresses[3]; 38 | const bobKey = keys[3]; 39 | 40 | beforeEach(async () => { 41 | const result = await deploy(operator, operatorAddress); 42 | ({plasma, firstHash, queue, storage} = result); 43 | }) 44 | 45 | it('should send an offer and accept it', async () => { 46 | // deposit to prevent stopping 47 | 48 | const withdrawCollateral = await plasma.WithdrawCollateral(); 49 | await plasma.deposit({from: alice, value: "100"}); 50 | 51 | const allTXes = []; 52 | const fundTX = createTransaction(TxTypeFund, 0, 53 | [{ 54 | blockNumber: 0, 55 | txNumberInBlock: 0, 56 | outputNumberInTransaction: 0, 57 | amount: 0 58 | }], 59 | [{ 60 | amount: 100, 61 | to: alice 62 | }], 63 | operatorKey 64 | ) 65 | allTXes.push(fundTX) 66 | const block = createBlock(1, allTXes.length, firstHash, allTXes, operatorKey) 67 | await testUtils.submitBlock(plasma, block); 68 | 69 | const proofObject = block.getProofForTransactionByNumber(0); 70 | const {proof, tx} = proofObject; 71 | let submissionReceipt = await plasma.startExit( 72 | 1, 0, ethUtil.bufferToHex(tx.serialize()), ethUtil.bufferToHex(proof), 73 | {from: alice, value: withdrawCollateral} 74 | ) 75 | 76 | const exitRecordHash = submissionReceipt.logs[2].args._partialHash; 77 | const exitRecord = await plasma.exitRecords(exitRecordHash); 78 | const txHash = ethUtil.bufferToHex(ethUtil.sha3(proofObject.tx.serialize())) 79 | 80 | assert(exitRecord[0] === txHash); 81 | assert(exitRecord[1].toNumber() === 100) 82 | assert(exitRecord[2] === alice); 83 | assert(exitRecord[4].toString(10) === "1") 84 | assert(exitRecord[5].toNumber() === 0) 85 | assert(exitRecord[6].toNumber() === 0) 86 | assert(exitRecord[7] === true) 87 | assert(exitRecord[8] === false) 88 | 89 | //now lets offer a buyoyt for half of the amount 90 | // offerOutputBuyout(bytes22 _index) 91 | submissionReceipt = await plasma.offerOutputBuyout(exitRecordHash, bob, {from: bob, value: 50}) 92 | assert(submissionReceipt.logs.length == 1); 93 | let offer = await plasma.exitBuyoutOffers(exitRecordHash); 94 | assert(offer[1] === bob); 95 | assert(offer[0].toString(10) === "50"); 96 | assert(!offer[2]); 97 | 98 | let oldBalanceAlice = await web3.eth.getBalance(alice); 99 | submissionReceipt = await plasma.acceptBuyoutOffer(exitRecordHash, {from: alice, gasPrice: 0}); 100 | let newBalanceAlice = await web3.eth.getBalance(alice); 101 | 102 | assert(newBalanceAlice.gt(oldBalanceAlice)); 103 | 104 | offer = await plasma.exitBuyoutOffers(exitRecordHash); 105 | assert(offer[1] === bob); 106 | assert(offer[0].toString(10) === "50"); 107 | assert(offer[2]); 108 | 109 | const delay = await plasma.ExitDelay(); 110 | await increaseTime(delay.toNumber() + 1); 111 | 112 | let oldBalanceBob = await web3.eth.getBalance(bob); 113 | oldBalanceAlice = newBalanceAlice 114 | submissionReceipt = await plasma.finalizeExits(1, {from: operator}); 115 | let newBalanceBob = await web3.eth.getBalance(bob); 116 | assert(newBalanceBob.gt(oldBalanceBob)); 117 | 118 | newBalanceAlice = await web3.eth.getBalance(alice); 119 | assert(newBalanceAlice.eq(oldBalanceAlice)); 120 | }) 121 | 122 | 123 | it('should send a presigned offer', async () => { 124 | // deposit to prevent stopping 125 | 126 | const withdrawCollateral = await plasma.WithdrawCollateral(); 127 | await plasma.deposit({from: alice, value: "100"}); 128 | 129 | const allTXes = []; 130 | const fundTX = createTransaction(TxTypeFund, 0, 131 | [{ 132 | blockNumber: 0, 133 | txNumberInBlock: 0, 134 | outputNumberInTransaction: 0, 135 | amount: 0 136 | }], 137 | [{ 138 | amount: 100, 139 | to: alice 140 | }], 141 | operatorKey 142 | ) 143 | allTXes.push(fundTX) 144 | const block = createBlock(1, allTXes.length, firstHash, allTXes, operatorKey) 145 | await testUtils.submitBlock(plasma, block); 146 | 147 | const proofObject = block.getProofForTransactionByNumber(0); 148 | const {proof, tx} = proofObject; 149 | let submissionReceipt = await plasma.startExit( 150 | 1, 0, ethUtil.bufferToHex(tx.serialize()), ethUtil.bufferToHex(proof), 151 | {from: alice, value: withdrawCollateral} 152 | ) 153 | 154 | const exitRecordHash = submissionReceipt.logs[2].args._partialHash; 155 | const exitRecord = await plasma.exitRecords(exitRecordHash); 156 | const txHash = ethUtil.bufferToHex(ethUtil.sha3(proofObject.tx.serialize())) 157 | 158 | assert(exitRecord[0] === txHash); 159 | assert(exitRecord[1].toNumber() === 100) 160 | assert(exitRecord[2] === alice); 161 | assert(exitRecord[4].toString(10) === "1") 162 | assert(exitRecord[5].toNumber() === 0) 163 | assert(exitRecord[6].toNumber() === 0) 164 | assert(exitRecord[7] === true) 165 | assert(exitRecord[8] === false) 166 | 167 | //now lets offer a buyoyt for half of the amount 168 | // function publishPreacceptedBuyout( 169 | // bytes22 _index, 170 | // uint256 _amount, 171 | // address _beneficiary, 172 | // uint8 v, 173 | // bytes32 r, 174 | // bytes32 s 175 | // ) 176 | 177 | const valueBuffer = (new BN(50)).toBuffer("be", 32) 178 | const dataToSign = Buffer.concat([ethUtil.toBuffer(exitRecordHash), valueBuffer, ethUtil.toBuffer(bob)]) 179 | const hashToSign = ethUtil.hashPersonalMessage(dataToSign) 180 | const signature = ethUtil.ecsign(hashToSign, aliceKey) 181 | const {v, r, s} = signature 182 | let oldBalanceAlice = await web3.eth.getBalance(alice); 183 | submissionReceipt = await plasma.publishPreacceptedBuyout( 184 | exitRecordHash, 185 | 50, 186 | bob, 187 | v, 188 | ethUtil.bufferToHex(r), 189 | ethUtil.bufferToHex(s), 190 | {from: bob, value: 50}) 191 | assert(submissionReceipt.logs.length == 1); 192 | let offer = await plasma.exitBuyoutOffers(exitRecordHash); 193 | assert(offer[1] === bob); 194 | assert(offer[0].toString(10) === "50"); 195 | assert(offer[2]); 196 | 197 | let newBalanceAlice = await web3.eth.getBalance(alice); 198 | 199 | assert(newBalanceAlice.gt(oldBalanceAlice)); 200 | 201 | const delay = await plasma.ExitDelay(); 202 | await increaseTime(delay.toNumber() + 1); 203 | 204 | let oldBalanceBob = await web3.eth.getBalance(bob); 205 | oldBalanceAlice = newBalanceAlice 206 | submissionReceipt = await plasma.finalizeExits(1, {from: operator}); 207 | let newBalanceBob = await web3.eth.getBalance(bob); 208 | assert(newBalanceBob.gt(oldBalanceBob)); 209 | 210 | newBalanceAlice = await web3.eth.getBalance(alice); 211 | assert(newBalanceAlice.eq(oldBalanceAlice)); 212 | }) 213 | 214 | it('should allow returning funds for expired offer', async () => { 215 | // deposit to prevent stopping 216 | 217 | const withdrawCollateral = await plasma.WithdrawCollateral(); 218 | await plasma.deposit({from: alice, value: "100"}); 219 | 220 | const allTXes = []; 221 | const fundTX = createTransaction(TxTypeFund, 0, 222 | [{ 223 | blockNumber: 0, 224 | txNumberInBlock: 0, 225 | outputNumberInTransaction: 0, 226 | amount: 0 227 | }], 228 | [{ 229 | amount: 100, 230 | to: alice 231 | }], 232 | operatorKey 233 | ) 234 | allTXes.push(fundTX) 235 | const block = createBlock(1, allTXes.length, firstHash, allTXes, operatorKey) 236 | await testUtils.submitBlock(plasma, block); 237 | 238 | const proofObject = block.getProofForTransactionByNumber(0); 239 | const {proof, tx} = proofObject; 240 | let submissionReceipt = await plasma.startExit( 241 | 1, 0, ethUtil.bufferToHex(tx.serialize()), ethUtil.bufferToHex(proof), 242 | {from: alice, value: withdrawCollateral} 243 | ) 244 | 245 | const exitRecordHash = submissionReceipt.logs[2].args._partialHash; 246 | const exitRecord = await plasma.exitRecords(exitRecordHash); 247 | const txHash = ethUtil.bufferToHex(ethUtil.sha3(proofObject.tx.serialize())) 248 | 249 | assert(exitRecord[0] === txHash); 250 | assert(exitRecord[1].toNumber() === 100) 251 | assert(exitRecord[2] === alice); 252 | assert(exitRecord[4].toString(10) === "1") 253 | assert(exitRecord[5].toNumber() === 0) 254 | assert(exitRecord[6].toNumber() === 0) 255 | assert(exitRecord[7] === true) 256 | assert(exitRecord[8] === false) 257 | 258 | //now lets offer a buyoyt for half of the amount 259 | // offerOutputBuyout(bytes22 _index) 260 | submissionReceipt = await plasma.offerOutputBuyout(exitRecordHash, bob, {from: bob, value: 50}) 261 | assert(submissionReceipt.logs.length == 1); 262 | let offer = await plasma.exitBuyoutOffers(exitRecordHash); 263 | assert(offer[1] === bob); 264 | assert(offer[0].toString(10) === "50"); 265 | assert(!offer[2]); 266 | 267 | let oldBalanceBob = await web3.eth.getBalance(bob); 268 | submissionReceipt = await plasma.returnExpiredBuyoutOffer(exitRecordHash, {from: bob, gasPrice: 0}); 269 | let newBalanceBob = await web3.eth.getBalance(bob); 270 | 271 | assert(newBalanceBob.gt(oldBalanceBob)); 272 | await expectThrow(plasma.acceptBuyoutOffer(exitRecordHash, {from: alice, gasPrice: 0})); 273 | offer = await plasma.exitBuyoutOffers(exitRecordHash); 274 | assert(offer[0].toString(10) === "0"); 275 | assert(!offer[2]); 276 | 277 | const delay = await plasma.ExitDelay(); 278 | await increaseTime(delay.toNumber() + 1); 279 | 280 | let oldBalanceAlice = await web3.eth.getBalance(alice); 281 | oldBalanceBob = newBalanceBob 282 | submissionReceipt = await plasma.finalizeExits(1, {from: operator}); 283 | let newBalanceAlice = await web3.eth.getBalance(alice); 284 | newBalanceBob = await web3.eth.getBalance(bob); 285 | assert(newBalanceBob.eq(oldBalanceBob)); 286 | assert(newBalanceAlice.gt(oldBalanceAlice)); 287 | }) 288 | 289 | it('should not allow accepting for already exited transaction', async () => { 290 | // deposit to prevent stopping 291 | 292 | const withdrawCollateral = await plasma.WithdrawCollateral(); 293 | await plasma.deposit({from: alice, value: "100"}); 294 | 295 | const allTXes = []; 296 | const fundTX = createTransaction(TxTypeFund, 0, 297 | [{ 298 | blockNumber: 0, 299 | txNumberInBlock: 0, 300 | outputNumberInTransaction: 0, 301 | amount: 0 302 | }], 303 | [{ 304 | amount: 100, 305 | to: alice 306 | }], 307 | operatorKey 308 | ) 309 | allTXes.push(fundTX) 310 | const block = createBlock(1, allTXes.length, firstHash, allTXes, operatorKey) 311 | await testUtils.submitBlock(plasma, block); 312 | 313 | const proofObject = block.getProofForTransactionByNumber(0); 314 | const {proof, tx} = proofObject; 315 | let submissionReceipt = await plasma.startExit( 316 | 1, 0, ethUtil.bufferToHex(tx.serialize()), ethUtil.bufferToHex(proof), 317 | {from: alice, value: withdrawCollateral} 318 | ) 319 | 320 | const exitRecordHash = submissionReceipt.logs[2].args._partialHash; 321 | const exitRecord = await plasma.exitRecords(exitRecordHash); 322 | const txHash = ethUtil.bufferToHex(ethUtil.sha3(proofObject.tx.serialize())) 323 | 324 | assert(exitRecord[0] === txHash); 325 | assert(exitRecord[1].toNumber() === 100) 326 | assert(exitRecord[2] === alice); 327 | assert(exitRecord[4].toString(10) === "1") 328 | assert(exitRecord[5].toNumber() === 0) 329 | assert(exitRecord[6].toNumber() === 0) 330 | assert(exitRecord[7] === true) 331 | assert(exitRecord[8] === false) 332 | 333 | //now lets offer a buyoyt for half of the amount 334 | // offerOutputBuyout(bytes22 _index) 335 | submissionReceipt = await plasma.offerOutputBuyout(exitRecordHash, bob, {from: bob, value: 50}) 336 | assert(submissionReceipt.logs.length == 1); 337 | let offer = await plasma.exitBuyoutOffers(exitRecordHash); 338 | assert(offer[1] === bob); 339 | assert(offer[0].toString(10) === "50"); 340 | assert(!offer[2]); 341 | 342 | const delay = await plasma.ExitDelay(); 343 | await increaseTime(delay.toNumber() + 1); 344 | 345 | submissionReceipt = await plasma.finalizeExits(1, {from: operator}); 346 | 347 | await expectThrow(plasma.acceptBuyoutOffer(exitRecordHash, {from: alice, gasPrice: 0})); 348 | }) 349 | 350 | it('should not allow to offer for already exited transaction', async () => { 351 | // deposit to prevent stopping 352 | 353 | const withdrawCollateral = await plasma.WithdrawCollateral(); 354 | await plasma.deposit({from: alice, value: "100"}); 355 | 356 | const allTXes = []; 357 | const fundTX = createTransaction(TxTypeFund, 0, 358 | [{ 359 | blockNumber: 0, 360 | txNumberInBlock: 0, 361 | outputNumberInTransaction: 0, 362 | amount: 0 363 | }], 364 | [{ 365 | amount: 100, 366 | to: alice 367 | }], 368 | operatorKey 369 | ) 370 | allTXes.push(fundTX) 371 | const block = createBlock(1, allTXes.length, firstHash, allTXes, operatorKey) 372 | await testUtils.submitBlock(plasma, block); 373 | 374 | const proofObject = block.getProofForTransactionByNumber(0); 375 | const {proof, tx} = proofObject; 376 | let submissionReceipt = await plasma.startExit( 377 | 1, 0, ethUtil.bufferToHex(tx.serialize()), ethUtil.bufferToHex(proof), 378 | {from: alice, value: withdrawCollateral} 379 | ) 380 | 381 | const exitRecordHash = submissionReceipt.logs[2].args._partialHash; 382 | const exitRecord = await plasma.exitRecords(exitRecordHash); 383 | const txHash = ethUtil.bufferToHex(ethUtil.sha3(proofObject.tx.serialize())) 384 | 385 | assert(exitRecord[0] === txHash); 386 | assert(exitRecord[1].toNumber() === 100) 387 | assert(exitRecord[2] === alice); 388 | assert(exitRecord[4].toString(10) === "1") 389 | assert(exitRecord[5].toNumber() === 0) 390 | assert(exitRecord[6].toNumber() === 0) 391 | assert(exitRecord[7] === true) 392 | assert(exitRecord[8] === false) 393 | 394 | const delay = await plasma.ExitDelay(); 395 | await increaseTime(delay.toNumber() + 1); 396 | 397 | submissionReceipt = await plasma.finalizeExits(1, {from: operator}); 398 | await expectThrow(plasma.offerOutputBuyout(exitRecordHash, bob, {from: bob, value: 50})) 399 | }) 400 | 401 | }) 402 | 403 | -------------------------------------------------------------------------------- /test/createBlock.js: -------------------------------------------------------------------------------- 1 | const {Block} = require("../lib/Block/RLPblock"); 2 | const ethUtil = require("ethereumjs-util"); 3 | const BN = ethUtil.BN; 4 | const MerkleTools = require("../lib/merkle-tools"); 5 | const {EmptyTransactionBuffer} = require("../lib/Tx/RLPtxWithSignature") 6 | 7 | function createBlock(blockNumber, numberOfTransactions, previousHash, transactions, privateKey) { 8 | const params = { 9 | blockNumber : (new BN(blockNumber)).toBuffer("be", 4), 10 | transactions : transactions, 11 | parentHash : ethUtil.toBuffer(previousHash), 12 | } 13 | const block = new Block(params) 14 | block.numberOfTransactions = (new BN(numberOfTransactions)).toBuffer("be",4) 15 | block.sign(privateKey); 16 | return block 17 | } 18 | 19 | function createMerkleTree(dataArray) { 20 | const treeOptions = { 21 | hashType: 'sha3' 22 | } 23 | 24 | const merkleTree = new MerkleTools(treeOptions) 25 | for (let i = 0; i < dataArray.length; i++) { 26 | const txHash = ethUtil.hashPersonalMessage(dataArray[i]); 27 | merkleTree.addLeaf(txHash); 28 | } 29 | merkleTree.makePlasmaTree(EmptyTransactionBuffer); 30 | return merkleTree 31 | } 32 | 33 | module.exports = {createBlock, createMerkleTree} -------------------------------------------------------------------------------- /test/createTransaction.js: -------------------------------------------------------------------------------- 1 | const {PlasmaTransaction, 2 | TxTypeFund, 3 | TxTypeMerge, 4 | TxTypeSplit} = require("../lib/Tx/RLPtx.js"); 5 | 6 | const {PlasmaTransactionWithSignature} = require("../lib/Tx/RLPtxWithSignature"); 7 | 8 | const {TransactionInput} = require("../lib/Tx/RLPinput"); 9 | const {TransactionOutput} = require("../lib/Tx/RLPoutput"); 10 | 11 | const ethUtil = require("ethereumjs-util"); 12 | const BN = ethUtil.BN; 13 | 14 | function createTransaction(transactionType, transactionNumber, inputs, outputs, privateKey) { 15 | const allInputs = []; 16 | const allOutputs = []; 17 | 18 | for (const input of inputs) { 19 | const inp = new TransactionInput({ 20 | blockNumber: (new BN(input.blockNumber)).toBuffer("be",4), 21 | txNumberInBlock: (new BN(input.txNumberInBlock)).toBuffer("be",4), 22 | outputNumberInTransaction: (new BN(input.outputNumberInTransaction)).toBuffer("be",1), 23 | amountBuffer: (new BN(input.amount)).toBuffer("be",32) 24 | }) 25 | allInputs.push(inp) 26 | } 27 | let outputCounter = 0 28 | for (const output of outputs) { 29 | const out = new TransactionOutput({ 30 | outputNum: (new BN(outputCounter)).toBuffer("be", 1), 31 | amountBuffer: (new BN(output.amount)).toBuffer("be",32), 32 | to: ethUtil.toBuffer(output.to) 33 | }) 34 | allOutputs.push(out) 35 | outputCounter++; 36 | } 37 | 38 | const plasmaTransaction = new PlasmaTransaction({ 39 | transactionType: (new BN(transactionType)).toBuffer("be",1), 40 | inputs: allInputs, 41 | outputs: allOutputs, 42 | }) 43 | 44 | // const hash = plasmaTransaction.hash(); 45 | // const signature = ethUtil.ecsign(hash, privateKey) 46 | // const sig = ethUtil.toRpcSig(signature.v, signature.r, signature.s) 47 | const sigTX = new PlasmaTransactionWithSignature({ 48 | transaction: plasmaTransaction 49 | }); 50 | sigTX.sign(privateKey); 51 | 52 | return sigTX; 53 | } 54 | 55 | // function createTransaction(transactionType, transactionNumber, inputs, outputs, privateKey) { 56 | // const allInputs = []; 57 | // const allOutputs = []; 58 | 59 | // for (const input of inputs) { 60 | // const inp = new TransactionInput({ 61 | // blockNumber: (new BN(input.blockNumber)).toBuffer("be",4), 62 | // txNumberInBlock: (new BN(input.txNumberInBlock)).toBuffer("be",4), 63 | // outputNumberInTransaction: (new BN(input.outputNumberInTransaction)).toBuffer("be",1), 64 | // amountBuffer: (new BN(input.amount)).toBuffer("be",32) 65 | // }) 66 | // allInputs.push(inp) 67 | // } 68 | 69 | // for (const output of outputs) { 70 | // const out = new TransactionOutput({ 71 | // amountBuffer: (new BN(output.amount)).toBuffer("be",32), 72 | // to: ethUtil.toBuffer(output.to) 73 | // }) 74 | // allOutputs.push(out) 75 | // } 76 | 77 | // const plasmaTransaction = new PlasmaTransaction({ 78 | // transactionType: (new BN(transactionType)).toBuffer("be",1), 79 | // inputs: allInputs, 80 | // outputs: allOutputs, 81 | // }) 82 | 83 | // // const hash = plasmaTransaction.hash(); 84 | // // const signature = ethUtil.ecsign(hash, privateKey) 85 | // // const sig = ethUtil.toRpcSig(signature.v, signature.r, signature.s) 86 | // const sigTX = new PlasmaTransactionWithSignature({ 87 | // transaction: plasmaTransaction 88 | // }); 89 | // sigTX.sign(privateKey); 90 | 91 | // // sigTX.serializeSignature(sig) 92 | 93 | // const numberedTX = new PlasmaTransactionWithNumberAndSignature({ 94 | // transactionNumberInBlock: (new BN(transactionNumber)).toBuffer("be", 4), 95 | // signedTransaction: sigTX 96 | // }) 97 | // return numberedTX 98 | // } 99 | 100 | function parseTransactionIndex(index) { 101 | let idx = new BN(index) 102 | const ONE = new BN(1); 103 | let outputNumber = idx.mod(ONE.ushln(8)); 104 | idx = idx.ushrn(8); 105 | const txNumber = idx.mod(ONE.ushln(32)); 106 | idx = idx.ushrn(32); 107 | const blockNumber = idx.mod(ONE.ushln(32)); 108 | return {blockNumber, txNumber, outputNumber} 109 | } 110 | 111 | module.exports = {createTransaction, parseTransactionIndex}; -------------------------------------------------------------------------------- /test/deploy.js: -------------------------------------------------------------------------------- 1 | const PlasmaParent = artifacts.require('PlasmaParent'); 2 | const PriorityQueue = artifacts.require('PriorityQueue'); 3 | const BlockStorage = artifacts.require("PlasmaBlockStorage"); 4 | const Challenger = artifacts.require("PlasmaChallenges"); 5 | const BuyoutProcessor = artifacts.require("PlasmaBuyoutProcessor"); 6 | const LimboExitGame = artifacts.require("PlasmaLimboExitGame"); 7 | 8 | const assert = require("assert"); 9 | const truffleContract = require("truffle-contract"); 10 | const _ = require("lodash"); 11 | 12 | console.log("Parent bytecode size = " + (PlasmaParent.bytecode.length -2)/2); 13 | console.log("Buyouts processor bytecode size = " + (BuyoutProcessor.bytecode.length -2)/2); 14 | console.log("Challenger bytecode size = " + (Challenger.bytecode.length -2)/2); 15 | console.log("Limbo exit game bytecode length = " + (LimboExitGame.bytecode.length -2)/2); 16 | 17 | async function deploy(operator, operatorAddress) { 18 | const storage = await BlockStorage.new({from: operator}) 19 | const queue = await PriorityQueue.new({from: operator}) 20 | const parent = await PlasmaParent.new(queue.address, storage.address, {from: operator, value: "10000000000000000000"}) 21 | await storage.setOwner(parent.address, {from: operator}) 22 | await queue.setOwner(parent.address, {from: operator}) 23 | 24 | const buyoutProcessor = await BuyoutProcessor.new({from: operator}); 25 | const challenger = await Challenger.new({from: operator}); 26 | const limboExitGame = await LimboExitGame.new({from: operator}); 27 | 28 | await parent.allowDeposits(buyoutProcessor.address, {from: operator}) 29 | await parent.allowChallenges(challenger.address, {from: operator}); 30 | await parent.allowLimboExits(limboExitGame.address, {from: operator}) 31 | await parent.setOperator(operatorAddress, 2, {from: operator}); 32 | 33 | const canSignBlocks = await storage.canSignBlocks(operator); 34 | assert(canSignBlocks); 35 | 36 | const buyoutProcessorAddress = await parent.buyoutProcessorContract(); 37 | assert(buyoutProcessorAddress === buyoutProcessor.address); 38 | 39 | const challengesAddress = await parent.challengesContract(); 40 | assert(challengesAddress === challenger.address); 41 | 42 | const limboExitGameAddress = await parent.limboExitContract(); 43 | assert(limboExitGameAddress == limboExitGame.address); 44 | 45 | let parentAbi = parent.abi; 46 | let buyoutAbi = buyoutProcessor.abi; 47 | let challengerAbi = challenger.abi; 48 | let limboAbi = limboExitGame.abi; 49 | 50 | const mergedABI = _.uniqBy([...parentAbi, ...buyoutAbi, ...challengerAbi, ...limboAbi], a => a.name || a.type); 51 | 52 | const plasmaMergedContract = truffleContract({abi: mergedABI}, {gas: 4700000}); 53 | plasmaMergedContract.setProvider(web3.currentProvider) 54 | plasmaMergedContract.defaults({from: operator, gas: 1000000}); 55 | const plasma = plasmaMergedContract.at(parent.address); 56 | const firstHash = await plasma.hashOfLastSubmittedBlock(); 57 | 58 | return {plasma, firstHash, queue, storage} 59 | } 60 | 61 | module.exports = deploy -------------------------------------------------------------------------------- /test/depositWithdrawFunctions.js: -------------------------------------------------------------------------------- 1 | const util = require("util"); 2 | const ethUtil = require('ethereumjs-util'); 3 | // const BN = ethUtil.BN; 4 | var BN; 5 | const t = require('truffle-test-utils'); 6 | t.init(); 7 | const expectThrow = require("../helpers/expectThrow"); 8 | const {addresses, keys} = require("./keys"); 9 | const {createTransaction} = require("./createTransaction"); 10 | const {createBlock, createMerkleTree} = require("./createBlock"); 11 | const testUtils = require('./utils'); 12 | const deploy = require("./deploy"); 13 | 14 | const { 15 | TxTypeFund, 16 | TxTypeMerge, 17 | TxTypeSplit} = require("../lib/Tx/RLPtx"); 18 | 19 | // const Web3 = require("web3"); 20 | 21 | const increaseTime = async function(addSeconds) { 22 | await web3.currentProvider.send({jsonrpc: "2.0", method: "evm_increaseTime", params: [addSeconds], id: 0}); 23 | await web3.currentProvider.send({jsonrpc: "2.0", method: "evm_mine", params: [], id: 1}) 24 | }; 25 | 26 | contract('Deposit withdraw functions', async (accounts) => { 27 | BN = web3.BigNumber; 28 | const operatorAddress = accounts[0]; 29 | const operatorKey = keys[0]; 30 | 31 | let queue; 32 | let plasma; 33 | let storage; 34 | let firstHash; 35 | 36 | const operator = accounts[0]; 37 | 38 | const alice = addresses[2]; 39 | const aliceKey = keys[2]; 40 | const bob = addresses[3]; 41 | const bobKey = keys[3]; 42 | 43 | beforeEach(async () => { 44 | const result = await deploy(operator, operatorAddress); 45 | ({plasma, firstHash, queue, storage} = result); 46 | }) 47 | 48 | it('should emit deposit event', async () => { 49 | const depositAmount = 42; 50 | // const depositedBefore = await plasma.totalAmountDeposited(); 51 | let receipt = await plasma.deposit({from: alice, value: depositAmount}); 52 | // const depositIndex = testUtils.depositIndex(receipt.receipt.blockNumber); 53 | const depositIndex = new web3.BigNumber(0); 54 | // const depositedAfter = await plasma.totalAmountDeposited(); 55 | await testUtils.expectEvents(plasma, receipt.receipt.blockNumber, 'DepositEvent', {_from: alice, _amount: depositAmount, _depositIndex: depositIndex.toNumber()}); 56 | // assert.equal(depositedAfter.toNumber(), depositedBefore.toNumber() + depositAmount, 'Deposit counter should increase'); 57 | }); 58 | 59 | it('should allow deposit withdraw process', async () => { 60 | let receipt = await plasma.deposit({from: alice, value: 314}); 61 | // const depositIndex = testUtils.depositIndex(receipt.receipt.blockNumber); 62 | const depositIndex = new web3.BigNumber(0); 63 | const depositWithdrawCollateral = await plasma.DepositWithdrawCollateral(); 64 | receipt = await plasma.startDepositWithdraw(depositIndex.toString(), {from: alice, value: depositWithdrawCollateral.toString()}); 65 | await testUtils.expectEvents(plasma, receipt.receipt.blockNumber, 'DepositWithdrawStartedEvent', {_depositIndex: depositIndex.toNumber()}); 66 | }); 67 | 68 | it('should require bond for deposit withdraw start', async () => { 69 | const receipt = await plasma.deposit({from: alice, value: 314}); 70 | // const depositIndex = testUtils.depositIndex(receipt.receipt.blockNumber); 71 | const depositIndex = new web3.BigNumber(0); 72 | const promise = plasma.startDepositWithdraw(depositIndex, {from: alice, value: 0}); 73 | // Will also fail if contract's bond constant is set to 0 74 | await expectThrow(promise); 75 | }); 76 | 77 | it('should not allow early deposit withdraw', async () => { 78 | let receipt = await plasma.deposit({from: alice, value: 314}); 79 | const depositIndex = new web3.BigNumber(0); 80 | // const depositIndex = testUtils.depositIndex(receipt.receipt.blockNumber); 81 | 82 | const depositWithdrawCollateral = await plasma.DepositWithdrawCollateral(); 83 | await plasma.startDepositWithdraw(depositIndex.toString(), {from: alice, value: depositWithdrawCollateral.toString()}); 84 | 85 | const promise = plasma.finalizeDepositWithdraw(depositIndex.toString(), {from: alice}); 86 | await expectThrow(promise); 87 | }); 88 | 89 | it('should allow successful deposit withdraw', async () => { 90 | const depositAmount = new BN(314); 91 | let receipt = await plasma.deposit({from: alice, value: depositAmount.toString()}); 92 | const depositIndex = new web3.BigNumber(0); 93 | // const depositIndex = testUtils.depositIndex(receipt.receipt.blockNumber); 94 | 95 | const depositWithdrawCollateral = await plasma.DepositWithdrawCollateral(); 96 | await plasma.startDepositWithdraw(depositIndex.toString(), {from: alice, value: depositWithdrawCollateral.toString()}); 97 | 98 | const delay = await plasma.DepositWithdrawDelay(); 99 | await increaseTime(delay.toNumber() + 1); 100 | const balanceBefore = await web3.eth.getBalance(alice); 101 | // const depositedBefore = await plasma.totalAmountDeposited(); 102 | receipt = await plasma.finalizeDepositWithdraw(depositIndex.toString(), {from: alice, gasPrice: web3.eth.gasPrice}); 103 | // const depositedAfter = await plasma.totalAmountDeposited(); 104 | const balanceAfter = await web3.eth.getBalance(alice); 105 | await testUtils.expectEvents(plasma, receipt.receipt.blockNumber, 'DepositWithdrawCompletedEvent', {_depositIndex: depositIndex.toNumber()}); 106 | 107 | assert(balanceAfter.gt(balanceBefore)); 108 | const expectedBalance = balanceBefore 109 | .add(depositAmount) 110 | .add(depositWithdrawCollateral) 111 | .sub(web3.eth.gasPrice.mul(receipt.receipt.gasUsed)); 112 | assert.equal(balanceAfter.toString(), expectedBalance.toString(), 'Balance not equal'); 113 | // assert.equal(depositedAfter.toString(), (depositedBefore - depositAmount).toString(), 'Deposit counter should decrease'); 114 | }); 115 | 116 | it('should respond to deposit withdraw challenge', async () => { 117 | const depositAmount = new BN(42); 118 | let receipt = await plasma.deposit({from: alice, value: depositAmount.toString()}); 119 | const depositIndex = new web3.BigNumber(0); 120 | // const depositIndex = testUtils.depositIndex(receipt.receipt.blockNumber); 121 | 122 | const tx = createTransaction(TxTypeFund, 0, [{ 123 | blockNumber: 0, 124 | txNumberInBlock: 0, 125 | outputNumberInTransaction: 0, 126 | amount: depositIndex.toString(10) 127 | }], [{ 128 | amount: depositAmount.toString(10), 129 | to: alice 130 | }], 131 | operatorKey 132 | ); 133 | const block = createBlock(1, 1, firstHash, [tx], operatorKey); 134 | await testUtils.submitBlock(plasma, block); 135 | const proof = block.merkleTree.getProof(0, true); 136 | 137 | const depositWithdrawCollateral = await plasma.DepositWithdrawCollateral(); 138 | await plasma.startDepositWithdraw(depositIndex, {from: alice, value: depositWithdrawCollateral}); 139 | 140 | const balanceBefore = await web3.eth.getBalance(bob); 141 | receipt = await plasma.challengeDepositWithdraw(depositIndex.toString(), 1, ethUtil.bufferToHex(tx.rlpEncode()), ethUtil.bufferToHex(proof), {from: bob, gasPrice: web3.eth.gasPrice}); 142 | const balanceAfter = await web3.eth.getBalance(bob); 143 | await testUtils.expectEvents(plasma, receipt.receipt.blockNumber, 'DepositWithdrawChallengedEvent', {_depositIndex: depositIndex.toNumber()}); 144 | 145 | const expectedBalance = balanceBefore 146 | .add(depositWithdrawCollateral) 147 | .sub(web3.eth.gasPrice.mul(receipt.receipt.gasUsed)); 148 | assert.equal(balanceAfter.toString(), expectedBalance.toString(), 'Balance not equal'); 149 | }); 150 | 151 | it('should stop Plasma on funding without deposit', async () => { 152 | const depositAmount = new BN(42); 153 | const tx = createTransaction(TxTypeFund, 0, [{ 154 | blockNumber: 0, 155 | txNumberInBlock: 0, 156 | outputNumberInTransaction: 0, 157 | amount: 1 158 | }], [{ 159 | amount: depositAmount.toString(), 160 | to: alice 161 | }], 162 | operatorKey 163 | ); 164 | const block = createBlock(1, 1, firstHash, [tx], operatorKey); 165 | await testUtils.submitBlock(plasma, block); 166 | const proof = block.merkleTree.getProof(0, true); 167 | 168 | const balanceBefore = await web3.eth.getBalance(bob); 169 | const receipt = await plasma.proveInvalidDeposit(1, ethUtil.bufferToHex(tx.rlpEncode()), 170 | ethUtil.bufferToHex(proof), {from: bob, gasPrice: web3.eth.gasPrice}); 171 | const balanceAfter = await web3.eth.getBalance(bob); 172 | 173 | assert.equal(true, await plasma.plasmaErrorFound()); 174 | // const two = new BN(2); 175 | // const bond = await plasma.operatorsBond(); 176 | assert(balanceAfter.gt(balanceBefore)); 177 | // const expectedBalance = balanceBefore 178 | // .add(bond.div(two)) 179 | // .sub(web3.eth.gasPrice.mul(receipt.receipt.gasUsed)); 180 | // assert.equal(balanceAfter.toString(), expectedBalance.toString(), 'Balance not equal'); 181 | }); 182 | 183 | it('should stop Plasma on double funding', async () => { 184 | const depositAmount = new BN(42); 185 | let receipt = await plasma.deposit({from: alice, value: depositAmount.toString()}); 186 | const depositIndex = new web3.BigNumber(0); 187 | // const depositIndex = testUtils.depositIndex(receipt.receipt.blockNumber); 188 | 189 | const tx1 = createTransaction(TxTypeFund, 0, [{ 190 | blockNumber: 0, 191 | txNumberInBlock: 0, 192 | outputNumberInTransaction: 0, 193 | amount: depositIndex.toString() 194 | }], [{ 195 | amount: depositAmount.toString(), 196 | to: alice 197 | }], 198 | operatorKey 199 | ); 200 | const tx2 = createTransaction(TxTypeFund, 1, [{ 201 | blockNumber: 0, 202 | txNumberInBlock: 0, 203 | outputNumberInTransaction: 0, 204 | amount: depositIndex.toString() 205 | }], [{ 206 | amount: depositAmount.toString(), 207 | to: alice 208 | }], 209 | operatorKey 210 | ); 211 | const block = createBlock(1, 1, firstHash, [tx1, tx2], operatorKey); 212 | await testUtils.submitBlock(plasma, block); 213 | const proof1 = block.merkleTree.getProof(0, true); 214 | const proof2 = block.merkleTree.getProof(1, true); 215 | 216 | const balanceBefore = await web3.eth.getBalance(bob); 217 | receipt = await plasma.proveDoubleFunding( 218 | 1, ethUtil.bufferToHex(tx1.rlpEncode()), ethUtil.bufferToHex(Buffer.concat(proof1)), 219 | 1, ethUtil.bufferToHex(tx2.rlpEncode()), ethUtil.bufferToHex(Buffer.concat(proof2)), 220 | {from: bob, gasPrice: web3.eth.gasPrice}); 221 | const balanceAfter = await web3.eth.getBalance(bob); 222 | 223 | assert.equal(true, await plasma.plasmaErrorFound()); 224 | assert(balanceAfter.gt(balanceBefore)); 225 | 226 | // const bond = await plasma.operatorsBond(); 227 | // const expectedBalance = balanceBefore 228 | // .add(bond.div(2)) 229 | // .sub(web3.eth.gasPrice.mul(receipt.receipt.gasUsed)); 230 | // assert.equal(balanceAfter.toString(), expectedBalance.toString(), 'Balance not equal'); 231 | }); 232 | 233 | }); 234 | -------------------------------------------------------------------------------- /test/fullExitProcedure.js: -------------------------------------------------------------------------------- 1 | const util = require("util"); 2 | const ethUtil = require('ethereumjs-util') 3 | const BN = ethUtil.BN; 4 | const t = require('truffle-test-utils') 5 | t.init() 6 | const expectThrow = require("../helpers/expectThrow"); 7 | const {addresses, keys} = require("./keys"); 8 | const {createTransaction, parseTransactionIndex} = require("./createTransaction"); 9 | const {createBlock, createMerkleTree} = require("./createBlock"); 10 | const testUtils = require('./utils'); 11 | const deploy = require("./deploy"); 12 | 13 | const increaseTime = async function(addSeconds) { 14 | await web3.currentProvider.send({jsonrpc: "2.0", method: "evm_increaseTime", params: [addSeconds], id: 0}) 15 | await web3.currentProvider.send({jsonrpc: "2.0", method: "evm_mine", params: [], id: 1}) 16 | } 17 | 18 | const { 19 | TxTypeFund, 20 | TxTypeMerge, 21 | TxTypeSplit} = require("../lib/Tx/RLPtx"); 22 | 23 | contract('PlasmaParent exit procedure', async (accounts) => { 24 | const operatorAddress = accounts[0]; 25 | const operatorKey = keys[0]; 26 | 27 | let queue; 28 | let plasma; 29 | let storage; 30 | let firstHash; 31 | 32 | const operator = accounts[0]; 33 | 34 | const alice = addresses[2]; 35 | const aliceKey = keys[2]; 36 | const bob = addresses[3]; 37 | const bobKey = keys[3]; 38 | 39 | beforeEach(async () => { 40 | const result = await deploy(operator, operatorAddress); 41 | ({plasma, firstHash, queue, storage} = result); 42 | }) 43 | 44 | it('Simulate exit procedure', async () => { 45 | const withdrawCollateral = await plasma.WithdrawCollateral(); 46 | await plasma.deposit({from: alice, value: "100"}); 47 | 48 | const allTXes = []; 49 | const fundTX = createTransaction(TxTypeFund, 0, 50 | [{ 51 | blockNumber: 0, 52 | txNumberInBlock: 0, 53 | outputNumberInTransaction: 0, 54 | amount: 0 55 | }], 56 | [{ 57 | amount: 100, 58 | to: alice 59 | }], 60 | operatorKey 61 | ) 62 | allTXes.push(fundTX) 63 | const block = createBlock(1, allTXes.length, firstHash, allTXes, operatorKey) 64 | await testUtils.submitBlock(plasma, block); 65 | 66 | const proofObject = block.getProofForTransactionByNumber(0); 67 | const {proof, tx} = proofObject; 68 | let submissionReceipt = await plasma.startExit( 69 | 1, 0, ethUtil.bufferToHex(tx.serialize()), ethUtil.bufferToHex(proof), 70 | {from: alice, value: withdrawCollateral} 71 | ) 72 | console.log("Single exit gas price for exiting a deposit transaction is " + submissionReceipt.receipt.gasUsed) 73 | // struct ExitRecord { 74 | // bytes32 transactionRef; 75 | // //32 bytes 76 | // uint256 amount; 77 | // // 64 bytes 78 | // address owner; 79 | // uint64 timePublished; 80 | // uint32 blockNumber; 81 | // // 96 bytes 82 | // uint32 transactionNumber; 83 | // uint8 outputNumber; 84 | // bool isValid; 85 | // bool isLimbo; 86 | // // 96 + 7 bytes 87 | // } 88 | const transactionPublishedEvent = submissionReceipt.logs[0] 89 | const txHashFromEvent = transactionPublishedEvent.args._hash; 90 | const txDataFromEvent = transactionPublishedEvent.args._data; 91 | const exitRecordHash = submissionReceipt.logs[2].args._partialHash; 92 | const exitRecord = await plasma.exitRecords(exitRecordHash); 93 | const txData = ethUtil.bufferToHex(tx.serialize()) 94 | const txHash = ethUtil.bufferToHex(ethUtil.sha3(proofObject.tx.serialize())) 95 | 96 | assert(exitRecord[0] === txHash); 97 | assert(exitRecord[1].toNumber() === 100) 98 | assert(exitRecord[2] === alice); 99 | assert(exitRecord[4].toString(10) === "1") 100 | assert(exitRecord[5].toNumber() === 0) 101 | assert(exitRecord[6].toNumber() === 0) 102 | assert(exitRecord[7] === true) 103 | assert(exitRecord[8] === false) 104 | 105 | assert(txHash === txHashFromEvent); 106 | assert(txData === txDataFromEvent); 107 | 108 | let oldBalance = await web3.eth.getBalance(alice); 109 | 110 | size = await queue.currentSize(); 111 | assert(size.toString(10) === "1"); 112 | 113 | minimalItem = await queue.getMin(); 114 | assert(minimalItem === exitRecordHash); 115 | 116 | let exitDelay = await plasma.ExitDelay() 117 | await increaseTime(exitDelay.toNumber() + 1) 118 | 119 | submissionReceipt = await plasma.finalizeExits(1); 120 | console.log("One item in the queue finalization gas = " + submissionReceipt.receipt.gasUsed) 121 | let newBalance = await web3.eth.getBalance(alice); 122 | assert(newBalance.gt(oldBalance)); 123 | let succesfulExit = await plasma.succesfulExits(exitRecordHash); 124 | assert(succesfulExit); 125 | size = await queue.currentSize(); 126 | assert(size.toString(10) === "0"); 127 | 128 | }) 129 | 130 | it('Simulate exit procedure with invalid (spent) transaction in queue before valid', async () => { 131 | const withdrawCollateral = await plasma.WithdrawCollateral(); 132 | await plasma.deposit({from: alice, value: "100"}); 133 | 134 | const allTXes = []; 135 | const fundTX = createTransaction(TxTypeFund, 0, 136 | [{ 137 | blockNumber: 0, 138 | txNumberInBlock: 0, 139 | outputNumberInTransaction: 0, 140 | amount: 0 141 | }], 142 | [{ 143 | amount: 100, 144 | to: alice 145 | }], 146 | operatorKey 147 | ) 148 | allTXes.push(fundTX) 149 | const block = createBlock(1, allTXes.length, firstHash, allTXes, operatorKey) 150 | await testUtils.submitBlock(plasma, block); 151 | let proofObject = block.getProofForTransactionByNumber(0); 152 | let {proof, tx} = proofObject; 153 | let submissionReceipt = await plasma.startExit( 154 | 1, 0, ethUtil.bufferToHex(tx.serialize()), ethUtil.bufferToHex(proof), 155 | {from: alice, value: withdrawCollateral} 156 | ) 157 | const transactionPublishedEvent = submissionReceipt.logs[0] 158 | const txHashFromEvent = transactionPublishedEvent.args._hash; 159 | const txDataFromEvent = transactionPublishedEvent.args._data; 160 | const alicePriority = submissionReceipt.logs[1].args._priority; 161 | 162 | let exitRecordHash = submissionReceipt.logs[2].args._partialHash; 163 | let exitRecord = await plasma.exitRecords(exitRecordHash); 164 | let txData = ethUtil.bufferToHex(tx.serialize()) 165 | let txHash = ethUtil.bufferToHex(ethUtil.sha3(proofObject.tx.serialize())) 166 | 167 | assert(exitRecord[0] === txHash); 168 | assert(exitRecord[1].toNumber() === 100) 169 | assert(exitRecord[2] === alice); 170 | assert(exitRecord[4].toString(10) === "1") 171 | assert(exitRecord[5].toNumber() === 0) 172 | assert(exitRecord[6].toNumber() === 0) 173 | assert(exitRecord[7] === true) 174 | assert(exitRecord[8] === false) 175 | 176 | assert(txHash === txHashFromEvent); 177 | assert(txData === txDataFromEvent); 178 | 179 | let nextHash = await plasma.hashOfLastSubmittedBlock(); 180 | const spendingTX = createTransaction(TxTypeSplit, 0, 181 | [{ 182 | blockNumber: 1, 183 | txNumberInBlock: 0, 184 | outputNumberInTransaction: 0, 185 | amount: 100 186 | }], 187 | [{ 188 | amount: 100, 189 | to: bob 190 | }], 191 | aliceKey 192 | ) 193 | const block2 = createBlock(2, 1, nextHash, [spendingTX], operatorKey) 194 | await testUtils.submitBlock(plasma, block2); 195 | 196 | proofObject = block2.getProofForTransactionByNumber(0); 197 | ({proof, tx} = proofObject); 198 | 199 | submissionReceipt = await plasma.challengeNormalExitByShowingExitBeingSpent( 200 | exitRecordHash, 2, ethUtil.bufferToHex(tx.serialize()), ethUtil.bufferToHex(proof), 0 201 | ) 202 | 203 | exitRecordHash = submissionReceipt.logs[0].args._partialHash; 204 | exitRecord = await plasma.exitRecords(exitRecordHash); 205 | const aliceHash = exitRecordHash; 206 | assert(exitRecord[0] === txHash); 207 | assert(exitRecord[1].toNumber() === 100) 208 | assert(exitRecord[2] === alice); 209 | assert(exitRecord[4].toString(10) === "1") 210 | assert(exitRecord[5].toNumber() === 0) 211 | assert(exitRecord[6].toNumber() === 0) 212 | assert(exitRecord[7] === false) 213 | assert(exitRecord[8] === false) 214 | 215 | assert(txHash === txHashFromEvent); 216 | assert(txData === txDataFromEvent); 217 | 218 | submissionReceipt = await plasma.startExit( 2, 0, ethUtil.bufferToHex(tx.serialize()), ethUtil.bufferToHex(proof), 219 | {from: bob, value: withdrawCollateral}); 220 | 221 | exitRecordHash = submissionReceipt.logs[2].args._partialHash; 222 | exitRecord = await plasma.exitRecords(exitRecordHash); 223 | const bobPriority = submissionReceipt.logs[1].args._priority; 224 | 225 | assert(bobPriority.gt(alicePriority)); 226 | txData = ethUtil.bufferToHex(tx.serialize()) 227 | txHash = ethUtil.bufferToHex(ethUtil.sha3(proofObject.tx.serialize())) 228 | 229 | const bobHash = exitRecordHash; 230 | assert(exitRecord[0] === txHash); 231 | assert(exitRecord[1].toNumber() === 100) 232 | assert(exitRecord[2] === bob); 233 | assert(exitRecord[4].toString(10) === "2") 234 | assert(exitRecord[5].toNumber() === 0) 235 | assert(exitRecord[6].toNumber() === 0) 236 | assert(exitRecord[7] === true) 237 | assert(exitRecord[8] === false) 238 | 239 | let oldBalanceAlice = await web3.eth.getBalance(alice); 240 | let oldBalanceBob = await web3.eth.getBalance(bob); 241 | 242 | size = await queue.currentSize(); 243 | assert(size.toString(10) === "2"); 244 | 245 | minimalItem = await queue.getMin(); 246 | assert(minimalItem === aliceHash); 247 | 248 | let exitDelay = await plasma.ExitDelay() 249 | await increaseTime(exitDelay.toNumber() + 1) 250 | 251 | submissionReceipt = await plasma.finalizeExits(2); 252 | console.log("Two items in the queue finalization gas = " + submissionReceipt.receipt.gasUsed) 253 | let newBalanceAlice = await web3.eth.getBalance(alice); 254 | let newBalanceBob = await web3.eth.getBalance(bob); 255 | assert(newBalanceAlice.eq(oldBalanceAlice)); 256 | assert(newBalanceBob.gt(oldBalanceBob)); 257 | let succesfulExit = await plasma.succesfulExits(bobHash); 258 | assert(succesfulExit); 259 | succesfulExit = await plasma.succesfulExits(aliceHash); 260 | assert(!succesfulExit); 261 | size = await queue.currentSize(); 262 | assert(size.toString(10) === "0"); 263 | 264 | }) 265 | 266 | it('Should pop only matured items from the exit queue', async () => { 267 | const withdrawCollateral = await plasma.WithdrawCollateral(); 268 | await plasma.deposit({from: alice, value: "100"}); 269 | 270 | const allTXes = []; 271 | const fundTX = createTransaction(TxTypeFund, 0, 272 | [{ 273 | blockNumber: 0, 274 | txNumberInBlock: 0, 275 | outputNumberInTransaction: 0, 276 | amount: 0 277 | }], 278 | [{ 279 | amount: 100, 280 | to: alice 281 | }], 282 | operatorKey 283 | ) 284 | allTXes.push(fundTX) 285 | const block = createBlock(1, allTXes.length, firstHash, allTXes, operatorKey) 286 | await testUtils.submitBlock(plasma, block); 287 | let proofObject = block.getProofForTransactionByNumber(0); 288 | let {proof, tx} = proofObject; 289 | let submissionReceipt = await plasma.startExit( 290 | 1, 0, ethUtil.bufferToHex(tx.serialize()), ethUtil.bufferToHex(proof), 291 | {from: alice, value: withdrawCollateral} 292 | ) 293 | const transactionPublishedEvent = submissionReceipt.logs[0] 294 | const txHashFromEvent = transactionPublishedEvent.args._hash; 295 | const txDataFromEvent = transactionPublishedEvent.args._data; 296 | const alicePriority = submissionReceipt.logs[1].args._priority; 297 | 298 | let exitRecordHash = submissionReceipt.logs[2].args._partialHash; 299 | const aliceHash = exitRecordHash; 300 | let exitRecord = await plasma.exitRecords(exitRecordHash); 301 | let txData = ethUtil.bufferToHex(tx.serialize()) 302 | let txHash = ethUtil.bufferToHex(ethUtil.sha3(proofObject.tx.serialize())) 303 | 304 | assert(exitRecord[0] === txHash); 305 | assert(exitRecord[1].toNumber() === 100) 306 | assert(exitRecord[2] === alice); 307 | assert(exitRecord[4].toString(10) === "1") 308 | assert(exitRecord[5].toNumber() === 0) 309 | assert(exitRecord[6].toNumber() === 0) 310 | assert(exitRecord[7] === true) 311 | assert(exitRecord[8] === false) 312 | 313 | assert(txHash === txHashFromEvent); 314 | assert(txData === txDataFromEvent); 315 | 316 | let nextHash = await plasma.hashOfLastSubmittedBlock(); 317 | const spendingTX = createTransaction(TxTypeSplit, 0, 318 | [{ 319 | blockNumber: 1, 320 | txNumberInBlock: 0, 321 | outputNumberInTransaction: 0, 322 | amount: 100 323 | }], 324 | [{ 325 | amount: 100, 326 | to: bob 327 | }], 328 | aliceKey 329 | ) 330 | const block2 = createBlock(2, 1, nextHash, [spendingTX], operatorKey) 331 | await testUtils.submitBlock(plasma, block2); 332 | 333 | proofObject = block2.getProofForTransactionByNumber(0); 334 | ({proof, tx} = proofObject); 335 | 336 | let exitDelay = await plasma.ExitDelay() 337 | await increaseTime(Math.floor(exitDelay.toNumber()/2)) 338 | 339 | submissionReceipt = await plasma.startExit( 2, 0, ethUtil.bufferToHex(tx.serialize()), ethUtil.bufferToHex(proof), 340 | {from: bob, value: withdrawCollateral}); 341 | 342 | exitRecordHash = submissionReceipt.logs[2].args._partialHash; 343 | exitRecord = await plasma.exitRecords(exitRecordHash); 344 | const bobPriority = submissionReceipt.logs[1].args._priority; 345 | 346 | assert(bobPriority.gt(alicePriority)); 347 | txData = ethUtil.bufferToHex(tx.serialize()) 348 | txHash = ethUtil.bufferToHex(ethUtil.sha3(proofObject.tx.serialize())) 349 | 350 | const bobHash = exitRecordHash; 351 | assert(exitRecord[0] === txHash); 352 | assert(exitRecord[1].toNumber() === 100) 353 | assert(exitRecord[2] === bob); 354 | assert(exitRecord[4].toString(10) === "2") 355 | assert(exitRecord[5].toNumber() === 0) 356 | assert(exitRecord[6].toNumber() === 0) 357 | assert(exitRecord[7] === true) 358 | assert(exitRecord[8] === false) 359 | 360 | await expectThrow(plasma.finalizeExits(100)); 361 | 362 | await increaseTime(Math.floor(exitDelay.toNumber()/2) + 100) 363 | 364 | let oldBalanceAlice = await web3.eth.getBalance(alice); 365 | let oldBalanceBob = await web3.eth.getBalance(bob); 366 | 367 | size = await queue.currentSize(); 368 | assert(size.toString(10) === "2"); 369 | 370 | minimalItem = await queue.getMin(); 371 | assert(minimalItem === aliceHash); 372 | 373 | submissionReceipt = await plasma.finalizeExits(100); 374 | await expectThrow(plasma.finalizeExits(100)); 375 | let newBalanceAlice = await web3.eth.getBalance(alice); 376 | let newBalanceBob = await web3.eth.getBalance(bob); 377 | assert(newBalanceAlice.gt(oldBalanceAlice)); 378 | assert(newBalanceBob.eq(oldBalanceBob)); 379 | let succesfulExit = await plasma.succesfulExits(bobHash); 380 | assert(!succesfulExit); 381 | succesfulExit = await plasma.succesfulExits(aliceHash); 382 | assert(succesfulExit); 383 | size = await queue.currentSize(); 384 | assert(size.toString(10) === "1"); 385 | minimalItem = await queue.getMin(); 386 | assert(minimalItem === bobHash); 387 | 388 | await increaseTime(exitDelay.toNumber()) 389 | submissionReceipt = await plasma.finalizeExits(100); 390 | newBalanceBob = await web3.eth.getBalance(bob); 391 | assert(newBalanceBob.gt(oldBalanceBob)); 392 | succesfulExit = await plasma.succesfulExits(bobHash); 393 | assert(succesfulExit); 394 | }) 395 | }) 396 | 397 | function prettyPrint(res) { 398 | for (let field of res) { 399 | console.log(field.toString(10)); 400 | } 401 | } 402 | 403 | -------------------------------------------------------------------------------- /test/keys.js: -------------------------------------------------------------------------------- 1 | const addresses = [ 2 | "0x627306090abab3a6e1400e9345bc60c78a8bef57", 3 | "0xf17f52151ebef6c7334fad080c5704d77216b732", 4 | "0xc5fdf4076b8f3a5357c5e395ab970b5b54098fef", 5 | "0x821aea9a577a9b44299b9c15c88cf3087f3b5544", 6 | "0x0d1d4e623d10f9fba5db95830f7d3839406c6af2", 7 | "0x2932b7a2355d6fecc4b5c0b6bd44cc31df247a2e", 8 | "0x2191ef87e392377ec08e7c08eb105ef5448eced5", 9 | "0x0f4f2ac550a1b4e2280d04c21cea7ebd822934b5", 10 | "0x6330a553fc93768f612722bb8c2ec78ac90b3bbc", 11 | "0x5aeda56215b167893e80b4fe645ba6d5bab767de", 12 | ]; 13 | 14 | const keys = [ 15 | Buffer.from("c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3", "hex"), 16 | Buffer.from("ae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f", "hex"), 17 | Buffer.from("0dbbe8e4ae425a6d2687f1a7e3ba17bc98c673636790f1b8ad91193c05875ef1", "hex"), 18 | Buffer.from("c88b703fb08cbea894b6aeff5a544fb92e78a18e19814cd85da83b71f772aa6c", "hex"), 19 | Buffer.from("388c684f0ba1ef5017716adb5d21a053ea8e90277d0868337519f97bede61418", "hex"), 20 | Buffer.from("659cbb0e2411a44db63778987b1e22153c086a95eb6b18bdf89de078917abc63", "hex"), 21 | Buffer.from("82d052c865f5763aad42add438569276c00d3d88a2d062d36b2bae914d58b8c8", "hex"), 22 | Buffer.from("aa3680d5d48a8283413f7a108367c7299ca73f553735860a87b08f39395618b7", "hex"), 23 | Buffer.from("0f62d96d6675f32685bbdb8ac13cda7c23436f63efbb9d07700d8669ff12b7c4", "hex"), 24 | Buffer.from("8d5366123cb560bb606379f90a0bfd4769eecc0557f1b362dcae9012b548b1e5", "hex"), 25 | ]; 26 | 27 | module.exports = { 28 | addresses, keys 29 | }; -------------------------------------------------------------------------------- /test/priorityQueue.js: -------------------------------------------------------------------------------- 1 | const PriorityQueue = artifacts.require('PriorityQueue'); 2 | const util = require("util"); 3 | const ethUtil = require('ethereumjs-util') 4 | const BN = ethUtil.BN; 5 | const t = require('truffle-test-utils') 6 | t.init() 7 | const expectThrow = require("../helpers/expectThrow"); 8 | const {addresses, keys} = require("./keys.js"); 9 | const crypto = require("crypto"); 10 | const exitPartialHashSize = 22; 11 | // const Web3 = require("web3"); 12 | 13 | const increaseTime = async function(addSeconds) { 14 | await web3.currentProvider.send({jsonrpc: "2.0", method: "evm_increaseTime", params: [addSeconds], id: 0}) 15 | await web3.currentProvider.send({jsonrpc: "2.0", method: "evm_mine", params: [], id: 1}) 16 | } 17 | 18 | const { 19 | TxTypeFund, 20 | TxTypeMerge, 21 | TxTypeSplit} = require("../lib/Tx/RLPtx.js"); 22 | 23 | contract('Priority queue', async (accounts) => { 24 | let priorityQueue; 25 | 26 | const operator = accounts[0]; 27 | 28 | beforeEach(async () => { 29 | priorityQueue = await PriorityQueue.new({from: operator}); 30 | }) 31 | 32 | it('should insert one item into queue', async () => { 33 | let priority = new BN(1); 34 | let hash = ethUtil.bufferToHex(crypto.randomBytes(exitPartialHashSize)); 35 | let size = await priorityQueue.currentSize(); 36 | assert(size.toString(10) === "0", "queue size should be zero at deployment"); 37 | let submissionReceipt = await priorityQueue.insert([priority], hash); 38 | console.log("Inserting one item to the queue gas price = " + submissionReceipt.receipt.gasUsed); 39 | let minimalItem = await priorityQueue.getMin(); 40 | assert(minimalItem === hash, "inserted hash should match"); 41 | size = await priorityQueue.currentSize(); 42 | assert(size.toString(10) === "1", "queue size should increase"); 43 | }); 44 | 45 | it('should insert two items into queue', async () => { 46 | let priority = new BN(1); 47 | let hash = ethUtil.bufferToHex(crypto.randomBytes(exitPartialHashSize)); 48 | let submissionReceipt = await priorityQueue.insert([priority], hash); 49 | let size = await priorityQueue.currentSize(); 50 | assert(size.toString(10) === "1"); 51 | priority = new BN(2); 52 | let anotherHash = ethUtil.bufferToHex(crypto.randomBytes(exitPartialHashSize)); 53 | submissionReceipt = await priorityQueue.insert([priority], anotherHash); 54 | size = await priorityQueue.currentSize(); 55 | assert(size.toString(10) === "2"); 56 | let minimalItem = await priorityQueue.getMin(); 57 | assert(minimalItem == hash); 58 | }); 59 | 60 | it('should insert two items into queue with the same priority', async () => { 61 | let priority = new BN(1); 62 | let hash = ethUtil.bufferToHex(crypto.randomBytes(exitPartialHashSize)); 63 | let submissionReceipt = await priorityQueue.insert([priority], hash); 64 | let size = await priorityQueue.currentSize(); 65 | assert(size.toString(10) === "1"); 66 | priority = new BN(1); 67 | let anotherHash = ethUtil.bufferToHex(crypto.randomBytes(exitPartialHashSize)); 68 | submissionReceipt = await priorityQueue.insert([priority], anotherHash); 69 | size = await priorityQueue.currentSize(); 70 | assert(size.toString(10) === "2"); 71 | let minimalItem = await priorityQueue.getMin(); 72 | assert(minimalItem == hash); 73 | }); 74 | 75 | 76 | // not usable for heap queue 77 | // it('should insert many items and find position', async () => { 78 | // let maxSize = 100 79 | // let mod = new BN(maxSize/2); 80 | // let p; 81 | // let h; 82 | // for (let i = 0; i < maxSize; i++) { 83 | // let priority = new BN(crypto.randomBytes(1)); 84 | // let hash = ethUtil.bufferToHex(crypto.randomBytes(exitPartialHashSize)); 85 | // await priorityQueue.insert([priority], hash); 86 | // if (i == maxSize/2) { 87 | // p = priority 88 | // h = hash 89 | // } 90 | // } 91 | // let size = await priorityQueue.currentSize(); 92 | // assert(size.toString(10) === "" + maxSize); 93 | // let position = await priorityQueue.getEstimateQueuePositionForPriority([p]); 94 | // let item = await priorityQueue.heapList(position) 95 | // assert(item[0].toNumber() >= p.toNumber()) 96 | // // assert(item[0].toString(10) === p.toString(10)) 97 | // // assert(item[1] === h); 98 | // }); 99 | 100 | it('should insert many items and pop with checking priority', async () => { 101 | let maxSize = 100 102 | for (let i = 0; i < maxSize; i++) { 103 | let priority = new BN(crypto.randomBytes(1)); 104 | let hash = ethUtil.bufferToHex(crypto.randomBytes(exitPartialHashSize)); 105 | await priorityQueue.insert([priority], hash); 106 | } 107 | let size = await priorityQueue.currentSize(); 108 | assert(size.toString(10) === "" + maxSize); 109 | let prevPrior = 0 110 | for (let i = 0; i < maxSize; i++) { 111 | let item = await priorityQueue.heapList(1); 112 | assert(item[0].toNumber() >= prevPrior); 113 | prevPrior = item[0].toNumber(); 114 | await priorityQueue.delMin() 115 | } 116 | }); 117 | }) -------------------------------------------------------------------------------- /test/testInvalidBlockChallenges.js: -------------------------------------------------------------------------------- 1 | const util = require("util"); 2 | const ethUtil = require('ethereumjs-util') 3 | const BN = ethUtil.BN; 4 | const t = require('truffle-test-utils') 5 | t.init() 6 | const expectThrow = require("../helpers/expectThrow"); 7 | const {addresses, keys} = require("./keys.js"); 8 | const {createTransaction, parseTransactionIndex} = require("./createTransaction"); 9 | const {createBlock, createMerkleTree} = require("./createBlock"); 10 | const testUtils = require('./utils'); 11 | const deploy = require("./deploy"); 12 | 13 | // const Web3 = require("web3"); 14 | 15 | const increaseTime = async function(addSeconds) { 16 | await web3.currentProvider.send({jsonrpc: "2.0", method: "evm_increaseTime", params: [addSeconds], id: 0}) 17 | await web3.currentProvider.send({jsonrpc: "2.0", method: "evm_mine", params: [], id: 1}) 18 | } 19 | 20 | const { 21 | TxTypeFund, 22 | TxTypeMerge, 23 | TxTypeSplit} = require("../lib/Tx/RLPtx.js"); 24 | 25 | contract('PlasmaParent invalid block challenges', async (accounts) => { 26 | 27 | const operatorAddress = accounts[0]; 28 | const operatorKey = keys[0]; 29 | 30 | let queue; 31 | let plasma; 32 | let storage; 33 | let firstHash; 34 | 35 | const operator = accounts[0]; 36 | 37 | const alice = addresses[2]; 38 | const aliceKey = keys[2]; 39 | const bob = addresses[3]; 40 | const bobKey = keys[3]; 41 | 42 | beforeEach(async () => { 43 | const result = await deploy(operator, operatorAddress); 44 | ({plasma, firstHash, queue, storage} = result); 45 | }) 46 | 47 | it('Transaction in block references the future', async () => { 48 | const tx = createTransaction(TxTypeSplit, 0, 49 | [{ 50 | blockNumber: 100, 51 | txNumberInBlock: 0, 52 | outputNumberInTransaction: 0, 53 | amount: 10 54 | }], 55 | [{ 56 | amount: 10, 57 | to: alice 58 | }], 59 | aliceKey 60 | ) 61 | let block = createBlock(1, 1, firstHash, [tx], operatorKey) 62 | const reencodedTX = tx.serialize(); 63 | const proof = block.merkleTree.getProof(0, true); 64 | const blockOneArray = block.serialize(); 65 | const blockOne = Buffer.concat(blockOneArray); 66 | const blockOneHeader = blockOne.slice(0,137); 67 | const deserialization = ethUtil.rlp.decode(blockOneArray[7]); 68 | let lastBlockNumber = await plasma.lastBlockNumber() 69 | let lastBlockHash = await plasma.hashOfLastSubmittedBlock() 70 | lastBlockHash = await plasma.hashOfLastSubmittedBlock() 71 | assert(lastBlockNumber.toString() == "0"); 72 | let submissionReceipt = await plasma.submitBlockHeaders(ethUtil.bufferToHex(blockOneHeader)); 73 | lastBlockNumber = await plasma.lastBlockNumber(); 74 | assert(lastBlockNumber.toString() == "1"); 75 | await testUtils.expectEvents( 76 | storage, 77 | submissionReceipt.receipt.blockNumber, 78 | 'BlockHeaderSubmitted', 79 | {_blockNumber: 1, _merkleRoot: ethUtil.bufferToHex(block.header.merkleRootHash)} 80 | ); 81 | let bl = await storage.blocks(1); 82 | assert(bl[2] == ethUtil.bufferToHex(block.header.merkleRootHash)); 83 | // let root = await storage.getMerkleRoot(1); 84 | // assert(root = ethUtil.bufferToHex(ethUtil.hashPersonalMessage(reencodedTX))); 85 | submissionReceipt = await plasma.proveReferencingInvalidBlock(1, 0, ethUtil.bufferToHex(reencodedTX), ethUtil.bufferToHex(proof)); 86 | const plasmaIsStopped = await plasma.plasmaErrorFound(); 87 | assert(plasmaIsStopped); 88 | }) 89 | 90 | it('Spend without owner signature', async () => { 91 | // first we fund Alice with something 92 | 93 | let tx = createTransaction(TxTypeFund, 0, 94 | [{ 95 | blockNumber: 0, 96 | txNumberInBlock: 0, 97 | outputNumberInTransaction: 0, 98 | amount: 1 99 | }], 100 | [{ 101 | amount: 100, 102 | to: alice 103 | }], 104 | operatorKey 105 | ) 106 | let block = createBlock(1, 1, firstHash, [tx], operatorKey) 107 | const reencodedTX = tx.serialize(); 108 | const proof = block.merkleTree.getProof(0, true); 109 | let blockArray = block.serialize(); 110 | let blockHeader = Buffer.concat(blockArray).slice(0,137); 111 | let deserialization = ethUtil.rlp.decode(blockArray[7]); 112 | let lastBlockNumber = await plasma.lastBlockNumber() 113 | assert(lastBlockNumber.toString() == "0"); 114 | let submissionReceipt = await plasma.submitBlockHeaders(ethUtil.bufferToHex(blockHeader)); 115 | lastBlockNumber = await plasma.lastBlockNumber(); 116 | assert(lastBlockNumber.toString() == "1"); 117 | let allEvents = storage.allEvents({fromBlock: submissionReceipt.receipt.blockNumber, toBlock: submissionReceipt.receipt.blockNumber}); 118 | let get = util.promisify(allEvents.get.bind(allEvents)) 119 | let evs = await get() 120 | assert.web3Event({logs: evs}, { 121 | event: 'BlockHeaderSubmitted', 122 | args: {_blockNumber: 1, 123 | _merkleRoot: ethUtil.bufferToHex(block.header.merkleRootHash)} 124 | }, 'The event is emitted'); 125 | let bl = await storage.blocks(1); 126 | assert(bl[2] == ethUtil.bufferToHex(block.header.merkleRootHash)); 127 | 128 | // than we spend an output, but now Bob signs instead of Alice 129 | 130 | const newHash = await plasma.hashOfLastSubmittedBlock(); 131 | const tx2 = createTransaction(TxTypeSplit, 0, 132 | [{ 133 | blockNumber: 1, 134 | txNumberInBlock: 0, 135 | outputNumberInTransaction: 0, 136 | amount: 100 137 | }], 138 | [{ 139 | amount: 100, 140 | to: bob 141 | }], 142 | bobKey 143 | ) 144 | block = createBlock(2, 1, newHash, [tx2], operatorKey) 145 | const reencodedTX2 = tx2.serialize(); 146 | const proof2 = block.merkleTree.getProof(0, true); 147 | blockArray = block.serialize(); 148 | blockHeader = Buffer.concat(blockArray).slice(0,137); 149 | deserialization = ethUtil.rlp.decode(blockArray[7]); 150 | submissionReceipt = await plasma.submitBlockHeaders(ethUtil.bufferToHex(blockHeader)); 151 | lastBlockNumber = await plasma.lastBlockNumber(); 152 | 153 | allEvents = storage.allEvents({fromBlock: submissionReceipt.receipt.blockNumber, toBlock: submissionReceipt.receipt.blockNumber}); 154 | get = util.promisify(allEvents.get.bind(allEvents)) 155 | evs = await get() 156 | assert.web3Event({logs: evs}, { 157 | event: 'BlockHeaderSubmitted', 158 | args: {_blockNumber: 2, 159 | _merkleRoot: ethUtil.bufferToHex(block.header.merkleRootHash)} 160 | }, 'The event is emitted'); 161 | 162 | bl = await storage.blocks(2); 163 | assert(bl[2] == ethUtil.bufferToHex(block.header.merkleRootHash)); 164 | // uint32 _plasmaBlockNumber, //references and proves ownership on withdraw transaction 165 | // uint32 _plasmaTxNumInBlock, 166 | // bytes _plasmaTransaction, 167 | // bytes _merkleProof, 168 | // uint32 _originatingPlasmaBlockNumber, //references and proves ownership on output of original transaction 169 | // uint32 _originatingPlasmaTxNumInBlock, 170 | // bytes _originatingPlasmaTransaction, 171 | // bytes _originatingMerkleProof, 172 | // uint256 _inputOfInterest 173 | 174 | submissionReceipt = await plasma.proveBalanceOrOwnershipBreakingBetweenInputAndOutput( 175 | 2, ethUtil.bufferToHex(reencodedTX2), ethUtil.bufferToHex(proof2), 176 | 1, ethUtil.bufferToHex(reencodedTX), ethUtil.bufferToHex(proof), 177 | 0); 178 | const plasmaIsStopped = await plasma.plasmaErrorFound(); 179 | assert(plasmaIsStopped); 180 | }) 181 | 182 | it('UTXO amount is not equal to input amount', async () => { 183 | // first we fund Alice with something 184 | 185 | let tx = createTransaction(TxTypeFund, 0, 186 | [{ 187 | blockNumber: 0, 188 | txNumberInBlock: 0, 189 | outputNumberInTransaction: 0, 190 | amount: 1 191 | }], 192 | [{ 193 | amount: 100, 194 | to: alice 195 | }], 196 | operatorKey 197 | ) 198 | let block = createBlock(1, 1, firstHash, [tx], operatorKey) 199 | const reencodedTX = tx.serialize(); 200 | const proof = block.merkleTree.getProof(0, true); 201 | let blockArray = block.serialize(); 202 | let blockHeader = Buffer.concat(blockArray).slice(0,137); 203 | let deserialization = ethUtil.rlp.decode(blockArray[7]); 204 | let lastBlockNumber = await plasma.lastBlockNumber() 205 | assert(lastBlockNumber.toString() == "0"); 206 | let submissionReceipt = await plasma.submitBlockHeaders(ethUtil.bufferToHex(blockHeader)); 207 | lastBlockNumber = await plasma.lastBlockNumber(); 208 | assert(lastBlockNumber.toString() == "1"); 209 | let allEvents = storage.allEvents({fromBlock: submissionReceipt.receipt.blockNumber, toBlock: submissionReceipt.receipt.blockNumber}); 210 | let get = util.promisify(allEvents.get.bind(allEvents)) 211 | let evs = await get() 212 | assert.web3Event({logs: evs}, { 213 | event: 'BlockHeaderSubmitted', 214 | args: {_blockNumber: 1, 215 | _merkleRoot: ethUtil.bufferToHex(block.header.merkleRootHash)} 216 | }, 'The event is emitted'); 217 | let bl = await storage.blocks(1); 218 | assert(bl[2] == ethUtil.bufferToHex(block.header.merkleRootHash)); 219 | 220 | // than we spend an output, but now Bob signs instead of Alice 221 | 222 | const newHash = await plasma.hashOfLastSubmittedBlock(); 223 | const tx2 = createTransaction(TxTypeSplit, 0, 224 | [{ 225 | blockNumber: 1, 226 | txNumberInBlock: 0, 227 | outputNumberInTransaction: 0, 228 | amount: 1000 229 | }], 230 | [{ 231 | amount: 1000, 232 | to: bob 233 | }], 234 | aliceKey 235 | ) 236 | block = createBlock(2, 1, newHash, [tx2], operatorKey) 237 | const reencodedTX2 = tx2.serialize(); 238 | const proof2 = block.merkleTree.getProof(0, true); 239 | blockArray = block.serialize(); 240 | blockHeader = Buffer.concat(blockArray).slice(0,137); 241 | deserialization = ethUtil.rlp.decode(blockArray[7]); 242 | submissionReceipt = await plasma.submitBlockHeaders(ethUtil.bufferToHex(blockHeader)); 243 | lastBlockNumber = await plasma.lastBlockNumber(); 244 | 245 | allEvents = storage.allEvents({fromBlock: submissionReceipt.receipt.blockNumber, toBlock: submissionReceipt.receipt.blockNumber}); 246 | get = util.promisify(allEvents.get.bind(allEvents)) 247 | evs = await get() 248 | assert.web3Event({logs: evs}, { 249 | event: 'BlockHeaderSubmitted', 250 | args: {_blockNumber: 2, 251 | _merkleRoot: ethUtil.bufferToHex(block.header.merkleRootHash)} 252 | }, 'The event is emitted'); 253 | 254 | bl = await storage.blocks(2); 255 | assert(bl[2] == ethUtil.bufferToHex(block.header.merkleRootHash)); 256 | // uint32 _plasmaBlockNumber, //references and proves ownership on withdraw transaction 257 | // uint32 _plasmaTxNumInBlock, 258 | // bytes _plasmaTransaction, 259 | // bytes _merkleProof, 260 | // uint32 _originatingPlasmaBlockNumber, //references and proves ownership on output of original transaction 261 | // uint32 _originatingPlasmaTxNumInBlock, 262 | // bytes _originatingPlasmaTransaction, 263 | // bytes _originatingMerkleProof, 264 | // uint256 _inputOfInterest 265 | 266 | submissionReceipt = await plasma.proveBalanceOrOwnershipBreakingBetweenInputAndOutput( 267 | 2, ethUtil.bufferToHex(reencodedTX2), ethUtil.bufferToHex(proof2), 268 | 1, ethUtil.bufferToHex(reencodedTX), ethUtil.bufferToHex(proof), 269 | 0); 270 | const plasmaIsStopped = await plasma.plasmaErrorFound(); 271 | assert(plasmaIsStopped); 272 | }) 273 | 274 | it('Double spend', async () => { 275 | // first we fund Alice with something 276 | 277 | let tx = createTransaction(TxTypeFund, 0, 278 | [{ 279 | blockNumber: 0, 280 | txNumberInBlock: 0, 281 | outputNumberInTransaction: 0, 282 | amount: 1 283 | }], 284 | [{ 285 | amount: 100, 286 | to: alice 287 | }], 288 | operatorKey 289 | ) 290 | let block = createBlock(1, 1, firstHash, [tx], operatorKey) 291 | const reencodedTX = tx.serialize(); 292 | const proof = block.merkleTree.getProof(0, true); 293 | let blockArray = block.serialize(); 294 | let blockHeader = Buffer.concat(blockArray).slice(0,137); 295 | let deserialization = ethUtil.rlp.decode(blockArray[7]); 296 | let lastBlockNumber = await plasma.lastBlockNumber() 297 | assert(lastBlockNumber.toString() == "0"); 298 | let submissionReceipt = await plasma.submitBlockHeaders(ethUtil.bufferToHex(blockHeader)); 299 | lastBlockNumber = await plasma.lastBlockNumber(); 300 | assert(lastBlockNumber.toString() == "1"); 301 | let allEvents = storage.allEvents({fromBlock: submissionReceipt.receipt.blockNumber, toBlock: submissionReceipt.receipt.blockNumber}); 302 | let get = util.promisify(allEvents.get.bind(allEvents)) 303 | let evs = await get() 304 | assert.web3Event({logs: evs}, { 305 | event: 'BlockHeaderSubmitted', 306 | args: {_blockNumber: 1, 307 | _merkleRoot: ethUtil.bufferToHex(block.header.merkleRootHash)} 308 | }, 'The event is emitted'); 309 | let bl = await storage.blocks(1); 310 | assert(bl[2] == ethUtil.bufferToHex(block.header.merkleRootHash)); 311 | 312 | // than we spend an output, but now Bob signs instead of Alice 313 | 314 | const newHash = await plasma.hashOfLastSubmittedBlock(); 315 | const tx2 = createTransaction(TxTypeSplit, 0, 316 | [{ 317 | blockNumber: 1, 318 | txNumberInBlock: 0, 319 | outputNumberInTransaction: 0, 320 | amount: 100 321 | }], 322 | [{ 323 | amount: 100, 324 | to: bob 325 | }], 326 | aliceKey 327 | ) 328 | const tx3 = createTransaction(TxTypeSplit, 1, 329 | [{ 330 | blockNumber: 1, 331 | txNumberInBlock: 0, 332 | outputNumberInTransaction: 0, 333 | amount: 100 334 | }], 335 | [{ 336 | amount: 100, 337 | to: bob 338 | }], 339 | aliceKey 340 | ) 341 | block = createBlock(2, 2, newHash, [tx2, tx3], operatorKey) 342 | const reencodedTX2 = tx2.serialize(); 343 | const proof2 = Buffer.concat(block.merkleTree.getProof(0, true)); 344 | 345 | const reencodedTX3 = tx3.serialize(); 346 | const proof3 = Buffer.concat(block.merkleTree.getProof(1, true)); 347 | 348 | blockArray = block.serialize(); 349 | blockHeader = Buffer.concat(blockArray).slice(0,137); 350 | deserialization = ethUtil.rlp.decode(blockArray[7]); 351 | submissionReceipt = await plasma.submitBlockHeaders(ethUtil.bufferToHex(blockHeader)); 352 | lastBlockNumber = await plasma.lastBlockNumber(); 353 | 354 | allEvents = storage.allEvents({fromBlock: submissionReceipt.receipt.blockNumber, toBlock: submissionReceipt.receipt.blockNumber}); 355 | get = util.promisify(allEvents.get.bind(allEvents)) 356 | evs = await get() 357 | assert.web3Event({logs: evs}, { 358 | event: 'BlockHeaderSubmitted', 359 | args: {_blockNumber: 2, 360 | _merkleRoot: ethUtil.bufferToHex(block.header.merkleRootHash)} 361 | }, 'The event is emitted'); 362 | 363 | bl = await storage.blocks(2); 364 | assert(bl[2] == ethUtil.bufferToHex(block.header.merkleRootHash)); 365 | // function proveDoubleSpend(uint32 _plasmaBlockNumber1, //references and proves transaction number 1 366 | // uint32 _plasmaTxNumInBlock1, 367 | // uint8 _inputNumber1, 368 | // bytes _plasmaTransaction1, 369 | // bytes _merkleProof1, 370 | // uint32 _plasmaBlockNumber2, //references and proves transaction number 2 371 | // uint32 _plasmaTxNumInBlock2, 372 | // uint8 _inputNumber2, 373 | // bytes _plasmaTransaction2, 374 | // bytes _merkleProof2) 375 | 376 | submissionReceipt = await plasma.proveDoubleSpend( 377 | 2, 0, ethUtil.bufferToHex(reencodedTX2), ethUtil.bufferToHex(proof2), 378 | 2, 0, ethUtil.bufferToHex(reencodedTX3), ethUtil.bufferToHex(proof3)); 379 | const plasmaIsStopped = await plasma.plasmaErrorFound(); 380 | assert(plasmaIsStopped); 381 | }) 382 | 383 | it('UTXO was successfully withdrawn and than spent in Plasma', async () => { 384 | const withdrawCollateral = await plasma.WithdrawCollateral(); 385 | await plasma.deposit({from: alice, value: "100"}); 386 | 387 | const allTXes = []; 388 | const fundTX = createTransaction(TxTypeFund, 0, 389 | [{ 390 | blockNumber: 0, 391 | txNumberInBlock: 0, 392 | outputNumberInTransaction: 0, 393 | amount: 0 394 | }], 395 | [{ 396 | amount: 100, 397 | to: alice 398 | }], 399 | operatorKey 400 | ) 401 | allTXes.push(fundTX) 402 | const block = createBlock(1, allTXes.length, firstHash, allTXes, operatorKey) 403 | await testUtils.submitBlock(plasma, block); 404 | 405 | let proofObject = block.getProofForTransactionByNumber(0); 406 | let {proof, tx} = proofObject; 407 | let submissionReceipt = await plasma.startExit( 408 | 1, 0, ethUtil.bufferToHex(tx.serialize()), ethUtil.bufferToHex(proof), 409 | {from: alice, value: withdrawCollateral} 410 | ) 411 | console.log("Single exit gas price for exiting a deposit transaction is " + submissionReceipt.receipt.gasUsed) 412 | 413 | const transactionPublishedEvent = submissionReceipt.logs[0] 414 | const txHashFromEvent = transactionPublishedEvent.args._hash; 415 | const txDataFromEvent = transactionPublishedEvent.args._data; 416 | let exitRecordHash = submissionReceipt.logs[2].args._partialHash; 417 | let exitRecord = await plasma.exitRecords(exitRecordHash); 418 | const txData = ethUtil.bufferToHex(tx.serialize()) 419 | const txHash = ethUtil.bufferToHex(ethUtil.sha3(proofObject.tx.serialize())) 420 | 421 | assert(exitRecord[0] === txHash); 422 | assert(exitRecord[1].toNumber() === 100) 423 | assert(exitRecord[2] === alice); 424 | assert(exitRecord[4].toString(10) === "1") 425 | assert(exitRecord[5].toNumber() === 0) 426 | assert(exitRecord[6].toNumber() === 0) 427 | assert(exitRecord[7] === true) 428 | assert(exitRecord[8] === false) 429 | 430 | assert(txHash === txHashFromEvent); 431 | assert(txData === txDataFromEvent); 432 | 433 | let nextHash = await plasma.hashOfLastSubmittedBlock(); 434 | const txToSpend = allTXes[0]; 435 | const spendingTX = createTransaction(TxTypeSplit, 0, 436 | [{ 437 | blockNumber: 1, 438 | txNumberInBlock: 0, 439 | outputNumberInTransaction: 0, 440 | amount: 100 441 | }], 442 | [{ 443 | amount: 100, 444 | to: alice 445 | }], 446 | aliceKey 447 | ) 448 | const block2 = createBlock(2, 1, nextHash, [spendingTX], operatorKey) 449 | await testUtils.submitBlock(plasma, block2); 450 | 451 | let proofObject2 = block2.getProofForTransactionByNumber(0); 452 | 453 | let exitDelay = await plasma.ExitDelay() 454 | await increaseTime(exitDelay.toNumber() + 1) 455 | 456 | submissionReceipt = await plasma.finalizeExits(2); 457 | 458 | // function proveSpendAndWithdraw( 459 | // uint32 _plasmaBlockNumber, //references and proves ownership on withdraw transaction 460 | // bytes _plasmaTransaction, 461 | // bytes _merkleProof, 462 | // bytes _originatingPlasmaTransaction, 463 | // bytes _originatingMerkleProof, 464 | // uint8 _inputNumber, 465 | // bytes22 _partialHash) 466 | 467 | submissionReceipt = await plasma.proveSpendAndWithdraw(2, 468 | ethUtil.bufferToHex(proofObject2.tx.serialize()), 469 | ethUtil.bufferToHex(proofObject2.proof), 470 | ethUtil.bufferToHex(tx.serialize()), 471 | ethUtil.bufferToHex(proof), 472 | 0, {from: bob}); 473 | const plasmaIsStopped = await plasma.plasmaErrorFound(); 474 | assert(plasmaIsStopped); 475 | 476 | }) 477 | 478 | }) -------------------------------------------------------------------------------- /test/transactionSerialization.js: -------------------------------------------------------------------------------- 1 | const TXTester = artifacts.require("TXTester"); 2 | const util = require("util"); 3 | const ethUtil = require('ethereumjs-util') 4 | const BN = ethUtil.BN; 5 | const t = require('truffle-test-utils') 6 | t.init() 7 | const expectThrow = require("../helpers/expectThrow"); 8 | const {addresses, keys} = require("./keys.js"); 9 | const {createTransaction} = require("./createTransaction"); 10 | const {createBlock, createMerkleTree} = require("./createBlock"); 11 | const {PlasmaTransactionWithSignature} = require("../lib/Tx/RLPtxWithSignature"); 12 | const testUtils = require('./utils'); 13 | 14 | // const Web3 = require("web3"); 15 | 16 | const increaseTime = async function(addSeconds) { 17 | await web3.currentProvider.send({jsonrpc: "2.0", method: "evm_increaseTime", params: [addSeconds], id: 0}) 18 | await web3.currentProvider.send({jsonrpc: "2.0", method: "evm_mine", params: [], id: 1}) 19 | } 20 | 21 | const { 22 | TxTypeFund, 23 | TxTypeMerge, 24 | TxTypeSplit} = require("../lib/Tx/RLPtx.js"); 25 | 26 | contract('Transaction deserialization tester', async (accounts) => { 27 | 28 | const operatorAddress = accounts[0]; 29 | const operatorKey = keys[0]; 30 | let txTester; 31 | 32 | const operator = accounts[0]; 33 | 34 | const alice = addresses[2]; 35 | const aliceKey = keys[2]; 36 | const bob = addresses[3]; 37 | const bobKey = keys[3]; 38 | 39 | let firstHash; 40 | 41 | beforeEach(async () => { 42 | txTester = await TXTester.new({from: operator}); 43 | }) 44 | 45 | it('should encode and decode the transaction locally', () => { 46 | const tx = createTransaction(TxTypeSplit, 100, 47 | [{ 48 | blockNumber: 1, 49 | txNumberInBlock: 200, 50 | outputNumberInTransaction: 0, 51 | amount: 10 52 | }], 53 | [{ 54 | amount: 10, 55 | to: alice 56 | }], 57 | aliceKey 58 | ) 59 | const reencodedTX = tx.serialize(); 60 | const parsedTX = new PlasmaTransactionWithSignature(reencodedTX); 61 | assert(ethUtil.bufferToHex(parsedTX.from) == alice); 62 | }) 63 | 64 | it('should give proper information about the TX', async () => { 65 | const tx = createTransaction(TxTypeSplit, 100, 66 | [{ 67 | blockNumber: 1, 68 | txNumberInBlock: 200, 69 | outputNumberInTransaction: 0, 70 | amount: 10 71 | }], 72 | [{ 73 | amount: 10, 74 | to: alice 75 | }], 76 | aliceKey 77 | ) 78 | const reencodedTX = tx.serialize(); 79 | const info = await txTester.parseTransaction(ethUtil.bufferToHex(reencodedTX)); 80 | const txNumberInBlock = info[0].toNumber(); 81 | const txType = info[1].toNumber(); 82 | const inputsLength = info[2].toNumber(); 83 | const outputsLength = info[3].toNumber(); 84 | const sender = info[4]; 85 | const isWellFormed = info[5]; 86 | assert(isWellFormed); 87 | assert(sender === alice); 88 | assert(txType === TxTypeSplit); 89 | assert(inputsLength === 1); 90 | assert(outputsLength === 1); 91 | // assert(txNumberInBlock === 100); 92 | }); 93 | 94 | it('should give proper information about the TX input', async () => { 95 | const tx = createTransaction(TxTypeSplit, 0, 96 | [{ 97 | blockNumber: 1, 98 | txNumberInBlock: 200, 99 | outputNumberInTransaction: 0, 100 | amount: 10 101 | }], 102 | [{ 103 | amount: 10, 104 | to: alice 105 | }], 106 | aliceKey 107 | ) 108 | const reencodedTX = tx.serialize(); 109 | const info = await txTester.getInputInfo(ethUtil.bufferToHex(reencodedTX), 0); 110 | const blockNumber = info[0].toNumber(); 111 | const txNumberInBlock = info[1].toNumber(); 112 | const outputNumber = info[2].toNumber(); 113 | const amount = info[3].toString(10); 114 | assert(blockNumber === 1); 115 | // assert(txNumberInBlock === 200); 116 | assert(outputNumber === 0); 117 | assert(amount === ""+10); 118 | }); 119 | 120 | it('should give proper information about the TX output', async () => { 121 | const tx = createTransaction(TxTypeSplit, 0, 122 | [{ 123 | blockNumber: 1, 124 | txNumberInBlock: 200, 125 | outputNumberInTransaction: 0, 126 | amount: 10 127 | }], 128 | [{ 129 | amount: 10, 130 | to: bob 131 | }], 132 | aliceKey 133 | ) 134 | const reencodedTX = tx.serialize(); 135 | const info = await txTester.getOutputInfo(ethUtil.bufferToHex(reencodedTX), 0); 136 | const outputNumber = info[0].toNumber(); 137 | const recipient = info[1]; 138 | const amount = info[2].toString(10); 139 | assert(outputNumber === 0); 140 | assert(recipient === bob); 141 | assert(amount === ""+10); 142 | }); 143 | 144 | it('should give proper information about the TX in block', async () => { 145 | const tx = createTransaction(TxTypeSplit, 100, 146 | [{ 147 | blockNumber: 1, 148 | txNumberInBlock: 200, 149 | outputNumberInTransaction: 0, 150 | amount: 10 151 | }], 152 | [{ 153 | amount: 10, 154 | to: alice 155 | }], 156 | aliceKey 157 | ) 158 | const tx2 = createTransaction(TxTypeSplit, 100, 159 | [{ 160 | blockNumber: 1, 161 | txNumberInBlock: 100, 162 | outputNumberInTransaction: 0, 163 | amount: 10 164 | }], 165 | [{ 166 | amount: 10, 167 | to: alice 168 | }], 169 | aliceKey 170 | ) 171 | const block = createBlock(2, 2, Buffer.alloc(32), [tx, tx2], operatorKey); 172 | const reencodedTX2 = tx2.serialize(); 173 | let proof = block.getProofForTransaction(tx.serialize()); 174 | let proof2 = block.getProofForTransaction(tx2.serialize()); 175 | proof = proof.proof 176 | proof2 = proof2.proof 177 | assert(!proof2.equals(proof)); 178 | const root = block.getMerkleHash(); 179 | const info = await txTester.parseFromBlock(ethUtil.bufferToHex(reencodedTX2), ethUtil.bufferToHex(proof2), ethUtil.bufferToHex(root)); 180 | const txNumberInBlock = info[0].toNumber(); 181 | const txType = info[1].toNumber(); 182 | const inputsLength = info[2].toNumber(); 183 | const outputsLength = info[3].toNumber(); 184 | const sender = info[4]; 185 | const isWellFormed = info[5]; 186 | assert(txNumberInBlock === 1); 187 | assert(isWellFormed); 188 | assert(sender === alice); 189 | assert(txType === TxTypeSplit); 190 | assert(inputsLength === 1); 191 | assert(outputsLength === 1); 192 | }); 193 | 194 | it('should parse deeped block', async () => { 195 | const tx = createTransaction(TxTypeSplit, 100, 196 | [{ 197 | blockNumber: 1, 198 | txNumberInBlock: 200, 199 | outputNumberInTransaction: 0, 200 | amount: 10 201 | }], 202 | [{ 203 | amount: 10, 204 | to: alice 205 | }], 206 | aliceKey 207 | ) 208 | const tx2 = createTransaction(TxTypeSplit, 100, 209 | [{ 210 | blockNumber: 1, 211 | txNumberInBlock: 100, 212 | outputNumberInTransaction: 0, 213 | amount: 10 214 | }], 215 | [{ 216 | amount: 10, 217 | to: alice 218 | }], 219 | aliceKey 220 | ) 221 | const tx3 = createTransaction(TxTypeSplit, 100, 222 | [{ 223 | blockNumber: 1, 224 | txNumberInBlock: 300, 225 | outputNumberInTransaction: 0, 226 | amount: 100 227 | }], 228 | [{ 229 | amount: 100, 230 | to: alice 231 | }], 232 | aliceKey 233 | ) 234 | const block = createBlock(2, 2, Buffer.alloc(32), [tx, tx2, tx3], operatorKey); 235 | const reencodedTX3 = tx3.serialize(); 236 | let proof = block.getProofForTransaction(tx.serialize()); 237 | let proof2 = block.getProofForTransaction(tx2.serialize()); 238 | let proof3 = block.getProofForTransaction(tx3.serialize()); 239 | proof = proof.proof 240 | proof2 = proof2.proof 241 | proof3 = proof3.proof 242 | assert(!proof2.equals(proof)); 243 | const proof3copy = Buffer.concat(block.merkleTree.getProof(2, true)); 244 | assert(Buffer(proof3copy).equals(Buffer(proof3))); 245 | const root = block.getMerkleHash(); 246 | const info = await txTester.parseFromBlock(ethUtil.bufferToHex(reencodedTX3), ethUtil.bufferToHex(proof3), ethUtil.bufferToHex(root)); 247 | // console.log(ethUtil.bufferToHex(reencodedTX3)); 248 | // console.log(ethUtil.bufferToHex(proof3)); 249 | // console.log(ethUtil.bufferToHex(root)); 250 | const txNumberInBlock = info[0].toNumber(); 251 | const txType = info[1].toNumber(); 252 | const inputsLength = info[2].toNumber(); 253 | const outputsLength = info[3].toNumber(); 254 | const sender = info[4]; 255 | const isWellFormed = info[5]; 256 | assert(txNumberInBlock === 2); 257 | assert(isWellFormed); 258 | assert(sender === alice); 259 | assert(txType === TxTypeSplit); 260 | assert(inputsLength === 1); 261 | assert(outputsLength === 1); 262 | }); 263 | 264 | it('should parse deeped block 2', async () => { 265 | const tx = createTransaction(TxTypeSplit, 100, 266 | [{ 267 | blockNumber: 1, 268 | txNumberInBlock: 200, 269 | outputNumberInTransaction: 0, 270 | amount: 10 271 | }], 272 | [{ 273 | amount: 10, 274 | to: alice 275 | }], 276 | aliceKey 277 | ) 278 | const tx2 = createTransaction(TxTypeSplit, 100, 279 | [{ 280 | blockNumber: 1, 281 | txNumberInBlock: 100, 282 | outputNumberInTransaction: 0, 283 | amount: 10 284 | }], 285 | [{ 286 | amount: 10, 287 | to: alice 288 | }], 289 | aliceKey 290 | ) 291 | const tx3 = createTransaction(TxTypeSplit, 100, 292 | [{ 293 | blockNumber: 1, 294 | txNumberInBlock: 300, 295 | outputNumberInTransaction: 0, 296 | amount: 100 297 | }], 298 | [{ 299 | amount: 100, 300 | to: alice 301 | }], 302 | aliceKey 303 | ) 304 | const block = createBlock(2, 2, Buffer.alloc(32), [tx, tx2, tx3], operatorKey); 305 | const reencodedTX3 = tx3.serialize(); 306 | let proof = block.getProofForTransaction(tx.serialize()); 307 | let proof2 = block.getProofForTransaction(tx2.serialize()); 308 | let proof3 = block.getProofForTransaction(tx3.serialize()); 309 | proof = proof.proof 310 | proof2 = proof2.proof 311 | proof3 = proof3.proof 312 | assert(!proof2.equals(proof)); 313 | const proof3copy = Buffer.concat(block.merkleTree.getProof(2, true)); 314 | assert(Buffer(proof3copy).equals(Buffer(proof3))); 315 | const root = block.getMerkleHash(); 316 | const info = await txTester.parseFromBlockLimited(ethUtil.bufferToHex(reencodedTX3), ethUtil.bufferToHex(proof3), ethUtil.bufferToHex(root)); 317 | // console.log(ethUtil.bufferToHex(reencodedTX3)); 318 | // console.log(ethUtil.bufferToHex(proof3)); 319 | // console.log(ethUtil.bufferToHex(root)); 320 | const txNumberInBlock = info[1].toNumber(); 321 | assert(info[0]); 322 | assert(txNumberInBlock === 2); 323 | }); 324 | 325 | 326 | 327 | }) -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | const util = require("util"); 2 | const {Block} = require("../lib/Block/RLPblock"); 3 | const ethUtil = require('ethereumjs-util'); 4 | 5 | module.exports = { 6 | /** 7 | * Asserts that even is emitted 8 | * 9 | * @param contract A contract instance to check 10 | * @param {int} blockNumber Block number to look at 11 | * @param {(string|Array)} event Event name or an array of events 12 | * @param {Object} args Optional event args 13 | * @returns {Promise} 14 | */ 15 | async expectEvents(contract, blockNumber, event, args = {}) { 16 | let allEvents = contract.allEvents({fromBlock: blockNumber, toBlock: blockNumber}); 17 | let get = util.promisify(allEvents.get.bind(allEvents)); 18 | let evs = await get(); 19 | assert.web3Events({logs: evs}, Array.isArray(event) ? event : [{event, args}], 'The event is emitted'); 20 | }, 21 | 22 | /** 23 | * Submit a block header to the Plasma contract 24 | * 25 | * @param plasma The Plasma contract 26 | * @param {Block} block A block to submit 27 | * @returns {Promise} 28 | */ 29 | async submitBlock(plasma, block) { 30 | const blockHeader = Buffer.concat(block.serialize()).slice(0,137); 31 | await plasma.submitBlockHeaders(ethUtil.bufferToHex(blockHeader)); 32 | }, 33 | 34 | depositIndex(blockNumber) { 35 | return (new web3.BigNumber(blockNumber)).mul(new web3.BigNumber(2).pow(32)); 36 | }, 37 | 38 | 39 | async checkPlasmaStopped() { 40 | 41 | } 42 | }; -------------------------------------------------------------------------------- /truffle.js: -------------------------------------------------------------------------------- 1 | const env = process.env; 2 | // don't load .env file in prod 3 | 4 | module.exports = { 5 | networks: { 6 | mainnet: { 7 | provider: function() { 8 | if (env.NODE_ENV !== 'production') { 9 | require('dotenv').load(); 10 | } 11 | let WalletProvider = require("truffle-wallet-provider"); 12 | let NonceTrackerSubprovider = require("web3-provider-engine/subproviders/nonce-tracker"); 13 | let walletEth = require('ethereumjs-wallet').fromPrivateKey(Buffer.from(env.ETH_KEY, 'hex')); 14 | let wallet = new WalletProvider(walletEth, "https://mainnet.infura.io/" + env.INFURA_TOKEN) 15 | let nonceTracker = new NonceTrackerSubprovider() 16 | wallet.engine._providers.unshift(nonceTracker) 17 | nonceTracker.setEngine(wallet.engine) 18 | return wallet 19 | }, 20 | network_id: 1, 21 | gasPrice: 5000000000 22 | // gas: 7000000, 23 | }, 24 | mainnet2: { 25 | provider: function() { 26 | if (env.NODE_ENV !== 'production') { 27 | require('dotenv').load(); 28 | } 29 | let HDWalletProvider = require("truffle-hdwallet-provider"); 30 | return new HDWalletProvider(env.ETH_MNEMONIC, "https://mainnet.infura.io/" + env.INFURA_TOKEN, 0) 31 | }, 32 | network_id: 1 33 | }, 34 | rinkeby: { 35 | provider: function() { 36 | if (env.NODE_ENV !== 'production') { 37 | require('dotenv').load(); 38 | } 39 | let WalletProvider = require("truffle-wallet-provider"); 40 | let wallet = require('ethereumjs-wallet').fromPrivateKey(Buffer.from(env.ETH_KEY, 'hex')); 41 | return new WalletProvider(wallet, "https://rinkeby.infura.io/" + env.INFURA_TOKEN) 42 | }, 43 | network_id: 4, 44 | gasPrice: 1000000000 45 | // gas: 7000000, 46 | }, 47 | rinkeby2: { 48 | provider: function() { 49 | if (env.NODE_ENV !== 'production') { 50 | require('dotenv').load(); 51 | } 52 | let HDWalletProvider = require("truffle-hdwallet-provider"); 53 | return new HDWalletProvider(env.ETH_MNEMONIC, "https://rinkeby.infura.io/" + env.INFURA_TOKEN, 0) 54 | }, 55 | network_id: 4 56 | }, 57 | ganache: { 58 | host: "127.0.0.1", 59 | gas: 7000000, 60 | port: 7545, 61 | network_id: "*", // Match any network id, 62 | }, 63 | development: { 64 | host: '127.0.0.1', 65 | gas: 7000000, 66 | port: 8545, 67 | network_id: '*' // Match any network id 68 | }, 69 | cli: { 70 | host: '127.0.0.1', 71 | gas: 7000000, 72 | port: 9545, 73 | network_id: '*' // Match any network id 74 | } 75 | }, 76 | }; 77 | --------------------------------------------------------------------------------