commit 5721c43fc574f3049aca3d4b7152ad53f2c73e7f
parent dc238b9bba621179c560df2df4c15807be3f3e1c
Author: lash <dev@holbrook.no>
Date: Mon, 9 Feb 2026 14:33:42 +0000
Apply XML as sign material for ledger + cleanup, docs
Diffstat:
4 files changed, 233 insertions(+), 347 deletions(-)
diff --git a/dummy/usawa/crypto.py b/dummy/usawa/crypto.py
@@ -3,6 +3,7 @@ import logging
import rencode
import nacl.signing
+import nacl.exceptions
AXX_ALL = 0xffffffff
AXX_ANY = 0x01
@@ -146,10 +147,17 @@ class DemoWallet(Wallet):
def verify(self, v, sig):
"""Implements usawa.Wallet.verify
+
+ :raises usawa.VerifyError: Signature does not match data.
"""
#return self.pubk.verify(v, sig)
- self.pubk.verify(v, sig)
- return True
+ r = False
+ try:
+ self.pubk.verify(v, sig)
+ r = True
+ except nacl.exceptions.BadSignatureError:
+ pass
+ return r
class ACL:
diff --git a/dummy/usawa/entry.py b/dummy/usawa/entry.py
@@ -4,7 +4,7 @@ import datetime
import uuid
import hashlib
-from lxml import etree
+import lxml.etree
import rencode
from .constant import DEFAULTPARENT, NSPREFIX
@@ -73,17 +73,17 @@ class EntryPart:
if self.isdebit:
tag = 'debit'
- part = etree.Element(tag, type=self.typ, nsmap=nsmap())
+ part = lxml.etree.Element(tag, type=self.typ, nsmap=nsmap())
- o = etree.Element('unit')
+ o = lxml.etree.Element('unit')
o.text = self.unit
part.append(o)
- o = etree.Element('account')
+ o = lxml.etree.Element('account')
o.text = self.account
part.append(o)
- o = etree.Element('amount')
+ o = lxml.etree.Element('amount')
o.text = str(self.amount)
logg.debug('tree amount {} {}'.format(self.unit, o.text))
part.append(o)
@@ -426,35 +426,31 @@ class Entry:
"""
def to_tree(self):
#tree = etree.Element('entry', type=self.typ)
- tree = etree.Element(NSPREFIX + 'entry', nsmap=nsmap())
- data = etree.Element('data')
+ tree = lxml.etree.Element(NSPREFIX + 'entry', nsmap=nsmap())
+ data = lxml.etree.Element('data')
- o = etree.Element('parent')
+ o = lxml.etree.Element('parent')
o.text = self.parent.hex()
data.append(o)
- o = etree.Element('ref')
+ o = lxml.etree.Element('ref')
o.text = self.ref
data.append(o)
- o = etree.Element('serial')
+ o = lxml.etree.Element('serial')
o.text = str(self.serial)
data.append(o)
-# o = etree.Element('unit')
-# o.text = self.unit
-# data.append(o)
-
- o = etree.Element('date')
+ o = lxml.etree.Element('date')
o.text = self.dt.strftime('%Y-%m-%d')
data.append(o)
- o = etree.Element('dateTimeRegistered')
+ o = lxml.etree.Element('dateTimeRegistered')
o.text = self.dtreg.strftime('%Y-%m-%dT%H:%M:%SZ')
data.append(o)
if self.description:
- o = etree.Element('description')
+ o = lxml.etree.Element('description')
o.text = self.description
data.append(o)
@@ -470,7 +466,7 @@ class Entry:
v = k
if isinstance(v, bytes):
v = k.hex()
- o = etree.Element('sig', type='ed25519', keyid=v)
+ o = lxml.etree.Element('sig', type='ed25519', keyid=v)
o.text = self.sigs[k].hex()
tree.append(o)
@@ -479,8 +475,7 @@ class Entry:
def canon(self):
tree = self.to_tree()
- b = etree.canonicalize(tree, strip_text=True, exclude_tags=['sig'])
- logg.debug('b {}'.format(b.encode('utf-8')))
+ b = lxml.etree.canonicalize(tree, strip_text=True, exclude_tags=['sig'])
return b.encode('utf-8')
diff --git a/dummy/usawa/ledger.py b/dummy/usawa/ledger.py
@@ -4,7 +4,7 @@ import datetime
import logging
import hashlib
-import lxml
+import lxml.etree
import rencode
import varints.leb128s
@@ -14,6 +14,7 @@ from .constant import NSPREFIX, DEFAULTPARENT
from .entry import Entry
from .unit import UnitIndex
from .util import to_datestring
+from .error import VerifyError
logg = logging.getLogger('usawa.ledger')
@@ -52,13 +53,17 @@ class RunningTotal:
+ """Mark the running total as representing a "real" (non-derivative) asset.
+
+ :seealso: to_tree()
+ """
def set_real(self):
self.real = True
"""Return the current asset/liability balance.
- :returns: Balance
+ :return: Balance
:rtype: int
"""
def get_balance(self):
@@ -66,7 +71,7 @@ class RunningTotal:
"""Return the current income/expense result.
- :returns: Result.
+ :return: Result.
:rtype: int
"""
def get_result(self):
@@ -118,8 +123,14 @@ class RunningTotal:
fn(amount)
+ """Instantiate a Running total object from XML.
- """
+ The expected XML is the Balance complex type; ledger/incoming/real or ledger/incoming/virt.
+
+ :param tree: XML tree
+ :type tree: lxml.etree.Element
+ :return: Unit index
+ :rtype: usawa.UnitIndex
"""
@staticmethod
def from_tree(tree):
@@ -129,6 +140,15 @@ class RunningTotal:
return RunningTotal(unit, asset=asset, liability=liability)
+ """Generate an XML tree from the current state of the object.
+
+ The XML generated can be used as a "real" or "virt" sub-element of the ledger/incoming/ element.
+
+ By default, a "virt" tag will be used, unless set_real() has been previously called.
+
+ :return: XML tree
+ :rtype: lxml.etree.Element
+ """
def to_tree(self):
tag = 'virt'
if self.real:
@@ -154,7 +174,10 @@ class RunningTotal:
return tree
- """
+ """Generate the simple data structure used for rencode serialization.
+
+ :returns: data structure
+ :rtype: list
"""
def to_list(self):
d = [
@@ -167,6 +190,11 @@ class RunningTotal:
return d
+ """Generate the unit index part of an Entry in wire format.
+
+ :return: rencoded object
+ :rtype: bytes
+ """
def serialize(self):
d = self.to_list()
return rencode.dumps(d)
@@ -188,10 +216,10 @@ class Ledger:
If the serial number parameter is non-zero, the base must be a non-zero digest value.
+ The src parameter is not the same as resolved. Whereas the resolver is used to retrieve assets from a content-addressed storage, the src rather points to a context rich resource containing ledger and entry data intended for human consumption.
+
:param unitindex: The unit index to resolve transaction values used in the entries.
:type unitindex: usawa.UnitIndex
- :param tree: A verified XML tree to use to populate the ledger.
- :type tree: lxml.etree.ElementTree
:param acl: A collection of public keys to use to verify signatures. Will override existing public key lists.
:type acl: usawa.ACL
:param serial: Serial number to start the current state of the ledger on.
@@ -200,18 +228,20 @@ class Ledger:
:type base: str
:param topic: Hexadecimal userdata providing context of the ledger.
:type topic: str
+ :param src: URI for the source of the data contained by the legder.
+ :type src: str
+ :param wallet: Wallet to use to sign the ledger state for XML exports.
+ :type wallet: usawa.Wallet
:todo: Add warnings for ignored parameters
+ :todo: Remove enclosing array for entries
"""
-
- #def __init__(self, unitindex, tree=None, acl=None, serial=0, base=DEFAULTPARENT, topic=None, src=None, wallet=None):
def __init__(self, unitindex, acl=None, serial=0, base=DEFAULTPARENT, topic=None, src=None, wallet=None):
self.uidx = unitindex
self.sigs = {}
self.entries = {}
self.running = {}
self.wallet = None
-# base_running = RunningTotal(self.uidx.base, self.uidx)
-# self.running = {self.uidx.base: base_running}
+
for k in self.uidx.syms():
if self.running.get(k) != None:
continue
@@ -220,7 +250,7 @@ class Ledger:
if self.uidx.base == k:
self.running[k].set_real()
self.resolvers = {}
- #self.tree = tree
+
self.base = base
self.base_serial = serial
self.src = src
@@ -236,29 +266,23 @@ class Ledger:
h.update(topic)
h.update(DEFAULTPARENT)
base = h.digest()
- #if self.tree == None:
- # self.reset()
+
self.serial = self.base_serial
self.cur = base
logg.debug('ledger base {} serial {} from topic {}'.format(self.base.hex(), self.serial, self.topic.hex()))
- """Wallet to add public key for in identities element in ledger XML.
+ """Wallet to use to sign ledger for XML exports.
:param v: Wallet to set
:type v: usawa.Wallet
+ :todo: Verify that this does not inadvertently affect the ACL
"""
def set_wallet(self, v):
self.wallet = v
if self.acl == None:
self.acl = ACL.from_wallet(self.wallet)
- self.apply_wallet()
-
- """
- """
- def apply_wallet(self):
- #incoming = self.tree.find('incoming', namespaces=nsmap())
v = self.wallet.pubkey()
try:
self.sigs[v]
@@ -267,18 +291,8 @@ class Ledger:
pass
self.sigs[v] = b''
- ## cut old xml related
- return
-
- #o = lxml.etree.Element(NSPREFIX + 'identity', nsmap=nsmap())
- o = lxml.etree.Element('identity')
- o.set('keyid', v.hex())
- did = self.wallet.did()
- o.set('didtype', did.method())
- incoming.addprevious(o)
-
- """Retrieve the serial that will be assigned to the next entry.
+ """Retrieve the serial that will be assigned to the next entry, without incrementing it in the object state.
:rtype: int
:return: Serial
@@ -296,26 +310,26 @@ class Ledger:
return self.serial
- """Remove all entries from the ledger, and reset all metadata to defaults.
+ """Generate a ledger XML tree from the current state of the ledger.
- 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.
+ It includes XML for all subordinate objects necessary to build a document that can be validated by the XML schema.
- :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
- :todo: swapping tree keeps two trees in memory, perhaps it can be more efficient
- :todo: add permissions array to the identities elements
+ This method will also generate XML entries for all entries in the object state. If only the current ledger state is desired, a preceding call to truncate() is needed.
+
+ :return: XML tree of ledger
+ :rtype: lxml.etree.Element
+ :todo: split up function
+ :todo: implement XML identity export for wallet
+ :todo: enable use of multiple "real" elements
+ :todo: deduplicate signature from wallet identity if already exists
"""
- #def reset(self, src=None, topic=None, acl=None, wallet=None):
def to_tree(self):
- #if wallet != None:
- # self.set_wallet(wallet)
self.serial = self.base_serial
self.cur = self.base
self.entries[self.uidx.base] = []
tree = lxml.etree.XML('<ledger xmlns="http://usawa.defalsify.org/" version="{}"></ledger>'.format(XML_FORMAT_VERSION))
+
+ # generate topic
o = lxml.etree.Element('topic')
tree.append(o)
if self.topic == None:
@@ -324,12 +338,13 @@ class Ledger:
else:
topic = self.topic.hex()
o.text = topic
- #o = lxml.etree.SubElement(tree, NSPREFIX + 'generated', nsmap=nsmap())
+
+ # datetime (this) ledger snapshot generated
o = lxml.etree.SubElement(tree, 'generated')
self.dt = datetime.datetime.now()
o.text = to_datestring(self.dt)
- #self.tree.append(o)
- #o = lxml.etree.SubElement(tree, NSPREFIX + 'src', nsmap=nsmap())
+
+ # the human-readable source for ledger data
o = lxml.etree.SubElement(tree, 'src')
if self.src == None:
src = self.default_src
@@ -337,125 +352,55 @@ class Ledger:
src = self.src
o.text = src
+ # unit index
units_tree = self.uidx.to_tree()
tree.append(units_tree)
-
- v = self.wallet.pubkey()
- try:
- self.sigs[v]
- except KeyError:
- self.sigs[v] = b''
-
- #o = lxml.etree.Element(NSPREFIX + 'identity', nsmap=nsmap())
+
+ # identity entry for the key signing the ledger state.
+ pubkey = self.wallet.pubkey()
o = lxml.etree.Element('identity')
- o.set('keyid', v.hex())
+ o.set('keyid', pubkey.hex())
did = self.wallet.did()
o.set('didtype', did.method())
tree.append(o)
-##
-##
-## #ns = {'ns': nsmap()[None]}
-##
-## # TODO: move identity tree generation to wallet object
-## identities = []
-## #for tree_identity in self.tree.xpath('ns:identity', namespaces=ns):
-## for tree_identity in self.tree.xpath('identity'):
-## keyid = tree_identity.get('keyid')
-## try:
-## self.sigs[keyid]
-## continue
-## except KeyError:
-## pass
-## v = tree_identity.text
-## identities.append(keyid)
-## #identity = lxml.etree.SubElement(tree, NSPREFIX + 'identity', nsmap=nsmap())
-## identity = lxml.etree.SubElement(tree, 'identity')
-## identity.text = v
-## identity.set('keyid', keyid)
-## identity.set('didtype', tree_identity.get('didtype'))
-##
-## if acl != None:
-## for v in acl.pubkeys(binary=False):
-## if v not in identities:
-## identities.append(v)
-## #identity = lxml.etree.SubElement(tree, NSPREFIX + 'identity', nsmap=nsmap())
-## identity = lxml.etree.SubElement(tree, 'identity')
-## identity.set('keyid', v)
-## identity.set('didtype', acl.did(v).method())
-##
-## if len(identities) == 0:
-## logg.warning('no identities in xml, need at least one to validate against schema')
-##
- #incoming = lxml.etree.SubElement(tree, 'incoming', nsmap=nsmap())
+ # incoming state
incoming = lxml.etree.SubElement(tree, 'incoming')
+
+ # incoming serial
incoming.set('serial', str(self.serial))
-
-
- # swap running and apply all bases
- #self.running = {}
- #incoming_old = self.tree.find('incoming', namespaces=nsmap())
- #logg.debug('inc {}'.format(lxml.etree.tostring(incoming_old)))
-
-# if incoming_old != None:
-# i = 0
-# #for tree_real in incoming_old.xpath('ns:real[@unit]', namespaces=ns):
-# for tree_real in incoming_old.xpath('real[@unit]'):
-# unit = tree_real.get('unit')
-# #v = tree_real.find('asset', namespaces=nsmap())
-# v = tree_real.find('asset')
-# asset = int(v.text)
-# #v = tree_real.find('liability', namespaces=nsmap())
-# v = tree_real.find('liability')
-# liability = int(v.text)
-# self.running[unit] = RunningTotal(unit, self.uidx, asset=asset, liability=liability)
-# real = lxml.etree.SubElement(incoming, NSPREFIX + 'real', nsmap=nsmap())
-# real.attrib['unit'] = unit
-# o = lxml.etree.SubElement(real, NSPREFIX + 'asset', nsmap=nsmap())
-# o.text = str(self.running[unit].asset)
-# o = lxml.etree.SubElement(real, NSPREFIX + 'liability', nsmap=nsmap())
-# o.text = str(self.running[unit].liability)
-# i += 1
-#
-# if i == 0:
-# real = lxml.etree.SubElement(incoming, NSPREFIX + 'real', nsmap=nsmap())
-# real.set('unit', self.uidx.default_unit)
-# o = lxml.etree.SubElement(real, NSPREFIX + 'asset', nsmap=nsmap())
-# o.text = '0'
-# o = lxml.etree.SubElement(real, NSPREFIX + 'liability', nsmap=nsmap())
-# o.text = '0'
-#
-
+
+ # incoming base (real) currency balance
o = self.running[self.uidx.base].to_tree()
incoming.append(o)
+ # incoming remaining (virt) currency balances
for k in self.running.keys():
if k == self.uidx.base:
continue
o = self.running[k].to_tree()
incoming.append(o)
- #o = lxml.etree.SubElement(incoming, NSPREFIX + 'digest', nsmap=nsmap())
+ # incoming entry digest
o = lxml.etree.SubElement(incoming, 'digest')
o.attrib['algo'] = 'sha512'
o.text = self.base.hex()
+ # incoming signatures
+ # sign the ledger if it has no signatures
if len(self.sigs.keys()) == 0:
self.sign()
+ # apply all already existing signatures (e.g. from import)
for k in self.sigs.keys():
- #o = lxml.etree.SubElement(incoming, NSPREFIX + 'sig', nsmap=nsmap())
sig = self.sigs[k]
o = lxml.etree.SubElement(incoming, 'sig')
o.set('keyid', k.hex())
o.set('type', 'ed25519')
o.text = sig.hex()
incoming.append(o)
- else:
- k = list(self.sigs.keys())[0]
- logg.debug('sig {}'.format(self.sigs[k].hex()))
-
+ # apply all entries in object state
# TODO: entry should not be array
for k in self.entries.keys():
for v in self.entries[k]:
@@ -465,60 +410,11 @@ class Ledger:
return tree
- """Add a decentralized identity definition for signers of the ledger.
-
- :param keyid: Hexadecimal representation of the key identifier.
- :type keyid: str
- :param did: Did provider.
- :type did: str
- :param typ: Type of did provider, default 'web'.
- :type did: str
- :todo: should append after last
- """
- #def add_identity(self, keyid, did, typ='web'):
- def load_identities(self, root, keyid, did, typ='web'):
- tree = root.find('resolver', namespaces=nsmap())
- if tree == None:
- tree = root.find('units', namespaces=nsmap())
- if tree == None:
- logg.debug('exception tree {}'.format(lxml.etree.tostring(self.tree)))
- raise Exception('cannot find units node')
- o = lxml.etree.Element(NSPREFIX + 'identity', nsmap=nsmap())
- o.attrib['keyid'] = keyid
- o.attrib['didtype'] = typ
- o.text = did
- tree.addnext(o)
-
-
- """Add an endpoint to resolve content by digest.
-
- :param location: The endpoint location, host and path or only path, depending on the scheme.
- :type path: str
- :param algo: Digest algorithm to use to retrieve and verify content, default 'sha256'.
- :type path: str
- :param proto: URI scheme used to connect to the endpoint.
- :param algo: str
- :todo: should append after last
- """
- #def add_resolver(self, location, algo='sha256', scheme='https'):
- def load_resolvers(self, root, location, algo='sha256', scheme='https'):
- #tree = self.tree.find('units', namespaces=nsmap())
- tree = root.find('units', namespaces=nsmap())
- o = lxml.etree.Element(NSPREFIX + 'resolver', nsmap=nsmap())
- o.attrib['algo'] = algo
- o.attrib['proto'] = proto
- o.text = location
-
- if self.resolvers.get(algo) == None:
- self.resolvers[algo] = []
- self.resolvers[algo].append((proto, location,))
-
- tree.addnext(o)
-
"""Check signature on individual ledger entry.
:param entry: The individual entry to verify.
:type entry: usawa.Entry
+ :raises usawa.VerifyError: No valid signatures found.
"""
def check_sigs(self, entry):
have = False
@@ -545,55 +441,51 @@ class Ledger:
wallet = DemoWallet(publickey=b)
v = entry.sum()
return wallet.verify(v[0], sig)
- #return have
+ have = True
+ if not have:
+ raise VerifyError()
"""Append entry to ledger. The entry must have a valid signature from a trusted public key.
+ Any entry added here will be part of the XML export, unless truncate() is called afterwards.
+
:param entry: The entry to append.
:type entry: usawa.Entry
- :param modify_tree: If True, also append the entry to the XML export.
- :type modify_tree: boolean
:raises ValueError: When entry parent does not match ledger state.
- :raises PermissionError: When entry is missing valid signature.
- :todo: modify_tree is too low-level for this API
+ :raises VerifyError: When entry is missing valid signature.
"""
- #def add_entry(self, entry, modify_tree=True):
def add_entry(self, entry):
if self.cur != entry.parent:
raise ValueError('entry parent does not match ledger state')
- if not self.check_sigs(entry):
- raise PermissionError('entry must have at least one valid signature')
+ self.check_sigs(entry)
+ # TODO: entries should not be list
try:
entries = self.entries[entry.serial]
except KeyError:
self.entries[entry.serial] = []
- #entries = self.entries[entry.serial]
+
+
+ # update the internal state
self.serial = entry.serial
- #oldbase = self.base
oldsum = self.cur
self.cur = entry.sum()[0]
entry.parent = oldsum
- self.entries[entry.serial].append(entry)
- #self.running[entry.unit].apply_entry(entry)
self.apply_entryparts(entry)
- # skip xml part
- return
+ # Add entry to the ledger object.
+ self.entries[entry.serial].append(entry)
- if self.tree != None and modify_tree:
- entry_tree = entry.to_tree()
- self.tree.append(entry_tree)
+
+ """Update running total according to the entry.
+ Object does not keep track of which entries have been applied to the running total. Caller must take care not to call this more than once for each entry.
+ :param entry: Entry whose parts to apply to running total.
+ :type entry: usawa.Entry
+ """
def apply_entryparts(self, entry):
for v in entry.debit:
-# src = entry.src.typ
-# dst = entry.dst.typ
-# src_isbalance = src in ['liability', 'asset']
-# dst_isbalance = dst in ['liability', 'asset']
-# src_unit = entry.src.unit
-# dst_unit = entry.dst.unit
amount = v.amount
if v.isdebit:
amount *= -1
@@ -604,41 +496,31 @@ class Ledger:
if v.isdebit:
amount *= -1
self.running[v.unit].apply(v.typ, amount)
-
-# src_amount = entry.src.amount
-# dst_amount = entry.dst.amount
-# if src_isbalance and dst_isbalance:
-# if dst == 'liability':
-# src_amount *= -1
-# dst_amount *= -1
-# self.running[src_unit].apply(src, src_amount)
-# self.running[dst_unit].apply(dst, dst_amount)
logg.debug('applied entry {} src {} dst {}'.format(entry.serial, entry.debit, entry.credit))
- """
-
- :todo: handle canonical hex
- """
- #def apply_signature(self, identity):
- def apply_signatures(self, root, identity):
- sig = self.sigs[identity]
- sig_hx = sig.hex()
- #tree = self.tree.find('incoming', namespaces=nsmap())
- tree = root.find('incoming')
-
- #for v in tree.iter(NSPREFIX + 'sig'):
- for v in tree.iter('sig', namespaces=nsmap()):
- if v.get('keyid') == identity.hex():
- v.text = sig_hx
- return
- #o = lxml.etree.SubElement(tree, NSPREFIX + 'sig', nsmap=nsmap())
- #o = lxml.etree.SubElement(tree, 'sig', nsmap=nsmap())
- o = lxml.etree.SubElement(tree, 'sig')
- o.set('keyid', identity.hex())
- o.set('type', 'ed25519')
- o.text = sig_hx
+# """
+#
+# :todo: handle canonical hex
+# """
+# def apply_signatures(self, root, identity):
+# sig = self.sigs[identity]
+# sig_hx = sig.hex()
+# #tree = self.tree.find('incoming', namespaces=nsmap())
+# tree = root.find('incoming')
+#
+# #for v in tree.iter(NSPREFIX + 'sig'):
+# for v in tree.iter('sig', namespaces=nsmap()):
+# if v.get('keyid') == identity.hex():
+# v.text = sig_hx
+# return
+# #o = lxml.etree.SubElement(tree, NSPREFIX + 'sig', nsmap=nsmap())
+# #o = lxml.etree.SubElement(tree, 'sig', nsmap=nsmap())
+# o = lxml.etree.SubElement(tree, 'sig')
+# o.set('keyid', identity.hex())
+# o.set('type', 'ed25519')
+# o.text = sig_hx
"""Add a signature on the ledger.
@@ -646,11 +528,8 @@ class Ledger:
:todo: not an appropriate API function?
:todo: implement validity checks for signature.
"""
- #def add_signature(self, sigdata, identity, modify_tree=True):
def add_signature(self, sigdata, identity):
self.sigs[identity] = sigdata
- #if modify_tree:
- # self.apply_signature(identity)
logg.debug('add sig from key {}: {}'.format(identity.hex(), sigdata.hex()))
@@ -674,25 +553,22 @@ class Ledger:
units = tree.find('units', namespaces=nsmap())
unit = units.get('base')
- part = tree.find('incoming', namespaces=nsmap()) #, namespaces=nsmap())
+ part = tree.find('incoming', namespaces=nsmap())
serial = int(part.get('serial'))
- #o = part.find('digest', namespaces=nsmap()).text # verify that is sha512
+
o = part.find('digest', namespaces=nsmap()).text # verify that is sha512
- #r = Ledger(unitindex, topic=topic, tree=tree, acl=acl, serial=serial, base=bytes.fromhex(o))
+
ledger = Ledger(unitindex, topic=topic, acl=acl, serial=serial, base=bytes.fromhex(o))
- #for sig in part.iter(NSPREFIX + 'sig'):
for sig in part.iter('sig'):
keyid = sig.get('keyid')
digest = sig.text
ledger.add_signature(bytes.fromhex(digest), bytes.fromhex(keyid), modify_tree=False)
- #for sig in part.iter(NSPREFIX + 'identity'):
for identity in tree.findall('identity', namespaces=nsmap()):
keyid = identity.get('keyid')
didtyp = identity.get('didtype')
did = identity.text
- #ledger.add_identity(keyid, did, typ=didtyp)
public_key = bytes.fromhex(keyid)
wallet = DemoWallet(publickey=public_key)
ledger.set_wallet(wallet)
@@ -716,7 +592,6 @@ class Ledger:
if ledger.running.get(unit) == None:
ledger.running[unit] = RunningTotal(unit, unitindex)
- #r.apply_tree(tree)
ledger.apply_entries(tree)
logg.debug('loaded ledger tree last serial {}'.format(ledger.serial))
return ledger.check()
@@ -727,7 +602,6 @@ class Ledger:
:param tree: A parsed XML tree.
:todo: Not an API method.
"""
- #def apply_tree(self, tree):
def apply_entries(self, tree):
start = self.serial
last = 1
@@ -736,7 +610,6 @@ class Ledger:
i += 1
logg.debug('processing entry {}'.format(v))
o = Entry.from_tree(v, self.uidx, min=self.serial)
- #self.add_entry(o, modify_tree=False)
self.add_entry(o)
if o.serial > last:
last = o.serial
@@ -745,35 +618,15 @@ class Ledger:
logg.info('last entry from tree serial ' + str(self.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, modify_tree=True):
def truncate(self):
self.base = self.cur
self.base_serial = self.serial
+ self.entries = {}
- # skip xml mods
- return
- #if not modify_tree:
- # return
-
- inc_tree = self.tree.find('incoming', namespaces=nsmap())
- inc_tree.set('serial', str(self.base_serial))
- #o = inc_tree.find('digest', namespaces=nsmap())
- o = inc_tree.find('digest', namespaces=nsmap())
- o.text = self.base.hex()
-
- # xpath does not support empty namespace names
- ns = {'ns': nsmap()[None]}
- for k in self.running:
- o = inc_tree.xpath("ns:real[@unit='{}']".format(k), namespaces=ns)
- v = o[0].find('asset', namespaces=nsmap())
- logg.debug('setting asset {} for {}'.format(self.running[k].asset, k))
- v.text = str(self.running[k].asset)
- v = o[0].find('liability', namespaces=nsmap())
- v.text = str(self.running[k].liability)
-
- self.reset()
"""Verify digest chain and signatures in ledger.
@@ -785,7 +638,7 @@ class Ledger:
"""Return a string representation of the XML tree.
- :returns: XML document in UTF-8 format.
+ :return: XML document in UTF-8 format.
:rtype: str
"""
def to_string(self):
@@ -799,74 +652,96 @@ class Ledger:
The digest type is defined in the usawa.Entry.digest_algo.
- :returns: Digest.
+ :return: Digest.
:rtype: bytes
"""
def current(self):
return self.cur
- """Generate the serialization format used to calculate the digest for the entry.
+ def canon(self):
+ tree = self.to_tree()
+ b = lxml.etree.canonicalize(tree, strip_text=True, exclude_tags=['sig'])
+ return b.encode('utf-8')
- :returns: String representation of the entry, in rencode format.
- :rtype: str
- """
- def serialize(self):
- ts = int(self.dt.timestamp())
- ts_bytes = ts.to_bytes(4, byteorder='big')
- #units = self.uidx.serialize()
- units = self.uidx.to_list()
- #identities = self.acl.serialize()
- identities = self.acl.to_list()
- totals = []
- #v = self.running[self.uidx.base].serialize()
- v = self.running[self.uidx.base].to_list()
- totals.append(v)
- for k in self.running.keys():
- if k == self.uidx.base:
- continue
- v = self.running[k].serialize()
- totals.append(v)
- d = [
- self.topic,
- varints.leb128s.encode(self.serial),
- self.cur,
- ts_bytes,
- units,
- identities,
- totals,
- ]
- logg.debug('serialize ledger {}'.format(d))
- return rencode.dumps(d)
+ """Sign the ledger with the assigned wallet.
- """
+ The wallet must have been passed in the wallet attribute during instantiation, or using set_wallet() preceding this call.
+
+ Any existing signature for the same object will be overwritten.
:raises AttributeError: Ledger is missing wallet
+ :returns: Signature data
+ :rtype: bytes
"""
def sign(self):
if self.wallet == None:
raise AttributeError()
- v = self.serialize()
+ #v = self.serialize()
+ v = self.canon()
r = self.wallet.sign(v)
k = self.wallet.pubkey()
self.add_signature(r, k)
return r
+# """Generate the simple data structure used for rencode serialization.
+#
+# :returns: data structure
+# :rtype: list
+# """
+# def to_list(self):
+# ts = int(self.dt.timestamp())
+# ts_bytes = ts.to_bytes(4, byteorder='big')
+# #units = self.uidx.serialize()
+# units = self.uidx.to_list()
+# #identities = self.acl.serialize()
+# identities = self.acl.to_list()
+# totals = []
+# #v = self.running[self.uidx.base].serialize()
+# v = self.running[self.uidx.base].to_list()
+# totals.append(v)
+# for k in self.running.keys():
+# if k == self.uidx.base:
+# continue
+# v = self.running[k].serialize()
+# totals.append(v)
+# d = [
+# self.topic,
+# varints.leb128s.encode(self.serial),
+# self.cur,
+# ts_bytes,
+# units,
+# identities,
+# totals,
+# ]
+# return d
+#
+#
+# """Generate the unit index part of an Entry in wire format.
+#
+# :returns: String representation of the entry, in rencode format.
+# :rtype: str
+# """
+# def serialize(self):
+# b = self.to_list()
+# return rencode.dumps(b)
+#
- """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
+# """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):
diff --git a/dummy/usawa/unit.py b/dummy/usawa/unit.py
@@ -50,19 +50,21 @@ class UnitIndex:
self.exchange[sym] = ex
- """Create a unit index from a full ledger XML tree.
+ """Create a unit index object from XML.
- :param tree: Parsed and verified XML tree.
- :type tree: lxml.etree.ElementTree
+ The XML element expected is ledger/units.
+
+ :param tree: XML tree.
+ :type tree: lxml.etree.Element
:returns: The unit index object.
:rtype: usawa.UnitIndex
"""
@staticmethod
def from_tree(tree):
- unit_tree = tree.find(NSPREFIX + 'units')
- base = unit_tree.get('base')
+ #tree = tree.find(NSPREFIX + 'units')
+ base = tree.get('base')
r = UnitIndex(base)
- for o in unit_tree.iter(NSPREFIX + 'unit'):
+ for o in tree.iter(NSPREFIX + 'unit'):
logg.debug('add unit ' + o.get('sym'))
r.detail[o.get('sym')] = int(o.find('precision', namespaces=nsmap()).text)
r.exchange[o.get('sym')] = int(o.find('exchange', namespaces=nsmap()).text)
@@ -223,6 +225,12 @@ class UnitIndex:
pass
+ """Generate XML tree from current state of the object.
+
+ The XML element generated is the units sub-element of the root ledger element.
+ :returns: XML tree
+ :rtype: lxml.etree.Element
+ """
def to_tree(self):
tree = lxml.etree.XML('<units></units>')
tree.set('base', self.base)