@mikeargento/bitgraph-audit 0.9.0 → 0.10.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/README.md +5 -3
- package/dist/audit.d.ts +4 -3
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +9 -3
- package/dist/audit.js.map +1 -1
- package/dist/ceilings.d.ts.map +1 -1
- package/dist/ceilings.js +1 -0
- package/dist/ceilings.js.map +1 -1
- package/dist/cli.js +72 -19
- package/dist/cli.js.map +1 -1
- package/dist/exports.d.ts.map +1 -1
- package/dist/exports.js +6 -3
- package/dist/exports.js.map +1 -1
- package/dist/floors.d.ts +23 -0
- package/dist/floors.d.ts.map +1 -0
- package/dist/floors.js +203 -0
- package/dist/floors.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/ingest.d.ts +2 -0
- package/dist/ingest.d.ts.map +1 -1
- package/dist/ingest.js +14 -0
- package/dist/ingest.js.map +1 -1
- package/dist/report-json.d.ts.map +1 -1
- package/dist/report-json.js +15 -0
- package/dist/report-json.js.map +1 -1
- package/dist/report-md.js +95 -27
- package/dist/report-md.js.map +1 -1
- package/dist/temporal.d.ts +12 -0
- package/dist/temporal.d.ts.map +1 -1
- package/dist/temporal.js +245 -41
- package/dist/temporal.js.map +1 -1
- package/dist/types.d.ts +136 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/__tests__/base-floor.test.ts +405 -0
- package/src/audit.ts +9 -3
- package/src/ceilings.ts +1 -0
- package/src/cli.ts +80 -20
- package/src/exports.ts +7 -3
- package/src/floors.ts +242 -0
- package/src/index.ts +8 -0
- package/src/ingest.ts +15 -0
- package/src/report-json.ts +15 -0
- package/src/report-md.ts +109 -31
- package/src/temporal.ts +279 -48
- package/src/types.ts +140 -6
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026 Argento Computing Inc.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base floors (enclave v10) in the audit.
|
|
5
|
+
*
|
|
6
|
+
* A proof made by enclave v10 signs its floor as commit.slotFloor, a Base
|
|
7
|
+
* block, instead of an Ethereum anchor. The audit reads it through
|
|
8
|
+
* signedFloorOf and gives the proof a NOT-BEFORE bound labelled Base, never a
|
|
9
|
+
* not-after: a later proof's floor block can predate this proof. Floors count
|
|
10
|
+
* toward an epoch's coverage but never order epochs. Bundles from before the
|
|
11
|
+
* cutover must audit exactly as before, so the mixed case checks that the old
|
|
12
|
+
* epoch's segments are the same with and without the new epoch beside them.
|
|
13
|
+
*
|
|
14
|
+
* The real fixtures are the v10 harness proofs over Base block #52,271,417
|
|
15
|
+
* (src/__tests__/fuse3-fixtures/, the block's header saved beside them). The
|
|
16
|
+
* synthetic proofs are signed here with throwaway keys, exactly as the
|
|
17
|
+
* enclave signs (the canonical signed body), so they verify.
|
|
18
|
+
*
|
|
19
|
+
* Run, after `npm run build` in packages/audit:
|
|
20
|
+
* node --test packages/audit/src/__tests__/base-floor.test.ts
|
|
21
|
+
*/
|
|
22
|
+
import { test, describe, before, after } from "node:test";
|
|
23
|
+
import assert from "node:assert/strict";
|
|
24
|
+
import { readFileSync } from "node:fs";
|
|
25
|
+
import { mkdtemp, mkdir, writeFile, rm, readFile } from "node:fs/promises";
|
|
26
|
+
import { tmpdir } from "node:os";
|
|
27
|
+
import { join, dirname } from "node:path";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
import { spawnSync } from "node:child_process";
|
|
30
|
+
import { getPublicKeyAsync, signAsync } from "@noble/ed25519";
|
|
31
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
32
|
+
import { keccak_256 } from "@noble/hashes/sha3";
|
|
33
|
+
import {
|
|
34
|
+
BASE_MAINNET_GENESIS_TIME, assembleCarrierV3Payload, buildCarrier, buildSignedBody, canonicalize, computeChainHash,
|
|
35
|
+
computeProofHash, type BitGraphProof,
|
|
36
|
+
} from "@mikeargento/bitgraph-verify";
|
|
37
|
+
import { buildJsonReport, buildMarkdownReport, computeExitFlags, runAudit, FLOOR_HEADER_VERSION } from "@mikeargento/bitgraph-audit";
|
|
38
|
+
import type { AuditResult, TemporalSegment } from "@mikeargento/bitgraph-audit";
|
|
39
|
+
|
|
40
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
41
|
+
const FIX = join(here, "../../../../src/__tests__/fuse3-fixtures");
|
|
42
|
+
const CLI = join(here, "../../dist/cli.js");
|
|
43
|
+
const HEADER = readFileSync(join(FIX, "floor-base-52271417.rlp.hex"), "utf8").trim();
|
|
44
|
+
const BLOCK = 52271417;
|
|
45
|
+
const BLOCK_HASH = "0x71d926aeae9847fcc15fad2d4de8871923f56862d42db7a7463e95a838f8f9b0";
|
|
46
|
+
const BLOCK_TIME = 1791332181;
|
|
47
|
+
const realProof = (name: string): string => readFileSync(join(FIX, name), "utf8");
|
|
48
|
+
const realHash = (name: string): string => computeProofHash(JSON.parse(realProof(name)));
|
|
49
|
+
|
|
50
|
+
const b64 = (u: Uint8Array): string => Buffer.from(u).toString("base64");
|
|
51
|
+
const hex = (u: Uint8Array): string => `0x${Buffer.from(u).toString("hex")}`;
|
|
52
|
+
const utf8 = (s: string): Uint8Array => new TextEncoder().encode(s);
|
|
53
|
+
const scheduled = (n: number): number => BASE_MAINNET_GENESIS_TIME + 2 * n;
|
|
54
|
+
|
|
55
|
+
interface Key { priv: Uint8Array; pub: string }
|
|
56
|
+
async function key(seed: string): Promise<Key> {
|
|
57
|
+
const priv = sha256(utf8(`base-floor audit test key ${seed} (TEST ONLY)`));
|
|
58
|
+
return { priv, pub: b64(await getPublicKeyAsync(priv)) };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface Minted { proof: BitGraphProof; proofHash: string; chainHash: string }
|
|
62
|
+
|
|
63
|
+
async function sign(k: Key, artifactBytes: Uint8Array, commit: Record<string, unknown>, attribution?: Record<string, string>): Promise<Minted> {
|
|
64
|
+
const proof = {
|
|
65
|
+
version: "bitgraph/1",
|
|
66
|
+
artifact: { hashAlg: "sha256", digestB64: b64(sha256(artifactBytes)) },
|
|
67
|
+
commit: { nonceB64: b64(sha256(utf8(`nonce ${JSON.stringify(commit)}`))), ...commit },
|
|
68
|
+
signer: { publicKeyB64: k.pub, signatureB64: "" },
|
|
69
|
+
environment: { enforcement: "stub", measurement: "test-measurement-base-floor" },
|
|
70
|
+
...(attribution !== undefined ? { attribution } : {}),
|
|
71
|
+
} as unknown as BitGraphProof;
|
|
72
|
+
proof.signer.signatureB64 = b64(await signAsync(canonicalize(buildSignedBody(proof) as never), k.priv));
|
|
73
|
+
return { proof, proofHash: computeProofHash(proof), chainHash: computeChainHash(proof) };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function baseFloor(blockNumber: number, opts?: { hash?: string; time?: number }): Record<string, unknown> {
|
|
77
|
+
return {
|
|
78
|
+
chain: "base",
|
|
79
|
+
evmChainId: 8453,
|
|
80
|
+
blockNumber,
|
|
81
|
+
blockHash: opts?.hash ?? hex(sha256(utf8(`base block ${blockNumber}`))),
|
|
82
|
+
blockTimestamp: opts?.time ?? scheduled(blockNumber),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** A user proof whose floor is a Base block (enclave v10). */
|
|
87
|
+
async function baseProof(k: Key, epochId: string, slot: string, counter: string, floor: Record<string, unknown>, prevB64?: string, extra?: Record<string, unknown>): Promise<Minted> {
|
|
88
|
+
return sign(k, utf8(`base ${epochId} ${counter}`), {
|
|
89
|
+
counter, slotCounter: slot, ...(prevB64 !== undefined ? { prevB64 } : {}), epochId, slotFloor: floor, chainId: "bitgraph:main", ...(extra ?? {}),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// --- the old world: an Ethereum anchor and its witness ---------------------
|
|
94
|
+
|
|
95
|
+
function rlp(item: Uint8Array | Uint8Array[]): Uint8Array {
|
|
96
|
+
const len = (n: number, off: number): number[] => {
|
|
97
|
+
if (n <= 55) return [off + n];
|
|
98
|
+
const b: number[] = [];
|
|
99
|
+
for (let v = n; v > 0; v = Math.floor(v / 256)) b.unshift(v & 0xff);
|
|
100
|
+
return [off + 55 + b.length, ...b];
|
|
101
|
+
};
|
|
102
|
+
if (item instanceof Uint8Array) return item.length === 1 && item[0]! < 0x80 ? item : new Uint8Array([...len(item.length, 0x80), ...item]);
|
|
103
|
+
const body = item.flatMap((i) => [...rlp(i)]);
|
|
104
|
+
return new Uint8Array([...len(body.length, 0xc0), ...body]);
|
|
105
|
+
}
|
|
106
|
+
function be(n: number): Uint8Array {
|
|
107
|
+
const b: number[] = [];
|
|
108
|
+
for (let v = n; v > 0; v = Math.floor(v / 256)) b.unshift(v & 0xff);
|
|
109
|
+
return new Uint8Array(b);
|
|
110
|
+
}
|
|
111
|
+
function ethHeader(blockNumber: number, timestamp: number): { rlpHex: string; hash: string } {
|
|
112
|
+
const items: Uint8Array[] = [];
|
|
113
|
+
for (let i = 0; i < 20; i++) items.push(i === 8 ? be(blockNumber) : i === 11 ? be(timestamp) : new Uint8Array(32).fill(i + 1));
|
|
114
|
+
const bytes = rlp(items);
|
|
115
|
+
return { rlpHex: hex(bytes), hash: hex(keccak_256(bytes)) };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function anchorProof(k: Key, epochId: string, slot: string, counter: string, blockNumber: number, blockHash: string, prevB64?: string): Promise<Minted> {
|
|
119
|
+
return sign(k, utf8(blockHash), { counter, slotCounter: slot, ...(prevB64 !== undefined ? { prevB64 } : {}), epochId, chainId: "bitgraph:main" }, {
|
|
120
|
+
name: "Ethereum Anchor", title: `https://etherscan.io/block/${blockNumber}`, message: blockHash,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
async function userProof(k: Key, epochId: string, slot: string, counter: string, prevB64?: string): Promise<Minted> {
|
|
124
|
+
return sign(k, utf8(`user ${epochId} ${counter}`), { counter, slotCounter: slot, ...(prevB64 !== undefined ? { prevB64 } : {}), epochId, chainId: "bitgraph:main" });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// --- bundles ----------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
const dirs: string[] = [];
|
|
130
|
+
async function bundle(files: Record<string, string>): Promise<string> {
|
|
131
|
+
const dir = await mkdtemp(join(tmpdir(), "audit-base-floor-"));
|
|
132
|
+
dirs.push(dir);
|
|
133
|
+
for (const [path, content] of Object.entries(files)) {
|
|
134
|
+
await mkdir(dirname(join(dir, path)), { recursive: true });
|
|
135
|
+
await writeFile(join(dir, path), content);
|
|
136
|
+
}
|
|
137
|
+
return dir;
|
|
138
|
+
}
|
|
139
|
+
after(async () => {
|
|
140
|
+
for (const d of dirs) await rm(d, { recursive: true, force: true });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const json = (m: Minted): string => JSON.stringify(m.proof, null, 2);
|
|
144
|
+
const floorHeaderFile = (header = HEADER, extra?: Record<string, unknown>): string =>
|
|
145
|
+
JSON.stringify({ version: FLOOR_HEADER_VERSION, chain: "base", evmChainId: 8453, blockNumber: BLOCK, blockHash: BLOCK_HASH, blockTimestamp: BLOCK_TIME, header, ...(extra ?? {}) });
|
|
146
|
+
|
|
147
|
+
function segmentOf(r: AuditResult, proofHash: string): TemporalSegment {
|
|
148
|
+
const s = r.temporal.segments.find((x) => x.memberProofHashes.includes(proofHash));
|
|
149
|
+
assert.ok(s, `proof ${proofHash} lands in a segment`);
|
|
150
|
+
return s;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
describe("a Base floor bounds its proof not-before, labelled Base", () => {
|
|
156
|
+
test("real v10 harness proofs with the floor header: a checked not-before on Base block 52,271,417, no not-after", async () => {
|
|
157
|
+
const dir = await bundle({
|
|
158
|
+
"proofs/trailer3.json": realProof("trailer3.proof.json"),
|
|
159
|
+
"proofs/made3.json": realProof("made3.proof.json"),
|
|
160
|
+
"base-floor/floor-header.json": floorHeaderFile(),
|
|
161
|
+
});
|
|
162
|
+
const r = await runAudit(dir);
|
|
163
|
+
assert.equal(r.temporal.floorProblems, undefined, "no floor problem");
|
|
164
|
+
const floors = r.temporal.signedFloors ?? [];
|
|
165
|
+
assert.equal(floors.length, 2);
|
|
166
|
+
for (const f of floors) {
|
|
167
|
+
assert.equal(f.chain, "base");
|
|
168
|
+
assert.equal(f.blockNumber, BLOCK);
|
|
169
|
+
assert.equal(f.blockHash, BLOCK_HASH);
|
|
170
|
+
assert.equal(f.header, "checked");
|
|
171
|
+
assert.equal(f.headerPath, "base-floor/floor-header.json");
|
|
172
|
+
assert.equal(f.bound, "not-before");
|
|
173
|
+
}
|
|
174
|
+
for (const name of ["trailer3.proof.json", "made3.proof.json"]) {
|
|
175
|
+
const seg = segmentOf(r, realHash(name));
|
|
176
|
+
assert.equal(seg.upperBounds.length, 0, "a floor never gives a not-after");
|
|
177
|
+
const b = seg.lowerBounds[0]!;
|
|
178
|
+
assert.equal(b.kind, "not-before");
|
|
179
|
+
assert.equal(b.source, "signed-floor");
|
|
180
|
+
assert.equal(b.chain, "base");
|
|
181
|
+
assert.equal(b.timeSource, "header");
|
|
182
|
+
assert.equal(b.blockNumber, String(BLOCK));
|
|
183
|
+
assert.equal(b.timestamp, BLOCK_TIME);
|
|
184
|
+
assert.equal(b.basis, "block-hash-unpredictability");
|
|
185
|
+
assert.equal(b.boundClass, "evidence");
|
|
186
|
+
assert.equal(b.weaker, false);
|
|
187
|
+
assert.match(b.claim, /Base block 52271417/);
|
|
188
|
+
assert.doesNotMatch(b.claim, /Ethereum/);
|
|
189
|
+
assert.doesNotMatch(b.claim, /[–—]/, "no dashes in report copy");
|
|
190
|
+
}
|
|
191
|
+
// made3 follows trailer3 in the chain? Either way its own floor is the bound.
|
|
192
|
+
assert.equal(segmentOf(r, realHash("made3.proof.json")).lowerBounds.some((b) => b.evidence === "signed-floor"), true);
|
|
193
|
+
// Epoch coverage counts both as floored.
|
|
194
|
+
const epoch = r.reconstruction.epochRelationships.epochs[0]!;
|
|
195
|
+
const nb = (epoch.anchorBounds ?? []).find((b) => b.kind === "not-before");
|
|
196
|
+
assert.ok(nb, "the epoch has a not-before");
|
|
197
|
+
assert.equal(nb.source, "signed-floor");
|
|
198
|
+
assert.equal(nb.chain, "base");
|
|
199
|
+
assert.equal(nb.coveredProofCount, 2);
|
|
200
|
+
assert.equal((epoch.anchorBounds ?? []).some((b) => b.kind === "not-after"), false);
|
|
201
|
+
assert.equal(computeExitFlags(r).chainAnomaliesOrDivergences && (r.temporal.floorProblems ?? []).length > 0, false);
|
|
202
|
+
const report = buildJsonReport(r);
|
|
203
|
+
assert.deepEqual(report.summary.temporal.baseFloors, { signed: 2, bounding: 2, headersChecked: 2, withheld: 0, problems: 0 });
|
|
204
|
+
const md = buildMarkdownReport(r);
|
|
205
|
+
assert.match(md, /2 proofs sign a Base block as their floor/);
|
|
206
|
+
assert.match(md, /Base block 52,271,417/);
|
|
207
|
+
assert.doesNotMatch(md, /this report makes no wall-clock claims/);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("without the header: the time is the signed one, and the bound says it needs a Base lookup", async () => {
|
|
211
|
+
const dir = await bundle({ "proofs/made3.json": realProof("made3.proof.json") });
|
|
212
|
+
const r = await runAudit(dir);
|
|
213
|
+
const f = r.temporal.signedFloors![0]!;
|
|
214
|
+
assert.equal(f.header, "not-carried");
|
|
215
|
+
assert.equal(f.bound, "not-before");
|
|
216
|
+
const b = segmentOf(r, realHash("made3.proof.json")).lowerBounds[0]!;
|
|
217
|
+
assert.equal(b.timeSource, "signed");
|
|
218
|
+
assert.equal(b.timestamp, BLOCK_TIME);
|
|
219
|
+
assert.match(b.claim, /needs a Base lookup/);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("no not-after is invented from a later proof's floor, even when that floor is older", async () => {
|
|
223
|
+
const k = await key("chain");
|
|
224
|
+
// B2 follows B1 in the chain but signs an OLDER Base block: its floor
|
|
225
|
+
// says nothing about B1, and B1 gets no upper bound from it.
|
|
226
|
+
const b1 = await baseProof(k, "E-base", "1", "2", baseFloor(52_000_100));
|
|
227
|
+
const b2 = await baseProof(k, "E-base", "3", "4", baseFloor(52_000_000), b1.chainHash);
|
|
228
|
+
const b3 = await baseProof(k, "E-base", "5", "6", baseFloor(52_000_200), b2.chainHash);
|
|
229
|
+
const r = await runAudit(await bundle({ "b1.json": json(b1), "b2.json": json(b2), "b3.json": json(b3) }));
|
|
230
|
+
for (const seg of r.temporal.segments) {
|
|
231
|
+
assert.equal(seg.upperBounds.length, 0, "no segment carries a not-after");
|
|
232
|
+
assert.equal(seg.status, "lower-bounded");
|
|
233
|
+
}
|
|
234
|
+
const s1 = segmentOf(r, b1.proofHash);
|
|
235
|
+
assert.equal(s1.lowerBounds[0]!.timestamp, scheduled(52_000_100));
|
|
236
|
+
assert.equal(s1.lowerBounds[0]!.evidence, "signed-floor");
|
|
237
|
+
// B2's tightest not-before is B1's floor, reached by the hash link (it is later than B2's own).
|
|
238
|
+
const s2 = segmentOf(r, b2.proofHash);
|
|
239
|
+
assert.equal(s2.lowerBounds[0]!.timestamp, scheduled(52_000_100));
|
|
240
|
+
assert.equal(s2.lowerBounds[0]!.evidence, "chain-link");
|
|
241
|
+
assert.equal(s2.lowerBounds[0]!.anchorProofHash, b1.proofHash);
|
|
242
|
+
// B3's own floor is the latest.
|
|
243
|
+
assert.equal(segmentOf(r, b3.proofHash).lowerBounds[0]!.evidence, "signed-floor");
|
|
244
|
+
const epoch = r.reconstruction.epochRelationships.epochs[0]!;
|
|
245
|
+
assert.equal((epoch.anchorBounds ?? []).some((b) => b.kind === "not-after"), false);
|
|
246
|
+
assert.equal(r.temporal.anchorOrderedPairs.length, 0);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe("a bundle mixing Ethereum-anchored and Base-floored proofs", () => {
|
|
251
|
+
let oldOnly: AuditResult;
|
|
252
|
+
let mixed: AuditResult;
|
|
253
|
+
const h: Record<string, Minted> = {};
|
|
254
|
+
const T_ANCHOR = 1_700_000_000;
|
|
255
|
+
|
|
256
|
+
before(async () => {
|
|
257
|
+
const k1 = await key("old epoch");
|
|
258
|
+
const k2 = await key("new epoch");
|
|
259
|
+
const H = ethHeader(100, T_ANCHOR);
|
|
260
|
+
h.p1 = await userProof(k1, "E-old", "1", "2");
|
|
261
|
+
h.a1 = await anchorProof(k1, "E-old", "3", "4", 100, H.hash, h.p1.chainHash);
|
|
262
|
+
h.p2 = await userProof(k1, "E-old", "5", "6", h.a1.chainHash);
|
|
263
|
+
h.b1 = await baseProof(k2, "E-new", "1", "2", baseFloor(52_100_000));
|
|
264
|
+
h.b2 = await baseProof(k2, "E-new", "3", "4", baseFloor(52_100_010), h.b1.chainHash);
|
|
265
|
+
const old = {
|
|
266
|
+
"old/p1.json": json(h.p1), "old/a1.json": json(h.a1), "old/p2.json": json(h.p2),
|
|
267
|
+
"old/a1.witness.json": JSON.stringify({ version: "bitgraph-anchor-witness/1", headerRlpHex: H.rlpHex, blockNumber: 100, blockHash: H.hash }),
|
|
268
|
+
};
|
|
269
|
+
oldOnly = await runAudit(await bundle(old));
|
|
270
|
+
mixed = await runAudit(await bundle({ ...old, "new/b1.json": json(h.b1), "new/b2.json": json(h.b2) }));
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("the old epoch's bounds are exactly what they are without the new epoch beside it", () => {
|
|
274
|
+
const oldSegs = (r: AuditResult) => r.temporal.segments.filter((s) => s.partition.epochId === "E-old");
|
|
275
|
+
assert.deepEqual(oldSegs(mixed), oldSegs(oldOnly));
|
|
276
|
+
const p2 = segmentOf(mixed, h.p2!.proofHash).lowerBounds[0]!;
|
|
277
|
+
assert.equal(p2.source, undefined, "an anchor bound carries no source");
|
|
278
|
+
assert.equal(p2.chain, undefined, "an anchor bound is Ethereum");
|
|
279
|
+
assert.equal(p2.anchorProofHash, h.a1!.proofHash);
|
|
280
|
+
assert.equal(p2.timestamp, T_ANCHOR);
|
|
281
|
+
assert.equal(segmentOf(mixed, h.p1!.proofHash).upperBounds[0]!.anchorProofHash, h.a1!.proofHash);
|
|
282
|
+
const eOld = (r: AuditResult) => r.reconstruction.epochRelationships.epochs.find((e) => e.epochId === "E-old")!.anchorBounds;
|
|
283
|
+
assert.deepEqual(eOld(mixed), eOld(oldOnly));
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("the new epoch is floored on Base, and floors order no epoch", () => {
|
|
287
|
+
for (const m of [h.b1!, h.b2!]) {
|
|
288
|
+
const b = segmentOf(mixed, m.proofHash).lowerBounds[0]!;
|
|
289
|
+
assert.equal(b.chain, "base");
|
|
290
|
+
assert.equal(b.source, "signed-floor");
|
|
291
|
+
}
|
|
292
|
+
const eNew = mixed.reconstruction.epochRelationships.epochs.find((e) => e.epochId === "E-new")!;
|
|
293
|
+
const nb = eNew.anchorBounds!.find((b) => b.kind === "not-before")!;
|
|
294
|
+
assert.equal(nb.chain, "base");
|
|
295
|
+
assert.equal(nb.coveredProofCount, 2);
|
|
296
|
+
assert.match(nb.claim!, /orders no epoch/);
|
|
297
|
+
// The old epoch has a not-after (an anchor followed P1) earlier than every
|
|
298
|
+
// Base floor, yet no pair is claimed: the after side reads anchors only.
|
|
299
|
+
assert.equal(mixed.temporal.anchorOrderedPairs.length, 0);
|
|
300
|
+
const report = buildJsonReport(mixed);
|
|
301
|
+
assert.equal(report.summary.temporal.anchorsIdentified, 1);
|
|
302
|
+
assert.equal(report.summary.temporal.baseFloors?.signed, 2);
|
|
303
|
+
const md = buildMarkdownReport(mixed);
|
|
304
|
+
assert.match(md, /1 Ethereum anchor proof was/);
|
|
305
|
+
assert.match(md, /2 proofs sign a Base block as their floor/);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe("floor problems are reported, never silently bounded (exit bit 2)", () => {
|
|
310
|
+
test("a proof that signs both an Ethereum anchor and a Base floor", async () => {
|
|
311
|
+
const k = await key("both");
|
|
312
|
+
const both = await baseProof(k, "E-both", "1", "2", baseFloor(52_200_000), undefined, {
|
|
313
|
+
slotAnchor: { counter: "0", blockNumber: 25_000_000, blockHash: hex(sha256(utf8("eth block"))) },
|
|
314
|
+
});
|
|
315
|
+
const r = await runAudit(await bundle({ "both.json": json(both) }));
|
|
316
|
+
const problems = r.temporal.floorProblems ?? [];
|
|
317
|
+
assert.equal(problems.length, 1);
|
|
318
|
+
assert.equal(problems[0]!.code, "floor-ambiguous");
|
|
319
|
+
assert.equal(problems[0]!.proofHash, both.proofHash);
|
|
320
|
+
assert.equal(r.temporal.signedFloors, undefined, "nothing is read from it as a floor");
|
|
321
|
+
assert.equal(segmentOf(r, both.proofHash).lowerBounds.length, 0);
|
|
322
|
+
assert.equal(computeExitFlags(r).code & 2, 2);
|
|
323
|
+
assert.match(buildMarkdownReport(r), /floor-ambiguous/);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("a Base floor off Base mainnet's schedule", async () => {
|
|
327
|
+
const k = await key("schedule");
|
|
328
|
+
const off = await baseProof(k, "E-off", "1", "2", baseFloor(52_200_000, { time: scheduled(52_200_000) + 1 }));
|
|
329
|
+
const r = await runAudit(await bundle({ "off.json": json(off) }));
|
|
330
|
+
assert.equal(r.temporal.floorProblems?.[0]?.code, "floor-off-schedule");
|
|
331
|
+
assert.equal(r.temporal.signedFloors?.[0]?.bound, "withheld");
|
|
332
|
+
assert.equal(segmentOf(r, off.proofHash).lowerBounds.length, 0);
|
|
333
|
+
assert.equal(computeExitFlags(r).code & 2, 2);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("a slotFloor that does not name Base mainnet", async () => {
|
|
337
|
+
const k = await key("chain id");
|
|
338
|
+
const wrong = await baseProof(k, "E-wrong", "1", "2", { ...baseFloor(52_200_000), evmChainId: 84532 });
|
|
339
|
+
const r = await runAudit(await bundle({ "wrong.json": json(wrong) }));
|
|
340
|
+
assert.equal(r.temporal.floorProblems?.[0]?.code, "floor-malformed");
|
|
341
|
+
assert.equal(segmentOf(r, wrong.proofHash).lowerBounds.length, 0);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("a floor header for the signed hash that carries another number contradicts the proof", async () => {
|
|
345
|
+
const k = await key("header");
|
|
346
|
+
// Signs the real block's hash at the wrong height (on schedule for that height).
|
|
347
|
+
const liar = await baseProof(k, "E-liar", "1", "2", baseFloor(BLOCK + 1, { hash: BLOCK_HASH }));
|
|
348
|
+
const r = await runAudit(await bundle({ "liar.json": json(liar), "floor.json": floorHeaderFile() }));
|
|
349
|
+
const p = (r.temporal.floorProblems ?? []).find((x) => x.code === "floor-header-mismatch");
|
|
350
|
+
assert.ok(p, "the mismatch is a problem");
|
|
351
|
+
assert.equal(p.proofHash, liar.proofHash);
|
|
352
|
+
assert.equal(r.temporal.signedFloors?.[0]?.bound, "withheld");
|
|
353
|
+
assert.equal(segmentOf(r, liar.proofHash).lowerBounds.length, 0);
|
|
354
|
+
assert.equal(computeExitFlags(r).code & 2, 2);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("a floor header that hashes to another block than it names, and one no proof signs", async () => {
|
|
358
|
+
const tampered = HEADER.slice(0, -2) + (HEADER.endsWith("0") ? "1" : "0");
|
|
359
|
+
const r1 = await runAudit(await bundle({ "proofs/made3.json": realProof("made3.proof.json"), "h.json": floorHeaderFile(tampered) }));
|
|
360
|
+
assert.ok((r1.temporal.floorProblems ?? []).some((p) => p.code === "floor-header-mismatch" || p.code === "floor-header-malformed"));
|
|
361
|
+
assert.equal(r1.temporal.signedFloors?.[0]?.header, "not-carried", "a tampered header never checks the floor");
|
|
362
|
+
const r2 = await runAudit(await bundle({ "h.json": floorHeaderFile() }));
|
|
363
|
+
assert.equal(r2.temporal.floorProblems?.[0]?.code, "floor-header-unmatched");
|
|
364
|
+
assert.equal(computeExitFlags(r2).code & 2, 2);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
describe("bitgraph-carrier/3 through the CLI", () => {
|
|
369
|
+
test("the Base floor header is unpacked, checked and reported; no closing anchor is asked for", async () => {
|
|
370
|
+
const k = await key("carrier");
|
|
371
|
+
const inner = utf8("a BitGraphed file's committed bytes\n");
|
|
372
|
+
const floor = JSON.parse(realProof("trailer3.proof.json")).commit.slotFloor as Record<string, unknown>;
|
|
373
|
+
const minted = await sign(k, inner, { counter: "2", slotCounter: "1", epochId: "E-carrier", slotFloor: floor, chainId: "bitgraph:main" });
|
|
374
|
+
const payload = assembleCarrierV3Payload({
|
|
375
|
+
proof: minted.proof as never,
|
|
376
|
+
floor: { status: "present", basis: "base-header", header: HEADER },
|
|
377
|
+
ceilingInTime: { status: "unfetched" },
|
|
378
|
+
});
|
|
379
|
+
const dir = await bundle({});
|
|
380
|
+
const file = join(dir, "note.bitgraph.txt");
|
|
381
|
+
await writeFile(file, buildCarrier(inner, payload));
|
|
382
|
+
const out = join(dir, "out");
|
|
383
|
+
const run = spawnSync(process.execPath, [CLI, file, "--out", out, "--format", "json"], { encoding: "utf8" });
|
|
384
|
+
assert.ok(run.status === 0 || run.status === 1 || run.status === 2 || run.status === 3, `ran (${run.status}): ${run.stderr}`);
|
|
385
|
+
assert.match(run.stdout, /no earlier than: Base block 52271417/);
|
|
386
|
+
assert.match(run.stdout, /no ceiling in position/);
|
|
387
|
+
assert.doesNotMatch(run.stdout, /closing anchor/);
|
|
388
|
+
assert.doesNotMatch(run.stdout, /floor PROBLEM/);
|
|
389
|
+
const report = JSON.parse(await readFile(join(out, "audit-report.json"), "utf8"));
|
|
390
|
+
const f = report.temporal.signedFloors[0];
|
|
391
|
+
assert.equal(f.header, "checked");
|
|
392
|
+
assert.equal(f.headerPath, "base-floor/floor-header.json");
|
|
393
|
+
assert.equal(f.bound, "not-before");
|
|
394
|
+
assert.equal(report.anchors.records.length, 0, "no anchor proof stands under a /3 floor");
|
|
395
|
+
assert.equal(report.temporal.segments[0].lowerBounds[0].chain, "base");
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("--help names the /3 layout", () => {
|
|
399
|
+
const run = spawnSync(process.execPath, [CLI, "--help"], { encoding: "utf8" });
|
|
400
|
+
assert.equal(run.status, 0);
|
|
401
|
+
assert.match(run.stdout, /base-floor\/floor-header\.json/);
|
|
402
|
+
assert.match(run.stdout, /bitgraph-floor-header\/1/);
|
|
403
|
+
assert.doesNotMatch(run.stdout, /[–—]/);
|
|
404
|
+
});
|
|
405
|
+
});
|
package/src/audit.ts
CHANGED
|
@@ -40,7 +40,7 @@ import type { AuditOptions, AuditResult, ExitFlags, IngestResult } from "./types
|
|
|
40
40
|
* this equals package.json's version, so the constant cannot drift silently
|
|
41
41
|
* across releases.
|
|
42
42
|
*/
|
|
43
|
-
export const AUDIT_VERSION = "0.
|
|
43
|
+
export const AUDIT_VERSION = "0.10.0";
|
|
44
44
|
|
|
45
45
|
/** The audit package's own version. */
|
|
46
46
|
export function auditToolVersion(): string {
|
|
@@ -159,8 +159,9 @@ const WITNESS_VERIFICATION_FAILURE_CODES: ReadonlySet<string> = new Set([
|
|
|
159
159
|
* documented on the ExitFlags type: bit 1 is verification failures
|
|
160
160
|
* (including unsupported-version rejections, and any export with a FALSE
|
|
161
161
|
* claim or an export-shaped file that could not be checked), bit 2 is chain
|
|
162
|
-
* or authority anomalies, divergences between valid proofs,
|
|
163
|
-
* witness verification failures
|
|
162
|
+
* or authority anomalies, divergences between valid proofs, anchor
|
|
163
|
+
* witness verification failures, a failed ceiling, or a floor problem
|
|
164
|
+
* (TemporalAnalysis.floorProblems). artifact-unavailable is never a failure by
|
|
164
165
|
* itself; the attestation stage's results and informational anchor findings
|
|
165
166
|
* never set bits (an export's own attestation claims are part of its
|
|
166
167
|
* verdict); benign ingest findings never set bits.
|
|
@@ -180,8 +181,13 @@ export function computeExitFlags(result: AuditResult): ExitFlags {
|
|
|
180
181
|
// A ceiling file that is present and wrong is failed evidence, like a bad
|
|
181
182
|
// witness: bit 2. Pending, unmatched and status notes are reported, never bits.
|
|
182
183
|
const ceilingFailures = (result.ceilings?.checks ?? []).some((c) => c.status === "failed");
|
|
184
|
+
// A floor problem (a proof signing two floors, a malformed or off-schedule
|
|
185
|
+
// Base floor, a floor header that contradicts or matches no signed floor)
|
|
186
|
+
// is bad evidence too: bit 2, never silently bounded.
|
|
187
|
+
const floorProblems = (result.temporal.floorProblems ?? []).length > 0;
|
|
183
188
|
const chainAnomaliesOrDivergences =
|
|
184
189
|
ceilingFailures ||
|
|
190
|
+
floorProblems ||
|
|
185
191
|
result.anomalies.anomalies.length > 0 ||
|
|
186
192
|
result.anomalies.divergences.length > 0 ||
|
|
187
193
|
result.authorities.anomalies.length > 0 ||
|
package/src/ceilings.ts
CHANGED
|
@@ -74,6 +74,7 @@ export async function verifyCeilings(ingest: IngestResult, options: CeilingAudit
|
|
|
74
74
|
status: "verified",
|
|
75
75
|
...(r.label ? { label: r.label } : {}),
|
|
76
76
|
window: {
|
|
77
|
+
...(w.floor.chain === "base" ? { floorChain: "base" as const } : {}),
|
|
77
78
|
floorBlock: w.floor.blockNumber || null,
|
|
78
79
|
floorTime: w.floor.blockTimestamp,
|
|
79
80
|
ceilingChainId: w.ceiling.chainId,
|
package/src/cli.ts
CHANGED
|
@@ -21,11 +21,12 @@ import { readFileSync } from "node:fs";
|
|
|
21
21
|
import { mkdir, mkdtemp, stat, writeFile } from "node:fs/promises";
|
|
22
22
|
import { tmpdir } from "node:os";
|
|
23
23
|
import { join, basename } from "node:path";
|
|
24
|
-
import { parseCarrier, verifyCarrier } from "@mikeargento/bitgraph-verify";
|
|
25
|
-
import type { VerificationPolicy } from "@mikeargento/bitgraph-verify";
|
|
24
|
+
import { CARRIER_VERSION_3, baseHeaderFields, parseCarrier, verifyCarrier } from "@mikeargento/bitgraph-verify";
|
|
25
|
+
import type { CarrierPayload, VerificationPolicy } from "@mikeargento/bitgraph-verify";
|
|
26
26
|
import { auditToolVersion, computeExitFlags, runAudit } from "./audit.js";
|
|
27
27
|
import { attestationTimestampMs } from "./attestation.js";
|
|
28
28
|
import { exportRunClaims } from "./exports.js";
|
|
29
|
+
import { FLOOR_HEADER_VERSION } from "./ingest.js";
|
|
29
30
|
import { buildJsonReport } from "./report-json.js";
|
|
30
31
|
import { buildMarkdownReport } from "./report-md.js";
|
|
31
32
|
import type { ExitFlags } from "./types.js";
|
|
@@ -62,11 +63,27 @@ function helpText(): string {
|
|
|
62
63
|
USAGE_LINE,
|
|
63
64
|
"",
|
|
64
65
|
"The bundle may be a directory, a .tar archive, a .tar.gz/.tgz,",
|
|
65
|
-
"or a single bitgraph-carrier/1
|
|
66
|
-
"a carrier is unpacked and audited as the
|
|
67
|
-
"own verdict (TRUE / FALSE / UNDETERMINED,
|
|
68
|
-
"printed first.",
|
|
69
|
-
"
|
|
66
|
+
"or a single BitGraphed file (bitgraph-carrier/1, /2 or /3: a file",
|
|
67
|
+
"with its proof inside): a carrier is unpacked and audited as the",
|
|
68
|
+
"bundle it carries, and its own verdict (TRUE / FALSE / UNDETERMINED,",
|
|
69
|
+
"with the time window) is printed first. The unpacked bundle holds the",
|
|
70
|
+
"committed bytes and proof.json, and the floor: for /1 and /2 the",
|
|
71
|
+
"Ethereum anchor proofs and their witnesses under ethereum-anchors/",
|
|
72
|
+
"(anchor-floor.json, anchor-floor.witness.json, and the closing",
|
|
73
|
+
"anchor's pair when the file carries it); for /3 the Base block the",
|
|
74
|
+
"proof signs as its floor, as base-floor/floor-header.json",
|
|
75
|
+
"(bitgraph-floor-header/1: chain, evmChainId, blockNumber, blockHash,",
|
|
76
|
+
"blockTimestamp, and the header's RLP as 0x hex). A /3 file has no",
|
|
77
|
+
"closing anchor: order after the record is the chain of proof hashes.",
|
|
78
|
+
"The audit runs entirely offline: no RPC, no HTTP, no DNS.",
|
|
79
|
+
"",
|
|
80
|
+
"Floors. A proof's floor is the block the enclave signs into it: an",
|
|
81
|
+
"Ethereum anchor (commit.slotAnchor, enclave v7 to v9) or a Base block",
|
|
82
|
+
"(commit.slotFloor, enclave v10). A Base floor bounds its proof not",
|
|
83
|
+
"before the block's time, never after anything. A floor header file",
|
|
84
|
+
"(bitgraph-floor-header/1) anywhere in the bundle is checked against",
|
|
85
|
+
"the floor a proof signs; without one, the time is the signed one and",
|
|
86
|
+
"confirming the block needs a Base lookup.",
|
|
70
87
|
"",
|
|
71
88
|
"Exports (bitgraph-export/1) anywhere in the bundle are found by their",
|
|
72
89
|
"format field. Each is checked with verifyExport once per file in the",
|
|
@@ -122,6 +139,10 @@ function helpText(): string {
|
|
|
122
139
|
" RLP malformation, an invalid candidate anchor, or an unmatched",
|
|
123
140
|
" witness), or a ceiling in time (bitgraph-ceiling/1) that fails",
|
|
124
141
|
" its check against its proof (wrong writer, root, block, header).",
|
|
142
|
+
" Also a floor problem: a proof that signs two floors (an Ethereum",
|
|
143
|
+
" anchor and a Base block), a Base floor that is malformed or off",
|
|
144
|
+
" Base mainnet's schedule, or a floor header file that contradicts",
|
|
145
|
+
" the floor a proof signs or matches none.",
|
|
125
146
|
" A pending ceiling or a ceiling status note never sets a bit.",
|
|
126
147
|
" Benign findings are reported but never set exit bits:",
|
|
127
148
|
" duplicate copies, manifest advisories, unsafe paths, embedded",
|
|
@@ -256,6 +277,45 @@ function exitMeaning(flags: ExitFlags): string {
|
|
|
256
277
|
return parts.join("; ");
|
|
257
278
|
}
|
|
258
279
|
|
|
280
|
+
/**
|
|
281
|
+
* Write a carrier's floor into the unpacked bundle. /1 and /2: the Ethereum
|
|
282
|
+
* anchor proofs and witnesses under ethereum-anchors/, exactly as before.
|
|
283
|
+
* /3: the Base block the proof signs as its floor, as
|
|
284
|
+
* base-floor/floor-header.json (bitgraph-floor-header/1); no anchor proof
|
|
285
|
+
* stands under it and there is no closing anchor to write.
|
|
286
|
+
*/
|
|
287
|
+
async function writeCarrierFloor(dir: string, payload: CarrierPayload): Promise<void> {
|
|
288
|
+
const floor = payload.floor;
|
|
289
|
+
if (floor.basis === "base-header") {
|
|
290
|
+
const fields = baseHeaderFields(floor.header);
|
|
291
|
+
const out = join(dir, "base-floor");
|
|
292
|
+
await mkdir(out);
|
|
293
|
+
await writeFile(
|
|
294
|
+
join(out, "floor-header.json"),
|
|
295
|
+
JSON.stringify(
|
|
296
|
+
{
|
|
297
|
+
version: FLOOR_HEADER_VERSION,
|
|
298
|
+
chain: "base",
|
|
299
|
+
evmChainId: 8453,
|
|
300
|
+
...(fields !== null ? { blockNumber: fields.blockNumber, blockHash: fields.blockHash, blockTimestamp: fields.timestamp } : {}),
|
|
301
|
+
header: floor.header,
|
|
302
|
+
},
|
|
303
|
+
null,
|
|
304
|
+
2
|
|
305
|
+
)
|
|
306
|
+
);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const anchors = join(dir, "ethereum-anchors");
|
|
310
|
+
await mkdir(anchors);
|
|
311
|
+
await writeFile(join(anchors, "anchor-floor.json"), JSON.stringify(floor.anchor, null, 2));
|
|
312
|
+
await writeFile(join(anchors, "anchor-floor.witness.json"), JSON.stringify({ version: "bitgraph-anchor-witness/1", ...floor.witness }, null, 2));
|
|
313
|
+
if (payload.ceiling.status === "present") {
|
|
314
|
+
await writeFile(join(anchors, "anchor-ceiling.json"), JSON.stringify(payload.ceiling.anchor, null, 2));
|
|
315
|
+
await writeFile(join(anchors, "anchor-ceiling.witness.json"), JSON.stringify({ version: "bitgraph-anchor-witness/1", ...payload.ceiling.witness }, null, 2));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
259
319
|
async function main(): Promise<number> {
|
|
260
320
|
let parsed: ParsedArgs | "help";
|
|
261
321
|
try {
|
|
@@ -286,8 +346,8 @@ async function main(): Promise<number> {
|
|
|
286
346
|
}
|
|
287
347
|
|
|
288
348
|
/* A single carrier file: unpack it to a temp bundle (the committed bytes,
|
|
289
|
-
the proof, the floor
|
|
290
|
-
and audit THAT, after printing the carrier's own offline verdict. The
|
|
349
|
+
the proof, and the floor: for /1 and /2 the floor anchor and witness and
|
|
350
|
+
the ceiling pair when present; for /3 the Base floor header) and audit THAT, after printing the carrier's own offline verdict. The
|
|
291
351
|
temp dir is the audit's input, so the reports describe exactly what the
|
|
292
352
|
file carries; everything else about the run is unchanged. Detection is
|
|
293
353
|
from the last 8 bytes only, so no ordinary bundle path changes behaviour. */
|
|
@@ -316,11 +376,17 @@ async function main(): Promise<number> {
|
|
|
316
376
|
((verdict.payload.proof as { environment?: { attestation?: { reportB64?: string } } }).environment?.attestation?.reportB64) ?? ""
|
|
317
377
|
)
|
|
318
378
|
: null;
|
|
379
|
+
// carrier/3 (enclave v10): the floor is a Base block and no closing
|
|
380
|
+
// anchor exists; order after the record is the chain of proof hashes.
|
|
381
|
+
const v3 = verdict.payload?.carrier === CARRIER_VERSION_3 || b?.notBefore.chain === "base";
|
|
382
|
+
const chainWord = b?.notBefore.chain === "base" ? "Base" : "Ethereum";
|
|
319
383
|
const lines = [
|
|
320
384
|
`carrier: ${verdict.verdict}${verdict.carrier === "corrupt" ? " (block unreadable: corrupted, not judged)" : ""}`,
|
|
321
|
-
b ? ` no earlier than: block ${b.notBefore.blockNumber}${b.notBefore.timestamp !== null ? ` (mined ${new Date(b.notBefore.timestamp * 1000).toISOString()})` : ""}` : null,
|
|
385
|
+
b ? ` no earlier than: ${chainWord} block ${b.notBefore.blockNumber}${b.notBefore.timestamp !== null ? ` (${v3 ? "stamped" : "mined"} ${new Date(b.notBefore.timestamp * 1000).toISOString()})` : ""}` : null,
|
|
322
386
|
attestedMs !== null ? ` committed: ${new Date(attestedMs).toISOString()} per the enclave platform's signed clock` : null,
|
|
323
|
-
b
|
|
387
|
+
b && v3
|
|
388
|
+
? " committed before: no ceiling in position (none exists for a Base floor: order after the record is the chain of proof hashes)"
|
|
389
|
+
: b ? ` committed before: ${b.notAfter === null ? "NOT FETCHED (the closing anchor is not inside this file)" : `the anchoring of block ${b.notAfter.blockNumber}${b.notAfter.timestamp !== null ? ` (that block mined ${new Date(b.notAfter.timestamp * 1000).toISOString()})` : ""}`}` : null,
|
|
324
390
|
...verdict.reasons.map((r) => ` - ${r}`),
|
|
325
391
|
].filter((l): l is string => l !== null);
|
|
326
392
|
process.stdout.write(lines.join("\n") + "\n");
|
|
@@ -331,14 +397,7 @@ async function main(): Promise<number> {
|
|
|
331
397
|
const name = basename(parsed.bundlePath).replace(/\.bitgraph(\.[^.]+)$/i, "$1").replace(/\.bitgraph$/i, "") || "artifact";
|
|
332
398
|
await writeFile(join(dir, name), parsedCarrier.inner);
|
|
333
399
|
await writeFile(join(dir, "proof.json"), JSON.stringify(parsedCarrier.payload.proof, null, 2));
|
|
334
|
-
|
|
335
|
-
await mkdir(anchors);
|
|
336
|
-
await writeFile(join(anchors, "anchor-floor.json"), JSON.stringify(parsedCarrier.payload.floor.anchor, null, 2));
|
|
337
|
-
await writeFile(join(anchors, "anchor-floor.witness.json"), JSON.stringify({ version: "bitgraph-anchor-witness/1", ...parsedCarrier.payload.floor.witness }, null, 2));
|
|
338
|
-
if (parsedCarrier.payload.ceiling.status === "present") {
|
|
339
|
-
await writeFile(join(anchors, "anchor-ceiling.json"), JSON.stringify(parsedCarrier.payload.ceiling.anchor, null, 2));
|
|
340
|
-
await writeFile(join(anchors, "anchor-ceiling.witness.json"), JSON.stringify({ version: "bitgraph-anchor-witness/1", ...parsedCarrier.payload.ceiling.witness }, null, 2));
|
|
341
|
-
}
|
|
400
|
+
await writeCarrierFloor(dir, parsedCarrier.payload);
|
|
342
401
|
bundlePath = dir;
|
|
343
402
|
}
|
|
344
403
|
}
|
|
@@ -393,6 +452,7 @@ async function main(): Promise<number> {
|
|
|
393
452
|
for (const line of c.settlement?.lines ?? []) process.stdout.write(` ${line}\n`);
|
|
394
453
|
}
|
|
395
454
|
for (const s of result.ceilings?.statuses ?? []) process.stdout.write(`ceiling ${s.status} ${s.path}: ${s.note}\n`);
|
|
455
|
+
for (const p of result.temporal.floorProblems ?? []) process.stdout.write(`floor PROBLEM ${p.code}${p.path !== undefined ? ` ${p.path}` : ""}: ${p.message}\n`);
|
|
396
456
|
for (const e of result.exports?.checks ?? []) {
|
|
397
457
|
if (e.status !== "checked") {
|
|
398
458
|
process.stdout.write(`export NOT CHECKED ${e.path}: ${e.reason ?? e.status}\n`);
|
|
@@ -409,7 +469,7 @@ async function main(): Promise<number> {
|
|
|
409
469
|
const c = firstClaims.find((x) => x.id === id);
|
|
410
470
|
return c ? `not established (${c.result}: ${c.detail})` : "not established";
|
|
411
471
|
};
|
|
412
|
-
process.stdout.write(` floor: ${t.floor ? `committed bytes finished after Ethereum block ${t.floor.blockNumber} (mined ${when(t.floor.blockTimestamp)})` : why("floor.header")}\n`);
|
|
472
|
+
process.stdout.write(` floor: ${t.floor ? `committed bytes finished after ${t.floor.chain === "base" ? "Base" : "Ethereum"} block ${t.floor.blockNumber} (${t.floor.chain === "base" ? "stamped" : "mined"} ${when(t.floor.blockTimestamp)})` : why("floor.header")}\n`);
|
|
413
473
|
process.stdout.write(` ceiling on Base: ${t.ceilingBase ? `existed by Base block ${t.ceilingBase.blockNumber} (${when(t.ceilingBase.blockTimestamp)})${t.ceilingBase.provisional ? ", provisional until checked against Base" : ""}` : why("ceiling.base")}\n`);
|
|
414
474
|
process.stdout.write(` ceiling on Ethereum: ${t.ceilingEthereum ? `existed by Ethereum block ${t.ceilingEthereum.blockNumber} (${when(t.ceilingEthereum.blockTimestamp)})` : why("ceiling.ethereum")}\n`);
|
|
415
475
|
}
|
package/src/exports.ts
CHANGED
|
@@ -24,8 +24,9 @@
|
|
|
24
24
|
* about its file) with their places in verifyExport's order, and
|
|
25
25
|
* exportRunClaims puts a run's whole list back together. The three time
|
|
26
26
|
* claims are kept apart, never merged: the floor (the committed bytes were
|
|
27
|
-
* finished after
|
|
28
|
-
*
|
|
27
|
+
* finished after the block the proof signs: Ethereum block N for an anchor
|
|
28
|
+
* floor, Base block N for a Base floor, enclave v10; an as-is leaf has no
|
|
29
|
+
* floor), the ceiling on Base (the record existed by Base block B, provisional until B is
|
|
29
30
|
* checked against Base) and the ceiling on Ethereum (the record existed by
|
|
30
31
|
* Ethereum block H). An export with any FALSE claim fails the audit (exit
|
|
31
32
|
* bit 1), as a bad proof does; its attestation claims count, because an
|
|
@@ -394,7 +395,10 @@ function claimKey(c: ExportClaimRecord): string {
|
|
|
394
395
|
|
|
395
396
|
function copyTimes(t: ExportVerifyResult["times"]): ExportTimes {
|
|
396
397
|
return {
|
|
397
|
-
floor:
|
|
398
|
+
floor:
|
|
399
|
+
t.floor === null
|
|
400
|
+
? null
|
|
401
|
+
: { ...(t.floor.chain === "base" ? { chain: "base" as const } : {}), blockNumber: t.floor.blockNumber, blockHash: t.floor.blockHash, blockTimestamp: t.floor.blockTimestamp },
|
|
398
402
|
ceilingBase:
|
|
399
403
|
t.ceilingBase === null
|
|
400
404
|
? null
|