Skip to content

Commit 9f4cf06

Browse files
committed
[seal] handle committee v2 servers
1 parent 9899370 commit 9f4cf06

File tree

8 files changed

+253
-28
lines changed

8 files changed

+253
-28
lines changed

packages/seal/src/bcs.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,44 @@ export const KeyServerMoveV1 = bcs.struct('KeyServerV1', {
5151
pk: bcs.byteVector(),
5252
});
5353

54+
/**
55+
* The Move struct for PartialKeyServer.
56+
*/
57+
export const PartialKeyServer = bcs.struct('PartialKeyServer', {
58+
partialPk: bcs.byteVector(),
59+
url: bcs.string(),
60+
partyId: bcs.u16(),
61+
});
62+
63+
/**
64+
* The Move enum for ServerType (V2).
65+
*/
66+
export const ServerType = bcs.enum('ServerType', {
67+
Independent: bcs.struct('Independent', {
68+
url: bcs.string(),
69+
}),
70+
Committee: bcs.struct('Committee', {
71+
version: bcs.u32(),
72+
threshold: bcs.u16(),
73+
partialKeyServers: bcs.vector(
74+
bcs.struct('VecMapEntry', {
75+
key: bcs.Address,
76+
value: PartialKeyServer,
77+
}),
78+
),
79+
}),
80+
});
81+
82+
/**
83+
* The Move struct for the KeyServerV2 object.
84+
*/
85+
export const KeyServerMoveV2 = bcs.struct('KeyServerV2', {
86+
name: bcs.string(),
87+
keyType: bcs.u8(),
88+
pk: bcs.byteVector(),
89+
serverType: ServerType,
90+
});
91+
5492
/**
5593
* The Move struct for the parent object.
5694
*/

