diff --git a/src/keri/peer/exchanging.py b/src/keri/peer/exchanging.py index f56e67f77..154cbe35b 100644 --- a/src/keri/peer/exchanging.py +++ b/src/keri/peer/exchanging.py @@ -150,10 +150,18 @@ def processEvent(self, serder, tsgs=None, cigars=None, **kwargs): logger.info(f"Behavior for {route} missing or does not have verify for said={serder.said}") logger.debug(f"event=\n{serder.pretty()}\n") + # A re-parse of an exn we already handled (endpoint retry, escrow release, mailbox + # replay, another group member's copy) must not run the behavior again: notices are + # keyed by a random rid, so handling twice notifies twice. + handled = self.hby.db.exns.get(keys=(serder.said,)) is not None + # Always persist events self.logEvent(serder, pathed, tsgs, cigars, essrs) self.cues.append(dict(kin="saved", said=serder.said)) + if handled: + return + # Execute any behavior specific handling, not sure if this should be different than verify try: behavior.handle(serder=serder, **kwargs) diff --git a/src/keri/vdr/eventing.py b/src/keri/vdr/eventing.py index afa0941e7..0c77cf101 100644 --- a/src/keri/vdr/eventing.py +++ b/src/keri/vdr/eventing.py @@ -1579,6 +1579,10 @@ def processEvent(self, serder, seqner=None, saider=None, wigers=None): if ilk in (Ilks.vcp,): # we don't have multiple signatures to verify so this # is already first seen and then lifely duplicitious + dig = self.reger.getTel(snKey(pre=regk, sn=0)) + if dig is not None and bytes(dig) == serder.saidb: + return # event is a duplicate but not duplicitous + raise LikelyDuplicitousError("Likely Duplicitous event={}.".format(ked)) tever = self.tevers[regk] @@ -1604,7 +1608,11 @@ def processEvent(self, serder, seqner=None, saider=None, wigers=None): # actually, lets not because the Kevery has no idea what to do with them! # self.cues.append(dict(kin="receipt", serder=serder)) pass - else: # duplicitious + else: # maybe duplicitous + dig = self.reger.getTel(snKey(pre=pre, sn=sn)) + if dig is not None and bytes(dig) == serder.saidb: + return # event is a duplicate but not duplicitous + raise LikelyDuplicitousError("Likely Duplicitous event={} with sn {}.".format(ked, sn)) def processQuery(self, serder, source=None, sigers=None, cigars=None): diff --git a/tests/app/test_agenting.py b/tests/app/test_agenting.py index 6f2470369..2af62bf52 100644 --- a/tests/app/test_agenting.py +++ b/tests/app/test_agenting.py @@ -8,12 +8,9 @@ from hio.base import doing, tyming from keri import kering, core -from keri.core import coring, serdering -from keri.core.coring import Seqner -from keri.help import nowIso8601 from keri.app import habbing, indirecting, agenting, directing from keri.db import dbing -from keri.vdr import eventing, viring +from keri.help import nowIso8601 def test_withness_receiptor(seeder): @@ -131,12 +128,12 @@ def __init__(self, wanHby, wilHby, wesHby, palHby, seeder): wilDoers = indirecting.setupWitness(alias="wil", hby=wilHby, tcpPort=5633, httpPort=5643) wesDoers = indirecting.setupWitness(alias="wes", hby=wesHby, tcpPort=5634, httpPort=5644) - wanHab = wanHby.habByName(name="wan") - wilHab = wilHby.habByName(name="wil") - wesHab = wesHby.habByName(name="wes") - seeder.seedWitEnds(palHby.db, witHabs=[wanHab, wilHab, wesHab], protocols=[kering.Schemes.tcp]) + self.wanHab = wanHby.habByName(name="wan") + self.wilHab = wilHby.habByName(name="wil") + self.wesHab = wesHby.habByName(name="wes") + seeder.seedWitEnds(palHby.db, witHabs=[self.wanHab, self.wilHab, self.wesHab], protocols=[kering.Schemes.tcp]) - self.palHab = palHby.makeHab(name="pal", wits=[wanHab.pre, wilHab.pre, wesHab.pre], transferable=True) + self.palHab = palHby.makeHab(name="pal", wits=[self.wanHab.pre, self.wilHab.pre, self.wesHab.pre], transferable=True) self.witDoer = agenting.WitnessPublisher(hby=palHby) doers = wanDoers + wilDoers + wesDoers + [self.witDoer] @@ -151,32 +148,13 @@ def testDo(self, tymth, tock=0.0): self.tock = tock yield self.tock - regser = eventing.incept(pre=self.palHab.pre, baks=[], code=coring.MtrDex.Blake3_256) - serder = eventing.issue(vcdig=regser.pre, - regk="EbA1o_bItVC9i6YB3hr2C3I_Gtqvz02vCmavJNoBA3Jg") - msg = bytearray(serder.raw) - msg.extend(core.Counter(core.Codens.SealSourceCouples, count=1, - gvrsn=kering.Vrsn_1_0).qb64b) - msg.extend(Seqner(sn=self.palHab.kever.sn).qb64b) - msg.extend(self.palHab.kever.serder.saidb) - + msg = self.palHab.makeOwnEvent(sn=0) self.witDoer.msgs.append(dict(pre=self.palHab.pre, msg=msg)) - while not self.witDoer.cues: - yield self.tock - - cue = self.witDoer.cues.popleft() - assert cue["pre"] == self.palHab.pre - assert cue["msg"] == msg - - for name in ["wes", "wil", "wan"]: - reger = viring.Reger(name=name) + for hab in [self.wanHab, self.wilHab, self.wesHab]: while True: - raw = reger.getTvt(dbing.dgKey(serder.preb, serder.saidb)) - if raw: - found = serdering.SerderKERI(raw=bytes(raw)) - if found and serder.pre == found.pre: - break + if self.palHab.pre in hab.kevers: + break yield self.tock self.remove(self.toRemove) diff --git a/tests/peer/test_exchanging.py b/tests/peer/test_exchanging.py index e98d03af9..876d450fb 100644 --- a/tests/peer/test_exchanging.py +++ b/tests/peer/test_exchanging.py @@ -134,6 +134,38 @@ def test_exchanger(): assert recHby.db.epse.get(keys=(fwd.said,)) is None +def test_exchanger_replay_does_not_rehandle(): + with habbing.openHby(salt=core.Salter(raw=b'0123456789abcdef').qb64) as hby: + hab = hby.makeHab(name="test") + + class CountingHandler: + resource = "/counting" + + def __init__(self): + self.count = 0 + + def verify(self, serder, **kwargs): + return True + + def handle(self, serder, **kwargs): + self.count += 1 + + handler = CountingHandler() + exc = exchanging.Exchanger(hby=hby, handlers=[handler]) + + msg = hab.exchange(route="/counting", recipient="", payload=dict(m="hello")) + said = serdering.SerderKERI(raw=msg).said + + hby.psr.parseOne(ims=bytearray(msg), exc=exc) + assert handler.count == 1 + assert hby.db.exns.get(keys=(said,)) is not None + + # A re-parse still logs, so signatures from other members merge, but the behavior + # must not run again or the controller is notified twice. + hby.psr.parseOne(ims=bytearray(msg), exc=exc) + assert handler.count == 1 + + def test_exchange_ps_escrow_timeout(): with habbing.openHab(name="sid", base="test", salt=b'0123456789abcdef') as (hby, hab), \ habbing.openHab(name="rec", base="test", salt=b'0123456789abcdef') as (recHby, recHab): diff --git a/tests/vdr/test_eventing.py b/tests/vdr/test_eventing.py index 7a0f4618f..8eb8f4d0e 100644 --- a/tests/vdr/test_eventing.py +++ b/tests/vdr/test_eventing.py @@ -633,9 +633,9 @@ def test_tevery(): assert tev.prefixer.qb64 == vcp.pre assert tev.sn == 0 - # send vcp again, get error - with pytest.raises(LikelyDuplicitousError): - tvy.processEvent(serder=vcp, seqner=seqner, saider=saider) + # send vcp again, duplicate but not duplicitous + tvy.processEvent(serder=vcp, seqner=seqner, saider=saider) + assert tev.sn == 0 # process issue vc event vcdig = b'EEBp64Aw2rsjdJpAR0e2qCq3jX7q7gLld3LjAwZgaLXU' @@ -671,6 +671,20 @@ def test_tevery(): assert status.et == Ilks.rev assert status.s == '1' + # send rev again, state unchanged + tvy.processEvent(serder=rev, seqner=seqner, saider=saider) + status = tev.vcState(vcdig.decode("utf-8")) + assert status.et == Ilks.rev + assert status.s == '1' + assert status.d == rev.said + + # a different rev at the same sn is still duplicitous + other = eventing.revoke(vcdig=vcdig.decode("utf-8"), regk=regk, dig=iss.said, + dt="2021-01-01T00:00:00.000000+00:00") + assert other.said != rev.said + with pytest.raises(LikelyDuplicitousError): + tvy.processEvent(serder=other, seqner=seqner, saider=saider) + def test_tevery_process_escrow(mockCoringRandomNonce): with basing.openDB() as db, keeping.openKS() as kpr, viring.openReger() as reg: