Skip to content
This repository was archived by the owner on Sep 26, 2022. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,3 @@ omit =
teos/teosd.py
teos/logger.py
teos/sample_conf.py
teos/utils/auth_proxy.py
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ structlog
python-daemon
waitress
gunicorn; platform_system != "Windows"
python-bitcoinlib
73 changes: 43 additions & 30 deletions teos/block_processor.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from teos.logger import get_logger
import teos.utils.rpc_errors as rpc_errors
from common.exceptions import BasicException

from teos.tools import bitcoin_cli
from teos.utils.auth_proxy import JSONRPCException
import bitcoin.rpc
from bitcoin.rpc import JSONRPCError
from bitcoin.core import b2x, b2lx, lx


class InvalidTransactionFormat(BasicException):
Expand All @@ -26,6 +28,24 @@ def __init__(self, btc_connect_params):
self.logger = get_logger(component=BlockProcessor.__name__)
self.btc_connect_params = btc_connect_params

def proxy(self):
"""
Returns a new ``http`` connection with ``bitcoind`` using the ``json-rpc`` interface, using
``btc_connect_params`` for the connectio parameters.

Returns:
:obj:`Proxy <bitcoin.rpc.Proxy>`: An authenticated service proxy to ``bitcoind``
that can be used to send ``json-rpc`` commands.
"""

service_url = "http://%s:%s@%s:%d" % (
self.btc_connect_params.get("BTC_RPC_USER"),
self.btc_connect_params.get("BTC_RPC_PASSWORD"),
self.btc_connect_params.get("BTC_RPC_CONNECT"),
self.btc_connect_params.get("BTC_RPC_PORT"),
)
return bitcoin.rpc.Proxy(service_url)

def get_block(self, block_hash):
"""
Gets a block given a block hash by querying ``bitcoind``.
Expand All @@ -38,15 +58,12 @@ def get_block(self, block_hash):

Returns :obj:`None` otherwise.
"""

try:
block = bitcoin_cli(self.btc_connect_params).getblock(block_hash)

except JSONRPCException as e:
block = None
# by using "call" we obtain a dict, rather than a CBlock that we obtain calling .getblock().
return self.proxy().call("getblock", block_hash)
except JSONRPCError as e:
self.logger.error("Couldn't get block from bitcoind", error=e.error)

return block
return None

def get_best_block_hash(self):
"""
Expand All @@ -57,15 +74,11 @@ def get_best_block_hash(self):

Returns :obj:`None` otherwise (not even sure this can actually happen).
"""

try:
block_hash = bitcoin_cli(self.btc_connect_params).getbestblockhash()

except JSONRPCException as e:
block_hash = None
return b2lx(self.proxy().getbestblockhash())
except JSONRPCError as e:
self.logger.error("Couldn't get block hash", error=e.error)

return block_hash
return None

def get_block_count(self):
"""
Expand All @@ -78,13 +91,10 @@ def get_block_count(self):
"""

try:
block_count = bitcoin_cli(self.btc_connect_params).getblockcount()

except JSONRPCException as e:
block_count = None
return self.proxy().getblockcount()
except JSONRPCError as e:
self.logger.error("Couldn't get block count", error=e.error)

return block_count
return None

def decode_raw_transaction(self, raw_tx):
"""
Expand All @@ -99,17 +109,20 @@ def decode_raw_transaction(self, raw_tx):

Raises:
:obj:`InvalidTransactionFormat`: If the `provided ``raw_tx`` has invalid format.
:obj:`JSONRPCError`: on any other error from the rpc call.
"""

try:
tx = bitcoin_cli(self.btc_connect_params).decoderawtransaction(raw_tx)

except JSONRPCException as e:
msg = "Cannot build transaction from decoded data"
self.logger.error(msg, error=e.error)
raise InvalidTransactionFormat(msg)

return tx
return self.proxy().call("decoderawtransaction", raw_tx)
except JSONRPCError as e:
errno = e.error.get("code")
if errno == rpc_errors.RPC_DESERIALIZATION_ERROR:
msg = "Cannot build transaction from decoded data"
self.logger.error(msg, error=e.error)
raise InvalidTransactionFormat(msg)
else:
self.logger.error(e.error.get("message"), error=e.error)
raise e

def get_distance_to_tip(self, target_block_hash):
"""
Expand Down
114 changes: 71 additions & 43 deletions teos/carrier.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
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
import bitcoin.rpc
from bitcoin.rpc import JSONRPCError, VerifyRejectedError, VerifyError, VerifyAlreadyInChainError
from bitcoin.core import x, lx, b2lx
from bitcoin.core.serialize import SerializationError, SerializationTruncationError
from common.errors import UNKNOWN_JSON_RPC_EXCEPTION, RPC_TX_REORGED_AFTER_BROADCAST

# FIXME: This class is not fully covered by unit tests
Expand Down Expand Up @@ -52,6 +55,24 @@ def __init__(self, btc_connect_params):
self.btc_connect_params = btc_connect_params
self.issued_receipts = {}

def proxy(self):
"""
Returns a new ``http`` connection with ``bitcoind`` using the ``json-rpc`` interface, using
``btc_connect_params`` for the connectio parameters.

