commit 649252027133e93298cff57e648442a0f2d27cc9
parent 9b9ac428ee2a7aab190c68c0e597bcd6b3e307d9
Author: lash <dev@holbrook.no>
Date: Mon, 5 Jan 2026 15:57:01 +0100
Add ledger restoration from store
Diffstat:
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()