usawa

Unnamed repository; edit this file 'description' to name the repository.
Info | Log | Files | Refs | Submodules | LICENSE

commit 649252027133e93298cff57e648442a0f2d27cc9
parent 9b9ac428ee2a7aab190c68c0e597bcd6b3e307d9
Author: lash <dev@holbrook.no>
Date:   Mon,  5 Jan 2026 15:57:01 +0100

Add ledger restoration from store

Diffstat:
Mdummy/tests/store.py | 32++++++++++++++++++++++++++++----
Mdummy/usawa/entry.py | 16+++++++++++-----
Mdummy/usawa/ledger.py | 82++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Mdummy/usawa/store.py | 30+++++++++++++++++++++++++++++-
4 files changed, 135 insertions(+), 25 deletions(-)

diff --git a/dummy/tests/store.py b/dummy/tests/store.py @@ -3,12 +3,14 @@ import datetime import unittest import os import copy +import uuid import lxml.etree from whee.mem import MemStore from usawa import Ledger, UnitIndex, EntryPart, Entry, DemoWallet from usawa.store import LedgerStore +from usawa.crypto import ACL logging.basicConfig(level=logging.DEBUG) logg = logging.getLogger() @@ -27,8 +29,8 @@ class TestStore(unittest.TestCase): def test_store_entry(self): - uidx = UnitIndex('FOO') - ledger = Ledger(uidx) + uidx = UnitIndex('USD') + ledger = Ledger(uidx, serial=42, base=self.parent) store = LedgerStore(self.store, ledger) dst = EntryPart('asset', 'foo', 1337) src = EntryPart('income', 'foo', 1337, src=True) @@ -42,10 +44,32 @@ class TestStore(unittest.TestCase): def test_store_ledger(self): - uidx = UnitIndex('FOO') - ledger = Ledger(uidx) + uidx = UnitIndex('USD') + wallet = DemoWallet() + acl = ACL() + acl.add(wallet.pubkey()) + ledger = Ledger(uidx, serial=42, base=self.parent) store = LedgerStore(self.store, ledger) + dst = EntryPart('asset', 'foo', 1337) + src = EntryPart('income', 'foo', 1337, src=True) + o = Entry(src, dst, 'USD', 42, datetime.datetime.strptime('2025-11-11', '%Y-%m-%d'), parent=self.parent, ref=self.ref, description=self.description, tx_datereg=self.dtreg) + o.sign(wallet) + store.add_entry(o) + + ref = str(uuid.uuid4()) + parent = o.sum()[0] + description = 'barbarbar' + dtreg = datetime.datetime.now() + dst = EntryPart('expense', 'bar', 4200) + src = EntryPart('liability', 'bar', 4200, src=True) + o = Entry(src, dst, 'USD', 43, datetime.datetime.strptime('2025-11-12', '%Y-%m-%d'), parent=parent, ref=ref, description=description, tx_datereg=dtreg) + o.sign(wallet) + store.add_entry(o) + + ledger = Ledger(uidx, serial=42, base=self.parent, acl=acl, topic=ledger.topic) + store.load() + if __name__ == '__main__': unittest.main() diff --git a/dummy/usawa/entry.py b/dummy/usawa/entry.py @@ -240,17 +240,17 @@ class Entry: @staticmethod def deserialize(data): v = rencode.loads(data) - parent = v[0] + parent = v[0].hex() serial = v[1] ref = v[2].decode('utf-8') date_reg = datetime.datetime.strptime(v[3].decode('utf-8'), '%Y%m%d%H%M%S') date = datetime.datetime.strptime(v[4].decode('utf-8'), '%Y%m%d') - unit = v[5] + unit = v[5].decode('utf-8') description = v[6].decode('utf-8') src_data = v[7] dst_data = v[8] - src = EntryPart(src_data[0], src_data[1], src_data[2], src=True) - dst = EntryPart(dst_data[0], dst_data[1], dst_data[2]) + src = EntryPart(src_data[0].decode('utf-8'), src_data[1].decode('utf-8'), src_data[2], src=True) + dst = EntryPart(dst_data[0].decode('utf-8'), dst_data[1].decode('utf-8'), dst_data[2]) return Entry(src, dst, unit, serial, date, ref=ref, description=description, parent=parent, tx_datereg=date_reg) @@ -352,11 +352,14 @@ class Entry: (z, b) = entry.sum() if not wallet.verify(z, sig): raise VerifyError() + # TODO: demo only takes into account single signature + entry.add_signature(pubkey_bytes, sig) return entry """Generate and return an XML representation of the entry. + :todo: Make sure that sigs publickey lookup key is bytes type :returns: XML tree representing the entry. :rtype: lxml.etree.ElementTree """ @@ -400,7 +403,10 @@ class Entry: tree.append(data) for k in self.sigs.keys(): - o = etree.Element('sig', type='ed25519', keyid=k) + v = k + if isinstance(v, bytes): + v = k.hex() + o = etree.Element('sig', type='ed25519', keyid=v) o.text = self.sigs[k].hex() tree.append(o) diff --git a/dummy/usawa/ledger.py b/dummy/usawa/ledger.py @@ -127,6 +127,9 @@ class RunningTotal: class Ledger: + + default_src = 'defalsify.org' + """Ledger represents a signed and verified chain of transaction entries. Apart from the entries chain, it also holds metadata required to expand underlying assets referenced by the entries, aswell as resolution and verification of identities of signatories. @@ -150,26 +153,28 @@ class Ledger: :todo: Add warnings for ignored parameters """ - def __init__(self, unitindex, tree=None, acl=None, serial=0, base=None, topic=None): + def __init__(self, unitindex, tree=None, acl=None, serial=0, base=DEFAULTPARENT, topic=None): self.uidx = unitindex self.sigs = {} self.entries = {} self.running = {} self.tree = tree - if self.tree == None: - self.reset() - self.serial = serial - if topic == None: - topic = os.urandom(64) + self.base = base + self.base_serial = serial + self.src = None + self.topic = topic + self.acl = acl + if self.topic == None: + self.topic = os.urandom(64) if base == None: h = hashlib.sha512() h.update(topic) h.update(DEFAULTPARENT) base = h.digest() - self.base = base - self.topic = topic + if self.tree == None: + self.reset() + self.serial = self.base_serial self.cur = base - self.acl = acl logg.debug('ledger base {} from topic {}'.format(self.base.hex(), self.topic.hex())) @@ -193,24 +198,38 @@ class Ledger: """Remove all entries from the ledger, and reset all metadata to defaults. + If either src or topic is not defined, the existing src or topic value on the existing ledger will be preserved. If none is set, they will be set to default values. + :param src: URI to the source of ledger information. :type src: str + :param topic: Topic to set for new ledger. + :type topic: bytes :rtype: None """ - def reset(self, src='defalsify.org', topic=None): + def reset(self, src=None, topic=None): + self.serial = self.base_serial + self.cur = self.base self.entries[self.uidx.base] = [] self.running[self.uidx.base] = RunningTotal(self.uidx.base, self.uidx) self.tree = lxml.etree.XML('<ledger xmlns="http://usawa.defalsify.org/" version="{}"></ledger>'.format(XML_FORMAT_VERSION)) #self.tree = lxml.etree.Element('ledger', nsmap=nsmap()) o = lxml.etree.SubElement(self.tree, NSPREFIX + 'topic', nsmap=nsmap()) if topic == None: - topic = os.urandom(64) - topic = topic.hex() + if self.topic == None: + topic = os.urandom(64) + topic = topic.hex() + else: + topic = self.topic.hex() o.text = topic o = lxml.etree.SubElement(self.tree, NSPREFIX + 'retrieved', nsmap=nsmap()) o.text = datetime.datetime.strftime(datetime.datetime.now(), '%Y-%m-%dT%H:%M:%SZ') #self.tree.append(o) o = lxml.etree.SubElement(self.tree, NSPREFIX + 'src', nsmap=nsmap()) + if src == None: + if self.src != None: + src = self.src + else: + src = self.default_src o.text = src units = lxml.etree.SubElement(self.tree, NSPREFIX + 'units', nsmap=nsmap()) @@ -228,7 +247,7 @@ class Ledger: #self.tree.append(units) incoming = lxml.etree.SubElement(self.tree, NSPREFIX + 'incoming', nsmap=nsmap()) - incoming.attrib['serial'] = '0' + incoming.attrib['serial'] = str(self.serial) real = lxml.etree.SubElement(incoming, NSPREFIX + 'real', nsmap=nsmap()) real.attrib['unit'] = self.uidx.base @@ -242,7 +261,7 @@ class Ledger: o = lxml.etree.SubElement(incoming, NSPREFIX + 'digest', nsmap=nsmap()) o.attrib['algo'] = 'sha512' - o.text = DEFAULTPARENT.hex() + o.text = self.base.hex() #incoming.append(o) #self.tree.append(incoming) @@ -299,15 +318,21 @@ class Ledger: have = False valid_keys = None if self.acl == None: + logg.debug('no acl in ledger') valid_keys = list(entry.sigs.keys()) else: valid_keys = list(self.acl.pubkeys(binary=False)) logg.debug('testing valid keys {}'.format(valid_keys)) for k in valid_keys: - b = bytes.fromhex(k) + b = None + try: + b = bytes.fromhex(k) + except: + b = k try: sig = entry.sigs[k] except KeyError: + logg.debug('no signature from {}'.format(k)) continue wallet = DemoWallet(publickey=b) v = entry.sum() @@ -460,5 +485,32 @@ class Ledger: return self.cur + """Generate the serialization format used to calculate the digest for the entry. + + :returns: String representation of the entry, in rencode format. + :rtype: str + """ + def serialize(self, ledger): + d = [ + self.topic, + ] + logg.debug('serialize ledger {}'.format(d)) + return rencode.dumps(d) + + + """Create a ledger object from serialized data. + + :param data: rencoded ledger object, as produced by the serialize() method. + :type data: str + :returns: Ledger object. + :rtype: usawa.Ledger + """ + @staticmethod + def deserialize(self, unitindex, serial=None, base=None, acl=None, src=None): + v = rencode.loads(data) + o = Ledger(base=base, serial=serial, acl=acl, src=src, topic=v[0]) + return o + + def __str__(self): return "state: " + self.base.hex() diff --git a/dummy/usawa/store.py b/dummy/usawa/store.py @@ -1,5 +1,6 @@ import enum import os +import logging from whee import Interface @@ -11,6 +12,8 @@ PFX_LEDGER = b'\x01' PFX_LEDGER_LOCK = b'\x02' PFX_ENTRY = b'\x04' +logg = logging.getLogger('usawa.store') + def pfx_ledger_topic(topic): """Return ledger store prefix for topic. @@ -115,7 +118,8 @@ class LedgerStore(Interface): """Add an entry to the store. :param entry: Entry to add. - :type entry: usawa.Entry + :type entry: usawa.Entry or int + :raises: ValueError if the entry is not the right object type. :raises: FileExistsError if entry is already in store. """ def add_entry(self, entry): @@ -123,8 +127,32 @@ class LedgerStore(Interface): v = entry.wrap() self.__o.put(k, v) + """Restore an entry from data from the store. + The entry is referenced by its serial number within the store's ledger. It can either be specified as an integer, or an entry object with the serial number property set accordingly. + + :param entry: Entry of entry serial to restore. + :type entry: usawa.Entry or int + :param acl: Optional list of public keys to validate signatures against. + :type acl: usawa.ACL + :raises: PermissionError if the entry does not have a valid signature. + :raises: ValueError if the serial number cannot be retrieved from the entry argument. + :raises: FileExistsError if entry is already in store. + """ def get_entry(self, entry, acl=None): k = pfx_entry(self.ledger, entry) v = self.__o.get(k) return Entry.unwrap(v, acl=acl) + + + def load(self): + logg.debug('load ledger {}'.format(self.ledger.acl)) + while True: + o = None + try: + o = self.get_entry(self.ledger.serial) + except FileNotFoundError: + break + logg.debug('entry {}'.format(o)) + self.ledger.add_entry(o, modify_tree=True) + self.ledger.next_serial()