Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: take blockchain.info out of the broadcast mix, and shore up vali… #322

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
18 changes: 17 additions & 1 deletion cert_issuer/blockchain_handlers/bitcoin/connectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,22 @@ def broadcast_tx_with_chain(tx, bitcoin_chain, bitcoind=False):
try:
tx_id = method_provider(tx)
if tx_id:
# We have to check to make sure that the tx_id at least kind of looks like a sha256 hash string.
if len(tx_id) != 64:
if final_tx_id:
# If we already found a final_tx_id, then some other provider got it, but we should
# log the bad provider.
logging.error("Ignoring invalid tx_id %s from provider %s since valid" +
"tx_id %s already exists (another provider already sent the transaction). " +
"The current provider might be broken! Bad txid!",
tx_id, str(method_provider), final_tx_id)
continue
else:
logging.error("Provider %s returned an invalid tx_id: %s. " +
"Aborting further retries. Bad txid!",
str(method_provider), tx_id)
raise BroadcastError("Invalid tx_id received: " + tx_id)

logging.info('Broadcasting succeeded with method_provider=%s, txid=%s', str(method_provider),
tx_id)
if final_tx_id and final_tx_id != tx_id:
Expand Down Expand Up @@ -242,7 +258,7 @@ def broadcast_tx_with_chain(tx, bitcoin_chain, bitcoind=False):
config = cert_issuer.config.CONFIG
blockcypher_token = None if config is None else config.blockcypher_api_token

PYCOIN_BTC_PROVIDERS = "blockchain.info chain.so" # blockcypher.com
PYCOIN_BTC_PROVIDERS = "chain.so" # blockchain.info blockcypher.com
PYCOIN_XTN_PROVIDERS = "" # chain.so

# initialize connectors
Expand Down
73 changes: 73 additions & 0 deletions tests/blockchain_handlers/bitcoin/test_connectors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import unittest
from unittest.mock import patch, MagicMock
import time

from cert_issuer.blockchain_handlers.bitcoin.connectors import (
BitcoinServiceProviderConnector,
BroadcastError
)

from cert_core import Chain

# A dummy transaction class with an as_hex() method.
class DummyTx:
def as_hex(self):
return "deadbeef"

# Provider functions to simulate various scenarios.
def provider_valid(tx):
# Return a valid tx id (64-character hex string)
return "a" * 64

def provider_invalid(tx):
# Return an invalid tx id (e.g., not 64 characters long)
return "bad_tx"

def provider_exception(tx):
# Simulate a provider that raises an exception.
raise Exception("Simulated provider failure")

class TestBroadcastTxWithChain(unittest.TestCase):

@patch('cert_issuer.blockchain_handlers.bitcoin.connectors.time.sleep', return_value=None)
@patch('cert_issuer.blockchain_handlers.bitcoin.connectors.service_provider_methods')
def test_valid_tx_single_provider(self, mock_service_methods, mock_sleep):
# Test when one provider returns a valid tx id.
mock_service_methods.return_value = [provider_valid]
tx = DummyTx()
result = BitcoinServiceProviderConnector.broadcast_tx_with_chain(tx, Chain.bitcoin_mainnet, bitcoind=False)
self.assertEqual(result, "a" * 64)

@patch('cert_issuer.blockchain_handlers.bitcoin.connectors.time.sleep', return_value=None)
@patch('cert_issuer.blockchain_handlers.bitcoin.connectors.service_provider_methods')
def test_valid_and_invalid_providers(self, mock_service_methods, mock_sleep):
# Test that if the first provider returns a valid tx id,
# then later invalid tx ids are ignored.
# The inner loop should break on the first valid response.
mock_service_methods.return_value = [provider_valid, provider_invalid]
tx = DummyTx()
result = BitcoinServiceProviderConnector.broadcast_tx_with_chain(tx, Chain.bitcoin_mainnet, bitcoind=False)
self.assertEqual(result, "a" * 64)

@patch('cert_issuer.blockchain_handlers.bitcoin.connectors.time.sleep', return_value=None)
@patch('cert_issuer.blockchain_handlers.bitcoin.connectors.service_provider_methods')
def test_all_invalid_providers(self, mock_service_methods, mock_sleep):
# Test that if all providers return an invalid tx id,
# the method eventually raises BroadcastError.
mock_service_methods.return_value = [provider_invalid]
tx = DummyTx()
with self.assertRaises(BroadcastError):
BitcoinServiceProviderConnector.broadcast_tx_with_chain(tx, Chain.bitcoin_mainnet, bitcoind=False)

@patch('cert_issuer.blockchain_handlers.bitcoin.connectors.time.sleep', return_value=None)
@patch('cert_issuer.blockchain_handlers.bitcoin.connectors.service_provider_methods')
def test_provider_exception_then_valid(self, mock_service_methods, mock_sleep):
# Test that if one provider raises an exception and another returns a valid tx id,
# the valid one is used.
mock_service_methods.return_value = [provider_exception, provider_valid]
tx = DummyTx()
result = BitcoinServiceProviderConnector.broadcast_tx_with_chain(tx, Chain.bitcoin_mainnet, bitcoind=False)
self.assertEqual(result, "a" * 64)

if __name__ == '__main__':
unittest.main()