@unconfirmed/kei 0.0.1 → 0.0.2
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/bcs.d.ts +96 -0
- package/dist/bitmap.d.ts +11 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +433 -0
- package/dist/committee.d.ts +27 -0
- package/dist/digest.d.ts +10 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +318 -0
- package/dist/parse.d.ts +9 -0
- package/dist/types.d.ts +63 -0
- package/dist/verify.d.ts +47 -0
- package/package.json +4 -3
- package/src/cli.ts +0 -195
package/dist/bcs.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BCS schemas for Sui checkpoint types.
|
|
3
|
+
* Field order matches the Rust struct declarations exactly.
|
|
4
|
+
*/
|
|
5
|
+
export declare const bcsCheckpointSummary: import("@mysten/bcs").BcsStruct<{
|
|
6
|
+
epoch: import("@mysten/bcs").BcsType<string, string | number | bigint, "u64">;
|
|
7
|
+
sequenceNumber: import("@mysten/bcs").BcsType<string, string | number | bigint, "u64">;
|
|
8
|
+
networkTotalTransactions: import("@mysten/bcs").BcsType<string, string | number | bigint, "u64">;
|
|
9
|
+
contentDigest: import("@mysten/bcs").BcsType<number[], Iterable<number> & {
|
|
10
|
+
length: number;
|
|
11
|
+
}, "vector<u8>">;
|
|
12
|
+
previousDigest: import("@mysten/bcs").BcsType<number[] | null, (Iterable<number> & {
|
|
13
|
+
length: number;
|
|
14
|
+
}) | null | undefined, "Option<vector<u8>>">;
|
|
15
|
+
epochRollingGasCostSummary: import("@mysten/bcs").BcsStruct<{
|
|
16
|
+
computationCost: import("@mysten/bcs").BcsType<string, string | number | bigint, "u64">;
|
|
17
|
+
storageCost: import("@mysten/bcs").BcsType<string, string | number | bigint, "u64">;
|
|
18
|
+
storageRebate: import("@mysten/bcs").BcsType<string, string | number | bigint, "u64">;
|
|
19
|
+
nonRefundableStorageFee: import("@mysten/bcs").BcsType<string, string | number | bigint, "u64">;
|
|
20
|
+
}, string>;
|
|
21
|
+
timestampMs: import("@mysten/bcs").BcsType<string, string | number | bigint, "u64">;
|
|
22
|
+
checkpointCommitments: import("@mysten/bcs").BcsType<import("@mysten/bcs").EnumOutputShapeWithKeys<{
|
|
23
|
+
ECMHLiveObjectSetDigest: number[];
|
|
24
|
+
CheckpointArtifactsDigest: number[];
|
|
25
|
+
}, "ECMHLiveObjectSetDigest" | "CheckpointArtifactsDigest">[], Iterable<import("@mysten/bcs").EnumInputShape<{
|
|
26
|
+
ECMHLiveObjectSetDigest: Iterable<number> & {
|
|
27
|
+
length: number;
|
|
28
|
+
};
|
|
29
|
+
CheckpointArtifactsDigest: Iterable<number> & {
|
|
30
|
+
length: number;
|
|
31
|
+
};
|
|
32
|
+
}>> & {
|
|
33
|
+
length: number;
|
|
34
|
+
}, string>;
|
|
35
|
+
endOfEpochData: import("@mysten/bcs").BcsType<{
|
|
36
|
+
nextEpochCommittee: [number[], string][];
|
|
37
|
+
nextEpochProtocolVersion: string;
|
|
38
|
+
epochCommitments: import("@mysten/bcs").EnumOutputShapeWithKeys<{
|
|
39
|
+
ECMHLiveObjectSetDigest: number[];
|
|
40
|
+
CheckpointArtifactsDigest: number[];
|
|
41
|
+
}, "ECMHLiveObjectSetDigest" | "CheckpointArtifactsDigest">[];
|
|
42
|
+
} | null, {
|
|
43
|
+
nextEpochCommittee: Iterable<readonly [Iterable<number> & {
|
|
44
|
+
length: number;
|
|
45
|
+
}, string | number | bigint]> & {
|
|
46
|
+
length: number;
|
|
47
|
+
};
|
|
48
|
+
nextEpochProtocolVersion: string | number | bigint;
|
|
49
|
+
epochCommitments: Iterable<import("@mysten/bcs").EnumInputShape<{
|
|
50
|
+
ECMHLiveObjectSetDigest: Iterable<number> & {
|
|
51
|
+
length: number;
|
|
52
|
+
};
|
|
53
|
+
CheckpointArtifactsDigest: Iterable<number> & {
|
|
54
|
+
length: number;
|
|
55
|
+
};
|
|
56
|
+
}>> & {
|
|
57
|
+
length: number;
|
|
58
|
+
};
|
|
59
|
+
} | null | undefined, `Option<${string}>`>;
|
|
60
|
+
versionSpecificData: import("@mysten/bcs").BcsType<number[], Iterable<number> & {
|
|
61
|
+
length: number;
|
|
62
|
+
}, string>;
|
|
63
|
+
}, string>;
|
|
64
|
+
export declare const bcsCheckpointContents: import("@mysten/bcs").BcsEnum<{
|
|
65
|
+
V1: import("@mysten/bcs").BcsStruct<{
|
|
66
|
+
transactions: import("@mysten/bcs").BcsType<{
|
|
67
|
+
transaction: number[];
|
|
68
|
+
effects: number[];
|
|
69
|
+
}[], Iterable<{
|
|
70
|
+
transaction: Iterable<number> & {
|
|
71
|
+
length: number;
|
|
72
|
+
};
|
|
73
|
+
effects: Iterable<number> & {
|
|
74
|
+
length: number;
|
|
75
|
+
};
|
|
76
|
+
}> & {
|
|
77
|
+
length: number;
|
|
78
|
+
}, string>;
|
|
79
|
+
userSignatures: import("@mysten/bcs").BcsType<number[][][], Iterable<Iterable<Iterable<number> & {
|
|
80
|
+
length: number;
|
|
81
|
+
}> & {
|
|
82
|
+
length: number;
|
|
83
|
+
}> & {
|
|
84
|
+
length: number;
|
|
85
|
+
}, string>;
|
|
86
|
+
}, string>;
|
|
87
|
+
}, "CheckpointContents">;
|
|
88
|
+
export declare const bcsAuthorityQuorumSignInfo: import("@mysten/bcs").BcsStruct<{
|
|
89
|
+
epoch: import("@mysten/bcs").BcsType<string, string | number | bigint, "u64">;
|
|
90
|
+
signature: import("@mysten/bcs").BcsType<number[], Iterable<number> & {
|
|
91
|
+
length: number;
|
|
92
|
+
}, string>;
|
|
93
|
+
signersMap: import("@mysten/bcs").BcsType<number[], Iterable<number> & {
|
|
94
|
+
length: number;
|
|
95
|
+
}, string>;
|
|
96
|
+
}, string>;
|
package/dist/bitmap.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal RoaringBitmap decoder for Sui's validator signers bitmap.
|
|
3
|
+
*
|
|
4
|
+
* Supports the standard portable serialization format:
|
|
5
|
+
* - Cookie 12346: array and bitset containers
|
|
6
|
+
* - Cookie 12347: array, bitset, and run containers
|
|
7
|
+
*
|
|
8
|
+
* See: https://github.com/RoaringBitmap/RoaringFormatSpec
|
|
9
|
+
*/
|
|
10
|
+
/** Decode a serialized RoaringBitmap into a sorted array of set bit positions. */
|
|
11
|
+
export declare function decodeRoaringBitmap(data: Uint8Array): number[];
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* CLI for testing Sui light client verification against live checkpoints.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bun src/cli.ts verify <checkpoint_seq> [--url <fullnode_url>]
|
|
7
|
+
* bun src/cli.ts verify-range <from> <to> [--url <fullnode_url>]
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
5
|
+
|
|
6
|
+
// src/bitmap.ts
|
|
7
|
+
var SERIAL_COOKIE_NO_RUNCONTAINER = 12346;
|
|
8
|
+
var SERIAL_COOKIE = 12347;
|
|
9
|
+
var NO_OFFSET_THRESHOLD = 4;
|
|
10
|
+
function decodeRoaringBitmap(data) {
|
|
11
|
+
if (data.byteLength < 4) {
|
|
12
|
+
throw new Error(`RoaringBitmap too small: ${data.byteLength} bytes`);
|
|
13
|
+
}
|
|
14
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
15
|
+
let offset = 0;
|
|
16
|
+
const firstU32 = view.getUint32(0, true);
|
|
17
|
+
const firstU16 = firstU32 & 65535;
|
|
18
|
+
let containerCount;
|
|
19
|
+
let isRunBitmap = [];
|
|
20
|
+
if (firstU32 === SERIAL_COOKIE_NO_RUNCONTAINER) {
|
|
21
|
+
offset = 4;
|
|
22
|
+
containerCount = view.getUint32(offset, true);
|
|
23
|
+
offset += 4;
|
|
24
|
+
} else if (firstU16 === SERIAL_COOKIE) {
|
|
25
|
+
offset = 2;
|
|
26
|
+
containerCount = view.getUint16(offset, true) + 1;
|
|
27
|
+
offset += 2;
|
|
28
|
+
const runBitmapBytes = Math.ceil(containerCount / 8);
|
|
29
|
+
for (let i = 0;i < containerCount; i++) {
|
|
30
|
+
const byteIdx = Math.floor(i / 8);
|
|
31
|
+
const bitIdx = i % 8;
|
|
32
|
+
isRunBitmap.push((data[offset + byteIdx] & 1 << bitIdx) !== 0);
|
|
33
|
+
}
|
|
34
|
+
offset += runBitmapBytes;
|
|
35
|
+
} else {
|
|
36
|
+
throw new Error(`Invalid RoaringBitmap cookie: ${firstU32}`);
|
|
37
|
+
}
|
|
38
|
+
const keys = [];
|
|
39
|
+
const cardinalities = [];
|
|
40
|
+
for (let i = 0;i < containerCount; i++) {
|
|
41
|
+
keys.push(view.getUint16(offset, true));
|
|
42
|
+
offset += 2;
|
|
43
|
+
cardinalities.push(view.getUint16(offset, true) + 1);
|
|
44
|
+
offset += 2;
|
|
45
|
+
}
|
|
46
|
+
if (firstU32 === SERIAL_COOKIE_NO_RUNCONTAINER) {
|
|
47
|
+
offset += containerCount * 4;
|
|
48
|
+
} else if (containerCount >= NO_OFFSET_THRESHOLD) {
|
|
49
|
+
offset += containerCount * 4;
|
|
50
|
+
}
|
|
51
|
+
const result = [];
|
|
52
|
+
for (let i = 0;i < containerCount; i++) {
|
|
53
|
+
const highBits = keys[i] << 16;
|
|
54
|
+
const cardinality = cardinalities[i];
|
|
55
|
+
if (isRunBitmap[i]) {
|
|
56
|
+
const numRuns = view.getUint16(offset, true);
|
|
57
|
+
offset += 2;
|
|
58
|
+
for (let r = 0;r < numRuns; r++) {
|
|
59
|
+
const start = view.getUint16(offset, true);
|
|
60
|
+
offset += 2;
|
|
61
|
+
const length = view.getUint16(offset, true);
|
|
62
|
+
offset += 2;
|
|
63
|
+
for (let v = start;v <= start + length; v++) {
|
|
64
|
+
result.push(highBits | v);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} else if (cardinality <= 4096) {
|
|
68
|
+
for (let j = 0;j < cardinality; j++) {
|
|
69
|
+
result.push(highBits | view.getUint16(offset, true));
|
|
70
|
+
offset += 2;
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
for (let word = 0;word < 1024; word++) {
|
|
74
|
+
const lo = view.getUint32(offset, true);
|
|
75
|
+
const hi = view.getUint32(offset + 4, true);
|
|
76
|
+
offset += 8;
|
|
77
|
+
for (let bit = 0;bit < 32; bit++) {
|
|
78
|
+
if (lo & 1 << bit)
|
|
79
|
+
result.push(highBits | word * 64 + bit);
|
|
80
|
+
if (hi & 1 << bit)
|
|
81
|
+
result.push(highBits | word * 64 + 32 + bit);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/verify.ts
|
|
90
|
+
import { bls12_381 } from "@noble/curves/bls12-381";
|
|
91
|
+
|
|
92
|
+
// src/bitmap.ts
|
|
93
|
+
var SERIAL_COOKIE_NO_RUNCONTAINER2 = 12346;
|
|
94
|
+
var SERIAL_COOKIE2 = 12347;
|
|
95
|
+
var NO_OFFSET_THRESHOLD2 = 4;
|
|
96
|
+
function decodeRoaringBitmap2(data) {
|
|
97
|
+
if (data.byteLength < 4) {
|
|
98
|
+
throw new Error(`RoaringBitmap too small: ${data.byteLength} bytes`);
|
|
99
|
+
}
|
|
100
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
101
|
+
let offset = 0;
|
|
102
|
+
const firstU32 = view.getUint32(0, true);
|
|
103
|
+
const firstU16 = firstU32 & 65535;
|
|
104
|
+
let containerCount;
|
|
105
|
+
let isRunBitmap = [];
|
|
106
|
+
if (firstU32 === SERIAL_COOKIE_NO_RUNCONTAINER2) {
|
|
107
|
+
offset = 4;
|
|
108
|
+
containerCount = view.getUint32(offset, true);
|
|
109
|
+
offset += 4;
|
|
110
|
+
} else if (firstU16 === SERIAL_COOKIE2) {
|
|
111
|
+
offset = 2;
|
|
112
|
+
containerCount = view.getUint16(offset, true) + 1;
|
|
113
|
+
offset += 2;
|
|
114
|
+
const runBitmapBytes = Math.ceil(containerCount / 8);
|
|
115
|
+
for (let i = 0;i < containerCount; i++) {
|
|
116
|
+
const byteIdx = Math.floor(i / 8);
|
|
117
|
+
const bitIdx = i % 8;
|
|
118
|
+
isRunBitmap.push((data[offset + byteIdx] & 1 << bitIdx) !== 0);
|
|
119
|
+
}
|
|
120
|
+
offset += runBitmapBytes;
|
|
121
|
+
} else {
|
|
122
|
+
throw new Error(`Invalid RoaringBitmap cookie: ${firstU32}`);
|
|
123
|
+
}
|
|
124
|
+
const keys = [];
|
|
125
|
+
const cardinalities = [];
|
|
126
|
+
for (let i = 0;i < containerCount; i++) {
|
|
127
|
+
keys.push(view.getUint16(offset, true));
|
|
128
|
+
offset += 2;
|
|
129
|
+
cardinalities.push(view.getUint16(offset, true) + 1);
|
|
130
|
+
offset += 2;
|
|
131
|
+
}
|
|
132
|
+
if (firstU32 === SERIAL_COOKIE_NO_RUNCONTAINER2) {
|
|
133
|
+
offset += containerCount * 4;
|
|
134
|
+
} else if (containerCount >= NO_OFFSET_THRESHOLD2) {
|
|
135
|
+
offset += containerCount * 4;
|
|
136
|
+
}
|
|
137
|
+
const result = [];
|
|
138
|
+
for (let i = 0;i < containerCount; i++) {
|
|
139
|
+
const highBits = keys[i] << 16;
|
|
140
|
+
const cardinality = cardinalities[i];
|
|
141
|
+
if (isRunBitmap[i]) {
|
|
142
|
+
const numRuns = view.getUint16(offset, true);
|
|
143
|
+
offset += 2;
|
|
144
|
+
for (let r = 0;r < numRuns; r++) {
|
|
145
|
+
const start = view.getUint16(offset, true);
|
|
146
|
+
offset += 2;
|
|
147
|
+
const length = view.getUint16(offset, true);
|
|
148
|
+
offset += 2;
|
|
149
|
+
for (let v = start;v <= start + length; v++) {
|
|
150
|
+
result.push(highBits | v);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} else if (cardinality <= 4096) {
|
|
154
|
+
for (let j = 0;j < cardinality; j++) {
|
|
155
|
+
result.push(highBits | view.getUint16(offset, true));
|
|
156
|
+
offset += 2;
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
for (let word = 0;word < 1024; word++) {
|
|
160
|
+
const lo = view.getUint32(offset, true);
|
|
161
|
+
const hi = view.getUint32(offset + 4, true);
|
|
162
|
+
offset += 8;
|
|
163
|
+
for (let bit = 0;bit < 32; bit++) {
|
|
164
|
+
if (lo & 1 << bit)
|
|
165
|
+
result.push(highBits | word * 64 + bit);
|
|
166
|
+
if (hi & 1 << bit)
|
|
167
|
+
result.push(highBits | word * 64 + 32 + bit);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/digest.ts
|
|
176
|
+
import { blake2b } from "@noble/hashes/blake2b";
|
|
177
|
+
var encoder = new TextEncoder;
|
|
178
|
+
|
|
179
|
+
// src/types.ts
|
|
180
|
+
var QUORUM_THRESHOLD = 6667n;
|
|
181
|
+
var CHECKPOINT_SUMMARY_INTENT = new Uint8Array([2, 0, 0]);
|
|
182
|
+
|
|
183
|
+
// src/bcs.ts
|
|
184
|
+
import { bcs } from "@mysten/bcs";
|
|
185
|
+
var Digest = bcs.vector(bcs.u8());
|
|
186
|
+
var AuthorityPublicKeyBytes = bcs.vector(bcs.u8());
|
|
187
|
+
var GasCostSummary = bcs.struct("GasCostSummary", {
|
|
188
|
+
computationCost: bcs.u64(),
|
|
189
|
+
storageCost: bcs.u64(),
|
|
190
|
+
storageRebate: bcs.u64(),
|
|
191
|
+
nonRefundableStorageFee: bcs.u64()
|
|
192
|
+
});
|
|
193
|
+
var CheckpointCommitment = bcs.enum("CheckpointCommitment", {
|
|
194
|
+
ECMHLiveObjectSetDigest: Digest,
|
|
195
|
+
CheckpointArtifactsDigest: Digest
|
|
196
|
+
});
|
|
197
|
+
var EndOfEpochData = bcs.struct("EndOfEpochData", {
|
|
198
|
+
nextEpochCommittee: bcs.vector(bcs.tuple([AuthorityPublicKeyBytes, bcs.u64()])),
|
|
199
|
+
nextEpochProtocolVersion: bcs.u64(),
|
|
200
|
+
epochCommitments: bcs.vector(CheckpointCommitment)
|
|
201
|
+
});
|
|
202
|
+
var bcsCheckpointSummary = bcs.struct("CheckpointSummary", {
|
|
203
|
+
epoch: bcs.u64(),
|
|
204
|
+
sequenceNumber: bcs.u64(),
|
|
205
|
+
networkTotalTransactions: bcs.u64(),
|
|
206
|
+
contentDigest: Digest,
|
|
207
|
+
previousDigest: bcs.option(Digest),
|
|
208
|
+
epochRollingGasCostSummary: GasCostSummary,
|
|
209
|
+
timestampMs: bcs.u64(),
|
|
210
|
+
checkpointCommitments: bcs.vector(CheckpointCommitment),
|
|
211
|
+
endOfEpochData: bcs.option(EndOfEpochData),
|
|
212
|
+
versionSpecificData: bcs.vector(bcs.u8())
|
|
213
|
+
});
|
|
214
|
+
var ExecutionDigests = bcs.struct("ExecutionDigests", {
|
|
215
|
+
transaction: Digest,
|
|
216
|
+
effects: Digest
|
|
217
|
+
});
|
|
218
|
+
var CheckpointContentsV1 = bcs.struct("CheckpointContentsV1", {
|
|
219
|
+
transactions: bcs.vector(ExecutionDigests),
|
|
220
|
+
userSignatures: bcs.vector(bcs.vector(bcs.vector(bcs.u8())))
|
|
221
|
+
});
|
|
222
|
+
var bcsCheckpointContents = bcs.enum("CheckpointContents", {
|
|
223
|
+
V1: CheckpointContentsV1
|
|
224
|
+
});
|
|
225
|
+
var bcsAuthorityQuorumSignInfo = bcs.struct("AuthorityQuorumSignInfo", {
|
|
226
|
+
epoch: bcs.u64(),
|
|
227
|
+
signature: bcs.vector(bcs.u8()),
|
|
228
|
+
signersMap: bcs.vector(bcs.u8())
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// src/verify.ts
|
|
232
|
+
class PreparedCommittee {
|
|
233
|
+
epoch;
|
|
234
|
+
members;
|
|
235
|
+
constructor(committee) {
|
|
236
|
+
this.epoch = committee.epoch;
|
|
237
|
+
this.members = committee.members.map((m) => ({
|
|
238
|
+
point: bls12_381.G2.ProjectivePoint.fromHex(m.publicKey),
|
|
239
|
+
votingPower: m.votingPower
|
|
240
|
+
}));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function verifyCheckpoint(summaryBcs, authSignature, committee) {
|
|
244
|
+
const prepared = committee instanceof PreparedCommittee ? committee : new PreparedCommittee(committee);
|
|
245
|
+
if (authSignature.epoch !== prepared.epoch) {
|
|
246
|
+
throw new Error(`Epoch mismatch: signature epoch ${authSignature.epoch} !== committee epoch ${prepared.epoch}`);
|
|
247
|
+
}
|
|
248
|
+
const signerIndices = decodeRoaringBitmap2(authSignature.signersMap);
|
|
249
|
+
let totalPower = 0n;
|
|
250
|
+
let aggregatedPoint = null;
|
|
251
|
+
for (const idx of signerIndices) {
|
|
252
|
+
if (idx >= prepared.members.length) {
|
|
253
|
+
throw new Error(`Signer index ${idx} exceeds committee size ${prepared.members.length}`);
|
|
254
|
+
}
|
|
255
|
+
const member = prepared.members[idx];
|
|
256
|
+
totalPower += member.votingPower;
|
|
257
|
+
aggregatedPoint = aggregatedPoint ? aggregatedPoint.add(member.point) : member.point;
|
|
258
|
+
}
|
|
259
|
+
if (totalPower < QUORUM_THRESHOLD) {
|
|
260
|
+
throw new Error(`Insufficient voting power: ${totalPower} < ${QUORUM_THRESHOLD} (${signerIndices.length}/${prepared.members.length} validators)`);
|
|
261
|
+
}
|
|
262
|
+
const epochBytes = new Uint8Array(8);
|
|
263
|
+
const epochView = new DataView(epochBytes.buffer);
|
|
264
|
+
epochView.setBigUint64(0, authSignature.epoch, true);
|
|
265
|
+
const message = new Uint8Array(CHECKPOINT_SUMMARY_INTENT.length + summaryBcs.length + epochBytes.length);
|
|
266
|
+
message.set(CHECKPOINT_SUMMARY_INTENT);
|
|
267
|
+
message.set(summaryBcs, CHECKPOINT_SUMMARY_INTENT.length);
|
|
268
|
+
message.set(epochBytes, CHECKPOINT_SUMMARY_INTENT.length + summaryBcs.length);
|
|
269
|
+
const hashedMessage = bls12_381.shortSignatures.hash(message);
|
|
270
|
+
const valid = bls12_381.shortSignatures.verify(authSignature.signature, hashedMessage, aggregatedPoint.toRawBytes(true));
|
|
271
|
+
if (!valid) {
|
|
272
|
+
throw new Error("BLS signature verification failed");
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/cli.ts
|
|
277
|
+
function usage() {
|
|
278
|
+
console.log(`Usage:
|
|
279
|
+
sui-light-client verify <checkpoint_seq> --network <testnet|mainnet> --url <grpc_url>
|
|
280
|
+
sui-light-client verify-range <from> <to> --network <testnet|mainnet> --url <grpc_url>
|
|
281
|
+
|
|
282
|
+
Environment variables (override flags):
|
|
283
|
+
GRPC_URL \u2014 fullnode gRPC endpoint
|
|
284
|
+
NETWORK \u2014 testnet or mainnet
|
|
285
|
+
|
|
286
|
+
Examples:
|
|
287
|
+
bun src/cli.ts verify 318460000 --network testnet --url https://fullnode.testnet.sui.io
|
|
288
|
+
GRPC_URL=https://fullnode.testnet.sui.io NETWORK=testnet bun src/cli.ts verify 318460000`);
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
function getFlag(args, flag) {
|
|
292
|
+
const idx = args.indexOf(flag);
|
|
293
|
+
return idx !== -1 ? args[idx + 1] : undefined;
|
|
294
|
+
}
|
|
295
|
+
function parseArgs() {
|
|
296
|
+
const args = process.argv.slice(2);
|
|
297
|
+
if (args.length === 0)
|
|
298
|
+
usage();
|
|
299
|
+
const command = args[0];
|
|
300
|
+
const network = process.env.NETWORK || getFlag(args, "--network");
|
|
301
|
+
const url = process.env.GRPC_URL || getFlag(args, "--url");
|
|
302
|
+
if (!network || !url) {
|
|
303
|
+
console.error(`Error: --network and --url are required (or set NETWORK and GRPC_URL env vars)
|
|
304
|
+
`);
|
|
305
|
+
usage();
|
|
306
|
+
}
|
|
307
|
+
if (command === "verify") {
|
|
308
|
+
const seq = args[1];
|
|
309
|
+
if (!seq || isNaN(Number(seq)))
|
|
310
|
+
usage();
|
|
311
|
+
return { command: "verify", seq: Number(seq), network, url };
|
|
312
|
+
}
|
|
313
|
+
if (command === "verify-range") {
|
|
314
|
+
const from = args[1];
|
|
315
|
+
const to = args[2];
|
|
316
|
+
if (!from || !to || isNaN(Number(from)) || isNaN(Number(to)))
|
|
317
|
+
usage();
|
|
318
|
+
return { command: "verify-range", from: Number(from), to: Number(to), network, url };
|
|
319
|
+
}
|
|
320
|
+
usage();
|
|
321
|
+
}
|
|
322
|
+
var _grpcClient = null;
|
|
323
|
+
async function getGrpcClient(network, url) {
|
|
324
|
+
if (_grpcClient)
|
|
325
|
+
return _grpcClient;
|
|
326
|
+
const { SuiGrpcClient } = await import("@mysten/sui/grpc");
|
|
327
|
+
_grpcClient = new SuiGrpcClient({ network, baseUrl: url });
|
|
328
|
+
return _grpcClient;
|
|
329
|
+
}
|
|
330
|
+
async function fetchCheckpoint(network, url, seq) {
|
|
331
|
+
const client = await getGrpcClient(network, url);
|
|
332
|
+
const { response } = await client.ledgerService.getCheckpoint({
|
|
333
|
+
checkpointId: { oneofKind: "sequenceNumber", sequenceNumber: BigInt(seq) },
|
|
334
|
+
readMask: { paths: ["summary.bcs", "signature"] }
|
|
335
|
+
});
|
|
336
|
+
return response.checkpoint;
|
|
337
|
+
}
|
|
338
|
+
async function fetchCommittee(url, epoch) {
|
|
339
|
+
const resp = await fetch(url, {
|
|
340
|
+
method: "POST",
|
|
341
|
+
headers: { "Content-Type": "application/json" },
|
|
342
|
+
body: JSON.stringify({
|
|
343
|
+
jsonrpc: "2.0",
|
|
344
|
+
id: 1,
|
|
345
|
+
method: "suix_getCommitteeInfo",
|
|
346
|
+
params: [epoch]
|
|
347
|
+
})
|
|
348
|
+
});
|
|
349
|
+
const json = await resp.json();
|
|
350
|
+
return {
|
|
351
|
+
epoch: BigInt(epoch),
|
|
352
|
+
members: json.result.validators.map(([pk, stake]) => ({
|
|
353
|
+
publicKey: new Uint8Array(Buffer.from(pk, "base64")),
|
|
354
|
+
votingPower: BigInt(stake)
|
|
355
|
+
}))
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
function extractAuthSignature(cp) {
|
|
359
|
+
return {
|
|
360
|
+
epoch: cp.signature.epoch,
|
|
361
|
+
signature: cp.signature.signature,
|
|
362
|
+
signersMap: cp.signature.bitmap
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
async function verifySingle(seq, network, url) {
|
|
366
|
+
const total = performance.now();
|
|
367
|
+
process.stdout.write(`Fetching checkpoint ${seq}...`);
|
|
368
|
+
let t = performance.now();
|
|
369
|
+
const cp = await fetchCheckpoint(network, url, seq);
|
|
370
|
+
console.log(` ${(performance.now() - t).toFixed(0)}ms`);
|
|
371
|
+
const summaryBcs = cp.summary.bcs.value;
|
|
372
|
+
const authSignature = extractAuthSignature(cp);
|
|
373
|
+
const signers = decodeRoaringBitmap(authSignature.signersMap);
|
|
374
|
+
process.stdout.write(`Fetching committee for epoch ${authSignature.epoch}...`);
|
|
375
|
+
t = performance.now();
|
|
376
|
+
const committee = await fetchCommittee(url, authSignature.epoch.toString());
|
|
377
|
+
console.log(` ${(performance.now() - t).toFixed(0)}ms (${committee.members.length} validators)`);
|
|
378
|
+
process.stdout.write(`Verifying signature (${signers.length} signers)...`);
|
|
379
|
+
t = performance.now();
|
|
380
|
+
verifyCheckpoint(summaryBcs, authSignature, committee);
|
|
381
|
+
console.log(` ${(performance.now() - t).toFixed(0)}ms`);
|
|
382
|
+
console.log(`
|
|
383
|
+
Checkpoint ${seq} verified in ${(performance.now() - total).toFixed(0)}ms`);
|
|
384
|
+
}
|
|
385
|
+
async function verifyRange(from, to, network, url) {
|
|
386
|
+
const count = to - from + 1;
|
|
387
|
+
console.log(`Verifying ${count} checkpoints (${from} \u2192 ${to})
|
|
388
|
+
`);
|
|
389
|
+
process.stdout.write("Fetching first checkpoint...");
|
|
390
|
+
let t = performance.now();
|
|
391
|
+
const firstCp = await fetchCheckpoint(network, url, from);
|
|
392
|
+
const firstAuth = extractAuthSignature(firstCp);
|
|
393
|
+
console.log(` epoch ${firstAuth.epoch} (${(performance.now() - t).toFixed(0)}ms)`);
|
|
394
|
+
process.stdout.write("Preparing committee...");
|
|
395
|
+
t = performance.now();
|
|
396
|
+
const committee = await fetchCommittee(url, firstAuth.epoch.toString());
|
|
397
|
+
const prepared = new PreparedCommittee(committee);
|
|
398
|
+
console.log(` ${committee.members.length} validators, ${(performance.now() - t).toFixed(0)}ms
|
|
399
|
+
`);
|
|
400
|
+
let verified = 0;
|
|
401
|
+
let totalVerifyMs = 0;
|
|
402
|
+
const batchStart = performance.now();
|
|
403
|
+
for (let seq = from;seq <= to; seq++) {
|
|
404
|
+
t = performance.now();
|
|
405
|
+
const cp = await fetchCheckpoint(network, url, seq);
|
|
406
|
+
const fetchMs = performance.now() - t;
|
|
407
|
+
const authSignature = extractAuthSignature(cp);
|
|
408
|
+
const signers = decodeRoaringBitmap(authSignature.signersMap);
|
|
409
|
+
t = performance.now();
|
|
410
|
+
verifyCheckpoint(cp.summary.bcs.value, authSignature, prepared);
|
|
411
|
+
const verifyMs = performance.now() - t;
|
|
412
|
+
totalVerifyMs += verifyMs;
|
|
413
|
+
verified++;
|
|
414
|
+
console.log(` [${verified}/${count}] seq=${seq} signers=${signers.length} fetch=${fetchMs.toFixed(0)}ms verify=${verifyMs.toFixed(0)}ms`);
|
|
415
|
+
}
|
|
416
|
+
const elapsed = performance.now() - batchStart;
|
|
417
|
+
console.log(`
|
|
418
|
+
${verified} checkpoints verified in ${(elapsed / 1000).toFixed(1)}s`);
|
|
419
|
+
console.log(`Avg verify: ${(totalVerifyMs / verified).toFixed(1)}ms/checkpoint`);
|
|
420
|
+
console.log(`Throughput: ${(verified / (elapsed / 1000)).toFixed(1)} checkpoints/sec (including network)`);
|
|
421
|
+
}
|
|
422
|
+
async function main() {
|
|
423
|
+
const parsed = parseArgs();
|
|
424
|
+
if (parsed.command === "verify") {
|
|
425
|
+
await verifySingle(parsed.seq, parsed.network, parsed.url);
|
|
426
|
+
} else {
|
|
427
|
+
await verifyRange(parsed.from, parsed.to, parsed.network, parsed.url);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
main().catch((err) => {
|
|
431
|
+
console.error(err.message);
|
|
432
|
+
process.exit(1);
|
|
433
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Committee management and transition verification.
|
|
3
|
+
*/
|
|
4
|
+
import type { CheckpointSummary, Committee, AuthorityQuorumSignInfo } from './types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Verify a committee transition: given a certified end-of-epoch checkpoint
|
|
7
|
+
* and the current committee, extract and return the next epoch's committee.
|
|
8
|
+
*
|
|
9
|
+
* @param summaryBcs - Raw BCS bytes of the CheckpointSummary
|
|
10
|
+
* @param summary - Parsed CheckpointSummary (for reading endOfEpochData)
|
|
11
|
+
* @param authSignature - The quorum signature
|
|
12
|
+
* @param currentCommittee - The committee that signed this checkpoint
|
|
13
|
+
*/
|
|
14
|
+
export declare function verifyCommitteeTransition(summaryBcs: Uint8Array, summary: CheckpointSummary, authSignature: AuthorityQuorumSignInfo, currentCommittee: Committee): Committee;
|
|
15
|
+
/**
|
|
16
|
+
* Walk a chain of end-of-epoch checkpoints to advance from a trusted committee
|
|
17
|
+
* to a target epoch.
|
|
18
|
+
*
|
|
19
|
+
* @param checkpoints - Ordered end-of-epoch certified checkpoints with raw BCS + parsed summary
|
|
20
|
+
* @param trustedCommittee - Starting committee (must match first checkpoint's epoch)
|
|
21
|
+
* @returns The committee for the epoch after the last checkpoint
|
|
22
|
+
*/
|
|
23
|
+
export declare function walkCommitteeChain(checkpoints: {
|
|
24
|
+
summaryBcs: Uint8Array;
|
|
25
|
+
summary: CheckpointSummary;
|
|
26
|
+
authSignature: AuthorityQuorumSignInfo;
|
|
27
|
+
}[], trustedCommittee: Committee): Committee;
|
package/dist/digest.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Sui digest computation: Blake2b-256 with struct name domain separators. */
|
|
2
|
+
/**
|
|
3
|
+
* Compute a Sui digest: Blake2b-256("StructName::" || data)
|
|
4
|
+
* All Sui digests follow this pattern for domain separation.
|
|
5
|
+
*/
|
|
6
|
+
export declare function suiDigest(structName: string, bcsBytes: Uint8Array): Uint8Array;
|
|
7
|
+
export declare function checkpointDigest(summaryBcs: Uint8Array): Uint8Array;
|
|
8
|
+
export declare function checkpointContentsDigest(contentsBcs: Uint8Array): Uint8Array;
|
|
9
|
+
export declare function transactionDigest(txDataBcs: Uint8Array): Uint8Array;
|
|
10
|
+
export declare function transactionEffectsDigest(effectsBcs: Uint8Array): Uint8Array;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { verifyCheckpoint, verifyCheckpointContents, verifyTransactionInCheckpoint, PreparedCommittee, digestsEqual } from './verify.js';
|
|
2
|
+
export { parseBcsSummary } from './parse.js';
|
|
3
|
+
export { verifyCommitteeTransition, walkCommitteeChain } from './committee.js';
|
|
4
|
+
export { suiDigest, checkpointDigest, checkpointContentsDigest, transactionDigest, transactionEffectsDigest } from './digest.js';
|
|
5
|
+
export { decodeRoaringBitmap } from './bitmap.js';
|
|
6
|
+
export { bcsCheckpointSummary, bcsCheckpointContents, bcsAuthorityQuorumSignInfo } from './bcs.js';
|
|
7
|
+
export type { CheckpointSummary, CheckpointContents, ExecutionDigests, Committee, CommitteeMember, AuthorityQuorumSignInfo, CertifiedCheckpointSummary, EndOfEpochData, GasCostSummary, CheckpointCommitment, } from './types.js';
|
|
8
|
+
export { TOTAL_VOTING_POWER, QUORUM_THRESHOLD, CHECKPOINT_SUMMARY_INTENT } from './types.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
// src/verify.ts
|
|
2
|
+
import { bls12_381 } from "@noble/curves/bls12-381";
|
|
3
|
+
|
|
4
|
+
// src/bitmap.ts
|
|
5
|
+
var SERIAL_COOKIE_NO_RUNCONTAINER = 12346;
|
|
6
|
+
var SERIAL_COOKIE = 12347;
|
|
7
|
+
var NO_OFFSET_THRESHOLD = 4;
|
|
8
|
+
function decodeRoaringBitmap(data) {
|
|
9
|
+
if (data.byteLength < 4) {
|
|
10
|
+
throw new Error(`RoaringBitmap too small: ${data.byteLength} bytes`);
|
|
11
|
+
}
|
|
12
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
13
|
+
let offset = 0;
|
|
14
|
+
const firstU32 = view.getUint32(0, true);
|
|
15
|
+
const firstU16 = firstU32 & 65535;
|
|
16
|
+
let containerCount;
|
|
17
|
+
let isRunBitmap = [];
|
|
18
|
+
if (firstU32 === SERIAL_COOKIE_NO_RUNCONTAINER) {
|
|
19
|
+
offset = 4;
|
|
20
|
+
containerCount = view.getUint32(offset, true);
|
|
21
|
+
offset += 4;
|
|
22
|
+
} else if (firstU16 === SERIAL_COOKIE) {
|
|
23
|
+
offset = 2;
|
|
24
|
+
containerCount = view.getUint16(offset, true) + 1;
|
|
25
|
+
offset += 2;
|
|
26
|
+
const runBitmapBytes = Math.ceil(containerCount / 8);
|
|
27
|
+
for (let i = 0;i < containerCount; i++) {
|
|
28
|
+
const byteIdx = Math.floor(i / 8);
|
|
29
|
+
const bitIdx = i % 8;
|
|
30
|
+
isRunBitmap.push((data[offset + byteIdx] & 1 << bitIdx) !== 0);
|
|
31
|
+
}
|
|
32
|
+
offset += runBitmapBytes;
|
|
33
|
+
} else {
|
|
34
|
+
throw new Error(`Invalid RoaringBitmap cookie: ${firstU32}`);
|
|
35
|
+
}
|
|
36
|
+
const keys = [];
|
|
37
|
+
const cardinalities = [];
|
|
38
|
+
for (let i = 0;i < containerCount; i++) {
|
|
39
|
+
keys.push(view.getUint16(offset, true));
|
|
40
|
+
offset += 2;
|
|
41
|
+
cardinalities.push(view.getUint16(offset, true) + 1);
|
|
42
|
+
offset += 2;
|
|
43
|
+
}
|
|
44
|
+
if (firstU32 === SERIAL_COOKIE_NO_RUNCONTAINER) {
|
|
45
|
+
offset += containerCount * 4;
|
|
46
|
+
} else if (containerCount >= NO_OFFSET_THRESHOLD) {
|
|
47
|
+
offset += containerCount * 4;
|
|
48
|
+
}
|
|
49
|
+
const result = [];
|
|
50
|
+
for (let i = 0;i < containerCount; i++) {
|
|
51
|
+
const highBits = keys[i] << 16;
|
|
52
|
+
const cardinality = cardinalities[i];
|
|
53
|
+
if (isRunBitmap[i]) {
|
|
54
|
+
const numRuns = view.getUint16(offset, true);
|
|
55
|
+
offset += 2;
|
|
56
|
+
for (let r = 0;r < numRuns; r++) {
|
|
57
|
+
const start = view.getUint16(offset, true);
|
|
58
|
+
offset += 2;
|
|
59
|
+
const length = view.getUint16(offset, true);
|
|
60
|
+
offset += 2;
|
|
61
|
+
for (let v = start;v <= start + length; v++) {
|
|
62
|
+
result.push(highBits | v);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} else if (cardinality <= 4096) {
|
|
66
|
+
for (let j = 0;j < cardinality; j++) {
|
|
67
|
+
result.push(highBits | view.getUint16(offset, true));
|
|
68
|
+
offset += 2;
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
for (let word = 0;word < 1024; word++) {
|
|
72
|
+
const lo = view.getUint32(offset, true);
|
|
73
|
+
const hi = view.getUint32(offset + 4, true);
|
|
74
|
+
offset += 8;
|
|
75
|
+
for (let bit = 0;bit < 32; bit++) {
|
|
76
|
+
if (lo & 1 << bit)
|
|
77
|
+
result.push(highBits | word * 64 + bit);
|
|
78
|
+
if (hi & 1 << bit)
|
|
79
|
+
result.push(highBits | word * 64 + 32 + bit);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/digest.ts
|
|
88
|
+
import { blake2b } from "@noble/hashes/blake2b";
|
|
89
|
+
var encoder = new TextEncoder;
|
|
90
|
+
function suiDigest(structName, bcsBytes) {
|
|
91
|
+
const prefix = encoder.encode(`${structName}::`);
|
|
92
|
+
const input = new Uint8Array(prefix.length + bcsBytes.length);
|
|
93
|
+
input.set(prefix);
|
|
94
|
+
input.set(bcsBytes, prefix.length);
|
|
95
|
+
return blake2b(input, { dkLen: 32 });
|
|
96
|
+
}
|
|
97
|
+
function checkpointDigest(summaryBcs) {
|
|
98
|
+
return suiDigest("CheckpointSummary", summaryBcs);
|
|
99
|
+
}
|
|
100
|
+
function checkpointContentsDigest(contentsBcs) {
|
|
101
|
+
return suiDigest("CheckpointContents", contentsBcs);
|
|
102
|
+
}
|
|
103
|
+
function transactionDigest(txDataBcs) {
|
|
104
|
+
return suiDigest("TransactionData", txDataBcs);
|
|
105
|
+
}
|
|
106
|
+
function transactionEffectsDigest(effectsBcs) {
|
|
107
|
+
return suiDigest("TransactionEffects", effectsBcs);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/types.ts
|
|
111
|
+
var TOTAL_VOTING_POWER = 10000n;
|
|
112
|
+
var QUORUM_THRESHOLD = 6667n;
|
|
113
|
+
var CHECKPOINT_SUMMARY_INTENT = new Uint8Array([2, 0, 0]);
|
|
114
|
+
|
|
115
|
+
// src/bcs.ts
|
|
116
|
+
import { bcs } from "@mysten/bcs";
|
|
117
|
+
var Digest = bcs.vector(bcs.u8());
|
|
118
|
+
var AuthorityPublicKeyBytes = bcs.vector(bcs.u8());
|
|
119
|
+
var GasCostSummary = bcs.struct("GasCostSummary", {
|
|
120
|
+
computationCost: bcs.u64(),
|
|
121
|
+
storageCost: bcs.u64(),
|
|
122
|
+
storageRebate: bcs.u64(),
|
|
123
|
+
nonRefundableStorageFee: bcs.u64()
|
|
124
|
+
});
|
|
125
|
+
var CheckpointCommitment = bcs.enum("CheckpointCommitment", {
|
|
126
|
+
ECMHLiveObjectSetDigest: Digest,
|
|
127
|
+
CheckpointArtifactsDigest: Digest
|
|
128
|
+
});
|
|
129
|
+
var EndOfEpochData = bcs.struct("EndOfEpochData", {
|
|
130
|
+
nextEpochCommittee: bcs.vector(bcs.tuple([AuthorityPublicKeyBytes, bcs.u64()])),
|
|
131
|
+
nextEpochProtocolVersion: bcs.u64(),
|
|
132
|
+
epochCommitments: bcs.vector(CheckpointCommitment)
|
|
133
|
+
});
|
|
134
|
+
var bcsCheckpointSummary = bcs.struct("CheckpointSummary", {
|
|
135
|
+
epoch: bcs.u64(),
|
|
136
|
+
sequenceNumber: bcs.u64(),
|
|
137
|
+
networkTotalTransactions: bcs.u64(),
|
|
138
|
+
contentDigest: Digest,
|
|
139
|
+
previousDigest: bcs.option(Digest),
|
|
140
|
+
epochRollingGasCostSummary: GasCostSummary,
|
|
141
|
+
timestampMs: bcs.u64(),
|
|
142
|
+
checkpointCommitments: bcs.vector(CheckpointCommitment),
|
|
143
|
+
endOfEpochData: bcs.option(EndOfEpochData),
|
|
144
|
+
versionSpecificData: bcs.vector(bcs.u8())
|
|
145
|
+
});
|
|
146
|
+
var ExecutionDigests = bcs.struct("ExecutionDigests", {
|
|
147
|
+
transaction: Digest,
|
|
148
|
+
effects: Digest
|
|
149
|
+
});
|
|
150
|
+
var CheckpointContentsV1 = bcs.struct("CheckpointContentsV1", {
|
|
151
|
+
transactions: bcs.vector(ExecutionDigests),
|
|
152
|
+
userSignatures: bcs.vector(bcs.vector(bcs.vector(bcs.u8())))
|
|
153
|
+
});
|
|
154
|
+
var bcsCheckpointContents = bcs.enum("CheckpointContents", {
|
|
155
|
+
V1: CheckpointContentsV1
|
|
156
|
+
});
|
|
157
|
+
var bcsAuthorityQuorumSignInfo = bcs.struct("AuthorityQuorumSignInfo", {
|
|
158
|
+
epoch: bcs.u64(),
|
|
159
|
+
signature: bcs.vector(bcs.u8()),
|
|
160
|
+
signersMap: bcs.vector(bcs.u8())
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// src/verify.ts
|
|
164
|
+
class PreparedCommittee {
|
|
165
|
+
epoch;
|
|
166
|
+
members;
|
|
167
|
+
constructor(committee) {
|
|
168
|
+
this.epoch = committee.epoch;
|
|
169
|
+
this.members = committee.members.map((m) => ({
|
|
170
|
+
point: bls12_381.G2.ProjectivePoint.fromHex(m.publicKey),
|
|
171
|
+
votingPower: m.votingPower
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function verifyCheckpoint(summaryBcs, authSignature, committee) {
|
|
176
|
+
const prepared = committee instanceof PreparedCommittee ? committee : new PreparedCommittee(committee);
|
|
177
|
+
if (authSignature.epoch !== prepared.epoch) {
|
|
178
|
+
throw new Error(`Epoch mismatch: signature epoch ${authSignature.epoch} !== committee epoch ${prepared.epoch}`);
|
|
179
|
+
}
|
|
180
|
+
const signerIndices = decodeRoaringBitmap(authSignature.signersMap);
|
|
181
|
+
let totalPower = 0n;
|
|
182
|
+
let aggregatedPoint = null;
|
|
183
|
+
for (const idx of signerIndices) {
|
|
184
|
+
if (idx >= prepared.members.length) {
|
|
185
|
+
throw new Error(`Signer index ${idx} exceeds committee size ${prepared.members.length}`);
|
|
186
|
+
}
|
|
187
|
+
const member = prepared.members[idx];
|
|
188
|
+
totalPower += member.votingPower;
|
|
189
|
+
aggregatedPoint = aggregatedPoint ? aggregatedPoint.add(member.point) : member.point;
|
|
190
|
+
}
|
|
191
|
+
if (totalPower < QUORUM_THRESHOLD) {
|
|
192
|
+
throw new Error(`Insufficient voting power: ${totalPower} < ${QUORUM_THRESHOLD} (${signerIndices.length}/${prepared.members.length} validators)`);
|
|
193
|
+
}
|
|
194
|
+
const epochBytes = new Uint8Array(8);
|
|
195
|
+
const epochView = new DataView(epochBytes.buffer);
|
|
196
|
+
epochView.setBigUint64(0, authSignature.epoch, true);
|
|
197
|
+
const message = new Uint8Array(CHECKPOINT_SUMMARY_INTENT.length + summaryBcs.length + epochBytes.length);
|
|
198
|
+
message.set(CHECKPOINT_SUMMARY_INTENT);
|
|
199
|
+
message.set(summaryBcs, CHECKPOINT_SUMMARY_INTENT.length);
|
|
200
|
+
message.set(epochBytes, CHECKPOINT_SUMMARY_INTENT.length + summaryBcs.length);
|
|
201
|
+
const hashedMessage = bls12_381.shortSignatures.hash(message);
|
|
202
|
+
const valid = bls12_381.shortSignatures.verify(authSignature.signature, hashedMessage, aggregatedPoint.toRawBytes(true));
|
|
203
|
+
if (!valid) {
|
|
204
|
+
throw new Error("BLS signature verification failed");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function verifyCheckpointContents(summary, contents) {
|
|
208
|
+
const contentsBcs = bcsCheckpointContents.serialize({ V1: contents }).toBytes();
|
|
209
|
+
const computedDigest = checkpointContentsDigest(contentsBcs);
|
|
210
|
+
if (!digestsEqual(computedDigest, summary.contentDigest)) {
|
|
211
|
+
throw new Error("Checkpoint contents digest mismatch");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function verifyTransactionInCheckpoint(txDigest, contents) {
|
|
215
|
+
for (const exec of contents.transactions) {
|
|
216
|
+
if (digestsEqual(exec.transaction, txDigest)) {
|
|
217
|
+
return exec;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
throw new Error("Transaction not found in checkpoint contents");
|
|
221
|
+
}
|
|
222
|
+
function digestsEqual(a, b) {
|
|
223
|
+
if (a.length !== b.length)
|
|
224
|
+
return false;
|
|
225
|
+
for (let i = 0;i < a.length; i++) {
|
|
226
|
+
if (a[i] !== b[i])
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
// src/parse.ts
|
|
232
|
+
function parseBcsSummary(bcsBytes) {
|
|
233
|
+
const p = bcsCheckpointSummary.parse(bcsBytes);
|
|
234
|
+
return {
|
|
235
|
+
epoch: BigInt(p.epoch),
|
|
236
|
+
sequenceNumber: BigInt(p.sequenceNumber),
|
|
237
|
+
networkTotalTransactions: BigInt(p.networkTotalTransactions),
|
|
238
|
+
contentDigest: Uint8Array.from(p.contentDigest),
|
|
239
|
+
previousDigest: p.previousDigest ? Uint8Array.from(p.previousDigest) : null,
|
|
240
|
+
epochRollingGasCostSummary: {
|
|
241
|
+
computationCost: BigInt(p.epochRollingGasCostSummary.computationCost),
|
|
242
|
+
storageCost: BigInt(p.epochRollingGasCostSummary.storageCost),
|
|
243
|
+
storageRebate: BigInt(p.epochRollingGasCostSummary.storageRebate),
|
|
244
|
+
nonRefundableStorageFee: BigInt(p.epochRollingGasCostSummary.nonRefundableStorageFee)
|
|
245
|
+
},
|
|
246
|
+
timestampMs: BigInt(p.timestampMs),
|
|
247
|
+
checkpointCommitments: p.checkpointCommitments.map(convertCommitment),
|
|
248
|
+
endOfEpochData: p.endOfEpochData ? convertEndOfEpochData(p.endOfEpochData) : null,
|
|
249
|
+
versionSpecificData: Uint8Array.from(p.versionSpecificData)
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function convertCommitment(c) {
|
|
253
|
+
if ("ECMHLiveObjectSetDigest" in c && c.ECMHLiveObjectSetDigest) {
|
|
254
|
+
return { ECMHLiveObjectSetDigest: Uint8Array.from(c.ECMHLiveObjectSetDigest) };
|
|
255
|
+
}
|
|
256
|
+
if ("CheckpointArtifactsDigest" in c && c.CheckpointArtifactsDigest) {
|
|
257
|
+
return { CheckpointArtifactsDigest: Uint8Array.from(c.CheckpointArtifactsDigest) };
|
|
258
|
+
}
|
|
259
|
+
throw new Error(`Unknown CheckpointCommitment variant: ${JSON.stringify(c)}`);
|
|
260
|
+
}
|
|
261
|
+
function convertEndOfEpochData(e) {
|
|
262
|
+
return {
|
|
263
|
+
nextEpochCommittee: e.nextEpochCommittee.map(([pk, stake]) => [
|
|
264
|
+
Uint8Array.from(pk),
|
|
265
|
+
BigInt(stake)
|
|
266
|
+
]),
|
|
267
|
+
nextEpochProtocolVersion: BigInt(e.nextEpochProtocolVersion),
|
|
268
|
+
epochCommitments: e.epochCommitments.map(convertCommitment)
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
// src/committee.ts
|
|
272
|
+
function verifyCommitteeTransition(summaryBcs, summary, authSignature, currentCommittee) {
|
|
273
|
+
verifyCheckpoint(summaryBcs, authSignature, currentCommittee);
|
|
274
|
+
if (!summary.endOfEpochData) {
|
|
275
|
+
throw new Error("Checkpoint is not an end-of-epoch checkpoint");
|
|
276
|
+
}
|
|
277
|
+
const nextEpoch = summary.epoch + 1n;
|
|
278
|
+
const members = summary.endOfEpochData.nextEpochCommittee.map(([publicKey, votingPower]) => ({
|
|
279
|
+
publicKey: Uint8Array.from(publicKey),
|
|
280
|
+
votingPower
|
|
281
|
+
}));
|
|
282
|
+
if (members.length === 0) {
|
|
283
|
+
throw new Error("End-of-epoch checkpoint has empty next committee");
|
|
284
|
+
}
|
|
285
|
+
return { epoch: nextEpoch, members };
|
|
286
|
+
}
|
|
287
|
+
function walkCommitteeChain(checkpoints, trustedCommittee) {
|
|
288
|
+
let committee = trustedCommittee;
|
|
289
|
+
for (const { summaryBcs, summary, authSignature } of checkpoints) {
|
|
290
|
+
if (summary.epoch !== committee.epoch) {
|
|
291
|
+
throw new Error(`Epoch gap: expected checkpoint for epoch ${committee.epoch}, got ${summary.epoch}`);
|
|
292
|
+
}
|
|
293
|
+
committee = verifyCommitteeTransition(summaryBcs, summary, authSignature, committee);
|
|
294
|
+
}
|
|
295
|
+
return committee;
|
|
296
|
+
}
|
|
297
|
+
export {
|
|
298
|
+
walkCommitteeChain,
|
|
299
|
+
verifyTransactionInCheckpoint,
|
|
300
|
+
verifyCommitteeTransition,
|
|
301
|
+
verifyCheckpointContents,
|
|
302
|
+
verifyCheckpoint,
|
|
303
|
+
transactionEffectsDigest,
|
|
304
|
+
transactionDigest,
|
|
305
|
+
suiDigest,
|
|
306
|
+
parseBcsSummary,
|
|
307
|
+
digestsEqual,
|
|
308
|
+
decodeRoaringBitmap,
|
|
309
|
+
checkpointDigest,
|
|
310
|
+
checkpointContentsDigest,
|
|
311
|
+
bcsCheckpointSummary,
|
|
312
|
+
bcsCheckpointContents,
|
|
313
|
+
bcsAuthorityQuorumSignInfo,
|
|
314
|
+
TOTAL_VOTING_POWER,
|
|
315
|
+
QUORUM_THRESHOLD,
|
|
316
|
+
PreparedCommittee,
|
|
317
|
+
CHECKPOINT_SUMMARY_INTENT
|
|
318
|
+
};
|
package/dist/parse.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert BCS-deserialized checkpoint data to typed CheckpointSummary.
|
|
3
|
+
*
|
|
4
|
+
* @mysten/bcs returns u64 as strings and byte arrays as number[].
|
|
5
|
+
* This module bridges from the BCS output shape to our typed interfaces.
|
|
6
|
+
*/
|
|
7
|
+
import type { CheckpointSummary } from './types.js';
|
|
8
|
+
/** Parse raw BCS bytes into a typed CheckpointSummary. */
|
|
9
|
+
export declare function parseBcsSummary(bcsBytes: Uint8Array): CheckpointSummary;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Sui checkpoint types for BCS serialization and light client verification. */
|
|
2
|
+
/** 32-byte digest (Blake2b-256 output) */
|
|
3
|
+
export type Digest = Uint8Array;
|
|
4
|
+
/** BLS12-381 public key in min-sig mode (G2 compressed) */
|
|
5
|
+
export type AuthorityPublicKeyBytes = Uint8Array;
|
|
6
|
+
/** BLS12-381 aggregate signature in min-sig mode (G1 compressed) */
|
|
7
|
+
export type AggregateSignatureBytes = Uint8Array;
|
|
8
|
+
export interface GasCostSummary {
|
|
9
|
+
computationCost: bigint;
|
|
10
|
+
storageCost: bigint;
|
|
11
|
+
storageRebate: bigint;
|
|
12
|
+
nonRefundableStorageFee: bigint;
|
|
13
|
+
}
|
|
14
|
+
export interface EndOfEpochData {
|
|
15
|
+
nextEpochCommittee: [AuthorityPublicKeyBytes, bigint][];
|
|
16
|
+
nextEpochProtocolVersion: bigint;
|
|
17
|
+
epochCommitments: CheckpointCommitment[];
|
|
18
|
+
}
|
|
19
|
+
export type CheckpointCommitment = {
|
|
20
|
+
ECMHLiveObjectSetDigest: Digest;
|
|
21
|
+
} | {
|
|
22
|
+
CheckpointArtifactsDigest: Digest;
|
|
23
|
+
};
|
|
24
|
+
export interface CheckpointSummary {
|
|
25
|
+
epoch: bigint;
|
|
26
|
+
sequenceNumber: bigint;
|
|
27
|
+
networkTotalTransactions: bigint;
|
|
28
|
+
contentDigest: Digest;
|
|
29
|
+
previousDigest: Digest | null;
|
|
30
|
+
epochRollingGasCostSummary: GasCostSummary;
|
|
31
|
+
timestampMs: bigint;
|
|
32
|
+
checkpointCommitments: CheckpointCommitment[];
|
|
33
|
+
endOfEpochData: EndOfEpochData | null;
|
|
34
|
+
versionSpecificData: Uint8Array;
|
|
35
|
+
}
|
|
36
|
+
export interface AuthorityQuorumSignInfo {
|
|
37
|
+
epoch: bigint;
|
|
38
|
+
signature: AggregateSignatureBytes;
|
|
39
|
+
signersMap: Uint8Array;
|
|
40
|
+
}
|
|
41
|
+
export interface CertifiedCheckpointSummary {
|
|
42
|
+
summary: CheckpointSummary;
|
|
43
|
+
authSignature: AuthorityQuorumSignInfo;
|
|
44
|
+
}
|
|
45
|
+
export interface ExecutionDigests {
|
|
46
|
+
transaction: Digest;
|
|
47
|
+
effects: Digest;
|
|
48
|
+
}
|
|
49
|
+
export interface CheckpointContents {
|
|
50
|
+
transactions: ExecutionDigests[];
|
|
51
|
+
userSignatures: Uint8Array[][];
|
|
52
|
+
}
|
|
53
|
+
export interface CommitteeMember {
|
|
54
|
+
publicKey: AuthorityPublicKeyBytes;
|
|
55
|
+
votingPower: bigint;
|
|
56
|
+
}
|
|
57
|
+
export interface Committee {
|
|
58
|
+
epoch: bigint;
|
|
59
|
+
members: CommitteeMember[];
|
|
60
|
+
}
|
|
61
|
+
export declare const TOTAL_VOTING_POWER = 10000n;
|
|
62
|
+
export declare const QUORUM_THRESHOLD = 6667n;
|
|
63
|
+
export declare const CHECKPOINT_SUMMARY_INTENT: Uint8Array<ArrayBuffer>;
|
package/dist/verify.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkpoint certificate verification.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that a CheckpointSummary was signed by a quorum (≥6667/10000)
|
|
5
|
+
* of validators using BLS12-381 aggregate signatures.
|
|
6
|
+
*/
|
|
7
|
+
import { bls12_381 } from '@noble/curves/bls12-381';
|
|
8
|
+
import { type AuthorityQuorumSignInfo, type CheckpointContents, type CheckpointSummary, type Committee, type ExecutionDigests } from './types.js';
|
|
9
|
+
type G2Point = ReturnType<typeof bls12_381.G2.ProjectivePoint.fromHex>;
|
|
10
|
+
/**
|
|
11
|
+
* A committee with pre-parsed G2 public key points.
|
|
12
|
+
*
|
|
13
|
+
* Parsing 96-byte compressed G2 points is the bottleneck (~1ms per key).
|
|
14
|
+
* Pre-parsing the full committee once (~113ms for 118 validators) lets
|
|
15
|
+
* per-checkpoint aggregation drop from ~75ms to <1ms.
|
|
16
|
+
*
|
|
17
|
+
* Create once per epoch, reuse for all checkpoints in that epoch.
|
|
18
|
+
*/
|
|
19
|
+
export declare class PreparedCommittee {
|
|
20
|
+
readonly epoch: bigint;
|
|
21
|
+
readonly members: {
|
|
22
|
+
point: G2Point;
|
|
23
|
+
votingPower: bigint;
|
|
24
|
+
}[];
|
|
25
|
+
constructor(committee: Committee);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Verify a checkpoint certificate against a committee.
|
|
29
|
+
*
|
|
30
|
+
* Takes the raw BCS bytes of the CheckpointSummary — the exact bytes
|
|
31
|
+
* that validators signed. Using raw bytes avoids re-serialization which
|
|
32
|
+
* could introduce subtle mismatches.
|
|
33
|
+
*
|
|
34
|
+
* ~10ms per checkpoint with a PreparedCommittee (vs ~150ms without).
|
|
35
|
+
*/
|
|
36
|
+
export declare function verifyCheckpoint(summaryBcs: Uint8Array, authSignature: AuthorityQuorumSignInfo, committee: Committee | PreparedCommittee): void;
|
|
37
|
+
/**
|
|
38
|
+
* Verify that checkpoint contents match the content digest in a checkpoint summary.
|
|
39
|
+
*/
|
|
40
|
+
export declare function verifyCheckpointContents(summary: CheckpointSummary, contents: CheckpointContents): void;
|
|
41
|
+
/**
|
|
42
|
+
* Verify that a transaction (by digest) is included in checkpoint contents.
|
|
43
|
+
* Returns the execution digests (tx + effects) for the matched transaction.
|
|
44
|
+
*/
|
|
45
|
+
export declare function verifyTransactionInCheckpoint(txDigest: Uint8Array, contents: CheckpointContents): ExecutionDigests;
|
|
46
|
+
export declare function digestsEqual(a: Uint8Array, b: Uint8Array): boolean;
|
|
47
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unconfirmed/kei",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"files": ["dist"],
|
|
8
8
|
"bin": {
|
|
9
|
-
"kei": "
|
|
9
|
+
"kei": "dist/cli.js"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"
|
|
12
|
+
"prepublishOnly": "bun run build",
|
|
13
|
+
"build": "bun build src/index.ts --outdir dist --target node --external '@noble/*' --external '@mysten/*' && bun build src/cli.ts --outdir dist --target node --external '@noble/*' --external '@mysten/*' && bunx tsc --emitDeclarationOnly --outDir dist",
|
|
13
14
|
"test": "bun test",
|
|
14
15
|
"verify": "bun src/cli.ts verify"
|
|
15
16
|
},
|
package/src/cli.ts
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* CLI for testing Sui light client verification against live checkpoints.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* bun src/cli.ts verify <checkpoint_seq> [--url <fullnode_url>]
|
|
7
|
-
* bun src/cli.ts verify-range <from> <to> [--url <fullnode_url>]
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { decodeRoaringBitmap } from './bitmap.js';
|
|
11
|
-
import { verifyCheckpoint, PreparedCommittee } from './verify.js';
|
|
12
|
-
import type { Committee, AuthorityQuorumSignInfo } from './types.js';
|
|
13
|
-
|
|
14
|
-
type Network = 'testnet' | 'mainnet';
|
|
15
|
-
|
|
16
|
-
function usage(): never {
|
|
17
|
-
console.log(`Usage:
|
|
18
|
-
sui-light-client verify <checkpoint_seq> --network <testnet|mainnet> --url <grpc_url>
|
|
19
|
-
sui-light-client verify-range <from> <to> --network <testnet|mainnet> --url <grpc_url>
|
|
20
|
-
|
|
21
|
-
Environment variables (override flags):
|
|
22
|
-
GRPC_URL — fullnode gRPC endpoint
|
|
23
|
-
NETWORK — testnet or mainnet
|
|
24
|
-
|
|
25
|
-
Examples:
|
|
26
|
-
bun src/cli.ts verify 318460000 --network testnet --url https://fullnode.testnet.sui.io
|
|
27
|
-
GRPC_URL=https://fullnode.testnet.sui.io NETWORK=testnet bun src/cli.ts verify 318460000`);
|
|
28
|
-
process.exit(1);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function getFlag(args: string[], flag: string): string | undefined {
|
|
32
|
-
const idx = args.indexOf(flag);
|
|
33
|
-
return idx !== -1 ? args[idx + 1] : undefined;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function parseArgs() {
|
|
37
|
-
const args = process.argv.slice(2);
|
|
38
|
-
if (args.length === 0) usage();
|
|
39
|
-
|
|
40
|
-
const command = args[0];
|
|
41
|
-
const network = (process.env.NETWORK || getFlag(args, '--network')) as Network | undefined;
|
|
42
|
-
const url = process.env.GRPC_URL || getFlag(args, '--url');
|
|
43
|
-
|
|
44
|
-
if (!network || !url) {
|
|
45
|
-
console.error('Error: --network and --url are required (or set NETWORK and GRPC_URL env vars)\n');
|
|
46
|
-
usage();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (command === 'verify') {
|
|
50
|
-
const seq = args[1];
|
|
51
|
-
if (!seq || isNaN(Number(seq))) usage();
|
|
52
|
-
return { command: 'verify' as const, seq: Number(seq), network, url };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (command === 'verify-range') {
|
|
56
|
-
const from = args[1];
|
|
57
|
-
const to = args[2];
|
|
58
|
-
if (!from || !to || isNaN(Number(from)) || isNaN(Number(to))) usage();
|
|
59
|
-
return { command: 'verify-range' as const, from: Number(from), to: Number(to), network, url };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
usage();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
interface GrpcCheckpoint {
|
|
66
|
-
summary: { bcs: { value: Uint8Array } };
|
|
67
|
-
signature: { epoch: bigint; signature: Uint8Array; bitmap: Uint8Array };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
let _grpcClient: InstanceType<typeof import('@mysten/sui/grpc').SuiGrpcClient> | null = null;
|
|
71
|
-
async function getGrpcClient(network: Network, url: string) {
|
|
72
|
-
if (_grpcClient) return _grpcClient;
|
|
73
|
-
const { SuiGrpcClient } = await import('@mysten/sui/grpc');
|
|
74
|
-
_grpcClient = new SuiGrpcClient({ network, baseUrl: url });
|
|
75
|
-
return _grpcClient;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function fetchCheckpoint(network: Network, url: string, seq: number): Promise<GrpcCheckpoint> {
|
|
79
|
-
const client = await getGrpcClient(network, url);
|
|
80
|
-
const { response } = await client.ledgerService.getCheckpoint({
|
|
81
|
-
checkpointId: { oneofKind: 'sequenceNumber', sequenceNumber: BigInt(seq) },
|
|
82
|
-
readMask: { paths: ['summary.bcs', 'signature'] },
|
|
83
|
-
});
|
|
84
|
-
return response.checkpoint as unknown as GrpcCheckpoint;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async function fetchCommittee(url: string, epoch: string): Promise<Committee> {
|
|
88
|
-
const resp = await fetch(url, {
|
|
89
|
-
method: 'POST',
|
|
90
|
-
headers: { 'Content-Type': 'application/json' },
|
|
91
|
-
body: JSON.stringify({
|
|
92
|
-
jsonrpc: '2.0', id: 1,
|
|
93
|
-
method: 'suix_getCommitteeInfo',
|
|
94
|
-
params: [epoch],
|
|
95
|
-
}),
|
|
96
|
-
});
|
|
97
|
-
const json = (await resp.json()) as { result: { validators: [string, string][] } };
|
|
98
|
-
return {
|
|
99
|
-
epoch: BigInt(epoch),
|
|
100
|
-
members: json.result.validators.map(([pk, stake]) => ({
|
|
101
|
-
publicKey: new Uint8Array(Buffer.from(pk, 'base64')),
|
|
102
|
-
votingPower: BigInt(stake),
|
|
103
|
-
})),
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function extractAuthSignature(cp: GrpcCheckpoint): AuthorityQuorumSignInfo {
|
|
108
|
-
return {
|
|
109
|
-
epoch: cp.signature.epoch,
|
|
110
|
-
signature: cp.signature.signature,
|
|
111
|
-
signersMap: cp.signature.bitmap,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async function verifySingle(seq: number, network: Network, url: string) {
|
|
116
|
-
const total = performance.now();
|
|
117
|
-
|
|
118
|
-
process.stdout.write(`Fetching checkpoint ${seq}...`);
|
|
119
|
-
let t = performance.now();
|
|
120
|
-
const cp = await fetchCheckpoint(network, url, seq);
|
|
121
|
-
console.log(` ${(performance.now() - t).toFixed(0)}ms`);
|
|
122
|
-
|
|
123
|
-
const summaryBcs = cp.summary.bcs.value;
|
|
124
|
-
const authSignature = extractAuthSignature(cp);
|
|
125
|
-
const signers = decodeRoaringBitmap(authSignature.signersMap);
|
|
126
|
-
|
|
127
|
-
process.stdout.write(`Fetching committee for epoch ${authSignature.epoch}...`);
|
|
128
|
-
t = performance.now();
|
|
129
|
-
const committee = await fetchCommittee(url, authSignature.epoch.toString());
|
|
130
|
-
console.log(` ${(performance.now() - t).toFixed(0)}ms (${committee.members.length} validators)`);
|
|
131
|
-
|
|
132
|
-
process.stdout.write(`Verifying signature (${signers.length} signers)...`);
|
|
133
|
-
t = performance.now();
|
|
134
|
-
verifyCheckpoint(summaryBcs, authSignature, committee);
|
|
135
|
-
console.log(` ${(performance.now() - t).toFixed(0)}ms`);
|
|
136
|
-
|
|
137
|
-
console.log(`\nCheckpoint ${seq} verified in ${(performance.now() - total).toFixed(0)}ms`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async function verifyRange(from: number, to: number, network: Network, url: string) {
|
|
141
|
-
const count = to - from + 1;
|
|
142
|
-
console.log(`Verifying ${count} checkpoints (${from} → ${to})\n`);
|
|
143
|
-
|
|
144
|
-
process.stdout.write('Fetching first checkpoint...');
|
|
145
|
-
let t = performance.now();
|
|
146
|
-
const firstCp = await fetchCheckpoint(network, url, from);
|
|
147
|
-
const firstAuth = extractAuthSignature(firstCp);
|
|
148
|
-
console.log(` epoch ${firstAuth.epoch} (${(performance.now() - t).toFixed(0)}ms)`);
|
|
149
|
-
|
|
150
|
-
process.stdout.write('Preparing committee...');
|
|
151
|
-
t = performance.now();
|
|
152
|
-
const committee = await fetchCommittee(url, firstAuth.epoch.toString());
|
|
153
|
-
const prepared = new PreparedCommittee(committee);
|
|
154
|
-
console.log(` ${committee.members.length} validators, ${(performance.now() - t).toFixed(0)}ms\n`);
|
|
155
|
-
|
|
156
|
-
let verified = 0;
|
|
157
|
-
let totalVerifyMs = 0;
|
|
158
|
-
const batchStart = performance.now();
|
|
159
|
-
|
|
160
|
-
for (let seq = from; seq <= to; seq++) {
|
|
161
|
-
t = performance.now();
|
|
162
|
-
const cp = await fetchCheckpoint(network, url, seq);
|
|
163
|
-
const fetchMs = performance.now() - t;
|
|
164
|
-
|
|
165
|
-
const authSignature = extractAuthSignature(cp);
|
|
166
|
-
const signers = decodeRoaringBitmap(authSignature.signersMap);
|
|
167
|
-
|
|
168
|
-
t = performance.now();
|
|
169
|
-
verifyCheckpoint(cp.summary.bcs.value, authSignature, prepared);
|
|
170
|
-
const verifyMs = performance.now() - t;
|
|
171
|
-
totalVerifyMs += verifyMs;
|
|
172
|
-
verified++;
|
|
173
|
-
|
|
174
|
-
console.log(` [${verified}/${count}] seq=${seq} signers=${signers.length} fetch=${fetchMs.toFixed(0)}ms verify=${verifyMs.toFixed(0)}ms`);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const elapsed = performance.now() - batchStart;
|
|
178
|
-
console.log(`\n${verified} checkpoints verified in ${(elapsed / 1000).toFixed(1)}s`);
|
|
179
|
-
console.log(`Avg verify: ${(totalVerifyMs / verified).toFixed(1)}ms/checkpoint`);
|
|
180
|
-
console.log(`Throughput: ${(verified / (elapsed / 1000)).toFixed(1)} checkpoints/sec (including network)`);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
async function main() {
|
|
184
|
-
const parsed = parseArgs();
|
|
185
|
-
if (parsed.command === 'verify') {
|
|
186
|
-
await verifySingle(parsed.seq, parsed.network, parsed.url);
|
|
187
|
-
} else {
|
|
188
|
-
await verifyRange(parsed.from, parsed.to, parsed.network, parsed.url);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
main().catch((err) => {
|
|
193
|
-
console.error(err.message);
|
|
194
|
-
process.exit(1);
|
|
195
|
-
});
|