regtest.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. #!/usr/bin/env python
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2018 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. regtest: Coin daemon regression test mode setup and operations for the MMGen suite
  20. """
  21. import os,subprocess,time,shutil
  22. from mmgen.common import *
  23. PIPE = subprocess.PIPE
  24. data_dir = os.path.join(g.data_dir_root,'regtest',g.coin.lower())
  25. daemon_dir = os.path.join(data_dir,'regtest')
  26. rpc_ports = { 'btc':8552, 'bch':8553, 'b2x':8554, 'ltc':8555 }
  27. rpc_port = rpc_ports[g.coin.lower()]
  28. rpc_user = 'bobandalice'
  29. rpc_password = 'hodltothemoon'
  30. tr_wallet = lambda user: os.path.join(daemon_dir,'wallet.dat.'+user)
  31. common_args = lambda: (
  32. '--rpcuser={}'.format(rpc_user),
  33. '--rpcpassword={}'.format(rpc_password),
  34. '--rpcport={}'.format(rpc_port),
  35. '--regtest',
  36. '--datadir={}'.format(data_dir))
  37. def start_daemon(user,quiet=False,daemon=True,reindex=False):
  38. # requires Bitcoin ABC version >= 0.16.2
  39. add_args = ((),('--usecashaddr=0',))[g.proto.daemon_name=='bitcoind-abc']
  40. cmd = (
  41. g.proto.daemon_name,
  42. '--listen=0',
  43. '--keypool=1',
  44. '--wallet={}'.format(os.path.basename(tr_wallet(user)))
  45. ) + add_args + common_args()
  46. if daemon: cmd += ('--daemon',)
  47. if reindex: cmd += ('--reindex',)
  48. if not g.debug or quiet: vmsg('{}'.format(' '.join(cmd)))
  49. p = subprocess.Popen(cmd,stdout=PIPE,stderr=PIPE)
  50. err = process_output(p,silent=False)[1]
  51. if err:
  52. rdie(1,'Error starting the {} daemon:\n{}'.format(g.proto.name.capitalize(),err))
  53. def start_daemon_mswin(user,quiet=False,reindex=False):
  54. import threading
  55. t = threading.Thread(target=start_daemon,args=[user,quiet,False,reindex])
  56. t.daemon = True
  57. t.start()
  58. if not opt.verbose: Msg_r(' \b') # blocks w/o this...crazy
  59. def start_cmd(*args,**kwargs):
  60. cmd = args
  61. if args[0] == 'cli':
  62. cmd = (g.proto.name+'-cli',) + common_args() + args[1:]
  63. if g.debug or not 'quiet' in kwargs:
  64. vmsg('{}'.format(' '.join(cmd)))
  65. ip = op = ep = (PIPE,None)['no_pipe' in kwargs and kwargs['no_pipe']]
  66. if 'pipe_stdout_only' in kwargs and kwargs['pipe_stdout_only']: ip = ep = None
  67. return subprocess.Popen(cmd,stdin=ip,stdout=op,stderr=ep)
  68. def test_daemon():
  69. p = start_cmd('cli','getblockcount',quiet=True)
  70. err = process_output(p,silent=True)[1]
  71. ret,state = p.wait(),None
  72. if "error: couldn't connect" in err: state = 'stopped'
  73. if not state: state = ('busy','ready')[ret==0]
  74. return state
  75. def wait_for_daemon(state,silent=False,nonl=False):
  76. for i in range(200):
  77. ret = test_daemon()
  78. if not silent:
  79. if opt.verbose: msg('returning state '+ret)
  80. else: gmsg_r('.')
  81. if ret == state and not nonl: msg('')
  82. if ret == state: return True
  83. time.sleep(1)
  84. else:
  85. die(1,'timeout exceeded')
  86. def get_balances():
  87. user1 = get_current_user(quiet=True)
  88. if user1 == None:
  89. user('bob')
  90. user1 = get_current_user(quiet=True)
  91. # die(1,'Regtest daemon not running')
  92. user2 = ('bob','alice')[user1=='bob']
  93. tbal = 0
  94. # don't need to save and restore these, as we exit immediately
  95. g.rpc_host = 'localhost'
  96. g.rpc_port = rpc_port
  97. g.rpc_user = rpc_user
  98. g.rpc_password = rpc_password
  99. g.testnet = True
  100. rpc_init()
  101. for u in (user1,user2):
  102. bal = g.proto.coin_amt(g.rpch.getbalance('*',0,True))
  103. if u == user1: user(user2)
  104. msg('{:<16} {:12}'.format(u.capitalize()+"'s balance:",bal))
  105. tbal += bal
  106. msg('{:<16} {:12}'.format('Total balance:',tbal))
  107. def create_data_dir():
  108. try: os.stat(os.path.join(data_dir,'regtest')) # don't use daemon_dir, as data_dir may change
  109. except: pass
  110. else:
  111. m = "Delete your existing MMGen regtest setup at '{}' and create a new one?"
  112. if keypress_confirm(m.format(data_dir)):
  113. shutil.rmtree(data_dir)
  114. else:
  115. die()
  116. try: os.makedirs(data_dir)
  117. except: pass
  118. def process_output(p,silent=False):
  119. out = p.stdout.read()
  120. if g.platform == 'win' and not opt.verbose: Msg_r(' \b')
  121. err = p.stderr.read()
  122. if g.debug or not silent:
  123. vmsg('stdout: [{}]'.format(out.strip()))
  124. vmsg('stderr: [{}]'.format(err.strip()))
  125. return out,err
  126. def start_and_wait(user,silent=False,nonl=False,reindex=False):
  127. vmsg('Starting {} regtest daemon'.format(g.proto.name))
  128. (start_daemon_mswin,start_daemon)[g.platform=='linux'](user,reindex=reindex)
  129. wait_for_daemon('ready',silent=silent,nonl=nonl)
  130. def stop_and_wait(silent=False,nonl=False,stop_silent=False,ignore_noconnect_error=False):
  131. stop(silent=stop_silent,ignore_noconnect_error=ignore_noconnect_error)
  132. wait_for_daemon('stopped',silent=silent,nonl=nonl)
  133. def send(addr,amt):
  134. user('miner')
  135. gmsg('Sending {} {} to address {}'.format(amt,g.coin,addr))
  136. p = start_cmd('cli','sendtoaddress',addr,str(amt))
  137. process_output(p)
  138. p.wait()
  139. generate(1)
  140. def show_mempool():
  141. p = start_cmd('cli','getrawmempool')
  142. from pprint import pformat
  143. from ast import literal_eval
  144. msg(pformat(literal_eval(p.stdout.read())))
  145. p.wait()
  146. def cli(*args):
  147. p = start_cmd(*(('cli',) + args))
  148. from pprint import pformat
  149. Msg_r(p.stdout.read())
  150. msg_r(p.stderr.read())
  151. p.wait()
  152. def fork(coin):
  153. coin = coin.upper()
  154. from mmgen.protocol import CoinProtocol
  155. forks = CoinProtocol(coin,False).forks
  156. if not [f for f in forks if f[2] == g.coin.lower() and f[3] == True]:
  157. die(1,"Coin {} is not a replayable fork of coin {}".format(g.coin,coin))
  158. gmsg('Creating fork from coin {} to coin {}'.format(coin,g.coin))
  159. source_data_dir = os.path.join(g.data_dir_root,'regtest',coin.lower())
  160. try: os.stat(source_data_dir)
  161. except: die(1,"Source directory '{}' does not exist!".format(source_data_dir))
  162. # stop the other daemon
  163. global rpc_port,data_dir
  164. rpc_port_save,data_dir_save = rpc_port,data_dir
  165. rpc_port = rpc_ports[coin.lower()]
  166. data_dir = os.path.join(g.data_dir_root,'regtest',coin.lower())
  167. if test_daemon() != 'stopped':
  168. stop_and_wait(silent=True,stop_silent=True)
  169. rpc_port,data_dir = rpc_port_save,data_dir_save
  170. try: os.makedirs(data_dir)
  171. except: pass
  172. # stop our daemon
  173. if test_daemon() != 'stopped':
  174. stop_and_wait(silent=True,stop_silent=True)
  175. create_data_dir()
  176. os.rmdir(data_dir)
  177. shutil.copytree(source_data_dir,data_dir,symlinks=True)
  178. start_and_wait('miner',reindex=True,silent=True)
  179. stop_and_wait(silent=True,stop_silent=True)
  180. gmsg('Fork {} successfully created'.format(g.coin))
  181. def setup():
  182. try: os.makedirs(data_dir)
  183. except: pass
  184. if test_daemon() != 'stopped':
  185. stop_and_wait(silent=True,stop_silent=True)
  186. create_data_dir()
  187. gmsg('Starting setup')
  188. gmsg_r('Creating miner wallet')
  189. start_and_wait('miner')
  190. generate(432,silent=True)
  191. stop_and_wait(silent=True,stop_silent=True)
  192. for user in ('alice','bob'):
  193. gmsg_r("Creating {}'s tracking wallet".format(user.capitalize()))
  194. start_and_wait(user)
  195. if user == 'bob' and opt.setup_no_stop_daemon:
  196. msg('Leaving daemon running with Bob as current user')
  197. else:
  198. stop_and_wait(silent=True,stop_silent=True)
  199. gmsg('Setup complete')
  200. def get_current_user_win(quiet=False):
  201. if test_daemon() == 'stopped': return None
  202. p = start_cmd('grep','Using wallet',os.path.join(daemon_dir,'debug.log'),quiet=True)
  203. try: wallet_fn = p.stdout.readlines()[-1].split()[-1]
  204. except: return None
  205. for k in ('miner','bob','alice'):
  206. if wallet_fn == 'wallet.dat.'+k:
  207. if not quiet: msg('Current user is {}'.format(k.capitalize()))
  208. return k
  209. return None
  210. def get_current_user_unix(quiet=False):
  211. p = start_cmd('pgrep','-af','{}.*--rpcport={}.*'.format(g.proto.daemon_name,rpc_port))
  212. cmdline = p.stdout.read()
  213. if not cmdline: return None
  214. for k in ('miner','bob','alice'):
  215. if 'wallet.dat.'+k in cmdline:
  216. if not quiet: msg('Current user is {}'.format(k.capitalize()))
  217. return k
  218. return None
  219. get_current_user = (get_current_user_win,get_current_user_unix)[g.platform=='linux']
  220. def bob(): return user('bob',quiet=False)
  221. def alice(): return user('alice',quiet=False)
  222. def miner(): return user('miner',quiet=False)
  223. def user(user=None,quiet=False):
  224. if user==None:
  225. get_current_user()
  226. return True
  227. if test_daemon() == 'busy':
  228. wait_for_daemon('ready')
  229. if test_daemon() == 'ready':
  230. if user == get_current_user(quiet=True):
  231. if not quiet: msg('{} is already the current user for coin {}'.format(user.capitalize(),g.coin))
  232. return True
  233. gmsg_r('Switching to user {} for coin {}'.format(user.capitalize(),g.coin))
  234. stop_and_wait(silent=False,nonl=True,stop_silent=True)
  235. time.sleep(0.1) # file lock has race condition - TODO: test for lock file
  236. start_and_wait(user,nonl=True)
  237. else:
  238. gmsg_r('Starting regtest daemon for coin {} with current user {}'.format(g.coin,user.capitalize()))
  239. start_and_wait(user,nonl=True)
  240. gmsg('done')
  241. def stop(silent=False,ignore_noconnect_error=True):
  242. if test_daemon() != 'stopped' and not silent:
  243. gmsg('Stopping {} regtest daemon for coin {}'.format(g.proto.name,g.coin))
  244. p = start_cmd('cli','stop')
  245. err = process_output(p)[1]
  246. if err:
  247. if "couldn't connect to server" in err and not ignore_noconnect_error:
  248. rdie(1,'Error stopping the {} daemon:\n{}'.format(g.proto.name.capitalize(),err))
  249. msg(err)
  250. return p.wait()
  251. def generate(blocks=1,silent=False):
  252. if test_daemon() == 'stopped':
  253. die(1,'Regtest daemon is not running')
  254. wait_for_daemon('ready',silent=True)
  255. p = start_cmd('cli','generate',str(blocks))
  256. out = process_output(p,silent=silent)[0]
  257. from ast import literal_eval
  258. if len(literal_eval(out)) != blocks:
  259. rdie(1,'Error generating blocks')
  260. p.wait()
  261. gmsg('Mined {} block{}'.format(blocks,suf(blocks,'s')))