@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.
Files changed (3) hide show
  1. package/README.md +213 -0
  2. package/package.json +25 -0
  3. 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
+ });