33import { bcs , fromBase64 , fromHex , toBase64 , toHex } from '@mysten/bcs' ;
44import { bls12_381 } from '@noble/curves/bls12-381' ;
55
6- import { KeyServerMove , KeyServerMoveV1 } from './bcs.js' ;
6+ import { KeyServerMove , KeyServerMoveV1 , KeyServerMoveV2 } from './bcs.js' ;
77import { InvalidKeyServerError , InvalidKeyServerVersionError , SealAPIError } from './error.js' ;
88import { DST_POP } from './ibe.js' ;
99import { PACKAGE_VERSION } from './version.js' ;
@@ -13,14 +13,14 @@ import { flatten, Version } from './utils.js';
1313import { elgamalDecrypt } from './elgamal.js' ;
1414import type { Certificate } from './session-key.js' ;
1515
16- const EXPECTED_SERVER_VERSION = 1 ;
17-
1816export type KeyServer = {
1917 objectId : string ;
2018 name : string ;
2119 url : string ;
2220 keyType : KeyServerType ;
2321 pk : Uint8Array < ArrayBuffer > ;
22+ /** Server type: 'Independent' or 'Committee' */
23+ serverType : 'Independent' | 'Committee' ;
2424} ;
2525
2626export enum KeyServerType {
@@ -33,6 +33,9 @@ export const SERVER_VERSION_REQUIREMENT = new Version('0.4.1');
3333 * Given a list of key server object IDs, returns a list of SealKeyServer
3434 * from onchain state containing name, objectId, URL and pk.
3535 *
36+ * Supports both V1 (independent servers) and V2 (independent + committee servers).
37+ * For V2 committee servers, returns the aggregator URL which clients use to fetch keys.
38+ *
3639 * @param objectIds - The key server object IDs.
3740 * @param client - The SuiClient to use.
3841 * @returns - An array of SealKeyServer.
@@ -51,39 +54,94 @@ export async function retrieveKeyServers({
5154 objectId,
5255 } ) ;
5356 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- ) {
57+
58+ // Determine which version to use (prefer V2 if available)
59+ let version : number ;
60+ if ( Number ( ks . firstVersion ) <= 2 && Number ( ks . lastVersion ) >= 2 ) {
61+ version = 2 ;
62+ } else if ( Number ( ks . firstVersion ) <= 1 && Number ( ks . lastVersion ) >= 1 ) {
63+ version = 1 ;
64+ } else {
5865 throw new InvalidKeyServerVersionError (
59- `Key server ${ objectId } supports versions between ${ ks . firstVersion } and ${ ks . lastVersion } (inclusive), but SDK expects version ${ EXPECTED_SERVER_VERSION } ` ,
66+ `Key server ${ objectId } supports versions between ${ ks . firstVersion } and ${ ks . lastVersion } (inclusive), but SDK expects version 1 or 2 ` ,
6067 ) ;
6168 }
6269
63- // Then fetch the expected versioned object and parse it.
70+ // Fetch the versioned object
6471 const resVersionedKs = await client . core . getDynamicField ( {
6572 parentId : objectId ,
6673 name : {
6774 type : 'u64' ,
68- bcs : bcs . u64 ( ) . serialize ( EXPECTED_SERVER_VERSION ) . toBytes ( ) ,
75+ bcs : bcs . u64 ( ) . serialize ( version ) . toBytes ( ) ,
6976 } ,
7077 } ) ;
7178
72- const ksVersioned = KeyServerMoveV1 . parse ( resVersionedKs . dynamicField . value . bcs ) ;
79+ if ( version === 2 ) {
80+ // Parse V2 key server (supports both Independent and Committee modes)
81+ const ksV2 = KeyServerMoveV2 . parse ( resVersionedKs . dynamicField . value . bcs ) ;
7382
74- if ( ksVersioned . keyType !== KeyServerType . BonehFranklinBLS12381 ) {
75- throw new InvalidKeyServerError (
76- `Server ${ objectId } has invalid key type: ${ ksVersioned . keyType } ` ,
77- ) ;
78- }
83+ // Handle Move hex literal format: x"0x..."
84+ // If pk starts with 'x"0x', it's stored as ASCII and needs to be converted
85+ let pk = ksV2 . pk ;
86+ if ( pk . length > 96 && pk [ 0 ] === 120 && pk [ 1 ] === 34 && pk [ 2 ] === 48 && pk [ 3 ] === 120 ) {
87+ // Decode Move hex literal: x"0x..." -> bytes
88+ const hexStr = String . fromCharCode ( ...pk ) ;
89+ // Extract hex string between x"0x and "
90+ const match = hexStr . match ( / ^ x " ( 0 x [ 0 - 9 a - f A - F ] + ) " $ / ) ;
91+ if ( match ) {
92+ pk = new Uint8Array ( fromHex ( match [ 1 ] ) ) ;
93+ }
94+ }
7995
80- return {
81- objectId,
82- name : ksVersioned . name ,
83- url : ksVersioned . url ,
84- keyType : ksVersioned . keyType ,
85- pk : new Uint8Array ( ksVersioned . pk ) ,
86- } ;
96+ if ( ksV2 . keyType !== KeyServerType . BonehFranklinBLS12381 ) {
97+ throw new InvalidKeyServerError (
98+ `Server ${ objectId } has invalid key type: ${ ksV2 . keyType } ` ,
99+ ) ;
100+ }
101+
102+ // Extract URL and server type
103+ let url : string ;
104+ let serverType : 'Independent' | 'Committee' ;
105+
106+ if ( ksV2 . serverType . $kind === 'Independent' ) {
107+ url = ksV2 . serverType . Independent . url ;
108+ serverType = 'Independent' ;
109+ } else if ( ksV2 . serverType . $kind === 'Committee' ) {
110+ // For committee mode, URL will be provided by client config
111+ // The aggregator is not registered onchain
112+ url = '' ; // Placeholder, will be replaced by config
113+ serverType = 'Committee' ;
114+ } else {
115+ throw new InvalidKeyServerError ( `Unknown server type for ${ objectId } ` ) ;
116+ }
117+
118+ return {
119+ objectId,
120+ name : ksV2 . name ,
121+ url,
122+ keyType : ksV2 . keyType ,
123+ pk : new Uint8Array ( pk ) , // Aggregated public key (converted from Move hex literal if needed)
124+ serverType,
125+ } ;
126+ } else {
127+ // Parse V1 key server (backward compatibility)
128+ const ksV1 = KeyServerMoveV1 . parse ( resVersionedKs . dynamicField . value . bcs ) ;
129+
130+ if ( ksV1 . keyType !== KeyServerType . BonehFranklinBLS12381 ) {
131+ throw new InvalidKeyServerError (
132+ `Server ${ objectId } has invalid key type: ${ ksV1 . keyType } ` ,
133+ ) ;
134+ }
135+
136+ return {
137+ objectId,
138+ name : ksV1 . name ,
139+ url : ksV1 . url ,
140+ keyType : ksV1 . keyType ,
141+ pk : new Uint8Array ( ksV1 . pk ) ,
142+ serverType : 'Independent' ,
143+ } ;
144+ }
87145 } ) ,
88146 ) ;
89147}
0 commit comments