async def cancel_queued_transaction(self, tx_hash, signature): if not validate_transaction_hash(tx_hash): raise JsonRPCInvalidParamsError(data={'id': 'invalid_transaction_hash', 'message': 'Invalid Transaction Hash'}) if not validate_signature(signature): raise JsonRPCInvalidParamsError(data={'id': 'invalid_signature', 'message': 'Invalid Signature'}) async with self.db: tx = await self.db.fetchrow("SELECT * FROM transactions WHERE hash = $1 AND (status != 'error' OR status IS NULL)", tx_hash) if tx is None: raise JsonRPCError(None, -32000, "Transaction not found", {'id': 'not_found', 'message': 'Transaction not found'}) elif tx['status'] != 'queued' and tx['status'] is not None: raise JsonRPCError(None, -32000, "Transaction already sent to node", {'id': 'invalid_transaction_status', 'message': 'Transaction already sent to node'}) message = "Cancel transaction " + tx_hash if not personal_ecrecover(message, signature, tx['from_address']): raise JsonRPCError(None, -32000, "Permission Denied", {'id': 'permission_denied', 'message': 'Permission Denied'}) log.info("Setting tx '{}' to error due to user cancelation".format(tx['hash'])) self.tasks.update_transaction(tx['transaction_id'], 'error')
def test_validate_signature(self): self.assertTrue( utils.validate_signature( "0x114655db4898a6580f0abfc53fc0c0a88110724abf8d41f2abf206c69d7d4c821ed2cdf6939484ef6aebc39ce5662363b82140106bbc374a0f1381b6948214b001" )) self.assertTrue( utils.validate_signature( u"0x114655db4898a6580f0abfc53fc0c0a88110724abf8d41f2abf206c69d7d4c821ed2cdf6939484ef6aebc39ce5662363b82140106bbc374a0f1381b6948214b001" )) self.assertFalse(utils.validate_signature("hello")) self.assertFalse(utils.validate_signature("0x12345")) self.assertFalse(utils.validate_signature(None)) self.assertFalse(utils.validate_signature({})) self.assertFalse( utils.validate_signature( 0x114655db4898a6580f0abfc53fc0c0a88110724abf8d41f2abf206c69d7d4c821ed2cdf6939484ef6aebc39ce5662363b82140106bbc374a0f1381b6948214b001 )) self.assertFalse( utils.validate_signature( "0x056db290f8ba3250ca64a45d16284d04bc6f5fbf"))
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
def verify_request(self): """Verifies that the signature and the payload match the expected address raising a JSONHTTPError (400) if something is wrong with the request""" if TOSHI_ID_ADDRESS_HEADER in self.request.headers: expected_address = self.request.headers[ TOSHI_ID_ADDRESS_HEADER] elif self.get_argument(TOSHI_ID_ADDRESS_QUERY_ARG, None): expected_address = self.get_argument( TOSHI_ID_ADDRESS_QUERY_ARG) elif TOKEN_ID_ADDRESS_HEADER in self.request.headers: expected_address = self.request.headers[ TOKEN_ID_ADDRESS_HEADER] elif self.get_argument(TOKEN_ID_ADDRESS_QUERY_ARG, None): expected_address = self.get_argument( TOKEN_ID_ADDRESS_QUERY_ARG) else: raise JSONHTTPError(400, body={ 'errors': [{ 'id': 'bad_arguments', 'message': 'Missing Toshi-ID-Address' }] }) if TOSHI_SIGNATURE_HEADER in self.request.headers: signature = self.request.headers[TOSHI_SIGNATURE_HEADER] elif self.get_argument(TOSHI_SIGNATURE_QUERY_ARG, None): signature = self.get_argument(TOSHI_SIGNATURE_QUERY_ARG) elif TOKEN_SIGNATURE_HEADER in self.request.headers: signature = self.request.headers[TOKEN_SIGNATURE_HEADER] elif self.get_argument(TOKEN_SIGNATURE_QUERY_ARG, None): signature = self.get_argument(TOKEN_SIGNATURE_QUERY_ARG) else: raise JSONHTTPError(400, body={ 'errors': [{ 'id': 'bad_arguments', 'message': 'Missing Toshi-Signature' }] }) if TOSHI_TIMESTAMP_HEADER in self.request.headers: timestamp = self.request.headers[TOSHI_TIMESTAMP_HEADER] elif self.get_argument(TOSHI_TIMESTAMP_QUERY_ARG, None): timestamp = self.get_argument(TOSHI_TIMESTAMP_QUERY_ARG) elif TOKEN_TIMESTAMP_HEADER in self.request.headers: timestamp = self.request.headers[TOKEN_TIMESTAMP_HEADER] elif self.get_argument(TOKEN_TIMESTAMP_QUERY_ARG, None): timestamp = self.get_argument(TOKEN_TIMESTAMP_QUERY_ARG) else: raise JSONHTTPError(400, body={ 'errors': [{ 'id': 'bad_arguments', 'message': 'Missing Toshi-Timestamp' }] }) timestamp = parse_int(timestamp) if timestamp is None: raise JSONHTTPError(400, body={ 'errors': [{ 'id': 'invalid_timestamp', 'message': 'Given Toshi-Timestamp is invalid' }] }) if not validate_address(expected_address): raise JSONHTTPError(400, body={ 'errors': [{ 'id': 'invalid_id_address', 'message': 'Invalid Toshi-ID-Address' }] }) if not validate_signature(signature): raise JSONHTTPError(400, body={ 'errors': [{ 'id': 'invalid_signature', 'message': 'Invalid Toshi-Signature' }] }) try: signature = data_decoder(signature) except Exception: raise JSONHTTPError(400, body={ 'errors': [{ 'id': 'invalid_signature', 'message': 'Invalid Toshi-Signature' }] }) verb = self.request.method uri = self.request.path if self.request.body: datahash = self.request.body else: datahash = "" data_string = generate_request_signature_data_string( verb, uri, timestamp, datahash) if not ecrecover(data_string, signature, expected_address): raise JSONHTTPError(400, body={ 'errors': [{ 'id': 'invalid_signature', 'message': 'Invalid Toshi-Signature' }] }) if abs(int(time.time()) - timestamp) > TIMESTAMP_EXPIRY: raise JSONHTTPError( 400, body={ 'errors': [{ 'id': 'invalid_timestamp', 'message': 'The difference between the timestamp and the current time is too large' }] }) return expected_address