async def test_transaction_nonce_lock(self):
        """Spams transactions with the same nonce, and ensures the server rejects all but one"""

        no_tests = 20

        txs = []
        tx = await self.get_tx_skel(FAUCET_PRIVATE_KEY, TEST_ADDRESS, 10**10)
        dtx = decode_transaction(tx)
        txs.append(sign_transaction(tx, FAUCET_PRIVATE_KEY))
        for i in range(11, 10 + no_tests):
            tx = await self.get_tx_skel(FAUCET_PRIVATE_KEY, TEST_ADDRESS,
                                        10**i)
            self.assertEqual(decode_transaction(tx).nonce, dtx.nonce)
            txs.append(sign_transaction(tx, FAUCET_PRIVATE_KEY))

        responses = await asyncio.gather(*(to_asyncio_future(
            self.fetch("/tx", method="POST", body={"tx": tx})) for tx in txs))

        ok = 0
        bad = 0
        for resp in responses:
            if resp.code == 200:
                ok += 1
            else:
                bad += 1
        self.assertEqual(ok, 1)
        self.assertEqual(bad, no_tests - 1)

        # TODO: deal with lingering ioloop tasks better
        await asyncio.sleep(1)
Example #2
0
    def test_add_signature_to_transaction_with_netowrk_id(self):

        for network_id in [1, 2, 66, 100]:

            sender_private_key = "0x0164f7c7399f4bb1eafeaae699ebbb12050bc6a50b2836b9ca766068a9d000c0"
            sender_address = "0xde3d2d9dd52ea80f7799ef4791063a5458d13913"
            to_address = "0x056db290f8ba3250ca64a45d16284d04bc6f5fbf"
            value = 10000000000
            nonce = 1048576
            data = b''
            gasprice = DEFAULT_GASPRICE
            startgas = DEFAULT_STARTGAS
            network_id = 1

            tx1 = Transaction(nonce, gasprice, startgas, to_address, value,
                              data, network_id, 0, 0)
            tx = encode_transaction(tx1)
            tx1.sign(data_decoder(sender_private_key), network_id=network_id)
            expected_signed_tx = encode_transaction(tx1)
            sig = data_encoder(signature_from_transaction(tx1))

            signed_tx = add_signature_to_transaction(tx, sig)

            self.assertEqual(signed_tx, expected_signed_tx)

            tx_obj = decode_transaction(tx)

            add_signature_to_transaction(tx_obj, sig)

            self.assertEqual(tx_obj.network_id, network_id)
            self.assertEqual(data_encoder(tx_obj.sender), sender_address)

            self.assertEqual(encode_transaction(tx_obj), expected_signed_tx)
    async def test_create_and_send_transaction_with_separate_sig(self):

        body = {"from": FAUCET_ADDRESS, "to": TEST_ADDRESS, "value": 10**10}

        resp = await self.fetch("/tx/skel", method="POST", body=body)

        self.assertEqual(resp.code, 200)

        body = json_decode(resp.body)
        tx = decode_transaction(body['tx'])
        tx = sign_transaction(tx, FAUCET_PRIVATE_KEY)
        sig = signature_from_transaction(tx)

        body = {"tx": body['tx'], "signature": data_encoder(sig)}

        resp = await self.fetch("/tx", method="POST", body=body)

        self.assertEqual(resp.code, 200, resp.body)

        # ensure we get a tracking events
        self.assertEqual((await self.next_tracking_event())[0], None)

        body = json_decode(resp.body)
        tx_hash = body['tx_hash']

        async def check_db():
            async with self.pool.acquire() as con:
                rows = await con.fetch(
                    "SELECT * FROM transactions WHERE nonce = $1", tx.nonce)
            self.assertEqual(len(rows), 1)
            self.assertEqual(rows[0]['hash'], tx_hash)

        await self.wait_on_tx_confirmation(tx_hash)
    async def test_resend_old_after_overwrite(self, *, ethminer, parity,
                                              monitor, push_client):
        # make sure no blocks are confirmed for the meantime
        ethminer.pause()

        # set up pn registrations
        async with self.pool.acquire() as con:
            await con.fetch(
                "INSERT INTO notification_registrations (service, registration_id, toshi_id, eth_address) VALUES ($1, $2, $3, $4)",
                'gcm', TEST_GCM_ID, TEST_ID_ADDRESS, TEST_WALLET_ADDRESS)

        # get tx skeleton
        tx1 = await self.get_tx_skel(FAUCET_PRIVATE_KEY, TEST_WALLET_ADDRESS,
                                     10**18)
        tx2 = await self.get_tx_skel(FAUCET_PRIVATE_KEY, TEST_WALLET_ADDRESS,
                                     0)
        self.assertEqual(
            decode_transaction(tx1).nonce,
            decode_transaction(tx2).nonce)
        # sign and send
        tx1_hash = await self.sign_and_send_tx(FAUCET_PRIVATE_KEY, tx1)
        # wait for tx PN
        await push_client.get()

        # send tx2 manually
        rpcclient = JsonRPCClient(parity.dsn()['url'])
        tx2_hash = await rpcclient.eth_sendRawTransaction(
            sign_transaction(tx2, FAUCET_PRIVATE_KEY))
        await monitor.filter_poll()
        _, pn = await push_client.get()
        _, pn = await push_client.get()

        # resend tx1 manually
        tx1_hash = await rpcclient.eth_sendRawTransaction(
            sign_transaction(tx1, FAUCET_PRIVATE_KEY))
        await monitor.filter_poll()
        _, pn = await push_client.get()
        _, pn = await push_client.get()

        async with self.pool.acquire() as con:
            tx1_row = await con.fetchrow(
                "SELECT * FROM transactions WHERE hash = $1", tx1_hash)
            tx2_row = await con.fetchrow(
                "SELECT * FROM transactions WHERE hash = $1", tx2_hash)

        self.assertEqual(tx1_row['status'], 'unconfirmed')
        self.assertEqual(tx2_row['status'], 'error')
    async def test_sending_transactions_and_storing_the_hash_correctly(
            self, *, push_client):
        """This test is born out of the fact that `UnsingedTransaction.hash` always
        calculates the hash of the transaction without the signature, even after `.sign`
        has been called on the transaction. This caused errors in what was being stored
        in the database and incorrectly detecting transaction overwrites.

        This test exposed the behaviour correctly so that it could be fixed"""

        # register for GCM PNs
        body = {"registration_id": TEST_GCM_ID, "address": TEST_ID_ADDRESS}
        resp = await self.fetch_signed("/gcm/register",
                                       signing_key=TEST_ID_KEY,
                                       method="POST",
                                       body=body)
        self.assertResponseCodeEqual(resp, 204, resp.body)

        body = {"from": FAUCET_ADDRESS, "to": TEST_ID_ADDRESS, "value": 10**10}

        resp = await self.fetch("/tx/skel", method="POST", body=body)

        self.assertEqual(resp.code, 200)

        body = json_decode(resp.body)
        tx = decode_transaction(body['tx'])
        tx = sign_transaction(tx, FAUCET_PRIVATE_KEY)
        sig = signature_from_transaction(tx)

        body = {"tx": body['tx'], "signature": data_encoder(sig)}

        resp = await self.fetch("/tx", method="POST", body=body)

        self.assertEqual(resp.code, 200, resp.body)

        body = json_decode(resp.body)
        tx_hash = body['tx_hash']

        async def check_db():
            async with self.pool.acquire() as con:
                rows = await con.fetch(
                    "SELECT * FROM transactions WHERE nonce = $1", tx.nonce)
            self.assertEqual(len(rows), 1)
            self.assertEqual(rows[0]['hash'], tx_hash)
            if rows[0]['status'] is not None:
                self.assertEqual(rows[0]['status'], 'unconfirmed')
            self.assertIsNone(rows[0]['error'])

        await self.wait_on_tx_confirmation(tx_hash, check_db)
        while True:
            token, payload = await push_client.get()
            message = parse_sofa_message(payload['message'])

            self.assertIsInstance(message, SofaPayment)
            self.assertEqual(message['txHash'], tx_hash)

            if message['status'] == "confirmed":
                break
    async def test_create_and_send_transaction(self):

        val = 10 ** 10
        body = {
            "from": FAUCET_ADDRESS,
            "to": TEST_ADDRESS,
            "value": val
        }

        resp = await self.fetch("/tx/skel", method="POST", body=body)

        self.assertEqual(resp.code, 200)

        body = json_decode(resp.body)

        tx = sign_transaction(body['tx'], FAUCET_PRIVATE_KEY)

        body = {
            "tx": tx
        }

        resp = await self.fetch("/tx", method="POST", body=body)

        self.assertEqual(resp.code, 200, resp.body)

        # ensure we get a tracking events
        self.assertEqual((await self.next_tracking_event())[0], None)

        body = json_decode(resp.body)
        tx_hash = body['tx_hash']

        tx = decode_transaction(tx)
        self.assertEqual(tx_hash, data_encoder(tx.hash))

        async with self.pool.acquire() as con:
            rows = await con.fetch("SELECT * FROM transactions WHERE nonce = $1", tx.nonce)
        self.assertEqual(len(rows), 1)

        # wait for a push notification
        await self.wait_on_tx_confirmation(tx_hash)
        while True:
            async with self.pool.acquire() as con:
                row = await con.fetchrow("SELECT * FROM transactions WHERE nonce = $1", tx.nonce)
            if row['status'] == 'confirmed':
                break

        # make sure updated field is updated
        self.assertGreater(row['updated'], row['created'])

        # make sure balance is returned correctly
        resp = await self.fetch('/balance/{}'.format(TEST_ADDRESS))
        self.assertEqual(resp.code, 200)
        data = json_decode(resp.body)
        self.assertEqual(parse_int(data['confirmed_balance']), val)
        self.assertEqual(parse_int(data['unconfirmed_balance']), val)
