From 97564dab8ddde77423bb9f4c95f0fffaf89e9f9c Mon Sep 17 00:00:00 2001 From: Miroslav Gatsanoga Date: Tue, 21 Jan 2025 17:22:16 +0200 Subject: [PATCH 1/6] feat: add synthetic node creates to record stream at genesis Signed-off-by: Miroslav Gatsanoga --- .../workflows/handle/record/SystemSetup.java | 51 +++++++++++++- .../hedera-app/src/main/java/module-info.java | 3 +- .../handle/steps/SystemSetupTest.java | 66 ++++++++++++++++++- .../impl/schemas/SyntheticNodeCreator.java | 61 +++++++++++++++++ .../src/main/java/module-info.java | 21 +++++- .../suites/hip993/SystemFileExportsTest.java | 17 +++++ 6 files changed, 212 insertions(+), 7 deletions(-) create mode 100644 hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/schemas/SyntheticNodeCreator.java diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/SystemSetup.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/SystemSetup.java index ec0320db7b9e..d84bd7d77b10 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/SystemSetup.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/SystemSetup.java @@ -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; @@ -31,6 +32,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; @@ -39,6 +41,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.token.Account; import com.hedera.hapi.node.token.CryptoCreateTransactionBody; @@ -46,9 +49,11 @@ import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.node.app.ids.EntityIdService; 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.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.schemas.SyntheticAccountCreator; import com.hedera.node.app.service.token.records.GenesisAccountStreamBuilder; import com.hedera.node.app.service.token.records.TokenContext; @@ -106,16 +111,19 @@ public class SystemSetup { private static final String TREASURY_CLONE_MEMO = "Synthetic zero-balance treasury clone"; private static final Comparator ACCOUNT_COMPARATOR = Comparator.comparing(Account::accountId, ACCOUNT_ID_COMPARATOR); + public static final Comparator NODE_COMPARATOR = Comparator.comparing(Node::nodeId, Long::compare); private SortedSet systemAccounts = new TreeSet<>(ACCOUNT_COMPARATOR); private SortedSet stakingAccounts = new TreeSet<>(ACCOUNT_COMPARATOR); private SortedSet miscAccounts = new TreeSet<>(ACCOUNT_COMPARATOR); private SortedSet treasuryClones = new TreeSet<>(ACCOUNT_COMPARATOR); private SortedSet blocklistAccounts = new TreeSet<>(ACCOUNT_COMPARATOR); + private SortedSet 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}. @@ -123,9 +131,11 @@ public class 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); } /** @@ -347,6 +357,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()); @@ -378,6 +390,12 @@ public void externalizeInitSideEffects( log.info("Queued {} blocklist account records", blocklistAccounts.size()); blocklistAccounts = null; } + + if (!genesisNodes.isEmpty()) { + createNodeRecordBuilders(genesisNodes, context); + log.info(" - Queued {} node create records", genesisNodes.size()); + genesisNodes = null; + } } private void systemAccounts(@NonNull final SortedSet accounts) { @@ -400,6 +418,10 @@ private void blocklistAccounts(@NonNull final SortedSet accounts) { requireNonNull(blocklistAccounts, "Genesis records already exported").addAll(requireNonNull(accounts)); } + private void nodes(@NonNull final SortedSet nodes) { + requireNonNull(genesisNodes, "Genesis records already exported").addAll(requireNonNull(nodes)); + } + private void createAccountRecordBuilders( @NonNull final SortedSet map, @NonNull final TokenContext context, @@ -408,6 +430,22 @@ private void createAccountRecordBuilders( createAccountRecordBuilders(map, context, recordMemo, null, exchangeRateSet); } + private void createNodeRecordBuilders(SortedSet nodes, @NonNull final TokenContext context) { + for (final Node node : nodes) { + final var recordBuilder = + context.addPrecedingChildRecordBuilder(NodeCreateStreamBuilder.class, NODE_CREATE); + recordBuilder.nodeID(node.nodeId()); + + 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 accts, @NonNull final TokenContext context, @@ -466,6 +504,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(); diff --git a/hedera-node/hedera-app/src/main/java/module-info.java b/hedera-node/hedera-app/src/main/java/module-info.java index 8004f231ff12..96cee23a7385 100644 --- a/hedera-node/hedera-app/src/main/java/module-info.java +++ b/hedera-node/hedera-app/src/main/java/module-info.java @@ -70,9 +70,10 @@ requires io.netty.transport.classes.epoll; requires io.netty.transport; requires org.apache.commons.lang3; + requires org.jetbrains.annotations; // javax.annotation.processing.Generated requires static com.github.spotbugs.annotations; requires static com.google.auto.service; - requires static java.compiler; // javax.annotation.processing.Generated + requires static java.compiler; exports com.hedera.node.app; exports com.hedera.node.app.state; diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/steps/SystemSetupTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/steps/SystemSetupTest.java index 0cf5346b766a..d5b07410fd08 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/steps/SystemSetupTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/steps/SystemSetupTest.java @@ -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. @@ -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; @@ -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; @@ -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; @@ -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"; @@ -102,6 +131,9 @@ class SystemSetupTest { @Mock private SyntheticAccountCreator syntheticAccountCreator; + @Mock + private SyntheticNodeCreator syntheticNodeCreator; + @Mock private FileServiceImpl fileService; @@ -111,6 +143,9 @@ class SystemSetupTest { @Mock private GenesisAccountStreamBuilder genesisAccountRecordBuilder; + @Mock + private NodeCreateStreamBuilder genesisNodeRecordBuilder; + @Mock private StoreFactory storeFactory; @@ -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 @@ -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>) invocationOnMock.getArgument(1)).accept(sysAccts); ((Consumer>) invocationOnMock.getArgument(2)).accept(stakingAccts); @@ -261,6 +301,13 @@ void externalizeInitSideEffectsCreatesAllRecords() { .generateSyntheticAccounts(any(), any(), any(), any(), any(), any()); given(genesisAccountRecordBuilder.accountID(any())).willReturn(genesisAccountRecordBuilder); + doAnswer(invocationOnMock -> { + ((Consumer>) invocationOnMock.getArgument(1)).accept(nodes); + return null; + }) + .when(syntheticNodeCreator) + .generateSyntheticNodes(any(), any()); + // Call the first time to make sure records are generated subject.externalizeInitSideEffects(context, ExchangeRateSet.DEFAULT); @@ -270,6 +317,21 @@ 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); assertThatThrownBy(() -> subject.externalizeInitSideEffects(context, ExchangeRateSet.DEFAULT)) diff --git a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/schemas/SyntheticNodeCreator.java b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/schemas/SyntheticNodeCreator.java new file mode 100644 index 000000000000..dc238de8d9a8 --- /dev/null +++ b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/schemas/SyntheticNodeCreator.java @@ -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_COMPARATOR = Comparator.comparing(Node::nodeId, Long::compare); + + /** + * Create a new instance. + */ + @Inject + public SyntheticNodeCreator() {} + + public void generateSyntheticNodes( + @NonNull final ReadableNodeStore readableNodeStore, + @NonNull final Consumer> 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); + } +} diff --git a/hedera-node/hedera-network-admin-service-impl/src/main/java/module-info.java b/hedera-node/hedera-network-admin-service-impl/src/main/java/module-info.java index 2336784160de..d8cf6fafee8d 100644 --- a/hedera-node/hedera-network-admin-service-impl/src/main/java/module-info.java +++ b/hedera-node/hedera-network-admin-service-impl/src/main/java/module-info.java @@ -1,3 +1,19 @@ +/* + * 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. + */ + import com.hedera.node.app.service.networkadmin.NetworkService; import com.hedera.node.app.service.networkadmin.impl.FreezeServiceImpl; @@ -9,10 +25,10 @@ requires transitive com.hedera.node.app.service.token; requires transitive com.hedera.node.app.spi; requires transitive com.hedera.node.hapi; + requires transitive com.hedera.pbj.runtime; requires transitive com.swirlds.config.api; requires transitive com.swirlds.platform.core; requires transitive com.swirlds.state.api; - requires transitive com.hedera.pbj.runtime; requires transitive dagger; requires transitive java.compiler; // javax.annotation.processing.Generated requires transitive javax.inject; @@ -31,6 +47,5 @@ exports com.hedera.node.app.service.networkadmin.impl; exports com.hedera.node.app.service.networkadmin.impl.handlers; - exports com.hedera.node.app.service.networkadmin.impl.schemas to - com.hedera.node.app; + exports com.hedera.node.app.service.networkadmin.impl.schemas; } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip993/SystemFileExportsTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip993/SystemFileExportsTest.java index dc7d55c39d19..1de1ad275270 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip993/SystemFileExportsTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip993/SystemFileExportsTest.java @@ -61,6 +61,7 @@ import static com.hedera.services.bdd.suites.utils.sysfiles.serdes.StandardSerdes.SYS_FILE_SERDES; import static com.hederahashgraph.api.proto.java.HederaFunctionality.FileCreate; import static com.hederahashgraph.api.proto.java.HederaFunctionality.FileUpdate; +import static com.hederahashgraph.api.proto.java.HederaFunctionality.NodeCreate; import static com.hederahashgraph.api.proto.java.HederaFunctionality.NodeStakeUpdate; import static com.hederahashgraph.api.proto.java.HederaFunctionality.NodeUpdate; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.BATCH_SIZE_LIMIT_EXCEEDED; @@ -209,6 +210,14 @@ final Stream syntheticNodeDetailsCreatedAtGenesis() { "First user entity num doesn't match config"))); } + @GenesisHapiTest + final Stream syntheticNodeCreatesExternalizedAtGenesis() { + return hapiTest( + recordStreamMustIncludeNoFailuresFrom(visibleItems(syntheticNodeCreatesValidator(), "genesisTxn")), + // This is the genesis transaction + cryptoCreate("firstUser").via("genesisTxn")); + } + @GenesisHapiTest final Stream syntheticFeeSchedulesUpdateHappensAtUpgradeBoundary() throws InvalidProtocolBufferException { @@ -545,6 +554,14 @@ private static byte[] getHexStringBytesFromBytes(final byte[] rawBytes) { return Normalizer.normalize(hexString, Normalizer.Form.NFD).getBytes(UTF_8); } + private static VisibleItemsValidator syntheticNodeCreatesValidator() { + return (spec, records) -> { + final var items = requireNonNull(records.get("genesisTxn")); + final var histogram = statusHistograms(items.entries()); + assertEquals(Map.of(SUCCESS, CLASSIC_HAPI_TEST_NETWORK_SIZE), histogram.get(NodeCreate)); + }; + } + private static VisibleItemsValidator addressBookExportValidator( @NonNull final String fileNumProperty, @NonNull final byte[][] grpcCertHashes) { return (spec, records) -> { From cce78a3d37870d2031d6d9c04bb28f53d9d2a657 Mon Sep 17 00:00:00 2001 From: Miroslav Gatsanoga Date: Tue, 21 Jan 2025 17:44:06 +0200 Subject: [PATCH 2/6] fix: revert not needed change Signed-off-by: Miroslav Gatsanoga --- hedera-node/hedera-app/src/main/java/module-info.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hedera-node/hedera-app/src/main/java/module-info.java b/hedera-node/hedera-app/src/main/java/module-info.java index 96cee23a7385..8004f231ff12 100644 --- a/hedera-node/hedera-app/src/main/java/module-info.java +++ b/hedera-node/hedera-app/src/main/java/module-info.java @@ -70,10 +70,9 @@ requires io.netty.transport.classes.epoll; requires io.netty.transport; requires org.apache.commons.lang3; - requires org.jetbrains.annotations; // javax.annotation.processing.Generated requires static com.github.spotbugs.annotations; requires static com.google.auto.service; - requires static java.compiler; + requires static java.compiler; // javax.annotation.processing.Generated exports com.hedera.node.app; exports com.hedera.node.app.state; From 400a3e2474759fa38ac89269bb8f5957ea3bd1da Mon Sep 17 00:00:00 2001 From: Miroslav Gatsanoga Date: Wed, 22 Jan 2025 11:17:35 +0200 Subject: [PATCH 3/6] fix: set exchange rate in synthetic node create records Signed-off-by: Miroslav Gatsanoga --- .../node/app/workflows/handle/record/SystemSetup.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/SystemSetup.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/SystemSetup.java index d84bd7d77b10..19a116577e92 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/SystemSetup.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/SystemSetup.java @@ -392,7 +392,7 @@ public void externalizeInitSideEffects( } if (!genesisNodes.isEmpty()) { - createNodeRecordBuilders(genesisNodes, context); + createNodeRecordBuilders(genesisNodes, context, exchangeRateSet); log.info(" - Queued {} node create records", genesisNodes.size()); genesisNodes = null; } @@ -430,11 +430,14 @@ private void createAccountRecordBuilders( createAccountRecordBuilders(map, context, recordMemo, null, exchangeRateSet); } - private void createNodeRecordBuilders(SortedSet nodes, @NonNull final TokenContext context) { + private void createNodeRecordBuilders( + SortedSet 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()); + recordBuilder.nodeID(node.nodeId()).exchangeRate(exchangeRateSet); final var op = newNodeCreate(node); final var bodyBuilder = TransactionBody.newBuilder().nodeCreate(op); From 57270c14196be874530a9c28990d6e52caec6b10 Mon Sep 17 00:00:00 2001 From: Miroslav Gatsanoga Date: Wed, 22 Jan 2025 11:18:13 +0200 Subject: [PATCH 4/6] test: update tests Signed-off-by: Miroslav Gatsanoga --- .../node/app/workflows/handle/steps/SystemSetupTest.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/steps/SystemSetupTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/steps/SystemSetupTest.java index d5b07410fd08..e2cd6477e796 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/steps/SystemSetupTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/steps/SystemSetupTest.java @@ -307,6 +307,7 @@ void externalizeInitSideEffectsCreatesAllRecords() { }) .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); @@ -334,15 +335,18 @@ void externalizeInitSideEffectsCreatesAllRecords() { // 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) { From 27b560f4438639927e81e0a31d016fc211f7ba51 Mon Sep 17 00:00:00 2001 From: Miroslav Gatsanoga Date: Wed, 22 Jan 2025 16:57:11 +0200 Subject: [PATCH 5/6] fix: parity translator to see genesis node creates Signed-off-by: Miroslav Gatsanoga --- .../junit/support/translators/BaseTranslator.java | 12 ++++++------ .../BlockTransactionalUnitTranslator.java | 9 ++++----- .../block/TransactionRecordParityValidator.java | 11 +++++------ 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/translators/BaseTranslator.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/translators/BaseTranslator.java index 7180760d486f..a915a721aec7 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/translators/BaseTranslator.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/translators/BaseTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Hedera Hashgraph, LLC + * Copyright (C) 2024-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. @@ -86,7 +86,8 @@ public class BaseTranslator { */ private long highestKnownEntityNum = 0L; - private long highestKnownNodeId; + private long highestKnownNodeId = + -1L; // Default to negative value so that we allow for nodeId with 0 value to be created private ExchangeRateSet activeRates; private final Map totalSupplies = new HashMap<>(); @@ -118,11 +119,10 @@ void accept( } /** - * Constructs a translator with the given highest known node ID. - * @param highestKnownNodeId the highest known node ID + * Constructs a base translator. */ - public BaseTranslator(final long highestKnownNodeId) { - this.highestKnownNodeId = highestKnownNodeId; + public BaseTranslator() { + // Using default field values } /** diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/translators/BlockTransactionalUnitTranslator.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/translators/BlockTransactionalUnitTranslator.java index e9d6217bc1bf..2af83c46a02a 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/translators/BlockTransactionalUnitTranslator.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/translators/BlockTransactionalUnitTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Hedera Hashgraph, LLC + * Copyright (C) 2024-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. @@ -179,11 +179,10 @@ public class BlockTransactionalUnitTranslator { }; /** - * Constructs a new {@link BlockTransactionalUnitTranslator} with the given network size. - * @param networkSize the network size + * Constructs a new {@link BlockTransactionalUnitTranslator}. */ - public BlockTransactionalUnitTranslator(final int networkSize) { - baseTranslator = new BaseTranslator(networkSize - 1); + public BlockTransactionalUnitTranslator() { + baseTranslator = new BaseTranslator(); } /** diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/validators/block/TransactionRecordParityValidator.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/validators/block/TransactionRecordParityValidator.java index 00f076d7cae7..d9e3210b2c57 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/validators/block/TransactionRecordParityValidator.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/validators/block/TransactionRecordParityValidator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Hedera Hashgraph, LLC + * Copyright (C) 2024-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. @@ -67,13 +67,12 @@ public boolean appliesTo(@NonNull final HapiSpec spec) { @Override public @NonNull TransactionRecordParityValidator create(@NonNull final HapiSpec spec) { - return new TransactionRecordParityValidator( - spec.targetNetworkOrThrow().nodes().size()); + return new TransactionRecordParityValidator(); } }; - public TransactionRecordParityValidator(final int networkSize) { - translator = new BlockTransactionalUnitTranslator(networkSize); + public TransactionRecordParityValidator() { + translator = new BlockTransactionalUnitTranslator(); } /** @@ -93,7 +92,7 @@ public static void main(@NonNull final String[] args) throws IOException { node0Data.resolve("recordStreams/record0.0.3").toAbsolutePath().normalize(); final var records = StreamFileAccess.STREAM_FILE_ACCESS.readStreamDataFrom(recordsLoc.toString(), "sidecar"); - final var validator = new TransactionRecordParityValidator(4); + final var validator = new TransactionRecordParityValidator(); validator.validateBlockVsRecords(blocks, records); } From a1182a926070b00b88854f8aeb8118b3358cca7b Mon Sep 17 00:00:00 2001 From: Miroslav Gatsanoga Date: Wed, 22 Jan 2025 18:56:57 +0200 Subject: [PATCH 6/6] chore: move test to separate class Signed-off-by: Miroslav Gatsanoga --- .../suites/hip993/SystemFileExportsTest.java | 17 ------ .../SyntheticNodeCreateExportsTest.java | 55 +++++++++++++++++++ 2 files changed, 55 insertions(+), 17 deletions(-) create mode 100644 hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/records/SyntheticNodeCreateExportsTest.java diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip993/SystemFileExportsTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip993/SystemFileExportsTest.java index 1de1ad275270..dc7d55c39d19 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip993/SystemFileExportsTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip993/SystemFileExportsTest.java @@ -61,7 +61,6 @@ import static com.hedera.services.bdd.suites.utils.sysfiles.serdes.StandardSerdes.SYS_FILE_SERDES; import static com.hederahashgraph.api.proto.java.HederaFunctionality.FileCreate; import static com.hederahashgraph.api.proto.java.HederaFunctionality.FileUpdate; -import static com.hederahashgraph.api.proto.java.HederaFunctionality.NodeCreate; import static com.hederahashgraph.api.proto.java.HederaFunctionality.NodeStakeUpdate; import static com.hederahashgraph.api.proto.java.HederaFunctionality.NodeUpdate; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.BATCH_SIZE_LIMIT_EXCEEDED; @@ -210,14 +209,6 @@ final Stream syntheticNodeDetailsCreatedAtGenesis() { "First user entity num doesn't match config"))); } - @GenesisHapiTest - final Stream syntheticNodeCreatesExternalizedAtGenesis() { - return hapiTest( - recordStreamMustIncludeNoFailuresFrom(visibleItems(syntheticNodeCreatesValidator(), "genesisTxn")), - // This is the genesis transaction - cryptoCreate("firstUser").via("genesisTxn")); - } - @GenesisHapiTest final Stream syntheticFeeSchedulesUpdateHappensAtUpgradeBoundary() throws InvalidProtocolBufferException { @@ -554,14 +545,6 @@ private static byte[] getHexStringBytesFromBytes(final byte[] rawBytes) { return Normalizer.normalize(hexString, Normalizer.Form.NFD).getBytes(UTF_8); } - private static VisibleItemsValidator syntheticNodeCreatesValidator() { - return (spec, records) -> { - final var items = requireNonNull(records.get("genesisTxn")); - final var histogram = statusHistograms(items.entries()); - assertEquals(Map.of(SUCCESS, CLASSIC_HAPI_TEST_NETWORK_SIZE), histogram.get(NodeCreate)); - }; - } - private static VisibleItemsValidator addressBookExportValidator( @NonNull final String fileNumProperty, @NonNull final byte[][] grpcCertHashes) { return (spec, records) -> { diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/records/SyntheticNodeCreateExportsTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/records/SyntheticNodeCreateExportsTest.java new file mode 100644 index 000000000000..89c82f6bf745 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/records/SyntheticNodeCreateExportsTest.java @@ -0,0 +1,55 @@ +/* + * 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.services.bdd.suites.records; + +import static com.hedera.node.app.hapi.utils.forensics.OrderedComparison.statusHistograms; +import static com.hedera.services.bdd.junit.SharedNetworkLauncherSessionListener.CLASSIC_HAPI_TEST_NETWORK_SIZE; +import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.recordStreamMustIncludeNoFailuresFrom; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.visibleItems; +import static com.hederahashgraph.api.proto.java.HederaFunctionality.NodeCreate; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.hedera.services.bdd.junit.GenesisHapiTest; +import com.hedera.services.bdd.spec.utilops.streams.assertions.VisibleItemsValidator; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.DynamicTest; + +/** + * Asserts the synthetic node creations after the network has handled the genesis transaction. + */ +public class SyntheticNodeCreateExportsTest { + @GenesisHapiTest + final Stream syntheticNodeCreatesExternalizedAtGenesis() { + return hapiTest( + recordStreamMustIncludeNoFailuresFrom(visibleItems(syntheticNodeCreatesValidator(), "genesisTxn")), + // This is the genesis transaction + cryptoCreate("firstUser").via("genesisTxn")); + } + + private static VisibleItemsValidator syntheticNodeCreatesValidator() { + return (spec, records) -> { + final var items = requireNonNull(records.get("genesisTxn")); + final var histogram = statusHistograms(items.entries()); + assertEquals(Map.of(SUCCESS, CLASSIC_HAPI_TEST_NETWORK_SIZE), histogram.get(NodeCreate)); + }; + } +}