@@ -66,6 +66,7 @@ type UpOptions = {
6666 allowSchemaMismatch : boolean ;
6767 resume : boolean ;
6868 dryRun : boolean ;
69+ reset : boolean ;
6970} ;
7071
7172type CleanOptions = {
@@ -239,6 +240,7 @@ const parseCli = (argv: string[]) => {
239240 "instance-env" : { type : "string" , multiple : true , default : [ ] } ,
240241 "instance-arg" : { type : "string" , multiple : true , default : [ ] } ,
241242 "allow-schema-mismatch" : { type : "boolean" , default : false } ,
243+ reset : { type : "boolean" , default : false } ,
242244 } ,
243245 } ) ;
244246 const target = parsed . values . target as string ;
@@ -301,21 +303,45 @@ const bundleFromFile = async (target: UpOptions["target"], lockFile: string) =>
301303 return { ...bundle , target } ;
302304} ;
303305
304- const resolveBundle = async ( options : Pick < UpOptions , "target" | "sha" | "lockFile" > , deps : RuntimeDeps ) => {
305- const bundle = options . lockFile
306- ? await bundleFromFile ( options . target , options . lockFile )
307- : await resolveTarget ( options . target , createGitHubClient ( deps . runner ) , { sha : options . sha } ) ;
306+ const resolveCachePath = ( target : string , sha ?: string ) =>
307+ path . join ( LOCK_DIR , `.cache-${ target } ${ sha ? `-${ sha . toLowerCase ( ) . slice ( 0 , 7 ) } ` : "" } .json` ) ;
308+
309+ const cachedResolve = async (
310+ options : Pick < UpOptions , "target" | "sha" | "lockFile" | "reset" > ,
311+ deps : RuntimeDeps ,
312+ ) : Promise < VersionBundle > => {
313+ if ( options . lockFile ) {
314+ return bundleFromFile ( options . target , options . lockFile ) ;
315+ }
316+ const cachePath = resolveCachePath ( options . target , options . sha ) ;
317+ if ( ! options . reset ) {
318+ try {
319+ const bundle = await readJson < VersionBundle > ( cachePath ) ;
320+ if ( bundle . target === options . target ) {
321+ log ( "[resolve] using cached bundle" ) ;
322+ return bundle ;
323+ }
324+ } catch {
325+ // no cache or invalid — resolve fresh
326+ }
327+ }
328+ log ( "[resolve] fetching versions from GitHub..." ) ;
329+ const bundle = await resolveTarget ( options . target , createGitHubClient ( deps . runner ) , { sha : options . sha } ) ;
330+ await writeJson ( cachePath , bundle ) ;
331+ return bundle ;
332+ } ;
333+
334+ const resolveBundle = async ( options : Pick < UpOptions , "target" | "sha" | "lockFile" | "reset" > , deps : RuntimeDeps ) => {
335+ const bundle = await cachedResolve ( options , deps ) ;
308336 const resolved = applyVersionEnvOverrides ( bundle , deps . env ) ;
309337 const lockPath = await writeLock ( resolved ) ;
310338 return { bundle : resolved , lockPath } ;
311339} ;
312340
313- const previewBundle = ( options : Pick < UpOptions , "target" | "sha" | "lockFile" > , deps : RuntimeDeps ) =>
314- ( options . lockFile
315- ? bundleFromFile ( options . target , options . lockFile )
316- : resolveTarget ( options . target , createGitHubClient ( deps . runner ) , { sha : options . sha } ) ) . then ( ( bundle ) =>
317- applyVersionEnvOverrides ( bundle , deps . env ) ,
318- ) ;
341+ const previewBundle = async ( options : Pick < UpOptions , "target" | "sha" | "lockFile" | "reset" > , deps : RuntimeDeps ) => {
342+ const bundle = await cachedResolve ( options , deps ) ;
343+ return applyVersionEnvOverrides ( bundle , deps . env ) ;
344+ } ;
319345
320346const partialSchemaOverrides = ( overrides : LocalOverride [ ] ) =>
321347 overrides . filter (
@@ -468,29 +494,41 @@ const responseSnippet = async (response: Response) => {
468494 return text ? `: ${ text . slice ( 0 , 200 ) } ` : "" ;
469495} ;
470496
471- const discoverSigner = async ( deps : RuntimeDeps ) => {
472- for ( let attempt = 0 ; attempt < 30 ; attempt += 1 ) {
497+ /** Try both `PUB/PUB/` (KMS ≤ v0.12) and `PUB/` (KMS ≥ v0.13) prefixes. */
498+ const MINIO_KEY_PREFIXES = [ "PUB/PUB" , "PUB" ] as const ;
499+
500+ const discoverSigner = async ( deps : RuntimeDeps ) : Promise < { address : string ; minioKeyPrefix : string } > => {
501+ let lastHandle = "" ;
502+ for ( let attempt = 0 ; attempt < 60 ; attempt += 1 ) {
473503 const logs = await deps . runner ( [ "docker" , "logs" , "kms-core" ] , { allowFailure : true } ) ;
474504 const match = logs . stdout . match ( / h a n d l e ( [ a - z A - Z 0 - 9 ] + ) / ) ?? logs . stderr . match ( / h a n d l e ( [ a - z A - Z 0 - 9 ] + ) / ) ;
475505 if ( match ) {
476- try {
477- const response = await deps . fetch ( `http://localhost:9000/kms-public/PUB/VerfAddress/${ match [ 1 ] } ` ) ;
478- if ( ! response . ok ) {
479- throw new Error ( `Could not fetch KMS signer address (HTTP ${ response . status } )${ await responseSnippet ( response ) } ` ) ;
480- }
481- return ( await response . text ( ) ) . trim ( ) ;
482- } catch ( error ) {
483- if ( shouldLogRetry ( attempt ) ) {
484- log ( `[wait] kms signer fetch: ${ toError ( error ) . message } ` ) ;
506+ lastHandle = match [ 1 ] ;
507+ for ( const prefix of MINIO_KEY_PREFIXES ) {
508+ try {
509+ const response = await deps . fetch ( `http://localhost:9000/kms-public/${ prefix } /VerfAddress/${ lastHandle } ` ) ;
510+ if ( response . ok ) {
511+ return { address : ( await response . text ( ) ) . trim ( ) , minioKeyPrefix : prefix } ;
512+ }
513+ // Consume body to avoid leaking connections
514+ await response . text ( ) ;
515+ } catch {
516+ // network error — retry
485517 }
486518 }
487- }
488- if ( shouldLogRetry ( attempt ) ) {
519+ if ( shouldLogRetry ( attempt ) ) {
520+ log ( `[wait] kms signer fetch (handle: ${ lastHandle . slice ( 0 , 12 ) } …)` ) ;
521+ }
522+ } else if ( shouldLogRetry ( attempt ) ) {
489523 log ( "[wait] kms signer handle" ) ;
490524 }
491525 await sleep ( 1000 ) ;
492526 }
493- throw new Error ( "Could not extract KMS signer handle from kms-core logs after 30 attempts" ) ;
527+ throw new Error (
528+ lastHandle
529+ ? `KMS signer address not available in MinIO after 60 attempts (handle: ${ lastHandle } )`
530+ : "Could not extract KMS signer handle from kms-core logs after 60 attempts" ,
531+ ) ;
494532} ;
495533
496534const waitForKmsCore = async ( deps : RuntimeDeps ) => {
@@ -609,14 +647,15 @@ export const probeBootstrap = async (state: State, deps: RuntimeDeps, attempt =
609647 const actualFheKeyId = uint256ToId ( actualKey ) ;
610648 const actualCrsKeyId = uint256ToId ( actualCrs ) ;
611649 // Keys are on-chain — material MUST appear. Let ensureMaterialUrl throw if it doesn't.
650+ const kp = state . discovery ! . minioKeyPrefix ?? "PUB" ;
612651 await Promise . all ( [
613652 ensureMaterialUrl (
614653 deps ,
615- hostReachableMaterialUrl ( `${ state . discovery ! . endpoints . minioExternal } /kms-public/PUB /PublicKey/${ actualFheKeyId } ` ) ,
654+ hostReachableMaterialUrl ( `${ state . discovery ! . endpoints . minioExternal } /kms-public/${ kp } /PublicKey/${ actualFheKeyId } ` ) ,
616655 ) ,
617656 ensureMaterialUrl (
618657 deps ,
619- hostReachableMaterialUrl ( `${ state . discovery ! . endpoints . minioExternal } /kms-public/PUB /CRS/${ actualCrsKeyId } ` ) ,
658+ hostReachableMaterialUrl ( `${ state . discovery ! . endpoints . minioExternal } /kms-public/${ kp } /CRS/${ actualCrsKeyId } ` ) ,
620659 ) ,
621660 ] ) ;
622661 state . discovery ! . actualFheKeyId = actualFheKeyId ;
@@ -892,7 +931,9 @@ const runStep = async (state: State, step: StepName, deps: RuntimeDeps) => {
892931 minioExternal : `http://${ await minioIp ( deps ) } :9000` ,
893932 } ,
894933 } ;
895- state . discovery . kmsSigner = await discoverSigner ( deps ) ;
934+ const signer = await discoverSigner ( deps ) ;
935+ state . discovery . kmsSigner = signer . address ;
936+ state . discovery . minioKeyPrefix = signer . minioKeyPrefix ;
896937 await regen ( state , deps ) ;
897938 break ;
898939 case "gateway-deploy" :
@@ -908,6 +949,7 @@ const runStep = async (state: State, step: StepName, deps: RuntimeDeps) => {
908949 crsKeyId : state . discovery ?. crsKeyId ?? predictedCrsId ( ) ,
909950 actualFheKeyId : state . discovery ?. actualFheKeyId ,
910951 actualCrsKeyId : state . discovery ?. actualCrsKeyId ,
952+ minioKeyPrefix : state . discovery ?. minioKeyPrefix ,
911953 endpoints : state . discovery ?. endpoints ?? {
912954 gatewayHttp : "http://gateway-node:8546" ,
913955 gatewayWs : "ws://gateway-node:8546" ,
@@ -943,6 +985,7 @@ const runStep = async (state: State, step: StepName, deps: RuntimeDeps) => {
943985 crsKeyId : state . discovery ?. crsKeyId ?? predictedCrsId ( ) ,
944986 actualFheKeyId : state . discovery ?. actualFheKeyId ,
945987 actualCrsKeyId : state . discovery ?. actualCrsKeyId ,
988+ minioKeyPrefix : state . discovery ?. minioKeyPrefix ,
946989 endpoints : state . discovery ?. endpoints ?? {
947990 gatewayHttp : "http://gateway-node:8546" ,
948991 gatewayWs : "ws://gateway-node:8546" ,
@@ -1076,6 +1119,10 @@ const runUp = async (options: UpOptions, deps: RuntimeDeps) => {
10761119 if ( options . resume && ! state ) {
10771120 throw new Error ( "No .fhevm/state.json to resume from" ) ;
10781121 }
1122+ if ( ! options . resume && ( await loadState ( ) ) ) {
1123+ log ( "[up] cleaning previous run" ) ;
1124+ await runDown ( deps ) ;
1125+ }
10791126 if ( ! state ) {
10801127 state = await bootstrapState ( options , deps ) ;
10811128 }
@@ -1454,6 +1501,7 @@ up options:
14541501 --from-step <${ STEP_NAMES . join ( "|" ) } > requires --resume, except in --dry-run
14551502 --resume
14561503 --dry-run
1504+ --reset re-resolve versions from GitHub (ignore cache)
14571505
14581506clean options:
14591507 --images remove CLI-owned local override images too
@@ -1462,9 +1510,10 @@ clean options:
14621510
14631511export const main = async ( argv = process . argv , deps : Partial < RuntimeDeps > = { } ) => {
14641512 const runtime = { ...defaultDeps , ...deps } ;
1513+ let command : string | undefined ;
14651514 try {
14661515 const parsed = parseCli ( argv ) ;
1467- const command = parsed . command === "deploy" ? "up" : parsed . command ;
1516+ command = parsed . command === "deploy" ? "up" : parsed . command ;
14681517 const fromStep = ensureStep ( parsed . parsed . values [ "from-step" ] as string | undefined ) ;
14691518 if ( command === "up" && fromStep && ! parsed . parsed . values . resume && ! parsed . parsed . values [ "dry-run" ] ) {
14701519 throw new Error ( "--from-step requires --resume or --dry-run" ) ;
@@ -1481,6 +1530,7 @@ export const main = async (argv = process.argv, deps: Partial<RuntimeDeps> = {})
14811530 fromStep,
14821531 lockFile : parsed . parsed . values [ "lock-file" ] as string | undefined ,
14831532 allowSchemaMismatch : parsed . parsed . values [ "allow-schema-mismatch" ] ,
1533+ reset : parsed . parsed . values . reset ,
14841534 } ,
14851535 runtime ,
14861536 ) ;
@@ -1497,6 +1547,7 @@ export const main = async (argv = process.argv, deps: Partial<RuntimeDeps> = {})
14971547 allowSchemaMismatch : parsed . parsed . values [ "allow-schema-mismatch" ] ,
14981548 resume : parsed . parsed . values . resume ,
14991549 dryRun : parsed . parsed . values [ "dry-run" ] ,
1550+ reset : parsed . parsed . values . reset ,
15001551 } ,
15011552 runtime ,
15021553 ) ;
@@ -1534,6 +1585,8 @@ export const main = async (argv = process.argv, deps: Partial<RuntimeDeps> = {})
15341585 case "doctor" :
15351586 throw new Error ( "`doctor` was removed; use `fhevm-cli up --dry-run ...`" ) ;
15361587 case "help" :
1588+ case "--help" :
1589+ case "-h" :
15371590 case undefined :
15381591 usage ( ) ;
15391592 return ;
@@ -1542,6 +1595,9 @@ export const main = async (argv = process.argv, deps: Partial<RuntimeDeps> = {})
15421595 }
15431596 } catch ( error ) {
15441597 console . error ( toError ( error ) . message ) ;
1598+ if ( command === "up" && ( await loadState ( ) ) ) {
1599+ console . error ( "Hint: run with --resume to continue, or without to start fresh." ) ;
1600+ }
15451601 process . exitCode = 1 ;
15461602 }
15471603} ;
0 commit comments