@forestrie/receipt-verify 0.7.1 → 0.8.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 +10 -0
- package/dist/build-receipt-offline.d.ts.map +1 -1
- package/dist/build-receipt-offline.js +19 -1
- package/dist/freshen-receipt.d.ts +32 -0
- package/dist/freshen-receipt.d.ts.map +1 -0
- package/dist/freshen-receipt.js +171 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/package.json +2 -2
- package/src/build-receipt-offline.ts +26 -1
- package/src/freshen-receipt.ts +242 -0
- package/src/index.ts +7 -0
|
@@ -46,6 +46,16 @@ export type BuildReceiptOfflineInput = {
|
|
|
46
46
|
* on any failure.
|
|
47
47
|
*/
|
|
48
48
|
export declare function buildReceiptOffline(input: BuildReceiptOfflineInput): Uint8Array;
|
|
49
|
+
/**
|
|
50
|
+
* Assemble a receipt from a checkpoint's pre-signed peak receipt and a
|
|
51
|
+
* caller-supplied inclusion `proof` for `mmrIndex`: pick the peak receipt the
|
|
52
|
+
* proof roots to, attach the proof at header 396 and copy the label-1000
|
|
53
|
+
* delegation cert. The checkpoint signature covers only the accumulator (peak),
|
|
54
|
+
* so the unprotected 396 header is free to carry the path — this is why a path
|
|
55
|
+
* from ANY source (massif tiles here; a tile-free consistency-proof extension in
|
|
56
|
+
* `freshenReceipt`) yields a `verify --genesis`-equivalent native receipt.
|
|
57
|
+
*/
|
|
58
|
+
export declare function assembleReceiptFromProof(checkpoint: ParsedCheckpoint, mmrIndex: bigint, proof: Uint8Array[]): Uint8Array;
|
|
49
59
|
export type ComputedAccumulatorPeak = {
|
|
50
60
|
/** Root computed from the massif's leaf value and rebuilt path. */
|
|
51
61
|
peak: Uint8Array;
|
|
@@ -1 +1 @@
|
|
|
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,
|
|
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,CAyCZ;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,gBAAgB,EAC5B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,UAAU,EAAE,GAClB,UAAU,CA4CZ;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"}
|
|
@@ -79,7 +79,25 @@ export function buildReceiptOffline(input) {
|
|
|
79
79
|
`${store.massifIndex.toString(10)}`);
|
|
80
80
|
}
|
|
81
81
|
const proof = inclusionProof(store.get, mmrLastIndex, mmrIndex);
|
|
82
|
-
|
|
82
|
+
return assembleReceiptFromProof(checkpoint, mmrIndex, proof);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Assemble a receipt from a checkpoint's pre-signed peak receipt and a
|
|
86
|
+
* caller-supplied inclusion `proof` for `mmrIndex`: pick the peak receipt the
|
|
87
|
+
* proof roots to, attach the proof at header 396 and copy the label-1000
|
|
88
|
+
* delegation cert. The checkpoint signature covers only the accumulator (peak),
|
|
89
|
+
* so the unprotected 396 header is free to carry the path — this is why a path
|
|
90
|
+
* from ANY source (massif tiles here; a tile-free consistency-proof extension in
|
|
91
|
+
* `freshenReceipt`) yields a `verify --genesis`-equivalent native receipt.
|
|
92
|
+
*/
|
|
93
|
+
export function assembleReceiptFromProof(checkpoint, mmrIndex, proof) {
|
|
94
|
+
if (checkpoint.mmrSize === null || checkpoint.mmrSize <= 0n) {
|
|
95
|
+
throw new Error("checkpoint carries no sealed size");
|
|
96
|
+
}
|
|
97
|
+
if (!checkpoint.peakReceipts) {
|
|
98
|
+
throw new Error("checkpoint carries no pre-signed peak receipts (label -65931)");
|
|
99
|
+
}
|
|
100
|
+
const peakIdx = peakIndexForLeafProof(checkpoint.mmrSize, proof.length);
|
|
83
101
|
const receiptBytes = checkpoint.peakReceipts[peakIdx];
|
|
84
102
|
if (!(receiptBytes instanceof Uint8Array)) {
|
|
85
103
|
throw new Error(`checkpoint peak receipt slot ${peakIdx} is missing or not bstr`);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type CheckpointConsistencyProof } from "./checkpoint-chain.js";
|
|
2
|
+
export type FreshenReceiptInput = {
|
|
3
|
+
/** The stale receipt (COSE Sign1 with a 396 inclusion proof). */
|
|
4
|
+
oldReceiptBytes: Uint8Array;
|
|
5
|
+
/** The leaf's committed value: `SHA-256(idtimestamp ‖ inner)` — the same
|
|
6
|
+
* value `verify` recomputes from the entry (caller derives it). */
|
|
7
|
+
leafValue: Uint8Array;
|
|
8
|
+
/** Consistency-proof chain covering [0 or a trusted seed] → the latest sealed
|
|
9
|
+
* size, in ascending contiguous order (the raw per-checkpoint proofs, with
|
|
10
|
+
* `paths`). The chain's last link must end at the checkpoint's sealed size. */
|
|
11
|
+
consistencyProofs: readonly CheckpointConsistencyProof[];
|
|
12
|
+
/** Trusted accumulator seed for a suffix chain; omit for a chain from base 0.
|
|
13
|
+
* Its peak count must match the first link's tree-size-1. */
|
|
14
|
+
accumulatorFrom?: Uint8Array[];
|
|
15
|
+
/** The latest checkpoint (`.sth`): its pre-signed peak receipts + delegation
|
|
16
|
+
* cert become the freshened receipt's signature. */
|
|
17
|
+
latestCheckpointBytes: Uint8Array;
|
|
18
|
+
};
|
|
19
|
+
export type FreshenReceiptResult = {
|
|
20
|
+
/** The freshened native receipt (verifies against the latest state). */
|
|
21
|
+
receipt: Uint8Array;
|
|
22
|
+
/** Sealed size the freshened receipt is anchored at. */
|
|
23
|
+
sealedSize: bigint;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Freshen a stale receipt to the latest sealed state. Throws if the chain is
|
|
27
|
+
* not a contiguous cover from the trusted base to the checkpoint's sealed size,
|
|
28
|
+
* if a climb node is missing from the supplied proofs, or if the recomputed
|
|
29
|
+
* peak does not match the folded latest accumulator.
|
|
30
|
+
*/
|
|
31
|
+
export declare function freshenReceipt(input: FreshenReceiptInput): Promise<FreshenReceiptResult>;
|
|
32
|
+
//# sourceMappingURL=freshen-receipt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"freshen-receipt.d.ts","sourceRoot":"","sources":["../src/freshen-receipt.ts"],"names":[],"mappings":"AA+CA,OAAO,EAEL,KAAK,0BAA0B,EAChC,MAAM,uBAAuB,CAAC;AAI/B,MAAM,MAAM,mBAAmB,GAAG;IAChC,iEAAiE;IACjE,eAAe,EAAE,UAAU,CAAC;IAC5B;uEACmE;IACnE,SAAS,EAAE,UAAU,CAAC;IACtB;;mFAE+E;IAC/E,iBAAiB,EAAE,SAAS,0BAA0B,EAAE,CAAC;IACzD;iEAC6D;IAC7D,eAAe,CAAC,EAAE,UAAU,EAAE,CAAC;IAC/B;wDACoD;IACpD,qBAAqB,EAAE,UAAU,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,wEAAwE;IACxE,OAAO,EAAE,UAAU,CAAC;IACpB,wDAAwD;IACxD,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AASF;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,oBAAoB,CAAC,CAmJ/B"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Freshen a stale receipt (FOR-418 Phase 3, plan-2607-32).
|
|
3
|
+
*
|
|
4
|
+
* A receipt goes stale when log growth buries the peak it commits to. Freshening
|
|
5
|
+
* re-anchors it to the CURRENT sealed state without tiles: extend the leaf's
|
|
6
|
+
* inclusion path from its old peak up to the latest accumulator using the
|
|
7
|
+
* checkpoint chain's consistency proofs (the tile-free source of the climb
|
|
8
|
+
* nodes), then attach that fresh path at header 396 to the LATEST checkpoint's
|
|
9
|
+
* pre-signed peak receipt. The result is a native receipt that verifies with
|
|
10
|
+
* plain `verify --genesis` against the current state — the latest checkpoint
|
|
11
|
+
* carries the genesis-verifiable delegation cert (see plan-2607-32's Phase 3
|
|
12
|
+
* finding: the calldata provider supplies the climb material tile-free, but the
|
|
13
|
+
* signature/cert comes from the latest `.sth`).
|
|
14
|
+
*
|
|
15
|
+
* Trust note: the freshened receipt is defined SOLELY by the latest checkpoint —
|
|
16
|
+
* its label-1000 delegation cert and its pre-signed peak receipt. Owner-rooting
|
|
17
|
+
* is established by the verify step's trust anchor: `verify --genesis` rejects a
|
|
18
|
+
* cert whose delegator is not the genesis owner (a hard failure, not a warning).
|
|
19
|
+
* A rotation of the delegated-TO sealer key is routine and within the owner's
|
|
20
|
+
* authority, so freshen does NOT flag it — there is no signer-change gate (this
|
|
21
|
+
* supersedes the earlier `--allow-new-signer` sketch; see the plan-2607-32 F2
|
|
22
|
+
* note). The old receipt's cert is irrelevant to the emitted artifact.
|
|
23
|
+
*
|
|
24
|
+
* Two independent fail-closed guards keep a bad assembly from ever minting:
|
|
25
|
+
* - Cross-checks against the checkpoint being borrowed from: the supplied
|
|
26
|
+
* chain must reach the checkpoint's sealed size and fold to exactly the
|
|
27
|
+
* number of peaks the checkpoint pre-signed (freshen holds no signing key, so
|
|
28
|
+
* the cryptographic peak↔signature tie stays with downstream verify; these
|
|
29
|
+
* structural checks turn a chain/checkpoint mismatch into a mint-time error
|
|
30
|
+
* instead of an unverifiable receipt).
|
|
31
|
+
* - The path self-check: `calculateRoot(leaf, freshPath)` must equal the
|
|
32
|
+
* covering peak of the folded latest accumulator.
|
|
33
|
+
*
|
|
34
|
+
* The climb arithmetic is `@forestrie/merklelog`'s `inclusionProofPath` (a
|
|
35
|
+
* tested go-merklelog port); this module only assembles node values by index
|
|
36
|
+
* from the old receipt path + the consistency proofs.
|
|
37
|
+
*/
|
|
38
|
+
import { calculateRoot, inclusionProofPath, peakIndexForLeafProof, peakMMRIndexes, } from "@forestrie/merklelog";
|
|
39
|
+
import { assembleReceiptFromProof, parseCheckpoint, } from "./build-receipt-offline.js";
|
|
40
|
+
import { computeCheckpointAccumulator, } from "./checkpoint-chain.js";
|
|
41
|
+
import { parseReceipt } from "./parse-receipt.js";
|
|
42
|
+
import { SubtleHasher } from "./subtle-hasher.js";
|
|
43
|
+
function bytesEqual(a, b) {
|
|
44
|
+
if (a.length !== b.length)
|
|
45
|
+
return false;
|
|
46
|
+
let x = 0;
|
|
47
|
+
for (let i = 0; i < a.length; i++)
|
|
48
|
+
x |= a[i] ^ b[i];
|
|
49
|
+
return x === 0;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Freshen a stale receipt to the latest sealed state. Throws if the chain is
|
|
53
|
+
* not a contiguous cover from the trusted base to the checkpoint's sealed size,
|
|
54
|
+
* if a climb node is missing from the supplied proofs, or if the recomputed
|
|
55
|
+
* peak does not match the folded latest accumulator.
|
|
56
|
+
*/
|
|
57
|
+
export async function freshenReceipt(input) {
|
|
58
|
+
const { proof } = parseReceipt(input.oldReceiptBytes);
|
|
59
|
+
// `parseReceipt` addresses the leaf by MMR index (396 proof entry key 1);
|
|
60
|
+
// `inclusionProofPath` below needs exactly that MMR index. (`Proof.leafIndex`
|
|
61
|
+
// is a distinct leaf-ordinal field that parseReceipt never sets — do not use
|
|
62
|
+
// it here, or a future producer that populated it would feed a non-MMR-index
|
|
63
|
+
// into the climb.)
|
|
64
|
+
const leafMmrIndex = proof.mmrIndex;
|
|
65
|
+
if (leafMmrIndex === undefined) {
|
|
66
|
+
throw new Error("receipt inclusion proof carries no mmr index");
|
|
67
|
+
}
|
|
68
|
+
const oldPath = proof.path;
|
|
69
|
+
const latest = parseCheckpoint(input.latestCheckpointBytes);
|
|
70
|
+
if (latest.mmrSize === null || latest.mmrSize <= 0n) {
|
|
71
|
+
throw new Error("latest checkpoint carries no sealed size");
|
|
72
|
+
}
|
|
73
|
+
const sealedSize = latest.mmrSize;
|
|
74
|
+
if (leafMmrIndex >= sealedSize) {
|
|
75
|
+
throw new Error(`receipt entry ${leafMmrIndex} is newer than the latest sealed size ${sealedSize}`);
|
|
76
|
+
}
|
|
77
|
+
// --- validate the supplied chain shape before folding (F5) ---
|
|
78
|
+
const links = input.consistencyProofs;
|
|
79
|
+
if (links.length === 0) {
|
|
80
|
+
throw new Error("freshen requires at least one consistency proof linking the receipt's era to the checkpoint");
|
|
81
|
+
}
|
|
82
|
+
const firstLink = links[0];
|
|
83
|
+
// Base: a base-0 chain starts from an empty accumulator; a suffix chain's
|
|
84
|
+
// trusted seed must have the peak count of its tree-size-1.
|
|
85
|
+
const baseCount = input.accumulatorFrom?.length ?? 0;
|
|
86
|
+
if (firstLink.treeSize1 === 0n) {
|
|
87
|
+
if (baseCount !== 0) {
|
|
88
|
+
throw new Error("base-0 consistency chain must start from an empty accumulator seed");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const wanted = peakMMRIndexes(firstLink.treeSize1 - 1n).length;
|
|
93
|
+
if (baseCount !== wanted) {
|
|
94
|
+
throw new Error(`base accumulator has ${baseCount} peaks; first link size ${firstLink.treeSize1} requires ${wanted}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Contiguity: each link continues where the previous one sealed.
|
|
98
|
+
for (let i = 1; i < links.length; i++) {
|
|
99
|
+
if (links[i].treeSize1 !== links[i - 1].treeSize2) {
|
|
100
|
+
throw new Error(`consistency chain is not contiguous at link ${i}: base ${links[i].treeSize1} != previous sealed size ${links[i - 1].treeSize2}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Endpoint: the chain must reach exactly the checkpoint's sealed size (F1).
|
|
104
|
+
const lastLink = links[links.length - 1];
|
|
105
|
+
if (lastLink.treeSize2 !== sealedSize) {
|
|
106
|
+
throw new Error(`consistency chain ends at size ${lastLink.treeSize2} but the checkpoint sealed size ${sealedSize}`);
|
|
107
|
+
}
|
|
108
|
+
// Fold the chain to the latest accumulator (self-check target).
|
|
109
|
+
let accumulator = input.accumulatorFrom ?? [];
|
|
110
|
+
for (const p of links) {
|
|
111
|
+
accumulator = await computeCheckpointAccumulator(p, accumulator);
|
|
112
|
+
}
|
|
113
|
+
const aLatest = accumulator;
|
|
114
|
+
// Cross-check the fold against the checkpoint we are borrowing from (F1): the
|
|
115
|
+
// folded accumulator must have the structural peak count for the sealed size
|
|
116
|
+
// AND match the number of pre-signed peak receipts the checkpoint carries.
|
|
117
|
+
// (The cryptographic peak↔signature tie is enforced by downstream verify;
|
|
118
|
+
// freshen holds no key.)
|
|
119
|
+
const structuralPeaks = peakMMRIndexes(sealedSize - 1n).length;
|
|
120
|
+
if (aLatest.length !== structuralPeaks) {
|
|
121
|
+
throw new Error(`folded accumulator has ${aLatest.length} peaks; sealed size ${sealedSize} requires ${structuralPeaks}`);
|
|
122
|
+
}
|
|
123
|
+
if (!latest.peakReceipts) {
|
|
124
|
+
throw new Error("latest checkpoint carries no pre-signed peak receipts (label -65931)");
|
|
125
|
+
}
|
|
126
|
+
if (latest.peakReceipts.length !== aLatest.length) {
|
|
127
|
+
throw new Error(`checkpoint carries ${latest.peakReceipts.length} peak receipts but the folded accumulator has ${aLatest.length} peaks — chain does not match this checkpoint`);
|
|
128
|
+
}
|
|
129
|
+
// Assemble the leaf's inclusion path at the latest size from index-addressed
|
|
130
|
+
// node values: the old receipt path (leaf → old peak, a prefix by MMR
|
|
131
|
+
// prefix-composability) + the consistency proofs (the climb extension).
|
|
132
|
+
const fullIndices = inclusionProofPath(sealedSize - 1n, leafMmrIndex);
|
|
133
|
+
if (fullIndices.length < oldPath.length) {
|
|
134
|
+
throw new Error("receipt path is longer than the latest inclusion path — stale/forged receipt");
|
|
135
|
+
}
|
|
136
|
+
const store = new Map();
|
|
137
|
+
for (let k = 0; k < oldPath.length; k++) {
|
|
138
|
+
store.set(fullIndices[k], oldPath[k]);
|
|
139
|
+
}
|
|
140
|
+
for (const link of links) {
|
|
141
|
+
const fromPeaks = peakMMRIndexes(link.treeSize1 - 1n);
|
|
142
|
+
fromPeaks.forEach((peakIndex, j) => {
|
|
143
|
+
const climb = link.paths[j];
|
|
144
|
+
if (climb === undefined)
|
|
145
|
+
return;
|
|
146
|
+
const climbIndices = inclusionProofPath(link.treeSize2 - 1n, peakIndex);
|
|
147
|
+
climbIndices.forEach((ix, e) => {
|
|
148
|
+
const v = climb[e];
|
|
149
|
+
if (v !== undefined)
|
|
150
|
+
store.set(ix, v);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
const freshPath = fullIndices.map((ix) => {
|
|
155
|
+
const v = store.get(ix);
|
|
156
|
+
if (v === undefined) {
|
|
157
|
+
throw new Error(`checkpoint chain does not cover the extension node at ${ix} — supply the full chain from the receipt's era`);
|
|
158
|
+
}
|
|
159
|
+
return v;
|
|
160
|
+
});
|
|
161
|
+
// Self-check: the fresh path must recompute the covering peak of the latest
|
|
162
|
+
// accumulator. Fails closed rather than emitting a bad receipt.
|
|
163
|
+
const hasher = new SubtleHasher();
|
|
164
|
+
const root = await calculateRoot(hasher, input.leafValue, { path: freshPath, mmrIndex: leafMmrIndex }, leafMmrIndex);
|
|
165
|
+
const peakIdx = peakIndexForLeafProof(sealedSize, freshPath.length);
|
|
166
|
+
if (peakIdx >= aLatest.length || !bytesEqual(root, aLatest[peakIdx])) {
|
|
167
|
+
throw new Error("freshened path does not recompute the latest accumulator peak — chain/leaf mismatch");
|
|
168
|
+
}
|
|
169
|
+
const receipt = assembleReceiptFromProof(latest, leafMmrIndex, freshPath);
|
|
170
|
+
return { receipt, sealedSize };
|
|
171
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,10 @@ export type { VerifyGrantReceiptOfflineInput } from "./verify-grant-receipt-offl
|
|
|
5
5
|
export type { VerifyReceiptOfflineInput } from "./verify-grant-receipt-offline.js";
|
|
6
6
|
export { parseReceipt } from "./parse-receipt.js";
|
|
7
7
|
export type { BuildReceiptOfflineInput, ComputedAccumulatorPeak, MassifNodeStore, ParsedCheckpoint, } from "./build-receipt-offline.js";
|
|
8
|
-
export { buildReceiptOffline, computeAccumulatorPeak, openMassifNodeStore, parseCheckpoint, } from "./build-receipt-offline.js";
|
|
8
|
+
export { assembleReceiptFromProof, buildReceiptOffline, computeAccumulatorPeak, openMassifNodeStore, parseCheckpoint, } from "./build-receipt-offline.js";
|
|
9
|
+
/** Freshen a stale receipt to the latest sealed state, tile-free (FOR-418). */
|
|
10
|
+
export { freshenReceipt } from "./freshen-receipt.js";
|
|
11
|
+
export type { FreshenReceiptInput, FreshenReceiptResult, } from "./freshen-receipt.js";
|
|
9
12
|
/**
|
|
10
13
|
* MMR proof math now lives in @forestrie/merklelog (plan-2607-15 §4 hoist);
|
|
11
14
|
* re-exported here to preserve the receipt-verify public surface.
|
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,YAAY,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AACnF,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,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AACzE;;;;GAIG;AACH,OAAO,EACL,iCAAiC,EACjC,4BAA4B,GAC7B,MAAM,mCAAmC,CAAC;AAC3C,YAAY,EACV,sCAAsC,EACtC,iCAAiC,GAClC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,YAAY,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAC7E,yEAAyE;AACzE,OAAO,EAAE,qCAAqC,EAAE,MAAM,6BAA6B,CAAC;AACpF,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;AACzD,OAAO,EACL,kBAAkB,EAClB,0BAA0B,EAC1B,4BAA4B,EAC5B,qBAAqB,EACrB,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,0BAA0B,GAChC,MAAM,uBAAuB,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,YAAY,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AACnF,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,YAAY,EACV,wBAAwB,EACxB,uBAAuB,EACvB,eAAe,EACf,gBAAgB,GACjB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,sBAAsB,EACtB,mBAAmB,EACnB,eAAe,GAChB,MAAM,4BAA4B,CAAC;AACpC,+EAA+E;AAC/E,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,YAAY,EACV,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,sBAAsB,CAAC;AAC9B;;;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,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AACzE;;;;GAIG;AACH,OAAO,EACL,iCAAiC,EACjC,4BAA4B,GAC7B,MAAM,mCAAmC,CAAC;AAC3C,YAAY,EACV,sCAAsC,EACtC,iCAAiC,GAClC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,YAAY,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAC7E,yEAAyE;AACzE,OAAO,EAAE,qCAAqC,EAAE,MAAM,6BAA6B,CAAC;AACpF,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;AACzD,OAAO,EACL,kBAAkB,EAClB,0BAA0B,EAC1B,4BAA4B,EAC5B,qBAAqB,EACrB,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,0BAA0B,GAChC,MAAM,uBAAuB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { parseReceipt } from "./parse-receipt.js";
|
|
2
|
-
export { buildReceiptOffline, computeAccumulatorPeak, openMassifNodeStore, parseCheckpoint, } from "./build-receipt-offline.js";
|
|
2
|
+
export { assembleReceiptFromProof, buildReceiptOffline, computeAccumulatorPeak, openMassifNodeStore, parseCheckpoint, } from "./build-receipt-offline.js";
|
|
3
|
+
/** Freshen a stale receipt to the latest sealed state, tile-free (FOR-418). */
|
|
4
|
+
export { freshenReceipt } from "./freshen-receipt.js";
|
|
3
5
|
/**
|
|
4
6
|
* MMR proof math now lives in @forestrie/merklelog (plan-2607-15 §4 hoist);
|
|
5
7
|
* re-exported here to preserve the receipt-verify public surface.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forestrie/receipt-verify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Offline SCITT grant receipt verification (ADR-0045)",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@noble/hashes": "^1.7.1",
|
|
31
31
|
"@forestrie/encoding": "0.5.0",
|
|
32
|
-
"@forestrie/merklelog": "0.
|
|
32
|
+
"@forestrie/merklelog": "0.3.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"esbuild": "^0.24.0",
|
|
@@ -137,7 +137,32 @@ export function buildReceiptOffline(
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
const proof = inclusionProof(store.get, mmrLastIndex, mmrIndex);
|
|
140
|
-
|
|
140
|
+
return assembleReceiptFromProof(checkpoint, mmrIndex, proof);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Assemble a receipt from a checkpoint's pre-signed peak receipt and a
|
|
145
|
+
* caller-supplied inclusion `proof` for `mmrIndex`: pick the peak receipt the
|
|
146
|
+
* proof roots to, attach the proof at header 396 and copy the label-1000
|
|
147
|
+
* delegation cert. The checkpoint signature covers only the accumulator (peak),
|
|
148
|
+
* so the unprotected 396 header is free to carry the path — this is why a path
|
|
149
|
+
* from ANY source (massif tiles here; a tile-free consistency-proof extension in
|
|
150
|
+
* `freshenReceipt`) yields a `verify --genesis`-equivalent native receipt.
|
|
151
|
+
*/
|
|
152
|
+
export function assembleReceiptFromProof(
|
|
153
|
+
checkpoint: ParsedCheckpoint,
|
|
154
|
+
mmrIndex: bigint,
|
|
155
|
+
proof: Uint8Array[],
|
|
156
|
+
): Uint8Array {
|
|
157
|
+
if (checkpoint.mmrSize === null || checkpoint.mmrSize <= 0n) {
|
|
158
|
+
throw new Error("checkpoint carries no sealed size");
|
|
159
|
+
}
|
|
160
|
+
if (!checkpoint.peakReceipts) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
"checkpoint carries no pre-signed peak receipts (label -65931)",
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
const peakIdx = peakIndexForLeafProof(checkpoint.mmrSize, proof.length);
|
|
141
166
|
const receiptBytes = checkpoint.peakReceipts[peakIdx];
|
|
142
167
|
if (!(receiptBytes instanceof Uint8Array)) {
|
|
143
168
|
throw new Error(
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Freshen a stale receipt (FOR-418 Phase 3, plan-2607-32).
|
|
3
|
+
*
|
|
4
|
+
* A receipt goes stale when log growth buries the peak it commits to. Freshening
|
|
5
|
+
* re-anchors it to the CURRENT sealed state without tiles: extend the leaf's
|
|
6
|
+
* inclusion path from its old peak up to the latest accumulator using the
|
|
7
|
+
* checkpoint chain's consistency proofs (the tile-free source of the climb
|
|
8
|
+
* nodes), then attach that fresh path at header 396 to the LATEST checkpoint's
|
|
9
|
+
* pre-signed peak receipt. The result is a native receipt that verifies with
|
|
10
|
+
* plain `verify --genesis` against the current state — the latest checkpoint
|
|
11
|
+
* carries the genesis-verifiable delegation cert (see plan-2607-32's Phase 3
|
|
12
|
+
* finding: the calldata provider supplies the climb material tile-free, but the
|
|
13
|
+
* signature/cert comes from the latest `.sth`).
|
|
14
|
+
*
|
|
15
|
+
* Trust note: the freshened receipt is defined SOLELY by the latest checkpoint —
|
|
16
|
+
* its label-1000 delegation cert and its pre-signed peak receipt. Owner-rooting
|
|
17
|
+
* is established by the verify step's trust anchor: `verify --genesis` rejects a
|
|
18
|
+
* cert whose delegator is not the genesis owner (a hard failure, not a warning).
|
|
19
|
+
* A rotation of the delegated-TO sealer key is routine and within the owner's
|
|
20
|
+
* authority, so freshen does NOT flag it — there is no signer-change gate (this
|
|
21
|
+
* supersedes the earlier `--allow-new-signer` sketch; see the plan-2607-32 F2
|
|
22
|
+
* note). The old receipt's cert is irrelevant to the emitted artifact.
|
|
23
|
+
*
|
|
24
|
+
* Two independent fail-closed guards keep a bad assembly from ever minting:
|
|
25
|
+
* - Cross-checks against the checkpoint being borrowed from: the supplied
|
|
26
|
+
* chain must reach the checkpoint's sealed size and fold to exactly the
|
|
27
|
+
* number of peaks the checkpoint pre-signed (freshen holds no signing key, so
|
|
28
|
+
* the cryptographic peak↔signature tie stays with downstream verify; these
|
|
29
|
+
* structural checks turn a chain/checkpoint mismatch into a mint-time error
|
|
30
|
+
* instead of an unverifiable receipt).
|
|
31
|
+
* - The path self-check: `calculateRoot(leaf, freshPath)` must equal the
|
|
32
|
+
* covering peak of the folded latest accumulator.
|
|
33
|
+
*
|
|
34
|
+
* The climb arithmetic is `@forestrie/merklelog`'s `inclusionProofPath` (a
|
|
35
|
+
* tested go-merklelog port); this module only assembles node values by index
|
|
36
|
+
* from the old receipt path + the consistency proofs.
|
|
37
|
+
*/
|
|
38
|
+
import {
|
|
39
|
+
calculateRoot,
|
|
40
|
+
inclusionProofPath,
|
|
41
|
+
peakIndexForLeafProof,
|
|
42
|
+
peakMMRIndexes,
|
|
43
|
+
} from "@forestrie/merklelog";
|
|
44
|
+
import {
|
|
45
|
+
assembleReceiptFromProof,
|
|
46
|
+
parseCheckpoint,
|
|
47
|
+
} from "./build-receipt-offline.js";
|
|
48
|
+
import {
|
|
49
|
+
computeCheckpointAccumulator,
|
|
50
|
+
type CheckpointConsistencyProof,
|
|
51
|
+
} from "./checkpoint-chain.js";
|
|
52
|
+
import { parseReceipt } from "./parse-receipt.js";
|
|
53
|
+
import { SubtleHasher } from "./subtle-hasher.js";
|
|
54
|
+
|
|
55
|
+
export type FreshenReceiptInput = {
|
|
56
|
+
/** The stale receipt (COSE Sign1 with a 396 inclusion proof). */
|
|
57
|
+
oldReceiptBytes: Uint8Array;
|
|
58
|
+
/** The leaf's committed value: `SHA-256(idtimestamp ‖ inner)` — the same
|
|
59
|
+
* value `verify` recomputes from the entry (caller derives it). */
|
|
60
|
+
leafValue: Uint8Array;
|
|
61
|
+
/** Consistency-proof chain covering [0 or a trusted seed] → the latest sealed
|
|
62
|
+
* size, in ascending contiguous order (the raw per-checkpoint proofs, with
|
|
63
|
+
* `paths`). The chain's last link must end at the checkpoint's sealed size. */
|
|
64
|
+
consistencyProofs: readonly CheckpointConsistencyProof[];
|
|
65
|
+
/** Trusted accumulator seed for a suffix chain; omit for a chain from base 0.
|
|
66
|
+
* Its peak count must match the first link's tree-size-1. */
|
|
67
|
+
accumulatorFrom?: Uint8Array[];
|
|
68
|
+
/** The latest checkpoint (`.sth`): its pre-signed peak receipts + delegation
|
|
69
|
+
* cert become the freshened receipt's signature. */
|
|
70
|
+
latestCheckpointBytes: Uint8Array;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export type FreshenReceiptResult = {
|
|
74
|
+
/** The freshened native receipt (verifies against the latest state). */
|
|
75
|
+
receipt: Uint8Array;
|
|
76
|
+
/** Sealed size the freshened receipt is anchored at. */
|
|
77
|
+
sealedSize: bigint;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
81
|
+
if (a.length !== b.length) return false;
|
|
82
|
+
let x = 0;
|
|
83
|
+
for (let i = 0; i < a.length; i++) x |= a[i]! ^ b[i]!;
|
|
84
|
+
return x === 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Freshen a stale receipt to the latest sealed state. Throws if the chain is
|
|
89
|
+
* not a contiguous cover from the trusted base to the checkpoint's sealed size,
|
|
90
|
+
* if a climb node is missing from the supplied proofs, or if the recomputed
|
|
91
|
+
* peak does not match the folded latest accumulator.
|
|
92
|
+
*/
|
|
93
|
+
export async function freshenReceipt(
|
|
94
|
+
input: FreshenReceiptInput,
|
|
95
|
+
): Promise<FreshenReceiptResult> {
|
|
96
|
+
const { proof } = parseReceipt(input.oldReceiptBytes);
|
|
97
|
+
// `parseReceipt` addresses the leaf by MMR index (396 proof entry key 1);
|
|
98
|
+
// `inclusionProofPath` below needs exactly that MMR index. (`Proof.leafIndex`
|
|
99
|
+
// is a distinct leaf-ordinal field that parseReceipt never sets — do not use
|
|
100
|
+
// it here, or a future producer that populated it would feed a non-MMR-index
|
|
101
|
+
// into the climb.)
|
|
102
|
+
const leafMmrIndex = proof.mmrIndex;
|
|
103
|
+
if (leafMmrIndex === undefined) {
|
|
104
|
+
throw new Error("receipt inclusion proof carries no mmr index");
|
|
105
|
+
}
|
|
106
|
+
const oldPath = proof.path;
|
|
107
|
+
|
|
108
|
+
const latest = parseCheckpoint(input.latestCheckpointBytes);
|
|
109
|
+
if (latest.mmrSize === null || latest.mmrSize <= 0n) {
|
|
110
|
+
throw new Error("latest checkpoint carries no sealed size");
|
|
111
|
+
}
|
|
112
|
+
const sealedSize = latest.mmrSize;
|
|
113
|
+
if (leafMmrIndex >= sealedSize) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`receipt entry ${leafMmrIndex} is newer than the latest sealed size ${sealedSize}`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// --- validate the supplied chain shape before folding (F5) ---
|
|
120
|
+
const links = input.consistencyProofs;
|
|
121
|
+
if (links.length === 0) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
"freshen requires at least one consistency proof linking the receipt's era to the checkpoint",
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const firstLink = links[0]!;
|
|
127
|
+
// Base: a base-0 chain starts from an empty accumulator; a suffix chain's
|
|
128
|
+
// trusted seed must have the peak count of its tree-size-1.
|
|
129
|
+
const baseCount = input.accumulatorFrom?.length ?? 0;
|
|
130
|
+
if (firstLink.treeSize1 === 0n) {
|
|
131
|
+
if (baseCount !== 0) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
"base-0 consistency chain must start from an empty accumulator seed",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
const wanted = peakMMRIndexes(firstLink.treeSize1 - 1n).length;
|
|
138
|
+
if (baseCount !== wanted) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`base accumulator has ${baseCount} peaks; first link size ${firstLink.treeSize1} requires ${wanted}`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Contiguity: each link continues where the previous one sealed.
|
|
145
|
+
for (let i = 1; i < links.length; i++) {
|
|
146
|
+
if (links[i]!.treeSize1 !== links[i - 1]!.treeSize2) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`consistency chain is not contiguous at link ${i}: base ${links[i]!.treeSize1} != previous sealed size ${links[i - 1]!.treeSize2}`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Endpoint: the chain must reach exactly the checkpoint's sealed size (F1).
|
|
153
|
+
const lastLink = links[links.length - 1]!;
|
|
154
|
+
if (lastLink.treeSize2 !== sealedSize) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`consistency chain ends at size ${lastLink.treeSize2} but the checkpoint sealed size ${sealedSize}`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Fold the chain to the latest accumulator (self-check target).
|
|
161
|
+
let accumulator = input.accumulatorFrom ?? [];
|
|
162
|
+
for (const p of links) {
|
|
163
|
+
accumulator = await computeCheckpointAccumulator(p, accumulator);
|
|
164
|
+
}
|
|
165
|
+
const aLatest = accumulator;
|
|
166
|
+
|
|
167
|
+
// Cross-check the fold against the checkpoint we are borrowing from (F1): the
|
|
168
|
+
// folded accumulator must have the structural peak count for the sealed size
|
|
169
|
+
// AND match the number of pre-signed peak receipts the checkpoint carries.
|
|
170
|
+
// (The cryptographic peak↔signature tie is enforced by downstream verify;
|
|
171
|
+
// freshen holds no key.)
|
|
172
|
+
const structuralPeaks = peakMMRIndexes(sealedSize - 1n).length;
|
|
173
|
+
if (aLatest.length !== structuralPeaks) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`folded accumulator has ${aLatest.length} peaks; sealed size ${sealedSize} requires ${structuralPeaks}`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
if (!latest.peakReceipts) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
"latest checkpoint carries no pre-signed peak receipts (label -65931)",
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
if (latest.peakReceipts.length !== aLatest.length) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`checkpoint carries ${latest.peakReceipts.length} peak receipts but the folded accumulator has ${aLatest.length} peaks — chain does not match this checkpoint`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Assemble the leaf's inclusion path at the latest size from index-addressed
|
|
190
|
+
// node values: the old receipt path (leaf → old peak, a prefix by MMR
|
|
191
|
+
// prefix-composability) + the consistency proofs (the climb extension).
|
|
192
|
+
const fullIndices = inclusionProofPath(sealedSize - 1n, leafMmrIndex);
|
|
193
|
+
if (fullIndices.length < oldPath.length) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
"receipt path is longer than the latest inclusion path — stale/forged receipt",
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
const store = new Map<bigint, Uint8Array>();
|
|
199
|
+
for (let k = 0; k < oldPath.length; k++) {
|
|
200
|
+
store.set(fullIndices[k]!, oldPath[k]!);
|
|
201
|
+
}
|
|
202
|
+
for (const link of links) {
|
|
203
|
+
const fromPeaks = peakMMRIndexes(link.treeSize1 - 1n);
|
|
204
|
+
fromPeaks.forEach((peakIndex, j) => {
|
|
205
|
+
const climb = link.paths[j];
|
|
206
|
+
if (climb === undefined) return;
|
|
207
|
+
const climbIndices = inclusionProofPath(link.treeSize2 - 1n, peakIndex);
|
|
208
|
+
climbIndices.forEach((ix, e) => {
|
|
209
|
+
const v = climb[e];
|
|
210
|
+
if (v !== undefined) store.set(ix, v);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
const freshPath = fullIndices.map((ix) => {
|
|
215
|
+
const v = store.get(ix);
|
|
216
|
+
if (v === undefined) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`checkpoint chain does not cover the extension node at ${ix} — supply the full chain from the receipt's era`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
return v;
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Self-check: the fresh path must recompute the covering peak of the latest
|
|
225
|
+
// accumulator. Fails closed rather than emitting a bad receipt.
|
|
226
|
+
const hasher = new SubtleHasher();
|
|
227
|
+
const root = await calculateRoot(
|
|
228
|
+
hasher,
|
|
229
|
+
input.leafValue,
|
|
230
|
+
{ path: freshPath, mmrIndex: leafMmrIndex },
|
|
231
|
+
leafMmrIndex,
|
|
232
|
+
);
|
|
233
|
+
const peakIdx = peakIndexForLeafProof(sealedSize, freshPath.length);
|
|
234
|
+
if (peakIdx >= aLatest.length || !bytesEqual(root, aLatest[peakIdx]!)) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
"freshened path does not recompute the latest accumulator peak — chain/leaf mismatch",
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const receipt = assembleReceiptFromProof(latest, leafMmrIndex, freshPath);
|
|
241
|
+
return { receipt, sealedSize };
|
|
242
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -14,11 +14,18 @@ export type {
|
|
|
14
14
|
ParsedCheckpoint,
|
|
15
15
|
} from "./build-receipt-offline.js";
|
|
16
16
|
export {
|
|
17
|
+
assembleReceiptFromProof,
|
|
17
18
|
buildReceiptOffline,
|
|
18
19
|
computeAccumulatorPeak,
|
|
19
20
|
openMassifNodeStore,
|
|
20
21
|
parseCheckpoint,
|
|
21
22
|
} from "./build-receipt-offline.js";
|
|
23
|
+
/** Freshen a stale receipt to the latest sealed state, tile-free (FOR-418). */
|
|
24
|
+
export { freshenReceipt } from "./freshen-receipt.js";
|
|
25
|
+
export type {
|
|
26
|
+
FreshenReceiptInput,
|
|
27
|
+
FreshenReceiptResult,
|
|
28
|
+
} from "./freshen-receipt.js";
|
|
22
29
|
/**
|
|
23
30
|
* MMR proof math now lives in @forestrie/merklelog (plan-2607-15 §4 hoist);
|
|
24
31
|
* re-exported here to preserve the receipt-verify public surface.
|