From 53ea1e7403874e6a3c5956f3ed552b3a5cc661b7 Mon Sep 17 00:00:00 2001 From: bun919tw Date: Wed, 10 Oct 2018 14:46:32 +0000 Subject: [PATCH] Fix deposit flaw --- .../features/exit_deposit_flow.feature | 9 +++ .../features/steps/basic_flow.py | 3 - .../steps/challenge_double_spending_flow.py | 13 ++-- .../features/steps/challenge_history_flow.py | 3 - .../steps/challenge_spent_coin_flow.py | 3 - .../features/steps/exit_deposit_flow.py | 61 ++++++++++++++++ plasma_cash/child_chain/child_chain.py | 26 ++++--- plasma_cash/client/client.py | 18 +++++ .../contracts/RootChain/RootChain.sol | 70 +++++++++++++++++-- unit_tests/child_chain/test_child_chain.py | 14 +++- unit_tests/client/test_client.py | 38 ++++++++++ unit_tests/root_chain/test_root_chain.py | 2 +- 12 files changed, 231 insertions(+), 29 deletions(-) create mode 100644 integration_tests/features/exit_deposit_flow.feature create mode 100644 integration_tests/features/steps/exit_deposit_flow.py diff --git a/integration_tests/features/exit_deposit_flow.feature b/integration_tests/features/exit_deposit_flow.feature new file mode 100644 index 0000000..22478ee --- /dev/null +++ b/integration_tests/features/exit_deposit_flow.feature @@ -0,0 +1,9 @@ +Feature: Exit deposit flow + + Scenario: userA deposits to plasma cash and then tries to exit the deposit + Given userA deposits 1 eth in plasma cash + When userA starts to exit 1 eth from plasma cash + Then root chain got the start-deposit-exit record + When two weeks have passed from depositing exit + And userA finalize the deposit exit + Then userA has around 100 eth in root chain after exit diff --git a/integration_tests/features/steps/basic_flow.py b/integration_tests/features/steps/basic_flow.py index 5a0eedf..7913cd4 100644 --- a/integration_tests/features/steps/basic_flow.py +++ b/integration_tests/features/steps/basic_flow.py @@ -42,9 +42,6 @@ def userA_deposit_some_eth_to_plasma(context, amount): client.deposit(amount=amount, currency=eth_currency) time.sleep(5) - operator = Client(container.get_root_chain(), container.get_child_chain_client(), operator_key) - operator.submit_block() - @then('userA has around {amount:d} eth in root chain') def userA_has_around_some_amount_of_eth_in_root_chain(context, amount): diff --git a/integration_tests/features/steps/challenge_double_spending_flow.py b/integration_tests/features/steps/challenge_double_spending_flow.py index 04b376b..d3989ed 100644 --- a/integration_tests/features/steps/challenge_double_spending_flow.py +++ b/integration_tests/features/steps/challenge_double_spending_flow.py @@ -31,9 +31,6 @@ def userA_deposits_some_amount_of_eth_in_plasma_cash(context, amount): client.deposit(amount=amount, currency=eth_currency) time.sleep(5) - operator = Client(container.get_root_chain(), container.get_child_chain_client(), operator_key) - operator.submit_block() - @given('userA transfers {amount:d} eth to userB') def userA_transfers_some_eth_to_userB(context, amount): @@ -52,9 +49,13 @@ def userA_tries_to_double_spend_some_eth_to_userC(context, amount): invalid_tx_merkle = SparseMerkleTree(257, {uid: invalid_tx.merkle_hash}) root_chain = container.get_root_chain() - root_chain.functions.submitBlock(invalid_tx_merkle.root, TRANSFER_TX_2_BLOCK).transact({ - 'from': operator - }) + root_chain.functions.submitBlock( + invalid_tx_merkle.root, + TRANSFER_TX_2_BLOCK, + False, + b'', + b'' + ).transact({'from': operator}) @when('userC starts to exit {amount:d} eth from plasma cash') diff --git a/integration_tests/features/steps/challenge_history_flow.py b/integration_tests/features/steps/challenge_history_flow.py index f41df4d..d74645d 100644 --- a/integration_tests/features/steps/challenge_history_flow.py +++ b/integration_tests/features/steps/challenge_history_flow.py @@ -31,9 +31,6 @@ def userA_deposits_some_amount_of_eth_in_plasma_cash(context, amount): client.deposit(amount=amount, currency=eth_currency) time.sleep(5) - operator = Client(container.get_root_chain(), container.get_child_chain_client(), operator_key) - operator.submit_block() - @given('userA transfers {amount:d} eth to userB') def userA_transfers_some_eth_to_userB(context, amount): diff --git a/integration_tests/features/steps/challenge_spent_coin_flow.py b/integration_tests/features/steps/challenge_spent_coin_flow.py index 31290e8..6fa24ff 100644 --- a/integration_tests/features/steps/challenge_spent_coin_flow.py +++ b/integration_tests/features/steps/challenge_spent_coin_flow.py @@ -27,9 +27,6 @@ def userA_deposits_some_amount_of_eth_in_plasma_cash(context, amount): client.deposit(amount=amount, currency=eth_currency) time.sleep(5) - operator = Client(container.get_root_chain(), container.get_child_chain_client(), operator_key) - operator.submit_block() - @given('userA transfers {amount:d} eth to userB') def userA_transfers_some_eth_to_userB(context, amount): diff --git a/integration_tests/features/steps/exit_deposit_flow.py b/integration_tests/features/steps/exit_deposit_flow.py new file mode 100644 index 0000000..7977c80 --- /dev/null +++ b/integration_tests/features/steps/exit_deposit_flow.py @@ -0,0 +1,61 @@ +import time + +from behave import given, then, when +from web3.auto import w3 + +from integration_tests.features.utils import has_value +from plasma_cash.client.client import Client +from plasma_cash.dependency_config import container + +userA = '0xb83e232458A092696bE9717045d9A605FB0FEc2b' +operator_key = '0xa18969817c2cefadf52b93eb20f917dce760ce13b2ac9025e0361ad1e7a1d448' +userA_key = '0xe4807cf08191b310fe1821e6e5397727ee6bc694e92e25115eca40114e3a4e6b' +eth_currency = '0x0000000000000000000000000000000000000000' +uid = 1693390459388381052156419331572168595237271043726428428352746834777341368960 + +DEPOSIT_TX_BLOCK = 1 +TRANSFER_TX_1_BLOCK = 2 + + +@given('userA deposits {amount:d} eth in plasma cash') +def userA_deposits_some_amount_of_eth_in_plasma_cash(context, amount): + client = Client(container.get_root_chain(), container.get_child_chain_client(), userA_key) + client.deposit(amount=amount, currency=eth_currency) + time.sleep(5) + + +@when('userA starts to exit {amount:d} eth from plasma cash') +def userA_starts_to_exit_deposit_from_plasma_cash(context, amount): + client = Client(container.get_root_chain(), container.get_child_chain_client(), userA_key) + client.start_deposit_exit(uid, tx_blk_num=DEPOSIT_TX_BLOCK) + time.sleep(5) + + operator = Client(container.get_root_chain(), container.get_child_chain_client(), operator_key) + operator.submit_block() + + +@then('root chain got the start-deposit-exit record') +def root_chain_got_the_start_deposit_exit_record(context): + root_chain = container.get_root_chain() + assert has_value(root_chain.functions.exits(uid).call({'from': userA})) + + +@when('two weeks have passed from depositing exit') +def two_week_passed(context): + TWO_WEEK_SECOND = 60 * 60 * 24 * 14 + for provider in w3.providers: + provider.make_request('evm_increaseTime', TWO_WEEK_SECOND) + + +@when('userA finalize the deposit exit') +def userA_finalize_exit(context): + client = Client(container.get_root_chain(), container.get_child_chain_client(), userA_key) + client.finalize_exit(uid) + time.sleep(5) + + +@then('userA has around {amount:d} eth in root chain after exit') +def userB_successfully_exit_from_root_chain_after_exit(context, amount): + balance = w3.eth.getBalance(userA) + assert_msg = 'balance: {} is not in around: {}'.format(w3.fromWei(balance, 'ether'), amount) + assert w3.toWei(amount - 0.05, 'ether') <= balance <= w3.toWei(amount, 'ether'), assert_msg diff --git a/plasma_cash/child_chain/child_chain.py b/plasma_cash/child_chain/child_chain.py index e44c54c..b3e8c8c 100644 --- a/plasma_cash/child_chain/child_chain.py +++ b/plasma_cash/child_chain/child_chain.py @@ -5,7 +5,7 @@ from ethereum import utils from web3.auto import w3 -from plasma_cash.utils.utils import get_sender +from plasma_cash.utils.utils import get_sender, sign from .block import Block from .event import emit @@ -53,26 +53,36 @@ def apply_deposit(self, depositor, amount, uid): if not self.current_block.get_tx_by_uid(uid): deposit_tx = Transaction(0, uid, amount, new_owner) self.current_block.add_tx(deposit_tx) + sig = sign(self.current_block.hash, self.key) + self.submit_block(sig.hex(), True, uid) return deposit_tx.hash err_msg = 'deposit of uid: {} is already applied previously'.format(uid) raise DepositAlreadyAppliedException(err_msg) - def submit_block(self, sig): + def submit_block(self, sig, isDepositBlock=False, uid=None): signature = bytes.fromhex(sig) if (signature == b'\x00' * 65 or get_sender(self.current_block.hash, signature) != self.authority): raise InvalidBlockSignatureException('failed to submit a block') merkle_hash = self.current_block.merklize_transaction_set() + deposit_tx, deposit_tx_proof = b'', b'' + if uid is not None: + deposit_tx = self.current_block.get_tx_by_uid(uid) + deposit_tx_proof = self.current_block.merkle.create_merkle_proof(uid) authority_address = w3.toChecksumAddress('0x' + self.authority.hex()) - tx = (self.root_chain.functions - .submitBlock(merkle_hash, self.current_block_number) - .buildTransaction({ - 'from': authority_address, - 'nonce': w3.eth.getTransactionCount(authority_address, 'pending') - })) + tx = self.root_chain.functions.submitBlock( + merkle_hash, + self.current_block_number, + isDepositBlock, + rlp.encode(deposit_tx), + deposit_tx_proof + ).buildTransaction({ + 'from': authority_address, + 'nonce': w3.eth.getTransactionCount(authority_address, 'pending') + }) signed = w3.eth.account.signTransaction(tx, self.key) w3.eth.sendRawTransaction(signed.rawTransaction) diff --git a/plasma_cash/client/client.py b/plasma_cash/client/client.py index 90eab31..b4109e5 100644 --- a/plasma_cash/client/client.py +++ b/plasma_cash/client/client.py @@ -81,6 +81,24 @@ def start_exit(self, uid, prev_tx_blk_num, tx_blk_num): ).buildTransaction({'from': self.address}) self._sign_and_send_tx(tx) + def abort_deposit(self, uid): + tx = self.root_chain.functions.abortDeposit(uid).buildTransaction({'from': self.address}) + self._sign_and_send_tx(tx) + + def start_deposit_exit(self, uid, tx_blk_num): + block = self.get_block(tx_blk_num) + + tx = block.get_tx_by_uid(uid) + block.merklize_transaction_set() + tx_proof = block.merkle.create_merkle_proof(uid) + + tx = self.root_chain.functions.startDepositExit( + rlp.encode(tx), + tx_proof, + tx_blk_num + ).buildTransaction({'from': self.address}) + self._sign_and_send_tx(tx) + def challenge_exit(self, uid, tx_blk_num): block = self.get_block(tx_blk_num) diff --git a/plasma_cash/root_chain/contracts/RootChain/RootChain.sol b/plasma_cash/root_chain/contracts/RootChain/RootChain.sol index f06cb26..592507c 100644 --- a/plasma_cash/root_chain/contracts/RootChain/RootChain.sol +++ b/plasma_cash/root_chain/contracts/RootChain/RootChain.sol @@ -22,7 +22,7 @@ contract RootChain { uint public depositCount; uint public currentBlkNum; mapping(uint => bytes32) public childChain; - mapping(uint => uint) public wallet; + mapping(uint => funds) public wallet; mapping(uint => exit) public exits; mapping(uint => Challenge.challenge[]) public challenges; @@ -36,6 +36,13 @@ contract RootChain { address owner; } + struct funds { + bool hasValue; + bool isConfirmed; + uint amount; + address depositor; + } + /* * Modifiers */ @@ -56,10 +63,24 @@ contract RootChain { // @dev Allows Plasma chain operator to submit block root // @param blkRoot The root of a child chain block // @param blknum The child chain block number - function submitBlock(bytes32 blkRoot, uint blknum) + function submitBlock( + bytes32 blkRoot, + uint blknum, + bool isDepositBlock, + bytes depositTx, + bytes depositTxProof + ) public isAuthority { + if (isDepositBlock) { + Transaction.Tx memory txObj = depositTx.createTx(); + bytes32 merkleHash = keccak256(depositTx); + // Check if the deposit is aborted + require(wallet[txObj.uid].hasValue); + require(merkleHash.checkMembership(txObj.uid, blkRoot, depositTxProof)); + wallet[txObj.uid].isConfirmed = true; + } require(currentBlkNum + 1 == blknum); childChain[blknum] = blkRoot; currentBlkNum += 1; @@ -78,12 +99,53 @@ contract RootChain { require(amount * 10**18 == msg.value); } uint uid = uint256(keccak256(currency, msg.sender, depositCount)); - wallet[uid] = amount; + wallet[uid] = funds({ + hasValue: true, + isConfirmed: false, + amount: amount, + depositor: msg.sender + }); depositCount += 1; emit Deposit(msg.sender, amount, uid); return uid; } + // @dev Abort an deposit which is not included in child chain + // @param uid The id to specify the deposit + function abortDeposit(uint uid) public { + require(!wallet[uid].isConfirmed); + require(wallet[uid].depositor == msg.sender); + + msg.sender.transfer(wallet[uid].amount*10**18); + delete wallet[uid].hasValue; + } + + // @dev Starts to exit a deposit transaction + // @param tx The transaction in bytes that user wants to exit + // @param txProof The merkle proof of the tx + // @param txBlkNum The block number of the tx + function startDepositExit(bytes tx, bytes txProof, uint txBlkNum) public { + Transaction.Tx memory txObj = tx.createTx(); + require(txObj.prevBlock == 0); + require(msg.sender == txObj.newOwner); + + bytes32 merkleHash = keccak256(tx); + bytes32 root = childChain[txBlkNum]; + require(merkleHash.checkMembership(txObj.uid, root, txProof)); + + // Record the exit tx. + require(!exits[txObj.uid].hasValue); + exits[txObj.uid] = exit({ + hasValue: true, + exitTime: block.timestamp + 2 weeks, + exitTxBlkNum: txBlkNum, + exitTx: tx, + txBeforeExitTxBlkNum: 0, + txBeforeExitTx: "", + owner: msg.sender + }); + } + // @dev Starts to exit a transaction // @param prevTx The previous transaction in bytes of the transaction that user wants to exit // @param prevTxProof The merkle proof of the prevTx @@ -211,7 +273,7 @@ contract RootChain { require(!challenges[uid][i].hasValue); } - exits[uid].owner.transfer(wallet[uid]*10**18); + exits[uid].owner.transfer(wallet[uid].amount*10**18); delete exits[uid].hasValue; } diff --git a/unit_tests/child_chain/test_child_chain.py b/unit_tests/child_chain/test_child_chain.py index df1e0f0..3a890ee 100644 --- a/unit_tests/child_chain/test_child_chain.py +++ b/unit_tests/child_chain/test_child_chain.py @@ -60,7 +60,13 @@ def test_apply_deposit(self, child_chain): DUMMY_AMOUNT = 123 DUMMY_UID = 0 DUMMY_ADDR = b'\xfd\x02\xec\xeeby~u\xd8k\xcf\xf1d.\xb0\x84J\xfb(\xc7' + DUMMY_SIG = ('e79be9e20e121a8447b845c1b95b30b9bc4ed33db1de8e0f2c4401f56660506b7' + + 'f67dde4068f0a3a3763ef15d0c86988db8bbdaddfa9f42a36a9721349433e051b') + DUMMY_IS_DEPOSIT_BLOCK = True + (when(child_chain) + .submit_block(DUMMY_SIG, DUMMY_IS_DEPOSIT_BLOCK, DUMMY_UID) + .thenReturn(None)) tx_hash = child_chain.apply_deposit(DUMMY_ADDR, DUMMY_AMOUNT, DUMMY_UID) tx = child_chain.current_block.transaction_set[0] @@ -73,7 +79,13 @@ def test_apply_deposit_should_fail_when_is_already_applied(self, child_chain, ro DUMMY_AMOUNT = 123 DUMMY_UID = 0 DUMMY_ADDR = b'\xfd\x02\xec\xeeby~u\xd8k\xcf\xf1d.\xb0\x84J\xfb(\xc7' + DUMMY_SIG = ('e79be9e20e121a8447b845c1b95b30b9bc4ed33db1de8e0f2c4401f56660506b7' + + 'f67dde4068f0a3a3763ef15d0c86988db8bbdaddfa9f42a36a9721349433e051b') + DUMMY_IS_DEPOSIT_BLOCK = True + (when(child_chain) + .submit_block(DUMMY_SIG, DUMMY_IS_DEPOSIT_BLOCK, DUMMY_UID) + .thenReturn(None)) child_chain.apply_deposit(DUMMY_ADDR, DUMMY_AMOUNT, DUMMY_UID) with pytest.raises(DepositAlreadyAppliedException): child_chain.apply_deposit(DUMMY_ADDR, DUMMY_AMOUNT, DUMMY_UID) @@ -88,7 +100,7 @@ def test_submit_block(self, child_chain, root_chain): block = child_chain.current_block when(child_chain.current_block).merklize_transaction_set().thenReturn(DUMMY_MERKLE) (when(root_chain.functions) - .submitBlock(DUMMY_MERKLE, block_number) + .submitBlock(DUMMY_MERKLE, block_number, ANY, ANY, ANY) .thenReturn(MOCK_FUNCTION)) when(MOCK_FUNCTION).buildTransaction(ANY).thenReturn(DUMMY_TX) (when('plasma_cash.child_chain.child_chain') diff --git a/unit_tests/client/test_client.py b/unit_tests/client/test_client.py index c066c55..d098a5c 100644 --- a/unit_tests/client/test_client.py +++ b/unit_tests/client/test_client.py @@ -186,6 +186,44 @@ def test_start_exit(self, client, root_chain): verify(client)._sign_and_send_tx(ANY) + def test_abort_deposit(self, client, root_chain): + DUMMY_UID = 'dummy uid' + when(root_chain.functions).abortDeposit(DUMMY_UID).thenReturn(mock()) + when(client)._sign_and_send_tx(ANY).thenReturn(None) + + client.abort_deposit(DUMMY_UID) + verify(client)._sign_and_send_tx(ANY) + + def test_start_deposit_exit(self, client, root_chain): + MOCK_BLOCK = mock() + + DUMMY_TX = 'dummy tx' + DUMMY_ENCODED_TX = 'dummy encoded tx' + DUMMY_TX_PROOF = 'dummy tx proof' + DUMMY_TX_BLK_NUM = 'dummy tx blk num' + DUMMY_UID = 'dummy uid' + + when(root_chain.functions).startDepositExit( + DUMMY_ENCODED_TX, + DUMMY_TX_PROOF, + DUMMY_TX_BLK_NUM + ).thenReturn(mock()) + when(client).get_block(DUMMY_TX_BLK_NUM).thenReturn(MOCK_BLOCK) + when(MOCK_BLOCK).get_tx_by_uid(DUMMY_UID).thenReturn(DUMMY_TX) + + MOCK_BLOCK.merkle = mock() + (when(MOCK_BLOCK.merkle) + .create_merkle_proof(DUMMY_UID) + .thenReturn(DUMMY_TX_PROOF)) + (when('plasma_cash.client.client.rlp') + .encode(DUMMY_TX) + .thenReturn(DUMMY_ENCODED_TX)) + when(client)._sign_and_send_tx(ANY).thenReturn(None) + + client.start_deposit_exit(DUMMY_UID, DUMMY_TX_BLK_NUM) + + verify(client)._sign_and_send_tx(ANY) + def test_challenge_exit(self, client, root_chain): MOCK_BLOCK = mock() diff --git a/unit_tests/root_chain/test_root_chain.py b/unit_tests/root_chain/test_root_chain.py index a08e568..fd571b1 100644 --- a/unit_tests/root_chain/test_root_chain.py +++ b/unit_tests/root_chain/test_root_chain.py @@ -48,4 +48,4 @@ def test_deposit(self, tester_chain, contract): sender=tester_chain.k1 ) assert contract.depositCount() == 1 - assert contract.wallet(uid) == DUMMY_AMOUNT + assert contract.wallet(uid) == [False, DUMMY_AMOUNT, '0x' + tester_chain.a1.hex()]