From a3f9676677d4757ff5f3cb709fe6bc51f198ecf8 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Sat, 22 Aug 2026 18:34:47 +0000 Subject: [PATCH] cmdtest.py regtest: enable deterministic testing with descriptor wallet --- mmgen/main_regtest.py | 3 +++ mmgen/proto/btc/regtest.py | 51 +++++++++++++++++++++++++++++++++--- test/cmdtest_d/chainsplit.py | 4 +-- test/cmdtest_d/regtest.py | 16 +++++++++-- test/cmdtest_d/swap.py | 2 +- 5 files changed, 67 insertions(+), 9 deletions(-) diff --git a/mmgen/main_regtest.py b/mmgen/main_regtest.py index 40d64b3b..e08d67e4 100755 --- a/mmgen/main_regtest.py +++ b/mmgen/main_regtest.py @@ -37,12 +37,15 @@ opts_data = { -n, --setup-no-stop-daemon Don't stop daemon after setup is finished -q, --quiet Produce quieter output -v, --verbose Produce more verbose output +-w, --wallet-src=DIR Initialize miner and tracking wallets from wallets in + DIR instead of creating new ones """, 'notes': """ AVAILABLE COMMANDS setup - set up Bob and Alice regtest mode + create_wallets - create Miner wallet and Bob and Alice’s tracking wallets start - start the regtest coin daemon stop - stop the regtest coin daemon generate N - mine N blocks (defaults to 1) diff --git a/mmgen/proto/btc/regtest.py b/mmgen/proto/btc/regtest.py index acd2cbd1..b554d35c 100755 --- a/mmgen/proto/btc/regtest.py +++ b/mmgen/proto/btc/regtest.py @@ -56,6 +56,7 @@ class MMGenRegtest(MMGenObject): coins = ('btc', 'bch', 'ltc') usr_cmds = ( 'setup', + 'create_wallets', 'generate', 'send', 'start', @@ -95,12 +96,27 @@ class MMGenRegtest(MMGenObject): async def rpc(self): return await rpc_init(self.cfg, self.proto, backend=None, daemon=self.d) + @property + async def miner_bip_hd_node(self): + if not hasattr(self, '_miner_bip_hd_node'): + from ...cfg import Config + from ...bip_hd import BipHDNode + data = await self.rpc_call('gethdkeys', {'private': True}, wallet='miner') + self._miner_bip_hd_node = BipHDNode.from_path( + Config({'regtest': True}), + None, + "m/{}'/1'/0'/0/0".format(44 if self.proto.coin == 'BCH' else 84), + xprv = data[0]['xprv'], + addr_type = 'compressed' if self.proto.coin == 'BCH' else 'bech32', + no_path_checks = True) + return self._miner_bip_hd_node + @property async def miner_addr(self): if not hasattr(self, '_miner_addr'): self._miner_addr = ( self.bdb_miner_addrs[self.coin] if self.bdb_wallet else - await self.rpc_call('getnewaddress', wallet='miner')) + (await self.miner_bip_hd_node).address) return self._miner_addr @property @@ -108,7 +124,7 @@ class MMGenRegtest(MMGenObject): if not hasattr(self, '_miner_wif'): self._miner_wif = ( self.bdb_miner_wif if self.bdb_wallet else - None) + (await self.miner_bip_hd_node).privkey.wif) return self._miner_wif def create_hdseed_wif(self): @@ -150,7 +166,19 @@ class MMGenRegtest(MMGenObject): descriptors = not self.bdb_wallet, load_on_startup = False) - async def setup(self): + async def copy_wallet(self, user): + gmsg(f'Copying {capfirst(user)}’s tracking wallet') + from shutil import copytree + src = os.path.join(self.cfg.wallet_src, user) + if not os.path.exists(src): + die(2, f'Source wallet ‘{os.path.abspath(src)}’ does not exist!') + dest = os.path.join(self.d.network_datadir, 'wallets', user) + copytree(src, dest) + + async def create_wallets(self): + await self.setup(create_wallets_only=True) + + async def setup(self, create_wallets_only=False): try: os.makedirs(self.d.datadir) @@ -167,7 +195,19 @@ class MMGenRegtest(MMGenObject): self.d.start(silent=True) for user in ('miner', 'bob', 'alice'): - await self.create_wallet(user) + if self.cfg.wallet_src: + await self.copy_wallet(user) + else: + await self.create_wallet(user) + + if create_wallets_only: + msg(f"Wallets are in '{self.d.network_datadir}/wallets'") + msg('Stopping regtest daemon') + await self.rpc_call('stop') + return + + if self.cfg.wallet_src: + await self.load_wallets() # BCH and LTC daemons refuse to set HD seed with empty blockchain ("in IBD" error), # so generate a block: @@ -204,6 +244,9 @@ class MMGenRegtest(MMGenObject): async def start_daemon(self, *, reindex=False, silent=True): self.init_daemon(reindex=reindex) self.d.start(silent=silent) + await self.load_wallets() + + async def load_wallets(self): for user in ('miner', 'bob', 'alice'): msg(f'Loading {capfirst(user)}’s wallet') await self.rpc_call('loadwallet', user, start_daemon=False) diff --git a/test/cmdtest_d/chainsplit.py b/test/cmdtest_d/chainsplit.py index 846c0df3..43b4b1c8 100755 --- a/test/cmdtest_d/chainsplit.py +++ b/test/cmdtest_d/chainsplit.py @@ -60,14 +60,14 @@ class CmdTestChainsplit(CmdTestRegtest): def split_fork(self): self.coin = 'B2X' - t = self.spawn('mmgen-regtest', ['fork', 'btc']) + t = self.spawn('mmgen-regtest', self.regtest_opts + ['fork', 'btc']) t.expect('Creating fork from coin') t.expect('successfully created') t.ok() def split_start(self, coin): self.coin = coin - t = self.spawn('mmgen-regtest', ['bob']) + t = self.spawn('mmgen-regtest', self.regtest_opts + ['bob']) t.expect('Starting') t.expect('done') t.ok() diff --git a/test/cmdtest_d/regtest.py b/test/cmdtest_d/regtest.py index d6ddc8aa..8dc0fc03 100755 --- a/test/cmdtest_d/regtest.py +++ b/test/cmdtest_d/regtest.py @@ -27,7 +27,7 @@ from mmgen.proto.btc.regtest import MMGenRegtest from mmgen.proto.bch.cashaddr import b32a from mmgen.proto.btc.common import b58a from mmgen.color import yellow -from mmgen.util import msg_r, die, gmsg, capfirst, suf, fmt_list, is_hex_str +from mmgen.util import msg_r, die, gmsg, capfirst, suf, fmt_list, is_hex_str, fmt from mmgen.protocol import init_proto from mmgen.addrlist import AddrList from mmgen.wallet import Wallet, get_wallet_cls @@ -505,6 +505,7 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared): self.test_rbf = True # tests are non-coin-dependent, so run just once for BTC if cfg.test_suite_deterministic: self.deterministic = True + omsg('Deterministic testing enabled!') self.spawn_env['MMGEN_BOGUS_SEND'] = '' self.write_to_tmpfile('wallet_password', rt_pw) @@ -548,6 +549,17 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared): rmtree(joinpath(self.tr.data_dir, 'regtest')) except: pass + if self.deterministic: + action = 'Loading' + if not os.path.exists('wallets/alice'): + die(1, fmt(""" + Saved tracking wallets are required for deterministic testing. + + Run ‘mmgen-regtest create_wallets’, move or copy the resulting ‘wallet’ + directory to the repository root, and re-run the test. + """)) + else: + action = 'Creating' t = self.spawn( 'mmgen-regtest', self.regtest_opts @@ -556,7 +568,7 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared): no_passthru_opts = True) t.expect('Starting') for _ in range(3): - t.expect('Creating') + t.expect(action) for _ in range(17): t.expect('Mined') t.expect('Setup complete') diff --git a/test/cmdtest_d/swap.py b/test/cmdtest_d/swap.py index 8fe280a8..9ce9e0d6 100755 --- a/test/cmdtest_d/swap.py +++ b/test/cmdtest_d/swap.py @@ -588,7 +588,7 @@ class CmdTestSwap(CmdTestSwapMethods, CmdTestRegtest, CmdTestAutosignThreaded): addr = next(s for s in strip_ansi_escapes(t.read()).splitlines() if 'C:1 No' in s).split()[3] t = self.spawn( 'mmgen-regtest', - [coin_arg, 'send', addr, str(amt)], + self.regtest_opts + [coin_arg, 'send', addr, str(amt)], no_passthru_opts = ['coin'], no_msg = True) return t