@mikeargento/bitgraph-audit 0.8.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 +8 -2
- package/dist/audit.d.ts +9 -6
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +34 -8
- 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 +109 -21
- package/dist/cli.js.map +1 -1
- package/dist/exports.d.ts +39 -0
- package/dist/exports.d.ts.map +1 -0
- package/dist/exports.js +374 -0
- package/dist/exports.js.map +1 -0
- 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 +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -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 +193 -19
- package/dist/ingest.js.map +1 -1
- package/dist/report-json.d.ts.map +1 -1
- package/dist/report-json.js +30 -0
- package/dist/report-json.js.map +1 -1
- package/dist/report-md.d.ts.map +1 -1
- package/dist/report-md.js +315 -25
- 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 +348 -7
- 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 +36 -8
- package/src/ceilings.ts +1 -0
- package/src/cli.ts +117 -22
- package/src/exports.ts +482 -0
- package/src/floors.ts +242 -0
- package/src/index.ts +20 -0
- package/src/ingest.ts +207 -18
- package/src/report-json.ts +30 -0
- package/src/report-md.ts +353 -29
- package/src/temporal.ts +279 -48
- package/src/types.ts +341 -6
package/src/floors.ts
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026 Argento Computing Inc. Licensed under the MIT License. See LICENSE.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* bitgraph-audit signed floors (enclave v10)
|
|
5
|
+
*
|
|
6
|
+
* From enclave v10 a proof's floor is a Base block the enclave signs into the
|
|
7
|
+
* proof as commit.slotFloor { chain: "base", evmChainId: 8453, blockNumber,
|
|
8
|
+
* blockHash, blockTimestamp }, one per proof, instead of an Ethereum anchor
|
|
9
|
+
* (commit.slotAnchor, read through the anchor proofs of the chain). Every
|
|
10
|
+
* floor is read through signedFloorOf from bitgraph-verify, which refuses a
|
|
11
|
+
* proof that signs both kinds: such a proof is ambiguous and nothing is read
|
|
12
|
+
* from it as a floor.
|
|
13
|
+
*
|
|
14
|
+
* What a Base floor gives the temporal stage: a NOT-BEFORE for the proof that
|
|
15
|
+
* signs it, grounded in block-hash unpredictability (the block hash did not
|
|
16
|
+
* exist before the block, and the proof signs it). Never a not-after: a later
|
|
17
|
+
* proof's floor block can predate this proof, so a floor bounds nothing
|
|
18
|
+
* before it.
|
|
19
|
+
*
|
|
20
|
+
* The block time. Base mainnet stamps every block by its height, and the
|
|
21
|
+
* enclave signs the time it read from a header it hashed itself. When the
|
|
22
|
+
* bundle carries that header (a floor header file, bitgraph-floor-header/1,
|
|
23
|
+
* as the carrier/3 unpacker writes it; or the floor inside a ceiling file or
|
|
24
|
+
* an export) it is checked with checkFloorHeader: keccak-256 to the signed
|
|
25
|
+
* hash, the signed number and time, Base mainnet's schedule. Without one the
|
|
26
|
+
* time is the signed one, which must still be on Base mainnet's schedule for
|
|
27
|
+
* the signed number, and confirming the block itself needs a Base lookup,
|
|
28
|
+
* which this offline audit never makes.
|
|
29
|
+
*
|
|
30
|
+
* A floor stamped after the proof's own attestation document is withheld as a
|
|
31
|
+
* bound (floorTimeIsBound): the record did not exist before that block, but
|
|
32
|
+
* the block's time cannot be a "not before" for a record already attested.
|
|
33
|
+
*
|
|
34
|
+
* Proofs with no slotFloor are not read here at all: bundles from before the
|
|
35
|
+
* cutover audit exactly as before.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import {
|
|
39
|
+
baseHeaderFields,
|
|
40
|
+
checkFloorHeader,
|
|
41
|
+
evmHexToBytes,
|
|
42
|
+
floorTimeIsBound,
|
|
43
|
+
onBaseSchedule,
|
|
44
|
+
signedFloorOf,
|
|
45
|
+
} from "@mikeargento/bitgraph-verify";
|
|
46
|
+
import type { SignedFloor } from "@mikeargento/bitgraph-verify";
|
|
47
|
+
import { attestationTimestampMs } from "./attestation.js";
|
|
48
|
+
import type { FloorProblem, IngestResult, ObservedProof, SignedFloorRecord } from "./types.js";
|
|
49
|
+
|
|
50
|
+
/** A Base floor that bounds the proof signing it. */
|
|
51
|
+
export interface FloorEvidence {
|
|
52
|
+
/** The proof that signs the floor. */
|
|
53
|
+
proofHash: string;
|
|
54
|
+
blockNumber: number;
|
|
55
|
+
blockHash: string;
|
|
56
|
+
/** Unix seconds: the signed time, equal to the checked header's when one was carried. */
|
|
57
|
+
timestamp: number;
|
|
58
|
+
timeSource: "header" | "signed";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface FloorReading {
|
|
62
|
+
/** Base floors usable as not-before bounds, keyed by the proof that signs them. */
|
|
63
|
+
bounds: Map<string, FloorEvidence>;
|
|
64
|
+
/** Every Base floor a proof signs, bounding or withheld, in observation order. */
|
|
65
|
+
records: SignedFloorRecord[];
|
|
66
|
+
problems: FloorProblem[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface HeaderCandidate {
|
|
70
|
+
path: string;
|
|
71
|
+
raw: Uint8Array;
|
|
72
|
+
/** Only floor header files are judged here; ceilings and exports are judged by their own stages. */
|
|
73
|
+
fromFloorFile: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function record(value: unknown): Record<string, unknown> | null {
|
|
77
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function iso(unix: number): string {
|
|
81
|
+
return new Date(unix * 1000).toISOString();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function firstPath(p: ObservedProof): string | undefined {
|
|
85
|
+
return p.sources[0]?.path;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Read every Base floor the bundle's proofs sign, and the headers that check them. */
|
|
89
|
+
export function readSignedFloors(ingest: IngestResult): FloorReading {
|
|
90
|
+
const problems: FloorProblem[] = [];
|
|
91
|
+
const records: SignedFloorRecord[] = [];
|
|
92
|
+
const bounds = new Map<string, FloorEvidence>();
|
|
93
|
+
|
|
94
|
+
// The Base floors the proofs sign, by block hash (to match header files).
|
|
95
|
+
const signedHashes = new Set<string>();
|
|
96
|
+
for (const p of ingest.proofs) {
|
|
97
|
+
const f = p.proof.commit?.slotFloor as { blockHash?: unknown } | undefined;
|
|
98
|
+
if (f !== undefined && typeof f.blockHash === "string") signedHashes.add(f.blockHash.toLowerCase());
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Headers in the bundle, by the hash their bytes compute to.
|
|
102
|
+
const headers = new Map<string, HeaderCandidate[]>();
|
|
103
|
+
const add = (hash: string, c: HeaderCandidate): void => {
|
|
104
|
+
const list = headers.get(hash) ?? [];
|
|
105
|
+
list.push(c);
|
|
106
|
+
headers.set(hash, list);
|
|
107
|
+
};
|
|
108
|
+
for (const file of ingest.floorHeaders ?? []) {
|
|
109
|
+
const json = file.json;
|
|
110
|
+
const header = json["header"];
|
|
111
|
+
const fields = typeof header === "string" ? baseHeaderFields(header) : null;
|
|
112
|
+
if (json["chain"] !== "base" || fields === null) {
|
|
113
|
+
problems.push({
|
|
114
|
+
code: "floor-header-malformed",
|
|
115
|
+
path: file.path,
|
|
116
|
+
message:
|
|
117
|
+
json["chain"] !== "base"
|
|
118
|
+
? `the floor header file names chain ${JSON.stringify(json["chain"])}; only a Base header (chain "base") is a floor header`
|
|
119
|
+
: "the floor header file carries no readable Base block header",
|
|
120
|
+
});
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const declared = typeof json["blockHash"] === "string" ? (json["blockHash"] as string).toLowerCase() : undefined;
|
|
124
|
+
if (declared !== undefined && declared !== fields.blockHash) {
|
|
125
|
+
problems.push({
|
|
126
|
+
code: "floor-header-mismatch",
|
|
127
|
+
path: file.path,
|
|
128
|
+
message: `the header in this file hashes to ${fields.blockHash}, not to the block it names (${declared})`,
|
|
129
|
+
});
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (!signedHashes.has(fields.blockHash)) {
|
|
133
|
+
problems.push({
|
|
134
|
+
code: "floor-header-unmatched",
|
|
135
|
+
path: file.path,
|
|
136
|
+
message: `the header is Base block ${fields.blockNumber} (${fields.blockHash}), which no proof in this bundle signs as its floor`,
|
|
137
|
+
});
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
add(fields.blockHash, { path: file.path, raw: evmHexToBytes(header as string), fromFloorFile: true });
|
|
141
|
+
}
|
|
142
|
+
// The floor a ceiling file or an export carries, when it is a Base header.
|
|
143
|
+
const carried = (path: string, floor: Record<string, unknown> | null, field: string): void => {
|
|
144
|
+
if (floor === null || floor["chain"] !== "base" || typeof floor[field] !== "string") return;
|
|
145
|
+
const fields = baseHeaderFields(floor[field] as string);
|
|
146
|
+
if (fields === null || !signedHashes.has(fields.blockHash)) return;
|
|
147
|
+
add(fields.blockHash, { path, raw: evmHexToBytes(floor[field] as string), fromFloorFile: false });
|
|
148
|
+
};
|
|
149
|
+
for (const c of ingest.ceilings ?? []) carried(c.path, record(c.json["floor"]), "blockHeader");
|
|
150
|
+
for (const e of ingest.exports ?? []) if (e.status === "ok") carried(e.path, record(e.json["floor"]), "header");
|
|
151
|
+
|
|
152
|
+
for (const p of ingest.proofs) {
|
|
153
|
+
const commit = p.proof.commit as unknown as Record<string, unknown> | undefined;
|
|
154
|
+
if (commit === undefined || commit["slotFloor"] === undefined) continue;
|
|
155
|
+
const path = firstPath(p);
|
|
156
|
+
let floor: SignedFloor | null;
|
|
157
|
+
try {
|
|
158
|
+
floor = signedFloorOf(p.proof);
|
|
159
|
+
} catch (e) {
|
|
160
|
+
const both = commit["slotAnchor"] !== undefined;
|
|
161
|
+
problems.push({
|
|
162
|
+
code: both ? "floor-ambiguous" : "floor-malformed",
|
|
163
|
+
proofHash: p.proofHash,
|
|
164
|
+
...(path !== undefined ? { path } : {}),
|
|
165
|
+
message: both
|
|
166
|
+
? "the proof signs two floors (commit.slotAnchor, an Ethereum anchor, and commit.slotFloor, a Base block); it is ambiguous and neither is read as its floor"
|
|
167
|
+
: `the proof's commit.slotFloor is not a Base floor: ${(e as Error).message}`,
|
|
168
|
+
});
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (floor === null || floor.chain !== "base" || floor.blockTimestamp === undefined) continue;
|
|
172
|
+
const base = {
|
|
173
|
+
proofHash: p.proofHash,
|
|
174
|
+
chain: "base" as const,
|
|
175
|
+
blockNumber: floor.blockNumber,
|
|
176
|
+
blockHash: floor.blockHash,
|
|
177
|
+
blockTimestamp: floor.blockTimestamp,
|
|
178
|
+
};
|
|
179
|
+
const withheld = (reason: string, header: SignedFloorRecord["header"], headerPath?: string): void => {
|
|
180
|
+
records.push({ ...base, header, ...(headerPath !== undefined ? { headerPath } : {}), bound: "withheld", withheldReason: reason });
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
if (!onBaseSchedule(floor.blockNumber, floor.blockTimestamp)) {
|
|
184
|
+
const reason = `the signed time of Base block ${floor.blockNumber} (${floor.blockTimestamp}) is not Base mainnet's schedule for that block`;
|
|
185
|
+
problems.push({ code: "floor-off-schedule", proofHash: p.proofHash, ...(path !== undefined ? { path } : {}), message: reason });
|
|
186
|
+
withheld(reason, "not-carried");
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// A header in the bundle for the signed block: every floor header file
|
|
191
|
+
// naming it must hold; the first that holds dates the floor.
|
|
192
|
+
let checkedPath: string | undefined;
|
|
193
|
+
let mismatch: string | undefined;
|
|
194
|
+
for (const c of headers.get(floor.blockHash) ?? []) {
|
|
195
|
+
const r = checkFloorHeader(floor, c.raw, "base");
|
|
196
|
+
if (r.ok) {
|
|
197
|
+
checkedPath ??= c.path;
|
|
198
|
+
} else if (c.fromFloorFile) {
|
|
199
|
+
mismatch = `${c.path}: ${r.reason}`;
|
|
200
|
+
problems.push({
|
|
201
|
+
code: "floor-header-mismatch",
|
|
202
|
+
proofHash: p.proofHash,
|
|
203
|
+
path: c.path,
|
|
204
|
+
message: `the floor header does not match the Base floor the proof signs: ${r.reason}`,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const header: SignedFloorRecord["header"] = checkedPath !== undefined ? "checked" : "not-carried";
|
|
209
|
+
if (mismatch !== undefined) {
|
|
210
|
+
withheld(`a floor header in the bundle contradicts the signed floor (${mismatch})`, header, checkedPath);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (p.verification?.status === "failed") {
|
|
214
|
+
withheld("the proof does not verify, so the floor in it is not a signed one", header, checkedPath);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const attestedMs = attestationTimestampMs(
|
|
218
|
+
((p.proof as { environment?: { attestation?: { reportB64?: string } } }).environment?.attestation?.reportB64) ?? ""
|
|
219
|
+
);
|
|
220
|
+
const timeOk = floorTimeIsBound(floor.blockTimestamp, attestedMs);
|
|
221
|
+
if (!timeOk.ok) {
|
|
222
|
+
withheld(timeOk.reason, header, checkedPath);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
records.push({ ...base, header, ...(checkedPath !== undefined ? { headerPath: checkedPath } : {}), bound: "not-before" });
|
|
226
|
+
bounds.set(p.proofHash, {
|
|
227
|
+
proofHash: p.proofHash,
|
|
228
|
+
blockNumber: floor.blockNumber,
|
|
229
|
+
blockHash: floor.blockHash,
|
|
230
|
+
timestamp: floor.blockTimestamp,
|
|
231
|
+
timeSource: checkedPath !== undefined ? "header" : "signed",
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return { bounds, records, problems };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** The sentence a Base floor bound states about its block time, shared by the reports. */
|
|
238
|
+
export function baseFloorTimeSentence(blockNumber: number | string, timestamp: number, timeSource: "header" | "signed"): string {
|
|
239
|
+
return timeSource === "header"
|
|
240
|
+
? `Base block ${blockNumber} is stamped ${iso(timestamp)}; its header is in the bundle and was checked against the signed block hash, number and time.`
|
|
241
|
+
: `Base block ${blockNumber} is stamped ${iso(timestamp)} as the proof signs it, on Base mainnet's schedule for that block; no header for it is in the bundle, so confirming the block needs a Base lookup.`;
|
|
242
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -65,6 +65,10 @@ export type {
|
|
|
65
65
|
TemporalSegment,
|
|
66
66
|
AnchorOrderedPair,
|
|
67
67
|
TemporalAnalysis,
|
|
68
|
+
SignedFloorRecord,
|
|
69
|
+
FloorProblem,
|
|
70
|
+
FloorProblemCode,
|
|
71
|
+
FloorHeaderFile,
|
|
68
72
|
AttestationCheck,
|
|
69
73
|
NitroValidationOptions,
|
|
70
74
|
NitroValidationResult,
|
|
@@ -83,6 +87,14 @@ export type {
|
|
|
83
87
|
ReportInputSummary,
|
|
84
88
|
ReportSummary,
|
|
85
89
|
AuditJsonReport,
|
|
90
|
+
ExportFile,
|
|
91
|
+
ExportClaimRecord,
|
|
92
|
+
ExportOwnClaim,
|
|
93
|
+
ExportTimes,
|
|
94
|
+
ExportCoveredFile,
|
|
95
|
+
ExportRun,
|
|
96
|
+
ExportCheck,
|
|
97
|
+
ExportAnalysis,
|
|
86
98
|
} from "./types.js";
|
|
87
99
|
|
|
88
100
|
export { ingestBundle, ingestEntries, streamMatchedArtifacts, streamArtifactsByHash, DEFAULT_INGEST_LIMITS } from "./ingest.js";
|
|
@@ -101,9 +113,17 @@ export { identifyAnchors } from "./anchors.js";
|
|
|
101
113
|
export { verifyAnchorWitnesses, verifyAnchorWitness } from "./witness.js";
|
|
102
114
|
|
|
103
115
|
export { deriveTemporalBounds } from "./temporal.js";
|
|
116
|
+
// Base floors (enclave v10): the floor each proof signs, read through signedFloorOf.
|
|
117
|
+
export { readSignedFloors } from "./floors.js";
|
|
118
|
+
export type { FloorEvidence, FloorReading } from "./floors.js";
|
|
119
|
+
export { FLOOR_HEADER_VERSION } from "./ingest.js";
|
|
104
120
|
export { verifyCeilings, BITGRAPH_CEILING_WRITER, BASE_MAINNET_CHAIN_ID } from "./ceilings.js";
|
|
105
121
|
export type { CeilingAuditOptions } from "./ceilings.js";
|
|
106
122
|
|
|
123
|
+
// Exports (bitgraph-export/1): each checked with verifyExport, once per file in the bundle it covers.
|
|
124
|
+
export { verifyExports, exportRunClaims, DEFAULT_AS_GIVEN_LEAF_BUDGET } from "./exports.js";
|
|
125
|
+
export type { ExportAuditOptions } from "./exports.js";
|
|
126
|
+
|
|
107
127
|
// The blob layer of a ceiling's settlement on Ethereum (bitgraph-settlement/1): KZG, frames, channel, batches, the ceiling transaction.
|
|
108
128
|
export { verifySettlementBlobs, decodeOpBlob, parseFrames, decompressChannel, decodeBatches, decodeSpanBatch, BASE_MAINNET_ROLLUP, BLOB_BYTES, MAX_BLOB_DATA_BYTES } from "./settlement-blobs.js";
|
|
109
129
|
export type { SettlementBlobsResult, SettlementBlobsCheck, SettlementBlobsOptions, SettlementLocated, ChannelFrame, DecodedBatch, BatchTx } from "./settlement-blobs.js";
|