15
15
16
16
import static tech .pegasys .teku .cli .subcommand .RemoteSpecLoader .getSpec ;
17
17
18
+ import com .fasterxml .jackson .core .JsonProcessingException ;
18
19
import it .unimi .dsi .fastutil .objects .Object2IntMap ;
19
20
import it .unimi .dsi .fastutil .objects .Object2IntOpenHashMap ;
21
+ import java .io .File ;
22
+ import java .io .IOException ;
20
23
import java .net .ConnectException ;
21
24
import java .net .URI ;
22
25
import java .net .http .HttpClient ;
23
26
import java .nio .charset .Charset ;
27
+ import java .nio .file .Files ;
24
28
import java .nio .file .Path ;
25
29
import java .util .HashMap ;
26
30
import java .util .List ;
27
31
import java .util .Map ;
28
32
import java .util .Optional ;
29
33
import java .util .Scanner ;
30
34
import java .util .concurrent .Callable ;
35
+ import java .util .concurrent .atomic .AtomicInteger ;
31
36
import java .util .function .Supplier ;
32
37
import java .util .stream .Collectors ;
33
38
import org .apache .tuweni .bytes .Bytes32 ;
44
49
import tech .pegasys .teku .cli .options .ValidatorClientDataOptions ;
45
50
import tech .pegasys .teku .cli .options .ValidatorClientOptions ;
46
51
import tech .pegasys .teku .cli .options .ValidatorKeysOptions ;
52
+ import tech .pegasys .teku .cli .subcommand .debug .PrettyPrintCommand ;
47
53
import tech .pegasys .teku .config .TekuConfiguration ;
48
54
import tech .pegasys .teku .infrastructure .async .AsyncRunner ;
49
55
import tech .pegasys .teku .infrastructure .async .AsyncRunnerFactory ;
50
56
import tech .pegasys .teku .infrastructure .async .MetricTrackingExecutorFactory ;
51
57
import tech .pegasys .teku .infrastructure .exceptions .ExceptionUtil ;
52
58
import tech .pegasys .teku .infrastructure .exceptions .InvalidConfigurationException ;
59
+ import tech .pegasys .teku .infrastructure .json .JsonUtil ;
53
60
import tech .pegasys .teku .infrastructure .logging .SubCommandLogger ;
54
61
import tech .pegasys .teku .infrastructure .logging .ValidatorLogger ;
55
62
import tech .pegasys .teku .infrastructure .unsigned .UInt64 ;
82
89
footerHeading = "%n" ,
83
90
footer = "Teku is licensed under the Apache License 2.0" )
84
91
public class VoluntaryExitCommand implements Callable <Integer > {
92
+
85
93
public static final SubCommandLogger SUB_COMMAND_LOG = new SubCommandLogger ();
86
94
private static final int MAX_PUBLIC_KEY_BATCH_SIZE = 50 ;
87
95
private OkHttpValidatorRestApiClient apiClient ;
@@ -150,19 +158,38 @@ public class VoluntaryExitCommand implements Callable<Integer> {
150
158
arity = "0..1" )
151
159
private boolean includeKeyManagerKeys = false ;
152
160
161
+ @ CommandLine .Option (
162
+ names = {"--save-exits-path" },
163
+ description =
164
+ "Save the generated exit messages to the specified path, don't validate exit epoch, and skip publishing them." ,
165
+ paramLabel = "<FOLDER>" ,
166
+ arity = "1" )
167
+ public File voluntaryExitsFolder ;
168
+
153
169
private AsyncRunnerFactory asyncRunnerFactory ;
154
170
155
171
@ Override
156
172
public Integer call () {
157
173
SUB_COMMAND_LOG .display ("Loading configuration..." );
158
174
try {
159
175
initialise ();
160
- if (confirmationEnabled ) {
161
- if (!confirmExits ()) {
176
+
177
+ if (voluntaryExitsFolder != null ) {
178
+ SUB_COMMAND_LOG .display (
179
+ "Saving exits to folder "
180
+ + voluntaryExitsFolder
181
+ + ", and not submitting to beacon-node." );
182
+ if (!saveExitsToFolder ()) {
162
183
return 1 ;
163
184
}
185
+ } else {
186
+ if (confirmationEnabled ) {
187
+ if (!confirmExits ()) {
188
+ return 1 ;
189
+ }
190
+ }
191
+ getValidatorIndices (validatorsMap ).forEach (this ::submitExitForValidator );
164
192
}
165
- getValidatorIndices (validatorsMap ).forEach (this ::submitExitForValidator );
166
193
} catch (Exception ex ) {
167
194
if (ExceptionUtil .hasCause (ex , ConnectException .class )) {
168
195
SUB_COMMAND_LOG .error (getFailedToConnectMessage ());
@@ -180,6 +207,30 @@ public Integer call() {
180
207
return 0 ;
181
208
}
182
209
210
+ private boolean saveExitsToFolder () {
211
+ if (voluntaryExitsFolder .exists () && !voluntaryExitsFolder .isDirectory ()) {
212
+ SUB_COMMAND_LOG .error (
213
+ String .format (
214
+ "%s exists and is not a directory, cannot export to this path." ,
215
+ voluntaryExitsFolder ));
216
+ return false ;
217
+ } else if (!voluntaryExitsFolder .exists ()) {
218
+ voluntaryExitsFolder .mkdirs ();
219
+ }
220
+ final AtomicInteger failures = new AtomicInteger ();
221
+ getValidatorIndices (validatorsMap )
222
+ .forEach (
223
+ (publicKey , validatorIndex ) -> {
224
+ if (!storeExitForValidator (publicKey , validatorIndex )) {
225
+ failures .incrementAndGet ();
226
+ }
227
+ });
228
+ if (failures .get () > 0 ) {
229
+ return false ;
230
+ }
231
+ return true ;
232
+ }
233
+
183
234
private boolean confirmExits () {
184
235
SUB_COMMAND_LOG .display ("Exits are going to be generated for validators: " );
185
236
SUB_COMMAND_LOG .display (getValidatorAbbreviatedKeys ());
@@ -207,7 +258,7 @@ private String getValidatorAbbreviatedKeys() {
207
258
}
208
259
209
260
private Object2IntMap <BLSPublicKey > getValidatorIndices (
210
- Map <BLSPublicKey , Validator > validatorsMap ) {
261
+ final Map <BLSPublicKey , Validator > validatorsMap ) {
211
262
final Object2IntMap <BLSPublicKey > validatorIndices = new Object2IntOpenHashMap <>();
212
263
final List <String > publicKeys =
213
264
validatorsMap .keySet ().stream ().map (BLSPublicKey ::toString ).toList ();
@@ -237,16 +288,10 @@ private Object2IntMap<BLSPublicKey> getValidatorIndices(
237
288
238
289
private void submitExitForValidator (final BLSPublicKey publicKey , final int validatorIndex ) {
239
290
try {
240
- final ForkInfo forkInfo = new ForkInfo (fork , genesisRoot );
241
- final VoluntaryExit message = new VoluntaryExit (epoch , UInt64 .valueOf (validatorIndex ));
242
- final BLSSignature signature =
243
- Optional .ofNullable (validatorsMap .get (publicKey ))
244
- .orElseThrow ()
245
- .getSigner ()
246
- .signVoluntaryExit (message , forkInfo )
247
- .join ();
248
- Optional <PostDataFailureResponse > response =
249
- apiClient .sendVoluntaryExit (new SignedVoluntaryExit (message , signature ));
291
+ final tech .pegasys .teku .spec .datastructures .operations .SignedVoluntaryExit exit =
292
+ generateSignedExit (publicKey , validatorIndex );
293
+ final Optional <PostDataFailureResponse > response =
294
+ apiClient .sendVoluntaryExit (new SignedVoluntaryExit (exit ));
250
295
if (response .isPresent ()) {
251
296
SUB_COMMAND_LOG .error (response .get ().message );
252
297
} else {
@@ -261,6 +306,52 @@ private void submitExitForValidator(final BLSPublicKey publicKey, final int vali
261
306
}
262
307
}
263
308
309
+ private tech .pegasys .teku .spec .datastructures .operations .SignedVoluntaryExit generateSignedExit (
310
+ final BLSPublicKey publicKey , final int validatorIndex ) {
311
+ final ForkInfo forkInfo = new ForkInfo (fork , genesisRoot );
312
+ final VoluntaryExit message = new VoluntaryExit (epoch , UInt64 .valueOf (validatorIndex ));
313
+ final BLSSignature signature =
314
+ Optional .ofNullable (validatorsMap .get (publicKey ))
315
+ .orElseThrow ()
316
+ .getSigner ()
317
+ .signVoluntaryExit (message , forkInfo )
318
+ .join ();
319
+ return new tech .pegasys .teku .spec .datastructures .operations .SignedVoluntaryExit (
320
+ message , signature );
321
+ }
322
+
323
+ private boolean storeExitForValidator (
324
+ final BLSPublicKey blsPublicKey , final Integer validatorIndex ) {
325
+ final tech .pegasys .teku .spec .datastructures .operations .SignedVoluntaryExit exit =
326
+ generateSignedExit (blsPublicKey , validatorIndex );
327
+ try {
328
+ SUB_COMMAND_LOG .display ("Writing signed exit for " + blsPublicKey .toAbbreviatedString ());
329
+ Files .writeString (
330
+ voluntaryExitsFolder .toPath ().resolve (blsPublicKey .toAbbreviatedString () + "_exit.json" ),
331
+ prettyExitMessage (exit ));
332
+ return true ;
333
+ } catch (IOException e ) {
334
+ SUB_COMMAND_LOG .error ("Failed to store exit for " + blsPublicKey .toAbbreviatedString ());
335
+ return false ;
336
+ }
337
+ }
338
+
339
+ private String prettyExitMessage (
340
+ final tech .pegasys .teku .spec .datastructures .operations .SignedVoluntaryExit
341
+ signedVoluntaryExit )
342
+ throws JsonProcessingException {
343
+ final PrettyPrintCommand .OutputFormat json = PrettyPrintCommand .OutputFormat .JSON ;
344
+ return JsonUtil .serialize (
345
+ json .createFactory (),
346
+ gen -> {
347
+ gen .useDefaultPrettyPrinter ();
348
+ signedVoluntaryExit
349
+ .getSchema ()
350
+ .getJsonTypeDefinition ()
351
+ .serialize (signedVoluntaryExit , gen );
352
+ });
353
+ }
354
+
264
355
private Optional <UInt64 > getEpoch () {
265
356
return apiClient
266
357
.getBlockHeader ("head" )
@@ -367,7 +458,9 @@ private void validateOrDefaultEpoch() {
367
458
"Could not calculate epoch from latest block header, please specify --epoch" );
368
459
}
369
460
epoch = maybeEpoch .orElseThrow ();
370
- } else if (maybeEpoch .isPresent () && epoch .isGreaterThan (maybeEpoch .get ())) {
461
+ } else if (maybeEpoch .isPresent ()
462
+ && epoch .isGreaterThan (maybeEpoch .get ())
463
+ && voluntaryExitsFolder == null ) {
371
464
throw new InvalidConfigurationException (
372
465
String .format (
373
466
"The specified epoch %s is greater than current epoch %s, cannot continue." ,
0 commit comments