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 04e4dd4e0ddb..cd758d6f412d 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; @@ -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; @@ -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; @@ -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; @@ -126,16 +131,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}. @@ -143,9 +151,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); } /** @@ -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()); @@ -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 accounts) { @@ -509,6 +527,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, @@ -517,6 +539,25 @@ private void createAccountRecordBuilders( createAccountRecordBuilders(map, context, recordMemo, null, exchangeRateSet); } + 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()).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 accts, @NonNull final TokenContext context, @@ -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(); 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..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 @@ -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,14 @@ 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()); + given(genesisNodeRecordBuilder.nodeID(any(Long.class))).willReturn(genesisNodeRecordBuilder); + // Call the first time to make sure records are generated subject.externalizeInitSideEffects(context, ExchangeRateSet.DEFAULT); @@ -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) { 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/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); } 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)); + }; + } +}