@adastracomputing/ink 0.1.0-alpha.2 → 0.1.0-alpha.5
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 +56 -5
- package/CODE_OF_CONDUCT.md +1 -1
- package/README.md +7 -5
- package/SECURITY.md +1 -1
- package/bin/verify-inclusion-impl.mjs +4 -1
- package/dist/audit/inclusion-receipt.d.ts +142 -0
- package/dist/audit/inclusion-receipt.js +496 -0
- package/dist/crypto/ink.d.ts +178 -0
- package/dist/crypto/ink.js +915 -0
- package/dist/crypto/keys.d.ts +42 -0
- package/dist/crypto/keys.js +179 -0
- package/dist/crypto/multi-key-verify.d.ts +29 -0
- package/dist/crypto/multi-key-verify.js +153 -0
- package/dist/crypto/sign.d.ts +17 -0
- package/dist/crypto/sign.js +152 -0
- package/dist/crypto/verify.js +1 -0
- package/dist/discovery/agent-card.d.ts +83 -0
- package/dist/discovery/agent-card.js +545 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +15 -0
- package/dist/ink/checkpoint.d.ts +19 -0
- package/dist/ink/checkpoint.js +69 -0
- package/dist/ink/discovery-gating.d.ts +237 -0
- package/dist/ink/discovery-gating.js +91 -0
- package/dist/ink/handshake-budget.d.ts +90 -0
- package/dist/ink/handshake-budget.js +397 -0
- package/dist/ink/receipts.d.ts +31 -0
- package/dist/ink/receipts.js +89 -0
- package/dist/ink/transport-auth.d.ts +47 -0
- package/dist/ink/transport-auth.js +77 -0
- package/dist/middleware/ink-auth.d.ts +68 -0
- package/dist/middleware/ink-auth.js +214 -0
- package/dist/models/agent-card.d.ts +154 -0
- package/dist/models/agent-card.js +59 -0
- package/dist/models/ink-audit.d.ts +344 -0
- package/dist/models/ink-audit.js +167 -0
- package/dist/models/ink-handshake.d.ts +129 -0
- package/dist/models/ink-handshake.js +89 -0
- package/dist/models/intent.d.ts +437 -0
- package/dist/models/intent.js +172 -0
- package/dist/models/key-entry.d.ts +60 -0
- package/dist/models/key-entry.js +13 -0
- package/dist/models/profile.d.ts +61 -0
- package/dist/models/profile.js +24 -0
- package/docs/maturity.md +3 -3
- package/docs/threat-model.md +1 -1
- package/package.json +17 -13
- package/specs/ink-auditability.md +37 -12
- package/specs/ink-compliance-checklist.md +9 -1
- package/src/audit/inclusion-receipt.ts +0 -268
- package/src/crypto/ink.ts +0 -902
- package/src/crypto/keys.ts +0 -210
- package/src/crypto/multi-key-verify.ts +0 -170
- package/src/crypto/sign.ts +0 -155
- package/src/discovery/agent-card.ts +0 -508
- package/src/index.ts +0 -67
- package/src/ink/checkpoint.ts +0 -75
- package/src/ink/discovery-gating.ts +0 -147
- package/src/ink/handshake-budget.ts +0 -413
- package/src/ink/receipts.ts +0 -114
- package/src/ink/transport-auth.ts +0 -96
- package/src/middleware/ink-auth.ts +0 -263
- package/src/models/agent-card.ts +0 -63
- package/src/models/ink-audit.ts +0 -205
- package/src/models/ink-handshake.ts +0 -123
- package/src/models/intent.ts +0 -201
- package/src/models/key-entry.ts +0 -52
- package/src/models/profile.ts +0 -31
- /package/{src/crypto/verify.ts → dist/crypto/verify.d.ts} +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const KeyStatusSchema: z.ZodEnum<{
|
|
3
|
+
active: "active";
|
|
4
|
+
retired: "retired";
|
|
5
|
+
revoked: "revoked";
|
|
6
|
+
}>;
|
|
7
|
+
export type KeyStatus = z.infer<typeof KeyStatusSchema>;
|
|
8
|
+
export declare const KeyRoleSchema: z.ZodEnum<{
|
|
9
|
+
signing: "signing";
|
|
10
|
+
encryption: "encryption";
|
|
11
|
+
}>;
|
|
12
|
+
export type KeyRole = z.infer<typeof KeyRoleSchema>;
|
|
13
|
+
export declare const KeyEntrySchema: z.ZodObject<{
|
|
14
|
+
keyId: z.ZodString;
|
|
15
|
+
algorithm: z.ZodEnum<{
|
|
16
|
+
Ed25519: "Ed25519";
|
|
17
|
+
X25519: "X25519";
|
|
18
|
+
}>;
|
|
19
|
+
publicKeyMultibase: z.ZodString;
|
|
20
|
+
status: z.ZodEnum<{
|
|
21
|
+
active: "active";
|
|
22
|
+
retired: "retired";
|
|
23
|
+
revoked: "revoked";
|
|
24
|
+
}>;
|
|
25
|
+
validFrom: z.ZodString;
|
|
26
|
+
validUntil: z.ZodOptional<z.ZodString>;
|
|
27
|
+
revokedAt: z.ZodOptional<z.ZodString>;
|
|
28
|
+
revokeReason: z.ZodOptional<z.ZodString>;
|
|
29
|
+
}, z.core.$strip>;
|
|
30
|
+
export type KeyEntry = z.infer<typeof KeyEntrySchema>;
|
|
31
|
+
export interface CandidateKey {
|
|
32
|
+
keyId: string;
|
|
33
|
+
publicKey: Uint8Array;
|
|
34
|
+
status: KeyStatus;
|
|
35
|
+
/** ISO 8601 timestamp the key becomes usable. Verifier rejects messages
|
|
36
|
+
* whose `body.timestamp` falls outside [validFrom, validUntil]. Optional
|
|
37
|
+
* for backward compat with legacy callers that don't track windows. */
|
|
38
|
+
validFrom?: string;
|
|
39
|
+
/** ISO 8601 timestamp the key stops being usable. Typically set when a
|
|
40
|
+
* key transitions to `retired`. A retired key with no validUntil keeps
|
|
41
|
+
* verifying indefinitely (legacy behavior); set validUntil to bound it. */
|
|
42
|
+
validUntil?: string;
|
|
43
|
+
/** ISO 8601 timestamp the key was revoked. Defensive: status === "revoked"
|
|
44
|
+
* already blocks verification; this field documents the moment. */
|
|
45
|
+
revokedAt?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface StoredKey {
|
|
48
|
+
keyId: string;
|
|
49
|
+
agentId: string;
|
|
50
|
+
role: KeyRole;
|
|
51
|
+
algorithm: string;
|
|
52
|
+
publicKeyMultibase: string;
|
|
53
|
+
privateKey: Uint8Array | null;
|
|
54
|
+
status: KeyStatus;
|
|
55
|
+
validFrom: string;
|
|
56
|
+
validUntil: string | null;
|
|
57
|
+
revokedAt: string | null;
|
|
58
|
+
createdAt: string;
|
|
59
|
+
updatedAt: string;
|
|
60
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const KeyStatusSchema = z.enum(["active", "retired", "revoked"]);
|
|
3
|
+
export const KeyRoleSchema = z.enum(["signing", "encryption"]);
|
|
4
|
+
export const KeyEntrySchema = z.object({
|
|
5
|
+
keyId: z.string().min(1),
|
|
6
|
+
algorithm: z.enum(["Ed25519", "X25519"]),
|
|
7
|
+
publicKeyMultibase: z.string().startsWith("z"),
|
|
8
|
+
status: KeyStatusSchema,
|
|
9
|
+
validFrom: z.string().datetime(),
|
|
10
|
+
validUntil: z.string().datetime().optional(),
|
|
11
|
+
revokedAt: z.string().datetime().optional(),
|
|
12
|
+
revokeReason: z.string().optional(),
|
|
13
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const AvailabilityConfigSchema: z.ZodObject<{
|
|
3
|
+
timezone: z.ZodString;
|
|
4
|
+
meetingHours: z.ZodOptional<z.ZodString>;
|
|
5
|
+
responseSla: z.ZodOptional<z.ZodString>;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
export declare const ProfileSnapshotSchema: z.ZodObject<{
|
|
8
|
+
headline: z.ZodString;
|
|
9
|
+
skills: z.ZodArray<z.ZodString>;
|
|
10
|
+
interests: z.ZodArray<z.ZodString>;
|
|
11
|
+
availability: z.ZodOptional<z.ZodObject<{
|
|
12
|
+
timezone: z.ZodString;
|
|
13
|
+
meetingHours: z.ZodOptional<z.ZodString>;
|
|
14
|
+
responseSla: z.ZodOptional<z.ZodString>;
|
|
15
|
+
}, z.core.$strip>>;
|
|
16
|
+
openTo: z.ZodArray<z.ZodString>;
|
|
17
|
+
}, z.core.$strip>;
|
|
18
|
+
export declare const ProfileSchema: z.ZodObject<{
|
|
19
|
+
agentId: z.ZodString;
|
|
20
|
+
handle: z.ZodString;
|
|
21
|
+
displayName: z.ZodString;
|
|
22
|
+
bio: z.ZodString;
|
|
23
|
+
snapshots: z.ZodObject<{
|
|
24
|
+
public: z.ZodObject<{
|
|
25
|
+
headline: z.ZodString;
|
|
26
|
+
skills: z.ZodArray<z.ZodString>;
|
|
27
|
+
interests: z.ZodArray<z.ZodString>;
|
|
28
|
+
availability: z.ZodOptional<z.ZodObject<{
|
|
29
|
+
timezone: z.ZodString;
|
|
30
|
+
meetingHours: z.ZodOptional<z.ZodString>;
|
|
31
|
+
responseSla: z.ZodOptional<z.ZodString>;
|
|
32
|
+
}, z.core.$strip>>;
|
|
33
|
+
openTo: z.ZodArray<z.ZodString>;
|
|
34
|
+
}, z.core.$strip>;
|
|
35
|
+
connected: z.ZodObject<{
|
|
36
|
+
headline: z.ZodString;
|
|
37
|
+
skills: z.ZodArray<z.ZodString>;
|
|
38
|
+
interests: z.ZodArray<z.ZodString>;
|
|
39
|
+
availability: z.ZodOptional<z.ZodObject<{
|
|
40
|
+
timezone: z.ZodString;
|
|
41
|
+
meetingHours: z.ZodOptional<z.ZodString>;
|
|
42
|
+
responseSla: z.ZodOptional<z.ZodString>;
|
|
43
|
+
}, z.core.$strip>>;
|
|
44
|
+
openTo: z.ZodArray<z.ZodString>;
|
|
45
|
+
}, z.core.$strip>;
|
|
46
|
+
custom: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
47
|
+
headline: z.ZodString;
|
|
48
|
+
skills: z.ZodArray<z.ZodString>;
|
|
49
|
+
interests: z.ZodArray<z.ZodString>;
|
|
50
|
+
availability: z.ZodOptional<z.ZodObject<{
|
|
51
|
+
timezone: z.ZodString;
|
|
52
|
+
meetingHours: z.ZodOptional<z.ZodString>;
|
|
53
|
+
responseSla: z.ZodOptional<z.ZodString>;
|
|
54
|
+
}, z.core.$strip>>;
|
|
55
|
+
openTo: z.ZodArray<z.ZodString>;
|
|
56
|
+
}, z.core.$strip>>;
|
|
57
|
+
}, z.core.$strip>;
|
|
58
|
+
}, z.core.$strip>;
|
|
59
|
+
export type AvailabilityConfig = z.infer<typeof AvailabilityConfigSchema>;
|
|
60
|
+
export type ProfileSnapshot = z.infer<typeof ProfileSnapshotSchema>;
|
|
61
|
+
export type Profile = z.infer<typeof ProfileSchema>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const AvailabilityConfigSchema = z.object({
|
|
3
|
+
timezone: z.string(),
|
|
4
|
+
meetingHours: z.string().optional(),
|
|
5
|
+
responseSla: z.string().optional(),
|
|
6
|
+
});
|
|
7
|
+
export const ProfileSnapshotSchema = z.object({
|
|
8
|
+
headline: z.string().max(500),
|
|
9
|
+
skills: z.array(z.string().max(100)).max(50),
|
|
10
|
+
interests: z.array(z.string().max(100)).max(50),
|
|
11
|
+
availability: AvailabilityConfigSchema.optional(),
|
|
12
|
+
openTo: z.array(z.string().max(100)).max(20),
|
|
13
|
+
});
|
|
14
|
+
export const ProfileSchema = z.object({
|
|
15
|
+
agentId: z.string(),
|
|
16
|
+
handle: z.string(),
|
|
17
|
+
displayName: z.string().max(200),
|
|
18
|
+
bio: z.string().max(2000),
|
|
19
|
+
snapshots: z.object({
|
|
20
|
+
public: ProfileSnapshotSchema,
|
|
21
|
+
connected: ProfileSnapshotSchema,
|
|
22
|
+
custom: z.record(z.string(), ProfileSnapshotSchema),
|
|
23
|
+
}),
|
|
24
|
+
});
|
package/docs/maturity.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
implementations should report discrepancies as issues.
|
|
18
18
|
- The protocol is in use by one production integrator (Tulpa). That is
|
|
19
19
|
one data point, not a guarantee of robustness at scale.
|
|
20
|
-
- The
|
|
20
|
+
- The library in `src/` runs on any runtime providing
|
|
21
21
|
standard Web Crypto (`crypto.subtle`) and `fetch`, modern Node, Deno,
|
|
22
22
|
Bun, and edge runtimes. Browser use is feasible but not exercised by
|
|
23
23
|
the maintainers.
|
|
@@ -55,7 +55,7 @@ Pre-1.0 releases follow `0.Y.Z` semantics:
|
|
|
55
55
|
|
|
56
56
|
- `0.Y.0`, Minor version bump indicates a wire-format change. Receivers
|
|
57
57
|
must support at least one prior minor during a transition window.
|
|
58
|
-
- `0.Y.Z` (Z > 0), Patch bumps fix bugs in the
|
|
58
|
+
- `0.Y.Z` (Z > 0), Patch bumps fix bugs in the library
|
|
59
59
|
and update test vectors where needed. They do not change wire format.
|
|
60
60
|
|
|
61
61
|
Breaking changes before v1.0 will be announced in the repository
|
|
@@ -71,7 +71,7 @@ be a real incident:
|
|
|
71
71
|
falls inside the in-scope protections and you accept the out-of-scope
|
|
72
72
|
limits.
|
|
73
73
|
2. Run `../test-vectors/*` against your implementation.
|
|
74
|
-
3. Fuzz your envelope parser. The
|
|
74
|
+
3. Fuzz your envelope parser. The library's tests are
|
|
75
75
|
not a substitute.
|
|
76
76
|
4. Pen-test the rotation and revocation flows specifically. The
|
|
77
77
|
authority rule is the single most security-sensitive piece and the
|
package/docs/threat-model.md
CHANGED
|
@@ -80,7 +80,7 @@ bounds the damage window but does not eliminate it. Key custody is out of
|
|
|
80
80
|
scope.
|
|
81
81
|
|
|
82
82
|
### Malicious marketplace extensions (if you integrate one)
|
|
83
|
-
The
|
|
83
|
+
The library does not include an extension/marketplace
|
|
84
84
|
layer. A product that integrates INK and adds a delegation-token layer
|
|
85
85
|
(for third-party agents to act on behalf of users) must design its own
|
|
86
86
|
trust model for the marketplace, manifest review, and capability
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adastracomputing/ink",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.0-alpha.5",
|
|
4
|
+
"description": "Library and specification for the INK (Inter-agent Networking Kernel) protocol",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"author": "Ad Astra Computing Inc.",
|
|
7
7
|
"repository": {
|
|
@@ -13,24 +13,24 @@
|
|
|
13
13
|
"url": "https://github.com/Ad-Astra-Computing/ink/issues"
|
|
14
14
|
},
|
|
15
15
|
"type": "module",
|
|
16
|
-
"main": "./
|
|
17
|
-
"types": "./
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
18
|
"exports": {
|
|
19
19
|
".": {
|
|
20
|
-
"types": "./
|
|
21
|
-
"default": "./
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"default": "./dist/index.js"
|
|
22
22
|
},
|
|
23
23
|
"./package.json": "./package.json"
|
|
24
24
|
},
|
|
25
25
|
"sideEffects": false,
|
|
26
26
|
"engines": {
|
|
27
|
-
"node": ">=
|
|
27
|
+
"node": ">=24"
|
|
28
28
|
},
|
|
29
29
|
"bin": {
|
|
30
30
|
"ink": "./bin/ink.mjs"
|
|
31
31
|
},
|
|
32
32
|
"files": [
|
|
33
|
-
"
|
|
33
|
+
"dist/",
|
|
34
34
|
"bin/",
|
|
35
35
|
"specs/",
|
|
36
36
|
"docs/",
|
|
@@ -43,21 +43,25 @@
|
|
|
43
43
|
"CODE_OF_CONDUCT.md"
|
|
44
44
|
],
|
|
45
45
|
"scripts": {
|
|
46
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
46
47
|
"test": "vitest run",
|
|
47
48
|
"typecheck": "tsc --noEmit",
|
|
48
49
|
"lint": "eslint src/ test/ scripts/",
|
|
49
|
-
"check:surface": "tsx scripts/check-public-surface.ts"
|
|
50
|
+
"check:surface": "tsx scripts/check-public-surface.ts",
|
|
51
|
+
"check:pack": "./scripts/check-pack.sh",
|
|
52
|
+
"prepack": "npm run build",
|
|
53
|
+
"prepublishOnly": "npm run build"
|
|
50
54
|
},
|
|
51
55
|
"dependencies": {
|
|
52
56
|
"@noble/curves": "^2.2.0",
|
|
53
57
|
"@noble/ed25519": "^3.1.0",
|
|
54
|
-
"@noble/hashes": "^
|
|
58
|
+
"@noble/hashes": "^2.2.0",
|
|
55
59
|
"canonicalize": "^2.1.0",
|
|
56
|
-
"zod": "^
|
|
60
|
+
"zod": "^4.4.3"
|
|
57
61
|
},
|
|
58
62
|
"devDependencies": {
|
|
59
63
|
"@cloudflare/workers-types": "^4.20260418.1",
|
|
60
|
-
"@types/node": "^
|
|
64
|
+
"@types/node": "^24.12.4",
|
|
61
65
|
"@typescript-eslint/eslint-plugin": "^8.60.0",
|
|
62
66
|
"@typescript-eslint/parser": "^8.60.0",
|
|
63
67
|
"eslint": "^10.4.0",
|
|
@@ -71,6 +75,6 @@
|
|
|
71
75
|
"ed25519",
|
|
72
76
|
"atproto",
|
|
73
77
|
"agent-to-agent",
|
|
74
|
-
"
|
|
78
|
+
"transparency-log"
|
|
75
79
|
]
|
|
76
80
|
}
|
|
@@ -461,9 +461,9 @@ The audit service:
|
|
|
461
461
|
1. Accepts signed `InkAuditEvent` submissions from agents
|
|
462
462
|
2. Appends them to a **Merkle tree** (not just a hash chain, enables efficient inclusion proofs)
|
|
463
463
|
3. Returns a **signed inclusion receipt** proving the event was recorded at a specific tree position and timestamp
|
|
464
|
-
4. Serves **inclusion proofs** and **
|
|
464
|
+
4. Serves **inclusion proofs** on demand (per-submission via the inclusion receipt and per-query via the signed `audit_query_response` envelope). **Consistency proofs** between two arbitrary checkpoints are not in scope for alpha.3; consistency-proof verification against external `tlog-witness` cosigners (§7.0) is the alpha.3 mitigation against split-view attacks.
|
|
465
465
|
|
|
466
|
-
The service CANNOT forge events
|
|
466
|
+
The service CANNOT forge events that verifiers will accept, because every returned event carries the submitting agent's Ed25519 `agentSignature` and §7.3 verifiers re-check it against the agent's published keys. A witness that commits a fabricated event_json into its Merkle tree can produce a valid inclusion proof, but verifiers will reject the response when the agent signature fails to validate. (Verifiers that walk Merkle proofs without checking `agentSignature` lose this guarantee; see §7.5.) The service CAN prove:
|
|
467
467
|
- That a specific event was submitted at a specific time (inclusion)
|
|
468
468
|
- That the log is append-only and no events have been removed (consistency)
|
|
469
469
|
- That two parties submitted conflicting events for the same message (conflict detection)
|
|
@@ -531,41 +531,66 @@ Authorization: INK-Ed25519 <signature>
|
|
|
531
531
|
}
|
|
532
532
|
```
|
|
533
533
|
|
|
534
|
-
**Response includes Merkle inclusion proofs:**
|
|
534
|
+
**Response includes Merkle inclusion proofs and is signed by the witness:**
|
|
535
535
|
|
|
536
536
|
```json
|
|
537
537
|
{
|
|
538
538
|
"protocol": "ink/0.1",
|
|
539
|
-
"type": "network.tulpa.
|
|
539
|
+
"type": "network.tulpa.audit_query_response",
|
|
540
|
+
"serviceDid": "did:web:witness.example.com",
|
|
540
541
|
"messageId": "msg-123",
|
|
541
|
-
"
|
|
542
|
+
"requester": "did:plc:requester",
|
|
543
|
+
"events": [ /* InkAuditEvent[] visible to the requester */ ],
|
|
542
544
|
"proofs": [
|
|
543
545
|
{
|
|
544
546
|
"eventId": "01JBTEST0001",
|
|
545
547
|
"leafIndex": 48290,
|
|
546
|
-
"
|
|
547
|
-
"treeSize": 48291,
|
|
548
|
-
"rootHash": "<SHA-256 hex>"
|
|
548
|
+
"inclusionProof": ["<hash>", "<hash>", "..."]
|
|
549
549
|
}
|
|
550
550
|
],
|
|
551
|
-
"
|
|
551
|
+
"treeSize": 48291,
|
|
552
|
+
"rootHash": "<SHA-256 hex of Merkle tree root at response time>",
|
|
553
|
+
"timestamp": "2026-03-19T13:00:01Z",
|
|
554
|
+
"serviceSignature": "<Ed25519, see canonical format below>"
|
|
552
555
|
}
|
|
553
556
|
```
|
|
554
557
|
|
|
558
|
+
**Canonical signature format.** `serviceSignature` is an Ed25519 signature over the bytes:
|
|
559
|
+
|
|
560
|
+
```
|
|
561
|
+
"ink/audit-query-response/v1\n" || JCS(response-fields-without-serviceSignature)
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
The signed payload binds the witness's `serviceDid`, the `messageId` requested, the authenticated `requester` whose access-control scope produced the result, every returned event, every inclusion proof, the witness's `treeSize` and `rootHash` at response time and the `timestamp`. The `proofs` array has one entry per event, keyed by `eventId`; verifiers MUST reject if proofs do not match events one-to-one. `treeSize` and `rootHash` apply uniformly to every proof. The `requester` binding prevents cross-requester replay: a witness response generated for Alice cannot be presented to Bob as Bob's authoritative view of the same `messageId`. Verifiers MUST check `requester` equals the locally authenticated requester before treating the response as their own scoped view.
|
|
565
|
+
|
|
566
|
+
Per-event scope: a signed envelope binds `messageId` and `requester` but says nothing about the event objects until verifiers look inside them. To prevent a witness or a tampering intermediary from smuggling out-of-scope events into a signed response, verifiers MUST reject any response where, for any returned event, `event.messageId` differs from the envelope `messageId`, OR the envelope `requester` is neither `event.agentId` nor `event.counterpartyId`. Witnesses SHOULD reject the same conditions at signing time as defense in depth against storage corruption.
|
|
567
|
+
|
|
568
|
+
Empty-log responses: a witness that has not yet committed any leaves reports `treeSize: 0` and `rootHash` equal to SHA-256 of the empty string (`e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`). A signed response with `treeSize: 0` is legitimate but MUST also have empty `events`, empty `proofs` and the empty-tree `rootHash`. Verifiers MUST reject any `treeSize: 0` response that deviates from this shape.
|
|
569
|
+
|
|
570
|
+
Per-event agent signatures: Merkle validity alone does NOT prove a returned event was produced by the agent named in `event.agentId`. A witness could in principle commit a fabricated event_json that is not a real `InkAuditEvent`. Every returned event MUST therefore include its `agentSignature` field. Verifiers MUST resolve the submitting agent's published Ed25519 keys (via Agent Card §2) and verify `agentSignature` on every event in addition to walking the Merkle proof. A response that omits `agentSignature` on any event MUST be rejected as structurally invalid.
|
|
571
|
+
|
|
572
|
+
Truncation: witnesses MUST NOT silently sign a partial result. If the requester's visible event set for a `messageId` exceeds the witness's response cap, the witness MUST return an unsigned error response (HTTP 413). A signed response is, by definition, a complete enumeration of the requester's visible events for that `messageId` at `(treeSize, rootHash)`.
|
|
573
|
+
|
|
574
|
+
Determinism: witnesses MUST emit `events` and matching `proofs` in a stable, deterministic order so verifiers can reproduce the signed bytes from the underlying records.
|
|
575
|
+
|
|
576
|
+
Leaf hash: each event's Merkle leaf hash is `SHA-256(0x00 || JCS(event-without-agentSignature))`. The leading `0x00` byte is the RFC 6962 leaf-domain-separation tag; internal Merkle nodes use `0x01 || left || right`. Verifiers MUST rehash the returned `event` object themselves (stripping `agentSignature`, then JCS, then SHA-256 with the `0x00` prefix) and use that hash as the leaf input to `inclusionProof`. They MUST NOT trust any leaf-hash value supplied by the witness alongside the event. Walking the proof from this computed leaf hash up through `inclusionProof` MUST reach the top-level `rootHash` per the proof construction in §7.2. The INK library exposes this exact computation as `computeAuditMerkleLeafHash`; it is distinct from `computeEventHash`, which is the unprefixed SHA-256 used for `previousEventHash` chain linkage and MUST NOT be used as the Merkle leaf input.
|
|
577
|
+
|
|
555
578
|
#### 7.4 Access Control
|
|
556
579
|
|
|
557
580
|
The audit service operates under **access-controlled transparency** (per SCITT):
|
|
558
581
|
- Events are tagged with the `messageId` and the DIDs of sender/recipient
|
|
559
|
-
- Only the sender
|
|
582
|
+
- Only the sender or recipient (i.e. an event's own `agentId` or `counterpartyId`) can query events for a given `messageId`. The witness MUST refuse to serve a row to any other requester
|
|
560
583
|
- The service verifies the requester's identity via INK auth (§3.3) before serving events
|
|
561
|
-
- The Merkle tree structure is public
|
|
584
|
+
- The Merkle tree structure is public: anyone can verify inclusion proofs against signed checkpoints and, where consistency-proof endpoints are deployed, cross-check that checkpoints are append-only. Event contents remain access-controlled
|
|
585
|
+
|
|
586
|
+
Delegated queries (where a third-party agent queries events on behalf of a principal via an INK Authorization Chain) are not in scope for alpha.3. A future revision will define the additional envelope fields the witness signs to bind the effective principal alongside the immediate requester, and verifiers will be updated accordingly. Until then, conformant witnesses MUST treat the per-event scope rule in §7.3 as authoritative: a returned event's `agentId` or `counterpartyId` MUST equal the response `requester`.
|
|
562
587
|
|
|
563
588
|
This follows SCITT's model: the transparency guarantee (append-only, no suppression) is public, but the data itself is private.
|
|
564
589
|
|
|
565
590
|
#### 7.5 Trust Model
|
|
566
591
|
|
|
567
592
|
The audit service is a **semi-trusted witness**, not an arbiter:
|
|
568
|
-
- It CANNOT forge events
|
|
593
|
+
- It CANNOT forge events that verifiers will accept, because verifiers re-check `event.agentSignature` against the agent's published Ed25519 keys (§7.3). A witness that commits a fabricated event_json into its Merkle tree can produce a valid inclusion proof, but the per-event agent-signature check fails and the response is rejected. Verifiers that walk the proof without checking `agentSignature` lose this guarantee.
|
|
569
594
|
- It CANNOT modify events without breaking Merkle proofs
|
|
570
595
|
- It CAN suppress events by refusing to include them (detectable via consistency proofs between submissions)
|
|
571
596
|
- It CAN be unavailable (agents fall back to bilateral exchange)
|
|
@@ -16,7 +16,7 @@ This checklist lets an independent implementer verify INK conformance without re
|
|
|
16
16
|
- **SHOULD**, recommended; deviations require justification
|
|
17
17
|
- **MAY**, truly optional; advertised via capability
|
|
18
18
|
|
|
19
|
-
**Status column** applies to the Tulpa
|
|
19
|
+
**Status column** applies to the Tulpa implementation:
|
|
20
20
|
- **Required**, part of the v1 wire contract
|
|
21
21
|
- **Optional**, capability-gated, not assumed
|
|
22
22
|
- **Extension**, defined but not required for base interop
|
|
@@ -145,6 +145,14 @@ This checklist lets an independent implementer verify INK conformance without re
|
|
|
145
145
|
| W6 | Checkpoint: C2SP tlog-checkpoint format at `GET /ink/v1/checkpoint` | SHOULD | Optional | Auditability §7 |, | `witness/witness/test/endpoints.test.ts (witness repo)` |
|
|
146
146
|
| W7 | Transport auth on submit: dual signature (transport + event) | MUST | Optional | Auditability §7 | `witness.json` | `witness/witness/test/endpoints.test.ts (witness repo)` |
|
|
147
147
|
| W8 | Submit includes `signingKeyId` in transport auth | SHOULD | Required | Key Rotation Phase 3 |, | `test/ink-key-rotation.test.ts` |
|
|
148
|
+
| W9 | Query response is the signed `network.tulpa.audit_query_response` envelope binding `serviceDid`, `messageId`, `requester`, `events`, `proofs`, `treeSize`, `rootHash`, `timestamp` | MUST | Optional | Auditability §7.3 | `witness.json` | `test/audit-query-response.test.ts`, `test/verify-audit-query-response.test.ts` |
|
|
149
|
+
| W10 | Per-event Merkle proof rule: leaf = `SHA-256(0x00 \|\| JCS(event-without-agentSignature))` (RFC 6962) | MUST | Optional | Auditability §7.3 | `witness.json` | `test/merkle-leaf-hash.test.ts` |
|
|
150
|
+
| W11 | Per-event scope: `event.messageId == envelope.messageId` AND `envelope.requester ∈ {event.agentId, event.counterpartyId}` | MUST | Optional | Auditability §7.3, §7.4 |, | `test/verify-audit-query-response.test.ts` |
|
|
151
|
+
| W12 | Deterministic result-set ordering so signed bytes are reproducible | MUST | Optional | Auditability §7.3 |, | `witness/witness/test/security-round12.test.ts (witness repo)` |
|
|
152
|
+
| W13 | Fail-closed on truncation: refuse to sign a partial result; return unsigned 413 | MUST | Optional | Auditability §7.3 |, | `witness/witness/test/security-round12.test.ts (witness repo)` |
|
|
153
|
+
| W14 | Fail-closed on storage integrity (event_hash mismatch, missing Merkle node, column-vs-event_json drift): HTTP 500, no signed response | MUST | Optional | Auditability §7.3 |, | `witness/witness/test/security-round12.test.ts (witness repo)` |
|
|
154
|
+
| W15 | Empty-log response: `treeSize == 0` MUST have empty `events`, empty `proofs` and canonical empty-tree `rootHash` | MUST | Optional | Auditability §7.3 |, | `test/verify-audit-query-response.test.ts` |
|
|
155
|
+
| W16 | Every returned event MUST include `agentSignature`; verifiers MUST verify it against the agent's published keys (witness Merkle validity does not prove agent provenance) | MUST | Optional | Auditability §7.3, §7.5 |, | `test/verify-audit-query-response.test.ts` |
|
|
148
156
|
|
|
149
157
|
---
|
|
150
158
|
|
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* INK Auditability Section 7 inclusion-receipt verification.
|
|
3
|
-
*
|
|
4
|
-
* A witness returns a signed inclusion receipt when an agent submits
|
|
5
|
-
* an audit event. The receipt commits the witness to a specific
|
|
6
|
-
* (leafIndex, treeSize, rootHash) for the submitted event.
|
|
7
|
-
*
|
|
8
|
-
* To verify a receipt independently:
|
|
9
|
-
* 1. Check the witness's serviceSignature against its published
|
|
10
|
-
* Ed25519 public key. The signed bytes are
|
|
11
|
-
* `ink/audit-inclusion/v1\n` + JCS({eventId, leafIndex, treeSize,
|
|
12
|
-
* rootHash, timestamp}).
|
|
13
|
-
* 2. (Optional) Re-hash the audit event to derive the leaf hash and
|
|
14
|
-
* walk the inclusion proof up to the witness's claimed rootHash.
|
|
15
|
-
* 3. (Optional) Cross-check the receipt against a later signed
|
|
16
|
-
* checkpoint: the tree only grew (treeSize >= receipt.treeSize)
|
|
17
|
-
* and if equal, the rootHash matches.
|
|
18
|
-
*
|
|
19
|
-
* This module ships the pure verification logic. The bin/verify-inclusion
|
|
20
|
-
* CLI is a thin wrapper that fetches the witness DID document + a
|
|
21
|
-
* current checkpoint and calls verifyInclusionReceipt.
|
|
22
|
-
*/
|
|
23
|
-
import * as ed from "@noble/ed25519";
|
|
24
|
-
import { base64urlDecode, jcsCanonicalize, hexToBytes, bytesToHex } from "../crypto/ink.js";
|
|
25
|
-
|
|
26
|
-
export interface InclusionReceipt {
|
|
27
|
-
eventId: string;
|
|
28
|
-
leafIndex: number;
|
|
29
|
-
treeSize: number;
|
|
30
|
-
rootHash: string;
|
|
31
|
-
inclusionProof: string[];
|
|
32
|
-
/** ISO 8601 timestamp at which the witness committed the leaf. */
|
|
33
|
-
timestamp: string;
|
|
34
|
-
/** Base64url Ed25519 signature over the canonical bytes. */
|
|
35
|
-
serviceSignature: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface VerifyStep {
|
|
39
|
-
name: string;
|
|
40
|
-
pass: boolean;
|
|
41
|
-
detail?: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface InclusionReceiptVerifyResult {
|
|
45
|
-
valid: boolean;
|
|
46
|
-
steps: VerifyStep[];
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Verify an INK inclusion receipt.
|
|
51
|
-
*
|
|
52
|
-
* Always performs:
|
|
53
|
-
* - Structural validation of the receipt object
|
|
54
|
-
* - Service signature verification against `witnessPublicKey`
|
|
55
|
-
*
|
|
56
|
-
* Optionally performs (when the corresponding input is provided):
|
|
57
|
-
* - Leaf-to-root proof walk (`eventHash`)
|
|
58
|
-
* - Cross-check against a later signed checkpoint (`laterCheckpoint`)
|
|
59
|
-
*/
|
|
60
|
-
export async function verifyInclusionReceipt(opts: {
|
|
61
|
-
receipt: InclusionReceipt;
|
|
62
|
-
/** Raw 32-byte Ed25519 public key of the witness service. */
|
|
63
|
-
witnessPublicKey: Uint8Array;
|
|
64
|
-
/** Optional leaf hash (SHA-256 of JCS(audit event without agentSignature),
|
|
65
|
-
* hex-encoded). When provided, the inclusion proof is walked from
|
|
66
|
-
* the leaf up to the claimed rootHash. */
|
|
67
|
-
eventHash?: string;
|
|
68
|
-
/** Optional later checkpoint to cross-check the receipt against.
|
|
69
|
-
* Must come from a `/ink/v1/checkpoint` response that the verifier
|
|
70
|
-
* has separately validated as authentic. */
|
|
71
|
-
laterCheckpoint?: { treeSize: number; rootHash: string };
|
|
72
|
-
}): Promise<InclusionReceiptVerifyResult> {
|
|
73
|
-
const steps: VerifyStep[] = [];
|
|
74
|
-
const { receipt, witnessPublicKey, eventHash, laterCheckpoint } = opts;
|
|
75
|
-
|
|
76
|
-
// ── Step 1: structural validation ──
|
|
77
|
-
const structuralProblem = checkReceiptShape(receipt);
|
|
78
|
-
if (structuralProblem) {
|
|
79
|
-
steps.push({ name: "structure", pass: false, detail: structuralProblem });
|
|
80
|
-
return { valid: false, steps };
|
|
81
|
-
}
|
|
82
|
-
steps.push({ name: "structure", pass: true });
|
|
83
|
-
|
|
84
|
-
// ── Step 2: signature ──
|
|
85
|
-
const signedPayload = {
|
|
86
|
-
eventId: receipt.eventId,
|
|
87
|
-
leafIndex: receipt.leafIndex,
|
|
88
|
-
treeSize: receipt.treeSize,
|
|
89
|
-
rootHash: receipt.rootHash,
|
|
90
|
-
timestamp: receipt.timestamp,
|
|
91
|
-
};
|
|
92
|
-
const sigBase = `ink/audit-inclusion/v1\n${jcsCanonicalize(signedPayload)}`;
|
|
93
|
-
let sigValid = false;
|
|
94
|
-
try {
|
|
95
|
-
const sig = base64urlDecode(receipt.serviceSignature);
|
|
96
|
-
sigValid = await ed.verifyAsync(sig, new TextEncoder().encode(sigBase), witnessPublicKey);
|
|
97
|
-
} catch (e) {
|
|
98
|
-
steps.push({
|
|
99
|
-
name: "signature",
|
|
100
|
-
pass: false,
|
|
101
|
-
detail: e instanceof Error ? e.message : "signature decode failed",
|
|
102
|
-
});
|
|
103
|
-
return { valid: false, steps };
|
|
104
|
-
}
|
|
105
|
-
if (!sigValid) {
|
|
106
|
-
steps.push({ name: "signature", pass: false, detail: "Ed25519 verification failed" });
|
|
107
|
-
return { valid: false, steps };
|
|
108
|
-
}
|
|
109
|
-
steps.push({ name: "signature", pass: true });
|
|
110
|
-
|
|
111
|
-
// ── Step 3: inclusion-proof walk (optional) ──
|
|
112
|
-
if (eventHash !== undefined) {
|
|
113
|
-
if (!/^[0-9a-f]{64}$/.test(eventHash)) {
|
|
114
|
-
steps.push({ name: "proof", pass: false, detail: "eventHash must be 64 lowercase hex chars" });
|
|
115
|
-
return { valid: false, steps };
|
|
116
|
-
}
|
|
117
|
-
const verified = await verifyInclusionProof(
|
|
118
|
-
eventHash,
|
|
119
|
-
receipt.inclusionProof,
|
|
120
|
-
receipt.leafIndex,
|
|
121
|
-
receipt.treeSize,
|
|
122
|
-
receipt.rootHash,
|
|
123
|
-
);
|
|
124
|
-
if (!verified) {
|
|
125
|
-
steps.push({ name: "proof", pass: false, detail: "leaf-to-root walk did not reach claimed rootHash" });
|
|
126
|
-
return { valid: false, steps };
|
|
127
|
-
}
|
|
128
|
-
steps.push({ name: "proof", pass: true });
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// ── Step 4: later-checkpoint cross-check (optional) ──
|
|
132
|
-
if (laterCheckpoint !== undefined) {
|
|
133
|
-
const cpShape = checkCheckpointShape(laterCheckpoint);
|
|
134
|
-
if (cpShape) {
|
|
135
|
-
steps.push({ name: "checkpoint", pass: false, detail: cpShape });
|
|
136
|
-
return { valid: false, steps };
|
|
137
|
-
}
|
|
138
|
-
if (laterCheckpoint.treeSize < receipt.treeSize) {
|
|
139
|
-
steps.push({
|
|
140
|
-
name: "checkpoint",
|
|
141
|
-
pass: false,
|
|
142
|
-
detail: `checkpoint treeSize ${laterCheckpoint.treeSize} < receipt treeSize ${receipt.treeSize} (witness rewound the tree)`,
|
|
143
|
-
});
|
|
144
|
-
return { valid: false, steps };
|
|
145
|
-
}
|
|
146
|
-
if (laterCheckpoint.treeSize === receipt.treeSize && laterCheckpoint.rootHash !== receipt.rootHash) {
|
|
147
|
-
steps.push({
|
|
148
|
-
name: "checkpoint",
|
|
149
|
-
pass: false,
|
|
150
|
-
detail: "checkpoint rootHash differs from receipt rootHash at same treeSize (fork)",
|
|
151
|
-
});
|
|
152
|
-
return { valid: false, steps };
|
|
153
|
-
}
|
|
154
|
-
steps.push({ name: "checkpoint", pass: true });
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return { valid: true, steps };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// ── Internal helpers ──
|
|
161
|
-
|
|
162
|
-
/** Generous upper bound on inclusion-proof length. Real proofs are
|
|
163
|
-
* ceil(log2(treeSize)) entries; a treeSize > 2^60 is implausible for
|
|
164
|
-
* any real log, so capping at 64 entries bounds memory + walker depth
|
|
165
|
-
* without rejecting legitimate input. The signed payload binds
|
|
166
|
-
* treeSize but not the proof array itself, so an attacker could
|
|
167
|
-
* otherwise append unbounded garbage to a valid receipt. */
|
|
168
|
-
const MAX_PROOF_LENGTH = 64;
|
|
169
|
-
|
|
170
|
-
function checkReceiptShape(receipt: InclusionReceipt): string | null {
|
|
171
|
-
if (receipt === null || typeof receipt !== "object") return "receipt is not an object";
|
|
172
|
-
if (typeof receipt.eventId !== "string" || receipt.eventId.length === 0) return "eventId missing";
|
|
173
|
-
if (!Number.isInteger(receipt.leafIndex) || receipt.leafIndex < 0) return "leafIndex must be non-negative integer";
|
|
174
|
-
if (!Number.isInteger(receipt.treeSize) || receipt.treeSize < 1) return "treeSize must be positive integer";
|
|
175
|
-
if (receipt.leafIndex >= receipt.treeSize) return "leafIndex must be < treeSize";
|
|
176
|
-
if (typeof receipt.rootHash !== "string" || !/^[0-9a-f]{64}$/.test(receipt.rootHash)) {
|
|
177
|
-
return "rootHash must be 64 lowercase hex chars";
|
|
178
|
-
}
|
|
179
|
-
if (!Array.isArray(receipt.inclusionProof)) return "inclusionProof must be an array";
|
|
180
|
-
if (receipt.inclusionProof.length > MAX_PROOF_LENGTH) {
|
|
181
|
-
return `inclusionProof exceeds max length of ${MAX_PROOF_LENGTH} entries`;
|
|
182
|
-
}
|
|
183
|
-
for (const p of receipt.inclusionProof) {
|
|
184
|
-
if (typeof p !== "string" || !/^[0-9a-f]{64}$/.test(p)) {
|
|
185
|
-
return "every inclusionProof entry must be 64 lowercase hex chars";
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
if (typeof receipt.timestamp !== "string" || receipt.timestamp.length === 0) return "timestamp missing";
|
|
189
|
-
if (typeof receipt.serviceSignature !== "string" || receipt.serviceSignature.length === 0) {
|
|
190
|
-
return "serviceSignature missing";
|
|
191
|
-
}
|
|
192
|
-
return null;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function checkCheckpointShape(cp: { treeSize: number; rootHash: string }): string | null {
|
|
196
|
-
if (cp === null || typeof cp !== "object") return "laterCheckpoint must be an object";
|
|
197
|
-
if (!Number.isInteger(cp.treeSize) || cp.treeSize < 0) {
|
|
198
|
-
return "laterCheckpoint.treeSize must be a non-negative integer";
|
|
199
|
-
}
|
|
200
|
-
if (typeof cp.rootHash !== "string" || !/^[0-9a-f]{64}$/.test(cp.rootHash)) {
|
|
201
|
-
return "laterCheckpoint.rootHash must be 64 lowercase hex chars";
|
|
202
|
-
}
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
async function hashPair(left: string, right: string): Promise<string> {
|
|
207
|
-
const l = hexToBytes(left);
|
|
208
|
-
const r = hexToBytes(right);
|
|
209
|
-
const buf = new Uint8Array(1 + l.length + r.length);
|
|
210
|
-
buf[0] = 0x01;
|
|
211
|
-
buf.set(l, 1);
|
|
212
|
-
buf.set(r, 1 + l.length);
|
|
213
|
-
const out = new Uint8Array(await crypto.subtle.digest("SHA-256", buf));
|
|
214
|
-
return bytesToHex(out);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function largestPowerOf2LessThan(n: number): number {
|
|
218
|
-
if (n <= 1) return 0;
|
|
219
|
-
let p = 1;
|
|
220
|
-
while (p * 2 < n) p *= 2;
|
|
221
|
-
return p;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
async function recomputeRoot(
|
|
225
|
-
currentHash: string,
|
|
226
|
-
proof: string[],
|
|
227
|
-
proofIdx: number,
|
|
228
|
-
leafIndex: number,
|
|
229
|
-
start: number,
|
|
230
|
-
size: number,
|
|
231
|
-
): Promise<string> {
|
|
232
|
-
if (size === 1) {
|
|
233
|
-
// Reached the leaf. Any proof entries left over mean the proof was
|
|
234
|
-
// padded with extras; reject it as malformed.
|
|
235
|
-
if (proofIdx !== proof.length) throw new Error("inclusion proof has unused entries");
|
|
236
|
-
return currentHash;
|
|
237
|
-
}
|
|
238
|
-
if (proofIdx >= proof.length) {
|
|
239
|
-
// Proof exhausted before walking down to the leaf. Without this,
|
|
240
|
-
// an attacker can present a short proof against a tree > 1 leaf
|
|
241
|
-
// and the walker returns currentHash (the leaf), which a verifier
|
|
242
|
-
// might mistakenly equate to rootHash.
|
|
243
|
-
throw new Error("inclusion proof too short for declared treeSize");
|
|
244
|
-
}
|
|
245
|
-
const split = largestPowerOf2LessThan(size);
|
|
246
|
-
if (leafIndex - start < split) {
|
|
247
|
-
const leftResult = await recomputeRoot(currentHash, proof, proofIdx + 1, leafIndex, start, split);
|
|
248
|
-
return hashPair(leftResult, proof[proofIdx]!);
|
|
249
|
-
}
|
|
250
|
-
const rightResult = await recomputeRoot(currentHash, proof, proofIdx + 1, leafIndex, start + split, size - split);
|
|
251
|
-
return hashPair(proof[proofIdx]!, rightResult);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
async function verifyInclusionProof(
|
|
255
|
-
leafHash: string,
|
|
256
|
-
proof: string[],
|
|
257
|
-
leafIndex: number,
|
|
258
|
-
treeSize: number,
|
|
259
|
-
expectedRootHash: string,
|
|
260
|
-
): Promise<boolean> {
|
|
261
|
-
if (leafIndex < 0 || leafIndex >= treeSize) return false;
|
|
262
|
-
try {
|
|
263
|
-
const computed = await recomputeRoot(leafHash, proof, 0, leafIndex, 0, treeSize);
|
|
264
|
-
return computed === expectedRootHash;
|
|
265
|
-
} catch {
|
|
266
|
-
return false;
|
|
267
|
-
}
|
|
268
|
-
}
|