daemon.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. daemon.py: Daemon control interface for the MMGen suite
  20. """
  21. import os,shutil,time
  22. from subprocess import run,PIPE,CompletedProcess
  23. from collections import namedtuple
  24. from .globalvars import g
  25. from .opts import opt
  26. from .util import msg,die,list_gen,get_subclasses
  27. from .flags import *
  28. _dd = namedtuple('daemon_data',['coind_name','coind_version','coind_version_str']) # latest tested version
  29. _cd = namedtuple('coins_data',['coin_name','daemon_ids'])
  30. _nw = namedtuple('coin_networks',['mainnet','testnet','regtest'])
  31. class Daemon(Lockable):
  32. desc = 'daemon'
  33. debug = False
  34. wait = True
  35. use_pidfile = True
  36. pids = ()
  37. use_threads = False
  38. cfg_file = None
  39. new_console_mswin = False
  40. lockfile = None
  41. private_port = None
  42. avail_opts = ()
  43. avail_flags = () # like opts, but can be set or unset after instantiation
  44. _reset_ok = ('debug','wait','pids')
  45. def __init__(self,opts=None,flags=None):
  46. self.platform = g.platform
  47. if self.platform == 'win':
  48. self.use_pidfile = False
  49. self.use_threads = True
  50. self.opt = ClassOpts(self,opts)
  51. self.flag = ClassFlags(self,flags)
  52. def exec_cmd_thread(self,cmd):
  53. import threading
  54. tname = ('exec_cmd','exec_cmd_win_console')[self.platform == 'win' and self.new_console_mswin]
  55. t = threading.Thread(target=getattr(self,tname),args=(cmd,))
  56. t.daemon = True
  57. t.start()
  58. if self.platform == 'win':
  59. Msg_r(' \b') # blocks w/o this...crazy
  60. return True
  61. def exec_cmd_win_console(self,cmd):
  62. from subprocess import Popen,CREATE_NEW_CONSOLE,STARTUPINFO,STARTF_USESHOWWINDOW,SW_HIDE
  63. si = STARTUPINFO(dwFlags=STARTF_USESHOWWINDOW,wShowWindow=SW_HIDE)
  64. p = Popen(cmd,creationflags=CREATE_NEW_CONSOLE,startupinfo=si)
  65. p.wait()
  66. def exec_cmd(self,cmd,is_daemon=False):
  67. out = (PIPE,None)[is_daemon and self.opt.no_daemonize]
  68. try:
  69. cp = run(cmd,check=False,stdout=out,stderr=out)
  70. except Exception as e:
  71. from .exception import MMGenCalledProcessError
  72. raise MMGenCalledProcessError(f'Error starting executable: {type(e).__name__} [Errno {e.errno}]')
  73. if self.debug:
  74. print(cp)
  75. return cp
  76. def run_cmd(self,cmd,silent=False,is_daemon=False):
  77. if is_daemon and not silent:
  78. msg(f'Starting {self.desc} on port {self.bind_port}')
  79. if self.debug:
  80. msg(f'\nExecuting: {" ".join(cmd)}')
  81. if self.use_threads and is_daemon and not self.opt.no_daemonize:
  82. ret = self.exec_cmd_thread(cmd)
  83. else:
  84. ret = self.exec_cmd(cmd,is_daemon)
  85. if isinstance(ret,CompletedProcess):
  86. if ret.stdout and (self.debug or not silent):
  87. msg(ret.stdout.decode().rstrip())
  88. if ret.stderr and (self.debug or (ret.returncode and not silent)):
  89. msg(ret.stderr.decode().rstrip())
  90. return ret
  91. @property
  92. def pid(self):
  93. if self.use_pidfile:
  94. with open(self.pidfile) as fp:
  95. return fp.read().strip()
  96. elif self.platform == 'win':
  97. """
  98. Assumes only one running instance of given daemon. If multiple daemons are running,
  99. the first PID in the list is returned and self.pids is set to the PID list.
  100. """
  101. ss = f'{self.exec_fn}.exe'
  102. cp = self.run_cmd(['ps','-Wl'],silent=True)
  103. self.pids = ()
  104. # use Windows, not Cygwin, PID
  105. pids = tuple(line.split()[3] for line in cp.stdout.decode().splitlines() if ss in line)
  106. if pids:
  107. if len(pids) > 1:
  108. self.pids = pids
  109. return pids[0]
  110. elif self.platform == 'linux':
  111. ss = ' '.join(self.start_cmd)
  112. cp = self.run_cmd(['pgrep','-f',ss],silent=True)
  113. if cp.stdout:
  114. return cp.stdout.strip().decode()
  115. die(2,f'{ss!r} not found in process list, cannot determine PID')
  116. @property
  117. def bind_port(self):
  118. return self.private_port or self.rpc_port
  119. @property
  120. def state(self):
  121. if self.debug:
  122. msg(f'Testing port {self.bind_port}')
  123. return 'ready' if self.test_socket('localhost',self.bind_port) else 'stopped'
  124. @property
  125. def start_cmds(self):
  126. return [self.start_cmd]
  127. @property
  128. def stop_cmd(self):
  129. return ['kill','-Wf',self.pid] if self.platform == 'win' else ['kill',self.pid]
  130. def cmd(self,action,*args,**kwargs):
  131. return getattr(self,action)(*args,**kwargs)
  132. def do_start(self,silent=False):
  133. if not silent:
  134. msg(f'Starting {self.desc} on port {self.bind_port}')
  135. return self.run_cmd(self.start_cmd,silent=True,is_daemon=True)
  136. def do_stop(self,silent=False):
  137. if not silent:
  138. msg(f'Stopping {self.desc} on port {self.bind_port}')
  139. return self.run_cmd(self.stop_cmd,silent=True)
  140. def cli(self,*cmds,silent=False):
  141. return self.run_cmd(self.cli_cmd(*cmds),silent=silent)
  142. def state_msg(self,extra_text=None):
  143. extra_text = f'{extra_text} ' if extra_text else ''
  144. return '{:{w}} {:10} {}'.format(
  145. f'{self.desc} {extra_text}running',
  146. 'pid N/A' if self.pid is None or self.pids else f'pid {self.pid}',
  147. f'port {self.bind_port}',
  148. w = 52 + len(extra_text) )
  149. def pre_start(self): pass
  150. def start(self,quiet=False,silent=False):
  151. if self.state == 'ready':
  152. if not (quiet or silent):
  153. msg(self.state_msg(extra_text='already'))
  154. return True
  155. self.wait_for_state('stopped')
  156. self.pre_start()
  157. ret = self.do_start(silent=silent)
  158. if self.wait:
  159. self.wait_for_state('ready')
  160. return ret
  161. def stop(self,quiet=False,silent=False):
  162. if self.state == 'ready':
  163. ret = self.do_stop(silent=silent)
  164. if self.pids:
  165. msg('Warning: multiple PIDs [{}] -- we may be stopping the wrong instance'.format(
  166. fmt_list(self.pids,fmt='bare')
  167. ))
  168. if self.wait:
  169. self.wait_for_state('stopped')
  170. return ret
  171. else:
  172. if not (quiet or silent):
  173. msg(f'{self.desc} on port {self.bind_port} not running')
  174. return True
  175. def restart(self,silent=False):
  176. self.stop(silent=silent)
  177. return self.start(silent=silent)
  178. def test_socket(self,host,port,timeout=10):
  179. import socket
  180. try:
  181. socket.create_connection((host,port),timeout=timeout).close()
  182. except:
  183. return False
  184. else:
  185. return True
  186. def wait_for_state(self,req_state):
  187. for i in range(300):
  188. if self.state == req_state:
  189. return True
  190. time.sleep(0.2)
  191. else:
  192. die(2,f'Wait for state {req_state!r} timeout exceeded for {self.desc} (port {self.bind_port})')
  193. class RPCDaemon(Daemon):
  194. avail_opts = ('no_daemonize',)
  195. def __init__(self):
  196. super().__init__()
  197. self.desc = '{} {} {}RPC daemon'.format(
  198. self.rpc_type,
  199. getattr(self.proto.network_names,self.proto.network),
  200. 'test suite ' if self.test_suite else '' )
  201. self._set_ok += ('usr_daemon_args',)
  202. self.usr_daemon_args = []
  203. @property
  204. def start_cmd(self):
  205. return ([self.exec_fn] + self.daemon_args + self.usr_daemon_args)
  206. class MoneroWalletDaemon(RPCDaemon):
  207. master_daemon = 'monero_daemon'
  208. rpc_type = 'Monero wallet'
  209. exec_fn = 'monero-wallet-rpc'
  210. coin = 'XMR'
  211. new_console_mswin = True
  212. rpc_ports = _nw(13131, 13141, None) # testnet is non-standard
  213. def __init__(self, proto, wallet_dir,
  214. test_suite = False,
  215. host = None,
  216. user = None,
  217. passwd = None,
  218. daemon_addr = None,
  219. proxy = None,
  220. port_shift = None,
  221. datadir = None ):
  222. self.proto = proto
  223. self.test_suite = test_suite
  224. super().__init__()
  225. self.network = proto.network
  226. self.wallet_dir = wallet_dir
  227. self.rpc_port = getattr(self.rpc_ports,self.network) + (11 if test_suite else 0)
  228. if port_shift:
  229. self.rpc_port += port_shift
  230. id_str = f'{self.exec_fn}-{self.bind_port}'
  231. self.datadir = os.path.join((datadir or self.exec_fn),('','test_suite')[test_suite])
  232. self.pidfile = os.path.join(self.datadir,id_str+'.pid')
  233. self.logfile = os.path.join(self.datadir,id_str+'.log')
  234. self.proxy = proxy
  235. self.daemon_addr = daemon_addr
  236. self.daemon_port = None if daemon_addr else CoinDaemon(proto=proto,test_suite=test_suite).rpc_port
  237. self.host = host or opt.wallet_rpc_host or g.monero_wallet_rpc_host
  238. self.user = user or opt.wallet_rpc_user or g.monero_wallet_rpc_user
  239. self.passwd = passwd or opt.wallet_rpc_password or g.monero_wallet_rpc_password
  240. assert self.host
  241. assert self.user
  242. if not self.passwd:
  243. die(1,
  244. 'You must set your Monero wallet RPC password.\n' +
  245. 'This can be done on the command line with the --wallet-rpc-password option\n' +
  246. "(insecure, not recommended), or by setting 'monero_wallet_rpc_password' in\n" +
  247. "the MMGen config file." )
  248. self.daemon_args = list_gen(
  249. ['--untrusted-daemon'],
  250. [f'--rpc-bind-port={self.rpc_port}'],
  251. ['--wallet-dir='+self.wallet_dir],
  252. ['--log-file='+self.logfile],
  253. [f'--rpc-login={self.user}:{self.passwd}'],
  254. [f'--daemon-address={self.daemon_addr}', self.daemon_addr],
  255. [f'--daemon-port={self.daemon_port}', not self.daemon_addr],
  256. [f'--proxy={self.proxy}', self.proxy],
  257. [f'--pidfile={self.pidfile}', self.platform == 'linux'],
  258. ['--detach', not (self.opt.no_daemonize or self.platform=='win')],
  259. ['--stagenet', self.network == 'testnet'],
  260. )
  261. from .rpc import MoneroWalletRPCClient
  262. self.rpc = MoneroWalletRPCClient( daemon=self, test_connection=False )
  263. class CoinDaemon(Daemon):
  264. networks = ('mainnet','testnet','regtest')
  265. cfg_file_hdr = ''
  266. avail_flags = ('keep_cfg_file',)
  267. avail_opts = ('no_daemonize','online')
  268. testnet_dir = None
  269. test_suite_port_shift = 1237
  270. rpc_user = None
  271. rpc_password = None
  272. coins = {
  273. 'BTC': _cd('Bitcoin', ['bitcoin_core']),
  274. 'BCH': _cd('Bitcoin Cash Node', ['bitcoin_cash_node']),
  275. 'LTC': _cd('Litecoin', ['litecoin_core']),
  276. 'XMR': _cd('Monero', ['monero']),
  277. 'ETH': _cd('Ethereum', ['openethereum','geth'] + (['erigon'] if g.enable_erigon else []) ),
  278. 'ETC': _cd('Ethereum Classic', ['parity']),
  279. }
  280. @classmethod
  281. def get_network_ids(cls): # FIXME: gets IDs for _default_ daemon only
  282. from .protocol import CoinProtocol
  283. def gen():
  284. for coin,data in cls.coins.items():
  285. for network in globals()[data.daemon_ids[0]+'_daemon'].networks:
  286. yield CoinProtocol.Base.create_network_id(coin,network)
  287. return list(gen())
  288. def __new__(cls,
  289. network_id = None,
  290. proto = None,
  291. opts = None,
  292. flags = None,
  293. test_suite = False,
  294. port_shift = None,
  295. p2p_port = None,
  296. datadir = None,
  297. daemon_id = None ):
  298. assert network_id or proto, 'CoinDaemon_chk1'
  299. assert not (network_id and proto), 'CoinDaemon_chk2'
  300. if proto:
  301. network_id = proto.network_id
  302. network = proto.network
  303. coin = proto.coin
  304. else:
  305. network_id = network_id.lower()
  306. from .protocol import CoinProtocol,init_proto
  307. proto = init_proto(network_id=network_id)
  308. coin,network = CoinProtocol.Base.parse_network_id(network_id)
  309. coin = coin.upper()
  310. daemon_ids = cls.coins[coin].daemon_ids
  311. daemon_id = daemon_id or g.daemon_id or daemon_ids[0]
  312. if daemon_id not in daemon_ids:
  313. die(1,f'{daemon_id!r}: invalid daemon_id - valid choices: {fmt_list(daemon_ids)}')
  314. me = Daemon.__new__(globals()[daemon_id + '_daemon'])
  315. assert network in me.networks, f'{network!r}: unsupported network for daemon {daemon_id}'
  316. me.network = network
  317. me.coin = coin
  318. me.coin_name = cls.coins[coin].coin_name
  319. me.id = daemon_id
  320. me.proto = proto
  321. return me
  322. def __init__(self,
  323. network_id = None,
  324. proto = None,
  325. opts = None,
  326. flags = None,
  327. test_suite = False,
  328. port_shift = None,
  329. p2p_port = None,
  330. datadir = None,
  331. daemon_id = None ):
  332. self.test_suite = test_suite
  333. super().__init__(opts=opts,flags=flags)
  334. self._set_ok += ('shared_args','usr_coind_args')
  335. self.shared_args = []
  336. self.usr_coind_args = []
  337. for k,v in self.daemon_data._asdict().items():
  338. setattr(self,k,v)
  339. self.desc = '{} {} {}daemon'.format(
  340. self.coind_name,
  341. getattr(self.proto.network_names,self.network),
  342. 'test suite ' if test_suite else '' )
  343. # user-set values take precedence
  344. self.datadir = os.path.abspath(datadir or g.daemon_data_dir or self.init_datadir())
  345. self.non_dfl_datadir = bool(datadir or g.daemon_data_dir or test_suite or self.network == 'regtest')
  346. # init_datadir() may have already initialized logdir
  347. self.logdir = os.path.abspath(getattr(self,'logdir',self.datadir))
  348. ps_adj = (port_shift or 0) + (self.test_suite_port_shift if test_suite else 0)
  349. # user-set values take precedence
  350. self.rpc_port = (g.rpc_port or 0) + (port_shift or 0) if g.rpc_port else ps_adj + self.get_rpc_port()
  351. self.p2p_port = (
  352. p2p_port or (
  353. self.get_p2p_port() + ps_adj if self.get_p2p_port() and (test_suite or ps_adj) else None
  354. ) if self.network != 'regtest' else None )
  355. if hasattr(self,'private_ports'):
  356. self.private_port = getattr(self.private_ports,self.network)
  357. # bind_port == self.private_port or self.rpc_port
  358. self.pidfile = '{}/{}-{}-daemon-{}.pid'.format(self.logdir,self.id,self.network,self.bind_port)
  359. self.logfile = '{}/{}-{}-daemon-{}.log'.format(self.logdir,self.id,self.network,self.bind_port)
  360. self.init_subclass()
  361. def init_datadir(self):
  362. if self.test_suite:
  363. return os.path.join('test','daemons',self.coin.lower())
  364. else:
  365. return os.path.join(*self.datadirs[self.platform])
  366. @property
  367. def network_datadir(self):
  368. return self.datadir
  369. def get_rpc_port(self):
  370. return getattr(self.rpc_ports,self.network)
  371. def get_p2p_port(self):
  372. return None
  373. @property
  374. def start_cmd(self):
  375. return ([self.exec_fn]
  376. + self.coind_args
  377. + self.shared_args
  378. + self.usr_coind_args )
  379. def cli_cmd(self,*cmds):
  380. return ([self.cli_fn]
  381. + self.shared_args
  382. + list(cmds) )
  383. def start(self,*args,**kwargs):
  384. assert self.test_suite or self.network == 'regtest', 'start() restricted to test suite and regtest'
  385. return super().start(*args,**kwargs)
  386. def stop(self,*args,**kwargs):
  387. assert self.test_suite or self.network == 'regtest', 'stop() restricted to test suite and regtest'
  388. return super().stop(*args,**kwargs)
  389. def pre_start(self):
  390. os.makedirs(self.datadir,exist_ok=True)
  391. if self.test_suite or self.network == 'regtest':
  392. if self.cfg_file and not self.flag.keep_cfg_file:
  393. with open(f'{self.datadir}/{self.cfg_file}','w') as fp:
  394. fp.write(self.cfg_file_hdr)
  395. if self.use_pidfile and os.path.exists(self.pidfile):
  396. # Parity overwrites the data in the existing pidfile without zeroing it first, leading
  397. # to interesting consequences when the new PID has fewer digits than the previous one.
  398. os.unlink(self.pidfile)
  399. def remove_datadir(self):
  400. "remove the network's datadir"
  401. assert self.test_suite, 'datadir removal restricted to test suite'
  402. if self.state == 'stopped':
  403. try: # exception handling required for MSWin/MSYS2
  404. run(['/bin/rm','-rf',self.network_datadir])
  405. except:
  406. pass
  407. else:
  408. msg(f'Cannot remove {self.network_datadir!r} - daemon is not stopped')
  409. class bitcoin_core_daemon(CoinDaemon):
  410. daemon_data = _dd('Bitcoin Core', 220000, '22.0.0')
  411. exec_fn = 'bitcoind'
  412. cli_fn = 'bitcoin-cli'
  413. testnet_dir = 'testnet3'
  414. cfg_file_hdr = '# Bitcoin Core config file\n'
  415. tracking_wallet_name = 'mmgen-tracking-wallet'
  416. rpc_ports = _nw(8332, 18332, 18443)
  417. cfg_file = 'bitcoin.conf'
  418. datadirs = {
  419. 'linux': [g.home_dir,'.bitcoin'],
  420. 'win': [os.getenv('APPDATA'),'Bitcoin']
  421. }
  422. nonstd_datadir = False
  423. def init_datadir(self):
  424. if self.network == 'regtest' and not self.test_suite:
  425. return os.path.join( g.data_dir_root, 'regtest', g.coin.lower() )
  426. else:
  427. return super().init_datadir()
  428. @property
  429. def network_datadir(self):
  430. "location of the network's blockchain data and authentication cookie"
  431. return os.path.join (
  432. self.datadir, {
  433. 'mainnet': '',
  434. 'testnet': self.testnet_dir,
  435. 'regtest': 'regtest',
  436. }[self.network] )
  437. def init_subclass(self):
  438. if self.network == 'regtest':
  439. """
  440. fall back on hard-coded credentials
  441. """
  442. from .regtest import MMGenRegtest
  443. self.rpc_user = MMGenRegtest.rpc_user
  444. self.rpc_password = MMGenRegtest.rpc_password
  445. self.shared_args = list_gen(
  446. [f'--datadir={self.datadir}', self.nonstd_datadir or self.non_dfl_datadir],
  447. [f'--rpcport={self.rpc_port}'],
  448. [f'--rpcuser={self.rpc_user}', self.network == 'regtest'],
  449. [f'--rpcpassword={self.rpc_password}', self.network == 'regtest'],
  450. ['--testnet', self.network == 'testnet'],
  451. ['--regtest', self.network == 'regtest'],
  452. )
  453. self.coind_args = list_gen(
  454. ['--listen=0'],
  455. ['--keypool=1'],
  456. ['--rpcallowip=127.0.0.1'],
  457. [f'--rpcbind=127.0.0.1:{self.rpc_port}'],
  458. ['--pid='+self.pidfile, self.use_pidfile],
  459. ['--daemon', self.platform == 'linux' and not self.opt.no_daemonize],
  460. ['--fallbackfee=0.0002', self.coin == 'BTC' and self.network == 'regtest'],
  461. ['--usecashaddr=0', self.coin == 'BCH'],
  462. ['--mempoolreplacement=1', self.coin == 'LTC'],
  463. ['--txindex=1', self.coin == 'LTC' or self.network == 'regtest'],
  464. )
  465. self.lockfile = os.path.join(self.network_datadir,'.cookie')
  466. @property
  467. def state(self):
  468. cp = self.cli('getblockcount',silent=True)
  469. err = cp.stderr.decode()
  470. if ("error: couldn't connect" in err
  471. or "error: Could not connect" in err
  472. or "does not exist" in err ):
  473. # regtest has no cookie file, so test will always fail
  474. ret = 'busy' if (self.lockfile and os.path.exists(self.lockfile)) else 'stopped'
  475. elif cp.returncode == 0:
  476. ret = 'ready'
  477. else:
  478. ret = 'busy'
  479. if self.debug:
  480. print(f'State: {ret!r}')
  481. return ret
  482. @property
  483. def stop_cmd(self):
  484. return self.cli_cmd('stop')
  485. class bitcoin_cash_node_daemon(bitcoin_core_daemon):
  486. daemon_data = _dd('Bitcoin Cash Node', 24000000, '24.0.0')
  487. exec_fn = 'bitcoind-bchn'
  488. cli_fn = 'bitcoin-cli-bchn'
  489. rpc_ports = _nw(8432, 18432, 18543) # use non-standard ports (core+100)
  490. datadirs = {
  491. 'linux': [g.home_dir,'.bitcoin-bchn'],
  492. 'win': [os.getenv('APPDATA'),'Bitcoin_ABC']
  493. }
  494. cfg_file_hdr = '# Bitcoin Cash Node config file\n'
  495. nonstd_datadir = True
  496. class litecoin_core_daemon(bitcoin_core_daemon):
  497. daemon_data = _dd('Litecoin Core', 180100, '0.18.1')
  498. exec_fn = 'litecoind'
  499. cli_fn = 'litecoin-cli'
  500. testnet_dir = 'testnet4'
  501. rpc_ports = _nw(9332, 19332, 19443)
  502. cfg_file = 'litecoin.conf'
  503. datadirs = {
  504. 'linux': [g.home_dir,'.litecoin'],
  505. 'win': [os.getenv('APPDATA'),'Litecoin']
  506. }
  507. cfg_file_hdr = '# Litecoin Core config file\n'
  508. class monero_daemon(CoinDaemon):
  509. daemon_data = _dd('Monero', 'N/A', 'N/A')
  510. networks = ('mainnet','testnet')
  511. exec_fn = 'monerod'
  512. testnet_dir = 'stagenet'
  513. new_console_mswin = True
  514. host = 'localhost' # FIXME
  515. rpc_ports = _nw(18081, 38081, None) # testnet is stagenet
  516. cfg_file = 'bitmonero.conf'
  517. datadirs = {
  518. 'linux': [g.home_dir,'.bitmonero'],
  519. 'win': ['/','c','ProgramData','bitmonero']
  520. }
  521. def init_datadir(self):
  522. self.logdir = super().init_datadir()
  523. return os.path.join(
  524. self.logdir,
  525. self.testnet_dir if self.network == 'testnet' else '' )
  526. def get_p2p_port(self):
  527. return self.rpc_port - 1
  528. def init_subclass(self):
  529. from .rpc import MoneroRPCClientRaw
  530. self.rpc = MoneroRPCClientRaw(
  531. host = self.host,
  532. port = self.rpc_port,
  533. user = None,
  534. passwd = None,
  535. test_connection = False,
  536. daemon = self )
  537. self.shared_args = list_gen(
  538. [f'--no-zmq'],
  539. [f'--p2p-bind-port={self.p2p_port}', self.p2p_port],
  540. [f'--rpc-bind-port={self.rpc_port}'],
  541. ['--stagenet', self.network == 'testnet'],
  542. )
  543. self.coind_args = list_gen(
  544. ['--hide-my-port'],
  545. ['--no-igd'],
  546. [f'--data-dir={self.datadir}', self.non_dfl_datadir],
  547. [f'--pidfile={self.pidfile}', self.platform == 'linux'],
  548. ['--detach', not (self.opt.no_daemonize or self.platform=='win')],
  549. ['--offline', not self.opt.online],
  550. )
  551. @property
  552. def stop_cmd(self):
  553. return ['kill','-Wf',self.pid] if self.platform == 'win' else [self.exec_fn] + self.shared_args + ['exit']
  554. class ethereum_daemon(CoinDaemon):
  555. chain_subdirs = _nw('ethereum','goerli','DevelopmentChain')
  556. base_rpc_port = 8545 # same for all networks!
  557. base_p2p_port = 30303 # same for all networks!
  558. daemon_port_offset = 100
  559. network_port_offsets = _nw(0,10,20)
  560. def __init__(self,*args,**kwargs):
  561. if not hasattr(self,'all_daemons'):
  562. ethereum_daemon.all_daemons = get_subclasses(ethereum_daemon,names=True)
  563. self.port_offset = (
  564. self.all_daemons.index(self.id+'_daemon') * self.daemon_port_offset
  565. + getattr(self.network_port_offsets,self.network) )
  566. return super().__init__(*args,**kwargs)
  567. def get_rpc_port(self):
  568. return self.base_rpc_port + self.port_offset
  569. def get_p2p_port(self):
  570. return self.base_p2p_port + self.port_offset
  571. def init_datadir(self):
  572. self.logdir = super().init_datadir()
  573. return os.path.join(
  574. self.logdir,
  575. self.id,
  576. getattr(self.chain_subdirs,self.network) )
  577. class openethereum_daemon(ethereum_daemon):
  578. daemon_data = _dd('OpenEthereum', 3003000, '3.3.0')
  579. version_pat = r'OpenEthereum//v(\d+)\.(\d+)\.(\d+)'
  580. exec_fn = 'openethereum'
  581. cfg_file = 'parity.conf'
  582. datadirs = {
  583. 'linux': [g.home_dir,'.local','share','io.parity.ethereum'],
  584. 'win': [os.getenv('LOCALAPPDATA'),'Parity','Ethereum']
  585. }
  586. def init_subclass(self):
  587. ld = self.platform == 'linux' and not self.opt.no_daemonize
  588. self.coind_args = list_gen(
  589. ['--no-ws'],
  590. ['--no-ipc'],
  591. ['--no-secretstore'],
  592. [f'--jsonrpc-port={self.rpc_port}'],
  593. [f'--port={self.p2p_port}', self.p2p_port],
  594. [f'--base-path={self.datadir}', self.non_dfl_datadir],
  595. [f'--chain={self.proto.chain_name}', self.network!='regtest'],
  596. [f'--config=dev', self.network=='regtest'], # no presets for mainnet or testnet
  597. ['--mode=offline', self.test_suite or self.network=='regtest'],
  598. [f'--log-file={self.logfile}', self.non_dfl_datadir],
  599. ['daemon', ld],
  600. [self.pidfile, ld],
  601. )
  602. class parity_daemon(openethereum_daemon):
  603. daemon_data = _dd('Parity', 2007002, '2.7.2')
  604. version_pat = r'Parity-Ethereum//v(\d+)\.(\d+)\.(\d+)'
  605. exec_fn = 'parity'
  606. class geth_daemon(ethereum_daemon):
  607. daemon_data = _dd('Geth', 1010014, '1.10.14')
  608. version_pat = r'Geth/v(\d+)\.(\d+)\.(\d+)'
  609. exec_fn = 'geth'
  610. use_pidfile = False
  611. use_threads = True
  612. datadirs = {
  613. 'linux': [g.home_dir,'.ethereum','geth'],
  614. 'win': [os.getenv('LOCALAPPDATA'),'Geth'] # FIXME
  615. }
  616. def init_subclass(self):
  617. self.coind_args = list_gen(
  618. ['--verbosity=0'],
  619. ['--http'],
  620. ['--http.api=eth,web3,txpool'],
  621. [f'--http.port={self.rpc_port}'],
  622. [f'--port={self.p2p_port}', self.p2p_port], # geth binds p2p port even with --maxpeers=0
  623. ['--maxpeers=0', not self.opt.online],
  624. [f'--datadir={self.datadir}', self.non_dfl_datadir],
  625. ['--goerli', self.network=='testnet'],
  626. ['--dev', self.network=='regtest'],
  627. )
  628. # https://github.com/ledgerwatch/erigon
  629. class erigon_daemon(geth_daemon):
  630. daemon_data = _dd('Erigon', 2021009005, '2021.09.5')
  631. version_pat = r'erigon/(\d+)\.(\d+)\.(\d+)'
  632. exec_fn = 'erigon'
  633. private_ports = _nw(9090,9091,9092) # testnet and regtest are non-standard
  634. datadirs = {
  635. 'linux': [g.home_dir,'.local','share','erigon'],
  636. 'win': [os.getenv('LOCALAPPDATA'),'Erigon'] # FIXME
  637. }
  638. def init_subclass(self):
  639. self.coind_args = list_gen(
  640. ['--verbosity=0'],
  641. [f'--port={self.p2p_port}', self.p2p_port],
  642. ['--maxpeers=0', not self.opt.online],
  643. [f'--private.api.addr=127.0.0.1:{self.private_port}'],
  644. [f'--datadir={self.datadir}', self.non_dfl_datadir and not self.network=='regtest'],
  645. ['--chain=goerli', self.network=='testnet'],
  646. ['--chain=dev', self.network=='regtest'],
  647. ['--mine', self.network=='regtest'],
  648. )
  649. self.rpc_d = erigon_rpcdaemon(
  650. proto = self.proto,
  651. rpc_port = self.rpc_port,
  652. private_port = self.private_port,
  653. test_suite = self.test_suite,
  654. datadir = self.datadir )
  655. def start(self,quiet=False,silent=False):
  656. super().start(quiet=quiet,silent=silent)
  657. self.rpc_d.debug = self.debug
  658. return self.rpc_d.start(quiet=quiet,silent=silent)
  659. def stop(self,quiet=False,silent=False):
  660. self.rpc_d.debug = self.debug
  661. self.rpc_d.stop(quiet=quiet,silent=silent)
  662. return super().stop(quiet=quiet,silent=silent)
  663. @property
  664. def start_cmds(self):
  665. return [self.start_cmd,self.rpc_d.start_cmd]
  666. class erigon_rpcdaemon(RPCDaemon):
  667. master_daemon = 'erigon_daemon'
  668. rpc_type = 'Erigon'
  669. exec_fn = 'rpcdaemon'
  670. use_pidfile = False
  671. use_threads = True
  672. def __init__(self,proto,rpc_port,private_port,test_suite,datadir):
  673. self.proto = proto
  674. self.test_suite = test_suite
  675. super().__init__()
  676. self.network = proto.network
  677. self.rpc_port = rpc_port
  678. self.datadir = datadir
  679. self.daemon_args = list_gen(
  680. ['--verbosity=0'],
  681. [f'--private.api.addr=127.0.0.1:{private_port}'],
  682. [f'--http.port={self.rpc_port}'],
  683. [f'--datadir={self.datadir}', self.network != 'regtest'],
  684. ['--http.api=eth,web3,txpool'],
  685. )