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

feat: add synthetic node creates to record stream at genesis #17461

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
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