Example #7
0
    def test_send_tx(self):

        tx = decode_transaction(
            "0xe9808504a817c80082520894db089a4f9a8c5f17040b4fc51647e942b5fc601d880de0b6b3a764000080"
        )
        sig = "0xf5a43adea07d366ae420a5c75a5cae6c60d3e4aaa0b72c2f37fc387efd43d7fd30c4327f2dbd959f654857f58912129b09763329459d08e25547d895ae90fa0f01"

        resp = self.service_client.send_tx(tx, sig)

        self.assertEqual(
            resp,
            "0x07ca664267650c87c11318710e3afa2f8f191814d25def8e0c4f2768f3ef5ccb"
        )
Example #8
0
    def test_add_signature_to_transaction(self):

        tx = "0xe9808504a817c80082520894db089a4f9a8c5f17040b4fc51647e942b5fc601d880de0b6b3a764000080"
        sig = "0xf5a43adea07d366ae420a5c75a5cae6c60d3e4aaa0b72c2f37fc387efd43d7fd30c4327f2dbd959f654857f58912129b09763329459d08e25547d895ae90fa0f01"

        expected_signed_tx = "0xf86c808504a817c80082520894db089a4f9a8c5f17040b4fc51647e942b5fc601d880de0b6b3a7640000801ca0f5a43adea07d366ae420a5c75a5cae6c60d3e4aaa0b72c2f37fc387efd43d7fda030c4327f2dbd959f654857f58912129b09763329459d08e25547d895ae90fa0f"

        signed_tx = add_signature_to_transaction(tx, sig)

        self.assertEqual(signed_tx, expected_signed_tx)

        tx_obj = decode_transaction(tx)

        add_signature_to_transaction(tx_obj, sig)

        self.assertEqual(encode_transaction(tx_obj), expected_signed_tx)
    async def test_create_transaction_with_large_data(self):

        body = {
            "from": "0x0004DE837Ea93edbE51c093f45212AB22b4B35fc",
            "to": "0xa0c4d49fe1a00eb5ee3d85dc7a287d84d8c66699",
            "value": 0,
            "data": "0x94d9cf8f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000003c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
        }

        resp = await self.fetch("/tx/skel", method="POST", body=body)

        self.assertEqual(resp.code, 200)

        tx = sign_transaction(json_decode(resp.body)['tx'], FAUCET_PRIVATE_KEY)
        print(", ".join(["0x{:02x}".format(b) for b in data_decoder(tx)]))
        print(FAUCET_ADDRESS)
        print(", ".join(["0x{:02x}".format(b) for b in decode_transaction(tx).hash]))
    async def test_prevent_out_of_order_txs(self):
        """Spams transactions with the same nonce, and ensures the server rejects all but one"""

        tx1 = await self.get_tx_skel(FAUCET_PRIVATE_KEY, TEST_ADDRESS, 10 ** 10)
        dtx1 = decode_transaction(tx1)
        stx1 = sign_transaction(tx1, FAUCET_PRIVATE_KEY)
        tx2 = await self.get_tx_skel(FAUCET_PRIVATE_KEY, TEST_ADDRESS, 10 ** 10, dtx1.nonce + 1)
        stx2 = sign_transaction(tx2, FAUCET_PRIVATE_KEY)

        resp = await self.fetch("/tx", method="POST", body={"tx": stx2})
        self.assertEqual(resp.code, 400, resp.body)

        resp = await self.fetch("/tx", method="POST", body={"tx": stx1})
        self.assertEqual(resp.code, 200, resp.body)
        resp = await self.fetch("/tx", method="POST", body={"tx": stx2})
        self.assertEqual(resp.code, 200, resp.body)

        # lets the transaction queue processing run before ending the test
        await asyncio.sleep(0.1)
    def generate_tx_skel(self,
                         from_address,
                         to_address,
                         value,
                         gas=None,
                         gas_price=None,
                         nonce=None,
                         data=None,
                         **kwargs):

        reqdata = {"from": from_address, "to": to_address, "value": value}
        if gas is not None:
            reqdata['gas'] = gas
        if gas_price is not None:
            reqdata['gas_price'] = gas_price
        if nonce is not None:
            reqdata['nonce'] = nonce
        if data is not None:
            reqdata['data'] = data

        resp = self._fetch("/v1/tx/skel", "POST", reqdata, **kwargs)
        return decode_transaction(resp['tx'])
    async def test_create_tx_with_no_to_address(self):

        body = {
            "from": FAUCET_ADDRESS,
            "data": data_encoder(b"Hello World"),
            "value": 0,
            "gas": 530000,
        }

        resp = await self.fetch("/tx/skel", method="POST", body=body)

        self.assertEqual(resp.code, 200)

        body = json_decode(resp.body)

        tx = sign_transaction(body['tx'], FAUCET_PRIVATE_KEY)

        body = {
            "tx": tx
        }

        resp = await self.fetch("/tx", method="POST", body=body)

        self.assertEqual(resp.code, 200, resp.body)

        body = json_decode(resp.body)
        tx_hash = body['tx_hash']

        tx = decode_transaction(tx)
        self.assertEqual(tx_hash, data_encoder(tx.hash))

        async with self.pool.acquire() as con:
            rows = await con.fetch("SELECT * FROM transactions WHERE nonce = $1", tx.nonce)
        self.assertEqual(len(rows), 1)
        print(rows[0])

        await self.wait_on_tx_confirmation(tx_hash)
    async def send_transaction(self, *, tx, signature=None):

        try:
            tx = decode_transaction(tx)
        except:
            raise JsonRPCInvalidParamsError(data={
                'id': 'invalid_transaction',
                'message': 'Invalid Transaction'
            })

        if is_transaction_signed(tx):

            tx_sig = data_encoder(signature_from_transaction(tx))

            if signature:

                if tx_sig != signature:

                    raise JsonRPCInvalidParamsError(
                        data={
                            'id':
                            'invalid_signature',
                            'message':
                            'Invalid Signature: Signature in payload and signature of transaction do not match'
                        })
            else:

                signature = tx_sig
        else:

            if signature is None:
                raise JsonRPCInvalidParamsError(data={
                    'id': 'missing_signature',
                    'message': 'Missing Signature'
                })

            if not validate_signature(signature):
                raise JsonRPCInvalidParamsError(
                    data={
                        'id':
                        'invalid_signature',
                        'message':
                        'Invalid Signature: {}'.format('Invalid length' if len(
                            signature) != 132 else 'Invalid hex value')
                    })

            try:
                sig = data_decoder(signature)
            except Exception:
                log.exception(
                    "Unexpected error decoding valid signature: {}".format(
                        signature))
                raise JsonRPCInvalidParamsError(data={
                    'id': 'invalid_signature',
                    'message': 'Invalid Signature'
                })

            add_signature_to_transaction(tx, sig)

        # validate network id, if it's not for "all networks"
        if tx.network_id is not None and self.network_id != tx.network_id:
            raise JsonRPCInvalidParamsError(data={
                'id': 'invalid_network_id',
                'message': 'Invalid Network ID'
            })

        from_address = data_encoder(tx.sender)
        to_address = data_encoder(tx.to)

        # prevent spamming of transactions with the same nonce from the same sender
        with RedisLock(self.redis,
                       "{}:{}".format(from_address, tx.nonce),
                       raise_when_locked=partial(JsonRPCInvalidParamsError,
                                                 data={
                                                     'id':
                                                     'invalid_nonce',
                                                     'message':
                                                     'Nonce already used'
                                                 }),
                       ex=5):

            # disallow transaction overwriting for known transactions
            async with self.db:
                existing = await self.db.fetchrow(
                    "SELECT * FROM transactions WHERE "
                    "from_address = $1 AND nonce = $2 AND status != $3",
                    from_address, tx.nonce, 'error')
            if existing:
                # debugging checks
                existing_tx = await self.eth.eth_getTransactionByHash(
                    existing['hash'])
                raise JsonRPCInvalidParamsError(data={
                    'id': 'invalid_nonce',
                    'message': 'Nonce already used'
                })

            # make sure the account has enough funds for the transaction
            network_balance, balance, _, _ = await self.get_balances(
                from_address)

            #log.info("Attempting to send transaction\nHash: {}\n{} -> {}\nValue: {} + {} (gas) * {} (startgas) = {}\nSender's Balance {} ({} unconfirmed)".format(
            #    calculate_transaction_hash(tx), from_address, to_address, tx.value, tx.startgas, tx.gasprice, tx.value + (tx.startgas * tx.gasprice), network_balance, balance))

            if balance < (tx.value + (tx.startgas * tx.gasprice)):
                raise JsonRPCInsufficientFundsError(
                    data={
                        'id': 'insufficient_funds',
                        'message': 'Insufficient Funds'
                    })

            # validate the nonce
            c_nonce = await self.get_transaction_count(from_address)

            if tx.nonce < c_nonce:
                raise JsonRPCInvalidParamsError(
                    data={
                        'id': 'invalid_nonce',
                        'message': 'Provided nonce is too low'
                    })
            if tx.nonce > c_nonce:
                raise JsonRPCInvalidParamsError(
                    data={
                        'id': 'invalid_nonce',
                        'message': 'Provided nonce is too high'
                    })

            if tx.intrinsic_gas_used > tx.startgas:
                raise JsonRPCInvalidParamsError(
                    data={
                        'id':
                        'invalid_transaction',
                        'message':
                        'Transaction gas is too low. There is not enough gas to cover minimal cost of the transaction (minimal: {}, got: {}). Try increasing supplied gas.'
                        .format(tx.intrinsic_gas_used, tx.startgas)
                    })

            # now this tx fits enough of the criteria to allow it
            # onto the transaction queue
            tx_hash = calculate_transaction_hash(tx)

            # add tx to database
            async with self.db:
                await self.db.execute(
                    "INSERT INTO transactions "
                    "(hash, from_address, to_address, nonce, "
                    "value, gas, gas_price, "
                    "data, v, r, s, "
                    "sender_toshi_id) "
                    "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)",
                    tx_hash, from_address, to_address, tx.nonce, hex(tx.value),
                    hex(tx.startgas), hex(tx.gasprice), data_encoder(tx.data),
                    hex(tx.v), hex(tx.r), hex(tx.s), self.user_toshi_id)
                await self.db.commit()

            # trigger processing the transaction queue
            self.tasks.process_transaction_queue(from_address)
            # analytics
            # use notification registrations to try find toshi ids for users
            if self.user_toshi_id:
                sender_toshi_id = self.user_toshi_id
            else:
                async with self.db:
                    sender_toshi_id = await self.db.fetchval(
                        "SELECT toshi_id FROM notification_registrations WHERE "
                        "eth_address = $1", from_address)
            async with self.db:
                receiver_toshi_id = await self.db.fetchval(
                    "SELECT toshi_id FROM notification_registrations WHERE "
                    "eth_address = $1", to_address)
            self.track(sender_toshi_id, "Sent transaction")
            # it doesn't make sense to add user agent here as we
            # don't know the receiver's user agent
            self.track(receiver_toshi_id,
                       "Received transaction",
                       add_user_agent=False)

        return tx_hash
