From 536e4ac2156bfa6add7fc76da3ce123820437fc8 Mon Sep 17 00:00:00 2001 From: colin axner Date: Wed, 11 Dec 2019 22:08:03 +0100 Subject: [PATCH] merge develop to master (#176) * update discord server link, remove public slack link (#93) * added default validator pubkey (#92) * added default validator pubkey * empty fee address * added print statement to add fee address * Colin/docs (#95) * added docs for using sidechain * fix syntax * Colin/plasma config (#97) * added docs for using sidechain * fix syntax * added plasma config file, priv key now in child chain struct * added validator bool and test for setting options * added rootchain address, removed gas limit, tests reflected * UTXO Refactor (#96) * change deleting utxo to invalidating * colin's comments * Eth module (#103) * starter * testing client with ganache * Squashed 'contracts/' content from commit ab4fd0e git-subtree-dir: contracts git-subtree-split: ab4fd0ed780c5828ca815946922ba3b05c1e9de4 * contract wrappers and bindings * subscribed to deposits/exists. Some tests * added wrappers to the contracts folder * Squashed 'contracts/' changes from ab4fd0e..dc28e6a dc28e6a [Audit Milestone] No rootchain finality checks (#79) b7ddf5c Simple Merkle Tree (#77) 62a9462 position in the exit struct. removed helpers (#78) git-subtree-dir: contracts git-subtree-split: dc28e6a17dc3e4b7eb9574464dd97d5a16861095 * added clean to npm script. temp removed wrappers * Squashed 'contracts/' changes from dc28e6a..4fcc49e 4fcc49e doc typo (#83) 088adc7 [Audit milestone] Fee withdrawal for validators (#80) bc1b116 Finalize Revert (#82) git-subtree-dir: contracts git-subtree-split: 4fcc49e72a86674bfb7ebb731bef59834ce7bd79 * updated wrappers * Squashed 'contracts/' changes from 4fcc49e..1b58d54 1b58d54 added eth blocknum to the deposit struct (#85) git-subtree-dir: contracts git-subtree-split: 1b58d54a6ba8d54bd3e802189e07f68b83afc1be * deposits with finality checks * removed err from HasTxBeenExited * refactors and added tests. Fixed deposit encoding/decoding * typo * changed rlp to json * gopkg fix, updated travis travis travis fix rearrange travis avoid timeout npm install longer sleep time cd back changed back to sleep 5 * Squashed 'contracts/' changes from 1b58d54..7404701 7404701 Hamdi/fee challenge (#87) git-subtree-dir: contracts git-subtree-split: 74047018dd36b0bf76adbb9b350e45aa285f1974 * subtree mods * Squashed 'contracts/' changes from 7404701..c2932c6 c2932c6 renamed docs and fixed a typo (#89) 646fcd9 Colin/deposit priority (#90) git-subtree-dir: contracts git-subtree-split: c2932c687191cb75167782cd9dfcd19ab3441199 * first revision of the eth module * hastxbeenexited more robust like getdeposit * travis * race condition * addressed feedback * MOAR PROGRESS * papusa, merge gopkg fix updated config for block finality and eth priv key default directory * debugging save progress * check if parent is exiting * save confirmations sigs into app state (#102) * starter * testing client with ganache * Squashed 'contracts/' content from commit ab4fd0e git-subtree-dir: contracts git-subtree-split: ab4fd0ed780c5828ca815946922ba3b05c1e9de4 * contract wrappers and bindings * subscribed to deposits/exists. Some tests * added wrappers to the contracts folder * Squashed 'contracts/' changes from ab4fd0e..dc28e6a dc28e6a [Audit Milestone] No rootchain finality checks (#79) b7ddf5c Simple Merkle Tree (#77) 62a9462 position in the exit struct. removed helpers (#78) git-subtree-dir: contracts git-subtree-split: dc28e6a17dc3e4b7eb9574464dd97d5a16861095 * MOAR PROGRESS * papusa * gopkg fix * added clean to npm script. temp removed wrappers * Squashed 'contracts/' changes from dc28e6a..4fcc49e 4fcc49e doc typo (#83) 088adc7 [Audit milestone] Fee withdrawal for validators (#80) bc1b116 Finalize Revert (#82) git-subtree-dir: contracts git-subtree-split: 4fcc49e72a86674bfb7ebb731bef59834ce7bd79 * updated wrappers * updated config for block finality and eth priv key default directory * Squashed 'contracts/' changes from 4fcc49e..1b58d54 1b58d54 added eth blocknum to the deposit struct (#85) git-subtree-dir: contracts git-subtree-split: 1b58d54a6ba8d54bd3e802189e07f68b83afc1be * deposits with finality checks * merge conflicts * removed err from HasTxBeenExited * refactors and added tests. Fixed deposit encoding/decoding * typo * debugging * changed rlp to json * save progress * save confirmations sigs into app state * added check for fee amount * added confirmation signatures to prove command * ante * adjusted names, load latest version * remove prints * merge conflicts * merge conflicts for contracts * more merge conflicts * conflict * undo merges that didnt delete * last of conflicts * fixed build and test issues * refactored app tests, works around genesis deposit issue * add simulation test added exiting test reflected github comments * fixed input.ammount bug * develop2 [major refactor] (#105) * consolidated plasma types * txIndex and oIndex do not need to be big ints * implement spendmsg. redo utils * abstracted positions * Signatures encapsulated within inputs. Position can be serialized to bytes. More functions * no pointers within tx struct. deposit serialization * priority calculation for position * tests for position * eth module uses plasma types * spendMsg implements sdk.Tx interface * refactor of the ante handler complete * implement stores. spend message handler * a valid spendMsg should not allow the second output to get burned * removed x/ and types/ * remove auth folder * update cosmos-sdk dep and codspacetypes to strings * output serialization and test * store uses rlp * spendMsg validation checks for the precense of confirm signatures * handlers with first set of tests. abstracted position from input struct * refactored app.go into root dir * save ethConnection in app struct * consistent naming * submit block during endBlocker * refactored plasmad into server * app within plasmad and updated dependenceis * eth module does not persist any information and queries geth node instead * nop logger for tests * utxo helpers and deposits are store in app state during ante * plasmad refactor * plasmacli refactor underway * eth module supports non operators * swapped in amino codec * only submit blocks if the operator * confirmsig key creation * rlp decoding form spendmsg. Position string conversion * client refactor * remove binary and update toml * added crypto libs to Gopkg, keystore and error code fixes * plasma config options * check grandparents in antehandler * extracted validation into the plasma module * rest of the application reflects validation changes * fee inputs should not post confirm signatures * signature validation does not occur in ValidateBasic * handlers updated to reflect change * GetSigners() bug, comment for fauxMerkleMode * utxo serialization test * removing subtree * Squashed 'contracts/' content from commit 2922c20 git-subtree-dir: contracts git-subtree-split: 2922c20e722348c1c3662190500e442851b2c30d * updated contract wrappers * naming, updated tests * fixed bug. tests pass * i lied. updated truffle in travis * keystore import directly from the command line * spendMsg does not need to implement the rlp interface * progess. plasma config hardcoded for now * fixed keystore * progress * progress * dependency * rest routes * lastest block rest call * dependecies * removed rest module * removed binaries and print statements * fee bug. state update should not occur in ante * build error * fixed handler tests * fixed plasma toml * binaries in gitignore * fixed syncing issues (#106) * fixed syncing issues * elaborated on a comment * Colin/docs (#109) * Update README * cleanup * fix wording * R4R: Colin/templates (#115) * add changelog, templates * update changelog * Keys Package (#108) * refactored keys cmd, made query dir * fixed directory flag issue, add delete list updated * completed keys command * adjusted other commands to use keyname * changes discussed on discord * build fix * forgot to adjust account.go * rm print, 0x modifier * changed from rlp to []byte * redundant string casting * connect query cmd * code clean, new format * update changelog * R4R: added confirm sig client storage (#116) * added confirm sig client storage * added test * Wip adding tests * upgrade to v0.32.0 sdk * added tests * updated dep * fix dep issues, upgraded to 0.32.0 sdk * updated deps * replace deprecated functions * deprecate exportState * rm space between import * update changelog * Fixed Length TxBytes (#118) * txBytes length fixed * decode sigs to nil if zero bytes * fix decode equal values issue * fixed 811 txBytes issue * update changelog * Update changelog * add space * plasmacli eth connection (#110) * refactored keys cmd, made query dir * fixed directory flag issue, add delete list updated * completed keys command * adjusted other commands to use keyname * changes discussed on discord * build fix * forgot to adjust account.go * rm print, 0x modifier * changed from rlp to []byte * redundant string casting * connect query cmd * added eth command, and deposit subcommand * query cmd for deposits * added several eth cmds * code clean up, various bug fixes * getExit bug * err/code clean up, gas limit * syntax fix * some exit fixes, still needs work * added finalize/withdraw cmds * fix error * Add fee exit, deposit flag * build fix * rm contracts * Squashed 'contracts/' content from commit 2a41203 git-subtree-dir: contracts git-subtree-split: 2a41203ea25d85001323606cf3266858a7f8852f * update getExits use queue length * add challenge cmd * rm import space * add support for querying by position * docs, code cleanup * some feedback changes * add time format * added time pending * updated deposit cmds * fix errors * fix exit bugs * Add support for confirm sig retrieval * rm non 0 proof size * update gas for challenge * IncludeDepositMsg: Explicit deposit inclusion (#120) * refactored keys cmd, made query dir * fixed directory flag issue, add delete list updated * completed keys command * adjusted other commands to use keyname * changes discussed on discord * build fix * forgot to adjust account.go * rm print, 0x modifier * changed from rlp to []byte * redundant string casting * connect query cmd * added eth command, and deposit subcommand * query cmd for deposits * added several eth cmds * code clean up, various bug fixes * getExit bug * err/code clean up, gas limit * syntax fix * added IncludeDepositMsg to explicitly add deposits into sidechain after they are finalized * fixed bug in GetDeposit, added basic cli command, and fmt * Fix bug in threshold calculation * fix merge errors * fix txDecoder * address PR comments * further comments * fmt * retrieve flag with correct value * Changelog * fixed issues from manual testing * Colin/scripts (#126) * added install script, set defaults * Update plasma_install.sh * Update plasma_install.sh * Update plasma_install.sh * updated changelog * Colin/readme (#125) * update readme * small changes * shorten quick start * update readme * update readme * address pr comments * Update README.md * update permissions (#131) * Colin/client (#129) * init refactor of sign and spend * updated sign cmd, spend finds inputs to spend * fix merge issues * fix test, reorg spend * various bug fixes in sign/spend * updated sigs test * small bug fixes * spend checks for exitted tx, updated changelog * updated eth main file * updated sign * small fixes * check pass twice in update * ante handler fix * various fixes * update prove * reverse from and to * Colin/docs (#136) * init work on updating docs * updated docs up to deposits * updated exiting and challenging without fees * current work * updated docs * add finalizing example * updated docs, fix trust-node issue - Set overrides cmd line flag * Switch to go modules, add Makefile (#141) * Switch to go modules, add Makefile * Update Changelog * fixed dependency issue, updated docs removing dep * update phony * Sync check on startup (#142) * InitEthConn fails if the endpoint is not synced. Refactored errors * update dependencies * Colin/exit pos (#144) * get position from exit calculation * add print statement * move calc to position file * deposit position fixed * added test * Fix Syncing Issue (#147) * continue * fix sync bug by pegging ETH blocks to TM blocks * remove unnecessary smart contract field * fmt * minor fixes * address PR comments and add tests * fmt * typo fix * Fix Include-Deposit Issue (#154) * fix include deposit issue by verifying msg.Owner == deposit.Owner * add in var * Fix Changelog * [WIP] Queriers & Rest Server (#155) * kvstore is a private member * query routes for utxo * message when no utxos are available * block querier. store utxo already satisfies rlp interface * file rename. remove resp objects * wip rest server * revert spend to original functionality * query metadata about 10 blocks * blocks query commond and likewise rest interface * SubmitBlock test no longer applicable * abstracted query to its own submodule * changes to the cli * async capability when submitting tx * async tx submission support * remove shelljs dep (#156) * Hamdi/[Docker+refactor] (#138) * fix 404 link * added testing flag to the the init command * almost done with the dockerfile * no need for a mutex in eth.Plasma,double pasted private key in the default settings * progress * plasmacli/plasmad dockerfiles seperated * move docker file back to root directory * remove dep * client/server under cmd/ * refactor eth module so it can be used in plasmacli * removed circular dependency. HasTxExited returns an error. removed func init() from cli. refactors * updated makefile. prove cmd now under eth * go.mod tidy * dockerfile * removed binaries from git * Dockerfile * explicit client context creation * better error mesasge if operator address is wrong * better naming * extract contract connection from the cli cmds * trust-node to false. small cmd refactors * custom rest flags. remove init function in include * remove init functions from base commands * chain-id as a persistent flag * removed remaining init functions * fixed test files * moved app to the top level * renamed inner cmd -> subcmd * revert readme changes * mixExitBond a constant. challenge cmd should bing flags on prerun * flags should be bound for balance.go * finalize should not bind flags until RunE * misspelled minexitbond * move tx related cmds to their own folder * sdk errors are not needed in the cli * missing returns in rest cmd & typos * refactor keystore error messages * allow the account to be an address. uniform errors * account/address * tm flags only if required. some eth cmds make use of a --use-node flag * bug fix in cmd. exits usage string * Hamdi/config (#157) * config changes for plasmacli * persistent flag does not need to be var * plasmad config * sigs build err * extra comments. Flags directory to reduce tendermint imports until the sdk version is updated * remove flag definition in main * removed flag changed check * build error * R4R: Colin/store (#153) * kvstore is a private member * query routes for utxo * message when no utxos are available * change to deposit, block, and tx store * block querier. store utxo already satisfies rlp interface * adjusting inputs outputs to be variable, updated app * file rename. remove resp objects * refactored plasma/, updated tests * updated msgs/ * refactored handlers/ still need to update tests * Fix build errors and test issue in store/ * added tests for deposit and block stores * added tests to store, failing on deep equal * wip rest server * revert spend to original functionality * store test bug fix * added account, updated querier * commit current work, prep to merge deposit/tx stores * updated output store * deleted depositStore, fixed bugs in outputStore * refactored handlers/ tests * added fee support to store/ with tests * in progress work being committed for review * query metadata about 10 blocks * blocks query commond and likewise rest interface * SubmitBlock test no longer applicable * reflect some pr comments * update client query, handler err fixes * some more refactor on cli * change account to wallet * consolidate verifyandsign func * improves output store and prove * finish prove and fmt * revert mistake * some refactors on comments and pr comments * updated store querying * fix various build/merge issue * cli fixes * rm todos * store fee * validateBasic change * fix bugs * Apply suggestions from code review: Doc fixes * address some pr comments * build fix * reflect my own pr comments * error update * changelog and updated err in ante * update changelog * Hamdi/store (#165) * ctx as a first param in getProof * store block type doesnt need to implement the RLP interface * constant store names * removed string literal for store names * remove hardcoded strings * rename to utxo file to output * remove utxo file and hardcoded strings within block querier * output store name changed to data * naming * Hamdi/datastore (#170) * merge the blockstore into the data store. add corresponding keys and lowercase filename * merged into one datastore. fixed tests * re-add NextPlasmaBlockHeight into store api * handlers reflect the changes of the store * single querier file under the /data/... route * add the height into the block struct. app reflect changes * remove hardcoded store name * fixed errors in the cli * fixed build errors in test * datastore arg after context. parseHeight bug and ErrDNE for nonexistent plasma blcok * Update store/querier.go * Update godoc (#173) * update msgs, handlers, and plasmacli docs * update store godoc * update more godoc * add godoc package description * fix store build issues * Update handlers/errors.go Co-Authored-By: Aditya * address pr comments * reflect pr review * WIP updated readme (#177) * updated readme * reword readme * remove new line * update readme * Hamdi/client (#171) * extract client call and rest routes into a client module * start tx and info handler * utils removeHashPrefix also ensures an even length * statusnotfound instead of statusbadrequest for StoreDNE errors * tx retrieval and code reuse * enforce byte length on hashes. consist return styles * undo strictness * fix utils * client Tx query accepts bytes and not a hex string * fix formatter * output handler. plasma.Position doesnt need extra internal struct * easier reasoning * syntax * import rename * build errors * unneeded string formatting * restserver needs the tm flags * added height handler * first cmd changes * godoc * missed the tx cmd --- .dockerignore | 7 + CONTRIBUTING.md => .github/CONTRIBUTING.md | 8 +- .github/ISSUE_TEMPLATE/bug-report.md | 26 + .github/ISSUE_TEMPLATE/feature-request.md | 21 + .gitignore | 7 +- .travis.yml | 24 +- CHANGELOG.md | 58 + Dockerfile | 35 + Gopkg.lock | 766 ------- Gopkg.toml | 63 - Makefile | 40 + README.md | 86 +- app/app.go | 279 +-- app/app_test.go | 610 ------ app/genesis.go | 174 +- app/genesis_test.go | 59 - app/options.go | 58 + auth/ante.go | 178 -- auth/ante_test.go | 296 --- client/context/helpers.go | 139 -- client/context/query.go | 83 - client/context/types.go | 81 - client/context/viper.go | 30 - client/helpers.go | 151 -- client/plasmacli/cmd/add.go | 41 - client/plasmacli/cmd/balance.go | 43 - client/plasmacli/cmd/confirmSig.go | 114 - client/plasmacli/cmd/delete.go | 56 - client/plasmacli/cmd/export.go | 1 - client/plasmacli/cmd/find.go | 40 - client/plasmacli/cmd/import.go | 60 - client/plasmacli/cmd/info.go | 53 - client/plasmacli/cmd/list.go | 30 - client/plasmacli/cmd/prove.go | 74 - client/plasmacli/cmd/root.go | 40 - client/plasmacli/cmd/sendtx.go | 142 -- client/plasmacli/cmd/update.go | 50 - client/plasmacli/main.go | 9 - client/plasmad/cmd/init.go | 96 - client/plasmad/cmd/utils.go | 113 - client/plasmad/main.go | 50 - client/query.go | 156 ++ client/rest.go | 287 +++ cmd/plasmacli/config/config.go | 110 + cmd/plasmacli/config/connection.go | 55 + cmd/plasmacli/config/tendermint.go | 17 + cmd/plasmacli/flags/flags.go | 10 + cmd/plasmacli/main.go | 15 + cmd/plasmacli/store/keystore.go | 291 +++ cmd/plasmacli/store/keystore_test.go | 92 + cmd/plasmacli/store/sigs.go | 68 + cmd/plasmacli/store/sigs_test.go | 122 ++ cmd/plasmacli/subcmd/eth/challenge.go | 111 + cmd/plasmacli/subcmd/eth/deposit.go | 63 + cmd/plasmacli/subcmd/eth/exit.go | 176 ++ cmd/plasmacli/subcmd/eth/finalize.go | 63 + cmd/plasmacli/subcmd/eth/prove.go | 120 ++ cmd/plasmacli/subcmd/eth/query/balance.go | 44 + cmd/plasmacli/subcmd/eth/query/block.go | 57 + cmd/plasmacli/subcmd/eth/query/deposit.go | 81 + cmd/plasmacli/subcmd/eth/query/exits.go | 193 ++ cmd/plasmacli/subcmd/eth/query/root.go | 42 + cmd/plasmacli/subcmd/eth/query/rootchain.go | 48 + cmd/plasmacli/subcmd/eth/root.go | 71 + cmd/plasmacli/subcmd/eth/withdraw.go | 55 + cmd/plasmacli/subcmd/keys/add.go | 33 + cmd/plasmacli/subcmd/keys/delete.go | 29 + cmd/plasmacli/subcmd/keys/import.go | 69 + cmd/plasmacli/subcmd/keys/list.go | 35 + cmd/plasmacli/subcmd/keys/root.go | 30 + cmd/plasmacli/subcmd/keys/update.go | 39 + cmd/plasmacli/subcmd/query/balance.go | 46 + cmd/plasmacli/subcmd/query/block.go | 82 + cmd/plasmacli/subcmd/query/info.go | 64 + cmd/plasmacli/subcmd/query/root.go | 24 + cmd/plasmacli/subcmd/restserver.go | 47 + cmd/plasmacli/subcmd/root.go | 76 + cmd/plasmacli/subcmd/tx/include.go | 93 + cmd/plasmacli/subcmd/tx/root.go | 36 + cmd/plasmacli/subcmd/tx/sign.go | 136 ++ cmd/plasmacli/subcmd/tx/spend.go | 408 ++++ .../cmd => cmd/plasmacli/subcmd}/version.go | 10 +- cmd/plasmad/config/config.go | 105 + cmd/plasmad/main.go | 72 + cmd/plasmad/subcmd/init.go | 135 ++ contracts/.gitattributes | 1 + contracts/.gitignore | 4 + contracts/.solcover.js | 3 + contracts/.travis.yml | 16 + contracts/CONTRIBUTING.md | 35 + contracts/LICENSE | 201 ++ contracts/README.md | 49 + contracts/contracts/Migrations.sol | 23 + contracts/contracts/PlasmaMVP.sol | 616 ++++++ contracts/contracts/libraries/BytesUtil.sol | 53 + .../contracts/libraries/BytesUtil_Test.sol | 9 + contracts/contracts/libraries/ECDSA.sol | 45 + .../contracts/libraries/MinPriorityQueue.sol | 85 + .../libraries/MinPriorityQueue_Test.sol | 18 + contracts/contracts/libraries/SafeMath.sol | 61 + .../libraries/TMSimpleMerkleTree.sol | 71 + .../libraries/TMSimpleMerkleTree_Test.sol | 15 + contracts/docs/plasmaMVPFunctions.md | 139 ++ contracts/generate.js | 26 + contracts/migrations/1_initial_migration.js | 5 + contracts/migrations/2_deploy_rootchain.js | 5 + contracts/package-lock.json | 472 +++++ contracts/package.json | 19 + .../test/libraries/TMSimpleMerkleTree.js | 92 + contracts/test/libraries/bytesUtil.js | 49 + contracts/test/libraries/priorityQueue.js | 132 ++ contracts/test/plasmamvp/blockSubmissions.js | 71 + contracts/test/plasmamvp/deposits.js | 296 +++ contracts/test/plasmamvp/plasmamvp_helpers.js | 155 ++ contracts/test/plasmamvp/transactions.js | 511 +++++ contracts/test/utilities.js | 25 + contracts/truffle-config.js | 4 + contracts/truffle.js | 11 + contracts/wrappers/plasma_mvp.go | 1886 +++++++++++++++++ docs/architecure/store.md | 25 + docs/eth.md | 314 +++ docs/example.md | 184 ++ docs/example_rootchain_deployment.md | 116 + docs/keys.md | 108 + docs/overview.md | 100 +- docs/testnet-setup/example_genesis.json | 34 +- .../example_plasmacli_plasma.toml | 12 + .../testnet-setup/example_plasmad_plasma.toml | 24 + docs/testnet-setup/example_truffle.js | 19 + docs/testnet-setup/full_node_config.toml | 107 +- docs/testnet-setup/validator_config.toml | 106 +- eth/common_test.go | 26 + eth/main.go | 82 + eth/plasma.go | 268 +++ eth/plasma_test.go | 177 ++ eth/util.go | 38 + eth/util_test.go | 53 + go.mod | 73 + go.sum | 244 +++ handlers/anteHandler.go | 187 ++ handlers/anteHandler_test.go | 544 +++++ handlers/common_test.go | 33 + handlers/depositHandler.go | 26 + handlers/depositHandler_test.go | 30 + handlers/errors.go | 48 + handlers/spendMsgHandler.go | 67 + handlers/spendMsgHandler_test.go | 71 + msgs/depositMsg.go | 57 + msgs/depositMsg_test.go | 55 + msgs/errors.go | 23 + msgs/spendMsg.go | 70 + msgs/spendMsg_test.go | 30 + msgs/txDecoder.go | 23 + plasma/block.go | 23 + plasma/block_test.go | 24 + plasma/deposit.go | 51 + plasma/deposit_test.go | 24 + plasma/input.go | 83 + plasma/input_test.go | 62 + plasma/output.go | 74 + plasma/output_test.go | 51 + plasma/position.go | 170 ++ plasma/position_test.go | 86 + plasma/transaction.go | 280 +++ plasma/transaction_test.go | 143 ++ scripts/plasma_install.sh | 66 + store/blocks.go | 67 + store/blocks_test.go | 75 + store/common_test.go | 29 + store/datastore.go | 53 + store/errors.go | 29 + store/keys.go | 57 + store/outputs.go | 381 ++++ store/outputs_test.go | 224 ++ store/querier.go | 300 +++ store/types.go | 88 + store/types_test.go | 88 + types/errors.go | 33 - types/tx.go | 140 -- types/tx_test.go | 149 -- types/utxo.go | 180 -- types/utxo_test.go | 98 - utils/crypto.go | 13 + utils/crypto_test.go | 19 + utils/utils.go | 48 +- utils/utils_test.go | 31 + x/metadata/mapper.go | 30 - x/utxo/errors.go | 55 - x/utxo/handler.go | 88 - x/utxo/handler_test.go | 164 -- x/utxo/mapper.go | 110 - x/utxo/mapper_test.go | 290 --- x/utxo/types.go | 51 - x/utxo/utils.go | 32 - 194 files changed, 15429 insertions(+), 5414 deletions(-) create mode 100644 .dockerignore rename CONTRIBUTING.md => .github/CONTRIBUTING.md (88%) create mode 100644 .github/ISSUE_TEMPLATE/bug-report.md create mode 100644 .github/ISSUE_TEMPLATE/feature-request.md create mode 100644 CHANGELOG.md create mode 100644 Dockerfile delete mode 100644 Gopkg.lock delete mode 100644 Gopkg.toml create mode 100644 Makefile delete mode 100644 app/app_test.go delete mode 100644 app/genesis_test.go create mode 100644 app/options.go delete mode 100644 auth/ante.go delete mode 100644 auth/ante_test.go delete mode 100644 client/context/helpers.go delete mode 100644 client/context/query.go delete mode 100644 client/context/types.go delete mode 100644 client/context/viper.go delete mode 100644 client/helpers.go delete mode 100644 client/plasmacli/cmd/add.go delete mode 100644 client/plasmacli/cmd/balance.go delete mode 100644 client/plasmacli/cmd/confirmSig.go delete mode 100644 client/plasmacli/cmd/delete.go delete mode 100644 client/plasmacli/cmd/export.go delete mode 100644 client/plasmacli/cmd/find.go delete mode 100644 client/plasmacli/cmd/import.go delete mode 100644 client/plasmacli/cmd/info.go delete mode 100644 client/plasmacli/cmd/list.go delete mode 100644 client/plasmacli/cmd/prove.go delete mode 100644 client/plasmacli/cmd/root.go delete mode 100644 client/plasmacli/cmd/sendtx.go delete mode 100644 client/plasmacli/cmd/update.go delete mode 100644 client/plasmacli/main.go delete mode 100644 client/plasmad/cmd/init.go delete mode 100644 client/plasmad/cmd/utils.go delete mode 100644 client/plasmad/main.go create mode 100644 client/query.go create mode 100644 client/rest.go create mode 100644 cmd/plasmacli/config/config.go create mode 100644 cmd/plasmacli/config/connection.go create mode 100644 cmd/plasmacli/config/tendermint.go create mode 100644 cmd/plasmacli/flags/flags.go create mode 100644 cmd/plasmacli/main.go create mode 100644 cmd/plasmacli/store/keystore.go create mode 100644 cmd/plasmacli/store/keystore_test.go create mode 100644 cmd/plasmacli/store/sigs.go create mode 100644 cmd/plasmacli/store/sigs_test.go create mode 100644 cmd/plasmacli/subcmd/eth/challenge.go create mode 100644 cmd/plasmacli/subcmd/eth/deposit.go create mode 100644 cmd/plasmacli/subcmd/eth/exit.go create mode 100644 cmd/plasmacli/subcmd/eth/finalize.go create mode 100644 cmd/plasmacli/subcmd/eth/prove.go create mode 100644 cmd/plasmacli/subcmd/eth/query/balance.go create mode 100644 cmd/plasmacli/subcmd/eth/query/block.go create mode 100644 cmd/plasmacli/subcmd/eth/query/deposit.go create mode 100644 cmd/plasmacli/subcmd/eth/query/exits.go create mode 100644 cmd/plasmacli/subcmd/eth/query/root.go create mode 100644 cmd/plasmacli/subcmd/eth/query/rootchain.go create mode 100644 cmd/plasmacli/subcmd/eth/root.go create mode 100644 cmd/plasmacli/subcmd/eth/withdraw.go create mode 100644 cmd/plasmacli/subcmd/keys/add.go create mode 100644 cmd/plasmacli/subcmd/keys/delete.go create mode 100644 cmd/plasmacli/subcmd/keys/import.go create mode 100644 cmd/plasmacli/subcmd/keys/list.go create mode 100644 cmd/plasmacli/subcmd/keys/root.go create mode 100644 cmd/plasmacli/subcmd/keys/update.go create mode 100644 cmd/plasmacli/subcmd/query/balance.go create mode 100644 cmd/plasmacli/subcmd/query/block.go create mode 100644 cmd/plasmacli/subcmd/query/info.go create mode 100644 cmd/plasmacli/subcmd/query/root.go create mode 100644 cmd/plasmacli/subcmd/restserver.go create mode 100644 cmd/plasmacli/subcmd/root.go create mode 100644 cmd/plasmacli/subcmd/tx/include.go create mode 100644 cmd/plasmacli/subcmd/tx/root.go create mode 100644 cmd/plasmacli/subcmd/tx/sign.go create mode 100644 cmd/plasmacli/subcmd/tx/spend.go rename {client/plasmacli/cmd => cmd/plasmacli/subcmd}/version.go (57%) create mode 100644 cmd/plasmad/config/config.go create mode 100644 cmd/plasmad/main.go create mode 100644 cmd/plasmad/subcmd/init.go create mode 100644 contracts/.gitattributes create mode 100644 contracts/.gitignore create mode 100644 contracts/.solcover.js create mode 100644 contracts/.travis.yml create mode 100644 contracts/CONTRIBUTING.md create mode 100644 contracts/LICENSE create mode 100644 contracts/README.md create mode 100644 contracts/contracts/Migrations.sol create mode 100644 contracts/contracts/PlasmaMVP.sol create mode 100644 contracts/contracts/libraries/BytesUtil.sol create mode 100644 contracts/contracts/libraries/BytesUtil_Test.sol create mode 100644 contracts/contracts/libraries/ECDSA.sol create mode 100644 contracts/contracts/libraries/MinPriorityQueue.sol create mode 100644 contracts/contracts/libraries/MinPriorityQueue_Test.sol create mode 100644 contracts/contracts/libraries/SafeMath.sol create mode 100644 contracts/contracts/libraries/TMSimpleMerkleTree.sol create mode 100644 contracts/contracts/libraries/TMSimpleMerkleTree_Test.sol create mode 100644 contracts/docs/plasmaMVPFunctions.md create mode 100755 contracts/generate.js create mode 100644 contracts/migrations/1_initial_migration.js create mode 100644 contracts/migrations/2_deploy_rootchain.js create mode 100644 contracts/package-lock.json create mode 100644 contracts/package.json create mode 100644 contracts/test/libraries/TMSimpleMerkleTree.js create mode 100644 contracts/test/libraries/bytesUtil.js create mode 100644 contracts/test/libraries/priorityQueue.js create mode 100644 contracts/test/plasmamvp/blockSubmissions.js create mode 100644 contracts/test/plasmamvp/deposits.js create mode 100644 contracts/test/plasmamvp/plasmamvp_helpers.js create mode 100644 contracts/test/plasmamvp/transactions.js create mode 100644 contracts/test/utilities.js create mode 100644 contracts/truffle-config.js create mode 100644 contracts/truffle.js create mode 100644 contracts/wrappers/plasma_mvp.go create mode 100644 docs/architecure/store.md create mode 100644 docs/eth.md create mode 100644 docs/example.md create mode 100644 docs/example_rootchain_deployment.md create mode 100644 docs/keys.md create mode 100644 docs/testnet-setup/example_plasmacli_plasma.toml create mode 100644 docs/testnet-setup/example_plasmad_plasma.toml create mode 100644 docs/testnet-setup/example_truffle.js create mode 100644 eth/common_test.go create mode 100644 eth/main.go create mode 100644 eth/plasma.go create mode 100644 eth/plasma_test.go create mode 100644 eth/util.go create mode 100644 eth/util_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 handlers/anteHandler.go create mode 100644 handlers/anteHandler_test.go create mode 100644 handlers/common_test.go create mode 100644 handlers/depositHandler.go create mode 100644 handlers/depositHandler_test.go create mode 100644 handlers/errors.go create mode 100644 handlers/spendMsgHandler.go create mode 100644 handlers/spendMsgHandler_test.go create mode 100644 msgs/depositMsg.go create mode 100644 msgs/depositMsg_test.go create mode 100644 msgs/errors.go create mode 100644 msgs/spendMsg.go create mode 100644 msgs/spendMsg_test.go create mode 100644 msgs/txDecoder.go create mode 100644 plasma/block.go create mode 100644 plasma/block_test.go create mode 100644 plasma/deposit.go create mode 100644 plasma/deposit_test.go create mode 100644 plasma/input.go create mode 100644 plasma/input_test.go create mode 100644 plasma/output.go create mode 100644 plasma/output_test.go create mode 100644 plasma/position.go create mode 100644 plasma/position_test.go create mode 100644 plasma/transaction.go create mode 100644 plasma/transaction_test.go create mode 100755 scripts/plasma_install.sh create mode 100644 store/blocks.go create mode 100644 store/blocks_test.go create mode 100644 store/common_test.go create mode 100644 store/datastore.go create mode 100644 store/errors.go create mode 100644 store/keys.go create mode 100644 store/outputs.go create mode 100644 store/outputs_test.go create mode 100644 store/querier.go create mode 100644 store/types.go create mode 100644 store/types_test.go delete mode 100644 types/errors.go delete mode 100644 types/tx.go delete mode 100644 types/tx_test.go delete mode 100644 types/utxo.go delete mode 100644 types/utxo_test.go create mode 100644 utils/crypto.go create mode 100644 utils/crypto_test.go create mode 100644 utils/utils_test.go delete mode 100644 x/metadata/mapper.go delete mode 100644 x/utxo/errors.go delete mode 100644 x/utxo/handler.go delete mode 100644 x/utxo/handler_test.go delete mode 100644 x/utxo/mapper.go delete mode 100644 x/utxo/mapper_test.go delete mode 100644 x/utxo/types.go delete mode 100644 x/utxo/utils.go diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ddbb004 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.DS_Store +.gitignore +vendor/ +contracts/abi +contracts/node_modules +cmd/plasmad/plasmad +cmd/plasmacli/plasmacli diff --git a/CONTRIBUTING.md b/.github/CONTRIBUTING.md similarity index 88% rename from CONTRIBUTING.md rename to .github/CONTRIBUTING.md index ddfb39f..12cf78e 100644 --- a/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -2,22 +2,20 @@ Thank you for considering making contributions to Fourth State's Plasma MVP implementation! We welcome contributions from anyone! See the [open issues](https://github.com/FourthState/plasma-mvp-sidechain/issues) for things we need help with! -Contribute to design discussions and conversation by joining the #plasma-dev channel on Blockchain @ Berkeley's [public slack](https://www.berkeleyblockchain.slack.com/). +Contribute to design discussions and conversation by joining our [Discord Server](https://discord.gg/YTB5A4P) ## Prerequisites * [Golang](https://golang.org/doc/install) -* [dep](https://github.com/golang/dep) - ## How to get started: Add this repository into your $GOPATH/src/github.com/FourthState directory: `go get "github.com/FourthState/plasma-mvp-sidechain"` -To get dependencies: +Testing: -`dep ensure` +`make test` ### Forking diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 0000000..7a47232 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,26 @@ +--- +name: Bug Report +about: Create a report to help us squash bugs! + +--- + + + +## Summary of bug + + +## Expected behavior + + +## Actual behavior + + +## Steps to reproduce + + +## Versions/External Software + diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 0000000..f6226bf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,21 @@ +--- +name: Feature Request +about: Create a proposal to request a feature or protocol upgrade + +--- + + + +## Summary + + +## Problem + + +## Proposal + diff --git a/.gitignore b/.gitignore index a725465..6ed09cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ -vendor/ \ No newline at end of file +.DS_Store +vendor/ +contracts/abi +contracts/node_modules +cmd/plasmad/plasmad +cmd/plasmacli/plasmacli diff --git a/.travis.yml b/.travis.yml index 1bf5948..e4cb95c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,22 +1,22 @@ language: go go: - - 1.10.x - -env: - - DEP_VERSION="0.4.1" - -before_install: - # Download the binary to bin folder in $GOPATH - - curl -L -s https://github.com/golang/dep/releases/download/v${DEP_VERSION}/dep-linux-amd64 -o $GOPATH/bin/dep - # Make the binary executable - - chmod +x $GOPATH/bin/dep + - 1.11.x install: - - dep ensure -vendor-only + - make install + +before_script: + - npm install -g truffle@5.0.8 ganache-cli@6.4.1 script: - - go test -race -coverprofile=coverage.txt -covermode=atomic -v ./... + - ganache-cli -m=plasma > /dev/null & + - sleep 5 + - cd contracts/ + - npm install + - truffle migrate + - cd ../ + - make test after_success: - bash <(curl -s https://codecov.io/bash) diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0bf919e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,58 @@ +# Changelog +All notable changes to this project will be documented in this file. + +This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] +### Added +- [\#141](https://github.com/FourthState/plasma-mvp-sidechain/pull/141) Added Makefile +- [\#126](https://github.com/FourthState/plasma-mvp-sidechain/pull/126) Added installation script +- **plasmacli:** [\#110](https://github.com/FourthState/plasma-mvp-sidechain/pull/110) Added eth subcommand for rootchain interaction +- **plasmacli:** [\#110](https://github.com/FourthState/plasma-mvp-sidechain/pull/110) Added plasma.toml in .plasmacli/ for rootchain connection configuration +- **plasmacli:** [\#108](https://github.com/FourthState/plasma-mvp-sidechain/pull/108) Added keys subcommand with account mapping +- **plasmacli:** [\#116](https://github.com/FourthState/plasma-mvp-sidechain/pull/116) Added local confirmation signature storage +- **plasmacli:** [\#120](https://github.com/FourthState/plasma-mvp-sidechain/pull/120) Added `include-deposit` command to add deposit nonce into sidechain +- Ethereum connection to smart contract +- Implemented Fees +- Unit tests +- Multinode network +- Query sidechain state +- Plasma configuration file +- Added IncludeDepositMsg with handling to allow explicit deposit inclusion into sidechain +### Changed +- [\#153](https://github.com/FourthState/plasma-mvp-sidechain/pull/153) Major refactor of store/, [Store architecture details](https://github.com/FourthState/plasma-mvp-sidechain/tree/develop/docs/architecure/store.md). REST Supported. +- [\#141](https://github.com/FourthState/plasma-mvp-sidechain/pull/141) Dependency management is now handled by go modules instead of Dep +- [\#129](https://github.com/FourthState/plasma-mvp-sidechain/pull/129) Updated sign command to iterate over an account to finalize transactions +- [\#129](https://github.com/FourthState/plasma-mvp-sidechain/pull/129) Updated spend to auto generate transaction for users based on the utxos they own +- [\#120](https://github.com/FourthState/plasma-mvp-sidechain/pull/118) Fixed Length TxBytes (811), compatible with rootchain v1.0.0 +- **plasmacli:** [\#108](https://github.com/FourthState/plasma-mvp-sidechain/pull/108) home flag renamed to directory, flags have suffix "F" for local flags, and "Flag" for persistent flags +- **plasmacli:** [\#116](https://github.com/FourthState/plasma-mvp-sidechain/pull/116) client keystore/ renamed to store/ +- Made UTXO model modular +- Transaction verification to be compatible with rootchain +- Decrease dependency on amino encoding +- Updated client +- Updated documentation +- Upgrade to v0.32.0 of Cosmos SDK, v0.28.0 of TM +### Fixed +- [\#147](https://github.com/FourthState/plasma-mvp-sidechain/pull/147) Fix Syncing bug where syncing nodes would panic after processing exitted inputs/deposits. Bug is explained in detail here: [\#143](https://github.com/FourthState/plasma-mvp-sidechain/issues/143) +- [\#154](https://github.com/FourthState/plasma-mvp-sidechain/pull/154) Fixes issue where include-Deposit msg.Owner == deposit.Owner not enforced. This is necessary to prevent malicious users from rewriting an already included UTXO in store. +### Deprecated +- Dep is no longer supported + +## PreHistory + +#### v0.2.0 - July 14th, 2018 +- Functional client. Can initialize with genesis UTXOs, start Tendermint daemon, and spend UTXOs using CLI. +- More extensive app tests +- Upgrade to SDK v0.21.0 +- Can now retrieve all UTXOs owned by an address. + +#### v0.1.1 [HOTFIX] - July 8th, 2018 +- Fix double spend bug when same position is spent twice in single tx +- Added documentation + +#### v0.1.0 - June 11th, 2018 +- Contains base layer of the blockchain. +- Only capable of validating transactions and updating state. + + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c0b0ba4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +# Simple usage with a mounted data directory: +# > docker build -t plasma . +# +# It is important to link the right volume to the container. The volume contains configuration files used to launch the server dameon +# +# plasmad +# > docker run -it -p 26657:26657 -p 26656:26656 -v ~/.plasmad:/root/.plasmad +FROM golang:1.12-alpine3.9 AS builder + +RUN apk add git make npm curl gcc libc-dev && \ + mkdir -p /root/plasma-mvp-sidechain + +# install dependencies +WORKDIR /root/plasma-mvp-sidechain +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +# install plasmad and plasmacli +RUN go install -mod=readonly ./cmd/plasmad ./cmd/plasmacli + +### Final image +FROM alpine:edge + +# Install ca-certificates +RUN apk add --update ca-certificates +WORKDIR /root + +# Copy over the plasmad and plasmacli binaries from the build-env +COPY --from=builder /go/bin/plasmad /usr/bin/plasmad +COPY --from=builder /go/bin/plasmacli /usr/bin/plasmacli + +# As an executable, the dameon will simply start +CMD ["plasmad", "start"] diff --git a/Gopkg.lock b/Gopkg.lock deleted file mode 100644 index 78429fe..0000000 --- a/Gopkg.lock +++ /dev/null @@ -1,766 +0,0 @@ -# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. - - -[[projects]] - branch = "master" - digest = "1:7736fc6da04620727f8f3aa2ced8d77be8e074a302820937aa5993848c769b27" - name = "github.com/ZondaX/hid-go" - packages = ["."] - pruneopts = "UT" - revision = "48b08affede2cea076a3cf13b2e3f72ed262b743" - -[[projects]] - branch = "master" - digest = "1:7d191fd0c54ff370eaf6116a14dafe2a328df487baea280699f597aae858d00d" - name = "github.com/aristanetworks/goarista" - packages = ["monotime"] - pruneopts = "UT" - revision = "5bb443fba8e05f4a819301a63af91fe3cbadcc17" - -[[projects]] - branch = "master" - digest = "1:09a7f74eb6bb3c0f14d8926610c87f569c5cff68e978d30e9a3540aeb626fdf0" - name = "github.com/bartekn/go-bip39" - packages = ["."] - pruneopts = "UT" - revision = "a05967ea095d81c8fe4833776774cfaff8e5036c" - -[[projects]] - branch = "master" - digest = "1:d6afaeed1502aa28e80a4ed0981d570ad91b2579193404256ce672ed0a609e0d" - name = "github.com/beorn7/perks" - packages = ["quantile"] - pruneopts = "UT" - revision = "3a771d992973f24aa725d07868b467d1ddfceafb" - -[[projects]] - digest = "1:1343a2963481a305ca4d051e84bc2abd16b601ee22ed324f8d605de1adb291b0" - name = "github.com/bgentry/speakeasy" - packages = ["."] - pruneopts = "UT" - revision = "4aabc24848ce5fd31929f7d1e4ea74d3709c14cd" - version = "v0.1.0" - -[[projects]] - branch = "master" - digest = "1:c0decf632843204d2b8781de7b26e7038584e2dcccc7e2f401e88ae85b1df2b7" - name = "github.com/btcsuite/btcd" - packages = ["btcec"] - pruneopts = "UT" - revision = "67e573d211ace594f1366b4ce9d39726c4b19bd0" - -[[projects]] - digest = "1:386de157f7d19259a7f9c81f26ce011223ce0f090353c1152ffdf730d7d10ac2" - name = "github.com/btcsuite/btcutil" - packages = ["bech32"] - pruneopts = "UT" - revision = "d4cc87b860166d00d6b5b9e0d3b3d71d6088d4d4" - -[[projects]] - digest = "1:faf42ad680b8b774253199d737487c8ec512c4408b88372c55e9999204e4bc64" - name = "github.com/cosmos/cosmos-sdk" - packages = [ - "baseapp", - "client", - "client/keys", - "codec", - "crypto", - "crypto/keys", - "crypto/keys/hd", - "crypto/keys/keyerror", - "crypto/keys/mintkey", - "server", - "server/config", - "store", - "types", - "version", - ] - pruneopts = "UT" - revision = "a4af659b3359be3268e38e041fe68996db062621" - version = "v0.26.0" - -[[projects]] - digest = "1:e8a3550c8786316675ff54ad6f09d265d129c9d986919af7f541afba50d87ce2" - name = "github.com/cosmos/go-bip39" - packages = ["."] - pruneopts = "UT" - revision = "52158e4697b87de16ed390e1bdaf813e581008fa" - -[[projects]] - digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec" - name = "github.com/davecgh/go-spew" - packages = ["spew"] - pruneopts = "UT" - revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73" - version = "v1.1.1" - -[[projects]] - digest = "1:e47d51dab652d26c3fba6f8cba403f922d02757a82abdc77e90df7948daf296e" - name = "github.com/deckarep/golang-set" - packages = ["."] - pruneopts = "UT" - revision = "cbaa98ba5575e67703b32b4b19f73c91f3c4159e" - version = "v1.7.1" - -[[projects]] - digest = "1:a8ca8dd35e0110f87f54ac5c0dcf5f21faae1b60e35cbc01e40ab5bcb756dfad" - name = "github.com/ethereum/go-ethereum" - packages = [ - ".", - "accounts", - "accounts/keystore", - "common", - "common/hexutil", - "common/math", - "common/mclock", - "common/prque", - "core/types", - "crypto", - "crypto/secp256k1", - "crypto/secp256k1/libsecp256k1", - "crypto/secp256k1/libsecp256k1/include", - "crypto/secp256k1/libsecp256k1/src", - "crypto/secp256k1/libsecp256k1/src/modules/recovery", - "crypto/sha3", - "ethdb", - "event", - "log", - "metrics", - "params", - "rlp", - "trie", - ] - pruneopts = "UT" - revision = "8bbe72075e4e16442c4e28d999edee12e294329e" - version = "v1.8.17" - -[[projects]] - digest = "1:abeb38ade3f32a92943e5be54f55ed6d6e3b6602761d74b4aab4c9dd45c18abd" - name = "github.com/fsnotify/fsnotify" - packages = ["."] - pruneopts = "UT" - revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9" - version = "v1.4.7" - -[[projects]] - digest = "1:fdf5169073fb0ad6dc12a70c249145e30f4058647bea25f0abd48b6d9f228a11" - name = "github.com/go-kit/kit" - packages = [ - "log", - "log/level", - "log/term", - "metrics", - "metrics/discard", - "metrics/internal/lv", - "metrics/prometheus", - ] - pruneopts = "UT" - revision = "4dc7be5d2d12881735283bcab7352178e190fc71" - version = "v0.6.0" - -[[projects]] - digest = "1:31a18dae27a29aa074515e43a443abfd2ba6deb6d69309d8d7ce789c45f34659" - name = "github.com/go-logfmt/logfmt" - packages = ["."] - pruneopts = "UT" - revision = "390ab7935ee28ec6b286364bba9b4dd6410cb3d5" - version = "v0.3.0" - -[[projects]] - digest = "1:586ea76dbd0374d6fb649a91d70d652b7fe0ccffb8910a77468e7702e7901f3d" - name = "github.com/go-stack/stack" - packages = ["."] - pruneopts = "UT" - revision = "2fee6af1a9795aafbe0253a0cfbdf668e1fb8a9a" - version = "v1.8.0" - -[[projects]] - digest = "1:35621fe20f140f05a0c4ef662c26c0ab4ee50bca78aa30fe87d33120bd28165e" - name = "github.com/gogo/protobuf" - packages = [ - "gogoproto", - "jsonpb", - "proto", - "protoc-gen-gogo/descriptor", - "sortkeys", - "types", - ] - pruneopts = "UT" - revision = "636bf0302bc95575d69441b25a2603156ffdddf1" - version = "v1.1.1" - -[[projects]] - digest = "1:17fe264ee908afc795734e8c4e63db2accabaf57326dbf21763a7d6b86096260" - name = "github.com/golang/protobuf" - packages = [ - "proto", - "ptypes", - "ptypes/any", - "ptypes/duration", - "ptypes/timestamp", - ] - pruneopts = "UT" - revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265" - version = "v1.1.0" - -[[projects]] - branch = "master" - digest = "1:4a0c6bb4805508a6287675fac876be2ac1182539ca8a32468d8128882e9d5009" - name = "github.com/golang/snappy" - packages = ["."] - pruneopts = "UT" - revision = "2e65f85255dbc3072edf28d6b5b8efc472979f5a" - -[[projects]] - digest = "1:3a26588bc48b96825977c1b3df964f8fd842cd6860cc26370588d3563433cf11" - name = "github.com/google/uuid" - packages = ["."] - pruneopts = "UT" - revision = "d460ce9f8df2e77fb1ba55ca87fafed96c607494" - version = "v1.0.0" - -[[projects]] - digest = "1:c79fb010be38a59d657c48c6ba1d003a8aa651fa56b579d959d74573b7dff8e1" - name = "github.com/gorilla/context" - packages = ["."] - pruneopts = "UT" - revision = "08b5f424b9271eedf6f9f0ce86cb9396ed337a42" - version = "v1.1.1" - -[[projects]] - digest = "1:e73f5b0152105f18bc131fba127d9949305c8693f8a762588a82a48f61756f5f" - name = "github.com/gorilla/mux" - packages = ["."] - pruneopts = "UT" - revision = "e3702bed27f0d39777b0b37b664b6280e8ef8fbf" - version = "v1.6.2" - -[[projects]] - digest = "1:43dd08a10854b2056e615d1b1d22ac94559d822e1f8b6fcc92c1a1057e85188e" - name = "github.com/gorilla/websocket" - packages = ["."] - pruneopts = "UT" - revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b" - version = "v1.2.0" - -[[projects]] - digest = "1:ea40c24cdbacd054a6ae9de03e62c5f252479b96c716375aace5c120d68647c8" - name = "github.com/hashicorp/hcl" - packages = [ - ".", - "hcl/ast", - "hcl/parser", - "hcl/scanner", - "hcl/strconv", - "hcl/token", - "json/parser", - "json/scanner", - "json/token", - ] - pruneopts = "UT" - revision = "8cb6e5b959231cc1119e43259c4a608f9c51a241" - version = "v1.0.0" - -[[projects]] - digest = "1:870d441fe217b8e689d7949fef6e43efbc787e50f200cb1e70dbca9204a1d6be" - name = "github.com/inconshreveable/mousetrap" - packages = ["."] - pruneopts = "UT" - revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" - version = "v1.0" - -[[projects]] - branch = "master" - digest = "1:39b27d1381a30421f9813967a5866fba35dc1d4df43a6eefe3b7a5444cb07214" - name = "github.com/jmhodges/levigo" - packages = ["."] - pruneopts = "UT" - revision = "c42d9e0ca023e2198120196f842701bb4c55d7b9" - -[[projects]] - branch = "master" - digest = "1:a64e323dc06b73892e5bb5d040ced475c4645d456038333883f58934abbf6f72" - name = "github.com/kr/logfmt" - packages = ["."] - pruneopts = "UT" - revision = "b84e30acd515aadc4b783ad4ff83aff3299bdfe0" - -[[projects]] - digest = "1:c568d7727aa262c32bdf8a3f7db83614f7af0ed661474b24588de635c20024c7" - name = "github.com/magiconair/properties" - packages = ["."] - pruneopts = "UT" - revision = "c2353362d570a7bfa228149c62842019201cfb71" - version = "v1.8.0" - -[[projects]] - digest = "1:0981502f9816113c9c8c4ac301583841855c8cf4da8c72f696b3ebedf6d0e4e5" - name = "github.com/mattn/go-isatty" - packages = ["."] - pruneopts = "UT" - revision = "6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c" - version = "v0.0.4" - -[[projects]] - digest = "1:ff5ebae34cfbf047d505ee150de27e60570e8c394b3b8fdbb720ff6ac71985fc" - name = "github.com/matttproud/golang_protobuf_extensions" - packages = ["pbutil"] - pruneopts = "UT" - revision = "c12348ce28de40eed0136aa2b644d0ee0650e56c" - version = "v1.0.1" - -[[projects]] - digest = "1:78bbb1ba5b7c3f2ed0ea1eab57bdd3859aec7e177811563edc41198a760b06af" - name = "github.com/mitchellh/go-homedir" - packages = ["."] - pruneopts = "UT" - revision = "ae18d6b8b3205b561c79e8e5f69bff09736185f4" - version = "v1.0.0" - -[[projects]] - digest = "1:53bc4cd4914cd7cd52139990d5170d6dc99067ae31c56530621b18b35fc30318" - name = "github.com/mitchellh/mapstructure" - packages = ["."] - pruneopts = "UT" - revision = "3536a929edddb9a5b34bd6861dc4a9647cb459fe" - version = "v1.1.2" - -[[projects]] - digest = "1:e5d0bd87abc2781d14e274807a470acd180f0499f8bf5bb18606e9ec22ad9de9" - name = "github.com/pborman/uuid" - packages = ["."] - pruneopts = "UT" - revision = "adf5a7427709b9deb95d29d3fa8a2bf9cfd388f1" - version = "v1.2" - -[[projects]] - digest = "1:95741de3af260a92cc5c7f3f3061e85273f5a81b5db20d4bd68da74bd521675e" - name = "github.com/pelletier/go-toml" - packages = ["."] - pruneopts = "UT" - revision = "c01d1270ff3e442a8a57cddc1c92dc1138598194" - version = "v1.2.0" - -[[projects]] - digest = "1:40e195917a951a8bf867cd05de2a46aaf1806c50cf92eebf4c16f78cd196f747" - name = "github.com/pkg/errors" - packages = ["."] - pruneopts = "UT" - revision = "645ef00459ed84a119197bfb8d8205042c6df63d" - version = "v0.8.0" - -[[projects]] - digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe" - name = "github.com/pmezard/go-difflib" - packages = ["difflib"] - pruneopts = "UT" - revision = "792786c7400a136282c1664665ae0a8db921c6c2" - version = "v1.0.0" - -[[projects]] - digest = "1:c1a04665f9613e082e1209cf288bf64f4068dcd6c87a64bf1c4ff006ad422ba0" - name = "github.com/prometheus/client_golang" - packages = [ - "prometheus", - "prometheus/promhttp", - ] - pruneopts = "UT" - revision = "ae27198cdd90bf12cd134ad79d1366a6cf49f632" - -[[projects]] - branch = "master" - digest = "1:2d5cd61daa5565187e1d96bae64dbbc6080dacf741448e9629c64fd93203b0d4" - name = "github.com/prometheus/client_model" - packages = ["go"] - pruneopts = "UT" - revision = "5c3871d89910bfb32f5fcab2aa4b9ec68e65a99f" - -[[projects]] - branch = "master" - digest = "1:db712fde5d12d6cdbdf14b777f0c230f4ff5ab0be8e35b239fc319953ed577a4" - name = "github.com/prometheus/common" - packages = [ - "expfmt", - "internal/bitbucket.org/ww/goautoneg", - "model", - ] - pruneopts = "UT" - revision = "7e9e6cabbd393fc208072eedef99188d0ce788b6" - -[[projects]] - branch = "master" - digest = "1:ef74914912f99c79434d9c09658274678bc85080ebe3ab32bec3940ebce5e1fc" - name = "github.com/prometheus/procfs" - packages = [ - ".", - "internal/util", - "nfs", - "xfs", - ] - pruneopts = "UT" - revision = "185b4288413d2a0dd0806f78c90dde719829e5ae" - -[[projects]] - digest = "1:c4556a44e350b50a490544d9b06e9fba9c286c21d6c0e47f54f3a9214597298c" - name = "github.com/rcrowley/go-metrics" - packages = ["."] - pruneopts = "UT" - revision = "e2704e165165ec55d062f5919b4b29494e9fa790" - -[[projects]] - digest = "1:31d83d1b1c288073c91abadee3caec87de2a1fb5dbe589039264a802e67a26b8" - name = "github.com/rjeczalik/notify" - packages = ["."] - pruneopts = "UT" - revision = "69d839f37b13a8cb7a78366f7633a4071cb43be7" - version = "v0.9.2" - -[[projects]] - digest = "1:6a4a11ba764a56d2758899ec6f3848d24698d48442ebce85ee7a3f63284526cd" - name = "github.com/spf13/afero" - packages = [ - ".", - "mem", - ] - pruneopts = "UT" - revision = "d40851caa0d747393da1ffb28f7f9d8b4eeffebd" - version = "v1.1.2" - -[[projects]] - digest = "1:08d65904057412fc0270fc4812a1c90c594186819243160dc779a402d4b6d0bc" - name = "github.com/spf13/cast" - packages = ["."] - pruneopts = "UT" - revision = "8c9545af88b134710ab1cd196795e7f2388358d7" - version = "v1.3.0" - -[[projects]] - digest = "1:7ffc0983035bc7e297da3688d9fe19d60a420e9c38bef23f845c53788ed6a05e" - name = "github.com/spf13/cobra" - packages = ["."] - pruneopts = "UT" - revision = "7b2c5ac9fc04fc5efafb60700713d4fa609b777b" - version = "v0.0.1" - -[[projects]] - digest = "1:68ea4e23713989dc20b1bded5d9da2c5f9be14ff9885beef481848edd18c26cb" - name = "github.com/spf13/jwalterweatherman" - packages = ["."] - pruneopts = "UT" - revision = "4a4406e478ca629068e7768fc33f3f044173c0a6" - version = "v1.0.0" - -[[projects]] - digest = "1:c1b1102241e7f645bc8e0c22ae352e8f0dc6484b6cb4d132fa9f24174e0119e2" - name = "github.com/spf13/pflag" - packages = ["."] - pruneopts = "UT" - revision = "298182f68c66c05229eb03ac171abe6e309ee79a" - version = "v1.0.3" - -[[projects]] - digest = "1:f8e1a678a2571e265f4bf91a3e5e32aa6b1474a55cb0ea849750cc177b664d96" - name = "github.com/spf13/viper" - packages = ["."] - pruneopts = "UT" - revision = "25b30aa063fc18e48662b86996252eabdcf2f0c7" - version = "v1.0.0" - -[[projects]] - digest = "1:7e8d267900c7fa7f35129a2a37596e38ed0f11ca746d6d9ba727980ee138f9f6" - name = "github.com/stretchr/testify" - packages = [ - "assert", - "require", - ] - pruneopts = "UT" - revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71" - version = "v1.2.1" - -[[projects]] - branch = "master" - digest = "1:9ff9e1808adfc43f788f1c1e9fd2660c285b522243da985a4c043ec6f2a2d736" - name = "github.com/syndtr/goleveldb" - packages = [ - "leveldb", - "leveldb/cache", - "leveldb/comparer", - "leveldb/errors", - "leveldb/filter", - "leveldb/iterator", - "leveldb/journal", - "leveldb/memdb", - "leveldb/opt", - "leveldb/storage", - "leveldb/table", - "leveldb/util", - ] - pruneopts = "UT" - revision = "f9080354173f192dfc8821931eacf9cfd6819253" - -[[projects]] - digest = "1:605b6546f3f43745695298ec2d342d3e952b6d91cdf9f349bea9315f677d759f" - name = "github.com/tendermint/btcd" - packages = ["btcec"] - pruneopts = "UT" - revision = "e5840949ff4fff0c56f9b6a541e22b63581ea9df" - -[[projects]] - digest = "1:10b3a599325740c84a7c81f3f3cb2e1fdb70b3ea01b7fa28495567a2519df431" - name = "github.com/tendermint/go-amino" - packages = ["."] - pruneopts = "UT" - revision = "6dcc6ddc143e116455c94b25c1004c99e0d0ca12" - version = "v0.14.0" - -[[projects]] - digest = "1:9f8c4c93658315a795ffd3e0c943d39f78067dd8382b8d7bcfaf6686b92f3978" - name = "github.com/tendermint/iavl" - packages = ["."] - pruneopts = "UT" - revision = "fa74114f764f9827c4ad5573f990ed25bf8c4bac" - version = "v0.11.1" - -[[projects]] - digest = "1:0ca6fdbc80b4082637066772fce683f8f8913b44aa5a91114d1dc1b48b3b2dad" - name = "github.com/tendermint/tendermint" - packages = [ - "abci/client", - "abci/example/code", - "abci/example/kvstore", - "abci/server", - "abci/types", - "blockchain", - "cmd/tendermint/commands", - "config", - "consensus", - "consensus/types", - "crypto", - "crypto/armor", - "crypto/ed25519", - "crypto/encoding/amino", - "crypto/merkle", - "crypto/multisig", - "crypto/multisig/bitarray", - "crypto/secp256k1", - "crypto/tmhash", - "crypto/xsalsa20symmetric", - "evidence", - "libs/autofile", - "libs/bech32", - "libs/cli", - "libs/cli/flags", - "libs/clist", - "libs/common", - "libs/db", - "libs/errors", - "libs/events", - "libs/fail", - "libs/flowrate", - "libs/log", - "libs/pubsub", - "libs/pubsub/query", - "lite", - "lite/client", - "lite/errors", - "lite/proxy", - "mempool", - "node", - "p2p", - "p2p/conn", - "p2p/pex", - "p2p/upnp", - "privval", - "proxy", - "rpc/client", - "rpc/core", - "rpc/core/types", - "rpc/grpc", - "rpc/lib/client", - "rpc/lib/server", - "rpc/lib/types", - "state", - "state/txindex", - "state/txindex/kv", - "state/txindex/null", - "types", - "types/time", - "version", - ] - pruneopts = "UT" - revision = "6e9aee546061423864fef3f9fa332ef37c3dca96" - version = "v0.26.1-rc1" - -[[projects]] - digest = "1:7886f86064faff6f8d08a3eb0e8c773648ff5a2e27730831e2bfbf07467f6666" - name = "github.com/zondax/ledger-goclient" - packages = ["."] - pruneopts = "UT" - revision = "58598458c11bc0ad1c1b8dac3dc3e11eaf270b79" - version = "v0.1.0" - -[[projects]] - digest = "1:9ee7b5b1fdcf6dcb0d609912076bb8da0c1c953a55141c7d83fa21dee29ad4f2" - name = "golang.org/x/crypto" - packages = [ - "bcrypt", - "blowfish", - "chacha20poly1305", - "curve25519", - "ed25519", - "ed25519/internal/edwards25519", - "hkdf", - "internal/chacha20", - "internal/subtle", - "nacl/box", - "nacl/secretbox", - "openpgp/armor", - "openpgp/errors", - "pbkdf2", - "poly1305", - "ripemd160", - "salsa20/salsa", - "scrypt", - ] - pruneopts = "UT" - revision = "3764759f34a542a3aef74d6b02e35be7ab893bba" - source = "https://github.com/tendermint/crypto" - -[[projects]] - digest = "1:d36f55a999540d29b6ea3c2ea29d71c76b1d9853fdcd3e5c5cb4836f2ba118f1" - name = "golang.org/x/net" - packages = [ - "context", - "http/httpguts", - "http2", - "http2/hpack", - "idna", - "internal/timeseries", - "netutil", - "trace", - ] - pruneopts = "UT" - revision = "292b43bbf7cb8d35ddf40f8d5100ef3837cced3f" - -[[projects]] - branch = "master" - digest = "1:e9609f7e36a3fecb7dc9e3a15f9881011f9d8c42f94c9d5da231eb0c07826dc8" - name = "golang.org/x/sys" - packages = [ - "cpu", - "unix", - ] - pruneopts = "UT" - revision = "66b7b1311ac80bbafcd2daeef9a5e6e2cd1e2399" - -[[projects]] - digest = "1:a2ab62866c75542dd18d2b069fec854577a20211d7c0ea6ae746072a1dccdd18" - name = "golang.org/x/text" - packages = [ - "collate", - "collate/build", - "internal/colltab", - "internal/gen", - "internal/tag", - "internal/triegen", - "internal/ucd", - "language", - "secure/bidirule", - "transform", - "unicode/bidi", - "unicode/cldr", - "unicode/norm", - "unicode/rangetable", - ] - pruneopts = "UT" - revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" - version = "v0.3.0" - -[[projects]] - branch = "master" - digest = "1:56b0bca90b7e5d1facf5fbdacba23e4e0ce069d25381b8e2f70ef1e7ebfb9c1a" - name = "google.golang.org/genproto" - packages = ["googleapis/rpc/status"] - pruneopts = "UT" - revision = "5fc9ac5403620be16bcdb0c8e7644b1178472c3b" - -[[projects]] - digest = "1:2dab32a43451e320e49608ff4542fdfc653c95dcc35d0065ec9c6c3dd540ed74" - name = "google.golang.org/grpc" - packages = [ - ".", - "balancer", - "balancer/base", - "balancer/roundrobin", - "codes", - "connectivity", - "credentials", - "encoding", - "encoding/proto", - "grpclog", - "internal", - "internal/backoff", - "internal/channelz", - "internal/grpcrand", - "keepalive", - "metadata", - "naming", - "peer", - "resolver", - "resolver/dns", - "resolver/passthrough", - "stats", - "status", - "tap", - "transport", - ] - pruneopts = "UT" - revision = "168a6198bcb0ef175f7dacec0b8691fc141dc9b8" - version = "v1.13.0" - -[[projects]] - digest = "1:342378ac4dcb378a5448dd723f0784ae519383532f5e70ade24132c4c8693202" - name = "gopkg.in/yaml.v2" - packages = ["."] - pruneopts = "UT" - revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" - version = "v2.2.1" - -[solve-meta] - analyzer-name = "dep" - analyzer-version = 1 - input-imports = [ - "github.com/bgentry/speakeasy", - "github.com/cosmos/cosmos-sdk/baseapp", - "github.com/cosmos/cosmos-sdk/codec", - "github.com/cosmos/cosmos-sdk/server", - "github.com/cosmos/cosmos-sdk/server/config", - "github.com/cosmos/cosmos-sdk/store", - "github.com/cosmos/cosmos-sdk/types", - "github.com/ethereum/go-ethereum/accounts", - "github.com/ethereum/go-ethereum/accounts/keystore", - "github.com/ethereum/go-ethereum/common", - "github.com/ethereum/go-ethereum/crypto", - "github.com/ethereum/go-ethereum/rlp", - "github.com/mattn/go-isatty", - "github.com/pkg/errors", - "github.com/spf13/cobra", - "github.com/spf13/pflag", - "github.com/spf13/viper", - "github.com/stretchr/testify/assert", - "github.com/stretchr/testify/require", - "github.com/tendermint/go-amino", - "github.com/tendermint/tendermint/abci/types", - "github.com/tendermint/tendermint/crypto", - "github.com/tendermint/tendermint/crypto/encoding/amino", - "github.com/tendermint/tendermint/crypto/secp256k1", - "github.com/tendermint/tendermint/libs/cli", - "github.com/tendermint/tendermint/libs/common", - "github.com/tendermint/tendermint/libs/db", - "github.com/tendermint/tendermint/libs/log", - "github.com/tendermint/tendermint/rpc/client", - "github.com/tendermint/tendermint/rpc/core/types", - "github.com/tendermint/tendermint/types", - ] - solver-name = "gps-cdcl" - solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml deleted file mode 100644 index a9edd45..0000000 --- a/Gopkg.toml +++ /dev/null @@ -1,63 +0,0 @@ -# Gopkg.toml example -# -# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md -# for detailed Gopkg.toml documentation. -# -# required = ["github.com/user/thing/cmd/thing"] -# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] -# -# [[constraint]] -# name = "github.com/user/project" -# version = "1.0.0" -# -# [[constraint]] -# name = "github.com/user/project2" -# branch = "dev" -# source = "github.com/myfork/project2" -# -# [[override]] -# name = "github.com/x/y" -# version = "2.4.0" -# -# [prune] -# non-go = false -# go-tests = true -# unused-packages = true - - -[[constraint]] - name = "github.com/cosmos/cosmos-sdk" - version = "0.26.0" - -[[constraint]] - name = "github.com/stretchr/testify" - version = "1.2.1" - -[[constraint]] - name = "github.com/ethereum/go-ethereum" - version = "1.8.8" - -[[override]] - name = "github.com/golang/protobuf" - version = "=1.1.0" - -[[override]] - name = "github.com/tendermint/go-amino" - version = "v0.14.0" - -[[override]] - name = "github.com/tendermint/iavl" - version = "=v0.11.1" - -[[override]] - name = "github.com/tendermint/tendermint" - version = "v0.26.1-rc0" - -[[override]] - name = "golang.org/x/crypto" - source = "https://github.com/tendermint/crypto" - revision = "3764759f34a542a3aef74d6b02e35be7ab893bba" - -[prune] - go-tests = true - unused-packages = true diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7a0a294 --- /dev/null +++ b/Makefile @@ -0,0 +1,40 @@ +export GO111MODULE=on + +all: install test + +######################################## +### Install & Build + +build: go.sum +ifeq ($(OS),Windows_NT) + go build -o -mod=readonly -o build/plasmad.exe ./cmd/plasmad + go build -o -mod=readonly -o build/plasmacli.exe ./cmd/plasmacli +else + go build -o -mod=readonly -o build/plasmad ./cmd/plasmad + go build -o -mod=readonly -o build/plasmacli ./cmd/plasmacli +endif + +install: go.sum + go install -mod=readonly ./cmd/plasmad + go install -mod=readonly ./cmd/plasmacli + +######################################## +### Dependencies & Maintenance + +go.sum: go.mod + @echo "--> Ensure dependencies have not been modified" + @go mod verify + +clean: + rm -rf build/ coverage.txt + +######################################## +### + +test: test-unit + +test-unit: + go test -mod=readonly -race -coverprofile=coverage.txt -covermode=atomic -v ./... + +# https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html +.PHONY: all build install go.sum test test-unit diff --git a/README.md b/README.md index bbadeff..04129fd 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,60 @@ # Plasma MVP Sidechain - +[![Go Report](https://goreportcard.com/badge/github.com/FourthState/plasma-mvp-sidechain)](https://goreportcard.com/report/github.com/FourthState/plasma-mvp-sidechain) +[![Build Status](https://travis-ci.org/FourthState/plasma-mvp-sidechain.svg?branch=develop)](https://travis-ci.org/FourthState/plasma-mvp-sidechain) +[![codecov](https://codecov.io/gh/FourthState/plasma-mvp-sidechain/branch/develop/graph/badge.svg)](https://codecov.io/gh/FourthState/plasma-mvp-sidechain) +[![Discord](https://img.shields.io/badge/discord-join%20chat-blue.svg)](https://discord.gg/YTB5A4P) [![license](https://img.shields.io/github/license/FourthState/plasma-mvp-rootchain.svg)](https://github.com/FourthState/plasma-mvp-sidechain/blob/master/LICENSE) -Branch | Tests | Coverage -----------|-------|---------- -develop | [![Build Status](https://travis-ci.org/FourthState/plasma-mvp-sidechain.svg?branch=develop)](https://travis-ci.org/FourthState/plasma-mvp-sidechain) | [![codecov](https://codecov.io/gh/FourthState/plasma-mvp-sidechain/branch/develop/graph/badge.svg)](https://codecov.io/gh/FourthState/plasma-mvp-sidechain) -master | [![Build Status](https://travis-ci.org/FourthState/plasma-mvp-sidechain.svg?branch=master)](https://travis-ci.org/FourthState/plasma-mvp-sidechain) | [![codecov](https://codecov.io/gh/FourthState/plasma-mvp-sidechain/branch/master/graph/badge.svg)](https://codecov.io/gh/FourthState/plasma-mvp-sidechain) - -This is the latest [Minimum Viable Plasma](https://ethresear.ch/t/minimal-viable-plasma/426) version. - -**Note**: This sidechain is being constructed to be compatible with our [rootchain contract](https://github.com/FourthState/plasma-mvp-rootchain) - -## Overview -As a layer 2 scaling solution, Plasma has two major components: verification and computation. Verification is handled by the rootchain contract which resolves any disputes and distributes funds accordingly. +Implementation of [Minimum Viable Plasma](https://ethresear.ch/t/minimal-viable-plasma/426) compatible with our [rootchain contract](https://github.com/FourthState/plasma-mvp-rootchain) -Computation is handled separately by a sidechain. This sidechain leverages the Cosmos SDK to create a scalable and flexible blockchain, that can maintain it's security through reporting merkle roots to the root chain. We will be using [Tendermint](https://github.com/tendermint/tendermint) for consensus on this blockchain. +## Project Status +There is very little development occuring for this project. +We will continue to maintain this repository by thoroughly reviewing any open source contributions. +We will provide support and guidance for anyone looking to continue development. -We are using a UTXO model for this blockchain. This allows us to do secure and compact proofs when interacting with the rootchain contract. +## What is Plasma? +Plasma has two major components: verification and computation. +Verification is handled by the rootchain contract, which resolves any disputes and distributes funds accordingly. +Computation is handled separately by a sidechain, which maintains its security through reporting proofs via merkle roots to the rootchain contract. -## Starting a sidechain +Plasma MVP utilizes a UTXO model, which allows for secure and compact proofs. Learn more about plasma on [learnplasma.org](https://www.learnplasma.org/en/)! -In order to run a sidechain with tendermint consensus and a client to form transaction, a plasma node and light client will need to be initialized. +We are using [Tendermint](https://github.com/tendermint/tendermint) for our consensus protocol. +This sidechain currently supports a single validator, but will be updated in the future to support multiple validators. -**Note**: The following assumes you have [golang](https://golang.org/) properly setup and all dependecies have already been installed. See [Contribution Guidelines](https://github.com/FourthState/plasma-mvp-sidechain/blob/master/CONTRIBUTING.md) for more information. +## Quick Start -Plasma Node: +### Install using a script -- Navigate to `client/plasmad/` directory -- Run `go install` via command line +This script can be used on a fresh server that has no dependencies installed. -The plasma node (plasmad) is now installed and can be called from any directory with `plasmad` - -Run `plasmad init` via command line to start an instance of a plasma node with a connection to a tendermint validator. - -Run `plasmad start` via command line to begin running the plasma node. You should see empty blocks being proposed and committed. +``` +curl https://raw.githubusercontent.com/FourthState/plasma-mvp-sidechain/develop/scripts/plasma_install.sh > install.sh +chmod +x install.sh +./install.sh +``` -Plasma Light Client: +### Manual Install -- Navigate to `client/plasmacli/` directory -- Run `go install` via command line +**Requirements**: +- [golang v1.11+](https://golang.org/) -Use `plasmacli` to run any of the commands for this light client +Pull the latest version of the develop branch. -The light client uses the Ethereum keystore to create and store passphrase encrypted keys in `$HOME/.plasmacli/keys/` +`make install` -### dep ensure -When building the sidechain, go dep is used to manage dependencies. -Running `dep ensure` followed by `go build` will result in the following output: +***Plasma Node:*** -``` -# github.com/FourthState/plasma-mvp-sidechain/vendor/github.com/ethereum/go-ethereum/crypto/secp256k1 -../vendor/github.com/ethereum/go-ethereum/crypto/secp256k1/curve.go:42:44: fatal error: libsecp256k1/include/secp256k1.h: No such file or directory -``` -This is caused by a go dep issue outlined [here](https://github.com/tools/godep/issues/422). -To fix this locally, add the following in Gopkg.lock under `crypto/secp256k1` and above `crypto/sha3`: +Run `plasmad init` to start an instance of a plasma node. +Use the `--home ` to specify a location where you want your plasma node to exist. -``` -"crypto/secp256k1/libsecp256k1", -"crypto/secp256k1/libsecp256k1/include", -"crypto/secp256k1/libsecp256k1/src", -"crypto/secp256k1/libsecp256k1/src/modules/recovery", -``` +Navigate to `/config/` (default is `$HOME/.plasmad/config`), set configuration parameters in config.toml and plasma.toml. +Run `plasmad start` to begin running the plasma node. -Run `dep ensure -vendor-only` +***Plasma Client:*** -Your vendor folder should now contain all the necessary dependencies, there is no need to run `dep ensure` again. +Navigate to `$HOME/.plasmacli`, set ethereum client configuration parameters in plasma.toml. +Use `plasmacli` to run any of the commands for this light client ### Plasma Architecture See our [research repository](https://github.com/FourthState/plasma-research) for architectural explanations of our Plasma implementation. @@ -73,4 +63,4 @@ See our [research repository](https://github.com/FourthState/plasma-research) fo See our [documentation](https://github.com/FourthState/plasma-mvp-sidechain/blob/master/docs/overview.md) ### Contributing -See our [contributing guidelines](https://github.com/FourthState/plasma-mvp-sidechain/blob/master/CONTRIBUTING.md) +See our [contributing guidelines](https://github.com/FourthState/plasma-mvp-sidechain/blob/master/.github/CONTRIBUTING.md). Join our [Discord Server](https://discord.gg/YTB5A4P). diff --git a/app/app.go b/app/app.go index 7304abe..2c93efb 100644 --- a/app/app.go +++ b/app/app.go @@ -1,120 +1,153 @@ +// Package app provides the construction and execution of the plasma chain package app import ( - "encoding/binary" + "crypto/ecdsa" "encoding/json" - auth "github.com/FourthState/plasma-mvp-sidechain/auth" - "github.com/FourthState/plasma-mvp-sidechain/types" - "github.com/FourthState/plasma-mvp-sidechain/x/metadata" - "github.com/FourthState/plasma-mvp-sidechain/x/utxo" - abci "github.com/tendermint/tendermint/abci/types" - "io" - - bam "github.com/cosmos/cosmos-sdk/baseapp" + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/eth" + "github.com/FourthState/plasma-mvp-sidechain/handlers" + "github.com/FourthState/plasma-mvp-sidechain/msgs" + "github.com/FourthState/plasma-mvp-sidechain/plasma" + "github.com/FourthState/plasma-mvp-sidechain/store" + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" - rlp "github.com/ethereum/go-ethereum/rlp" - "github.com/tendermint/go-amino" - cryptoAmino "github.com/tendermint/tendermint/crypto/encoding/amino" - cmn "github.com/tendermint/tendermint/libs/common" + "github.com/ethereum/go-ethereum/common" + abci "github.com/tendermint/tendermint/abci/types" dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" tmtypes "github.com/tendermint/tendermint/types" - - ethcmn "github.com/ethereum/go-ethereum/common" + "io" + "math/big" + "os" + "time" ) const ( - appName = "plasmaChildChain" + appName = "plasmaMVP" ) -// Extended ABCI application -type ChildChain struct { - *bam.BaseApp - - cdc *amino.Codec +// PlasmaMVPChain is an extended ABCI application +type PlasmaMVPChain struct { + *baseapp.BaseApp + cdc *codec.Codec - txIndex uint16 + txIndex uint16 + feeAmount *big.Int - feeAmount uint64 + // persistent stores + dataStore store.DataStore - // keys to access the substores - capKeyMainStore *sdk.KVStoreKey + // smart contract connection + ethConnection *eth.Plasma - capKeyMetadataStore *sdk.KVStoreKey + /* Config */ + isOperator bool // contract operator + operatorPrivateKey *ecdsa.PrivateKey + operatorAddress common.Address + plasmaContractAddress common.Address + blockCommitmentRate time.Duration + nodeURL string // client that satisfies the web3 interface + blockFinality uint64 // presumed finality bound for the ethereum network +} - // Manage addition and deletion of utxo's - utxoMapper utxo.Mapper +// NewPlasmaMVPChain creates a PlasmaMVPChain instance +func NewPlasmaMVPChain(logger log.Logger, db dbm.DB, traceStore io.Writer, options ...func(*PlasmaMVPChain)) *PlasmaMVPChain { + baseApp := baseapp.NewBaseApp(appName, logger, db, msgs.TxDecoder) + cdc := MakeCodec() + baseApp.SetCommitMultiStoreTracer(traceStore) - // Address that validator uses to collect fees - validatorAddress ethcmn.Address + dataStoreKey := sdk.NewKVStoreKey(store.DataStoreName) + dataStore := store.NewDataStore(dataStoreKey) - metadataMapper metadata.MetadataMapper -} + app := &PlasmaMVPChain{ + BaseApp: baseApp, + cdc: cdc, + txIndex: 0, + feeAmount: big.NewInt(0), // we do not use `utils.BigZero` because the feeAmount is going to be updated -func NewChildChain(logger log.Logger, db dbm.DB, traceStore io.Writer) *ChildChain { - cdc := MakeCodec() + dataStore: dataStore, + } - bapp := bam.NewBaseApp(appName, logger, db, txDecoder) - bapp.SetCommitMultiStoreTracer(traceStore) + // set configs + for _, option := range options { + option(app) + } - var app = &ChildChain{ - BaseApp: bapp, - cdc: cdc, - txIndex: 0, - feeAmount: 0, - capKeyMainStore: sdk.NewKVStoreKey("main"), - capKeyMetadataStore: sdk.NewKVStoreKey("metadata"), + // connect to remote client + eth.SetLogger(logger) + ethClient, err := eth.InitEthConn(app.nodeURL) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + plasmaClient, err := eth.InitPlasma(app.plasmaContractAddress, ethClient, app.blockFinality) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + if app.isOperator { + plasmaClient, err = plasmaClient.WithOperatorSession(app.operatorPrivateKey, app.blockCommitmentRate) } + if err != nil { + fmt.Println(err) + os.Exit(1) + } + app.ethConnection = plasmaClient - // define the utxoMapper - app.utxoMapper = utxo.NewBaseMapper( - app.capKeyMainStore, // target store - cdc, - ) + // query for the operator address + addr, err := plasmaClient.OperatorAddress() + if err != nil { + logger.Error("unable to query the contract for the operator address") + fmt.Println(err) + os.Exit(1) + } + app.operatorAddress = addr - app.metadataMapper = metadata.NewMetadataMapper( - app.capKeyMetadataStore, - ) + // Route spends to the handler + nextTxIndex := func() uint16 { + app.txIndex++ + return app.txIndex - 1 + } + feeUpdater := func(amt *big.Int) sdk.Error { + app.feeAmount = app.feeAmount.Add(app.feeAmount, amt) + return nil + } + app.Router().AddRoute(msgs.SpendMsgRoute, handlers.NewSpendHandler(app.dataStore, nextTxIndex, feeUpdater)) + app.Router().AddRoute(msgs.IncludeDepositMsgRoute, handlers.NewDepositHandler(app.dataStore, nextTxIndex, plasmaClient)) - app.Router(). - AddRoute("spend", utxo.NewSpendHandler(app.utxoMapper, app.nextPosition, types.ProtoUTXO)) + // custom queriers + app.QueryRouter().AddRoute(store.QuerierRouteName, store.NewQuerier(app.dataStore)) - app.MountStoresIAVL(app.capKeyMainStore) - app.MountStoresIAVL(app.capKeyMetadataStore) + // Set the AnteHandler + app.SetAnteHandler(handlers.NewAnteHandler(app.dataStore, plasmaClient)) - app.SetInitChainer(app.initChainer) + // set the rest of the chain flow app.SetEndBlocker(app.endBlocker) + app.SetInitChainer(app.initChainer) - // NOTE: type AnteHandler func(ctx Context, tx Tx) (newCtx Context, result Result, abort bool) - app.SetAnteHandler(auth.NewAnteHandler(app.utxoMapper, app.metadataMapper, app.feeUpdater)) - - err := app.LoadLatestVersion(app.capKeyMainStore) - if err != nil { - cmn.Exit(err.Error()) + // mount and load stores + // IAVL store used by default. `fauxMerkleMode` defaults to false + app.MountStores(dataStoreKey) + if err := app.LoadLatestVersion(dataStoreKey); err != nil { + fmt.Println(err) + os.Exit(1) } return app } -func (app *ChildChain) initChainer(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { +// initChainer initializes genesis state before the chain begins +func (app *PlasmaMVPChain) initChainer(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { stateJSON := req.AppStateBytes - // TODO is this now the whole genesis file? - var genesisState GenesisState - err := app.cdc.UnmarshalJSON(stateJSON, &genesisState) - if err != nil { + genesisState := GenesisState{} + if err := app.cdc.UnmarshalJSON(stateJSON, &genesisState); err != nil { panic(err) // TODO https://github.com/cosmos/cosmos-sdk/issues/468 // return sdk.ErrGenesisParse("").TraceCause(err, "") } - // load the accounts - for _, gutxo := range genesisState.UTXOs { - utxo := ToUTXO(gutxo) - app.utxoMapper.AddUTXO(ctx, utxo) - } - - app.validatorAddress = ethcmn.HexToAddress(genesisState.Validator.Address) - // load the initial stake information return abci.ResponseInitChain{Validators: []abci.ValidatorUpdate{abci.ValidatorUpdate{ PubKey: tmtypes.TM2PB.PubKey(genesisState.Validator.ConsPubKey), @@ -122,83 +155,51 @@ func (app *ChildChain) initChainer(ctx sdk.Context, req abci.RequestInitChain) a }}} } -func (app *ChildChain) endBlocker(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock { - if app.feeAmount != 0 { - position := types.PlasmaPosition{ - Blknum: uint64(ctx.BlockHeight()), - TxIndex: uint16(1<<16 - 1), - Oindex: 0, - DepositNum: 0, - } - utxo := types.BaseUTXO{ - Address: app.validatorAddress, - InputAddresses: [2]ethcmn.Address{app.validatorAddress, ethcmn.Address{}}, - Amount: app.feeAmount, - Denom: types.Denom, - Position: position, - } - app.utxoMapper.AddUTXO(ctx, &utxo) - } +// Reset state at the end of each block +func (app *PlasmaMVPChain) endBlocker(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock { + ds := app.dataStore - // reset txIndex and fee - app.txIndex = 0 - app.feeAmount = 0 - - blknumKey := make([]byte, binary.MaxVarintLen64) - binary.PutUvarint(blknumKey, uint64(ctx.BlockHeight())) - - if ctx.BlockHeader().DataHash != nil { - app.metadataMapper.StoreMetadata(ctx, blknumKey, ctx.BlockHeader().DataHash) + // skip if the block is empty + if app.txIndex == 0 { + // try to commit any headers in the store + app.ethConnection.CommitPlasmaHeaders(ctx, ds) + return abci.ResponseEndBlock{} } - return abci.ResponseEndBlock{} -} + tmBlockHeight := uint64(ctx.BlockHeight()) + plasmaBlockHeight := ds.NextPlasmaBlockHeight(ctx) -// RLP decodes the txBytes to a BaseTx -func txDecoder(txBytes []byte) (sdk.Tx, sdk.Error) { - var tx = types.BaseTx{} + var header [32]byte + copy(header[:], ctx.BlockHeader().DataHash) + block := plasma.NewBlock(header, app.txIndex, app.feeAmount, plasmaBlockHeight) + ds.StoreBlock(ctx, tmBlockHeight, block) - err := rlp.DecodeBytes(txBytes, &tx) - if err != nil { - return nil, sdk.ErrTxDecode(err.Error()) + if app.feeAmount.Sign() == 1 { + ds.StoreFee(ctx, plasmaBlockHeight, plasma.NewOutput(app.operatorAddress, app.feeAmount)) } - return tx, nil -} -// Return the next output position given ctx -// and secondary flag which indicates if it is for secondary outputs from single tx. -func (app *ChildChain) nextPosition(ctx sdk.Context, secondary bool) utxo.Position { - if !secondary { - app.txIndex++ - return types.NewPlasmaPosition(uint64(ctx.BlockHeight()), app.txIndex-1, 0, 0) - } - return types.NewPlasmaPosition(uint64(ctx.BlockHeight()), app.txIndex-1, 1, 0) -} + app.ethConnection.CommitPlasmaHeaders(ctx, ds) -// Unimplemented for now -func (app *ChildChain) feeUpdater(output []utxo.Output) sdk.Error { - if len(output) != 1 || output[0].Denom != types.Denom { - return utxo.ErrInvalidFee(2, "Fee must be paid in Eth") - } - app.feeAmount += output[0].Amount - return nil -} + app.txIndex = 0 + app.feeAmount = big.NewInt(0) -func MakeCodec() *amino.Codec { - cdc := amino.NewCodec() - cdc.RegisterInterface((*sdk.Msg)(nil), nil) - cdc.RegisterConcrete(PlasmaGenTx{}, "app/PlasmaGenTx", nil) - types.RegisterAmino(cdc) - utxo.RegisterAmino(cdc) - cryptoAmino.RegisterAmino(cdc) - return cdc + return abci.ResponseEndBlock{} } -func (app *ChildChain) ExportAppStateJSON() (appState json.RawMessage, validators []tmtypes.GenesisValidator, err error) { +// ExportAppStateJSON exports the current applicatoin state into JSON. +func (app *PlasmaMVPChain) ExportAppStateJSON() (appState json.RawMessage, validators []tmtypes.GenesisValidator, err error) { // TODO: Implement // Currently non-functional, just enough to compile - tx := types.BaseTx{} - appState, err = app.cdc.MarshalJSONIndent(tx, "", "\t") + tx := msgs.SpendMsg{} + appState, err = json.MarshalIndent(tx, "", "\t") validators = []tmtypes.GenesisValidator{} return appState, validators, err } + +// MakeCodec returns a new codec with registered sdk and crypto types +func MakeCodec() *codec.Codec { + cdc := codec.New() + sdk.RegisterCodec(cdc) + codec.RegisterCrypto(cdc) + return cdc +} diff --git a/app/app_test.go b/app/app_test.go deleted file mode 100644 index a64e02e..0000000 --- a/app/app_test.go +++ /dev/null @@ -1,610 +0,0 @@ -package app - -import ( - "encoding/binary" - "fmt" - "os" - "testing" - - "crypto/ecdsa" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/common" - ethcrypto "github.com/ethereum/go-ethereum/crypto" - "github.com/stretchr/testify/require" - abci "github.com/tendermint/tendermint/abci/types" - secp256k1 "github.com/tendermint/tendermint/crypto/secp256k1" - "github.com/tendermint/tendermint/crypto/tmhash" - dbm "github.com/tendermint/tendermint/libs/db" - "github.com/tendermint/tendermint/libs/log" - - types "github.com/FourthState/plasma-mvp-sidechain/types" - utils "github.com/FourthState/plasma-mvp-sidechain/utils" - rlp "github.com/ethereum/go-ethereum/rlp" -) - -/* - Note: Check() has been taken out from testing - at the moment because it increments txIndex - -*/ - -func newChildChain() *ChildChain { - logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)).With("module", "sdk/app") - db := dbm.NewMemDB() - return NewChildChain(logger, db, nil) -} - -// Creates a deposit of value 100 for each address in input -func InitTestChain(cc *ChildChain, valAddr common.Address, addrs ...common.Address) { - var genUTXOs []GenesisUTXO - for i, addr := range addrs { - genUTXOs = append(genUTXOs, NewGenesisUTXO(addr.Hex(), "100", [4]string{"0", "0", "0", fmt.Sprintf("%d", i+1)})) - } - - pubKey := secp256k1.GenPrivKey().PubKey() - - genValidator := GenesisValidator{ - ConsPubKey: pubKey, - Address: valAddr.String(), - } - - genState := GenesisState{ - Validator: genValidator, - UTXOs: genUTXOs, - } - - appStateBytes, err := cc.cdc.MarshalJSON(genState) - if err != nil { - panic(err) - } - - initRequest := abci.RequestInitChain{AppStateBytes: appStateBytes} - cc.InitChain(initRequest) -} - -func GenerateSimpleMsg(Owner0, NewOwner0 common.Address, position [4]uint64, amount0 uint64, fee uint64) types.SpendMsg { - var confirmSigs [][65]byte - return types.SpendMsg{ - Blknum0: position[0], - Txindex0: uint16(position[1]), - Oindex0: uint8(position[2]), - DepositNum0: position[3], - Owner0: Owner0, - Input0ConfirmSigs: confirmSigs, - Blknum1: 0, - Txindex1: 0, - Oindex1: 0, - DepositNum1: 0, - Owner1: common.Address{}, - Input1ConfirmSigs: confirmSigs, - Newowner0: NewOwner0, - Amount0: amount0, - Newowner1: common.Address{}, - Amount1: 0, - FeeAmount: fee, - } -} - -// Returns a confirmsig array signed by privKey0 and privKey1 -func CreateConfirmSig(hash []byte, privKey0, privKey1 *ecdsa.PrivateKey, two_inputs bool) (confirmSigs [][65]byte) { - - var confirmSig0 [65]byte - signHash := utils.SignHash(hash) - confirmSig0Slice, _ := ethcrypto.Sign(signHash, privKey0) - copy(confirmSig0[:], confirmSig0Slice) - confirmSigs = append(confirmSigs, confirmSig0) - - var confirmSig1 [65]byte - if two_inputs { - confirmSig1Slice, _ := ethcrypto.Sign(signHash, privKey1) - copy(confirmSig1[:], confirmSig1Slice) - confirmSigs = append(confirmSigs, confirmSig1) - } - return confirmSigs -} - -// helper for constructing single or double input tx -func GetTx(msg types.SpendMsg, privKeyA, privKeyB *ecdsa.PrivateKey, two_sigs bool) (tx types.BaseTx) { - hash := ethcrypto.Keccak256(msg.GetSignBytes()) - signHash := utils.SignHash(hash) - var sigs [2][65]byte - sig, _ := ethcrypto.Sign(signHash, privKeyA) - copy(sigs[0][:], sig) - - if two_sigs { - sig1, _ := ethcrypto.Sign(signHash, privKeyB) - copy(sigs[1][:], sig1) - } - - tx = types.NewBaseTx(msg, sigs) - return tx -} - -// Attempts to spend a non-existent utxo -// without depositing first. -func TestBadSpendMsg(t *testing.T) { - cc := newChildChain() - - privKeyA, _ := ethcrypto.GenerateKey() - privKeyB, _ := ethcrypto.GenerateKey() - - // Construct a SpendMsg - msg := GenerateSimpleMsg(utils.PrivKeyToAddress(privKeyA), utils.PrivKeyToAddress(privKeyB), - [4]uint64{1, 0, 0, 0}, 1000, 1) - - // Signs the hash of the transaction - hash := ethcrypto.Keccak256(msg.GetSignBytes()) - var sigs [2][65]byte - sig, _ := ethcrypto.Sign(hash, privKeyA) - copy(sigs[0][:], sig) - tx := types.NewBaseTx(msg, sigs) - - txBytes, err := rlp.EncodeToBytes(tx) - - require.NoError(t, err) - - // Must Commit to set checkState - cc.BeginBlock(abci.RequestBeginBlock{}) - cc.EndBlock(abci.RequestEndBlock{}) - cc.Commit() - - // Run a check - cres := cc.CheckTx(txBytes) - require.Equal(t, sdk.CodeType(6), - sdk.CodeType(cres.Code), cres.Log) - - // Simulate a Block - cc.BeginBlock(abci.RequestBeginBlock{}) - dres := cc.DeliverTx(txBytes) - require.Equal(t, sdk.CodeType(6), sdk.CodeType(dres.Code), dres.Log) - -} - -func TestSpendDeposit(t *testing.T) { - cc := newChildChain() - - privKeyA, _ := ethcrypto.GenerateKey() - privKeyB, _ := ethcrypto.GenerateKey() - addrA := utils.PrivKeyToAddress(privKeyA) - addrB := utils.PrivKeyToAddress(privKeyB) - - InitTestChain(cc, utils.GenerateAddress(), addrA) - - msg := GenerateSimpleMsg(addrA, addrB, [4]uint64{0, 0, 0, 1}, 100, 0) - - // no confirmation signatures needed for spending a deposit - - // Signs the hash of the transaction - tx := GetTx(msg, privKeyA, nil, false) - txBytes, _ := rlp.EncodeToBytes(tx) - - // Must commit for checkState to be set correctly. Should be fixed in next version of SDK - cc.BeginBlock(abci.RequestBeginBlock{}) - cc.EndBlock(abci.RequestEndBlock{}) - cc.Commit() - - // Simulate a block - cc.BeginBlock(abci.RequestBeginBlock{Header: abci.Header{Height: 1}}) - - // Deliver tx, updates states - dres := cc.DeliverTx(txBytes) - - require.Equal(t, sdk.CodeType(0), sdk.CodeType(dres.Code), dres.Log) - - // Create context - ctx := cc.NewContext(false, abci.Header{}) - - // Retrieve UTXO from context - position := types.NewPlasmaPosition(1, 0, 0, 0) - utxo := cc.utxoMapper.GetUTXO(ctx, addrB.Bytes(), position) - - expected := types.NewBaseUTXO(addrB, [2]common.Address{addrA, common.Address{}}, 100, "", position) - expected.TxHash = tmhash.Sum(txBytes) - - require.Equal(t, expected, utxo, "UTXO did not get added to store correctly") -} - -func TestSpendTx(t *testing.T) { - cc := newChildChain() - - privKeyA, _ := ethcrypto.GenerateKey() - privKeyB, _ := ethcrypto.GenerateKey() - addrA := utils.PrivKeyToAddress(privKeyA) - addrB := utils.PrivKeyToAddress(privKeyB) - - InitTestChain(cc, utils.GenerateAddress(), addrA) - cc.Commit() - - msg := GenerateSimpleMsg(addrA, addrB, [4]uint64{0, 0, 0, 1}, 100, 0) - - // Signs the hash of the transaction - tx := GetTx(msg, privKeyA, nil, false) - txBytes, _ := rlp.EncodeToBytes(tx) - txHash := tmhash.Sum(txBytes) - - // Simulate a block - cc.BeginBlock(abci.RequestBeginBlock{Header: abci.Header{Height: 1}}) - - // Deliver tx, updates states - res := cc.DeliverTx(txBytes) - - require.True(t, res.IsOK(), res.Log) - - cc.EndBlock(abci.RequestEndBlock{}) - cc.Commit() - - cc.BeginBlock(abci.RequestBeginBlock{Header: abci.Header{Height: 5}}) - - // Create context - ctx := cc.NewContext(false, abci.Header{}) - blknumKey := make([]byte, binary.MaxVarintLen64) - binary.PutUvarint(blknumKey, uint64(1)) - blockhash := cc.metadataMapper.GetMetadata(ctx, blknumKey) - - // Test that spending from a non-deposit/non-genesis UTXO works - - // generate simple msg - msg = GenerateSimpleMsg(addrB, addrA, [4]uint64{1, 0, 0, 0}, 100, 0) - - hash := tmhash.Sum(append(txHash, blockhash...)) - // Set confirm signatures - msg.Input0ConfirmSigs = CreateConfirmSig(hash, privKeyA, &ecdsa.PrivateKey{}, false) - - // Signs the hash of the transaction - tx = GetTx(msg, privKeyB, nil, false) - txBytes, _ = rlp.EncodeToBytes(tx) - txHash = tmhash.Sum(txBytes) - - cc.BeginBlock(abci.RequestBeginBlock{Header: abci.Header{Height: 5}}) - - dres := cc.DeliverTx(txBytes) - - require.Equal(t, sdk.CodeType(0), sdk.CodeType(dres.Code), dres.Log) - - // Retrieve UTXO from context - position := types.NewPlasmaPosition(5, 0, 0, 0) - utxo := cc.utxoMapper.GetUTXO(ctx, addrA.Bytes(), position) - - expected := types.NewBaseUTXO(addrA, [2]common.Address{addrB, common.Address{}}, 100, "", position) - expected.TxHash = txHash - - require.Equal(t, expected, utxo, "UTXO did not get added to store correctly") - -} - -// helper struct for readability -type Input struct { - owner_index int64 - addr common.Address - position types.PlasmaPosition - input_index0 int64 - input_index1 int64 -} - -// Tests 1 input 2 ouput, 2 input (different addresses) 1 output, -// 2 input (different addresses) 2 ouputs, and 2 input (same address) 1 output -func TestDifferentTxForms(t *testing.T) { - // Initialize child chain with deposit - cc := newChildChain() - var keys [6]*ecdsa.PrivateKey - var addrs []common.Address - - for i := 0; i < 6; i++ { - keys[i], _ = ethcrypto.GenerateKey() - addrs = append(addrs, utils.PrivKeyToAddress(keys[i])) - } - - InitTestChain(cc, utils.GenerateAddress(), addrs...) - cc.Commit() - - cases := []struct { - input0 Input - input1 Input - newowner0 common.Address - amount0 uint64 - newowner1 common.Address - amount1 uint64 - }{ - // Test Case 0: 1 input 2 output - // Tx spends the genesis deposit and creates 2 new ouputs for addr[1] and addr[2] - { - Input{0, addrs[0], types.NewPlasmaPosition(0, 0, 0, 1), 0, -1}, - Input{0, common.Address{}, types.PlasmaPosition{}, -1, -1}, - addrs[1], 20, - addrs[2], 80, - }, - - // Test Case 1: 2 different inputs, 1 output - // Tx spends outputs from test case 0 and creates 1 output for addr[3] - { - Input{1, addrs[1], types.NewPlasmaPosition(7, 0, 0, 0), 0, -1}, - Input{2, addrs[2], types.NewPlasmaPosition(7, 0, 1, 0), 0, -1}, - addrs[3], 100, - common.Address{}, 0, - }, - - // Test Case 2: 1 input 2 ouput - // Tx spends output from test case 1 and creates 2 new outputs for addr[3] and addr[4] - { - Input{3, addrs[3], types.NewPlasmaPosition(8, 0, 0, 0), 1, 2}, - Input{0, common.Address{}, types.PlasmaPosition{}, -1, -1}, - addrs[3], 75, - addrs[4], 25, - }, - - // Test Case 3: 2 different inputs 2 outputs - // Tx spends outputs from test case 2 and creates 2 new outputs both for addr[3] - { - Input{3, addrs[3], types.NewPlasmaPosition(9, 0, 0, 0), 3, -1}, - Input{4, addrs[4], types.NewPlasmaPosition(9, 0, 1, 0), 3, -1}, - addrs[3], 70, - addrs[3], 30, - }, - - // Test Case 4: 2 same inputs, 1 output (merge) - // Tx spends outputs from test case 3 and creates 1 new output for addr[3] - { - Input{3, addrs[3], types.NewPlasmaPosition(10, 0, 0, 0), 3, 4}, - Input{3, addrs[3], types.NewPlasmaPosition(10, 0, 1, 0), 3, 4}, - addrs[3], 100, - common.Address{}, 0, - }, - } - - for index, tc := range cases { - cc.BeginBlock(abci.RequestBeginBlock{Header: abci.Header{Height: 7 + int64(index)}}) - - input0_index1 := utils.GetIndex(tc.input0.input_index1) - input1_index0 := utils.GetIndex(tc.input1.input_index0) - input1_index1 := utils.GetIndex(tc.input1.input_index1) - - // Create context - ctx := cc.NewContext(false, abci.Header{}) - - msg := types.SpendMsg{ - Blknum0: tc.input0.position.Blknum, - Txindex0: tc.input0.position.TxIndex, - Oindex0: tc.input0.position.Oindex, - DepositNum0: tc.input0.position.DepositNum, - Owner0: tc.input0.addr, - Blknum1: tc.input1.position.Blknum, - Txindex1: tc.input1.position.TxIndex, - Oindex1: tc.input1.position.Oindex, - DepositNum1: tc.input1.position.DepositNum, - Owner1: tc.input1.addr, - Newowner0: tc.newowner0, - Amount0: tc.amount0, - Newowner1: tc.newowner1, - Amount1: tc.amount1, - FeeAmount: 0, - } - - if tc.input0.position.DepositNum == 0 && tc.input0.position.Blknum != 0 { - // note: all cases currently have inputs belonging to the previous tx - // and therefore we only need to grab the first txhash from the inptus - input_utxo := cc.utxoMapper.GetUTXO(ctx, tc.input0.addr.Bytes(), tc.input0.position) - baseutxo, _ := input_utxo.(*types.BaseUTXO) - txhash := baseutxo.TxHash - blknumKey := make([]byte, binary.MaxVarintLen64) - binary.PutUvarint(blknumKey, uint64(7+uint64(index-1))) - blockhash := cc.metadataMapper.GetMetadata(ctx, blknumKey) - hash := tmhash.Sum(append(txhash, blockhash...)) - - msg.Input0ConfirmSigs = CreateConfirmSig(hash, keys[tc.input0.input_index0], keys[input0_index1], tc.input0.input_index1 != -1) - msg.Input1ConfirmSigs = CreateConfirmSig(hash, keys[input1_index0], keys[input1_index1], tc.input1.input_index1 != -1) - } - - tx := GetTx(msg, keys[tc.input0.owner_index], keys[tc.input1.owner_index], !utils.ZeroAddress(msg.Owner1)) - txBytes, _ := rlp.EncodeToBytes(tx) - - dres := cc.DeliverTx(txBytes) - - require.Equal(t, sdk.CodeType(0), sdk.CodeType(dres.Code), dres.Log) - - // Retrieve utxo from context - position := types.NewPlasmaPosition(uint64(index)+7, 0, 0, 0) - utxo := cc.utxoMapper.GetUTXO(ctx, tc.newowner0.Bytes(), position) - - expected := types.NewBaseUTXO(tc.newowner0, [2]common.Address{msg.Owner0, msg.Owner1}, tc.amount0, "", position) - expected.TxHash = tmhash.Sum(txBytes) - - require.Equal(t, expected, utxo, fmt.Sprintf("First UTXO did not get added to the utxo store correctly. Failed on test case: %d", index)) - - if !utils.ZeroAddress(msg.Newowner1) { - position = types.NewPlasmaPosition(uint64(index)+7, 0, 1, 0) - utxo = cc.utxoMapper.GetUTXO(ctx, tc.newowner1.Bytes(), position) - - expected = types.NewBaseUTXO(tc.newowner1, [2]common.Address{msg.Owner0, msg.Owner1}, tc.amount1, "", position) - expected.TxHash = tmhash.Sum(txBytes) - - require.Equal(t, expected, utxo, fmt.Sprintf("Second UTXO did not get added to the utxo store correctly. Failed on test case: %d", index)) - } - - // Check that inputs were removed - utxo = cc.utxoMapper.GetUTXO(ctx, msg.Owner0.Bytes(), tc.input0.position) - require.Nil(t, utxo, fmt.Sprintf("first input was not removed from the utxo store. Failed on test case: %d", index)) - - if !utils.ZeroAddress(msg.Owner1) { - utxo = cc.utxoMapper.GetUTXO(ctx, msg.Owner1.Bytes(), tc.input1.position) - require.Nil(t, utxo, fmt.Sprintf("second input was not removed from the utxo store. Failed on test case: %d", index)) - } - - cc.EndBlock(abci.RequestEndBlock{}) - cc.Commit() - } -} - -// Test that several txs can go into a block and that txindex increments correctly -// Change value of N to increase or decrease txs in the block -func TestMultiTxBlocks(t *testing.T) { - const N = 5 - // Initialize child chain with deposit - cc := newChildChain() - var keys [N]*ecdsa.PrivateKey - var addrs []common.Address - var msgs [N]types.SpendMsg - var txs [N]sdk.Tx - - for i := 0; i < N; i++ { - keys[i], _ = ethcrypto.GenerateKey() - addrs = append(addrs, utils.PrivKeyToAddress(keys[i])) - } - - InitTestChain(cc, utils.GenerateAddress(), addrs...) - cc.Commit() - cc.BeginBlock(abci.RequestBeginBlock{Header: abci.Header{Height: 1}}) - - for i := uint64(0); i < N; i++ { - msgs[i] = GenerateSimpleMsg(addrs[i], addrs[i], [4]uint64{0, 0, 0, i + 1}, 100, 0) - txs[i] = GetTx(msgs[i], keys[i], &ecdsa.PrivateKey{}, false) - txBytes, _ := rlp.EncodeToBytes(txs[i]) - - dres := cc.DeliverTx(txBytes) - require.Equal(t, sdk.CodeType(0), sdk.CodeType(dres.Code), dres.Log) - - } - ctx := cc.NewContext(false, abci.Header{}) - - // Retrieve and check UTXO from context - for i := uint16(0); i < N; i++ { - txBytes, _ := rlp.EncodeToBytes(txs[i]) - position := types.NewPlasmaPosition(1, i, 0, 0) - utxo := cc.utxoMapper.GetUTXO(ctx, addrs[i].Bytes(), position) - - expected := types.NewBaseUTXO(addrs[i], [2]common.Address{addrs[i], common.Address{}}, 100, "", position) - expected.TxHash = tmhash.Sum(txBytes) - - require.Equal(t, expected, utxo, fmt.Sprintf("UTXO %d did not get added to store correctly", i+1)) - - position = types.NewPlasmaPosition(0, 0, 0, uint64(i)+1) - utxo = cc.utxoMapper.GetUTXO(ctx, addrs[i].Bytes(), position) - require.Nil(t, utxo, fmt.Sprintf("deposit %d did not get removed correctly from the utxo store", i+1)) - } - - cc.EndBlock(abci.RequestEndBlock{}) - cc.Commit() - cc.BeginBlock(abci.RequestBeginBlock{Header: abci.Header{Height: 2}}) - - // send to different address - for i := uint16(0); i < N; i++ { - msgs[i].Blknum0 = 1 - msgs[i].Txindex0 = i - msgs[i].DepositNum0 = 0 - - blknumKey := make([]byte, binary.MaxVarintLen64) - binary.PutUvarint(blknumKey, uint64(1)) - blockhash := cc.metadataMapper.GetMetadata(ctx, blknumKey) - - txBytes, _ := rlp.EncodeToBytes(txs[i]) - txHash := tmhash.Sum(txBytes) - - hash := tmhash.Sum(append(txHash, blockhash...)) - msgs[i].Input0ConfirmSigs = CreateConfirmSig(hash, keys[i], &ecdsa.PrivateKey{}, false) - - msgs[i].Newowner0 = addrs[(i+1)%N] - txs[i] = GetTx(msgs[i], keys[i], &ecdsa.PrivateKey{}, false) - txBytes, _ = rlp.EncodeToBytes(txs[i]) - - dres := cc.DeliverTx(txBytes) - require.Equal(t, sdk.CodeType(0), sdk.CodeType(dres.Code), dres.Log) - } - - ctx = cc.NewContext(false, abci.Header{}) - - // Retrieve and check UTXO from context - for i := uint16(0); i < N; i++ { - txBytes, _ := rlp.EncodeToBytes(txs[i]) - utxo := cc.utxoMapper.GetUTXO(ctx, addrs[(i+1)%N].Bytes(), types.NewPlasmaPosition(2, i, 0, 0)) - - expected := types.NewBaseUTXO(addrs[(i+1)%N], [2]common.Address{addrs[i], common.Address{}}, 100, "", types.NewPlasmaPosition(2, i, 0, 0)) - expected.TxHash = tmhash.Sum(txBytes) - - require.Equal(t, expected, utxo, fmt.Sprintf("UTXO %d did not get added to store correctly", i+1)) - - utxo = cc.utxoMapper.GetUTXO(ctx, addrs[i].Bytes(), types.NewPlasmaPosition(1, i, 0, 0)) - require.Nil(t, utxo, fmt.Sprintf("UTXO %d did not get removed from the utxo store correctly", i)) - } - -} - -func TestFee(t *testing.T) { - cc := newChildChain() - - valPrivKey, _ := ethcrypto.GenerateKey() - valAddr := utils.PrivKeyToAddress(valPrivKey) - privKeys := make([]*ecdsa.PrivateKey, 2) - addrs := make([]common.Address, 2) - - for i, _ := range privKeys { - privKeys[i], _ = ethcrypto.GenerateKey() - addrs[i] = utils.PrivKeyToAddress(privKeys[i]) - } - - InitTestChain(cc, valAddr, addrs...) - cc.Commit() - - cc.BeginBlock(abci.RequestBeginBlock{Header: abci.Header{Height: 1}}) - - // Create tx's with fees and deliver them in block 1 - msg1 := GenerateSimpleMsg(addrs[0], addrs[1], [4]uint64{0, 0, 0, 1}, 90, 10) - msg2 := GenerateSimpleMsg(addrs[1], addrs[0], [4]uint64{0, 0, 0, 2}, 90, 10) - - tx1 := GetTx(msg1, privKeys[0], nil, false) - tx2 := GetTx(msg2, privKeys[1], nil, false) - - check1 := cc.Check(tx1) - check2 := cc.Check(tx2) - - res1 := cc.Deliver(tx1) - res2 := cc.Deliver(tx2) - - // Assert checks pass - require.Equal(t, sdk.CodeOK, sdk.CodeType(check1.Code), check1.Log) - require.Equal(t, sdk.CodeOK, sdk.CodeType(check2.Code), check2.Log) - - // Assert delivering tx passes - require.Equal(t, sdk.CodeOK, sdk.CodeType(res1.Code), res1.Log) - require.Equal(t, sdk.CodeOK, sdk.CodeType(res2.Code), res2.Log) - - cc.EndBlock(abci.RequestEndBlock{}) - cc.Commit() - - cc.BeginBlock(abci.RequestBeginBlock{Header: abci.Header{Height: 2}}) - - expectedPosition1 := types.NewPlasmaPosition(1, uint16(0), uint8(0), 0) - expectedPosition2 := types.NewPlasmaPosition(1, uint16(1), uint8(0), 0) - - expectedValPosition := types.NewPlasmaPosition(1, uint16(1<<16-1), uint8(0), 0) - - ctx := cc.NewContext(false, abci.Header{Height: 2}) - - utxo1 := cc.utxoMapper.GetUTXO(ctx, addrs[1].Bytes(), expectedPosition1) - utxo2 := cc.utxoMapper.GetUTXO(ctx, addrs[0].Bytes(), expectedPosition2) - - valUTXO := cc.utxoMapper.GetUTXO(ctx, valAddr.Bytes(), expectedValPosition) - - // Check that users and validators have expected UTXO's - require.Equal(t, uint64(90), utxo1.GetAmount(), "UTXO1 does not have expected amount") - require.Equal(t, uint64(90), utxo2.GetAmount(), "UTXO2 does not have expected amount") - require.Equal(t, uint64(20), valUTXO.GetAmount(), "Validator fees did not get collected into UTXO correctly") - - // Check that validator can spend his fees as if they were a regular UTXO on sidechain - valMsg := GenerateSimpleMsg(valAddr, addrs[0], [4]uint64{1, 1<<16 - 1, 0, 0}, 10, 10) - - valTx := GetTx(valMsg, valPrivKey, nil, false) - - valRes := cc.Deliver(valTx) - - require.Equal(t, sdk.CodeOK, sdk.CodeType(valRes.Code), valRes.Log) - - cc.EndBlock(abci.RequestEndBlock{}) - cc.Commit() - - cc.BeginBlock(abci.RequestBeginBlock{Header: abci.Header{Height: 3}}) - - ctx = cc.NewContext(false, abci.Header{Height: 3}) - - // Check that fee Amount gets reset between blocks. feeAmount for block 2 is 10 not 30. - feeUTXO2 := cc.utxoMapper.GetUTXO(ctx, valAddr.Bytes(), types.NewPlasmaPosition(2, 1<<16-1, 0, 0)) - require.Equal(t, uint64(10), feeUTXO2.GetAmount(), "Fee Amount on second block is incorrect") -} diff --git a/app/genesis.go b/app/genesis.go index 6ce9856..1f858bf 100644 --- a/app/genesis.go +++ b/app/genesis.go @@ -1,181 +1,25 @@ package app import ( - "encoding/json" - "errors" - "fmt" - "os" - "strconv" - - "github.com/spf13/pflag" - "github.com/spf13/viper" - crypto "github.com/tendermint/tendermint/crypto" - tmtypes "github.com/tendermint/tendermint/types" - - "github.com/FourthState/plasma-mvp-sidechain/types" - "github.com/FourthState/plasma-mvp-sidechain/x/utxo" - "github.com/cosmos/cosmos-sdk/codec" - "github.com/cosmos/cosmos-sdk/server" - "github.com/ethereum/go-ethereum/common" + "github.com/tendermint/tendermint/crypto" ) -// State to Unmarshal +// GenesisState specifies the validator of the chain type GenesisState struct { - Validator GenesisValidator `json:"genvalidator"` - UTXOs []GenesisUTXO `json:"UTXOs"` + Validator GenesisValidator `json:"validator"` } +// GenesisValidator holds the consensus public key and fee address of +// the validator. ConsPubKey is tendermint Ed25119 public key for signing +// consensus messages. Address is Ethereum address for collecting fees. type GenesisValidator struct { ConsPubKey crypto.PubKey `json:"validator_pubkey"` Address string `json:"fee_address"` } -type GenesisUTXO struct { - Address string - Denom string - Position [4]string -} - -func NewGenesisUTXO(addr string, amount string, position [4]string) GenesisUTXO { - utxo := GenesisUTXO{ - Address: addr, - Denom: amount, - Position: position, - } - return utxo -} - -func ToUTXO(gutxo GenesisUTXO) utxo.UTXO { - // Any failed str conversion defaults to 0 - addr := common.HexToAddress(gutxo.Address) - amount, _ := strconv.ParseUint(gutxo.Denom, 10, 64) - utxo := &types.BaseUTXO{ - InputAddresses: [2]common.Address{addr, common.Address{}}, - Address: addr, - Amount: amount, - Denom: types.Denom, - } - blkNum, _ := strconv.ParseUint(gutxo.Position[0], 10, 64) - txIndex, _ := strconv.ParseUint(gutxo.Position[1], 10, 16) - oIndex, _ := strconv.ParseUint(gutxo.Position[2], 10, 8) - depNum, _ := strconv.ParseUint(gutxo.Position[3], 10, 64) - - position := types.NewPlasmaPosition(blkNum, uint16(txIndex), uint8(oIndex), depNum) - utxo.SetPosition(position) - return utxo -} - -var ( - flagAddress = "address" - flagClientHome = "home-client" - flagOWK = "owk" - - // UTXO amount awarded - freeEtherVal = int64(100) - - // default home directories for expected binaries - DefaultCLIHome = os.ExpandEnv("$HOME/.plasmacli") - DefaultNodeHome = os.ExpandEnv("$HOME/.plasmad") -) - -// get app init parameters for server init command -func PlasmaAppInit() server.AppInit { - fsAppGenTx := pflag.NewFlagSet("", pflag.ContinueOnError) - fsAppGenTx.String(flagAddress, "", "address, required") - fsAppGenTx.String(flagClientHome, DefaultCLIHome, - "home directory for the client, used for key generation") - fsAppGenTx.Bool(flagOWK, false, "overwrite the accounts created") - - return server.AppInit{ - AppGenState: PlasmaAppGenStateJSON, - } -} - -// simple genesis tx -type PlasmaGenTx struct { - // currently takes address as string because unmarshaling Ether address fails - Address string `json:"address"` -} - -// Generate a gaia genesis transaction with flags -func PlasmaAppGenTx(cdc *codec.Codec, pk crypto.PubKey) ( - appGenTx, cliPrint json.RawMessage, validator tmtypes.GenesisValidator, err error) { - addrString := viper.GetString(flagAddress) - overwrite := viper.GetBool(flagOWK) - - bz, err := cdc.MarshalJSON("success") - cliPrint = json.RawMessage(bz) - appGenTx, _, validator, err = PlasmaAppGenTxNF(cdc, pk, addrString, overwrite) - return -} - -// Generate a gaia genesis transaction without flags -func PlasmaAppGenTxNF(cdc *codec.Codec, pk crypto.PubKey, addr string, overwrite bool) ( - appGenTx, cliPrint json.RawMessage, validator tmtypes.GenesisValidator, err error) { - - var bz []byte - plasmaGenTx := PlasmaGenTx{ - Address: addr, - } - bz, err = codec.MarshalJSONIndent(cdc, plasmaGenTx) - if err != nil { - return - } - appGenTx = json.RawMessage(bz) - - validator = tmtypes.GenesisValidator{ - PubKey: pk, - Power: 1, - } - return -} - -// Create the core parameters for genesis initialization for gaia -// note that the pubkey input is this machines pubkey -func PlasmaAppGenState(cdc *codec.Codec, genDoc tmtypes.GenesisDoc, appGenTxs []json.RawMessage) (genesisState GenesisState, err error) { - - if err = cdc.UnmarshalJSON(genDoc.AppState, &genesisState); err != nil { - return genesisState, err - } - - if len(appGenTxs) == 0 { - err = errors.New("must provide at least genesis transaction") - return - } - - // get genesis flag account information - genUTXO := make([]GenesisUTXO, len(appGenTxs)) - for i, appGenTx := range appGenTxs { - - var genTx PlasmaGenTx - err = cdc.UnmarshalJSON(appGenTx, &genTx) - if err != nil { - return - } - - genUTXO[i] = NewGenesisUTXO(genTx.Address, "100", [4]string{"0", "0", "0", fmt.Sprintf("%d", i+1)}) - } - - // create the final app state - genesisState.UTXOs = genUTXO - return -} - -// PlasmaAppGenState but with JSON -func PlasmaAppGenStateJSON(cdc *codec.Codec, genDoc tmtypes.GenesisDoc, appGenTxs []json.RawMessage) (appState json.RawMessage, err error) { - - // create the final app state - genesisState, err := PlasmaAppGenState(cdc, genDoc, appGenTxs) - if err != nil { - return nil, err - } - appState, err = codec.MarshalJSONIndent(cdc, genesisState) - return -} - -func NewDefaultGenesisState() GenesisState { +// NewDefaultGenesisState returns a GenesisState instance +func NewDefaultGenesisState(pubKey crypto.PubKey) GenesisState { return GenesisState{ - Validator: GenesisValidator{}, - UTXOs: nil, + Validator: GenesisValidator{pubKey, ""}, } } diff --git a/app/genesis_test.go b/app/genesis_test.go deleted file mode 100644 index 4509813..0000000 --- a/app/genesis_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package app - -import ( - "fmt" - "github.com/ethereum/go-ethereum/common" - "github.com/stretchr/testify/assert" - abci "github.com/tendermint/tendermint/abci/types" - secp256k1 "github.com/tendermint/tendermint/crypto/secp256k1" - dbm "github.com/tendermint/tendermint/libs/db" - "github.com/tendermint/tendermint/libs/log" - tmtypes "github.com/tendermint/tendermint/types" - "os" - "testing" - - "github.com/FourthState/plasma-mvp-sidechain/utils" -) - -func TestGenesisState(t *testing.T) { - logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)).With("module", "sdk/app") - db := dbm.NewMemDB() - app := NewChildChain(logger, db, nil) - - addrs := []common.Address{utils.GenerateAddress(), utils.GenerateAddress()} - - var genUTXOs []GenesisUTXO - for i, addr := range addrs { - genUTXOs = append(genUTXOs, NewGenesisUTXO(addr.Hex(), "100", [4]string{"0", "0", "0", fmt.Sprintf("%d", i+1)})) - } - - pubKey := secp256k1.GenPrivKey().PubKey() - valAddr := utils.GenerateAddress() - - genValidator := GenesisValidator{ - ConsPubKey: pubKey, - Address: valAddr.String(), - } - - genState := GenesisState{ - Validator: genValidator, - UTXOs: genUTXOs, - } - - appBytes, err := app.cdc.MarshalJSON(genState) - assert.Nil(t, err) - var genState2 GenesisState - err = app.cdc.UnmarshalJSON(appBytes, &genState2) - assert.Nil(t, err) - - assert.Equal(t, genState, genState2) - - res := app.InitChain(abci.RequestInitChain{AppStateBytes: appBytes}) - expected := abci.ResponseInitChain{ - Validators: []abci.ValidatorUpdate{abci.ValidatorUpdate{ - PubKey: tmtypes.TM2PB.PubKey(pubKey), - Power: 1, - }}, - } - assert.Equal(t, expected, res) -} diff --git a/app/options.go b/app/options.go new file mode 100644 index 0000000..832e1f2 --- /dev/null +++ b/app/options.go @@ -0,0 +1,58 @@ +package app + +import ( + "crypto/ecdsa" + "encoding/hex" + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmad/config" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "strconv" + "time" +) + +// SetPlasmaOptionsFromConfig sets the plasma specific configurations into +// app state. The parameters are passed from the plasmad config file. +func SetPlasmaOptionsFromConfig(conf config.PlasmaConfig) func(*PlasmaMVPChain) { + var privateKey *ecdsa.PrivateKey + var blockFinality uint64 + + if conf.IsOperator { + d, err := hex.DecodeString(conf.OperatorPrivateKey) + + if err != nil { + errMsg := fmt.Sprintf("Could not parse private key: %v", err) + panic(errMsg) + } + + privateKey, err = crypto.ToECDSA(d) + if err != nil { + errMsg := fmt.Sprintf("Could not load the private key: %v", err) + panic(errMsg) + } + } + + blockFinality, err := strconv.ParseUint(conf.EthBlockFinality, 10, 64) + if err != nil { + errMsg := fmt.Sprintf("Could not parse block finality: %v", err) + panic(errMsg) + } + + if !common.IsHexAddress(conf.EthPlasmaContractAddr) { + panic("invalid contract address. please use hex format") + } + + dur, err := time.ParseDuration(conf.PlasmaCommitmentRate) + if err != nil { + panic("commitment rate must be able to be parsed into a golang Duration type") + } + + return func(pc *PlasmaMVPChain) { + pc.operatorPrivateKey = privateKey + pc.isOperator = conf.IsOperator + pc.plasmaContractAddress = common.HexToAddress(conf.EthPlasmaContractAddr) + pc.blockCommitmentRate = dur + pc.nodeURL = conf.EthNodeURL + pc.blockFinality = blockFinality + } +} diff --git a/auth/ante.go b/auth/ante.go deleted file mode 100644 index 3c0129c..0000000 --- a/auth/ante.go +++ /dev/null @@ -1,178 +0,0 @@ -package auth - -import ( - "encoding/binary" - "fmt" - types "github.com/FourthState/plasma-mvp-sidechain/types" - utils "github.com/FourthState/plasma-mvp-sidechain/utils" - "github.com/FourthState/plasma-mvp-sidechain/x/metadata" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/common" - ethcrypto "github.com/ethereum/go-ethereum/crypto" - "github.com/tendermint/tendermint/crypto/tmhash" - "reflect" - - "github.com/FourthState/plasma-mvp-sidechain/x/utxo" -) - -// NewAnteHandler returns an AnteHandler that checks signatures, -// confirm signatures, and increments the feeAmount -func NewAnteHandler(utxoMapper utxo.Mapper, metadataMapper metadata.MetadataMapper, feeUpdater utxo.FeeUpdater) sdk.AnteHandler { - return func( - ctx sdk.Context, tx sdk.Tx, simulate bool, - ) (_ sdk.Context, _ sdk.Result, abort bool) { - - baseTx, ok := tx.(types.BaseTx) - if !ok { - return ctx, sdk.ErrInternal("tx must be in form of BaseTx").Result(), true - } - - // Assert that there are signatures - sigs := baseTx.GetSignatures() - if len(sigs) == 0 { - return ctx, - sdk.ErrUnauthorized("no signers").Result(), - true - } - - // Base Tx must have only one msg - msg := baseTx.GetMsgs()[0] - - // Assert that number of signatures is correct. - var signerAddrs = msg.GetSigners() - - spendMsg, ok := msg.(types.SpendMsg) - if !ok { - return ctx, sdk.ErrInternal("msg must be of type SpendMsg").Result(), true - } - signBytes := spendMsg.GetSignBytes() - - // Verify the first input signature - addr0 := common.BytesToAddress(signerAddrs[0].Bytes()) - position0 := types.PlasmaPosition{spendMsg.Blknum0, spendMsg.Txindex0, spendMsg.Oindex0, spendMsg.DepositNum0} - - res := checkUTXO(ctx, utxoMapper, position0, addr0) - if !res.IsOK() { - return ctx, res, true - } - - res = processSig(addr0, sigs[0], signBytes) - - if !res.IsOK() { - return ctx, res, true - } - - // verify the confirmation signature if the input is not a deposit - if position0.DepositNum == 0 && position0.TxIndex != 1<<16-1 { - res = processConfirmSig(ctx, utxoMapper, metadataMapper, position0, addr0, spendMsg.Input0ConfirmSigs) - if !res.IsOK() { - return ctx, res, true - } - } - - // Verify the second input - if utils.ValidAddress(spendMsg.Owner1) { - addr1 := common.BytesToAddress(signerAddrs[1].Bytes()) - position1 := types.PlasmaPosition{spendMsg.Blknum1, spendMsg.Txindex1, spendMsg.Oindex1, spendMsg.DepositNum1} - - res := checkUTXO(ctx, utxoMapper, position1, addr1) - if !res.IsOK() { - return ctx, res, true - } - - res = processSig(addr1, sigs[1], signBytes) - - if !res.IsOK() { - return ctx, res, true - } - - if position1.DepositNum == 0 && position1.TxIndex != 1<<16-1 { - res = processConfirmSig(ctx, utxoMapper, metadataMapper, position1, addr1, spendMsg.Input1ConfirmSigs) - if !res.IsOK() { - return ctx, res, true - } - } - } - - balanceErr := utxo.AnteHelper(ctx, utxoMapper, tx, simulate, feeUpdater) - if balanceErr != nil { - return ctx, balanceErr.Result(), true - } - - // TODO: tx tags (?) - return ctx, sdk.Result{}, false // continue... - } -} - -func processSig( - addr common.Address, sig [65]byte, signBytes []byte) ( - res sdk.Result) { - - hash := ethcrypto.Keccak256(signBytes) - signHash := utils.SignHash(hash) - pubKey, err := ethcrypto.SigToPub(signHash, sig[:]) - - if err != nil || !reflect.DeepEqual(ethcrypto.PubkeyToAddress(*pubKey).Bytes(), addr.Bytes()) { - return sdk.ErrUnauthorized(fmt.Sprintf("signature verification failed for: %X", addr.Bytes())).Result() - } - - return sdk.Result{} -} - -func processConfirmSig( - ctx sdk.Context, utxoMapper utxo.Mapper, metadataMapper metadata.MetadataMapper, - position types.PlasmaPosition, addr common.Address, sigs [][65]byte) ( - res sdk.Result) { - - // Verify utxo exists - utxo := utxoMapper.GetUTXO(ctx, addr.Bytes(), &position) - if utxo == nil { - return sdk.ErrUnknownRequest(fmt.Sprintf("confirm Sig verification failed: UTXO trying to be spent, does not exist: %v.", position)).Result() - } - plasmaUTXO, ok := utxo.(*types.BaseUTXO) - if !ok { - return sdk.ErrInternal("utxo must be of type BaseUTXO").Result() - } - inputAddresses := plasmaUTXO.GetInputAddresses() - - // Get the block hash - blknumKey := make([]byte, binary.MaxVarintLen64) - binary.PutUvarint(blknumKey, plasmaUTXO.GetPosition().Get()[0].Uint64()) - blockHash := metadataMapper.GetMetadata(ctx, blknumKey) - - txHash := plasmaUTXO.GetTxHash() - - hash := append(txHash, blockHash...) - confirmHash := tmhash.Sum(hash) - signHash := utils.SignHash(confirmHash) - - pubKey0, err0 := ethcrypto.SigToPub(signHash, sigs[0][:]) - if err0 != nil || !reflect.DeepEqual(ethcrypto.PubkeyToAddress(*pubKey0).Bytes(), inputAddresses[0].Bytes()) { - return sdk.ErrUnauthorized("confirm signature 0 verification failed").Result() - } - - if utils.ValidAddress(inputAddresses[1]) { - pubKey1, err1 := ethcrypto.SigToPub(signHash, sigs[1][:]) - if err1 != nil || !reflect.DeepEqual(ethcrypto.PubkeyToAddress(*pubKey1).Bytes(), inputAddresses[1].Bytes()) { - return sdk.ErrUnauthorized("confirm signature 1 verification failed").Result() - } - } - - return sdk.Result{} -} - -// Checks that utxo at the position specified exists, matches the address in the SpendMsg -// and returns the denomination associated with the utxo -func checkUTXO(ctx sdk.Context, mapper utxo.Mapper, position types.PlasmaPosition, addr common.Address) sdk.Result { - utxo := mapper.GetUTXO(ctx, addr.Bytes(), &position) - if utxo == nil { - return sdk.ErrUnknownRequest(fmt.Sprintf("UTXO trying to be spent, does not exist: %v.", position)).Result() - } - - // Verify that utxo owner equals input address in the transaction - if !reflect.DeepEqual(utxo.GetAddress(), addr.Bytes()) { - return sdk.ErrUnauthorized(fmt.Sprintf("signer does not match utxo owner, signer: %X owner: %X", addr.Bytes(), utxo.GetAddress())).Result() - } - - return sdk.Result{} -} diff --git a/auth/ante_test.go b/auth/ante_test.go deleted file mode 100644 index 3401977..0000000 --- a/auth/ante_test.go +++ /dev/null @@ -1,296 +0,0 @@ -package auth - -import ( - "crypto/ecdsa" - "encoding/binary" - "fmt" - types "github.com/FourthState/plasma-mvp-sidechain/types" - utils "github.com/FourthState/plasma-mvp-sidechain/utils" - "github.com/FourthState/plasma-mvp-sidechain/x/metadata" - "github.com/FourthState/plasma-mvp-sidechain/x/utxo" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/common" - ethcrypto "github.com/ethereum/go-ethereum/crypto" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - abci "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/crypto/tmhash" - "github.com/tendermint/tendermint/libs/log" - "testing" -) - -func setup() (sdk.Context, utxo.Mapper, metadata.MetadataMapper, utxo.FeeUpdater) { - ms, capKey, metadataCapKey := utxo.SetupMultiStore() - - ctx := sdk.NewContext(ms, abci.Header{}, false, log.NewNopLogger()) - cdc := utxo.MakeCodec() - types.RegisterAmino(cdc) - - mapper := utxo.NewBaseMapper(capKey, cdc) - metadataMapper := metadata.NewMetadataMapper(metadataCapKey) - - return ctx, mapper, metadataMapper, feeUpdater -} - -// should be modified when fees are implemented -func feeUpdater(outputs []utxo.Output) sdk.Error { - return nil -} - -func GenSpendMsg() types.SpendMsg { - // Creates Basic Spend Msg with owners and recipients - var confirmSigs [][65]byte - privKeyA, _ := ethcrypto.GenerateKey() - privKeyB, _ := ethcrypto.GenerateKey() - - return types.SpendMsg{ - Blknum0: 1, - Txindex0: 0, - Oindex0: 0, - DepositNum0: 0, - Owner0: utils.PrivKeyToAddress(privKeyA), - Input0ConfirmSigs: confirmSigs, - Blknum1: 1, - Txindex1: 1, - Oindex1: 0, - DepositNum1: 0, - Owner1: utils.PrivKeyToAddress(privKeyA), - Input1ConfirmSigs: confirmSigs, - Newowner0: utils.PrivKeyToAddress(privKeyB), - Amount0: 150, - Newowner1: utils.PrivKeyToAddress(privKeyB), - Amount1: 50, - FeeAmount: 0, - } -} - -// Returns a confirmsig array signed by privKey0 and privKey1 -func CreateConfirmSig(hash []byte, privKey0, privKey1 *ecdsa.PrivateKey, two_inputs bool) (confirmSigs [][65]byte) { - - var confirmSig0 [65]byte - signHash := utils.SignHash(hash) - confirmSig0Slice, _ := ethcrypto.Sign(signHash, privKey0) - copy(confirmSig0[:], confirmSig0Slice) - - var confirmSig1 [65]byte - if two_inputs { - confirmSig1Slice, _ := ethcrypto.Sign(signHash, privKey1) - copy(confirmSig1[:], confirmSig1Slice) - } - confirmSigs = [][65]byte{confirmSig0, confirmSig1} - return confirmSigs -} - -// helper for constructing single or double input tx -func GetTx(msg types.SpendMsg, privKey0, privKey1 *ecdsa.PrivateKey, two_sigs bool) (tx types.BaseTx) { - hash := ethcrypto.Keccak256(msg.GetSignBytes()) - signHash := utils.SignHash(hash) - sig0, _ := ethcrypto.Sign(signHash, privKey0) - var sigs [2][65]byte - copy(sigs[0][:], sig0) - - if two_sigs { - sig1, _ := ethcrypto.Sign(signHash, privKey1) - copy(sigs[1][:], sig1) - } - - tx = types.NewBaseTx(msg, sigs) - - return tx -} - -// helper for constructing input addresses -func getInputAddr(addr0, addr1 common.Address, two bool) [2]common.Address { - if two { - return [2]common.Address{addr0, addr1} - } else { - return [2]common.Address{addr0, common.Address{}} - } -} - -// No signatures are provided -func TestNoSigs(t *testing.T) { - ctx, mapper, metadataMapper, feeUpdater := setup() - - var msg = GenSpendMsg() - var emptysigs [2][65]byte - tx := types.NewBaseTx(msg, emptysigs) - - // Add input UTXOs to mapper - utxo1 := types.NewBaseUTXO(msg.Owner0, [2]common.Address{}, 100, types.Denom, types.NewPlasmaPosition(1, 0, 0, 0)) - utxo2 := types.NewBaseUTXO(msg.Owner0, [2]common.Address{}, 100, types.Denom, types.NewPlasmaPosition(1, 1, 0, 0)) - mapper.AddUTXO(ctx, utxo1) - mapper.AddUTXO(ctx, utxo2) - - handler := NewAnteHandler(mapper, metadataMapper, feeUpdater) - _, res, abort := handler(ctx, tx, false) - - assert.Equal(t, true, abort, "did not abort with no signatures") - require.Equal(t, sdk.ToABCICode(sdk.CodespaceType(1), sdk.CodeType(4)), res.Code, fmt.Sprintf("tx had processed with no signatures: %s", res.Log)) -} - -// The wrong amount of signatures are provided -func TestNotEnoughSigs(t *testing.T) { - ctx, mapper, metadataMapper, feeUpdater := setup() - - var msg = GenSpendMsg() - priv, _ := ethcrypto.GenerateKey() - hash := ethcrypto.Keccak256(msg.GetSignBytes()) - sig, _ := ethcrypto.Sign(hash, priv) - var sigs [2][65]byte - copy(sigs[0][:], sig) - tx := types.NewBaseTx(msg, sigs) - - // Add input UTXOs to mapper - utxo1 := types.NewBaseUTXO(msg.Owner0, [2]common.Address{}, 100, types.Denom, types.NewPlasmaPosition(1, 0, 0, 0)) - utxo2 := types.NewBaseUTXO(msg.Owner0, [2]common.Address{}, 100, types.Denom, types.NewPlasmaPosition(1, 1, 0, 0)) - mapper.AddUTXO(ctx, utxo1) - mapper.AddUTXO(ctx, utxo2) - - handler := NewAnteHandler(mapper, metadataMapper, feeUpdater) - _, res, abort := handler(ctx, tx, false) - - assert.Equal(t, true, abort, "did not abort with incorrect number of signatures") - require.Equal(t, sdk.ToABCICode(sdk.CodespaceType(1), sdk.CodeType(4)), res.Code, fmt.Sprintf("tx had processed with incorrect number of signatures: %s", res.Log)) -} - -// helper struct for readability -type input struct { - owner_index int64 - addr common.Address - position types.PlasmaPosition - input_index0 int64 - input_index1 int64 -} - -// Tests a different cases. -func TestDifferentCases(t *testing.T) { - ctx, mapper, metadataMapper, feeUpdater := setup() - - var keys [6]*ecdsa.PrivateKey - var addrs []common.Address - - for i := 0; i < 6; i++ { - keys[i], _ = ethcrypto.GenerateKey() - addrs = append(addrs, utils.PrivKeyToAddress(keys[i])) - } - - cases := []struct { - input0 input - input1 input - newowner0 common.Address - amount0 uint64 - newowner1 common.Address - amount1 uint64 - abort bool - }{ - // Test Case 0: Tx signed by the wrong address - { - input{1, addrs[0], types.NewPlasmaPosition(2, 0, 0, 0), 1, -1}, // first input - input{-1, common.Address{}, types.PlasmaPosition{}, -1, -1}, // second input - addrs[1], 1000, // first output - addrs[2], 1000, // second output - true, - }, - - // Test Case 1: Inputs != Outputs + Fee - { - input{0, addrs[0], types.NewPlasmaPosition(3, 0, 0, 0), 1, -1}, - input{-1, common.Address{}, types.PlasmaPosition{}, -1, -1}, - addrs[1], 2000, - addrs[2], 1000, - true, - }, - - // Test Case 2: 1 input 2 output - { - input{0, addrs[0], types.NewPlasmaPosition(4, 0, 0, 0), 1, -1}, - input{-1, common.Address{}, types.PlasmaPosition{}, -1, -1}, - addrs[1], 1000, - addrs[2], 1000, - false, - }, - - // Test Case 3: 2 input 2 output - { - input{1, addrs[1], types.NewPlasmaPosition(5, 0, 0, 0), 0, -1}, - input{2, addrs[2], types.NewPlasmaPosition(5, 0, 1, 0), 0, -1}, - addrs[3], 2500, - addrs[4], 1500, - false, - }, - } - - for index, tc := range cases { - input0_index1 := utils.GetIndex(tc.input0.input_index1) - input1_index0 := utils.GetIndex(tc.input1.input_index0) - input1_index1 := utils.GetIndex(tc.input1.input_index1) - - // for ease of testing, blockHash is hash of case number - blockHash := tmhash.Sum([]byte(string(index))) - var msg = types.SpendMsg{ - Blknum0: tc.input0.position.Blknum, - Txindex0: tc.input0.position.TxIndex, - Oindex0: tc.input0.position.Oindex, - DepositNum0: tc.input0.position.DepositNum, - Owner0: tc.input0.addr, - Blknum1: tc.input1.position.Blknum, - Txindex1: tc.input1.position.TxIndex, - Oindex1: tc.input1.position.Oindex, - DepositNum1: tc.input1.position.DepositNum, - Owner1: tc.input1.addr, - Newowner0: tc.newowner0, - Amount0: tc.amount0, - Newowner1: tc.newowner1, - Amount1: tc.amount1 - 5, - FeeAmount: 5, - } - - owner_index1 := utils.GetIndex(tc.input1.owner_index) - tx := GetTx(msg, keys[tc.input0.owner_index], keys[owner_index1], tc.input1.owner_index != -1) - - handler := NewAnteHandler(mapper, metadataMapper, feeUpdater) - _, res, abort := handler(ctx, tx, false) - - assert.Equal(t, true, abort, fmt.Sprintf("did not abort on utxo that does not exist. Case: %d", index)) - require.Equal(t, sdk.ToABCICode(sdk.CodespaceType(1), sdk.CodeType(6)), res.Code, res.Log) - - inputAddr := getInputAddr(addrs[tc.input0.input_index0], addrs[input0_index1], tc.input0.input_index1 != -1) - utxo0 := types.NewBaseUTXO(tc.input0.addr, inputAddr, 2000, types.Denom, tc.input0.position) - txhash0 := tmhash.Sum([]byte("first utxo")) - utxo0.TxHash = txhash0 - - var utxo1 *types.BaseUTXO - var txhash1 []byte - if tc.input1.owner_index != -1 { - txhash1 = tmhash.Sum([]byte("second utxo")) - inputAddr = getInputAddr(addrs[input1_index0], addrs[input1_index1], tc.input0.input_index1 != -1) - utxo1 = types.NewBaseUTXO(tc.input1.addr, inputAddr, 2000, types.Denom, tc.input1.position) - utxo1.TxHash = txhash1 - } - - blknumKey := make([]byte, binary.MaxVarintLen64) - binary.PutUvarint(blknumKey, tc.input0.position.Get()[0].Uint64()) - metadataMapper.StoreMetadata(ctx, blknumKey, blockHash) - - // for ease of testing, txhash is simplified - // app_test tests for correct functionality when setting tx_hash - mapper.AddUTXO(ctx, utxo0) - hash := tmhash.Sum(append(txhash0, blockHash...)) - msg.Input0ConfirmSigs = CreateConfirmSig(hash, keys[tc.input0.input_index0], keys[input0_index1], tc.input0.input_index1 != -1) - if tc.input1.owner_index != -1 { - hash = tmhash.Sum(append(txhash1, blockHash...)) - mapper.AddUTXO(ctx, utxo1) - msg.Input1ConfirmSigs = CreateConfirmSig(hash, keys[input1_index0], keys[input1_index1], tc.input1.input_index1 != -1) - } - tx = GetTx(msg, keys[tc.input0.owner_index], keys[owner_index1], tc.input1.owner_index != -1) - _, res, abort = handler(ctx, tx, false) - - assert.Equal(t, tc.abort, abort, fmt.Sprintf("aborted on case: %d", index)) - if tc.abort == false { - require.Equal(t, sdk.ToABCICode(sdk.CodespaceType(1), sdk.CodeType(0)), res.Code, res.Log) - } else { - require.NotEqual(t, sdk.ToABCICode(sdk.CodespaceType(1), sdk.CodeType(0)), res.Code, res.Log) - } - } -} diff --git a/client/context/helpers.go b/client/context/helpers.go deleted file mode 100644 index 7994b75..0000000 --- a/client/context/helpers.go +++ /dev/null @@ -1,139 +0,0 @@ -package context - -import ( - "fmt" - "github.com/pkg/errors" - "strings" - - "github.com/ethereum/go-ethereum/accounts" - "github.com/ethereum/go-ethereum/common" - ethcrypto "github.com/ethereum/go-ethereum/crypto" - rlp "github.com/ethereum/go-ethereum/rlp" - ctypes "github.com/tendermint/tendermint/rpc/core/types" - - "github.com/FourthState/plasma-mvp-sidechain/client" - "github.com/FourthState/plasma-mvp-sidechain/types" - "github.com/FourthState/plasma-mvp-sidechain/utils" - "github.com/FourthState/plasma-mvp-sidechain/x/utxo" -) - -// Broadcast the transaction bytes to Tendermint -func (ctx ClientContext) BroadcastTx(tx []byte) (*ctypes.ResultBroadcastTxCommit, error) { - node, err := ctx.GetNode() - if err != nil { - return nil, err - } - - res, err := node.BroadcastTxCommit(tx) - if err != nil { - return res, err - } - - if res.CheckTx.Code != uint32(0) { - return res, errors.Errorf("CheckTx failed: (%d) %s", - res.CheckTx.Code, res.CheckTx.Log) - } - if res.DeliverTx.Code != uint32(0) { - return res, errors.Errorf("DeliverTx failed: (%d) %s", - res.DeliverTx.Code, res.DeliverTx.Log) - } - return res, err - -} - -// sign and build the transaction from the msg -func (ctx ClientContext) SignBuildBroadcast(addrs [2]common.Address, msg types.SpendMsg, dir string) (res *ctypes.ResultBroadcastTxCommit, err error) { - - sig, err := ctx.GetSignature(addrs[0], msg, dir) - if err != nil { - return nil, err - } - var sigs [2][65]byte - copy(sigs[0][:], sig) - - if utils.ValidAddress(addrs[1]) { - sig, err = ctx.GetSignature(addrs[1], msg, dir) - if err != nil { - return nil, err - } - copy(sigs[1][:], sig) - } - - tx := types.NewBaseTx(msg, sigs) - - txBytes, err := rlp.EncodeToBytes(tx) - if err != nil { - return nil, err - } - - return ctx.BroadcastTx(txBytes) -} - -func (ctx ClientContext) GetSignature(addr common.Address, msg utxo.SpendMsg, dir string) (sig []byte, err error) { - - passphrase, err := ctx.GetPassphraseFromStdin(addr) - if err != nil { - return nil, err - } - ks := client.GetKeyStore(dir) - acc := accounts.Account{ - Address: addr, - } - - acct, err := ks.Find(acc) - if err != nil { - return nil, err - } - - bz := msg.GetSignBytes() - hash := ethcrypto.Keccak256(bz) - signHash := utils.SignHash(hash) - - sig, err = ks.SignHashWithPassphrase(acct, passphrase, signHash) - if err != nil { - return nil, err - } - return sig, nil - -} - -// Get the from address from the name flag -func (ctx ClientContext) GetInputAddresses(dir string) (from [2]common.Address, err error) { - - ks := client.GetKeyStore(dir) - - addrsStr := ctx.InputAddresses - if addrsStr == "" { - return [2]common.Address{}, errors.Errorf("must provide an address to send from") - } - - addrs := strings.Split(addrsStr, ",") - // first input address - from[0], err = client.StrToAddress(strings.TrimSpace(addrs[0])) - if err != nil { - return [2]common.Address{}, err - } - if len(addrs) > 1 { - // second input address - from[1], err = client.StrToAddress(strings.TrimSpace(addrs[1])) - if err != nil { - return [2]common.Address{}, err - } - } - - if !ks.HasAddress(from[0]) { - return [2]common.Address{}, errors.Errorf("no account for: %s", from[0].Hex()) - } - if len(from) > 1 && !utils.ZeroAddress(from[1]) && !ks.HasAddress(from[1]) { - return [2]common.Address{}, errors.Errorf("no account for: %s", from[1].Hex()) - } - - return from, nil -} - -// Get passphrase from std input -func (ctx ClientContext) GetPassphraseFromStdin(addr common.Address) (pass string, err error) { - buf := client.BufferStdin() - prompt := fmt.Sprintf("Password to sign with '%s':", addr.Hex()) - return client.GetPassword(prompt, buf) -} diff --git a/client/context/query.go b/client/context/query.go deleted file mode 100644 index fa13449..0000000 --- a/client/context/query.go +++ /dev/null @@ -1,83 +0,0 @@ -package context - -import ( - "fmt" - - sdk "github.com/cosmos/cosmos-sdk/types" - - "github.com/pkg/errors" - - cmn "github.com/tendermint/tendermint/libs/common" - rpcclient "github.com/tendermint/tendermint/rpc/client" -) - -// GetNode returns an RPC client. If the context's client is not defined, an -// error is returned. -func (ctx ClientContext) GetNode() (rpcclient.Client, error) { - if ctx.Client == nil { - return nil, errors.New("no RPC client defined") - } - - return ctx.Client, nil -} - -// Query performs a query for information about the connected node. -func (ctx ClientContext) Query(path string, data cmn.HexBytes) (res []byte, err error) { - return ctx.query(path, data) -} - -// Query information about the connected node with a data payload -func (ctx ClientContext) QueryWithData(path string, data []byte) (res []byte, err error) { - return ctx.query(path, data) -} - -// QueryStore performs a query from a Tendermint node with the provided key and -// store name. -func (ctx ClientContext) QueryStore(key cmn.HexBytes, storeName string) (res []byte, err error) { - return ctx.queryStore(key, storeName, "key") -} - -// QuerySubspace performs a query from a Tendermint node with the provided -// store name and subspace. -func (ctx ClientContext) QuerySubspace(subspace []byte, storeName string) (res []sdk.KVPair, err error) { - resRaw, err := ctx.queryStore(subspace, storeName, "subspace") - if err != nil { - return res, err - } - - ctx.Codec.MustUnmarshalBinaryLengthPrefixed(resRaw, &res) - return -} - -// query performs a query from a Tendermint node with the provided store name -// and path. -func (ctx ClientContext) query(path string, key cmn.HexBytes) (res []byte, err error) { - node, err := ctx.GetNode() - if err != nil { - return res, err - } - - opts := rpcclient.ABCIQueryOptions{ - Height: ctx.Height, - Prove: !ctx.TrustNode, - } - - result, err := node.ABCIQueryWithOptions(path, key, opts) - if err != nil { - return res, err - } - - resp := result.Response - if !resp.IsOK() { - return res, errors.Errorf(resp.Log) - } - - return resp.Value, nil -} - -// queryStore performs a query from a Tendermint node with the provided a store -// name and path. -func (ctx ClientContext) queryStore(key cmn.HexBytes, storeName, endPath string) ([]byte, error) { - path := fmt.Sprintf("/store/%s/%s", storeName, endPath) - return ctx.query(path, key) -} diff --git a/client/context/types.go b/client/context/types.go deleted file mode 100644 index 31a2bc7..0000000 --- a/client/context/types.go +++ /dev/null @@ -1,81 +0,0 @@ -package context - -import ( - "github.com/FourthState/plasma-mvp-sidechain/x/utxo" - "github.com/cosmos/cosmos-sdk/codec" - tmlite "github.com/tendermint/tendermint/lite" - rpcclient "github.com/tendermint/tendermint/rpc/client" -) - -var ( - verifier tmlite.Verifier -) - -// redefine in utxo.go -type UTXODecoder func(utxoBytes []byte) (utxo.UTXO, error) - -type ClientContext struct { - Codec *codec.Codec - Height int64 - TrustNode bool - NodeURI string - InputAddresses string - Client rpcclient.Client - Decoder UTXODecoder - Verifier tmlite.Verifier - UTXOStore string - MetadataStore string -} - -// Returns a copy of the context with an updated height -func (c ClientContext) WithHeight(height int64) ClientContext { - c.Height = height - return c -} - -// Returns a copy of the context with an updated TrustNode flag -func (c ClientContext) WithTrustNode(trustNode bool) ClientContext { - c.TrustNode = trustNode - return c -} - -// Returns a copy of the xontext with an updated node URI -func (c ClientContext) WithNodeURI(nodeURI string) ClientContext { - c.NodeURI = nodeURI - c.Client = rpcclient.NewHTTP(nodeURI, "/websocket") - return c -} - -// Returns a copy of the context with an updated from address -func (c ClientContext) WithInputAddresses(inputAddresses string) ClientContext { - c.InputAddresses = inputAddresses - return c -} - -// Returns a copy of the context with an updated RPC client instance -func (c ClientContext) WithClient(client rpcclient.Client) ClientContext { - c.Client = client - return c -} - -// Returns a copy of the context with an updated utxo decoder -func (c ClientContext) WithDecoder(decoder UTXODecoder) ClientContext { - c.Decoder = decoder - return c -} - -// Returns a copy of the context with an updated UTXOStore -func (c ClientContext) WithUTXOStore(utxoStore string) ClientContext { - c.UTXOStore = utxoStore - return c -} - -func (c ClientContext) WithMetadataStore(metadataStore string) ClientContext { - c.MetadataStore = metadataStore - return c -} - -func (c ClientContext) WithCodec(cdc *codec.Codec) ClientContext { - c.Codec = cdc - return c -} diff --git a/client/context/viper.go b/client/context/viper.go deleted file mode 100644 index 5cb4899..0000000 --- a/client/context/viper.go +++ /dev/null @@ -1,30 +0,0 @@ -package context - -import ( - "github.com/spf13/viper" - - rpcclient "github.com/tendermint/tendermint/rpc/client" - - "github.com/FourthState/plasma-mvp-sidechain/app" - "github.com/FourthState/plasma-mvp-sidechain/client" -) - -// Return a new context with parameters from the command line -func NewClientContextFromViper() ClientContext { - nodeURI := viper.GetString(client.FlagNode) - var rpc rpcclient.Client - if nodeURI != "" { - rpc = rpcclient.NewHTTP(nodeURI, "/websocket") - } - return ClientContext{ - Height: viper.GetInt64(client.FlagHeight), - TrustNode: viper.GetBool(client.FlagTrustNode), - Codec: app.MakeCodec(), - InputAddresses: viper.GetString(client.FlagAddress), - NodeURI: nodeURI, - Client: rpc, - Decoder: nil, - UTXOStore: "main", - MetadataStore: "metadata", - } -} diff --git a/client/helpers.go b/client/helpers.go deleted file mode 100644 index d14fbcf..0000000 --- a/client/helpers.go +++ /dev/null @@ -1,151 +0,0 @@ -package client - -import ( - "bufio" - "fmt" - "os" - "strconv" - "strings" - - "github.com/FourthState/plasma-mvp-sidechain/types" - "github.com/bgentry/speakeasy" - "github.com/ethereum/go-ethereum/accounts/keystore" - "github.com/ethereum/go-ethereum/common" - isatty "github.com/mattn/go-isatty" - "github.com/pkg/errors" -) - -const ( - // Minimum acceptable password length - MinPassLength = 8 - - // Flags - FlagNode = "node" - FlagHeight = "height" - FlagTrustNode = "trust-node" - FlagAddress = "address" -) - -var ks *keystore.KeyStore - -// Allows for reading prompts for stdin -func BufferStdin() *bufio.Reader { - return bufio.NewReader(os.Stdin) -} - -// Build SpendMsg -func BuildMsg(inaddr0, inaddr1, addr0, addr1 common.Address, position0, position1 types.PlasmaPosition, confirmSigs0, confirmSigs1 [][65]byte, amount0, amount1, fee uint64) types.SpendMsg { - return types.SpendMsg{ - Blknum0: position0.Blknum, - Txindex0: position0.TxIndex, - Oindex0: position0.Oindex, - DepositNum0: position0.DepositNum, - Owner0: inaddr0, - Input0ConfirmSigs: confirmSigs0, - Blknum1: position1.Blknum, - Txindex1: position1.TxIndex, - Oindex1: position1.Oindex, - DepositNum1: position1.DepositNum, - Owner1: inaddr1, - Input1ConfirmSigs: confirmSigs1, - Newowner0: addr0, - Amount0: amount0, - Newowner1: addr1, - Amount1: amount1, - FeeAmount: fee, - } -} - -// initialize a keystore in the specified directory -func GetKeyStore(dir string) *keystore.KeyStore { - if ks == nil { - ks = keystore.NewKeyStore(dir, keystore.StandardScryptN, keystore.StandardScryptP) - } - return ks -} - -// Prompts for a password one-time -// Enforces minimum password length -func GetPassword(prompt string, buf *bufio.Reader) (pass string, err error) { - if inputIsTty() { - pass, err = speakeasy.Ask(prompt) - } else { - pass, err = readLineFromBuf(buf) - } - if err != nil { - return "", err - } - if len(pass) < MinPassLength { - return "", fmt.Errorf("Password must be at least %d characters", MinPassLength) - } - return pass, nil -} - -// Prompts for a password twice to verify they match -func GetCheckPassword(prompt, prompt1 string, buf *bufio.Reader) (string, error) { - if !inputIsTty() { - return GetPassword(prompt, buf) - } - - pass, err := GetPassword(prompt, buf) - if err != nil { - return "", err - } - pass1, err := GetPassword(prompt1, buf) - if err != nil { - return "", err - } - if pass != pass1 { - return "", errors.New("Passphrases did not match") - } - return pass, nil -} - -// value in a position defaults to 0 if not provided -func ParsePositions(posStr string) (position [2]types.PlasmaPosition, err error) { - for i, v := range strings.Split(posStr, "::") { - var pos [4]uint64 - for k, number := range strings.Split(v, ".") { - pos[k], err = strconv.ParseUint(strings.TrimSpace(number), 0, 64) - if err != nil { - return [2]types.PlasmaPosition{}, err - } - } - position[i] = types.NewPlasmaPosition(pos[0], uint16(pos[1]), uint8(pos[2]), uint64(pos[3])) - } - return position, nil -} - -// Amounts will default to 0 if not provided -func ParseAmounts(amtStr string) (amount [3]uint64, err error) { - for i, v := range strings.Split(amtStr, ",") { - amount[i], err = strconv.ParseUint(strings.TrimSpace(v), 0, 64) - if err != nil { - return [3]uint64{}, err - } - } - return amount, nil - -} - -// Convert string to Ethereum Address -func StrToAddress(addrStr string) (common.Address, error) { - if !common.IsHexAddress(strings.TrimSpace(addrStr)) { - return common.Address{}, errors.New("invalid address provided, please use hex format") - } - return common.HexToAddress(addrStr), nil -} - -// Returns true iff we have an interactive prompt -func inputIsTty() bool { - return isatty.IsTerminal(os.Stdin.Fd()) || isatty.IsCygwinTerminal(os.Stdin.Fd()) -} - -// reads one line from stdin -func readLineFromBuf(buf *bufio.Reader) (string, error) { - pass, err := buf.ReadString('\n') - if err != nil { - return "", err - } - return strings.TrimSpace(pass), nil -} diff --git a/client/plasmacli/cmd/add.go b/client/plasmacli/cmd/add.go deleted file mode 100644 index cfb8370..0000000 --- a/client/plasmacli/cmd/add.go +++ /dev/null @@ -1,41 +0,0 @@ -package cmd - -import ( - "fmt" - "github.com/FourthState/plasma-mvp-sidechain/client" - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -func init() { - rootCmd.AddCommand(addKeyCmd) -} - -var addKeyCmd = &cobra.Command{ - Use: "add", - Short: "Create a new account", - Long: `Add an encrypted account to the keystore.`, - RunE: func(cmd *cobra.Command, args []string) error { - - buf := client.BufferStdin() - - dir := viper.GetString(FlagHomeDir) - ks := client.GetKeyStore(dir) - - pass, err := client.GetCheckPassword("Enter a passphrase for your key:", "Repeat the passphrase:", buf) - if err != nil { - return err - } - - acc, err := ks.NewAccount(pass) - if err != nil { - return err - } - - fmt.Println("\n**Important** do not lose your passphrase.") - fmt.Println("It is the only way to recover your account") - fmt.Println("You should export this account and store it in a secure location") - fmt.Printf("Your new account address is: %s\n", acc.Address.Hex()) - return nil - }, -} diff --git a/client/plasmacli/cmd/balance.go b/client/plasmacli/cmd/balance.go deleted file mode 100644 index 53793eb..0000000 --- a/client/plasmacli/cmd/balance.go +++ /dev/null @@ -1,43 +0,0 @@ -package cmd - -import ( - "fmt" - "github.com/FourthState/plasma-mvp-sidechain/client" - "github.com/FourthState/plasma-mvp-sidechain/client/context" - "github.com/FourthState/plasma-mvp-sidechain/types" - "github.com/ethereum/go-ethereum/common" - "github.com/spf13/cobra" -) - -func init() { - rootCmd.AddCommand(balanceCmd) - balanceCmd.Flags().String(client.FlagNode, "tcp://localhost:26657", ": to tendermint rpc interface for this chain") - -} - -var balanceCmd = &cobra.Command{ - Use: "balance
", - Short: "Query Balances", - Long: "Query Balances", - RunE: func(cmd *cobra.Command, args []string) error { - ctx := context.NewClientContextFromViper() - - ethAddr := common.HexToAddress(args[0]) - - res, err2 := ctx.QuerySubspace(ethAddr.Bytes(), ctx.UTXOStore) - if err2 != nil { - return err2 - } - - for _, pair := range res { - var utxo types.BaseUTXO - err := ctx.Codec.UnmarshalBinaryBare(pair.Value, &utxo) - if err != nil { - return err - } - fmt.Printf("Position: %v \nAmount: %d \n", utxo.Position, utxo.Amount) - } - - return nil - }, -} diff --git a/client/plasmacli/cmd/confirmSig.go b/client/plasmacli/cmd/confirmSig.go deleted file mode 100644 index ff37203..0000000 --- a/client/plasmacli/cmd/confirmSig.go +++ /dev/null @@ -1,114 +0,0 @@ -package cmd - -import ( - "encoding/binary" - "errors" - "fmt" - - "github.com/spf13/cobra" - "github.com/spf13/viper" - - "github.com/FourthState/plasma-mvp-sidechain/client" - "github.com/FourthState/plasma-mvp-sidechain/client/context" - "github.com/FourthState/plasma-mvp-sidechain/types" - "github.com/FourthState/plasma-mvp-sidechain/utils" - "github.com/ethereum/go-ethereum/accounts" - "github.com/ethereum/go-ethereum/common" - "github.com/tendermint/tendermint/crypto/tmhash" -) - -const ( - flagFrom = "from" // address to sign with - flagOwner = "owner" // address that owns the utxo being queried for -) - -func init() { - rootCmd.AddCommand(signCmd) - signCmd.Flags().String(flagFrom, "", "Address used to sign the confirmation signature") - signCmd.Flags().String(flagOwner, "", "Owner of the newly created utxo") - viper.BindPFlags(signCmd.Flags()) -} - -var signCmd = &cobra.Command{ - Use: "sign ", - Short: "Sign confirmation signatures for position provided (0.0.0.0), if it exists.", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - - ctx := context.NewClientContextFromViper() - - position, err := client.ParsePositions(args[0]) - if err != nil { - return err - } - - fromStr := viper.GetString(flagFrom) - if fromStr == "" { - return errors.New("must provide the address to sign with using the --from flag") - } - - ownerStr := viper.GetString(flagOwner) - if ownerStr == "" { - return fmt.Errorf("must provide the address that owns position %v using the --owner flag", position) - } - - ethAddr := common.HexToAddress(ownerStr) - signerAddr := common.HexToAddress(fromStr) - - posBytes, err := ctx.Codec.MarshalBinaryBare(position[0]) - if err != nil { - return err - } - - key := append(ethAddr.Bytes(), posBytes...) - res, err := ctx.QueryStore(key, ctx.UTXOStore) - if err != nil { - return err - } - - var utxo types.BaseUTXO - err = ctx.Codec.UnmarshalBinaryBare(res, &utxo) - - blknumKey := make([]byte, binary.MaxVarintLen64) - binary.PutUvarint(blknumKey, utxo.GetPosition().Get()[0].Uint64()) - - blockhash, err := ctx.QueryStore(blknumKey, ctx.MetadataStore) - if err != nil { - return err - } - - hash := tmhash.Sum(append(utxo.TxHash, blockhash...)) - signHash := utils.SignHash(hash) - - dir := viper.GetString(FlagHomeDir) - ks := client.GetKeyStore(dir) - - acc := accounts.Account{ - Address: signerAddr, - } - // get account to sign with - acct, err := ks.Find(acc) - if err != nil { - return err - } - - // get passphrase - passphrase, err := ctx.GetPassphraseFromStdin(signerAddr) - if err != nil { - return err - } - - sig, err := ks.SignHashWithPassphrase(acct, passphrase, signHash) - - fmt.Printf("\nConfirmation Signature for utxo with\nposition: %v \namount: %d\n", utxo.Position, utxo.Amount) - fmt.Printf("signature: %x\n", sig) - - inputLen := 1 - // check number of inputs - if !utils.ZeroAddress(utxo.InputAddresses[1]) { - inputLen = 2 - } - fmt.Printf("UTXO had %d inputs\n", inputLen) - return nil - }, -} diff --git a/client/plasmacli/cmd/delete.go b/client/plasmacli/cmd/delete.go deleted file mode 100644 index 9033fd0..0000000 --- a/client/plasmacli/cmd/delete.go +++ /dev/null @@ -1,56 +0,0 @@ -package cmd - -import ( - "errors" - "fmt" - "github.com/FourthState/plasma-mvp-sidechain/client" - "github.com/ethereum/go-ethereum/accounts" - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -func init() { - rootCmd.AddCommand(deleteKeyCmd) -} - -var deleteKeyCmd = &cobra.Command{ - Use: "delete
", - Short: "Delete the given address", - Long: `Deletes the account from the keystore matching the address provided, if the passphrase - is correct.`, - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - addrStr := args[0] - addr, err := client.StrToAddress(addrStr) - if err != nil { - return err - } - - buf := client.BufferStdin() - pass, err := client.GetPassword("DANGER - enter passphrase to permanetly delete key:", buf) - if err != nil { - return err - } - - dir := viper.GetString(FlagHomeDir) - ks := client.GetKeyStore(dir) - if err != nil { - return err - } - - if !ks.HasAddress(addr) { - return errors.New("the account trying to be deleted does not exist") - } - - acc := accounts.Account{ - Address: addr, - } - - err = ks.Delete(acc, pass) - if err != nil { - return err - } - fmt.Println("Account deleted forever") - return nil - }, -} diff --git a/client/plasmacli/cmd/export.go b/client/plasmacli/cmd/export.go deleted file mode 100644 index 1d619dd..0000000 --- a/client/plasmacli/cmd/export.go +++ /dev/null @@ -1 +0,0 @@ -package cmd diff --git a/client/plasmacli/cmd/find.go b/client/plasmacli/cmd/find.go deleted file mode 100644 index 5e06a63..0000000 --- a/client/plasmacli/cmd/find.go +++ /dev/null @@ -1,40 +0,0 @@ -package cmd - -import ( - "fmt" - "github.com/FourthState/plasma-mvp-sidechain/client" - "github.com/ethereum/go-ethereum/accounts" - - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -func init() { - rootCmd.AddCommand(showKeysCmd) -} - -var showKeysCmd = &cobra.Command{ - Use: "find
", - Short: "Find the account for the given address", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - addrStr := args[0] - addr, err := client.StrToAddress(addrStr) - if err != nil { - return err - } - acct := accounts.Account{ - Address: addr, - } - - dir := viper.GetString(FlagHomeDir) - ks := client.GetKeyStore(dir) - acc, err := ks.Find(acct) - if err != nil { - return err - } - fmt.Println("Your account has been found!") - fmt.Printf("Account Address: %s\nAccount Location:%s\n", acc.Address.Hex(), acc.URL.String()) - return nil - }, -} diff --git a/client/plasmacli/cmd/import.go b/client/plasmacli/cmd/import.go deleted file mode 100644 index c25c013..0000000 --- a/client/plasmacli/cmd/import.go +++ /dev/null @@ -1,60 +0,0 @@ -package cmd - -import ( - "errors" - "fmt" - "github.com/FourthState/plasma-mvp-sidechain/client" - "github.com/ethereum/go-ethereum/crypto" - "github.com/spf13/viper" - - "github.com/spf13/cobra" -) - -func init() { - rootCmd.AddCommand(importCmd) -} - -var importCmd = &cobra.Command{ - Use: "import ", - Short: "Import a private key into a new account on the sidechain", - Long: ` -plasmacli import - -Imports an unencrypted private key from and creates a new account on the sidechain. -Prints the address. - -The keyfile is assumed to contain an unencrypted private key in hexadecimal format. - -The account is saved in encrypted format, you are prompted for a passphrase. - -You must remember this passphrase to unlock your account in the future. -`, - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - keyfile := args[0] - if len(keyfile) == 0 { - return errors.New("keyfile must be given as argument") - } - key, err := crypto.LoadECDSA(keyfile) - if err != nil { - return err - } - - buf := client.BufferStdin() - passphrase, err := client.GetCheckPassword("Please set a passphrase for your imported account.\nPassphrase:", "Repeat the passphrase:", buf) - if err != nil { - return err - } - - dir := viper.GetString(FlagHomeDir) - ks := client.GetKeyStore(dir) - - acct, err := ks.ImportECDSA(key, passphrase) - if err != nil { - return err - } - - fmt.Printf("Address: {%x}\n", acct.Address) - return nil - }, -} diff --git a/client/plasmacli/cmd/info.go b/client/plasmacli/cmd/info.go deleted file mode 100644 index bc3c43f..0000000 --- a/client/plasmacli/cmd/info.go +++ /dev/null @@ -1,53 +0,0 @@ -package cmd - -import ( - "fmt" - "github.com/FourthState/plasma-mvp-sidechain/client" - "github.com/FourthState/plasma-mvp-sidechain/client/context" - "github.com/FourthState/plasma-mvp-sidechain/types" - "github.com/FourthState/plasma-mvp-sidechain/utils" - "github.com/ethereum/go-ethereum/common" - "github.com/spf13/cobra" -) - -func init() { - rootCmd.AddCommand(infoCmd) - infoCmd.Flags().String(client.FlagNode, "tcp://localhost:26657", ": to tendermint rpc interface for this chain") - -} - -var infoCmd = &cobra.Command{ - Use: "info
", - Short: "Information on owned utxos ", - RunE: func(cmd *cobra.Command, args []string) error { - ctx := context.NewClientContextFromViper() - - ethAddr := common.HexToAddress(args[0]) - - res, err2 := ctx.QuerySubspace(ethAddr.Bytes(), ctx.UTXOStore) - if err2 != nil { - return err2 - } - - for _, pair := range res { - var utxo types.BaseUTXO - err := ctx.Codec.UnmarshalBinaryBare(pair.Value, &utxo) - if err != nil { - return err - } - fmt.Printf("\nPosition: %v \nAmount: %d \nDenomination: %s \n", utxo.Position, utxo.Amount, utxo.GetDenom()) - inputAddrHelper(utxo) - } - - return nil - }, -} - -func inputAddrHelper(utxo types.BaseUTXO) { - if utxo.Position.DepositNum == 0 { - fmt.Printf("First Input Address: %s \n", utxo.InputAddresses[0].Hex()) - if !utils.ZeroAddress(utxo.InputAddresses[1]) { - fmt.Printf("Second Input Address: %s \n", utxo.InputAddresses[1].Hex()) - } - } -} diff --git a/client/plasmacli/cmd/list.go b/client/plasmacli/cmd/list.go deleted file mode 100644 index e1e8977..0000000 --- a/client/plasmacli/cmd/list.go +++ /dev/null @@ -1,30 +0,0 @@ -package cmd - -import ( - "fmt" - "github.com/FourthState/plasma-mvp-sidechain/client" - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -func init() { - rootCmd.AddCommand(listKeysCmd) -} - -var listKeysCmd = &cobra.Command{ - Use: "list", - Short: "List all accounts", - Long: "Return a list of all account addresses stored by this keystore", - RunE: func(cmd *cobra.Command, args []string) error { - - dir := viper.GetString(FlagHomeDir) - ks := client.GetKeyStore(dir) - - fmt.Println("Index:\t\tAddress:") - accounts := ks.Accounts() - for i, acc := range accounts { - fmt.Printf("%d\t\t%s\n", i, acc.Address.Hex()) - } - return nil - }, -} diff --git a/client/plasmacli/cmd/prove.go b/client/plasmacli/cmd/prove.go deleted file mode 100644 index a022b31..0000000 --- a/client/plasmacli/cmd/prove.go +++ /dev/null @@ -1,74 +0,0 @@ -package cmd - -import ( - "encoding/hex" - "fmt" - "github.com/FourthState/plasma-mvp-sidechain/client" - "github.com/FourthState/plasma-mvp-sidechain/client/context" - "github.com/FourthState/plasma-mvp-sidechain/types" - "github.com/ethereum/go-ethereum/common" - "github.com/spf13/cobra" -) - -func init() { - rootCmd.AddCommand(proveCmd) - proveCmd.Flags().String(client.FlagNode, "tcp://localhost:26657", ": to tendermint rpc interface for this chain") - -} - -var proveCmd = &cobra.Command{ - Use: "prove", - Short: "Prove tx inclusion: prove
", - Long: "Returns proof for tx inclusion. Use to exit tx on rootchain", - RunE: func(cmd *cobra.Command, args []string) error { - ctx := context.NewClientContextFromViper() - - ethAddr := common.HexToAddress(args[0]) - position, err := client.ParsePositions(args[1]) - if err != nil { - return err - } - - posBytes, err := ctx.Codec.MarshalBinaryBare(position[0]) - if err != nil { - return err - } - - key := append(ethAddr.Bytes(), posBytes...) - - res, err2 := ctx.QueryStore(key, ctx.UTXOStore) - if err2 != nil { - return err2 - } - - var utxo types.BaseUTXO - err = ctx.Codec.UnmarshalBinaryBare(res, &utxo) - if err != nil { - return err - } - - result, err := ctx.Client.Tx(utxo.TxHash, true) - if err != nil { - return err - } - - fmt.Printf("Roothash: 0x%s\n", hex.EncodeToString(result.Proof.RootHash)) - fmt.Printf("Total: %d\n", result.Proof.Proof.Total) - fmt.Printf("LeafHash: 0x%s\n", hex.EncodeToString(result.Proof.Proof.LeafHash)) - fmt.Printf("TxBytes: 0x%s\n", hex.EncodeToString(result.Tx)) - - // flatten aunts - var proof []byte - for _, aunt := range result.Proof.Proof.Aunts { - proof = append(proof, aunt...) - } - - if len(proof) == 0 { - fmt.Println("Proof: nil") - } else { - fmt.Printf("Proof: 0x%s\n", hex.EncodeToString(proof)) - } - - return nil - }, -} diff --git a/client/plasmacli/cmd/root.go b/client/plasmacli/cmd/root.go deleted file mode 100644 index 2780448..0000000 --- a/client/plasmacli/cmd/root.go +++ /dev/null @@ -1,40 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -var homeDir string = os.ExpandEnv("$HOME/.plasmacli/keys") - -const ( - FlagHomeDir = "home" -) - -var rootCmd = &cobra.Command{ - Use: "plasmacli", - Short: "Plasma Client", -} - -func Execute() { - if err := rootCmd.Execute(); err != nil { - fmt.Println(err) - os.Exit(1) - } -} - -func init() { - // initConfig to be ran when Execute is called - cobra.OnInitialize(initConfig) - rootCmd.PersistentFlags().StringP(FlagHomeDir, "", homeDir, "directory for keystore") - viper.BindPFlags(rootCmd.Flags()) - viper.Set(FlagHomeDir, homeDir) -} - -// initConfig reads in config file and ENV variables if set -func initConfig() { - viper.AutomaticEnv() -} diff --git a/client/plasmacli/cmd/sendtx.go b/client/plasmacli/cmd/sendtx.go deleted file mode 100644 index 300f4f4..0000000 --- a/client/plasmacli/cmd/sendtx.go +++ /dev/null @@ -1,142 +0,0 @@ -package cmd - -import ( - "encoding/hex" - "errors" - "fmt" - - "github.com/FourthState/plasma-mvp-sidechain/client" - "github.com/FourthState/plasma-mvp-sidechain/client/context" - "github.com/FourthState/plasma-mvp-sidechain/utils" - "github.com/ethereum/go-ethereum/common" - "strings" - - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -const ( - flagTo = "to" - flagPositions = "position" - flagConfirmSigs0 = "Input0ConfirmSigs" - flagConfirmSigs1 = "Input1ConfirmSigs" - flagAmounts = "amounts" -) - -func init() { - rootCmd.AddCommand(sendTxCmd) - sendTxCmd.Flags().String(flagTo, "", "Addresses sending to (separated by commas)") - // Format for positions can be adjusted - sendTxCmd.Flags().String(flagPositions, "", "UTXO Positions to be spent, format: blknum0.txindex0.oindex0.depositnonce0::blknum1.txindex1.oindex1.depositnonce1") - - sendTxCmd.Flags().String(flagConfirmSigs0, "", "Input Confirmation Signatures for first input to be spent (separated by commas)") - sendTxCmd.Flags().String(flagConfirmSigs1, "", "Input Confirmation Signatures for second input to be spent (separated by commas)") - - sendTxCmd.Flags().String(flagAmounts, "", "Amounts to be spent, format: amount1, amount2, fee") - - sendTxCmd.Flags().String(client.FlagNode, "tcp://localhost:26657", ": to tendermint rpc interface for this chain") - sendTxCmd.Flags().String(client.FlagAddress, "", "Address to sign with") - viper.BindPFlags(sendTxCmd.Flags()) -} - -var sendTxCmd = &cobra.Command{ - Use: "send", - Short: "Build, Sign, and Send transactions", - RunE: func(cmd *cobra.Command, args []string) error { - ctx := context.NewClientContextFromViper() - - // get the directory for our keystore - dir := viper.GetString(FlagHomeDir) - - // get the from/to address - from, err := ctx.GetInputAddresses(dir) - if err != nil { - return err - } - toStr := viper.GetString(flagTo) - if toStr == "" { - return errors.New("must provide an address to send to") - } - - toAddrs := strings.Split(toStr, ",") - if len(toAddrs) > 2 { - return errors.New("incorrect amount of addresses provided") - } - - var addr1, addr2 common.Address - addr1, err = client.StrToAddress(toAddrs[0]) - if err != nil { - return err - } - if len(toAddrs) > 1 && toAddrs[1] != "" { - addr2, err = client.StrToAddress(toAddrs[1]) - if err != nil { - return err - } - } - - csStr := viper.GetString(flagConfirmSigs0) - cs0 := strings.Split(csStr, ",") - csStr = viper.GetString(flagConfirmSigs1) - cs1 := strings.Split(csStr, ",") - - confirmSigs0, err := getConfirmSigs(cs0) - if err != nil { - return err - } - confirmSigs1, err := getConfirmSigs(cs1) - if err != nil { - return err - } - - // Get positions for transaction inputs - posStr := viper.GetString(flagPositions) - position, err := client.ParsePositions(posStr) - if err != nil { - return err - } - - // Get amounts and fee - amtStr := viper.GetString(flagAmounts) - amounts, err := client.ParseAmounts(amtStr) - if utils.ZeroAddress(addr2) && amounts[1] != 0 { - return fmt.Errorf("You are trying to send %d amount to the nil address. Please input the zero address if you would like to burn your amount", amounts[1]) - } - msg := client.BuildMsg(from[0], from[1], addr1, addr2, position[0], position[1], confirmSigs0, confirmSigs1, amounts[0], amounts[1], amounts[2]) - res, err := ctx.SignBuildBroadcast(from, msg, dir) - if err != nil { - return err - } - fmt.Printf("Committed at block %d. Hash %s\n", res.Height, res.Hash.String()) - return nil - }, -} - -func getConfirmSigs(sigs []string) (confirmSigs [][65]byte, err error) { - var cs0, cs1 []byte - var confirmSig0, confirmSig1 [65]byte - - if strings.Compare(sigs[0], "") == 0 { - return confirmSigs, nil - } - switch len(sigs) { - case 1: - if cs0, err = hex.DecodeString(strings.TrimSpace(sigs[0])); err != nil { - return confirmSigs, err - } - copy(confirmSig0[:], cs0) - return append(confirmSigs, confirmSig0), nil - case 2: - if cs0, err = hex.DecodeString(strings.TrimSpace(sigs[0])); err != nil { - return confirmSigs, err - } - if cs1, err = hex.DecodeString(strings.TrimSpace(sigs[1])); err != nil { - return confirmSigs, err - } - copy(confirmSig0[:], cs0) - copy(confirmSig1[:], cs1) - - return append(confirmSigs, confirmSig0, confirmSig1), nil - } - return confirmSigs, errors.New("the provided confirmSigs caused undefined behavior. Pass in 0, 1 or 2 confirm sigs per flag") -} diff --git a/client/plasmacli/cmd/update.go b/client/plasmacli/cmd/update.go deleted file mode 100644 index cf44f8c..0000000 --- a/client/plasmacli/cmd/update.go +++ /dev/null @@ -1,50 +0,0 @@ -package cmd - -import ( - "fmt" - "github.com/FourthState/plasma-mvp-sidechain/client" - "github.com/ethereum/go-ethereum/accounts" - - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -func init() { - rootCmd.AddCommand(updateKeysCmd) -} - -var updateKeysCmd = &cobra.Command{ - Use: "update
", - Short: "Change the password used to protect the account", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - addrStr := args[0] - addr, err := client.StrToAddress(addrStr) - if err != nil { - return err - } - - buf := client.BufferStdin() - oldpass, err := client.GetPassword("Enter the current passphrase:", buf) - if err != nil { - return err - } - newpass, err := client.GetCheckPassword("Enter the new passphrase:", "Repeat the new passphrase:", buf) - if err != nil { - return err - } - - dir := viper.GetString(FlagHomeDir) - ks := client.GetKeyStore(dir) - - acc := accounts.Account{ - Address: addr, - } - err = ks.Update(acc, oldpass, newpass) - if err != nil { - return err - } - fmt.Println("Password successfully updated!") - return nil - }, -} diff --git a/client/plasmacli/main.go b/client/plasmacli/main.go deleted file mode 100644 index 0a8f708..0000000 --- a/client/plasmacli/main.go +++ /dev/null @@ -1,9 +0,0 @@ -package main - -import ( - "github.com/FourthState/plasma-mvp-sidechain/client/plasmacli/cmd" -) - -func main() { - cmd.Execute() -} diff --git a/client/plasmad/cmd/init.go b/client/plasmad/cmd/init.go deleted file mode 100644 index d790e8f..0000000 --- a/client/plasmad/cmd/init.go +++ /dev/null @@ -1,96 +0,0 @@ -package cmd - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - - "github.com/FourthState/plasma-mvp-sidechain/app" - - "github.com/cosmos/cosmos-sdk/client" - "github.com/cosmos/cosmos-sdk/codec" - "github.com/cosmos/cosmos-sdk/server" - "github.com/spf13/cobra" - "github.com/spf13/viper" - cfg "github.com/tendermint/tendermint/config" - "github.com/tendermint/tendermint/libs/cli" - "github.com/tendermint/tendermint/libs/common" -) - -const ( - flagOverwrite = "overwrite" - flagClientHome = "home-client" - flagMoniker = "moniker" -) - -type printInfo struct { - Moniker string `json:"moniker"` - ChainID string `json:"chain_id"` - NodeID string `json:"node_id"` - AppMessage json.RawMessage `json:"app_message"` -} - -// nolint: errcheck -func displayInfo(cdc *codec.Codec, info printInfo) error { - out, err := codec.MarshalJSONIndent(cdc, info) - if err != nil { - return err - } - fmt.Fprintf(os.Stderr, "%s\n", string(out)) - return nil -} - -// get cmd to initialize all files for tendermint and application -// nolint -func InitCmd(ctx *server.Context, cdc *codec.Codec, appInit server.AppInit) *cobra.Command { - cmd := &cobra.Command{ - Use: "init", - Short: "Initialize private validator, p2p, genesis, and application configuration files", - Long: `Initialize validators's and node's configuration files.`, - Args: cobra.NoArgs, - RunE: func(_ *cobra.Command, _ []string) error { - config := ctx.Config - config.SetRoot(viper.GetString(cli.HomeFlag)) - chainID := viper.GetString(client.FlagChainID) - if chainID == "" { - chainID = fmt.Sprintf("test-chain-%v", common.RandStr(6)) - } - nodeID, _, err := InitializeNodeValidatorFiles(config) - if err != nil { - return err - } - - if viper.GetString(flagMoniker) != "" { - config.Moniker = viper.GetString(flagMoniker) - } - - var appState json.RawMessage - genFile := config.GenesisFile() - if appState, err = initializeEmptyGenesis(cdc, genFile, chainID, - viper.GetBool(flagOverwrite)); err != nil { - return err - } - if err = ExportGenesisFile(genFile, chainID, nil, appState); err != nil { - return err - } - - toPrint := printInfo{ - ChainID: chainID, - Moniker: config.Moniker, - NodeID: nodeID, - AppMessage: appState, - } - - cfg.WriteConfigFile(filepath.Join(config.RootDir, "config", "config.toml"), config) - - return displayInfo(cdc, toPrint) - }, - } - - cmd.Flags().String(cli.HomeFlag, app.DefaultNodeHome, "node's home directory") - cmd.Flags().BoolP(flagOverwrite, "o", false, "overwrite the genesis.json file") - cmd.Flags().String(client.FlagChainID, "", "genesis file chain-id, if left blank will be randomly created") - cmd.Flags().String(flagMoniker, "", "set the validator's moniker") - return cmd -} diff --git a/client/plasmad/cmd/utils.go b/client/plasmad/cmd/utils.go deleted file mode 100644 index 652df6e..0000000 --- a/client/plasmad/cmd/utils.go +++ /dev/null @@ -1,113 +0,0 @@ -package cmd - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "time" - - "github.com/FourthState/plasma-mvp-sidechain/app" - - "github.com/cosmos/cosmos-sdk/codec" - amino "github.com/tendermint/go-amino" - cfg "github.com/tendermint/tendermint/config" - "github.com/tendermint/tendermint/crypto" - "github.com/tendermint/tendermint/libs/common" - "github.com/tendermint/tendermint/p2p" - "github.com/tendermint/tendermint/privval" - "github.com/tendermint/tendermint/types" -) - -// ExportGenesisFile creates and writes the genesis configuration to disk. An -// error is returned if building or writing the configuration to file fails. -func ExportGenesisFile( - genFile, chainID string, validators []types.GenesisValidator, appState json.RawMessage, -) error { - - genDoc := types.GenesisDoc{ - ChainID: chainID, - Validators: validators, - AppState: appState, - } - - if err := genDoc.ValidateAndComplete(); err != nil { - return err - } - - return genDoc.SaveAs(genFile) -} - -// ExportGenesisFileWithTime creates and writes the genesis configuration to disk. -// An error is returned if building or writing the configuration to file fails. -func ExportGenesisFileWithTime( - genFile, chainID string, validators []types.GenesisValidator, - appState json.RawMessage, genTime time.Time, -) error { - - genDoc := types.GenesisDoc{ - GenesisTime: genTime, - ChainID: chainID, - Validators: validators, - AppState: appState, - } - - if err := genDoc.ValidateAndComplete(); err != nil { - return err - } - - return genDoc.SaveAs(genFile) -} - -// read of create the private key file for this config -func ReadOrCreatePrivValidator(privValFile string) crypto.PubKey { - var privValidator *privval.FilePV - - if common.FileExists(privValFile) { - privValidator = privval.LoadFilePV(privValFile) - } else { - privValidator = privval.GenFilePV(privValFile) - privValidator.Save() - } - - return privValidator.GetPubKey() -} - -// InitializeNodeValidatorFiles creates private validator and p2p configuration files. -func InitializeNodeValidatorFiles( - config *cfg.Config) (nodeID string, valPubKey crypto.PubKey, err error, -) { - - nodeKey, err := p2p.LoadOrGenNodeKey(config.NodeKeyFile()) - if err != nil { - return nodeID, valPubKey, err - } - - nodeID = string(nodeKey.ID()) - valPubKey = ReadOrCreatePrivValidator(config.PrivValidatorFile()) - - return nodeID, valPubKey, nil -} - -func loadGenesisDoc(cdc *amino.Codec, genFile string) (genDoc types.GenesisDoc, err error) { - genContents, err := ioutil.ReadFile(genFile) - if err != nil { - return genDoc, err - } - - if err := cdc.UnmarshalJSON(genContents, &genDoc); err != nil { - return genDoc, err - } - - return genDoc, err -} - -func initializeEmptyGenesis( - cdc *codec.Codec, genFile, chainID string, overwrite bool, -) (appState json.RawMessage, err error) { - - if !overwrite && common.FileExists(genFile) { - return nil, fmt.Errorf("genesis.json file already exists: %v", genFile) - } - - return codec.MarshalJSONIndent(cdc, app.NewDefaultGenesisState()) -} diff --git a/client/plasmad/main.go b/client/plasmad/main.go deleted file mode 100644 index df8e932..0000000 --- a/client/plasmad/main.go +++ /dev/null @@ -1,50 +0,0 @@ -package main - -import ( - "encoding/json" - "io" - "os" - - "github.com/spf13/cobra" - - "github.com/cosmos/cosmos-sdk/server" - abci "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/libs/cli" - dbm "github.com/tendermint/tendermint/libs/db" - "github.com/tendermint/tendermint/libs/log" - tmtypes "github.com/tendermint/tendermint/types" - - "github.com/FourthState/plasma-mvp-sidechain/app" - "github.com/FourthState/plasma-mvp-sidechain/client/plasmad/cmd" -) - -func main() { - cdc := app.MakeCodec() - ctx := server.NewDefaultContext() - - rootCmd := &cobra.Command{ - Use: "plasmad", - Short: "Plasma Daemon (server)", - PersistentPreRunE: server.PersistentPreRunEFn(ctx), - } - rootCmd.AddCommand(cmd.InitCmd(ctx, cdc, app.PlasmaAppInit())) - - server.AddCommands(ctx, cdc, rootCmd, app.PlasmaAppInit(), - newApp, - exportAppState) - - // prepare and add flags - rootDir := os.ExpandEnv("$HOME/.plasmad") - executor := cli.PrepareBaseCmd(rootCmd, "PC", rootDir) - executor.Execute() -} - -func newApp(logger log.Logger, db dbm.DB, traceStore io.Writer) abci.Application { - return app.NewChildChain(logger, db, traceStore) -} - -// non-functional -func exportAppState(logger log.Logger, db dbm.DB, traceStore io.Writer) (json.RawMessage, []tmtypes.GenesisValidator, error) { - papp := app.NewChildChain(logger, db, traceStore) - return papp.ExportAppStateJSON() -} diff --git a/client/query.go b/client/query.go new file mode 100644 index 0000000..2a27419 --- /dev/null +++ b/client/query.go @@ -0,0 +1,156 @@ +package client + +import ( + "encoding/json" + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/plasma" + "github.com/FourthState/plasma-mvp-sidechain/store" + "github.com/cosmos/cosmos-sdk/client/context" + ethcmn "github.com/ethereum/go-ethereum/common" + "math/big" +) + +// TxOutput retrieves the output located at `pos` and contextual transaction information +func TxOutput(ctx context.CLIContext, pos plasma.Position) (store.TxOutput, error) { + queryRoute := fmt.Sprintf("custom/%s/%s/%s", + store.QuerierRouteName, store.QueryTxOutput, pos) + data, err := ctx.Query(queryRoute, nil) + if err != nil { + return store.TxOutput{}, err + } + + var output store.TxOutput + if err := json.Unmarshal(data, &output); err != nil { + return store.TxOutput{}, fmt.Errorf("json: %s", err) + } + + return output, nil +} + +// TxInput retrieves the tx hash and the inputs that created the output located at `pos` +func TxInput(ctx context.CLIContext, pos plasma.Position) (store.TxInput, error) { + queryRoute := fmt.Sprintf("custom/%s/%s/%s", + store.QuerierRouteName, store.QueryTxInput, pos) + data, err := ctx.Query(queryRoute, nil) + if err != nil { + return store.TxInput{}, err + } + + var input store.TxInput + if err := json.Unmarshal(data, &input); err != nil { + return store.TxInput{}, fmt.Errorf("json: %s", err) + } + + return input, nil +} + +// Tx locates a transaction and given it's hash +// @param hash 32-byte hexadecimal string +func Tx(ctx context.CLIContext, hash []byte) (store.Transaction, error) { + queryRoute := fmt.Sprintf("custom/%s/%s/%x", + store.QuerierRouteName, store.QueryTx, hash) + data, err := ctx.Query(queryRoute, nil) + if err != nil { + return store.Transaction{}, err + } + + var tx store.Transaction + if err := json.Unmarshal(data, &tx); err != nil { + return store.Transaction{}, fmt.Errorf("json: %s", err) + } + + return tx, nil +} + +// Info retrieves the unspent utxo set of an owned address +func Info(ctx context.CLIContext, addr ethcmn.Address) ([]store.TxOutput, error) { + queryRoute := fmt.Sprintf("custom/%s/%s/%s", + store.QuerierRouteName, store.QueryInfo, addr.Hex()) + data, err := ctx.Query(queryRoute, nil) + if err != nil { + return nil, err + } + + var utxos []store.TxOutput + if err := json.Unmarshal(data, &utxos); err != nil { + return nil, fmt.Errorf("json: %s", err) + } + + return utxos, nil +} + +// Balance retrieves the aggregate value across unspent utxos of an address +func Balance(ctx context.CLIContext, addr ethcmn.Address) (string, error) { + queryRoute := fmt.Sprintf("custom/%s/%s/%s", + store.QuerierRouteName, store.QueryBalance, addr.Hex()) + data, err := ctx.Query(queryRoute, nil) + if err != nil { + return "", err + } + + // balance returned in string format + return string(data), err +} + +// Height retrieves the current plasma block height +func Height(ctx context.CLIContext) (string, error) { + queryRoute := fmt.Sprintf("custom/%s/%s", + store.QuerierRouteName, store.QueryHeight) + data, err := ctx.Query(queryRoute, nil) + if err != nil { + return "", err + } + + // data returned in stirng format + return string(data), nil +} + +// Block retrieves block information specified at `height` +func Block(ctx context.CLIContext, height *big.Int) (store.Block, error) { + if height == nil || height.Sign() <= 0 { + return store.Block{}, fmt.Errorf("block numbering starts at 1") + } + + queryPath := fmt.Sprintf("custom/%s/%s/%s", + store.QuerierRouteName, store.QueryBlock, height) + data, err := ctx.Query(queryPath, nil) + if err != nil { + return store.Block{}, err + } + + var block store.Block + if err := json.Unmarshal(data, &block); err != nil { + return block, fmt.Errorf("json: %s", err) + } + + return block, nil +} + +// Blocks retrieves 10 blocks from `startingHeight`. if `startingHeight == nil`, the latest 10 are retrieved +func Blocks(ctx context.CLIContext, startingHeight *big.Int) ([]store.Block, error) { + if startingHeight != nil && startingHeight.Sign() <= 0 { + return nil, fmt.Errorf("block height starts at 1") + } + + var queryPath string + if startingHeight == nil { + queryPath = "latest" + } else { + queryPath = startingHeight.String() + } + + // add prefix + queryPath = fmt.Sprintf("custom/%s/%s/%s", + store.QuerierRouteName, store.QueryBlocks, queryPath) + data, err := ctx.Query(queryPath, nil) + if err != nil { + return nil, err + } + + var blocks []store.Block + if err = json.Unmarshal(data, &blocks); err != nil { + return nil, fmt.Errorf("json: %s", err) + } + + return blocks, nil +} diff --git a/client/rest.go b/client/rest.go new file mode 100644 index 0000000..3c224a4 --- /dev/null +++ b/client/rest.go @@ -0,0 +1,287 @@ +package client + +import ( + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/plasma" + "github.com/FourthState/plasma-mvp-sidechain/store" + "github.com/FourthState/plasma-mvp-sidechain/utils" + "github.com/cosmos/cosmos-sdk/client/context" + sdk "github.com/cosmos/cosmos-sdk/types" + ethcmn "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rlp" + "github.com/gorilla/mux" + "math/big" + "net/http" +) + +func RegisterRoutes(ctx context.CLIContext, r *mux.Router) { + // Getters + r.HandleFunc("/height", heightHandler(ctx)).Methods("GET") + r.HandleFunc("/block/{height}", blockHandler(ctx)).Methods("GET") + r.HandleFunc("/blocks/{height}", blocksHandler(ctx)).Methods("GET") + + r.HandleFunc("/info/{address}", infoHandler(ctx)).Methods("GET") + r.HandleFunc("/balance/{address}", balanceHandler(ctx)).Methods("GET") + + r.HandleFunc("/tx/{hash}", txHandler(ctx)).Methods("GET") + r.HandleFunc("/output/{position}", outputHandler(ctx)).Methods("GET") + + // Post + r.HandleFunc("/submit", submitHandler(ctx)).Methods("POST") +} + +func heightHandler(ctx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + height, err := Height(ctx) + if err != nil { + writeClientRetrievalErr(w, err) + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte(height)) + } +} + +func blockHandler(ctx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + num, ok := new(big.Int).SetString(mux.Vars(r)["height"], 10) + if !ok || num.Sign() <= 0 { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("block height must be in decimal format starting from 1")) + return + } + + block, err := Block(ctx, num) + if err != nil { + writeClientRetrievalErr(w, err) + return + } + + resp := struct { + store.Block + Txs [][]byte + }{} + + // Query the tendermint block. + // Tendermint stores transactions in base64 format + // + // Transcode base64 to hex + height := int64(block.TMBlockHeight) + tmBlock, err := ctx.Client.Block(&height) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + + for _, tx := range tmBlock.Block.Data.Txs { + hexFormat, err := base64.StdEncoding.DecodeString(string(tx)) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("error with transaction transcoding")) + return + } + + resp.Txs = append(resp.Txs, hexFormat) + } + + writeJSONResponse(w, resp) + } +} + +func blocksHandler(ctx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var blockHeight *big.Int + arg := mux.Vars(r)["height"] + if arg != "latest" { + var ok bool + if blockHeight, ok = new(big.Int).SetString(arg, 10); !ok || blockHeight.Sign() <= 0 { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("block height must be in decimal format starting from 1. /blocks/latest for the lastest 10 blocks")) + } + } + + blocks, err := Blocks(ctx, blockHeight) + if err != nil { + writeClientRetrievalErr(w, err) + return + } + + writeJSONResponse(w, blocks) + } +} + +func infoHandler(ctx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + addr := vars["address"] + if !ethcmn.IsHexAddress(addr) { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("address must be an ethereum 20-byte hex string")) + return + } + + txo, err := Info(ctx, ethcmn.HexToAddress(addr)) + if err != nil { + writeClientRetrievalErr(w, err) + return + } + + writeJSONResponse(w, txo) + } +} + +func balanceHandler(ctx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + addr := vars["address"] + if !ethcmn.IsHexAddress(addr) { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("address must be an ethereum 20-byte hex string")) + return + } + + total, err := Balance(ctx, ethcmn.HexToAddress(addr)) + if err != nil { + writeClientRetrievalErr(w, err) + return + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte(total)) + } +} + +func txHandler(ctx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + txHash := utils.RemoveHexPrefix(mux.Vars(r)["hash"]) + + // validation + bytes, err := hex.DecodeString(txHash) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(fmt.Sprintf("tx hash expected in hexadecimal format"))) + return + } else if len(txHash) != 64 { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("tx hash expected to be 32 bytes in length")) + return + } + + tx, err := Tx(ctx, bytes) + if err != nil { + writeClientRetrievalErr(w, err) + return + } + + writeJSONResponse(w, tx) + } +} + +func outputHandler(ctx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + pos, err := plasma.FromPositionString(mux.Vars(r)["position"]) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + + txo, err := TxOutput(ctx, pos) + if err != nil { + writeClientRetrievalErr(w, err) + } + + writeJSONResponse(w, txo) + } +} + +func submitHandler(ctx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + type reqBody struct { + Async bool `json:"async"` + TxBytes string `json:"txBytes"` + } + + var body reqBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("unable to read request body: " + err.Error())) + return + } + + // clean up txBytes string + if len(body.TxBytes) > 2 && body.TxBytes[:2] == "0x" || body.TxBytes[:2] == "0X" { + body.TxBytes = body.TxBytes[2:] + } + if len(body.TxBytes)%2 != 0 { + body.TxBytes = "0" + body.TxBytes + } + txBytes, err := hex.DecodeString(body.TxBytes) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("tx bytes must be in hexadecimal format")) + return + } + + var tx plasma.Transaction + if err := rlp.DecodeBytes(txBytes, &tx); err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("malformed tx bytes")) + return + } + + if err := tx.ValidateBasic(); err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + + // deliver the tx + if body.Async { + _, err = ctx.BroadcastTxAsync(txBytes) + } else { + _, err = ctx.BroadcastTxAndAwaitCommit(txBytes) + } + + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + + w.WriteHeader(http.StatusOK) + } +} + +/**** Helpers ****/ + +func writeJSONResponse(w http.ResponseWriter, obj interface{}) { + data, err := json.Marshal(obj) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("json:" + err.Error())) + return + } + + w.WriteHeader(http.StatusOK) + w.Write(data) +} + +func writeClientRetrievalErr(w http.ResponseWriter, err error) { + // If the client request (GET) could not be fulfilled for some + // other reason than the requested information not existing (DNE), the request + // must have been malformed or an internal server error must have occured + sdkerr, ok := err.(sdk.Error) + if ok && sdkerr.Code() == store.CodeDNE { + w.WriteHeader(http.StatusNotFound) + } else { + w.WriteHeader(http.StatusBadRequest) + // TODO: log the error + } + + w.Write([]byte(err.Error())) +} diff --git a/cmd/plasmacli/config/config.go b/cmd/plasmacli/config/config.go new file mode 100644 index 0000000..259567b --- /dev/null +++ b/cmd/plasmacli/config/config.go @@ -0,0 +1,110 @@ +package config + +import ( + "bytes" + "fmt" + "github.com/spf13/viper" + cmn "github.com/tendermint/tendermint/libs/common" + "os" + "path/filepath" + "strings" + "text/template" +) + +const defaultConfigTemplate = `# This is a TOML config file. +# For more information, see https://github.com/toml-lang/toml + +##### ethereum configuration ##### + +# Ethereum plasma contract address +ethereum_contract_address = "{{ .EthPlasmaContractAddr }}" + +# Node URL for eth client +ethereum_nodeurl = "{{ .EthNodeURL }}" + + +##### plasamd configuration ##### + +# Node URL for plasmad +node = "{{ .PlasmadNodeURL }}" + +# Trust the connected plasmad node (don't verify proofs for responses) +trust_node = {{ .PlasmadTrustNode }} + +# Chain identifier. Must be set if trust-node == false +chain_id = "{{ .PlasmadChainID }}"` + +// Config is the struct representation of the toml file. Must match the +// above defaultConfigTemplate +type Config struct { + // Ethereum config + EthPlasmaContractAddr string `mapstructure:"ethereum_contract_address"` + EthNodeURL string `mapstructure:"ethereum_nodeurl"` + + // Plasmad config + PlasmadNodeURL string `mapstructure:"node"` + PlasmadTrustNode bool `mapstructure:"trust_node"` + PlasmadChainID string `mapstructure:"chain_id"` +} + +var configTemplate *template.Template + +func init() { + var err error + tmpl := template.New("configFileTemplate") + if configTemplate, err = tmpl.Parse(defaultConfigTemplate); err != nil { + panic(err) + } +} + +// DefaultConfig returns the default configuration for the Plasma CLI +func DefaultConfig() Config { + return Config{ + EthPlasmaContractAddr: "", + EthNodeURL: "http://localhost:8545", + PlasmadNodeURL: "tcp://localhost:26657", + PlasmadTrustNode: false, + PlasmadChainID: "", + } +} + +// RegisterViper will match client flags with config and register env +func RegisterViperAndEnv() { + viper.SetEnvPrefix("PCLI") + viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + viper.AutomaticEnv() + + // The configuration files use underscores while the Cosmos SDK uses + // hypens. These aliases align `Viper.Get(..)` for both + // the SDK and the configuration file + viper.RegisterAlias("trust_node", "trust-node") + viper.RegisterAlias("chain_id", "chain-id") +} + +// ParseConfigFromViper parses the plasma.toml file and unmarshals it into a +// Config struct +func ParseConfigFromViper() (Config, error) { + config := Config{} + err := viper.Unmarshal(&config) + return config, err +} + +// WriteConfigFile renders config using the template and writes it to configFilePath. +func WriteConfigFile(configFilePath string, config Config) error { + var buffer bytes.Buffer + + if err := configTemplate.Execute(&buffer, &config); err != nil { + return fmt.Errorf("template: %s", err) + } + + if err := cmn.EnsureDir(filepath.Dir(configFilePath), os.ModePerm); err != nil { + return fmt.Errorf("ensuredir: %s", err) + } + + // 0666 allows for read and write for any user + if err := cmn.WriteFile(configFilePath, buffer.Bytes(), 0666); err != nil { + return fmt.Errorf("writefile: %s", err) + } + + return nil +} diff --git a/cmd/plasmacli/config/connection.go b/cmd/plasmacli/config/connection.go new file mode 100644 index 0000000..b0643f3 --- /dev/null +++ b/cmd/plasmacli/config/connection.go @@ -0,0 +1,55 @@ +package config + +import ( + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/flags" + "github.com/FourthState/plasma-mvp-sidechain/eth" + ethcmn "github.com/ethereum/go-ethereum/common" + "github.com/spf13/viper" +) + +var plasmaContract *eth.Plasma + +// GetContractConn returns the eth plasma contract connection +func GetContractConn() (*eth.Plasma, error) { + if plasmaContract != nil { + return plasmaContract, nil + } + + conn, err := setupContractConn() + if err != nil { + return nil, fmt.Errorf("unable to enable contract connection: %s", err) + } + + plasmaContract = conn + return plasmaContract, nil +} + +func setupContractConn() (*eth.Plasma, error) { + conf, err := ParseConfigFromViper() + if err != nil { + return nil, err + } + + dir := viper.GetString(flags.Home) + if dir[len(dir)-1] != '/' { + dir = dir + "/" + } + + if conf.EthNodeURL == "" { + return nil, fmt.Errorf("please specify a node url for eth connection in %sconfig.toml", dir) + } else if conf.EthPlasmaContractAddr == "" || !ethcmn.IsHexAddress(conf.EthPlasmaContractAddr) { + return nil, fmt.Errorf("please specify a valid contract address in %sconfig.toml", dir) + } + + ethClient, err := eth.InitEthConn(conf.EthNodeURL) + if err != nil { + return nil, err + } + plasma, err := eth.InitPlasma(ethcmn.HexToAddress(conf.EthPlasmaContractAddr), ethClient, 0) + if err != nil { + return nil, err + } + + return plasma, nil +} diff --git a/cmd/plasmacli/config/tendermint.go b/cmd/plasmacli/config/tendermint.go new file mode 100644 index 0000000..1482a92 --- /dev/null +++ b/cmd/plasmacli/config/tendermint.go @@ -0,0 +1,17 @@ +package config + +import ( + "github.com/cosmos/cosmos-sdk/client" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// AddPersistentTMFlags adds the tendermint flags as persistent flags +func AddPersistentTMFlags(cmd *cobra.Command) { + cmd.PersistentFlags().String(client.FlagNode, "", ": to tendermint rpc interface for this chain") + cmd.PersistentFlags().Bool(client.FlagTrustNode, false, "Trust connected full node (don't verify proofs for responses)") + cmd.PersistentFlags().String(client.FlagChainID, "", "id of the chain. Required if --trust-node=false") + viper.BindPFlag(client.FlagNode, cmd.PersistentFlags().Lookup(client.FlagNode)) + viper.BindPFlag(client.FlagTrustNode, cmd.PersistentFlags().Lookup(client.FlagTrustNode)) + viper.BindPFlag(client.FlagChainID, cmd.PersistentFlags().Lookup(client.FlagChainID)) +} diff --git a/cmd/plasmacli/flags/flags.go b/cmd/plasmacli/flags/flags.go new file mode 100644 index 0000000..e71df62 --- /dev/null +++ b/cmd/plasmacli/flags/flags.go @@ -0,0 +1,10 @@ +package flags + +import ( + tmcli "github.com/tendermint/tendermint/libs/cli" +) + +// Flags +const ( + Home = tmcli.HomeFlag +) diff --git a/cmd/plasmacli/main.go b/cmd/plasmacli/main.go new file mode 100644 index 0000000..09ba908 --- /dev/null +++ b/cmd/plasmacli/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/subcmd" + "os" +) + +func main() { + rootCmd := subcmd.RootCmd() + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/cmd/plasmacli/store/keystore.go b/cmd/plasmacli/store/keystore.go new file mode 100644 index 0000000..f0187ca --- /dev/null +++ b/cmd/plasmacli/store/keystore.go @@ -0,0 +1,291 @@ +package store + +import ( + "crypto/ecdsa" + "fmt" + cosmoscli "github.com/cosmos/cosmos-sdk/client" + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/accounts/keystore" + ethcmn "github.com/ethereum/go-ethereum/common" + "github.com/syndtr/goleveldb/leveldb" + "github.com/syndtr/goleveldb/leveldb/iterator" + "io/ioutil" + "path/filepath" +) + +const ( + MinPasswordLength = 8 + + NewPassphrasePrompt = "Enter new passphrase for your key:" + NewPassphrasePromptRepeat = "Repeat passphrase:" + + PassphrasePrompt = "Enter passphrase:" + + accountsDir = "data/accounts.ldb" + keysDir = "keys" +) + +var ( + home string + ks *keystore.KeyStore +) + +// InitKeystore initializes a keystore in the specified directory +func InitKeystore(homeDir string) { + home = homeDir + + dir := getDir(keysDir) + if ks == nil { + ks = keystore.NewKeyStore(dir, keystore.StandardScryptN, keystore.StandardScryptP) + } +} + +// AccountIterator returns an iterator for accounts. +// CONTRACT: Caller is responsible for closing db after use. +func AccountIterator() (iterator.Iterator, *leveldb.DB) { + dir := getDir(accountsDir) + db, err := leveldb.OpenFile(dir, nil) + if err != nil { + fmt.Printf("leveldb: %s", err) + return nil, nil + } + + return db.NewIterator(nil, nil), db +} + +// AddAccount adds a new account to the keystore +func AddAccount(name string) (ethcmn.Address, error) { + dir := getDir(accountsDir) + db, err := leveldb.OpenFile(dir, nil) + if err != nil { + return ethcmn.Address{}, fmt.Errorf("leveldb: %s", err) + } + defer db.Close() + + key := []byte(name) + if _, err = db.Get(key, nil); err == nil { + return ethcmn.Address{}, fmt.Errorf("you are trying to override an existing private key name. Please delete it first") + } + + buf := cosmoscli.BufferStdin() + password, err := cosmoscli.GetCheckPassword(NewPassphrasePrompt, NewPassphrasePromptRepeat, buf) + if err != nil { + return ethcmn.Address{}, err + } + + acc, err := ks.NewAccount(password) + if err != nil { + return ethcmn.Address{}, fmt.Errorf("keystore: %s", err) + } + + if err = db.Put(key, acc.Address.Bytes(), nil); err != nil { + return ethcmn.Address{}, fmt.Errorf("leveldb: %s", err) + } + + return acc.Address, nil +} + +// GetAccount retrieves the address of an account. +func GetAccount(name string) (ethcmn.Address, error) { + dir := getDir(accountsDir) + db, err := leveldb.OpenFile(dir, nil) + if err != nil { + return ethcmn.Address{}, fmt.Errorf("leveldb: %s", err) + } + defer db.Close() + + addr, err := db.Get([]byte(name), nil) + if err == leveldb.ErrNotFound { + return ethcmn.Address{}, fmt.Errorf("account does not exist") + } else if err != nil { + return ethcmn.Address{}, fmt.Errorf("leveldb: %s", err) + } + + return ethcmn.BytesToAddress(addr), nil +} + +// DeleteAccount removes an account from keystore +func DeleteAccount(name string) error { + dir := getDir(accountsDir) + db, err := leveldb.OpenFile(dir, nil) + if err != nil { + return fmt.Errorf("leveldb: %s", err) + } + defer db.Close() + + addr, err := db.Get([]byte(name), nil) + if err == leveldb.ErrNotFound { + return fmt.Errorf("account does not exist") + } else if err != nil { + return fmt.Errorf("leveldb: %s", err) + } + + buf := cosmoscli.BufferStdin() + password, err := cosmoscli.GetPassword(PassphrasePrompt, buf) + if err != nil { + return err + } + + acc := accounts.Account{ + Address: ethcmn.BytesToAddress(addr), + } + + if err = ks.Delete(acc, password); err != nil { + return fmt.Errorf("keystore: %s", err) + } + + return nil +} + +// UpdateAccount updates either the name of an account or the passphrase for +// an account. +func UpdateAccount(name string, updatedName string) (msg string, err error) { + dir := getDir(accountsDir) + db, err := leveldb.OpenFile(dir, nil) + if err != nil { + return msg, fmt.Errorf("leveldb: %s", err) + } + defer db.Close() + + key := []byte(name) + addr, err := db.Get(key, nil) + if err == leveldb.ErrNotFound { + return msg, fmt.Errorf("account does not exist") + } else if err != nil { + return msg, fmt.Errorf("leveldb: %s", err) + } + + if updatedName != "" { + // Update key name + if err = db.Delete(key, nil); err != nil { + return msg, fmt.Errorf("leveldb: %s", err) + } + + if err = db.Put([]byte(updatedName), addr, nil); err != nil { + return msg, fmt.Errorf("leveldb: %s", err) + } + msg = "Account name has been updated." + } else { + // Update passphrase + buf := cosmoscli.BufferStdin() + password, err := cosmoscli.GetPassword(PassphrasePrompt, buf) + updatedPassword, err := cosmoscli.GetCheckPassword(NewPassphrasePrompt, NewPassphrasePromptRepeat, buf) + if err != nil { + return msg, err + } + + acc := accounts.Account{ + Address: ethcmn.BytesToAddress(addr), + } + if err = ks.Update(acc, password, updatedPassword); err != nil { + return msg, fmt.Errorf("keystore: %s", err) + } + msg = "Account passphrase has been updated." + } + + return msg, nil +} + +// ImportECDSA imports a private key with associated an account name. +func ImportECDSA(name string, pk *ecdsa.PrivateKey) (ethcmn.Address, error) { + dir := getDir(accountsDir) + db, err := leveldb.OpenFile(dir, nil) + if err != nil { + return ethcmn.Address{}, fmt.Errorf("leveldb: %s", err) + } + defer db.Close() + + buf := cosmoscli.BufferStdin() + password, err := cosmoscli.GetCheckPassword(NewPassphrasePrompt, NewPassphrasePromptRepeat, buf) + if err != nil { + return ethcmn.Address{}, err + } + + acct, err := ks.ImportECDSA(pk, password) + if err != nil { + return ethcmn.Address{}, fmt.Errorf("keystore: %s", err) + } + + if err = db.Put([]byte(name), acct.Address.Bytes(), nil); err != nil { + return ethcmn.Address{}, fmt.Errorf("leveldb: %s", err) + } + + return acct.Address, nil + +} + +// SignHashWithPassphrase will sign over the provided hash if the the passphrase +// provided through user interaction is correct. +func SignHashWithPassphrase(signer string, hash []byte) ([]byte, error) { + addr, err := GetAccount(signer) + if err != nil { + return nil, err + } + + acc := accounts.Account{ + Address: addr, + } + + buf := cosmoscli.BufferStdin() + password, err := cosmoscli.GetPassword(PassphrasePrompt, buf) + if err != nil { + return nil, err + } + + var sig []byte + sig, err = ks.SignHashWithPassphrase(acc, password, hash) + if err != nil { + return nil, fmt.Errorf("keystore: %s", err) + } + + return sig, nil +} + +// GetKey returns the private key mapped to the provided key name. +func GetKey(name string) (*ecdsa.PrivateKey, error) { + dir := getDir(accountsDir) + db, err := leveldb.OpenFile(dir, nil) + if err != nil { + return nil, fmt.Errorf("leveldb: %s", err) + } + defer db.Close() + + addr, err := db.Get([]byte(name), nil) + if err == leveldb.ErrNotFound { + return nil, fmt.Errorf("account does not exist") + } else if err != nil { + return nil, fmt.Errorf("leveldb: %s", err) + } + + acc, err := ks.Find( + accounts.Account{ + Address: ethcmn.BytesToAddress(addr), + }, + ) + if err != nil { + return nil, fmt.Errorf("keystore: %s", err) + } + + bz, err := ioutil.ReadFile(acc.URL.Path) + if err != nil { + return nil, fmt.Errorf("ioutil: %s", err) + } + + buf := cosmoscli.BufferStdin() + pass, err := cosmoscli.GetPassword(PassphrasePrompt, buf) + if err != nil { + return nil, err + } + + key, err := keystore.DecryptKey(bz, pass) + if err != nil { + return nil, fmt.Errorf("keystore: %s", err) + } + return key.PrivateKey, nil +} + +// returns the directory specified by the --directory flag +// with the passed in string appended to the end +func getDir(location string) string { + return filepath.Join(home, location) +} diff --git a/cmd/plasmacli/store/keystore_test.go b/cmd/plasmacli/store/keystore_test.go new file mode 100644 index 0000000..ea0dd82 --- /dev/null +++ b/cmd/plasmacli/store/keystore_test.go @@ -0,0 +1,92 @@ +package store + +import ( + "bufio" + cosmoscli "github.com/cosmos/cosmos-sdk/client" + ethcmn "github.com/ethereum/go-ethereum/common" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + "os" + "strings" + "testing" +) + +// Add, Delete, Update and iterate through accounts +func TestAccounts(t *testing.T) { + // setup testing env + os.Mkdir("testing", os.ModePerm) + InitKeystore("./testing") + + // cleanup + defer func() { + viper.Reset() + os.RemoveAll("testing") + }() + + cases := []string{ + "mykey", + "another-key", + "!aADS_AS@#$%^&*()", + " last key", + } + + // Check adding and getting accounts + for i, n := range cases { + cleanUp := cosmoscli.OverrideStdin(bufio.NewReader(strings.NewReader("test1234\ntest1234\n"))) + defer cleanUp() + addr1, err := AddAccount(n) + require.NoErrorf(t, err, "case %d: failed to add account { %s }", i, n) + + addr2, err := GetAccount(n) + require.NoError(t, err, "case %d: failed to get account { %s }", i, n) + require.Equal(t, addr1, addr2, "case %d: address added and retrieved not equal for account { %s }", i, n) + } + + // Check that iterator matches + iter, db := AccountIterator() + i := 0 + var actualNames []string + var actualAddrs []ethcmn.Address + for iter.Next() { + actualNames = append(actualNames, string(iter.Key())) + actualAddrs = append(actualAddrs, ethcmn.BytesToAddress(iter.Value())) + i++ + } + iter.Release() + db.Close() + + for i, n := range actualNames { + expectedAddr, err := GetAccount(string(n)) + require.NoError(t, err, "case %d: failed to get account { %s }", i, n) + require.Equal(t, actualAddrs[i], expectedAddr, "case %d: address returned from iterator does not match actual address for account { %s }", i, n) + i++ + } + + updatedNames := []string{ + "key", + "k", + "sdfghjk", + "plasma", + } + + // Update Account name and password then Delete Accounts + for i, n := range cases { + _, err := UpdateAccount(cases[i], updatedNames[i]) + require.NoError(t, err, "case %d: failed to update account name { %s }", i, n) + + _, err = GetAccount(cases[i]) + require.Error(t, err, "case %d: retireved account for mapping that should not exist, account { %s }", i, n) + _, err = GetAccount(updatedNames[i]) + require.NoError(t, err, "case %d: failed to retrieve account after updating, account { %s }", i, updatedNames[i]) + + cleanUp := cosmoscli.OverrideStdin(bufio.NewReader(strings.NewReader("test1234\nnewpass1234\n"))) + defer cleanUp() + _, err = UpdateAccount(updatedNames[i], "") + require.NoError(t, err, "case %d: failed to update account passphrase for account { %s }", i, updatedNames[i]) + + cleanUp = cosmoscli.OverrideStdin(bufio.NewReader(strings.NewReader("newpass1234\n"))) + defer cleanUp() + err = DeleteAccount(updatedNames[i]) + require.NoError(t, err, "case %d: failed to delete account { %s }", i, updatedNames[i]) + } +} diff --git a/cmd/plasmacli/store/sigs.go b/cmd/plasmacli/store/sigs.go new file mode 100644 index 0000000..0a16069 --- /dev/null +++ b/cmd/plasmacli/store/sigs.go @@ -0,0 +1,68 @@ +package store + +import ( + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/plasma" + "github.com/syndtr/goleveldb/leveldb" +) + +const ( + signatureDir = "data/signatures.ldb" +) + +// SaveSig saves the confirmation signatures. If prepend is set to true, sig +// will be prepended to the currently stored signatures. Otherwise it will be appended +// This ordering should be determined by input order in the transaction. If +// the length of the currently stored signatures is 130 an error is returned. +func SaveSig(position plasma.Position, sig []byte, prepend bool) error { + if len(sig) != 65 { + return fmt.Errorf("signature must have a length of 65 bytes") + } + + signatures, err := GetSig(position) + if len(signatures) == 130 { + return fmt.Errorf("two signatures already exist for the given position") + } + + if prepend { + signatures = append(sig, signatures...) + } else { + signatures = append(signatures, sig...) + } + + dir := getDir(signatureDir) + db, err := leveldb.OpenFile(dir, nil) + if err != nil { + return fmt.Errorf("failed to open db for signatures: { %s }", err) + } + defer db.Close() + + k := getSigKey(position) + if err := db.Put(k, signatures, nil); err != nil { + return fmt.Errorf("failed to save confirmation signature: { %s }", err) + } + + return nil +} + +// GetSig retrieves the confirmation signatures. +func GetSig(position plasma.Position) ([]byte, error) { + dir := getDir(signatureDir) + db, err := leveldb.OpenFile(dir, nil) + if err != nil { + return nil, fmt.Errorf("failed to open db for signature: { %s }", err) + } + defer db.Close() + + k := getSigKey(position) + if sig, err := db.Get(k, nil); err != nil { + return []byte{}, fmt.Errorf("failed to get signature: { %s }", err) + } else { + return sig, nil + } +} + +// returns the key used for confirm signature mapping +func getSigKey(pos plasma.Position) []byte { + return append(pos.BlockNum.Bytes(), []byte(string(pos.TxIndex))...) +} diff --git a/cmd/plasmacli/store/sigs_test.go b/cmd/plasmacli/store/sigs_test.go new file mode 100644 index 0000000..7c23fb1 --- /dev/null +++ b/cmd/plasmacli/store/sigs_test.go @@ -0,0 +1,122 @@ +package store + +import ( + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/flags" + "github.com/FourthState/plasma-mvp-sidechain/plasma" + "github.com/ethereum/go-ethereum/crypto" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + "math/big" + "os" + "testing" +) + +func TestSavSig(t *testing.T) { + // setup testing env + os.Mkdir("testing", os.ModePerm) + viper.Set(flags.Home, "./testing") + + // cleanup + defer func() { + viper.Reset() + os.RemoveAll("testing") + + }() + + cases := [][]int64{ + // must save for both output indicies + {5, 0, 0, 0}, + // deposit + {0, 0, 0, 10}, + // different txindex + {5, 1, 0, 0}, + // different blk num + {6, 1, 0, 0}, + } + + for i, p := range cases { + key, _ := crypto.GenerateKey() + txHash := crypto.Keccak256([]byte("txhash")) + + expected, err := crypto.Sign(txHash, key) + pos := plasma.NewPosition(big.NewInt(p[0]), uint16(p[1]), uint8(p[2]), big.NewInt(p[3])) + + _, err = GetSig(pos) + require.Errorf(t, err, "case %d: did not error when getting non existent signature for position %s", i, pos) + + err = SaveSig(pos, expected, true) + require.NoError(t, err, "case %d: failed to save signature for position %s", i, pos) + + actual, err := GetSig(pos) + require.NoError(t, err, "case %d: failed when getting signature for position %s", i, pos) + require.Equal(t, expected, actual, "case %d: actual signature was not equal to expected signature for position %s", i, pos) + + if !pos.IsDeposit() { + // changing output index should not effect + // retrieval of signature + pos.OutputIndex = uint8(1) + actual, err = GetSig(pos) + + require.NoError(t, err, "case %d: failed when getting signature for position %s", i, pos) + require.Equal(t, expected, actual, "case %d: actual signature was not equal to expected signature for position %s", i, pos) + } + } +} + +// pass in a signature without length 65 bytes +func TestBadSigs(t *testing.T) { + // setup testing env + os.Mkdir("testing", os.ModePerm) + viper.Set(flags.Home, "./testing") + + // cleanup + defer func() { + viper.Reset() + os.RemoveAll("testing") + + }() + + key, _ := crypto.GenerateKey() + txHash := crypto.Keccak256([]byte("transaction hash")) + + sig, _ := crypto.Sign(txHash, key) + pos := plasma.NewPosition(big.NewInt(10), uint16(5), uint8(0), big.NewInt(0)) + + err := SaveSig(pos, sig[:60], true) + require.Error(t, err, "did not reject a signature with a length not equal to 65") +} + +// Save two confirmation signatures +func TestMultiConfirmSig(t *testing.T) { + // setup testing env + os.Mkdir("testing", os.ModePerm) + viper.Set(flags.Home, "./testing") + + // cleanup + defer func() { + viper.Reset() + os.RemoveAll("testing") + + }() + + key, _ := crypto.GenerateKey() + txHash0 := crypto.Keccak256([]byte("hash one")) + txHash1 := crypto.Keccak256([]byte("the second hash")) + + sig0, _ := crypto.Sign(txHash0, key) + sig1, _ := crypto.Sign(txHash1, key) + + pos := plasma.NewPosition(big.NewInt(1000), uint16(256), uint8(0), big.NewInt(0)) + + // save sig1 first + err := SaveSig(pos, sig1, false) + require.NoError(t, err, "failed to save first confirm signature") + + // prepend sig0 + err = SaveSig(pos, sig0, true) + require.NoError(t, err, "failed to save second confirm signature") + + sigs, err := GetSig(pos) + require.NoError(t, err, "failed to retrieve confirm signatures") + require.Equal(t, append(sig0, sig1...), sigs, "retrieved signatures do not match expected signatures") +} diff --git a/cmd/plasmacli/subcmd/eth/challenge.go b/cmd/plasmacli/subcmd/eth/challenge.go new file mode 100644 index 0000000..8889480 --- /dev/null +++ b/cmd/plasmacli/subcmd/eth/challenge.go @@ -0,0 +1,111 @@ +package eth + +import ( + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/config" + ks "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/store" + "github.com/FourthState/plasma-mvp-sidechain/plasma" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/spf13/cobra" + "github.com/spf13/viper" + tm "github.com/tendermint/tendermint/rpc/core/types" + "math/big" + "strconv" +) + +// ChallengeCmd returns the eth challenge command +func ChallengeCmd() *cobra.Command { + config.AddPersistentTMFlags(challengeCmd) + challengeCmd.Flags().StringP(gasLimitF, "g", "300000", "gas limit for ethereum transaction") + challengeCmd.Flags().String(proofF, "", "merkle proof of inclusion") + challengeCmd.Flags().String(sigsF, "", "confirmation signatures for the challenging transaction") + challengeCmd.Flags().Bool(useNodeF, false, "trust connected full node") + challengeCmd.Flags().String(txBytesF, "", "bytes of the challenging transaction") + return challengeCmd +} + +var challengeCmd = &cobra.Command{ + Use: "challenge ", + Short: "Challenge an existing exit", + Long: `Challenge a pending exit. If the trust-node flag is set, +the necessary information will be retrieved from the connected full node. +Otherwise, the transaction bytes, merkle proof, and confirmation signatures must be given. +Usage of flags override information retrieved from full node. + +Usage: + plasmacli eth challenge --trust-node --gas-limit 30000 + plasmacli eth cahllenge --proof --signatures --txBytes `, + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + viper.BindPFlags(cmd.Flags()) + + // parse positions + exitingPos, err := plasma.FromPositionString(args[0]) + if err != nil { + return err + } + + challengingPos, err := plasma.FromPositionString(args[1]) + if err != nil { + return err + } + + gasLimit, err := strconv.ParseUint(viper.GetString(gasLimitF), 10, 64) + if err != nil { + return fmt.Errorf("failed to parse gas limit: { %s }", err) + } + + key, err := ks.GetKey(args[2]) + if err != nil { + return fmt.Errorf("failed to retrieve account key: { %s }", err) + } + + // bind key + auth := bind.NewKeyedTransactor(key) + transactOpts := &bind.TransactOpts{ + From: auth.From, + Signer: auth.Signer, + GasLimit: gasLimit, + } + + var txBytes, proof, confirmSignatures []byte + if viper.GetBool(useNodeF) { + var result *tm.ResultTx + ctx := context.NewCLIContext() + result, confirmSignatures, err = getProof(ctx, challengingPos) + if err != nil { + return fmt.Errorf("failed to retrieve exit information: { %s }", err) + } + + txBytes = result.Tx + + // flatten proof + for _, aunt := range result.Proof.Proof.Aunts { + proof = append(proof, aunt...) + } + } + + if len(confirmSignatures) == 0 { + sigs, err := ks.GetSig(challengingPos) + if err == nil { + confirmSignatures = sigs + } + } + + txBytes, proof, confirmSignatures, err = parseProof(txBytes, proof, confirmSignatures) + if err != nil { + return err + } + + exitPos := [4]*big.Int{exitingPos.BlockNum, big.NewInt(int64(exitingPos.TxIndex)), big.NewInt(int64(exitingPos.OutputIndex)), exitingPos.DepositNonce} + challengePos := [2]*big.Int{challengingPos.BlockNum, big.NewInt(int64(challengingPos.TxIndex))} + tx, err := plasmaContract.ChallengeExit(transactOpts, exitPos, challengePos, txBytes, proof, confirmSignatures) + if err != nil { + return fmt.Errorf("failed to send challenge transaction: { %s }", err) + } + + fmt.Printf("Sent challenge transaction\nTransaction Hash: 0x%x\n", tx.Hash()) + return nil + }, +} diff --git a/cmd/plasmacli/subcmd/eth/deposit.go b/cmd/plasmacli/subcmd/eth/deposit.go new file mode 100644 index 0000000..b678869 --- /dev/null +++ b/cmd/plasmacli/subcmd/eth/deposit.go @@ -0,0 +1,63 @@ +package eth + +import ( + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/store" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/crypto" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "math/big" + "strconv" +) + +// DepositCmd returns the eth deposit command +func DepositCmd() *cobra.Command { + depositCmd.Flags().StringP(gasLimitF, "g", "150000", "gas limit for ethereum transaction") + return depositCmd +} + +var depositCmd = &cobra.Command{ + Use: "deposit ", + Short: "Deposit to rootchain contract", + Long: `Deposit to the rootchain contract as specified in plasma.toml. + +Usage: + plasmacli eth deposit --gas-limit 30000`, + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + viper.BindPFlags(cmd.Flags()) + + key, err := store.GetKey(args[1]) + if err != nil { + return fmt.Errorf("failed to retrieve account: { %s }", err) + } + + amt, err := strconv.ParseInt(args[0], 10, 64) + if err != nil { + return fmt.Errorf("failed to parse amount: { %s }", err) + } + + gasLimit, err := strconv.ParseUint(viper.GetString(gasLimitF), 10, 64) + if err != nil { + return fmt.Errorf("failed to parse gas limit: { %s }", err) + } + + // bind key, generate transact opts + auth := bind.NewKeyedTransactor(key) + transactOpts := &bind.TransactOpts{ + From: auth.From, + Signer: auth.Signer, + GasLimit: gasLimit, + Value: big.NewInt(amt), + } + + tx, err := plasmaContract.Deposit(transactOpts, crypto.PubkeyToAddress(key.PublicKey)) + if err != nil { + return fmt.Errorf("failed to deposit: { %s }", err) + } + + fmt.Printf("Successfully sent deposit transaction\nTransaction Hash: 0x%x\n", tx.Hash()) + return nil + }, +} diff --git a/cmd/plasmacli/subcmd/eth/exit.go b/cmd/plasmacli/subcmd/eth/exit.go new file mode 100644 index 0000000..73fe49d --- /dev/null +++ b/cmd/plasmacli/subcmd/eth/exit.go @@ -0,0 +1,176 @@ +package eth + +import ( + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/config" + ks "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/store" + "github.com/FourthState/plasma-mvp-sidechain/plasma" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + ethcmn "github.com/ethereum/go-ethereum/common" + eth "github.com/ethereum/go-ethereum/core/types" + "github.com/spf13/cobra" + "github.com/spf13/viper" + tm "github.com/tendermint/tendermint/rpc/core/types" + "math/big" + "strconv" +) + +// ExitCmd returns the eth exit command +func ExitCmd() *cobra.Command { + config.AddPersistentTMFlags(exitCmd) + exitCmd.Flags().String(feeF, "0", "fee committed in an unfinalized spend of the input") + exitCmd.Flags().StringP(gasLimitF, "g", "300000", "gas limit for ethereum transaction") + exitCmd.Flags().String(proofF, "", "merkle proof of inclusion") + exitCmd.Flags().String(sigsF, "", "confirmation signatures for exiting utxo") + exitCmd.Flags().Bool(useNodeF, false, "retrieve information from connected full node") + exitCmd.Flags().String(txBytesF, "", "bytes of the transaction that created the utxo ") + return exitCmd +} + +var exitCmd = &cobra.Command{ + Use: "exit ", + Short: "Start an exit for the given position", + Long: `Starts an exit for the given position. If the trust-node flag is set, +the necessary information will be retrieved from the connected full node. +Otherwise, the transaction bytes, merkle proof, and confirmation signatures must be given. +Usage of flags override information retrieved from full node. + +Deposit/Fee Exit Usage: + plasmacli exit + +Transaction Exit Usage: + plasmacli exit --trust-node --gas-limit 30000 + plasmacli exit -t --fee + plasmacli exit -b --proof -S --fee `, + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) (err error) { + viper.BindPFlags(cmd.Flags()) + var tx *eth.Transaction + + // parse position + position, err := plasma.FromPositionString(args[1]) + if err != nil { + return err + } + + fee, err := strconv.ParseInt(viper.GetString(feeF), 10, 64) + if err != nil { + return fmt.Errorf("failed to parse fee: { %s }", err) + } + + gasLimit, err := strconv.ParseUint(viper.GetString(gasLimitF), 10, 64) + if err != nil { + return fmt.Errorf("failed to parse gas limit: { %s }", err) + } + + // retrieve account key + key, err := ks.GetKey(args[0]) + if err != nil { + return fmt.Errorf("failed to retrieve account key: { %s }", err) + } + + // bind key, generate transact opts + auth := bind.NewKeyedTransactor(key) + transactOpts := &bind.TransactOpts{ + From: auth.From, + Signer: auth.Signer, + GasLimit: gasLimit, + Value: big.NewInt(minExitBond), // minExitBond + } + + // send fee exit + if position.IsFee() { + tx, err = plasmaContract.StartFeeExit(transactOpts, position.BlockNum, big.NewInt(fee)) + if err != nil { + return fmt.Errorf("failed to start fee exit: { %s }", err) + } + fmt.Printf("Sent fee exit transaction\nTransaction Hash: 0x%x\n", tx.Hash()) + return nil + } + + // send deposit exit + if position.IsDeposit() { + tx, err := plasmaContract.StartDepositExit(transactOpts, position.DepositNonce, big.NewInt(fee)) + if err != nil { + return fmt.Errorf("failed to start deposit exit: { %s }", err) + } + fmt.Printf("Sent deposit exit transaction\nTransaction Hash: 0x%x\n", tx.Hash()) + return nil + } + + // retrieve information necessary for transaction exit + var txBytes, proof, confirmSignatures []byte + if viper.GetBool(useNodeF) { // query full node + var result *tm.ResultTx + ctx := context.NewCLIContext() + result, confirmSignatures, err = getProof(ctx, position) + if err != nil { + return fmt.Errorf("failed to retrieve exit information: { %s }", err) + } + + txBytes = result.Tx + + // flatten proof + for _, aunt := range result.Proof.Proof.Aunts { + proof = append(proof, aunt...) + } + } + + if len(confirmSignatures) == 0 { + sigs, err := ks.GetSig(position) + if err == nil { + confirmSignatures = sigs + } + } + + txBytes, proof, confirmSignatures, err = parseProof(txBytes, proof, confirmSignatures) + if err != nil { + return err + } + + txPos := [3]*big.Int{position.BlockNum, big.NewInt(int64(position.TxIndex)), big.NewInt(int64(position.OutputIndex))} + tx, err = plasmaContract.StartTransactionExit(transactOpts, txPos, txBytes, proof, confirmSignatures, big.NewInt(fee)) + if err != nil { + return fmt.Errorf("failed to start transaction exit: { %s }", err) + } + fmt.Printf("Sent exit transaction\nTransaction Hash: 0x%x\n", tx.Hash()) + return nil + }, +} + +// Parses flags related to proving exit/challenge +// Flags override full node information +// All necessary exit/challenge information is returned, or error is thrown +func parseProof(txBytes, proof, confirmSignatures []byte) ([]byte, []byte, []byte, error) { + if viper.GetString(txBytesF) != "" { + txBytes = ethcmn.FromHex(viper.GetString(txBytesF)) + } + + if viper.GetString(proofF) != "" { + proof = ethcmn.FromHex(viper.GetString(proofF)) + } + + if viper.GetString(sigsF) != "" { + confirmSignatures = ethcmn.FromHex(viper.GetString(sigsF)) + } + + // return error if information is missing + if len(txBytes) != 811 { + return txBytes, proof, confirmSignatures, fmt.Errorf("please provide txBytes with a length of 811 bytes. Current length: %d", len(txBytes)) + } + + if len(proof)%32 != 0 { + return txBytes, proof, confirmSignatures, fmt.Errorf("please provide a merkle proof of inclusion for the given position. Proof must consist of 32 byte hashes") + } + + if len(confirmSignatures)%65 != 0 { + return txBytes, proof, confirmSignatures, fmt.Errorf("please provde confirmation signatures for the given position. Signatures must be 65 bytes in length") + } + + if len(proof) == 0 { + fmt.Println("Warning: No proof was found or provided. If the exiting transaction was not the only transaction included in the block, this transaction will fail.") + } + + return txBytes, proof, confirmSignatures, nil +} diff --git a/cmd/plasmacli/subcmd/eth/finalize.go b/cmd/plasmacli/subcmd/eth/finalize.go new file mode 100644 index 0000000..9bb9ccd --- /dev/null +++ b/cmd/plasmacli/subcmd/eth/finalize.go @@ -0,0 +1,63 @@ +package eth + +import ( + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/store" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + eth "github.com/ethereum/go-ethereum/core/types" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "strconv" +) + +// FinalizeCmd returns the eth finalize command +func FinalizeCmd() *cobra.Command { + finalizeCmd.Flags().BoolP(depositsF, "D", false, "indicate that deposit exits should be finalized") + finalizeCmd.Flags().StringP(gasLimitF, "g", "240000", "gas limit for ethereum transaction") + return finalizeCmd +} + +var finalizeCmd = &cobra.Command{ + Use: "finalize ", + Short: "Finalize exit queue on rootchain", + Long: `Defaults to finalizing transaction exits. Use deposit flag to finalize deposit exit queue + +Usage: + plasmacli eth finalize --gas-limit 30000 + plasmacli eth finalize --deposits`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) (err error) { + viper.BindPFlags(cmd.Flags()) + + key, err := store.GetKey(args[0]) + if err != nil { + return fmt.Errorf("failed to retrieve account: { %s }", err) + } + + gasLimit, err := strconv.ParseUint(viper.GetString(gasLimitF), 10, 64) + if err != nil { + return fmt.Errorf("failed to parse gas limit: { %s }", err) + } + + // bind key, generate transact opts + auth := bind.NewKeyedTransactor(key) + transactOpts := &bind.TransactOpts{ + From: auth.From, + Signer: auth.Signer, + GasLimit: gasLimit, + } + + var tx *eth.Transaction + if viper.GetBool(depositsF) { + tx, err = plasmaContract.FinalizeDepositExits(transactOpts) + } else { + tx, err = plasmaContract.FinalizeTransactionExits(transactOpts) + } + if err != nil { + return fmt.Errorf("failed to finalize exits: { %s }", err) + } + + fmt.Printf("Successfully sent finalize exits transaction\nTransaction Hash: 0x%x\n", tx.Hash()) + return nil + }, +} diff --git a/cmd/plasmacli/subcmd/eth/prove.go b/cmd/plasmacli/subcmd/eth/prove.go new file mode 100644 index 0000000..f4a8d6e --- /dev/null +++ b/cmd/plasmacli/subcmd/eth/prove.go @@ -0,0 +1,120 @@ +package eth + +import ( + "encoding/json" + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/config" + "github.com/FourthState/plasma-mvp-sidechain/plasma" + "github.com/FourthState/plasma-mvp-sidechain/store" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/ethereum/go-ethereum/rlp" + "github.com/spf13/cobra" + tm "github.com/tendermint/tendermint/rpc/core/types" +) + +// ProveCmd returns the eth prove command +func ProveCmd() *cobra.Command { + config.AddPersistentTMFlags(proveCmd) + return proveCmd +} + +var proveCmd = &cobra.Command{ + Use: "prove ", + Short: "Prove transaction inclusion: prove ", + Args: cobra.ExactArgs(2), + Long: "Returns proof for transaction inclusion. Use to exit transactions in the smart contract", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.NewCLIContext() + + // parse position + position, err := plasma.FromPositionString(args[1]) + if err != nil { + return err + } + + result, sigs, err := getProof(ctx, position) + if err != nil { + return err + } + + // print meta data + fmt.Printf("Roothash: 0x%x\n", result.Proof.RootHash) + fmt.Printf("Total: %d\n", result.Proof.Proof.Total) + fmt.Printf("LeafHash: 0x%x\n", result.Proof.Proof.LeafHash) + fmt.Printf("TxBytes: 0x%x\n", []byte(result.Tx)) + + switch len(sigs) { + case 65: + fmt.Printf("Confirmation Signatures: 0x%x\n", sigs[:]) + case 130: + fmt.Printf("Confirmation Signatures: 0x%x, 0x%x\n", sigs[:65], sigs[65:]) + } + + // flatten aunts + var proof []byte + for _, aunt := range result.Proof.Proof.Aunts { + proof = append(proof, aunt...) + } + + if len(proof) == 0 { + if result.Proof.Proof.Total == 1 { + fmt.Println("No proof required since this was the only transaction in the block") + } else { + fmt.Printf("Proof: nil\n") + } + } else { + fmt.Printf("Proof: 0x%x\n", proof) + } + + return nil + }, +} + +// Returns transaction results for given position +// Trusts connected full node +func getProof(ctx context.CLIContext, position plasma.Position) (*tm.ResultTx, []byte, error) { + key := store.GetOutputKey(position) + hash, err := ctx.QueryStore(key, store.DataStoreName) + if err != nil { + return &tm.ResultTx{}, nil, err + } + + txKey := store.GetTxKey(hash) + txBytes, err := ctx.QueryStore(txKey, store.DataStoreName) + + var tx store.Transaction + if err := rlp.DecodeBytes(txBytes, &tx); err != nil { + return &tm.ResultTx{}, nil, fmt.Errorf("Transaction decoding failed: %s", err.Error()) + } + + // query tm node for information about this tx + result, err := ctx.Client.Tx(tx.Transaction.MerkleHash(), true) + if err != nil { + return &tm.ResultTx{}, nil, err + } + + // Look for confirmation signatures + // Ignore error if no confirm sig currently exists in store + var sigs []byte + if len(tx.SpenderTxs[position.OutputIndex]) > 0 { + queryPath := fmt.Sprintf("custom/data/tx/%s", tx.SpenderTxs[position.OutputIndex]) + data, err := ctx.Query(queryPath, nil) + if err != nil { + return &tm.ResultTx{}, nil, err + } + + var spenderTx store.Transaction + if err := json.Unmarshal(data, &spenderTx); err != nil { + return &tm.ResultTx{}, nil, fmt.Errorf("unmarshaling json query response: %s", err) + } + for _, input := range spenderTx.Transaction.Inputs { + if input.Position.String() == position.String() { + for _, sig := range input.ConfirmSignatures { + sigs = append(sigs, sig[:]...) + } + } + } + } + + return result, sigs, nil +} diff --git a/cmd/plasmacli/subcmd/eth/query/balance.go b/cmd/plasmacli/subcmd/eth/query/balance.go new file mode 100644 index 0000000..e750776 --- /dev/null +++ b/cmd/plasmacli/subcmd/eth/query/balance.go @@ -0,0 +1,44 @@ +package query + +import ( + "fmt" + ks "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/store" + ethcmn "github.com/ethereum/go-ethereum/common" + "github.com/spf13/cobra" +) + +// BalanceCmd returns the eth query balance command +func BalanceCmd() *cobra.Command { + return balanceCmd +} + +var balanceCmd = &cobra.Command{ + Use: "balance ", + Short: "Query for balance avaliable for withdraw from rootchain", + SilenceUsage: true, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + var ( + addr ethcmn.Address + err error + ) + + if !ethcmn.IsHexAddress(args[0]) { + fmt.Println("Hex address not provided, retrieving account..") + if addr, err = ks.GetAccount(args[0]); err != nil { + return fmt.Errorf("failed account retrieval: %s", err) + } + } else { + addr = ethcmn.HexToAddress(args[0]) + } + + balance, err := plasmaContract.BalanceOf(nil, addr) + + if err != nil { + return fmt.Errorf("failed to retrieve balance: { %s }", err) + } + + fmt.Printf("Rootchain Balance: %d\n", balance) + return nil + }, +} diff --git a/cmd/plasmacli/subcmd/eth/query/block.go b/cmd/plasmacli/subcmd/eth/query/block.go new file mode 100644 index 0000000..44933fc --- /dev/null +++ b/cmd/plasmacli/subcmd/eth/query/block.go @@ -0,0 +1,57 @@ +package query + +import ( + "fmt" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "math/big" + "strconv" + "time" +) + +// BlockCmd returns the eth query block command +func BlockCmd() *cobra.Command { + blockCmd.Flags().String(limitF, "1", "number of plasma blocks to be displayed") + return blockCmd +} + +var blockCmd = &cobra.Command{ + Use: "block ", + Short: "Query a plasma block submitted to the rootchain", + Long: `Returns the reported block header, number of transactions, fee amount, +and creation time for the requested plasma block. + +Usage: + plasmacli eth query block + plasmacli eth query block --limit `, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + viper.BindPFlags(cmd.Flags()) + curr, err := strconv.ParseInt(args[0], 10, 64) + if err != nil { + return fmt.Errorf("failed to parse block number: { %s }", err) + } + + lim, err := strconv.ParseInt(viper.GetString(limitF), 10, 64) + if err != nil { + return fmt.Errorf("failed to parse limit: { %s }", err) + } + + end := curr + lim + for curr < end { + block, err := plasmaContract.PlasmaChain(nil, big.NewInt(curr)) + if err != nil { + return fmt.Errorf("failed to retrieve block: { %s }", err) + } + if block.CreatedAt.Int64() == 0 { + break + } + + fmt.Printf("Block: %d\nHeader: 0x%x\nTxs: %d\nFee: %d\nCreated: %v\n\n", + curr, block.Header, block.NumTxns, block.FeeAmount, time.Unix(block.CreatedAt.Int64(), 0)) + curr++ + } + + return nil + }, +} diff --git a/cmd/plasmacli/subcmd/eth/query/deposit.go b/cmd/plasmacli/subcmd/eth/query/deposit.go new file mode 100644 index 0000000..3143ed7 --- /dev/null +++ b/cmd/plasmacli/subcmd/eth/query/deposit.go @@ -0,0 +1,81 @@ +package query + +import ( + "fmt" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "math/big" + "strconv" +) + +// DepositCmd returns the eth query deposit command +func DepositCmd() *cobra.Command { + depositCmd.Flags().Bool(allF, false, "all deposits will be displayed") + depositCmd.Flags().String(limitF, "1", "amount of deposits to be displayed") + return depositCmd +} + +var depositCmd = &cobra.Command{ + Use: "deposit ", + Short: "Query for a deposit that occurred on the rootchain", + Long: `Queries for deposits that occurred on the rootchain. + +Usage: + plasmacli eth query deposit + plasmacli eth query deposit --limit + plasmacli eth query deposit --all`, + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) (err error) { + viper.BindPFlags(cmd.Flags()) + var curr, lim int64 + + if lim, err = strconv.ParseInt(viper.GetString(limitF), 10, 64); err != nil { + return fmt.Errorf("failed to parse limit: %s", err) + } + + lastNonce, err := plasmaContract.DepositNonce(nil) + if err != nil { + return fmt.Errorf("failed to trying to get last deposit nonce: %s", err) + } + + if viper.GetBool(allF) { // Print all deposits + curr = 1 + lim = lastNonce.Int64() - 1 + } else if len(args) > 0 { // Use command line arg as starting nonce + if curr, err = strconv.ParseInt(args[0], 10, 64); err != nil { + return fmt.Errorf("failed to parse nonce - %s", err) + } + } else { + return fmt.Errorf("please provide a nonce") + } + + if curr >= lastNonce.Int64() { + return fmt.Errorf("deposit nonce provided does not exist") + } + + if err = displayDeposits(curr, lim); err != nil { + return fmt.Errorf("failed while displaying deposits: %s", err) + } + + return err + }, +} + +func displayDeposits(curr, lim int64) error { + for lim > 0 { + deposit, err := plasmaContract.Deposits(nil, big.NewInt(curr)) + if err != nil { + return err + } + + if deposit.EthBlockNum.Int64() == 0 { + break + } + + fmt.Printf("Owner: 0x%x\nAmount: %d\nNonce: %d\nEthereum Block: %d\n\n", deposit.Owner, deposit.Amount, curr, deposit.EthBlockNum) + curr++ + lim-- + } + + return nil +} diff --git a/cmd/plasmacli/subcmd/eth/query/exits.go b/cmd/plasmacli/subcmd/eth/query/exits.go new file mode 100644 index 0000000..ab4235f --- /dev/null +++ b/cmd/plasmacli/subcmd/eth/query/exits.go @@ -0,0 +1,193 @@ +package query + +import ( + "fmt" + ks "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/store" + "github.com/FourthState/plasma-mvp-sidechain/plasma" + "github.com/FourthState/plasma-mvp-sidechain/utils" + ethcmn "github.com/ethereum/go-ethereum/common" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "math/big" + "strconv" + "time" +) + +// ExitsCmd returns the eth exit command +func ExitsCmd() *cobra.Command { + exitsCmd.Flags().StringP(accountF, "a", "", "display exits for given account or address") + exitsCmd.Flags().Bool(allF, false, "all pending exits will be displayed") + exitsCmd.Flags().BoolP(depositsF, "D", false, "display deposit exits") + exitsCmd.Flags().String(indexF, "0", "index to begin displaying exits from") + exitsCmd.Flags().String(limitF, "1", "amount of exits to display") + exitsCmd.Flags().StringP(positionF, "p", "", "display exit status for specified position") + return exitsCmd +} + +var exitsCmd = &cobra.Command{ + Use: "exits", + Short: "Display pending exits", + Long: `Display pending rootchain exits. Queries the rootchain exit queue. +Use the deposit flag to display deposit exits. + +Usage: + plasmacli eth query exits -a + plasmacli eth query exits --deposits + plasmacli eth query exits --all + plasmacli eth query exits --index --limit + plasmacli eth query exits --position `, + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + viper.BindPFlags(cmd.Flags()) + + var ( + curr int64 + addr ethcmn.Address + err error + ) + + // check if position specified + if viper.GetString(positionF) != "" { + pos, err := plasma.FromPositionString(viper.GetString(positionF)) + if err != nil { + return fmt.Errorf("failed to parse position: %s", err) + } + return displayExit(pos.Priority(), addr, pos.IsDeposit()) + } + + if viper.GetString(accountF) != "" { + str := viper.GetString(accountF) + if !ethcmn.IsHexAddress(str) { + if addr, err = ks.GetAccount(str); err != nil { + return fmt.Errorf("failed to retrieve local account: %s", err) + } + } else { + addr = ethcmn.HexToAddress(str) + } + } + + // parse queue length + var queueLength *big.Int + if viper.GetBool(depositsF) { + queueLength, err = plasmaContract.DepositQueueLength(nil) + } else { + queueLength, err = plasmaContract.TxQueueLength(nil) + } + + if err != nil { + return fmt.Errorf("failed to retrieve exit queue length: %s", err) + } + + lim, err := strconv.ParseInt(viper.GetString(limitF), 10, 64) + if err != nil { + return fmt.Errorf("failed to parse limit: %s", err) + } + + // adjust passed in limit to avoid error + // when interacting with rootchain + if lim > queueLength.Int64() { + lim = queueLength.Int64() + } + + if viper.GetBool(allF) { // print all exits + lim = queueLength.Int64() + } else { // use index/limit + index := viper.GetString(indexF) + if index == "" { + return fmt.Errorf("please specify one of the following flags: --all, --index, --account") + } + curr, err = strconv.ParseInt(viper.GetString(indexF), 10, 64) + if err != nil { + return fmt.Errorf("failed to parse index: { %s }", err) + } + } + + if err := displayExits(curr, lim, addr, viper.GetBool(depositsF)); err != nil { + return fmt.Errorf("failure occured while querying exits: %s ", err) + } + + return nil + }, +} + +func displayExits(curr, lim int64, addr ethcmn.Address, deposits bool) (err error) { + for lim > 0 { + var key *big.Int + + if deposits { + key, err = plasmaContract.DepositExitQueue(nil, big.NewInt(curr)) + } else { + key, err = plasmaContract.TxExitQueue(nil, big.NewInt(curr)) + } + if err != nil { + return err + } + + // Get right 128 bits for position mapping + key = new(big.Int).SetBytes(key.Bytes()[16:]) + + if err := displayExit(key, addr, deposits); err != nil { + return err + } + + curr++ + lim-- + } + return nil +} + +// display a single exit given the position key in big.Int format +func displayExit(key *big.Int, addr ethcmn.Address, deposits bool) (err error) { + var exit struct { + Amount *big.Int + CommittedFee *big.Int + CreatedAt *big.Int + EthBlockNum *big.Int + Owner ethcmn.Address + State uint8 + } + + position := plasma.FromExitKey(key, deposits) + + if deposits { + exit, err = plasmaContract.DepositExits(nil, key) + } else { + exit, err = plasmaContract.TxExits(nil, key) + } + + if err != nil { + return err + } + + if !utils.IsZeroAddress(addr) && exit.Owner != addr { + return nil + } + state := parseState(exit.State) + fmt.Printf("Owner: 0x%x\nAmount: %d\nState: %s\nCommitted Fee: %d\nCreated: %v\nEthBlockNum: %v\nPosition %s\n\n", + exit.Owner, exit.Amount, state, exit.CommittedFee, time.Unix(exit.CreatedAt.Int64(), 0), exit.EthBlockNum, position) + if state == "Pending" { + oneWeek := time.Duration(168) // 168 hours in one week + timeLeft := time.Until(time.Unix(exit.CreatedAt.Int64(), 0).Add(time.Hour * oneWeek)) + if timeLeft > 0 { + fmt.Printf("Exit will be finalized in about: %v hours\n\n", timeLeft.Hours()) + } else { + fmt.Printf("Exit is ready to be finalized!\n\n") + } + } + + return nil +} + +func parseState(exit uint8) (state string) { + switch exit { + case 0: + state = "Nonexistent" + case 1: + state = "Pending" + case 2: + state = "Challenged" + case 3: + state = "Finalized" + } + return state +} diff --git a/cmd/plasmacli/subcmd/eth/query/root.go b/cmd/plasmacli/subcmd/eth/query/root.go new file mode 100644 index 0000000..280e772 --- /dev/null +++ b/cmd/plasmacli/subcmd/eth/query/root.go @@ -0,0 +1,42 @@ +package query + +import ( + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/config" + "github.com/FourthState/plasma-mvp-sidechain/eth" + "github.com/spf13/cobra" +) + +var plasmaContract *eth.Plasma + +var ( + // flags + allF = "all" + accountF = "account" + depositsF = "deposits" + indexF = "index" + limitF = "limit" + positionF = "position" +) + +// RootCmd returns the eth query command +func RootCmd() *cobra.Command { + queryCmd.AddCommand( + BalanceCmd(), + BlockCmd(), + DepositCmd(), + ExitsCmd(), + RootchainCmd(), + ) + + return queryCmd +} + +var queryCmd = &cobra.Command{ + Use: "query", + Short: "Query for rootchain related information", + PreRunE: func(cmd *cobra.Command, args []string) error { + plasma, err := config.GetContractConn() + plasmaContract = plasma + return err + }, +} diff --git a/cmd/plasmacli/subcmd/eth/query/rootchain.go b/cmd/plasmacli/subcmd/eth/query/rootchain.go new file mode 100644 index 0000000..516fea9 --- /dev/null +++ b/cmd/plasmacli/subcmd/eth/query/rootchain.go @@ -0,0 +1,48 @@ +package query + +import ( + "fmt" + "github.com/spf13/cobra" +) + +// RootchainCmd returns the eth query rootchain command +func RootchainCmd() *cobra.Command { + return rootchainCmd +} + +var rootchainCmd = &cobra.Command{ + Use: "rootchain", + Short: "Display rootchain contract information", + Long: `Display last committed block, total contract balance, total withdraw balance, minimum exit bond, and operator address. +Total contract balance does not include total withdraw balance. The total withdraw balance are exits that have been finalized, but not transferred yet.`, + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, agrs []string) error { + lastCommittedBlock, err := plasmaContract.LastCommittedBlock(nil) + if err != nil { + return err + } + + totalBalance, err := plasmaContract.PlasmaChainBalance(nil) + if err != nil { + return err + } + + withdrawBalance, err := plasmaContract.TotalWithdrawBalance(nil) + if err != nil { + return err + } + + minExitBond, err := plasmaContract.MinExitBond(nil) + if err != nil { + return err + } + + operator, err := plasmaContract.Operator(nil) + if err != nil { + return err + } + fmt.Printf("Last Committed Block: %d\nContract Balance: %d\nWithdraw Balance: %d\nMinimum Exit Bond: %d\nOperator: 0x%x\n", + lastCommittedBlock, totalBalance, withdrawBalance, minExitBond, operator) + return nil + }, +} diff --git a/cmd/plasmacli/subcmd/eth/root.go b/cmd/plasmacli/subcmd/eth/root.go new file mode 100644 index 0000000..b170176 --- /dev/null +++ b/cmd/plasmacli/subcmd/eth/root.go @@ -0,0 +1,71 @@ +package eth + +import ( + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/config" + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/subcmd/eth/query" + "github.com/FourthState/plasma-mvp-sidechain/eth" + "github.com/FourthState/plasma-mvp-sidechain/plasma" + "github.com/cosmos/cosmos-sdk/client" + "github.com/spf13/cobra" +) + +const ( + // flags + accountF = "account" + allF = "all" + depositsF = "deposits" + feeF = "fee" + gasLimitF = "gas-limit" + indexF = "index" + limitF = "limit" + ownerF = "owner" + positionF = "position" + proofF = "proof" + sigsF = "signatures" + useNodeF = "use-node" + txBytesF = "tx-bytes" + + minExitBond = 200000 +) + +var plasmaContract *eth.Plasma + +// RootCmd returns root eth command +func RootCmd() *cobra.Command { + ethCmd.AddCommand( + ProveCmd(), + ChallengeCmd(), + ExitCmd(), + FinalizeCmd(), + DepositCmd(), + WithdrawCmd(), + client.LineBreak, + + query.RootCmd(), + ) + + return ethCmd +} + +var ethCmd = &cobra.Command{ + Use: "eth", + Short: "Interact with the plasma smart contract", + Long: `Configurations for interacting with the rootchain contract can be specified in /plasma.toml. +An eth node instance needs to be running for this command to work.`, + PreRunE: func(cmd *cobra.Command, args []string) error { + fmt.Println("setting up eth connection") + plasma, err := config.GetContractConn() + plasmaContract = plasma + return err + }, +} + +func HasTxExited(pos plasma.Position) (bool, error) { + conn, err := config.GetContractConn() + if err != nil { + return false, err + } + + return conn.HasTxExited(nil, pos) +} diff --git a/cmd/plasmacli/subcmd/eth/withdraw.go b/cmd/plasmacli/subcmd/eth/withdraw.go new file mode 100644 index 0000000..2ca578c --- /dev/null +++ b/cmd/plasmacli/subcmd/eth/withdraw.go @@ -0,0 +1,55 @@ +package eth + +import ( + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/store" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "strconv" +) + +// WithdrawCmd returns the eth withdraw command +func WithdrawCmd() *cobra.Command { + withdrawCmd.Flags().StringP(gasLimitF, "g", "150000", "gas limit for ethereum transaction") + return withdrawCmd +} + +var withdrawCmd = &cobra.Command{ + Use: "withdraw ", + Short: "Withdraw all available funds from rootchain contract", + Long: `Withdraw all available funds from the rootchain contract + +Usage: + plasmacli eth withdraw --gas-limit 30000`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + viper.BindPFlags(cmd.Flags()) + + key, err := store.GetKey(args[0]) + if err != nil { + return fmt.Errorf("failed to retrieve account: { %s }", err) + } + + gasLimit, err := strconv.ParseUint(viper.GetString(gasLimitF), 10, 64) + if err != nil { + return fmt.Errorf("failed to parse gas limit: { %s }", err) + } + + // bind key, generate transact opts + auth := bind.NewKeyedTransactor(key) + transactOpts := &bind.TransactOpts{ + From: auth.From, + Signer: auth.Signer, + GasLimit: gasLimit, + } + + tx, err := plasmaContract.Withdraw(transactOpts) + if err != nil { + return fmt.Errorf("failed to withdraw: { %s }", err) + } + + fmt.Printf("Successfully sent withdraw transaction\nTransaction Hash: 0x%x\n", tx.Hash()) + return nil + }, +} diff --git a/cmd/plasmacli/subcmd/keys/add.go b/cmd/plasmacli/subcmd/keys/add.go new file mode 100644 index 0000000..8dbc266 --- /dev/null +++ b/cmd/plasmacli/subcmd/keys/add.go @@ -0,0 +1,33 @@ +package keys + +import ( + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/store" + "github.com/spf13/cobra" +) + +// AddCmd returns the keys add command +func AddCmd() *cobra.Command { + return addCmd +} + +var addCmd = &cobra.Command{ + Use: "add ", + Short: "Create a new account", + Long: `Add an encrypted account to your local keystore.`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + name := args[0] + + address, err := store.AddAccount(name) + if err != nil { + return err + } + + fmt.Println("\n**Important** do not lose your passphrase.") + fmt.Println("It is the only way to recover your account") + fmt.Println("You should export this account and store it in a secure location") + fmt.Printf("NAME: %s\tADDRESS: 0x%x\n", name, address) + return nil + }, +} diff --git a/cmd/plasmacli/subcmd/keys/delete.go b/cmd/plasmacli/subcmd/keys/delete.go new file mode 100644 index 0000000..c1ef178 --- /dev/null +++ b/cmd/plasmacli/subcmd/keys/delete.go @@ -0,0 +1,29 @@ +package keys + +import ( + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/store" + "github.com/spf13/cobra" +) + +// DeleteCmd returns the keys delete command +func DeleteCmd() *cobra.Command { + return deleteCmd +} + +var deleteCmd = &cobra.Command{ + Use: "delete ", + Short: "Delete the given address", + Long: `Deletes the account from the keystore if the passphrase is correct.`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + name := args[0] + + if err := store.DeleteAccount(name); err != nil { + return err + } + + fmt.Println("Account deleted.") + return nil + }, +} diff --git a/cmd/plasmacli/subcmd/keys/import.go b/cmd/plasmacli/subcmd/keys/import.go new file mode 100644 index 0000000..0dc5176 --- /dev/null +++ b/cmd/plasmacli/subcmd/keys/import.go @@ -0,0 +1,69 @@ +package keys + +import ( + "crypto/ecdsa" + "errors" + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/store" + "github.com/ethereum/go-ethereum/crypto" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// ImportCmd returns the keys import command +func ImportCmd() *cobra.Command { + importCmd.Flags().String(fileF, "", "read the private key from the specified keyfile (must be absolute path)") + return importCmd +} + +var importCmd = &cobra.Command{ + Use: "import ", + Short: "Import a private key", + Long: `Imports an unencrypted private key read in hexadecimal format and creates a new account on the sidechain. +Prints the address. + +Usage: + plasmacli import + plasmacli import --file + +If the file flag is set: +The keyfile is assumed to contain an unencrypted private key in hexadecimal format. +The keyfile must also be an absolute path + +The account is saved in encrypted format, you are prompted for a passphrase. +You must remember this passphrase to unlock your account in the future. +`, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + viper.BindPFlags(cmd.Flags()) + + name := args[0] + var key *ecdsa.PrivateKey + var err error + file := viper.GetString(fileF) + if file != "" { + key, err = crypto.LoadECDSA(file) + if err != nil { + return fmt.Errorf("failed loading the keyfile: { %s }", err) + } + } else { + if len(args) < 2 { + return errors.New("please provide an unencrytped private if the --file flag is not set") + } + key, err = crypto.HexToECDSA(args[1]) + if err != nil { + return fmt.Errorf("failed parsing private key: { %s }", err) + } + + } + + address, err := store.ImportECDSA(name, key) + if err != nil { + return err + } + + fmt.Println("Successfully imported.") + fmt.Printf("NAME: %s\t\tADDRESS: 0x%x\n", name, address) + return nil + }, +} diff --git a/cmd/plasmacli/subcmd/keys/list.go b/cmd/plasmacli/subcmd/keys/list.go new file mode 100644 index 0000000..45e5230 --- /dev/null +++ b/cmd/plasmacli/subcmd/keys/list.go @@ -0,0 +1,35 @@ +package keys + +import ( + "errors" + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/store" + ethcmn "github.com/ethereum/go-ethereum/common" + "github.com/spf13/cobra" +) + +// ListCmd returns the keys list command +func ListCmd() *cobra.Command { + return listCmd +} + +var listCmd = &cobra.Command{ + Use: "list", + Short: "List all accounts", + Long: "Return a list of all account addresses stored by the local keystore", + RunE: func(cmd *cobra.Command, args []string) error { + iter, db := store.AccountIterator() + if iter == nil || db == nil { + return errors.New("unexpected error encountered when opening account data") + } + defer db.Close() + + fmt.Printf("NAME:\t\tADDRESS:\n") + for iter.Next() { + fmt.Printf("%s\t\t0x%x\n", iter.Key(), ethcmn.BytesToAddress(iter.Value())) + } + iter.Release() + + return nil + }, +} diff --git a/cmd/plasmacli/subcmd/keys/root.go b/cmd/plasmacli/subcmd/keys/root.go new file mode 100644 index 0000000..70d4427 --- /dev/null +++ b/cmd/plasmacli/subcmd/keys/root.go @@ -0,0 +1,30 @@ +package keys + +import ( + "github.com/spf13/cobra" +) + +// flags +const ( + nameF = "name" + fileF = "file" +) + +// RootCmd returns the keys command +func RootCmd() *cobra.Command { + keysCmd.AddCommand( + AddCmd(), + DeleteCmd(), + ImportCmd(), + ListCmd(), + UpdateCmd(), + ) + + return keysCmd +} + +var keysCmd = &cobra.Command{ + Use: "keys", + Short: "Manage local private keys", + Long: `Keys allows you to manage your local keystore.`, +} diff --git a/cmd/plasmacli/subcmd/keys/update.go b/cmd/plasmacli/subcmd/keys/update.go new file mode 100644 index 0000000..42919aa --- /dev/null +++ b/cmd/plasmacli/subcmd/keys/update.go @@ -0,0 +1,39 @@ +package keys + +import ( + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/store" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// UpdateCmd returns the keys update command +func UpdateCmd() *cobra.Command { + updateCmd.Flags().String(nameF, "", "updated key name.") + return updateCmd +} + +var updateCmd = &cobra.Command{ + Use: "update ", + Short: "Update account passphrase or key name", + Long: `Update local encrypted private keys to be encrypted with the new passphrase. + +Usage: + plasmacli keys update + plasmacli keys update --name + `, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + viper.BindPFlags(cmd.Flags()) + + name := args[0] + updatedName := viper.GetString(nameF) + msg, err := store.UpdateAccount(name, updatedName) + if err != nil { + return err + } + + fmt.Println(msg) + return nil + }, +} diff --git a/cmd/plasmacli/subcmd/query/balance.go b/cmd/plasmacli/subcmd/query/balance.go new file mode 100644 index 0000000..814970e --- /dev/null +++ b/cmd/plasmacli/subcmd/query/balance.go @@ -0,0 +1,46 @@ +package query + +import ( + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/client" + ks "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/store" + "github.com/cosmos/cosmos-sdk/client/context" + ethcmn "github.com/ethereum/go-ethereum/common" + "github.com/spf13/cobra" +) + +// BalanceCmd returns the query balance command +func BalanceCmd() *cobra.Command { + return balanceCmd +} + +var balanceCmd = &cobra.Command{ + Use: "balance ", + Short: "Total plasma chain balance across utxos", + SilenceUsage: true, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.NewCLIContext() + var ( + addr ethcmn.Address + err error + ) + + if !ethcmn.IsHexAddress(args[0]) { + if addr, err = ks.GetAccount(args[0]); err != nil { + return fmt.Errorf("failed local account retrieval: %s", err) + } + } else { + addr = ethcmn.HexToAddress(args[0]) + } + + total, err := client.Balance(ctx, addr) + if err != nil { + return err + } + + fmt.Printf("Address: %0x\n", addr) + fmt.Printf("Total: %s\n", string(total)) + return nil + }, +} diff --git a/cmd/plasmacli/subcmd/query/block.go b/cmd/plasmacli/subcmd/query/block.go new file mode 100644 index 0000000..031b600 --- /dev/null +++ b/cmd/plasmacli/subcmd/query/block.go @@ -0,0 +1,82 @@ +package query + +import ( + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/client" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/spf13/cobra" + "math/big" +) + +// BlockCmd returns the query block command +func BlockCmd() *cobra.Command { + return blockCmd +} + +// BlocksCmd returns the query blocks command +func BlocksCmd() *cobra.Command { + return blocksCmd +} + +var blockCmd = &cobra.Command{ + Use: "block ", + Short: "Query information about a plasma block", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.NewCLIContext() + num, ok := new(big.Int).SetString(args[0], 10) + if !ok { + return fmt.Errorf("number must be in decimal format") + } + cmd.SilenceUsage = true + + block, err := client.Block(ctx, num) + if err != nil { + return err + } + + fmt.Printf("Block Header: 0x%x\n", block.Header) + fmt.Printf("Transaction Count: %d, FeeAmount: %d\n", block.TxnCount, block.FeeAmount) + fmt.Printf("Tendermint BlockHeight: %d\n", block.TMBlockHeight) + + return nil + }, +} + +var blocksCmd = &cobra.Command{ + Use: "blocks ", + Short: "Query Metadata about blocks", + Long: "Query Metadata about blocks. If no height is provided, the latest 10 blocks will be queried. Otherwise 10 starting from the specified height", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.NewCLIContext() + + var startingHeight *big.Int + if len(args) > 0 { + var ok bool + startingHeight, ok = new(big.Int).SetString(args[0], 10) + if !ok { + return fmt.Errorf("block number must be in decimal format") + } + } + + blocks, err := client.Blocks(ctx, startingHeight) + if err != nil { + return err + } + + if len(blocks) == 0 { + fmt.Println("no blocks") + return nil + } + + for _, block := range blocks { + fmt.Printf("Block Height: %d\n", block.Height) + fmt.Printf("Header: 0x%x\n", block.Header) + fmt.Printf("Transaction Count: %d\n", block.TxnCount) + fmt.Printf("Fee Amount: %s\n\n", block.FeeAmount) + } + + return nil + }, +} diff --git a/cmd/plasmacli/subcmd/query/info.go b/cmd/plasmacli/subcmd/query/info.go new file mode 100644 index 0000000..acd4f16 --- /dev/null +++ b/cmd/plasmacli/subcmd/query/info.go @@ -0,0 +1,64 @@ +package query + +import ( + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/client" + ks "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/store" + "github.com/cosmos/cosmos-sdk/client/context" + ethcmn "github.com/ethereum/go-ethereum/common" + "github.com/spf13/cobra" +) + +// InfoCmd returns the query information command +func InfoCmd() *cobra.Command { + return infoCmd +} + +var infoCmd = &cobra.Command{ + Use: "info ", + Short: "Information on owned utxos valid and invalid", + SilenceUsage: true, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.NewCLIContext() + var ( + addr ethcmn.Address + err error + ) + + if !ethcmn.IsHexAddress(args[0]) { + if addr, err = ks.GetAccount(args[0]); err != nil { + return fmt.Errorf("failed local account retrieval: %s", err) + } + } else { + addr = ethcmn.HexToAddress(args[0]) + } + + utxos, err := client.Info(ctx, addr) + if err != nil { + return err + } + + for i, utxo := range utxos { + fmt.Printf("UTXO %d\n", i) + fmt.Printf("Position: %s, Amount: %s, Spent: %t\nSpender Hash: %s\n", utxo.Position, utxo.Output.Amount.String(), utxo.Spent, utxo.SpenderTx) + fmt.Printf("Transaction Hash: 0x%x\nConfirmationHash: 0x%x\n", utxo.TxHash, utxo.ConfirmationHash) + /* + // TODO: Add --verbose flag that if set will query TxInput and print InputAddresses and InputPositions as well + // print inputs if applicable + positions := utxo.InputPositions + for i, p := range positions { + fmt.Printf("Input %d Position: %s\n", i, p) + } + */ + + fmt.Printf("End UTXO %d info\n\n", i) + } + + if len(utxos) == 0 { + fmt.Println("no information available for this address") + } + + return nil + }, +} diff --git a/cmd/plasmacli/subcmd/query/root.go b/cmd/plasmacli/subcmd/query/root.go new file mode 100644 index 0000000..264c120 --- /dev/null +++ b/cmd/plasmacli/subcmd/query/root.go @@ -0,0 +1,24 @@ +package query + +import ( + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/config" + "github.com/spf13/cobra" +) + +// RootCmd returns the query command for plasmacli +func RootCmd() *cobra.Command { + config.AddPersistentTMFlags(queryCmd) + queryCmd.AddCommand( + BalanceCmd(), + BlockCmd(), + BlocksCmd(), + InfoCmd(), + ) + + return queryCmd +} + +var queryCmd = &cobra.Command{ + Use: "query", + Short: "Query information related to the sidechain", +} diff --git a/cmd/plasmacli/subcmd/restserver.go b/cmd/plasmacli/subcmd/restserver.go new file mode 100644 index 0000000..e3a7f93 --- /dev/null +++ b/cmd/plasmacli/subcmd/restserver.go @@ -0,0 +1,47 @@ +package subcmd + +import ( + "github.com/FourthState/plasma-mvp-sidechain/app" + "github.com/FourthState/plasma-mvp-sidechain/client" + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/config" + sdkCli "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/lcd" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// RestServerCmd creates the cobra command that starts the rest server +func RestServerCmd() *cobra.Command { + serverCmd.Flags().String(sdkCli.FlagListenAddr, "tcp://localhost:1317", "The address for the server to listen on") + serverCmd.Flags().Bool(sdkCli.FlagTLS, false, "Enable SSL/TLS layer") + serverCmd.Flags().String(sdkCli.FlagSSLHosts, "", "Comma-separated hostnames and IPs to generate a certificate for") + serverCmd.Flags().String(sdkCli.FlagSSLCertFile, "", "Path to a SSL certificate file. If not supplied, a self-signed certificate will be generated.") + serverCmd.Flags().String(sdkCli.FlagSSLKeyFile, "", "Path to a key file; ignored if a certificate file is not supplied.") + serverCmd.Flags().String(sdkCli.FlagCORS, "", "Set the domains that can make CORS requests (* for all)") + serverCmd.Flags().Int(sdkCli.FlagMaxOpenConnections, 1000, "The number of maximum open connections") + + config.AddPersistentTMFlags(serverCmd) + return serverCmd +} + +var serverCmd = &cobra.Command{ + Use: "rest-server", + Short: "Start LCD (light-client daemon), a local REST server", + RunE: func(cmd *cobra.Command, args []string) error { + viper.BindPFlags(cmd.Flags()) + + rs := lcd.NewRestServer(app.MakeCodec()) + client.RegisterRoutes(rs.CliCtx, rs.Mux) + + // Start the rest server and return error if one exists + err := rs.Start( + viper.GetString(sdkCli.FlagListenAddr), + viper.GetString(sdkCli.FlagSSLHosts), + viper.GetString(sdkCli.FlagSSLCertFile), + viper.GetString(sdkCli.FlagSSLKeyFile), + viper.GetInt(sdkCli.FlagMaxOpenConnections), + viper.GetBool(sdkCli.FlagTLS)) + + return err + }, +} diff --git a/cmd/plasmacli/subcmd/root.go b/cmd/plasmacli/subcmd/root.go new file mode 100644 index 0000000..e58227e --- /dev/null +++ b/cmd/plasmacli/subcmd/root.go @@ -0,0 +1,76 @@ +package subcmd + +import ( + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/config" + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/flags" + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/store" + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/subcmd/eth" + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/subcmd/keys" + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/subcmd/query" + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/subcmd/tx" + "github.com/cosmos/cosmos-sdk/client" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "os" + "path/filepath" +) + +// default home directory +var homeDir = os.ExpandEnv("$HOME/.plasmacli/") + +// RootCmd returns the initialized root cmd for plasmacli +func RootCmd() *cobra.Command { + cobra.EnableCommandSorting = false + rootCmd.PersistentFlags().String(flags.Home, homeDir, "home directory for plasmacli") + if err := viper.BindPFlags(rootCmd.PersistentFlags()); err != nil { + fmt.Println(err) + os.Exit(1) + } + + rootCmd.AddCommand( + tx.RootCmd(), + eth.RootCmd(), + query.RootCmd(), + client.LineBreak, + + RestServerCmd(), + client.LineBreak, + + keys.RootCmd(), + client.LineBreak, + + VersionCmd(), + ) + + return rootCmd +} + +var rootCmd = &cobra.Command{ + Use: "plasmacli", + Short: "Plasma Client", + SilenceErrors: true, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + config.RegisterViperAndEnv() + homeDir := viper.GetString(flags.Home) + + store.InitKeystore(homeDir) + + configFilepath := filepath.Join(homeDir, "config.toml") + if _, err := os.Stat(configFilepath); os.IsNotExist(err) { + if err := config.WriteConfigFile(configFilepath, config.DefaultConfig()); err != nil { + return err + } + } else if err != nil { + return err + } + + viper.AddConfigPath(homeDir) + viper.SetConfigName("config") + if err := viper.MergeInConfig(); err != nil { + return err + } + + return nil + }, +} diff --git a/cmd/plasmacli/subcmd/tx/include.go b/cmd/plasmacli/subcmd/tx/include.go new file mode 100644 index 0000000..26cf020 --- /dev/null +++ b/cmd/plasmacli/subcmd/tx/include.go @@ -0,0 +1,93 @@ +package tx + +import ( + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/store" + "github.com/FourthState/plasma-mvp-sidechain/msgs" + "github.com/FourthState/plasma-mvp-sidechain/utils" + "github.com/cosmos/cosmos-sdk/client/context" + ethcmn "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rlp" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "math/big" + "strings" +) + +func IncludeCmd() *cobra.Command { + includeCmd.Flags().Int64P(replayF, "r", 0, "Replay Nonce that can be incremented to allow for resubmissions of include deposit messages") + includeCmd.Flags().String(addressF, "", "address represented as hex string") + includeCmd.Flags().Bool(asyncF, false, "wait for transaction commitment synchronously") + return includeCmd +} + +var includeCmd = &cobra.Command{ + Use: "include-deposit ", + Short: "Include a deposit from with given nonce", + Long: `Example usage: + plasmacli include-deposit + plasmacli include-deposit --address
+ plasmacli include-deposit --address
-r 3`, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + viper.BindPFlags(cmd.Flags()) + ctx := context.NewCLIContext() + + // validate addresses + var address ethcmn.Address + var err error + if len(args) == 2 { + address, err = store.GetAccount(args[1]) + if err != nil { + return fmt.Errorf("Could not retrieve account: %s", args[1]) + } + } else { + addrToken := viper.GetString(addressF) + addrToken = strings.TrimSpace(addrToken) + if !ethcmn.IsHexAddress(addrToken) { + return fmt.Errorf("invalid address provided. please use hex format") + } + address := ethcmn.HexToAddress(addrToken) + if utils.IsZeroAddress(address) { + return fmt.Errorf("cannot include deposit from the zero address") + } + } + + nonce, ok := new(big.Int).SetString(strings.TrimSpace(args[0]), 10) + if !ok { + return fmt.Errorf("could not parse deposit nonce. Please resubmit with nonce in base 10") + } + + replay := viper.GetInt(replayF) + + msg := msgs.IncludeDepositMsg{ + DepositNonce: nonce, + Owner: address, + ReplayNonce: uint64(replay), + } + + if err := msg.ValidateBasic(); err != nil { + return err + } + + txBytes, err := rlp.EncodeToBytes(&msg) + if err != nil { + return err + } + + // broadcast to the node + if viper.GetBool(asyncF) { + if _, err := ctx.BroadcastTxAsync(txBytes); err != nil { + return err + } + } else { + res, err := ctx.BroadcastTxAndAwaitCommit(txBytes) + if err != nil { + return err + } + fmt.Printf("Committed at block %d. Hash %s\n", res.Height, res.TxHash) + } + + return nil + }, +} diff --git a/cmd/plasmacli/subcmd/tx/root.go b/cmd/plasmacli/subcmd/tx/root.go new file mode 100644 index 0000000..032c5c2 --- /dev/null +++ b/cmd/plasmacli/subcmd/tx/root.go @@ -0,0 +1,36 @@ +package tx + +import ( + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/config" + "github.com/spf13/cobra" +) + +const ( + accountF = "accounts" + addressF = "address" + asyncF = "async" + confirmSigs0F = "Input0ConfirmSigs" + confirmSigs1F = "Input1ConfirmSigs" + feeF = "fee" + inputsF = "inputValues" + ownerF = "owner" + positionF = "position" + replayF = "replay" +) + +// RootCmd returns the root tx command +func RootCmd() *cobra.Command { + config.AddPersistentTMFlags(txCmd) + txCmd.AddCommand( + IncludeCmd(), + SpendCmd(), + SignCmd(), + ) + + return txCmd +} + +var txCmd = &cobra.Command{ + Use: "tx", + Short: "Submit or interact with plasma chain txs", +} diff --git a/cmd/plasmacli/subcmd/tx/sign.go b/cmd/plasmacli/subcmd/tx/sign.go new file mode 100644 index 0000000..a598d61 --- /dev/null +++ b/cmd/plasmacli/subcmd/tx/sign.go @@ -0,0 +1,136 @@ +package tx + +import ( + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/client" + clistore "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/store" + "github.com/FourthState/plasma-mvp-sidechain/plasma" + "github.com/FourthState/plasma-mvp-sidechain/utils" + cosmoscli "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/context" + ethcmn "github.com/ethereum/go-ethereum/common" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "strings" +) + +const ( + signPrompt = "Would you like to finalize this transaction? [Y/n]" +) + +func SignCmd() *cobra.Command { + signCmd.Flags().String(ownerF, "", "Owner of the output (required with position flag)") + signCmd.Flags().String(positionF, "", "Position of transaction to finalize (required with owner flag)") + return signCmd +} + +var signCmd = &cobra.Command{ + Use: "sign ", + Short: "Sign confirmation signatures for pending transactions", + Long: `Iterate over all unfinalized transaction corresponding to the provided account. +Prompt the user for confirmation to finailze the pending transactions. Owner and Position flags can be used to finalize a specific transaction. + +Usage: + plasmacli sign + plasmacli sign --position "(blknum.txindex.oindex.depositnonce)"`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + viper.BindPFlags(cmd.Flags()) + ctx := context.NewCLIContext() + + name := args[0] + + signerAddr, err := clistore.GetAccount(name) + if err != nil { + return err + } + + positionS := viper.GetString(positionF) + if positionS != "" { + position, err := plasma.FromPositionString(strings.TrimSpace(viper.GetString(positionF))) + if err != nil { + return err + } + + err = signSingleConfirmSig(ctx, position, signerAddr, name) + return err + } + + utxos, err := client.Info(ctx, signerAddr) + if err != nil { + return err + } + + for _, output := range utxos { + + if output.Spent { + tx, err := client.Tx(ctx, output.SpenderTx) + if err != nil { + return err + } + + for _, pos := range tx.Transaction.InputPositions() { + err = signSingleConfirmSig(ctx, pos, signerAddr, name) + if err != nil { + fmt.Println(err) + } + } + } + } + + return nil + }, +} + +// generate confirmation signature for specified position and verify that +// the inputs provided are correct. Signing address should match one of +// the input addresses. Generate confirmation signature for given output. +func signSingleConfirmSig(ctx context.CLIContext, position plasma.Position, signerAddr ethcmn.Address, name string) error { + // query for output for the specified position + output, err := client.TxOutput(ctx, position) + if err != nil { + return err + } + inputInfo, err := client.TxInput(ctx, position) + if err != nil { + return err + } + + sig, _ := clistore.GetSig(output.Position) + inputAddrs := inputInfo.InputAddresses + + if len(sig) == 130 || (len(sig) == 65 && len(inputAddrs) == 1) { + return nil + } + + for i, input := range inputAddrs { + if input != signerAddr { + continue + } + // get confirmation to generate signature + fmt.Printf("\nUTXO\nPosition: %s\nOwner: 0x%x\nValue: %d\n", output.Position, output.Output.Owner, output.Output.Amount) + buf := cosmoscli.BufferStdin() + auth, err := cosmoscli.GetString(signPrompt, buf) + if err != nil { + return err + } + if auth != "Y" { + return nil + } + + hash := utils.ToEthSignedMessageHash(output.ConfirmationHash) + sig, err := clistore.SignHashWithPassphrase(name, hash) + if err != nil { + return fmt.Errorf("failed to generate confirmation signature: { %s }", err) + } + + if err := clistore.SaveSig(output.Position, sig, i == 0); err != nil { + return err + } + + // print the results + fmt.Printf("Confirmation Signature for output with position: %s\n", output.Position) + fmt.Printf("0x%x\n", sig) + } + return nil +} diff --git a/cmd/plasmacli/subcmd/tx/spend.go b/cmd/plasmacli/subcmd/tx/spend.go new file mode 100644 index 0000000..e020bed --- /dev/null +++ b/cmd/plasmacli/subcmd/tx/spend.go @@ -0,0 +1,408 @@ +package tx + +import ( + "encoding/hex" + "encoding/json" + "fmt" + clistore "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/store" + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmacli/subcmd/eth" + "github.com/FourthState/plasma-mvp-sidechain/msgs" + "github.com/FourthState/plasma-mvp-sidechain/plasma" + "github.com/FourthState/plasma-mvp-sidechain/store" + "github.com/FourthState/plasma-mvp-sidechain/utils" + "github.com/cosmos/cosmos-sdk/client/context" + ethcmn "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rlp" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "math/big" + "strings" +) + +func SpendCmd() *cobra.Command { + spendCmd.Flags().String(positionF, "", "UTXO Positions to be spent, format: (blknum0.txindex0.oindex0.depositnonce0)::(blknum1.txindex1.oindex1.depositnonce1)") + spendCmd.Flags().StringP(confirmSigs0F, "0", "", "Input Confirmation Signatures for first input to be spent (separated by commas)") + spendCmd.Flags().StringP(confirmSigs1F, "1", "", "Input Confirmation Signatures for second input to be spent (separated by commas)") + spendCmd.Flags().String(feeF, "0", "Fee to be spent") + spendCmd.Flags().Bool(asyncF, false, "broadcast transactions asynchronously") + return spendCmd +} + +var spendCmd = &cobra.Command{ + Use: "spend ", + Short: "Send a transaction spending utxos", + Long: `Send a transaction spending from the specified account. If sending to multiple addresses, the account specified must contain exact utxo values. +If a single spending account is specified, leftover value from spending the utxo will be sent back to the account. +In the case that the spending account does not have a large enough single utxo, two input utxos will be used. User can override retireved data with position and confirm signature flags. + in the following usage is the address being sent the utxo amounts. + +Usage: + plasmacli + plasmacli --fee + plasmacli --confirmSigs0 --confirmSig1 `, + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + viper.BindPFlags(cmd.Flags()) + ctx := context.NewCLIContext() + + // parse accounts + var accs []string + names := args[0] + + accTokens := strings.Split(strings.TrimSpace(names), ",") + if len(accTokens) == 0 || len(accTokens) > 2 { + return fmt.Errorf("1 or 2 accounts must be specified") + } + for _, token := range accTokens { + accs = append(accs, strings.TrimSpace(token)) + } + + toAddrs, err := parseToAddresses(args[2]) + if err != nil { + return err + } + + amounts, fee, total, err := parseAmounts(args[1], toAddrs) + if err != nil { + return err + } + + inputs, err := parseInputs() + if err != nil { + return err + } + + change := new(big.Int) + if len(inputs) == 0 { + inputs, change, err = retrieveInputs(ctx, accs, total) + if err != nil { + return err + } + + if len(inputs) == 0 { + return fmt.Errorf("failed to generate a valid transaction. Please provide the inputs") + } + } + + // get confirmation signatures from local storage + confirmSignatures := getConfirmSignatures(inputs) + + // override retireved signatures if provided through flags + confirmSignatures, err = parseConfirmSignatures(confirmSignatures) + if err != nil { + return err + } + + // build transaction + // create the inputs without signatures + tx := plasma.Transaction{} + tx.Inputs = append(tx.Inputs, plasma.NewInput(inputs[0], [65]byte{}, confirmSignatures[0])) + if len(inputs) > 1 { + tx.Inputs = append(tx.Inputs, plasma.NewInput(inputs[1], [65]byte{}, confirmSignatures[1])) + } else { + tx.Inputs = append(tx.Inputs, plasma.NewInput(plasma.NewPosition(nil, 0, 0, nil), [65]byte{}, nil)) + } + + // generate outputs + // use change to determine outcome of second output + tx.Outputs = append(tx.Outputs, plasma.NewOutput(toAddrs[0], amounts[0])) + if len(toAddrs) > 1 { + if change.Sign() == 0 { + tx.Outputs = append(tx.Outputs, plasma.NewOutput(toAddrs[1], amounts[1])) + } else { + return fmt.Errorf("cannot spend to two addresses since exact utxo inputs could not be found") + } + } else if change.Sign() == 1 { + addr, err := clistore.GetAccount(accs[0]) + if err != nil { + return err + } + tx.Outputs = append(tx.Outputs, plasma.NewOutput(addr, change)) + } else { + tx.Outputs = append(tx.Outputs, plasma.NewOutput(ethcmn.Address{}, nil)) + } + tx.Fee = fee + + // create and fill in the signatures + signer := accs[0] + txHash := utils.ToEthSignedMessageHash(tx.TxHash()) + var signature [65]byte + sig, err := clistore.SignHashWithPassphrase(signer, txHash) + if err != nil { + return err + } + copy(signature[:], sig) + tx.Inputs[0].Signature = signature + if len(inputs) > 1 { + if len(accs) > 2 { + signer = accs[1] + } + sig, err := clistore.SignHashWithPassphrase(signer, txHash) + if err != nil { + return err + } + copy(signature[:], sig) + tx.Inputs[1].Signature = signature + } + + // create SpendMsg and txBytes + msg := msgs.SpendMsg{ + Transaction: tx, + } + if err := msg.ValidateBasic(); err != nil { + return fmt.Errorf("failed on validating transaction. If you didn't provide the inputs please open an issue on github. Error: { %s }", err) + } + + txBytes, err := rlp.EncodeToBytes(&msg) + if err != nil { + return err + } + + // broadcast to the node + if viper.GetBool(asyncF) { + if _, err := ctx.BroadcastTxAsync(txBytes); err != nil { + return err + } + } else { + res, err := ctx.BroadcastTxAndAwaitCommit(txBytes) + if err != nil { + return err + } + fmt.Printf("Committed at block %d. Hash 0x%x\n", res.Height, res.TxHash) + } + + return nil + }, +} + +// Retrieve confirmation signatures from local storage if they exist +func getConfirmSignatures(inputs []plasma.Position) (confirmSignatures [2][][65]byte) { + for i, input := range inputs { + sig, _ := clistore.GetSig(input) + if sig != nil { + var sigs [][65]byte + switch len(sig) { + case 65: + var s [65]byte + copy(s[:], sig[:]) + sigs = append(sigs, s) + case 130: + var s [65]byte + copy(s[:], sig[:65]) + sigs = append(sigs, s) + copy(s[:], sig[65:]) + sigs = append(sigs, s) + } + confirmSignatures[i] = sigs + } + } + return confirmSignatures +} + +// parses input amounts and fee +// amounts - [amount0, amount1] +func parseAmounts(amtArgs string, toAddrs []ethcmn.Address) (amounts []*big.Int, fee, total *big.Int, err error) { + total = new(big.Int) + amountTokens := strings.Split(strings.TrimSpace(amtArgs), ",") + if len(amountTokens) != 1 && len(amountTokens) != 2 { + return amounts, fee, total, fmt.Errorf("1 or 2 output amounts must be specified") + } + + if len(amountTokens) != len(toAddrs) { + return amounts, fee, total, fmt.Errorf("provided amounts to not match the number of outputs") + } + + for _, token := range amountTokens { + token = strings.TrimSpace(token) + num, ok := new(big.Int).SetString(token, 10) + if !ok { + return amounts, fee, total, fmt.Errorf("failed to parsing amount: %s", token) + } + total.Add(total, num) + amounts = append(amounts, num) + } + + var ok bool + fee, ok = new(big.Int).SetString(strings.TrimSpace(viper.GetString(feeF)), 10) + if !ok { + return amounts, fee, total, fmt.Errorf("failed to parse fee: %s", fee) + } + total.Add(total, fee) + + return amounts, fee, total, nil +} + +// parse confirmation signatures passed in through flags +func parseConfirmSignatures(confirmSignatures [2][][65]byte) ([2][][65]byte, error) { + for i := 0; i < 2; i++ { + var flag string + if i == 0 { + flag = confirmSigs0F + } else { + flag = confirmSigs1F + + } + confirmSigTokens := strings.Split(strings.TrimSpace(viper.GetString(flag)), ",") + // empty confirmsig + if len(confirmSigTokens) == 1 && confirmSigTokens[0] == "" { + continue + } else if len(confirmSigTokens) > 2 { + return confirmSignatures, fmt.Errorf("only pass in 0, 1 or 2, confirm signatures") + } + + var confirmSignature [][65]byte + for _, token := range confirmSigTokens { + token := strings.TrimSpace(token) + sig, err := hex.DecodeString(token) + if err != nil { + return confirmSignatures, err + } + if len(sig) != 65 { + return confirmSignatures, fmt.Errorf("signatures must be of length 65 bytes") + } + + var signature [65]byte + copy(signature[:], sig) + confirmSignature = append(confirmSignature, signature) + } + + confirmSignatures[i] = confirmSignature + } + return confirmSignatures, nil +} + +// parse inputs passed in through flags +// Split will return a slice of at least length 1 +func parseInputs() (inputs []plasma.Position, err error) { + positions := strings.Split(strings.TrimSpace(viper.GetString(positionF)), "::") + if len(positions) == 1 && len(positions[0]) == 0 { + return inputs, err + } + + if len(positions) > 2 { + return inputs, fmt.Errorf("only pass in 1 or 2 positions") + } + + for _, token := range positions { + token = strings.TrimSpace(token) + position, err := plasma.FromPositionString(token) + if err != nil { + return inputs, err + } + inputs = append(inputs, position) + } + + return inputs, nil +} + +// parse the passed in addresses that will be sent to +func parseToAddresses(addresses string) (toAddrs []ethcmn.Address, err error) { + toAddrTokens := strings.Split(strings.TrimSpace(addresses), ",") + if len(toAddrTokens) == 0 || len(toAddrTokens) > 2 { + return toAddrs, fmt.Errorf("1 or 2 outputs must be specified") + } + + for _, token := range toAddrTokens { + token := strings.TrimSpace(token) + if !ethcmn.IsHexAddress(token) { + return toAddrs, fmt.Errorf("invalid address provided. please use hex format") + } + + addr := ethcmn.HexToAddress(token) + if utils.IsZeroAddress(addr) { + return toAddrs, fmt.Errorf("cannot spend to the zero address") + } + toAddrs = append(toAddrs, addr) + } + + return toAddrs, nil +} + +// attempt to retrieve inputs to generate a valid spend transaction +// returns inputs and sum(inputs) - total +func retrieveInputs(ctx context.CLIContext, accs []string, total *big.Int) (inputs []plasma.Position, change *big.Int, err error) { + change = total + // must specifiy inputs if using two accounts + if len(accs) > 1 { + return inputs, change, nil + } + + addr, err := clistore.GetAccount(accs[0]) + if err != nil { + return inputs, change, err + } + + queryPath := fmt.Sprintf("custom/data/info/%s", addr) + res, err := ctx.Query(queryPath, nil) + if err != nil { + return inputs, change, err + } + + var utxos []store.TxOutput + if err := json.Unmarshal(res, &utxos); err != nil { + return inputs, change, err + } + + var optimalChange = total + var position0, position1 plasma.Position + // iterate through utxo's looking for optimal input pairing + // return input pairing if input + input == total + for i, utxo0 := range utxos { + exitted, err := eth.HasTxExited(utxo0.Position) + if err != nil { + return nil, nil, fmt.Errorf("Must connect full eth node or specify inputs using flags. Error encountered: %s", err) + } + if exitted || utxo0.Spent { + continue + } + // check if first utxo satisfies transfer amount + if utxo0.Output.Amount.Cmp(total) == 0 { + inputs = append(inputs, utxo0.Position) + return inputs, big.NewInt(0), nil + } + for k, utxo1 := range utxos { + // do not pair an input with itself + if i == k { + continue + } + + exitted, err := eth.HasTxExited(utxo1.Position) + if err != nil { + return nil, nil, fmt.Errorf("Must connect full eth node or specify inputs using flags. Error encountered: %s", err) + } + if exitted || utxo1.Spent { + continue + } + + // check if only utxo1 satisfies transfer amount + if utxo0.Output.Amount.Cmp(total) == 0 { + inputs = append(inputs, utxo0.Position) + return inputs, big.NewInt(0), nil + } + // check for exact match + sum := new(big.Int).Add(utxo0.Output.Amount, utxo1.Output.Amount) + if sum.Cmp(total) == 0 { + inputs = append(inputs, utxo0.Position) + inputs = append(inputs, utxo1.Position) + return inputs, big.NewInt(0), nil + } + + diff := new(big.Int).Sub(sum, total) + if diff.Sign() == 1 && diff.Cmp(optimalChange) == -1 { + optimalChange = diff + position0 = utxo0.Position + position1 = utxo1.Position + } + + } + } + + // check if a pairing was found + if optimalChange.Cmp(total) == 0 { + return inputs, optimalChange, nil + } + + inputs = append(inputs, position0) + inputs = append(inputs, position1) + return inputs, optimalChange, nil +} diff --git a/client/plasmacli/cmd/version.go b/cmd/plasmacli/subcmd/version.go similarity index 57% rename from client/plasmacli/cmd/version.go rename to cmd/plasmacli/subcmd/version.go index af12120..c161cc5 100644 --- a/client/plasmacli/cmd/version.go +++ b/cmd/plasmacli/subcmd/version.go @@ -1,19 +1,19 @@ -package cmd +package subcmd import ( "fmt" - "github.com/spf13/cobra" ) -func init() { - rootCmd.AddCommand(versionCmd) +// VersionCmd returns the version cmd for plasmacli +func VersionCmd() *cobra.Command { + return versionCmd } var versionCmd = &cobra.Command{ Use: "version", Short: "Print the version number of the plasma client", Run: func(cmd *cobra.Command, args []string) { - fmt.Println("Plasma Client v0.2.0") + fmt.Println("Plasma Client v0.3.0") }, } diff --git a/cmd/plasmad/config/config.go b/cmd/plasmad/config/config.go new file mode 100644 index 0000000..a651bea --- /dev/null +++ b/cmd/plasmad/config/config.go @@ -0,0 +1,105 @@ +package config + +import ( + "bytes" + "github.com/spf13/viper" + cmn "github.com/tendermint/tendermint/libs/common" + "text/template" +) + +const defaultConfigTemplate = `# This is a TOML config file. +# For more information, see https://github.com/toml-lang/toml + +##### ethereum configuration ##### + +# Ethereum plasma contract address +ethereum_plasma_contract_address = "{{ .EthPlasmaContractAddr }}" + +# Node URL for eth client +ethereum_nodeurl = "{{ .EthNodeURL }}" + +# Number of Ethereum blocks until a submitted block header is considered final +ethereum_finality = "{{ .EthBlockFinality }}" + +##### plasma configuration ##### + +# Plasma block commitment rate. i.e 1m30s, 1m, 1h, etc. +block_commitment_rate = "{{ .PlasmaCommitmentRate }}" + +# Boolean specifying if this node is the operator of the plasma contract +is_operator = "{{ .IsOperator }}" + +# Hex encoded private key +# Used to sign eth transactions interacting with the contract +operator_privatekey = "{{ .OperatorPrivateKey }}"` + +// PlasmaConfig is the object representation of config file. It must match +// the above defaultConfigTemplate. +type PlasmaConfig struct { + EthPlasmaContractAddr string `mapstructure:"ethereum_plasma_contract_address"` + EthNodeURL string `mapstructure:"ethereum_nodeurl"` + EthBlockFinality string `mapstructure:"ethereum_finality"` + + IsOperator bool `mapstructure:"is_operator"` + OperatorPrivateKey string `mapstructure:"operator_privatekey"` + PlasmaCommitmentRate string `mapstructure:"block_commitment_rate"` +} + +var configTemplate *template.Template + +func init() { + var err error + tmpl := template.New("plasmaConfigFileTemplate") + if configTemplate, err = tmpl.Parse(defaultConfigTemplate); err != nil { + panic(err) + } +} + +// DefaultPlasmaConfig returns the default plasma daemon configuration +func DefaultPlasmaConfig() PlasmaConfig { + return PlasmaConfig{ + EthPlasmaContractAddr: "", + EthNodeURL: "http://localhost:8545", + EthBlockFinality: "16", + + IsOperator: false, + OperatorPrivateKey: "", + PlasmaCommitmentRate: "1m", + } +} + +// TestPlasmaConfig writes the plasma.toml file used for testing. NodeURL is +// powered by ganache locally. Contract address and private key generated +// deterministically using the "plasma" moniker with ganache. +func TestPlasmaConfig() PlasmaConfig { + return PlasmaConfig{ + EthPlasmaContractAddr: "31E491FC70cDb231774c61B7F46d94699dacE664", + EthNodeURL: "http://localhost:8545", + EthBlockFinality: "0", + + IsOperator: true, + OperatorPrivateKey: "9cd69f009ac86203e54ec50e3686de95ff6126d3b30a19f926a0fe9323c17181", + PlasmaCommitmentRate: "1m", + } +} + +// ParsePlasmaConfigFromViper parses the plasma.toml file and unmarshals it +// into a PlasmaConfig struct. +func ParsePlasmaConfigFromViper() (PlasmaConfig, error) { + config := DefaultPlasmaConfig() + err := viper.Unmarshal(&config) + return config, err +} + +// WritePlasmaConfigFile renders config using the template and writes it to +// configFilePath. +func WritePlasmaConfigFile(configFilePath string, config PlasmaConfig) { + var buffer bytes.Buffer + + if err := configTemplate.Execute(&buffer, &config); err != nil { + panic(err) + } + + // 0600 for owner only read+write permissions + cmn.MustWriteFile(configFilePath, buffer.Bytes(), 0600) +} diff --git a/cmd/plasmad/main.go b/cmd/plasmad/main.go new file mode 100644 index 0000000..532a725 --- /dev/null +++ b/cmd/plasmad/main.go @@ -0,0 +1,72 @@ +package main + +import ( + "github.com/FourthState/plasma-mvp-sidechain/app" + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmad/config" + "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmad/subcmd" + "github.com/cosmos/cosmos-sdk/server" + "github.com/spf13/cobra" + "github.com/spf13/viper" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/cli" + dbm "github.com/tendermint/tendermint/libs/db" + "github.com/tendermint/tendermint/libs/log" + "io" + "os" + "path/filepath" +) + +func main() { + // codec only used for server.AddCommand + cdc := app.MakeCodec() + ctx := server.NewDefaultContext() + + rootCmd := &cobra.Command{ + Use: "plasmad", + Short: "Plasma Daemon (server)", + PersistentPreRunE: persistentPreRunEFn(ctx), + } + rootCmd.AddCommand(subcmd.InitCmd(ctx, cdc)) + server.AddCommands(ctx, cdc, rootCmd, newApp, nil) + + // HomeFlag in tendermint cli will be set to `~/.plasmad` + rootDir := os.ExpandEnv("$HOME/.plasmad") + executor := cli.PrepareBaseCmd(rootCmd, "PD", rootDir) + if err := executor.Execute(); err != nil { + panic(err) + } +} + +// wraps the default cosmos-sdk function with additional logic to handle plasma specific configuration +func persistentPreRunEFn(context *server.Context) func(*cobra.Command, []string) error { + return func(cmd *cobra.Command, args []string) error { + // run sdk/tendermint configuration + if err := server.PersistentPreRunEFn(context)(cmd, args); err != nil { + return err + } + + // custom plasma config + plasmaConfigFilePath := filepath.Join(context.Config.RootDir, "config/plasma.toml") + + if _, err := os.Stat(plasmaConfigFilePath); os.IsNotExist(err) { + plasmaConfig := config.DefaultPlasmaConfig() + config.WritePlasmaConfigFile(plasmaConfigFilePath, plasmaConfig) + } + + // read in plasma.toml from the config directory + viper.SetConfigName("plasma") + err := viper.MergeInConfig() + return err + } +} + +func newApp(logger log.Logger, db dbm.DB, traceStore io.Writer) abci.Application { + plasmaConfig, err := config.ParsePlasmaConfigFromViper() + if err != nil { + panic(err) + } + + return app.NewPlasmaMVPChain(logger, db, traceStore, + app.SetPlasmaOptionsFromConfig(plasmaConfig), + ) +} diff --git a/cmd/plasmad/subcmd/init.go b/cmd/plasmad/subcmd/init.go new file mode 100644 index 0000000..6e358dc --- /dev/null +++ b/cmd/plasmad/subcmd/init.go @@ -0,0 +1,135 @@ +package subcmd + +import ( + "encoding/json" + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/app" + pConfig "github.com/FourthState/plasma-mvp-sidechain/cmd/plasmad/config" + gaiaInit "github.com/cosmos/cosmos-sdk/cmd/gaia/init" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/server" + "github.com/spf13/cobra" + "github.com/spf13/viper" + tmConfig "github.com/tendermint/tendermint/config" + "github.com/tendermint/tendermint/libs/cli" + tmCommon "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/privval" + "os" + "path/filepath" +) + +const ( + flagOverwrite = "overwrite" + flagMoniker = "moniker" + flagChainID = "chainId" + flagTest = "test" +) + +type chainInfo struct { + Moniker string `json:"moniker"` + ChainID string `json:"chain_id"` + NodeID string `json:"node_id"` + AppMessage json.RawMessage `json:"app_message"` +} + +// InitCmd initializes all files for tendermint and application. +func InitCmd(ctx *server.Context, cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "init", + Short: "Initialize private validator, p2p, genesis, and application configuration files", + Long: `Initialize validators's and node's configuration files.`, + Args: cobra.NoArgs, + RunE: func(_ *cobra.Command, _ []string) error { + config := ctx.Config + config.SetRoot(viper.GetString(cli.HomeFlag)) + + overwrite := viper.GetBool(flagOverwrite) + + /* Tendermint configuration */ + + chainID := viper.GetString(flagChainID) + if chainID == "" { + chainID = fmt.Sprintf("test-chain-%v", tmCommon.RandStr(7)) + } + if viper.GetString(flagMoniker) != "" { + config.Moniker = viper.GetString(flagMoniker) + } + + nodeID, _, err := gaiaInit.InitializeNodeValidatorFiles(config) + if err != nil { + return err + } + var appState json.RawMessage + genFile := config.GenesisFile() + if tmCommon.FileExists(genFile) { + if !overwrite { + return fmt.Errorf("genesis.json file already exists: %v", genFile) + } else { + fmt.Printf("overwriting genesis.json...\n") + } + } + // read of create the private key file for this config + var privValidator *privval.FilePV + privValFile := config.PrivValidatorKeyFile() + + if tmCommon.FileExists(privValFile) { + privValidator = privval.LoadFilePV(privValFile, config.PrivValidatorStateFile()) + } else { + privValidator = privval.GenFilePV(privValFile, config.PrivValidatorStateFile()) + privValidator.Save() + } + + valPubKey := privValidator.GetPubKey() + + // create genesis and write to disk + appState, err = codec.MarshalJSONIndent(cdc, app.NewDefaultGenesisState(valPubKey)) + if err != nil { + return err + } + + if err = gaiaInit.ExportGenesisFile(genFile, chainID, nil, appState); err != nil { + return err + } + + // write tendermint and plasma config files to disk + tmConfig.WriteConfigFile(filepath.Join(config.RootDir, "config", "config.toml"), config) + var plasmaConfig pConfig.PlasmaConfig + plasmaPath := filepath.Join(config.RootDir, "config", "plasma.toml") + + if viper.GetBool(flagTest) { + plasmaConfig = pConfig.TestPlasmaConfig() + } else { + plasmaConfig = pConfig.DefaultPlasmaConfig() + } + if tmCommon.FileExists(plasmaPath) { + if !overwrite { + return fmt.Errorf("plasma.toml already exists at '%s'. Use -o flag to overwrite", plasmaPath) + } else { + fmt.Println("overwriting plasma.toml...") + } + } + pConfig.WritePlasmaConfigFile(plasmaPath, plasmaConfig) + + // display chain info + info, err := json.MarshalIndent(chainInfo{ + ChainID: chainID, + Moniker: config.Moniker, + NodeID: nodeID, + AppMessage: appState, + }, "", "\t") + if err != nil { + return err + } + fmt.Printf("%s\n", string(info)) + + return nil + }, + } + + cmd.Flags().String(cli.HomeFlag, os.ExpandEnv("$HOME/.plasmad"), "node's home directory") + cmd.Flags().BoolP(flagOverwrite, "o", false, "overwrite the genesis.json file") + cmd.Flags().BoolP(flagTest, "t", false, "write default testing configuration") + cmd.Flags().String(flagChainID, "", "genesis file chain-id, if left blank will be randomly created") + cmd.Flags().String(flagMoniker, "m", "set the validator's moniker") + return cmd +} diff --git a/contracts/.gitattributes b/contracts/.gitattributes new file mode 100644 index 0000000..52031de --- /dev/null +++ b/contracts/.gitattributes @@ -0,0 +1 @@ +*.sol linguist-language=Solidity diff --git a/contracts/.gitignore b/contracts/.gitignore new file mode 100644 index 0000000..dd5f792 --- /dev/null +++ b/contracts/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +coverage/ +coverage.json +build/ diff --git a/contracts/.solcover.js b/contracts/.solcover.js new file mode 100644 index 0000000..4494f88 --- /dev/null +++ b/contracts/.solcover.js @@ -0,0 +1,3 @@ +module.exports = { + copyNodeModules: true +} diff --git a/contracts/.travis.yml b/contracts/.travis.yml new file mode 100644 index 0000000..4b53b97 --- /dev/null +++ b/contracts/.travis.yml @@ -0,0 +1,16 @@ +language: node_js + +node_js: "node" + +before_script: + - npm install -g truffle@5.0.2 ganache-cli@6.2.5 + - npm install + +script: + - ganache-cli -m "they only media any modify banner suffer pole tag rule creek harvest" > /dev/null & + - sleep 5 + - truffle migrate + - truffle test + +after_script: + - npm run coverage && cat coverage/lcov.info | coveralls diff --git a/contracts/CONTRIBUTING.md b/contracts/CONTRIBUTING.md new file mode 100644 index 0000000..8cd1dd2 --- /dev/null +++ b/contracts/CONTRIBUTING.md @@ -0,0 +1,35 @@ +# Contributing + +Thank you for considering making contributions to Fourth State's Plasma MVP implementation! We welcome contributions from anyone! See the [open issues](https://github.com/FourthState/plasma-mvp-rootchain/issues) for things we need help with! + +Contribute to design discussions and conversation by joining our [Discord Server](https://discord.gg/YTB5A4P). + +## How to get started: + +Fork, then clone the repo: + +If you have ssh keys: +``git clone git@github.com:your-username/plasma-mvp-rootchain`` + +Otherwise: +``git clone https://github.com/your-username/plasma-mvp-rootchain`` + +Install dependencies with: +``npm install`` + +**Note**: requires Solidity 0.4.24 and Truffle 4.1.14 + +Make sure the tests pass: +1. Start ganache-cli: ``ganache-cli -m=plasma_mvp`` +2. Run tests: ``truffle test`` + +Create a branch that is named off the feature you are trying to implement. See these [guidelines](https://nvie.com/posts/a-successful-git-branching-model/) + +Make your changes. Add tests and comment those changes. + +If your tests pass, push to your fork and [submit a pull request](https://github.com/FourthState/plasma-mvp-rootchain/pulls) to the master branch. + +## Proposals: + +If you would like to propose a protocol change, open up an issue. If the reviewers decide the proposed change is in line with the project's aim, then a writeup should also be added to the [research repository](https://github.com/FourthState/plasma-research). It is also advisable to publish the proposed change to [Eth Research](https://ethresear.ch/), so other plasma implementations can benefit from the proposed change. + diff --git a/contracts/LICENSE b/contracts/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/contracts/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/contracts/README.md b/contracts/README.md new file mode 100644 index 0000000..f321ef9 --- /dev/null +++ b/contracts/README.md @@ -0,0 +1,49 @@ +# PLASMA MVP + +[![travis build](https://travis-ci.org/FourthState/plasma-mvp-rootchain.svg?branch=master)](https://travis-ci.org/FourthState/plasma-mvp-rootchain) +[![license](https://img.shields.io/github/license/FourthState/plasma-mvp-rootchain.svg)](https://github.com/FourthState/plasma-mvp-rootchain/blob/master/LICENSE) +[![Coverage Status](https://coveralls.io/repos/github/FourthState/plasma-mvp-rootchain/badge.svg?branch=master)](https://coveralls.io/github/FourthState/plasma-mvp-rootchain?branch=master) + +Implementation of [Minimum Viable Plasma](https://ethresear.ch/t/minimal-viable-plasma/426) + +## Overview +Plasma is a layer 2 scaling solution which conducts transaction processing off chain and allows for only merkle roots of each block to be reported to a root chain. This allows for users to benefit from off chain scaling while still relying on decentralized security. + +The root contract of a Plasma child chain represents an intermediary who can resolve any disputes. The root contract is responsible for maintaining a mapping from block number to merkle root, processing deposits, and processing withdrawals. + +## Root Contract Details +A transaction is encoded in the following form: + +``` +RLP_ENCODE([ + [Blknum1, TxIndex1, Oindex1, DepositNonce1, Input1ConfirmSig, + + Blknum2, TxIndex2, Oindex2, DepositNonce2, Input2ConfirmSig, + + NewOwner, Denom1, NewOwner, Denom2, Fee], + + [Signature1, Signature2] +]) +``` +The signatures are over the hash of the transaction list (first list) signed by the owner of each respective utxo input. + +### Documentation + +See our [documentation](https://github.com/FourthState/plasma-mvp-rootchain/blob/master/docs/rootchainFunctions.md) for a more detailed description of the smart contract functions. + +### Testing +1. ``git clone https://github.com/fourthstate/plasma-mvp-rootchain`` +2. ``cd plasma-mvp-rootchain`` +3. ``npm install`` +4. ``npm install -g truffle ganache-cli`` // if not installed already +5. ``ganache-cli`` // run as a background process +6. ``npm test`` + +### Running +The first migration file `1_initial_migration` deploys the `PriorityQueue` library and links it to the `RootChain` contract, while the second one `2_deploy_rootchain` finally makes the deployment. Ethereum requires libraries to already be deployed prior to be used by other contracts. + +If you encounter problems, make sure your local test rpc (e.g. [ganache](https://github.com/trufflesuite/ganache-core)) has the same network id as the contract's json from the `build` folder. + +### Contributing + +See our [contribution guidelines](https://github.com/FourthState/plasma-mvp-rootchain/blob/master/CONTRIBUTING.md). Join our [Discord Server](https://discord.gg/YTB5A4P). diff --git a/contracts/contracts/Migrations.sol b/contracts/contracts/Migrations.sol new file mode 100644 index 0000000..89b4d5c --- /dev/null +++ b/contracts/contracts/Migrations.sol @@ -0,0 +1,23 @@ +pragma solidity ^0.5.0; + +contract Migrations { + address public owner; + uint public last_completed_migration; + + modifier restricted() { + if (msg.sender == owner) _; + } + + constructor() public { + owner = msg.sender; + } + + function setCompleted(uint completed) public restricted { + last_completed_migration = completed; + } + + function upgrade(address new_address) public restricted { + Migrations upgraded = Migrations(new_address); + upgraded.setCompleted(last_completed_migration); + } +} diff --git a/contracts/contracts/PlasmaMVP.sol b/contracts/contracts/PlasmaMVP.sol new file mode 100644 index 0000000..5c78382 --- /dev/null +++ b/contracts/contracts/PlasmaMVP.sol @@ -0,0 +1,616 @@ +pragma solidity ^0.5.0; + +// external modules +import "solidity-rlp/contracts/RLPReader.sol"; + +// libraries +import "./libraries/BytesUtil.sol"; +import "./libraries/SafeMath.sol"; +import "./libraries/ECDSA.sol"; +import "./libraries/TMSimpleMerkleTree.sol"; +import "./libraries/MinPriorityQueue.sol"; + +contract PlasmaMVP { + using MinPriorityQueue for uint256[]; + using BytesUtil for bytes; + using RLPReader for bytes; + using RLPReader for RLPReader.RLPItem; + using SafeMath for uint256; + using TMSimpleMerkleTree for bytes32; + using ECDSA for bytes32; + + /* + * Events + */ + + event ChangedOperator(address oldOperator, address newOperator); + + event AddedToBalances(address owner, uint256 amount); + event BlockSubmitted(bytes32 header, uint256 blockNumber, uint256 numTxns, uint256 feeAmount); + event Deposit(address depositor, uint256 amount, uint256 depositNonce, uint256 ethBlockNum); + + event StartedTransactionExit(uint256[3] position, address owner, uint256 amount, bytes confirmSignatures, uint256 committedFee); + event StartedDepositExit(uint256 nonce, address owner, uint256 amount, uint256 committedFee); + + event ChallengedExit(uint256[4] position, address owner, uint256 amount); + event FinalizedExit(uint256[4] position, address owner, uint256 amount); + + /* + * Storage + */ + + address public operator; + + uint256 public lastCommittedBlock; + uint256 public depositNonce; + mapping(uint256 => plasmaBlock) public plasmaChain; + mapping(uint256 => depositStruct) public deposits; + struct plasmaBlock{ + bytes32 header; + uint256 numTxns; + uint256 feeAmount; + uint256 createdAt; + uint256 ethBlockNum; + } + struct depositStruct { + address owner; + uint256 amount; + uint256 createdAt; + uint256 ethBlockNum; + } + + // exits + uint256 public minExitBond; + uint256[] public txExitQueue; + uint256[] public depositExitQueue; + mapping(uint256 => exit) public txExits; + mapping(uint256 => exit) public depositExits; + enum ExitState { NonExistent, Pending, Challenged, Finalized } + struct exit { + uint256 amount; + uint256 committedFee; + uint256 createdAt; + uint256 ethBlockNum; + address owner; + uint256[4] position; // (blkNum, txIndex, outputIndex, depositNonce) + ExitState state; // default value is `NonExistent` + } + + // funds + mapping(address => uint256) public balances; + uint256 public totalWithdrawBalance; + + // constants + uint256 constant txIndexFactor = 10; + uint256 constant blockIndexFactor = 1000000; + uint256 constant lastBlockNum = 2**109; + uint256 constant feeIndex = 2**16-1; + + /** Modifiers **/ + modifier isBonded() + { + require(msg.value >= minExitBond); + if (msg.value > minExitBond) { + uint256 excess = msg.value.sub(minExitBond); + balances[msg.sender] = balances[msg.sender].add(excess); + totalWithdrawBalance = totalWithdrawBalance.add(excess); + } + + _; + } + + modifier onlyOperator() + { + require(msg.sender == operator); + _; + } + + function changeOperator(address newOperator) + public + onlyOperator + { + require(newOperator != address(0)); + + emit ChangedOperator(operator, newOperator); + operator = newOperator; + } + + constructor() public + { + operator = msg.sender; + + lastCommittedBlock = 0; + depositNonce = 1; + minExitBond = 200000; + } + + // @param blocks 32 byte merkle headers appended in ascending order + // @param txnsPerBlock number of transactions per block + // @param feesPerBlock amount of fees the validator has collected per block + // @param blockNum the block number of the first header + // @notice each block is capped at 2**16-1 transactions + function submitBlock(bytes32[] memory headers, uint256[] memory txnsPerBlock, uint256[] memory feePerBlock, uint256 blockNum) + public + onlyOperator + { + require(blockNum == lastCommittedBlock.add(1)); + require(headers.length == txnsPerBlock.length && txnsPerBlock.length == feePerBlock.length); + + for (uint i = 0; i < headers.length && lastCommittedBlock <= lastBlockNum; i++) { + require(headers[i] != bytes32(0) && txnsPerBlock[i] > 0 && txnsPerBlock[i] < feeIndex); + + lastCommittedBlock = lastCommittedBlock.add(1); + plasmaChain[lastCommittedBlock] = plasmaBlock({ + header: headers[i], + numTxns: txnsPerBlock[i], + feeAmount: feePerBlock[i], + createdAt: block.timestamp, + ethBlockNum: block.number + }); + + emit BlockSubmitted(headers[i], lastCommittedBlock, txnsPerBlock[i], feePerBlock[i]); + } + } + + // @param owner owner of this deposit + function deposit(address owner) + public + payable + { + deposits[depositNonce] = depositStruct(owner, msg.value, block.timestamp, block.number); + emit Deposit(owner, msg.value, depositNonce, block.number); + + depositNonce = depositNonce.add(uint256(1)); + } + + // @param depositNonce the nonce of the specific deposit + function startDepositExit(uint256 nonce, uint256 committedFee) + public + payable + isBonded + { + require(deposits[nonce].owner == msg.sender); + require(deposits[nonce].amount > committedFee); + require(depositExits[nonce].state == ExitState.NonExistent); + + address owner = deposits[nonce].owner; + uint256 amount = deposits[nonce].amount; + uint256 priority = block.timestamp << 128 | nonce; + depositExitQueue.insert(priority); + depositExits[nonce] = exit({ + owner: owner, + amount: amount, + committedFee: committedFee, + createdAt: block.timestamp, + ethBlockNum: block.number, + position: [0,0,0,nonce], + state: ExitState.Pending + }); + + emit StartedDepositExit(nonce, owner, amount, committedFee); + } + + // Transaction encoding: + // [[Blknum1, TxIndex1, Oindex1, DepositNonce1, Input1ConfirmSig, + // Blknum2, TxIndex2, Oindex2, DepositNonce2, Input2ConfirmSig, + // NewOwner, Denom1, NewOwner, Denom2, Fee], + // [Signature1, Signature2]] + // + // All integers are padded to 32 bytes. Input's confirm signatures are 130 bytes for each input. + // Zero bytes if unapplicable (deposit/fee inputs) Signatures are 65 bytes in length + // + // @param txBytes rlp encoded transaction + // @notice this function will revert if the txBytes are malformed + function decodeTransaction(bytes memory txBytes) + internal + pure + returns (RLPReader.RLPItem[] memory txList, RLPReader.RLPItem[] memory sigList, bytes32 txHash) + { + // entire byte length of the rlp encoded transaction. + require(txBytes.length == 811); + + RLPReader.RLPItem[] memory spendMsg = txBytes.toRlpItem().toList(); + require(spendMsg.length == 2); + + txList = spendMsg[0].toList(); + require(txList.length == 15); + + sigList = spendMsg[1].toList(); + require(sigList.length == 2); + + // bytes the signatures are over + txHash = keccak256(spendMsg[0].toRlpBytes()); + } + + + // @param txPos location of the transaction [blkNum, txIndex, outputIndex] + // @param txBytes transaction bytes containing the exiting output + // @param proof merkle proof of inclusion in the plasma chain + // @param confSig0 confirm signatures sent by the owners of the first input acknowledging the spend. + // @param confSig1 confirm signatures sent by the owners of the second input acknowledging the spend (if applicable). + // @notice `confirmSignatures` and `ConfirmSig0`/`ConfirmSig1` are unrelated to each other. + // @notice `confirmSignatures` is either 65 or 130 bytes in length dependent on if a second input is present + // @notice `confirmSignatures` should be empty if the output trying to be exited is a fee output + function startTransactionExit(uint256[3] memory txPos, bytes memory txBytes, bytes memory proof, bytes memory confirmSignatures, uint256 committedFee) + public + payable + isBonded + { + require(txPos[1] < feeIndex); + uint256 position = calcPosition(txPos); + require(txExits[position].state == ExitState.NonExistent); + + uint256 amount = startTransactionExitHelper(txPos, txBytes, proof, confirmSignatures); + require(amount > committedFee); + + // calculate the priority of the transaction taking into account the withdrawal delay attack + // withdrawal delay attack: https://github.com/FourthState/plasma-mvp-rootchain/issues/42 + uint256 createdAt = plasmaChain[txPos[0]].createdAt; + txExitQueue.insert(SafeMath.max(createdAt.add(1 weeks), block.timestamp) << 128 | position); + + // write exit to storage + txExits[position] = exit({ + owner: msg.sender, + amount: amount, + committedFee: committedFee, + createdAt: block.timestamp, + ethBlockNum: block.number, + position: [txPos[0], txPos[1], txPos[2], 0], + state: ExitState.Pending + }); + + emit StartedTransactionExit(txPos, msg.sender, amount, confirmSignatures, committedFee); + } + + // @returns amount of the exiting transaction + // @notice the purpose of this helper was to work around the capped evm stack frame + function startTransactionExitHelper(uint256[3] memory txPos, bytes memory txBytes, bytes memory proof, bytes memory confirmSignatures) + private + view + returns (uint256) + { + bytes32 txHash; + RLPReader.RLPItem[] memory txList; + RLPReader.RLPItem[] memory sigList; + (txList, sigList, txHash) = decodeTransaction(txBytes); + + uint base = txPos[2].mul(2); + require(msg.sender == txList[base.add(10)].toAddress()); + + plasmaBlock memory blk = plasmaChain[txPos[0]]; + + // Validation + + bytes32 merkleHash = sha256(txBytes); + require(merkleHash.checkMembership(txPos[1], blk.header, proof, blk.numTxns)); + + address recoveredAddress; + bytes32 confirmationHash = sha256(abi.encodePacked(merkleHash, blk.header)); + + bytes memory sig = sigList[0].toBytes(); + require(sig.length == 65 && confirmSignatures.length % 65 == 0 && confirmSignatures.length > 0 && confirmSignatures.length <= 130); + recoveredAddress = confirmationHash.recover(confirmSignatures.slice(0, 65)); + require(recoveredAddress != address(0) && recoveredAddress == txHash.recover(sig)); + if (txList[5].toUintStrict() > 0 || txList[8].toUintStrict() > 0) { // existence of a second input + sig = sigList[1].toBytes(); + require(sig.length == 65 && confirmSignatures.length == 130); + recoveredAddress = confirmationHash.recover(confirmSignatures.slice(65, 65)); + require(recoveredAddress != address(0) && recoveredAddress == txHash.recover(sig)); + } + + // check that the UTXO's two direct inputs have not been previously exited + require(validateTransactionExitInputs(txList)); + + return txList[base.add(11)].toUintStrict(); + } + + // For any attempted exit of an UTXO, validate that the UTXO's two inputs have not + // been previously exited or are currently pending an exit. + function validateTransactionExitInputs(RLPReader.RLPItem[] memory txList) + private + view + returns (bool) + { + for (uint256 i = 0; i < 2; i++) { + ExitState state; + uint256 base = uint256(5).mul(i); + uint depositNonce_ = txList[base.add(3)].toUintStrict(); + if (depositNonce_ == 0) { + uint256 blkNum = txList[base].toUintStrict(); + uint256 txIndex = txList[base.add(1)].toUintStrict(); + uint256 outputIndex = txList[base.add(2)].toUintStrict(); + uint256 position = calcPosition([blkNum, txIndex, outputIndex]); + state = txExits[position].state; + } else + state = depositExits[depositNonce_].state; + + if (state != ExitState.NonExistent && state != ExitState.Challenged) + return false; + } + + return true; + } + + // Validator of any block can call this function to exit the fees collected + // for that particular block. The fee exit is added to exit queue with the lowest priority for that block. + // In case of the fee UTXO already spent, anyone can challenge the fee exit by providing + // the spend of the fee UTXO. + // @param blockNumber the block for which the validator wants to exit fees + function startFeeExit(uint256 blockNumber, uint256 committedFee) + public + payable + onlyOperator + isBonded + { + plasmaBlock memory blk = plasmaChain[blockNumber]; + require(blk.header != bytes32(0)); + + uint256 feeAmount = blk.feeAmount; + + // nonzero fee and prevent and greater than the committed fee if spent. + // default value for a fee amount is zero. Will revert if a block for + // this number has not been committed + require(feeAmount > committedFee); + + // a fee UTXO has explicitly defined position [blockNumber, 2**16 - 1, 0] + uint256 position = calcPosition([blockNumber, feeIndex, 0]); + require(txExits[position].state == ExitState.NonExistent); + + txExitQueue.insert(SafeMath.max(blk.createdAt.add(1 weeks), block.timestamp) << 128 | position); + + txExits[position] = exit({ + owner: msg.sender, + amount: feeAmount, + committedFee: committedFee, + createdAt: block.timestamp, + ethBlockNum: block.number, + position: [blockNumber, feeIndex, 0, 0], + state: ExitState.Pending + }); + + // pass in empty bytes for confirmSignatures for StartedTransactionExit event. + emit StartedTransactionExit([blockNumber, feeIndex, 0], operator, feeAmount, "", 0); +} + + // @param exitingTxPos position of the invalid exiting transaction [blkNum, txIndex, outputIndex] + // @param challengingTxPos position of the challenging transaction [blkNum, txIndex] + // @param txBytes raw transaction bytes of the challenging transaction + // @param proof proof of inclusion for this merkle hash + // @param confirmSignature signature used to invalidate the invalid exit. Signature is over (merkleHash, block header) + // @notice The operator can challenge an exit which commits an invalid fee by simply passing in empty bytes for confirm signature as they are not needed. + // The committed fee is checked againt the challenging tx bytes + function challengeExit(uint256[4] memory exitingTxPos, uint256[2] memory challengingTxPos, bytes memory txBytes, bytes memory proof, bytes memory confirmSignature) + public + { + bytes32 txHash; + RLPReader.RLPItem[] memory txList; + RLPReader.RLPItem[] memory sigList; + (txList, sigList, txHash) = decodeTransaction(txBytes); + + // `challengingTxPos` is sequentially after `exitingTxPos` + require(exitingTxPos[0] < challengingTxPos[0] || (exitingTxPos[0] == challengingTxPos[0] && exitingTxPos[1] < challengingTxPos[1])); + + // must be a direct spend + bool firstInput = exitingTxPos[0] == txList[0].toUintStrict() && exitingTxPos[1] == txList[1].toUintStrict() && exitingTxPos[2] == txList[2].toUintStrict() && exitingTxPos[3] == txList[3].toUintStrict(); + require(firstInput || exitingTxPos[0] == txList[5].toUintStrict() && exitingTxPos[1] == txList[6].toUintStrict() && exitingTxPos[2] == txList[7].toUintStrict() && exitingTxPos[3] == txList[8].toUintStrict()); + + // transaction to be challenged should have a pending exit + exit storage exit_ = exitingTxPos[3] == 0 ? + txExits[calcPosition([exitingTxPos[0], exitingTxPos[1], exitingTxPos[2]])] : depositExits[exitingTxPos[3]]; + require(exit_.state == ExitState.Pending); + + plasmaBlock memory blk = plasmaChain[challengingTxPos[0]]; + + bytes32 merkleHash = sha256(txBytes); + require(blk.header != bytes32(0) && merkleHash.checkMembership(challengingTxPos[1], blk.header, proof, blk.numTxns)); + + address recoveredAddress; + // we check for confirm signatures if: + // The exiting tx is a first input and commits the correct fee + // OR + // The exiting tx is the second input in the challenging transaction + // + // If this challenge was a fee mismatch, then we check the first transaction signature + // to prevent the operator from forging invalid inclusions + // + // For a fee mismatch, the state becomes `NonExistent` so that the exit can be reopened. + // Otherwise, `Challenged` so that the exit can never be opened. + if (firstInput && exit_.committedFee != txList[14].toUintStrict()) { + bytes memory sig = sigList[0].toBytes(); + recoveredAddress = txHash.recover(sig); + require(sig.length == 65 && recoveredAddress != address(0) && exit_.owner == recoveredAddress); + + exit_.state = ExitState.NonExistent; + } else { + bytes32 confirmationHash = sha256(abi.encodePacked(merkleHash, blk.header)); + recoveredAddress = confirmationHash.recover(confirmSignature); + require(confirmSignature.length == 65 && recoveredAddress != address(0) && exit_.owner == recoveredAddress); + + exit_.state = ExitState.Challenged; + } + + // exit successfully challenged. Award the sender with the bond + balances[msg.sender] = balances[msg.sender].add(minExitBond); + totalWithdrawBalance = totalWithdrawBalance.add(minExitBond); + emit AddedToBalances(msg.sender, minExitBond); + + emit ChallengedExit(exit_.position, exit_.owner, exit_.amount - exit_.committedFee); + } + + function finalizeDepositExits() public { finalize(depositExitQueue, true); } + function finalizeTransactionExits() public { finalize(txExitQueue, false); } + + // Finalizes exits by iterating through either the depositExitQueue or txExitQueue. + // Users can determine the number of exits they're willing to process by varying + // the amount of gas allow finalize*Exits() to process. + // Each transaction takes < 80000 gas to process. + function finalize(uint256[] storage queue, bool isDeposits) + private + { + if (queue.length == 0) return; + + // retrieve the lowest priority and the appropriate exit struct + uint256 priority = queue[0]; + exit memory currentExit; + uint256 position; + // retrieve the right 128 bits from the priority to obtain the position + assembly { + position := and(priority, div(not(0x0), exp(256, 16))) + } + + currentExit = isDeposits ? depositExits[position] : txExits[position]; + + /* + * Conditions: + * 1. Exits exist + * 2. Exits must be a week old + * 3. Funds must exist for the exit to withdraw + */ + uint256 amountToAdd; + uint256 challengePeriod = isDeposits ? 5 days : 1 weeks; + while (block.timestamp.sub(currentExit.createdAt) > challengePeriod && + plasmaChainBalance() > 0 && + gasleft() > 80000) { + + // skip currentExit if it is not in 'started/pending' state. + if (currentExit.state != ExitState.Pending) { + queue.delMin(); + } else { + // reimburse the bond but remove fee allocated for the operator + amountToAdd = currentExit.amount.add(minExitBond).sub(currentExit.committedFee); + + balances[currentExit.owner] = balances[currentExit.owner].add(amountToAdd); + totalWithdrawBalance = totalWithdrawBalance.add(amountToAdd); + + if (isDeposits) + depositExits[position].state = ExitState.Finalized; + else + txExits[position].state = ExitState.Finalized; + + emit FinalizedExit(currentExit.position, currentExit.owner, amountToAdd); + emit AddedToBalances(currentExit.owner, amountToAdd); + + // move onto the next oldest exit + queue.delMin(); + } + + if (queue.length == 0) { + return; + } + + // move onto the next oldest exit + priority = queue[0]; + + // retrieve the right 128 bits from the priority to obtain the position + assembly { + position := and(priority, div(not(0x0), exp(256, 16))) + } + + currentExit = isDeposits ? depositExits[position] : txExits[position]; + } + } + + // @notice will revert if the output index is out of bounds + function calcPosition(uint256[3] memory txPos) + private + view + returns (uint256) + { + require(validatePostion([txPos[0], txPos[1], txPos[2], 0])); + + uint256 position = txPos[0].mul(blockIndexFactor).add(txPos[1].mul(txIndexFactor)).add(txPos[2]); + require(position <= 2**128-1); // check for an overflow + + return position; + } + + function validatePostion(uint256[4] memory position) + private + view + returns (bool) + { + uint256 blkNum = position[0]; + uint256 txIndex = position[1]; + uint256 oIndex = position[2]; + uint256 depNonce = position[3]; + + if (blkNum > 0) { // utxo input + // uncommitted block + if (blkNum > lastCommittedBlock) + return false; + // txIndex out of bounds for the block + if (txIndex >= plasmaChain[blkNum].numTxns && txIndex != feeIndex) + return false; + // fee input must have a zero output index + if (txIndex == feeIndex && oIndex > 0) + return false; + // deposit nonce must be zero + if (depNonce > 0) + return false; + // only two outputs + if (oIndex > 1) + return false; + } else { // deposit or fee input + // deposit input must be zero'd output position + // `blkNum` is not checked as it will fail above + if (depNonce > 0 && (txIndex > 0 || oIndex > 0)) + return false; + } + + return true; + } + + function withdraw() + public + returns (uint256) + { + if (balances[msg.sender] == 0) { + return 0; + } + + uint256 transferAmount = balances[msg.sender]; + delete balances[msg.sender]; + totalWithdrawBalance = totalWithdrawBalance.sub(transferAmount); + + // will revert the above deletion if it fails + msg.sender.transfer(transferAmount); + return transferAmount; + } + + /* + * Getters + */ + + function plasmaChainBalance() + public + view + returns (uint) + { + // takes into accounts the failed withdrawals + return address(this).balance - totalWithdrawBalance; + } + + function balanceOf(address _address) + public + view + returns (uint256) + { + return balances[_address]; + } + + function txQueueLength() + public + view + returns (uint) + { + return txExitQueue.length; + } + + function depositQueueLength() + public + view + returns (uint) + { + return depositExitQueue.length; + } +} diff --git a/contracts/contracts/libraries/BytesUtil.sol b/contracts/contracts/libraries/BytesUtil.sol new file mode 100644 index 0000000..ca1b828 --- /dev/null +++ b/contracts/contracts/libraries/BytesUtil.sol @@ -0,0 +1,53 @@ +pragma solidity ^0.5.0; + +library BytesUtil { + uint8 constant WORD_SIZE = 32; + + // @param _bytes raw bytes that needs to be slices + // @param start start of the slice relative to `_bytes` + // @param len length of the sliced byte array + function slice(bytes memory _bytes, uint start, uint len) + internal + pure + returns (bytes memory) + { + require(_bytes.length - start >= len); + + if (_bytes.length == len) + return _bytes; + + bytes memory result; + uint src; + uint dest; + assembly { + // memory & free memory pointer + result := mload(0x40) + mstore(result, len) // store the size in the prefix + mstore(0x40, add(result, and(add(add(0x20, len), 0x1f), not(0x1f)))) // padding + + // pointers + src := add(start, add(0x20, _bytes)) + dest := add(0x20, result) + } + + // copy as many word sizes as possible + for(; len >= WORD_SIZE; len -= WORD_SIZE) { + assembly { + mstore(dest, mload(src)) + } + + src += WORD_SIZE; + dest += WORD_SIZE; + } + + // copy remaining bytes + uint mask = 256 ** (WORD_SIZE - len) - 1; + assembly { + let srcpart := and(mload(src), not(mask)) // zero out src + let destpart := and(mload(dest), mask) // retrieve the bytes + mstore(dest, or(destpart, srcpart)) + } + + return result; + } +} diff --git a/contracts/contracts/libraries/BytesUtil_Test.sol b/contracts/contracts/libraries/BytesUtil_Test.sol new file mode 100644 index 0000000..aa44433 --- /dev/null +++ b/contracts/contracts/libraries/BytesUtil_Test.sol @@ -0,0 +1,9 @@ +pragma solidity ^0.5.0; + +import "./BytesUtil.sol"; + +contract BytesUtil_Test { + using BytesUtil for bytes; + + function slice(bytes memory a, uint start, uint len) public pure returns (bytes memory) { return a.slice(start, len); } +} diff --git a/contracts/contracts/libraries/ECDSA.sol b/contracts/contracts/libraries/ECDSA.sol new file mode 100644 index 0000000..6c5786e --- /dev/null +++ b/contracts/contracts/libraries/ECDSA.sol @@ -0,0 +1,45 @@ +pragma solidity ^0.5.0; + +library ECDSA { + /** + * @dev Recover signer address from a message by using their signature + * @param hash bytes32 message, the hash is the signed message. What is recovered is the signer address. + * @param signature bytes signature, the signature is generated using web3.eth.sign() + */ + function recover(bytes32 hash, bytes memory signature) internal pure returns (address) { + bytes32 r; + bytes32 s; + uint8 v; + + // prefix the hash with an ethereum signed message + hash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); + + // Check the signature length + if (signature.length != 65) { + return (address(0)); + } + + // Divide the signature in r, s and v variables + // ecrecover takes the signature parameters, and the only way to get them + // currently is to use assembly. + // solium-disable-next-line security/no-inline-assembly + assembly { + r := mload(add(signature, 0x20)) + s := mload(add(signature, 0x40)) + v := byte(0, mload(add(signature, 0x60))) + } + + // Version of signature should be 27 or 28, but 0 and 1 are also possible versions + if (v < 27) { + v += 27; + } + + // If the version is correct return the signer address + if (v != 27 && v != 28) { + return (address(0)); + } else { + // solium-disable-next-line arg-overflow + return ecrecover(hash, v, r, s); + } + } +} diff --git a/contracts/contracts/libraries/MinPriorityQueue.sol b/contracts/contracts/libraries/MinPriorityQueue.sol new file mode 100644 index 0000000..966e579 --- /dev/null +++ b/contracts/contracts/libraries/MinPriorityQueue.sol @@ -0,0 +1,85 @@ +pragma solidity ^0.5.0; + +// external modules +import "./SafeMath.sol"; + +// MinPriorityQueue for only positive integers +library MinPriorityQueue { + using SafeMath for uint256; + + function insert(uint256[] storage heapList, uint256 k) + internal + { + heapList.push(k); + if (heapList.length > 1) + percUp(heapList, heapList.length.sub(1)); + } + + function delMin(uint256[] storage heapList) + internal + returns (uint256) + { + require(heapList.length > 0); + + uint256 min = heapList[0]; + + // move the last element to the front + heapList[0] = heapList[heapList.length.sub(1)]; + delete heapList[heapList.length.sub(1)]; + heapList.length = heapList.length.sub(1); + + if (heapList.length > 1) { + percDown(heapList, 0); + } + + return min; + } + + function minChild(uint256[] storage heapList, uint256 i) + private + view + returns (uint256) + { + uint lChild = i.mul(2).add(1); + uint rChild = i.mul(2).add(2); + + if (rChild > heapList.length.sub(1) || heapList[lChild] < heapList[rChild]) + return lChild; + else + return rChild; + } + + function percUp(uint256[] storage heapList, uint256 i) + private + { + uint256 position = i; + uint256 value = heapList[i]; + + // continue to percolate up while smaller than the parent + while (i != 0 && value < heapList[i.sub(1).div(2)]) { + heapList[i] = heapList[i.sub(1).div(2)]; + i = i.sub(1).div(2); + } + + // place the value in the correct parent + if (position != i) heapList[i] = value; + } + + function percDown(uint256[] storage heapList, uint256 i) + private + { + uint position = i; + uint value = heapList[i]; + + // continue to percolate down while larger than the child + uint child = minChild(heapList, i); + while(child < heapList.length && value > heapList[child]) { + heapList[i] = heapList[child]; + i = child; + child = minChild(heapList, i); + } + + // place value in the correct child + if (position != i) heapList[i] = value; + } +} diff --git a/contracts/contracts/libraries/MinPriorityQueue_Test.sol b/contracts/contracts/libraries/MinPriorityQueue_Test.sol new file mode 100644 index 0000000..504055c --- /dev/null +++ b/contracts/contracts/libraries/MinPriorityQueue_Test.sol @@ -0,0 +1,18 @@ +pragma solidity ^0.5.0; + +import "./MinPriorityQueue.sol"; + +// Purpose of this contract is to forward calls to the library for testing +contract MinPriorityQueue_Test { + using MinPriorityQueue for uint256[]; + + uint256[] heapList; + + function insert(uint256 k) public { heapList.insert(k); } + function delMin() public { heapList.delMin(); } + function currentSize() public view returns (uint256) { return heapList.length; } + function getMin() public view returns (uint256) { + require(heapList.length != 0, "empty queue"); + return heapList[0]; + } +} diff --git a/contracts/contracts/libraries/SafeMath.sol b/contracts/contracts/libraries/SafeMath.sol new file mode 100644 index 0000000..ba9fd33 --- /dev/null +++ b/contracts/contracts/libraries/SafeMath.sol @@ -0,0 +1,61 @@ +pragma solidity ^0.5.0; + +// Openzeppelin-soldity SafeMath contract will only the neccessary functions +library SafeMath { + /** + * @dev Subtracts two unsigned integers, reverts on overflow (i.e. if subtrahend is greater than minuend). + */ + function sub(uint256 a, uint256 b) internal pure returns (uint256) { + require(b <= a); + uint256 c = a - b; + + return c; + } + + /** + * @dev Adds two signed integers, reverts on overflow. + */ + function add(uint256 a, uint256 b) internal pure returns (uint256) { + uint256 c = a + b; + require((b >= 0 && c >= a) || (b < 0 && c < a)); + + return c; + } + + /** + * @dev Multiplies two unsigned integers, reverts on overflow. + */ + function mul(uint256 a, uint256 b) internal pure returns (uint256) { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522 + if (a == 0) { + return 0; + } + + uint256 c = a * b; + require(c / a == b); + + return c; + } + + /** + * @dev Integer division of two unsigned integers truncating the quotient, reverts on division by zero. + */ + function div(uint256 a, uint256 b) internal pure returns (uint256) { + // Solidity only automatically asserts when dividing by 0 + require(b > 0); + uint256 c = a / b; + // assert(a == b * c + a % b); // There is no case in which this doesn't hold + + return c; + } + + function max(uint256 a, uint256 b) + internal + pure + returns (uint256) + { + return a >= b ? a : b; + } +} diff --git a/contracts/contracts/libraries/TMSimpleMerkleTree.sol b/contracts/contracts/libraries/TMSimpleMerkleTree.sol new file mode 100644 index 0000000..503140f --- /dev/null +++ b/contracts/contracts/libraries/TMSimpleMerkleTree.sol @@ -0,0 +1,71 @@ +pragma solidity ^0.5.0; + +import "./BytesUtil.sol"; + +// from https://tendermint.com/docs/spec/blockchain/encoding.html#merkle-trees +library TMSimpleMerkleTree { + using BytesUtil for bytes; + + // @param leaf a leaf of the tree + // @param index position of this leaf in the tree that is zero indexed + // @param rootHash block header of the merkle tree + // @param proof sequence of 32-byte hashes from the leaf up to, but excluding, the root + // @paramt total total # of leafs in the tree + function checkMembership(bytes32 leaf, uint256 index, bytes32 rootHash, bytes memory proof, uint256 total) + internal + pure + returns (bool) + { + // variable size Merkle tree, but proof must consist of 32-byte hashes + require(proof.length % 32 == 0); // incorrect proof length + + bytes32 computedHash = computeHashFromAunts(index, total, leaf, proof); + return computedHash == rootHash; + } + + // helper function as described in the tendermint docs + function computeHashFromAunts(uint256 index, uint256 total, bytes32 leaf, bytes memory innerHashes) + private + pure + returns (bytes32) + { + require(index < total); // index must be within bound of the # of leave + require(total > 0); // must have one leaf node + + if (total == 1) { + require(innerHashes.length == 0); // 1 txn has no proof + return leaf; + } + require(innerHashes.length != 0); // >1 txns should have a proof + + uint256 numLeft = (total + 1) / 2; + bytes32 proofElement; + + // prepend 0x20 byte literal to hashes + // tendermint prefixes intermediate hashes with 0x20 bytes literals + // before hashing them. + bytes memory b = new bytes(1); + assembly { + let memPtr := add(b, 0x20) + mstore8(memPtr, 0x20) + } + + uint innerHashesMemOffset = innerHashes.length - 32; + if (index < numLeft) { + bytes32 leftHash = computeHashFromAunts(index, numLeft, leaf, innerHashes.slice(0, innerHashes.length - 32)); + assembly { + // get the last 32-byte hash from innerHashes array + proofElement := mload(add(add(innerHashes, 0x20), innerHashesMemOffset)) + } + + return sha256(abi.encodePacked(b, leftHash, b, proofElement)); + } else { + bytes32 rightHash = computeHashFromAunts(index-numLeft, total-numLeft, leaf, innerHashes.slice(0, innerHashes.length - 32)); + assembly { + // get the last 32-byte hash from innerHashes array + proofElement := mload(add(add(innerHashes, 0x20), innerHashesMemOffset)) + } + return sha256(abi.encodePacked(b, proofElement, b, rightHash)); + } + } +} diff --git a/contracts/contracts/libraries/TMSimpleMerkleTree_Test.sol b/contracts/contracts/libraries/TMSimpleMerkleTree_Test.sol new file mode 100644 index 0000000..aa59c5d --- /dev/null +++ b/contracts/contracts/libraries/TMSimpleMerkleTree_Test.sol @@ -0,0 +1,15 @@ +pragma solidity ^0.5.0; + +import "./TMSimpleMerkleTree.sol"; + +contract TMSimpleMerkleTree_Test { + using TMSimpleMerkleTree for bytes32; + + function checkMembership(bytes32 leaf, uint256 index, bytes32 rootHash, bytes memory proof, uint256 total) + public + pure + returns (bool) + { + return leaf.checkMembership(index, rootHash, proof, total); + } +} diff --git a/contracts/docs/plasmaMVPFunctions.md b/contracts/docs/plasmaMVPFunctions.md new file mode 100644 index 0000000..ce1fc54 --- /dev/null +++ b/contracts/docs/plasmaMVPFunctions.md @@ -0,0 +1,139 @@ +# PlasmaMVP Documentation + +The transaction bytes, `txBytes`, in the contract follow the convention: +``` +RLP_ENCODE ([ + [Blknum1, TxIndex1, Oindex1, DepositNonce1, Owner1, Input1ConfirmSig, + + Blknum2, TxIndex2, Oindex2, DepositNonce2, Owner2, Input2ConfirmSig, + + NewOwner, Denom1, NewOwner, Denom2, Fee], + + [Signature1, Signature2] +]) +``` +```solidity +function submitBlock(bytes32[] blocks, uint256[] txnsPerBlock, uint256[] feesPerBlock, uint256 blockNum) +``` +The validator submits an array of block headers in ascending order. Each block can be of variable block size(capped at 2^16 txns per block). The total number of transactions per block must be passed in through `txnsPerBlock`. The amount of transaction fees collected by the validator per block must be passed in through `feesPerBlock`. +`blockNum` must be the intended block number of the first header in this call. Ordering is enforced on each call. `blockNum == lastCommittedBlock + 1`. + +
+ +```solidity +function deposit(address owner) +``` +Entry point into the child chain. The user has the option to create a spendable utxo owned by the address, `owner`. Once created, +the private keys of the `owner` address has complete control of the new utxo. + +Deposits are not recorded in the child chain blocks and are entirely represented on the rootchain. Each deposit is identified with an incremental nonce. +Validators catch deposits through event handlers and maintain a collection of spendable deposits. +```solidity +mapping(uint256 => depositStruct) deposits; // The key is the incrementing nonce +struct depositStruct { + address owner, + uint256 amount, + uint256 ethBlockNum, + uint256 createdAt +} +``` + +
+ +```solidity +function startTransactionExit(uint256[3] txPos, bytes txBytes, bytes proof, bytes confirmSignatures, uint256 committedFee) +``` +`txPos` follows the convention - `[blockNumber, transactionIndex, outputIndex]` + +Exit procedure for exiting a utxo on the child chain(not deposits). The `txPos` locates the transaction on the child chain. The leaf, hash(hash(`txBytes`), `sigs`) is checked against the block header using the `proof`. +The `confirmSignatures` represent the acknowledgement of the inclusion by both inputs. If only one input was used to create this transactions, only one confirm signature should be passed in for the corresponding +input. However, if there are two distinct inputs in the exiting transactions, both confirm signatures should be appended together in order for a total of 130 bytes. The owner of the exit must commit to any fees payed, `committedFee`. + +A valid exit satisfies the following properties: + - Exit has not previously been finalized or challenged + - The creator of this exit posted a sufficient bond. Excess funds are refunded the the senders rootchain balance and are immediately withdrawable. + - If present, the confirm signatures are correct and signed by the same address which signed the corresponding input signatures. + +
+ +```solidity +function startDepositExit(uint256 nonce, uint256 committedFee) +``` +Exit procedure for deposits that have not been spent. Deposits are purely identified by their `nonce`. The caller's address must match the owner of the deposit. +A valid exit must satisfy the same constraints listed above for normal utxo exits except confirm signatures. Deposits exits are also collected into their own seperate queue from normal transactions. +This is because of the differing priority calculation. The priority of a deposit is purely it's nonce while the priority of a utxo is calculated from it's location in the child chain. The owner of the exit must +commit to any fee, `committedFee`. + +
+ +```solidity +function startFeeExit(uint256 blockNumber, uint256 committedFee) +``` +The validator of any block should call this function to exit the fees they've collected for that particular block. +The validator declares the `blockNumber` of the block for which they'd like to exit fees. This exit is then added to exit queue with the lowest priority for that block. +Note that if the validator attempts to start an exit for a fee-UTXO that has already been spent in a later block, the exit can be challenged through `startTransactionExit` the same way as a regular transaction exit. + +
+ +```solidity +function challengeExit(uint256[4] exitingTxPos, uint256[2] challengingTxPos, bytes txBytes, bytes proof, bytes confirmSignature) +``` +`challengingTxPos` follows the convention - `[blockNumber, transactionIndex]` +`exitingTxPos` follows the convention - `[blockNumber, transactionIndex, outputIndex, depositNonce`] + +A uxto that has starting an exit phase but was already spent on the child chain can be challenged using this function call. A successful challenge awards the caller with the exit bond. +The `exitingTxPos` locates the malicious utxo and is used to calculate the priority. `challengingTxPos` locates the transaction that is the child (offending transaction is an input into this tx). +The `proof`, `txBytes` and `sigs` is sufficient for a proof of inclusion in the child chain of the parent transaction. The `confirmSignature`, signed by the owner of the malicious transaction, +acknowledges the inclusion of it's parent in the plasma chain and allows anyone with this confirm signature to challenge a malicious exit of the child. + +An operator can also use this function to do a fee mismatch challenge. In this case, the confirm singature can be empty. The exitingTxPos must be the first input of the challenging transaction +and must have a mismatch in the committed fee. The first transaction signature is checked against to prevent the operator for performing invalid inclusions to challenge any exit. + +
+ +```solidity +function finalizeTransactionExits() +``` +Process all "finalized" exits in the priority queue. "Finalized" exits are those that have been in the priority queue for at least one week and have not been proven to be malicious through a challenge. + +
+ +```solidity +function finalizeDepositExits() +``` +Process all "finalized" deposit exits in the priority queue. "Finalized" exits are those that have been in the priority queue for at least one week and have not been proven to be malicious through a challenge. + +
+ +```solidity +function withdraw() +``` +Sender withdraws all funds associated with their balance from the contract. + +
+ +```solidity +function balanceOf(address _address) returns (uint256 amount) +``` +Getter for the withdrawable balance of `_address` + +
+ +```solidity +function plasmaChainBalance() returns (uint256 funds) +``` +Query the total funds of the plasma chain + +
+ +```solidity +function txQueueLength() returns (uint256 length) +``` +Getter for the length of the transaction exit queue + +
+ +```solidity +function depositQueueLength() returns (uint256 length) +``` +Getter for the length of the deposit exit queue diff --git a/contracts/generate.js b/contracts/generate.js new file mode 100755 index 0000000..ea8b781 --- /dev/null +++ b/contracts/generate.js @@ -0,0 +1,26 @@ +#!/usr/bin/env node +var shell = require('shelljs'); +var fs = require("fs"); + +console.log('Generating Go Wrappers...'); +console.log('Cleaning previous build...'); +shell.rm('-rf', 'build wrappers abi'); +shell.exec('truffle compile'); + +// files to generate +generate('./build/contracts/PlasmaMVP.json'); + +function generate(path) { + shell.mkdir('-p', ['wrappers', 'abi']); + + let contract = JSON.parse(fs.readFileSync(path, {encoding: 'utf8'})); + + //const filename = contract.contractName; + let snakeCasedFilename = ( + contract.contractName.replace(/([a-z])([A-Z])/g, '$1_$2').replace(/([A-Z])([A-Z][a-z])/g, '$1_$2') + ).toLowerCase(); + + + fs.writeFileSync(`abi/${contract.contractName}.abi`, JSON.stringify(contract.abi)) + shell.exec(`abigen --abi abi/${contract.contractName}.abi --pkg wrappers --type ${contract.contractName} --out wrappers/${snakeCasedFilename}.go`); +} diff --git a/contracts/migrations/1_initial_migration.js b/contracts/migrations/1_initial_migration.js new file mode 100644 index 0000000..1349ace --- /dev/null +++ b/contracts/migrations/1_initial_migration.js @@ -0,0 +1,5 @@ +let Migrations = artifacts.require("Migrations"); + +module.exports = function(deployer, network, accounts) { + deployer.deploy(Migrations); +}; diff --git a/contracts/migrations/2_deploy_rootchain.js b/contracts/migrations/2_deploy_rootchain.js new file mode 100644 index 0000000..33f3313 --- /dev/null +++ b/contracts/migrations/2_deploy_rootchain.js @@ -0,0 +1,5 @@ +let PlasmaMVP = artifacts.require("PlasmaMVP"); + +module.exports = function(deployer, network, accounts) { + deployer.deploy(PlasmaMVP, {from: accounts[0]}); +}; diff --git a/contracts/package-lock.json b/contracts/package-lock.json new file mode 100644 index 0000000..a27d065 --- /dev/null +++ b/contracts/package-lock.json @@ -0,0 +1,472 @@ +{ + "name": "plasma-mvp-rootchain", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "bindings": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.4.0.tgz", + "integrity": "sha512-7znEVX22Djn+nYjxCWKDne0RRloa9XfYa84yk3s+HkE3LpDYZmhArYr9O9huBoHY3/oXispx5LorIX7Sl2CgSQ==", + "dev": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bip66": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", + "integrity": "sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI=", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "create-hash": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "drbg.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz", + "integrity": "sha1-Pja2xCs3BDgjzbwzLVjzHiRFSAs=", + "dev": true, + "requires": { + "browserify-aes": "^1.0.6", + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4" + } + }, + "elliptic": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz", + "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==", + "dev": true, + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "ethereumjs-util": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-6.1.0.tgz", + "integrity": "sha512-URESKMFbDeJxnAxPppnk2fN6Y3BIatn9fwn76Lm8bQlt+s52TpG8dN9M66MLPuRAiAOIqL3dfwqWJf0sd0fL0Q==", + "dev": true, + "requires": { + "bn.js": "^4.11.0", + "create-hash": "^1.1.2", + "ethjs-util": "0.1.6", + "keccak": "^1.0.2", + "rlp": "^2.0.0", + "safe-buffer": "^5.1.1", + "secp256k1": "^3.0.1" + } + }, + "ethjs-util": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.6.tgz", + "integrity": "sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w==", + "dev": true, + "requires": { + "is-hex-prefixed": "1.0.0", + "strip-hex-prefix": "1.0.0" + } + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "interpret": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", + "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==" + }, + "is-hex-prefixed": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-hex-prefixed/-/is-hex-prefixed-1.0.0.tgz", + "integrity": "sha1-fY035q135dEnFIkTxXPggtd39VQ=", + "dev": true + }, + "keccak": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-1.4.0.tgz", + "integrity": "sha512-eZVaCpblK5formjPjeTBik7TAg+pqnDrMHIffSvi9Lh7PQgM1+hSzakUeZFCk9DVVG0dacZJuaz2ntwlzZUIBw==", + "dev": true, + "requires": { + "bindings": "^1.2.1", + "inherits": "^2.0.3", + "nan": "^2.2.1", + "safe-buffer": "^5.1.0" + } + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "nan": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.1.tgz", + "integrity": "sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "requires": { + "resolve": "^1.1.6" + } + }, + "resolve": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz", + "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", + "requires": { + "path-parse": "^1.0.6" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "rlp": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/rlp/-/rlp-2.2.2.tgz", + "integrity": "sha512-Ng2kJEN731Sfv4ZAY2i0ytPMc0BbJKBsVNl0QZY8LxOWSwd+1xpg+fpSRfaMn0heHU447s6Kgy8qfHZR0XTyVw==", + "dev": true, + "requires": { + "bn.js": "^4.11.1", + "safe-buffer": "^5.1.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "secp256k1": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.6.2.tgz", + "integrity": "sha512-90nYt7yb0LmI4A2jJs1grglkTAXrBwxYAjP9bpeKjvJKOjG2fOeH/YI/lchDMIvjrOasd5QXwvV2jwN168xNng==", + "dev": true, + "requires": { + "bindings": "^1.2.1", + "bip66": "^1.1.3", + "bn.js": "^4.11.3", + "create-hash": "^1.1.2", + "drbg.js": "^1.0.1", + "elliptic": "^6.2.3", + "nan": "^2.2.1", + "safe-buffer": "^5.1.0" + } + }, + "sha.js": { + "version": "2.4.11", + "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shelljs": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.3.tgz", + "integrity": "sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A==", + "requires": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + } + }, + "solidity-rlp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/solidity-rlp/-/solidity-rlp-2.0.0.tgz", + "integrity": "sha512-rUwmzbevytB0jk2zT03h7/JHYOKPDXgzrUyvvb/hI5OcnWjjjvqK/+hcDwBffl6dMalfvSZo7/KWn3lOLeb09g==" + }, + "strip-hex-prefix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz", + "integrity": "sha1-DF8VX+8RUTczd96du1iNoFUA428=", + "dev": true, + "requires": { + "is-hex-prefixed": "1.0.0" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + } + } +} diff --git a/contracts/package.json b/contracts/package.json new file mode 100644 index 0000000..4e08209 --- /dev/null +++ b/contracts/package.json @@ -0,0 +1,19 @@ +{ + "name": "plasma-mvp-rootchain", + "version": "1.0.0", + "directories": { + "test": "test" + }, + "scripts": { + "test": "rm -rf build && truffle test", + "coverage": "./node_modules/.bin/solidity-coverage" + }, + "devDependencies": { + "chai": "^4.2.0", + "ethereumjs-util": "^6.1.0", + "rlp": "^2.2.2" + }, + "dependencies": { + "solidity-rlp": "^2.0.0" + } +} diff --git a/contracts/test/libraries/TMSimpleMerkleTree.js b/contracts/test/libraries/TMSimpleMerkleTree.js new file mode 100644 index 0000000..69b86c6 --- /dev/null +++ b/contracts/test/libraries/TMSimpleMerkleTree.js @@ -0,0 +1,92 @@ +let assert = require('chai').assert; + +let TMSimpleMerkleTree_Test = artifacts.require("TMSimpleMerkleTree_Test"); + +let { catchError, toHex } = require('../utilities.js'); +let { generateMerkleRootAndProof } = require('../plasmamvp/plasmamvp_helpers.js'); + +contract('TMSimpleMerkleTree', async (accounts) => { + let instance; + before(async () => { + instance = await TMSimpleMerkleTree_Test.new(); + }); + + it("Verifies the membership in a merkle tree with 7 leaves (hardcoded)", async () => { + // this utxo input and the merkle root of its block were generated + // by the side chain + // this side chain block contains 7 txns + let rootHash = "0x4edd08572735720e2769fa536c03e5d2ed29ff2ba97fb102cfe71fdae3c30428"; + let leaf = "0x714c0269d202e4302fadab4d62a4e9171fbf09508cb589616342cce45981d329"; + let proof = "0xfbec635c930057fdc76939052216ed2aed7af618109b983ee1ae7b13c909f2dd1a61753dc9eccd5506144081ba000a8e44c9262be17ab934dda9e4fa10495fccdbb108450dad46789d67f5cfb5ee4be7f505bd8835a0867410412f22cfda8ad5"; + let total = 7; // Merkle tree contains 7 leaf nodes (transactions) + let index = 2; // index of the leaf we want to prove + + // checking membership of 3rd leaf + assert.isTrue(await instance.checkMembership.call(toHex(leaf), index, toHex(rootHash), toHex(proof), total), "Didn't prove membership"); + + + // checking membership of 4th leaf + leaf = "0xfbec635c930057fdc76939052216ed2aed7af618109b983ee1ae7b13c909f2dd"; + proof = "0x714c0269d202e4302fadab4d62a4e9171fbf09508cb589616342cce45981d3291a61753dc9eccd5506144081ba000a8e44c9262be17ab934dda9e4fa10495fccdbb108450dad46789d67f5cfb5ee4be7f505bd8835a0867410412f22cfda8ad5"; + index = 3; + assert.isTrue(await instance.checkMembership.call(toHex(leaf), index, toHex(rootHash), toHex(proof), total), "Didn't prove membership"); + }); + + it("Verifies the membership in a merkle tree with only one transaction", async () => { + let leafHash = web3.utils.keccak256("inputSeed"); + + let root, proof; + [root, proof] = generateMerkleRootAndProof([leafHash], 0); + + assert.isTrue(await instance.checkMembership.call(toHex(leafHash), 0, toHex(root), toHex(proof), 1), "Didn't prove membership"); + }); + + it("Catches bad input on checkMembership", async () => { + let leafHash1 = web3.utils.keccak256("inputSeed1"); + let leafHash2 = web3.utils.keccak256("inputSeed2"); + let leafHash3 = web3.utils.keccak256("inputSeed3"); + + let root, proof; + [root, proof] = generateMerkleRootAndProof([leafHash1, leafHash2, leafHash3], 0); + + let badLeafHash = web3.utils.keccak256("wrongInputSeed", {encoding: 'hex'}); + assert.isFalse(await instance.checkMembership.call(toHex(badLeafHash), 0, toHex(root), toHex(proof), 3), "Returned true on wrong leaf"); + + assert.isFalse(await instance.checkMembership.call(toHex(leafHash1), 1, toHex(root), toHex(proof), 3), "Returned true on wrong index"); + + let badRoot = web3.utils.keccak256("wrongRoot", {encoding: 'hex'}); + assert.isFalse(await instance.checkMembership.call(toHex(leafHash1), 0, toHex(badRoot), toHex(proof), 3), "Returned true on wrong root"); + + let badProof = "a".repeat(proof.length - 2); + assert.isFalse(await instance.checkMembership.call(toHex(leafHash1), 0, toHex(root), toHex(badProof), 3), "Returned true on wrong proof"); + + let err; + [err] = await catchError(instance.checkMembership.call(toHex(leafHash1), 0, toHex(root), toHex(proof + "0000"), 3)); + if (!err) + assert.fail("Didn't revert on an proof with the bad size"); + }); + + it("Verifies membership in a merkle tree with multiple transactions", async () => { + let leafHash1 = web3.utils.keccak256("inputSeed1"); + let leafHash2 = web3.utils.keccak256("inputSeed2"); + let leafHash3 = web3.utils.keccak256("inputSeed3"); + let leafHash4 = web3.utils.keccak256("inputSeed4"); + let leafHash5 = web3.utils.keccak256("inputSeed5"); + + let root, proof; + [root, proof] = generateMerkleRootAndProof([leafHash1, leafHash2, leafHash3, leafHash4, leafHash5], 0); + assert.isTrue(await instance.checkMembership.call(toHex(leafHash1), 0, toHex(root), toHex(proof), 5), "Didn't prove membership"); + + [root, proof] = generateMerkleRootAndProof([leafHash1, leafHash2, leafHash3, leafHash4, leafHash5], 1); + assert.isTrue(await instance.checkMembership.call(toHex(leafHash2), 1, toHex(root), toHex(proof), 5), "Didn't prove membership"); + + [root, proof] = generateMerkleRootAndProof([leafHash1, leafHash2, leafHash3, leafHash4, leafHash5], 2); + assert.isTrue(await instance.checkMembership.call(toHex(leafHash3), 2, toHex(root), toHex(proof), 5), "Didn't prove membership"); + + [root, proof] = generateMerkleRootAndProof([leafHash1, leafHash2, leafHash3, leafHash4, leafHash5], 3); + assert.isTrue(await instance.checkMembership.call(toHex(leafHash4), 3, toHex(root), toHex(proof), 5), "Didn't prove membership"); + + [root, proof] = generateMerkleRootAndProof([leafHash1, leafHash2, leafHash3, leafHash4, leafHash5], 4); + assert.isTrue(await instance.checkMembership.call(toHex(leafHash5), 4, toHex(root), toHex(proof), 5), "Didn't prove membership"); + }); +}); diff --git a/contracts/test/libraries/bytesUtil.js b/contracts/test/libraries/bytesUtil.js new file mode 100644 index 0000000..669c4c3 --- /dev/null +++ b/contracts/test/libraries/bytesUtil.js @@ -0,0 +1,49 @@ +let assert = require('chai').assert; + +let BytesUtil_Test = artifacts.require("BytesUtil_Test"); + +let { catchError, toHex } = require('../utilities.js'); + +contract('BytesUtil', async (accounts) => { + let instance; + before(async () => { + instance = await BytesUtil_Test.new(); + }); + + it("Slices bytes correctly", async () => { + let inputHash = web3.utils.keccak256("inputSeed"); + + assert.equal((await instance.slice.call(toHex(inputHash), 0, 32)).toString(), inputHash, "Slice didn't get entire substring"); + + assert.equal((await instance.slice.call(toHex(inputHash), 0, 16)).toString(), toHex(inputHash.substring(2,34)), "Didn't get first half of the hash"); + assert.equal((await instance.slice.call(toHex(inputHash), 16, 16)).toString(), toHex(inputHash.substring(34)), "Didn't get second half of the hash"); + + assert.equal((await instance.slice.call(toHex(inputHash), 0, 8)).toString(), toHex(inputHash.substring(2,18)), "Didn't get first quarter of the hash"); + assert.equal((await instance.slice.call(toHex(inputHash), 8, 24)).toString(), toHex(inputHash.substring(18)), "Didn't get rest of the hash"); + }); + + it("Returns the same bytes if there's nothing to slice", async () => { + let inputHash = web3.utils.keccak256("seed"); + + assert.equal((await instance.slice.call(toHex(inputHash), 0, 32)), inputHash, "same slice not returned"); + }); + + it("Reverts if trying to slice out of range", async () => { + let inputHash = web3.utils.keccak256("inputSeed"); + + // sha3 maps input to a 32 byte hash (64 charac + let err; + [err] = await catchError(instance.slice.call(toHex(inputHash), 1, 32)); + if (!err) + assert.fail("slice did not revert when inputs produce an out of bounds error"); + }); + + it("Can slice bytes larger than a evm word size", async () => { + let input = "0x"; + for (let i = 0; i < 100; i++) { // 50 bytes + input += Math.floor(Math.random()*10) // include a random hex digit from 0-9 + } + + assert.equal((await instance.slice.call(toHex(input), 1, 40)).toString(), toHex(input.substring(4, 84)), "Didn't copy over a whole word size then left over bytes"); + }); +}); diff --git a/contracts/test/libraries/priorityQueue.js b/contracts/test/libraries/priorityQueue.js new file mode 100644 index 0000000..1ba92d7 --- /dev/null +++ b/contracts/test/libraries/priorityQueue.js @@ -0,0 +1,132 @@ +let assert = require('chai').assert; + +let MinMinPriorityQueue_Test = artifacts.require("MinPriorityQueue_Test"); +let { catchError } = require('../utilities.js'); + +contract('MinPriorityQueue', async (accounts) => { + let instance; + beforeEach(async () => { + instance = await MinMinPriorityQueue_Test.new(); + }); + + it("Reverts when deleting on an empty queue", async() => { + let err; + [err] = await catchError(instance.getMin.call()); + if (!err) + assert.fail("Didn't revert on getting min of an empty queue."); + + [err] = await catchError(instance.delMin()); + if (!err) + assert.fail("Didn't revert on deleting min of an empty queue."); + }); + + it("Correctly adds then remove elements", async () => { + await instance.insert(2); + await instance.insert(1); + await instance.insert(3); + + assert.equal((await instance.getMin.call()).toNumber(), 1, "Did not delete correct minimum"); + + await instance.delMin(); + assert.equal((await instance.getMin.call()).toNumber(), 2, "Did not delete correct minimum"); + + await instance.delMin(); + assert.equal((await instance.getMin.call()).toNumber(), 3, "Did not delete correct minimum"); + + await instance.delMin(); + assert.equal((await instance.currentSize.call()).toNumber(), 0, "Size is not zero"); + }); + + it("Handles ascending inserts", async () => { + let currSize = (await instance.currentSize.call()).toNumber(); + for (i = 1; i < 6; i++) { + await instance.insert(i); + } + + currSize = (await instance.currentSize.call()).toNumber(); + assert.equal(currSize, 5, "currentSize did not increment"); + + let min = (await instance.getMin.call()).toNumber(); + assert.equal(1, min, "getMin did not return the minimum"); + + for (i = 0; i < 3; i++) { + await instance.delMin(); + } + + min = (await instance.getMin.call()).toNumber(); + currSize = (await instance.currentSize.call()).toNumber(); + assert.equal(min, 4, "delMin deleted priorities out of order"); + assert.equal(currSize, 2, "currSize did not decrement"); + + // Clear the queue + for (i = 0; i < 2; i++) { + await instance.delMin(); + } + currSize = (await instance.currentSize.call()).toNumber(); + assert.equal(currSize, 0, "The MinMinPriority queue has not been emptied"); + }); + + it("Can insert, delete min, then insert again", async () => { + for (i = 1; i < 6; i++) { + await instance.insert(i); + let min = (await instance.getMin.call()).toNumber(); + assert.equal(min, 1, "getMin does not return minimum element in pq."); + } + + // partially clear the pq + for (i = 0; i < 3; i++) { + await instance.delMin(); + } + min = (await instance.getMin.call()).toNumber(); + assert.equal(min, 4, "delMin deleted priorities out of order"); + + // insert to pq after partial delete + for (i = 2; i < 4; i++) { + await instance.insert(i); + let min = (await instance.getMin.call()).toNumber(); + assert.equal(min, 2, "getMin does not return minimum element in pq."); + } + // clear the pq + for (i = 0; i < 4; i++) { + await instance.delMin(); + } + + currSize = (await instance.currentSize.call()).toNumber(); + assert.equal(currSize, 0, "The MinMinPriority queue has not been emptied"); + }); + + it("Handles duplicate entries", async () => { + let currentSize = (await instance.currentSize.call()).toNumber(); + assert.equal(currentSize, 0, "The size is not 0"); + + await instance.insert(10); + let min = (await instance.getMin.call()).toNumber(); + currentSize = (await instance.currentSize.call()).toNumber(); + assert.equal(currentSize, 1, "The size is not 0"); + + // Breaks here - has min as 0 + assert.equal(min, 10, "First insert did not work"); + + await instance.insert(10); + min = (await instance.getMin.call()).toNumber(); + assert.equal(min, 10, "Second insert of same MinMinPriority did not work"); + await instance.insert(5); + await instance.insert(5); + + currentSize = (await instance.currentSize.call()).toNumber(); + assert.equal(currentSize, 4, "The currentSize is incorrect") + + await instance.delMin(); + min = (await instance.getMin.call()).toNumber(); + assert.equal(min, 5, "MinMinPriorityQueue did not handle same priorities correctly"); + + await instance.delMin(); + await instance.delMin(); + + min = (await instance.getMin.call()).toNumber(); + assert.equal(min, 10, "MinMinPriorityQueue did not delete duplicate correctly") + + await instance.delMin(); + assert.equal(await instance.currentSize.call(), 0); + }); +}); diff --git a/contracts/test/plasmamvp/blockSubmissions.js b/contracts/test/plasmamvp/blockSubmissions.js new file mode 100644 index 0000000..0f3b9fa --- /dev/null +++ b/contracts/test/plasmamvp/blockSubmissions.js @@ -0,0 +1,71 @@ +let assert = require('chai').assert; + +let PlasmaMVP = artifacts.require("PlasmaMVP"); + +let { toHex, catchError } = require('../utilities.js'); + +contract('[PlasmaMVP] Block Submissions', async (accounts) => { + let instance; + let authority = accounts[0]; + let minExitBond = 10000; + beforeEach(async () => { + instance = await PlasmaMVP.new({from: authority}); + }); + + it("Submit block from authority", async () => { + let header = web3.utils.keccak256('1234'); + let tx = await instance.submitBlock([header], [1], [0], 1, {from: authority}); + + // BlockSubmitted event + assert.equal(tx.logs[0].args.header, header, "incorrect block header in BlockSubmitted event"); + assert.equal(tx.logs[0].args.blockNumber.toNumber(), 1, "incorrect block number in BlockSubmitted event"); + assert.equal(tx.logs[0].args.numTxns.toNumber(), 1, "incorrect block size in BlockSubmitted event"); + + assert.equal((await instance.plasmaChain.call(1))[0], header, 'child block merkle header does not match submitted merkle header.'); + }); + + it("Submit block from someone other than authority", async () => { + let prev = (await instance.lastCommittedBlock.call()).toNumber(); + + let [err] = await catchError(instance.submitBlock([web3.utils.keccak256('578484785954')], [1], + [0], 1, {from: accounts[1]})); + if (!err) + assert.fail("Submitted blocked without being the authority"); + + let curr = (await instance.lastCommittedBlock.call()).toNumber(); + assert.equal(prev, curr, "Child blocknum incorrectly changed"); + }); + + it("Can submit more than one merkle header", async () => { + let header1 = web3.utils.keccak256("header1").slice(2); + let header2 = web3.utils.keccak256("header2").slice(2); + + await instance.submitBlock([toHex(header1), toHex(header2)], [1, 2], [0, 0], 1, {from: authority}); + + assert.equal((await instance.lastCommittedBlock.call()).toNumber(), 2, "lastCommittedBlock not incremented incorrectly"); + assert.equal((await instance.plasmaChain.call(1))[0], toHex(header1), "mismatch in block header"); + assert.equal((await instance.plasmaChain.call(2))[0], toHex(header2), "mismatch in block header"); + }); + + it("Enforces block number ordering", async () => { + let header1 = web3.utils.keccak256("header1").slice(2) + let header3 = web3.utils.keccak256("header3").slice(2) + + await instance.submitBlock([toHex(header1)], [1], [0], 1, {from: authority}); + let err; + [err] = await catchError(instance.submitBlock([toHex(header3)], [1], 3, {from: authority})); + if (!err) + assert.fail("allowed block submission with inconsistent ordering"); + }); + + it("Enforces block size capacity", async () => { + let header = web3.utils.keccak256("header"); + let [err] = await catchError(instance.submitBlock([header], [Math.pow(2, 16)], [0], 1, {from: authority})) + if (!err) + assert.fail("block size capacity not enforeced"); + + [err] = await catchError(instance.submitBlock([header], [0], [0], 1, {from: authority})) + if (!err) + assert.fail("block size capacity not enforced"); + }); +}); diff --git a/contracts/test/plasmamvp/deposits.js b/contracts/test/plasmamvp/deposits.js new file mode 100644 index 0000000..37b0e7b --- /dev/null +++ b/contracts/test/plasmamvp/deposits.js @@ -0,0 +1,296 @@ +let RLP = require('rlp'); +let assert = require('chai').assert; + +let PlasmaMVP = artifacts.require("PlasmaMVP"); + +let { fastForward, proof, zeroHashes, sha256String, generateMerkleRootAndProof, fillTxList } = require('./plasmamvp_helpers.js'); +let { catchError, toHex } = require('../utilities.js'); + +contract('[PlasmaMVP] Deposits', async (accounts) => { + let instance; + let oneWeek = 604800; // in seconds + let minExitBond = 200000; + + let authority = accounts[0]; + before(async () => { + instance = await PlasmaMVP.deployed(); + }); + + it("Will not revert finalizeDeposit on an empty queue", async () => { + await instance.finalizeDepositExits(); + }); + + it("Catches Deposit event", async () => { + let nonce = (await instance.depositNonce.call()).toNumber(); + let tx = await instance.deposit(accounts[1], {from: accounts[1], value: 100}); + // check Deposit event + assert.equal(tx.logs[0].args.depositor, accounts[1], "incorrect deposit owner"); + assert.equal(tx.logs[0].args.amount.toNumber(), 100, "incorrect deposit amount"); + assert.equal(tx.logs[0].args.depositNonce, nonce, "incorrect deposit nonce"); + }); + + it("Allows deposits of funds into a different address", async () => { + let nonce = (await instance.depositNonce.call()).toNumber(); + let tx = await instance.deposit(accounts[2], {from: accounts[1], value: 100}); + + // check Deposit event + assert.equal(tx.logs[0].args.depositor, accounts[2], "incorrect deposit owner"); + assert.equal(tx.logs[0].args.amount.toNumber(), 100, "incorrect deposit amount"); + assert.equal(tx.logs[0].args.depositNonce, nonce, "incorrect deposit nonce"); + + // check instance deposit mapping + let deposit = await instance.deposits.call(nonce); + assert.equal(deposit[0], accounts[2], "incorrect deposit owner"); + assert.equal(deposit[1], 100, "incorrect deposit amount"); + }); + + it("Rejects a committed fee larger than the deposit amount", async () => { + let nonce = (await instance.depositNonce.call()).toNumber(); + await instance.deposit(accounts[2], {from: accounts[1], value: 100}); + + let err; + [err] = await catchError(instance.startDepositExit(nonce, 100, {from: accounts[1], value: minExitBond})) + if (!err) + assert.fail("started a deposit exit with a committed fee equal to the amount"); + }); + + it("Only allows deposit owner to start a deposit exit", async () => { + let nonce = (await instance.depositNonce.call()).toNumber(); + await instance.deposit(accounts[2], {from: accounts[1], value: 100}); + + let err; + // accounts[1] cannot start exit because it's not the owner + [err] = await catchError(instance.startDepositExit(nonce, 0, {from: accounts[1], value: minExitBond})); + if (!err) + assert.fail("Non deposit owner allowed to start an exit"); + + // accounts[2] should be able to start exit + await instance.startDepositExit(nonce, 0, {from: accounts[2], value: minExitBond}); + }); + + it("Rejects exiting a deposit with a malicious committed fee", async () => { + let nonce = (await instance.depositNonce.call()).toNumber(); + await instance.deposit(accounts[2], {from: accounts[2], value: 10}); + + let err; + [err] = await catchError(instance.startDepositExit(nonce, 11, {from: accounts[2], value: minExitBond})); + if (!err) + assert.fail("exited a deposit with a committed fee larger than the deposit amount"); + }); + + it("Rejects exiting a deposit twice", async () => { + let nonce = (await instance.depositNonce.call()).toNumber(); + await instance.deposit(accounts[2], {from: accounts[2], value: 100}); + await instance.startDepositExit(nonce, 0, {from: accounts[2], value: minExitBond}); + + let err; + [err] = await catchError(instance.startDepositExit(nonce, 0, {from: accounts[2], value: minExitBond})); + if (!err) + assert.fail("Started an exit for the same deposit twice."); + }); + + it("Catches StartedDepositExit event", async () => { + let nonce = (await instance.depositNonce.call()).toNumber(); + await instance.deposit(accounts[2], {from: accounts[2], value: 100}); + let tx = await instance.startDepositExit(nonce, 0, {from: accounts[2], value: minExitBond}); + + assert.equal(tx.logs[0].args.nonce.toNumber(), nonce, "StartedDepositExit event emits incorrect nonce"); + assert.equal(tx.logs[0].args.owner, accounts[2], "StartedDepositExit event emits incorrect owner"); + assert.equal(tx.logs[0].args.amount.toNumber(), 100, "StartedDepositExit event emits incorrect amount"); + }); + + it("Requires sufficient bond and refunds excess if overpayed", async () => { + let nonce = (await instance.depositNonce.call()).toNumber(); + await instance.deposit(accounts[2], {from: accounts[2], value: 100}); + + let err; + [err] = await catchError(instance.startDepositExit(nonce, 0, {from: accounts[2], value: minExitBond-10})); + if (!err) + assert.fail("started exit with insufficient bond"); + + await instance.startDepositExit(nonce, 0, {from: accounts[2], value: minExitBond+10}); + + let balance = (await instance.balanceOf.call(accounts[2])).toNumber(); + assert.equal(balance, 10, "excess for overpayed bond not refunded to sender"); + }); + + it("Can start and finalize a deposit exit", async () => { + instance = await PlasmaMVP.new({from: authority}); + + let nonce = (await instance.depositNonce.call()).toNumber(); + await instance.deposit(accounts[2], {from: accounts[2], value: 100}); + + let plasmaChainBalance = (await instance.plasmaChainBalance.call()).toNumber(); + assert.equal(plasmaChainBalance, 100); + + await instance.startDepositExit(nonce, 0, {from: accounts[2], value: minExitBond}); + await fastForward(oneWeek + 100); + + await instance.finalizeDepositExits(); + + plasmaChainBalance = (await instance.plasmaChainBalance.call()).toNumber(); + assert.equal(plasmaChainBalance, 0); + + let balance = (await instance.balanceOf.call(accounts[2])).toNumber(); + assert.equal(balance, 100 + minExitBond, "deposit exit not finalized after a week"); + + let exit = await instance.depositExits.call(nonce); + assert.equal(exit[4], 3, "exit's state not set to finalized"); + }); + + it("Cannot reopen a finalized deposit exit", async () => { + let nonce = (await instance.depositNonce.call()).toNumber(); + await instance.deposit(accounts[2], {from: accounts[2], value: 100}); + await instance.startDepositExit(nonce, 0, {from: accounts[2], value: minExitBond}); + + await fastForward(oneWeek + 100); + + await instance.finalizeDepositExits(); + let err; + [err] = await catchError(instance.startDepositExit(nonce, 0, {from: accounts[2], value: minExitBond})); + if (!err) + assert.fail("reopened a finalized deposit exit"); + }); + + it("Correctly challenge a spent deposit", async () => { + let nonce = (await instance.depositNonce.call()).toNumber(); + await instance.deposit(accounts[2], {from: accounts[2], value: 100}); + + // construct transcation with first input as the deposit + let txList = Array(15).fill(0); + txList[3] = nonce; txList[10] = accounts[1]; txList[11] = 100; + txList = fillTxList(txList); + let txHash = web3.utils.soliditySha3(toHex(RLP.encode(txList).toString('hex'))); + let sigs = [await web3.eth.sign(txHash, accounts[2]), toHex(Buffer.alloc(65).toString('hex'))]; + let txBytes = [txList, sigs]; + txBytes = RLP.encode(txBytes).toString('hex'); + + // include this transaction in the next block + let root; + [root, proof] = generateMerkleRootAndProof([sha256String(txBytes)], 0); + + let blockNum = (await instance.lastCommittedBlock.call()).toNumber() + 1; + await instance.submitBlock([toHex(root)], [1], [0], blockNum, {from: authority}); + + // create the confirm sig + let confirmHash = sha256String(sha256String(txBytes) + root.slice(2)); + let confirmSig = await web3.eth.sign(confirmHash, accounts[2]); + + // start the malicious exit + await instance.startDepositExit(nonce, 0, {from: accounts[2], value: minExitBond}); + + // checks matching inputs + let err; + [err] = await catchError(instance.challengeExit([0,0,0,nonce-1], [blockNum, 0], + toHex(txBytes), toHex(sigs), toHex(proof), toHex(confirmSig), {from: accounts[3]})); + if (!err) + assert.fail("did not check against matching inputs"); + + // invalid confirm signature + [err] = await catchError(instance.challengeExit([0,0,0,nonce-1], [blockNum, 0], + toHex(txBytes), toHex(sigs), toHex(proof), toHex(confirmSig.substring(0, 30)), {from: accounts[3]})); + if (!err) + assert.fail("challenged with an invalid signature"); + + // correctly challenge + await instance.challengeExit([0,0,0,nonce], [blockNum, 0], + toHex(txBytes), toHex(proof), toHex(confirmSig), {from: accounts[3]}); + + let balance = (await instance.balanceOf.call(accounts[3])).toNumber(); + assert.equal(balance, minExitBond, "challenger not awarded exit bond"); + + let exit = await instance.depositExits.call(nonce); + assert.equal(exit[4], 2, "exit state not changed to challenged"); + + // Cannot challenge twice + [err] = await catchError(instance.challengeExit([0,0,0,nonce], [blockNum, 0], + toHex(txBytes), toHex(sigs), toHex(proof), toHex(confirmSig), {from: accounts[3]})); + if (!err) + assert.fail("Allowed a challenge for an exit already challenged"); + }); + + it("Challenge an invalid first input exit fee mismatch and exit the fee", async () => { + let nonce = (await instance.depositNonce.call()).toNumber(); + await instance.deposit(accounts[2], {from: accounts[2], value: 100}); + let nonce2 = (await instance.depositNonce.call()).toNumber(); + await instance.deposit(accounts[2], {from: accounts[2], value: 100}); + + let txList = Array(15).fill(0); + txList[3] = nonce; txList[14] = 5; // fee + txList[8] = nonce2; // second input + txList[10] = accounts[1]; txList[11] = 100; + txList = fillTxList(txList); + let txHash = web3.utils.soliditySha3(toHex(RLP.encode(txList).toString('hex'))); + let sigs = [toHex(await web3.eth.sign(txHash, accounts[2])), toHex(Buffer.alloc(65).toString('hex'))]; + let txBytes = [txList, sigs]; + txBytes = RLP.encode(txBytes).toString('hex'); + + // submit the block + let [merkleRoot, proof] = generateMerkleRootAndProof([sha256String(txBytes)], 0); + let blockNum = (await instance.lastCommittedBlock.call()).toNumber() + 1; + await instance.submitBlock([toHex(merkleRoot)], [1], [5], blockNum, {from: authority}); + + // exit the first deposit input. commit incorrect fee + await instance.startDepositExit(nonce, 1, {from: accounts[2], value: minExitBond}); + + // exit the second deposit input + await instance.startDepositExit(nonce2, 0, {from: accounts[2], value: minExitBond}); + + // challenge the first input with a fee mismatch + let tx = await instance.challengeExit([0,0,0,nonce], [blockNum, 0], toHex(txBytes), toHex(proof), toHex("")); + + let exit = await instance.depositExits.call(nonce); + assert.equal(exit[4].toNumber(), 0, "exit state not changed to non existent"); + + // second input should not be able to be challenged with a fee mismatch + let err; + [err] = await catchError(instance.challengeExit([0,0,0,nonce2], [blockNum, 0], toHex(txBytes), toHex(proof), toHex(""))); + if (!err) + assert.fail("challenged second input with a fee mismatch"); + + // start the exit again with the correct committed fee + await instance.startDepositExit(nonce, 5, {from: accounts[2], value: minExitBond}); + + // try challenge the exit with a fee mismatch + [err] = await catchError(instance.challengeExit([0,0,0,nonce], [blockNum, 0], toHex(txBytes), toHex(proof), toHex(""))); + if (!err) + assert.fail("operator challenged exit with correct committed fee"); + + // start a fee exit + await instance.startFeeExit(blockNum, 0, {from: authority, value: minExitBond}); + }); + + it("Attempts a withdrawal delay attack on exiting deposits", async () => { + + /* 1. Start exit for nonce_2 (newest) + * 2. Start exit for nonce_1, nonce_0 in the same eth block + * 3. Check exit ordering: nonce_2, nonce_0, nonce_1 + */ + + instance = await PlasmaMVP.new({from: authority}); + + let nonce_0 = (await instance.depositNonce.call()).toNumber(); + await instance.deposit(accounts[2], {from: accounts[2], value: 100}); + + let nonce_1 = (await instance.depositNonce.call()).toNumber(); + await instance.deposit(accounts[2], {from: accounts[2], value: 100}); + + let nonce_2 = (await instance.depositNonce.call()).toNumber(); + await instance.deposit(accounts[2], {from: accounts[2], value: 100}); + + // exit nonce_2 + await instance.startDepositExit(nonce_2, 0, {from: accounts[2], value: minExitBond}); + await fastForward(10); + + // exit nonce_1 then nonce_0 in the same ethereum block + await instance.startDepositExit(nonce_0, 0, {from: accounts[2], value: minExitBond}); + await instance.startDepositExit(nonce_1, 0, {from: accounts[2], value: minExitBond}); + + await fastForward(oneWeek + 10); + let depositExits = await instance.finalizeDepositExits({from: authority}); + // every even index to skip the `AddedToBalances` event + assert.equal(depositExits.logs[0].args.position.toString(), [0, 0, 0, nonce_2].toString(), "nonce_2 was not finalized first"); + assert.equal(depositExits.logs[2].args.position.toString(), [0, 0, 0, nonce_0].toString(), "nonce_0 was not finalized second"); + assert.equal(depositExits.logs[4].args.position.toString(), [0, 0, 0, nonce_1].toString(), "nonce_1 was not finalized last"); + }); +}); diff --git a/contracts/test/plasmamvp/plasmamvp_helpers.js b/contracts/test/plasmamvp/plasmamvp_helpers.js new file mode 100644 index 0000000..6bcde82 --- /dev/null +++ b/contracts/test/plasmamvp/plasmamvp_helpers.js @@ -0,0 +1,155 @@ +let RLP = require('rlp'); +let ethjs_util = require('ethereumjs-util'); + +let { toHex } = require('../utilities.js'); + +// Fast forward 1 week +let fastForward = async function(time) { + let oldTime = (await web3.eth.getBlock("latest")).timestamp; + + // fast forward + try { + await sendRPC({jsonrpc: "2.0", method: "evm_increaseTime", params: [time], id: 0}); + await sendRPC({jsonrpc: "2.0", method: "evm_mine", params: [], id: 0}); + } catch (err) { + assert.fail("failed to increase the evm time"); + } + + let newTime = (await web3.eth.getBlock("latest")).timestamp; + assert.isAtLeast(newTime - oldTime, time, `Did not fast forward at least ${time} seconds`); +} + +let sendRPC = async function(payload) { + return new Promise((resolve, reject) => { + web3.currentProvider.send(payload, (err, result) => { + if (err) reject(err) + else resolve(result) + }) + }) +} + +// SHA256 hash the input and returns it in string form. +// Expects a hex input. +let sha256String = function(input) { + return toHex(ethjs_util.sha256(toHex(input)).toString('hex')); +}; + +// SHA256 hashes together 2 inputs and returns it in string form. +// Expects hex inputs, and prepend each input with a 0x20 byte literal. +// Tendermint prefixes intermediate hashes with 0x20 bytes literals +// before hashing them. +let sha256StringMultiple = function(input1, input2) { + let toHash = "0x20" + input1.slice(2) + "20" + input2.slice(2); + return toHex(ethjs_util.sha256(toHash).toString('hex')); +}; + +// For a given list of leaves, this function constructs a simple merkle tree. +// It returns the merkle root and the merkle proof for the txn at index. +// @param leaves The leaves for which this function generates a merkle root and proof +// @param index The leaf for which this function generates a merkle proof +// +// Simple Tree: https://tendermint.com/docs/spec/blockchain/encoding.html#merkle-trees +let generateMerkleRootAndProof = function(leaves, index) { + if (leaves.length == 0) { // If there are no leaves, then we can't generate anything + return ["", ""]; + } else if (leaves.length == 1) { // If there's only 1 leaf, return it with and empty proof + return [leaves[0], ""]; + } else { + let pivot = Math.floor((leaves.length + 1) / 2); + + let left, right; + let proof = ""; + + // If the index will be in the left subtree (index < pivot), then we + // need to generate the proof using the intermediary hash from the right + // side. Otherwise, do the reverse. + if (index < pivot) { + // recursively call the function on the leaves that will be in the + // left and right sub trees. + left = generateMerkleRootAndProof(leaves.slice(0, pivot), index); + right = generateMerkleRootAndProof(leaves.slice(pivot, leaves.length), -1); + + // add current level's right intermediary hash to the proof + if (index >= 0) { + proof = left[1] + right[0].slice(2); + } + } else { + // recursively call the function on the leaves that will be in the + // left and right sub trees. + // since the index will be in the right sub tree, we need to update + // it's value. + left = generateMerkleRootAndProof(leaves.slice(0, pivot), -1); + right = generateMerkleRootAndProof(leaves.slice(pivot, leaves.length), index - pivot); + + // add current level's left intermediary hash to the proof + if (index >= 0) { + proof = right[1] + left[0].slice(2); + } + } + return [sha256StringMultiple(left[0], right[0]), toHex(proof)]; + } +}; + +let fillTxList = function(txList) { + // check inputs + for (let i = 0; i < 2; i++) { + // check inputs + let base = 5*i; + for (let j = base; j < base+4; j++) { + if (txList[j] == 0) { + txList[j] = toHex(Buffer.alloc(32).toString('hex')); + } + else { + // pad to 32 bytes + num = txList[j].toString(16); + num = Array(64-num.length).fill(0).join('') + num; + txList[j] = toHex(num); + } + } + + // confirm signature + if (txList[base+4] == 0) { + txList[base+4] = toHex(Buffer.alloc(130).toString('hex')); + } + } + + // check outputs + let outputAddresses = [10, 12]; + for (let i = 0; i < outputAddresses.length; i++) { + if (txList[outputAddresses[i]] == 0) { + txList[outputAddresses[i]] = toHex(Buffer.alloc(20).toString('hex')); + } + } + + let outputAmounts = [11, 13]; + for (let i = 0; i < outputAmounts.length; i++) { + if (txList[outputAmounts[i]] == 0) { + txList[outputAmounts[i]] = toHex(Buffer.alloc(32).toString('hex')); + } else { + // pad to 32 bytes + num = txList[outputAmounts[i]].toString(16); + num = Array(64-num.length).fill(0).join('') + num; + txList[outputAmounts[i]] = toHex(num); + } + } + + // check fee + if (txList[14] == 0) { + txList[14] = toHex(Buffer.alloc(32).toString('hex')); + } else { + num = txList[14].toString(16); + num = Array(64-num.length).fill(0).join('') + num; + txList[14] = toHex(num); + } + + return txList; +} + + +module.exports = { + fastForward, + sha256String, + sendRPC, + generateMerkleRootAndProof, + fillTxList +}; diff --git a/contracts/test/plasmamvp/transactions.js b/contracts/test/plasmamvp/transactions.js new file mode 100644 index 0000000..fab300e --- /dev/null +++ b/contracts/test/plasmamvp/transactions.js @@ -0,0 +1,511 @@ +let RLP = require('rlp'); +let assert = require('chai').assert + +let PlasmaMVP = artifacts.require('PlasmaMVP'); + +let { + fastForward, + sha256String, + generateMerkleRootAndProof, + fillTxList, +} = require('./plasmamvp_helpers.js'); + +let { toHex, catchError } = require('../utilities.js'); + +contract('[PlasmaMVP] Transactions', async (accounts) => { + let instance; + let oneWeek = 604800; // in seconds + let authority = accounts[0]; + let minExitBond = 200000; + + // deploy the instance contract before each test. + // deposit from authority and mine the first block which + // includes a spend of that full deposit to account[1] (first input) + let amount = 100; let feeAmount = 10; + let depositNonce; + let txPos, txBytes; + let proof, feeProof; + let sigs, confirmSignatures; + beforeEach(async () => { + instance = await PlasmaMVP.new({from: authority}); + + depositNonce = (await instance.depositNonce.call()).toNumber(); + await instance.deposit(authority, {from: authority, value: amount*2 + 10}); + + // deposit is the first input. authority creates two outputs. + // split equally with a fee amount of 10 + let txList = Array(15).fill(0); + txList[3] = depositNonce; + txList[10] = authority; txList[11] = amount; + txList[12] = authority; txList[13] = amount; + txList[14] = feeAmount; + txList = fillTxList(txList); + let txHash = web3.utils.soliditySha3(toHex(RLP.encode(txList).toString('hex'))); + let sigs = [toHex(await web3.eth.sign(txHash, authority)), toHex(Buffer.alloc(65).toString('hex'))]; + txBytes = [txList, sigs]; + txBytes = RLP.encode(txBytes).toString('hex'); + + let merkleHash = sha256String(txBytes); + + // submit the block + let merkleRoot; + [merkleRoot, proof] = generateMerkleRootAndProof([merkleHash], 0); + let blockNum = (await instance.lastCommittedBlock.call()).toNumber() + 1; + await instance.submitBlock([toHex(merkleRoot)], [1], [10], blockNum, {from: authority}); + + // construct the confirm signature + let confirmHash = sha256String(merkleHash + merkleRoot.slice(2)); + confirmSignatures = await web3.eth.sign(confirmHash, authority); + + txPos = [blockNum, 0, 0]; + }); + + it("Will not revert finalizeExit with an empty queue", async () => { + await instance.finalizeTransactionExits(); + }); + + it("Will reject an exit with a committedFee larger than the transaction amount", async () => { + let err; + [err] = await catchError(instance.startTransactionExit(txPos, + toHex(txBytes), toHex(proof), toHex(confirmSignatures), amount+1, {from: accounts[1], value: minExitBond})); + + if (!err) + assert.fail("Started a transaction exit with a committed fee larger than the tx amount"); + }); + + it("Allows only the utxo owner to start an exit", async () => { + let err; + [err] = await catchError(instance.startTransactionExit(txPos, + toHex(txBytes), toHex(proof), toHex(confirmSignatures), 0, {from: accounts[3], value: minExitBond})); + if (!err) + assert.fail("address exited an output it does not own"); + }); + + it("Can challenge a spend of a utxo with only a valid confirm signature", async () => { + // spend the first output back to authority + let txList = Array(15).fill(0); + txList[0] = txPos[0]; txList[1] = txPos[1]; txList[2] = txPos[2]; + txList[10] = authority; txList[11] = amount; + txList = fillTxList(txList); + txHash = web3.utils.soliditySha3(toHex(RLP.encode(txList).toString('hex'))); + let sigs = [await web3.eth.sign(txHash, authority), toHex(Buffer.alloc(65).toString('hex'))]; + let challengingTxBytes = [txList, sigs]; + challengingTxBytes = RLP.encode(challengingTxBytes).toString('hex'); + let merkleHash = sha256String(challengingTxBytes); + + // submit the block + let challengingBlockNum = (await instance.lastCommittedBlock.call()).toNumber() + 1; + let [merkleRoot, challengingProof] = generateMerkleRootAndProof([merkleHash], 0); + await instance.submitBlock([merkleRoot], [1], [0], challengingBlockNum, {from: authority}); + + let confirmationHash = sha256String(merkleHash + merkleRoot.slice(2)); + let challengingConfirmSig = await web3.eth.sign(confirmationHash, authority); + + // start an transaction exit of the input + await instance.startTransactionExit(txPos, toHex(txBytes), toHex(proof), toHex(confirmSignatures), 0, {from: authority, value: minExitBond}); + + // attempt to challenge with an invalid confirm signature + let wrongConfirmSig = await web3.eth.sign(confirmationHash, accounts[1]); + let [err] = await catchError(instance.challengeExit([...txPos, 0], [challengingBlockNum, 0], toHex(challengingTxBytes), toHex(challengingProof), + toHex(wrongConfirmSig), {from: accounts[2]})); + + // correctly challenge the exit above + await instance.challengeExit([...txPos, 0], [challengingBlockNum, 0], toHex(challengingTxBytes), toHex(challengingProof), + toHex(challengingConfirmSig), {from: accounts[2]}); + + // check that the bond has been rewarded to the challenger + let balance = (await instance.balanceOf.call(accounts[2])).toNumber(); + assert.equal(balance, minExitBond, "exit bond not rewarded to challenger"); + + let position = 1000000*txPos[0] + 10*txPos[1]; + let exit = await instance.txExits.call(position); + assert.equal(exit[4].toNumber(), 2, "Fee exit state is not Challenged"); + + // cannot reopen the exit + [err] = await catchError(instance.startTransactionExit(txPos, toHex(txBytes), toHex(proof), + toHex(confirmSignatures), 0, {from: authority, value: minExitBond})); + if (!err) + assert.fail("reopened challenged exit"); + }); + + it("Catches StartedTransactionExit event", async () => { + let tx = await instance.startTransactionExit(txPos, toHex(txBytes), + toHex(proof), toHex(confirmSignatures), 0, {from: authority, value: minExitBond}); + + assert.equal(tx.logs[0].args.position.toString(), txPos.toString(), "StartedTransactionExit event emits incorrect priority"); + assert.equal(tx.logs[0].args.owner, authority, "StartedTransactionExit event emits incorrect owner"); + assert.equal(tx.logs[0].args.amount.toNumber(), amount, "StartedTransactionExit event emits incorrect amount"); + assert.equal(tx.logs[0].args.confirmSignatures, toHex(confirmSignatures), "StartedTransactionExit event does not emit confirm signatures"); + }); + + it("Can start and finalize a transaction exit", async () => { + await instance.startTransactionExit(txPos, toHex(txBytes), + toHex(proof), toHex(confirmSignatures), 0, {from: authority, value: minExitBond}); + + await fastForward(oneWeek + 10); + + await instance.finalizeTransactionExits(); + + let balance = (await instance.balanceOf.call(authority)).toNumber(); + assert.equal(balance, amount + minExitBond); + + let position = 1000000*txPos[0]; + let exit = await instance.txExits.call(position); + assert.equal(exit[4].toNumber(), 3, "exit's state not set to finalized"); + }); + + it("Only authority to start a fee withdrawal exit", async () => { + // non-authoritys cannot start fee exits + let err; + [err] = await catchError(instance.startFeeExit(txPos[0], 0, {from: accounts[1], value: minExitBond})); + if (!err) + assert.fail("fee exit start from non-authority"); + + // authority cannot start a fee exit without putting a sufficient exit bond + [err] = await catchError(instance.startFeeExit(txPos[0], 0, {from: authority, value: minExitBond - 100})); + if (!err) + assert.fail("started fee exit with insufficient bond"); + + // the committed fee must be less than the fee amount + [err] = await catchError(instance.startFeeExit(txPos[0], feeAmount+10, {from: authority, value: minExitBond})); + if (!err) + assert.fail("started fee exit with a commited fee larger than the fee amount"); + + // cannot start a fee exit for a non-existent block + let nonExistentBlockNum = txPos[0] + 100; + [err] = await catchError(instance.startFeeExit(nonExistentBlockNum, 0, {from: authority, value: minExitBond})); + if (!err) + assert.fail("started fee exit for non-existent block"); + + // authority can start a fee exit with sufficient exit bond + await instance.startFeeExit(txPos[0], 0, {from: authority, value: minExitBond}); + + let position = 1000000*txPos[0] + 10*(Math.pow(2, 16) - 1); + let feeExit = await instance.txExits.call(position); + assert.equal(feeExit[0].toNumber(), 10, "Incorrect fee exit amount"); + assert.equal(feeExit[3], authority, "Incorrect fee exit owner"); + assert.equal(feeExit[4].toNumber(), 1, "Incorrect exit state."); + + // can only start a fee exit for any particular block once + [err] = await catchError(instance.startFeeExit(txPos[0], 0, {from: authority, value: minExitBond})); + if (!err) + assert.fail("attempted the same exit while a pending one existed"); + + // fast forward one week + await fastForward(oneWeek+10); + + // finalize exits + await instance.finalizeTransactionExits(); + + let balance = (await instance.balanceOf.call(authority)).toNumber(); + assert.equal(balance, feeAmount + minExitBond, "authority has incorrect balance"); + + feeExit = await instance.txExits.call(position); + assert.equal(feeExit[4].toNumber(), 3, "Fee exit state is not Finalized"); + }); + + it("Allows authority to challenge only a incorrect committed fee", async () => { + // spend both inputs with a fee. fee should only come from the first input + let txList2 = Array(15).fill(0); + txList2[0] = txPos[0]; txList2[1] = txPos[1]; txList2[2] = txPos[2]; + txList2[5] = txPos[0]; txList2[6] = txPos[1]; txList2[7] = 1; + txList2[10] = authority; txList2[11] = amount - 5; + txList2[12] = authority; txList2[13] = amount; + txList2[14] = 5; // fee + txList2 = fillTxList(txList2); + let txHash2 = web3.utils.soliditySha3(toHex(RLP.encode(txList2).toString('hex'))); + let sigs2 = [toHex(await web3.eth.sign(txHash2, authority)), toHex(await web3.eth.sign(txHash2, authority))]; + let txBytes2 = [txList2, sigs2]; + txBytes2 = RLP.encode(txBytes2).toString('hex'); + + // submit the block and claim the fee + let merkleHash2 = sha256String(txBytes2); + let [merkleRoot2, proof2] = generateMerkleRootAndProof([merkleHash2], 0); + let blockNum2 = (await instance.lastCommittedBlock.call()).toNumber() + 1; + await instance.submitBlock([toHex(merkleRoot2)], [1], [5], blockNum2, {from: authority}); + + // first input exits without committing to the fee + await instance.startTransactionExit(txPos, toHex(txBytes), toHex(proof), + toHex(confirmSignatures), 1, {from: authority, value: minExitBond}); + + // second input will exit (not forced to commit the fee) + let secondOutput = [txPos[0], txPos[1], 1]; + await instance.startTransactionExit(secondOutput, toHex(txBytes), toHex(proof), + toHex(confirmSignatures), 0, {from: authority, value: minExitBond}); + + // operator will try incorrectly challenge with second output + let err; + [err] = await catchError(instance.challengeExit([...secondOutput, 0], [blockNum2, 0], toHex(txBytes2), toHex(proof2), toHex(""))); + if (!err) + assert.fail("operator challenged with the second input"); + + // operator will challenge the exit with the first input + await instance.challengeExit([...txPos, 0], [blockNum2, 0], toHex(txBytes2), toHex(proof2), toHex("")); + + let exit = await instance.txExits.call(1000000*txPos[0] + 10*txPos[1]); + assert.equal(exit[4].toNumber(), 0, "exit with incorrect fee not changed to non existent after challenge"); + + // should not be able to challenge an exit which does not exist + [err] = await catchError(instance.challengeExit([...txPos, 0], [blockNum2, 0], toHex(txBytes2), toHex(proof2), toHex(""))); + if (!err) + assert.fail("operator challenged an exit which does not exist"); + + // first input will exit with the correct committed fee + await instance.startTransactionExit(txPos, toHex(txBytes), toHex(proof), + toHex(confirmSignatures), 5, {authority, value: minExitBond}); + + // operator will challenge the exit and will fail without a confirm signature + [err] = await catchError(instance.challengeExit([...txPos, 0], [blockNum2, 0], toHex(txBytes2), toHex(proof2), toHex(""))); + if (!err) + assert.fail("operator challenged an exit with the correct committed fee"); + }); + + it("Transaction signatures are checked during a fee mismatch challenge", async () => { + // spend both inputs with a fee. fee should only come from the first input + let txList2 = Array(15).fill(0); + txList2[0] = txPos[0]; txList2[1] = txPos[1]; txList2[2] = txPos[2]; + txList2[5] = txPos[0]; txList2[6] = txPos[1]; txList2[7] = 1; + txList2[10] = authority; txList2[11] = amount - 5; + txList2[12] = authority; txList2[13] = amount; + txList2[14] = 5; // fee + txList2 = fillTxList(txList2); + let txHash2 = web3.utils.soliditySha3(toHex(RLP.encode(txList2).toString('hex'))); + // incorrect sig + let sigs2 = [toHex(await web3.eth.sign(txHash2, accounts[1])), toHex(await web3.eth.sign(txHash2, authority))]; + let txBytes2 = [txList2, sigs2]; + txBytes2 = RLP.encode(txBytes2).toString('hex'); + + // submit the block and claim the fee + let merkleHash2 = sha256String(txBytes2); + let [merkleRoot2, proof2] = generateMerkleRootAndProof([merkleHash2], 0); + let blockNum2 = (await instance.lastCommittedBlock.call()).toNumber() + 1; + await instance.submitBlock([toHex(merkleRoot2)], [1], [5], blockNum2, {from: authority}); + + // first input exits not committing the fee + await instance.startTransactionExit(txPos, toHex(txBytes), toHex(proof), + toHex(confirmSignatures), 0, {from: authority, value: minExitBond}); + + // operator will try challenge with the invalid spend + let [err] = await catchError(instance.challengeExit([...txPos, 0], [blockNum2, 0], + toHex(txBytes2), toHex(proof2), toHex(""))); + if (!err) + assert.fail("challenge fee mismatch with an invalid inclusion"); + }); + + it("Challenge a fee withdrawal exit", async () => { + // spend the fee in txPos[0] + let txList = Array(15).fill(0); + txList[0] = txPos[0]; txList[1] = Math.pow(2, 16) - 1; + txList[10] = authority; txList[11] = feeAmount; + txList = fillTxList(txList); + let txHash = sha256String(toHex(RLP.encode(txList).toString('hex'))); + let sigs = [await web3.eth.sign(txHash, authority), toHex(Buffer.alloc(65).toString('hex'))]; + let txBytes = [txList, sigs]; + txBytes = RLP.encode(txBytes).toString('hex'); + + // submit the block + let merkleHash = sha256String(txBytes); + let [merkleRoot, proof] = generateMerkleRootAndProof([merkleHash], 0); + let challengingBlockNum = (await instance.lastCommittedBlock.call()).toNumber() + 1; + await instance.submitBlock([toHex(merkleRoot)], [1], [0], challengingBlockNum, {from: authority}); + + // create the confirm sig + let confirmHash = sha256String(merkleHash + merkleHash.slice(2)); + let challengingConfirmSig = await web3.eth.sign(confirmHash, authority); + + // start fee exit + await instance.startFeeExit(txPos[0], 0, {from: authority, value: minExitBond}); + + let position = 1000000*txPos[0] + 10*(Math.pow(2, 16) - 1); + let feeExit = await instance.txExits.call(position); + assert.equal(feeExit[0].toNumber(), feeAmount, "Incorrect fee exit amount"); + assert.equal(feeExit[3], authority, "Incorrect fee exit owner"); + assert.equal(feeExit[4].toNumber(), 1, "Fee exit state is not Pending"); + + // challenge fee exit + await instance.challengeExit([txPos[0], Math.pow(2, 16) - 1, 0, 0], [challengingBlockNum, 0], + toHex(txBytes), toHex(proof), toHex(challengingConfirmSig), {from: accounts[2]}); + + let balance = (await instance.balanceOf.call(accounts[2])).toNumber(); + assert.equal(balance, minExitBond, "exit bond not rewarded to challenger"); + + feeExit = await instance.txExits.call(position); + assert.equal(feeExit[4].toNumber(), 2, "Fee exit state is not Challenged"); + + // cannot reopen the fee exit + let [err] = await catchError(instance.startFeeExit(txPos[0], 0, {from: authority, value: minExitBond})); + if (!err) + assert.fail("reopend a challenged fee exit"); + }); + + it("Requires sufficient bond and refunds excess if overpayed", async () => { + let [err] = await catchError(instance.startTransactionExit(txPos, toHex(txBytes), + toHex(proof), toHex(confirmSignatures), 0, {from: authority, value: minExitBond - 100})); + if (!err) + assert.fail("started exit with insufficient bond"); + + await instance.startTransactionExit(txPos, toHex(txBytes), + toHex(proof), toHex(confirmSignatures), 0, {from: authority, value: minExitBond + 100}); + + let balance = (await instance.balanceOf(authority)).toNumber(); + assert.equal(balance, 100, "excess funds not repayed back to caller"); + }); + + it("Cannot exit a utxo with an input pending an exit", async () => { + await instance.startDepositExit(depositNonce, 0, {from: authority, value: minExitBond}); + + let err; + [err] = await catchError(instance.startTransactionExit(txPos, + toHex(txBytes), toHex(proof), toHex(confirmSignatures), 0, + {from: accounts[1], value: minExitBond})); + + if (!err) + assert.fail("started an exit with an input who has a pending exit state"); + }); + + it("Attempt a withdrawal delay attack", async () => { + let fiveDays = 432000 // in seconds + + // authority spends (txPos[0], 0, 1) utxo, sends 1 utxo to themself and the other to accounts[1] + let txList2 = Array(15).fill(0); + txList2[0] = txPos[0]; txList2[2] = 1; // first input + txList2[10] = authority; txList2[11] = amount / 2; // first output + txList2[12] = accounts[1]; txList2[13] = amount / 2; // second output + txList2 = fillTxList(txList2); + let txHash2 = web3.utils.soliditySha3(toHex(RLP.encode(txList2).toString('hex'))); + let sigs2 = [toHex(await web3.eth.sign(txHash2, authority)), toHex(Buffer.alloc(65).toString('hex'))]; + let txBytes2 = RLP.encode([txList2, sigs2]).toString('hex'); + let merkleHash2 = sha256String(txBytes2); + + let root2, proof2; + [root2, proof2] = generateMerkleRootAndProof([merkleHash2], 0); + let blockNum2 = (await instance.lastCommittedBlock.call()).toNumber() + 1; + await instance.submitBlock([toHex(root2)], [1], [0], blockNum2, {from: authority}); + + // create confirmation signature + let confirmationHash2 = sha256String(merkleHash2 + root2.slice(2)); + let confirmSigs2 = await web3.eth.sign(confirmationHash2, authority); + + // make utxos > 1 week old + await fastForward(oneWeek + 100); + + // start exit for accounts[1], last utxo to be created + await instance.startTransactionExit([blockNum2, 0, 1], + toHex(txBytes2), toHex(proof2), toHex(confirmSigs2), 0, {from: accounts[1], value: minExitBond}); + + // increase time slightly, so exit by accounts[1] has better priority than authority + await fastForward(10); + + // start exit for authority utxo + await instance.startTransactionExit([blockNum2, 0, 0], + toHex(txBytes2), toHex(proof2), toHex(confirmSigs2), 0, {from: authority, value: minExitBond}); + + // Fast Forward ~5 days + await fastForward(fiveDays); + + // Check to make sure challenge period has not ended + let position = 1000000 * blockNum2 + 1; + let currExit = await instance.txExits.call(position); + assert.ok((currExit[2] + 604800) > (await web3.eth.getBlock("latest")).timestamp); + + // start exit for oldest utxo avaliable + await instance.startTransactionExit([txPos[0], 0, 0], + toHex(txBytes), toHex(proof), toHex(confirmSignatures), 0, {from: authority, value: minExitBond}); + + // Fast Forward < 1 week + await fastForward(fiveDays); + + // finalize exits should finalize accounts[1] then authority + let finalizedExits = await instance.finalizeTransactionExits({from: authority}); + let finalizedExit = await instance.txExits.call(position); + assert.equal(finalizedExits.logs[0].args.position.toString(), [blockNum2, 0, 1, 0].toString(), "Incorrect position for finalized tx"); + assert.equal(finalizedExits.logs[0].args.owner, accounts[1], "Incorrect finalized exit owner"); + assert.equal(finalizedExits.logs[0].args.amount.toNumber(), amount/2 + minExitBond, "Incorrect finalized exit amount."); + assert.equal(finalizedExit[4].toNumber(), 3, "Incorrect finalized exit state."); + + // Check other exits + position = 1000000 * blockNum2; + finalizedExit = await instance.txExits.call(position); + assert.equal(finalizedExits.logs[2].args.position.toString(), [blockNum2, 0, 0, 0].toString(), "Incorrect position for finalized tx"); + assert.equal(finalizedExits.logs[2].args.owner, authority, "Incorrect finalized exit owner"); + assert.equal(finalizedExits.logs[2].args.amount.toNumber(), amount/2 + minExitBond, "Incorrect finalized exit amount."); + assert.equal(finalizedExit[4].toNumber(), 3, "Incorrect finalized exit state."); + + // Last exit should still be pending + position = 1000000 * txPos[0]; + let pendingExit = await instance.txExits.call(position); + assert.equal(pendingExit[3], authority, "Incorrect pending exit owner"); + assert.equal(pendingExit[0].toNumber(), amount, "Incorrect pending exit amount"); + assert.equal(pendingExit[4].toNumber(), 1, "Incorrect pending exit state."); + + // Fast Forward rest of challenge period + await fastForward(oneWeek + 1000); + await instance.finalizeTransactionExits({from: authority}); + // Check that last exit was processed + finalizedExit = await instance.txExits.call(position); + assert.equal(finalizedExit[4].toNumber(), 3, "Incorrect finalized exit state."); + }); + + it("Reverts if finalizeExit runs out of gas", async () => { + // exit both outputs + let txPos2 = [txPos[0], 0, 1]; + await instance.startTransactionExit(txPos, toHex(txBytes), toHex(proof), + toHex(confirmSignatures), 0, {from: authority, value: minExitBond}); + await instance.startTransactionExit(txPos2, toHex(txBytes), toHex(proof), + toHex(confirmSignatures), 0, {from: authority, value: minExitBond}); + + await fastForward(oneWeek + 1000); + + // Only provide enough gas for 1 txn to be finalized + await instance.finalizeTransactionExits({gas: 120000}); + + // The first utxo should have been exited correctly + let balance = (await instance.balanceOf.call(authority)).toNumber(); + assert.equal(balance, amount + minExitBond); + + let position = 1000000*txPos[0]; + let exit = await instance.txExits.call(position); + assert.equal(exit[4].toNumber(), 3, "first exit's state not set to finalized"); + + position = 1000000*txPos2[0] + 1; + exit = await instance.txExits.call(position); + assert.equal(exit[4].toNumber(), 1, "second exit should still be pending"); + }); + + it("Requires two correct confirm signatures with two inputs", async () => { + // spend both outputs + let txList = Array(15).fill(0); + txList[0] = txPos[0]; txList[5] = txPos[0]; txList[8] = 1; + txList[10] = authority; txList[11] = amount*2; + txList = fillTxList(txList); + let txHash = web3.utils.soliditySha3(toHex(RLP.encode(txList).toString('hex'))); + let sig = toHex(await web3.eth.sign(txHash, authority)); + let sigs = [sig, sig]; + let txBytes = [txList, sigs]; + txBytes = RLP.encode(txBytes).toString('hex'); + let merkleHash = sha256String(txBytes); + + let blockNum = (await instance.lastCommittedBlock.call()).toNumber() + 1; + let [merkleRoot, proof] = generateMerkleRootAndProof([merkleHash], 0); + await instance.submitBlock([toHex(merkleRoot)], [1], [0], blockNum, {from: authority}); + + let confirmHash = sha256String(merkleHash + merkleRoot.slice(2)); + let confirmSig = (await web3.eth.sign(confirmHash, authority)).slice(2); + let incorrectConfirmSig = (await web3.eth.sign(confirmHash, accounts[1])).slice(2); + + // exit the new output with incorrect sigs + let [err] = await catchError(instance.startTransactionExit([blockNum, 0, 0], toHex(txBytes), + toHex(proof), toHex(incorrectConfirmSig + confirmSig), 0, {from: authority, value: minExitBond})); + if (!err) + assert.fail("started exit with incorrect first confirm sig"); + + [err] = await catchError(instance.startTransactionExit([blockNum, 0, 0], toHex(txBytes), + toHex(proof), toHex(confirmSig + incorrectConfirmSig), 0, {from: authority, value: minExitBond})); + if (!err) + assert.fail("started exit with incorrect second confirm sig"); + + // start successfully + await instance.startTransactionExit([blockNum, 0, 0], toHex(txBytes), toHex(proof), + toHex(confirmSig + confirmSig), 0, {from: authority, value: minExitBond}); + }); +}); diff --git a/contracts/test/utilities.js b/contracts/test/utilities.js new file mode 100644 index 0000000..a5ac0f7 --- /dev/null +++ b/contracts/test/utilities.js @@ -0,0 +1,25 @@ +/* + How to avoid using try/catch blocks with promises' that could fail using async/await + - https://blog.grossman.io/how-to-write-async-await-without-try-catch-blocks-in-javascript/ + */ +let catchError = function(promise) { + return promise.then(result => [null, result]) + .catch(err => [err]); +}; + +let toHex = function(buffer) { + buffer = buffer.toString('hex'); + if (buffer.substring(0, 2) == '0x') + return buffer; + return '0x' + buffer; +}; + +let sleep = function(ms) { + return new Promise((resolve, reject) => setTimeout(resolve, ms) ) +} + +module.exports = { + catchError, + sleep, + toHex +}; diff --git a/contracts/truffle-config.js b/contracts/truffle-config.js new file mode 100644 index 0000000..a6330d6 --- /dev/null +++ b/contracts/truffle-config.js @@ -0,0 +1,4 @@ +module.exports = { + // See + // to customize your Truffle configuration! +}; diff --git a/contracts/truffle.js b/contracts/truffle.js new file mode 100644 index 0000000..96e4126 --- /dev/null +++ b/contracts/truffle.js @@ -0,0 +1,11 @@ +module.exports = { + // See + // to customize your Truffle configuration! + networks: { + development: { + host: "localhost", + port: 8545, + network_id: "*" // Match any network id + } + } +}; diff --git a/contracts/wrappers/plasma_mvp.go b/contracts/wrappers/plasma_mvp.go new file mode 100644 index 0000000..f9648ab --- /dev/null +++ b/contracts/wrappers/plasma_mvp.go @@ -0,0 +1,1886 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package wrappers + +import ( + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = abi.U256 + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription +) + +// PlasmaMVPABI is the input ABI used to generate the binding from. +const PlasmaMVPABI = "[{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"address\"}],\"name\":\"balances\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"lastCommittedBlock\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"plasmaChain\",\"outputs\":[{\"name\":\"header\",\"type\":\"bytes32\"},{\"name\":\"numTxns\",\"type\":\"uint256\"},{\"name\":\"feeAmount\",\"type\":\"uint256\"},{\"name\":\"createdAt\",\"type\":\"uint256\"},{\"name\":\"ethBlockNum\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"operator\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"depositExitQueue\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"txExits\",\"outputs\":[{\"name\":\"amount\",\"type\":\"uint256\"},{\"name\":\"committedFee\",\"type\":\"uint256\"},{\"name\":\"createdAt\",\"type\":\"uint256\"},{\"name\":\"ethBlockNum\",\"type\":\"uint256\"},{\"name\":\"owner\",\"type\":\"address\"},{\"name\":\"state\",\"type\":\"uint8\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"txExitQueue\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"deposits\",\"outputs\":[{\"name\":\"owner\",\"type\":\"address\"},{\"name\":\"amount\",\"type\":\"uint256\"},{\"name\":\"createdAt\",\"type\":\"uint256\"},{\"name\":\"ethBlockNum\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"totalWithdrawBalance\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"depositExits\",\"outputs\":[{\"name\":\"amount\",\"type\":\"uint256\"},{\"name\":\"committedFee\",\"type\":\"uint256\"},{\"name\":\"createdAt\",\"type\":\"uint256\"},{\"name\":\"ethBlockNum\",\"type\":\"uint256\"},{\"name\":\"owner\",\"type\":\"address\"},{\"name\":\"state\",\"type\":\"uint8\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"minExitBond\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"depositNonce\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"oldOperator\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"newOperator\",\"type\":\"address\"}],\"name\":\"ChangedOperator\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"AddedToBalances\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"header\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"blockNumber\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"numTxns\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"feeAmount\",\"type\":\"uint256\"}],\"name\":\"BlockSubmitted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"depositor\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"amount\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"depositNonce\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"ethBlockNum\",\"type\":\"uint256\"}],\"name\":\"Deposit\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"position\",\"type\":\"uint256[3]\"},{\"indexed\":false,\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"amount\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"confirmSignatures\",\"type\":\"bytes\"},{\"indexed\":false,\"name\":\"committedFee\",\"type\":\"uint256\"}],\"name\":\"StartedTransactionExit\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"nonce\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"amount\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"committedFee\",\"type\":\"uint256\"}],\"name\":\"StartedDepositExit\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"position\",\"type\":\"uint256[4]\"},{\"indexed\":false,\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"ChallengedExit\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"position\",\"type\":\"uint256[4]\"},{\"indexed\":false,\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"FinalizedExit\",\"type\":\"event\"},{\"constant\":false,\"inputs\":[{\"name\":\"newOperator\",\"type\":\"address\"}],\"name\":\"changeOperator\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"headers\",\"type\":\"bytes32[]\"},{\"name\":\"txnsPerBlock\",\"type\":\"uint256[]\"},{\"name\":\"feePerBlock\",\"type\":\"uint256[]\"},{\"name\":\"blockNum\",\"type\":\"uint256\"}],\"name\":\"submitBlock\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"owner\",\"type\":\"address\"}],\"name\":\"deposit\",\"outputs\":[],\"payable\":true,\"stateMutability\":\"payable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"nonce\",\"type\":\"uint256\"},{\"name\":\"committedFee\",\"type\":\"uint256\"}],\"name\":\"startDepositExit\",\"outputs\":[],\"payable\":true,\"stateMutability\":\"payable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"txPos\",\"type\":\"uint256[3]\"},{\"name\":\"txBytes\",\"type\":\"bytes\"},{\"name\":\"proof\",\"type\":\"bytes\"},{\"name\":\"confirmSignatures\",\"type\":\"bytes\"},{\"name\":\"committedFee\",\"type\":\"uint256\"}],\"name\":\"startTransactionExit\",\"outputs\":[],\"payable\":true,\"stateMutability\":\"payable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"blockNumber\",\"type\":\"uint256\"},{\"name\":\"committedFee\",\"type\":\"uint256\"}],\"name\":\"startFeeExit\",\"outputs\":[],\"payable\":true,\"stateMutability\":\"payable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"exitingTxPos\",\"type\":\"uint256[4]\"},{\"name\":\"challengingTxPos\",\"type\":\"uint256[2]\"},{\"name\":\"txBytes\",\"type\":\"bytes\"},{\"name\":\"proof\",\"type\":\"bytes\"},{\"name\":\"confirmSignature\",\"type\":\"bytes\"}],\"name\":\"challengeExit\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"finalizeDepositExits\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"finalizeTransactionExits\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"withdraw\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"plasmaChainBalance\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_address\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"txQueueLength\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"depositQueueLength\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"}]" + +// PlasmaMVP is an auto generated Go binding around an Ethereum contract. +type PlasmaMVP struct { + PlasmaMVPCaller // Read-only binding to the contract + PlasmaMVPTransactor // Write-only binding to the contract + PlasmaMVPFilterer // Log filterer for contract events +} + +// PlasmaMVPCaller is an auto generated read-only Go binding around an Ethereum contract. +type PlasmaMVPCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// PlasmaMVPTransactor is an auto generated write-only Go binding around an Ethereum contract. +type PlasmaMVPTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// PlasmaMVPFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type PlasmaMVPFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// PlasmaMVPSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type PlasmaMVPSession struct { + Contract *PlasmaMVP // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// PlasmaMVPCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type PlasmaMVPCallerSession struct { + Contract *PlasmaMVPCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// PlasmaMVPTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type PlasmaMVPTransactorSession struct { + Contract *PlasmaMVPTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// PlasmaMVPRaw is an auto generated low-level Go binding around an Ethereum contract. +type PlasmaMVPRaw struct { + Contract *PlasmaMVP // Generic contract binding to access the raw methods on +} + +// PlasmaMVPCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type PlasmaMVPCallerRaw struct { + Contract *PlasmaMVPCaller // Generic read-only contract binding to access the raw methods on +} + +// PlasmaMVPTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type PlasmaMVPTransactorRaw struct { + Contract *PlasmaMVPTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewPlasmaMVP creates a new instance of PlasmaMVP, bound to a specific deployed contract. +func NewPlasmaMVP(address common.Address, backend bind.ContractBackend) (*PlasmaMVP, error) { + contract, err := bindPlasmaMVP(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &PlasmaMVP{PlasmaMVPCaller: PlasmaMVPCaller{contract: contract}, PlasmaMVPTransactor: PlasmaMVPTransactor{contract: contract}, PlasmaMVPFilterer: PlasmaMVPFilterer{contract: contract}}, nil +} + +// NewPlasmaMVPCaller creates a new read-only instance of PlasmaMVP, bound to a specific deployed contract. +func NewPlasmaMVPCaller(address common.Address, caller bind.ContractCaller) (*PlasmaMVPCaller, error) { + contract, err := bindPlasmaMVP(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &PlasmaMVPCaller{contract: contract}, nil +} + +// NewPlasmaMVPTransactor creates a new write-only instance of PlasmaMVP, bound to a specific deployed contract. +func NewPlasmaMVPTransactor(address common.Address, transactor bind.ContractTransactor) (*PlasmaMVPTransactor, error) { + contract, err := bindPlasmaMVP(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &PlasmaMVPTransactor{contract: contract}, nil +} + +// NewPlasmaMVPFilterer creates a new log filterer instance of PlasmaMVP, bound to a specific deployed contract. +func NewPlasmaMVPFilterer(address common.Address, filterer bind.ContractFilterer) (*PlasmaMVPFilterer, error) { + contract, err := bindPlasmaMVP(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &PlasmaMVPFilterer{contract: contract}, nil +} + +// bindPlasmaMVP binds a generic wrapper to an already deployed contract. +func bindPlasmaMVP(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := abi.JSON(strings.NewReader(PlasmaMVPABI)) + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_PlasmaMVP *PlasmaMVPRaw) Call(opts *bind.CallOpts, result interface{}, method string, params ...interface{}) error { + return _PlasmaMVP.Contract.PlasmaMVPCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_PlasmaMVP *PlasmaMVPRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _PlasmaMVP.Contract.PlasmaMVPTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_PlasmaMVP *PlasmaMVPRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _PlasmaMVP.Contract.PlasmaMVPTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_PlasmaMVP *PlasmaMVPCallerRaw) Call(opts *bind.CallOpts, result interface{}, method string, params ...interface{}) error { + return _PlasmaMVP.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_PlasmaMVP *PlasmaMVPTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _PlasmaMVP.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_PlasmaMVP *PlasmaMVPTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _PlasmaMVP.Contract.contract.Transact(opts, method, params...) +} + +// BalanceOf is a free data retrieval call binding the contract method 0x70a08231. +// +// Solidity: function balanceOf(_address address) constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPCaller) BalanceOf(opts *bind.CallOpts, _address common.Address) (*big.Int, error) { + var ( + ret0 = new(*big.Int) + ) + out := ret0 + err := _PlasmaMVP.contract.Call(opts, out, "balanceOf", _address) + return *ret0, err +} + +// BalanceOf is a free data retrieval call binding the contract method 0x70a08231. +// +// Solidity: function balanceOf(_address address) constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPSession) BalanceOf(_address common.Address) (*big.Int, error) { + return _PlasmaMVP.Contract.BalanceOf(&_PlasmaMVP.CallOpts, _address) +} + +// BalanceOf is a free data retrieval call binding the contract method 0x70a08231. +// +// Solidity: function balanceOf(_address address) constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPCallerSession) BalanceOf(_address common.Address) (*big.Int, error) { + return _PlasmaMVP.Contract.BalanceOf(&_PlasmaMVP.CallOpts, _address) +} + +// Balances is a free data retrieval call binding the contract method 0x27e235e3. +// +// Solidity: function balances( address) constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPCaller) Balances(opts *bind.CallOpts, arg0 common.Address) (*big.Int, error) { + var ( + ret0 = new(*big.Int) + ) + out := ret0 + err := _PlasmaMVP.contract.Call(opts, out, "balances", arg0) + return *ret0, err +} + +// Balances is a free data retrieval call binding the contract method 0x27e235e3. +// +// Solidity: function balances( address) constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPSession) Balances(arg0 common.Address) (*big.Int, error) { + return _PlasmaMVP.Contract.Balances(&_PlasmaMVP.CallOpts, arg0) +} + +// Balances is a free data retrieval call binding the contract method 0x27e235e3. +// +// Solidity: function balances( address) constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPCallerSession) Balances(arg0 common.Address) (*big.Int, error) { + return _PlasmaMVP.Contract.Balances(&_PlasmaMVP.CallOpts, arg0) +} + +// DepositExitQueue is a free data retrieval call binding the contract method 0x5b3081d7. +// +// Solidity: function depositExitQueue( uint256) constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPCaller) DepositExitQueue(opts *bind.CallOpts, arg0 *big.Int) (*big.Int, error) { + var ( + ret0 = new(*big.Int) + ) + out := ret0 + err := _PlasmaMVP.contract.Call(opts, out, "depositExitQueue", arg0) + return *ret0, err +} + +// DepositExitQueue is a free data retrieval call binding the contract method 0x5b3081d7. +// +// Solidity: function depositExitQueue( uint256) constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPSession) DepositExitQueue(arg0 *big.Int) (*big.Int, error) { + return _PlasmaMVP.Contract.DepositExitQueue(&_PlasmaMVP.CallOpts, arg0) +} + +// DepositExitQueue is a free data retrieval call binding the contract method 0x5b3081d7. +// +// Solidity: function depositExitQueue( uint256) constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPCallerSession) DepositExitQueue(arg0 *big.Int) (*big.Int, error) { + return _PlasmaMVP.Contract.DepositExitQueue(&_PlasmaMVP.CallOpts, arg0) +} + +// DepositExits is a free data retrieval call binding the contract method 0xce84f906. +// +// Solidity: function depositExits( uint256) constant returns(amount uint256, committedFee uint256, createdAt uint256, ethBlockNum uint256, owner address, state uint8) +func (_PlasmaMVP *PlasmaMVPCaller) DepositExits(opts *bind.CallOpts, arg0 *big.Int) (struct { + Amount *big.Int + CommittedFee *big.Int + CreatedAt *big.Int + EthBlockNum *big.Int + Owner common.Address + State uint8 +}, error) { + ret := new(struct { + Amount *big.Int + CommittedFee *big.Int + CreatedAt *big.Int + EthBlockNum *big.Int + Owner common.Address + State uint8 + }) + out := ret + err := _PlasmaMVP.contract.Call(opts, out, "depositExits", arg0) + return *ret, err +} + +// DepositExits is a free data retrieval call binding the contract method 0xce84f906. +// +// Solidity: function depositExits( uint256) constant returns(amount uint256, committedFee uint256, createdAt uint256, ethBlockNum uint256, owner address, state uint8) +func (_PlasmaMVP *PlasmaMVPSession) DepositExits(arg0 *big.Int) (struct { + Amount *big.Int + CommittedFee *big.Int + CreatedAt *big.Int + EthBlockNum *big.Int + Owner common.Address + State uint8 +}, error) { + return _PlasmaMVP.Contract.DepositExits(&_PlasmaMVP.CallOpts, arg0) +} + +// DepositExits is a free data retrieval call binding the contract method 0xce84f906. +// +// Solidity: function depositExits( uint256) constant returns(amount uint256, committedFee uint256, createdAt uint256, ethBlockNum uint256, owner address, state uint8) +func (_PlasmaMVP *PlasmaMVPCallerSession) DepositExits(arg0 *big.Int) (struct { + Amount *big.Int + CommittedFee *big.Int + CreatedAt *big.Int + EthBlockNum *big.Int + Owner common.Address + State uint8 +}, error) { + return _PlasmaMVP.Contract.DepositExits(&_PlasmaMVP.CallOpts, arg0) +} + +// DepositNonce is a free data retrieval call binding the contract method 0xde35f5cb. +// +// Solidity: function depositNonce() constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPCaller) DepositNonce(opts *bind.CallOpts) (*big.Int, error) { + var ( + ret0 = new(*big.Int) + ) + out := ret0 + err := _PlasmaMVP.contract.Call(opts, out, "depositNonce") + return *ret0, err +} + +// DepositNonce is a free data retrieval call binding the contract method 0xde35f5cb. +// +// Solidity: function depositNonce() constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPSession) DepositNonce() (*big.Int, error) { + return _PlasmaMVP.Contract.DepositNonce(&_PlasmaMVP.CallOpts) +} + +// DepositNonce is a free data retrieval call binding the contract method 0xde35f5cb. +// +// Solidity: function depositNonce() constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPCallerSession) DepositNonce() (*big.Int, error) { + return _PlasmaMVP.Contract.DepositNonce(&_PlasmaMVP.CallOpts) +} + +// DepositQueueLength is a free data retrieval call binding the contract method 0xca11dd97. +// +// Solidity: function depositQueueLength() constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPCaller) DepositQueueLength(opts *bind.CallOpts) (*big.Int, error) { + var ( + ret0 = new(*big.Int) + ) + out := ret0 + err := _PlasmaMVP.contract.Call(opts, out, "depositQueueLength") + return *ret0, err +} + +// DepositQueueLength is a free data retrieval call binding the contract method 0xca11dd97. +// +// Solidity: function depositQueueLength() constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPSession) DepositQueueLength() (*big.Int, error) { + return _PlasmaMVP.Contract.DepositQueueLength(&_PlasmaMVP.CallOpts) +} + +// DepositQueueLength is a free data retrieval call binding the contract method 0xca11dd97. +// +// Solidity: function depositQueueLength() constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPCallerSession) DepositQueueLength() (*big.Int, error) { + return _PlasmaMVP.Contract.DepositQueueLength(&_PlasmaMVP.CallOpts) +} + +// Deposits is a free data retrieval call binding the contract method 0xb02c43d0. +// +// Solidity: function deposits( uint256) constant returns(owner address, amount uint256, createdAt uint256, ethBlockNum uint256) +func (_PlasmaMVP *PlasmaMVPCaller) Deposits(opts *bind.CallOpts, arg0 *big.Int) (struct { + Owner common.Address + Amount *big.Int + CreatedAt *big.Int + EthBlockNum *big.Int +}, error) { + ret := new(struct { + Owner common.Address + Amount *big.Int + CreatedAt *big.Int + EthBlockNum *big.Int + }) + out := ret + err := _PlasmaMVP.contract.Call(opts, out, "deposits", arg0) + return *ret, err +} + +// Deposits is a free data retrieval call binding the contract method 0xb02c43d0. +// +// Solidity: function deposits( uint256) constant returns(owner address, amount uint256, createdAt uint256, ethBlockNum uint256) +func (_PlasmaMVP *PlasmaMVPSession) Deposits(arg0 *big.Int) (struct { + Owner common.Address + Amount *big.Int + CreatedAt *big.Int + EthBlockNum *big.Int +}, error) { + return _PlasmaMVP.Contract.Deposits(&_PlasmaMVP.CallOpts, arg0) +} + +// Deposits is a free data retrieval call binding the contract method 0xb02c43d0. +// +// Solidity: function deposits( uint256) constant returns(owner address, amount uint256, createdAt uint256, ethBlockNum uint256) +func (_PlasmaMVP *PlasmaMVPCallerSession) Deposits(arg0 *big.Int) (struct { + Owner common.Address + Amount *big.Int + CreatedAt *big.Int + EthBlockNum *big.Int +}, error) { + return _PlasmaMVP.Contract.Deposits(&_PlasmaMVP.CallOpts, arg0) +} + +// LastCommittedBlock is a free data retrieval call binding the contract method 0x3acb097a. +// +// Solidity: function lastCommittedBlock() constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPCaller) LastCommittedBlock(opts *bind.CallOpts) (*big.Int, error) { + var ( + ret0 = new(*big.Int) + ) + out := ret0 + err := _PlasmaMVP.contract.Call(opts, out, "lastCommittedBlock") + return *ret0, err +} + +// LastCommittedBlock is a free data retrieval call binding the contract method 0x3acb097a. +// +// Solidity: function lastCommittedBlock() constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPSession) LastCommittedBlock() (*big.Int, error) { + return _PlasmaMVP.Contract.LastCommittedBlock(&_PlasmaMVP.CallOpts) +} + +// LastCommittedBlock is a free data retrieval call binding the contract method 0x3acb097a. +// +// Solidity: function lastCommittedBlock() constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPCallerSession) LastCommittedBlock() (*big.Int, error) { + return _PlasmaMVP.Contract.LastCommittedBlock(&_PlasmaMVP.CallOpts) +} + +// MinExitBond is a free data retrieval call binding the contract method 0xd68545a3. +// +// Solidity: function minExitBond() constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPCaller) MinExitBond(opts *bind.CallOpts) (*big.Int, error) { + var ( + ret0 = new(*big.Int) + ) + out := ret0 + err := _PlasmaMVP.contract.Call(opts, out, "minExitBond") + return *ret0, err +} + +// MinExitBond is a free data retrieval call binding the contract method 0xd68545a3. +// +// Solidity: function minExitBond() constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPSession) MinExitBond() (*big.Int, error) { + return _PlasmaMVP.Contract.MinExitBond(&_PlasmaMVP.CallOpts) +} + +// MinExitBond is a free data retrieval call binding the contract method 0xd68545a3. +// +// Solidity: function minExitBond() constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPCallerSession) MinExitBond() (*big.Int, error) { + return _PlasmaMVP.Contract.MinExitBond(&_PlasmaMVP.CallOpts) +} + +// Operator is a free data retrieval call binding the contract method 0x570ca735. +// +// Solidity: function operator() constant returns(address) +func (_PlasmaMVP *PlasmaMVPCaller) Operator(opts *bind.CallOpts) (common.Address, error) { + var ( + ret0 = new(common.Address) + ) + out := ret0 + err := _PlasmaMVP.contract.Call(opts, out, "operator") + return *ret0, err +} + +// Operator is a free data retrieval call binding the contract method 0x570ca735. +// +// Solidity: function operator() constant returns(address) +func (_PlasmaMVP *PlasmaMVPSession) Operator() (common.Address, error) { + return _PlasmaMVP.Contract.Operator(&_PlasmaMVP.CallOpts) +} + +// Operator is a free data retrieval call binding the contract method 0x570ca735. +// +// Solidity: function operator() constant returns(address) +func (_PlasmaMVP *PlasmaMVPCallerSession) Operator() (common.Address, error) { + return _PlasmaMVP.Contract.Operator(&_PlasmaMVP.CallOpts) +} + +// PlasmaChain is a free data retrieval call binding the contract method 0x3cdac42c. +// +// Solidity: function plasmaChain( uint256) constant returns(header bytes32, numTxns uint256, feeAmount uint256, createdAt uint256, ethBlockNum uint256) +func (_PlasmaMVP *PlasmaMVPCaller) PlasmaChain(opts *bind.CallOpts, arg0 *big.Int) (struct { + Header [32]byte + NumTxns *big.Int + FeeAmount *big.Int + CreatedAt *big.Int + EthBlockNum *big.Int +}, error) { + ret := new(struct { + Header [32]byte + NumTxns *big.Int + FeeAmount *big.Int + CreatedAt *big.Int + EthBlockNum *big.Int + }) + out := ret + err := _PlasmaMVP.contract.Call(opts, out, "plasmaChain", arg0) + return *ret, err +} + +// PlasmaChain is a free data retrieval call binding the contract method 0x3cdac42c. +// +// Solidity: function plasmaChain( uint256) constant returns(header bytes32, numTxns uint256, feeAmount uint256, createdAt uint256, ethBlockNum uint256) +func (_PlasmaMVP *PlasmaMVPSession) PlasmaChain(arg0 *big.Int) (struct { + Header [32]byte + NumTxns *big.Int + FeeAmount *big.Int + CreatedAt *big.Int + EthBlockNum *big.Int +}, error) { + return _PlasmaMVP.Contract.PlasmaChain(&_PlasmaMVP.CallOpts, arg0) +} + +// PlasmaChain is a free data retrieval call binding the contract method 0x3cdac42c. +// +// Solidity: function plasmaChain( uint256) constant returns(header bytes32, numTxns uint256, feeAmount uint256, createdAt uint256, ethBlockNum uint256) +func (_PlasmaMVP *PlasmaMVPCallerSession) PlasmaChain(arg0 *big.Int) (struct { + Header [32]byte + NumTxns *big.Int + FeeAmount *big.Int + CreatedAt *big.Int + EthBlockNum *big.Int +}, error) { + return _PlasmaMVP.Contract.PlasmaChain(&_PlasmaMVP.CallOpts, arg0) +} + +// PlasmaChainBalance is a free data retrieval call binding the contract method 0x45cbefa2. +// +// Solidity: function plasmaChainBalance() constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPCaller) PlasmaChainBalance(opts *bind.CallOpts) (*big.Int, error) { + var ( + ret0 = new(*big.Int) + ) + out := ret0 + err := _PlasmaMVP.contract.Call(opts, out, "plasmaChainBalance") + return *ret0, err +} + +// PlasmaChainBalance is a free data retrieval call binding the contract method 0x45cbefa2. +// +// Solidity: function plasmaChainBalance() constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPSession) PlasmaChainBalance() (*big.Int, error) { + return _PlasmaMVP.Contract.PlasmaChainBalance(&_PlasmaMVP.CallOpts) +} + +// PlasmaChainBalance is a free data retrieval call binding the contract method 0x45cbefa2. +// +// Solidity: function plasmaChainBalance() constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPCallerSession) PlasmaChainBalance() (*big.Int, error) { + return _PlasmaMVP.Contract.PlasmaChainBalance(&_PlasmaMVP.CallOpts) +} + +// TotalWithdrawBalance is a free data retrieval call binding the contract method 0xc430c438. +// +// Solidity: function totalWithdrawBalance() constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPCaller) TotalWithdrawBalance(opts *bind.CallOpts) (*big.Int, error) { + var ( + ret0 = new(*big.Int) + ) + out := ret0 + err := _PlasmaMVP.contract.Call(opts, out, "totalWithdrawBalance") + return *ret0, err +} + +// TotalWithdrawBalance is a free data retrieval call binding the contract method 0xc430c438. +// +// Solidity: function totalWithdrawBalance() constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPSession) TotalWithdrawBalance() (*big.Int, error) { + return _PlasmaMVP.Contract.TotalWithdrawBalance(&_PlasmaMVP.CallOpts) +} + +// TotalWithdrawBalance is a free data retrieval call binding the contract method 0xc430c438. +// +// Solidity: function totalWithdrawBalance() constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPCallerSession) TotalWithdrawBalance() (*big.Int, error) { + return _PlasmaMVP.Contract.TotalWithdrawBalance(&_PlasmaMVP.CallOpts) +} + +// TxExitQueue is a free data retrieval call binding the contract method 0x875b8ea0. +// +// Solidity: function txExitQueue( uint256) constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPCaller) TxExitQueue(opts *bind.CallOpts, arg0 *big.Int) (*big.Int, error) { + var ( + ret0 = new(*big.Int) + ) + out := ret0 + err := _PlasmaMVP.contract.Call(opts, out, "txExitQueue", arg0) + return *ret0, err +} + +// TxExitQueue is a free data retrieval call binding the contract method 0x875b8ea0. +// +// Solidity: function txExitQueue( uint256) constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPSession) TxExitQueue(arg0 *big.Int) (*big.Int, error) { + return _PlasmaMVP.Contract.TxExitQueue(&_PlasmaMVP.CallOpts, arg0) +} + +// TxExitQueue is a free data retrieval call binding the contract method 0x875b8ea0. +// +// Solidity: function txExitQueue( uint256) constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPCallerSession) TxExitQueue(arg0 *big.Int) (*big.Int, error) { + return _PlasmaMVP.Contract.TxExitQueue(&_PlasmaMVP.CallOpts, arg0) +} + +// TxExits is a free data retrieval call binding the contract method 0x6d3d8b1a. +// +// Solidity: function txExits( uint256) constant returns(amount uint256, committedFee uint256, createdAt uint256, ethBlockNum uint256, owner address, state uint8) +func (_PlasmaMVP *PlasmaMVPCaller) TxExits(opts *bind.CallOpts, arg0 *big.Int) (struct { + Amount *big.Int + CommittedFee *big.Int + CreatedAt *big.Int + EthBlockNum *big.Int + Owner common.Address + State uint8 +}, error) { + ret := new(struct { + Amount *big.Int + CommittedFee *big.Int + CreatedAt *big.Int + EthBlockNum *big.Int + Owner common.Address + State uint8 + }) + out := ret + err := _PlasmaMVP.contract.Call(opts, out, "txExits", arg0) + return *ret, err +} + +// TxExits is a free data retrieval call binding the contract method 0x6d3d8b1a. +// +// Solidity: function txExits( uint256) constant returns(amount uint256, committedFee uint256, createdAt uint256, ethBlockNum uint256, owner address, state uint8) +func (_PlasmaMVP *PlasmaMVPSession) TxExits(arg0 *big.Int) (struct { + Amount *big.Int + CommittedFee *big.Int + CreatedAt *big.Int + EthBlockNum *big.Int + Owner common.Address + State uint8 +}, error) { + return _PlasmaMVP.Contract.TxExits(&_PlasmaMVP.CallOpts, arg0) +} + +// TxExits is a free data retrieval call binding the contract method 0x6d3d8b1a. +// +// Solidity: function txExits( uint256) constant returns(amount uint256, committedFee uint256, createdAt uint256, ethBlockNum uint256, owner address, state uint8) +func (_PlasmaMVP *PlasmaMVPCallerSession) TxExits(arg0 *big.Int) (struct { + Amount *big.Int + CommittedFee *big.Int + CreatedAt *big.Int + EthBlockNum *big.Int + Owner common.Address + State uint8 +}, error) { + return _PlasmaMVP.Contract.TxExits(&_PlasmaMVP.CallOpts, arg0) +} + +// TxQueueLength is a free data retrieval call binding the contract method 0xb06f4777. +// +// Solidity: function txQueueLength() constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPCaller) TxQueueLength(opts *bind.CallOpts) (*big.Int, error) { + var ( + ret0 = new(*big.Int) + ) + out := ret0 + err := _PlasmaMVP.contract.Call(opts, out, "txQueueLength") + return *ret0, err +} + +// TxQueueLength is a free data retrieval call binding the contract method 0xb06f4777. +// +// Solidity: function txQueueLength() constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPSession) TxQueueLength() (*big.Int, error) { + return _PlasmaMVP.Contract.TxQueueLength(&_PlasmaMVP.CallOpts) +} + +// TxQueueLength is a free data retrieval call binding the contract method 0xb06f4777. +// +// Solidity: function txQueueLength() constant returns(uint256) +func (_PlasmaMVP *PlasmaMVPCallerSession) TxQueueLength() (*big.Int, error) { + return _PlasmaMVP.Contract.TxQueueLength(&_PlasmaMVP.CallOpts) +} + +// ChallengeExit is a paid mutator transaction binding the contract method 0xd344e8e4. +// +// Solidity: function challengeExit(exitingTxPos uint256[4], challengingTxPos uint256[2], txBytes bytes, proof bytes, confirmSignature bytes) returns() +func (_PlasmaMVP *PlasmaMVPTransactor) ChallengeExit(opts *bind.TransactOpts, exitingTxPos [4]*big.Int, challengingTxPos [2]*big.Int, txBytes []byte, proof []byte, confirmSignature []byte) (*types.Transaction, error) { + return _PlasmaMVP.contract.Transact(opts, "challengeExit", exitingTxPos, challengingTxPos, txBytes, proof, confirmSignature) +} + +// ChallengeExit is a paid mutator transaction binding the contract method 0xd344e8e4. +// +// Solidity: function challengeExit(exitingTxPos uint256[4], challengingTxPos uint256[2], txBytes bytes, proof bytes, confirmSignature bytes) returns() +func (_PlasmaMVP *PlasmaMVPSession) ChallengeExit(exitingTxPos [4]*big.Int, challengingTxPos [2]*big.Int, txBytes []byte, proof []byte, confirmSignature []byte) (*types.Transaction, error) { + return _PlasmaMVP.Contract.ChallengeExit(&_PlasmaMVP.TransactOpts, exitingTxPos, challengingTxPos, txBytes, proof, confirmSignature) +} + +// ChallengeExit is a paid mutator transaction binding the contract method 0xd344e8e4. +// +// Solidity: function challengeExit(exitingTxPos uint256[4], challengingTxPos uint256[2], txBytes bytes, proof bytes, confirmSignature bytes) returns() +func (_PlasmaMVP *PlasmaMVPTransactorSession) ChallengeExit(exitingTxPos [4]*big.Int, challengingTxPos [2]*big.Int, txBytes []byte, proof []byte, confirmSignature []byte) (*types.Transaction, error) { + return _PlasmaMVP.Contract.ChallengeExit(&_PlasmaMVP.TransactOpts, exitingTxPos, challengingTxPos, txBytes, proof, confirmSignature) +} + +// ChangeOperator is a paid mutator transaction binding the contract method 0x06394c9b. +// +// Solidity: function changeOperator(newOperator address) returns() +func (_PlasmaMVP *PlasmaMVPTransactor) ChangeOperator(opts *bind.TransactOpts, newOperator common.Address) (*types.Transaction, error) { + return _PlasmaMVP.contract.Transact(opts, "changeOperator", newOperator) +} + +// ChangeOperator is a paid mutator transaction binding the contract method 0x06394c9b. +// +// Solidity: function changeOperator(newOperator address) returns() +func (_PlasmaMVP *PlasmaMVPSession) ChangeOperator(newOperator common.Address) (*types.Transaction, error) { + return _PlasmaMVP.Contract.ChangeOperator(&_PlasmaMVP.TransactOpts, newOperator) +} + +// ChangeOperator is a paid mutator transaction binding the contract method 0x06394c9b. +// +// Solidity: function changeOperator(newOperator address) returns() +func (_PlasmaMVP *PlasmaMVPTransactorSession) ChangeOperator(newOperator common.Address) (*types.Transaction, error) { + return _PlasmaMVP.Contract.ChangeOperator(&_PlasmaMVP.TransactOpts, newOperator) +} + +// Deposit is a paid mutator transaction binding the contract method 0xf340fa01. +// +// Solidity: function deposit(owner address) returns() +func (_PlasmaMVP *PlasmaMVPTransactor) Deposit(opts *bind.TransactOpts, owner common.Address) (*types.Transaction, error) { + return _PlasmaMVP.contract.Transact(opts, "deposit", owner) +} + +// Deposit is a paid mutator transaction binding the contract method 0xf340fa01. +// +// Solidity: function deposit(owner address) returns() +func (_PlasmaMVP *PlasmaMVPSession) Deposit(owner common.Address) (*types.Transaction, error) { + return _PlasmaMVP.Contract.Deposit(&_PlasmaMVP.TransactOpts, owner) +} + +// Deposit is a paid mutator transaction binding the contract method 0xf340fa01. +// +// Solidity: function deposit(owner address) returns() +func (_PlasmaMVP *PlasmaMVPTransactorSession) Deposit(owner common.Address) (*types.Transaction, error) { + return _PlasmaMVP.Contract.Deposit(&_PlasmaMVP.TransactOpts, owner) +} + +// FinalizeDepositExits is a paid mutator transaction binding the contract method 0xfcf5f9eb. +// +// Solidity: function finalizeDepositExits() returns() +func (_PlasmaMVP *PlasmaMVPTransactor) FinalizeDepositExits(opts *bind.TransactOpts) (*types.Transaction, error) { + return _PlasmaMVP.contract.Transact(opts, "finalizeDepositExits") +} + +// FinalizeDepositExits is a paid mutator transaction binding the contract method 0xfcf5f9eb. +// +// Solidity: function finalizeDepositExits() returns() +func (_PlasmaMVP *PlasmaMVPSession) FinalizeDepositExits() (*types.Transaction, error) { + return _PlasmaMVP.Contract.FinalizeDepositExits(&_PlasmaMVP.TransactOpts) +} + +// FinalizeDepositExits is a paid mutator transaction binding the contract method 0xfcf5f9eb. +// +// Solidity: function finalizeDepositExits() returns() +func (_PlasmaMVP *PlasmaMVPTransactorSession) FinalizeDepositExits() (*types.Transaction, error) { + return _PlasmaMVP.Contract.FinalizeDepositExits(&_PlasmaMVP.TransactOpts) +} + +// FinalizeTransactionExits is a paid mutator transaction binding the contract method 0x884fc7d6. +// +// Solidity: function finalizeTransactionExits() returns() +func (_PlasmaMVP *PlasmaMVPTransactor) FinalizeTransactionExits(opts *bind.TransactOpts) (*types.Transaction, error) { + return _PlasmaMVP.contract.Transact(opts, "finalizeTransactionExits") +} + +// FinalizeTransactionExits is a paid mutator transaction binding the contract method 0x884fc7d6. +// +// Solidity: function finalizeTransactionExits() returns() +func (_PlasmaMVP *PlasmaMVPSession) FinalizeTransactionExits() (*types.Transaction, error) { + return _PlasmaMVP.Contract.FinalizeTransactionExits(&_PlasmaMVP.TransactOpts) +} + +// FinalizeTransactionExits is a paid mutator transaction binding the contract method 0x884fc7d6. +// +// Solidity: function finalizeTransactionExits() returns() +func (_PlasmaMVP *PlasmaMVPTransactorSession) FinalizeTransactionExits() (*types.Transaction, error) { + return _PlasmaMVP.Contract.FinalizeTransactionExits(&_PlasmaMVP.TransactOpts) +} + +// StartDepositExit is a paid mutator transaction binding the contract method 0x70e4abf6. +// +// Solidity: function startDepositExit(nonce uint256, committedFee uint256) returns() +func (_PlasmaMVP *PlasmaMVPTransactor) StartDepositExit(opts *bind.TransactOpts, nonce *big.Int, committedFee *big.Int) (*types.Transaction, error) { + return _PlasmaMVP.contract.Transact(opts, "startDepositExit", nonce, committedFee) +} + +// StartDepositExit is a paid mutator transaction binding the contract method 0x70e4abf6. +// +// Solidity: function startDepositExit(nonce uint256, committedFee uint256) returns() +func (_PlasmaMVP *PlasmaMVPSession) StartDepositExit(nonce *big.Int, committedFee *big.Int) (*types.Transaction, error) { + return _PlasmaMVP.Contract.StartDepositExit(&_PlasmaMVP.TransactOpts, nonce, committedFee) +} + +// StartDepositExit is a paid mutator transaction binding the contract method 0x70e4abf6. +// +// Solidity: function startDepositExit(nonce uint256, committedFee uint256) returns() +func (_PlasmaMVP *PlasmaMVPTransactorSession) StartDepositExit(nonce *big.Int, committedFee *big.Int) (*types.Transaction, error) { + return _PlasmaMVP.Contract.StartDepositExit(&_PlasmaMVP.TransactOpts, nonce, committedFee) +} + +// StartFeeExit is a paid mutator transaction binding the contract method 0xed1695df. +// +// Solidity: function startFeeExit(blockNumber uint256, committedFee uint256) returns() +func (_PlasmaMVP *PlasmaMVPTransactor) StartFeeExit(opts *bind.TransactOpts, blockNumber *big.Int, committedFee *big.Int) (*types.Transaction, error) { + return _PlasmaMVP.contract.Transact(opts, "startFeeExit", blockNumber, committedFee) +} + +// StartFeeExit is a paid mutator transaction binding the contract method 0xed1695df. +// +// Solidity: function startFeeExit(blockNumber uint256, committedFee uint256) returns() +func (_PlasmaMVP *PlasmaMVPSession) StartFeeExit(blockNumber *big.Int, committedFee *big.Int) (*types.Transaction, error) { + return _PlasmaMVP.Contract.StartFeeExit(&_PlasmaMVP.TransactOpts, blockNumber, committedFee) +} + +// StartFeeExit is a paid mutator transaction binding the contract method 0xed1695df. +// +// Solidity: function startFeeExit(blockNumber uint256, committedFee uint256) returns() +func (_PlasmaMVP *PlasmaMVPTransactorSession) StartFeeExit(blockNumber *big.Int, committedFee *big.Int) (*types.Transaction, error) { + return _PlasmaMVP.Contract.StartFeeExit(&_PlasmaMVP.TransactOpts, blockNumber, committedFee) +} + +// StartTransactionExit is a paid mutator transaction binding the contract method 0xcf024ea6. +// +// Solidity: function startTransactionExit(txPos uint256[3], txBytes bytes, proof bytes, confirmSignatures bytes, committedFee uint256) returns() +func (_PlasmaMVP *PlasmaMVPTransactor) StartTransactionExit(opts *bind.TransactOpts, txPos [3]*big.Int, txBytes []byte, proof []byte, confirmSignatures []byte, committedFee *big.Int) (*types.Transaction, error) { + return _PlasmaMVP.contract.Transact(opts, "startTransactionExit", txPos, txBytes, proof, confirmSignatures, committedFee) +} + +// StartTransactionExit is a paid mutator transaction binding the contract method 0xcf024ea6. +// +// Solidity: function startTransactionExit(txPos uint256[3], txBytes bytes, proof bytes, confirmSignatures bytes, committedFee uint256) returns() +func (_PlasmaMVP *PlasmaMVPSession) StartTransactionExit(txPos [3]*big.Int, txBytes []byte, proof []byte, confirmSignatures []byte, committedFee *big.Int) (*types.Transaction, error) { + return _PlasmaMVP.Contract.StartTransactionExit(&_PlasmaMVP.TransactOpts, txPos, txBytes, proof, confirmSignatures, committedFee) +} + +// StartTransactionExit is a paid mutator transaction binding the contract method 0xcf024ea6. +// +// Solidity: function startTransactionExit(txPos uint256[3], txBytes bytes, proof bytes, confirmSignatures bytes, committedFee uint256) returns() +func (_PlasmaMVP *PlasmaMVPTransactorSession) StartTransactionExit(txPos [3]*big.Int, txBytes []byte, proof []byte, confirmSignatures []byte, committedFee *big.Int) (*types.Transaction, error) { + return _PlasmaMVP.Contract.StartTransactionExit(&_PlasmaMVP.TransactOpts, txPos, txBytes, proof, confirmSignatures, committedFee) +} + +// SubmitBlock is a paid mutator transaction binding the contract method 0xd84ba62f. +// +// Solidity: function submitBlock(headers bytes32[], txnsPerBlock uint256[], feePerBlock uint256[], blockNum uint256) returns() +func (_PlasmaMVP *PlasmaMVPTransactor) SubmitBlock(opts *bind.TransactOpts, headers [][32]byte, txnsPerBlock []*big.Int, feePerBlock []*big.Int, blockNum *big.Int) (*types.Transaction, error) { + return _PlasmaMVP.contract.Transact(opts, "submitBlock", headers, txnsPerBlock, feePerBlock, blockNum) +} + +// SubmitBlock is a paid mutator transaction binding the contract method 0xd84ba62f. +// +// Solidity: function submitBlock(headers bytes32[], txnsPerBlock uint256[], feePerBlock uint256[], blockNum uint256) returns() +func (_PlasmaMVP *PlasmaMVPSession) SubmitBlock(headers [][32]byte, txnsPerBlock []*big.Int, feePerBlock []*big.Int, blockNum *big.Int) (*types.Transaction, error) { + return _PlasmaMVP.Contract.SubmitBlock(&_PlasmaMVP.TransactOpts, headers, txnsPerBlock, feePerBlock, blockNum) +} + +// SubmitBlock is a paid mutator transaction binding the contract method 0xd84ba62f. +// +// Solidity: function submitBlock(headers bytes32[], txnsPerBlock uint256[], feePerBlock uint256[], blockNum uint256) returns() +func (_PlasmaMVP *PlasmaMVPTransactorSession) SubmitBlock(headers [][32]byte, txnsPerBlock []*big.Int, feePerBlock []*big.Int, blockNum *big.Int) (*types.Transaction, error) { + return _PlasmaMVP.Contract.SubmitBlock(&_PlasmaMVP.TransactOpts, headers, txnsPerBlock, feePerBlock, blockNum) +} + +// Withdraw is a paid mutator transaction binding the contract method 0x3ccfd60b. +// +// Solidity: function withdraw() returns(uint256) +func (_PlasmaMVP *PlasmaMVPTransactor) Withdraw(opts *bind.TransactOpts) (*types.Transaction, error) { + return _PlasmaMVP.contract.Transact(opts, "withdraw") +} + +// Withdraw is a paid mutator transaction binding the contract method 0x3ccfd60b. +// +// Solidity: function withdraw() returns(uint256) +func (_PlasmaMVP *PlasmaMVPSession) Withdraw() (*types.Transaction, error) { + return _PlasmaMVP.Contract.Withdraw(&_PlasmaMVP.TransactOpts) +} + +// Withdraw is a paid mutator transaction binding the contract method 0x3ccfd60b. +// +// Solidity: function withdraw() returns(uint256) +func (_PlasmaMVP *PlasmaMVPTransactorSession) Withdraw() (*types.Transaction, error) { + return _PlasmaMVP.Contract.Withdraw(&_PlasmaMVP.TransactOpts) +} + +// PlasmaMVPAddedToBalancesIterator is returned from FilterAddedToBalances and is used to iterate over the raw logs and unpacked data for AddedToBalances events raised by the PlasmaMVP contract. +type PlasmaMVPAddedToBalancesIterator struct { + Event *PlasmaMVPAddedToBalances // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *PlasmaMVPAddedToBalancesIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(PlasmaMVPAddedToBalances) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(PlasmaMVPAddedToBalances) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *PlasmaMVPAddedToBalancesIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *PlasmaMVPAddedToBalancesIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// PlasmaMVPAddedToBalances represents a AddedToBalances event raised by the PlasmaMVP contract. +type PlasmaMVPAddedToBalances struct { + Owner common.Address + Amount *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterAddedToBalances is a free log retrieval operation binding the contract event 0xf8552a24c7d58fd05114f6fc9db7b3a354db64d5fc758184af1696ccd8f158f3. +// +// Solidity: e AddedToBalances(owner address, amount uint256) +func (_PlasmaMVP *PlasmaMVPFilterer) FilterAddedToBalances(opts *bind.FilterOpts) (*PlasmaMVPAddedToBalancesIterator, error) { + + logs, sub, err := _PlasmaMVP.contract.FilterLogs(opts, "AddedToBalances") + if err != nil { + return nil, err + } + return &PlasmaMVPAddedToBalancesIterator{contract: _PlasmaMVP.contract, event: "AddedToBalances", logs: logs, sub: sub}, nil +} + +// WatchAddedToBalances is a free log subscription operation binding the contract event 0xf8552a24c7d58fd05114f6fc9db7b3a354db64d5fc758184af1696ccd8f158f3. +// +// Solidity: e AddedToBalances(owner address, amount uint256) +func (_PlasmaMVP *PlasmaMVPFilterer) WatchAddedToBalances(opts *bind.WatchOpts, sink chan<- *PlasmaMVPAddedToBalances) (event.Subscription, error) { + + logs, sub, err := _PlasmaMVP.contract.WatchLogs(opts, "AddedToBalances") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(PlasmaMVPAddedToBalances) + if err := _PlasmaMVP.contract.UnpackLog(event, "AddedToBalances", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// PlasmaMVPBlockSubmittedIterator is returned from FilterBlockSubmitted and is used to iterate over the raw logs and unpacked data for BlockSubmitted events raised by the PlasmaMVP contract. +type PlasmaMVPBlockSubmittedIterator struct { + Event *PlasmaMVPBlockSubmitted // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *PlasmaMVPBlockSubmittedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(PlasmaMVPBlockSubmitted) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(PlasmaMVPBlockSubmitted) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *PlasmaMVPBlockSubmittedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *PlasmaMVPBlockSubmittedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// PlasmaMVPBlockSubmitted represents a BlockSubmitted event raised by the PlasmaMVP contract. +type PlasmaMVPBlockSubmitted struct { + Header [32]byte + BlockNumber *big.Int + NumTxns *big.Int + FeeAmount *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterBlockSubmitted is a free log retrieval operation binding the contract event 0x044ff3798f9b3ad55d1155cea9a40508c71b4c64335f5dae87e8e11551515a06. +// +// Solidity: e BlockSubmitted(header bytes32, blockNumber uint256, numTxns uint256, feeAmount uint256) +func (_PlasmaMVP *PlasmaMVPFilterer) FilterBlockSubmitted(opts *bind.FilterOpts) (*PlasmaMVPBlockSubmittedIterator, error) { + + logs, sub, err := _PlasmaMVP.contract.FilterLogs(opts, "BlockSubmitted") + if err != nil { + return nil, err + } + return &PlasmaMVPBlockSubmittedIterator{contract: _PlasmaMVP.contract, event: "BlockSubmitted", logs: logs, sub: sub}, nil +} + +// WatchBlockSubmitted is a free log subscription operation binding the contract event 0x044ff3798f9b3ad55d1155cea9a40508c71b4c64335f5dae87e8e11551515a06. +// +// Solidity: e BlockSubmitted(header bytes32, blockNumber uint256, numTxns uint256, feeAmount uint256) +func (_PlasmaMVP *PlasmaMVPFilterer) WatchBlockSubmitted(opts *bind.WatchOpts, sink chan<- *PlasmaMVPBlockSubmitted) (event.Subscription, error) { + + logs, sub, err := _PlasmaMVP.contract.WatchLogs(opts, "BlockSubmitted") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(PlasmaMVPBlockSubmitted) + if err := _PlasmaMVP.contract.UnpackLog(event, "BlockSubmitted", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// PlasmaMVPChallengedExitIterator is returned from FilterChallengedExit and is used to iterate over the raw logs and unpacked data for ChallengedExit events raised by the PlasmaMVP contract. +type PlasmaMVPChallengedExitIterator struct { + Event *PlasmaMVPChallengedExit // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *PlasmaMVPChallengedExitIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(PlasmaMVPChallengedExit) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(PlasmaMVPChallengedExit) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *PlasmaMVPChallengedExitIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *PlasmaMVPChallengedExitIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// PlasmaMVPChallengedExit represents a ChallengedExit event raised by the PlasmaMVP contract. +type PlasmaMVPChallengedExit struct { + Position [4]*big.Int + Owner common.Address + Amount *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterChallengedExit is a free log retrieval operation binding the contract event 0xe1289dafb1083e540206bcd7d95a9705ba2590d6a9229c35a1c4c4c5efbda901. +// +// Solidity: e ChallengedExit(position uint256[4], owner address, amount uint256) +func (_PlasmaMVP *PlasmaMVPFilterer) FilterChallengedExit(opts *bind.FilterOpts) (*PlasmaMVPChallengedExitIterator, error) { + + logs, sub, err := _PlasmaMVP.contract.FilterLogs(opts, "ChallengedExit") + if err != nil { + return nil, err + } + return &PlasmaMVPChallengedExitIterator{contract: _PlasmaMVP.contract, event: "ChallengedExit", logs: logs, sub: sub}, nil +} + +// WatchChallengedExit is a free log subscription operation binding the contract event 0xe1289dafb1083e540206bcd7d95a9705ba2590d6a9229c35a1c4c4c5efbda901. +// +// Solidity: e ChallengedExit(position uint256[4], owner address, amount uint256) +func (_PlasmaMVP *PlasmaMVPFilterer) WatchChallengedExit(opts *bind.WatchOpts, sink chan<- *PlasmaMVPChallengedExit) (event.Subscription, error) { + + logs, sub, err := _PlasmaMVP.contract.WatchLogs(opts, "ChallengedExit") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(PlasmaMVPChallengedExit) + if err := _PlasmaMVP.contract.UnpackLog(event, "ChallengedExit", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// PlasmaMVPChangedOperatorIterator is returned from FilterChangedOperator and is used to iterate over the raw logs and unpacked data for ChangedOperator events raised by the PlasmaMVP contract. +type PlasmaMVPChangedOperatorIterator struct { + Event *PlasmaMVPChangedOperator // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *PlasmaMVPChangedOperatorIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(PlasmaMVPChangedOperator) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(PlasmaMVPChangedOperator) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *PlasmaMVPChangedOperatorIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *PlasmaMVPChangedOperatorIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// PlasmaMVPChangedOperator represents a ChangedOperator event raised by the PlasmaMVP contract. +type PlasmaMVPChangedOperator struct { + OldOperator common.Address + NewOperator common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterChangedOperator is a free log retrieval operation binding the contract event 0x3aff32e6289f2f2a2463481071051456b2768bb391c64ae3c91f9033e208cda1. +// +// Solidity: e ChangedOperator(oldOperator address, newOperator address) +func (_PlasmaMVP *PlasmaMVPFilterer) FilterChangedOperator(opts *bind.FilterOpts) (*PlasmaMVPChangedOperatorIterator, error) { + + logs, sub, err := _PlasmaMVP.contract.FilterLogs(opts, "ChangedOperator") + if err != nil { + return nil, err + } + return &PlasmaMVPChangedOperatorIterator{contract: _PlasmaMVP.contract, event: "ChangedOperator", logs: logs, sub: sub}, nil +} + +// WatchChangedOperator is a free log subscription operation binding the contract event 0x3aff32e6289f2f2a2463481071051456b2768bb391c64ae3c91f9033e208cda1. +// +// Solidity: e ChangedOperator(oldOperator address, newOperator address) +func (_PlasmaMVP *PlasmaMVPFilterer) WatchChangedOperator(opts *bind.WatchOpts, sink chan<- *PlasmaMVPChangedOperator) (event.Subscription, error) { + + logs, sub, err := _PlasmaMVP.contract.WatchLogs(opts, "ChangedOperator") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(PlasmaMVPChangedOperator) + if err := _PlasmaMVP.contract.UnpackLog(event, "ChangedOperator", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// PlasmaMVPDepositIterator is returned from FilterDeposit and is used to iterate over the raw logs and unpacked data for Deposit events raised by the PlasmaMVP contract. +type PlasmaMVPDepositIterator struct { + Event *PlasmaMVPDeposit // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *PlasmaMVPDepositIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(PlasmaMVPDeposit) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(PlasmaMVPDeposit) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *PlasmaMVPDepositIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *PlasmaMVPDepositIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// PlasmaMVPDeposit represents a Deposit event raised by the PlasmaMVP contract. +type PlasmaMVPDeposit struct { + Depositor common.Address + Amount *big.Int + DepositNonce *big.Int + EthBlockNum *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterDeposit is a free log retrieval operation binding the contract event 0x36af321ec8d3c75236829c5317affd40ddb308863a1236d2d277a4025cccee1e. +// +// Solidity: e Deposit(depositor address, amount uint256, depositNonce uint256, ethBlockNum uint256) +func (_PlasmaMVP *PlasmaMVPFilterer) FilterDeposit(opts *bind.FilterOpts) (*PlasmaMVPDepositIterator, error) { + + logs, sub, err := _PlasmaMVP.contract.FilterLogs(opts, "Deposit") + if err != nil { + return nil, err + } + return &PlasmaMVPDepositIterator{contract: _PlasmaMVP.contract, event: "Deposit", logs: logs, sub: sub}, nil +} + +// WatchDeposit is a free log subscription operation binding the contract event 0x36af321ec8d3c75236829c5317affd40ddb308863a1236d2d277a4025cccee1e. +// +// Solidity: e Deposit(depositor address, amount uint256, depositNonce uint256, ethBlockNum uint256) +func (_PlasmaMVP *PlasmaMVPFilterer) WatchDeposit(opts *bind.WatchOpts, sink chan<- *PlasmaMVPDeposit) (event.Subscription, error) { + + logs, sub, err := _PlasmaMVP.contract.WatchLogs(opts, "Deposit") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(PlasmaMVPDeposit) + if err := _PlasmaMVP.contract.UnpackLog(event, "Deposit", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// PlasmaMVPFinalizedExitIterator is returned from FilterFinalizedExit and is used to iterate over the raw logs and unpacked data for FinalizedExit events raised by the PlasmaMVP contract. +type PlasmaMVPFinalizedExitIterator struct { + Event *PlasmaMVPFinalizedExit // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *PlasmaMVPFinalizedExitIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(PlasmaMVPFinalizedExit) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(PlasmaMVPFinalizedExit) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *PlasmaMVPFinalizedExitIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *PlasmaMVPFinalizedExitIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// PlasmaMVPFinalizedExit represents a FinalizedExit event raised by the PlasmaMVP contract. +type PlasmaMVPFinalizedExit struct { + Position [4]*big.Int + Owner common.Address + Amount *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterFinalizedExit is a free log retrieval operation binding the contract event 0xb5083a27a38f8a9aa999efb3306b7be96dc3f42010a968dd86627880ba7fdbe2. +// +// Solidity: e FinalizedExit(position uint256[4], owner address, amount uint256) +func (_PlasmaMVP *PlasmaMVPFilterer) FilterFinalizedExit(opts *bind.FilterOpts) (*PlasmaMVPFinalizedExitIterator, error) { + + logs, sub, err := _PlasmaMVP.contract.FilterLogs(opts, "FinalizedExit") + if err != nil { + return nil, err + } + return &PlasmaMVPFinalizedExitIterator{contract: _PlasmaMVP.contract, event: "FinalizedExit", logs: logs, sub: sub}, nil +} + +// WatchFinalizedExit is a free log subscription operation binding the contract event 0xb5083a27a38f8a9aa999efb3306b7be96dc3f42010a968dd86627880ba7fdbe2. +// +// Solidity: e FinalizedExit(position uint256[4], owner address, amount uint256) +func (_PlasmaMVP *PlasmaMVPFilterer) WatchFinalizedExit(opts *bind.WatchOpts, sink chan<- *PlasmaMVPFinalizedExit) (event.Subscription, error) { + + logs, sub, err := _PlasmaMVP.contract.WatchLogs(opts, "FinalizedExit") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(PlasmaMVPFinalizedExit) + if err := _PlasmaMVP.contract.UnpackLog(event, "FinalizedExit", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// PlasmaMVPStartedDepositExitIterator is returned from FilterStartedDepositExit and is used to iterate over the raw logs and unpacked data for StartedDepositExit events raised by the PlasmaMVP contract. +type PlasmaMVPStartedDepositExitIterator struct { + Event *PlasmaMVPStartedDepositExit // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *PlasmaMVPStartedDepositExitIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(PlasmaMVPStartedDepositExit) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(PlasmaMVPStartedDepositExit) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *PlasmaMVPStartedDepositExitIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *PlasmaMVPStartedDepositExitIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// PlasmaMVPStartedDepositExit represents a StartedDepositExit event raised by the PlasmaMVP contract. +type PlasmaMVPStartedDepositExit struct { + Nonce *big.Int + Owner common.Address + Amount *big.Int + CommittedFee *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterStartedDepositExit is a free log retrieval operation binding the contract event 0xe90dc7204eb622c87d2c8d67d8e27afdfd34042584591e7b3d35014873cf9cfd. +// +// Solidity: e StartedDepositExit(nonce uint256, owner address, amount uint256, committedFee uint256) +func (_PlasmaMVP *PlasmaMVPFilterer) FilterStartedDepositExit(opts *bind.FilterOpts) (*PlasmaMVPStartedDepositExitIterator, error) { + + logs, sub, err := _PlasmaMVP.contract.FilterLogs(opts, "StartedDepositExit") + if err != nil { + return nil, err + } + return &PlasmaMVPStartedDepositExitIterator{contract: _PlasmaMVP.contract, event: "StartedDepositExit", logs: logs, sub: sub}, nil +} + +// WatchStartedDepositExit is a free log subscription operation binding the contract event 0xe90dc7204eb622c87d2c8d67d8e27afdfd34042584591e7b3d35014873cf9cfd. +// +// Solidity: e StartedDepositExit(nonce uint256, owner address, amount uint256, committedFee uint256) +func (_PlasmaMVP *PlasmaMVPFilterer) WatchStartedDepositExit(opts *bind.WatchOpts, sink chan<- *PlasmaMVPStartedDepositExit) (event.Subscription, error) { + + logs, sub, err := _PlasmaMVP.contract.WatchLogs(opts, "StartedDepositExit") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(PlasmaMVPStartedDepositExit) + if err := _PlasmaMVP.contract.UnpackLog(event, "StartedDepositExit", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// PlasmaMVPStartedTransactionExitIterator is returned from FilterStartedTransactionExit and is used to iterate over the raw logs and unpacked data for StartedTransactionExit events raised by the PlasmaMVP contract. +type PlasmaMVPStartedTransactionExitIterator struct { + Event *PlasmaMVPStartedTransactionExit // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *PlasmaMVPStartedTransactionExitIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(PlasmaMVPStartedTransactionExit) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(PlasmaMVPStartedTransactionExit) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *PlasmaMVPStartedTransactionExitIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *PlasmaMVPStartedTransactionExitIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// PlasmaMVPStartedTransactionExit represents a StartedTransactionExit event raised by the PlasmaMVP contract. +type PlasmaMVPStartedTransactionExit struct { + Position [3]*big.Int + Owner common.Address + Amount *big.Int + ConfirmSignatures []byte + CommittedFee *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterStartedTransactionExit is a free log retrieval operation binding the contract event 0x20d695720ae96d3511520c6f51d6ab23aa19a3796da77024ad027b344bb72530. +// +// Solidity: e StartedTransactionExit(position uint256[3], owner address, amount uint256, confirmSignatures bytes, committedFee uint256) +func (_PlasmaMVP *PlasmaMVPFilterer) FilterStartedTransactionExit(opts *bind.FilterOpts) (*PlasmaMVPStartedTransactionExitIterator, error) { + + logs, sub, err := _PlasmaMVP.contract.FilterLogs(opts, "StartedTransactionExit") + if err != nil { + return nil, err + } + return &PlasmaMVPStartedTransactionExitIterator{contract: _PlasmaMVP.contract, event: "StartedTransactionExit", logs: logs, sub: sub}, nil +} + +// WatchStartedTransactionExit is a free log subscription operation binding the contract event 0x20d695720ae96d3511520c6f51d6ab23aa19a3796da77024ad027b344bb72530. +// +// Solidity: e StartedTransactionExit(position uint256[3], owner address, amount uint256, confirmSignatures bytes, committedFee uint256) +func (_PlasmaMVP *PlasmaMVPFilterer) WatchStartedTransactionExit(opts *bind.WatchOpts, sink chan<- *PlasmaMVPStartedTransactionExit) (event.Subscription, error) { + + logs, sub, err := _PlasmaMVP.contract.WatchLogs(opts, "StartedTransactionExit") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(PlasmaMVPStartedTransactionExit) + if err := _PlasmaMVP.contract.UnpackLog(event, "StartedTransactionExit", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} diff --git a/docs/architecure/store.md b/docs/architecure/store.md new file mode 100644 index 0000000..053b523 --- /dev/null +++ b/docs/architecure/store.md @@ -0,0 +1,25 @@ +# Store Design # + +The store package provides the backend storage for all information necessary for the sidechain to function. +There are 2 stores, the block store and output store. + +## Block Store ## +The block store maintains all necessary information related to each plasma block produced. +The Block type within the block store wraps the tendermint block it was committed at with a plasma block. +The Block store keeps a counter for the current and next plasma block number to be used. + +## Output Store ## +All deposits, fees, and regular outputs can be stored and queried from the output store. +There are several mappings in the output store. +There exists the following mappings: +- transaction hash to transaction +- position to transaction hash +- deposit nonce to deposit +- fee position to total fees collected in block +- address to wallet + +## Wallet ## +Wallets are a convenience struct to maintain track of address balances, unspent outputs and spent outputs. + + + diff --git a/docs/eth.md b/docs/eth.md new file mode 100644 index 0000000..c45a7b8 --- /dev/null +++ b/docs/eth.md @@ -0,0 +1,314 @@ +The eth subcommand acts an interface enabling interaction with the rootchain contract. +It requires a connection to a full eth node which can be specified in `~/.plasmacli/plasma.toml` +See an example [plasma.toml](https://github.com/FourthState/plasma-mvp-sidechain/blob/develop/docs/testnet-setup/example_plasmacli_plasma.toml) for an example setup. + +You must have eth in your account to use the commands deposit, exit, challenge, withdraw, and finalize. + +## Depositing ## +Example usage: + +``` +plasmacli eth deposit 1000 acc1 +Enter passphrase: +Successfully sent deposit transaction +Transaction Hash: 0x04d2a92c52e4417382c8a1a59ada3aa3a8619bea3ea61d70870ecb4e7bbded30 +``` + +You can use etherscan to check the status of your transaction. + +You can also use the query command to check to see if your deposit occured on the rootchain + +``` +plasmacli eth query deposit --all +Owner: 0xec36ead9c897b609a4ffa5820e1b2b137d454343 +Amount: 1000 +Nonce: 1 +Rootchain Block: 4071013 +``` + +If you know what your deposit nonce should be you can also query in the following manner: + +``` +plasmacli eth query deposit 1 +Owner: 0xec36ead9c897b609a4ffa5820e1b2b137d454343 +Amount: 1000 +Nonce: 1 +Rootchain Block: 4071013 +``` + +## Checking Submitted headers ## + +``` +plasmacli eth query block 1 +Block: 1 +Header: 0x5a4f97a64e82a4aa090b0946ed299c2d75f4c8353be0a4b97df8607120713183 +Txs: 1 +Fee: 0 +Created: 2019-03-21 19:03:55 +0000 UTC +``` + +## Exiting ## + +UTXO's and unspent deposits can be exitted. +When exiting, the user can use the "trust-node" flag if they trust the full node specified by the "node" flag. +When "trust-node" flag is used, information necessary for exiting will be retireved from the connected full node. +Exiting a deposit, only requires its position and committed fee so no flags are necessary. +A proof is not required for transactions included in a block of size 1. + +Exiting an unspent deposit: + +``` +plasmacli eth exit acc1 "(0.0.0.3)" +Enter passphrase: +Sent deposit exit transaction +Transaction Hash: 0xa16f3909e1dd749f5093199e7b597974f5f643d9b833b3a9546ff674dac0af28 +``` + +Exiting a fee can be done in the same format as exiting a deposit. +Specifiying the position and committed fee is the only information required to do a successful fee withdrawal. + + +Exiting a utxo with trust-node: +``` +plasmacli eth exit acc1 "(10.0.0.0)" -t +Enter passphrase: +Warning: No proof was found or provided. If the exiting transaction was not the only transaction included in the block then this transaction will fail. +Sent exit transaction +Transaction Hash: 0x4df9c79d17036b7468b69f83a313a1fb87ccc987e9e28be1e78f81d2d19afab8 +``` + +Exiting a utxo without trust-node: +``` +plasmacli prove acc1 "(22.0.1.0)" +Roothash: 0xC6BA74C556C3114598214AC828766DC485E688F217B1506C33F2095045B0300E +Total: 1 +LeafHash: 0xc6ba74c556c3114598214ac828766dc485e688f217b1506c33f2095045b0300e +TxBytes: 0xf90328f9029da0000000000000000000000000000000000000000000000000000000000000000ba00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000b882b0452100c6e01ab4e04e44bc5fd767dbcaa8abf930585be257d9e33dfab9b8230d39e48c3a350d3727237efe9c70d24e2e769516b930b56eea5188bec35065a7010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005b88200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000945475b99e01ac3bb08b24fd754e2868dbb829bc3aa0000000000000000000000000000000000000000000000000000000000000232894ec36ead9c897b609a4ffa5820e1b2b137d454343a000000000000000000000000000000000000000000000000000000000000007d0a00000000000000000000000000000000000000000000000000000000000000000f886b8415a1ba592dc188288fedd7dfd86cfd953e9993a0e50948fa230c4a6b33b71ecb87a26c15322034c3db1f58be930a25ef1565461cee75bda4103624f9bd30f6a1f00b8415a1ba592dc188288fedd7dfd86cfd953e9993a0e50948fa230c4a6b33b71ecb87a26c15322034c3db1f58be930a25ef1565461cee75bda4103624f9bd30f6a1f00 + +plasmacli eth exit acc1 "(22.0.1.0)" -b 0xf90328f9029da0000000000000000000000000000000000000000000000000000000000000000ba00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000b882b0452100c6e01ab4e04e44bc5fd767dbcaa8abf930585be257d9e33dfab9b8230d39e48c3a350d3727237efe9c70d24e2e769516b930b56eea5188bec35065a7010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005b88200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000945475b99e01ac3bb08b24fd754e2868dbb829bc3aa0000000000000000000000000000000000000000000000000000000000000232894ec36ead9c897b609a4ffa5820e1b2b137d454343a000000000000000000000000000000000000000000000000000000000000007d0a00000000000000000000000000000000000000000000000000000000000000000f886b8415a1ba592dc188288fedd7dfd86cfd953e9993a0e50948fa230c4a6b33b71ecb87a26c15322034c3db1f58be930a25ef1565461cee75bda4103624f9bd30f6a1f00b8415a1ba592dc188288fedd7dfd86cfd953e9993a0e50948fa230c4a6b33b71ecb87a26c15322034c3db1f58be930a25ef1565461cee75bda4103624f9bd30f6a1f00 +Sent exit transaction +Transaction Hash: 0xeea2e9e6ff8f93ba189d938cf531052c55f724cce87c38895d3eeec20299e615 +``` + +Querying for deposit exits: + +``` +plasmacli eth query exit --deposits --all +Owner: 0xec36ead9c897b609a4ffa5820e1b2b137d454343 +Amount: 1000 +State: Pending +Committed Fee: 0 +Created: 2019-03-21 22:05:40 +0000 UTC + +Exit will be finalized in about: 167.40055942302556 hours + +Owner: 0x5475b99e01ac3bb08b24fd754e2868dbb829bc3a +Amount: 10000 +State: Challenged +Committed Fee: 0 +Created: 2019-03-21 22:21:40 +0000 UTC +``` + +Querying for transaction exits: + +``` +plasmacli eth query exit --all +Owner: 0xec36ead9c897b609a4ffa5820e1b2b137d454343 +Amount: 10000 +State: Pending +Committed Fee: 0 +Created: 2019-03-21 22:40:10 +0000 UTC + +Exit will be finalized in about: 167.96671164467028 hours +``` + +Querying for a specific exit: + +``` +plasmacli eth query exit --position "(10.0.0.0)" +Owner: 0xec36ead9c897b609a4ffa5820e1b2b137d454343 +Amount: 10000 +State: Pending +Committed Fee: 0 +Created: 2019-03-21 22:40:10 +0000 UTC + +Exit will be finalized in about: 167.9601120748311 hours +``` + +## Challenging ## + +A pending exit may be challenged if the exit committed to an incorrect fee amount or if the utxo was spent on the sidechain in a finalized transaction. +Every exit commits to the fee of an unfinalized spend of that deposit/utxo. +If the deposit/utxo was never spent, the committed fee is 0. +If the deposit/utxo was involved in an unfinalized spend which included a non zero fee, then the exit must commit to that non zero fee or risk being challenged. +A deposit/utxo may also be challenged with a finalized spend, if it exists. + +Challenging a deposit with an incorrect committed fee: + + +``` +plasmacli spend 0x5475b99e01ac3bb08b24fd754e2868dbb829bc3a 9000 acc1 --position "(0.0.0.6)" --fee 1000 + +plasmacli eth exit acc1 "(0.0.0.6)" --fee 0 +Enter passphrase: +Sent deposit exit transaction +Transaction Hash: 0x6b23f7fc7cfdd5b4948e78740f38220133392e77b99b535984faafcc380567a8 + +plasmacli eth query exit --position "(0.0.0.6)" +Owner: 0xec36ead9c897b609a4ffa5820e1b2b137d454343 +Amount: 10000 +State: Pending +Committed Fee: 0 +Created: 2019-03-26 20:17:36 +0000 UTC + +Exit will be finalized in about: 167.99566772949944 hours + +plasmacli eth challenge "(0.0.0.6)" "(25.0.0.0)" --tx-bytes 0xf90328f9029da00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000006b88200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000b88200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000945475b99e01ac3bb08b24fd754e2868dbb829bc3aa00000000000000000000000000000000000000000000000000000000000002328940000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000003e8f886b8412efef4db94eaf2186e9cd51aacba30876596a2d467ed954c06e348925bc60611028ec63150039305d32ebe8081511f1b8542935758c841be5aa424d4d7f7f61200b8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +Enter passphrase: +Sent challenge transaction +Transaction Hash: 0xbf7ea33c09a187eeb6bb9172e0459c9a137058b93a56ea869d206e615648cc5f + +plasmacli eth query exit --position "(0.0.0.6)" +Owner: 0xec36ead9c897b609a4ffa5820e1b2b137d454343 +Amount: 10000 +State: Nonexistent +Committed Fee: 0 +Created: 2019-03-26 20:17:36 +0000 UTC +``` + +Challenging a deposit with a finalized spend: + +Deposit '(0.0.0.4)' was exitted and spent on the sidechain. + +``` +plasmacli eth challenge "(0.0.0.4)" "(10.0.0.0)" acc1 -t --signatures 0x19984e40ce233d31db3a5bbf724f079e306644046c0d30bdbd93027bd3e3c04f314c41f14a8b3c771e1faad6c41a07ed84d418123a8c2a0e82237eb8a22ca62501 +Enter passphrase: +Warning: No proof was found or provided. If the exiting transaction was not the only transaction included in the block then this transaction will fail. +Sent challenge transaction +Transaction Hash: 0x6347965d4cb1af2160ff56f53cce18fdd50e0be8bbbc7f03ff475139f8422d8b +``` + +**Note:** + +If your local signature storage contains the confirmation signature, then the "signature" flag is unnecessary. +If "trust-node" is not used, "proof" and "tx-bytes" flags are required. + +Challenging a transaction exit with an incorrect committed fee: + +The following exiting utxo was involved in a spend that committed a fee of 1000, but exitted with a committed fee of 0. + +``` +plasmacli eth challenge "(22.0.0.0)" "(24.0.0.0)" acc1 -t +Enter passphrase: +Warning: No proof was found or provided. If the exiting transaction was not the only transaction included in the block then this transaction will fail. +Sent challenge transaction +Transaction Hash: 0xdcf512c38b12670e36914fc7f2129e246e0ba61aa0abd1c284077e364c36ae1b + +plasmacli eth query exit --position "(22.0.0.0)" +Owner: 0x5475b99e01ac3bb08b24fd754e2868dbb829bc3a +Amount: 9000 +State: Nonexistent +Committed Fee: 0 +Created: 2019-03-22 20:09:55 +0000 UTC +``` + +Since the exiting utxo can still be exitted with the correct fee amount, its state is set to Nonexistent. + +Challenging a transaction exit with a finalized spend: + +Now the utxo exits with the correct fee amount, but the spend in block 24 is finalized so we can challenge it again. + +``` +plasmacli eth exit acc2 "(22.0.0.0)" --fee 1000 +Enter passphrase: +Warning: No proof was found or provided. If the exiting transaction was not the only transaction included in the block then this transaction will fail. +Sent exit transaction +Transaction Hash: 0xef7a0798bcafc4b575d4005e0dedacc69c4bfe3c37e13fbbb71397e32c99bc05 + +plasmacli sign acc2 + +UTXO +Position: (24.0.0.0) +Owner: 0xec36ead9c897b609a4ffa5820e1b2b137d454343 +Value: 15000 +> Would you like to finalize this transaction? [Y/n] +Y +Enter passphrase: +Confirmation Signature for output with position: (24.0.0.0) +0x151367f8b7ab02d4a616e175f3a6d5955306933fe54dce6d5247f45a543db071798fbd61acb254cfb02cc53d72c19d7ce0903c5fe3bd0570f98a11ed26dc83e701 + +UTXO +Position: (24.0.0.0) +Owner: 0xec36ead9c897b609a4ffa5820e1b2b137d454343 +Value: 15000 +> Would you like to finalize this transaction? [Y/n] +Y +Enter passphrase: +Confirmation Signature for output with position: (24.0.0.0) +0x151367f8b7ab02d4a616e175f3a6d5955306933fe54dce6d5247f45a543db071798fbd61acb254cfb02cc53d72c19d7ce0903c5fe3bd0570f98a11ed26dc83e701 + +plasmacli eth challenge "(22.0.0.0)" "(24.0.0.0)" acc1 +Enter passphrase: +Warning: No proof was found or provided. If the exiting transaction was not the only transaction included in the block then this transaction will fail. +Sent challenge transaction +Transaction Hash: 0x5a4633533895bcd65514975b755509d452cc283bf5f299cbe1224999a66b00e7 + + +``` + +## Finalize ## + +Exits may be finalized, after the challenge period has ended. +The default challenge period is 5 days for deposits and 7 days for transaction exits + +Balance before finalizing: +``` +plasmacli eth query balance acc1 +Rootchain Balance: 200000 +``` + +Finalize exits: +``` +plasmacli eth finalize acc1 +Enter passphrase: +Successfully sent finalize exits transaction +Transaction Hash: 0x95d028a4ae3e90833ab222880b366e32818353195acbb7e9c7516972d224c1f6 +``` + +Balance after finalizing: +``` +plasmacli eth query balance acc1 +Rootchain Balance: 618000 +``` + +## Withdraw ## + +Checking the balance on the rootchain that can be withdrawn: + +``` +plasmacli eth query balance acc1 +Rootchain Balance: 200000 +``` + +Withdrawing an entire balance from the rootchain: + +``` +plasmacli eth withdraw acc1 +Enter passphrase: +Successfully sent withdraw transaction +Transaction Hash: 0xd790a512f15051ee866bf8751b9913de3d2fedae0e2228b5c0ff97ed6451f2e7 +``` + +## Query Rootchain ## + +Query rootchain specific information: + +``` +plasmacli eth query rootchain +Last Committed Block: 0 +Contract Balance: 1000 +Withdraw Balance: 0 +Minimum Exit Bond: 200000 +Operator: 0xec36ead9c897b609a4ffa5820e1b2b137d454343 +``` + diff --git a/docs/example.md b/docs/example.md new file mode 100644 index 0000000..e4962a0 --- /dev/null +++ b/docs/example.md @@ -0,0 +1,184 @@ +# Using the Sidechain Example # + +The following assumes you have already deployed the rootchain contract to either ganache or a testnet. +See our rootchain deployment [example](https://github.com/FourthState/plasma-mvp-sidechain/blob/develop/docs/example_rootchain_deployment.md) + +Plasmacli: the command-line interface for interacting with the sidechain and rootchain. + +Plasmad: runs a sidechain full-node + +## Setting up a full-node ## + +Install the latest version of plasmad: + +``` +cd server/plasmad/ +go install +``` + +Run `plasmad init` to initalize a validator. cd into `~/.plasmad/config`. +Open genesis.json and add an ethereum address to `fee_address`. +See our example [genesis.json](https://github.com/FourthState/plasma-mvp-sidechain/blob/develop/docs/testnet-setup/example_genesis.json) + +Open config.toml and add any configurations you would like to add for your validator, such as a moniker. TODO: add section on seeds + +Open plasma.toml, set `is_operator` to true if you are running a validator. +Set `ethereum_operator_privatekey` to be the unencrypted private key that will be used to submit blocks to the rootchain. +It must contain sufficient eth to pay gas costs for every submitted plasma block. +Set `ethereum_plasma_contract_address` to be the contract address of the deployed rootchain. +Set `plasma_block_commitment_rate` to be the rate at which you want plasma blocks to be submitted to the rootchain. +Set `ethereum_nodeurl` to be the url which contains your ethereum full node. +Set `ethereum_finality` to be the number of ethereum blocks until a submitted header is presumed to be final. + +See our example [plasma.toml](https://github.com/FourthState/plasma-mvp-sidechain/blob/develop/docs/testnet-setup/example_plasma.toml) + +Run `plasmad unsafe-reset-all` followed by `plasmad start` + +You should be successfully producing empty blocks + +Things to keep in mind: +- You can change `timeout_commit` in config.toml to slow down block time. +- go install `plasmacli` and `plasmad` when updating to newer versions +- Using `plasmad unsafe-reset-all` will erase all chain history. You will need to redeploy the rootchain contract. + +## Setting up the client ## + +You will need to run a full eth node to interact with the rootchain contract. +See the install [script](https://github.com/FourthState/plasma-mvp-sidechain/blob/develop/scripts/plasma_install.sh) for an example of setting up a full eth node. + +Install the latest version of plasmacli: + +``` +cd client/plasmacli/ +go install +``` + +cd into `~/.plasmacli/`. Open plasma.toml. +Set `ethereum_plasma_contract_address` to be the contract address of the deployed rootchain. +Set `ethereum_nodeurl` to be the url which contains your ethereum full node. +Set `ethereum_finality` to be the number of ethereum blocks until a submitted header is presumed to be final. + +Things to keep in mind: +- plasmacli can be used without a full node, but certain features will be disabled such as interacting with the rootchain +- Using the `-h` will provide short documentation and example usage for each command + +See [keys documentation](https://github.com/FourthState/plasma-mvp-sidechain/blob/develop/docs/keys.md) for examples on how to use the keys subcommand. + +See [eth documentation](https://github.com/FourthState/plasma-mvp-sidechain/blob/develop/docs/eth.md) for examples on how to use the eth subcommand. + +## Spending Deposits ## + +In order to spend a deposit on the sidechain, first a user must deposit on the rootchain and then send an include-deposit transaction (after presumed finality). +A user can deposit using the eth subcommand. See this [example](https://github.com/FourthState/plasma-mvp-sidechain/blob/develop/docs/eth.md#depositing) + +Sending an include-deposit transaction: +``` +plasmacli include-deposit 1 acc1 +``` + +You can also use the --sync flag +``` +plasmacli include-deposit 1 acc1 --sync +Error: broadcast_tx_commit: Response error: RPC error -32603 - Internal error: Error on broadcastTxCommit: Tx already exists in cache +``` + +The above error simply means that the above transaction has been sent but not yet included in a block. + +If you query your account balance you should see your deposit: +``` +plasmacli query balance acc1 +Position: (0.0.0.1) , Amount: 1000 +Total: 1000 +``` + +**Note:** The include-deposit transaction will fail if presumed finality specified by the validator has not yet been reached. + +Spending the deposit: +First argument is address being sent to, followed by amounts to send (first output, second output), followed by the account to send from. +Position flag of inputs to be spent must be specified. + +``` + plasmacli spend 0x5475b99e01ac3bb08b24fd754e2868dbb829bc3a 1000,0 acc1 --position "(0.0.0.1)" +``` + +The above address being sent to corresponds to acc2 + +``` +plasmacli query balance acc2 +Position: (2.0.0.0) , Amount: 1000 +Total: 1000 +``` + +Deposits and Fees do not need a confirmation signature to be spent. + +## Spending Fees ## + +Fees can be spent in the same manner as deposits + +``` + +``` +## Spending UTXOS ## + +In order to spend a utxo, a user must have the confirmation signatures for it. +Confirmation signatures can be generated using the sign command: + +``` +plasmacli sign acc1 --owner 0x5475b99e01ac3bb08b24fd754e2868dbb829bc3a --position "(4.0.0.0)" + +UTXO +Position: (4.0.0.0) +Owner: 0x5475b99e01ac3bb08b24fd754e2868dbb829bc3a +Value: 1000 +> Would you like to finalize this transaction? [Y/n] +Y +Enter passphrase: +Confirmation Signature for output with position: (4.0.0.0) +0x1ff4bc8fe08e14a480ff4744e802c8ff05a9ca5e17f1d72be7a718dc8869feaa58e07359cae70aa210a77a065d14495c79ca369cfcb23d94af921eaf16ec103701 +``` + +Spending the utxo: + +``` +plasmacli spend 0xec36ead9c897b609a4ffa5820e1b2b137d454343 1000 acc2 --position "(4.0.0.0)" +Enter passphrase: +Committed at block 2891. Hash 0x33434538413842383733393530443230314242324539303539324235343646364544323535333943304236423838353943413335363733454533363245463534 +``` + +If you cannot use the sign command to generate the confirmation signature because another user sent the transaction, use the "Input0ConfirmSigs" flag or "-0" for confirmation signatures for the first input and "Input1ConfirmSigs" flag or "-1" for confirmation signatures for the second input. + +A user can also make spends using generated inputs by not specifying the positions to use. + +``` +plasmacli query balance acc2 +Position: (2.0.0.0) , Amount: 1000 +Position: (22.0.0.0) , Amount: 9000 +Position: (0.0.0.7) , Amount: 10000 + +plasmacli spend 0xec36ead9c897b609a4ffa5820e1b2b137d454343 15000 acc2 --fee 1000 +Enter passphrase: +Enter passphrase: +``` + +The above transaction was committed in plasma block 24. + +The following transaction generated would spend positions (22.0.0.0) and (0.0.0.7), send 15000 to the specified address, use 1000 for a fee and send the remaining 3000 to acc2. + +``` +plasmacli query balance acc2 +Position: (2.0.0.0) , Amount: 1000 +Position: (24.0.1.0) , Amount: 3000 +Total: 4000 +``` + +``` +plasmacli query balance acc1 +Position: (10.0.0.0) , Amount: 10000 +Position: (22.0.1.0) , Amount: 2000 +Position: (24.0.0.0) , Amount: 15000 +Position: (0.0.0.3) , Amount: 1000 +Position: (0.0.0.6) , Amount: 10000 +Position: (24.65535.0.0) , Amount: 1000 +Total: 39000 +``` + diff --git a/docs/example_rootchain_deployment.md b/docs/example_rootchain_deployment.md new file mode 100644 index 0000000..7368151 --- /dev/null +++ b/docs/example_rootchain_deployment.md @@ -0,0 +1,116 @@ +The following example deploys the rootchain contract to the rinkeby testnet. +It requires a full eth node and truffle to be installed. + +## Deployment ## + +Attach your eth node to the geth console and check that it has finished syncing + +``` +geth attach /opt/geth/rinkeby/chaindata/geth.ipc +Welcome to the Geth JavaScript console! + +instance: Geth/v1.8.23-stable-c9427004/linux-amd64/go1.10.4 +coinbase: 0xec36ead9c897b609a4ffa5820e1b2b137d454343 +at block: 4070914 (Thu, 21 Mar 2019 18:30:08 UTC) + datadir: /opt/geth/rinkeby/chaindata + modules: admin:1.0 clique:1.0 debug:1.0 eth:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0 + +> eth.syncing +false +``` + +Import your eth private key by adding its file to the keystore located in `chaindata/keystore` or generate a new key using the geth commands. + +Ensure that your eth private key has more than 0.12 eth to deploy the contract. Use a faucet if you don't have any testnet eth. + +Unlock your account using the geth console: + +``` +> personal.unlockAccount("0xec36ead9c897b609a4ffa5820e1b2b137d454343", "1234567890", 1000000) +true +``` + +Either exit geth console or using another screen navigate to `go/src/github.com/FourthState/plasma-mvp-sidechain/contracts/`. +Open truffle.js and add: +``` +rinkeby: { + host: "localhost", + port: 8545, + network_id: "4", // Rinkeby ID 4 + from: "0xec36ead9c897b609a4ffa5820e1b2b137d454343", // account from which to deploy + gas: 6712390 + } + +``` + +See our [example_truffle.js](https://github.com/FourthState/plasma-mvp-sidechain/blob/develop/docs/testnet-setup/example_truffle.js) + +Run `truffle migrate --network rinkeby` + +Example Output: +``` +truffle migrate --network rinkeby + +Compiling ./contracts/PlasmaMVP.sol... +Writing artifacts to ./build/contracts + +⚠️ Important ⚠️ +If you're using an HDWalletProvider, it must be Web3 1.0 enabled or your migration will hang. + + +Starting migrations... +====================== +> Network name: 'rinkeby' +> Network id: 4 +> Block gas limit: 6996931 + + +1_initial_migration.js +====================== + + Deploying 'Migrations' + ---------------------- + > transaction hash: 0x46d8b7e69f5b00c91c21df4003fb702ab302c712555e0013fd19c3818d0f22ff + > Blocks: 2 Seconds: 16 + > contract address: 0x073951236805bf6332fb3F90CdE294aC2D373172 + > account: 0xEc36eaD9c897b609A4fFa5820E1B2B137D454343 + > balance: 18.586470732988776 + > gas used: 284908 + > gas price: 20 gwei + > value sent: 0 ETH + > total cost: 0.00569816 ETH + + > Saving artifacts + ------------------------------------- + > Total cost: 0.00569816 ETH + + +2_deploy_rootchain.js +===================== + + Replacing 'PlasmaMVP' + --------------------- + > transaction hash: 0xe1448402b7f41fb77048e16649f32ec58faf5355ac69060312c81cd751b4ec63 + > Blocks: 0 Seconds: 12 + > contract address: 0x9c36F39E87f3EA5283e186b385232044dC2f8c30 + > account: 0xEc36eaD9c897b609A4fFa5820E1B2B137D454343 + > balance: 18.473328172988776 + > gas used: 5657128 + > gas price: 20 gwei + > value sent: 0 ETH + > total cost: 0.11314256 ETH + + > Saving artifacts + ------------------------------------- + > Total cost: 0.11314256 ETH + + +Summary +======= +> Total deployments: 2 +> Final cost: 0.11884072 ETH + +``` + +Congratulations! You have just deployed the rootchain contract. +In the above example, the rootchain exists at address `0x9c36F39E87f3EA5283e186b385232044dC2f8c30` diff --git a/docs/keys.md b/docs/keys.md new file mode 100644 index 0000000..07095bf --- /dev/null +++ b/docs/keys.md @@ -0,0 +1,108 @@ +In order to spend utxos on the sidechain, we will need keys corresponding to the addresses that own those utxos. +We can use the keys command to generate and manage these keys. +We use a mapping between a name for our key and its address to simplify usage of these addresses. + +The keystore's default location is `~/.plasmacli/keys/` + +## Generating Keys ## + +For example, the following generates a new key with the associated name "mykey": + +``` +plasmacli keys add mykey +Enter new passphrase for your key: +Repeat passphrase: + +**Important** do not lose your passphrase. +It is the only way to recover your account +You should export this account and store it in a secure location +NAME: mykey ADDRESS: 0x3b00b1deee88ac18a2a53c988ff30ba1f561d3a5 +``` + +#### Import unencrypted private keys #### + +Ganache-cli display + +``` + Ganache CLI v6.1.8 (ganache-core: 2.2.1) + +Available Accounts +================== +(0) 0xea6ed4bb7cba09c391c11a15d5472e806caa3986 (~100 ETH) +(1) 0x6a06b2fd816021568b5b8abad00eab4679ab3450 (~100 ETH) + +Private Keys +================== +(0) 0x46b7f5573b110d21ca809d1e17d97c582e6e402b1d7daa7d09264c13015e73ae +(1) 0x2a21b4bcbc0d812adf41ff098f336a86325e3c7df31d5c576ac7b760ed2d56f0 + +``` + +Import by passing in the unecrypted key via command line: + +``` +plasmacli keys import imported_key 46b7f5573b110d21ca809d1e17d97c582e6e402b1d7daa7d09264c13015e73ae +Enter new passphrase for your key: +Repeat passphrase: +Successfully imported. +NAME: imported_key ADDRESS: 0xea6ed4bb7cba09c391c11a15d5472e806caa3986 +``` + +Import using a file which stores the unencrypted key: + +Put the private key you wish to import in a file + +keyfile: +``` +46b7f5573b110d21ca809d1e17d97c582e6e402b1d7daa7d09264c13015e73ae +``` + +Use plasmacli +``` +plasmacli keys import imported_keyfile --file ~/keyfile +Enter new passphrase for your key: +Repeat passphrase: +Successfully imported. +NAME: imported_keyfile ADDRESS: 0x6a06b2fd816021568b5b8abad00eab4679ab3450 +``` + +#### List Keys #### + +``` +plasmacli keys list +NAME: ADDRESS: +acc1 0xec36ead9c897b609a4ffa5820e1b2b137d454343 +acc2 0x5475b99e01ac3bb08b24fd754e2868dbb829bc3a +imported_key 0xea6ed4bb7cba09c391c11a15d5472e806caa3986 +imported_keyfile 0x6a06b2fd816021568b5b8abad00eab4679ab3450 +mykey 0x3b00b1deee88ac18a2a53c988ff30ba1f561d3a5 +``` + +#### Delete Keys #### +You must know a key password to delete a key + +``` +plasmacli keys delete imported_keyfile +Enter passphrase: +Account deleted. +``` + +#### Update Keys #### +Update the password: + +``` +plasmacli keys update imported_key +Enter passphrase: +Enter new passphrase for your key: +Repeat passphrase: +Account passphrase has been updated. +``` + +Update the name of a key: + +``` +plasmacli keys update imported_key --name new_key_name +Account name has been updated. +``` + + diff --git a/docs/overview.md b/docs/overview.md index ef91727..c65a1bd 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -2,61 +2,105 @@ This sidechain is built on top of the Cosmos SDK and uses Tendermint for consensus. Therefore it is important to note the flow of a transaction as it would occur on any chain built off the SDK and not specifically on this one. -The cosmos sdk docs can be found [here](https://cosmos.network/docs/sdk/overview.html) +[Cosmos SDK documentation](https://cosmos.network/docs/) ## Tx and Msg -There are two main types in the SDK, a Tx and a Msg. A message (Msg) can be thought of in the traditional distributed systems sense of being data passed around between nodes. A transaction (Tx) contains a message as well as any other authentication data necessary to validate a message. Both Tx and Msg are interfaces such that there can be many different types of messages and transactions. - -In our implementation, we have a SpendMsg and a BaseTx. The SpendMsg contains all the information necessary to Spend 1 or 2 UTXOs, while BaseTx contains a SpendMsg and signatures of the RLP encoded SpendMsg. +There are two main types in the SDK, a Tx and a Msg. +A message (Msg) can be thought of in the traditional distributed systems sense of being data passed around between nodes. +A transaction (Tx) contains a message as well as any other authentication data necessary to validate a message. +Both Tx and Msg are interfaces such that there can be many different types of messages and transactions. + +In our implementation, we have a SpendMsg, IncludeDepositMsg and a Transaction. +The SpendMsg contains all the information necessary to Spend 1 or 2 UTXOs, while Transaction contains a SpendMsg and signatures of the RLP encoded SpendMsg. +We also have IncludeDepositMsg, which signifies to the sidechain that a deposit has occured on the rootchain and should be included into the utxo store. -## App -Within the app package, there is a struct called ChildChain. This represents the blockchain itself. A validator must first initialize an instance of the ChildChain and then begin passing transaction through it to get it to update state. +## Server/App +In the server directory we have two packages, app and plasmad. +Within the app directory, there exists a struct called PlasmaMVPChain in app.go. +This represents the blockchain itself. +A validator must first initialize an instance of the PlasmaMVP and then begin passing transaction through it to get it to update state. +The plasmad directory represents this initialization via a command line program. +Any user who wants to run a full node must install this package. -When passing around transactions and messages within the application, the SDK utilizes Context which provides all the necessary information to access context on the state of the blockchain when the transaction is being processed. There are two types of processes that can be run with a Context, checkTx and deliverTx. +When passing around transactions and messages within the application, the SDK utilizes Context which provides all the necessary information to access context on the state of the blockchain when the transaction is being processed. +There are two types of processes that can be run with a Context, checkTx and deliverTx. -CheckTx is executed on a transaction by a validator when it is deciding whether to include a transaction into a block (checkTx does not update state). DeliverTx is executed on a transaction after a transaction has been included into a block and therefore updates the state of our blockchain. +CheckTx is executed on a transaction by a validator when it is deciding whether to include a transaction into a block (checkTx does not update state). +DeliverTx is executed on a transaction after a transaction has been included into a block and therefore updates the state of our blockchain. ## Processing a transaction -When the tx bytes are sent to a validator, the validator executes the function ValidateBasic() which belongs to the Msg interface. ValidateBasic does a simple check to ensure that the message created is well formed. For example, SpendMsg will check that the two inputs provided don't equal each other (double spend) and that fields such as Oindex, which require a certain range of numbers, have been filled in appropriately. +When the tx bytes are sent to a validator, the validator executes the function ValidateBasic() which belongs to the Msg interface. +ValidateBasic does a simple check to ensure that the message created is well formed. +For example, SpendMsg will check that the two inputs provided don't equal each other (double spend) and that fields such as Oindex, which require a certain range of numbers, have been filled in appropriately. -If ValidateBasic does not return any errors, then the transaction is added to the mempool. CheckTx will then be executed before the tx is included into a block. CheckTx uses the ante handler to verify the authentication data in a transaction, verify the message against the state of the blockchain, and check that the fee provided is sufficient. +If ValidateBasic does not return any errors, then the transaction is added to the mempool. +CheckTx will then be executed before the tx is included into a block. +CheckTx uses the ante handler to verify the authentication data in a transaction, verify the message against the state of the blockchain, and check that the fee provided is sufficient. -Our ante handler will check that the address that created the signatures in BaseTx match those that own the inputs of the transaction as well as check that the inputs being spent exist. The ante handler also checks that the inputs = outputs + fee. InputConfirmationSigantures will also be checked for each input that is not a deposit nor a fee utxo. +Our ante handler will check that the address that created the signatures in Transaction match those that own the inputs of the transaction as well as check that the inputs being spent exist. +The ante handler also checks that the inputs = outputs + fee and that the utxo has not been exitted on the rootchain. +InputConfirmationSigantures will also be checked for each input that is not a deposit nor a fee utxo. Once a transaction has been included into a block, DeliverTx will be executed which will do the same functionality as CheckTx as well as route the Msg to a handler. -Our implementation utilizes our utxo module in x/utxo. The utxo module allows for modularity when creating a utxo based system. We use our BaseUTXO to implement the UTXO interface. In addition to the base traits of a UTXO, we also keep the TxHash, and InputAddresses in our UTXO. The ProtoUTXO() function allows for our extra information to be stored when the handler in x/utxo creates a new output utxo. +The UTXOMapper is our utxo database. +Our mapper uses keys in the form: < encoded address > + < encoded position > . +It maps to the encoded utxo and uses go-amino for its encoding. +The < encoded position > at the beginning of the key is used for prefix iteration which will return all the utxo's owned by a specified address. +## Store -The UTXOMapper is our utxo database. Our mapper uses keys in the form: < encoded address > + < encoded position > . It maps to the encoded utxo and uses go-amino for its encoding. The < encoded position > at the beginning of the key is used for prefix iteration which will return all the utxo's owned by a specified address. +There are two key value stores in our implementation, utxo store and plasma store. +The UTXO store is our utxo database and contains all information related to utxo's. +The plasma store contains plasma specific information including plasma block headers and confirmation signatures. -## Types +## Client -**SpendMsg** +The client directory contains command line client in plasmacli/ and key value stores for confirmation signatures and private keys in store/. +Plasmacli uses the eth command to do rootchain related actions and the query command to query the current state of the sidechain. +The keys command manages private key storage. +Sign can be used to generate and store confirmation signatures. +Confirmation signatures are stored using a goleveldb in .plasmacli/data/signatures.ldb. +We use a mapping from string to address to allow users assign human readable names to their keys. +This data is stored in .plasmacli/data/accounts.ldb -A SpendMsg contains the position of the input utxos (block number, transaction index, output index, deposit number), input confirmation signatures signed by the owners of the parent inputs (the inputs to the SpendMsg inputs), the addresses of the outputs, the amount of each output, and the fee amount. -GetSignBytes() returns the rlp encoded bytes of the SpendMsg +## Plasma -GetSigners() returns the input owner addresses as sdk.Address's +**Block** -**Position** +A block contains root hash, number of transactions, and fee associated with a plasma block. +A plasma block is different from a tendermint block. A tendermint block will contains all the messages of the sidechain while a plasma block contains only valid utxo transactions. -A Position contains the block number, transaction index, output index, and deposit number. +**Deposit** -IsValid() returns true if the position provided is valid in the context of the blockchain or false otherwise. For example, position {0, 0, 0, 0} is considered invalid on our sidechain. +A deposit contains the owner address, the amount of the deposit, and the ethereum block number it was created in. -**UTXO** +**Input** -A UTXO contains the address of the owner of the utxo, the amount, the position, the denomination, and the input addresses that were used to create the utxo. +A input contains the Position, transaction signature, and confirmation signature for the input. -## Utils +**Output** + +A output contains the owner address and amount of the output. + +**Position** + +A Position contains the block number, transaction index, output index, and deposit nonce. + +`FromPositionString(string)` takes in a string in the format "(blknum.txindex.oindex.depositnonce)" and returns a Position with the passed in values. -ZeroAddress(common.Address) returns true if the address provided is the zero address (0x00000...) and false otherwise +**Confirmation Signatures** -ValidAddress(common.Address) returns true if the address provided is properly formatted and false otherwise. +Confirmation Signatures are signed by the owners of the inputs in a transaction. +The Confirmation Hash that is signed over, comprises of the: hash of the transaction + root hash of the block the transaction occured in. +Transaction are not considered final until the confirmation signauture is signed and sent to the output owners. + +## Utils -PrivKeyToAddress(*ecdsa.PrivateKey) returns the common.Address corresponding to the private key provided. +IsZeroAddress(common.Address) returns true if the address provided is the zero address (0x00000...) and false otherwise -GenerateAddress() generates a random common.Address +RemoveHexPrefix(string) returns the passed in string without a "0x" prefix if it exists. +ToEthSignedMessage([] byte) returns the hash of the passed in message with "\x19EthereumSignedMessage\n32" prefixed. +This is [standard procedure](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign) for generating ethereum transactions. diff --git a/docs/testnet-setup/example_genesis.json b/docs/testnet-setup/example_genesis.json index 3daeb4d..9808f1f 100644 --- a/docs/testnet-setup/example_genesis.json +++ b/docs/testnet-setup/example_genesis.json @@ -1,6 +1,6 @@ { - "genesis_time": "2018-11-10T23:09:36.891350775Z", - "chain_id": "test-chain-z467uy", + "genesis_time": "2019-02-04T03:55:12.9352086Z", + "chain_id": "fourth-state-plasma", "consensus_params": { "block_size": { "max_bytes": "22020096", @@ -17,34 +17,12 @@ }, "app_hash": "", "app_state": { - "genvalidator": { + "validator": { "validator_pubkey": { "type": "tendermint/PubKeyEd25519", - "value": "M2fnNquqN9I17/gb2pK8Tq8QNy4vaQwUxEGKUe0f0Ys=" + "value": "pWyYZblMTzOqekbnRv9V7YHOr0tIXrVJayIHxoUuLS0=" }, - "fee_address": "0x495B9e12A0bC69a9dC83631bEde98de0CF0B298D" - }, - "UTXOs": [ - { - "Address": "0xc01BD94dB00C19d44D42c866cC14111015923160", - "Denom": "100", - "Position": [ - "0", - "0", - "0", - "1" - ] - }, - { - "Address": "0xc01BD94dB00C19d44D42c866cC14111015923160", - "Denom": "100", - "Position": [ - "0", - "0", - "0", - "2" - ] - } - ] + "fee_address": "0xec36ead9c897b609a4ffa5820e1b2b137d454343" + } } } diff --git a/docs/testnet-setup/example_plasmacli_plasma.toml b/docs/testnet-setup/example_plasmacli_plasma.toml new file mode 100644 index 0000000..4837dd9 --- /dev/null +++ b/docs/testnet-setup/example_plasmacli_plasma.toml @@ -0,0 +1,12 @@ +# This is a TOML config file. +# For more information, see https://github.com/toml-lang/toml + +# Ethereum plasma contract address +ethereum_plasma_contract_address = "0x9c36F39E87f3EA5283e186b385232044dC2f8c30" + +# Node URL for eth client +ethereum_nodeurl = "http://localhost:8545" + +# Number of Ethereum blocks until a transaction is considered final +ethereum_finality = "1" + diff --git a/docs/testnet-setup/example_plasmad_plasma.toml b/docs/testnet-setup/example_plasmad_plasma.toml new file mode 100644 index 0000000..c169677 --- /dev/null +++ b/docs/testnet-setup/example_plasmad_plasma.toml @@ -0,0 +1,24 @@ +# This is a TOML config file. +# For more information, see https://github.com/toml-lang/toml + +##### ethereum config options ##### +# Boolean specifying if this node is the operator of the plasma contract +is_operator = "true" + +# Hex encoded private key +# Used to sign eth transactions interacting with the contract +ethereum_operator_privatekey = "3cf72c162c8ba0348abecab744f9f1abf6d9bbf95ae33f7d2384ac9784dc1910" + +# Ethereum plasma contract address +ethereum_plasma_contract_address = "0x9c36F39E87f3EA5283e186b385232044dC2f8c30" + + +# Plasma block commitment rate. i.e 1m30s, 1m, 1h, etc. +plasma_block_commitment_rate = "1m" + +# Node URL for eth client +ethereum_nodeurl = "http://localhost:8545" + +# Number of Ethereum blocks until a submitted block header is considered final +ethereum_finality = "30" + diff --git a/docs/testnet-setup/example_truffle.js b/docs/testnet-setup/example_truffle.js new file mode 100644 index 0000000..174dbc2 --- /dev/null +++ b/docs/testnet-setup/example_truffle.js @@ -0,0 +1,19 @@ +module.exports = { + // See + // to customize your Truffle configuration! + networks: { + development: { + host: "localhost", + port: 8545, + network_id: "*" // Match any network id + }, + rinkeby: { + host: "localhost", + port: 8545, + network_id: "4", // Rinkeby ID 4 + from: "0xec36ead9c897b609a4ffa5820e1b2b137d454343", // account from which to deploy + gas: 6712390 + } + } +}; + diff --git a/docs/testnet-setup/full_node_config.toml b/docs/testnet-setup/full_node_config.toml index 096cbce..e565cce 100644 --- a/docs/testnet-setup/full_node_config.toml +++ b/docs/testnet-setup/full_node_config.toml @@ -8,7 +8,7 @@ proxy_app = "tcp://127.0.0.1:26658" # A custom human readable name for this node -moniker = "colinaxner" +moniker = "fourth-state-labs-sentry" # If this node is many blocks behind the tip of the chain, FastSync # allows them to catchup quickly by downloading blocks in parallel @@ -24,6 +24,9 @@ db_path = "data" # Output level for logging, including package level options log_level = "main:info,state:info,*:error" +# Output format: 'plain' (colored text) or 'json' +log_format = "plain + ##### additional base config options ##### # Path to the JSON file containing the initial validator set and other meta data @@ -32,6 +35,10 @@ genesis_file = "config/genesis.json" # Path to the JSON file containing the private key to use as a validator in the consensus protocol priv_validator_file = "config/priv_validator.json" +# TCP or UNIX socket address for Tendermint to listen on for +# connections from an external PrivValidator process +priv_validator_laddr = "" + # Path to the JSON file containing the private key to use for node authentication in the p2p protocol node_key_file = "config/node_key.json" @@ -53,15 +60,28 @@ filter_peers = false # TCP or UNIX socket address for the RPC server to listen on laddr = "tcp://0.0.0.0:26657" +# A list of origins a cross-domain request can be executed from +# Default value '[]' disables cors support +# Use '["*"]' to allow any origin +cors_allowed_origins = [] + +# A list of methods the client is allowed to use with cross-domain requests +cors_allowed_methods = ["HEAD", "GET", "POST", ] + +# A list of non simple headers the client is allowed to use with cross-domain requests +cors_allowed_headers = ["Origin", "Accept", "Content-Type", "X-Requested-With", "X-Server-Time", ] + # TCP or UNIX socket address for the gRPC server to listen on # NOTE: This server only supports /broadcast_tx_commit grpc_laddr = "" # Maximum number of simultaneous connections. # Does not include RPC (HTTP&WebSocket) connections. See max_open_connections -# If you want to accept more significant number than the default, make sure +# If you want to accept a larger number than the default, make sure # you increase your OS limits. # 0 - unlimited. +# Should be < {ulimit -Sn} - {MaxNumInboundPeers} - {MaxNumOutboundPeers} - {N of wal, db and other open files} +# 1024 - 40 - 10 - 50 = 924 = ~900 grpc_max_open_connections = 900 # Activate unsafe RPC commands like /dial_seeds and /unsafe_flush_mempool @@ -69,9 +89,11 @@ unsafe = false # Maximum number of simultaneous connections (including WebSocket). # Does not include gRPC connections. See grpc_max_open_connections -# If you want to accept more significant number than the default, make sure +# If you want to accept a larger number than the default, make sure # you increase your OS limits. # 0 - unlimited. +# Should be < {ulimit -Sn} - {MaxNumInboundPeers} - {MaxNumOutboundPeers} - {N of wal, db and other open files} +# 1024 - 40 - 10 - 50 = 924 = ~900 max_open_connections = 900 ##### peer to peer configuration options ##### @@ -101,11 +123,14 @@ addr_book_file = "config/addrbook.json" # Set true for strict address routability rules addr_book_strict = true -# Time to wait before flushing messages out on the connection, in ms -flush_throttle_timeout = 100 +# Maximum number of inbound peers +max_num_inbound_peers = 40 + +# Maximum number of outbound peers to connect to, excluding persistent peers +max_num_outbound_peers = 10 -# Maximum number of peers to connect to -max_num_peers = 50 +# Time to wait before flushing messages out on the connection +flush_throttle_timeout = "100ms" # Maximum size of a message packet payload, in bytes max_packet_msg_payload_size = 1024 @@ -128,44 +153,52 @@ seed_mode = false # Comma separated list of peer IDs to keep private (will not be gossiped to other peers) private_peer_ids = "" +# Toggle to disable guard against peers connecting from the same ip. +allow_duplicate_ip = true + +# Peer connection configuration. +handshake_timeout = "20s" +dial_timeout = "3s" + ##### mempool configuration options ##### [mempool] recheck = true -recheck_empty = true broadcast = true -wal_dir = "data/mempool.wal" +wal_dir = "" # size of the mempool -size = 100000 +size = 5000 # size of the cache (used to filter transactions we saw earlier) -cache_size = 100000 +cache_size = 10000 ##### consensus configuration options ##### [consensus] wal_file = "data/cs.wal/wal" -# All timeouts are in milliseconds -timeout_propose = 3000 -timeout_propose_delta = 500 -timeout_prevote = 1000 -timeout_prevote_delta = 500 -timeout_precommit = 1000 -timeout_precommit_delta = 500 -timeout_commit = 5000 +timeout_propose = "3s" +timeout_propose_delta = "500ms" +timeout_prevote = "1s" +timeout_prevote_delta = "500ms" +timeout_precommit = "1s" +timeout_precommit_delta = "500ms" +timeout_commit = "180s" # Make progress as soon as we have all the precommits (as if TimeoutCommit = 0) skip_timeout_commit = false -# EmptyBlocks mode and possible interval between empty blocks in seconds -create_empty_blocks = false -create_empty_blocks_interval = 0 +# EmptyBlocks mode and possible interval between empty blocks +create_empty_blocks = true +create_empty_blocks_interval = "0s" + +# Reactor sleep duration parameters +peer_gossip_sleep_duration = "100ms" +peer_query_maj23_sleep_duration = "2s" -# Reactor sleep duration parameters are in milliseconds -peer_gossip_sleep_duration = 100 -peer_query_maj23_sleep_duration = 2000 +# Block time parameters. Corresponds to the minimum time increment between consecutive blocks. +blocktime_iota = "1s" ##### transactions indexer configuration options ##### [tx_index] @@ -173,20 +206,24 @@ peer_query_maj23_sleep_duration = 2000 # What indexer to use for transactions # # Options: -# 1) "null" (default) -# 2) "kv" - the simplest possible indexer, backed by key-value storage (defaults to levelDB; see DBBackend). +# 1) "null" +# 2) "kv" (default) - the simplest possible indexer, backed by key-value storage (defaults to levelDB; see DBBackend). indexer = "kv" -# Comma-separated list of tags to index (by default the only tag is tx hash) +# Comma-separated list of tags to index (by default the only tag is "tx.hash") +# +# You can also index transactions by height by adding "tx.height" tag here. # # It's recommended to index only a subset of tags due to possible memory # bloat. This is, of course, depends on the indexer's DB and the volume of # transactions. index_tags = "" - -# When set to true, tells indexer to index all tags. Note this may be not -# desirable (see the comment above). IndexTags has a precedence over -# IndexAllTags (i.e. when given both, IndexTags will be indexed). +# When set to true, tells indexer to index all tags (predefined tags: +# "tx.hash", "tx.height" and all tags from DeliverTx responses). +# +# Note this may be not desirable (see the comment above). IndexTags has a +# precedence over IndexAllTags (i.e. when given both, IndexTags will be +# indexed). index_all_tags = true ##### instrumentation configuration options ##### @@ -201,7 +238,11 @@ prometheus = false prometheus_listen_addr = ":26660" # Maximum number of simultaneous connections. -# If you want to accept more significant number than the default, make sure +# If you want to accept a larger number than the default, make sure # you increase your OS limits. # 0 - unlimited. max_open_connections = 3 + +# Instrumentation namespace +namespace = "tendermint" + diff --git a/docs/testnet-setup/validator_config.toml b/docs/testnet-setup/validator_config.toml index dc841d9..8b8b14e 100644 --- a/docs/testnet-setup/validator_config.toml +++ b/docs/testnet-setup/validator_config.toml @@ -8,7 +8,7 @@ proxy_app = "tcp://127.0.0.1:26658" # A custom human readable name for this node -moniker = "aditya" +moniker = "fourth-state-labs" # If this node is many blocks behind the tip of the chain, FastSync # allows them to catchup quickly by downloading blocks in parallel @@ -24,6 +24,9 @@ db_path = "data" # Output level for logging, including package level options log_level = "main:info,state:info,*:error" +# Output format: 'plain' (colored text) or 'json' +log_format = "plain" + ##### additional base config options ##### # Path to the JSON file containing the initial validator set and other meta data @@ -32,6 +35,10 @@ genesis_file = "config/genesis.json" # Path to the JSON file containing the private key to use as a validator in the consensus protocol priv_validator_file = "config/priv_validator.json" +# TCP or UNIX socket address for Tendermint to listen on for +# connections from an external PrivValidator process +priv_validator_laddr = "" + # Path to the JSON file containing the private key to use for node authentication in the p2p protocol node_key_file = "config/node_key.json" @@ -53,15 +60,28 @@ filter_peers = false # TCP or UNIX socket address for the RPC server to listen on laddr = "tcp://0.0.0.0:26657" +# A list of origins a cross-domain request can be executed from +# Default value '[]' disables cors support +# Use '["*"]' to allow any origin +cors_allowed_origins = [] + +# A list of methods the client is allowed to use with cross-domain requests +cors_allowed_methods = ["HEAD", "GET", "POST", ] + +# A list of non simple headers the client is allowed to use with cross-domain requests +cors_allowed_headers = ["Origin", "Accept", "Content-Type", "X-Requested-With", "X-Server-Time", ] + # TCP or UNIX socket address for the gRPC server to listen on # NOTE: This server only supports /broadcast_tx_commit grpc_laddr = "" # Maximum number of simultaneous connections. # Does not include RPC (HTTP&WebSocket) connections. See max_open_connections -# If you want to accept more significant number than the default, make sure +# If you want to accept a larger number than the default, make sure # you increase your OS limits. # 0 - unlimited. +# Should be < {ulimit -Sn} - {MaxNumInboundPeers} - {MaxNumOutboundPeers} - {N of wal, db and other open files} +# 1024 - 40 - 10 - 50 = 924 = ~900 grpc_max_open_connections = 900 # Activate unsafe RPC commands like /dial_seeds and /unsafe_flush_mempool @@ -69,9 +89,11 @@ unsafe = false # Maximum number of simultaneous connections (including WebSocket). # Does not include gRPC connections. See grpc_max_open_connections -# If you want to accept more significant number than the default, make sure +# If you want to accept a larger number than the default, make sure # you increase your OS limits. # 0 - unlimited. +# Should be < {ulimit -Sn} - {MaxNumInboundPeers} - {MaxNumOutboundPeers} - {N of wal, db and other open files} +# 1024 - 40 - 10 - 50 = 924 = ~900 max_open_connections = 900 ##### peer to peer configuration options ##### @@ -101,11 +123,14 @@ addr_book_file = "config/addrbook.json" # Set true for strict address routability rules addr_book_strict = true -# Time to wait before flushing messages out on the connection, in ms -flush_throttle_timeout = 100 +# Maximum number of inbound peers +max_num_inbound_peers = 40 + +# Maximum number of outbound peers to connect to, excluding persistent peers +max_num_outbound_peers = 10 -# Maximum number of peers to connect to -max_num_peers = 50 +# Time to wait before flushing messages out on the connection +flush_throttle_timeout = "100ms" # Maximum size of a message packet payload, in bytes max_packet_msg_payload_size = 1024 @@ -128,44 +153,52 @@ seed_mode = true # Comma separated list of peer IDs to keep private (will not be gossiped to other peers) private_peer_ids = "" +# Toggle to disable guard against peers connecting from the same ip. +allow_duplicate_ip = true + +# Peer connection configuration. +handshake_timeout = "20s" +dial_timeout = "3s" + ##### mempool configuration options ##### [mempool] recheck = true -recheck_empty = true broadcast = true -wal_dir = "data/mempool.wal" +wal_dir = "" # size of the mempool -size = 100000 +size = 5000 # size of the cache (used to filter transactions we saw earlier) -cache_size = 100000 +cache_size = 10000 ##### consensus configuration options ##### [consensus] wal_file = "data/cs.wal/wal" -# All timeouts are in milliseconds -timeout_propose = 3000 -timeout_propose_delta = 500 -timeout_prevote = 1000 -timeout_prevote_delta = 500 -timeout_precommit = 1000 -timeout_precommit_delta = 500 -timeout_commit = 5000 +timeout_propose = "3s" +timeout_propose_delta = "500ms" +timeout_prevote = "1s" +timeout_prevote_delta = "500ms" +timeout_precommit = "1s" +timeout_precommit_delta = "500ms" +timeout_commit = "180s" # Make progress as soon as we have all the precommits (as if TimeoutCommit = 0) skip_timeout_commit = false -# EmptyBlocks mode and possible interval between empty blocks in seconds -create_empty_blocks = false -create_empty_blocks_interval = 0 +# EmptyBlocks mode and possible interval between empty blocks +create_empty_blocks = true +create_empty_blocks_interval = "0s" + +# Reactor sleep duration parameters +peer_gossip_sleep_duration = "100ms" +peer_query_maj23_sleep_duration = "2s" -# Reactor sleep duration parameters are in milliseconds -peer_gossip_sleep_duration = 100 -peer_query_maj23_sleep_duration = 2000 +# Block time parameters. Corresponds to the minimum time increment between consecutive blocks. +blocktime_iota = "1s" ##### transactions indexer configuration options ##### [tx_index] @@ -173,20 +206,25 @@ peer_query_maj23_sleep_duration = 2000 # What indexer to use for transactions # # Options: -# 1) "null" (default) -# 2) "kv" - the simplest possible indexer, backed by key-value storage (defaults to levelDB; see DBBackend). +# 1) "null" +# 2) "kv" (default) - the simplest possible indexer, backed by key-value storage (defaults to levelDB; see DBBackend). indexer = "kv" -# Comma-separated list of tags to index (by default the only tag is tx hash) +# Comma-separated list of tags to index (by default the only tag is "tx.hash") +# +# You can also index transactions by height by adding "tx.height" tag here. # # It's recommended to index only a subset of tags due to possible memory # bloat. This is, of course, depends on the indexer's DB and the volume of # transactions. index_tags = "" -# When set to true, tells indexer to index all tags. Note this may be not -# desirable (see the comment above). IndexTags has a precedence over -# IndexAllTags (i.e. when given both, IndexTags will be indexed). +# When set to true, tells indexer to index all tags (predefined tags: +# "tx.hash", "tx.height" and all tags from DeliverTx responses). +# +# Note this may be not desirable (see the comment above). IndexTags has a +# precedence over IndexAllTags (i.e. when given both, IndexTags will be +# indexed). index_all_tags = true ##### instrumentation configuration options ##### @@ -201,7 +239,11 @@ prometheus = false prometheus_listen_addr = ":26660" # Maximum number of simultaneous connections. -# If you want to accept more significant number than the default, make sure +# If you want to accept a larger number than the default, make sure # you increase your OS limits. # 0 - unlimited. max_open_connections = 3 + +# Instrumentation namespace +namespace = "tendermint" + diff --git a/eth/common_test.go b/eth/common_test.go new file mode 100644 index 0000000..97f24c1 --- /dev/null +++ b/eth/common_test.go @@ -0,0 +1,26 @@ +package eth + +import ( + "github.com/FourthState/plasma-mvp-sidechain/store" + cosmosStore "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/db" + "github.com/tendermint/tendermint/libs/log" +) + +func setup() (sdk.Context, store.DataStore) { + db := db.NewMemDB() + ms := cosmosStore.NewCommitMultiStore(db) + + ctx := sdk.NewContext(ms, abci.Header{}, false, log.NewNopLogger()) + + dsKey := sdk.NewKVStoreKey(store.DataStoreName) + + ms.MountStoreWithDB(dsKey, sdk.StoreTypeIAVL, db) + ms.LoadLatestVersion() + + ds := store.NewDataStore(dsKey) + + return ctx, ds +} diff --git a/eth/main.go b/eth/main.go new file mode 100644 index 0000000..499f709 --- /dev/null +++ b/eth/main.go @@ -0,0 +1,82 @@ +// Package eth provides the connection of the plasma chain to the ethereum +// chain. +package eth + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/utils" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/rpc" + "math/big" +) + +// Client defines wrappers to a remote endpoint +type Client struct { + rpc *rpc.Client + ec *ethclient.Client +} + +// InitEthConn will instantiate a connection and bind the go plasma contract +// wrapper with this client. Will return an error if the ethereum client is +// not fully sycned. +func InitEthConn(nodeUrl string) (Client, error) { + // Connect to a remote etheruem client + // + // Ethclient wraps around the underlying rpc module and provides convenient functions. We still keep reference + // to the underlying rpc module to make calls that the wrapper does not support + c, err := rpc.Dial(nodeUrl) + if err != nil { + return Client{}, err + } + ec := ethclient.NewClient(c) + + // check if the client is synced + client := Client{c, ec} + if synced, err := client.Synced(); !synced || err != nil { + if err != nil { + return client, err + } else { + return client, fmt.Errorf("geth endpoint is not fully synced") + } + } + + return client, nil +} + +// Synced checks of the status of the geth endpoint with it's network +func (client Client) Synced() (bool, error) { + var res json.RawMessage + if err := client.rpc.Call(&res, "eth_syncing"); err != nil { + return false, fmt.Errorf("rpc: %s", err) + } + + if string(res) != "false" { + return false, nil + } + + return true, nil +} + +// LatestBlockNum retrieves the latest block height of the of the geth endpoint +func (client Client) LatestBlockNum() (*big.Int, error) { + var hexStr string + if err := client.rpc.Call(&hexStr, "eth_blockNumber"); err != nil { + return nil, fmt.Errorf("rpc: %s", err) + } + + hexStr = utils.RemoveHexPrefix(hexStr) + + // pad if hex length is odd + if len(hexStr)%2 != 0 { + hexStr = "0" + hexStr + } + + hexBytes, err := hex.DecodeString(hexStr) + if err != nil { + return nil, fmt.Errorf("hex: %s", err) + } + + return new(big.Int).SetBytes(hexBytes), nil +} diff --git a/eth/plasma.go b/eth/plasma.go new file mode 100644 index 0000000..e75a6bc --- /dev/null +++ b/eth/plasma.go @@ -0,0 +1,268 @@ +package eth + +import ( + "bytes" + "crypto/ecdsa" + "fmt" + contracts "github.com/FourthState/plasma-mvp-sidechain/contracts/wrappers" + plasmaTypes "github.com/FourthState/plasma-mvp-sidechain/plasma" + "github.com/FourthState/plasma-mvp-sidechain/store" + "github.com/FourthState/plasma-mvp-sidechain/utils" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/tendermint/tendermint/libs/log" + "math/big" + "time" +) + +var logger log.Logger = log.NewNopLogger() + +// SetLogger will adjust the logger used in the eth module +func SetLogger(l log.Logger) { + logger = l +} + +// Plasma holds related unexported members +type Plasma struct { + *contracts.PlasmaMVP // expose all the contract methods + + client Client + finalityBound uint64 + operatorSession *operatorSession +} + +type operatorSession struct { + *contracts.PlasmaMVPSession + + commitmentRate time.Duration + lastBlockSubmission time.Time +} + +// InitPlasma binds the go wrapper to the deployed contract. This private key provides authentication for the operator +func InitPlasma(contractAddr common.Address, client Client, finalityBound uint64) (*Plasma, error) { + logger.Info(fmt.Sprintf("binding to contract address 0x%x", contractAddr)) + plasmaContract, err := contracts.NewPlasmaMVP(contractAddr, client.ec) + if err != nil { + return nil, err + } + + plasma := &Plasma{ + PlasmaMVP: plasmaContract, + client: client, + finalityBound: finalityBound, + } + + return plasma, nil +} + +// WithOperatorSession will set up an operators session with the smart contract. The contract's operator public key must +// match the public key corresponding `operatorPrivKey` +func (plasma *Plasma) WithOperatorSession(operatorPrivkey *ecdsa.PrivateKey, commitmentRate time.Duration) (*Plasma, error) { + logger.Info(fmt.Sprintf("block commitment rate set to %s", commitmentRate)) + + // check that the public key matches the address of the operator + addr := crypto.PubkeyToAddress(operatorPrivkey.PublicKey) + operator, err := plasma.OperatorAddress() + if err != nil { + return plasma, err + } + if !bytes.Equal(operator[:], addr[:]) { + return plasma, fmt.Errorf("operator address mismatch. Got 0x%x. Expected:0x%x", addr, operator) + } + + auth := bind.NewKeyedTransactor(operatorPrivkey) + contractSession := &contracts.PlasmaMVPSession{ + Contract: plasma.PlasmaMVP, + CallOpts: bind.CallOpts{ + Pending: true, + }, + TransactOpts: bind.TransactOpts{ + From: auth.From, + Signer: auth.Signer, + GasLimit: 3141592, // aribitrary. TODO: check this + }, + } + + opSession := &operatorSession{ + PlasmaMVPSession: contractSession, + commitmentRate: commitmentRate, + lastBlockSubmission: time.Now(), + } + + plasma.operatorSession = opSession + return plasma, nil +} + +// OperatorAddress will fetch the plasma operator address from the connected smart contract +func (plasma *Plasma) OperatorAddress() (common.Address, error) { + return plasma.Operator(nil) +} + +// CommitPlasmaHeaders will commit all new non-committed headers to the smart contract. +// the commitmentRate interval must pass since the last commitment +func (plasma *Plasma) CommitPlasmaHeaders(ctx sdk.Context, ds store.DataStore) error { + // only the contract operator can submit blocks. The commitment duration must also pass + if plasma.operatorSession == nil || time.Since(plasma.operatorSession.lastBlockSubmission).Seconds() < plasma.operatorSession.commitmentRate.Seconds() { + return nil + } + + logger.Info("attempting to commit plasma headers...") + + lastCommittedBlock, err := plasma.LastCommittedBlock(nil) + if err != nil { + logger.Error("error retrieving the last committed block number") + return err + } + + firstBlockNum := new(big.Int).Add(lastCommittedBlock, utils.Big1) + blockNum := lastCommittedBlock.Add(lastCommittedBlock, utils.Big1) + + var ( + headers [][32]byte + txnsPerBlock []*big.Int + feesPerBlock []*big.Int + ) + + block, ok := ds.GetBlock(ctx, blockNum) + if !ok { // no blocks to submit + logger.Info("no plasma blocks to commit") + return nil + } + + for ok { + headers = append(headers, block.Header) + txnsPerBlock = append(txnsPerBlock, big.NewInt(int64(block.TxnCount))) + feesPerBlock = append(feesPerBlock, block.FeeAmount) + + blockNum = blockNum.Add(blockNum, utils.Big1) + block, ok = ds.GetBlock(ctx, blockNum) + } + + logger.Info(fmt.Sprintf("committing %d plasma blocks. first block num: %s", len(headers), firstBlockNum)) + plasma.operatorSession.lastBlockSubmission = time.Now() + _, err = plasma.operatorSession.SubmitBlock(headers, txnsPerBlock, feesPerBlock, firstBlockNum) + if err != nil { + logger.Error(fmt.Sprintf("error committing headers { %s }", err)) + return err + } + + return err +} + +// GetDeposit checks the existence of a deposit nonce. The state is synchronized with the provided `plasmaBlockHeight. The deposit +// must have occured before or at the same pegged ethereum block as `plasmaBlockHeight`. +func (plasma *Plasma) GetDeposit(plasmaBlockHeight *big.Int, nonce *big.Int) (plasmaTypes.Deposit, *big.Int, bool) { + deposit, err := plasma.Deposits(nil, nonce) + if err != nil { + logger.Error(fmt.Sprintf("failed deposit retrieval: %s", err)) + return plasmaTypes.Deposit{}, nil, false + } + + if deposit.CreatedAt.Sign() == 0 { + return plasmaTypes.Deposit{}, nil, false + } + + // check the finality bound based off pegged ETH block + ethBlockNum, err := plasma.ethBlockPeg(plasmaBlockHeight) + if err != nil { + logger.Error(fmt.Sprintf("could not get pegged ETH Block for sidechain block %s: %s", plasmaBlockHeight, err)) + return plasmaTypes.Deposit{}, nil, false + } + + // how many blocks have occurred since deposit. + // Note: Since pegged ETH block num could be before deposit's EthBlockNum, interval may be negative + interval := new(big.Int).Sub(ethBlockNum, deposit.EthBlockNum) + // how many more blocks need to get added for deposit to be considered final + // Note: If deposit is finalized, threshold can be 0 or negative + threshold := new(big.Int).Sub(big.NewInt(int64(plasma.finalityBound)), interval) + if threshold.Sign() > 0 { + return plasmaTypes.Deposit{}, threshold, false + } + + return plasmaTypes.Deposit{ + Owner: deposit.Owner, + Amount: deposit.Amount, + EthBlockNum: deposit.EthBlockNum, + }, threshold, true +} + +// HasTxExited indicates if the position has ever been exited at a time less than or equal to +// the time `plasmaBlockHeight` was submitted. If nil, it is checked against the latest state +func (plasma *Plasma) HasTxExited(plasmaBlockHeight *big.Int, position plasmaTypes.Position) (bool, error) { + type exit struct { + Amount *big.Int + CommittedFee *big.Int + CreatedAt *big.Int + EthBlockNum *big.Int + Owner common.Address + State uint8 + } + + var ( + e exit + err error + ) + + priority := position.Priority() + if position.IsDeposit() { + e, err = plasma.DepositExits(nil, priority) + } else { + e, err = plasma.TxExits(nil, priority) + } + + // censor spends until the error is fixed + if err != nil { + logger.Error(fmt.Sprintf("failed to retrieve exit information about position %s { %s }", position, err)) + return true, err + } + + // `Pending` or `Challenged` stateee + exited := e.State == 1 || e.State == 3 + + // synchronize with the correct ethereum state + if plasmaBlockHeight != nil { + ethBlockNum, err := plasma.ethBlockPeg(plasmaBlockHeight) + if err != nil { + // censore spends until the error is fixed + logger.Error(fmt.Sprintf("could not get associated ETH Block for plasma block %s: %s", plasmaBlockHeight, err)) + return true, err + } + + // exited AND the exit occured before or in the pegged ethereum block + return exited && e.EthBlockNum.Cmp(ethBlockNum) <= 0, nil + } + + return exited, nil +} + +// Return the Ethereum Block that sidechain should use to synchronize current block tx's with rootchain state +func (plasma *Plasma) ethBlockPeg(plasmaBlockHeight *big.Int) (*big.Int, error) { + lastCommittedBlock, err := plasma.LastCommittedBlock(nil) + if err != nil { + return nil, err + } + // If no blocks submitted, use latestBlock as peg + if lastCommittedBlock.Sign() == 0 { + latestBlock, err := plasma.client.LatestBlockNum() + if err != nil { + return nil, err + } + return latestBlock, nil + } + var blockIndex *big.Int + prevBlock := new(big.Int).Sub(plasmaBlockHeight, big.NewInt(1)) + // For syncing nodes, peg to EthBlock at plasmaBlock-1 submission + // For live nodes, peg to LastCommittedBlock + if lastCommittedBlock.Cmp(prevBlock) == 1 { + blockIndex = prevBlock + } else { + blockIndex = lastCommittedBlock + } + submittedBlock, err := plasma.PlasmaChain(nil, blockIndex) + if err != nil { + return nil, err + } + return submittedBlock.EthBlockNum, nil +} diff --git a/eth/plasma_test.go b/eth/plasma_test.go new file mode 100644 index 0000000..308f0bd --- /dev/null +++ b/eth/plasma_test.go @@ -0,0 +1,177 @@ +package eth + +import ( + "bytes" + "crypto/sha256" + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/plasma" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" + "math/big" + "testing" + "time" +) + +// private/public keys using the `plasma` mnemonic with ganache-cli +// `ganache-cli -m=plasma` +// plasmaContractAddr will be deterministic. `truffle migrate` immediately after `ganache-cli -m=plasma` +const ( + clientAddr = "http://127.0.0.1:8545" + plasmaContractAddr = "31E491FC70cDb231774c61B7F46d94699dacE664" + operatorPrivKey = "9cd69f009ac86203e54ec50e3686de95ff6126d3b30a19f926a0fe9323c17181" + + minExitBond = 200000 +) + +var ( + commitmentRate, _ = time.ParseDuration("1s") +) + +func TestConnection(t *testing.T) { + t.Logf("Connecting to remote client: %s", clientAddr) + t.Logf("Connecting to contract address: 0x%s", plasmaContractAddr) + client, err := InitEthConn(clientAddr) + require.NoError(t, err, "connection error") + + _, err = client.Synced() + require.NoError(t, err, "error checking synced status") +} + +func TestLatestBlockNum(t *testing.T) { + client, _ := InitEthConn(clientAddr) + _, err := client.LatestBlockNum() + + require.NoError(t, err) +} + +func TestPlasmaInit(t *testing.T) { + client, err := InitEthConn(clientAddr) + _, err = InitPlasma(common.HexToAddress(plasmaContractAddr), client, 1) + + require.NoError(t, err, "error binding to contract") +} + +// Test needs to be changed to simulate the sdk context and plasma store. +func TestSubmitBlock(t *testing.T) { + client, _ := InitEthConn(clientAddr) + privKey, _ := crypto.HexToECDSA(operatorPrivKey) + plasmaContract, _ := InitPlasma(common.HexToAddress(plasmaContractAddr), client, 1) + plasmaContract, _ = plasmaContract.WithOperatorSession(privKey, commitmentRate) + + // Setup context and block store + ctx, blockStore := setup() + + time.Sleep(2 * time.Second) + + // Submit 2 blocks + var expectedBlocks []plasma.Block + for i := 1; i < 3; i++ { + block := plasma.Block{ + Header: sha256.Sum256([]byte(fmt.Sprintf("Block: %d", i))), + TxnCount: uint16(i + 1), + FeeAmount: big.NewInt(int64(i + 2)), + } + expectedBlocks = append(expectedBlocks, block) + blockStore.StoreBlock(ctx, uint64(i), block) + } + + err := plasmaContract.CommitPlasmaHeaders(ctx, blockStore) + + require.NoError(t, err, "block submission error") + + blockNum, err := plasmaContract.LastCommittedBlock(nil) + require.NoError(t, err, "failed to query for the last committed block") + require.Equal(t, big.NewInt(2), blockNum, "Did not submit both blocks correctly") + + for j := 0; j < 2; j++ { + result, err := plasmaContract.PlasmaChain(nil, big.NewInt(int64(j+1))) + require.NoError(t, err, "failed contract plasma chain query") + + require.Truef(t, bytes.Compare(expectedBlocks[j].Header[:], result.Header[:]) == 0, + "Mismatch in block headers for submitted block %d. Got: %x. Expected: %x", j, result.Header[:], expectedBlocks[j].Header[:]) + + require.Equal(t, big.NewInt(int64(expectedBlocks[j].TxnCount)), result.NumTxns, fmt.Sprintf("Wrong number of tx's for submitted block: %d", j)) + + require.Equal(t, expectedBlocks[j].FeeAmount, result.FeeAmount, fmt.Sprintf("Wrong Fee amount for submitted block: %d", j)) + + } +} + +func TestDepositFinalityBound(t *testing.T) { + client, _ := InitEthConn(clientAddr) + privKey, _ := crypto.HexToECDSA(operatorPrivKey) + plasmaContract, _ := InitPlasma(common.HexToAddress(plasmaContractAddr), client, 1) + plasmaContract, _ = plasmaContract.WithOperatorSession(privKey, commitmentRate) + + // mine a block so that the headers channel is filled with a block + err := client.rpc.Call(nil, "evm_mine") + require.NoError(t, err, "error mining a block") + time.Sleep(1 * time.Second) + + nonce, err := plasmaContract.DepositNonce(nil) + require.NoError(t, err, "error querying for the deposit nonce") + + // Deposit 10 eth from the operator + plasmaContract.operatorSession.TransactOpts.Value = big.NewInt(10) + operatorAddress := crypto.PubkeyToAddress(privKey.PublicKey) + _, err = plasmaContract.operatorSession.Deposit(operatorAddress) + require.NoError(t, err, "error sending a deposit tx") + + // Setup context and block store + ctx, blockStore := setup() + + // Reset operatorSession + plasmaContract.operatorSession.TransactOpts.Value = nil + + var block plasma.Block + // Must restore old blocks since we're using fresh plasmaStore but using old contract + // that already has submitted blocks. Store blocks 1-3 to get plasmaConn to submit new block 3 + for i := 1; i < 4; i++ { + block = plasma.Block{ + Header: sha256.Sum256([]byte(fmt.Sprintf("Block: %d", i))), + TxnCount: uint16(i + 1), + FeeAmount: big.NewInt(int64(i + 2)), + } + blockStore.StoreBlock(ctx, uint64(i), block) + } + + err = plasmaContract.CommitPlasmaHeaders(ctx, blockStore) + + require.NoError(t, err, "block submission error") + + err = plasmaContract.CommitPlasmaHeaders(ctx, blockStore) + require.NoError(t, err, "block submission error") + + // Try to retrieve deposit from before peg + _, threshold, ok := plasmaContract.GetDeposit(big.NewInt(2), nonce) + require.False(t, ok, "retrieved a deposit that occurred after pegged block") + require.Equal(t, big.NewInt(3), threshold, "Finality threshold calculated incorrectly. Should still need to wait two more blocks") + + /* Mine 3 blocks for finality bound */ + for i := 0; i < 3; i++ { + // mine another block so that the deposit falls outside the finality bound + err = client.rpc.Call(nil, "evm_mine") + require.NoError(t, err, "error mining a block") + time.Sleep(1 * time.Second) + } + + /* Submit block to advance peg */ + block = plasma.Block{ + Header: sha256.Sum256([]byte("Block: 4")), + TxnCount: uint16(2), + FeeAmount: big.NewInt(3), + } + blockStore.StoreBlock(ctx, uint64(4), block) + + err = plasmaContract.CommitPlasmaHeaders(ctx, blockStore) + require.NoError(t, err, "block submission error") + + // Try to retrieve deposit once peg has advanced AND finality bound reached. + deposit, threshold, ok := plasmaContract.GetDeposit(big.NewInt(4), nonce) + require.True(t, ok, "could not retrieve a deposit that was deemed final") + + require.Equal(t, uint64(10), deposit.Amount.Uint64(), "deposit amount mismatch") + require.True(t, bytes.Equal(operatorAddress[:], deposit.Owner[:]), "deposit owner mismatch") + require.True(t, threshold.Sign() == 0, "Finality threshold not calculated correctly. Deposit should be final with threshold = 0") +} diff --git a/eth/util.go b/eth/util.go new file mode 100644 index 0000000..b19a739 --- /dev/null +++ b/eth/util.go @@ -0,0 +1,38 @@ +package eth + +import ( + "bytes" + "math/big" +) + +const ( + // prefixes + prefixSeperator = "::" + depositPrefix = "deposit" + transactionExitPrefix = "txExit" + depositExitPrefix = "depositExit" + + // constants + blockIndexFactor = 1000000 + txIndexFactor = 10 +) + +func prefixKey(prefix string, key []byte) []byte { + buffer := new(bytes.Buffer) + buffer.Write([]byte(prefix)) + buffer.Write([]byte(prefixSeperator)) + buffer.Write(key) + return buffer.Bytes() +} + +// [blockNumber, txIndex, outputIndex] +func calcPriority(position [3]*big.Int) *big.Int { + bFactor := big.NewInt(blockIndexFactor) + tFactor := big.NewInt(txIndexFactor) + + bFactor = bFactor.Mul(bFactor, position[0]) + tFactor = tFactor.Mul(tFactor, position[1]) + + temp := new(big.Int).Add(bFactor, tFactor) + return temp.Add(temp, position[2]) +} diff --git a/eth/util_test.go b/eth/util_test.go new file mode 100644 index 0000000..86d667c --- /dev/null +++ b/eth/util_test.go @@ -0,0 +1,53 @@ +package eth + +import ( + "bytes" + "math/big" + "testing" +) + +func TestPriorityCalc(t *testing.T) { + position := [3]*big.Int{big.NewInt(1), big.NewInt(2), big.NewInt(3)} + expected := uint64(1000000*1 + 10*2 + 3) + + priority := calcPriority(position).Uint64() + if priority != expected { + t.Fatalf("Position [1,2,3] yielded priority %d. Expected %d", + priority, expected) + } + + position = [3]*big.Int{big.NewInt(0), big.NewInt(2), big.NewInt(3)} + expected = uint64(10*2 + 3) + + priority = calcPriority(position).Uint64() + if priority != expected { + t.Fatalf("Position [0,2,3] yielded priority %d. Expected %d", + priority, expected) + } + + position = [3]*big.Int{big.NewInt(1), big.NewInt(0), big.NewInt(3)} + expected = uint64(1000000*1 + 3) + + priority = calcPriority(position).Uint64() + if priority != expected { + t.Fatalf("Position [1,0,3] yielded priority %d. Expected %d", + priority, expected) + } + + position = [3]*big.Int{big.NewInt(1), big.NewInt(2), big.NewInt(0)} + expected = uint64(1000000*1 + 10*2) + + priority = calcPriority(position).Uint64() + if priority != expected { + t.Fatalf("Position [1,2,0] yielded priority %d. Expected %d", + priority, expected) + } +} + +func TestPrefixKey(t *testing.T) { + expectedKey := []byte("prefix::key") + key := prefixKey("prefix", []byte("key")) + if !bytes.Equal(key, expectedKey) { + t.Fatalf("Actual: %s, Got: %s", string(expectedKey), string(key)) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..34949cd --- /dev/null +++ b/go.mod @@ -0,0 +1,73 @@ +module github.com/FourthState/plasma-mvp-sidechain + +require ( + github.com/VividCortex/gohistogram v1.0.0 // indirect + github.com/allegro/bigcache v1.2.0 // indirect + github.com/aristanetworks/goarista v0.0.0-20190219163901-728bce664cf5 // indirect + github.com/bartekn/go-bip39 v0.0.0-20171116152956-a05967ea095d // indirect + github.com/bgentry/speakeasy v0.1.0 // indirect + github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d // indirect + github.com/cespare/cp v1.1.1 // indirect + github.com/cosmos/cosmos-sdk v0.32.0 + github.com/cosmos/go-bip39 v0.0.0-20180618194314-52158e4697b8 // indirect + github.com/deckarep/golang-set v1.7.1 // indirect + github.com/edsrzf/mmap-go v1.0.0 // indirect + github.com/ethereum/go-ethereum v1.8.20 + github.com/fjl/memsize v0.0.0-20180929194037-2a09253e352a // indirect + github.com/fortytw2/leaktest v1.3.0 // indirect + github.com/go-logfmt/logfmt v0.4.0 // indirect + github.com/google/gofuzz v1.0.0 // indirect + github.com/google/uuid v1.1.1 // indirect + github.com/gorilla/mux v1.7.0 + github.com/gorilla/websocket v1.4.0 // indirect + github.com/hashicorp/golang-lru v0.5.1 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/huin/goupnp v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/jackpal/go-nat-pmp v1.0.1 // indirect + github.com/jmhodges/levigo v1.0.0 // indirect + github.com/karalabe/hid v0.0.0-20181128192157-d815e0c1a2e2 // indirect + github.com/magiconair/properties v1.8.0 // indirect + github.com/mattn/go-colorable v0.1.1 // indirect + github.com/mattn/go-isatty v0.0.6 // indirect + github.com/mitchellh/mapstructure v1.1.2 // indirect + github.com/pborman/uuid v0.0.0-20180906182336-adf5a7427709 // indirect + github.com/pelletier/go-toml v1.2.0 // indirect + github.com/prometheus/client_golang v0.9.2 // indirect + github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 // indirect + github.com/prometheus/common v0.2.0 // indirect + github.com/prometheus/procfs v0.0.0-20190227231451-bbced9601137 // indirect + github.com/rakyll/statik v0.1.6 // indirect + github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165 // indirect + github.com/rjeczalik/notify v0.9.2 // indirect + github.com/rs/cors v1.6.0 // indirect + github.com/spf13/afero v1.2.1 // indirect + github.com/spf13/cast v1.3.0 // indirect + github.com/spf13/cobra v0.0.3 + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.3 // indirect + github.com/spf13/viper v1.0.3 + github.com/stretchr/testify v1.2.2 + github.com/syndtr/goleveldb v0.0.0-20190226153722-4217c9f31f58 + github.com/tendermint/btcd v0.1.0 // indirect + github.com/tendermint/go-amino v0.14.1 // indirect + github.com/tendermint/iavl v0.12.0 // indirect + github.com/tendermint/tendermint v0.28.0 + github.com/zondax/hid v0.9.0 // indirect + github.com/zondax/ledger-cosmos-go v0.9.7 // indirect + github.com/zondax/ledger-go v0.8.0 // indirect + golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5 // indirect + golang.org/x/lint v0.0.0-20190409202823-959b441ac422 // indirect + golang.org/x/net v0.0.0-20190603091049-60506f45cf65 // indirect + golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed // indirect + golang.org/x/text v0.3.2 // indirect + golang.org/x/tools v0.0.0-20190603231351-8aaa1484dc10 // indirect + google.golang.org/genproto v0.0.0-20190227213309-4f5b463f9597 // indirect + gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect + gopkg.in/urfave/cli.v1 v1.20.0 // indirect + gopkg.in/yaml.v2 v2.2.2 // indirect +) + +replace golang.org/x/crypto => github.com/tendermint/crypto v0.0.0-20180820045704-3764759f34a5 + +go 1.13 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..cb94f3c --- /dev/null +++ b/go.sum @@ -0,0 +1,244 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/allegro/bigcache v1.2.0 h1:qDaE0QoF29wKBb3+pXFrJFy1ihe5OT9OiXhg1t85SxM= +github.com/allegro/bigcache v1.2.0/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= +github.com/aristanetworks/goarista v0.0.0-20190219163901-728bce664cf5 h1:L0TwgZQo7Mga9im6FvKEZGIvyLE/VG/HI5loz5LpvC0= +github.com/aristanetworks/goarista v0.0.0-20190219163901-728bce664cf5/go.mod h1:D/tb0zPVXnP7fmsLZjtdUhSsumbK/ij54UXjjVgMGxQ= +github.com/bartekn/go-bip39 v0.0.0-20171116152956-a05967ea095d h1:1aAija9gr0Hyv4KfQcRcwlmFIrhkDmIj2dz5bkg/s/8= +github.com/bartekn/go-bip39 v0.0.0-20171116152956-a05967ea095d/go.mod h1:icNx/6QdFblhsEjZehARqbNumymUT/ydwlLojFdv7Sk= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d h1:xG8Pj6Y6J760xwETNmMzmlt38QSwz0BLp1cZ09g27uw= +github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d/go.mod h1:d3C0AkH6BRcvO8T0UEPu53cnw4IbV63x1bEjildYhO0= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a h1:RQMUrEILyYJEoAT34XS/kLu40vC0+po/UfxrBBA4qZE= +github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/cespare/cp v1.1.1 h1:nCb6ZLdB7NRaqsm91JtQTAme2SKJzXVsdPIPkyJr1MU= +github.com/cespare/cp v1.1.1/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cosmos/cosmos-sdk v0.32.0 h1:l4HtecbD+0XyQqDh7J+b8wy9mkRoz9VUK5nBOxLMrpA= +github.com/cosmos/cosmos-sdk v0.32.0/go.mod h1:JrX/JpJunJQXBI5PEX2zELHMFzQr/159jDjIhesOh2c= +github.com/cosmos/go-bip39 v0.0.0-20180618194314-52158e4697b8 h1:Iwin12wRQtyZhH6FV3ykFcdGNlYEzoeR0jN8Vn+JWsI= +github.com/cosmos/go-bip39 v0.0.0-20180618194314-52158e4697b8/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9rTHJQ= +github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ= +github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/ethereum/go-ethereum v1.8.20 h1:Sr6DLbdc7Fl2IMDC0sjF2wO1jTO5nALFC1SoQnyAQEk= +github.com/ethereum/go-ethereum v1.8.20/go.mod h1:PwpWDrCLZrV+tfrhqqF6kPknbISMHaJv9Ln3kPCZLwY= +github.com/fjl/memsize v0.0.0-20180929194037-2a09253e352a h1:1znxn4+q2MrEdTk1eCk6KIV3muTYVclBIB6CTVR/zBc= +github.com/fjl/memsize v0.0.0-20180929194037-2a09253e352a/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-kit/kit v0.8.0 h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.7.0 h1:tOSd0UKHQd6urX6ApfOn4XdBMY6Sh1MfxV3kmaazO+U= +github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huin/goupnp v1.0.0 h1:wg75sLpL6DZqwHQN6E1Cfk6mtfzS45z8OV+ic+DtHRo= +github.com/huin/goupnp v1.0.0/go.mod h1:n9v9KO1tAxYH82qOn+UTIFQDmx5n1Zxd/ClZDMX7Bnc= +github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jackpal/go-nat-pmp v1.0.1 h1:i0LektDkO1QlrTm/cSuP+PyBCDnYvjPLGl4LdWEMiaA= +github.com/jackpal/go-nat-pmp v1.0.1/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= +github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/karalabe/hid v0.0.0-20181128192157-d815e0c1a2e2 h1:BkkpZxPVs3gIf+3Tejt8lWzuo2P29N1ChGUMEpuSJ8U= +github.com/karalabe/hid v0.0.0-20181128192157-d815e0c1a2e2/go.mod h1:YvbcH+3Wo6XPs9nkgTY3u19KXLauXW+J5nB7hEHuX0A= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.6 h1:SrwhHcpV4nWrMGdNcC2kXpMfcBVYGDuTArqyhocJgvA= +github.com/mattn/go-isatty v0.0.6/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pborman/uuid v0.0.0-20180906182336-adf5a7427709 h1:zNBQb37RGLmJybyMcs983HfUfpkw9OTFD9tbBfAViHE= +github.com/pborman/uuid v0.0.0-20180906182336-adf5a7427709/go.mod h1:VyrYX9gd7irzKovcSS6BIIEwPRkP2Wm2m9ufcdFSJ34= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740= +github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.2.0 h1:kUZDBDTdBVBYBj5Tmh2NZLlF60mfjA27rM34b+cVwNU= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190227231451-bbced9601137 h1:3l8oligPtjd4JuM+OZ+U8sjtwFGJs98cdWsqs6QZRWs= +github.com/prometheus/procfs v0.0.0-20190227231451-bbced9601137/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/rakyll/statik v0.1.6 h1:uICcfUXpgqtw2VopbIncslhAmE5hwc4g20TEyEENBNs= +github.com/rakyll/statik v0.1.6/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs= +github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165 h1:nkcn14uNmFEuGCb2mBZbBb24RdNRL08b/wb+xBOYpuk= +github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rjeczalik/notify v0.9.2 h1:MiTWrPj55mNDHEiIX5YUSKefw/+lCQVoAFmD6oQm5w8= +github.com/rjeczalik/notify v0.9.2/go.mod h1:aErll2f0sUX9PXZnVNyeiObbmTlk5jnMoCa4QEjJeqM= +github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI= +github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/spf13/afero v1.2.1 h1:qgMbHoJbPbw579P+1zVY+6n4nIFuIchaIjzZ/I/Yq8M= +github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.0.3 h1:z5LPUc2iz8VLT5Cw1UyrESG6FUUnOGecYGY08BLKSuc= +github.com/spf13/viper v1.0.3/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/syndtr/goleveldb v0.0.0-20190226153722-4217c9f31f58 h1:DLVQCtatLabge71DWz2Qn/GkZyYrqgI8EzirU7NqVLs= +github.com/syndtr/goleveldb v0.0.0-20190226153722-4217c9f31f58/go.mod h1:9OrXJhf154huy1nPWmuSrkgjPUtUNhA+Zmy+6AESzuA= +github.com/tendermint/btcd v0.1.0 h1:2bR8bGTlOLEiO9eoz81Upbs8LFSRF2MVT42WiyW88eU= +github.com/tendermint/btcd v0.1.0/go.mod h1:DC6/m53jtQzr/NFmMNEu0rxf18/ktVoVtMrnDD5pN+U= +github.com/tendermint/crypto v0.0.0-20180820045704-3764759f34a5 h1:u8i49c+BxloX3XQ55cvzFNXplizZP/q00i+IlttUjAU= +github.com/tendermint/crypto v0.0.0-20180820045704-3764759f34a5/go.mod h1:z4YtwM70uOnk8h0pjJYlj3zdYwi9l03By6iAIF5j/Pk= +github.com/tendermint/go-amino v0.14.1 h1:o2WudxNfdLNBwMyl2dqOJxiro5rfrEaU0Ugs6offJMk= +github.com/tendermint/go-amino v0.14.1/go.mod h1:i/UKE5Uocn+argJJBb12qTZsCDBcAYMbR92AaJVmKso= +github.com/tendermint/iavl v0.12.0 h1:xcaFAr+ycqCj7WN1RzL2EfcBioRDOHcU1oWcg83K028= +github.com/tendermint/iavl v0.12.0/go.mod h1:EoKMMv++tDOL5qKKVnoIqtVPshRrEPeJ0WsgDOLAauM= +github.com/tendermint/tendermint v0.28.0 h1:36tmP5hcN1TXzG0p6xIOlU9I4XKIYqjP89OCQ3YB470= +github.com/tendermint/tendermint v0.28.0/go.mod h1:ymcPyWblXCplCPQjbOYbrF1fWnpslATMVqiGgWbZrlc= +github.com/zondax/hid v0.9.0 h1:eiT3P6vNxAEVxXMw66eZUAAnU2zD33JBkfG/EnfAKl8= +github.com/zondax/hid v0.9.0/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= +github.com/zondax/ledger-cosmos-go v0.9.7 h1:ESMr8Oz14qePJ4oJd1Qqxgd7bxhySkrSTbem5eMhXng= +github.com/zondax/ledger-cosmos-go v0.9.7/go.mod h1:uhu/ldrtzhUH7RoWoTKK4MxBi/CXfyViVA0OCYTiHdM= +github.com/zondax/ledger-go v0.8.0 h1:QU2VXfwq2hSfgOMRDbJfPZLs8PyhdcuASIAyLSL5ewo= +github.com/zondax/ledger-go v0.8.0/go.mod h1:b2vIcu3u9gJoIx4kTWuXOgzGV7FPWeUktqRqVf6feG0= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422 h1:QzoH/1pFpZguR8NrRHLcO6jKqfv2zpuSqZLgdm7ZmjI= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd h1:HuTn7WObtcDo9uEEU7rEqL0jYthdXAmZ6PP+meazmaU= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190302025703-b6889370fb10 h1:xQJI9OEiErEQ++DoXOHqEpzsGMrAv2Q2jyCpi7DmfpQ= +golang.org/x/sys v0.0.0-20190302025703-b6889370fb10/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed h1:uPxWBzB3+mlnjy9W58qY1j/cjyFjutgw/Vhan2zLy/A= +golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190603231351-8aaa1484dc10 h1:9LyHDLeGJwq8p/x0xbF8aG63cLC6EDjlNbGYc7dacDc= +golang.org/x/tools v0.0.0-20190603231351-8aaa1484dc10/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190227213309-4f5b463f9597 h1:DudzI9CcA4MroJt1/8h46j45RjcTCAeVlpoe4mwGW84= +google.golang.org/genproto v0.0.0-20190227213309-4f5b463f9597/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/urfave/cli.v1 v1.20.0 h1:NdAVW6RYxDif9DhDHaAortIu956m2c0v+09AZBPTbE0= +gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/handlers/anteHandler.go b/handlers/anteHandler.go new file mode 100644 index 0000000..8e30a48 --- /dev/null +++ b/handlers/anteHandler.go @@ -0,0 +1,187 @@ +// Package handlers provides the ante handler and message handlers for verifying +// transactions and updating state. +package handlers + +import ( + "bytes" + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/msgs" + "github.com/FourthState/plasma-mvp-sidechain/plasma" + "github.com/FourthState/plasma-mvp-sidechain/store" + "github.com/FourthState/plasma-mvp-sidechain/utils" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "math/big" +) + +// the reason for an interface is to allow the connection object +// to be cooked when testing the ante handler +type plasmaConn interface { + GetDeposit(*big.Int, *big.Int) (plasma.Deposit, *big.Int, bool) + HasTxExited(*big.Int, plasma.Position) (bool, error) +} + +// NewAnteHandler returns an ante handler capable of handling include_deposit +// and spend_utxo Msgs. +func NewAnteHandler(ds store.DataStore, client plasmaConn) sdk.AnteHandler { + return func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, res sdk.Result, abort bool) { + msg := tx.GetMsgs()[0] // tx should only have one msg + switch mtype := msg.Type(); mtype { + case "include_deposit": + depositMsg := msg.(msgs.IncludeDepositMsg) + return includeDepositAnteHandler(ctx, ds, depositMsg, client) + case "spend_utxo": + spendMsg := msg.(msgs.SpendMsg) + return spendMsgAnteHandler(ctx, ds, spendMsg, client) + default: + return ctx, ErrInvalidTransaction("msg is not of type SpendMsg or IncludeDepositMsg").Result(), true + } + } +} + +// validates that the spend msg is valid given the current plasma sidechain state +func spendMsgAnteHandler(ctx sdk.Context, ds store.DataStore, spendMsg msgs.SpendMsg, client plasmaConn) (newCtx sdk.Context, res sdk.Result, abort bool) { + var totalInputAmt, totalOutputAmt *big.Int + totalInputAmt = big.NewInt(0) + totalOutputAmt = big.NewInt(0) + + // attempt to recover signers + signers := spendMsg.GetSigners() + if len(signers) == 0 { + return ctx, ErrInvalidTransaction("failed recovering signers").Result(), true + } + + if len(signers) != len(spendMsg.Inputs) { + return ctx, ErrInvalidSignature("number of signers does not equal number of signatures").Result(), true + } + + /* validate inputs */ + for i, signer := range signers { + amt, res := validateInput(ctx, ds, spendMsg.Inputs[i], common.BytesToAddress(signer), client) + if !res.IsOK() { + return ctx, res, true + } + + // first input must cover the fee + if i == 0 && amt.Cmp(spendMsg.Fee) < 0 { + return ctx, ErrInsufficientFee("first input has an insufficient amount to pay the fee").Result(), true + } + + totalInputAmt = totalInputAmt.Add(totalInputAmt, amt) + } + + // input0 + input1 (totalInputAmt) == output0 + output1 + Fee (totalOutputAmt) + for _, output := range spendMsg.Outputs { + totalOutputAmt = new(big.Int).Add(totalOutputAmt, output.Amount) + } + totalOutputAmt = new(big.Int).Add(totalOutputAmt, spendMsg.Fee) + + if totalInputAmt.Cmp(totalOutputAmt) != 0 { + return ctx, ErrInvalidTransaction("inputs do not equal Outputs (+ fee)").Result(), true + } + + return ctx, sdk.Result{}, false +} + +// validates the inputs against the output store and returns the amount of the respective input +func validateInput(ctx sdk.Context, ds store.DataStore, input plasma.Input, signer common.Address, client plasmaConn) (*big.Int, sdk.Result) { + var amt *big.Int + + // inputUTXO must be owned by the signer due to the prefix so we do not need to + // check the owner of the position + inputUTXO, ok := ds.GetOutput(ctx, input.Position) + if !ok { + return nil, ErrInvalidInput("input, %v, does not exist", input.Position).Result() + } + if !bytes.Equal(inputUTXO.Output.Owner[:], signer[:]) { + return nil, ErrSignatureVerificationFailure(fmt.Sprintf("transaction was not signed by correct address. Got: 0x%x. Expected: 0x%x", signer, inputUTXO.Output.Owner)).Result() + } + if inputUTXO.Spent { + return nil, ErrInvalidInput("input, %v, already spent", input.Position).Result() + } + exited, err := client.HasTxExited(ds.PlasmaBlockHeight(ctx), input.Position) + if err != nil { + return nil, ErrInvalidInput("failed to retrieve exit information on input, %v", input.Position).Result() + } else if exited { + return nil, ErrExitedInput("input, %v, utxo has exitted", input.Position).Result() + } + + // validate inputs/confirmation signatures if not a fee utxo or deposit utxo + if !input.IsDeposit() && !input.IsFee() { + tx, ok := ds.GetTxWithPosition(ctx, input.Position) + if !ok { + return nil, sdk.ErrInternal(fmt.Sprintf("failed to retrieve the transaction that input with position %s belongs to", input.Position)).Result() + } + + res := validateConfirmSignatures(ctx, ds, input, tx) + if !res.IsOK() { + return nil, res + } + + // check if the parent utxo has exited + for _, in := range tx.Transaction.Inputs { + exited, err = client.HasTxExited(ds.PlasmaBlockHeight(ctx), in.Position) + if err != nil { + return nil, ErrInvalidInput(fmt.Sprintf("failed to retrieve exit information on input, %v", in.Position)).Result() + } else if exited { + return nil, ErrExitedInput(fmt.Sprintf("a parent of the input has exited. Position: %v", in.Position)).Result() + } + } + } + + amt = inputUTXO.Output.Amount + + return amt, sdk.Result{} +} + +// validates the input's confirm signatures +func validateConfirmSignatures(ctx sdk.Context, ds store.DataStore, input plasma.Input, inputTx store.Transaction) sdk.Result { + if len(input.ConfirmSignatures) > 0 && len(inputTx.Transaction.Inputs) != len(input.ConfirmSignatures) { + return ErrInvalidTransaction("incorrect number of confirm signatures").Result() + } + confirmationHash := utils.ToEthSignedMessageHash(inputTx.ConfirmationHash[:])[:] + for i, in := range inputTx.Transaction.Inputs { + utxo, ok := ds.GetOutput(ctx, in.Position) + if !ok { + return ErrInvalidInput(fmt.Sprintf("failed to retrieve input utxo with position %s", in.Position)).Result() + } + + pubKey, _ := crypto.SigToPub(confirmationHash, input.ConfirmSignatures[i][:]) + signer := crypto.PubkeyToAddress(*pubKey) + if !bytes.Equal(signer[:], utxo.Output.Owner[:]) { + return ErrSignatureVerificationFailure(fmt.Sprintf("confirm signature not signed by the correct address. Got: %x. Expected: %x", signer, utxo.Output.Owner)).Result() + } + } + + return sdk.Result{} +} + +// validates the the include deposit msg corresponds to an existing deposit +func includeDepositAnteHandler(ctx sdk.Context, ds store.DataStore, msg msgs.IncludeDepositMsg, client plasmaConn) (newCtx sdk.Context, res sdk.Result, abort bool) { + if ds.HasDeposit(ctx, msg.DepositNonce) { + return ctx, ErrInvalidTransaction("deposit, %s, already exists in store", msg.DepositNonce.String()).Result(), true + } + deposit, threshold, ok := client.GetDeposit(ds.PlasmaBlockHeight(ctx), msg.DepositNonce) + if !ok && threshold == nil { + return ctx, ErrInvalidTransaction("deposit, %s, does not exist.", msg.DepositNonce.String()).Result(), true + } + if !ok { + return ctx, ErrInvalidTransaction("deposit, %s, has not finalized yet. Please wait at least %d blocks before resubmitting", msg.DepositNonce.String(), threshold.Int64()).Result(), true + } + if !bytes.Equal(deposit.Owner[:], msg.Owner[:]) { + return ctx, ErrInvalidTransaction("deposit, %s, does not equal the owner specified in the include-deposit Msg", msg.DepositNonce).Result(), true + } + + depositPosition := plasma.NewPosition(big.NewInt(0), 0, 0, msg.DepositNonce) + exited, err := client.HasTxExited(ds.PlasmaBlockHeight(ctx), depositPosition) + if err != nil { + return ctx, ErrInvalidTransaction("failed to retrieve deposit information for deposit, %s", msg.DepositNonce.String()).Result(), true + } else if exited { + return ctx, ErrInvalidTransaction("deposit, %s, has already exitted from rootchain", msg.DepositNonce.String()).Result(), true + } + if !bytes.Equal(msg.Owner.Bytes(), deposit.Owner.Bytes()) { + return ctx, ErrInvalidTransaction(fmt.Sprintf("msg has the wrong owner field for given deposit. Resubmit with correct deposit owner: %s", deposit.Owner.String())).Result(), true + } + return ctx, sdk.Result{}, false +} diff --git a/handlers/anteHandler_test.go b/handlers/anteHandler_test.go new file mode 100644 index 0000000..a9dcc78 --- /dev/null +++ b/handlers/anteHandler_test.go @@ -0,0 +1,544 @@ +package handlers + +import ( + "crypto/sha256" + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/msgs" + "github.com/FourthState/plasma-mvp-sidechain/plasma" + "github.com/FourthState/plasma-mvp-sidechain/store" + "github.com/FourthState/plasma-mvp-sidechain/utils" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" + "math/big" + "testing" +) + +var ( + privKey, _ = crypto.GenerateKey() + addr = crypto.PubkeyToAddress(privKey.PublicKey) + // bad keys to check against the deposit + badPrivKey, _ = crypto.GenerateKey() + badAddr = crypto.PubkeyToAddress(badPrivKey.PublicKey) +) + +type Tx struct { + Transaction plasma.Transaction + ConfirmationHash []byte + Spent []bool + SpenderTxs [][]byte + Position plasma.Position +} + +type Deposit struct { + Owner common.Address + Nonce *big.Int + EthBlockNum *big.Int + Amount *big.Int +} + +type Output struct { + Output plasma.Output + Position plasma.Position +} + +// cook the plasma connection +type conn struct{} + +// all deposits should be in an amount of 10eth owner by addr(defined above) +func (p conn) GetDeposit(tmBlock *big.Int, nonce *big.Int) (plasma.Deposit, *big.Int, bool) { + dep := plasma.Deposit{ + Owner: addr, + Amount: big.NewInt(10), + EthBlockNum: utils.Big0, + } + return dep, big.NewInt(-2), true +} +func (p conn) HasTxExited(tmBlock *big.Int, pos plasma.Position) (bool, error) { return false, nil } + +var _ plasmaConn = conn{} + +// cook up different plasma connection that will always claim Inputs exitted +type exitConn struct{} + +// all deposits should be in an amount of 10eth owner by addr(defined above) +func (p exitConn) GetDeposit(tmBlock *big.Int, nonce *big.Int) (plasma.Deposit, *big.Int, bool) { + dep := plasma.Deposit{ + Owner: addr, + Amount: big.NewInt(10), + EthBlockNum: utils.Big0, + } + return dep, big.NewInt(-2), true +} +func (p exitConn) HasTxExited(tmBlock *big.Int, pos plasma.Position) (bool, error) { + return true, nil +} + +func TestAnteChecks(t *testing.T) { + // setup + ctx, ds := setup() + handler := NewAnteHandler(ds, conn{}) + + feePosition := getPosition("(100.65535.0.0)") + // cook up some input deposits + inputs := []Deposit{ + {addr, big.NewInt(1), big.NewInt(100), big.NewInt(10)}, + {addr, big.NewInt(2), big.NewInt(101), big.NewInt(10)}, + {addr, big.NewInt(3), big.NewInt(102), big.NewInt(10)}, + } + fee := Output{ + Output: plasma.NewOutput(addr, big.NewInt(10)), + Position: feePosition, + } + + setupDeposits(ctx, ds, inputs...) + setupFees(ctx, ds, fee) + + ds.SpendFee(ctx, feePosition, []byte("spender hash")) + + type validationCase struct { + reason string + msgs.SpendMsg + } + + // cases to check for. cases with signature checks will get set subsequent to this step + // array of pointers because we are setting signatures after using `range` + // since InputsKeys not set, confirm signatures will simply be 0 bytes + invalidCases := []*validationCase{ + &validationCase{ + reason: "incorrect first signature", + SpendMsg: msgs.SpendMsg{ + Transaction: plasma.Transaction{ + Inputs: []plasma.Input{plasma.NewInput(plasma.NewPosition(nil, 0, 0, utils.Big1), [65]byte{}, nil)}, + Outputs: []plasma.Output{plasma.NewOutput(addr, big.NewInt(10))}, + Fee: utils.Big0, + }, + }, + }, + &validationCase{ + reason: "incorrect second signature", + SpendMsg: msgs.SpendMsg{ + Transaction: plasma.Transaction{ + Inputs: []plasma.Input{plasma.NewInput(plasma.NewPosition(nil, 0, 0, utils.Big1), [65]byte{}, nil), plasma.NewInput(feePosition, [65]byte{}, nil)}, + Outputs: []plasma.Output{plasma.NewOutput(addr, big.NewInt(20))}, + Fee: utils.Big0, + }, + }, + }, + &validationCase{ + reason: "no signatures", + SpendMsg: msgs.SpendMsg{ + Transaction: plasma.Transaction{ + Inputs: []plasma.Input{plasma.NewInput(plasma.NewPosition(nil, 0, 0, utils.Big1), [65]byte{}, nil), plasma.NewInput(plasma.NewPosition(nil, 0, 0, utils.Big2), [65]byte{}, nil)}, + Outputs: []plasma.Output{plasma.NewOutput(addr, big.NewInt(20))}, + Fee: utils.Big0, + }, + }, + }, + &validationCase{ + reason: "invalid fee", + SpendMsg: msgs.SpendMsg{ + Transaction: plasma.Transaction{ + Inputs: []plasma.Input{plasma.NewInput(plasma.NewPosition(nil, 0, 0, utils.Big1), [65]byte{}, nil), plasma.NewInput(plasma.NewPosition(nil, 0, 0, utils.Big2), [65]byte{}, nil)}, + Outputs: []plasma.Output{plasma.NewOutput(addr, big.NewInt(20))}, + Fee: big.NewInt(20), + }, + }, + }, + &validationCase{ + reason: "unbalanced transaction", + SpendMsg: msgs.SpendMsg{ + Transaction: plasma.Transaction{ + Inputs: []plasma.Input{plasma.NewInput(plasma.NewPosition(nil, 0, 0, utils.Big1), [65]byte{}, nil), plasma.NewInput(plasma.NewPosition(nil, 0, 0, utils.Big2), [65]byte{}, nil)}, + Outputs: []plasma.Output{plasma.NewOutput(addr, big.NewInt(10)), plasma.NewOutput(addr, big.NewInt(10))}, + Fee: utils.Big1, + }, + }, + }, + &validationCase{ + reason: "Inputs deposit utxo does not exist", + SpendMsg: msgs.SpendMsg{ + Transaction: plasma.Transaction{ + Inputs: []plasma.Input{plasma.NewInput(plasma.NewPosition(nil, 0, 0, utils.Big1), [65]byte{}, nil), plasma.NewInput(plasma.NewPosition(nil, 0, 0, big.NewInt(4)), [65]byte{}, nil)}, + Outputs: []plasma.Output{plasma.NewOutput(addr, big.NewInt(10)), plasma.NewOutput(addr, big.NewInt(10))}, + Fee: utils.Big0, + }, + }, + }, + &validationCase{ + reason: "Inputs transaction utxo does not exist", + SpendMsg: msgs.SpendMsg{ + Transaction: plasma.Transaction{ + Inputs: []plasma.Input{plasma.NewInput(plasma.NewPosition(nil, 0, 0, utils.Big1), [65]byte{}, nil), plasma.NewInput(plasma.NewPosition(utils.Big1, 3, 1, nil), [65]byte{}, nil)}, + Outputs: []plasma.Output{plasma.NewOutput(addr, big.NewInt(10)), plasma.NewOutput(addr, big.NewInt(10))}, + Fee: utils.Big0, + }, + }, + }, + &validationCase{ + reason: "Inputs utxo already spent", + SpendMsg: msgs.SpendMsg{ + Transaction: plasma.Transaction{ + Inputs: []plasma.Input{plasma.NewInput(plasma.NewPosition(nil, 0, 0, utils.Big1), [65]byte{}, nil), plasma.NewInput(feePosition, [65]byte{}, nil)}, + Outputs: []plasma.Output{plasma.NewOutput(addr, big.NewInt(10)), plasma.NewOutput(addr, big.NewInt(10))}, + Fee: utils.Big0, + }, + }, + }, + } + + // set invalid first signature + txHash := utils.ToEthSignedMessageHash(invalidCases[0].SpendMsg.TxHash()) + sig, _ := crypto.Sign(txHash, badPrivKey) + copy(invalidCases[0].SpendMsg.Inputs[0].Signature[:], sig) + + // set invalid second signature but correct first signature + txHash = utils.ToEthSignedMessageHash(invalidCases[1].SpendMsg.TxHash()) + sig, _ = crypto.Sign(txHash, badPrivKey) + copy(invalidCases[1].SpendMsg.Inputs[1].Signature[:], sig) + sig, _ = crypto.Sign(txHash, privKey) + copy(invalidCases[1].SpendMsg.Inputs[0].Signature[:], sig) + + // set valid signatures for remaining cases + for _, txCase := range invalidCases[3:] { + txHash = utils.ToEthSignedMessageHash(txCase.SpendMsg.TxHash()) + sig, _ = crypto.Sign(txHash, privKey) + copy(txCase.SpendMsg.Inputs[0].Signature[:], sig[:]) + copy(txCase.SpendMsg.Inputs[1].Signature[:], sig[:]) + } + + for _, txCase := range invalidCases { + _, res, abort := handler(ctx, txCase.SpendMsg, false) + require.False(t, res.IsOK(), txCase.reason) + require.True(t, abort, txCase.reason) + } +} + +func TestAnteExitedInputs(t *testing.T) { + // setup + ctx, ds := setup() + handler := NewAnteHandler(ds, exitConn{}) + + // place inputs in store + inputs := Tx{ + Transaction: plasma.Transaction{[]plasma.Input{plasma.NewInput(getPosition("10.0.0.0"), [65]byte{}, nil)}, []plasma.Output{plasma.NewOutput(addr, big.NewInt(10))}, big.NewInt(0)}, + ConfirmationHash: []byte("confirmation hash"), + Spent: []bool{false}, + SpenderTxs: [][]byte{}, + Position: getPosition("(1.0.0.0)"), + } + setupTxs(ctx, ds, inputs) + + // create msg + spendMsg := msgs.SpendMsg{ + Transaction: plasma.Transaction{ + Inputs: []plasma.Input{plasma.NewInput(plasma.NewPosition(utils.Big1, 0, 0, nil), [65]byte{}, nil), plasma.NewInput(plasma.NewPosition(nil, 0, 0, nil), [65]byte{}, nil)}, + Outputs: []plasma.Output{plasma.NewOutput(addr, big.NewInt(10)), plasma.NewOutput(addr, big.NewInt(9))}, + Fee: utils.Big1, + }, + } + + // set signature + txHash := utils.ToEthSignedMessageHash(spendMsg.TxHash()) + sig, _ := crypto.Sign(txHash, privKey) + copy(spendMsg.Inputs[0].Signature[:], sig[:]) + + _, res, abort := handler(ctx, spendMsg, false) + require.False(t, res.IsOK(), "Result OK even though inputs exitted") + require.True(t, abort, "Did not abort tx even though inputs exitted") + + // TODO: test case where grandparent exitted but parent didn't +} + +func TestAnteInvalidConfirmSig(t *testing.T) { + // setup + ctx, ds := setup() + handler := NewAnteHandler(ds, conn{}) + + // place inputs in store + inputs := []Deposit{ + {addr, utils.Big1, big.NewInt(50), big.NewInt(10)}, + {addr, utils.Big2, big.NewInt(55), big.NewInt(10)}, + } + setupDeposits(ctx, ds, inputs...) + + parentTx := msgs.SpendMsg{ + Transaction: plasma.Transaction{ + Inputs: []plasma.Input{plasma.NewInput(plasma.NewPosition(nil, 0, 0, utils.Big2), [65]byte{}, nil)}, + Outputs: []plasma.Output{plasma.NewOutput(addr, big.NewInt(10))}, + Fee: utils.Big0, + }, + } + + // set regular transaction output in store + // parent input was 0.0.0.2 + // must create confirmation hash + // also need confirm sig of parent in order to spend + confBytes := sha256.Sum256(append(parentTx.MerkleHash(), ctx.BlockHeader().DataHash...)) + confHash := utils.ToEthSignedMessageHash(confBytes[:]) + badConfSig, _ := crypto.Sign(confHash, badPrivKey) + + tx := store.Transaction{ + Transaction: parentTx.Transaction, + ConfirmationHash: confHash[:], + Spent: []bool{false}, + SpenderTxs: make([][]byte, len(parentTx.Transaction.Outputs)), + Position: getPosition("(1.0.0.0)"), + } + ds.StoreTx(ctx, tx) + + // store confirm sig into correct format + var invalidConfirmSig [65]byte + copy(invalidConfirmSig[:], badConfSig) + + // create msg + spendMsg := msgs.SpendMsg{ + Transaction: plasma.Transaction{ + Inputs: []plasma.Input{plasma.NewInput(plasma.NewPosition(utils.Big1, 0, 0, nil), [65]byte{}, [][65]byte{invalidConfirmSig}), plasma.NewInput(plasma.NewPosition(nil, 0, 0, utils.Big1), [65]byte{}, nil)}, + Outputs: []plasma.Output{plasma.NewOutput(addr, big.NewInt(10)), plasma.NewOutput(addr, big.NewInt(9))}, + Fee: utils.Big1, + }, + } + + // set signature + txHash := utils.ToEthSignedMessageHash(spendMsg.TxHash()) + sig, _ := crypto.Sign(txHash, privKey) + copy(spendMsg.Inputs[0].Signature[:], sig[:]) + copy(spendMsg.Inputs[1].Signature[:], sig[:]) + + _, res, abort := handler(ctx, spendMsg, false) + require.False(t, res.IsOK(), "tx OK with invalid parent confirm sig") + require.True(t, abort, "tx with invalid parent confirm sig did not abort") + +} + +func TestAnteValidTx(t *testing.T) { + // setup + ctx, ds := setup() + handler := NewAnteHandler(ds, conn{}) + + // place inputs in store + inputs := []Deposit{ + {addr, utils.Big1, big.NewInt(1000), big.NewInt(10)}, + {addr, utils.Big2, big.NewInt(1200), big.NewInt(10)}, + } + setupDeposits(ctx, ds, inputs...) + + parentTx := msgs.SpendMsg{ + Transaction: plasma.Transaction{ + Inputs: []plasma.Input{plasma.NewInput(plasma.NewPosition(nil, 0, 0, utils.Big2), [65]byte{}, nil)}, + Outputs: []plasma.Output{plasma.NewOutput(addr, big.NewInt(10))}, + Fee: utils.Big0, + }, + } + + // set regular transaction utxo in store + // parent input was 0.0.0.2 + // must create input key and confirmation hash + // also need confirm sig of parent in order to spend + confBytes := sha256.Sum256(append(parentTx.MerkleHash(), ctx.BlockHeader().DataHash...)) + confHash := utils.ToEthSignedMessageHash(confBytes[:]) + confSig, _ := crypto.Sign(confHash, privKey) + + tx := store.Transaction{ + Transaction: parentTx.Transaction, + ConfirmationHash: confBytes[:], + Spent: []bool{false}, + SpenderTxs: make([][]byte, len(parentTx.Transaction.Outputs)), + Position: getPosition("(1.0.0.0)"), + } + ds.StoreTx(ctx, tx) + ds.StoreOutputs(ctx, tx) + ds.SpendDeposit(ctx, big.NewInt(2), tx.Transaction.TxHash()) + + output, check := ds.GetOutput(ctx, getPosition("(1.0.0.0)")) + if !check { + fmt.Println("WHy?") + } + fmt.Printf("Output: %v\n", output) + + // store confirm sig into correct format + var confirmSig [65]byte + copy(confirmSig[:], confSig) + + // create msg + spendMsg := msgs.SpendMsg{ + Transaction: plasma.Transaction{ + Inputs: []plasma.Input{plasma.NewInput(plasma.NewPosition(utils.Big1, 0, 0, nil), [65]byte{}, [][65]byte{confirmSig}), plasma.NewInput(plasma.NewPosition(nil, 0, 0, utils.Big1), [65]byte{}, nil)}, + Outputs: []plasma.Output{plasma.NewOutput(addr, big.NewInt(10)), plasma.NewOutput(addr, big.NewInt(9))}, + Fee: utils.Big1, + }, + } + + // set signature + txHash := utils.ToEthSignedMessageHash(spendMsg.TxHash()) + sig, _ := crypto.Sign(txHash, privKey) + copy(spendMsg.Inputs[0].Signature[:], sig[:]) + copy(spendMsg.Inputs[1].Signature[:], sig[:]) + + _, res, abort := handler(ctx, spendMsg, false) + require.True(t, res.IsOK(), fmt.Sprintf("Valid tx does not have OK result: %s", res.Log)) + require.False(t, abort, "Valid tx aborted") +} + +/*=====================================================================================================================================*/ +// Deposit Antehandler tests + +func TestAnteDeposit(t *testing.T) { + // setup + ctx, ds := setup() + handler := NewAnteHandler(ds, conn{}) + + // place input in store + inputs := []Deposit{ + {addr, utils.Big1, big.NewInt(150), big.NewInt(10)}, + {addr, utils.Big2, big.NewInt(200), big.NewInt(10)}, + } + setupDeposits(ctx, ds, inputs...) + + msg := msgs.IncludeDepositMsg{ + DepositNonce: big.NewInt(3), + Owner: addr, + } + + _, res, abort := handler(ctx, msg, false) + + require.True(t, res.IsOK(), "Valid IncludeDepositMsg has erroneous result") + require.False(t, abort, "Valid IncludeDepositMsg aborted") + + // try to include Deposit that already exists + msg.DepositNonce = big.NewInt(1) + + _, res, abort = handler(ctx, msg, false) + + require.False(t, res.IsOK(), "Allowed to re-include deposit") + require.True(t, abort, "Redundant IncludeDepositMsg did not abort") +} + +type unfinalConn struct{} + +func (u unfinalConn) GetDeposit(tmBlock *big.Int, nonce *big.Int) (plasma.Deposit, *big.Int, bool) { + dep := plasma.Deposit{ + Owner: addr, + Amount: big.NewInt(10), + EthBlockNum: big.NewInt(50), + } + return dep, big.NewInt(10), false +} + +func (u unfinalConn) HasTxExited(tmBlock *big.Int, pos plasma.Position) (bool, error) { + return false, nil +} + +type dneConn struct{} + +func (d dneConn) GetDeposit(tmBlock *big.Int, nonce *big.Int) (plasma.Deposit, *big.Int, bool) { + return plasma.Deposit{}, nil, false +} + +func (d dneConn) HasTxExited(tmBlock *big.Int, pos plasma.Position) (bool, error) { + return false, nil +} + +func TestAnteDepositUnfinal(t *testing.T) { + // setup + ctx, ds := setup() + // connection always returns unfinalized deposits + handler := NewAnteHandler(ds, unfinalConn{}) + + msg := msgs.IncludeDepositMsg{ + DepositNonce: big.NewInt(3), + Owner: addr, + } + + _, res, abort := handler(ctx, msg, false) + + require.False(t, res.IsOK(), "Unfinalized deposit inclusion did not error") + require.True(t, abort, "Unfinalized deposit inclusion did not abort") + +} + +func TestAnteDepositExitted(t *testing.T) { + // setup + ctx, ds := setup() + // connection always returns exitted deposits + handler := NewAnteHandler(ds, exitConn{}) + + msg := msgs.IncludeDepositMsg{ + DepositNonce: big.NewInt(3), + Owner: addr, + } + + _, res, abort := handler(ctx, msg, false) + + require.False(t, res.IsOK(), "Exitted deposit inclusion did not error") + require.True(t, abort, "Exitted deposit inclusion did not abort") + +} + +func TestAnteDepositDNE(t *testing.T) { + // setup + ctx, ds := setup() + // connection always returns exitted deposits + handler := NewAnteHandler(ds, dneConn{}) + + msg := msgs.IncludeDepositMsg{ + DepositNonce: big.NewInt(3), + Owner: addr, + } + + _, res, abort := handler(ctx, msg, false) + + require.False(t, res.IsOK(), "Nonexistent deposit inclusion did not error") + require.True(t, abort, "Nonexistent deposit inclusion did not abort") + +} + +func setupFees(ctx sdk.Context, ds store.DataStore, inputs ...Output) { + for _, output := range inputs { + ds.StoreFee(ctx, output.Position.BlockNum, output.Output) + } +} + +func TestAnteDepositWrongOwner(t *testing.T) { + // setup + ctx, ds := setup() + // connection always returns valid deposits + handler := NewAnteHandler(ds, conn{}) + + // Try to include with wrong owner + msg := msgs.IncludeDepositMsg{ + DepositNonce: big.NewInt(3), + Owner: badAddr, + } + + _, res, abort := handler(ctx, msg, false) + + require.False(t, res.IsOK(), "Wrong Owner deposit inclusion did not error") + require.True(t, abort, "Wrong owner deposit inclusion did not abort") +} + +func setupDeposits(ctx sdk.Context, ds store.DataStore, inputs ...Deposit) { + for _, i := range inputs { + deposit := plasma.Deposit{ + Owner: i.Owner, + Amount: i.Amount, + EthBlockNum: i.EthBlockNum, + } + ds.StoreDeposit(ctx, i.Nonce, deposit) + } +} + +func setupTxs(ctx sdk.Context, ds store.DataStore, inputs ...Tx) { + for _, i := range inputs { + tx := store.Transaction{ + Transaction: i.Transaction, + ConfirmationHash: i.ConfirmationHash, + Spent: i.Spent, + SpenderTxs: i.SpenderTxs, + Position: i.Position, + } + ds.StoreTx(ctx, tx) + } +} diff --git a/handlers/common_test.go b/handlers/common_test.go new file mode 100644 index 0000000..e025700 --- /dev/null +++ b/handlers/common_test.go @@ -0,0 +1,33 @@ +package handlers + +import ( + "github.com/FourthState/plasma-mvp-sidechain/plasma" + "github.com/FourthState/plasma-mvp-sidechain/store" + cosmosStore "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/db" + "github.com/tendermint/tendermint/libs/log" +) + +/* This file contains helper functions for testing */ + +func setup() (sdk.Context, store.DataStore) { + db := db.NewMemDB() + ms := cosmosStore.NewCommitMultiStore(db) + + ctx := sdk.NewContext(ms, abci.Header{}, false, log.NewNopLogger()) + + dataStoreKey := sdk.NewKVStoreKey("data") + + ms.MountStoreWithDB(dataStoreKey, sdk.StoreTypeIAVL, db) + ms.LoadLatestVersion() + + dataStore := store.NewDataStore(dataStoreKey) + return ctx, dataStore +} + +func getPosition(posStr string) plasma.Position { + pos, _ := plasma.FromPositionString(posStr) + return pos +} diff --git a/handlers/depositHandler.go b/handlers/depositHandler.go new file mode 100644 index 0000000..eaa917a --- /dev/null +++ b/handlers/depositHandler.go @@ -0,0 +1,26 @@ +package handlers + +import ( + "github.com/FourthState/plasma-mvp-sidechain/msgs" + "github.com/FourthState/plasma-mvp-sidechain/store" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// NewDepositHandler adds the rootchain deposit to the data store +func NewDepositHandler(ds store.DataStore, nextTxIndex NextTxIndex, client plasmaConn) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + depositMsg, ok := msg.(msgs.IncludeDepositMsg) + if !ok { + panic("Msg does not implement IncludeDepositMsg") + } + + // Increment txIndex so that it doesn't collide with SpendMsg + nextTxIndex() + + deposit, _, _ := client.GetDeposit(ds.PlasmaBlockHeight(ctx), depositMsg.DepositNonce) + + ds.StoreDeposit(ctx, depositMsg.DepositNonce, deposit) + + return sdk.Result{} + } +} diff --git a/handlers/depositHandler_test.go b/handlers/depositHandler_test.go new file mode 100644 index 0000000..bcb40fb --- /dev/null +++ b/handlers/depositHandler_test.go @@ -0,0 +1,30 @@ +package handlers + +import ( + "github.com/FourthState/plasma-mvp-sidechain/msgs" + "github.com/stretchr/testify/require" + "math/big" + "testing" +) + +func TestIncludeDeposit(t *testing.T) { + // blockStore is at next block height 1 + ctx, ds := setup() + + // Give deposit a cooked connection that will always provide deposit with given position + depositHandler := NewDepositHandler(ds, nextTxIndex, conn{}) + + // create a msg that spends the first input and creates two outputs + msg := msgs.IncludeDepositMsg{ + DepositNonce: big.NewInt(5), + Owner: addr, + } + + depositHandler(ctx, msg) + deposit, ok := ds.GetDeposit(ctx, big.NewInt(5)) + + require.True(t, ok, "deposit does not exist in store") + require.Equal(t, addr, deposit.Deposit.Owner, "deposit has wrong owner") + require.Equal(t, big.NewInt(10), deposit.Deposit.Amount, "deposit has wrong amount") + require.False(t, deposit.Spent, "Deposit is incorrectly marked as spent") +} diff --git a/handlers/errors.go b/handlers/errors.go new file mode 100644 index 0000000..ae518c5 --- /dev/null +++ b/handlers/errors.go @@ -0,0 +1,48 @@ +package handlers + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Error codes for handlers +const ( + DefaultCodespace sdk.CodespaceType = "handlers" + + CodeInsufficientFee sdk.CodeType = 1 + CodeExitedInput sdk.CodeType = 2 + CodeSignatureVerificationFailure sdk.CodeType = 3 + CodeInvalidTransaction sdk.CodeType = 4 + CodeInvalidSignature sdk.CodeType = 5 + CodeInvalidInput sdk.CodeType = 6 +) + +// ErrInsufficientFee error for an insufficient fee +func ErrInsufficientFee(msg string, args ...interface{}) sdk.Error { + return sdk.NewError(DefaultCodespace, CodeInsufficientFee, msg, args) +} + +// ErrExitedInput error for if the input has already exited +func ErrExitedInput(msg string, args ...interface{}) sdk.Error { + return sdk.NewError(DefaultCodespace, CodeExitedInput, msg, args) +} + +// ErrSignatureVerficiationFailure error for signature verifcation failing to +// complete +func ErrSignatureVerificationFailure(msg string, args ...interface{}) sdk.Error { + return sdk.NewError(DefaultCodespace, CodeSignatureVerificationFailure, msg, args) +} + +// ErrInvalidTransaction error for an invalid transaction +func ErrInvalidTransaction(msg string, args ...interface{}) sdk.Error { + return sdk.NewError(DefaultCodespace, CodeInvalidTransaction, msg, args) +} + +// ErrInvalidSignature error for an incorrect signature +func ErrInvalidSignature(msg string, args ...interface{}) sdk.Error { + return sdk.NewError(DefaultCodespace, CodeInvalidSignature, msg, args) +} + +// ErrInvalidInput +func ErrInvalidInput(msg string, args ...interface{}) sdk.Error { + return sdk.NewError(DefaultCodespace, CodeInvalidInput, msg, args) +} diff --git a/handlers/spendMsgHandler.go b/handlers/spendMsgHandler.go new file mode 100644 index 0000000..6edbcae --- /dev/null +++ b/handlers/spendMsgHandler.go @@ -0,0 +1,67 @@ +package handlers + +import ( + "crypto/sha256" + "github.com/FourthState/plasma-mvp-sidechain/msgs" + "github.com/FourthState/plasma-mvp-sidechain/plasma" + "github.com/FourthState/plasma-mvp-sidechain/store" + sdk "github.com/cosmos/cosmos-sdk/types" + "math/big" +) + +// returns the next tx index in the current block +type NextTxIndex func() uint16 + +// FeeUpdater updates the aggregate fee amount in a block +type FeeUpdater func(amt *big.Int) sdk.Error + +// NewSpendHandler sets the inputs of a spend msg to spent and creates new +// outputs that are added to the data store. +func NewSpendHandler(ds store.DataStore, nextTxIndex NextTxIndex, feeUpdater FeeUpdater) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + spendMsg, ok := msg.(msgs.SpendMsg) + if !ok { + panic("Msg does not implement SpendMsg") + } + + txIndex := nextTxIndex() + nextBlockHeight := ds.NextPlasmaBlockHeight(ctx) + + // construct the confirmation hash + merkleHash := spendMsg.MerkleHash() + header := ctx.BlockHeader().DataHash + confirmationHash := sha256.Sum256(append(merkleHash, header...)) + + /* Spend Inputs */ + for _, input := range spendMsg.Inputs { + var res sdk.Result + if input.Position.IsDeposit() { + res = ds.SpendDeposit(ctx, input.Position.DepositNonce, spendMsg.TxHash()) + } else { + res = ds.SpendOutput(ctx, input.Position, spendMsg.TxHash()) + } + + if !res.IsOK() { + return res + } + } + + /* Store Transaction and create new outputs */ + tx := store.Transaction{ + Transaction: spendMsg.Transaction, + Spent: make([]bool, len(spendMsg.Outputs)), + SpenderTxs: make([][]byte, len(spendMsg.Outputs)), + ConfirmationHash: confirmationHash[:], + Position: plasma.NewPosition(nextBlockHeight, txIndex, 0, big.NewInt(0)), + } + ds.StoreTx(ctx, tx) + ds.StoreOutputs(ctx, tx) + + // update the aggregate fee amount for the block + if err := feeUpdater(spendMsg.Fee); err != nil { + return sdk.ErrInternal("error updating the aggregate fee").Result() + } + + return sdk.Result{} + } +} diff --git a/handlers/spendMsgHandler_test.go b/handlers/spendMsgHandler_test.go new file mode 100644 index 0000000..9f93ed9 --- /dev/null +++ b/handlers/spendMsgHandler_test.go @@ -0,0 +1,71 @@ +package handlers + +import ( + "github.com/FourthState/plasma-mvp-sidechain/msgs" + "github.com/FourthState/plasma-mvp-sidechain/plasma" + "github.com/FourthState/plasma-mvp-sidechain/utils" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" + "math/big" + "testing" +) + +var nextTxIndex = func() uint16 { return 0 } +var feeUpdater = func(num *big.Int) sdk.Error { return nil } + +func TestSpend(t *testing.T) { + // blockStore is at next block height 1 + ctx, ds := setup() + privKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privKey.PublicKey) + + spendHandler := NewSpendHandler(ds, nextTxIndex, feeUpdater) + + // store inputs to be spent + pos := plasma.NewPosition(utils.Big0, 0, 0, utils.Big1) + deposit := plasma.Deposit{ + Owner: addr, + Amount: big.NewInt(20), + EthBlockNum: big.NewInt(1000), + } + ds.StoreDeposit(ctx, pos.DepositNonce, deposit) + + // create a msg that spends the first input and creates two outputs + newOwner := common.HexToAddress("1") + msg := msgs.SpendMsg{ + Transaction: plasma.Transaction{ + Inputs: []plasma.Input{plasma.NewInput(pos, [65]byte{}, nil)}, + Outputs: []plasma.Output{plasma.NewOutput(newOwner, big.NewInt(10)), plasma.NewOutput(newOwner, big.NewInt(10))}, + Fee: utils.Big0, + }, + } + // fill in the signature + sig, err := crypto.Sign(utils.ToEthSignedMessageHash(msg.TxHash()), privKey) + copy(msg.Inputs[0].Signature[:], sig) + err = msg.ValidateBasic() + require.NoError(t, err) + + res := spendHandler(ctx, msg) + require.Truef(t, res.IsOK(), "failed to handle spend: %s", res) + + // check that the output store reflects the spend + dep, ok := ds.GetDeposit(ctx, pos.DepositNonce) + require.True(t, ok, "input to the msg does not exist in the store") + require.True(t, dep.Spent, "input not marked as spent after the handler") + + // new first output was created at BlockHeight 1 and txIndex 0 and outputIndex 0 + pos = plasma.NewPosition(utils.Big1, 0, 0, nil) + utxo, ok := ds.GetOutput(ctx, pos) + require.True(t, ok, "new output was not created") + require.False(t, utxo.Spent, "new output marked as spent") + require.Equal(t, utxo.Output.Amount, big.NewInt(10), "new output has incorrect amount") + + // new second output was created at BlockHeight 0 and txIndex 0 and outputIndex 1 + pos = plasma.NewPosition(utils.Big1, 0, 1, nil) + utxo, ok = ds.GetOutput(ctx, pos) + require.True(t, ok, "new output was not created") + require.False(t, utxo.Spent, "new output marked as spent") + require.Equal(t, utxo.Output.Amount, big.NewInt(10), "new output has incorrect amount") +} diff --git a/msgs/depositMsg.go b/msgs/depositMsg.go new file mode 100644 index 0000000..ff437d5 --- /dev/null +++ b/msgs/depositMsg.go @@ -0,0 +1,57 @@ +package msgs + +import ( + "github.com/FourthState/plasma-mvp-sidechain/utils" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + "math/big" +) + +const ( + // Msg route + IncludeDepositMsgRoute = "include" +) + +var _ sdk.Tx = IncludeDepositMsg{} + +// IncludeDepositMsg implements sdk.Msg and sdk.Tx interfaces since +// no authentication is happening. +type IncludeDepositMsg struct { + DepositNonce *big.Int + Owner common.Address + ReplayNonce uint64 // to get around tx cache issues when resubmitting +} + +// Type returns the message type. +func (msg IncludeDepositMsg) Type() string { return "include_deposit" } + +// Route returns the route for this message. +func (msg IncludeDepositMsg) Route() string { return IncludeDepositMsgRoute } + +// GetSigners returns nil since no signers necessary on IncludeDepositMsg. +func (msg IncludeDepositMsg) GetSigners() []sdk.AccAddress { + return nil +} + +// GetSignBytes returns nil since no signature validation required for +// IncludeDepositMsg. +func (msg IncludeDepositMsg) GetSignBytes() []byte { + return nil +} + +// ValidateBasic asserts that the DepositNonce is positive and that the +// Owner field is not the zero address. +func (msg IncludeDepositMsg) ValidateBasic() sdk.Error { + if msg.DepositNonce.Sign() != 1 { + return ErrInvalidIncludeDepositMsg(DefaultCodespace, "DepositNonce must be greater than 0") + } + if utils.IsZeroAddress(msg.Owner) { + return ErrInvalidIncludeDepositMsg(DefaultCodespace, "Owner must have non-zero address") + } + return nil +} + +// GetMsgs implements the sdk.Tx interface +func (msg IncludeDepositMsg) GetMsgs() []sdk.Msg { + return []sdk.Msg{msg} +} diff --git a/msgs/depositMsg_test.go b/msgs/depositMsg_test.go new file mode 100644 index 0000000..dad2a55 --- /dev/null +++ b/msgs/depositMsg_test.go @@ -0,0 +1,55 @@ +package msgs + +import ( + "fmt" + ethcmn "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/rlp" + "github.com/stretchr/testify/require" + "math/big" + "reflect" + "testing" +) + +var ( + privKey, _ = crypto.GenerateKey() + addr = crypto.PubkeyToAddress(privKey.PublicKey) +) + +func TestDepositMsgValidate(t *testing.T) { + type depositCase struct { + Nonce *big.Int + Address ethcmn.Address + } + + invalidCases := []depositCase{ + {big.NewInt(-1), addr}, + {big.NewInt(0), addr}, + {big.NewInt(1), ethcmn.Address{}}, + } + + for i, c := range invalidCases { + depositMsg := IncludeDepositMsg{ + DepositNonce: c.Nonce, + Owner: c.Address, + } + require.NotNil(t, depositMsg.ValidateBasic(), fmt.Sprintf("Testcase %d failed", i)) + } +} + +func TestDepositMsgSerialization(t *testing.T) { + msg := IncludeDepositMsg{ + DepositNonce: big.NewInt(3), + Owner: addr, + ReplayNonce: 2, + } + + bytes, err := rlp.EncodeToBytes(&msg) + require.NoError(t, err, "serialization error") + + tx, err := TxDecoder(bytes) + + require.NoError(t, err, "deserialization error") + + require.True(t, reflect.DeepEqual(msg, tx), "serialized and deserialized msgs not equal") +} diff --git a/msgs/errors.go b/msgs/errors.go new file mode 100644 index 0000000..99bebe7 --- /dev/null +++ b/msgs/errors.go @@ -0,0 +1,23 @@ +package msgs + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Codes for msgs errors +const ( + DefaultCodespace sdk.CodespaceType = "msgs" + + CodeInvalidSpendMsg sdk.CodeType = 1 + CodeInvalidIncludeDepositMsg sdk.CodeType = 2 +) + +// ErrInvalidSpendMsg error for an invalid spend msg +func ErrInvalidSpendMsg(codespace sdk.CodespaceType, msg string, args ...interface{}) sdk.Error { + return sdk.NewError(codespace, CodeInvalidSpendMsg, msg, args...) +} + +// ErrInvalidIncludeDepositMsg error for an invalid include deposit msg +func ErrInvalidIncludeDepositMsg(codespace sdk.CodespaceType, msg string, args ...interface{}) sdk.Error { + return sdk.NewError(codespace, CodeInvalidIncludeDepositMsg, msg, args...) +} diff --git a/msgs/spendMsg.go b/msgs/spendMsg.go new file mode 100644 index 0000000..0cfdbd9 --- /dev/null +++ b/msgs/spendMsg.go @@ -0,0 +1,70 @@ +// Package msgs provides the messages that indicate either a spend of utxos or +// a deposit on the rootchain contract. +package msgs + +import ( + "github.com/FourthState/plasma-mvp-sidechain/plasma" + "github.com/FourthState/plasma-mvp-sidechain/utils" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/crypto" +) + +const ( + // SpendMsgRoute is used for routing this message. + SpendMsgRoute = "spend" +) + +// SpendMsg implements the RLP interface through `Transaction` +type SpendMsg struct { + plasma.Transaction +} + +// Type implements the sdk.Msg interface. +func (msg SpendMsg) Type() string { return "spend_utxo" } + +// Route implements the sdk.Msg interface. +func (msg SpendMsg) Route() string { return SpendMsgRoute } + +// GetSigners will attempt to retrieve the signers of the message. +// CONTRACT: a nil slice is returned if recovery fails +func (msg SpendMsg) GetSigners() []sdk.AccAddress { + txHash := utils.ToEthSignedMessageHash(msg.TxHash()) + var addrs []sdk.AccAddress + + // recover first owner + pubKey, err := crypto.SigToPub(txHash, msg.Inputs[0].Signature[:]) + if err != nil { + return nil + } + addrs = append(addrs, sdk.AccAddress(crypto.PubkeyToAddress(*pubKey).Bytes())) + + if len(msg.Inputs) > 1 { + // recover the second owner + pubKey, err = crypto.SigToPub(txHash, msg.Inputs[1].Signature[:]) + if err != nil { + return nil + } + addrs = append(addrs, sdk.AccAddress(crypto.PubkeyToAddress(*pubKey).Bytes())) + } + + return addrs +} + +// GetSignBytes returns the Keccak256 hash of the transaction. +func (msg SpendMsg) GetSignBytes() []byte { + return msg.TxHash() +} + +// ValidateBasic verifies that the transaction is valid. +func (msg SpendMsg) ValidateBasic() sdk.Error { + if err := msg.Transaction.ValidateBasic(); err != nil { + return ErrInvalidSpendMsg(DefaultCodespace, err.Error()) + } + + return nil +} + +// GetMsgs implements the sdk.Tx interface +func (msg SpendMsg) GetMsgs() []sdk.Msg { + return []sdk.Msg{msg} +} diff --git a/msgs/spendMsg_test.go b/msgs/spendMsg_test.go new file mode 100644 index 0000000..bc56990 --- /dev/null +++ b/msgs/spendMsg_test.go @@ -0,0 +1,30 @@ +package msgs + +import ( + "github.com/FourthState/plasma-mvp-sidechain/plasma" + "github.com/FourthState/plasma-mvp-sidechain/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rlp" + "github.com/stretchr/testify/require" + "reflect" + "testing" +) + +func TestSpendMsgSerialization(t *testing.T) { + msg := SpendMsg{ + Transaction: plasma.Transaction{ + Inputs: []plasma.Input{plasma.NewInput(plasma.NewPosition(nil, 1, 0, nil), [65]byte{}, nil), plasma.NewInput(plasma.NewPosition(utils.Big1, 1, 1, nil), [65]byte{}, nil)}, + Outputs: []plasma.Output{plasma.NewOutput(common.HexToAddress("1"), utils.Big1)}, + Fee: utils.Big1, + }, + } + + bytes, err := rlp.EncodeToBytes(&msg) + require.NoError(t, err, "serialization error") + + recoveredMsg := SpendMsg{} + err = rlp.DecodeBytes(bytes, &recoveredMsg) + require.NoError(t, err, "deserialization error") + + require.True(t, reflect.DeepEqual(msg, recoveredMsg), "serialized and deserialized msgs not equal") +} diff --git a/msgs/txDecoder.go b/msgs/txDecoder.go new file mode 100644 index 0000000..1532e1f --- /dev/null +++ b/msgs/txDecoder.go @@ -0,0 +1,23 @@ +package msgs + +import ( + "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/rlp" +) + +// TxDecoder attempts to RLP decode the transaction bytes into a SpendMsg first +// then to a IncludeDepositMsg otherwise returns an error. +func TxDecoder(txBytes []byte) (sdk.Tx, sdk.Error) { + var spendMsg SpendMsg + if err := rlp.DecodeBytes(txBytes, &spendMsg); err != nil { + var depositMsg IncludeDepositMsg + if err2 := rlp.DecodeBytes(txBytes, &depositMsg); err2 != nil { + return nil, sdk.ErrTxDecode(fmt.Sprintf("Decode to SpendMsg error: { %s } Decode to DepositMsg error: { %s }", + err.Error(), err2.Error())) + } + return depositMsg, nil + } + + return spendMsg, nil +} diff --git a/plasma/block.go b/plasma/block.go new file mode 100644 index 0000000..3e77b5d --- /dev/null +++ b/plasma/block.go @@ -0,0 +1,23 @@ +package plasma + +import ( + "math/big" +) + +// Block represents a plasma block. +type Block struct { + Header [32]byte + TxnCount uint16 + FeeAmount *big.Int + Height *big.Int +} + +// NewBlock creates a Block object. +func NewBlock(header [32]byte, txnCount uint16, feeAmount, height *big.Int) Block { + return Block{ + Header: header, + TxnCount: txnCount, + FeeAmount: feeAmount, + Height: height, + } +} diff --git a/plasma/block_test.go b/plasma/block_test.go new file mode 100644 index 0000000..97892ba --- /dev/null +++ b/plasma/block_test.go @@ -0,0 +1,24 @@ +package plasma + +import ( + "crypto/sha256" + "github.com/ethereum/go-ethereum/rlp" + "github.com/stretchr/testify/require" + "math/big" + "reflect" + "testing" +) + +func TestBlockSerialization(t *testing.T) { + header := sha256.Sum256([]byte("header")) + block := NewBlock(header, 10, big.NewInt(1), big.NewInt(1337)) + + bytes, err := rlp.EncodeToBytes(&block) + require.NoError(t, err, "Error serializing block") + + recoveredBlock := &Block{} + err = rlp.DecodeBytes(bytes, recoveredBlock) + require.NoError(t, err, "Error deserializing block") + + require.True(t, reflect.DeepEqual(&block, recoveredBlock), "serialized and deserialized objects not deeply equal") +} diff --git a/plasma/deposit.go b/plasma/deposit.go new file mode 100644 index 0000000..d25f47f --- /dev/null +++ b/plasma/deposit.go @@ -0,0 +1,51 @@ +package plasma + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rlp" + "io" + "math/big" +) + +// Deposit represents a deposit that occured on the ethereum plasma smart contract. +type Deposit struct { + Owner common.Address `json:"Owner"` + Amount *big.Int `json:"Amount"` + EthBlockNum *big.Int `json:"EthBlockNum"` +} + +type deposit struct { + Owner common.Address + Amount []byte + EthBlockNum []byte +} + +// NewDeposit creates a Deposit object. +func NewDeposit(owner common.Address, amount *big.Int, ethBlockNum *big.Int) Deposit { + return Deposit{ + Owner: owner, + Amount: amount, + EthBlockNum: ethBlockNum, + } +} + +// EncodeRLP satisfies the rlp interface for Deposit. +func (d *Deposit) EncodeRLP(w io.Writer) error { + deposit := &deposit{d.Owner, d.Amount.Bytes(), d.EthBlockNum.Bytes()} + + return rlp.Encode(w, deposit) +} + +// DecodeRLP satisfies the rlp interface for Deposit. +func (d *Deposit) DecodeRLP(s *rlp.Stream) error { + var dep deposit + if err := s.Decode(&dep); err != nil { + return err + } + + d.Owner = dep.Owner + d.Amount = new(big.Int).SetBytes(dep.Amount) + d.EthBlockNum = new(big.Int).SetBytes(dep.EthBlockNum) + + return nil +} diff --git a/plasma/deposit_test.go b/plasma/deposit_test.go new file mode 100644 index 0000000..4800273 --- /dev/null +++ b/plasma/deposit_test.go @@ -0,0 +1,24 @@ +package plasma + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rlp" + "github.com/stretchr/testify/require" + "math/big" + "reflect" + "testing" +) + +func TestDepositSerialization(t *testing.T) { + one := big.NewInt(1) + deposit := &Deposit{common.HexToAddress("0"), one, one} + + bytes, err := rlp.EncodeToBytes(deposit) + require.NoError(t, err, "Error serializing deposit") + + recoveredDeposit := &Deposit{} + err = rlp.DecodeBytes(bytes, recoveredDeposit) + require.NoError(t, err, "Error deserializing deposit") + + require.True(t, reflect.DeepEqual(deposit, recoveredDeposit), "serialized and deserialized deposits are not deeply equal") +} diff --git a/plasma/input.go b/plasma/input.go new file mode 100644 index 0000000..b2bd908 --- /dev/null +++ b/plasma/input.go @@ -0,0 +1,83 @@ +package plasma + +import ( + "bytes" + "fmt" +) + +// Input represents the input to a spend transaction. +type Input struct { + Position + Signature [65]byte + ConfirmSignatures [][65]byte +} + +// NewInput creates a Input object. +func NewInput(position Position, sig [65]byte, confirmSigs [][65]byte) Input { + // nil != empty slice. avoid deserialization issues by forcing empty slices + if confirmSigs == nil { + confirmSigs = [][65]byte{} + } + + return Input{ + Position: position, + Signature: sig, + ConfirmSignatures: confirmSigs, + } +} + +/* + So far in the project, serialization for the Input struct was not needed. This can + be added in here when needed +*/ + +// ValidateBasic ensures a nil or valid input +func (i Input) ValidateBasic() error { + var emptySig [65]byte + if i.Position.IsNilPosition() { + if !bytes.Equal(i.Signature[:], emptySig[:]) || len(i.ConfirmSignatures) > 0 { + return fmt.Errorf("nil input should not specify a signature nor confirm signatures") + } + } else { + if err := i.Position.ValidateBasic(); err != nil { + return fmt.Errorf("invalid position { %s }", err) + } + + if bytes.Equal(i.Signature[:], emptySig[:]) { + return fmt.Errorf("cannot provide an empty signature") + } + + confSigLen := len(i.ConfirmSignatures) + if i.Position.IsDeposit() || i.TxIndex == 1<<16-1 { + if confSigLen != 0 { + return fmt.Errorf("deposit or fee inputs must not include confirm signatures") + } + } else { + if confSigLen != 1 && confSigLen != 2 { + return fmt.Errorf("transaction inputs must specify 1 or 2 confirm signatures") + } + + for _, sig := range i.ConfirmSignatures { + if len(sig) != 65 { + return fmt.Errorf("confirm signatures must be 65 bytes in length") + } + } + } + } + + return nil +} + +func (i Input) String() string { + if len(i.ConfirmSignatures) != 0 { + str := fmt.Sprintf("Position: %s, Signature: 0x%x, Confirm Signatures: 0x%x", + i.Position, i.Signature, i.ConfirmSignatures[0]) + if len(i.ConfirmSignatures) > 1 { + str = str + fmt.Sprintf(", 0x%x", i.ConfirmSignatures[1]) + } + + return str + } + + return fmt.Sprintf("Position: %s, Signature: 0x%x, Confirm Signatures: nil", i.Position, i.Signature) +} diff --git a/plasma/input_test.go b/plasma/input_test.go new file mode 100644 index 0000000..e185937 --- /dev/null +++ b/plasma/input_test.go @@ -0,0 +1,62 @@ +package plasma + +import ( + "github.com/stretchr/testify/require" + "testing" +) + +func TestInputValidation(t *testing.T) { + type validationCase struct { + reason string + Input + } + + pos, _ := FromPositionString("(1.0.0.0)") + depositPos, _ := FromPositionString("(0.0.0.1)") + nilPos, _ := FromPositionString("(0.0.0.0)") + + sampleSig := [65]byte{} + sampleSig[0] = byte(1) + + invalidInputs := []validationCase{ + validationCase{ + reason: "nil input with a signature", + Input: NewInput(nilPos, sampleSig, nil), + }, + validationCase{ + reason: "nil input with a confirm signature", + Input: NewInput(nilPos, [65]byte{}, [][65]byte{sampleSig}), + }, + validationCase{ + reason: "input with no signature", + Input: NewInput(pos, [65]byte{}, [][65]byte{sampleSig}), + }, + validationCase{ + reason: "transaction input with no confirm signature", + Input: NewInput(pos, sampleSig, nil), + }, + validationCase{ + reason: "deposit input with a no signature", + Input: NewInput(depositPos, [65]byte{}, nil), + }, + validationCase{ + reason: "deposit input with a confirmSignature signature", + Input: NewInput(depositPos, sampleSig, [][65]byte{sampleSig}), + }, + } + + for _, input := range invalidInputs { + err := input.ValidateBasic() + require.Error(t, err, input.reason) + } + + pos, _ = FromPositionString("(1.1.0.0)") + input := NewInput(pos, sampleSig, [][65]byte{sampleSig}) + err := input.ValidateBasic() + require.NoError(t, err) + + pos, _ = FromPositionString("(0.0.0.0)") + input = NewInput(pos, [65]byte{}, nil) + err = input.ValidateBasic() + require.NoError(t, err) +} diff --git a/plasma/output.go b/plasma/output.go new file mode 100644 index 0000000..a24dfca --- /dev/null +++ b/plasma/output.go @@ -0,0 +1,74 @@ +package plasma + +import ( + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rlp" + "io" + "math/big" +) + +// Output represents the outputs of a transaction +type Output struct { + Owner common.Address + Amount *big.Int +} + +type output struct { + Owner common.Address + Amount []byte +} + +func NewOutput(owner common.Address, amount *big.Int) Output { + if amount == nil { + amount = big.NewInt(0) + } + + return Output{ + Owner: owner, + Amount: amount, + } +} + +func (o *Output) EncodeRLP(w io.Writer) error { + output := output{o.Owner, o.Amount.Bytes()} + + return rlp.Encode(w, output) +} + +func (o *Output) DecodeRLP(s *rlp.Stream) error { + var output output + if err := s.Decode(&output); err != nil { + return err + } + + o.Owner = output.Owner + o.Amount = new(big.Int).SetBytes(output.Amount) + + return nil +} + +// ValidateBasic ensures either a nil or valid output +func (o Output) ValidateBasic() error { + if utils.IsZeroAddress(o.Owner) { + if o.Amount.Sign() > 0 { + return fmt.Errorf("amount specified with a nil owner") + } + } else { + if o.Amount.Sign() == 0 { + return fmt.Errorf("cannot send a zero amount") + } + } + + return nil +} + +func (o Output) Bytes() []byte { + bytes, _ := rlp.EncodeToBytes(&o) + return bytes +} + +func (o Output) String() string { + return fmt.Sprintf("Owner: %x, Amount: %s", o.Owner, o.Amount) +} diff --git a/plasma/output_test.go b/plasma/output_test.go new file mode 100644 index 0000000..3de732b --- /dev/null +++ b/plasma/output_test.go @@ -0,0 +1,51 @@ +package plasma + +import ( + "github.com/FourthState/plasma-mvp-sidechain/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rlp" + "github.com/stretchr/testify/require" + "math/big" + "reflect" + "testing" +) + +func TestOutputSerialization(t *testing.T) { + output := NewOutput(common.HexToAddress("69"), big.NewInt(10)) + + data, err := rlp.EncodeToBytes(&output) + require.NoError(t, err, "error serializing output") + + recoveredOutput := Output{} + err = rlp.DecodeBytes(data, &recoveredOutput) + require.NoError(t, err, "error deserializing output") + + require.True(t, reflect.DeepEqual(output, recoveredOutput), "serialized and deserialized output are not deeply equal") +} + +func TestOutputValidation(t *testing.T) { + type validationCase struct { + reason string + Output + } + + invalidOutputs := []validationCase{ + validationCase{ + reason: "output spending funds to the nil address", + Output: NewOutput(utils.ZeroAddress, utils.Big1), + }, + validationCase{ + reason: "output with spending no funds", + Output: NewOutput(common.HexToAddress("1"), utils.Big0), + }, + } + for _, output := range invalidOutputs { + err := output.ValidateBasic() + require.Error(t, err, "did not catch: %s", output.reason) + } + + // valid output + output := NewOutput(common.HexToAddress("1"), utils.Big1) + err := output.ValidateBasic() + require.NoError(t, err, "marked output as invalid: %s", err) +} diff --git a/plasma/position.go b/plasma/position.go new file mode 100644 index 0000000..d20c66e --- /dev/null +++ b/plasma/position.go @@ -0,0 +1,170 @@ +package plasma + +import ( + "fmt" + "github.com/ethereum/go-ethereum/rlp" + "math/big" + "strconv" + "strings" +) + +type Position struct { + BlockNum *big.Int + TxIndex uint16 + OutputIndex uint8 + DepositNonce *big.Int +} + +const ( + blockIndexFactor = 1000000 + txIndexFactor = 10 +) + +func NewPosition(blkNum *big.Int, txIndex uint16, oIndex uint8, depositNonce *big.Int) Position { + if depositNonce == nil { + depositNonce = big.NewInt(0) + } + if blkNum == nil { + blkNum = big.NewInt(0) + } + + return Position{ + BlockNum: blkNum, + TxIndex: txIndex, + OutputIndex: oIndex, + DepositNonce: depositNonce, + } +} + +func (p Position) Bytes() []byte { + bytes, _ := rlp.EncodeToBytes(&p) + return bytes +} + +// ValidateBasic ensures deposit and child chain positions are mutually exclusive. +// Block numbering starts at 1 +func (p Position) ValidateBasic() error { + if p.IsNilPosition() { + return fmt.Errorf("nil position is not a valid position") + } + + // deposit position + if p.IsDeposit() { + if p.BlockNum.Sign() > 0 || p.TxIndex > 0 || p.OutputIndex > 0 { + return fmt.Errorf("chain position must be all zero if a deposit nonce is specified. (0.0.0.nonce)") + } + } else { + if p.BlockNum.Sign() == 0 { + return fmt.Errorf("block numbering starts at 1") + } + if p.OutputIndex > 1 { + return fmt.Errorf("output index must be 0 or 1") + } + } + + return nil +} + +func (p Position) IsDeposit() bool { + return p.DepositNonce.Sign() != 0 +} + +func (p Position) IsFee() bool { + return p.TxIndex == 1<<16-1 +} + +func (p Position) IsNilPosition() bool { + return p.BlockNum.Sign() == 0 && p.DepositNonce.Sign() == 0 +} + +func (p Position) Priority() *big.Int { + if p.IsDeposit() { + return p.DepositNonce + } + + bFactor := big.NewInt(blockIndexFactor) + tFactor := big.NewInt(txIndexFactor) + + bFactor = bFactor.Mul(bFactor, p.BlockNum) + tFactor = tFactor.Mul(tFactor, big.NewInt(int64(p.TxIndex))) + + temp := new(big.Int).Add(bFactor, tFactor) + return temp.Add(temp, big.NewInt(int64(p.OutputIndex))) +} + +func (p Position) ToBigIntArray() [4]*big.Int { + return [4]*big.Int{p.BlockNum, big.NewInt(int64(p.TxIndex)), big.NewInt(int64(p.OutputIndex)), p.DepositNonce} +} + +func (p Position) String() string { + if p.BlockNum == nil { + p.BlockNum = big.NewInt(0) + } + if p.DepositNonce == nil { + p.DepositNonce = big.NewInt(0) + } + return fmt.Sprintf("(%s.%d.%d.%s)", + p.BlockNum, p.TxIndex, p.OutputIndex, p.DepositNonce) +} + +func FromPositionString(posStr string) (Position, error) { + posStr = strings.TrimSpace(posStr) + if string(posStr[0]) != "(" || string(posStr[len(posStr)-1]) != ")" { + return Position{}, fmt.Errorf("position must be enclosed in parens. (blockNum,txIndex,oIndex,depositNonce)") + } + + // remove the parens + posStr = posStr[1 : len(posStr)-1] + + blkNum := new(big.Int) + depositNonce := new(big.Int) + + var txIndex uint16 + var oIndex uint8 + + tokens := strings.Split(posStr, ".") + if len(tokens) != 4 { + return Position{}, + fmt.Errorf("invalid position. positions follow (blockNum.txIndex.oIndex.depositNonce). ex: (1.0.0.0)") + } + + var err error + var ok bool + var num uint64 + for i, token := range tokens { + token = strings.TrimSpace(token) + if i == 0 { + blkNum, ok = blkNum.SetString(token, 10) + if !ok { + return Position{}, fmt.Errorf("error parsing the block number") + } + } else if i == 1 { + num, err = strconv.ParseUint(token, 0, 16) + txIndex = uint16(num) + } else if i == 2 { + num, err = strconv.ParseUint(token, 0, 8) + oIndex = uint8(num) + } else { + depositNonce, ok = depositNonce.SetString(token, 10) + if !ok { + return Position{}, fmt.Errorf("error parsing the deposit nonce") + } + } + + if err != nil { + return Position{}, err + } + } + + pos := NewPosition(blkNum, txIndex, oIndex, depositNonce) + return pos, pos.ValidateBasic() +} + +// Return the position of a utxo given the exit key +func FromExitKey(key *big.Int, deposit bool) Position { + if deposit { + return NewPosition(big.NewInt(0), 0, 0, key) + } else { + return NewPosition(new(big.Int).Div(key, big.NewInt(blockIndexFactor)), uint16(key.Int64()%blockIndexFactor/txIndexFactor), uint8(key.Int64()%2), big.NewInt(0)) + } +} diff --git a/plasma/position_test.go b/plasma/position_test.go new file mode 100644 index 0000000..8c70a24 --- /dev/null +++ b/plasma/position_test.go @@ -0,0 +1,86 @@ +package plasma + +import ( + "github.com/ethereum/go-ethereum/rlp" + "github.com/stretchr/testify/require" + "math/big" + "reflect" + "testing" +) + +func TestPositionSerialization(t *testing.T) { + position := NewPosition(big.NewInt(1), 6, 9, big.NewInt(0)) + bytes, err := rlp.EncodeToBytes(&position) + require.NoError(t, err, "error serializing position") + + recoveredPosition := Position{} + err = rlp.DecodeBytes(bytes, &recoveredPosition) + require.NoError(t, err, "error deserializing position") + + require.True(t, reflect.DeepEqual(&position, &recoveredPosition), "serialized and deserialized position not deeply equal") +} + +func TestPositionDepositIndication(t *testing.T) { + position := NewPosition(big.NewInt(1), 0, 0, big.NewInt(0)) + require.False(t, position.IsDeposit(), "position [1,0,0,0] marked as a deposit") + + position = NewPosition(big.NewInt(0), 0, 0, big.NewInt(1)) + require.True(t, position.IsDeposit(), "position [0,0,0,1] not marked as a deposit") +} + +func TestPositionFeeIndication(t *testing.T) { + position := NewPosition(big.NewInt(2), 0, 0, big.NewInt(0)) + require.False(t, position.IsFee(), "position [2, 0, 0, 0] marked as a fee") + + position = NewPosition(big.NewInt(1), 65535, 0, big.NewInt(0)) + require.True(t, position.IsFee(), "position [1, 65535, 0, 0] not marked as a fee") +} + +func TestPositionFromString(t *testing.T) { + posStr := "(1.0.0.0)" + pos, err := FromPositionString(posStr) + require.NoError(t, err, "error converting from string") + require.Equal(t, pos.String(), posStr) + + posStr = "(0.0.5.0)" + pos, err = FromPositionString(posStr) + require.Error(t, err, "converted with an invalid output index") + + posStr = "(1.0.0.1)" + pos, err = FromPositionString(posStr) + require.Error(t, err, "converted with nonce and chain positions specified") +} + +func TestPositionValidation(t *testing.T) { + posStr := "(1.0.0.0)" + pos, _ := FromPositionString(posStr) + require.NoError(t, pos.ValidateBasic(), "valid position marked as an error") + + // specify both deposit nonce and chain position + cases := []string{ + // mutual exclusivity between deposit nonce and chain position required + "(1.0.0.5)", + "(0.1.0.5)", + "(0.0.1.5)", + // chain position with block number zero + "(0.1.1.0)", + // invalid output index + "(1.1.3.0)", + // nil position is not a valid position + "(0.0.0.0)", + } + for _, posStr := range cases { + pos, _ = FromPositionString(posStr) + require.Errorf(t, pos.ValidateBasic(), "invalid position: %s", posStr) + } +} + +func TestPositionFromKey(t *testing.T) { + utxoKey := big.NewInt(133*blockIndexFactor + 14*txIndexFactor) + utxo := FromExitKey(utxoKey, false) + require.Equal(t, utxo, NewPosition(big.NewInt(133), 14, 0, big.NewInt(0)), "error retrieving correct position from exit key") + + depositKey := big.NewInt(10) + deposit := FromExitKey(depositKey, true) + require.Equal(t, deposit, NewPosition(big.NewInt(0), 0, 0, depositKey), "error retrieving correct position from deposit exit key") +} diff --git a/plasma/transaction.go b/plasma/transaction.go new file mode 100644 index 0000000..2209441 --- /dev/null +++ b/plasma/transaction.go @@ -0,0 +1,280 @@ +// Package plasma declares and implements all plasma types used by the plasma +// chain. +package plasma + +import ( + "bytes" + "crypto/sha256" + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/rlp" + "io" + "math/big" +) + +// Transaction represents a spend of inputs. Fields should not be accessed directly +type Transaction struct { + Inputs []Input + Outputs []Output + Fee *big.Int +} + +type txList struct { + BlkNum0 [32]byte + TxIndex0 [32]byte + OIndex0 [32]byte + DepositNonce0 [32]byte + Input0ConfirmSigs [130]byte + BlkNum1 [32]byte + TxIndex1 [32]byte + OIndex1 [32]byte + DepositNonce1 [32]byte + Input1ConfirmSigs [130]byte + NewOwner0 common.Address + Amount0 [32]byte + NewOwner1 common.Address + Amount1 [32]byte + Fee [32]byte +} + +type rawTx struct { + Tx txList + Sigs [2][65]byte +} + +// EncodeRLP satisfies the rlp interface for Transaction +func (tx *Transaction) EncodeRLP(w io.Writer) error { + t := &rawTx{tx.toTxList(), tx.Sigs()} + + return rlp.Encode(w, t) +} + +// DecodeRLP satisfies the rlp interface for Transaction +func (tx *Transaction) DecodeRLP(s *rlp.Stream) error { + var t rawTx + if err := s.Decode(&t); err != nil { + return err + } + + confirmSigs0 := parseSig(t.Tx.Input0ConfirmSigs) + confirmSigs1 := parseSig(t.Tx.Input1ConfirmSigs) + + tx.Inputs = append(tx.Inputs, NewInput(NewPosition(big.NewInt(new(big.Int).SetBytes(t.Tx.BlkNum0[:]).Int64()), uint16(new(big.Int).SetBytes(t.Tx.TxIndex0[:]).Int64()), uint8(new(big.Int).SetBytes(t.Tx.OIndex0[:]).Int64()), big.NewInt(new(big.Int).SetBytes(t.Tx.DepositNonce0[:]).Int64())), + t.Sigs[0], confirmSigs0)) + pos := NewPosition(big.NewInt(new(big.Int).SetBytes(t.Tx.BlkNum1[:]).Int64()), uint16(new(big.Int).SetBytes(t.Tx.TxIndex1[:]).Int64()), uint8(new(big.Int).SetBytes(t.Tx.OIndex1[:]).Int64()), big.NewInt(new(big.Int).SetBytes(t.Tx.DepositNonce1[:]).Int64())) + if !pos.IsNilPosition() { + tx.Inputs = append(tx.Inputs, NewInput(pos, t.Sigs[1], confirmSigs1)) + } + // set signatures if applicable + tx.Outputs = append(tx.Outputs, NewOutput(t.Tx.NewOwner0, big.NewInt(new(big.Int).SetBytes(t.Tx.Amount0[:]).Int64()))) + if !utils.IsZeroAddress(t.Tx.NewOwner1) { + tx.Outputs = append(tx.Outputs, NewOutput(t.Tx.NewOwner1, big.NewInt(new(big.Int).SetBytes(t.Tx.Amount1[:]).Int64()))) + } + tx.Fee = big.NewInt(new(big.Int).SetBytes(t.Tx.Fee[:]).Int64()) + + return nil +} + +func (tx Transaction) ValidateBasic() error { + // Error if more than 2 inputs or outputs. Will be allowed in future versions + if len(tx.Inputs) > 2 || len(tx.Outputs) > 2 { + return fmt.Errorf("invalid tx, maximum of 2 input/output allowed") + } + + // validate inputs + for i, input := range tx.Inputs { + if err := input.ValidateBasic(); err != nil { + return fmt.Errorf("invalid input with index %d: %s", i, err) + } + if input.Position.IsNilPosition() { + return fmt.Errorf("input position cannot be nil") + } + } + + if len(tx.Inputs) > 1 { + if tx.Inputs[0].Position.String() == tx.Inputs[1].Position.String() { + return fmt.Errorf("same position cannot be spent twice") + } + } + + // validate outputs + for i, output := range tx.Outputs { + if err := output.ValidateBasic(); err != nil { + return fmt.Errorf("invalid output with index %d: %s", i, err) + } + if utils.IsZeroAddress(output.Owner) || output.Amount.Sign() == 0 { + return fmt.Errorf("output with index %d must have a valid address and non-zero amount", i) + } + } + + return nil +} + +func (tx Transaction) TxBytes() []byte { + bytes, _ := rlp.EncodeToBytes(&tx) + return bytes +} + +// TxHash returns the bytes the signatures are signed over +func (tx Transaction) TxHash() []byte { + txList := tx.toTxList() + bytes, _ := rlp.EncodeToBytes(&txList) + + return crypto.Keccak256(bytes) +} + +// MerkleHash returns the bytes that is included in the merkle tree +func (tx Transaction) MerkleHash() []byte { + hash := sha256.Sum256(tx.TxBytes()) + return hash[:] +} + +// Sigs returns the signatures that signed over this transaction +// These signatures were generated by the inputs +func (tx Transaction) Sigs() [2][65]byte { + var sigs [2][65]byte + for i, input := range tx.Inputs { + sigs[i] = input.Signature + } + + return sigs +} + +// InputPositions returns the input positions for this transaction +func (tx Transaction) InputPositions() []Position { + var positions []Position + for _, input := range tx.Inputs { + positions = append(positions, input.Position) + } + + return positions +} + +// OutputAddresses returns the output addresses for this transaction. +func (tx Transaction) OutputAddresses() []common.Address { + var addrs []common.Address + for _, output := range tx.Outputs { + addrs = append(addrs, output.Owner) + } + + return addrs +} +func (tx Transaction) String() string { + str := "" + for i, input := range tx.Inputs { + str += fmt.Sprintf("Input %d: %s\n", i, input) + } + for i, output := range tx.Outputs { + str += fmt.Sprintf("Output %d: %s\n", i, output) + } + str += fmt.Sprintf("Fee: %s\n", tx.Fee) + + return str +} + +/* Helpers */ + +func (tx Transaction) toTxList() txList { + + // pointer safety if a transaction + // object was ever created with Transaction{} + txList := txList{} + + for i, input := range tx.Inputs { + if input.BlockNum == nil { + tx.Inputs[i].BlockNum = utils.Big0 + } + if input.DepositNonce == nil { + tx.Inputs[i].DepositNonce = utils.Big0 + } + } + + for i, output := range tx.Outputs { + if output.Amount == nil { + tx.Outputs[i].Amount = utils.Big0 + } + } + + if tx.Fee == nil { + tx.Fee = utils.Big0 + } + + // fill in txList with values + // Input 0 + input := tx.Inputs[0] + if len(input.BlockNum.Bytes()) > 0 { + copy(txList.BlkNum0[32-len(input.BlockNum.Bytes()):], input.BlockNum.Bytes()) + } + txList.TxIndex0[31] = byte(input.TxIndex) + txList.TxIndex0[30] = byte(input.TxIndex >> 8) + txList.OIndex0[31] = byte(input.OutputIndex) + if len(input.DepositNonce.Bytes()) > 0 { + copy(txList.DepositNonce0[32-len(input.DepositNonce.Bytes()):], input.DepositNonce.Bytes()) + } + switch len(input.ConfirmSignatures) { + case 1: + copy(txList.Input0ConfirmSigs[:65], input.ConfirmSignatures[0][:]) + case 2: + copy(txList.Input0ConfirmSigs[:65], input.ConfirmSignatures[0][:]) + copy(txList.Input0ConfirmSigs[65:], input.ConfirmSignatures[1][:]) + } + + // Input 1 + if len(tx.Inputs) > 1 { + input = tx.Inputs[1] + if len(input.BlockNum.Bytes()) > 0 { + copy(txList.BlkNum1[32-len(input.BlockNum.Bytes()):], input.BlockNum.Bytes()) + } + txList.TxIndex1[31] = byte(input.TxIndex) + txList.TxIndex1[30] = byte(input.TxIndex >> 8) + txList.OIndex1[31] = byte(input.OutputIndex) + if len(input.DepositNonce.Bytes()) > 0 { + copy(txList.DepositNonce1[32-len(input.DepositNonce.Bytes()):], input.DepositNonce.Bytes()) + } + + switch len(input.ConfirmSignatures) { + case 1: + copy(txList.Input1ConfirmSigs[:65], input.ConfirmSignatures[0][:]) + case 2: + copy(txList.Input1ConfirmSigs[:65], input.ConfirmSignatures[0][:]) + copy(txList.Input1ConfirmSigs[65:], input.ConfirmSignatures[1][:]) + } + } + // Outputs and Fee + output := tx.Outputs[0] + txList.NewOwner0 = output.Owner + if len(output.Amount.Bytes()) > 0 { + copy(txList.Amount0[32-len(output.Amount.Bytes()):], output.Amount.Bytes()) + } + if len(tx.Outputs) > 1 { + output = tx.Outputs[1] + txList.NewOwner1 = output.Owner + if len(output.Amount.Bytes()) > 0 { + copy(txList.Amount1[32-len(output.Amount.Bytes()):], output.Amount.Bytes()) + } + } + if len(tx.Fee.Bytes()) > 0 { + copy(txList.Fee[32-len(tx.Fee.Bytes()):], tx.Fee.Bytes()) + } + + return txList +} + +// Helpers +// Convert 130 byte input confirm sigs to 65 byte slices +func parseSig(sig [130]byte) [][65]byte { + if bytes.Equal(sig[:65], make([]byte, 65)) { + return [][65]byte{} + } else if bytes.Equal(sig[65:], make([]byte, 65)) { + newSig := make([][65]byte, 1) + copy(newSig[0][:], sig[:65]) + return newSig + } else { + newSig := make([][65]byte, 2) + copy(newSig[0][:], sig[:65]) + copy(newSig[1][:], sig[65:]) + return newSig + } +} diff --git a/plasma/transaction_test.go b/plasma/transaction_test.go new file mode 100644 index 0000000..71a4b58 --- /dev/null +++ b/plasma/transaction_test.go @@ -0,0 +1,143 @@ +package plasma + +import ( + "github.com/FourthState/plasma-mvp-sidechain/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/rlp" + "github.com/stretchr/testify/require" + "math/big" + "reflect" + "testing" +) + +func TestTransactionSerialization(t *testing.T) { + one := big.NewInt(1) + + // contstruct a transaction + tx := &Transaction{} + pos, _ := FromPositionString("(1.10000.1.0)") + confirmSig0 := make([][65]byte, 1) + copy(confirmSig0[0][65-len([]byte("confirm sig")):], []byte("confirm sig")) + tx.Inputs = append(tx.Inputs, NewInput(pos, [65]byte{}, confirmSig0)) + tx.Inputs[0].Signature[1] = byte(1) + pos, _ = FromPositionString("(0.0.0.1)") + confirmSig1 := make([][65]byte, 2) + copy(confirmSig1[0][65-len([]byte("the second confirm sig")):], []byte("the second confirm sig")) + copy(confirmSig1[1][65-len([]byte("a very long string turned into bytes")):], []byte("a very long string turned into bytes")) + tx.Inputs = append(tx.Inputs, NewInput(pos, [65]byte{}, confirmSig1)) + tx.Outputs = append(tx.Outputs, NewOutput(common.HexToAddress("1"), one)) + tx.Fee = big.NewInt(1) + + bytes, err := rlp.EncodeToBytes(tx) + require.NoError(t, err, "error serializing transaction") + require.Equal(t, 811, len(bytes), "encoded bytes should sum to 811") + + recoveredTx := &Transaction{} + err = rlp.DecodeBytes(bytes, recoveredTx) + require.NoError(t, err, "error deserializing transaction") + + require.EqualValues(t, tx, recoveredTx, "serialized and deserialized transaction not deeply equal") + require.True(t, reflect.DeepEqual(tx, recoveredTx), "serialized and deserialized transactions not deeply equal") +} + +func GetPosition(posStr string) Position { + pos, _ := FromPositionString(posStr) + return pos +} + +func TestTransactionValidation(t *testing.T) { + privKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privKey.PublicKey) + + emptySig := [65]byte{} + + sampleSig := [65]byte{} + sampleSig[0] = byte(1) + sampleConfirmSig := [][65]byte{sampleSig} + + type validationCase struct { + reason string + Transaction + } + + invalidTxs := []validationCase{ + validationCase{ + reason: "tx with an empty first input", + Transaction: Transaction{ + Inputs: []Input{NewInput(GetPosition("(0.0.0.0)"), emptySig, nil)}, + Outputs: []Output{NewOutput(utils.ZeroAddress, utils.Big0)}, + Fee: utils.Big0, + }, + }, + validationCase{ + reason: "tx with no recipient", + Transaction: Transaction{ + Inputs: []Input{NewInput(GetPosition("(0.0.0.1)"), sampleSig, sampleConfirmSig)}, + Outputs: []Output{NewOutput(utils.ZeroAddress, utils.Big1)}, + Fee: utils.Big0, + }, + }, + validationCase{ + reason: "tx with no output amount", + Transaction: Transaction{ + Inputs: []Input{NewInput(GetPosition("(0.0.0.1)"), sampleSig, sampleConfirmSig)}, + Outputs: []Output{NewOutput(addr, utils.Big0)}, + Fee: utils.Big0, + }, + }, + validationCase{ + reason: "tx with the same position for both inputs", + Transaction: Transaction{ + Inputs: []Input{NewInput(GetPosition("(0.0.0.1)"), sampleSig, sampleConfirmSig), NewInput(GetPosition("(0.0.0.1)"), sampleSig, sampleConfirmSig)}, + Outputs: []Output{NewOutput(addr, utils.Big0)}, + Fee: utils.Big0, + }, + }, + } + + for _, tx := range invalidTxs { + err := tx.ValidateBasic() + require.Error(t, err, tx.reason) + } + + validTxs := []validationCase{ + validationCase{ + reason: "tx with one input and one output", + Transaction: Transaction{ + Inputs: []Input{NewInput(GetPosition("(0.0.0.1)"), sampleSig, nil)}, + Outputs: []Output{NewOutput(addr, utils.Big1)}, + Fee: utils.Big0, + }, + }, + validationCase{ + reason: "tx with one input and two output", + Transaction: Transaction{ + Inputs: []Input{NewInput(GetPosition("(0.0.0.1)"), sampleSig, nil)}, + Outputs: []Output{NewOutput(addr, utils.Big1), NewOutput(addr, utils.Big1)}, + Fee: utils.Big0, + }, + }, + validationCase{ + reason: "tx with two input and one output", + Transaction: Transaction{ + Inputs: []Input{NewInput(GetPosition("(0.0.0.1)"), sampleSig, nil), NewInput(GetPosition("(1.0.1.0)"), sampleSig, sampleConfirmSig)}, + Outputs: []Output{NewOutput(addr, utils.Big1)}, + Fee: utils.Big0, + }, + }, + validationCase{ + reason: "tx with two input and two outputs", + Transaction: Transaction{ + Inputs: []Input{NewInput(GetPosition("(0.0.0.1)"), sampleSig, nil), NewInput(GetPosition("(1.0.1.0)"), sampleSig, sampleConfirmSig)}, + Outputs: []Output{NewOutput(addr, utils.Big1), NewOutput(addr, utils.Big1)}, + Fee: utils.Big0, + }, + }, + } + + for _, tx := range validTxs { + err := tx.ValidateBasic() + require.NoError(t, err, tx.reason) + } +} diff --git a/scripts/plasma_install.sh b/scripts/plasma_install.sh new file mode 100755 index 0000000..dad2b07 --- /dev/null +++ b/scripts/plasma_install.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# This script is intended to install geth, plasmad, and plasmacli +# It assumes none of the dependencies have been installed +# It will format geth to be a system service + +# Upgrade the system and install go, gcc, make, geth +sudo apt-get install software-properties-common +sudo add-apt-repository -y ppa:ethereum/ethereum +sudo apt update +sudo apt upgrade -y +sudo apt install gcc make ethereum -y +sudo snap install --classic go +sudo mkdir -p ~/go/bin/ + +# Export GO path and append to .profile file +echo "export PATH=\$PATH:/usr/local/go/bin" >> ~/.profile +echo "export GOPATH=$HOME/go" >> ~/.profile +echo "export PATH=\$PATH:\$GOPATH/bin" >> ~/.profile + +source ~/.profile + +# Install plasmad and plasmacli +go get github.com/FourthState/plasma-mvp-sidechain +cd ~/go/src/github.com/FourthState/plasma-mvp-sidechain/ +git fetch --all +git checkout develop +make install +plasmad unsafe-reset-all + +# Install npm, Truffle +apt-get install nodejs +apt-get install npm +cd ~/go/src/github.com/FourthState/plasma-mvp-sidechain/contracts/ +npm install -g + +# setup geth as system service +sudo useradd -m -d /opt/geth --system --shell /usr/sbin/nologin geth +sudo -u geth mkdir -p /opt/geth/rinkeby +cd /opt/geth/rinkeby/ + +# geth.service +echo "[Unit] +Description=Geth +After=network-online.target +[Service] +User=geth +ExecStart=/usr/bin/geth --datadir=/opt/geth/rinkeby/chaindata/ --rinkeby --rpc --rpcapi db,eth,net,web3,personal +Restart=always +RestartSec=3 +LimitNOFILE=4096 +[Install] +WantedBy=multi-user.target" > geth.service + +sudo mv geth.service /etc/systemd/system/ +sudo systemctl enable geth.service +sudo service geth start + +echo "" +echo "Geth has begun syncing to rinkeby network" +echo "Run 'sudo service geth status' to check its status" +echo "Run 'sudo service geth stop' to stop the geth full node" +echo "Set configuration parameters in ~./plasmacli/plasma.toml and ~/.plasmad/config/" +echo "Copy your genesis file to ~/.plasmad/config/genesis.json or modify the existing one" +echo "Add the operators node to 'persisten peers' in ~/.plasmad/config/config.toml" +echo "Add the plasma contract address to plasma.toml in ~/.plasmacli and ~/.plasmad/config" +echo "" diff --git a/store/blocks.go b/store/blocks.go new file mode 100644 index 0000000..439a18c --- /dev/null +++ b/store/blocks.go @@ -0,0 +1,67 @@ +package store + +import ( + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/plasma" + "github.com/FourthState/plasma-mvp-sidechain/utils" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/rlp" + "math/big" +) + +// GetBlock returns a block at the specified height +func (ds DataStore) GetBlock(ctx sdk.Context, blockHeight *big.Int) (Block, bool) { + key := GetBlockKey(blockHeight) + data := ds.Get(ctx, key) + if data == nil { + return Block{}, false + } + + block := Block{} + if err := rlp.DecodeBytes(data, &block); err != nil { + panic(fmt.Sprintf("block store corrupted: %s", err)) + } + + return block, true +} + +// StoreBlock will store the plasma block and return the plasma block number +// in which it was stored at. +func (ds DataStore) StoreBlock(ctx sdk.Context, tmBlockHeight uint64, block plasma.Block) *big.Int { + blockHeight := ds.NextPlasmaBlockHeight(ctx) + + blockKey := GetBlockKey(blockHeight) + blockData, err := rlp.EncodeToBytes(&Block{block, tmBlockHeight}) + if err != nil { + panic(fmt.Sprintf("error rlp encoding block: %s", err)) + } + + // store the block and updated the height counter + ds.Set(ctx, blockKey, blockData) + ds.Set(ctx, GetBlockHeightKey(), blockHeight.Bytes()) + + return blockHeight +} + +// PlasmaBlockHeight returns the current plasma block height. nil if no blocks exist +func (ds DataStore) PlasmaBlockHeight(ctx sdk.Context) *big.Int { + var plasmaBlockNum *big.Int + data := ds.Get(ctx, GetBlockHeightKey()) + if data == nil { + return nil + } else { + plasmaBlockNum = new(big.Int).SetBytes(data) + } + + return plasmaBlockNum +} + +// NextPlasmaBlockHeight returns the next plasma block height +func (ds DataStore) NextPlasmaBlockHeight(ctx sdk.Context) *big.Int { + height := ds.PlasmaBlockHeight(ctx) + if height == nil { + return big.NewInt(1) + } + + return height.Add(height, utils.Big1) +} diff --git a/store/blocks_test.go b/store/blocks_test.go new file mode 100644 index 0000000..c75f9e4 --- /dev/null +++ b/store/blocks_test.go @@ -0,0 +1,75 @@ +package store + +import ( + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/plasma" + "github.com/FourthState/plasma-mvp-sidechain/utils" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/rlp" + "github.com/stretchr/testify/require" + "math/big" + "reflect" + "testing" +) + +// test that a block can be serialized and deserialized +func TestBlockSerialization(t *testing.T) { + // Construct Block + plasmaBlock := plasma.Block{} + plasmaBlock.Header[0] = byte(10) + plasmaBlock.TxnCount = 3 + plasmaBlock.FeeAmount = utils.Big2 + plasmaBlock.Height = utils.Big2 + + block := Block{ + Block: plasmaBlock, + TMBlockHeight: 2, + } + + // RLP Encode + bytes, err := rlp.EncodeToBytes(&block) + require.NoError(t, err) + + // RLP Decode + recoveredBlock := Block{} + err = rlp.DecodeBytes(bytes, &recoveredBlock) + require.NoError(t, err) + + require.True(t, reflect.DeepEqual(block, recoveredBlock), "mismatch in serialized and deserialized block") +} + +// test that the plasma block number increments correctly +func TestPlasmaBlockStorage(t *testing.T) { + ctx, key := setup() + store := NewDataStore(key) + + height := store.PlasmaBlockHeight(ctx) + require.Nil(t, height, "non nil height with an empty block store") + + for i := int64(1); i <= 10; i++ { + // Retrieve nonexistent blocks + recoveredBlock, ok := store.GetBlock(ctx, big.NewInt(i)) + require.Empty(t, recoveredBlock, "did not return empty struct for nonexistent block") + require.False(t, ok, "did not return error on nonexistent block") + + nextHeight := store.NextPlasmaBlockHeight(ctx) + require.Equal(t, nextHeight, big.NewInt(i), "next block height calculated incorrectly") + + // Create and store new block + var header [32]byte + hash := crypto.Keccak256([]byte("a plasma block header")) + copy(header[:], hash[:]) + + plasmaBlock := plasma.NewBlock(header, uint16(i*13), big.NewInt(i*123), big.NewInt(1337)) + block := Block{plasmaBlock, uint64(i * 1123)} + blockNum := store.StoreBlock(ctx, uint64(i*1123), plasmaBlock) + + // check the height + height := store.PlasmaBlockHeight(ctx) + require.Equal(t, height, blockNum, "block height does not reflect the changes") + + recoveredBlock, ok = store.GetBlock(ctx, blockNum) + require.True(t, ok, "error when retrieving block") + require.True(t, reflect.DeepEqual(block, recoveredBlock), fmt.Sprintf("mismatch in stored block and retrieved block, iteration %d", i)) + } +} diff --git a/store/common_test.go b/store/common_test.go new file mode 100644 index 0000000..9bc4142 --- /dev/null +++ b/store/common_test.go @@ -0,0 +1,29 @@ +package store + +import ( + "github.com/FourthState/plasma-mvp-sidechain/plasma" + cosmosStore "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/db" + "github.com/tendermint/tendermint/libs/log" +) + +/* This file contains helper functions for testing */ + +func setup() (ctx sdk.Context, key sdk.StoreKey) { + db := db.NewMemDB() + ms := cosmosStore.NewCommitMultiStore(db) + + ctx = sdk.NewContext(ms, abci.Header{}, false, log.NewNopLogger()) + key = sdk.NewKVStoreKey("store") + ms.MountStoreWithDB(key, sdk.StoreTypeIAVL, db) + ms.LoadLatestVersion() + + return ctx, key +} + +func getPosition(posStr string) plasma.Position { + pos, _ := plasma.FromPositionString(posStr) + return pos +} diff --git a/store/datastore.go b/store/datastore.go new file mode 100644 index 0000000..78bfc35 --- /dev/null +++ b/store/datastore.go @@ -0,0 +1,53 @@ +// Package store implements a Cosmos-SDK KVStore that is capable of storing +// plasma blocks, transactions, and outputs. +package store + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + // Name - store name + DataStoreName = "data" +) + +// DataStore +type DataStore struct { + contextStoreKey sdk.StoreKey +} + +// NewDataStore returns a new data store object +func NewDataStore(contextStoreKey sdk.StoreKey) DataStore { + return DataStore{ + contextStoreKey: contextStoreKey, + } +} + +// Set sets the key value pair in the store +func (ds DataStore) Set(ctx sdk.Context, key []byte, value []byte) { + store := ctx.KVStore(ds.contextStoreKey) + store.Set(key, value) +} + +// Get returns the value for the provided key from the store +func (ds DataStore) Get(ctx sdk.Context, key []byte) []byte { + store := ctx.KVStore(ds.contextStoreKey) + return store.Get(key) +} + +// Delete removes the provided key value pair from the store +func (ds DataStore) Delete(ctx sdk.Context, key []byte) { + store := ctx.KVStore(ds.contextStoreKey) + store.Delete(key) +} + +// Has returns whether the key exists in the store +func (ds DataStore) Has(ctx sdk.Context, key []byte) bool { + store := ctx.KVStore(ds.contextStoreKey) + return store.Has(key) +} + +// KVStore returns the key value store +func (ds DataStore) KVStore(ctx sdk.Context) sdk.KVStore { + return ctx.KVStore(ds.contextStoreKey) +} diff --git a/store/errors.go b/store/errors.go new file mode 100644 index 0000000..042dee2 --- /dev/null +++ b/store/errors.go @@ -0,0 +1,29 @@ +package store + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Error codes for data store +const ( + DefaultCodespace sdk.CodespaceType = "store" + + CodeDNE sdk.CodeType = 1 + CodeOutputSpent sdk.CodeType = 2 + CodeInvalidPath sdk.CodeType = 3 +) + +// ErrDNE error for an object that does not exist +func ErrDNE(msg string, args ...interface{}) sdk.Error { + return sdk.NewError(DefaultCodespace, CodeDNE, msg, args) +} + +// ErrOutputSpent error for an output that is marked as spent +func ErrOutputSpent(msg string, args ...interface{}) sdk.Error { + return sdk.NewError(DefaultCodespace, CodeOutputSpent, msg, args) +} + +// ErrInvalidPath error for an invalid query path +func ErrInvalidPath(msg string, args ...interface{}) sdk.Error { + return sdk.NewError(DefaultCodespace, CodeInvalidPath, msg, args) +} diff --git a/store/keys.go b/store/keys.go new file mode 100644 index 0000000..94c6a05 --- /dev/null +++ b/store/keys.go @@ -0,0 +1,57 @@ +package store + +import ( + "github.com/FourthState/plasma-mvp-sidechain/plasma" + "github.com/ethereum/go-ethereum/common" + "math/big" +) + +// keys +var ( + walletKey = []byte{0x0} + depositKey = []byte{0x1} + feeKey = []byte{0x2} + txKey = []byte{0x3} + outputKey = []byte{0x4} + blockKey = []byte{0x5} + blockHeightKey = []byte{0x6} +) + +// GetWalletKey returns the key to retrieve wallet for given address. +func GetWalletKey(addr common.Address) []byte { + return prefixKey(walletKey, addr.Bytes()) +} + +// GetDepositKey returns the key to retrieve deposit for given nonce. +func GetDepositKey(nonce *big.Int) []byte { + return prefixKey(depositKey, nonce.Bytes()) +} + +// GetFeeKey returns the key to retrieve fee for given position. +func GetFeeKey(pos plasma.Position) []byte { + return prefixKey(feeKey, pos.Bytes()) +} + +// GetOutputKey returns key to retrieve Output for given position. +func GetOutputKey(pos plasma.Position) []byte { + return prefixKey(outputKey, pos.Bytes()) +} + +// GetTxKey returns key to retrieve Transaction for given hash. +func GetTxKey(hash []byte) []byte { + return prefixKey(txKey, hash) +} + +// GetBlockKey returns the key for the specified height +func GetBlockKey(height *big.Int) []byte { + return prefixKey(blockKey, height.Bytes()) +} + +// GetBlockHeightKey returns the key for the height counter +func GetBlockHeightKey() []byte { + return blockHeightKey +} + +func prefixKey(prefix, key []byte) []byte { + return append(prefix, key...) +} diff --git a/store/outputs.go b/store/outputs.go new file mode 100644 index 0000000..65b3787 --- /dev/null +++ b/store/outputs.go @@ -0,0 +1,381 @@ +package store + +import ( + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/plasma" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rlp" + "math/big" +) + +// ----------------------------------------------------------------------------- +/* Getters */ + +// GetWallet returns the wallet at the associated address. +func (ds DataStore) GetWallet(ctx sdk.Context, addr common.Address) (Wallet, bool) { + key := GetWalletKey(addr) + data := ds.Get(ctx, key) + if data == nil { + return Wallet{}, false + } + + var wallet Wallet + if err := rlp.DecodeBytes(data, &wallet); err != nil { + panic(fmt.Sprintf("transaction store corrupted: %s", err)) + } + + return wallet, true +} + +// GetDeposit returns the deposit at the given nonce. +func (ds DataStore) GetDeposit(ctx sdk.Context, nonce *big.Int) (Deposit, bool) { + key := GetDepositKey(nonce) + data := ds.Get(ctx, key) + if data == nil { + return Deposit{}, false + } + + var deposit Deposit + if err := rlp.DecodeBytes(data, &deposit); err != nil { + panic(fmt.Sprintf("deposit store corrupted: %s", err)) + } + + return deposit, true +} + +// GetFee returns the fee at the given position. +func (ds DataStore) GetFee(ctx sdk.Context, pos plasma.Position) (Output, bool) { + key := GetFeeKey(pos) + data := ds.Get(ctx, key) + if data == nil { + return Output{}, false + } + + var fee Output + if err := rlp.DecodeBytes(data, &fee); err != nil { + panic(fmt.Sprintf("output store corrupted: %s", err)) + } + + return fee, true +} + +// GetOutput returns the output at the given position. +func (ds DataStore) GetOutput(ctx sdk.Context, pos plasma.Position) (Output, bool) { + // allow deposits/fees to returned as an output + if pos.IsDeposit() { + return ds.depositToOutput(ctx, pos.DepositNonce) + } + + if pos.IsFee() { + fee, ok := ds.GetFee(ctx, pos) + if !ok { + return Output{}, ok + } + return fee, ok + } + + key := GetOutputKey(pos) + hash := ds.Get(ctx, key) + + tx, ok := ds.GetTx(ctx, hash) + if !ok { + return Output{}, ok + } + + output := Output{ + Output: tx.Transaction.Outputs[pos.OutputIndex], + Spent: tx.Spent[pos.OutputIndex], + SpenderTx: tx.SpenderTxs[pos.OutputIndex], + } + + return output, ok +} + +// GetTx returns the transaction with the provided transaction hash. +func (ds DataStore) GetTx(ctx sdk.Context, hash []byte) (Transaction, bool) { + key := GetTxKey(hash) + data := ds.Get(ctx, key) + if data == nil { + return Transaction{}, false + } + + var tx Transaction + if err := rlp.DecodeBytes(data, &tx); err != nil { + panic(fmt.Sprintf("transaction store corrupted: %s", err)) + } + + return tx, true +} + +// GetTxWithPosition returns the transaction that contains the provided +// position as an output. +func (ds DataStore) GetTxWithPosition(ctx sdk.Context, pos plasma.Position) (Transaction, bool) { + key := GetOutputKey(pos) + hash := ds.Get(ctx, key) + return ds.GetTx(ctx, hash) +} + +// ----------------------------------------------------------------------------- +/* Has */ + +// HasWallet returns whether an wallet at the given address exists. +func (ds DataStore) HasWallet(ctx sdk.Context, addr common.Address) bool { + key := GetWalletKey(addr) + return ds.Has(ctx, key) +} + +// HasDeposit returns whether a deposit with the given nonce exists. +func (ds DataStore) HasDeposit(ctx sdk.Context, nonce *big.Int) bool { + key := GetDepositKey(nonce) + return ds.Has(ctx, key) +} + +// HasFee returns whether a fee with the given position exists. +func (ds DataStore) HasFee(ctx sdk.Context, pos plasma.Position) bool { + key := GetFeeKey(pos) + return ds.Has(ctx, key) +} + +// HasOutput returns whether an output with the given position exists. +func (ds DataStore) HasOutput(ctx sdk.Context, pos plasma.Position) bool { + key := GetOutputKey(pos) + hash := ds.Get(ctx, key) + + return ds.HasTx(ctx, hash) +} + +// HasTx returns whether a transaction with the given transaction hash +// exists. +func (ds DataStore) HasTx(ctx sdk.Context, hash []byte) bool { + key := GetTxKey(hash) + return ds.Has(ctx, key) +} + +// ----------------------------------------------------------------------------- +/* Set */ + +// setWallet overwrites the wallet stored at the given address. +func (ds DataStore) setWallet(ctx sdk.Context, addr common.Address, wallet Wallet) { + key := GetWalletKey(addr) + data, err := rlp.EncodeToBytes(&wallet) + if err != nil { + panic(fmt.Sprintf("error marshaling wallet with address %s: %s", addr, err)) + } + + ds.Set(ctx, key, data) +} + +// setDeposit overwrites the deposit stored at the given nonce. +func (ds DataStore) setDeposit(ctx sdk.Context, nonce *big.Int, deposit Deposit) { + data, err := rlp.EncodeToBytes(&deposit) + if err != nil { + panic(fmt.Sprintf("error marshaling deposit with nonce %s: %s", nonce, err)) + } + + key := GetDepositKey(nonce) + ds.Set(ctx, key, data) +} + +// setFee overwrites the fee stored at the given position. +func (ds DataStore) setFee(ctx sdk.Context, pos plasma.Position, fee Output) { + data, err := rlp.EncodeToBytes(&fee) + if err != nil { + panic(fmt.Sprintf("error marshaling fee with position %s: %s", pos, err)) + } + + key := GetFeeKey(pos) + ds.Set(ctx, key, data) +} + +// setOutput adds a mapping from position to transaction hash. +func (ds DataStore) setOutput(ctx sdk.Context, pos plasma.Position, hash []byte) { + key := GetOutputKey(pos) + ds.Set(ctx, key, hash) +} + +// setTx overwrites the mapping from transaction hash to transaction. +func (ds DataStore) setTx(ctx sdk.Context, tx Transaction) { + data, err := rlp.EncodeToBytes(&tx) + if err != nil { + panic(fmt.Sprintf("error marshaling transaction: %s", err)) + } + + key := GetTxKey(tx.Transaction.TxHash()) + ds.Set(ctx, key, data) +} + +// ----------------------------------------------------------------------------- +/* Store */ + +// StoreDeposit adds an unspent deposit and updates the deposit owner's +// wallet. +func (ds DataStore) StoreDeposit(ctx sdk.Context, nonce *big.Int, deposit plasma.Deposit) { + ds.setDeposit(ctx, nonce, Deposit{deposit, false, make([]byte, 0)}) + ds.addToWallet(ctx, deposit.Owner, deposit.Amount, plasma.NewPosition(big.NewInt(0), 0, 0, nonce)) +} + +// StoreFee adds an unspent fee and updates the fee owner's wallet. +func (ds DataStore) StoreFee(ctx sdk.Context, blockNum *big.Int, output plasma.Output) { + pos := plasma.NewPosition(blockNum, 1<<16-1, 0, big.NewInt(0)) + ds.setFee(ctx, pos, Output{output, false, make([]byte, 0)}) + ds.addToWallet(ctx, output.Owner, output.Amount, pos) +} + +// StoreTx adds the transaction. +func (ds DataStore) StoreTx(ctx sdk.Context, tx Transaction) { + ds.setTx(ctx, tx) +} + +// StoreOutputs adds new Output UTXO's to respective owner's wallets. +func (ds DataStore) StoreOutputs(ctx sdk.Context, tx Transaction) { + for i, output := range tx.Transaction.Outputs { + ds.addToWallet(ctx, output.Owner, output.Amount, plasma.NewPosition(tx.Position.BlockNum, tx.Position.TxIndex, uint8(i), big.NewInt(0))) + ds.setOutput(ctx, plasma.NewPosition(tx.Position.BlockNum, tx.Position.TxIndex, uint8(i), big.NewInt(0)), tx.Transaction.TxHash()) + } +} + +// ----------------------------------------------------------------------------- +/* Spend */ + +// SpendDeposit changes the deposit to be spent and updates the wallet of +// the deposit owner. +func (ds DataStore) SpendDeposit(ctx sdk.Context, nonce *big.Int, spenderTx []byte) sdk.Result { + deposit, ok := ds.GetDeposit(ctx, nonce) + if !ok { + return ErrDNE(fmt.Sprintf("deposit with nonce %s does not exist", nonce)).Result() + } else if deposit.Spent { + return ErrOutputSpent(fmt.Sprintf("deposit with nonce %s is already spent", nonce)).Result() + } + + deposit.Spent = true + deposit.SpenderTx = spenderTx + + ds.setDeposit(ctx, nonce, deposit) + ds.subtractFromWallet(ctx, deposit.Deposit.Owner, deposit.Deposit.Amount, plasma.NewPosition(big.NewInt(0), 0, 0, nonce)) + + return sdk.Result{} +} + +// SpendFee changes the fee to be spent and updates the wallet of the fee +// owner. +func (ds DataStore) SpendFee(ctx sdk.Context, pos plasma.Position, spenderTx []byte) sdk.Result { + fee, ok := ds.GetFee(ctx, pos) + if !ok { + return ErrDNE(fmt.Sprintf("fee with position %s does not exist", pos)).Result() + } else if fee.Spent { + return ErrOutputSpent(fmt.Sprintf("fee with position %s is already spent", pos)).Result() + } + + fee.Spent = true + fee.SpenderTx = spenderTx + + ds.setFee(ctx, pos, fee) + ds.subtractFromWallet(ctx, fee.Output.Owner, fee.Output.Amount, pos) + + return sdk.Result{} +} + +// SpendOutput changes the output to be spent and updates the wallet of the +// output owner. +func (ds DataStore) SpendOutput(ctx sdk.Context, pos plasma.Position, spenderTx []byte) sdk.Result { + key := GetOutputKey(pos) + hash := ds.Get(ctx, key) + + tx, ok := ds.GetTx(ctx, hash) + if !ok { + return ErrDNE(fmt.Sprintf("output with index %x and transaction hash 0x%x does not exist", pos.OutputIndex, hash)).Result() + } else if tx.Spent[pos.OutputIndex] { + return ErrOutputSpent(fmt.Sprintf("output with index %x and transaction hash 0x%x is already spent", pos.OutputIndex, hash)).Result() + } + + tx.Spent[pos.OutputIndex] = true + tx.SpenderTxs[pos.OutputIndex] = spenderTx + + ds.setTx(ctx, tx) + ds.subtractFromWallet(ctx, tx.Transaction.Outputs[pos.OutputIndex].Owner, tx.Transaction.Outputs[pos.OutputIndex].Amount, pos) + + return sdk.Result{} +} + +// GetUnspentForWallet returns the unspent outputs that belong to the given +// wallet. Returns the struct TxOutput so the user has access to the +// transactional information related to the output. +func (ds DataStore) GetUnspentForWallet(ctx sdk.Context, wallet Wallet) (utxos []TxOutput) { + for _, p := range wallet.Unspent { + output, ok := ds.GetOutput(ctx, p) + if !ok { + panic(fmt.Sprintf("Corrupted store: Wallet contains unspent position (%v) that doesn't exist in store", p)) + } + tx, ok := ds.GetTxWithPosition(ctx, p) + if !ok { + panic(fmt.Sprintf("Corrupted store: Wallet contains unspent position (%v) that doesn't have corresponding tx", p)) + } + + txo := NewTxOutput(output.Output, p, tx.ConfirmationHash, tx.Transaction.TxHash(), output.Spent, output.SpenderTx) + utxos = append(utxos, txo) + } + return utxos +} + +// ----------------------------------------------------------------------------- +/* Helpers */ + +// depositToOutput retrieves the deposit with the given nonce, and returns +// it as an output. +func (ds DataStore) depositToOutput(ctx sdk.Context, nonce *big.Int) (Output, bool) { + deposit, ok := ds.GetDeposit(ctx, nonce) + if !ok { + return Output{}, ok + } + output := Output{ + Output: plasma.NewOutput(deposit.Deposit.Owner, deposit.Deposit.Amount), + Spent: deposit.Spent, + SpenderTx: deposit.SpenderTx, + } + return output, ok +} + +// addToWallet adds the passed in amount to the wallet with the given +// address and adds the position provided to the list of unspent positions +// within the wallet. +func (ds DataStore) addToWallet(ctx sdk.Context, addr common.Address, amount *big.Int, pos plasma.Position) { + wallet, ok := ds.GetWallet(ctx, addr) + if !ok { + wallet = Wallet{big.NewInt(0), make([]plasma.Position, 0), make([]plasma.Position, 0)} + } + + wallet.Balance = new(big.Int).Add(wallet.Balance, amount) + wallet.Unspent = append(wallet.Unspent, pos) + ds.setWallet(ctx, addr, wallet) +} + +// subtractFromWallet subtracts the passed in amount from the wallet with +// the given address and moves the provided position from the unspent list +// to the spent list. +func (ds DataStore) subtractFromWallet(ctx sdk.Context, addr common.Address, amount *big.Int, pos plasma.Position) { + wallet, ok := ds.GetWallet(ctx, addr) + if !ok { + panic(fmt.Sprintf("output store has been corrupted")) + } + + // Update Wallet + wallet.Balance = new(big.Int).Sub(wallet.Balance, amount) + if wallet.Balance.Sign() == -1 { + panic(fmt.Sprintf("wallet with address 0x%x has a negative balance", addr)) + } + + wallet.Unspent = removePosition(wallet.Unspent, pos) + wallet.Spent = append(wallet.Spent, pos) + ds.setWallet(ctx, addr, wallet) +} + +// helper function to remove a position from the unspent list. +func removePosition(positions []plasma.Position, pos plasma.Position) []plasma.Position { + for i, p := range positions { + if p.String() == pos.String() { + positions = append(positions[:i], positions[i+1:]...) + } + } + return positions +} diff --git a/store/outputs_test.go b/store/outputs_test.go new file mode 100644 index 0000000..f980dec --- /dev/null +++ b/store/outputs_test.go @@ -0,0 +1,224 @@ +package store + +import ( + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/plasma" + "github.com/FourthState/plasma-mvp-sidechain/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" + "math/big" + "reflect" + "testing" +) + +// Test Get, Has, Store, Spend functions for deposits +func TestDeposits(t *testing.T) { + ctx, key := setup() + outputStore := NewDataStore(key) + + addr := common.BytesToAddress([]byte("asdfasdf")) + for i := int64(1); i <= 15; i++ { + nonce := big.NewInt(i) + hash := []byte("hash that the deposit was spent in") + + // Retrieve/Spend nonexistent deposits + exists := outputStore.HasDeposit(ctx, nonce) + require.False(t, exists, "returned true for nonexistent deposit") + recoveredDeposit, ok := outputStore.GetDeposit(ctx, nonce) + require.Empty(t, recoveredDeposit, "did not return empty struct for nonexistent deposit") + require.False(t, ok, "did not return error on nonexistent deposit") + res := outputStore.SpendDeposit(ctx, nonce, hash) + require.Equal(t, res.Code, CodeDNE, "did not return that deposit does not exist") + + // Create and store new deposit + plasmaDeposit := plasma.NewDeposit(addr, big.NewInt(i*4123), big.NewInt(i*123)) + deposit := Deposit{plasmaDeposit, false, []byte{}} + outputStore.StoreDeposit(ctx, nonce, plasmaDeposit) + + exists = outputStore.HasDeposit(ctx, nonce) + require.True(t, exists, "returned false for deposit that was stored") + recoveredDeposit, ok = outputStore.GetDeposit(ctx, nonce) + require.True(t, ok, "error when retrieving deposit") + require.True(t, reflect.DeepEqual(deposit, recoveredDeposit), fmt.Sprintf("mismatch in stored deposit and retrieved deposit on iteration %d", i)) + + // Spend Deposit + res = outputStore.SpendDeposit(ctx, nonce, hash) + require.True(t, res.IsOK(), "returned error when spending deposit") + res = outputStore.SpendDeposit(ctx, nonce, hash) + require.Equal(t, res.Code, CodeOutputSpent, "allowed output to be spent twice") + + deposit.Spent = true + deposit.SpenderTx = hash + recoveredDeposit, ok = outputStore.GetDeposit(ctx, nonce) + require.True(t, ok, "error when retrieving deposit") + require.True(t, reflect.DeepEqual(deposit, recoveredDeposit), "mismatch in stored and retrieved deposit") + } +} + +// Test Get, Has, Store, Spend functions for fees +func TestFees(t *testing.T) { + ctx, key := setup() + outputStore := NewDataStore(key) + + addr := common.BytesToAddress([]byte("asdfasdf")) + for i := int64(1); i <= 15; i++ { + pos := plasma.NewPosition(big.NewInt(int64(i)), 1<<16-1, 0, big.NewInt(0)) + hash := []byte("hash that the fee was spent in") + + // Retrieve/Spend nonexistent fee + exists := outputStore.HasFee(ctx, pos) + require.False(t, exists, "returned true for nonexistent fee") + recoveredFee, ok := outputStore.GetFee(ctx, pos) + require.Empty(t, recoveredFee, "did not return empty struct for nonexistent fee") + require.False(t, ok, "did not return error on nonexistent fee") + res := outputStore.SpendFee(ctx, pos, hash) + require.Equal(t, res.Code, CodeDNE, "did not return that fee does not exist") + + // Create and store new fee + output := plasma.NewOutput(addr, big.NewInt(int64(1000*i))) + fee := Output{output, false, make([]byte, 0)} + outputStore.StoreFee(ctx, pos.BlockNum, output) + + exists = outputStore.HasFee(ctx, pos) + require.True(t, exists, "returned false for fee that was stored") + recoveredFee, ok = outputStore.GetFee(ctx, pos) + require.True(t, ok, "error when retrieving fee") + require.True(t, reflect.DeepEqual(fee, recoveredFee), fmt.Sprintf("mismatch in stored fee and retrieved fee on iteration %d", i)) + + // Spend Fee + res = outputStore.SpendFee(ctx, pos, hash) + require.True(t, res.IsOK(), "returned error when spending fee") + res = outputStore.SpendFee(ctx, pos, hash) + require.Equal(t, res.Code, CodeOutputSpent, "allowed output to be spent twice") + + fee.Spent = true + fee.SpenderTx = hash + recoveredFee, ok = outputStore.GetFee(ctx, pos) + require.True(t, ok, "error when retrieving fee") + require.True(t, reflect.DeepEqual(fee, recoveredFee), "mismatch in stored and retrieved fee") + } +} + +// Test Get, Has, Store, Spend functions for transactions +func TestTransactions(t *testing.T) { + // Setup + ctx, key := setup() + outputStore := NewDataStore(key) + + privKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privKey.PublicKey) + + sampleSig := [65]byte{} + sampleSig[0] = byte(1) + sampleConfirmSig := [][65]byte{sampleSig} + confirmationHash := []byte("confirmation hash") + + type validationCase struct { + reason string + plasma.Transaction + plasma.Position + } + + txs := []validationCase{ + validationCase{ + reason: "tx with two inputs and one output", + Transaction: plasma.Transaction{ + Inputs: []plasma.Input{plasma.NewInput(getPosition("(122.3.1.0)"), sampleSig, nil), plasma.NewInput(getPosition("(17622.13.5.0)"), sampleSig, nil)}, + Outputs: []plasma.Output{plasma.NewOutput(addr, utils.Big1)}, + Fee: utils.Big0, + }, + Position: getPosition("(8765.6847.0.0)"), + }, + validationCase{ + reason: "tx with one input and two output", + Transaction: plasma.Transaction{ + Inputs: []plasma.Input{plasma.NewInput(getPosition("(4.1234.1.0)"), sampleSig, nil)}, + Outputs: []plasma.Output{plasma.NewOutput(addr, utils.Big1), plasma.NewOutput(addr, utils.Big1)}, + Fee: utils.Big0, + }, + Position: getPosition("(4354.8765.0.0)"), + }, + validationCase{ + reason: "tx with two input and one output", + Transaction: plasma.Transaction{ + Inputs: []plasma.Input{plasma.NewInput(getPosition("(123.1.0.0)"), sampleSig, nil), plasma.NewInput(getPosition("(10.12.1.0)"), sampleSig, sampleConfirmSig)}, + Outputs: []plasma.Output{plasma.NewOutput(addr, utils.Big1)}, + Fee: utils.Big0, + }, + Position: getPosition("(11.123.0.0)"), + }, + validationCase{ + reason: "tx with two input and two outputs", + Transaction: plasma.Transaction{ + Inputs: []plasma.Input{plasma.NewInput(getPosition("(132.231.1.0)"), sampleSig, nil), plasma.NewInput(getPosition("(635.927.1.0)"), sampleSig, sampleConfirmSig)}, + Outputs: []plasma.Output{plasma.NewOutput(addr, utils.Big1), plasma.NewOutput(addr, utils.Big1)}, + Fee: utils.Big0, + }, + Position: getPosition("(1121234.12.0.0)"), + }, + } + + for i, plasmaTx := range txs { + pos := plasmaTx.Position + // Retrieve/Spend nonexistent txs + exists := outputStore.HasTx(ctx, plasmaTx.Transaction.TxHash()) + require.False(t, exists, "returned true for nonexistent transaction") + recoveredTx, ok := outputStore.GetTx(ctx, plasmaTx.Transaction.TxHash()) + require.Empty(t, recoveredTx, "did not return empty struct for nonexistent transaction") + require.False(t, ok, "did not return error on nonexistent transaction") + + // Retrieve/Spend nonexistent Outputs + for j, _ := range plasmaTx.Transaction.Outputs { + p := plasma.NewPosition(pos.BlockNum, pos.TxIndex, uint8(j), pos.DepositNonce) + exists = outputStore.HasOutput(ctx, p) + require.False(t, exists, "returned true for nonexistent output") + res := outputStore.SpendOutput(ctx, p, plasmaTx.Transaction.MerkleHash()) + require.Equal(t, res.Code, CodeDNE, "did not return that Output does not exist") + } + + // Create and store new transaction + tx := Transaction{plasmaTx.Transaction, confirmationHash, make([]bool, len(plasmaTx.Transaction.Outputs)), make([][]byte, len(plasmaTx.Transaction.Outputs)), plasmaTx.Position} + for i, _ := range tx.SpenderTxs { + tx.SpenderTxs[i] = []byte{} + } + outputStore.StoreTx(ctx, tx) + outputStore.StoreOutputs(ctx, tx) + + // Check for outputs + for j, _ := range plasmaTx.Transaction.Outputs { + p := plasma.NewPosition(pos.BlockNum, pos.TxIndex, uint8(j), pos.DepositNonce) + exists = outputStore.HasOutput(ctx, p) + require.True(t, exists, fmt.Sprintf("returned false for stored output with index %d on case %d", j, i)) + } + + // Check for Tx + exists = outputStore.HasTx(ctx, plasmaTx.Transaction.TxHash()) + require.True(t, exists, "returned false for transaction that was stored") + recoveredTx, ok = outputStore.GetTx(ctx, plasmaTx.Transaction.TxHash()) + require.True(t, ok, "error when retrieving transaction") + require.Equal(t, tx, recoveredTx, fmt.Sprintf("mismatch in stored transaction and retrieved transaction before spends on case %d", i)) + + // Spend Outputs + for j, _ := range plasmaTx.Transaction.Outputs { + p := plasma.NewPosition(pos.BlockNum, pos.TxIndex, uint8(j), pos.DepositNonce) + recoveredTx, ok = outputStore.GetTxWithPosition(ctx, p) + require.True(t, ok, "error when retrieving transaction") + require.True(t, reflect.DeepEqual(tx, recoveredTx), fmt.Sprintf("mismatch in stored transaction and retrieved transaction on case %d", i)) + + res := outputStore.SpendOutput(ctx, p, plasmaTx.Transaction.MerkleHash()) + require.True(t, res.IsOK(), "returned error when spending output") + res = outputStore.SpendOutput(ctx, p, plasmaTx.Transaction.MerkleHash()) + require.Equal(t, res.Code, CodeOutputSpent, fmt.Sprintf("allowed output with index %d to be spent twice on case %d", j, i)) + + tx.Spent[j] = true + tx.SpenderTxs[j] = plasmaTx.Transaction.MerkleHash() + recoveredTx, ok = outputStore.GetTxWithPosition(ctx, p) + require.True(t, ok, "error when retrieving transaction") + require.True(t, reflect.DeepEqual(tx, recoveredTx), fmt.Sprintf("mismatch in stored and retrieved transaction on case %d", i)) + recoveredTx, ok = outputStore.GetTx(ctx, plasmaTx.Transaction.TxHash()) + require.True(t, ok, "error when retrieving transaction") + require.True(t, reflect.DeepEqual(tx, recoveredTx), fmt.Sprintf("mismatch in stored and retrieved transaction on case %d", i)) + } + } +} diff --git a/store/querier.go b/store/querier.go new file mode 100644 index 0000000..8569ac7 --- /dev/null +++ b/store/querier.go @@ -0,0 +1,300 @@ +package store + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "github.com/FourthState/plasma-mvp-sidechain/plasma" + "github.com/FourthState/plasma-mvp-sidechain/utils" + sdk "github.com/cosmos/cosmos-sdk/types" + ethcmn "github.com/ethereum/go-ethereum/common" + abci "github.com/tendermint/tendermint/abci/types" + "math/big" +) + +const ( + // RouteName to mount the querier + QuerierRouteName = DataStoreName + + /*** block routes ***/ + + // QueryHeight retrieves the current block height + QueryHeight = "height" + + // QueryBlocks retrieves full information about a + // speficied block + QueryBlock = "block" + + // QueryBlocks retrieves metadata about 10 blocks from + // a specified start point or the last 10 from the latest + // block + QueryBlocks = "blocks" + + /*** output routes ***/ + + // QueryBalance retrieves the aggregate value of + // the set of owned by the specified address + QueryBalance = "balance" + + // QueryInfo retrieves the entire output set owned + // by the specified address + QueryInfo = "info" + + // QueryTxOutput retrieves a single output at + // the given position and returns it with transactional + // information + QueryTxOutput = "output" + + // QueryTxInput retrieves basic transaction data at + // given position along with input information + QueryTxInput = "input" + + // QueryTx retrieves a transaction at the given hash + QueryTx = "tx" +) + +// NewQuerier returns an SDK querier to interact with the store +func NewQuerier(ds DataStore) sdk.Querier { + return func(ctx sdk.Context, path []string, req abci.RequestQuery) ([]byte, sdk.Error) { + if len(path) == 0 { + return nil, ErrInvalidPath("path not specified") + } + + switch path[0] { + case QueryHeight: + return queryHeight(ctx, ds) + case QueryBlock: + return queryBlock(ctx, ds, path[1:]) + case QueryBlocks: + return queryBlocks(ctx, ds, path[1:]) + case QueryBalance: + return queryBalance(ctx, ds, path[1:]) + case QueryInfo: + return queryInfo(ctx, ds, path[1:]) + case QueryTxOutput: + return queryTxOutput(ctx, ds, path[1:]) + case QueryTxInput: + return queryTxInput(ctx, ds, path[1:]) + case QueryTx: + return queryTx(ctx, ds, path[1:]) + default: + return nil, ErrInvalidPath("unregistered query path") + } + } +} + +func queryHeight(ctx sdk.Context, ds DataStore) ([]byte, sdk.Error) { + height := ds.PlasmaBlockHeight(ctx) + if height == nil { + height = utils.Big0 + } + + return []byte(height.String()), nil +} + +func queryBlock(ctx sdk.Context, ds DataStore, path []string) ([]byte, sdk.Error) { + if len(path) != 1 { + return nil, ErrInvalidPath("expected %s/", QueryBlock) + } + + height, err := parseHeight(path[0]) + if err != nil { + return nil, err + } + + block, ok := ds.GetBlock(ctx, height) + if !ok { + return nil, ErrDNE("plasma block %s does not exist", height) + } + + return marshalResponse(block) +} + +func queryBlocks(ctx sdk.Context, ds DataStore, path []string) ([]byte, sdk.Error) { + if len(path) != 1 { + return nil, ErrInvalidPath("expected %s/ or %s/latest", QueryBlocks, QueryBlocks) + } + + var height *big.Int + if path[0] == "latest" { + // latest 10 blocks + height = ds.PlasmaBlockHeight(ctx) + if height == nil { + return nil, ErrDNE("no blocks") + } + + bigNine := big.NewInt(9) + if height.Cmp(bigNine) <= 0 { + height = big.NewInt(1) + } else { + height = height.Sub(height, bigNine) + } + } else { + // predefined starting point + h, err := parseHeight(path[0]) + if err != nil { + return nil, err + } + height = h + } + + var blocks []Block + for i := 0; i < 10; i++ { + block, ok := ds.GetBlock(ctx, height) + if !ok { + break + } + blocks = append(blocks, block) + height = height.Add(height, utils.Big1) + } + + if len(blocks) != 0 { + return nil, ErrDNE("no blocks") + } + + return marshalResponse(blocks) +} + +func queryBalance(ctx sdk.Context, ds DataStore, path []string) ([]byte, sdk.Error) { + if len(path) != 1 { + return nil, ErrInvalidPath("expected %s/
", QueryBalance) + } + + addr, err := parseAddress(path[0]) + if err != nil { + return nil, err + } + + acc, ok := ds.GetWallet(ctx, addr) + if !ok { + return nil, ErrDNE("no wallet exists for the address provided: 0x%x", addr) + } + + return []byte(acc.Balance.String()), nil +} + +func queryInfo(ctx sdk.Context, ds DataStore, path []string) ([]byte, sdk.Error) { + if len(path) != 1 { + return nil, ErrInvalidPath("expected %s/
", QueryInfo) + } + + addr, err := parseAddress(path[0]) + if err != nil { + return nil, err + } + + acc, ok := ds.GetWallet(ctx, addr) + if !ok { + return nil, ErrDNE("no wallet exists for the address provided: 0x%x", addr) + } + + outputs := ds.GetUnspentForWallet(ctx, acc) + return marshalResponse(outputs) +} + +func queryTxOutput(ctx sdk.Context, ds DataStore, path []string) ([]byte, sdk.Error) { + if len(path) != 1 { + return nil, ErrInvalidPath("expected %s/", QueryTxOutput) + } + + pos, err := plasma.FromPositionString(path[0]) + if err != nil { + return nil, ErrInvalidPath("position is encoded in the format (blocknum,txIndex,oIndex,depositNonce)") + } + + o, ok := ds.GetOutput(ctx, pos) + if !ok { + return nil, ErrDNE("no output exists for the position provided: %s", pos) + } + + tx, ok := ds.GetTxWithPosition(ctx, pos) + if !ok { + return nil, ErrDNE("no transaction exists for the position provided: %s", pos) + } + + txo := NewTxOutput(o.Output, pos, tx.ConfirmationHash, tx.Transaction.TxHash(), o.Spent, o.SpenderTx) + return marshalResponse(txo) +} + +func queryTxInput(ctx sdk.Context, ds DataStore, path []string) ([]byte, sdk.Error) { + if len(path) != 1 { + return nil, ErrInvalidPath("expected %s/", QueryTxInput) + } + + pos, err := plasma.FromPositionString(path[0]) + if err != nil { + return nil, ErrInvalidPath("position is encoded in the format (blocknum,txIndex,oIndex,depositNonce)") + } + + tx, ok := ds.GetTxWithPosition(ctx, pos) + if !ok { + return nil, ErrDNE("no transaction exists for the position provided: %s", pos) + } + + o, ok := ds.GetOutput(ctx, pos) + if !ok { + return nil, ErrDNE("no output exists for the position provided: %s", pos) + } + + inputPositions := tx.Transaction.InputPositions() + var inputAddresses []ethcmn.Address + for _, inPos := range inputPositions { + input, ok := ds.GetOutput(ctx, inPos) + if !ok { + panic(fmt.Sprintf("Corrupted store: input position for given transaction does not exist: %s", pos)) + } + inputAddresses = append(inputAddresses, input.Output.Owner) + } + + txinput := NewTxInput(o.Output, pos, tx.Transaction.TxHash(), inputAddresses, inputPositions) + return marshalResponse(txinput) +} + +func queryTx(ctx sdk.Context, ds DataStore, path []string) ([]byte, sdk.Error) { + if len(path) != 1 { + return nil, ErrInvalidPath("expected %s/", QueryTx) + } + txHash, err := hex.DecodeString(utils.RemoveHexPrefix(path[0])) + if err != nil { + return nil, ErrInvalidPath("tx hash expected in hexadecimal format. hex: %s", err) + } else if len(txHash) != 32 { + return nil, ErrInvalidPath("tx hash expected to be 32 bytes in length") + } + + tx, ok := ds.GetTx(ctx, txHash) + if !ok { + return nil, ErrDNE("no transaction exists for the hash provided: %s", txHash) + } + + return marshalResponse(tx) +} + +/** helpers **/ + +func marshalResponse(resp interface{}) ([]byte, sdk.Error) { + data, err := json.Marshal(resp) + if err != nil { + return nil, sdk.ErrInternal(fmt.Sprintf("json: %s", err)). + WithDefaultCodespace(DefaultCodespace) + } + return data, nil +} + +func parseHeight(height string) (*big.Int, sdk.Error) { + h, ok := new(big.Int).SetString(height, 10) + if !ok || h.Sign() <= 0 { + return nil, ErrInvalidPath("block height must start from 1 in decimal format") + } + + return h, nil +} + +func parseAddress(addr string) (ethcmn.Address, sdk.Error) { + addr = utils.RemoveHexPrefix(addr) + + if !ethcmn.IsHexAddress(addr) { + return utils.ZeroAddress, ErrInvalidPath("address expected to be a valid 20-byte ethereum address") + } + + return ethcmn.HexToAddress(addr), nil +} diff --git a/store/types.go b/store/types.go new file mode 100644 index 0000000..876b832 --- /dev/null +++ b/store/types.go @@ -0,0 +1,88 @@ +package store + +import ( + "github.com/FourthState/plasma-mvp-sidechain/plasma" + ethcmn "github.com/ethereum/go-ethereum/common" + "math/big" +) + +// Wallet holds reference to the total balance, unspent, and spent outputs +// at a given address +type Wallet struct { + Balance *big.Int // total amount available to be spent + Unspent []plasma.Position // position of unspent transaction outputs + Spent []plasma.Position // position of spent transaction outputs +} + +// Deposit wraps a plasma deposit with spend information. +type Deposit struct { + Deposit plasma.Deposit + Spent bool + SpenderTx []byte // transaction hash that spends this deposit +} + +// Output wraps a plasma output with spend information. +type Output struct { + Output plasma.Output + Spent bool + SpenderTx []byte // transaction hash that spent this output +} + +// Transaction wraps a plasma transaction with spend information. +type Transaction struct { + Transaction plasma.Transaction + ConfirmationHash []byte + Spent []bool + SpenderTxs [][]byte // transaction hashes that spend the outputs of this transaction + Position plasma.Position +} + +// TxOutput holds all transactional information related to an output. +type TxOutput struct { + plasma.Output + Position plasma.Position + ConfirmationHash []byte + TxHash []byte + Spent bool + SpenderTx []byte +} + +// NewTxOutput creates a TxOutput object. +func NewTxOutput(output plasma.Output, pos plasma.Position, confirmationHash, txHash []byte, + spent bool, spenderTx []byte) TxOutput { + return TxOutput{ + Output: output, + Position: pos, + ConfirmationHash: confirmationHash, + TxHash: txHash, + Spent: spent, + SpenderTx: spenderTx, + } +} + +// TxInput holds basic transactional data along with input information +type TxInput struct { + plasma.Output + Position plasma.Position + TxHash []byte + InputAddresses []ethcmn.Address + InputPositions []plasma.Position +} + +// NewTxInput creates a TxInput object. +func NewTxInput(output plasma.Output, pos plasma.Position, txHash []byte, + inputAddresses []ethcmn.Address, inputPositions []plasma.Position) TxInput { + return TxInput{ + Output: output, + Position: pos, + TxHash: txHash, + InputAddresses: inputAddresses, + InputPositions: inputPositions, + } +} + +// Block wraps a plasma block with the tendermint block height. +type Block struct { + plasma.Block + TMBlockHeight uint64 +} diff --git a/store/types_test.go b/store/types_test.go new file mode 100644 index 0000000..f015176 --- /dev/null +++ b/store/types_test.go @@ -0,0 +1,88 @@ +package store + +import ( + "github.com/FourthState/plasma-mvp-sidechain/plasma" + "github.com/FourthState/plasma-mvp-sidechain/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rlp" + "github.com/stretchr/testify/require" + "math/big" + "reflect" + "testing" +) + +// Test that an wallet can be serialized and deserialized +func TestWalletSerialization(t *testing.T) { + // Construct Wallet + acc := Wallet{ + Balance: big.NewInt(234578), + Unspent: []plasma.Position{getPosition("(8745.1239.1.0)"), getPosition("(23409.12456.0.0)"), getPosition("(894301.1.1.0)"), getPosition("(0.0.0.540124)")}, + Spent: []plasma.Position{getPosition("(0.0.0.3)"), getPosition("(7.734.1.3)")}, + } + + bytes, err := rlp.EncodeToBytes(&acc) + require.NoError(t, err) + + recoveredAcc := Wallet{} + err = rlp.DecodeBytes(bytes, &recoveredAcc) + require.NoError(t, err) + + require.True(t, reflect.DeepEqual(acc, recoveredAcc), "mismatch in serialized and deserialized wallet") +} + +// Test that the Deposit can be serialized and deserialized without loss of information +func TestDepositSerialization(t *testing.T) { + // Construct deposit + plasmaDeposit := plasma.Deposit{ + Owner: common.BytesToAddress([]byte("an ethereum address")), + Amount: big.NewInt(12312310), + EthBlockNum: big.NewInt(100123123), + } + + deposit := Deposit{ + Deposit: plasmaDeposit, + Spent: true, + SpenderTx: []byte{}, + } + + bytes, err := rlp.EncodeToBytes(&deposit) + require.NoError(t, err) + + recoveredDeposit := Deposit{} + err = rlp.DecodeBytes(bytes, &recoveredDeposit) + require.NoError(t, err) + + require.True(t, reflect.DeepEqual(deposit, recoveredDeposit), "mismatch in serialized and deserialized deposit") +} + +// Test that Transaction can be serialized and deserialized without loss of information +func TestTxSerialization(t *testing.T) { + hashes := []byte("fourthstate") + var sigs [65]byte + + // Construct Transaction + transaction := plasma.Transaction{ + Inputs: []plasma.Input{plasma.NewInput(getPosition("(1.15.1.0)"), sigs, [][65]byte{}), plasma.NewInput(getPosition("(0.0.0.1)"), sigs, [][65]byte{})}, + Outputs: []plasma.Output{plasma.NewOutput(common.HexToAddress("1"), utils.Big1), plasma.NewOutput(common.HexToAddress("2"), utils.Big2)}, + Fee: utils.Big1, + } + + pos := getPosition("(0.1.1.1)") + + tx := Transaction{ + Transaction: transaction, + Spent: []bool{false, false}, + SpenderTxs: [][]byte{}, + ConfirmationHash: hashes, + Position: pos, + } + + bytes, err := rlp.EncodeToBytes(&tx) + require.NoError(t, err) + + recoveredTx := Transaction{} + err = rlp.DecodeBytes(bytes, &recoveredTx) + require.NoError(t, err) + + require.True(t, reflect.DeepEqual(tx, recoveredTx), "mismatch in serialized and deserialized transactions") +} diff --git a/types/errors.go b/types/errors.go deleted file mode 100644 index f781e86..0000000 --- a/types/errors.go +++ /dev/null @@ -1,33 +0,0 @@ -package types - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" -) - -// Reserve errors 100 ~ 199 -const ( - DefaultCodespace sdk.CodespaceType = 3 - - CodeInvalidAddress sdk.CodeType = 201 - CodeInvalidOIndex sdk.CodeType = 202 - CodeInvalidAmount sdk.CodeType = 203 - CodeInvalidTransaction sdk.CodeType = 204 -) - -//---------------------------------------- -// Error constructors -func ErrInvalidTransaction(codespace sdk.CodespaceType, msg string, args ...interface{}) sdk.Error { - return sdk.NewError(codespace, CodeInvalidTransaction, msg, args) -} - -func ErrInvalidAddress(codespace sdk.CodespaceType, msg string, args ...interface{}) sdk.Error { - return sdk.NewError(codespace, CodeInvalidAddress, msg, args) -} - -func ErrInvalidOIndex(codespace sdk.CodespaceType, msg string) sdk.Error { - return sdk.NewError(codespace, CodeInvalidOIndex, msg) -} - -func ErrInvalidAmount(codespace sdk.CodespaceType, msg string) sdk.Error { - return sdk.NewError(codespace, CodeInvalidAmount, msg) -} diff --git a/types/tx.go b/types/tx.go deleted file mode 100644 index 42cadb8..0000000 --- a/types/tx.go +++ /dev/null @@ -1,140 +0,0 @@ -package types - -import ( - "fmt" - utils "github.com/FourthState/plasma-mvp-sidechain/utils" - utxo "github.com/FourthState/plasma-mvp-sidechain/x/utxo" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/common" - rlp "github.com/ethereum/go-ethereum/rlp" -) - -var _ utxo.SpendMsg = SpendMsg{} - -type SpendMsg struct { - Blknum0 uint64 - Txindex0 uint16 - Oindex0 uint8 - DepositNum0 uint64 - Owner0 common.Address - Input0ConfirmSigs [][65]byte - Blknum1 uint64 - Txindex1 uint16 - Oindex1 uint8 - DepositNum1 uint64 - Owner1 common.Address - Input1ConfirmSigs [][65]byte - Newowner0 common.Address - Amount0 uint64 - Newowner1 common.Address - Amount1 uint64 - FeeAmount uint64 -} - -// Implements Msg. Improve later -func (msg SpendMsg) Type() string { return "spend_utxo" } - -// Implements Msg. -func (msg SpendMsg) Route() string { return "spend" } - -// Implements Msg. -func (msg SpendMsg) ValidateBasic() sdk.Error { - if !utils.ValidAddress(msg.Owner0) { - return ErrInvalidAddress(DefaultCodespace, "input owner must have a valid address", msg.Owner0) - } - if !utils.ValidAddress(msg.Newowner0) { - return ErrInvalidAddress(DefaultCodespace, "no recipients of transaction") - } - if msg.Blknum0 == msg.Blknum1 && msg.Txindex0 == msg.Txindex1 && msg.Oindex0 == msg.Oindex1 && msg.DepositNum0 == msg.DepositNum1 { - return ErrInvalidTransaction(DefaultCodespace, fmt.Sprintf("cannot spend same position twice: (%d, %d, %d, %d)", msg.Blknum0, msg.Txindex0, msg.Oindex0, msg.DepositNum0)) - - } - - switch { - - case msg.Oindex0 != 0 && msg.Oindex0 != 1: - return ErrInvalidOIndex(DefaultCodespace, "output index 0 must be either 0 or 1") - - case msg.DepositNum0 != 0 && (msg.Blknum0 != 0 || msg.Txindex0 != 0 || msg.Oindex0 != 0): - return ErrInvalidTransaction(DefaultCodespace, "first input is malformed. Deposit's position must be 0, 0, 0") - - case msg.DepositNum1 != 0 && (msg.Blknum1 != 0 || msg.Txindex1 != 0 || msg.Oindex1 != 0): - return ErrInvalidTransaction(DefaultCodespace, "second input is malformed. Deposit's position must be 0, 0, 0") - - case msg.Blknum1 != 0 && msg.Oindex1 != 0 && msg.Oindex1 != 1: - return ErrInvalidOIndex(DefaultCodespace, "output index 1 must be either 0 or 1") - - case msg.Amount0 == 0: - return ErrInvalidAmount(DefaultCodespace, "first amount must be positive") - } - - return nil -} - -// Implements Msg. -func (msg SpendMsg) GetSignBytes() []byte { - b, err := rlp.EncodeToBytes(msg) - if err != nil { - panic(err) - } - return b -} - -// Implements Msg. -func (msg SpendMsg) GetSigners() []sdk.AccAddress { - addrs := make([]sdk.AccAddress, 1) - addrs[0] = sdk.AccAddress(msg.Owner0.Bytes()) - if utils.ValidAddress(msg.Owner1) { - addrs = append(addrs, sdk.AccAddress(msg.Owner1.Bytes())) - } - return addrs -} - -func (msg SpendMsg) Inputs() []utxo.Input { - inputs := []utxo.Input{utxo.Input{ - Owner: msg.Owner0.Bytes(), - Position: NewPlasmaPosition(msg.Blknum0, msg.Txindex0, msg.Oindex0, msg.DepositNum0), - }} - if NewPlasmaPosition(msg.Blknum1, msg.Txindex1, msg.Oindex1, msg.DepositNum1).IsValid() { - // Add valid second input - inputs = append(inputs, utxo.Input{ - Owner: msg.Owner1.Bytes(), - Position: NewPlasmaPosition(msg.Blknum1, msg.Txindex1, msg.Oindex1, msg.DepositNum1), - }) - } - return inputs -} - -func (msg SpendMsg) Outputs() []utxo.Output { - outputs := []utxo.Output{utxo.Output{msg.Newowner0.Bytes(), Denom, msg.Amount0}} - if msg.Amount1 != 0 { - outputs = append(outputs, utxo.Output{msg.Newowner1.Bytes(), Denom, msg.Amount1}) - } - return outputs -} - -func (msg SpendMsg) Fee() []utxo.Output { - return []utxo.Output{utxo.Output{ - Denom: Denom, - Amount: msg.FeeAmount, - }} -} - -//---------------------------------------- -// BaseTx -var _ sdk.Tx = BaseTx{} - -type BaseTx struct { - Msg SpendMsg - Signatures [2][65]byte -} - -func NewBaseTx(msg SpendMsg, sigs [2][65]byte) BaseTx { - return BaseTx{ - Msg: msg, - Signatures: sigs, - } -} - -func (tx BaseTx) GetMsgs() []sdk.Msg { return []sdk.Msg{tx.Msg} } -func (tx BaseTx) GetSignatures() [2][65]byte { return tx.Signatures } diff --git a/types/tx_test.go b/types/tx_test.go deleted file mode 100644 index 6b93f7c..0000000 --- a/types/tx_test.go +++ /dev/null @@ -1,149 +0,0 @@ -package types - -import ( - "github.com/stretchr/testify/require" - "testing" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/common" - ethcrypto "github.com/ethereum/go-ethereum/crypto" - - "github.com/FourthState/plasma-mvp-sidechain/utils" -) - -func GenBasicSpendMsg() SpendMsg { - // Creates Basic Spend Msg with no owners or recipients - var confirmSigs [][65]byte - return SpendMsg{ - Blknum0: 1, - Txindex0: 0, - Oindex0: 0, - DepositNum0: 0, - Owner0: common.Address{}, - Input0ConfirmSigs: confirmSigs, - Blknum1: 1, - Txindex1: 1, - Oindex1: 0, - DepositNum1: 0, - Owner1: common.Address{}, - Input1ConfirmSigs: confirmSigs, - Newowner0: common.Address{}, - Amount0: 150, - Newowner1: common.Address{}, - Amount1: 50, - FeeAmount: 0, - } -} - -func GenSpendMsgWithAddresses() SpendMsg { - // Creates Basic Spend Msg with owners and recipients - var confirmSigs [][65]byte - privKeyA, _ := ethcrypto.GenerateKey() - privKeyB, _ := ethcrypto.GenerateKey() - - return SpendMsg{ - Blknum0: 1, - Txindex0: 0, - Oindex0: 0, - DepositNum0: 0, - Owner0: utils.PrivKeyToAddress(privKeyA), - Input0ConfirmSigs: confirmSigs, - Blknum1: 1, - Txindex1: 1, - Oindex1: 0, - DepositNum1: 0, - Owner1: utils.PrivKeyToAddress(privKeyA), - Input1ConfirmSigs: confirmSigs, - Newowner0: utils.PrivKeyToAddress(privKeyB), - Amount0: 150, - Newowner1: utils.PrivKeyToAddress(privKeyB), - Amount1: 50, - FeeAmount: 0, - } -} - -// Creates a transaction with no owners -func TestNoOwners(t *testing.T) { - var msg = GenBasicSpendMsg() - err := msg.ValidateBasic() - require.Equal(t, sdk.CodeType(201), - err.Code(), err.Error()) -} - -// Creates a transaction with no recipients -func TestNoRecipients(t *testing.T) { - privKeyA, _ := ethcrypto.GenerateKey() - var msg = GenBasicSpendMsg() - msg.Owner0 = utils.PrivKeyToAddress(privKeyA) - msg.Owner1 = utils.PrivKeyToAddress(privKeyA) - err := msg.ValidateBasic() - require.Equal(t, sdk.CodeType(201), - err.Code(), err.Error()) -} - -// The oindex is neither 0 or 1 -func TestIncorrectOIndex(t *testing.T) { - var msg = GenSpendMsgWithAddresses() - msg.Oindex0 = 2 - - err := msg.ValidateBasic() - require.Equal(t, sdk.CodeType(202), - err.Code(), err.Error()) - - msg.Oindex0 = 0 - msg.Oindex1 = 2 - err = msg.ValidateBasic() - require.Equal(t, sdk.CodeType(202), - err.Code(), err.Error()) - -} - -// Creates an invalid transaction referencing utxo and deposit -func TestInvalidSpendDeposit(t *testing.T) { - var msg = GenSpendMsgWithAddresses() - msg.DepositNum0 = 5 - - err := msg.ValidateBasic() - require.Equal(t, sdk.CodeType(204), err.Code(), err.Error()) - - msg.DepositNum0 = 0 - msg.DepositNum1 = 123 - err = msg.ValidateBasic() - require.Equal(t, sdk.CodeType(204), err.Code(), err.Error()) -} - -// Creates an invalid transaction spending same position twice -func TestInvalidPosition(t *testing.T) { - var msg = GenSpendMsgWithAddresses() - // Set second position equal to first position - msg.Txindex1 = 0 - - err := msg.ValidateBasic() - require.Equal(t, sdk.CodeType(204), err.Code(), err.Error()) -} - -// Try to spend with 0 denomination for first output -func TestInvalidDenomination(t *testing.T) { - var msg = GenSpendMsgWithAddresses() - msg.Amount0 = 0 - - err := msg.ValidateBasic() - require.Equal(t, sdk.CodeType(203), err.Code(), err.Error()) -} - -// Tests GetSigners method -func TestGetSigners(t *testing.T) { - msg := GenBasicSpendMsg() - privKeyA, _ := ethcrypto.GenerateKey() - privKeyB, _ := ethcrypto.GenerateKey() - - msg.Owner0 = utils.PrivKeyToAddress(privKeyA) - addrs := []sdk.AccAddress{sdk.AccAddress(msg.Owner0.Bytes())} - signers := msg.GetSigners() // GetSigners() returns []sdk.AccAddress by interface constraint - require.Equal(t, addrs, signers, "signer Address do not match") - - msg.Owner1 = utils.PrivKeyToAddress(privKeyB) - addrs = []sdk.AccAddress{sdk.AccAddress(msg.Owner0.Bytes()), sdk.AccAddress(msg.Owner1.Bytes())} - signers = msg.GetSigners() - require.Equal(t, addrs, signers, "signer Addresses do not match") -} diff --git a/types/utxo.go b/types/utxo.go deleted file mode 100644 index 483bf40..0000000 --- a/types/utxo.go +++ /dev/null @@ -1,180 +0,0 @@ -package types - -import ( - "errors" - "fmt" - amino "github.com/tendermint/go-amino" - - utils "github.com/FourthState/plasma-mvp-sidechain/utils" - "github.com/FourthState/plasma-mvp-sidechain/x/utxo" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/common" - - "github.com/tendermint/tendermint/crypto/tmhash" -) - -const ( - // Only allowed Denomination on this plasma chain - Denom = "Ether" -) - -var _ utxo.UTXO = &BaseUTXO{} - -// Implements UTXO interface -type BaseUTXO struct { - InputAddresses [2]common.Address - Address common.Address - Amount uint64 - Denom string - Position PlasmaPosition - TxHash []byte -} - -func ProtoUTXO(ctx sdk.Context, msg sdk.Msg) utxo.UTXO { - spendmsg, ok := msg.(SpendMsg) - if !ok { - return nil - } - - return &BaseUTXO{ - InputAddresses: [2]common.Address{spendmsg.Owner0, spendmsg.Owner1}, - TxHash: tmhash.Sum(ctx.TxBytes()), - } -} - -func NewBaseUTXO(addr common.Address, inputaddr [2]common.Address, amount uint64, - denom string, position PlasmaPosition) *BaseUTXO { - return &BaseUTXO{ - InputAddresses: inputaddr, - Address: addr, - Amount: amount, - Denom: denom, - Position: position, - } -} - -func (baseutxo BaseUTXO) GetTxHash() []byte { - return baseutxo.TxHash -} - -//Implements UTXO -func (baseutxo BaseUTXO) GetAddress() []byte { - return baseutxo.Address.Bytes() -} - -//Implements UTXO -func (baseutxo *BaseUTXO) SetAddress(addr []byte) error { - if !utils.ZeroAddress(baseutxo.Address) { - return fmt.Errorf("address already set to: %X", baseutxo.Address) - } - address := common.BytesToAddress(addr) - if utils.ZeroAddress(address) { - return fmt.Errorf("invalid address provided: %X", address) - } - baseutxo.Address = address - return nil -} - -//Implements UTXO -func (baseutxo *BaseUTXO) SetInputAddresses(addrs [2]common.Address) error { - if !utils.ZeroAddress(baseutxo.InputAddresses[0]) { - return fmt.Errorf("input addresses already set to: %X, %X", baseutxo.InputAddresses[0], baseutxo.InputAddresses[1]) - } - if utils.ZeroAddress(addrs[0]) { - return fmt.Errorf("invalid address provided: %X", addrs[0]) - } - baseutxo.InputAddresses = addrs - return nil -} - -//Implements UTXO -func (baseutxo BaseUTXO) GetInputAddresses() [2]common.Address { - return baseutxo.InputAddresses -} - -//Implements UTXO -func (baseutxo BaseUTXO) GetAmount() uint64 { - return baseutxo.Amount -} - -//Implements UTXO -func (baseutxo *BaseUTXO) SetAmount(amount uint64) error { - if baseutxo.Amount != 0 { - return fmt.Errorf("amount already set to: %d", baseutxo.Amount) - } - baseutxo.Amount = amount - return nil -} - -func (baseutxo BaseUTXO) GetPosition() utxo.Position { - return baseutxo.Position -} - -func (baseutxo *BaseUTXO) SetPosition(position utxo.Position) error { - if baseutxo.Position.IsValid() { - return fmt.Errorf("position already set to: %v", baseutxo.Position) - } else if !position.IsValid() { - return errors.New("invalid position provided") - } - - plasmaposition, ok := position.(PlasmaPosition) - if !ok { - return errors.New("position must be of type PlasmaPosition") - } - baseutxo.Position = plasmaposition - return nil -} - -func (baseutxo BaseUTXO) GetDenom() string { - return Denom -} - -func (baseutxo *BaseUTXO) SetDenom(denom string) error { - return nil -} - -//---------------------------------------- -// Position - -var _ utxo.Position = &PlasmaPosition{} - -type PlasmaPosition struct { - Blknum uint64 - TxIndex uint16 - Oindex uint8 - DepositNum uint64 -} - -func NewPlasmaPosition(blknum uint64, txIndex uint16, oIndex uint8, depositNum uint64) PlasmaPosition { - return PlasmaPosition{ - Blknum: blknum, - TxIndex: txIndex, - Oindex: oIndex, - DepositNum: depositNum, - } -} - -func (position PlasmaPosition) Get() []sdk.Uint { - return []sdk.Uint{sdk.NewUint(position.Blknum), sdk.NewUint(uint64(position.TxIndex)), sdk.NewUint(uint64(position.Oindex)), sdk.NewUint(position.DepositNum)} -} - -// check that the position is formatted correctly -// Implements Position -func (position PlasmaPosition) IsValid() bool { - // If position is a regular tx, output index must be 0 or 1 and depositnum must be 0 - if position.Blknum != 0 { - return position.Oindex < 2 && position.DepositNum == 0 - } else { - // If position represents deposit, depositnum is not 0 and txindex and oindex are 0. - return position.DepositNum != 0 && position.TxIndex == 0 && position.Oindex == 0 - } -} - -//------------------------------------------------------- -// misc -func RegisterAmino(cdc *amino.Codec) { - cdc.RegisterConcrete(&BaseUTXO{}, "types/BaseUTXO", nil) - cdc.RegisterConcrete(&PlasmaPosition{}, "types/PlasmaPosition", nil) - cdc.RegisterConcrete(BaseTx{}, "types/BaseTX", nil) - cdc.RegisterConcrete(SpendMsg{}, "types/SpendMsg", nil) -} diff --git a/types/utxo_test.go b/types/utxo_test.go deleted file mode 100644 index 57e58fb..0000000 --- a/types/utxo_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package types - -import ( - "fmt" - "github.com/stretchr/testify/require" - "testing" - - "github.com/FourthState/plasma-mvp-sidechain/utils" - "github.com/ethereum/go-ethereum/common" - ethcrypto "github.com/ethereum/go-ethereum/crypto" -) - -// return a base utxo with nothing set, along with two addresses -func GetBareUTXO() (utxo *BaseUTXO, addrA, addrB common.Address) { - privKeyA, _ := ethcrypto.GenerateKey() - privKeyB, _ := ethcrypto.GenerateKey() - addrA = utils.PrivKeyToAddress(privKeyA) - addrB = utils.PrivKeyToAddress(privKeyB) - return &BaseUTXO{}, addrA, addrB -} - -// Basic tests checking methods for BaseUTXO -func TestGetSetAddress(t *testing.T) { - utxo, addrA, addrB := GetBareUTXO() - - // try to set address to another blank address - err := utxo.SetAddress(common.Address{}.Bytes()) - require.Error(t, err) - - // set address to addrB - err = utxo.SetAddress(addrB.Bytes()) - require.NoError(t, err) - - // try to set address to addrA (currently set to addrB) - err = utxo.SetAddress(addrA.Bytes()) - require.Error(t, err) - - // check get method - addr := utxo.GetAddress() - require.Equal(t, addr, addrB.Bytes(), fmt.Sprintf("BaseUTXO GetAddress() method returned the wrong address: %s", addr)) -} - -// Test GetInputAddresses() and SetInputAddresses -func TestInputAddresses(t *testing.T) { - utxo, addrA, addrB := GetBareUTXO() - - // try to set input address to blank addresses - err := utxo.SetInputAddresses([2]common.Address{common.Address{}, common.Address{}}) - require.Error(t, err) - - // set input addresses to addrA, addrA - err = utxo.SetInputAddresses([2]common.Address{addrA, addrA}) - require.NoError(t, err) - - // try to set input address to addrB - err = utxo.SetInputAddresses([2]common.Address{addrB, common.Address{}}) - require.Error(t, err) - - // check get method - addrs := utxo.GetInputAddresses() - require.Equal(t, addrs, [2]common.Address{addrA, addrA}) -} - -// Test GetAmount() and SetAmount() -func TestAmount(t *testing.T) { - utxo := NewBaseUTXO(common.Address{}, [2]common.Address{common.Address{}, common.Address{}}, 100, "ether", PlasmaPosition{}) - - // try to set denom when it already has a value - err := utxo.SetAmount(100000000) - require.Error(t, err) - - // check get method - amount := utxo.GetAmount() - require.Equal(t, amount, uint64(100), "the wrong amount was returned by GetAmount()") -} - -// Test GetPosition() and SetPosition() -func TestPosition(t *testing.T) { - utxo := BaseUTXO{} - position := NewPlasmaPosition(0, uint16(0), uint8(0), 0) - - // try to set position to incorrect position 0, 0, 0, 0 - err := utxo.SetPosition(position) - require.Error(t, err) - - // set position - position = NewPlasmaPosition(5, uint16(12), uint8(1), 0) - err = utxo.SetPosition(position) - require.NoError(t, err) - - // try to set to different position - position = NewPlasmaPosition(1, uint16(23), uint8(1), 0) - err = utxo.SetPosition(position) - require.Error(t, err) - - // check get method - require.Equal(t, utxo.GetPosition(), NewPlasmaPosition(5, uint16(12), uint8(1), 0), "the wrong position was returned") -} diff --git a/utils/crypto.go b/utils/crypto.go new file mode 100644 index 0000000..cd7dff2 --- /dev/null +++ b/utils/crypto.go @@ -0,0 +1,13 @@ +package utils + +import ( + "bytes" + "github.com/ethereum/go-ethereum/crypto" +) + +func ToEthSignedMessageHash(msg []byte) []byte { + buffer := new(bytes.Buffer) + buffer.Write([]byte("\x19Ethereum Signed Message:\n32")) + buffer.Write(msg) + return crypto.Keccak256(buffer.Bytes()) +} diff --git a/utils/crypto_test.go b/utils/crypto_test.go new file mode 100644 index 0000000..185572b --- /dev/null +++ b/utils/crypto_test.go @@ -0,0 +1,19 @@ +package utils + +import ( + "bytes" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" + "testing" +) + +func TestEthSignedMessageHash(t *testing.T) { + hash := crypto.Keccak256([]byte("fourthstate")) + + ethSignedMessageHash := ToEthSignedMessageHash(hash) + expectedMessage := crypto.Keccak256(append([]byte("\x19Ethereum Signed Message:\n32"), hash...)) + + require.True(t, + bytes.Equal(expectedMessage, ethSignedMessageHash), + "did not create the appropriate ethereum signed hash") +} diff --git a/utils/utils.go b/utils/utils.go index 352298c..02f3e26 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1,44 +1,32 @@ package utils import ( - "crypto/ecdsa" - "fmt" + "bytes" "github.com/ethereum/go-ethereum/common" - ethcrypto "github.com/ethereum/go-ethereum/crypto" "math/big" - "reflect" ) -func ZeroAddress(addr common.Address) bool { - return new(big.Int).SetBytes(addr.Bytes()).Sign() == 0 -} - -func ValidAddress(addr common.Address) bool { - return !reflect.DeepEqual(addr, common.Address{}) -} +var ( + Big0 = big.NewInt(0) + Big1 = big.NewInt(1) + Big2 = big.NewInt(2) + ZeroAddress = common.Address{} +) -func PrivKeyToAddress(p *ecdsa.PrivateKey) common.Address { - return ethcrypto.PubkeyToAddress(ecdsa.PublicKey(p.PublicKey)) +// IsZeroAddress is an indicator if the address is the "0x0" address +func IsZeroAddress(addr common.Address) bool { + return bytes.Equal(addr[:], ZeroAddress[:]) } -func GenerateAddress() common.Address { - priv, err := ethcrypto.GenerateKey() - if err != nil { - panic(err) +func RemoveHexPrefix(hexStr string) string { + if len(hexStr) >= 2 && (hexStr[:2] == "0x" || hexStr[:2] == "0X") { + hexStr = hexStr[2:] } - return PrivKeyToAddress(priv) -} - -func SignHash(data []byte) []byte { - msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data) - return ethcrypto.Keccak256([]byte(msg)) -} -// helper function for tests -func GetIndex(index int64) int64 { - if index >= 0 { - return index - } else { - return 0 + // ensure the hex string has an even length + if len(hexStr)%2 != 0 { + hexStr = "0" + hexStr } + + return hexStr } diff --git a/utils/utils_test.go b/utils/utils_test.go new file mode 100644 index 0000000..ad3e452 --- /dev/null +++ b/utils/utils_test.go @@ -0,0 +1,31 @@ +package utils + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "testing" +) + +func TestNilAddressDetector(t *testing.T) { + nilAddress := common.Address{} + require.True(t, IsZeroAddress(nilAddress), "marked 0x0 as not the nil address") + + addr := common.HexToAddress("1") + require.False(t, IsZeroAddress(addr), "marked a non 0x0 address as nil") +} + +func TestHexPrefixRemoval(t *testing.T) { + str := "0x0123" + require.Equal(t, "0123", RemoveHexPrefix(str)) + + str = "0x123" + require.Equal(t, "0123", RemoveHexPrefix(str)) + + str = "0123" + require.Equal(t, "0123", RemoveHexPrefix(str)) + + str = "123" + require.Equal(t, "0123", RemoveHexPrefix(str)) + + require.Equal(t, "", RemoveHexPrefix("")) +} diff --git a/x/metadata/mapper.go b/x/metadata/mapper.go deleted file mode 100644 index f5b0b05..0000000 --- a/x/metadata/mapper.go +++ /dev/null @@ -1,30 +0,0 @@ -package metadata - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" -) - -type MetadataMapper struct { - contextKey sdk.StoreKey -} - -func NewMetadataMapper(contextKey sdk.StoreKey) MetadataMapper { - return MetadataMapper{ - contextKey: contextKey, - } -} - -func (mm MetadataMapper) StoreMetadata(ctx sdk.Context, key []byte, metadata []byte) { - store := ctx.KVStore(mm.contextKey) - store.Set(key, metadata) -} - -func (mm MetadataMapper) GetMetadata(ctx sdk.Context, key []byte) []byte { - store := ctx.KVStore(mm.contextKey) - return store.Get(key) -} - -func (mm MetadataMapper) DeleteMetadata(ctx sdk.Context, key []byte) { - store := ctx.KVStore(mm.contextKey) - store.Delete(key) -} diff --git a/x/utxo/errors.go b/x/utxo/errors.go deleted file mode 100644 index e64aa8f..0000000 --- a/x/utxo/errors.go +++ /dev/null @@ -1,55 +0,0 @@ -package utxo - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" -) - -// Reserve errors 100 ~ 199 -const ( - DefaultCodespace sdk.CodespaceType = 2 - - CodeInvalidAddress sdk.CodeType = 101 - CodeInvalidOIndex sdk.CodeType = 102 - CodeInvalidDenomination sdk.CodeType = 103 - CodeInvalidIOF sdk.CodeType = 104 - CodeInvalidUTXO sdk.CodeType = 105 - CodeInvalidTransaction sdk.CodeType = 106 - CodeInvalidFee sdk.CodeType = 107 -) - -func codeToDefaultMsg(code sdk.CodeType) string { - switch code { - default: - return sdk.CodeToDefaultMsg(code) - } -} - -//---------------------------------------- -// Error constructors -func ErrInvalidTransaction(codespace sdk.CodespaceType, msg string) sdk.Error { - return sdk.NewError(codespace, CodeInvalidTransaction, msg) -} - -func ErrInvalidAddress(codespace sdk.CodespaceType, msg string) sdk.Error { - return sdk.NewError(codespace, CodeInvalidAddress, msg) -} - -func ErrInvalidOIndex(codespace sdk.CodespaceType, msg string) sdk.Error { - return sdk.NewError(codespace, CodeInvalidOIndex, msg) -} - -func ErrInvalidDenom(codespace sdk.CodespaceType, msg string) sdk.Error { - return sdk.NewError(codespace, CodeInvalidDenomination, msg) -} - -func ErrInvalidIOF(codespace sdk.CodespaceType, msg string) sdk.Error { - return sdk.NewError(codespace, CodeInvalidIOF, msg) -} - -func ErrInvalidUTXO(codespace sdk.CodespaceType, msg string) sdk.Error { - return sdk.NewError(codespace, CodeInvalidUTXO, msg) -} - -func ErrInvalidFee(codespace sdk.CodespaceType, msg string) sdk.Error { - return sdk.NewError(codespace, CodeInvalidFee, msg) -} diff --git a/x/utxo/handler.go b/x/utxo/handler.go deleted file mode 100644 index 4573fe8..0000000 --- a/x/utxo/handler.go +++ /dev/null @@ -1,88 +0,0 @@ -package utxo - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" -) - -// Return the next position for handler to store newly created UTXOs -// Secondary is true if NextPosition is meant to return secondary output positions for a single multioutput transaction -// If false, NextPosition will increment position to accomadate outputs for a new transaction -type NextPosition func(ctx sdk.Context, secondary bool) Position - -// Proto function to create application's UTXO implementation and fill with some proto-information -type ProtoUTXO func(sdk.Context, sdk.Msg) UTXO - -// User-defined fee update function -type FeeUpdater func([]Output) sdk.Error - -// Handler handles spends of arbitrary utxo implementation -func NewSpendHandler(um Mapper, nextPos NextPosition, proto ProtoUTXO) sdk.Handler { - return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { - spendMsg, ok := msg.(SpendMsg) - if !ok { - panic("Msg does not implement SpendMsg") - } - - // Delete inputs from store - for _, i := range spendMsg.Inputs() { - um.DeleteUTXO(ctx, i.Owner, i.Position) - } - - // Add outputs from store - for i, o := range spendMsg.Outputs() { - var next Position - if i == 0 { - next = nextPos(ctx, false) - } else { - next = nextPos(ctx, true) - } - utxo := proto(ctx, msg) - utxo.SetPosition(next) - utxo.SetAddress(o.Owner) - utxo.SetDenom(o.Denom) - utxo.SetAmount(o.Amount) - um.AddUTXO(ctx, utxo) - } - - return sdk.Result{} - } -} - -// This function should be called within the antehandler -// Checks that the inputs = outputs + fee and handles fee collection -func AnteHelper(ctx sdk.Context, um Mapper, tx sdk.Tx, simulate bool, feeUpdater FeeUpdater) sdk.Error { - msg := tx.GetMsgs()[0] - spendMsg, ok := msg.(SpendMsg) - if !ok { - panic("Msg does not implement SpendMsg") - } - - // Add up all inputs - totalInput := map[string]uint64{} - for _, i := range spendMsg.Inputs() { - utxo := um.GetUTXO(ctx, i.Owner, i.Position) - totalInput[utxo.GetDenom()] += utxo.GetAmount() - } - - // Add up all outputs and fee - totalOutput := map[string]uint64{} - for _, o := range spendMsg.Outputs() { - totalOutput[o.Denom] += o.Amount - } - for _, fee := range spendMsg.Fee() { - totalOutput[fee.Denom] += fee.Amount - } - - for denom, _ := range totalInput { - if totalInput[denom] != totalOutput[denom] { - return ErrInvalidTransaction(2, "Inputs do not equal Outputs") - } - } - - // Only update fee when we are actually delivering tx - if !ctx.IsCheckTx() && !simulate { - err := feeUpdater(spendMsg.Fee()) - return err - } - return nil -} diff --git a/x/utxo/handler_test.go b/x/utxo/handler_test.go deleted file mode 100644 index 845afff..0000000 --- a/x/utxo/handler_test.go +++ /dev/null @@ -1,164 +0,0 @@ -package utxo - -import ( - "crypto/ecdsa" - "fmt" - "github.com/stretchr/testify/require" - "testing" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/common" - ethcrypto "github.com/ethereum/go-ethereum/crypto" - rlp "github.com/ethereum/go-ethereum/rlp" - abci "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/libs/log" - - utils "github.com/FourthState/plasma-mvp-sidechain/utils" -) - -/* The following structs are used to do testing on the handler. -They are not fully implemented and should not be used -besides for testing -*/ - -type testApp struct { - txindex uint64 - oindex uint64 -} - -func (a *testApp) testNextPosition(ctx sdk.Context, secondary bool) Position { - if !secondary { - a.txindex++ - } - a.oindex++ - return newTestPosition([]uint64{uint64(ctx.BlockHeight()), uint64(a.txindex - 1), uint64(a.oindex - 1)}) -} - -var _ SpendMsg = testSpendMsg{} - -type testSpendMsg struct { - Input []Input - Output []Output - Fees Output -} - -func (msg testSpendMsg) Type() string { return "spend_utxo" } - -func (msg testSpendMsg) Route() string { return "spend" } - -func (msg testSpendMsg) ValidateBasic() sdk.Error { - return nil -} - -func (msg testSpendMsg) GetSignBytes() []byte { - b, err := rlp.EncodeToBytes(msg) - if err != nil { - panic(err) - } - return b -} - -func (msg testSpendMsg) GetSigners() []sdk.AccAddress { - addrs := make([]sdk.AccAddress, 1) - addrs[0] = sdk.AccAddress(msg.Input[0].Owner) - return addrs -} - -func (msg testSpendMsg) Inputs() []Input { - return msg.Input -} - -func (msg testSpendMsg) Outputs() []Output { - return msg.Output -} - -func (msg testSpendMsg) Fee() []Output { - return []Output{msg.Fees} -} - -func TestHandleSpendMessage(t *testing.T) { - const len int = 10 // number of addresses avaliable - var keys [len]*ecdsa.PrivateKey - var addrs []common.Address - for i := 0; i < len; i++ { - keys[i], _ = ethcrypto.GenerateKey() - addrs = append(addrs, utils.PrivKeyToAddress(keys[i])) - } - - cases := []struct { - inputNum int - outputNum int - }{ - // Test Case 0: 1 input 1 ouput - {1, 1}, - // Test Case 1: 1 input multiple outputs - {1, 10}, - // Test Case 2: multiple inputs 1 output - {10, 1}, - // Test Case 3: multiple inputs multiple outputs - {10, 10}, - } - - for index, tc := range cases { - ms, capKey, _ := SetupMultiStore() - - cdc := MakeCodec() - cdc.RegisterConcrete(&testUTXO{}, "x/utxo/testUTXO", nil) - cdc.RegisterConcrete(&testPosition{}, "x/utxo/testPosition", nil) - mapper := NewBaseMapper(capKey, cdc) - app := testApp{0, 0} - handler := NewSpendHandler(mapper, app.testNextPosition, testProtoUTXO) - - ctx := sdk.NewContext(ms, abci.Header{Height: 6}, false, log.NewNopLogger()) - var inputs []Input - var outputs []Output - - // Add utxo's that will be spent - for i := 0; i < tc.inputNum; i++ { - position := newTestPosition([]uint64{5, uint64(i), 0}) - utxo := newTestUTXO(addrs[i].Bytes(), 100, position) - mapper.AddUTXO(ctx, utxo) - - utxo = mapper.GetUTXO(ctx, addrs[i].Bytes(), position) - require.NotNil(t, utxo) - - inputs = append(inputs, Input{addrs[i].Bytes(), position}) - } - - for i := 0; i < tc.outputNum; i++ { - outputs = append(outputs, Output{addrs[(i+1)%len].Bytes(), "Ether", uint64((100 * tc.inputNum) / tc.outputNum)}) - } - - // Create spend msg - msg := testSpendMsg{ - Input: inputs, - Output: outputs, - Fees: Output{[]byte{}, "Ether", 100}, - } - - res := handler(ctx, msg) - require.Equal(t, sdk.CodeType(0), sdk.CodeType(res.Code), res.Log) - - // Delete inputs - for _, in := range msg.Inputs() { - mapper.DeleteUTXO(ctx, in.Owner, in.Position) - utxo := mapper.GetUTXO(ctx, in.Owner, in.Position) - require.Nil(t, utxo) - } - - // Check that outputs were created and are valid - // Then delete the outputs - for i, o := range msg.Outputs() { - position := newTestPosition([]uint64{6, 0, uint64(i)}) - utxo := mapper.GetUTXO(ctx, o.Owner, position) - require.NotNil(t, utxo, fmt.Sprintf("test case %d, output %d", index, i)) - - require.Equal(t, uint64((tc.inputNum*100)/tc.outputNum), utxo.GetAmount()) - require.EqualValues(t, addrs[(i+1)%len].Bytes(), utxo.GetAddress()) - - mapper.DeleteUTXO(ctx, o.Owner, position) - utxo = mapper.GetUTXO(ctx, o.Owner, position) - require.Nil(t, utxo) - } - } -} diff --git a/x/utxo/mapper.go b/x/utxo/mapper.go deleted file mode 100644 index 4bc3d93..0000000 --- a/x/utxo/mapper.go +++ /dev/null @@ -1,110 +0,0 @@ -package utxo - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" - amino "github.com/tendermint/go-amino" -) - -// Mapper stores and retrieves UTXO's from stores -// retrieved from the context. -type Mapper interface { - GetUTXO(ctx sdk.Context, addr []byte, position Position) UTXO - GetUTXOsForAddress(ctx sdk.Context, addr []byte) []UTXO - AddUTXO(ctx sdk.Context, utxo UTXO) - DeleteUTXO(ctx sdk.Context, addr []byte, position Position) -} - -// Maps Address+Position to UTXO -// Uses go-amino encoding/decoding library -// Implements Mapper -type baseMapper struct { - - // The contextKey used to access the store from the Context. - contextKey sdk.StoreKey - - // The Amino codec for binary encoding/decoding - cdc *amino.Codec -} - -func NewBaseMapper(contextKey sdk.StoreKey, cdc *amino.Codec) Mapper { - return baseMapper{ - contextKey: contextKey, - cdc: cdc, - } - -} - -// Returns the UTXO corresponding to the address + go amino encoded Position struct -// Returns nil if no UTXO exists at that position -func (um baseMapper) GetUTXO(ctx sdk.Context, addr []byte, position Position) UTXO { - store := ctx.KVStore(um.contextKey) - key := um.constructKey(addr, position) - bz := store.Get(key) - - if bz == nil { - return nil - } - - utxo := um.decodeUTXO(bz) - return utxo -} - -// Returns all the UTXOs owned by an address. -// Returns empty slice if no UTXO exists for the address. -func (um baseMapper) GetUTXOsForAddress(ctx sdk.Context, addr []byte) []UTXO { - store := ctx.KVStore(um.contextKey) - iterator := sdk.KVStorePrefixIterator(store, addr) - utxos := make([]UTXO, 0) - - for ; iterator.Valid(); iterator.Next() { - utxo := um.decodeUTXO(iterator.Value()) - utxos = append(utxos, utxo) - } - iterator.Close() - - return utxos -} - -// Adds the UTXO to the mapper -func (um baseMapper) AddUTXO(ctx sdk.Context, utxo UTXO) { - position := utxo.GetPosition() - address := utxo.GetAddress() - store := ctx.KVStore(um.contextKey) - - key := um.constructKey(address, position) - bz := um.encodeUTXO(utxo) - store.Set(key, bz) -} - -// Deletes UTXO corresponding to address + position from mapping -func (um baseMapper) DeleteUTXO(ctx sdk.Context, addr []byte, position Position) { - store := ctx.KVStore(um.contextKey) - key := um.constructKey(addr, position) - store.Delete(key) -} - -// (
+ ) forms the unique key that maps to an UTXO. -func (um baseMapper) constructKey(address []byte, position Position) []byte { - posBytes, err := um.cdc.MarshalBinaryBare(position) - if err != nil { - panic(err) - } - key := append(address, posBytes...) - return key -} - -func (um baseMapper) encodeUTXO(utxo UTXO) []byte { - bz, err := um.cdc.MarshalBinaryBare(utxo) - if err != nil { - panic(err) - } - return bz -} - -func (um baseMapper) decodeUTXO(bz []byte) (utxo UTXO) { - err := um.cdc.UnmarshalBinaryBare(bz, &utxo) - if err != nil { - panic(err) - } - return utxo -} diff --git a/x/utxo/mapper_test.go b/x/utxo/mapper_test.go deleted file mode 100644 index 5de727c..0000000 --- a/x/utxo/mapper_test.go +++ /dev/null @@ -1,290 +0,0 @@ -package utxo - -import ( - "errors" - "fmt" - "github.com/stretchr/testify/require" - "testing" - - sdk "github.com/cosmos/cosmos-sdk/types" - ethcrypto "github.com/ethereum/go-ethereum/crypto" - abci "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/libs/log" - - "github.com/FourthState/plasma-mvp-sidechain/utils" -) - -/* structs used to test the mapper */ -var _ Position = &testPosition{} - -type testPosition struct { - BlockNumber uint64 - TxIndex uint32 - OutputIndex uint8 -} - -func testProtoUTXO(ctx sdk.Context, msg sdk.Msg) UTXO { - return &testUTXO{} -} - -func newTestPosition(newPos []uint64) testPosition { - return testPosition{ - BlockNumber: newPos[0], - TxIndex: uint32(newPos[1]), - OutputIndex: uint8(newPos[2]), - } -} - -func (pos testPosition) Get() []sdk.Uint { - return []sdk.Uint{sdk.NewUint(pos.BlockNumber), sdk.NewUint(uint64(pos.TxIndex)), sdk.NewUint(uint64(pos.OutputIndex))} -} - -func (pos testPosition) IsValid() bool { - if pos.OutputIndex > 4 { - return false - } - return true -} - -var _ UTXO = &testUTXO{} - -type testUTXO struct { - Owner []byte - Amount uint64 - Denom string - Position testPosition -} - -func newTestUTXO(owner []byte, amount uint64, position testPosition) UTXO { - return &testUTXO{ - Owner: owner, - Amount: amount, - Denom: "Ether", - Position: position, - } -} - -func (utxo testUTXO) GetAddress() []byte { - return utxo.Owner -} - -func (utxo *testUTXO) SetAddress(address []byte) error { - if utxo.Owner != nil { - return errors.New("Owner already set") - } - utxo.Owner = address - return nil -} - -func (utxo testUTXO) GetAmount() uint64 { - return utxo.Amount -} - -func (utxo *testUTXO) SetAmount(amount uint64) error { - if utxo.Amount != 0 { - return errors.New("Owner already set") - } - utxo.Amount = amount - return nil -} - -func (utxo testUTXO) GetDenom() string { - return utxo.Denom -} - -func (utxo *testUTXO) SetDenom(denom string) error { - if utxo.Denom != "" { - return errors.New("Owner already set") - } - utxo.Denom = denom - return nil -} - -func (utxo testUTXO) GetPosition() Position { - return utxo.Position -} - -func (utxo *testUTXO) SetPosition(pos Position) error { - - position, ok := pos.(testPosition) - if !ok { - fmt.Println("ah") - return errors.New("position setting err") - } - utxo.Position = position - return nil -} - -/* - Basic test of Get, Add, Delete - Creates a valid UTXO and adds it to the uxto mapping. - Checks to make sure UTXO isn't nil after adding to mapping. - Then deletes the UTXO from the mapping -*/ - -func TestUTXOGetAddDelete(t *testing.T) { - ms, capKey, _ := SetupMultiStore() - - ctx := sdk.NewContext(ms, abci.Header{}, false, log.NewNopLogger()) - cdc := MakeCodec() - cdc.RegisterConcrete(&testUTXO{}, "x/utxo/testUTXO", nil) - cdc.RegisterConcrete(&testPosition{}, "x/utxo/testPosition", nil) - mapper := NewBaseMapper(capKey, cdc) - - priv, _ := ethcrypto.GenerateKey() - addr := utils.PrivKeyToAddress(priv) - - position := newTestPosition([]uint64{1, 0, 0}) - - utxo := newTestUTXO(addr.Bytes(), 100, position) - - require.NotNil(t, utxo) - require.Equal(t, addr.Bytes(), utxo.GetAddress()) - require.EqualValues(t, position, utxo.GetPosition()) - - mapper.AddUTXO(ctx, utxo) - - utxo = mapper.GetUTXO(ctx, addr.Bytes(), position) - require.NotNil(t, utxo) - - mapper.DeleteUTXO(ctx, addr.Bytes(), position) - utxo = mapper.GetUTXO(ctx, addr.Bytes(), position) - require.Nil(t, utxo) -} - -/* - Test multiple additions and deletions in the same block and different blocks - Creates a valid UTXOs and adds them to the uxto mapping. - Then deletes the UTXO's from the mapping. -*/ - -func TestMultiUTXOAddDeleteSameBlock(t *testing.T) { - ms, capKey, _ := SetupMultiStore() - - ctx := sdk.NewContext(ms, abci.Header{}, false, log.NewNopLogger()) - cdc := MakeCodec() - cdc.RegisterConcrete(&testUTXO{}, "x/utxo/testUTXO", nil) - cdc.RegisterConcrete(&testPosition{}, "x/utxo/testPosition", nil) - mapper := NewBaseMapper(capKey, cdc) - - priv, _ := ethcrypto.GenerateKey() - addr := utils.PrivKeyToAddress(priv) - - for i := 0; i < 20; i++ { - position := newTestPosition([]uint64{uint64(i%4) + 1, uint64(i / 4), 0}) - utxo := newTestUTXO(addr.Bytes(), 100, position) - mapper.AddUTXO(ctx, utxo) - - utxo = mapper.GetUTXO(ctx, addr.Bytes(), position) - require.NotNil(t, utxo) - } - - for i := 0; i < 20; i++ { - position := newTestPosition([]uint64{uint64(i%4) + 1, uint64(i / 4), 0}) - - utxo := mapper.GetUTXO(ctx, addr.Bytes(), position) - require.NotNil(t, utxo) - mapper.DeleteUTXO(ctx, addr.Bytes(), position) - utxo = mapper.GetUTXO(ctx, addr.Bytes(), position) - require.Nil(t, utxo) - } - -} - -func TestInvalidAddress(t *testing.T) { - ms, capKey, _ := SetupMultiStore() - - ctx := sdk.NewContext(ms, abci.Header{}, false, log.NewNopLogger()) - - cdc := MakeCodec() - cdc.RegisterConcrete(&testUTXO{}, "x/utxo/testUTXO", nil) - cdc.RegisterConcrete(&testPosition{}, "x/utxo/testPosition", nil) - mapper := NewBaseMapper(capKey, cdc) - - priv0, _ := ethcrypto.GenerateKey() - addr0 := utils.PrivKeyToAddress(priv0) - - priv1, _ := ethcrypto.GenerateKey() - addr1 := utils.PrivKeyToAddress(priv1) - - position := newTestPosition([]uint64{1, 0, 0}) - - utxo := newTestUTXO(addr0.Bytes(), 100, position) - - require.NotNil(t, utxo) - require.Equal(t, addr0.Bytes(), utxo.GetAddress()) - require.EqualValues(t, position, utxo.GetPosition()) - - mapper.AddUTXO(ctx, utxo) - - // GetUTXO with correct position but wrong address - utxo = mapper.GetUTXO(ctx, addr1.Bytes(), position) - require.Nil(t, utxo) - - utxo = mapper.GetUTXO(ctx, addr0.Bytes(), position) - require.NotNil(t, utxo) - - // DeleteUTXO with correct position but wrong address - mapper.DeleteUTXO(ctx, addr1.Bytes(), position) - utxo = mapper.GetUTXO(ctx, addr0.Bytes(), position) - require.NotNil(t, utxo) - - mapper.DeleteUTXO(ctx, addr0.Bytes(), position) - utxo = mapper.GetUTXO(ctx, addr0.Bytes(), position) - require.Nil(t, utxo) -} - -/* - Test getting all UTXOs for an Address. -*/ - -func TestGetUTXOsForAddress(t *testing.T) { - ms, capKey, _ := SetupMultiStore() - - ctx := sdk.NewContext(ms, abci.Header{}, false, log.NewNopLogger()) - - cdc := MakeCodec() - cdc.RegisterConcrete(&testUTXO{}, "x/utxo/testUTXO", nil) - cdc.RegisterConcrete(&testPosition{}, "x/utxo/testPosition", nil) - mapper := NewBaseMapper(capKey, cdc) - - privA, _ := ethcrypto.GenerateKey() - addrA := utils.PrivKeyToAddress(privA) - - privB, _ := ethcrypto.GenerateKey() - addrB := utils.PrivKeyToAddress(privB) - - privC, _ := ethcrypto.GenerateKey() - addrC := utils.PrivKeyToAddress(privC) - - positionB0 := newTestPosition([]uint64{1, 0, 0}) - positionB1 := newTestPosition([]uint64{2, 1, 0}) - positionB2 := newTestPosition([]uint64{3, 2, 1}) - - utxo0 := newTestUTXO(addrB.Bytes(), 100, positionB0) - utxo1 := newTestUTXO(addrB.Bytes(), 200, positionB1) - utxo2 := newTestUTXO(addrB.Bytes(), 300, positionB2) - - mapper.AddUTXO(ctx, utxo0) - mapper.AddUTXO(ctx, utxo1) - mapper.AddUTXO(ctx, utxo2) - - utxosForAddressB := mapper.GetUTXOsForAddress(ctx, addrB.Bytes()) - require.NotNil(t, utxosForAddressB) - require.Equal(t, 3, len(utxosForAddressB)) - require.Equal(t, utxo0, utxosForAddressB[0]) - require.Equal(t, utxo1, utxosForAddressB[1]) - require.Equal(t, utxo2, utxosForAddressB[2]) - - positionC0 := newTestPosition([]uint64{2, 3, 0}) - utxo3 := newTestUTXO(addrC.Bytes(), 300, positionC0) - mapper.AddUTXO(ctx, utxo3) - utxosForAddressC := mapper.GetUTXOsForAddress(ctx, addrC.Bytes()) - require.NotNil(t, utxosForAddressC) - require.Equal(t, 1, len(utxosForAddressC)) - require.Equal(t, utxo3, utxosForAddressC[0]) - - // check returns empty slice if no UTXOs exist for address - utxosForAddressA := mapper.GetUTXOsForAddress(ctx, addrA.Bytes()) - require.Empty(t, utxosForAddressA) -} diff --git a/x/utxo/types.go b/x/utxo/types.go deleted file mode 100644 index da69a01..0000000 --- a/x/utxo/types.go +++ /dev/null @@ -1,51 +0,0 @@ -package utxo - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" -) - -// UTXO is a standard unspent transaction output -type UTXO interface { - // Address that owns UTXO - GetAddress() []byte - SetAddress([]byte) error // errors if already set - - GetAmount() uint64 - SetAmount(uint64) error // errors if already set - - GetDenom() string - SetDenom(string) error // errors if already set - - GetPosition() Position - SetPosition(Position) error // errors if already set -} - -// Positions must be unqiue or a collision may result when using mapper.go -type Position interface { - // Position is a uint slice - Get() []sdk.Uint // get position int slice. Return nil if unset. - - // returns true if the position is valid, false otherwise - IsValid() bool -} - -// SpendMsg is an interface that wraps sdk.Msg with additional information -// for the UTXO spend handler. -type SpendMsg interface { - sdk.Msg - - Inputs() []Input - Outputs() []Output - Fee() []Output // Owner is nil -} - -type Input struct { - Owner []byte - Position -} - -type Output struct { - Owner []byte - Denom string - Amount uint64 -} diff --git a/x/utxo/utils.go b/x/utxo/utils.go deleted file mode 100644 index b4b2a3d..0000000 --- a/x/utxo/utils.go +++ /dev/null @@ -1,32 +0,0 @@ -package utxo - -import ( - "github.com/cosmos/cosmos-sdk/store" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/tendermint/go-amino" - cryptoAmino "github.com/tendermint/tendermint/crypto/encoding/amino" - dbm "github.com/tendermint/tendermint/libs/db" -) - -func SetupMultiStore() (sdk.MultiStore, *sdk.KVStoreKey, *sdk.KVStoreKey) { - db := dbm.NewMemDB() - capKey := sdk.NewKVStoreKey("capkey") - capKey2 := sdk.NewKVStoreKey("capkey2") - ms := store.NewCommitMultiStore(db) - ms.MountStoreWithDB(capKey, sdk.StoreTypeIAVL, db) - ms.MountStoreWithDB(capKey2, sdk.StoreTypeIAVL, db) - ms.LoadLatestVersion() - return ms, capKey, capKey2 -} - -func MakeCodec() *amino.Codec { - cdc := amino.NewCodec() - RegisterAmino(cdc) - cryptoAmino.RegisterAmino(cdc) - return cdc -} - -func RegisterAmino(cdc *amino.Codec) { - cdc.RegisterInterface((*Position)(nil), nil) - cdc.RegisterInterface((*UTXO)(nil), nil) -}