Skip to content
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
'cbor2>=5.6.2',
'multidict>=6.0.5',
'ordered-set>=4.1.0',
'hio>=0.6.14',
'hio==0.6.14',
'multicommand>=1.0.0',
'jsonschema>=4.21.1',
'falcon>=3.1.3',
Expand Down
29 changes: 18 additions & 11 deletions src/keri/app/agenting.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,16 +297,23 @@ def __init__(self, hby, msgs=None, cues=None, force=False, auths=None, **kwa):

def receiptDo(self, tymth=None, tock=0.0, **kwa):
"""
Returns doifiable Doist compatible generator method (doer dog)

Usage:
add result of doify on this method to doers list

Generator that asynchronously processes witness receipt requests from self.msgs queue.

For each request in msgs (dict with 'pre' and optional 'sn'):
1. Creates HTTPMessenger or TCPMessenger for each witness
2. Sends delegator KEL (if delegated) to each witness for context
3. Sends full KEL to newly added witnesses (icp/dip or rotation adds)
4. Sends the target event to each witness
5. Waits for all witness receipts to arrive in hab.db (via MailboxDirector)
6. Propagates all collected receipts to each witness so they know about each other
7. Pushes the original request to self.cues to signal completion

The receipts themselves arrive asynchronously via the MailboxDirector which
polls the witness mailbox and parses incoming receipts into hab.db.

Parameters:
tymth is injected function wrapper closure returned by .tymen() of
Tymist instance. Calling tymth() returns associated Tymist .tyme.
tock is injected initial tock value

tymth: injected function wrapper closure from Tymist for timing
tock: injected initial tock value for yielding
"""
self.wind(tymth)
self.tock = tock
Expand Down Expand Up @@ -358,13 +365,13 @@ def receiptDo(self, tymth=None, tock=0.0, **kwa):
witer.msgs.append(bytearray(msg)) # make a copy
_ = (yield self.tock)

while True:
while True: # wait for all receipts to arrive
wigs = hab.db.getWigs(dgkey)
if len(wigs) == len(wits):
break
_ = yield self.tock

# If we started with all our recipts, exit unless told to force resubmit of all receipts
# If we started with all our receipts, exit unless told to force resubmit of all receipts
if completed and not self.force:
self.cues.push(evt)
continue
Expand Down
18 changes: 6 additions & 12 deletions src/keri/app/cli/commands/delegate/confirm.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,10 @@ def confirmDo(self, tymth, tock=0.0):
if ilk in (coring.Ilks.dip,):
typ = "inception"
delpre = eserder.sad["di"]

elif ilk in (coring.Ilks.drt,):
typ = "rotation"
dkever = self.hby.kevers[eserder.pre]
delpre = dkever.delpre

else:
continue

Expand Down Expand Up @@ -170,11 +168,11 @@ def confirmDo(self, tymth, tock=0.0):
saider = self.hby.db.cgms.get(keys=(prefixer.qb64, sner.qb64))
if saider is not None:
break

yield self.tock

print(f"Delegate {eserder.pre} {typ} event committed.")
print(f"Delegate {typ} event {eserder.pre} committed.")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Print statements in this file were reordered for readability.


self.hby.db.delegables.rem(keys=(pre, sn), val=edig)
self.remove(self.toRemove)
return True

Expand Down Expand Up @@ -211,27 +209,23 @@ def confirmDo(self, tymth, tock=0.0):
while not witDoer.cues:
_ = yield self.tock

print(f'Delegagtor Prefix {hab.pre}')
print(f'\tDelegate {eserder.pre} {typ} Anchored at Seq. No. {hab.kever.sner.num}')
print(f'Delegagtor Prefix {hab.pre}')
print(f'\tDelegate {typ} event {eserder.pre} Anchored at Seq. No. {hab.kever.sner.num}')

# wait for confirmation of fully commited event
if eserder.pre in self.hby.kevers:
self.witq.query(src=hab.pre, pre=eserder.pre, sn=eserder.sn)

