transaction versioning

Transaction versioning provides a mechanism to require the user to keep
online and offline installations in sync.
This commit is contained in:
The MMGen Project 2026-09-09 18:33:59 +00:00
commit 7e2aa8088b
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
4 changed files with 34 additions and 6 deletions

View file

@ -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'

View file

@ -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
} | {

View file

@ -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'])

View file

@ -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}'