regtest.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  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. regtest: Coin daemon regression test mode setup and operations for the MMGen suite
  20. """
  21. import os,time,shutil,json,re
  22. from subprocess import run,PIPE
  23. from .common import *
  24. from .protocol import init_proto
  25. from .rpc import rpc_init,json_encoder
  26. def create_data_dir(data_dir):
  27. try: os.stat(os.path.join(data_dir,'regtest'))
  28. except: pass
  29. else:
  30. if keypress_confirm(
  31. f'Delete your existing MMGen regtest setup at {data_dir!r} and create a new one?'):
  32. shutil.rmtree(data_dir)
  33. else:
  34. die()
  35. try: os.makedirs(data_dir)
  36. except: pass
  37. miner_addr = {
  38. # cTyMdQ2BgfAsjopRVZrj7AoEGp97pKfrC2NkqLuwHr4KHfPNAKwp hdseed=1 ('beadcafe'*8)
  39. 'btc': 'bcrt1qaq8t3pakcftpk095tnqfv5cmmczysls024atnd',
  40. 'ltc': 'rltc1qaq8t3pakcftpk095tnqfv5cmmczysls05c8zyn',
  41. 'bch': 'n2fxhNx27GhHAWQhyuZ5REcBNrJqCJsJ12',
  42. }
  43. def create_hdseed(proto):
  44. from .tool.api import tool_api
  45. t = tool_api()
  46. t.init_coin(proto.coin,proto.network)
  47. t.addrtype = 'compressed' if proto.coin == 'BCH' else 'bech32'
  48. return t.hex2wif('beadcafe'*8)
  49. def cliargs_convert(args):
  50. def gen():
  51. for arg in args:
  52. if arg.lower() in ('true','false'):
  53. yield (True,False)[arg.lower() == 'false']
  54. elif len(str(arg)) < 20 and re.match(r'[0-9]+',arg):
  55. yield int(arg)
  56. else:
  57. yield arg
  58. return tuple(gen())
  59. class MMGenRegtest(MMGenObject):
  60. rpc_user = 'bobandalice'
  61. rpc_password = 'hodltothemoon'
  62. users = ('bob','alice','miner')
  63. coins = ('btc','bch','ltc')
  64. usr_cmds = ('setup','generate','send','start','stop', 'state', 'balances','mempool','cli','wallet_cli')
  65. def __init__(self,coin):
  66. self.coin = coin.lower()
  67. assert self.coin in self.coins, f'{coin!r}: invalid coin for regtest'
  68. from .daemon import CoinDaemon
  69. self.proto = init_proto(self.coin,regtest=True,need_amt=True)
  70. self.d = CoinDaemon(self.coin+'_rt',test_suite=g.test_suite)
  71. async def generate(self,blocks=1,silent=False):
  72. blocks = int(blocks)
  73. if self.d.state == 'stopped':
  74. die(1,'Regtest daemon is not running')
  75. self.d.wait_for_state('ready')
  76. out = await self.rpc_call(
  77. 'generatetoaddress',
  78. blocks,
  79. miner_addr[self.coin],
  80. wallet = 'miner' )
  81. if len(out) != blocks:
  82. die(4,'Error generating blocks')
  83. gmsg(f'Mined {blocks} block{suf(blocks)}')
  84. async def setup(self):
  85. try: os.makedirs(self.d.datadir)
  86. except: pass
  87. if self.d.state != 'stopped':
  88. await self.rpc_call('stop')
  89. create_data_dir(self.d.datadir)
  90. gmsg(f'Starting {self.coin.upper()} regtest setup')
  91. self.d.start(silent=True)
  92. rpc = await rpc_init(self.proto,backend=None,daemon=self.d)
  93. for user in ('miner','bob','alice'):
  94. gmsg(f'Creating {capfirst(user)}’s tracking wallet')
  95. await rpc.icall(
  96. 'createwallet',
  97. wallet_name = user,
  98. blank = True,
  99. no_keys = user != 'miner',
  100. load_on_startup = False )
  101. # BCH and LTC daemons refuse to set HD seed with empty blockchain ("in IBD" error),
  102. # so generate a block:
  103. await self.generate(1,silent=True)
  104. # Unfortunately, we don’t get deterministic output with BCH and LTC even with fixed
  105. # hdseed, as their 'sendtoaddress' calls produce non-deterministic TXIDs due to random
  106. # input ordering and fee estimation.
  107. await rpc.call(
  108. 'sethdseed',
  109. True,
  110. create_hdseed(self.proto),
  111. wallet = 'miner' )
  112. await self.generate(432,silent=True)
  113. gmsg('Setup complete')
  114. if opt.setup_no_stop_daemon:
  115. msg('Leaving regtest daemon running')
  116. else:
  117. msg('Stopping regtest daemon')
  118. await self.rpc_call('stop')
  119. def init_daemon(self,reindex=False):
  120. if reindex:
  121. self.d.usr_coind_args.append('--reindex')
  122. async def start_daemon(self,reindex=False,silent=True):
  123. self.init_daemon(reindex=reindex)
  124. self.d.start(silent=silent)
  125. for user in ('miner','bob','alice'):
  126. msg(f'Loading {capfirst(user)}’s wallet')
  127. await self.rpc_call('loadwallet',user,start_daemon=False)
  128. async def rpc_call(self,*args,wallet=None,start_daemon=True):
  129. if start_daemon and self.d.state == 'stopped':
  130. await self.start_daemon()
  131. rpc = await rpc_init(self.proto,backend=None,daemon=self.d)
  132. return await rpc.call(*args,wallet=wallet)
  133. async def start(self):
  134. if self.d.state == 'stopped':
  135. await self.start_daemon(silent=False)
  136. else:
  137. msg(f'{g.coin} regtest daemon already started')
  138. async def stop(self):
  139. if self.d.state == 'stopped':
  140. msg(f'{g.coin} regtest daemon already stopped')
  141. else:
  142. msg(f'Stopping {g.coin} regtest daemon')
  143. self.d.stop(silent=True)
  144. def state(self):
  145. msg(self.d.state)
  146. async def balances(self):
  147. bal = {}
  148. users = ('bob','alice')
  149. for user in users:
  150. out = await self.rpc_call('listunspent',0,wallet=user)
  151. bal[user] = sum(e['amount'] for e in out)
  152. fs = '{:<16} {:18.8f}'
  153. for user in users:
  154. msg(fs.format(user.capitalize()+"'s balance:",bal[user]))
  155. msg(fs.format('Total balance:',sum(v for k,v in bal.items())))
  156. async def send(self,addr,amt):
  157. gmsg(f'Sending {amt} miner {self.d.coin} to address {addr}')
  158. cp = await self.rpc_call('sendtoaddress',addr,str(amt),wallet='miner')
  159. await self.generate(1)
  160. async def mempool(self):
  161. await self.cli('getrawmempool')
  162. async def cli(self,*args):
  163. ret = await self.rpc_call(*cliargs_convert(args))
  164. print(ret if type(ret) == str else json.dumps(ret,cls=json_encoder,indent=4))
  165. async def wallet_cli(self,wallet,*args):
  166. ret = await self.rpc_call(*cliargs_convert(args),wallet=wallet)
  167. print(ret if type(ret) == str else json.dumps(ret,cls=json_encoder,indent=4))
  168. async def cmd(self,args):
  169. ret = getattr(self,args[0])(*args[1:])
  170. return (await ret) if type(ret).__name__ == 'coroutine' else ret
  171. async def fork(self,coin): # currently disabled
  172. proto = init_proto(coin,False)
  173. if not [f for f in proto.forks if f[2] == proto.coin.lower() and f[3] == True]:
  174. die(1,f'Coin {proto.coin} is not a replayable fork of coin {coin}')
  175. gmsg(f'Creating fork from coin {coin} to coin {proto.coin}')
  176. source_rt = MMGenRegtest(coin)
  177. try:
  178. os.stat(source_rt.d.datadir)
  179. except:
  180. die(1,f'Source directory {source_rt.d.datadir!r} does not exist!')
  181. # stop the source daemon
  182. if source_rt.d.state != 'stopped':
  183. await source_rt.d.cli('stop')
  184. # stop our daemon
  185. if self.d.state != 'stopped':
  186. await self.rpc_call('stop')
  187. try: os.makedirs(self.d.datadir)
  188. except: pass
  189. create_data_dir(self.d.datadir)
  190. os.rmdir(self.d.datadir)
  191. shutil.copytree(source_data_dir,self.d.datadir,symlinks=True)
  192. await self.start_daemon(reindex=True)
  193. await self.rpc_call('stop')
  194. gmsg(f'Fork {proto.coin} successfully created')