while eserder.sn < self.hby.kevers[eserder.pre].sn:
yield self.tock

print(f"Delegate {eserder.pre} {typ} event committed.")
else: # It should be an inception event then...
wits = [werfer.qb64 for werfer in eserder.berfers]
self.witq.query(src=hab.pre, pre=eserder.pre, sn=eserder.sn, wits=wits)

while eserder.pre not in self.hby.kevers:
yield self.tock

print(f"Delegate {eserder.pre} {typ} event committed.")
print(f"Delegate {typ} event {eserder.pre} committed.")

self.hby.db.delegables.rem(keys=(pre, sn))
self.hby.db.delegables.rem(keys=(pre, sn), val=edig)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .rem() logic was changed here to mirror what is found in the escrow logic in core/eventing.py

self.remove(self.toRemove)
return True

Expand Down
2 changes: 1 addition & 1 deletion src/keri/app/cli/commands/multisig/join.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ def incept(self, attrs):

inits["toad"] = oicp.ked["bt"]
inits["wits"] = oicp.ked["b"]
inits["delpre"] = oicp.ked["di"] if "di" in ked else None
inits["delpre"] = oicp.ked["di"] if "di" in oicp.ked else None
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a bug that prevented multisig join operations from working for delegated inception and rotation, and probably for interaction as well.`


print()
print("Group Multisig Inception proposed:")
Expand Down
2 changes: 1 addition & 1 deletion src/keri/app/habbing.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def openHab(name="test", base="", salt=None, temp=True, cf=None, **kwa):

with openHby(name=name, base=base, salt=salt, temp=temp, cf=cf) as hby:
if (hab := hby.habByName(name)) is None:
hab = hby.makeHab(name=name, icount=1, isith='1', ncount=1, nsith='1', **kwa)
hab = hby.makeHab(name=name, icount=1, isith='1', ncount=1, nsith='1', cf=cf, **kwa)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small config bugfix to make so temp=True works correctly in tests.


yield hby, hab

Expand Down
117 changes: 96 additions & 21 deletions src/keri/core/eventing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1682,27 +1682,58 @@ def locallyOwned(self, pre: str | None = None):


def locallyDelegated(self, pre: str):
"""Returns True if pre is in .prefixes and not in .groups
False otherwise. Use when pre is a delegator for some event and
want to confirm that pre is also locallyOwned thereby making the
associated event locallyDelegated.
"""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 but is not a group with local member.
i.e pre is a locally owned (controlled) AID (identifier prefix)
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 but not group hab
(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 self.locallyOwned(pre=pre)
return pre in self.prefixes
Copy link
Contributor Author

@kentbull kentbull Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is part of the big delegation fix. self.locallyOwned(...) was too strict of a check and excluded multisig events.

pre in self.prefixes works because self.prefixes includes multisig identifiers.



def locallyWitnessed(self, *, wits: list[str]=None, serder: (str)=None):
Expand Down Expand Up @@ -2388,8 +2419,7 @@ def valSigsWigsDel(self, serder, sigers, verfers, tholder,
# seal in this case can't be malicious since sourced locally.
# Doesn't get to here until fully signed and witnessed.

if self.locallyDelegated(delpre) and not self.locallyOwned(): # local delegator
# must be local if locallyDelegated or caught above as misfit
if serder.ilk in (Ilks.dip, Ilks.drt) and self.locallyDelegated(delpre) and not self.locallyOwned(): # local delegator of delegated event
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the big fix to kli delegate confirm that solves #1087. The reason this solves #1087 is because interaction events would incorrectly pass through this block to be escrowed before the type guard if serder.ilk in (Ilks.dip, Ilks.drt) was added.

Only delegated inception and delegated rotation should be going to the delegables escrow.

if delseqner is None or delsaider is None: # missing delegation seal
# so escrow delegable. So local delegator can approve OOB.
# and create delegator event with valid event seal of this
Expand Down Expand Up @@ -5408,6 +5438,7 @@ def processEscrows(self):
self.processEscrowPartialWigs()
self.processEscrowPartialSigs()
self.processEscrowDuplicitous()
self.processEscrowDelegables()
self.processQueryNotFound()

except Exception as ex: # log diagnostics errors etc
Expand Down Expand Up @@ -6398,17 +6429,9 @@ def processEscrowDelegables(self):
wigers = [Siger(qb64b=bytes(wig)) for wig in wigs]

# get delgate seal
couple = self.db.getAes(dgkey)
if couple is not None: # Only try to parse the event if we have the del seal
raw = bytearray(couple)
seqner = coring.Seqner(qb64b=raw, strip=True)
saider = coring.Saider(qb64b=raw)

# process event
self.processEvent(serder=eserder, sigers=sigers, wigers=wigers, delseqner=seqner,
delsaider=saider, local=esr.local)
else:
raise MissingDelegableApprovalError("No delegation seal found for event.")
seqner, saider = self._getDelegationSeal(eserder=eserder, dgkey=dgkey)
self.processEvent(serder=eserder, sigers=sigers, wigers=wigers, delseqner=seqner,
delsaider=saider, local=esr.local)

except MissingDelegableApprovalError as ex:
# still waiting on missing delegation approval
Expand All @@ -6432,6 +6455,58 @@ def processEscrowDelegables(self):
"event=%s", eserder.said)
logger.debug("event=\n%s\n", eserder.pretty())

def _getDelegationSeal(self, eserder: serdering.SerderKERI, dgkey: bytes) -> tuple[
coring.Seqner, coring.Saider]:
"""
Get sequence number (delseqner) and event digest (delsaider)
for delegated inception (dip) or rotation (drt) event from AES seal database or KEL state.

Parameters:
eserder: SerderKERI instance of the delegated event
dgkey: bytes of the digest key of the delegated event
Returns:
(Seqner, Saider): sequence number and event digest
Raises:
MissingDelegableApprovalError: if the delegation seal is not found
"""
# get delgate seal
couple = self.db.getAes(dgkey)
if couple is not None: # Only try to parse the event if we have the del seal
raw = bytearray(couple)
seqner = coring.Seqner(qb64b=raw, strip=True)
saider = coring.Saider(qb64b=raw)
return seqner, saider
else:
# Check KEL for seal (like in valSigsWigsDel)
# This handles the case where delegation was approved
# and the seal is in the delegator's KEL
if eserder.ilk in (Ilks.dip, Ilks.drt):
# Get delpre (delegator prefix) from dip or drt
if eserder.ilk == Ilks.dip:
delpre = eserder.delpre # delegator from dip event
if not delpre:
raise MissingDelegableApprovalError(
f"Empty or missing delegator for delegated inception event = {eserder.said}.")
else: # For drt, delpre is in kever state
# If we have the kever, use it; otherwise we can't process
if eserder.pre in self.kevers:
delpre = self.kevers[eserder.pre].delpre
else:
raise MissingDelegableApprovalError(
f"No kever found for delegated rotation event = {eserder.said}.")

# Look up seal in delegator's KEL
seal = dict(i=eserder.pre, s=eserder.snh, d=eserder.said)
dserder = self.db.fetchLastSealingEventByEventSeal(pre=delpre, seal=seal)
if dserder is not None: # found seal - use it
seqner = coring.Seqner(sn=dserder.sn)
saider = coring.Saider(qb64=dserder.said)
return seqner, saider
else:
raise MissingDelegableApprovalError("No delegation seal found for event.")
else:
raise MissingDelegableApprovalError("No delegation seal found for event.")

def processQueryNotFound(self):
"""
Process qry events escrowed by Kevery for KELs that have not yet met the criteria of the query.
Expand Down
Loading
Loading