@mikeargento/bitgraph 1.11.0 → 1.12.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 +42 -32
- package/dist/export.d.ts +17 -7
- package/dist/export.d.ts.map +1 -1
- package/dist/export.js +102 -34
- package/dist/export.js.map +1 -1
- package/dist/fuse.d.ts +23 -12
- package/dist/fuse.d.ts.map +1 -1
- package/dist/fuse.js +37 -12
- package/dist/fuse.js.map +1 -1
- package/package.json +3 -3
- package/src/export.ts +102 -32
- package/src/fuse.ts +59 -24
package/src/fuse.ts
CHANGED
|
@@ -117,15 +117,36 @@ export interface AnchorMark {
|
|
|
117
117
|
blockHash: string;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/** The Base floor an enclave v10 allocation hands back: the one it signs at commit as commit.slotFloor. */
|
|
121
|
+
export interface BaseFloorMark {
|
|
122
|
+
chain: "base";
|
|
123
|
+
evmChainId: 8453;
|
|
124
|
+
blockNumber: number;
|
|
125
|
+
blockHash: string;
|
|
126
|
+
blockTimestamp: number;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Either floor an allocation can return. A Base floor makes a fuse/3 commitment, an Ethereum anchor a fuse/2 one. */
|
|
130
|
+
export type FloorMark = AnchorMark | BaseFloorMark;
|
|
131
|
+
|
|
132
|
+
export function isBaseFloorMark(x: unknown): x is BaseFloorMark {
|
|
133
|
+
if (x === null || typeof x !== "object" || Array.isArray(x)) return false;
|
|
134
|
+
const a = x as Record<string, unknown>;
|
|
135
|
+
return a.chain === "base" && a.evmChainId === 8453
|
|
136
|
+
&& typeof a.blockNumber === "number" && Number.isSafeInteger(a.blockNumber) && a.blockNumber > 0
|
|
137
|
+
&& typeof a.blockHash === "string" && /^0x[0-9a-f]{64}$/.test(a.blockHash)
|
|
138
|
+
&& typeof a.blockTimestamp === "number" && Number.isSafeInteger(a.blockTimestamp);
|
|
139
|
+
}
|
|
140
|
+
|
|
120
141
|
/** What the builder receives. The raw nonce is deliberately absent. */
|
|
121
142
|
export interface BuilderInput {
|
|
122
|
-
/** 32-byte commitment to the signed slot record (
|
|
143
|
+
/** 32-byte commitment to the signed slot record (fuse/2 and fuse/3 also bind the floor block). Write this into the artifact. */
|
|
123
144
|
commitment: Uint8Array;
|
|
124
145
|
commitmentHex: string;
|
|
125
|
-
/** Which commitment this is:
|
|
126
|
-
fuseVersion: 1 | 2;
|
|
127
|
-
/** The floor bound into
|
|
128
|
-
floor?:
|
|
146
|
+
/** Which commitment this is: 3 for a Base floor (enclave v10), 2 for an Ethereum floor anchor (v9), else 1. */
|
|
147
|
+
fuseVersion: 1 | 2 | 3;
|
|
148
|
+
/** The floor bound into the commitment, when there is one. */
|
|
149
|
+
floor?: FloorMark;
|
|
129
150
|
/** The origin digest, when the fused artifact names a source. */
|
|
130
151
|
originDigest?: Uint8Array;
|
|
131
152
|
/** The signed slot record, for producers that want to embed its fields. Contains the nonce: do not copy it into the artifact. */
|
|
@@ -368,11 +389,12 @@ function isAnchorMark(x: unknown): x is AnchorMark {
|
|
|
368
389
|
|
|
369
390
|
/**
|
|
370
391
|
* 1. nonce. The signed slot record from the boundary; it must sit on the
|
|
371
|
-
* anchored chain.
|
|
372
|
-
*
|
|
373
|
-
*
|
|
392
|
+
* anchored chain. The response also carries the floor the boundary will sign
|
|
393
|
+
* at commit: a Base block since enclave v10 (a fuse/3 commitment), an Ethereum
|
|
394
|
+
* anchor on v9 (fuse/2); without one the producer makes a fuse/1 commitment.
|
|
395
|
+
* A response carrying both is refused: a proof has one floor.
|
|
374
396
|
*/
|
|
375
|
-
async function allocateSlot(t: BoundTransport): Promise<{ slot: SlotAllocation; anchor:
|
|
397
|
+
async function allocateSlot(t: BoundTransport): Promise<{ slot: SlotAllocation; anchor: FloorMark | null }> {
|
|
376
398
|
const alloc = await request(t, t.allocatePath, { method: "POST", body: {} });
|
|
377
399
|
if (alloc.status === 503 && codeOf(alloc.json) === "tee-restarting") throw new FuseError("tee-restarting", messageOf(alloc.json, "the boundary is restarting"), 503);
|
|
378
400
|
if (alloc.status !== 200) throw new FuseError("allocate-failed", messageOf(alloc.json, `allocation failed (${alloc.status})`), alloc.status);
|
|
@@ -381,7 +403,11 @@ async function allocateSlot(t: BoundTransport): Promise<{ slot: SlotAllocation;
|
|
|
381
403
|
if (!isSlotRecord(slot) || slotId !== slot.nonceB64) throw new FuseError("allocate-failed", "the allocation response is not a slot record", alloc.status);
|
|
382
404
|
if (slot.chainId !== "bitgraph:main") throw new FuseError("allocate-failed", "the slot is not on the anchored chain; a fused floor needs bitgraph:main");
|
|
383
405
|
const anchorRaw = (alloc.json as { anchor?: unknown } | null)?.anchor;
|
|
384
|
-
|
|
406
|
+
const floorRaw = (alloc.json as { floor?: unknown } | null)?.floor;
|
|
407
|
+
const eth = isAnchorMark(anchorRaw) ? anchorRaw : null;
|
|
408
|
+
const base = isBaseFloorMark(floorRaw) ? floorRaw : null;
|
|
409
|
+
if (eth && base) throw new FuseError("allocate-failed", "the allocation returned two floors (an Ethereum anchor and a Base block); a proof has one, so nothing was bound");
|
|
410
|
+
return { slot, anchor: base ?? eth };
|
|
385
411
|
}
|
|
386
412
|
|
|
387
413
|
/**
|
|
@@ -492,7 +518,8 @@ export async function fuse(builder: FuseBuilder, options: FuseOptions): Promise<
|
|
|
492
518
|
chainId: "bitgraph:main",
|
|
493
519
|
attribution,
|
|
494
520
|
// fuse/2: the boundary checks the bound floor against its ledger before spending the slot.
|
|
495
|
-
|
|
521
|
+
// fuse/3: the bound Base floor, so the boundary can check it is the one it handed out.
|
|
522
|
+
...(version === 2 ? { anchor } : version === 3 ? { floor: anchor } : {}),
|
|
496
523
|
};
|
|
497
524
|
if (options.agency !== undefined) body.agency = options.agency;
|
|
498
525
|
const { proof, recovered } = await commitUnderSlot(t, body, artifactDigestB64, slot);
|
|
@@ -543,10 +570,10 @@ export type SetMemberPlacement = "trailer/1" | "container/1" | "container/2";
|
|
|
543
570
|
|
|
544
571
|
/** What a hashed member's fused digest is computed for: the held slot and its commitment. */
|
|
545
572
|
export interface FusedDigestInput {
|
|
546
|
-
/** Which commitment this is:
|
|
547
|
-
fuseVersion: 1 | 2;
|
|
548
|
-
/** The floor bound into
|
|
549
|
-
floor?:
|
|
573
|
+
/** Which commitment this is: 3 for a Base floor, 2 for an Ethereum floor anchor, else 1. */
|
|
574
|
+
fuseVersion: 1 | 2 | 3;
|
|
575
|
+
/** The floor bound into the commitment, when there is one. */
|
|
576
|
+
floor?: FloorMark;
|
|
550
577
|
commitment: Uint8Array;
|
|
551
578
|
commitmentHex: string;
|
|
552
579
|
slot: SlotAllocation;
|
|
@@ -887,7 +914,7 @@ export async function fuseSet(members: readonly FuseSetMember[], options: FuseSe
|
|
|
887
914
|
chainId: "bitgraph:main",
|
|
888
915
|
attribution: fuseAttribution(setKind === "set/1" ? SET_PLACEMENT_ID_LOCAL : SET2_PLACEMENT_ID, undefined, version),
|
|
889
916
|
metadata: { [SET_METADATA_KEY]: manifest },
|
|
890
|
-
...(version === 2 ? { anchor } : {}),
|
|
917
|
+
...(version === 2 ? { anchor } : version === 3 ? { floor: anchor } : {}),
|
|
891
918
|
};
|
|
892
919
|
if (options.agency !== undefined) body.agency = options.agency;
|
|
893
920
|
report("commit", 0, 1);
|
|
@@ -1099,10 +1126,10 @@ export interface FuseTreeResult {
|
|
|
1099
1126
|
count: number;
|
|
1100
1127
|
/** The tree's root, lowercase hex. */
|
|
1101
1128
|
rootHex: string;
|
|
1102
|
-
/** commitment/2, which every placed member's committed bytes carry. committedBytesFor(member.code, original, commitment) rebuilds them. */
|
|
1129
|
+
/** commitment/2 or /3, which every placed member's committed bytes carry. committedBytesFor(member.code, original, commitment) rebuilds them. */
|
|
1103
1130
|
commitment: Uint8Array;
|
|
1104
|
-
/** The floor block the commitment binds, as the proof signs it (commit.slotAnchor). */
|
|
1105
|
-
floor:
|
|
1131
|
+
/** The floor block the commitment binds, as the proof signs it (commit.slotAnchor or commit.slotFloor). */
|
|
1132
|
+
floor: FloorMark;
|
|
1106
1133
|
/** The spec hash the signed attribution pins, standard base64. */
|
|
1107
1134
|
specHashB64: string;
|
|
1108
1135
|
/** Every leaf, in tree order (strictly ascending artifact digest). */
|
|
@@ -1238,10 +1265,16 @@ export async function fuseTree(members: readonly FuseTreeMember[], options: Fuse
|
|
|
1238
1265
|
// 1. nonce: one slot for the whole tree, and the floor it binds.
|
|
1239
1266
|
const { slot, anchor } = await allocateSlot(t);
|
|
1240
1267
|
if (anchor === null) {
|
|
1241
|
-
throw new FuseError("floor-missing", "the allocation returned no floor
|
|
1268
|
+
throw new FuseError("floor-missing", "the allocation returned no floor, so no tree/1 commitment can be made (tree/1 binds the floor block: bitgraph-fuse/2 or /3, enclave v9 and later); nothing was committed and the position will expire");
|
|
1242
1269
|
}
|
|
1243
1270
|
const { commitment, version } = producerCommitment(slot, anchor);
|
|
1244
|
-
if (version
|
|
1271
|
+
if (version === 1) throw new FuseError("floor-missing", "the floor could not be bound into the commitment; nothing was committed and the position will expire");
|
|
1272
|
+
// The spec follows the floor: SPEC v1 defines tree/1 under fuse/2, SPEC v2 under fuse/3.
|
|
1273
|
+
try {
|
|
1274
|
+
specHash = currentTreeSpecHash(version);
|
|
1275
|
+
} catch (err) {
|
|
1276
|
+
throw new FuseError("bad-input", `no tree/1 spec hash to pin for bitgraph-fuse/${version}: ${err instanceof Error ? err.message : String(err)}; nothing was committed and the position will expire`);
|
|
1277
|
+
}
|
|
1245
1278
|
const commitmentHex = bytesToHex(commitment);
|
|
1246
1279
|
const expiring = "nothing was committed and the slot will expire";
|
|
1247
1280
|
|
|
@@ -1330,7 +1363,8 @@ export async function fuseTree(members: readonly FuseTreeMember[], options: Fuse
|
|
|
1330
1363
|
attribution: treeAttribution(specHash),
|
|
1331
1364
|
metadata: { [TREE_METADATA_KEY]: bytesToHex(rootDocument) },
|
|
1332
1365
|
// fuse/2: the boundary checks the bound floor against its ledger before spending the slot.
|
|
1333
|
-
|
|
1366
|
+
// fuse/3: the bound Base floor, so the boundary can check it is the one it handed out.
|
|
1367
|
+
...(version === 2 ? { anchor } : { floor: anchor }),
|
|
1334
1368
|
};
|
|
1335
1369
|
if (options.agency !== undefined) body.agency = options.agency;
|
|
1336
1370
|
report("commit", 0, 1);
|
|
@@ -1357,7 +1391,8 @@ export async function fuseTree(members: readonly FuseTreeMember[], options: Fuse
|
|
|
1357
1391
|
}
|
|
1358
1392
|
rootDocumentEchoed = true;
|
|
1359
1393
|
}
|
|
1360
|
-
|
|
1394
|
+
// The floor the proof signs: a Base block (fuse/3) or an Ethereum anchor (fuse/2).
|
|
1395
|
+
const floor: FloorMark = proof.commit.slotFloor ? { ...proof.commit.slotFloor } : proof.commit.slotAnchor!;
|
|
1361
1396
|
|
|
1362
1397
|
// Every member is bound to the verified root by its own path (the
|
|
1363
1398
|
// verifier's check, run here once per member); no member's bytes are read
|
|
@@ -1409,7 +1444,7 @@ export async function fuseTree(members: readonly FuseTreeMember[], options: Fuse
|
|
|
1409
1444
|
count,
|
|
1410
1445
|
rootHex: bytesToHex(built.root),
|
|
1411
1446
|
commitment,
|
|
1412
|
-
floor: { counter: floor.counter, blockNumber: floor.blockNumber, blockHash: floor.blockHash },
|
|
1447
|
+
floor: "chain" in floor ? { ...floor } : { counter: floor.counter, blockNumber: floor.blockNumber, blockHash: floor.blockHash },
|
|
1413
1448
|
specHashB64,
|
|
1414
1449
|
leaves: built.sorted,
|
|
1415
1450
|
tree: built.tree,
|