basicswap/bin/basicswap_prepare.py
2019-10-02 22:38:24 +02:00

563 lines
20 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2019 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE.txt or http://www.opensource.org/licenses/mit-license.php.
"""
Atomic Swap Client - Proof of Concept
sudo pip install python-gnupg
"""
import sys
import os
import json
import hashlib
import mmap
import tarfile
import zipfile
import stat
import time
from urllib.request import urlretrieve
import urllib.parse
import logging
import platform
import gnupg
import basicswap.config as cfg
from basicswap.util import callrpc_cli
from bin.basicswap_run import startDaemon
if platform.system() == 'Darwin':
BIN_ARCH = 'osx64.tar.gz'
elif platform.system() == 'Windows':
BIN_ARCH = 'win64.zip'
else:
BIN_ARCH = 'x86_64-linux-gnu.tar.gz'
known_coins = {
'particl': '0.18.1.5',
'litecoin': '0.17.1',
'bitcoin': '0.18.1',
'namecoin': '0.18.0',
}
logger = logging.getLogger()
logger.level = logging.DEBUG
if not len(logger.handlers):
logger.addHandler(logging.StreamHandler(sys.stdout))
def make_reporthook():
read = 0 # number of bytes read so far
last_percent_str = ''
def reporthook(blocknum, blocksize, totalsize):
nonlocal read
nonlocal last_percent_str
read += blocksize
if totalsize > 0:
percent_str = '%5.1f%%' % (read * 1e2 / totalsize)
if percent_str != last_percent_str:
logger.info(percent_str)
last_percent_str = percent_str
else:
logger.info('read %d' % (read,))
return reporthook
def downloadFile(url, path):
logger.info('Downloading file %s', url)
opener = urllib.request.build_opener()
opener.addheaders = [('User-agent', 'Mozilla/5.0')]
urllib.request.install_opener(opener)
urlretrieve(url, path, make_reporthook())
def prepareCore(coin, version, settings, data_dir):
logger.info('prepareCore %s v%s', coin, version)
bin_dir = settings['chainclients'][coin]['bindir']
if not os.path.exists(bin_dir):
os.makedirs(bin_dir)
if 'osx' in BIN_ARCH:
os_dir_name = 'osx-unsigned'
os_name = 'osx'
elif 'win32' in BIN_ARCH or 'win64' in BIN_ARCH:
os_dir_name = 'win-unsigned'
os_name = 'win'
else:
os_dir_name = 'linux'
os_name = 'linux'
release_filename = '{}-{}-{}'.format(coin, version, BIN_ARCH)
if coin == 'particl':
signing_key_name = 'tecnovert'
release_url = 'https://github.com/particl/particl-core/releases/download/v{}/{}'.format(version, release_filename)
assert_filename = '{}-{}-{}-build.assert'.format(coin, os_name, version)
assert_url = 'https://raw.githubusercontent.com/particl/gitian.sigs/master/%s-%s/%s/%s' % (version, os_dir_name, signing_key_name, assert_filename)
elif coin == 'litecoin':
signing_key_name = 'thrasher'
release_url = 'https://download.litecoin.org/litecoin-{}/{}/{}'.format(version, os_name, release_filename)
assert_filename = '{}-{}-{}-build.assert'.format(coin, os_name, version.rsplit('.', 1)[0])
assert_url = 'https://raw.githubusercontent.com/litecoin-project/gitian.sigs.ltc/master/%s-%s/%s/%s' % (version, os_dir_name, signing_key_name, assert_filename)
elif coin == 'bitcoin':
signing_key_name = 'laanwj'
release_url = 'https://bitcoincore.org/bin/bitcoin-core-{}/{}'.format(version, release_filename)
assert_filename = '{}-{}-{}-build.assert'.format(coin, os_name, version.rsplit('.', 1)[0])
assert_url = 'https://raw.githubusercontent.com/bitcoin-core/gitian.sigs/master/%s-%s/%s/%s' % (version, os_dir_name, signing_key_name, assert_filename)
elif coin == 'namecoin':
signing_key_name = 'JeremyRand'
release_url = 'https://beta.namecoin.org/files/namecoin-core/namecoin-core-{}/{}'.format(version, release_filename)
assert_filename = '{}-{}-{}-build.assert'.format(coin, os_name, version.rsplit('.', 1)[0])
assert_url = 'https://raw.githubusercontent.com/namecoin/gitian.sigs/master/%s-%s/%s/%s' % (version, os_dir_name, signing_key_name, assert_filename)
else:
raise ValueError('Unknown coin')
assert_sig_filename = assert_filename + '.sig'
assert_sig_url = assert_url + '.sig'
release_path = os.path.join(bin_dir, release_filename)
if not os.path.exists(release_path):
downloadFile(release_url, release_path)
assert_path = os.path.join(bin_dir, assert_filename)
if not os.path.exists(assert_path):
downloadFile(assert_url, assert_path)
assert_sig_path = os.path.join(bin_dir, assert_sig_filename)
if not os.path.exists(assert_sig_path):
downloadFile(assert_sig_url, assert_sig_path)
hasher = hashlib.sha256()
with open(release_path, 'rb') as fp:
hasher.update(fp.read())
release_hash = hasher.digest()
logger.info('%s hash: %s', release_filename, release_hash.hex())
with open(assert_path, 'rb', 0) as fp, mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ) as s:
if s.find(bytes(release_hash.hex(), 'utf-8')) == -1:
raise ValueError('Error: release hash %s not found in assert file.' % (release_hash.hex()))
else:
logger.info('Found release hash in assert file.')
"""
gnupghome = os.path.join(data_dir, 'gpg')
if not os.path.exists(gnupghome):
os.makedirs(gnupghome)
"""
gpg = gnupg.GPG()
with open(assert_sig_path, 'rb') as fp:
verified = gpg.verify_file(fp, assert_path)
if verified.username is None:
logger.warning('Signature not verified.')
pubkeyurl = 'https://raw.githubusercontent.com/tecnovert/basicswap/master/gitianpubkeys/{}_{}.pgp'.format(coin, signing_key_name)
logger.info('Importing public key from url: ' + pubkeyurl)
gpg.import_keys(urllib.request.urlopen(pubkeyurl).read())
with open(assert_sig_path, 'rb') as fp:
verified = gpg.verify_file(fp, assert_path)
if verified.username is None:
raise ValueError('Signature verification failed.')
bins = [coin + 'd', coin + '-cli', coin + '-tx']
if os_name == 'win':
with zipfile.ZipFile(release_path) as fz:
for b in bins:
b += '.exe'
out_path = os.path.join(bin_dir, b)
with open(out_path, 'wb') as fout:
fout.write(fz.read('{}-{}/bin/{}'.format(coin, version, b)))
os.chmod(out_path, stat.S_IRWXU | stat.S_IXGRP | stat.S_IXOTH)
else:
with tarfile.open(release_path) as ft:
for b in bins:
out_path = os.path.join(bin_dir, b)
fi = ft.extractfile('{}-{}/bin/{}'.format(coin, version, b))
with open(out_path, 'wb') as fout:
fout.write(fi.read())
fi.close()
os.chmod(out_path, stat.S_IRWXU | stat.S_IXGRP | stat.S_IXOTH)
def prepareDataDir(coin, settings, data_dir, chain, particl_mnemonic):
core_settings = settings['chainclients'][coin]
data_dir = core_settings['datadir']
if not os.path.exists(data_dir):
os.makedirs(data_dir)
core_conf_path = os.path.join(data_dir, coin + '.conf')
if os.path.exists(core_conf_path):
exitWithError('{} exists'.format(core_conf_path))
with open(core_conf_path, 'w') as fp:
if chain != 'mainnet':
fp.write(chain + '=1\n')
if chain == 'testnet':
fp.write('[test]\n\n')
if chain == 'regtest':
fp.write('[regtest]\n\n')
else:
logger.warning('Unknown chain %s', chain)
fp.write('rpcport={}\n'.format(core_settings['rpcport']))
fp.write('printtoconsole=0\n')
fp.write('daemon=0\n')
if coin == 'particl':
fp.write('debugexclude=libevent\n')
fp.write('zmqpubsmsg=tcp://127.0.0.1:{}\n'.format(settings['zmqport']))
fp.write('spentindex=1\n')
fp.write('txindex=1\n')
fp.write('staking=0\n')
if particl_mnemonic == 'none':
fp.write('createdefaultmasterkey=1')
elif coin == 'litecoin':
fp.write('prune=1000\n')
elif coin == 'bitcoin':
fp.write('prune=1000\n')
elif coin == 'namecoin':
fp.write('prune=1000\n')
else:
logger.warning('Unknown coin %s', coin)
def extractCore(coin, version, settings):
logger.info('extractCore %s v%s', coin, version)
def printVersion():
from basicswap import __version__
logger.info('Basicswap version:', __version__)
def printHelp():
logger.info('Usage: basicswap-prepare ')
logger.info('\n--help, -h Print help.')
logger.info('--version, -v Print version.')
logger.info('--datadir=PATH Path to basicswap data directory, default:~/.basicswap.')
logger.info('--bindir=PATH Path to cores directory, default:datadir/bin.')
logger.info('--mainnet Run in mainnet mode.')
logger.info('--testnet Run in testnet mode.')
logger.info('--regtest Run in regtest mode.')
logger.info('--particl_mnemonic= Recovery phrase to use for the Particl wallet, default is randomly generated,\n'
+ ' "none" to set autogenerate account mode.')
logger.info('--withcoin= Prepare system to run daemon for coin.')
logger.info('--withoutcoin= Do not prepare system to run daemon for coin.')
logger.info('--addcoin= Add coin to existing setup.')
logger.info('--disablecoin= Make coin inactive.')
logger.info('--preparebinonly Don\'t prepare settings or datadirs.')
logger.info('--portoffset=n Raise all ports by n.')
logger.info('\n' + 'Known coins: %s', ', '.join(known_coins.keys()))
def make_rpc_func(bin_dir, data_dir, chain):
bin_dir = bin_dir
data_dir = data_dir
chain = '' if chain == 'mainnet' else chain
def rpc_func(cmd):
nonlocal bin_dir
nonlocal data_dir
nonlocal chain
return callrpc_cli(bin_dir, data_dir, chain, cmd, cfg.PARTICL_CLI)
return rpc_func
def waitForRPC(rpc_func, wallet=None):
for i in range(10):
try:
rpc_func('getwalletinfo')
return
except Exception as ex:
logging.warning('Can\'t connect to daemon RPC: %s. Trying again in %d second/s.', str(ex), (1 + i))
time.sleep(1 + i)
raise ValueError('waitForRPC failed')
def exitWithError(error_msg):
sys.stderr.write('Error: {}, exiting.\n'.format(error_msg))
sys.exit(1)
def main():
data_dir = None
bin_dir = None
port_offset = None
chain = 'mainnet'
particl_wallet_mnemonic = None
prepare_bin_only = False
with_coins = {'particl', 'litecoin'}
add_coin = ''
disable_coin = ''
for v in sys.argv[1:]:
if len(v) < 2 or v[0] != '-':
exitWithError('Unknown argument {}'.format(v))
s = v.split('=')
name = s[0].strip()
for i in range(2):
if name[0] == '-':
name = name[1:]
if name == 'v' or name == 'version':
printVersion()
return 0
if name == 'h' or name == 'help':
printHelp()
return 0
if name == 'mainnet':
continue
if name == 'testnet':
chain = 'testnet'
continue
if name == 'regtest':
chain = 'regtest'
continue
if name == 'preparebinonly':
prepare_bin_only = True
continue
if len(s) == 2:
if name == 'datadir':
data_dir = os.path.expanduser(s[1].strip('"'))
continue
if name == 'bindir':
bin_dir = os.path.expanduser(s[1].strip('"'))
continue
if name == 'portoffset':
port_offset = int(s[1])
continue
if name == 'particl_mnemonic':
particl_wallet_mnemonic = s[1].strip('"')
continue
if name == 'withcoin':
if s[1] not in known_coins:
exitWithError('Unknown coin {}'.format(s[1]))
with_coins.add(s[1])
continue
if name == 'withoutcoin':
if s[1] not in known_coins:
exitWithError('Unknown coin {}'.format(s[1]))
with_coins.discard(s[1])
continue
if name == 'addcoin':
if s[1] not in known_coins:
exitWithError('Unknown coin {}'.format(s[1]))
add_coin = s[1]
with_coins = [add_coin, ]
continue
if name == 'disablecoin':
if s[1] not in known_coins:
exitWithError('Unknown coin {}'.format(s[1]))
disable_coin = s[1]
continue
exitWithError('Unknown argument {}'.format(v))
if data_dir is None:
default_datadir = '~/.basicswap'
data_dir = os.path.join(os.path.expanduser(default_datadir))
if bin_dir is None:
bin_dir = os.path.join(data_dir, 'bin')
logger.info('Using datadir: %s', data_dir)
logger.info('Chain: %s', chain)
if port_offset is None:
port_offset = 300 if chain == 'testnet' else 0
if not os.path.exists(data_dir):
os.makedirs(data_dir)
config_path = os.path.join(data_dir, 'basicswap.json')
withchainclients = {}
chainclients = {
'particl': {
'connection_type': 'rpc',
'manage_daemon': True,
'rpcport': 19792 + port_offset,
'datadir': os.path.join(data_dir, 'particl'),
'bindir': os.path.join(bin_dir, 'particl'),
'blocks_confirmed': 2,
'override_feerate': 0.002,
'conf_target': 2,
'core_version_group': 18,
'chain_lookups': 'local',
},
'litecoin': {
'connection_type': 'rpc' if 'litecoin' in with_coins else 'none',
'manage_daemon': True if 'litecoin' in with_coins else False,
'rpcport': 19795 + port_offset,
'datadir': os.path.join(data_dir, 'litecoin'),
'bindir': os.path.join(bin_dir, 'litecoin'),
'use_segwit': True,
'blocks_confirmed': 2,
'conf_target': 2,
'core_version_group': 17,
'chain_lookups': 'local',
},
'bitcoin': {
'connection_type': 'rpc' if 'bitcoin' in with_coins else 'none',
'manage_daemon': True if 'bitcoin' in with_coins else False,
'rpcport': 19796 + port_offset,
'datadir': os.path.join(data_dir, 'bitcoin'),
'bindir': os.path.join(bin_dir, 'bitcoin'),
'use_segwit': True,
'blocks_confirmed': 1,
'conf_target': 2,
'core_version_group': 18,
'chain_lookups': 'local',
},
'namecoin': {
'connection_type': 'rpc' if 'namecoin' in with_coins else 'none',
'manage_daemon': True if 'namecoin' in with_coins else False,
'rpcport': 19798 + port_offset,
'datadir': os.path.join(data_dir, 'namecoin'),
'bindir': os.path.join(bin_dir, 'namecoin'),
'use_segwit': False,
'use_csv': False,
'blocks_confirmed': 1,
'conf_target': 2,
'core_version_group': 18,
'chain_lookups': 'local',
}
}
if disable_coin != '':
logger.info('Disabling coin: %s', disable_coin)
if not os.path.exists(config_path):
exitWithError('{} does not exist'.format(config_path))
with open(config_path) as fs:
settings = json.load(fs)
if disable_coin not in settings['chainclients']:
exitWithError('{} has not been prepared'.format(disable_coin))
settings['chainclients'][disable_coin]['connection_type'] = 'none'
settings['chainclients'][disable_coin]['manage_daemon'] = False
with open(config_path, 'w') as fp:
json.dump(settings, fp, indent=4)
logger.info('Done.')
return 0
if add_coin != '':
logger.info('Adding coin: %s', add_coin)
if not os.path.exists(config_path):
exitWithError('{} does not exist'.format(config_path))
with open(config_path) as fs:
settings = json.load(fs)
if add_coin in settings['chainclients']:
coin_settings = settings['chainclients'][add_coin]
if coin_settings['connection_type'] == 'none' and coin_settings['manage_daemon'] is False:
logger.info('Enabling coin: %s', add_coin)
coin_settings['connection_type'] = 'rpc'
coin_settings['manage_daemon'] = True
with open(config_path, 'w') as fp:
json.dump(settings, fp, indent=4)
logger.info('Done.')
return 0
exitWithError('{} is already in the settings file'.format(add_coin))
settings['chainclients'][add_coin] = chainclients[add_coin]
prepareCore(add_coin, known_coins[add_coin], settings, data_dir)
if not prepare_bin_only:
prepareDataDir(add_coin, settings, data_dir, chain, particl_wallet_mnemonic)
with open(config_path, 'w') as fp:
json.dump(settings, fp, indent=4)
logger.info('Done.')
return 0
logger.info('With coins: %s', ', '.join(with_coins))
if os.path.exists(config_path):
if not prepare_bin_only:
exitWithError('{} exists'.format(config_path))
else:
with open(config_path) as fs:
settings = json.load(fs)
else:
for c in with_coins:
withchainclients[c] = chainclients[c]
settings = {
'debug': True,
'zmqhost': 'tcp://127.0.0.1',
'zmqport': 20792 + port_offset,
'htmlhost': 'localhost',
'htmlport': 12700 + port_offset,
'network_key': '7sW2UEcHXvuqEjkpE5mD584zRaQYs6WXYohue4jLFZPTvMSxwvgs',
'network_pubkey': '035758c4a22d7dd59165db02a56156e790224361eb3191f02197addcb3bde903d2',
'chainclients': withchainclients,
'auto_reply_delay_min': 5, # Min delay before sending a response message
'auto_reply_delay_max': 50, # Max delay before sending a response message
'check_progress_seconds': 60,
'check_watched_seconds': 60,
'check_expired_seconds': 60
}
for c in with_coins:
prepareCore(c, known_coins[c], settings, data_dir)
if prepare_bin_only:
logger.info('Done.')
return 0
for c in with_coins:
prepareDataDir(c, settings, data_dir, chain, particl_wallet_mnemonic)
with open(config_path, 'w') as fp:
json.dump(settings, fp, indent=4)
if particl_wallet_mnemonic == 'none':
logger.info('Done.')
return 0
logger.info('Loading Particl mnemonic')
particl_settings = settings['chainclients']['particl']
partRpc = make_rpc_func(particl_settings['bindir'], particl_settings['datadir'], chain)
d = startDaemon(particl_settings['datadir'], particl_settings['bindir'], cfg.PARTICLD, ['-noconnect', '-nofindpeers', '-nostaking', '-nodnsseed', '-nolisten'])
try:
waitForRPC(partRpc)
if particl_wallet_mnemonic is None:
particl_wallet_mnemonic = partRpc('mnemonic new')['mnemonic']
partRpc('extkeyimportmaster "{}"'.format(particl_wallet_mnemonic))
finally:
logger.info('Terminating {}'.format(d.pid))
d.terminate()
d.wait(timeout=120)
if d.stdout:
d.stdout.close()
if d.stderr:
d.stderr.close()
if d.stdin:
d.stdin.close()
logger.info('IMPORTANT - Save your particl wallet recovery phrase:\n{}\n'.format(particl_wallet_mnemonic))
logger.info('Done.')
if __name__ == '__main__':
main()