keri-ts 2.2.5 → 2.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/keri/npm/src/acdc/dhking.js +29 -2
- package/esm/keri/npm/src/acdc/observing.js +10 -3
- package/esm/keri/npm/src/acdc/query-resend.js +74 -0
- package/esm/keri/npm/src/acdc/regengine.js +39 -9
- package/esm/keri/npm/src/acdc/regeventing.js +5 -3
- package/esm/keri/npm/src/acdc/unblinding.js +38 -3
- package/esm/keri/npm/src/app/delivery/forwarding.js +28 -1
- package/esm/keri/npm/src/app/ipex/group-issuance.js +28 -14
- package/esm/keri/npm/src/app/ipex/ipex-discloser.js +58 -9
- package/esm/keri/npm/src/app/ipex/issuance.js +16 -9
- package/esm/keri/npm/src/app/ipex/unblinding-workflow.js +23 -8
- package/esm/keri/npm/src/app/runtime/agent-runtime.js +1 -1
- package/esm/keri/npm/src/app/runtime/ipex-auxiliaries.js +8 -1
- package/esm/keri/npm/src/core/mailbox/kram-policy.js +8 -0
- package/esm/keri/npm/src/core/missing-anchor-query.js +20 -0
- package/esm/keri/npm/src/vdr/eventing.js +43 -9
- package/esm/keri/npm/src/version.js +2 -2
- package/package.json +1 -1
- package/types/acdc/dhking.d.ts +8 -0
- package/types/acdc/dhking.d.ts.map +1 -1
- package/types/acdc/observing.d.ts +5 -1
- package/types/acdc/observing.d.ts.map +1 -1
- package/types/acdc/query-resend.d.ts +24 -0
- package/types/acdc/query-resend.d.ts.map +1 -0
- package/types/acdc/regengine.d.ts +19 -1
- package/types/acdc/regengine.d.ts.map +1 -1
- package/types/acdc/regeventing.d.ts.map +1 -1
- package/types/acdc/unblinding.d.ts +13 -1
- package/types/acdc/unblinding.d.ts.map +1 -1
- package/types/app/delivery/forwarding.d.ts +21 -0
- package/types/app/delivery/forwarding.d.ts.map +1 -1
- package/types/app/ipex/group-issuance.d.ts +6 -2
- package/types/app/ipex/group-issuance.d.ts.map +1 -1
- package/types/app/ipex/ipex-discloser.d.ts +25 -0
- package/types/app/ipex/ipex-discloser.d.ts.map +1 -1
- package/types/app/ipex/issuance.d.ts +11 -5
- package/types/app/ipex/issuance.d.ts.map +1 -1
- package/types/app/ipex/unblinding-workflow.d.ts +9 -1
- package/types/app/ipex/unblinding-workflow.d.ts.map +1 -1
- package/types/app/runtime/ipex-auxiliaries.d.ts.map +1 -1
- package/types/core/mailbox/kram-policy.d.ts +6 -0
- package/types/core/mailbox/kram-policy.d.ts.map +1 -1
- package/types/core/missing-anchor-query.d.ts +34 -0
- package/types/core/missing-anchor-query.d.ts.map +1 -0
- package/types/vdr/eventing.d.ts +32 -1
- package/types/vdr/eventing.d.ts.map +1 -1
- package/types/version.d.ts +2 -2
|
@@ -17,6 +17,7 @@ import { query, reply } from "../core/protocol-eventing.js";
|
|
|
17
17
|
import { messagize } from "../core/protocol-serialization.js";
|
|
18
18
|
import { fetchStoredTsgs, unverifiedReplyDecision } from "../core/routing.js";
|
|
19
19
|
import { timestampMicros } from "../time/mod.js";
|
|
20
|
+
import { QUERY_RESEND_MS } from "./query-resend.js";
|
|
20
21
|
import { validateUpdate, verifyRegistryAnchor } from "./regeventing.js";
|
|
21
22
|
import { publicOpeningAttachment } from "./registering.js";
|
|
22
23
|
const domain = new TextEncoder().encode("KERI/ipex/dhke/X25519-HKDF-SHA256-Salt128/v1");
|
|
@@ -343,6 +344,8 @@ export class Dhke {
|
|
|
343
344
|
peer: null,
|
|
344
345
|
query: null,
|
|
345
346
|
reply: null,
|
|
347
|
+
accepted: null,
|
|
348
|
+
repliedAt: 0,
|
|
346
349
|
proposal: null,
|
|
347
350
|
contribution: null,
|
|
348
351
|
custody: null,
|
|
@@ -493,6 +496,12 @@ export class Dhke {
|
|
|
493
496
|
if (own && (!op.proposal || !bytesEqual(op.proposal, serder.raw))) {
|
|
494
497
|
throw new ValidationError("Own DH query lacks exact local proposal approval");
|
|
495
498
|
}
|
|
499
|
+
// A group re-sends its frozen query unchanged (one member cannot re-date it), and KERIpy resends identical
|
|
500
|
+
// bytes. The wire, attachments included, is compared against the one already authenticated, so a copy is
|
|
501
|
+
// never trusted on its body or SAID alone.
|
|
502
|
+
const wire = auxiliaryQueryWire(envelope);
|
|
503
|
+
if (!own && op.accepted && bytesEqual(op.accepted, wire))
|
|
504
|
+
return this.duplicateQuery(op);
|
|
496
505
|
if (!verifyQueryAttachments(this.hab.db, envelope)) {
|
|
497
506
|
return { kind: "drop", reason: "missingAuthentication" };
|
|
498
507
|
}
|
|
@@ -513,6 +522,7 @@ export class Dhke {
|
|
|
513
522
|
}
|
|
514
523
|
if (this.group && !this.custody(op)) {
|
|
515
524
|
op.peer = { ...peer };
|
|
525
|
+
op.accepted = wire;
|
|
516
526
|
return { kind: "accept" };
|
|
517
527
|
}
|
|
518
528
|
if (!op.reply) {
|
|
@@ -537,10 +547,12 @@ export class Dhke {
|
|
|
537
547
|
}
|
|
538
548
|
if (this.group) {
|
|
539
549
|
this.outgoing.push({ kin: "sign", dest: this.hab.pre, msg: op.contribution.slice() });
|
|
550
|
+
op.accepted = wire;
|
|
540
551
|
return { kind: "accept" };
|
|
541
552
|
}
|
|
542
553
|
}
|
|
543
|
-
this.
|
|
554
|
+
this.emitReply(op);
|
|
555
|
+
op.accepted = wire;
|
|
544
556
|
return { kind: "accept" };
|
|
545
557
|
}
|
|
546
558
|
catch (error) {
|
|
@@ -549,6 +561,21 @@ export class Dhke {
|
|
|
549
561
|
throw error;
|
|
550
562
|
}
|
|
551
563
|
}
|
|
564
|
+
/**
|
|
565
|
+
* A copy of the accepted query: nothing is verified, derived or contributed again, so a member round that
|
|
566
|
+
* already holds its `sign` contribution is not asked for another. If the reply exists it is queued again for a
|
|
567
|
+
* peer that lost it, at most once per {@link QUERY_RESEND_MS}.
|
|
568
|
+
*/
|
|
569
|
+
duplicateQuery(op) {
|
|
570
|
+
if (op.reply && this.now() - op.repliedAt >= QUERY_RESEND_MS)
|
|
571
|
+
this.emitReply(op);
|
|
572
|
+
return { kind: "accept" };
|
|
573
|
+
}
|
|
574
|
+
/** Queue the stored reply for the issuer and note when. */
|
|
575
|
+
emitReply(op) {
|
|
576
|
+
this.outgoing.push({ kin: "reply", dest: op.issuer, msg: op.reply.slice() });
|
|
577
|
+
op.repliedAt = this.now();
|
|
578
|
+
}
|
|
552
579
|
/** Revery decides reply threshold acceptance before any issuer derivation or retention. */
|
|
553
580
|
processReply({ serder, diger, route, cigars = [], tsgs = [] }) {
|
|
554
581
|
try {
|
|
@@ -574,7 +601,7 @@ export class Dhke {
|
|
|
574
601
|
});
|
|
575
602
|
}
|
|
576
603
|
if (this.custody(op))
|
|
577
|
-
this.
|
|
604
|
+
this.emitReply(op);
|
|
578
605
|
}
|
|
579
606
|
else if (this.custody(op) && !op.completed)
|
|
580
607
|
this.retain(op, peer, this.derive(op, peer, "issuer"));
|
|
@@ -11,9 +11,10 @@ import { publicOpeningAttachment } from "./registering.js";
|
|
|
11
11
|
* event commits independently; a later failure preserves the already accepted prefix.
|
|
12
12
|
* Parsed evidence stays borrowed. Composition supplies this owner to v2 protocol dispatch;
|
|
13
13
|
* no local habitat, alias or application disclosure custody is created by public intake.
|
|
14
|
+
* `cues` receives the issuer KEL `logs` query of each event that first waits for its anchoring KEL event.
|
|
14
15
|
*/
|
|
15
16
|
export class RegistryObserver {
|
|
16
|
-
constructor(db, store) {
|
|
17
|
+
constructor(db, store, cues = null) {
|
|
17
18
|
Object.defineProperty(this, "db", {
|
|
18
19
|
enumerable: true,
|
|
19
20
|
configurable: true,
|
|
@@ -26,6 +27,12 @@ export class RegistryObserver {
|
|
|
26
27
|
writable: true,
|
|
27
28
|
value: store
|
|
28
29
|
});
|
|
30
|
+
Object.defineProperty(this, "cues", {
|
|
31
|
+
enumerable: true,
|
|
32
|
+
configurable: true,
|
|
33
|
+
writable: true,
|
|
34
|
+
value: cues
|
|
35
|
+
});
|
|
29
36
|
}
|
|
30
37
|
/** Stage parsed events before inception, otherwise delegate ordinary validation and acceptance. */
|
|
31
38
|
processEvent(serder, locators = {}) {
|
|
@@ -62,7 +69,7 @@ export class RegistryObserver {
|
|
|
62
69
|
if (event.ilk === Ilks.rip) {
|
|
63
70
|
if (!event.issuer)
|
|
64
71
|
return { kind: "reject", reason: "issuer" };
|
|
65
|
-
engine ??= new RegistryEventer(this.db, event.issuer, this.store, event.regid);
|
|
72
|
+
engine ??= new RegistryEventer(this.db, event.issuer, this.store, event.regid, this.cues);
|
|
66
73
|
}
|
|
67
74
|
if (engine) {
|
|
68
75
|
const decision = engine.processEvent(serder, locators);
|
|
@@ -124,6 +131,6 @@ export class RegistryObserver {
|
|
|
124
131
|
if (result.kind !== "valid" || result.event.regid !== regid || !result.event.issuer) {
|
|
125
132
|
throw new Error("Corrupt observer registry inception.");
|
|
126
133
|
}
|
|
127
|
-
return new RegistryEventer(this.db, result.event.issuer, this.store, regid);
|
|
134
|
+
return new RegistryEventer(this.db, result.event.issuer, this.store, regid, this.cues);
|
|
128
135
|
}
|
|
129
136
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resend schedule for a group's threshold DH or Unblind query.
|
|
3
|
+
*
|
|
4
|
+
* A single signer re-dates every resend (`Dhke.requery`, `Unblind.requery`), so a resend is a new query the peer's
|
|
5
|
+
* KRAM accepts. A group cannot: re-dating needs the members' signatures, so the frozen query is resent as the same
|
|
6
|
+
* bytes. The peer's KRAM drops identical copies cheaply only while its replay cache row lives, and once the query's
|
|
7
|
+
* `dt` is older than the multi-key acceptance window it refuses every copy as untimely. This schedule therefore
|
|
8
|
+
* backs off and stops resending an unchanged query when the peer can no longer accept it. It never ends the
|
|
9
|
+
* operation: the owner keeps waiting until its own deadline, exactly as before.
|
|
10
|
+
*/
|
|
11
|
+
import { parseBytes } from "cesr-ts";
|
|
12
|
+
import { ValidationError } from "../core/errors.js";
|
|
13
|
+
import { timestampMicros } from "../time/mod.js";
|
|
14
|
+
/** Milliseconds between the first resends of a query, and the shortest gap a receiver re-sends a stored reply. */
|
|
15
|
+
export const QUERY_RESEND_MS = 1000;
|
|
16
|
+
/** The longest gap the backoff reaches, in milliseconds. */
|
|
17
|
+
export const QUERY_RESEND_CAP_MS = 8000;
|
|
18
|
+
/** One group operation's frozen threshold query and when it may next be sent; memory only. */
|
|
19
|
+
export class ThresholdQueryResend {
|
|
20
|
+
constructor() {
|
|
21
|
+
/** The frozen signed query, unchanged on every resend; absent until the members' contributions complete it. */
|
|
22
|
+
Object.defineProperty(this, "query", {
|
|
23
|
+
enumerable: true,
|
|
24
|
+
configurable: true,
|
|
25
|
+
writable: true,
|
|
26
|
+
value: void 0
|
|
27
|
+
});
|
|
28
|
+
Object.defineProperty(this, "queryAt", {
|
|
29
|
+
enumerable: true,
|
|
30
|
+
configurable: true,
|
|
31
|
+
writable: true,
|
|
32
|
+
value: 0
|
|
33
|
+
});
|
|
34
|
+
Object.defineProperty(this, "next", {
|
|
35
|
+
enumerable: true,
|
|
36
|
+
configurable: true,
|
|
37
|
+
writable: true,
|
|
38
|
+
value: 0
|
|
39
|
+
});
|
|
40
|
+
Object.defineProperty(this, "wait", {
|
|
41
|
+
enumerable: true,
|
|
42
|
+
configurable: true,
|
|
43
|
+
writable: true,
|
|
44
|
+
value: 0
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** Take the frozen query and restart the schedule; its own `dt` is what the acceptance window is measured from. */
|
|
48
|
+
set(query) {
|
|
49
|
+
const body = parseBytes(query)[0]?.body;
|
|
50
|
+
const micros = timestampMicros(body?.sad?.dt);
|
|
51
|
+
if (micros === null)
|
|
52
|
+
throw new ValidationError("Threshold query has no valid date");
|
|
53
|
+
this.query = query;
|
|
54
|
+
this.queryAt = Number(micros / 1000n);
|
|
55
|
+
this.next = 0;
|
|
56
|
+
this.wait = 0;
|
|
57
|
+
}
|
|
58
|
+
/** Whether a query is frozen and the running gap has elapsed at `now` (epoch ms). */
|
|
59
|
+
ready(now) {
|
|
60
|
+
return !!this.query && now >= this.next;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Whether the query is still younger than `windowMs` at `now`; past it a receiver can no longer accept the
|
|
64
|
+
* unchanged query, so it is not sent again. A null window (this store never ran KRAM) never closes.
|
|
65
|
+
*/
|
|
66
|
+
fresh(now, windowMs) {
|
|
67
|
+
return windowMs === null || now - this.queryAt < windowMs;
|
|
68
|
+
}
|
|
69
|
+
/** Record a send at `now`: the next gap doubles from `baseMs` up to {@link QUERY_RESEND_CAP_MS}. */
|
|
70
|
+
sent(now, baseMs) {
|
|
71
|
+
this.wait = this.wait ? Math.max(baseMs, Math.min(this.wait * 2, QUERY_RESEND_CAP_MS)) : baseMs;
|
|
72
|
+
this.next = now + this.wait;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -2,14 +2,18 @@
|
|
|
2
2
|
import { Diger, Ilks, NumberPrimitive, } from "cesr-ts";
|
|
3
3
|
import { DatabaseNotOpenError, DatabaseOperationError } from "../core/errors.js";
|
|
4
4
|
import { consoleLogger } from "../core/logger.js";
|
|
5
|
+
import { missingAnchorCue, missingAnchorQuery } from "../core/missing-anchor-query.js";
|
|
5
6
|
import { findRegistryAnchor, validateRegistrySourceLocators, validateRip, validateUpdate, verifyRegistryAnchor, } from "./regeventing.js";
|
|
6
7
|
/**
|
|
7
8
|
* Borrow the issuer's accepted KEL and registry store; no keys or handles are acquired.
|
|
8
9
|
* The local facade and foreign observer share this accepted-state engine.
|
|
9
10
|
* Publication is atomic; binding this instance happens only after successful commit.
|
|
11
|
+
* An event that first enters the anchor escrow publishes one issuer KEL `logs` query to `cues` after that write.
|
|
10
12
|
*/
|
|
11
13
|
export class RegistryEventer {
|
|
12
|
-
constructor(db, issuer, store, regk = null
|
|
14
|
+
constructor(db, issuer, store, regk = null,
|
|
15
|
+
/** Application cue flow for missing-anchor queries; null for a local issuer, whose own KEL holds every anchor. */
|
|
16
|
+
cues = null) {
|
|
13
17
|
Object.defineProperty(this, "db", {
|
|
14
18
|
enumerable: true,
|
|
15
19
|
configurable: true,
|
|
@@ -34,6 +38,12 @@ export class RegistryEventer {
|
|
|
34
38
|
writable: true,
|
|
35
39
|
value: regk
|
|
36
40
|
});
|
|
41
|
+
Object.defineProperty(this, "cues", {
|
|
42
|
+
enumerable: true,
|
|
43
|
+
configurable: true,
|
|
44
|
+
writable: true,
|
|
45
|
+
value: cues
|
|
46
|
+
});
|
|
37
47
|
}
|
|
38
48
|
/** Validated accepted head metadata; local construction may inspect it without duplicating acceptance checks. */
|
|
39
49
|
get acceptedHeadEvent() {
|
|
@@ -234,6 +244,9 @@ export class RegistryEventer {
|
|
|
234
244
|
const decision = this.processOne(serder, locators);
|
|
235
245
|
if (decision.kind === "accept")
|
|
236
246
|
this.processQueued();
|
|
247
|
+
// The escrow row is committed; ask the issuer's KEL holders for what the anchor needs through the ordinary cue path.
|
|
248
|
+
if (decision.kind === "escrow" && decision.query)
|
|
249
|
+
this.cues?.push(missingAnchorCue(decision.query));
|
|
237
250
|
return decision;
|
|
238
251
|
}
|
|
239
252
|
/** Decide and persist one event; callers own subsequent chain progression. */
|
|
@@ -269,10 +282,8 @@ export class RegistryEventer {
|
|
|
269
282
|
return previous;
|
|
270
283
|
this.store.putEvent(serder);
|
|
271
284
|
this.discoverAnchor(event);
|
|
272
|
-
if (!this.store.anchor(event.said))
|
|
273
|
-
this.
|
|
274
|
-
return { kind: "escrow", reason: "anchor", event };
|
|
275
|
-
}
|
|
285
|
+
if (!this.store.anchor(event.said))
|
|
286
|
+
return this.escrowAnchorless(event, locators);
|
|
276
287
|
this.store.baser.maes.rem(event.regid, Number(event.sn), [event.said]);
|
|
277
288
|
if (!prior) {
|
|
278
289
|
this.store.escrowOutOfOrder(event.regid, Number(event.sn), event.said);
|
|
@@ -314,10 +325,8 @@ export class RegistryEventer {
|
|
|
314
325
|
throw new Error("Registry head exists without its accepted inception.");
|
|
315
326
|
this.store.putEvent(serder);
|
|
316
327
|
this.discoverAnchor(event);
|
|
317
|
-
if (!this.store.anchor(event.said))
|
|
318
|
-
this.
|
|
319
|
-
return { kind: "escrow", reason: "anchor", event };
|
|
320
|
-
}
|
|
328
|
+
if (!this.store.anchor(event.said))
|
|
329
|
+
return this.escrowAnchorless(event, locators);
|
|
321
330
|
this.commit(event);
|
|
322
331
|
return { kind: "accept", event };
|
|
323
332
|
});
|
|
@@ -325,6 +334,27 @@ export class RegistryEventer {
|
|
|
325
334
|
this.regk = event.regid;
|
|
326
335
|
return decision;
|
|
327
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* Retain an event whose anchoring issuer KEL event is not accepted yet and decide whether to ask for it.
|
|
339
|
+
*
|
|
340
|
+
* The decision names the missing KEL only when this call put the event into the anchor escrow; retries and repeat
|
|
341
|
+
* arrivals find the row present. A source seal naming this issuer supplies the anchor's sequence number, and the
|
|
342
|
+
* highest one bounds the replay; without one the query names only where the recipient's KEL ends.
|
|
343
|
+
*/
|
|
344
|
+
escrowAnchorless(event, locators) {
|
|
345
|
+
const added = this.store.escrowMissingAnchor(event.regid, Number(event.sn), event.said);
|
|
346
|
+
const decision = { kind: "escrow", reason: "anchor", event };
|
|
347
|
+
if (!added)
|
|
348
|
+
return decision;
|
|
349
|
+
const seals = [
|
|
350
|
+
...(locators.sscs ?? []).map((seal) => seal.s.num),
|
|
351
|
+
...(locators.ssts ?? []).filter((seal) => seal.i.qb64 === this.issuer).map((seal) => seal.s.num),
|
|
352
|
+
];
|
|
353
|
+
const anchor = seals.length ? seals.reduce((high, sn) => (sn > high ? sn : high)) : null;
|
|
354
|
+
const accepted = this.db.getKever(this.issuer, { refresh: true })?.sner.num ?? null;
|
|
355
|
+
const query = missingAnchorQuery(this.issuer, accepted, anchor);
|
|
356
|
+
return query ? { ...decision, query } : decision;
|
|
357
|
+
}
|
|
328
358
|
/** Replay is read-only; first acceptance repeats this check under its write transaction. */
|
|
329
359
|
acceptedEvent(event) {
|
|
330
360
|
const existing = this.acceptedAt(event.regid, Number(event.sn));
|
|
@@ -150,8 +150,9 @@ function sealDigests(event) {
|
|
|
150
150
|
export function findRegistryAnchor(db, issuer, registry) {
|
|
151
151
|
let earliest = null;
|
|
152
152
|
for (const event of currentRegistryBranch(db, issuer)) {
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
// Seal test first: fullyWitnessed walks back to the governing establishment, so it runs only on matches.
|
|
154
|
+
if (event.seals.some((seal) => matchesRegistrySeal(seal, registry))
|
|
155
|
+
&& db.fullyWitnessed(event)) {
|
|
155
156
|
earliest = [event.sn, event.said];
|
|
156
157
|
}
|
|
157
158
|
}
|
|
@@ -161,7 +162,8 @@ export function findRegistryAnchor(db, issuer, registry) {
|
|
|
161
162
|
function findDigestAnchor(db, issuer, digest) {
|
|
162
163
|
let earliest = null;
|
|
163
164
|
for (const event of currentRegistryBranch(db, issuer)) {
|
|
164
|
-
|
|
165
|
+
// Seal test first: fullyWitnessed walks back to the governing establishment, so it runs only on matches.
|
|
166
|
+
if (sealDigests(event).includes(digest) && db.fullyWitnessed(event)) {
|
|
165
167
|
earliest = [event.sn, event.said];
|
|
166
168
|
}
|
|
167
169
|
}
|
|
@@ -25,6 +25,7 @@ import { query, reply } from "../core/protocol-eventing.js";
|
|
|
25
25
|
import { messagize } from "../core/protocol-serialization.js";
|
|
26
26
|
import { fetchStoredTsgs, unverifiedReplyDecision } from "../core/routing.js";
|
|
27
27
|
import { prepareNests, publishNests } from "../db/exchange/exchange-nests.js";
|
|
28
|
+
import { QUERY_RESEND_MS } from "./query-resend.js";
|
|
28
29
|
import { validateUpdate, verifyRegistryAnchor, vet, vetBlind, vetBound, } from "./regeventing.js";
|
|
29
30
|
import { publicOpeningAttachment } from "./registering.js";
|
|
30
31
|
/** Ordinary registry states an Unblind operation may reveal unless its approval names others. */
|
|
@@ -89,7 +90,7 @@ const KEL_ASK_MS = 5000;
|
|
|
89
90
|
*/
|
|
90
91
|
export class Unblind {
|
|
91
92
|
/** Bind the exact Grant verifier's store; the state vocabulary is chosen per approved operation. */
|
|
92
|
-
constructor({ hab, exc, registry, rvy = hab.rvy, kram = true }) {
|
|
93
|
+
constructor({ hab, exc, registry, rvy = hab.rvy, now = Date.now, kram = true }) {
|
|
93
94
|
Object.defineProperty(this, "resource", {
|
|
94
95
|
enumerable: true,
|
|
95
96
|
configurable: true,
|
|
@@ -162,7 +163,14 @@ export class Unblind {
|
|
|
162
163
|
writable: true,
|
|
163
164
|
value: void 0
|
|
164
165
|
});
|
|
166
|
+
Object.defineProperty(this, "now", {
|
|
167
|
+
enumerable: true,
|
|
168
|
+
configurable: true,
|
|
169
|
+
writable: true,
|
|
170
|
+
value: void 0
|
|
171
|
+
});
|
|
165
172
|
this.kram = kram;
|
|
173
|
+
this.now = now;
|
|
166
174
|
if (!(hab instanceof Hab) || hab.db !== exc.hby.db || registry.db !== hab.db || rvy.db !== hab.db
|
|
167
175
|
|| exc.hby.habs.get(hab.pre) !== hab) {
|
|
168
176
|
throw new ValidationError("Unblind requires a local signing habitat");
|
|
@@ -228,6 +236,8 @@ export class Unblind {
|
|
|
228
236
|
scope: new Map([...context.registries].map(([regid, nodes]) => [regid, [...nodes]])),
|
|
229
237
|
query: null,
|
|
230
238
|
reply: null,
|
|
239
|
+
accepted: null,
|
|
240
|
+
repliedAt: 0,
|
|
231
241
|
requested: null,
|
|
232
242
|
selected: null,
|
|
233
243
|
openings: new Map(),
|
|
@@ -389,6 +399,11 @@ export class Unblind {
|
|
|
389
399
|
if (own && (!op.proposal || !bytesEqual(op.proposal, serder.raw))) {
|
|
390
400
|
throw new ValidationError("Own Unblind query lacks its exact locally approved proposal");
|
|
391
401
|
}
|
|
402
|
+
// A group re-sends its frozen query unchanged (one member cannot re-date it), and KERIpy resends identical
|
|
403
|
+
// bytes. The wire, attachments and evidence included, is compared against the one already authenticated, so
|
|
404
|
+
// a copy is never trusted on its body or SAID alone.
|
|
405
|
+
if (!own && op.accepted && bytesEqual(op.accepted, this.queryWire(envelope)))
|
|
406
|
+
return this.duplicateQuery(op);
|
|
392
407
|
if (!verifyQueryAttachments(this.hab.db, envelope))
|
|
393
408
|
return { kind: "drop", reason: "missingAuthentication" };
|
|
394
409
|
if (op.requested && !sameSelection(op.requested, selected)) {
|
|
@@ -429,9 +444,11 @@ export class Unblind {
|
|
|
429
444
|
op.requestedByPeer = true;
|
|
430
445
|
// KRAM now holds this query as seen, so a resend of it is a replay and a group cannot re-date it: the
|
|
431
446
|
// owed reply must outlive this owner. The accepted request is the durable record; the operation is a cache.
|
|
432
|
-
|
|
447
|
+
const wire = this.queryWire(envelope);
|
|
448
|
+
this.hab.db.heldu.pin([this.hab.pre, op.xid], wire);
|
|
433
449
|
if (!this.group)
|
|
434
450
|
this.respond(op.xid);
|
|
451
|
+
op.accepted = wire;
|
|
435
452
|
return { kind: "accept" };
|
|
436
453
|
}
|
|
437
454
|
catch (error) {
|
|
@@ -533,9 +550,27 @@ export class Unblind {
|
|
|
533
550
|
}
|
|
534
551
|
op.reply = signed;
|
|
535
552
|
}
|
|
536
|
-
this.
|
|
553
|
+
this.emitReply(op);
|
|
537
554
|
return op.reply.slice();
|
|
538
555
|
}
|
|
556
|
+
/** The query as held and compared: its frame, every attachment group, the public evidence and KRAM's normalized proof. */
|
|
557
|
+
queryWire(envelope) {
|
|
558
|
+
return auxiliaryQueryWire(envelope, this.publicEvents(envelope.nests));
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* A copy of the accepted query: nothing is verified or derived again. If the reply exists it is queued again for
|
|
562
|
+
* a peer that lost it, at most once per {@link QUERY_RESEND_MS}.
|
|
563
|
+
*/
|
|
564
|
+
duplicateQuery(op) {
|
|
565
|
+
if (op.reply && this.now() - op.repliedAt >= QUERY_RESEND_MS)
|
|
566
|
+
this.emitReply(op);
|
|
567
|
+
return { kind: "accept" };
|
|
568
|
+
}
|
|
569
|
+
/** Queue the stored reply for the recipient and note when. */
|
|
570
|
+
emitReply(op) {
|
|
571
|
+
this.outgoing.push({ kin: "reply", dest: op.recipient, msg: op.reply.slice() });
|
|
572
|
+
op.repliedAt = this.now();
|
|
573
|
+
}
|
|
539
574
|
/** Independently vet openings before caching them; Revery owns signature threshold and BADA. */
|
|
540
575
|
processReply({ serder, diger, route, cigars = [], tsgs = [], attachments }) {
|
|
541
576
|
try {
|
|
@@ -5,6 +5,7 @@ import { Deck } from "../../core/deck.js";
|
|
|
5
5
|
import { ValidationError } from "../../core/errors.js";
|
|
6
6
|
import { exchange } from "../../core/exchange/protocol-exchanging.js";
|
|
7
7
|
import { Roles } from "../../core/roles.js";
|
|
8
|
+
import { holdsHeads, kelHeads } from "../mailbox/kel-publishing.js";
|
|
8
9
|
import { mailboxTopicKey } from "../mailbox/mailboxing.js";
|
|
9
10
|
import { transportMailboxTopic } from "../mailbox/topic-routing.js";
|
|
10
11
|
import { localGroupMember } from "../multisig/group-members.js";
|
|
@@ -690,6 +691,32 @@ export class Poster {
|
|
|
690
691
|
}
|
|
691
692
|
return remoteCesrTarget(endpoint, evt.msg);
|
|
692
693
|
}
|
|
694
|
+
/**
|
|
695
|
+
* Whether the configured relay is already known to hold what it needs to verify `signer`'s `/fwd` wrappers, so
|
|
696
|
+
* the whole-KEL introduction can be left out.
|
|
697
|
+
*
|
|
698
|
+
* The relay verifies a wrapper against the signer's current key state, which the signer's latest establishment
|
|
699
|
+
* event fixes; interaction events do not change it. The evidence is `Baser.kpub`, the record the KEL publisher
|
|
700
|
+
* keeps per (identifier, mailbox) of the establishment event whose KEL (delegation chain included, and only once a
|
|
701
|
+
* delegated establishment is anchored) the mailbox was last given. The relay is the signer's mailbox, so the
|
|
702
|
+
* publisher and the mailbox admin request keep that record. Any other endpoint, and any mismatch or missing
|
|
703
|
+
* record, keeps today's full introduction.
|
|
704
|
+
*
|
|
705
|
+
* Known limit: a 2xx is not acceptance, and a relay that lost its store answers a `/fwd` from a sender it no
|
|
706
|
+
* longer knows with the same 204 acknowledgement and drops it. Nothing in the `/fwd` path signals that today; the
|
|
707
|
+
* sender's next mailbox poll gets the relay's 403, which makes `KelPublisher` clear the record and repost the KEL.
|
|
708
|
+
* Wrappers sent in between are lost until then.
|
|
709
|
+
*
|
|
710
|
+
* @param signer Local habitat that signs the wrapper (a group's local member).
|
|
711
|
+
* @param eid Endpoint AID the prepared wire goes to.
|
|
712
|
+
* @returns True only when `eid` is the relay and the record holds `signer`'s current establishment heads.
|
|
713
|
+
*/
|
|
714
|
+
relayHoldsKel(signer, eid) {
|
|
715
|
+
if (!this.relay || this.relay.aid !== eid || !signer.pre)
|
|
716
|
+
return false;
|
|
717
|
+
const heads = kelHeads(this.hby, signer.pre);
|
|
718
|
+
return !!heads && holdsHeads(this.hby.db.kpub.get([signer.pre, eid]), heads);
|
|
719
|
+
}
|
|
693
720
|
/**
|
|
694
721
|
* Prepare complete signed wire and its introduction before opening transport.
|
|
695
722
|
*
|
|
@@ -726,7 +753,7 @@ export class Poster {
|
|
|
726
753
|
const forwarded = [];
|
|
727
754
|
for (const child of frames)
|
|
728
755
|
forwarded.push(yield* buildForwardedDelivery(signer, { recipient, topic, child }));
|
|
729
|
-
const intro = introduce(signer, endpoint.eid);
|
|
756
|
+
const intro = this.relayHoldsKel(signer, endpoint.eid) ? new Uint8Array() : introduce(signer, endpoint.eid);
|
|
730
757
|
let message = concatBytes(intro, ...forwarded);
|
|
731
758
|
if (evt.kin === "stream") {
|
|
732
759
|
// This node's own wrappers around already-decoded children: a parse failure here is a fault.
|
|
@@ -46,10 +46,12 @@ import { call } from "effection";
|
|
|
46
46
|
import { RegistryAuthor } from "../../acdc/authoring.js";
|
|
47
47
|
import { RegistryCustody } from "../../acdc/custody.js";
|
|
48
48
|
import { Dhke, grantTransactionId } from "../../acdc/dhking.js";
|
|
49
|
+
import { ThresholdQueryResend } from "../../acdc/query-resend.js";
|
|
49
50
|
import { vet } from "../../acdc/regeventing.js";
|
|
50
51
|
import { RegistryRoot } from "../../acdc/salting.js";
|
|
51
52
|
import { ValidationError } from "../../core/errors.js";
|
|
52
53
|
import { Kever } from "../../core/kel/kever.js";
|
|
54
|
+
import { multiKeyQueryWindow } from "../../core/mailbox/kram-policy.js";
|
|
53
55
|
import { messagize } from "../../core/protocol-serialization.js";
|
|
54
56
|
import { loadNests } from "../../db/exchange/exchange-nests.js";
|
|
55
57
|
import { timestampMicros } from "../../time/mod.js";
|
|
@@ -613,17 +615,12 @@ export class GroupIssuance {
|
|
|
613
615
|
writable: true,
|
|
614
616
|
value: void 0
|
|
615
617
|
});
|
|
616
|
-
|
|
618
|
+
/** The frozen threshold query and its resend schedule; a group cannot re-date it (`Dhke.requery` refuses groups). */
|
|
619
|
+
Object.defineProperty(this, "resend", {
|
|
617
620
|
enumerable: true,
|
|
618
621
|
configurable: true,
|
|
619
622
|
writable: true,
|
|
620
|
-
value:
|
|
621
|
-
});
|
|
622
|
-
Object.defineProperty(this, "nextQuery", {
|
|
623
|
-
enumerable: true,
|
|
624
|
-
configurable: true,
|
|
625
|
-
writable: true,
|
|
626
|
-
value: 0
|
|
623
|
+
value: new ThresholdQueryResend()
|
|
627
624
|
});
|
|
628
625
|
Object.defineProperty(this, "nextProofScan", {
|
|
629
626
|
enumerable: true,
|
|
@@ -651,6 +648,13 @@ export class GroupIssuance {
|
|
|
651
648
|
writable: true,
|
|
652
649
|
value: void 0
|
|
653
650
|
});
|
|
651
|
+
/** What the Grant send's proof support carried; absent when this operation did not send it (rebuilt after a restart). */
|
|
652
|
+
Object.defineProperty(this, "supportMark", {
|
|
653
|
+
enumerable: true,
|
|
654
|
+
configurable: true,
|
|
655
|
+
writable: true,
|
|
656
|
+
value: void 0
|
|
657
|
+
});
|
|
654
658
|
Object.defineProperty(this, "mode", {
|
|
655
659
|
enumerable: true,
|
|
656
660
|
configurable: true,
|
|
@@ -886,7 +890,9 @@ export function* stepGroup(ops, op) {
|
|
|
886
890
|
const input = deps.exc.retainedInput(grant, { pending: true });
|
|
887
891
|
const frame = parseBytes(messagize(input.serder, { tsgs: input.tsgs, cigars: input.cigars, gvrsn: Vrsn_2_0 }))[0];
|
|
888
892
|
frame.nests = [...input.nests ?? []];
|
|
889
|
-
const
|
|
893
|
+
const support = ops.discloser.proofSupport(g.nodeBytes, hab);
|
|
894
|
+
g.supportMark = ops.discloser.proofSupportMark(g.nodeBytes, hab);
|
|
895
|
+
const full = concatBytes(support, yield* wire(frame));
|
|
890
896
|
for (const peer of party.others())
|
|
891
897
|
yield* send(peer, full);
|
|
892
898
|
if (issuer) {
|
|
@@ -940,13 +946,17 @@ export function* stepGroup(ops, op) {
|
|
|
940
946
|
yield* broadcast(yield* party.wrap(child, MULTISIG_RPY_ROUTE));
|
|
941
947
|
}
|
|
942
948
|
else if (message.kin === "query")
|
|
943
|
-
g.
|
|
949
|
+
g.resend.set(message.msg);
|
|
944
950
|
else
|
|
945
951
|
yield* send(message.dest, message.msg, "credential");
|
|
946
952
|
}
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
953
|
+
// The unchanged query is resent with backoff while the holder can still accept it; the operation keeps waiting
|
|
954
|
+
// for the holder past that, to its own deadline.
|
|
955
|
+
const now = deps.clock.now(), base = deps.queryResendMs ?? ISSUANCE_QUERY_RESEND_MS;
|
|
956
|
+
if (g.resend.ready(now) && g.resend.fresh(now, multiKeyQueryWindow(deps.hby.db, Dhke.Route))
|
|
957
|
+
&& !dh.complete(xid)) {
|
|
958
|
+
yield* send(g.grant.sad.ri, g.resend.query, "credential");
|
|
959
|
+
g.resend.sent(now, base);
|
|
950
960
|
}
|
|
951
961
|
if (!lead || !dh.complete(xid))
|
|
952
962
|
return false;
|
|
@@ -980,7 +990,11 @@ export function* stepGroup(ops, op) {
|
|
|
980
990
|
if (!deps.exc.complete(grant))
|
|
981
991
|
throw new ValidationError("Issued group Grant remains unaccepted");
|
|
982
992
|
const completed = parseBytes(yield* storedBytes(deps.exc, grant))[0];
|
|
983
|
-
|
|
993
|
+
// The Grant send already carried the support; proving added the anchoring event and the registry update. An
|
|
994
|
+
// operation without a mark did not send it, so it sends in full.
|
|
995
|
+
const support = g.supportMark
|
|
996
|
+
? ops.discloser.proofSupportSince(g.nodeBytes, hab, g.supportMark)
|
|
997
|
+
: ops.discloser.proofSupport(g.nodeBytes, hab);
|
|
984
998
|
// KERIpy form: the elected member shares the proof with the other members by mailbox.
|
|
985
999
|
const wrapped = yield* party.wrap(completed, MULTISIG_IPEX_PROOF_ROUTE);
|
|
986
1000
|
// Kept locally, so the proof the members received is the one {@link groupIssuanceProof} exports.
|
|
@@ -235,6 +235,56 @@ export class IpexDiscloser {
|
|
|
235
235
|
* name. Reads accepted state only and signs nothing, so it needs no keys.
|
|
236
236
|
*/
|
|
237
237
|
proofSupportFor(raw, parties) {
|
|
238
|
+
const { ordered, registries } = this.supportSelection(raw, parties);
|
|
239
|
+
const messages = [Counter.makeGVC(Vrsn_2_0)];
|
|
240
|
+
// Keep first-seen authorizers and exact stored attachments, including superseded history.
|
|
241
|
+
// clonePreIter skips broken rows; finite disclosure must instead fail if an accepted row cannot be replayed.
|
|
242
|
+
for (const aid of ordered)
|
|
243
|
+
messages.push(...this.supportKel(aid, 0));
|
|
244
|
+
for (const regid of registries)
|
|
245
|
+
messages.push(this.registry.cloneTel(regid));
|
|
246
|
+
return concatBytes(...messages);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Record how far `proofSupport(raw, hab)` reaches right now: each KEL's last first-seen ordinal and each
|
|
250
|
+
* registry's last TEL ordinal. Call it in the same synchronous turn as the send it describes, so the mark is
|
|
251
|
+
* exactly what that send carried; {@link proofSupportSince} then sends what accepted state has gained.
|
|
252
|
+
*/
|
|
253
|
+
proofSupportMark(raw, hab) {
|
|
254
|
+
const { ordered, registries } = this.supportSelection(raw, [hab.pre]);
|
|
255
|
+
const kels = new Map(), tels = new Map();
|
|
256
|
+
for (const aid of ordered) {
|
|
257
|
+
let last = -1;
|
|
258
|
+
for (const [, fn] of this.hby.db.fels.getAllItemIter(aid))
|
|
259
|
+
last = fn;
|
|
260
|
+
kels.set(aid, last);
|
|
261
|
+
}
|
|
262
|
+
for (const regid of registries) {
|
|
263
|
+
let last = -1;
|
|
264
|
+
for (const [, sn] of this.registry.baser.tels.getAllItemIter(regid))
|
|
265
|
+
last = sn;
|
|
266
|
+
tels.set(regid, last);
|
|
267
|
+
}
|
|
268
|
+
return { kels, tels };
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* The part of `proofSupport(raw, hab)` that accepted state gained after `mark` was taken, in the same order and
|
|
272
|
+
* with the same stored attachments: later first-seen KEL events of each identifier (the anchoring interaction
|
|
273
|
+
* event of a registry update, say) and later TEL events of each registry. An identifier or registry the mark does
|
|
274
|
+
* not name (support that grew a new party) is carried whole. A recipient that holds what `mark` describes holds
|
|
275
|
+
* the same state after this as after a second full `proofSupport`.
|
|
276
|
+
*/
|
|
277
|
+
proofSupportSince(raw, hab, mark) {
|
|
278
|
+
const { ordered, registries } = this.supportSelection(raw, [hab.pre]);
|
|
279
|
+
const messages = [Counter.makeGVC(Vrsn_2_0)];
|
|
280
|
+
for (const aid of ordered)
|
|
281
|
+
messages.push(...this.supportKel(aid, (mark.kels.get(aid) ?? -1) + 1));
|
|
282
|
+
for (const regid of registries)
|
|
283
|
+
messages.push(this.registry.cloneTel(regid, (mark.tels.get(regid) ?? -1) + 1));
|
|
284
|
+
return concatBytes(...messages);
|
|
285
|
+
}
|
|
286
|
+
/** The identifiers (each after its delegation ancestry) and registries that support for `raw` and `parties` names. */
|
|
287
|
+
supportSelection(raw, parties) {
|
|
238
288
|
const aids = new Set(parties), registries = new Set();
|
|
239
289
|
for (const frame of parseBytes(raw)) {
|
|
240
290
|
if (!(frame.body instanceof SerderACDC))
|
|
@@ -256,7 +306,6 @@ export class IpexDiscloser {
|
|
|
256
306
|
aids.add(seal.i.qb64);
|
|
257
307
|
}
|
|
258
308
|
}
|
|
259
|
-
const messages = [Counter.makeGVC(Vrsn_2_0)];
|
|
260
309
|
const db = this.hby.db, ordered = new Set();
|
|
261
310
|
// An ancestry claim selects supporting KELs; it does not establish delegation authority.
|
|
262
311
|
for (const aid of aids) {
|
|
@@ -275,15 +324,15 @@ export class IpexDiscloser {
|
|
|
275
324
|
for (const ancestor of ancestors.reverse())
|
|
276
325
|
ordered.add(ancestor);
|
|
277
326
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
327
|
+
return { ordered, registries };
|
|
328
|
+
}
|
|
329
|
+
/** Stored first-seen events of `aid` from ordinal `from`, each with its exact stored attachments. */
|
|
330
|
+
supportKel(aid, from) {
|
|
331
|
+
const messages = [];
|
|
332
|
+
for (const [, fn, said] of this.hby.db.fels.getAllItemIter(aid, from)) {
|
|
333
|
+
messages.push(this.hby.db.cloneEvtMsg(aid, fn, said, Vrsn_2_0));
|
|
283
334
|
}
|
|
284
|
-
|
|
285
|
-
messages.push(this.registry.cloneTel(regid));
|
|
286
|
-
return concatBytes(...messages);
|
|
335
|
+
return messages;
|
|
287
336
|
}
|
|
288
337
|
/** Public replay includes actual stored witness receipts; it grants no new consent. */
|
|
289
338
|
kel(hab) {
|