keri-ts 2.2.0 → 2.2.1
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/custody.js +23 -0
- package/esm/keri/npm/src/app/ipex/group-ipex.js +14 -0
- package/esm/keri/npm/src/app/ipex/group-issuance.js +6 -3
- package/esm/keri/npm/src/app/ipex/ipex-grouping.js +16 -8
- package/esm/keri/npm/src/app/ipex/ipex-node-authentication.js +42 -18
- package/esm/keri/npm/src/app/ipex/issuance.js +9 -1
- package/esm/keri/npm/src/app/ipex/presenting.js +8 -1
- package/esm/keri/npm/src/app/ipex/unblinding-workflow.js +8 -1
- package/esm/keri/npm/src/app/multisig/counseling.js +7 -1
- package/esm/keri/npm/src/app/multisig/multisig-messaging.js +33 -6
- package/esm/keri/npm/src/app/multisig/multisig-workflows.js +5 -9
- package/esm/keri/npm/src/cli/credentials/ipex.js +7 -1
- package/esm/keri/npm/src/cli/messaging/group-join.js +7 -3
- package/esm/keri/npm/src/cli/messaging/multisig.js +37 -11
- package/esm/keri/npm/src/runtime/index.js +1 -1
- package/esm/keri/npm/src/version.js +2 -2
- package/package.json +1 -1
- package/types/acdc/custody.d.ts +7 -0
- package/types/acdc/custody.d.ts.map +1 -1
- package/types/app/ipex/group-ipex.d.ts +7 -0
- package/types/app/ipex/group-ipex.d.ts.map +1 -1
- package/types/app/ipex/group-issuance.d.ts +1 -1
- package/types/app/ipex/group-issuance.d.ts.map +1 -1
- package/types/app/ipex/ipex-grouping.d.ts.map +1 -1
- package/types/app/ipex/ipex-node-authentication.d.ts +9 -1
- package/types/app/ipex/ipex-node-authentication.d.ts.map +1 -1
- package/types/app/ipex/issuance.d.ts +2 -0
- package/types/app/ipex/issuance.d.ts.map +1 -1
- package/types/app/ipex/presenting.d.ts +2 -0
- package/types/app/ipex/presenting.d.ts.map +1 -1
- package/types/app/ipex/unblinding-workflow.d.ts +2 -0
- package/types/app/ipex/unblinding-workflow.d.ts.map +1 -1
- package/types/app/multisig/counseling.d.ts.map +1 -1
- package/types/app/multisig/multisig-messaging.d.ts +23 -6
- package/types/app/multisig/multisig-messaging.d.ts.map +1 -1
- package/types/app/multisig/multisig-workflows.d.ts.map +1 -1
- package/types/cli/credentials/ipex.d.ts.map +1 -1
- package/types/cli/messaging/group-join.d.ts +4 -1
- package/types/cli/messaging/group-join.d.ts.map +1 -1
- package/types/cli/messaging/multisig.d.ts +1 -0
- package/types/cli/messaging/multisig.d.ts.map +1 -1
- package/types/runtime/index.d.ts +2 -2
- package/types/runtime/index.d.ts.map +1 -1
- package/types/version.d.ts +2 -2
|
@@ -135,6 +135,29 @@ export class RegistryCustody {
|
|
|
135
135
|
this.options.store.pin(event.said, value);
|
|
136
136
|
return value;
|
|
137
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* The opening of the accepted update that issued `credential`, for a holder of the registry root that did not
|
|
140
|
+
* author the update and so retained no coordinates (a group member that consented to it). The opening is derived
|
|
141
|
+
* from the credential and the `issued` state and must open the update's own commitment; it is then retained like
|
|
142
|
+
* an authored one. Only an unbound issuance update is derived here.
|
|
143
|
+
*/
|
|
144
|
+
issuedOpening(event, credential) {
|
|
145
|
+
if (this.options.store.get(event.said))
|
|
146
|
+
return this.derivedOpening(event);
|
|
147
|
+
if (event.ilk !== "bup" || !event.verify())
|
|
148
|
+
throw new ValidationError("Exact accepted blinded update is required");
|
|
149
|
+
const ordinal = Number(BigInt(`0x${event.sad.n}`));
|
|
150
|
+
const stored = event.regid ? this.options.registry.seqEvent(event.regid, ordinal) : null;
|
|
151
|
+
if (!Number.isSafeInteger(ordinal) || !stored || !bytesEqual(stored.raw, event.raw)) {
|
|
152
|
+
throw new ValidationError("Exact accepted registry event is required");
|
|
153
|
+
}
|
|
154
|
+
const opening = this.derive(event.regid, ordinal, credential, "issued");
|
|
155
|
+
if (!vetBlind(opening, event.sad.b)) {
|
|
156
|
+
throw new ValidationError("The accepted update is not this credential's issuance");
|
|
157
|
+
}
|
|
158
|
+
this.retainDerivedUpdate(event, opening);
|
|
159
|
+
return opening;
|
|
160
|
+
}
|
|
138
161
|
/** Reproduce only the exact accepted event, never today's head or a later credential's coordinates. */
|
|
139
162
|
derivedOpening(event) {
|
|
140
163
|
if (event.ilk !== "bup" || !event.verify())
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { concatBytes, Counter, Vrsn_2_0 } from "cesr-ts";
|
|
25
25
|
import { ValidationError } from "../../core/errors.js";
|
|
26
|
+
import { loadNests } from "../../db/exchange/exchange-nests.js";
|
|
26
27
|
import { registryEventStatus, settleRegistryEvent } from "../credentials/registry-settling.js";
|
|
27
28
|
import { isLocalGroupHab } from "../multisig/group-members.js";
|
|
28
29
|
import { MULTISIG_EXN_ROUTE } from "../multisig/grouping.js";
|
|
@@ -30,6 +31,8 @@ import { RetryBackoff } from "../runtime/retry-backoff.js";
|
|
|
30
31
|
import { observeUntil } from "../runtime/workflow-contract.js";
|
|
31
32
|
import { IpexAdmitter } from "./ipex-admitting.js";
|
|
32
33
|
import { approveMultisigIpexLocally, approveStoredMultisigIpexWrappers, findStoredMultisigIpex, groupIpexApproved, prepareAcdcV2GroupIpex, publishGroupIpexProposal, publishInitialGroupIpex, requireAcdcV2GroupUse, sendCompletedGroupIpex, storedMultisigIpexBody, } from "./ipex-grouping.js";
|
|
34
|
+
import { isIssuanceOrigin } from "./ipex-node-authentication.js";
|
|
35
|
+
import { IPEX_GRANT_ROUTE } from "./ipexing.js";
|
|
33
36
|
/** Milliseconds between collections of the other members' stored contributions. */
|
|
34
37
|
export const GROUP_IPEX_COLLECT_MS = 250;
|
|
35
38
|
/** How long delivery failures may last before an operation ends `failed`; a group IPEX message has no deadline. */
|
|
@@ -186,6 +189,7 @@ export class GroupIpexOperations {
|
|
|
186
189
|
this.outcomes.set(op.said, {
|
|
187
190
|
messages: op.publication?.messages ?? [],
|
|
188
191
|
deliveries: op.publication?.deliveries ?? [],
|
|
192
|
+
members: op.publication?.members ?? [],
|
|
189
193
|
sent: op.deliveries,
|
|
190
194
|
});
|
|
191
195
|
this.deps.changes?.push({ kind: "ipex", id: op.said });
|
|
@@ -257,6 +261,9 @@ export function groupIpexProposals(ops, hab) {
|
|
|
257
261
|
continue;
|
|
258
262
|
if (found.some((item) => item.said === embedded.said))
|
|
259
263
|
continue;
|
|
264
|
+
if (embedded.route === IPEX_GRANT_ROUTE
|
|
265
|
+
&& isIssuanceOrigin(group, loadNests(hby.db, wrapper.said)[0]?.nests?.[0]))
|
|
266
|
+
continue;
|
|
260
267
|
found.push({
|
|
261
268
|
said: embedded.said,
|
|
262
269
|
route: embedded.route,
|
|
@@ -319,6 +326,13 @@ export function groupIpexStatus(ops, said) {
|
|
|
319
326
|
}
|
|
320
327
|
return { state: "active", phase: "awaitingConsent", needs: "approve" };
|
|
321
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* The member wrappers this process has published for one message: from the live operation once it published, from
|
|
331
|
+
* the outcome after it ended. Empty before publication. A pure read.
|
|
332
|
+
*/
|
|
333
|
+
export function groupIpexMessages(ops, said) {
|
|
334
|
+
return ops.live.get(said)?.publication?.messages ?? ops.outcomes.get(said)?.messages ?? [];
|
|
335
|
+
}
|
|
322
336
|
/** Observe until the message is accepted and delivered, or ended, or the budget expires. Starts no worker. */
|
|
323
337
|
export function* waitForGroupIpex(ops, said, { timeoutMs, clock = ops.deps.clock, until = (status) => status.state !== "active" }) {
|
|
324
338
|
const settled = yield* observeUntil(() => until(groupIpexStatus(ops, said)), { timeoutMs, clock });
|
|
@@ -45,10 +45,11 @@ import { timestampMicros } from "../../time/mod.js";
|
|
|
45
45
|
import { registryEventRef, registryEventStatus, settleRegistryEvent, } from "../credentials/registry-settling.js";
|
|
46
46
|
import { groupElectedMember, groupSigningMembers, isLocalGroupHab, latestGroupContribution, localGroupMember, } from "../multisig/group-members.js";
|
|
47
47
|
import { MULTISIG_EXN_ROUTE, MULTISIG_IPEX_PROOF_ROUTE, MULTISIG_QRY_ROUTE, MULTISIG_RPY_ROUTE, multisigExnBody, } from "../multisig/grouping.js";
|
|
48
|
+
import { remoteGroupMembers } from "../multisig/multisig-messaging.js";
|
|
48
49
|
import { proposeRegistryEvent } from "../multisig/proposing.js";
|
|
49
50
|
import { envelopeFromFrame } from "../protocol/parsering.js";
|
|
50
51
|
import { exchangeWire } from "./ipex-discloser.js";
|
|
51
|
-
import { acceptedRegistryEvidence } from "./ipex-node-authentication.js";
|
|
52
|
+
import { acceptedRegistryEvidence, isIssuanceOrigin } from "./ipex-node-authentication.js";
|
|
52
53
|
import { ISSUANCE_QUERY_RESEND_MS, } from "./issuance.js";
|
|
53
54
|
/** Milliseconds between scans of the stored wrappers for the final proof. */
|
|
54
55
|
const PROOF_SCAN_MS = 1000;
|
|
@@ -119,9 +120,9 @@ export class GroupParty {
|
|
|
119
120
|
get lead() {
|
|
120
121
|
return this.member.pre === this.custodian;
|
|
121
122
|
}
|
|
122
|
-
/** The other current signing members of the group. */
|
|
123
|
+
/** The other current signing members of the group that are not held in this Habery; local ones share its stores. */
|
|
123
124
|
others() {
|
|
124
|
-
return
|
|
125
|
+
return remoteGroupMembers(this.hby, this.member, groupSigningMembers(this.hby, this.group.pre));
|
|
125
126
|
}
|
|
126
127
|
/** The election this party stands for. */
|
|
127
128
|
election() {
|
|
@@ -269,6 +270,8 @@ export function groupIssuanceProposals(ops, group) {
|
|
|
269
270
|
? child.pre !== group.pre
|
|
270
271
|
: !hby.habs.get(child.pre) || !isLocalGroupHab(hby, hby.habs.get(child.pre))))
|
|
271
272
|
continue;
|
|
273
|
+
if (!isIssuanceOrigin(child.pre, loadNests(hby.db, parent.said)[0]?.nests?.[0]))
|
|
274
|
+
continue;
|
|
272
275
|
const self = localGroupMember(hby, child.pre).pre, known = found.get(child.said);
|
|
273
276
|
if (known && (parent.pre === self || known.proposer !== self))
|
|
274
277
|
continue;
|
|
@@ -36,15 +36,20 @@ function acdcV2GraphUse(hby, registry, origin, nests, walk) {
|
|
|
36
36
|
const graph = walkIpexGraph(origin, nests, walk);
|
|
37
37
|
if (!graph)
|
|
38
38
|
throw new ValidationError("ACDC v2 group graph is malformed or incomplete.");
|
|
39
|
-
const verdicts = new Map();
|
|
39
|
+
const verdicts = new Map(), why = [];
|
|
40
40
|
for (const [said, node] of graph.nodes) {
|
|
41
|
-
|
|
41
|
+
const authentication = authenticateIpexNode(node.body, envelopeFromFrame(node), hby.db, registry);
|
|
42
|
+
verdicts.set(said, issuedNodeVerdict(authentication));
|
|
43
|
+
if (authentication.kind !== "authenticated")
|
|
44
|
+
why.push(`${said} ${authentication.kind}: ${authentication.reason}`);
|
|
42
45
|
}
|
|
43
46
|
const verdict = evaluateChain(graph, verdicts, hby.db);
|
|
44
47
|
if (verdict?.kind === "unknown" && verdict.retryable)
|
|
45
48
|
return null;
|
|
46
|
-
if (verdict?.kind !== "valid")
|
|
47
|
-
|
|
49
|
+
if (verdict?.kind !== "valid") {
|
|
50
|
+
const found = !verdict ? "no verdict" : verdict.kind === "unknown" ? `unknown: ${verdict.reason}` : verdict.kind;
|
|
51
|
+
throw new ValidationError(`ACDC v2 group graph is false or unresolved (${found}${why.length ? `; ${why.join("; ")}` : ""}).`);
|
|
52
|
+
}
|
|
48
53
|
return true;
|
|
49
54
|
}
|
|
50
55
|
/** Current retained Grant authentication and graph truth are separate from protocol completion/history. */
|
|
@@ -176,12 +181,13 @@ export function* approveMultisigIpex(hby, protocol, postman, reger, wrapper, opt
|
|
|
176
181
|
const local = yield* approveMultisigIpexLocally(hby, protocol, reger, wrapper);
|
|
177
182
|
const { embedded, groupHab, genus, approved } = local;
|
|
178
183
|
const embeddedSaid = embedded.said;
|
|
179
|
-
if (!approved)
|
|
180
|
-
return { approved: null, accepted: protocol.exc.complete(embeddedSaid), messages: [], deliveries: [] };
|
|
184
|
+
if (!approved) {
|
|
185
|
+
return { approved: null, accepted: protocol.exc.complete(embeddedSaid), messages: [], deliveries: [], members: [] };
|
|
186
|
+
}
|
|
181
187
|
const publication = options.publish
|
|
182
188
|
? yield* publishGroupIpexProposal(hby, protocol, postman, groupHab, approved, genus)
|
|
183
|
-
: { messages: [], deliveries: [] };
|
|
184
|
-
const deliveries = publication.deliveries;
|
|
189
|
+
: { messages: [], deliveries: [], members: [] };
|
|
190
|
+
const deliveries = [...publication.deliveries];
|
|
185
191
|
if ((options.sendLead ?? true)
|
|
186
192
|
&& hby.db.exns.get([embeddedSaid])?.said === embeddedSaid
|
|
187
193
|
&& protocol.exc.lead(groupHab, embeddedSaid)) {
|
|
@@ -191,6 +197,7 @@ export function* approveMultisigIpex(hby, protocol, postman, reger, wrapper, opt
|
|
|
191
197
|
accepted: hby.db.exns.get([embeddedSaid])?.said === embeddedSaid,
|
|
192
198
|
messages: publication.messages,
|
|
193
199
|
deliveries,
|
|
200
|
+
members: publication.members,
|
|
194
201
|
approved,
|
|
195
202
|
};
|
|
196
203
|
}
|
|
@@ -430,6 +437,7 @@ export function* publishInitialGroupIpex(hby, protocol, postman, groupHab, embed
|
|
|
430
437
|
recipient: message.recipient,
|
|
431
438
|
exnSaid: message.exnSaid,
|
|
432
439
|
payloadSaid: message.payloadSaid,
|
|
440
|
+
outcome: "delivered",
|
|
433
441
|
deliveries: outcome.deliveries,
|
|
434
442
|
});
|
|
435
443
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/** Node issuer authentication over public KEL/TEL evidence; no conversation or validity policy. */
|
|
2
|
-
import { Diger, NumberPrimitive, Prefixer } from "cesr-ts";
|
|
2
|
+
import { Diger, NumberPrimitive, Prefixer, SerderACDC } from "cesr-ts";
|
|
3
3
|
import { validateRip, validateUpdate, vetBinds, vetBlind, vetBound, } from "../../acdc/regeventing.js";
|
|
4
4
|
import { TransIdxSigGroup } from "../../core/dispatch.js";
|
|
5
5
|
import { DatabaseOperationError } from "../../core/errors.js";
|
|
6
6
|
import { verifyExchangeProofs } from "../../core/exchange/exchange-proofing.js";
|
|
7
|
+
import { envelopeFromFrame } from "../protocol/parsering.js";
|
|
7
8
|
/** Protocol admission authenticates registry observations without imposing an application state vocabulary. */
|
|
8
9
|
export function authenticatedNodeVerdict(authentication) {
|
|
9
10
|
switch (authentication.kind) {
|
|
@@ -65,35 +66,57 @@ export function acceptedRegistryEvidence(store, regid) {
|
|
|
65
66
|
}
|
|
66
67
|
return { rip, updates };
|
|
67
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Whether a Grant's origin node is a fresh issuance: a credential its own sender issues, disclosed with no registry
|
|
71
|
+
* proof because no registry update has issued it yet. A presentation of an issued credential carries the proof
|
|
72
|
+
* that opens its registry update, so it never matches. Both are Grants on the wire; only a fresh one is consented to
|
|
73
|
+
* as an issuance. The answer is the same before and after the registry update is approved.
|
|
74
|
+
*/
|
|
75
|
+
export function isIssuanceOrigin(sender, origin) {
|
|
76
|
+
const body = origin?.body;
|
|
77
|
+
if (!(body instanceof SerderACDC) || body.issuer !== sender || !body.regid)
|
|
78
|
+
return false;
|
|
79
|
+
const { bsqs, bsss } = envelopeFromFrame(origin);
|
|
80
|
+
return !bsqs.length && !bsss.length;
|
|
81
|
+
}
|
|
68
82
|
/** A declared registry, then supplied anchors, take precedence over current signatures. */
|
|
69
83
|
export function authenticateIpexNode(serder, envelope, db, registry, historical = false) {
|
|
70
84
|
if (serder.regid) {
|
|
71
85
|
const proofs = [...envelope.bsqs, ...envelope.bsss];
|
|
72
|
-
if (!proofs.length || !registry)
|
|
73
|
-
return {
|
|
86
|
+
if (!proofs.length || !registry) {
|
|
87
|
+
return {
|
|
88
|
+
kind: "reject",
|
|
89
|
+
reason: "The node declares a registry but carries no issuer proof or no registry store is open.",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
74
92
|
const chain = acceptedRegistryEvidence(registry, serder.regid);
|
|
75
93
|
if (!chain)
|
|
76
94
|
return { kind: "missing", reason: `Missing local TEL evidence for ${serder.regid}.` };
|
|
77
95
|
// A RIP alone authenticates ownership, never a disclosed credential state.
|
|
78
|
-
if (!chain.updates.length)
|
|
79
|
-
return {
|
|
96
|
+
if (!chain.updates.length) {
|
|
97
|
+
return {
|
|
98
|
+
kind: "reject",
|
|
99
|
+
reason: "The registry has only its inception; a RIP alone authenticates ownership, never a disclosed credential state.",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
80
102
|
let at;
|
|
81
103
|
if (historical) {
|
|
82
104
|
if (!serder.verify() || serder.issuer !== chain.rip.issuer || serder.regid !== chain.rip.said) {
|
|
83
|
-
return { kind: "reject" };
|
|
105
|
+
return { kind: "reject", reason: "The node differs from the registry inception it declares." };
|
|
84
106
|
}
|
|
85
107
|
// Authenticate all supplied bodies/duplicates and known bound contradictions
|
|
86
108
|
// before missing evidence wins, independently of attachment order.
|
|
87
109
|
if (proofs.some((proof) => !vetBlind(proof, proof.d.nonce))
|
|
88
110
|
|| new Set(proofs.map((proof) => proof.d.nonce)).size !== proofs.length)
|
|
89
|
-
return { kind: "reject" };
|
|
111
|
+
return { kind: "reject", reason: "The disclosed issuer proofs are malformed or repeat a nonce." };
|
|
90
112
|
let unresolved = false;
|
|
91
113
|
for (const proof of proofs) {
|
|
92
114
|
if (proof.td.nonce && proof.td.nonce !== serder.said)
|
|
93
115
|
continue;
|
|
94
116
|
const bound = vetBound(proof, serder, db);
|
|
95
|
-
if (bound?.kind === "reject")
|
|
96
|
-
return { kind: "reject" };
|
|
117
|
+
if (bound?.kind === "reject") {
|
|
118
|
+
return { kind: "reject", reason: "A disclosed issuer proof contradicts the node." };
|
|
119
|
+
}
|
|
97
120
|
if (bound)
|
|
98
121
|
unresolved = true;
|
|
99
122
|
}
|
|
@@ -103,7 +126,7 @@ export function authenticateIpexNode(serder, envelope, db, registry, historical
|
|
|
103
126
|
continue;
|
|
104
127
|
const matches = chain.updates.filter((event) => event.sad.b === proof.d.nonce);
|
|
105
128
|
if (matches.length > 1)
|
|
106
|
-
return { kind: "reject" };
|
|
129
|
+
return { kind: "reject", reason: "Several registry updates match one issuer proof." };
|
|
107
130
|
if (matches.length)
|
|
108
131
|
selected.push(matches[0]);
|
|
109
132
|
else
|
|
@@ -111,8 +134,9 @@ export function authenticateIpexNode(serder, envelope, db, registry, historical
|
|
|
111
134
|
}
|
|
112
135
|
if (unresolved)
|
|
113
136
|
return { kind: "missing", reason: `Missing retained issuer proof evidence for ${serder.said}.` };
|
|
114
|
-
if (!selected.length)
|
|
115
|
-
return { kind: "reject" };
|
|
137
|
+
if (!selected.length) {
|
|
138
|
+
return { kind: "reject", reason: "No registry update matches the disclosed issuer proofs." };
|
|
139
|
+
}
|
|
116
140
|
const candidates = new Set(selected.map((event) => event.said));
|
|
117
141
|
// Accepted history is ascending. Vacuous and other-target openings cannot
|
|
118
142
|
// advance this issuer prefix; generic source locators never select it.
|
|
@@ -131,7 +155,7 @@ export function authenticateIpexNode(serder, envelope, db, registry, historical
|
|
|
131
155
|
}
|
|
132
156
|
return result.kind === "verified" && result.state.regid === serder.regid
|
|
133
157
|
? { kind: "authenticated", registration: result.state }
|
|
134
|
-
: { kind: "reject" };
|
|
158
|
+
: { kind: "reject", reason: "The registry update does not bind the node." };
|
|
135
159
|
}
|
|
136
160
|
// Disclosed inner rd without top-level rd is an unsupported hidden issuer registry.
|
|
137
161
|
// Compact aggregate entries expose no declaration and remain opaque here.
|
|
@@ -139,15 +163,15 @@ export function authenticateIpexNode(serder, envelope, db, registry, historical
|
|
|
139
163
|
const sections = [serder.sad.a, ...(Array.isArray(aggregate) ? aggregate.slice(1) : [aggregate])];
|
|
140
164
|
if (sections.some(section => section && typeof section === "object" && !Array.isArray(section)
|
|
141
165
|
&& section.rd))
|
|
142
|
-
return { kind: "reject" };
|
|
166
|
+
return { kind: "reject", reason: "A disclosed inner registry has no top-level declaration." };
|
|
143
167
|
if (!serder.issuer)
|
|
144
|
-
return { kind: "reject" };
|
|
168
|
+
return { kind: "reject", reason: "The node names no issuer." };
|
|
145
169
|
let issuer;
|
|
146
170
|
try {
|
|
147
171
|
issuer = new Prefixer({ qb64: serder.issuer });
|
|
148
172
|
}
|
|
149
173
|
catch {
|
|
150
|
-
return { kind: "reject" };
|
|
174
|
+
return { kind: "reject", reason: "The node names a malformed issuer." };
|
|
151
175
|
}
|
|
152
176
|
const sourceSeals = [
|
|
153
177
|
...envelope.sscs.map((seal) => ({ i: issuer, ...seal })),
|
|
@@ -159,7 +183,7 @@ export function authenticateIpexNode(serder, envelope, db, registry, historical
|
|
|
159
183
|
? { kind: "missing", reason: `Missing issuer KEL anchor for ${serder.said}.` }
|
|
160
184
|
: result.sourceSeals.length > 0
|
|
161
185
|
? { kind: "authenticated", registration: null }
|
|
162
|
-
: { kind: "reject" };
|
|
186
|
+
: { kind: "reject", reason: "No issuer KEL anchor seals the node." };
|
|
163
187
|
}
|
|
164
188
|
const explicit = envelope.tsgs.filter((group) => group.pre === issuer.qb64);
|
|
165
189
|
const last = envelope.ssgs.filter((group) => group.pre === issuer.qb64);
|
|
@@ -181,5 +205,5 @@ export function authenticateIpexNode(serder, envelope, db, registry, historical
|
|
|
181
205
|
? { kind: "missing", reason: `Missing current issuer KEL evidence for ${serder.said}.` }
|
|
182
206
|
: result.tsgs.length + result.cigars.length > 0
|
|
183
207
|
? { kind: "authenticated", registration: null }
|
|
184
|
-
: { kind: "reject" };
|
|
208
|
+
: { kind: "reject", reason: "No current issuer signature authenticates the node." };
|
|
185
209
|
}
|
|
@@ -393,6 +393,10 @@ export class IssuanceOperations {
|
|
|
393
393
|
deps.changes?.push({ kind: "issuance", id: group.said });
|
|
394
394
|
return { grant: group.said, xid };
|
|
395
395
|
}
|
|
396
|
+
/** Whether the live operation for `grant` still holds a message the transport has not taken. */
|
|
397
|
+
owes(grant) {
|
|
398
|
+
return (this.live.get(grant)?.outbox.length ?? 0) > 0;
|
|
399
|
+
}
|
|
396
400
|
/** One pass of the issuer loop, in the order of the CLI loop it replaces. */
|
|
397
401
|
*step(op) {
|
|
398
402
|
if (op.group) {
|
|
@@ -776,7 +780,11 @@ function heldRoot(deps, hab, grant) {
|
|
|
776
780
|
* worker and drives nothing: someone else must call `processOnce`.
|
|
777
781
|
*/
|
|
778
782
|
export function* waitForIssuance(ops, grant, { timeoutMs, clock = ops.deps.clock, until = (status) => status.state !== "active" }) {
|
|
779
|
-
|
|
783
|
+
// A phase is not reached while the step that produced it still owes a delivery: the host would stop its workers first.
|
|
784
|
+
const settled = yield* observeUntil(() => until(issuanceStatus(ops, grant)) && !ops.owes(grant), {
|
|
785
|
+
timeoutMs,
|
|
786
|
+
clock,
|
|
787
|
+
});
|
|
780
788
|
const status = issuanceStatus(ops, grant);
|
|
781
789
|
return settled ? { outcome: "settled", status } : { outcome: "timed-out", status, timeoutMs };
|
|
782
790
|
}
|
|
@@ -87,6 +87,10 @@ export class PresentationOperations {
|
|
|
87
87
|
value: new Map()
|
|
88
88
|
});
|
|
89
89
|
}
|
|
90
|
+
/** Whether the live presentation `said` still holds a message the transport has not taken. */
|
|
91
|
+
owes(said) {
|
|
92
|
+
return (this.live.get(said)?.outbox.length ?? 0) > 0;
|
|
93
|
+
}
|
|
90
94
|
/** Whether a delivery is live. */
|
|
91
95
|
active() {
|
|
92
96
|
return this.live.size > 0;
|
|
@@ -340,7 +344,10 @@ export function presentationStatus(ops, said) {
|
|
|
340
344
|
* nothing: someone else must call `processOnce`.
|
|
341
345
|
*/
|
|
342
346
|
export function* waitForPresentation(ops, said, { timeoutMs, clock = ops.deps.clock, until = (status) => status.state !== "active" }) {
|
|
343
|
-
const settled = yield* observeUntil(() => until(presentationStatus(ops, said))
|
|
347
|
+
const settled = yield* observeUntil(() => until(presentationStatus(ops, said)) && !ops.owes(said), {
|
|
348
|
+
timeoutMs,
|
|
349
|
+
clock,
|
|
350
|
+
});
|
|
344
351
|
const status = presentationStatus(ops, said);
|
|
345
352
|
return settled ? { outcome: "settled", status } : { outcome: "timed-out", status, timeoutMs };
|
|
346
353
|
}
|
|
@@ -135,6 +135,10 @@ export class UnblindingOperations {
|
|
|
135
135
|
value: null
|
|
136
136
|
});
|
|
137
137
|
}
|
|
138
|
+
/** Whether the live operation for `grant` still holds a message the transport has not taken. */
|
|
139
|
+
owes(grant) {
|
|
140
|
+
return (this.live.get(grant)?.outbox.length ?? 0) > 0;
|
|
141
|
+
}
|
|
138
142
|
/**
|
|
139
143
|
* Whether a pass has work: a live operation, or an owner holds an authenticated peer query whose reply is
|
|
140
144
|
* owed (an in-memory walk of the owners, no store read).
|
|
@@ -635,7 +639,10 @@ export function unblindingStatus(ops, grant) {
|
|
|
635
639
|
* someone else must call `processOnce`.
|
|
636
640
|
*/
|
|
637
641
|
export function* waitForUnblinding(ops, grant, { timeoutMs, clock = ops.deps.clock }) {
|
|
638
|
-
const settled = yield* observeUntil(() => unblindingStatus(ops, grant).state !== "active"
|
|
642
|
+
const settled = yield* observeUntil(() => unblindingStatus(ops, grant).state !== "active" && !ops.owes(grant), {
|
|
643
|
+
timeoutMs,
|
|
644
|
+
clock,
|
|
645
|
+
});
|
|
639
646
|
const status = unblindingStatus(ops, grant);
|
|
640
647
|
return settled ? { outcome: "settled", status } : { outcome: "timed-out", status, timeoutMs };
|
|
641
648
|
}
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* restart never leaves a delegated event waiting for one. Collaborators are
|
|
36
36
|
* narrow (ADR-0013); nothing here reaches a runtime.
|
|
37
37
|
*/
|
|
38
|
-
import { Diger, Ilks, NumberPrimitive, NumDex, Verfer } from "cesr-ts";
|
|
38
|
+
import { Diger, Ilks, NumberPrimitive, NumDex, ParserError, Verfer } from "cesr-ts";
|
|
39
39
|
import { ValidationError } from "../../core/errors.js";
|
|
40
40
|
import { Kever } from "../../core/kel/kever.js";
|
|
41
41
|
import { dgKey } from "../../db/core/keys.js";
|
|
@@ -394,6 +394,12 @@ export class Counselor {
|
|
|
394
394
|
// A witness refusing or being unreachable is a remote outcome: report it in status and retry later.
|
|
395
395
|
run.failure = error instanceof Error ? error.message : String(error);
|
|
396
396
|
run.delay = Math.min(run.delay * 2, this.receiptRetry.maxMs);
|
|
397
|
+
// A witness that answered with a reply this member cannot read (a receipt the elected member already
|
|
398
|
+
// obtained) will not answer better later: submit on the next attempt instead of waiting out the grace.
|
|
399
|
+
if (!submit && error instanceof ParserError) {
|
|
400
|
+
run.fallbackAfterMs = 0;
|
|
401
|
+
run.delay = this.receiptRetry.baseMs;
|
|
402
|
+
}
|
|
397
403
|
}
|
|
398
404
|
run.nextAt = now + run.delay;
|
|
399
405
|
if (!this.hby.db.fullyWitnessed(serder))
|
|
@@ -142,23 +142,50 @@ export function* sendGroupMessage(postman, prepared) {
|
|
|
142
142
|
recipient: prepared.recipient,
|
|
143
143
|
exnSaid: prepared.exnSaid,
|
|
144
144
|
payloadSaid: prepared.payloadSaid,
|
|
145
|
+
outcome: "delivered",
|
|
145
146
|
deliveries: [...outcome.deliveries],
|
|
146
147
|
};
|
|
147
148
|
}
|
|
148
|
-
/**
|
|
149
|
+
/** The outcome of sending `prepared` as a failed delivery with the transport's reason. */
|
|
150
|
+
export function failedGroupDelivery(prepared, error) {
|
|
151
|
+
return {
|
|
152
|
+
recipient: prepared.recipient,
|
|
153
|
+
exnSaid: prepared.exnSaid,
|
|
154
|
+
payloadSaid: prepared.payloadSaid,
|
|
155
|
+
outcome: "failed",
|
|
156
|
+
deliveries: [],
|
|
157
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Like {@link sendGroupMessage}, but a transport failure is this member's outcome and not an error: the wrapper
|
|
162
|
+
* is already accepted locally, so one unreachable member must not stop the others or the caller's next step.
|
|
163
|
+
*/
|
|
164
|
+
export function* deliverGroupMessage(postman, prepared) {
|
|
165
|
+
try {
|
|
166
|
+
return yield* sendGroupMessage(postman, prepared);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
return failedGroupDelivery(prepared, error);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/** Collect recipient outcomes: delivered message identities and completed target URLs, and every member's outcome. */
|
|
149
173
|
export function groupSendResult(outcomes) {
|
|
174
|
+
const delivered = outcomes.filter(outcome => outcome.outcome === "delivered");
|
|
150
175
|
return {
|
|
151
|
-
messages:
|
|
152
|
-
deliveries:
|
|
176
|
+
messages: delivered.map(({ recipient, exnSaid, payloadSaid }) => ({ recipient, exnSaid, payloadSaid })),
|
|
177
|
+
deliveries: delivered.flatMap(outcome => outcome.deliveries),
|
|
178
|
+
members: [...outcomes],
|
|
153
179
|
};
|
|
154
180
|
}
|
|
155
181
|
/**
|
|
156
182
|
* Process the exact signed local contribution before admitting remote delivery,
|
|
157
183
|
* so an immediate peer reply cannot outrun local Multiplexor evidence. Await
|
|
158
|
-
* normal intake, then send the same bytes without signing again. Intake
|
|
159
|
-
*
|
|
184
|
+
* normal intake, then send the same bytes without signing again. Intake failures
|
|
185
|
+
* propagate; a send failure is the member's `failed` outcome. No cross-store/network
|
|
186
|
+
* transaction is implied.
|
|
160
187
|
*/
|
|
161
188
|
export function* admitAndSendGroupMessage(input, postman, prepared) {
|
|
162
189
|
yield* input.processBytes(prepared.message, { local: true });
|
|
163
|
-
return yield*
|
|
190
|
+
return yield* deliverGroupMessage(postman, prepared);
|
|
164
191
|
}
|
|
@@ -25,7 +25,7 @@ import { abortable } from "../runtime/abortable.js";
|
|
|
25
25
|
import { collectBytes } from "../transport/byte-source.js";
|
|
26
26
|
import { findLocalGroupMember as findLocalMember, groupMemberHasKey, uniqueMembers } from "./group-members.js";
|
|
27
27
|
import { acdcV2RegistryProposal, MULTISIG_ICP_ROUTE, MULTISIG_ISS_ROUTE, MULTISIG_IXN_ROUTE, MULTISIG_REV_ROUTE, MULTISIG_ROT_ROUTE, MULTISIG_RPY_ROUTE, MULTISIG_VCP_ROUTE, multisigChild, multisigChildEvidence, multisigChildGenusAttrs, prepareAcdcV2RegistryExn, prepareMultisigEventExn, prepareMultisigVdrExn, registryProposalEvent, } from "./grouping.js";
|
|
28
|
-
import { admitAndSendGroupMessage, groupSendResult, prepareGroupMessage, remoteGroupMembers, signGroupMessage, storedGroupMessage, } from "./multisig-messaging.js";
|
|
28
|
+
import { admitAndSendGroupMessage, failedGroupDelivery, groupSendResult, prepareGroupMessage, remoteGroupMembers, signGroupMessage, storedGroupMessage, } from "./multisig-messaging.js";
|
|
29
29
|
/** Approve one exact ACDC v2 TEL/anchor through existing member signing and ordinary processors.
|
|
30
30
|
* A returned wrapper records consent, not quorum/witness/TEL completion; retry the accepted-state owners.
|
|
31
31
|
* `resume` reuses this member's recorded anchor signatures after a crash instead of signing again.
|
|
@@ -135,14 +135,10 @@ export function* publishAcdcV2Registry(hby, postman, group, approval) {
|
|
|
135
135
|
notify: true,
|
|
136
136
|
});
|
|
137
137
|
const outcome = yield* postman.wait(evt);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
exnSaid: approval[0].said,
|
|
143
|
-
payloadSaid: approval[0].sad.a.anc,
|
|
144
|
-
deliveries: outcome.deliveries,
|
|
145
|
-
});
|
|
138
|
+
const ref = { recipient, exnSaid: approval[0].said, payloadSaid: approval[0].sad.a.anc };
|
|
139
|
+
outcomes.push(outcome.kin === "failed"
|
|
140
|
+
? failedGroupDelivery(ref, outcome.error)
|
|
141
|
+
: { ...ref, outcome: "delivered", deliveries: outcome.deliveries });
|
|
146
142
|
}
|
|
147
143
|
return groupSendResult(outcomes);
|
|
148
144
|
}
|
|
@@ -372,7 +372,10 @@ export function* joinIpexProposal(hby, runtime, reger, ipexArgs) {
|
|
|
372
372
|
throw new ValidationError(`Multisig IPEX sender ${group ?? "<missing>"} is not a local group AID.`);
|
|
373
373
|
}
|
|
374
374
|
if (!ipexArgs.auto) {
|
|
375
|
-
return {
|
|
375
|
+
return {
|
|
376
|
+
exn,
|
|
377
|
+
group: { embedded, accepted: null, approved: null, lead: null, messages: [], deliveries: [], members: [] },
|
|
378
|
+
};
|
|
376
379
|
}
|
|
377
380
|
if (embedded.pvrsn.major === 2) {
|
|
378
381
|
// ACDC v2: the runtime's group IPEX owner approves, publishes, collects and lets the lead deliver.
|
|
@@ -393,6 +396,7 @@ export function* joinIpexProposal(hby, runtime, reger, ipexArgs) {
|
|
|
393
396
|
lead: runtime.exc.lead(hby.habs.get(group), said),
|
|
394
397
|
messages: outcome?.messages ?? [],
|
|
395
398
|
deliveries: [...(outcome?.deliveries ?? []), ...(outcome?.sent ?? [])],
|
|
399
|
+
members: outcome?.members ?? [],
|
|
396
400
|
},
|
|
397
401
|
};
|
|
398
402
|
}
|
|
@@ -416,6 +420,7 @@ export function* joinIpexProposal(hby, runtime, reger, ipexArgs) {
|
|
|
416
420
|
credentialSaved,
|
|
417
421
|
messages: [],
|
|
418
422
|
deliveries: [],
|
|
423
|
+
members: [],
|
|
419
424
|
},
|
|
420
425
|
};
|
|
421
426
|
}
|
|
@@ -437,6 +442,7 @@ export function* joinIpexProposal(hby, runtime, reger, ipexArgs) {
|
|
|
437
442
|
credentialSaved,
|
|
438
443
|
messages: approval.messages,
|
|
439
444
|
deliveries: approval.deliveries,
|
|
445
|
+
members: approval.members,
|
|
440
446
|
},
|
|
441
447
|
};
|
|
442
448
|
}
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Join approves exactly one stored proposal, the first the user consents to:
|
|
5
5
|
* - an explicit `--said` names the proposal, otherwise the unapproved proposals
|
|
6
|
-
* the notice store announces are offered in notice order;
|
|
6
|
+
* the notice store announces are offered in notice order; `--kind` restricts the
|
|
7
|
+
* offer (and a `--said`) to one proposal kind, so a join never signs another kind;
|
|
7
8
|
* - without `--auto` every offered proposal is shown and approved only on a yes;
|
|
8
9
|
* - with `--auto` the first offered proposal is approved without a prompt. This is
|
|
9
10
|
* the whole of auto consent: it exists only inside the `join` the user ran for
|
|
@@ -19,17 +20,20 @@ function joinedGroup(hby, group) {
|
|
|
19
20
|
return group === undefined ? undefined : (hby.habByName(group) ?? hby.habs.get(group))?.pre ?? undefined;
|
|
20
21
|
}
|
|
21
22
|
/** Candidates for one pass, oldest first. */
|
|
22
|
-
function candidates(deps, { said, group }) {
|
|
23
|
+
function candidates(deps, { said, group, kind }) {
|
|
23
24
|
const wanted = joinedGroup(deps.hby, group);
|
|
24
25
|
if (said !== undefined) {
|
|
25
26
|
const proposal = findGroupProposal(deps, said);
|
|
27
|
+
if (proposal && kind !== undefined && proposal.kind !== kind) {
|
|
28
|
+
throw new ValidationError(`Multisig exchange ${said} is a ${proposal.kind} proposal, not ${kind}.`);
|
|
29
|
+
}
|
|
26
30
|
if (proposal && wanted !== undefined && proposal.group !== wanted) {
|
|
27
31
|
throw new ValidationError(`Multisig exchange ${said} belongs to group ${proposal.group}, not ${wanted}.`);
|
|
28
32
|
}
|
|
29
33
|
return proposal ? [proposal] : [];
|
|
30
34
|
}
|
|
31
35
|
return listGroupProposals(deps, wanted === undefined ? {} : { group: wanted })
|
|
32
|
-
.filter(proposal => proposal.local === "unapproved");
|
|
36
|
+
.filter(proposal => proposal.local === "unapproved" && (kind === undefined || proposal.kind === kind));
|
|
33
37
|
}
|
|
34
38
|
/**
|
|
35
39
|
* Approve the first proposal the user consents to, polling stored proposals until
|