# -*- coding: utf-8 -*-
"""
keri.core.eventing module
"""
import datetime
import logging
from collections import namedtuple
from dataclasses import asdict
from urllib.parse import urlsplit
from math import ceil
from ordered_set import OrderedSet as oset
from hio.help import decking, ogler
from ..kering import (MissingEntryError, UntrustedKeyStateSource,
ValidationError, MissingSignatureError,
MissingWitnessSignatureError, UnverifiedReplyError,
MissingDelegationError, OutOfOrderError,
LikelyDuplicitousError, UnverifiedWitnessReceiptError,
UnverifiedReceiptError, UnverifiedTransferableReceiptError,
QueryNotFoundError, MisfitEventSourceError,
MissingDelegableApprovalError, Version, Versionage,
TraitDex, Vrsn_1_0, Vrsn_2_0, GVC_1_0, GVC_2_0,
Roles, Schemes, Ilks, versify, Kinds)
from ..help import helping
from .coring import (PreDex, DigDex, NonTransDex, NumDex, Prefixer,
Diger, Number, Seqner, Cigar, Dater, Noncer,
Verfer, Diger, Prefixer, Tholder, Texter)
from .counting import Counter, Codens
from .structing import (Structor, Sealer, SealEvent, SealSource, SealLast, BlindState,
BoundState, TypeMedia, StateEstEvent)
from .indexing import Siger
from .serdering import SerderKERI
from ..db import Baser, dgKey, snKey
from ..recording import (EndpointRecord, EventSourceRecord, KeyStateRecord,
LocationRecord, OobiRecord, ObservedRecord,
StateEERecord)
logger = ogler.getLogger()
EscrowTimeoutPS = 3600 # seconds for partial signed escrow timeout
MaxIntThold = 2 ** 32 - 1
# Location of last establishment key event: sn is int, dig is qb64 digest
LastEstLoc = namedtuple("LastEstLoc", 's d')
# Future make Cues dataclasses instead of dicts. Dataclasses so may be converted
# to/from dicts easily example: dict(kin="receipt", serder=serder)
[docs]
def simple(n):
"""
Returns int as simple majority of n when n >=1
otherwise returns 0
Parameters:
n is int total number of elements
"""
return min(max(0, n), (max(0, n) // 2) + 1)
[docs]
def ample(n, f=None, weak=True):
"""
Returns int as sufficient immune (ample) majority of n when n >=1
otherwise returns 0
Parameters:
n is int total number of elements
f is int optional fault number
weak is Boolean
If f is not None and
weak is True then minimize m for f
weak is False then maximize m for f that satisfies n >= 3*f+1
Else
weak is True then find maximum f and minimize m
weak is False then find maximum f and maximize m
n,m,f are subject to
f >= 1 if n > 0
n >= 3*f+1
(n+f+1)/2 <= m <= n-f
"""
n = max(0, n) # no negatives
if f is None:
f1 = max(1, max(0, n - 1) // 3) # least floor f subject to n >= 3*f+1
f2 = max(1, ceil(max(0, n - 1) / 3)) # most ceil f subject to n >= 3*f+1
if weak: # try both fs to see which one has lowest m
return min(n, ceil((n + f1 + 1) / 2), ceil((n + f2 + 1) / 2))
else:
return min(n, max(0, n - f1, ceil((n + f1 + 1) / 2)))
else:
f = max(0, f)
m1 = ceil((n + f + 1) / 2)
m2 = max(0, n - f)
if m2 < m1 and n > 0:
raise ValueError("Invalid f={} is too big for n={}.".format(f, n))
if weak:
return min(n, m1, m2)
else:
return min(n, max(m1, m2))
# Utility functions for extracting groups of primitives
# bytearray of memoryview makes a copy so does not delete underlying data
# behind memory view but del on bytearray itself does delete bytearray
[docs]
def deWitnessCouple(data, strip=False):
"""
Returns tuple of (diger, wiger) extracted from bytes or bytearray
that hold concatenated data couple where:
diger is Diger instance
wiger is Siger instance
Couple is dig+wig where:
dig is receipted event digest
wig is indexed signature made with key pair derived from witness nontrans
identifier prefix from witness list. Index is offset into witness
list of latest establishment event for receipted event.
Parameters:
data is couple of bytes concatenation of dig+wig from receipt
deletive is Boolean True means delete from data each part as parsed
Only useful if data is bytearray from front of stream
Witness couple is used for escrows of unverified witness recipts signed by
nontransferable witness prefix keys with indexed signatures where index
is offset into associated witness list. At time of escrow receipted event
may not be in KEL so need the dig to look up event and then look up witness
list from key state.
"""
if isinstance(data, memoryview):
data = bytes(data)
if hasattr(data, "encode"):
data = data.encode("utf-8") # convert to bytes
diger = Diger(qb64b=data, strip=strip)
if not strip:
data = data[len(diger.qb64b):]
wiger = Siger(qb64b=data, strip=strip)
return (diger, wiger)
[docs]
def deReceiptCouple(data, strip=False):
"""
Returns tuple of (prefixer, cigar) from concatenated bytes or bytearray
of data couple made up of qb64 or qb64b versions of pre+cig where:
pre is nontransferable identifier prefix of receiptor
cig is nonindexed signature made with key pair derived from pre
Couple is used for receipts signed by nontransferable prefix keys
Parameters:
data is couple of bytes concatenation of pre+sig from receipt
strip is Boolean True means delete from data each part as parsed
Only useful if data is bytearray from front of stream
Raises error if not bytearray
"""
if isinstance(data, memoryview):
data = bytes(data)
if hasattr(data, "encode"):
data = data.encode("utf-8") # convert to bytes
prefixer = Prefixer(qb64b=data, strip=strip)
if not strip:
data = data[len(prefixer.qb64b):]
cigar = Cigar(qb64b=data, strip=strip)
return (prefixer, cigar)
[docs]
def deSourceCouple(data, strip=False):
"""
Returns tuple of (number, diger) from concatenated bytes or bytearray
of data couple made up of qb64 or qb64b versions of snu+dig where:
snu is sn of delegator/issuer source event
dig is digest of delegator/issuer source event
Couple is used for delegated/issued event attachment of delegator/issuer evt
Parameters:
data is couple of bytes concatenation of pre+sig from receipt
strip is Boolean True means delete from data each part as parsed
Only useful if data is bytearray from front of stream
Raises error if not bytearray
"""
if isinstance(data, memoryview):
data = bytes(data)
if hasattr(data, "encode"):
data = data.encode("utf-8") # convert to bytes
number = Number(qb64b=data, strip=strip)
if not strip:
data = data[len(number.qb64b):]
diger = Diger(qb64b=data, strip=strip)
return (number, diger)
[docs]
def deReceiptTriple(data, strip=False):
"""
Returns tuple of (diger, prefixer, cigar) from concatenated bytes or bytearray
of data triple made up of qb64 or qb64b versions of dig+pre+cig where:
dig is receipted event digest
pre is nontransferable identifier prefix of receiptor
cig is nonindexed signature made with key pair derived from pre
Triple is used for escrows of unverified receipts signed by nontransferable
prefix keys
Parameters:
data is triple of bytes concatenation of dig+pre+cig from receipt
deletive is Boolean True means delete from data each part as parsed
Only useful if data is bytearray from front of stream
"""
if isinstance(data, memoryview):
data = bytes(data)
if hasattr(data, "encode"):
data = data.encode("utf-8") # convert to bytes
diger = Diger(qb64b=data, strip=strip)
if not strip:
data = data[len(diger.qb64b):]
prefixer = Prefixer(qb64b=data, strip=strip)
if not strip:
data = data[len(prefixer.qb64b):]
cigar = Cigar(qb64b=data, strip=strip)
return (diger, prefixer, cigar)
[docs]
def deTransReceiptQuadruple(data, strip=False):
"""
Returns tuple (quadruple) of (prefixer, number, diger, siger) from
concatenated bytes or bytearray of quadruple made up of qb64 or qb64b
versions of spre+ssnu+sdig+sig.
Quadruple is used for receipts signed by transferable prefix keys. Recept
for event that is in kel where event is given by context or key
Parameters:
quadruple is bytes concatenation of pre+snu+dig+sig from receipt
deletive is Boolean True means delete from data each part as parsed
Only useful if data is bytearray from front of stream
"""
if isinstance(data, memoryview):
data = bytes(data)
if hasattr(data, "encode"):
data = data.encode("utf-8") # convert to bytes
prefixer = Prefixer(qb64b=data, strip=strip)
if not strip:
data = data[len(prefixer.qb64b):]
number = Number(qb64b=data, strip=strip)
if not strip:
data = data[len(number.qb64b):]
diger = Diger(qb64b=data, strip=strip)
if not strip:
data = data[len(diger.qb64b):]
siger = Siger(qb64b=data, strip=strip)
return (prefixer, number, diger, siger)
[docs]
def deTransReceiptQuintuple(data, strip=False):
"""
Returns tuple of (ediger, seal prefixer, seal number, seal diger, siger)
from concatenated bytes or bytearray of quintuple made up of qb64 or qb64b
versions of quntipuple given by concatenation of edig+spre+ssnu+sdig+sig.
Quintuple is used for unverified escrows of validator receipts signed
by transferable prefix keys. Receipt for event that is not yet in KEL where
event is given by event digest (ediger)
Parameters:
quintuple is bytes concatenation of edig+spre+ssnu+sdig+sig from receipt
deletive is Boolean True means delete from data each part as parsed
Only useful if data is bytearray from front of stream
"""
if isinstance(data, memoryview):
data = bytes(data)
if hasattr(data, "encode"):
data = data.encode("utf-8") # convert to bytes
if isinstance(data, tuple) and len(data) == 5:
return data # already typed CESR objects
ediger = Diger(qb64b=data, strip=strip) # diger of receipted event
if not strip:
data = data[len(ediger.qb64b):]
sprefixer = Prefixer(qb64b=data, strip=strip) # prefixer of recipter
if not strip:
data = data[len(sprefixer.qb64b):]
snumber = Number(qb64b=data, strip=strip) # seqnumber of receipting event
if not strip:
data = data[len(snumber.qb64b):]
sdiger = Diger(qb64b=data, strip=strip) # diger of receipting event
if not strip:
data = data[len(sdiger.qb64b):]
siger = Siger(qb64b=data, strip=strip) # indexed siger of event
return ediger, sprefixer, snumber, sdiger, siger
[docs]
def verifySigs(raw, sigers, verfers):
"""
Returns tuple of (vsigers, vindices) where:
vsigers is list of unique verified sigers with assigned verfer
vindices is list of indices from those verified sigers
The returned vsigers and vindices may be used for threshold validation
Assigns appropriate verfer from verfers to each siger based on siger index
If no signatures verify then sigers and indices are empty
Parameters:
raw (bytes) signed data
sigers is list of indexed Siger instances (signatures)
verfers is list of Verfer instance (public keys)
"""
if sigers is None:
sigers = []
# Ensure no duplicate sigers by using set math on sigers' sigs otherwise
# indices count for threshold will be erroneous. Does not modify in place
# passed in sigers list, but instead depends on caller to use indices to
# modify its copy to filter out unverifiable or duplicate sigers
usigs = oset([siger.qb64 for siger in sigers])
usigers = [Siger(qb64=sig) for sig in usigs]
# verify indexes of attached signatures against verifiers and assign
# verfer to each usiger
uvsigers = []
for siger in usigers:
if siger.index >= len(verfers):
logger.info(f"Skipped sig: index={siger.index} too large")
continue
siger.verfer = verfers[siger.index] # assign verfer
uvsigers.append(siger)
# create lists of unique verified signatures and indices
vindices = []
vsigers = []
for siger in uvsigers:
if siger.verfer.verify(siger.raw, raw):
vindices.append(siger.index)
vsigers.append(siger)
return (vsigers, vindices)
[docs]
def validateSigs(serder, sigers, verfers, tholder):
"""
Validates signatures given by sigers using keys given by verfers on msg
given by serder subject to threshold given by tholder. Returns subset of
valid signatures for storage.
Returns:
result (tuple): (sigers, valid) where:
sigers (list): subset of of provided sigers of verified signatures
on serder using verfers
valid (bool): True means threshold from tholder satisfied by sigers,
False otherwise.
Parameters:
serder (SerderKERI): instance of message
sigers (Iterable): Siger instances of indexed signatures.
Index is offset into verfers list each providing verification key
verfers (Iterable): Verfer instances of keys
tholder (Tholder): instance of signing threshold (sith)
seqner is Seqner instance of delegating event sequence number.
If this event is not delegated then seqner is ignored
diger is Diger instance of of delegating event digest.
If this event is not delegated then diger is ignored
"""
valid = False
if len(verfers) < tholder.size:
raise ValidationError("Invalid sith = {} for keys = {}."
"".format(tholder.sith,
[verfer.qb64 for verfer in verfers]))
# get unique verified sigers and indices lists from sigers list
sigers, indices = verifySigs(raw=serder.raw, sigers=sigers, verfers=verfers)
# sigers now have .verfer assigned
# check if satisfies threshold for fully signed
if not indices: # must have a least one verified sig
raise ValidationError("No verified signatures for message={}."
"".format(serder.ked))
valid = tholder.satisfy(indices)
return (sigers, valid)
[docs]
def state(pre,
sn,
pig,
dig,
fn,
eilk,
keys,
eevt,
stamp=None, # default current datetime
sith=None, # default based on keys
ndigs=None,
nsith=None,
toad=None, # default based on wits
wits=None, # default to []
cnfg=None, # default to []
dpre=None,
version=Version,
kind=Kinds.json,
intive = False,
):
"""
Returns instance of KeyStateRecord in support of key state notification messages.
Utility function to automate creation embedded key static notices
Parameters:
pre (str): identifier prefix qb64
sn (int): sequence number of latest event
pig (str): SAID qb64 of prior event
dig (str): SAID qb64 of latest (current) event
fn (int): first seen ordinal number of latest event
eilk (str): event (message) type (ilk) of latest (current) event
keys (list): qb64 signing keys
eevt (StateEstEvent): namedtuple (s,d,wr,wa) for latest est event
s = sn of est event
d = SAID of est event
wr = witness remove list (cuts)
wa = witness add list (adds)
stamp (str | None): date-time-stamp RFC-3339 profile of ISO-8601 datetime of
creation of message or data
sith sith (int | str | list | None): current signing threshold input to Tholder
ndigs (list | None): current signing key digests qb64
nsith int | str | list | None): next signing threshold input to Tholder
toad (int | str | None): witness threshold number if str then hex str
wits (list | None): prior witness identifier prefixes qb64
cnfg (list | None): strings from TraitDex configuration trait strings
dpre (str | None): identifier prefix qb64 delegator if any
If None then dpre in state is empty ""
version (Version): KERI protocol version string
kind (str): serialization kind from Serials
intive (bool): True means sith, nsith, and toad are serialized as ints
instead of hex str when numeric threshold
"""
sner = Number(num=sn) # raises InvalidValueError if sn < 0
fner = Number(num=fn) # raises InvalidValueError if fn < 0
if eilk not in (Ilks.icp, Ilks.rot, Ilks.ixn, Ilks.dip, Ilks.drt):
raise ValueError(f"Invalid event type et={eilk} in key state.")
if stamp is None:
stamp = helping.nowIso8601()
if sith is None:
sith = "{:x}".format(max(1, ceil(len(keys) / 2)))
tholder = Tholder(sith=sith)
if tholder.num is not None and tholder.num < 1:
raise ValueError(f"Invalid sith = {tholder.num} less than 1.")
if tholder.size > len(keys):
raise ValueError(f"Invalid sith = {tholder.num} for keys = {keys}")
if ndigs is None:
ndigs = []
if nsith is None:
nsith = max(0, ceil(len(ndigs) / 2))
ntholder = Tholder(sith=nsith)
if ntholder.num is not None and ntholder.num < 0:
raise ValueError(f"Invalid nsith = {ntholder.num} less than 0.")
if ntholder.size > len(ndigs):
raise ValueError(f"Invalid nsith = {ntholder.num} for keys = {ndigs}")
wits = wits if wits is not None else []
witset = oset(wits)
if len(witset) != len(wits):
raise ValueError(f"Invalid wits = {wits}, has duplicates.")
if toad is None:
if not witset:
toad = 0
else:
toad = max(1, ceil(len(witset) / 2))
if toad is None:
if not witset:
toad = 0
else: # compute default f and m for len(wits)
toad = ample(len(witset))
toader = Number(num=toad)
if witset:
if toader.num < 1 or toader.num > len(witset): # out of bounds toad
raise ValueError(f"Invalid toad = {toader.num} for wits = {witset}")
else:
if toader.num != 0: # invalid toad
raise ValueError(f"Invalid toad = {toader.num} for wits = {witset}")
if not eevt or not isinstance(eevt, StateEstEvent):
raise ValueError(f"Missing or invalid latest est event = {eevt} for key "
f"state.")
eesner = Number(numh=eevt.s) # if not whole number raises InvalidValueError
# cuts is relative to prior wits not current wits provided here
cuts = eevt.br if eevt.br is not None else []
cutset = oset(cuts)
if len(cutset) != len(cuts): # duplicates in cuts
raise ValueError(f"Invalid cuts = {cuts}, has "
f"duplicates, in latest est event, .")
# adds is relative to prior wits not current wits provided here
adds = eevt.ba if eevt.ba is not None else []
addset = oset(adds)
if len(addset) != len(adds): # duplicates in adds
raise ValueError(f"Invalid adds = {adds}, has duplicates,"
f" in latest est event,.")
if cutset & addset: # non empty intersection
raise ValueError(f"Intersecting cuts = {cuts} and adds = {adds} in "
f"latest est event.")
ksr = KeyStateRecord(
vn=list(version), # version number as list [major, minor]
i=pre, # qb64 prefix
s=sner.numh, # lowercase hex string no leading zeros
p=pig,
d=dig,
f=fner.numh, # lowercase hex string no leading zeros
dt=stamp,
et=eilk,
kt=(tholder.num if intive and tholder.num is not None and
tholder.num <= MaxIntThold else tholder.sith),
k=keys, # list of qb64
nt=(ntholder.num if intive and ntholder.num is not None and
ntholder.num <= MaxIntThold else ntholder.sith),
n=ndigs,
bt=toader.num if intive and toader.num <= MaxIntThold else toader.numh,
b=wits, # list of qb64 may be empty
c=cnfg if cnfg is not None else [],
ee=StateEERecord._fromdict(eevt._asdict()), # latest est event dict
di=dpre if dpre is not None else "",
)
return ksr # return KeyStateRecord use asdict(ksr) to get dict version
# should remove intive as its not standard KERI so confusing and leads to errors
# this is an old feature that is now deprecated.
[docs]
def incept(keys,
*,
isith=None,
ndigs=None,
nsith=None,
toad=None,
wits=None,
cnfg=None,
data=None,
version=Version,
pvrsn=None,
gvrsn=None,
kind=Kinds.cesr,
code=None,
intive=False,
delpre=None,
):
"""
Returns serder of inception event message.
Utility function to automate creation of inception events.
Parameters:
keys (list): current signing keys qb64
isith (int | str | list | None): current signing threshold input to Tholder
ndigs (list | None): current signing key digests qb64
nsith (int | str | list | None): next signing threshold input to Tholder
toad (int | str | None): witness threshold number if str then hex str
wits (list | None): witness identifier prefixes qb64
cnfg (list | None): configuration traits from TraitDex
data (list | None): seal dicts
version (Versionage): KERI protocol default version if psvrsn is None
pvrsn (Versionage): KERI protocol version
gvrsn (Versionage): CESR genus vrsion
kind (str): serialization kind from Serials
code (str | None): derivation code for computed prefix
intive (bool): True means sith, nsith, and toad are serialized as ints
not hex str when numeric threshold. Most compact JSON representation
when Numbers are small because no quotes. Number accepts both.
delpre (str | None): delegator identifier prefix qb64. When not None
makes this a msg type "dip", delegated inception event.
"""
pvrsn = pvrsn if pvrsn is not None else version
vs = versify(pvrsn=pvrsn, kind=kind, size=0, gvrsn=gvrsn) # ensures cesr v2 only
ilk = Ilks.icp if delpre is None else Ilks.dip # inception or delegated inception
sner = Number(num=0) # sn for incept must be 0
if isith is None:
isith = max(1, ceil(len(keys) / 2))
tholder = Tholder(sith=isith)
if tholder.num is not None and tholder.num < 1:
raise ValueError(f"Invalid sith = {tholder.num} less than 1.")
if tholder.size > len(keys):
raise ValueError(f"Invalid sith = {tholder.num} for keys = {keys}")
if ndigs is None:
ndigs = []
if nsith is None:
nsith = max(0, ceil(len(ndigs) / 2))
ntholder = Tholder(sith=nsith)
if ntholder.num is not None and ntholder.num < 0:
raise ValueError(f"Invalid nsith = {ntholder.num} less than 0.")
if ntholder.size > len(ndigs):
raise ValueError(f"Invalid nsith = {ntholder.num} for keys = {ndigs}")
wits = wits if wits is not None else []
if len(oset(wits)) != len(wits):
raise ValueError(f"Invalid wits = {wits}, has duplicates.")
if toad is None:
if not wits:
toad = 0
else: # compute default f and m for len(wits)
toad = ample(len(wits))
toader = Number(num=toad)
if wits:
if toader.num < 1 or toader.num > len(wits): # out of bounds toad
raise ValueError(f"Invalid toad = {toader.num} for wits = {wits}")
else:
if toader.num != 0: # invalid toad
raise ValueError(f"Invalid toad = {toader.num} for wits = {wits}")
cnfg = cnfg if cnfg is not None else []
if not isinstance(cnfg, list):
raise ValueError(f"Expected list got {cnfg=}")
data = data if data is not None else []
if not isinstance(data, list):
raise ValueError(f"Expected list got {data=}")
ked = dict(v=vs, # version string
t=ilk,
d="", # qb64 SAID
i="", # qb64 prefix
s=sner.numh, # hex string no leading zeros lowercase
kt=(tholder.num if intive and tholder.num is not None and
tholder.num <= MaxIntThold else tholder.sith),
k=keys, # list of qb64
nt=(ntholder.num if intive and ntholder.num is not None and
ntholder.num <= MaxIntThold else ntholder.sith),
n=ndigs, # list of hashes qb64
bt=toader.num if intive and toader.num <= MaxIntThold else toader.numh,
b=wits, # list of qb64 may be empty
c=cnfg, # list of config ordered mappings may be empty
a=data, # list of seal dicts
)
pre = ""
saids = None
if delpre is not None: # delegated inception with ilk = dip
ked['di'] = delpre # SerderKERI .verify will ensure valid prefix
else: # non delegated
if (code is None or code not in DigDex) and len(keys) == 1: # use key[0] as default
ked["i"] = keys[0] # SerderKERI .verify will ensure valid prefix
if code is not None and code in PreDex: # use code to override all else
saids = {'i': code}
serder = SerderKERI(sad=ked, makify=True, saids=saids)
return serder
[docs]
def delcept(keys, delpre, **kwa):
"""
Returns serder of delegated inception event message.
Utility function to automate creation of delegated inception events.
Syntactic suger that calls incept but with delpre so ilk is dip.
Parameters:
keys (list): current signing keys qb64
isith (int | str | list | None): current signing threshold input to Tholder
ndigs (list | None): current signing key digests qb64
nsith int | str | list | None): next signing threshold input to Tholder
toad (int | str | None): witness threshold number if str then hex str
wits (list | None): witness identifier prefixes qb64
cnfg (list | None): configuration traits from TraitDex
data (list | None): seal dicts
version (Versionage): KERI protocol default version if psvrsn is None
pvrsn (Versionage): KERI protocol version
gvrsn (Versionage): CESR genus vrsion
kind (str): serialization kind from Serials
code (str | None): derivation code for computed prefix
intive (bool): True means sith, nsith, and toad are serialized as ints
not hex str when numeric threshold
delpre (str | None): delegator identifier prefix qb64. When not None
makes this a msg type "dip", delegated inception event.
"""
return incept(keys=keys, delpre=delpre, **kwa)
[docs]
def rotate(pre,
keys,
dig,
*,
ilk=Ilks.rot,
sn=1,
isith=None,
ndigs=None,
nsith=None,
toad=None,
wits=None, # prior existing wits
cuts=None,
adds=None,
cnfg=None,
data=None,
version=Version,
pvrsn=None,
gvrsn=None,
kind=Kinds.cesr,
intive = False,
):
"""
Returns serder of rotation event message.
Utility function to automate creation of rotation events.
Parameters:
pre (str): identifier prefix qb64
keys (list): current signing keys qb64
dig (str): SAID of previous event qb64
ilk (str): ilk of event. Must be in (Ilks.rot, Ilks.drt)
sn (int | str): sequence number int or hex str
isith (int | str | list | None): current signing threshold input to Tholder
ndigs (list | None): current signing key digests qb64
nsith (int | str | list | None): next signing threshold input to Tholder
toad (int | str | None): witness threshold number if str then hex str
wits (list | None): prior witness identifier prefixes qb64
cuts (list | None): witness prefixes to cut qb64
adds (list | None): witness prefixes to add qb64
cnfg (list | None): configuration traits from TraitDex
data (list | None): seal dicts
version (Versionage): KERI protocol default version if psvrsn is None
pvrsn (Versionage): KERI protocol version
gvrsn (Versionage): CESR genus vrsion
kind (str): serialization kind from Serials
intive (bool): True means sith, nsith, and toad are serialized as ints
instead of hex str when numeric threshold
"""
pvrsn = pvrsn if pvrsn is not None else version
vs = versify(pvrsn=pvrsn, kind=kind, size=0, gvrsn=gvrsn) # ensures cesr v2 only
ilk = ilk
if ilk not in (Ilks.rot, Ilks.drt):
raise ValueError(f"Invalid ilk ={ilk} for rot or drt.")
sner = Number(num=sn)
if sner.num < 1: # sn for rotate must be >= 1
raise ValueError(f"Invalid sn = 0x{sner.numh} for rot or drt.")
if isith is None:
isith = max(1, ceil(len(keys) / 2))
tholder = Tholder(sith=isith)
if tholder.num is not None and tholder.num < 1:
raise ValueError(f"Invalid sith = {tholder.num} less than 1.")
if tholder.size > len(keys):
raise ValueError(f"Invalid sith = {tholder.num} for keys = {keys}")
if ndigs is None:
ndigs = []
if nsith is None:
nsith = max(0, ceil(len(ndigs) / 2))
ntholder = Tholder(sith=nsith)
if ntholder.num is not None and ntholder.num < 0:
raise ValueError(f"Invalid nsith = {ntholder.num} less than 0.")
if ntholder.size > len(ndigs):
raise ValueError(f"Invalid nsith = {ntholder.num} for keys = {ndigs}")
wits = wits if wits is not None else []
witset = oset(wits)
if len(witset) != len(wits):
raise ValueError(f"Invalid wits = {wits}, has duplicates.")
cuts = cuts if cuts is not None else []
cutset = oset(cuts)
if len(cutset) != len(cuts):
raise ValueError(f"Invalid cuts = {cuts}, has duplicates.")
if (witset & cutset) != cutset: # some cuts not in wits
raise ValueError(f"Invalid cuts = {cuts}, not all members in wits.")
adds = adds if adds is not None else []
addset = oset(adds)
if len(addset) != len(adds):
raise ValueError(f"Invalid adds = {adds}, has duplicates.")
if witset & addset: # non empty intersection
raise ValueError(f"Intersecting wits = {wits} and adds = {adds}.")
if cutset & addset: # non empty intersection
raise ValueError(f"Intersecting cuts = {cuts} and adds = {adds}.")
newitset = (witset - cutset) | addset
if len(newitset) != (len(wits) - len(cuts) + len(adds)): # redundant?
raise ValueError(f"Invalid member combination among wits = {wits}, "
f"cuts ={cuts}, and adds = {adds}.")
if toad is None:
if not newitset:
toad = 0
else: # compute default f and m for len(wits)
toad = ample(len(newitset))
toader = Number(num=toad)
if newitset:
if toader.num < 1 or toader.num > len(newitset): # out of bounds toad
raise ValueError(f"Invalid toad = {toader.num} for wits = {newitset}")
else:
if toader.num != 0: # invalid toad
raise ValueError(f"Invalid toad = {toader.num} for wits = {newitset}")
cnfg = cnfg if cnfg is not None else []
if not isinstance(cnfg, list):
raise ValueError(f"Expected list got {cnfg=}")
data = data if data is not None else []
if not isinstance(data, list):
raise ValueError(f"Expected list got {data=}")
ked = dict(v=vs, # version string
t=ilk,
d="", # qb64 SAID
i=pre, # qb64 prefix
s=sner.numh, # hex string no leading zeros lowercase
p=dig, # SAID qb64 digest of prior event
kt=(tholder.num if intive and tholder.num is not None and
tholder.num <= MaxIntThold else tholder.sith),
k=keys, # list of qb64
nt=(ntholder.num if intive and ntholder.num is not None and
ntholder.num <= MaxIntThold else ntholder.sith),
n=ndigs, # hash qual Base64
bt=toader.num if intive and toader.num <= MaxIntThold else toader.numh,
br=cuts, # list of qb64 may be empty
ba=adds, # list of qb64 may be empty
)
if pvrsn.major >= 2:
ked['c'] = cnfg # list of config traits
ked['a'] = data # list of seals
else:
ked['a'] = data # list of seals
serder = SerderKERI(sad=ked, makify=True)
return serder
[docs]
def deltate(pre,
keys,
dig,
ilk=Ilks.drt,
**kwa
):
"""
Returns serder of delegated rotation event message.
Utility function to automate creation of delegated rotation events.
Syntactic suger that calls rotate but with ilk set to drt.
Inherited Parameters:
pre (str): identifier prefix qb64
keys (list): current signing keys qb64
dig (str): said of previous event qb64
ilk (str): ilk of event. Must be in (Ilks.rot, Ilks.drt)
sn (int | str): sequence number int or hex str
isith (int | str | list): current signing threshold input to Tholder
ndigs (list): current signing key digests qb64
nsith int | str | list): next signing threshold input to Tholder
toad (int | str ): witness threshold number if str then hex str
wits (list): prior witness identifier prefixes qb64
cuts (list): witness prefixes to cut qb64
adds (list): witness prefixes to add qb64
cnfg (list | None): configuration traits from TraitDex
data (list): seal dicts
version (Versionage): KERI protocol default version if psvrsn is None
pvrsn (Versionage): KERI protocol version
gvrsn (Versionage): CESR genus vrsion
kind (str): serialization kind from Serials
intive (bool): True means sith, nsith, and toad are serialized as ints
not hex str when numeric threshold
"""
return rotate(pre=pre, keys=keys, dig=dig, ilk=ilk, **kwa)
[docs]
def interact(pre,
dig,
sn=1,
data=None,
version=Version,
pvrsn=None,
gvrsn=None,
kind=Kinds.cesr,
):
"""
Returns serder of interaction event message.
Utility function to automate creation of interaction events.
Parameters:
pre is identifier prefix qb64
dig is said digest of previous event qb64
sn is int sequence number
data is list of dicts of comitted data such as seals
version (Versionage): KERI protocol default version if psvrsn is None
pvrsn (Versionage): KERI protocol version
gvrsn (Versionage): CESR genus vrsion
kind is serialization kind
"""
pvrsn = pvrsn if pvrsn is not None else version
vs = versify(pvrsn=pvrsn, kind=kind, size=0, gvrsn=gvrsn) # ensures cesr v2 only
ilk = Ilks.ixn
sner = Number(num=sn)
if sner.num < 1: # sn for interact must be >= 1
raise ValueError(f"Invalid sn = 0x{sner.numh} for ixn.")
data = data if data is not None else []
if not isinstance(data, list):
raise ValueError(f"Expected list got {data=}")
sad = dict(v=vs, # version string
t=ilk,
d="",
i=pre, # qb64 prefix
s=sner.numh, # hex string no leading zeros lowercase
p=dig, # qb64 digest of prior event
a=data, # list of seals
)
serder = SerderKERI(sad=sad, makify=True)
return serder
[docs]
def receipt(pre,
sn,
said,
*,
version=Version,
pvrsn=None,
gvrsn=None,
kind=Kinds.cesr
):
"""
Returns serder of event receipt message. Used for both non-trans and trans
signers as determined by signature attachment type (cigar or siger)
Utility function to automate creation of receipts.
Parameters:
pre is qb64 str of prefix of event being receipted
sn is int sequence number of event being receipted
said is qb64 of said of event being receipted
version (Versionage): KERI protocol default version if psvrsn is None
pvrsn (Versionage): KERI protocol version
gvrsn (Versionage): CESR genus vrsion
kind is serialization kind of receipt
"""
pvrsn = pvrsn if pvrsn is not None else version
vs = versify(pvrsn=pvrsn, kind=kind, size=0, gvrsn=gvrsn) # ensures cesr v2 only
ilk = Ilks.rct
sner = Number(num=sn)
if sner.num < 0: # sn for receipt must be >= 1
raise ValueError(f"Invalid sn = 0x{sner.numh} for rect.")
sad = dict(v=vs, # version string
t=ilk, # Ilks.rct
d=said, # qb64 digest of receipted event
i=pre, # qb64 prefix
s=sner.numh, # hex string no leading zeros lowercase
)
serder = SerderKERI(sad=sad, makify=True)
return serder
[docs]
def query(pre="",
route="",
replyRoute="",
query=None,
stamp=None,
version=Version,
pvrsn=None,
gvrsn=None,
kind=Kinds.json):
"""
Returns serder of query 'qry' message.
Utility function to automate creation of query messages.
Parameters:
pre (str): Identifier prefix (AID) of sender controller (Version 2)
route (str): namesapaced path, '/' delimited, that indicates data flow
handler (behavior) to processs the query
replyRoute (str): namesapaced path, '/' delimited, that indicates data flow
handler (behavior) to processs reply message to query if any.
query (dict): query data paramaters modifiers
stamp (str): date-time-stamp RFC-3339 profile of ISO-8601 datetime of
creation of message
version (Versionage): KERI protocol default version if psvrsn is None
pvrsn (Versionage): KERI protocol version
gvrsn (Versionage): CESR genus vrsion
kind (str): serialization kind value of Serials
Version 1.0
{
"v" : "KERI10JSON00011c_",
"t" : "qry",
"d": "EZ-i0d8JZAoTNZH3ULaU6JR2nmwyvYAfSVPzhzS6b5CM",
"dt": "2020-08-22T17:50:12.988921+00:00",
"r" : "logs",
"rr": "log/processor",
"q" :
{
"i": "EaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM",
"sn": "5",
"dt": "2020-08-01T12:20:05.123456+00:00",
}
}
Version 2.0
{
"v" : "KERI10JSON00011c_",
"t" : "qry",
"d": "EZ-i0d8JZAoTNZH3ULaU6JR2nmwyvYAfSVPzhzS6b5CM",
"i": "EaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM"
"dt": "2020-08-22T17:50:12.988921+00:00",
"r" : "logs",
"rr": "log/processor",
"q" :
{
"i": "EaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM",
"sn": "5",
"dt": "2020-08-01T12:20:05.123456+00:00",
}
}
"""
pvrsn = pvrsn if pvrsn is not None else version
vs = versify(pvrsn=pvrsn, kind=kind, size=0, gvrsn=gvrsn)
ilk = Ilks.qry
if pvrsn.major == Vrsn_1_0.major:
sad = dict(v=vs, # version string
t=ilk,
d="",
dt=stamp if stamp is not None else helping.nowIso8601(),
r=route, # resource type for single item request
rr=replyRoute,
q=query,
)
else:
sad = dict(v=vs, # version string
t=ilk,
d="",
i=pre,
dt=stamp if stamp is not None else helping.nowIso8601(),
r=route, # resource type for single item request
rr=replyRoute,
q=query,
)
serder = SerderKERI(sad=sad, makify=True)
return serder
[docs]
def reply(pre="",
route="",
data=None,
stamp=None,
version=Version,
pvrsn=None,
gvrsn=None,
kind=Kinds.json):
"""Returns serder of reply 'rpy' message.
Utility function to automate creation of reply messages.
Reply 'rpy' message is a SAD item with an associated derived SAID in its
'd' field.
Parameters:
pre (str): identifier prefix of sender (AID)
route (str): '/' delimited path identifier of data flow handler
(behavior) to processs the reply if any
data (dict): attribute section of reply
stamp (str): date-time-stamp RFC-3339 profile of ISO-8601 datetime of
creation of message or data
version (Versionage): KERI protocol default version if psvrsn is None
pvrsn (Versionage): KERI protocol version
gvrsn (Versionage): CESR genus vrsion
kind (str): serialization kind value of Serials
Version 1:
{
"v" : "KERI10JSON00011c_",
"t" : "rpy",
"d": "EZ-i0d8JZAoTNZH3ULaU6JR2nmwyvYAfSVPzhzS6b5CM",
"dt": "2020-08-22T17:50:12.988921+00:00",
"r" : "logs/processor",
"a" :
{
"d": "EaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM",
"i": "EAoTNZH3ULvYAfSVPzhzS6baU6JR2nmwyZ-i0d8JZ5CM",
"name": "John Jones",
"role": "Founder",
}
}
Version 2:
{
"v" : "KERI10JSON00011c_",
"t" : "rpy",
"d": "EZ-i0d8JZAoTNZH3ULaU6JR2nmwyvYAfSVPzhzS6b5CM",
"i": "EAoTNZH3ULvYAfSVPzhzS6baU6JR2nmwyZ-i0d8JZ5CM",
"dt": "2020-08-22T17:50:12.988921+00:00",
"r" : "logs/processor",
"a" :
{
"d": "EaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM",
"i": "EAoTNZH3ULvYAfSVPzhzS6baU6JR2nmwyZ-i0d8JZ5CM",
"name": "John Jones",
"role": "Founder",
}
}
"""
pvrsn = pvrsn if pvrsn is not None else version
vs = versify(pvrsn=pvrsn, kind=kind, size=0, gvrsn=gvrsn) # ensures cesr v2 only
if data is None:
data = {}
if pvrsn.major == Vrsn_1_0.major:
sad = dict(v=vs, # version string
t=Ilks.rpy,
d="",
dt=stamp if stamp is not None else helping.nowIso8601(),
r=route if route is not None else "", # route
a=data if data else {}, # attributes
)
else: # Vrsn_2_0
sad = dict(v=vs, # version string
t=Ilks.rpy,
d="",
i=pre,
dt=stamp if stamp is not None else helping.nowIso8601(),
r=route if route is not None else "", # route
a=data if data else {}, # attributes
)
serder = SerderKERI(sad=sad, makify=True)
return serder
[docs]
def prod(pre="",
route="",
replyRoute="",
query=None,
stamp=None,
version=Version,
pvrsn=None,
gvrsn=None,
kind=Kinds.json):
"""Prod message
Returns:
prod (SerderKERI): of prod, 'pro', msg to request disclosure via bare, 'bar' msg
of data anchored via seal(s) on KEL for identifier prefix, pre, when given
by all SAIDs given in digs list.
Parameters:
pre (str): Identifier prefix (AID) of sender controller (Version 2)
route (str): namesapaced path, '/' delimited, that indicates data flow
handler (behavior) to processs the query
replyRoute (str): namesapaced path, '/' delimited, that indicates data flow
handler (behavior) to processs reply message to query if any.
query (dict): query data paramaters modifiers
stamp (str): date-time-stamp RFC-3339 profile of ISO-8601 datetime of
creation of message
version (Versionage): KERI protocol default version if psvrsn is None
pvrsn (Versionage): KERI protocol version
gvrsn (Versionage): CESR genus vrsion
kind (str): serialization kind value of Serials
Version 1
{
"v" : "KERI10JSON00011c_",
"t" : "pro",
"d": "EZ-i0d8JZAoTNZH3ULaU6JR2nmwyvYAfSVPzhzS6b5CM",
"dt": "2020-08-22T17:50:12.988921+00:00",
"r" : "data",
"rr": "data/processor",
"q":
{
"d":"EaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM"
}
}
Version 2
{
"v" : "KERI10JSON00011c_",
"t" : "pro",
"d": "EZ-i0d8JZAoTNZH3ULaU6JR2nmwyvYAfSVPzhzS6b5CM",
"i": "EaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM",
"dt": "2020-08-22T17:50:12.988921+00:00",
"r" : "data",
"rr": "data/processor",
"q":
{
"d":"EaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM"
}
}
"""
pvrsn = pvrsn if pvrsn is not None else version
vs = versify(pvrsn=pvrsn, kind=kind, size=0, gvrsn=gvrsn) # ensures cesr v2 only
ilk = Ilks.pro
if pvrsn.major == Vrsn_1_0.major:
sad = dict(v=vs, # version string
t=ilk,
d="",
dt=stamp if stamp is not None else helping.nowIso8601(),
r=route, # resource type for single item request
rr=replyRoute,
q=query,
)
else:
sad = dict(v=vs, # version string
t=ilk,
d="",
i=pre,
dt=stamp if stamp is not None else helping.nowIso8601(),
r=route, # resource type for single item request
rr=replyRoute,
q=query,
)
serder = SerderKERI(sad=sad, makify=True)
return serder
[docs]
def bare(pre="",
route="",
data=None,
stamp=None,
version=Version,
pvrsn=None,
gvrsn=None,
kind=Kinds.json):
"""Bare message
Returns:
bare (SerderKERI): serder of bare 'bar' message.
Utility function to automate creation of unhiding (bareing) messages for
disclosure of sealed data associated with anchored seals in a KEL.
Reference to anchoring seal is provided as an attachment to bare message.
Bare 'bar' message is a SAD item with an associated derived SAID in a 'd'
field in side its 'a' block.
Parameters:
pre (str): Identifier prefix (AID) of sender controller (Version 2)
route (str): namesapaced path, '/' delimited, that indicates data flow
handler (behavior) to processs the exposure
data (dict): dict of dicts of comitted SADS for SAIDs in seals keyed by SAID
stamp (str): date-time-stamp RFC-3339 profile of ISO-8601 datetime of
creation of message
version (Versionage): KERI protocol default version if psvrsn is None
pvrsn (Versionage): KERI protocol version
gvrsn (Versionage): CESR genus vrsion
kind (str): serialization kind value of Serials
route is route path string that indicates data flow handler (behavior)
to processs the exposure
data is dict of dicts of comitted SADS for SAIDs in seals keyed by SAID
stamp (str): date-time-stamp RFC-3339 profile of ISO-8601 datetime of
creation of message or data
version is Version instance
kind is serialization kind
Version 1
{
"v" : "KERI10JSON00011c_",
"t" : "bar",
"d": "EZ-i0d8JZAoTNZH3ULaU6JR2nmwyvYAfSVPzhzS6b5CM",
"dt": "2020-08-22T17:50:12.988921+00:00",
"r" : "sealed/processor",
"a" :
{
"EaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM":
{
"d": "EaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM",
"i": "EAoTNZH3ULvYAfSVPzhzS6baU6JR2nmwyZ-i0d8JZ5CM",
"dt": "2020-08-22T17:50:12.988921+00:00",
"name": "John Jones",
"role": "Founder",
}
}
}
Version 2
{
"v" : "KERI10JSON00011c_",
"t" : "bar",
"d": "EZ-i0d8JZAoTNZH3ULaU6JR2nmwyvYAfSVPzhzS6b5CM",
"i": "EaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM",
"dt": "2020-08-22T17:50:12.988921+00:00",
"r" : "sealed/processor",
"a" :
{
"EaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM":
{
"d": "EaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM",
"i": "EAoTNZH3ULvYAfSVPzhzS6baU6JR2nmwyZ-i0d8JZ5CM",
"dt": "2020-08-22T17:50:12.988921+00:00",
"name": "John Jones",
"role": "Founder",
}
}
}
"""
pvrsn = pvrsn if pvrsn is not None else version
vs = versify(pvrsn=pvrsn, kind=kind, size=0, gvrsn=gvrsn) # ensures cesr v2 only
ilk = Ilks.bar
if pvrsn.major == Vrsn_1_0.major:
sad = dict(v=vs, # version string
t=ilk,
d="",
dt=stamp if stamp is not None else helping.nowIso8601(),
r=route if route is not None else "", # route
a=data if data else {}, # dict of SADs
)
else:
sad = dict(v=vs, # version string
t=ilk,
d="",
i=pre,
dt=stamp if stamp is not None else helping.nowIso8601(),
r=route if route is not None else "", # route
a=data if data else {}, # dict of SADs
)
serder = SerderKERI(sad=sad, makify=True)
return serder
[docs]
def exchept(sender="",
receiver="",
route="",
modifiers=None,
attributes=None,
nonce=None,
stamp=None,
version=Vrsn_2_0,
pvrsn=None,
gvrsn=None,
kind=Kinds.json):
"""Utility function to automate creation of exchange incept, exchept, 'xip',
message. The exchept 'xip' message is a SAD item with an associated derived
SAID in its 'd' field. Only defined for KERI v2.
Returns:
exchept (SerderKERI): 'xip' message.
Fields in order:
(v, t, d, u, ri, dt, r, q, a),
Parameters:
sender (str): qb64 of sender identifier (AID)
receiver (str): qb64 of receiver identifier (AID)
route (str): '/' delimited path identifier of data flow handler
(behavior) to processs the reply if any
modifiers (dict): modifiers
attributes (dict): attributes
nonce (str|None): qb64 of UUID salty nonce. When None generate nonce.
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 vrsion
kind (str): serialization kind value of Serials
Version 2:
{
"v" : "KERI10JSON00011c_",
"t" : "rpy",
"d": "EZ-i0d8JZAoTNZH3ULaU6JR2nmwyvYAfSVPzhzS6b5CM",
"u": '0AAwMTIzNDU2Nzg5YWJjZGVm',
"i": "EAoTNZH3ULvYAfSVPzhzS6baU6JR2nmwyZ-i0d8JZ5CM",
"ri": "EBPzhzS6baU6JR2nmwyZ-i0d8JZ5CMAoTNZH3ULvYAfS",
"dt": "2020-08-22T17:50:12.988921+00:00",
"r" : "/logs/processor",
"q":
{
"name": "Zoe",
"color": "Blue"
}
"a":
{
"d": "EaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM"
}
}
"""
pvrsn = pvrsn if pvrsn is not None else version
vs = versify(pvrsn=pvrsn, kind=kind, size=0, gvrsn=gvrsn) # ensures cesr v2 only
ilk = Ilks.xip
sad = dict(v=vs, # version string
t=ilk, # message type
d="", # message said
u=nonce if nonce is not None else Noncer().qb64,
i=sender, # sender aid qb64
ri=receiver, # receiver aid qb64
dt=stamp if stamp is not None else helping.nowIso8601(),
r=route if route is not None else "", # route
q=modifiers if modifiers is not None else {}, # modifiers
a=attributes if attributes is not None else {}, # attributes
)
serder = SerderKERI(sad=sad, makify=True)
return serder
[docs]
def exchange(*,
sender="",
receiver="",
xid="",
prior="",
route="",
modifiers=None,
attributes=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")
"""
pvrsn = pvrsn if pvrsn is not None else version
vs = versify(pvrsn=pvrsn, kind=kind, size=0, gvrsn=gvrsn) # ensures cesr v2 only
if pvrsn.major < 2:
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 {},
a=attributes if attributes is not None else {})
else: # v2
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 {},
a=attributes if attributes is not None else {}
)
return SerderKERI(sad=sad, makify=True)
[docs]
def messagize(serder, *, sigers=None, source=None, bonds=None, wigers=None,
cigars=None, framed=False, nested=False, gvrsn=Version,
genusify=False):
"""Attaches authenticator(s) from sigers (with or without source as seal) and/or
cigars and/or wigers and/or bonds. A bond is typically a seal reference to
an event with anchoring seal of message as authenticator. In v2 bonds may
also include any Structor subclass not simply seal references.
Parameters::
serder (SerderKERI): instance containing the event
sigers (list): of Siger instances (optional) to create indexed signatures
based on seal type if any
source (SealEvent|SealLast|None): optiona modifier to sigers when provided
If SealEvent use attachment group code TransIdxSigGroups plus attach
triple pre+snu+dig made from (i,s,d) of seal plus ControllerIdxSigs
plus attached indexed sigs in sigers
Elif SealLast use attachment group code TransLastIdxSigGroups plus
attach uniple pre made from (i,) of seal plus ControllerIdxSigs
plus attached indexed sigs in sigers
Else None use ControllerIdxSigs plus attached indexed sigs in sigers
bonds (list[]|SealEvent|SealSource|SealLast|BlindState|BoundState|TypeMedia|None):
Non signature based authenticator typically an event reference or may
Only v2 supports BlindState|BoundState|TypeMedia
if bonds is not list convert to list.
wigers (list): optional list of Siger instances of witness index signatures
cigars (list): optional list of Cigars instances of non-transferable non indexed
signatures from which to form receipt couples.
Each cigar.vefer.qb64 is pre of receiptor and cigar.qb64 is signature
framed (bool): True means each message plus attachments may be assumed to
be isolated as frame when parsing so do not need
attachment group
False means use attachment group since message plus
attachments may not be isolated as frame when parsing
nested (bool): True means messagize for non-top level
This forces non-native serializion to be embedded
in non-native group code
False means messagize for top level of stream.
This allows bare non-native serialization of message
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
genusify (bool): True means prepend genus version code from gvrsn before
serder to override default stream genus version
False means do nothing
Returns::
msg (bytearray): KERI event with attachments if any
"""
if not (sigers or cigars or wigers or bonds):
raise ValueError(f"Missing authenticator for msg={serder.pretty()}")
svrsn = serder.gvrsn if serder.gvrsn else serder.pvrsn # effective serder gvrsn
if nested and gvrsn.major < 2:
gvrsn = Vrsn_2_0 # force gvrsn to v2 for nesting
if (gvrsn.major < svrsn.major or
(gvrsn.major == svrsn.major and gvrsn.minor < svrsn.minor)):
gvrsn = svrsn # serder vrsn greater than gvrsn so use it instead
gims = bytearray() # for grouped message stream
if genusify: # create and insert stream genus version code
gvc = Counter.makeGVC(version=gvrsn)
gims.extend(gvc)
aims = bytearray() # attachment message stream
if gvrsn.major < 2 and not nested: # version 1 legacy toplevel attachments
if sigers:
if isinstance(source, SealEvent):
aims.extend(Counter(Codens.TransIdxSigGroups, count=1,
version=Vrsn_1_0).qb64b)
aims.extend(source.i.encode())
aims.extend(Number(snh=source.s).qb64b)
aims.extend(source.d.encode())
elif isinstance(source, SealLast):
aims.extend(Counter(Codens.TransLastIdxSigGroups, count=1,
version=Vrsn_1_0).qb64b)
aims.extend(source.i.encode("utf-8"))
elif source:
raise ValueError(f"Invalid trans modifier {source} for "
f"sigers on msg={serder.pretty()}")
aims.extend(Counter(Codens.ControllerIdxSigs, count=len(sigers),
version=Vrsn_1_0).qb64b)
for siger in sigers:
aims.extend(siger.qb64b)
if bonds:
if isinstance(bonds, tuple):
bonds = [bonds] # convert to list
clans = {}
for bond in bonds: # collate seals from bonds into groups by clan
if (bond.__class__ not in (SealEvent, SealSource, SealLast)):
raise ValueError(f"Unsupported authenticator {bond} kind for "
f"version={gvrsn} msg={serder.pretty()}")
if bond.__class__ not in clans: # zeroth one in group
clans[bond.__class__] = [bond] # make list
else:
clans[bond.__class__].append(bond)
for clan, group in clans.items():
if issubclass(clan, SealEvent): # authenticator is event seal
aims.extend(Counter(Codens.SealSourceTriples, count=len(group),
version=Vrsn_1_0).qb64b)
for bond in group:
aims.extend(bond.i.encode())
aims.extend(Number(snh=bond.s).qb64b)
aims.extend(bond.d.encode())
elif issubclass(clan, SealSource): # authenticator is last seal
aims.extend(Counter(Codens.SealSourceCouples, count=len(group),
version=Vrsn_1_0).qb64b)
for bond in group:
aims.extend(Number(snh=bond.s).qb64b)
aims.extend(bond.d.encode())
elif issubclass(clan, SealLast): # authenticator is last seal
aims.extend(Counter(Codens.SealSourceLastSingles, count=len(group),
version=Vrsn_1_0).qb64b)
for bond in group:
aims.extend(bond.i.encode())
else:
raise ValueError(f"Unsupported authenticator {clan} for"
f" version={gvrsn} msg={serder.pretty()}")
if wigers:
aims.extend(Counter(Codens.WitnessIdxSigs, count=len(wigers),
version=Vrsn_1_0).qb64b)
for wiger in wigers:
if wiger.verfer and wiger.verfer.code not in NonTransDex:
raise ValueError(f"Attempt to use tranferable prefix="
f"{wiger.verfer.qb64} for receipt.")
aims.extend(wiger.qb64b)
if cigars:
aims.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(f"Attempt to use tranferable prefix="
f"{cigar.verfer.qb64} for receipt.")
aims.extend(cigar.verfer.qb64b)
aims.extend(cigar.qb64b)
if len(aims) % 4:
raise ValueError(f"Invalid attachments size={len(atc)}, "
f"nonintegral quadlets.")
msg = bytearray(serder.raw)
if not framed:
msg.extend(Counter(Codens.AttachmentGroup,
count=(len(aims) // 4), version=Vrsn_1_0).qb64b)
msg.extend(aims)
elif gvrsn.major == 2: # version 2.x for attachments and/or nesting
if sigers:
eims = bytearray() # enclosed incoming message stream
sims = bytearray() # composes idxsig group inside group
coden = None
if isinstance(source, SealEvent): # composed idx sig group
coden = Codens.TransIdxSigGroups
eims.extend(source.i.encode())
eims.extend(Number(snh=source.s).qb64b)
eims.extend(source.d.encode())
elif isinstance(source, SealLast): # composed idx sig group
coden = Codens.TransLastIdxSigGroups
eims.extend(source.i.encode("utf-8"))
elif source:
raise ValueError(f"Unsupported trans modifier {source} for "
f"sigers on msg={serder.pretty()}")
for siger in sigers:
sims.extend(siger.qb64b)
eims.extend(Counter.enclose(qb64=sims,
code=Codens.ControllerIdxSigs,
version=gvrsn))
if coden:
aims.extend(Counter.enclose(qb64=eims, code=coden, version=gvrsn))
else:
aims.extend(eims)
if bonds:
if isinstance(bonds, tuple):
bonds = [bonds] # convert to list
clans = {} # dict of structor lists keyed by clan
for bond in bonds: # collate structors made from bonds by clan group
structor = Structor(crew=bond)
if (structor.clan not in (SealEvent, SealSource, SealLast,
BlindState, BoundState, TypeMedia)):
raise ValueError(f"Unsupported authenticator {structor.clan}"
f" for version={gvrsn} msg={serder.pretty()}")
if structor.clan not in clans:
clans[structor.clan] = [structor]
else:
clans[structor.clan].append(structor)
for structors in clans.values():
aims.extend(Structor.enclose(structors))
if wigers:
eims = bytearray()
for wiger in wigers:
if wiger.verfer and wiger.verfer.code not in NonTransDex:
raise ValueError(f"Mismatch: tranferable prefix="
f"{wiger.verfer.qb64} for receipt.")
eims.extend(wiger.qb64b)
aims.extend(Counter.enclose(qb64=eims,
code=Codens.WitnessIdxSigs,
version=gvrsn))
if cigars:
eims = bytearray()
for cigar in cigars:
if cigar.verfer.code not in NonTransDex:
raise ValueError(f"Mismatch: tranferable prefix="
f"{cigar.verfer.qb64} with nontrans cnt code.")
eims.extend(cigar.verfer.qb64b)
eims.extend(cigar.qb64b)
aims.extend(Counter.enclose(qb64=eims,
code=Codens.NonTransReceiptCouples,
version=gvrsn))
if len(aims) % 4:
raise ValueError(f"Invalid attachments size={len(aims)}, "
f"nonintegral quadlets.")
if not nested and not framed:
aims = Counter.enclose(qb64=aims,
code=Codens.AttachmentGroup,
version=gvrsn)
if nested: # enclose message+attachments in body+attach group
if serder.kind != Kinds.cesr: # non-native v1 always or v2
# enclose msg in non-native body group
texter = Texter(raw=serder.raw)
msg = Counter.enclose(qb64=texter.qb64b,
code=Codens.NonNativeBodyGroup,
version=gvrsn)
else: # native cesr v2 only
msg = bytearray(serder.raw)
msg.extend(aims) # attach attachments
msg = Counter.enclose(qb64=msg, code=Codens.BodyWithAttachmentGroup)
else:
msg = bytearray(serder.raw)
msg.extend(aims) # attach attachments
else: # not a supported gvrsn for attachments and nesting
raise ValueError(f"Unsupported protocol version={serder.pvrsn}")
gims.extend(msg)
return gims
[docs]
class Kever:
"""
Kever is KERI key event verifier class
Only supports current version VERSION
Has the following public attributes and properties:
Class Attributes:
EstOnly (bool):
True means allow only establishment events
False means allow all events
DoNotDelegate (bool):
True means do not allow delegation other identifiers
False means allow delegation of delegated identifiers
Attributes:
db (Baser | None): instance that manages the LMDB database when provided.
When None provided then create and assign vacuous instance of Baser.
cues (deque | None): Injected Kevery.cues when provided. Default None.
prefixes (list | None): Injected from Kevery when provided.
qb64 identifier prefixes of own habitat identifiers.
Assign db.prefixes when None
When empty operate in promiscuous mode
version (Versionage): serder.version instance of current event state version
prefixer (Prefixer): instance for current event state
sner (Number): instance of sequence number
fner (Number): instance of first seen ordinal number
dater (Dater): instance of first seen datetime
serder (SerderKERI): instance of current event with .serder.diger for digest
ilk (str): from Ilks for current event type
tholder (Tholder): instance for event signing threshold
verfers (list): of Verfer instances for current event state set of signing keys
ndigers (list): of Diger instances for current event state set of
next (rotation) key digests
ntholder (Tholder): instance for next (rotation) threshold
from serder.ntholder
toader (Number): instance of TOAD (threshold of accountable duplicity)
wits (list): of qualified qb64 aids for witnesses
cuts (list): of qualified qb64 aids for witnesses cut from prev wits list
adds (list) of qualified qb64 aids for witnesses added to prev wits list
estOnly (bool): config trait True means only allow establishment events
Default False. Corresponds to config trait string "EO"
doNotDelegate (bool): config trait True means do not allow delegation
Default False. Corresponds to config trait string "DND"
lastEst (LastEstLoc): namedtuple of int sn .s and qb64 digest .d of last est event
delegated (bool): True means delegated identifier, False not delegated
delpre(str): qb64 of delegator's prefix
Properties:
sn (int): sequence number property that returns .sner.num
fn (int): first seen ordinal number property the returns .fner.num
ndigs (list): of digests qb64 of .digers
kevers (dict): reference to self.db.kevers
transferable (bool): True if .digers is not empty and pre is transferable
ToDo:
Add Registrar Backer support:
Class variable, instance variable and parse support config trait.
raise error for now
"""
EstOnly = False
DoNotDelegate = False
[docs]
def __init__(self, *, state=None, serder=None, sigers=None, wigers=None,
db=None, estOnly=None, delsner=None, delsger=None, firner=None,
dater=None, cues=None, eager=False, local=True, check=False):
"""
Create incepting kever and state from inception serder
Verify incepting serder against sigers raises ValidationError if not
Parameters:
state (KeyStateRecord | None): instance for key state notice
serder (SerderKERI | None): instance of inception event
sigers (list | None): of Siger instances of indexed controller signatures
of event. Index is offset into keys list from latest est event
wigers (list | None): of Siger instances of indexed witness signatures of
event. Index is offset into wits list from latest est event
db (Baser | None): instance of lmdb database
estOnly (bool | None): True means establishment only events allowed 'EO'.
False all events allowed.
delsner (Seqner | None): instance of delegating event sequence number.
If this event is not delegated then seqner is ignored
delsger (Diger | None): instance of of delegating event SAID diger.
If this event is not delegated then saider is ignored
firner (Seqner | None): instance optional of cloned first seen ordinal
If cloned mode then firner maybe provided (not None)
When firner provided then compare fn of dater and database and
first seen if not match then log and add cue notify problem
dater (Dater | None): optional instance of cloned replay datetime
If cloned mode then dater maybe provided (not None)
When dater provided then use dater for first seen datetime
cues (Deck | None): reference to Kevery.cues Deck when provided
i.e. notices of events or requests to respond to
eager (bool): True means try harder to find validate events by
walking KELs. Enables only being eager
in escrow processing not initial parsing.
False means only use pre-existing information
if any, either percolated attached or in database.
local (bool): event source for validation logic
True means event source is local (protected).
False means event source is remote (unprotected).
Event validation logic is a function of local or remote
check (bool): True means do not update the database in any
non-idempotent way. Useful for reinitializing the Kevers from
a persisted KEL without updating non-idempotent first seen .fels
and timestamps.
"""
if not (state or (serder and sigers)):
raise ValueError("Missing required arguments. Need state or serder"
" and sigers")
if db is None:
db = Baser(reopen=True) # default name = "main"
self.db = db
self.cues = cues
local = True if local else False
if state: # preload from state
self.reload(state)
return
# may update state as we go because if invalid we fail to finish init
self.version = serder.pvrsn # version dispatch ?
ilk = serder.ilk # serder.ked["t"]
if ilk not in (Ilks.icp, Ilks.dip):
raise ValidationError("Expected ilk = {} or {} got {} for evt = {}."
"".format(Ilks.icp, Ilks.dip,
ilk, serder.ked))
self.ilk = ilk
self.incept(serder=serder) # do major event validation and state setting
self.config(serder=serder, estOnly=estOnly) # assign config traits perms
# Validates signers, delegation if any, and witnessing when applicable
# If does not validate then escrows as needed and raises ValidationError
sigers, wigers, delpre, delsner, delsger = self.valSigsWigsDel(
serder=serder,
sigers=sigers,
verfers=serder.verfers,
tholder=self.tholder,
wigers=wigers,
toader=self.toader,
wits=self.wits,
delsner=delsner,
delsger=delsger,
eager=eager,
local=local)
self.delpre = delpre # may be None
self.delegated = True if self.delpre else False
wits = serder.backs # serder.ked["b"]
# .validateSigsDelWigs above ensures thresholds met otherwise raises exception
# all validated above so may add to KEL and FEL logs as first seen
# returns fn == None if already logged fn log is non idempotent
fn, dts = self.logEvent(serder=serder, sigers=sigers, wigers=wigers,
wits=wits,
first=True if not check else False,
delnum=delsner, diger=delsger,
firner=firner, dater=dater, local=local)
if fn is not None: # first is non-idempotent for fn check mode fn is None
self.fner = Number(num=fn)
self.dater = Dater(dts=dts)
self.db.states.pin(keys=self.prefixer.qb64,
val=self.state())
@property
def sn(self):
"""
Returns:
(int): .sner.num
"""
return self.sner.num
@property
def fn(self):
"""
Returns:
(int): .fner.num
"""
return self.fner.num
@property
def ndigs(self):
"""
Returns:
(list): digs of digers
"""
return [diger.qb64 for diger in self.ndigers]
@property
def kevers(self):
"""
Returns .baser.kevers
"""
return self.db.kevers
@property
def prefixes(self):
"""
Returns .db.prefixes
"""
return self.db.prefixes
@property
def groups(self):
"""
Returns .db.gids oset of group hab ids (prefixes)
"""
return self.db.groups
@property
def transferable(self):
"""
Property transferable:
Returns True if identifier does not have non-transferable derivation code
and .nextor is not None
False otherwise
"""
return True if self.ndigers and self.prefixer.transferable else False
[docs]
def locallyOwned(self, pre: str | None = None):
"""Returns True if pre is in .prefixes and not in .groups
False otherwise. Exclusively locally controlled.
Indicates that provided identifier prefix is controlled by a local
controller from .prefixes but is not a group with local member.
i.e pre is an exclusively locally owned (controlled) AID
(identifier prefix)
Returns:
(bool): True if pre is local hab but not group hab
When pre="" empty then returns False
Parameters:
pre (str|None): qb64 identifier prefix if any. Default None
None means use self.prefixer.qb64
"""
pre = pre if pre is not None else self.prefixer.qb64
return pre in self.prefixes and pre not in self.groups
[docs]
def locallyDelegated(self, pre: str):
"""Returns True if pre w is in .prefixes which includes group AIDs in
self.groups which have a local member AID.
Which means it is either locally controlled single sig or a multi-sig
group with a locally controlled member.
False otherwise.
Use when pre is a delegator, i.e. the delpre from some delegated event and
want to confirm that pre is also locally controller as either the single
sig AID or the group multisig AID of a locally controlled member of the group.
Indicates that provided identifier prefix is controlled by a local
controller from .prefixes is a group prefix that is controlled by a local
member of that group.
Because delpre may be None, changes the default to "" instead of
self.prefixer.pre because self.prefixer.pre is delegate not delegator
of self. Unaccepted dip events do not have self.delpre set yet.
Returns:
(bool): True if pre is local hab or group hab that has a local member
When pre="" empty or None then returns False
Parameters:
pre (str): qb64 identifier prefix if any.
ToDo: this code does not account for stale group members as delegators.
i.e. a stale group membed is a member AID for a group AID in .groups
for which the member AID was a signing (smids) or rotating (rmids) member
in the past but is no longer. For delegation approval there must be
a local member for the delegator group AID that is a current signing member
i,e. in .smids for the group hab.
The current logic allows an event to be escrowed for later approval
but whose delpre (delegator) is a group with a stale local member
That later approval must detect and properly handle the staleness.
Alternatively the logic could be changed to short circut that later
work by checking here for staleness. For example:
delpre.mhab.pre in delpre's hab.smids (not stale )
if pre in self.groups: # local group delegator
habord = self.db.habs.get(keys=(pre,))
return habord.mid in habord.smids # True not stale, False stale
return pre in self.prefixes # otherwise local non-group delegator
"""
pre = pre if pre is not None else ""
return pre in self.prefixes
[docs]
def locallyWitnessed(self, *, wits: list[str]=None, serder: (str)=None):
"""Returns True if a local controller is a witness of this Kever's KEL
of wits in serder of if None then current wits for this Kever.
i.e. self is witnessd by locally owned (controlled) AID (identifier prefix)
Parameters:
wits (list[str]): qb64 identifier prefixes of witnesses
serder ( SerderKERI | None): SerderKERI instace if any
"""
if not wits:
if not serder:
wits = self.wits
else:
if serder.pre != self.prefixer.qb64: # not same KEL as self
return False
wits, _, _ = self.deriveBacks(serder=serder)
return True if (self.prefixes & oset(wits)) else False
[docs]
def locallyMembered(self, pre: str | None = None):
"""Returns True if group hab identifier prefix pre has as a contributing
member a locally owned prefix by virture of pre in .groups
Returns:
(bool): True if pre is group hab identifier in .groups
False otherwise
Parameters:
pre (str|None): qb64 identifier prefix if any or None
When None default to use self.prefixer.qb64
"""
# assumes stale group membership is taken care of by presence of groups
# i.e where once a local member but no more.
pre = pre if pre is not None else self.prefixer.qb64
return pre in self.groups # groups
[docs]
def locallyContributedIndices(self, verfers: list[Verfer]):
"""Returns list of indices of public keys contributed by local members
to the KEL with current signing keys represented by verfers
Using the pubs index to find members of a signing group
Parameters:
verfers (list[Verfer]): instance for each current signing key
Returns:
indices list[int]: list of indices of keys contributed by local members
"""
habord = self.db.habs.get(keys=(self.prefixer.qb64,))
kever = self.kevers[habord.mid]
idx = [verfer.qb64 for verfer in verfers].index(kever.verfers[0].qb64)
return [idx]
[docs]
def reload(self, state):
"""
Reload Kever attributes (aka its state) from state (KeyStateRecord)
Parameters:
state (KeyStateRecord | None): instance for key state notice
"""
self.version = Versionage._make(state.vn)
self.prefixer = Prefixer(qb64=state.i)
self.sner = Number(numh=state.s) # sequence number Number instance hex str
self.fner = Number(numh=state.f) # first seen ordinal Number hex str
self.dater = Dater(dts=state.dt)
self.ilk = state.et
self.tholder = Tholder(sith=state.kt)
self.ntholder = Tholder(sith=state.nt)
self.verfers = [Verfer(qb64=key) for key in state.k]
self.ndigers = [Diger(qb64=dig) for dig in state.n]
self.toader = Number(numh=state.bt) # auto converts from hex num
self.wits = state.b
self.cuts = state.ee.br
self.adds = state.ee.ba
self.estOnly = False
self.doNotDelegate = True if TraitDex.DoNotDelegate in state.c else False
self.estOnly = True if TraitDex.EstOnly in state.c else False
self.lastEst = LastEstLoc(s=int(state.ee.s, 16),
d=state.ee.d)
self.delpre = state.di if state.di else None
self.delegated = True if self.delpre else False
if (serder := self.db.evts.get(keys=(self.prefixer.qb64, state.d))) is None:
raise MissingEntryError(f"Corresponding event not found for state="
f"{state}.")
self.serder = serder
# May want to do additional checks here
[docs]
def incept(self, serder, estOnly=None):
"""
Verify incept key event message from serder
Parameters:
serder is SerderKERI instance of inception event
estOnly is boolean to indicate establish only events allowed
"""
ked = serder.ked
self.sner = serder.sner
if self.sner.positive:
raise ValidationError(f"Nonzero sn={self.sner.num} in inception event.")
self.verfers = serder.verfers # converts keys to verifiers
self.tholder = serder.tholder # Tholder(sith=ked["kt"]) # parse sith into Tholder instance
if len(self.verfers) < self.tholder.size:
raise ValidationError("Invalid sith = {} for keys = {} for evt = {}."
"".format(ked["kt"],
[verfer.qb64 for verfer in self.verfers],
ked))
self.prefixer = Prefixer(qb64=serder.pre)
self.serder = serder # need whole serder for digest agility comparisons
ndigs = serder.ndigs # ked["n"]
if not self.prefixer.transferable and ndigs: # nxt must be empty for nontrans prefix
raise ValidationError("Invalid inception next digest list not empty for "
"non-transferable prefix = {} for evt = {}."
"".format(self.prefixer.qb64, ked))
self.ndigers = serder.ndigers
self.ntholder = serder.ntholder
self.cuts = [] # always empty at inception since no prev event
self.adds = [] # always empty at inception since no prev event
wits = ked["b"]
if not self.prefixer.transferable and wits: # wits must be empty for nontrans prefix
raise ValidationError("Invalid inception wits not empty for "
"non-transferable prefix = {} for evt = {}."
"".format(self.prefixer.qb64, ked))
if len(oset(wits)) != len(wits):
raise ValidationError("Invalid backers = {}, has duplicates for evt = {}."
"".format(wits, ked))
self.wits = wits
toader = Number(num=ked["bt"]) # auto converts hex num to int
if wits:
if toader.num < 1 or toader.num > len(wits): # out of bounds toad
raise ValueError(f"Invalid toad = {toader.num} for backers "
f"(wits)={wits} for event={ked}.")
else:
if toader.num != 0: # invalid toad
raise ValueError(f"Invalid toad = {toader.num} for backers "
"(wits)={wits} for event={ked}.")
self.toader = toader
data = ked["a"]
if not self.prefixer.transferable and data: # data must be empty for nontrans prefix
raise ValidationError("Invalid inception data not empty for "
"non-transferable prefix = {} for evt = {}."
"".format(self.prefixer.qb64, ked))
# need this to recognize recovery events and transferable receipts
# last establishment event location
self.lastEst = LastEstLoc(s=self.sner.num, d=self.serder.said)
[docs]
def config(self, serder, estOnly=None, doNotDelegate=None):
"""
Process cnfg field for configuration traits
"""
# assign traits
self.estOnly = (True if (estOnly if estOnly is not None else self.EstOnly)
else False) # ensure default estOnly is boolean
self.doNotDelegate = (True if (doNotDelegate if doNotDelegate is not None
else self.DoNotDelegate)
else False) # ensure default doNotDelegate is boolean
cnfg = serder.traits # serder.ked["c"] # process cnfg for traits
if TraitDex.EstOnly in cnfg:
self.estOnly = True
if TraitDex.DoNotDelegate in cnfg:
self.doNotDelegate = True
[docs]
def update(self, serder, sigers, wigers=None, delsner=None, delsger=None,
firner=None, dater=None, eager=False, local=True, check=False):
"""
Not an inception event. Verify event serder and indexed signatures
in sigers and update state
Parameters:
serder (SerderKERI): instance of event
sigers (list): of SigMat instances of indexed signatures of controller
signatures of event. Index is offset into keys list from latest
est event and when provided ondex is offset into key digest list
from prior next est event to latest est event.
wigers (list | None): of Siger instances of indexed witness signatures of
event. Index is offset into wits list from latest est event
delsner (Seqner | None): instance of delegating event sequence number.
If this event is not delegated then seqner is ignored
delsger (Diger | None): instance of of delegating event said diger.
If this event is not delegated then diger is ignored
firner (Seqner | None): Seqner instance of cloned first seen ordinal
If cloned mode then firner maybe provided (not None)
When firner provided then compare fn of dater and database and
first seen if not match then log and add cue notify problem
dater (Dater | None): Dater instance of cloned replay datetime
If cloned mode then dater maybe provided (not None)
When dater provided then use dater for first seen datetime
eager (bool): True means try harder to find validate events by
walking KELs. Enables only being eager
in escrow processing not initial parsing.
False means only use pre-existing information
if any, either percolated attached or in database.
local (bool): event source for validation logic
True means event source is local (protected).
False means event source is remote (unprotected).
Event validation logic is a function of local or remote
check (bool): True means do not update the database in any
non-idempotent way. Useful for reinitializing the Kevers from
a persisted KEL without updating non-idempotent first seen .fels
and timestamps.
"""
ked = serder.ked
if not self.transferable: # not transferable so no further events allowed
raise ValidationError("Unexpected event = {} is nontransferable "
" or abandoned state.".format(ked))
if serder.pre != self.prefixer.qb64:
raise ValidationError("Mismatch event aid prefix = {} expecting"
" = {} for evt = {}.".format(serder.pre,
self.prefixer.qb64,
ked))
local = True if local else False
sner = serder.sner # Number instance ensures whole number for sequence number
ilk = serder.ilk # ked["t"]
if ilk in (Ilks.rot, Ilks.drt): # rotation (or delegated rotation) event
if self.delegated and ilk != Ilks.drt:
raise ValidationError("Attempted non delegated rotation on "
"delegated pre = {} with evt = {}."
"".format(serder.pre, ked))
tholder, toader, wits, cuts, adds = self.rotate(serder)
# Validates signers, delegation if any, and witnessing when applicable
# returned sigers and wigers are verified signatures
# If does not validate then escrows as needed and raises ValidationError
sigers, wigers, delpre, delsner, delsger = self.valSigsWigsDel(
serder=serder,
sigers=sigers,
verfers=serder.verfers,
tholder=tholder,
wigers=wigers,
toader=toader,
wits=wits,
delsner=delsner,
delsger=delsger,
eager=eager,
local=local)
# .valSigWigsDel above ensures thresholds met otherwise raises exception
# all validated above so may add to KEL and FEL logs as first seen
fn, dts = self.logEvent(serder=serder, sigers=sigers, wigers=wigers,
wits=wits,
first=True if not check else False,
delnum=delsner, diger=delsger,
firner=firner, dater=dater, local=local)
# nxt and signatures verify so update state
self.sner = sner # sequence number Number instance
self.serder = serder # need whole serder for digest agility compare
self.ilk = ilk
self.tholder = tholder
self.verfers = serder.verfers
self.ndigers = serder.ndigers
self.ntholder = serder.ntholder
self.toader = toader
self.wits = wits
self.cuts = cuts
self.adds = adds
# last establishment event location need this to recognize recovery events
self.lastEst = LastEstLoc(s=self.sner.num, d=self.serder.said)
if fn is not None: # first is non-idempotent for fn check mode fn is None
self.fner = Number(num=fn)
self.dater = Dater(dts=dts)
self.db.states.pin(keys=self.prefixer.qb64, val=self.state())
elif ilk == Ilks.ixn: # subsequent interaction event
if self.estOnly:
raise ValidationError("Unexpected non-establishment event = {}."
"".format(serder.ked))
#for k in IXN_LABELS:
#if k not in ked:
#raise ValidationError("Missing element = {} from {} event."
#" evt = {}.".format(k, Ilks.ixn, ked))
if not sner.num == (self.sner.num + 1): # sn not in order
raise ValidationError("Invalid sn = {} expecting = {} for evt "
"= {}.".format(sner.num, self.sner.num + 1, ked))
if not self.serder.compare(said=ked["p"]): # prior event dig not match
raise ValidationError("Mismatch event dig = {} with state dig"
" = {} for evt = {}.".format(ked["p"],
self.serder.said,
ked))
# interaction event use keys, sith, toad, and wits from pre-existing Kever state
# Validates signers, delegation if any, and witnessing when applicable
# If does not validate then escrows as needed and raises ValidationError
sigers, wigers, delpre, _, _ = self.valSigsWigsDel(
serder=serder,
sigers=sigers,
verfers=self.verfers,
tholder=self.tholder,
wigers=wigers,
toader=self.toader,
wits=self.wits,
eager=eager,
local=local)
# .validateSigsDelWigs above ensures thresholds met otherwise raises exception
# all validated above so may add to KEL and FEL logs as first seen
fn, dts = self.logEvent(serder=serder, sigers=sigers, wigers=wigers,
first=True if not check else False) # First seen accepted
# validates so update state
self.sner = sner # sequence number Number instance
self.serder = serder # need for digest agility includes .serder.diger
self.ilk = ilk
if fn is not None: # first is non-idempotent for fn check mode fn is None
self.fner = Number(num=fn)
self.dater = Dater(dts=dts)
self.db.states.pin(keys=self.prefixer.qb64, val=self.state())
else: # unsupported event ilk so discard
raise ValidationError("Unsupported ilk = {} for evt = {}.".format(ilk, ked))
[docs]
def rotate(self, serder):
"""
Generic Rotate Operation Validation Processing
Validates provisional rotation
Same logic for both 'rot' and 'drt' (plain and delegated rotation)
Returns: tuple (tholder, toader, wits, cuts, adds) of provisional results
of rotation subject to additional validation
Parameters:
serder (SerderKERI): instance of rotation ('rot' or 'drt') event.
"""
ked = serder.ked
sner = serder.sner
pre = serder.pre # ked["i"] # controller AID prefix
prior = serder.prior # ked["p"] # prior event said
ilk = serder.ilk
if sner.num > self.sner.num + 1: # out of order event
raise ValidationError(f"Out of order event sn = {sner.num} expecting"
f" = {self.sner.num + 1} for evt = {ked}.")
elif sner.num <= self.sner.num: # stale or recovery
# stale events could be duplicitous
# duplicity detection should have happend in Kevery before .update
# and .rotate called so raise exception if stale
# seems redundant but protects bare .update if not called by Kevery
if ((ilk == Ilks.rot and sner.num <= self.lastEst.s) or
(ilk == Ilks.drt and sner.num < self.lastEst.s)): # stale event
raise ValidationError("Stale event sn = {} expecting"
" = {} for evt = {}.".format(sner.num,
self.sner.num + 1,
ked))
else: # recovery event rot sn > self.lastEst.s or drt sn = self.lastEst.s
if ilk == Ilks.rot and self.ilk != Ilks.ixn: # rot recovery may only override ixn state
raise ValidationError("Invalid recovery attempt: Recovery"
"at ilk = {} not ilk = {} for evt"
" = {}.".format(self.ilk,
Ilks.ixn,
ked))
psn = sner.num - 1 # use sn of prior event to fetch prior event
# fetch raw serialization of last inserted event at psn
pdig = self.db.kels.getLast(keys=pre, on=psn)
if pdig is None:
raise ValidationError("Invalid recovery attempt: "
"Bad sn = {} for event = {}."
"".format(psn, ked))
pdig = pdig.encode("utf-8")
if (pserder := self.db.evts.get(keys=(pre, pdig))) is None:
raise ValidationError("Invalid recovery attempt: "
" Bad dig = {}.".format(pdig))
if not pserder.compare(said=prior): # bad recovery event
raise ValidationError("Invalid recovery attempt:"
"Mismatch recovery event prior dig"
"= {} with dig = {} of event sn = {}"
" evt = {}.".format(prior,
pserder.said,
psn,
ked))
else: # sn == self.sn + 1 new non-recovery event
if not self.serder.compare(said=prior): # prior event dig not match
raise ValidationError("Mismatch event dig = {} with"
" state dig = {} for evt = {}."
"".format(prior, self.serder.said, ked))
# check derivation code of pre for non-transferable
if not self.ndigers: # prior next list is empty so rotations not allowed
raise ValidationError("Attempted rotation for nontransferable"
" prefix = {} for evt = {}."
"".format(self.prefixer.qb64, ked))
tholder = serder.tholder # Tholder(sith=ked["kt"]) # parse sith into Tholder instance
keys = serder.keys # event's keys ked["k"]
if len(keys) < tholder.size:
raise ValidationError(f"Invalid sith = {serder.tholder} for keys = "
f"{keys} for evt = {ked}.")
# compute wits from existing .wits with new cuts and adds from event
# use ordered set math ops to verify and ensure strict ordering of wits
# cuts and add to ensure that indexed signatures on indexed witness
# receipts work
wits, cuts, adds = self.deriveBacks(serder)
toader = serder.bner # Number(num=ked["bt"]) # auto converts hex num to int
if wits:
if toader.num < 1 or toader.num > len(wits): # out of bounds toad
raise ValueError(f"Invalid toad = {toader.num} for backers "
f"(wits)={wits} for event={ked}.")
else:
if toader.num != 0: # invalid toad
raise ValueError(f"Invalid toad = {toader.num} for backers "
"(wits)={wits} for event={ked}.")
return tholder, toader, wits, cuts, adds
[docs]
def deriveBacks(self, serder):
"""Derives and return tuple of (wits, cuts, adds) for backers given
current set and any changes provided by serder.
Returns:
wca (tuple): of
wits (list[str]): prefixes of witnesses full list (backers)
cuts (list[str]): prefixes of witnesses removed in latest est evt
adds (list[str]): prefixes of witnesses added in latest est evt
Parameters:
serder (SerderKeri): instance of current event
"""
if serder.ilk not in (Ilks.rot, Ilks.drt) or self.sn >= serder.sn: # no changes
return self.wits, self.cuts, self.adds
witset = oset(self.wits)
cuts = serder.cuts
cutset = oset(cuts)
if len(cutset) != len(cuts):
raise ValidationError("Invalid cuts = {cuts}, has duplicates for "
f"evt = {serder.ked}.")
if (witset & cutset) != cutset: # some cuts not in wits
raise ValidationError(f"Invalid cuts = {cuts}, not all members in "
f"wits for evt = {serder.ked}.")
adds = serder.adds
addset = oset(adds)
if len(addset) != len(adds):
raise ValidationError(f"Invalid adds = {adds}, has duplicates for "
f"evt = {serder.ked}.")
if cutset & addset: # non empty intersection
raise ValidationError(f"Intersecting cuts = {cuts} and adds = {adds}"
f"for evt = {serder.ked}.")
if witset & addset: # non empty intersection
raise ValidationError(f"Intersecting wits = {self.wits} and adds ="
f"{adds} for evt = {serder.ked}.")
wits = list((witset - cutset) | addset)
if len(wits) != (len(self.wits) - len(cuts) + len(adds)): # redundant?
raise ValidationError(f"Invalid member combination among wits = "
f"{self.wits}, cuts ={cuts}, and adds = "
f"{adds,} for evt = {serder.ked}.")
return (wits, cuts, adds)
[docs]
def valSigsWigsDel(self, serder, sigers, verfers, tholder,
wigers, toader, wits, *,
delsner=None, delsger=None, eager=False,
local=True):
"""
Returns triple (sigers, wigers, delegator) where:
sigers is unique validated signature verified members of inputed sigers
wigers is unique validated signature verified members of inputed wigers
delegator is qb64 delegator prefix if delegated else None
Validates sigers signatures by validating indexes, verifying signatures, and
validating threshold sith.
Validate witness receipts by validating indexes, verifying
witness signatures and validating toad.
Witness validation is a function of wits .prefixes and .local
Parameters:
serder (SerderKERI): instance of event
sigers (list): of Siger instances of indexed controllers signatures.
Index is offset into verfers list from which public key may be derived.
verfers (list): of Verfer instances of keys from latest est event
tholder (Tholder): instance of sith threshold
wigers (list): of Siger instances of indexed witness signatures.
Index is offset into wits list of associated witness nontrans pre
from which public key may be derived.
toader (Number): instance of backer witness threshold
wits (list): of qb64 non-transferable prefixes of witnesses used to
derive werfers for wigers
delsner (Number | None): instance of delegating event sequence number.
If this event is not delegated then ignored
delsger (Diger | None): instance of delegating event said diger.
If this event is not delegated then diger is ignored
eager (bool): True means try harder to find validate events by
walking KELs. Enables only being eager
in escrow processing not initial parsing.
False means only use pre-existing information
if any, either percolated attached or in database.
local (bool): event source for validation logic
True means event source is local (protected).
False means event source is remote (unprotected).
Event validation logic is a function of local or remote
"""
if len(verfers) < tholder.size:
raise ValidationError("Invalid sith = {} for keys = {} for evt = {}."
"".format(tholder.sith,
[verfer.qb64 for verfer in verfers],
serder.ked))
# Filters sigers to remove any signatures from locally membered groups
# when not local (remote) event source. So that attacker can't source
# remotely compromised but locally membered signatures to satisfy threshold.
if not local and self.locallyMembered(): # is this Kever's pre a local group
if indices := self.locallyContributedIndices(verfers):
for siger in list(sigers): # copy so clean del on original elements
if siger.index in indices:
sigers.remove(siger)
if self.cues:
self.cues.push(dict(kin="remoteMemberedSig",
serder=serder,
index=siger.index))
# get unique verified sigers and indices lists from sigers list
sigers, indices = verifySigs(raw=serder.raw, sigers=sigers, verfers=verfers)
# sigers now have .verfer assigned
# check if minimally signed in order to continue processing
if not indices: # must have a least one verified sig
raise ValidationError("No verified signatures for evt = {}."
"".format(serder.ked))
# at least one valid controller signature so we can now escrow event
# Misfit check events that must be locally sourced (protected).
# Misfit escrow process may repair (upgrade) the protection when appropriate
# Put all misfit checks up front so never goes into any escrow but
# misfit if fails any misfit checks.
# get delegator's delpre if any for misfit check
if serder.ilk == Ilks.dip: # dip so delpre in event "di" field
delpre = serder.delpre # delegator from dip event
if not delpre: # empty or None
raise ValidationError(f"Empty or missing delegator for delegated"
f" inception event = {serder.ked}.")
elif serder.ilk == Ilks.drt: # serder.ilk == Ilks.drt so rotation
delpre = self.delpre # delpre in kever state
else: # not delegable event icp, rot, ixn
delpre = None
# Misfit escrow checks
if (not local and (self.locallyOwned() or
self.locallyWitnessed(wits=wits) or
self.locallyDelegated(pre=delpre))):
self.escrowMFEvent(serder=serder, sigers=sigers, wigers=wigers,
delsner=delsner, delsger=delsger, local=local)
raise MisfitEventSourceError(f"Nonlocal source for locally owned or"
f"locally witnessed or locally delegated"
f"event={serder.ked}, local aids="
f"{self.prefixes}, {wits=}, "
f"delgator={delpre}.")
werfers = [Verfer(qb64=wit) for wit in wits] # get witness public key verifiers
# get unique verified wigers and windices lists from wigers list
wigers, windices = verifySigs(raw=serder.raw, sigers=wigers, verfers=werfers)
# each wiger now has added to it a werfer of its wit in its .verfer property
# escrow if not fully signed vs signing threshold
pre = self.prefixer.qb64
if not tholder.satisfy(indices): # at least one but not enough
self.escrowPSEvent(serder=serder, sigers=sigers, wigers=wigers,
delsner=delsner, delsger=delsger, local=local)
msg = (f"AID {pre[:4]}...{pre[-4:]}: Failure satisfying sith = {tholder.sith} "
f"on sigs {[siger.qb64 for siger in sigers]} "
f"for evt = {serder.said}")
logger.trace(msg)
logger.trace("Event Body=\n%s\n", serder.pretty())
raise MissingSignatureError(msg)
# escrow if not fully signed vs prior next rotation threshold
if serder.ilk in (Ilks.rot, Ilks.drt): # rotation so check prior next threshold
# prior next threshold in .ntholder and digers in .ndigers
ondices = self.exposeds(sigers)
if not self.ntholder.satisfy(indices=ondices):
self.escrowPSEvent(serder=serder, sigers=sigers, wigers=wigers,
delsner=delsner, delsger=delsger,local=local)
msg = (
f"AID {pre[:4]}...{pre[-4:]}: Failure satisfying prior nsith = {self.ntholder.sith} "
f"with exposed sigs {[siger.qb64 for siger in sigers]} "
f"for new est evt={serder.said}")
logger.trace(msg)
logger.trace("Event Body=\n%s\n", serder.pretty())
raise MissingSignatureError(msg)
# this point the sigers have been verified and the wigers have been verified
# even if locallyOwned or locallyMembered or locallyWitnessed.
# But the toad has not yet been verified.
if not wits:
if toader.num != 0: # bad toad non-zero and not witnessed bad event
raise ValidationError(f"Invalid toad = {toader.num} for wits = {wits}")
else: # wits so may need to validate toad
# The toad is only verified against the wigers as fully witnessed if
# not (locallyOwned or locallyMembered or locallyWitnessed)
if (not (self.locallyOwned() or self.locallyMembered() or
self.locallyWitnessed(wits=wits))):
if wits: # is witnessed
if toader.num < 1 or toader.num > len(wits): # out of bounds toad
raise ValidationError(f"Invalid toad = {toader.num} for wits = {wits}")
else: # not witnessed
if toader.num != 0: # invalid toad
raise ValidationError(f"Invalid toad = {toader.num} for wits = {wits}")
if len(windices) < toader.num: # not fully witnessed yet
if self.escrowPWEvent(serder=serder, wigers=wigers, sigers=sigers,
delsner=delsner, delsger=delsger,
local=local):
# cue to query for witness receipts
self.cues.push(dict(kin="query", q=dict(pre=serder.pre, sn=serder.snh)))
msg = (f"AID {pre[:4]}...{pre[-4:]}: Failure satisfying toad={toader.num} "
f"on witness sigs {[siger.qb64 for siger in wigers]} "
f"for event={serder.said}")
logger.info(msg)
logger.debug("Event Body=\n%s\n", serder.pretty())
raise MissingWitnessSignatureError(msg)
# Delegator approves delegation by attaching valid source
# seal and reprocessing event which shows up here as delnum, deldiger.
# Won't get to here if not local and locallyDelegated(delpre) misfit
# checks above will send nonlocal sourced delegable event to
# misfit escrow first. Mistfit escrow must first promote to local and
# reprocess event before we get to here. Assumes that attached source
# seal in this case can't be malicious since sourced locally.
# Doesn't get to here until fully signed and witnessed.
if (serder.ilk in (Ilks.dip, Ilks.drt) and self.locallyDelegated(delpre)
and not self.locallyOwned()): # local delegator of delegated event
if delsner is None or delsger is None: # missing delegation seal
# so escrow delegable. So local delegator can approve OOB.
# and create delegator event with valid event seal of this
# delegated event and then reprocess event with attached source
# seal to delegating event, i.e. delnum, deldiger.
self.escrowDelegableEvent(serder=serder, sigers=sigers,
wigers=wigers, local=local)
msg = f"Missing approval for delegation by {delpre} of event = {serder.said}"
logger.info(msg)
logger.debug("Event Body=\n%s\n", serder.pretty())
raise MissingDelegableApprovalError(msg)
# validateDelegation returns (None, None) when delegation validation
# does not apply. Raises ValidationError if validation applies but
# does not validate.
delsner, delsger = self.validateDelegation(serder,
sigers=sigers,
wigers=wigers,
wits=wits,
delpre=delpre,
delsner=delsner,
delsger=delsger,
eager=eager,
local=local)
return (sigers, wigers, delpre, delsner, delsger)
[docs]
def exposeds(self, sigers):
"""Returns list of ondices (indices) suitable for Tholder.satisfy
from self.ndigers (prior next key digests ) as exposed by event sigers.
Uses dual index feature of siger. Assumes that each siger.verfer is
from the correct key given by siger.index and the signature has been verified.
A key given by siger.verfer (at siger.index in the current key list)
may expose a prior next key hidden by the diger at siger.ondex in .digers.
Each returned ondex must be properly exposed by a siger in sigers
such that the siger's indexed key given by siger.verfer matches the
siger's ondexed digest from digers.
The ondexed digest's code is used to compute the digest of the corresponding
indexed key verfer to verify that they match. This supports crypto agility
for different digest codes, i.e. all digests in .digers may use a different
algorithm.
Only ondices from properly matching key and digest are returned.
Used to extract the indices from the list of prior next digests .digers
exposed by the signatures (sigers) on a rotation event of the newly
current keys given by each .verfer at .index from sigers. Only checks
keys and digests that correspond to provided signatures not all keys and
digests defined by the rotation event.
Parameters:
sigers (list): of Siger instances of indexed signature with .verfer
"""
odxs = []
for siger in sigers:
try:
diger = self.ndigers[siger.ondex]
except TypeError as ex: # ondex may be None
continue
except IndexError as ex:
continue
#raise ValidationError(f'Invalid ondex={siger.ondex} '
#f'to expose digest.') from ex
kdig = Diger(ser=siger.verfer.qb64b, code=diger.code).qb64
if kdig == diger.qb64:
odxs.append(siger.ondex)
return odxs
[docs]
def validateDelegation(self, serder, sigers, wigers, wits, delpre, *,
delsner=None, delsger=None, eager=False, local=True):
"""
Returns delegator's qb64 identifier prefix if validation successful.
Assumes that local vs remote source checks have been applied before
this function is called.
Rules:
If event is not a delegated event then not valid delegation
If delegatee's own event (.mine) then valid delegation
If delegation seal found in delgator's KEL then valid delegation given
valid superseding rules below
Otherwise escrow or reject if error condition
seal validates with respect to Delegator's KEL
Location Seal is from Delegate's establishment event
Assumes state setup
self is last accepted event if any or yet to be accepted event,
serder is received event
Parameters:
serder (SerderKERI): instance of delegated event serder
sigers (list[Siger]): of Siger instances of indexed controller sigs of
delegated event. Assumes sigers is list of unique verified sigs
wigers (list[Siger]): of optional Siger instance of indexed witness sigs of
delegated event. Assumes wigers is list of unique verified sigs
wits (list[str]): of qb64 non-transferable prefixes of witnesses used to
derive werfers for wigers
delpre (str): qb64 prefix of delegator
delsner (Number | None): instance of delegating event sequence number.
If this event is not delegated then ignored
delsger (Diger | None): instance of delegating event said diger.
If this event is not delegated ignored
local (bool): event source for validation logic
True means event source is local (protected).
False means event source is remote (unprotected).
Event validation logic is a function of local or remote
eager (bool): True means try harder to validate event by
walking KELs. Enables only being eager
in escrow processing not initial parsing.
False means only use pre-existing information
if any, either percolated attached or in database.
local (bool): event source for validation logic
True means event source is local (protected).
False means event source is remote (unprotected).
Event validation logic is a function of local or remote
Returns:
None
Process Logic:
A delegative event is processed differently for each of four different
parties, namely, controller of event, witness to controller of event,
delegator of event , and validator of event that is not controller,
witness or delegator. Events are processed as either local (protected)
or remote. A local event may assume that the event only came via a
protected transmission path. This might be because the event is
functions locally on a device under the supervision of the controller
or was received via some protected channel using some form of MFA.
A remote event is received in an unprotected manner. The purpose of
local and remote is to allow increased security on local events where
a threshold structure is imposed.
A witness pool may act a threshold structure for enchanced security
when each witness only accepts local events that are protected
by a unique authentication factor thereby making the controller's
signature(s) the first factor and the set of unique witness factors
a secondary threshold factor. An attacker therefore has to compromise
not merely the controller's private key(s) but also the unique second factor
on each of a threshold satisfycing number of witnesses.
Likewise a delegator may act as a threshold structure for enhanced security
when the delegator only accepts local events for delegation that are
protected by a unique authentication factor thereby making the
controller's signatures the first factor, a threshold satisfycing
number of unique witness factors the second layer of factors, and
the delegator's unique authentication factor as the third factor.
An attacker therefore has to compromise not merely the controller's
private key(s) but also the unique second factor
on each of a threshold satisfycing number of witnesses and the unique
third factor for the delegator.
Controller as delegatee must accept its own delegated event prior
to full witnessing or delegator approval (anchored seal) by signing the
event in order to trigger the logic to get witness receipts and
delegator approval. This means a local (protected) event may be
accepted into controller's KEL when fully signed by controller.
Witness must accept a controller's delegated event it witnesses prior to
full witnessing or delegator approval in order to trigger its
witnessing logic. This means a local (protected) event may be
accepted into a witness' KEL when fully signed by its controller.
Delegator may escrow or sandbox a delegated event prior to it anchoring
a seal of the event in its KEL in order to trigger its approval logic.
Alternatively the approval logic may be triggered immediately after
it is received and authenticated on it its local (protected) channel
but before it is submitted to its local Kevery for processing.
The delegator MUST NOT accept a delegable event unless it is locally
sourced, fully signed by its controller, and fully witnessed by its
controller's designated witness pool.
A Delegator may impose additional validation logic prior to approval.
The approval logic may be handled by an escrow that only runs if
the delegable event is sourced as local. This may require a
sandboxed kel for the delegatee in order to not corrupt its pristine
copy of the delegatee's KEL with a valid delegable event from a
malicious source. The sandboxing logic may create a virtual
delegation event with seal for the purpose of checking the delegated
event superseding logic prior to acceptance.
A malicious attacker that compromises the pre-rotated keys of the
delegatee may issue a rotation that changes its witness pool in order
to bypass the local security logic of the witness pool. The approval
logic of the delegator may choose to not automatically approve a
delegable rotation event unliess the change to the witness pool is
below the threshold.
The logic for superseded events is NOT a requirement for acceptance in
either a delegated event controller's KEL or its witness' KEL. The
delegator's kel creates a virtual (provisional) delegating interaction
event in order to evaluate correct superseding logic so as not to
accept an invalid supderseding delegated event into its local copy
of the delegated KEL. This virtual event is needed because superseding
logic requires an anchoring seal be present before the rules can
be fully evaluated.
Should the actual anchor be via a superseding rotation in the
delegator's KEL not via an interaction event then the delegator must
check the logic for a virtual delegating rotation instead.
In either case the delegated event does not change so the virtual
delegating checks are sufficient to accept the delegated event
into the delegator's local copy of the delegatee's KEL.
Any of delegated controller, delegated witness, or delegator
of delegated event may after the fact fully validate event by
processing it as a remote event.
Then the logic applied is same as validator below.
A validator of a delegated event that is not the event's controller,
witness, or delegator must not accept the event until is is fully
signed by the controller (threshold), fully witnessed by the witness
pool (threshold) and its seal anchored in the delegator's KEL. The
rules for event superseding in the delegated controller's kel must
also be satisfied. The logic should be the same for both local and
remote event because the validator is not one of the protected parties
to the event.
Superseding Recovery:
Supersede means that after an event has already been accepted as first seen
into a KEL that a different event with the same sequence number is accepted
that supersedes the pre-existing event at that sn. This enables the recovery of
events signed by compromised keys. The result of superseded recovery is that
the KEL is forked at the sn of the superseding event. All events in the
superseded branch of the fork still exist but, by virtue of being superseded,
are disputed. The set of superseding events in the superseding fork forms
the authoritative branch of the KEL.
All the already seen superseded events in the superseded fork
still remain in the KEL and may be viewed in order of their original acceptance
because the database stores all accepted events in order of acceptance and
denotes this order using the first seen ordinal number, fn.
Recall that the fn is not the same as the sn (sequence number).
Each event accepted into a KEL has a unique fn but multiple events due to
recovery forks may share the same sn.
Superseding Rules for Recovery at given 'sn' (sequence number)
A0. Any rotation event may supersede an interaction event at the same sn.
where that interaction event is not before any other rotation event.
(existing rule)
A1. A non-delegated rotation may not supersede another rotation at the
same sn. (modified rule)
A2. An interaction event may not supersede any event. ( existing rule).
(B. and C. below provide the new rules)
B. A delegated rotation may supersede the latest seen delegated rotation
at the same sn under either of the following conditions:
B1. The superseding rotation's delegating event is later than
the superseded rotation's delegating event in the delegator's KEL,
i.e. the sn of the superseding event's delegation is higher than
the superseded event's delegation.
B2. The sn of the superseding rotation's delegating event is the
same as the sn of the superseded rotation's delegating event in the
delegator's KEL both have the same delegating event and the index
of the delegating seal of the superceding delegation seal is later
(higher) than the index of the delegating seal of the supderseded
rotation. In other words the delegating event is the same event but
the superseding delegation seal appears later in the seal list of the
delegating event than the superseded delegation seal.
B3. The sn of the superseding rotation's delegating event is the
same as the sn of the superseded rotation's delegating event in the
delegator's KEL and the superseding rotation's delegating event is
a rotation and the superseded rotation's delegating event is an
interaction, i.e. the delegating events are not the same event and
the superseding rotation's delegating event is itself a superseding
rotation of the superseded rotations delegating interaction event
in the delgator's KEL.
C. IF Neither A nor B is satisfied, then recursively apply rules A. and B. to
the delegating events of those delegating events and so on until
either A. or B. is satisfied, or the root KEL of the delegation
which must be undelegated has been reached.
C1. If neither A. nor B. is satisfied by recursive application on the
delegator's KEL (i.e. the root KEL of the delegation has been reached
without satisfaction) then the superseding rotation is discarded.
The terminal case of the recursive application will occur at the
root KEL which by defintion is non-delegated wherefore either
A. or B. must be satisfied, or else the superseding rotation must
be discarded.
Note: The latest seen delegated rotation constraint means that any earlier
delegated rotations CAN NOT be superseded. This greatly simplifies the
validation logic and avoids a potential infinite regress of forks in the
delegated identifier's KEL while allowing the delegate to
detect that a compromised delegation has occurred and give an
opportunity for the delegator to refuse to approve a subsequent
delegated rotation without additional verification with the delegate
that the subsequent delegated rotation was not compromised.
In order to capture control of a delegated identifier the attacker must
issue a delegated rotation that rotates to keys under the control of the
attacker that must be approved by the delegator. A recovery rotation must
therefore supersede the compromised rotation. If the attacker is able
to issue and get approved by the delegator a subsequent rotation
that follows but does not supersede the compromising rotation then
recovery is no longer possible because the delegatee would no longer
control the privete keys needed to verifiably sign a recovery rotation.
One way that detectability may be assured is when the delegator imposes
a minimum time between approvals of a delegated rotation that is
sufficient for the delgate to detect a compromised rotation recovery.
Attempts to rotate sooner than the minimum time since the immediately
prior rotation are refused until further verification has occurred.
A delegated rotation that occurs after the minimum time since the
immediately prior delegated rotation might be automatically approved
to minimize latency. While a subsequent delegated rotation that occurs
within the minimum time would not be approed to maximize safety.
The minimum time window is designed to give the delegate enough time
to detect a comprimised or duplicitious superseding rotation and
prevent the additional verification from proceding.
Mitigations of malicious source seal couples:
Repair the approval source seal couple in the 'aess' database on recursive
climb the kel tree. Once an event has been accepted into its kel.
Later adding a source seal couple to 'aes' should then be OK from a
security perspective since its only making discovery less expensive.
When a malicious source seal couple is received but event is validly
delegated and the delegation source seal is repaired then replace
malicious source seal couple with repaired seal so repaired seal not
malicous seal gets written to 'aes' db. When the event is valid but
non-delegated then need to nullify malicous source seal couple so it
does not get written to 'aes' database.
"""
if not delpre: # not delegable delpre is None if ilk is not dip or drt
return (None, None) # non-delegated so delnum deldiger must be None
# if we are the delegatee, accept the event without requiring the
# delegator validation via an anchored delegation seal or by requiring
# it to be witnessed. Query witness cue in Kevery will then request witnessing.
# Controller accepts without waiting for witnessor nor for the delegation
# seal to be anchored in delegator's KEL.
# Witness accepts without waiting for delegation seal to be anchored in
# delegator's KEL. Witness cue in Kevery will then generate receipt
if (self.locallyOwned() or self.locallyMembered() or
self.locallyWitnessed(wits=wits)):
return (None, None) # not validated so delnum deldiger must be None
if self.kevers is None or delpre not in self.kevers: # missing delegator KEL
# ToDo XXXX cue a trigger to get the KEL of the delegator. This may
# require OOBIing with the delegator.
# The processPDEvent should also cue a trigger to get KEL
# of delegator if still missing when processing escrow later.
self.escrowPDEvent(serder=serder, sigers=sigers, wigers=wigers,
delsner=delsner, delsger=delsger, local=local)
msg = f"Missing KEL of delegator {delpre} of evt {serder.sn} {serder.ilk} {serder.said}"
logger.info(msg)
logger.debug("Event Body=\n%s\n", serder.pretty())
raise MissingDelegationError(msg)
dkever = self.kevers[delpre] # get delegator's KEL
if dkever.doNotDelegate: # drop event if delegation not allowed
msg = (f"Delegator {delpre} does not allow delegation on evt "
f"{serder.sn} {serder.ilk} {serder.said}")
logger.info(msg)
logger.debug("Event Body=\n%s\n", serder.pretty())
raise ValidationError(msg)
dserder = None # no delegation event yet
if delsner is None or delsger is None: # missing delegation seal ref
if eager: # walk kel here to find
seal = dict(i=serder.pre, s=serder.snh, d=serder.said)
dserder = self.db.fetchLastSealingEventByEventSeal(pre=delpre,
seal=seal)
if dserder is not None: # found seal in dserder
delsner = Number(num=dserder.sn) # replace with found
delsger = Diger(qb64=dserder.said) # replace with found
if not dserder: # just escrow and try later
self.escrowPDEvent(serder=serder, sigers=sigers, wigers=wigers,
delsner=delsner, delsger=delsger, local=local)
msg = (f"No delegation seal for delegator {delpre} on evt "
f"{serder.sn} {serder.ilk} {serder.said}")
logger.info(msg)
logger.debug("Event Body=\n%s\n", serder.pretty())
raise MissingDelegationError(msg)
if delsner and delsger and not dserder: # given couple not found
# Get delegating event from delnum and delpre
ssn = delsner.validate(inceptive=False).sn
# get the dig of the delegating event. Using getKeLast ensures delegating
# event has not already been superceded
# get dig of last delegating event purported at sn
raw = self.db.kels.getLast(keys=delpre, on=ssn) # last means not disputed or superseded
if raw is None: # no delegatint event yet. no index at key pre, sn
# Have to wait until delegating event at sn shows up in kel
# ToDo XXXX process this cue of query to fetch delegating event from
# delegator
self.cues.push(dict(kin="query", q=dict(pre=delpre,
sn=delsner.numh,
dig=delsger.qb64)))
# escrow event here
inceptive = True if serder.ilk in (Ilks.icp, Ilks.dip) else False
sn = Number(num=serder.sn).validate(inceptive=inceptive).sn
# if we were to allow for malicous local delegator source seal then we
# must check for locallyOwned(delpre) first and escrowDelegable.
# otherwise escrowPDEvent
self.escrowPDEvent(serder=serder, sigers=sigers, wigers=wigers,
delsner=delsner, delsger=delsger, local=local)
msg = (f"No delegating event from {delpre} at {delsger.qb64} for evt "
f"{serder.sn} {serder.ilk} {serder.said}")
logger.info(msg)
logger.debug("Event Body=\n%s\n", serder.pretty())
raise MissingDelegationError(msg)
ddig = raw.encode("utf-8")
# get the latest delegating event candidate from dig given by pre,sn index
if (dserder := self.db.evts.get(keys=(delpre, ddig))) is None: # drop event should never happen unless database is broken
msg = (f"Missing delegation from {delpre} at event dig = {ddig} for evt "
f"{serder.sn} {serder.ilk} {serder.said}")
logger.info(msg)
logger.debug("Event Body=\n%s\n", serder.pretty())
raise ValidationError(msg)
found = False # find event seal of delegated event in delegating data
# search purported delegating event from source seal couple
for dseal in dserder.seals: # find delegating seal anchor
# XXXX ToDo need to change logic here to support native CESR seals not just dicts
# for JSON, CBOR, MGPK
if tuple(dseal) == SealEvent._fields:
dseal = SealEvent(**dseal)
if (dseal.i == serder.pre and
dseal.s == serder.sner.numh and
serder.compare(said=dseal.d)):
found = True
break
if not found: # nullify and escrow to try harder later
# worst case assume source seal was malicious so nullify it and
# attempt to repair by escrowing and eager search later
delsner = delsger = None # nullify
self.escrowPDEvent(serder=serder, sigers=sigers, wigers=wigers,
delsner=delsner, delsger=delsger, local=local)
msg = (f"No delegation seal for delegator {delpre} of evt "
f"{serder.sn} {serder.ilk} {serder.said}")
logger.info(msg)
logger.debug("Event Body=\n%s\n", serder.pretty())
raise MissingDelegationError(msg)
## Found valid anchoring seal of delegator delpre
## compare saids to ensure match of delegating event and source seal
## repair source seal to match last event at sn in source seal.
## Original delegating event in seal may have been disputed or
## superseded, but if latest matches then we want to repair
#if not dserder.compare(said=deldiger.qb64): # drop event
#raise ValidationError(f"Invalid delegation from {delpre} at event"
#f" dig={ddig} for evt={serder.ked}.")
delsner = Number(numh=dserder.snh) # replace with found
delsger = Diger(qb64=dserder.said) # replace with found
# Since found valid anchoring seal so can confirm delegation successful
# unless its one of the superseding conditions.
# Valid if not superseding drt of drt.
# Assumes database is reverified each bootup or otherwise when
# chain-of-custody of disc has been broken.
# Rule for non-supeding delegated rotation of rotation.
# Returning delegator indicates success and eventually results in acceptance
# via Kever.logEvent which also writes the delgating event source couple to
# db.aess so we can find it later
if ((serder.ilk == Ilks.dip) or # superseding is delegated inception or
(serder.sner.num == self.sner.num + 1) or # superseding is inorder later or
(serder.sner.num == self.sner.num and # superseding event at same sn and
self.ilk == Ilks.ixn and # superseded is interaction and
serder.ilk == Ilks.drt)): # superseding is rotation
return (delsner, delsger) # indicates delegation valid
# get to here means drt rotation superseding another drt rotation
# Kever.logEvent saves authorizer (delegator) seal source couple in
# db.aess (.getAes, .setAes etc) data base so can use it here to
# recusively look up delegating events
# set up recursive search for superseding delegations
# get original potentially superseded delegation
serfo = self.serder # original accepted delegated event i.e. serf original
if not (bosso := self.fetchDelegatingEvent(delpre, serfo, original=True,
eager=eager)):
self.escrowPDEvent(serder=serder, sigers=sigers, wigers=wigers,
delsner=delsner, delsger=delsger, local=local)
msg = (f"No delegating event from {delpre} at {delsger.qb64} for evt "
f"{serder.sn} {serder.ilk} {serder.said}")
logger.info(msg)
logger.debug("Event Body=\n%s\n", serder.pretty())
raise MissingDelegationError(msg)
# already have new potential superseding delegation
serfn = serder # new potentially superseding delegated event i.e. serf new
bossn = dserder # new delegating event of superseding delegated event i.e. boss new
while (True): # superseding delegated rotation of rotation recovery rules
# Only get to here if same sn for delegated event and both
# existing and superseding delegated events are rotations
if (bossn.sn > bosso.sn or # superseding delgation is later or
(bossn.Ilk == Ilks.drt and # superseding delegation is rotation and
bosso.Ilk == Ilks.ixn) ): # superseded delegation is interaction
# valid superseding delegation up chain so tail link valid
return (delsner, delsger) # tail event's delegation source
if bossn.said == bosso.said: # same delegating event
nseals = [SealEvent(**seal) for seal in bossn.seals
if tuple(seal) == SealEvent._fields]
nindex = nseals.index(SealEvent(i=serfn.pre,
s=serfn.snh,
d=serfn.said))
oseals = [SealEvent(**seal) for seal in bosso.seals
if tuple(seal) == SealEvent._fields]
oindex = oseals.index(SealEvent(i=serfo.pre,
s=serfo.snh,
d=serfo.said))
if nindex > oindex: # superseding delegation seal is later
# assumes index can't be None
# valid superseding delegation up chain so tail link valid
return (delsner, delsger) # tail event's delegation source
else: # not superseded
# ToDo: XXXX may want to cue up business logic for delegator
# if self.mine(delegator): # failed attempt at recovery
msg = f"Invalid delegation recovery rotation of {serfo.pre} by {serfn.pre}"
logger.info(msg)
logger.debug("Delegate Event Body=\n%s\n", serfo.pretty())
logger.debug("Delegator Event Body=\n%s\n", serfn.pretty())
raise ValidationError(msg)
# tie condition same sn and drt so need to climb delegation chain
serfn = bossn
if not (bossn := self.fetchDelegatingEvent(delpre, serfn,
original=False,
eager=eager)):
self.escrowPDEvent(serder=serder, sigers=sigers, wigers=wigers,
delsner=delsner, delsger=delsger, local=local)
msg = (f"No delegating event from {delpre} at {delsger.qb64} for evt "
f"{serder.sn} {serder.ilk} {serder.said}")
logger.info(msg)
logger.debug("Event Body=\n%s\n", serder.pretty())
raise MissingDelegationError(msg)
serfo = bosso
if not (bosso := self.fetchDelegatingEvent(delpre, serfo,
original=True,
eager=eager)):
self.escrowPDEvent(serder=serder, sigers=sigers, wigers=wigers,
delsner=delsner, delsger=delsger, local=local)
msg = (f"No delegating event from {delpre} at {delsger.qb64} for evt "
f"{serder.sn} {serder.ilk} {serder.said}")
logger.info(msg)
logger.debug("Event Body=\n%s\n", serder.pretty())
raise MissingDelegationError(msg)
# repeat
# should never get to here
[docs]
def fetchDelegatingEvent(self, delpre, serder, *, original=True, eager=False):
"""Returns delegating event of delegator given by its aid delpre of
delegated event given by serder otherwise raises ValidationError.
Assumes delegation event must have been accepted at some point even if
it has subsequently been superseded. Therefore only looks in .aess for
delegating event source seal references. So do not use for fetching
delegating eventsthat have yet to be accepted.
Checks that found delegation events have both been accepted.
When delegated event in serder has been accepted then will repair its
.aess entry if needed.
Returns:
dserder (SerderKERI): delegating key event with delegating seal of
delegated event serder. If can't fetch then
returns None when eager==False or raises
ValidationError if can't fetch but eager==True
Parameters:
delpre (str): qb64 of identifier prefix of delegator
serder (SerderKERI): delegated serder
original (bool): True means delegated event is the original candidate
to be superseded. This means kel walk search should
include superseded or disputed events.
False means the delegated event is new candidate to
supersede. This means kel walk search should not
include superseded or disputed events.
eager (bool): True means do more expensive KEL walk instead of escrow
False means do not do expensive KEL walk now.
Assumes db.aes source seal couple of delegating event cannot be written
unless that event has been accepted (first seen) into delegator's KEL
even if it has later been superseded.
Confirms this by checking the .fons database of the delegator.
A malicious escrow of delegating event could only write source seal
couple to escrow in db.udes not in accepted db.aes
Delegating event may have been superceded but delegated event validator
does not know it yet because db.aes keeps original delegating event
source seal from before is was superseded.
Therefore lookup of delegating event needs to check delegating event via
db.fons and not last key event in .kels which would only return the
superseding event.
When walking delegation tree a given delegating event may have
been superceded by another delegating event in the same delegator
KEL. This method does not distinguish between superseding and
superseded so can't assume that the delegating event is the last
event at its sn, i.e. the superseding event.
So this method must use db.fons to ensure that delegating
event was accepted (first seen) even if it has subsequently been
superseded.
When the .aess entry is missing and eager is True and a valid delegation
is found and if delegate has been accepted then repairs missing .aess
entry. Otherwise does not repair but simply returns found delegation.
Found delegation may not be superseding so do not repair .aess unless
delegate was already accepted.
"""
if (duple := self.db.aess.get(keys=(serder.preb, serder.saidb))): # delegation source couple at delegate
delnum, diger = duple
deldig = diger.qb64 # dig of delegating event
# extra careful double check that .aes is valid by getting
# fner = first seen Number instance index
if not self.db.fons.get(keys=(delpre, deldig)): # Not first seen yet?
if original: # should not happen aes database broken
# repair by deleting aes and returning None so it escrows
# and then next time around find below with repair it
self.db.aess.rem(keys=(serder.preb, serder.saidb)) # delete aes so next time repairs it
# superseding may not have happened yet so let it escrow
return None
if not (dserder := self.db.evts.get(keys=(delpre, deldig))): # in fons but no event
# database broken this should never happen
msg = f"Missing delegation event for {serder.said}"
logger.info(msg)
logger.debug("Event Body=\n%s\n", serder.pretty())
raise ValidationError(msg)
# original delegating event i.e. boss original
return dserder
elif eager: # missing aes but try to find seal by walking delegator's KEL
seal = SealEvent(i=serder.pre, s=serder.snh, d=serder.said)._asdict()
if original: # search all events in delegator's kel not just last
if not (dserder := self.db.fetchLastSealingEventByEventSeal(pre=delpre,
seal=seal)):
# database broken this should never happen so do not validate
# since original must have been validated so it must have
# all its delegation chain.
msg = f"Missing delegation source seal for {serder.said}"
logger.info(msg)
logger.debug("Event Body=\n%s\n", serder.pretty())
raise ValidationError(msg)
else: # only search last events in delegator's kel
if not (dserder := self.db.fetchLastSealingEventByEventSeal(pre=delpre,
seal=seal)):
# superseding delegation may not have happened yet so escrow
# ToDo XXXX need to cue up to get latest events in
# delegator's kel.
# raise ValidationError(f"Missing delegation source seal for {serder.ked}")
return None
# Only repair .aess when found delegation is for delegated event that
# has been accepted delegated event i.e has .fons entry
if self.db.fons.get(keys=(serder.pre, serder.said)):
# Repair .aess of delegated event by writing found source
# seal couple of delegation. This is safe becaause we confirmed
# delegation event was accepted in delegator's kel.
diger = Diger(qb64b=dserder.saidb)
self.db.aess.pin(keys=(serder.preb, serder.saidb), val=(Number(num=dserder.sn), diger)) # authorizer (delegator/issuer) event seal
return dserder
else: # not found so return None to escrow and get fixed later
return None
[docs]
def logEvent(self, serder, sigers=None, wigers=None, wits=None, first=False,
delnum=None, diger=None, firner=None, dater=None, local=True):
"""
Update associated logs for verified event.
Update is idempotent. Logs will not write dup at key if already exists.
Parameters:
serder is SerderKERI instance of current event
sigers is optional list of Siger instance for current event
wigers is optional list of Siger instance of indexed witness sigs
wits is optional list of current witnesses provide during any establishment event
first is Boolean True means first seen accepted log of event.
Otherwise means idempotent log of event to accept additional
signatures beyond the threshold provided for first seen
delnum is Number instance of delegating event sequence number.
If this event is not delegated then delnum is ignored
diger is Diger instance of of delegating event said.
If this event is not delegated then diger is ignored
firner is optional Seqner instance of cloned first seen ordinal
If cloned mode then firner maybe provided (not None)
When firner provided then compare fn of dater and database and
first seen if not match then log and add cue notify problem
dater is optional Dater instance of cloned replay datetime
If cloned mode then dater maybe provided (not None)
When dater provided then use dater for first seen datetime
local (bool): event source for validation logic
True means event source is local (protected).
False means event source is remote (unprotected).
Event validation logic is a function of local or remote
"""
local = True if local else False
fn = None # None means not a first seen log event so does not return an fn
dgkeys = (serder.pre, serder.said)
dgkey = dgKey(serder.preb, serder.saidb)
nowdater = Dater() # now timestamp
self.db.dtss.put(keys=dgkey, val=nowdater) # idempotent do not change dts if already
if sigers:
self.db.sigs.put(keys=dgkey, vals=sigers) # idempotent
if wigers:
self.db.wigs.put(keys=dgkey, vals=wigers)
if wits:
self.db.wits.put(keys=dgkey, vals=[Prefixer(qb64=w) for w in wits])
self.db.evts.put(keys=(serder.pre, serder.said), val=serder) # idempotent (maybe already excrowed)
# update event source
# delegation for authorized delegated or issued event
# when delnum and diger are provided they are only assured to be valid
# kever for event if kel is delegated and not locallyOwned
# and not locallyWitnessed as the validateDelegation is short circuited
# for non delegated kels, local controllers, and local witnesses.
# These checks prevent ddos via malicious source seal attachments.
# MUST NOT setAes if not delegated or locallyOwned or locallyWitnessed
if (self.delpre and not serder.ilk == Ilks.ixn and not self.locallyOwned()
and not self.locallyWitnessed(wits=wits) and delnum and diger):
self.db.aess.pin(keys=(serder.preb, serder.saidb), val=(Number(num=delnum.num, code=NumDex.Huge), diger)) # authorizer (delegator/issuer) event seal
if esr := self.db.esrs.get(keys=dgkeys): # preexisting esr
if local and not esr.local: # local overwrites prexisting remote
esr.local = local
self.db.esrs.pin(keys=dgkeys, val=esr)
# otherwise don't change
else: # not preexisting so put
esr = EventSourceRecord(local=local)
self.db.esrs.put(keys=dgkeys, val=esr)
pre = self.prefixer.qb64
if first: # append event dig to first seen database in order
fn = self.db.fels.append(keys=serder.preb, val=serder.saidb)
if firner and fn != firner.sn: # cloned replay but replay fn not match
if self.cues is not None: # cue to notice BadCloneFN
self.cues.push(dict(kin="noticeBadCloneFN", serder=serder,
fn=fn, firner=firner, dater=dater))
logger.info("Kever: Mismatch Cloned Replay FN: %s First seen "
"ordinal fn %s and clone fn %s, said=%s",
serder.preb, fn, firner.sn, serder.said)
logger.debug("Event body=\n%s\n", serder.pretty())
if dater: # cloned replay use original's dts from dater
nowdater = dater
self.db.dtss.pin(keys=dgkey, val=nowdater) # first seen so set dts to now
self.db.fons.pin(keys=dgkey, val=Seqner(sn=fn))
logger.debug("AID %s...%s: First seen %s at sn=%s valid event SAID=%s for %s at %s",
pre[:4], pre[-4:], serder.ilk, fn, serder.said,
serder.pre, nowdater.dts)
logger.debug("Event Body=\n%s\n", serder.pretty())
self.db.kels.add(keys=serder.preb, on=serder.sn, val=serder.saidb)
logger.info("AID %s...%s: Added to KEL %s at sn=%s valid event SAID=%s",
pre[:4], pre[-4:], serder.ilk, serder.sn, serder.said)
logger.debug("Event Body=\n%s\n", serder.pretty())
return (fn, nowdater.dts) # (fn int, dts str) if first else (None, dts str)
[docs]
def escrowMFEvent(self, serder, sigers, wigers=None,
delsner=None, delsger=None, local=True):
"""
Update associated logs for escrow of MisFit event
Parameters:
serder (SerderKERI): instance of event
sigers (list): of Siger instance for event
wigers (list): of witness signatures
delsner (Number): instance of sn of delegating event if any
delsger (Diger): instance of said digest of delegating event if any
local (bool): event source for validation logic
True means event source is local (protected).
False means event source is remote (unprotected).
Event validation logic is a function of local or remote
"""
local = True if local else False
dgkey = dgKey(serder.preb, serder.saidb)
if esr := self.db.esrs.get(keys=dgkey): # preexisting esr
if local and not esr.local: # local overwrites prexisting remote
esr.local = local
self.db.esrs.pin(keys=dgkey, val=esr)
# otherwise don't change
else: # not preexisting so put
esr = EventSourceRecord(local=local)
self.db.esrs.put(keys=dgkey, val=esr)
self.db.dtss.put(keys=dgkey, val=Dater())
self.db.sigs.put(keys=(serder.preb, serder.saidb), vals=sigers)
self.db.evts.put(keys=(serder.preb, serder.saidb), val=serder)
if wigers:
self.db.wigs.put(keys=dgkey, vals=wigers)
if delsner and delsger:
self.db.udes.put(keys=dgkey, val=(delsner, delsger))
self.db.misfits.add(keys=(serder.pre, serder.snh), val=serder.saidb)
# log escrowed
logger.debug("Kever: escrowed misfit event=\n%s\n", serder.pretty())
[docs]
def escrowDelegableEvent(self, serder, sigers, wigers=None, local=True):
"""
Update associated logs for escrow of Delegable event that needs delegation
via an anchored seal in delegator's KEl
Parameters:
serder (SerderKERI): instance of event
sigers (list): of Siger instance for event
wigers (list): of witness signatures
local (bool): event source for validation logic
True means event source is local (protected).
False means event source is remote (unprotected).
Event validation logic is a function of local or remote
"""
local = True if local else False
dgkey = dgKey(serder.preb, serder.saidb)
if esr := self.db.esrs.get(keys=dgkey): # preexisting esr
if local and not esr.local: # local overwrites prexisting remote
esr.local = local
self.db.esrs.pin(keys=dgkey, val=esr)
# otherwise don't change
else: # not preexisting so put
esr = EventSourceRecord(local=local)
self.db.esrs.put(keys=dgkey, val=esr)
self.db.dtss.put(keys=dgkey, val=Dater())
self.db.sigs.put(keys=dgkey, vals=sigers)
self.db.evts.put(keys=(serder.preb, serder.saidb), val=serder)
if wigers:
self.db.wigs.put(keys=(serder.preb, serder.saidb), vals=wigers)
self.db.delegables.add(snKey(serder.preb, serder.sn), serder.saidb)
# log escrowed
logger.debug("Kever: escrowed delegable event =\n%s\n", serder.pretty())
[docs]
def escrowPSEvent(self, serder, *, sigers=None, wigers=None,
delsner=None, delsger=None, local=True):
"""
Update associated logs for escrow of partially signed event
or fully signed delegated event but not yet verified delegation.
Parameters:
serder is SerderKERI instance of event
sigers is list of Siger instances of indexed controller sigs
wigers is optional list of Siger instance of indexed witness sigs
delsner (Number): instance of sn of delegating event if any
delsger (Diger): instance of said digest of delegating event if any
local (bool): event source for validation logic
True means event source is local (protected).
False means event source is remote (unprotected).
Event validation logic is a function of local or remote
"""
local = True if local else False
dgkey = dgKey(serder.preb, serder.saidb)
self.db.dtss.put(keys=dgkey, val=Dater()) # idempotent
if sigers:
self.db.sigs.put(keys=dgkey, vals=sigers)
if wigers:
self.db.wigs.put(keys=dgkey, vals=wigers)
if delsner and delsger:
self.db.udes.put(keys=dgkey, val=(delsner, delsger)) # idempotent
self.db.evts.put(keys=(serder.preb, serder.saidb), val=serder)
# update event source
if esr := self.db.esrs.get(keys=dgkey): # preexisting esr
if local and not esr.local: # local overwrites prexisting remote
esr.local = local
self.db.esrs.pin(keys=dgkey, val=esr)
# otherwise don't change
else: # not preexisting so put
esr = EventSourceRecord(local=local)
self.db.esrs.put(keys=dgkey, val=esr)
snkey = snKey(serder.preb, serder.sn)
self.db.pses.add(keys=serder.preb, on=serder.sn, val=serder.saidb)
logger.debug("Kever: Escrowed partially signed or delegated event = \n%s\n", serder.pretty())
[docs]
def escrowPWEvent(self, serder, *, sigers=None, wigers=None,
delsner=None, delsger=None, local=True):
"""
Update associated logs for escrow of partially witnessed event
Parameters:
serder is SerderKERI instance of event
sigers is optional list of Siger instances of indexed controller sigs
wigers is list of Siger instance of indexed witness sigs
delsner (Number): instance of sn of delegating event if any
delsger (Diger): instance of said digest of delegating event if any
local (bool): event source for validation logic
True means event source is local (protected).
False means event source is remote (unprotected).
Event validation logic is a function of local or remote
"""
local = True if local else False
dgkey = dgKey(serder.preb, serder.saidb)
self.db.dtss.put(keys=dgkey, val=Dater()) # idempotent
if sigers:
self.db.sigs.put(keys=dgkey, vals=sigers)
if wigers:
self.db.wigs.put(keys=dgkey, vals=wigers)
if delsner and delsger:
self.db.udes.put(keys=dgkey, val=(delsner, delsger)) # idempotent
self.db.evts.put(keys=(serder.preb, serder.saidb), val=serder)
# update event source
if (esr := self.db.esrs.get(keys=dgkey)): # preexisting esr
if local and not esr.local: # local overwrites prexisting remote
esr.local = local
self.db.esrs.pin(keys=dgkey, val=esr)
# otherwise don't change
else: # not preexisting so put
esr = EventSourceRecord(local=local)
self.db.esrs.put(keys=dgkey, val=esr)
logger.trace("Kever state: Escrowed partially witnessed event = %s", serder.said)
logger.trace("Event Body=\n%s\n", serder.pretty())
return self.db.pwes.add(keys=serder.preb, on=serder.sn, val=serder.saidb)
[docs]
def escrowPDEvent(self, serder, *, sigers=None, wigers=None,
delsner=None, delsger=None, local=True):
"""Update associated logs for escrow of partially delegated event.
Assumes sigs (controller signatures) and wigs (witness signatures) are
provided elsewhere. Partial authentication occurs once an event is
fully signed and witnessed but the authorizing (delegating) source
seal in the authorizer's (delegator's) key has not yet been verified.
The escrow of the authorizing event ref via source seal is not idempotent
so that malicious or erroneous source seals may be nullified and/or
replaced by found source seals.
Escrow allows escrow processor to retrieve serder from key and source
couple from val in order to to re-verify authentication status.
Parameters:
serder is SerderKERI instance of event
sigers is optional list of Siger instances of indexed controller sigs
wigers is list of Siger instance of indexed witness sigs
delsner is Number instance of sn of seal source delegating event sn
delsger is Diger instance of digest of delegating event said
local (bool): event source for validation logic
True means event source is local (protected).
False means event source is remote (unprotected).
Event validation logic is a function of local or remote
"""
local = True if local else False
dgkey = dgKey(serder.preb, serder.saidb)
self.db.dtss.put(keys=dgkey, val=Dater()) # idempotent
if sigers: # idempotent
self.db.sigs.put(keys=dgkey, vals=sigers)
if wigers: # idempotent
self.db.wigs.put(keys=dgkey, vals=wigers)
if delsner and delsger: # non-idempotent pin to repair replace
self.db.udes.pin(keys=dgkey, val=(delsner, delsger)) # non-idempotent
logger.debug(f"Kever state: Replaced escrow source couple sn="
f"{delsner.num}, said={delsger.qb64} for partially "
f"delegated/authorized event said={serder.said}.")
else:
self.db.udes.rem(keys=dgkey) # nullify non-idempotent
logger.debug(f"Kever state: Nullified escrow source couple for "
f"partially delegated/authorized event said="
f"{serder.said}.")
self.db.evts.put(keys=(serder.preb, serder.saidb), val=serder) # idempotent
# update event source local or remote
if (esr := self.db.esrs.get(keys=dgkey)): # preexisting esr
if local and not esr.local: # local overwrites prexisting remote
esr.local = local
self.db.esrs.pin(keys=dgkey, val=esr)
# otherwise don't change
else: # not preexisting so put
esr = EventSourceRecord(local=local)
self.db.esrs.put(keys=dgkey, val=esr)
logger.debug(f"Kever: Escrowed partially delegated event=\n%s\n", serder.pretty())
return self.db.pdes.add(keys=serder.pre, on=serder.sn, val=serder.said)
[docs]
def state(self):
"""
Returns KeyStateRecord instance of current key state
"""
eevt = StateEstEvent(s="{:x}".format(self.lastEst.s),
d=self.lastEst.d,
br=self.cuts,
ba=self.adds)
cnfg = []
if self.estOnly:
cnfg.append(TraitDex.EstOnly)
if self.doNotDelegate:
cnfg.append(TraitDex.DoNotDelegate)
return (state(pre=self.prefixer.qb64,
sn=self.sn, # property self.sner.num
pig=(self.serder.prior if self.serder.prior is not None else ""),
dig=self.serder.said,
fn=self.fn, # property self.fner.num
stamp=self.dater.dts, # need to add dater object for first seen dts
eilk=self.ilk,
keys=[verfer.qb64 for verfer in self.verfers],
eevt=eevt,
sith=self.tholder.sith,
nsith=self.ntholder.sith if self.ntholder else '0',
ndigs=[diger.qb64 for diger in self.ndigers],
toad=self.toader.num,
wits=self.wits,
cnfg=cnfg,
dpre=self.delpre,
version=self.serder.pvrsn,
)
)
[docs]
def fetchPriorDigers(self, sn: int | None = None) -> list | None:
""" Returns either the most recent prior list of digers before .lastEst or None
Starts searching at sn or if sn is None at sn = .lastEst.s - 1
Returns list of Digers instances at the most recent prior est event relative
to the given sequence number (sn) otherwise returns None.
Walks backwards to the more recent prior establishment event before the
.sn if any.
If sn represents an interaction event (ixn) then the result will be the
current valid list of digers. If sn represents an establishment event then
the result will be the list of digers immediately prior to the current list.
Parameters:
sn (int | None): sn to start searching. If None then start at .lastEst.s - 1
Returns:
digers (list | None): of Diger instances or None if no prior est evt
to current .lastEst
"""
pre = self.prefixer.qb64
if sn is None:
sn = self.lastEst.s - 1
if sn < 0:
return None
for digb in self.db.kels.getBackIter(keys=pre, on=sn):
if (serder := self.db.evts.get(keys=(pre, digb))) is None:
continue
if serder.estive: # establishment event
return serder.ndigers
return None
[docs]
def fetchLatestContribTo(self, verfers, sn: int | None = None):
""" Returns tuple of (sn, index, verfer) from latest est event whose
verfer is found in verfers at index offset else None if not found.
Fetches latest event sn and associated index and verfer that contributed
to the provided verfers at index.
Starts searching at sn or if sn is None at sn = .lastEst.s
Returns tuple (sn, index, verfer) from the latest est event that matches by
starting at the given sequence number (sn) and walking backwards
otherwise returns None.
If given sn represents an interaction event (ixn) then a latest possible
matching result may be from an event that is no later than the last est
event prior to that interaction event.
If the sn represents an establishment event then the latest possible
matching result may be from that event.
Parameters:
verfers (list[Verfer]): of verfer instances
sn (int | None): sn to start searching. If None then start at .lastEst.s
Returns:
tuple(int, int,Verfer) | None: where tuple is of form (sn, idx, verfer).
sn is sequence number.
idx is index of verfer in verfers
verfer is instance of Verfer
"""
pre = self.prefixer.qb64
if sn is None:
sn = self.lastEst.s
keys = [verfer.qb64 for verfer in verfers]
for digb in self.db.kels.getBackIter(keys=pre, on=sn):
if (serder := self.db.evts.get(keys=(pre, digb))) is None:
continue
if serder.estive: # establishment event
key = serder.verfers[0].qb64
try:
i = keys.index(key) # find index of key in keys
except ValueError: # not found
continue
return (serder.sn, i, serder.verfers[0])
return None
[docs]
def fetchLatestContribFrom(self, verfer, sn: int | None = None):
""" Returns tuple of form (sn, index, verfers) where verfers is a list of
verfers from latest est event where verfer is found in that event's
verfers at index offset else None if not found.
Fetches latest event sn and associated verfers list that recieved a
contribution from the provided verfer at index.
Starts searching at sn or if sn is None at sn = .lastEst.s
Returns tuple (sn, index, list[verfers]) from the latest est event that
matches by starting at the given sequence number (sn) and walking backwards
otherwise returns None.
If given sn represents an interaction event (ixn) then a latest possible
matching result may be from an event that is no later than the last est
event prior to that interaction event.
If the sn represents an establishment event then the latest possible
matching result may be from that event.
Parameters:
verfer (Verfer): instance of verfer
sn (int | None): sn to start searching. If None then start at .lastEst.s
Returns:
tuple(int, int, list[Verfer]) | None: where tuple is of form
(sn, index, verfers)
sn is sequence number
index is index into verfers of verfers
verfers is list of Verfer instances.
"""
pre = self.prefixer.qb64
if sn is None:
sn = self.lastEst.s
key = verfer.qb64
for digb in self.db.kels.getBackIter(keys=pre, on=sn):
if (serder := self.db.evts.get(keys=(pre, digb))) is None:
continue
if serder.estive: # establishment event
keys = [verfer.qb64 for verfer in serder.verfers]
try:
i = keys.index(key) # find index of key in keys
except ValueError: # not found
continue
return (serder.sn, i, serder.verfers)
return None
[docs]
class Kevery:
"""
Kevery (Key Event Message Processing Facility) processes an incoming
message stream composed of KERI key event related messages and attachments.
Kevery acts a Kever (key event verifier) factory for managing key state of
KERI identifier prefixes.
Only supports current version VERSION
Has the following public attributes and properties:
Attributes:
cues (Deck): of Cues i.e. notices of events needing receipt or
requests needing response
db (Baser): instance of LMDB Baser object
lax (bool): True means operate in promiscuous (unrestricted) mode,
False means operate in nonpromiscuous (restricted) mode
as determined by local and prefixes
local (bool): True means only process msgs for own events if not lax
False means only process msgs for not own events if not lax
cloned (bool): True means cloned message stream so use attached
datetimes from clone source not own.
False means use current datetime
direct (bool): True means direct mode so cue notices for receipts etc
False means indirect mode so don't cue notices
check (bool): True means do not update the database in any
non-idempotent way. Useful for reinitializing the Kevers from
a persisted KEL without updating non-idempotent first seen .fels
and timestamps.
Properties:
.kevers is dict of db kevers indexed by pre (qb64) of each Kever
.prefixes is OrderedSet of fully qualified base64 identifier prefixes of db
local habitats if any.
"""
TimeoutOOE = 1200 # seconds to timeout out of order escrows
TimeoutPSE = 3600 # seconds to timeout partially signed or delegated escrows
TimeoutPWE = 3600 # seconds to timeout partially witnessed escrows
TimeoutLDE = 3600 # seconds to timeout likely duplicitous escrows
TimeoutUWE = 3600 # seconds to timeout unverified receipt escrows
TimeoutURE = 3600 # seconds to timeout unverified receipt escrows
TimeoutVRE = 3600 # seconds to timeout unverified transferable receipt escrows
TimeoutKSN = 3600 # seconds to timeout key state notice message escrows
TimeoutQNF = 300 # seconds to timeout query not found escrows
[docs]
def __init__(self, *, cues=None, db=None, rvy=None, exc=None, tvy=None,
kramer=None, lax=True, local=False, cloned=False, direct=True,
check=False):
"""
Initialize instance:
Parameters:
cues (Deck) notices to create responses to evts
kevers is dict of Kever instances of key state in db
db (Baser): instance of database
rvy (Revery): instance for reply message processing
exc (Exchanger): instance for exchange message processing
tvy (Tevery): instance for TEL query route processing
kramer (Kramer): instance for KRAM processing
lax (bool): True means operate in promiscuous (unrestricted) mode,
False means operate in nonpromiscuous (restricted) mode
as determined by local and prefixes
local (bool): True means event source is local (protected) for validation
False means event source is remote (unprotected) for validation
cloned (bool): True means cloned message stream so use attached
datetimes from clone source not own.
False means use current datetime
direct (bool): True means direct mode so cue notices for receipts etc
False means indirect mode so don't cue notices
check (bool): True means do not update the database in any
non-idempotent way. Useful for reinitializing the Kevers from
a persisted KEL without updating non-idempotent first seen .fels
and timestamps.
"""
self.cues = cues if cues is not None else decking.Deck() # subclass of deque
if db is None:
db = Baser(reopen=True) # default name = "main"
self.db = db
self.rvy = rvy
self.exc = exc # Exchanger instance for exn messages
self.tvy = tvy # Tevery instance for TEL query routes
self.kramer = kramer # Kramer instance for KRAM processing
if self.kramer is not None:
self.kramer.cues = self.cues
self.allowList = oset()
self.denyList = oset()
self.lax = True if lax else False # promiscuous mode
self.local = True if local else False # local vs nonlocal default
self.cloned = True if cloned else False # process as cloned
self.direct = True if direct else False # process as direct mode
self.check = True if check else False # process as check mode
@property
def kevers(self):
"""
Returns .db.kevers
"""
return self.db.kevers
@property
def prefixes(self):
"""
Returns .db.prefixes
"""
return self.db.prefixes
[docs]
def fetchWitnessState(self, pre, sn):
""" Returns the list of witness for the identifier prefix at the sequence number
Returns the witness state (list of witnesses) at the given sequence number (sn) of the
identifier prefix (pre). It uses the .wits database that stores witness state at the
sequence number of each establishment event. If sn represents an interaction event (ixn) it
searches backwards for the last establishment event prior to sn and returns that witness state.
Args:
pre (str): identifier prefix qb64
sn (int): sequence number of the event for which witness state is desired
Returns:
list: list of Prefixer objects representing the witness state for the identifier prefix at
the sequence number
"""
preb = pre.encode("utf-8")
for digb in self.db.kels.getBackIter(keys=preb, on=sn):
serder = self.db.evts.get(keys=(preb, digb))
if serder.estive:
wits = self.db.wits.get(keys=(preb, digb))
return wits
return []
[docs]
def processEvent(self, serder, sigers, *, wigers=None, delsner=None, delsger=None,
firner=None, dater=None, eager=False, local=None, **kwa):
"""
Process one event serder with attached indexd signatures sigers
Parameters:
serder (SerderKERI): instance of event to process
sigers (list[Siger]|None): instances of attached controller indexed sigs
wigers (list[Siger]|None): instances of attached witness indexed sigs
otherwise None
delsner (Number|None): instance of delegating event sequence number.
If this event is not delegated then delnumber is ignored
delsger (Diger|None): instance of of delegating event SAID diger.
If this event is not delegated then deldiger is ignored
firner (Seqner|None): instance of cloned first seen ordinal
If cloned mode then firner maybe provided (not None)
When firner provided then compare fn of dater and database and
first seen if not match then log and add cue notify problem
dater (Dater|None): instance of cloned replay datetime
If cloned mode then dater maybe provided (not None)
When dater provided then use dater for first seen datetime
eager (bool): True means try harder to find validate events by
walking KELs. Enables only being eager
in escrow processing not initial parsing.
False means only use pre-existing information
if any, either percolated attached or in database.
local (bool|None): True means local (protected) event source.
False means remote (unprotected).
None means use default .local .
"""
local = local if local is not None else self.local
local = True if local else False # force boolean
# fetch ked ilk pre, sn, dig to see how to process
pre = serder.pre
ked = serder.ked
# See todo for Prefixer fix redundancy XXX
try: # see if code of pre is supported and matches size of pre
Prefixer(qb64=pre)
except Exception as ex: # if unsupported code or bad size raises error
raise ValidationError("Invalid pre = {} for evt = {}."
"".format(pre, ked)) from ex
sn = serder.sn
ilk = serder.ilk # ked["t"]
said = serder.said
sigers = sigers if sigers is not None else []
if pre not in self.kevers: # first seen event for pre
if ilk in (Ilks.icp, Ilks.dip): # first seen and inception so verify event keys
# kever init verifies basic inception stuff and signatures
# raises exception if problem
# otherwise adds to KEL
# create kever from serder
kever = Kever(serder=serder,
sigers=sigers,
wigers=wigers,
db=self.db,
delsner=delsner,
delsger=delsger,
firner=firner if self.cloned else None,
dater=dater if self.cloned else None,
cues=self.cues,
eager=eager,
local=local,
check=self.check)
self.kevers[pre] = kever # not exception so add to kevers
# At this point the inceptive event (icp or dip) given by serder
# together with its attachments has been accepted as valid with finality.
# Events that don't get to here have either been dropped as
# invalid by raising an error or have been escrowed as not
# yet complete enough to decide their validity, i.e. are
# missing signatures, or witness receipts or delegations.
# Any actions that should be triggered as a result of final
# acceptance should follow from here.
if self.direct or self.lax or pre not in self.prefixes: # not own event when owned
# create cue for receipt controller or watcher
# receipt of actual type is dependent on own type of identifier
self.cues.push(dict(kin="receipt", serder=serder))
elif not self.direct: # notice of new event
self.cues.push(dict(kin="notice", serder=serder))
if self.local and kever.locallyWitnessed(): #
# ToDo XXXX need to cue task here kin = "witness" and process
# cued witness and then combine with reciept above so only
# one receipt is generated not two
self.cues.push(dict(kin="witness", serder=serder))
else: # not inception so can't verify sigs etc, add to out-of-order escrow
self.escrowOOEvent(serder=serder, sigers=sigers,
delsner=delsner, delsger=delsger, wigers=wigers, local=local)
raise OutOfOrderError("Out-of-order event={}.".format(ked))
else: # already accepted inception event for pre so already first seen
if ilk in (Ilks.icp, Ilks.dip): # another inception event so maybe duplicitous
if sn != 0:
raise ValueError("Invalid sn={} for inception event={}."
"".format(sn, serder.ked))
# check if duplicate of existing inception event since est is icp
eserder = self.fetchEstEvent(pre, sn) # latest est evt wrt sn
if eserder.said == said: # event is a duplicate but not duplicitous
# may have attached valid signature not yet logged
# raises ValidationError if no valid sig
kever = self.kevers[pre] # get key state
# get unique verified lists of sigers and indices from sigers
sigers, indices = verifySigs(raw=serder.raw,
sigers=sigers,
verfers=eserder.verfers)
wigers, windices = verifySigs(raw=serder.raw,
sigers=wigers,
verfers=eserder.berfers)
if sigers or wigers: # at least one verified sig or wig so log evt
# this allows late arriving witness receipts or controller
# signatures to be added to the databse
# Not first seen version of event so ignore return
# idempotent update db logs
kever.logEvent(serder, sigers=sigers, wigers=wigers)
else: # escrow likely duplicitous event
self.escrowLDEvent(serder=serder, sigers=sigers)
msg = f"Likely Duplicitous Event sn={serder.sn} type={serder.ilk} SAID={serder.said}"
logger.debug(msg)
logger.debug("Duplicitous event body=\n%s\n", serder.pretty())
raise LikelyDuplicitousError(msg)
else: # rot, drt, or ixn, so sn matters
kever = self.kevers[pre] # get existing kever for pre
sno = kever.sner.num + 1 # proper sn of new inorder event
if sn > sno: # sn later than sno so out of order escrow
# escrow out-of-order event
self.escrowOOEvent(serder=serder, sigers=sigers,
delsner=delsner, delsger=delsger, wigers=wigers, local=local)
msg = f"Out-of-order event sn={serder.sn} type={serder.ilk} SAID={serder.said}"
logger.debug(msg)
logger.debug("Out-of-order event body=\n%s\n", serder.pretty())
raise OutOfOrderError(msg)
elif ((sn == sno) or # inorder event (ixn, rot, drt) or
(ilk == Ilks.rot and # superseding recovery rot or
kever.lastEst.s < sn <= sno) or
(ilk == Ilks.drt and #exts['sigers'].extend(result) delegated superseding recovery drt
kever.lastEst.s <= sn <= sno)):
# verify signatures etc and update state if valid
# raise exception if problem.
# Otherwise adds to KELs
kever.update(serder=serder, sigers=sigers, wigers=wigers,
delsner=delsner, delsger=delsger,
firner=firner if self.cloned else None,
dater=dater if self.cloned else None,
eager=eager, local=local, check=self.check)
# At this point the non-inceptive event (rot, drt, or ixn)
# given by serder together with its attachments has been
# accepted as valid with finality.
# Events that don't get to here have either been dropped as
# invalid by raising an error or have been escrowed as not
# yet complete enough to decide their validity, i.e. are
# missing signatures, or witness receipts or delegations.
# Any actions that should be triggered as a result of final
# acceptance should follow from here.
if self.direct or self.lax or pre not in self.prefixes: # not own event when owned
# create cue for receipt controller or watcher
# receipt of actual type is dependent on own type of identifier
self.cues.push(dict(kin="receipt", serder=serder))
elif not self.direct: # notice of new event
self.cues.push(dict(kin="notice", serder=serder))
if self.local and kever.locallyWitnessed(): #
# ToDo XXXX need to cue task here kin = "witness" and process
# cued witness and then combine with reciept above so only
# one receipt is generated not two
self.cues.push(dict(kin="witness", serder=serder))
else: # maybe duplicitous
# check if duplicate of existing valid accepted event
ddig = self.db.kels.getLast(keys=pre, on=sn)
if ddig == said: # event is a duplicate but not duplicitous
eserder = self.fetchEstEvent(pre, sn) # latest est event wrt sn
# may have attached valid signature not yet logged
# raises ValidationError if no valid sig
kever = self.kevers[pre]
# get unique verified lists of sigers and indices from sigers
sigers, indices = verifySigs(raw=serder.raw,
sigers=sigers,
verfers=eserder.verfers)
wits = [wit.qb64 for wit in self.fetchWitnessState(pre, sn)]
werfers = [Verfer(qb64=wit) for wit in wits]
wigers, windices = verifySigs(raw=serder.raw,
sigers=wigers,
verfers=werfers)
if sigers or wigers: # at least one verified sig or wig so log evt
# this allows late arriving witness receipts or controller
# signatures to be added to the databse
# Not first seen version of event so ignore return
# idempotent update db logs
kever.logEvent(serder, sigers=sigers, wigers=wigers) # idempotent update db logs
else: # escrow likely duplicitous event
self.escrowLDEvent(serder=serder, sigers=sigers)
msg = f"Likely Duplicitous Event sn={serder.sn} type={serder.ilk} SAID={serder.said}"
logger.debug(msg)
logger.debug("Duplicitous event body=\n%s\n", serder.pretty())
raise LikelyDuplicitousError(msg)
[docs]
def processReceipt(self, serder, *, cigars=None, wigers=None, tsgs=None, local=None, **kwa):
"""
Process one receipt serder with attached cigars
may or may not be a witness receipt. If prefix matches witness then
promote to indexed witness signature and store appropriately. Otherwise
signature is nontrans nonwitness endorser (watcher etc)
Parameters:
serder (SerderKERI): rct instance of serialized receipt message
cigars (list[Cigar]): instances that contain receipt couple
signature in .raw and public key in .verfer
wigers (list[Siger]): instances that with witness indexed signatures
signature in .raw. Index is offset into witness list of latest
establishment event for receipted event. Signature uses key pair
derived from nontrans witness prefix in associated witness list.
tsgs (list[tuple]): from extracted transferable indexed sig groups
each converted group is tuple of (i,s,d) triple plus list of sigs
local (bool|None): True means local (protected) event source.
False means remote (unprotected).
None means use default .local .
Receipt dict labels
vs # version string
pre # qb64 prefix
sn # hex string sequence number
ilk # rct
dig # qb64 digest of receipted event
"""
local = local if local is not None else self.local
local = True if local else False # force boolean
# fetch pre dig to process
ked = serder.ked
pre = serder.pre
sn = serder.sn
# Only accept receipt if for last seen version of event at sn
ldig = self.db.kels.getLast(keys=pre, on=sn) # retrieve dig of last event at sn.
if ldig is not None: # verify digs match
# retrieve event by dig assumes if ldig is not None that event exists at ldig
dgkey = dgKey(serder.preb, serder.saidb)
lserder = self.db.evts.get(keys=(serder.preb, serder.saidb)) # retrieve receipted event at dig
# assumes db ensures that lserder must not be none
if not lserder.compare(said=ked["d"]): # stale receipt at sn discard
raise ValidationError("Stale receipt at sn = {} for rct = {}."
"".format(ked["s"], ked))
# process each couple verify sig and write to db
for cigar in cigars:
if cigar.verfer.transferable: # skip transferable verfers
continue # skip invalid couplets
if not self.lax and cigar.verfer.qb64 in self.prefixes: # own is receiptor
if pre in self.prefixes: # skip own receipter of own event
# sign own events not receipt them
logger.debug("Kevery process: skipped own receipt attachment"
" on own event receipt=%s", serder.said)
logger.debug("Event=\n%s\n", serder.pretty())
continue # skip own receipt attachment on own event
if not local: # skip own receipt on other event when not local
logger.debug("Kevery process: skipped own receipt attachment"
" on nonlocal event receipt=%s", serder.said)
logger.debug("Event=\n%s\n", serder.pretty())
continue # skip own receipt attachment on non-local event
if cigar.verfer.verify(cigar.raw, lserder.raw):
wits = [wit.qb64 for wit in self.fetchWitnessState(pre, sn)]
rpre = cigar.verfer.qb64 # prefix of receiptor
if rpre in wits: # its a witness receipt write in .wigs
index = wits.index(rpre)
# create witness indexed signature
wiger = Siger(raw=cigar.raw, index=index, verfer=cigar.verfer)
self.db.wigs.add(keys=dgkey, val=wiger) # write to db
else: # not witness rect write receipt to database
self.db.rcts.add(keys=dgkey, val=(cigar.verfer, cigar))
wits = [wit.qb64 for wit in self.fetchWitnessState(pre, sn)]
for wiger in wigers:
# assign verfers from witness list
if wiger.index >= len(wits):
continue # skip invalid witness index
wiger.verfer = Verfer(qb64=wits[wiger.index]) # assign verfer
if wiger.verfer.transferable: # skip transferable verfers
continue # skip invalid witness prefix
if not self.lax and wiger.verfer.qb64 in self.prefixes: # own is witness
if pre in self.prefixes: # skip own receiptor of own event
# sign own events not receipt them
logger.info("Kevery: skipped own receipt attachment"
" on own event receipt=%s", serder.said)
logger.debug("Event=\n%s\n", serder.pretty())
continue # skip own receipt attachment on own event
if not local: # so skip own receipt on other event when non-local source
logger.debug("Kevery: skipped own receipt attachment"
" on nonlocal event receipt=%s", serder.said)
logger.debug("Event=\n%s\n", serder.pretty())
continue # skip own receipt attachment on non-local event
if wiger.verfer.verify(wiger.raw, lserder.raw):
# write receipt indexed sig to database
self.db.wigs.add(keys=dgkey, val=wiger)
for sprefixer, snumber, sdiger, sigers in tsgs: # iterate over each tsg
if not self.lax and sprefixer.qb64 in self.prefixes: # own is receipter
if pre in self.prefixes: # skip own receipter of own event
# sign own events as controller not endorse them via receipt
raise ValidationError("Own pre={} receipter of own event"
" {}.".format(self.prefixes, serder.pretty()))
if not local: # skip own receipts of nonlocal events
raise ValidationError("Own pre={} receipter of nonlocal event "
"{}.".format(self.prefixes, serder.pretty()))
# receipted event in db so attempt to get receipter est evt
# retrieve dig of last event at sn of est evt of receiptor.
sdig = self.db.kels.getLast(keys=sprefixer.qb64b, on=snumber.sn)
if sdig is None:
# receiptor's est event not yet in receiptors's KEL
# so need cue to discover est evt KEL for receipter from watcher etc
self.escrowTReceipts(serder, sprefixer, snumber, sdiger, sigers)
raise UnverifiedTransferableReceiptError("Unverified receipt: "
"missing establishment event of transferable "
"receipter for event={}."
"".format(ked))
sdig = sdig.encode("utf-8")
# retrieve last event itself of receiptor est evt from sdig.
sserder = self.db.evts.get(keys=(sprefixer.qb64b, bytes(sdig)))
# assumes db ensures that sserder must not be none because sdig was in KE
if not sserder.compare(said=sdiger.qb64): # endorser's dig not match event
raise ValidationError("Bad trans indexed sig group at sn = {}"
" for ksn = {}."
"".format(snumber.sn, sserder.ked))
# verify sigs and if so write receipt to database
sverfers = sserder.verfers
if not sverfers:
raise ValidationError("Invalid receipter's est. event"
" dig = {} from pre ={}, no keys."
"".format(sdiger.qb64, sprefixer.qb64))
for siger in sigers: # endorser (non-controller) signatures
if siger.index >= len(sverfers):
raise ValidationError("Index = {} to large for keys."
"".format(siger.index))
siger.verfer = sverfers[siger.index] # assign verfer
if siger.verfer.verify(siger.raw, lserder.raw): # verify sig
# good sig so write receipt quadruple to database
quadruple = (sprefixer, snumber, sdiger, siger)
self.db.vrcs.add(keys=dgKey(pre=pre, dig=ldig),
val=quadruple) # dups kept
else: # no events to be receipted yet at that sn so escrow
if cigars:
self.escrowUReceipt(serder, cigars, said=ked["d"]) # digest in receipt
if tsgs:
self.escrowTRGroups(serder, tsgs)
if wigers:
self.escrowUWReceipt(serder=serder, wigers=wigers, said=ked["d"])
msg = f"Unverified receipt = {serder.said}"
logger.info(msg)
logger.debug("event=\n%s\n", serder.pretty())
raise UnverifiedReceiptError(msg)
[docs]
def processMsg(self, serder, kwa=None):
"""Process one non-key-event KERI message with attachments.
Consolidated entry point for non-event message types:
qry, rpy, pro, bar, xip, exn.
Processing order:
1. AID-based allow/deny logic
2. KRAM processing via self.kramer.intake()
3. Message-type-specific processing delegation
Parameters:
serder (SerderKERI): message instance
kwa (dict | None): parser exts / attachment dict (sigers, cigars, tsgs,
ssgs, sscs, ssts, tdcs, wigers, trqs, frcs, ptds, essrs,
bsqs, bsss, tmqs, local, etc.); mutated in place (KRAM
normalization, rvy/exc/tvy pops, qry source/sigers).
Also accepts processor overrides injected by parser:
rvy (Revery), exc (Exchanger), tvy (Tevery)
"""
if kwa is None:
kwa = {}
ilk = serder.ilk
# Extract processor overrides injected by parser, fall back to self
rvy = kwa.pop('rvy', None) or self.rvy
exc = kwa.pop('exc', None) or self.exc
tvy = kwa.pop('tvy', None) or self.tvy
# Normalize attachment shape: KRAM and qry dispatch always see a list.
kwa.setdefault('sigers', [])
# Step 1: AID-based allow/deny Draft
# Determine sender AID using the serder
sender = serder.pre
# Still needs fleshing out for delegate messages and multisig
# Apply allow/deny rules
if sender is not None:
# Denylist always wins
if sender in self.denyList:
logger.info(f"Message dropped from {sender} (in denylist)")
return # drop silently
# Allowlist only restricts when non-empty
if self.allowList and sender not in self.allowList:
logger.info(
f"Message dropped from {sender} "
f"(not in allowlist; allowlist active)"
)
return # drop silently
# Step 2: KRAM
if self.kramer:
self.kramer.reconcileConfig()
result = self.kramer.intake(serder, kwa)
if result is None:
return # message dropped or pending in KRAM
# Step 3: Dispatch to message-specific processing
match ilk:
case Ilks.qry:
# Extract source and sigers from ssgs (like parser originally did)
if kwa.get('ssgs'):
pre, sigers = kwa['ssgs'][-1]
kwa['source'] = pre
kwa['sigers'] = sigers
elif kwa['sigers'] and not kwa.get('source'):
kwa['source'] = Prefixer(qb64=serder.pre)
if not (kwa.get('source') or kwa.get('cigars', [])):
raise ValidationError(
f"Missing attached requester source for query"
f" msg = {serder.pretty()}.")
route = serder.ked["r"]
if route in ["logs", "ksn", "mbx"]:
self.processQuery(serder, **kwa)
elif route in ["tels", "tsn"]:
if tvy is None:
raise ValidationError(
f"No tevery to process so dropped msg"
f"={serder.pretty()}")
tvy.processQuery(serder, **kwa)
else:
raise ValidationError(
f"Invalid resource type {route}"
f"so dropped msg={serder.pretty()}.")
case Ilks.rpy:
cigars = kwa.get('cigars', [])
tsgs = kwa.get('tsgs', [])
if not (cigars or tsgs):
raise ValidationError(
f"Missing attached endorser signature(s) "
f"to reply msg = {serder.pretty()}.")
if rvy is None:
raise ValidationError(
f"No revery to process so dropped msg"
f"= {serder.pretty()}.")
rvy.processReply(serder=serder, **kwa)
case Ilks.exn:
cigars = kwa.get('cigars', [])
tsgs = kwa.get('tsgs', [])
if not (cigars or tsgs):
raise ValidationError(
f"Missing attached exchanger "
f"signatures for msg={serder.pretty()}")
if exc is None:
raise ValidationError(
f"No exchanger to process so "
f"dropped msg={serder.pretty()}.")
exc.processEvent(serder=serder, **kwa)
case Ilks.xip:
self.processXip(serder, kwa)
case Ilks.pro:
self.processPro(serder, kwa)
case Ilks.bar:
self.processBar(serder, kwa)
case _:
raise ValidationError(
f"Unexpected non-event message type {ilk} "
f"for msg={serder.pretty()}")
[docs]
def processXip(self, serder, kwa):
"""Stub: KERI v2 exchange transaction; no processing yet."""
pass
[docs]
def processPro(self, serder, kwa):
"""Stub: exchange proposal message; no processing yet."""
pass
[docs]
def processBar(self, serder, kwa):
"""Stub: exchange barrier message; no processing yet."""
pass
[docs]
def processAttachedReceiptCouples(self, serder, cigars, *, firner=None,
local=None, **kwa):
"""
Process one attachment couple that represents an endorsement from
a nontransferable AID that may or may not be a witness, maybe a watcher.
Originally may have been a non-transferable receipt or key event attachment
if signature is for witness then promote to indexed sig and store
appropriately.
The is the attachment version of .processReceipt
Parameters:
serder (SerderKERI): instance serialized event message to which
attachments come from replay (clone)
cigars (list[Cigar]): instances that contain receipt couple
signature in .raw and public key in .verfer
firner (Seqner): instance of first seen ordinal,
if provided lookup event by fn = firner.sn
used when in cloned replay mode
local (bool|None): True means local (protected) event source.
False means remote (unprotected).
None means use default .local .
"""
local = local if local is not None else self.local
local = True if local else False # force boolean
# fetch pre dig to process
ked = serder.ked
pre = serder.pre
sn = serder.sn
# Only accept receipt if event is latest event at sn. Means its been
# first seen and is the most recent first seen with that sn
if firner:
ldig = self.db.fels.get(keys=pre, on=firner.sn)
else:
ldig = self.db.kels.getLast(keys=pre, on=sn) # retrieve dig of last event at sn.
if ldig is None: # escrow because event does not yet exist in database
# # take advantage of fact that receipt and event have same pre, sn fields
self.escrowUReceipt(serder, cigars, said=serder.said) # digest in receipt
raise UnverifiedReceiptError("Unverified receipt={}.".format(ked))
if isinstance(ldig, bytes):
ldig = ldig.decode("utf-8") # normalize to str for compare
# retrieve event by dig assumes if ldig is not None that event exists at ldig
if not serder.compare(said=ldig): # mismatch events problem with replay
raise ValidationError("Mismatch replay event at sn = {} with db."
"".format(ked["s"]))
# process each couple to verify sig and write to db
for cigar in cigars:
if cigar.verfer.transferable: # skip transferable verfers
continue # skip invalid couplets
if not self.lax and cigar.verfer.qb64 in self.prefixes: # own is receiptor
if pre in self.prefixes: # skip own receipter on own event
# sign own events not receipt them
logger.debug("Kevery process: skipped own receipt attachment"
" on own event receipt=%s", serder.said)
logger.debug("Event=\n%s\n", serder.pretty())
continue # skip own receipt attachment on own event
if not local: # own receipt on other event when not local
logger.debug("Kevery process: skipped own receipt attachment"
" on nonlocal event receipt=%s", serder.said)
logger.debug("Event=\n%s\n", serder.pretty())
continue # skip own receipt attachment on non-local event
if cigar.verfer.verify(cigar.raw, serder.raw):
wits = self.fetchWitnessState(pre, sn)
rpre = cigar.verfer.qb64 # prefix of receiptor
if rpre in wits: # its a witness receipt
index = wits.index(rpre)
# create witness indexed signature and write to db
wiger = Siger(raw=cigar.raw, index=index, verfer=cigar.verfer)
self.db.wigs.add(keys=(pre, ldig), val=wiger)
else: # write receipt to database
self.db.rcts.add(keys=(pre, ldig), val=(cigar.verfer, cigar))
[docs]
def processAttachedReceiptQuadruples(self, serder, trqs, *, firner=None,
local=None, **kwa):
"""Process one attachment quadruple that represents an endorsement from
a transferable AID that is not the controller. Maybe a watcher.
Originally may have been a transferable receipt or key event attachment
This is the attachement version of .processReceiptTrans
Parameters:
serder (serderKERI): instance serialized event message to which
attachments come from replay (clone)
trqs (list[tuple]): quadruples of (prefixer, number, diger, siger)
firner (Seqner): instance of first seen ordinal,
if provided lookup event by fn = firner.sn
used when in cloned replay mode
local (bool|None): True means local (protected) event source.
False means remote (unprotected).
None means use default .local .
"""
local = local if local is not None else self.local
local = True if local else False # force boolean
# fetch ldig to process
ked = serder.ked
pre = serder.pre
sn = serder.sn
if firner: # retrieve last event by fn ordinal
ldig = self.db.fels.get(keys=pre, on=firner.sn)
else:
# Only accept receipt if for last seen version of receipted event at sn
ldig = self.db.kels.getLast(keys=pre, on=sn) # retrieve dig of last event at sn.
for sprefixer, snumber, diger, siger in trqs: # iterate over each trq
if not self.lax and sprefixer.qb64 in self.prefixes: # own trans receipt quadruple (chit)
if pre in self.prefixes: # skip own trans receipts of own events
raise ValidationError("Own pre={} replay attached transferable "
"receipt quadruple of own event {}."
"".format(self.prefixes, serder.pretty()))
if not local: # skip own trans receipt quadruples of nonlocal events
raise ValidationError("Own pre={} seal in replay attached "
"transferable receipt quadruples of nonlocal"
" event {}.".format(self.prefixes, serder.pretty()))
if ldig is not None and sprefixer.qb64 in self.kevers:
# both receipted event and receipter in database so retreive
if isinstance(ldig, memoryview):
ldig = bytes(ldig).decode("utf-8")
if not serder.compare(said=ldig): # mismatch events problem with replay
raise ValidationError("Mismatch replay event at sn = {} with db."
"".format(ked["s"]))
# retrieve dig of last event at sn of receipter.
sdig = self.db.kels.getLast(keys=sprefixer.qb64b, on=snumber.sn)
if sdig is None:
# receipter's est event not yet in receipter's KEL
# receipter's seal event not in receipter's KEL
self.escrowTRQuadruple(serder, sprefixer, snumber, diger, siger)
raise UnverifiedTransferableReceiptError("Unverified receipt: "
"missing establishment event of transferable "
"validator receipt quadruple for event={}."
"".format(ked))
sdig = sdig.encode("utf-8")
# retrieve last event itself of receipter
sserder = self.db.evts.get(keys=(sprefixer.qb64b, bytes(sdig)))
# assumes db ensures that sserder must not be none because sdig was in KE
if not sserder.compare(said=diger.qb64): # seal dig not match event
raise ValidationError("Bad trans receipt quadruple at sn = {}"
" for rct = {}."
"".format(snumber.sn, sserder.ked))
# verify sigs and if so write quadruple to database
sverfers = sserder.verfers
if not sverfers:
raise ValidationError("Invalid trans receipt quad est. event"
" dig = {} for receipt from pre ={}, "
"no keys."
"".format(diger.qb64, sprefixer.qb64))
if siger.index >= len(sverfers):
raise ValidationError("Index = {} to large for keys."
"".format(siger.index))
siger.verfer = sverfers[siger.index] # assign verfer
if not siger.verfer.verify(siger.raw, serder.raw): # verify sig
msg = f"Bad escrowed trans receipt sig pre={pre} sn={sn:x} receipter={sprefixer.qb64}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# good sig so write receipt quadruple to database
# Set up quadruple
quadruple = (sprefixer, snumber, diger, siger)
self.db.vrcs.add(keys=dgKey(pre, serder.said), val=quadruple)
else: # escrow either receiptor or receipted event not yet in database
self.escrowTRQuadruple(serder, sprefixer, snumber, diger, siger)
msg = (f"Unverified receipt: missing associated event for transferable validator"
f"receipt quadruple for event {serder.said}")
logger.info(msg)
logger.debug("Event=\n%s\n", serder.pretty())
raise UnverifiedTransferableReceiptError(msg)
[docs]
def removeStaleReplyEndRole(self, saider):
"""
Process reply escrow at saider for route "/end/role"
"""
pass
[docs]
def removeStaleReplyLocScheme(self, saider):
"""
Process reply escrow at saider for route "/loc/scheme"
"""
pass
[docs]
def registerReplyRoutes(self, router):
""" Register the routes for processing messages embedded in `rpy` event messages
Parameters:
router(Router): reply message router
"""
router.addRoute("/end/role/{action}", self, suffix="EndRole")
router.addRoute("/loc/scheme", self, suffix="LocScheme")
router.addRoute("/ksn/{aid}", self, suffix="KeyStateNotice")
router.addRoute("/watcher/{aid}/{action}", self, suffix="AddWatched")
[docs]
def processReplyEndRole(self, *, serder, diger, route, cigars=None, tsgs=None, **kwargs):
"""
Process one reply message for route = /end/role/add or /end/role/cut
with either attached nontrans receipt couples in cigars or attached trans
indexed sig groups in tsgs.
Assumes already validated diger, dater, and route from serder.ked
Parameters:
serder (SerderKERI): instance of reply msg (SAD)
diger (Diger): instance from said in serder (SAD)
route (str): reply route
cigars (list): of Cigar instances that contain nontrans signing couple
signature in .raw and public key in .verfer
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
EndpointRecord:
allowed: bool = False # True eid allowed (add), False eid disallowed (cut)
name: str = "" # optional user friendly name of endpoint
Reply Message:
{
"v" : "KERI10JSON00011c_",
"t" : "rpy",
"d": "EZ-i0d8JZAoTNZH3ULaU6JR2nmwyvYAfSVPzhzS6b5CM",
"dt": "2020-08-22T17:50:12.988921+00:00",
"r" : "/end/role/add",
"a" :
{
"cid": "EaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM",
"role": "watcher", # one of Roles
"eid": "BrHLayDN-mXKv62DAjFLX1_Y5yEUe0vA9YPe_ihiKYHE",
}
}
{
"v" : "KERI10JSON00011c_",
"t" : "rpy",
"d": "EZ-i0d8JZAoTNZH3ULaU6JR2nmwyvYAfSVPzhzS6b5CM",
"dt": "2020-08-22T17:50:12.988921+00:00",
"r" : "/end/role/cut",
"a" :
{
"cid": "EaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM",
"role": "watcher", # one of Roles
"eid": "BrHLayDN-mXKv62DAjFLX1_Y5yEUe0vA9YPe_ihiKYHE",
}
}
"""
# reply specific logic
if route.startswith("/end/role/add"):
allowed = True
elif route.startswith("/end/role/cut"):
allowed = False
else: # unsupported route
raise ValidationError(f"Usupported route={route} in {Ilks.rpy} "
f"msg={serder.ked}.")
route = "/end/role" # escrow based on route base
data = serder.ked['a']
for k in ("cid", "role", "eid"):
if k not in data:
raise ValidationError(f"Missing element={k} from attributes in"
f" {Ilks.rpy} msg={serder.ked}.")
cider = Prefixer(qb64=data["cid"]) # raises error if unsupported code
cid = cider.qb64 # controller authorizing eid at role
role = data["role"]
if role not in Roles:
raise ValidationError(f"Invalid role={role} from attributes in "
f"{Ilks.rpy} msg={serder.ked}.")
eider = Prefixer(qb64=data["eid"]) # raises error if unsupported code
eid = eider.qb64 # controller of endpoint at role
aid = cid # authorizing attribution id
keys = (aid, role, eid)
osaider = self.db.eans.get(keys=keys) # get old said if any
if osaider is not None and osaider.qb64b == diger.qb64b: # check idempotent
osaider = None
# BADA Logic
accepted = self.rvy.acceptReply(serder=serder, saider=diger, route=route,
aid=aid, osaider=osaider, cigars=cigars,
tsgs=tsgs)
if not accepted:
msg = f"Unverified end role reply = {serder.said} role = {role}"
logger.debug(f"Kevery: %s", msg)
logger.debug(f"Event=\n%s\n", serder.pretty())
raise UnverifiedReplyError(msg)
self.updateEnd(keys=keys, saider=diger, allowed=allowed) # update .eans and .ends
[docs]
def processReplyLocScheme(self, *, serder, diger, route,
cigars=None, tsgs=None):
"""
Process one reply message for route = /loc/scheme with either
attached nontrans receipt couples in cigars or attached trans indexed
sig groups in tsgs.
Assumes already validated diger, dater, and route from serder.ked
Parameters:
serder (SerderKERI): instance of reply msg (SAD)
diger (Diger): instance from said in serder (SAD)
route (str): reply route
cigars (list): of Cigar instances that contain nontrans signing couple
signature in .raw and public key in .verfer
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
EndAuthRecord
cid: str = "" # identifier prefix of controller that authorizes endpoint
roles: list[str] = field(default_factory=list) # str endpoint roles such as watcher, witness etc
LocationRecord:
url: str # full url including host:port/path?query scheme is optional
cids: list[EndAuthRecord] = field(default_factory=list) # optional authorization record references
Reply Message:
{
"v" : "KERI10JSON00011c_",
"t" : "rpy",
"d": "EZ-i0d8JZAoTNZH3ULaU6JR2nmwyvYAfSVPzhzS6b5CM",
"dt": "2020-08-22T17:50:12.988921+00:00",
"r" : "/loc/scheme",
"a" :
{
"eid": "BrHLayDN-mXKv62DAjFLX1_Y5yEUe0vA9YPe_ihiKYHE",
"scheme": "http", # one of Schemes
"url": "http://localhost:8080/watcher/wilma",
}
}
{
"v" : "KERI10JSON00011c_",
"t" : "rpy",
"d": "EZ-i0d8JZAoTNZH3ULaU6JR2nmwyvYAfSVPzhzS6b5CM",
"dt": "2020-08-22T17:50:12.988921+00:00",
"r" : "/loc/scheme",
"a" :
{
"eid": "BrHLayDN-mXKv62DAjFLX1_Y5yEUe0vA9YPe_ihiKYHE",
"scheme": "http", # one of Schemes
"url": "", # Nullifies
}
}
"""
# reply specific logic
if not route.startswith("/loc/scheme"):
raise ValidationError("Usupported route={} in {} msg={}."
"".format(route, Ilks.rpy, serder.ked))
route = "/loc/scheme" # escrow based on route base
data = serder.ked["a"]
for k in ("eid", "scheme", "url"):
if k not in data:
raise ValidationError("Missing element={} from attributes in {} "
"msg={}.".format(k, Ilks.rpy, serder.ked))
eider = Prefixer(qb64=data["eid"]) # raises error if unsupported code
eid = eider.qb64 # controller of endpoint at role
scheme = data["scheme"]
if scheme not in Schemes:
raise ValidationError("Invalid scheme={} from attributes in {} "
"msg={}.".format(scheme, Ilks.rpy, serder.ked))
url = data["url"]
splits = urlsplit(url)
# empty scheme allowed in, will use scheme field
if splits.scheme and splits.scheme != scheme: # non empty but not match
raise ValidationError("Invalid url={} for scheme={} from attributes in {} "
"msg={}.".format(url, scheme, Ilks.rpy, serder.ked))
# empty host port allowed will use default localhost:8080
aid = eid # authorizing attribution id
keys = (aid, scheme)
osaider = self.db.lans.get(keys=keys) # get old said if any
# BADA Logic
accepted = self.rvy.acceptReply(serder=serder, saider=diger, route=route,
aid=aid, osaider=osaider, cigars=cigars,
tsgs=tsgs)
if not accepted:
msg = f"Unverified loc scheme reply URL={url} SAID={serder.said}"
logger.debug(msg)
logger.debug("Event Body=\n%s\n", serder.pretty())
raise UnverifiedReplyError(msg)
self.updateLoc(keys=keys, saider=diger, url=url) # update .lans and .locs
[docs]
def processReplyKeyStateNotice(self, *, serder, diger, route,
cigars=None, tsgs=None, **kwa):
""" Process one reply message for key state = /ksn
Process one reply message for key state = /ksn
with either attached nontrans receipt couples in cigars or attached trans
indexed sig groups in tsgs.
Assumes already validated diger, dater, and route from serder.ked
Parameters:
serder (SerderKERI): instance of reply msg (SAD)
diger (Diger): instance from said in serder (SAD)
route (str): reply route
cigars (list): of Cigar instances that contain nontrans signing couple
signature in .raw and public key in .verfer
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
Reply Message:
{
"v" : "KERI10JSON00011c_",
"t" : "rpy",
"d": "EZ-i0d8JZAoTNZH3ULaU6JR2nmwyvYAfSVPzhzS6b5CM",
"dt": "2020-08-22T17:50:12.988921+00:00",
"r" : "/ksn/EeS834LMlGVEOGR8WU3rzZ9M6HUv_vtF32pSXQXKP7jg",
"a" :
{
"v": "KERI10JSON000274_",
"i": "EeS834LMlGVEOGR8WU3rzZ9M6HUv_vtF32pSXQXKP7jg",
"s": "1",
"t": "ksn",
"p": "ESORkffLV3qHZljOcnijzhCyRT0aXM2XHGVoyd5ST-Iw",
"d": "EtgNGVxYd6W0LViISr7RSn6ul8Yn92uyj2kiWzt51mHc",
"f": "1",
"dt": "2021-11-04T12:55:14.480038+00:00",
"et": "ixn",
"kt": "1",
"k": [
"DTH0PwWwsrcO_4zGe7bUR-LJX_ZGBTRsmP-ZeJ7fVg_4"
],
"n": "E6qpfz7HeczuU3dAd1O9gPPS6-h_dCxZGYhU8UaDY2pc",
"bt": "3",
"b": [
"BGKVzj4ve0VSd8z_AmvhLg4lqcC_9WYX90k03q-R_Ydo",
"BuyRFMideczFZoapylLIyCjSdhtqVb31wZkRKvPfNqkw",
"Bgoq68HCmYNUDgOz4Skvlu306o_NY-NrYuKAVhk3Zh9c"
],
"c": [],
"ee": {
"s": "0",
"d": "ESORkffLV3qHZljOcnijzhCyRT0aXM2XHGVoyd5ST-Iw",
"br": [],
"ba": []
},
"di": ""
}
}
"""
cigars = cigars if cigars is not None else []
tsgs = tsgs if tsgs is not None else []
# reply specific logic
if not route.startswith("/ksn"):
raise ValidationError(f"Usupported route={route} in {Ilks.rpy} "
f"msg={serder.ked}.")
aid = kwa["aid"]
data = serder.ked["a"]
try:
ksr = KeyStateRecord._fromdict(d=data)
except Exception as ex:
raise ValidationError(f"Malformed key state notice = {data}.") from ex
# fetch from serder to process
pre = ksr.i
sn = int(ksr.s, 16)
# check source and ensure we should accept it
baks = ksr.b
wats = set()
for _, habr in self.db.habs.getTopItemIter():
wats |= set(habr.watchers)
# not in promiscuous mode
if not self.lax:
if aid != ksr.i and \
aid not in baks and \
aid not in wats:
raise UntrustedKeyStateSource("key state notice for {} from untrusted source {} "
.format(ksr.pre, aid))
if ksr.i in self.kevers:
kever = self.kevers[ksr.i]
if int(ksr.s, 16) < kever.sner.num:
raise ValidationError("Skipped stale key state at sn {} for {}."
"".format(int(ksr.s, 16), ksr.i))
keys = (pre, aid,)
osaider = self.db.knas.get(keys=keys) # get old said if any
dater = Dater(dts=ksr.dt)
# BADA Logic
accepted = self.rvy.acceptReply(serder=serder, saider=diger, route=route,
aid=aid, osaider=osaider, cigars=cigars,
tsgs=tsgs)
if not accepted:
raise UnverifiedReplyError(f"Unverified key state notice reply. {serder.ked}")
ldig = self.db.kels.getLast(keys=pre, on=sn) # retrieve dig of last event at sn.
ksr_diger = Diger(qb64=ksr.d)
# Only accept key state if for last seen version of event at sn
if ldig is not None: # escrow because event does not yet exist in database
ldig = ldig.encode("utf-8")
# retrieve last event itself of signer given sdig
sserder = self.db.evts.get(keys=(pre, ldig))
# assumes db ensures that sserder must not be none because sdig was in KE
if not sserder.compare(said=ksr_diger.qb64b): # mismatch events problem with replay
raise ValidationError(f"Mismatch keystate at sn = {int(ksr.s,16)} with db.")
self.updateKeyState(aid=aid, ksr=ksr, saider=ksr_diger, dater=dater)
self.cues.push(dict(kin="keyStateSaved", ksn=asdict(ksr)))
[docs]
def updateEnd(self, keys, saider, allowed=None):
"""
Update end auth database .eans and end database .ends.
Parameters:
keys (tuple): of key strs for databases (cid, role, eid)
saider (Diger): instance from said in reply serder (SAD)
allowed (bool): True allow eid to be endpoint provided
False otherwise
"""
# update .eans and .ends
self.db.eans.pin(keys=keys, val=saider) # overwrite
if ender := self.db.ends.get(keys=keys): # preexisting record
ender.allowed = allowed # update allowed status
else: # no preexisting record
ender = EndpointRecord(allowed=allowed) # create new record
self.db.ends.pin(keys=keys, val=ender) # overwrite
[docs]
def updateLoc(self, keys, saider, url):
"""
Update loc auth database .lans and loc database .locs.
Parameters:
keys (tuple): of key strs for databases (eid, scheme)
saider (Diger): instance from said in reply serder (SAD)
url (str): endpoint url
"""
self.db.lans.pin(keys=keys, val=saider) # overwrite
if locer := self.db.locs.get(keys=keys): # preexisting record
locer.url = url # update preexisting record
else: # no preexisting record
locer = LocationRecord(url=url) # create new record
self.db.locs.pin(keys=keys, val=locer) # overwrite
[docs]
def updateKeyState(self, aid, ksr, saider, dater):
"""
Update Reply SAD in database given by by serder and associated databases
for attached cig couple or sig quadruple.
Overwrites val at key if already exists.
Parameters:
aid (str): identifier of key state
ksr (KeyStateRecord): converted from key state notice dict in reply msg
saider (Diger): instance from said in serder (SAD)
dater (Dater): instance from date-time in serder (SAD)
"""
keys = (saider.qb64,)
# Add source of ksn to the key for DATEs too... (source AID, ksn AID)
self.db.kdts.put(keys=keys, val=dater) # first one idempotent
self.db.ksns.pin(keys=keys, val=ksr) # first one idempotent
# Add source of ksr to the key... (ksr AID, source aid)
self.db.knas.pin(keys=(ksr.i, aid), val=saider) # overwrite
def removeKeyState(self, saider):
if saider:
keys = (saider.qb64,)
self.db.ksns.rem(keys=keys)
self.db.kdts.rem(keys=keys)
[docs]
def processReplyAddWatched(self, *, serder, diger, route,
cigars=None, tsgs=None, **kwa):
""" Process one reply message for adding an AID for a watcher to watch
Process one reply message for adding an AID for a watcher to watch = /watcher/{aid}/add
with either attached nontrans receipt couples in cigars or attached trans
indexed sig groups in tsgs.
Assumes already validated diger, dater, and route from serder.ked
Parameters:
serder (SerderKERI): instance of reply msg (SAD)
diger (Diger): instance from said in serder (SAD)
route (str): reply route
cigars (list): of Cigar instances that contain nontrans signing couple
signature in .raw and public key in .verfer
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
Reply Message:
{
"v" : "KERI10JSON00011c_",
"t" : "rpy",
"d": "EZ-i0d8JZAoTNZH3ULaU6JR2nmwyvYAfSVPzhzS6b5CM",
"dt": "2020-08-22T17:50:12.988921+00:00",
"r" : "/watcher/BrHLayDN-mXKv62DAjFLX1_Y5yEUe0vA9YPe_ihiKYHE/add",
"a" :
{
"cid": "EyX-zd8JZAoTNZH3ULaU6JR2nmwyvYAfSVPzhzS6b5CM"
"oid": "EM0-i05TNZJZAoH3UR2nmLaU6JwyvPzhzS6YAfSVbMC5"
"oobi": "http://example.com/oobi/EyX-zd8JZAoTNZH3ULaU6JR2nmwyvYAfSVPzhzS6b5CM"
}
}
"""
aid = kwa["aid"]
action = kwa["action"]
# reply specific logic
if not route.startswith("/watcher"):
raise ValidationError(f"Usupported route={route} in {Ilks.rpy} "
f"msg={serder.ked}.")
# reply specific logic
if action == "add":
enabled = True
elif action == "cut":
enabled = False
else: # unsupported route
raise ValidationError(f"Usupported route={route} in {Ilks.rpy} "
f"msg={serder.ked}.")
route = f"/watcher/{aid}" # escrow based on route base
cigars = cigars if cigars is not None else []
tsgs = tsgs if tsgs is not None else []
data = serder.ked["a"]
cid = data["cid"]
oid = data["oid"]
oobi = data["oobi"] if "oobi" in data else None
keys = (cid, aid, oid)
osaider = self.db.wwas.get(keys=keys) # get old said if any
# BADA Logic
accepted = self.rvy.acceptReply(serder=serder, saider=diger, route=route,
aid=cid, osaider=osaider, cigars=cigars,
tsgs=tsgs)
if not accepted:
raise UnverifiedReplyError(f"Unverified watcher add reply. {serder.ked}")
if oobi:
self.db.oobis.pin(keys=(oobi,), val=OobiRecord(date=helping.nowIso8601()))
self.updateWatched(keys=keys, saider=diger, enabled=enabled)
[docs]
def updateWatched(self, keys, saider, enabled=None):
"""
Update loc auth database .lans and loc database .locs.
Parameters:
keys (tuple): of key strs for databases (eid, scheme)
saider (Diger): instance from said in reply serder (SAD)
enabled (bool): True means add observed to watcher, False means remove (cut)
"""
self.db.wwas.pin(keys=keys, val=saider) # overwrite
if observed := self.db.obvs.get(keys=keys): # preexisting record
observed.enabled = enabled # update preexisting record
else: # no preexisting record
observed = ObservedRecord(enabled=enabled, datetime=helping.nowIso8601()) # create new record
self.db.obvs.pin(keys=keys, val=observed) # overwrite
[docs]
def processQuery(self, serder, *, source=None, sigers=None, cigars=None, **kwa):
"""Process query mode replay message for collective or single element query.
Assume promiscuous mode for now.
Parameters:
serder (SerderKERI) is query message serder
source (Prefixer) identifier prefix of querier
sigers (list) of Siger instances of attached controller indexed sigs
cigars (list) of Cigar instance of attached non-trans sigs
"""
ked = serder.ked
ilk = ked["t"]
route = ked["r"]
qry = ked["q"]
# do signature validation and replay attack prevention logic here
# src, dt, route
if source is None and cigars:
dest = cigars[0].verfer.qb64
else:
dest = source.qb64
if route == "logs":
pre = qry["i"]
src = qry["src"]
anchor = qry["a"] if "a" in qry else None
sn = int(qry["s"], 16) if "s" in qry else None
fn = int(qry["fn"], 16) if "fn" in qry else 0
if pre not in self.kevers:
self.escrowQueryNotFoundEvent(serder=serder, prefixer=source, sigers=sigers, cigars=cigars)
msg = f"Query not found error on event route={route} SAID={serder.said}"
logger.debug(msg)
logger.debug("Query Body=\n%s\n", serder.pretty())
raise QueryNotFoundError(msg)
kever = self.kevers[pre]
if anchor:
if not self.db.fetchAllSealingEventByEventSeal(pre=pre, seal=anchor):
self.escrowQueryNotFoundEvent(serder=serder, prefixer=source, sigers=sigers, cigars=cigars)
msg = f"Query not found error on event route={route} SAID={serder.said}"
logger.debug(msg)
logger.debug("Query Body=\n%s\n", serder.pretty())
raise QueryNotFoundError(msg)
elif sn is not None:
if kever.sner.num < sn or not self.db.fullyWitnessed(kever.serder):
self.escrowQueryNotFoundEvent(serder=serder, prefixer=source, sigers=sigers, cigars=cigars)
msg = f"Query not found error on event route={route} SAID={serder.said}"
logger.debug(msg)
logger.debug("Query Body=\n%s\n", serder.pretty())
raise QueryNotFoundError(msg)
msgs = list() # outgoing messages
for msg in self.db.clonePreIter(pre=pre, fn=fn):
msgs.append(msg)
if kever.delpre:
cloner = self.db.clonePreIter(pre=kever.delpre, fn=0) # create iterator at 0
for msg in cloner:
msgs.append(msg)
if msgs:
self.cues.push(dict(kin="replay", pre=pre, src=src, msgs=msgs, dest=dest))
elif route == "ksn":
pre = qry["i"]
src = qry["src"]
if pre not in self.kevers:
self.escrowQueryNotFoundEvent(serder=serder, prefixer=source, sigers=sigers, cigars=cigars)
msg = f"Query not found error on event route={route} SAID={serder.said}"
logger.debug(msg)
logger.debug("Query Body=\n%s\n", serder.pretty())
raise QueryNotFoundError(msg)
kever = self.kevers[pre]
# get list of witness signatures to ensure we are presenting a fully witnessed event
wigers = self.db.wigs.get(keys=(pre, kever.serder.saidb))
if len(wigers) < kever.toader.num:
self.escrowQueryNotFoundEvent(serder=serder, prefixer=source, sigers=sigers, cigars=cigars)
msg = f"Query not found error on event route={route} SAID={serder.said}"
logger.debug(msg)
logger.debug("Query Body=\n%s\n", serder.pretty())
raise QueryNotFoundError(msg)
rserder = reply(route=f"/ksn/{src}", data=kever.state()._asdict())
self.cues.push(dict(kin="reply", src=src, route="/ksn", serder=rserder,
dest=dest))
elif route == "mbx":
pre = qry["i"]
src = qry["src"]
topics = qry["topics"]
if pre not in self.kevers:
self.escrowQueryNotFoundEvent(serder=serder, prefixer=source, sigers=sigers, cigars=cigars)
msg = f"Query not found error on event route={route} SAID={serder.said}"
logger.debug(msg)
logger.debug("Query Body=\n%s\n", serder.pretty())
raise QueryNotFoundError(msg)
self.cues.push(dict(kin="stream", serder=serder, pre=pre, src=src, topics=topics))
# if pre in self.kevers:
# kever = self.kevers[pre]
# if src in kever.wits and src in self.db.prefixes: # We are a witness for identifier
# self.cues.push(dict(kin="stream", serder=serder, pre=pre, src=src, topics=topics))
else:
self.cues.push(dict(kin="invalid", serder=serder))
msg = f"Invalid query message {ilk} for event route={route} SAID={serder.said}"
logger.info(msg)
logger.debug("Query Body=\n%s\n", serder.pretty())
raise ValidationError(msg)
[docs]
def fetchEstEvent(self, pre, sn):
"""
Returns SerderKERI instance of establishment event that is authoritative for
event in KEL for pre at sn.
Returns None if no event at sn accepted in KEL for pre
Parameters:
pre is qb64 of identifier prefix for KEL
sn is int sequence number of event in KEL of pre
"""
found = False
while not found:
dig = self.db.kels.getLast(keys=pre, on=sn)
if not dig:
return None
# retrieve event by dig
dig = dig.encode("utf-8")
if not (serder := self.db.evts.get(keys=(pre, dig))):
return None
if serder.ked["t"] in (Ilks.icp, Ilks.dip, Ilks.rot, Ilks.drt):
return serder # establishment event so return
sn = int(serder.ked["s"], 16) - 1 # set sn to previous event
if sn < 0: # no more events
return None
[docs]
def escrowMFEvent(self, serder, sigers, wigers=None,
number=None, diger=None, local=True):
"""
Update associated logs for escrow of MisFit event
Parameters:
serder (SerderKERI): instance of event
sigers (list): of Siger instance for event
number (Number): instance of sn of delegating/issuing event if any
diger (Diger): instance of dig of delegating/issuing event if any
wigers (list): of witness signatures
local (bool): event source for validation logic
True means event source is local (protected).
False means event source is remote (unprotected).
Event validation logic is a function of local or remote
"""
local = True if local else False
dgkey = dgKey(serder.preb, serder.saidb)
if esr := self.db.esrs.get(keys=dgkey): # preexisting esr
if local and not esr.local: # local overwrites prexisting remote
esr.local = local
self.db.esrs.pin(keys=dgkey, val=esr)
# otherwise don't change
else: # not preexisting so put
esr = EventSourceRecord(local=local)
self.db.esrs.put(keys=(serder.preb, serder.saidb), val=esr)
self.db.dtss.put(keys=dgkey, val=Dater())
self.db.sigs.put(keys=dgkey, vals=sigers)
self.db.evts.put(keys=(serder.preb, serder.saidb), val=serder)
if wigers:
self.db.wigs.put(keys=dgkey, vals=wigers)
if number and diger:
self.db.udes.put(keys=dgkey, val=(number, diger)) # idempotent
self.db.misfits.add(keys=(serder.pre, serder.snh), val=serder.saidb)
# log escrowed
logger.debug("Kevery process: escrowed misfit event=\n%s", serder.pretty())
[docs]
def escrowOOEvent(self, serder, sigers, delsner=None, delsger=None, wigers=None, local=True):
"""Update associated logs for escrow of Out-of-Order event
Parameters:
serder (SerderKERI): instance of event
sigers (list): of Siger instance for event
delnumber (Number): instance of sn of delegating/issuing event if any
diger (Diger): instance of said of delegating/issuing event if any
wigers (list): of witness signatures
local (bool): event source for validation logic
True means event source is local (protected).
False means event source is remote (unprotected).
Event validation logic is a function of local or remote
"""
local = True if local else False
dgkey = dgKey(serder.preb, serder.saidb)
if esr := self.db.esrs.get(keys=dgkey): # preexisting esr
if local and not esr.local: # local overwrites prexisting remote
esr.local = local
self.db.esrs.pin(keys=dgkey, val=esr)
# otherwise don't change
else: # not preexisting so put
esr = EventSourceRecord(local=local)
self.db.esrs.put(keys=dgkey, val=esr)
self.db.dtss.put(keys=dgkey, val=Dater())
self.db.sigs.put(keys=dgkey, vals=sigers)
self.db.evts.put(keys=(serder.preb, serder.saidb), val=serder)
if wigers:
self.db.wigs.put(keys=dgkey, vals=wigers)
if delsner and delsger:
self.db.udes.put(keys=dgkey, val=(delsner, delsger)) # idempotent
self.db.ooes.add(keys=serder.preb, on=serder.sn, val=serder.saidb)
# log escrowed
logger.debug("Kevery process: escrowed out of order event=\n%s", serder.pretty())
[docs]
def escrowQueryNotFoundEvent(self, prefixer, serder, sigers, cigars=None):
"""
Update associated logs for escrow of Query Not Found event
Parameters:
prefixer (Prefixer): source of query message
serder (SerderKERI): instance of event
sigers (list): of Siger instance for event
cigars (list): of non-transferable receipts
"""
cigars = cigars if cigars is not None else []
dgkey = dgKey(prefixer.qb64b, serder.saidb)
self.db.dtss.put(keys=dgkey, val=Dater())
self.db.sigs.put(keys=dgkey, vals=sigers)
self.db.evts.put(keys=(prefixer.qb64b, serder.saidb), val=serder)
self.db.qnfs.add(keys=(prefixer.qb64, serder.said), val=serder.saidb)
for cigar in cigars:
self.db.rcts.add(keys=(prefixer.qb64, serder.said), val=(cigar.verfer, cigar))
# log escrowed
logger.trace("Kevery: escrowed query not found event = %s", serder.said)
logger.trace("Event Body=\n%s\n", serder.pretty())
[docs]
def escrowLDEvent(self, serder, sigers, local=True):
"""
Update associated logs for escrow of Likely Duplicitous event
Parameters:
serder is SerderKERI instance of event
sigers is list of Siger instance for event
local (bool): event source for validation logic
True means event source is local (protected).
False means event source is remote (unprotected).
Event validation logic is a function of local or remote
"""
local = True if local else False
dgkey = dgKey(serder.preb, serder.saidb)
if esr := self.db.esrs.get(keys=dgkey): # preexisting esr
if local and not esr.local: # local overwrites prexisting remote
esr.local = local
self.db.esrs.pin(keys=dgkey, val=esr)
# otherwise don't change
else: # not preexisting so put
esr = EventSourceRecord(local=local)
self.db.esrs.put(keys=dgkey, val=esr)
self.db.dtss.put(keys=dgkey, val=Dater())
self.db.sigs.put(keys=dgkey, vals=sigers)
self.db.evts.put(keys=(serder.preb, serder.saidb), val=serder)
self.db.addLde(snKey(serder.preb, serder.sn), serder.saidb)
# log duplicitous
logger.debug("Kevery process: escrowed likely duplicitous event=\n%s\n", serder.pretty())
[docs]
def escrowUWReceipt(self, serder, wigers, said):
"""
Update associated logs for escrow of Unverified Event Witness Receipt
(non-transferable)
Escrowed value is couple edig+wig where:
edig is receipted event dig not serder.dig
wig is witness indexed signature on receipted event with key pair
derived from witness nontrans identifier prefix in witness list.
Index is offset into witness list of latest establishment event
for receipted event.
Parameters:
serder (SerderKERI): instance of receipt msg not receipted event
wigers (list): of Siger instances for witness indexed signature
of receipted event
said (str) qb64 said of receipted event not serder.dig because
serder is a receipt not the receipted event
"""
# note receipt dig algo may not match database dig also so must always
# serder.compare to match. So receipts for same event may have different
# digs of that event due to different algos. So the escrow may have
# different dup at same key, sn. Escrow needs to include dig
# so can compare digs from receipt and in database for receipted event
# with different algos. Can't lookup event by dig for same reason. Must
# lookup last event by sn not by dig.
self.db.dtss.put(keys=dgKey(serder.preb, said), val=Dater())
for wiger in wigers: # escrow each couple
# don't know witness pre yet without witness list so no verfer in wiger
# if wiger.verfer.transferable: # skip transferable verfers
# continue # skip invalid triplets
self.db.uwes.add(keys=serder.preb, on=serder.sn, val=(said, wiger.qb64))
# log escrowed
logger.debug("Kevery process: escrowed unverified witness indexed receipt"
" of pre= %s sn=%x dig=%s", serder.pre, serder.sn, said)
[docs]
def escrowUReceipt(self, serder, cigars, said):
"""
Update associated logs for escrow of Unverified Event Receipt (non-transferable)
Escrowed value is triple edig+rpre+cig where:
edig is event dig
rpre is nontrans receiptor prefix
cig is non-indexed signature on event with key pair derived from rpre
Parameters:
serder (SerderKERI): instance of receipt msg not receipted event
cigars (list): of Cigar instances for event receipt
said (str): qb64 said in receipt of receipted event not serder.dig because
serder is of receipt not receipted event
"""
# note receipt dig algo may not match database dig also so must always
# serder.compare to match. So receipts for same event may have different
# digs of that event due to different algos. So the escrow may have
# different dup at same key, sn. Escrow needs to include dig
# so can compare digs from receipt and in database for receipted event
# with different algos. Can't lookup event by dig for same reason. Must
# lookup last event by sn not by dig.
self.db.dtss.put(keys=dgKey(serder.preb, said), val=Dater())
for cigar in cigars: # escrow each triple
if cigar.verfer.transferable: # skip transferable verfers
continue # skip invalid triplets
trituple = (
Diger(qb64=said),
Prefixer(qb64=cigar.verfer.qb64),
cigar
)
self.db.ures.add(keys=(serder.pre, Number(num=serder.sn, code=NumDex.Huge).qb64), val=trituple)
# log escrowed
logger.debug("Kevery process: escrowed unverified receipt of pre= %s "
" sn=%x dig=%s", serder.pre, serder.sn, said)
[docs]
def escrowTRGroups(self, serder, tsgs):
"""
Update associated logs for escrow of Transferable Receipt Groups for
event (transferable)
Parameters:
serder instance of receipt message not receipted event
tsgs is list of tuples of form: (prefixer,seqner,diger, sigers)
prefixer is Prefixer instance of prefix of receipter
number is Number instance of sn of est event of receiptor
diger is Diger instance of digest of est event of receiptor
sigers is list of Siger instances of multi-sig of receiptor
escrow quintuple for each siger
quintuple = edig+pre+snu+dig+sig
where:
edig is receipted event dig (serder.dig)
pre is receipter prefix
snu is receipter est event sn
dig is receipt est evant dig
sig is indexed sig of receiptor of receipted event
"""
# Receipt dig algo may not match database dig. So must always
# serder.compare to match. So receipts for same event may have different
# digs of that event due to different algos. So the escrow may have
# different dup at same key, sn. Escrow needs to be quintuple with
# edig, validator prefix, validtor est event sn, validator est evvent dig
# and sig stored at kel pre, sn so can compare digs
# with different algos. Can't lookup by dig for the same reason. Must
# lookup last event by sn not by dig.
for tsg in tsgs:
prefixer, number, saider, sigers = tsg
self.db.dtss.put(keys=dgKey(serder.preb, serder.saidb), val=Dater())
# since serder of of receipt not receipted event must use dig in
# serder.ked["d"] not serder.dig
for siger in sigers: # escrow each quintlet
quintuple = (
Diger(qb64=serder.ked["d"]),
prefixer,
Number(num=number.sn),
Diger(qb64=saider.qb64),
siger,
)
self.db.vres.add(keys=snKey(serder.preb, serder.sn), val=quintuple)
# log escrowed
logger.debug("Kevery process: escrowed unverified transferable receipt "
"of pre=%s sn=%x dig=%s by pre=%s", serder.pre,
serder.sn, serder.ked["d"], prefixer.qb64)
[docs]
def escrowTReceipts(self, serder, prefixer, number, saider, sigers):
"""
Update associated logs for escrow of Transferable Event Receipt Group
(transferable)
Parameters:
serder instance of receipt message not receipted event
prefixer is Prefixer instance of prefix of receipter
number is Number instance of sn of est event of receiptor
saider is Saider instance of said of est event of receiptor
igers is list of Siger instances of multi-sig of receiptor
escrow quintuple for each siger
quintuple = edig+pre+snu+dig+sig
where:
edig is receipted event dig (serder.dig)
pre is receipter prefix
snu is receipter est event sn
dig is receipt est evant dig
sig is indexed sig of receiptor of receipted event
"""
# Receipt dig algo may not match database dig. So must always
# serder.compare to match. So receipts for same event may have different
# digs of that event due to different algos. So the escrow may have
# different dup at same key, sn. Escrow needs to be quintuple with
# edig, validator prefix, validtor est event sn, validator est evvent dig
# and sig stored at kel pre, sn so can compare digs
# with different algos. Can't lookup by dig for the same reason. Must
# lookup last event by sn not by dig.
self.db.dtss.put(keys=dgKey(serder.preb, serder.saidb), val=Dater())
# since serder of of receipt not receipted event must use dig in
# serder.ked["d"] not serder.dig
for siger in sigers: # escrow each quintlet
quintuple = (
Diger(qb64=serder.ked["d"]),
prefixer,
Number(num=number.sn),
Diger(qb64=saider.qb64),
siger,
)
self.db.vres.add(keys=snKey(serder.preb, serder.sn), val=quintuple)
# log escrowed
logger.debug("Kevery process: escrowed unverified transferable receipt "
"of pre=%s sn=%x dig=%s by pre=%s", serder.pre,
serder.sn, serder.ked["d"], prefixer.qb64)
[docs]
def escrowTRQuadruple(self, serder, sprefixer, snumber, diger, siger):
"""Update associated logs for escrow of Unverified Transferable Receipt
(transferable)
escrow quintuple made from quadruple where:
quadruple = spre+ssnu+sdig+sig (s is trans receipt signer)
quintuple = edig+spre+ssnu+sdig+sig (edig is signed event digest)
Parameters:
serder (SerderKERI): instance of receipt message not receipted event
sprefixer (Prefixer): instance receiptor AID
snumber (Number): instance of sn of est event for receiptor key state
diger (Diger): instance said digest est event or receipt key state
siger (Siger): instance of signature of receiptor
"""
# Receipt dig algo may not match database dig. So must always
# serder.compare to match. So receipts for same event may have different
# digs of that event due to different algos. So the escrow may have
# different dup at same key, sn. Escrow needs to be quintuple with
# edig, validator prefix, validtor est event sn, validator est evvent dig
# and sig stored at kel pre, sn so can compare digs
# with different algos. Can't lookup by dig for the same reason. Must
# lookup last event by sn not by dig.
self.db.dtss.put(keys=dgKey(serder.preb, serder.said), val=Dater())
quintuple = (
Diger(qb64=serder.said), # event digest
sprefixer, # Prefixer receiptor
Number(num=snumber.sn), # SN of est event of receiptor key state
Diger(qb64=diger.qb64), # est event said of receiptro key state
siger, # signature of receiptor using est event key state
)
self.db.vres.add(keys=snKey(serder.preb, serder.sn), val=quintuple)
# log escrowed
logger.debug("Kevery process: escrowed unverified transferabe validator "
"receipt of pre= %s sn=%x dig=%s", serder.pre, serder.sn,
serder.said)
[docs]
def processEscrows(self):
"""
Iterate throush escrows and process any that may now be finalized
Parameters:
"""
try:
self.processEscrowOutOfOrders()
self.processEscrowUnverWitness()
self.processEscrowUnverNonTrans()
self.processEscrowUnverTrans()
self.processEscrowPartialDels()
self.processEscrowPartialWigs()
self.processEscrowPartialSigs()
self.processEscrowDuplicitous()
self.processQueryNotFound()
except Exception as ex: # log diagnostics errors etc
if logger.isEnabledFor(logging.DEBUG):
logger.trace("Kevery: other escrow process error: %s\n", ex.args[0])
logger.exception("Kevery other escrow process error: %s\n", ex.args[0])
raise ex
[docs]
def processEscrowOutOfOrders(self):
"""
Process events escrowed by Kever that are recieved out-of-order.
An event is out of order if its prior event has not been accepted into its KEL.
Without the prior event there is no way to know the key state and therefore no way
to verify signatures on the out-of-order event.
Escrowed items are indexed in database table keyed by prefix and
sn with duplicates given by different dig inserted in insertion order.
This allows FIFO processing of events with same prefix and sn but different
digest.
Uses .db.ooes.addOn(pre, key, val) which is OnIoVal with dups.
Value is dgkey for event stored in .Evt where .Evt has serder.raw of event.
Original Escrow steps:
dgkey = dgKey(pre, serder.dig)
self.db.dtss.put(keys=dgkey, val=Dater())
self.db.sigs.put(keys=dgkey, vals=sigers)
self.db.evts.put(keys=(pre, serder.dig), val=serder)
self.db.ooes.addOn(pre, sn, serder.dig)
where:
serder is SerderKERI instance of event
sigers is list of Siger instance for event
pre is str qb64 of identifier prefix of event
sn is int sequence number of event
Steps:
Each pass (walk index table)
For each prefix,sn
For each escrow item dup at prefix,sn:
Get Event
Get and Attach Signatures
Process event as if it came in over the wire
If successful then remove from escrow table
"""
for pre, sn, edig in self.db.ooes.getAllItemIter():
if isinstance(pre, (tuple, list)):
pre = pre[0]
edig = edig.encode("utf-8") # convert back to bytes
try:
dgkey = dgKey(pre, edig)
if not (esr := self.db.esrs.get(keys=dgkey)): # get event source, otherwise error
# no local source so raise ValidationError which unescrows below
# no local source so raise ValidationError which unescrows below
msg = f"OOO Missing escrowed event source at dig = {edig}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# check date if expired then remove escrow.
dater = self.db.dtss.get(keys=dgkey)
if dater is None: # no datetime stored
# no date time so raise ValidationError which unescrows below
msg = f"OOO Missing escrowed event datetime at dig = {edig.decode()}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# do date math here and discard if stale
dtnow = helping.nowUTC()
dte = dater.datetime
if (dtnow - dte) > datetime.timedelta(seconds=self.TimeoutOOE):
# escrow stale so raise ValidationError which unescrows below
msg = f"OOO Stale event escrow at dig = {edig.decode()}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# get the escrowed event using edig
if (eserder := self.db.evts.get(keys=(pre, bytes(edig)))) is None:
# no event so raise ValidationError which unescrows below
msg = f"OOO Missing escrowed event at dig = {edig.decode()}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# get sigs and attach
sigers = self.db.sigs.get(keys=(pre, edig))
if not sigers: # otherwise its a list of sigs
# no sigs so raise ValidationError which unescrows below
msg = f"OOO Missing escrowed event sigs at dig = {edig.decode()}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# process event
sigers = self.db.sigs.get(keys=(pre, edig))
# get wigers
wigers = self.db.wigs.get(keys=(pre, bytes(edig)))
self.processEvent(serder=eserder, sigers=sigers, wigers=wigers, local=esr.local)
# If process does NOT validate event with sigs, becasue it is
# still out of order then process will attempt to re-escrow
# and then raise OutOfOrderError (subclass of ValidationError)
# so we can distinquish between ValidationErrors that are
# re-escrow vs non re-escrow. We want process to be idempotent
# with respect to processing events that result in escrow items.
# On re-escrow attempt by process, Ooe escrow is called by
# Kevery.self.escrowOOEvent Which calls
# self.db.ooes.addOn(pre, sn, serder.digb)
# which in turn will not enter dig as dup if one already exists.
# So re-escrow attempt will not change the escrowed ooe db.
# Non re-escrow ValidationError means some other issue so unescrow.
# No error at all means processed successfully so also unescrow.
except OutOfOrderError as ex:
# still waiting on missing prior event to validate
if logger.isEnabledFor(logging.TRACE):
logger.trace("Kevery OOO escrow unescrow failed: %s\n", ex.args[0])
logger.exception("Kevery OOO escrow unescrow failed: %s\n", ex.args[0])
except Exception as ex: # log diagnostics errors etc
# error other than out of order so remove from OO escrow
self.db.ooes.rem(keys=pre, on=sn, val=edig) # removes one escrow at key val
if logger.isEnabledFor(logging.DEBUG):
logger.debug("Kevery: OOO escrow other error on escrow: %s\n", ex.args[0])
logger.exception("Kevery: OOO escrow other error on : %s\n", ex.args[0])
else: # unescrow succeeded, remove from escrow
# We don't remove all escrows at pre,sn because some might be
# duplicitous so we process remaining escrows in spite of found
# valid event escrow.
self.db.ooes.rem(keys=pre, on=sn, val=edig) # removes one escrow at key val
logger.info("Kevery OOO unescrow succeeded in valid event: "
"event=%s", eserder.said)
logger.debug("Event=\n%s\n", eserder.pretty())
[docs]
def processEscrowPartialSigs(self):
"""
Process events escrowed by Kever that were only partially fulfilled,
either due to missing signatures or missing dependent events like a
delegating event. But event has at least one verified signature.
Escrowed items are indexed in database table keyed by prefix and
sequence number with duplicates inserted in insertion order. This allows
FIFO processing of events with same prefix and sn.
Uses .db.pses.addOn(pre, sn, val) which is OnIoVal with dups.
Value is dgkey for event stored in .Evt where .Evt has serder.raw of event.
Original Escrow steps:
dgkey = dgKey(pre, serder.digb)
.db.dtss.put(keys=dgkey, val=Dater())
self.db.sigs.put(keys=dgkey, vals=sigers)
.db.evts.put(keys=(pre, serder.digb), val=serder)
.db.pses.addOn(pre, sn, serder.digb)
where:
serder is SerderKERI instance of event
sigers is list of Siger instance for event
pre is str qb64 of identifier prefix of event
sn is int sequence number of event
Steps:
Each pass (walk index table)
For each prefix,sn
For each escrow item dup at prefix,sn:
Get Event
Get and Attach Signatures
Process event as if it came in over the wire
If successful then remove from escrow table
"""
#key = ekey = b'' # both start same. when not same means escrows found
#while True: # break when done
for pre, sn, edig in self.db.pses.getAllItemIter():
eserder = None
try:
if isinstance(pre, (tuple, list)):
pre = pre[0]
edig = edig.encode("utf-8") # convert back to bytes
dgkey = dgKey(pre, edig)
if not (esr := self.db.esrs.get(keys=dgkey)): # get event source, otherwise error
# no local source so raise ValidationError which unescrows below
msg = f"PSE Missing escrowed event source at dig = {edig.decode()}"
logger.info("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# check date if expired then remove escrow.
dater = self.db.dtss.get(keys=dgkey)
if dater is None: # no datetime stored
# no date time so raise ValidationError which unescrows below
msg = f"PSE Missing escrowed event datetime at dig = {edig.decode()}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# do date math here and discard if stale
dtnow = helping.nowUTC()
dte = dater.datetime
if (dtnow - dte) > datetime.timedelta(seconds=self.TimeoutPSE):
# escrow stale so raise ValidationError which unescrows below
msg = f"PSE Stale event escrow at dig = {edig.decode()}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# get the escrowed event using edig
if (eserder := self.db.evts.get(keys=(pre, bytes(edig)))) is None:
# no event so so raise ValidationError which unescrows below
msg = f"PSE Missing escrowed evt at dig = {edig.decode()}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# get sigs and attach
sigers = self.db.sigs.get(keys=(pre, edig))
if not sigers: # otherwise its a list of sigs
# no sigs so raise ValidationError which unescrows below
msg = f"PSE Missing escrowed evt sigs at dig = {edig.decode()}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
wigers = self.db.wigs.get(keys=(pre, bytes(edig)))
if not wigers: # empty list wigs witness sigs not wits
# wigs maybe empty if not wits or if wits while waiting
# for first witness signature
# which may not arrive until some time after event is fully signed
# so just log for debugging but do not unescrow by raising
# ValidationError
logger.debug("Kevery unescrow wigs: No event wigs yet at."
"dig = %s", edig.decode())
# seal source couple (sequence number, said diger) of delegator/issuer if any
sner, sger = None, None
if (couple := self.db.udes.get(keys=dgkey)):
sner, sger = couple
# process event
sigers = self.db.sigs.get(keys=(pre, edig))
self.processEvent(serder=eserder, sigers=sigers, wigers=wigers,
delsner=sner,
delsger=sger,
eager=True, local=esr.local)
# If process does NOT validate sigs or delegation seal (when delegated),
# but there is still one valid signature then process will
# attempt to re-escrow and then raise MissingSignatureError
# or MissingDelegationSealError (subclass of ValidationError)
# so we can distinquish between ValidationErrors that are
# re-escrow vs non re-escrow. We want process to be idempotent
# with respect to processing events that result in escrow items.
# On re-escrow attempt by process, Pse escrow is called by
# Kever.self.escrowPSEvent Which calls
# self.db.pses.addOn(pre, sn, serder.digb)
# which in turn will not enter dig as dup if one already exists.
# So re-escrow attempt will not change the escrowed pse db.
# Non re-escrow ValidationError means some other issue so unescrow.
# No error at all means processed successfully so also unescrow.
except MissingSignatureError as ex: # MissingDelegationError)
# still waiting on missing sigs or missing seal to validate
# processEvent idempotently reescrowed
if logger.isEnabledFor(logging.TRACE):
logger.trace("Kevery: PSE unescrow failed: %s\n", ex.args[0])
logger.exception("Kevery: PSE unescrow failed: %s\n", ex.args[0])
except Exception as ex: # log diagnostics errors etc
# error other than waiting on sigs so remove from escrow
self.db.pses.rem(keys=pre, on=sn, val=edig) # removes one escrow at key val
#self.db.udes.rem(keys=dgkey) # leave here since could PartialDelegationEscrow
if eserder is not None and eserder.ked["t"] in (Ilks.dip, Ilks.drt,):
self.cues.push(dict(kin="psUnescrow", serder=eserder))
if logger.isEnabledFor(logging.DEBUG):
logger.trace("Kevery: PSE other error on unescrow: %s\n",
ex.args[0])
logger.exception("Kevery: PSE other error on unescrow: %s\n",
ex.args[0])
else: # unescrow succeeded, remove from escrow
# We don't remove all escrows at pre,sn because some might be
# duplicitous so we process remaining escrows in spite of found
# valid event escrow.
self.db.pses.rem(keys=pre, on=sn, val=edig) # removes one escrow at key val
self.db.udes.rem(keys=dgkey) # remove escrow if any
if eserder is not None and eserder.ked["t"] in (Ilks.dip, Ilks.drt,):
self.cues.push(dict(kin="psUnescrow", serder=eserder))
logger.info("Kevery: PSE unescrow succeeded in valid event event= %s", eserder.said)
logger.debug(f"Event=\n%s\n", eserder.pretty())
#if ekey == key: # still same so no escrows found on last while iteration
#break
#key = ekey # setup next while iteration, with key after ekey
[docs]
def processEscrowPartialWigs(self):
"""
Process events escrowed by Kever that were only partially fulfilled
due to missing signatures from witnesses. Events only make into this
escrow after fully signed and if delegated, delegation has been verified.
Escrowed items in .pwes are indexed in database table keyed by prefix and
sequence number with duplicates inserted in insertion order. This allows
FIFO processing of events with same prefix and sn.
Reads db.pwes .db.getPwe put there by .db.pwes.addOn(keys, on, val)
which is IOVal with dups.
Value is dgkey for event stored in .Evt where .Evt has serder.raw of event.
Original Escrow steps:
dgkey = dgKey(pre, serder.digb)
.db.dtss.put(keys=dgkey, val=Dater())
.db.putWigs(dgkey, [siger.qb64b for siger in sigers])
.db.pwes.addOn(pre, sn, serder.digb)
.db.evts.put(keys=(pre, serder.digb), val=serder)
where:
serder is SerderKERI instance of event
wigers is list of Siger instance for of witnesses of event
pre is str qb64 of identifier prefix of event
sn is int sequence number of event
Steps:
Each pass (walk index table)
For each prefix,sn
For each escrow item dup at prefix,sn:
Get Event
Get and Attach Signatures
Get and Attach Witness Signatures
Process event as if it came in over the wire
If successful then remove from escrow table
"""
for pre, sn, edig in self.db.pwes.getAllItemIter(keys=b''):
try:
if isinstance(pre, (tuple, list)):
pre = pre[0]
edig = edig.encode("utf-8")
dgkey = dgKey(pre, edig)
if not (esr := self.db.esrs.get(keys=dgkey)): # get event source, otherwise error
# no local source so raise ValidationError which unescrows below
msg = f"PWE Missing escrowed event source at dig = {edig.decode()}"
logger.info("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# check date if expired then remove escrow.
dater = self.db.dtss.get(keys=dgkey)
if dater is None: # no datetime stored
# no date time so raise ValidationError which unescrows below
msg = f"PWE Missing escrowed event datetime at dig = {edig.decode()}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# do date math here and discard if stale
dtnow = helping.nowUTC()
dte = dater.datetime
if (dtnow - dte) > datetime.timedelta(seconds=self.TimeoutPWE):
# escrow stale so raise ValidationError which unescrows below
msg = f"PWE Stale event escrow at dig = {edig.decode()}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# get the escrowed event using edig
if (eserder := self.db.evts.get((pre, bytes(edig)))) is None:
# no event so so raise ValidationError which unescrows below
msg = f"PWE Missing escrowed evt at dig = {edig.decode()}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# get sigs
sigers = self.db.sigs.get(keys=(pre, edig)) # list of sigs
if not sigers: # empty list
# no sigs so raise ValidationError which unescrows below
msg = f"PWE Missing escrowed evt sigs at dig = {edig.decode()}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# get witness signatures (wigs not wits)
wigers = self.db.wigs.get(keys=(pre, bytes(edig)))
if not wigers: # empty list
# wigs maybe empty if not wits or if wits while waiting
# for first witness signature
# which may not arrive until some time after event is fully signed
# so just log for debugging but do not unescrow by raising
# ValidationError
logger.debug("Kevery: PWE unescrow wigs: No event wigs yet at."
"dig = %s", edig.decode())
# raise ValidationError("Missing escrowed evt wigs at "
# "dig = {}.".format(bytes(edig)))
# process event
sigers = self.db.sigs.get(keys=(pre, edig))
# seal source couple (sequence number, said diger) of delegator/issuer if any
sner = sger = None
if (couple := self.db.udes.get(keys=(pre, bytes(edig)))):
sner, sger = couple
self.processEvent(serder=eserder, sigers=sigers, wigers=wigers,
delsner=sner,
delsger=sger,
eager=True, local=esr.local)
# If process does NOT validate wigs then process will attempt
# to re-escrow and then raise MissingWitnessSignatureError
# (subclass of ValidationError)
# so we can distinquish between ValidationErrors that are
# re-escrow vs non re-escrow. We want process to be idempotent
# with respect to processing events that result in escrow items.
# On re-escrow attempt by process, Pwe escrow is called by
# Kever.self.escrowPWEvent Which calls
# self.db.pwes.addOn(pre, sn, serder.digb)
# which in turn will NOT enter dig as dup if one already exists.
# So re-escrow attempt will not change the escrowed pwe db.
# Non re-escrow ValidationError means some other issue so unescrow.
# No error at all means processed successfully so also unescrow.
# Assumes that controller signature validation and delegation
# validation will be successful as event would not be in
# partially witnessed escrow unless they had already validated
except MissingWitnessSignatureError as ex: # MissingDelegationError
# still waiting on missing witness sigs or delegation
# processEvent idempotently reescrowed
if logger.isEnabledFor(logging.TRACE):
logger.trace("Kevery: PWE unescrow failed: %s\n", ex.args[0])
logger.exception("Kevery: PWE unescrow failed: %s\n", ex.args[0])
except Exception as ex: # log diagnostics errors etc
# error other than waiting on wigs so remove from escrow
self.db.pwes.rem(keys=pre, on=sn, val=edig) # removes one escrow at key val
#self.db.udes.rem(keys=dgkey) # leave here since could PartialDelegationEscrow
if logger.isEnabledFor(logging.TRACE):
logger.trace("Kevery: PWE other error on unescrow: %s\n", ex.args[0])
logger.exception("Kevery: PWE other error unescrow: %s\n", ex.args[0])
else: # unescrow succeeded, remove from escrow
# We don't remove all escrows at pre,sn because some might be
# duplicitous so we process remaining escrows in spite of found
# valid event escrow.
self.db.pwes.rem(keys=pre, on=sn, val=edig) # removes one escrow at key val
self.db.udes.rem(keys=dgkey) # remove escrow if any
logger.info("Kevery: PWE unescrow succeeded in valid event: key = %s \tdigest = %s",
pre, sn, edig.decode())
logger.debug("Event=\n%s\n", eserder.pretty())
[docs]
def processEscrowPartialDels(self):
"""
Process delgated events escrowed by Kever that were only partially fulfilled
due to missing or unverified delegation seals from delegators.
Events only make into this escrow after fully signed and if witnessed
fully witnessed.
db.pdes is an instance of subing.IoSetSuber and uses instance methods
for access to the underlying database.
Escrowed items in .pdes are indexed in database table keyed by prefix and
sequence number with multiple entries at same key held in insertion order.
This allows FIFO processing of escrowed events with same prefix and sn.
Value in each .pdes entry is dgkey (SAID) of event stored in db.evts where
db.evts holds SerderKERI.raw of event.
Steps:
Each pass (walk index table)
For each prefix,sn
For each escrow item dup at prefix,sn:
Get Event
Get and Attach Signatures
Get and Attach Witness Signatures
Process event as if it came in over the wire
If successful then remove from escrow table
"""
for (epre,), esn, edig in self.db.pdes.getAllItemIter(keys=b''):
try:
dgkey = dgKey(epre, edig)
if not (esr := self.db.esrs.get(keys=dgkey)): # get event source, otherwise error
# no local source so raise ValidationError which unescrows below
msg = f"PDE Missing escrowed event source at dig = {edig}"
logger.info("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# check date if expired then remove escrow.
dater = self.db.dtss.get(keys=dgkey)
if dater is None: # no datetime stored
# no date time so raise ValidationError which unescrows below
msg = f"PDE Missing escrowed event datetime at dig = {edig}"
logger.info("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# do date math here and discard if stale
dtnow = helping.nowUTC()
dte = dater.datetime
if (dtnow - dte) > datetime.timedelta(seconds=self.TimeoutPWE):
# escrow stale so raise ValidationError which unescrows below
msg = f"PDE Stale event escrow at dig = {edig}"
logger.info("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# get the escrowed event using edig
if (eserder := self.db.evts.get(keys=(epre, edig))) is None:
# no event so so raise ValidationError which unescrows below
msg = f"PDE Missing escrowed evt at dig = {edig}"
logger.info("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# get sigs
sigers = self.db.sigs.get(keys=dgkey) # list of sigs
if not sigers: # empty list
# no sigs so raise ValidationError which unescrows below
msg = f"PDE Missing escrowed evt sigs at dig = {edig}"
logger.info("Kevery unescrow error: %s", edig)
raise ValidationError(msg)
# get witness signatures (wigs not wits) assumes wont be in this
# escrow if wigs not needed because no wits
wigers = self.db.wigs.get(keys=dgkey) # list of wigs if any
# may want to checks wits and wigs here. We are assuming that
# never get to this escrow if wits and not wigs
#if wits and not wigers: # non empty wits but empty wigs
## wigs maybe empty if not wits or if wits while waiting
## for first witness signature
## which may not arrive until some time after event is fully signed
## so just log for debugging but do not unescrow by raising
## ValidationError
#logger.info("Kevery unescrow error: Missing event wigs at."
#"dig = %s", bytes(edig))
#raise ValidationError("Missing escrowed evt wigs at "
#"dig = {}.".format(bytes(edig)))
# setup parameters to process event
sigers = self.db.sigs.get(keys=dgkey)
# seal source couple (sequence number, said diger) of delegator/issuer if any
# If delegator KEL not available should also cue a trigger to
# get it if still missing when processing escrow.
sner = sger = None
if (couple := self.db.udes.get(keys=(epre, edig))):
sner, sger = couple # provided
#elif eserder.ked["t"] in (Ilks.dip, Ilks.drt,): # walk kel to find
#if eserder.pre in self.kevers:
#delpre = self.kevers[eserder.pre].delpre
#else:
#delpre = eserder.ked["di"]
#seal = dict(i=eserder.ked["i"], s=eserder.snh, d=eserder.said)
#srdr = self.db.findAnchoringSealEvent(pre=delpre, seal=seal)
#if srdr is not None: # found seal in srdr
#number = Number(sn=srdr.sn)
#diger = Diger(qb64=srdr.said)
#self.db.udes.put(keys=dgkey, val=(number, diger))
self.processEvent(serder=eserder, sigers=sigers, wigers=wigers,
delsner=sner,
delsger=sger,
eager=True, local=esr.local)
# If process does NOT validate delegation then process will attempt
# to re-escrow and then raise MissingDelegationError
# (subclass of ValidationError)
# so we can distinquish between ValidationErrors that are
# re-escrow vs non re-escrow. We want process to be idempotent
# with respect to processing events that result in escrow items.
# On re-escrow attempt by process, Pses escrow is called by
# Kever.self.escrowPDEvent Which calls
# self.db.pdes.addOn(pre, sn, serder.digb)
# which in turn will NOT enter dig as dup if one already exists.
# So re-escrow attempt will not change the escrows in db.pdes.
# Non re-escrow ValidationError means some other issue so unescrow.
# No error at all means processed successfully so also unescrow.
# Assumes that controller signature validation and delegation
# validation will be successful as event would not be in
# partially witnessed escrow unless they had already validated
except MissingDelegationError as ex:
# still waiting on missing delegation source seal
# processEvent idempotently reescrowed
if logger.isEnabledFor(logging.DEBUG):
logger.exception("Kevery PDE unescrow failed: %s", ex.args[0])
except Exception as ex: # log diagnostics errors etc
# error other than waiting on sigs or seal so remove from escrow
# removes one event escrow at key val
self.db.pdes.rem(keys=epre, on=esn, val=edig) # event idx escrow
self.db.udes.rem(keys=dgkey) # remove source seal escrow if any
if logger.isEnabledFor(logging.DEBUG):
logger.exception("Kevery PDE unescrowed: %s", ex.args[0])
else:
logger.error("Kevery PDE unescrowed: %s", ex.args[0])
else: # unescrow succeeded, remove from escrow
# We don't remove all escrows at pre,sn because some might be
# duplicitous so we process remaining escrows in spite of found
# valid event escrow.
# removes one event escrow at key val
self.db.pdes.rem(keys=epre, on=esn, val=edig) # event idx escrow
self.db.udes.rem(keys=dgkey) # remove source seal escrow if any
logger.info("Kevery PDE unescrow succeeded in valid event: "
"event=%s", eserder.said)
logger.debug("Event=\n%s\n", eserder.pretty())
[docs]
def processEscrowUnverWitness(self):
"""
Process escrowed unverified event receipts from witness receiptors
A receipt is unverified if the associated event has not been accepted
into its KEL.
Without the event, there is no way to know where to store the receipt
signatures neither to look up the witness list to verify the indexed
signatures.
The escrow is a couple with edig+wig where:
edig is receipted event digest
wig is witness indexed signature by key-pair derived from witness
prefix in associated witness list. Index is offset into witness
list of of latest establishment event for receipted event.
The (unescrowed) verified receipt is stored as wig at event digest edig
Escrowed items are indexed in database table keyed by prefix and
sn with duplicates given by different receipt couple inserted in insertion order.
This allows FIFO processing of escrows for events with same prefix and
sn but different digest.
Uses .uwes reads .db.uwes.get() which is B64OnIoDupSuber
Value is couple
Original Escrow steps:
self.db.dtss.put(keys=dgKey(pre, dig), val=Dater())
for wiger in wigers: # escrow each couple
couple = dig.encode("utf-8") + wiger.qb64b
self.db.addUwe(key=snKey(pre, sn), val=triple)
where:
dig is dig in receipt of receipted event
wigers is list of Siger instances witness indexed signature of
receipted event
pre is str qb64 of identifier prefix of receipted event
sn is int sequence number of receipted event
Steps:
Each pass (walk index table)
For each prefix,sn
For each escrow item dup at prefix,sn:
Get Event
compare dig so same event
verify wigs via wigers
If successful then remove from escrow table
"""
#for (pre, snh), (rdiger, wiger) in self.db.uwes.getTopItemIter():
for (pre, ), sn, (rdig, wig) in self.db.uwes.getTopItemIter():
try:
#rdigerBytes = rdig.encode('utf-8')
# check date if expired then remove escrow.
dater = self.db.dtss.get(keys=dgKey(pre, rdig))
if dater is None: # no datetime stored
# no date time so raise ValidationError which unescrows below
msg = f"UWE Missing escrowed event datetime at dig = {rdig}"
logger.trace("Kevery unescrow error: %s", rdig)
raise ValidationError(msg)
# do date math here and discard if stale
dtnow = helping.nowUTC()
dte = dater.datetime
if (dtnow - dte) > datetime.timedelta(seconds=self.TimeoutUWE):
# escrow stale so raise ValidationError which unescrows below
msg = f"UWE Stale event escrow at dig = {rdig}"
logger.trace("Kevery unescrow error: %s", rdig)
raise ValidationError(msg)
# lookup database dig of the receipted event in pwes escrow
# using pre and sn lastEvt
#sn = int(snh, 16)
rdiger = Diger(qb64=rdig)
wiger = Siger(qb64=wig)
found = self._processEscrowFindUnver(pre=pre,
sn=sn,
rsaider=rdiger,
wiger=wiger)
if not found: # no partial witness escrow of event found
# so keep in escrow by raising UnverifiedWitnessReceiptError
msg = f"UWE Missing witness receipted evt at pre={pre} {sn=}"
logger.trace("Kevery unescrow error: %s", msg)
raise UnverifiedWitnessReceiptError(msg)
except UnverifiedWitnessReceiptError as ex:
# still waiting on missing prior event to validate
# only happens if we process above
if logger.isEnabledFor(logging.DEBUG): # adds exception data
logger.trace("Kevery: UWE unescrow failed: %s\n", ex.args[0])
logger.exception("Kevery: UWE unescrow failed: %s\n",
ex.args[0])
except Exception as ex: # log diagnostics errors etc
# error other than out of order so remove from OO escrow
self.db.uwes.rem(keys=(pre,), on=sn, val=(rdig, wig))
if logger.isEnabledFor(logging.DEBUG): # adds exception data
logger.trace("Kevery: UWE other unescrow error: %s\n", ex.args[0])
logger.exception("Kevery: UWE other unescrow error: %s\n", ex.args[0])
else: # unescrow succeeded, remove from escrow
# We don't remove all escrows at pre,sn because some might be
# duplicitous so we process remaining escrows in spite of found
# valid event escrow.
self.db.uwes.rem(keys=(pre,), on=sn, val=(rdig, wig))
logger.info("Kevery UWE unescrow succeeded for event pre=%s sn=%s", pre, sn)
[docs]
def processEscrowUnverNonTrans(self):
"""
Process escrowed unverified event receipts from nontrans receiptors
A receipt is unverified if the associated event has not been accepted
into its KEL.
Without the event, there is no way to know where to store the receipts.
The escrow is a triple with edig+rpre+cig where:
edig is event digest
rpre is receiptor (signer) of event
cig is non-indexed signature by key-pair derived from rpre of event
The verified receipt is just the couple rpre+cig that is stored by event
digest edig
Escrowed items are indexed in database table keyed by prefix and
sn with duplicates given by different receipt triple inserted in insertion order.
This allows FIFO processing of escrows for events with same prefix and
sn but different digest.
Uses .db.ures.add(diger,prefixer,cigar)
Value is triple
Original Escrow steps:
self.db.dtss.put(keys=dgKey(pre, dig), val=Dater())
for cigar in cigars: # escrow each triple
if cigar.verfer.transferable: # skip transferable verfers
continue # skip invalid couplets
triple = dig.encode("utf-8") + cigar.verfer.qb64b + cigar.qb64b
self.db.ures.add(keys=(serder.pre, Number(num=serder.sn, code=NumDex.Huge).qb64), val=triple) # should be snKey
where:
dig is dig in receipt of receipted event
cigars is list of cigars instances for receipted event
pre is str qb64 of identifier prefix of receipted event
sn is int sequence number of receipted event
Steps:
Each pass (walk index table)
For each prefix,sn
For each escrow item dup at prefix,sn:
Get Event
compare dig so same event
verify sigs via cigars
If successful then remove from escrow table
"""
for (pre, sn), (rsaider, sprefixer, cigar) in self.db.ures.getTopItemIter():
sn = Seqner(qb64=sn).sn
try:
cigar.verfer = Verfer(qb64b=sprefixer.qb64b)
# check date if expired then remove escrow.
dater = self.db.dtss.get(keys=dgKey(pre, bytes(rsaider.qb64b)))
if dater is None: # no datetime stored
# no date time so raise ValidationError which unescrows below
msg = f"URE Missing escrowed event datetime at dig = {rsaider.qb64b}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# do date math here and discard if stale
dtnow = helping.nowUTC()
dte = dater.datetime
if (dtnow - dte) > datetime.timedelta(seconds=self.TimeoutURE):
# escrow stale so raise ValidationError which unescrows below
msg = f"URE Stale event escrow at dig = {rsaider.qb64b}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# Is receipt for unverified witnessed event in .Pwes escrow
# if found then try else clause will remove from escrow
found = self._processEscrowFindUnver(pre=pre,
sn=sn,
rsaider=rsaider,
cigar=cigar)
if not found: # no partial witness escrow of event found
# so process as escrow of receipt for accept event
# not two stage witnessed event escrow
# get dig of receipted accepted event in kel using lastEvt
# at pre and sn
dig = self.db.kels.getLast(keys=pre, on=sn)
if dig is None: # no receipted event so keep in escrow
msg = f"URE Missing receipted evt at pre={pre} sn={sn:x}"
logger.trace("Kevery unescrow error: %s", msg)
raise UnverifiedReceiptError(msg)
dig = dig.encode("utf-8")
# get receipted event using pre and edig
if (serder := self.db.evts.get(keys=(pre, dig))) is None:
# receipted event superseded so remove from escrow
msg = f"URE Invalid receipted event reference at pre={pre} sn={sn:x}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# compare digs
if rsaider.qb64b != serder.saidb:
msg = f"URE Bad escrowed receipt dig at pre={pre} sn={sn:x} receipter={sprefixer.qb64}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# verify sig verfer key is prefixer from triple
if not cigar.verfer.verify(cigar.raw, serder.raw):
# no sigs so raise ValidationError which unescrows below
msg = f"URE Bad escrowed receipt sig at pre={pre} sn={sn:x} receipter={sprefixer.qb64}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# Check if kever exists before accessing it
if serder.pre not in self.kevers:
# event exists in database but kever not ready yet, keep in escrow
msg = f"URE Kever not ready for receipted evt at pre={pre} sn={sn:x}"
logger.trace("Kevery unescrow error: %s", msg)
raise UnverifiedReceiptError(msg)
# get current wits from kever state assuming not stale
# receipt. Need function here to compute wits for actual
# state at pre, sn. XXXX
wits = self.kevers[serder.pre].wits
rpre = cigar.verfer.qb64 # prefix of receiptor
if rpre in wits: # its a witness receipt
# this only works for extra receipts that come in later
# after event is out of .Pwes escrow
index = wits.index(rpre)
# create witness indexed signature and write to db
wiger = Siger(raw=cigar.raw, index=index, verfer=cigar.verfer)
self.db.wigs.add(keys=(pre, serder.said), val=wiger)
else: # write receipt to database
self.db.rcts.add(keys=(pre, serder.said), val=(cigar.verfer, cigar))
except UnverifiedReceiptError as ex:
# still waiting on missing prior event to validate
# only happens if we process above
if logger.isEnabledFor(logging.TRACE): # adds exception data
logger.trace("Kevery: UNT other error on unescrow: %s\n", ex.args[0])
logger.exception("Kevery: UNT other error on unescrow: %s\n", ex.args[0])
except Exception as ex: # log diagnostics errors etc
# error other than out of order so remove from OO escrow
self.db.ures.rem(keys=(pre, Number(num=sn, code=NumDex.Huge).qb64), val=(rsaider, sprefixer, cigar)) # removes one escrow at key val
if logger.isEnabledFor(logging.DEBUG): # adds exception data
logger.exception("Kevery URE unescrowed: %s", ex.args[0])
else:
logger.error("Kevery URE unescrowed: %s", ex.args[0])
else: # unescrow succeeded, remove from escrow
# We don't remove all escrows at pre,sn because some might be
# duplicitous so we process remaining escrows in spite of found
# valid event escrow.
self.db.ures.rem(keys=(pre, Number(num=sn, code=NumDex.Huge).qb64), val=(rsaider, sprefixer, cigar)) # removes one escrow at key val
logger.info("Kevery URE unescrow succeeded for event pre=%s "
"sn=%s", pre, sn)
[docs]
def processEscrowDelegables(self):
"""
Process events escrowed by Kever that require delegation.
Escrowed items are indexed in database table keyed by prefix and
sn with duplicates given by different dig inserted in insertion order.
This allows FIFO processing of events with same prefix and sn but different
digest.
Uses .db.delegables.add(key, val) which is IOVal with dups.
Value is dgkey for event stored in .Evt where .Evt has serder.raw of event.
Steps:
Each pass (walk index table)
For each prefix,sn
For each escrow item dup at prefix,sn:
Get Event
Get and Attach Signatures
Process event as if it came in over the wire
If successful then remove from escrow table
"""
for (pre, sn), dig in self.db.delegables.getTopItemIter():
try:
edig = dig.encode("utf-8")
dgkey = dgKey(pre.encode("utf-8"), edig)
if not (esr := self.db.esrs.get(keys=dgkey)): # get event source, otherwise error
# no local source so raise ValidationError which unescrows below
msg = f"DEL Missing escrowed event source at dig = {edig.decode()}"
logger.info("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# check date if expired then remove escrow.
dater = self.db.dtss.get(keys=dgkey)
if dater is None: # no datetime stored
# no date time so raise ValidationError which unescrows below
msg = f"DEL Missing escrowed event datetime at dig = {edig.decode()}"
logger.info("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# do date math here and discard if stale
dtnow = helping.nowUTC()
dte = dater.datetime
if (dtnow - dte) > datetime.timedelta(seconds=self.TimeoutOOE):
# escrow stale so raise ValidationError which unescrows below
msg = f"DEL Stale event escrow at dig = {edig.decode()}"
logger.info("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# get the escrowed event using edig
if (eserder := self.db.evts.get(keys=(pre, bytes(edig)))) is None:
# no event so raise ValidationError which unescrows below
msg = f"DEL Missing escrowed evt at dig = {edig.decode()}"
logger.info("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# get sigs and attach
sigers = self.db.sigs.get(keys=(pre, edig))
if not sigers: # otherwise its a list of sigs
# no sigs so raise ValidationError which unescrows below
msg = f"DEL Missing escrowed evt sigs at dig = {edig.decode()}"
logger.info("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
sigers = self.db.sigs.get(keys=(pre, edig))
# get wigers
wigers = self.db.wigs.get(keys=(pre, bytes(edig)))
# parse the event if we have a delegate seal
if (duple := self.db.aess.get(keys=(pre.encode("utf-8"), edig))) is not None:
delsner, delsger = duple # Number from aess
# process event
self.processEvent(serder=eserder, sigers=sigers, wigers=wigers,
delsner=delsner, delsger=delsger, local=esr.local)
else:
raise MissingDelegableApprovalError("No delegation seal found for event.")
except MissingDelegableApprovalError as ex:
# still waiting on missing delegation approval
if logger.isEnabledFor(logging.DEBUG):
logger.exception("Kevery DEL unescrow failed: %s", ex.args[0])
except Exception as ex: # log diagnostics errors etc
# error other than out of order so remove from OO escrow
self.db.delegables.rem(keys=(pre, sn,), val=edig) # removes one escrow at key val
if logger.isEnabledFor(logging.DEBUG):
logger.exception("Kevery DEL other unescrow error: %s", ex.args[0])
else:
logger.error("Kevery DEL other unescrow error: %s", ex.args[0])
else: # unescrow succeeded, remove from escrow
# We don't remove all escrows at pre,sn because some might be
# duplicitous so we process remaining escrows in spite of found
# valid event escrow.
self.db.delegables.rem(keys=(pre, sn,), val=edig) # removes one escrow at key val
logger.info("Kevery DEL unescrow succeeded in valid event: "
"event=%s", eserder.said)
logger.debug(f"Event=\n%s\n", eserder.pretty())
[docs]
def processQueryNotFound(self):
"""
Process qry events escrowed by Kevery for KELs that have not yet met the criteria of the query.
A missing KEL or criteria for an event in a KEL at a particular sequence number or an event containing a
specific anchor can result in query not found escrowed events.
Escrowed items are indexed in database table keyed by prefix and
sn with duplicates given by different dig inserted in insertion order.
This allows FIFO processing of events with same prefix and sn but different
digest.
Uses .db.qnfs.add(key=(pre, said), val) which is IOVal with dups
Value is dgkey for event stored in .Evt where .Evt has serder.raw of event.
Steps:
Each pass (walk index table)
For each prefix,sn
For each escrow item dup at prefix,sn:
Get Event
Get and Attach Signatures
Process event as if it came in over the wire
If successful then remove from escrow table
"""
key = ekey = b'' # both start same. when not same means escrows found
pre = b''
sn = 0
while True: # break when done
for (pre, said), edig in self.db.qnfs.getTopItemIter(keys=key):
try:
# check date if expired then remove escrow.
dgkey = dgKey(pre.encode("utf-8"), edig.encode("utf-8"))
dater = self.db.dtss.get(keys=dgkey)
if dater is None: # no datetime stored
# no date time so raise ValidationError which unescrows below
msg = f"QNF Missing escrowed event datetime at dig = {edig}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# do date math here and discard if stale
dtnow = helping.nowUTC()
dte = dater.datetime
if (dtnow - dte) > datetime.timedelta(seconds=self.TimeoutQNF):
# escrow stale so raise ValidationError which unescrows below
msg = f"QNF Stale qry event escrow at dig = {edig}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# get the escrowed event using edig
if (eserder := self.db.evts.get(keys=(pre.encode("utf-8"), edig.encode("utf-8")))) is None:
# no event so raise ValidationError which unescrows below
msg = f"QNF Missing escrowed evt at dig = {edig}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# get sigs and attach
sigers = self.db.sigs.get(keys=(pre, edig))
if not sigers: # otherwise its a list of sigs
# no sigs so raise ValidationError which unescrows below
msg = f"QNF Missing escrowed evt sigs at dig = {edig}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# process event
sigers = self.db.sigs.get(keys=(pre, edig))
# ToDo XXXX get trans endorsements
# getVrcs
# get nontrans endorsements
cigars = []
for prefixer, cigar in self.db.rcts.getIter(keys=(pre.encode("utf-8"), edig.encode("utf-8"))):
cigars.append(cigar)
source = Prefixer(qb64b=pre)
self.processQuery(serder=eserder, source=source, sigers=sigers, cigars=cigars)
except QueryNotFoundError as ex:
# still waiting on missing prior event to validate
if logger.isEnabledFor(logging.TRACE):
logger.trace("Kevery: QNF unescrow failed: %s\n", ex.args[0])
logger.exception("Kevery: QNF unescrow failed: %s\n", ex.args[0])
except Exception as ex: # log diagnostics errors etc
# error other than out of order so remove from OO escrow
self.db.qnfs.rem(keys=(pre, said), val=edig) # removes one escrow at key val
if logger.isEnabledFor(logging.DEBUG):
logger.debug("Kevery: QNF other unescrow error: %s\n", ex.args[0])
logger.exception("Kevery: QNF other unescrow error: %s\n", ex.args[0])
else: # unescrow succeeded, remove from escrow
# We don't remove all escrows at pre,sn because some might be
# duplicitous so we process remaining escrows in spite of found
# valid event escrow.
self.db.qnfs.rem(keys=(pre, said), val=edig) # removes one escrow at key val
logger.info("Kevery: QNF unescrow succeeded in valid event: "
"key = %s \tdigest = %s", ekey.decode(), edig)
logger.debug("Event=\n%s\n", eserder.pretty())
if ekey == key: # still same so no escrows found on last while iteration
break
key = ekey # setup next while iteration, with key after ekey
def _processEscrowFindUnver(self, pre, sn, rsaider, wiger=None, cigar=None):
"""
ToDo XXXX Incomplete Placeholder
Support method called by other processEscowUnverXXX Receipt methods to
find escrowed serder in .Pwes for unverifiable receipt due to signed but
partially witnessed event
Returns:
found (bool): True means found matching event in .Pwes and added wig
to .Wigs. False means dig not find matching event in .Pwes
Raises:
Validation error if found matching event but signature does not verify
Parameters:
pre (Union[str,bytes]): pre of receipted event controller kel
sn (int): sequence number of receipted event
rsaider (Saider): derived from receipt's dig of receipted event to find
wiger (Siger): instance of witness indexed signature from receipt
cigar (Cigar): instance of witness nonindexed signature from receipt
"""
# lookup the database dig of the receipted event in pwes escrow using
# snKey(pre,sn) where pre is controller and sn is event sequence number
# compare dig to rdiger derived from receipt's dig of receipted event
found = False
for dig in self.db.pwes.getIter(keys=pre, on=sn): # search entries
dig = dig.encode("utf-8") # database dig of receipted event
# get the escrowed event using database dig in .Pwes
serder = self.db.evts.get(keys=(pre, dig)) # receipted event
# compare digs to ensure database dig and rdiger (receipt's dig) match
if rsaider.qb64b != dig:
continue # not match keep looking
# Extract or compute witness list
if serder.ked['t'] in (Ilks.icp, Ilks.dip): # inception get from event
wits = serder.ked['b'] # get wits from event itself
if len(oset(wits)) != len(wits):
raise ValidationError("Invalid wits = {}, has duplicates for evt = {}."
"".format(wits, serder.ked))
elif serder.ked['t'] in (Ilks.rot, Ilks.drt): # rotation compute from state
# calculate wits from rotation and kever key state.
if serder.pre not in self.kevers:
# kever not ready yet, can't process rotation witness receipts
return False # not found, caller should keep in escro
wits = self.kevers[serder.pre].wits # get wits from key state
cuts = serder.ked['br']
adds = serder.ked['ba']
witset = oset(wits)
cutset = oset(cuts)
addset = oset(adds)
if len(cutset) != len(cuts):
raise ValidationError("Invalid cuts={}, has duplicates "
"for evt={}.".format(cuts, serder.ked))
if (witset & cutset) != cutset: # some cuts not in wits
raise ValidationError("Invalid cuts={}, not all members "
"in wits for evt={}.".format(cuts, serder.ked))
if len(addset) != len(adds):
raise ValidationError("Invalid adds={}, has duplicates "
"for evt={}.".format(adds, serder.ked))
if cutset & addset: # non empty intersection
raise ValidationError("Intersecting cuts={} and adds={} "
"for evt={}.".format(cuts, adds, serder.ked))
if witset & addset: # non empty intersection
raise ValidationError("Intersecting wits={} and adds={} "
"for evt={}.".format(self.wits, adds, serder.ked))
wits = list((witset - cutset) | addset)
else: # interaction so get wits from kever key state
# would not be in this escrow if out of order event
wits = self.kevers[serder.pre].wits # get wits fromkey state
if cigar: # if recipter is a witness make wiger
rpre = cigar.verfer.qb64 # prefix of receiptor
if rpre in wits: # its a witness receipt
index = wits.index(rpre)
# create witness indexed signature wiger from cigar and wit index
wiger = Siger(raw=cigar.raw, index=index, verfer=cigar.verfer)
found = True
break # done with search have caller add wig.
elif wiger: # check index and assign verfier to wiger
if wiger.index >= len(wits): # bad index
# raise ValidationError which removes from escrow by caller
msg = f"PWE Bad escrowed witness receipt index={wiger.index} at pre={pre} sn={sn:x}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
wiger.verfer = Verfer(qb64=wits[wiger.index])
found = True
break # done with search have caller add wig.
if found: # verify signature and if verified write to .Wigs
if not wiger.verfer.verify(wiger.raw, serder.raw): # not verify
# raise ValidationError which unescrows .Uwes or .Ures in caller
msg = f"PWE Bad escrowed witness receipt wig at pre={pre} sn={sn:x}."
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
self.db.wigs.add(keys=(pre, serder.said), val=wiger)
# processEscrowPartialWigs removes from this .Pwes escrow
# when fully witnessed using self.db.pwes.remOn(pre, sn, dig)
return found
[docs]
def processEscrowUnverTrans(self):
"""
Process event receipts from transferable identifiers (validators)
escrowed by Kever that are unverified.
A transferable receipt is unverified if either the receipted event has not
been accepted into the receipted's KEL or the establishment event of the
receiptor has not been accepted into the receipter's KEL.
Without either event there is no way to know where to store the receipt
quadruples.
The escrow is a quintuple with dig+spre+ssnu+sdig+sig
the verified receipt is just the quadruple spre+ssnu+sdig+sig that is
stored by event dig
Escrowed items are indexed in database table keyed by prefix and
sn with duplicates given by different receipt quintuple inserted in insertion order.
This allows FIFO processing of escrows of events with same prefix and sn
but different digest.
Uses .db.vres.add(key, val) which is IOVal with dups.
Value is quintuple
Original Escrow steps:
self.db.dtss.put(keys=dgKey(serder.preb, dig), val=Dater())
prelet = (dig.encode("utf-8") + seal.i.encode("utf-8") +
Seqner(sn=int(seal.s, 16)).qb64b + seal.d.encode("utf-8"))
for siger in sigers: # escrow each quintlet
quintuple = prelet + siger.qb64b # quintuple
self.db.vres.add(keys=snKey(serder.preb, serder.sn), val=quintuple)
where:
dig is dig in receipt of receipted event
sigers is list of Siger instances for receipted event
Steps:
Each pass (walk index table)
For each prefix,sn
For each escrow item dup at prefix,sn:
Get Event
compare dig so same event
verify sigs via sigers
If successful then remove from escrow table
"""
ims = bytearray()
key = ekey = b'' # both start same. when not same means escrows found
while True: # break when done
for ekey, equinlet in self.db.vres.getTopItemIter(keys=key):
try:
pre, sn_hex = ekey # ekey is a tuple (pre, sn)
sn = int(sn_hex, 16)
esaider, sprefixer, snumber, ssaider, siger = equinlet
# check date if expired then remove escrow.
dater = self.db.dtss.get(keys=dgKey(pre, bytes(esaider.qb64b)))
if dater is None: # no datetime stored
# no date time so raise ValidationError which unescrows below
msg = f"VRE Missing escrowed event datetime at dig = {esaider.qb64b}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# do date math here and discard if stale
dtnow = helping.nowUTC()
dte = dater.datetime
if (dtnow - dte) > datetime.timedelta(seconds=self.TimeoutVRE):
# escrow stale so raise ValidationError which unescrows below
msg = f"VRE Stale event escrow at dig = {esaider.qb64b}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# get dig of the receipted event using pre and sn lastEvt
raw = self.db.kels.getLast(keys=pre, on=sn)
if raw is None:
# no event so keep in escrow
msg = f"VRE Missing receipted evt at pre={pre} sn={sn:x}"
logger.trace("Kevery unescrow error: %s", msg)
raise UnverifiedTransferableReceiptError(msg)
dig = raw.encode("utf-8")
# get receipted event using pre and edig
if (serder := self.db.evts.get(keys=(pre, dig))) is None: # receipted event superseded so remove from escrow
msg = f"VRE Invalid receipted evt reference at pre={pre} sn={sn:x}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# compare digs
if esaider.qb64b != serder.saidb:
msg = f"VRE Bad escrowed receipt dig at pre={pre} sn={sn:x} receipter={sprefixer.qb64}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# get receipter's last est event
# retrieve dig of last event at sn of receipter.
sdig = self.db.kels.getLast(keys=sprefixer.qb64b, on=snumber.sn)
if sdig is None:
# no event so keep in escrow
msg = f"VRE Missing receipted evt at pre={pre} sn={sn:x}"
logger.trace("Kevery unescrow error: %s", msg)
raise UnverifiedTransferableReceiptError(msg)
sdig = sdig.encode("utf-8")
# retrieve last event itself of receipter
sserder = self.db.evts.get(keys=(sprefixer.qb64b, bytes(sdig)))
# assumes db ensures that sserder must not be none because sdig was in KE
if not sserder.compare(said=ssaider.qb64): # seal dig not match event
# this unescrows
msg = f"VRE Bad chit seal at sn = {snumber.sn} for rct = {sserder.ked}"
logger.info("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# verify sigs and if so write quadruple to database
verfers = sserder.verfers
if not verfers:
msg = (f"VRE Invalid seal est. event dig = {ssaider.qb64} "
f"for receipt from pre = {sprefixer.qb64} no keys")
logger.info("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
if siger.index >= len(verfers):
msg = f"VRE Index = {siger.index} too large for keys"
logger.info("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
siger.verfer = verfers[siger.index] # assign verfer
if not siger.verfer.verify(siger.raw, serder.raw): # verify sig
msg = f"VRE Bad escrowed trans receipt sig at pre={pre} sn={sn:x} receipter={sprefixer.qb64}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# good sig so write receipt quadruple to database
quadruple = (sprefixer, snumber, ssaider, siger)
self.db.vrcs.add(keys=dgKey(pre, serder.said), val=quadruple)
except UnverifiedTransferableReceiptError as ex:
# still waiting on missing prior event to validate
# only happens if we process above
if logger.isEnabledFor(logging.TRACE): # adds exception data
logger.trace("Kevery: VRE escrow unescrow failed: %s\n", ex.args[0])
logger.exception("Kevery: VRE escrow unescrow failed: %s\n", ex.args[0])
except Exception as ex: # log diagnostics errors etc
# error other than out of order so remove from OO escrow
self.db.vres.rem(keys=snKey(pre, sn), val=equinlet) # removes one escrow at key val
if logger.isEnabledFor(logging.DEBUG): # adds exception data
logger.debug("Kevery: VRE other error on unescrow: %s\n", ex.args[0])
logger.exception("Kevery: VRE other error on unescrow: %s\n", ex.args[0])
else: # unescrow succeeded, remove from escrow
# We don't remove all escrows at pre,sn because some might be
# duplicitous so we process remaining escrows in spite of found
# valid event escrow.
self.db.vres.rem(keys=snKey(pre, sn), val=equinlet) # removes one escrow at key val
logger.info("Kevery VRE unescrow succeeded for event = %s", serder.said)
logger.debug("Event=\n%s\n", serder.pretty())
if ekey == key: # still same so no escrows found on last while iteration
break
key = ekey # setup next while iteration, with key after ekey
[docs]
def processEscrowDuplicitous(self):
"""
Process events escrowed by Kever that are likely duplicitous.
An event is likely duplicitous if a different version of event already
has been accepted into the KEL.
Escrowed items are indexed in database table keyed by prefix and
sn with duplicates given by different dig inserted in insertion order.
This allows FIFO processing of events with same prefix and sn but different
digest.
Uses .db.addLde(self, key, val) which is IOVal with dups.
Value is dgkey for event stored in .Evt where .Evt has serder.raw of event.
Original Escrow steps:
dgkey = dgKey(pre, serder.dig)
self.db.dtss.put(keys=dgkey, val=Dater())
self.db.sigs.put(keys=dgkey, vals=sigers)
self.db.evts.put(keys=(pre, serder.dig), val=serder)
self.db.addLde(snKey(pre, sn), serder.digb)
where:
serder is SerderKERI instance of event
sigers is list of Siger instance for event
pre is str qb64 of identifier prefix of event
sn is int sequence number of event
Steps:
Each pass (walk index table)
For each prefix,sn
For each escrow item dup at prefix,sn:
Get Event
Get and Attach Signatures
Process event as if it came in over the wire
If successful then remove from escrow table
"""
key = ekey = b'' # both start same. when not same means escrows found
while True: # break when done
for (pre,), sn, edig in self.db.ldes.getAllItemIter(keys=key):
try:
# pre and sn are already unpacked
ekey = snKey(pre, sn)
if hasattr(edig, "encode"):
edig = edig.encode("utf-8") # convert to bytes for legacy compatibility
dgkey = dgKey(pre, edig)
if not (esr := self.db.esrs.get(keys=dgkey)): # get event source, otherwise error
# no local source so raise ValidationError which unescrows below
msg = f"DUP Missing escrowed event source at dig = {edig.decode()}"
logger.info("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# check date if expired then remove escrow.
dater = self.db.dtss.get(keys=dgkey)
if dater is None: # no datetime stored
# no date time so raise ValidationError which unescrows below
msg = f"DUP Missing escrowed event datetime at dig = {edig.decode()}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# do date math here and discard if stale
dtnow = helping.nowUTC()
dte = dater.datetime
if (dtnow - dte) > datetime.timedelta(seconds=self.TimeoutLDE):
# escrow stale so raise ValidationError which unescrows below
msg = f"DUP Stale event escrow at dig = {edig.decode()}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# get the escrowed event using edig
if (eserder := self.db.evts.get(keys=(pre, bytes(edig)))) is None:
# no event so raise ValidationError which unescrows below
msg = f"DUP Missing escrowed evt at dig = {edig.decode()}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
# get sigs and attach
sigers = self.db.sigs.get(keys=(pre, edig))
if not sigers: # otherwise its a list of sigs
# no sigs so raise ValidationError which unescrows below
msg = f"DUP Missing escrowed evt sigs at dig = {edig.decode()}"
logger.trace("Kevery unescrow error: %s", msg)
raise ValidationError(msg)
sigers = self.db.sigs.get(keys=(pre, edig))
self.processEvent(serder=eserder, sigers=sigers, local=esr.local)
# If process does NOT validate event with sigs, becasue it is
# still out of order then process will attempt to re-escrow
# and then raise OutOfOrderError (subclass of ValidationError)
# so we can distinquish between ValidationErrors that are
# re-escrow vs non re-escrow. We want process to be idempotent
# with respect to processing events that result in escrow items.
# On re-escrow attempt by process, Ooe escrow is called by
# Kevery.self.escrowOOEvent Which calls
# self.db.ooes.addOn(pre, sn, serder.digb)
# which in turn will not enter dig as dup if one already exists.
# So re-escrow attempt will not change the escrowed ooe db.
# Non re-escrow ValidationError means some other issue so unescrow.
# No error at all means processed successfully so also unescrow.
except LikelyDuplicitousError as ex:
# still can't determine if duplicitous
if logger.isEnabledFor(logging.TRACE):
logger.trace("Kevery: DUP unescrow failed: %s\n", ex.args[0])
logger.exception("Kevery: DUP unescrow failed: %s\n", ex.args[0])
except Exception as ex: # log diagnostics errors etc
# error other than likely duplicitous so remove from escrow
self.db.ldes.rem(keys=pre, on=sn, val=edig) # removes one escrow at key val
if logger.isEnabledFor(logging.DEBUG):
logger.trace("Kevery: DUP other unescrow error: %s\n", ex.args[0])
logger.exception("Kevery: DUP other unescrow error: %s\n", ex.args[0])
else: # unescrow succeeded, remove from escrow
# We don't remove all escrows at pre,sn because some might be
# duplicitous so we process remaining escrows in spite of found
# valid event escrow.
self.db.ldes.rem(keys=pre, on=sn, val=edig) # removes one escrow at key val
logger.info("Kevery DUP unescrow succeeded in valid event: event=%s",
eserder.said)
logger.debug("event=\n%s\n", eserder.pretty())
if ekey == key: # still same so no escrows found on last while iteration
break
key = ekey # setup next while iteration, with key after ekey
[docs]
def duplicity(self, serder, sigers):
"""
PlaceHolder Reminder
Processes potential duplicitous events in PDELs
Handles duplicity detection and logging if duplicitous
Placeholder here for logic need to move
"""
pass
[docs]
def loadEvent(db, preb, dig):
""" Load event details from database
Args:
db (Baser): database to load event fro,
preb (bytes): qb64b identifier prefix
dig (bytes): digest of event to load
Returns:
dict: data from event
"""
event = dict()
dgkey = dgKey(preb, dig) # get message
if not (serder := db.evts.get(keys=(preb, dig))):
raise ValueError("Missing event for dig={}.".format(dig))
event["ked"] = serder.ked
sn = serder.sn
sdig = db.kels.getLast(keys=preb, on=sn)
if sdig is not None:
event["stored"] = True
# add indexed signatures to attachments
sigers = db.sigs.get(keys=dgkey)
dsigs = []
for siger in sigers:
dsigs.append(dict(index=siger.index, signature=siger.qb64))
event["signatures"] = dsigs
# add witness state at this event
wits = db.wits.get(dgkey) if serder.estive else []
event["witnesses"] = [wit.qb64 for wit in wits]
# add indexed witness signatures to attachments
dwigers = []
if wigers := db.wigs.get(keys=(preb, dig)):
for wiger in wigers:
dwigers.append(dict(index=wiger.index, signature=wiger.qb64))
event["witness_signatures"] = dwigers
# add authorizer (delegator/issuer) source seal event couple to attachments
if (duple := db.aess.get(keys=(preb, dig))) is not None:
number, diger = duple
event["source_seal"] = dict(sequence=number.sn, said=diger.qb64)
receipts = dict()
# add trans receipts quadruples
if quads := db.vrcs.get(keys=dgkey):
trans = []
for prefixer, number, diger, siger in quads:
trans.append(dict(
prefix=prefixer.qb64,
sequence=number.qb64,
said=diger.qb64,
signature=siger.qb64,
))
receipts["transferable"] = trans
# add nontrans receipts couples
if duple := db.rcts.get(keys=dgkey):
nontrans = []
for prefixer, cigar in duple:
nontrans.append(dict(prefix=prefixer.qb64, signature=cigar.qb64))
receipts["nontransferable"] = nontrans
event["receipts"] = receipts
# add first seen replay couple to attachments
if not (dater := db.dtss.get(keys=dgkey)):
raise ValueError("Missing datetime for dig={}.".format(dig))
event["timestamp"] = dater.dts
return event