Skip to content

Commit deadf41

Browse files
committed
Rehydrate escrowed signatures into kwa after threshold is satisfied
1 parent 82446e8 commit deadf41

2 files changed

Lines changed: 206 additions & 0 deletions

File tree

src/keri/core/kraming.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,114 @@ def _remNonAuthAttachments(self, key):
806806
self.db.kramBSSS.rem(key)
807807
self.db.kramTMQS.rem(key)
808808

809+
@staticmethod
810+
def _dedupeAttachmentItems(items):
811+
"""Return items in first-seen order with duplicates removed.
812+
813+
Items are CESR tuples or bytes; identity uses a nested qb64/raw key.
814+
"""
815+
seen = set()
816+
out = []
817+
for item in items:
818+
key = Kramer._attachmentItemKey(item)
819+
if key in seen:
820+
continue
821+
seen.add(key)
822+
out.append(item)
823+
return out
824+
825+
@staticmethod
826+
def _attachmentItemKey(item):
827+
if isinstance(item, (bytes, memoryview)):
828+
return ('b', bytes(item))
829+
if isinstance(item, (list, tuple)):
830+
return tuple(Kramer._attachmentItemKey(x) for x in item)
831+
if hasattr(item, 'qb64'):
832+
return item.qb64
833+
return repr(item)
834+
835+
def _rehydrateKwaFromEscrow(self, partialKey, _senderId, kwa):
836+
"""Merge escrowed partial multisig state into ``kwa`` for downstream dispatch.
837+
838+
After threshold is satisfied on a later delivery, ``kwa`` reflects only
839+
that parse; ``kramPMKS`` and non-auth attachment DBs hold the union of
840+
state accumulated across deliveries.
841+
842+
Parameters:
843+
partialKey (tuple): ``(AID, MID)`` escrow key
844+
_senderId (str): message sender AID (qb64); reserved for filtering
845+
kwa (dict): parser attachment dict; mutated in place
846+
"""
847+
escrow_sigs = self.db.kramPMKS.get(partialKey)
848+
if escrow_sigs:
849+
kwa['sigers'] = sorted(escrow_sigs, key=lambda s: s.index)
850+
851+
trqs_esc = list(self.db.kramTRQS.get(partialKey) or [])
852+
kwa['trqs'] = self._dedupeAttachmentItems(
853+
trqs_esc + kwa.get('trqs', []))
854+
855+
flat_tsgs = list(self.db.kramTSGS.get(partialKey) or [])
856+
groups = {}
857+
for prefixer, number, diger, siger in flat_tsgs:
858+
gk = (prefixer.qb64, number.sn, diger.qb64)
859+
if gk not in groups:
860+
groups[gk] = [prefixer, number, diger, []]
861+
sigers = groups[gk][3]
862+
if not any(s.qb64 == siger.qb64 for s in sigers):
863+
sigers.append(siger)
864+
865+
merged_tsgs = {}
866+
for quad in groups.values():
867+
prefixer, number, diger, sigers = quad
868+
gk = (prefixer.qb64, number.sn, diger.qb64)
869+
merged_tsgs[gk] = (prefixer, number, diger, list(sigers))
870+
871+
for prefixer, number, diger, sigers in kwa.get('tsgs', []):
872+
gk = (prefixer.qb64, number.sn, diger.qb64)
873+
if gk not in merged_tsgs:
874+
merged_tsgs[gk] = (prefixer, number, diger, [])
875+
bucket = merged_tsgs[gk][3]
876+
for s in sigers:
877+
if not any(x.qb64 == s.qb64 for x in bucket):
878+
bucket.append(s)
879+
880+
if merged_tsgs:
881+
kwa['tsgs'] = list(merged_tsgs.values())
882+
else:
883+
kwa.pop('tsgs', None)
884+
885+
ssts_esc = list(self.db.kramSSTS.get(partialKey) or [])
886+
kwa['ssts'] = self._dedupeAttachmentItems(
887+
ssts_esc + kwa.get('ssts', []))
888+
889+
frcs_esc = list(self.db.kramFRCS.get(partialKey) or [])
890+
kwa['frcs'] = self._dedupeAttachmentItems(
891+
frcs_esc + kwa.get('frcs', []))
892+
893+
tdcs_esc = list(self.db.kramTDCS.get(partialKey) or [])
894+
kwa['tdcs'] = self._dedupeAttachmentItems(
895+
tdcs_esc + kwa.get('tdcs', []))
896+
897+
ptds_esc = list(self.db.kramPTDS.get(partialKey) or [])
898+
kwa['ptds'] = self._dedupeAttachmentItems(
899+
ptds_esc + kwa.get('ptds', []))
900+
901+
bsqs_esc = list(self.db.kramBSQS.get(partialKey) or [])
902+
kwa['bsqs'] = self._dedupeAttachmentItems(
903+
bsqs_esc + kwa.get('bsqs', []))
904+
905+
bsss_esc = list(self.db.kramBSSS.get(partialKey) or [])
906+
kwa['bsss'] = self._dedupeAttachmentItems(
907+
bsss_esc + kwa.get('bsss', []))
908+
909+
tmqs_esc = list(self.db.kramTMQS.get(partialKey) or [])
910+
kwa['tmqs'] = self._dedupeAttachmentItems(
911+
tmqs_esc + kwa.get('tmqs', []))
912+
913+
for name in ('trqs', 'ssts', 'frcs', 'tdcs', 'ptds', 'bsqs', 'bsss',
914+
'tmqs'):
915+
if not kwa.get(name):
916+
kwa.pop(name, None)
809917

