Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: support override node admin keys #16781

Open
wants to merge 9 commits into
base: release/0.57
Choose a base branch
from
3 changes: 3 additions & 0 deletions hedera-node/configuration/dev/node-admin-keys.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"0": "0aa8e21064c61eab86e2a9c164565b4e7a9a4146106e0a6cd03a8c395a110e92"
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,15 @@

package com.hedera.node.app.service.addressbook.impl.schemas;

import static com.swirlds.common.utility.CommonUtils.unhex;
import static com.swirlds.platform.roster.RosterUtils.formatNodeName;
import static java.util.Collections.emptyMap;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toMap;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.hedera.hapi.node.base.AccountID;
import com.hedera.hapi.node.base.FileID;
import com.hedera.hapi.node.base.Key;
Expand All @@ -41,17 +47,18 @@
import com.swirlds.state.spi.ReadableKVState;
import com.swirlds.state.spi.WritableKVState;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
* General schema for the addressbook service.
* {@code V052AddressBookSchema} is used for migrating the address book on Version 0.52.0
* Genesis schema of the address book service.
*/
public class V053AddressBookSchema extends Schema {
private static final Logger log = LogManager.getLogger(V053AddressBookSchema.class);
Expand All @@ -78,6 +85,13 @@
@Override
public void migrate(@NonNull final MigrationContext ctx) {
requireNonNull(ctx);
// Since this schema's version is several releases behind the current version,
// its migrate() will only be called at genesis in any case, but this makes it
// explicit that the override admin keys apply only at genesis
final Map<Long, Key> nodeAdminKeys = ctx.isGenesis()
? parseEd25519NodeAdminKeysFrom(
ctx.configuration().getConfigData(BootstrapConfig.class).nodeAdminKeysPath())
: emptyMap();
final var networkInfo = ctx.genesisNetworkInfo();
if (networkInfo == null) {
throw new IllegalStateException("Genesis network info is not found");
Expand All @@ -90,12 +104,16 @@
final var adminKey = getAccountAdminKey(ctx);
final var nodeDetailMap = getNodeAddressMap(ctx);

Key finalAdminKey = adminKey == null || adminKey.equals(Key.DEFAULT)
final var defaultAdminKey = adminKey == null || adminKey.equals(Key.DEFAULT)
? Key.newBuilder().ed25519(bootstrapConfig.genesisPublicKey()).build()
: adminKey;
NodeAddress nodeDetail;
final var addressBook = networkInfo.addressBook();
for (final var nodeInfo : addressBook) {
final var nodeAdminKey = nodeAdminKeys.getOrDefault(nodeInfo.nodeId(), defaultAdminKey);
if (nodeAdminKey != defaultAdminKey) {
log.info("Override admin key for node{} is :: {}", nodeInfo.nodeId(), nodeAdminKey);
}
final var nodeBuilder = Node.newBuilder()
.nodeId(nodeInfo.nodeId())
.accountId(nodeInfo.accountId())
Expand All @@ -104,7 +122,7 @@
.gossipEndpoint(nodeInfo.gossipEndpoints())
.gossipCaCertificate(nodeInfo.sigCertBytes())
.weight(nodeInfo.stake())
.adminKey(finalAdminKey);
.adminKey(nodeAdminKey);
if (nodeDetailMap != null) {
nodeDetail = nodeDetailMap.get(nodeInfo.nodeId());
if (nodeDetail != null) {
Expand Down Expand Up @@ -161,8 +179,7 @@
final var nodeDetails = NodeAddressBook.PROTOBUF
.parse(nodeDetailFile.contents())
.nodeAddress();
nodeDetailMap =
nodeDetails.stream().collect(Collectors.toMap(NodeAddress::nodeId, Function.identity()));
nodeDetailMap = nodeDetails.stream().collect(toMap(NodeAddress::nodeId, Function.identity()));
} catch (ParseException e) {
log.warn("Can not parse file 102 ", e);
}
Expand Down Expand Up @@ -194,4 +211,39 @@
}
return builder.build();
}

/**
* Parses the given JSON file as a map from node ids to hexed Ed25519 public keys.
* @param loc the location of the JSON file
* @return the map from node ids to Ed25519 keys
*/
private static Map<Long, Key> parseEd25519NodeAdminKeysFrom(@NonNull final String loc) {
final var path = Paths.get(loc);
try {
final var json = Files.readString(path);
return parseEd25519NodeAdminKeys(json);
} catch (IOException e) {
log.warn("Unable to read override keys from {}", path.toAbsolutePath(), e);
return emptyMap();
}
}

/**
* Parses the given JSON string as a map from node ids to hexed Ed25519 public keys.
* @param json the JSON string
* @return the map from node ids to Ed25519 keys
*/
public static Map<Long, Key> parseEd25519NodeAdminKeys(@NonNull final String json) {
requireNonNull(json);
final var mapper = new ObjectMapper();
try {
final Map<Long, String> result = mapper.readValue(json, new TypeReference<>() {});
return result.entrySet().stream().collect(toMap(Map.Entry::getKey, e -> Key.newBuilder()
.ed25519(Bytes.wrap(unhex(e.getValue())))
.build()));
} catch (JsonProcessingException e) {
log.warn("Unable to parse override keys", e);
return emptyMap();

Check warning on line 246 in hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/schemas/V053AddressBookSchema.java

View check run for this annotation

Codecov / codecov/patch

hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/schemas/V053AddressBookSchema.java#L244-L246

Added lines #L244 - L246 were not covered by tests
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
requires transitive dagger;
requires transitive javax.inject;
requires com.hedera.node.app.service.token;
requires com.swirlds.common;
requires com.swirlds.platform.core;
requires com.fasterxml.jackson.core;
requires com.fasterxml.jackson.databind;
requires org.apache.logging.log4j;
requires static transitive java.compiler;
requires static com.github.spotbugs.annotations;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public class AddressBookTestBase {
protected final Key key = A_COMPLEX_KEY;
protected final Key anotherKey = B_COMPLEX_KEY;

protected final Bytes defauleAdminKeyBytes =
protected final Bytes defaultAdminKeyBytes =
Bytes.wrap("0aa8e21064c61eab86e2a9c164565b4e7a9a4146106e0a6cd03a8c395a110e92");

final Key invalidKey = Key.newBuilder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

import com.hedera.hapi.node.base.AccountID;
import com.hedera.hapi.node.base.FileID;
import com.hedera.hapi.node.base.Key;
import com.hedera.hapi.node.base.NodeAddress;
import com.hedera.hapi.node.base.NodeAddressBook;
import com.hedera.hapi.node.state.addressbook.Node;
Expand All @@ -47,18 +48,29 @@
import com.swirlds.state.lifecycle.info.NetworkInfo;
import com.swirlds.state.test.fixtures.MapWritableKVState;
import com.swirlds.state.test.fixtures.MapWritableStates;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith({MockitoExtension.class, LogCaptureExtension.class})
class V053AddressBookSchemaTest extends AddressBookTestBase {
private static final Key NODE0_ADMIN_KEY = Key.newBuilder()
.ed25519(Bytes.fromHex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))
.build();
private static final Key NODE1_ADMIN_KEY = Key.newBuilder()
.ed25519(Bytes.fromHex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"))
.build();

@LoggingTarget
private LogCaptor logCaptor;

Expand All @@ -68,6 +80,9 @@ class V053AddressBookSchemaTest extends AddressBookTestBase {
@Mock
private NetworkInfo networkInfo;

@TempDir
java.nio.file.Path tempDir;

@LoggingSubject
private V053AddressBookSchema subject;

Expand Down Expand Up @@ -97,6 +112,15 @@ void registersExpectedSchema() {
assertEquals(NODES_KEY, iter.next());
}

@Test
void parsesExpectedAdminKeys() {
final Map<Long, Key> expectedKeys = Map.of(
0L, NODE0_ADMIN_KEY,
1L, NODE1_ADMIN_KEY);
final var actualKeys = V053AddressBookSchema.parseEd25519NodeAdminKeys(nodeAdminKeysJson());
assertEquals(expectedKeys, actualKeys);
}

@Test
void migrateAsExpected() {
setupMigrationContext();
Expand Down Expand Up @@ -124,7 +148,7 @@ void migrateAsExpected2() {
.gossipEndpoint(List.of(endpointFor("23.45.34.245", 22), endpointFor("127.0.0.1", 123)))
.gossipCaCertificate(Bytes.wrap(gossipCaCertificate))
.weight(0)
.adminKey(anotherKey)
.adminKey(NODE1_ADMIN_KEY)
.build(),
writableNodes.get(EntityNumber.newBuilder().number(1).build()));
assertEquals(
Expand Down Expand Up @@ -204,8 +228,10 @@ void migrateAsExpected4() {

assertThatCode(() -> subject.migrate(migrationContext)).doesNotThrowAnyException();
assertThat(logCaptor.infoLogs()).contains("Started migrating nodes from address book");
assertThat(logCaptor.warnLogs()).hasSize(1);
assertThat(logCaptor.warnLogs()).matches(logs -> logs.getFirst()
assertThat(logCaptor.warnLogs()).hasSize(2);
assertThat(logCaptor.warnLogs()).matches(logs -> logs.getFirst().contains("Unable to read override keys"));

assertThat(logCaptor.warnLogs()).matches(logs -> logs.getLast()
.contains("Can not parse file 102 com.hedera.pbj.runtime.ParseException: "));
assertThat(logCaptor.infoLogs()).contains("Migrated 3 nodes from address book");
assertEquals(
Expand Down Expand Up @@ -253,6 +279,7 @@ void failedNullNetworkinfo() {

private void setupMigrationContext() {
writableStates = MapWritableStates.builder().state(writableNodes).build();
given(migrationContext.isGenesis()).willReturn(true);
given(migrationContext.newStates()).willReturn(writableStates);

final var nodeInfo1 = new NodeInfoImpl(
Expand All @@ -276,7 +303,7 @@ private void setupMigrationContext() {
given(networkInfo.addressBook()).willReturn(List.of(nodeInfo1, nodeInfo2, nodeInfo3));
given(migrationContext.genesisNetworkInfo()).willReturn(networkInfo);
final var config = HederaTestConfigBuilder.create()
.withValue("bootstrap.genesisPublicKey", defauleAdminKeyBytes)
.withValue("bootstrap.genesisPublicKey", defaultAdminKeyBytes)
.getOrCreateConfig();
given(migrationContext.configuration()).willReturn(config);
}
Expand All @@ -292,8 +319,17 @@ private void setupMigrationContext2() {
.build();
given(migrationContext.newStates()).willReturn(writableStates);

final var adminKeysLoc = tempDir.resolve("node-admin-keys.json");
try {
Files.writeString(adminKeysLoc, nodeAdminKeysJson());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
final var config = HederaTestConfigBuilder.create()
.withValue("bootstrap.genesisPublicKey", defauleAdminKeyBytes)
.withValue("bootstrap.genesisPublicKey", defaultAdminKeyBytes)
.withValue(
"bootstrap.nodeAdminKeys.path",
adminKeysLoc.toAbsolutePath().toString())
.withValue("accounts.addressBookAdmin", "55")
.getOrCreateConfig();
given(migrationContext.configuration()).willReturn(config);
Expand Down Expand Up @@ -327,7 +363,7 @@ private void setupMigrationContext3() {
given(migrationContext.newStates()).willReturn(writableStates);

final var config = HederaTestConfigBuilder.create()
.withValue("bootstrap.genesisPublicKey", defauleAdminKeyBytes)
.withValue("bootstrap.genesisPublicKey", defaultAdminKeyBytes)
.withValue("accounts.addressBookAdmin", "55")
.withValue("files.nodeDetails", "102")
.getOrCreateConfig();
Expand All @@ -348,10 +384,18 @@ private void setupMigrationContext4() {
given(migrationContext.newStates()).willReturn(writableStates);

final var config = HederaTestConfigBuilder.create()
.withValue("bootstrap.genesisPublicKey", defauleAdminKeyBytes)
.withValue("bootstrap.genesisPublicKey", defaultAdminKeyBytes)
.withValue("accounts.addressBookAdmin", "55")
.withValue("files.nodeDetails", "102")
.getOrCreateConfig();
given(migrationContext.configuration()).willReturn(config);
}

private String nodeAdminKeysJson() {
return """
{
"0": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"1": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
}""";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.swirlds.state.lifecycle.info.NetworkInfo;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.time.Instant;
import java.util.function.Consumer;

/**
* Lets a service do genesis entity creations that must be legible in the block stream as specific HAPI
Expand All @@ -39,12 +40,12 @@ public interface SystemContext {
void dispatchCreation(@NonNull TransactionBody txBody, long entityNum);

/**
* Dispatches a transaction to the appropriate service
* Dispatches a transaction body customized by the given specification to the appropriate service.
*
* @param txBody the transaction body
* @param spec the transaction body
* @throws IllegalArgumentException if the entity number is not less than the first user entity number
*/
void dispatchUpdate(@NonNull TransactionBody txBody);
void dispatchAdmin(@NonNull Consumer<TransactionBody.Builder> spec);

/**
* The {@link Configuration} at genesis.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import static com.swirlds.logging.legacy.LogMarker.EXCEPTION;
import static com.swirlds.platform.builder.PlatformBuildConstants.DEFAULT_CONFIG_FILE_NAME;
import static com.swirlds.platform.builder.PlatformBuildConstants.DEFAULT_SETTINGS_FILE_NAME;
import static com.swirlds.platform.builder.PlatformBuildConstants.LOG4J_FILE_NAME;
import static com.swirlds.platform.builder.internal.StaticPlatformBuilder.getMetricsProvider;
import static com.swirlds.platform.builder.internal.StaticPlatformBuilder.setupGlobalMetrics;
import static com.swirlds.platform.config.internal.PlatformConfigUtils.checkConfiguration;
Expand Down Expand Up @@ -54,6 +55,7 @@
import com.swirlds.common.merkle.crypto.MerkleCryptoFactory;
import com.swirlds.common.merkle.crypto.MerkleCryptographyFactory;
import com.swirlds.common.platform.NodeId;
import com.swirlds.common.startup.Log4jSetup;
import com.swirlds.config.api.Configuration;
import com.swirlds.config.api.ConfigurationBuilder;
import com.swirlds.config.extensions.sources.SystemEnvironmentConfigSource;
Expand Down Expand Up @@ -227,6 +229,8 @@

// Create initial state for the platform
final var isGenesis = new AtomicBoolean(false);
// We want to be able to see the schema migration logs, so init logging here
initLogging();

Check warning on line 233 in hedera-node/hedera-app/src/main/java/com/hedera/node/app/ServicesMain.java

View check run for this annotation

Codecov / codecov/patch

hedera-node/hedera-app/src/main/java/com/hedera/node/app/ServicesMain.java#L233

Added line #L233 was not covered by tests
final var reservedState = getInitialState(
configuration,
recycleBin,
Expand Down Expand Up @@ -343,6 +347,18 @@
hedera.run();
}

private static void initLogging() {
final var log4jPath = getAbsolutePath(LOG4J_FILE_NAME);

Check warning on line 351 in hedera-node/hedera-app/src/main/java/com/hedera/node/app/ServicesMain.java

View check run for this annotation

Codecov / codecov/patch

hedera-node/hedera-app/src/main/java/com/hedera/node/app/ServicesMain.java#L351

Added line #L351 was not covered by tests
try {
Log4jSetup.startLoggingFramework(log4jPath).await();
} catch (final InterruptedException e) {

Check warning on line 354 in hedera-node/hedera-app/src/main/java/com/hedera/node/app/ServicesMain.java

View check run for this annotation

Codecov / codecov/patch

hedera-node/hedera-app/src/main/java/com/hedera/node/app/ServicesMain.java#L353-L354

Added lines #L353 - L354 were not covered by tests
// since the logging framework has not been instantiated, also log to stderr
e.printStackTrace();
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted while waiting for log4j to initialize", e);
}
}

Check warning on line 360 in hedera-node/hedera-app/src/main/java/com/hedera/node/app/ServicesMain.java

View check run for this annotation

Codecov / codecov/patch

hedera-node/hedera-app/src/main/java/com/hedera/node/app/ServicesMain.java#L356-L360

Added lines #L356 - L360 were not covered by tests

/**
* Build the configuration for this node.
*
Expand Down
Loading
Loading