Skip to content

Commit bea42cf

Browse files
fix: resolve missing AES on delegate's witness after delegation
Witnesses accept delegated events (dip/drt) before the delegator has anchored them, to avoid a deadlock in the delegation flow. However, the authorizer event seal (AES) was never back-filled once the delegator's anchoring event arrived, leaving the witness permanently showing "Not anchored" for the delegated identifier. Introduce a pending witness delegation escrow (pwde) that tracks delegated events accepted by a witness without an AES. A new processEscrowWitnessAnchors method, called from processEscrows, periodically checks whether the delegator's KEL now contains the anchoring seal and stores the AES when it does. Fixes #1317
1 parent cbbf700 commit bea42cf

3 files changed

Lines changed: 235 additions & 4 deletions

File tree

src/keri/core/eventing.py

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3151,10 +3151,9 @@ def logEvent(self, serder, sigers=None, wigers=None, wits=None, first=False,
31513151
and not self.locallyWitnessed(wits=wits) and seqner and saider):
31523152
couple = seqner.qb64b + saider.qb64b
31533153
self.db.setAes(dgkey, couple) # authorizer (delegator/issuer) event seal
3154-
3155-
#if seqner and saider:
3156-
#couple = seqner.qb64b + saider.qb64b
3157-
#self.db.setAes(dgkey, couple) # authorizer (delegator/issuer) event seal
3154+
elif (self.delpre and not serder.ilk == Ilks.ixn
3155+
and self.locallyWitnessed(wits=wits)):
3156+
self.db.pwde.addOn(keys=serder.pre, on=serder.sn, val=serder.said)
31583157

31593158
if esr := self.db.esrs.get(keys=dgkeys): # preexisting esr
31603159
if local and not esr.local: # local overwrites prexisting remote
@@ -5391,6 +5390,54 @@ def escrowTRQuadruple(self, serder, sprefixer, sseqner, saider, siger):
53915390
"receipt of pre= %s sn=%x dig=%s", serder.pre, serder.sn,
53925391
serder.said)
53935392