Returns:
:obj:`Proxy <bitcoin.rpc.Proxy>`: An authenticated service proxy to ``bitcoind``
that can be used to send ``json-rpc`` commands.
"""

service_url = "http://%s:%s@%s:%d" % (
self.btc_connect_params.get("BTC_RPC_USER"),
self.btc_connect_params.get("BTC_RPC_PASSWORD"),
self.btc_connect_params.get("BTC_RPC_CONNECT"),
self.btc_connect_params.get("BTC_RPC_PORT"),
)
return bitcoin.rpc.Proxy(service_url)

# NOTCOVERED
def send_transaction(self, rawtx, txid):
"""
Expand All @@ -73,43 +94,50 @@ def send_transaction(self, rawtx, txid):

try:
self.logger.info("Pushing transaction to the network", txid=txid, rawtx=rawtx)
bitcoin_cli(self.btc_connect_params).sendrawtransaction(rawtx)
tx = bitcoin.core.CTransaction.deserialize(x(rawtx))
self.proxy().sendrawtransaction(tx)

receipt = Receipt(delivered=True)

except JSONRPCException as e:
except (SerializationError, SerializationTruncationError) as e:
receipt = Receipt(delivered=False, reason=rpc_errors.RPC_DESERIALIZATION_ERROR)
self.logger.error("Transaction couldn't be broadcasted", error=e)

# Since we're pushing a raw transaction to the network we can face several rejections
except VerifyRejectedError as e:
# DISCUSS: 37-transaction-rejection
receipt = Receipt(delivered=False, reason=rpc_errors.RPC_VERIFY_REJECTED)
self.logger.error("Transaction couldn't be broadcasted", error=e.error)

except VerifyError as e:
# DISCUSS: 37-transaction-rejection
receipt = Receipt(delivered=False, reason=rpc_errors.RPC_VERIFY_ERROR)
self.logger.error("Transaction couldn't be broadcasted", error=e.error)

except VerifyAlreadyInChainError as e:
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)

except JSONRPCError as e:
# Other errors that don't have a class in python-bitcoinlib

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:
if 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
Expand All @@ -118,7 +146,7 @@ def send_transaction(self, rawtx, 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.send_transaction", error=e.error)
self.logger.error("JSONRPCError", method="Carrier.send_transaction", error=e.error)
receipt = Receipt(delivered=False, reason=UNKNOWN_JSON_RPC_EXCEPTION)

self.issued_receipts[txid] = receipt
Expand All @@ -138,18 +166,18 @@ def get_transaction(self, txid):
"""

try:
tx_info = bitcoin_cli(self.btc_connect_params).getrawtransaction(txid, 1)
return tx_info
return self.proxy().getrawtransaction(lx(txid), verbose=True)

except JSONRPCException as e:
except IndexError 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)
self.logger.info("Transaction not found in mempool nor blockchain", txid=txid)
return None

except JSONRPCError as e:
# If something else happens (unlikely but possible) log it so we can treat it in future releases
self.logger.error("JSONRPCError", method="Carrier.get_transaction", error=e.error)

return None
4 changes: 4 additions & 0 deletions teos/teosd.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from getopt import getopt, GetoptError
from signal import signal, SIGINT, SIGQUIT, SIGTERM

import bitcoin

from common.config_loader import ConfigLoader
from common.cryptographer import Cryptographer
from common.tools import setup_data_folder
Expand Down Expand Up @@ -469,6 +471,8 @@ def run():

config = get_config(command_line_conf, data_dir)

bitcoin.SelectParams(config.get("BTC_NETWORK"))

if config.get("DAEMON"):
print("Starting TEOS")
with daemon.DaemonContext():
Expand Down
11 changes: 6 additions & 5 deletions teos/tools.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from socket import timeout
from http.client import HTTPException

from teos.utils.auth_proxy import AuthServiceProxy, JSONRPCException
import bitcoin.rpc
from bitcoin.rpc import JSONRPCError

from common.constants import MAINNET_RPC_PORT, TESTNET_RPC_PORT, REGTEST_RPC_PORT

Expand All @@ -20,11 +21,11 @@ def bitcoin_cli(btc_connect_params):
(``rpc user, rpc password, host and port``)

Returns:
:obj:`AuthServiceProxy <teos.utils.auth_proxy.AuthServiceProxy>`: An authenticated service proxy to ``bitcoind``
:obj:`Proxy <bitcoin.rpc.Proxy>`: An authenticated service proxy to ``bitcoind``
that can be used to send ``json-rpc`` commands.
"""

return AuthServiceProxy(
return bitcoin.rpc.Proxy(
"http://%s:%s@%s:%d"
% (
btc_connect_params.get("BTC_RPC_USER"),
Expand All @@ -50,8 +51,8 @@ def can_connect_to_bitcoind(btc_connect_params):
can_connect = True

try:
bitcoin_cli(btc_connect_params).help()
except (timeout, ConnectionRefusedError, JSONRPCException, HTTPException, OSError):
bitcoin_cli(btc_connect_params).getbestblockhash()
except (timeout, ConnectionRefusedError, JSONRPCError, HTTPException, OSError) as e:
can_connect = False

return can_connect
Expand Down
Loading