Skip to content

Commit

Permalink
feat: add synthetic node creates to record stream at genesis (#17461)
Browse files Browse the repository at this point in the history
Signed-off-by: Miroslav Gatsanoga <[email protected]>
  • Loading branch information
MiroslavGatsanoga authored Jan 28, 2025
1 parent 5f7e2d9 commit 22830ad
Show file tree
Hide file tree
Showing 8 changed files with 270 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.hedera.node.app.workflows.handle.record;

import static com.hedera.hapi.node.base.HederaFunctionality.CRYPTO_CREATE;
import static com.hedera.hapi.node.base.HederaFunctionality.NODE_CREATE;
import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS;
import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS_BUT_MISSING_EXPECTED_OPERATION;
import static com.hedera.hapi.util.HapiUtils.ACCOUNT_ID_COMPARATOR;
Expand Down Expand Up @@ -45,6 +46,7 @@
import static com.hedera.node.app.util.FileUtilities.createFileID;
import static java.util.Objects.requireNonNull;

import com.hedera.hapi.node.addressbook.NodeCreateTransactionBody;
import com.hedera.hapi.node.addressbook.NodeUpdateTransactionBody;
import com.hedera.hapi.node.base.AccountID;
import com.hedera.hapi.node.base.CurrentAndNextFeeSchedule;
Expand All @@ -53,6 +55,7 @@
import com.hedera.hapi.node.base.ResponseCodeEnum;
import com.hedera.hapi.node.base.TransactionID;
import com.hedera.hapi.node.base.TransferList;
import com.hedera.hapi.node.state.addressbook.Node;
import com.hedera.hapi.node.state.common.EntityNumber;
import com.hedera.hapi.node.state.entity.EntityCounts;
import com.hedera.hapi.node.state.token.Account;
Expand All @@ -62,11 +65,13 @@
import com.hedera.node.app.ids.EntityIdService;
import com.hedera.node.app.service.addressbook.AddressBookService;
import com.hedera.node.app.service.addressbook.ReadableNodeStore;
import com.hedera.node.app.service.addressbook.impl.records.NodeCreateStreamBuilder;
import com.hedera.node.app.service.addressbook.impl.schemas.V053AddressBookSchema;
import com.hedera.node.app.service.consensus.ConsensusService;
import com.hedera.node.app.service.contract.ContractService;
import com.hedera.node.app.service.file.impl.FileServiceImpl;
import com.hedera.node.app.service.file.impl.schemas.V0490FileSchema;
import com.hedera.node.app.service.networkadmin.impl.schemas.SyntheticNodeCreator;
import com.hedera.node.app.service.schedule.ScheduleService;
import com.hedera.node.app.service.token.TokenService;
import com.hedera.node.app.service.token.impl.schemas.SyntheticAccountCreator;
Expand Down Expand Up @@ -126,26 +131,31 @@ public class SystemSetup {
private static final String TREASURY_CLONE_MEMO = "Synthetic zero-balance treasury clone";
private static final Comparator<Account> ACCOUNT_COMPARATOR =
Comparator.comparing(Account::accountId, ACCOUNT_ID_COMPARATOR);
public static final Comparator<Node> NODE_COMPARATOR = Comparator.comparing(Node::nodeId, Long::compare);

private SortedSet<Account> systemAccounts = new TreeSet<>(ACCOUNT_COMPARATOR);
private SortedSet<Account> stakingAccounts = new TreeSet<>(ACCOUNT_COMPARATOR);
private SortedSet<Account> miscAccounts = new TreeSet<>(ACCOUNT_COMPARATOR);
private SortedSet<Account> treasuryClones = new TreeSet<>(ACCOUNT_COMPARATOR);
private SortedSet<Account> blocklistAccounts = new TreeSet<>(ACCOUNT_COMPARATOR);
private SortedSet<Node> genesisNodes = new TreeSet<>(NODE_COMPARATOR);

private final AtomicInteger nextDispatchNonce = new AtomicInteger(1);
private final FileServiceImpl fileService;
private final SyntheticAccountCreator syntheticAccountCreator;
private final SyntheticNodeCreator syntheticNodeCreator;

/**
* Constructs a new {@link SystemSetup}.
*/
@Inject
public SystemSetup(
@NonNull final FileServiceImpl fileService,
@NonNull final SyntheticAccountCreator syntheticAccountCreator) {
@NonNull final SyntheticAccountCreator syntheticAccountCreator,
@NonNull final SyntheticNodeCreator syntheticNodeCreator) {
this.fileService = requireNonNull(fileService);
this.syntheticAccountCreator = requireNonNull(syntheticAccountCreator);
this.syntheticNodeCreator = requireNonNull(syntheticNodeCreator);
}

/**
Expand Down Expand Up @@ -456,6 +466,8 @@ public void externalizeInitSideEffects(
this::miscAccounts,
this::blocklistAccounts);

syntheticNodeCreator.generateSyntheticNodes(context.readableStore(ReadableNodeStore.class), this::nodes);

if (!systemAccounts.isEmpty()) {
createAccountRecordBuilders(systemAccounts, context, SYSTEM_ACCOUNT_CREATION_MEMO, exchangeRateSet);
log.info(" - Queued {} system account records", systemAccounts.size());
Expand Down Expand Up @@ -487,6 +499,12 @@ public void externalizeInitSideEffects(
log.info("Queued {} blocklist account records", blocklistAccounts.size());
blocklistAccounts = null;
}

if (!genesisNodes.isEmpty()) {
createNodeRecordBuilders(genesisNodes, context, exchangeRateSet);
log.info(" - Queued {} node create records", genesisNodes.size());
genesisNodes = null;
}
}

private void systemAccounts(@NonNull final SortedSet<Account> accounts) {
Expand All @@ -509,6 +527,10 @@ private void blocklistAccounts(@NonNull final SortedSet<Account> accounts) {
requireNonNull(blocklistAccounts, "Genesis records already exported").addAll(requireNonNull(accounts));
}

private void nodes(@NonNull final SortedSet<Node> nodes) {
requireNonNull(genesisNodes, "Genesis records already exported").addAll(requireNonNull(nodes));
}

private void createAccountRecordBuilders(
@NonNull final SortedSet<Account> map,
@NonNull final TokenContext context,
Expand All @@ -517,6 +539,25 @@ private void createAccountRecordBuilders(
createAccountRecordBuilders(map, context, recordMemo, null, exchangeRateSet);
}

private void createNodeRecordBuilders(
SortedSet<Node> nodes,
@NonNull final TokenContext context,
@NonNull final ExchangeRateSet exchangeRateSet) {
for (final Node node : nodes) {
final var recordBuilder =
context.addPrecedingChildRecordBuilder(NodeCreateStreamBuilder.class, NODE_CREATE);
recordBuilder.nodeID(node.nodeId()).exchangeRate(exchangeRateSet);

final var op = newNodeCreate(node);
final var bodyBuilder = TransactionBody.newBuilder().nodeCreate(op);
final var body = bodyBuilder.build();
recordBuilder.transaction(transactionWith(body));
recordBuilder.status(SUCCESS);

log.debug("Queued synthetic NodeCreate for node {}", node);
}
}

private void createAccountRecordBuilders(
@NonNull final SortedSet<Account> accts,
@NonNull final TokenContext context,
Expand Down Expand Up @@ -575,6 +616,17 @@ private static CryptoCreateTransactionBody.Builder newCryptoCreate(@NonNull fina
.alias(account.alias());
}

private static NodeCreateTransactionBody.Builder newNodeCreate(Node node) {
return NodeCreateTransactionBody.newBuilder()
.accountId(node.accountId())
.description(node.description())
.gossipEndpoint(node.gossipEndpoint())
.serviceEndpoint(node.serviceEndpoint())
.gossipCaCertificate(node.gossipCaCertificate())
.grpcCertificateHash(node.grpcCertificateHash())
.adminKey(node.adminKey());
}

private static Bytes parseFeeSchedules(@NonNull final InputStream in) {
try {
final var bytes = in.readAllBytes();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Hedera Hashgraph, LLC
* Copyright (C) 2023-2025 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -17,7 +17,12 @@
package com.hedera.node.app.workflows.handle.steps;

import static com.hedera.hapi.node.base.HederaFunctionality.CRYPTO_CREATE;
import static com.hedera.hapi.node.base.HederaFunctionality.NODE_CREATE;
import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS;
import static com.hedera.node.app.service.addressbook.impl.schemas.V053AddressBookSchema.endpointFor;
import static com.hedera.node.app.service.file.impl.schemas.V0490FileSchema.parseFeeSchedules;
import static com.hedera.node.app.spi.workflows.record.StreamBuilder.transactionWith;
import static com.hedera.node.app.workflows.handle.record.SystemSetup.NODE_COMPARATOR;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.mockito.ArgumentMatchers.any;
Expand All @@ -30,19 +35,25 @@
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verifyNoInteractions;

import com.hedera.hapi.node.addressbook.NodeCreateTransactionBody;
import com.hedera.hapi.node.base.AccountAmount;
import com.hedera.hapi.node.base.AccountID;
import com.hedera.hapi.node.base.CurrentAndNextFeeSchedule;
import com.hedera.hapi.node.base.Key;
import com.hedera.hapi.node.base.ServicesConfigurationList;
import com.hedera.hapi.node.base.Setting;
import com.hedera.hapi.node.base.Timestamp;
import com.hedera.hapi.node.base.TransferList;
import com.hedera.hapi.node.state.addressbook.Node;
import com.hedera.hapi.node.state.blockrecords.BlockInfo;
import com.hedera.hapi.node.state.token.Account;
import com.hedera.hapi.node.transaction.ExchangeRateSet;
import com.hedera.hapi.node.transaction.TransactionBody;
import com.hedera.node.app.service.addressbook.ReadableNodeStore;
import com.hedera.node.app.service.addressbook.impl.records.NodeCreateStreamBuilder;
import com.hedera.node.app.service.file.impl.FileServiceImpl;
import com.hedera.node.app.service.file.impl.schemas.V0490FileSchema;
import com.hedera.node.app.service.networkadmin.impl.schemas.SyntheticNodeCreator;
import com.hedera.node.app.service.token.impl.comparator.TokenComparators;
import com.hedera.node.app.service.token.impl.schemas.SyntheticAccountCreator;
import com.hedera.node.app.service.token.records.GenesisAccountStreamBuilder;
Expand All @@ -66,6 +77,7 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.time.Instant;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.function.Consumer;
Expand All @@ -90,6 +102,23 @@ class SystemSetupTest {
.build();
private static final Account ACCOUNT_2 =
Account.newBuilder().accountId(ACCOUNT_ID_2).build();
private static final byte[] gossipCaCertificate = "gossipCaCertificate".getBytes();
private static final byte[] grpcCertificateHash = "grpcCertificateHash".getBytes();
private static final Key NODE1_ADMIN_KEY = Key.newBuilder()
.ed25519(Bytes.fromHex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"))
.build();

private static final Node NODE_1 = Node.newBuilder()
.nodeId(1)
.accountId(ACCOUNT_ID_1)
.description("node1")
.gossipEndpoint(List.of(endpointFor("23.45.34.240", 23), endpointFor("127.0.0.2", 123)))
.serviceEndpoint(List.of(endpointFor("127.0.0.2", 123)))
.gossipCaCertificate(Bytes.wrap(gossipCaCertificate))
.grpcCertificateHash(Bytes.wrap(grpcCertificateHash))
.adminKey(NODE1_ADMIN_KEY)
.build();

private static final Instant CONSENSUS_NOW = Instant.parse("2023-08-10T00:00:00Z");

private static final String EXPECTED_SYSTEM_ACCOUNT_CREATION_MEMO = "Synthetic system creation";
Expand All @@ -102,6 +131,9 @@ class SystemSetupTest {
@Mock
private SyntheticAccountCreator syntheticAccountCreator;

@Mock
private SyntheticNodeCreator syntheticNodeCreator;

@Mock
private FileServiceImpl fileService;

Expand All @@ -111,6 +143,9 @@ class SystemSetupTest {
@Mock
private GenesisAccountStreamBuilder genesisAccountRecordBuilder;

@Mock
private NodeCreateStreamBuilder genesisNodeRecordBuilder;

@Mock
private StoreFactory storeFactory;

Expand Down Expand Up @@ -140,8 +175,10 @@ void setup() {
given(context.consensusTime()).willReturn(CONSENSUS_NOW);
given(context.addPrecedingChildRecordBuilder(GenesisAccountStreamBuilder.class, CRYPTO_CREATE))
.willReturn(genesisAccountRecordBuilder);
given(context.addPrecedingChildRecordBuilder(NodeCreateStreamBuilder.class, NODE_CREATE))
.willReturn(genesisNodeRecordBuilder);

subject = new SystemSetup(fileService, syntheticAccountCreator);
subject = new SystemSetup(fileService, syntheticAccountCreator, syntheticNodeCreator);
}

@Test
Expand Down Expand Up @@ -249,6 +286,9 @@ void externalizeInitSideEffectsCreatesAllRecords() {
treasuryAccts.add(acct4);
final var blocklistAccts = new TreeSet<>(TokenComparators.ACCOUNT_COMPARATOR);
blocklistAccts.add(acct5);
final var nodes = new TreeSet<>(NODE_COMPARATOR);
nodes.add(NODE_1);

doAnswer(invocationOnMock -> {
((Consumer<SortedSet<Account>>) invocationOnMock.getArgument(1)).accept(sysAccts);
((Consumer<SortedSet<Account>>) invocationOnMock.getArgument(2)).accept(stakingAccts);
Expand All @@ -261,6 +301,14 @@ void externalizeInitSideEffectsCreatesAllRecords() {
.generateSyntheticAccounts(any(), any(), any(), any(), any(), any());
given(genesisAccountRecordBuilder.accountID(any())).willReturn(genesisAccountRecordBuilder);

doAnswer(invocationOnMock -> {
((Consumer<SortedSet<Node>>) invocationOnMock.getArgument(1)).accept(nodes);
return null;
})
.when(syntheticNodeCreator)
.generateSyntheticNodes(any(), any());
given(genesisNodeRecordBuilder.nodeID(any(Long.class))).willReturn(genesisNodeRecordBuilder);

// Call the first time to make sure records are generated
subject.externalizeInitSideEffects(context, ExchangeRateSet.DEFAULT);

Expand All @@ -270,17 +318,35 @@ void externalizeInitSideEffectsCreatesAllRecords() {
verifyBuilderInvoked(acctId4, EXPECTED_TREASURY_CLONE_MEMO);
verifyBuilderInvoked(acctId5, null);

verify(genesisNodeRecordBuilder).nodeID(NODE_1.nodeId());
verify(genesisNodeRecordBuilder)
.transaction(transactionWith(TransactionBody.newBuilder()
.nodeCreate(NodeCreateTransactionBody.newBuilder()
.accountId(NODE_1.accountId())
.description(NODE_1.description())
.gossipEndpoint(NODE_1.gossipEndpoint())
.serviceEndpoint(NODE_1.serviceEndpoint())
.gossipCaCertificate(NODE_1.gossipCaCertificate())
.grpcCertificateHash(NODE_1.grpcCertificateHash())
.adminKey(NODE_1.adminKey())
.build())
.build()));
verify(genesisNodeRecordBuilder).status(SUCCESS);

// Call externalizeInitSideEffects() a second time to make sure no other records are created
Mockito.clearInvocations(genesisAccountRecordBuilder);
Mockito.clearInvocations(genesisNodeRecordBuilder);
assertThatThrownBy(() -> subject.externalizeInitSideEffects(context, ExchangeRateSet.DEFAULT))
.isInstanceOf(NullPointerException.class);
verifyNoInteractions(genesisAccountRecordBuilder);
verifyNoInteractions(genesisNodeRecordBuilder);
}

@Test
void externalizeInitSideEffectsCreatesNoRecordsWhenEmpty() {
subject.externalizeInitSideEffects(context, ExchangeRateSet.DEFAULT);
verifyNoInteractions(genesisAccountRecordBuilder);
verifyNoInteractions(genesisNodeRecordBuilder);
}

private void verifyBuilderInvoked(final AccountID acctId, final String expectedMemo) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright (C) 2025 Hedera Hashgraph, LLC
*
* 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 com.hedera.node.app.service.networkadmin.impl.schemas;

import static java.util.Objects.requireNonNull;

import com.hedera.hapi.node.state.addressbook.Node;
import com.hedera.node.app.service.addressbook.ReadableNodeStore;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.Comparator;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.function.Consumer;
import javax.inject.Inject;
import javax.inject.Singleton;

/**
* This class generates synthetic records for all nodes created in state during genesis.
*/
@Singleton
public class SyntheticNodeCreator {
private static final Comparator<Node> NODE_COMPARATOR = Comparator.comparing(Node::nodeId, Long::compare);

/**
* Create a new instance.
*/
@Inject
public SyntheticNodeCreator() {}

public void generateSyntheticNodes(
@NonNull final ReadableNodeStore readableNodeStore,
@NonNull final Consumer<SortedSet<Node>> nodesConsumer) {
requireNonNull(readableNodeStore);
requireNonNull(nodesConsumer);

final var nodes = new TreeSet<>(NODE_COMPARATOR);
final var iter = readableNodeStore.keys();
while (iter.hasNext()) {
final var node = readableNodeStore.get(iter.next().number());
if (node != null) {
nodes.add(node);
}
}

nodesConsumer.accept(nodes);
}
}
Loading

0 comments on commit 22830ad

Please sign in to comment.