usawa

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

commit 8305afaa29dedbc12a7f8037816625a602c423f5
parent 93638ab8d231912e1226acb00ce1aebbf6fa7a07
Author: lash <dev@holbrook.no>
Date:   Tue, 17 Feb 2026 09:05:26 +0000

Implement lookup element in entry and ledger

Diffstat:
Mdummy/tests/entry.py | 2--
Adummy/tests/resolver.py | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ddummy/tests/test_resolver.py | 53-----------------------------------------------------
Mdummy/usawa/data/schema.xsd | 1+
Mdummy/usawa/entry.py | 30++++++++++++++++++++++++++++--
Mdummy/usawa/ledger.py | 45+++++++++++++++++++++++++++++++++++++--------
6 files changed, 160 insertions(+), 65 deletions(-)

diff --git a/dummy/tests/entry.py b/dummy/tests/entry.py @@ -2,7 +2,6 @@ import logging import datetime import unittest import os -import copy from usawa import EntryPart, Entry, DemoWallet, ACL, UnitIndex, Asset from usawa.error import ACLError @@ -29,7 +28,6 @@ class TestEntry(unittest.TestCase): o = Entry(42, datetime.datetime.strptime('2025-11-11', '%Y-%m-%d'), parent=self.parent, ref=self.ref, description=self.description, tx_datereg=self.dtreg) o.add_part(src, debit=True) o.add_part(dst) - oo = copy.deepcopy(o) s = o.serialize() o = Entry.deserialize(s) diff --git a/dummy/tests/resolver.py b/dummy/tests/resolver.py @@ -0,0 +1,94 @@ +import unittest +import logging +import tempfile +import hashlib +import shutil +import os +import datetime + +import lxml.etree + +from usawa import Ledger, UnitIndex, Entry, EntryPart, DemoWallet +from usawa.resolve.fs import FSResolver +from usawa.error import VerifyError + + +logging.basicConfig(level=logging.DEBUG) +logg = logging.getLogger() + +testdir = os.path.realpath(os.path.dirname(__file__)) + +hash_of_foo = 'f7fbba6e0636f890e56fbbf3283e524c6fa3204ae298382d624741d0dc6638326e282c41be5e4254d8820772c5518a2c5a8c0c7f7eda19594a7eb539453e1ed7' +hash_of_bar = 'd82c4eb5261cb9c8aa9855edd67d1bd10482f41529858d925094d173fa662aa91ff39bc5b188615273484021dfb16fd8284cf684ccf0fc795be3aa2fc1e6c181' + +class TestResolver(unittest.TestCase): + + def setUp(self): + self.parent = b'\x00' * 64 + self.path = tempfile.mkdtemp() + self.backend = FSResolver(self.path) + self.dtreg = datetime.datetime.now() + + + def tearDown(self): + shutil.rmtree(self.path) + + + def test_resolve_putget(self): + h = hashlib.sha512() + v = os.urandom(1337) + h.update(v) + k = h.digest() + self.backend.put(k, v) + r = self.backend.get(k) + self.assertEqual(r, v) + + k_wrong = os.urandom(32) + with self.assertRaises(ValueError): + r = self.backend.get(k_wrong) + + + def test_resolve_get_evil(self): + fp = os.path.join(self.path, hash_of_foo) + f = open(fp, 'wb') + f.write(b'bar') + f.close() + with self.assertRaises(VerifyError): + self.backend.get(hash_of_foo) + + + def test_resolve_lookup(self): + uidx = UnitIndex('FOO') + wallet = DemoWallet() + ledger = Ledger(uidx, wallet=wallet, topic=hash_of_foo.encode('utf-8')) + dst = EntryPart('FOO', 'asset', 'foo', 1337) + src = EntryPart('FOO', 'income', 'foo', 1337, debit=True) + entry = Entry(42, datetime.datetime.strptime('2025-11-11', '%Y-%m-%d'), parent=self.parent, tx_datereg=self.dtreg) + entry.add_part(src, debit=True) + entry.add_part(dst) + entry.sign(wallet) + ledger.add_entry(entry) + + dst = EntryPart('FOO', 'expense', 'bar̈́', 42, debit=True) + src = EntryPart('FOO', 'liability', 'bar', 42) + entry = Entry(ledger.peek(), datetime.datetime.now(), parent=ledger.current()) + entry.add_part(src, debit=True) + entry.add_part(dst) + entry.sign(wallet) + ledger.add_entry(entry) + + #s = lxml.etree.tostring(tree) + ledger.truncate(lookup='sha512') + tree = ledger.to_tree(lookup='sha512') + s = lxml.etree.tostring(tree) + print(s.decode('utf-8')) + + for k in ledger.entries.keys(): + tree = ledger.entries[k].to_tree() + s = lxml.etree.tostring(tree) + + ledger.truncate() + + +if __name__ == '__main__': + unittest.main() diff --git a/dummy/tests/test_resolver.py b/dummy/tests/test_resolver.py @@ -1,53 +0,0 @@ -import unittest -import logging -import tempfile -import hashlib -import shutil -import os - -from usawa.resolve.fs import FSResolver -from usawa.error import VerifyError - - -logging.basicConfig(level=logging.DEBUG) -logg = logging.getLogger() - -hash_of_foo = 'f7fbba6e0636f890e56fbbf3283e524c6fa3204ae298382d624741d0dc6638326e282c41be5e4254d8820772c5518a2c5a8c0c7f7eda19594a7eb539453e1ed7' -hash_of_bar = 'd82c4eb5261cb9c8aa9855edd67d1bd10482f41529858d925094d173fa662aa91ff39bc5b188615273484021dfb16fd8284cf684ccf0fc795be3aa2fc1e6c181' - -class TestResolver(unittest.TestCase): - - def setUp(self): - self.path = tempfile.mkdtemp() - self.backend = FSResolver(self.path) - - - def tearDown(self): - shutil.rmtree(self.path) - - - def test_resolve_putget(self): - h = hashlib.sha512() - v = os.urandom(1337) - h.update(v) - k = h.digest() - self.backend.put(k, v) - r = self.backend.get(k) - self.assertEqual(r, v) - - k_wrong = os.urandom(32) - with self.assertRaises(ValueError): - r = self.backend.get(k_wrong) - - - def test_resolve_get_evil(self): - fp = os.path.join(self.path, hash_of_foo) - f = open(fp, 'wb') - f.write(b'bar') - f.close() - with self.assertRaises(VerifyError): - self.backend.get(hash_of_foo) - - -if __name__ == '__main__': - unittest.main() diff --git a/dummy/usawa/data/schema.xsd b/dummy/usawa/data/schema.xsd @@ -27,6 +27,7 @@ <xs:element name="real" type="Balance" minOccurs="1" maxOccurs="unbounded" /> <xs:element name="virt" type="Balance" minOccurs="0" maxOccurs="unbounded" /> <xs:element name="digest" type="Digest" minOccurs="1" maxOccurs="1"/> + <xs:element name="lookup" type="Lookup" minOccurs="0" maxOccurs="unbounded" /> <xs:element name="sig" type="Signature" minOccurs="1" maxOccurs="unbounded" /> </xs:sequence> <xs:attribute name="serial" type="xs:nonNegativeInteger" /> diff --git a/dummy/usawa/entry.py b/dummy/usawa/entry.py @@ -292,6 +292,10 @@ class Entry: asset = Asset.from_tree(v) entry.attach(asset) + o = tree.find('lookup') + if o != None: + self.lookup_algo = o.get('algo') + for sig in tree.iter(NSPREFIX + 'sig'): entry.add_signature(sig.get('keyid'), bytes.fromhex(sig.text)) @@ -529,7 +533,7 @@ class Entry: :returns: XML tree representing the entry. :rtype: lxml.etree.Element """ - def to_tree(self, canon=False): + def to_tree(self, canon=False, lookup=None): #tree = etree.Element('entry', type=self.typ) tree = lxml.etree.Element(NSPREFIX + 'entry', nsmap=nsmap()) data = lxml.etree.Element('data') @@ -581,8 +585,30 @@ class Entry: o.text = self.sigs[k].hex() tree.append(o) + if lookup: + v = self.get_lookup(lookup, tree=tree) + o = lxml.etree.Element('lookup') + o.set('algo', lookup) + o.text = v.hex + data.append(o) + return tree + def get_lookup(self, lookup, tree=None): + if tree == None: + tree = self.to_tree(lookup=False) + h = None + if lookup == 'sha512': + h = hashlib.sha512() + elif lookup == 'sha256': + h = hashlib.sha256() + else: + raise ValueError('invalid lookup algo') + + b = lxml.etree.canonicalize(tree, strip_text="True") + h.update(b.encode('utf-8')) + + return h.digest().hex() """Generate canonical XML for signature material. @@ -592,7 +618,7 @@ class Entry: """ def canon(self): tree = self.to_tree(canon=True) - b = lxml.etree.canonicalize(tree, strip_text=True, exclude_tags=['sig']) + b = lxml.etree.canonicalize(tree, strip_text=True, exclude_tags=['sig', 'lookup']) return b.encode('utf-8') diff --git a/dummy/usawa/ledger.py b/dummy/usawa/ledger.py @@ -242,6 +242,7 @@ class Ledger: self.entries = {} self.running = {} self.wallet = None + self.lookup = None for k in self.uidx.syms(): if self.running.get(k) != None: @@ -326,7 +327,7 @@ class Ledger: :todo: enable use of multiple "real" elements :todo: deduplicate signature from wallet identity if already exists """ - def to_tree(self): + def to_tree(self, lookup=None): self.serial = self.base_serial self.cur = self.base tree = lxml.etree.XML('<ledger xmlns="http://usawa.defalsify.org/" version="{}"></ledger>'.format(XML_FORMAT_VERSION)) @@ -359,8 +360,9 @@ class Ledger: tree.append(units_tree) # identity entry for the key signing the ledger state. - o = self.wallet.to_tree() - tree.append(o) + if self.wallet != None: + o = self.wallet.to_tree() + tree.append(o) # incoming state incoming = lxml.etree.SubElement(tree, 'incoming') @@ -383,6 +385,13 @@ class Ledger: o = lxml.etree.SubElement(incoming, 'digest') o.attrib['algo'] = 'sha512' o.text = self.base.hex() + incoming.append(o) + + if self.lookup != None: + o = lxml.etree.SubElement(incoming, 'lookup') + o.attrib['algo'] = self.lookup_algo + o.text = self.lookup + incoming.append(o) # incoming signatures # sign the ledger if it has no signatures @@ -401,7 +410,7 @@ class Ledger: # apply all entries in object state for k in self.entries.keys(): v = self.entries[k] - entry_tree = v.to_tree() + entry_tree = v.to_tree(lookup=lookup) tree.append(entry_tree) return tree @@ -561,6 +570,11 @@ class Ledger: if ledger.running.get(unit) == None: ledger.running[unit] = RunningTotal(unit, unitindex) + o = part.find('lookup') + if o != None: + self.lookup = o.text + self.lookup_algo = o.get('algo') + ledger.apply_entries(tree) logg.debug('loaded ledger tree last serial {}'.format(ledger.serial)) @@ -598,13 +612,28 @@ class Ledger: logg.info('last entry from tree serial ' + str(self.serial)) + def last_entry(self): + serial = -1 + digest = None + for i in self.entries.keys(): + if i > serial: + serial = i + return self.entries[serial] + + """Calculate and apply ledger state from current entries in the object, and remove entries. After this call, the XML export will not contain any entry elements, and will have digest and serial from the last entry that existed in the ledger. """ - def truncate(self): + def truncate(self, lookup=None): self.base = self.cur self.base_serial = self.serial + try: + entry = self.last_entry() + except KeyError: # if no entries + return + self.lookup = entry.get_lookup(lookup) + self.lookup_algo = lookup self.entries = {} @@ -621,8 +650,8 @@ class Ledger: :return: XML document in UTF-8 format. :rtype: str """ - def to_string(self): - tree = self.to_tree() + def to_string(self, lookup=None): + tree = self.to_tree(lookup=lookup) return lxml.etree.tostring(tree) @@ -645,7 +674,7 @@ class Ledger: :rtype: str """ def canon(self): - tree = self.to_tree() + tree = self.to_tree(lookup=False) b = lxml.etree.canonicalize(tree, strip_text=True, exclude_tags=['sig']) return b.encode('utf-8')