@unconfirmed/kei 0.0.1
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 +213 -0
- package/package.json +25 -0
- package/src/cli.ts +195 -0
package/README.md
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Kei
|
|
2
|
+
|
|
3
|
+
Lightweight Sui light client in pure TypeScript. Verify checkpoint signatures, committee transitions, and transaction inclusion using BLS12-381.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
Sui fullnodes return data, but how do you know it's correct? Kei verifies responses cryptographically by checking that checkpoint summaries are signed by a quorum of validators, then proving that transactions and objects are included in those certified checkpoints.
|
|
8
|
+
|
|
9
|
+
No native dependencies — works in browsers, Cloudflare Workers, Bun, and Node.js.
|
|
10
|
+
|
|
11
|
+
## How it works
|
|
12
|
+
|
|
13
|
+
Sui validators sign checkpoint summaries using BLS12-381 aggregate signatures. A checkpoint is valid if validators holding ≥66.67% of voting power (6667/10000) have signed it. Kei:
|
|
14
|
+
|
|
15
|
+
1. **Decodes the signers bitmap** (RoaringBitmap) to identify which validators signed
|
|
16
|
+
2. **Aggregates their BLS public keys** (G2 points in min-sig mode)
|
|
17
|
+
3. **Verifies the aggregate signature** against the BCS-serialized checkpoint summary
|
|
18
|
+
4. **Validates quorum** — total voting power of signers must meet the threshold
|
|
19
|
+
|
|
20
|
+
Once a checkpoint is verified, any data committed to it (transactions, objects, events) can be trusted via hash chains.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
Genesis → Committee₀ → verify Checkpoint₀ → Committee₁ → verify Checkpoint₁ → ...
|
|
24
|
+
↓
|
|
25
|
+
ContentDigest → CheckpointContents → TransactionDigest
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Performance
|
|
29
|
+
|
|
30
|
+
| Operation | Time | Notes |
|
|
31
|
+
|-----------|------|-------|
|
|
32
|
+
| `PreparedCommittee` init | ~118ms | One-time per epoch (~24h), parses all G2 pubkeys |
|
|
33
|
+
| Checkpoint verification | **~11ms** | With `PreparedCommittee` |
|
|
34
|
+
| Cold verification | ~188ms | Without pre-parsed keys |
|
|
35
|
+
| Throughput | **~88 checkpoints/sec** | Single core, prepared committee |
|
|
36
|
+
|
|
37
|
+
No WASM needed. The bottleneck was G2 point deserialization, not the BLS pairing math — `PreparedCommittee` pre-parses all public keys once per epoch, making per-checkpoint aggregation <1ms (point additions instead of decompression).
|
|
38
|
+
|
|
39
|
+
## CLI
|
|
40
|
+
|
|
41
|
+
Try it out against live checkpoints:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
# Set your fullnode endpoint
|
|
45
|
+
export GRPC_URL=https://fullnode.testnet.sui.io
|
|
46
|
+
export NETWORK=testnet
|
|
47
|
+
|
|
48
|
+
# Verify a single checkpoint
|
|
49
|
+
bun src/cli.ts verify 318460000
|
|
50
|
+
|
|
51
|
+
# Verify a range (uses PreparedCommittee for bulk speed)
|
|
52
|
+
bun src/cli.ts verify-range 318460000 318460009
|
|
53
|
+
|
|
54
|
+
# Or pass as flags
|
|
55
|
+
bun src/cli.ts verify 318460000 --network testnet --url https://fullnode.testnet.sui.io
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
$ bun src/cli.ts verify-range 318460000 318460009
|
|
60
|
+
|
|
61
|
+
Verifying 10 checkpoints (318460000 → 318460009)
|
|
62
|
+
|
|
63
|
+
Fetching first checkpoint... epoch 1052 (204ms)
|
|
64
|
+
Preparing committee... 118 validators, 232ms
|
|
65
|
+
|
|
66
|
+
[1/10] seq=318460000 signers=74 fetch=111ms verify=78ms
|
|
67
|
+
[2/10] seq=318460001 signers=73 fetch=95ms verify=11ms
|
|
68
|
+
[3/10] seq=318460002 signers=76 fetch=176ms verify=11ms
|
|
69
|
+
...
|
|
70
|
+
[10/10] seq=318460009 signers=75 fetch=96ms verify=10ms
|
|
71
|
+
|
|
72
|
+
10 checkpoints verified in 1.3s
|
|
73
|
+
Avg verify: 17.4ms/checkpoint
|
|
74
|
+
Throughput: 7.5 checkpoints/sec (including network)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Usage
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import {
|
|
81
|
+
verifyCheckpoint,
|
|
82
|
+
PreparedCommittee,
|
|
83
|
+
} from '@unconfirmed/kei';
|
|
84
|
+
|
|
85
|
+
// Build committee from validator data (once per epoch)
|
|
86
|
+
const committee = {
|
|
87
|
+
epoch: 1052n,
|
|
88
|
+
members: validators.map(({ publicKey, votingPower }) => ({
|
|
89
|
+
publicKey, // 96-byte BLS12-381 G2 compressed
|
|
90
|
+
votingPower,
|
|
91
|
+
})),
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Pre-parse for fast bulk verification
|
|
95
|
+
const prepared = new PreparedCommittee(committee);
|
|
96
|
+
|
|
97
|
+
// Verify using the raw BCS bytes from the gRPC response
|
|
98
|
+
// (the exact bytes validators signed — no re-serialization)
|
|
99
|
+
verifyCheckpoint(summaryBcsBytes, authSignature, prepared);
|
|
100
|
+
// Throws on invalid signature or insufficient quorum
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### With Sui gRPC API
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import { SuiGrpcClient } from '@mysten/sui/grpc';
|
|
107
|
+
import { verifyCheckpoint, PreparedCommittee } from '@unconfirmed/kei';
|
|
108
|
+
|
|
109
|
+
const client = new SuiGrpcClient({ network: 'testnet', baseUrl: 'https://fullnode.testnet.sui.io' });
|
|
110
|
+
|
|
111
|
+
// Fetch checkpoint with BCS summary + validator signature
|
|
112
|
+
const { response } = await client.ledgerService.getCheckpoint({
|
|
113
|
+
checkpointId: { oneofKind: 'sequenceNumber', sequenceNumber: '318460000' },
|
|
114
|
+
readMask: { paths: ['summary.bcs', 'signature'] },
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const cp = response.checkpoint!;
|
|
118
|
+
|
|
119
|
+
// Build auth signature from gRPC response
|
|
120
|
+
const authSignature = {
|
|
121
|
+
epoch: cp.signature!.epoch!,
|
|
122
|
+
signature: cp.signature!.signature!, // 48-byte BLS aggregate sig
|
|
123
|
+
signersMap: cp.signature!.bitmap!, // RoaringBitmap of signer indices
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// Verify using raw BCS bytes
|
|
127
|
+
verifyCheckpoint(cp.summary!.bcs!.value!, authSignature, prepared);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Committee transitions
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import { verifyCommitteeTransition, walkCommitteeChain, parseBcsSummary } from '@unconfirmed/kei';
|
|
134
|
+
|
|
135
|
+
// Parse the summary to read endOfEpochData
|
|
136
|
+
const summary = parseBcsSummary(summaryBcsBytes);
|
|
137
|
+
|
|
138
|
+
// Verify a single epoch transition from an end-of-epoch checkpoint
|
|
139
|
+
const nextCommittee = verifyCommitteeTransition(summaryBcsBytes, summary, authSignature, currentCommittee);
|
|
140
|
+
|
|
141
|
+
// Walk a chain of end-of-epoch checkpoints to advance multiple epochs
|
|
142
|
+
const latestCommittee = walkCommitteeChain(endOfEpochCheckpoints, trustedCommittee);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Cryptographic details
|
|
146
|
+
|
|
147
|
+
| Component | Implementation |
|
|
148
|
+
|-----------|---------------|
|
|
149
|
+
| Signature scheme | BLS12-381 min-sig (G2 pubkeys 96 bytes, G1 sigs 48 bytes) |
|
|
150
|
+
| Hash function | Blake2b-256 with struct name domain separators |
|
|
151
|
+
| Serialization | BCS (Binary Canonical Serialization) |
|
|
152
|
+
| Signers bitmap | RoaringBitmap (standard portable format) |
|
|
153
|
+
| Quorum threshold | 6667/10000 (Byzantine 2f+1) |
|
|
154
|
+
|
|
155
|
+
### Signed message format
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
[0x02, 0x00, 0x00] Intent: scope=CheckpointSummary, version=V0, app=Sui
|
|
159
|
+
|| BCS(CheckpointSummary) The checkpoint data
|
|
160
|
+
|| epoch (u64 LE) Appended epoch
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Digest computation
|
|
164
|
+
|
|
165
|
+
All Sui digests follow: `Blake2b-256("StructName::" || BCS(struct))`
|
|
166
|
+
|
|
167
|
+
## API
|
|
168
|
+
|
|
169
|
+
### `verifyCheckpoint(summaryBcs, authSignature, committee)`
|
|
170
|
+
|
|
171
|
+
Verify a checkpoint certificate using raw BCS bytes. Throws on failure.
|
|
172
|
+
|
|
173
|
+
### `PreparedCommittee`
|
|
174
|
+
|
|
175
|
+
Pre-parses G2 public keys for fast bulk verification. Create once per epoch.
|
|
176
|
+
|
|
177
|
+
### `parseBcsSummary(bcsBytes)`
|
|
178
|
+
|
|
179
|
+
Parse raw BCS bytes into a typed `CheckpointSummary`.
|
|
180
|
+
|
|
181
|
+
### `verifyCheckpointContents(summary, contents)`
|
|
182
|
+
|
|
183
|
+
Verify that checkpoint contents match the content digest in the summary.
|
|
184
|
+
|
|
185
|
+
### `verifyTransactionInCheckpoint(txDigest, contents)`
|
|
186
|
+
|
|
187
|
+
Prove a transaction exists in checkpoint contents. Returns the execution digests.
|
|
188
|
+
|
|
189
|
+
### `verifyCommitteeTransition(summaryBcs, summary, authSignature, committee)`
|
|
190
|
+
|
|
191
|
+
Verify an epoch transition and extract the next committee.
|
|
192
|
+
|
|
193
|
+
### `walkCommitteeChain(checkpoints, trustedCommittee)`
|
|
194
|
+
|
|
195
|
+
Walk multiple epoch transitions from a trusted starting committee.
|
|
196
|
+
|
|
197
|
+
### BCS schemas
|
|
198
|
+
|
|
199
|
+
`bcsCheckpointSummary`, `bcsCheckpointContents`, `bcsAuthorityQuorumSignInfo` — for parsing raw BCS bytes from gRPC responses.
|
|
200
|
+
|
|
201
|
+
### Utilities
|
|
202
|
+
|
|
203
|
+
`suiDigest(structName, bcsBytes)`, `decodeRoaringBitmap(data)`, `digestsEqual(a, b)`, and specific digest helpers (`checkpointDigest`, `transactionDigest`, etc.).
|
|
204
|
+
|
|
205
|
+
## Dependencies
|
|
206
|
+
|
|
207
|
+
- [`@noble/curves`](https://github.com/paulmillr/noble-curves) — BLS12-381 (audited, pure JS)
|
|
208
|
+
- [`@noble/hashes`](https://github.com/paulmillr/noble-hashes) — Blake2b-256 (audited, pure JS)
|
|
209
|
+
- [`@mysten/bcs`](https://github.com/MystenLabs/sui/tree/main/sdk/bcs) — BCS serialization
|
|
210
|
+
|
|
211
|
+
## License
|
|
212
|
+
|
|
213
|
+
Apache-2.0
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@unconfirmed/kei",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": ["dist"],
|
|
8
|
+
"bin": {
|
|
9
|
+
"kei": "src/cli.ts"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "bun build src/index.ts --outdir dist --target node",
|
|
13
|
+
"test": "bun test",
|
|
14
|
+
"verify": "bun src/cli.ts verify"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@mysten/bcs": "^1.3.0",
|
|
18
|
+
"@noble/curves": "^1.8.0",
|
|
19
|
+
"@noble/hashes": "^1.7.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@mysten/sui": "^2.13.0",
|
|
23
|
+
"bun-types": "^1.3.11"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
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
|
+
});
|