810918
def intake(self, serder, kwa=None):
811919
"""Process message through KRAM denial and cache logic.
@@ -981,6 +1089,7 @@ def kramit(self, msg, kwa=None):
9811089
sigIndices = [sig.index for sig in allSigs]
9821090

9831091
if kever.tholder.satisfy(indices=sigIndices):
1092+
self._rehydrateKwaFromEscrow(key, senderId, kwa)
9841093
return msg
9851094

9861095
# Threshold not satisfied, message remains pending
@@ -1206,6 +1315,7 @@ def kramit(self, msg, kwa=None):
12061315
sigIndices = [sig.index for sig in allSigs]
12071316

12081317
if kever.tholder.satisfy(indices=sigIndices):
1318+
self._rehydrateKwaFromEscrow(partialKey, senderId, kwa)
12091319
return msg
12101320

12111321
# Threshold not satisfied, message remains pending

tests/core/test_kraming.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1916,6 +1916,102 @@ def test_non_auth_attachments_stored(mockHelpingNowUTC):
19161916
"""Done Test"""
19171917

19181918

1919+
def test_multisig_kwa_rehydration_after_threshold(mockHelpingNowUTC):
1920+
"""Threshold on a later delivery merges escrow PMKS + non-auth into kwa.
1921+
1922+
The second parse may omit non-auth attachments that were stored on the
1923+
first partial; :meth:`Kramer._rehydrateKwaFromEscrow` restores them.
1924+
"""
1925+
1926+
salt1 = Salter(raw=b'0123456789abcdef').qb64
1927+
salt2 = Salter(raw=b'0123456789abcdeg').qb64
1928+
1929+
with (openHby(name="rehSender", base="test", salt=salt1) as senderHby,
1930+
openHby(name="rehReceiver", base="test", salt=salt2) as receiverHby):
1931+
1932+
senderHab = senderHby.makeHab(name="rehSender", isith='2', icount=3,
1933+
transferable=True)
1934+
receiverHab = receiverHby.makeHab(name="rehReceiver", isith='1', icount=1,
1935+
transferable=True)
1936+
1937+
crossKvy = Kevery(db=receiverHby.db, lax=False, local=False)
1938+
senderIcp = senderHab.makeOwnEvent(sn=0)
1939+
Parser(version=Vrsn_1_0).parse(ims=bytearray(senderIcp), kvy=crossKvy)
1940+
1941+
with openCF(name="rehKram", base="test") as cf:
1942+
cf.put(KRAM_INTEGRATION_CONFIG)
1943+
kramer = Kramer(db=receiverHby.db, cf=cf)
1944+
1945+
stamp = helping.nowIso8601()
1946+
prefixer = Prefixer(qb64=senderHab.pre)
1947+
1948+
msg = query(pre=senderHab.pre,
1949+
route="ksn",
1950+
query=dict(i=senderHab.pre, src=senderHab.pre),
1951+
stamp=stamp,
1952+
pvrsn=Vrsn_2_0)
1953+
1954+
allSigers = senderHab.mgr.sign(ser=msg.raw,
1955+
verfers=senderHab.kever.verfers,
1956+
indexed=True)
1957+
1958+
partialKey = (senderHab.pre, msg.said)
1959+
1960+
senderKever = receiverHby.db.kevers[senderHab.pre]
1961+
seqner = Seqner(sn=senderKever.sner.num)
1962+
saider = Saider(qb64=senderKever.serder.said)
1963+
diger = Diger(ser=msg.raw)
1964+
otherPrefixer = Prefixer(qb64=receiverHab.pre)
1965+
1966+
trqs = [(prefixer, seqner, saider, allSigers[0])]
1967+
tsgs = [(otherPrefixer, seqner, saider, [allSigers[0]])]
1968+
ssts = [(otherPrefixer, seqner, saider)]
1969+
firner = Seqner(sn=0)
1970+
dater = Dater(dts=stamp)
1971+
frcs = [(firner, dater)]
1972+
verser = Verser(pvrsn=Vrsn_2_0)
1973+
tdcs = [(verser, diger)]
1974+
ptds = [b'\x00\x01\x02\x03']
1975+
noncer0 = Noncer()
1976+
noncer1 = Noncer()
1977+
labeler = Labeler(label='test')
1978+
bsqs = [(diger, noncer0, noncer1, labeler)]
1979+
number = Number(num=1)
1980+
noncer2 = Noncer()
1981+
bsss = [(diger, noncer0, noncer1, labeler, number, noncer2)]
1982+
texter = Texter(text='application/json')
1983+
tmqs = [(diger, noncer0, labeler, texter)]
1984+
1985+
kwa = dict(ssgs=[(prefixer, [allSigers[0]])],
1986+
trqs=trqs, tsgs=tsgs, ssts=ssts,
1987+
frcs=frcs, tdcs=tdcs, ptds=ptds,
1988+
bsqs=bsqs, bsss=bsss, tmqs=tmqs)
1989+
1990+
r1 = kramer.kramit(msg, kwa)
1991+
assert r1 is None
1992+
assert len(kramer.db.kramPMKS.get(keys=partialKey)) == 1
1993+
1994+
kwa2 = dict(ssgs=[(prefixer, [allSigers[2]])])
1995+
r2 = kramer.kramit(msg, kwa2)
1996+
assert r2 is not None
1997+
1998+
assert {s.index for s in kwa2['sigers']} == {0, 2}
1999+
assert len(kwa2['trqs']) == 1
2000+
assert len(kwa2['tsgs']) == 1
2001+
assert len(kwa2['ssts']) == 1
2002+
assert len(kwa2['frcs']) == 1
2003+
assert len(kwa2['tdcs']) == 1
2004+
assert len(kwa2['ptds']) == 1
2005+
assert len(kwa2['bsqs']) == 1
2006+
assert len(kwa2['bsss']) == 1
2007+
assert len(kwa2['tmqs']) == 1
2008+
2009+
escrow = kramer.db.kramPMKS.get(keys=partialKey)
2010+
assert {s.index for s in escrow} == {0, 2}
2011+
2012+
"""Done Test"""
2013+
2014+
19192015
def test_non_auth_attachments_empty_kwa(mockHelpingNowUTC):
19202016
"""Test that _storeNonAuthAttachments is a no-op when kwa contains no
19212017
non-auth attachment keys. Partial dbs remain empty.

0 commit comments

Comments
 (0)