@forestrie/receipt-verify 0.5.0 → 0.6.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/dist/build-receipt-offline.d.ts.map +1 -1
- package/dist/build-receipt-offline.js +5 -8
- package/dist/decode-forestrie-grant-cose.js +2 -2
- package/dist/decode-trust-root-from-genesis.js +2 -2
- package/dist/find-grant-leaf.d.ts +37 -0
- package/dist/find-grant-leaf.d.ts.map +1 -0
- package/dist/find-grant-leaf.js +39 -0
- package/dist/grant-codec.d.ts.map +1 -1
- package/dist/grant-codec.js +4 -4
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/parse-receipt.js +2 -2
- package/package.json +3 -4
- package/src/build-receipt-offline.ts +8 -8
- package/src/decode-forestrie-grant-cose.ts +2 -2
- package/src/decode-trust-root-from-genesis.ts +2 -2
- package/src/find-grant-leaf.ts +46 -0
- package/src/grant-codec.ts +7 -4
- package/src/index.ts +8 -0
- package/src/parse-receipt.ts +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-receipt-offline.d.ts","sourceRoot":"","sources":["../src/build-receipt-offline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;
|
|
1
|
+
{"version":3,"file":"build-receipt-offline.d.ts","sourceRoot":"","sources":["../src/build-receipt-offline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH,OAAO,EAIL,mBAAmB,EAEnB,KAAK,eAAe,EAErB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,oBAAoB,CAAC;AAQ5B;;;;GAIG;AACH,OAAO,EAAE,mBAAmB,EAAE,CAAC;AAC/B,YAAY,EAAE,eAAe,EAAE,CAAC;AAEhC,MAAM,MAAM,gBAAgB,GAAG;IAC7B,SAAS,EAAE,SAAS,CAAC;IACrB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,oEAAoE;IACpE,YAAY,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAC/B,wEAAwE;IACxE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,cAAc,EAAE,UAAU,GAAG,IAAI,CAAC;CACnC,CAAC;AAEF,wDAAwD;AACxD,wBAAgB,eAAe,CAAC,eAAe,EAAE,UAAU,GAAG,gBAAgB,CAqB7E;AAED,MAAM,MAAM,wBAAwB,GAAG;IACrC,WAAW,EAAE,UAAU,CAAC;IACxB,eAAe,EAAE,UAAU,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,wBAAwB,GAC9B,UAAU,CA2EZ;AAED,MAAM,MAAM,uBAAuB,GAAG;IACpC,mEAAmE;IACnE,IAAI,EAAE,UAAU,CAAC;IACjB,+DAA+D;IAC/D,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,SAAS,EAAE,UAAU,CAAC;CACvB,CAAC;AAEF;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAAC,IAAI,EAAE;IACjD,WAAW,EAAE,UAAU,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAwBnC"}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* at an externally attested tree size (e.g. Univocity `logState` on Base
|
|
15
15
|
* Sepolia) and compare the computed peak with the published accumulator.
|
|
16
16
|
*/
|
|
17
|
-
import {
|
|
17
|
+
import { decodeCborDeterministic, encodeCborDeterministic, } from "@forestrie/encoding";
|
|
18
18
|
import { calculateRoot, inclusionProof, massifIndexFromMMRIndex, openMassifNodeStore, peakIndexForLeafProof, } from "@forestrie/merklelog";
|
|
19
19
|
import { requireCoseSign1, toHeaderMap, unwrapCoseSign1Tag, } from "./parse-receipt.js";
|
|
20
20
|
import { SubtleHasher } from "./subtle-hasher.js";
|
|
@@ -30,7 +30,7 @@ const VDP_CONSISTENCY_PROOF_KEY = -2;
|
|
|
30
30
|
export { openMassifNodeStore };
|
|
31
31
|
/** Parse a format-v3 checkpoint (`.sth`) COSE Sign1. */
|
|
32
32
|
export function parseCheckpoint(checkpointBytes) {
|
|
33
|
-
const decoded =
|
|
33
|
+
const decoded = decodeCborDeterministic(checkpointBytes);
|
|
34
34
|
const coseSign1 = requireCoseSign1(unwrapCoseSign1Tag(decoded));
|
|
35
35
|
const unprotected = toHeaderMap(coseSign1[1]);
|
|
36
36
|
const peakReceiptsRaw = unprotected.get(SEAL_PEAK_RECEIPTS_LABEL);
|
|
@@ -84,7 +84,7 @@ export function buildReceiptOffline(input) {
|
|
|
84
84
|
if (!(receiptBytes instanceof Uint8Array)) {
|
|
85
85
|
throw new Error(`checkpoint peak receipt slot ${peakIdx} is missing or not bstr`);
|
|
86
86
|
}
|
|
87
|
-
const receiptSign1 = requireCoseSign1(unwrapCoseSign1Tag(
|
|
87
|
+
const receiptSign1 = requireCoseSign1(unwrapCoseSign1Tag(decodeCborDeterministic(receiptBytes)));
|
|
88
88
|
const receiptUnprotected = toHeaderMap(receiptSign1[1]);
|
|
89
89
|
if (checkpoint.delegationCert) {
|
|
90
90
|
receiptUnprotected.set(DELEGATION_CERT_LABEL, checkpoint.delegationCert);
|
|
@@ -134,10 +134,7 @@ export async function computeAccumulatorPeak(opts) {
|
|
|
134
134
|
}
|
|
135
135
|
// --- helpers ---
|
|
136
136
|
function cborBytes(value) {
|
|
137
|
-
|
|
138
|
-
return encoded instanceof Uint8Array
|
|
139
|
-
? encoded
|
|
140
|
-
: new Uint8Array(encoded);
|
|
137
|
+
return encodeCborDeterministic(value);
|
|
141
138
|
}
|
|
142
139
|
/**
|
|
143
140
|
* Sealed mmr size from a format-v3 checkpoint: tree-size-2 of the consistency
|
|
@@ -152,7 +149,7 @@ function sealedSizeFromCheckpoint(unprotected) {
|
|
|
152
149
|
const proofBstr = vdp.get(VDP_CONSISTENCY_PROOF_KEY);
|
|
153
150
|
if (!(proofBstr instanceof Uint8Array))
|
|
154
151
|
return null;
|
|
155
|
-
const proof =
|
|
152
|
+
const proof = decodeCborDeterministic(proofBstr);
|
|
156
153
|
if (!Array.isArray(proof) || proof.length < 2)
|
|
157
154
|
return null;
|
|
158
155
|
const treeSize2 = proof[1];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { sha256 } from "@noble/hashes/sha256";
|
|
2
|
-
import {
|
|
2
|
+
import { decodeCborDeterministic } from "@forestrie/encoding";
|
|
3
3
|
import { HEADER_FORESTRIE_GRANT_V0, HEADER_IDTIMESTAMP, } from "./forest-genesis-labels.js";
|
|
4
4
|
import { decodeGrantPayload } from "./grant-codec.js";
|
|
5
5
|
const IDTIMESTAMP_BYTES = 8;
|
|
@@ -31,7 +31,7 @@ function digestEquals(a, b) {
|
|
|
31
31
|
* Decode Forestrie-Grant COSE Sign1 (Custodian transparent statement profile).
|
|
32
32
|
*/
|
|
33
33
|
export function decodeForestrieGrantCose(bytes) {
|
|
34
|
-
const raw =
|
|
34
|
+
const raw = decodeCborDeterministic(bytes);
|
|
35
35
|
const arr = Array.isArray(raw) ? raw : null;
|
|
36
36
|
if (!arr || arr.length !== 4) {
|
|
37
37
|
throw new Error("Forestrie-Grant must be COSE Sign1 (array of 4)");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { decodeCborDeterministic } from "@forestrie/encoding";
|
|
2
2
|
import { COSE_ALG_ES256, COSE_CRV_P256, COSE_EC2_CRV, COSE_EC2_X, COSE_EC2_Y, COSE_KEY_ALG, COSE_KEY_KTY, COSE_KTY_EC2, } from "./cose-key.js";
|
|
3
3
|
import { FOREST_GENESIS_LABEL_BOOTSTRAP_KEY, FOREST_GENESIS_LABEL_GENESIS_ALG, FOREST_GENESIS_LABEL_GENESIS_VERSION, FOREST_GENESIS_SCHEMA_V1, FOREST_GENESIS_SCHEMA_V2, } from "./forest-genesis-labels.js";
|
|
4
4
|
import { decodeTrustRootCbor } from "./decode-trust-root-cbor.js";
|
|
@@ -26,7 +26,7 @@ function asGenesisUint8Array(v) {
|
|
|
26
26
|
export async function decodeTrustRootFromGenesis(genesisCbor) {
|
|
27
27
|
let raw;
|
|
28
28
|
try {
|
|
29
|
-
raw =
|
|
29
|
+
raw = decodeCborDeterministic(genesisCbor);
|
|
30
30
|
}
|
|
31
31
|
catch {
|
|
32
32
|
throw new Error("genesis CBOR decode failed");
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline grant-leaf lookup (FOR-344): locate a grant's sequenced leaf in a
|
|
3
|
+
* local massif blob using only the grant statement and the massif `.log`.
|
|
4
|
+
*
|
|
5
|
+
* The sequencer commits a grant to the log under its grant commitment hash
|
|
6
|
+
* (`grantCommitmentHashFromGrant` — the ContentHash enqueued in
|
|
7
|
+
* canopy-api `grant-sequencing.ts`), and the v2 urkle index region records that
|
|
8
|
+
* hash in each leaf's value column keyed by the leaf's 8-byte idtimestamp. So a
|
|
9
|
+
* client holding the owner-log massif can recover BOTH halves of the permanent
|
|
10
|
+
* entry id (`idtimestamp_be8 || mmrIndex_be8`) with no operator round-trip —
|
|
11
|
+
* the derivation that lets `forestrie complete-grant` build the completed grant
|
|
12
|
+
* header offline (grants are derivable from log data, not operator-issued).
|
|
13
|
+
*
|
|
14
|
+
* Pure over bytes; no network, no signing key. Browser-safe (ADR-0048).
|
|
15
|
+
*
|
|
16
|
+
* The lookup is a linear scan of the leaf-table value column. That is optimal
|
|
17
|
+
* here: recovering the entry id needs the leaf's *position* (ordinal → mmrIndex
|
|
18
|
+
* + idtimestamp key), and no content-hash→position index exists in the blob.
|
|
19
|
+
* The index region's bloom filter 0 IS keyed on the content hash, but a bloom
|
|
20
|
+
* only answers membership (maybe/definitely-not), never position — so it could
|
|
21
|
+
* at best short-circuit the not-present case, not the recovery; the positional
|
|
22
|
+
* structure (the urkle trie) is keyed by idtimestamp, which is exactly what we
|
|
23
|
+
* are recovering. Building a value→position map would itself be an O(n) scan.
|
|
24
|
+
*/
|
|
25
|
+
import { type LocatedLeaf } from "@forestrie/merklelog";
|
|
26
|
+
import type { Grant } from "@forestrie/encoding";
|
|
27
|
+
export type { LocatedLeaf };
|
|
28
|
+
/**
|
|
29
|
+
* Find the grant's leaf in `massifBytes` and return its `mmrIndex` and the
|
|
30
|
+
* 8-byte idtimestamp key, or `null` when the grant's commitment hash is not
|
|
31
|
+
* present in this massif's index region (searched, not found).
|
|
32
|
+
*
|
|
33
|
+
* Throws `MissingIndexError` (from `@forestrie/merklelog`) when the blob has no
|
|
34
|
+
* populated index region at all — distinguishable from a not-found `null`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function findGrantLeafInMassif(massifBytes: Uint8Array, grant: Grant): Promise<LocatedLeaf | null>;
|
|
37
|
+
//# sourceMappingURL=find-grant-leaf.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"find-grant-leaf.d.ts","sourceRoot":"","sources":["../src/find-grant-leaf.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC7E,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAGjD,YAAY,EAAE,WAAW,EAAE,CAAC;AAE5B;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CACzC,WAAW,EAAE,UAAU,EACvB,KAAK,EAAE,KAAK,GACX,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAI7B"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline grant-leaf lookup (FOR-344): locate a grant's sequenced leaf in a
|
|
3
|
+
* local massif blob using only the grant statement and the massif `.log`.
|
|
4
|
+
*
|
|
5
|
+
* The sequencer commits a grant to the log under its grant commitment hash
|
|
6
|
+
* (`grantCommitmentHashFromGrant` — the ContentHash enqueued in
|
|
7
|
+
* canopy-api `grant-sequencing.ts`), and the v2 urkle index region records that
|
|
8
|
+
* hash in each leaf's value column keyed by the leaf's 8-byte idtimestamp. So a
|
|
9
|
+
* client holding the owner-log massif can recover BOTH halves of the permanent
|
|
10
|
+
* entry id (`idtimestamp_be8 || mmrIndex_be8`) with no operator round-trip —
|
|
11
|
+
* the derivation that lets `forestrie complete-grant` build the completed grant
|
|
12
|
+
* header offline (grants are derivable from log data, not operator-issued).
|
|
13
|
+
*
|
|
14
|
+
* Pure over bytes; no network, no signing key. Browser-safe (ADR-0048).
|
|
15
|
+
*
|
|
16
|
+
* The lookup is a linear scan of the leaf-table value column. That is optimal
|
|
17
|
+
* here: recovering the entry id needs the leaf's *position* (ordinal → mmrIndex
|
|
18
|
+
* + idtimestamp key), and no content-hash→position index exists in the blob.
|
|
19
|
+
* The index region's bloom filter 0 IS keyed on the content hash, but a bloom
|
|
20
|
+
* only answers membership (maybe/definitely-not), never position — so it could
|
|
21
|
+
* at best short-circuit the not-present case, not the recovery; the positional
|
|
22
|
+
* structure (the urkle trie) is keyed by idtimestamp, which is exactly what we
|
|
23
|
+
* are recovering. Building a value→position map would itself be an O(n) scan.
|
|
24
|
+
*/
|
|
25
|
+
import { openMassifLeafIndex } from "@forestrie/merklelog";
|
|
26
|
+
import { grantCommitmentHashFromGrant } from "./grant-commitment.js";
|
|
27
|
+
/**
|
|
28
|
+
* Find the grant's leaf in `massifBytes` and return its `mmrIndex` and the
|
|
29
|
+
* 8-byte idtimestamp key, or `null` when the grant's commitment hash is not
|
|
30
|
+
* present in this massif's index region (searched, not found).
|
|
31
|
+
*
|
|
32
|
+
* Throws `MissingIndexError` (from `@forestrie/merklelog`) when the blob has no
|
|
33
|
+
* populated index region at all — distinguishable from a not-found `null`.
|
|
34
|
+
*/
|
|
35
|
+
export async function findGrantLeafInMassif(massifBytes, grant) {
|
|
36
|
+
const inner = await grantCommitmentHashFromGrant(grant);
|
|
37
|
+
const index = openMassifLeafIndex(massifBytes);
|
|
38
|
+
return index.findLeafByContentHash(inner);
|
|
39
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grant-codec.d.ts","sourceRoot":"","sources":["../src/grant-codec.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"grant-codec.d.ts","sourceRoot":"","sources":["../src/grant-codec.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAuDjD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,GAAG,KAAK,CAO3D;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,UAAU,GAAG;IACtD,KAAK,EAAE,KAAK,CAAC;IACb,WAAW,EAAE,UAAU,CAAC;CACzB,CAsBA;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,KAAK,GAAG,UAAU,CAa3D"}
|
package/dist/grant-codec.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { decodeCborDeterministic, encodeCborDeterministic, } from "@forestrie/encoding";
|
|
2
2
|
import { grantDataToBytes } from "@forestrie/encoding";
|
|
3
3
|
import { fromPaddedWire32, toPaddedWire32 } from "./uuid-bytes.js";
|
|
4
4
|
const CBOR_KEY_IDTIMESTAMP = 0;
|
|
@@ -48,14 +48,14 @@ function mapToGrant(m) {
|
|
|
48
48
|
export function decodeGrantPayload(bytes) {
|
|
49
49
|
if (!bytes?.length)
|
|
50
50
|
throw new Error("Grant payload is empty");
|
|
51
|
-
const raw =
|
|
51
|
+
const raw = decodeCborDeterministic(bytes);
|
|
52
52
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
53
53
|
throw new Error("Grant payload must be a CBOR map");
|
|
54
54
|
}
|
|
55
55
|
return mapToGrant(raw);
|
|
56
56
|
}
|
|
57
57
|
export function decodeGrantResponse(bytes) {
|
|
58
|
-
const raw =
|
|
58
|
+
const raw = decodeCborDeterministic(bytes);
|
|
59
59
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
60
60
|
throw new Error("Grant response must be a CBOR map");
|
|
61
61
|
}
|
|
@@ -84,5 +84,5 @@ export function encodeGrantPayload(grant) {
|
|
|
84
84
|
grantDataToBytes(grant.grantData ?? new Uint8Array(0)),
|
|
85
85
|
],
|
|
86
86
|
]);
|
|
87
|
-
return
|
|
87
|
+
return encodeCborDeterministic(map);
|
|
88
88
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -18,4 +18,11 @@ export { decodeGrantPayload, decodeGrantResponse } from "./grant-codec.js";
|
|
|
18
18
|
export { HEADER_RECEIPT, attachReceiptAndIdtimestampToTransparentStatement, } from "./attach-transparent-statement-receipt.js";
|
|
19
19
|
export { decodeEntryIdHex, entryIdHexToIdtimestampBe8 } from "./entry-id.js";
|
|
20
20
|
export type { DecodedEntryId } from "./entry-id.js";
|
|
21
|
+
/** Grant commitment hash (ContentHash) — the value the sequencer commits. */
|
|
22
|
+
export { grantCommitmentHashFromGrant } from "./grant-commitment.js";
|
|
23
|
+
/** Offline grant-leaf lookup from a local massif blob (FOR-344). */
|
|
24
|
+
export { findGrantLeafInMassif } from "./find-grant-leaf.js";
|
|
25
|
+
export type { LocatedLeaf } from "./find-grant-leaf.js";
|
|
26
|
+
/** Re-exported so callers of findGrantLeafInMassif can catch it (FOR-344). */
|
|
27
|
+
export { MissingIndexError } from "@forestrie/merklelog";
|
|
21
28
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,4BAA4B,CAAC;AACpC,oFAAoF;AACpF,YAAY,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,YAAY,EAAE,8BAA8B,EAAE,MAAM,mCAAmC,CAAC;AACxF,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,YAAY,EACV,wBAAwB,EACxB,uBAAuB,EACvB,eAAe,EACf,gBAAgB,GACjB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,mBAAmB,EACnB,eAAe,GAChB,MAAM,4BAA4B,CAAC;AACpC;;;GAGG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC;AACjF,OAAO,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAC5E,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAE3E,0EAA0E;AAC1E,OAAO,EACL,cAAc,EACd,iDAAiD,GAClD,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC;AAC7E,YAAY,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,4BAA4B,CAAC;AACpC,oFAAoF;AACpF,YAAY,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,YAAY,EAAE,8BAA8B,EAAE,MAAM,mCAAmC,CAAC;AACxF,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,YAAY,EACV,wBAAwB,EACxB,uBAAuB,EACvB,eAAe,EACf,gBAAgB,GACjB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,mBAAmB,EACnB,eAAe,GAChB,MAAM,4BAA4B,CAAC;AACpC;;;GAGG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC;AACjF,OAAO,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAC5E,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAE3E,0EAA0E;AAC1E,OAAO,EACL,cAAc,EACd,iDAAiD,GAClD,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC;AAC7E,YAAY,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAEpD,6EAA6E;AAC7E,OAAO,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AACrE,oEAAoE;AACpE,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,YAAY,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,8EAA8E;AAC9E,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -12,3 +12,9 @@ export { decodeGrantPayload, decodeGrantResponse } from "./grant-codec.js";
|
|
|
12
12
|
/** Deterministic receipt construction (plan-2607-12 Phase 2, FOR-353). */
|
|
13
13
|
export { HEADER_RECEIPT, attachReceiptAndIdtimestampToTransparentStatement, } from "./attach-transparent-statement-receipt.js";
|
|
14
14
|
export { decodeEntryIdHex, entryIdHexToIdtimestampBe8 } from "./entry-id.js";
|
|
15
|
+
/** Grant commitment hash (ContentHash) — the value the sequencer commits. */
|
|
16
|
+
export { grantCommitmentHashFromGrant } from "./grant-commitment.js";
|
|
17
|
+
/** Offline grant-leaf lookup from a local massif blob (FOR-344). */
|
|
18
|
+
export { findGrantLeafInMassif } from "./find-grant-leaf.js";
|
|
19
|
+
/** Re-exported so callers of findGrantLeafInMassif can catch it (FOR-344). */
|
|
20
|
+
export { MissingIndexError } from "@forestrie/merklelog";
|
package/dist/parse-receipt.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { decodeCborDeterministic } from "@forestrie/encoding";
|
|
2
2
|
const VDS_COSE_RECEIPT_PROOFS_TAG = 396;
|
|
3
3
|
export function unwrapCoseSign1Tag(value) {
|
|
4
4
|
if (value && typeof value === "object" && !(value instanceof Map)) {
|
|
@@ -42,7 +42,7 @@ export function toHeaderMap(value) {
|
|
|
42
42
|
return out;
|
|
43
43
|
}
|
|
44
44
|
export function parseReceipt(receiptBytes) {
|
|
45
|
-
const decoded =
|
|
45
|
+
const decoded = decodeCborDeterministic(receiptBytes);
|
|
46
46
|
const unwrapped = unwrapCoseSign1Tag(decoded);
|
|
47
47
|
requireCoseSign1(unwrapped);
|
|
48
48
|
const coseSign1 = requireCoseSign1(unwrapped);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forestrie/receipt-verify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Offline SCITT grant receipt verification (ADR-0045)",
|
|
@@ -28,9 +28,8 @@
|
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@noble/hashes": "^1.7.1",
|
|
31
|
-
"
|
|
32
|
-
"@forestrie/merklelog": "0.1.
|
|
33
|
-
"@forestrie/encoding": "0.2.0"
|
|
31
|
+
"@forestrie/encoding": "0.3.0",
|
|
32
|
+
"@forestrie/merklelog": "0.1.1"
|
|
34
33
|
},
|
|
35
34
|
"devDependencies": {
|
|
36
35
|
"esbuild": "^0.24.0",
|
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
* Sepolia) and compare the computed peak with the published accumulator.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
decodeCborDeterministic,
|
|
20
|
+
encodeCborDeterministic,
|
|
21
|
+
} from "@forestrie/encoding";
|
|
19
22
|
import {
|
|
20
23
|
calculateRoot,
|
|
21
24
|
inclusionProof,
|
|
@@ -58,7 +61,7 @@ export type ParsedCheckpoint = {
|
|
|
58
61
|
|
|
59
62
|
/** Parse a format-v3 checkpoint (`.sth`) COSE Sign1. */
|
|
60
63
|
export function parseCheckpoint(checkpointBytes: Uint8Array): ParsedCheckpoint {
|
|
61
|
-
const decoded =
|
|
64
|
+
const decoded = decodeCborDeterministic(checkpointBytes);
|
|
62
65
|
const coseSign1 = requireCoseSign1(unwrapCoseSign1Tag(decoded));
|
|
63
66
|
const unprotected = toHeaderMap(coseSign1[1]);
|
|
64
67
|
|
|
@@ -143,7 +146,7 @@ export function buildReceiptOffline(
|
|
|
143
146
|
}
|
|
144
147
|
|
|
145
148
|
const receiptSign1 = requireCoseSign1(
|
|
146
|
-
unwrapCoseSign1Tag(
|
|
149
|
+
unwrapCoseSign1Tag(decodeCborDeterministic(receiptBytes)),
|
|
147
150
|
);
|
|
148
151
|
const receiptUnprotected = toHeaderMap(receiptSign1[1]);
|
|
149
152
|
|
|
@@ -220,10 +223,7 @@ export async function computeAccumulatorPeak(opts: {
|
|
|
220
223
|
// --- helpers ---
|
|
221
224
|
|
|
222
225
|
function cborBytes(value: unknown): Uint8Array {
|
|
223
|
-
|
|
224
|
-
return encoded instanceof Uint8Array
|
|
225
|
-
? encoded
|
|
226
|
-
: new Uint8Array(encoded as ArrayLike<number>);
|
|
226
|
+
return encodeCborDeterministic(value);
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
/**
|
|
@@ -239,7 +239,7 @@ function sealedSizeFromCheckpoint(
|
|
|
239
239
|
const vdp = toHeaderMap(vdpRaw as Map<number, unknown>);
|
|
240
240
|
const proofBstr = vdp.get(VDP_CONSISTENCY_PROOF_KEY);
|
|
241
241
|
if (!(proofBstr instanceof Uint8Array)) return null;
|
|
242
|
-
const proof =
|
|
242
|
+
const proof = decodeCborDeterministic(proofBstr);
|
|
243
243
|
if (!Array.isArray(proof) || proof.length < 2) return null;
|
|
244
244
|
const treeSize2 = proof[1];
|
|
245
245
|
if (typeof treeSize2 === "bigint") return treeSize2;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { sha256 } from "@noble/hashes/sha256";
|
|
2
|
-
import {
|
|
2
|
+
import { decodeCborDeterministic } from "@forestrie/encoding";
|
|
3
3
|
import type { Grant } from "@forestrie/encoding";
|
|
4
4
|
import {
|
|
5
5
|
HEADER_FORESTRIE_GRANT_V0,
|
|
@@ -42,7 +42,7 @@ export function decodeForestrieGrantCose(bytes: Uint8Array): {
|
|
|
42
42
|
grant: Grant;
|
|
43
43
|
idtimestampBe8: Uint8Array;
|
|
44
44
|
} {
|
|
45
|
-
const raw =
|
|
45
|
+
const raw = decodeCborDeterministic(bytes);
|
|
46
46
|
const arr = Array.isArray(raw) ? raw : null;
|
|
47
47
|
if (!arr || arr.length !== 4) {
|
|
48
48
|
throw new Error("Forestrie-Grant must be COSE Sign1 (array of 4)");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { decodeCborDeterministic } from "@forestrie/encoding";
|
|
2
2
|
import {
|
|
3
3
|
COSE_ALG_ES256,
|
|
4
4
|
COSE_CRV_P256,
|
|
@@ -45,7 +45,7 @@ export async function decodeTrustRootFromGenesis(
|
|
|
45
45
|
): Promise<RootVerifyKey> {
|
|
46
46
|
let raw: unknown;
|
|
47
47
|
try {
|
|
48
|
-
raw =
|
|
48
|
+
raw = decodeCborDeterministic(genesisCbor);
|
|
49
49
|
} catch {
|
|
50
50
|
throw new Error("genesis CBOR decode failed");
|
|
51
51
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline grant-leaf lookup (FOR-344): locate a grant's sequenced leaf in a
|
|
3
|
+
* local massif blob using only the grant statement and the massif `.log`.
|
|
4
|
+
*
|
|
5
|
+
* The sequencer commits a grant to the log under its grant commitment hash
|
|
6
|
+
* (`grantCommitmentHashFromGrant` — the ContentHash enqueued in
|
|
7
|
+
* canopy-api `grant-sequencing.ts`), and the v2 urkle index region records that
|
|
8
|
+
* hash in each leaf's value column keyed by the leaf's 8-byte idtimestamp. So a
|
|
9
|
+
* client holding the owner-log massif can recover BOTH halves of the permanent
|
|
10
|
+
* entry id (`idtimestamp_be8 || mmrIndex_be8`) with no operator round-trip —
|
|
11
|
+
* the derivation that lets `forestrie complete-grant` build the completed grant
|
|
12
|
+
* header offline (grants are derivable from log data, not operator-issued).
|
|
13
|
+
*
|
|
14
|
+
* Pure over bytes; no network, no signing key. Browser-safe (ADR-0048).
|
|
15
|
+
*
|
|
16
|
+
* The lookup is a linear scan of the leaf-table value column. That is optimal
|
|
17
|
+
* here: recovering the entry id needs the leaf's *position* (ordinal → mmrIndex
|
|
18
|
+
* + idtimestamp key), and no content-hash→position index exists in the blob.
|
|
19
|
+
* The index region's bloom filter 0 IS keyed on the content hash, but a bloom
|
|
20
|
+
* only answers membership (maybe/definitely-not), never position — so it could
|
|
21
|
+
* at best short-circuit the not-present case, not the recovery; the positional
|
|
22
|
+
* structure (the urkle trie) is keyed by idtimestamp, which is exactly what we
|
|
23
|
+
* are recovering. Building a value→position map would itself be an O(n) scan.
|
|
24
|
+
*/
|
|
25
|
+
import { openMassifLeafIndex, type LocatedLeaf } from "@forestrie/merklelog";
|
|
26
|
+
import type { Grant } from "@forestrie/encoding";
|
|
27
|
+
import { grantCommitmentHashFromGrant } from "./grant-commitment.js";
|
|
28
|
+
|
|
29
|
+
export type { LocatedLeaf };
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Find the grant's leaf in `massifBytes` and return its `mmrIndex` and the
|
|
33
|
+
* 8-byte idtimestamp key, or `null` when the grant's commitment hash is not
|
|
34
|
+
* present in this massif's index region (searched, not found).
|
|
35
|
+
*
|
|
36
|
+
* Throws `MissingIndexError` (from `@forestrie/merklelog`) when the blob has no
|
|
37
|
+
* populated index region at all — distinguishable from a not-found `null`.
|
|
38
|
+
*/
|
|
39
|
+
export async function findGrantLeafInMassif(
|
|
40
|
+
massifBytes: Uint8Array,
|
|
41
|
+
grant: Grant,
|
|
42
|
+
): Promise<LocatedLeaf | null> {
|
|
43
|
+
const inner = await grantCommitmentHashFromGrant(grant);
|
|
44
|
+
const index = openMassifLeafIndex(massifBytes);
|
|
45
|
+
return index.findLeafByContentHash(inner);
|
|
46
|
+
}
|
package/src/grant-codec.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
decodeCborDeterministic,
|
|
3
|
+
encodeCborDeterministic,
|
|
4
|
+
} from "@forestrie/encoding";
|
|
2
5
|
import type { Grant } from "@forestrie/encoding";
|
|
3
6
|
import { grantDataToBytes } from "@forestrie/encoding";
|
|
4
7
|
import { fromPaddedWire32, toPaddedWire32 } from "./uuid-bytes.js";
|
|
@@ -56,7 +59,7 @@ function mapToGrant(m: Map<number, unknown> | Record<number, unknown>): Grant {
|
|
|
56
59
|
|
|
57
60
|
export function decodeGrantPayload(bytes: Uint8Array): Grant {
|
|
58
61
|
if (!bytes?.length) throw new Error("Grant payload is empty");
|
|
59
|
-
const raw =
|
|
62
|
+
const raw = decodeCborDeterministic(bytes);
|
|
60
63
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
61
64
|
throw new Error("Grant payload must be a CBOR map");
|
|
62
65
|
}
|
|
@@ -67,7 +70,7 @@ export function decodeGrantResponse(bytes: Uint8Array): {
|
|
|
67
70
|
grant: Grant;
|
|
68
71
|
idtimestamp: Uint8Array;
|
|
69
72
|
} {
|
|
70
|
-
const raw =
|
|
73
|
+
const raw = decodeCborDeterministic(bytes);
|
|
71
74
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
72
75
|
throw new Error("Grant response must be a CBOR map");
|
|
73
76
|
}
|
|
@@ -102,5 +105,5 @@ export function encodeGrantPayload(grant: Grant): Uint8Array {
|
|
|
102
105
|
grantDataToBytes(grant.grantData ?? new Uint8Array(0)),
|
|
103
106
|
],
|
|
104
107
|
]);
|
|
105
|
-
return
|
|
108
|
+
return encodeCborDeterministic(map);
|
|
106
109
|
}
|
package/src/index.ts
CHANGED
|
@@ -35,3 +35,11 @@ export {
|
|
|
35
35
|
} from "./attach-transparent-statement-receipt.js";
|
|
36
36
|
export { decodeEntryIdHex, entryIdHexToIdtimestampBe8 } from "./entry-id.js";
|
|
37
37
|
export type { DecodedEntryId } from "./entry-id.js";
|
|
38
|
+
|
|
39
|
+
/** Grant commitment hash (ContentHash) — the value the sequencer commits. */
|
|
40
|
+
export { grantCommitmentHashFromGrant } from "./grant-commitment.js";
|
|
41
|
+
/** Offline grant-leaf lookup from a local massif blob (FOR-344). */
|
|
42
|
+
export { findGrantLeafInMassif } from "./find-grant-leaf.js";
|
|
43
|
+
export type { LocatedLeaf } from "./find-grant-leaf.js";
|
|
44
|
+
/** Re-exported so callers of findGrantLeafInMassif can catch it (FOR-344). */
|
|
45
|
+
export { MissingIndexError } from "@forestrie/merklelog";
|
package/src/parse-receipt.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { decodeCborDeterministic } from "@forestrie/encoding";
|
|
2
2
|
import type { Proof } from "@forestrie/merklelog";
|
|
3
3
|
|
|
4
4
|
const VDS_COSE_RECEIPT_PROOFS_TAG = 396;
|
|
@@ -62,7 +62,7 @@ export function parseReceipt(receiptBytes: Uint8Array): {
|
|
|
62
62
|
receiptCbor: Uint8Array;
|
|
63
63
|
coseSign1: CoseSign1;
|
|
64
64
|
} {
|
|
65
|
-
const decoded =
|
|
65
|
+
const decoded = decodeCborDeterministic(receiptBytes);
|
|
66
66
|
const unwrapped = unwrapCoseSign1Tag(decoded);
|
|
67
67
|
requireCoseSign1(unwrapped);
|
|
68
68
|
|