@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/temporal.ts
CHANGED
|
@@ -47,6 +47,18 @@
|
|
|
47
47
|
* are evidence, never divergence; overlapping or absent bounds mean
|
|
48
48
|
* concurrent-or-unordered.
|
|
49
49
|
*
|
|
50
|
+
* Base floors (enclave v10). A proof that signs a Base block as its floor
|
|
51
|
+
* (commit.slotFloor, read through floors.ts and signedFloorOf) is bounded
|
|
52
|
+
* NOT-BEFORE by that block, with evidence "signed-floor": the proof's own
|
|
53
|
+
* signed field. Proofs after it in the chain inherit the bound like an
|
|
54
|
+
* anchor's ("chain-link", or "counter-order", weaker). A floor never gives a
|
|
55
|
+
* not-after, to any proof: a later proof's floor block can predate this
|
|
56
|
+
* proof. Floors count toward an epoch's lower-bound coverage; cross-epoch
|
|
57
|
+
* ordering pairs stay anchor-only (a pair needs a not-after, which only an
|
|
58
|
+
* anchor supplies, and the after side is read from anchor bounds alone, so
|
|
59
|
+
* no ordering is ever claimed from floors). Proofs without slotFloor are not
|
|
60
|
+
* touched: a bundle from before the cutover yields exactly the bounds it did.
|
|
61
|
+
*
|
|
50
62
|
* Run after verifyObservedProofs, reconstructChains, identifyAnchors, and
|
|
51
63
|
* verifyAnchorWitnesses. Populates EpochRecord.anchorBounds on the given
|
|
52
64
|
* reconstruction (the typed Phase 4c extension point) and returns the
|
|
@@ -68,6 +80,7 @@ import type {
|
|
|
68
80
|
TemporalSegment,
|
|
69
81
|
} from "./types.js";
|
|
70
82
|
import { byCounterThenHash, parseCounter, pushMap } from "./validity.js";
|
|
83
|
+
import { baseFloorTimeSentence, readSignedFloors, type FloorEvidence } from "./floors.js";
|
|
71
84
|
|
|
72
85
|
// ---------------------------------------------------------------------------
|
|
73
86
|
// Public API
|
|
@@ -102,20 +115,28 @@ export function deriveTemporalBounds(
|
|
|
102
115
|
.filter((h) => !evidence.has(h))
|
|
103
116
|
.sort();
|
|
104
117
|
|
|
118
|
+
// Base floors the proofs sign (enclave v10). Empty for older bundles.
|
|
119
|
+
const floors = readSignedFloors(ingest);
|
|
120
|
+
|
|
105
121
|
// Per-partition segments.
|
|
106
122
|
const segments: TemporalSegment[] = [];
|
|
123
|
+
const anchorLower = new Map<string, SegmentBound[]>();
|
|
107
124
|
for (const partition of reconstruction.partitions) {
|
|
108
|
-
|
|
125
|
+
const built = buildPartitionSegments(partition, byHash, evidence, floors.bounds);
|
|
126
|
+
segments.push(...built.segments);
|
|
127
|
+
for (const [h, b] of built.anchorLower) anchorLower.set(h, b);
|
|
109
128
|
}
|
|
110
129
|
|
|
111
130
|
// Epoch-level aggregation and cross-epoch ordering.
|
|
112
|
-
const anchorOrderedPairs = aggregateEpochs(reconstruction, segments);
|
|
131
|
+
const anchorOrderedPairs = aggregateEpochs(reconstruction, segments, anchorLower);
|
|
113
132
|
|
|
114
133
|
return {
|
|
115
134
|
segments,
|
|
116
135
|
anchorOrderedPairs,
|
|
117
136
|
verifiedAnchorProofHashes,
|
|
118
137
|
unverifiedAnchorProofHashes,
|
|
138
|
+
...(floors.records.length > 0 ? { signedFloors: floors.records } : {}),
|
|
139
|
+
...(floors.problems.length > 0 ? { floorProblems: floors.problems } : {}),
|
|
119
140
|
};
|
|
120
141
|
}
|
|
121
142
|
|
|
@@ -124,6 +145,32 @@ interface VerifiedAnchorEvidence {
|
|
|
124
145
|
timestamp: number;
|
|
125
146
|
blockHash: string;
|
|
126
147
|
blockNumber?: string;
|
|
148
|
+
/** Present for a Base floor: how its time was read. Absent for an anchor. */
|
|
149
|
+
floor?: { timeSource: "header" | "signed" };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Tie-break strength of bound evidence at an equal block time. */
|
|
153
|
+
const EVIDENCE_RANK: Record<BoundEvidence, number> = { "signed-floor": 2, "chain-link": 1, "counter-order": 0 };
|
|
154
|
+
|
|
155
|
+
/** Candidate key of a Base floor: kept apart from anchor keys (a proof hash). */
|
|
156
|
+
const FLOOR_KEY = "floor:";
|
|
157
|
+
|
|
158
|
+
function floorSource(f: FloorEvidence): VerifiedAnchorEvidence {
|
|
159
|
+
return {
|
|
160
|
+
anchorProofHash: f.proofHash,
|
|
161
|
+
timestamp: f.timestamp,
|
|
162
|
+
blockHash: f.blockHash,
|
|
163
|
+
blockNumber: String(f.blockNumber),
|
|
164
|
+
floor: { timeSource: f.timeSource },
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The tighter of two floors (later block time; at a tie, the lower proof hash, for determinism). */
|
|
169
|
+
function laterFloor(a: FloorEvidence | undefined, b: FloorEvidence | undefined): FloorEvidence | undefined {
|
|
170
|
+
if (a === undefined) return b;
|
|
171
|
+
if (b === undefined) return a;
|
|
172
|
+
if (a.timestamp !== b.timestamp) return a.timestamp > b.timestamp ? a : b;
|
|
173
|
+
return a.proofHash <= b.proofHash ? a : b;
|
|
127
174
|
}
|
|
128
175
|
|
|
129
176
|
// ---------------------------------------------------------------------------
|
|
@@ -139,15 +186,26 @@ interface MemberBounds {
|
|
|
139
186
|
function buildPartitionSegments(
|
|
140
187
|
partition: ChainPartition,
|
|
141
188
|
byHash: Map<string, ObservedProof>,
|
|
142
|
-
|
|
143
|
-
|
|
189
|
+
anchorEvidence: Map<string, VerifiedAnchorEvidence>,
|
|
190
|
+
floorBounds: Map<string, FloorEvidence>
|
|
191
|
+
): { segments: TemporalSegment[]; anchorLower: Map<string, SegmentBound[]> } {
|
|
144
192
|
const members = partition.memberProofHashes.map((h) => byHash.get(h) as ObservedProof);
|
|
145
193
|
// Keyed by CHAIN hash: this map is used only to resolve prevB64 pointers
|
|
146
194
|
// (here and in collectAncestors), and prevB64 references the predecessor's
|
|
147
195
|
// chain hash, not its identity hash.
|
|
148
196
|
const memberSet = new Map<string, ObservedProof>(members.map((m) => [m.chainHash, m]));
|
|
149
197
|
|
|
150
|
-
const anchors = members.filter((m) =>
|
|
198
|
+
const anchors = members.filter((m) => anchorEvidence.has(m.proofHash));
|
|
199
|
+
|
|
200
|
+
// Base floors signed in this partition. Floor sources join the evidence
|
|
201
|
+
// map under their own keys, so selection treats a floor exactly as it
|
|
202
|
+
// treats an anchor's not-before.
|
|
203
|
+
const floored = members.filter((m) => floorBounds.has(m.proofHash));
|
|
204
|
+
let evidence = anchorEvidence;
|
|
205
|
+
if (floored.length > 0) {
|
|
206
|
+
evidence = new Map(anchorEvidence);
|
|
207
|
+
for (const m of floored) evidence.set(FLOOR_KEY + m.proofHash, floorSource(floorBounds.get(m.proofHash) as FloorEvidence));
|
|
208
|
+
}
|
|
151
209
|
|
|
152
210
|
// Hash-link structure within the partition.
|
|
153
211
|
const successors = new Map<string, ObservedProof[]>();
|
|
@@ -167,6 +225,58 @@ function buildPartitionSegments(
|
|
|
167
225
|
chainBefore.set(anchor.proofHash, collectAncestors(anchor, memberSet));
|
|
168
226
|
}
|
|
169
227
|
|
|
228
|
+
// Base floors reach a member three ways: its own signed floor; the
|
|
229
|
+
// tightest floor among its hash-link ancestors (chain-link); the tightest
|
|
230
|
+
// floor signed at a lower commit counter (counter-order). Both inherited
|
|
231
|
+
// forms are computed in one pass each, never per pair.
|
|
232
|
+
const chainFloor = new Map<string, FloorEvidence | undefined>();
|
|
233
|
+
const counterFloor = new Map<string, FloorEvidence | undefined>();
|
|
234
|
+
if (floored.length > 0) {
|
|
235
|
+
const pred = (m: ObservedProof): ObservedProof | undefined => {
|
|
236
|
+
if (m.prevB64 === undefined) return undefined;
|
|
237
|
+
const p = memberSet.get(m.prevB64);
|
|
238
|
+
return p === undefined || p.proofHash === m.proofHash ? undefined : p;
|
|
239
|
+
};
|
|
240
|
+
for (const m of members) {
|
|
241
|
+
const path: ObservedProof[] = [];
|
|
242
|
+
const seen = new Set<string>();
|
|
243
|
+
let cur: ObservedProof | undefined = m;
|
|
244
|
+
while (cur !== undefined && !chainFloor.has(cur.proofHash) && !seen.has(cur.proofHash)) {
|
|
245
|
+
seen.add(cur.proofHash);
|
|
246
|
+
path.push(cur);
|
|
247
|
+
cur = pred(cur);
|
|
248
|
+
}
|
|
249
|
+
let carry =
|
|
250
|
+
cur !== undefined && chainFloor.has(cur.proofHash)
|
|
251
|
+
? laterFloor(chainFloor.get(cur.proofHash), floorBounds.get(cur.proofHash))
|
|
252
|
+
: undefined;
|
|
253
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
254
|
+
const node = path[i] as ObservedProof;
|
|
255
|
+
chainFloor.set(node.proofHash, carry);
|
|
256
|
+
carry = laterFloor(carry, floorBounds.get(node.proofHash));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const counted = members
|
|
260
|
+
.map((m) => ({ m, c: parseCounter(m.counter) }))
|
|
261
|
+
.filter((x): x is { m: ObservedProof; c: bigint } => x.c !== undefined)
|
|
262
|
+
.sort((a, b) => (a.c < b.c ? -1 : a.c > b.c ? 1 : 0));
|
|
263
|
+
let running: FloorEvidence | undefined;
|
|
264
|
+
for (let i = 0; i < counted.length; ) {
|
|
265
|
+
let j = i;
|
|
266
|
+
while (j < counted.length && (counted[j] as { c: bigint }).c === (counted[i] as { c: bigint }).c) j++;
|
|
267
|
+
let group: FloorEvidence | undefined;
|
|
268
|
+
for (let k = i; k < j; k++) {
|
|
269
|
+
const m = (counted[k] as { m: ObservedProof }).m;
|
|
270
|
+
counterFloor.set(m.proofHash, running);
|
|
271
|
+
group = laterFloor(group, floorBounds.get(m.proofHash));
|
|
272
|
+
}
|
|
273
|
+
running = laterFloor(running, group);
|
|
274
|
+
i = j;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const mixed = floored.length > 0 && anchors.length > 0;
|
|
278
|
+
const anchorLower = new Map<string, SegmentBound[]>();
|
|
279
|
+
|
|
170
280
|
// Candidate bounds per member.
|
|
171
281
|
const memberBounds: MemberBounds[] = members.map((member) => {
|
|
172
282
|
const lowerCandidates = new Map<string, BoundEvidence>();
|
|
@@ -205,6 +315,26 @@ function buildPartitionSegments(
|
|
|
205
315
|
}
|
|
206
316
|
}
|
|
207
317
|
|
|
318
|
+
// A partition holding both anchors and floors keeps each member's
|
|
319
|
+
// anchor-only not-before apart, for the cross-epoch pairs.
|
|
320
|
+
if (mixed && lowerCandidates.size > 0) {
|
|
321
|
+
anchorLower.set(member.proofHash, selectBounds("not-before", lowerCandidates, evidence));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Base floors: not-before only. Never an upper candidate.
|
|
325
|
+
if (floored.length > 0) {
|
|
326
|
+
const own = floorBounds.get(member.proofHash);
|
|
327
|
+
if (own !== undefined) lowerCandidates.set(FLOOR_KEY + own.proofHash, "signed-floor");
|
|
328
|
+
const viaChain = chainFloor.get(member.proofHash);
|
|
329
|
+
if (viaChain !== undefined && !lowerCandidates.has(FLOOR_KEY + viaChain.proofHash)) {
|
|
330
|
+
lowerCandidates.set(FLOOR_KEY + viaChain.proofHash, "chain-link");
|
|
331
|
+
}
|
|
332
|
+
const viaCounter = counterFloor.get(member.proofHash);
|
|
333
|
+
if (viaCounter !== undefined && !lowerCandidates.has(FLOOR_KEY + viaCounter.proofHash)) {
|
|
334
|
+
lowerCandidates.set(FLOOR_KEY + viaCounter.proofHash, "counter-order");
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
208
338
|
return {
|
|
209
339
|
member,
|
|
210
340
|
lower: selectBounds("not-before", lowerCandidates, evidence),
|
|
@@ -245,7 +375,7 @@ function buildPartitionSegments(
|
|
|
245
375
|
}
|
|
246
376
|
|
|
247
377
|
segments.sort(compareSegments);
|
|
248
|
-
return segments;
|
|
378
|
+
return { segments, anchorLower };
|
|
249
379
|
}
|
|
250
380
|
|
|
251
381
|
/**
|
|
@@ -277,8 +407,9 @@ function selectBounds(
|
|
|
277
407
|
? a.info.timestamp > b.info.timestamp
|
|
278
408
|
: a.info.timestamp < b.info.timestamp;
|
|
279
409
|
}
|
|
280
|
-
// Tie: prefer
|
|
281
|
-
|
|
410
|
+
// Tie: prefer the proof's own signed floor, then chain-link evidence over
|
|
411
|
+
// counter order, then the lower key.
|
|
412
|
+
if (a.evidenceKind !== b.evidenceKind) return EVIDENCE_RANK[a.evidenceKind] > EVIDENCE_RANK[b.evidenceKind];
|
|
282
413
|
return a.anchorHash < b.anchorHash;
|
|
283
414
|
};
|
|
284
415
|
|
|
@@ -286,15 +417,11 @@ function selectBounds(
|
|
|
286
417
|
let bestChain: (typeof entries)[number] | undefined;
|
|
287
418
|
for (const entry of entries) {
|
|
288
419
|
if (better(entry, best)) best = entry;
|
|
289
|
-
if (entry.evidenceKind
|
|
420
|
+
if (entry.evidenceKind !== "counter-order" && better(entry, bestChain)) bestChain = entry;
|
|
290
421
|
}
|
|
291
422
|
|
|
292
423
|
const bounds: SegmentBound[] = [makeBound(kind, best as (typeof entries)[number])];
|
|
293
|
-
if (
|
|
294
|
-
bestChain !== undefined &&
|
|
295
|
-
((best as (typeof entries)[number]).anchorHash !== bestChain.anchorHash ||
|
|
296
|
-
(best as (typeof entries)[number]).evidenceKind !== "chain-link")
|
|
297
|
-
) {
|
|
424
|
+
if (bestChain !== undefined && bestChain !== best) {
|
|
298
425
|
bounds.push(makeBound(kind, bestChain));
|
|
299
426
|
}
|
|
300
427
|
return bounds;
|
|
@@ -305,6 +432,7 @@ function makeBound(
|
|
|
305
432
|
entry: { anchorHash: string; evidenceKind: BoundEvidence; info: VerifiedAnchorEvidence }
|
|
306
433
|
): SegmentBound {
|
|
307
434
|
const { info, evidenceKind } = entry;
|
|
435
|
+
if (info.floor !== undefined) return makeFloorBound(entry);
|
|
308
436
|
const iso = new Date(info.timestamp * 1000).toISOString();
|
|
309
437
|
const blockName =
|
|
310
438
|
info.blockNumber !== undefined ? `Ethereum block ${info.blockNumber}` : "an Ethereum block";
|
|
@@ -342,9 +470,48 @@ function makeBound(
|
|
|
342
470
|
};
|
|
343
471
|
}
|
|
344
472
|
|
|
473
|
+
/**
|
|
474
|
+
* A not-before from a Base floor (enclave v10). The proof that signs the
|
|
475
|
+
* floor block is named in anchorProofHash; source, chain and timeSource say
|
|
476
|
+
* what it is. Always evidence, never an assumption, and never a not-after.
|
|
477
|
+
*/
|
|
478
|
+
function makeFloorBound(entry: { anchorHash: string; evidenceKind: BoundEvidence; info: VerifiedAnchorEvidence }): SegmentBound {
|
|
479
|
+
const { info, evidenceKind } = entry;
|
|
480
|
+
const timeSource = (info.floor as { timeSource: "header" | "signed" }).timeSource;
|
|
481
|
+
const iso = new Date(info.timestamp * 1000).toISOString();
|
|
482
|
+
const evidenceSentence =
|
|
483
|
+
evidenceKind === "signed-floor"
|
|
484
|
+
? "Evidence: the proof signs this block as its floor (commit.slotFloor)."
|
|
485
|
+
: evidenceKind === "chain-link"
|
|
486
|
+
? "Evidence: a verified hash-link path connects these proofs to a proof that signs this block as its floor."
|
|
487
|
+
: "Evidence: commit-counter ordering within the partition only, after a proof that signs this block as its " +
|
|
488
|
+
"floor; this relies on the authority's counter discipline rather than verifiable hash links (weaker).";
|
|
489
|
+
const claim =
|
|
490
|
+
`These proofs were committed no earlier than ${iso} (unix ${info.timestamp}), the time of Base block ` +
|
|
491
|
+
`${info.blockNumber}: the block hash was unpredictable before that time and the floor proof signs it. ` +
|
|
492
|
+
`${baseFloorTimeSentence(info.blockNumber as string, info.timestamp, timeSource)} Reading this as a wall-clock ` +
|
|
493
|
+
`floor additionally assumes the block is a genuine, publicly published Base block, which this offline audit ` +
|
|
494
|
+
`cannot confirm. A floor is never an upper bound on anything. ${evidenceSentence}`;
|
|
495
|
+
return {
|
|
496
|
+
kind: "not-before",
|
|
497
|
+
anchorProofHash: info.anchorProofHash,
|
|
498
|
+
source: "signed-floor",
|
|
499
|
+
chain: "base",
|
|
500
|
+
timeSource,
|
|
501
|
+
...(info.blockNumber !== undefined ? { blockNumber: info.blockNumber } : {}),
|
|
502
|
+
blockHash: info.blockHash,
|
|
503
|
+
timestamp: info.timestamp,
|
|
504
|
+
evidence: evidenceKind,
|
|
505
|
+
weaker: evidenceKind === "counter-order",
|
|
506
|
+
basis: "block-hash-unpredictability",
|
|
507
|
+
boundClass: "evidence",
|
|
508
|
+
claim,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
345
512
|
function boundSetKey(mb: MemberBounds): string {
|
|
346
513
|
const part = (bounds: SegmentBound[]): string =>
|
|
347
|
-
bounds.map((b) => `${b.anchorProofHash}:${b.evidence}`).join(",");
|
|
514
|
+
bounds.map((b) => `${b.source === "signed-floor" ? FLOOR_KEY : ""}${b.anchorProofHash}:${b.evidence}`).join(",");
|
|
348
515
|
return `${part(mb.lower)}|${part(mb.upper)}`;
|
|
349
516
|
}
|
|
350
517
|
|
|
@@ -420,6 +587,8 @@ interface CoverageRepresentative {
|
|
|
420
587
|
anchorProofHash: string;
|
|
421
588
|
blockHash: string;
|
|
422
589
|
blockNumber?: string;
|
|
590
|
+
/** Present when the representative is a Base floor. */
|
|
591
|
+
floor?: true;
|
|
423
592
|
/** Evidence class of the bound chosen as the conservative representative. */
|
|
424
593
|
evidence: BoundEvidence;
|
|
425
594
|
/** True when that representative rests on counter-order evidence. */
|
|
@@ -428,9 +597,16 @@ interface CoverageRepresentative {
|
|
|
428
597
|
|
|
429
598
|
interface EpochCoverage {
|
|
430
599
|
totalProofCount: number;
|
|
431
|
-
/** Distinct members covered by any not-before bound, and the most conservative (minimum) covering timestamp. */
|
|
600
|
+
/** Distinct members covered by any not-before bound (anchor or Base floor), and the most conservative (minimum) covering timestamp. */
|
|
432
601
|
lowerCovered: Set<string>;
|
|
433
602
|
lowerMin?: CoverageRepresentative;
|
|
603
|
+
/** Whether any not-before bound counted here is a Base floor (changes the claim's wording only). */
|
|
604
|
+
lowerFromFloors: boolean;
|
|
605
|
+
/** Whether any not-before bound counted here is an anchor. */
|
|
606
|
+
lowerFromAnchors: boolean;
|
|
607
|
+
/** The same, from anchor bounds alone: the after side of a cross-epoch pair. Floors never order epochs. */
|
|
608
|
+
anchorLowerCovered: Set<string>;
|
|
609
|
+
anchorLowerMin?: CoverageRepresentative;
|
|
434
610
|
/** Distinct members covered by any not-after bound, and the most conservative (maximum) covering timestamp. */
|
|
435
611
|
upperCovered: Set<string>;
|
|
436
612
|
upperMax?: CoverageRepresentative;
|
|
@@ -457,15 +633,69 @@ function weakerCaveat(weaker: boolean): string {
|
|
|
457
633
|
: "";
|
|
458
634
|
}
|
|
459
635
|
|
|
636
|
+
/** Fold one not-before bound into a coverage minimum, preferring stronger evidence at an equal time. */
|
|
637
|
+
function foldLower(current: CoverageRepresentative | undefined, bound: SegmentBound): CoverageRepresentative {
|
|
638
|
+
if (
|
|
639
|
+
current === undefined ||
|
|
640
|
+
bound.timestamp < current.timestamp ||
|
|
641
|
+
(bound.timestamp === current.timestamp && current.weaker && !bound.weaker)
|
|
642
|
+
) {
|
|
643
|
+
return {
|
|
644
|
+
timestamp: bound.timestamp,
|
|
645
|
+
anchorProofHash: bound.anchorProofHash,
|
|
646
|
+
blockHash: bound.blockHash,
|
|
647
|
+
...(bound.blockNumber !== undefined ? { blockNumber: bound.blockNumber } : {}),
|
|
648
|
+
...(bound.source === "signed-floor" ? { floor: true as const } : {}),
|
|
649
|
+
evidence: bound.evidence,
|
|
650
|
+
weaker: bound.weaker,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
return current;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/** The epoch-level not-before claim, worded for what grounds it. Pure-anchor wording is unchanged. */
|
|
657
|
+
function lowerClaim(cov: EpochCoverage): string {
|
|
658
|
+
const min = cov.lowerMin as CoverageRepresentative;
|
|
659
|
+
if (!cov.lowerFromFloors) {
|
|
660
|
+
return (
|
|
661
|
+
`${cov.lowerCovered.size} of ${cov.totalProofCount} observed proofs of this epoch were ` +
|
|
662
|
+
`committed no earlier than unix ${min.timestamp}, grounded in block-hash ` +
|
|
663
|
+
`unpredictability through verified anchor witnesses. This additionally assumes the anchored ` +
|
|
664
|
+
`header is a genuine, publicly published Ethereum block, which this offline audit cannot ` +
|
|
665
|
+
`confirm.` +
|
|
666
|
+
weakerCaveat(min.weaker) +
|
|
667
|
+
` The remaining proofs sit causally before the covering anchors and carry no lower bound ` +
|
|
668
|
+
`from this evidence.`
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
const grounds = cov.lowerFromAnchors
|
|
672
|
+
? "the Base blocks the proofs sign as their floors and verified Ethereum anchor witnesses"
|
|
673
|
+
: "the Base blocks the proofs sign as their floors";
|
|
674
|
+
const blocks = cov.lowerFromAnchors ? "Base or Ethereum block" : "Base block";
|
|
675
|
+
return (
|
|
676
|
+
`${cov.lowerCovered.size} of ${cov.totalProofCount} observed proofs of this epoch were ` +
|
|
677
|
+
`committed no earlier than unix ${min.timestamp} (${min.floor ? `Base block ${min.blockNumber}` : `Ethereum block ${min.blockNumber ?? "(unnumbered)"}`}), ` +
|
|
678
|
+
`grounded in block-hash unpredictability through ${grounds}. This additionally assumes each ` +
|
|
679
|
+
`${blocks} is a genuine, publicly published one, which this offline audit cannot confirm.` +
|
|
680
|
+
weakerCaveat(min.weaker) +
|
|
681
|
+
` The remaining proofs sign no Base floor and follow no covering floor or anchor; they carry no ` +
|
|
682
|
+
`lower bound from this evidence. A floor is a lower bound only: it orders no epoch.`
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
|
|
460
686
|
function aggregateEpochs(
|
|
461
687
|
reconstruction: ReconstructionResult,
|
|
462
|
-
segments: TemporalSegment[]
|
|
688
|
+
segments: TemporalSegment[],
|
|
689
|
+
anchorLower: Map<string, SegmentBound[]>
|
|
463
690
|
): AnchorOrderedPair[] {
|
|
464
691
|
const coverage = new Map<string, EpochCoverage>();
|
|
465
692
|
for (const epoch of reconstruction.epochRelationships.epochs) {
|
|
466
693
|
coverage.set(epoch.epochId, {
|
|
467
694
|
totalProofCount: epoch.proofCount,
|
|
468
695
|
lowerCovered: new Set(),
|
|
696
|
+
lowerFromFloors: false,
|
|
697
|
+
lowerFromAnchors: false,
|
|
698
|
+
anchorLowerCovered: new Set(),
|
|
469
699
|
upperCovered: new Set(),
|
|
470
700
|
});
|
|
471
701
|
}
|
|
@@ -483,19 +713,25 @@ function aggregateEpochs(
|
|
|
483
713
|
// never marked weaker when a hash-link bound justifies the same time.
|
|
484
714
|
for (const bound of segment.lowerBounds) {
|
|
485
715
|
for (const h of segment.memberProofHashes) cov.lowerCovered.add(h);
|
|
486
|
-
if (
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
}
|
|
716
|
+
if (bound.source === "signed-floor") cov.lowerFromFloors = true;
|
|
717
|
+
else cov.lowerFromAnchors = true;
|
|
718
|
+
cov.lowerMin = foldLower(cov.lowerMin, bound);
|
|
719
|
+
}
|
|
720
|
+
// Anchor-only coverage, for the pairs. In a partition holding both
|
|
721
|
+
// anchors and floors a member's anchor bound may have been displaced
|
|
722
|
+
// from its segment by a tighter floor, so it is read per member there.
|
|
723
|
+
if (segment.memberProofHashes.some((h) => anchorLower.has(h))) {
|
|
724
|
+
for (const h of segment.memberProofHashes) {
|
|
725
|
+
for (const bound of anchorLower.get(h) ?? []) {
|
|
726
|
+
cov.anchorLowerCovered.add(h);
|
|
727
|
+
cov.anchorLowerMin = foldLower(cov.anchorLowerMin, bound);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
} else {
|
|
731
|
+
for (const bound of segment.lowerBounds) {
|
|
732
|
+
if (bound.source === "signed-floor") continue;
|
|
733
|
+
for (const h of segment.memberProofHashes) cov.anchorLowerCovered.add(h);
|
|
734
|
+
cov.anchorLowerMin = foldLower(cov.anchorLowerMin, bound);
|
|
499
735
|
}
|
|
500
736
|
}
|
|
501
737
|
// Maximum timestamp for not-after, same chain-link tie preference.
|
|
@@ -526,6 +762,7 @@ function aggregateEpochs(
|
|
|
526
762
|
bounds.push({
|
|
527
763
|
kind: "not-before",
|
|
528
764
|
anchorProofHash: cov.lowerMin.anchorProofHash,
|
|
765
|
+
...(cov.lowerMin.floor ? { source: "signed-floor" as const, chain: "base" as const } : {}),
|
|
529
766
|
...(cov.lowerMin.blockNumber !== undefined ? { blockNumber: cov.lowerMin.blockNumber } : {}),
|
|
530
767
|
blockHash: cov.lowerMin.blockHash,
|
|
531
768
|
witnessTimestamp: cov.lowerMin.timestamp,
|
|
@@ -535,15 +772,7 @@ function aggregateEpochs(
|
|
|
535
772
|
basis: "block-hash-unpredictability",
|
|
536
773
|
evidence: cov.lowerMin.evidence,
|
|
537
774
|
weaker: cov.lowerMin.weaker,
|
|
538
|
-
claim:
|
|
539
|
-
`${cov.lowerCovered.size} of ${cov.totalProofCount} observed proofs of this epoch were ` +
|
|
540
|
-
`committed no earlier than unix ${cov.lowerMin.timestamp}, grounded in block-hash ` +
|
|
541
|
-
`unpredictability through verified anchor witnesses. This additionally assumes the anchored ` +
|
|
542
|
-
`header is a genuine, publicly published Ethereum block, which this offline audit cannot ` +
|
|
543
|
-
`confirm.` +
|
|
544
|
-
weakerCaveat(cov.lowerMin.weaker) +
|
|
545
|
-
` The remaining proofs sit causally before the covering anchors and carry no lower bound ` +
|
|
546
|
-
`from this evidence.`,
|
|
775
|
+
claim: lowerClaim(cov),
|
|
547
776
|
});
|
|
548
777
|
}
|
|
549
778
|
if (cov.upperMax !== undefined) {
|
|
@@ -583,18 +812,20 @@ function aggregateEpochs(
|
|
|
583
812
|
for (const after of epochs) {
|
|
584
813
|
if (after.epochId === before.epochId) continue;
|
|
585
814
|
const covB = coverage.get(after.epochId) as EpochCoverage;
|
|
586
|
-
|
|
587
|
-
|
|
815
|
+
// The after side is read from anchor bounds alone: floors order no epoch.
|
|
816
|
+
const lowerB = covB.anchorLowerMin;
|
|
817
|
+
if (lowerB === undefined) continue;
|
|
818
|
+
if (covA.upperMax.timestamp >= lowerB.timestamp) continue;
|
|
588
819
|
const upperEvidence = covA.upperMax.evidence;
|
|
589
|
-
const lowerEvidence =
|
|
590
|
-
const weaker = covA.upperMax.weaker ||
|
|
820
|
+
const lowerEvidence = lowerB.evidence;
|
|
821
|
+
const weaker = covA.upperMax.weaker || lowerB.weaker;
|
|
591
822
|
pairs.push({
|
|
592
823
|
beforeEpochId: before.epochId,
|
|
593
824
|
afterEpochId: after.epochId,
|
|
594
825
|
upperAnchorProofHash: covA.upperMax.anchorProofHash,
|
|
595
826
|
upperBoundTimestamp: covA.upperMax.timestamp,
|
|
596
|
-
lowerAnchorProofHash:
|
|
597
|
-
lowerBoundTimestamp:
|
|
827
|
+
lowerAnchorProofHash: lowerB.anchorProofHash,
|
|
828
|
+
lowerBoundTimestamp: lowerB.timestamp,
|
|
598
829
|
basis: "anchor-bounds",
|
|
599
830
|
assumptionDependent: true,
|
|
600
831
|
upperEvidence,
|
|
@@ -602,15 +833,15 @@ function aggregateEpochs(
|
|
|
602
833
|
weaker,
|
|
603
834
|
beforeCoveredProofCount: covA.upperCovered.size,
|
|
604
835
|
beforeTotalProofCount: covA.totalProofCount,
|
|
605
|
-
afterCoveredProofCount: covB.
|
|
836
|
+
afterCoveredProofCount: covB.anchorLowerCovered.size,
|
|
606
837
|
afterTotalProofCount: covB.totalProofCount,
|
|
607
838
|
note:
|
|
608
839
|
`The anchor-covered portion of epoch ${before.epochId} ` +
|
|
609
840
|
`(${covA.upperCovered.size} of ${covA.totalProofCount} proofs) precedes the ` +
|
|
610
841
|
`anchor-covered portion of epoch ${after.epochId} ` +
|
|
611
|
-
`(${covB.
|
|
842
|
+
`(${covB.anchorLowerCovered.size} of ${covB.totalProofCount} proofs): the first is bounded ` +
|
|
612
843
|
`not-after unix ${covA.upperMax.timestamp} and the second not-before unix ` +
|
|
613
|
-
`${
|
|
844
|
+
`${lowerB.timestamp}. One-sided evidence about the covered portions only; the ` +
|
|
614
845
|
`not-after side rests on the anchor-freshness assumption documented on its bound.` +
|
|
615
846
|
(weaker
|
|
616
847
|
? ` At least one side rests on commit-counter ordering rather than a verified hash-link ` +
|