@mikeargento/bitgraph-audit 0.6.4 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit.d.ts +1 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +9 -2
- package/dist/audit.js.map +1 -1
- package/dist/ceilings.d.ts +41 -0
- package/dist/ceilings.d.ts.map +1 -0
- package/dist/ceilings.js +198 -0
- package/dist/ceilings.js.map +1 -0
- package/dist/cli.js +48 -4
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +4 -0
- 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.map +1 -1
- package/dist/ingest.js +17 -0
- package/dist/ingest.js.map +1 -1
- package/dist/report-json.d.ts.map +1 -1
- package/dist/report-json.js +3 -0
- package/dist/report-json.js.map +1 -1
- package/dist/report-md.js +24 -0
- package/dist/report-md.js.map +1 -1
- package/dist/settlement-blobs.d.ts +131 -0
- package/dist/settlement-blobs.d.ts.map +1 -0
- package/dist/settlement-blobs.js +682 -0
- package/dist/settlement-blobs.js.map +1 -0
- package/dist/types.d.ts +96 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -3
- package/src/__tests__/fixtures/settlement/0x018efc046e94610e2530344caf1574e507a3182e1a975fb25b032ec1434a3f5c.bin +0 -0
- package/src/__tests__/fixtures/settlement/ceiling-51979918.json +101 -0
- package/src/__tests__/fixtures/settlement/pointer-51979918.json +69 -0
- package/src/__tests__/settlement.test.ts +254 -0
- package/src/audit.ts +8 -1
- package/src/ceilings.ts +219 -0
- package/src/cli.ts +47 -7
- package/src/index.ts +6 -0
- package/src/ingest.ts +19 -0
- package/src/report-json.ts +3 -0
- package/src/report-md.ts +22 -0
- package/src/settlement-blobs.ts +689 -0
- package/src/types.ts +85 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026 Argento Computing Inc.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The blob layer of a ceiling's settlement on Ethereum, on REAL data.
|
|
5
|
+
*
|
|
6
|
+
* Base block 51,979,918 holds the ceiling transaction 0x72cc…9450. Its batch
|
|
7
|
+
* went to Ethereum block 26,088,463 in channel 0x42b5…f4b5: 7 frames in 7
|
|
8
|
+
* blobs across two batcher transactions (0xcb57…a63a carries frames 0-5,
|
|
9
|
+
* 0x6165…0190 frame 6). The bundle carries ONE blob, frame 0 (131,072
|
|
10
|
+
* bytes): the prototype showed that frame 0 alone decompresses to 577,321
|
|
11
|
+
* bytes, past the ceiling transaction at offset 501,594, so the transaction
|
|
12
|
+
* is located from that one blob while the batch holding it is cut (it ends
|
|
13
|
+
* at 650,075). Decoding the whole channel needs all 7 blobs, which the repo
|
|
14
|
+
* does not carry. Nothing here touches a network.
|
|
15
|
+
*
|
|
16
|
+
* Run, after `npm run build` in packages/audit:
|
|
17
|
+
* node --test packages/audit/src/__tests__/settlement.test.ts
|
|
18
|
+
*/
|
|
19
|
+
import { test, describe, before, after } from "node:test";
|
|
20
|
+
import assert from "node:assert/strict";
|
|
21
|
+
import { readFileSync } from "node:fs";
|
|
22
|
+
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
|
|
23
|
+
import { tmpdir } from "node:os";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import {
|
|
26
|
+
verifySettlementBlobs, decodeOpBlob, parseFrames, decodeSpanBatch, verifyCeilings, ingestEntries, runAudit, computeExitFlags,
|
|
27
|
+
buildMarkdownReport, BASE_MAINNET_ROLLUP, BLOB_BYTES,
|
|
28
|
+
} from "@mikeargento/bitgraph-audit";
|
|
29
|
+
import type { SettlementBlobsOptions } from "@mikeargento/bitgraph-audit";
|
|
30
|
+
import {
|
|
31
|
+
verifySettlementPointer, BASE_MAINNET_SETTLEMENT_PINS, evmHexToBytes as hexToBytes, evmBytesToHex as bytesToHex, keccak256, rlpEncode,
|
|
32
|
+
type SettlementPointer,
|
|
33
|
+
} from "@mikeargento/bitgraph-verify";
|
|
34
|
+
|
|
35
|
+
const fixtureUrl = (name: string): URL => new URL(`./fixtures/settlement/${name}`, import.meta.url);
|
|
36
|
+
const pointer = JSON.parse(readFileSync(fixtureUrl("pointer-51979918.json"), "utf8")) as SettlementPointer;
|
|
37
|
+
const ceilingText = readFileSync(fixtureUrl("ceiling-51979918.json"), "utf8");
|
|
38
|
+
const ceiling = JSON.parse(ceilingText) as { anchor: { txHash: string; rawTx: string; blockNumber: number; blockHash: string } };
|
|
39
|
+
const VH0 = "0x018efc046e94610e2530344caf1574e507a3182e1a975fb25b032ec1434a3f5c";
|
|
40
|
+
const BLOB_FILE = `${VH0}.bin`;
|
|
41
|
+
const blob0 = new Uint8Array(readFileSync(fixtureUrl(BLOB_FILE)));
|
|
42
|
+
const CHANNEL = "0x42b5f4f4249919699b40a61bc497f4b5";
|
|
43
|
+
const CEILING_TX = "0x72cc33632237a96db749836905e92e45bd5cc4167403bc7970a69f250b3e9450";
|
|
44
|
+
const BASE_PARENT = "0xd2b1d2cece1bd23dcd34da3463ce2aa9b78da0f3234d1a5bd2d6c328b3a597da";
|
|
45
|
+
const clone = <T>(x: T): T => JSON.parse(JSON.stringify(x)) as T;
|
|
46
|
+
const blobs = (bytes: Uint8Array = blob0, vh = VH0): Map<string, Uint8Array> => new Map([[vh, bytes]]);
|
|
47
|
+
const expect: SettlementBlobsOptions["expect"] = { txHash: ceiling.anchor.txHash, rawTx: ceiling.anchor.rawTx, baseParentHash: BASE_PARENT };
|
|
48
|
+
|
|
49
|
+
describe("the real blob (frame 0 of the channel)", () => {
|
|
50
|
+
test("decodes to one frame of the channel, the pointer layer accepts the pointer", () => {
|
|
51
|
+
assert.equal(blob0.length, BLOB_BYTES);
|
|
52
|
+
const data = decodeOpBlob(blob0);
|
|
53
|
+
assert.equal(data.length, 130044);
|
|
54
|
+
const frames = parseFrames(data);
|
|
55
|
+
assert.equal(frames.length, 1);
|
|
56
|
+
assert.deepEqual({ ...frames[0], frameData: frames[0].frameData.length }, { channelId: CHANNEL, frameNumber: 0, frameData: 130020, isLast: false });
|
|
57
|
+
assert.equal(verifySettlementPointer(pointer, BASE_MAINNET_SETTLEMENT_PINS).ok, true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("KZG-verified, decoded as a prefix, the ceiling transaction is located in Base block 51,979,918 at index 38", async () => {
|
|
61
|
+
const r = await verifySettlementBlobs(pointer, blobs(), { expect });
|
|
62
|
+
assert.equal(r.ok, true, r.reason);
|
|
63
|
+
assert.deepEqual(r.checks.map((c) => c.name), ["blob 0", "channel", "decompress", "batches", "block", "transaction"]);
|
|
64
|
+
assert.ok(r.checks.every((c) => c.ok));
|
|
65
|
+
assert.match(r.checks[0].detail!, /KZG commitment matches/);
|
|
66
|
+
assert.deepEqual(r.channel, { id: CHANNEL, framesDecoded: 1, framesTotal: 7, complete: false, compression: "brotli", decodedBytes: 577321, baseBlocks: [51979915, 51979916, 51979917] });
|
|
67
|
+
assert.deepEqual(r.located, {
|
|
68
|
+
baseBlockNumber: 51979918, baseBlockTimestamp: 1790749183, baseParentHash: BASE_PARENT, baseTxIndex: 38, txHash: CEILING_TX,
|
|
69
|
+
rawTx: ceiling.anchor.rawTx, batchComplete: false,
|
|
70
|
+
});
|
|
71
|
+
assert.match(r.checks[5].detail!, /bytes equal the ceiling's rawTx/);
|
|
72
|
+
assert.match(r.checks[4].detail!, /parent 0xd2b1d2ce…97da as the ceiling's header says/);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("without expectations, the pointer's own located.txHash is what gets located", async () => {
|
|
76
|
+
const r = await verifySettlementBlobs(pointer, blobs());
|
|
77
|
+
assert.equal(r.ok, true, r.reason);
|
|
78
|
+
assert.equal(r.located!.baseTxIndex, 38);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("the 4,067,670-byte channel is not decodable whole from one blob, and the audit says so", async () => {
|
|
82
|
+
const p = clone(pointer);
|
|
83
|
+
p.baseBlockNumber = 51979930; // in the channel, past what frame 0 reaches
|
|
84
|
+
const r = await verifySettlementBlobs(p, blobs());
|
|
85
|
+
assert.equal(r.ok, false);
|
|
86
|
+
assert.match(r.reason!, /^block: Base block 51979930 is not among the decoded batches/);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("rejects", () => {
|
|
91
|
+
const rejects = (name: string, mk: () => { p?: SettlementPointer; b?: Map<string, Uint8Array>; o?: SettlementBlobsOptions }, re: RegExp): void => {
|
|
92
|
+
test(name, async () => {
|
|
93
|
+
const { p, b, o } = mk();
|
|
94
|
+
const r = await verifySettlementBlobs(p ?? pointer, b ?? blobs(), o ?? { expect });
|
|
95
|
+
assert.equal(r.ok, false, `accepted: ${name}`);
|
|
96
|
+
assert.match(r.reason!, re);
|
|
97
|
+
assert.equal(r.located, undefined);
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
rejects("a flipped blob byte (KZG)", () => { const x = blob0.slice(); x[5000] ^= 1; return { b: blobs(x) }; }, /^blob 0: .*do not commit to the listed KZG commitment/);
|
|
101
|
+
rejects("a blob of the wrong size", () => ({ b: blobs(blob0.slice(0, 1000)) }), /^blob 0: 1000 bytes, a blob is 131072/);
|
|
102
|
+
rejects("bytes under a versioned hash the pointer does not list", () => ({ b: blobs(blob0, "0x01" + "ab".repeat(31)) }), /^blobs: no bytes were supplied/);
|
|
103
|
+
rejects("no blobs at all", () => ({ b: new Map() }), /^blobs: no bytes/);
|
|
104
|
+
rejects("a pointer naming no blobs", () => { const p = clone(pointer); p.blobs = []; return { p }; }, /^blobs: the pointer names no blobs/);
|
|
105
|
+
rejects("a listed commitment that is not the blob's", () => {
|
|
106
|
+
const p = clone(pointer);
|
|
107
|
+
const c = hexToBytes(p.blobs[0].kzgCommitment); c[20] ^= 1; p.blobs[0].kzgCommitment = bytesToHex(c);
|
|
108
|
+
return { p };
|
|
109
|
+
}, /^blob 0: .*do not commit/);
|
|
110
|
+
rejects("a pointer naming another channel", () => { const p = clone(pointer); p.channel!.id = "0x" + "11".repeat(16); return { p }; }, /^channel: no supplied blob carries a frame of channel/);
|
|
111
|
+
rejects("a pointer claiming zlib", () => { const p = clone(pointer); p.channel!.compression = "zlib"; return { p }; }, /^decompress: the channel is brotli, the pointer says zlib/);
|
|
112
|
+
rejects("a Base block not in the channel", () => { const p = clone(pointer); p.baseBlockNumber = 51979900; return { p }; }, /^block: .*not among/);
|
|
113
|
+
rejects("a parent hash the batch does not carry", () => ({ o: { expect: { ...expect, baseParentHash: "0x" + "ee".repeat(32) } } }), /^block: the batch's parent_hash is/);
|
|
114
|
+
rejects("a ceiling transaction that is not in the batch", () => ({ o: { expect: { txHash: "0x" + "cd".repeat(32) } } }), /^transaction: 0xcdcdcdcd…cdcd is not among the \d+ transactions readable before the cut/);
|
|
115
|
+
rejects("a located index the pointer states wrongly", () => { const p = clone(pointer); p.located!.baseTxIndex = 37; return { p }; }, /^transaction: located at index 38, the pointer says 37/);
|
|
116
|
+
rejects("raw bytes that are not the ceiling's", () => ({ o: { expect: { txHash: CEILING_TX, rawTx: "0x02c0" } } }), /^transaction: the bytes in the batch are not the ceiling's rawTx/);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("through the audit's ceiling check", () => {
|
|
120
|
+
const entries = (files: Record<string, string | Uint8Array>) =>
|
|
121
|
+
ingestEntries(Object.entries(files).map(([path, v]) => ({ path, open: () => (typeof v === "string" ? new TextEncoder().encode(v) : v) })));
|
|
122
|
+
|
|
123
|
+
test("a ceiling with a pointer and no blob bytes: pointer ok, blobs not in bundle, never a failure", async () => {
|
|
124
|
+
const ingest = await entries({ "base-ceiling/ceiling.json": ceilingText });
|
|
125
|
+
const r = await verifyCeilings(ingest);
|
|
126
|
+
const c = r.checks[0];
|
|
127
|
+
assert.equal(c.status, "unmatched"); // the proof is not in this bundle; the settlement is checked all the same
|
|
128
|
+
assert.equal(c.settlement!.status, "pointer-only");
|
|
129
|
+
assert.deepEqual(c.settlement!.lines, [
|
|
130
|
+
"settlement: pointer ok, Ethereum block 26088463 at 2026-09-30 06:20:35 UTC",
|
|
131
|
+
"settlement: blobs not in bundle",
|
|
132
|
+
]);
|
|
133
|
+
assert.deepEqual(c.settlement!.pointer.existedBy, { blockNumber: 26088463, blockHash: "0x8608654cd9d6d66c2ce8a80108db7e5202a6d9ad0072f8c81353cc988d3a93f2", blockTimestamp: 1790749235 });
|
|
134
|
+
assert.deepEqual(c.settlement!.blobs, { status: "absent", detail: "blobs not in bundle", listed: 6, supplied: 0 });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("the blob next to the ceiling file: both layers verified, one line each", async () => {
|
|
138
|
+
const ingest = await entries({ "base-ceiling/ceiling.json": ceilingText, [`base-ceiling/${BLOB_FILE}`]: blob0 });
|
|
139
|
+
const c = (await verifyCeilings(ingest)).checks[0];
|
|
140
|
+
assert.equal(c.settlement!.status, "verified");
|
|
141
|
+
assert.equal(c.settlement!.lines[1], "settlement: blobs decoded (frames 0..0 of 7), ceiling tx located in Base block 51979918 (batch cut at the last frame supplied)");
|
|
142
|
+
assert.deepEqual(c.settlement!.blobs.located, { baseBlockNumber: 51979918, baseTxIndex: 38, txHash: CEILING_TX, batchComplete: false, framesDecoded: 1, framesTotal: 7 });
|
|
143
|
+
assert.equal(c.settlement!.blobs.supplied, 1);
|
|
144
|
+
assert.equal(c.status, "unmatched");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("the blob under blobs/ at the bundle root is found too", async () => {
|
|
148
|
+
const ingest = await entries({ "base-ceiling/ceiling.json": ceilingText, [`blobs/${BLOB_FILE}`]: blob0 });
|
|
149
|
+
assert.equal((await verifyCeilings(ingest)).checks[0].settlement!.status, "verified");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("a tampered pointer fails the ceiling", async () => {
|
|
153
|
+
const s = JSON.parse(ceilingText);
|
|
154
|
+
const h = hexToBytes(s.settlement.l1.blockHeader); h[300] ^= 1; s.settlement.l1.blockHeader = bytesToHex(h);
|
|
155
|
+
const c = (await verifyCeilings(await entries({ "c.json": JSON.stringify(s) }))).checks[0];
|
|
156
|
+
assert.equal(c.status, "failed");
|
|
157
|
+
assert.match(c.reason!, /^settlement: pointer FAILED \(header: /);
|
|
158
|
+
assert.equal(c.settlement!.status, "failed");
|
|
159
|
+
assert.equal(c.settlement!.blobs.status, "absent");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("a pointer for another Base block than the ceiling's fails the ceiling", async () => {
|
|
163
|
+
const s = JSON.parse(ceilingText);
|
|
164
|
+
s.anchor.blockNumber = 51967531;
|
|
165
|
+
const c = (await verifyCeilings(await entries({ "c.json": JSON.stringify(s) }))).checks[0];
|
|
166
|
+
assert.equal(c.status, "failed");
|
|
167
|
+
assert.match(c.reason!, /the pointer names Base block 51979918 .* this ceiling is in block 51967531/);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("a wrong blob in the bundle fails the ceiling", async () => {
|
|
171
|
+
const bad = blob0.slice(); bad[70000] ^= 1;
|
|
172
|
+
const c = (await verifyCeilings(await entries({ "c.json": ceilingText, [BLOB_FILE]: bad }))).checks[0];
|
|
173
|
+
assert.equal(c.status, "failed");
|
|
174
|
+
assert.match(c.reason!, /^settlement: blobs FAILED \(blob 0: /);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("other pins refuse the pointer", async () => {
|
|
178
|
+
const c = (await verifyCeilings(await entries({ "c.json": ceilingText }), { settlementPins: { ...BASE_MAINNET_SETTLEMENT_PINS, batcher: "0x" + "11".repeat(20) } })).checks[0];
|
|
179
|
+
assert.equal(c.status, "failed");
|
|
180
|
+
assert.match(c.reason!, /pointer FAILED \(batcher:/);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("through runAudit on a directory bundle", () => {
|
|
185
|
+
let dir: string;
|
|
186
|
+
before(async () => { dir = await mkdtemp(join(tmpdir(), "audit-settlement-")); });
|
|
187
|
+
after(async () => { await rm(dir, { recursive: true, force: true }); });
|
|
188
|
+
|
|
189
|
+
test("exit 0, and the markdown report carries both settlement lines", async () => {
|
|
190
|
+
const b = join(dir, "ok");
|
|
191
|
+
await mkdir(join(b, "base-ceiling", "blobs"), { recursive: true });
|
|
192
|
+
await writeFile(join(b, "base-ceiling", "ceiling.json"), ceilingText);
|
|
193
|
+
await writeFile(join(b, "base-ceiling", "blobs", BLOB_FILE), blob0);
|
|
194
|
+
const r = await runAudit(b);
|
|
195
|
+
const c = r.ceilings!.checks[0];
|
|
196
|
+
assert.equal(c.settlement!.status, "verified");
|
|
197
|
+
assert.equal(computeExitFlags(r).code, 0);
|
|
198
|
+
const md = buildMarkdownReport(r);
|
|
199
|
+
assert.match(md, / - settlement: pointer ok, Ethereum block 26088463 at 2026-09-30 06:20:35 UTC/);
|
|
200
|
+
assert.match(md, / - settlement: blobs decoded \(frames 0\.\.0 of 7\), ceiling tx located in Base block 51979918/);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("a bad pointer sets exit bit 2", async () => {
|
|
204
|
+
const b = join(dir, "bad");
|
|
205
|
+
await mkdir(b, { recursive: true });
|
|
206
|
+
const s = JSON.parse(ceilingText);
|
|
207
|
+
s.settlement.l1.txIndex = 19;
|
|
208
|
+
await writeFile(join(b, "ceiling.json"), JSON.stringify(s));
|
|
209
|
+
const r = await runAudit(b);
|
|
210
|
+
assert.equal(r.ceilings!.checks[0].status, "failed");
|
|
211
|
+
assert.equal(computeExitFlags(r).code & 2, 2);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe("span batches (type 1), synthetic", () => {
|
|
216
|
+
const uvarint = (n: bigint): number[] => { const out: number[] = []; for (;;) { const b = Number(n & 0x7fn); n >>= 7n; if (n === 0n) { out.push(b); return out; } out.push(b | 0x80); } };
|
|
217
|
+
const bitsBytes = (bits: number[]): number[] => { let v = 0n; bits.forEach((b, i) => { if (b) v |= 1n << BigInt(i); }); const n = Math.ceil(bits.length / 8); const out = new Array<number>(n).fill(0); for (let i = n - 1; i >= 0; i--) { out[i] = Number(v & 0xffn); v >>= 8n; } return out; };
|
|
218
|
+
const u = (n: bigint): Uint8Array => { if (n === 0n) return new Uint8Array(0); let h = n.toString(16); if (h.length % 2) h = "0" + h; return hexToBytes(h); };
|
|
219
|
+
|
|
220
|
+
test("a block with a type-2 and a protected legacy transaction is rebuilt into the same raw transactions", () => {
|
|
221
|
+
const chainId = 8453n;
|
|
222
|
+
const to = hexToBytes("0x" + "ab".repeat(20));
|
|
223
|
+
const r1 = hexToBytes("0x" + "11".repeat(32)), s1 = hexToBytes("0x" + "22".repeat(32));
|
|
224
|
+
const r2 = hexToBytes("0x" + "33".repeat(32)), s2 = hexToBytes("0x" + "44".repeat(32));
|
|
225
|
+
const input = hexToBytes("0xdeadbeef");
|
|
226
|
+
// The raw transactions a Base block would carry.
|
|
227
|
+
const type2 = new Uint8Array([0x02, ...rlpEncode([u(chainId), u(7n), u(1000n), u(2000n), u(21000n), to, u(5n), input, [], u(1n), r1, s1])]);
|
|
228
|
+
const legacy = rlpEncode([u(8n), u(3000n), u(50000n), to, u(0n), new Uint8Array(0), u(chainId * 2n + 35n + 0n), r2, s2]);
|
|
229
|
+
// The span batch: prefix, then columns.
|
|
230
|
+
const relTimestamp = 2n * 1000n;
|
|
231
|
+
const bytes = Uint8Array.from([
|
|
232
|
+
...uvarint(relTimestamp), ...uvarint(123n), ...new Array(20).fill(1), ...new Array(20).fill(2),
|
|
233
|
+
...uvarint(1n), ...bitsBytes([0]), ...uvarint(2n),
|
|
234
|
+
...bitsBytes([0, 0]), ...bitsBytes([1, 0]),
|
|
235
|
+
...r1, ...s1, ...r2, ...s2,
|
|
236
|
+
...to, ...to,
|
|
237
|
+
0x02, ...rlpEncode([u(5n), u(1000n), u(2000n), input, []]),
|
|
238
|
+
...rlpEncode([u(0n), u(3000n), new Uint8Array(0)]),
|
|
239
|
+
...uvarint(7n), ...uvarint(8n),
|
|
240
|
+
...uvarint(21000n), ...uvarint(50000n),
|
|
241
|
+
...bitsBytes([1]),
|
|
242
|
+
]);
|
|
243
|
+
const blocks = decodeSpanBatch(bytes, BASE_MAINNET_ROLLUP, chainId);
|
|
244
|
+
assert.equal(blocks.length, 1);
|
|
245
|
+
assert.equal(blocks[0].number, 1000);
|
|
246
|
+
assert.equal(blocks[0].timestamp, BASE_MAINNET_ROLLUP.l2GenesisTime + 2000);
|
|
247
|
+
assert.deepEqual(blocks[0].txs.map((t) => t.hash), [type2, legacy].map((raw) => bytesToHex(keccak256(raw))));
|
|
248
|
+
assert.deepEqual(blocks[0].txs.map((t) => bytesToHex(t.raw)), [type2, legacy].map(bytesToHex));
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("trailing bytes are refused", () => {
|
|
252
|
+
assert.throws(() => decodeSpanBatch(Uint8Array.from([...[0, 0], ...new Array(40).fill(0), 1, 0, 0, 0, 0, 0, 0, 0, 0]), BASE_MAINNET_ROLLUP, 8453n), /span batch/);
|
|
253
|
+
});
|
|
254
|
+
});
|
package/src/audit.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* Zero network access, as everywhere in this package.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
import { verifyCeilings } from "./ceilings.js";
|
|
20
21
|
import { ingestBundle } from "./ingest.js";
|
|
21
22
|
import { verifyObservedProofs } from "./verify-tiers.js";
|
|
22
23
|
import { reconstructChains } from "./reconstruct.js";
|
|
@@ -37,7 +38,7 @@ import type { AuditOptions, AuditResult, ExitFlags, IngestResult } from "./types
|
|
|
37
38
|
* this equals package.json's version, so the constant cannot drift silently
|
|
38
39
|
* across releases.
|
|
39
40
|
*/
|
|
40
|
-
export const AUDIT_VERSION = "0.
|
|
41
|
+
export const AUDIT_VERSION = "0.8.0";
|
|
41
42
|
|
|
42
43
|
/** The audit package's own version. */
|
|
43
44
|
export function auditToolVersion(): string {
|
|
@@ -80,6 +81,7 @@ export async function auditIngest(
|
|
|
80
81
|
const witnesses = await verifyAnchorWitnesses(ingest, anchors);
|
|
81
82
|
// Populates EpochRecord.anchorBounds on the reconstruction result.
|
|
82
83
|
const temporal = deriveTemporalBounds(ingest, reconstruction, anchors, witnesses);
|
|
84
|
+
const ceilings = await verifyCeilings(ingest, options?.ceilings ?? {});
|
|
83
85
|
const attestations = await validateAttestations(
|
|
84
86
|
ingest,
|
|
85
87
|
authorities,
|
|
@@ -104,6 +106,7 @@ export async function auditIngest(
|
|
|
104
106
|
witnesses,
|
|
105
107
|
temporal,
|
|
106
108
|
attestations,
|
|
109
|
+
ceilings,
|
|
107
110
|
};
|
|
108
111
|
}
|
|
109
112
|
|
|
@@ -152,7 +155,11 @@ export function computeExitFlags(result: AuditResult): ExitFlags {
|
|
|
152
155
|
const witnessVerificationFailures = result.witnesses.findings.some((f) =>
|
|
153
156
|
WITNESS_VERIFICATION_FAILURE_CODES.has(f.code)
|
|
154
157
|
);
|
|
158
|
+
// A ceiling file that is present and wrong is failed evidence, like a bad
|
|
159
|
+
// witness: bit 2. Pending, unmatched and status notes are reported, never bits.
|
|
160
|
+
const ceilingFailures = (result.ceilings?.checks ?? []).some((c) => c.status === "failed");
|
|
155
161
|
const chainAnomaliesOrDivergences =
|
|
162
|
+
ceilingFailures ||
|
|
156
163
|
result.anomalies.anomalies.length > 0 ||
|
|
157
164
|
result.anomalies.divergences.length > 0 ||
|
|
158
165
|
result.authorities.anomalies.length > 0 ||
|
package/src/ceilings.ts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026 Argento Computing Inc. Licensed under the MIT License. See LICENSE.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ceilings in time (bitgraph-ceiling/1): each ceiling file in the bundle is
|
|
5
|
+
* matched to its proof by proofHash and checked with verifyCeiling, offline.
|
|
6
|
+
* The one thing offline cannot say is whether the carried Base header is on
|
|
7
|
+
* Base's chain. The audit reports that as unchecked; `bitgraph ceiling
|
|
8
|
+
* verify --rpc` or any Base explorer answers it.
|
|
9
|
+
*
|
|
10
|
+
* The writer address is a trust input, like a measurement allowlist: the
|
|
11
|
+
* default is the address BitGraph publishes, and a reader can pass another.
|
|
12
|
+
*
|
|
13
|
+
* A ceiling file may carry its SETTLEMENT on Ethereum (bitgraph-settlement/1,
|
|
14
|
+
* the `settlement` field): the batcher transaction whose blobs hold the Base
|
|
15
|
+
* block. Two layers, both offline, reported as separate lines: the POINTER
|
|
16
|
+
* (verifySettlementPointer, against the Base mainnet pins or ones the reader
|
|
17
|
+
* passes) and the BLOBS (blob bytes kept in the bundle next to the ceiling
|
|
18
|
+
* file or under a `blobs/` folder, verified against the listed KZG
|
|
19
|
+
* commitments and decoded down to the ceiling transaction). Blob bytes
|
|
20
|
+
* absent from the bundle are reported, never a failure; a pointer or blob
|
|
21
|
+
* that is present and wrong fails the ceiling like any other bad evidence.
|
|
22
|
+
* The settlement is checked whether or not the ceiling's proof is in the
|
|
23
|
+
* bundle: it is evidence about the ceiling file itself.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
verifyCeiling, checkCeilingOnline, verifySettlementPointer, BASE_MAINNET_SETTLEMENT_PINS, decodeHeader, evmHexToBytes,
|
|
28
|
+
type CeilingSidecar, type SettlementPointer, type SettlementPins,
|
|
29
|
+
} from "@mikeargento/bitgraph-verify";
|
|
30
|
+
import { streamArtifactsByHash } from "./ingest.js";
|
|
31
|
+
import { verifySettlementBlobs } from "./settlement-blobs.js";
|
|
32
|
+
import type { CeilingAnalysis, CeilingCheck, CeilingSettlementCheck, IngestResult } from "./types.js";
|
|
33
|
+
|
|
34
|
+
/** BitGraph's published ceiling writer on Base mainnet (bitgraph.ing/ceilings). */
|
|
35
|
+
export const BITGRAPH_CEILING_WRITER = "0xf3972408D853c975F86351C311f4310220bbF2a3";
|
|
36
|
+
export const BASE_MAINNET_CHAIN_ID = 8453;
|
|
37
|
+
|
|
38
|
+
export interface CeilingAuditOptions {
|
|
39
|
+
writer?: string;
|
|
40
|
+
chainId?: number;
|
|
41
|
+
/**
|
|
42
|
+
* The one online question, supplied by an embedder that allows network:
|
|
43
|
+
* the Base chain's block hash at a height. The audit itself never makes a
|
|
44
|
+
* network call (its zero-network guarantee), so the CLI never sets this.
|
|
45
|
+
*/
|
|
46
|
+
getBlockHash?: (blockNumber: number) => Promise<string | null>;
|
|
47
|
+
/** The batch inbox and batcher a settlement pointer must name. Base mainnet's by default. */
|
|
48
|
+
settlementPins?: SettlementPins;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function verifyCeilings(ingest: IngestResult, options: CeilingAuditOptions = {}): Promise<CeilingAnalysis> {
|
|
52
|
+
const writer = options.writer ?? BITGRAPH_CEILING_WRITER;
|
|
53
|
+
const chainId = options.chainId ?? BASE_MAINNET_CHAIN_ID;
|
|
54
|
+
const pins = options.settlementPins ?? BASE_MAINNET_SETTLEMENT_PINS;
|
|
55
|
+
const byHash = new Map(ingest.proofs.map((p) => [p.proofHash, p]));
|
|
56
|
+
const checks: CeilingCheck[] = [];
|
|
57
|
+
for (const f of ingest.ceilings ?? []) {
|
|
58
|
+
const side = f.json as unknown as CeilingSidecar;
|
|
59
|
+
const proofHash = typeof side.proofHash === "string" ? side.proofHash : null;
|
|
60
|
+
const observed = proofHash ? byHash.get(proofHash) : undefined;
|
|
61
|
+
let check: CeilingCheck;
|
|
62
|
+
if (!observed) {
|
|
63
|
+
check = { path: f.path, proofHash, status: "unmatched", reason: "no proof in this bundle has this ceiling's proofHash", onChain: null };
|
|
64
|
+
} else {
|
|
65
|
+
const r = await verifyCeiling(observed.proof, side, { writerAddress: writer, chainId });
|
|
66
|
+
if (!r.ok) {
|
|
67
|
+
const pending = r.status === "pending";
|
|
68
|
+
check = { path: f.path, proofHash, status: pending ? "pending" : "failed", ...(r.reason ? { reason: r.reason } : {}), onChain: null };
|
|
69
|
+
} else {
|
|
70
|
+
const w = r.window!;
|
|
71
|
+
check = {
|
|
72
|
+
path: f.path,
|
|
73
|
+
proofHash,
|
|
74
|
+
status: "verified",
|
|
75
|
+
...(r.label ? { label: r.label } : {}),
|
|
76
|
+
window: {
|
|
77
|
+
floorBlock: w.floor.blockNumber || null,
|
|
78
|
+
floorTime: w.floor.blockTimestamp,
|
|
79
|
+
ceilingChainId: w.ceiling.chainId,
|
|
80
|
+
ceilingBlock: w.ceiling.blockNumber,
|
|
81
|
+
ceilingTime: w.ceiling.blockTimestamp,
|
|
82
|
+
widthSeconds: w.widthSeconds,
|
|
83
|
+
},
|
|
84
|
+
onChain: null,
|
|
85
|
+
onChainDetail: "header not checked against chain",
|
|
86
|
+
};
|
|
87
|
+
if (options.getBlockHash) {
|
|
88
|
+
const online = await checkCeilingOnline(side, options.getBlockHash);
|
|
89
|
+
check.onChain = online.onChain;
|
|
90
|
+
check.onChainDetail = online.detail;
|
|
91
|
+
if (online.onChain === false) {
|
|
92
|
+
check.status = "failed";
|
|
93
|
+
check.reason = `chain: ${online.detail}`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const carried = f.json["settlement"];
|
|
99
|
+
if (carried !== null && carried !== undefined) {
|
|
100
|
+
const settlement = await auditSettlement(f.path, side, carried, ingest, pins);
|
|
101
|
+
check.settlement = settlement;
|
|
102
|
+
if (settlement.status === "failed" && check.status !== "failed") {
|
|
103
|
+
check.status = "failed";
|
|
104
|
+
check.reason = settlement.lines.find((l) => l.includes("FAILED")) ?? "settlement: failed";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
checks.push(check);
|
|
108
|
+
}
|
|
109
|
+
const statuses = (ingest.ceilingStatuses ?? []).map((f) => ({
|
|
110
|
+
path: f.path,
|
|
111
|
+
status: String(f.json["status"] ?? "unknown"),
|
|
112
|
+
note: String(f.json["note"] ?? ""),
|
|
113
|
+
}));
|
|
114
|
+
return { writer, chainId, checks, statuses };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── settlement ─────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
function whenUtc(unix: number): string {
|
|
120
|
+
const iso = new Date(unix * 1000).toISOString();
|
|
121
|
+
return `${iso.slice(0, 10)} ${iso.slice(11, 19)} UTC`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The bundle paths a blob file named `file` may sit at, next to the ceiling file first. */
|
|
125
|
+
function blobCandidatePaths(ceilingPath: string, file: string): string[] {
|
|
126
|
+
const slash = ceilingPath.lastIndexOf("/");
|
|
127
|
+
const dir = slash < 0 ? "" : ceilingPath.slice(0, slash + 1);
|
|
128
|
+
return [`${dir}${file}`, `${dir}blobs/${file}`, `blobs/${file}`, file];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function auditSettlement(
|
|
132
|
+
path: string,
|
|
133
|
+
side: CeilingSidecar,
|
|
134
|
+
carried: unknown,
|
|
135
|
+
ingest: IngestResult,
|
|
136
|
+
pins: SettlementPins,
|
|
137
|
+
): Promise<CeilingSettlementCheck> {
|
|
138
|
+
const lines: string[] = [];
|
|
139
|
+
const failed = (pointer: CeilingSettlementCheck["pointer"], blobs: CeilingSettlementCheck["blobs"]): CeilingSettlementCheck =>
|
|
140
|
+
({ status: "failed", pointer, blobs, lines });
|
|
141
|
+
const noBlobs = (detail: string, listed: number): CeilingSettlementCheck["blobs"] => ({ status: "absent", detail, listed, supplied: 0 });
|
|
142
|
+
|
|
143
|
+
// 1. The pointer: header, inclusion, batcher, blob commitments.
|
|
144
|
+
const pointer = carried as SettlementPointer;
|
|
145
|
+
const p = verifySettlementPointer(pointer, pins);
|
|
146
|
+
const listed = Array.isArray(pointer?.blobs) ? pointer.blobs.length : 0;
|
|
147
|
+
if (!p.ok) {
|
|
148
|
+
lines.push(`settlement: pointer FAILED (${p.reason ?? "invalid"})`);
|
|
149
|
+
return failed({ ok: false, reason: p.reason ?? "invalid" }, noBlobs("not checked: the pointer failed", listed));
|
|
150
|
+
}
|
|
151
|
+
const existedBy = p.existedBy!;
|
|
152
|
+
// The pointer must be about THIS ceiling's Base block and transaction.
|
|
153
|
+
const anchor = side.anchor;
|
|
154
|
+
let mismatch: string | null = null;
|
|
155
|
+
if (!anchor) mismatch = "the ceiling has no Base transaction yet";
|
|
156
|
+
else if (pointer.baseBlockNumber !== anchor.blockNumber || pointer.baseBlockHash.toLowerCase() !== anchor.blockHash.toLowerCase()) {
|
|
157
|
+
mismatch = `the pointer names Base block ${pointer.baseBlockNumber} (${pointer.baseBlockHash.slice(0, 10)}…), this ceiling is in block ${anchor.blockNumber} (${anchor.blockHash.slice(0, 10)}…)`;
|
|
158
|
+
} else if (pointer.located && pointer.located.txHash.toLowerCase() !== anchor.txHash.toLowerCase()) {
|
|
159
|
+
mismatch = `the pointer locates transaction ${pointer.located.txHash.slice(0, 10)}…, this ceiling's is ${anchor.txHash.slice(0, 10)}…`;
|
|
160
|
+
}
|
|
161
|
+
if (mismatch) {
|
|
162
|
+
lines.push(`settlement: pointer FAILED (${mismatch})`);
|
|
163
|
+
return failed({ ok: false, reason: mismatch, existedBy }, noBlobs("not checked: the pointer is for another block", listed));
|
|
164
|
+
}
|
|
165
|
+
lines.push(`settlement: pointer ok, Ethereum block ${existedBy.blockNumber} at ${whenUtc(existedBy.blockTimestamp)}`);
|
|
166
|
+
const pointerOk: CeilingSettlementCheck["pointer"] = { ok: true, existedBy };
|
|
167
|
+
|
|
168
|
+
// 2. The blobs, when the bundle holds their bytes.
|
|
169
|
+
const wanted = new Map<string, string>(); // sha256 hex -> versioned hash
|
|
170
|
+
const byPath = new Map<string, string>(); // bundle path -> sha256 hex
|
|
171
|
+
for (const a of ingest.artifacts) for (const ap of a.paths) byPath.set(ap, a.sha256Hex);
|
|
172
|
+
const byBasename = new Map<string, string>();
|
|
173
|
+
for (const a of ingest.artifacts) for (const ap of a.paths) byBasename.set(ap.slice(ap.lastIndexOf("/") + 1), a.sha256Hex);
|
|
174
|
+
for (const b of pointer.blobs) {
|
|
175
|
+
const file = typeof b.file === "string" ? b.file : `${b.versionedHash.toLowerCase()}.bin`;
|
|
176
|
+
if (file.includes("/") || file.includes("\\") || file.includes("..")) continue;
|
|
177
|
+
const hex = blobCandidatePaths(path, file).map((c) => byPath.get(c)).find((h) => h !== undefined) ?? byBasename.get(file);
|
|
178
|
+
if (hex) wanted.set(hex, b.versionedHash.toLowerCase());
|
|
179
|
+
}
|
|
180
|
+
if (wanted.size === 0) {
|
|
181
|
+
lines.push("settlement: blobs not in bundle");
|
|
182
|
+
return { status: "pointer-only", pointer: pointerOk, blobs: noBlobs("blobs not in bundle", listed), lines };
|
|
183
|
+
}
|
|
184
|
+
const bytesByHash = new Map<string, Uint8Array>();
|
|
185
|
+
for await (const m of streamArtifactsByHash(ingest, wanted.keys())) bytesByHash.set(wanted.get(m.sha256Hex)!, m.bytes);
|
|
186
|
+
let expectParent: string | undefined;
|
|
187
|
+
try {
|
|
188
|
+
expectParent = anchor ? decodeHeader(evmHexToBytes(anchor.blockHeader)).parentHash : undefined;
|
|
189
|
+
} catch {
|
|
190
|
+
expectParent = undefined;
|
|
191
|
+
}
|
|
192
|
+
const r = await verifySettlementBlobs(pointer, bytesByHash, {
|
|
193
|
+
expect: {
|
|
194
|
+
txHash: anchor!.txHash,
|
|
195
|
+
rawTx: anchor!.rawTx,
|
|
196
|
+
...(expectParent ? { baseParentHash: expectParent } : {}),
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
if (!r.ok) {
|
|
200
|
+
lines.push(`settlement: blobs FAILED (${r.reason ?? "invalid"})`);
|
|
201
|
+
return failed(pointerOk, { status: "failed", detail: r.reason ?? "invalid", listed, supplied: bytesByHash.size });
|
|
202
|
+
}
|
|
203
|
+
const c = r.channel!;
|
|
204
|
+
const frames = c.complete ? `all ${c.framesDecoded} frames` : `frames 0..${c.framesDecoded - 1}${c.framesTotal ? ` of ${c.framesTotal}` : ""}`;
|
|
205
|
+
const loc = r.located!;
|
|
206
|
+
lines.push(`settlement: blobs decoded (${frames}), ceiling tx located in Base block ${loc.baseBlockNumber}${loc.batchComplete ? "" : " (batch cut at the last frame supplied)"}`);
|
|
207
|
+
return {
|
|
208
|
+
status: "verified",
|
|
209
|
+
pointer: pointerOk,
|
|
210
|
+
blobs: {
|
|
211
|
+
status: "verified",
|
|
212
|
+
detail: `${bytesByHash.size} of ${listed} blobs supplied; ${frames} decoded; ${r.checks.map((k) => k.detail ?? k.name).join("; ")}`,
|
|
213
|
+
listed,
|
|
214
|
+
supplied: bytesByHash.size,
|
|
215
|
+
located: { baseBlockNumber: loc.baseBlockNumber, baseTxIndex: loc.baseTxIndex, txHash: loc.txHash, batchComplete: loc.batchComplete, framesDecoded: c.framesDecoded, framesTotal: c.framesTotal },
|
|
216
|
+
},
|
|
217
|
+
lines,
|
|
218
|
+
};
|
|
219
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -52,7 +52,7 @@ const KNOWN_POLICY_FIELDS: readonly string[] = [
|
|
|
52
52
|
class UsageError extends Error {}
|
|
53
53
|
|
|
54
54
|
const USAGE_LINE =
|
|
55
|
-
"Usage: bitgraph-audit <path-to-bundle> [--out <dir>] [--format json,md] [--trust-policy <path>]";
|
|
55
|
+
"Usage: bitgraph-audit <path-to-bundle> [--out <dir>] [--format json,md] [--trust-policy <path>] [--ceiling-writer <0x...>] [--ceiling-chain <id>]";
|
|
56
56
|
|
|
57
57
|
function helpText(): string {
|
|
58
58
|
return [
|
|
@@ -78,6 +78,14 @@ function helpText(): string {
|
|
|
78
78
|
" verification tiers. Valid fields:",
|
|
79
79
|
` ${KNOWN_POLICY_FIELDS.join(", ")}.`,
|
|
80
80
|
" Any other field is an error.",
|
|
81
|
+
" --ceiling-writer <0x…> The address ceilings in time must come from",
|
|
82
|
+
" (default: BitGraph's published writer,",
|
|
83
|
+
" 0xf3972408D853c975F86351C311f4310220bbF2a3).",
|
|
84
|
+
" --ceiling-chain <id> Base chain id for ceilings (default 8453).",
|
|
85
|
+
" The audit never goes online, so whether each",
|
|
86
|
+
" ceiling's Base block is on Base's chain is",
|
|
87
|
+
" reported as unchecked: confirm it on basescan.org",
|
|
88
|
+
" or with `bitgraph ceiling verify --rpc <url>`.",
|
|
81
89
|
" --help, -h Print this help and exit 0.",
|
|
82
90
|
"",
|
|
83
91
|
"Exit codes (bit flags):",
|
|
@@ -98,7 +106,10 @@ function helpText(): string {
|
|
|
98
106
|
" or a supplied anchor witness that fails its offline verification",
|
|
99
107
|
" (block-hash mismatch, digest binding, block number, header or",
|
|
100
108
|
" RLP malformation, an invalid candidate anchor, or an unmatched",
|
|
101
|
-
" witness)
|
|
109
|
+
" witness), or a ceiling in time (bitgraph-ceiling/1) that fails",
|
|
110
|
+
" its check against its proof (wrong writer, root, block, header).",
|
|
111
|
+
" A pending ceiling or a ceiling status note never sets a bit.",
|
|
112
|
+
" Benign findings are reported but never set exit bits:",
|
|
102
113
|
" duplicate copies, manifest advisories, unsafe paths, embedded",
|
|
103
114
|
" proofHash mismatches, and informational anchor findings (unsigned",
|
|
104
115
|
" metadata disagreements, metadata-only anchor claims, and",
|
|
@@ -120,6 +131,8 @@ interface ParsedArgs {
|
|
|
120
131
|
outDir: string;
|
|
121
132
|
formats: ReadonlySet<"json" | "md">;
|
|
122
133
|
trustPolicyPath?: string;
|
|
134
|
+
ceilingWriter?: string;
|
|
135
|
+
ceilingChain?: number;
|
|
123
136
|
}
|
|
124
137
|
|
|
125
138
|
function parseArgs(argv: string[]): ParsedArgs | "help" {
|
|
@@ -127,6 +140,8 @@ function parseArgs(argv: string[]): ParsedArgs | "help" {
|
|
|
127
140
|
let outDir = ".";
|
|
128
141
|
let formats: Set<"json" | "md"> = new Set(["json", "md"]);
|
|
129
142
|
let trustPolicyPath: string | undefined;
|
|
143
|
+
let ceilingWriter: string | undefined;
|
|
144
|
+
let ceilingChain: number | undefined;
|
|
130
145
|
|
|
131
146
|
const takeValue = (flag: string, index: number): string => {
|
|
132
147
|
const value = argv[index + 1];
|
|
@@ -158,6 +173,15 @@ function parseArgs(argv: string[]): ParsedArgs | "help" {
|
|
|
158
173
|
} else if (arg === "--trust-policy") {
|
|
159
174
|
trustPolicyPath = takeValue(arg, i);
|
|
160
175
|
i++;
|
|
176
|
+
} else if (arg === "--ceiling-writer") {
|
|
177
|
+
ceilingWriter = takeValue(arg, i);
|
|
178
|
+
i++;
|
|
179
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(ceilingWriter)) throw new UsageError("--ceiling-writer must be a 0x address.");
|
|
180
|
+
} else if (arg === "--ceiling-chain") {
|
|
181
|
+
ceilingChain = Number(takeValue(arg, i));
|
|
182
|
+
i++;
|
|
183
|
+
if (!Number.isInteger(ceilingChain) || ceilingChain <= 0) throw new UsageError("--ceiling-chain must be a chain id.");
|
|
184
|
+
|
|
161
185
|
} else if (arg.startsWith("-")) {
|
|
162
186
|
throw new UsageError(`unknown option "${arg}".`);
|
|
163
187
|
} else if (bundlePath === undefined) {
|
|
@@ -173,6 +197,9 @@ function parseArgs(argv: string[]): ParsedArgs | "help" {
|
|
|
173
197
|
outDir,
|
|
174
198
|
formats,
|
|
175
199
|
...(trustPolicyPath !== undefined ? { trustPolicyPath } : {}),
|
|
200
|
+
...(ceilingWriter !== undefined ? { ceilingWriter } : {}),
|
|
201
|
+
...(ceilingChain !== undefined ? { ceilingChain } : {}),
|
|
202
|
+
|
|
176
203
|
};
|
|
177
204
|
}
|
|
178
205
|
|
|
@@ -209,7 +236,7 @@ function exitMeaning(flags: ExitFlags): string {
|
|
|
209
236
|
const parts: string[] = [];
|
|
210
237
|
if (flags.verificationFailures) parts.push("verification failures");
|
|
211
238
|
if (flags.chainAnomaliesOrDivergences) {
|
|
212
|
-
parts.push("chain anomalies, divergences, or anchor witness verification failures");
|
|
239
|
+
parts.push("chain anomalies, divergences, or anchor witness verification failures (or a failed ceiling in time)");
|
|
213
240
|
}
|
|
214
241
|
return parts.join("; ");
|
|
215
242
|
}
|
|
@@ -308,10 +335,15 @@ async function main(): Promise<number> {
|
|
|
308
335
|
|
|
309
336
|
let result;
|
|
310
337
|
try {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
338
|
+
const ceilings = {
|
|
339
|
+
...(parsed.ceilingWriter !== undefined ? { writer: parsed.ceilingWriter } : {}),
|
|
340
|
+
...(parsed.ceilingChain !== undefined ? { chainId: parsed.ceilingChain } : {}),
|
|
341
|
+
|
|
342
|
+
};
|
|
343
|
+
result = await runAudit(bundlePath, {
|
|
344
|
+
...(trustAnchors !== undefined ? { trustAnchors } : {}),
|
|
345
|
+
ceilings,
|
|
346
|
+
});
|
|
315
347
|
} catch (err) {
|
|
316
348
|
process.stderr.write(
|
|
317
349
|
`bitgraph-audit: cannot audit "${parsed.bundlePath}": ` +
|
|
@@ -338,6 +370,14 @@ async function main(): Promise<number> {
|
|
|
338
370
|
if (carrierFlags & 1) { flags.verificationFailures = true; }
|
|
339
371
|
if (carrierFlags & 2) { flags.chainAnomaliesOrDivergences = true; }
|
|
340
372
|
flags.code = flags.code | carrierFlags;
|
|
373
|
+
for (const c of result.ceilings?.checks ?? []) {
|
|
374
|
+
const width = c.window?.widthSeconds != null ? ` (window ${c.window.widthSeconds}s after the floor block)` : "";
|
|
375
|
+
process.stdout.write(c.status === "verified"
|
|
376
|
+
? `ceiling VERIFIED ${c.path}: ${c.label ?? ""}${width}; ${c.onChainDetail ?? ""}\n`
|
|
377
|
+
: `ceiling ${c.status.toUpperCase()} ${c.path}: ${c.reason ?? ""}\n`);
|
|
378
|
+
for (const line of c.settlement?.lines ?? []) process.stdout.write(` ${line}\n`);
|
|
379
|
+
}
|
|
380
|
+
for (const s of result.ceilings?.statuses ?? []) process.stdout.write(`ceiling ${s.status} ${s.path}: ${s.note}\n`);
|
|
341
381
|
process.stdout.write(
|
|
342
382
|
`bitgraph-audit ${auditToolVersion()}: wrote ${written.join(", ")}\n` +
|
|
343
383
|
`exit ${flags.code}: ${exitMeaning(flags)}\n`
|
package/src/index.ts
CHANGED
|
@@ -101,6 +101,12 @@ export { identifyAnchors } from "./anchors.js";
|
|
|
101
101
|
export { verifyAnchorWitnesses, verifyAnchorWitness } from "./witness.js";
|
|
102
102
|
|
|
103
103
|
export { deriveTemporalBounds } from "./temporal.js";
|
|
104
|
+
export { verifyCeilings, BITGRAPH_CEILING_WRITER, BASE_MAINNET_CHAIN_ID } from "./ceilings.js";
|
|
105
|
+
export type { CeilingAuditOptions } from "./ceilings.js";
|
|
106
|
+
|
|
107
|
+
// The blob layer of a ceiling's settlement on Ethereum (bitgraph-settlement/1): KZG, frames, channel, batches, the ceiling transaction.
|
|
108
|
+
export { verifySettlementBlobs, decodeOpBlob, parseFrames, decompressChannel, decodeBatches, decodeSpanBatch, BASE_MAINNET_ROLLUP, BLOB_BYTES, MAX_BLOB_DATA_BYTES } from "./settlement-blobs.js";
|
|
109
|
+
export type { SettlementBlobsResult, SettlementBlobsCheck, SettlementBlobsOptions, SettlementLocated, ChannelFrame, DecodedBatch, BatchTx } from "./settlement-blobs.js";
|
|
104
110
|
|
|
105
111
|
export { validateAttestations, validateNitroAttestationDocument } from "./attestation.js";
|
|
106
112
|
|