diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/SignerConfiguration.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/SignerConfiguration.java index 7317b5fb4..a5a68fa60 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/SignerConfiguration.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/SignerConfiguration.java @@ -76,6 +76,7 @@ public class SignerConfiguration { private Optional downstreamTlsOptions; private final Duration startupTimeout; private final ChainIdProvider chainIdProvider; + private final Optional walletBulkloadParameters; public SignerConfiguration( final String hostname, @@ -120,7 +121,8 @@ public SignerConfiguration( final int downstreamHttpPort, final Optional downstreamTlsOptions, final ChainIdProvider chainIdProvider, - final Optional trustedSetup) { + final Optional trustedSetup, + final Optional walletBulkloadParameters) { this.hostname = hostname; this.logLevel = logLevel; this.httpRpcPort = httpRpcPort; @@ -164,6 +166,7 @@ public SignerConfiguration( this.downstreamTlsOptions = downstreamTlsOptions; this.chainIdProvider = chainIdProvider; this.trustedSetup = trustedSetup; + this.walletBulkloadParameters = walletBulkloadParameters; } public String hostname() { @@ -345,4 +348,8 @@ public ChainIdProvider getChainIdProvider() { public Optional getTrustedSetup() { return trustedSetup; } + + public Optional getWalletBulkloadParameters() { + return walletBulkloadParameters; + } } diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/SignerConfigurationBuilder.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/SignerConfigurationBuilder.java index dc78a259b..09ac8b7a1 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/SignerConfigurationBuilder.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/SignerConfigurationBuilder.java @@ -80,6 +80,8 @@ public class SignerConfigurationBuilder { private ChainIdProvider chainIdProvider = new ConfigurationChainId(DEFAULT_CHAIN_ID); private String trustedSetup; + private KeystoresParameters walletBulkloadParameters; + public SignerConfigurationBuilder withLogLevel(final Level logLevel) { this.logLevel = logLevel; return this; @@ -309,6 +311,12 @@ public SignerConfigurationBuilder withTrustedSetup(final String trustedSetup) { return this; } + public SignerConfigurationBuilder withWalletBulkloadParameters( + final KeystoresParameters walletBulkloadParameters) { + this.walletBulkloadParameters = walletBulkloadParameters; + return this; + } + public SignerConfiguration build() { if (mode == null) { throw new IllegalArgumentException("Mode cannot be null"); @@ -356,6 +364,7 @@ public SignerConfiguration build() { downstreamHttpPort, Optional.ofNullable(downstreamTlsOptions), chainIdProvider, - Optional.ofNullable(trustedSetup)); + Optional.ofNullable(trustedSetup), + Optional.ofNullable(walletBulkloadParameters)); } } diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/runner/CmdLineParamsDefaultImpl.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/runner/CmdLineParamsDefaultImpl.java index 42762aa00..fc9054c9c 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/runner/CmdLineParamsDefaultImpl.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/runner/CmdLineParamsDefaultImpl.java @@ -22,6 +22,7 @@ import static tech.pegasys.web3signer.commandline.PicoCliAwsSecretsManagerParameters.AWS_SECRETS_TAG_NAMES_FILTER_OPTION; import static tech.pegasys.web3signer.commandline.PicoCliAwsSecretsManagerParameters.AWS_SECRETS_TAG_VALUES_FILTER_OPTION; +import tech.pegasys.web3signer.commandline.config.PicoV3WalletBulkloadParameters; import tech.pegasys.web3signer.core.config.ClientAuthConstraints; import tech.pegasys.web3signer.core.config.TlsOptions; import tech.pegasys.web3signer.core.config.client.ClientTlsOptions; @@ -38,6 +39,7 @@ import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.function.Consumer; import com.google.common.collect.Lists; @@ -128,11 +130,29 @@ public List createCmdLineParams() { if (signerConfig.getAzureKeyVaultParameters().isPresent()) { createAzureArgs(params); } + + signerConfig.getWalletBulkloadParameters().ifPresent(setWalletBulkloadParameters(params)); } return params; } + private static Consumer setWalletBulkloadParameters( + final List params) { + return keystoresParameters -> { + params.add(PicoV3WalletBulkloadParameters.WALLETS_PATH); + params.add(keystoresParameters.getKeystoresPath().toAbsolutePath().toString()); + if (keystoresParameters.getKeystoresPasswordsPath() != null) { + params.add(PicoV3WalletBulkloadParameters.WALLETS_PASSWORDS_PATH); + params.add(keystoresParameters.getKeystoresPasswordsPath().toAbsolutePath().toString()); + } + if (keystoresParameters.getKeystoresPasswordFile() != null) { + params.add(PicoV3WalletBulkloadParameters.WALLETS_PASSWORD_FILE); + params.add(keystoresParameters.getKeystoresPasswordFile().toAbsolutePath().toString()); + } + }; + } + @Override public Optional slashingProtectionDbUrl() { return slashingProtectionDbUrl; diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/HealthCheckResultUtil.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/HealthCheckResultUtil.java new file mode 100644 index 000000000..da03dee82 --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/HealthCheckResultUtil.java @@ -0,0 +1,82 @@ +/* + * Copyright 2023 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.web3signer.dsl.utils; + +import java.util.List; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Splitter; + +public class HealthCheckResultUtil { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + /* + Healthcheck Json looks like: + { + "status": "UP", + "checks": [ + { + "id": "default-check", + "status": "UP" + }, + { + "id": "keys-check", + "status": "UP", + "checks": [ + { + "id": "wallet-bulk-loading", + "status": "UP", + "data": { + "keys-loaded": 4, + "error-count": 0 + } + }, + { + "id": "config-files-loading", + "status": "UP", + "data": { + "keys-loaded": 4, + "error-count": 0 + } + } + ] + } + ], + "outcome": "UP" + } + */ + public static int getHealthCheckKeysCheckData( + String healthCheckJsonBody, final String healthCheckKeyName, final String dataKey) + throws JsonProcessingException { + final List keyCheckIds = Splitter.on('/').splitToList(healthCheckKeyName); + int dataKeyValue = -1; + final JsonNode jsonNode = OBJECT_MAPPER.readTree(healthCheckJsonBody); + for (JsonNode checksNode : jsonNode.get("checks")) { + // id = keys-check + if (checksNode.get("id").asText().equals(keyCheckIds.get(0))) { + for (JsonNode keyChecksNode : checksNode.get("checks")) { + // id = See HealthCheckNames.java + if (keyChecksNode.get("id").asText().equals(keyCheckIds.get(1))) { + dataKeyValue = keyChecksNode.get("data").get(dataKey).asInt(); + break; + } + } + } + } + + return dataKeyValue; + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/bulkloading/SecpWalletBulkloadAcceptanceTest.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/bulkloading/SecpWalletBulkloadAcceptanceTest.java new file mode 100644 index 000000000..199a69353 --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/bulkloading/SecpWalletBulkloadAcceptanceTest.java @@ -0,0 +1,123 @@ +/* + * Copyright 2023 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.web3signer.tests.bulkloading; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +import static org.web3j.crypto.WalletUtils.generateWalletFile; +import static tech.pegasys.web3signer.core.config.HealthCheckNames.KEYS_CHECK_V3_WALLET_BULK_LOADING; +import static tech.pegasys.web3signer.dsl.utils.HealthCheckResultUtil.getHealthCheckKeysCheckData; + +import tech.pegasys.web3signer.dsl.signer.SignerConfigurationBuilder; +import tech.pegasys.web3signer.dsl.utils.DefaultKeystoresParameters; +import tech.pegasys.web3signer.signing.KeyType; +import tech.pegasys.web3signer.signing.config.KeystoresParameters; +import tech.pegasys.web3signer.signing.secp256k1.EthPublicKeyUtils; +import tech.pegasys.web3signer.signing.util.IdentifierUtils; +import tech.pegasys.web3signer.tests.AcceptanceTestBase; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.interfaces.ECPublicKey; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import io.restassured.http.ContentType; +import io.restassured.response.Response; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.web3j.crypto.CipherException; +import org.web3j.crypto.ECKeyPair; +import org.web3j.crypto.Keys; + +public class SecpWalletBulkloadAcceptanceTest extends AcceptanceTestBase { + @TempDir private static Path walletsDir; + @TempDir private static Path walletsPasswordDir; + + private static List publicKeys; + + @BeforeAll + static void initV3Wallets() throws IOException, GeneralSecurityException, CipherException { + publicKeys = new ArrayList<>(); + for (int i = 0; i < 4; i++) { + final ECKeyPair ecKeyPair = Keys.createEcKeyPair(); + final ECPublicKey ecPublicKey = EthPublicKeyUtils.createPublicKey(ecKeyPair.getPublicKey()); + final String publicKeyHex = + IdentifierUtils.normaliseIdentifier(EthPublicKeyUtils.toHexString(ecPublicKey)); + publicKeys.add(publicKeyHex); + + // generate v3 wallet + final boolean useFullScrypt = false; + final String fileName = + generateWalletFile("test123", ecKeyPair, walletsDir.toFile(), useFullScrypt); + + // write corresponding password + final Path passwordFile = + walletsPasswordDir.resolve(fileName.substring(0, fileName.lastIndexOf(".json")) + ".txt"); + Files.writeString(passwordFile, "test123"); + } + } + + @ParameterizedTest + @MethodSource("buildWalletParameters") + void walletFilesAreBulkloaded(final KeystoresParameters walletBulkloadParameters) + throws Exception { + final SignerConfigurationBuilder configBuilder = + new SignerConfigurationBuilder() + .withMode("eth1") + .withWalletBulkloadParameters(walletBulkloadParameters); + + startSigner(configBuilder.build()); + + final Response response = signer.callApiPublicKeys(KeyType.SECP256K1); + response + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("", containsInAnyOrder(publicKeys.toArray(String[]::new))); + + final Response healthcheckResponse = signer.healthcheck(); + healthcheckResponse + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("status", equalTo("UP")); + + final String jsonBody = healthcheckResponse.body().asString(); + final int keysLoaded = + getHealthCheckKeysCheckData(jsonBody, KEYS_CHECK_V3_WALLET_BULK_LOADING, "keys-loaded"); + assertThat(keysLoaded).isEqualTo(publicKeys.size()); + } + + private static Stream buildWalletParameters() { + // build wallet bulkloading parameters, one with password dir, other with password file + final KeystoresParameters withPasswordDir = + new DefaultKeystoresParameters(walletsDir, walletsPasswordDir, null); + + try (final Stream passwordFiles = Files.list(walletsPasswordDir)) { + final Path passwordFile = passwordFiles.findAny().orElseThrow(); + final KeystoresParameters withPasswordFile = + new DefaultKeystoresParameters(walletsDir, null, passwordFile); + return Stream.of(withPasswordDir, withPasswordFile); + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + } +}