class BitcoindConfiguration(object):
    file: ConfigurationFile
    hard_drives: HardDrives
    zmq_block_port: int
    zmq_tx_port: int

    def __init__(self):
        self.hard_drives = HardDrives()
        self.file = None
        file_name = 'bitcoin.conf'
        bitcoin_data_path = BITCOIN_DATA_PATH[OPERATING_SYSTEM]
        self.file_path = os.path.join(bitcoin_data_path, file_name)

    @property
    def args(self) -> List[str]:
        return [f'-conf={self.file_path}']

    @property
    def cli_args(self) -> List[str]:
        return self.args

    def load(self):
        log.info('bitcoin_configuration_file_path',
                 configuration_file_path=self.file_path)
        self.file = ConfigurationFile(self.file_path)

    def check(self):
        log.debug('datadir', datadir=self.file['datadir'])

        if (self.file['datadir'] is None
                or not os.path.exists(self.file['datadir'])):
            self.autoconfigure_datadir()

        if 'bitcoin.conf' in os.listdir(self.file['datadir']):
            actual_conf_file = os.path.join(self.file['datadir'],
                                            'bitcoin.conf')
            if self.file_path != actual_conf_file:
                log.info('datadir_redirect',
                         configuration_file_path=self.file_path,
                         actual_conf_file=actual_conf_file)
                self.file = ConfigurationFile(actual_conf_file)
                if (self.file['datadir'] is None
                        or not os.path.exists(self.file['datadir'])):
                    self.autoconfigure_datadir()

        if os.path.exists(os.path.join(self.file['datadir'], 'blocks')):
            if self.file['prune'] is None:
                self.set_prune(False)

        self.wallet_paths = self.get_wallet_paths()

        if self.file['server'] is None:
            self.file['server'] = True

        if self.file['disablewallet'] is None and not self.wallet_paths:
            self.file['disablewallet'] = True
        elif self.file['disablewallet'] is None and self.wallet_paths:
            self.file['disablewallet'] = False

        if self.file['timeout'] is None:
            self.file['timeout'] = 6000

        if self.file['rpcuser'] is None:
            self.file['rpcuser'] = '******'

        if self.file['rpcpassword'] is None:
            self.file['rpcpassword'] = get_random_password()

        if self.file['prune'] is None:
            should_prune = self.hard_drives.should_prune(self.file['datadir'],
                                                         has_bitcoin=True)
            self.set_prune(should_prune)

        self.zmq_block_port = get_zmq_port()
        self.zmq_tx_port = get_zmq_port()

        self.file['zmqpubrawblock'] = f'tcp://127.0.0.1:{self.zmq_block_port}'
        self.file['zmqpubrawtx'] = f'tcp://127.0.0.1:{self.zmq_tx_port}'

        self.file['proxy'] = '127.0.0.1:9050'
        self.file['listen'] = True
        self.file['bind'] = '127.0.0.1'
        self.file['debug'] = 'tor'
        self.file['discover'] = True

        # noinspection PyBroadException
        try:
            memory = psutil.virtual_memory()
            free_mb = round(memory.available / 1000000)
            free_mb -= int(free_mb * .3)
            self.file['dbcache'] = free_mb
        except:
            log.warning('dbcache psutil.virtual_memory', exc_info=True)
            self.file['dbcache'] = 1000

        self.config_snapshot = self.file.snapshot.copy()
        # self.file.file_watcher.fileChanged.connect(self.config_file_changed)

    def autoconfigure_datadir(self):
        default_datadir = BITCOIN_DATA_PATH[OPERATING_SYSTEM]
        big_drive = self.hard_drives.get_big_drive()
        default_is_big_enough = not self.hard_drives.should_prune(
            input_directory=default_datadir, has_bitcoin=True)
        default_is_biggest = self.hard_drives.is_default_partition(big_drive)
        log.info('autoconfigure_datadir',
                 default_is_big_enough=default_is_big_enough,
                 default_is_biggest=default_is_biggest)
        if default_is_big_enough or default_is_biggest:
            self.file['datadir'] = default_datadir
            log.info('autoconfigure_datadir', datadir=default_datadir)
            return

        if not self.hard_drives.should_prune(big_drive.mountpoint, False):
            datadir = os.path.join(big_drive.mountpoint, 'Bitcoin')
            self.file['datadir'] = datadir
            log.info('autoconfigure_datadir', datadir=datadir)
            if not os.path.exists(self.file['datadir']):
                os.mkdir(self.file['datadir'])
        else:
            self.file['datadir'] = default_datadir
            log.info('autoconfigure_datadir', datadir=default_datadir)

    def get_wallet_paths(self):
        exclude_files = {
            'addr.dat', 'banlist.dat', 'fee_estimates.dat', 'mempool.dat',
            'peers.dat'
        }
        candidate_paths = []
        datadir = self.file['datadir']
        wallet_dir = self.file['main.walletdir']
        wallets = self.file['main.wallet']
        for file in os.listdir(datadir):
            if file not in exclude_files:
                path = os.path.join(datadir, file)
                candidate_paths.append(path)
        default_walletdir = os.path.join(datadir, 'wallets')
        if os.path.exists(default_walletdir):
            for file in os.listdir(default_walletdir):
                if file not in exclude_files:
                    candidate_paths.append(
                        os.path.join(default_walletdir, file))
        if wallet_dir is not None:
            for file in os.listdir(wallet_dir):
                if file not in exclude_files:
                    candidate_paths += os.path.join(
                        os.path.join(wallet_dir, file))
        dat_files = [
            f for f in candidate_paths
            if f.endswith('.dat') and not f.startswith('blk')
        ]
        dat_files = set(dat_files)
        wallet_paths = set(dat_files - exclude_files)
        if wallets is not None:
            if isinstance(wallets, list):
                for wallet in wallets:
                    wallet_paths.add(wallet)
            else:
                wallet_paths.add(wallets)
        return wallet_paths

    @property
    def node_port(self):
        custom_port = self.file['main.port']
        if custom_port is not None:
            return custom_port
        return BITCOIN_MAINNET_PEER_PORT

    @property
    def rpc_port(self):
        custom_port = self.file['main.rpcport']
        if custom_port is not None:
            return custom_port
        return BITCOIN_MAINNET_RPC_PORT

    def set_prune(self, should_prune: bool = None):

        if should_prune is None:
            should_prune = self.hard_drives.should_prune(self.file['datadir'],
                                                         has_bitcoin=True)
        if should_prune:
            prune = MAINNET_PRUNE
            self.file['prune'] = prune
        else:
            self.file['prune'] = 0
        self.file['txindex'] = not should_prune

    def config_file_changed(self):
        # Refresh config file
        self.file.file_watcher.blockSignals(True)
        self.file.populate_cache()
        self.file.file_watcher.blockSignals(False)
        if self.file['zmqpubrawblock']:
            self.zmq_block_port = int(
                self.file['zmqpubrawblock'].split(':')[-1])
        if self.file['zmqpubrawtx']:
            self.zmq_tx_port = int(self.file['zmqpubrawtx'].split(':')[-1])
        # Some text editors do not modify the file, they delete and replace the file
        # Check if file is still in file_watcher list of files, if not add back
        files_watched = self.file.file_watcher.files()
        if len(files_watched) == 0:
            self.file.file_watcher.addPath(self.file.path)

    @property
    def restart_required(self):
        old_config = self.config_snapshot.copy()
        new_config = self.file.snapshot

        # First check that both config files are still on the same network
        old_config_network = 'testnet' in old_config.keys()
        new_config_network = 'testnet' in new_config.keys()

        if (old_config_network == new_config_network) and self.running:
            common_fields = [
                'rpcuser', 'rpcpassword', 'disablewallet', 'datadir',
                'disablewallet', 'zmqpubrawblock', 'zmqpubrawtx', 'prune',
                'txindex', 'timeout'
            ]

            for field in common_fields:

                # First check if field is found in both configs
                found_in_old_config = field in old_config.keys()
                found_in_new_config = field in new_config.keys()
                if found_in_old_config != found_in_new_config:
                    return True

                # Now check that values are the same
                if found_in_old_config:
                    if old_config[field] != new_config[field]:
                        return True

            else:
                # Only check mainnet fields if currently running mainnet
                mainnet_fields = ['rpcport', 'port']

                for field in mainnet_fields:
                    # First check if field is found in both configs
                    found_in_old_config = field in old_config.keys()
                    found_in_new_config = field in new_config.keys()
                    if found_in_old_config != found_in_new_config:
                        return True

                    # Now check that values are the same
                    if found_in_old_config:
                        if old_config[field] != new_config[field]:
                            return True

            return False
        elif self.running:
            # Network has changed and the node is running - Restart is required
            return True

        return False