5393+
def processEscrowWitnessAnchors(self):
5394+
"""Process escrowed delegated events accepted by a witness that are
5395+
still missing their authorizer event seal (AES).
5396+
5397+
Witnesses accept delegated events without requiring the delegation
5398+
anchor (validateDelegation short-circuits for locallyWitnessed) so
5399+
they can issue receipts and let the delegation flow proceed. The AES
5400+
cannot be stored at acceptance time because the delegator has not yet
5401+
anchored the event.
5402+
5403+
db.pwde tracks these events. Once the delegator's KEL arrives
5404+
(containing the anchoring seal), this method verifies it and stores
5405+
the AES, then removes the entry from the escrow.
5406+
"""
5407+
for (epre,), esn, edig in self.db.pwde.getOnItemIter(keys=b''):
5408+
try:
5409+
dgkey = dgKey(epre, edig)
5410+
5411+
eraw = self.db.getEvt(dgkey)
5412+
if eraw is None:
5413+
raise ValidationError(f"PWDE missing event at dig={bytes(edig)}")
5414+
5415+
eserder = serdering.SerderKERI(raw=bytes(eraw))
5416+
delpre = eserder.ked.get('di')
5417+
if not delpre or delpre not in self.kevers:
5418+
continue
5419+
5420+
seal = dict(i=eserder.pre, s=eserder.snh, d=eserder.said)
5421+
dserder = self.db.fetchLastSealingEventByEventSeal(
5422+
delpre, seal=seal)
5423+
if dserder is None:
5424+
continue
5425+
5426+
seqner = coring.Seqner(sn=dserder.sn)
5427+
couple = seqner.qb64b + dserder.saidb
5428+
self.db.setAes(dgkey, couple)
5429+
5430+
except Exception as ex:
5431+
self.db.pwde.remOn(keys=epre, on=esn, val=edig)
5432+
if logger.isEnabledFor(logging.DEBUG):
5433+
logger.exception("Kevery PWDE unescrowed: %s", ex.args[0])
5434+
else:
5435+
logger.error("Kevery PWDE unescrowed: %s", ex.args[0])
5436+
else:
5437+
self.db.pwde.remOn(keys=epre, on=esn, val=edig)
5438+
logger.info("Kevery PWDE resolved delegation anchor for %s",
5439+
eserder.pre)
5440+
53945441
def processEscrows(self):
53955442
"""
53965443
Iterate throush escrows and process any that may now be finalized
@@ -5404,6 +5451,7 @@ def processEscrows(self):
54045451
self.processEscrowUnverNonTrans()
54055452
self.processEscrowUnverTrans()
54065453
self.processEscrowPartialDels()
5454+
self.processEscrowWitnessAnchors()
54075455
self.processEscrowPartialWigs()
54085456
self.processEscrowPartialSigs()
54095457
self.processEscrowDuplicitous()

src/keri/db/basing.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,6 +1023,7 @@ def reopen(self, **kwa):
10231023
self.pdes = subing.OnIoDupSuber(db=self, subkey='pdes.')
10241024
self.udes = subing.CatCesrSuber(db=self, subkey='udes.',
10251025
klas=(coring.Seqner, coring.Saider))
1026+
self.pwde = subing.OnIoDupSuber(db=self, subkey='pwde.')
10261027
self.uwes = self.env.open_db(key=b'uwes.', dupsort=True)
10271028
self.ooes = self.env.open_db(key=b'ooes.', dupsort=True)
10281029
self.dels = self.env.open_db(key=b'dels.', dupsort=True)

tests/core/test_escrow.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,188 @@ def test_missing_delegator_escrow():
749749
"""End Test"""
750750

751751

752+
def test_witness_anchor_escrow():
753+
"""
754+
Test pending witness delegation anchor escrow (pwde).
755+
756+
bob is the delegator
757+
del is the delegate
758+
wit is the delegate's witness
759+
760+
When a witness accepts a delegated inception event, validateDelegation
761+
short-circuits (returns (None, None)) for locallyWitnessed events to
762+
avoid a deadlock: the delegate needs witness receipts, but a strict
763+
witness would need the delegation anchor first. Instead, logEvent
764+
tracks the un-anchored delegated event in db.pwde.
765+
766+
Once the delegator's KEL arrives (containing the anchoring ixn with a
767+
SealEvent for the delegate's dip), processEscrowWitnessAnchors finds
768+
the seal via fetchLastSealingEventByEventSeal and stores the AES.
769+
"""
770+
771+
bobSalt = core.Salter(raw=b'0123456789abcdef').qb64
772+
delSalt = core.Salter(raw=b'abcdef0123456789').qb64
773+
witSalt = core.Salter(raw=b'wxyzabcdefghijkl').qb64
774+
775+
psr = parsing.Parser()
776+
777+
with (basing.openDB(name="bob") as bobDB,
778+
keeping.openKS(name="bob") as bobKS,
779+
basing.openDB(name="del") as delDB,
780+
keeping.openKS(name="del") as delKS,
781+
basing.openDB(name="wit") as witDB,
782+
keeping.openKS(name="wit") as witKS):
783+
784+
bobMgr = keeping.Manager(ks=bobKS, salt=bobSalt)
785+
delMgr = keeping.Manager(ks=delKS, salt=delSalt)
786+
witMgr = keeping.Manager(ks=witKS, salt=witSalt)
787+
788+
bobKvy = eventing.Kevery(db=bobDB)
789+
delKvy = eventing.Kevery(db=delDB)
790+
witKvy = eventing.Kevery(db=witDB)
791+
792+
# --- Setup Wit as a non-transferable identifier (required for witnesses) ---
793+
verfers, digers = witMgr.incept(stem='wit', ncount=0,
794+
transferable=False, temp=True)
795+
witSrdr = eventing.incept(keys=[verfer.qb64 for verfer in verfers])
796+
797+
witPre = witSrdr.pre
798+
witMgr.move(old=verfers[0].qb64, new=witPre)
799+
witDB.prefixes.add(witPre)
800+
assert witPre in witDB.prefixes
801+
802+
sigers = witMgr.sign(ser=witSrdr.raw, verfers=verfers)
803+
msg = bytearray(witSrdr.raw)
804+
counter = core.Counter(core.Codens.ControllerIdxSigs,
805+
count=len(sigers), gvrsn=kering.Vrsn_1_0)
806+
msg.extend(counter.qb64b)
807+
for siger in sigers:
808+
msg.extend(siger.qb64b)
809+
witIcpMsg = msg
810+
811+
psr.parse(ims=bytearray(witIcpMsg), kvy=witKvy, local=True)
812+
witK = witKvy.kevers[witPre]
813+
assert witK.prefixer.qb64 == witPre
814+
assert witK.serder.said == witSrdr.said
815+
816+
# --- Setup Bob (delegator) with own inception event ---
817+
verfers, digers = bobMgr.incept(stem='bob', temp=True)
818+
bobSrdr = eventing.incept(keys=[verfer.qb64 for verfer in verfers],
819+
ndigs=[diger.qb64 for diger in digers],
820+
code=coring.MtrDex.Blake3_256)
821+
822+
bobPre = bobSrdr.pre
823+
bobMgr.move(old=verfers[0].qb64, new=bobPre)
824+
bobDB.prefixes.add(bobPre)
825+
assert bobPre in bobDB.prefixes
826+
827+
sigers = bobMgr.sign(ser=bobSrdr.raw, verfers=verfers)
828+
msg = bytearray(bobSrdr.raw)
829+
counter = core.Counter(core.Codens.ControllerIdxSigs,
830+
count=len(sigers), gvrsn=kering.Vrsn_1_0)
831+
msg.extend(counter.qb64b)
832+
for siger in sigers:
833+
msg.extend(siger.qb64b)
834+
bobIcpMsg = msg
835+
836+
psr.parse(ims=bytearray(bobIcpMsg), kvy=bobKvy, local=True)
837+
bobK = bobKvy.kevers[bobPre]
838+
assert bobK.prefixer.qb64 == bobPre
839+
assert bobK.serder.said == bobSrdr.said
840+
assert bobK.sn == 0
841+
842+
psr.parse(ims=bytearray(bobIcpMsg), kvy=delKvy, local=True)
843+
assert bobPre in delKvy.kevers
844+
845+
# --- Create Del's delegated inception event ---
846+
verfers, digers = delMgr.incept(stem='del', temp=True)
847+
delSrdr = eventing.delcept(keys=[verfer.qb64 for verfer in verfers],
848+
delpre=bobPre,
849+
ndigs=[diger.qb64 for diger in digers],
850+
wits=[witPre],
851+
toad=1)
852+
853+
delPre = delSrdr.pre
854+
delMgr.move(old=verfers[0].qb64, new=delPre)
855+
856+
sigers = delMgr.sign(ser=delSrdr.raw, verfers=verfers)
857+
msg = bytearray(delSrdr.raw)
858+
counter = core.Counter(core.Codens.ControllerIdxSigs,
859+
count=len(sigers), gvrsn=kering.Vrsn_1_0)
860+
msg.extend(counter.qb64b)
861+
for siger in sigers:
862+
msg.extend(siger.qb64b)
863+
delIcpMsg = msg
864+
865+
# --- Witness receives dip (no anchor exists yet) ---
866+
psr.parse(ims=bytearray(delIcpMsg), kvy=witKvy, local=True)
867+
868+
assert delPre in witKvy.kevers
869+
witDelK = witKvy.kevers[delPre]
870+
assert witDelK.delegated
871+
assert witDelK.serder.said == delSrdr.said
872+
873+
# AES not set — witness accepted without delegation anchor
874+
assert witKvy.db.getAes(dbing.dgKey(delPre, delSrdr.said)) is None
875+
876+
# Event tracked in pwde escrow
877+
escrows = witKvy.db.pwde.getOn(keys=delPre, on=delSrdr.sn)
878+
assert len(escrows) == 1
879+
assert escrows[0] == delSrdr.said
880+
881+
# --- Escrow processing: delegator KEL unknown, nothing resolves ---
882+
witKvy.processEscrowWitnessAnchors()
883+
assert witKvy.db.getAes(dbing.dgKey(delPre, delSrdr.said)) is None
884+
assert len(witKvy.db.pwde.getOn(keys=delPre, on=delSrdr.sn)) == 1
885+
886+
# --- Delegator creates ixn with seal (after witness accepted dip) ---
887+
seal = eventing.SealEvent(i=delPre,
888+
s=delSrdr.ked["s"],
889+
d=delSrdr.said)
890+
bobIxnSrdr = eventing.interact(pre=bobK.prefixer.qb64,
891+
dig=bobK.serder.said,
892+
sn=bobK.sn + 1,
893+
data=[seal._asdict()])
894+
895+
sigers = bobMgr.sign(ser=bobIxnSrdr.raw, verfers=bobK.verfers)
896+
msg = bytearray(bobIxnSrdr.raw)
897+
counter = core.Counter(core.Codens.ControllerIdxSigs,
898+
count=len(sigers), gvrsn=kering.Vrsn_1_0)
899+
msg.extend(counter.qb64b)
900+
for siger in sigers:
901+
msg.extend(siger.qb64b)
902+
bobIxnMsg = msg
903+
904+
psr.parse(ims=bytearray(bobIxnMsg), kvy=bobKvy, local=True)
905+
assert bobK.sn == 1
906+
907+
# --- Deliver Bob's full KEL (icp + ixn) to witness ---
908+
psr.parse(ims=bytearray(bobIcpMsg), kvy=witKvy, local=False)
909+
assert bobPre in witKvy.kevers
910+
911+
psr.parse(ims=bytearray(bobIxnMsg), kvy=witKvy, local=False)
912+
assert witKvy.kevers[bobPre].sn == 1
913+
914+
# --- processEscrowWitnessAnchors resolves the AES ---
915+
witKvy.processEscrowWitnessAnchors()
916+
917+
seqner = coring.Seqner(sn=bobIxnSrdr.sn)
918+
couple = witKvy.db.getAes(dbing.dgKey(delPre, delSrdr.said))
919+
assert couple == seqner.qb64b + bobIxnSrdr.saidb
920+
921+
# pwde escrow cleared
922+
assert len(witKvy.db.pwde.getOn(keys=delPre, on=delSrdr.sn)) == 0
923+
924+
assert not os.path.exists(witKS.path)
925+
assert not os.path.exists(witDB.path)
926+
assert not os.path.exists(delKS.path)
927+
assert not os.path.exists(delDB.path)
928+
assert not os.path.exists(bobKS.path)
929+
assert not os.path.exists(bobDB.path)
930+
931+
"""End Test"""
932+
933+
752934
def test_misfit_escrow():
753935
"""
754936
Test misfit escrow

0 commit comments

Comments
 (0)