Source code for keri.peer.exchanging

# -*- encoding: utf-8 -*-
"""
keri.peer.exchanging module

"""
import datetime
import logging
from datetime import timedelta

from hio.help import decking, ogler

from ..kering import (Vrsn_1_0, Vrsn_2_0, Ilks,
                      Kinds, Version, versify,
                      ValidationError, MissingSignatureError)
from ..core import (Counter, Pather, Dater, Diger,
                    Prefixer, Seqner, Saider,
                    Noncer, Sadder, SerderKERI,
                    NonTransDex, Saids, Codens,
                    verifySigs)
from ..db import fetchTsgs
from ..help import helping

ExchangeMessageTimeWindow = timedelta(seconds=300)

logger = ogler.getLogger()


[docs] class Exchanger: """ Peer to Peer KERI message Exchanger. """ TimeoutPSE = 10 # seconds to timeout partially signed or delegated escrows
[docs] def __init__(self, hby, handlers, cues=None, delta=ExchangeMessageTimeWindow): """ Initialize instance Parameters: hby (Haberyu): database environment handlers(list): list of Handlers capable of responding to exn messages cues (Deck): of Cues i.e. notices of requests needing response delta (timedelta): message timeout window """ self.hby = hby self.kevers = self.hby.db.kevers self.delta = delta self.routes = dict() self.cues = cues if cues is not None else decking.Deck() # subclass of deque for handler in handlers: if handler.resource in self.routes: raise ValidationError("unable to register behavior {}, it has already been registered" "".format(handler.resource)) self.routes[handler.resource] = handler
def addHandler(self, handler): if handler.resource in self.routes: raise ValidationError("unable to register behavior {}, it has already been registered" "".format(handler.resource)) self.routes[handler.resource] = handler
[docs] def processEvent(self, serder, tsgs=None, cigars=None, ptds=None, essrs=None, **kwa): """ Process one serder event with attached indexed signatures representing a Peer to Peer exchange message. Parameters: serder (Serder): instance of event to process tsgs (list): tuples (quadruples) of form (prefixer, seqner, diger, [sigers]) where: prefixer is pre of trans endorser seqner is sequence number of trans endorser's est evt for keys for sigs diger is digest of trans endorser's est evt for keys for sigs [sigers] is list of indexed sigs from trans endorser's keys from est evt cigars (list): of Cigar instances of attached non-trans sigs ptds (list[bytes]): pathed Cesr Streams essrs (list[Texter]): ESSR streams as Texters """ ptds = ptds if ptds is not None else [] essrs = essrs if essrs is not None else [] route = serder.ked["r"] sender = serder.ked["i"] behavior = self.routes[route] if route in self.routes else None if tsgs: for prefixer, snumber, sdiger, sigers in tsgs: # iterate over each tsg if sender != prefixer.qb64: # sig not by aid msg = (f"Skipped signature not from aid = " f"{sender}, from {prefixer.qb64} on exn msg = {serder.said}") logger.info(msg) logger.debug("Exchange message body=\n%s\n", serder.pretty()) raise MissingSignatureError(msg) if prefixer.qb64 not in self.kevers or self.kevers[prefixer.qb64].sn < snumber.sn: if self.escrowPSEvent(serder=serder, tsgs=tsgs, pathed=ptds): self.cues.append(dict(kin="query", q=dict(r="logs", pre=prefixer.qb64, sn=snumber.snh))) msg = f"Unable to find sender {prefixer.qb64} in kevers for evt = {serder.said}" logger.info(msg) logger.debug("Exchange message body=\n%s\n", serder.pretty()) raise MissingSignatureError(msg) # Verify the signatures are valid and that the signature threshold as of the signing event is met tholder, verfers = self.hby.db.resolveVerifiers(pre=prefixer.qb64, sn=snumber.sn, dig=sdiger.qb64) _, indices = verifySigs(serder.raw, sigers, verfers) if not tholder.satisfy(indices): # We still don't have all the sigers, need to escrow if self.escrowPSEvent(serder=serder, tsgs=tsgs, pathed=ptds): self.cues.append(dict(kin="query", q=dict(r="logs", pre=prefixer.qb64, sn=snumber.snh))) msg = (f"Not enough signatures in idx={indices} route={route} " f"for evt = {serder.said} receiver={serder.ked.get('rp', '')}") logger.info(msg) logger.debug("Exchange message body=\n%s\n", serder.pretty()) raise MissingSignatureError(msg) elif cigars: for cigar in cigars: if sender != cigar.verfer.qb64: # cig not by aid msg = (f"Skipped cig not from aid={sender} route={route} " f"for exn evt = {serder.said} receiver={serder.ked.get('rp', '')}") logger.info(msg) logger.debug("Exchange message body=\n%s\n", serder.pretty()) raise MissingSignatureError(msg) if not cigar.verfer.verify(cigar.raw, serder.raw): # cig not verify msg = (f"Failure satisfying exn on cigs for {cigar} route={route} " f"for evt = {serder.said} receiver={serder.ked.get('rp', '')}") logger.info(msg) logger.debug("Exchange message body=\n%s\n", serder.pretty()) raise MissingSignatureError(msg) else: self.escrowPSEvent(serder=serder, tsgs=[], pathed=ptds) msg = ( f"Failure satisfying exn, no cigs or sigs for evt = {serder.said} " f"on route {route} receiver = {serder.ked.get('rp', '')}") logger.info(msg) logger.debug("Exchange message body=\n%s\n", serder.pretty()) raise MissingSignatureError(msg) e = Pather(parts=["e"]) kwa = dict() attachments = [] for p in ptds: pattach = bytearray(p) pather = Pather(qb64b=pattach, strip=True) if pather.startswith(e): np = pather.strip(e) attachments.append((np, pattach)) kwa["attachments"] = attachments if essrs: kwa["essr"] = b''.join([texter.raw for texter in essrs]) if isinstance(serder.seals, str): if 'essr' not in kwa: raise ValidationError("at least one essr attachment is required") essr = kwa['essr'] dig = serder.seals diger = Diger(qb64=dig) if not diger.verify(ser=essr): raise ValidationError(f"essr diger={diger.qb64} is invalid against content") # Perform behavior specific verification, think IPEX chaining requirements try: if not behavior.verify(serder=serder, **kwa): logger.error("exn event for route %s failed behavior verification. said=%s", route, serder.said) logger.debug(f"Event=\n%s\n", serder.pretty()) return except AttributeError: logger.debug("Behavior for %s missing or does not have verify for said %s", route, serder.said) logger.debug("Exn Event Body=\n%s\n", serder.pretty()) # Always persist events self.logEvent(serder, ptds, tsgs, cigars, essrs) self.cues.append(dict(kin="saved", said=serder.said)) # Execute any behavior specific handling, not sure if this should be different than verify try: behavior.handle(serder=serder, **kwa) except AttributeError: logger.debug("Behavior for %s missing or does not have handle for SAID=%s", route, serder.said) logger.debug("Event=\n%s\n", serder.pretty())
[docs] def processEscrow(self): """ Process all escrows for `exn` messages """ self.processEscrowPartialSigned()
[docs] def escrowPSEvent(self, serder, tsgs, pathed): """ Escrow event that does not have enough signatures. Parameters: serder (Serder): instance of event tsgs (list): quadlet of prefixer seqner, saider, sigers pathed (list): list of bytes of attached paths """ dig = serder.said for prefixer, seqner, ssaider, sigers in tsgs: # iterate over each tsg quadkeys = (serder.said, prefixer.qb64, f"{seqner.sn:032x}", ssaider.qb64) for siger in sigers: self.hby.db.esigs.add(keys=quadkeys, val=siger) self.hby.db.epsd.put(keys=(dig,), val=Dater()) self.hby.db.epath.pin(keys=(dig,), vals=[bytes(p) for p in pathed]) return self.hby.db.epse.put(keys=(dig,), val=serder)
[docs] def processEscrowPartialSigned(self): """ Process escrow of partially signed messages """ for (dig,), serder in self.hby.db.epse.getTopItemIter(): try: tsgs = [] klases = (Prefixer, Seqner, Saider) args = ("qb64", "snh", "qb64") sigers = [] dtnow = helping.nowUTC() dater = self.hby.db.epsd.get(keys=(dig,)) if dater is None: raise ValidationError("Missing exn escrowed event datetime " f"at dig = {dig}.") dte = dater.datetime if (dtnow - dte) > datetime.timedelta(seconds=self.TimeoutPSE): # escrow stale so raise ValidationError which unescrows below raise ValidationError("Stale exn event escrow " f"at dig = {dig}.") old = None # empty keys for keys, siger in self.hby.db.esigs.getTopItemIter(keys=(dig, "")): quad = keys[1:] if quad != old: # new tsg if sigers: # append tsg made for old and sigers prefixer, seqner, saider = helping.klasify(sers=old, klases=klases, args=args) tsgs.append((prefixer, seqner, saider, sigers)) sigers = [] old = quad sigers.append(siger) if sigers and old: prefixer, seqner, saider = helping.klasify(sers=old, klases=klases, args=args) tsgs.append((prefixer, seqner, saider, sigers)) pathed = [bytearray(p.encode("utf-8")) for p in self.hby.db.epath.get(keys=(dig,))] essrs = [texter for texter in self.hby.db.essrs.get(keys=(dig,))] self.processEvent(serder=serder, tsgs=tsgs, ptds=pathed, essrs=essrs) except MissingSignatureError as ex: if logger.isEnabledFor(logging.TRACE): logger.trace("Exchange partially signed unescrow failed: %s\n", ex.args[0]) logger.debug(f"Event body=\n%s\n", serder.pretty()) except Exception as ex: self.hby.db.epse.rem(dig) self.hby.db.epsd.rem(dig) self.hby.db.esigs.rem(dig) if logger.isEnabledFor(logging.DEBUG): logger.exception("Exchange partially signed unescrowed: %s", ex.args[0]) else: logger.error("Exchange partially signed unescrowed: %s", ex.args[0]) else: self.hby.db.epse.rem(dig) self.hby.db.esigs.rem(dig) logger.info("Exchanger unescrow succeeded in valid exchange: creder=%s", serder.said) logger.debug("Event=\n%s\n", serder.pretty())
def logEvent(self, serder, pathed=None, tsgs=None, cigars=None, essrs=None): dig = serder.said pdig = serder.ked['p'] pathed = pathed or [] tsgs = tsgs or [] cigars = cigars or [] essrs = essrs or [] for prefixer, seqner, ssaider, sigers in tsgs: # iterate over each tsg quadkeys = (serder.said, prefixer.qb64, f"{seqner.sn:032x}", ssaider.qb64) for siger in sigers: self.hby.db.esigs.add(keys=quadkeys, val=siger) for cigar in cigars: self.hby.db.ecigs.add(keys=(dig,), val=(cigar.verfer, cigar)) diger = Diger(qb64=serder.said) self.hby.db.epath.pin(keys=(dig,), vals=[bytes(p) for p in pathed]) for texter in essrs: self.hby.db.essrs.add(keys=(dig,), val=texter) if pdig: self.hby.db.erpy.pin(keys=(pdig,), val=diger) self.hby.db.exns.put(keys=(dig,), val=serder)
[docs] def lead(self, hab, said): """ Determines is current member represented by hab is the lead of an exn message Lead is the signer of the exn with the lowest signing index Parameters: hab (Hab): Habitat for sending of exchange message represented by SAID said (str): qb64 SAID of exchange message Returns: bool: True means hab is the lead """ from ..app import GroupHab if not isinstance(hab, GroupHab): return True keys = [verfer.qb64 for verfer in hab.kever.verfers] tsgs = fetchTsgs(self.hby.db.esigs, Diger(qb64=said)) if not tsgs: # otherwise it contains a list of sigs return False (_, _, _, sigers) = tsgs[0] windex = min([siger.index for siger in sigers]) # True if Elected to send an EXN to its receiver return hab.mhab.kever.verfers[0].qb64 == keys[windex]
[docs] def complete(self, said): """ Args: said (str): qb64 said of exchange message to check status Returns: bool: True means exchange message is has been saved """ serder = self.hby.db.exns.get(keys=(said,)) if not serder: return False else: if serder.said != said: raise ValidationError(f"invalid exchange escrowed event {serder.said}-{said}") return True
[docs] def exchangeOld(*, sender="", receiver="", xid="", prior="", route="", modifiers=None, attributes=None, diger=None, embeds=None, stamp=None, version=Version, pvrsn=None, gvrsn=None, kind=Kinds.json,): """ Create an `exn` message with the specified route and payload Parameters: sender (str): qb64 of sender identifier (AID) receiver (str): qb64 of receiver identifier (AID) xid (str): qb64 of exchange ID which is SAID of exchange inception 'xip' if any prior (str): qb64 of prior exchange event including 'xip" if any route (str): '/' delimited path identifier of data flow handler (behavior) to processs the reply if any (equivalent of url path to resource) modifiers (dict): modifiers field map (equvalent of http query string) attributes (dict): attributes field map (payload body) stamp (str): date-time-stamp RFC-3339 profile of ISO-8601 datetime of creation of message or data, default is now. version (Versionage): KERI protocol default version if psvrsn is None pvrsn (Versionage): KERI protocol version gvrsn (Versionage): CESR Genus version for attachment group codes or nesting group code (useful when serder.gvrsn < 2) gvrsn = max(svrsn, gvrsn) where svrsn = serder.gvrsn if serder.gvrsn else serder.pvrsn kind (str): serialization for key event message one of Kinds ("json","cbor","mgpk","cesr") diger (Diger): qb64 digest of attributes section (payload) embeds (dict): named embeded KERI event CESR stream with attachments """ pvrsn = pvrsn if pvrsn is not None else version vs = versify(pvrsn=pvrsn, kind=kind, size=0, gvrsn=gvrsn) #ilk = Ilks.exn #dt = stamp if stamp is not None else helping.nowIso8601() #xid = xid if xid is not None else "" #p = prior if prior is not None else "" #ri = receiver if receiver is not None else "" #modifiers = modifiers if modifiers is not None else {} end = bytearray() if pvrsn.major == Vrsn_1_0.major: embeds = embeds if embeds is not None else {} e = dict() for label, msg in embeds.items(): serder = Sadder(raw=msg) e[label] = serder.ked atc = bytes(msg[serder.size:]) if not atc: continue pathed = bytearray() pather = Pather(parts=["e", label]) pathed.extend(pather.qb64b) pathed.extend(atc) if len(pathed) // 4 < 4096: end.extend(Counter(Codens.PathedMaterialCouples, count=(len(pathed) // 4), version=Vrsn_1_0).qb64b) else: end.extend(Counter(Codens.BigPathedMaterialCouples, count=(len(pathed) // 4), version=Vrsn_1_0).qb64b) end.extend(pathed) if e: e["d"] = "" _, e = Saider.saidify(sad=e, label=Saids.d) if diger is None: #attrs = dict() if receiver: # not (empty or None) attributes = attributes if attributes is not None else {} attributes['i'] = receiver #attrs['i'] = receiver #attrs |= attributes else: # only in v1 exn can the attributes field 'a' be either a said or # a field map. In v2 it must be a field map. attributes = diger.qb64 # SAID of ESSR encrypted attachment sad = dict(v=vs, t=Ilks.exn, d="", # computed by SerderKERI init i=sender if sender is not None else "", rp=receiver if receiver is not None else "", p=prior if prior is not None else "", dt=stamp if stamp is not None else helping.nowIso8601(), r=route if route is not None else "", q=modifiers if modifiers is not None else {}, # q field required a=attributes if attributes is not None else {}, e=e) else: if end or diger: raise ValueError(f"Invalid diger or embeds not supported in " f"version {pvrsn.major} exchange") sad = dict(v=vs, t=Ilks.exn, d="", # computed by SerderKERI init i=sender if sender is not None else "", ri=receiver if receiver is not None else "", x=xid if xid is not None else "", p=prior if prior is not None else "", dt=stamp if stamp is not None else helping.nowIso8601(), r=route if route is not None else "", q=modifiers if modifiers is not None else {}, # q field required a=attributes if attributes is not None else {} ) return SerderKERI(sad=sad, makify=True) # return serialized ked
#return SerderKERI(sad=sad, makify=True), end # return serialized ked
[docs] def specialExchange(*, sender="", receiver="", xid="", prior="", route="", modifiers=None, attributes=None, diger=None, embeds=None, stamp=None, version=Vrsn_1_0, pvrsn=None, gvrsn=None, kind=Kinds.json,): """Create an `exn` with either an ESSR attachment or embeds with path attachment as determined by the presence of diger or embeds parameters repectively Parameters:: sender (str): qb64 of sender identifier (AID) receiver (str): qb64 of receiver identifier (AID) xid (str): qb64 of exchange ID which is SAID of exchange inception 'xip' if any prior (str): qb64 of prior exchange event including 'xip" if any route (str): '/' delimited path identifier of data flow handler (behavior) to processs the reply if any (equivalent of url path to resource) modifiers (dict): modifiers field map (equvalent of http query string) attributes (dict): attributes field map (payload body) stamp (str): date-time-stamp RFC-3339 profile of ISO-8601 datetime of creation of message or data, default is now. version (Versionage): KERI protocol default version if psvrsn is None pvrsn (Versionage): KERI protocol version gvrsn (Versionage): CESR Genus version for attachment group codes or nesting group code (useful when serder.gvrsn < 2) gvrsn = max(svrsn, gvrsn) where svrsn = serder.gvrsn if serder.gvrsn else serder.pvrsn kind (str): serialization for key event message one of Kinds ("json","cbor","mgpk","cesr") diger (Diger): qb64 digest of attributes section (payload) embeds (dict): named embeded KERI event CESR stream with attachments Returns:: embedded (SerderKeri, bytearray): of form (exchange, attachments) where exchange is serder of exchange message and atc is serialized path attachments of embeds """ pvrsn = pvrsn if pvrsn is not None else version vs = versify(pvrsn=pvrsn, kind=kind, size=0, gvrsn=gvrsn) #ilk = Ilks.exn #dt = stamp if stamp is not None else helping.nowIso8601() #xid = xid if xid is not None else "" #p = prior if prior is not None else "" #ri = receiver if receiver is not None else "" #modifiers = modifiers if modifiers is not None else {} if pvrsn.major == Vrsn_1_0.major: end = bytearray() embeds = embeds if embeds is not None else {} e = dict() for label, msg in embeds.items(): serder = Sadder(raw=msg) e[label] = serder.ked atc = bytes(msg[serder.size:]) if not atc: continue pathed = bytearray() pather = Pather(parts=["e", label]) pathed.extend(pather.qb64b) pathed.extend(atc) if len(pathed) // 4 < 4096: end.extend(Counter(Codens.PathedMaterialCouples, count=(len(pathed) // 4), version=Vrsn_1_0).qb64b) else: end.extend(Counter(Codens.BigPathedMaterialCouples, count=(len(pathed) // 4), version=Vrsn_1_0).qb64b) end.extend(pathed) if e: e["d"] = "" _, e = Saider.saidify(sad=e, label=Saids.d) if diger is None: if receiver: # not (empty or None) attributes = attributes if attributes is not None else {} attributes['i'] = receiver else: # only in v1 exn can the attributes field 'a' be either a said or # a field map. In v2 it must be a field map. attributes = diger.qb64 # SAID of ESSR encrypted attachment sad = dict(v=vs, t=Ilks.exn, d="", # computed by SerderKERI init i=sender if sender is not None else "", rp=receiver if receiver is not None else "", p=prior if prior is not None else "", dt=stamp if stamp is not None else helping.nowIso8601(), r=route if route is not None else "", q=modifiers if modifiers is not None else {}, # q field required a=attributes if attributes is not None else {}, e=e) else: raise ValueError(f"Invalid specialExchange not supported in version" f" {pvrsn.major} exchange") return SerderKERI(sad=sad, makify=True), end # return serialized ked
[docs] def cloneMessage(hby, said): """ Load and verify signatures on message exn Parameters: hby (Habery): database environment from which to clone message said (str): qb64 SAID of message exn to load Returns: tuple: (serder, list) of message exn and pathed signatures on embedded attachments """ exn = hby.db.exns.get(keys=(said,)) if exn is None: return None, None verify(hby=hby, serder=exn) pathed = dict() e = Pather(parts=["e"]) for p in hby.db.epath.get(keys=(exn.said,)): pb = bytearray(p.encode("utf-8")) pather = Pather(qb64b=pb, strip=True) if pather.startswith(e): np = pather.strip(e) nesting(np.rparts, pathed, pb) # no unit test for this return exn, pathed
[docs] def serializeMessage(hby, said, framed=False): """Fetch message and attachments from hby.db by said and then serialize them Parameters:: hby (Habery): environment with db said (str): of message framed (bool): True means may assume each message plus its attachments is isolated as frame when parsing so do not need attachment group when messagizing False means may not assume eash message plus its attachments is isolated as frame when parsing so do need attachment group when messagizing Returns:: msg (bytearray): message by said with attachments """ atc = bytearray() exn = hby.db.exns.get(keys=(said,)) if exn is None: return None, None atc.extend(exn.raw) tsgs, cigars = verify(hby=hby, serder=exn) if len(tsgs) > 0: for (prefixer, seqner, saider, sigers) in tsgs: atc.extend(Counter(Codens.TransIdxSigGroups, count=1, version=Vrsn_1_0).qb64b) atc.extend(prefixer.qb64b) atc.extend(seqner.qb64b) atc.extend(saider.qb64b) atc.extend(Counter(Codens.ControllerIdxSigs, count=len(sigers), version=Vrsn_1_0).qb64b) for siger in sigers: atc.extend(siger.qb64b) if len(cigars) > 0: atc.extend(Counter(Codens.NonTransReceiptCouples, count=len(cigars), version=Vrsn_1_0).qb64b) for cigar in cigars: if cigar.verfer.code not in NonTransDex: raise ValueError("Attempt to use tranferable prefix={} for " "receipt.".format(cigar.verfer.qb64)) atc.extend(cigar.verfer.qb64b) atc.extend(cigar.qb64b) # Smash the pathed components on the end for p in hby.db.epath.get(keys=(exn.said,)): atc.extend(Counter(Codens.PathedMaterialCouples, count=(len(p) // 4), version=Vrsn_1_0).qb64b) atc.extend(p.encode("utf-8")) msg = bytearray() if len(atc) % 4: raise ValueError("Invalid attachments size={}, nonintegral" " quadlets.".format(len(atc))) if not framed: msg.extend(Counter(Codens.AttachmentGroup, count=(len(atc) // 4), version=Vrsn_1_0).qb64b) msg.extend(atc) return msg
[docs] def nesting(paths, acc, val): """Nesting Pather parts Parameters: paths (list[list]): list of path parts """ if len(paths) == 0: return val else: first_value = paths[0] nacc = dict() acc[first_value] = nesting(paths[1:], nacc, val) return acc
[docs] def verify(hby, serder): """ Verify that the signatures in the database are valid for the provided exn Parameters: hby (Habery): database environment from which to verify message serder (Serder): exn serder to load and verify signatures for Returns: bool: True means threshold satisfyig signatures were loaded and verified successfully """ tsgs = [] klases = (Prefixer, Seqner, Saider) args = ("qb64", "snh", "qb64") sigers = [] old = None # empty keys for keys, siger in hby.db.esigs.getTopItemIter(keys=(serder.said, "")): quad = keys[1:] if quad != old: # new tsg if sigers: # append tsg made for old and sigers prefixer, seqner, saider = helping.klasify(sers=old, klases=klases, args=args) tsgs.append((prefixer, seqner, saider, sigers)) sigers = [] old = quad sigers.append(siger) if sigers and old: prefixer, seqner, saider = helping.klasify(sers=old, klases=klases, args=args) tsgs.append((prefixer, seqner, saider, sigers)) accepted = False for prefixer, seqner, ssaider, sigers in tsgs: if prefixer.qb64 not in hby.kevers or hby.kevers[prefixer.qb64].sn < seqner.sn: msg = f"Unable to find sender {prefixer.qb64} in kevers for evt = {serder.said}" logger.info(msg) logger.debug("Exn Body=\n%s\n", serder.pretty()) raise MissingSignatureError(msg) # Verify the signatures are valid and that the signature threshold as of the signing event is met tholder, verfers = hby.db.resolveVerifiers(pre=prefixer.qb64, sn=seqner.sn, dig=ssaider.qb64) _, indices = verifySigs(serder.raw, sigers, verfers) if not tholder.satisfy(indices): # We still don't have all the sigers, need to escrow msg = f"Not enough signatures in idx={indices} for evt = {serder.said}" logger.info(msg) logger.debug("Exn Body=\n%s\n", serder.pretty()) raise MissingSignatureError(msg) accepted = True cigars = hby.db.ecigs.get(keys=(serder.said,)) for cigar in cigars: if not cigar.verfer.verify(cigar.raw, serder.raw): # cig not verify msg = f"Failure satisfying exn on cigs for {cigar} for evt = {serder.said}" logger.info(msg) logger.debug("Exn Body=\n%s\n", serder.pretty()) raise MissingSignatureError(msg) accepted = True if not accepted: msg = f"No valid signatures stored for evt = {serder.said}" logger.info(msg) logger.debug("Exn Body=\n%s\n", serder.pretty()) raise MissingSignatureError(msg) return tsgs, cigars