Source code for teos.carrier

from teos.logger import get_logger
from teos.tools import bitcoin_cli
import teos.utils.rpc_errors as rpc_errors
from teos.utils.auth_proxy import JSONRPCException
from common.errors import UNKNOWN_JSON_RPC_EXCEPTION, RPC_TX_REORGED_AFTER_BROADCAST

# FIXME: This class is not fully covered by unit tests


[docs]class Receipt: """ The :class:`Receipt` class represent the interaction between the :obj:`Carrier` and ``bitcoind`` when broadcasting transactions. It is used to signal whether or not a transaction has been successfully broadcast and why. Args: delivered (:obj:`bool`): whether or not the transaction has been successfully broadcast. confirmations (:obj:`int`): the number of confirmations of the transaction to broadcast. In certain situations the :obj:`Carrier` may fail to broadcast a transaction because it was already in the blockchain. This attribute signals those situations. reason (:obj:`int`): an error code describing why the transaction broadcast failed. Returns: :obj:`Receipt`: A receipt describing whether or not the transaction was delivered. Notice that transactions that are already on chain are flagged as delivered with a ``confirmations > 0`` whereas new transactions are so with ``confirmations = 0``. """ def __init__(self, delivered, confirmations=0, reason=None): self.delivered = delivered self.confirmations = confirmations self.reason = reason
[docs]class Carrier: """ The :class:`Carrier` is in charge of interacting with ``bitcoind`` to send/get transactions. It uses :obj:`Receipt` objects to report about the sending outcome. Args: btc_connect_params (:obj:`dict`): a dictionary with the parameters to connect to bitcoind (``rpc user, rpc password, host and port``). Attributes: logger (:obj:`Logger <teos.logger.Logger>`): The logger for this component. issued_receipts (:obj:`dict`): A dictionary of issued receipts to prevent resending the same transaction over and over. It should periodically be reset to prevent it from growing unbounded. """ def __init__(self, btc_connect_params): self.logger = get_logger(component=Carrier.__name__) self.btc_connect_params = btc_connect_params self.issued_receipts = {} # NOTCOVERED
[docs] def send_transaction(self, rawtx, txid): """ Tries to send a given raw transaction to the Bitcoin network using ``bitcoind``. Args: rawtx (:obj:`str`): a (potentially) signed raw transaction ready to be broadcast. txid (:obj:`str`): the transaction id corresponding to ``rawtx``. Returns: :obj:`Receipt`: A receipt reporting whether the transaction was successfully delivered or not and why. """ if txid in self.issued_receipts: self.logger.info("Transaction already sent", txid=txid) receipt = self.issued_receipts[txid] return receipt try: self.logger.info("Pushing transaction to the network", txid=txid, rawtx=rawtx) bitcoin_cli(self.btc_connect_params).sendrawtransaction(rawtx) receipt = Receipt(delivered=True) except JSONRPCException as e: errno = e.error.get("code") # Since we're pushing a raw transaction to the network we can face several rejections if errno == rpc_errors.RPC_VERIFY_REJECTED: # DISCUSS: 37-transaction-rejection receipt = Receipt(delivered=False, reason=rpc_errors.RPC_VERIFY_REJECTED) self.logger.error("Transaction couldn't be broadcast", error=e.error) elif errno == rpc_errors.RPC_VERIFY_ERROR: # DISCUSS: 37-transaction-rejection receipt = Receipt(delivered=False, reason=rpc_errors.RPC_VERIFY_ERROR) self.logger.error("Transaction couldn't be broadcast", error=e.error) elif errno == rpc_errors.RPC_VERIFY_ALREADY_IN_CHAIN: self.logger.info("Transaction is already in the blockchain. Getting confirmation count", txid=txid) # If the transaction is already in the chain, we get the number of confirmations and watch the tracker # until the end of the appointment tx_info = self.get_transaction(txid) if tx_info is not None: confirmations = int(tx_info.get("confirmations")) receipt = Receipt( delivered=True, confirmations=confirmations, reason=rpc_errors.RPC_VERIFY_ALREADY_IN_CHAIN ) else: # There's a really unlikely edge case where a transaction can be reorged between receiving the # notification and querying the data. Notice that this implies the tx being also kicked off the # mempool, which again is really unlikely. receipt = Receipt(delivered=False, reason=RPC_TX_REORGED_AFTER_BROADCAST) elif errno == rpc_errors.RPC_DESERIALIZATION_ERROR: # Adding this here just for completeness. We should never end up here. The Carrier only sends txs # handed by the Responder, who receives them from the Watcher, who checks that the tx can be properly # deserialized self.logger.info("Transaction cannot be deserialized", txid=txid) receipt = Receipt(delivered=False, reason=rpc_errors.RPC_DESERIALIZATION_ERROR) else: # If something else happens (unlikely but possible) log it so we can treat it in future releases self.logger.error("JSONRPCException", method="Carrier.send_transaction", error=e.error) receipt = Receipt(delivered=False, reason=UNKNOWN_JSON_RPC_EXCEPTION) self.issued_receipts[txid] = receipt return receipt
[docs] def get_transaction(self, txid): """ Queries transaction data to ``bitcoind`` given a transaction id. Args: txid (:obj:`str`): a 32-byte hex-formatted string representing the transaction id. Returns: :obj:`dict` or :obj:`None`: A dictionary with the transaction data if the transaction can be found on the chain. :obj:`None` otherwise. """ try: tx_info = bitcoin_cli(self.btc_connect_params).getrawtransaction(txid, 1) return tx_info except JSONRPCException as e: # While it's quite unlikely, the transaction that was already in the blockchain could have been # reorged while we were querying bitcoind to get the confirmation count. In that case we just restart # the tracker if e.error.get("code") == rpc_errors.RPC_INVALID_ADDRESS_OR_KEY: self.logger.info("Transaction not found in mempool nor blockchain", txid=txid) else: # If something else happens (unlikely but possible) log it so we can treat it in future releases self.logger.error("JSONRPCException", method="Carrier.get_transaction", error=e.error) return None