Skip to content

Commit

Permalink
chore: support override node admin keys (#16781)
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Tinker <[email protected]>
  • Loading branch information
tinker-michaelj authored Dec 2, 2024
1 parent 1450da9 commit cd75b68
Show file tree
Hide file tree
Showing 25 changed files with 422 additions and 122 deletions.
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 @@ public Set<StateDefinition> statesToCreate() {
@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 @@ public void migrate(@NonNull final MigrationContext ctx) {
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 @@ public void migrate(@NonNull final MigrationContext ctx) {
.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 @@ private Map<Long, NodeAddress> getNodeAddressMap(@NonNull final MigrationContext
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 @@ public static ServiceEndpoint endpointFor(@NonNull final String host, final int
}
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();
}
}
}
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 @@ -225,8 +227,19 @@ public static void main(final String... args) throws Exception {
final var recycleBin =
RecycleBin.create(metrics, configuration, getStaticThreadManager(), time, fileSystemManager, selfId);

final var cryptography = CryptographyFactory.create();
CryptographyHolder.set(cryptography);
// the AddressBook is not changed after this point, so we calculate the hash now
cryptography.digestSync(diskAddressBook);

// Initialize the Merkle cryptography
final var merkleCryptography = MerkleCryptographyFactory.create(configuration, cryptography);
MerkleCryptoFactory.set(merkleCryptography);

// 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();
final var reservedState = getInitialState(
configuration,
recycleBin,
Expand Down Expand Up @@ -256,15 +269,6 @@ public static void main(final String... args) throws Exception {
configuration);
}

final var cryptography = CryptographyFactory.create();
CryptographyHolder.set(cryptography);
// the AddressBook is not changed after this point, so we calculate the hash now
cryptography.digestSync(diskAddressBook);

// Initialize the Merkle cryptography
final var merkleCryptography = MerkleCryptographyFactory.create(configuration, cryptography);
MerkleCryptoFactory.set(merkleCryptography);

// Create the platform context
final var platformContext = PlatformContext.create(
configuration,
Expand Down Expand Up @@ -343,6 +347,18 @@ public static void main(final String... args) throws Exception {
hedera.run();
}

private static void initLogging() {
final var log4jPath = getAbsolutePath(LOG4J_FILE_NAME);
try {
Log4jSetup.startLoggingFramework(log4jPath).await();
} catch (final InterruptedException e) {
// 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);
}
}

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

0 comments on commit cd75b68

Please sign in to comment.