@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,763 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
+
import type { Out } from "@forestrie/cli-kit/reporting";
|
|
4
|
+
import type { CreateReceiptOptions } from "../options/create-receipt.js";
|
|
5
|
+
import {
|
|
6
|
+
CreateReceiptFailure,
|
|
7
|
+
deriveCheckpointReceipt,
|
|
8
|
+
type CreateReceiptReason,
|
|
9
|
+
type DerivedReceipt,
|
|
10
|
+
} from "../lib/create-receipt-derive.js";
|
|
11
|
+
import {
|
|
12
|
+
loadCreateReceiptArtifacts,
|
|
13
|
+
type CreateReceiptArtifacts,
|
|
14
|
+
} from "../lib/create-receipt-inputs.js";
|
|
15
|
+
import { grantCommitmentHashFromGrant } from "@forestrie/receipt-verify";
|
|
16
|
+
import {
|
|
17
|
+
loadPayloadVerifyArtifacts,
|
|
18
|
+
loadVerifyArtifacts,
|
|
19
|
+
} from "../lib/verify-inputs.js";
|
|
20
|
+
import { loadCheckpointChainFiles } from "../lib/verify-checkpoint-chain.js";
|
|
21
|
+
import {
|
|
22
|
+
assertSnapshotBinding,
|
|
23
|
+
decodeKnownAccumulator,
|
|
24
|
+
type KnownAccumulator,
|
|
25
|
+
} from "../lib/verify-known-accumulator.js";
|
|
26
|
+
import {
|
|
27
|
+
freshenFromCalldataChain,
|
|
28
|
+
freshenFromSthChain,
|
|
29
|
+
type FreshenResult,
|
|
30
|
+
} from "../lib/resolve-receipt-freshen.js";
|
|
31
|
+
import {
|
|
32
|
+
ChainVerifyFailure,
|
|
33
|
+
verifyChainAnchored,
|
|
34
|
+
type ChainVerifyOutcome,
|
|
35
|
+
type ChainVerifyReason,
|
|
36
|
+
type ChainVerifyResult,
|
|
37
|
+
} from "../lib/create-receipt-chain-verify.js";
|
|
38
|
+
|
|
39
|
+
/** Where an operational error broke the run. */
|
|
40
|
+
export type CreateReceiptErrorStage = "input" | "parse" | "derive";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* `--json` operational-error shape on stdout — stable stage + reason
|
|
44
|
+
* tokens (input load/decode; artefact parse; proof/receipt derivation,
|
|
45
|
+
* including `leaf_not_in_massif` / `checkpoint_does_not_cover_leaf`).
|
|
46
|
+
*/
|
|
47
|
+
export type CreateReceiptErrorReport = {
|
|
48
|
+
error:
|
|
49
|
+
| "create_receipt_input_failed"
|
|
50
|
+
| "create_receipt_parse_failed"
|
|
51
|
+
| "create_receipt_derive_failed";
|
|
52
|
+
command: "create-receipt";
|
|
53
|
+
stage: CreateReceiptErrorStage;
|
|
54
|
+
reason?: CreateReceiptReason;
|
|
55
|
+
message: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Exit codes for chain-anchored (`--univocity`) mode — one code per
|
|
60
|
+
* verification outcome so scripts can branch without parsing text. Operational
|
|
61
|
+
* errors (bad input / RPC) stay on the shared exit 1.
|
|
62
|
+
*/
|
|
63
|
+
export const CHAIN_EXIT_CODE: Record<ChainVerifyOutcome, number> = {
|
|
64
|
+
verified: 0,
|
|
65
|
+
not_yet_anchored: 2,
|
|
66
|
+
coverage: 3,
|
|
67
|
+
peak_mismatch: 4,
|
|
68
|
+
// plan-2607-18 W3: `wrong_massif` (V2/V3) and `accumulator_short` (V4) split
|
|
69
|
+
// out of the coverage / peak_mismatch buckets; next free codes in the 2/3/4
|
|
70
|
+
// scheme. No collision with `verify`, which only uses 0/1.
|
|
71
|
+
wrong_massif: 5,
|
|
72
|
+
accumulator_short: 6,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* `--json` chain-mode success/verdict shape (report-only — NO signed
|
|
77
|
+
* receipt; the selling point is receipt-free, on-chain verification). The
|
|
78
|
+
* shape is a contract: `outcome` + the on-chain size, computed peak, matched
|
|
79
|
+
* slot, and PASS/FAIL.
|
|
80
|
+
*/
|
|
81
|
+
export type CreateReceiptChainReport = {
|
|
82
|
+
command: "create-receipt";
|
|
83
|
+
anchor: "chain";
|
|
84
|
+
/** True only when `outcome === "verified"`. */
|
|
85
|
+
ok: boolean;
|
|
86
|
+
outcome: ChainVerifyOutcome;
|
|
87
|
+
univocity: string;
|
|
88
|
+
logId: string;
|
|
89
|
+
leaf: {
|
|
90
|
+
mmrIndex: string;
|
|
91
|
+
source: "mmr-index" | "entry-id";
|
|
92
|
+
entryId?: string;
|
|
93
|
+
idtimestamp?: string;
|
|
94
|
+
};
|
|
95
|
+
massif: {
|
|
96
|
+
massifIndex: string;
|
|
97
|
+
massifHeight: number;
|
|
98
|
+
firstIndex: string;
|
|
99
|
+
lastIndex: string;
|
|
100
|
+
};
|
|
101
|
+
onchain: {
|
|
102
|
+
size: string;
|
|
103
|
+
peakCount: number;
|
|
104
|
+
};
|
|
105
|
+
peakCheck?: {
|
|
106
|
+
proofLength: number;
|
|
107
|
+
peakIndex: number;
|
|
108
|
+
peakMMRIndex: string;
|
|
109
|
+
computedPeakHex: string;
|
|
110
|
+
onchainPeakHex: string;
|
|
111
|
+
matched: boolean;
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/** `--json` chain-mode operational-error shape (bad massif / RPC). */
|
|
116
|
+
export type CreateReceiptChainErrorReport = {
|
|
117
|
+
error: "create_receipt_chain_failed";
|
|
118
|
+
command: "create-receipt";
|
|
119
|
+
anchor: "chain";
|
|
120
|
+
stage: "parse" | "chain";
|
|
121
|
+
reason?: ChainVerifyReason;
|
|
122
|
+
message: string;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/** Structured `--json` success report — the shape is a contract. */
|
|
126
|
+
export type CreateReceiptReport = {
|
|
127
|
+
command: "create-receipt";
|
|
128
|
+
anchor: "checkpoint";
|
|
129
|
+
leaf: {
|
|
130
|
+
mmrIndex: string;
|
|
131
|
+
source: "mmr-index" | "entry-id";
|
|
132
|
+
entryId?: string;
|
|
133
|
+
idtimestamp?: string;
|
|
134
|
+
};
|
|
135
|
+
massif: {
|
|
136
|
+
massifIndex: string;
|
|
137
|
+
massifHeight: number;
|
|
138
|
+
firstIndex: string;
|
|
139
|
+
lastIndex: string;
|
|
140
|
+
};
|
|
141
|
+
checkpoint: {
|
|
142
|
+
sealedSize: string;
|
|
143
|
+
peakCount: number;
|
|
144
|
+
};
|
|
145
|
+
proof: {
|
|
146
|
+
length: number;
|
|
147
|
+
peakIndex: number;
|
|
148
|
+
peakMMRIndex: string;
|
|
149
|
+
};
|
|
150
|
+
certCopied: boolean;
|
|
151
|
+
receiptBytes: number;
|
|
152
|
+
/** Output path, when `--out` was given. */
|
|
153
|
+
out?: string;
|
|
154
|
+
/** Base64 receipt bytes, when no `--out` (JSON owns stdout). */
|
|
155
|
+
receiptB64?: string;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const ERROR_CODES: Record<
|
|
159
|
+
CreateReceiptErrorStage,
|
|
160
|
+
CreateReceiptErrorReport["error"]
|
|
161
|
+
> = {
|
|
162
|
+
input: "create_receipt_input_failed",
|
|
163
|
+
parse: "create_receipt_parse_failed",
|
|
164
|
+
derive: "create_receipt_derive_failed",
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/** Structured envelope under `--json`; one clean line on stderr otherwise. */
|
|
168
|
+
function reportRunError(
|
|
169
|
+
out: Out,
|
|
170
|
+
options: CreateReceiptOptions,
|
|
171
|
+
stage: CreateReceiptErrorStage,
|
|
172
|
+
err: unknown,
|
|
173
|
+
): void {
|
|
174
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
175
|
+
const reason = err instanceof CreateReceiptFailure ? err.reason : undefined;
|
|
176
|
+
if (options.json) {
|
|
177
|
+
const report: CreateReceiptErrorReport = {
|
|
178
|
+
error: ERROR_CODES[stage],
|
|
179
|
+
command: "create-receipt",
|
|
180
|
+
stage,
|
|
181
|
+
...(reason !== undefined ? { reason } : {}),
|
|
182
|
+
message,
|
|
183
|
+
};
|
|
184
|
+
out.out(JSON.stringify(report, null, 2));
|
|
185
|
+
} else {
|
|
186
|
+
out.warn("forestrie create-receipt: %s: %s", stage, message);
|
|
187
|
+
}
|
|
188
|
+
process.exitCode = 1;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** One-line human explanation for each non-verified outcome. */
|
|
192
|
+
const CHAIN_OUTCOME_NARRATION: Record<ChainVerifyOutcome, string> = {
|
|
193
|
+
verified: "computed peak matches the on-chain accumulator",
|
|
194
|
+
not_yet_anchored:
|
|
195
|
+
"leaf postdates the last on-chain anchor (mmrIndex >= on-chain size) — anchor lag",
|
|
196
|
+
wrong_massif:
|
|
197
|
+
"the massif blob is for a different massif than the leaf",
|
|
198
|
+
coverage:
|
|
199
|
+
"local massif blob does not hold nodes up to the on-chain size — fetch the covering massif",
|
|
200
|
+
accumulator_short:
|
|
201
|
+
"the on-chain accumulator holds fewer peaks than the leaf's proof selects — chain state is behind or truncated",
|
|
202
|
+
peak_mismatch:
|
|
203
|
+
"computed peak differs from the anchored peak — local node data does not match the chain",
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* FOR-345 (phase 2, plan-2607-15 §3/§6): chain-anchored, REPORT-ONLY
|
|
208
|
+
* verification. No `.sth`, no signed receipt — the selling point is
|
|
209
|
+
* receipt-free verification, because the checkpoint signature was already
|
|
210
|
+
* enforced on-chain at publish. Reads `logState(bytes32)`, rebuilds the
|
|
211
|
+
* leaf's inclusion path at the on-chain size, and compares the computed peak
|
|
212
|
+
* with `accumulator[peakIndex]`. There is NO exact-size constraint: any
|
|
213
|
+
* `mmrIndex < size` works given local node data to that size.
|
|
214
|
+
*/
|
|
215
|
+
async function runChainAnchored(
|
|
216
|
+
out: Out,
|
|
217
|
+
options: CreateReceiptOptions,
|
|
218
|
+
artifacts: CreateReceiptArtifacts,
|
|
219
|
+
): Promise<void> {
|
|
220
|
+
let result: ChainVerifyResult;
|
|
221
|
+
try {
|
|
222
|
+
result = await verifyChainAnchored({
|
|
223
|
+
massifBytes: artifacts.massifBytes,
|
|
224
|
+
mmrIndex: artifacts.leaf.mmrIndex,
|
|
225
|
+
// Options parsing guarantees these in chain mode.
|
|
226
|
+
univocity: options.univocity!,
|
|
227
|
+
logId: options.logId!,
|
|
228
|
+
rpcUrl: options.rpcUrl!,
|
|
229
|
+
});
|
|
230
|
+
} catch (err) {
|
|
231
|
+
reportChainRunError(out, options, err);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const leaf = artifacts.leaf;
|
|
236
|
+
const ok = result.outcome === "verified";
|
|
237
|
+
|
|
238
|
+
if (options.json) {
|
|
239
|
+
const report: CreateReceiptChainReport = {
|
|
240
|
+
command: "create-receipt",
|
|
241
|
+
anchor: "chain",
|
|
242
|
+
ok,
|
|
243
|
+
outcome: result.outcome,
|
|
244
|
+
univocity: options.univocity!,
|
|
245
|
+
logId: options.logId!,
|
|
246
|
+
leaf: {
|
|
247
|
+
mmrIndex: leaf.mmrIndex.toString(10),
|
|
248
|
+
source: leaf.source,
|
|
249
|
+
...(leaf.entryId !== undefined ? { entryId: leaf.entryId } : {}),
|
|
250
|
+
...(leaf.idtimestamp !== undefined
|
|
251
|
+
? { idtimestamp: leaf.idtimestamp.toString(10) }
|
|
252
|
+
: {}),
|
|
253
|
+
},
|
|
254
|
+
massif: {
|
|
255
|
+
massifIndex: result.massif.massifIndex.toString(10),
|
|
256
|
+
massifHeight: result.massif.massifHeight,
|
|
257
|
+
firstIndex: result.massif.firstIndex.toString(10),
|
|
258
|
+
lastIndex: result.massif.lastIndex.toString(10),
|
|
259
|
+
},
|
|
260
|
+
onchain: {
|
|
261
|
+
size: result.onchain.size.toString(10),
|
|
262
|
+
peakCount: result.onchain.peakCount,
|
|
263
|
+
},
|
|
264
|
+
...(result.peakCheck !== undefined
|
|
265
|
+
? {
|
|
266
|
+
peakCheck: {
|
|
267
|
+
proofLength: result.peakCheck.proofLength,
|
|
268
|
+
peakIndex: result.peakCheck.peakIndex,
|
|
269
|
+
peakMMRIndex: result.peakCheck.peakMMRIndex.toString(10),
|
|
270
|
+
computedPeakHex: result.peakCheck.computedPeakHex,
|
|
271
|
+
onchainPeakHex: result.peakCheck.onchainPeakHex,
|
|
272
|
+
matched: result.peakCheck.matched,
|
|
273
|
+
},
|
|
274
|
+
}
|
|
275
|
+
: {}),
|
|
276
|
+
};
|
|
277
|
+
out.out(JSON.stringify(report, null, 2));
|
|
278
|
+
process.exitCode = CHAIN_EXIT_CODE[result.outcome];
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Human narration: on-chain size, computed peak, matched slot, PASS/FAIL.
|
|
283
|
+
out.print(
|
|
284
|
+
"create-receipt: on-chain — %s @ log %s: size %s, %d peak(s)",
|
|
285
|
+
options.univocity,
|
|
286
|
+
options.logId,
|
|
287
|
+
result.onchain.size.toString(10),
|
|
288
|
+
result.onchain.peakCount,
|
|
289
|
+
);
|
|
290
|
+
out.print(
|
|
291
|
+
"create-receipt: leaf — mmrIndex %s (from --%s)",
|
|
292
|
+
leaf.mmrIndex.toString(10),
|
|
293
|
+
leaf.source,
|
|
294
|
+
);
|
|
295
|
+
if (result.peakCheck !== undefined) {
|
|
296
|
+
out.print(
|
|
297
|
+
"create-receipt: proof — %d node(s) to peak %d/%d (mmrIndex %s)",
|
|
298
|
+
result.peakCheck.proofLength,
|
|
299
|
+
result.peakCheck.peakIndex + 1,
|
|
300
|
+
result.onchain.peakCount,
|
|
301
|
+
result.peakCheck.peakMMRIndex.toString(10),
|
|
302
|
+
);
|
|
303
|
+
out.print(
|
|
304
|
+
"create-receipt: computed — %s",
|
|
305
|
+
result.peakCheck.computedPeakHex,
|
|
306
|
+
);
|
|
307
|
+
out.print(
|
|
308
|
+
"create-receipt: on-chain — %s (slot %d)",
|
|
309
|
+
result.peakCheck.onchainPeakHex,
|
|
310
|
+
result.peakCheck.peakIndex,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
if (ok) {
|
|
314
|
+
out.out(
|
|
315
|
+
`PASS: leaf verified against the on-chain accumulator (size ${result.onchain.size}, peak slot ${result.peakCheck!.peakIndex}) — no receipt needed`,
|
|
316
|
+
);
|
|
317
|
+
} else {
|
|
318
|
+
// `wrong_massif` names the concrete mmrIndex the caller must supply the
|
|
319
|
+
// massif for (plan-2607-18 W3: "supply the massif containing mmrIndex N").
|
|
320
|
+
const detail =
|
|
321
|
+
result.outcome === "wrong_massif"
|
|
322
|
+
? `${CHAIN_OUTCOME_NARRATION.wrong_massif} (supply the massif containing mmrIndex ${leaf.mmrIndex})`
|
|
323
|
+
: CHAIN_OUTCOME_NARRATION[result.outcome];
|
|
324
|
+
out.out(`FAIL: ${result.outcome} — ${detail}`);
|
|
325
|
+
}
|
|
326
|
+
process.exitCode = CHAIN_EXIT_CODE[result.outcome];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Structured envelope for chain-mode operational errors (input / RPC). */
|
|
330
|
+
function reportChainRunError(
|
|
331
|
+
out: Out,
|
|
332
|
+
options: CreateReceiptOptions,
|
|
333
|
+
err: unknown,
|
|
334
|
+
): void {
|
|
335
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
336
|
+
const stage = err instanceof ChainVerifyFailure ? err.stage : "chain";
|
|
337
|
+
const reason = err instanceof ChainVerifyFailure ? err.reason : undefined;
|
|
338
|
+
if (options.json) {
|
|
339
|
+
const report: CreateReceiptChainErrorReport = {
|
|
340
|
+
error: "create_receipt_chain_failed",
|
|
341
|
+
command: "create-receipt",
|
|
342
|
+
anchor: "chain",
|
|
343
|
+
stage,
|
|
344
|
+
...(reason !== undefined ? { reason } : {}),
|
|
345
|
+
message,
|
|
346
|
+
};
|
|
347
|
+
out.out(JSON.stringify(report, null, 2));
|
|
348
|
+
} else {
|
|
349
|
+
out.warn("forestrie create-receipt: %s: %s", stage, message);
|
|
350
|
+
}
|
|
351
|
+
process.exitCode = 1;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* FOR-345 self-serve receipt derivation, two anchor modes:
|
|
356
|
+
*
|
|
357
|
+
* - `checkpoint` (phase 1, plan-2607-15 §6): rebuild the leaf→peak inclusion
|
|
358
|
+
* path from the massif blob and attach it to the checkpoint's pre-signed
|
|
359
|
+
* peak receipt (`buildReceiptOffline`). No network, no key, no operator
|
|
360
|
+
* API call; the result is verify-equivalent with an API-issued receipt.
|
|
361
|
+
* Receipt bytes go to `--out`, or raw to stdout — except under `--json`
|
|
362
|
+
* without `--out`, where they ride base64 inside the report.
|
|
363
|
+
* - `chain` (phase 2, plan-2607-15 §3): REPORT-ONLY verification of the
|
|
364
|
+
* computed peak against the on-chain accumulator (`--univocity`). No
|
|
365
|
+
* `.sth`, no signed receipt — its selling point is receipt-free
|
|
366
|
+
* verification. Distinct exit codes per outcome (see `CHAIN_EXIT_CODE`).
|
|
367
|
+
*/
|
|
368
|
+
export async function runCreateReceipt(
|
|
369
|
+
out: Out,
|
|
370
|
+
options: CreateReceiptOptions,
|
|
371
|
+
): Promise<void> {
|
|
372
|
+
if (
|
|
373
|
+
options.anchor === "freshen-sth" ||
|
|
374
|
+
options.anchor === "freshen-calldata"
|
|
375
|
+
) {
|
|
376
|
+
await runFreshen(out, options);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let artifacts: CreateReceiptArtifacts;
|
|
381
|
+
try {
|
|
382
|
+
artifacts = loadCreateReceiptArtifacts(options);
|
|
383
|
+
} catch (err) {
|
|
384
|
+
reportRunError(out, options, "input", err);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (options.anchor === "chain") {
|
|
389
|
+
await runChainAnchored(out, options, artifacts);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
let derived: DerivedReceipt;
|
|
394
|
+
try {
|
|
395
|
+
derived = await deriveCheckpointReceipt({
|
|
396
|
+
massifBytes: artifacts.massifBytes,
|
|
397
|
+
// Checkpoint mode: options parsing guarantees --checkpoint.
|
|
398
|
+
checkpointBytes: artifacts.checkpointBytes!,
|
|
399
|
+
mmrIndex: artifacts.leaf.mmrIndex,
|
|
400
|
+
});
|
|
401
|
+
} catch (err) {
|
|
402
|
+
const stage = err instanceof CreateReceiptFailure ? err.stage : "derive";
|
|
403
|
+
reportRunError(out, options, stage, err);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const { receiptCbor, details } = derived;
|
|
408
|
+
try {
|
|
409
|
+
if (options.out !== undefined) {
|
|
410
|
+
writeFileSync(options.out, receiptCbor);
|
|
411
|
+
}
|
|
412
|
+
} catch (err) {
|
|
413
|
+
reportRunError(out, options, "input", err);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const leaf = artifacts.leaf;
|
|
418
|
+
if (options.json) {
|
|
419
|
+
const report: CreateReceiptReport = {
|
|
420
|
+
command: "create-receipt",
|
|
421
|
+
anchor: "checkpoint",
|
|
422
|
+
leaf: {
|
|
423
|
+
mmrIndex: leaf.mmrIndex.toString(10),
|
|
424
|
+
source: leaf.source,
|
|
425
|
+
...(leaf.entryId !== undefined ? { entryId: leaf.entryId } : {}),
|
|
426
|
+
...(leaf.idtimestamp !== undefined
|
|
427
|
+
? { idtimestamp: leaf.idtimestamp.toString(10) }
|
|
428
|
+
: {}),
|
|
429
|
+
},
|
|
430
|
+
massif: {
|
|
431
|
+
massifIndex: details.massifIndex.toString(10),
|
|
432
|
+
massifHeight: details.massifHeight,
|
|
433
|
+
firstIndex: details.firstIndex.toString(10),
|
|
434
|
+
lastIndex: details.lastIndex.toString(10),
|
|
435
|
+
},
|
|
436
|
+
checkpoint: {
|
|
437
|
+
sealedSize: details.sealedSize.toString(10),
|
|
438
|
+
peakCount: details.peakCount,
|
|
439
|
+
},
|
|
440
|
+
proof: {
|
|
441
|
+
length: details.proofLength,
|
|
442
|
+
peakIndex: details.peakIndex,
|
|
443
|
+
peakMMRIndex: details.peakMMRIndex.toString(10),
|
|
444
|
+
},
|
|
445
|
+
certCopied: details.certCopied,
|
|
446
|
+
receiptBytes: receiptCbor.length,
|
|
447
|
+
...(options.out !== undefined
|
|
448
|
+
? { out: options.out }
|
|
449
|
+
: { receiptB64: Buffer.from(receiptCbor).toString("base64") }),
|
|
450
|
+
};
|
|
451
|
+
out.out(JSON.stringify(report, null, 2));
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (options.out === undefined) {
|
|
456
|
+
// Raw CBOR to stdout (pipeable); the narration stays on stderr.
|
|
457
|
+
writeFileSync(1, receiptCbor);
|
|
458
|
+
}
|
|
459
|
+
out.print(
|
|
460
|
+
"create-receipt: massif — index %s (height %d, mmr indexes %s..%s)",
|
|
461
|
+
details.massifIndex.toString(10),
|
|
462
|
+
details.massifHeight,
|
|
463
|
+
details.firstIndex.toString(10),
|
|
464
|
+
details.lastIndex.toString(10),
|
|
465
|
+
);
|
|
466
|
+
out.print(
|
|
467
|
+
"create-receipt: leaf — mmrIndex %s (from --%s)",
|
|
468
|
+
leaf.mmrIndex.toString(10),
|
|
469
|
+
leaf.source,
|
|
470
|
+
);
|
|
471
|
+
out.print(
|
|
472
|
+
"create-receipt: checkpoint — sealed size %s, %d peak(s)",
|
|
473
|
+
details.sealedSize.toString(10),
|
|
474
|
+
details.peakCount,
|
|
475
|
+
);
|
|
476
|
+
out.print(
|
|
477
|
+
"create-receipt: proof — %d node(s) to peak %d/%d (mmrIndex %s)",
|
|
478
|
+
details.proofLength,
|
|
479
|
+
details.peakIndex + 1,
|
|
480
|
+
details.peakCount,
|
|
481
|
+
details.peakMMRIndex.toString(10),
|
|
482
|
+
);
|
|
483
|
+
out.print(
|
|
484
|
+
"create-receipt: cert — delegation cert copied: %s",
|
|
485
|
+
details.certCopied ? "yes" : "no",
|
|
486
|
+
);
|
|
487
|
+
out.print(
|
|
488
|
+
"create-receipt: receipt — %d bytes -> %s",
|
|
489
|
+
receiptCbor.length,
|
|
490
|
+
options.out ?? "stdout",
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// ---------------------------------------------------------------------------
|
|
495
|
+
// Freshen source (FOR-418 Phase 3c): tile-free re-anchor of a stale --receipt
|
|
496
|
+
// against a retained .sth checkpoint chain. New surface, so its report uses the
|
|
497
|
+
// forward name `resolve-receipt`; the tile-mode reports keep `create-receipt`
|
|
498
|
+
// for contract stability.
|
|
499
|
+
// ---------------------------------------------------------------------------
|
|
500
|
+
|
|
501
|
+
/** Freshen source, for the report + narration. */
|
|
502
|
+
type FreshenSource = "checkpoint-chain" | "calldata";
|
|
503
|
+
|
|
504
|
+
/** `--json` freshen success report — the shape is a contract. */
|
|
505
|
+
export type ResolveReceiptFreshenReport = {
|
|
506
|
+
command: "resolve-receipt";
|
|
507
|
+
source: FreshenSource;
|
|
508
|
+
leaf: { mmrIndex: string; entryId?: string };
|
|
509
|
+
chain: { links: number; sealedSize: string; sources: string[] };
|
|
510
|
+
proof: { length: number };
|
|
511
|
+
receiptBytes: number;
|
|
512
|
+
out?: string;
|
|
513
|
+
receiptB64?: string;
|
|
514
|
+
/** True when the `--receipt` file was rewritten in place. */
|
|
515
|
+
inPlace?: boolean;
|
|
516
|
+
/** Present when `--known-accumulator` bound the freshened state. */
|
|
517
|
+
knownAccumulator?: { matched: boolean };
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
/** `--json` freshen operational-error shape. */
|
|
521
|
+
export type ResolveReceiptFreshenErrorReport = {
|
|
522
|
+
error: "resolve_receipt_freshen_failed";
|
|
523
|
+
command: "resolve-receipt";
|
|
524
|
+
source: FreshenSource;
|
|
525
|
+
stage: "input" | "freshen";
|
|
526
|
+
message: string;
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
function reportFreshenError(
|
|
530
|
+
out: Out,
|
|
531
|
+
options: CreateReceiptOptions,
|
|
532
|
+
source: FreshenSource,
|
|
533
|
+
stage: "input" | "freshen",
|
|
534
|
+
err: unknown,
|
|
535
|
+
): void {
|
|
536
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
537
|
+
if (options.json) {
|
|
538
|
+
const report: ResolveReceiptFreshenErrorReport = {
|
|
539
|
+
error: "resolve_receipt_freshen_failed",
|
|
540
|
+
command: "resolve-receipt",
|
|
541
|
+
source,
|
|
542
|
+
stage,
|
|
543
|
+
message,
|
|
544
|
+
};
|
|
545
|
+
out.out(JSON.stringify(report, null, 2));
|
|
546
|
+
} else {
|
|
547
|
+
out.warn("forestrie resolve-receipt: %s: %s", stage, message);
|
|
548
|
+
}
|
|
549
|
+
process.exitCode = 1;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** Load + bind the optional `--known-accumulator` snapshot for the freshen run. */
|
|
553
|
+
function loadFreshenSnapshot(
|
|
554
|
+
options: CreateReceiptOptions,
|
|
555
|
+
): KnownAccumulator | undefined {
|
|
556
|
+
if (options.knownAccumulator === undefined) return undefined;
|
|
557
|
+
const snapshot = decodeKnownAccumulator(
|
|
558
|
+
new Uint8Array(readFileSync(options.knownAccumulator)),
|
|
559
|
+
);
|
|
560
|
+
// Reject a snapshot for the wrong log/contract before it can anchor anything.
|
|
561
|
+
// (Only calldata freshen supplies --univocity/--log-id; the `.sth` path skips
|
|
562
|
+
// the binding it has no target for.)
|
|
563
|
+
assertSnapshotBinding(snapshot, {
|
|
564
|
+
univocity: options.univocity,
|
|
565
|
+
logId: options.logId,
|
|
566
|
+
});
|
|
567
|
+
return snapshot;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/** The stale receipt + its leaf inputs, from whichever leaf source was given. */
|
|
571
|
+
type FreshenLeaf = {
|
|
572
|
+
receiptCbor: Uint8Array;
|
|
573
|
+
/** Leaf ContentHash: `SHA-256(payload)` (statement) or the grant commitment. */
|
|
574
|
+
inner: Uint8Array;
|
|
575
|
+
idtimestampBe8: Uint8Array;
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Load the stale receipt + recompute the leaf ContentHash the same way `verify`
|
|
580
|
+
* does — from `--payload` (statement: `SHA-256(payload)`) or the committed grant
|
|
581
|
+
* (`--committed-grant`/`-file`). Options parsing guarantees exactly one.
|
|
582
|
+
*/
|
|
583
|
+
async function loadFreshenLeaf(
|
|
584
|
+
options: CreateReceiptOptions,
|
|
585
|
+
): Promise<FreshenLeaf> {
|
|
586
|
+
if (options.payload !== undefined) {
|
|
587
|
+
const a = loadPayloadVerifyArtifacts({
|
|
588
|
+
genesis: undefined,
|
|
589
|
+
receipt: options.receipt!,
|
|
590
|
+
payload: options.payload,
|
|
591
|
+
entryId: options.entryId!,
|
|
592
|
+
});
|
|
593
|
+
const inner = new Uint8Array(
|
|
594
|
+
await crypto.subtle.digest("SHA-256", new Uint8Array(a.payload)),
|
|
595
|
+
);
|
|
596
|
+
return { receiptCbor: a.receiptCbor, inner, idtimestampBe8: a.idtimestampBe8 };
|
|
597
|
+
}
|
|
598
|
+
const a = loadVerifyArtifacts({
|
|
599
|
+
genesis: undefined,
|
|
600
|
+
receipt: options.receipt!,
|
|
601
|
+
committedGrant: options.committedGrant,
|
|
602
|
+
committedGrantFile: options.committedGrantFile,
|
|
603
|
+
entryId: options.entryId,
|
|
604
|
+
});
|
|
605
|
+
const inner = await grantCommitmentHashFromGrant(a.grant);
|
|
606
|
+
return { receiptCbor: a.receiptCbor, inner, idtimestampBe8: a.idtimestampBe8 };
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** Build the FreshenResult from whichever tile-free source the flags selected. */
|
|
610
|
+
async function freshenFromSource(
|
|
611
|
+
options: CreateReceiptOptions,
|
|
612
|
+
leaf: FreshenLeaf,
|
|
613
|
+
): Promise<FreshenResult> {
|
|
614
|
+
const knownAccumulator = loadFreshenSnapshot(options);
|
|
615
|
+
if (options.anchor === "freshen-calldata") {
|
|
616
|
+
// Options parsing guarantees these in calldata-freshen mode.
|
|
617
|
+
const latestCheckpointBytes = new Uint8Array(
|
|
618
|
+
readFileSync(options.checkpoint!),
|
|
619
|
+
);
|
|
620
|
+
return freshenFromCalldataChain({
|
|
621
|
+
oldReceiptBytes: leaf.receiptCbor,
|
|
622
|
+
inner: leaf.inner,
|
|
623
|
+
idtimestampBe8: leaf.idtimestampBe8,
|
|
624
|
+
univocity: options.univocity!,
|
|
625
|
+
logId: options.logId!,
|
|
626
|
+
rpcUrl: options.rpcUrl!,
|
|
627
|
+
latestCheckpointBytes,
|
|
628
|
+
knownAccumulator,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
const chain = loadCheckpointChainFiles(options.checkpointChain!);
|
|
632
|
+
return freshenFromSthChain({
|
|
633
|
+
oldReceiptBytes: leaf.receiptCbor,
|
|
634
|
+
inner: leaf.inner,
|
|
635
|
+
idtimestampBe8: leaf.idtimestampBe8,
|
|
636
|
+
checkpoints: chain.checkpoints,
|
|
637
|
+
sourceRefs: chain.files.map((f) => basename(f)),
|
|
638
|
+
knownAccumulator,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Freshen a stale `--receipt` tile-free. Reuses `verify`'s leaf-value loaders
|
|
644
|
+
* (recomputed identically from `--payload` or the committed grant), reads the
|
|
645
|
+
* climb material from the selected source — a retained `.sth` chain
|
|
646
|
+
* (`--checkpoint-chain`; emitted under the chain's own tail checkpoint) or the
|
|
647
|
+
* on-chain `publishCheckpoint` calldata (`--rpc-url`/`--univocity`/`--log-id` +
|
|
648
|
+
* a separately-supplied latest `--checkpoint` for emission) — extends the
|
|
649
|
+
* receipt's old path to the current peak, and re-emits. Both sources produce a
|
|
650
|
+
* receipt re-anchored to the current accumulator; the trust posture (freshness
|
|
651
|
+
* vs signer provenance) is chosen at verify. See the receipt trust model:
|
|
652
|
+
* https://github.com/forestrie/protocol/blob/main/spec/receipt-trust-model.md
|
|
653
|
+
*/
|
|
654
|
+
async function runFreshen(
|
|
655
|
+
out: Out,
|
|
656
|
+
options: CreateReceiptOptions,
|
|
657
|
+
): Promise<void> {
|
|
658
|
+
const source: FreshenSource =
|
|
659
|
+
options.anchor === "freshen-calldata" ? "calldata" : "checkpoint-chain";
|
|
660
|
+
|
|
661
|
+
let leaf: FreshenLeaf;
|
|
662
|
+
try {
|
|
663
|
+
leaf = await loadFreshenLeaf(options);
|
|
664
|
+
} catch (err) {
|
|
665
|
+
reportFreshenError(out, options, source, "input", err);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
let result: FreshenResult;
|
|
670
|
+
try {
|
|
671
|
+
result = await freshenFromSource(options, leaf);
|
|
672
|
+
} catch (err) {
|
|
673
|
+
// Chain read / fold / self-check failures are the freshen stage; a bad
|
|
674
|
+
// --checkpoint / --checkpoint-chain path surfaces here too but reads as
|
|
675
|
+
// input — keep the single "freshen" stage token, the message names it.
|
|
676
|
+
reportFreshenError(out, options, source, "freshen", err);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const { receiptCbor, details } = result;
|
|
681
|
+
// --in-place rewrites the stale receipt file; otherwise --out, else stdout.
|
|
682
|
+
const writeTarget = options.inPlace ? options.receipt! : options.out;
|
|
683
|
+
try {
|
|
684
|
+
if (options.inPlace) {
|
|
685
|
+
// Crash-safe overwrite of the source receipt: write a sibling temp then
|
|
686
|
+
// atomically rename, so a failed write never truncates the original.
|
|
687
|
+
const tmp = `${writeTarget}.freshen.tmp`;
|
|
688
|
+
writeFileSync(tmp, receiptCbor);
|
|
689
|
+
renameSync(tmp, writeTarget!);
|
|
690
|
+
} else if (writeTarget !== undefined) {
|
|
691
|
+
writeFileSync(writeTarget, receiptCbor);
|
|
692
|
+
}
|
|
693
|
+
} catch (err) {
|
|
694
|
+
reportFreshenError(out, options, source, "input", err);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (options.json) {
|
|
699
|
+
const report: ResolveReceiptFreshenReport = {
|
|
700
|
+
command: "resolve-receipt",
|
|
701
|
+
source,
|
|
702
|
+
leaf: {
|
|
703
|
+
mmrIndex: details.leafMmrIndex.toString(10),
|
|
704
|
+
...(options.entryId !== undefined ? { entryId: options.entryId } : {}),
|
|
705
|
+
},
|
|
706
|
+
chain: {
|
|
707
|
+
links: details.chainLinks,
|
|
708
|
+
sealedSize: details.sealedSize.toString(10),
|
|
709
|
+
sources: details.sourceRefs,
|
|
710
|
+
},
|
|
711
|
+
proof: { length: details.proofLength },
|
|
712
|
+
receiptBytes: receiptCbor.length,
|
|
713
|
+
...(writeTarget !== undefined
|
|
714
|
+
? { out: writeTarget }
|
|
715
|
+
: { receiptB64: Buffer.from(receiptCbor).toString("base64") }),
|
|
716
|
+
...(options.inPlace ? { inPlace: true } : {}),
|
|
717
|
+
...(details.knownAccumulatorMatched !== undefined
|
|
718
|
+
? { knownAccumulator: { matched: details.knownAccumulatorMatched } }
|
|
719
|
+
: {}),
|
|
720
|
+
};
|
|
721
|
+
out.out(JSON.stringify(report, null, 2));
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (writeTarget === undefined) {
|
|
726
|
+
// Raw CBOR to stdout (pipeable); narration stays on stderr.
|
|
727
|
+
writeFileSync(1, receiptCbor);
|
|
728
|
+
}
|
|
729
|
+
const trust =
|
|
730
|
+
source === "calldata"
|
|
731
|
+
? "climb from on-chain publishCheckpoint calldata (authority checked on-chain at publish), emitted under the supplied latest .sth"
|
|
732
|
+
: "climb from the retained .sth chain, emitted under its own tail checkpoint";
|
|
733
|
+
const unit = source === "calldata" ? "calldata link(s)" : ".sth link(s)";
|
|
734
|
+
out.print(
|
|
735
|
+
"resolve-receipt: freshen — re-anchored to the current accumulator, no tiles; %s",
|
|
736
|
+
trust,
|
|
737
|
+
);
|
|
738
|
+
out.print(
|
|
739
|
+
"resolve-receipt: leaf — mmrIndex %s",
|
|
740
|
+
details.leafMmrIndex.toString(10),
|
|
741
|
+
);
|
|
742
|
+
out.print(
|
|
743
|
+
"resolve-receipt: chain — %d %s -> sealed size %s",
|
|
744
|
+
details.chainLinks,
|
|
745
|
+
unit,
|
|
746
|
+
details.sealedSize.toString(10),
|
|
747
|
+
);
|
|
748
|
+
if (details.knownAccumulatorMatched) {
|
|
749
|
+
out.print(
|
|
750
|
+
"resolve-receipt: anchor — folded state matches --known-accumulator (trusted)",
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
out.print(
|
|
754
|
+
"resolve-receipt: proof — %d node(s) in the extended path",
|
|
755
|
+
details.proofLength,
|
|
756
|
+
);
|
|
757
|
+
out.print(
|
|
758
|
+
"resolve-receipt: receipt — %d bytes -> %s%s",
|
|
759
|
+
receiptCbor.length,
|
|
760
|
+
writeTarget ?? "stdout",
|
|
761
|
+
options.inPlace ? " (in place)" : "",
|
|
762
|
+
);
|
|
763
|
+
}
|