@mikeargento/bitgraph-player 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +32 -18
- package/dist/__tests__/base-floor-check.test.d.ts +2 -0
- package/dist/__tests__/base-floor-check.test.d.ts.map +1 -0
- package/dist/__tests__/base-floor-check.test.js +163 -0
- package/dist/__tests__/base-floor-check.test.js.map +1 -0
- package/dist/__tests__/cast-order.test.js +1 -1
- package/dist/__tests__/cast-order.test.js.map +1 -1
- package/dist/__tests__/check.test.js +2 -2
- package/dist/__tests__/check.test.js.map +1 -1
- package/dist/__tests__/evaluate-verdict.test.js +1 -1
- package/dist/__tests__/evaluate-verdict.test.js.map +1 -1
- package/dist/__tests__/fixtures.js +1 -1
- package/dist/__tests__/fixtures.js.map +1 -1
- package/dist/__tests__/fuse-check.test.js +2 -2
- package/dist/__tests__/fuse-check.test.js.map +1 -1
- package/dist/__tests__/init-play.test.js +1 -1
- package/dist/__tests__/init-play.test.js.map +1 -1
- package/dist/__tests__/logic-rule.test.js +1 -1
- package/dist/__tests__/logic-rule.test.js.map +1 -1
- package/dist/__tests__/sig-v2.test.js +1 -1
- package/dist/__tests__/sig-v2.test.js.map +1 -1
- package/dist/cast.js +1 -1
- package/dist/cast.js.map +1 -1
- package/dist/check.d.ts +58 -9
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +102 -32
- package/dist/check.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/evaluate.js +1 -1
- package/dist/evaluate.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/init.js +1 -1
- package/dist/init.js.map +1 -1
- package/dist/logic.js +1 -1
- package/dist/logic.js.map +1 -1
- package/dist/order.d.ts +4 -1
- package/dist/order.d.ts.map +1 -1
- package/dist/order.js +10 -1
- package/dist/order.js.map +1 -1
- package/dist/play.js +1 -1
- package/dist/play.js.map +1 -1
- package/dist/rule.js +1 -1
- package/dist/rule.js.map +1 -1
- package/dist/sig.js +1 -1
- package/dist/sig.js.map +1 -1
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/dist/verdict.d.ts +1 -1
- package/dist/verdict.js +2 -2
- package/dist/verdict.js.map +1 -1
- package/dist-web/verify.html +3 -3
- package/package.json +15 -7
- package/src/__tests__/base-floor-check.test.ts +187 -0
- package/src/__tests__/cast-order.test.ts +1 -1
- package/src/__tests__/check.test.ts +2 -2
- package/src/__tests__/evaluate-verdict.test.ts +1 -1
- package/src/__tests__/fixtures.ts +1 -1
- package/src/__tests__/fuse-check.test.ts +2 -2
- package/src/__tests__/init-play.test.ts +1 -1
- package/src/__tests__/logic-rule.test.ts +1 -1
- package/src/__tests__/sig-v2.test.ts +1 -1
- package/src/cast.ts +1 -1
- package/src/check.ts +167 -40
- package/src/cli.ts +1 -1
- package/src/evaluate.ts +1 -1
- package/src/index.ts +1 -1
- package/src/init.ts +1 -1
- package/src/logic.ts +1 -1
- package/src/order.ts +13 -2
- package/src/play.ts +1 -1
- package/src/rule.ts +1 -1
- package/src/sig.ts +1 -1
- package/src/types.ts +1 -1
- package/src/verdict.ts +2 -2
package/src/check.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Copyright (c) 2024-2026
|
|
1
|
+
// Copyright (c) 2024-2026 Argento Computing Inc. Licensed under the MIT License. See LICENSE.
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* `check`: what a bundle establishes about the recordings it holds, said
|
|
@@ -70,6 +70,43 @@ import type { ThreeValued } from "./types.js";
|
|
|
70
70
|
import { kleeneAll } from "./logic.js";
|
|
71
71
|
import { PLAYER_VERSION } from "./verdict.js";
|
|
72
72
|
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Base floors (enclave v10), read structurally
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* What an audit that reads Base floors adds (bitgraph-audit after 0.9.0:
|
|
79
|
+
* TemporalAnalysis.signedFloors and floorProblems, and source / chain /
|
|
80
|
+
* timeSource on a segment bound). Read structurally, so the Player builds and
|
|
81
|
+
* behaves exactly as before against an audit without them; every field is
|
|
82
|
+
* optional and absent means an Ethereum anchor bound.
|
|
83
|
+
* TODO(release): bump the bitgraph-audit dependency to the release that
|
|
84
|
+
* carries signedFloors, then these can be the audit's own exported types.
|
|
85
|
+
*/
|
|
86
|
+
export interface SignedFloorRecord {
|
|
87
|
+
proofHash: string;
|
|
88
|
+
chain: "base";
|
|
89
|
+
blockNumber: number;
|
|
90
|
+
blockHash: string;
|
|
91
|
+
blockTimestamp: number;
|
|
92
|
+
header: "checked" | "not-carried";
|
|
93
|
+
headerPath?: string;
|
|
94
|
+
bound: "not-before" | "withheld";
|
|
95
|
+
withheldReason?: string;
|
|
96
|
+
}
|
|
97
|
+
export interface FloorProblem {
|
|
98
|
+
code: string;
|
|
99
|
+
proofHash?: string;
|
|
100
|
+
path?: string;
|
|
101
|
+
message: string;
|
|
102
|
+
}
|
|
103
|
+
type FloorAwareBound = SegmentBound & { source?: "signed-floor"; chain?: "base"; timeSource?: "header" | "signed" };
|
|
104
|
+
|
|
105
|
+
function floorView(audit: AuditResult): { floors: SignedFloorRecord[]; problems: FloorProblem[] } {
|
|
106
|
+
const t = audit.temporal as { signedFloors?: SignedFloorRecord[]; floorProblems?: FloorProblem[] };
|
|
107
|
+
return { floors: t.signedFloors ?? [], problems: t.floorProblems ?? [] };
|
|
108
|
+
}
|
|
109
|
+
|
|
73
110
|
// ---------------------------------------------------------------------------
|
|
74
111
|
// Declared knowledge: published enclave measurements
|
|
75
112
|
// ---------------------------------------------------------------------------
|
|
@@ -114,7 +151,17 @@ export const KNOWN_ENCLAVE_MEASUREMENTS: ReadonlyArray<{ pcr0: string; label: st
|
|
|
114
151
|
{
|
|
115
152
|
pcr0: "eccfc1c78006f4b74f929c992785575c908a0f60eca08ff638cd6c0842f993f182ebb002457b8ef3e732a6a10805c72b",
|
|
116
153
|
label: "enclave v8 (reproducible, authenticated anchors and the floor gate)",
|
|
117
|
-
period: "2026-09-07
|
|
154
|
+
period: "2026-09-07 to 2026-09-30",
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
pcr0: "934feb8bb6f4f7e2d2f85d902a7d5edd0981f706d9d2385638988ac096a05ea0583c3d00eef2a7947865ec66efc1fcf8",
|
|
158
|
+
label: "enclave v9 (reproducible; the allocation returns the floor anchor, for bitgraph-fuse/2)",
|
|
159
|
+
period: "2026-09-30 to 2026-10-07",
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
pcr0: "5a947cc66095adcceefa9e5ece5d1416dfe08c3470df1bcaa5b2bc5267b0480e6cdc172fe077cd06b0afb07614307973",
|
|
163
|
+
label: "enclave v10 (reproducible; the floor is a Base block fixed at allocation, for bitgraph-fuse/3)",
|
|
164
|
+
period: "2026-10-08 onward",
|
|
118
165
|
},
|
|
119
166
|
];
|
|
120
167
|
|
|
@@ -129,15 +176,24 @@ export interface CheckLine {
|
|
|
129
176
|
detail: string;
|
|
130
177
|
}
|
|
131
178
|
|
|
132
|
-
/**
|
|
179
|
+
/**
|
|
180
|
+
* A verified bound on a recording: an Ethereum anchor's, from a witness in
|
|
181
|
+
* the bundle, or (enclave v10) the Base block a recording signs as its floor,
|
|
182
|
+
* which is a not-before only.
|
|
183
|
+
*/
|
|
133
184
|
export interface CheckBound {
|
|
185
|
+
/** Absent for Ethereum (an anchor); "base" for a Base floor (commit.slotFloor). */
|
|
186
|
+
chain?: "base";
|
|
134
187
|
blockNumber?: string;
|
|
135
188
|
blockHash: string;
|
|
136
|
-
/** Unix seconds from the verified block header. */
|
|
189
|
+
/** Unix seconds: from the verified block header, or for a Base floor without one, the time the proof signs. */
|
|
137
190
|
timestamp: number;
|
|
191
|
+
/** The anchor proof, or for a Base floor the recording that signs it. */
|
|
138
192
|
anchorProofHash: string;
|
|
139
|
-
evidence: "chain-link" | "counter-order";
|
|
193
|
+
evidence: "chain-link" | "counter-order" | "signed-floor";
|
|
140
194
|
weaker: boolean;
|
|
195
|
+
/** Base floors only: "header" checked in the bundle, or "signed" (confirming the block needs a Base lookup). */
|
|
196
|
+
timeSource?: "header" | "signed";
|
|
141
197
|
}
|
|
142
198
|
|
|
143
199
|
export interface CheckBounds {
|
|
@@ -148,18 +204,26 @@ export interface CheckBounds {
|
|
|
148
204
|
}
|
|
149
205
|
|
|
150
206
|
/**
|
|
151
|
-
* The wall-clock floor of a fused artifact
|
|
152
|
-
* block preceding its SLOT counter in
|
|
153
|
-
* evidence by construction (no hash path
|
|
154
|
-
*
|
|
207
|
+
* The wall-clock floor of a fused artifact. Ethereum floors (enclave v9 and
|
|
208
|
+
* before): the last verified anchored block preceding its SLOT counter in
|
|
209
|
+
* the same epoch chain, counter-order evidence by construction (no hash path
|
|
210
|
+
* reaches a slot record). Base floors (enclave v10): the block the recording
|
|
211
|
+
* itself signs as commit.slotFloor, which a bitgraph-fuse/3 commitment binds
|
|
212
|
+
* ("signed-floor"). Either is reported with the same "genuine block"
|
|
213
|
+
* assumption as every other bound.
|
|
155
214
|
*/
|
|
156
215
|
export interface CheckFloor {
|
|
216
|
+
/** Absent for Ethereum; "base" for the recording's own Base floor. */
|
|
217
|
+
chain?: "base";
|
|
157
218
|
blockNumber?: string;
|
|
158
219
|
blockHash: string;
|
|
159
|
-
/** Unix seconds from the verified block header. */
|
|
220
|
+
/** Unix seconds from the verified block header, or for a Base floor without one, the signed time. */
|
|
160
221
|
timestamp: number;
|
|
222
|
+
/** The anchor proof, or for a Base floor the recording itself. */
|
|
161
223
|
anchorProofHash: string;
|
|
162
|
-
evidence: "counter-order";
|
|
224
|
+
evidence: "counter-order" | "signed-floor";
|
|
225
|
+
/** Base floors only: "header" checked in the bundle, or "signed" (confirming the block needs a Base lookup). */
|
|
226
|
+
timeSource?: "header" | "signed";
|
|
163
227
|
}
|
|
164
228
|
|
|
165
229
|
/** What the bundle establishes about a recording marked fused (profile bitgraph-fuse/1, working name). */
|
|
@@ -332,6 +396,7 @@ export function buildCheckReport(audit: AuditResult, options?: CheckOptions): Ch
|
|
|
332
396
|
const attestationByHash = new Map<string, ProofAttestationRecord>(
|
|
333
397
|
audit.attestations.records.map((r) => [r.proofHash, r])
|
|
334
398
|
);
|
|
399
|
+
const floorByHash = new Map<string, SignedFloorRecord>(floorView(audit).floors.map((f) => [f.proofHash, f]));
|
|
335
400
|
const segmentByHash = new Map<string, TemporalSegment>();
|
|
336
401
|
for (const segment of audit.temporal.segments) {
|
|
337
402
|
for (const hash of segment.memberProofHashes) segmentByHash.set(hash, segment);
|
|
@@ -368,7 +433,8 @@ export function buildCheckReport(audit: AuditResult, options?: CheckOptions): Ch
|
|
|
368
433
|
artifactPathByProof.get(proof.proofHash),
|
|
369
434
|
webCrypto,
|
|
370
435
|
options?.fuseEvidence,
|
|
371
|
-
anchorByHash
|
|
436
|
+
anchorByHash,
|
|
437
|
+
floorByHash.get(proof.proofHash)
|
|
372
438
|
)
|
|
373
439
|
);
|
|
374
440
|
}
|
|
@@ -378,7 +444,7 @@ export function buildCheckReport(audit: AuditResult, options?: CheckOptions): Ch
|
|
|
378
444
|
sortByPosition(anchors);
|
|
379
445
|
const contradictions = collectContradictions(audit);
|
|
380
446
|
const notes = collectNotes(audit, recordings, anchors, options?.fuseOriginHexes);
|
|
381
|
-
const notChecked = collectNotChecked(anchors.length > 0);
|
|
447
|
+
const notChecked = collectNotChecked(anchors.length > 0, floorByHash.size > 0);
|
|
382
448
|
|
|
383
449
|
const allLines: ThreeValued[] = [
|
|
384
450
|
...recordings.map((r) => r.result),
|
|
@@ -432,7 +498,8 @@ function buildRecording(
|
|
|
432
498
|
filePath: string | undefined,
|
|
433
499
|
webCrypto: boolean,
|
|
434
500
|
fuseEvidence?: ReadonlyMap<string, FuseVerifyResult>,
|
|
435
|
-
anchorByHash?: ReadonlyMap<string, AnchorRecord
|
|
501
|
+
anchorByHash?: ReadonlyMap<string, AnchorRecord>,
|
|
502
|
+
signedFloor?: SignedFloorRecord
|
|
436
503
|
): CheckRecording {
|
|
437
504
|
const lines: CheckLine[] = [];
|
|
438
505
|
const v = proof.verification;
|
|
@@ -488,12 +555,13 @@ function buildRecording(
|
|
|
488
555
|
// fused: the proof's signed attribution marks a fused artifact (profile
|
|
489
556
|
// bitgraph-fuse/1). The commitment check runs over the fused bytes, or
|
|
490
557
|
// over the original by reconstruction; its floor is the last verified
|
|
491
|
-
// anchor preceding the SLOT, not the commit
|
|
558
|
+
// anchor preceding the SLOT, not the commit, or for a recording that signs
|
|
559
|
+
// a Base floor (enclave v10, bitgraph-fuse/3) that block itself.
|
|
492
560
|
const marker = readFuseAttribution(proof.proof);
|
|
493
561
|
let fused: CheckFused | undefined;
|
|
494
562
|
if (marker !== null) {
|
|
495
|
-
const floor = fusedFloor(segment, anchorByHash, proof.slotCounter, proof.chainId);
|
|
496
|
-
const built = fusedLine(marker.placement, marker.originDigest !== undefined ? proof.proof.attribution?.message ?? null : null, fuseEvidence?.get(proof.proofHash), floor, proof);
|
|
563
|
+
const floor = signedFloor !== undefined ? ownBaseFloor(signedFloor) : fusedFloor(segment, anchorByHash, proof.slotCounter, proof.chainId);
|
|
564
|
+
const built = fusedLine(marker.placement, marker.originDigest !== undefined ? proof.proof.attribution?.message ?? null : null, fuseEvidence?.get(proof.proofHash), floor, proof, signedFloor);
|
|
497
565
|
lines.push(built.line);
|
|
498
566
|
fused = built.fused;
|
|
499
567
|
}
|
|
@@ -511,7 +579,7 @@ function buildRecording(
|
|
|
511
579
|
...(filePath !== undefined && v?.tier === "full" ? { filePath } : {}),
|
|
512
580
|
lines,
|
|
513
581
|
result,
|
|
514
|
-
bounds: boundsFor(segment),
|
|
582
|
+
bounds: boundsFor(segment, signedFloor),
|
|
515
583
|
...(fused !== undefined ? { fused } : {}),
|
|
516
584
|
};
|
|
517
585
|
}
|
|
@@ -556,11 +624,29 @@ export function fusedFloor(
|
|
|
556
624
|
};
|
|
557
625
|
}
|
|
558
626
|
|
|
627
|
+
/**
|
|
628
|
+
* The fused floor of a recording that signs a Base floor (enclave v10): the
|
|
629
|
+
* block itself, which a bitgraph-fuse/3 commitment binds. Null when the audit
|
|
630
|
+
* withheld it as a bound (the reason is in the floor detail).
|
|
631
|
+
*/
|
|
632
|
+
export function ownBaseFloor(floor: SignedFloorRecord): CheckFloor | null {
|
|
633
|
+
if (floor.bound !== "not-before") return null;
|
|
634
|
+
return {
|
|
635
|
+
chain: "base",
|
|
636
|
+
blockNumber: String(floor.blockNumber),
|
|
637
|
+
blockHash: floor.blockHash,
|
|
638
|
+
timestamp: floor.blockTimestamp,
|
|
639
|
+
anchorProofHash: floor.proofHash,
|
|
640
|
+
evidence: "signed-floor",
|
|
641
|
+
timeSource: floor.header === "checked" ? "header" : "signed",
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
559
645
|
const FLOOR_UNDETERMINED = "floor undetermined: no anchor precedes this slot in its epoch";
|
|
560
646
|
|
|
561
647
|
function floorClause(floor: CheckFloor): string {
|
|
562
648
|
const block = floor.blockNumber !== undefined ? `#${floor.blockNumber}` : floor.blockHash;
|
|
563
|
-
return
|
|
649
|
+
return `${floor.chain === "base" ? "Base" : "anchored"} block ${block} (${new Date(floor.timestamp * 1000).toISOString()})`;
|
|
564
650
|
}
|
|
565
651
|
|
|
566
652
|
function fusedLine(
|
|
@@ -568,13 +654,19 @@ function fusedLine(
|
|
|
568
654
|
originMessage: string | null,
|
|
569
655
|
ev: FuseVerifyResult | undefined,
|
|
570
656
|
floor: CheckFloor | null,
|
|
571
|
-
proof: ObservedProof
|
|
657
|
+
proof: ObservedProof,
|
|
658
|
+
signedFloor?: SignedFloorRecord
|
|
572
659
|
): { line: CheckLine; fused: CheckFused } {
|
|
573
660
|
const slot = proof.slotCounter ?? "?";
|
|
574
661
|
const where = placement !== null ? ` (placement ${placement})` : " (placement undeclared)";
|
|
575
|
-
const floorDetail = floor !== null
|
|
662
|
+
const floorDetail = floor !== null && floor.chain === "base"
|
|
663
|
+
? `assembled after ${floorClause(floor)}, the Base block this recording signs as its floor` +
|
|
664
|
+
(floor.timeSource === "signed" ? "; no header for it is in this bundle, so confirming the block needs a Base lookup" : "")
|
|
665
|
+
: floor !== null
|
|
576
666
|
? `assembled after ${floorClause(floor)}, the last verified anchor preceding slot ${slot} in this epoch`
|
|
577
|
-
:
|
|
667
|
+
: signedFloor !== undefined
|
|
668
|
+
? `floor undetermined: the Base floor this recording signs is not used as a bound (${signedFloor.withheldReason ?? "withheld"})`
|
|
669
|
+
: proof.chainId !== ANCHORED_CHAIN
|
|
578
670
|
? `floor undetermined: the slot is on chain "${proof.chainId}", not the anchored chain`
|
|
579
671
|
: FLOOR_UNDETERMINED;
|
|
580
672
|
const span = ev?.span !== undefined && ev.span !== null
|
|
@@ -708,9 +800,13 @@ function enclaveLine(attestedPcr0: string | undefined, attestationResult: ThreeV
|
|
|
708
800
|
};
|
|
709
801
|
}
|
|
710
802
|
|
|
711
|
-
function boundsFor(segment: TemporalSegment | undefined): CheckBounds {
|
|
803
|
+
function boundsFor(segment: TemporalSegment | undefined, signedFloor?: SignedFloorRecord): CheckBounds {
|
|
804
|
+
const withheld =
|
|
805
|
+
signedFloor !== undefined && signedFloor.bound === "withheld"
|
|
806
|
+
? `; the Base floor it signs is not used as a bound (${signedFloor.withheldReason ?? "withheld"})`
|
|
807
|
+
: "";
|
|
712
808
|
if (segment === undefined) {
|
|
713
|
-
return { status: "unanchored", detail:
|
|
809
|
+
return { status: "unanchored", detail: `no verified Ethereum anchor bounds this recording in this bundle${withheld}` };
|
|
714
810
|
}
|
|
715
811
|
const lower = tightest(segment.lowerBounds, "not-before");
|
|
716
812
|
const upper = tightest(segment.upperBounds, "not-after");
|
|
@@ -726,8 +822,8 @@ function boundsFor(segment: TemporalSegment | undefined): CheckBounds {
|
|
|
726
822
|
: "unanchored";
|
|
727
823
|
const detail =
|
|
728
824
|
status === "unanchored"
|
|
729
|
-
?
|
|
730
|
-
: `recorded ${boundsPhrase(status, notBefore, notAfter)}`;
|
|
825
|
+
? `no verified Ethereum anchor bounds this recording in this bundle${withheld}`
|
|
826
|
+
: `recorded ${boundsPhrase(status, notBefore, notAfter)}${withheld}`;
|
|
731
827
|
return {
|
|
732
828
|
status,
|
|
733
829
|
...(notBefore !== undefined ? { notBefore } : {}),
|
|
@@ -751,29 +847,43 @@ function boundsPhrase(
|
|
|
751
847
|
switch (status) {
|
|
752
848
|
case "lower-bounded-with-following-anchor":
|
|
753
849
|
return (
|
|
754
|
-
|
|
850
|
+
`${afterBlock(notBefore as CheckBound)}; precedes an anchor that consumed block ${blockRef(notAfter as CheckBound)} (header verified), an anchor-latency assumption rather than an upper bound on this recording` +
|
|
755
851
|
weakerSuffix((notBefore as CheckBound).weaker || (notAfter as CheckBound).weaker)
|
|
756
852
|
);
|
|
757
853
|
case "lower-bounded":
|
|
758
|
-
return
|
|
854
|
+
return `${afterBlock(notBefore as CheckBound)}; no verified upper bound here` + weakerSuffix((notBefore as CheckBound).weaker);
|
|
759
855
|
case "upper-bounded":
|
|
760
|
-
return `
|
|
856
|
+
return `with no verified lower bound in this bundle; an anchor that consumed block ${blockRef(notAfter as CheckBound)} followed (header verified), which is not an upper bound` + weakerSuffix((notAfter as CheckBound).weaker);
|
|
761
857
|
default:
|
|
762
858
|
return "with no verified Ethereum bound in this bundle";
|
|
763
859
|
}
|
|
764
860
|
}
|
|
765
861
|
|
|
862
|
+
/**
|
|
863
|
+
* "after Ethereum block N (header verified in this bundle)", or for a Base
|
|
864
|
+
* floor (enclave v10) "after Base block N" with where its time comes from.
|
|
865
|
+
*/
|
|
866
|
+
function afterBlock(b: CheckBound): string {
|
|
867
|
+
if (b.chain !== "base") return `after Ethereum block ${blockRef(b)} (header verified in this bundle)`;
|
|
868
|
+
return b.timeSource === "header"
|
|
869
|
+
? `after Base block ${blockRef(b)} (its floor, header checked in this bundle)`
|
|
870
|
+
: `after Base block ${blockRef(b)} (its floor, at the time the proof signs; confirming the block needs a Base lookup)`;
|
|
871
|
+
}
|
|
872
|
+
|
|
766
873
|
/**
|
|
767
874
|
* The tightest bound of a kind: for not-before the LARGEST block number
|
|
768
875
|
* (latest verified anchor known to precede), for not-after the SMALLEST.
|
|
876
|
+
* Block numbers compare only within one chain: across Ethereum and Base
|
|
877
|
+
* (enclave v10 floors) the block times compare instead.
|
|
769
878
|
* Ties resolve by preferring chain-link evidence over counter-order.
|
|
770
879
|
*/
|
|
771
|
-
function tightest(bounds:
|
|
880
|
+
function tightest(bounds: FloorAwareBound[], kind: "not-before" | "not-after"): FloorAwareBound | undefined {
|
|
772
881
|
const ofKind = bounds.filter((b) => b.kind === kind);
|
|
773
882
|
if (ofKind.length === 0) return undefined;
|
|
774
883
|
const sorted = [...ofKind].sort((a, b) => {
|
|
775
|
-
const
|
|
776
|
-
const
|
|
884
|
+
const sameChain = a.chain === b.chain;
|
|
885
|
+
const an = sameChain && a.blockNumber !== undefined ? BigInt(a.blockNumber) : BigInt(a.timestamp);
|
|
886
|
+
const bn = sameChain && b.blockNumber !== undefined ? BigInt(b.blockNumber) : BigInt(b.timestamp);
|
|
777
887
|
if (an !== bn) return kind === "not-before" ? (an > bn ? -1 : 1) : an < bn ? -1 : 1;
|
|
778
888
|
if (a.weaker !== b.weaker) return a.weaker ? 1 : -1;
|
|
779
889
|
return 0;
|
|
@@ -781,14 +891,16 @@ function tightest(bounds: SegmentBound[], kind: "not-before" | "not-after"): Seg
|
|
|
781
891
|
return sorted[0];
|
|
782
892
|
}
|
|
783
893
|
|
|
784
|
-
function toBound(b:
|
|
894
|
+
function toBound(b: FloorAwareBound): CheckBound {
|
|
785
895
|
return {
|
|
896
|
+
...(b.chain === "base" ? { chain: "base" as const } : {}),
|
|
786
897
|
...(b.blockNumber !== undefined ? { blockNumber: b.blockNumber } : {}),
|
|
787
898
|
blockHash: b.blockHash,
|
|
788
899
|
timestamp: b.timestamp,
|
|
789
900
|
anchorProofHash: b.anchorProofHash,
|
|
790
901
|
evidence: b.evidence,
|
|
791
902
|
weaker: b.weaker,
|
|
903
|
+
...(b.timeSource !== undefined ? { timeSource: b.timeSource } : {}),
|
|
792
904
|
};
|
|
793
905
|
}
|
|
794
906
|
|
|
@@ -796,16 +908,20 @@ function toBound(b: SegmentBound): CheckBound {
|
|
|
796
908
|
function shortBoundsPhrase(b: CheckBounds): string {
|
|
797
909
|
switch (b.status) {
|
|
798
910
|
case "lower-bounded-with-following-anchor":
|
|
799
|
-
return `after
|
|
911
|
+
return `after ${chainWord(b.notBefore as CheckBound)} block ${blockRef(b.notBefore as CheckBound)}, then an anchor at block ${blockRef(b.notAfter as CheckBound)}`;
|
|
800
912
|
case "lower-bounded":
|
|
801
|
-
return `after
|
|
913
|
+
return `after ${chainWord(b.notBefore as CheckBound)} block ${blockRef(b.notBefore as CheckBound)}`;
|
|
802
914
|
case "upper-bounded":
|
|
803
|
-
return `
|
|
915
|
+
return `with no lower bound in this bundle, then an anchor at block ${blockRef(b.notAfter as CheckBound)} (not an upper bound)`;
|
|
804
916
|
default:
|
|
805
917
|
return "with no Ethereum bound in this bundle";
|
|
806
918
|
}
|
|
807
919
|
}
|
|
808
920
|
|
|
921
|
+
function chainWord(b: CheckBound): string {
|
|
922
|
+
return b.chain === "base" ? "Base" : "Ethereum";
|
|
923
|
+
}
|
|
924
|
+
|
|
809
925
|
function blockRef(b: CheckBound): string {
|
|
810
926
|
return b.blockNumber !== undefined ? b.blockNumber : b.blockHash;
|
|
811
927
|
}
|
|
@@ -910,6 +1026,12 @@ function collectContradictions(audit: AuditResult): CheckLine[] {
|
|
|
910
1026
|
push(`${finding.code}: ${finding.message}`);
|
|
911
1027
|
}
|
|
912
1028
|
}
|
|
1029
|
+
// A floor problem (enclave v10 floors): a recording that signs two floors,
|
|
1030
|
+
// a malformed or off-schedule Base floor, a floor header that contradicts
|
|
1031
|
+
// the signed floor or matches none. Evidence that contradicts itself.
|
|
1032
|
+
for (const problem of floorView(audit).problems) {
|
|
1033
|
+
push(`${problem.code}: ${problem.message}${problem.path !== undefined ? ` (${problem.path})` : ""}`);
|
|
1034
|
+
}
|
|
913
1035
|
// An embedded proofHash that does not match the recomputed one: the
|
|
914
1036
|
// stored proof file was altered after the ledger wrote it, or is not
|
|
915
1037
|
// what it claims.
|
|
@@ -937,7 +1059,7 @@ function collectNotes(audit: AuditResult, recordings: CheckRecording[], anchors:
|
|
|
937
1059
|
}
|
|
938
1060
|
if (missingPositions > 0n) {
|
|
939
1061
|
notes.push(
|
|
940
|
-
`${missingPositions} causal position${missingPositions === 1n ? "" : "s"} between the earliest and latest recording here ${missingPositions === 1n ? "is" : "are"} not in this bundle: normal for an export, which is an excerpt of the chain;
|
|
1062
|
+
`${missingPositions} causal position${missingPositions === 1n ? "" : "s"} between the earliest and latest recording here ${missingPositions === 1n ? "is" : "are"} not in this bundle: normal for an export, which is an excerpt of the chain; the positions between belong to whoever holds those recordings`
|
|
941
1063
|
);
|
|
942
1064
|
}
|
|
943
1065
|
if (predecessorsAbsent > 0) {
|
|
@@ -951,7 +1073,7 @@ function collectNotes(audit: AuditResult, recordings: CheckRecording[], anchors:
|
|
|
951
1073
|
const originals = audit.ingest.artifacts.filter((a) => a.matchedProofHashes.length === 0 && fuseOriginHexes?.has(a.sha256Hex) === true);
|
|
952
1074
|
if (originals.length > 0) {
|
|
953
1075
|
notes.push(
|
|
954
|
-
`${originals.length} file${originals.length === 1 ? "" : "s"} in this bundle ${originals.length === 1 ? "is" : "are"} the original of a fused recording: ${originals.slice(0, 5).map((a) => a.paths[0] ?? "(unnamed)").join(", ")}${originals.length > 5 ? ", …" : ""}. The original
|
|
1076
|
+
`${originals.length} file${originals.length === 1 ? "" : "s"} in this bundle ${originals.length === 1 ? "is" : "are"} the original of a fused recording: ${originals.slice(0, 5).map((a) => a.paths[0] ?? "(unnamed)").join(", ")}${originals.length > 5 ? ", …" : ""}. The original carries no floor of its own: the commit that consumed its digest shows it existed by that position, an order, not a time ceiling. The fused bytes carry the floor`
|
|
955
1077
|
);
|
|
956
1078
|
}
|
|
957
1079
|
const unmatched = audit.ingest.artifacts.filter((a) => a.matchedProofHashes.length === 0 && fuseOriginHexes?.has(a.sha256Hex) !== true);
|
|
@@ -995,15 +1117,20 @@ function collectNotes(audit: AuditResult, recordings: CheckRecording[], anchors:
|
|
|
995
1117
|
return notes;
|
|
996
1118
|
}
|
|
997
1119
|
|
|
998
|
-
function collectNotChecked(hasAnchors: boolean): string[] {
|
|
1120
|
+
function collectNotChecked(hasAnchors: boolean, hasBaseFloors = false): string[] {
|
|
999
1121
|
const out: string[] = [];
|
|
1000
1122
|
if (hasAnchors) {
|
|
1001
1123
|
out.push(
|
|
1002
1124
|
"whether the anchored Ethereum blocks are canonical: their headers are recomputed here, but canonicality needs an Ethereum node or a block explorer"
|
|
1003
1125
|
);
|
|
1004
1126
|
}
|
|
1127
|
+
if (hasBaseFloors) {
|
|
1128
|
+
out.push(
|
|
1129
|
+
"whether the Base floor blocks are Base's own: a header in the bundle is checked here against the signed block, but that the block is on Base's chain needs a Base node or a block explorer"
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1005
1132
|
out.push(
|
|
1006
|
-
"whether the
|
|
1133
|
+
"whether the anchors in this bundle are the ones BitGraph published: this is an offline check; the published anchors are listed at bitgraph.ing/ledger"
|
|
1007
1134
|
);
|
|
1008
1135
|
out.push("whether an epoch key was later quarantined: that is published outside any bundle");
|
|
1009
1136
|
return out;
|
package/src/cli.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Copyright (c) 2024-2026
|
|
2
|
+
// Copyright (c) 2024-2026 Argento Computing Inc. Licensed under the MIT License. See LICENSE.
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* bitgraph-play <rule.json> <bundle> [--out <file>] [--summary]
|
package/src/evaluate.ts
CHANGED
package/src/index.ts
CHANGED
package/src/init.ts
CHANGED
package/src/logic.ts
CHANGED
package/src/order.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Copyright (c) 2024-2026
|
|
1
|
+
// Copyright (c) 2024-2026 Argento Computing Inc. Licensed under the MIT License. See LICENSE.
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The ordering adapter. Player does not reimplement causal reconstruction —
|
|
@@ -31,7 +31,10 @@
|
|
|
31
31
|
* - "anchor-bounds": strict block separation between verified segment
|
|
32
32
|
* bounds; always assumption-dependent (anchor freshness). If BOTH
|
|
33
33
|
* directions satisfy strict separation the evidence is contradictory
|
|
34
|
-
* and answers nothing.
|
|
34
|
+
* and answers nothing. Only ANCHOR bounds take part: a Base floor
|
|
35
|
+
* (enclave v10, a segment bound with source "signed-floor") is a
|
|
36
|
+
* not-before for its own recording and orders nothing across epochs,
|
|
37
|
+
* and its block numbers are Base's, never comparable to Ethereum's.
|
|
35
38
|
* Epoch pairs supported by neither are concurrent-or-unordered, and the
|
|
36
39
|
* honest answer is "unordered", never a coin flip.
|
|
37
40
|
*/
|
|
@@ -215,13 +218,21 @@ function boundsStrictlyOrdered(upper: SegmentBound, lower: SegmentBound): boolea
|
|
|
215
218
|
return upper.timestamp < lower.timestamp;
|
|
216
219
|
}
|
|
217
220
|
|
|
221
|
+
/** A Base floor bound (enclave v10; audit field source "signed-floor"), read structurally. */
|
|
222
|
+
function isFloorBound(b: SegmentBound): boolean {
|
|
223
|
+
return (b as { source?: string }).source !== undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
218
226
|
function anchorPrecedes(
|
|
219
227
|
firstSegment: TemporalSegment,
|
|
220
228
|
secondSegment: TemporalSegment
|
|
221
229
|
): { weaker: boolean; upper: SegmentBound; lower: SegmentBound } | undefined {
|
|
222
230
|
let best: { weaker: boolean; upper: SegmentBound; lower: SegmentBound } | undefined;
|
|
223
231
|
for (const upper of firstSegment.upperBounds) {
|
|
232
|
+
if (isFloorBound(upper)) continue;
|
|
224
233
|
for (const lower of secondSegment.lowerBounds) {
|
|
234
|
+
// Anchor bounds only: a Base floor orders no epoch.
|
|
235
|
+
if (isFloorBound(lower)) continue;
|
|
225
236
|
if (!boundsStrictlyOrdered(upper, lower)) continue;
|
|
226
237
|
const weaker = upper.weaker || lower.weaker;
|
|
227
238
|
if (best === undefined || (best.weaker && !weaker)) {
|
package/src/play.ts
CHANGED
package/src/rule.ts
CHANGED
package/src/sig.ts
CHANGED
package/src/types.ts
CHANGED
package/src/verdict.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Copyright (c) 2024-2026
|
|
1
|
+
// Copyright (c) 2024-2026 Argento Computing Inc. Licensed under the MIT License. See LICENSE.
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Verdict construction and serialization.
|
|
@@ -31,7 +31,7 @@ import type { DeclaredEntry, Resolution, Rule, Verdict } from "./types.js";
|
|
|
31
31
|
* A unit test asserts this equals package.json's version, so the
|
|
32
32
|
* constant cannot drift silently across releases.
|
|
33
33
|
*/
|
|
34
|
-
export const PLAYER_VERSION = "0.
|
|
34
|
+
export const PLAYER_VERSION = "0.16.0";
|
|
35
35
|
|
|
36
36
|
/** The player package's own version. */
|
|
37
37
|
export function playerVersion(): string {
|