From 7e2aa8088be83ee671478cfc31dc28c3b15d5ee6 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Wed, 9 Sep 2026 18:33:59 +0000 Subject: [PATCH] transaction versioning Transaction versioning provides a mechanism to require the user to keep online and offline installations in sync. --- mmgen/tx/completed.py | 15 +++++++++++++++ mmgen/tx/file.py | 8 ++++++-- test/cmdtest_d/autosign.py | 9 ++++++--- test/modtest_d/tx.py | 8 +++++++- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/mmgen/tx/completed.py b/mmgen/tx/completed.py index 0c6a1127..38949260 100755 --- a/mmgen/tx/completed.py +++ b/mmgen/tx/completed.py @@ -21,6 +21,7 @@ class Completed(Base): signed or unsigned transaction with associated file """ filename_api = True + txfile_version = 4 def __init__(self, cfg, *args, filename=None, data=None, quiet_open=False, **kwargs): @@ -116,6 +117,20 @@ class Completed(Base): fs = f'Request to {{}} legacy-format transaction ({desc}). {{}}' self.die_on_version_error(fs.format(a, b)) + def txfile_version_chk(self, f_ver, s_ver): + if 'sign' in gc.prog_name: + a = 'sign' + b = 'Is your {} installation out of date?'.format('offline' if f_ver > s_ver else 'online') + elif 'send' in gc.prog_name: + a = 'send' + b = 'Is your {} installation out of date?'.format('offline' if f_ver < s_ver else 'online') + else: + return + fs = ( + f'Request to {{}} transaction with version {f_ver}, which is {{}} ' + f'than version {s_ver} current with installed software. {{}}') + self.die_on_version_error(fs.format(a, ('greater' if f_ver > s_ver else 'less'), b)) + class DummyCompleted: # required by MMGenTxFile.get_proto() desc = 'dummy transaction' diff --git a/mmgen/tx/file.py b/mmgen/tx/file.py index 8e8421c1..d4722e1d 100755 --- a/mmgen/tx/file.py +++ b/mmgen/tx/file.py @@ -94,7 +94,7 @@ class MMGenTxFile(MMGenObject): tx = self.tx tx.file_format = 'json' outer_data = json.loads(data) - if 'MoneroMMGenTX' in outer_data: + if 'MoneroMMGenTX' in outer_data: # txfile_version_chk for Monero TBD tx.proto = get_monero_proto(tx, outer_data) return None data = outer_data[self.data_label] @@ -107,6 +107,9 @@ class MMGenTxFile(MMGenObject): if get_proto_only: return + if data.get('version', 0) != tx.txfile_version: + tx.txfile_version_chk(data.get('version', 0), tx.txfile_version) + for k, v in self.attrs.items(): if v != 'skip': setattr(tx, k, v(data[k]) if v else data[k]) @@ -278,7 +281,8 @@ class MMGenTxFile(MMGenObject): def format_data_json(): data = txfile_json_dumps({ - 'coin_id': coin_id + 'coin_id': coin_id, + 'version': tx.txfile_version } | { k: getattr(tx, k) for k in self.attrs } | { diff --git a/test/cmdtest_d/autosign.py b/test/cmdtest_d/autosign.py index 990c5ded..c1a0b2bd 100755 --- a/test/cmdtest_d/autosign.py +++ b/test/cmdtest_d/autosign.py @@ -909,7 +909,7 @@ class CmdTestAutosign(CmdTestAutosignBase): self.remove_device() return 'ok' - def do_sign(self, args=[], have_msg=False, exc_exit_val=None, expect_str=None): + def do_sign(self, args=[], have_msg=False, exc_exit_val=None, expect_str=None, bad_version=False): tx_desc = Signable.transaction.desc self.insert_device() @@ -930,6 +930,9 @@ class CmdTestAutosign(CmdTestAutosignBase): if exc_exit_val: return do_return() + if bad_version: + t.expect('out of date') + t.expect('legacy-format') t.expect( @@ -953,10 +956,10 @@ class CmdTestAutosign(CmdTestAutosignBase): return do_return() def sign_quiet(self): - return self.do_sign(['--quiet']) + return self.do_sign(['--quiet'], bad_version=True) def sign_full_summary(self): - return self.do_sign(['--full-summary']) + return self.do_sign(['--full-summary'], bad_version=True) def sign_led(self): return self.do_sign(['--quiet', '--led']) diff --git a/test/modtest_d/tx.py b/test/modtest_d/tx.py index 6766a0ed..a5398907 100755 --- a/test/modtest_d/tx.py +++ b/test/modtest_d/tx.py @@ -45,7 +45,13 @@ async def do_txfile_test(desc, fns, cfg=cfg, do_format=True): ref_text = txfile_json_dumps(ref_data) # process newly-formatted data: - new_text = f.format() + if 'version' in ref_data: + new_text = f.format() + else: + new_data = json.loads(f.format()) + del new_data['MMGenTransaction']['version'] + new_data['chksum'] = make_chksum_6(txfile_json_dumps(new_data['MMGenTransaction'])) + new_text = txfile_json_dumps(new_data) assert new_text == ref_text, f'\nformatted text:\n{new_text}\n !=\noriginal file:\n{ref_text}'