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)
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) 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]['transaction_hash'], tx_hash) await self.wait_on_tx_confirmation(tx_hash)
async def test_create_and_send_transaction(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 = 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) await self.wait_on_tx_confirmation(tx_hash)
async def test_transaction_overwrite(self, *, ethminer, parity, monitor): # make sure no blocks are confirmed for the meantime ethminer.pause() # set up pn registrations async with self.pool.acquire() as con: await con.execute("INSERT INTO notification_registrations (token_id, eth_address) VALUES ($1, $2)", TEST_ID_ADDRESS, TEST_WALLET_ADDRESS) await con.fetch("INSERT INTO push_notification_registrations (service, registration_id, token_id) VALUES ($1, $2, $3)", 'gcm', TEST_GCM_ID, TEST_ID_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 pn = await monitor.pushclient.send_queue.get() # allow other processing to complete await asyncio.sleep(0.5) # send tx2 manually rpcclient = JsonRPCClient(parity.dsn()['url']) tx2_hash = await rpcclient.eth_sendRawTransaction(sign_transaction(tx2, FAUCET_PRIVATE_KEY)) # we expect 2 push notifications, one for the error of the # overwritten txz and one for the new tx pn = await monitor.pushclient.send_queue.get() pn = await monitor.pushclient.send_queue.get() # allow other processing to complete await asyncio.sleep(0.5) async with self.pool.acquire() as con: tx1_row = await con.fetchrow("SELECT * FROM transactions WHERE transaction_hash = $1", tx1_hash) tx2_row = await con.fetchrow("SELECT * FROM transactions WHERE transaction_hash = $1", tx2_hash) self.assertEqual(tx1_row['last_status'], 'error') self.assertEqual(tx2_row['last_status'], 'unconfirmed')
def test_send_tx(self): tx = decode_transaction( "0xe9808504a817c80082520894db089a4f9a8c5f17040b4fc51647e942b5fc601d880de0b6b3a764000080" ) sig = "0xf5a43adea07d366ae420a5c75a5cae6c60d3e4aaa0b72c2f37fc387efd43d7fd30c4327f2dbd959f654857f58912129b09763329459d08e25547d895ae90fa0f01" resp = self.service_client.send_tx(tx, sig) self.assertEqual( resp, "0x07ca664267650c87c11318710e3afa2f8f191814d25def8e0c4f2768f3ef5ccb" )
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'])
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_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)
async def test_missing_txs(self, *, ethminer, parity, monitor): values = [] for i in range(10): tx = await self.get_tx_skel(FAUCET_PRIVATE_KEY, TEST_WALLET_ADDRESS, random.randint(1, 10) ** 18, i) tx = decode_transaction(tx) values.append((data_encoder(tx.hash), FAUCET_ADDRESS, data_encoder(tx.to), tx.value, -1, datetime.utcnow() - timedelta(hours=1))) async with self.pool.acquire() as con: await con.executemany( "INSERT INTO transactions (transaction_hash, from_address, to_address, value, nonce, created) " "VALUES ($1, $2, $3, $4, $5, $6)", values) count = await con.fetchrow("SELECT COUNT(*) FROM transactions WHERE (last_status != 'error' OR last_status IS NULL)") self.assertEqual(count['count'], 10) await monitor.sanity_check() async with self.pool.acquire() as con: unconf_count = await con.fetchrow("SELECT COUNT(*) FROM transactions WHERE last_status = $1 OR last_status IS NULL", 'unconfirmed') error_count = await con.fetchrow("SELECT COUNT(*) FROM transactions WHERE last_status = $1", 'error') self.assertEqual(unconf_count['count'], 0) self.assertEqual(error_count['count'], 10)
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): if signature: tx_sig = signature_from_transaction(tx) if tx_sig != signature: raise JsonRPCInvalidParamsError( data={ 'id': 'invalid_signature', 'message': 'Invalid Signature: Signature in payload and signature of transaction do not match' }) 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: signature = 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, signature) 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 last_status != $3", from_address, tx.nonce, 'error') if existing: # debugging checks existing_tx = await self.eth.eth_getTransactionByHash( existing['transaction_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, ignore_pending_recieved=True) log.info( "Attempting to send transaction\n{} -> {}\nValue: {} + {} (gas) * {} (startgas) = {}\nSender's Balance {} ({} unconfirmed)" .format(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 = self.redis.get("nonce:{}".format(from_address)) if c_nonce: c_nonce = int(c_nonce) # get the network's value too nw_nonce = await self.eth.eth_getTransactionCount(from_address) if c_nonce is None or nw_nonce > c_nonce: c_nonce = nw_nonce 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' }) # send the transaction to the network try: tx_encoded = encode_transaction(tx) tx_hash = await self.eth.eth_sendRawTransaction(tx_encoded) except JsonRPCError as e: log.error(e.format()) raise JsonRPCInternalError( data={ 'id': 'unexpected_error', 'message': 'An error occured communicating with the ethereum network, try again later' }) # cache nonce self.redis.set("nonce:{}".format(from_address), tx.nonce + 1) # add tx to database async with self.db: await self.db.execute( "INSERT INTO transactions " "(transaction_hash, from_address, to_address, nonce, value, estimated_gas_cost, sender_token_id) " "VALUES ($1, $2, $3, $4, $5, $6, $7)", tx_hash, from_address, to_address, tx.nonce, str(tx.value), str(tx.startgas * tx.gasprice), self.user_token_id) await self.db.commit() # if there is a block monitor, force send PNs for this without # waiting for the node to see it if hasattr(self.application, 'monitor'): txjson = transaction_to_json(tx) assert txjson['hash'] == tx_hash IOLoop.current().add_callback( self.application.monitor.send_transaction_notifications, txjson) return tx_hash
async def test_sending_transactions_and_storing_the_hash_correctly(self, *, monitor): """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 } resp = await self.fetch_signed("/gcm/register", signing_key=TEST_ID_KEY, method="POST", body=body) self.assertResponseCodeEqual(resp, 204, resp.body) # register for notifications for the test address body = { "addresses": [TEST_ID_ADDRESS] } resp = await self.fetch_signed("/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]['transaction_hash'], tx_hash) if rows[0]['last_status'] is not None: self.assertEqual(rows[0]['last_status'], 'unconfirmed') self.assertIsNone(rows[0]['error']) await self.wait_on_tx_confirmation(tx_hash, check_db) while True: token, payload = await monitor.pushclient.send_queue.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_always_get_unconfirmed_push_notification(self, *, ethminer, monitor): """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 } 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 push_notification_registrations WHERE token_id = $1", TEST_ID_ADDRESS) self.assertIsNotNone(rows) self.assertEqual(len(rows), 1) # register for notifications for the test address body = { "addresses": [TEST_WALLET_ADDRESS] } resp = await self.fetch_signed("/register", signing_key=TEST_ID_KEY, method="POST", body=body) self.assertResponseCodeEqual(resp, 204, resp.body) # 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 monitor.pushclient.send_queue.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)
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)