daemon.py 25 KB

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