diff --git a/teku/src/integration-test/java/tech/pegasys/teku/cli/subcommand/VoluntaryExitCommandTest.java b/teku/src/integration-test/java/tech/pegasys/teku/cli/subcommand/VoluntaryExitCommandTest.java index f816f407d1b..9e089786510 100644 --- a/teku/src/integration-test/java/tech/pegasys/teku/cli/subcommand/VoluntaryExitCommandTest.java +++ b/teku/src/integration-test/java/tech/pegasys/teku/cli/subcommand/VoluntaryExitCommandTest.java @@ -37,6 +37,7 @@ import java.util.Collections; import java.util.List; import java.util.function.Supplier; +import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; @@ -52,6 +53,7 @@ import org.mockserver.model.Parameter; import tech.pegasys.teku.api.ConfigProvider; import tech.pegasys.teku.bls.BLSPublicKey; +import tech.pegasys.teku.bls.BLSSignature; import tech.pegasys.teku.bls.BLSSignatureVerifier; import tech.pegasys.teku.bls.BLSTestUtil; import tech.pegasys.teku.cli.BeaconNodeCommand; @@ -67,6 +69,7 @@ import tech.pegasys.teku.spec.constants.Domain; import tech.pegasys.teku.spec.datastructures.operations.SignedVoluntaryExit; import tech.pegasys.teku.spec.logic.common.helpers.MiscHelpers; +import tech.pegasys.teku.spec.util.DataStructureUtil; @ExtendWith(MockServerExtension.class) public class VoluntaryExitCommandTest { @@ -77,7 +80,7 @@ public class VoluntaryExitCommandTest { private final PrintWriter outputWriter = new PrintWriter(stringWriter, true); private final PrintWriter errorWriter = new PrintWriter(stringWriter, true); - private ClientAndServer mockBeaconServer; + private ClientAndServer server; private final BeaconNodeCommand beaconNodeCommand = new BeaconNodeCommand( @@ -113,8 +116,8 @@ public class VoluntaryExitCommandTest { @BeforeEach public void setup(final ClientAndServer server) throws IOException { - this.mockBeaconServer = server; - configureSuccessfulValidatorResponses(mockBeaconServer); + this.server = server; + configureSuccessfulValidatorResponses(); originalSystemIn = System.in; originalSystemOut = System.out; originalSytstemErr = System.err; @@ -125,7 +128,7 @@ public void setup(final ClientAndServer server) throws IOException { List.of( "voluntary-exit", "--beacon-node-api-endpoint", - getMockBeaconServerEndpoint(mockBeaconServer), + getMockBeaconServerEndpoint(), "--data-validator-path", Resources.getResource("tech/pegasys/teku/cli/subcommand/voluntary-exit/validator") .getPath(), @@ -141,7 +144,7 @@ public void setup(final ClientAndServer server) throws IOException { @AfterEach public void tearDown() { - mockBeaconServer.reset(); + server.reset(); System.setOut(originalSystemOut); System.setIn(originalSystemIn); System.setErr(originalSytstemErr); @@ -149,9 +152,9 @@ public void tearDown() { @Test public void shouldExitAllLoadedValidators() throws JsonProcessingException { - configureSuccessfulSpecResponse(mockBeaconServer); - configureSuccessfulGenesisResponse(mockBeaconServer); - configureSuccessfulVoluntaryExitResponse(mockBeaconServer); + configureSuccessfulSpecResponse(); + configureSuccessfulGenesisResponse(); + configureSuccessfulVoluntaryExitResponse(); final List args = getCommandArguments(true, false, List.of()); int parseResult = beaconNodeCommand.parse(args.toArray(new String[0])); @@ -164,9 +167,9 @@ public void shouldExitAllLoadedValidators() throws JsonProcessingException { @Test public void shouldExitLoadedValidatorsUsingConfirmationMessage() throws JsonProcessingException { setUserInput("yes"); - configureSuccessfulSpecResponse(mockBeaconServer); - configureSuccessfulGenesisResponse(mockBeaconServer); - configureSuccessfulVoluntaryExitResponse(mockBeaconServer); + configureSuccessfulSpecResponse(); + configureSuccessfulGenesisResponse(); + configureSuccessfulVoluntaryExitResponse(); final List args = getCommandArguments(true, true, List.of()); int parseResult = beaconNodeCommand.parse(args.toArray(new String[0])); @@ -178,9 +181,9 @@ public void shouldExitLoadedValidatorsUsingConfirmationMessage() throws JsonProc @Test public void shouldExitValidatorWithPubKeyFromKeyManagerOnly() throws JsonProcessingException { - configureSuccessfulSpecResponse(mockBeaconServer); - configureSuccessfulGenesisResponse(mockBeaconServer); - configureSuccessfulVoluntaryExitResponse(mockBeaconServer); + configureSuccessfulSpecResponse(); + configureSuccessfulGenesisResponse(); + configureSuccessfulVoluntaryExitResponse(); final List args = getCommandArguments( @@ -197,8 +200,8 @@ public void shouldExitValidatorWithPubKeyFromKeyManagerOnly() throws JsonProcess @Test public void shouldAcceptNetworkOnCommandLine() { - configureSuccessfulVoluntaryExitResponse(mockBeaconServer); - configureSuccessfulGenesisResponse(mockBeaconServer); + configureSuccessfulVoluntaryExitResponse(); + configureSuccessfulGenesisResponse(); // No beacon-api offered by spec, so would need to be loaded from local network option final List args = @@ -215,8 +218,8 @@ public void shouldAcceptNetworkOnCommandLine() { @Test public void shouldReturnRejectedReasonWhenExitIsRejectedByBeaconNode() throws IOException { - configureRejectedVoluntaryExitResponse(mockBeaconServer); - configureSuccessfulGenesisResponse(mockBeaconServer); + configureRejectedVoluntaryExitResponse(); + configureSuccessfulGenesisResponse(); final List args = getCommandArguments( @@ -245,9 +248,9 @@ public void shouldFailToRunExitWithoutASpec() { @Test public void shouldExitValidatorWithPubKeyFromPathOnly() throws JsonProcessingException { - configureSuccessfulSpecResponse(mockBeaconServer); - configureSuccessfulGenesisResponse(mockBeaconServer); - configureSuccessfulVoluntaryExitResponse(mockBeaconServer); + configureSuccessfulSpecResponse(); + configureSuccessfulGenesisResponse(); + configureSuccessfulVoluntaryExitResponse(); final List args = getCommandArguments( @@ -264,9 +267,9 @@ public void shouldExitValidatorWithPubKeyFromPathOnly() throws JsonProcessingExc @Test public void shouldSkipKeyManagerKeys() throws JsonProcessingException { - configureSuccessfulSpecResponse(mockBeaconServer); - configureSuccessfulGenesisResponse(mockBeaconServer); - configureSuccessfulVoluntaryExitResponse(mockBeaconServer); + configureSuccessfulSpecResponse(); + configureSuccessfulGenesisResponse(); + configureSuccessfulVoluntaryExitResponse(); final List args = getCommandArguments( @@ -283,8 +286,8 @@ public void shouldSkipKeyManagerKeys() throws JsonProcessingException { @Test void shouldNotWarn_NotWithdrawableIfCapellaEnabled() throws JsonProcessingException { - configureSuccessfulSpecResponse(mockBeaconServer, TestSpecFactory.createMinimalCapella()); - configureSuccessfulGenesisResponse(mockBeaconServer); + configureSuccessfulSpecResponse(TestSpecFactory.createMinimalCapella()); + configureSuccessfulGenesisResponse(); final List args = getCommandArguments(false, true, List.of()); setUserInput("no"); @@ -299,8 +302,8 @@ void shouldNotWarn_NotWithdrawableIfCapellaEnabled() throws JsonProcessingExcept @Test void shouldGenerateExitWithoutSendingToNode(@TempDir final Path tempDir) throws IOException { - configureSuccessfulSpecResponse(mockBeaconServer, TestSpecFactory.createMinimalCapella()); - configureSuccessfulGenesisResponse(mockBeaconServer); + configureSuccessfulSpecResponse(TestSpecFactory.createMinimalCapella()); + configureSuccessfulGenesisResponse(); final Path outputFolder = tempDir.resolve("out"); final List args = new ArrayList<>(); args.addAll(commandArgs); @@ -327,8 +330,8 @@ void shouldGenerateExitWithoutSendingToNode(@TempDir final Path tempDir) throws @Test void shouldFailIfSaveFolderCannotBeCreated(@TempDir final Path tempDir) throws IOException { - configureSuccessfulSpecResponse(mockBeaconServer, TestSpecFactory.createMinimalCapella()); - configureSuccessfulGenesisResponse(mockBeaconServer); + configureSuccessfulSpecResponse(TestSpecFactory.createMinimalCapella()); + configureSuccessfulGenesisResponse(); final Path invalidOutputDestination = tempDir.resolve("testFile"); Files.writeString(invalidOutputDestination, "test"); @@ -349,8 +352,8 @@ void shouldFailIfSaveFolderCannotBeCreated(@TempDir final Path tempDir) throws I @Test @DisabledOnOs(OS.WINDOWS) // can't set permissions on windows void shouldFailIfSaveFolderHasInsufficientAccess(@TempDir final Path tempDir) throws IOException { - configureSuccessfulSpecResponse(mockBeaconServer, TestSpecFactory.createMinimalCapella()); - configureSuccessfulGenesisResponse(mockBeaconServer); + configureSuccessfulSpecResponse(TestSpecFactory.createMinimalCapella()); + configureSuccessfulGenesisResponse(); final Path invalidOutputDestination = tempDir.resolve("testFile"); tempDir.toFile().mkdir(); @@ -370,7 +373,7 @@ void shouldFailIfSaveFolderHasInsufficientAccess(@TempDir final Path tempDir) th @Test void shouldFailIfGenesisDataNotAvailableAndNoEpochSpecified() throws JsonProcessingException { - configureSuccessfulSpecResponse(mockBeaconServer, TestSpecFactory.createMinimalCapella()); + configureSuccessfulSpecResponse(TestSpecFactory.createMinimalCapella()); final List args = getCommandArguments(false, true, List.of("--validator-public-keys", validatorPubKey1)); final int parseResult = beaconNodeCommand.parse(args.toArray(new String[0])); @@ -387,10 +390,60 @@ void shouldFailToGenerateExitWithoutBeaconNodeAvailable() { assertThat(stdErr.toString(UTF_8)).contains("Failed to connect to beacon node."); } + @Test + void shouldFailToGenerateIfExternalSignerNotAvailable() throws IOException { + configureSuccessfulSpecResponse(TestSpecFactory.createMinimalCapella()); + setUserInput("yes"); + configureSuccessfulGenesisResponse(); + final String validator2 = validatorResourceFile("aa51616_response.json"); + setupValidatorStatusResponse(validatorPubKey2, validator2); + + final List args = + List.of( + "voluntary-exit", + commandArgs.get(1), + commandArgs.get(2), + "--validators-external-signer-url=" + getMockSignerEndpoint(), + "--validators-external-signer-public-keys=" + validatorPubKey2); + final int parseResult = beaconNodeCommand.parse(args.toArray(new String[0])); + assertThat(parseResult).isEqualTo(1); + // this is not a perfect error, but it does demonstrate it's not saying spec wasn't available, + // which is a good start + assertThat(stdErr.toString(UTF_8)).contains("ExternalSignerException"); + } + + @Test + void canProcessExitForExternalSource(@TempDir final Path tempDir) throws IOException { + final DataStructureUtil dataStructureUtil = new DataStructureUtil(spec); + final List keys = List.of(validatorPubKey2); + setUserInput("yes"); + configureSuccessfulSpecResponse(TestSpecFactory.createMinimalCapella()); + configureExternalSignerUpcheck(); + configureSuccessfulGenesisResponse(); + configureExternalSignerPublicKeys(keys); + + final String validator2 = validatorResourceFile("aa51616_response.json"); + configureExternalSignerResponse(validatorPubKey2, dataStructureUtil.randomSignature()); + setupValidatorStatusResponse(validatorPubKey2, validator2); + + final List args = + List.of( + "voluntary-exit", + commandArgs.get(1), + commandArgs.get(2), + "--save-exits-path", + tempDir.toAbsolutePath().toString(), + "--validators-external-signer-url=" + getMockSignerEndpoint(), + "--validators-external-signer-public-keys=" + keys.get(0)); + final int parseResult = beaconNodeCommand.parse(args.toArray(new String[0])); + assertThat(parseResult).isEqualTo(0); + assertThat(stdOut.toString(UTF_8)).contains("Writing signed exit for aa51616"); + } + @Test void shouldExitFailureWithNoValidatorKeysFound() throws JsonProcessingException { - configureSuccessfulSpecResponse(mockBeaconServer); - configureSuccessfulGenesisResponse(mockBeaconServer); + configureSuccessfulSpecResponse(); + configureSuccessfulGenesisResponse(); final List args = commandArgs.subList(0, 5); int parseResult = beaconNodeCommand.parse(args.toArray(new String[0])); @@ -401,8 +454,8 @@ void shouldExitFailureWithNoValidatorKeysFound() throws JsonProcessingException @Test void shouldExitFailureFutureEpoch() throws IOException { - configureSuccessfulSpecResponse(mockBeaconServer); - configureSuccessfulGenesisResponse(mockBeaconServer); + configureSuccessfulSpecResponse(); + configureSuccessfulGenesisResponse(); final List args = getCommandArguments(false, true, List.of("--epoch=1024")); int parseResult = beaconNodeCommand.parse(args.toArray(new String[0])); @@ -415,8 +468,8 @@ void shouldExitFailureFutureEpoch() throws IOException { @Test void shouldCreateExitForFutureEpochIfOutputFolderDefined(@TempDir final Path tempDir) throws IOException { - configureSuccessfulSpecResponse(mockBeaconServer, TestSpecFactory.createMinimalCapella()); - configureSuccessfulGenesisResponse(mockBeaconServer); + configureSuccessfulSpecResponse(TestSpecFactory.createMinimalCapella()); + configureSuccessfulGenesisResponse(); final List args = new ArrayList<>(); args.addAll(commandArgs); args.addAll( @@ -436,11 +489,11 @@ void shouldCreateExitForFutureEpochIfOutputFolderDefined(@TempDir final Path tem @Test void shouldUseCurrentForkDomainForSignatureBeforeDeneb() throws JsonProcessingException { setUserInput("yes"); - configureSuccessfulSpecResponse(mockBeaconServer); - configureSuccessfulGenesisResponse(mockBeaconServer); + configureSuccessfulSpecResponse(); + configureSuccessfulGenesisResponse(); final Supplier> exitsCapture = - configureSuccessfulVoluntaryExitResponseWithCapture(mockBeaconServer); + configureSuccessfulVoluntaryExitResponseWithCapture(); final List args = getCommandArguments(false, true, List.of("--validator-public-keys", validatorPubKey1)); @@ -462,11 +515,11 @@ void shouldUseCurrentForkDomainForSignatureBeforeDeneb() throws JsonProcessingEx @Test void shouldUseCapellaForkDomainForSignatureAfterCapella() throws JsonProcessingException { setUserInput("yes"); - configureSuccessfulDenebSpecResponse(mockBeaconServer); - configureSuccessfulGenesisResponse(mockBeaconServer); + configureSuccessfulDenebSpecResponse(); + configureSuccessfulGenesisResponse(); final Supplier> exitsCapture = - configureSuccessfulVoluntaryExitResponseWithCapture(mockBeaconServer); + configureSuccessfulVoluntaryExitResponseWithCapture(); final List args = getCommandArguments(false, true, List.of("--validator-public-keys", validatorPubKey1)); @@ -504,28 +557,29 @@ private List getCommandArguments( return args; } - private String getMockBeaconServerEndpoint(final ClientAndServer mockBeaconServer) { - return String.format("http://127.0.0.1:%s/", mockBeaconServer.getLocalPort()); + private String getMockBeaconServerEndpoint() { + return String.format("http://127.0.0.1:%s/beacon", server.getLocalPort()); } - private void configureSuccessfulSpecResponse(final ClientAndServer mockBeaconServer) - throws JsonProcessingException { - configureSuccessfulSpecResponse(mockBeaconServer, spec); + private String getMockSignerEndpoint() { + return String.format("http://127.0.0.1:%s/signer", server.getLocalPort()); } - private void configureSuccessfulDenebSpecResponse(final ClientAndServer mockBeaconServer) - throws JsonProcessingException { - configureSuccessfulSpecResponse(mockBeaconServer, specDeneb); + private void configureSuccessfulSpecResponse() throws JsonProcessingException { + configureSuccessfulSpecResponse(spec); } - private void configureSuccessfulSpecResponse( - final ClientAndServer mockBeaconServer, final Spec spec) throws JsonProcessingException { - mockBeaconServer + private void configureSuccessfulDenebSpecResponse() throws JsonProcessingException { + configureSuccessfulSpecResponse(specDeneb); + } + + private void configureSuccessfulSpecResponse(final Spec spec) throws JsonProcessingException { + server .when(request().withPath("/eth/v1/config/spec")) .respond(response().withStatusCode(200).withBody(getTestSpecJsonString(spec))); } - private void configureSuccessfulGenesisResponse(final ClientAndServer mockBeaconServer) { + private void configureSuccessfulGenesisResponse() { final TimeProvider timeProvider = new SystemTimeProvider(); final SpecConfig config = spec.getGenesisSpec().getConfig(); final UInt64 genesisTime = @@ -540,72 +594,25 @@ private void configureSuccessfulGenesisResponse(final ClientAndServer mockBeacon + "\"genesis_fork_version\": \"0x00004105\"}}", genesisTime); - mockBeaconServer + server .when(request().withPath("/eth/v1/beacon/genesis")) .respond(response().withStatusCode(200).withBody(testHead)); } - private void configureSuccessfulValidatorResponses(final ClientAndServer mockBeaconServer) - throws IOException { - final String keyManagerValidator1 = - Resources.toString( - Resources.getResource( - "tech/pegasys/teku/cli/subcommand/voluntary-exit/validator-responses/8b0f19f_response.json"), - UTF_8); + private void configureSuccessfulValidatorResponses() throws IOException { + final String keyManagerValidator1 = validatorResourceFile("8b0f19f_response.json"); + final String keyManagerValidator2 = validatorResourceFile("82c2a92_response.json"); + final String validator1 = validatorResourceFile("a756543_response.json"); + final String validator2 = validatorResourceFile("aa51616_response.json"); - final String keyManagerValidator2 = - Resources.toString( - Resources.getResource( - "tech/pegasys/teku/cli/subcommand/voluntary-exit/validator-responses/82c2a92_response.json"), - UTF_8); + final String validators = validatorResourceFile("validators_response.json"); - final String validator1 = - Resources.toString( - Resources.getResource( - "tech/pegasys/teku/cli/subcommand/voluntary-exit/validator-responses/a756543_response.json"), - UTF_8); + setupValidatorStatusResponse(keyManagerPubKey1, keyManagerValidator1); + setupValidatorStatusResponse(keyManagerPubKey2, keyManagerValidator2); + setupValidatorStatusResponse(validatorPubKey1, validator1); + setupValidatorStatusResponse(validatorPubKey2, validator2); - final String validator2 = - Resources.toString( - Resources.getResource( - "tech/pegasys/teku/cli/subcommand/voluntary-exit/validator-responses/aa51616_response.json"), - UTF_8); - - final String validators = - Resources.toString( - Resources.getResource( - "tech/pegasys/teku/cli/subcommand/voluntary-exit/validator-responses/validators_response.json"), - UTF_8); - - mockBeaconServer - .when( - request() - .withPath("/eth/v1/beacon/states/head/validators") - .withQueryStringParameters(Parameter.param("id", keyManagerPubKey1))) - .respond(response().withStatusCode(200).withBody(keyManagerValidator1)); - - mockBeaconServer - .when( - request() - .withPath("/eth/v1/beacon/states/head/validators") - .withQueryStringParameters(Parameter.param("id", keyManagerPubKey2))) - .respond(response().withStatusCode(200).withBody(keyManagerValidator2)); - - mockBeaconServer - .when( - request() - .withPath("/eth/v1/beacon/states/head/validators") - .withQueryStringParameters(Parameter.param("id", validatorPubKey1))) - .respond(response().withStatusCode(200).withBody(validator1)); - - mockBeaconServer - .when( - request() - .withPath("/eth/v1/beacon/states/head/validators") - .withQueryStringParameters(Parameter.param("id", validatorPubKey2))) - .respond(response().withStatusCode(200).withBody(validator2)); - - mockBeaconServer + server .when( request() .withPath("/eth/v1/beacon/states/head/validators") @@ -621,16 +628,33 @@ private void configureSuccessfulValidatorResponses(final ClientAndServer mockBea .respond(response().withStatusCode(200).withBody(validators)); } - private void configureSuccessfulVoluntaryExitResponse(final ClientAndServer mockBeaconServer) { - mockBeaconServer + private void setupValidatorStatusResponse(final String publicKey, final String responseString) { + server + .when( + request() + .withPath("/eth/v1/beacon/states/head/validators") + .withQueryStringParameters(Parameter.param("id", publicKey))) + .respond(response().withStatusCode(200).withBody(responseString)); + } + + private String validatorResourceFile(final String validatorStatusFile) throws IOException { + return Resources.toString( + Resources.getResource( + "tech/pegasys/teku/cli/subcommand/voluntary-exit/validator-responses/" + + validatorStatusFile), + UTF_8); + } + + private void configureSuccessfulVoluntaryExitResponse() { + server .when(request().withPath("/eth/v1/beacon/pool/voluntary_exits")) .respond(response().withStatusCode(200)); } - private Supplier> configureSuccessfulVoluntaryExitResponseWithCapture( - final ClientAndServer mockBeaconServer) { + private Supplier> + configureSuccessfulVoluntaryExitResponseWithCapture() { final List responses = new ArrayList<>(); - mockBeaconServer + server .when(request().withPath("/eth/v1/beacon/pool/voluntary_exits")) .respond( httpRequest -> { @@ -651,15 +675,47 @@ private Supplier> configureSuccessfulVoluntaryExitResp .toList(); } - private void configureRejectedVoluntaryExitResponse(final ClientAndServer mockBeaconServer) - throws IOException { + private void configureExternalSignerUpcheck() { + server + .when(request().withMethod("GET").withPath("/upcheck")) + .respond(response().withStatusCode(200)); + } + + private void configureExternalSignerResponse( + final String validatorPubKey2, final BLSSignature blsSignature) { + server + .when(request().withMethod("POST").withPath("/api/v1/eth2/sign/" + validatorPubKey2)) + .respond(response().withStatusCode(200).withBody(blsSignature.toString())); + } + + private void configureExternalSignerPublicKeys(final List keys) { + final String body = + "{[" + + keys.stream() + .map( + k -> + "{" + + "\"validating_pubkey\": \"" + + k + + "\"," + + "\"derivation_path\": \"\"," + + "\"readonly\": false" + + "}") + .collect(Collectors.joining(",")) + + "]}"; + server + .when(request().withMethod("GET").withPath("/eth/v1/keystores")) + .respond(response().withStatusCode(200).withBody(body)); + } + + private void configureRejectedVoluntaryExitResponse() throws IOException { final String rejectedExitResponseBody = Resources.toString( Resources.getResource( "tech/pegasys/teku/cli/subcommand/voluntary-exit/rejected-exit.json"), UTF_8); - mockBeaconServer + server .when(request().withMethod("POST").withPath("/eth/v1/beacon/pool/voluntary_exits")) .respond(response().withStatusCode(400).withBody(rejectedExitResponseBody)); }