Ejemplo n.º 2
0
class LndConfiguration(object):
    file: ConfigurationFile

    def __init__(self):
        file_name = 'lnd.conf'
        lnd_dir_path = LND_DIR_PATH[OPERATING_SYSTEM]
        self.file_path = os.path.join(lnd_dir_path, file_name)

    @property
    def args(self):
        if IS_WINDOWS:
            arg_list = [
                f'--configfile={self.file_path}',
            ]
        else:
            arg_list = [
                f'--configfile="{self.file_path}"',
            ]

        arg_list += ['--bitcoin.mainnet']
        return arg_list

    def cli_args(self) -> List[str]:
        args = []
        if self.grpc_port != LND_DEFAULT_GRPC_PORT:
            args.append(f'--rpcserver=127.0.0.1:{self.grpc_port}')
        if self.lnddir != LND_DIR_PATH[OPERATING_SYSTEM]:
            args.append(f'''--lnddir="{self.lnddir}"''')
            args.append(f'--macaroonpath="{self.macaroon_path}"')
            args.append(f'--tlscertpath="{self.tls_cert_path}"')
        return args

    def load(self):
        log.info('lnd configuration_file_path',
                 configuration_file_path=self.file_path)
        self.file = ConfigurationFile(self.file_path)

    def check(self):
        self.lnddir = LND_DIR_PATH[OPERATING_SYSTEM]

        # Previous versions of the launcher set lnddir in the config file,
        # but it is not a valid key so this helps old users upgrading
        if self.file['lnddir'] is not None:
            self.file['lnddir'] = None

        if self.file['debuglevel'] is None:
            self.file['debuglevel'] = 'info'

        self.file['bitcoin.active'] = True
        self.file['bitcoin.node'] = 'bitcoind'
        bitcoind_conf = BitcoindConfiguration()
        bitcoind_conf.load()
        self.file['bitcoind.rpchost'] = f'127.0.0.1:{bitcoind_conf.rpc_port}'
        self.file['bitcoind.rpcuser'] = bitcoind_conf.file['rpcuser']
        self.file['bitcoind.rpcpass'] = bitcoind_conf.file['rpcpassword']
        self.file['bitcoind.zmqpubrawblock'] = bitcoind_conf.file[
            'zmqpubrawblock']
        self.file['bitcoind.zmqpubrawtx'] = bitcoind_conf.file['zmqpubrawtx']

        if self.file['restlisten'] is None:
            self.rest_port = get_port(LND_DEFAULT_REST_PORT)
            self.file['restlisten'] = f'127.0.0.1:{self.rest_port}'
        else:
            self.rest_port = self.file['restlisten'].split(':')[-1]

        if not self.file['rpclisten']:
            self.grpc_port = get_port(LND_DEFAULT_GRPC_PORT)
            self.file['rpclisten'] = f'127.0.0.1:{self.grpc_port}'
        else:
            self.grpc_port = int(self.file['rpclisten'].split(':')[-1])

        if not self.file['tlsextraip']:
            self.file['tlsextraip'] = '127.0.0.1'

        if self.file['color'] is None:
            self.file['color'] = '#000000'

        self.file['tor.active'] = True
        self.file['tor.v3'] = True
        self.file['tor.streamisolation'] = True

        self.macaroon_path = os.path.join(self.lnddir, 'data', 'chain',
                                          'bitcoin', 'mainnet')
        self.config_snapshot = self.file.snapshot.copy()
        # self.file.file_watcher.fileChanged.connect(self.config_file_changed)
        # self.file.file_watcher.fileChanged.connect(
        #     self.bitcoin_config_file_changed)

        hostname_file = os.path.join(TOR_SERVICE_PATH, 'hostname')
        with open(hostname_file, 'r') as f:
            self.file['externalip'] = f.readline().strip()

    def config_file_changed(self):
        # Refresh config file
        self.file.file_watcher.blockSignals(True)
        self.file.populate_cache()
        self.file.file_watcher.blockSignals(False)
        if self.file['restlisten']:
            self.rest_port = int(self.file['restlisten'].split(':')[-1])
        if self.file['rpclisten']:
            self.grpc_port = int(self.file['rpclisten'].split(':')[-1])

        # Some text editors do not modify the file, they delete and replace the file
        # Check if file is still in file_watcher list of files, if not add back
        files_watched = self.file.file_watcher.files()
        if len(files_watched) == 0:
            self.file.file_watcher.addPath(self.file_path)

    def bitcoin_config_file_changed(self):
        # Refresh config file
        self.file.file_watcher.blockSignals(True)
        self.file.populate_cache()
        self.file.file_watcher.blockSignals(False)
        bitcoind_conf = BitcoindConfiguration()
        bitcoind_conf.load()
        self.file['bitcoind.rpchost'] = f'127.0.0.1:{bitcoind_conf.rpc_port}'
        self.file['bitcoind.rpcuser'] = bitcoind_conf.file['rpcuser']
        self.file['bitcoind.rpcpass'] = bitcoind_conf.file['rpcpassword']
        self.file['bitcoind.zmqpubrawblock'] = bitcoind_conf.file[
            'zmqpubrawblock']
        self.file['bitcoind.zmqpubrawtx'] = bitcoind_conf.file['zmqpubrawtx']

    @property
    def node_port(self) -> int:
        if self.file['listen'] is None:
            port = get_port(LND_DEFAULT_PEER_PORT)
            self.file['listen'] = f'127.0.0.1:{port}'
        else:
            if not isinstance(self.file['listen'], list):
                port = int(self.file['listen'].split(':')[-1])
            else:
                port = int(self.file['listen'][0].split(':')[-1])
        return port

    @property
    def admin_macaroon_path(self) -> str:
        path = os.path.join(self.macaroon_path, 'admin.macaroon')
        return path

    @property
    def wallet_path(self) -> str:
        wallet_path = os.path.join(self.macaroon_path, 'wallet.db')
        return wallet_path

    @property
    def has_wallet(self) -> bool:
        return os.path.isfile(self.wallet_path)

    @property
    def tls_cert_path(self) -> str:
        tls_cert_path = os.path.join(self.lnddir, 'tls.cert')
        return tls_cert_path

    @property
    def rest_url(self) -> str:
        return f'https://127.0.0.1:{self.rest_port}'

    @property
    def grpc_url(self) -> str:
        return f'127.0.0.1:{self.grpc_port}'

    @property
    def restart_required(self):
        if self.running:
            # Did bitcoin details change
            if self.restart_required:
                return True and self.running

            old_config = self.config_snapshot.copy()
            new_config = self.file.snapshot

            fields = ['restlisten', 'listen', 'rpclisten']

            for field in fields:
                # First check if field is found in both configs
                found_in_old_config = field in old_config.keys()
                found_in_new_config = field in new_config.keys()
                if found_in_old_config != found_in_new_config:
                    return True

                # Now check that values are the same
                if found_in_old_config:
                    if old_config[field] != new_config[field]:
                        return True

        return False

    @staticmethod
    def base64URL_from_base64(s):
        return s.replace('+', '-').replace('/', '_').rstrip('=')

    @property
    def lndconnect_mobile_url(self):
        host = self.grpc_url.split(':')[0]
        port = self.grpc_url.split(':')[1]
        with open(self.tls_cert_path, 'r') as cert_file:
            lines = cert_file.read().split('\n')
            lines = [line for line in lines if line != '']
            cert = ''.join(lines[1:-1])
            cert = self.base64URL_from_base64(cert)

        with open(self.admin_macaroon_path, 'rb') as macaroon_file:
            macaroon = base64.b64encode(macaroon_file.read()).decode('ascii')
            macaroon = self.base64URL_from_base64(macaroon)

        return f'lndconnect://{host}:{port}?cert={cert}&macaroon={macaroon}'

    @property
    def lndconnect_url(self):
        host = self.grpc_url.split(':')[0]
        port = self.grpc_url.split(':')[1]
        return f'lndconnect://{host}:{port}' \
            f'?cert={self.tls_cert_path}&macaroon={self.admin_macaroon_path}'

    @property
    def lndconnect_qrcode(self):
        img = qrcode.make(self.lndconnect_mobile_url)
        return img

    def test_tls_cert(self):
        context = ssl.create_default_context()
        context.load_verify_locations(cafile=self.tls_cert_path)
        conn = context.wrap_socket(socket.socket(socket.AF_INET),
                                   server_hostname='127.0.0.1')
        conn.connect(('127.0.0.1', int(self.rest_port)))
        cert = conn.getpeercert()
        return cert