From efe3cd22610ec1db6fbdd376774677c1c25cfc66 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Thu, 21 May 2026 12:09:27 +0000 Subject: [PATCH 01/13] update secp256k1 extmod for free-threaded Python builds --- extmod/secp256k1mod.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/extmod/secp256k1mod.c b/extmod/secp256k1mod.c index 8fdea24c..bd1a7dc1 100755 --- a/extmod/secp256k1mod.c +++ b/extmod/secp256k1mod.c @@ -428,12 +428,19 @@ PyMODINIT_FUNC PyInit_secp256k1(void) { if (module == NULL) INITERROR; + struct module_state *st = GETSTATE(module); st->error = PyErr_NewException("secp256k1.Error", NULL, NULL); + if (st->error == NULL) { Py_DECREF(module); INITERROR; } + +#ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(module, Py_MOD_GIL_NOT_USED); +#endif + return module; } From 2b1fbfa0fcefdaec54ba47a1843ca8052502daf6 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Thu, 21 May 2026 12:09:32 +0000 Subject: [PATCH 02/13] cmdtest.py: new `skip_on_condition()` method --- test/cmdtest_d/base.py | 12 ++++++------ test/cmdtest_d/input.py | 14 ++++++++++---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/test/cmdtest_d/base.py b/test/cmdtest_d/base.py index 6cda6a27..1cec2938 100755 --- a/test/cmdtest_d/base.py +++ b/test/cmdtest_d/base.py @@ -95,21 +95,21 @@ class CmdTestBase: except: msg(f'{fn}: file does not exist or could not be deleted') - def skip_for_platform(self, name, extra_msg=None): - if gc.platform == name: - msg(gray('Skipping test {!r} for {} platform{}'.format( + def skip_on_condition(self, condition, message, extra_msg): + if condition: + msg(gray('Skipping test {!r} {}{}'.format( self.test_name, - name, + message, f' ({extra_msg})' if extra_msg else ""))) return True else: return False def skip_for_mac(self, extra_msg=None): - return self.skip_for_platform('darwin', extra_msg) + return self.skip_on_condition(gc.platform=='darwin', 'for macOS platform', extra_msg) def skip_for_win(self, extra_msg=None): - return self.skip_for_platform('win32', extra_msg) + return self.skip_on_condition(gc.platform=='win32', 'for win32 platform', extra_msg) def spawn_chk(self, *args, **kwargs): """ diff --git a/test/cmdtest_d/input.py b/test/cmdtest_d/input.py index e558dbf2..9dbd4316 100755 --- a/test/cmdtest_d/input.py +++ b/test/cmdtest_d/input.py @@ -106,6 +106,12 @@ class CmdTestInput(CmdTestBase): ) } + def skip_no_readline_insert(self, extra_msg=None): + return self.skip_on_condition( + gc.platform == 'darwin', + ' (no readline insert support)', + extra_msg) + def get_seed_from_stdin(self): self.spawn(msg_only=True) from subprocess import run, PIPE @@ -336,7 +342,7 @@ class CmdTestInput(CmdTestBase): False) def line_input_insert_term1(self): - if self.skip_for_mac('readline text buffer issues'): + if self.skip_no_readline_insert(): return 'skip' return self._line_input( ['prompt> ', True, 'foo', True], @@ -346,7 +352,7 @@ class CmdTestInput(CmdTestBase): hold_protect_delay) def line_input_insert_term2(self): - if self.skip_for_mac('readline text buffer issues'): + if self.skip_no_readline_insert(): return 'skip' return self._line_input( ['prompt> ', True, 'foo', False], @@ -363,7 +369,7 @@ class CmdTestInput(CmdTestBase): hold_protect_delay) def line_input_edit_term_insert(self): - if self.skip_for_mac('readline text buffer issues'): + if self.skip_no_readline_insert(): return 'skip' return self._line_input( ['prompt> ', True, 'φυφυ', True], @@ -373,7 +379,7 @@ class CmdTestInput(CmdTestBase): hold_protect_delay) def line_input_erase_term(self): - if self.skip_for_mac('readline text buffer issues'): + if self.skip_no_readline_insert(): return 'skip' return self._line_input( ['prompt> ', True, 'foobarbaz', True], From 86ac4e84181ac27126bf59246ee3f0e493a7d663 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Thu, 21 May 2026 12:09:32 +0000 Subject: [PATCH 03/13] new `threaded_python` cfg var --- mmgen/cfg.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mmgen/cfg.py b/mmgen/cfg.py index a5a3442c..67715c39 100755 --- a/mmgen/cfg.py +++ b/mmgen/cfg.py @@ -228,6 +228,7 @@ class Config(Lockable): enable_erigon = False autochg_ignore_labels = False autosign = False + threaded_python = not sys._is_gil_enabled() # regtest: bob = False @@ -345,6 +346,7 @@ class Config(Lockable): 'MMGEN_TEST_SUITE_PEXPECT_TIMEOUT', 'MMGEN_TEST_SUITE_POPEN_SPAWN', 'MMGEN_TEST_SUITE_ROOT_PFX', + 'MMGEN_THREADED_PYTHON', 'MMGEN_TRACEBACK', 'MMGEN_BLACKLIST_DAEMONS', 'MMGEN_BOGUS_SEND', From 7ca24493068f7342a4a69e3a39c35b84d6cfeac6 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Thu, 21 May 2026 12:09:32 +0000 Subject: [PATCH 04/13] cmdtest.py input: disable some tests for free-threaded Python builds --- test/cmdtest_d/input.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/cmdtest_d/input.py b/test/cmdtest_d/input.py index 9dbd4316..0743fb0e 100755 --- a/test/cmdtest_d/input.py +++ b/test/cmdtest_d/input.py @@ -108,10 +108,13 @@ class CmdTestInput(CmdTestBase): def skip_no_readline_insert(self, extra_msg=None): return self.skip_on_condition( - gc.platform == 'darwin', + gc.platform == 'darwin' or self.cfg.threaded_python, ' (no readline insert support)', extra_msg) + def skip_no_readline_edit(self, extra_msg=None): + return self.skip_on_condition(self.cfg.threaded_python, ' (no readline edit support)', extra_msg) + def get_seed_from_stdin(self): self.spawn(msg_only=True) from subprocess import run, PIPE @@ -361,6 +364,8 @@ class CmdTestInput(CmdTestBase): True) def line_input_edit_term(self): + if self.skip_no_readline_edit(): + return 'skip' return self._line_input( ['prompt> ', True, '', True], '\b\bφυφυ\b\bβαρ', @@ -369,7 +374,7 @@ class CmdTestInput(CmdTestBase): hold_protect_delay) def line_input_edit_term_insert(self): - if self.skip_no_readline_insert(): + if self.skip_no_readline_edit() or self.skip_no_readline_insert(): return 'skip' return self._line_input( ['prompt> ', True, 'φυφυ', True], From b13acfa3b173e3c3cc45a83048100dff2f0ccba3 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Thu, 21 May 2026 12:09:33 +0000 Subject: [PATCH 05/13] wallet.mmgen: increase salt length to 32 bytes --- mmgen/crypto.py | 1 - mmgen/wallet/enc.py | 2 +- mmgen/wallet/incog_base.py | 10 ++++++---- mmgen/wallet/mmgen.py | 1 + test/cmdtest_d/wallet.py | 3 +-- test/ref/98831F3A-27F2BF93[256,1].mmdat | 6 ------ test/ref/98831F3A-E2687906[256,1].mmdat | 6 ------ test/ref/98831F3A-F825E2A0[256,1].mmdat | 6 ++++++ 8 files changed, 15 insertions(+), 20 deletions(-) delete mode 100644 test/ref/98831F3A-27F2BF93[256,1].mmdat delete mode 100644 test/ref/98831F3A-E2687906[256,1].mmdat create mode 100644 test/ref/98831F3A-F825E2A0[256,1].mmdat diff --git a/mmgen/crypto.py b/mmgen/crypto.py index f9fad0fe..5c7e5630 100755 --- a/mmgen/crypto.py +++ b/mmgen/crypto.py @@ -31,7 +31,6 @@ class Crypto: mmenc_ext = 'mmenc' scramble_hash_rounds = 10 - salt_len = 16 aesctr_iv_len = 16 aesctr_dfl_iv = int.to_bytes(1, aesctr_iv_len, 'big') hincog_chk_len = 8 diff --git a/mmgen/wallet/enc.py b/mmgen/wallet/enc.py index 7e2423cd..21fd3d17 100755 --- a/mmgen/wallet/enc.py +++ b/mmgen/wallet/enc.py @@ -94,7 +94,7 @@ class wallet(wallet): d.passwd = self._get_new_passphrase() from hashlib import sha256 - d.salt = sha256(self.crypto.get_random(128)).digest()[:self.crypto.salt_len] + d.salt = sha256(self.crypto.get_random(128)).digest()[:self.salt_len] key = self.crypto.make_key(d.passwd, d.salt, d.hash_preset) d.key_id = make_chksum_8(key) d.enc_seed = self.crypto.encrypt_seed(self.seed.data, key) diff --git a/mmgen/wallet/incog_base.py b/mmgen/wallet/incog_base.py index 0766ce31..b2b43f80 100755 --- a/mmgen/wallet/incog_base.py +++ b/mmgen/wallet/incog_base.py @@ -18,6 +18,8 @@ from .enc import wallet class wallet(wallet): + salt_len = 16 + _msg = { 'check_incog_id': """ Check the generated Incog ID above against your records. If it doesn't @@ -36,7 +38,7 @@ class wallet(wallet): def _get_incog_data_len(self, seed_len): return ( self.crypto.aesctr_iv_len - + self.crypto.salt_len + + self.salt_len + (0 if self.cfg.old_incog_fmt else self.crypto.hincog_chk_len) + seed_len//8) @@ -71,7 +73,7 @@ class wallet(wallet): self.cfg._util.qmsg('Make a record of this value') self.cfg._util.vmsg('\n ' + self.msg['record_incog_id'].strip()+'\n') - d.salt = crypto.get_random(crypto.salt_len) + d.salt = crypto.get_random(self.salt_len) seed_key = crypto.make_key( passwd = d.passwd, salt = d.salt, @@ -167,8 +169,8 @@ class wallet(wallet): iv = d.iv, desc = 'incog data') - d.salt = dd[0:crypto.salt_len] - d.enc_seed = dd[crypto.salt_len:] + d.salt = dd[0:self.salt_len] + d.enc_seed = dd[self.salt_len:] seed_key = crypto.make_key( passwd = d.passwd, diff --git a/mmgen/wallet/mmgen.py b/mmgen/wallet/mmgen.py index 4c43319c..139cf125 100755 --- a/mmgen/wallet/mmgen.py +++ b/mmgen/wallet/mmgen.py @@ -24,6 +24,7 @@ from .enc import wallet class wallet(wallet): desc = 'MMGen wallet' + salt_len = 32 def __init__(self, *args, **kwargs): if self.cfg.label: diff --git a/test/cmdtest_d/wallet.py b/test/cmdtest_d/wallet.py index 42dd3dab..bc45b567 100755 --- a/test/cmdtest_d/wallet.py +++ b/test/cmdtest_d/wallet.py @@ -53,13 +53,12 @@ class CmdTestWalletConv(CmdTestBase, CmdTestShared): 'hic_wallet_old': '1378FC64-B55E9958-D85FF20C[192,1].incog-old.offset123', }, '256': { - 'ref_wallet': '98831F3A-27F2BF93[256,1].mmdat', + 'ref_wallet': '98831F3A-F825E2A0[256,1].mmdat', 'ic_wallet': '98831F3A-5482381C-18460FB1[256,1].mmincog', 'ic_wallet_hex': '98831F3A-1630A9F2-870376A9[256,1].mmincox', 'hic_wallet': '98831F3A-F59B07A0-559CEF19[256,1].incog.offset123', 'hic_wallet_old': '98831F3A-F59B07A0-848535F3[256,1].incog-old.offset123', - }, } cmd_group = ( diff --git a/test/ref/98831F3A-27F2BF93[256,1].mmdat b/test/ref/98831F3A-27F2BF93[256,1].mmdat deleted file mode 100644 index 9874260c..00000000 --- a/test/ref/98831F3A-27F2BF93[256,1].mmdat +++ /dev/null @@ -1,6 +0,0 @@ -cd828f -"#$%&()*+,- ./0123456789:;<=>?@AIZ[\]^_`aiz{|}~' -98831f3a 27f2bf93 256 NE 20150405_075000 -1: 12 8 1 -9440eb NBDH bKqG a23q FtYi nRo1 kk -7a2f32 5nZf LqjP R9bj vgzc sMGr WPtu PR7S 6NpZ pgGd fc4e QCKt diff --git a/test/ref/98831F3A-E2687906[256,1].mmdat b/test/ref/98831F3A-E2687906[256,1].mmdat deleted file mode 100644 index a56c3d71..00000000 --- a/test/ref/98831F3A-E2687906[256,1].mmdat +++ /dev/null @@ -1,6 +0,0 @@ -9cc19b -test.py ref. wallet (pw 'abc', seed len 256) -98831f3a e2687906 256 NE 20161110_135346 -1: 12 8 1 -70413d 74ev zjeq Zw2g DspF RKpE 7H -7c26e6 1otd mVTn 5MCR cDTF sZqY uNKA rsAm mjTw EJmS yzwX ZPJd diff --git a/test/ref/98831F3A-F825E2A0[256,1].mmdat b/test/ref/98831F3A-F825E2A0[256,1].mmdat new file mode 100644 index 00000000..2d7fcb0d --- /dev/null +++ b/test/ref/98831F3A-F825E2A0[256,1].mmdat @@ -0,0 +1,6 @@ +42f7d8 +"#$%&()*+,- ./0123456789:;<=>?@AIZ[\]^_`aiz{|}~' +98831f3a f825e2a0 256 NE 20260520_074802 +1: 12 8 1 +384094 8MgZ HHVn QTmH RbiQ hufm obJw 9FkX 7Uv5 AbtK k4yU hqba +2a4856 8kQ7 JVwg zDQq isws f4g1 7oWi pKS7 gcvq siCX VX3i BeP7 From 3d8e98f31ed71c10fbf87488b4b85dfdae83e6b5 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Thu, 21 May 2026 12:09:33 +0000 Subject: [PATCH 06/13] crypto.Crypto: new `encrypt_aes_ctr()` method --- mmgen/altcoin/util.py | 16 ++++++---------- mmgen/crypto.py | 29 +++++++++++++++-------------- mmgen/proto/eth/util.py | 1 + mmgen/tool/fileutil.py | 9 +++------ test/modtest_d/dep.py | 7 ++----- 5 files changed, 27 insertions(+), 35 deletions(-) diff --git a/mmgen/altcoin/util.py b/mmgen/altcoin/util.py index 89dd185a..d2811be2 100755 --- a/mmgen/altcoin/util.py +++ b/mmgen/altcoin/util.py @@ -14,7 +14,7 @@ altcoin.util: various altcoin-related utilities from ..util import die -def decrypt_keystore(data, passwd, *, mac_algo=None, mac_params={}): +def decrypt_keystore(cfg, data, passwd, *, mac_algo=None, mac_params={}): """ Decrypt the encrypted data in a cross-chain keystore Returns the decrypted data as a bytestring @@ -70,12 +70,8 @@ def decrypt_keystore(data, passwd, *, mac_algo=None, mac_params={}): die(1, 'incorrect password') # Decrypt data: - from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes - from cryptography.hazmat.backends import default_backend - cipher_len = int(cipher.split('-')[1]) // 8 - c = Cipher( - algorithms.AES(hashed_pw[:cipher_len]), - modes.CTR(bytes.fromhex(cdata['cipherparams']['iv'])), - backend = default_backend()) - encryptor = c.encryptor() - return encryptor.update(bytes.fromhex(cdata['ciphertext'])) + encryptor.finalize() + from ..crypto import Crypto + return Crypto(cfg).encrypt_aes_ctr( + hashed_pw[:int(cipher.split('-')[1]) // 8], + bytes.fromhex(cdata['cipherparams']['iv']), + bytes.fromhex(cdata['ciphertext'])) diff --git a/mmgen/crypto.py b/mmgen/crypto.py index 5c7e5630..4caa45cd 100755 --- a/mmgen/crypto.py +++ b/mmgen/crypto.py @@ -68,6 +68,16 @@ class Crypto: self.cfg = cfg self.util = cfg._util + @staticmethod + def get_aes_ctr(key, iv): + from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + from cryptography.hazmat.backends import default_backend + return Cipher(algorithms.AES(key), modes.CTR(iv), backend=default_backend()).encryptor() + + def encrypt_aes_ctr(self, key, iv, data): + encryptor = self.get_aes_ctr(key, iv) + return encryptor.update(data) + encryptor.finalize() + def get_hash_params(self, hash_preset): if hash_preset in self.hash_presets: return self.hash_presets[hash_preset] # N, r, p @@ -126,20 +136,14 @@ class Crypto: verify = True, silent = False): - from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes - from cryptography.hazmat.backends import default_backend if not silent: self.util.vmsg(f'Encrypting {desc}') - c = Cipher(algorithms.AES(key), modes.CTR(iv), backend=default_backend()) - encryptor = c.encryptor() - enc_data = encryptor.update(data) + encryptor.finalize() + + enc_data = self.encrypt_aes_ctr(key, iv, data) if verify: self.util.vmsg_r(f'Performing a test decryption of the {desc}...') - c = Cipher(algorithms.AES(key), modes.CTR(iv), backend=default_backend()) - encryptor = c.encryptor() - dec_data = encryptor.update(enc_data) + encryptor.finalize() - if dec_data != data: + if self.encrypt_aes_ctr(key, iv, enc_data) != data: die(2, f'ERROR.\nDecrypted {desc} doesn’t match original {desc}') if not silent: self.util.vmsg('done') @@ -154,12 +158,9 @@ class Crypto: iv = aesctr_dfl_iv, desc = 'data'): - from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes - from cryptography.hazmat.backends import default_backend self.util.vmsg_r(f'Decrypting {desc} with key...') - c = Cipher(algorithms.AES(key), modes.CTR(iv), backend=default_backend()) - encryptor = c.encryptor() - return encryptor.update(enc_data) + encryptor.finalize() + + return self.encrypt_aes_ctr(key, iv, enc_data) def scrypt_hash_passphrase( self, diff --git a/mmgen/proto/eth/util.py b/mmgen/proto/eth/util.py index e5edec09..3a59928e 100755 --- a/mmgen/proto/eth/util.py +++ b/mmgen/proto/eth/util.py @@ -27,6 +27,7 @@ def decrypt_geth_keystore(cfg, wallet_fn, passwd, *, check_addr=True): from ...altcoin.util import decrypt_keystore key = decrypt_keystore( + cfg, wallet_data, passwd, mac_algo = get_keccak()) diff --git a/mmgen/tool/fileutil.py b/mmgen/tool/fileutil.py index 6330af93..b6fe0283 100755 --- a/mmgen/tool/fileutil.py +++ b/mmgen/tool/fileutil.py @@ -94,15 +94,12 @@ class tool_cmd(tool_cmd_base): """ from threading import Thread from queue import Queue - from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes - from cryptography.hazmat.backends import default_backend from ..util2 import parse_bytespec + from ..crypto import Crypto def encrypt_worker(): - ctr_init_val = os.urandom(Crypto.aesctr_iv_len) - c = Cipher(algorithms.AES(key), modes.CTR(ctr_init_val), backend=default_backend()) - encryptor = c.encryptor() + encryptor = Crypto(self.cfg).get_aes_ctr(key, os.urandom(Crypto.aesctr_iv_len)) while True: q2.put(encryptor.update(q1.get())) q1.task_done() @@ -161,7 +158,7 @@ class tool_cmd(tool_cmd_base): with open(wallet_file) as fh: data = json.loads(fh.read()) from ..altcoin.util import decrypt_keystore - ret = decrypt_keystore(data[0]['keystore'], passwd) + ret = decrypt_keystore(self.cfg, data[0]['keystore'], passwd) return ret.hex() if output_hex else ret def decrypt_geth_keystore(self, wallet_file: str, *, check_addr=True): diff --git a/test/modtest_d/dep.py b/test/modtest_d/dep.py index 411b198a..29afda48 100755 --- a/test/modtest_d/dep.py +++ b/test/modtest_d/dep.py @@ -75,11 +75,8 @@ class unit_tests: return False def cryptography(self, name, ut): - from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes - from cryptography.hazmat.backends import default_backend - c = Cipher(algorithms.AES(b'deadbeef'*4), modes.CTR(b'deadbeef'*2), backend=default_backend()) - encryptor = c.encryptor() - encryptor.update(b'foo') + encryptor.finalize() + from mmgen.crypto import Crypto + Crypto(cfg).encrypt_aes_ctr(b'deadbeef' * 4, b'deadbeef' * 2, b'foo') return True def ecdsa(self, name, ut): From 75ced150ba9ba17081f52242284b07029d97ddb5 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Thu, 21 May 2026 12:09:33 +0000 Subject: [PATCH 07/13] cfg: sort env vars --- mmgen/cfg.py | 64 ++++++++++++++++++++++++++-------------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/mmgen/cfg.py b/mmgen/cfg.py index 67715c39..071d80d7 100755 --- a/mmgen/cfg.py +++ b/mmgen/cfg.py @@ -333,51 +333,51 @@ class Config(Lockable): _env_opts = ( 'MMGEN_DEBUG_ALL', # special: there is no `debug_all` attribute + 'MMGEN_BLACKLIST_DAEMONS', + 'MMGEN_BOGUS_SEND', + 'MMGEN_BOGUS_UNSPENT_DATA', 'MMGEN_COLUMNS', + 'MMGEN_DAEMON_STATE_TIMEOUT', + 'MMGEN_DEBUG', + 'MMGEN_DEBUG_ADDRLIST', + 'MMGEN_DEBUG_DAEMON', + 'MMGEN_DEBUG_EVM', + 'MMGEN_DEBUG_OPTS', + 'MMGEN_DEBUG_RPC', + 'MMGEN_DEBUG_SUBSEED', + 'MMGEN_DEBUG_TW', + 'MMGEN_DEBUG_UTF8', + 'MMGEN_DEVTOOLS', + 'MMGEN_DISABLE_COLOR', + 'MMGEN_ENABLE_ERIGON', + 'MMGEN_EXEC_WRAPPER', + 'MMGEN_FORCE_256_COLOR', + 'MMGEN_HOLD_PROTECT_DISABLE', + 'MMGEN_HTTP_TIMEOUT', + 'MMGEN_IGNORE_DAEMON_VERSION', + 'MMGEN_IGNORE_TEST_PY_EXCEPTION', + 'MMGEN_NO_LICENSE', + 'MMGEN_QUIET', + 'MMGEN_REGTEST', + 'MMGEN_RPC_BACKEND', + 'MMGEN_RPC_FAIL_ON_COMMAND', + 'MMGEN_RPC_HOST', + 'MMGEN_TESTNET', 'MMGEN_TEST_SUITE', 'MMGEN_TEST_SUITE_AUTOSIGN_LED_SIMULATE', 'MMGEN_TEST_SUITE_AUTOSIGN_THREADED', - 'MMGEN_TEST_SUITE_DEVNET_BLOCK_PERIOD', - 'MMGEN_TEST_SUITE_XMR_AUTOSIGN', 'MMGEN_TEST_SUITE_CFGTEST', 'MMGEN_TEST_SUITE_DETERMINISTIC', + 'MMGEN_TEST_SUITE_DEVNET_BLOCK_PERIOD', 'MMGEN_TEST_SUITE_ENABLE_COLOR', 'MMGEN_TEST_SUITE_PEXPECT', 'MMGEN_TEST_SUITE_PEXPECT_TIMEOUT', 'MMGEN_TEST_SUITE_POPEN_SPAWN', 'MMGEN_TEST_SUITE_ROOT_PFX', + 'MMGEN_TEST_SUITE_XMR_AUTOSIGN', 'MMGEN_THREADED_PYTHON', 'MMGEN_TRACEBACK', - 'MMGEN_BLACKLIST_DAEMONS', - 'MMGEN_BOGUS_SEND', - 'MMGEN_BOGUS_UNSPENT_DATA', - 'MMGEN_DAEMON_STATE_TIMEOUT', - 'MMGEN_DEBUG', - 'MMGEN_DEBUG_DAEMON', - 'MMGEN_DEBUG_EVM', - 'MMGEN_DEBUG_OPTS', - 'MMGEN_DEBUG_RPC', - 'MMGEN_DEBUG_ADDRLIST', - 'MMGEN_DEBUG_TW', - 'MMGEN_DEBUG_UTF8', - 'MMGEN_DEBUG_SUBSEED', - 'MMGEN_DEVTOOLS', - 'MMGEN_FORCE_256_COLOR', - 'MMGEN_HOLD_PROTECT_DISABLE', - 'MMGEN_HTTP_TIMEOUT', - 'MMGEN_QUIET', - 'MMGEN_NO_LICENSE', - 'MMGEN_RPC_HOST', - 'MMGEN_RPC_FAIL_ON_COMMAND', - 'MMGEN_TESTNET', - 'MMGEN_REGTEST', - 'MMGEN_EXEC_WRAPPER', - 'MMGEN_IGNORE_TEST_PY_EXCEPTION', - 'MMGEN_RPC_BACKEND', - 'MMGEN_IGNORE_DAEMON_VERSION', - 'MMGEN_USE_STANDALONE_SCRYPT_MODULE', - 'MMGEN_ENABLE_ERIGON', - 'MMGEN_DISABLE_COLOR') + 'MMGEN_USE_STANDALONE_SCRYPT_MODULE') _infile_opts = ( 'keys_from_file', From c4feeccb6670316990dd2fbaec1c6bc7826be942 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Thu, 21 May 2026 12:09:33 +0000 Subject: [PATCH 08/13] enable use of `pyaes` package for testing --- mmgen/autosign/__init__.py | 5 ++++- mmgen/cfg.py | 2 ++ mmgen/crypto.py | 13 +++++++++++++ nix/user-packages.nix | 1 + test/modtest_d/dep.py | 2 +- 5 files changed, 21 insertions(+), 2 deletions(-) diff --git a/mmgen/autosign/__init__.py b/mmgen/autosign/__init__.py index 1cd5c565..6adca942 100755 --- a/mmgen/autosign/__init__.py +++ b/mmgen/autosign/__init__.py @@ -401,7 +401,10 @@ class Autosign: cfg = self.cfg, prompt = f'Default wallet ‘{wf}’ found.\nUse default wallet for autosigning?', default_yes = True): - ss_in = Wallet(Config(), fn=wf) + ss_in = Wallet(Config({ + 'test_suite': self.cfg.test_suite, + 'aes_backend': self.cfg.aes_backend + }), fn=wf) else: ss_in = get_mn_wallet() diff --git a/mmgen/cfg.py b/mmgen/cfg.py index 071d80d7..f47fd01c 100755 --- a/mmgen/cfg.py +++ b/mmgen/cfg.py @@ -229,6 +229,7 @@ class Config(Lockable): autochg_ignore_labels = False autosign = False threaded_python = not sys._is_gil_enabled() + aes_backend = 'cryptography' # regtest: bob = False @@ -333,6 +334,7 @@ class Config(Lockable): _env_opts = ( 'MMGEN_DEBUG_ALL', # special: there is no `debug_all` attribute + 'MMGEN_AES_BACKEND', 'MMGEN_BLACKLIST_DAEMONS', 'MMGEN_BOGUS_SEND', 'MMGEN_BOGUS_UNSPENT_DATA', diff --git a/mmgen/crypto.py b/mmgen/crypto.py index 4caa45cd..b148cb3c 100755 --- a/mmgen/crypto.py +++ b/mmgen/crypto.py @@ -67,6 +67,9 @@ class Crypto: def __init__(self, cfg): self.cfg = cfg self.util = cfg._util + if cfg.test_suite and self.cfg.aes_backend == 'pyaes': + self.get_aes_ctr = self.get_aes_ctr_pyaes + self.encrypt_aes_ctr = self.encrypt_aes_ctr_pyaes @staticmethod def get_aes_ctr(key, iv): @@ -78,6 +81,16 @@ class Crypto: encryptor = self.get_aes_ctr(key, iv) return encryptor.update(data) + encryptor.finalize() + @staticmethod + def get_aes_ctr_pyaes(key, iv): + import pyaes + class MyAES(pyaes.AESModeOfOperationCTR): + update = pyaes.AESModeOfOperationCTR.encrypt + return MyAES(key, pyaes.Counter(int.from_bytes(iv))) + + def encrypt_aes_ctr_pyaes(self, key, iv, data): + return self.get_aes_ctr_pyaes(key, iv).encrypt(data) + def get_hash_params(self, hash_preset): if hash_preset in self.hash_presets: return self.hash_presets[hash_preset] # N, r, p diff --git a/nix/user-packages.nix b/nix/user-packages.nix index 3c6e2c8b..cf61e739 100644 --- a/nix/user-packages.nix +++ b/nix/user-packages.nix @@ -54,5 +54,6 @@ rec { # pydantic = pydantic; # eth-keys # pure-protobuf = pure-protobuf; # THORChain # bip-utils = bip-utils; # bip_hd + # pyaes = pyaes; # developer testing }; } diff --git a/test/modtest_d/dep.py b/test/modtest_d/dep.py index 29afda48..b52c2c53 100755 --- a/test/modtest_d/dep.py +++ b/test/modtest_d/dep.py @@ -74,7 +74,7 @@ class unit_tests: msg('Is the ‘pysocks’ package installed?') return False - def cryptography(self, name, ut): + def aes(self, name, ut): from mmgen.crypto import Crypto Crypto(cfg).encrypt_aes_ctr(b'deadbeef' * 4, b'deadbeef' * 2, b'foo') return True From 77a42a1df95650d5ce31b99950f085f4105873da Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Thu, 21 May 2026 12:09:33 +0000 Subject: [PATCH 09/13] tool rand2file: use `os.process_cpu_count()` to set thread count --- mmgen/tool/fileutil.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/mmgen/tool/fileutil.py b/mmgen/tool/fileutil.py index b6fe0283..dcd62a3c 100755 --- a/mmgen/tool/fileutil.py +++ b/mmgen/tool/fileutil.py @@ -70,7 +70,7 @@ class tool_cmd(tool_cmd_base): os.close(f) return True - def rand2file(self, outfile: str, nbytes: str, *, threads=4, silent=False): + def rand2file(self, outfile: str, nbytes: str, *, silent=False): """ write ‘nbytes’ bytes of random data to specified file (dd-style byte specifiers supported) @@ -118,7 +118,12 @@ class tool_cmd(tool_cmd_base): key = Crypto(self.cfg).get_random(32) q1, q2 = (Queue(), Queue()) - for i in range(max(1, threads-2)): + try: + threads = os.process_cpu_count() # Python 3.13 + except AttributeError: + threads = os.cpu_count() + + for i in range(max(1, threads - 1)): t = Thread(target=encrypt_worker) t.daemon = True t.start() From b29b8b4b9cfe744e48b479c3a50b8717a3d195d0 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Thu, 21 May 2026 12:09:33 +0000 Subject: [PATCH 10/13] cmdtest.py: new `--dev-mode` option --- test/cmdtest.py | 2 ++ test/cmdtest_d/include/pexpect.py | 3 ++- test/cmdtest_d/include/runner.py | 9 +++++++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/test/cmdtest.py b/test/cmdtest.py index 92574ca9..c8dfd3d5 100755 --- a/test/cmdtest.py +++ b/test/cmdtest.py @@ -122,6 +122,8 @@ opts_data = { -x, --debug-pexpect Produce debugging output for pexpect calls --, --demo Add extra delay after each send to make input visible. Implies --exact-output --pexpect-spawn --buf-keypress +--, --dev-mode Run spawned scripts in Python Development Mode + (PYTHONDEVMODE=1 PYTHONTRACEMALLOC=10) -d, --deps-only Run a command or command subgroup’s dependencies without running the command or command group itself. -D, --no-daemon-stop Don't stop auto-started daemons after running tests diff --git a/test/cmdtest_d/include/pexpect.py b/test/cmdtest_d/include/pexpect.py index 32c08aec..ea9ba5a2 100755 --- a/test/cmdtest_d/include/pexpect.py +++ b/test/cmdtest_d/include/pexpect.py @@ -67,7 +67,8 @@ class CmdTestPexpect: timeout = int( timeout or cfg.pexpect_timeout - or cfg.test_suite_pexpect_timeout) or (60, 5)[bool(cfg.debug_pexpect)] + or cfg.test_suite_pexpect_timeout) or ( + 5 if cfg.debug_pexpect else 180 if cfg.dev_mode else 60) if pexpect_spawn: self.p = pexpect.spawn(args[0], args[1:], encoding='utf8', timeout=timeout, env=spawn_env) else: diff --git a/test/cmdtest_d/include/runner.py b/test/cmdtest_d/include/runner.py index 86a69068..06d6ffae 100755 --- a/test/cmdtest_d/include/runner.py +++ b/test/cmdtest_d/include/runner.py @@ -105,14 +105,19 @@ class CmdTestRunner: def set_spawn_env(self): self.spawn_env = dict(os.environ) + self.spawn_env.update({ 'MMGEN_NO_LICENSE': '1', 'MMGEN_BOGUS_SEND': '1', 'MMGEN_TEST_SUITE_PEXPECT': '1', 'EXEC_WRAPPER_DO_RUNTIME_MSG':'1', # if cmdtest.py itself is running under exec_wrapper, disable writing of traceback file for spawned script - 'EXEC_WRAPPER_TRACEBACK': '' if os.getenv('MMGEN_EXEC_WRAPPER') else '1', - }) + 'EXEC_WRAPPER_TRACEBACK': '' if os.getenv('MMGEN_EXEC_WRAPPER') else '1'}) + + if self.cfg.dev_mode: + self.spawn_env.update({ + 'PYTHONDEVMODE': '1', + 'PYTHONTRACEMALLOC': '10'}) if self.cfg.exact_output: from mmgen.term import get_terminal_size From 8488755790fd5ed1b29d65ce581a4a8a5db10c3f Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Thu, 21 May 2026 12:09:33 +0000 Subject: [PATCH 11/13] CoinAmt: Python 3.15 fix --- mmgen/amt.py | 2 +- mmgen/data/version | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mmgen/amt.py b/mmgen/amt.py index 3796fe39..b4b9899d 100755 --- a/mmgen/amt.py +++ b/mmgen/amt.py @@ -138,7 +138,7 @@ class CoinAmt(Decimal, Hilite, InitErrors): # abstract class def __sub__(self, other, *args, **kwargs): if type(other) is type(self): - return type(self)(Decimal.__sub__(self, other, *args, **kwargs), from_decimal=True) + return type(self)(Decimal.__sub__(Decimal(self), Decimal(other), *args, **kwargs), from_decimal=True) raise TypeError( f'operand {other} is of incorrect type ({type(other).__name__} != {type(self).__name__})') diff --git a/mmgen/data/version b/mmgen/data/version index e0228cf1..daf7db89 100644 --- a/mmgen/data/version +++ b/mmgen/data/version @@ -1 +1 @@ -16.1.0 +16.2.dev0 From d63dd743f512a40eb22b90f6155b588ae440095e Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Sat, 23 May 2026 13:32:31 +0000 Subject: [PATCH 12/13] update README, documentation (minor) --- README.md | 2 +- doc/wiki/Altcoin-and-Forkcoin-Support.md | 5 +++-- ...actice.md => XOR-Seed-Splitting-^-Theory-and-Practice.md} | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) rename doc/wiki/{XOR-Seed-Splitting-Theory-and-Practice.md => XOR-Seed-Splitting-^-Theory-and-Practice.md} (98%) diff --git a/README.md b/README.md index fbc93040..8e90bfaa 100644 --- a/README.md +++ b/README.md @@ -201,7 +201,7 @@ Donate: [R]: ../../wiki/Getting-Started-with-MMGen-Wallet#a_rbf [B]: ../../wiki/command-help-txbump [69]: https://github.com/bitcoin/bips/blob/master/bip-0069.mediawiki -[O]: ../../wiki/XOR-Seed-Splitting:-Theory-and-Practice +[O]: ../../wiki/XOR-Seed-Splitting-^-Theory-and-Practice [ms]: ../../wiki/command-help-seedsplit [ta]: ../../wiki/Tool-API [L]: ../../wiki/command-help-tool diff --git a/doc/wiki/Altcoin-and-Forkcoin-Support.md b/doc/wiki/Altcoin-and-Forkcoin-Support.md index c9bd7dc4..f9d9eb85 100644 --- a/doc/wiki/Altcoin-and-Forkcoin-Support.md +++ b/doc/wiki/Altcoin-and-Forkcoin-Support.md @@ -244,8 +244,9 @@ commands. It’s that simple! Monero is fully supported by MMGen Wallet. -Make sure that [Monerod][M] is installed and running and that -`monero-wallet-rpc` is located in your executable path. +Make sure that [Monerod][M] is installed and running on your online machine and +that `monero-wallet-rpc` is installed to your executable path on both online and +offline machines. Install the Python XMR requirements: diff --git a/doc/wiki/XOR-Seed-Splitting-Theory-and-Practice.md b/doc/wiki/XOR-Seed-Splitting-^-Theory-and-Practice.md similarity index 98% rename from doc/wiki/XOR-Seed-Splitting-Theory-and-Practice.md rename to doc/wiki/XOR-Seed-Splitting-^-Theory-and-Practice.md index 5a2e0402..ac3d6b7d 100644 --- a/doc/wiki/XOR-Seed-Splitting-Theory-and-Practice.md +++ b/doc/wiki/XOR-Seed-Splitting-^-Theory-and-Practice.md @@ -8,8 +8,9 @@ ### XOR Seed Splitting: A Theoretical Introduction -The bitwise exclusive-or operation (usually denoted as `XOR`, or “![⊕]”) -has interesting properties that make it very useful in cryptography. +The bitwise exclusive-or operation (usually denoted as `XOR`, “![⊕]”, or “^” in +programming languages), has interesting properties that make it very useful in +cryptography. Suppose we have two bytes, *a* and *b*: From 191e8ec26b2c12342ba95c2b1401d5590b729733 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Sat, 23 May 2026 13:32:37 +0000 Subject: [PATCH 13/13] update PGP signing keys --- SIGNING_KEYS.pub | 41 ++++++++++++++++++++++++----------------- mmgen/data/version | 2 +- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/SIGNING_KEYS.pub b/SIGNING_KEYS.pub index cdbd7ec2..6ff4a1da 100644 --- a/SIGNING_KEYS.pub +++ b/SIGNING_KEYS.pub @@ -52,7 +52,6 @@ dRrWK1eeHDwJ7AnLSfwxBGc= -----END PGP PUBLIC KEY BLOCK----- -----BEGIN PGP PUBLIC KEY BLOCK----- -Version: GnuPG v1 mQSuBFqMFuERDACE4JN6sXMel/fc2YB7F1N4h6utfdcTBHKtx97eTfq0yH1laFa/ IpkBrDZ5GOdxTDdW3NVB4q/Wx8fR0BVUktv3cXF/d8b/B/z4OSSWbRYgNw6+C9Ol @@ -79,20 +78,28 @@ m+8wy4zDYefUQuF8ydgNcebBRWnTU6Tb3ujVMW22WIGRK1cbo8BfnlbFvSd1ka2T FKjiUgGypA7aLQKDt4s0QQ6fNnTn/BGcJj0tFwoZ28CRdruCG9cbdDVMKQQOERr+ msnL7OOAKrwpLNZ3+g20lFbp8dTQZpJEiH1bog27M6U231/OUavo/6HyAH+rs5Lk MMdeJ3BYd6UszbJoAUWTcHgxN3HiEe+UmU8J1Spl6xQjCICVAKg87SOjjAQwTiWc -77QnTU1HZW4gKERTQSBzaWduaW5nIGtleSkgPG1tZ2VuQHR1dGEuaW8+iHoEExEI -ACIFAlqMFuECGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJED+LGGHjK32i -VP4A/0bL7rJTGuyGL5YSMJRsRx8iqvWEpuLo20/CW68C69OmAP9rc0OAcTsLbT+x -4wT+3d58sBuCn9fLl1inoHo5XFZSEIkCHAQQAQIABgUCWowXfAAKCRBi2+nlIS8F -vlfmD/4hl3XSidJjeWNVKs+qjtOSm3MOCLqqK9PukzX1B98eyWgiBd2UsHv0yWAI -u4ldppIgpJY/2NyntWTNZojmoNftc/nRnz3DIOzmlm+9G61TfsIxvEuQn6Vxs9rD -LzF/8X6ChaaVQI41d4C4wFBoKdxt6mmm7mYgWcY/IzIAnjYWgC9sBwZxuvXYorfk -8xFU0zB/fTQQUjkidpXX48Zyox+2l/0KN24jbpoNAQcqRSkdgjL9+5BuYryp3Pvc -oJHJYA4VI9LscYXhyiUZQQIOx272JIo6Ycg1Lb7YXv8SU9p9r5anxJfW7mZNULGi -KUPR+WqpFkG4kjSh6JbCd4KIw60vSCmQiXOi+9tDFJHBtZiUweSK/qzazieLTIDu -sqv2BX4WVOtTPuBFuFYzXUkEbaB916QlFgE9IGWdxDRgGHEVe6hAS33mRAPaMGJ7 -SN4A9R18YCEzfUC/ZSu3iZIL6Y/hvGI3aSENwsfyJ5ipk/1JvhWLrP7tLVRmqVzB -vP8uJpjTKJTn+xs0r7yv3VveHo44fPOoDGiC/P5MzL2oni3MRPv88/zTCjlebTne -id850rPAZGY4J325pPE9grvuF5A/tbPm01CUWwRVzaBYSdFO2haRJjyeNW5s305L -s8+pI8KYEcBY9jsjMKSRU+7O7N6ANl079US9wRf6shjTklbB6A== -=x3WZ +77Q6VGhlIE1NR2VuIFByb2plY3QgKHJlcG9zaXRvcnkgc2lnbmluZyBrZXkpIDxt +bWdlbkB0dXRhLmlvPoiQBBMRCAA4FiEEXITLRa7iJQ8xpqVwP4sYYeMrfaIFAl2t +5dsCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQP4sYYeMrfaL3sAD9HvMy +95DddVooCJud2r02urQ6O63239DkEn9Uqdv3fu8A/iwOzmOg7q21yUceJY8+xqgZ +pYtctenGqYZKtBB/y/YNtCdNTUdlbiAoRFNBIHNpZ25pbmcga2V5KSA8bW1nZW5A +dHV0YS5pbz6IegQTEQgAIgUCWowW4QIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgEC +F4AACgkQP4sYYeMrfaJU/gD/RsvuslMa7IYvlhIwlGxHHyKq9YSm4ujbT8JbrwLr +06YA/2tzQ4BxOwttP7HjBP7d3nywG4Kf18uXWKegejlcVlIQiQIcBBABAgAGBQJa +jBd8AAoJEGLb6eUhLwW+V+YP/iGXddKJ0mN5Y1Uqz6qO05Kbcw4Iuqor0+6TNfUH +3x7JaCIF3ZSwe/TJYAi7iV2mkiCklj/Y3Ke1ZM1miOag1+1z+dGfPcMg7OaWb70b +rVN+wjG8S5CfpXGz2sMvMX/xfoKFppVAjjV3gLjAUGgp3G3qaabuZiBZxj8jMgCe +NhaAL2wHBnG69diit+TzEVTTMH99NBBSOSJ2ldfjxnKjH7aX/Qo3biNumg0BBypF +KR2CMv37kG5ivKnc+9ygkclgDhUj0uxxheHKJRlBAg7HbvYkijphyDUtvthe/xJT +2n2vlqfEl9buZk1QsaIpQ9H5aqkWQbiSNKHolsJ3gojDrS9IKZCJc6L720MUkcG1 +mJTB5Ir+rNrOJ4tMgO6yq/YFfhZU61M+4EW4VjNdSQRtoH3XpCUWAT0gZZ3ENGAY +cRV7qEBLfeZEA9owYntI3gD1HXxgITN9QL9lK7eJkgvpj+G8YjdpIQ3Cx/InmKmT +/Um+FYus/u0tVGapXMG8/y4mmNMolOf7GzSvvK/dW94ejjh886gMaIL8/kzMvaie +LcxE+/zz/NMKOV5tOd6J3znSs8BkZjgnfbmk8T2Cu+4XkD+1s+bTUJRbBFXNoFhJ +0U7aFpEmPJ41bmzfTkuzz6kjwpgRwFj2OyMwpJFT7s7s3oA2XTv1RL3BF/qyGNOS +VsHouDgEahCWthIKKwYBBAGXVQEFAQEHQNYXr/lI34GouI0536OljciggBQlgs0+ +IavFkpP6zVFkAwEIB4h4BBgRCAAgFiEEXITLRa7iJQ8xpqVwP4sYYeMrfaIFAmoQ +lrYCGwwACgkQP4sYYeMrfaJB6AEAncaaUqjyqL0acGFoKdFTS+gi8jt5KvCEHtVR +hWKP3XsBAKT/fsAIIbXDvu1+g3/HIRlfbZaMFQs45FNtzLOVu7vy +=7u5V -----END PGP PUBLIC KEY BLOCK----- diff --git a/mmgen/data/version b/mmgen/data/version index daf7db89..6dbf8228 100644 --- a/mmgen/data/version +++ b/mmgen/data/version @@ -1 +1 @@ -16.2.dev0 +16.2.dev1