@forestrie/forestrie-cli 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/LICENSE +21 -0
- package/README.md +425 -0
- package/TRUST-MODEL.md +8 -0
- package/dist/cli.js +33811 -0
- package/dist/decode-receipt.d.ts +19 -0
- package/dist/decode-receipt.d.ts.map +1 -0
- package/dist/decode-receipt.js +19 -0
- package/dist/decode-receipt.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/decode-receipt-cbor.d.ts +26 -0
- package/dist/lib/decode-receipt-cbor.d.ts.map +1 -0
- package/dist/lib/decode-receipt-cbor.js +182 -0
- package/dist/lib/decode-receipt-cbor.js.map +1 -0
- package/dist/lib/decode-receipt-decode.d.ts +103 -0
- package/dist/lib/decode-receipt-decode.d.ts.map +1 -0
- package/dist/lib/decode-receipt-decode.js +237 -0
- package/dist/lib/decode-receipt-decode.js.map +1 -0
- package/dist/lib/decode-receipt-labels.d.ts +54 -0
- package/dist/lib/decode-receipt-labels.d.ts.map +1 -0
- package/dist/lib/decode-receipt-labels.js +117 -0
- package/dist/lib/decode-receipt-labels.js.map +1 -0
- package/dist/lib/decode-receipt-render.d.ts +11 -0
- package/dist/lib/decode-receipt-render.d.ts.map +1 -0
- package/dist/lib/decode-receipt-render.js +242 -0
- package/dist/lib/decode-receipt-render.js.map +1 -0
- package/package.json +83 -0
- package/src/cli.ts +5 -0
- package/src/command.ts +51 -0
- package/src/commands/admin-onboard-token.ts +51 -0
- package/src/commands/admin.ts +20 -0
- package/src/commands/complete-grant.ts +41 -0
- package/src/commands/create-consistency-proof.ts +44 -0
- package/src/commands/create-log.ts +95 -0
- package/src/commands/decode-receipt.ts +20 -0
- package/src/commands/delegate.ts +55 -0
- package/src/commands/deploy.ts +72 -0
- package/src/commands/fetch-accumulator.ts +49 -0
- package/src/commands/onboard-genesis.ts +70 -0
- package/src/commands/onboard-request.ts +59 -0
- package/src/commands/register-grant.ts +74 -0
- package/src/commands/register.ts +54 -0
- package/src/commands/resolve-receipt.ts +101 -0
- package/src/commands/sign-statement.ts +53 -0
- package/src/commands/verify-grant.ts +89 -0
- package/src/commands/verify.ts +92 -0
- package/src/commoncli.ts +34 -0
- package/src/decode-receipt.ts +56 -0
- package/src/index.ts +10 -0
- package/src/lib/bytes.ts +7 -0
- package/src/lib/checkpoint-provider.ts +297 -0
- package/src/lib/complete-grant-derive.ts +237 -0
- package/src/lib/consistency-proof.ts +271 -0
- package/src/lib/create-log-build.ts +168 -0
- package/src/lib/create-log-prepare.ts +75 -0
- package/src/lib/create-receipt-chain-verify.ts +267 -0
- package/src/lib/create-receipt-derive.ts +208 -0
- package/src/lib/create-receipt-inputs.ts +79 -0
- package/src/lib/decode-checkpoint-calldata.ts +188 -0
- package/src/lib/decode-receipt-cbor.ts +202 -0
- package/src/lib/decode-receipt-decode.ts +333 -0
- package/src/lib/decode-receipt-labels.ts +132 -0
- package/src/lib/decode-receipt-render.ts +284 -0
- package/src/lib/delegate-flow.ts +289 -0
- package/src/lib/delegate-voucher.ts +122 -0
- package/src/lib/deploy-artifact.ts +79 -0
- package/src/lib/deploy-bootstrap.ts +100 -0
- package/src/lib/deploy-flow.ts +184 -0
- package/src/lib/deploy-genesis-log-id.ts +29 -0
- package/src/lib/deploy-rpc.ts +40 -0
- package/src/lib/deployment-record.ts +65 -0
- package/src/lib/fsio.ts +91 -0
- package/src/lib/grant-flags-local.ts +19 -0
- package/src/lib/onboard-attestation.ts +120 -0
- package/src/lib/onboard-genesis-body.ts +94 -0
- package/src/lib/openssl-error-queue.ts +56 -0
- package/src/lib/register-flow.ts +292 -0
- package/src/lib/register-grant-build.ts +173 -0
- package/src/lib/register-grant-complete.ts +47 -0
- package/src/lib/register-grant-flow.ts +258 -0
- package/src/lib/resolve-receipt-freshen.ts +221 -0
- package/src/lib/sign-statement-build.ts +117 -0
- package/src/lib/sign-statement-key.ts +145 -0
- package/src/lib/verify-anchored.ts +326 -0
- package/src/lib/verify-checkpoint-chain.ts +160 -0
- package/src/lib/verify-eventscan.ts +214 -0
- package/src/lib/verify-inputs.ts +143 -0
- package/src/lib/verify-known-accumulator.ts +161 -0
- package/src/lib/verify-report.ts +265 -0
- package/src/lib/x402-payment.ts +115 -0
- package/src/main/admin-onboard-token.ts +216 -0
- package/src/main/complete-grant.ts +265 -0
- package/src/main/create-consistency-proof.ts +83 -0
- package/src/main/create-log.ts +347 -0
- package/src/main/create-receipt.ts +763 -0
- package/src/main/decode-receipt.ts +72 -0
- package/src/main/delegate.ts +141 -0
- package/src/main/deploy.ts +273 -0
- package/src/main/fetch-accumulator.ts +181 -0
- package/src/main/not-implemented.ts +43 -0
- package/src/main/onboard-genesis.ts +192 -0
- package/src/main/onboard-request.ts +333 -0
- package/src/main/register-grant.ts +270 -0
- package/src/main/register.ts +243 -0
- package/src/main/sign-statement.ts +168 -0
- package/src/main/verify.ts +767 -0
- package/src/options/admin-onboard-token.ts +48 -0
- package/src/options/common.ts +69 -0
- package/src/options/complete-grant.ts +40 -0
- package/src/options/create-consistency-proof.ts +63 -0
- package/src/options/create-log.ts +99 -0
- package/src/options/create-receipt.ts +243 -0
- package/src/options/decode-receipt.ts +25 -0
- package/src/options/delegate.ts +93 -0
- package/src/options/deploy.ts +105 -0
- package/src/options/fetch-accumulator.ts +62 -0
- package/src/options/onboard-genesis.ts +74 -0
- package/src/options/onboard-request.ts +57 -0
- package/src/options/register-grant.ts +72 -0
- package/src/options/register.ts +71 -0
- package/src/options/sign-statement.ts +64 -0
- package/src/options/verify.ts +250 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { bytesEqual } from "./bytes.js";
|
|
2
|
+
import {
|
|
3
|
+
computeAccumulatorPeak,
|
|
4
|
+
openMassifNodeStore,
|
|
5
|
+
peakMMRIndexes,
|
|
6
|
+
type MassifNodeStore,
|
|
7
|
+
} from "@forestrie/receipt-verify";
|
|
8
|
+
import {
|
|
9
|
+
fetchOnChainLogState,
|
|
10
|
+
type OnChainLogState,
|
|
11
|
+
} from "./verify-anchored.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Chain-anchored, report-only verification for `forestrie create-receipt`
|
|
15
|
+
* (FOR-345 phase 2, plan-2607-15 §3/§6). This is the option-B path: it emits
|
|
16
|
+
* NO signed receipt. Its selling point is receipt-free verification — the
|
|
17
|
+
* checkpoint signature was already enforced on-chain at `publishCheckpoint`
|
|
18
|
+
* time, so no `.sth` is needed at all.
|
|
19
|
+
*
|
|
20
|
+
* The computation:
|
|
21
|
+
* 1. read the on-chain `logState(bytes32) -> {accumulator, size}`;
|
|
22
|
+
* 2. build the leaf's inclusion path AT the on-chain size
|
|
23
|
+
* (`mmrLastIndex = size - 1`) from the local massif blob and compute the
|
|
24
|
+
* accumulator peak it commits to (`computeAccumulatorPeak`);
|
|
25
|
+
* 3. compare that peak against `accumulator[peakIndex]`, where `peakIndex`
|
|
26
|
+
* is the same proof-length -> accumulator-slot selection the contract's
|
|
27
|
+
* own verifier uses (returned by `computeAccumulatorPeak`).
|
|
28
|
+
*
|
|
29
|
+
* Because the path is built at the CURRENT size, the target peak is by
|
|
30
|
+
* construction a member of the current accumulator — burial cannot occur at
|
|
31
|
+
* creation time. There is NO exact-accumulator-size constraint: any leaf with
|
|
32
|
+
* `mmrIndex < size` works, given local node data up to that size (plan §3,
|
|
33
|
+
* decision 4).
|
|
34
|
+
*
|
|
35
|
+
* Pure over bytes plus exactly one `eth_call`; no signing key, no operator.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/** Outcome token — the `--json` shape and exit-code mapping are a contract. */
|
|
39
|
+
export type ChainVerifyOutcome =
|
|
40
|
+
| "verified"
|
|
41
|
+
| "not_yet_anchored"
|
|
42
|
+
| "wrong_massif"
|
|
43
|
+
| "coverage"
|
|
44
|
+
| "accumulator_short"
|
|
45
|
+
| "peak_mismatch";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Where a chain-mode *operational* error (as opposed to a verification
|
|
49
|
+
* outcome) broke the run: massif parse, or the on-chain read/decode.
|
|
50
|
+
*/
|
|
51
|
+
export type ChainVerifyErrorStage = "parse" | "chain";
|
|
52
|
+
|
|
53
|
+
/** Stable snake_case reasons for operational failures (`--json` contract). */
|
|
54
|
+
export type ChainVerifyReason = "massif_parse_failed";
|
|
55
|
+
|
|
56
|
+
export class ChainVerifyFailure extends Error {
|
|
57
|
+
readonly stage: ChainVerifyErrorStage;
|
|
58
|
+
readonly reason: ChainVerifyReason;
|
|
59
|
+
|
|
60
|
+
constructor(
|
|
61
|
+
stage: ChainVerifyErrorStage,
|
|
62
|
+
reason: ChainVerifyReason,
|
|
63
|
+
message: string,
|
|
64
|
+
) {
|
|
65
|
+
super(message);
|
|
66
|
+
this.name = "ChainVerifyFailure";
|
|
67
|
+
this.stage = stage;
|
|
68
|
+
this.reason = reason;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Facts for narration / `--json` reporting (bigints stay bigint here). */
|
|
73
|
+
export type ChainVerifyResult = {
|
|
74
|
+
outcome: ChainVerifyOutcome;
|
|
75
|
+
leaf: { mmrIndex: bigint };
|
|
76
|
+
massif: {
|
|
77
|
+
massifIndex: bigint;
|
|
78
|
+
massifHeight: number;
|
|
79
|
+
firstIndex: bigint;
|
|
80
|
+
lastIndex: bigint;
|
|
81
|
+
};
|
|
82
|
+
onchain: {
|
|
83
|
+
/** Anchored MMR size read from the contract. */
|
|
84
|
+
size: bigint;
|
|
85
|
+
/** Number of accumulator peaks the contract holds. */
|
|
86
|
+
peakCount: number;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Peak-check facts. Absent on `not_yet_anchored` / `coverage` (the peak was
|
|
90
|
+
* never computed) — present on `verified` and `peak_mismatch`.
|
|
91
|
+
*/
|
|
92
|
+
peakCheck?: {
|
|
93
|
+
/** Inclusion path length (leaf -> accumulator peak) at the on-chain size. */
|
|
94
|
+
proofLength: number;
|
|
95
|
+
/** Accumulator slot selected for a proof of this length. */
|
|
96
|
+
peakIndex: number;
|
|
97
|
+
/** MMR index of the selected accumulator peak. */
|
|
98
|
+
peakMMRIndex: bigint;
|
|
99
|
+
/** The peak recomputed from local node data (hex). */
|
|
100
|
+
computedPeakHex: string;
|
|
101
|
+
/** The peak the contract holds at `peakIndex` (hex). */
|
|
102
|
+
onchainPeakHex: string;
|
|
103
|
+
/** True iff the two peaks match byte-for-byte. */
|
|
104
|
+
matched: boolean;
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
function errorMessage(err: unknown): string {
|
|
109
|
+
return err instanceof Error ? err.message : String(err);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function toHex(bytes: Uint8Array): string {
|
|
113
|
+
let hex = "";
|
|
114
|
+
for (const b of bytes) hex += b.toString(16).padStart(2, "0");
|
|
115
|
+
return hex;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
/** Dependencies, injectable so tests can mock the on-chain read. */
|
|
120
|
+
export type ChainVerifyDeps = {
|
|
121
|
+
fetchOnChainLogState: (opts: {
|
|
122
|
+
univocity: string;
|
|
123
|
+
logId: string;
|
|
124
|
+
rpcUrl: string;
|
|
125
|
+
}) => Promise<OnChainLogState>;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const defaultDeps: ChainVerifyDeps = { fetchOnChainLogState };
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Run the report-only chain-anchored peak check.
|
|
132
|
+
*
|
|
133
|
+
* Failure taxonomy (plan §3, plan-2607-18 W3, all reported — never thrown):
|
|
134
|
+
* - `not_yet_anchored` — `mmrIndex >= size`: the leaf postdates the last
|
|
135
|
+
* anchor ("anchor lag"); the local massif may hold it, but the chain does
|
|
136
|
+
* not attest it yet.
|
|
137
|
+
* - `wrong_massif` — the loaded massif blob is for a *different* massif than
|
|
138
|
+
* the leaf: the leaf's `mmrIndex` falls outside this blob's own
|
|
139
|
+
* `firstIndex..lastIndex` window, so its inclusion path cannot be built
|
|
140
|
+
* from these bytes. The remedy is a different blob (the one holding the
|
|
141
|
+
* leaf), NOT a *later* blob — which is why this is distinct from
|
|
142
|
+
* `coverage`. Mirrors the offline builder's `leaf_not_in_massif` guard.
|
|
143
|
+
* - `coverage` — the leaf IS in this massif, but the blob does not hold nodes
|
|
144
|
+
* up to the on-chain size (the on-chain tip lives in a later massif); a
|
|
145
|
+
* later blob is required.
|
|
146
|
+
* - `peak_mismatch` — the computed peak differs from the anchored one: the
|
|
147
|
+
* local node data has been tampered with (or is for the wrong log).
|
|
148
|
+
*
|
|
149
|
+
* Operational errors (bad massif bytes, RPC transport/decode) throw
|
|
150
|
+
* `ChainVerifyFailure` / propagate — the caller maps them to exit 1.
|
|
151
|
+
*/
|
|
152
|
+
export async function verifyChainAnchored(
|
|
153
|
+
input: {
|
|
154
|
+
massifBytes: Uint8Array;
|
|
155
|
+
mmrIndex: bigint;
|
|
156
|
+
univocity: string;
|
|
157
|
+
logId: string;
|
|
158
|
+
rpcUrl: string;
|
|
159
|
+
},
|
|
160
|
+
deps: ChainVerifyDeps = defaultDeps,
|
|
161
|
+
): Promise<ChainVerifyResult> {
|
|
162
|
+
let store: MassifNodeStore;
|
|
163
|
+
try {
|
|
164
|
+
store = openMassifNodeStore(input.massifBytes);
|
|
165
|
+
} catch (err) {
|
|
166
|
+
throw new ChainVerifyFailure(
|
|
167
|
+
"parse",
|
|
168
|
+
"massif_parse_failed",
|
|
169
|
+
`massif blob is not v2 layout: ${errorMessage(err)}`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// The on-chain read is the one networked call (throws -> operational error).
|
|
174
|
+
const state = await deps.fetchOnChainLogState({
|
|
175
|
+
univocity: input.univocity,
|
|
176
|
+
logId: input.logId,
|
|
177
|
+
rpcUrl: input.rpcUrl,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const massif = {
|
|
181
|
+
massifIndex: store.massifIndex,
|
|
182
|
+
massifHeight: store.massifHeight,
|
|
183
|
+
firstIndex: store.firstIndex,
|
|
184
|
+
lastIndex: store.lastIndex,
|
|
185
|
+
};
|
|
186
|
+
const onchain = { size: state.size, peakCount: state.accumulator.length };
|
|
187
|
+
const base = { leaf: { mmrIndex: input.mmrIndex }, massif, onchain };
|
|
188
|
+
|
|
189
|
+
// Anchor lag: the entry is newer than the last on-chain checkpoint.
|
|
190
|
+
if (input.mmrIndex >= state.size) {
|
|
191
|
+
return { outcome: "not_yet_anchored", ...base };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Wrong massif: the loaded blob must actually hold the leaf. The leaf's own
|
|
195
|
+
// inclusion path is built from THIS blob's node data, so the leaf must fall
|
|
196
|
+
// inside `firstIndex..lastIndex`. A leaf below `firstIndex` belongs to an
|
|
197
|
+
// earlier massif; above `lastIndex`, a later one. Either way the remedy is a
|
|
198
|
+
// *different* blob (the one containing the leaf) — not a later one — so this
|
|
199
|
+
// is distinct from `coverage`. Mirrors `buildReceiptOffline`'s
|
|
200
|
+
// `leaf_not_in_massif` guard (create-receipt-derive.ts), which the chain
|
|
201
|
+
// path previously omitted (plan-2607-18 W3, V2/V3). Without it a
|
|
202
|
+
// wrong-massif blob was misreported as `coverage` ("fetch the covering
|
|
203
|
+
// massif"), a misleading remedy.
|
|
204
|
+
if (input.mmrIndex < store.firstIndex || input.mmrIndex > store.lastIndex) {
|
|
205
|
+
return { outcome: "wrong_massif", ...base };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Coverage: the path is built at `size - 1`, so the local blob must hold
|
|
209
|
+
// the log tip up to `size - 1`. Nodes below `firstIndex` resolve through
|
|
210
|
+
// the ancestor peak stack, so only the upper bound can be short here.
|
|
211
|
+
const mmrLastIndex = state.size - 1n;
|
|
212
|
+
if (mmrLastIndex > store.lastIndex) {
|
|
213
|
+
return { outcome: "coverage", ...base };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Build the path AT the on-chain size and compute the accumulator peak it
|
|
217
|
+
// commits to (no exact-size constraint — this works for any covered leaf).
|
|
218
|
+
let computed: Awaited<ReturnType<typeof computeAccumulatorPeak>>;
|
|
219
|
+
try {
|
|
220
|
+
computed = await computeAccumulatorPeak({
|
|
221
|
+
massifBytes: input.massifBytes,
|
|
222
|
+
mmrIndex: input.mmrIndex,
|
|
223
|
+
mmrSize: state.size,
|
|
224
|
+
});
|
|
225
|
+
} catch (err) {
|
|
226
|
+
// A throw here means the blob could not furnish a proof node up to the
|
|
227
|
+
// on-chain size despite the range check — treat as coverage, not a crash.
|
|
228
|
+
return { outcome: "coverage", ...base };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const peaks = peakMMRIndexes(mmrLastIndex);
|
|
232
|
+
const peakMMRIndex = peaks[computed.peakIndex] ?? -1n;
|
|
233
|
+
const onchainPeak = state.accumulator[computed.peakIndex];
|
|
234
|
+
if (onchainPeak === undefined) {
|
|
235
|
+
// The contract holds FEWER peaks than the selected slot: a shape/length
|
|
236
|
+
// disagreement, not a byte-level peak mismatch. Distinct reason
|
|
237
|
+
// (`accumulator_short`, plan-2607-18 W3 V4) so a truncated on-chain
|
|
238
|
+
// accumulator reads differently from tampered node data — the computed
|
|
239
|
+
// peak is fine; the chain simply has no slot to compare it against.
|
|
240
|
+
return {
|
|
241
|
+
outcome: "accumulator_short",
|
|
242
|
+
...base,
|
|
243
|
+
peakCheck: {
|
|
244
|
+
proofLength: computed.proof.length,
|
|
245
|
+
peakIndex: computed.peakIndex,
|
|
246
|
+
peakMMRIndex,
|
|
247
|
+
computedPeakHex: toHex(computed.peak),
|
|
248
|
+
onchainPeakHex: "",
|
|
249
|
+
matched: false,
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const matched = bytesEqual(computed.peak, onchainPeak);
|
|
255
|
+
return {
|
|
256
|
+
outcome: matched ? "verified" : "peak_mismatch",
|
|
257
|
+
...base,
|
|
258
|
+
peakCheck: {
|
|
259
|
+
proofLength: computed.proof.length,
|
|
260
|
+
peakIndex: computed.peakIndex,
|
|
261
|
+
peakMMRIndex,
|
|
262
|
+
computedPeakHex: toHex(computed.peak),
|
|
263
|
+
onchainPeakHex: toHex(onchainPeak),
|
|
264
|
+
matched,
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildReceiptOffline,
|
|
3
|
+
computeAccumulatorPeak,
|
|
4
|
+
openMassifNodeStore,
|
|
5
|
+
parseCheckpoint,
|
|
6
|
+
parseReceipt,
|
|
7
|
+
peakMMRIndexes,
|
|
8
|
+
type MassifNodeStore,
|
|
9
|
+
type ParsedCheckpoint,
|
|
10
|
+
} from "@forestrie/receipt-verify";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Checkpoint-mode receipt derivation for `forestrie create-receipt`
|
|
14
|
+
* (FOR-345 phase 1, plan-2607-15 §6): stage the
|
|
15
|
+
* `@forestrie/receipt-verify` primitives so every failure is a typed
|
|
16
|
+
* `CreateReceiptFailure` with a stable stage + reason, and collect the
|
|
17
|
+
* narration facts (massif index, proof length, selected peak, cert copy)
|
|
18
|
+
* alongside the receipt bytes. Pure over bytes — no network, no key.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Where derivation broke: artefact parsing vs. proof/receipt assembly. */
|
|
22
|
+
export type CreateReceiptStage = "parse" | "derive";
|
|
23
|
+
|
|
24
|
+
/** Stable snake_case reasons — the `--json` error shape is a contract. */
|
|
25
|
+
export type CreateReceiptReason =
|
|
26
|
+
| "massif_parse_failed"
|
|
27
|
+
| "checkpoint_parse_failed"
|
|
28
|
+
| "checkpoint_missing_peak_receipts"
|
|
29
|
+
| "checkpoint_missing_sealed_size"
|
|
30
|
+
| "checkpoint_does_not_cover_leaf"
|
|
31
|
+
| "leaf_not_in_massif"
|
|
32
|
+
| "derive_failed";
|
|
33
|
+
|
|
34
|
+
export class CreateReceiptFailure extends Error {
|
|
35
|
+
readonly stage: CreateReceiptStage;
|
|
36
|
+
readonly reason: CreateReceiptReason;
|
|
37
|
+
|
|
38
|
+
constructor(
|
|
39
|
+
stage: CreateReceiptStage,
|
|
40
|
+
reason: CreateReceiptReason,
|
|
41
|
+
message: string,
|
|
42
|
+
) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.name = "CreateReceiptFailure";
|
|
45
|
+
this.stage = stage;
|
|
46
|
+
this.reason = reason;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Facts for narration / `--json` reporting (bigints stay bigint here). */
|
|
51
|
+
export type DerivedReceiptDetails = {
|
|
52
|
+
massifIndex: bigint;
|
|
53
|
+
massifHeight: number;
|
|
54
|
+
firstIndex: bigint;
|
|
55
|
+
lastIndex: bigint;
|
|
56
|
+
/** Sealed tree size the checkpoint signature commits to. */
|
|
57
|
+
sealedSize: bigint;
|
|
58
|
+
/** Inclusion path length (leaf → accumulator peak). */
|
|
59
|
+
proofLength: number;
|
|
60
|
+
/** Accumulator slot of the selected pre-signed peak receipt. */
|
|
61
|
+
peakIndex: number;
|
|
62
|
+
/** Total accumulator peaks at the sealed size. */
|
|
63
|
+
peakCount: number;
|
|
64
|
+
/** MMR index of the selected peak. */
|
|
65
|
+
peakMMRIndex: bigint;
|
|
66
|
+
/** Delegation cert (label 1000) copied from checkpoint to receipt. */
|
|
67
|
+
certCopied: boolean;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export type DerivedReceipt = {
|
|
71
|
+
receiptCbor: Uint8Array;
|
|
72
|
+
details: DerivedReceiptDetails;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function fail(
|
|
76
|
+
stage: CreateReceiptStage,
|
|
77
|
+
reason: CreateReceiptReason,
|
|
78
|
+
message: string,
|
|
79
|
+
): never {
|
|
80
|
+
throw new CreateReceiptFailure(stage, reason, message);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function errorMessage(err: unknown): string {
|
|
84
|
+
return err instanceof Error ? err.message : String(err);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Derive a receipt for `mmrIndex` from local massif + checkpoint bytes.
|
|
89
|
+
* The assembly itself is `buildReceiptOffline`; this wrapper pre-parses
|
|
90
|
+
* both artefacts so the precondition failures the taxonomy names —
|
|
91
|
+
* leaf-not-in-massif, checkpoint-doesn't-cover-leaf — are classified
|
|
92
|
+
* before derivation, and the checkpoint parse feeds the narration.
|
|
93
|
+
*/
|
|
94
|
+
export async function deriveCheckpointReceipt(input: {
|
|
95
|
+
massifBytes: Uint8Array;
|
|
96
|
+
checkpointBytes: Uint8Array;
|
|
97
|
+
mmrIndex: bigint;
|
|
98
|
+
}): Promise<DerivedReceipt> {
|
|
99
|
+
const { massifBytes, checkpointBytes, mmrIndex } = input;
|
|
100
|
+
|
|
101
|
+
let checkpoint: ParsedCheckpoint;
|
|
102
|
+
try {
|
|
103
|
+
checkpoint = parseCheckpoint(checkpointBytes);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
fail(
|
|
106
|
+
"parse",
|
|
107
|
+
"checkpoint_parse_failed",
|
|
108
|
+
`checkpoint is not a format-v3 COSE Sign1: ${errorMessage(err)}`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
if (!checkpoint.peakReceipts) {
|
|
112
|
+
fail(
|
|
113
|
+
"parse",
|
|
114
|
+
"checkpoint_missing_peak_receipts",
|
|
115
|
+
"checkpoint carries no pre-signed peak receipts (label -65931)",
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
const sealedSize = checkpoint.mmrSize;
|
|
119
|
+
if (sealedSize === null || sealedSize <= 0n) {
|
|
120
|
+
fail(
|
|
121
|
+
"parse",
|
|
122
|
+
"checkpoint_missing_sealed_size",
|
|
123
|
+
"checkpoint carries no consistency proof (cannot determine sealed size)",
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let store: MassifNodeStore;
|
|
128
|
+
try {
|
|
129
|
+
store = openMassifNodeStore(massifBytes);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
fail(
|
|
132
|
+
"parse",
|
|
133
|
+
"massif_parse_failed",
|
|
134
|
+
`massif blob is not v2 layout: ${errorMessage(err)}`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (mmrIndex >= sealedSize) {
|
|
139
|
+
fail(
|
|
140
|
+
"derive",
|
|
141
|
+
"checkpoint_does_not_cover_leaf",
|
|
142
|
+
`checkpoint does not cover the leaf: mmrIndex ${mmrIndex} >= sealed ` +
|
|
143
|
+
`size ${sealedSize} — the entry postdates this checkpoint`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
if (mmrIndex < store.firstIndex || mmrIndex > store.lastIndex) {
|
|
147
|
+
fail(
|
|
148
|
+
"derive",
|
|
149
|
+
"leaf_not_in_massif",
|
|
150
|
+
`leaf mmrIndex ${mmrIndex} is not in this massif blob (massif ` +
|
|
151
|
+
`${store.massifIndex} holds mmr indexes ${store.firstIndex}..` +
|
|
152
|
+
`${store.lastIndex})`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let receiptCbor: Uint8Array;
|
|
157
|
+
try {
|
|
158
|
+
receiptCbor = buildReceiptOffline({ massifBytes, checkpointBytes, mmrIndex });
|
|
159
|
+
} catch (err) {
|
|
160
|
+
fail("derive", "derive_failed", errorMessage(err));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Narration facts. The proof length comes from the assembled receipt.
|
|
164
|
+
//
|
|
165
|
+
// The reported peak MUST identify the exact same accumulator peak whose
|
|
166
|
+
// pre-signed receipt buildReceiptOffline attached — this narration is the
|
|
167
|
+
// demo's proof of *which* peak was signed, so it cannot drift from the
|
|
168
|
+
// builder. We therefore reuse the builder's own selection rather than
|
|
169
|
+
// recomputing it independently: computeAccumulatorPeak returns the same
|
|
170
|
+
// `peakIndex` (peakIndexForLeafProof(mmrSize, proofLen)) buildReceiptOffline
|
|
171
|
+
// indexes the -65931 array with, plus the peak hash it commits to. The
|
|
172
|
+
// -65931 array and peakMMRIndexes share the sealer's descending-height /
|
|
173
|
+
// ascending-mmr-index order (go-merklelog PeakHashes), so peakIndex maps
|
|
174
|
+
// straight into peakMMRIndexes — no inversion.
|
|
175
|
+
const proofLength = parseReceipt(receiptCbor).proof.path.length;
|
|
176
|
+
const peaks = peakMMRIndexes(sealedSize - 1n);
|
|
177
|
+
let peakIndex: number;
|
|
178
|
+
try {
|
|
179
|
+
({ peakIndex } = await computeAccumulatorPeak({
|
|
180
|
+
massifBytes,
|
|
181
|
+
mmrIndex,
|
|
182
|
+
mmrSize: sealedSize,
|
|
183
|
+
}));
|
|
184
|
+
} catch (err) {
|
|
185
|
+
fail("derive", "derive_failed", errorMessage(err));
|
|
186
|
+
}
|
|
187
|
+
const peakMMRIndex = peaks[peakIndex];
|
|
188
|
+
if (peakMMRIndex === undefined) {
|
|
189
|
+
// Unreachable given mmrIndex < sealedSize; keep the failure typed.
|
|
190
|
+
fail("derive", "derive_failed", "no accumulator peak covers the leaf");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
receiptCbor,
|
|
195
|
+
details: {
|
|
196
|
+
massifIndex: store.massifIndex,
|
|
197
|
+
massifHeight: store.massifHeight,
|
|
198
|
+
firstIndex: store.firstIndex,
|
|
199
|
+
lastIndex: store.lastIndex,
|
|
200
|
+
sealedSize,
|
|
201
|
+
proofLength,
|
|
202
|
+
peakIndex,
|
|
203
|
+
peakCount: peaks.length,
|
|
204
|
+
peakMMRIndex,
|
|
205
|
+
certCopied: checkpoint.delegationCert !== null,
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { decodeEntryIdHex } from "@forestrie/receipt-verify";
|
|
3
|
+
import type { CreateReceiptOptions } from "../options/create-receipt.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Load and resolve `forestrie create-receipt` inputs (FOR-345). Pure
|
|
7
|
+
* file/decode work — strictly no network.
|
|
8
|
+
*
|
|
9
|
+
* Leaf addressing: `--mmr-index` is used verbatim; `--entry-id` is the
|
|
10
|
+
* permanent SCRAPI entry id whose second 8 bytes ARE the big-endian
|
|
11
|
+
* mmrIndex (`decodeEntryIdHex`), so entry-id addressing is fully
|
|
12
|
+
* derivable offline — no massif index-region (urkle) lookup is involved.
|
|
13
|
+
* (Content-hash addressing is the phase-2 feature that waits on the
|
|
14
|
+
* urkle index reader, FOR-373; entry-id does not.)
|
|
15
|
+
*/
|
|
16
|
+
export type CreateReceiptLeaf = {
|
|
17
|
+
mmrIndex: bigint;
|
|
18
|
+
source: "mmr-index" | "entry-id";
|
|
19
|
+
/** Present with `--entry-id`: the id as given. */
|
|
20
|
+
entryId?: string;
|
|
21
|
+
/** Present with `--entry-id`: the decoded idtimestamp (decimal). */
|
|
22
|
+
idtimestamp?: bigint;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type CreateReceiptArtifacts = {
|
|
26
|
+
massifBytes: Uint8Array;
|
|
27
|
+
/** Present in checkpoint mode (`--checkpoint`). */
|
|
28
|
+
checkpointBytes: Uint8Array | undefined;
|
|
29
|
+
leaf: CreateReceiptLeaf;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function readBytes(path: string, flag: string): Uint8Array {
|
|
33
|
+
try {
|
|
34
|
+
return new Uint8Array(readFileSync(path));
|
|
35
|
+
} catch (err) {
|
|
36
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
37
|
+
throw new Error(`cannot read ${flag} '${path}': ${message}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Resolve the leaf mmrIndex from whichever addressing flag was given. */
|
|
42
|
+
export function resolveLeaf(options: CreateReceiptOptions): CreateReceiptLeaf {
|
|
43
|
+
if (options.mmrIndex !== undefined) {
|
|
44
|
+
return { mmrIndex: options.mmrIndex, source: "mmr-index" };
|
|
45
|
+
}
|
|
46
|
+
// Options parsing guarantees exactly one of the two flags.
|
|
47
|
+
const entryId = options.entryId!;
|
|
48
|
+
let decoded: ReturnType<typeof decodeEntryIdHex>;
|
|
49
|
+
try {
|
|
50
|
+
decoded = decodeEntryIdHex(entryId);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
53
|
+
throw new Error(`invalid --entry-id '${entryId}': ${message}`);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
mmrIndex: decoded.mmrIndex,
|
|
57
|
+
source: "entry-id",
|
|
58
|
+
entryId,
|
|
59
|
+
idtimestamp: decoded.idtimestamp,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Read massif/checkpoint artefacts from disk and resolve the leaf. */
|
|
64
|
+
export function loadCreateReceiptArtifacts(
|
|
65
|
+
options: CreateReceiptOptions,
|
|
66
|
+
): CreateReceiptArtifacts {
|
|
67
|
+
const leaf = resolveLeaf(options);
|
|
68
|
+
if (options.massif === undefined) {
|
|
69
|
+
// Unreachable: only tile anchors (checkpoint/chain) reach here, and options
|
|
70
|
+
// parsing requires --massif for those. Guard keeps the type honest.
|
|
71
|
+
throw new Error("--massif is required to build a receipt from tiles");
|
|
72
|
+
}
|
|
73
|
+
const massifBytes = readBytes(options.massif, "--massif");
|
|
74
|
+
const checkpointBytes =
|
|
75
|
+
options.checkpoint !== undefined
|
|
76
|
+
? readBytes(options.checkpoint, "--checkpoint")
|
|
77
|
+
: undefined;
|
|
78
|
+
return { massifBytes, checkpointBytes, leaf };
|
|
79
|
+
}
|