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_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 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
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