diff --git a/otsclient/args.py b/otsclient/args.py index e84b07e..f42c723 100644 --- a/otsclient/args.py +++ b/otsclient/args.py @@ -78,6 +78,10 @@ def make_common_options_arg_parser(): help="Bitcoin node URL to connect to (defaults to local " "configuration)") + parser.add_argument('--bitcoin-dns', metavar='DOMAIN', action='store', type=str, + default=None, + help='Fetch Bitcoin headers over DNS if local node is unavailable') + return parser def handle_common_options(args, parser): @@ -142,11 +146,7 @@ def setup_bitcoin(): else: assert False - try: - return bitcoin.rpc.Proxy(service_url=args.bitcoin_node) - except Exception as exp: - logging.error("Could not connect to Bitcoin node: %s" % exp) - sys.exit(1) + return bitcoin.rpc.Proxy(service_url=args.bitcoin_node) args.setup_bitcoin = setup_bitcoin diff --git a/otsclient/cmds.py b/otsclient/cmds.py index d18c185..9c1a9ed 100644 --- a/otsclient/cmds.py +++ b/otsclient/cmds.py @@ -38,6 +38,7 @@ import opentimestamps.calendar import otsclient +import otsclient.dns_headers def remote_calendar(calendar_uri): """Create a remote calendar with User-Agent set appropriately""" @@ -406,19 +407,23 @@ def attestation_key(item): (attestation.height, b2lx(msg))) continue - proxy = args.setup_bitcoin() try: + proxy = args.setup_bitcoin() block_count = proxy.getblockcount() blockhash = proxy.getblockhash(attestation.height) + block_header = proxy.getblockheader(blockhash) except IndexError: logging.error("Bitcoin block height %d not found; %d is highest known block" % (attestation.height, block_count)) continue - except ConnectionError as exp: - logging.error("Could not connect to local Bitcoin node: %s" % exp) - continue - - block_header = proxy.getblockheader(blockhash) + except Exception as exp: + if args.bitcoin_dns is None: + logging.error("Could not connect to local Bitcoin node: %s" % exp) + continue + else: + logging.warning("WARNING: Falling back to insecure Bitcoin headers over DNS") + block_header = otsclient.dns_headers.get_header_from_dns(args.bitcoin_dns, attestation.height) + blockhash = block_header.GetHash() logging.debug("Attestation block hash: %s" % b2lx(blockhash)) diff --git a/otsclient/dns_headers.py b/otsclient/dns_headers.py new file mode 100644 index 0000000..e12f8f4 --- /dev/null +++ b/otsclient/dns_headers.py @@ -0,0 +1,46 @@ +# Copyright (C) The OpenTimestamps developers +# +# This file is part of the OpenTimestamps Client. +# +# It is subject to the license terms in the LICENSE file found in the top-level +# directory of this distribution. +# +# No part of the OpenTimestamps Client, including this file, may be copied, +# modified, propagated, or distributed except according to the terms contained +# in the LICENSE file. + +import logging +import socket + +from ipaddress import IPv6Address + +from bitcoin.core import b2x, x, CBlockHeader + +def get_header_from_dns(domain, n): + domain = '%d.%d.%s' % (n, n / 10000, domain) + + logging.debug("Getting block header %d from %s" % (n, domain)) + + nibble_chunks = [] + for (_family, _type, _port, _name, (addr, _, _, _)) in \ + socket.getaddrinfo(domain, None, family=socket.AF_INET6, type=socket.SocketKind.SOCK_DGRAM): + + addr = IPv6Address(addr) + + addr_bytes = addr.packed + + if addr_bytes[0:2] != b'\x20\x01': + continue + + idx = addr_bytes[2] >> 4 + + nibble_chunks.append((idx, b2x(addr_bytes[2:])[1:])) + + header_nibbles = '' + for (_n, chunk) in sorted(nibble_chunks): + header_nibbles += chunk + + header_bytes = x(header_nibbles) + assert header_bytes[0] == 0 + + return CBlockHeader.deserialize(header_bytes[1:])