@integraledger/lcp-verify 0.9.0
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/CHANGELOG.md +13 -0
- package/LICENSE +202 -0
- package/NOTICE +14 -0
- package/README.md +116 -0
- package/dist/composition.d.ts +53 -0
- package/dist/composition.d.ts.map +1 -0
- package/dist/composition.js +54 -0
- package/dist/composition.js.map +1 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +100 -0
- package/dist/index.js.map +1 -0
- package/dist/report.d.ts +72 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +38 -0
- package/dist/report.js.map +1 -0
- package/dist/required.d.ts +47 -0
- package/dist/required.d.ts.map +1 -0
- package/dist/required.js +63 -0
- package/dist/required.js.map +1 -0
- package/dist/steps.d.ts +182 -0
- package/dist/steps.d.ts.map +1 -0
- package/dist/steps.js +404 -0
- package/dist/steps.js.map +1 -0
- package/package.json +61 -0
- package/src/composition.ts +100 -0
- package/src/index.ts +277 -0
- package/src/report.ts +100 -0
- package/src/required.ts +99 -0
- package/src/steps.ts +517 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `verify` — the walk at the caller-specified `depth` (default `"structural"`). Pure over
|
|
3
|
+
* its supplied inputs (the fetched ATR bytes, the enumerated settlements, the ATA chain, the acceptance)
|
|
4
|
+
* — the imperative shell that fetches them via live ports is the buyer gate's. The report is honest about
|
|
5
|
+
* coverage: a step whose inputs are absent is `not-attempted(<why>)`.
|
|
6
|
+
*
|
|
7
|
+
* At STRUCTURAL depth `verified` is always `false` (a presence/absence readout → class, not a verdict).
|
|
8
|
+
* At MECHANICAL depth `verified` is an honest function of the walk via `computeVerified` — raised `true`
|
|
9
|
+
* iff every class-required step is `proved` and no step `failed`.
|
|
10
|
+
*
|
|
11
|
+
* `supportedClass` is an HONEST READOUT, never a target: the record is *shaped for* `claimedClass`, and
|
|
12
|
+
* the structural walk can only CONFIRM-OR-IMPEACH that shape — any `failed` step downgrades it to `TC-0`;
|
|
13
|
+
* the walk never raises it (raising requires the mechanical verification `computeVerified` supplies).
|
|
14
|
+
*/
|
|
15
|
+
import type {
|
|
16
|
+
Bounds,
|
|
17
|
+
ChainWalkResult,
|
|
18
|
+
SignatureVerifier,
|
|
19
|
+
SignedAcceptance,
|
|
20
|
+
} from "@integraledger/lcp-authority";
|
|
21
|
+
import {
|
|
22
|
+
type CompositionInput,
|
|
23
|
+
discoveryIntegrityStep,
|
|
24
|
+
frcNonGatingStep,
|
|
25
|
+
offerBoundStep,
|
|
26
|
+
operationsStep,
|
|
27
|
+
proportionalityStep,
|
|
28
|
+
} from "./composition.js";
|
|
29
|
+
import type {
|
|
30
|
+
StepOutcome,
|
|
31
|
+
VerificationReport,
|
|
32
|
+
VerificationStep,
|
|
33
|
+
VerifyDepth,
|
|
34
|
+
} from "./report.js";
|
|
35
|
+
import {
|
|
36
|
+
computeVerified,
|
|
37
|
+
type StepName,
|
|
38
|
+
type TransactionClass,
|
|
39
|
+
} from "./required.js";
|
|
40
|
+
import {
|
|
41
|
+
type AuthorityLink,
|
|
42
|
+
acceptanceStep,
|
|
43
|
+
authorityStep,
|
|
44
|
+
authorityStepFromWalk,
|
|
45
|
+
commitmentStep,
|
|
46
|
+
fingerprintStep,
|
|
47
|
+
type PlacementInput,
|
|
48
|
+
type RecordIdentity,
|
|
49
|
+
recourseStep,
|
|
50
|
+
referencePlacementStep,
|
|
51
|
+
resolvePartyStep,
|
|
52
|
+
settlementStep,
|
|
53
|
+
} from "./steps.js";
|
|
54
|
+
|
|
55
|
+
export {
|
|
56
|
+
type CompositionInput,
|
|
57
|
+
type CompositionReadout,
|
|
58
|
+
discoveryIntegrityStep,
|
|
59
|
+
frcNonGatingStep,
|
|
60
|
+
offerBoundStep,
|
|
61
|
+
operationsStep,
|
|
62
|
+
proportionalityStep,
|
|
63
|
+
readCompositionSlots,
|
|
64
|
+
} from "./composition.js";
|
|
65
|
+
export {
|
|
66
|
+
jcsCanonicalize,
|
|
67
|
+
type StepOutcome,
|
|
68
|
+
serializeReport,
|
|
69
|
+
type VerificationReport,
|
|
70
|
+
type VerificationStep,
|
|
71
|
+
type VerifyDepth,
|
|
72
|
+
} from "./report.js";
|
|
73
|
+
export {
|
|
74
|
+
computeVerified,
|
|
75
|
+
REQUIRED_STEPS,
|
|
76
|
+
type StepName,
|
|
77
|
+
type TransactionClass,
|
|
78
|
+
} from "./required.js";
|
|
79
|
+
export {
|
|
80
|
+
type AuthorityLink,
|
|
81
|
+
acceptanceStep,
|
|
82
|
+
authorityStep,
|
|
83
|
+
authorityStepFromWalk,
|
|
84
|
+
commitmentStep,
|
|
85
|
+
fingerprintStep,
|
|
86
|
+
type PlacementInput,
|
|
87
|
+
RCS4_REQUIRED_ROLES,
|
|
88
|
+
type RecordIdentity,
|
|
89
|
+
recourseStep,
|
|
90
|
+
referencePlacementStep,
|
|
91
|
+
resolvePartyStep,
|
|
92
|
+
settlementStep,
|
|
93
|
+
} from "./steps.js";
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Everything a verification walk is given. **The caller gathers the inputs; `verify()` opens no network
|
|
97
|
+
* connection of its own** — it takes `atrBytes`, not a reference to fetch, and takes `settlements` rather
|
|
98
|
+
* than enumerating them. That is what makes the walk pure and reproducible.
|
|
99
|
+
*
|
|
100
|
+
* Nearly every field is optional, and this is the type's central property: an absent input yields an
|
|
101
|
+
* `indeterminate` or `not-attempted` step, never a pass. `coverage` is how the report stays honest about
|
|
102
|
+
* that — it states which ports and bindings were actually supplied, so a thin walk cannot read as a
|
|
103
|
+
* thorough one.
|
|
104
|
+
*/
|
|
105
|
+
export interface VerifyInput {
|
|
106
|
+
/** The settlement's chain-anchored time (every validity check is evaluated against it). */
|
|
107
|
+
asOf: string;
|
|
108
|
+
/** Which ports/bindings were supplied — coverage is honest depth. */
|
|
109
|
+
coverage: { ports: string[]; bindings: string[] };
|
|
110
|
+
/** The retrieved ATR bytes (absent ⇒ the fingerprint step is indeterminate). */
|
|
111
|
+
atrBytes?: Uint8Array;
|
|
112
|
+
/** The atrHash the settlement committed (nonce/salt) — the fingerprint is checked against it. */
|
|
113
|
+
settledAtrHash?: string;
|
|
114
|
+
/** Settlements found for the fingerprint across supplied forward-indexable bindings. */
|
|
115
|
+
settlements?: unknown[];
|
|
116
|
+
/** The buyer's presented signed acceptance over the fingerprint (TRM-6). */
|
|
117
|
+
acceptance?: SignedAcceptance;
|
|
118
|
+
/** The port that checks the acceptance's signature. Absent ⇒ the acceptance step cannot prove — a
|
|
119
|
+
* signature nobody checked is not evidence of a signature. The EVM impl is `binding-evm-common`'s. */
|
|
120
|
+
acceptanceVerifier?: SignatureVerifier;
|
|
121
|
+
/**
|
|
122
|
+
* The ATA chain to check for attenuation + as-of revocation/expiry, ALREADY FLATTENED.
|
|
123
|
+
*
|
|
124
|
+
* Whoever produced this array is a trusted oracle — every field on a link is a derived fact the step
|
|
125
|
+
* takes at face value. Prefer `authorityWalk`, which removes that trust. This door stays open because
|
|
126
|
+
* a foreign conformance subject may legitimately derive its links some other way.
|
|
127
|
+
*/
|
|
128
|
+
authorityChain?: AuthorityLink[];
|
|
129
|
+
/**
|
|
130
|
+
* The custody walk's readout (`authority.walkChain`) — the PREFERRED authority input.
|
|
131
|
+
*
|
|
132
|
+
* `authorityChain` cannot express what the walk found: a spliced link, an issuer discontinuity, a root
|
|
133
|
+
* issued by someone other than the declared principal, a leaf granted to anyone but the acceptance
|
|
134
|
+
* signer. Flattening a `refused` walk throws those away and hands over links that then read as a clean
|
|
135
|
+
* `not-attempted`, so the report says the record simply did not carry a chain when in fact it carried
|
|
136
|
+
* a contradictory one. Supplied here, the refusal reaches the report WITH its halt class, and the
|
|
137
|
+
* walk's own gap depths pass through verbatim.
|
|
138
|
+
*
|
|
139
|
+
* Mutually exclusive with `authorityChain` — supplying both is a contradiction, not a precedence
|
|
140
|
+
* question, so it throws rather than silently picking one (the same ruling `makeProposalRecord` makes
|
|
141
|
+
* on its own exactly-one-of invariant).
|
|
142
|
+
*/
|
|
143
|
+
authorityWalk?: ChainWalkResult;
|
|
144
|
+
/** The accepted commitment vs the leaf grant's bounds (ATA-4). */
|
|
145
|
+
commitment?: { commitment: Bounds; leafBounds: Bounds };
|
|
146
|
+
/** Both parties' resolutions, each at a stated assurance (IDN-1/IDN-3) — the record's `identity` slot. */
|
|
147
|
+
identity?: RecordIdentity;
|
|
148
|
+
/** The retained evidence package's manifest roles (RCS-4 completeness; `RCS4_REQUIRED_ROLES` is the floor). */
|
|
149
|
+
evidenceRoles?: readonly string[];
|
|
150
|
+
/** The class the record is shaped for (default TC-2). The taxonomy is closed (TC-0..TC-4); a caller
|
|
151
|
+
* at a trust boundary validates untyped input before calling (Zod there, not here). */
|
|
152
|
+
claimedClass?: TransactionClass;
|
|
153
|
+
/** Verification depth (default "structural"). Mechanical depth lets `verified` be raised honestly. */
|
|
154
|
+
depth?: VerifyDepth;
|
|
155
|
+
/** The TC-4 commerce-plane slots. Absent ⇒ the composition steps are NOT appended to the walk
|
|
156
|
+
* (TC-0..TC-3 reports are byte-identical). Present ⇒ the four required composition steps + the
|
|
157
|
+
* impeachment-only `frc-non-gating` step are appended and flow through the existing machinery. */
|
|
158
|
+
composition?: CompositionInput;
|
|
159
|
+
/** The reference recovered from a commerce protocol's own document. Absent ⇒ the
|
|
160
|
+
* `reference-placement` step is NOT appended and every existing report stays byte-identical. Present ⇒
|
|
161
|
+
* the step is appended and flows through the same machinery as every other: it impeaches when it FAILS
|
|
162
|
+
* and never blocks by its absence. REPORTED, never REQUIRED — no class lists it. */
|
|
163
|
+
placement?: PlacementInput;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Run the walk at the caller-specified `depth` (default `"structural"`) and emit the vector-anchored, JCS-serializable report. */
|
|
167
|
+
export async function verify(input: VerifyInput): Promise<VerificationReport> {
|
|
168
|
+
if (input.authorityWalk !== undefined && input.authorityChain !== undefined)
|
|
169
|
+
throw new Error(
|
|
170
|
+
"verify: authorityWalk and authorityChain are mutually exclusive — a walked chain is already the readout, and flattening it alongside would ask which of two answers about the same chain to believe",
|
|
171
|
+
);
|
|
172
|
+
const steps: VerificationStep[] = [
|
|
173
|
+
{
|
|
174
|
+
name: "atr-fingerprint",
|
|
175
|
+
outcome: await fingerprintStep(input.atrBytes, input.settledAtrHash),
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: "settlement-enumeration",
|
|
179
|
+
outcome: settlementStep(input.settlements),
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: "buyer-acceptance",
|
|
183
|
+
outcome: await acceptanceStep(
|
|
184
|
+
input.acceptance,
|
|
185
|
+
input.settledAtrHash,
|
|
186
|
+
input.acceptanceVerifier,
|
|
187
|
+
),
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: "authority-attenuation",
|
|
191
|
+
// One step name either way: the report's shape does not change with how the caller obtained its
|
|
192
|
+
// chain, only how much the walk was able to say about it.
|
|
193
|
+
outcome:
|
|
194
|
+
input.authorityWalk !== undefined
|
|
195
|
+
? authorityStepFromWalk(input.authorityWalk)
|
|
196
|
+
: authorityStep(input.authorityChain),
|
|
197
|
+
},
|
|
198
|
+
{ name: "commitment-vs-leaf", outcome: commitmentStep(input.commitment) },
|
|
199
|
+
{
|
|
200
|
+
name: "recourse-elections",
|
|
201
|
+
outcome: recourseStep(input.atrBytes, input.evidenceRoles),
|
|
202
|
+
},
|
|
203
|
+
{ name: "resolve-party", outcome: resolvePartyStep(input.identity) },
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
// Append the TC-4 composition steps ONLY when a composition slot is supplied — the slot is additive:
|
|
207
|
+
// a TC-0..TC-3 report that supplies none is byte-identical to one produced without the slot existing.
|
|
208
|
+
// The appended steps flow through the same `anyFailed`/`computeVerified` machinery as every other.
|
|
209
|
+
if (input.composition !== undefined) {
|
|
210
|
+
steps.push(
|
|
211
|
+
{ name: "offer-bound", outcome: offerBoundStep(input.composition) },
|
|
212
|
+
{ name: "operations-bound", outcome: operationsStep(input.composition) },
|
|
213
|
+
{
|
|
214
|
+
name: "discovery-integrity",
|
|
215
|
+
outcome: discoveryIntegrityStep(input.composition),
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: "proportionality-declared",
|
|
219
|
+
outcome: proportionalityStep(input.composition),
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: "frc-non-gating",
|
|
223
|
+
outcome: frcNonGatingStep(input.composition),
|
|
224
|
+
},
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Append the reference-placement step ONLY when a placement slot is supplied — its own `if`, because a
|
|
229
|
+
// placement is independent of a composition and a record may carry either, both, or neither. Absent ⇒
|
|
230
|
+
// nothing is appended and existing reports stay byte-identical, which is what makes the slot additive.
|
|
231
|
+
// Supplied ⇒ it flows through the same `anyFailed`/`computeVerified` machinery as every other step, so
|
|
232
|
+
// a placement that CONTRADICTS the record impeaches `supportedClass` to TC-0 — the one power the step
|
|
233
|
+
// grants it. Without this block the step would appear in no report and impeach nothing.
|
|
234
|
+
if (input.placement !== undefined) {
|
|
235
|
+
steps.push({
|
|
236
|
+
name: "reference-placement",
|
|
237
|
+
outcome: referencePlacementStep(input.placement, input.settledAtrHash),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const anyFailed = steps.some((s) => s.outcome.status === "failed");
|
|
242
|
+
const found = input.settlements ?? [];
|
|
243
|
+
const depth = input.depth ?? "structural";
|
|
244
|
+
const claimed = input.claimedClass ?? "TC-2";
|
|
245
|
+
const stepsForVerified = steps.map((s) => ({
|
|
246
|
+
name: s.name as StepName,
|
|
247
|
+
outcome: s.outcome,
|
|
248
|
+
}));
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
verified: computeVerified(stepsForVerified, claimed, depth),
|
|
252
|
+
// The BUYER's stated level: the report's assurance answers "at what assurance did the party who
|
|
253
|
+
// committed resolve?" — the seller's own level rides in the record's identity slot and the package.
|
|
254
|
+
// `buyer` is optional-chained on purpose: `RecordIdentity` requires it, but this readout must be TOTAL
|
|
255
|
+
// over the same half-shaped input `resolvePartyStep` is total over (an untyped caller, a foreign
|
|
256
|
+
// conformance subject). A walk that throws on a malformed record cannot report the malformation.
|
|
257
|
+
//
|
|
258
|
+
// The fallback is NOT a ladder value. `wallet-signature-only` would be conservative in
|
|
259
|
+
// direction, since it never inflates, but still a level the verifier elected for a record that stated
|
|
260
|
+
// none, and the floor of a ladder reads as a finding rather than as silence. `no-assurance-stated` is
|
|
261
|
+
// the SAME token `resolvePartyStep` emits for this condition (steps.ts), so absence has one spelling
|
|
262
|
+
// across the step and the report instead of contradicting itself inside a single run.
|
|
263
|
+
assurance: input.identity?.buyer?.assurance ?? "no-assurance-stated",
|
|
264
|
+
supportedClass: anyFailed ? "TC-0" : claimed,
|
|
265
|
+
asOf: input.asOf,
|
|
266
|
+
steps,
|
|
267
|
+
coverage: input.coverage,
|
|
268
|
+
settlements: { found, multiplySettled: found.length > 1 },
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Discriminant helper — did any step fail (the honest impeachment signal)? */
|
|
273
|
+
export function anyStepFailed(report: VerificationReport): boolean {
|
|
274
|
+
return report.steps.some(
|
|
275
|
+
(s: VerificationStep) => (s.outcome as StepOutcome).status === "failed",
|
|
276
|
+
);
|
|
277
|
+
}
|
package/src/report.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `VerificationReport` and its **deterministic serialization — RFC 8785 (JCS) canonical JSON**.
|
|
3
|
+
* The protocol promises byte-identical reports across independent implementations: the report is RECOMPUTED by
|
|
4
|
+
* every verifier, and JCS is exactly the multi-producer-convergence case it exists for. This is NOT the
|
|
5
|
+
* ATR rule and the two must never be conflated — canonicalization is banned for the *ATR* because it has
|
|
6
|
+
* one producer and the exact received bytes are the fingerprint; the *report* has many producers.
|
|
7
|
+
*
|
|
8
|
+
* All monetary values in reports are decimal-integer base-unit STRINGS, so JCS's
|
|
9
|
+
* ECMAScript number serialization never touches money. JCS = recursively sort object keys by UTF-16 code
|
|
10
|
+
* units (JS default string sort), then serialize with JSON's escaping + ECMAScript number formatting
|
|
11
|
+
* (which already match RFC 8785 §3.2.2). Confirmed byte-for-byte against an independent JCS implementation.
|
|
12
|
+
*/
|
|
13
|
+
import type { HaltClass } from "@integraledger/lcp-binding-core";
|
|
14
|
+
|
|
15
|
+
/** Verification depth: structural (presence/absence readout, verified always false) vs mechanical
|
|
16
|
+
* (inputs gathered over live ports; verified is an honest function of the walk). */
|
|
17
|
+
export type VerifyDepth = "structural" | "mechanical";
|
|
18
|
+
|
|
19
|
+
/** A step outcome — aligned to `report.schema.json`. */
|
|
20
|
+
export type StepOutcome =
|
|
21
|
+
| { status: "proved" }
|
|
22
|
+
| { status: "failed"; haltClass: HaltClass }
|
|
23
|
+
| { status: "indeterminate" } // unretrievable ATR — not a failure
|
|
24
|
+
| { status: "not-attempted"; depth: string };
|
|
25
|
+
|
|
26
|
+
/** One named step of the walk and how it came out. The `name` is a {@link StepName} in this
|
|
27
|
+
* implementation but is typed as `string` here so a foreign implementation's report is representable —
|
|
28
|
+
* a report type that cannot hold another engine's step cannot be used to compare against it. */
|
|
29
|
+
export interface VerificationStep {
|
|
30
|
+
name: string;
|
|
31
|
+
outcome: StepOutcome;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The output of a verification walk — the artifact a stranger reads instead of re-running it.
|
|
35
|
+
*
|
|
36
|
+
* Read `steps` before `verified`. `verified` is a summary that is FALSE at structural depth by
|
|
37
|
+
* construction, so a `false` may mean "impeached" or merely "not attempted mechanically", and only the
|
|
38
|
+
* step list distinguishes them. `supportedClass` is what the record honestly supports, not what the
|
|
39
|
+
* caller asked for. Several fields are `string` rather than a union on purpose: this type has to be able
|
|
40
|
+
* to hold another implementation's answer, including a wrong one. */
|
|
41
|
+
export interface VerificationReport {
|
|
42
|
+
/** Honest function of depth: `false` at structural (presence/absence → class); at mechanical, raised by
|
|
43
|
+
* `computeVerified` iff every class-required step is `proved` and none `failed`. */
|
|
44
|
+
verified: boolean;
|
|
45
|
+
/** The stated assurance level — never a bare boolean. One of the four ladder values
|
|
46
|
+
* (`wallet-signature-only` | `domain-controlled` | `attested` | `legal-party`), or
|
|
47
|
+
* `no-assurance-stated` where the record states none. Left as `string` rather than a union for the same
|
|
48
|
+
* reason `supportedClass` is: a foreign implementation's report must be representable before it can be
|
|
49
|
+
* compared, and a type that refuses to hold a wrong value cannot report one. */
|
|
50
|
+
assurance: string;
|
|
51
|
+
/** The transaction class the record honestly supports — an honest readout, never a target. */
|
|
52
|
+
supportedClass: string;
|
|
53
|
+
/** The settlement's chain-anchored time every validity check is evaluated against. */
|
|
54
|
+
asOf: string;
|
|
55
|
+
steps: VerificationStep[];
|
|
56
|
+
/** Supplied ports/bindings — coverage is part of honest depth. */
|
|
57
|
+
coverage: { ports: string[]; bindings: string[] };
|
|
58
|
+
settlements: { found: unknown[]; multiplySettled: boolean };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Emit the canonical text DIRECTLY from the sorted key list — never by re-assigning sorted keys onto an
|
|
63
|
+
* object and handing it to `JSON.stringify`. JS object property order puts integer-like keys FIRST, in
|
|
64
|
+
* ascending numeric order, whatever the insertion order (the same engine behaviour `kernel.assemble`
|
|
65
|
+
* refuses integer-like slot names over), so the object round-trip silently re-sorts `{"10":…,"9":…}` to
|
|
66
|
+
* `9,10` while RFC 8785 §3.2.3 requires UTF-16 code-unit order — `"1" (0x31) < "9" (0x39)`. Two conformant
|
|
67
|
+
* implementations would then disagree byte-for-byte on the same report, which is the one thing this buys.
|
|
68
|
+
* Reachable through `settlements.found` (caller-supplied objects). Pinned by `vectors/report/serialization.json`.
|
|
69
|
+
*/
|
|
70
|
+
function serializeCanonical(v: unknown): string {
|
|
71
|
+
if (Array.isArray(v)) return `[${v.map(serializeCanonical).join(",")}]`;
|
|
72
|
+
if (v !== null && typeof v === "object") {
|
|
73
|
+
const obj = v as Record<string, unknown>;
|
|
74
|
+
// Default Array#sort compares by UTF-16 code units — exactly RFC 8785's ordering.
|
|
75
|
+
const parts = Object.keys(obj)
|
|
76
|
+
.sort()
|
|
77
|
+
.map((k) => `${JSON.stringify(k)}:${serializeCanonical(obj[k])}`);
|
|
78
|
+
return `{${parts.join(",")}}`;
|
|
79
|
+
}
|
|
80
|
+
// Primitives: JSON's own escaping + ECMAScript number formatting already match RFC 8785 §3.2.2.
|
|
81
|
+
return JSON.stringify(v) as string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** RFC 8785 JCS canonical JSON string for any JSON value (keys sorted by UTF-16 code units, no whitespace). */
|
|
85
|
+
export function jcsCanonicalize(value: unknown): string {
|
|
86
|
+
// Round-trip through JSON first so `toJSON`, dropped `undefined`/function members, and non-finite
|
|
87
|
+
// numbers resolve EXACTLY as JSON.stringify defines them; `serializeCanonical` then sees pure JSON
|
|
88
|
+
// and owns only the ordering rule.
|
|
89
|
+
const json = JSON.stringify(value);
|
|
90
|
+
if (json === undefined)
|
|
91
|
+
throw new Error(
|
|
92
|
+
"value is not JSON-serializable (JCS is defined only over JSON values)",
|
|
93
|
+
);
|
|
94
|
+
return serializeCanonical(JSON.parse(json));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The report's canonical bytes (UTF-8 of its JCS form) — the byte-for-byte target. */
|
|
98
|
+
export function serializeReport(report: VerificationReport): Uint8Array {
|
|
99
|
+
return new TextEncoder().encode(jcsCanonicalize(report));
|
|
100
|
+
}
|
package/src/required.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { VerifyDepth } from "./report.js";
|
|
2
|
+
|
|
3
|
+
export type { VerifyDepth };
|
|
4
|
+
/** The transaction-class ladder. Higher classes require strictly more steps, so a record supporting TC-3
|
|
5
|
+
* supports every class below it. A class is a CLAIM about what a record can support — it is never a
|
|
6
|
+
* quality score, and nothing in this package tries to reach a higher one. */
|
|
7
|
+
export type TransactionClass = "TC-0" | "TC-1" | "TC-2" | "TC-3" | "TC-4";
|
|
8
|
+
/** Every step the walk can append, in one closed union. Closed because `REQUIRED_STEPS` is keyed by these
|
|
9
|
+
* names and a typo would silently make a required step unfindable — which `computeVerified` would read as
|
|
10
|
+
* "not proved", failing safe but for the wrong reason. */
|
|
11
|
+
export type StepName =
|
|
12
|
+
| "atr-fingerprint"
|
|
13
|
+
| "settlement-enumeration"
|
|
14
|
+
| "buyer-acceptance"
|
|
15
|
+
| "authority-attenuation"
|
|
16
|
+
| "commitment-vs-leaf"
|
|
17
|
+
| "recourse-elections"
|
|
18
|
+
| "resolve-party"
|
|
19
|
+
// The TC-4 composition steps:
|
|
20
|
+
| "offer-bound"
|
|
21
|
+
| "operations-bound"
|
|
22
|
+
| "discovery-integrity"
|
|
23
|
+
| "proportionality-declared"
|
|
24
|
+
| "frc-non-gating"
|
|
25
|
+
// The reference-placement step is REPORTED, NEVER REQUIRED. Deliberately absent from every entry in REQUIRED_STEPS below,
|
|
26
|
+
// for the same reason frc-non-gating is: it impeaches when it FAILS (the reference found in the
|
|
27
|
+
// protocol's native field contradicts the record) but its ABSENCE never blocks. A protocol that never
|
|
28
|
+
// settles has nothing to enumerate, and letting a placement stand in for a settlement rung would let a
|
|
29
|
+
// record that moved no money read as classed — the exact inflation the center lock forbids.
|
|
30
|
+
| "reference-placement";
|
|
31
|
+
|
|
32
|
+
/** PAY + IDN — the attribution rungs every class from TC-1 up carries. */
|
|
33
|
+
const TC1: readonly StepName[] = ["settlement-enumeration", "resolve-party"];
|
|
34
|
+
/** + TRM/WLD: the fingerprint step IS the weld check (recomputed hash vs the hash the settlement committed). */
|
|
35
|
+
const TC2: readonly StepName[] = [...TC1, "atr-fingerprint"];
|
|
36
|
+
/** + TRM-6 + ATA + RCS-1/2/4 — the definition of the class, verbatim. */
|
|
37
|
+
const TC3: readonly StepName[] = [
|
|
38
|
+
...TC2,
|
|
39
|
+
"buyer-acceptance",
|
|
40
|
+
"authority-attenuation",
|
|
41
|
+
"commitment-vs-leaf",
|
|
42
|
+
"recourse-elections",
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* What each class MECHANICALLY requires to be `proved` before `verified` can hold, derived row-for-row from
|
|
47
|
+
* The class requirements (see `vectors/verify/mechanical.json`, the authored-from-spec truth table):
|
|
48
|
+
*
|
|
49
|
+
* PAY (value settles) ................. `settlement-enumeration` TC-1 +
|
|
50
|
+
* IDN (identity, stated assurance) .... `resolve-party` TC-1 +
|
|
51
|
+
* TRM/WLD (fingerprint + weld) ........ `atr-fingerprint` TC-2 +
|
|
52
|
+
* TRM-6 (signed acceptance) ........... `buyer-acceptance` TC-3 +
|
|
53
|
+
* ATA (authority + delegation chain) .. `authority-attenuation`, `commitment-vs-leaf` TC-3 +
|
|
54
|
+
* RCS-1/2/4 (forum, law, package) ..... `recourse-elections` TC-3 +
|
|
55
|
+
* OFR/OPS/DSC (commerce plane) ........ the four composition steps TC-4
|
|
56
|
+
*
|
|
57
|
+
* The ladder is CUMULATIVE: a class requires every rung below it, which is why TC-4 cannot hold without the
|
|
58
|
+
* TC-3 acceptance. It is also EXACT: a class never requires a step it does not depend on — TRM is not a TC-1
|
|
59
|
+
* rung, so a TC-1 record with an unattempted fingerprint still verifies.
|
|
60
|
+
*
|
|
61
|
+
* DECLARED GAP — ASP (spend authority, verifiable bounds) is marked from TC-1 and has NO step here: the bounds
|
|
62
|
+
* live inside the rail's own settlement authorization (an EIP-3009 `value`, an escrow `PaymentInfo` cap) and
|
|
63
|
+
* `verify` is pure over supplied inputs with no rail decoder. RCS-4 reaches it indirectly — `recourse-elections`
|
|
64
|
+
* requires the `spend artifact` role in the retained package, so the artifact is kept and referenced even though
|
|
65
|
+
* its bounds are not re-derived here. Closing it properly is a rail-decoder work item, named rather than omitted.
|
|
66
|
+
*
|
|
67
|
+
* `frc-non-gating` is DELIBERATELY absent from TC-4: it impeaches when it FAILS (a signal that gated settlement)
|
|
68
|
+
* but its ABSENCE never blocks (FRC is a downstream consequence, not a v1 requirement).
|
|
69
|
+
*/
|
|
70
|
+
export const REQUIRED_STEPS: Record<TransactionClass, readonly StepName[]> = {
|
|
71
|
+
"TC-0": [],
|
|
72
|
+
"TC-1": TC1,
|
|
73
|
+
"TC-2": TC2,
|
|
74
|
+
"TC-3": TC3,
|
|
75
|
+
"TC-4": [
|
|
76
|
+
...TC3,
|
|
77
|
+
"offer-bound",
|
|
78
|
+
"operations-bound",
|
|
79
|
+
"discovery-integrity",
|
|
80
|
+
"proportionality-declared",
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** Whether a walk affirms the claimed class. Three rules, in order: **structural depth is always `false`**
|
|
85
|
+
* (a presence/absence readout never affirms liveness), any `failed` step impeaches the whole walk, and
|
|
86
|
+
* otherwise every step required by the claimed class must be `proved`. A step that is absent, or
|
|
87
|
+
* `indeterminate`, is not `proved` — absent inputs never prove. */
|
|
88
|
+
export function computeVerified(
|
|
89
|
+
steps: readonly { name: StepName; outcome: { status: string } }[],
|
|
90
|
+
claimedClass: TransactionClass,
|
|
91
|
+
depth: VerifyDepth,
|
|
92
|
+
): boolean {
|
|
93
|
+
if (depth === "structural") return false; // structural = presence/absence readout, never affirms liveness
|
|
94
|
+
if (steps.some((s) => s.outcome.status === "failed")) return false; // any failure impeaches
|
|
95
|
+
const required = REQUIRED_STEPS[claimedClass];
|
|
96
|
+
return required.every(
|
|
97
|
+
(name) => steps.find((s) => s.name === name)?.outcome.status === "proved",
|
|
98
|
+
);
|
|
99
|
+
}
|