Example #14
0
    async def send_transaction(self, *, tx, signature=None):

        try:
            tx = decode_transaction(tx)
        except:
            raise JsonRPCInvalidParamsError(data={
                'id': 'invalid_transaction',
                'message': 'Invalid Transaction'
            })

        if is_transaction_signed(tx):

            tx_sig = data_encoder(signature_from_transaction(tx))

            if signature:

                if tx_sig != signature:

                    raise JsonRPCInvalidParamsError(
                        data={
                            'id':
                            'invalid_signature',
                            'message':
                            'Invalid Signature: Signature in payload and signature of transaction do not match'
                        })
            else:

                signature = tx_sig
        else:

            if signature is None:
                raise JsonRPCInvalidParamsError(data={
                    'id': 'missing_signature',
                    'message': 'Missing Signature'
                })

            if not validate_signature(signature):
                raise JsonRPCInvalidParamsError(
                    data={
                        'id':
                        'invalid_signature',
                        'message':
                        'Invalid Signature: {}'.format('Invalid length' if len(
                            signature) != 132 else 'Invalid hex value')
                    })

            try:
                sig = data_decoder(signature)
            except Exception:
                log.exception(
                    "Unexpected error decoding valid signature: {}".format(
                        signature))
                raise JsonRPCInvalidParamsError(data={
                    'id': 'invalid_signature',
                    'message': 'Invalid Signature'
                })

            add_signature_to_transaction(tx, sig)

        # validate network id, if it's not for "all networks"
        if tx.network_id is not None and self.network_id != tx.network_id:
            raise JsonRPCInvalidParamsError(data={
                'id': 'invalid_network_id',
                'message': 'Invalid Network ID'
            })

        from_address = data_encoder(tx.sender)
        to_address = data_encoder(tx.to)

        # prevent spamming of transactions with the same nonce from the same sender
        async with RedisLock("{}:{}".format(from_address, tx.nonce),
                             raise_when_locked=partial(
                                 JsonRPCInvalidParamsError,
                                 data={
                                     'id': 'invalid_nonce',
                                     'message': 'Nonce already used'
                                 }),
                             ex=5):

            # check for transaction overwriting
            async with self.db:
                existing = await self.db.fetchrow(
                    "SELECT * FROM transactions WHERE "
                    "from_address = $1 AND nonce = $2 AND "
                    "(status != 'error' or status is NULL)", from_address,
                    tx.nonce)

            # disallow transaction overwriting when the gas is lower or the transaction is confirmed
            if existing and (parse_int(existing['gas_price']) >= tx.gasprice
                             or existing['status'] == 'confirmed'):
                raise JsonRPCInvalidParamsError(data={
                    'id': 'invalid_nonce',
                    'message': 'Nonce already used'
                })

            # make sure the account has enough funds for the transaction
            network_balance, balance, _, _ = await self.get_balances(
                from_address)
            if existing:
                balance += parse_int(existing['value']) + parse_int(
                    existing['gas']) * parse_int(existing['gas_price'])

            if balance < (tx.value + (tx.startgas * tx.gasprice)):
                raise JsonRPCInsufficientFundsError(
                    data={
                        'id': 'insufficient_funds',
                        'message': 'Insufficient Funds'
                    })

            # validate the nonce (only necessary if tx doesn't already exist)
            if not existing:
                c_nonce = await self.get_transaction_count(from_address)

                if tx.nonce < c_nonce:
                    raise JsonRPCInvalidParamsError(
                        data={
                            'id': 'invalid_nonce',
                            'message': 'Provided nonce is too low'
                        })
                if tx.nonce > c_nonce:
                    raise JsonRPCInvalidParamsError(
                        data={
                            'id': 'invalid_nonce',
                            'message': 'Provided nonce is too high'
                        })

            if tx.intrinsic_gas_used > tx.startgas:
                raise JsonRPCInvalidParamsError(
                    data={
                        'id':
                        'invalid_transaction',
                        'message':
                        'Transaction gas is too low. There is not enough gas to cover minimal cost of the transaction (minimal: {}, got: {}). Try increasing supplied gas.'
                        .format(tx.intrinsic_gas_used, tx.startgas)
                    })

            # now this tx fits enough of the criteria to allow it
            # onto the transaction queue
            tx_hash = calculate_transaction_hash(tx)

            if existing:
                log.info(
                    "Setting tx '{}' to error due to forced overwrite".format(
                        existing['hash']))
                manager_dispatcher.update_transaction(
                    existing['transaction_id'], 'error')

            data = data_encoder(tx.data)
            if data and \
               ((data.startswith("0xa9059cbb") and len(data) == 138) or \
                (data.startswith("0x23b872dd") and len(data) == 202)):
                # check if the token is a known erc20 token
                async with self.db:
                    erc20_token = await self.db.fetchrow(
                        "SELECT * FROM tokens WHERE contract_address = $1",
                        to_address)
            else:
                erc20_token = False

            # add tx to database
            async with self.db:
                db_tx = await self.db.fetchrow(
                    "INSERT INTO transactions "
                    "(hash, from_address, to_address, nonce, "
                    "value, gas, gas_price, "
                    "data, v, r, s, "
                    "sender_toshi_id) "
                    "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) "
                    "RETURNING transaction_id", tx_hash, from_address,
                    to_address, tx.nonce, hex(tx.value), hex(tx.startgas),
                    hex(tx.gasprice), data_encoder(tx.data), hex(tx.v),
                    hex(tx.r), hex(tx.s), self.user_toshi_id)

                if erc20_token:
                    token_value = int(data[-64:], 16)
                    if data.startswith("0x23b872dd"):
                        erc20_from_address = "0x" + data[34:74]
                        erc20_to_address = "0x" + data[98:138]
                    else:
                        erc20_from_address = from_address
                        erc20_to_address = "0x" + data[34:74]
                    await self.db.execute(
                        "INSERT INTO token_transactions "
                        "(transaction_id, transaction_log_index, contract_address, from_address, to_address, value) "
                        "VALUES ($1, $2, $3, $4, $5, $6)",
                        db_tx['transaction_id'], 0,
                        erc20_token['contract_address'], erc20_from_address,
                        erc20_to_address, hex(token_value))

                await self.db.commit()

            # trigger processing the transaction queue
            manager_dispatcher.process_transaction_queue(from_address)
            # analytics
            # use notification registrations to try find toshi ids for users
            if self.user_toshi_id:
                sender_toshi_id = self.user_toshi_id
            else:
                async with self.db:
                    sender_toshi_id = await self.db.fetchval(
                        "SELECT toshi_id FROM notification_registrations WHERE "
                        "eth_address = $1", from_address)
            async with self.db:
                receiver_toshi_id = await self.db.fetchval(
                    "SELECT toshi_id FROM notification_registrations WHERE "
                    "eth_address = $1", to_address)
            self.track(sender_toshi_id, "Sent transaction")
            # it doesn't make sense to add user agent here as we
            # don't know the receiver's user agent
            self.track(receiver_toshi_id,
                       "Received transaction",
                       add_user_agent=False)

        return tx_hash
    async def test_always_get_unconfirmed_push_notification(self, *, ethminer, push_client):
        """Tests that when tx's are send through our systems we always get
        an unconfirmed push notification"""

        # register for GCM PNs
        body = {
            "registration_id": TEST_GCM_ID,
            "address": TEST_WALLET_ADDRESS
        }
        resp = await self.fetch_signed("/gcm/register", signing_key=TEST_ID_KEY, method="POST", body=body)
        self.assertResponseCodeEqual(resp, 204, resp.body)

        async with self.pool.acquire() as con:
            rows = await con.fetch("SELECT * FROM notification_registrations WHERE toshi_id = $1", TEST_ID_ADDRESS)
        self.assertIsNotNone(rows)
        self.assertEqual(len(rows), 1)

        # run this a bunch of times to see if
        # we can expose any race conditions
        for iteration in range(4):

            if iteration > 2:
                ethminer.pause()

            value = 2821181018869341261

            resp = await self.fetch("/tx/skel", method="POST", body={
                "from": FAUCET_ADDRESS,
                "to": TEST_WALLET_ADDRESS,
                "value": value
            })
            self.assertResponseCodeEqual(resp, 200, resp.body)
            body = json_decode(resp.body)
            tx = sign_transaction(body['tx'], FAUCET_PRIVATE_KEY)
            resp = await self.fetch("/tx", method="POST", body={
                "tx": tx
            })
            self.assertResponseCodeEqual(resp, 200, resp.body)
            tx_hash = json_decode(resp.body)['tx_hash']

            tx = decode_transaction(tx)
            self.assertEqual(tx_hash, data_encoder(tx.hash))

            if iteration > 2:
                await asyncio.sleep(5)
                ethminer.start()

            async with self.pool.acquire() as con:
                rows = await con.fetch("SELECT * FROM transactions WHERE nonce = $1", tx.nonce)
            self.assertEqual(len(rows), 1)

            unconfirmed_count = 0
            while True:
                token, payload = await push_client.get()

                self.assertEqual(token, TEST_GCM_ID)

                message = parse_sofa_message(payload['message'])

                self.assertIsInstance(message, SofaPayment)
                self.assertEqual(message['value'], hex(value))
                self.assertEqual(message['txHash'], tx_hash)

                if message['status'] == "confirmed":
                    break

                self.assertEqual(message['status'], "unconfirmed")
                unconfirmed_count += 1
            # when the tx is sent through our systems we should
            # always get one unconfirmed notification, and we
            # should never get more than one
            self.assertEqual(unconfirmed_count, 1)