packages/seal/src/client.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ export class SealClient {
262262
await retrieveKeyServers({
263263
objectIds: missingKeyServers,
264264
client: this.#suiClient,
265+
configs: new Map(),
265266
})
266267
).forEach((keyServer) =>
267268
this.#cachedPublicKeys.set(keyServer.objectId, G2Element.fromBytes(keyServer.pk)),
@@ -295,8 +296,9 @@ export class SealClient {
295296

296297
async #loadKeyServers(): Promise<Map<string, KeyServer>> {
297298
const keyServers = await retrieveKeyServers({
298-
objectIds: [...this.#configs].map(([objectId]) => objectId),
299+
objectIds: [...this.#configs.keys()],
299300
client: this.#suiClient,
301+
configs: this.#configs,
300302
});
301303

302304
if (keyServers.length === 0) {
@@ -306,6 +308,10 @@ export class SealClient {
306308
if (this.#verifyKeyServers) {
307309
await Promise.all(
308310
keyServers.map(async (server) => {
311+
// Skip /service verification for committee/aggregator servers
312+
if (server.serverType === 'Committee') {
313+
return;
314+
}
309315
const config = this.#configs.get(server.objectId);
310316
if (!(await verifyKeyServer(server, this.#timeout, config?.apiKeyName, config?.apiKey))) {
311317
throw new InvalidKeyServerError(`Key server ${server.objectId} is not valid`);

packages/seal/src/key-server.ts

Lines changed: 85 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,30 @@
33
import { bcs, fromBase64, fromHex, toBase64, toHex } from '@mysten/bcs';
44
import { bls12_381 } from '@noble/curves/bls12-381';
55

6-
import { KeyServerMove, KeyServerMoveV1 } from './bcs.js';
7-
import { InvalidKeyServerError, InvalidKeyServerVersionError, SealAPIError } from './error.js';
6+
import { KeyServerMove, KeyServerMoveV1, KeyServerMoveV2 } from './bcs.js';
7+
import {
8+
InvalidClientOptionsError,
9+
InvalidKeyServerError,
10+
InvalidKeyServerVersionError,
11+
SealAPIError,
12+
} from './error.js';
813
import { DST_POP } from './ibe.js';
914
import { PACKAGE_VERSION } from './version.js';
10-
import type { SealCompatibleClient } from './types.js';
15+
import type { KeyServerConfig, SealCompatibleClient } from './types.js';
1116
import type { G1Element } from './bls12381.js';
1217
import { flatten, Version } from './utils.js';
1318
import { elgamalDecrypt } from './elgamal.js';
1419
import type { Certificate } from './session-key.js';
1520

16-
const EXPECTED_SERVER_VERSION = 1;
21+
const SUPPORTED_SERVER_VERSIONS = [2, 1]; // Try V2 first, fallback to V1
1722

1823
export type KeyServer = {
1924
objectId: string;
2025
name: string;
2126
url: string;
2227
keyType: KeyServerType;
2328
pk: Uint8Array<ArrayBuffer>;
29+
serverType: 'Independent' | 'Committee';
2430
};
2531

2632
export enum KeyServerType {
@@ -33,16 +39,22 @@ export const SERVER_VERSION_REQUIREMENT = new Version('0.4.1');
3339
* Given a list of key server object IDs, returns a list of SealKeyServer
3440
* from onchain state containing name, objectId, URL and pk.
3541
*
42+
* Supports both V1 (independent servers) and V2 (independent + committee servers).
43+
* For V2 committee servers, returns the aggregator URL from the config.
44+
*
3645
* @param objectIds - The key server object IDs.
3746
* @param client - The SuiClient to use.
47+
* @param configs - The key server configurations containing aggregator URLs.
3848
* @returns - An array of SealKeyServer.
3949
*/
4050
export async function retrieveKeyServers({
4151
objectIds,
4252
client,
53+
configs,
4354
}: {
4455
objectIds: string[];
4556
client: SealCompatibleClient;
57+
configs: Map<string, KeyServerConfig>;
4658
}): Promise<KeyServer[]> {
4759
return await Promise.all(
4860
objectIds.map(async (objectId) => {
@@ -51,39 +63,88 @@ export async function retrieveKeyServers({
5163
objectId,
5264
});
5365
const ks = KeyServerMove.parse(await res.object.content);
54-
if (
55-
EXPECTED_SERVER_VERSION < Number(ks.firstVersion) ||
56-
EXPECTED_SERVER_VERSION > Number(ks.lastVersion)
57-
) {
66+
67+
// Find the highest supported version
68+
let version: number | null = null;
69+
for (const v of SUPPORTED_SERVER_VERSIONS) {
70+
if (Number(ks.firstVersion) <= v && Number(ks.lastVersion) >= v) {
71+
version = v;
72+
break;
73+
}
74+
}
75+
76+
if (version === null) {
5877
throw new InvalidKeyServerVersionError(
59-
`Key server ${objectId} supports versions between ${ks.firstVersion} and ${ks.lastVersion} (inclusive), but SDK expects version ${EXPECTED_SERVER_VERSION}`,
78+
`Key server ${objectId} supports versions between ${ks.firstVersion} and ${ks.lastVersion} (inclusive), but SDK expects version 1 or 2`,
6079
);
6180
}
6281

63-
// Then fetch the expected versioned object and parse it.
82+
// Fetch the versioned object
6483
const resVersionedKs = await client.core.getDynamicField({
6584
parentId: objectId,
6685
name: {
6786
type: 'u64',
68-
bcs: bcs.u64().serialize(EXPECTED_SERVER_VERSION).toBytes(),
87+
bcs: bcs.u64().serialize(version).toBytes(),
6988
},
7089
});
7190

72-
const ksVersioned = KeyServerMoveV1.parse(resVersionedKs.dynamicField.value.bcs);
91+
if (version === 2) {
92+
// Parse V2 key server
93+
const ksV2 = KeyServerMoveV2.parse(resVersionedKs.dynamicField.value.bcs);
94+
if (ksV2.keyType !== KeyServerType.BonehFranklinBLS12381) {
95+
throw new InvalidKeyServerError(
96+
`Server ${objectId} has invalid key type: ${ksV2.keyType}`,
97+
);
98+
}
7399

74-
if (ksVersioned.keyType !== KeyServerType.BonehFranklinBLS12381) {
75-
throw new InvalidKeyServerError(
76-
`Server ${objectId} has invalid key type: ${ksVersioned.keyType}`,
77-
);
78-
}
100+
// Extract URL and server type
101+
let url: string;
102+
let serverType: 'Independent' | 'Committee';
79103

80-
return {
81-
objectId,
82-
name: ksVersioned.name,
83-
url: ksVersioned.url,
84-
keyType: ksVersioned.keyType,
85-
pk: new Uint8Array(ksVersioned.pk),
86-
};
104+
if (ksV2.serverType.$kind === 'Independent') {
105+
url = ksV2.serverType.Independent.url;
106+
serverType = 'Independent';
107+
} else if (ksV2.serverType.$kind === 'Committee') {
108+
// For committee mode, get aggregator URL from config
109+
const config = configs.get(objectId);
110+
if (!config?.aggregatorUrl) {
111+
throw new InvalidClientOptionsError(
112+
`Committee server ${objectId} requires aggregatorUrl in config`,
113+
);
114+
}
115+
url = config.aggregatorUrl;
116+
serverType = 'Committee';
117+
} else {
118+
throw new InvalidKeyServerError(`Unknown server type for ${objectId}`);
119+
}
120+
121+
return {
122+
objectId,
123+
name: ksV2.name,
124+
url,
125+
keyType: ksV2.keyType,
126+
pk: new Uint8Array(ksV2.pk),
127+
serverType,
128+
};
129+
} else {
130+
// Parse V1 key server
131+
const ksV1 = KeyServerMoveV1.parse(resVersionedKs.dynamicField.value.bcs);
132+
133+
if (ksV1.keyType !== KeyServerType.BonehFranklinBLS12381) {
134+
throw new InvalidKeyServerError(
135+
`Server ${objectId} has invalid key type: ${ksV1.keyType}`,
136+
);
137+
}
138+
139+
return {
140+
objectId,
141+
name: ksV1.name,
142+
url: ksV1.url,
143+
keyType: ksV1.keyType,
144+
pk: new Uint8Array(ksV1.pk),
145+
serverType: 'Independent',
146+
};
147+
}
87148
}),
88149
);
89150
}

packages/seal/src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ export interface KeyServerConfig {
3636
weight: number;
3737
apiKeyName?: string;
3838
apiKey?: string;
39+
/** Must be provided for committee mode server. The SDK calls to the aggregator URL which fans
40+
* out to all committee servers. */
41+
aggregatorUrl?: string;
3942
}
4043

4144
export interface SealClientOptions extends SealClientExtensionOptions {
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright (c) Mysten Labs, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { fromHex } from '@mysten/bcs';
5+
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
6+
import { Transaction } from '@mysten/sui/transactions';
7+
import { getFullnodeUrl, SuiClient } from '@mysten/sui/client';
8+
import { describe, it, expect } from 'vitest';
9+
10+
import { SealClient } from '../../src/client.js';
11+
import { SessionKey } from '../../src/session-key.js';
12+
13+
/**
14+
* Committee Aggregator Integration Tests
15+
*
16+
* Requires a running aggregator server and committee key servers:
17+
* cd /path/to/seal
18+
* bash scripts/start-committee-servers.sh
19+
* Wait for all servers ready and run this test.
20+
* ✓ Key Server 0 is ready
21+
* ✓ Key Server 1 is ready
22+
* ✓ Key Server 2 is ready
23+
* ✓ Aggregator is ready
24+
*/
25+
describe('Committee Aggregator Tests', () => {
26+
it('encrypt and decrypt through aggregator', { timeout: 12000 }, async () => {
27+
// Committee key server configuration
28+
const COMMITTEE_KEY_SERVER_OBJ_ID =
29+
'0x0c9b2a1185f42bebdc16baf0a393ec5bd93bab8b0cb902b694198077b27c15da';
30+
const INDEPENDENT_SERVER_OBJ_ID =
31+
'0x71a3962c5d06a94d1ef5a9c0e7d63ad72cefb48acc93001eaa7ba13fab52786e';
32+
// also works with 0x81aeaa8c25d2c912e1dc23b4372305b7a602c4ec4cc3e510963bc635e500aa37
33+
const AGGREGATOR_URL = 'http://localhost:2027';
34+
const PACKAGE_ID = '0x58dce5d91278bceb65d44666ffa225ab397fc3ae9d8398c8c779c5530bd978c2'; // Testnet package with account_based policy
35+
36+
const testKeypair = Ed25519Keypair.generate();
37+
const testAddress = testKeypair.getPublicKey().toSuiAddress();
38+
39+
const testData = crypto.getRandomValues(new Uint8Array(100));
40+
const suiClient = new SuiClient({ url: getFullnodeUrl('testnet') });
41+
42+
// Create Seal client with aggregator URL
43+
const client = new SealClient({
44+
suiClient,
45+
serverConfigs: [
46+
{
47+
objectId: COMMITTEE_KEY_SERVER_OBJ_ID,
48+
weight: 1,
49+
aggregatorUrl: AGGREGATOR_URL,
50+
},
51+
{
52+
objectId: INDEPENDENT_SERVER_OBJ_ID,
53+
weight: 1,
54+
},
55+
],
56+
verifyKeyServers: false,
57+
});
58+
59+
// Encrypt with policy and 2 servers (1 for committee, 1 for independent)
60+
const { encryptedObject: encryptedBytes } = await client.encrypt({
61+
threshold: 1,
62+
packageId: PACKAGE_ID,
63+
id: testAddress,
64+
data: testData,
65+
});
66+
67+
// Create session key
68+
const sessionKey = await SessionKey.create({
69+
address: testAddress,
70+
packageId: PACKAGE_ID,
71+
ttlMin: 10,
72+
signer: testKeypair,
73+
suiClient,
74+
});
75+
76+
// Build transaction
77+
const tx = new Transaction();
78+
const keyIdArg = tx.pure.vector('u8', fromHex(testAddress));
79+
tx.moveCall({
80+
target: `${PACKAGE_ID}::account_based::seal_approve`,
81+
arguments: [keyIdArg],
82+
});
83+
const txBytes = await tx.build({ client: suiClient, onlyTransactionKind: true });
84+
85+
// Decrypt data through aggregator
86+
const decryptedData = await client.decrypt({
87+
data: encryptedBytes,
88+
sessionKey,
89+
txBytes,
90+
});
91+
92+
// Verify decrypted data matches original
93+
expect(decryptedData).toEqual(testData);
94+
});
95+
});

0 commit comments

Comments
 (0)