44 * Drives the two ProtocolConfig KMS-context lifecycle flows end to end on a threshold-mode cluster and
55 * proves the KMS reacts to the emitted events:
66 * 1. NewKmsContext — broadcast `defineNewKmsContextAndEpoch` (same committee, so no new signing
7- * keys are needed), wait for the new context to become the on-chain active one, then decrypt.
7+ * keys are needed; on a spare-core cluster, a node swap with the dropped node's tx-sender
8+ * stopped first), wait for the new context to become the on-chain active one, then decrypt.
89 * 2. NewKmsEpoch — broadcast `defineNewEpochForCurrentKmsContext` (same context, new epoch), wait
910 * for the new epoch to activate, then decrypt again.
1011 *
2223import { PreflightError } from "../errors" ;
2324import { castCall , resolveKmsGenerationTarget , waitForContainer } from "../flow/readiness" ;
2425import { stepComposeTask } from "../flow/runtime-compose" ;
25- import { reconstructionThreshold } from "../kms-party" ;
26+ import { kmsTxSenderName , reconstructionThreshold } from "../kms-party" ;
2627import type { State } from "../types" ;
2728import {
2829 type DecryptionRunner ,
2930 partyContainers ,
3031 setRunning ,
32+ waitForContainersStopped ,
3133 waitForPartiesRunning ,
3234 waitForPartiesStopped ,
3335} from "./kms-generation" ;
@@ -104,11 +106,13 @@ const committeeSwapPlan = (kms: State["scenario"]["kms"]) => {
104106
105107/**
106108 * NewKmsContext step. On a cluster with a spare core this is a genuine node swap: the new context
107- * drops a committee node and promotes the spare, keeping n = committeeSize so the threshold stays
108- * valid. The dropped node reshares as Set 1 (outgoing), the spare as Set 2 (incoming), the rest as
109- * both. Without a spare it is a same-committee reshare. Activation already gates on the spare:
110- * it requires ALL new-committee signers to confirm, so the context id only advances once the
111- * spare has reshared and submitted confirmKmsContextCreation.
109+ * drops a committee node and promotes the spare, keeping n = committeeSize. The dropped node's
110+ * tx-sender is stopped before the broadcast, so the switch must complete on the n − t
111+ * previous-side quorum without its confirmation — a node that cannot transact must not veto its
112+ * own removal. The rest of the node stays up because the reshare still needs its core (the KMS
113+ * core cannot yet reshare around an absent outgoing party); upgrade to a full party stop once it
114+ * can. Without a spare it is a same-committee reshare. Activation gates on ALL new-committee
115+ * signers, so the context id only advances once the spare has reshared and confirmed.
112116 */
113117const switchKmsContext = async (
114118 state : State ,
@@ -122,9 +126,19 @@ const switchKmsContext = async (
122126 const hostEnv : Record < string , string > = isSwap ? { HOST_SC_CONTEXT_ENV : "host-sc-swap.env" } : { } ;
123127 const gatewayEnv : Record < string , string > = isSwap ? { GATEWAY_SC_CONTEXT_ENV : "gateway-sc-swap.env" } : { } ;
124128
129+ if ( isSwap ) {
130+ // Stopped before the broadcast and left down: the new committee does not include the node.
131+ const droppedTxSenders = dropped . map ( ( party ) => kmsTxSenderName ( party ) ) ;
132+ console . log (
133+ `[kms-context-switch] stopping dropped node(s) ${ dropped . join ( "," ) } tx-sender before the switch — a node that cannot confirm must not block its own replacement…` ,
134+ ) ;
135+ await setRunning ( droppedTxSenders , "stop" ) ;
136+ await waitForContainersStopped ( droppedTxSenders ) ;
137+ }
138+
125139 console . log (
126140 isSwap
127- ? `[kms-context-switch] broadcasting defineNewKmsContextAndEpoch — node swap (drop ${ dropped . join ( "," ) } , add ${ added . join ( "," ) } , keep ${ continuing . join ( "," ) } )…`
141+ ? `[kms-context-switch] broadcasting defineNewKmsContextAndEpoch — node swap (drop ${ dropped . join ( "," ) } with tx-sender stopped , add ${ added . join ( "," ) } , keep ${ continuing . join ( "," ) } )…`
128142 : "[kms-context-switch] broadcasting defineNewKmsContextAndEpoch (NewKmsContext, same committee)…" ,
129143 ) ;
130144 await stepComposeTask ( "host-sc" , state , [ "host-sc-context-switch" ] , { noDeps : true , env : hostEnv } ) ;
0 commit comments