Example #16
0
    def do_POST(self):

        # TODO: figure out why read is blocking here
        data = self.rfile.read(len(self.rfile.peek()))
        data = data.decode('utf-8')
        data = json.loads(data)

        if self.path == "/v1/tx/skel":

            gas_price = parse_int(
                data['gas_price']) if 'gas_price' in data else DEFAULT_GASPRICE
            gas = parse_int(data['gas']) if 'gas' in data else DEFAULT_STARTGAS
            nonce = parse_int(data['nonce']) if 'nonce' in data else 0

            if 'value' not in data or 'from' not in data or 'to' not in data:
                self.write_data(
                    400, {
                        'errors': [{
                            'id': 'bad_arguments',
                            'message': 'Bad Arguments'
                        }]
                    })
                return
            value = parse_int(data['value'])
            to_address = data['to']
            from_address = data['from']

            if not validate_address(to_address):
                self.write_data(
                    400, {
                        'errors': [{
                            'id': 'invalid_to_address',
                            'message': 'Invalid To Address'
                        }]
                    })
                return
            if not validate_address(from_address):
                self.write_data(
                    400, {
                        'errors': [{
                            'id': 'invalid_from_address',
                            'message': 'Invalid From Address'
                        }]
                    })
                return

            tx = create_transaction(nonce=nonce,
                                    gasprice=gas_price,
                                    startgas=gas,
                                    to=to_address,
                                    value=value)

            transaction = encode_transaction(tx)

            self.write_data(
                200, {
                    "tx_data": {
                        "nonce": hex(nonce),
                        "from": from_address,
                        "to": to_address,
                        "value": hex(value),
                        "startGas": hex(gas),
                        "gasPrice": hex(gas_price)
                    },
                    "tx": transaction
                })

        elif self.path == "/v1/tx":

            tx = decode_transaction(data['tx'])

            if 'signature' in data:

                sig = data_decoder(data['signature'])

                add_signature_to_transaction(tx, sig)

            self.write_data(200, {"tx_hash": data_encoder(tx.hash)})

        else:

            self.write_data(404)
    async def test_tx_overwrite(self, *, ethminer, parity, push_client):
        """Tests that if a transaction with the same nonce and one the system knows about
        is sent from outside of the system and included in the block, that the error
        handling picks this up correctly"""

        # start 2nd parity server
        p2 = ParityServer(bootnodes=parity.dsn()['node'])
        e2 = EthMiner(jsonrpc_url=p2.dsn()['url'], debug=False)
        rpcclient2 = JsonRPCClient(p2.dsn()['url'])

        addr1, pk1 = TEST_ADDRESSES[0]
        addr2, pk2 = TEST_ADDRESSES[1]
        addr3, pk3 = TEST_ADDRESSES[2]

        val = 1000 * 10**18

        # send funds to address1
        f_tx_hash = await self.send_tx(FAUCET_PRIVATE_KEY, TEST_ADDRESS_1, val)

        # make sure sync is done
        while True:
            data2 = await rpcclient2.eth_getTransactionByHash(f_tx_hash)
            if data2 and data2['blockNumber'] is not None:
                break
            await asyncio.sleep(1)

        # make sure no blocks are mined
        ethminer.pause()
        e2.pause()

        # make sure transactions are "interesting" to the monitory
        async with self.pool.acquire() as con:
            for addr, pk in TEST_ADDRESSES[:3]:
                await con.fetch(
                    "INSERT INTO notification_registrations (service, registration_id, toshi_id, eth_address) VALUES ($1, $2, $3, $4)",
                    'gcm', "abc", addr, addr)

        # create two transactions with the same nonce and submit them both
        tx1 = await self.get_tx_skel(pk1, addr2, int(val / 3))
        tx2 = await self.get_tx_skel(pk1,
                                     addr3,
                                     int(val / 3),
                                     nonce=decode_transaction(tx1).nonce)
        tx2 = sign_transaction(tx2, pk1)
        tx_hash_2 = await rpcclient2.eth_sendRawTransaction(tx2)
        tx_hash_1 = await self.sign_and_send_tx(pk1, tx1)

        # start mining again
        e2.start()

        # wait for one of the two transactions to complete
        try:
            while True:
                async with self.pool.acquire() as con:
                    tx1_row = await con.fetchrow(
                        "SELECT * FROM transactions WHERE hash = $1",
                        tx_hash_1)
                    tx2_row = await con.fetchrow(
                        "SELECT * FROM transactions WHERE hash = $1",
                        tx_hash_2)
                if tx2_row is not None and tx2_row['status'] == 'confirmed':
                    # good!
                    break
                if tx1_row is not None and tx1_row['status'] == 'confirmed':
                    self.assertFail(
                        "tx1 confirmed, expected tx1 overwrite and tx2 confirmed"
                    )
                await asyncio.sleep(1)
        finally:
            e2.stop()
            p2.stop()