From 84c5adf130684b7ad2e48dbf8cb0b213c3a6401d Mon Sep 17 00:00:00 2001 From: Iris Simon <122310714+iwsimon@users.noreply.github.com> Date: Mon, 13 Jan 2025 11:24:45 -0500 Subject: [PATCH 01/19] fix: FileService address book and node details should be updated at genesis (#17278) Signed-off-by: Iris Simon --- .../workflows/handle/record/SystemSetup.java | 5 +- .../standalone/TransactionExecutorsTest.java | 13 +- .../service/file/impl/FileServiceImpl.java | 9 +- .../impl/handlers/FileGetContentsHandler.java | 29 ++--- .../file/impl/schemas/V0490FileSchema.java | 45 +------ .../src/main/java/module-info.java | 5 +- .../handlers/FileGetContentsHandlerTest.java | 8 +- .../spec/utilops/grouping/GroupingVerbs.java | 17 ++- .../suites/hip993/SystemFileExportsTest.java | 114 ++++++++++++++++-- 9 files changed, 164 insertions(+), 81 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 10280e33d89e..7d783c6413a1 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 @@ -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. @@ -133,7 +133,8 @@ public SystemSetup( */ public void doGenesisSetup(@NonNull final Dispatch dispatch) { final var systemContext = systemContextFor(dispatch); - fileService.createSystemEntities(systemContext); + final var nodeStore = dispatch.handleContext().storeFactory().readableStore(ReadableNodeStore.class); + fileService.createSystemEntities(systemContext, nodeStore); } /** diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/standalone/TransactionExecutorsTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/standalone/TransactionExecutorsTest.java index 6456994f07a7..5b3c3264546d 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/standalone/TransactionExecutorsTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/standalone/TransactionExecutorsTest.java @@ -53,7 +53,10 @@ import com.hedera.node.app.ids.EntityIdService; import com.hedera.node.app.info.NodeInfoImpl; import com.hedera.node.app.records.BlockRecordService; +import com.hedera.node.app.service.addressbook.AddressBookService; +import com.hedera.node.app.service.addressbook.ReadableNodeStore; import com.hedera.node.app.service.addressbook.impl.AddressBookServiceImpl; +import com.hedera.node.app.service.addressbook.impl.ReadableNodeStoreImpl; import com.hedera.node.app.service.consensus.impl.ConsensusServiceImpl; import com.hedera.node.app.service.contract.impl.ContractServiceImpl; import com.hedera.node.app.service.file.FileService; @@ -391,8 +394,10 @@ private State genesisState(@NonNull final Map overrides) { NO_OP_METRICS, startupNetworks); final var writableStates = state.getWritableStates(FileService.NAME); + final var readableStates = state.getReadableStates(AddressBookService.NAME); + final var nodeStore = new ReadableNodeStoreImpl(readableStates); final var files = writableStates.get(V0490FileSchema.BLOBS_KEY); - genesisContentProviders(networkInfo, config).forEach((fileNum, provider) -> { + genesisContentProviders(nodeStore, config).forEach((fileNum, provider) -> { final var fileId = createFileID(fileNum, config); files.put( fileId, @@ -407,12 +412,12 @@ private State genesisState(@NonNull final Map overrides) { } private Map> genesisContentProviders( - @NonNull final NetworkInfo networkInfo, @NonNull final Configuration config) { + @NonNull final ReadableNodeStore nodeStore, @NonNull final Configuration config) { final var genesisSchema = new V0490FileSchema(); final var filesConfig = config.getConfigData(FilesConfig.class); return Map.of( - filesConfig.addressBook(), ignore -> genesisSchema.genesisAddressBook(networkInfo), - filesConfig.nodeDetails(), ignore -> genesisSchema.genesisNodeDetails(networkInfo), + filesConfig.addressBook(), ignore -> genesisSchema.nodeStoreAddressBook(nodeStore), + filesConfig.nodeDetails(), ignore -> genesisSchema.nodeStoreNodeDetails(nodeStore), filesConfig.feeSchedules(), genesisSchema::genesisFeeSchedules, filesConfig.exchangeRates(), genesisSchema::genesisExchangeRates, filesConfig.networkProperties(), genesisSchema::genesisNetworkProperties, diff --git a/hedera-node/hedera-file-service-impl/src/main/java/com/hedera/node/app/service/file/impl/FileServiceImpl.java b/hedera-node/hedera-file-service-impl/src/main/java/com/hedera/node/app/service/file/impl/FileServiceImpl.java index 7c89fb23587a..cc5e766298f8 100644 --- a/hedera-node/hedera-file-service-impl/src/main/java/com/hedera/node/app/service/file/impl/FileServiceImpl.java +++ b/hedera-node/hedera-file-service-impl/src/main/java/com/hedera/node/app/service/file/impl/FileServiceImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2024 Hedera Hashgraph, LLC + * Copyright (C) 2020-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. @@ -49,12 +49,13 @@ public void registerSchemas(@NonNull final SchemaRegistry registry) { } /** - * Creates the system files in the given genesis context. + * Creates the system files in the given genesis context and the nodeStore data. * * @param context the genesis context + * @param nodeStore the ReadableNodeStore */ - public void createSystemEntities(@NonNull final SystemContext context) { - fileSchema.createGenesisAddressBookAndNodeDetails(context); + public void createSystemEntities(@NonNull final SystemContext context, @NonNull final ReadableNodeStore nodeStore) { + fileSchema.createGenesisAddressBookAndNodeDetails(context, nodeStore); fileSchema.createGenesisFeeSchedule(context); fileSchema.createGenesisExchangeRate(context); fileSchema.createGenesisNetworkProperties(context); diff --git a/hedera-node/hedera-file-service-impl/src/main/java/com/hedera/node/app/service/file/impl/handlers/FileGetContentsHandler.java b/hedera-node/hedera-file-service-impl/src/main/java/com/hedera/node/app/service/file/impl/handlers/FileGetContentsHandler.java index ea0fc1852af1..edaff860f3b7 100644 --- a/hedera-node/hedera-file-service-impl/src/main/java/com/hedera/node/app/service/file/impl/handlers/FileGetContentsHandler.java +++ b/hedera-node/hedera-file-service-impl/src/main/java/com/hedera/node/app/service/file/impl/handlers/FileGetContentsHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2024 Hedera Hashgraph, LLC + * Copyright (C) 2022-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. @@ -34,6 +34,7 @@ import com.hedera.hapi.node.transaction.Response; import com.hedera.node.app.hapi.utils.CommonPbjConverters; import com.hedera.node.app.hapi.utils.fee.FileFeeBuilder; +import com.hedera.node.app.service.addressbook.ReadableNodeStore; import com.hedera.node.app.service.file.ReadableFileStore; import com.hedera.node.app.service.file.impl.base.FileQueryBase; import com.hedera.node.app.service.file.impl.schemas.V0490FileSchema; @@ -45,7 +46,6 @@ import com.hederahashgraph.api.proto.java.FeeData; import com.hederahashgraph.api.proto.java.ResponseType; import com.swirlds.config.api.Configuration; -import com.swirlds.state.lifecycle.info.NetworkInfo; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.util.Map; @@ -60,7 +60,6 @@ public class FileGetContentsHandler extends FileQueryBase { private final FileFeeBuilder usageEstimator; private final V0490FileSchema genesisSchema; - private final NetworkInfo networkInfo; /** * Constructs a {@link FileGetContentsHandler} with the given {@link FileFeeBuilder}. @@ -68,12 +67,9 @@ public class FileGetContentsHandler extends FileQueryBase { */ @Inject public FileGetContentsHandler( - @NonNull final FileFeeBuilder usageEstimator, - @NonNull final V0490FileSchema genesisSchema, - @NonNull final NetworkInfo networkInfo) { + @NonNull final FileFeeBuilder usageEstimator, @NonNull final V0490FileSchema genesisSchema) { this.usageEstimator = requireNonNull(usageEstimator); this.genesisSchema = requireNonNull(genesisSchema); - this.networkInfo = networkInfo; } @Override @@ -102,10 +98,11 @@ public void validate(@NonNull final QueryContext context) throws PreCheckExcepti public @NonNull Fees computeFees(@NonNull QueryContext queryContext) { final var query = queryContext.query(); final var fileStore = queryContext.createStore(ReadableFileStore.class); + final var nodeStore = queryContext.createStore(ReadableNodeStore.class); final var op = query.fileGetContentsOrThrow(); final var fileId = op.fileIDOrElse(FileID.DEFAULT); final var responseType = op.headerOrElse(QueryHeader.DEFAULT).responseType(); - final FileContents fileContents = contentFile(fileId, fileStore, queryContext.configuration()); + final FileContents fileContents = contentFile(fileId, fileStore, queryContext.configuration(), nodeStore); return queryContext .feeCalculator() .legacyCalculate(sigValueObj -> @@ -117,6 +114,7 @@ public void validate(@NonNull final QueryContext context) throws PreCheckExcepti requireNonNull(header); final var query = context.query(); final var fileStore = context.createStore(ReadableFileStore.class); + final var nodeStore = context.createStore(ReadableNodeStore.class); final var op = query.fileGetContentsOrThrow(); final var responseBuilder = FileGetContentsResponse.newBuilder(); final var fileId = op.fileIDOrThrow(); @@ -124,7 +122,7 @@ public void validate(@NonNull final QueryContext context) throws PreCheckExcepti final var responseType = op.headerOrElse(QueryHeader.DEFAULT).responseType(); responseBuilder.header(header); if (header.nodeTransactionPrecheckCode() == OK && responseType != COST_ANSWER) { - final var content = contentFile(fileId, fileStore, context.configuration()); + final var content = contentFile(fileId, fileStore, context.configuration(), nodeStore); if (content == null) { responseBuilder.header(header.copyBuilder() .nodeTransactionPrecheckCode(INVALID_FILE_ID) @@ -143,18 +141,20 @@ public void validate(@NonNull final QueryContext context) throws PreCheckExcepti * @param fileID the file to get information about * @param fileStore the file store * @param config the configuration + * @param nodeStore the ReadableNodeStore * @return the content about the file */ private @Nullable FileContents contentFile( @NonNull final FileID fileID, @NonNull final ReadableFileStore fileStore, - @NonNull final Configuration config) { + @NonNull final Configuration config, + @NonNull final ReadableNodeStore nodeStore) { final var meta = fileStore.getFileMetadata(fileID); if (meta == null) { if (notGenesisCreation(fileID, config)) { return null; } else { - final var genesisContent = genesisContentProviders(config) + final var genesisContent = genesisContentProviders(config, nodeStore) .getOrDefault(fileID.fileNum(), ignore -> EMPTY) .apply(config); return new FileContents(fileID, genesisContent); @@ -167,11 +167,12 @@ public void validate(@NonNull final QueryContext context) throws PreCheckExcepti } } - private Map> genesisContentProviders(@NonNull final Configuration config) { + private Map> genesisContentProviders( + @NonNull final Configuration config, @NonNull final ReadableNodeStore nodeStore) { final var filesConfig = config.getConfigData(FilesConfig.class); return Map.of( - filesConfig.addressBook(), ignore -> genesisSchema.genesisAddressBook(networkInfo), - filesConfig.nodeDetails(), ignore -> genesisSchema.genesisNodeDetails(networkInfo), + filesConfig.addressBook(), ignore -> genesisSchema.nodeStoreAddressBook(nodeStore), + filesConfig.nodeDetails(), ignore -> genesisSchema.nodeStoreNodeDetails(nodeStore), filesConfig.feeSchedules(), genesisSchema::genesisFeeSchedules, filesConfig.exchangeRates(), genesisSchema::genesisExchangeRates, filesConfig.networkProperties(), genesisSchema::genesisNetworkProperties, diff --git a/hedera-node/hedera-file-service-impl/src/main/java/com/hedera/node/app/service/file/impl/schemas/V0490FileSchema.java b/hedera-node/hedera-file-service-impl/src/main/java/com/hedera/node/app/service/file/impl/schemas/V0490FileSchema.java index 79c8eb03a354..b0cdd7bdc6d7 100644 --- a/hedera-node/hedera-file-service-impl/src/main/java/com/hedera/node/app/service/file/impl/schemas/V0490FileSchema.java +++ b/hedera-node/hedera-file-service-impl/src/main/java/com/hedera/node/app/service/file/impl/schemas/V0490FileSchema.java @@ -51,7 +51,6 @@ import com.hedera.hapi.node.transaction.ThrottleDefinitions; import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.node.app.service.addressbook.ReadableNodeStore; -import com.hedera.node.app.service.addressbook.impl.schemas.V053AddressBookSchema; import com.hedera.node.app.spi.workflows.SystemContext; import com.hedera.node.config.ConfigProvider; import com.hedera.node.config.data.BootstrapConfig; @@ -64,8 +63,6 @@ import com.swirlds.state.lifecycle.MigrationContext; import com.swirlds.state.lifecycle.Schema; import com.swirlds.state.lifecycle.StateDefinition; -import com.swirlds.state.lifecycle.info.NetworkInfo; -import com.swirlds.state.lifecycle.info.NodeInfo; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.io.ByteArrayInputStream; @@ -166,9 +163,9 @@ public void migrate(@NonNull final MigrationContext ctx) { // ================================================================================================================ // Creates and loads the Address Book into state - public void createGenesisAddressBookAndNodeDetails(@NonNull final SystemContext systemContext) { + public void createGenesisAddressBookAndNodeDetails( + @NonNull final SystemContext systemContext, @NonNull final ReadableNodeStore nodeStore) { requireNonNull(systemContext); - final var networkInfo = systemContext.networkInfo(); final var filesConfig = systemContext.configuration().getConfigData(FilesConfig.class); final var bootstrapConfig = systemContext.configuration().getConfigData(BootstrapConfig.class); @@ -184,7 +181,7 @@ public void createGenesisAddressBookAndNodeDetails(@NonNull final SystemContext systemContext.dispatchCreation( TransactionBody.newBuilder() .fileCreate(FileCreateTransactionBody.newBuilder() - .contents(genesisAddressBook(networkInfo)) + .contents(nodeStoreAddressBook(nodeStore)) .keys(masterKey) .expirationTime(maxLifetimeExpiry(systemContext)) .build()) @@ -195,7 +192,7 @@ public void createGenesisAddressBookAndNodeDetails(@NonNull final SystemContext systemContext.dispatchCreation( TransactionBody.newBuilder() .fileCreate(FileCreateTransactionBody.newBuilder() - .contents(genesisNodeDetails(networkInfo)) + .contents(nodeStoreNodeDetails(nodeStore)) .keys(masterKey) .expirationTime(maxLifetimeExpiry(systemContext)) .build()) @@ -203,36 +200,6 @@ public void createGenesisAddressBookAndNodeDetails(@NonNull final SystemContext nodeInfoFileNum); } - public Bytes genesisAddressBook(@NonNull final NetworkInfo networkInfo) { - final var nodeAddresses = networkInfo.addressBook().stream() - .sorted(Comparator.comparingLong(NodeInfo::nodeId)) - .map(nodeInfo -> NodeAddress.newBuilder() - .nodeId(nodeInfo.nodeId()) - .nodeAccountId(nodeInfo.accountId()) - .rsaPubKey(nodeInfo.hexEncodedPublicKey()) - .serviceEndpoint(V053AddressBookSchema.endpointFor("1.0.0.0", 1)) - .build()) - .toList(); - return NodeAddressBook.PROTOBUF.toBytes( - NodeAddressBook.newBuilder().nodeAddress(nodeAddresses).build()); - } - - public Bytes genesisNodeDetails(@NonNull final NetworkInfo networkInfo) { - final var nodeDetails = networkInfo.addressBook().stream() - .sorted(Comparator.comparingLong(NodeInfo::nodeId)) - .map(nodeInfo -> NodeAddress.newBuilder() - .stake(nodeInfo.weight()) - .nodeAccountId(nodeInfo.accountId()) - .nodeId(nodeInfo.nodeId()) - .rsaPubKey(nodeInfo.hexEncodedPublicKey()) - .serviceEndpoint(V053AddressBookSchema.endpointFor("1.0.0.0", 1)) - .build()) - .toList(); - - return NodeAddressBook.PROTOBUF.toBytes( - NodeAddressBook.newBuilder().nodeAddress(nodeDetails).build()); - } - public void updateAddressBookAndNodeDetailsAfterFreeze( @NonNull final SystemContext systemContext, @NonNull final ReadableNodeStore nodeStore) { requireNonNull(systemContext); @@ -262,7 +229,7 @@ public static void dispatchSynthFileUpdate( .build())); } - private Bytes nodeStoreNodeDetails(@NonNull final ReadableNodeStore nodeStore) { + public Bytes nodeStoreNodeDetails(@NonNull final ReadableNodeStore nodeStore) { final var nodeDetails = new ArrayList(); StreamSupport.stream(Spliterators.spliterator(nodeStore.keys(), nodeStore.sizeOfState(), DISTINCT), false) .mapToLong(EntityNumber::number) @@ -288,7 +255,7 @@ private Bytes getHexStringBytesFromBytes(final Bytes rawBytes) { return Bytes.wrap(Normalizer.normalize(hexString, Normalizer.Form.NFD).getBytes(UTF_8)); } - private Bytes nodeStoreAddressBook(@NonNull final ReadableNodeStore nodeStore) { + public Bytes nodeStoreAddressBook(@NonNull final ReadableNodeStore nodeStore) { final var nodeAddresses = new ArrayList(); StreamSupport.stream(Spliterators.spliterator(nodeStore.keys(), nodeStore.sizeOfState(), DISTINCT), false) .mapToLong(EntityNumber::number) diff --git a/hedera-node/hedera-file-service-impl/src/main/java/module-info.java b/hedera-node/hedera-file-service-impl/src/main/java/module-info.java index 1e22d589f4c9..31058e857c4f 100644 --- a/hedera-node/hedera-file-service-impl/src/main/java/module-info.java +++ b/hedera-node/hedera-file-service-impl/src/main/java/module-info.java @@ -6,13 +6,12 @@ requires transitive com.hedera.node.app.spi; requires transitive com.hedera.node.config; requires transitive com.hedera.node.hapi; + requires transitive com.hedera.pbj.runtime; requires transitive com.swirlds.config.api; requires transitive com.swirlds.state.api; - requires transitive com.hedera.pbj.runtime; requires transitive dagger; - requires transitive static java.compiler; // javax.annotation.processing.Generated + requires transitive java.compiler; // javax.annotation.processing.Generated requires transitive javax.inject; - requires com.hedera.node.app.service.addressbook.impl; requires com.swirlds.common; requires com.fasterxml.jackson.databind; requires com.google.protobuf; diff --git a/hedera-node/hedera-file-service-impl/src/test/java/com/hedera/node/app/service/file/impl/test/handlers/FileGetContentsHandlerTest.java b/hedera-node/hedera-file-service-impl/src/test/java/com/hedera/node/app/service/file/impl/test/handlers/FileGetContentsHandlerTest.java index 707e200a912b..a43e449a588f 100644 --- a/hedera-node/hedera-file-service-impl/src/test/java/com/hedera/node/app/service/file/impl/test/handlers/FileGetContentsHandlerTest.java +++ b/hedera-node/hedera-file-service-impl/src/test/java/com/hedera/node/app/service/file/impl/test/handlers/FileGetContentsHandlerTest.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. @@ -49,7 +49,6 @@ import com.hedera.node.app.spi.workflows.QueryContext; import com.hedera.node.config.data.FilesConfig; import com.hedera.pbj.runtime.io.buffer.Bytes; -import com.swirlds.state.lifecycle.info.NetworkInfo; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -68,14 +67,11 @@ class FileGetContentsHandlerTest extends FileTestBase { @Mock private V0490FileSchema genesisSchema; - @Mock - private NetworkInfo networkInfo; - private FileGetContentsHandler subject; @BeforeEach void setUp() { - subject = new FileGetContentsHandler(usageEstimator, genesisSchema, networkInfo); + subject = new FileGetContentsHandler(usageEstimator, genesisSchema); } @Test diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/grouping/GroupingVerbs.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/grouping/GroupingVerbs.java index 69110fcc27bf..abf871cd98d0 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/grouping/GroupingVerbs.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/grouping/GroupingVerbs.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. @@ -44,4 +44,19 @@ private GroupingVerbs() { public static SysFileLookups getSystemFiles(@NonNull final Consumer> observer) { return new SysFileLookups(fileNum -> true, observer); } + + /** + * Returns a utility operation to retrieve the contents of specific system files and pass them to an observer. + * + * @param sysfileNub the system file number + * @param observer the consumer of the system file contents + * @return the utility operation + */ + public static SysFileLookups getSystemFiles(final long sysfileNub, @NonNull final Consumer observer) { + final Consumer> temp = map -> { + final Bytes contents = map.get(new FileID(0, 0, sysfileNub)); + observer.accept(contents); + }; + return new SysFileLookups(fileNum -> fileNum == sysfileNub, temp); + } } 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 222e3f7fd42d..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 @@ -175,6 +175,40 @@ final Stream syntheticAddressBookUpdateHappensAtUpgradeBoundary() { cryptoCreate("secondUser").via("addressBookExport")); } + @GenesisHapiTest + final Stream syntheticAddressBookCreatedAtGenesis() { + final AtomicReference addressBookContent = new AtomicReference<>(); + return hapiTest( + recordStreamMustIncludeNoFailuresFrom(visibleItems( + validatorSpecificSysFileFor(addressBookContent, "files.addressBook", "genesisTxn"), + "genesisTxn")), + sourcingContextual(spec -> + getSystemFiles(spec.startupProperties().getLong("files.addressBook"), addressBookContent::set)), + cryptoCreate("firstUser").via("genesisTxn"), + // Assert the first created entity still has the expected number + withOpContext((spec, opLog) -> assertEquals( + spec.startupProperties().getLong("hedera.firstUserEntity"), + spec.registry().getAccountID("firstUser").getAccountNum(), + "First user entity num doesn't match config"))); + } + + @GenesisHapiTest + final Stream syntheticNodeDetailsCreatedAtGenesis() { + final AtomicReference addressBookContent = new AtomicReference<>(); + return hapiTest( + recordStreamMustIncludeNoFailuresFrom(visibleItems( + validatorSpecificSysFileFor(addressBookContent, "files.nodeDetails", "genesisTxn"), + "genesisTxn")), + sourcingContextual(spec -> + getSystemFiles(spec.startupProperties().getLong("files.nodeDetails"), addressBookContent::set)), + cryptoCreate("firstUser").via("genesisTxn"), + // Assert the first created entity still has the expected number + withOpContext((spec, opLog) -> assertEquals( + spec.startupProperties().getLong("hedera.firstUserEntity"), + spec.registry().getAccountID("firstUser").getAccountNum(), + "First user entity num doesn't match config"))); + } + @GenesisHapiTest final Stream syntheticFeeSchedulesUpdateHappensAtUpgradeBoundary() throws InvalidProtocolBufferException { @@ -569,21 +603,85 @@ private static void validateSystemFileExports( assertEquals(Map.of(SUCCESS, 1), histogram.get(NodeStakeUpdate)); final var postGenesisContents = SysFileLookups.getSystemFileContents(spec, fileNum -> true); items.entries().stream().filter(item -> item.function() == FileCreate).forEach(item -> { + final var fileId = item.createdFileId(); final var preContents = requireNonNull( - preGenesisContents.get(item.createdFileId()), - "No pre-genesis contents for " + item.createdFileId()); + preGenesisContents.get(item.createdFileId()), "No pre-genesis contents for " + fileId); final var postContents = requireNonNull( - postGenesisContents.get(item.createdFileId()), - "No post-genesis contents for " + item.createdFileId()); + postGenesisContents.get(item.createdFileId()), "No post-genesis contents for " + fileId); final var exportedContents = fromByteString(item.body().getFileCreate().getContents()); - assertEquals( - exportedContents, preContents, item.createdFileId() + " contents don't match pre-genesis query"); - assertEquals( - exportedContents, postContents, item.createdFileId() + " contents don't match post-genesis query"); + if (fileId.fileNum() + != 102) { // for nodedetail, the node's weight changed between preContent and exportedContents + assertEquals(exportedContents, preContents, fileId + " contents don't match pre-genesis query"); + } + assertEquals(exportedContents, postContents, fileId + " contents don't match post-genesis query"); }); } + private static VisibleItemsValidator validatorSpecificSysFileFor( + @NonNull final AtomicReference fileContent, + @NonNull final String fileNumProperty, + @NonNull final String specTxnIds) { + return (spec, records) -> + specificSysFileValidator(spec, records, fileContent.get(), fileNumProperty, specTxnIds); + } + + private static void specificSysFileValidator( + @NonNull final HapiSpec spec, + @NonNull final Map genesisRecords, + @NonNull final Bytes fileContent, + @NonNull final String fileNumProperty, + @NonNull final String specTxnIds) { + final var items = requireNonNull(genesisRecords.get(specTxnIds)); + final long fileNumb = spec.startupProperties().getLong(fileNumProperty); + final var histogram = statusHistograms(items.entries()); + final var systemFileNums = + SysFileLookups.allSystemFileNums(spec).boxed().toList(); + assertEquals(Map.of(SUCCESS, systemFileNums.size()), histogram.get(FileCreate)); + // Also check we export a node stake update at genesis + assertEquals(Map.of(SUCCESS, 1), histogram.get(NodeStakeUpdate)); + final var fileItem = items.entries().stream() + .filter(item -> item.function() == FileCreate) + .filter(item -> item.createdFileId().equals(new FileID(0, 0, fileNumb))) + .findFirst() + .orElse(null); + + assertNotNull(fileItem, "No create item for " + fileNumProperty + " found in " + specTxnIds + " txn"); + final var fileCreateContents = fileItem.body().getFileCreate().getContents(); + assertNotNull( + fileCreateContents, "No create content for " + fileNumProperty + " found in " + specTxnIds + " txn"); + if (fileNumProperty.equals("files.nodeDetails")) { + try { + final var addressBook = NodeAddressBook.PROTOBUF.parse(fileContent); + final var updatedAddressBook = + NodeAddressBook.PROTOBUF.parse(Bytes.wrap(fileCreateContents.toByteArray())); + assertEquals( + addressBook.nodeAddress().size(), + updatedAddressBook.nodeAddress().size(), + "address book size mismatch"); + + for (int i = 0; + i < addressBook.nodeAddress().size(); + i++) { // only stake not matching because of recalculating + final var address = updatedAddressBook.nodeAddress().get(i); + final var updatedAddress = updatedAddressBook.nodeAddress().get(i); + assertEquals(address.nodeId(), updatedAddress.nodeId(), "nodeId mismatch"); + assertEquals(address.nodeAccountId(), updatedAddress.nodeAccountId(), "nodeAccountId mismatch"); + assertEquals(address.nodeCertHash(), updatedAddress.nodeCertHash(), "nodeCertHash mismatch"); + assertEquals(address.description(), updatedAddress.description(), "description mismatch"); + assertEquals(address.rsaPubKey(), updatedAddress.rsaPubKey(), "rsaPubKey mismatch"); + assertEquals( + address.serviceEndpoint(), updatedAddress.serviceEndpoint(), "serviceEndpoint mismatch"); + } + } catch (ParseException e) { + Assertions.fail("Update contents was not protobuf " + e.getMessage()); + } + } else { + assertEquals( + fileContent, fromByteString(fileCreateContents), fileNumb + " contents don't match genesis query"); + } + } + private static Map generateCertificates(final int n) { final var randomAddressBook = RandomAddressBookBuilder.create(new Random()) .withSize(n) From 650d911d35ac70d0952b7a16b998b3c59e2f7b98 Mon Sep 17 00:00:00 2001 From: Edward Wertz <123979964+edward-swirldslabs@users.noreply.github.com> Date: Mon, 13 Jan 2025 11:10:35 -0600 Subject: [PATCH 02/19] refactor: move init logging to start of process (#17355) Signed-off-by: Edward Wertz --- .../com/hedera/node/app/ServicesMain.java | 18 ++-------- .../java/com/swirlds/platform/Browser.java | 13 ++------ .../internal/StaticPlatformBuilder.java | 33 ++++++++++--------- 3 files changed, 21 insertions(+), 43 deletions(-) diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/ServicesMain.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/ServicesMain.java index 0b9662a77b14..a7cfb6a1090e 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/ServicesMain.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/ServicesMain.java @@ -24,8 +24,8 @@ import static com.swirlds.platform.builder.PlatformBuildConstants.DEFAULT_CONFIG_FILE_NAME; import static com.swirlds.platform.builder.PlatformBuildConstants.DEFAULT_OVERRIDES_YAML_FILE_NAME; import static com.swirlds.platform.builder.PlatformBuildConstants.DEFAULT_SETTINGS_FILE_NAME; -import static com.swirlds.platform.builder.PlatformBuildConstants.LOG4J_FILE_NAME; import static com.swirlds.platform.builder.internal.StaticPlatformBuilder.getMetricsProvider; +import static com.swirlds.platform.builder.internal.StaticPlatformBuilder.initLogging; import static com.swirlds.platform.builder.internal.StaticPlatformBuilder.setupGlobalMetrics; import static com.swirlds.platform.config.internal.PlatformConfigUtils.checkConfiguration; import static com.swirlds.platform.crypto.CryptoStatic.initNodeSecurity; @@ -57,7 +57,6 @@ import com.swirlds.common.merkle.crypto.MerkleCryptoFactory; import com.swirlds.common.merkle.crypto.MerkleCryptographyFactory; import com.swirlds.common.platform.NodeId; -import com.swirlds.common.startup.Log4jSetup; import com.swirlds.config.api.Configuration; import com.swirlds.config.api.ConfigurationBuilder; import com.swirlds.config.extensions.sources.SystemEnvironmentConfigSource; @@ -218,6 +217,7 @@ public void run() { * @param args optionally, what node id to run; required if the address book is ambiguous */ public static void main(final String... args) throws Exception { + initLogging(); // --- Configure platform infrastructure and context from the command line and environment --- BootstrapUtils.setupConstructableRegistry(); final var diskAddressBook = loadAddressBook(DEFAULT_CONFIG_FILE_NAME); @@ -269,8 +269,6 @@ public static void main(final String... args) throws Exception { hedera = newHedera(selfId, metrics); final var version = hedera.getSoftwareVersion(); final var isGenesis = new AtomicBoolean(false); - // We want to be able to see the schema migration logs, so init logging here - initLogging(); logger.info("Starting node {} with version {}", selfId, version); final var reservedState = loadInitialState( platformConfig, @@ -348,18 +346,6 @@ private static String canonicalEventStreamLoc(final long nodeId, @NonNull final return accountId.shardNum() + "." + accountId.realmNum() + "." + accountId.accountNumOrThrow(); } - private static void initLogging() { - final var log4jPath = getAbsolutePath(LOG4J_FILE_NAME); - try { - Log4jSetup.startLoggingFramework(log4jPath).await(); - } catch (final InterruptedException e) { - // since the logging framework has not been instantiated, also log to stderr - e.printStackTrace(); - Thread.currentThread().interrupt(); - throw new RuntimeException("Interrupted while waiting for log4j to initialize", e); - } - } - /** * Creates a canonical {@link Hedera} instance for the given node id and metrics. * diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/Browser.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/Browser.java index 1728d7d67b6f..0bf08b6b6ed7 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/Browser.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/Browser.java @@ -24,8 +24,8 @@ import static com.swirlds.platform.builder.PlatformBuildConstants.DEFAULT_CONFIG_FILE_NAME; import static com.swirlds.platform.builder.PlatformBuildConstants.DEFAULT_OVERRIDES_YAML_FILE_NAME; import static com.swirlds.platform.builder.PlatformBuildConstants.DEFAULT_SETTINGS_FILE_NAME; -import static com.swirlds.platform.builder.PlatformBuildConstants.LOG4J_FILE_NAME; import static com.swirlds.platform.builder.internal.StaticPlatformBuilder.getMetricsProvider; +import static com.swirlds.platform.builder.internal.StaticPlatformBuilder.initLogging; import static com.swirlds.platform.builder.internal.StaticPlatformBuilder.setupGlobalMetrics; import static com.swirlds.platform.crypto.CryptoStatic.initNodeSecurity; import static com.swirlds.platform.gui.internal.BrowserWindowManager.addPlatforms; @@ -50,9 +50,7 @@ import com.swirlds.common.merkle.crypto.MerkleCryptoFactory; import com.swirlds.common.merkle.crypto.MerkleCryptographyFactory; import com.swirlds.common.platform.NodeId; -import com.swirlds.common.startup.Log4jSetup; import com.swirlds.common.threading.framework.config.ThreadConfiguration; -import com.swirlds.common.utility.CommonUtils; import com.swirlds.config.api.Configuration; import com.swirlds.config.api.ConfigurationBuilder; import com.swirlds.config.extensions.sources.SystemEnvironmentConfigSource; @@ -81,7 +79,6 @@ import com.swirlds.state.State; import edu.umd.cs.findbugs.annotations.NonNull; import java.awt.GraphicsEnvironment; -import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -153,13 +150,7 @@ public static void launch(@NonNull final CommandLineArgs commandLineArgs, final return; } - final Path log4jPath = getAbsolutePath(LOG4J_FILE_NAME); - try { - Log4jSetup.startLoggingFramework(log4jPath).await(); - } catch (final InterruptedException e) { - CommonUtils.tellUserConsole("Interrupted while waiting for log4j to initialize"); - Thread.currentThread().interrupt(); - } + initLogging(); logger = LogManager.getLogger(Browser.class); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/internal/StaticPlatformBuilder.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/internal/StaticPlatformBuilder.java index 0dbfa7936cf2..3eff317deb31 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/internal/StaticPlatformBuilder.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/internal/StaticPlatformBuilder.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. @@ -63,6 +63,22 @@ public final class StaticPlatformBuilder { private StaticPlatformBuilder() {} + public static void initLogging() { + final var log4jPath = getAbsolutePath(LOG4J_FILE_NAME); + try { + Log4jSetup.startLoggingFramework(log4jPath).await(); + + // Now that we have a logger, we can start using it for further messages + logger.info(STARTUP.getMarker(), "\n\n" + STARTUP_MESSAGE + "\n"); + logger.debug(STARTUP.getMarker(), () -> new NodeStartPayload().toString()); + } catch (final InterruptedException e) { + // since the logging framework has not been instantiated, also log to stderr + e.printStackTrace(); + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted while waiting for log4j to initialize", e); + } + } + /** * Setup global metrics. * @@ -91,21 +107,6 @@ public static boolean doStaticSetup(@NonNull final Configuration configuration, } staticSetupCompleted = true; - // Setup logging - final Path log4jPath = getAbsolutePath(LOG4J_FILE_NAME); - try { - Log4jSetup.startLoggingFramework(log4jPath).await(); - } catch (final InterruptedException e) { - // since the logging framework has not been instantiated, also log to stderr - e.printStackTrace(); - Thread.currentThread().interrupt(); - throw new RuntimeException("Interrupted while waiting for log4j to initialize", e); - } - - // Now that we have a logger, we can start using it for further messages - logger.info(STARTUP.getMarker(), "\n\n" + STARTUP_MESSAGE + "\n"); - logger.debug(STARTUP.getMarker(), () -> new NodeStartPayload().toString()); - BootstrapUtils.performHealthChecks(configPath, configuration); writeSettingsUsed(configuration); From 3aac5953366e59eed5ae07b0b00b382d727d779b Mon Sep 17 00:00:00 2001 From: Maxi Tartaglia <152629744+mxtartaglia-sl@users.noreply.github.com> Date: Mon, 13 Jan 2025 16:27:53 -0300 Subject: [PATCH 03/19] chore: Remove TssKeys from CryptoStatic (#17351) Signed-off-by: mxtartaglia --- .../swirlds/platform/crypto/CryptoStatic.java | 22 ------------------- .../swirlds/platform/crypto/KeysAndCerts.java | 11 ---------- .../src/main/java/module-info.java | 2 -- 3 files changed, 35 deletions(-) diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/crypto/CryptoStatic.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/crypto/CryptoStatic.java index dba52b921671..9160fe0b271c 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/crypto/CryptoStatic.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/crypto/CryptoStatic.java @@ -24,10 +24,6 @@ import static com.swirlds.platform.crypto.CryptoConstants.PUBLIC_KEYS_FILE; import static com.swirlds.platform.crypto.KeyCertPurpose.SIGNING; -import com.hedera.cryptography.bls.BlsKeyPair; -import com.hedera.cryptography.bls.GroupAssignment; -import com.hedera.cryptography.bls.SignatureSchema; -import com.hedera.cryptography.pairings.api.Curve; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.common.crypto.CryptographyException; import com.swirlds.common.crypto.config.CryptoConfig; @@ -97,8 +93,6 @@ * A collection of various static crypto methods */ public final class CryptoStatic { - public static final SignatureSchema SIGNATURE_SCHEMA = - SignatureSchema.create(Curve.ALT_BN128, GroupAssignment.SHORT_SIGNATURES); private static final Logger logger = LogManager.getLogger(CryptoStatic.class); private static final int SERIAL_NUMBER_BITS = 64; private static final int MASTER_KEY_MULTIPLIER = 157; @@ -643,22 +637,6 @@ public static Map initNodeSecurity( return store; } - /** - * Generate a {@link BlsKeyPair} using a {@link SignatureSchema} and a {@link SecureRandom} instance - * @param secureRandom the secure random number generator to use - * @return a new {@link BlsKeyPair} - * @throws NoSuchAlgorithmException the algorithm is not supported - */ - public static BlsKeyPair generateBlsKeyPair(@Nullable final SecureRandom secureRandom) - throws NoSuchAlgorithmException { - if (secureRandom == null) { - logger.debug("Generating a new BLS key pair using a default secure random number generator"); - return BlsKeyPair.generate(SIGNATURE_SCHEMA); - } - logger.debug("Generating a new BLS key pair using a custom secure random number generator"); - return BlsKeyPair.generate(SIGNATURE_SCHEMA, secureRandom); - } - /** * Check if a certificate is valid. A certificate is valid if it is not null, has a public key, and can be encoded. * diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/crypto/KeysAndCerts.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/crypto/KeysAndCerts.java index e7cb79ef453f..d8270e9fdec6 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/crypto/KeysAndCerts.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/crypto/KeysAndCerts.java @@ -16,7 +16,6 @@ package com.swirlds.platform.crypto; -import com.hedera.cryptography.bls.BlsKeyPair; import com.swirlds.common.crypto.internal.CryptoUtils; import edu.umd.cs.findbugs.annotations.NonNull; import java.security.Key; @@ -63,7 +62,6 @@ public record KeysAndCerts( PublicStores publicStores) { private static final int SIG_SEED = 2; private static final int AGR_SEED = 0; - private static final int TSS_ENCRYPTION_KEY_SEED = 3; /** * Creates an instance holding all the keys and certificates. It reads its own key pairs from privateKeyStore @@ -154,14 +152,12 @@ public static KeysAndCerts generate( final SecureRandom sigDetRandom; // deterministic CSPRNG, used briefly then discarded final SecureRandom agrDetRandom; // deterministic CSPRNG, used briefly then discarded - final SecureRandom tssEncryptionKeyRandom; sigKeyGen = KeyPairGenerator.getInstance(CryptoConstants.SIG_TYPE1, CryptoConstants.SIG_PROVIDER); agrKeyGen = KeyPairGenerator.getInstance(CryptoConstants.AGR_TYPE, CryptoConstants.AGR_PROVIDER); sigDetRandom = CryptoUtils.getDetRandom(); // deterministic, not shared agrDetRandom = CryptoUtils.getDetRandom(); // deterministic, not shared - tssEncryptionKeyRandom = CryptoUtils.getDetRandom(); // deterministic, not shared sigDetRandom.setSeed(masterKey); sigDetRandom.setSeed(swirldId); @@ -175,11 +171,6 @@ public static KeysAndCerts generate( agrDetRandom.setSeed(AGR_SEED); agrKeyGen.initialize(CryptoConstants.AGR_KEY_SIZE_BITS, agrDetRandom); - tssEncryptionKeyRandom.setSeed(masterKey); - tssEncryptionKeyRandom.setSeed(swirldId); - tssEncryptionKeyRandom.setSeed(memberId); - tssEncryptionKeyRandom.setSeed(TSS_ENCRYPTION_KEY_SEED); - final KeyPair sigKeyPair = sigKeyGen.generateKeyPair(); final KeyPair agrKeyPair = agrKeyGen.generateKeyPair(); @@ -197,8 +188,6 @@ public static KeysAndCerts generate( publicStores.setCertificate(KeyCertPurpose.SIGNING, sigCert, name); publicStores.setCertificate(KeyCertPurpose.AGREEMENT, agrCert, name); - final BlsKeyPair blsKeyPair = CryptoStatic.generateBlsKeyPair(tssEncryptionKeyRandom); - return new KeysAndCerts(sigKeyPair, agrKeyPair, sigCert, agrCert, publicStores); } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/module-info.java b/platform-sdk/swirlds-platform-core/src/main/java/module-info.java index a077a1836db9..b1e6a8cb2b05 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/module-info.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/module-info.java @@ -126,7 +126,6 @@ exports com.swirlds.platform.config.internal; exports com.swirlds.platform.roster; - requires transitive com.hedera.cryptography.bls; requires transitive com.hedera.node.hapi; requires transitive com.hedera.pbj.runtime; requires transitive com.swirlds.base; @@ -142,7 +141,6 @@ requires transitive com.fasterxml.jackson.databind; requires transitive info.picocli; requires transitive org.apache.logging.log4j; - requires com.hedera.cryptography.pairings.api; requires com.swirlds.config.extensions; requires com.swirlds.logging; requires com.swirlds.merkledb; From 1360988698c8ef10f474b356728c39465290afbe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 15:30:15 -0600 Subject: [PATCH 04/19] build(deps): bump actions/upload-artifact from 4.5.0 to 4.6.0 (#17306) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Roger Barker --- .../node-zxc-build-release-artifact.yaml | 2 +- .../node-zxc-compile-application-code.yaml | 38 +++++++++---------- .../zxc-verify-docker-build-determinism.yaml | 2 +- .../zxc-verify-gradle-build-determinism.yaml | 2 +- .../workflows/zxcron-extended-test-suite.yaml | 2 +- .../workflows/zxf-collect-workflow-logs.yaml | 2 +- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/.github/workflows/node-zxc-build-release-artifact.yaml b/.github/workflows/node-zxc-build-release-artifact.yaml index c264bd22aef4..2355966c996d 100644 --- a/.github/workflows/node-zxc-build-release-artifact.yaml +++ b/.github/workflows/node-zxc-build-release-artifact.yaml @@ -658,7 +658,7 @@ jobs: fi - name: Upload Manifests - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 if: ${{ (steps.gcp.outcome == 'success' || steps.jfrog.outcome == 'success') && !cancelled() && always() }} with: name: Production Image Manifests diff --git a/.github/workflows/node-zxc-compile-application-code.yaml b/.github/workflows/node-zxc-compile-application-code.yaml index 11b9c0d3b43a..d13c96792236 100644 --- a/.github/workflows/node-zxc-compile-application-code.yaml +++ b/.github/workflows/node-zxc-compile-application-code.yaml @@ -219,7 +219,7 @@ jobs: comment_mode: errors # only comment if we could not find or parse the JUnit XML files - name: Upload Unit Test Report Artifacts - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 if: ${{ inputs.enable-unit-tests && steps.gradle-build.conclusion == 'success' && !cancelled() }} with: name: Unit Test Report @@ -241,7 +241,7 @@ jobs: comment_mode: errors # only comment if we could not find or parse the JUnit XML files - name: Upload Unit Test (Timing Sensitive) Report Artifacts - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 if: ${{ inputs.enable-timing-sensitive-tests && steps.gradle-build.conclusion == 'success' && !cancelled() }} with: name: Unit Test Report (Timing Sensitive) @@ -263,7 +263,7 @@ jobs: comment_mode: errors # only comment if we could not find or parse the JUnit XML files - name: Upload Unit Test (Time Consuming) Report Artifacts - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 if: ${{ inputs.enable-time-consuming-tests && steps.gradle-build.conclusion == 'success' && !cancelled() }} with: name: Unit Test Report (Time Consuming) @@ -285,7 +285,7 @@ jobs: comment_mode: errors # only comment if we could not find or parse the JUnit XML files - name: Upload Hammer Test Report Artifacts - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 if: ${{ inputs.enable-hammer-tests && steps.gradle-build.conclusion == 'success' && !cancelled() }} with: name: Hammer Test Report @@ -311,7 +311,7 @@ jobs: comment_mode: errors # only comment if we could not find or parse the JUnit XML files - name: Upload HAPI Test (Misc) Report Artifacts - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 if: ${{ inputs.enable-hapi-tests-misc && steps.gradle-build.conclusion == 'success' && !cancelled() }} with: name: HAPI Test (Misc) Reports @@ -319,7 +319,7 @@ jobs: retention-days: 7 - name: Upload HAPI Test (Misc) Network Logs - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 if: ${{ inputs.enable-hapi-tests-misc && inputs.enable-network-log-capture && steps.gradle-hapi-misc.conclusion == 'failure' && !cancelled() }} with: name: HAPI Test (Misc) Network Logs @@ -345,7 +345,7 @@ jobs: comment_mode: errors # only comment if we could not find or parse the JUnit XML files - name: Upload HAPI Test (Crypto) Report Artifacts - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 if: ${{ inputs.enable-hapi-tests-crypto && steps.gradle-build.conclusion == 'success' && !cancelled() }} with: name: HAPI Test (Crypto) Report @@ -353,7 +353,7 @@ jobs: retention-days: 7 - name: Upload HAPI Test (crypto) Network Logs - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 if: ${{ inputs.enable-hapi-tests-crypto && inputs.enable-network-log-capture && steps.gradle-hapi-crypto.conclusion == 'failure' && !cancelled() }} with: name: HAPI Test (Crypto) Network Logs @@ -379,7 +379,7 @@ jobs: comment_mode: errors # only comment if we could not find or parse the JUnit XML files - name: Upload HAPI Test (Token) Report Artifacts - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 if: ${{ inputs.enable-hapi-tests-token && steps.gradle-build.conclusion == 'success' && !cancelled() }} with: name: HAPI Test (Token) Report @@ -387,7 +387,7 @@ jobs: retention-days: 7 - name: Upload HAPI Test (Token) Network Logs - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 if: ${{ inputs.enable-hapi-tests-token && inputs.enable-network-log-capture && steps.gradle-hapi-token.conclusion == 'failure' && !cancelled() }} with: name: HAPI Test (Token) Network Logs @@ -413,7 +413,7 @@ jobs: comment_mode: errors # only comment if we could not find or parse the JUnit XML files - name: Upload HAPI Test (Smart Contract) Report Artifacts - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 if: ${{ inputs.enable-hapi-tests-smart-contract && steps.gradle-build.conclusion == 'success' && !cancelled() }} with: name: HAPI Test (Smart Contract) Report @@ -421,7 +421,7 @@ jobs: retention-days: 7 - name: Upload HAPI Test (Smart Contract) Network Logs - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 if: ${{ inputs.enable-hapi-tests-smart-contract && inputs.enable-network-log-capture && steps.gradle-hapi-smart-contract.conclusion == 'failure' && !cancelled() }} with: name: HAPI Test (Smart Contract) Network Logs @@ -447,7 +447,7 @@ jobs: comment_mode: errors # only comment if we could not find or parse the JUnit XML files - name: Upload HAPI Test (Time Consuming) Report Artifacts - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 if: ${{ inputs.enable-hapi-tests-time-consuming && steps.gradle-build.conclusion == 'success' && !cancelled() }} with: name: HAPI Test (Time Consuming) Report @@ -455,7 +455,7 @@ jobs: retention-days: 7 - name: Upload HAPI Test (Time Consuming) Network Logs - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 if: ${{ inputs.enable-hapi-tests-time-consuming && inputs.enable-network-log-capture && steps.gradle-hapi-time-consuming.conclusion == 'failure' && !cancelled() }} with: name: HAPI Test (Time Consuming) Network Logs @@ -481,7 +481,7 @@ jobs: comment_mode: errors # only comment if we could not find or parse the JUnit XML files - name: Upload HAPI Test (Restart) Report Artifacts - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 if: ${{ inputs.enable-hapi-tests-restart && steps.gradle-build.conclusion == 'success' && !cancelled() }} with: name: HAPI Test (Restart) Report @@ -489,7 +489,7 @@ jobs: retention-days: 7 - name: Upload HAPI Test (Restart) Network Logs - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 if: ${{ inputs.enable-hapi-tests-restart && inputs.enable-network-log-capture && steps.gradle-hapi-restart.conclusion == 'failure' && !cancelled() }} with: name: HAPI Test (Restart) Network Logs @@ -516,7 +516,7 @@ jobs: comment_mode: errors # only comment if we could not find or parse the JUnit XML files - name: Upload HAPI Test (Node Death Reconnect) Report Artifacts - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 if: ${{ inputs.enable-hapi-tests-nd-reconnect && steps.gradle-build.conclusion == 'failure' && !cancelled() }} with: name: HAPI Test (Node Death Reconnect) Report @@ -524,7 +524,7 @@ jobs: retention-days: 7 - name: Upload HAPI Test (Node Death Reconnect) Network Logs - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 if: ${{ inputs.enable-hapi-tests-nd-reconnect && inputs.enable-network-log-capture && steps.gradle-hapi-nd-reconnect.conclusion == 'failure' && !cancelled() }} with: name: HAPI Test (Node Death Reconnect) Network Logs @@ -547,7 +547,7 @@ jobs: run: bash <(curl -Ls https://coverage.codacy.com/get.sh) report -l Java -r gradle/aggregation/build/reports/jacoco/testCodeCoverageReport/testCodeCoverageReport.xml - name: Upload Test Reports - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 if: ${{ inputs.enable-unit-tests && !cancelled() }} with: name: Test Reports diff --git a/.github/workflows/zxc-verify-docker-build-determinism.yaml b/.github/workflows/zxc-verify-docker-build-determinism.yaml index ea4d80088448..9c08c42518c7 100644 --- a/.github/workflows/zxc-verify-docker-build-determinism.yaml +++ b/.github/workflows/zxc-verify-docker-build-determinism.yaml @@ -499,7 +499,7 @@ jobs: fi - name: Publish Manifests - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 if: ${{ steps.regen-manifest.conclusion == 'success' && failure() && !cancelled() }} with: name: Docker Manifests [${{ join(matrix.os, ', ') }}] diff --git a/.github/workflows/zxc-verify-gradle-build-determinism.yaml b/.github/workflows/zxc-verify-gradle-build-determinism.yaml index a0b48595313f..925ff34964e5 100644 --- a/.github/workflows/zxc-verify-gradle-build-determinism.yaml +++ b/.github/workflows/zxc-verify-gradle-build-determinism.yaml @@ -247,7 +247,7 @@ jobs: fi - name: Publish Manifests - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 if: ${{ steps.regen-manifest.conclusion == 'success' && failure() && !cancelled() }} with: name: Gradle Manifests [${{ join(matrix.os, ', ') }}] diff --git a/.github/workflows/zxcron-extended-test-suite.yaml b/.github/workflows/zxcron-extended-test-suite.yaml index ec5cd7117d1f..94198750e670 100644 --- a/.github/workflows/zxcron-extended-test-suite.yaml +++ b/.github/workflows/zxcron-extended-test-suite.yaml @@ -281,7 +281,7 @@ jobs: done - name: Upload log as artifact - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: path: run.log diff --git a/.github/workflows/zxf-collect-workflow-logs.yaml b/.github/workflows/zxf-collect-workflow-logs.yaml index fa6f90ca73cc..f075dc7917cf 100644 --- a/.github/workflows/zxf-collect-workflow-logs.yaml +++ b/.github/workflows/zxf-collect-workflow-logs.yaml @@ -60,7 +60,7 @@ jobs: - name: Upload log as artifact id: upload-log - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: path: workflow-run.log From ce8b99af82cb98ed4c9faeafaf9381234189481a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 15:48:02 -0600 Subject: [PATCH 05/19] build(deps): bump docker/setup-qemu-action from 3.2.0 to 3.3.0 (#17277) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mihail Mihov --- .github/workflows/node-zxc-build-release-artifact.yaml | 2 +- .github/workflows/zxc-publish-production-image.yaml | 2 +- .github/workflows/zxc-verify-docker-build-determinism.yaml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/node-zxc-build-release-artifact.yaml b/.github/workflows/node-zxc-build-release-artifact.yaml index 2355966c996d..918cddbc379a 100644 --- a/.github/workflows/node-zxc-build-release-artifact.yaml +++ b/.github/workflows/node-zxc-build-release-artifact.yaml @@ -417,7 +417,7 @@ jobs: echo "docker-tag-base=${DOCKER_TAG_BASE}" >>"${GITHUB_OUTPUT}" - name: Setup QEmu Support - uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 + uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3.3.0 - name: Setup Docker Buildx Support uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0 diff --git a/.github/workflows/zxc-publish-production-image.yaml b/.github/workflows/zxc-publish-production-image.yaml index 8a5fdfadb3a5..521145e4688b 100644 --- a/.github/workflows/zxc-publish-production-image.yaml +++ b/.github/workflows/zxc-publish-production-image.yaml @@ -152,7 +152,7 @@ jobs: echo "docker-tag-base=${DOCKER_TAG_BASE}" >>"${GITHUB_OUTPUT}" - name: Setup QEmu Support - uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 + uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3.3.0 - name: Setup Docker Buildx Support uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0 diff --git a/.github/workflows/zxc-verify-docker-build-determinism.yaml b/.github/workflows/zxc-verify-docker-build-determinism.yaml index 9c08c42518c7..7bfbae7ecc14 100644 --- a/.github/workflows/zxc-verify-docker-build-determinism.yaml +++ b/.github/workflows/zxc-verify-docker-build-determinism.yaml @@ -193,7 +193,7 @@ jobs: # docker context use setup-docker-action - name: Setup QEmu Support - uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 + uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3.3.0 if: ${{ steps.baseline.outputs.exists == 'false' && !failure() && !cancelled() }} - name: Setup Docker Buildx Support @@ -423,7 +423,7 @@ jobs: # docker context use setup-docker-action - name: Setup QEmu Support - uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 + uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3.3.0 - name: Setup Docker Buildx Support uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0 From 6dae710e565091cdfb1653d2279dfa1348e57b57 Mon Sep 17 00:00:00 2001 From: Ivan Bankov Date: Tue, 14 Jan 2025 10:12:22 +0200 Subject: [PATCH 06/19] fix: move long term tests to repeatable (#17313) Signed-off-by: ibankov --- .../suites/hip423/LongTermScheduleUtils.java | 79 +- .../hip423/ScheduleLongTermExecutionTest.java | 915 ----------------- .../hip423/ScheduleLongTermSignTest.java | 572 +---------- .../integration/RepeatableHip423Tests.java | 6 +- ...RepeatableRosterLifecycleRestartTests.java | 4 +- ...peatableScheduleLongTermExecutionTest.java | 939 ++++++++++++++++++ .../RepeatableScheduleLongTermSignTest.java | 596 +++++++++++ 7 files changed, 1603 insertions(+), 1508 deletions(-) create mode 100644 hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/integration/RepeatableScheduleLongTermExecutionTest.java create mode 100644 hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/integration/RepeatableScheduleLongTermSignTest.java diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip423/LongTermScheduleUtils.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip423/LongTermScheduleUtils.java index 65d8e8517446..53dbdd727db8 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip423/LongTermScheduleUtils.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip423/LongTermScheduleUtils.java @@ -1,4 +1,19 @@ -// SPDX-License-Identifier: Apache-2.0 +/* + * 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.hip423; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getScheduleInfo; @@ -36,37 +51,37 @@ public final class LongTermScheduleUtils { public static final String RECEIVER = "receiver"; public static final String CREATE_TXN = "createTxn"; public static final String TRIGGERING_TXN = "triggeringTxn"; - static final String PAYER = "payer"; - static final String ADMIN = "admin"; - static final String EXTRA_KEY = "extraKey"; - static final String SHARED_KEY = "sharedKey"; - static final String BASIC_XFER = "basicXfer"; - static final String TWO_SIG_XFER = "twoSigXfer"; - static final String DEFERRED_XFER = "deferredXfer"; - static final String THREE_SIG_XFER = "threeSigXfer"; - static final String TOKEN_A = "tokenA"; - static final String CREATION = "creation"; - static final String BEFORE = "before"; - static final String DEFERRED_FALL = "deferredFall"; - static final String DEFERRED_CREATION = "deferredCreation"; - static final String PAYING_ACCOUNT_2 = "payingAccount2"; - static final String SCHEDULED_TRANSACTION_MUST_NOT_SUCCEED = "Scheduled transaction must not succeed"; - static final String SCHEDULE_CREATE_FEE = "scheduleCreateFee"; - static final String SENDER_1 = "sender1"; - static final String SENDER_2 = "sender2"; - static final String SENDER_3 = "sender3"; - static final String SIMPLE_UPDATE = "SimpleUpdate"; - static final String SUCCESS_TXN = "successTxn"; - static final String TRANSACTION_NOT_SCHEDULED = "Transaction not scheduled!"; - static final String VALID_SCHEDULE = "validSchedule"; - static final String WEIRDLY_POPULAR_KEY = "weirdlyPopularKey"; - static final String WRONG_CONSENSUS_TIMESTAMP = "Wrong consensus timestamp!"; - static final String WRONG_RECORD_ACCOUNT_ID = "Wrong record account ID!"; - static final String WRONG_SCHEDULE_ID = "Wrong schedule ID!"; - static final String WRONG_TRANSACTION_VALID_START = "Wrong transaction valid start!"; - static final String WRONG_TRANSFER_LIST = "Wrong transfer list!"; + public static final String PAYER = "payer"; + public static final String ADMIN = "admin"; + public static final String EXTRA_KEY = "extraKey"; + public static final String SHARED_KEY = "sharedKey"; + public static final String BASIC_XFER = "basicXfer"; + public static final String TWO_SIG_XFER = "twoSigXfer"; + public static final String DEFERRED_XFER = "deferredXfer"; + public static final String THREE_SIG_XFER = "threeSigXfer"; + public static final String TOKEN_A = "tokenA"; + public static final String CREATION = "creation"; + public static final String BEFORE = "before"; + public static final String DEFERRED_FALL = "deferredFall"; + public static final String DEFERRED_CREATION = "deferredCreation"; + public static final String PAYING_ACCOUNT_2 = "payingAccount2"; + public static final String SCHEDULED_TRANSACTION_MUST_NOT_SUCCEED = "Scheduled transaction must not succeed"; + public static final String SCHEDULE_CREATE_FEE = "scheduleCreateFee"; + public static final String SENDER_1 = "sender1"; + public static final String SENDER_2 = "sender2"; + public static final String SENDER_3 = "sender3"; + public static final String SIMPLE_UPDATE = "SimpleUpdate"; + public static final String SUCCESS_TXN = "successTxn"; + public static final String TRANSACTION_NOT_SCHEDULED = "Transaction not scheduled!"; + public static final String VALID_SCHEDULE = "validSchedule"; + public static final String WEIRDLY_POPULAR_KEY = "weirdlyPopularKey"; + public static final String WRONG_CONSENSUS_TIMESTAMP = "Wrong consensus timestamp!"; + public static final String WRONG_RECORD_ACCOUNT_ID = "Wrong record account ID!"; + public static final String WRONG_SCHEDULE_ID = "Wrong schedule ID!"; + public static final String WRONG_TRANSACTION_VALID_START = "Wrong transaction valid start!"; + public static final String WRONG_TRANSFER_LIST = "Wrong transfer list!"; - static final byte[] ORIG_FILE = "SOMETHING".getBytes(); + public static final byte[] ORIG_FILE = "SOMETHING".getBytes(); private LongTermScheduleUtils() {} @@ -98,7 +113,7 @@ public static boolean transferListCheck( return amountHasBeenTransferred && payerHasPaid; } - static SpecOperation[] scheduleFakeUpgrade( + public static SpecOperation[] scheduleFakeUpgrade( @NonNull final String payer, final long lifetime, @NonNull final String via) { final var operations = List.of( buildUpgradeZipFrom(FAKE_ASSETS_LOC), diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip423/ScheduleLongTermExecutionTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip423/ScheduleLongTermExecutionTest.java index 6af46800ae4e..522ecc75eb66 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip423/ScheduleLongTermExecutionTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip423/ScheduleLongTermExecutionTest.java @@ -17,125 +17,58 @@ package com.hedera.services.bdd.suites.hip423; import static com.hedera.services.bdd.junit.ContextRequirement.FEE_SCHEDULE_OVERRIDES; -import static com.hedera.services.bdd.junit.RepeatableReason.NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION; import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; -import static com.hedera.services.bdd.spec.assertions.TransactionRecordAsserts.recordWith; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getFileInfo; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getScheduleInfo; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTxnRecord; -import static com.hedera.services.bdd.spec.transactions.TxnUtils.asId; import static com.hedera.services.bdd.spec.transactions.TxnUtils.randomUppercase; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoDelete; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoTransfer; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.fileCreate; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.fileUpdate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.scheduleCreate; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.scheduleSign; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.systemFileDelete; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenAssociate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.uploadInitCode; import static com.hedera.services.bdd.spec.transactions.crypto.HapiCryptoTransfer.tinyBarsFromTo; import static com.hedera.services.bdd.spec.transactions.crypto.HapiCryptoTransfer.tinyBarsFromToWithInvalidAmounts; -import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.freezeAbort; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.recordFeeAmount; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sleepFor; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.uploadScheduledContractPrices; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; import static com.hedera.services.bdd.suites.HapiSuite.DEFAULT_PAYER; -import static com.hedera.services.bdd.suites.HapiSuite.FREEZE_ADMIN; import static com.hedera.services.bdd.suites.HapiSuite.GENESIS; -import static com.hedera.services.bdd.suites.HapiSuite.SYSTEM_ADMIN; -import static com.hedera.services.bdd.suites.HapiSuite.SYSTEM_DELETE_ADMIN; import static com.hedera.services.bdd.suites.HapiSuite.THREE_MONTHS_IN_SECONDS; -import static com.hedera.services.bdd.suites.HapiSuite.flattened; -import static com.hedera.services.bdd.suites.freeze.UpgradeSuite.standardUpdateFile; import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.ORIG_FILE; import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.PAYING_ACCOUNT_2; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.SCHEDULED_TRANSACTION_MUST_NOT_SUCCEED; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.SCHEDULE_CREATE_FEE; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.SENDER_1; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.SENDER_2; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.SENDER_3; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.SIMPLE_UPDATE; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.SUCCESS_TXN; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.TRANSACTION_NOT_SCHEDULED; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.TRIGGERING_TXN; import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.VALID_SCHEDULE; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.WEIRDLY_POPULAR_KEY; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.WRONG_CONSENSUS_TIMESTAMP; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.WRONG_RECORD_ACCOUNT_ID; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.WRONG_SCHEDULE_ID; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.WRONG_TRANSACTION_VALID_START; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.WRONG_TRANSFER_LIST; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.scheduleFakeUpgrade; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.transferListCheck; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.triggerSchedule; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_DELETED; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_ID_DOES_NOT_EXIST; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.AUTHORIZATION_FAILED; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.IDENTICAL_SCHEDULE_ALREADY_CREATED; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_ACCOUNT_BALANCE; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_PAYER_BALANCE; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_ACCOUNT_AMOUNTS; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_FILE_ID; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_PAYER_SIGNATURE; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_SCHEDULE_ID; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.PAYER_ACCOUNT_DELETED; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SCHEDULED_TRANSACTION_NOT_IN_WHITELIST; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SCHEDULE_EXPIRATION_TIME_TOO_FAR_IN_FUTURE; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SCHEDULE_EXPIRY_IS_BUSY; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; import com.hedera.services.bdd.junit.HapiTest; import com.hedera.services.bdd.junit.HapiTestLifecycle; import com.hedera.services.bdd.junit.LeakyHapiTest; -import com.hedera.services.bdd.junit.RepeatableHapiTest; import com.hedera.services.bdd.junit.support.TestLifecycle; import edu.umd.cs.findbugs.annotations.NonNull; -import java.math.BigInteger; -import java.time.Instant; import java.util.Map; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Stream; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DynamicTest; -import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.TestMethodOrder; -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) @HapiTestLifecycle public class ScheduleLongTermExecutionTest { private static final String PAYING_ACCOUNT = "payingAccount"; private static final String RECEIVER = "receiver"; private static final String SENDER = "sender"; private static final String SENDER_TXN = "senderTxn"; - private static final String PAYING_ACCOUNT_TXN = "payingAccountTxn"; - private static final String LUCKY_RECEIVER = "luckyReceiver"; private static final String FAILED_XFER = "failedXfer"; - private static final String WEIRDLY_POPULAR_KEY_TXN = "weirdlyPopularKeyTxn"; private static final String PAYER_TXN = "payerTxn"; private static final String FILE_NAME = "misc"; private static final long ONE_MINUTE = 60; private static final long TWO_MONTHS = 5356800; - private static final long PAYER_INITIAL_BALANCE = 1000000000000L; - - public static final String BASIC_XFER = "basicXfer"; - public static final String CREATE_TX = "createTxn"; - public static final String SIGN_TX = "sign_tx"; @BeforeAll static void beforeAll(@NonNull final TestLifecycle lifecycle) { - // override and preserve old values lifecycle.overrideInClass(Map.of( "scheduling.longTermEnabled", "true", @@ -145,566 +78,7 @@ static void beforeAll(@NonNull final TestLifecycle lifecycle) { + "Freeze,ContractCall,ContractCreate,ContractUpdate,ContractDelete")); } - @SuppressWarnings("java:S5960") - @HapiTest - @Order(1) - final Stream executionWithCustomPayerWorks() { - return hapiTest(flattened( - cryptoCreate(PAYING_ACCOUNT), - cryptoCreate(RECEIVER), - cryptoCreate(SENDER).via(SENDER_TXN), - scheduleCreate(BASIC_XFER, cryptoTransfer(tinyBarsFromTo(SENDER, RECEIVER, 1))) - .designatingPayer(PAYING_ACCOUNT) - .waitForExpiry() - .withRelativeExpiry(SENDER_TXN, 4) - .recordingScheduledTxn() - .via(CREATE_TX), - scheduleSign(BASIC_XFER) - .alsoSigningWith(SENDER, PAYING_ACCOUNT) - .via(SIGN_TX) - .hasKnownStatus(SUCCESS), - getScheduleInfo(BASIC_XFER) - .hasScheduleId(BASIC_XFER) - .hasWaitForExpiry() - .isNotExecuted() - .isNotDeleted() - .hasRelativeExpiry(SENDER_TXN, 4) - .hasRecordedScheduledTxn(), - triggerSchedule(BASIC_XFER), - withOpContext((spec, opLog) -> { - var createTx = getTxnRecord(CREATE_TX); - var signTx = getTxnRecord(SIGN_TX); - var triggeringTx = getTxnRecord(TRIGGERING_TXN); - var triggeredTx = getTxnRecord(CREATE_TX).scheduled(); - allRunFor(spec, createTx, signTx, triggeredTx, triggeringTx); - - Assertions.assertEquals( - SUCCESS, - triggeredTx.getResponseRecord().getReceipt().getStatus(), - SCHEDULED_TRANSACTION_MUST_NOT_SUCCEED); - - Instant triggerTime = Instant.ofEpochSecond( - triggeringTx - .getResponseRecord() - .getConsensusTimestamp() - .getSeconds(), - triggeringTx - .getResponseRecord() - .getConsensusTimestamp() - .getNanos()); - - Instant triggeredTime = Instant.ofEpochSecond( - triggeredTx - .getResponseRecord() - .getConsensusTimestamp() - .getSeconds(), - triggeredTx - .getResponseRecord() - .getConsensusTimestamp() - .getNanos()); - - Assertions.assertTrue(triggerTime.isBefore(triggeredTime), WRONG_CONSENSUS_TIMESTAMP); - - Assertions.assertEquals( - createTx.getResponseRecord().getTransactionID().getTransactionValidStart(), - triggeredTx.getResponseRecord().getTransactionID().getTransactionValidStart(), - WRONG_TRANSACTION_VALID_START); - - Assertions.assertEquals( - createTx.getResponseRecord().getTransactionID().getAccountID(), - triggeredTx.getResponseRecord().getTransactionID().getAccountID(), - WRONG_RECORD_ACCOUNT_ID); - - Assertions.assertTrue( - triggeredTx.getResponseRecord().getTransactionID().getScheduled(), - TRANSACTION_NOT_SCHEDULED); - - Assertions.assertEquals( - createTx.getResponseRecord().getReceipt().getScheduleID(), - triggeredTx.getResponseRecord().getScheduleRef(), - WRONG_SCHEDULE_ID); - - Assertions.assertTrue( - transferListCheck( - triggeredTx, - asId(SENDER, spec), - asId(RECEIVER, spec), - asId(PAYING_ACCOUNT, spec), - 1L), - WRONG_TRANSFER_LIST); - }))); - } - - @HapiTest - @Order(2) - final Stream executionWithCustomPayerAndAdminKeyWorks() { - return hapiTest(flattened( - newKeyNamed("adminKey"), - cryptoCreate(PAYING_ACCOUNT), - cryptoCreate(RECEIVER), - cryptoCreate(SENDER).via(SENDER_TXN), - scheduleCreate(BASIC_XFER, cryptoTransfer(tinyBarsFromTo(SENDER, RECEIVER, 1))) - .designatingPayer(PAYING_ACCOUNT) - .adminKey("adminKey") - .waitForExpiry() - .withRelativeExpiry(SENDER_TXN, 4) - .recordingScheduledTxn() - .via(CREATE_TX), - scheduleSign(BASIC_XFER) - .alsoSigningWith(SENDER, PAYING_ACCOUNT) - .via(SIGN_TX) - .hasKnownStatus(SUCCESS), - getScheduleInfo(BASIC_XFER) - .hasScheduleId(BASIC_XFER) - .hasWaitForExpiry() - .isNotExecuted() - .isNotDeleted() - .hasRelativeExpiry(SENDER_TXN, 4) - .hasRecordedScheduledTxn(), - triggerSchedule(BASIC_XFER), - withOpContext((spec, opLog) -> { - var createTx = getTxnRecord(CREATE_TX); - var signTx = getTxnRecord(SIGN_TX); - var triggeringTx = getTxnRecord(TRIGGERING_TXN); - var triggeredTx = getTxnRecord(CREATE_TX).scheduled(); - allRunFor(spec, createTx, signTx, triggeredTx, triggeringTx); - Assertions.assertEquals( - SUCCESS, - triggeredTx.getResponseRecord().getReceipt().getStatus(), - SCHEDULED_TRANSACTION_MUST_NOT_SUCCEED); - - Instant triggerTime = Instant.ofEpochSecond( - triggeringTx - .getResponseRecord() - .getConsensusTimestamp() - .getSeconds(), - triggeringTx - .getResponseRecord() - .getConsensusTimestamp() - .getNanos()); - - Instant triggeredTime = Instant.ofEpochSecond( - triggeredTx - .getResponseRecord() - .getConsensusTimestamp() - .getSeconds(), - triggeredTx - .getResponseRecord() - .getConsensusTimestamp() - .getNanos()); - - Assertions.assertTrue(triggerTime.isBefore(triggeredTime), WRONG_CONSENSUS_TIMESTAMP); - - Assertions.assertEquals( - createTx.getResponseRecord().getTransactionID().getTransactionValidStart(), - triggeredTx.getResponseRecord().getTransactionID().getTransactionValidStart(), - WRONG_TRANSACTION_VALID_START); - - Assertions.assertEquals( - createTx.getResponseRecord().getTransactionID().getAccountID(), - triggeredTx.getResponseRecord().getTransactionID().getAccountID(), - WRONG_RECORD_ACCOUNT_ID); - - Assertions.assertTrue( - triggeredTx.getResponseRecord().getTransactionID().getScheduled(), - TRANSACTION_NOT_SCHEDULED); - - Assertions.assertEquals( - createTx.getResponseRecord().getReceipt().getScheduleID(), - triggeredTx.getResponseRecord().getScheduleRef(), - WRONG_SCHEDULE_ID); - - Assertions.assertTrue( - transferListCheck( - triggeredTx, - asId(SENDER, spec), - asId(RECEIVER, spec), - asId(PAYING_ACCOUNT, spec), - 1L), - WRONG_TRANSFER_LIST); - }))); - } - - @HapiTest - @Order(3) - final Stream executionWithCustomPayerWhoSignsAtCreationAsPayerWorks() { - return hapiTest(flattened( - cryptoCreate(PAYING_ACCOUNT), - cryptoCreate(RECEIVER), - cryptoCreate(SENDER).via(SENDER_TXN), - scheduleCreate(BASIC_XFER, cryptoTransfer(tinyBarsFromTo(SENDER, RECEIVER, 1))) - .payingWith(PAYING_ACCOUNT) - .designatingPayer(PAYING_ACCOUNT) - .waitForExpiry() - .withRelativeExpiry(SENDER_TXN, 4) - .recordingScheduledTxn() - .via(CREATE_TX), - scheduleSign(BASIC_XFER).alsoSigningWith(SENDER).via(SIGN_TX).hasKnownStatus(SUCCESS), - getScheduleInfo(BASIC_XFER) - .hasScheduleId(BASIC_XFER) - .hasWaitForExpiry() - .isNotExecuted() - .isNotDeleted() - .hasRelativeExpiry(SENDER_TXN, 4) - .hasRecordedScheduledTxn(), - triggerSchedule(BASIC_XFER), - withOpContext((spec, opLog) -> { - var createTx = getTxnRecord(CREATE_TX); - var signTx = getTxnRecord(SIGN_TX); - var triggeringTx = getTxnRecord(TRIGGERING_TXN); - var triggeredTx = getTxnRecord(CREATE_TX).scheduled(); - allRunFor(spec, createTx, signTx, triggeredTx, triggeringTx); - - Assertions.assertEquals( - SUCCESS, - triggeredTx.getResponseRecord().getReceipt().getStatus(), - SCHEDULED_TRANSACTION_MUST_NOT_SUCCEED); - - Instant triggerTime = Instant.ofEpochSecond( - triggeringTx - .getResponseRecord() - .getConsensusTimestamp() - .getSeconds(), - triggeringTx - .getResponseRecord() - .getConsensusTimestamp() - .getNanos()); - - Instant triggeredTime = Instant.ofEpochSecond( - triggeredTx - .getResponseRecord() - .getConsensusTimestamp() - .getSeconds(), - triggeredTx - .getResponseRecord() - .getConsensusTimestamp() - .getNanos()); - - Assertions.assertTrue(triggerTime.isBefore(triggeredTime), WRONG_CONSENSUS_TIMESTAMP); - - Assertions.assertEquals( - createTx.getResponseRecord().getTransactionID().getTransactionValidStart(), - triggeredTx.getResponseRecord().getTransactionID().getTransactionValidStart(), - WRONG_TRANSACTION_VALID_START); - - Assertions.assertEquals( - createTx.getResponseRecord().getTransactionID().getAccountID(), - triggeredTx.getResponseRecord().getTransactionID().getAccountID(), - WRONG_RECORD_ACCOUNT_ID); - - Assertions.assertTrue( - triggeredTx.getResponseRecord().getTransactionID().getScheduled(), - TRANSACTION_NOT_SCHEDULED); - - Assertions.assertEquals( - createTx.getResponseRecord().getReceipt().getScheduleID(), - triggeredTx.getResponseRecord().getScheduleRef(), - WRONG_SCHEDULE_ID); - - Assertions.assertTrue( - transferListCheck( - triggeredTx, - asId(SENDER, spec), - asId(RECEIVER, spec), - asId(PAYING_ACCOUNT, spec), - 1L), - WRONG_TRANSFER_LIST); - }))); - } - - @LeakyHapiTest(requirement = FEE_SCHEDULE_OVERRIDES) - @Order(5) - public Stream executionWithContractCallWorksAtExpiry() { - final var payerBalance = new AtomicLong(); - return hapiTest(flattened( - // upload fees for SCHEDULE_CREATE_CONTRACT_CALL - uploadScheduledContractPrices(GENESIS), - uploadInitCode(SIMPLE_UPDATE), - contractCreate(SIMPLE_UPDATE).gas(500_000L), - cryptoCreate(PAYING_ACCOUNT).balance(PAYER_INITIAL_BALANCE).via(PAYING_ACCOUNT_TXN), - scheduleCreate( - BASIC_XFER, - contractCall(SIMPLE_UPDATE, "set", BigInteger.valueOf(5), BigInteger.valueOf(42)) - .gas(300000L)) - .waitForExpiry() - .withRelativeExpiry(PAYING_ACCOUNT_TXN, 4) - .designatingPayer(PAYING_ACCOUNT) - .alsoSigningWith(PAYING_ACCOUNT) - .recordingScheduledTxn() - .via(CREATE_TX), - getScheduleInfo(BASIC_XFER) - .hasScheduleId(BASIC_XFER) - .hasWaitForExpiry() - .isNotExecuted() - .isNotDeleted() - .hasRelativeExpiry(PAYING_ACCOUNT_TXN, 4) - .hasRecordedScheduledTxn(), - triggerSchedule(BASIC_XFER), - getAccountBalance(PAYING_ACCOUNT) - .hasTinyBars(spec -> - bal -> bal < PAYER_INITIAL_BALANCE ? Optional.empty() : Optional.of("didnt change")) - .exposingBalanceTo(payerBalance::set), - withOpContext((spec, opLog) -> { - var triggeredTx = getTxnRecord(CREATE_TX).scheduled(); - allRunFor(spec, triggeredTx); - final var txnFee = triggeredTx.getResponseRecord().getTransactionFee(); - // check if only designating payer was charged - Assertions.assertEquals(PAYER_INITIAL_BALANCE, txnFee + payerBalance.get()); - - Assertions.assertEquals( - SUCCESS, - triggeredTx.getResponseRecord().getReceipt().getStatus(), - SCHEDULED_TRANSACTION_MUST_NOT_SUCCEED); - - Assertions.assertTrue(triggeredTx - .getResponseRecord() - .getContractCallResult() - .getContractCallResult() - .size() - >= 0); - }))); - } - - @HapiTest - @Order(6) - public Stream executionWithContractCreateWorksAtExpiry() { - final var payerBalance = new AtomicLong(); - return hapiTest(flattened( - uploadInitCode(SIMPLE_UPDATE), - cryptoCreate(PAYING_ACCOUNT).balance(PAYER_INITIAL_BALANCE).via(PAYING_ACCOUNT_TXN), - scheduleCreate( - BASIC_XFER, - contractCreate(SIMPLE_UPDATE).gas(500_000L).adminKey(PAYING_ACCOUNT)) - .waitForExpiry() - .withRelativeExpiry(PAYING_ACCOUNT_TXN, 4) - .designatingPayer(PAYING_ACCOUNT) - .alsoSigningWith(PAYING_ACCOUNT) - .recordingScheduledTxn() - .via(CREATE_TX), - getScheduleInfo(BASIC_XFER) - .hasScheduleId(BASIC_XFER) - .hasWaitForExpiry() - .isNotExecuted() - .isNotDeleted() - .hasRelativeExpiry(PAYING_ACCOUNT_TXN, 4) - .hasRecordedScheduledTxn(), - triggerSchedule(BASIC_XFER), - getAccountBalance(PAYING_ACCOUNT) - .hasTinyBars(spec -> - bal -> bal < PAYER_INITIAL_BALANCE ? Optional.empty() : Optional.of("didnt change")) - .exposingBalanceTo(payerBalance::set), - withOpContext((spec, opLog) -> { - var triggeredTx = getTxnRecord(CREATE_TX).scheduled(); - allRunFor(spec, triggeredTx); - final var txnFee = triggeredTx.getResponseRecord().getTransactionFee(); - // check if only designating payer was charged - Assertions.assertEquals(PAYER_INITIAL_BALANCE, txnFee + payerBalance.get()); - - Assertions.assertEquals( - SUCCESS, - triggeredTx.getResponseRecord().getReceipt().getStatus(), - SCHEDULED_TRANSACTION_MUST_NOT_SUCCEED); - - Assertions.assertTrue( - triggeredTx.getResponseRecord().getReceipt().hasContractID()); - - Assertions.assertTrue(triggeredTx - .getResponseRecord() - .getContractCreateResult() - .getContractCallResult() - .size() - >= 0); - }))); - } - - @HapiTest - @Order(7) - public Stream executionWithDefaultPayerButNoFundsFails() { - long balance = 10_000_000L; - long noBalance = 0L; - long transferAmount = 1L; - return hapiTest(flattened( - cryptoCreate(PAYING_ACCOUNT).balance(balance), - cryptoCreate(LUCKY_RECEIVER), - cryptoCreate(SENDER).balance(transferAmount).via(SENDER_TXN), - cryptoCreate(RECEIVER).balance(noBalance), - scheduleCreate(BASIC_XFER, cryptoTransfer(tinyBarsFromTo(SENDER, RECEIVER, transferAmount))) - .waitForExpiry() - .withRelativeExpiry(SENDER_TXN, 4) - .payingWith(PAYING_ACCOUNT) - .recordingScheduledTxn() - .via(CREATE_TX), - recordFeeAmount(CREATE_TX, SCHEDULE_CREATE_FEE), - cryptoTransfer(tinyBarsFromTo(PAYING_ACCOUNT, LUCKY_RECEIVER, (spec -> { - long scheduleCreateFee = spec.registry().getAmount(SCHEDULE_CREATE_FEE); - return balance - scheduleCreateFee; - }))), - getAccountBalance(PAYING_ACCOUNT).hasTinyBars(noBalance), - scheduleSign(BASIC_XFER).alsoSigningWith(SENDER).hasKnownStatus(SUCCESS), - getScheduleInfo(BASIC_XFER) - .hasScheduleId(BASIC_XFER) - .hasWaitForExpiry() - .isNotExecuted() - .isNotDeleted() - .hasRelativeExpiry(SENDER_TXN, 4) - .hasRecordedScheduledTxn(), - triggerSchedule(BASIC_XFER), - getAccountBalance(SENDER).hasTinyBars(transferAmount), - getAccountBalance(RECEIVER).hasTinyBars(noBalance), - withOpContext((spec, opLog) -> { - var triggeredTx = getTxnRecord(CREATE_TX).scheduled(); - - allRunFor(spec, triggeredTx); - - Assertions.assertEquals( - INSUFFICIENT_PAYER_BALANCE, - triggeredTx.getResponseRecord().getReceipt().getStatus(), - SCHEDULED_TRANSACTION_MUST_NOT_SUCCEED); - }))); - } - - @HapiTest - @Order(8) - public Stream executionWithCustomPayerThatNeverSignsFails() { - long transferAmount = 1; - return hapiTest(flattened( - cryptoCreate(PAYING_ACCOUNT), - cryptoCreate(SENDER).via(SENDER_TXN), - cryptoCreate(RECEIVER), - scheduleCreate(BASIC_XFER, cryptoTransfer(tinyBarsFromTo(SENDER, RECEIVER, transferAmount))) - .waitForExpiry() - .withRelativeExpiry(SENDER_TXN, 4) - .recordingScheduledTxn() - .designatingPayer(PAYING_ACCOUNT) - .via(CREATE_TX), - scheduleSign(BASIC_XFER).alsoSigningWith(SENDER).via(SIGN_TX).hasKnownStatus(SUCCESS), - getScheduleInfo(BASIC_XFER) - .hasScheduleId(BASIC_XFER) - .hasWaitForExpiry() - .isNotExecuted() - .isNotDeleted() - .hasRelativeExpiry(SENDER_TXN, 4) - .hasRecordedScheduledTxn(), - triggerSchedule(BASIC_XFER), - getTxnRecord(CREATE_TX).scheduled().hasPriority(recordWith().status(INVALID_PAYER_SIGNATURE)))); - } - @HapiTest - @Order(9) - public Stream executionWithCustomPayerButNoFundsFails() { - long balance = 0L; - long transferAmount = 1; - return hapiTest(flattened( - cryptoCreate(PAYING_ACCOUNT).balance(balance), - cryptoCreate(SENDER).via(SENDER_TXN), - cryptoCreate(RECEIVER), - scheduleCreate(BASIC_XFER, cryptoTransfer(tinyBarsFromTo(SENDER, RECEIVER, transferAmount))) - .waitForExpiry() - .withRelativeExpiry(SENDER_TXN, 4) - .recordingScheduledTxn() - .designatingPayer(PAYING_ACCOUNT) - .via(CREATE_TX), - scheduleSign(BASIC_XFER) - .alsoSigningWith(SENDER, PAYING_ACCOUNT) - .via(SIGN_TX) - .hasKnownStatus(SUCCESS), - getScheduleInfo(BASIC_XFER) - .hasScheduleId(BASIC_XFER) - .hasWaitForExpiry() - .isNotExecuted() - .isNotDeleted() - .hasRelativeExpiry(SENDER_TXN, 4) - .hasRecordedScheduledTxn(), - triggerSchedule(BASIC_XFER), - withOpContext((spec, opLog) -> { - var triggeredTx = getTxnRecord(CREATE_TX).scheduled(); - - allRunFor(spec, triggeredTx); - - Assertions.assertEquals( - INSUFFICIENT_PAYER_BALANCE, - triggeredTx.getResponseRecord().getReceipt().getStatus(), - SCHEDULED_TRANSACTION_MUST_NOT_SUCCEED); - }))); - } - - @HapiTest - @Order(10) - public Stream executionWithDefaultPayerButAccountDeletedFails() { - long balance = 10_000_000L; - long noBalance = 0L; - long transferAmount = 1L; - return hapiTest(flattened( - cryptoCreate(PAYING_ACCOUNT).balance(balance), - cryptoCreate(LUCKY_RECEIVER), - cryptoCreate(SENDER).balance(transferAmount).via(SENDER_TXN), - cryptoCreate(RECEIVER).balance(noBalance), - scheduleCreate(BASIC_XFER, cryptoTransfer(tinyBarsFromTo(SENDER, RECEIVER, transferAmount))) - .waitForExpiry() - .withRelativeExpiry(SENDER_TXN, 4) - .recordingScheduledTxn() - .payingWith(PAYING_ACCOUNT) - .via(CREATE_TX), - recordFeeAmount(CREATE_TX, SCHEDULE_CREATE_FEE), - cryptoDelete(PAYING_ACCOUNT), - scheduleSign(BASIC_XFER).alsoSigningWith(SENDER).hasKnownStatus(SUCCESS), - getAccountBalance(SENDER).hasTinyBars(transferAmount), - getAccountBalance(RECEIVER).hasTinyBars(noBalance), - getScheduleInfo(BASIC_XFER) - .hasScheduleId(BASIC_XFER) - .hasWaitForExpiry() - .isNotExecuted() - .isNotDeleted() - .hasRelativeExpiry(SENDER_TXN, 4) - .hasRecordedScheduledTxn(), - triggerSchedule(BASIC_XFER), - getAccountBalance(SENDER).hasTinyBars(transferAmount), - getAccountBalance(RECEIVER).hasTinyBars(noBalance), - // future: a check if account was deleted will be added in DispatchValidator - getTxnRecord(CREATE_TX) - .scheduled() - .hasPriority(recordWith().statusFrom(PAYER_ACCOUNT_DELETED, INSUFFICIENT_PAYER_BALANCE)))); - } - - @HapiTest - @Order(11) - public Stream executionWithCustomPayerButAccountDeletedFails() { - long balance = 10_000_000L; - long noBalance = 0L; - long transferAmount = 1; - return hapiTest(flattened( - cryptoCreate(PAYING_ACCOUNT).balance(balance), - cryptoCreate(SENDER).balance(transferAmount).via(SENDER_TXN), - cryptoCreate(RECEIVER).balance(noBalance), - scheduleCreate(BASIC_XFER, cryptoTransfer(tinyBarsFromTo(SENDER, RECEIVER, transferAmount))) - .waitForExpiry() - .withRelativeExpiry(SENDER_TXN, 4) - .recordingScheduledTxn() - .designatingPayer(PAYING_ACCOUNT) - .alsoSigningWith(PAYING_ACCOUNT) - .via(CREATE_TX), - cryptoDelete(PAYING_ACCOUNT), - scheduleSign(BASIC_XFER).alsoSigningWith(SENDER).via(SIGN_TX).hasKnownStatus(SUCCESS), - getAccountBalance(SENDER).hasTinyBars(transferAmount), - getAccountBalance(RECEIVER).hasTinyBars(noBalance), - getScheduleInfo(BASIC_XFER) - .hasScheduleId(BASIC_XFER) - .hasWaitForExpiry() - .isNotExecuted() - .isNotDeleted() - .hasRelativeExpiry(SENDER_TXN, 4) - .hasRecordedScheduledTxn(), - triggerSchedule(BASIC_XFER), - getAccountBalance(SENDER).hasTinyBars(transferAmount), - getAccountBalance(RECEIVER).hasTinyBars(noBalance), - // future: a check if account was deleted will be added in DispatchValidator - getTxnRecord(CREATE_TX) - .scheduled() - .hasPriority(recordWith().statusFrom(INSUFFICIENT_PAYER_BALANCE, PAYER_ACCOUNT_DELETED)))); - } - - @HapiTest - @Order(12) public Stream executionWithInvalidAccountAmountsFails() { long transferAmount = 100; long senderBalance = 1000L; @@ -725,290 +99,6 @@ public Stream executionWithInvalidAccountAmountsFails() { } @HapiTest - @Order(13) - public Stream executionWithCryptoInsufficientAccountBalanceFails() { - long noBalance = 0L; - long senderBalance = 100L; - long transferAmount = 101L; - long payerBalance = 1_000_000L; - return hapiTest(flattened( - cryptoCreate(PAYING_ACCOUNT).balance(payerBalance), - cryptoCreate(SENDER).balance(senderBalance).via(SENDER_TXN), - cryptoCreate(RECEIVER).balance(noBalance), - scheduleCreate(FAILED_XFER, cryptoTransfer(tinyBarsFromTo(SENDER, RECEIVER, transferAmount))) - .waitForExpiry() - .withRelativeExpiry(SENDER_TXN, 4) - .designatingPayer(PAYING_ACCOUNT) - .recordingScheduledTxn() - .via(CREATE_TX), - scheduleSign(FAILED_XFER) - .alsoSigningWith(SENDER, PAYING_ACCOUNT) - .via(SIGN_TX) - .hasKnownStatus(SUCCESS), - getScheduleInfo(FAILED_XFER) - .hasScheduleId(BASIC_XFER) - .hasWaitForExpiry() - .isNotExecuted() - .isNotDeleted() - .hasRelativeExpiry(SENDER_TXN, 4) - .hasRecordedScheduledTxn(), - triggerSchedule(FAILED_XFER), - getAccountBalance(SENDER).hasTinyBars(senderBalance), - getAccountBalance(RECEIVER).hasTinyBars(noBalance), - withOpContext((spec, opLog) -> { - var triggeredTx = getTxnRecord(CREATE_TX).scheduled(); - - allRunFor(spec, triggeredTx); - - Assertions.assertEquals( - INSUFFICIENT_ACCOUNT_BALANCE, - triggeredTx.getResponseRecord().getReceipt().getStatus(), - SCHEDULED_TRANSACTION_MUST_NOT_SUCCEED); - }))); - } - - @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) - public Stream executionWithCryptoSenderDeletedFails() { - long noBalance = 0L; - long senderBalance = 100L; - long transferAmount = 101L; - long payerBalance = 1_000_000L; - return hapiTest(flattened( - cryptoCreate(PAYING_ACCOUNT).balance(payerBalance), - cryptoCreate(SENDER).balance(senderBalance).via(SENDER_TXN), - cryptoCreate(RECEIVER).balance(noBalance), - scheduleCreate(FAILED_XFER, cryptoTransfer(tinyBarsFromTo(SENDER, RECEIVER, transferAmount))) - .waitForExpiry() - .withRelativeExpiry(SENDER_TXN, 5) - .recordingScheduledTxn() - .designatingPayer(PAYING_ACCOUNT) - .via(CREATE_TX), - cryptoDelete(SENDER), - scheduleSign(FAILED_XFER) - .alsoSigningWith(SENDER, PAYING_ACCOUNT) - .via(SIGN_TX) - .hasKnownStatus(SUCCESS), - getAccountBalance(RECEIVER).hasTinyBars(noBalance), - getScheduleInfo(FAILED_XFER) - .hasScheduleId(FAILED_XFER) - .hasWaitForExpiry() - .isNotExecuted() - .isNotDeleted() - .hasRelativeExpiry(SENDER_TXN, 5) - .hasRecordedScheduledTxn(), - triggerSchedule(FAILED_XFER), - getAccountBalance(RECEIVER).hasTinyBars(noBalance), - withOpContext((spec, opLog) -> { - var triggeredTx = getTxnRecord(CREATE_TX).scheduled(); - allRunFor(spec, triggeredTx); - Assertions.assertEquals( - ACCOUNT_DELETED, - triggeredTx.getResponseRecord().getReceipt().getStatus(), - SCHEDULED_TRANSACTION_MUST_NOT_SUCCEED); - }))); - } - - @HapiTest - @Order(15) - public Stream executionTriggersWithWeirdlyRepeatedKey() { - String schedule = "dupKeyXfer"; - - return hapiTest(flattened( - cryptoCreate(WEIRDLY_POPULAR_KEY), - cryptoCreate(SENDER_1).key(WEIRDLY_POPULAR_KEY).balance(1L), - cryptoCreate(SENDER_2).key(WEIRDLY_POPULAR_KEY).balance(1L), - cryptoCreate(SENDER_3).key(WEIRDLY_POPULAR_KEY).balance(1L), - cryptoCreate(RECEIVER).balance(0L).via(WEIRDLY_POPULAR_KEY_TXN), - scheduleCreate( - schedule, - cryptoTransfer( - tinyBarsFromTo(SENDER_1, RECEIVER, 1L), - tinyBarsFromTo(SENDER_2, RECEIVER, 1L), - tinyBarsFromTo(SENDER_3, RECEIVER, 1L))) - .waitForExpiry() - .withRelativeExpiry(WEIRDLY_POPULAR_KEY_TXN, 4) - .payingWith(DEFAULT_PAYER) - .recordingScheduledTxn() - .via("creation"), - scheduleSign(schedule).alsoSigningWith(WEIRDLY_POPULAR_KEY), - getScheduleInfo(schedule) - .hasScheduleId(schedule) - .hasWaitForExpiry() - .isNotExecuted() - .isNotDeleted() - .hasRelativeExpiry(WEIRDLY_POPULAR_KEY_TXN, 4) - .hasRecordedScheduledTxn(), - triggerSchedule(schedule), - getAccountBalance(SENDER_1).hasTinyBars(0L), - getAccountBalance(SENDER_2).hasTinyBars(0L), - getAccountBalance(SENDER_3).hasTinyBars(0L), - getAccountBalance(RECEIVER).hasTinyBars(3L), - scheduleSign(schedule).alsoSigningWith(WEIRDLY_POPULAR_KEY).hasKnownStatus(INVALID_SCHEDULE_ID))); - } - - @HapiTest - @Order(16) - final Stream scheduledFreezeWorksAsExpected() { - return hapiTest(flattened( - cryptoCreate(PAYING_ACCOUNT).via(PAYER_TXN), - scheduleFakeUpgrade(PAYING_ACCOUNT, 4, SUCCESS_TXN), - scheduleSign(VALID_SCHEDULE) - .alsoSigningWith(GENESIS) - .payingWith(PAYING_ACCOUNT) - .hasKnownStatus(SUCCESS), - getScheduleInfo(VALID_SCHEDULE) - .hasScheduleId(VALID_SCHEDULE) - .hasWaitForExpiry() - .isNotExecuted() - .isNotDeleted() - .hasRecordedScheduledTxn(), - triggerSchedule(VALID_SCHEDULE), - freezeAbort().payingWith(GENESIS), - withOpContext((spec, opLog) -> { - var triggeredTx = getTxnRecord(SUCCESS_TXN).scheduled(); - allRunFor(spec, triggeredTx); - Assertions.assertEquals( - SUCCESS, - triggeredTx.getResponseRecord().getReceipt().getStatus(), - SCHEDULED_TRANSACTION_MUST_NOT_SUCCEED); - }))); - } - - @HapiTest - @Order(17) - final Stream scheduledFreezeWithUnauthorizedPayerFails() { - - return hapiTest(flattened( - cryptoCreate(PAYING_ACCOUNT).via(PAYER_TXN), - cryptoCreate(PAYING_ACCOUNT_2), - scheduleFakeUpgrade(PAYING_ACCOUNT, 4, "test"), - // future throttles will be exceeded because there is no throttle - // for freeze - // and the custom payer is not exempt from throttles like and admin - // user would be - // todo future throttle is not implemented yet - // .hasKnownStatus(SCHEDULE_FUTURE_THROTTLE_EXCEEDED) - - // note: the sleepFor and cryptoCreate operations are added only to clear the schedule before - // the next state. This was needed because an edge case in the BaseTranslator occur. - // When scheduleCreate trigger the schedules execution scheduleRef field is not the correct one. - sleepFor(6000), - cryptoCreate("foo"))); - } - - @HapiTest - @Order(18) - final Stream scheduledPermissionedFileUpdateWorksAsExpected() { - return hapiTest(flattened( - cryptoCreate(PAYING_ACCOUNT).via(PAYER_TXN), - scheduleCreate(VALID_SCHEDULE, fileUpdate(standardUpdateFile).contents("fooo!")) - .withEntityMemo(randomUppercase(100)) - .designatingPayer(SYSTEM_ADMIN) - .payingWith(PAYING_ACCOUNT) - .waitForExpiry() - .withRelativeExpiry(PAYER_TXN, 4) - .recordingScheduledTxn() - .via(SUCCESS_TXN), - scheduleSign(VALID_SCHEDULE) - .alsoSigningWith(SYSTEM_ADMIN) - .payingWith(PAYING_ACCOUNT) - .hasKnownStatus(SUCCESS), - getScheduleInfo(VALID_SCHEDULE) - .hasScheduleId(VALID_SCHEDULE) - .hasWaitForExpiry() - .isNotExecuted() - .isNotDeleted() - .hasRelativeExpiry(PAYER_TXN, 4) - .hasRecordedScheduledTxn(), - triggerSchedule(VALID_SCHEDULE), - withOpContext((spec, opLog) -> { - var triggeredTx = getTxnRecord(SUCCESS_TXN).scheduled(); - allRunFor(spec, triggeredTx); - - Assertions.assertEquals( - SUCCESS, - triggeredTx.getResponseRecord().getReceipt().getStatus(), - SCHEDULED_TRANSACTION_MUST_NOT_SUCCEED); - }))); - } - - @HapiTest - @Order(19) - final Stream scheduledPermissionedFileUpdateUnauthorizedPayerFails() { - return hapiTest(flattened( - cryptoCreate(PAYING_ACCOUNT).via(PAYER_TXN), - cryptoCreate(PAYING_ACCOUNT_2), - scheduleCreate(VALID_SCHEDULE, fileUpdate(standardUpdateFile).contents("fooo!")) - .withEntityMemo(randomUppercase(100)) - .designatingPayer(PAYING_ACCOUNT_2) - .payingWith(PAYING_ACCOUNT) - .waitForExpiry() - .withRelativeExpiry(PAYER_TXN, 4) - .recordingScheduledTxn() - .via(SUCCESS_TXN), - scheduleSign(VALID_SCHEDULE) - .alsoSigningWith(PAYING_ACCOUNT_2, FREEZE_ADMIN) - .payingWith(PAYING_ACCOUNT) - .hasKnownStatus(SUCCESS), - getScheduleInfo(VALID_SCHEDULE) - .hasScheduleId(VALID_SCHEDULE) - .hasWaitForExpiry() - .isNotExecuted() - .isNotDeleted() - .hasRelativeExpiry(PAYER_TXN, 4) - .hasRecordedScheduledTxn(), - triggerSchedule(VALID_SCHEDULE), - withOpContext((spec, opLog) -> { - var triggeredTx = getTxnRecord(SUCCESS_TXN).scheduled(); - allRunFor(spec, triggeredTx); - - Assertions.assertEquals( - AUTHORIZATION_FAILED, - triggeredTx.getResponseRecord().getReceipt().getStatus(), - "Scheduled transaction be AUTHORIZATION_FAILED!"); - }))); - } - - @HapiTest - @Order(20) - final Stream scheduledSystemDeleteWorksAsExpected() { - return hapiTest(flattened( - cryptoCreate(PAYING_ACCOUNT).via(PAYER_TXN), - fileCreate(FILE_NAME).lifetime(THREE_MONTHS_IN_SECONDS).contents(ORIG_FILE), - scheduleCreate(VALID_SCHEDULE, systemFileDelete(FILE_NAME).updatingExpiry(1L)) - .withEntityMemo(randomUppercase(100)) - .designatingPayer(SYSTEM_DELETE_ADMIN) - .payingWith(PAYING_ACCOUNT) - .waitForExpiry() - .withRelativeExpiry(PAYER_TXN, 4) - .recordingScheduledTxn() - .via(SUCCESS_TXN), - scheduleSign(VALID_SCHEDULE) - .alsoSigningWith(SYSTEM_DELETE_ADMIN) - .payingWith(PAYING_ACCOUNT) - .hasKnownStatus(SUCCESS), - getScheduleInfo(VALID_SCHEDULE) - .hasScheduleId(VALID_SCHEDULE) - .hasWaitForExpiry() - .isNotExecuted() - .isNotDeleted() - .hasRelativeExpiry(PAYER_TXN, 4) - .hasRecordedScheduledTxn(), - triggerSchedule(VALID_SCHEDULE), - getFileInfo(FILE_NAME).nodePayment(1_234L).hasAnswerOnlyPrecheck(INVALID_FILE_ID), - withOpContext((spec, opLog) -> { - var triggeredTx = getTxnRecord(SUCCESS_TXN).scheduled(); - allRunFor(spec, triggeredTx); - - Assertions.assertEquals( - SUCCESS, - triggeredTx.getResponseRecord().getReceipt().getStatus(), - SCHEDULED_TRANSACTION_MUST_NOT_SUCCEED); - }))); - } - - @HapiTest - @Order(21) final Stream scheduledSystemDeleteUnauthorizedPayerFails() { return hapiTest( cryptoCreate(PAYING_ACCOUNT).via(PAYER_TXN), @@ -1024,7 +114,6 @@ final Stream scheduledSystemDeleteUnauthorizedPayerFails() { } @HapiTest - @Order(22) final Stream scheduleCreateWithExpiringInMoreThenTwoMonths() { return hapiTest( cryptoCreate("luckyYou").balance(0L), @@ -1034,7 +123,6 @@ final Stream scheduleCreateWithExpiringInMoreThenTwoMonths() { } @HapiTest - @Order(23) final Stream scheduleCreateWithNonWhiteListedTransaction() { return hapiTest( cryptoCreate("luckyYou").balance(0L), @@ -1045,7 +133,6 @@ final Stream scheduleCreateWithNonWhiteListedTransaction() { } @HapiTest - @Order(24) final Stream scheduleCreateWithNonExistingPayer() { return hapiTest( cryptoCreate("luckyYou").balance(0L), @@ -1056,7 +143,6 @@ final Stream scheduleCreateWithNonExistingPayer() { } @HapiTest - @Order(25) final Stream scheduleCreateIdenticalTransactions() { return hapiTest( cryptoCreate("luckyYou").balance(0L).via("cryptoCreate"), @@ -1069,7 +155,6 @@ final Stream scheduleCreateIdenticalTransactions() { } @LeakyHapiTest(requirement = FEE_SCHEDULE_OVERRIDES) - @Order(26) final Stream scheduleCreateIdenticalContractCall() { final var contract = "CallOperationsChecker"; return hapiTest( diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip423/ScheduleLongTermSignTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip423/ScheduleLongTermSignTest.java index 7e8c8c28a196..b02ea2f92b8e 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip423/ScheduleLongTermSignTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip423/ScheduleLongTermSignTest.java @@ -1,80 +1,43 @@ -// SPDX-License-Identifier: Apache-2.0 +/* + * 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.hip423; import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; -import static com.hedera.services.bdd.spec.assertions.AccountInfoAsserts.changeFromSnapshot; -import static com.hedera.services.bdd.spec.keys.ControlForKey.forKey; -import static com.hedera.services.bdd.spec.keys.KeyShape.sigs; -import static com.hedera.services.bdd.spec.keys.KeyShape.threshOf; -import static com.hedera.services.bdd.spec.keys.SigControl.OFF; -import static com.hedera.services.bdd.spec.keys.SigControl.ON; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getScheduleInfo; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTxnRecord; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoTransfer; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoUpdate; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.mintToken; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.scheduleCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.scheduleSign; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenCreate; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenUpdate; import static com.hedera.services.bdd.spec.transactions.crypto.HapiCryptoTransfer.tinyBarsFromTo; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.balanceSnapshot; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.keyFromMutation; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyListNamed; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sleepFor; -import static com.hedera.services.bdd.suites.HapiSuite.ADDRESS_BOOK_CONTROL; import static com.hedera.services.bdd.suites.HapiSuite.DEFAULT_PAYER; -import static com.hedera.services.bdd.suites.HapiSuite.FUNDING; -import static com.hedera.services.bdd.suites.HapiSuite.GENESIS; -import static com.hedera.services.bdd.suites.HapiSuite.ONE_HBAR; -import static com.hedera.services.bdd.suites.HapiSuite.ONE_MILLION_HBARS; -import static com.hedera.services.bdd.suites.HapiSuite.THOUSAND_HBAR; -import static com.hedera.services.bdd.suites.HapiSuite.flattened; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.ADMIN; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.BASIC_XFER; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.BEFORE; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.CREATE_TXN; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.CREATION; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.DEFERRED_CREATION; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.DEFERRED_FALL; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.DEFERRED_XFER; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.EXTRA_KEY; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.NEW_SENDER_KEY; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.PAYER; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.SENDER_TXN; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.SHARED_KEY; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.THREE_SIG_XFER; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.TOKEN_A; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.TWO_SIG_XFER; -import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.triggerSchedule; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_SCHEDULE_ID; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.NO_NEW_VALID_SIGNATURES; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.RECORD_NOT_FOUND; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SCHEDULE_ALREADY_EXECUTED; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SOME_SIGNATURES_WERE_INVALID; import com.hedera.services.bdd.junit.HapiTest; import com.hedera.services.bdd.junit.HapiTestLifecycle; import com.hedera.services.bdd.junit.support.TestLifecycle; -import com.hedera.services.bdd.spec.keys.ControlForKey; -import com.hedera.services.bdd.spec.keys.OverlappingKeyGenerator; -import com.hederahashgraph.api.proto.java.Key; import edu.umd.cs.findbugs.annotations.NonNull; -import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DynamicTest; -import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.TestMethodOrder; @HapiTestLifecycle -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class ScheduleLongTermSignTest { private static final long ONE_MINUTE = 60; @@ -85,7 +48,6 @@ public class ScheduleLongTermSignTest { @BeforeAll static void beforeAll(@NonNull final TestLifecycle lifecycle) { - // override and preserve old values lifecycle.overrideInClass(Map.of( "scheduling.longTermEnabled", "true", @@ -96,492 +58,6 @@ static void beforeAll(@NonNull final TestLifecycle lifecycle) { } @HapiTest - @Order(1) - final Stream reductionInSigningReqsAllowsTxnToGoThrough() { - var senderShape = threshOf(2, threshOf(1, 3), threshOf(1, 3), threshOf(2, 3)); - var sigOne = senderShape.signedWith(sigs(sigs(OFF, OFF, ON), sigs(OFF, OFF, OFF), sigs(OFF, OFF, OFF))); - var sigTwo = senderShape.signedWith(sigs(sigs(OFF, OFF, OFF), sigs(ON, ON, ON), sigs(OFF, OFF, OFF))); - var firstSigThree = senderShape.signedWith(sigs(sigs(OFF, OFF, OFF), sigs(OFF, OFF, OFF), sigs(ON, OFF, OFF))); - String sender = "X"; - String receiver = "Y"; - String schedule = "Z"; - String senderKey = "sKey"; - - return hapiTest(flattened( - newKeyNamed(senderKey).shape(senderShape), - keyFromMutation(NEW_SENDER_KEY, senderKey).changing(this::lowerThirdNestedThresholdSigningReq), - cryptoCreate(sender).key(senderKey).via(SENDER_TXN), - cryptoCreate(receiver).balance(0L), - scheduleCreate(schedule, cryptoTransfer(tinyBarsFromTo(sender, receiver, 1))) - .payingWith(DEFAULT_PAYER) - .waitForExpiry() - .withRelativeExpiry(SENDER_TXN, 7) - .recordingScheduledTxn() - .alsoSigningWith(sender) - .sigControl(ControlForKey.forKey(senderKey, sigOne)), - getAccountBalance(receiver).hasTinyBars(0L), - scheduleSign(schedule) - .alsoSigningWith(NEW_SENDER_KEY) - .sigControl(forKey(NEW_SENDER_KEY, firstSigThree)), - getAccountBalance(receiver).hasTinyBars(0L), - cryptoUpdate(sender).key(NEW_SENDER_KEY), - scheduleSign(schedule).hasKnownStatus(NO_NEW_VALID_SIGNATURES), - getAccountBalance(receiver).hasTinyBars(0L), - getScheduleInfo(schedule) - .hasScheduleId(schedule) - .hasWaitForExpiry() - .isNotExecuted() - .isNotDeleted() - .hasRelativeExpiry(SENDER_TXN, 7) - .hasRecordedScheduledTxn(), - triggerSchedule(schedule, 8), - scheduleSign(schedule) - .alsoSigningWith(NEW_SENDER_KEY) - .sigControl(forKey(NEW_SENDER_KEY, sigTwo)) - .hasKnownStatus(INVALID_SCHEDULE_ID), - getAccountBalance(receiver).hasTinyBars(1L))); - } - - @HapiTest - @Order(2) - final Stream reductionInSigningReqsAllowsTxnToGoThroughAtExpiryWithWaitForExpiry() { - var senderShape = threshOf(2, threshOf(1, 3), threshOf(1, 3), threshOf(2, 3)); - var sigOne = senderShape.signedWith(sigs(sigs(OFF, OFF, ON), sigs(OFF, OFF, OFF), sigs(OFF, OFF, OFF))); - var firstSigThree = senderShape.signedWith(sigs(sigs(OFF, OFF, OFF), sigs(OFF, OFF, OFF), sigs(ON, OFF, OFF))); - String sender = "X"; - String receiver = "Y"; - String schedule = "Z"; - String senderKey = "sKey"; - - return hapiTest(flattened( - newKeyNamed(senderKey).shape(senderShape), - keyFromMutation(NEW_SENDER_KEY, senderKey).changing(this::lowerThirdNestedThresholdSigningReq), - cryptoCreate(sender).key(senderKey).via(SENDER_TXN), - cryptoCreate(receiver).balance(0L), - scheduleCreate(schedule, cryptoTransfer(tinyBarsFromTo(sender, receiver, 1))) - .payingWith(DEFAULT_PAYER) - .waitForExpiry() - .withRelativeExpiry(SENDER_TXN, 4) - .recordingScheduledTxn() - .alsoSigningWith(sender) - .sigControl(ControlForKey.forKey(senderKey, sigOne)), - getAccountBalance(receiver).hasTinyBars(0L), - scheduleSign(schedule) - .alsoSigningWith(NEW_SENDER_KEY) - .sigControl(forKey(NEW_SENDER_KEY, firstSigThree)), - getAccountBalance(receiver).hasTinyBars(0L), - cryptoUpdate(sender).key(NEW_SENDER_KEY), - getAccountBalance(receiver).hasTinyBars(0L), - getScheduleInfo(schedule) - .hasScheduleId(schedule) - .hasWaitForExpiry(true) - .isNotExecuted() - .isNotDeleted() - .hasRelativeExpiry(SENDER_TXN, 4) - .hasRecordedScheduledTxn(), - triggerSchedule(schedule), - getAccountBalance(receiver).hasTinyBars(1L))); - } - - @HapiTest - @Order(3) - final Stream nestedSigningReqsWorkAsExpected() { - var senderShape = threshOf(2, threshOf(1, 3), threshOf(1, 3), threshOf(1, 3)); - var sigOne = senderShape.signedWith(sigs(sigs(OFF, OFF, ON), sigs(OFF, OFF, OFF), sigs(OFF, OFF, OFF))); - var sigTwo = senderShape.signedWith(sigs(sigs(OFF, OFF, OFF), sigs(OFF, ON, OFF), sigs(OFF, OFF, OFF))); - String sender = "X"; - String receiver = "Y"; - String schedule = "Z"; - String senderKey = "sKey"; - - return hapiTest(flattened( - newKeyNamed(senderKey).shape(senderShape), - cryptoCreate(sender).key(senderKey).via(SENDER_TXN), - cryptoCreate(receiver).balance(0L), - scheduleCreate(schedule, cryptoTransfer(tinyBarsFromTo(sender, receiver, 1))) - .payingWith(DEFAULT_PAYER) - .waitForExpiry() - .withRelativeExpiry(SENDER_TXN, 4) - .recordingScheduledTxn() - .alsoSigningWith(sender) - .sigControl(ControlForKey.forKey(senderKey, sigOne)), - getAccountBalance(receiver).hasTinyBars(0L), - scheduleSign(schedule).alsoSigningWith(senderKey).sigControl(forKey(senderKey, sigTwo)), - getAccountBalance(receiver).hasTinyBars(0L), - getScheduleInfo(schedule) - .hasScheduleId(schedule) - .hasWaitForExpiry() - .isNotExecuted() - .isNotDeleted() - .hasRelativeExpiry(SENDER_TXN, 4) - .hasRecordedScheduledTxn(), - triggerSchedule(schedule), - getAccountBalance(receiver).hasTinyBars(1L))); - } - - @HapiTest - @Order(4) - final Stream receiverSigRequiredNotConfusedByOrder() { - var senderShape = threshOf(1, 3); - var sigOne = senderShape.signedWith(sigs(ON, OFF, OFF)); - String sender = "X"; - String receiver = "Y"; - String schedule = "Z"; - String senderKey = "sKey"; - - return hapiTest(flattened( - newKeyNamed(senderKey).shape(senderShape), - cryptoCreate(sender).key(senderKey).via(SENDER_TXN), - cryptoCreate(receiver).balance(0L).receiverSigRequired(true), - scheduleCreate(schedule, cryptoTransfer(tinyBarsFromTo(sender, receiver, 1))) - .waitForExpiry() - .withRelativeExpiry(SENDER_TXN, 4) - .recordingScheduledTxn() - .payingWith(DEFAULT_PAYER), - scheduleSign(schedule).alsoSigningWith(receiver), - getAccountBalance(receiver).hasTinyBars(0L), - scheduleSign(schedule).alsoSigningWith(senderKey).sigControl(forKey(senderKey, sigOne)), - getAccountBalance(receiver).hasTinyBars(0L), - getScheduleInfo(schedule) - .hasScheduleId(schedule) - .hasWaitForExpiry() - .isNotExecuted() - .isNotDeleted() - .hasRelativeExpiry(SENDER_TXN, 4) - .hasRecordedScheduledTxn(), - triggerSchedule(schedule), - getAccountBalance(receiver).hasTinyBars(1L))); - } - - @HapiTest - @Order(5) - final Stream extraSigsDontMatterAtExpiry() { - var senderShape = threshOf(1, 3); - var sigOne = senderShape.signedWith(sigs(ON, OFF, OFF)); - var sigTwo = senderShape.signedWith(sigs(OFF, ON, OFF)); - var sigThree = senderShape.signedWith(sigs(OFF, OFF, ON)); - String sender = "X"; - String receiver = "Y"; - String schedule = "Z"; - String senderKey = "sKey"; - - return hapiTest(flattened( - cryptoCreate(PAYER).balance(ONE_MILLION_HBARS).payingWith(GENESIS), - newKeyNamed(senderKey).shape(senderShape), - newKeyNamed(EXTRA_KEY), - cryptoCreate(sender).key(senderKey).via(SENDER_TXN), - cryptoCreate(receiver).balance(0L).receiverSigRequired(true), - scheduleCreate(schedule, cryptoTransfer(tinyBarsFromTo(sender, receiver, 1))) - .waitForExpiry() - .withRelativeExpiry(SENDER_TXN, 10) - .recordingScheduledTxn() - .payingWith(PAYER), - scheduleSign(schedule).payingWith(PAYER).fee(THOUSAND_HBAR).alsoSigningWith(receiver), - getAccountBalance(receiver).hasTinyBars(0L), - scheduleSign(schedule) - .payingWith(PAYER) - .fee(THOUSAND_HBAR) - .alsoSigningWith(senderKey) - .sigControl(forKey(senderKey, sigOne)), - getAccountBalance(receiver).hasTinyBars(0L), - scheduleSign(schedule) - .payingWith(PAYER) - .fee(THOUSAND_HBAR) - .alsoSigningWith(senderKey) - .sigControl(forKey(senderKey, sigTwo)), - getAccountBalance(receiver).hasTinyBars(0L), - scheduleSign(schedule) - .payingWith(PAYER) - .fee(THOUSAND_HBAR) - .alsoSigningWith(EXTRA_KEY) - .hasKnownStatus(NO_NEW_VALID_SIGNATURES), - getAccountBalance(receiver).hasTinyBars(0L), - scheduleSign(schedule) - .payingWith(PAYER) - .fee(THOUSAND_HBAR) - .alsoSigningWith(senderKey) - .sigControl(forKey(senderKey, sigTwo)) - .hasKnownStatus(NO_NEW_VALID_SIGNATURES), - getAccountBalance(receiver).hasTinyBars(0L), - scheduleSign(schedule) - .payingWith(PAYER) - .fee(THOUSAND_HBAR) - .alsoSigningWith(senderKey) - .sigControl(forKey(senderKey, sigThree)), - getAccountBalance(receiver).hasTinyBars(0L), - scheduleSign(schedule) - .payingWith(PAYER) - .fee(THOUSAND_HBAR) - .alsoSigningWith(senderKey) - .sigControl(forKey(senderKey, sigTwo)) - .hasKnownStatus(NO_NEW_VALID_SIGNATURES), - getAccountBalance(receiver).hasTinyBars(0L), - scheduleSign(schedule) - .payingWith(PAYER) - .fee(THOUSAND_HBAR) - .alsoSigningWith(EXTRA_KEY) - .hasKnownStatus(NO_NEW_VALID_SIGNATURES), - getAccountBalance(receiver).hasTinyBars(0L), - scheduleSign(schedule) - .payingWith(PAYER) - .fee(THOUSAND_HBAR) - .alsoSigningWith(senderKey) - .sigControl(forKey(senderKey, sigOne)) - .hasKnownStatus(NO_NEW_VALID_SIGNATURES), - getAccountBalance(receiver).hasTinyBars(0L), - getScheduleInfo(schedule) - .hasScheduleId(schedule) - .hasWaitForExpiry() - .isNotExecuted() - .isNotDeleted() - .hasRelativeExpiry(SENDER_TXN, 10) - .hasRecordedScheduledTxn(), - triggerSchedule(schedule, 11), - getAccountBalance(receiver).hasTinyBars(1L))); - } - - @HapiTest - @Order(7) - final Stream basicSignatureCollectionWorks() { - var txnBody = cryptoTransfer(tinyBarsFromTo(SENDER, RECEIVER, 1)); - - return hapiTest( - cryptoCreate(SENDER).via(SENDER_TXN), - cryptoCreate(RECEIVER).receiverSigRequired(true), - scheduleCreate(BASIC_XFER, txnBody) - .waitForExpiry() - .withRelativeExpiry(SENDER_TXN, 4) - .payingWith(SENDER), - scheduleSign(BASIC_XFER).alsoSigningWith(RECEIVER), - getScheduleInfo(BASIC_XFER).hasSignatories(RECEIVER, SENDER), - // note: the sleepFor and cryptoCreate operations are added only to clear the schedule before - // the next state. This was needed because an edge case in the BaseTranslator occur. - // When scheduleCreate trigger the schedules execution scheduleRef field is not the correct one. - sleepFor(6000), - cryptoCreate("foo")); - } - - @HapiTest - @Order(8) - final Stream signalsIrrelevantSig() { - var txnBody = cryptoTransfer(tinyBarsFromTo(SENDER, RECEIVER, 1)); - - return hapiTest( - cryptoCreate(SENDER).via(SENDER_TXN), - cryptoCreate(RECEIVER), - newKeyNamed("somebodyelse"), - scheduleCreate(BASIC_XFER, txnBody).waitForExpiry().withRelativeExpiry(SENDER_TXN, 4), - scheduleSign(BASIC_XFER) - .alsoSigningWith("somebodyelse") - .hasKnownStatusFrom(NO_NEW_VALID_SIGNATURES, SOME_SIGNATURES_WERE_INVALID), - // note: the sleepFor and cryptoCreate operations are added only to clear the schedule before - // the next state. This was needed because an edge case in the BaseTranslator occur. - // When scheduleCreate trigger the schedules execution scheduleRef field is not the correct one. - sleepFor(6000), - cryptoCreate("foo")); - } - - @HapiTest - @Order(9) - final Stream signalsIrrelevantSigEvenAfterLinkedEntityUpdate() { - var txnBody = mintToken(TOKEN_A, 50000000L); - - return hapiTest( - newKeyNamed(ADMIN), - newKeyNamed("mint"), - newKeyNamed("newMint"), - tokenCreate(TOKEN_A).adminKey(ADMIN).supplyKey("mint").via(CREATE_TXN), - scheduleCreate("tokenMintScheduled", txnBody).waitForExpiry().withRelativeExpiry(CREATE_TXN, 4), - tokenUpdate(TOKEN_A).supplyKey("newMint"), - scheduleSign("tokenMintScheduled") - .alsoSigningWith("mint") - /* In the rare, but possible, case that the the mint and newMint keys overlap - * in their first byte (and that byte is not shared by the DEFAULT_PAYER), - * we will get SOME_SIGNATURES_WERE_INVALID instead of NO_NEW_VALID_SIGNATURES. - * - * So we need this to stabilize CI. But if just testing locally, you may - * only use .hasKnownStatus(NO_NEW_VALID_SIGNATURES) and it will pass - * >99.99% of the time. */ - .hasKnownStatusFrom(NO_NEW_VALID_SIGNATURES, SOME_SIGNATURES_WERE_INVALID), - - // note: the sleepFor and cryptoCreate operations are added only to clear the schedule before - // the next state. This was needed because an edge case in the BaseTranslator occur. - // When scheduleCreate trigger the schedules execution scheduleRef field is not the correct one. - sleepFor(6000), - cryptoCreate("foo")); - } - - @HapiTest - @Order(10) - public Stream triggersUponFinishingPayerSig() { - return hapiTest(flattened( - cryptoCreate(PAYER).balance(ONE_HBAR), - cryptoCreate(SENDER).balance(1L).via(SENDER_TXN), - cryptoCreate(RECEIVER).balance(0L).receiverSigRequired(true), - scheduleCreate( - THREE_SIG_XFER, - cryptoTransfer(tinyBarsFromTo(SENDER, RECEIVER, 1)) - .fee(ONE_HBAR)) - .designatingPayer(PAYER) - .waitForExpiry() - .withRelativeExpiry(SENDER_TXN, 4) - .recordingScheduledTxn() - .alsoSigningWith(SENDER, RECEIVER), - getAccountBalance(RECEIVER).hasTinyBars(0L), - scheduleSign(THREE_SIG_XFER).alsoSigningWith(PAYER), - getAccountBalance(RECEIVER).hasTinyBars(0L), - getScheduleInfo(THREE_SIG_XFER) - .hasScheduleId(THREE_SIG_XFER) - .hasWaitForExpiry() - .isNotExecuted() - .isNotDeleted() - .hasRelativeExpiry(SENDER_TXN, 4) - .hasRecordedScheduledTxn(), - triggerSchedule(THREE_SIG_XFER), - getAccountBalance(RECEIVER).hasTinyBars(1L))); - } - - @HapiTest - @Order(11) - public Stream triggersUponAdditionalNeededSig() { - return hapiTest(flattened( - cryptoCreate(SENDER).balance(1L).via(SENDER_TXN), - cryptoCreate(RECEIVER).balance(0L).receiverSigRequired(true), - scheduleCreate( - TWO_SIG_XFER, - cryptoTransfer(tinyBarsFromTo(SENDER, RECEIVER, 1)) - .fee(ONE_HBAR)) - .waitForExpiry() - .withRelativeExpiry(SENDER_TXN, 4) - .recordingScheduledTxn() - .alsoSigningWith(SENDER), - getAccountBalance(RECEIVER).hasTinyBars(0L), - scheduleSign(TWO_SIG_XFER).alsoSigningWith(RECEIVER), - getAccountBalance(RECEIVER).hasTinyBars(0L), - getScheduleInfo(TWO_SIG_XFER) - .hasScheduleId(TWO_SIG_XFER) - .hasWaitForExpiry() - .isNotExecuted() - .isNotDeleted() - .hasRelativeExpiry(SENDER_TXN, 4) - .hasRecordedScheduledTxn(), - triggerSchedule(TWO_SIG_XFER), - getAccountBalance(RECEIVER).hasTinyBars(1L))); - } - - @HapiTest - @Order(12) - public Stream sharedKeyWorksAsExpected() { - return hapiTest(flattened( - newKeyNamed(SHARED_KEY), - cryptoCreate("payerWithSharedKey").key(SHARED_KEY).via(CREATE_TXN), - scheduleCreate( - DEFERRED_CREATION, - cryptoCreate("yetToBe") - .signedBy() - .receiverSigRequired(true) - .key(SHARED_KEY) - .balance(123L) - .fee(ONE_HBAR)) - .waitForExpiry() - .withRelativeExpiry(CREATE_TXN, 4) - .recordingScheduledTxn() - .payingWith("payerWithSharedKey") - .via(CREATION), - getTxnRecord(CREATION).scheduled().hasAnswerOnlyPrecheck(RECORD_NOT_FOUND), - getScheduleInfo(DEFERRED_CREATION) - .hasScheduleId(DEFERRED_CREATION) - .hasWaitForExpiry() - .isNotExecuted() - .isNotDeleted() - .hasRelativeExpiry(CREATE_TXN, 4) - .hasRecordedScheduledTxn(), - triggerSchedule(DEFERRED_CREATION), - getTxnRecord(CREATION).scheduled())); - } - - @HapiTest - @Order(13) - public Stream overlappingKeysTreatedAsExpected() { - var keyGen = OverlappingKeyGenerator.withAtLeastOneOverlappingByte(2); - final long scheduleLifetime = 6; - - return hapiTest(flattened( - newKeyNamed("aKey").generator(keyGen), - newKeyNamed("bKey").generator(keyGen), - newKeyNamed("cKey"), - cryptoCreate("aSender").key("aKey").balance(1L).via(SENDER_TXN), - cryptoCreate("cSender").key("cKey").balance(1L), - balanceSnapshot(BEFORE, ADDRESS_BOOK_CONTROL), - scheduleCreate( - DEFERRED_XFER, - cryptoTransfer( - tinyBarsFromTo("aSender", ADDRESS_BOOK_CONTROL, 1), - tinyBarsFromTo("cSender", ADDRESS_BOOK_CONTROL, 1))) - .waitForExpiry() - .withRelativeExpiry(SENDER_TXN, scheduleLifetime) - .recordingScheduledTxn(), - scheduleSign(DEFERRED_XFER).alsoSigningWith("aKey"), - scheduleSign(DEFERRED_XFER).alsoSigningWith("aKey").hasKnownStatus(NO_NEW_VALID_SIGNATURES), - scheduleSign(DEFERRED_XFER).alsoSigningWith("aKey", "bKey").hasKnownStatus(NO_NEW_VALID_SIGNATURES), - scheduleSign(DEFERRED_XFER) - .alsoSigningWith("bKey") - /* In the rare, but possible, case that the overlapping byte shared by aKey - * and bKey is _also_ shared by the DEFAULT_PAYER, the bKey prefix in the sig - * map will probably not collide with aKey any more, and we will get - * NO_NEW_VALID_SIGNATURES instead of SOME_SIGNATURES_WERE_INVALID. - * - * So we need this to stabilize CI. But if just testing locally, you may - * only use .hasKnownStatus(SOME_SIGNATURES_WERE_INVALID) and it will pass - * >99.99% of the time. */ - .hasKnownStatusFrom(SOME_SIGNATURES_WERE_INVALID, NO_NEW_VALID_SIGNATURES), - scheduleSign(DEFERRED_XFER).alsoSigningWith("aKey", "bKey", "cKey"), - getAccountBalance(ADDRESS_BOOK_CONTROL).hasTinyBars(changeFromSnapshot(BEFORE, 0)), - getScheduleInfo(DEFERRED_XFER) - .hasScheduleId(DEFERRED_XFER) - .hasWaitForExpiry() - .isNotExecuted() - .isNotDeleted() - .hasRelativeExpiry(SENDER_TXN, scheduleLifetime) - .hasRecordedScheduledTxn(), - triggerSchedule(DEFERRED_XFER, 7), - getAccountBalance(ADDRESS_BOOK_CONTROL).hasTinyBars(changeFromSnapshot(BEFORE, +2)))); - } - - @HapiTest - @Order(14) - public Stream retestsActivationOnSignWithEmptySigMap() { - return hapiTest(flattened( - newKeyNamed("a"), - newKeyNamed("b"), - newKeyListNamed("ab", List.of("a", "b")), - newKeyNamed(ADMIN), - cryptoCreate(SENDER).key("ab").balance(667L).via(SENDER_TXN), - scheduleCreate( - DEFERRED_FALL, - cryptoTransfer(tinyBarsFromTo(SENDER, FUNDING, 1)) - .fee(ONE_HBAR)) - .waitForExpiry() - .withRelativeExpiry(SENDER_TXN, 4) - .recordingScheduledTxn() - .alsoSigningWith("a"), - getAccountBalance(SENDER).hasTinyBars(667L), - cryptoUpdate(SENDER).key("a"), - scheduleSign(DEFERRED_FALL).alsoSigningWith().hasKnownStatus(NO_NEW_VALID_SIGNATURES), - getAccountBalance(SENDER).hasTinyBars(667L), - getScheduleInfo(DEFERRED_FALL) - .hasScheduleId(DEFERRED_FALL) - .hasWaitForExpiry() - .isNotExecuted() - .isNotDeleted() - .hasRelativeExpiry(SENDER_TXN, 4) - .hasRecordedScheduledTxn(), - triggerSchedule(DEFERRED_FALL), - getAccountBalance(SENDER).hasTinyBars(666L))); - } - - @HapiTest - @Order(15) final Stream scheduleSignWhenAllSigPresent() { return hapiTest( cryptoCreate("receiver").balance(0L).receiverSigRequired(true), @@ -594,7 +70,6 @@ final Stream scheduleSignWhenAllSigPresent() { } @HapiTest - @Order(16) final Stream scheduleSignWhenAllSigPresentNoWaitForExpiry() { return hapiTest( cryptoCreate("receiver").balance(0L).receiverSigRequired(true), @@ -605,7 +80,6 @@ final Stream scheduleSignWhenAllSigPresentNoWaitForExpiry() { } @HapiTest - @Order(18) final Stream scheduledTransactionWithWaitForExpiryFalseLessThen30Mins() { final var schedule = "s"; return hapiTest( @@ -621,7 +95,6 @@ final Stream scheduledTransactionWithWaitForExpiryFalseLessThen30Mi } @HapiTest - @Order(19) final Stream scheduledTransactionWithWaitForExpiryFalseMoreThen30Mins() { final var schedule = "s"; return hapiTest( @@ -637,7 +110,6 @@ final Stream scheduledTransactionWithWaitForExpiryFalseMoreThen30Mi } @HapiTest - @Order(20) final Stream scheduledTriggeredWhenAllKeysHaveSigned() { final var schedule = "s"; @@ -664,7 +136,6 @@ final Stream scheduledTriggeredWhenAllKeysHaveSigned() { } @HapiTest - @Order(21) final Stream scheduleSignWithNotNeededSignature() { final var schedule = "s"; @@ -680,7 +151,6 @@ final Stream scheduleSignWithNotNeededSignature() { } @HapiTest - @Order(22) final Stream scheduleSignWithEmptyKey() { final var schedule = "s"; @@ -696,7 +166,6 @@ final Stream scheduleSignWithEmptyKey() { } @HapiTest - @Order(23) final Stream scheduleSignWithTwoSignatures() { final var schedule = "s"; @@ -713,13 +182,4 @@ final Stream scheduleSignWithTwoSignatures() { cryptoCreate("trigger"), getAccountBalance(RECEIVER).hasTinyBars(1L)); } - - private Key lowerThirdNestedThresholdSigningReq(Key source) { - var newKey = source.getThresholdKey().getKeys().getKeys(2).toBuilder(); - newKey.setThresholdKey(newKey.getThresholdKeyBuilder().setThreshold(1)); - var newKeyList = source.getThresholdKey().getKeys().toBuilder().setKeys(2, newKey); - return source.toBuilder() - .setThresholdKey(source.getThresholdKey().toBuilder().setKeys(newKeyList)) - .build(); - } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/integration/RepeatableHip423Tests.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/integration/RepeatableHip423Tests.java index faaaae70323d..fe61cf5ed6c5 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/integration/RepeatableHip423Tests.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/integration/RepeatableHip423Tests.java @@ -129,9 +129,9 @@ import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.TRIGGERING_TXN; import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.transferListCheck; import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.triggerSchedule; -import static com.hedera.services.bdd.suites.hip423.ScheduleLongTermExecutionTest.BASIC_XFER; -import static com.hedera.services.bdd.suites.hip423.ScheduleLongTermExecutionTest.CREATE_TX; -import static com.hedera.services.bdd.suites.hip423.ScheduleLongTermExecutionTest.SIGN_TX; +import static com.hedera.services.bdd.suites.integration.RepeatableScheduleLongTermExecutionTest.BASIC_XFER; +import static com.hedera.services.bdd.suites.integration.RepeatableScheduleLongTermExecutionTest.CREATE_TX; +import static com.hedera.services.bdd.suites.integration.RepeatableScheduleLongTermExecutionTest.SIGN_TX; import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; import static com.hederahashgraph.api.proto.java.HederaFunctionality.ConsensusCreateTopic; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_DELETED; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/integration/RepeatableRosterLifecycleRestartTests.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/integration/RepeatableRosterLifecycleRestartTests.java index e92ee121038c..70409f4f8719 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/integration/RepeatableRosterLifecycleRestartTests.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/integration/RepeatableRosterLifecycleRestartTests.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. @@ -30,7 +30,7 @@ import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Tag; -@Order(3) +@Order(6) @Tag(INTEGRATION) @TargetEmbeddedMode(REPEATABLE) public class RepeatableRosterLifecycleRestartTests { diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/integration/RepeatableScheduleLongTermExecutionTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/integration/RepeatableScheduleLongTermExecutionTest.java new file mode 100644 index 000000000000..d7abb5c4d4d4 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/integration/RepeatableScheduleLongTermExecutionTest.java @@ -0,0 +1,939 @@ +/* + * 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.integration; + +import static com.hedera.services.bdd.junit.RepeatableReason.NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION; +import static com.hedera.services.bdd.junit.TestTags.INTEGRATION; +import static com.hedera.services.bdd.junit.hedera.embedded.EmbeddedMode.REPEATABLE; +import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; +import static com.hedera.services.bdd.spec.assertions.TransactionRecordAsserts.recordWith; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getFileInfo; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getScheduleInfo; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTxnRecord; +import static com.hedera.services.bdd.spec.transactions.TxnUtils.asId; +import static com.hedera.services.bdd.spec.transactions.TxnUtils.randomUppercase; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoDelete; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoTransfer; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.fileCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.fileUpdate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.scheduleCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.scheduleSign; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.systemFileDelete; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.uploadInitCode; +import static com.hedera.services.bdd.spec.transactions.crypto.HapiCryptoTransfer.tinyBarsFromTo; +import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.freezeAbort; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.recordFeeAmount; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.uploadScheduledContractPrices; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; +import static com.hedera.services.bdd.suites.HapiSuite.DEFAULT_PAYER; +import static com.hedera.services.bdd.suites.HapiSuite.FREEZE_ADMIN; +import static com.hedera.services.bdd.suites.HapiSuite.GENESIS; +import static com.hedera.services.bdd.suites.HapiSuite.SYSTEM_ADMIN; +import static com.hedera.services.bdd.suites.HapiSuite.SYSTEM_DELETE_ADMIN; +import static com.hedera.services.bdd.suites.HapiSuite.THREE_MONTHS_IN_SECONDS; +import static com.hedera.services.bdd.suites.HapiSuite.flattened; +import static com.hedera.services.bdd.suites.freeze.UpgradeSuite.standardUpdateFile; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.ORIG_FILE; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.PAYING_ACCOUNT_2; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.SCHEDULED_TRANSACTION_MUST_NOT_SUCCEED; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.SCHEDULE_CREATE_FEE; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.SENDER_1; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.SENDER_2; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.SENDER_3; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.SIMPLE_UPDATE; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.SUCCESS_TXN; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.TRANSACTION_NOT_SCHEDULED; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.TRIGGERING_TXN; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.VALID_SCHEDULE; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.WEIRDLY_POPULAR_KEY; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.WRONG_CONSENSUS_TIMESTAMP; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.WRONG_RECORD_ACCOUNT_ID; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.WRONG_SCHEDULE_ID; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.WRONG_TRANSACTION_VALID_START; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.WRONG_TRANSFER_LIST; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.scheduleFakeUpgrade; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.transferListCheck; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.triggerSchedule; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_DELETED; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.AUTHORIZATION_FAILED; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_ACCOUNT_BALANCE; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_PAYER_BALANCE; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_FILE_ID; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_PAYER_SIGNATURE; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_SCHEDULE_ID; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.PAYER_ACCOUNT_DELETED; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; + +import com.hedera.services.bdd.junit.HapiTestLifecycle; +import com.hedera.services.bdd.junit.RepeatableHapiTest; +import com.hedera.services.bdd.junit.TargetEmbeddedMode; +import com.hedera.services.bdd.junit.support.TestLifecycle; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.math.BigInteger; +import java.time.Instant; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Stream; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Tag; + +@Order(4) +@Tag(INTEGRATION) +@HapiTestLifecycle +@TargetEmbeddedMode(REPEATABLE) +public class RepeatableScheduleLongTermExecutionTest { + private static final String PAYING_ACCOUNT = "payingAccount"; + private static final String RECEIVER = "receiver"; + private static final String SENDER = "sender"; + private static final String SENDER_TXN = "senderTxn"; + private static final String PAYING_ACCOUNT_TXN = "payingAccountTxn"; + private static final String LUCKY_RECEIVER = "luckyReceiver"; + private static final String FAILED_XFER = "failedXfer"; + private static final String WEIRDLY_POPULAR_KEY_TXN = "weirdlyPopularKeyTxn"; + private static final String PAYER_TXN = "payerTxn"; + private static final String FILE_NAME = "misc"; + private static final long PAYER_INITIAL_BALANCE = 1000000000000L; + + public static final String BASIC_XFER = "basicXfer"; + public static final String CREATE_TX = "createTxn"; + public static final String SIGN_TX = "sign_tx"; + + @BeforeAll + static void beforeAll(@NonNull final TestLifecycle lifecycle) { + // override and preserve old values + lifecycle.overrideInClass(Map.of( + "scheduling.longTermEnabled", + "true", + "scheduling.whitelist", + "ConsensusSubmitMessage,CryptoTransfer,TokenMint,TokenBurn," + + "CryptoCreate,CryptoUpdate,FileUpdate,SystemDelete,SystemUndelete," + + "Freeze,ContractCall,ContractCreate,ContractUpdate,ContractDelete")); + } + + @SuppressWarnings("java:S5960") + @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) + final Stream executionWithCustomPayerWorks() { + return hapiTest(flattened( + cryptoCreate(PAYING_ACCOUNT), + cryptoCreate(RECEIVER), + cryptoCreate(SENDER).via(SENDER_TXN), + scheduleCreate(BASIC_XFER, cryptoTransfer(tinyBarsFromTo(SENDER, RECEIVER, 1))) + .designatingPayer(PAYING_ACCOUNT) + .waitForExpiry() + .withRelativeExpiry(SENDER_TXN, 4) + .recordingScheduledTxn() + .via(CREATE_TX), + scheduleSign(BASIC_XFER) + .alsoSigningWith(SENDER, PAYING_ACCOUNT) + .via(SIGN_TX) + .hasKnownStatus(SUCCESS), + getScheduleInfo(BASIC_XFER) + .hasScheduleId(BASIC_XFER) + .hasWaitForExpiry() + .isNotExecuted() + .isNotDeleted() + .hasRelativeExpiry(SENDER_TXN, 4) + .hasRecordedScheduledTxn(), + triggerSchedule(BASIC_XFER), + withOpContext((spec, opLog) -> { + var createTx = getTxnRecord(CREATE_TX); + var signTx = getTxnRecord(SIGN_TX); + var triggeringTx = getTxnRecord(TRIGGERING_TXN); + var triggeredTx = getTxnRecord(CREATE_TX).scheduled(); + allRunFor(spec, createTx, signTx, triggeredTx, triggeringTx); + + Assertions.assertEquals( + SUCCESS, + triggeredTx.getResponseRecord().getReceipt().getStatus(), + SCHEDULED_TRANSACTION_MUST_NOT_SUCCEED); + + Instant triggerTime = Instant.ofEpochSecond( + triggeringTx + .getResponseRecord() + .getConsensusTimestamp() + .getSeconds(), + triggeringTx + .getResponseRecord() + .getConsensusTimestamp() + .getNanos()); + + Instant triggeredTime = Instant.ofEpochSecond( + triggeredTx + .getResponseRecord() + .getConsensusTimestamp() + .getSeconds(), + triggeredTx + .getResponseRecord() + .getConsensusTimestamp() + .getNanos()); + + Assertions.assertTrue(triggerTime.isBefore(triggeredTime), WRONG_CONSENSUS_TIMESTAMP); + + Assertions.assertEquals( + createTx.getResponseRecord().getTransactionID().getTransactionValidStart(), + triggeredTx.getResponseRecord().getTransactionID().getTransactionValidStart(), + WRONG_TRANSACTION_VALID_START); + + Assertions.assertEquals( + createTx.getResponseRecord().getTransactionID().getAccountID(), + triggeredTx.getResponseRecord().getTransactionID().getAccountID(), + WRONG_RECORD_ACCOUNT_ID); + + Assertions.assertTrue( + triggeredTx.getResponseRecord().getTransactionID().getScheduled(), + TRANSACTION_NOT_SCHEDULED); + + Assertions.assertEquals( + createTx.getResponseRecord().getReceipt().getScheduleID(), + triggeredTx.getResponseRecord().getScheduleRef(), + WRONG_SCHEDULE_ID); + + Assertions.assertTrue( + transferListCheck( + triggeredTx, + asId(SENDER, spec), + asId(RECEIVER, spec), + asId(PAYING_ACCOUNT, spec), + 1L), + WRONG_TRANSFER_LIST); + }))); + } + + @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) + final Stream executionWithCustomPayerAndAdminKeyWorks() { + return hapiTest(flattened( + newKeyNamed("adminKey"), + cryptoCreate(PAYING_ACCOUNT), + cryptoCreate(RECEIVER), + cryptoCreate(SENDER).via(SENDER_TXN), + scheduleCreate(BASIC_XFER, cryptoTransfer(tinyBarsFromTo(SENDER, RECEIVER, 1))) + .designatingPayer(PAYING_ACCOUNT) + .adminKey("adminKey") + .waitForExpiry() + .withRelativeExpiry(SENDER_TXN, 4) + .recordingScheduledTxn() + .via(CREATE_TX), + scheduleSign(BASIC_XFER) + .alsoSigningWith(SENDER, PAYING_ACCOUNT) + .via(SIGN_TX) + .hasKnownStatus(SUCCESS), + getScheduleInfo(BASIC_XFER) + .hasScheduleId(BASIC_XFER) + .hasWaitForExpiry() + .isNotExecuted() + .isNotDeleted() + .hasRelativeExpiry(SENDER_TXN, 4) + .hasRecordedScheduledTxn(), + triggerSchedule(BASIC_XFER), + withOpContext((spec, opLog) -> { + var createTx = getTxnRecord(CREATE_TX); + var signTx = getTxnRecord(SIGN_TX); + var triggeringTx = getTxnRecord(TRIGGERING_TXN); + var triggeredTx = getTxnRecord(CREATE_TX).scheduled(); + allRunFor(spec, createTx, signTx, triggeredTx, triggeringTx); + Assertions.assertEquals( + SUCCESS, + triggeredTx.getResponseRecord().getReceipt().getStatus(), + SCHEDULED_TRANSACTION_MUST_NOT_SUCCEED); + + Instant triggerTime = Instant.ofEpochSecond( + triggeringTx + .getResponseRecord() + .getConsensusTimestamp() + .getSeconds(), + triggeringTx + .getResponseRecord() + .getConsensusTimestamp() + .getNanos()); + + Instant triggeredTime = Instant.ofEpochSecond( + triggeredTx + .getResponseRecord() + .getConsensusTimestamp() + .getSeconds(), + triggeredTx + .getResponseRecord() + .getConsensusTimestamp() + .getNanos()); + + Assertions.assertTrue(triggerTime.isBefore(triggeredTime), WRONG_CONSENSUS_TIMESTAMP); + + Assertions.assertEquals( + createTx.getResponseRecord().getTransactionID().getTransactionValidStart(), + triggeredTx.getResponseRecord().getTransactionID().getTransactionValidStart(), + WRONG_TRANSACTION_VALID_START); + + Assertions.assertEquals( + createTx.getResponseRecord().getTransactionID().getAccountID(), + triggeredTx.getResponseRecord().getTransactionID().getAccountID(), + WRONG_RECORD_ACCOUNT_ID); + + Assertions.assertTrue( + triggeredTx.getResponseRecord().getTransactionID().getScheduled(), + TRANSACTION_NOT_SCHEDULED); + + Assertions.assertEquals( + createTx.getResponseRecord().getReceipt().getScheduleID(), + triggeredTx.getResponseRecord().getScheduleRef(), + WRONG_SCHEDULE_ID); + + Assertions.assertTrue( + transferListCheck( + triggeredTx, + asId(SENDER, spec), + asId(RECEIVER, spec), + asId(PAYING_ACCOUNT, spec), + 1L), + WRONG_TRANSFER_LIST); + }))); + } + + @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) + final Stream executionWithCustomPayerWhoSignsAtCreationAsPayerWorks() { + return hapiTest(flattened( + cryptoCreate(PAYING_ACCOUNT), + cryptoCreate(RECEIVER), + cryptoCreate(SENDER).via(SENDER_TXN), + scheduleCreate(BASIC_XFER, cryptoTransfer(tinyBarsFromTo(SENDER, RECEIVER, 1))) + .payingWith(PAYING_ACCOUNT) + .designatingPayer(PAYING_ACCOUNT) + .waitForExpiry() + .withRelativeExpiry(SENDER_TXN, 4) + .recordingScheduledTxn() + .via(CREATE_TX), + scheduleSign(BASIC_XFER).alsoSigningWith(SENDER).via(SIGN_TX).hasKnownStatus(SUCCESS), + getScheduleInfo(BASIC_XFER) + .hasScheduleId(BASIC_XFER) + .hasWaitForExpiry() + .isNotExecuted() + .isNotDeleted() + .hasRelativeExpiry(SENDER_TXN, 4) + .hasRecordedScheduledTxn(), + triggerSchedule(BASIC_XFER), + withOpContext((spec, opLog) -> { + var createTx = getTxnRecord(CREATE_TX); + var signTx = getTxnRecord(SIGN_TX); + var triggeringTx = getTxnRecord(TRIGGERING_TXN); + var triggeredTx = getTxnRecord(CREATE_TX).scheduled(); + allRunFor(spec, createTx, signTx, triggeredTx, triggeringTx); + + Assertions.assertEquals( + SUCCESS, + triggeredTx.getResponseRecord().getReceipt().getStatus(), + SCHEDULED_TRANSACTION_MUST_NOT_SUCCEED); + + Instant triggerTime = Instant.ofEpochSecond( + triggeringTx + .getResponseRecord() + .getConsensusTimestamp() + .getSeconds(), + triggeringTx + .getResponseRecord() + .getConsensusTimestamp() + .getNanos()); + + Instant triggeredTime = Instant.ofEpochSecond( + triggeredTx + .getResponseRecord() + .getConsensusTimestamp() + .getSeconds(), + triggeredTx + .getResponseRecord() + .getConsensusTimestamp() + .getNanos()); + + Assertions.assertTrue(triggerTime.isBefore(triggeredTime), WRONG_CONSENSUS_TIMESTAMP); + + Assertions.assertEquals( + createTx.getResponseRecord().getTransactionID().getTransactionValidStart(), + triggeredTx.getResponseRecord().getTransactionID().getTransactionValidStart(), + WRONG_TRANSACTION_VALID_START); + + Assertions.assertEquals( + createTx.getResponseRecord().getTransactionID().getAccountID(), + triggeredTx.getResponseRecord().getTransactionID().getAccountID(), + WRONG_RECORD_ACCOUNT_ID); + + Assertions.assertTrue( + triggeredTx.getResponseRecord().getTransactionID().getScheduled(), + TRANSACTION_NOT_SCHEDULED); + + Assertions.assertEquals( + createTx.getResponseRecord().getReceipt().getScheduleID(), + triggeredTx.getResponseRecord().getScheduleRef(), + WRONG_SCHEDULE_ID); + + Assertions.assertTrue( + transferListCheck( + triggeredTx, + asId(SENDER, spec), + asId(RECEIVER, spec), + asId(PAYING_ACCOUNT, spec), + 1L), + WRONG_TRANSFER_LIST); + }))); + } + + @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) + public Stream executionWithContractCallWorksAtExpiry() { + final var payerBalance = new AtomicLong(); + return hapiTest(flattened( + // upload fees for SCHEDULE_CREATE_CONTRACT_CALL + uploadScheduledContractPrices(GENESIS), + uploadInitCode(SIMPLE_UPDATE), + contractCreate(SIMPLE_UPDATE).gas(500_000L), + cryptoCreate(PAYING_ACCOUNT).balance(PAYER_INITIAL_BALANCE).via(PAYING_ACCOUNT_TXN), + scheduleCreate( + BASIC_XFER, + contractCall(SIMPLE_UPDATE, "set", BigInteger.valueOf(5), BigInteger.valueOf(42)) + .gas(300000L)) + .waitForExpiry() + .withRelativeExpiry(PAYING_ACCOUNT_TXN, 4) + .designatingPayer(PAYING_ACCOUNT) + .alsoSigningWith(PAYING_ACCOUNT) + .recordingScheduledTxn() + .via(CREATE_TX), + getScheduleInfo(BASIC_XFER) + .hasScheduleId(BASIC_XFER) + .hasWaitForExpiry() + .isNotExecuted() + .isNotDeleted() + .hasRelativeExpiry(PAYING_ACCOUNT_TXN, 4) + .hasRecordedScheduledTxn(), + triggerSchedule(BASIC_XFER), + getAccountBalance(PAYING_ACCOUNT) + .hasTinyBars(spec -> + bal -> bal < PAYER_INITIAL_BALANCE ? Optional.empty() : Optional.of("didnt change")) + .exposingBalanceTo(payerBalance::set), + withOpContext((spec, opLog) -> { + var triggeredTx = getTxnRecord(CREATE_TX).scheduled(); + allRunFor(spec, triggeredTx); + final var txnFee = triggeredTx.getResponseRecord().getTransactionFee(); + // check if only designating payer was charged + Assertions.assertEquals(PAYER_INITIAL_BALANCE, txnFee + payerBalance.get()); + + Assertions.assertEquals( + SUCCESS, + triggeredTx.getResponseRecord().getReceipt().getStatus(), + SCHEDULED_TRANSACTION_MUST_NOT_SUCCEED); + + Assertions.assertTrue(triggeredTx + .getResponseRecord() + .getContractCallResult() + .getContractCallResult() + .size() + >= 0); + }))); + } + + @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) + public Stream executionWithContractCreateWorksAtExpiry() { + final var payerBalance = new AtomicLong(); + return hapiTest(flattened( + uploadInitCode(SIMPLE_UPDATE), + cryptoCreate(PAYING_ACCOUNT).balance(PAYER_INITIAL_BALANCE).via(PAYING_ACCOUNT_TXN), + scheduleCreate( + BASIC_XFER, + contractCreate(SIMPLE_UPDATE).gas(500_000L).adminKey(PAYING_ACCOUNT)) + .waitForExpiry() + .withRelativeExpiry(PAYING_ACCOUNT_TXN, 4) + .designatingPayer(PAYING_ACCOUNT) + .alsoSigningWith(PAYING_ACCOUNT) + .recordingScheduledTxn() + .via(CREATE_TX), + getScheduleInfo(BASIC_XFER) + .hasScheduleId(BASIC_XFER) + .hasWaitForExpiry() + .isNotExecuted() + .isNotDeleted() + .hasRelativeExpiry(PAYING_ACCOUNT_TXN, 4) + .hasRecordedScheduledTxn(), + triggerSchedule(BASIC_XFER), + getAccountBalance(PAYING_ACCOUNT) + .hasTinyBars(spec -> + bal -> bal < PAYER_INITIAL_BALANCE ? Optional.empty() : Optional.of("didnt change")) + .exposingBalanceTo(payerBalance::set), + withOpContext((spec, opLog) -> { + var triggeredTx = getTxnRecord(CREATE_TX).scheduled(); + allRunFor(spec, triggeredTx); + final var txnFee = triggeredTx.getResponseRecord().getTransactionFee(); + // check if only designating payer was charged + Assertions.assertEquals(PAYER_INITIAL_BALANCE, txnFee + payerBalance.get()); + + Assertions.assertEquals( + SUCCESS, + triggeredTx.getResponseRecord().getReceipt().getStatus(), + SCHEDULED_TRANSACTION_MUST_NOT_SUCCEED); + + Assertions.assertTrue( + triggeredTx.getResponseRecord().getReceipt().hasContractID()); + + Assertions.assertTrue(triggeredTx + .getResponseRecord() + .getContractCreateResult() + .getContractCallResult() + .size() + >= 0); + }))); + } + + @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) + public Stream executionWithDefaultPayerButNoFundsFails() { + long balance = 10_000_000L; + long noBalance = 0L; + long transferAmount = 1L; + return hapiTest(flattened( + cryptoCreate(PAYING_ACCOUNT).balance(balance), + cryptoCreate(LUCKY_RECEIVER), + cryptoCreate(SENDER).balance(transferAmount).via(SENDER_TXN), + cryptoCreate(RECEIVER).balance(noBalance), + scheduleCreate(BASIC_XFER, cryptoTransfer(tinyBarsFromTo(SENDER, RECEIVER, transferAmount))) + .waitForExpiry() + .withRelativeExpiry(SENDER_TXN, 10) + .payingWith(PAYING_ACCOUNT) + .recordingScheduledTxn() + .via(CREATE_TX), + recordFeeAmount(CREATE_TX, SCHEDULE_CREATE_FEE), + cryptoTransfer(tinyBarsFromTo(PAYING_ACCOUNT, LUCKY_RECEIVER, (spec -> { + long scheduleCreateFee = spec.registry().getAmount(SCHEDULE_CREATE_FEE); + return balance - scheduleCreateFee; + }))), + getAccountBalance(PAYING_ACCOUNT).hasTinyBars(noBalance), + scheduleSign(BASIC_XFER).alsoSigningWith(SENDER).hasKnownStatus(SUCCESS), + getScheduleInfo(BASIC_XFER) + .hasScheduleId(BASIC_XFER) + .hasWaitForExpiry() + .isNotExecuted() + .isNotDeleted() + .hasRelativeExpiry(SENDER_TXN, 10) + .hasRecordedScheduledTxn(), + triggerSchedule(BASIC_XFER, 15), + getAccountBalance(SENDER).hasTinyBars(transferAmount), + getAccountBalance(RECEIVER).hasTinyBars(noBalance), + withOpContext((spec, opLog) -> { + var triggeredTx = getTxnRecord(CREATE_TX).scheduled(); + + allRunFor(spec, triggeredTx); + + Assertions.assertEquals( + INSUFFICIENT_PAYER_BALANCE, + triggeredTx.getResponseRecord().getReceipt().getStatus(), + SCHEDULED_TRANSACTION_MUST_NOT_SUCCEED); + }))); + } + + @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) + public Stream executionWithCustomPayerThatNeverSignsFails() { + long transferAmount = 1; + return hapiTest(flattened( + cryptoCreate(PAYING_ACCOUNT), + cryptoCreate(SENDER).via(SENDER_TXN), + cryptoCreate(RECEIVER), + scheduleCreate(BASIC_XFER, cryptoTransfer(tinyBarsFromTo(SENDER, RECEIVER, transferAmount))) + .waitForExpiry() + .withRelativeExpiry(SENDER_TXN, 4) + .recordingScheduledTxn() + .designatingPayer(PAYING_ACCOUNT) + .via(CREATE_TX), + scheduleSign(BASIC_XFER).alsoSigningWith(SENDER).via(SIGN_TX).hasKnownStatus(SUCCESS), + getScheduleInfo(BASIC_XFER) + .hasScheduleId(BASIC_XFER) + .hasWaitForExpiry() + .isNotExecuted() + .isNotDeleted() + .hasRelativeExpiry(SENDER_TXN, 4) + .hasRecordedScheduledTxn(), + triggerSchedule(BASIC_XFER), + getTxnRecord(CREATE_TX).scheduled().hasPriority(recordWith().status(INVALID_PAYER_SIGNATURE)))); + } + + @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) + public Stream executionWithCustomPayerButNoFundsFails() { + long balance = 0L; + long transferAmount = 1; + return hapiTest(flattened( + cryptoCreate(PAYING_ACCOUNT).balance(balance), + cryptoCreate(SENDER).via(SENDER_TXN), + cryptoCreate(RECEIVER), + scheduleCreate(BASIC_XFER, cryptoTransfer(tinyBarsFromTo(SENDER, RECEIVER, transferAmount))) + .waitForExpiry() + .withRelativeExpiry(SENDER_TXN, 4) + .recordingScheduledTxn() + .designatingPayer(PAYING_ACCOUNT) + .via(CREATE_TX), + scheduleSign(BASIC_XFER) + .alsoSigningWith(SENDER, PAYING_ACCOUNT) + .via(SIGN_TX) + .hasKnownStatus(SUCCESS), + getScheduleInfo(BASIC_XFER) + .hasScheduleId(BASIC_XFER) + .hasWaitForExpiry() + .isNotExecuted() + .isNotDeleted() + .hasRelativeExpiry(SENDER_TXN, 4) + .hasRecordedScheduledTxn(), + triggerSchedule(BASIC_XFER), + withOpContext((spec, opLog) -> { + var triggeredTx = getTxnRecord(CREATE_TX).scheduled(); + + allRunFor(spec, triggeredTx); + + Assertions.assertEquals( + INSUFFICIENT_PAYER_BALANCE, + triggeredTx.getResponseRecord().getReceipt().getStatus(), + SCHEDULED_TRANSACTION_MUST_NOT_SUCCEED); + }))); + } + + @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) + public Stream executionWithDefaultPayerButAccountDeletedFails() { + long balance = 10_000_000L; + long noBalance = 0L; + long transferAmount = 1L; + return hapiTest(flattened( + cryptoCreate(PAYING_ACCOUNT).balance(balance), + cryptoCreate(LUCKY_RECEIVER), + cryptoCreate(SENDER).balance(transferAmount).via(SENDER_TXN), + cryptoCreate(RECEIVER).balance(noBalance), + scheduleCreate(BASIC_XFER, cryptoTransfer(tinyBarsFromTo(SENDER, RECEIVER, transferAmount))) + .waitForExpiry() + .withRelativeExpiry(SENDER_TXN, 10) + .recordingScheduledTxn() + .payingWith(PAYING_ACCOUNT) + .via(CREATE_TX), + recordFeeAmount(CREATE_TX, SCHEDULE_CREATE_FEE), + cryptoDelete(PAYING_ACCOUNT), + scheduleSign(BASIC_XFER).alsoSigningWith(SENDER).hasKnownStatus(SUCCESS), + getAccountBalance(SENDER).hasTinyBars(transferAmount), + getAccountBalance(RECEIVER).hasTinyBars(noBalance), + getScheduleInfo(BASIC_XFER) + .hasScheduleId(BASIC_XFER) + .hasWaitForExpiry() + .isNotExecuted() + .isNotDeleted() + .hasRelativeExpiry(SENDER_TXN, 10) + .hasRecordedScheduledTxn(), + triggerSchedule(BASIC_XFER, 15), + getAccountBalance(SENDER).hasTinyBars(transferAmount), + getAccountBalance(RECEIVER).hasTinyBars(noBalance), + // future: a check if account was deleted will be added in DispatchValidator + getTxnRecord(CREATE_TX) + .scheduled() + .hasPriority(recordWith().statusFrom(PAYER_ACCOUNT_DELETED, INSUFFICIENT_PAYER_BALANCE)))); + } + + @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) + public Stream executionWithCustomPayerButAccountDeletedFails() { + long balance = 10_000_000L; + long noBalance = 0L; + long transferAmount = 1; + return hapiTest(flattened( + cryptoCreate(PAYING_ACCOUNT).balance(balance), + cryptoCreate(SENDER).balance(transferAmount).via(SENDER_TXN), + cryptoCreate(RECEIVER).balance(noBalance), + scheduleCreate(BASIC_XFER, cryptoTransfer(tinyBarsFromTo(SENDER, RECEIVER, transferAmount))) + .waitForExpiry() + .withRelativeExpiry(SENDER_TXN, 10) + .recordingScheduledTxn() + .designatingPayer(PAYING_ACCOUNT) + .alsoSigningWith(PAYING_ACCOUNT) + .via(CREATE_TX), + cryptoDelete(PAYING_ACCOUNT), + scheduleSign(BASIC_XFER).alsoSigningWith(SENDER).via(SIGN_TX).hasKnownStatus(SUCCESS), + getAccountBalance(SENDER).hasTinyBars(transferAmount), + getAccountBalance(RECEIVER).hasTinyBars(noBalance), + getScheduleInfo(BASIC_XFER) + .hasScheduleId(BASIC_XFER) + .hasWaitForExpiry() + .isNotExecuted() + .isNotDeleted() + .hasRelativeExpiry(SENDER_TXN, 10) + .hasRecordedScheduledTxn(), + triggerSchedule(BASIC_XFER, 15), + getAccountBalance(SENDER).hasTinyBars(transferAmount), + getAccountBalance(RECEIVER).hasTinyBars(noBalance), + // future: a check if account was deleted will be added in DispatchValidator + getTxnRecord(CREATE_TX) + .scheduled() + .hasPriority(recordWith().statusFrom(INSUFFICIENT_PAYER_BALANCE, PAYER_ACCOUNT_DELETED)))); + } + + @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) + public Stream executionWithCryptoInsufficientAccountBalanceFails() { + long noBalance = 0L; + long senderBalance = 100L; + long transferAmount = 101L; + long payerBalance = 1_000_000L; + return hapiTest(flattened( + cryptoCreate(PAYING_ACCOUNT).balance(payerBalance), + cryptoCreate(SENDER).balance(senderBalance).via(SENDER_TXN), + cryptoCreate(RECEIVER).balance(noBalance), + scheduleCreate(FAILED_XFER, cryptoTransfer(tinyBarsFromTo(SENDER, RECEIVER, transferAmount))) + .waitForExpiry() + .withRelativeExpiry(SENDER_TXN, 4) + .designatingPayer(PAYING_ACCOUNT) + .recordingScheduledTxn() + .via(CREATE_TX), + scheduleSign(FAILED_XFER) + .alsoSigningWith(SENDER, PAYING_ACCOUNT) + .via(SIGN_TX) + .hasKnownStatus(SUCCESS), + getScheduleInfo(FAILED_XFER) + .hasScheduleId(BASIC_XFER) + .hasWaitForExpiry() + .isNotExecuted() + .isNotDeleted() + .hasRelativeExpiry(SENDER_TXN, 4) + .hasRecordedScheduledTxn(), + triggerSchedule(FAILED_XFER), + getAccountBalance(SENDER).hasTinyBars(senderBalance), + getAccountBalance(RECEIVER).hasTinyBars(noBalance), + withOpContext((spec, opLog) -> { + var triggeredTx = getTxnRecord(CREATE_TX).scheduled(); + + allRunFor(spec, triggeredTx); + + Assertions.assertEquals( + INSUFFICIENT_ACCOUNT_BALANCE, + triggeredTx.getResponseRecord().getReceipt().getStatus(), + SCHEDULED_TRANSACTION_MUST_NOT_SUCCEED); + }))); + } + + @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) + public Stream executionWithCryptoSenderDeletedFails() { + long noBalance = 0L; + long senderBalance = 100L; + long transferAmount = 101L; + long payerBalance = 1_000_000L; + return hapiTest(flattened( + cryptoCreate(PAYING_ACCOUNT).balance(payerBalance), + cryptoCreate(SENDER).balance(senderBalance).via(SENDER_TXN), + cryptoCreate(RECEIVER).balance(noBalance), + scheduleCreate(FAILED_XFER, cryptoTransfer(tinyBarsFromTo(SENDER, RECEIVER, transferAmount))) + .waitForExpiry() + .withRelativeExpiry(SENDER_TXN, 5) + .recordingScheduledTxn() + .designatingPayer(PAYING_ACCOUNT) + .via(CREATE_TX), + cryptoDelete(SENDER), + scheduleSign(FAILED_XFER) + .alsoSigningWith(SENDER, PAYING_ACCOUNT) + .via(SIGN_TX) + .hasKnownStatus(SUCCESS), + getAccountBalance(RECEIVER).hasTinyBars(noBalance), + getScheduleInfo(FAILED_XFER) + .hasScheduleId(FAILED_XFER) + .hasWaitForExpiry() + .isNotExecuted() + .isNotDeleted() + .hasRelativeExpiry(SENDER_TXN, 5) + .hasRecordedScheduledTxn(), + triggerSchedule(FAILED_XFER), + getAccountBalance(RECEIVER).hasTinyBars(noBalance), + withOpContext((spec, opLog) -> { + var triggeredTx = getTxnRecord(CREATE_TX).scheduled(); + allRunFor(spec, triggeredTx); + Assertions.assertEquals( + ACCOUNT_DELETED, + triggeredTx.getResponseRecord().getReceipt().getStatus(), + SCHEDULED_TRANSACTION_MUST_NOT_SUCCEED); + }))); + } + + @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) + public Stream executionTriggersWithWeirdlyRepeatedKey() { + String schedule = "dupKeyXfer"; + + return hapiTest(flattened( + cryptoCreate(WEIRDLY_POPULAR_KEY), + cryptoCreate(SENDER_1).key(WEIRDLY_POPULAR_KEY).balance(1L), + cryptoCreate(SENDER_2).key(WEIRDLY_POPULAR_KEY).balance(1L), + cryptoCreate(SENDER_3).key(WEIRDLY_POPULAR_KEY).balance(1L), + cryptoCreate(RECEIVER).balance(0L).via(WEIRDLY_POPULAR_KEY_TXN), + scheduleCreate( + schedule, + cryptoTransfer( + tinyBarsFromTo(SENDER_1, RECEIVER, 1L), + tinyBarsFromTo(SENDER_2, RECEIVER, 1L), + tinyBarsFromTo(SENDER_3, RECEIVER, 1L))) + .waitForExpiry() + .withRelativeExpiry(WEIRDLY_POPULAR_KEY_TXN, 4) + .payingWith(DEFAULT_PAYER) + .recordingScheduledTxn() + .via("creation"), + scheduleSign(schedule).alsoSigningWith(WEIRDLY_POPULAR_KEY), + getScheduleInfo(schedule) + .hasScheduleId(schedule) + .hasWaitForExpiry() + .isNotExecuted() + .isNotDeleted() + .hasRelativeExpiry(WEIRDLY_POPULAR_KEY_TXN, 4) + .hasRecordedScheduledTxn(), + triggerSchedule(schedule), + getAccountBalance(SENDER_1).hasTinyBars(0L), + getAccountBalance(SENDER_2).hasTinyBars(0L), + getAccountBalance(SENDER_3).hasTinyBars(0L), + getAccountBalance(RECEIVER).hasTinyBars(3L), + scheduleSign(schedule).alsoSigningWith(WEIRDLY_POPULAR_KEY).hasKnownStatus(INVALID_SCHEDULE_ID))); + } + + @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) + final Stream scheduledFreezeWorksAsExpected() { + return hapiTest(flattened( + cryptoCreate(PAYING_ACCOUNT).via(PAYER_TXN), + scheduleFakeUpgrade(PAYING_ACCOUNT, 4, SUCCESS_TXN), + scheduleSign(VALID_SCHEDULE) + .alsoSigningWith(GENESIS) + .payingWith(PAYING_ACCOUNT) + .hasKnownStatus(SUCCESS), + getScheduleInfo(VALID_SCHEDULE) + .hasScheduleId(VALID_SCHEDULE) + .hasWaitForExpiry() + .isNotExecuted() + .isNotDeleted() + .hasRecordedScheduledTxn(), + triggerSchedule(VALID_SCHEDULE), + freezeAbort().payingWith(GENESIS), + withOpContext((spec, opLog) -> { + var triggeredTx = getTxnRecord(SUCCESS_TXN).scheduled(); + allRunFor(spec, triggeredTx); + Assertions.assertEquals( + SUCCESS, + triggeredTx.getResponseRecord().getReceipt().getStatus(), + SCHEDULED_TRANSACTION_MUST_NOT_SUCCEED); + }))); + } + + @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) + final Stream scheduledPermissionedFileUpdateWorksAsExpected() { + return hapiTest(flattened( + cryptoCreate(PAYING_ACCOUNT).via(PAYER_TXN), + scheduleCreate(VALID_SCHEDULE, fileUpdate(standardUpdateFile).contents("fooo!")) + .withEntityMemo(randomUppercase(100)) + .designatingPayer(SYSTEM_ADMIN) + .payingWith(PAYING_ACCOUNT) + .waitForExpiry() + .withRelativeExpiry(PAYER_TXN, 4) + .recordingScheduledTxn() + .via(SUCCESS_TXN), + scheduleSign(VALID_SCHEDULE) + .alsoSigningWith(SYSTEM_ADMIN) + .payingWith(PAYING_ACCOUNT) + .hasKnownStatus(SUCCESS), + getScheduleInfo(VALID_SCHEDULE) + .hasScheduleId(VALID_SCHEDULE) + .hasWaitForExpiry() + .isNotExecuted() + .isNotDeleted() + .hasRelativeExpiry(PAYER_TXN, 4) + .hasRecordedScheduledTxn(), + triggerSchedule(VALID_SCHEDULE), + withOpContext((spec, opLog) -> { + var triggeredTx = getTxnRecord(SUCCESS_TXN).scheduled(); + allRunFor(spec, triggeredTx); + + Assertions.assertEquals( + SUCCESS, + triggeredTx.getResponseRecord().getReceipt().getStatus(), + SCHEDULED_TRANSACTION_MUST_NOT_SUCCEED); + }))); + } + + @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) + final Stream scheduledPermissionedFileUpdateUnauthorizedPayerFails() { + return hapiTest(flattened( + cryptoCreate(PAYING_ACCOUNT).via(PAYER_TXN), + cryptoCreate(PAYING_ACCOUNT_2), + scheduleCreate(VALID_SCHEDULE, fileUpdate(standardUpdateFile).contents("fooo!")) + .withEntityMemo(randomUppercase(100)) + .designatingPayer(PAYING_ACCOUNT_2) + .payingWith(PAYING_ACCOUNT) + .waitForExpiry() + .withRelativeExpiry(PAYER_TXN, 4) + .recordingScheduledTxn() + .via(SUCCESS_TXN), + scheduleSign(VALID_SCHEDULE) + .alsoSigningWith(PAYING_ACCOUNT_2, FREEZE_ADMIN) + .payingWith(PAYING_ACCOUNT) + .hasKnownStatus(SUCCESS), + getScheduleInfo(VALID_SCHEDULE) + .hasScheduleId(VALID_SCHEDULE) + .hasWaitForExpiry() + .isNotExecuted() + .isNotDeleted() + .hasRelativeExpiry(PAYER_TXN, 4) + .hasRecordedScheduledTxn(), + triggerSchedule(VALID_SCHEDULE), + withOpContext((spec, opLog) -> { + var triggeredTx = getTxnRecord(SUCCESS_TXN).scheduled(); + allRunFor(spec, triggeredTx); + + Assertions.assertEquals( + AUTHORIZATION_FAILED, + triggeredTx.getResponseRecord().getReceipt().getStatus(), + "Scheduled transaction be AUTHORIZATION_FAILED!"); + }))); + } + + @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) + final Stream scheduledSystemDeleteWorksAsExpected() { + return hapiTest(flattened( + cryptoCreate(PAYING_ACCOUNT).via(PAYER_TXN), + fileCreate(FILE_NAME).lifetime(THREE_MONTHS_IN_SECONDS).contents(ORIG_FILE), + scheduleCreate(VALID_SCHEDULE, systemFileDelete(FILE_NAME).updatingExpiry(1L)) + .withEntityMemo(randomUppercase(100)) + .designatingPayer(SYSTEM_DELETE_ADMIN) + .payingWith(PAYING_ACCOUNT) + .waitForExpiry() + .withRelativeExpiry(PAYER_TXN, 4) + .recordingScheduledTxn() + .via(SUCCESS_TXN), + scheduleSign(VALID_SCHEDULE) + .alsoSigningWith(SYSTEM_DELETE_ADMIN) + .payingWith(PAYING_ACCOUNT) + .hasKnownStatus(SUCCESS), + getScheduleInfo(VALID_SCHEDULE) + .hasScheduleId(VALID_SCHEDULE) + .hasWaitForExpiry() + .isNotExecuted() + .isNotDeleted() + .hasRelativeExpiry(PAYER_TXN, 4) + .hasRecordedScheduledTxn(), + triggerSchedule(VALID_SCHEDULE), + getFileInfo(FILE_NAME).nodePayment(1_234L).hasAnswerOnlyPrecheck(INVALID_FILE_ID), + withOpContext((spec, opLog) -> { + var triggeredTx = getTxnRecord(SUCCESS_TXN).scheduled(); + allRunFor(spec, triggeredTx); + + Assertions.assertEquals( + SUCCESS, + triggeredTx.getResponseRecord().getReceipt().getStatus(), + SCHEDULED_TRANSACTION_MUST_NOT_SUCCEED); + }))); + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/integration/RepeatableScheduleLongTermSignTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/integration/RepeatableScheduleLongTermSignTest.java new file mode 100644 index 000000000000..a083aed55200 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/integration/RepeatableScheduleLongTermSignTest.java @@ -0,0 +1,596 @@ +/* + * 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.integration; + +import static com.hedera.services.bdd.junit.RepeatableReason.NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION; +import static com.hedera.services.bdd.junit.TestTags.INTEGRATION; +import static com.hedera.services.bdd.junit.hedera.embedded.EmbeddedMode.REPEATABLE; +import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; +import static com.hedera.services.bdd.spec.assertions.AccountInfoAsserts.changeFromSnapshot; +import static com.hedera.services.bdd.spec.keys.ControlForKey.forKey; +import static com.hedera.services.bdd.spec.keys.KeyShape.sigs; +import static com.hedera.services.bdd.spec.keys.KeyShape.threshOf; +import static com.hedera.services.bdd.spec.keys.SigControl.OFF; +import static com.hedera.services.bdd.spec.keys.SigControl.ON; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getScheduleInfo; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTxnRecord; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoTransfer; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoUpdate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.mintToken; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.scheduleCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.scheduleSign; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenUpdate; +import static com.hedera.services.bdd.spec.transactions.crypto.HapiCryptoTransfer.tinyBarsFromTo; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.balanceSnapshot; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.keyFromMutation; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyListNamed; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sleepFor; +import static com.hedera.services.bdd.suites.HapiSuite.ADDRESS_BOOK_CONTROL; +import static com.hedera.services.bdd.suites.HapiSuite.DEFAULT_PAYER; +import static com.hedera.services.bdd.suites.HapiSuite.FUNDING; +import static com.hedera.services.bdd.suites.HapiSuite.GENESIS; +import static com.hedera.services.bdd.suites.HapiSuite.ONE_HBAR; +import static com.hedera.services.bdd.suites.HapiSuite.ONE_MILLION_HBARS; +import static com.hedera.services.bdd.suites.HapiSuite.THOUSAND_HBAR; +import static com.hedera.services.bdd.suites.HapiSuite.flattened; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.ADMIN; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.BASIC_XFER; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.BEFORE; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.CREATE_TXN; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.CREATION; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.DEFERRED_CREATION; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.DEFERRED_FALL; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.DEFERRED_XFER; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.EXTRA_KEY; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.NEW_SENDER_KEY; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.PAYER; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.SENDER_TXN; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.SHARED_KEY; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.THREE_SIG_XFER; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.TOKEN_A; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.TWO_SIG_XFER; +import static com.hedera.services.bdd.suites.hip423.LongTermScheduleUtils.triggerSchedule; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_SCHEDULE_ID; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.NO_NEW_VALID_SIGNATURES; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.RECORD_NOT_FOUND; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SOME_SIGNATURES_WERE_INVALID; + +import com.hedera.services.bdd.junit.HapiTestLifecycle; +import com.hedera.services.bdd.junit.RepeatableHapiTest; +import com.hedera.services.bdd.junit.TargetEmbeddedMode; +import com.hedera.services.bdd.junit.support.TestLifecycle; +import com.hedera.services.bdd.spec.keys.ControlForKey; +import com.hedera.services.bdd.spec.keys.OverlappingKeyGenerator; +import com.hederahashgraph.api.proto.java.Key; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Tag; + +@Order(5) +@Tag(INTEGRATION) +@HapiTestLifecycle +@TargetEmbeddedMode(REPEATABLE) +public class RepeatableScheduleLongTermSignTest { + + private static final String RECEIVER = "receiver"; + private static final String SENDER = "sender"; + + @BeforeAll + static void beforeAll(@NonNull final TestLifecycle lifecycle) { + // override and preserve old values + lifecycle.overrideInClass(Map.of( + "scheduling.longTermEnabled", + "true", + "scheduling.whitelist", + "ConsensusSubmitMessage,CryptoTransfer,TokenMint,TokenBurn," + + "CryptoCreate,CryptoUpdate,FileUpdate,SystemDelete,SystemUndelete," + + "Freeze,ContractCall,ContractCreate,ContractUpdate,ContractDelete")); + } + + @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) + final Stream reductionInSigningReqsAllowsTxnToGoThrough() { + var senderShape = threshOf(2, threshOf(1, 3), threshOf(1, 3), threshOf(2, 3)); + var sigOne = senderShape.signedWith(sigs(sigs(OFF, OFF, ON), sigs(OFF, OFF, OFF), sigs(OFF, OFF, OFF))); + var sigTwo = senderShape.signedWith(sigs(sigs(OFF, OFF, OFF), sigs(ON, ON, ON), sigs(OFF, OFF, OFF))); + var firstSigThree = senderShape.signedWith(sigs(sigs(OFF, OFF, OFF), sigs(OFF, OFF, OFF), sigs(ON, OFF, OFF))); + String sender = "X"; + String receiver = "Y"; + String schedule = "Z"; + String senderKey = "sKey"; + + return hapiTest(flattened( + newKeyNamed(senderKey).shape(senderShape), + keyFromMutation(NEW_SENDER_KEY, senderKey).changing(this::lowerThirdNestedThresholdSigningReq), + cryptoCreate(sender).key(senderKey).via(SENDER_TXN), + cryptoCreate(receiver).balance(0L), + scheduleCreate(schedule, cryptoTransfer(tinyBarsFromTo(sender, receiver, 1))) + .payingWith(DEFAULT_PAYER) + .waitForExpiry() + .withRelativeExpiry(SENDER_TXN, 7) + .recordingScheduledTxn() + .alsoSigningWith(sender) + .sigControl(ControlForKey.forKey(senderKey, sigOne)), + getAccountBalance(receiver).hasTinyBars(0L), + scheduleSign(schedule) + .alsoSigningWith(NEW_SENDER_KEY) + .sigControl(forKey(NEW_SENDER_KEY, firstSigThree)), + getAccountBalance(receiver).hasTinyBars(0L), + cryptoUpdate(sender).key(NEW_SENDER_KEY), + scheduleSign(schedule).hasKnownStatus(NO_NEW_VALID_SIGNATURES), + getAccountBalance(receiver).hasTinyBars(0L), + getScheduleInfo(schedule) + .hasScheduleId(schedule) + .hasWaitForExpiry() + .isNotExecuted() + .isNotDeleted() + .hasRelativeExpiry(SENDER_TXN, 7) + .hasRecordedScheduledTxn(), + triggerSchedule(schedule, 8), + scheduleSign(schedule) + .alsoSigningWith(NEW_SENDER_KEY) + .sigControl(forKey(NEW_SENDER_KEY, sigTwo)) + .hasKnownStatus(INVALID_SCHEDULE_ID), + getAccountBalance(receiver).hasTinyBars(1L))); + } + + @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) + final Stream reductionInSigningReqsAllowsTxnToGoThroughAtExpiryWithWaitForExpiry() { + long scheduleLifetime = 10; + var senderShape = threshOf(2, threshOf(1, 3), threshOf(1, 3), threshOf(2, 3)); + var sigOne = senderShape.signedWith(sigs(sigs(OFF, OFF, ON), sigs(OFF, OFF, OFF), sigs(OFF, OFF, OFF))); + var firstSigThree = senderShape.signedWith(sigs(sigs(OFF, OFF, OFF), sigs(OFF, OFF, OFF), sigs(ON, OFF, OFF))); + String sender = "X"; + String receiver = "Y"; + String schedule = "Z"; + String senderKey = "sKey"; + + return hapiTest(flattened( + newKeyNamed(senderKey).shape(senderShape), + keyFromMutation(NEW_SENDER_KEY, senderKey).changing(this::lowerThirdNestedThresholdSigningReq), + cryptoCreate(sender).key(senderKey).via(SENDER_TXN), + cryptoCreate(receiver).balance(0L), + scheduleCreate(schedule, cryptoTransfer(tinyBarsFromTo(sender, receiver, 1))) + .payingWith(DEFAULT_PAYER) + .waitForExpiry() + .withRelativeExpiry(SENDER_TXN, scheduleLifetime) + .recordingScheduledTxn() + .alsoSigningWith(sender) + .sigControl(ControlForKey.forKey(senderKey, sigOne)), + getAccountBalance(receiver).hasTinyBars(0L), + scheduleSign(schedule) + .alsoSigningWith(NEW_SENDER_KEY) + .sigControl(forKey(NEW_SENDER_KEY, firstSigThree)), + getAccountBalance(receiver).hasTinyBars(0L), + cryptoUpdate(sender).key(NEW_SENDER_KEY), + getAccountBalance(receiver).hasTinyBars(0L), + getScheduleInfo(schedule) + .hasScheduleId(schedule) + .hasWaitForExpiry(true) + .isNotExecuted() + .isNotDeleted() + .hasRelativeExpiry(SENDER_TXN, scheduleLifetime) + .hasRecordedScheduledTxn(), + triggerSchedule(schedule, scheduleLifetime), + getAccountBalance(receiver).hasTinyBars(1L))); + } + + @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) + final Stream nestedSigningReqsWorkAsExpected() { + var senderShape = threshOf(2, threshOf(1, 3), threshOf(1, 3), threshOf(1, 3)); + var sigOne = senderShape.signedWith(sigs(sigs(OFF, OFF, ON), sigs(OFF, OFF, OFF), sigs(OFF, OFF, OFF))); + var sigTwo = senderShape.signedWith(sigs(sigs(OFF, OFF, OFF), sigs(OFF, ON, OFF), sigs(OFF, OFF, OFF))); + String sender = "X"; + String receiver = "Y"; + String schedule = "Z"; + String senderKey = "sKey"; + + return hapiTest(flattened( + newKeyNamed(senderKey).shape(senderShape), + cryptoCreate(sender).key(senderKey).via(SENDER_TXN), + cryptoCreate(receiver).balance(0L), + scheduleCreate(schedule, cryptoTransfer(tinyBarsFromTo(sender, receiver, 1))) + .payingWith(DEFAULT_PAYER) + .waitForExpiry() + .withRelativeExpiry(SENDER_TXN, 4) + .recordingScheduledTxn() + .alsoSigningWith(sender) + .sigControl(ControlForKey.forKey(senderKey, sigOne)), + getAccountBalance(receiver).hasTinyBars(0L), + scheduleSign(schedule).alsoSigningWith(senderKey).sigControl(forKey(senderKey, sigTwo)), + getAccountBalance(receiver).hasTinyBars(0L), + getScheduleInfo(schedule) + .hasScheduleId(schedule) + .hasWaitForExpiry() + .isNotExecuted() + .isNotDeleted() + .hasRelativeExpiry(SENDER_TXN, 4) + .hasRecordedScheduledTxn(), + triggerSchedule(schedule), + getAccountBalance(receiver).hasTinyBars(1L))); + } + + @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) + final Stream receiverSigRequiredNotConfusedByOrder() { + long scheduleLifetime = 10; + var senderShape = threshOf(1, 3); + var sigOne = senderShape.signedWith(sigs(ON, OFF, OFF)); + String sender = "X"; + String receiver = "Y"; + String schedule = "Z"; + String senderKey = "sKey"; + + return hapiTest(flattened( + newKeyNamed(senderKey).shape(senderShape), + cryptoCreate(sender).key(senderKey).via(SENDER_TXN), + cryptoCreate(receiver).balance(0L).receiverSigRequired(true), + scheduleCreate(schedule, cryptoTransfer(tinyBarsFromTo(sender, receiver, 1))) + .waitForExpiry() + .withRelativeExpiry(SENDER_TXN, scheduleLifetime) + .recordingScheduledTxn() + .payingWith(DEFAULT_PAYER), + scheduleSign(schedule).alsoSigningWith(receiver), + getAccountBalance(receiver).hasTinyBars(0L), + scheduleSign(schedule).alsoSigningWith(senderKey).sigControl(forKey(senderKey, sigOne)), + getAccountBalance(receiver).hasTinyBars(0L), + getScheduleInfo(schedule) + .hasScheduleId(schedule) + .hasWaitForExpiry() + .isNotExecuted() + .isNotDeleted() + .hasRelativeExpiry(SENDER_TXN, scheduleLifetime) + .hasRecordedScheduledTxn(), + triggerSchedule(schedule, scheduleLifetime), + getAccountBalance(receiver).hasTinyBars(1L))); + } + + @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) + final Stream extraSigsDontMatterAtExpiry() { + long scheduleLifetime = 20; + var senderShape = threshOf(1, 3); + var sigOne = senderShape.signedWith(sigs(ON, OFF, OFF)); + var sigTwo = senderShape.signedWith(sigs(OFF, ON, OFF)); + var sigThree = senderShape.signedWith(sigs(OFF, OFF, ON)); + String sender = "X"; + String receiver = "Y"; + String schedule = "Z"; + String senderKey = "sKey"; + + return hapiTest(flattened( + cryptoCreate(PAYER).balance(ONE_MILLION_HBARS).payingWith(GENESIS), + newKeyNamed(senderKey).shape(senderShape), + newKeyNamed(EXTRA_KEY), + cryptoCreate(sender).key(senderKey).via(SENDER_TXN), + cryptoCreate(receiver).balance(0L).receiverSigRequired(true), + scheduleCreate(schedule, cryptoTransfer(tinyBarsFromTo(sender, receiver, 1))) + .waitForExpiry() + .withRelativeExpiry(SENDER_TXN, scheduleLifetime) + .recordingScheduledTxn() + .payingWith(PAYER), + scheduleSign(schedule).payingWith(PAYER).fee(THOUSAND_HBAR).alsoSigningWith(receiver), + getAccountBalance(receiver).hasTinyBars(0L), + scheduleSign(schedule) + .payingWith(PAYER) + .fee(THOUSAND_HBAR) + .alsoSigningWith(senderKey) + .sigControl(forKey(senderKey, sigOne)), + getAccountBalance(receiver).hasTinyBars(0L), + scheduleSign(schedule) + .payingWith(PAYER) + .fee(THOUSAND_HBAR) + .alsoSigningWith(senderKey) + .sigControl(forKey(senderKey, sigTwo)), + getAccountBalance(receiver).hasTinyBars(0L), + scheduleSign(schedule) + .payingWith(PAYER) + .fee(THOUSAND_HBAR) + .alsoSigningWith(EXTRA_KEY) + .hasKnownStatus(NO_NEW_VALID_SIGNATURES), + getAccountBalance(receiver).hasTinyBars(0L), + scheduleSign(schedule) + .payingWith(PAYER) + .fee(THOUSAND_HBAR) + .alsoSigningWith(senderKey) + .sigControl(forKey(senderKey, sigTwo)) + .hasKnownStatus(NO_NEW_VALID_SIGNATURES), + getAccountBalance(receiver).hasTinyBars(0L), + scheduleSign(schedule) + .payingWith(PAYER) + .fee(THOUSAND_HBAR) + .alsoSigningWith(senderKey) + .sigControl(forKey(senderKey, sigThree)), + getAccountBalance(receiver).hasTinyBars(0L), + scheduleSign(schedule) + .payingWith(PAYER) + .fee(THOUSAND_HBAR) + .alsoSigningWith(senderKey) + .sigControl(forKey(senderKey, sigTwo)) + .hasKnownStatus(NO_NEW_VALID_SIGNATURES), + getAccountBalance(receiver).hasTinyBars(0L), + scheduleSign(schedule) + .payingWith(PAYER) + .fee(THOUSAND_HBAR) + .alsoSigningWith(EXTRA_KEY) + .hasKnownStatus(NO_NEW_VALID_SIGNATURES), + getAccountBalance(receiver).hasTinyBars(0L), + scheduleSign(schedule) + .payingWith(PAYER) + .fee(THOUSAND_HBAR) + .alsoSigningWith(senderKey) + .sigControl(forKey(senderKey, sigOne)) + .hasKnownStatus(NO_NEW_VALID_SIGNATURES), + getAccountBalance(receiver).hasTinyBars(0L), + getScheduleInfo(schedule) + .hasScheduleId(schedule) + .hasWaitForExpiry() + .isNotExecuted() + .isNotDeleted() + .hasRelativeExpiry(SENDER_TXN, scheduleLifetime) + .hasRecordedScheduledTxn(), + triggerSchedule(schedule, scheduleLifetime), + getAccountBalance(receiver).hasTinyBars(1L))); + } + + @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) + final Stream basicSignatureCollectionWorks() { + var txnBody = cryptoTransfer(tinyBarsFromTo(SENDER, RECEIVER, 1)); + + return hapiTest( + cryptoCreate(SENDER).via(SENDER_TXN), + cryptoCreate(RECEIVER).receiverSigRequired(true), + scheduleCreate(BASIC_XFER, txnBody) + .waitForExpiry() + .withRelativeExpiry(SENDER_TXN, 4) + .payingWith(SENDER), + scheduleSign(BASIC_XFER).alsoSigningWith(RECEIVER), + getScheduleInfo(BASIC_XFER).hasSignatories(RECEIVER, SENDER), + // note: the sleepFor and cryptoCreate operations are added only to clear the schedule before + // the next state. This was needed because an edge case in the BaseTranslator occur. + // When scheduleCreate trigger the schedules execution scheduleRef field is not the correct one. + sleepFor(6000), + cryptoCreate("foo")); + } + + @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) + final Stream signalsIrrelevantSig() { + var txnBody = cryptoTransfer(tinyBarsFromTo(SENDER, RECEIVER, 1)); + + return hapiTest( + cryptoCreate(SENDER).via(SENDER_TXN), + cryptoCreate(RECEIVER), + newKeyNamed("somebodyelse"), + scheduleCreate(BASIC_XFER, txnBody).waitForExpiry().withRelativeExpiry(SENDER_TXN, 4), + scheduleSign(BASIC_XFER) + .alsoSigningWith("somebodyelse") + .hasKnownStatusFrom(NO_NEW_VALID_SIGNATURES, SOME_SIGNATURES_WERE_INVALID), + // note: the sleepFor and cryptoCreate operations are added only to clear the schedule before + // the next state. This was needed because an edge case in the BaseTranslator occur. + // When scheduleCreate trigger the schedules execution scheduleRef field is not the correct one. + sleepFor(6000), + cryptoCreate("foo")); + } + + @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) + final Stream signalsIrrelevantSigEvenAfterLinkedEntityUpdate() { + var txnBody = mintToken(TOKEN_A, 50000000L); + + return hapiTest( + newKeyNamed(ADMIN), + newKeyNamed("mint"), + newKeyNamed("newMint"), + tokenCreate(TOKEN_A).adminKey(ADMIN).supplyKey("mint").via(CREATE_TXN), + scheduleCreate("tokenMintScheduled", txnBody).waitForExpiry().withRelativeExpiry(CREATE_TXN, 4), + tokenUpdate(TOKEN_A).supplyKey("newMint"), + scheduleSign("tokenMintScheduled") + .alsoSigningWith("mint") + /* In the rare, but possible, case that the the mint and newMint keys overlap + * in their first byte (and that byte is not shared by the DEFAULT_PAYER), + * we will get SOME_SIGNATURES_WERE_INVALID instead of NO_NEW_VALID_SIGNATURES. + * + * So we need this to stabilize CI. But if just testing locally, you may + * only use .hasKnownStatus(NO_NEW_VALID_SIGNATURES) and it will pass + * >99.99% of the time. */ + .hasKnownStatusFrom(NO_NEW_VALID_SIGNATURES, SOME_SIGNATURES_WERE_INVALID), + + // note: the sleepFor and cryptoCreate operations are added only to clear the schedule before + // the next state. This was needed because an edge case in the BaseTranslator occur. + // When scheduleCreate trigger the schedules execution scheduleRef field is not the correct one. + sleepFor(6000), + cryptoCreate("foo")); + } + + @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) + public Stream triggersUponFinishingPayerSig() { + return hapiTest(flattened( + cryptoCreate(PAYER).balance(ONE_HBAR), + cryptoCreate(SENDER).balance(1L).via(SENDER_TXN), + cryptoCreate(RECEIVER).balance(0L).receiverSigRequired(true), + scheduleCreate( + THREE_SIG_XFER, + cryptoTransfer(tinyBarsFromTo(SENDER, RECEIVER, 1)) + .fee(ONE_HBAR)) + .designatingPayer(PAYER) + .waitForExpiry() + .withRelativeExpiry(SENDER_TXN, 4) + .recordingScheduledTxn() + .alsoSigningWith(SENDER, RECEIVER), + getAccountBalance(RECEIVER).hasTinyBars(0L), + scheduleSign(THREE_SIG_XFER).alsoSigningWith(PAYER), + getAccountBalance(RECEIVER).hasTinyBars(0L), + getScheduleInfo(THREE_SIG_XFER) + .hasScheduleId(THREE_SIG_XFER) + .hasWaitForExpiry() + .isNotExecuted() + .isNotDeleted() + .hasRelativeExpiry(SENDER_TXN, 4) + .hasRecordedScheduledTxn(), + triggerSchedule(THREE_SIG_XFER), + getAccountBalance(RECEIVER).hasTinyBars(1L))); + } + + @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) + public Stream triggersUponAdditionalNeededSig() { + return hapiTest(flattened( + cryptoCreate(SENDER).balance(1L).via(SENDER_TXN), + cryptoCreate(RECEIVER).balance(0L).receiverSigRequired(true), + scheduleCreate( + TWO_SIG_XFER, + cryptoTransfer(tinyBarsFromTo(SENDER, RECEIVER, 1)) + .fee(ONE_HBAR)) + .waitForExpiry() + .withRelativeExpiry(SENDER_TXN, 4) + .recordingScheduledTxn() + .alsoSigningWith(SENDER), + getAccountBalance(RECEIVER).hasTinyBars(0L), + scheduleSign(TWO_SIG_XFER).alsoSigningWith(RECEIVER), + getAccountBalance(RECEIVER).hasTinyBars(0L), + getScheduleInfo(TWO_SIG_XFER) + .hasScheduleId(TWO_SIG_XFER) + .hasWaitForExpiry() + .isNotExecuted() + .isNotDeleted() + .hasRelativeExpiry(SENDER_TXN, 4) + .hasRecordedScheduledTxn(), + triggerSchedule(TWO_SIG_XFER), + getAccountBalance(RECEIVER).hasTinyBars(1L))); + } + + @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) + public Stream sharedKeyWorksAsExpected() { + return hapiTest(flattened( + newKeyNamed(SHARED_KEY), + cryptoCreate("payerWithSharedKey").key(SHARED_KEY).via(CREATE_TXN), + scheduleCreate( + DEFERRED_CREATION, + cryptoCreate("yetToBe") + .signedBy() + .receiverSigRequired(true) + .key(SHARED_KEY) + .balance(123L) + .fee(ONE_HBAR)) + .waitForExpiry() + .withRelativeExpiry(CREATE_TXN, 4) + .recordingScheduledTxn() + .payingWith("payerWithSharedKey") + .via(CREATION), + getTxnRecord(CREATION).scheduled().hasAnswerOnlyPrecheck(RECORD_NOT_FOUND), + getScheduleInfo(DEFERRED_CREATION) + .hasScheduleId(DEFERRED_CREATION) + .hasWaitForExpiry() + .isNotExecuted() + .isNotDeleted() + .hasRelativeExpiry(CREATE_TXN, 4) + .hasRecordedScheduledTxn(), + triggerSchedule(DEFERRED_CREATION), + getTxnRecord(CREATION).scheduled())); + } + + @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) + public Stream overlappingKeysTreatedAsExpected() { + var keyGen = OverlappingKeyGenerator.withAtLeastOneOverlappingByte(2); + final long scheduleLifetime = 10; + + return hapiTest(flattened( + newKeyNamed("aKey").generator(keyGen), + newKeyNamed("bKey").generator(keyGen), + newKeyNamed("cKey"), + cryptoCreate("aSender").key("aKey").balance(1L).via(SENDER_TXN), + cryptoCreate("cSender").key("cKey").balance(1L), + balanceSnapshot(BEFORE, ADDRESS_BOOK_CONTROL), + scheduleCreate( + DEFERRED_XFER, + cryptoTransfer( + tinyBarsFromTo("aSender", ADDRESS_BOOK_CONTROL, 1), + tinyBarsFromTo("cSender", ADDRESS_BOOK_CONTROL, 1))) + .waitForExpiry() + .withRelativeExpiry(SENDER_TXN, scheduleLifetime) + .recordingScheduledTxn(), + scheduleSign(DEFERRED_XFER).alsoSigningWith("aKey"), + scheduleSign(DEFERRED_XFER).alsoSigningWith("aKey").hasKnownStatus(NO_NEW_VALID_SIGNATURES), + scheduleSign(DEFERRED_XFER).alsoSigningWith("aKey", "bKey").hasKnownStatus(NO_NEW_VALID_SIGNATURES), + scheduleSign(DEFERRED_XFER) + .alsoSigningWith("bKey") + /* In the rare, but possible, case that the overlapping byte shared by aKey + * and bKey is _also_ shared by the DEFAULT_PAYER, the bKey prefix in the sig + * map will probably not collide with aKey any more, and we will get + * NO_NEW_VALID_SIGNATURES instead of SOME_SIGNATURES_WERE_INVALID. + * + * So we need this to stabilize CI. But if just testing locally, you may + * only use .hasKnownStatus(SOME_SIGNATURES_WERE_INVALID) and it will pass + * >99.99% of the time. */ + .hasKnownStatusFrom(SOME_SIGNATURES_WERE_INVALID, NO_NEW_VALID_SIGNATURES), + scheduleSign(DEFERRED_XFER).alsoSigningWith("aKey", "bKey", "cKey"), + getAccountBalance(ADDRESS_BOOK_CONTROL).hasTinyBars(changeFromSnapshot(BEFORE, 0)), + getScheduleInfo(DEFERRED_XFER) + .hasScheduleId(DEFERRED_XFER) + .hasWaitForExpiry() + .isNotExecuted() + .isNotDeleted() + .hasRelativeExpiry(SENDER_TXN, scheduleLifetime) + .hasRecordedScheduledTxn(), + triggerSchedule(DEFERRED_XFER, scheduleLifetime), + getAccountBalance(ADDRESS_BOOK_CONTROL).hasTinyBars(changeFromSnapshot(BEFORE, +2)))); + } + + @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) + public Stream retestsActivationOnSignWithEmptySigMap() { + return hapiTest(flattened( + newKeyNamed("a"), + newKeyNamed("b"), + newKeyListNamed("ab", List.of("a", "b")), + newKeyNamed(ADMIN), + cryptoCreate(SENDER).key("ab").balance(667L).via(SENDER_TXN), + scheduleCreate( + DEFERRED_FALL, + cryptoTransfer(tinyBarsFromTo(SENDER, FUNDING, 1)) + .fee(ONE_HBAR)) + .waitForExpiry() + .withRelativeExpiry(SENDER_TXN, 4) + .recordingScheduledTxn() + .alsoSigningWith("a"), + getAccountBalance(SENDER).hasTinyBars(667L), + cryptoUpdate(SENDER).key("a"), + scheduleSign(DEFERRED_FALL).alsoSigningWith().hasKnownStatus(NO_NEW_VALID_SIGNATURES), + getAccountBalance(SENDER).hasTinyBars(667L), + getScheduleInfo(DEFERRED_FALL) + .hasScheduleId(DEFERRED_FALL) + .hasWaitForExpiry() + .isNotExecuted() + .isNotDeleted() + .hasRelativeExpiry(SENDER_TXN, 4) + .hasRecordedScheduledTxn(), + triggerSchedule(DEFERRED_FALL), + getAccountBalance(SENDER).hasTinyBars(666L))); + } + + private Key lowerThirdNestedThresholdSigningReq(Key source) { + var newKey = source.getThresholdKey().getKeys().getKeys(2).toBuilder(); + newKey.setThresholdKey(newKey.getThresholdKeyBuilder().setThreshold(1)); + var newKeyList = source.getThresholdKey().getKeys().toBuilder().setKeys(2, newKey); + return source.toBuilder() + .setThresholdKey(source.getThresholdKey().toBuilder().setKeys(newKeyList)) + .build(); + } +} From 5322bdc8b6c5fa70942bdc8030b105557dbc8214 Mon Sep 17 00:00:00 2001 From: Mustafa Uzun Date: Tue, 14 Jan 2025 13:35:27 +0200 Subject: [PATCH 07/19] refactor: enhance migration testing tool (#17246) Signed-off-by: Mustafa Uzun --- .../MigrationTestingTool/build.gradle.kts | 19 +- .../migration/MigrationTestingToolMain.java | 8 + .../migration/MigrationTestingToolState.java | 45 ++++ .../demo/migration/TransactionGenerator.java | 4 +- .../demo/migration/TransactionUtils.java | 13 +- .../MigrationTestingToolStateTest.java | 229 ++++++++++++++++++ 6 files changed, 314 insertions(+), 4 deletions(-) create mode 100644 platform-sdk/platform-apps/tests/MigrationTestingTool/src/test/java/com/swirlds/demo/migration/MigrationTestingToolStateTest.java diff --git a/platform-sdk/platform-apps/tests/MigrationTestingTool/build.gradle.kts b/platform-sdk/platform-apps/tests/MigrationTestingTool/build.gradle.kts index e3fca20be0ff..809f513f0913 100644 --- a/platform-sdk/platform-apps/tests/MigrationTestingTool/build.gradle.kts +++ b/platform-sdk/platform-apps/tests/MigrationTestingTool/build.gradle.kts @@ -1,9 +1,26 @@ -// SPDX-License-Identifier: Apache-2.0 +/* + * 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. + */ + plugins { id("org.hiero.gradle.module.application") } application.mainClass = "com.swirlds.demo.migration.MigrationTestingToolMain" testModuleInfo { requires("org.junit.jupiter.api") + requires("org.assertj.core") requires("org.junit.jupiter.params") + requires("org.mockito") } diff --git a/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/MigrationTestingToolMain.java b/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/MigrationTestingToolMain.java index b095fd60545b..cea14826eea1 100644 --- a/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/MigrationTestingToolMain.java +++ b/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/MigrationTestingToolMain.java @@ -22,6 +22,8 @@ import static com.swirlds.platform.test.fixtures.state.FakeStateLifecycles.registerMerkleStateRootClassIds; import com.hedera.hapi.node.state.roster.RosterEntry; +import com.hedera.hapi.platform.event.StateSignatureTransaction; +import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.common.constructable.ClassConstructorPair; import com.swirlds.common.constructable.ConstructableRegistry; import com.swirlds.common.constructable.ConstructableRegistryException; @@ -207,4 +209,10 @@ public PlatformMerkleStateRoot newMerkleStateRoot() { public BasicSoftwareVersion getSoftwareVersion() { return softwareVersion; } + + @Override + @NonNull + public Bytes encodeSystemTransaction(final @NonNull StateSignatureTransaction transaction) { + return StateSignatureTransaction.PROTOBUF.toBytes(transaction); + } } diff --git a/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/MigrationTestingToolState.java b/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/MigrationTestingToolState.java index 6d0e2ae39866..db801e25980a 100644 --- a/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/MigrationTestingToolState.java +++ b/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/MigrationTestingToolState.java @@ -17,11 +17,13 @@ package com.swirlds.demo.migration; import static com.swirlds.demo.migration.MigrationTestingToolMain.PREVIOUS_SOFTWARE_VERSION; +import static com.swirlds.demo.migration.TransactionUtils.isSystemTransaction; import static com.swirlds.logging.legacy.LogMarker.STARTUP; import static com.swirlds.platform.test.fixtures.state.FakeStateLifecycles.FAKE_MERKLE_STATE_LIFECYCLES; import com.hedera.hapi.node.base.SemanticVersion; import com.hedera.hapi.platform.event.StateSignatureTransaction; +import com.hedera.pbj.runtime.ParseException; import com.swirlds.common.constructable.ConstructableIgnored; import com.swirlds.common.crypto.DigestType; import com.swirlds.common.merkle.MerkleNode; @@ -47,7 +49,9 @@ import com.swirlds.platform.system.SoftwareVersion; import com.swirlds.platform.system.address.AddressBook; import com.swirlds.platform.system.events.ConsensusEvent; +import com.swirlds.platform.system.events.Event; import com.swirlds.platform.system.transaction.ConsensusTransaction; +import com.swirlds.platform.system.transaction.Transaction; import com.swirlds.virtualmap.VirtualMap; import com.swirlds.virtualmap.datasource.VirtualDataSourceBuilder; import edu.umd.cs.findbugs.annotations.NonNull; @@ -278,6 +282,27 @@ public void init( } } + @Override + public void preHandle( + final @NonNull Event event, + final @NonNull Consumer> stateSignatureTransaction) { + event.forEachTransaction(transaction -> { + + // We don't want to consume deprecated EventTransaction.STATE_SIGNATURE_TRANSACTION system transactions in + // the callback, since it's intended to be used only + // for the new form of encoded system transactions in Bytes. + // We skip the current iteration, if it processes a deprecated system transaction with the + // EventTransaction.STATE_SIGNATURE_TRANSACTION type. + if (transaction.isSystem()) { + return; + } + + if (isSystemTransaction(transaction.getApplicationTransaction())) { + consumeSystemTransaction(transaction, event, stateSignatureTransaction); + } + }); + } + /** * {@inheritDoc} */ @@ -295,6 +320,11 @@ public void handleConsensusRound( if (trans.isSystem()) { continue; } + if (isSystemTransaction(trans.getApplicationTransaction())) { + consumeSystemTransaction(trans, event, stateSignatureTransaction); + continue; + } + final MigrationTestingToolTransaction mTrans = TransactionUtils.parseTransaction(trans.getApplicationTransaction()); mTrans.applyTo(this); @@ -335,4 +365,19 @@ public int getVersion() { public int getMinimumSupportedVersion() { return ClassVersion.VIRTUAL_MAP; } + + private void consumeSystemTransaction( + final @NonNull Transaction transaction, + final @NonNull Event event, + final @NonNull Consumer> + stateSignatureTransactionCallback) { + try { + final var stateSignatureTransaction = + StateSignatureTransaction.PROTOBUF.parse(transaction.getApplicationTransaction()); + stateSignatureTransactionCallback.accept(new ScopedSystemTransaction<>( + event.getCreatorId(), event.getSoftwareVersion(), stateSignatureTransaction)); + } catch (final ParseException e) { + logger.error("Failed to parse StateSignatureTransaction", e); + } + } } diff --git a/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/TransactionGenerator.java b/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/TransactionGenerator.java index 9d61e9549126..f2194560614b 100644 --- a/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/TransactionGenerator.java +++ b/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/TransactionGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2024 Hedera Hashgraph, LLC + * Copyright (C) 2022-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. @@ -59,6 +59,8 @@ public byte[] generateTransaction() throws SignatureException { final ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); final SerializableDataOutputStream out = new SerializableDataOutputStream(byteOut); try { + // Adding additional byte to differentiate application transactions from system ones + out.write(1); out.writeSerializable(transaction, false); } catch (final IOException e) { throw new UncheckedIOException(e); diff --git a/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/TransactionUtils.java b/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/TransactionUtils.java index 850351c9ba14..31185e89977c 100644 --- a/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/TransactionUtils.java +++ b/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/TransactionUtils.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. @@ -26,11 +26,16 @@ * Utility methods for migration testing tool transactions */ public class TransactionUtils { + private static final byte APPLICATION_TRANSACTION_MARKER = 1; + /** * Parse a {@link MigrationTestingToolTransaction} from a {@link Bytes}. */ public static @NonNull MigrationTestingToolTransaction parseTransaction(@NonNull final Bytes bytes) { - final SerializableDataInputStream in = new SerializableDataInputStream(bytes.toInputStream()); + // Remove the first byte, which is marker added to distinguish application transactions from system ones in + // TransactionGenerator + final Bytes slicedBytes = bytes.slice(1, bytes.length() - 1); + final SerializableDataInputStream in = new SerializableDataInputStream(slicedBytes.toInputStream()); try { return in.readSerializable(false, MigrationTestingToolTransaction::new); @@ -38,4 +43,8 @@ public class TransactionUtils { throw new UncheckedIOException("Could not parse transaction kind:%s".formatted(bytes.toHex()), e); } } + + public static boolean isSystemTransaction(@NonNull final Bytes bytes) { + return bytes.getByte(0) != APPLICATION_TRANSACTION_MARKER; + } } diff --git a/platform-sdk/platform-apps/tests/MigrationTestingTool/src/test/java/com/swirlds/demo/migration/MigrationTestingToolStateTest.java b/platform-sdk/platform-apps/tests/MigrationTestingTool/src/test/java/com/swirlds/demo/migration/MigrationTestingToolStateTest.java new file mode 100644 index 000000000000..eff503460703 --- /dev/null +++ b/platform-sdk/platform-apps/tests/MigrationTestingTool/src/test/java/com/swirlds/demo/migration/MigrationTestingToolStateTest.java @@ -0,0 +1,229 @@ +/* + * 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.swirlds.demo.migration; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.hedera.hapi.node.base.Timestamp; +import com.hedera.hapi.platform.event.EventCore; +import com.hedera.hapi.platform.event.EventTransaction; +import com.hedera.hapi.platform.event.EventTransaction.TransactionOneOfType; +import com.hedera.hapi.platform.event.GossipEvent; +import com.hedera.hapi.platform.event.StateSignatureTransaction; +import com.hedera.pbj.runtime.OneOf; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; +import com.swirlds.platform.event.PlatformEvent; +import com.swirlds.platform.state.PlatformStateModifier; +import com.swirlds.platform.state.StateLifecycles; +import com.swirlds.platform.system.Round; +import com.swirlds.platform.system.events.ConsensusEvent; +import com.swirlds.platform.system.transaction.ConsensusTransaction; +import com.swirlds.platform.system.transaction.Transaction; +import com.swirlds.platform.system.transaction.TransactionWrapper; +import java.security.SignatureException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.function.Consumer; +import java.util.function.Function; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +class MigrationTestingToolStateTest { + private MigrationTestingToolState state; + private Random random; + private PlatformStateModifier platformStateModifier; + private Round round; + private ConsensusEvent event; + private List> consumedTransactions; + private Consumer> consumer; + private Transaction transaction; + private StateSignatureTransaction stateSignatureTransaction; + + @BeforeEach + void setUp() { + state = new MigrationTestingToolState(mock(StateLifecycles.class), mock(Function.class)); + random = new Random(); + round = mock(Round.class); + event = mock(ConsensusEvent.class); + + consumedTransactions = new ArrayList<>(); + consumer = systemTransaction -> consumedTransactions.add(systemTransaction); + transaction = mock(TransactionWrapper.class); + + final byte[] signature = new byte[384]; + random.nextBytes(signature); + final byte[] hash = new byte[48]; + random.nextBytes(hash); + stateSignatureTransaction = StateSignatureTransaction.newBuilder() + .signature(Bytes.wrap(signature)) + .hash(Bytes.wrap(hash)) + .round(round.getRoundNum()) + .build(); + } + + @Test + void handleConsensusRoundWithApplicationTransaction() throws SignatureException { + givenRoundAndEvent(); + final TransactionGenerator generator = new TransactionGenerator(5); + final var bytes = Bytes.wrap(generator.generateTransaction()); + final var tr = TransactionUtils.parseTransaction(bytes); + when(transaction.getApplicationTransaction()).thenReturn(bytes); + + try (MockedStatic utilities = + Mockito.mockStatic(TransactionUtils.class, Mockito.CALLS_REAL_METHODS)) { + MigrationTestingToolTransaction migrationTestingToolTransaction = Mockito.spy(tr); + utilities.when(() -> TransactionUtils.parseTransaction(any())).thenReturn(migrationTestingToolTransaction); + Mockito.doNothing().when(migrationTestingToolTransaction).applyTo(state); + state.handleConsensusRound(round, platformStateModifier, consumer); + } + + assertThat(consumedTransactions).isEmpty(); + } + + @Test + void handleConsensusRoundWithSystemTransaction() { + givenRoundAndEvent(); + final var stateSignatureTransactionBytes = + StateSignatureTransaction.PROTOBUF.toBytes(stateSignatureTransaction); + when(transaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + + state.handleConsensusRound(round, platformStateModifier, consumer); + + assertThat(consumedTransactions).hasSize(1); + } + + @Test + void handleConsensusRoundWithMultipleSystemTransactions() { + final var secondConsensusTransaction = mock(TransactionWrapper.class); + final var thirdConsensusTransaction = mock(TransactionWrapper.class); + when(round.iterator()).thenReturn(Collections.singletonList(event).iterator()); + when(event.getConsensusTimestamp()).thenReturn(Instant.now()); + when(event.consensusTransactionIterator()) + .thenReturn(List.of( + (ConsensusTransaction) transaction, + secondConsensusTransaction, + thirdConsensusTransaction) + .iterator()); + + final var stateSignatureTransactionBytes = + StateSignatureTransaction.PROTOBUF.toBytes(stateSignatureTransaction); + when(transaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + when(secondConsensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + when(thirdConsensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + + state.handleConsensusRound(round, platformStateModifier, consumer); + + assertThat(consumedTransactions).hasSize(3); + } + + @Test + void handleConsensusRoundWithDeprecatedSystemTransaction() { + givenRoundAndEvent(); + when(transaction.getApplicationTransaction()).thenReturn(Bytes.EMPTY); + when(transaction.isSystem()).thenReturn(true); + + state.handleConsensusRound(round, platformStateModifier, consumer); + + assertThat(consumedTransactions).isEmpty(); + } + + @Test + void preHandleEventWithMultipleSystemTransactions() { + final var gossipEvent = mock(GossipEvent.class); + final var eventCore = mock(EventCore.class); + when(gossipEvent.eventCore()).thenReturn(eventCore); + when(eventCore.timeCreated()).thenReturn(Timestamp.DEFAULT); + final var eventTransaction = mock(EventTransaction.class); + final var secondEventTransaction = mock(EventTransaction.class); + final var thirdEventTransaction = mock(EventTransaction.class); + + final var stateSignatureTransactionBytes = + StateSignatureTransaction.PROTOBUF.toBytes(stateSignatureTransaction); + final var transactionProto = com.hedera.hapi.node.base.Transaction.newBuilder() + .bodyBytes(stateSignatureTransactionBytes) + .build(); + final var transactionBytes = com.hedera.hapi.node.base.Transaction.PROTOBUF.toBytes(transactionProto); + + final var systemTransactionWithType = + new OneOf<>(TransactionOneOfType.APPLICATION_TRANSACTION, transactionBytes); + + when(eventTransaction.transaction()).thenReturn(systemTransactionWithType); + when(secondEventTransaction.transaction()).thenReturn(systemTransactionWithType); + when(thirdEventTransaction.transaction()).thenReturn(systemTransactionWithType); + when(gossipEvent.eventTransaction()) + .thenReturn(List.of(eventTransaction, secondEventTransaction, thirdEventTransaction)); + event = new PlatformEvent(gossipEvent); + + state.preHandle(event, consumer); + + assertThat(consumedTransactions).hasSize(3); + } + + @Test + void preHandleEventWithSystemTransaction() { + final var gossipEvent = mock(GossipEvent.class); + final var eventCore = mock(EventCore.class); + when(eventCore.timeCreated()).thenReturn(Timestamp.DEFAULT); + final var eventTransaction = mock(EventTransaction.class); + when(gossipEvent.eventCore()).thenReturn(eventCore); + when(gossipEvent.eventTransaction()).thenReturn(List.of(eventTransaction)); + + final var stateSignatureTransactionBytes = + StateSignatureTransaction.PROTOBUF.toBytes(stateSignatureTransaction); + final var transactionProto = com.hedera.hapi.node.base.Transaction.newBuilder() + .bodyBytes(stateSignatureTransactionBytes) + .build(); + final var transactionBytes = com.hedera.hapi.node.base.Transaction.PROTOBUF.toBytes(transactionProto); + final var systemTransactionWithType = + new OneOf<>(TransactionOneOfType.APPLICATION_TRANSACTION, transactionBytes); + when(eventTransaction.transaction()).thenReturn(systemTransactionWithType); + event = new PlatformEvent(gossipEvent); + + state.preHandle(event, consumer); + + assertThat(consumedTransactions).hasSize(1); + } + + @Test + void preHandleEventWithDeprecatedSystemTransaction() { + event = mock(PlatformEvent.class); + + when(round.iterator()).thenReturn(Collections.singletonList(event).iterator()); + when(transaction.isSystem()).thenReturn(true); + + state.preHandle(event, consumer); + + assertThat(consumedTransactions).isEmpty(); + } + + private void givenRoundAndEvent() { + when(round.iterator()).thenReturn(Collections.singletonList(event).iterator()); + when(event.getConsensusTimestamp()).thenReturn(Instant.now()); + when(event.consensusTransactionIterator()) + .thenReturn(Collections.singletonList((ConsensusTransaction) transaction) + .iterator()); + } +} From 2bc1e72fa4e241d1fb76a81bf4933492d3853dbc Mon Sep 17 00:00:00 2001 From: IvanKavaldzhiev Date: Tue, 14 Jan 2025 16:03:42 +0200 Subject: [PATCH 08/19] feat: adapt StressTestingTool to work with Bytes representation of a transaction (#17225) Signed-off-by: Ivan Kavaldzhiev --- .../tests/StressTestingTool/build.gradle.kts | 24 +- .../demo/stress/StressTestingToolMain.java | 15 +- .../demo/stress/StressTestingToolState.java | 97 +++++- .../swirlds/demo/stress/TransactionPool.java | 8 +- .../stress/StressTestingToolStateTest.java | 303 ++++++++++++++++++ 5 files changed, 435 insertions(+), 12 deletions(-) create mode 100644 platform-sdk/platform-apps/tests/StressTestingTool/src/test/java/com/swirlds/demo/stress/StressTestingToolStateTest.java diff --git a/platform-sdk/platform-apps/tests/StressTestingTool/build.gradle.kts b/platform-sdk/platform-apps/tests/StressTestingTool/build.gradle.kts index 240c46a781f5..01f2a9e9592f 100644 --- a/platform-sdk/platform-apps/tests/StressTestingTool/build.gradle.kts +++ b/platform-sdk/platform-apps/tests/StressTestingTool/build.gradle.kts @@ -1,6 +1,28 @@ -// SPDX-License-Identifier: Apache-2.0 +/* + * 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. + */ + plugins { id("org.hiero.gradle.module.application") } application.mainClass = "com.swirlds.demo.stress.StressTestingToolMain" mainModuleInfo { annotationProcessor("com.swirlds.config.processor") } + +testModuleInfo { + requires("org.assertj.core") + requires("org.junit.jupiter.api") + requires("org.junit.jupiter.params") + requires("org.mockito") +} diff --git a/platform-sdk/platform-apps/tests/StressTestingTool/src/main/java/com/swirlds/demo/stress/StressTestingToolMain.java b/platform-sdk/platform-apps/tests/StressTestingTool/src/main/java/com/swirlds/demo/stress/StressTestingToolMain.java index e1bbdcd6c1ed..5c5632c8681a 100644 --- a/platform-sdk/platform-apps/tests/StressTestingTool/src/main/java/com/swirlds/demo/stress/StressTestingToolMain.java +++ b/platform-sdk/platform-apps/tests/StressTestingTool/src/main/java/com/swirlds/demo/stress/StressTestingToolMain.java @@ -34,6 +34,8 @@ import static com.swirlds.platform.test.fixtures.state.FakeStateLifecycles.FAKE_MERKLE_STATE_LIFECYCLES; import static com.swirlds.platform.test.fixtures.state.FakeStateLifecycles.registerMerkleStateRootClassIds; +import com.hedera.hapi.platform.event.StateSignatureTransaction; +import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.common.constructable.ClassConstructorPair; import com.swirlds.common.constructable.ConstructableRegistry; import com.swirlds.common.constructable.ConstructableRegistryException; @@ -64,15 +66,15 @@ public class StressTestingToolMain implements SwirldMain { static { try { logger.info(STARTUP.getMarker(), "Registering StressTestingToolState with ConstructableRegistry"); - ConstructableRegistry constructableRegistry = ConstructableRegistry.getInstance(); + final ConstructableRegistry constructableRegistry = ConstructableRegistry.getInstance(); constructableRegistry.registerConstructable(new ClassConstructorPair(StressTestingToolState.class, () -> { - StressTestingToolState stressTestingToolState = new StressTestingToolState( + final StressTestingToolState stressTestingToolState = new StressTestingToolState( FAKE_MERKLE_STATE_LIFECYCLES, version -> new BasicSoftwareVersion(version.major())); return stressTestingToolState; })); registerMerkleStateRootClassIds(); logger.info(STARTUP.getMarker(), "StressTestingToolState is registered with ConstructableRegistry"); - } catch (ConstructableRegistryException e) { + } catch (final ConstructableRegistryException e) { logger.error(STARTUP.getMarker(), "Failed to register StressTestingToolState", e); throw new RuntimeException(e); } @@ -218,7 +220,7 @@ private synchronized void generateTransactions() { } // ramp up the TPS to the expected value - long elapsedTime = now / MILLISECONDS_TO_NANOSECONDS - rampUpStartTimeMilliSeconds; + final long elapsedTime = now / MILLISECONDS_TO_NANOSECONDS - rampUpStartTimeMilliSeconds; final double rampUpTPS; if (elapsedTime < TPS_RAMP_UP_WINDOW_MILLISECONDS) { rampUpTPS = expectedTPS * elapsedTime / ((double) (TPS_RAMP_UP_WINDOW_MILLISECONDS)); @@ -268,4 +270,9 @@ public PlatformMerkleStateRoot newMerkleStateRoot() { public BasicSoftwareVersion getSoftwareVersion() { return SOFTWARE_VERSION; } + + @Override + public Bytes encodeSystemTransaction(@NonNull final StateSignatureTransaction transaction) { + return StateSignatureTransaction.PROTOBUF.toBytes(transaction); + } } diff --git a/platform-sdk/platform-apps/tests/StressTestingTool/src/main/java/com/swirlds/demo/stress/StressTestingToolState.java b/platform-sdk/platform-apps/tests/StressTestingTool/src/main/java/com/swirlds/demo/stress/StressTestingToolState.java index fdeac817306c..c9ded890232b 100644 --- a/platform-sdk/platform-apps/tests/StressTestingTool/src/main/java/com/swirlds/demo/stress/StressTestingToolState.java +++ b/platform-sdk/platform-apps/tests/StressTestingTool/src/main/java/com/swirlds/demo/stress/StressTestingToolState.java @@ -26,6 +26,8 @@ * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. */ +import static com.swirlds.demo.stress.TransactionPool.APPLICATION_TRANSACTION_MARKER; + import com.hedera.hapi.node.base.SemanticVersion; import com.hedera.hapi.platform.event.StateSignatureTransaction; import com.swirlds.common.constructable.ConstructableIgnored; @@ -40,11 +42,14 @@ import com.swirlds.platform.system.SoftwareVersion; import com.swirlds.platform.system.events.Event; import com.swirlds.platform.system.transaction.ConsensusTransaction; +import com.swirlds.platform.system.transaction.Transaction; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.time.Duration; import java.util.function.Consumer; import java.util.function.Function; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; /** * This testing tool simulates configurable processing times for both preHandling and handling for stress testing @@ -54,6 +59,8 @@ public class StressTestingToolState extends PlatformMerkleStateRoot { private static final long CLASS_ID = 0x79900efa3127b6eL; + private static final Logger logger = LogManager.getLogger(StressTestingToolState.class); + /** A running sum of transaction contents */ private long runningSum = 0; @@ -87,6 +94,7 @@ public synchronized StressTestingToolState copy() { /** * {@inheritDoc} */ + @Override public void init( @NonNull final Platform platform, @NonNull final InitTrigger trigger, @@ -102,7 +110,26 @@ public void init( @Override public void preHandle( @NonNull final Event event, - @NonNull final Consumer> stateSignatureTransaction) { + @NonNull + final Consumer> + stateSignatureTransactionCallback) { + event.forEachTransaction(transaction -> { + // We are not interested in pre-handling any system transactions, as they are + // specific for the platform only.We also don't want to consume deprecated + // EventTransaction.STATE_SIGNATURE_TRANSACTION system transactions in the + // callback,since it's intended to be used only for the new form of encoded system + // transactions in Bytes.Thus, we can directly skip the current + // iteration, if it processes a deprecated system transaction with the + // EventTransaction.STATE_SIGNATURE_TRANSACTION type. + if (transaction.isSystem()) { + return; + } + + if (areTransactionBytesSystemOnes(transaction)) { + consumeSystemTransaction(transaction, event, stateSignatureTransactionCallback); + } + }); + busyWait(config.preHandleTime()); } @@ -113,20 +140,78 @@ public void preHandle( public void handleConsensusRound( @NonNull final Round round, @NonNull final PlatformStateModifier platformState, - @NonNull final Consumer> stateSignatureTransaction) { + @NonNull + final Consumer> + stateSignatureTransactionCallback) { throwIfImmutable(); - round.forEachTransaction(this::handleTransaction); + + for (final var event : round) { + event.consensusTransactionIterator().forEachRemaining(transaction -> { + // We are not interested in handling any system transactions, as they are + // specific for the platform only.We also don't want to consume deprecated + // EventTransaction.STATE_SIGNATURE_TRANSACTION system transactions in the + // callback,since it's intended to be used only for the new form of encoded system + // transactions in Bytes.Thus, we can directly skip the current + // iteration, if it processes a deprecated system transaction with the + // EventTransaction.STATE_SIGNATURE_TRANSACTION type. + if (transaction.isSystem()) { + return; + } + + if (areTransactionBytesSystemOnes(transaction)) { + consumeSystemTransaction(transaction, event, stateSignatureTransactionCallback); + } else { + handleTransaction(transaction); + } + }); + } } private void handleTransaction(@NonNull final ConsensusTransaction trans) { - if (trans.isSystem()) { - return; - } runningSum += ByteUtils.byteArrayToLong(trans.getApplicationTransaction().toByteArray(), 0); busyWait(config.handleTime()); } + /** + * Checks if the transaction bytes are system ones. The test creates application transactions with max length of 4. + * System transactions will be always bigger than that. + * + * @param transaction the consensus transaction to check + * @return true if the transaction bytes are system ones, false otherwise + */ + private boolean areTransactionBytesSystemOnes(@NonNull final Transaction transaction) { + final var transactionBytes = transaction.getApplicationTransaction(); + + if (transactionBytes.length() == 0) { + return false; + } + + return transactionBytes.getByte(0) != APPLICATION_TRANSACTION_MARKER; + } + + /** + * Converts a transaction to a {@link StateSignatureTransaction} and then consumes it into a callback. + * + * @param transaction the transaction to consume + * @param event the event that contains the transaction + * @param stateSignatureTransactionCallback the callback to call with the system transaction + */ + private void consumeSystemTransaction( + final @NonNull Transaction transaction, + final @NonNull Event event, + final @NonNull Consumer> + stateSignatureTransactionCallback) { + try { + final var stateSignatureTransaction = + StateSignatureTransaction.PROTOBUF.parse(transaction.getApplicationTransaction()); + stateSignatureTransactionCallback.accept(new ScopedSystemTransaction<>( + event.getCreatorId(), event.getSoftwareVersion(), stateSignatureTransaction)); + } catch (final com.hedera.pbj.runtime.ParseException e) { + logger.error("Failed to parse StateSignatureTransaction", e); + } + } + @SuppressWarnings("all") private void busyWait(@NonNull final Duration duration) { if (!duration.isZero() && !duration.isNegative()) { diff --git a/platform-sdk/platform-apps/tests/StressTestingTool/src/main/java/com/swirlds/demo/stress/TransactionPool.java b/platform-sdk/platform-apps/tests/StressTestingTool/src/main/java/com/swirlds/demo/stress/TransactionPool.java index 3aa1c11f3923..24e7cd8b55fd 100644 --- a/platform-sdk/platform-apps/tests/StressTestingTool/src/main/java/com/swirlds/demo/stress/TransactionPool.java +++ b/platform-sdk/platform-apps/tests/StressTestingTool/src/main/java/com/swirlds/demo/stress/TransactionPool.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2024 Hedera Hashgraph, LLC + * Copyright (C) 2022-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. @@ -23,6 +23,9 @@ * Provides pre-generated random transactions. */ final class TransactionPool { + + public static final byte APPLICATION_TRANSACTION_MARKER = 1; + /** * the array of transactions */ @@ -58,6 +61,9 @@ final class TransactionPool { for (int i = 0; i < transactions.length; i++) { final byte[] data = new byte[transactionSize]; random.nextBytes(data); + // Add byte with value of 1 as a marker to indicate the start of an application transaction. This is used + // to later differentiate between application transactions and system transactions. + data[0] = APPLICATION_TRANSACTION_MARKER; transactions[i] = data; } } diff --git a/platform-sdk/platform-apps/tests/StressTestingTool/src/test/java/com/swirlds/demo/stress/StressTestingToolStateTest.java b/platform-sdk/platform-apps/tests/StressTestingTool/src/test/java/com/swirlds/demo/stress/StressTestingToolStateTest.java new file mode 100644 index 000000000000..d2da88c45171 --- /dev/null +++ b/platform-sdk/platform-apps/tests/StressTestingTool/src/test/java/com/swirlds/demo/stress/StressTestingToolStateTest.java @@ -0,0 +1,303 @@ +/* + * 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.swirlds.demo.stress; + +import static com.hedera.hapi.platform.event.EventTransaction.TransactionOneOfType.APPLICATION_TRANSACTION; +import static com.hedera.hapi.platform.event.EventTransaction.TransactionOneOfType.STATE_SIGNATURE_TRANSACTION; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.hedera.hapi.node.base.SemanticVersion; +import com.hedera.hapi.node.base.Timestamp; +import com.hedera.hapi.platform.event.EventCore; +import com.hedera.hapi.platform.event.EventTransaction; +import com.hedera.hapi.platform.event.GossipEvent; +import com.hedera.hapi.platform.event.StateSignatureTransaction; +import com.hedera.pbj.runtime.OneOf; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.common.context.PlatformContext; +import com.swirlds.common.crypto.Hash; +import com.swirlds.common.merkle.crypto.internal.MerkleCryptoEngine; +import com.swirlds.common.metrics.platform.DefaultPlatformMetrics; +import com.swirlds.common.platform.NodeId; +import com.swirlds.common.test.fixtures.Randotron; +import com.swirlds.config.api.ConfigurationBuilder; +import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; +import com.swirlds.platform.crypto.KeyGeneratingException; +import com.swirlds.platform.crypto.KeysAndCerts; +import com.swirlds.platform.crypto.PlatformSigner; +import com.swirlds.platform.crypto.PublicStores; +import com.swirlds.platform.event.PlatformEvent; +import com.swirlds.platform.state.PlatformStateModifier; +import com.swirlds.platform.state.StateLifecycles; +import com.swirlds.platform.system.InitTrigger; +import com.swirlds.platform.system.Platform; +import com.swirlds.platform.system.Round; +import com.swirlds.platform.system.SoftwareVersion; +import com.swirlds.platform.system.events.ConsensusEvent; +import com.swirlds.platform.system.transaction.ConsensusTransaction; +import com.swirlds.platform.system.transaction.Transaction; +import com.swirlds.platform.system.transaction.TransactionWrapper; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class StressTestingToolStateTest { + + private static final byte[] EMPTY_ARRAY = new byte[] {}; + private StressTestingToolState state; + private StressTestingToolMain main; + private PlatformStateModifier platformStateModifier; + private Round round; + private ConsensusEvent event; + private Consumer> consumer; + private List> consumedSystemTransactions; + private Transaction transaction; + private StateSignatureTransaction stateSignatureTransaction; + + @BeforeEach + void setUp() throws NoSuchAlgorithmException, KeyStoreException, KeyGeneratingException, NoSuchProviderException { + state = new StressTestingToolState(mock(StateLifecycles.class), mock(Function.class)); + main = new StressTestingToolMain(); + platformStateModifier = mock(PlatformStateModifier.class); + event = mock(PlatformEvent.class); + + when(event.transactionIterator()).thenReturn(Collections.emptyIterator()); + round = mock(Round.class); + + consumedSystemTransactions = new ArrayList<>(); + consumer = systemTransaction -> consumedSystemTransactions.add(systemTransaction); + transaction = mock(TransactionWrapper.class); + + final Randotron randotron = Randotron.create(); + + final var keysAndCerts = + KeysAndCerts.generate("a-name", EMPTY_ARRAY, EMPTY_ARRAY, EMPTY_ARRAY, new PublicStores()); + + final var signer = new PlatformSigner(keysAndCerts); + final Hash stateHash = randotron.nextHash(); + final Bytes signature = signer.signImmutable(stateHash); + + stateSignatureTransaction = StateSignatureTransaction.newBuilder() + .round(1000L) + .signature(signature) + .hash(stateHash.getBytes()) + .build(); + + final var platform = mock(Platform.class); + final var initTrigger = mock(InitTrigger.class); + final var softwareVersion = mock(SoftwareVersion.class); + final var platformContext = mock(PlatformContext.class); + final var config = ConfigurationBuilder.create() + .withConfigDataType(StressTestingToolConfig.class) + .build(); + final var metrics = mock(DefaultPlatformMetrics.class); + final var cryptography = mock(MerkleCryptoEngine.class); + when(platform.getContext()).thenReturn(platformContext); + when(platformContext.getConfiguration()).thenReturn(config); + when(platformContext.getMetrics()).thenReturn(metrics); + when(platformContext.getMerkleCryptography()).thenReturn(cryptography); + + state.init(platform, initTrigger, softwareVersion); + } + + @ParameterizedTest + @ValueSource(ints = {100, 440, 600}) + void handleConsensusRoundWithApplicationTransaction(final Integer transactionSize) { + // Given + givenRoundAndEvent(); + + final var pool = new TransactionPool(1, transactionSize); + + when(transaction.getApplicationTransaction()).thenReturn(Bytes.wrap(pool.transaction())); + + // When + state.handleConsensusRound(round, platformStateModifier, consumer); + + // Then + assertThat(consumedSystemTransactions.size()).isZero(); + } + + @Test + void handleConsensusRoundWithSystemTransaction() { + // Given + givenRoundAndEvent(); + + final var stateSignatureTransactionBytes = main.encodeSystemTransaction(stateSignatureTransaction); + when(transaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + + // When + state.handleConsensusRound(round, platformStateModifier, consumer); + + // Then + assertThat(consumedSystemTransactions.size()).isEqualTo(1); + } + + @Test + void handleConsensusRoundWithMultipleSystemTransaction() { + // Given + final var secondConsensusTransaction = mock(TransactionWrapper.class); + final var thirdConsensusTransaction = mock(TransactionWrapper.class); + when(round.iterator()).thenReturn(List.of(event).iterator()); + when(event.getConsensusTimestamp()).thenReturn(Instant.now()); + when(event.consensusTransactionIterator()) + .thenReturn(List.of( + (ConsensusTransaction) transaction, + secondConsensusTransaction, + thirdConsensusTransaction) + .iterator()); + + final var stateSignatureTransactionBytes = main.encodeSystemTransaction(stateSignatureTransaction); + when(transaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + when(secondConsensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + when(thirdConsensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + + // When + state.handleConsensusRound(round, platformStateModifier, consumer); + + // Then + assertThat(consumedSystemTransactions.size()).isEqualTo(3); + } + + @Test + void handleConsensusRoundWithDeprecatedSystemTransaction() { + // Given + givenRoundAndEvent(); + + when(transaction.getApplicationTransaction()).thenReturn(Bytes.EMPTY); + when(transaction.isSystem()).thenReturn(true); + + // When + state.handleConsensusRound(round, platformStateModifier, consumer); + + // Then + assertThat(consumedSystemTransactions.size()).isZero(); + } + + @ParameterizedTest + @ValueSource(ints = {100, 440, 600}) + void preHandleConsensusRoundWithApplicationTransaction(final Integer transactionSize) { + // Given + givenRoundAndEvent(); + + final var pool = new TransactionPool(1, transactionSize); + + final var eventTransaction = + new EventTransaction(new OneOf<>(APPLICATION_TRANSACTION, Bytes.wrap(pool.transaction()))); + final var eventCore = mock(EventCore.class); + final var gossipEvent = new GossipEvent(eventCore, null, List.of(eventTransaction), Collections.emptyList()); + when(eventCore.timeCreated()).thenReturn(Timestamp.DEFAULT); + event = new PlatformEvent(gossipEvent); + + // When + state.preHandle(event, consumer); + + // Then + assertThat(consumedSystemTransactions.size()).isZero(); + } + + @Test + void preHandleConsensusRoundWithSystemTransaction() { + // Given + givenRoundAndEvent(); + + final var stateSignatureTransactionBytes = main.encodeSystemTransaction(stateSignatureTransaction); + final var eventTransaction = + new EventTransaction(new OneOf<>(APPLICATION_TRANSACTION, stateSignatureTransactionBytes)); + final var eventCore = mock(EventCore.class); + final var gossipEvent = new GossipEvent(eventCore, null, List.of(eventTransaction), Collections.emptyList()); + when(eventCore.timeCreated()).thenReturn(Timestamp.DEFAULT); + event = new PlatformEvent(gossipEvent); + + // When + state.preHandle(event, consumer); + + // Then + assertThat(consumedSystemTransactions.size()).isEqualTo(1); + } + + @Test + void preHandleConsensusRoundWithMultipleSystemTransaction() { + // Given + when(event.getConsensusTimestamp()).thenReturn(Instant.now()); + + final var stateSignatureTransactionBytes = main.encodeSystemTransaction(stateSignatureTransaction); + + final var eventTransaction = + new EventTransaction(new OneOf<>(APPLICATION_TRANSACTION, stateSignatureTransactionBytes)); + final var secondEventTransaction = + new EventTransaction(new OneOf<>(APPLICATION_TRANSACTION, stateSignatureTransactionBytes)); + final var thirdEventTransaction = + new EventTransaction(new OneOf<>(APPLICATION_TRANSACTION, stateSignatureTransactionBytes)); + final var eventCore = mock(EventCore.class); + final var gossipEvent = new GossipEvent( + eventCore, + null, + List.of(eventTransaction, secondEventTransaction, thirdEventTransaction), + Collections.emptyList()); + when(eventCore.timeCreated()).thenReturn(Timestamp.DEFAULT); + event = new PlatformEvent(gossipEvent); + + // When + state.preHandle(event, consumer); + + // Then + assertThat(consumedSystemTransactions.size()).isEqualTo(3); + } + + @Test + void preHandleConsensusRoundWithDeprecatedSystemTransaction() { + // Given + givenRoundAndEvent(); + when(transaction.isSystem()).thenReturn(true); + + final var stateSignatureTransactionBytes = main.encodeSystemTransaction(stateSignatureTransaction); + final var eventTransaction = + new EventTransaction(new OneOf<>(STATE_SIGNATURE_TRANSACTION, stateSignatureTransactionBytes)); + final var eventCore = mock(EventCore.class); + final var gossipEvent = new GossipEvent(eventCore, null, List.of(eventTransaction), Collections.emptyList()); + when(eventCore.timeCreated()).thenReturn(Timestamp.DEFAULT); + event = new PlatformEvent(gossipEvent); + + // When + state.preHandle(event, consumer); + + // Then + assertThat(consumedSystemTransactions.size()).isZero(); + } + + private void givenRoundAndEvent() { + when(event.getCreatorId()).thenReturn(new NodeId()); + when(event.getSoftwareVersion()).thenReturn(new SemanticVersion(1, 1, 1, "", "")); + when(event.getConsensusTimestamp()).thenReturn(Instant.now()); + when(event.consensusTransactionIterator()) + .thenReturn(Collections.singletonList((ConsensusTransaction) transaction) + .iterator()); + when(round.iterator()).thenReturn(Collections.singletonList(event).iterator()); + } +} From eeea59f8d86c62c132e71dcb394022c372d5f790 Mon Sep 17 00:00:00 2001 From: Derek Riley Date: Tue, 14 Jan 2025 10:13:41 -0500 Subject: [PATCH 09/19] chore: Additional Block Stream Validators for number and nonce (#17251) Signed-off-by: Derek Riley --- .../block/BlockItemNonceValidator.java | 106 ++++++++++++++++++ .../block/BlockNumberSequenceValidator.java | 75 +++++++++++++ .../utilops/streams/StreamValidationOp.java | 25 ++++- 3 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/validators/block/BlockItemNonceValidator.java create mode 100644 hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/validators/block/BlockNumberSequenceValidator.java diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/validators/block/BlockItemNonceValidator.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/validators/block/BlockItemNonceValidator.java new file mode 100644 index 000000000000..d1d784ae9f4a --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/validators/block/BlockItemNonceValidator.java @@ -0,0 +1,106 @@ +/* + * 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. + * 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.junit.support.validators.block; + +import com.hedera.hapi.block.stream.Block; +import com.hedera.hapi.block.stream.BlockItem; +import com.hedera.hapi.node.base.Transaction; +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.pbj.runtime.ParseException; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.hedera.services.bdd.junit.support.BlockStreamValidator; +import com.hedera.services.bdd.spec.HapiSpec; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.junit.jupiter.api.Assertions; + +/** + * A validator that ensures nonces are properly incremented for related transactions. + * For transactions with the same accountID and validStart, nonces should increment by 1. + */ +public class BlockItemNonceValidator implements BlockStreamValidator { + private static final Logger logger = LogManager.getLogger(BlockItemNonceValidator.class); + + public static final Factory FACTORY = new Factory() { + @NonNull + @Override + public BlockStreamValidator create(@NonNull final HapiSpec spec) { + return new BlockItemNonceValidator(); + } + }; + + private TransactionID getTransactionIdFromBlockItem(BlockItem item) throws ParseException { + if (item.hasEventTransaction()) { + Bytes txnBytes = Objects.requireNonNull(item.eventTransaction()).applicationTransactionOrThrow(); + Transaction txn = Transaction.PROTOBUF.parse(txnBytes); + // Skip if no transaction body, but still fail on parse errors + if (!txn.hasBody()) { + return null; + } + TransactionBody body = txn.bodyOrThrow(); + return body.hasTransactionID() ? body.transactionIDOrThrow() : null; + } + return null; + } + + @Override + public void validateBlocks(@NonNull final List blocks) { + logger.info("Beginning validation of BlockItem nonces"); + + // Track highest nonce seen for each account+validStart combination + Map highestNonces = new HashMap<>(); + + for (Block block : blocks) { + for (BlockItem item : block.items()) { + TransactionID txnId = null; + try { + txnId = getTransactionIdFromBlockItem(item); + } catch (ParseException e) { + throw new RuntimeException(e); + } + + if (txnId != null && txnId.hasAccountID() && txnId.hasTransactionValidStart()) { + // Create key from accountID and validStart + String key = txnId.accountID().toString() + ":" + + txnId.transactionValidStart().toString(); + int nonce = txnId.nonce(); + + // If we've seen this account+validStart before, verify nonce increment + if (highestNonces.containsKey(key)) { + int prevNonce = highestNonces.get(key); + if (nonce != prevNonce + 1) { + Assertions.fail(String.format( + "Invalid nonce increment for transaction with account=%s, validStart=%s: previous=%d, current=%d", + txnId.accountID(), txnId.transactionValidStart(), prevNonce, nonce)); + } + } + + // Update highest nonce seen for this account+validStart + highestNonces.put(key, nonce); + } + } + } + + logger.info("BlockItem nonce validation completed successfully"); + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/validators/block/BlockNumberSequenceValidator.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/validators/block/BlockNumberSequenceValidator.java new file mode 100644 index 000000000000..d761802c8838 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/validators/block/BlockNumberSequenceValidator.java @@ -0,0 +1,75 @@ +/* + * 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. + * 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.junit.support.validators.block; + +import com.hedera.hapi.block.stream.Block; +import com.hedera.services.bdd.junit.support.BlockStreamValidator; +import com.hedera.services.bdd.spec.HapiSpec; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.junit.jupiter.api.Assertions; + +/** + * A validator that ensures block numbers are sequential and not repeated in the block stream. + */ +public class BlockNumberSequenceValidator implements BlockStreamValidator { + private static final Logger logger = LogManager.getLogger(BlockNumberSequenceValidator.class); + + public static final Factory FACTORY = new Factory() { + @NonNull + @Override + public BlockStreamValidator create(@NonNull final HapiSpec spec) { + return new BlockNumberSequenceValidator(); + } + }; + + @Override + public void validateBlocks(@NonNull final List blocks) { + logger.info("Beginning validation of block number sequence"); + + Set seenBlockNumbers = new HashSet<>(); + long expectedBlockNumber = -1; + + for (int i = 0; i < blocks.size(); i++) { + Block block = blocks.get(i); + long currentBlockNumber = Objects.requireNonNull( + block.items().getFirst().blockHeader()) + .number(); + + // Check if block number is repeated + if (!seenBlockNumbers.add(currentBlockNumber)) { + Assertions.fail(String.format("Block number %d is repeated at position %d", currentBlockNumber, i)); + } + + // Check if block number is sequential + if (expectedBlockNumber >= 0 && currentBlockNumber != expectedBlockNumber) { + Assertions.fail(String.format( + "Block number %d at position %d is not sequential. Expected %d", + currentBlockNumber, i, expectedBlockNumber)); + } + + expectedBlockNumber = currentBlockNumber + 1; + } + + logger.info("Block number sequence validation completed successfully"); + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/StreamValidationOp.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/StreamValidationOp.java index 554928373813..a988fd59012c 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/StreamValidationOp.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/StreamValidationOp.java @@ -1,4 +1,19 @@ -// SPDX-License-Identifier: Apache-2.0 +/* + * 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.spec.utilops.streams; import static com.hedera.node.config.types.StreamMode.RECORDS; @@ -28,6 +43,8 @@ import com.hedera.services.bdd.junit.support.validators.TokenReconciliationValidator; import com.hedera.services.bdd.junit.support.validators.TransactionBodyValidator; import com.hedera.services.bdd.junit.support.validators.block.BlockContentsValidator; +import com.hedera.services.bdd.junit.support.validators.block.BlockItemNonceValidator; +import com.hedera.services.bdd.junit.support.validators.block.BlockNumberSequenceValidator; import com.hedera.services.bdd.junit.support.validators.block.StateChangesValidator; import com.hedera.services.bdd.junit.support.validators.block.TransactionRecordParityValidator; import com.hedera.services.bdd.spec.HapiSpec; @@ -65,7 +82,11 @@ public class StreamValidationOp extends UtilOp { new TokenReconciliationValidator()); private static final List BLOCK_STREAM_VALIDATOR_FACTORIES = List.of( - TransactionRecordParityValidator.FACTORY, StateChangesValidator.FACTORY, BlockContentsValidator.FACTORY); + TransactionRecordParityValidator.FACTORY, + StateChangesValidator.FACTORY, + BlockContentsValidator.FACTORY, + BlockNumberSequenceValidator.FACTORY, + BlockItemNonceValidator.FACTORY); public static void main(String[] args) {} From c0ce03afcc89022679733cd525223ae89e37ad71 Mon Sep 17 00:00:00 2001 From: Lazar Petrovic Date: Tue, 14 Jan 2025 17:06:04 +0100 Subject: [PATCH 10/19] chore: cleanup legacy event serialization (#17350) Signed-off-by: Lazar Petrovic --- .../2024-08-26T10_38_35.016340634Z.events | Bin 523684 -> 0 bytes .../2024-09-10T00_00_00.021456201Z.events | Bin 530775 -> 0 bytes .../platform/core/jmh/EventBenchmarks.java | 8 +- .../platform/builder/PlatformBuilder.java | 3 - .../event/EventSerializationUtils.java | 127 +---------- .../event/preconsensus/PcesFileIterator.java | 2 - .../event/preconsensus/PcesFileVersion.java | 4 +- .../recovery/EventRecoveryWorkflow.java | 6 +- .../system/StaticSoftwareVersion.java | 72 ------ .../platform/system/events/CesEvent.java | 5 +- .../system/events/EventDescriptorWrapper.java | 205 +----------------- .../swirlds/platform/event/CesEventTest.java | 11 +- .../platform/event/EventMigrationTest.java | 20 +- .../EventStreamMultiFileIteratorTest.java | 11 +- .../recovery/EventStreamPathIteratorTest.java | 11 +- .../EventStreamRoundIteratorTest.java | 11 +- .../EventStreamSingleFileIteratorTest.java | 11 +- .../recovery/ObjectStreamIteratorTest.java | 11 +- .../turtle/gossip/SimulatedGossipTests.java | 13 +- .../2025-01-10T00_00_00.016538241Z.events | Bin 0 -> 371587 bytes .../cli/EventStreamReportingToolTest.java | 19 +- .../test/event/PlatformEventTest.java | 30 +-- .../PcesBirthRoundMigrationTests.java | 16 +- .../preconsensus/PcesReadWriteTests.java | 17 -- .../event/preconsensus/PcesWriterTests.java | 17 -- .../swirlds/platform/test/sync/SyncTests.java | 15 +- 26 files changed, 26 insertions(+), 619 deletions(-) delete mode 100644 hedera-node/hedera-app/src/test/resources/eventFiles/previewnet-53/2024-08-26T10_38_35.016340634Z.events delete mode 100644 hedera-node/hedera-app/src/test/resources/eventFiles/testnet-53/2024-09-10T00_00_00.021456201Z.events delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/StaticSoftwareVersion.java rename {hedera-node/hedera-app/src/test/java/com/hedera/node/app => platform-sdk/swirlds-platform-core/src/test/java/com/swirlds}/platform/event/EventMigrationTest.java (78%) create mode 100644 platform-sdk/swirlds-platform-core/src/test/resources/eventFiles/testnet-57/2025-01-10T00_00_00.016538241Z.events diff --git a/hedera-node/hedera-app/src/test/resources/eventFiles/previewnet-53/2024-08-26T10_38_35.016340634Z.events b/hedera-node/hedera-app/src/test/resources/eventFiles/previewnet-53/2024-08-26T10_38_35.016340634Z.events deleted file mode 100644 index 85328cdde11d9abf43dd8fc37c221bc1dac4ba11..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 523684 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ü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∾_={hHa5zgKXt`~M;CEr6?PwzXa2LYzQcNr=0L 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�LZvjNST`6dBOKI1?=FM&dn3_avV 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 a$vZJYOeEUYzj$3(o1DZvAb{pN9Qt1uTgp}d diff --git a/hedera-node/hedera-app/src/test/resources/eventFiles/testnet-53/2024-09-10T00_00_00.021456201Z.events b/hedera-node/hedera-app/src/test/resources/eventFiles/testnet-53/2024-09-10T00_00_00.021456201Z.events deleted file mode 100644 index c062defc5c4b9ec1e4d428b9ce765eeae42dae69..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 530775 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

cb%{}iPe0UAhgyzIee6fuMs?aHAY7So8Kykm4 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%0A&2iNzY<<4l~lJz zm@9)iC@S1cI{iFn?GhTeTd*fSS*yP@#kd_U?nF~ZtIAjzit7n@WzcCTo8@(XdEV!5t1UOh zqwwB}6kWh{73I7vLx$F6Lxt;olFx7XW^@CH@rOv0@S;Tw=XW*^qdsyW^ZMiR_H@Gzrlz`^5X6E`F^38(mNLZymQE>H7uWua-89 z>VWm3Nm}7PVT@ElH6N|IfzjSf*WJ8-@ESoUSkaWqMb6sPgyoinQHo)c9fVnG|gd_!l;L})wlBIuw14hy(>ZNU)s7b#}&Vgu}kDhjVJ5x4T>f0s8s_`tbpMpxC2T@Jp zM%lhjoX6k0+TkS{=$#7|Q?pT)%<1Ul{L$^2Ui0V-T2@~8&4ga1(RJhbiNz)zW7goi zRDTsFzcKW$lX zeja__RP6!-wm?qWtXb2fbgf=2_b5-H{K`oj>k@H)(tz+u7Qf*e8T(2w)#VS z1G4?e;rzqnRKMW+A0H{u$whhkRp0qelM$u74Q8l?kHXy0rr0Iw?liLrWyuz@DZ{0} zH`9DQD}lRuX%n1eFlyX0(j~HI{Fy@mQe4QpbxVVYT6Czc_tL>>SFqzA{wy*TUwrUl z6sLAj6}jobV!MC zk=T>wfNT+5HoDyBnsduJZMW@?I>DJDx!=gMm~C~I3TMQOjD8te%bRi@x$F`*|48FMqMP12`6<7tw!O5v&6$%)`g2<{ zhqwGB&9_c?v^})T8W8NIhkV}4jAB%ge3CFq1$W*Cii(EGgz=0t2=H(#(uX=Q#-0xj zDopvTAGMpgP-u5%-)Km8CnJ*G5>s>dm>@^?QdorYF$}qFw|jWbZXQ6#_n(L>`5jAi z9^Tc&x+h)F*Bh^aMZ$2_W5>8!SLcUh53KfrFmVQI; zYHG2cPkh?NLoxp^X6m5Y<<|nl_ygZIe~9n@m}&Wk=h6Iv?|<4q`PaS-|BrxyLl5@e zwc?^)t72qz(PD#`u)QB5svSzw95{8H5HY&Mt@lU&ybn>CLXLsmEt1e<)9!j4 zqVZNt3OfaE97T_P!i7Zj5;~BcM)Tq!)Le!e{oPDCZ6GQy+Twtu-|%Yn+Nv3?yzeGd z);5klM2R5{>TfZ>EpX7RYI+$TI93*2&02t zW$Cp?c4`*$ejf(CPN8qivQI-k1{3|t6lRaJWQ!(U$ z#mp#sPM^ZNsMWOG2+V$->CuNF$WNsIC`@F>c^N|K{6Rz#a z)`0U@UoNp*uCf)1KH1n#YS4S9d&rW=p}n-AepwmucK0hBk97q0NH=XKJ$rTyU2OUr zQ)-Fzz>(;6F}Lv{9uq`aA}X^divQ0618{R7&a8Or;-Fd_f5&mxyh&A^_6huEyC~XA z|C{snIQy)ca-#JzfoIj*j!%QPVSkqkVbq|9$ib+(b>`6!W_?E$<;q_<8HT3-ng5%asAn&;ino z0_S|3dtW{xWbx&siaC6W)2ozjh8~cahG(ZMk!K~H+_dxQj9oC%Dh;--&TXDZy0^#& z9VIupeC5R8U;7;C3{75>@MZuP5%Flhp&jDv;a|N4&IL7iSFS0AFuJ<6RW4g=iJn1IYq_Ci8QK!m|BC3hO@%GNW#^F9i=1=wd%N#!40eJx?Mim0?PZuEl7Ygf6!fpZDSy-t*>fV7_sGGrDS!Bf(&Bb<>( zjl7ugP;-q}0~#UE9fRhvEqL^1w#IAV@=b4LK)M8qLb%#gu(HWXb%mf*<3dAyG@>i| ziYcRKw|mw-8=m#E@2iu{7A*~hu01Dkn(WUhwpCuEntNgH&X9o2l|SQ(p%k>&XVk#q z_q2XXb2+$A$!LgG&u|`Iw&P{)rF?Cz?-ZYM+2w>yxUOMrfo+McGkGdy0Ot7)il$7p zOm=lQsJn7pxBOWRhIIeNS_;7(WsVE6vXldpD^^gy4->gUTf9phV}DRKMD__@fJuTq z1@?Y5W;cA?QM&-^o3J%Ab(Bq>_rfd4$>z-s66L{;9v^viUTQ=<9}j%Ap>S&gCNmsr zdOrBl9yo-yEV9hws{oa0fy01U6Mi`p){PL4r}aKho09MwRVS96vIUVta9sbaQSw7i z0R&IfJEF5P8v+W%{r1|K23@*pZ^j0_5^KC~!bFM@#k ze~t<+4Uh#jBrn)#CEf%Ly|q7b&zzCQ+lbN#mso8v6UKY*c&4++-p)Z%09FF&+ zWXicyC&zOTv*PcytFq5wV6>6drLdLCvGMlSwysS3l8N0x_X_Iuar)YuQ0=xx&J^Y% zA)SaWoCi)?%b5M1e`1n&ZuFr&tswp_GmOux`XMq}r7y6^$8ikpPYr$;& zxuFi7gV{yCJDI_V@Anm~X6*-3yp_j~ug3^f$SQ059Bc z6c(B;`|@9p{m=1#cK?*Z@(06C$^2Tvt(UXkp=i(iLc7qdk~p+PVu&xag4zi z!n#zce4US%o}|Ym>?D{Nt7Yfspb78d?;^?1l`+ermJ{n>BNuk9PAhn$+tO@4eIO?e zSVG0KjgeoWjpH9jj)3zg7uBN=?$+8uPz%zyLB3(Aal&BmLe0=;60xw1bQAh2XDr(mBVLA@Rgm>EtBS`;dKo<|4Sa~F zVa0=SV9EVwqpsC!aq-N0!zJLumI~GsHau-YmU=fnw@MUaWgl@N*7t`+uAlmqB{CIU z`@HujH4HGBGM>q+z|7KH;U2%$d@YIruTg^aY7e7#ilU(jd%y2;VYs{mL4IeM%Mw?sXl+6sS0|c_e-VSmXY8zJC+pjP%Ug2X-$RpP6DNY*c8S)5B2H zOQWEne;o(?skksp8NV?;^_LSB>$f>I;T`VS?1o9q8$34n(AiEjiNDZtFb?|hq! z%~6MPH3l*5XPIWJAzJ7umt}A9uTZT<>S{~gX-7q@yed{UY4P`q)b9hX(I-Tmow%rA8bd4osW~|4j0iv5LZ0UHy4!67v{~n$GDUr zxpg=c_EpmCAPfd2Sb1*IcP`VSZ2+jr`&nxnr=dcvex0yCj?!eStP z*`_L&7RRM~R^OlPqvQ!&CX!{>@=KuaKOmznR4Hx?Q*ZAqX8 zV$rGx@Rn{Xg8x6_-U7O+oom#tySpoOcXxL$HR?uO%eQ zecl5v-ap@GQktoG!Y_mb5B8U%a#X(()o*pC{Fwmk6;R(m+0Wi7p6`#c%bucopq{_IDIP#fm(~qF2zN>5+nMVmNaE1B}c#>o5vA$MQnIt z4@d}ODyo32aEbH~#N;-6x-__)X_6-ly18!z6@}3XL1KoQxR(~2dxoBKt2#Tb)p-fy zrdShrZ3-XOT@C*ZjJtye1dr>!5LEr!OxOK87&-WtuHBhm;v3q19=zYxC+tAuMhx~o z6v^Gq?A@F_&*Xx0z5VRr%~eS5Rh{BMR#6lf7>jjXqA0esO}}X<0q^ks70*@g_C0D) zi>0K#B6SPNnv{t(|l&G!@Ef0({s@coD9z<@92C;k1O$Uy(YfXd?1i0HZ_=gK{H z>h{X~(~FK{@Nf%Bz32X3=xp^3Z&`LErN|_)$=|P-ho`U}+NZAB!F9<$WZ%3&N5tRZ zt1=50dgm7Permr}n)F6h0&u{oCoCak%>(MlY9EM0_d>?A(#9m;mn70uUu1E7* z9-&$_vQoB2_=Z0!+RA5nlQ_q$c|lMpkvw>>11CewLzeI=vuFZ1n5=+J0@c zM@n1ATe4-S#Wyn7@j9m4N&dJtqN?y6#>IK1`{Ez>>W@-+%e!rFy&OgPIcAp+<;Eb? zprw4Zi#eQDH{U*oodaL5@dmed5u)s`jGw^Hd_Rf7OYEI+5lDO>P0qS?jw|cGxq`nu zyWTdZv~m^Ypu!giba(zg52z}NgvQWP@OevEHNdWBQLR35oYN{ana`q#h2LLJE=cew zX5h~fdz2k39Yb!cB--kAV6btl(uXZ2prCza9P{?b8APKx(F2>)#mPxVlAA80UR|JR z6Mr%WH(F9jqYf&~P|YB^5zgB+KP)>N@9Vi0FolO9BO09I#9{DoR@R%tURF877S3cT zKdDsPAwd+WJ@f^a& z_?3?|4#Ci%*QgfL)vEh^c8#~(D^MM9)RibkpS&}3qQb3t7h6Kzj}h>=bXIP8;E!2t z`f`0dRSQJfnE^d2Q>o_k%V$xlEK*!9%6B7;Xd0{nkBf>697Zxb02%&9Vd2Co_5s`F z4;`}ezooD!%Y<3)ue?AozZ;x?sSSXC=C43vf2aRKVg2DQ|9cI-ZQelo&sMJQ9dfZuPH|k~lipx?*TkRio_uJ_DX4ZWS?K}5TgTqYShm|e>kl}T{eW-r3#qMGtO^aG8A zB3}DFE+&om4@akS9NI?s^$dx9?UR}4_ zLXkqvr(CweeQ!izRXmT}me#jv^8vB{;~vK>*~nF6?g-oa@v^$kZiI^)qe6%DPfSL) zlLD>Kb)wap);5l;ijQ?DES{WtGn*T!(rUT~5y5_epmk=nKPYFDE|v|eL~WCpZVEz* zGXa%&*l_NW;>VU4-fD7$A$!kw9x9D8{po?U>N2;A=GfxF@@bGPC2JqqtY0cwNaMZV zXFVyuPoGh_c7kZ7!(9u@g$Xp(Roz-&f@H8pn2+TZF2`_mhaKU?i926cxxVD_m30N@ zlP>PA%bFj00T9rC^Zip7gA=Rz&nX#~Kg2h1na58ZvUBc3iOK*sh;0x>ls{acAbENU zWtApZ1l{6OV$AL5UspNG;pK%;=!GMgPM)~D=lh#uAZ5V3fF92Ozxlqi5CK%=JKuOG z{F5d9_1j3-^YyTTWU8fJsFlf|#9_3^%ms7$6|GS+NNIL>M2O|km7MqVcGXQYdwH26 zSV9xVEm@9TRBlA@$vU>tDL#>f!*g=!Sd$Gz3_p9tPuG6)!P(xb(%ZB5^-?y88O0G; z4(dds?K7%Vk;-No2G{TyMiK+x4r^b7t|!iDC9-1(QBDWYhC*{zM>@(?0!T^%mN z302|oxT|Fq`frYP*PF7>`|t{^1|?aGd71o@l-#v774Qp*<^l~Ek-zZYLJXaRtGmU&P$h67fyVp+JanrKYwyfltzo7 z10qgU-`*GO=5zM;o}Izf`hA!YxkT%OK)Boani);I$(PWM5Xi?0a|4G7-WcM>O0RYv z&Mi@iVIAdR@iRK-Gpruu@r@#nBG7!|2=R?|dz+9AXIQrzqT`#DL)mJT0nEj{cw2uy z@v|=w5~#TyuXD0x13DxPu9Dn4Umu?BKBJJFpKtin}yC z+zvIo#zvGsE`;;w+N>UoJ9{lHYmY3hDjui9@I~tSOogN`qj|`{ zmGkWbAil9YZXC+vVh#4I4!Oh(y^GH7(CzaxqTwP%*Ku?=ij7B}GNCC}tyA4ePM^Tr zsllq6{+AMEIAsPS2cYrkdgtqCcjXEhp`U_JQa4=0&MJhWE~%Twp=)Z7KA{tH1VxVn zWbs?Rb^HKN0qv7-e7pT2zJbe7f70KyiEjx&=f>(uUdsiWi(MI6cG|ssy;`_)Yu_2> z!oEjLWGXooj(vFVt1#Z}m&no$`207%fqDItzF+Wd`dI_5JF-326U~DJ~tSr+|w=i0oe7?A4>R(*?;s9z(RlG8`WmW5|H+tZ%78S zA#&8031|~;v3OaBJu*&ny%IgIkdhl@H|=?Zq40LA;P42FV=#T|0jHF3l)(0HpoeR` zL(Y_VrHIdy;Vj#y%G#@GYbl25is zbJC_k`W5!8jlAJ3gG#$7xliB>o!(;R1O(E0nVPb~c4p33v3 zYS@6n;9!rv!A5?4ZIOOkA6dof*l(v5fs`mI-juY4HkY{a`BAVqum>r@eDAVXy`qwG zfmTd}Qs;W2-{I7V5#ymiD@j+lSikcS8X-KvOAPm_Kz?nvhu&(%W+v|S@*@0unL=V; zx&+Ydofp=~g(eas24)CV(a)9&QS?R%nJ)jNeBBj4_Sh;LxFKk@yC=SP3R_aC0)1%88{ z`2J6*?Ek@GKumfv&)cM5n-PYNomqGQl?lyPO=yYctNpUc1HTQU`5F+B?UGazHAr z4_05D6R<98@QhyOB`n-4D(Z+w9JBPXcVbrxYz6=!|xr>X*I@* zofu3`*|R=pjC$7KO&f;Go#aB0`8!-tVOH&HY}_1^$jRQGvmW^w;XpaDy}0uD7=I+s z`E|GV{&=fS$2i0$*VI)klk$})P7Piktlr=U*yaYZjV$NK?RKKee}f@AK|G%5hU*o*4IKYO2OTNhTb{PI79R6%U=W|nL=PGLPCP6Gp=zg7~=Op=m$f7 zWi9Ra{VAgk?`2AP?pR1=eN0)dYVL8ctluD&WMn6VGRvOTS;kV;Y|f;*89at3 z&!jfh($UV)73p6!U0+9T2QqBJy%cX0VDpP;Fy6 zSW_-vlr~xHMF_@<>&{XN)>ranYIa4ilA+XMly)f?7u#Z(TS~0+_<`onpNd>&g%PY9 zRVSHB$xDy`GW?yw>RgEew#y$nWRHJKVM%?0qao+kXe2kI5UwM3!0kVNSKrhi=s2iT zG=j|+*M1zd{&MAud{yFMmgF$0fHt67-#TPq6@eY{Z)RZsZHEEk{d&S0 z`H1W+6yL1(R(g{s;yHqq^@2W4WCYfoL!sGz+z6Q(q+YAEvDOuBPtfaY@c_;_rZ0T+ zjqC3)a{7ki5i>a=yVg=O9cPzfU2mjPg9c9I&u-1^DT(bRsK>eUqBANgweu(96mvdJ zBjSsO2=WI}SLwFD z15}zB$;UP6@OcK4uTkN`bWS37(=7&zAu2)V`!!M(c$)Zx&RqTH>4kNYS%tS}1 zez4f(4rckr>c+*{x{n>oCzjINNS_bq5sJ8rg4a+J-S1e_bUQ+u$M|ltq+{(SmK)nAK2In7kNdOD|sYCvIi|lv4 z9orfxm4XS?HYJcKSWM1&99kB%y$~F}7B3YGmMD|9XN9nC?gp;MfL6AJmq+(|B+twkT9? zGLjV74cJLMZKypEmJx>?`v-8?NZXjI$9JQ^0 z0lx~ogbq42kF!K`pN`zTZ~iSUHJnuG?mDGAtk1?4m1BjLzh5#p;paD5_Uu+&+i-nt41r%ZEjtl?U{ zWs7-Hv9HjiyLQm-C-$Ue5*49rcoKLX2^IuHIQ!#Jho+Xr~`JkBgnH8crRR#9bZSG}^~>g#h3X<%r(nM8f4Oe%~% z1#}!5NL>bQXU|X!PexT|K zCSAVtlxA#45~B_q-|Lw);r^0{`YRo-Kn>8^y&wFELTBd}PP27#I zHLubw#XJ}F7CHqvjCprj{Q&3t99y6<1Z05N=2zbiyW?`kV;#}ppWaKqb2H>Iz*NNO zsMN<{QU7$(5DJE_!U-jD_z3ygIN>8aZ4U>f1=*<`vS&MiF>n{@Z}~p_wny-r!@%ng z@%>*8-xqr(A+0JW^~AoOmhHUQ5}SYL1kCZ9OX5eD8!-JRzW<)D{?50)%~4K$wF39891wqd(wy&@BXACKQBA-g+P+I=KO zV$c3s0?|J8`la(Ym?s$JK9>#Org`Q^By-KY@>3_FVK^3@YpA>Mh6Cqr@Q%+>2x(t7 z7-th;W5*R$Ggh6`)IWb7t^V}1Ej?@bayTGgS2~dzF5YzcwNk}IBLlP`MsI+`+QLNa z6=%h&C?VO~z7X6QY1APw$V18ZRlNt%q>&QCFK~!bYDnOEtrv$1uA;2>j`MC{P;0YW zNM_xMEWHbJqMqUkglC9?&UmHSztv#%sn=Uq-r`l0Ig0+GLx*k%D?cwm@w|9rIKM8z z-(a|aLCH;jau(-VmZ}gj)rLpX;*~n{@}VjoA==xob*j9|k9DSylTQY@ADWYsyy81p zs1iQ6Z;T;1DJ)T-$%K&-j=x-KYKc{_r1ZKTZx~IjA#j)&FPgXAW%93~ap^=csNA`A z&Ob(IdT$1h&u{p~M^FOV0YB<*??1#hP=o~6Aul7MsY;^JF zsYDKqRmL@|U9~6b${krF4KPEJ6Ib^IU3ePodXsdr`O1`_V)BCrE}Wux?X@>s_YkQ+ z+EdrJ9uSW1NpdR9in}Y=omPG!j_4g+P=wA9At(jXH0h2f!)hNjk-FU->ae)C4;k!U zb6w!JmZn=4rXG$gvglJ|M)0EeJRX64GLR|9An7KX8nipTvN5Wd6jf|^0PsZu`DwojCEVl_c%Fz6aLJ{iZ z*f9-CipdcOr2RORB5zm$v)I(Y%KQUhlNI+bA`kfRMwKl>e zYVk~ac@VyE;Js?zQBpqqU-11#6xieb$hXhG#kYBj8W@^hT0J3aTM%Hy(>^Pi#2N2q znIu8cz!KDQA%q0A8c0-}E=IodE6cAGPZ*_&$mZvtBF<(6o8f zBeXLqz)TkS$}t& zIRnzZ^W8$xW!EzkFpTYg57(8R63ehuT`Z2Wr`KTCU@nnmwZ$q%B`qt3R)WS?nA%T7 zEvi7QkxW0J>n{j}*R{3Wo~QL@)Yy_L`n=~r6i)T|GY_|~I3>x_I}ga1n6z)<_KirB zK9tk5o5>y->MCu@9VT@q6j5A$?>WK{4`#1Nn2xWHI9tPnBqX!i1JK_9ekZzEo9W+`;6*q%O-Pn*swZcL^SP^{dFn zPpYi!vn|pTLy^)UFctWO`q@8RmQ6J7y8>FpkN8Y=o7x z6Lt^HfS;$7AedxKlB|_fRLKuh6U%H9n{y)dqJR)}g?w`oHx<_sh0k^FR{8{O0w&p7 z4(;wuEvLWxkm)MBbX1|e+@!x_cb7gKIt;T%5I`2c)8B7ofYTm7^6mSF_y%VClm0dg zwW7sL9AA1ofqe$XyO(GcPua=HDs6I}q5XhdBZwG1FBz?ij}~a_CWc%LD~u2L{5QUV zdHpZ=hLQ>xC-E!3cA$Q(Kbq7|Y+8;80YNiKk%5nnOwEMZqB(bEM%Yx9~C!i0hd zny{#>jG-*$F$AS+W)4Z`CE@cZJ4LOq=dP`Jp0jM*((1ICqW!ugn)F9YG9Z$E@26|& zE7ln4auYx=K6tAKzwcRUM=>(}!hTzbORE)-n3sHL@GSi1**Zk&lx*8%ldcVSPvR%R zHw#YQp)hx<0tKK}tb=#{rVlvK>RyVrm*kt?@&5PESCAOB;*>v!VE5GyjVcMyfl#X( zQ;M{V!<(7+@M*mj>@|K7?JwC)^hG5sTr|pr$GThtZ;55!DW*2o{A;s?CMPCFp3J&+ zPQ6QYfOB=isTA)eUZ@a%q{^XF^bH22?-Y{7fZ?P`b( z%oc%)?H3df6~Un82DQc1kmwvK4}38{>F>XH#DC}8Iw)lD%6ydD}g_$g2{c=?uBW}l<>jpB(i^VH~89iXAKy}-_!pW)TTvePi z0DecsJZY?9KJpzW1`?*^YecBT`D4HGM?BiOd>A86 zZdy{ayw*vo9yr_ShzyFS4T}Y~CS6I*_17&ehTxh)&!!*QJ;4Wd=@vmo?aVUYK&=dE z^zP8-j^r+GWOl&3@tfR`Z2>=EFaZRxOQo0!<;+e|UIv{ZSrAZdBYADt=trk*&z93Q zW#>glHkjPwD^K!i)FJlS8J-xyhh}fzCeB!`K9*dR<#XdZaPF=! zHwqxQ|MwNX1VlEnKr#N{Kk@%VeE-+@Qt<6tANay7@jALhv`ekf@~0_fRYMDEGawAK z!po)NvrX6}m6ppY_h{V0Sog?jGvDX}+`o-4%Yk(TTtMo6!8aaw3C(5XA^&WrU<-Qv zG}0NENdjGFfbEiQ8N`U$C9gl3-myl;J7rJQJh3WzQkcJU0_OP50{U?T3rzos@4uH9 ze&<`5jrn;@!P6X`-E}x{{O!V7V#^biNLGfxolC<_rD`&`y*&ppd)*ofbmt`&#%&cw zk&7hh$qexQ<%3BX?HVQL#V>r|c-z`_Od`Im+?>w>(_^M;2@9F=4g%@#25P5B#R(-w zMf%l4L+!*;Q1W3lOs4aCcG!H8;1HYy2QeJJJ{ZjH8cJLw9WW9`#L8Wn!7WzQGGS82 zgu_Ckgv;W@2?z`b?Yv>K9h`{YkUd({2J_f-ANSmT=UlX|)SyUsf+sJat>&{d8I7Hx z`4TVi&Btx2*{cuplk@e+)g(=&3d^Qtmsu_vPauw8+pp3&)6(^SfqDripz5>~)=$1f zN63n&B>`#FeJO2>xyojqf}Qr}!d2bYQX~Yd9I?Tq!yYq&38u^Hr2g^VX)^a5-}`lK zwg8VTO-ByZdTP&5J6+nA6tRFul6BMSAenHmrr!sZNaO=rI5miErWv7h-VanS%5?P! z!|k?a;bInf{01?d71uY(dhch45#lAzM<7Jt#o%7;PZdc1H7uWx+uEsW7Nms+xX+%FF980MC7)xE@M#x2oOSTKb$SzM&$XsmCCVV6w)Ev z*={TVGyPTqu(tu0@M8=5{`YSSLUCX{fe}r<^9_3mkN5?~v8P*^c4NplnZ}in+Mxet zdqu&`nPd4dYv^UC9SGCnX0EhtoKY#{=Cx@lvogneX~e5#T_!<+_8QLbwR-m z=2CM%gf*ZSM7XCNh5uHA!H{SpI^bZj9WAa3@%%w`T^dDC=_UVYC3bP;71%0+XmC6V z=wtIUeTTa24a^#RC-m z5-P)~kGWKzkiJB{yo;Z@1Or#{>etUE3vX81o@{{H`vu?s_WTa&B;QBCxBAX^b@U|3=z#H8So0^J zZSMDueN>07zhtMooDJ30illoJmrk@@1mfN{=DbrSlyszplFlE7>Lqy=yU!=oCyA!c zg$HFP`_}tlZ|&|y*m=l0EG=s<;LytMRk^JBT>=i4ib~t?`t+nnw(Z&1nj0W|SYa~l+g9&7N235+flo90~6k5=X^fMayv^GL1rB95P zjh)s4DqiFf-Fu%kcLhve84whF=A#bKS&@H11Daea#K52+cYOrg>zMTwj&b;fvPpKZ zsR#sOBv0pyH=$GUEkxXuWfH43b!9T4>N%ENaya zJfU(5!Z9O>(nz%R58TFHx@=Wi?ab&71{D%({3>UY#E)r^!O5&kVkeQQc71c`GY96c z8Q;DWzE;IvXO))*g8P5BPl!_GLjh0y$am1c#rM~o>DU$Q%)ZwE)oew$7iAOWXL(|_ z)#7VC5{*H*`|tJlziog2y`TI$-)tYI`153> z&U-0|R0>UO-C$dL{S2T9ArF;^=K1AO)@-iRh+;6}HV=r#$Koz^!ch{-QSjuz8N(_J z>nOlO_sckOWR6Yo75f#4jXZ_h=dzGHo|Mzmk(43%;SJ5x_-F{aVD&>Sv=d;2I}8)z z){cN)Jr(GY@1}J70`?>)6d_Th!aioJF}tbWR@y&Bd0v7T_X6=iriYq=rQM$axkxT7 zg{diq3z37YHx3)~&Y{B)d45J<7hSFl`pTO~*P<%$+1^t(8hM9n`1@z+2|6M|%veQ?!u@A^6=K{#D~q9Z$NzH zc_NAe&J^vRW5l13o#KL z6Cd=do}>!|*Y(IQ)|2M~!TlZIs@#C5e&jp&5AhA8;3xh4hrJuW(BFU9Lj!yV{ z_6bG}(6VHZ)ea& z(_A=<7;Iss=OI^fR+rM?3gy+%a5?id>c-P_c!}R~2%C(8U*gm{OUEP^DX!%_6`Hi4 z@SISa>dL`@MB}98pI%}3(!YkxxiP$oM|@=$+#Fm^ov zSK#+imM0D39GCv|RNcqU4gbqF(d9Es@ASbfb;N89;)g9TblY6F@(lDmSFjnXu2!@gf%rdwa~^oSb9A~KkF>W0fqcBe$)gd77*UD7xdcc&AHFT%3TDV%w9^PciHUJ zC5?aPGi33?S5Nu8&-ZZ%Useht>3pVGu_scyYo z_e_n3ZAGT`2TOeU95f;?ZkIFfwSmazxzYSZ?$=+n9Aw0Mn3b-VWxm9hm>}4l!-%^y z^Ytis5r2e~#<*QcH!*~q+Ntwd9d?8|k1C!jH|>;bGVYma#&BX(LkUCqW_PyZTTwIfg;Qr_m7Flx)1utAzIH5duksMp)Ad!)}S)qK!#_~1Kl zRhix>Uqv*= z3XGNeBj2Hai0}W3k@tt=PyB-KKfI;!zxe+9Z1V5=8$2uGYb+L4=R-R}QB!#;4=U9U-8bFXa#3tB8NDCVMx*SVV|&@D?a_>b5kBQ-zIGVdtfUqwSZoi?%Wp*q8BnY5d@Fr@8XaIyzsIdb zDK$$7HMByYe0B_RpOxE39ToC6EuLLJnIhWGZ+%TyroZ^wj$CT|y}wRX0x4o`U6jEk z9NX>t0z`ti1i-1=-5@=|n2;mznjb%?JHn8z?O(i3(b`YemxN6=Kl2S#&blQOzb$L5 zTyi;8durs(I_O>QW>D5dX!k^>@Bm}JS37;$%ZT{4qYWSJB8QkX*mE2n56<<&H9>CF51Fh{6{IK66%OF3{|z*#p;)Bt4NA)JPkl- zc!d_qYO}(3dQ@ILrkyv@p1a(OE8o}I6WO>tx`HT$kb%%3!8yvGZyKCl4(ptHYsaNT z*7bW_sB3*OK9(X8nmykTZ*1#guJJ0zgg!I$b3dNK3E}#J7i)naXW*1qtfWf!jN8l2 z2doJJRB3U#ALV}8>1n3}FYUm+g?!D7j*-s#T><$QcUwV(Eq)@wqIm9H8D%@MDIChW zt0z!x;Eu7oJYg2fKyZI!e~PA!rj}x>WsQX}$X>C41d3 z%vAjRgjw47{;q4fFIIb|444_*7k6>g#2owRoZ!BEVhx|TqYvD3+ z{7mD9Tl!%-`Glz27T4i^6gqFmIG^3#RFGe-SbH_hQhB1>$LQp;Wj~fZY{FXNzR7=T z%o-p~ZfvNy&c}%N%P@-#OIGH|Px|QTW3L}dV zkfNqp3w7H39`@OMMLzrp)V{DMqKr?@<(kGHS)vf0fXTixRXEuGLiSC{zvEko87Rgd^moL+#kWh=Nh0l4>0+Kq%X*oRaW6X* z!WPYhSIwGBgyjwurn+DYTOOM1W>VF7<;HtT9QAK>0jmeB<3GI9zpzh+4KrX)YYoC| zN#A!in2*bkgxZTGGUNy#m7E?c3>MI*Q8wwtjaWf<8oZh!mKb>ap@hFU{znPGLVw~L z>tIF{koKK#-Yy2u&Xg4qq_BhR3=rjoitfRp*S+RrW98uQaJvu&)Kk1z4r>m9Euc&;bWpkt9c;$pwV9Y{L5BK-W>=!N%qh)_{PTb(q5mj zuhv7hvH04;a_89zPzpGWC@~=Y??L&QpTm8q(#9gXT+g-DsZcu6@HG@PZdh%Eqq;88Uy;>_v+I5Txh zu+Rfc+u44Ij%)Aisj=-PiiU(IdM^j7#V-E=aY(e88B6(oQYm;XhJ_%6wPC_bcMA?8 zq~|QENQ-W5kYHnHiYK@|VWq7N4cukd5E>PSHbTa54XiOr-b5{Q$zcm9V~NJZaDZa`f$zva#P@%VFSCo} zY%t*N@h#;>NwK6)TFl7P=g`+Csr82%ewfcv-E z7vSm-;02`a7kuB_upt(S9Dgn;i{>3lg{;dW5SOOsVXTI@46#nOsuM9pM5K#7*8g{a899MYdCwDMjbLYa* zU)!aCEV7e~2U>En$8v4-NGQo#Ev$OHwq#u1Ur%G!vA&za2!R$qf}eeTJ{NYyJr?A& zTGmgP94uWjq(;}N?tOoB+p3O0kIvJ+1F`uI!I~enJ}i}XDc$}mxm^~kW*3PSgXCe( zhPcB6l`qN(f||ue4cfd6`r}(%slXFacygz#f(vMVj;L65$Y z4DbuBaiOvPGT8w4KAacsEgtaqY>Hn0ftb!x)bM z!eV~=LKV})L3(Tzf6@ecX)S9Asor&< zgaPP3_XGX{IMnLJIDR4_I*r0Wb4h`6uSQ1N$;-y;&T1~A(ui{5T&^9EZss?i!ft(} zdE`r2F#I2^8);o_PHls)^&@Yd*_&jn_q6li>2=OkQ#)Z%9Yhmf0ffW}bE; z9*`=M;`fHCJ;8bqV{lc@d^M)`o(O9Wdi&X%L=Wq)6kIXzBqzadJ~E^Fu9B5UoMhi3 zdZJ*f8#}ScFWro2wqc3wmqBc6b3uG{?y2@3hzQTUb;W*$wW3dIV4#R~YtF|f&ITp5 zt-@M{4YR6&2%(-{Hu(8K?WIYA(sVSU!3ZVJ;B%Lhk1zX{V%XDEe1gb3`*m^IdZGj# z@wJ25te#!gYmpzX;m#;W_UTQb`c25EF|ED;T_xoRq-HvEo+(qHV3bc)m&Nwi-3lr9 ze(OH`viFro#DI1494A|bWA)u!q(vb-sDPATK9wH{N!-~@6k}{y+p%xFhwr+*2{CVmE{3K_)UjQEIU&Nl+_OkE9ws^ zEO0=!z@P6S1OM=Hh+inIKRo3Pd@(McQgTBeH_=S`i9ZGdWN2AAR+ z8v5@klPU-EpY)iai`@y7R0QmnoW9qBv6SQ0Tbsj2;fkYHr$phvGk|p4a`I&E{H^BdY=NshW#1w{Drx;^H<6 zStQC#mXCE$Ik;5yXxWFRExJ9C?Uv1IsuSE<$0~W!`iWvU2XFi5E&?r)t_~YNShhyc z`LQ7+^R+;U*g#2x!;kpUyNKQ{I$O-xVlFD;TJ5Lo`Q%}{sf9MU2Vx!F?ZRlCwt+Q?PqU1`7+PO1VKtNAzb4|sPo#L)$(R>Wtji;QH z>s@(Md=tE>SW8To(F%j+0(bEWDlGpC_VobNOSB!=`5~t-b*p-`Y-z|m)6)AM(2~(R z&y1s@A*Lp8_r!VjpMw^n4mKLW=nR~k_k^4uKjGS?uda%REgK8zI*dI%&K^K4B)>^)+280*_ol$yBV4H>4V1feNn1sSO*Vdgl8uV|--d zLtpA*jiiILbDKpN!(%}Pki~EKCXHG9C*Lvu7T>1RI3OF8_1$M9D~4-p!-M^0z;=`daqNER9{gn~8Nu>`eR{UEl72;m(id;4k=&1m|*BTcM@mAM73M z6U&r2S(=O7PP9Yd!8%mcm9%%HXyyN;wo`*#{Mjm2JTT)wX9WPV2VB6G_|G95FzF}0 z@&B`Y_B-E$jTvx)mOZ5c@$@F5<*giV^gYV#WWtxU(y%{7&D16ucjBM%yf!oqP(Yy> zt%HS)$`)dJQBF*sPsH^henZ0J{m?m)6H}}s$99`54$4Go;WXjqo2Gi#m#BJ(UqlK+ zY|$|3%*5E3LT?9cJJirI78*ZP#|M)qF02&47!67|Kcbpfh?sFI6iYXXgr8p-M>FjY z^|i#S6?J#SNiD_%`9Q|z*J_X^&o3!+IJU}85>F=`M=wEyz_PLxfa~g;+2fGw8)ohc zmFH~Zf!Shp!jdtJ$f>l!-YIm+wjhJoNO%Q=sAQRxTeI|kr8cAhg0 zfnX!TT2t~pS*FKQ)xUz$ZiL07d~a}SOXA=F*)?t5HM|{YKjTc|+H|b%zu``B4~fVc zj~=hbWZx|PJc`O8&noo`{%8o3w<_K!S^3-LBJRD6W_Q8AAs(|&cD)sK7AadgtibAju#FC4j$&_ufxZC-|spD&K|#O z2?Pb4Jr0TE*FVEV1_tBqhzW-d2L%U6LLq~Lh5OGFD5PlSaHeo1B;GC3{=|J+{&x%Z zpQ^V8-SMPGd^>oIF8lc5!Y_IV@PwN_nBNN-9{H~e#M=6^9;+%*e(ia_UstfQ?fR61 zZOG)p>rNV>|62XkXCB;m^IQEMU6Otc#dTt;^1Wrp@^b{hSMsx2zaf-y*4PZ)QAx>S zi8If2uRHMTD8+JCPiN_Js*#svoF1slDMsOqXvA0dweV_kLQ=$8)@N^iNl0)SkD+_6 z=C430B>^`kK_#v<4fQd@iLtO!a5Tcq+pIj~qI7j2DVdQmh*P<~01p@=`{$Lr- zStFbHVzva>lDkKH1d8_|nx0;AM+(#cTM@z7v%$bp1UtF>4n>P#_az?`OyrMZai6VS zn8dW~xVS9yEsosUk#)Yj3j=9*Wx9qNf{^GZoXBq@zna~i3nr-zJuYDDyb9G>NOX{N z-mL&3DQO4-o>fWYP|QR4R1+1(Tt*>a)i8kFe^C30*+VOH-m>x1TUXF)o85k}&H?Fi z_!hFpYptU}+KlZ@IonMFgPbNGs~ua+x54Rr_Z%JuORO&^j$dcE8Y{{Z`{Q0=RUJr< zdlpsK!BBi<{Gi8K)tR9p;N4#^7_ zj5#ktT)_F$ZnN|9I5y}0V9~3xYp9IB=m%B~P(9#3fPSYEcn4g-*7A#X$u0cGl7NR5 z#@9h8TL)p}Yc2ZvyI`>m9Vw%+9gHy)Q0F6cyF@X|@?x@g+5vDJn}6r@EysVB@IC$8 zLJ>lDPrwl>|3hRYL^MK4bv6BGNZPa$%n#y7MSd!fnNb^wv(N>mb;T41CZV>i2ldj# zo3kMaPIc~rkDEf4;u8vQ$dL+OLjofeVRQjH2% zlY|~q2cBp`O^qU)LyXgHN7CNG6fP9RNh0Wc?v1l9EuggkY5hMtIs%Y66rSBtAao|5=yM)m#do{j_aNV5IYjO|8tReIMrDuO3Hv4M$foQ~O z{CupI$*b2>djoWVM zZXSi?dMq*K!`(Ddi#>DfZy?MaQ+O)-)n_)_@B~xBZJ-_Z_OeLAs*Ha!yIhGpYR6GS z@rG-AQ3xy`&~T(eeogURn)aoP){`eV$D0K6BhUvE;P^KD;EOK~xY*vnUP3%GbGJ}m zsWq5sJ~;TwIrX8BDm(upq}&MN!qqhn*+Z?}JY?nV667hdXVF9!K$&g4`<3!v7ki6ZXo{%yU>qW^{ zfOAl@wurClG?xaukM}aBnS1|r0M0vV>}9GNabLja|LVqp83C`~8_JrOD=yDzv|le+ z#$kFOK7?s4fn{KH=Da9-QT?OplN6bBai zlOgfD-4FPv`|9f}0 zuU&Zj9NITlnH_MW@gHmYjiF$n?pAhG^Nt|ggP{@kPHpvbwYLMqYmyy8v}hYHY^l%d z-Vj+a)RUeNq$8pl2dHqWJMG+@=7L&u=*>^wKYrm;sh}H#j%uYgzo_EFMFu^s=3wni z7bRH%wU!*$OtU@->BSV5K1X?Qg3BSS@->>)t&W~a+I_?sNMvw3P%d5S$0fv{VYa?o z@tHT1X1O8cd5ST)bFP;R5fK(kX|Z%eK=9h5fFBzmpWpCJ=3Dj8$Q%iOh;JYTKk@yC z*PZ>ML;k~S=l+Xt903{NI@foAA5f5g7|N}*O`veHaJT?N`T8$Ix&7hF4Gjhc1O)`A zqS_k=1P$u1-=Y6Mi;_mM5P~4%oQ{3H(w79#w zyA^jSP~6>%yHlh%6!`JkXLmpCIsM+=F>cRz)*q~nK$EOBU+bNjWF~>gjmO$&v)-GA zCLt0vjeDKmtyywd#-ee2V$h4D!dR6avvt&^7Y62+&(^D!n0tPMDLI~vRyl1(W^r0$ z_WUefat8S&{;-?+5TU#aqiX`IyYEH``M($skIlcWzXG0-j$F#IYA_TIhncC7NtxKn zXVo+&UeI*qc-^ifF%|wMq>M-R*ne-uE}Z`JnYF8o9o@Ujxe>u4Z*Is=n;l{nbNuE# z=OC5Sw!kNcg416bGH*773JSq(o)I(x?zg9A}OSA7UIq!x(-d;pYuzZI~gX_`o;;$wn_6Sm8!pnJv(d zPa%;DlYnm97vbmsX2y_nVSi|CBj|9A>da4zq(4J`ZX#&xoM0M_Zuw%S;Bd~i`r@-n zUS?+;I>wPW-rLRgiMb&J0o*aHV70Vw#{wx~JY3J)?q;qda`ZVcjbKn+=xsY1j1uPp z4-b7adXcWP>`!KpZ$z53?1$=-oZGF&(-cD(S!d-+(ls>!k134v#;JLuiv4eWs54m#d~cs5 zo$xJ-?T#A-UU-5{-4^6!RW=*#On=7eJ6V1)Uo(~Z0QIrA%O2;w<&Qf)|0VsuZ-_QI z6;ymF7iTzwLt#c>T4=E%%%_;jHI)Ni_L#Kia6Ke5ua^vMcib0$?L*hye1rDa2M2ik zzpsDu%>k)DH$;DJSn>bxoL%Hi$S@7xPYs81YUc$DkTnQmW=MWx2y)A_bJP_=jRz0L zE3A4iznXS_L7lTx*q?c-!+PQ#GN2s13%b+ z5a+wE>}3uldd_~cMpn{F2N~+FBst;RI116R%LZbfw#ZePWgdD(B+AeRnGkvsdvWeQ zP_s9f$aT)j_8>9NNulwO(bE+aHl_Jg6*Vw2-vJAYrgpsq@h&_TJl2TWUo^UsSJdzr^0Xyx2!r4(lCIOUA&Fn^lv&pX)pl+s z=&O4t_f<$ft|(?nS6TcG!5YCa2^PC{eC|Ft|3mlrc=;~2xC31I1;VM82*d|_xK>3d zVVxz2rNypfmWH6k1kG@WrE+I#t{UZvv)2(SqCTX%gpWO$Pi^ea#`-^d&guoBTm({T z>gQtHNqoK3>uN5Z=KRWbrJu9vd3}V?IT1E%ql{NocN*#Ua%mXNaUvRoJ>u7yG?bLj zUSHQwg~@$;Sem-%4>D@?aX)uLqgcEEvHa|&K>NYI2>v}ALeJ(T7(en`>yY4~+gIMb z@Nr9&ZR}SI^BZ=>x7%5aO4aJn-|mX|V@fFfT;YPeFj_NK#_6FmGMNvJr)}?zdUqKd z**eXzdcKVkr)wE8eL-|eh-8D{-33vwad1l-DaGBeD4ly4VKeX*Oe&shX!&@4M)3tJ z;!PKrc8S2}O5ChYHe-!0KFP2}BCxl_UDp_)4vCPI zjOS$2Huzr1;NiaaOHl1)m)9!eWT(e@;Aal=htysU6q*;n+BOckUbYCw5qGl>@O(@l<27fL)L)J7*`bre zz7cNaCzC-hiO?^IElb>P-w&l@!N13T70_NhvfNgpJ1#suYjy4{QW9GIm{+*p)A6CU zZDVubJa$kW>$?4wZl@2PGr}ke*Kk8bUaynT^vt|0*2F7f&u9iT&Dru3nwLdl&<)UX zi&FCZV7V4xD5V`GL!VwB;zj$YzpcYPbgkY(c+~F6G}Tc8+L!+3@NG9{aIaFvZf?g+ z2-~aD2b>}}zO2&0k}Xmq27v!c1 z1i}vXrqL18B^50vsxvC{#7-5Y;M)jFr5G!jmYQ#zn+)kz4~8d5si*UcL(2py5Qu;Y zy(aRVZzAZcotQXjsILuWwGD|+hXbKd^f4Jq+ChSHozbBD!VCoR;&`=!8To-_n9TAz z3+}t(QHbQ7yvWx zHU0K+Es?mxtXpb*J#v?M==xE$Yf!NhYoafo5OPeB`=A~Fx_sHaPX`2b$_5Q(Mb(Ku z9BC?StrJXF6@)PMq00tt6LDj4NIpnKGeul8Yj>kEdeYjePWkKw7xkXly3TRI)SER9 zj9l_!w}QNZ!(PSw{G5bq8zh>UNpbsXbn5croXNNYxWijUo!bJZPqQ-Lw3=gxNImnY zyLHM7=TB^9m~Z$Vr#jiHG9ae3ndKjcPv5!r4{WvPyW*v$69BiaXahGvFd!euv&H)E z9fdyI@Hc`*suBOM({BHNry9inA;J2OuFxVxWJ0pnWg{C<0K?fU1uMFgcnP??y+;uD zn?z)AxoJe29if64R~G$Mp?YqQDbeq8@qGX8VU<1q8*l;qo?i&oD=YBG}RW%v`TU=y?c#aMpwiti@NG|pkB?Bz}*)m6h_VC zp)3X!n_`tcOywcL5p{I7EeVoEX1|b$@(GLI)|nJ}#vOVIx=wha?iR#qirK@S74+h~ zY(@7o$Z4!<^kWMenE;C#0mWge3oZlQli9t2Nnk803_@HUm5vIXKr-`v#SAiao0BZ# zp8VQizz#Ua0p%eG!pod?2?#aa^%?rc24C3%Psz)#Rw8LvcKUW;YYgcRsoe^ERuLdq zok*VVCle7GT{JL!S@yYJZ(Ji2NpfIHqZBTIjFs1^9DNCB>>|*)@DqS|$M)!_YTMNo z!savW6JQelR_V186feA9kaqbM6Kt6wnv~-6URbN+yOAh9c=yR~=mZu>Ox<*u9kP!( zsMQozU`xH%P4Rf9Lv->KSD^b?ujxAXZ zmVAq;9@R{y$o1@QQp8vm+XLRapW@0e*U2Jh6-ir5>#(oeq3 z+@n#@*KL*4OmH}#1+y`6hJ$fL(p=$q;=Dczr^}?;IMLL$WDUDX^v7M-0_^Y`zR4jB zBLHUnf$zjW#5aJ9pZNa6Ge5uJ+m}9?<8`MO$_B5Af!EY3ZDx-M@=6JkB8yAs+5`e%pKl`jFdW*@^)NMFwp@mN3*s8UT^VeR z=(;MH#IRAGri<*FAMy2|))#nfBvW_o@*9ub=;FYcck_Wj%dg8HKlERP`gz-EO-po+ ze=C^f=d~e!(Myl6Cp#l^)ID!~fVCZPRM=dfuj^;vsw!^FUDT-T*tsnOouh9BHyK>W zRH)&w@DNdU714iu=m1RcCW4#|rhtPzc1i==IZ!)5qgu3M1s0i?remC`f}?gXy1n$U z&Nt4HKWT05P49*JrElVK7+r@}WCYPJ1_mRKO_r$_6pTOXxDy9nVG&P$%ftLP%onBK zs6I$bjH!vDb20d5Xppbr1rt0eNl2?`y(|cM2=~X%=E-ZZL2lg3Li8X-o%_-hwrOQC z9zt^aO6=@-K56>s*y0;koV~x&;hlW7OqLKUcS{q+(&W-wYVf`&zr%(z(x^SE%|}r^ zD;>g2dDpi}h1T;L-M#cQxzi9g6O#2(IY&T%V`xer2thGLPOnp?{K`-WGwa0Zg1%W zRq+$-(=yyEMnU(Jhw?NlqDG*tFdesji?pO5?gay-Ompk3deCh^@d$q1XnQy~B3x?o zkrOtK-Nf(SFxeJGie|Tj_=Wo$EV+|IN(2g|>5OosZ=VApPqK%-yhT2Wtu}>@WPT_e z4P1YExt(s8SJ*3*x7x@7FPn+p<_a%xEMf^U8C8B`O6@PkhaCMwD;q!l^QH zEj`i1ZxA~wUu%wCuN+EVQu=@`dFNxI>YlC7Nj@<&p&mm&DID z{Ec8y>~d2)|J9FzmGp-M3y|$kg7t?7+><*!2Jk# z**VkY5UD|u;Z&s#1~(RX-ni|sca*z$T1Qo!Y1GNwjh1Zl8wG<;J3is~nKYTHXtIh@ zbMHMbvz3%%D(w@v*~D^EHJx{48->&m^x8a|3sCB9|K@<>cGraZYI)(6yKRn@=JM;# z$?($d`=^U8u5OO(N-G!UF3p0&8mbFbaY)V3R%lq3bW_NMP;wBsi+ZW4^9swjs<0`D znXk_Wvm8e}4XGZf-t9q|$|BO2M>|B0}8lXDjNC7!`u zoe+>v<#5n25N({-AT}`C;S``G#5?hh;*K9UDyDmranhqy?fE?YHNlCuLfbR@6dUNYmiVS`LVT(k&{ ztW`!VVUm*>+tocfH-qSt%{QM@s8GUn0aM}!k7#5HlgK`%7o+U5!z7u%2oDFn`!*e( z2Arj=4szSniiOms;(_uCQgMJMf3Iy*G$g(RmX5T_Ah2$RguEfcfDU7MK zKEZ?vdr!h=zJ5j|!@c^Ly>|V8($Dyw4zKvloz>aMOsL+jBDO8p32vfti<_a&s9#gt z%78xtmn;GtFcMUm$GL$xu)iG%mRg~8kK1~g0w?isEsg}k;nuaOCBgQmiFt~8sx(isZutOry#PBQD7!{gsbiN%#P_puMk0MDp`t+RL@nu~CXk-p5 zRP0AUy}tWDfGPqe!++C5zM*NYMpJQI^x~+#7OgK8k^u7w>*PW@qjcT3by?Db)At0rKJjlgg zR7RhgKYwqVPWV-rDgMgPyUZ)Wx@6s5t#)1!*Q!1l8`SM>JBpY>inw0LCsQ9=(3E!TFUaRt_lQndV zWzhF@TQKA%5sLOH(tP!GPw9s5%gFn9zt$a3+9OZW2@uU?2 zL#yno&-VE(-~5dLGycGL${*qzK*3K9GDhC#YsejP^Q;o!f&v;@x=L}lVJ8(|RfmZx zr7i_5jn}aZ{IY4}&<Ob2%K2;KDsFhz79fz`uV%KbG*lq<>DiB~o*N zd`|p_Z`rdD)_XxNlPX+1J%x4EFr2TC{wKU`F*b7}e`%*E8$Ofc#s_70muX_DcpfDP^Mh~5ygO?H_GVu&A9DVff zGkGqeld$=8dKm>|v)B-e6hIsvocU@Bd%qKZLQrS{4rS2X)+k$ z#5s$LXm(;?_*-o1lPNpz>A@%vJQXFwx7;(nL^r;A^eWJwv zUEU(Ac8Qp?hz9M!KmjsNJf}en?lSY*oWgY)($qW9N<0Ftj9`y-%tHoBCuPr8eBQ6B z!=@%8a|;>ph4n;zCkrrf&i+n(Jvj%%_j12pX8MyNQ5uE#1I+jX->H9y@BfI3{=@3} z3%>u`J!FFSOn@);AHD;otT>J_8)89&`RaqMT!$7)@w`U|Busb`N!g4n)EfO*?p2~T zN<9`ISYTJZaw%=gxXzH#IdYC3)DLF}Ws3GLz?XprZwnjpskocB3Fj}_2{@l17&8Jd z_~P8ox^Shz2rKS4#xKAa8`7yI34u$&(?9GorTf~ONAwnGB(u6otamytln&05n0?E! zqFMr$5u@2f;aij5stuz4O0q=mgIn9uY%JRu z7;9;Ji1R+C$uE+ABIKw?!svwWNJ0e7IkeNx zmW$-I4H>6N%(BH;tm-rC5#7>&1lQbD>X2kA?r$(ur=|gHp!hreHj4T_^8LR{55Hwb?ujvmBA`g+?1;?w1RUzU-%s7EZe179)`men!$7lk%K)1FAyW` zusGCT$5{@NAVX29`91I^4X$BOk&U-8e27SGQk({}EyCOoK5s;%9WG!S6@~byAhu6d zOl*Keux_k@K%GN+x4%ARHmysh3tjMu`FTE&nGoZvQ_^BFcYdsTMD1st_j)F5SDcaD z`E>X>{MfPGU9WIQZUrB$ppsr{>QEHwxNVdsMK9Jcq|1t6v~z2t#9!lc)%4DCV=?35Aw&H_HA|M>l8!PA3mm*j zt7cviwDgtO*JxF<-PF?;HYgZpv*2?6L!YwT5oXKbt$7v)gUPUPL3^e?Pq?w2Em_BX znerl=hMA9MlD`pT3%7I9jVZ-Mi@A)TV>XtIJwYmp!J268eAo)_0AGtOW+K5;-r^d) zyv;2YF+*zRm>@+V9aPH?l-ZiuFL+-WhM1KZ=_pL>6PpVISsn*Fv?f{HqoFo(;$C;+ z{-VQ&^o_F>j=gDlx|q`PXg!$P@RcK?T=in5G;DwCKFyI?9kwmR7pn$c7vdy@(d+Qa znf8ixX~@`1SyHEU)-2-^0Rx^{Afxlka?I{2{&p6#T?@_(%W>YWkDlZH-dl%F2Wd z$b0MSeKQ;8$K~3O*}hGJQXS;Lay%3ar$NUW;%{aE>!ZH&{TETsd{+P->G=Yr|3bf? zvUeN$m;+=td&K(AzQ$@XZG-y|9VUCa*!iv#N3Bxt^?u{VT&Emu%Eiy#6!FskJ10Pn z-}{$8wh(~yziRM(b;#fI)&KBq?4GzsAk{rP(7ZMo+=VL?L+)^06*3pUCfkmg-C+wh zbbr5!LGI5g7EKK8w$ClrudVHu})=L~gI+r9e*JU3^?*nf&p?o*m^m(&-m zBW69?r*IMwA7mrP(BXLdz%EIS@eJoBac}i;F}kg7*J%>%?!YOp=z}&!;)uF?d5eLU zI3eT*q0~(nm2_D2rkSggNSiBRk`p)KFwh(hrWVgfM^`|l582K6i$`0dUqZ)GWl{`l z8anEl9_bp6hdZAv>iHb)efnY!r|5;exsZ?4f^KF-`_Q-kd>l|;BT>yJZH3`>nLBcK zOMj=2B<*bK)kv1|k(7QD#uLwR_&bh^Co>S0D#D(lwMNgqlR-AS-75{Hj54N|CK}w9 z`W{nmnL%xF4=yV+K$8#5Tx!C}15XTuU~-U{s;XkYi{=e`e;T%xu> z4}l*f$arWrC|W#b;5G64Bv{_zbcy_RBUQc#`+WzI8RD4sn3q!Zr#yh&{^xCssA8m# z0cQMx@612MH-PP*`2NFdVt>K+A6~8icricmjUyp#{hajc8LIbDv=G-arJmx!?~j<> zb)?SQ{gVjz_EkJC!rF*Dzp?3KdUiF;n5*te&yKU7m1m6E6)Mm}Vqd<|ET+WghZ(Y5 z$pB)+^wULnRF3m7<@^9G94WOiY~6F4lH4a29;V-J>VlRr3qA+LGr2dYmjTO$#8aYV z%k3xN_`kYT0GD}7lpI`!V-OoxfdRWDLvXzymgEz%`x-Q=!Hz+Nh z=DpvxDxP@ki?^vFkamJ>{y=bDi^5#_w-L2WwNGlafyed?!%f|CGQRUu=BUjT zg-$FcU`q48%Y_HAWeb?clp2^mSCWPO)D;)>-7c}5`TRf5#kSp-rYi+I&~i-D2@6zhvUdE@v;8^(yINaOgMO4v?H zY&+WB9Km4@0+SpxyKX&(Id&XNOl?OXV@f)E?yf+bl<+5ns>-^}^u%M@5PhBx!Nq$e z)beDk20>WOIqxNTKUdT3nqJqdPeDdz>?sIapl_ioZ__q7C#D1?_EOkJhPqRBDjYPo z@cL>Rp93q0LXQF)`RbZHG$}5qoX5bXe8Ik=mX3xBBhchV8lx~?UVQyR)J@m4Jk>0M5U@^wahTca??le?v2YpCO^b`)VcMfRHB1lTZi8`@3k6$^CzE(u{QV% zprpQKqtBu@SM9kA7c)HDqc%kGxuBO}rg~;yB3HWH1;Z@%_d>>dhrNTJ ztM!*$Jl_Gr2k21!IN|*Z!TN8{Q2o6r&OZ%u!zG)q2kMWLNBCUi-5`LhiMTngd?<+$RJQyJt;A89X zBnk!o?o+I&JcJ44dt=p4aN`+js}kiBF5lc|cd`m_wWV)VQ={iGsc$JuP&cOHT!b!O z1-baX$4z37z{m08y?JMYAqWoHW@9|DKrU72trz3Jd3bcbO9a#Izl%8MMcNumwN1VL zZE?Sm8)bQ3(_Ku6<4g&ALT(1u0$G5?tJbbsP343)NZlqv-7>Ag9-YAEWe?ST9b(tJ zkf!Swo6BDt_SsR*)V;0jK4%ikE=r&V_=&|yf8OgPD)qOoI`jq-GSQdCpKPDvI&HYIP(+#)K;Puw6F}Lh9 z6u;%$e+|IS4-Inme~WL-m@EIlBqppXx#>0#ueB8N?pW#vyVlYr;Blv#12034j?7Mo z39#d(x{kz&5d^a5YW+Lke>um0r~iWQKfK@>@M8X|!FPv~XpW2MIqe_5V`jbFU^3ej zyIOQ{%5)YNm+nV*nLF~@^eknOE=5+82O1%=Qs=W3w%BJ`q;y?12?J)D#yMN>;35m9 z!SMMlKI*Z;Zc*?D#IWl_t=Jcc+nL1|w)B~$j84f{qvw)+81oidd(k@YLtKKU7#>1W zKE$`q2r=5RODUpn212xsQ90=Rpd<3>P!!f9|2ykk~j{;G&#>p{4m zE5&T!YdH@a_>*2JY)b=^tfM(E_ym$5<{etRt@z8tM(n~f@q<+}r`lp;-6y0J_8Yd& zql&wX%M-1Idr|A1k2Qt_@ZMtMH5H^iDK@GNs$K^`Dl?8wrc6fhyTfkIp$L(X1@6>u zg=f;DFpYz<&Vsyi^MsKVH}bmn)TfrqUV*K7MTBw;f;6p2bhE_Dkk5Gy<%soxtL7i& zc)?{c$zr9P9cV&kB|Y2Xw|ocp1I+jX-#Py+zO&m_sj=Jws@~hgrK%g42ry;hEuih{ zM$08i_cN_xSg$_}Jv|?^o8NlBK7WQC}(;E5A%EC~2X)p=^m2%DjAK?YGxj3Ccj>CA^JRs7e(tm9Ff4$ktd_ z*;l&|E4O(uGRyJ$t?);5Po1>6& zIU()DQKV`)tir`aZ4j91;l}2aUturdktJ?~->V>><0T!$vdgnhEjk9Dx6@eg>(fz~ zxF3nH1&OQ;j16W-CFomNpZjA}p4_+yHRrs>3%#K3{s4T_inz{8HLJ~TUP6%2vBN?? zTfql*(PHYO0SU@@ms{Ct_$dNPs#YjbRI(UcshPKIqw?hIY^4C+r#*H7%)zdxVylD? zeD#l0-`;_vFAuVUrX+a9S0tX2?{U_AuC0Xl z2ol#6dsAGBD{ga}2fvmyOpB7wLn^C@ZsZVWzBDQ^@s0V@OCv`+r~>U95K(4C1p}#^ zvzDF--n0)4z?M{yt{o-qOmXY(&(`2 z4(PMI56{TtxY1r zd~?E``Ko7BpTe4Fa^J%;|GMhzFg<+}vD1#l*9@J595H}x&jg_@*>_)vDi4Zu_7uwO zvnTy&;TZj{nM{O7oh8;Ida5YJZQ$t_OU5&H|ep+Wxu{CD~^N>(J~%bipg?&yUw z(|jk`^jm|SA)RVIj|J6U=b|=3r^^R*iM32o)TqH1>FzUyS1x6UoW3|R+oQU%%ZziQ zBmT8|R*E-Djk|^>jWxIYOa6jj0{b%4AF$55kKF^O+E|JW-*F|Fv^%OL zCmF%6lI9xlNbP&sTPP%5GwJDu2Y4Bc@eDOo;4!!B3S;+dkH@xwCT84-0@qe~GwfhQlvHU((AL``2`9PD9}PQW zfJ;A1x)(Ep_v4WI`YG=n1m+`6XD67^ibxz+3_j7PtKX2qnmcEO^7pdZ0mrC+39PYe zU+n7pNN{?=QzPO`jl1)LUoz9hzM6d2wFp*q&IPMxOR(L6dE>3Thqor*8ckDN#tUho zmv4#F#z1)A*HF5JjTdEh3isg}>KT=MKE&Mk3Kd7)Ly?9HjP6+K`H7Sc2P06TBj!K? ztw50@2}njKdpgO>z*avlqKK0!LOrLi7fCVe=q^Oq#F3cNN4yXC$#{Ja9((!O7QfMN z8dD&^9tA(@cm5yZ`# z6rircS4Y_@Vr~g+QWVW&UadXv3ovGTz(P{a&XTzFDzP)^z1UDhRTeU5G+gq-y@XV= z5@e97wTp^;_4_t3iKPK)xD*jGgF;2DwUoMu`SFw2&Pl|h183@Azm-*5JAqk?u-g$Vry0sgmSjnnc$2~v{tGXi~bP|>cido7g`iB3oO2v%;9dC zpui}Qvywe(!6$P{#BNcq$zSz0(2jf*+2B7!3$1An?sI*%_i&Tx~-(Hi>z=D!c*nejkXC zI=p|VY)Kq&iox&rPB$$EnDGbyyWkJ;4aoK<{nm4CyddvY==)&vq1R4+*uc<$d&-r? z=;O_}++@7*rjnT8t~3t4I=4kY42&X%Ctwx!cfJ960r>vQm-;*X7y8|n2!tpQl1O~$ z=kz8mv=B(5Y~Dd<1s<+zhAGv7$Y&_V8%DVbDgjvSx_P595gq&QoB)~s#q7V807(Cd zZwhh(!0`A#e7{y4hlD@*LM2aMGi*3WK@4&`#uD3rHHvzFFmw(LiBH20BAcz;4JkoC$MkT&rRxkL_N|u#kj`)s@jRsNh#NFL(b&eNez?Fqrn8iS~FXSs>WDjJp9jK6<&k z=@>O23k@_m>)gp1@5}NnwRvB3H)~_d^f9U8nA&HSs_AW$t5!gO1hSpmSK~CyJ(la! zugJ8j9EC40hZy&ydw}?pQ07&RZ_Ckw?KG2IU?pT?o5>bjj^)E%Lr-h;lLTtMB&}q{ z!Qu{sOWPpFC@R7FXpbG5F8g9Cj9sb_vqi8Aj}WQGZ&&bcACx;qjIFS!oMIqzyEl7; zpEgl(M%%1EcG3P3^ClVo=;jLo!pzB=7jl(Ow6d>UpKbA5zOBCp%zdA*EBr%z1E#=! z;`zP=5tkomQDd;(C_O1q<_ZT+LdNB1%-+)= z4tQ3Nn(jIg0ce~eWJ3hG#_$d%U>_~ZvG5OQ9_W@>26 zwf+vIo4@+7>8T}TJ-PN#WF(+k*6+(eovTJ}R5e@fu7>&Nj48A7jZq|XW#@d5ma52>gEtU;wV6#lhs)b0~- zH*9*C`MpYLoJyPW@gt%nP;UxCqxj%@9@Pp4BprJ)5j$sr6u;WdaMw_?J+isZ2*a48Bt9#~ z0Ifrw41y#e2!imu@Sm_?Ak$>g7ITbghgme}&;=f2#9!5?XzU!8F`MNpoMV2r#c%kg zE0qECLVwinqW>1(w%Jqab)^Ogq6>NF)5y3R#*eS&44gftP_|)*GI`vwQ!pts^6%+c zRve9$P9TauJy+|yegnQ0fPVkz9sh!F(B%k=(H(xCc-0lk%v#q)0a{;(H~rB>4FOGei`a0Fqw!1 zOGeefSwJsg^k&$|FsM0`-6+)~q+s8&Anw?q6;T3x>LRHn6-Vwc2tpiME*VuPHtP!B z`%o(3VqkyQ8T7x{REVxr*%n~NAN-RKe~9n@2$=iBy}w`Z{f9$3{)2B)Y&NOq#DDl! z9U5VrU=`_IiM1+9a5D3$V4F%;Q418g_r-pr!*173Jv5={SL#Mup;RiE6u8{EVqt-h@< zQFB2vy=`d*AY<8|dpy?DTFu?b@9Y)rt&Y}Zw8Ry$?6DYWAYJkM+2qshnWofrvTk+h zqE*7!(o%qK{mYYBOfm2_h?C+N$v8gVYXrXZkNgjJ3Qk9-&ZA-(|={N$hfVej}CeDh8U_GbBa zllN{N?!1H$z_X?QNF4MI_vJ*bCVwCI3$h(7S80~($3AIDV31ZNgT3b(e%Eh6P5<4) z2PFNZ-+!-1{D*H#XCqlb|CCDhC1xP6Pe{F4<@XjR#NI(3hOxKB_OdJCFt&6gf(_2% zt_w#4?)_;s$VwtHG{#8-u?B*?Iktrki4p;N{4ojYMdQ+eqelT20#A~@PNg=+tCxkG zAPc5vH}EsYol%u!Y3Z8&$5%HPFJoewM#0F1l$`HiOUDEK=g%R1ctZ)l*#|C@^DK#)?ky(=J^3mwJ%M7bTXu4j}C%_}53Ppb7&7pNn|z z^njs-V12G;Nzs6tsaJ2{pfk)IgLUhx29}YPd`&41;0^AvdXJPZ|(Ik#mzC>MLpzf8z`ZlAcBtV^7$6vh;W1|X|BkNWK2n?GiTvT%q0yTt-@*3wm zUC;OLqj&%R?qA|O0G5gU!?%h7!zEVWJAB7c^W}^kc6!QH%IS|QzS7YC=eJDK z7+Zl*^8|Y&53wvvE}&%>9QHh)`AN*NiXJFP!Lf!5!*`YG`<86=T^o{@(<4agx0%HI z{9~iVflsFsNOo1Q+^ECVgU!GrPuwF{sVW|ts5N#8Eto81! zJqVWeRt&FhyIU?LPRuwFgL~hHQfEUj9j5Z*nOy*}!ohvX!zFfYx>iat>7#BI!6IJ1 ztb+21t$q1%ca#J(K6;i((_|`nyV-M|)5~qY+K9pTTQ{l*5S5@gW&0wZzR91`xT0AIS0LA#s}5W-F#8!Hc&My}fAq~h zfPRfvfvH|#Pae%MP`@+Lxz#Wfl2p{a&aEyZyg$_NvPjR|cZ)Zv3IOi^95BZakLL0G zS3mMy`rqQaV!s8e09Gw`Jclx%H~ov-G{N1(OE3dYQ}$rI74z!@$Lv_uHX3vJQ3gmr2j&{^E_I>sePXp%sz%2niRZ(q8f}H2UAa^t@_vllN$K? zjY9k#FCyiYFbvp(3g^Rh)ZaM)a{PJ`1_0;`}{&}|L0I+EqD2F~Ip2*f36n} z=lc%l!#3oQ%Jdgf{sKk{J>jfgMcFYcM%-qiup*d!K9Nqsz09i%3VB+ArjvRBZtt(I z(?M-X+z}bk4wP>V1CO|6haNtvWl{3t zHhz8Q8E_kjdbq8Gx&Je~2>V8|Jm3zZ_#-KN>v@|1xTd-_0~H=O@>_})AxZpP zceJnRFAd8Hpe=^b9q6Bp#UR4%q6VLGVd#xFo`EO~ge=IJwm;d!pvwIjg^$41Hr$l^ zu`b(_ks=69f~V57{CljZxfj6H&wDJ<;I1f_^VycA`9=x6M0e)ZwakuVSzRYfwmFk0 zxtBHAt8rplezbe7aK{I+vI%^ml_33=CWsurzQ3F)RD*=Mr3B^Tm%=EjHkk$LdYk+yP=FA zBSLsH!pobl)wA+n1Kz#V+Q3P{+X%+9wcBa@SlJ1~G?U{70$gBpHushG$tw12Esk+2 zevCru)M(7pC9~K|I4Zo0)CMDg1z%QNGtpc_^j7Z^t$77UNdBUTG5=iVXlcjMXZ?5E zN4^6-{h-6UpliHjt8%O?ilOzmg%Eygi7AaBXmL)^aVNDxQi~(AND_t}^NO7en%=d@ zs1(t5p-Xc5XrZN)KCTo-F#0X`pO@g!TLg?NNHfKQzO5jbx;E>xic@r|u>NC0;}3V|e<4_ZIArNBhyg#o2NaR#okg`S`rjD;yic4aW@VD(pMQf_0fOb!!k(v(pEo!qw=Sy}kJAxkD35MvK)M z=(asfG?;Y~?dKq2V7Ns~J1eOMy|^{3;fleiuzg~s2kfjcjz}EmHJ$egRMALvjb@us zyE&Q&K~7;SI`-ZogMk}XEk14|xjYv5F|*`w2al^{RaTj9C6>GN^ZY|sW7QWaq57E! zm_0`|__|aukj3*Y-v((ncz4f+YsD=VYoOChun+PkROH@_ z?djQ8Ln73y^`!3hJ;w+(WYpeB;9(?&N65Q`rj48Tt%otv?WW>FWn z&SU5@|7`QkP#CG;0LJPe-iy-ti}L=cEbsFgT0MjP2O}n=Amglnk=i$S{giGxbK%iO zgpJe9`QaK@0KNGQ-%O5(+5ijw;Bc1zA-?}(z?U}X-GHZ9k5^R(gUYI{9IHtu{=thA zd`*)C+Ueuf)KVBlkb&2}#1e4R+q`zA+<>O>dxQLaK`y&QT?A1jMDT7y48gso%T_BQm1}+WjPlu;fcNvg1i(uGbg2Hv2Kn!J|9|*) zz*O4SIuMh0cc;wVCXGbDm3-%|8lF2)Fzh}9)is1S?D9%d;3Rf0zSSuw>adSIQ$)n> z!UhB5j?#wxDT`4H7(sw)%#Cd4^-7x23;1B(B>rT`YDC{TTorL5z2wm|JHPkc$g@m| z7C;+^*?ldsk?*A`KQ+XO4zQgT`1DPXZ^Lc`Is##>$U~c))xv1Mm4i3HeU-5Ekt~r8 zhl#rJC2ppynDdGiOPgZ6LQI%>URZw$kif_7QA=~B*X0F8q@swLcBnG%-Z~1IN!1X^ zS%-DqDIS7~VNMw;wSLnAR%l%9Xw@xHf_mB*A#+4!HI$Apx^ojpiVorJ+a^d9HdYG; zni>B>m`MEq_EF{)T^sY^{=2)WlibEVoEJ^KXCQ3!Y_m&}NpW)QxYB`YSDq!QEz^~v z)b!B8X73^ja}x-JX0Q2X5(WrdgaAXPzvUa%7GT*Q_^$YG@eT1DD0iOA6Tg7Oft`yBoYe;+m787~->FJugfr82APq(y`2B>xa{}b} zeZcp}F9?wSR}H@FH*K7h`*YH-9Zu@=PVi5>ncis~*;H{1%N)Hl=;mzIcA&N|+c%(fz4&UCtb`0Z2RSrpK+zv+85vVN6X5eM4xw#@@6 z15nMZ$5H$)MyC65SXrmu4D)Me6nus0en?eBF*=9~UH$h)A2(9SH4khkG?qJ`&(tkK zx~H7v27R4F(QwS4*v5bLj=|jXKOv--~v(s1$bsX zFccKCYWE{g*v??7!Pl%oDnl=O8lY@}cMq;V)V^ryL6G*QJ+%QxB3rM}BZ^kyJeVYj zWGXo^rh>F+zKWg!A8KQaY6R;CQFiXGEB0L9d>r90Ix@B+T@K)ozHZzG_Hb|u7`CHL z!;r*b`0~+`n0h_D)$}3E3fbZtHOc8*b145CUaJ{YtNz-9qG$sbqKdE%!)IgtpB+x6 z5CxGKUQ38kuLZ)UWXy~=qMa&WIoPqT@EpT6he`Bg%I%9lXr74vQVFZdG#P`si|$YKK|2g-07g$|4OLkB^OBi4xwylc$iRA>I>Px-2bT3O$i( zh-JY-E|($=0Q>w!q>nuG}D`_43|~-!^`82pIMiOUZxV0|B`Y5^y&J ziQjLlLeU-xUH?HGiI3QAg}$pc{DPQjK!WdGbXNzI|2*7)1xugqiCW5Gyp9v_)DxX1 z<&KF@*b_-OJt!9^ zLPL{H)v`+z^$pnIZS>6`D4yBQgWG9oR>m~!Ux2$L^qcr%U~_t#(v{CH|9`ceWmFx@ zwzhGChTt09-Q5Z9?(XhRaQEQuuEE_QKyY_Wa0m_w_ObRk*osQ{9Y*I&u?SI3V~X}%WCHRN6`bfs;YiJ7@v{woD_9t+fN}YQU{(Ji z!2-CgpWbl(;rR6zg7t@8KA>ZM5-iHU^6CBu!J?Hu<*W<4C=>$YcD3hMdyJYOeFK%( zfrk{0x9)*pCW85`P!0mz6KJUKr03!*ZognM{VrW8 z9a1_IJ{1vH=0iLKa&W8?k^FZStAyx!m;(vHyWWz=%OMhYVw0!U;eIa~V%KDSkN3ZVjmSaLkHAr7qDpbYm{YaRl&y&F*N|XuC z!;I!#FMFXSodFAQA-A!()iJZ1Ax|oGBaYxSnS7n)gu2M7I$D z)c#|^a!$rcG7a9qUf(5!cIAJV3?P~PEK&>JckT%V2lW7Nw`8G zCL?0m;o)s%!TMc@q$wE+ix=V$n3vnyXQ)BNvZ87FS1cat9&j=D@*%F9eLe+W#F+i! z>*bxO-%k(f`JCT4JD(at`Jjf5R;&4U#FU|^Ht0|Gm=C2F`hY`P>ymH}kI~!l1wWWP zF!RcO@eQ0&-%h2{&Q7Cr3}JtPG_doKH(RuA6bHw_%KuounTU^_MJREeH%iI<{xj2M zA*)=I9crY`Q_dB~nk6Wy^6QJ;5Oi$&B5R`wUlm4sJB=~?xqTQT&tZ{nU;ijdTsa}S zYEkVM5iwHC=NTwc_BAxsB1heo_AT3S%no>4Y%;7Nv$fw7;Hi- zgta5;YdH>`S9~axk9mIvy17eQegN~8)3@i@hQAZ6tD65>lJNcy2^N5$p9JdSB%@#in_vta#u$DY3<2t<<@Fsf;#+DXa@cdKsfV>$U8cA!3sOJAZX;BC*&+oh5) zFmux2V$IC6R>{?3OPwg4w$JbOBxdvOC8rU{9a<8sAjULknvAF1I>s-L!!PuU-yoR3 zkQdP!fD%S?E>K|_|2lgP;g_%3L|$7WPQ-tdycfN@S!6xl%W8VPgHV zSbgA&x=rV90tL6;Zez5bG`+^8rH=8Vt@aJ6Blr@~|7q=H;G#{kH1W!H|VRmQiX-K)^NmyC*h*`3sjw`9; z$j2~>RtF>Czt)0flneEzhcXwS4Rm%fs!Ht_WbDwy7%Vv8J3+OZG6g=?Za@}Ad&k)? ziqhN$%wR*IeuMG3G*X|U<>?%;V_bIvCCC(fNUul6thcq0Ul>%yzZKd>Zhm9awL0T*3&YSt6DDBZ{Tsd+X-(+>X8ge+ z*Zfm_JBz&Ob!=eLTlW^+zItsO`!&OiUsQ}9qmanpQ1p%m7hAk}96d(D0@pgEig#H9k1`hz3> zkg|JhafWXVqv@GFtUeeN)1@>xge*>7Jc~=SGBe#Ln2(FNaA0}sv32)KxWM(N zYnQu~(8FY@GL-gTfmx(twGE-BGy^0%(9!1xUw(-R6Fcv+!6?g1pDjoQj&Un@dnKbZ zA}d^6Y@C*UzP4NWC3(zLL|#inKmP)@O9rv>PW=$#Nt#J&&X6Jz!*{Idgev8*j~P@M z%zE@1V|6=>`_3Y!qXBk>@)V|K720BG>Z7!=sqK6=I|`zn(`0$O#+4%@O}wG_Hxzu5 z&xbra`V;bNlLqMhSPx4f8Qoy20BSI*m27&S#;jtz_p>M#zP(4 zBXO@Q-nrM(hYB+`UvGJ*_9)J-6W9}0CZE#G7N3{WoEAhCmKvm$j?gR5pL;j1X zt3#+%cXWA<6*PMOFh%8HWr@AwAe%c}dZn6VeFF~~ezuCZs|;3(?1BQZ3AGZqu}sfx z|79s%x;!Sz+&;9rf=-8GLYu*!c*6GPm)AM6X$>2qx!gG$!v=WNk4qVlkEJb;N{6oRy_T(is~j zD8Ba&a!_>6$^0&i2O#7=``$Qebt~ld!Ff{Yg#>OBV$N*l5{Qr4Mst-wazSdSD0_(P zvw`aRHR>dN>DL!6&gU&8@}#haKwmmn zMYksAl)IP~O|A?qYg;}E8%T9QIidu~0ySF0Cjhf6#ya4{poC@D6?oz1W}(QdZIkSV zVRghdIWnk$xmBvo1{si$Mw+=frah6Gz#6$>24etL49dNSD2Ls^J`gSN;(seL>7_?{K{*w*)YX@%*$bV146xMZx*7&Z97vn zG7R`0uCLLfFiM`TUnp!vUy_;&_U*T+F}hVK_9wxyUPa}@2|e55H+(Y-xQ_wM_ygZ{ z{}kWLU{xW}KEq^nq_>@32(r>l!1_m#P!pSli;cBd0bPV+Dnf9Kj1N9$6dJ@{DqjGL zAAiyH{rLPs`NtP-zu@~1XZZs<<|n@Yy~Xd}eB0~BnUn)5p$ivD0|767nFcP-Tb4=H z5k_$ZMZZmgI#HB%Q9RqAANNu7sNyRRR6f{9Ph&wAG^$MZ$7tLYdK?;$ZOl83*aC0H zWsrr_V2kGN9nPNiz{Axyw{eL*kWNyoE2$Sp>?`9id3PEFe=p)gKpZ&Co`O;0CYlwbOSrO{zC$AvmP*_b$JPG;ZumypXh4Lg) zsQg)SkUT_~!XG!S{fn~yvS9KHzW?w`aX`oX#5dJn6*>RIgi{cBpfcpE8F`1Ni302} zspXXHZMx8g31z+(j26a0Daqs4LAmM!jDkdx8E~`MUKysM4#w6;Yfk!`iK-N_ew(o} z={%tw(RT6X#?bJBd%Pea!{*WYDcQU)rDGnrkCk24q+G}Ka5n9bdtBRoMBE?gR|6rO zv!!0+C@9Pf9O%y@L-h1=+AY7>Iq@NSt{G`FPsw%85okEWUsv15IP3N}L;m^fIiTK%`VpZUi4)A2*+W zx=YlZj*5yRQ%jBEX(B26O$K4J6qpU_asX}B` zJsr(?+=&-rZ!*W|uxUlyjYW8Up>s{UQfLhEhb`1cg? z>P?-;AF^vmPRvfSycF>*EQhz-w_z#i>pkKiuq>{^KZ;5@IBWC1a=~BtyifcFxZ>1T zc6f&^$~$UVvn@9hGntRGAic~$O9FLWj5aA*Pu2$Qls>X8USif@;bm9-98p3Z6* z#w;Gf#!jEY8H$y1)=V)byX=>sC`r)ifVCgxTfz0Bnb zU6AFE6K{0A;bZF|`e&F{P(||RYiy<_Ke}Xd2Re=xl@*4MZv>fIB2IVL@{3TZ&3CEF zsdu#Rn!4dQJvh?@qQfAiSG|rqL69#H^^9PrXqNj3%q!o`<;z1~E3B#MVj_`@{{qYJ zic3_)E*pw`--`YVXkK3j3I}i!)7KdZJ{=m>0r_;x41@%<FCVN)dzgwbDxxuKz%A3dhO86fr~$ym%?oL6w&=3){vP60(}7E@(00c_@@Mm zABfqbqdT5|S__#T1pS>9A-FxR;wOuPZ00o_dzT1@>%tAV?%K-sw>Pge%$UnPpC9Wl zx%hs37OYypvp#adY30`*9(WMNJMJNRQi8Fl zMsyZQIO|dEVByj7|GpC7ir-i9e!K&q{U^bq&5omZZu{FIV^M2Emn}a!DAHosmgSPy$`f349jGHWhXLzB6-@p`l$~m`SwA3~OK1oF{{FpSJ;jXkKcAy~k3&sGD)O5D_!)mwOJW zL_bH@s9UC~` z;hc=}NtC`ifXKb=D~kpp(v7z0FqVFcxm7RR*7J|7R=0bFS#g*5Khn%-Rt#|q-D;Jt znB3oV@_c#^w>bfX0p;m4g~3o8L*9UIyyYi5MPSgW_LPeAhV=B?oKu_+SQD&NifUZ) zn7HYg!(ZiYzjer=fYSaS`EL9}d;@~1p8_g^q;Iw|2n82{z#e*RQ_71`Uv$#P%Xfl} zd;)a0D0A><&j=&U2G{x)nGI6Yq3Iu=|NlGRfa?I>0R3hSxq})S=?=1lfLN1?Q*T_X zjoDE2G9QEWqD)XENr4RwVK6`tI&N#zu&?kA)$9@c{)an!PmBEV4uG5f%=f=inEyZD zmLe6jmXq_ePjg^ZZ-oQr5{fWMA=<@iN+t)`+&&?;mcH~L4u0y=TKmjx4eJ(SMuJ>Y z+r(e=7Vp)CFmlNoWq$W+#CS zrKIkILuoe%iku)|h&({2#-{2RE@5GcdhJfPTr@IO+en=fBwl3ZUVwT~mB?y7sRnvY zca5x765ycfIjuCppcIfwrXw;8Z;2O&bsF&zQhG!9CP=;J)@1b}k|98UkdrRFIkEum zEBNYAw!9)7!hS?zKiP8zrjE|7aN06&c!!j&<#xSm=(+aAm9p*#VQ!xoo4$Y#%nJP@ zD!)biIF+7lNa@XN+DwnQr3F^7BM~FpwLxywW)3;~faE>e7(PZRJ|y6}2We=!m0YRo+J|?Cx+^pdbN0Fy=W}iQ=VAhdq@)gu zs;&&ciE=sVRi=t(s+#YLedVYFxMryhmrqD(W2=3cwL85 zNezrBiUeG9`mU}c!bf|paw)wnPoHmxFQT%>BqQg1V|WR^ofnE!mtw>4#xrI;kHwJ^ar1 zkF$qg%uoDFn3!o}uZtfs4Y4!ddL~>@is^+H&UKP+xfOT@vJ8VyUffhb(Rdv`dKX=- ztO8c>zK@{4j?W{g4$vFVpP$;l=K%dXBk*s&qfBhjNOX_phxNWZkj2y5#pTt~Sb-+6 zn|QWiM{vCF8==1y;6L!L#<@=E1sbQqxVtW6EgHf-L?hKNw&X%cn>Pa$;9J2D)Jltf z<147C^2w+Macc2k0h8*=3pCIxK@oVk#YS>Iw(Oj3pre;*jaz;YAs;-IP1yBeo)*}i zXb^v`&J$$F>`vJ-a{F20I7~cG^-p2{n)CO{S zElc$X!*%j3JV9F^-l7276II8NzKdy6rS#06muvL8s#}+t z4C})@*Fcx??fxVt*fotuXtnd|jr|~d^I;EoIONzqN@Vxd3W(vS zZ48gN9CjO`k_f4xw+5Hjq8cSfWLjvxLgY@v0m8{r^hZNcuKGpX7a`NBuN__}QZ_u> z>9>3nW&_Om1K%xwh;M+$|4G09@a*9ieE(q&0qB^&IRDP~ze~XX<~!#25*pUNFA!3r zzFw%|5|~GFG}LvH6Y8~{xtwHg%DNx(-RpOgErsAL+iKW>CAX@M$_m^Ha~b_nil0?s zvaf+JT>OCxc{$%9 z=&>g|ezL1Z>LJ({h)*z+z{w&JY7p$O_G8t;lHWe0} zD6leknci4$Dw;F0p&e6+>4(QY6|;x zUT%2NwZuDYW>1GGi%|&wo*ptIyaig$ zSJO|ykUJR}oyKwW2L{Gjujbk0I;J&A(uh)MTP^0~bLJTOMeNS?*qUHugwNl*LOWYg zv!KgkY6Kmvtu${PUhiY+hKIprp$*9qDj-rT<}#DH-Rn|wgEA&fV(!#$pP*yKs5BG+ zp$7IIB@SpCwMpy%!2REghFA%Ni2-K(f$!Eo#5drwKk@yC*GGTB_a9EW0(^s?`2P16 zzoUP(XWhgGUG^;}{rjXQih5vZacB%^m=|A@9vdJK{8CD!5nsFjLI6T9tMoz#LWKD1 zZ%}lfo_SgVkQc7@*wC2Ju+V>>g1`t34gK$v5SS26ppBsMLFR+m=^%9bjQwM}nyX&V z&-c6rpB2J}Z>mYuSzG>O<;B2vZR=d^&`Zo~q@crnc7MsM z-4!hVzSC8Q?)Bog3h=RR(Ku~OwyY0tnJby>SD>yKp1-4yIBzlf)C2K|zB81moiTAm zQTw>1nQY%8Rm@NVN=YQ|iN{?0Oe+5^Z8)!1J``TxwHOGg-8l$;3D&S{(PrgX{(%Wd z|2^k%M$ur-MyeqD7mNwq?QLg)k1z1z>p1HAZ>nm|H!WWY8@xII5@%Sa6}I>WwJfj8 z?de1Tvbi&=MPEW*fhmS{cVmR&_-ENTwhxQQY8+&UKV`U?618 zhs~~~W>-(RbC=sg(v1nTPEYE+d!qt^OP3ev%jDDR!q?MZ)j5f_3@Dl=rr?f3LR)>u zoUcxqSC~hO>xm+#NVJ6f>_&n4$wT_=oD>*8RHOSNRN;96>pgK$EYOJ-AI~|%&_cX- zED&xp$?Yd#yS5&hWk^3Rf$^{N7c7kh-3>GMx32--Op@?V^mH2hAUf5x+k}S@K`RaE z<_m1B_J1grVH@aW2k`jAKyCY{2I|7QJ5&P4nXVCxDL+-qZ+tX3F*3UX@~+(eZW;lB zv6Us1G#kqNu;!zTNe46dnE*HXeYyj9#Q#H}{Es`LdQko6hx&E0wSiTWA-7g8jUvaE zT8NLf*u^Vu{rVKJT#fPKz-Dfp4sy>J;-+u*Xl@2>edFz)0}*>Eif>=la0KUQFC&Qc zFXG5$wXo0kKeT1j?d5W%AfxvBfP+SNHVrG$2nX$ifF|YW(N#Mbg~r+xxhJ{_UJH?m zJ0KS|>r)sbN1GCJcyhs6e1hAXz0`b_@+t)1;H?{hq$bJNxktLyq-q;$l3w&C;+k?X z&gQ|F?}u-z_A^%cGKo84Kl>d=I$cLgmyI3Ku>%hZ%wj|cjnIKyROFQ?(T;)lLt)9d z`yAw$v3Aq~w^Zf$C0Aj~5E$flq)%Vm7kVf{@D;F(GQ^s`jepF&;uG4z@3y?~$Iy@f zLs^dqq=gyU`Lf`xENtwbxQJ1zGvBbfu!VWFdskDGtO26q>b@`9E45HaM(@S~1rN+fPg*RM4Bx2B zLCx4_X*A8>~*b&(VIiOeZrr)zCZasr$AhC3LCk|~rdPX-fQR^XZ& zX{v-4dS+P#S+&@6fE&-|*pKSs9;~jZspg=11#v>js|9sYJ&S`r5_Pz779$ldTBYm& z^ay?Dosl(YgZUubowB1Ka<+7bawskgo0Cq52~UOJV<)w|1b$q~!)#gW5L^fOM$@KL zZA1_RtWyTDl{H`@|4o86-Q?T@jRZeihab#@hZp%!2xXaVhcaZ8uGR>b6lW7Ek6sq; z#_4$G(RKz@k<8a2J8|Pm&T&I}jS-8l!qTzFZsf-|(=4HBx*X0d=1be)F7^8$@F1O} zKC>6EQkt1(Ni)giueS&Ito)Fk&>bEf-5X_N5|C#&=k;4dN;nKWcRZ3Ik2hgo?H@K=o9@&WE~pQhSMNZ8mTTTW9_!9V)UYITzM)+- zBxrigOmE+Qfw@kKZhtF9!tx66IcOPR()e z;Y-c6Y+;O_#wZJr<27^o4b-Aq`di}o_+T1K=T**!xhuR)`}(|{;2R!Quv76WMfypc z=J{G2=}J?doIzQFIK;Jq)bR4hgv;csh#Nenf!15$?_mltJdl9%g*=KjC?Ok6J;XG) zgr|@o?G6*NxgAN&)%)&wUA4(R)6;H0gf6We#1BWF-so6j6XQ!jz7frf2&^o!?j7jm^uI9>|a2~{KPl;zkaRiZ@xX; za2a|r3m%mt6Kvy5vCKf3>{%GH`xvgIP!=Y@VCzP@-OBsk*4=A(DwE;D%-mGLj&OT< z7jo~DppxMptyqBIMK>^G*B&h`5mH>V^1_iX>!+E+i8IgQsqzLrRqL(g?ZXeZLeaci zi6x!=xL=E>^TH(vHf6QeTCmOB<{{#P(;VUfHp!43A563Gz`%^Ek;rF){9Uh-3G(GLSGR4LT8U2UMEIm| z45~Y>BPNp;0SgXLczSWp23s;T?ayN>)OzhyKn_c8x?O>Eqo}v0de})GnTT7>sAEti zs||au5+KvW*c(DZrIUys4V*;fc6g^r+xPEU<7aXuJ0ecpR$(+{zG_i0N<#EFU z$%s-y2#O}!U+Y$X-qS-_U^~p(qHiXhc~PM zbj(kDQ;j_WqQbxV&fo20z%#^o~AWp-XIj!}hE~!V;M>b*D%^MxC+}u5F_;1NyeMC%+1bCkc7+ z=m`0_EVoU*qNf^SF!S;ZfnF&-=-VaqG}zuy4l9tzw5j6TXbF&s|-&dlS5uR9=W#>3?;H+s{S3G$t#6h{t%uPj+eaKl? z2!d^~2|>G1CcJj*PP?dH!4eJ9IBH&m=Npq&BqkNgjHOczor%s5b9BPafXG#nSJmIX zMWOJzhjq2Ez^5&)Z)fZFK*YAe;spfugsi-#r*s*(Ru_PV-}1frUsE}|{wcnjWs4-W z!QxK?cdP9#N6M+g2G_$l6%55CKCG(4l)jq193SM%N7_Y3{%An)m`?hu+R`7_1%JW! zKfSU}rzvjojQp`PA(a_sVp&Nyeai5GsMq#;QOcuC;1{OKeao<%5&OuMUQvmo_=noR$ zpc$^2*eYkW`t2ZWVZrTKFn}#5EN-8&_?#|w-Ef=$zR)&oXClQNbL;6yyB~Ga19r8S z%X?OlS)U+$laQ72L$~3lOWJCB(`yp0mrnA{hPEQ zmQ3clapA;oh{38vQh^FxL-5MDY&4OOINW8x?kZ>6prL&xLnJX%cI+6g+>;f5XB`;xzdu;*M zcvzv=Dd`M(DJu*9{2K2-H1AXGdO&nV&c5bN(;}%s^Gqw1bfrd5;=JH7&ivO`0sIT@ znINrOS8+$X213(%Y-~XCg!CwwW6lH`*d}D&Ot6X{!-;vMP%W%{#o?|c&`)H_p z^|p#SU4Oe#)K_V5i;>+b7ZR^M5$bq=UU#bPaZSq4WuQVR!iIb#raBAJ2kRw!Sbagr z!>mm{p-b8C(ZBqE5vj02@xeaea@!mex0%Uf`-p2?BfCu1tp(}oN@PkrF{!U%(K z&Jx?4MK@0t@0Y|SG)WgQyo|(2J#6|Kl8)7?)i65~6C)2&LEA9p#XQr1Ubysnk^31D zDyEvx>BOBBZRY1x_4{;%#M z&{&L0^`_KG@Q(XXJ8r94sM{zOrpdgSeh(#1$`uQ(3j%5$`ePqAcs(z`o zYa=xRla8v~V-P~^8;I>;(CmEZD?TdT>dSFl%qv2_R%>&XXo9u++52`t0=K2a672}n z>elI|y8KNBC}aDBPY?z{gP3U<`LG!|fxqxHnSVsAYHzf(x9!Tyn|Q(duF5&19H(Lj zk&ZFz@&Z@Xc747oB86?swDX{SI+VEb2G5&lP}iu zkq!(9@mkJTk&$oEFQgyeflP5%ver!im=otRI diff --git a/platform-sdk/swirlds-platform-core/src/jmh/java/com/swirlds/platform/core/jmh/EventBenchmarks.java b/platform-sdk/swirlds-platform-core/src/jmh/java/com/swirlds/platform/core/jmh/EventBenchmarks.java index 270424f26eef..4708181009cf 100644 --- a/platform-sdk/swirlds-platform-core/src/jmh/java/com/swirlds/platform/core/jmh/EventBenchmarks.java +++ b/platform-sdk/swirlds-platform-core/src/jmh/java/com/swirlds/platform/core/jmh/EventBenchmarks.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. @@ -17,15 +17,12 @@ package com.swirlds.platform.core.jmh; import com.hedera.hapi.platform.event.GossipEvent; -import com.swirlds.common.constructable.ConstructableRegistry; import com.swirlds.common.constructable.ConstructableRegistryException; import com.swirlds.common.io.streams.MerkleDataInputStream; import com.swirlds.common.io.streams.MerkleDataOutputStream; import com.swirlds.platform.event.PlatformEvent; import com.swirlds.platform.event.hashing.EventHasher; import com.swirlds.platform.event.hashing.PbjStreamHasher; -import com.swirlds.platform.system.BasicSoftwareVersion; -import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.test.fixtures.event.TestingEventBuilder; import java.io.IOException; import java.io.PipedInputStream; @@ -78,9 +75,6 @@ public void setup() throws IOException, ConstructableRegistryException { .setSelfParent(new TestingEventBuilder(random).build()) .setOtherParent(new TestingEventBuilder(random).build()) .build(); - StaticSoftwareVersion.setSoftwareVersion( - new BasicSoftwareVersion(event.getSoftwareVersion().major())); - ConstructableRegistry.getInstance().registerConstructables("com.swirlds.platform.system"); final PipedInputStream inputStream = new PipedInputStream(); final PipedOutputStream outputStream = new PipedOutputStream(inputStream); outStream = new MerkleDataOutputStream(outputStream); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/PlatformBuilder.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/PlatformBuilder.java index 04c3b6229909..8f04b8794476 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/PlatformBuilder.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/PlatformBuilder.java @@ -58,7 +58,6 @@ import com.swirlds.platform.state.signed.ReservedSignedState; import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.SoftwareVersion; -import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.system.status.StatusActionSubmitter; import com.swirlds.platform.util.RandomBuilder; import edu.umd.cs.findbugs.annotations.NonNull; @@ -194,8 +193,6 @@ private PlatformBuilder( this.selfId = Objects.requireNonNull(selfId); this.consensusEventStreamName = Objects.requireNonNull(consensusEventStreamName); this.rosterHistory = Objects.requireNonNull(rosterHistory); - - StaticSoftwareVersion.setSoftwareVersion(softwareVersion); } /** diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/EventSerializationUtils.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/EventSerializationUtils.java index 68f43c45715c..442b91f58875 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/EventSerializationUtils.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/EventSerializationUtils.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. @@ -16,147 +16,22 @@ package com.swirlds.platform.event; -import com.hedera.hapi.platform.event.EventTransaction; -import com.hedera.hapi.platform.event.EventTransaction.TransactionOneOfType; import com.hedera.hapi.platform.event.GossipEvent; -import com.hedera.hapi.platform.event.StateSignatureTransaction; -import com.hedera.pbj.runtime.OneOf; -import com.hedera.pbj.runtime.io.buffer.Bytes; -import com.swirlds.common.crypto.SignatureType; import com.swirlds.common.io.streams.SerializableDataInputStream; import com.swirlds.common.io.streams.SerializableDataOutputStream; -import com.swirlds.common.platform.NodeId; -import com.swirlds.platform.system.SoftwareVersion; -import com.swirlds.platform.system.StaticSoftwareVersion; -import com.swirlds.platform.system.events.EventDescriptorWrapper; -import com.swirlds.platform.system.events.UnsignedEvent; import edu.umd.cs.findbugs.annotations.NonNull; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; /** * A utility class for serializing and deserializing events using legacy serialization. This will be removed as soon as * event serialization is completely migrated to protobuf. */ public final class EventSerializationUtils { - private static final int MAX_ARRAY_LENGTH = 1_000_000; - private static final long APPLICATION_TRANSACTION_CLASS_ID = 0x9ff79186f4c4db97L; - private static final int APPLICATION_TRANSACTION_VERSION = 1; - private static final long STATE_SIGNATURE_CLASS_ID = 0xaf7024c653caabf4L; - private static final int STATE_SIGNATURE_VERSION = 3; - private static final int UNSIGNED_EVENT_VERSION = 4; - private static final int PLATFORM_EVENT_VERSION = 3; - private EventSerializationUtils() { // Utility class } - - /** - * Deserialize the event as {@link UnsignedEvent}. - * - * @param in the stream from which this object is to be read - * @return the deserialized event - * @throws IOException if unsupported transaction types are encountered - */ - @NonNull - private static UnsignedEvent deserializeUnsignedEvent(@NonNull final SerializableDataInputStream in) - throws IOException { - Objects.requireNonNull(in, "The input stream must not be null"); - final int version = in.readInt(); - if (version != UNSIGNED_EVENT_VERSION) { - throw new IOException("Unsupported version: " + version); - } - - final SoftwareVersion softwareVersion = - in.readSerializable(StaticSoftwareVersion.getSoftwareVersionClassIdSet()); - - final NodeId creatorId = in.readSerializable(false, NodeId::new); - if (creatorId == null) { - throw new IOException("creatorId is null"); - } - final EventDescriptorWrapper selfParent = EventDescriptorWrapper.deserialize(in); - final List otherParents = EventDescriptorWrapper.deserializeList(in); - final long birthRound = in.readLong(); - - final Instant timeCreated = in.readInstant(); - in.readInt(); // read serialized length - final List transactionList = new ArrayList<>(); - final int transactionSize = in.readInt(); - if (transactionSize > 0) { - in.readBoolean(); // allSameClass - } - for (int i = 0; i < transactionSize; i++) { - final long classId = in.readLong(); - final int classVersion = in.readInt(); - if (classId == APPLICATION_TRANSACTION_CLASS_ID) { - final OneOf oneOf = new OneOf<>( - TransactionOneOfType.APPLICATION_TRANSACTION, - deserializeApplicationTransaction(in, classVersion)); - transactionList.add(new EventTransaction(oneOf)); - } else if (classId == STATE_SIGNATURE_CLASS_ID) { - final OneOf oneOf = new OneOf<>( - TransactionOneOfType.STATE_SIGNATURE_TRANSACTION, - deserializeStateSignatureTransaction(in, classVersion)); - transactionList.add(new EventTransaction(oneOf)); - } else { - throw new IOException("Unknown classId: " + classId); - } - } - - return new UnsignedEvent( - softwareVersion, creatorId, selfParent, otherParents, birthRound, timeCreated, transactionList); - } - - @NonNull - private static Bytes deserializeApplicationTransaction( - @NonNull final SerializableDataInputStream in, final int classVersion) throws IOException { - if (classVersion != APPLICATION_TRANSACTION_VERSION) { - throw new IOException("Unsupported application class version: " + classVersion); - } - return Bytes.wrap(Objects.requireNonNull(in.readByteArray(MAX_ARRAY_LENGTH))); - } - - @NonNull - private static StateSignatureTransaction deserializeStateSignatureTransaction( - @NonNull final SerializableDataInputStream in, final int classVersion) throws IOException { - if (classVersion != STATE_SIGNATURE_VERSION) { - throw new IOException("Unsupported state signature class version: " + classVersion); - } - final byte[] sigBytes = in.readByteArray(MAX_ARRAY_LENGTH); - final byte[] hashBytes = in.readByteArray(MAX_ARRAY_LENGTH); - final long round = in.readLong(); - in.readInt(); // epochHash is always null - return new StateSignatureTransaction(round, Bytes.wrap(sigBytes), Bytes.wrap(hashBytes)); - } - - /** - * Deserialize the event as {@link PlatformEvent}. - * - * @param in the stream from which this object is to be read - * @param readVersion if true, the event version number will be read from the stream - * @return the deserialized event - * @throws IOException if unsupported transaction types are encountered - */ - @NonNull - public static PlatformEvent deserializePlatformEvent( - @NonNull final SerializableDataInputStream in, final boolean readVersion) throws IOException { - if (readVersion) { - final int eventVersion = in.readInt(); - if (eventVersion != PLATFORM_EVENT_VERSION) { - throw new IOException("Unsupported event version: " + eventVersion); - } - } - final UnsignedEvent unsignedEvent = EventSerializationUtils.deserializeUnsignedEvent(in); - final byte[] signature = in.readByteArray(SignatureType.RSA.signatureLength()); - - return new PlatformEvent(unsignedEvent, signature); - } - /** * Serialize and then deserialize the given {@link PlatformEvent}. * diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/preconsensus/PcesFileIterator.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/preconsensus/PcesFileIterator.java index a48466483668..cd2c42231ae3 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/preconsensus/PcesFileIterator.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/preconsensus/PcesFileIterator.java @@ -20,7 +20,6 @@ import com.swirlds.common.io.IOIterator; import com.swirlds.common.io.streams.SerializableDataInputStream; import com.swirlds.platform.event.AncientMode; -import com.swirlds.platform.event.EventSerializationUtils; import com.swirlds.platform.event.PlatformEvent; import edu.umd.cs.findbugs.annotations.NonNull; import java.io.BufferedInputStream; @@ -85,7 +84,6 @@ private void findNext() throws IOException { try { final PlatformEvent candidate = switch (fileVersion) { - case ORIGINAL -> EventSerializationUtils.deserializePlatformEvent(stream, true); case PROTOBUF_EVENTS -> new PlatformEvent(stream.readPbjRecord(GossipEvent.PROTOBUF)); }; if (candidate.getAncientIndicator(fileType) >= lowerBound) { diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/preconsensus/PcesFileVersion.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/preconsensus/PcesFileVersion.java index 39bec6b02cee..5d5455d73e9b 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/preconsensus/PcesFileVersion.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/preconsensus/PcesFileVersion.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. @@ -22,8 +22,6 @@ * The version of the file format used to serialize preconsensus events. */ public enum PcesFileVersion { - /** The original version of the file format. */ - ORIGINAL(1), /** The version of the file format that serializes events as protobuf. */ PROTOBUF_EVENTS(2); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/recovery/EventRecoveryWorkflow.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/recovery/EventRecoveryWorkflow.java index a9e67fc85656..90a8f4952096 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/recovery/EventRecoveryWorkflow.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/recovery/EventRecoveryWorkflow.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. @@ -61,7 +61,6 @@ import com.swirlds.platform.state.snapshot.SignedStateFileWriter; import com.swirlds.platform.system.InitTrigger; import com.swirlds.platform.system.Round; -import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.system.SwirldMain; import com.swirlds.platform.system.SwirldState; import com.swirlds.platform.system.events.CesEvent; @@ -153,9 +152,6 @@ public static void recoverState( try (final ReservedSignedState initialState = SignedStateFileReader.readStateFile( platformContext.getConfiguration(), signedStateFile) .reservedSignedState()) { - StaticSoftwareVersion.setSoftwareVersion( - initialState.get().getState().getReadablePlatformState().getCreationSoftwareVersion()); - logger.info( STARTUP.getMarker(), "State from round {} loaded.", diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/StaticSoftwareVersion.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/StaticSoftwareVersion.java deleted file mode 100644 index b6d2d64f7794..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/StaticSoftwareVersion.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2024 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.swirlds.platform.system; - -import edu.umd.cs.findbugs.annotations.NonNull; -import java.util.Set; - -/** - * Holds a static reference to information about the current software version. Needed due to inability to cleanly inject - * contextual data during deserialization. - * - * @deprecated this class is a short term work around, do not add new dependencies on this class - */ -@Deprecated -public final class StaticSoftwareVersion { - - /** - * The current software version. - */ - private static Set softwareVersionClassIdSet; - /** - * The class ID of the Hedera software version. - * This is needed for migrating from 0.53.0 version of software to 0.54.0 - */ - private static final Long hederaSoftwareVersionClassId = 0x6f2b1bc2df8cbd0cL; - - private StaticSoftwareVersion() {} - - /** - * Set the current software version. - * - * @param softwareVersion the current software version - */ - public static void setSoftwareVersion(@NonNull final SoftwareVersion softwareVersion) { - softwareVersionClassIdSet = Set.of(softwareVersion.getClassId(), hederaSoftwareVersionClassId); - } - - /** - * Reset this object. Required for testing. - */ - public static void reset() { - softwareVersionClassIdSet = null; - } - - /** - * Get a set that contains the class ID of the current software version. A convenience method that avoids the - * recreation of the set every time it is needed. - * - * @return a set that contains the class ID of the current software version - */ - @NonNull - public static Set getSoftwareVersionClassIdSet() { - if (softwareVersionClassIdSet == null) { - throw new IllegalStateException("Software version not set"); - } - return softwareVersionClassIdSet; - } -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/CesEvent.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/CesEvent.java index f19b279f689a..7e0eb38bdc1f 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/CesEvent.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/CesEvent.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2024 Hedera Hashgraph, LLC + * Copyright (C) 2016-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. @@ -31,7 +31,6 @@ import com.swirlds.common.platform.NodeId; import com.swirlds.common.stream.StreamAligned; import com.swirlds.common.stream.Timestamped; -import com.swirlds.platform.event.EventSerializationUtils; import com.swirlds.platform.event.PlatformEvent; import com.swirlds.platform.system.transaction.ConsensusTransaction; import com.swirlds.platform.system.transaction.Transaction; @@ -51,7 +50,6 @@ public class CesEvent extends AbstractSerializableHashable public static final long UNDEFINED = -1; public static final long CLASS_ID = 0xe250a9fbdcc4b1baL; - private static final int CES_EVENT_VERSION_ORIGINAL = 1; private static final int CES_EVENT_VERSION_PBJ_EVENT = 2; private static final int CONSENSUS_DATA_CLASS_VERSION = 2; @@ -108,7 +106,6 @@ public void serialize(@NonNull final SerializableDataOutputStream out) throws IO @Override public void deserialize(@NonNull final SerializableDataInputStream in, final int version) throws IOException { this.platformEvent = switch (version) { - case CES_EVENT_VERSION_ORIGINAL -> EventSerializationUtils.deserializePlatformEvent(in, false); case CES_EVENT_VERSION_PBJ_EVENT -> new PlatformEvent(in.readPbjRecord(GossipEvent.PROTOBUF)); default -> throw new IOException("Unsupported version " + version);}; diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/EventDescriptorWrapper.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/EventDescriptorWrapper.java index b6a44b85f6f3..635ba86b4858 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/EventDescriptorWrapper.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/EventDescriptorWrapper.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. @@ -16,23 +16,11 @@ package com.swirlds.platform.system.events; -import static com.swirlds.common.io.streams.SerializableStreamConstants.NULL_LIST_ARRAY_LENGTH; -import static com.swirlds.common.io.streams.SerializableStreamConstants.NULL_VERSION; - import com.hedera.hapi.platform.event.EventDescriptor; import com.swirlds.common.crypto.Hash; -import com.swirlds.common.io.exceptions.InvalidVersionException; -import com.swirlds.common.io.streams.SerializableDataInputStream; -import com.swirlds.common.io.streams.SerializableDataOutputStream; import com.swirlds.common.platform.NodeId; import com.swirlds.platform.event.AncientMode; -import com.swirlds.platform.system.address.AddressBook; import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.Nullable; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; /** * A wrapper class for {@link EventDescriptor} that includes the hash of the event descriptor. @@ -41,27 +29,7 @@ public record EventDescriptorWrapper( @NonNull EventDescriptor eventDescriptor, @NonNull Hash hash, @NonNull NodeId creator) { public static final long CLASS_ID = 0x825e17f25c6e2566L; - private static final class ClassVersion { - public static final int ORIGINAL = 1; - /** - * The creator field is serialized as a self serializable node id. - * - * @since 0.40.0 - */ - public static final int SELF_SERIALIZABLE_NODE_ID = 2; - /** - * The birthRound field is added. - * - * @since 0.46.0 - */ - public static final int BIRTH_ROUND = 3; - - public static boolean valid(final int version) { - return version >= SELF_SERIALIZABLE_NODE_ID && version <= BIRTH_ROUND; - } - } - - public EventDescriptorWrapper(@NonNull EventDescriptor eventDescriptor) { + public EventDescriptorWrapper(@NonNull final EventDescriptor eventDescriptor) { this(eventDescriptor, new Hash(eventDescriptor.hash()), NodeId.of(eventDescriptor.creatorNodeId())); } @@ -77,173 +45,4 @@ public long getAncientIndicator(@NonNull final AncientMode ancientMode) { case BIRTH_ROUND_THRESHOLD -> eventDescriptor.birthRound(); }; } - - /** - * Get the version of the class. - * @return the version of the class - */ - public int getVersion() { - return ClassVersion.BIRTH_ROUND; - } - - /** - * Deserialize an {@link EventDescriptorWrapper} from a {@link SerializableDataInputStream}. - * - * @param out the {@link SerializableDataInputStream} to write to - * @param obj the {@link EventDescriptorWrapper} to serialize may be null - * - * @throws IOException if an IO error occurs - */ - public static void serialize( - @NonNull final SerializableDataOutputStream out, @Nullable final EventDescriptorWrapper obj) - throws IOException { - if (obj == null) { - out.writeInt(NULL_VERSION); - } else { - out.writeInt(ClassVersion.BIRTH_ROUND); - serializeObject(out, obj); - } - } - - private static void serializeObject( - @NonNull final SerializableDataOutputStream out, @NonNull final EventDescriptorWrapper obj) - throws IOException { - out.writeSerializable(obj.hash(), false); - out.writeSerializable(obj.creator(), false); - out.writeLong(obj.eventDescriptor().generation()); - out.writeLong(obj.eventDescriptor().birthRound()); - } - - /** - * Serialize a list of {@link EventDescriptorWrapper} to a {@link SerializableDataOutputStream}. - * - * @param out the {@link SerializableDataOutputStream} to write to - * @param list the list of {@link EventDescriptorWrapper} to serialize may be null - * - * @throws IOException if an IO error occurs - */ - public static void serializeList( - @NonNull final SerializableDataOutputStream out, @Nullable final List list) - throws IOException { - if (list == null) { - out.writeInt(NULL_LIST_ARRAY_LENGTH); - } else { - final Iterator iterator = list.iterator(); - int size = list.size(); - out.writeInt(size); - if (size != 0) { - out.writeBoolean( - true); // if the class ID and version is written only once, we need to write it when we come - // across - // the first non-null member, this variable will keep track of whether its written or not - boolean classIdVersionWritten = false; - while (iterator.hasNext()) { - final EventDescriptorWrapper serializable = iterator.next(); - if (serializable == null) { - out.writeBoolean(true); - continue; - } - out.writeBoolean(false); - if (!classIdVersionWritten) { - // this is the first non-null member, so we write the ID and version - - out.writeInt(serializable.getVersion()); - classIdVersionWritten = true; - } - serializeObject(out, serializable); - } - } - } - } - - /** - * Deserialize an {@link EventDescriptorWrapper} from a {@link SerializableDataInputStream}. - * - * @param in the {@link SerializableDataInputStream} to read from - * @return the deserialized {@link EventDescriptorWrapper} may be null - * - * @throws IOException if an IO error occurs - */ - @Nullable - public static EventDescriptorWrapper deserialize(@NonNull final SerializableDataInputStream in) throws IOException { - final int version = in.readInt(); - if (version == NULL_VERSION) { - return null; - } - - return deserializeObject(in, version); - } - - @NonNull - private static EventDescriptorWrapper deserializeObject(@NonNull final SerializableDataInputStream in, int version) - throws IOException { - if (!ClassVersion.valid(version)) { - throw new InvalidVersionException( - ClassVersion.SELF_SERIALIZABLE_NODE_ID, ClassVersion.BIRTH_ROUND, version); - } - final Hash hash = in.readSerializable(false, Hash::new); - if (hash == null) { - throw new IOException("hash cannot be null"); - } - final NodeId creator = in.readSerializable(false, NodeId::new); - if (creator == null) { - throw new IOException("creator cannot be null"); - } - final long generation = in.readLong(); - final long birthRound; - if (version < ClassVersion.BIRTH_ROUND) { - birthRound = EventConstants.BIRTH_ROUND_UNDEFINED; - } else { - birthRound = in.readLong(); - } - - return new EventDescriptorWrapper(new EventDescriptor(hash.getBytes(), creator.id(), birthRound, generation)); - } - - /** - * Deserialize a list of {@link EventDescriptorWrapper} from a {@link SerializableDataInputStream}. - * - * @param in the {@link SerializableDataInputStream} to read from - * @return the deserialized list of {@link EventDescriptorWrapper} may be null - * - * @throws IOException if an IO error occurs - */ - @Nullable - public static List deserializeList(@NonNull final SerializableDataInputStream in) - throws IOException { - final int length = in.readInt(); - if (length == NULL_LIST_ARRAY_LENGTH) { - return null; - } else { - final List list = new ArrayList<>(length); - if (length > AddressBook.MAX_ADDRESSES) { - throw new IOException(String.format( - "The input stream provided a length of %d for the list/array " - + "which exceeds the maxLength of %d", - length, AddressBook.MAX_ADDRESSES)); - } - if (length == 0) { - return list; - } else { - final boolean allSameClass = in.readBoolean(); - int version = -1; - for (int i = 0; i < length; i++) { - - if (allSameClass) { - final boolean isNull = in.readBoolean(); - if (isNull) { - list.add(null); - } else { - if (version == -1) { - // this is the first non-null member, so we read the ID and version - version = in.readInt(); - } - list.add(deserializeObject(in, version)); - } - } - } - } - return list; - } - } } diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/CesEventTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/CesEventTest.java index 12e1e38a78fe..9318af265b07 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/CesEventTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/CesEventTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2024 Hedera Hashgraph, LLC + * Copyright (C) 2022-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. @@ -22,12 +22,9 @@ import com.swirlds.common.constructable.ConstructableRegistryException; import com.swirlds.common.test.fixtures.Randotron; import com.swirlds.common.test.fixtures.io.InputOutputStream; -import com.swirlds.platform.system.BasicSoftwareVersion; -import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.system.events.CesEvent; import com.swirlds.platform.test.fixtures.event.TestingEventBuilder; import java.io.IOException; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -36,12 +33,6 @@ public class CesEventTest { public static void setUp() throws ConstructableRegistryException { final ConstructableRegistry registry = ConstructableRegistry.getInstance(); registry.registerConstructables("com.swirlds.platform"); - StaticSoftwareVersion.setSoftwareVersion(new BasicSoftwareVersion(1)); - } - - @AfterAll - static void afterAll() { - StaticSoftwareVersion.reset(); } @Test diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/platform/event/EventMigrationTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/EventMigrationTest.java similarity index 78% rename from hedera-node/hedera-app/src/test/java/com/hedera/node/app/platform/event/EventMigrationTest.java rename to platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/EventMigrationTest.java index 224ad006b637..426bd49cdc89 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/platform/event/EventMigrationTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/EventMigrationTest.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. @@ -14,17 +14,13 @@ * limitations under the License. */ -package com.hedera.node.app.platform.event; +package com.swirlds.platform.event; -import com.hedera.hapi.node.base.SemanticVersion; -import com.hedera.node.app.version.ServicesSoftwareVersion; import com.swirlds.common.constructable.ConstructableRegistry; import com.swirlds.common.constructable.ConstructableRegistryException; import com.swirlds.common.crypto.Hash; -import com.swirlds.platform.event.PlatformEvent; import com.swirlds.platform.event.hashing.DefaultEventHasher; import com.swirlds.platform.recovery.internal.EventStreamSingleFileIterator; -import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.system.events.EventDescriptorWrapper; import edu.umd.cs.findbugs.annotations.NonNull; import java.io.File; @@ -36,34 +32,26 @@ import java.util.stream.Stream; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -@Disabled("Temporarily disabling until platform refactoring is complete") public class EventMigrationTest { @BeforeAll public static void setUp() throws ConstructableRegistryException { ConstructableRegistry.getInstance().registerConstructables(""); - StaticSoftwareVersion.setSoftwareVersion(new ServicesSoftwareVersion(SemanticVersion.DEFAULT, 0)); } public static Stream migrationTestArguments() { - return Stream.of( - Arguments.of("eventFiles/previewnet-53/2024-08-26T10_38_35.016340634Z.events", 637, 4), - Arguments.of("eventFiles/testnet-53/2024-09-10T00_00_00.021456201Z.events", 635, 5)); + return Stream.of(Arguments.of("eventFiles/testnet-57/2025-01-10T00_00_00.016538241Z.events", 532, 3)); } /** - * Tests the migration of events as we are switching events to protobuf. The main thing we are testing is that the + * Tests the migration of events. The main thing we are testing is that the * hashes of old events can still be calculated when the code changes. This is done by calculating the hashes of the * events that are read and matching them to the parent descriptors inside the events. The parents of most events * will be present in the file, except for a few events at the beginning of the file. - *

- * Even though this could be considered a platform test, it needs to be in the services module because the event - * contains a {@link SerializableSemVers} which is a services class */ @ParameterizedTest @MethodSource("migrationTestArguments") diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamMultiFileIteratorTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamMultiFileIteratorTest.java index 862062bce34f..f1a5c667b3ea 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamMultiFileIteratorTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamMultiFileIteratorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2024 Hedera Hashgraph, LLC + * Copyright (C) 2022-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. @@ -37,8 +37,6 @@ import com.swirlds.platform.recovery.internal.EventStreamMultiFileIterator; import com.swirlds.platform.recovery.internal.EventStreamRoundLowerBound; import com.swirlds.platform.recovery.internal.EventStreamTimestampLowerBound; -import com.swirlds.platform.system.BasicSoftwareVersion; -import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.system.events.CesEvent; import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; @@ -53,7 +51,6 @@ import java.util.NoSuchElementException; import java.util.Objects; import java.util.Random; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -64,12 +61,6 @@ class EventStreamMultiFileIteratorTest { @BeforeAll static void beforeAll() throws ConstructableRegistryException { ConstructableRegistry.getInstance().registerConstructables("com.swirlds"); - StaticSoftwareVersion.setSoftwareVersion(new BasicSoftwareVersion(1)); - } - - @AfterAll - static void afterAll() { - StaticSoftwareVersion.reset(); } public static void assertEventsAreEqual(final CesEvent expected, final CesEvent actual) { diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamPathIteratorTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamPathIteratorTest.java index 8c94fd6b3893..87692bbd5bf7 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamPathIteratorTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamPathIteratorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2024 Hedera Hashgraph, LLC + * Copyright (C) 2022-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. @@ -33,8 +33,6 @@ import com.swirlds.platform.recovery.internal.EventStreamPathIterator; import com.swirlds.platform.recovery.internal.EventStreamRoundLowerBound; import com.swirlds.platform.recovery.internal.EventStreamTimestampLowerBound; -import com.swirlds.platform.system.BasicSoftwareVersion; -import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.system.events.CesEvent; import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; @@ -49,7 +47,6 @@ import java.util.NoSuchElementException; import java.util.Objects; import java.util.Random; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -60,12 +57,6 @@ class EventStreamPathIteratorTest { @BeforeAll static void beforeAll() throws ConstructableRegistryException { ConstructableRegistry.getInstance().registerConstructables("com.swirlds"); - StaticSoftwareVersion.setSoftwareVersion(new BasicSoftwareVersion(1)); - } - - @AfterAll - static void afterAll() { - StaticSoftwareVersion.reset(); } @Test diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamRoundIteratorTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamRoundIteratorTest.java index 66da0e3546d9..5dc433c3a632 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamRoundIteratorTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamRoundIteratorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2024 Hedera Hashgraph, LLC + * Copyright (C) 2022-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. @@ -38,8 +38,6 @@ import com.swirlds.platform.recovery.internal.EventStreamPathIterator; import com.swirlds.platform.recovery.internal.EventStreamRoundIterator; import com.swirlds.platform.recovery.internal.StreamedRound; -import com.swirlds.platform.system.BasicSoftwareVersion; -import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.system.events.CesEvent; import java.io.IOException; import java.nio.file.Files; @@ -50,7 +48,6 @@ import java.util.List; import java.util.NoSuchElementException; import java.util.Random; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; @@ -62,12 +59,6 @@ class EventStreamRoundIteratorTest { @BeforeAll static void beforeAll() throws ConstructableRegistryException { ConstructableRegistry.getInstance().registerConstructables("com.swirlds"); - StaticSoftwareVersion.setSoftwareVersion(new BasicSoftwareVersion(1)); - } - - @AfterAll - static void afterAll() { - StaticSoftwareVersion.reset(); } public static void assertEventsAreEqual(final CesEvent expected, final CesEvent actual) { diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamSingleFileIteratorTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamSingleFileIteratorTest.java index 31a4dd7ea11c..a41f4f918341 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamSingleFileIteratorTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamSingleFileIteratorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2024 Hedera Hashgraph, LLC + * Copyright (C) 2022-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. @@ -34,8 +34,6 @@ import com.swirlds.common.io.utility.FileUtils; import com.swirlds.common.io.utility.LegacyTemporaryFileBuilder; import com.swirlds.platform.recovery.internal.EventStreamSingleFileIterator; -import com.swirlds.platform.system.BasicSoftwareVersion; -import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.system.events.CesEvent; import java.io.IOException; import java.nio.file.Path; @@ -44,7 +42,6 @@ import java.util.List; import java.util.NoSuchElementException; import java.util.Random; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; @@ -56,12 +53,6 @@ class EventStreamSingleFileIteratorTest { @BeforeAll static void beforeAll() throws ConstructableRegistryException { ConstructableRegistry.getInstance().registerConstructables("com.swirlds"); - StaticSoftwareVersion.setSoftwareVersion(new BasicSoftwareVersion(1)); - } - - @AfterAll - static void afterAll() { - StaticSoftwareVersion.reset(); } public static void assertEventsAreEqual(final CesEvent expected, final CesEvent actual) { diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/ObjectStreamIteratorTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/ObjectStreamIteratorTest.java index be637b2cb894..1ab208802838 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/ObjectStreamIteratorTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/ObjectStreamIteratorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2024 Hedera Hashgraph, LLC + * Copyright (C) 2022-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. @@ -37,8 +37,6 @@ import com.swirlds.common.io.utility.FileUtils; import com.swirlds.common.io.utility.LegacyTemporaryFileBuilder; import com.swirlds.platform.recovery.internal.ObjectStreamIterator; -import com.swirlds.platform.system.BasicSoftwareVersion; -import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.system.events.CesEvent; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -48,7 +46,6 @@ import java.util.List; import java.util.NoSuchElementException; import java.util.Random; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -59,12 +56,6 @@ class ObjectStreamIteratorTest { @BeforeAll static void beforeAll() throws ConstructableRegistryException { ConstructableRegistry.getInstance().registerConstructables("com.swirlds"); - StaticSoftwareVersion.setSoftwareVersion(new BasicSoftwareVersion(1)); - } - - @AfterAll - static void afterAll() { - StaticSoftwareVersion.reset(); } public static void assertEventsAreEqual(final CesEvent expected, final CesEvent actual) { diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/turtle/gossip/SimulatedGossipTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/turtle/gossip/SimulatedGossipTests.java index 060f3a5623a4..805b957a31fa 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/turtle/gossip/SimulatedGossipTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/turtle/gossip/SimulatedGossipTests.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. @@ -21,8 +21,6 @@ import static org.mockito.Mockito.mock; import com.swirlds.base.test.fixtures.time.FakeTime; -import com.swirlds.common.constructable.ConstructableRegistry; -import com.swirlds.common.constructable.ConstructableRegistryException; import com.swirlds.common.context.PlatformContext; import com.swirlds.common.platform.NodeId; import com.swirlds.common.test.fixtures.Randotron; @@ -35,8 +33,6 @@ import com.swirlds.common.wiring.wires.output.StandardOutputWire; import com.swirlds.platform.event.PlatformEvent; import com.swirlds.platform.event.hashing.DefaultEventHasher; -import com.swirlds.platform.system.BasicSoftwareVersion; -import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.system.address.AddressBook; import com.swirlds.platform.system.events.EventDescriptorWrapper; import com.swirlds.platform.test.fixtures.addressbook.RandomAddressBookBuilder; @@ -51,7 +47,6 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -71,12 +66,6 @@ private static Set getUniqueDescriptors(@NonNull final L return uniqueDescriptors; } - @BeforeAll - static void beforeAll() throws ConstructableRegistryException { - StaticSoftwareVersion.setSoftwareVersion(new BasicSoftwareVersion(1)); - ConstructableRegistry.getInstance().registerConstructables(""); - } - @SuppressWarnings("unchecked") @ParameterizedTest @ValueSource(ints = {1, 2, 4, 8, 16, 32}) diff --git a/platform-sdk/swirlds-platform-core/src/test/resources/eventFiles/testnet-57/2025-01-10T00_00_00.016538241Z.events b/platform-sdk/swirlds-platform-core/src/test/resources/eventFiles/testnet-57/2025-01-10T00_00_00.016538241Z.events new file mode 100644 index 0000000000000000000000000000000000000000..c9363cde277382c65470c37f53eac0aa7060f7b3 GIT binary patch literal 371587 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

ppd6~5i5Aez-8@g^ur2V2i( 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

$V)`cYJo*nQKP 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 buildArguments() { return Stream.of(Arguments.of(GENERATION_THRESHOLD), Arguments.of(BIRTH_ROUND_THRESHOLD)); } - @BeforeAll - static void beforeAll() throws ConstructableRegistryException { - ConstructableRegistry.getInstance().registerConstructables(""); - StaticSoftwareVersion.setSoftwareVersion(new BasicSoftwareVersion(1)); - } - - @AfterAll - static void afterAll() { - StaticSoftwareVersion.reset(); - } - @BeforeEach void beforeEach() throws IOException { FileUtils.deleteDirectory(testDirectory); diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/sync/SyncTests.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/sync/SyncTests.java index dae03d566b52..f01e80662bfc 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/sync/SyncTests.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/sync/SyncTests.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2024 Hedera Hashgraph, LLC + * Copyright (C) 2021-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. @@ -41,8 +41,6 @@ import com.swirlds.platform.consensus.EventWindow; import com.swirlds.platform.event.AncientMode; import com.swirlds.platform.gossip.shadowgraph.ShadowEvent; -import com.swirlds.platform.system.BasicSoftwareVersion; -import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.system.events.EventConstants; import com.swirlds.platform.test.event.emitter.EventEmitterFactory; import com.swirlds.platform.test.event.emitter.StandardEventEmitter; @@ -61,7 +59,6 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; @@ -77,16 +74,6 @@ private static Stream bothAncientModes() { return Stream.of(Arguments.of(GENERATION_THRESHOLD), Arguments.of(BIRTH_ROUND_THRESHOLD)); } - @BeforeAll - static void beforeAll() { - StaticSoftwareVersion.setSoftwareVersion(new BasicSoftwareVersion(1)); - } - - @AfterAll - static void afterAll() { - StaticSoftwareVersion.reset(); - } - private static Stream fourNodeGraphParams() { return Stream.of( Arguments.of(new SyncTestParams(4, 100, 20, 0, GENERATION_THRESHOLD)), From a0b12b850fe3fc57aa522756b94c95f5fed9c1fa Mon Sep 17 00:00:00 2001 From: Jendrik Johannes Date: Tue, 14 Jan 2025 17:35:34 +0100 Subject: [PATCH 11/19] build: update hiero-gradle-conventions to 0.3.0 (#17364) Signed-off-by: Jendrik Johannes --- .../node-zxc-build-release-artifact.yaml | 46 ++++---- .../node-zxc-compile-application-code.yaml | 73 ++++++------- .github/workflows/node-zxf-snyk-monitor.yaml | 2 +- .github/workflows/zxc-jrs-regression.yaml | 54 +-------- .../zxc-verify-docker-build-determinism.yaml | 103 +----------------- .../zxc-verify-gradle-build-determinism.yaml | 4 +- hiero-dependency-versions/build.gradle.kts | 2 +- settings.gradle.kts | 2 +- 8 files changed, 70 insertions(+), 216 deletions(-) diff --git a/.github/workflows/node-zxc-build-release-artifact.yaml b/.github/workflows/node-zxc-build-release-artifact.yaml index 918cddbc379a..eff6f2eda921 100644 --- a/.github/workflows/node-zxc-build-release-artifact.yaml +++ b/.github/workflows/node-zxc-build-release-artifact.yaml @@ -191,11 +191,11 @@ jobs: - name: Gradle Update Version (As Specified) if: ${{ inputs.version-policy == 'specified' && !cancelled() && !failure() }} - run: ./gradlew versionAsSpecified -PnewVersion=${{ inputs.new-version }} --scan + run: ./gradlew versionAsSpecified -PnewVersion=${{ inputs.new-version }} - name: Gradle Update Version (Branch Commit) if: ${{ inputs.version-policy != 'specified' && !cancelled() && !failure() }} - run: ./gradlew versionAsPrefixedCommit -PcommitPrefix=${{ steps.parameters.outputs.commit-prefix }} --scan + run: ./gradlew versionAsPrefixedCommit -PcommitPrefix=${{ steps.parameters.outputs.commit-prefix }} - name: Compute Final Effective Version id: effective-version @@ -309,10 +309,10 @@ jobs: - name: Gradle Assemble id: gradle-build - run: ./gradlew assemble --scan + run: ./gradlew assemble - name: Gradle Version Summary - run: ./gradlew githubVersionSummary --scan + run: ./gradlew githubVersionSummary - name: Stage Artifact Build Folder id: artifact-staging @@ -396,25 +396,25 @@ jobs: uses: google-github-actions/auth@6fc4af4b145ae7821d527454aa9bd537d1f2dc5f # v2.1.7 if: ${{ inputs.dry-run-enabled != true && !cancelled() && !failure() }} with: - token_format: 'access_token' + token_format: "access_token" workload_identity_provider: "projects/235822363393/locations/global/workloadIdentityPools/hedera-builds-pool/providers/hedera-builds-gh-actions" service_account: "swirlds-automation@hedera-registry.iam.gserviceaccount.com" - name: Set Image Registry id: set-registry run: | - DOCKER_REGISTRY="gcr.io" - [[ "${{ inputs.version-policy }}" == "branch-commit" ]] && DOCKER_REGISTRY="us-docker.pkg.dev" - echo "docker-registry=${DOCKER_REGISTRY}" >>"${GITHUB_OUTPUT}" - - DOCKER_TAG_BASE="gcr.io/hedera-registry" - if [[ "${{ inputs.version-policy }}" == "branch-commit" && "${{ inputs.dry-run-enabled }}" != true ]]; then - DOCKER_TAG_BASE="us-docker.pkg.dev/swirlds-registry/local-node" - elif [[ "${{ inputs.dry-run-enabled }}" == true ]]; then - DOCKER_TAG_BASE="localhost:5000" - fi + DOCKER_REGISTRY="gcr.io" + [[ "${{ inputs.version-policy }}" == "branch-commit" ]] && DOCKER_REGISTRY="us-docker.pkg.dev" + echo "docker-registry=${DOCKER_REGISTRY}" >>"${GITHUB_OUTPUT}" + + DOCKER_TAG_BASE="gcr.io/hedera-registry" + if [[ "${{ inputs.version-policy }}" == "branch-commit" && "${{ inputs.dry-run-enabled }}" != true ]]; then + DOCKER_TAG_BASE="us-docker.pkg.dev/swirlds-registry/local-node" + elif [[ "${{ inputs.dry-run-enabled }}" == true ]]; then + DOCKER_TAG_BASE="localhost:5000" + fi - echo "docker-tag-base=${DOCKER_TAG_BASE}" >>"${GITHUB_OUTPUT}" + echo "docker-tag-base=${DOCKER_TAG_BASE}" >>"${GITHUB_OUTPUT}" - name: Setup QEmu Support uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3.3.0 @@ -474,8 +474,8 @@ jobs: push: true platforms: linux/amd64,linux/arm64 build-args: | - IMAGE_TAG=${{ needs.validate.outputs.version }} - IMAGE_PREFIX=${{ steps.set-registry.outputs.docker-tag-base }}/ + IMAGE_TAG=${{ needs.validate.outputs.version }} + IMAGE_PREFIX=${{ steps.set-registry.outputs.docker-tag-base }}/ context: hedera-node/infrastructure/docker/containers/local-node/main-network-node tags: ${{ steps.set-registry.outputs.docker-tag-base }}/main-network-node:${{ needs.validate.outputs.version }} @@ -570,7 +570,7 @@ jobs: id: google-auth uses: google-github-actions/auth@6fc4af4b145ae7821d527454aa9bd537d1f2dc5f # v2.1.7 with: - token_format: 'access_token' + token_format: "access_token" workload_identity_provider: "projects/235822363393/locations/global/workloadIdentityPools/hedera-builds-pool/providers/hedera-builds-gh-actions" service_account: "swirlds-automation@hedera-registry.iam.gserviceaccount.com" @@ -732,10 +732,10 @@ jobs: key: node-build-version-${{ needs.validate.outputs.version }}-${{ github.sha }} - name: Gradle Assemble - run: ./gradlew assemble --scan + run: ./gradlew assemble - name: Gradle Version Summary - run: ./gradlew githubVersionSummary --scan + run: ./gradlew githubVersionSummary - name: Stage SDK Release Archives working-directory: platform-sdk @@ -814,14 +814,14 @@ jobs: env: NEXUS_USERNAME: ${{ secrets.sdk-ossrh-username }} NEXUS_PASSWORD: ${{ secrets.sdk-ossrh-password }} - run: ./gradlew release${{ inputs.release-profile }} -PpublishingPackageGroup=com.swirlds -Ps01SonatypeHost=true -PpublishSigningEnabled=true --scan --no-configuration-cache + run: ./gradlew release${{ inputs.release-profile }} -PpublishingPackageGroup=com.swirlds -Ps01SonatypeHost=true -PpublishSigningEnabled=true --no-configuration-cache - name: Gradle Publish Services to ${{ inputs.version-policy == 'specified' && 'Maven Central' || 'Google Artifact Registry' }} (${{ inputs.release-profile }}) if: ${{ inputs.dry-run-enabled != true && inputs.release-profile != 'none' && !cancelled() && !failure() }} env: NEXUS_USERNAME: ${{ secrets.svcs-ossrh-username }} NEXUS_PASSWORD: ${{ secrets.svcs-ossrh-password }} - run: ./gradlew release${{ inputs.release-profile }} -PpublishingPackageGroup=com.hedera.hashgraph -PpublishSigningEnabled=true --scan --no-configuration-cache + run: ./gradlew release${{ inputs.release-profile }} -PpublishingPackageGroup=com.hedera.hashgraph -PpublishSigningEnabled=true --no-configuration-cache - name: Upload SDK Release Archives if: ${{ inputs.dry-run-enabled != true && inputs.version-policy == 'specified' && !cancelled() && !failure() }} diff --git a/.github/workflows/node-zxc-compile-application-code.yaml b/.github/workflows/node-zxc-compile-application-code.yaml index d13c96792236..e5b17c00e45a 100644 --- a/.github/workflows/node-zxc-compile-application-code.yaml +++ b/.github/workflows/node-zxc-compile-application-code.yaml @@ -193,28 +193,27 @@ jobs: - name: Compile id: gradle-build - run: ${GRADLE_EXEC} assemble :test-clients:yahCliJar --scan + run: ${GRADLE_EXEC} assemble :test-clients:yahCliJar - name: Spotless Check if: ${{ inputs.enable-spotless-check && !cancelled() }} - run: ${GRADLE_EXEC} spotlessCheck --scan + run: ${GRADLE_EXEC} spotlessCheck - name: Gradle Dependency Scopes Check if: ${{ inputs.enable-dependency-check && steps.gradle-build.conclusion == 'success' && !cancelled() }} - run: ${GRADLE_EXEC} checkAllModuleInfo validatePomFiles --scan --continue + run: ${GRADLE_EXEC} checkAllModuleInfo validatePomFiles --continue - name: Unit Testing id: gradle-test if: ${{ inputs.enable-unit-tests && steps.gradle-build.conclusion == 'success' && !cancelled() }} - run: ${GRADLE_EXEC} :aggregation:testCodeCoverageReport --continue --scan - + run: ${GRADLE_EXEC} :aggregation:testCodeCoverageReport --continue - name: Publish Unit Test Report uses: step-security/publish-unit-test-result-action@cc82caac074385ae176d39d2d143ad05e1130b2d # v2.18.0 if: ${{ inputs.enable-unit-tests && steps.gradle-build.conclusion == 'success' && !cancelled() }} with: - check_name: 'Node: Unit Test Results' - json_thousands_separator: ',' + check_name: "Node: Unit Test Results" + json_thousands_separator: "," junit_files: "**/build/test-results/test/TEST-*.xml" comment_mode: errors # only comment if we could not find or parse the JUnit XML files @@ -229,14 +228,14 @@ jobs: - name: Timing Sensitive Tests id: gradle-timing-sensitive if: ${{ inputs.enable-timing-sensitive-tests && steps.gradle-build.conclusion == 'success' && !cancelled() }} - run: ${GRADLE_EXEC} timingSensitive --continue --scan + run: ${GRADLE_EXEC} timingSensitive --continue - name: Publish Unit Test (Timing Sensitive) Report uses: step-security/publish-unit-test-result-action@cc82caac074385ae176d39d2d143ad05e1130b2d # v2.18.0 if: ${{ inputs.enable-timing-sensitive-tests && steps.gradle-build.conclusion == 'success' && !cancelled() }} with: - check_name: 'Node: Timing Sensitive Unit Test Results' - json_thousands_separator: ',' + check_name: "Node: Timing Sensitive Unit Test Results" + json_thousands_separator: "," junit_files: "**/build/test-results/timingSensitive/TEST-*.xml" comment_mode: errors # only comment if we could not find or parse the JUnit XML files @@ -251,14 +250,14 @@ jobs: - name: Time Consuming Tests id: gradle-time-consuming if: ${{ inputs.enable-time-consuming-tests && steps.gradle-build.conclusion == 'success' && !cancelled() }} - run: ${GRADLE_EXEC} timeConsuming --continue --scan + run: ${GRADLE_EXEC} timeConsuming --continue - name: Publish Unit Test (Time Consuming) Report uses: step-security/publish-unit-test-result-action@cc82caac074385ae176d39d2d143ad05e1130b2d # v2.18.0 if: ${{ inputs.enable-time-consuming-tests && steps.gradle-build.conclusion == 'success' && !cancelled() }} with: - check_name: 'Node: Time Consuming Unit Test Results' - json_thousands_separator: ',' + check_name: "Node: Time Consuming Unit Test Results" + json_thousands_separator: "," junit_files: "**/build/test-results/timeConsuming/TEST-*.xml" comment_mode: errors # only comment if we could not find or parse the JUnit XML files @@ -273,14 +272,14 @@ jobs: - name: Hammer Tests id: gradle-hammer-tests if: ${{ inputs.enable-hammer-tests && steps.gradle-build.conclusion == 'success' && !cancelled() }} - run: ${GRADLE_EXEC} hammer --continue --scan + run: ${GRADLE_EXEC} hammer --continue - name: Publish Hammer Test Report uses: step-security/publish-unit-test-result-action@cc82caac074385ae176d39d2d143ad05e1130b2d # v2.18.0 if: ${{ inputs.enable-hammer-tests && steps.gradle-build.conclusion == 'success' && !cancelled() }} with: - check_name: 'Node: Hammer Test Results' - json_thousands_separator: ',' + check_name: "Node: Hammer Test Results" + json_thousands_separator: "," junit_files: "**/build/test-results/hammer/TEST-*.xml" comment_mode: errors # only comment if we could not find or parse the JUnit XML files @@ -299,14 +298,14 @@ jobs: LC_ALL: en.UTF-8 LANG: en_US.UTF-8 # Run each tasks in isolation because we dynamically reconfigure Log4j for each mode - run: ${GRADLE_EXEC} hapiTestMisc --scan --no-daemon && ${GRADLE_EXEC} hapiEmbeddedMisc --scan --no-daemon && ${GRADLE_EXEC} hapiRepeatableMisc --scan --no-daemon + run: ${GRADLE_EXEC} hapiTestMisc --no-daemon && ${GRADLE_EXEC} hapiEmbeddedMisc --no-daemon && ${GRADLE_EXEC} hapiRepeatableMisc --no-daemon - name: Publish HAPI Test (Misc) Report uses: step-security/publish-unit-test-result-action@cc82caac074385ae176d39d2d143ad05e1130b2d # v2.18.0 if: ${{ inputs.enable-hapi-tests-misc && steps.gradle-build.conclusion == 'success' && !cancelled() }} with: - check_name: 'Node: HAPI Test (Misc) Results' - json_thousands_separator: ',' + check_name: "Node: HAPI Test (Misc) Results" + json_thousands_separator: "," junit_files: "**/test-clients/build/test-results/**/TEST-*.xml" comment_mode: errors # only comment if we could not find or parse the JUnit XML files @@ -333,14 +332,14 @@ jobs: env: LC_ALL: en.UTF-8 LANG: en_US.UTF-8 - run: ${GRADLE_EXEC} hapiTestCrypto --scan + run: ${GRADLE_EXEC} hapiTestCrypto - name: Publish HAPI Test (Crypto) Report uses: step-security/publish-unit-test-result-action@cc82caac074385ae176d39d2d143ad05e1130b2d # v2.18.0 if: ${{ inputs.enable-hapi-tests-crypto && steps.gradle-build.conclusion == 'success' && !cancelled() }} with: - check_name: 'Node: HAPI Test (Crypto) Results' - json_thousands_separator: ',' + check_name: "Node: HAPI Test (Crypto) Results" + json_thousands_separator: "," junit_files: "**/test-clients/build/test-results/testSubprocess/TEST-*.xml" comment_mode: errors # only comment if we could not find or parse the JUnit XML files @@ -367,14 +366,14 @@ jobs: env: LC_ALL: en.UTF-8 LANG: en_US.UTF-8 - run: ${GRADLE_EXEC} hapiTestToken --scan + run: ${GRADLE_EXEC} hapiTestToken - name: Publish HAPI Test (Token) Report uses: step-security/publish-unit-test-result-action@cc82caac074385ae176d39d2d143ad05e1130b2d # v2.18.0 if: ${{ inputs.enable-hapi-tests-token && steps.gradle-build.conclusion == 'success' && !cancelled() }} with: - check_name: 'Node: HAPI Test (Token) Results' - json_thousands_separator: ',' + check_name: "Node: HAPI Test (Token) Results" + json_thousands_separator: "," junit_files: "**/test-clients/build/test-results/testSubprocess/TEST-*.xml" comment_mode: errors # only comment if we could not find or parse the JUnit XML files @@ -401,14 +400,14 @@ jobs: env: LC_ALL: en.UTF-8 LANG: en_US.UTF-8 - run: ${GRADLE_EXEC} hapiTestSmartContract --scan + run: ${GRADLE_EXEC} hapiTestSmartContract - name: Publish HAPI Test (Smart Contract) Report uses: step-security/publish-unit-test-result-action@cc82caac074385ae176d39d2d143ad05e1130b2d # v2.18.0 if: ${{ inputs.enable-hapi-tests-smart-contract && steps.gradle-build.conclusion == 'success' && !cancelled() }} with: - check_name: 'Node: HAPI Test (Smart Contract) Results' - json_thousands_separator: ',' + check_name: "Node: HAPI Test (Smart Contract) Results" + json_thousands_separator: "," junit_files: "**/test-clients/build/test-results/testSubprocess/TEST-*.xml" comment_mode: errors # only comment if we could not find or parse the JUnit XML files @@ -435,14 +434,14 @@ jobs: env: LC_ALL: en.UTF-8 LANG: en_US.UTF-8 - run: ${GRADLE_EXEC} hapiTestTimeConsuming --scan + run: ${GRADLE_EXEC} hapiTestTimeConsuming - name: Publish HAPI Test (Time Consuming) Report uses: step-security/publish-unit-test-result-action@cc82caac074385ae176d39d2d143ad05e1130b2d # v2.18.0 if: ${{ inputs.enable-hapi-tests-time-consuming && steps.gradle-build.conclusion == 'success' && !cancelled() }} with: - check_name: 'Node: HAPI Test (Time Consuming) Results' - json_thousands_separator: ',' + check_name: "Node: HAPI Test (Time Consuming) Results" + json_thousands_separator: "," junit_files: "**/test-clients/build/test-results/testSubprocess/TEST-*.xml" comment_mode: errors # only comment if we could not find or parse the JUnit XML files @@ -469,14 +468,14 @@ jobs: env: LC_ALL: en.UTF-8 LANG: en_US.UTF-8 - run: ${GRADLE_EXEC} hapiTestRestart --scan + run: ${GRADLE_EXEC} hapiTestRestart - name: Publish HAPI Test (Restart) Report uses: step-security/publish-unit-test-result-action@cc82caac074385ae176d39d2d143ad05e1130b2d # v2.18.0 if: ${{ inputs.enable-hapi-tests-restart && steps.gradle-build.conclusion == 'success' && !cancelled() }} with: - check_name: 'Node: HAPI Test (Restart) Results' - json_thousands_separator: ',' + check_name: "Node: HAPI Test (Restart) Results" + json_thousands_separator: "," junit_files: "**/test-clients/build/test-results/testSubprocess/TEST-*.xml" comment_mode: errors # only comment if we could not find or parse the JUnit XML files @@ -504,14 +503,14 @@ jobs: env: LC_ALL: en.UTF-8 LANG: en_US.UTF-8 - run: ${GRADLE_EXEC} hapiTestNDReconnect --scan + run: ${GRADLE_EXEC} hapiTestNDReconnect - name: Publish HAPI Test (Node Death Reconnect) Report uses: step-security/publish-unit-test-result-action@cc82caac074385ae176d39d2d143ad05e1130b2d # v2.18.0 if: ${{ inputs.enable-hapi-tests-nd-reconnect && steps.gradle-build.conclusion == 'success' && !cancelled() }} with: - check_name: 'Node: HAPI Test (Node Death Reconnect) Results' - json_thousands_separator: ',' + check_name: "Node: HAPI Test (Node Death Reconnect) Results" + json_thousands_separator: "," junit_files: "**/test-clients/build/test-results/testSubprocess/TEST-*.xml" comment_mode: errors # only comment if we could not find or parse the JUnit XML files diff --git a/.github/workflows/node-zxf-snyk-monitor.yaml b/.github/workflows/node-zxf-snyk-monitor.yaml index e341a60e6064..87da00e8fb8b 100644 --- a/.github/workflows/node-zxf-snyk-monitor.yaml +++ b/.github/workflows/node-zxf-snyk-monitor.yaml @@ -52,7 +52,7 @@ jobs: gradle-version: wrapper - name: Compile - run: ./gradlew assemble --scan + run: ./gradlew assemble - name: Disable Gradle Configuration Cache run: sed -i 's/^org.gradle.configuration-cache=.*$/org.gradle.configuration-cache=false/' gradle.properties diff --git a/.github/workflows/zxc-jrs-regression.yaml b/.github/workflows/zxc-jrs-regression.yaml index 8acb02276bb1..80e8d70de0a8 100644 --- a/.github/workflows/zxc-jrs-regression.yaml +++ b/.github/workflows/zxc-jrs-regression.yaml @@ -160,7 +160,6 @@ on: description: "The Slack API access token used to run unit and JRS tests." required: true - env: MAVEN_OPTS: -Xmx16g -XX:ActiveProcessorCount=16 JAVA_OPTS: -Xmx28g -XX:ActiveProcessorCount=16 @@ -245,49 +244,6 @@ jobs: echo "file=${FINAL_PATH}" >> "${GITHUB_OUTPUT}" -# - name: Setup Control Groups -# run: | -# echo "::group::Get System Configuration" -# USR_ID="$(id -un)" -# GRP_ID="$(id -gn)" -# GRADLE_MEM_LIMIT="30064771072" -# PING_MEM_LIMIT="1073741824" -# AGENT_MEM_LIMIT="2147483648" -# GRADLE_GROUP_NAME="gradle-${{ github.run_id }}" -# PING_GROUP_NAME="ping-${{ github.run_id }}" -# AGENT_GROUP_NAME="agent-${{ github.run_id }}" -# echo "::endgroup::" -# -# echo "::group::Install Control Group Tools" -# if ! command -v cgcreate >/dev/null 2>&1; then -# sudo apt-get update -# sudo apt-get install -y cgroup-tools -# fi -# echo "::endgroup::" -# -# echo "::group::Create Control Groups" -# sudo cgcreate -g cpu,memory:${GRADLE_GROUP_NAME} -a ${USR_ID}:${GRP_ID} -t ${USR_ID}:${GRP_ID} -# sudo cgcreate -g cpu,memory:${PING_GROUP_NAME} -a ${USR_ID}:${GRP_ID} -t ${USR_ID}:${GRP_ID} -# sudo cgcreate -g cpu,memory:${AGENT_GROUP_NAME} -a ${USR_ID}:${GRP_ID} -t ${USR_ID}:${GRP_ID} -# echo "::endgroup::" -# -# echo "::group::Set Control Group Limits" -# cgset -r cpu.shares=768 ${GRADLE_GROUP_NAME} -# cgset -r cpu.shares=768 ${PING_GROUP_NAME} -# cgset -r cpu.shares=500 ${AGENT_GROUP_NAME} -# cgset -r memory.limit_in_bytes=${GRADLE_MEM_LIMIT} ${GRADLE_GROUP_NAME} -# cgset -r memory.limit_in_bytes=${PING_MEM_LIMIT} ${PING_GROUP_NAME} -# cgset -r memory.limit_in_bytes=${AGENT_MEM_LIMIT} ${AGENT_GROUP_NAME} -# cgset -r memory.memsw.limit_in_bytes=${GRADLE_MEM_LIMIT} ${GRADLE_GROUP_NAME} -# cgset -r memory.memsw.limit_in_bytes=${PING_MEM_LIMIT} ${PING_GROUP_NAME} -# cgset -r memory.memsw.limit_in_bytes=${AGENT_MEM_LIMIT} ${AGENT_GROUP_NAME} -# echo "::endgroup::" -# -# echo "::group::Move Runner Processes to Control Groups" -# sudo cgclassify --sticky -g cpu,memory:${AGENT_GROUP_NAME} $(pgrep 'Runner.Listener' | tr '\n' ' ') -# sudo cgclassify -g cpu,memory:${AGENT_GROUP_NAME} $(pgrep 'Runner.Worker' | tr '\n' ' ') -# echo "::endgroup::" - - name: Install Grafana Agent run: | sudo mkdir -p /usr/local/grafana-agent @@ -387,13 +343,13 @@ jobs: id: google-auth uses: google-github-actions/auth@6fc4af4b145ae7821d527454aa9bd537d1f2dc5f # v2.1.7 with: - workload_identity_provider: 'projects/785813846068/locations/global/workloadIdentityPools/jrs-identity-pool/providers/gh-provider' - service_account: 'swirlds-automation@swirlds-regression.iam.gserviceaccount.com' + workload_identity_provider: "projects/785813846068/locations/global/workloadIdentityPools/jrs-identity-pool/providers/gh-provider" + service_account: "swirlds-automation@swirlds-regression.iam.gserviceaccount.com" - name: Setup Google Cloud SDK uses: google-github-actions/setup-gcloud@6189d56e4096ee891640bb02ac264be376592d6a # v2.1.2 with: - install_components: 'alpha' + install_components: "alpha" - name: Install RClone run: | @@ -414,12 +370,12 @@ jobs: - name: Gradle Assemble id: gradle-build working-directory: ${{ github.workspace }} - run: ./gradlew assemble :test-clients:shadowJar --scan + run: ./gradlew assemble :test-clients:shadowJar - name: Regression Gradle Assemble id: regression-gradle-build working-directory: ${{ github.workspace }}/platform-sdk/regression - run: ./gradlew assemble --scan + run: ./gradlew assemble - name: Compute Actual Branch Name id: branch diff --git a/.github/workflows/zxc-verify-docker-build-determinism.yaml b/.github/workflows/zxc-verify-docker-build-determinism.yaml index 7bfbae7ecc14..1c2992888bec 100644 --- a/.github/workflows/zxc-verify-docker-build-determinism.yaml +++ b/.github/workflows/zxc-verify-docker-build-determinism.yaml @@ -136,62 +136,6 @@ jobs: sudo apt-get update sudo apt-get install --yes --no-install-recommends skopeo jq -# - name: Install KillAll -# if: ${{ steps.baseline.outputs.exists == 'false' && !failure() && !cancelled() }} -# run: sudo apt-get install --yes --no-install-recommends psmisc - -# - name: Create Docker Working Directory -# if: ${{ steps.baseline.outputs.exists == 'false' && !failure() && !cancelled() }} -# run: | -# USER="$(id -un)" -# GROUP="$(id -gn)" -# sudo mkdir -p /x -# sudo chown -vR ${USER}:${GROUP} /x -# sudo ls -lah /x - -# - name: Remove Docker from Self Hosted Runners -# if: ${{ steps.baseline.outputs.exists == 'false' && !failure() && !cancelled() }} -# run: | -# set -x -# sudo killall dockerd || true -# sudo killall containerd || true -# sudo rm -rvf /usr/bin/*containerd* || true -# sudo rm -rvf /usr/bin/docker* || true -# sudo rm -rvf /usr/local/bin/docker* || true -# sudo rm -rvf /usr/local/bin/*lima* || true - -# - name: Setup Containerd Support -# uses: crazy-max/ghaction-setup-containerd@60acbf31e6572da7b83a4ed6b428ed92a35ff4d7 # v3.0.0 -# if: ${{ steps.baseline.outputs.exists == 'false' && !failure() && !cancelled() }} -# with: -# containerd-version: v1.7.2 - -# - name: Setup Docker Support -# uses: step-security/ghaction-setup-docker@42e219a378b907a83f1b323a1458fbf352af3ffd # v3.3.0 -# if: ${{ steps.baseline.outputs.exists == 'false' && !failure() && !cancelled() }} -# env: -# HOME: /x -# with: -# version: v25.0.5 -# daemon-config: | -# { -# "registry-mirrors": [ -# "https://hub.mirror.docker.lat.ope.eng.hashgraph.io" -# ] -# } - -# - name: Configure Default Docker Context -# if: ${{ steps.baseline.outputs.exists == 'false' && !failure() && !cancelled() }} -# run: | -# set -x -# if grep setup-docker-action < <(docker context ls --format '{{ .Name }}') >/dev/null; then -# docker context rm -f setup-docker-action -# fi -# -# DOCKER_CONTEXT_PATH="$(sudo find /x -name docker.sock | tr -d '[:space:]')" -# docker context create setup-docker-action --docker "host=unix://${DOCKER_CONTEXT_PATH}" -# docker context use setup-docker-action - - name: Setup QEmu Support uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3.3.0 if: ${{ steps.baseline.outputs.exists == 'false' && !failure() && !cancelled() }} @@ -220,7 +164,7 @@ jobs: - name: Build Gradle Artifacts if: ${{ steps.baseline.outputs.exists == 'false' && !failure() && !cancelled() }} - run: ./gradlew assemble --scan + run: ./gradlew assemble - name: Prepare for Docker Build if: ${{ steps.baseline.outputs.exists == 'false' && !failure() && !cancelled() }} @@ -326,10 +270,6 @@ jobs: sudo apt-get install --yes --no-install-recommends skopeo fi -# - name: Install KillAll (Linux) -# if: ${{ runner.os == 'Linux' }} -# run: sudo apt-get install --yes --no-install-recommends psmisc - - name: Install Skopeo and JQ (macOS) if: ${{ runner.os == 'macOS' }} run: brew install skopeo jq @@ -360,15 +300,6 @@ jobs: tar -xzf "${{ needs.generate-baseline.outputs.name }}" mv "sdk" "${{ github.workspace }}/${{ env.DOCKER_CONTEXT_PATH }}/" -# - name: Create Docker Working Directory -# if: ${{ runner.os == 'Linux' }} -# run: | -# USER="$(id -un)" -# GROUP="$(id -gn)" -# sudo mkdir -p /x -# sudo chown -vR ${USER}:${GROUP} /x -# sudo ls -lah /x - - name: Remove Preinstalled Docker if: ${{ runner.os == 'macOS' }} run: | @@ -390,38 +321,6 @@ jobs: id: home run: echo "directory=$(tr -d '[:space:]' < <(cd ~ && pwd))" >> "${GITHUB_OUTPUT}" -# - name: Setup Containerd Support -# uses: crazy-max/ghaction-setup-containerd@60acbf31e6572da7b83a4ed6b428ed92a35ff4d7 # v3.0.0 -# if: ${{ runner.os == 'Linux' }} -# with: -# containerd-version: v1.7.2 -# -# - name: Setup Docker Support -# uses: step-security/ghaction-setup-docker@42e219a378b907a83f1b323a1458fbf352af3ffd # v3.3.0 -# env: -# HOME: ${{ runner.os == 'Linux' && '/x' || steps.home.outputs.directory }} -# with: -# version: v25.0.5 -# daemon-config: | -# { -# "registry-mirrors": [ -# "https://hub.mirror.docker.lat.ope.eng.hashgraph.io" -# ] -# } -# -# - name: Configure Default Docker Context -# env: -# SEARCH_PATH: ${{ runner.os == 'Linux' && '/x' || steps.home.outputs.directory }} -# run: | -# set -x -# if grep setup-docker-action < <(docker context ls --format '{{ .Name }}') >/dev/null; then -# docker context rm -f setup-docker-action -# fi -# -# DOCKER_CONTEXT_PATH="$(sudo find "${SEARCH_PATH}" -name docker.sock | tr -d '[:space:]')" -# docker context create setup-docker-action --docker "host=unix://${DOCKER_CONTEXT_PATH}" -# docker context use setup-docker-action - - name: Setup QEmu Support uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3.3.0 diff --git a/.github/workflows/zxc-verify-gradle-build-determinism.yaml b/.github/workflows/zxc-verify-gradle-build-determinism.yaml index 925ff34964e5..e1a9c99f7763 100644 --- a/.github/workflows/zxc-verify-gradle-build-determinism.yaml +++ b/.github/workflows/zxc-verify-gradle-build-determinism.yaml @@ -122,7 +122,7 @@ jobs: - name: Build Artifacts id: gradle-build if: ${{ steps.baseline.outputs.exists == 'false' && !failure() && !cancelled() }} - run: ./gradlew assemble --scan + run: ./gradlew assemble - name: Generate Manifest id: manifest @@ -212,7 +212,7 @@ jobs: - name: Build Artifacts id: gradle-build - run: ./gradlew assemble --scan --no-build-cache + run: ./gradlew assemble --no-build-cache - name: Regenerate Manifest id: regen-manifest diff --git a/hiero-dependency-versions/build.gradle.kts b/hiero-dependency-versions/build.gradle.kts index 554c7a0140bd..0f30a298ef3d 100644 --- a/hiero-dependency-versions/build.gradle.kts +++ b/hiero-dependency-versions/build.gradle.kts @@ -61,7 +61,7 @@ dependencies.constraints { because("com.fasterxml.jackson.databind") } api("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$jackson") { - because("com.fasterxml.jackson.dataformat") + because("com.fasterxml.jackson.dataformat.yaml") } api("com.github.ben-manes.caffeine:caffeine:3.1.8") { because("com.github.benmanes.caffeine") } api("com.github.docker-java:docker-java-api:3.2.13") { because("com.github.dockerjava.api") } diff --git a/settings.gradle.kts b/settings.gradle.kts index 03889906c696..8cc806346b4b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -14,7 +14,7 @@ * limitations under the License. */ -plugins { id("org.hiero.gradle.build") version "0.2.1" } +plugins { id("org.hiero.gradle.build") version "0.3.0" } javaModules { // This "intermediate parent project" should be removed From a39f0c74c3ae0545e940adf942d8c36300e4e4ee Mon Sep 17 00:00:00 2001 From: Michael Tinker Date: Tue, 14 Jan 2025 11:13:35 -0600 Subject: [PATCH 12/19] chore: make config.txt optional at startup (#17330) Signed-off-by: Michael Tinker --- .../impl/schemas/V053AddressBookSchema.java | 5 +- .../schemas/V053AddressBookSchemaTest.java | 5 +- .../main/java/com/hedera/node/app/Hedera.java | 43 ++++- .../com/hedera/node/app/ServicesMain.java | 149 +++++++++++------- .../com/hedera/node/app/ServicesMainTest.java | 21 +-- .../embedded/AbstractEmbeddedHedera.java | 26 ++- .../embedded/ConcurrentEmbeddedHedera.java | 8 +- .../embedded/RepeatableEmbeddedHedera.java | 9 +- .../embedded/fakes/AbstractFakePlatform.java | 9 +- .../embedded/fakes/FakePlatformContext.java | 23 +-- .../block/StateChangesValidator.java | 15 +- .../merkle/crypto/MerkleCryptoFactory.java | 2 +- .../java/com/swirlds/platform/Browser.java | 4 +- .../swirlds/platform/util/BootstrapUtils.java | 52 +++--- 14 files changed, 209 insertions(+), 162 deletions(-) diff --git a/hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/schemas/V053AddressBookSchema.java b/hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/schemas/V053AddressBookSchema.java index 75c5e43dc931..0bd812897429 100644 --- a/hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/schemas/V053AddressBookSchema.java +++ b/hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/schemas/V053AddressBookSchema.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2024 Hedera Hashgraph, LLC + * Copyright (C) 2020-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. @@ -222,8 +222,7 @@ private static Map parseEd25519NodeAdminKeysFrom(@NonNull final Strin try { final var json = Files.readString(path); return parseEd25519NodeAdminKeys(json); - } catch (IOException e) { - log.warn("Unable to read override keys from {}", path.toAbsolutePath(), e); + } catch (IOException ignore) { return emptyMap(); } } diff --git a/hedera-node/hedera-addressbook-service-impl/src/test/java/com/hedera/node/app/service/addressbook/impl/test/schemas/V053AddressBookSchemaTest.java b/hedera-node/hedera-addressbook-service-impl/src/test/java/com/hedera/node/app/service/addressbook/impl/test/schemas/V053AddressBookSchemaTest.java index 7cb776e649ad..41a55cb8566c 100644 --- a/hedera-node/hedera-addressbook-service-impl/src/test/java/com/hedera/node/app/service/addressbook/impl/test/schemas/V053AddressBookSchemaTest.java +++ b/hedera-node/hedera-addressbook-service-impl/src/test/java/com/hedera/node/app/service/addressbook/impl/test/schemas/V053AddressBookSchemaTest.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. @@ -228,8 +228,7 @@ void migrateAsExpected4() { assertThatCode(() -> subject.migrate(migrationContext)).doesNotThrowAnyException(); assertThat(logCaptor.infoLogs()).contains("Started migrating nodes from address book"); - assertThat(logCaptor.warnLogs()).hasSize(2); - assertThat(logCaptor.warnLogs()).matches(logs -> logs.getFirst().contains("Unable to read override keys")); + assertThat(logCaptor.warnLogs()).hasSize(1); assertThat(logCaptor.warnLogs()).matches(logs -> logs.getLast() .contains("Can not parse file 102 com.hedera.pbj.runtime.ParseException: ")); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/Hedera.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/Hedera.java index 57a5ade1bf0e..f6459c837f03 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/Hedera.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/Hedera.java @@ -318,7 +318,7 @@ public final class Hedera implements SwirldMain, PlatformStatusChangeListener, A /** * The metrics object being used for reporting. */ - private Metrics metrics; + private final Metrics metrics; /** * A {@link StateChangeListener} that accumulates state changes that are only reported once per block; in the @@ -386,6 +386,7 @@ public interface BlockHashSignerFactory { * @param migrator the migrator to use with the services * @param startupNetworksFactory the factory for the startup networks * @param blockHashSignerFactory the factory for the block hash signer + * @param metrics the metrics object to use for reporting */ public Hedera( @NonNull final ConstructableRegistry constructableRegistry, @@ -393,9 +394,11 @@ public Hedera( @NonNull final ServiceMigrator migrator, @NonNull final InstantSource instantSource, @NonNull final StartupNetworksFactory startupNetworksFactory, - @NonNull final BlockHashSignerFactory blockHashSignerFactory) { + @NonNull final BlockHashSignerFactory blockHashSignerFactory, + @NonNull final Metrics metrics) { requireNonNull(registryFactory); requireNonNull(constructableRegistry); + this.metrics = requireNonNull(metrics); this.serviceMigrator = requireNonNull(migrator); this.startupNetworksFactory = requireNonNull(startupNetworksFactory); this.blockHashSignerFactory = requireNonNull(blockHashSignerFactory); @@ -431,7 +434,7 @@ public Hedera( this, configSupplier, () -> daggerApp.networkInfo().selfNodeInfo(), - () -> metrics, + () -> this.metrics, new AppThrottleFactory( configSupplier, () -> daggerApp.workingStateAccessor().getState(), @@ -536,17 +539,35 @@ public void notify(@NonNull final PlatformStatusChangeNotification notification) * =================================================================================================================*/ + /** + * Can be collapsed back into {@link #initializeStatesApi(State, InitTrigger, Network, Configuration, AddressBook)} + * once the roster lifecycle is adopted. Needed now to ensure {@link #isRosterLifecycleEnabled()} has an initialized + * {@link ConfigProvider}. + */ + @Deprecated + public void initializeConfigProvider(@NonNull final InitTrigger trigger) { + requireNonNull(trigger); + this.configProvider = new ConfigProviderImpl(trigger == GENESIS, metrics); + } + + /** + * Initializes the States API in the given state based on the given startup conditions. + * + * @param state the state to initialize + * @param trigger the trigger that is calling migration + * @param genesisNetwork the genesis network, if applicable + * @param platformConfig the platform configuration + * @param diskAddressBook the address book from disk, if the roster lifecycle is not enabled + */ public void initializeStatesApi( @NonNull final State state, - @NonNull final Metrics metrics, @NonNull final InitTrigger trigger, @Nullable final Network genesisNetwork, @NonNull final Configuration platformConfig, @Deprecated @Nullable final AddressBook diskAddressBook) { requireNonNull(state); requireNonNull(platformConfig); - this.metrics = requireNonNull(metrics); - this.configProvider = new ConfigProviderImpl(trigger == GENESIS, metrics); + requireNonNull(configProvider); final var deserializedVersion = serviceMigrator.creationVersionOf(state); logger.info( "Initializing Hedera state version {} in {} mode with trigger {} and previous version {}", @@ -599,8 +620,7 @@ public void onStateInitialized( } this.platform = requireNonNull(platform); if (state.getReadableStates(PlatformStateService.NAME).isEmpty()) { - initializeStatesApi( - state, metrics, trigger, null, platform.getContext().getConfiguration(), null); + initializeStatesApi(state, trigger, null, platform.getContext().getConfiguration(), null); } // With the States API grounded in the working state, we can create the object graph from it initializeDagger(state, trigger); @@ -934,6 +954,13 @@ public void setInitialStateHash(@NonNull final Hash stateHash) { initialStateHashFuture = completedFuture(stateHash.getBytes()); } + /** + * Returns the startup networks. + */ + public @NonNull StartupNetworks startupNetworks() { + return requireNonNull(startupNetworks); + } + /*================================================================================================================== * * Exposed for use by embedded Hedera diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/ServicesMain.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/ServicesMain.java index a7cfb6a1090e..8c136aa1c62c 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/ServicesMain.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/ServicesMain.java @@ -29,9 +29,11 @@ import static com.swirlds.platform.builder.internal.StaticPlatformBuilder.setupGlobalMetrics; import static com.swirlds.platform.config.internal.PlatformConfigUtils.checkConfiguration; import static com.swirlds.platform.crypto.CryptoStatic.initNodeSecurity; +import static com.swirlds.platform.roster.RosterUtils.buildAddressBook; import static com.swirlds.platform.roster.RosterUtils.buildRosterHistory; import static com.swirlds.platform.state.signed.StartupStateUtils.copyInitialSignedState; -import static com.swirlds.platform.system.SystemExitCode.CONFIGURATION_ERROR; +import static com.swirlds.platform.system.InitTrigger.GENESIS; +import static com.swirlds.platform.system.InitTrigger.RESTART; import static com.swirlds.platform.system.SystemExitCode.NODE_ADDRESS_MISMATCH; import static com.swirlds.platform.system.SystemExitUtils.exitSystem; import static com.swirlds.platform.util.BootstrapUtils.detectSoftwareUpgrade; @@ -40,12 +42,13 @@ import com.google.common.annotations.VisibleForTesting; import com.hedera.node.app.info.DiskStartupNetworks; +import com.hedera.node.app.roster.RosterService; import com.hedera.node.app.service.addressbook.AddressBookService; import com.hedera.node.app.service.addressbook.impl.ReadableNodeStoreImpl; import com.hedera.node.app.services.OrderedServiceMigrator; import com.hedera.node.app.services.ServicesRegistryImpl; -import com.hedera.node.app.store.ReadableStoreFactory; import com.hedera.node.app.tss.TssBlockHashSigner; +import com.hedera.node.internal.network.Network; import com.swirlds.base.time.Time; import com.swirlds.common.constructable.ConstructableRegistry; import com.swirlds.common.constructable.RuntimeConstructable; @@ -77,7 +80,7 @@ import com.swirlds.platform.state.PlatformMerkleStateRoot; import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.state.address.AddressBookInitializer; -import com.swirlds.platform.state.service.ReadableRosterStore; +import com.swirlds.platform.state.service.ReadableRosterStoreImpl; import com.swirlds.platform.state.signed.HashedReservedSignedState; import com.swirlds.platform.state.signed.SignedState; import com.swirlds.platform.state.signed.StartupStateUtils; @@ -88,13 +91,14 @@ import com.swirlds.platform.system.SwirldState; import com.swirlds.platform.system.address.AddressBook; import com.swirlds.platform.util.BootstrapUtils; -import com.swirlds.state.State; import com.swirlds.state.merkle.MerkleStateRoot; import edu.umd.cs.findbugs.annotations.NonNull; import java.time.InstantSource; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Predicate; import java.util.function.Supplier; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -107,6 +111,18 @@ public class ServicesMain implements SwirldMain { private static final Logger logger = LogManager.getLogger(ServicesMain.class); + /** + * A supplier that refuses to satisfy fallback requests for a set of node ids to run + * simultaneously; this is only useful for certain platform testing applications + */ + private static final Supplier> ILLEGAL_FALLBACK_NODE_IDS = () -> { + throw new IllegalStateException("The node id must be configured explicitly"); + }; + /** + * Upfront validation on node ids is only useful for certain platform testing applications + */ + private static final Predicate NOOP_NODE_VALIDATOR = nodeId -> true; + /** * The {@link Hedera} singleton. */ @@ -217,10 +233,9 @@ public void run() { * @param args optionally, what node id to run; required if the address book is ambiguous */ public static void main(final String... args) throws Exception { + // --- Configure platform infrastructure and derive node id from the command line and environment --- initLogging(); - // --- Configure platform infrastructure and context from the command line and environment --- BootstrapUtils.setupConstructableRegistry(); - final var diskAddressBook = loadAddressBook(DEFAULT_CONFIG_FILE_NAME); final var commandLineArgs = CommandLineArgs.parse(args); if (commandLineArgs.localNodesToStart().size() > 1) { logger.error( @@ -232,6 +247,13 @@ public static void main(final String... args) throws Exception { throw new ConfigurationException(); } final var platformConfig = buildPlatformConfig(); + // Immediately initialize the cryptography and merkle cryptography factories + // to avoid using default behavior instead of that defined in platformConfig + final var cryptography = CryptographyFactory.create(); + CryptographyHolder.set(cryptography); + final var merkleCryptography = MerkleCryptographyFactory.create(platformConfig, cryptography); + MerkleCryptoFactory.set(merkleCryptography); + // Determine which nodes were _requested_ to run from the command line final var cliNodesToRun = commandLineArgs.localNodesToStart(); // Determine which nodes are _configured_ to run from the config file(s) @@ -239,69 +261,84 @@ public static void main(final String... args) throws Exception { platformConfig.getConfigData(BasicConfig.class).nodesToRun(); // Using the requested nodes to run from the command line, the nodes configured to run, and now the // address book on disk, reconcile the list of nodes to run - final List nodesToRun = getNodesToRun(diskAddressBook, cliNodesToRun, configNodesToRun); + final List nodesToRun = + getNodesToRun(cliNodesToRun, configNodesToRun, ILLEGAL_FALLBACK_NODE_IDS, NOOP_NODE_VALIDATOR); // Finally, verify that the reconciliation of above node IDs yields exactly one node to run final var selfId = ensureSingleNode(nodesToRun); - BootstrapUtils.setupConstructableRegistryWithConfiguration(platformConfig); - final var networkKeysAndCerts = initNodeSecurity(diskAddressBook, platformConfig, Set.copyOf(nodesToRun)); - final var keysAndCerts = networkKeysAndCerts.get(selfId); + + // --- Initialize the platform metrics and the Hedera instance --- setupGlobalMetrics(platformConfig); metrics = getMetricsProvider().createPlatformMetrics(selfId); + hedera = newHedera(metrics); + final var version = hedera.getSoftwareVersion(); + final var isGenesis = new AtomicBoolean(false); + logger.info("Starting node {} with version {}", selfId, version); + + // --- Build required infrastructure to load the initial state, then initialize the States API --- + final var maybeDiskAddressBook = loadLegacyAddressBook(); + BootstrapUtils.setupConstructableRegistryWithConfiguration(platformConfig); final var time = Time.getCurrent(); final var fileSystemManager = FileSystemManager.create(platformConfig); final var recycleBin = RecycleBin.create(metrics, platformConfig, getStaticThreadManager(), time, fileSystemManager, selfId); - final var cryptography = CryptographyFactory.create(); - CryptographyHolder.set(cryptography); - cryptography.digestSync(diskAddressBook); - final var merkleCryptography = MerkleCryptographyFactory.create(platformConfig, cryptography); - MerkleCryptoFactory.set(merkleCryptography); - final var platformContext = PlatformContext.create( - platformConfig, - Time.getCurrent(), - metrics, - cryptography, - FileSystemManager.create(platformConfig), - recycleBin, - merkleCryptography); - - // --- Construct the Hedera instance and use it to initialize the starting state --- - hedera = newHedera(selfId, metrics); - final var version = hedera.getSoftwareVersion(); - final var isGenesis = new AtomicBoolean(false); - logger.info("Starting node {} with version {}", selfId, version); final var reservedState = loadInitialState( platformConfig, recycleBin, version, () -> { isGenesis.set(true); + hedera.initializeConfigProvider(GENESIS); + final var genesisAddressBook = maybeDiskAddressBook.orElse(null); + if (!hedera.isRosterLifecycleEnabled()) { + requireNonNull(genesisAddressBook); + } + Network genesisNetwork; + try { + genesisNetwork = hedera.startupNetworks().genesisNetworkOrThrow(platformConfig); + } catch (Exception ignore) { + // Fallback to the legacy address book if genesis-network.json or equivalent not loaded + genesisNetwork = DiskStartupNetworks.fromLegacyAddressBook(maybeDiskAddressBook.orElseThrow()); + } final var genesisState = hedera.newMerkleStateRoot(); - final var genesisNetwork = DiskStartupNetworks.fromLegacyAddressBook(diskAddressBook); hedera.initializeStatesApi( - genesisState, - metrics, - InitTrigger.GENESIS, - genesisNetwork, - platformConfig, - diskAddressBook); + genesisState, GENESIS, genesisNetwork, platformConfig, genesisAddressBook); return genesisState; }, Hedera.APP_NAME, Hedera.SWIRLD_NAME, selfId); final var initialState = reservedState.state(); + final var state = initialState.get().getState(); if (!isGenesis.get()) { - hedera.initializeStatesApi( - initialState.get().getState(), metrics, InitTrigger.RESTART, null, platformConfig, diskAddressBook); + hedera.initializeConfigProvider(RESTART); + final var diskAddressBook = hedera.isRosterLifecycleEnabled() ? null : maybeDiskAddressBook.orElseThrow(); + hedera.initializeStatesApi(state, RESTART, null, platformConfig, diskAddressBook); } hedera.setInitialStateHash(reservedState.hash()); + // --- Create the platform context and initialize the cryptography --- + final var rosterStore = new ReadableRosterStoreImpl(state.getReadableStates(RosterService.NAME)); + final var currentRoster = requireNonNull(rosterStore.getActiveRoster()); + // For now we convert to a legacy representation of the roster for convenience + final var addressBook = requireNonNull(buildAddressBook(currentRoster)); + if (!addressBook.contains(selfId)) { + throw new IllegalStateException("Self node id " + selfId + " is not in the address book"); + } + final var networkKeysAndCerts = initNodeSecurity(addressBook, platformConfig, Set.copyOf(nodesToRun)); + final var keysAndCerts = networkKeysAndCerts.get(selfId); + cryptography.digestSync(addressBook); + final var platformContext = PlatformContext.create( + platformConfig, + Time.getCurrent(), + metrics, + cryptography, + FileSystemManager.create(platformConfig), + recycleBin, + merkleCryptography); + // --- Now build the platform and start it --- - final var stateRoot = initialState.get().getState(); final RosterHistory rosterHistory; if (hedera.isRosterLifecycleEnabled()) { - final var rosterStore = new ReadableStoreFactory(stateRoot).getStore(ReadableRosterStore.class); rosterHistory = RosterUtils.createRosterHistory(rosterStore); } else { // This constructor both does extensive validation and has the side effect of @@ -312,9 +349,9 @@ public static void main(final String... args) throws Exception { version, detectSoftwareUpgrade(version, initialState.get()), initialState.get(), - diskAddressBook.copy(), + addressBook.copy(), platformContext); - rosterHistory = buildRosterHistory((State) initialState.get().getState()); + rosterHistory = buildRosterHistory(initialState.get().getState()); } final var platformBuilder = PlatformBuilder.create( Hedera.APP_NAME, @@ -322,7 +359,7 @@ public static void main(final String... args) throws Exception { version, initialState, selfId, - canonicalEventStreamLoc(selfId.id(), stateRoot), + canonicalEventStreamLoc(selfId.id(), state), rosterHistory) .withPlatformContext(platformContext) .withConfiguration(platformConfig) @@ -349,12 +386,10 @@ private static String canonicalEventStreamLoc(final long nodeId, @NonNull final /** * Creates a canonical {@link Hedera} instance for the given node id and metrics. * - * @param selfNodeId the node id * @param metrics the metrics * @return the {@link Hedera} instance */ - public static Hedera newHedera(@NonNull final NodeId selfNodeId, @NonNull final Metrics metrics) { - requireNonNull(selfNodeId); + public static Hedera newHedera(@NonNull final Metrics metrics) { requireNonNull(metrics); return new Hedera( ConstructableRegistry.getInstance(), @@ -362,7 +397,8 @@ public static Hedera newHedera(@NonNull final NodeId selfNodeId, @NonNull final new OrderedServiceMigrator(), InstantSource.system(), DiskStartupNetworks::new, - TssBlockHashSigner::new); + TssBlockHashSigner::new, + metrics); } /** @@ -417,24 +453,19 @@ private static NodeId ensureSingleNode(@NonNull final List nodesToRun) { } /** - * Loads the address book from the specified path. + * Loads the legacy address book if it is present. Can be removed once the roster lifecycle is enabled. * - * @param addressBookPath the relative path and file name of the address book. * @return the address book. */ - private static AddressBook loadAddressBook(@NonNull final String addressBookPath) { - requireNonNull(addressBookPath); + @Deprecated + private static Optional loadLegacyAddressBook() { try { final LegacyConfigProperties props = - LegacyConfigPropertiesLoader.loadConfigFile(getAbsolutePath(addressBookPath)); + LegacyConfigPropertiesLoader.loadConfigFile(getAbsolutePath(DEFAULT_CONFIG_FILE_NAME)); props.appConfig().ifPresent(c -> ParameterProvider.getInstance().setParameters(c.params())); - return props.getAddressBook(); - } catch (final Exception e) { - logger.error(EXCEPTION.getMarker(), "Error loading address book", e); - exitSystem(CONFIGURATION_ERROR); - // the following throw is not reachable in production, - // but reachable in testing with static mocked system exit calls. - throw e; + return Optional.of(props.getAddressBook()); + } catch (final Exception ignore) { + return Optional.empty(); } } diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/ServicesMainTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/ServicesMainTest.java index 56a5a1625b6a..034ce2ed18e8 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/ServicesMainTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/ServicesMainTest.java @@ -21,8 +21,6 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertSame; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mock.Strictness.LENIENT; import static org.mockito.Mockito.mock; @@ -74,24 +72,7 @@ static void afterAll() { void throwsExceptionOnNoNodesToRun() { withBadCommandLineArgs(); String[] args = {}; - try (MockedStatic systemExitUtilsMockedStatic = mockStatic(SystemExitUtils.class)) { - assertThatThrownBy(() -> ServicesMain.main(args)).isInstanceOf(ConfigurationException.class); - systemExitUtilsMockedStatic.verify( - () -> SystemExitUtils.exitSystem(eq(NODE_ADDRESS_MISMATCH), anyString())); - } - } - - // local node specified which does not match the address book - @Test - void hardExitOnNonMatchingNodeId() { - withBadCommandLineArgs(); - String[] args = {"-local", "1234"}; // 1234 does not match anything in address book - - try (MockedStatic systemExitUtilsMockedStatic = mockStatic(SystemExitUtils.class)) { - assertThatThrownBy(() -> ServicesMain.main(args)).isInstanceOf(ConfigurationException.class); - systemExitUtilsMockedStatic.verify( - () -> SystemExitUtils.exitSystem(eq(NODE_ADDRESS_MISMATCH), anyString())); - } + assertThatThrownBy(() -> ServicesMain.main(args)).isInstanceOf(IllegalStateException.class); } // more than one local node specified on the commandline diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/AbstractEmbeddedHedera.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/AbstractEmbeddedHedera.java index 8afc3c6aa40b..ae2130851850 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/AbstractEmbeddedHedera.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/AbstractEmbeddedHedera.java @@ -19,6 +19,7 @@ import static com.hedera.hapi.util.HapiUtils.parseAccount; import static com.hedera.node.app.hapi.utils.CommonPbjConverters.fromPbj; import static com.hedera.services.bdd.junit.hedera.ExternalPath.ADDRESS_BOOK; +import static com.hedera.services.bdd.junit.hedera.embedded.fakes.FakePlatformContext.PLATFORM_CONFIG; import static com.swirlds.platform.roster.RosterUtils.rosterFrom; import static com.swirlds.platform.state.service.PbjConverter.toPbjAddressBook; import static com.swirlds.platform.state.service.schemas.V0540PlatformStateSchema.PLATFORM_STATE_KEY; @@ -55,7 +56,12 @@ import com.swirlds.base.utility.Pair; import com.swirlds.common.constructable.ConstructableRegistry; import com.swirlds.common.crypto.Hash; +import com.swirlds.common.metrics.config.MetricsConfig; +import com.swirlds.common.metrics.platform.DefaultPlatformMetrics; +import com.swirlds.common.metrics.platform.MetricKeyRegistry; +import com.swirlds.common.metrics.platform.PlatformMetricsFactoryImpl; import com.swirlds.common.platform.NodeId; +import com.swirlds.metrics.api.Metrics; import com.swirlds.platform.config.legacy.LegacyConfigPropertiesLoader; import com.swirlds.platform.listeners.PlatformStatusChangeNotification; import com.swirlds.platform.state.service.PlatformStateService; @@ -111,6 +117,7 @@ public abstract class AbstractEmbeddedHedera implements EmbeddedHedera { protected final Roster roster; protected final NodeId defaultNodeId; protected final AtomicInteger nextNano = new AtomicInteger(0); + protected final Metrics metrics; protected final Hedera hedera; protected final ServicesSoftwareVersion version; protected final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); @@ -143,13 +150,21 @@ protected AbstractEmbeddedHedera(@NonNull final EmbeddedNode node) { .collect(toMap(Pair::left, Pair::right)); defaultNodeId = NodeId.FIRST_NODE_ID; defaultNodeAccountId = fromPbj(accountIds.get(defaultNodeId)); + final var metricsConfig = PLATFORM_CONFIG.getConfigData(MetricsConfig.class); + metrics = new DefaultPlatformMetrics( + defaultNodeId, + new MetricKeyRegistry(), + executorService, + new PlatformMetricsFactoryImpl(metricsConfig), + metricsConfig); hedera = new Hedera( ConstructableRegistry.getInstance(), FakeServicesRegistry.FACTORY, new FakeServiceMigrator(), this::now, DiskStartupNetworks::new, - () -> this.blockHashSigner = new LapsingBlockHashSigner()); + () -> this.blockHashSigner = new LapsingBlockHashSigner(), + metrics); version = (ServicesSoftwareVersion) hedera.getSoftwareVersion(); blockStreamEnabled = hedera.isBlockStreamEnabled(); Runtime.getRuntime().addShutdownHook(new Thread(executorService::shutdownNow)); @@ -170,13 +185,8 @@ public void start() { } else { trigger = RESTART; } - hedera.initializeStatesApi( - state, - fakePlatform().getContext().getMetrics(), - trigger, - network, - ServicesMain.buildPlatformConfig(), - addressBook); + hedera.initializeConfigProvider(trigger); + hedera.initializeStatesApi(state, trigger, network, ServicesMain.buildPlatformConfig(), addressBook); // TODO - remove this after https://github.com/hashgraph/hedera-services/issues/16552 is done // and we are running all CI tests with the Roster lifecycle enabled diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/ConcurrentEmbeddedHedera.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/ConcurrentEmbeddedHedera.java index ccbf6a5e8b59..feb49e6b3ab0 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/ConcurrentEmbeddedHedera.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/ConcurrentEmbeddedHedera.java @@ -31,6 +31,7 @@ import com.hederahashgraph.api.proto.java.AccountID; import com.hederahashgraph.api.proto.java.Transaction; import com.hederahashgraph.api.proto.java.TransactionResponse; +import com.swirlds.metrics.api.Metrics; import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.events.ConsensusEvent; @@ -59,7 +60,7 @@ class ConcurrentEmbeddedHedera extends AbstractEmbeddedHedera implements Embedde public ConcurrentEmbeddedHedera(@NonNull final EmbeddedNode node) { super(node); - platform = new ConcurrentFakePlatform(executorService); + platform = new ConcurrentFakePlatform(executorService, metrics); } @Override @@ -126,8 +127,9 @@ private class ConcurrentFakePlatform extends AbstractFakePlatform implements Pla private final BlockingQueue queue = new ArrayBlockingQueue<>(MIN_CAPACITY); private final ScheduledExecutorService executorService; - public ConcurrentFakePlatform(@NonNull final ScheduledExecutorService executorService) { - super(defaultNodeId, roster, requireNonNull(executorService)); + public ConcurrentFakePlatform( + @NonNull final ScheduledExecutorService executorService, @NonNull final Metrics metrics) { + super(defaultNodeId, roster, requireNonNull(executorService), requireNonNull(metrics)); this.executorService = executorService; } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/RepeatableEmbeddedHedera.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/RepeatableEmbeddedHedera.java index 25f684921990..3dd83c81ee25 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/RepeatableEmbeddedHedera.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/RepeatableEmbeddedHedera.java @@ -34,6 +34,7 @@ import com.hederahashgraph.api.proto.java.TransactionResponse; import com.swirlds.base.test.fixtures.time.FakeTime; import com.swirlds.common.platform.NodeId; +import com.swirlds.metrics.api.Metrics; import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.Round; @@ -73,7 +74,7 @@ public class RepeatableEmbeddedHedera extends AbstractEmbeddedHedera implements public RepeatableEmbeddedHedera(@NonNull final EmbeddedNode node) { super(node); - platform = new SynchronousFakePlatform(defaultNodeId, executorService); + platform = new SynchronousFakePlatform(defaultNodeId, executorService, metrics); } @Override @@ -186,8 +187,10 @@ private class SynchronousFakePlatform extends AbstractFakePlatform implements Pl private FakeEvent lastCreatedEvent; public SynchronousFakePlatform( - @NonNull final NodeId selfId, @NonNull final ScheduledExecutorService executorService) { - super(selfId, roster, executorService); + @NonNull final NodeId selfId, + @NonNull final ScheduledExecutorService executorService, + @NonNull final Metrics metrics) { + super(selfId, roster, executorService, metrics); } @Override diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/AbstractFakePlatform.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/AbstractFakePlatform.java index 7c620dc29064..890c3174390a 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/AbstractFakePlatform.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/AbstractFakePlatform.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. @@ -25,6 +25,7 @@ import com.swirlds.common.notification.NotificationEngine; import com.swirlds.common.platform.NodeId; import com.swirlds.common.utility.AutoCloseableWrapper; +import com.swirlds.metrics.api.Metrics; import com.swirlds.platform.listeners.PlatformStatusChangeNotification; import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.SwirldState; @@ -47,11 +48,13 @@ public abstract class AbstractFakePlatform implements Platform { public AbstractFakePlatform( @NonNull final NodeId selfId, @NonNull final Roster roster, - @NonNull final ScheduledExecutorService executorService) { + @NonNull final ScheduledExecutorService executorService, + @NonNull final Metrics metrics) { + requireNonNull(metrics); requireNonNull(executorService); this.selfId = requireNonNull(selfId); this.roster = requireNonNull(roster); - platformContext = new FakePlatformContext(selfId, executorService); + this.platformContext = new FakePlatformContext(selfId, executorService, metrics); } /** diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/FakePlatformContext.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/FakePlatformContext.java index 0e5815219537..235ae881a51b 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/FakePlatformContext.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/FakePlatformContext.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. @@ -31,9 +31,6 @@ import com.swirlds.common.merkle.crypto.MerkleCryptography; import com.swirlds.common.merkle.crypto.MerkleCryptographyFactory; import com.swirlds.common.metrics.config.MetricsConfig; -import com.swirlds.common.metrics.platform.DefaultPlatformMetrics; -import com.swirlds.common.metrics.platform.MetricKeyRegistry; -import com.swirlds.common.metrics.platform.PlatformMetricsFactoryImpl; import com.swirlds.common.platform.NodeId; import com.swirlds.config.api.Configuration; import com.swirlds.config.api.ConfigurationBuilder; @@ -46,7 +43,7 @@ import java.util.concurrent.ScheduledExecutorService; public class FakePlatformContext implements PlatformContext { - private final Configuration platformConfig = ConfigurationBuilder.create() + public static final Configuration PLATFORM_CONFIG = ConfigurationBuilder.create() .withConfigDataType(MetricsConfig.class) .withConfigDataType(TransactionConfig.class) .withConfigDataType(CryptoConfig.class) @@ -60,22 +57,18 @@ public class FakePlatformContext implements PlatformContext { private final Metrics metrics; public FakePlatformContext( - @NonNull final NodeId defaultNodeId, @NonNull final ScheduledExecutorService executorService) { + @NonNull final NodeId defaultNodeId, + @NonNull final ScheduledExecutorService executorService, + @NonNull final Metrics metrics) { requireNonNull(defaultNodeId); requireNonNull(executorService); - final var metricsConfig = platformConfig.getConfigData(MetricsConfig.class); - this.metrics = new DefaultPlatformMetrics( - defaultNodeId, - new MetricKeyRegistry(), - executorService, - new PlatformMetricsFactoryImpl(metricsConfig), - metricsConfig); + this.metrics = requireNonNull(metrics); } @NonNull @Override public Configuration getConfiguration() { - return platformConfig; + return PLATFORM_CONFIG; } @NonNull @@ -117,6 +110,6 @@ public RecycleBin getRecycleBin() { @NonNull @Override public MerkleCryptography getMerkleCryptography() { - return MerkleCryptographyFactory.create(platformConfig, getCryptography()); + return MerkleCryptographyFactory.create(PLATFORM_CONFIG, getCryptography()); } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/validators/block/StateChangesValidator.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/validators/block/StateChangesValidator.java index 6bfd3b0ba426..5161a6d05412 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/validators/block/StateChangesValidator.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/validators/block/StateChangesValidator.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. @@ -28,6 +28,7 @@ import static com.hedera.services.bdd.junit.hedera.utils.WorkingDirUtils.workingDirFor; import static com.hedera.services.bdd.junit.support.validators.block.ChildHashUtils.hashesByName; import static com.hedera.services.bdd.spec.TargetNetworkType.SUBPROCESS_NETWORK; +import static com.swirlds.platform.system.InitTrigger.GENESIS; import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -63,11 +64,9 @@ import com.swirlds.common.merkle.crypto.MerkleCryptography; import com.swirlds.common.merkle.utility.MerkleTreeVisualizer; import com.swirlds.common.metrics.noop.NoOpMetrics; -import com.swirlds.common.platform.NodeId; import com.swirlds.platform.config.legacy.LegacyConfigPropertiesLoader; import com.swirlds.platform.crypto.CryptoStatic; import com.swirlds.platform.state.PlatformMerkleStateRoot; -import com.swirlds.platform.system.InitTrigger; import com.swirlds.platform.system.address.AddressBook; import com.swirlds.state.lifecycle.Service; import com.swirlds.state.merkle.MerkleStateRoot; @@ -205,16 +204,12 @@ public StateChangesValidator( final var servicesVersion = versionConfig.servicesVersion(); final var addressBook = loadLegacyBookWithGeneratedCerts(pathToAddressBook); final var metrics = new NoOpMetrics(); - final var hedera = ServicesMain.newHedera(NodeId.of(0L), metrics); + final var hedera = ServicesMain.newHedera(metrics); this.state = hedera.newMerkleStateRoot(); final var platformConfig = ServicesMain.buildPlatformConfig(); + hedera.initializeConfigProvider(GENESIS); hedera.initializeStatesApi( - state, - metrics, - InitTrigger.GENESIS, - DiskStartupNetworks.fromLegacyAddressBook(addressBook), - platformConfig, - addressBook); + state, GENESIS, DiskStartupNetworks.fromLegacyAddressBook(addressBook), platformConfig, addressBook); final var stateToBeCopied = state; state = state.copy(); // get the state hash before applying the state changes from current block diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/merkle/crypto/MerkleCryptoFactory.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/merkle/crypto/MerkleCryptoFactory.java index e7ab5fe9cba2..7e28cf448a26 100644 --- a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/merkle/crypto/MerkleCryptoFactory.java +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/merkle/crypto/MerkleCryptoFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2024 Hedera Hashgraph, LLC + * Copyright (C) 2016-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. diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/Browser.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/Browser.java index 0bf08b6b6ed7..bd7094699d29 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/Browser.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/Browser.java @@ -187,7 +187,9 @@ private static void launchUnhandled(@NonNull final CommandLineArgs commandLineAr final List configNodesToRun = bootstrapConfiguration.getConfigData(BasicConfig.class).nodesToRun(); final Set cliNodesToRun = commandLineArgs.localNodesToStart(); - final List nodesToRun = getNodesToRun(appAddressBook, cliNodesToRun, configNodesToRun); + final var validNodeIds = appAddressBook.getNodeIdSet(); + final List nodesToRun = + getNodesToRun(cliNodesToRun, configNodesToRun, () -> validNodeIds, validNodeIds::contains); logger.info(STARTUP.getMarker(), "The following nodes {} are set to run locally", nodesToRun); // Load all SwirldMain instances for locally run nodes. diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/util/BootstrapUtils.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/util/BootstrapUtils.java index 42f83345d5c0..ff56a99d5d99 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/util/BootstrapUtils.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/util/BootstrapUtils.java @@ -21,6 +21,7 @@ import static com.swirlds.platform.system.SystemExitCode.NODE_ADDRESS_MISMATCH; import static com.swirlds.platform.system.SystemExitUtils.exitSystem; import static com.swirlds.virtualmap.constructable.ConstructableUtils.registerVirtualMapConstructables; +import static java.util.Objects.requireNonNull; import com.swirlds.common.constructable.ClassConstructorPair; import com.swirlds.common.constructable.ConstructableRegistry; @@ -55,7 +56,6 @@ import com.swirlds.platform.system.SoftwareVersion; import com.swirlds.platform.system.SwirldMain; import com.swirlds.platform.system.address.Address; -import com.swirlds.platform.system.address.AddressBook; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.awt.Dimension; @@ -71,8 +71,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; +import java.util.function.Predicate; +import java.util.function.Supplier; import javax.swing.JFrame; import javax.swing.UIManager; import javax.swing.UnsupportedLookAndFeelException; @@ -135,7 +136,7 @@ public static void setupConfigBuilder( * @param configuration the configuration */ public static void performHealthChecks(@NonNull final Path configPath, @NonNull final Configuration configuration) { - Objects.requireNonNull(configuration); + requireNonNull(configuration); final OSFileSystemChecker osFileSystemChecker = new OSFileSystemChecker(configPath); OSHealthChecker.performOSHealthChecks( @@ -192,7 +193,7 @@ public static void setupConstructableRegistryWithConfiguration(Configuration con * @param appMainName the name of the app main class */ public static @NonNull SwirldMain loadAppMain(@NonNull final String appMainName) { - Objects.requireNonNull(appMainName); + requireNonNull(appMainName); try { final Class mainClass = Class.forName(appMainName); final Constructor[] constructors = mainClass.getDeclaredConstructors(); @@ -227,7 +228,7 @@ public static void setupConstructableRegistryWithConfiguration(Configuration con */ public static boolean detectSoftwareUpgrade( @NonNull final SoftwareVersion appVersion, @Nullable final SignedState loadedSignedState) { - Objects.requireNonNull(appVersion, "The app version must not be null."); + requireNonNull(appVersion, "The app version must not be null."); final SoftwareVersion loadedSoftwareVersion; if (loadedSignedState == null) { @@ -263,7 +264,7 @@ public static boolean detectSoftwareUpgrade( * @param configuration the configuration object */ public static void startJVMPauseDetectorThread(@NonNull final Configuration configuration) { - Objects.requireNonNull(configuration); + requireNonNull(configuration); final BasicConfig basicConfig = configuration.getConfigData(BasicConfig.class); if (basicConfig.jvmPauseDetectorSleepMs() > 0) { @@ -292,8 +293,8 @@ public static void startJVMPauseDetectorThread(@NonNull final Configuration conf */ public static @NonNull SwirldMain buildAppMain( @NonNull final ApplicationDefinition appDefinition, @NonNull final SwirldAppLoader appLoader) { - Objects.requireNonNull(appDefinition); - Objects.requireNonNull(appLoader); + requireNonNull(appDefinition); + requireNonNull(appLoader); try { return appLoader.instantiateSwirldMain(); } catch (final Exception e) { @@ -312,7 +313,7 @@ public static void startJVMPauseDetectorThread(@NonNull final Configuration conf * @param configuration the configuration values to write */ public static void writeSettingsUsed(@NonNull final Configuration configuration) { - Objects.requireNonNull(configuration); + requireNonNull(configuration); final StringBuilder settingsUsedBuilder = new StringBuilder(); // Add all settings values to the string builder @@ -340,28 +341,29 @@ public static void writeSettingsUsed(@NonNull final Configuration configuration) * through the system environment. If no nodes are specified on the commandline or through the system environment, * all nodes in the address book are returned to run locally. * - * @param addressBook the address book * @param cliNodesToRun nodes specified to start by the user on the command line * @param configNodesToRun nodes specified to start by the user in configuration + * @param knownNodeIds the set of known node ids + * @param validNodeId a predicate that determines if a node id is valid * @return A non-empty list of nodes to run locally - * @throws IllegalArgumentException if a node to run is not in the address book or the list of nodes to run is - * empty + * @throws IllegalArgumentException if a node to run is invalid or the list of nodes to run is empty */ public static @NonNull List getNodesToRun( - @NonNull final AddressBook addressBook, @NonNull final Set cliNodesToRun, - @NonNull final List configNodesToRun) { - Objects.requireNonNull(addressBook); - Objects.requireNonNull(cliNodesToRun); - Objects.requireNonNull(configNodesToRun); + @NonNull final List configNodesToRun, + @NonNull final Supplier> knownNodeIds, + @NonNull final Predicate validNodeId) { + requireNonNull(validNodeId); + requireNonNull(cliNodesToRun); + requireNonNull(configNodesToRun); + requireNonNull(knownNodeIds); final List nodesToRun = new ArrayList<>(); - final Set addressBookNodeIds = addressBook.getNodeIdSet(); - if (cliNodesToRun.isEmpty()) { if (configNodesToRun.isEmpty()) { - // if no node ids are provided by cli or config, run all nodes from the address book. - return new ArrayList<>(addressBookNodeIds); + // If no node ids are provided by cli or config, run all nodes from the address book; + // this will only be a useful fallback for Browser-based platform testing apps + return new ArrayList<>(knownNodeIds.get()); } else { // CLI did not provide any nodes to run, so use the node ids from the config nodesToRun.addAll(configNodesToRun); @@ -372,8 +374,8 @@ public static void writeSettingsUsed(@NonNull final Configuration configuration) } for (final NodeId nodeId : nodesToRun) { - if (!addressBook.contains(nodeId)) { - final String errorMessage = "Node " + nodeId + " is not in the address book and cannot be started."; + if (!validNodeId.test(nodeId)) { + final String errorMessage = "Node " + nodeId + " is invalid and cannot be started."; // all nodes to start must exist in the address book logger.error(EXCEPTION.getMarker(), errorMessage); exitSystem(NODE_ADDRESS_MISMATCH, errorMessage); @@ -398,8 +400,8 @@ public static void writeSettingsUsed(@NonNull final Configuration configuration) @NonNull public static Map loadSwirldMains( @NonNull final ApplicationDefinition appDefinition, @NonNull final Collection nodesToRun) { - Objects.requireNonNull(appDefinition, "appDefinition must not be null"); - Objects.requireNonNull(nodesToRun, "nodesToRun must not be null"); + requireNonNull(appDefinition, "appDefinition must not be null"); + requireNonNull(nodesToRun, "nodesToRun must not be null"); try { // Create the SwirldAppLoader final SwirldAppLoader appLoader; From abccf398a16c8b6628d7b7444eff2bbe62808ebc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Jan 2025 11:24:19 -0600 Subject: [PATCH 13/19] build(deps): bump mikefarah/yq from 4.44.6 to 4.45.1 (#17363) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/node-zxcron-release-branching.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/node-zxcron-release-branching.yaml b/.github/workflows/node-zxcron-release-branching.yaml index e9c1845a372c..7f82494e1fd9 100644 --- a/.github/workflows/node-zxcron-release-branching.yaml +++ b/.github/workflows/node-zxcron-release-branching.yaml @@ -51,7 +51,7 @@ jobs: - name: Read Trigger Time id: time - uses: mikefarah/yq@4839dbbf80445070a31c7a9c1055da527db2d5ee # v4.44.6 + uses: mikefarah/yq@8bf425b4d1344db7cd469a8d10a390876e0c77fd # v4.45.1 with: cmd: yq '.release.branching.execution.time' '${{ env.WORKFLOW_CONFIG_FILE }}' From 420d1722bdc70b8679c75eaf60dd3844dfd7c96f Mon Sep 17 00:00:00 2001 From: Roger Barker Date: Tue, 14 Jan 2025 12:06:24 -0600 Subject: [PATCH 14/19] chore: Update codeowners/docs to include platform-ci name change (#17381) Signed-off-by: Roger Barker --- .github/CODEOWNERS | 44 +++++++++++++++++++-------------------- docs/maintainers-guide.md | 4 ++-- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a0d05c23488d..ad857ec5ceef 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,7 +2,7 @@ ##### Global Protection Rule ###### ################################### # NOTE: This rule is overriden by the more specific rules below. This is the catch-all rule for all files not covered by the more specific rules below. -* @hashgraph/devops-ci @hashgraph/release-engineering-managers +* @hashgraph/platform-ci @hashgraph/release-engineering-managers ######################### ##### Example apps ###### ######################### @@ -29,10 +29,10 @@ # Hedera Node Deployments - Configuration & Grafana Dashboards /hedera-node/configuration/** @rbair23 @dalvizu @poulok @netopyr @Nana-EC @SimiHunjan @steven-sheehy @nathanklick /hedera-node/configuration/dev/** @hashgraph/hedera-services -/hedera-node/infrastructure/** @hashgraph/release-engineering-managers @hashgraph/devops-ci @hashgraph/devops @hashgraph/hedera-services +/hedera-node/infrastructure/** @hashgraph/release-engineering-managers @hashgraph/platform-ci @hashgraph/devops @hashgraph/hedera-services # Hedera Node Docker Definitions -/hedera-node/docker/ @hashgraph/hedera-services @hashgraph/devops-ci @hashgraph/release-engineering @hashgraph/release-engineering-managers +/hedera-node/docker/ @hashgraph/hedera-services @hashgraph/platform-ci @hashgraph/release-engineering @hashgraph/release-engineering-managers # Hedera Node Modules /hedera-node/hapi*/ @hashgraph/hedera-services @@ -102,40 +102,40 @@ # NOTE: Must be placed last to ensure enforcement over all other rules # Protection Rules for Github Configuration Files and Actions Workflows -/.github/ @hashgraph/devops-ci @hashgraph/release-engineering-managers -/.github/workflows/ @hashgraph/devops-ci @hashgraph/devops-ci-committers -/.github/workflows/node-zxf-deploy-integration.yaml @hashgraph/devops-ci @hashgraph/devops-ci-committers @hashgraph/devops -/.github/workflows/node-zxf-deploy-preview.yaml @hashgraph/devops-ci @hashgraph/devops-ci-committers @hashgraph/devops +/.github/ @hashgraph/platform-ci @hashgraph/release-engineering-managers +/.github/workflows/ @hashgraph/platform-ci @hashgraph/platform-ci-committers +/.github/workflows/node-zxf-deploy-integration.yaml @hashgraph/platform-ci @hashgraph/platform-ci-committers @hashgraph/devops +/.github/workflows/node-zxf-deploy-preview.yaml @hashgraph/platform-ci @hashgraph/platform-ci-committers @hashgraph/devops # Legacy Maven project files -**/pom.xml @hashgraph/devops-ci @hashgraph/devops +**/pom.xml @hashgraph/platform-ci @hashgraph/devops # Gradle project files and inline plugins -/gradle/ @hashgraph/devops-ci @hashgraph/devops-ci-committers -gradlew @hashgraph/devops-ci @hashgraph/devops-ci-committers -gradlew.bat @hashgraph/devops-ci @hashgraph/devops-ci-committers -**/build-logic/ @hashgraph/devops-ci @hashgraph/devops-ci-committers -**/gradle.* @hashgraph/devops-ci @hashgraph/devops-ci-committers -**/*.gradle.* @hashgraph/devops-ci @hashgraph/devops-ci-committers +/gradle/ @hashgraph/platform-ci @hashgraph/platform-ci-committers +gradlew @hashgraph/platform-ci @hashgraph/platform-ci-committers +gradlew.bat @hashgraph/platform-ci @hashgraph/platform-ci-committers +**/build-logic/ @hashgraph/platform-ci @hashgraph/platform-ci-committers +**/gradle.* @hashgraph/platform-ci @hashgraph/platform-ci-committers +**/*.gradle.* @hashgraph/platform-ci @hashgraph/platform-ci-committers # Codacy Tool Configurations -/config/ @hashgraph/devops-ci @hashgraph/release-engineering-managers -.remarkrc @hashgraph/devops-ci @hashgraph/release-engineering-managers +/config/ @hashgraph/platform-ci @hashgraph/release-engineering-managers +.remarkrc @hashgraph/platform-ci @hashgraph/release-engineering-managers # Self-protection for root CODEOWNERS files (this file should not exist and should definitely require approval) /CODEOWNERS @hashgraph/release-engineering-managers # Protect the repository root files -/README.md @hashgraph/devops-ci @hashgraph/release-engineering-managers @hashgraph/platform-base @hashgraph/hedera-services @hashgraph/platform-hashgraph +/README.md @hashgraph/platform-ci @hashgraph/release-engineering-managers @hashgraph/platform-base @hashgraph/hedera-services @hashgraph/platform-hashgraph **/LICENSE @hashgraph/release-engineering-managers # CodeCov configuration -**/codecov.yml @hashgraph/devops-ci @hashgraph/release-engineering-managers +**/codecov.yml @hashgraph/platform-ci @hashgraph/release-engineering-managers # Git Ignore definitions -**/.gitignore @hashgraph/devops-ci @hashgraph/release-engineering-managers @hashgraph/platform-base @hashgraph/hedera-services @hashgraph/platform-hashgraph -**/.gitignore.* @hashgraph/devops-ci @hashgraph/release-engineering-managers @hashgraph/platform-base @hashgraph/hedera-services @hashgraph/platform-hashgraph +**/.gitignore @hashgraph/platform-ci @hashgraph/release-engineering-managers @hashgraph/platform-base @hashgraph/hedera-services @hashgraph/platform-hashgraph +**/.gitignore.* @hashgraph/platform-ci @hashgraph/release-engineering-managers @hashgraph/platform-base @hashgraph/hedera-services @hashgraph/platform-hashgraph # Legacy CircleCI configuration -.circleci.settings.xml @hashgraph/devops-ci @hashgraph/release-engineering-managers -/.circleci/ @hashgraph/devops-ci @hashgraph/release-engineering-managers \ No newline at end of file +.circleci.settings.xml @hashgraph/platform-ci @hashgraph/release-engineering-managers +/.circleci/ @hashgraph/platform-ci @hashgraph/release-engineering-managers \ No newline at end of file diff --git a/docs/maintainers-guide.md b/docs/maintainers-guide.md index 34502f5d0319..2feab6c6c344 100644 --- a/docs/maintainers-guide.md +++ b/docs/maintainers-guide.md @@ -90,9 +90,9 @@ with 0.30 milestone on it. ![labels-on-issue](./assets/labels-on-issue.png) -### DevOps-CI Responsibilities +### Platform-CI Responsibilities -The DevOps-CI team will handle the following: +The Platform-CI team will handle the following: - Will provide automated release processes and coordinate release schedules - Will handle production releases From 46446f1f46b8e10893f42c4e17a38379bb3e2822 Mon Sep 17 00:00:00 2001 From: Ivan Malygin Date: Tue, 14 Jan 2025 13:57:52 -0500 Subject: [PATCH 15/19] refactor: 17353 Extracted lifecycle event handling logic from `PlatformMerkleStateRoot` (#17275) Signed-off-by: Ivan Malygin Co-authored-by: Mustafa Uzun --- .../main/java/com/hedera/node/app/Hedera.java | 17 +- .../com/hedera/node/app/ServicesMain.java | 22 +- .../com/hedera/node/app/package-info.java | 18 +- .../node/app/state/StateLifecyclesImpl.java | 16 +- .../app/workflows/handle/HandleWorkflow.java | 2 +- .../workflows/handle/record/SystemSetup.java | 4 +- .../prehandle/PreHandleWorkflow.java | 4 +- .../app/state/StateLifecyclesImplTest.java | 4 +- .../app/state/merkle/SerializationTest.java | 11 +- .../node/app/fixtures/state/FakePlatform.java | 7 +- .../src/testFixtures/java/module-info.java | 19 +- .../embedded/fakes/AbstractFakePlatform.java | 6 +- .../demo/crypto/CryptocurrencyDemoMain.java | 27 +- .../demo/crypto/CryptocurrencyDemoState.java | 96 +- .../CryptocurrencyDemoStateLifecycles.java | 136 ++ .../demo/hello/HelloSwirldDemoMain.java | 21 +- .../demo/hello/HelloSwirldDemoState.java | 30 +- .../hello/HelloSwirldDemoStateLifecycles.java | 91 ++ .../com/swirlds/demo/stats/StatsDemoMain.java | 22 +- .../swirlds/demo/stats/StatsDemoState.java | 18 +- .../AddressBookTestingToolConfig.java | 5 +- .../AddressBookTestingToolMain.java | 24 +- .../AddressBookTestingToolState.java | 813 +---------- ...AddressBookTestingToolStateLifecycles.java | 853 ++++++++++++ .../AddressBookTestingToolStateTest.java | 27 +- .../ConsistencyTestingToolMain.java | 23 +- .../ConsistencyTestingToolState.java | 256 ++-- ...ConsistencyTestingToolStateLifecycles.java | 168 +++ .../TransactionHandlingHistory.java | 16 +- .../ConsistencyTestingToolStateTest.java | 25 +- .../TransactionHandlingHistoryTests.java | 181 +-- .../swirlds/demo/iss/ISSTestingToolMain.java | 20 +- .../swirlds/demo/iss/ISSTestingToolState.java | 386 +----- .../iss/ISSTestingToolStateLifecycles.java | 391 ++++++ .../demo/iss/ISSTestingToolStateTest.java | 28 +- .../MigrationTestToolStateLifecycles.java | 170 +++ .../migration/MigrationTestingToolMain.java | 23 +- .../migration/MigrationTestingToolState.java | 140 +- .../MigrationTestingToolStateTest.java | 21 +- .../platform/PlatformTestingToolMain.java | 27 +- .../platform/PlatformTestingToolState.java | 1105 +-------------- .../PlatformTestingToolStateLifecycles.java | 1186 +++++++++++++++++ .../demo/merkle/map/MapValueFCQTests.java | 7 +- .../PlatformTestingToolStateTests.java | 67 - .../demo/platform/PttTransactionPoolTest.java | 7 +- .../signing/StatsSigningTestingToolMain.java | 23 +- .../signing/StatsSigningTestingToolState.java | 145 +- ...tatsSigningTestingToolStateLifecycles.java | 177 +++ .../demo/stress/StressTestingToolMain.java | 26 +- .../demo/stress/StressTestingToolState.java | 166 +-- .../StressTestingToolStateLifecycles.java | 186 +++ .../stress/StressTestingToolStateTest.java | 23 +- .../java/com/swirlds/platform/Browser.java | 9 +- .../platform/ReconnectStateLoader.java | 22 +- .../swirlds/platform/StateInitializer.java | 16 +- .../com/swirlds/platform/SwirldsPlatform.java | 21 +- .../platform/builder/PlatformBuilder.java | 53 +- .../builder/PlatformBuildingBlocks.java | 6 +- .../builder/PlatformComponentBuilder.java | 3 +- .../platform/cli/CompareStatesCommand.java | 2 +- .../cli/GenesisPlatformStateCommand.java | 2 +- .../cli/ValidateAddressBookStateCommand.java | 2 +- .../DefaultLatestCompleteStateNotifier.java | 4 +- .../DefaultTransactionHandler.java | 4 +- .../DefaultTransactionPrehandler.java | 18 +- .../swirlds/platform/gossip/SyncGossip.java | 2 +- .../ReconnectCompleteNotification.java | 16 +- .../StateWriteToDiskCompleteNotification.java | 6 +- ...rldStateMetrics.java => StateMetrics.java} | 31 +- .../platform/reconnect/ReconnectLearner.java | 3 +- .../reconnect/ReconnectLearnerFactory.java | 2 +- .../recovery/EventRecoveryWorkflow.java | 47 +- .../recovery/internal/RecoveryPlatform.java | 7 +- .../platform/state/NoOpStateLifecycles.java | 81 ++ .../state/PlatformMerkleStateRoot.java | 101 +- .../platform/state/StateLifecycles.java | 22 +- .../platform/state/SwirldStateManager.java | 68 +- .../state/SwirldStateManagerUtils.java | 6 +- .../platform/state/TransactionHandler.java | 24 +- .../state/address/AddressBookInitializer.java | 22 +- .../platform/state/editor/StateEditor.java | 2 +- .../platform/state/signed/SignedState.java | 20 +- .../state/signed/StartupStateUtils.java | 2 +- .../state/snapshot/SignedStateFileReader.java | 2 +- .../com/swirlds/platform/system/Platform.java | 5 +- .../platform/system/PlatformStatNames.java | 4 +- .../swirlds/platform/system/SwirldMain.java | 16 +- .../swirlds/platform/system/SwirldState.java | 127 -- .../system/address/AddressBookUtils.java | 14 +- .../NewRecoveredStateNotification.java | 20 +- .../NewSignedStateNotification.java | 17 +- .../platform/AddressBookInitializerTest.java | 123 +- .../SignedStateFileReadWriteTest.java | 2 +- ...andlerManagerFreezePeriodCheckerTest.java} | 4 +- .../platform/StateFileManagerTests.java | 6 +- .../components/DefaultAppNotifierTest.java | 6 +- .../DefaultTransactionHandlerTests.java | 6 +- .../TransactionHandlerTester.java | 24 +- .../TransactionPrehandlerTests.java | 14 +- .../recovery/EventRecoveryWorkflowTests.java | 33 +- .../state/PlatformMerkleStateRootTest.java | 122 +- .../platform/state/SignedStateTests.java | 7 +- ...> StateEventHandlerManagerUtilsTests.java} | 10 +- .../platform/state/StateRegistryTests.java | 8 +- ...sts.java => SwirldsStateManagerTests.java} | 15 +- .../state/hashlogger/HashLoggerTest.java | 5 +- .../state/signed/StartupStateUtilsTests.java | 4 +- .../platform/turtle/runner/Turtle.java | 5 +- .../platform/turtle/runner/TurtleNode.java | 8 +- .../turtle/runner/TurtleStateLifecycles.java | 86 ++ .../turtle/runner/TurtleTestingToolState.java | 24 +- .../wiring/SignedStateReserverTest.java | 2 +- ...ingSwirldState.java => BlockingState.java} | 33 +- .../fixtures/state/FakeStateLifecycles.java | 21 +- .../state/RandomSignedStateGenerator.java | 13 +- .../LatestCompleteStateNotifierTests.java | 8 +- .../platform/test/SignedStateUtils.java | 4 +- .../com/swirlds/platform/test/StateTest.java | 5 +- .../com/swirlds/platform/test/StateTests.java | 6 +- 119 files changed, 4637 insertions(+), 4082 deletions(-) create mode 100644 platform-sdk/platform-apps/demos/CryptocurrencyDemo/src/main/java/com/swirlds/demo/crypto/CryptocurrencyDemoStateLifecycles.java create mode 100644 platform-sdk/platform-apps/demos/HelloSwirldDemo/src/main/java/com/swirlds/demo/hello/HelloSwirldDemoStateLifecycles.java create mode 100644 platform-sdk/platform-apps/tests/AddressBookTestingTool/src/main/java/com/swirlds/demo/addressbook/AddressBookTestingToolStateLifecycles.java create mode 100644 platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/main/java/com/swirlds/demo/consistency/ConsistencyTestingToolStateLifecycles.java create mode 100644 platform-sdk/platform-apps/tests/ISSTestingTool/src/main/java/com/swirlds/demo/iss/ISSTestingToolStateLifecycles.java create mode 100644 platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/MigrationTestToolStateLifecycles.java create mode 100644 platform-sdk/platform-apps/tests/PlatformTestingTool/src/main/java/com/swirlds/demo/platform/PlatformTestingToolStateLifecycles.java delete mode 100644 platform-sdk/platform-apps/tests/PlatformTestingTool/src/timingSensitive/java/com/swirlds/demo/platform/PlatformTestingToolStateTests.java create mode 100644 platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolStateLifecycles.java create mode 100644 platform-sdk/platform-apps/tests/StressTestingTool/src/main/java/com/swirlds/demo/stress/StressTestingToolStateLifecycles.java rename platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/metrics/{SwirldStateMetrics.java => StateMetrics.java} (81%) create mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/NoOpStateLifecycles.java delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/SwirldState.java rename platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/{SwirldStateManagerFreezePeriodCheckerTest.java => StateEventHandlerManagerFreezePeriodCheckerTest.java} (95%) rename platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/{SwirldStateManagerUtilsTests.java => StateEventHandlerManagerUtilsTests.java} (83%) rename platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/{SwirldStateManagerTests.java => SwirldsStateManagerTests.java} (92%) create mode 100644 platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/turtle/runner/TurtleStateLifecycles.java rename platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/state/{BlockingSwirldState.java => BlockingState.java} (78%) diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/Hedera.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/Hedera.java index f6459c837f03..41cfa0572179 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/Hedera.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/Hedera.java @@ -137,6 +137,7 @@ import com.swirlds.platform.listeners.ReconnectCompleteNotification; import com.swirlds.platform.listeners.StateWriteToDiskCompleteListener; import com.swirlds.platform.state.PlatformMerkleStateRoot; +import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.state.service.PlatformStateService; import com.swirlds.platform.state.service.ReadablePlatformStateStore; import com.swirlds.platform.system.InitTrigger; @@ -203,7 +204,8 @@ * including its state. It constructs the Dagger dependency tree, and manages the gRPC server, and in all other ways, * controls execution of the node. If you want to understand our system, this is a great place to start! */ -public final class Hedera implements SwirldMain, PlatformStatusChangeListener, AppContext.Gossip { +public final class Hedera + implements SwirldMain, PlatformStatusChangeListener, AppContext.Gossip { private static final Logger logger = LogManager.getLogger(Hedera.class); // FUTURE: This should come from configuration, not be hardcoded. @@ -286,6 +288,8 @@ public final class Hedera implements SwirldMain, PlatformStatusChangeListener, A */ private final StartupNetworksFactory startupNetworksFactory; + private final StateLifecycles stateLifecycles; + /** * The Hashgraph Platform. This is set during state initialization. */ @@ -468,8 +472,9 @@ public Hedera( PLATFORM_STATE_SERVICE) .forEach(servicesRegistry::register); try { + stateLifecycles = new StateLifecyclesImpl(this); final Supplier baseSupplier = - () -> new PlatformMerkleStateRoot(new StateLifecyclesImpl(this), ServicesSoftwareVersion::new); + () -> new PlatformMerkleStateRoot(ServicesSoftwareVersion::new); final var blockStreamsEnabled = isBlockStreamEnabled(); stateRootSupplier = blockStreamsEnabled ? () -> withListeners(baseSupplier.get()) : baseSupplier; onSealConsensusRound = blockStreamsEnabled ? this::manageBlockEndRound : (round, state) -> {}; @@ -515,6 +520,14 @@ public PlatformMerkleStateRoot newMerkleStateRoot() { return stateRootSupplier.get(); } + /** + * {@inheritDoc} + */ + @Override + public StateLifecycles newStateLifecycles() { + return stateLifecycles; + } + @Override public void notify(@NonNull final PlatformStatusChangeNotification notification) { this.platformStatus = notification.getNewStatus(); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/ServicesMain.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/ServicesMain.java index 8c136aa1c62c..52baf92d6418 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/ServicesMain.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/ServicesMain.java @@ -47,6 +47,7 @@ import com.hedera.node.app.service.addressbook.impl.ReadableNodeStoreImpl; import com.hedera.node.app.services.OrderedServiceMigrator; import com.hedera.node.app.services.ServicesRegistryImpl; +import com.hedera.node.app.state.StateLifecyclesImpl; import com.hedera.node.app.tss.TssBlockHashSigner; import com.hedera.node.internal.network.Network; import com.swirlds.base.time.Time; @@ -88,9 +89,9 @@ import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.SoftwareVersion; import com.swirlds.platform.system.SwirldMain; -import com.swirlds.platform.system.SwirldState; import com.swirlds.platform.system.address.AddressBook; import com.swirlds.platform.util.BootstrapUtils; +import com.swirlds.state.State; import com.swirlds.state.merkle.MerkleStateRoot; import edu.umd.cs.findbugs.annotations.NonNull; import java.time.InstantSource; @@ -108,7 +109,7 @@ * *

This class simply delegates to {@link Hedera}. */ -public class ServicesMain implements SwirldMain { +public class ServicesMain implements SwirldMain { private static final Logger logger = LogManager.getLogger(ServicesMain.class); /** @@ -163,6 +164,14 @@ public void init(@NonNull final Platform platform, @NonNull final NodeId nodeId) return hederaOrThrow().newMerkleStateRoot(); } + /** + * {@inheritDoc} + */ + @Override + public StateLifecycles newStateLifecycles() { + return new StateLifecyclesImpl(hederaOrThrow()); + } + /** * {@inheritDoc} */ @@ -190,7 +199,7 @@ public void run() { * and the working directory settings.txt, providing the same * {@link Hedera#newMerkleStateRoot()} method reference as the genesis state * factory. (IMPORTANT: This step instantiates and invokes - * {@link SwirldState#init(Platform, InitTrigger, SoftwareVersion)} + * {@link StateLifecycles#onStateInitialized(MerkleStateRoot, Platform, InitTrigger, SoftwareVersion)} * on a {@link MerkleStateRoot} instance that delegates the call back to our * Hedera instance.) *

  • Call {@link Hedera#init(Platform, NodeId)} to complete startup phase @@ -281,6 +290,7 @@ public static void main(final String... args) throws Exception { final var fileSystemManager = FileSystemManager.create(platformConfig); final var recycleBin = RecycleBin.create(metrics, platformConfig, getStaticThreadManager(), time, fileSystemManager, selfId); + StateLifecycles stateLifecycles = hedera.newStateLifecycles(); final var reservedState = loadInitialState( platformConfig, recycleBin, @@ -350,7 +360,8 @@ public static void main(final String... args) throws Exception { detectSoftwareUpgrade(version, initialState.get()), initialState.get(), addressBook.copy(), - platformContext); + platformContext, + stateLifecycles); rosterHistory = buildRosterHistory(initialState.get().getState()); } final var platformBuilder = PlatformBuilder.create( @@ -358,6 +369,7 @@ public static void main(final String... args) throws Exception { Hedera.SWIRLD_NAME, version, initialState, + stateLifecycles, selfId, canonicalEventStreamLoc(selfId.id(), state), rosterHistory) @@ -377,7 +389,7 @@ public static void main(final String... args) throws Exception { * @param root the platform merkle state root * @return the event stream name */ - private static String canonicalEventStreamLoc(final long nodeId, @NonNull final PlatformMerkleStateRoot root) { + private static String canonicalEventStreamLoc(final long nodeId, @NonNull final State root) { final var nodeStore = new ReadableNodeStoreImpl(root.getReadableStates(AddressBookService.NAME)); final var accountId = requireNonNull(nodeStore.get(nodeId)).accountIdOrThrow(); return accountId.shardNum() + "." + accountId.realmNum() + "." + accountId.accountNumOrThrow(); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/package-info.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/package-info.java index ed46cee1b04f..0a8ade27702a 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/package-info.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/package-info.java @@ -1,9 +1,25 @@ +/* + * 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. + */ + /** * Main package for the Hedera node application. * *

    The Hashgraph Platform today follows a "container-managed" model, where applications extend from * {@link com.swirlds.platform.system.SwirldMain} to define their main entry point, much like a Java Applet would extend - * from {@code Applet}. An application also extends from {@link com.swirlds.platform.system.SwirldState} to define its + * from {@code Applet}. An application also extends from {@link com.swirlds.state.merkle.MerkleStateRoot} to define its * Merkle tree for holding state. The platform then dynamically looks up and creates these objects. The platform is thus * in charge of lifecycle management. * diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/StateLifecyclesImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/StateLifecyclesImpl.java index 34f9baf11bf9..fb91b48bca41 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/StateLifecyclesImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/StateLifecyclesImpl.java @@ -22,6 +22,7 @@ import com.hedera.node.app.Hedera; import com.swirlds.common.context.PlatformContext; import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; +import com.swirlds.platform.state.PlatformMerkleStateRoot; import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.system.InitTrigger; import com.swirlds.platform.system.Platform; @@ -29,7 +30,6 @@ import com.swirlds.platform.system.SoftwareVersion; import com.swirlds.platform.system.address.AddressBook; import com.swirlds.platform.system.events.Event; -import com.swirlds.state.State; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.util.function.Consumer; @@ -37,7 +37,7 @@ /** * Implements the major lifecycle events for Hedera Services by delegating to a Hedera instance. */ -public class StateLifecyclesImpl implements StateLifecycles { +public class StateLifecyclesImpl implements StateLifecycles { private final Hedera hedera; public StateLifecyclesImpl(@NonNull final Hedera hedera) { @@ -47,7 +47,7 @@ public StateLifecyclesImpl(@NonNull final Hedera hedera) { @Override public void onPreHandle( @NonNull final Event event, - @NonNull final State state, + @NonNull final PlatformMerkleStateRoot state, @NonNull Consumer> stateSignatureTransactionCallback) { hedera.onPreHandle(event, state, stateSignatureTransactionCallback); } @@ -55,13 +55,13 @@ public void onPreHandle( @Override public void onHandleConsensusRound( @NonNull final Round round, - @NonNull final State state, + @NonNull final PlatformMerkleStateRoot state, @NonNull Consumer> stateSignatureTxnCallback) { hedera.onHandleConsensusRound(round, state, stateSignatureTxnCallback); } @Override - public void onSealConsensusRound(@NonNull final Round round, @NonNull final State state) { + public void onSealConsensusRound(@NonNull final Round round, @NonNull final PlatformMerkleStateRoot state) { requireNonNull(state); requireNonNull(round); hedera.onSealConsensusRound(round, state); @@ -69,7 +69,7 @@ public void onSealConsensusRound(@NonNull final Round round, @NonNull final Stat @Override public void onStateInitialized( - @NonNull final State state, + @NonNull final PlatformMerkleStateRoot state, @NonNull final Platform platform, @NonNull final InitTrigger trigger, @Nullable SoftwareVersion previousVersion) { @@ -78,14 +78,14 @@ public void onStateInitialized( @Override public void onUpdateWeight( - @NonNull final State stateRoot, + @NonNull final PlatformMerkleStateRoot stateRoot, @NonNull final AddressBook configAddressBook, @NonNull final PlatformContext context) { // No-op } @Override - public void onNewRecoveredState(@NonNull final State recoveredStateRoot) { + public void onNewRecoveredState(@NonNull final PlatformMerkleStateRoot recoveredStateRoot) { hedera.onNewRecoveredState(recoveredStateRoot); } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleWorkflow.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleWorkflow.java index e8b9841eaaf2..8c1fe670ff5b 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleWorkflow.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleWorkflow.java @@ -324,7 +324,7 @@ private void handleEvents( // that have been being computed in background threads. The running hash has to be included in // state, but we want to synchronize with background threads as infrequently as possible. So once per // round is the minimum we can do. Note the BlockStreamManager#endRound() method is called in Hedera's - // implementation of SwirldState#sealConsensusRound(), since the BlockStreamManager cannot do its + // implementation of StateLifecycles#onSealConsensusRound(), since the BlockStreamManager cannot do its // end-of-block work until the platform has finished all its state changes. if (userTransactionsHandled && streamMode != BLOCKS) { blockRecordManager.endRound(state); 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 7d783c6413a1..ec0320db7b9e 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 @@ -62,10 +62,12 @@ import com.hedera.node.config.data.NodesConfig; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.config.api.Configuration; +import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.system.InitTrigger; import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.SoftwareVersion; import com.swirlds.state.lifecycle.info.NetworkInfo; +import com.swirlds.state.merkle.MerkleStateRoot; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.io.IOException; @@ -324,7 +326,7 @@ public Instant now() { /** * Called only once, before handling the first transaction in network history. Externalizes * side effects of genesis setup done in - * {@link com.swirlds.platform.system.SwirldState#init(Platform, InitTrigger, SoftwareVersion)}. + * {@link StateLifecycles#onStateInitialized(MerkleStateRoot, Platform, InitTrigger, SoftwareVersion)}. * * @throws NullPointerException if called more than once */ diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleWorkflow.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleWorkflow.java index c4736a045184..4cc83eb09196 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleWorkflow.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleWorkflow.java @@ -114,8 +114,8 @@ default PreHandleResult getCurrentPreHandleResult( if (metadata instanceof PreHandleResult result) { previousResult = result; } else { - // This should be impossible since the Platform contract guarantees that SwirldState.preHandle() - // is always called before SwirldState.handleTransaction(); and our preHandle() implementation + // This should be impossible since the Platform contract guarantees that StateLifecycles.onPreHandle() + // is always called before StateLifecycles.onHandleTransaction(); and our preHandle() implementation // always sets the metadata to a PreHandleResult log.error( "Received transaction without PreHandleResult metadata from node {} (was {})", diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/StateLifecyclesImplTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/StateLifecyclesImplTest.java index 27890d59df1d..372bd73bc916 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/StateLifecyclesImplTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/StateLifecyclesImplTest.java @@ -24,13 +24,13 @@ import com.hedera.node.app.Hedera; import com.swirlds.common.context.PlatformContext; import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; +import com.swirlds.platform.state.PlatformMerkleStateRoot; import com.swirlds.platform.system.InitTrigger; import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.Round; import com.swirlds.platform.system.address.AddressBook; import com.swirlds.platform.system.events.Event; import com.swirlds.platform.test.fixtures.state.MerkleTestBase; -import com.swirlds.state.merkle.MerkleStateRoot; import java.util.function.Consumer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -56,7 +56,7 @@ class StateLifecyclesImplTest extends MerkleTestBase { private PlatformContext platformContext; @Mock - private MerkleStateRoot merkleStateRoot; + private PlatformMerkleStateRoot merkleStateRoot; private StateLifecyclesImpl subject; diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/merkle/SerializationTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/merkle/SerializationTest.java index 9194fff928ea..11f6ee1ec35c 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/merkle/SerializationTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/merkle/SerializationTest.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. @@ -19,11 +19,9 @@ import static com.hedera.node.app.fixtures.AppTestBase.DEFAULT_CONFIG; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; import com.hedera.node.app.ids.WritableEntityIdStore; import com.hedera.node.app.services.MigrationStateChanges; -import com.hedera.node.app.version.ServicesSoftwareVersion; import com.hedera.node.config.data.HederaConfig; import com.swirlds.base.test.fixtures.time.FakeTime; import com.swirlds.common.config.StateCommonConfig_; @@ -35,6 +33,7 @@ import com.swirlds.common.crypto.config.CryptoConfig; import com.swirlds.common.io.utility.LegacyTemporaryFileBuilder; import com.swirlds.common.merkle.crypto.MerkleCryptographyFactory; +import com.swirlds.common.metrics.noop.NoOpMetrics; import com.swirlds.common.test.fixtures.platform.TestPlatformContextBuilder; import com.swirlds.config.api.Configuration; import com.swirlds.config.extensions.sources.SimpleConfigSource; @@ -44,8 +43,6 @@ import com.swirlds.platform.config.StateConfig_; import com.swirlds.platform.state.PlatformMerkleStateRoot; import com.swirlds.platform.state.signed.SignedState; -import com.swirlds.platform.system.InitTrigger; -import com.swirlds.platform.system.Platform; import com.swirlds.platform.test.fixtures.state.MerkleTestBase; import com.swirlds.platform.test.fixtures.state.RandomSignedStateGenerator; import com.swirlds.state.lifecycle.MigrationContext; @@ -234,9 +231,7 @@ void snapshot() throws IOException { .withTime(new FakeTime()) .build(); - Platform mockPlatform = mock(Platform.class); - when(mockPlatform.getContext()).thenReturn(context); - originalTree.init(mockPlatform, InitTrigger.RESTART, new ServicesSoftwareVersion(schemaV1.getVersion())); + originalTree.init(context.getTime(), new NoOpMetrics(), merkleCryptography); // prepare the tree and create a snapshot originalTree.copy(); diff --git a/hedera-node/hedera-app/src/testFixtures/java/com/hedera/node/app/fixtures/state/FakePlatform.java b/hedera-node/hedera-app/src/testFixtures/java/com/hedera/node/app/fixtures/state/FakePlatform.java index d4e262e6b40c..895f6a6b2f11 100644 --- a/hedera-node/hedera-app/src/testFixtures/java/com/hedera/node/app/fixtures/state/FakePlatform.java +++ b/hedera-node/hedera-app/src/testFixtures/java/com/hedera/node/app/fixtures/state/FakePlatform.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. @@ -38,9 +38,9 @@ import com.swirlds.config.api.Configuration; import com.swirlds.platform.roster.RosterRetriever; import com.swirlds.platform.system.Platform; -import com.swirlds.platform.system.SwirldState; import com.swirlds.platform.system.address.AddressBook; import com.swirlds.platform.test.fixtures.addressbook.RandomAddressBuilder; +import com.swirlds.state.merkle.MerkleStateRoot; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.List; import java.util.Random; @@ -134,7 +134,8 @@ public NodeId getSelfId() { } @Override - public AutoCloseableWrapper getLatestImmutableState(@NonNull String s) { + @NonNull + public AutoCloseableWrapper getLatestImmutableState(String reason) { return null; } diff --git a/hedera-node/hedera-app/src/testFixtures/java/module-info.java b/hedera-node/hedera-app/src/testFixtures/java/module-info.java index 611c7b509bcc..ceeac96459bc 100644 --- a/hedera-node/hedera-app/src/testFixtures/java/module-info.java +++ b/hedera-node/hedera-app/src/testFixtures/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. + */ + module com.hedera.node.app.test.fixtures { exports com.hedera.node.app.fixtures.state; @@ -10,16 +26,17 @@ requires transitive com.swirlds.platform.core; requires transitive com.swirlds.state.api.test.fixtures; requires transitive com.swirlds.state.api; + requires transitive com.swirlds.state.impl; requires com.hedera.node.app.hapi.utils; requires com.hedera.node.app.service.token; requires com.hedera.node.app.spi; requires com.hedera.node.config.test.fixtures; requires com.hedera.node.config; + requires com.hedera.pbj.runtime; requires com.swirlds.base; requires com.swirlds.config.extensions.test.fixtures; requires com.swirlds.platform.core.test.fixtures; requires com.swirlds.state.impl.test.fixtures; - requires com.hedera.pbj.runtime; requires org.apache.logging.log4j; requires org.assertj.core; requires static com.github.spotbugs.annotations; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/AbstractFakePlatform.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/AbstractFakePlatform.java index 890c3174390a..b10104239a9f 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/AbstractFakePlatform.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/AbstractFakePlatform.java @@ -28,7 +28,7 @@ import com.swirlds.metrics.api.Metrics; import com.swirlds.platform.listeners.PlatformStatusChangeNotification; import com.swirlds.platform.system.Platform; -import com.swirlds.platform.system.SwirldState; +import com.swirlds.state.merkle.MerkleStateRoot; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.atomic.AtomicLong; @@ -91,9 +91,9 @@ public NodeId getSelfId() { return selfId; } - @NonNull @Override - public AutoCloseableWrapper getLatestImmutableState(@NonNull String reason) { + public @NonNull AutoCloseableWrapper getLatestImmutableState( + @NonNull String reason) { throw new UnsupportedOperationException("Not used by Hedera"); } diff --git a/platform-sdk/platform-apps/demos/CryptocurrencyDemo/src/main/java/com/swirlds/demo/crypto/CryptocurrencyDemoMain.java b/platform-sdk/platform-apps/demos/CryptocurrencyDemo/src/main/java/com/swirlds/demo/crypto/CryptocurrencyDemoMain.java index 6586f7266f42..002b581682e1 100644 --- a/platform-sdk/platform-apps/demos/CryptocurrencyDemo/src/main/java/com/swirlds/demo/crypto/CryptocurrencyDemoMain.java +++ b/platform-sdk/platform-apps/demos/CryptocurrencyDemo/src/main/java/com/swirlds/demo/crypto/CryptocurrencyDemoMain.java @@ -41,7 +41,7 @@ import com.swirlds.common.utility.AutoCloseableWrapper; import com.swirlds.metrics.api.Metrics; import com.swirlds.platform.Browser; -import com.swirlds.platform.state.PlatformMerkleStateRoot; +import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.system.BasicSoftwareVersion; import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.SwirldMain; @@ -55,14 +55,14 @@ * ask or a bid on a stock, offering to sell or buy, respectively, a single share at a random price between 1 and 127 * cents (inclusive). */ -public class CryptocurrencyDemoMain implements SwirldMain { +public class CryptocurrencyDemoMain implements SwirldMain { static { try { ConstructableRegistry constructableRegistry = ConstructableRegistry.getInstance(); constructableRegistry.registerConstructable(new ClassConstructorPair(CryptocurrencyDemoState.class, () -> { - CryptocurrencyDemoState cryptocurrencyDemoState = new CryptocurrencyDemoState( - FAKE_MERKLE_STATE_LIFECYCLES, version -> new BasicSoftwareVersion(version.major())); + CryptocurrencyDemoState cryptocurrencyDemoState = + new CryptocurrencyDemoState(version -> new BasicSoftwareVersion(version.major())); return cryptocurrencyDemoState; })); registerMerkleStateRootClassIds(); @@ -199,16 +199,27 @@ public void run() { } } + /** + * {@inheritDoc} + */ @Override @NonNull - public PlatformMerkleStateRoot newMerkleStateRoot() { - final PlatformMerkleStateRoot state = new CryptocurrencyDemoState( - FAKE_MERKLE_STATE_LIFECYCLES, - version -> new BasicSoftwareVersion(softwareVersion.getSoftwareVersion())); + public CryptocurrencyDemoState newMerkleStateRoot() { + final CryptocurrencyDemoState state = + new CryptocurrencyDemoState(version -> new BasicSoftwareVersion(softwareVersion.getSoftwareVersion())); FAKE_MERKLE_STATE_LIFECYCLES.initStates(state); return state; } + /** + * {@inheritDoc} + */ + @Override + @NonNull + public StateLifecycles newStateLifecycles() { + return new CryptocurrencyDemoStateLifecycles(); + } + /** * {@inheritDoc} */ diff --git a/platform-sdk/platform-apps/demos/CryptocurrencyDemo/src/main/java/com/swirlds/demo/crypto/CryptocurrencyDemoState.java b/platform-sdk/platform-apps/demos/CryptocurrencyDemo/src/main/java/com/swirlds/demo/crypto/CryptocurrencyDemoState.java index 9cad75a6fadf..1e6eb65b3cf9 100644 --- a/platform-sdk/platform-apps/demos/CryptocurrencyDemo/src/main/java/com/swirlds/demo/crypto/CryptocurrencyDemoState.java +++ b/platform-sdk/platform-apps/demos/CryptocurrencyDemo/src/main/java/com/swirlds/demo/crypto/CryptocurrencyDemoState.java @@ -28,31 +28,19 @@ import com.hedera.hapi.node.base.SemanticVersion; import com.hedera.hapi.node.state.roster.RosterEntry; -import com.hedera.hapi.platform.event.StateSignatureTransaction; -import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.common.constructable.ConstructableIgnored; import com.swirlds.common.platform.NodeId; -import com.swirlds.platform.SwirldsPlatform; -import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.roster.RosterUtils; import com.swirlds.platform.state.PlatformMerkleStateRoot; -import com.swirlds.platform.state.PlatformStateModifier; -import com.swirlds.platform.state.StateLifecycles; -import com.swirlds.platform.system.InitTrigger; import com.swirlds.platform.system.Platform; -import com.swirlds.platform.system.Round; import com.swirlds.platform.system.SoftwareVersion; -import com.swirlds.platform.system.transaction.Transaction; import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.Nullable; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Random; import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Consumer; import java.util.function.Function; /** @@ -93,7 +81,7 @@ private static class ClassVersion { * the first byte of a transaction is the ordinal of one of these four: do not delete any of these or * change the order (and add new ones only to the end) */ - public static enum TransType { + public enum TransType { slow, fast, bid, @@ -104,8 +92,6 @@ public static enum TransType { public static final int NUM_STOCKS = 10; /** remember the last MAX_TRADES trades that occurred. */ private static final int MAX_TRADES = 200; - /** the platform running this app */ - private SwirldsPlatform platform = null; //////////////////////////////////////////////////// // the following are the shared state: @@ -137,15 +123,12 @@ public static enum TransType { //////////////////////////////////////////////////// - public CryptocurrencyDemoState( - @NonNull final StateLifecycles lifecycles, - @NonNull final Function versionFactory) { - super(lifecycles, versionFactory); + public CryptocurrencyDemoState(@NonNull final Function versionFactory) { + super(versionFactory); } private CryptocurrencyDemoState(final CryptocurrencyDemoState sourceState) { super(sourceState); - this.platform = sourceState.platform; this.tickerSymbol = sourceState.tickerSymbol.clone(); this.wallet = new HashMap<>(sourceState.wallet); this.shares = new HashMap<>(); @@ -207,59 +190,8 @@ public synchronized CryptocurrencyDemoState copy() { return new CryptocurrencyDemoState(this); } - @Override - public void handleConsensusRound( - @NonNull final Round round, - @NonNull final PlatformStateModifier platformState, - @NonNull final Consumer> stateSignatureTransaction) { - throwIfImmutable(); - round.forEachEventTransaction((event, transaction) -> handleTransaction(event.getCreatorId(), transaction)); - } - - /** - * {@inheritDoc} - * - * The matching algorithm for any given stock is as follows. The first bid or ask for a stock is - * remembered. Then, if there is a higher bid or lower ask, it is remembered, replacing the earlier one. - * Eventually, there will be a bid that is equal to or greater than the ask. At that point, they are - * matched, and a trade occurs, selling one share at the average of the bid and ask. Then the stored bid - * and ask are erased, and it goes back to waiting for a bid or ask to remember. - *

    - * If a member tries to sell a stock for which they own no shares, or if they try to buy a stock at a - * price higher than the amount of money they currently have, then their bid/ask for that stock will not - * be stored. - *

    - * A transaction is 1 or 3 bytes: - * - *

    -     * {SLOW} = run slowly
    -     * {FAST} = run quickly
    -     * {BID,s,p} = bid to buy 1 share of stock s at p cents (where 0 <= p <= 127)
    -     * {ASK,s,p} = ask to sell 1 share of stock s at p cents (where 1 <= p <= 127)
    -     * 
    - */ - private void handleTransaction(@NonNull final NodeId id, @NonNull final Transaction transaction) { - Objects.requireNonNull(id, "id must not be null"); - Objects.requireNonNull(transaction, "transaction must not be null"); - if (transaction.isSystem()) { - return; - } - final Bytes contents = transaction.getApplicationTransaction(); - if (contents.length() < 3) { - return; - } - if (contents.getByte(0) == TransType.slow.ordinal() || contents.getByte(0) == TransType.fast.ordinal()) { - return; - } - final int askBid = contents.getByte(0); - final int tradeStock = contents.getByte(1); - int tradePrice = contents.getByte(2); - - if (tradePrice < 1 || tradePrice > 127) { - return; // all asks and bids must be in the range 1 to 127 - } - - if (askBid == TransType.ask.ordinal()) { // it is an ask + void handleTransaction(NodeId id, int askBid, int tradeStock, int tradePrice) { + if (askBid == CryptocurrencyDemoState.TransType.ask.ordinal()) { // it is an ask // if they're trying to sell something they don't have, then ignore it if (shares.get(id).get(tradeStock).get() == 0) { return; @@ -341,7 +273,7 @@ private void handleTransaction(@NonNull final NodeId id, @NonNull final Transact /** * Do setup at genesis */ - public void genesisInit() { + void genesisInit(@NonNull final Platform platform) { tickerSymbol = new String[NUM_STOCKS]; wallet = new HashMap<>(); shares = new HashMap<>(); @@ -380,22 +312,6 @@ public void genesisInit() { } } - /** - * {@inheritDoc} - */ - @Override - public void init( - @NonNull final Platform platform, - @NonNull final InitTrigger trigger, - @Nullable final SoftwareVersion previousSoftwareVersion) { - super.init(platform, trigger, previousSoftwareVersion); - - this.platform = (SwirldsPlatform) platform; - if (trigger == InitTrigger.GENESIS) { - genesisInit(); - } - } - @Override public long getClassId() { return CLASS_ID; diff --git a/platform-sdk/platform-apps/demos/CryptocurrencyDemo/src/main/java/com/swirlds/demo/crypto/CryptocurrencyDemoStateLifecycles.java b/platform-sdk/platform-apps/demos/CryptocurrencyDemo/src/main/java/com/swirlds/demo/crypto/CryptocurrencyDemoStateLifecycles.java new file mode 100644 index 000000000000..a4a08a20bc8f --- /dev/null +++ b/platform-sdk/platform-apps/demos/CryptocurrencyDemo/src/main/java/com/swirlds/demo/crypto/CryptocurrencyDemoStateLifecycles.java @@ -0,0 +1,136 @@ +/* + * 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.swirlds.demo.crypto; + +import com.hedera.hapi.platform.event.StateSignatureTransaction; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.common.context.PlatformContext; +import com.swirlds.common.platform.NodeId; +import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; +import com.swirlds.platform.state.StateLifecycles; +import com.swirlds.platform.system.InitTrigger; +import com.swirlds.platform.system.Platform; +import com.swirlds.platform.system.Round; +import com.swirlds.platform.system.SoftwareVersion; +import com.swirlds.platform.system.address.AddressBook; +import com.swirlds.platform.system.events.Event; +import com.swirlds.platform.system.transaction.Transaction; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Objects; +import java.util.function.Consumer; + +/** + * This class handles the lifecycle events for the {@link CryptocurrencyDemoState}. + */ +public class CryptocurrencyDemoStateLifecycles implements StateLifecycles { + + @Override + public void onStateInitialized( + @NonNull CryptocurrencyDemoState state, + @NonNull Platform platform, + @NonNull InitTrigger trigger, + @Nullable SoftwareVersion previousVersion) { + if (trigger == InitTrigger.GENESIS) { + state.genesisInit(platform); + } + } + + @Override + public void onHandleConsensusRound( + @NonNull Round round, + @NonNull CryptocurrencyDemoState state, + @NonNull Consumer> stateSignatureTransactionCallback) { + state.throwIfImmutable(); + round.forEachEventTransaction( + (event, transaction) -> handleTransaction(event.getCreatorId(), transaction, state)); + } + + /** + * The matching algorithm for any given stock is as follows. The first bid or ask for a stock is + * remembered. Then, if there is a higher bid or lower ask, it is remembered, replacing the earlier one. + * Eventually, there will be a bid that is equal to or greater than the ask. At that point, they are + * matched, and a trade occurs, selling one share at the average of the bid and ask. Then the stored bid + * and ask are erased, and it goes back to waiting for a bid or ask to remember. + *

    + * If a member tries to sell a stock for which they own no shares, or if they try to buy a stock at a + * price higher than the amount of money they currently have, then their bid/ask for that stock will not + * be stored. + *

    + * A transaction is 1 or 3 bytes: + * + *

    +     * {SLOW} = run slowly
    +     * {FAST} = run quickly
    +     * {BID,s,p} = bid to buy 1 share of stock s at p cents (where 0 <= p <= 127)
    +     * {ASK,s,p} = ask to sell 1 share of stock s at p cents (where 1 <= p <= 127)
    +     * 
    + */ + private void handleTransaction( + @NonNull final NodeId id, + @NonNull final Transaction transaction, + @NonNull final CryptocurrencyDemoState state) { + Objects.requireNonNull(id, "id must not be null"); + Objects.requireNonNull(transaction, "transaction must not be null"); + if (transaction.isSystem()) { + return; + } + final Bytes contents = transaction.getApplicationTransaction(); + if (contents.length() < 3) { + return; + } + if (contents.getByte(0) == CryptocurrencyDemoState.TransType.slow.ordinal() + || contents.getByte(0) == CryptocurrencyDemoState.TransType.fast.ordinal()) { + return; + } + final int askBid = contents.getByte(0); + final int tradeStock = contents.getByte(1); + int tradePrice = contents.getByte(2); + + if (tradePrice < 1 || tradePrice > 127) { + return; // all asks and bids must be in the range 1 to 127 + } + + state.handleTransaction(id, askBid, tradeStock, tradePrice); + } + + @Override + public void onPreHandle( + @NonNull Event event, + @NonNull CryptocurrencyDemoState state, + @NonNull Consumer> stateSignatureTransactionCallback) { + // no-op + } + + @Override + public void onSealConsensusRound(@NonNull Round round, @NonNull CryptocurrencyDemoState state) { + // no-op + } + + @Override + public void onUpdateWeight( + @NonNull CryptocurrencyDemoState state, + @NonNull AddressBook configAddressBook, + @NonNull PlatformContext context) { + // no-op + } + + @Override + public void onNewRecoveredState(@NonNull CryptocurrencyDemoState recoveredState) { + // no-op + } +} diff --git a/platform-sdk/platform-apps/demos/HelloSwirldDemo/src/main/java/com/swirlds/demo/hello/HelloSwirldDemoMain.java b/platform-sdk/platform-apps/demos/HelloSwirldDemo/src/main/java/com/swirlds/demo/hello/HelloSwirldDemoMain.java index 7d8f9730c706..6b32b8896494 100644 --- a/platform-sdk/platform-apps/demos/HelloSwirldDemo/src/main/java/com/swirlds/demo/hello/HelloSwirldDemoMain.java +++ b/platform-sdk/platform-apps/demos/HelloSwirldDemo/src/main/java/com/swirlds/demo/hello/HelloSwirldDemoMain.java @@ -41,7 +41,7 @@ import com.swirlds.platform.listeners.PlatformStatusChangeListener; import com.swirlds.platform.listeners.PlatformStatusChangeNotification; import com.swirlds.platform.roster.RosterUtils; -import com.swirlds.platform.state.PlatformMerkleStateRoot; +import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.system.BasicSoftwareVersion; import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.SwirldMain; @@ -54,14 +54,14 @@ * into a busy loop (checking once a second) to see when the state gets the transaction. When it does, it * prints it, too. */ -public class HelloSwirldDemoMain implements SwirldMain { +public class HelloSwirldDemoMain implements SwirldMain { static { try { ConstructableRegistry constructableRegistry = ConstructableRegistry.getInstance(); constructableRegistry.registerConstructable(new ClassConstructorPair(HelloSwirldDemoState.class, () -> { - HelloSwirldDemoState helloSwirldDemoState = new HelloSwirldDemoState( - FAKE_MERKLE_STATE_LIFECYCLES, version -> new BasicSoftwareVersion(version.major())); + HelloSwirldDemoState helloSwirldDemoState = + new HelloSwirldDemoState(version -> new BasicSoftwareVersion(version.major())); return helloSwirldDemoState; })); registerMerkleStateRootClassIds(); @@ -129,14 +129,19 @@ public void run() { @NonNull @Override - public PlatformMerkleStateRoot newMerkleStateRoot() { - final PlatformMerkleStateRoot state = new HelloSwirldDemoState( - FAKE_MERKLE_STATE_LIFECYCLES, - version -> new BasicSoftwareVersion(softwareVersion.getSoftwareVersion())); + public HelloSwirldDemoState newMerkleStateRoot() { + final HelloSwirldDemoState state = + new HelloSwirldDemoState(version -> new BasicSoftwareVersion(softwareVersion.getSoftwareVersion())); FAKE_MERKLE_STATE_LIFECYCLES.initStates(state); return state; } + @NonNull + @Override + public StateLifecycles newStateLifecycles() { + return new HelloSwirldDemoStateLifecycles(); + } + private void platformStatusChange(final PlatformStatusChangeNotification notification) { final PlatformStatus newStatus = notification.getNewStatus(); if (PlatformStatus.ACTIVE.equals(newStatus)) { diff --git a/platform-sdk/platform-apps/demos/HelloSwirldDemo/src/main/java/com/swirlds/demo/hello/HelloSwirldDemoState.java b/platform-sdk/platform-apps/demos/HelloSwirldDemo/src/main/java/com/swirlds/demo/hello/HelloSwirldDemoState.java index d5767c140a9c..ae3365ad21f4 100644 --- a/platform-sdk/platform-apps/demos/HelloSwirldDemo/src/main/java/com/swirlds/demo/hello/HelloSwirldDemoState.java +++ b/platform-sdk/platform-apps/demos/HelloSwirldDemo/src/main/java/com/swirlds/demo/hello/HelloSwirldDemoState.java @@ -27,20 +27,12 @@ */ import com.hedera.hapi.node.base.SemanticVersion; -import com.hedera.hapi.platform.event.StateSignatureTransaction; import com.swirlds.common.constructable.ConstructableIgnored; -import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.state.PlatformMerkleStateRoot; -import com.swirlds.platform.state.PlatformStateModifier; -import com.swirlds.platform.state.StateLifecycles; -import com.swirlds.platform.system.Round; import com.swirlds.platform.system.SoftwareVersion; -import com.swirlds.platform.system.transaction.Transaction; import edu.umd.cs.findbugs.annotations.NonNull; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; -import java.util.function.Consumer; import java.util.function.Function; /** @@ -96,10 +88,8 @@ public synchronized String toString() { // /////////////////////////////////////////////////////////////////// - public HelloSwirldDemoState( - @NonNull final StateLifecycles lifecycles, - @NonNull final Function versionFactory) { - super(lifecycles, versionFactory); + public HelloSwirldDemoState(@NonNull final Function versionFactory) { + super(versionFactory); } private HelloSwirldDemoState(final HelloSwirldDemoState sourceState) { @@ -107,15 +97,6 @@ private HelloSwirldDemoState(final HelloSwirldDemoState sourceState) { this.strings = new ArrayList<>(sourceState.strings); } - @Override - public synchronized void handleConsensusRound( - @NonNull final Round round, - @NonNull final PlatformStateModifier platformState, - @NonNull final Consumer> stateSignatureTransaction) { - throwIfImmutable(); - round.forEachTransaction(this::handleTransaction); - } - @Override public synchronized HelloSwirldDemoState copy() { throwIfImmutable(); @@ -123,13 +104,6 @@ public synchronized HelloSwirldDemoState copy() { return new HelloSwirldDemoState(this); } - private void handleTransaction(final Transaction transaction) { - if (transaction.isSystem()) { - return; - } - strings.add(new String(transaction.getApplicationTransaction().toByteArray(), StandardCharsets.UTF_8)); - } - @Override public long getClassId() { return CLASS_ID; diff --git a/platform-sdk/platform-apps/demos/HelloSwirldDemo/src/main/java/com/swirlds/demo/hello/HelloSwirldDemoStateLifecycles.java b/platform-sdk/platform-apps/demos/HelloSwirldDemo/src/main/java/com/swirlds/demo/hello/HelloSwirldDemoStateLifecycles.java new file mode 100644 index 000000000000..b6bea56d2429 --- /dev/null +++ b/platform-sdk/platform-apps/demos/HelloSwirldDemo/src/main/java/com/swirlds/demo/hello/HelloSwirldDemoStateLifecycles.java @@ -0,0 +1,91 @@ +/* + * 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.swirlds.demo.hello; + +import com.hedera.hapi.platform.event.StateSignatureTransaction; +import com.swirlds.common.context.PlatformContext; +import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; +import com.swirlds.platform.state.StateLifecycles; +import com.swirlds.platform.system.InitTrigger; +import com.swirlds.platform.system.Platform; +import com.swirlds.platform.system.Round; +import com.swirlds.platform.system.SoftwareVersion; +import com.swirlds.platform.system.address.AddressBook; +import com.swirlds.platform.system.events.Event; +import com.swirlds.platform.system.transaction.Transaction; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.charset.StandardCharsets; +import java.util.function.Consumer; + +/** + * This class handles the lifecycle events for the {@link HelloSwirldDemoState}. + */ +public class HelloSwirldDemoStateLifecycles implements StateLifecycles { + + @Override + public void onHandleConsensusRound( + @NonNull Round round, + @NonNull HelloSwirldDemoState state, + @NonNull Consumer> stateSignatureTransactionCallback) { + state.throwIfImmutable(); + round.forEachTransaction(v -> handleTransaction(v, state)); + } + + private void handleTransaction(final Transaction transaction, HelloSwirldDemoState state) { + if (transaction.isSystem()) { + return; + } + state.getStrings() + .add(new String(transaction.getApplicationTransaction().toByteArray(), StandardCharsets.UTF_8)); + } + + @Override + public void onPreHandle( + @NonNull Event event, + @NonNull HelloSwirldDemoState state, + @NonNull Consumer> stateSignatureTransactionCallback) { + // no-op + } + + @Override + public void onSealConsensusRound(@NonNull Round round, @NonNull HelloSwirldDemoState state) { + // no-op + } + + @Override + public void onStateInitialized( + @NonNull HelloSwirldDemoState state, + @NonNull Platform platform, + @NonNull InitTrigger trigger, + @Nullable SoftwareVersion previousVersion) { + // no-op + } + + @Override + public void onUpdateWeight( + @NonNull HelloSwirldDemoState state, + @NonNull AddressBook configAddressBook, + @NonNull PlatformContext context) { + // no-op + } + + @Override + public void onNewRecoveredState(@NonNull HelloSwirldDemoState recoveredState) { + // no-op// no-op + } +} diff --git a/platform-sdk/platform-apps/demos/StatsDemo/src/main/java/com/swirlds/demo/stats/StatsDemoMain.java b/platform-sdk/platform-apps/demos/StatsDemo/src/main/java/com/swirlds/demo/stats/StatsDemoMain.java index cb82aa36018a..f17e1b94c585 100644 --- a/platform-sdk/platform-apps/demos/StatsDemo/src/main/java/com/swirlds/demo/stats/StatsDemoMain.java +++ b/platform-sdk/platform-apps/demos/StatsDemo/src/main/java/com/swirlds/demo/stats/StatsDemoMain.java @@ -44,7 +44,8 @@ import com.swirlds.metrics.api.Metrics; import com.swirlds.platform.Browser; import com.swirlds.platform.ParameterProvider; -import com.swirlds.platform.state.PlatformMerkleStateRoot; +import com.swirlds.platform.state.NoOpStateLifecycles; +import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.system.BasicSoftwareVersion; import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.SwirldMain; @@ -63,7 +64,7 @@ * screen, and also saves them to disk in a comma separated value (.csv) file. Each transaction is 100 * random bytes. So StatsDemoState.handleTransaction doesn't actually do anything. */ -public class StatsDemoMain implements SwirldMain { +public class StatsDemoMain implements SwirldMain { // the first four come from the parameters in the config.txt file /** should this run with no windows? */ @@ -94,8 +95,8 @@ public class StatsDemoMain implements SwirldMain { try { ConstructableRegistry constructableRegistry = ConstructableRegistry.getInstance(); constructableRegistry.registerConstructable(new ClassConstructorPair(StatsDemoState.class, () -> { - StatsDemoState statsDemoState = new StatsDemoState( - FAKE_MERKLE_STATE_LIFECYCLES, version -> new BasicSoftwareVersion(version.major())); + StatsDemoState statsDemoState = + new StatsDemoState(version -> new BasicSoftwareVersion(version.major())); return statsDemoState; })); registerMerkleStateRootClassIds(); @@ -315,14 +316,19 @@ private String valueAsString(final Metric metric) { @NonNull @Override - public PlatformMerkleStateRoot newMerkleStateRoot() { - final PlatformMerkleStateRoot state = new StatsDemoState( - FAKE_MERKLE_STATE_LIFECYCLES, - version -> new BasicSoftwareVersion(softwareVersion.getSoftwareVersion())); + public StatsDemoState newMerkleStateRoot() { + final StatsDemoState state = + new StatsDemoState(version -> new BasicSoftwareVersion(softwareVersion.getSoftwareVersion())); FAKE_MERKLE_STATE_LIFECYCLES.initStates(state); return state; } + @NonNull + @Override + public StateLifecycles newStateLifecycles() { + return NoOpStateLifecycles.NO_OP_STATE_LIFECYCLES; + } + /** * {@inheritDoc} */ diff --git a/platform-sdk/platform-apps/demos/StatsDemo/src/main/java/com/swirlds/demo/stats/StatsDemoState.java b/platform-sdk/platform-apps/demos/StatsDemo/src/main/java/com/swirlds/demo/stats/StatsDemoState.java index 9fc167600310..fbfd45b5d3ee 100644 --- a/platform-sdk/platform-apps/demos/StatsDemo/src/main/java/com/swirlds/demo/stats/StatsDemoState.java +++ b/platform-sdk/platform-apps/demos/StatsDemo/src/main/java/com/swirlds/demo/stats/StatsDemoState.java @@ -27,16 +27,10 @@ */ import com.hedera.hapi.node.base.SemanticVersion; -import com.hedera.hapi.platform.event.StateSignatureTransaction; import com.swirlds.common.constructable.ConstructableIgnored; -import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.state.PlatformMerkleStateRoot; -import com.swirlds.platform.state.PlatformStateModifier; -import com.swirlds.platform.state.StateLifecycles; -import com.swirlds.platform.system.Round; import com.swirlds.platform.system.SoftwareVersion; import edu.umd.cs.findbugs.annotations.NonNull; -import java.util.function.Consumer; import java.util.function.Function; /** @@ -68,22 +62,14 @@ private static class ClassVersion { private static final long CLASS_ID = 0xc550a1cd94e91ca3L; - public StatsDemoState( - @NonNull final StateLifecycles lifecycles, - @NonNull final Function versionFactory) { - super(lifecycles, versionFactory); + public StatsDemoState(@NonNull final Function versionFactory) { + super(versionFactory); } private StatsDemoState(final StatsDemoState sourceState) { super(sourceState); } - @Override - public void handleConsensusRound( - @NonNull final Round round, - @NonNull final PlatformStateModifier platformState, - @NonNull final Consumer> stateSignatureTransaction) {} - /** * {@inheritDoc} */ diff --git a/platform-sdk/platform-apps/tests/AddressBookTestingTool/src/main/java/com/swirlds/demo/addressbook/AddressBookTestingToolConfig.java b/platform-sdk/platform-apps/tests/AddressBookTestingTool/src/main/java/com/swirlds/demo/addressbook/AddressBookTestingToolConfig.java index ad25c2520202..03641319edce 100644 --- a/platform-sdk/platform-apps/tests/AddressBookTestingTool/src/main/java/com/swirlds/demo/addressbook/AddressBookTestingToolConfig.java +++ b/platform-sdk/platform-apps/tests/AddressBookTestingTool/src/main/java/com/swirlds/demo/addressbook/AddressBookTestingToolConfig.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. @@ -25,8 +25,7 @@ * * @param softwareVersion The integer value of the software version of the AddressBookTestingToolMain SwirldMain * application. - * @param weightingBehavior The integer value of the weighting behavior of the AddressBookTestingToolState - * SwirldState. + * @param weightingBehavior The integer value of the weighting behavior of the AddressBookTestingToolState. * @param testScenario The string value of the test scenario being run for validation. This must match an * enumerated value in {@link AddressBookTestScenario}. * @param freezeAfterGenesis if not 0, describes a moment in time, relative to genesis, when a freeze is scheduled. diff --git a/platform-sdk/platform-apps/tests/AddressBookTestingTool/src/main/java/com/swirlds/demo/addressbook/AddressBookTestingToolMain.java b/platform-sdk/platform-apps/tests/AddressBookTestingTool/src/main/java/com/swirlds/demo/addressbook/AddressBookTestingToolMain.java index 90ab90a8e10b..1b4ea622e968 100644 --- a/platform-sdk/platform-apps/tests/AddressBookTestingTool/src/main/java/com/swirlds/demo/addressbook/AddressBookTestingToolMain.java +++ b/platform-sdk/platform-apps/tests/AddressBookTestingTool/src/main/java/com/swirlds/demo/addressbook/AddressBookTestingToolMain.java @@ -30,7 +30,7 @@ import com.swirlds.config.api.Configuration; import com.swirlds.config.api.ConfigurationBuilder; import com.swirlds.platform.config.DefaultConfiguration; -import com.swirlds.platform.state.PlatformMerkleStateRoot; +import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.system.BasicSoftwareVersion; import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.SwirldMain; @@ -58,7 +58,7 @@ *
  • * */ -public class AddressBookTestingToolMain implements SwirldMain { +public class AddressBookTestingToolMain implements SwirldMain { /** The logger for this class. */ private static final Logger logger = LogManager.getLogger(AddressBookTestingToolMain.class); @@ -67,12 +67,9 @@ public class AddressBookTestingToolMain implements SwirldMain { try { logger.info(STARTUP.getMarker(), "Registering AddressBookTestingToolState with ConstructableRegistry"); ConstructableRegistry constructableRegistry = ConstructableRegistry.getInstance(); - constructableRegistry.registerConstructable( - new ClassConstructorPair(AddressBookTestingToolState.class, () -> { - AddressBookTestingToolState addressBookTestingToolState = new AddressBookTestingToolState( - FAKE_MERKLE_STATE_LIFECYCLES, version -> new BasicSoftwareVersion(version.major())); - return addressBookTestingToolState; - })); + constructableRegistry.registerConstructable(new ClassConstructorPair( + AddressBookTestingToolState.class, + () -> new AddressBookTestingToolState(version -> new BasicSoftwareVersion(version.major())))); registerMerkleStateRootClassIds(); logger.info(STARTUP.getMarker(), "AddressBookTestingToolState is registered with ConstructableRegistry"); } catch (ConstructableRegistryException e) { @@ -129,14 +126,19 @@ public void run() { */ @Override @NonNull - public PlatformMerkleStateRoot newMerkleStateRoot() { - final PlatformMerkleStateRoot state = new AddressBookTestingToolState( - FAKE_MERKLE_STATE_LIFECYCLES, + public AddressBookTestingToolState newMerkleStateRoot() { + final AddressBookTestingToolState state = new AddressBookTestingToolState( version -> new BasicSoftwareVersion(softwareVersion.getSoftwareVersion())); FAKE_MERKLE_STATE_LIFECYCLES.initStates(state); return state; } + @Override + @NonNull + public StateLifecycles newStateLifecycles() { + return new AddressBookTestingToolStateLifecycles(); + } + /** * {@inheritDoc} */ diff --git a/platform-sdk/platform-apps/tests/AddressBookTestingTool/src/main/java/com/swirlds/demo/addressbook/AddressBookTestingToolState.java b/platform-sdk/platform-apps/tests/AddressBookTestingTool/src/main/java/com/swirlds/demo/addressbook/AddressBookTestingToolState.java index 71d302cf9a87..be2695b7dda2 100644 --- a/platform-sdk/platform-apps/tests/AddressBookTestingTool/src/main/java/com/swirlds/demo/addressbook/AddressBookTestingToolState.java +++ b/platform-sdk/platform-apps/tests/AddressBookTestingTool/src/main/java/com/swirlds/demo/addressbook/AddressBookTestingToolState.java @@ -26,54 +26,19 @@ * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. */ -import static com.swirlds.logging.legacy.LogMarker.DEMO_INFO; -import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; import static com.swirlds.logging.legacy.LogMarker.STARTUP; -import static com.swirlds.platform.state.address.AddressBookInitializer.CONFIG_ADDRESS_BOOK_HEADER; -import static com.swirlds.platform.state.address.AddressBookInitializer.CONFIG_ADDRESS_BOOK_USED; -import static com.swirlds.platform.state.address.AddressBookInitializer.STATE_ADDRESS_BOOK_HEADER; -import static com.swirlds.platform.state.address.AddressBookInitializer.STATE_ADDRESS_BOOK_USED; -import static com.swirlds.platform.state.address.AddressBookInitializer.USED_ADDRESS_BOOK_HEADER; import com.hedera.hapi.node.base.SemanticVersion; -import com.hedera.hapi.node.state.roster.Roster; -import com.hedera.hapi.platform.event.StateSignatureTransaction; -import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.common.constructable.ConstructableIgnored; -import com.swirlds.common.context.PlatformContext; -import com.swirlds.common.platform.NodeId; import com.swirlds.common.utility.ByteUtils; -import com.swirlds.common.utility.StackTrace; -import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; -import com.swirlds.platform.config.AddressBookConfig; -import com.swirlds.platform.roster.RosterRetriever; -import com.swirlds.platform.roster.RosterUtils; import com.swirlds.platform.state.PlatformMerkleStateRoot; -import com.swirlds.platform.state.PlatformStateModifier; -import com.swirlds.platform.state.StateLifecycles; -import com.swirlds.platform.state.snapshot.SignedStateFileReader; -import com.swirlds.platform.system.InitTrigger; -import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.Round; import com.swirlds.platform.system.SoftwareVersion; -import com.swirlds.platform.system.address.Address; -import com.swirlds.platform.system.address.AddressBook; -import com.swirlds.platform.system.address.AddressBookUtils; -import com.swirlds.platform.system.events.Event; import com.swirlds.platform.system.transaction.ConsensusTransaction; import com.swirlds.platform.system.transaction.Transaction; import com.swirlds.state.merkle.singleton.StringLeaf; import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.Nullable; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.text.ParseException; -import java.time.Duration; import java.util.Objects; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Function; import org.apache.logging.log4j.LogManager; @@ -87,16 +52,6 @@ public class AddressBookTestingToolState extends PlatformMerkleStateRoot { private static final Logger logger = LogManager.getLogger(AddressBookTestingToolState.class); - /** the suffix for the debug address book */ - private static final String DEBUG = "debug"; - - /** the suffix for the test address book */ - private AddressBookTestingToolConfig testingToolConfig; - /** the address book configuration */ - private AddressBookConfig addressBookConfig; - /** flag indicating if weighting behavior has been logged. */ - private static final AtomicBoolean logWeightingBehavior = new AtomicBoolean(true); - private static class ClassVersion { public static final int ORIGINAL = 1; } @@ -106,15 +61,6 @@ private static class ClassVersion { private static final int RUNNING_SUM_INDEX = 3; private static final int ROUND_HANDLED_INDEX = 4; - private NodeId selfId; - - /** false until the test scenario has been validated, true afterwards. */ - private final AtomicBoolean validationPerformed = new AtomicBoolean(false); - - private Platform platform = null; - - private PlatformContext context = null; - /** * The true "state" of this app. Each transaction is just an integer that gets added to this value. */ @@ -122,26 +68,16 @@ private static class ClassVersion { /** * The number of rounds handled by this app. Is incremented each time - * {@link #handleConsensusRound(Round, PlatformStateModifier)} is called. Note that this may not actually equal the round - * number, since we don't call {@link #handleConsensusRound(Round, PlatformStateModifier)} for rounds with no events. + * {@link AddressBookTestingToolStateLifecycles#onHandleConsensusRound(Round, AddressBookTestingToolState, Consumer)} is called. Note that this may not actually equal the round + * number, since we don't call {@link AddressBookTestingToolStateLifecycles#onHandleConsensusRound(Round, AddressBookTestingToolState, Consumer)} for rounds with no events. * *

    * Affects the hash of this node. */ private long roundsHandled = 0; - /** - * If not zero and we are handling the first round after genesis, configure a freeze this duration later. - *

    - * Does not affect the hash of this node (although actions may be taken based on this info that DO affect the - * hash). - */ - private Duration freezeAfterGenesis = null; - - public AddressBookTestingToolState( - @NonNull final StateLifecycles lifecycles, - @NonNull final Function versionFactory) { - super(lifecycles, versionFactory); + public AddressBookTestingToolState(@NonNull final Function versionFactory) { + super(versionFactory); logger.info(STARTUP.getMarker(), "New State Constructed."); } @@ -151,49 +87,14 @@ public AddressBookTestingToolState( private AddressBookTestingToolState(@NonNull final AddressBookTestingToolState that) { super(that); Objects.requireNonNull(that, "the address book testing tool state to copy cannot be null"); - this.testingToolConfig = that.testingToolConfig; - this.addressBookConfig = that.addressBookConfig; this.runningSum = that.runningSum; - this.selfId = that.selfId; - this.platform = that.platform; - this.context = that.context; - this.validationPerformed.set(that.validationPerformed.get()); this.roundsHandled = that.roundsHandled; - this.freezeAfterGenesis = that.freezeAfterGenesis; } /** - * {@inheritDoc} + * Initializes state fields from the merkle tree children */ - @Override - public synchronized AddressBookTestingToolState copy() { - throwIfImmutable(); - setImmutable(true); - return new AddressBookTestingToolState(this); - } - - /** - * {@inheritDoc} - */ - @Override - public void init( - @NonNull final Platform platform, - @NonNull final InitTrigger trigger, - @Nullable final SoftwareVersion previousSoftwareVersion) { - Objects.requireNonNull(platform, "the platform cannot be null"); - Objects.requireNonNull(trigger, "the init trigger cannot be null"); - addressBookConfig = platform.getContext().getConfiguration().getConfigData(AddressBookConfig.class); - testingToolConfig = platform.getContext().getConfiguration().getConfigData(AddressBookTestingToolConfig.class); - this.freezeAfterGenesis = testingToolConfig.freezeAfterGenesis(); - - this.platform = platform; - this.context = platform.getContext(); - - logger.info(STARTUP.getMarker(), "init called in State."); - throwIfImmutable(); - - this.selfId = platform.getSelfId(); - + void initState() { final StringLeaf runningSumLeaf = getChild(RUNNING_SUM_INDEX); if (runningSumLeaf != null && runningSumLeaf.getLabel() != null) { this.runningSum = Long.parseLong(runningSumLeaf.getLabel()); @@ -204,93 +105,30 @@ public void init( this.roundsHandled = Long.parseLong(roundsHandledLeaf.getLabel()); logger.info(STARTUP.getMarker(), "State initialized with {} rounds handled.", roundsHandled); } + } - // Since this demo State doesn't call Hedera.onStateInitialized() to init States API for all services - // (because it doesn't call super.init(), and the FakeStateLifecycles doesn't do that anyway), - // we need to register PlatformService and RosterService states for the rest of the code to operate - // when an instance of this state is received via reconnect. In any other cases, this call - // should be idempotent. - SignedStateFileReader.registerServiceStates(this); - logger.info(STARTUP.getMarker(), "Registered PlatformService and RosterService states."); + void incrementRoundsHandled() { + roundsHandled++; + setChild(ROUND_HANDLED_INDEX, new StringLeaf(Long.toString(roundsHandled))); } - @Override - public void preHandle( - @NonNull final Event event, - @NonNull final Consumer> stateSignatureTransaction) { - event.transactionIterator().forEachRemaining(transaction -> { - // We are not interested in pre-handling any system transactions, as they are - // specific for the platform only.We also don't want to consume deprecated - // EventTransaction.STATE_SIGNATURE_TRANSACTION system transactions in the - // callback,since it's intended to be used only for the new form of encoded system - // transactions in Bytes. Thus, we can directly skip the current - // iteration, if it processes a deprecated system transaction with the - // EventTransaction.STATE_SIGNATURE_TRANSACTION type. - if (transaction.isSystem()) { - return; - } + void incrementRunningSum(final long value) { + runningSum += value; + setChild(RUNNING_SUM_INDEX, new StringLeaf(Long.toString(runningSum))); + } - // We should consume in the callback the new form of system transactions in Bytes - if (areTransactionBytesSystemOnes(transaction)) { - consumeSystemTransaction(transaction, event, stateSignatureTransaction); - } - }); + long getRoundsHandled() { + return roundsHandled; } /** * {@inheritDoc} */ @Override - public void handleConsensusRound( - @NonNull final Round round, - @NonNull final PlatformStateModifier platformState, - @NonNull final Consumer> stateSignatureTransaction) { - Objects.requireNonNull(round, "the round cannot be null"); - Objects.requireNonNull(platformState, "the platform state cannot be null"); + public synchronized AddressBookTestingToolState copy() { throwIfImmutable(); - - if (roundsHandled == 0 && !freezeAfterGenesis.equals(Duration.ZERO)) { - // This is the first round after genesis. - logger.info( - STARTUP.getMarker(), - "Setting freeze time to {} seconds after genesis.", - freezeAfterGenesis.getSeconds()); - platformState.setFreezeTime(round.getConsensusTimestamp().plus(freezeAfterGenesis)); - } - - roundsHandled++; - setChild(ROUND_HANDLED_INDEX, new StringLeaf(Long.toString(roundsHandled))); - - for (final var event : round) { - event.consensusTransactionIterator().forEachRemaining(transaction -> { - // We are not interested in handling any system transactions, as they are - // specific for the platform only.We also don't want to consume deprecated - // EventTransaction.STATE_SIGNATURE_TRANSACTION system transactions in the - // callback, since it's intended to be used only for the new form of encoded system - // transactions in Bytes. Thus, we can directly skip the current - // iteration, if it processes a deprecated system transaction with the - // EventTransaction.STATE_SIGNATURE_TRANSACTION type. - if (transaction.isSystem()) { - return; - } - - // We should consume in the callback the new form of system transactions in Bytes - if (areTransactionBytesSystemOnes(transaction)) { - consumeSystemTransaction(transaction, event, stateSignatureTransaction); - } else { - handleTransaction(transaction); - } - }); - } - - if (!validationPerformed.getAndSet(true)) { - String testScenario = testingToolConfig.testScenario(); - if (validateTestScenario()) { - logger.info(STARTUP.getMarker(), "Test scenario {}: finished without errors.", testScenario); - } else { - logger.error(EXCEPTION.getMarker(), "Test scenario {}: validation failed with errors.", testScenario); - } - } + setImmutable(true); + return new AddressBookTestingToolState(this); } /** @@ -300,31 +138,10 @@ public void handleConsensusRound( * @param transaction the consensus transaction to check * @return true if the transaction bytes are system ones, false otherwise */ - private boolean areTransactionBytesSystemOnes(final Transaction transaction) { + boolean areTransactionBytesSystemOnes(final Transaction transaction) { return transaction.getApplicationTransaction().length() > 4; } - /** - * Converts a transaction to a {@link StateSignatureTransaction} and then consumes it into a callback. - * - * @param transaction the transaction to consume - * @param event the event that contains the transaction - * @param stateSignatureTransactionCallback the callback to call with the system transaction - */ - private void consumeSystemTransaction( - final Transaction transaction, - final Event event, - final Consumer> stateSignatureTransactionCallback) { - try { - final var stateSignatureTransaction = - StateSignatureTransaction.PROTOBUF.parse(transaction.getApplicationTransaction()); - stateSignatureTransactionCallback.accept(new ScopedSystemTransaction<>( - event.getCreatorId(), event.getSoftwareVersion(), stateSignatureTransaction)); - } catch (final com.hedera.pbj.runtime.ParseException e) { - logger.error("Failed to parse StateSignatureTransaction", e); - } - } - /** * Apply a transaction to the state. * @@ -336,7 +153,6 @@ private void handleTransaction(@NonNull final ConsensusTransaction transaction) runningSum += delta; setChild(RUNNING_SUM_INDEX, new StringLeaf(Long.toString(runningSum))); } - /** * {@inheritDoc} */ @@ -357,593 +173,4 @@ public int getVersion() { public int getMinimumSupportedVersion() { return ClassVersion.ORIGINAL; } - - /** - * {@inheritDoc} - */ - @Override - @NonNull - public synchronized AddressBook updateWeight( - @NonNull final AddressBook addressBook, @NonNull final PlatformContext context) { - Objects.requireNonNull(addressBook, "the address book cannot be null"); - this.context = Objects.requireNonNull(context, "the platform context cannot be null"); - final int weightingBehavior = context.getConfiguration() - .getConfigData(AddressBookTestingToolConfig.class) - .weightingBehavior(); - logger.info(DEMO_INFO.getMarker(), "updateWeight called in State. Weighting Behavior: {}", weightingBehavior); - switch (weightingBehavior) { - case 1: - return weightingBehavior1(addressBook); - case 2: - return weightingBehavior2(addressBook); - default: - logger.info( - STARTUP.getMarker(), "Weighting Behavior {}: no change to address book.", weightingBehavior); - return addressBook; - } - } - - /** - * All nodes received 10 weight. - * - * @param addressBook the address book to update. - * @return the updated address book. - */ - @NonNull - private AddressBook weightingBehavior1(@NonNull final AddressBook addressBook) { - if (logWeightingBehavior.get()) { - logger.info(STARTUP.getMarker(), "Weighting Behavior 1: updating all nodes to have 10 weight."); - } - for (final Address address : addressBook) { - addressBook.updateWeight(address.getNodeId(), 10); - } - return addressBook; - } - - /** - * All nodes received weight equal to their nodeId. - * - * @param addressBook the address book to update. - * @return the updated address book. - */ - @NonNull - private AddressBook weightingBehavior2(@NonNull final AddressBook addressBook) { - if (logWeightingBehavior.get()) { - logger.info( - STARTUP.getMarker(), - "Weighting Behavior 2: updating all nodes to have weight equal to their nodeId."); - } - for (final Address address : addressBook) { - addressBook.updateWeight(address.getNodeId(), address.getNodeId().id()); - } - return addressBook; - } - - private boolean validateTestScenario() { - if (platform == null) { - throw new IllegalStateException("platform is null, init has not been called."); - } - logWeightingBehavior.set(false); - final AddressBookTestScenario testScenario = AddressBookTestScenario.valueOf(testingToolConfig.testScenario()); - try { - logger.info(DEMO_INFO.getMarker(), "Validating test scenario {}.", testScenario); - switch (testScenario) { - case GENESIS_FORCE_CONFIG_AB: - return genesisForceUseOfConfigAddressBookTrue(testScenario); - case GENESIS_NORMAL: - return genesisForceUseOfConfigAddressBookFalse(testScenario); - case NO_UPGRADE_USE_SAVED_STATE: - return noSoftwareUpgradeUseSavedStateAddressBook(testScenario); - case NO_UPGRADE_FORCE_CONFIG_AB: - return noSoftwareUpgradeForceUseOfConfigAddressBook(testScenario); - case UPGRADE_WEIGHT_BEHAVIOR_2: - return softwareUpgradeWeightingBehavior2(testScenario); - case UPGRADE_FORCE_CONFIG_AB: - return softwareUpgradeForceUseOfConfigAddressBook(testScenario); - case UPGRADE_ADD_NODE: - return softwareUpgradeAddNodeWeightingBehavior1(testScenario); - case UPGRADE_REMOVE_NODE: - return softwareUpgradeRemoveNodeWeightingBehavior1(testScenario); - case SKIP_VALIDATION: - // fall into default case. No validation performed. - default: - logger.info(DEMO_INFO.getMarker(), "Test Scenario {}: no validation performed.", testScenario); - return true; - } - } catch (final Exception e) { - logger.error(EXCEPTION.getMarker(), "Exception occurred in Test Scenario {}.", testScenario, e); - return false; - } - } - - private boolean softwareUpgradeRemoveNodeWeightingBehavior1(@NonNull final AddressBookTestScenario testScenario) - throws IOException, ParseException { - if (!checkTestScenarioConditions(false, testScenario, 2, 1)) { - return false; - } - - final AddressBook platformAddressBook = RosterUtils.buildAddressBook(platform.getRoster()); - final AddressBook configAddressBook = getConfigAddressBook(); - final AddressBook stateAddressBook = getStateAddressBook(); - final AddressBook usedAddressBook = getUsedAddressBook(); - final AddressBook updatedAddressBook = updateWeight(configAddressBook.copy(), context); - - return equalsAsRoster(platformAddressBook, configAddressBook, true) - && equalsAsRoster(platformAddressBook, stateAddressBook, false) - && equalsAsRoster(platformAddressBook, usedAddressBook, true) - && equalsAsRoster(platformAddressBook, updatedAddressBook, true) - && removedNodeFromAddressBook(platformAddressBook, stateAddressBook); - } - - private boolean softwareUpgradeAddNodeWeightingBehavior1(@NonNull final AddressBookTestScenario testScenario) - throws IOException, ParseException { - if (!checkTestScenarioConditions(false, testScenario, 2, 1)) { - return false; - } - - final AddressBook platformAddressBook = RosterUtils.buildAddressBook(platform.getRoster()); - final AddressBook configAddressBook = getConfigAddressBook(); - final AddressBook stateAddressBook = getStateAddressBook(); - final AddressBook usedAddressBook = getUsedAddressBook(); - final AddressBook updatedAddressBook = updateWeight(configAddressBook.copy(), context); - - if (equalsAsRosterWithoutCert(stateAddressBook, configAddressBook)) { - // This is a new node. - return equalsAsRoster(platformAddressBook, configAddressBook, true) - // genesis state uses the config address book. - && equalsAsRoster(platformAddressBook, stateAddressBook, true) - && equalsAsRoster(platformAddressBook, usedAddressBook, true) - && equalsAsRoster(platformAddressBook, updatedAddressBook, true); - } else { - // This is an existing node. - - // The equality to config text is due to a limitation of testing capability. - // Currently all nodes have to start with the same config.txt file that matches the updated weight. - return equalsAsRoster(platformAddressBook, configAddressBook, true) - && equalsAsRoster(platformAddressBook, stateAddressBook, false) - && equalsAsRoster(platformAddressBook, usedAddressBook, true) - && equalsAsRoster(platformAddressBook, updatedAddressBook, true) - && addedNodeToAddressBook(platformAddressBook, stateAddressBook); - } - } - - private boolean softwareUpgradeForceUseOfConfigAddressBook(@NonNull final AddressBookTestScenario testScenario) - throws IOException, ParseException { - if (!checkTestScenarioConditions(true, testScenario, 2, 2)) { - return false; - } - - final AddressBook platformAddressBook = RosterUtils.buildAddressBook(platform.getRoster()); - final AddressBook configAddressBook = getConfigAddressBook(); - final AddressBook stateAddressBook = getStateAddressBook(); - final AddressBook usedAddressBook = getUsedAddressBook(); - final AddressBook updatedAddressBook = updateWeight(configAddressBook.copy(), context); - - return equalsAsRoster(platformAddressBook, configAddressBook, true) - && equalsAsRoster(platformAddressBook, stateAddressBook, true) - && equalsAsRoster(platformAddressBook, usedAddressBook, true) - && equalsAsRoster(platformAddressBook, updatedAddressBook, false) - && theConfigurationAddressBookWasUsed(); - } - - private boolean softwareUpgradeWeightingBehavior2(@NonNull final AddressBookTestScenario testScenario) - throws IOException, ParseException { - if (!checkTestScenarioConditions(false, testScenario, 2, 2)) { - return false; - } - - final AddressBook platformAddressBook = RosterUtils.buildAddressBook(platform.getRoster()); - final AddressBook configAddressBook = getConfigAddressBook(); - final AddressBook stateAddressBook = getStateAddressBook(); - final AddressBook usedAddressBook = getUsedAddressBook(); - final AddressBook updatedAddressBook = updateWeight(configAddressBook.copy(), context); - - return equalsAsRoster(platformAddressBook, configAddressBook, false) - && equalsAsRoster(platformAddressBook, stateAddressBook, false) - && equalsAsRoster(platformAddressBook, usedAddressBook, true) - && equalsAsRoster(platformAddressBook, updatedAddressBook, true); - } - - private boolean noSoftwareUpgradeForceUseOfConfigAddressBook(@NonNull final AddressBookTestScenario testScenario) - throws IOException, ParseException { - if (!checkTestScenarioConditions(true, testScenario, 1, 1)) { - return false; - } - - final AddressBook platformAddressBook = RosterUtils.buildAddressBook(platform.getRoster()); - final AddressBook configAddressBook = getConfigAddressBook(); - final AddressBook stateAddressBook = getStateAddressBook(); - final AddressBook usedAddressBook = getUsedAddressBook(); - final AddressBook updatedAddressBook = updateWeight(configAddressBook.copy(), context); - - return equalsAsRoster(platformAddressBook, configAddressBook, true) - && equalsAsRoster(platformAddressBook, stateAddressBook, true) - && equalsAsRoster(platformAddressBook, usedAddressBook, true) - && equalsAsRoster(platformAddressBook, updatedAddressBook, false) - && theConfigurationAddressBookWasUsed(); - } - - private boolean noSoftwareUpgradeUseSavedStateAddressBook(AddressBookTestScenario testScenario) - throws IOException, ParseException { - if (!checkTestScenarioConditions(false, testScenario, 1, 1)) { - return false; - } - - final AddressBook platformAddressBook = RosterUtils.buildAddressBook(platform.getRoster()); - final AddressBook configAddressBook = getConfigAddressBook(); - final AddressBook stateAddressBook = getStateAddressBook(); - final AddressBook usedAddressBook = getUsedAddressBook(); - final AddressBook updatedAddressBook = updateWeight(configAddressBook.copy(), context); - - return equalsAsRoster(platformAddressBook, configAddressBook, true) - && equalsAsRoster(platformAddressBook, stateAddressBook, true) - && equalsAsRoster(platformAddressBook, usedAddressBook, true) - && equalsAsRoster(platformAddressBook, updatedAddressBook, false) - && theStateAddressBookWasUsed(); - } - - private boolean genesisForceUseOfConfigAddressBookFalse(@NonNull final AddressBookTestScenario testScenario) - throws IOException, ParseException { - if (!checkTestScenarioConditions(false, testScenario, 1, 1)) { - return false; - } - - final AddressBook platformAddressBook = RosterUtils.buildAddressBook(platform.getRoster()); - final AddressBook configAddressBook = getConfigAddressBook(); - final AddressBook stateAddressBook = getStateAddressBook(); - final AddressBook usedAddressBook = getUsedAddressBook(); - final AddressBook updatedAddressBook = updateWeight(configAddressBook.copy(), context); - - return equalsAsRoster(platformAddressBook, configAddressBook, true) - && equalsAsRoster(platformAddressBook, usedAddressBook, true) - && equalsAsRoster(platformAddressBook, stateAddressBook, true) - && equalsAsRoster(platformAddressBook, updatedAddressBook, false); - } - - private boolean genesisForceUseOfConfigAddressBookTrue(@NonNull final AddressBookTestScenario testScenario) - throws IOException, ParseException { - if (!checkTestScenarioConditions(true, testScenario, 1, 1)) { - return false; - } - - final AddressBook platformAddressBook = RosterUtils.buildAddressBook(platform.getRoster()); - final AddressBook configAddressBook = getConfigAddressBook(); - final AddressBook stateAddressBook = getStateAddressBook(); - final AddressBook usedAddressBook = getUsedAddressBook(); - final AddressBook updatedAddressBook = updateWeight(configAddressBook.copy(), context); - - return equalsAsRoster(platformAddressBook, configAddressBook, true) - && equalsAsRoster(platformAddressBook, usedAddressBook, true) - && equalsAsRoster(platformAddressBook, stateAddressBook, true) - && equalsAsRoster(platformAddressBook, updatedAddressBook, false) - && theConfigurationAddressBookWasUsed(); - } - - /** - * Check the test scenario preconditions. - * - * @param forceUseConfigAddressBook the expected value of `addressBook.forceUseOfConfigAddressBook` - * @param testScenario the expected value of `testingTool.testScenario` - * @param softwareVersion the expected value of `testingTool.softwareVersion` - * @param weightingBehavior the expected value of `testingTool.weightingBehavior` - * @return true if the preconditions are met, false otherwise - */ - private boolean checkTestScenarioConditions( - final boolean forceUseConfigAddressBook, - final AddressBookTestScenario testScenario, - final int softwareVersion, - final int weightingBehavior) { - boolean passed = true; - if (addressBookConfig.forceUseOfConfigAddressBook() != forceUseConfigAddressBook) { - logger.error( - EXCEPTION.getMarker(), - "The test scenario requires the setting `addressBook.forceUseOfConfigAddressBook, {}`", - forceUseConfigAddressBook); - passed = false; - } - if (!testScenario.toString().equals(testingToolConfig.testScenario())) { - logger.error( - EXCEPTION.getMarker(), - "The test scenario requires the setting `testingTool.testScenario, {}`", - testScenario); - passed = false; - } - if (testingToolConfig.softwareVersion() != softwareVersion) { - logger.error( - EXCEPTION.getMarker(), - "The test scenario requires the setting `testingTool.softwareVersion, {}`", - softwareVersion); - passed = false; - } - if (testingToolConfig.weightingBehavior() != weightingBehavior) { - logger.error( - EXCEPTION.getMarker(), - "The test scenario requires the setting `testingTool.weightingBehavior, {}`", - weightingBehavior); - passed = false; - } - return passed; - } - - /** - * Remove certificates from a roster. - * @param roster an input roster - * @return the same roster as input but with all gossip certs removed - */ - private Roster removeCerts(@NonNull final Roster roster) { - return roster.copyBuilder() - .rosterEntries(roster.rosterEntries().stream() - .map(re -> re.copyBuilder() - .gossipCaCertificate(Bytes.EMPTY) - .build()) - .toList()) - .build(); - } - - /** - * Compare two AddressBooks after converting them to Rosters and removing all certificates - * to avoid mismatches for fields absent from the Roster. - * @param addressBook1 the first address book - * @param addressBook2 the second address book - * @return true if equals, false otherwise - */ - private boolean equalsAsRosterWithoutCert( - @NonNull final AddressBook addressBook1, @NonNull final AddressBook addressBook2) { - final Roster roster1 = removeCerts(RosterRetriever.buildRoster(addressBook1)); - final Roster roster2 = removeCerts(RosterRetriever.buildRoster(addressBook2)); - return roster1.equals(roster2); - } - - /** - * This test compares the equality of two address books against the expected result. - * - * @param addressBook1 the first address book - * @param addressBook2 the second address book - * @return true if the comparison matches the expected result, false otherwise. - */ - private boolean equalsAsRoster( - @NonNull final AddressBook addressBook1, - @NonNull final AddressBook addressBook2, - final boolean expectedResult) { - final boolean pass = equalsAsRosterWithoutCert(addressBook1, addressBook2) == expectedResult; - if (!pass) { - if (expectedResult) { - logger.error( - EXCEPTION.getMarker(), - "The address books are not equal as Roster. {}", - StackTrace.getStackTrace()); - } else { - logger.error( - EXCEPTION.getMarker(), "The address books are equal as Roster. {}", StackTrace.getStackTrace()); - } - } - return pass; - } - - /** - * Checks if the state address book was used. - * - * @return true if the state address book was used, false otherwise. - */ - private boolean theStateAddressBookWasUsed() throws IOException { - final String fileContents = getLastAddressBookFileEndsWith(DEBUG); - final String textAfterUsedHeader = getTextAfterHeader(fileContents, USED_ADDRESS_BOOK_HEADER); - final boolean pass = textAfterUsedHeader.contains(STATE_ADDRESS_BOOK_USED); - if (!pass) { - logger.error(EXCEPTION.getMarker(), "The state address book was not used. {}", StackTrace.getStackTrace()); - } - return pass; - } - - /** - * Checks if the configuration address book was used. - * - * @return true if the configuration address book was used, false otherwise. - */ - private boolean theConfigurationAddressBookWasUsed() throws IOException { - final String fileContents = getLastAddressBookFileEndsWith(DEBUG); - final String textAfterUsedHeader = getTextAfterHeader(fileContents, USED_ADDRESS_BOOK_HEADER); - final boolean pass = textAfterUsedHeader.contains(CONFIG_ADDRESS_BOOK_USED); - if (!pass) { - logger.error( - EXCEPTION.getMarker(), - "The configuration address book was not used. {}", - StackTrace.getStackTrace()); - } - return pass; - } - - /** - * Checks if the new address book contains a proper subset of the node ids in the old address book and that the - * nextNodeId value of the new address book is the same. - * - * @param newAddressBook The new address book - * @param oldAddressBook The old address book - * @return true if the new address book has all the nodes in the old address book (ignoring weight differences) plus - * at least one more and its nextNodeId value is larger. - */ - private boolean removedNodeFromAddressBook( - @NonNull final AddressBook newAddressBook, @NonNull final AddressBook oldAddressBook) { - int missingCount = 0; - for (final Address address : oldAddressBook) { - final NodeId nodeId = address.getNodeId(); - if (newAddressBook.contains(nodeId)) { - continue; - } - missingCount++; - } - final int newSize = newAddressBook.getSize(); - final int oldSize = oldAddressBook.getSize(); - final boolean atLeastOneNodeRemoved = missingCount > 0; - if (!atLeastOneNodeRemoved) { - logger.error( - EXCEPTION.getMarker(), - "The new address book does not have at least one node removed. {}", - StackTrace.getStackTrace()); - } - final boolean sizesCorrespond = missingCount == oldSize - newSize; - if (!sizesCorrespond) { - logger.error( - EXCEPTION.getMarker(), - "The new address book contains new nodes instead of only having nodes removed. {}", - StackTrace.getStackTrace()); - } - return sizesCorrespond && atLeastOneNodeRemoved; - } - - /** - * Checks if the old address book contains a proper subset of node ids in the new address book and that the - * nextNodeId value of the new address book is larger. - * - * @param newAddressBook The new address book - * @param oldAddressBook The old address book - * @return true if the new address book has all the nodes in the old address book (ignoring weight differences) plus - * at least one more and its nextNodeId value is larger. - */ - private boolean addedNodeToAddressBook( - @NonNull final AddressBook newAddressBook, @NonNull final AddressBook oldAddressBook) { - int missingCount = 0; - for (final Address address : newAddressBook) { - final NodeId nodeId = address.getNodeId(); - if (oldAddressBook.contains(nodeId)) { - continue; - } - missingCount++; - } - final int newSize = newAddressBook.getSize(); - final int oldSize = oldAddressBook.getSize(); - final boolean atLeastOneNodeAdded = missingCount > 0; - if (!atLeastOneNodeAdded) { - logger.error( - EXCEPTION.getMarker(), - "The new address book does not have at least one node added. {}", - StackTrace.getStackTrace()); - } - final boolean sizesCorrespond = missingCount == newSize - oldSize; - if (!sizesCorrespond) { - logger.error( - EXCEPTION.getMarker(), - "The new address book has nodes removed instead of only having nodes added. {}", - StackTrace.getStackTrace()); - } - return sizesCorrespond && atLeastOneNodeAdded; - } - - /** - * Get the address book in the last usedAddressBook file. - * - * @return the address book in the last usedAddressBook file. - */ - @NonNull - private AddressBook getUsedAddressBook() throws IOException, ParseException { - final String fileContents = getLastAddressBookFileEndsWith("txt"); - return parseAddressBook(fileContents); - } - - /** - * Get the config address book from the last debug addressBook file. - * - * @return the config address book from the last debug addressBook file. - */ - @NonNull - private AddressBook getConfigAddressBook() throws IOException, ParseException { - return getDebugAddressBookAfterHeader(CONFIG_ADDRESS_BOOK_HEADER); - } - - /** - * Get the state address book from the last debug addressBook file. - * - * @return the state address book from the last debug addressBook file. - */ - @NonNull - private AddressBook getStateAddressBook() throws IOException, ParseException { - return getDebugAddressBookAfterHeader(STATE_ADDRESS_BOOK_HEADER); - } - - /** - * Get the address book in the last debug addressBook file after the header. - * - * @param header the header to find. - * @return the address book in the last debug addressBook file after the header. - */ - @NonNull - AddressBook getDebugAddressBookAfterHeader(@NonNull final String header) throws IOException, ParseException { - final String fileContents = getLastAddressBookFileEndsWith(DEBUG); - final String addressBookString = getTextAfterHeader(fileContents, header); - return parseAddressBook(addressBookString); - } - - /** - * Get the text from the fileContents after the header. - * - * @param fileContents the file contents. - * @param header the header to find. - * @return the text from the fileContents after the header. - */ - @NonNull - String getTextAfterHeader(@NonNull final String fileContents, @NonNull final String header) { - Objects.requireNonNull(fileContents, "fileContents must not be null"); - Objects.requireNonNull(header, "header must not be null"); - final int configABHeaderStart = fileContents.indexOf(CONFIG_ADDRESS_BOOK_HEADER); - final int stateABHeaderStart = fileContents.indexOf(STATE_ADDRESS_BOOK_HEADER); - final int usedABHeaderStart = fileContents.indexOf(USED_ADDRESS_BOOK_HEADER); - - final int headerStartIndex = fileContents.indexOf(header); - final int addressBookStartIndex = headerStartIndex + header.length(); - - final int addressBookEndIndex; - if (headerStartIndex == configABHeaderStart) { - addressBookEndIndex = stateABHeaderStart; - } else if (headerStartIndex == stateABHeaderStart) { - addressBookEndIndex = usedABHeaderStart; - } else { - addressBookEndIndex = fileContents.length(); - } - - return fileContents - .substring(addressBookStartIndex, addressBookEndIndex) - .trim(); - } - - /** - * Parse the address book from the given string. - * - * @param addressBookString the address book string. - * @return the address book. - * @throws ParseException if unable to parse the address book. - */ - @NonNull - private AddressBook parseAddressBook(@NonNull final String addressBookString) throws ParseException { - Objects.requireNonNull(addressBookString, "addressBookString must not be null"); - return AddressBookUtils.parseAddressBookText(addressBookString); - } - - /** - * Get the last address book file that ends with the given suffix. - * - * @param suffix the suffix to match. - * @return the last address book file that ends with the given suffix. - * @throws IOException if unable to read the file. - */ - @NonNull - private String getLastAddressBookFileEndsWith(@NonNull final String suffix) throws IOException { - final String nodeId = "node_%s".formatted(this.selfId); - final Path addressBookDirectory = Path.of(addressBookConfig.addressBookDirectory()); - File[] files = addressBookDirectory.toFile().listFiles(File::isFile); - final AtomicReference lastAddressBookDebugFile = new AtomicReference<>(null); - for (int i = 0; i < files.length; i++) { - final String fileName = files[i].getName(); - if (fileName.contains(nodeId) && fileName.endsWith(suffix)) { - final Path lastAddressBookDebugFilePath = lastAddressBookDebugFile.get(); - if (lastAddressBookDebugFilePath == null - || fileName.compareTo(lastAddressBookDebugFilePath - .getFileName() - .toString()) - > 0) { - lastAddressBookDebugFile.set(files[i].toPath()); - } - } - } - return Files.readString(lastAddressBookDebugFile.get()); - } } diff --git a/platform-sdk/platform-apps/tests/AddressBookTestingTool/src/main/java/com/swirlds/demo/addressbook/AddressBookTestingToolStateLifecycles.java b/platform-sdk/platform-apps/tests/AddressBookTestingTool/src/main/java/com/swirlds/demo/addressbook/AddressBookTestingToolStateLifecycles.java new file mode 100644 index 000000000000..efe5ace27d0a --- /dev/null +++ b/platform-sdk/platform-apps/tests/AddressBookTestingTool/src/main/java/com/swirlds/demo/addressbook/AddressBookTestingToolStateLifecycles.java @@ -0,0 +1,853 @@ +/* + * 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.swirlds.demo.addressbook; + +import static com.swirlds.logging.legacy.LogMarker.DEMO_INFO; +import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; +import static com.swirlds.logging.legacy.LogMarker.STARTUP; +import static com.swirlds.platform.state.address.AddressBookInitializer.CONFIG_ADDRESS_BOOK_HEADER; +import static com.swirlds.platform.state.address.AddressBookInitializer.CONFIG_ADDRESS_BOOK_USED; +import static com.swirlds.platform.state.address.AddressBookInitializer.STATE_ADDRESS_BOOK_HEADER; +import static com.swirlds.platform.state.address.AddressBookInitializer.STATE_ADDRESS_BOOK_USED; +import static com.swirlds.platform.state.address.AddressBookInitializer.USED_ADDRESS_BOOK_HEADER; +import static java.util.Objects.requireNonNull; + +import com.hedera.hapi.node.state.roster.Roster; +import com.hedera.hapi.platform.event.StateSignatureTransaction; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.common.context.PlatformContext; +import com.swirlds.common.platform.NodeId; +import com.swirlds.common.utility.ByteUtils; +import com.swirlds.common.utility.StackTrace; +import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; +import com.swirlds.platform.config.AddressBookConfig; +import com.swirlds.platform.roster.RosterRetriever; +import com.swirlds.platform.roster.RosterUtils; +import com.swirlds.platform.state.PlatformStateModifier; +import com.swirlds.platform.state.StateLifecycles; +import com.swirlds.platform.state.snapshot.SignedStateFileReader; +import com.swirlds.platform.system.InitTrigger; +import com.swirlds.platform.system.Platform; +import com.swirlds.platform.system.Round; +import com.swirlds.platform.system.SoftwareVersion; +import com.swirlds.platform.system.address.Address; +import com.swirlds.platform.system.address.AddressBook; +import com.swirlds.platform.system.address.AddressBookUtils; +import com.swirlds.platform.system.events.Event; +import com.swirlds.platform.system.transaction.ConsensusTransaction; +import com.swirlds.platform.system.transaction.Transaction; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.text.ParseException; +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * This class is responsible for processing lifecycle events for the {@link AddressBookTestingToolState}. + */ +public class AddressBookTestingToolStateLifecycles implements StateLifecycles { + + private static final Logger logger = LogManager.getLogger(AddressBookTestingToolStateLifecycles.class); + + /** the suffix for the debug address book */ + private static final String DEBUG = "debug"; + + /** flag indicating if weighting behavior has been logged. */ + private static final AtomicBoolean logWeightingBehavior = new AtomicBoolean(true); + + /** the suffix for the test address book */ + private AddressBookTestingToolConfig testingToolConfig; + /** the address book configuration */ + private AddressBookConfig addressBookConfig; + /** + * If not zero and we are handling the first round after genesis, configure a freeze this duration later. + *

    + * Does not affect the hash of this node (although actions may be taken based on this info that DO affect the + * hash). + */ + private Duration freezeAfterGenesis = null; + + private NodeId selfId; + + /** false until the test scenario has been validated, true afterwards. */ + private final AtomicBoolean validationPerformed = new AtomicBoolean(false); + + private Platform platform = null; + private PlatformContext context = null; + + @Override + public void onStateInitialized( + @NonNull AddressBookTestingToolState state, + @NonNull Platform platform, + @NonNull InitTrigger trigger, + @Nullable SoftwareVersion previousVersion) { + requireNonNull(platform, "the platform cannot be null"); + requireNonNull(trigger, "the init trigger cannot be null"); + addressBookConfig = platform.getContext().getConfiguration().getConfigData(AddressBookConfig.class); + testingToolConfig = platform.getContext().getConfiguration().getConfigData(AddressBookTestingToolConfig.class); + this.freezeAfterGenesis = testingToolConfig.freezeAfterGenesis(); + + this.platform = platform; + this.context = platform.getContext(); + + logger.info(STARTUP.getMarker(), "init called in State."); + state.throwIfImmutable(); + + this.selfId = platform.getSelfId(); + + state.initState(); + + // Since this demo State doesn't call Hedera.onStateInitialized() to init States API for all services + // (because it doesn't call super.init(), and the FakeStateLifecycles doesn't do that anyway), + // we need to register PlatformService and RosterService states for the rest of the code to operate + // when an instance of this state is received via reconnect. In any other cases, this call + // should be idempotent. + SignedStateFileReader.registerServiceStates(state); + logger.info(STARTUP.getMarker(), "Registered PlatformService and RosterService states."); + } + + @Override + public void onHandleConsensusRound( + @NonNull Round round, + @NonNull AddressBookTestingToolState state, + @NonNull Consumer> stateSignatureTransactionCallback) { + requireNonNull(round, "the round cannot be null"); + requireNonNull(state, "the state cannot be null"); + state.throwIfImmutable(); + PlatformStateModifier platformState = state.getWritablePlatformState(); + requireNonNull(platformState, "the platform state cannot be null"); + + if (state.getRoundsHandled() == 0 && !freezeAfterGenesis.equals(Duration.ZERO)) { + // This is the first round after genesis. + logger.info( + STARTUP.getMarker(), + "Setting freeze time to {} seconds after genesis.", + freezeAfterGenesis.getSeconds()); + platformState.setFreezeTime(round.getConsensusTimestamp().plus(freezeAfterGenesis)); + } + + state.incrementRoundsHandled(); + + for (final var event : round) { + event.consensusTransactionIterator().forEachRemaining(transaction -> { + // We are not interested in handling any system transactions, as they are + // specific for the platform only.We also don't want to consume deprecated + // EventTransaction.STATE_SIGNATURE_TRANSACTION system transactions in the + // callback, since it's intended to be used only for the new form of encoded system + // transactions in Bytes. Thus, we can directly skip the current + // iteration, if it processes a deprecated system transaction with the + // EventTransaction.STATE_SIGNATURE_TRANSACTION type. + if (transaction.isSystem()) { + return; + } + + // We should consume in the callback the new form of system transactions in Bytes + if (state.areTransactionBytesSystemOnes(transaction)) { + consumeSystemTransaction(transaction, event, stateSignatureTransactionCallback); + } else { + handleTransaction(transaction, state); + } + }); + } + + if (!validationPerformed.getAndSet(true)) { + String testScenario = testingToolConfig.testScenario(); + if (validateTestScenario(state)) { + logger.info(STARTUP.getMarker(), "Test scenario {}: finished without errors.", testScenario); + } else { + logger.error(EXCEPTION.getMarker(), "Test scenario {}: validation failed with errors.", testScenario); + } + } + } + + /** + * Apply a transaction to the state. + * + * @param transaction the transaction to apply + */ + private void handleTransaction( + @NonNull final ConsensusTransaction transaction, @NonNull final AddressBookTestingToolState state) { + if (transaction.isSystem()) { + return; + } + final int delta = + ByteUtils.byteArrayToInt(transaction.getApplicationTransaction().toByteArray(), 0); + state.incrementRunningSum(delta); + } + + @Override + public void onUpdateWeight( + @NonNull AddressBookTestingToolState state, + @NonNull AddressBook addressBook, + @NonNull PlatformContext context) { + Objects.requireNonNull(addressBook, "the address book cannot be null"); + this.context = Objects.requireNonNull(context, "the platform context cannot be null"); + final int weightingBehavior = context.getConfiguration() + .getConfigData(AddressBookTestingToolConfig.class) + .weightingBehavior(); + logger.info(DEMO_INFO.getMarker(), "updateWeight called in State. Weighting Behavior: {}", weightingBehavior); + switch (weightingBehavior) { + case 1: + weightingBehavior1(addressBook); + return; + case 2: + weightingBehavior2(addressBook); + return; + default: + logger.info( + STARTUP.getMarker(), "Weighting Behavior {}: no change to address book.", weightingBehavior); + } + } + + /** + * All nodes received 10 weight. + * + * @param addressBook the address book to update. + */ + private void weightingBehavior1(@NonNull final AddressBook addressBook) { + if (logWeightingBehavior.get()) { + logger.info(STARTUP.getMarker(), "Weighting Behavior 1: updating all nodes to have 10 weight."); + } + for (final Address address : addressBook) { + addressBook.updateWeight(address.getNodeId(), 10); + } + } + + /** + * All nodes received weight equal to their nodeId. + * + * @param addressBook the address book to update. + */ + private void weightingBehavior2(@NonNull final AddressBook addressBook) { + if (logWeightingBehavior.get()) { + logger.info( + STARTUP.getMarker(), + "Weighting Behavior 2: updating all nodes to have weight equal to their nodeId."); + } + for (final Address address : addressBook) { + addressBook.updateWeight(address.getNodeId(), address.getNodeId().id()); + } + } + + private boolean validateTestScenario(AddressBookTestingToolState state) { + if (platform == null) { + throw new IllegalStateException("platform is null, init has not been called."); + } + logWeightingBehavior.set(false); + final AddressBookTestScenario testScenario = AddressBookTestScenario.valueOf(testingToolConfig.testScenario()); + try { + logger.info(DEMO_INFO.getMarker(), "Validating test scenario {}.", testScenario); + switch (testScenario) { + case GENESIS_FORCE_CONFIG_AB: + return genesisForceUseOfConfigAddressBookTrue(testScenario, state); + case GENESIS_NORMAL: + return genesisForceUseOfConfigAddressBookFalse(testScenario, state); + case NO_UPGRADE_USE_SAVED_STATE: + return noSoftwareUpgradeUseSavedStateAddressBook(testScenario, state); + case NO_UPGRADE_FORCE_CONFIG_AB: + return noSoftwareUpgradeForceUseOfConfigAddressBook(testScenario, state); + case UPGRADE_WEIGHT_BEHAVIOR_2: + return softwareUpgradeWeightingBehavior2(testScenario, state); + case UPGRADE_FORCE_CONFIG_AB: + return softwareUpgradeForceUseOfConfigAddressBook(testScenario, state); + case UPGRADE_ADD_NODE: + return softwareUpgradeAddNodeWeightingBehavior1(testScenario, state); + case UPGRADE_REMOVE_NODE: + return softwareUpgradeRemoveNodeWeightingBehavior1(testScenario, state); + case SKIP_VALIDATION: + // fall into default case. No validation performed. + default: + logger.info(DEMO_INFO.getMarker(), "Test Scenario {}: no validation performed.", testScenario); + return true; + } + } catch (final Exception e) { + logger.error(EXCEPTION.getMarker(), "Exception occurred in Test Scenario {}.", testScenario, e); + return false; + } + } + + private boolean softwareUpgradeRemoveNodeWeightingBehavior1( + @NonNull final AddressBookTestScenario testScenario, @NonNull final AddressBookTestingToolState state) + throws IOException, ParseException { + if (!checkTestScenarioConditions(false, testScenario, 2, 1)) { + return false; + } + + final AddressBook platformAddressBook = RosterUtils.buildAddressBook(platform.getRoster()); + final AddressBook configAddressBook = getConfigAddressBook(); + final AddressBook stateAddressBook = getStateAddressBook(); + final AddressBook usedAddressBook = getUsedAddressBook(); + final AddressBook updatedAddressBook = configAddressBook.copy(); + onUpdateWeight(state, updatedAddressBook, context); + + return equalsAsRoster(platformAddressBook, configAddressBook, true) + && equalsAsRoster(platformAddressBook, stateAddressBook, false) + && equalsAsRoster(platformAddressBook, usedAddressBook, true) + && equalsAsRoster(platformAddressBook, updatedAddressBook, true) + && removedNodeFromAddressBook(platformAddressBook, stateAddressBook); + } + + private boolean softwareUpgradeAddNodeWeightingBehavior1( + @NonNull final AddressBookTestScenario testScenario, @NonNull final AddressBookTestingToolState state) + throws IOException, ParseException { + if (!checkTestScenarioConditions(false, testScenario, 2, 1)) { + return false; + } + + final AddressBook platformAddressBook = RosterUtils.buildAddressBook(platform.getRoster()); + final AddressBook configAddressBook = getConfigAddressBook(); + final AddressBook stateAddressBook = getStateAddressBook(); + final AddressBook usedAddressBook = getUsedAddressBook(); + + final AddressBook updatedAddressBook = configAddressBook.copy(); + onUpdateWeight(state, updatedAddressBook, context); + if (equalsAsRosterWithoutCert(stateAddressBook, configAddressBook)) { + // This is a new node. + return equalsAsRoster(platformAddressBook, configAddressBook, true) + // genesis state uses the config address book. + && equalsAsRoster(platformAddressBook, stateAddressBook, true) + && equalsAsRoster(platformAddressBook, usedAddressBook, true) + && equalsAsRoster(platformAddressBook, updatedAddressBook, true); + } else { + // This is an existing node. + + // The equality to config text is due to a limitation of testing capability. + // Currently all nodes have to start with the same config.txt file that matches the updated weight. + return equalsAsRoster(platformAddressBook, configAddressBook, true) + && equalsAsRoster(platformAddressBook, stateAddressBook, false) + && equalsAsRoster(platformAddressBook, usedAddressBook, true) + && equalsAsRoster(platformAddressBook, updatedAddressBook, true) + && addedNodeToAddressBook(platformAddressBook, stateAddressBook); + } + } + + private boolean softwareUpgradeForceUseOfConfigAddressBook( + @NonNull final AddressBookTestScenario testScenario, @NonNull final AddressBookTestingToolState state) + throws IOException, ParseException { + if (!checkTestScenarioConditions(true, testScenario, 2, 2)) { + return false; + } + + final AddressBook platformAddressBook = RosterUtils.buildAddressBook(platform.getRoster()); + final AddressBook configAddressBook = getConfigAddressBook(); + final AddressBook stateAddressBook = getStateAddressBook(); + final AddressBook usedAddressBook = getUsedAddressBook(); + final AddressBook updatedAddressBook = configAddressBook.copy(); + onUpdateWeight(state, updatedAddressBook, context); + + return equalsAsRoster(platformAddressBook, configAddressBook, true) + && equalsAsRoster(platformAddressBook, stateAddressBook, true) + && equalsAsRoster(platformAddressBook, usedAddressBook, true) + && equalsAsRoster(platformAddressBook, updatedAddressBook, false) + && theConfigurationAddressBookWasUsed(); + } + + private boolean softwareUpgradeWeightingBehavior2( + @NonNull final AddressBookTestScenario testScenario, @NonNull final AddressBookTestingToolState state) + throws IOException, ParseException { + if (!checkTestScenarioConditions(false, testScenario, 2, 2)) { + return false; + } + + final AddressBook platformAddressBook = RosterUtils.buildAddressBook(platform.getRoster()); + final AddressBook configAddressBook = getConfigAddressBook(); + final AddressBook stateAddressBook = getStateAddressBook(); + final AddressBook usedAddressBook = getUsedAddressBook(); + final AddressBook updatedAddressBook = configAddressBook.copy(); + onUpdateWeight(state, updatedAddressBook, context); + + return equalsAsRoster(platformAddressBook, configAddressBook, false) + && equalsAsRoster(platformAddressBook, stateAddressBook, false) + && equalsAsRoster(platformAddressBook, usedAddressBook, true) + && equalsAsRoster(platformAddressBook, updatedAddressBook, true); + } + + private boolean noSoftwareUpgradeForceUseOfConfigAddressBook( + @NonNull final AddressBookTestScenario testScenario, @NonNull final AddressBookTestingToolState state) + throws IOException, ParseException { + if (!checkTestScenarioConditions(true, testScenario, 1, 1)) { + return false; + } + + final AddressBook platformAddressBook = RosterUtils.buildAddressBook(platform.getRoster()); + final AddressBook configAddressBook = getConfigAddressBook(); + final AddressBook stateAddressBook = getStateAddressBook(); + final AddressBook usedAddressBook = getUsedAddressBook(); + final AddressBook updatedAddressBook = configAddressBook.copy(); + onUpdateWeight(state, updatedAddressBook, context); + + return equalsAsRoster(platformAddressBook, configAddressBook, true) + && equalsAsRoster(platformAddressBook, stateAddressBook, true) + && equalsAsRoster(platformAddressBook, usedAddressBook, true) + && equalsAsRoster(platformAddressBook, updatedAddressBook, false) + && theConfigurationAddressBookWasUsed(); + } + + private boolean noSoftwareUpgradeUseSavedStateAddressBook( + AddressBookTestScenario testScenario, @NonNull final AddressBookTestingToolState state) + throws IOException, ParseException { + if (!checkTestScenarioConditions(false, testScenario, 1, 1)) { + return false; + } + + final AddressBook platformAddressBook = RosterUtils.buildAddressBook(platform.getRoster()); + final AddressBook configAddressBook = getConfigAddressBook(); + final AddressBook stateAddressBook = getStateAddressBook(); + final AddressBook usedAddressBook = getUsedAddressBook(); + final AddressBook updatedAddressBook = configAddressBook.copy(); + onUpdateWeight(state, updatedAddressBook, context); + + return equalsAsRoster(platformAddressBook, configAddressBook, true) + && equalsAsRoster(platformAddressBook, stateAddressBook, true) + && equalsAsRoster(platformAddressBook, usedAddressBook, true) + && equalsAsRoster(platformAddressBook, updatedAddressBook, false) + && theStateAddressBookWasUsed(); + } + + private boolean genesisForceUseOfConfigAddressBookFalse( + @NonNull final AddressBookTestScenario testScenario, @NonNull final AddressBookTestingToolState state) + throws IOException, ParseException { + if (!checkTestScenarioConditions(false, testScenario, 1, 1)) { + return false; + } + + final AddressBook platformAddressBook = RosterUtils.buildAddressBook(platform.getRoster()); + final AddressBook configAddressBook = getConfigAddressBook(); + final AddressBook stateAddressBook = getStateAddressBook(); + final AddressBook usedAddressBook = getUsedAddressBook(); + final AddressBook updatedAddressBook = configAddressBook.copy(); + onUpdateWeight(state, updatedAddressBook, context); + + return equalsAsRoster(platformAddressBook, configAddressBook, true) + && equalsAsRoster(platformAddressBook, usedAddressBook, true) + && equalsAsRoster(platformAddressBook, stateAddressBook, true) + && equalsAsRoster(platformAddressBook, updatedAddressBook, false); + } + + private boolean genesisForceUseOfConfigAddressBookTrue( + @NonNull final AddressBookTestScenario testScenario, @NonNull final AddressBookTestingToolState state) + throws IOException, ParseException { + if (!checkTestScenarioConditions(true, testScenario, 1, 1)) { + return false; + } + + final AddressBook platformAddressBook = RosterUtils.buildAddressBook(platform.getRoster()); + final AddressBook configAddressBook = getConfigAddressBook(); + final AddressBook stateAddressBook = getStateAddressBook(); + final AddressBook usedAddressBook = getUsedAddressBook(); + final AddressBook updatedAddressBook = configAddressBook.copy(); + onUpdateWeight(state, usedAddressBook, context); + + return equalsAsRoster(platformAddressBook, configAddressBook, true) + && equalsAsRoster(platformAddressBook, usedAddressBook, true) + && equalsAsRoster(platformAddressBook, stateAddressBook, true) + && equalsAsRoster(platformAddressBook, updatedAddressBook, false) + && theConfigurationAddressBookWasUsed(); + } + + /** + * Check the test scenario preconditions. + * + * @param forceUseConfigAddressBook the expected value of `addressBook.forceUseOfConfigAddressBook` + * @param testScenario the expected value of `testingTool.testScenario` + * @param softwareVersion the expected value of `testingTool.softwareVersion` + * @param weightingBehavior the expected value of `testingTool.weightingBehavior` + * @return true if the preconditions are met, false otherwise + */ + private boolean checkTestScenarioConditions( + final boolean forceUseConfigAddressBook, + final AddressBookTestScenario testScenario, + final int softwareVersion, + final int weightingBehavior) { + boolean passed = true; + if (addressBookConfig.forceUseOfConfigAddressBook() != forceUseConfigAddressBook) { + logger.error( + EXCEPTION.getMarker(), + "The test scenario requires the setting `addressBook.forceUseOfConfigAddressBook, {}`", + forceUseConfigAddressBook); + passed = false; + } + if (!testScenario.toString().equals(testingToolConfig.testScenario())) { + logger.error( + EXCEPTION.getMarker(), + "The test scenario requires the setting `testingTool.testScenario, {}`", + testScenario); + passed = false; + } + if (testingToolConfig.softwareVersion() != softwareVersion) { + logger.error( + EXCEPTION.getMarker(), + "The test scenario requires the setting `testingTool.softwareVersion, {}`", + softwareVersion); + passed = false; + } + if (testingToolConfig.weightingBehavior() != weightingBehavior) { + logger.error( + EXCEPTION.getMarker(), + "The test scenario requires the setting `testingTool.weightingBehavior, {}`", + weightingBehavior); + passed = false; + } + return passed; + } + + /** + * Remove certificates from a roster. + * @param roster an input roster + * @return the same roster as input but with all gossip certs removed + */ + private Roster removeCerts(@NonNull final Roster roster) { + return roster.copyBuilder() + .rosterEntries(roster.rosterEntries().stream() + .map(re -> re.copyBuilder() + .gossipCaCertificate(Bytes.EMPTY) + .build()) + .toList()) + .build(); + } + + /** + * Compare two AddressBooks after converting them to Rosters and removing all certificates + * to avoid mismatches for fields absent from the Roster. + * @param addressBook1 the first address book + * @param addressBook2 the second address book + * @return true if equals, false otherwise + */ + private boolean equalsAsRosterWithoutCert( + @NonNull final AddressBook addressBook1, @NonNull final AddressBook addressBook2) { + final Roster roster1 = removeCerts(RosterRetriever.buildRoster(addressBook1)); + final Roster roster2 = removeCerts(RosterRetriever.buildRoster(addressBook2)); + return roster1.equals(roster2); + } + + /** + * This test compares the equality of two address books against the expected result. + * + * @param addressBook1 the first address book + * @param addressBook2 the second address book + * @return true if the comparison matches the expected result, false otherwise. + */ + private boolean equalsAsRoster( + @NonNull final AddressBook addressBook1, + @NonNull final AddressBook addressBook2, + final boolean expectedResult) { + final boolean pass = equalsAsRosterWithoutCert(addressBook1, addressBook2) == expectedResult; + if (!pass) { + if (expectedResult) { + logger.error( + EXCEPTION.getMarker(), + "The address books are not equal as Roster. {}", + StackTrace.getStackTrace()); + } else { + logger.error( + EXCEPTION.getMarker(), "The address books are equal as Roster. {}", StackTrace.getStackTrace()); + } + } + return pass; + } + + /** + * Checks if the state address book was used. + * + * @return true if the state address book was used, false otherwise. + */ + private boolean theStateAddressBookWasUsed() throws IOException { + final String fileContents = getLastAddressBookFileEndsWith(DEBUG); + final String textAfterUsedHeader = getTextAfterHeader(fileContents, USED_ADDRESS_BOOK_HEADER); + final boolean pass = textAfterUsedHeader.contains(STATE_ADDRESS_BOOK_USED); + if (!pass) { + logger.error(EXCEPTION.getMarker(), "The state address book was not used. {}", StackTrace.getStackTrace()); + } + return pass; + } + + /** + * Checks if the configuration address book was used. + * + * @return true if the configuration address book was used, false otherwise. + */ + private boolean theConfigurationAddressBookWasUsed() throws IOException { + final String fileContents = getLastAddressBookFileEndsWith(DEBUG); + final String textAfterUsedHeader = getTextAfterHeader(fileContents, USED_ADDRESS_BOOK_HEADER); + final boolean pass = textAfterUsedHeader.contains(CONFIG_ADDRESS_BOOK_USED); + if (!pass) { + logger.error( + EXCEPTION.getMarker(), + "The configuration address book was not used. {}", + StackTrace.getStackTrace()); + } + return pass; + } + + /** + * Checks if the new address book contains a proper subset of the node ids in the old address book and that the + * nextNodeId value of the new address book is the same. + * + * @param newAddressBook The new address book + * @param oldAddressBook The old address book + * @return true if the new address book has all the nodes in the old address book (ignoring weight differences) plus + * at least one more and its nextNodeId value is larger. + */ + private boolean removedNodeFromAddressBook( + @NonNull final AddressBook newAddressBook, @NonNull final AddressBook oldAddressBook) { + int missingCount = 0; + for (final Address address : oldAddressBook) { + final NodeId nodeId = address.getNodeId(); + if (newAddressBook.contains(nodeId)) { + continue; + } + missingCount++; + } + final int newSize = newAddressBook.getSize(); + final int oldSize = oldAddressBook.getSize(); + final boolean atLeastOneNodeRemoved = missingCount > 0; + if (!atLeastOneNodeRemoved) { + logger.error( + EXCEPTION.getMarker(), + "The new address book does not have at least one node removed. {}", + StackTrace.getStackTrace()); + } + final boolean sizesCorrespond = missingCount == oldSize - newSize; + if (!sizesCorrespond) { + logger.error( + EXCEPTION.getMarker(), + "The new address book contains new nodes instead of only having nodes removed. {}", + StackTrace.getStackTrace()); + } + return sizesCorrespond && atLeastOneNodeRemoved; + } + + /** + * Checks if the old address book contains a proper subset of node ids in the new address book and that the + * nextNodeId value of the new address book is larger. + * + * @param newAddressBook The new address book + * @param oldAddressBook The old address book + * @return true if the new address book has all the nodes in the old address book (ignoring weight differences) plus + * at least one more and its nextNodeId value is larger. + */ + private boolean addedNodeToAddressBook( + @NonNull final AddressBook newAddressBook, @NonNull final AddressBook oldAddressBook) { + int missingCount = 0; + for (final Address address : newAddressBook) { + final NodeId nodeId = address.getNodeId(); + if (oldAddressBook.contains(nodeId)) { + continue; + } + missingCount++; + } + final int newSize = newAddressBook.getSize(); + final int oldSize = oldAddressBook.getSize(); + final boolean atLeastOneNodeAdded = missingCount > 0; + if (!atLeastOneNodeAdded) { + logger.error( + EXCEPTION.getMarker(), + "The new address book does not have at least one node added. {}", + StackTrace.getStackTrace()); + } + final boolean sizesCorrespond = missingCount == newSize - oldSize; + if (!sizesCorrespond) { + logger.error( + EXCEPTION.getMarker(), + "The new address book has nodes removed instead of only having nodes added. {}", + StackTrace.getStackTrace()); + } + return sizesCorrespond && atLeastOneNodeAdded; + } + + /** + * Get the address book in the last usedAddressBook file. + * + * @return the address book in the last usedAddressBook file. + */ + @NonNull + private AddressBook getUsedAddressBook() throws IOException, ParseException { + final String fileContents = getLastAddressBookFileEndsWith("txt"); + return parseAddressBook(fileContents); + } + + /** + * Get the config address book from the last debug addressBook file. + * + * @return the config address book from the last debug addressBook file. + */ + @NonNull + private AddressBook getConfigAddressBook() throws IOException, ParseException { + return getDebugAddressBookAfterHeader(CONFIG_ADDRESS_BOOK_HEADER); + } + + /** + * Get the state address book from the last debug addressBook file. + * + * @return the state address book from the last debug addressBook file. + */ + @NonNull + private AddressBook getStateAddressBook() throws IOException, ParseException { + return getDebugAddressBookAfterHeader(STATE_ADDRESS_BOOK_HEADER); + } + + /** + * Get the address book in the last debug addressBook file after the header. + * + * @param header the header to find. + * @return the address book in the last debug addressBook file after the header. + */ + @NonNull + AddressBook getDebugAddressBookAfterHeader(@NonNull final String header) throws IOException, ParseException { + final String fileContents = getLastAddressBookFileEndsWith(DEBUG); + final String addressBookString = getTextAfterHeader(fileContents, header); + return parseAddressBook(addressBookString); + } + + /** + * Get the text from the fileContents after the header. + * + * @param fileContents the file contents. + * @param header the header to find. + * @return the text from the fileContents after the header. + */ + @NonNull + String getTextAfterHeader(@NonNull final String fileContents, @NonNull final String header) { + Objects.requireNonNull(fileContents, "fileContents must not be null"); + Objects.requireNonNull(header, "header must not be null"); + final int configABHeaderStart = fileContents.indexOf(CONFIG_ADDRESS_BOOK_HEADER); + final int stateABHeaderStart = fileContents.indexOf(STATE_ADDRESS_BOOK_HEADER); + final int usedABHeaderStart = fileContents.indexOf(USED_ADDRESS_BOOK_HEADER); + + final int headerStartIndex = fileContents.indexOf(header); + final int addressBookStartIndex = headerStartIndex + header.length(); + + final int addressBookEndIndex; + if (headerStartIndex == configABHeaderStart) { + addressBookEndIndex = stateABHeaderStart; + } else if (headerStartIndex == stateABHeaderStart) { + addressBookEndIndex = usedABHeaderStart; + } else { + addressBookEndIndex = fileContents.length(); + } + + return fileContents + .substring(addressBookStartIndex, addressBookEndIndex) + .trim(); + } + + /** + * Parse the address book from the given string. + * + * @param addressBookString the address book string. + * @return the address book. + * @throws ParseException if unable to parse the address book. + */ + @NonNull + private AddressBook parseAddressBook(@NonNull final String addressBookString) throws ParseException { + Objects.requireNonNull(addressBookString, "addressBookString must not be null"); + return AddressBookUtils.parseAddressBookText(addressBookString); + } + + /** + * Get the last address book file that ends with the given suffix. + * + * @param suffix the suffix to match. + * @return the last address book file that ends with the given suffix. + * @throws IOException if unable to read the file. + */ + @NonNull + private String getLastAddressBookFileEndsWith(@NonNull final String suffix) throws IOException { + final String nodeId = "node_%s".formatted(this.selfId); + final Path addressBookDirectory = Path.of(addressBookConfig.addressBookDirectory()); + File[] files = addressBookDirectory.toFile().listFiles(File::isFile); + final AtomicReference lastAddressBookDebugFile = new AtomicReference<>(null); + for (int i = 0; i < files.length; i++) { + final String fileName = files[i].getName(); + if (fileName.contains(nodeId) && fileName.endsWith(suffix)) { + final Path lastAddressBookDebugFilePath = lastAddressBookDebugFile.get(); + if (lastAddressBookDebugFilePath == null + || fileName.compareTo(lastAddressBookDebugFilePath + .getFileName() + .toString()) + > 0) { + lastAddressBookDebugFile.set(files[i].toPath()); + } + } + } + return Files.readString(lastAddressBookDebugFile.get()); + } + + @Override + public void onPreHandle( + @NonNull Event event, + @NonNull AddressBookTestingToolState state, + @NonNull Consumer> stateSignatureTransactionCallback) { + event.transactionIterator().forEachRemaining(transaction -> { + // We are not interested in pre-handling any system transactions, as they are + // specific for the platform only.We also don't want to consume deprecated + // EventTransaction.STATE_SIGNATURE_TRANSACTION system transactions in the + // callback,since it's intended to be used only for the new form of encoded system + // transactions in Bytes. Thus, we can directly skip the current + // iteration, if it processes a deprecated system transaction with the + // EventTransaction.STATE_SIGNATURE_TRANSACTION type. + if (transaction.isSystem()) { + return; + } + + // We should consume in the callback the new form of system transactions in Bytes + if (state.areTransactionBytesSystemOnes(transaction)) { + consumeSystemTransaction(transaction, event, stateSignatureTransactionCallback); + } + }); + } + + /** + * Converts a transaction to a {@link StateSignatureTransaction} and then consumes it into a callback. + * + * @param transaction the transaction to consume + * @param event the event that contains the transaction + * @param stateSignatureTransactionCallback the callback to call with the system transaction + */ + private void consumeSystemTransaction( + final Transaction transaction, + final Event event, + final Consumer> stateSignatureTransactionCallback) { + try { + final var stateSignatureTransaction = + StateSignatureTransaction.PROTOBUF.parse(transaction.getApplicationTransaction()); + stateSignatureTransactionCallback.accept(new ScopedSystemTransaction<>( + event.getCreatorId(), event.getSoftwareVersion(), stateSignatureTransaction)); + } catch (final com.hedera.pbj.runtime.ParseException e) { + logger.error("Failed to parse StateSignatureTransaction", e); + } + } + + @Override + public void onSealConsensusRound(@NonNull Round round, @NonNull AddressBookTestingToolState state) { + // no-op + } + + @Override + public void onNewRecoveredState(@NonNull AddressBookTestingToolState recoveredState) { + // no-op + } +} diff --git a/platform-sdk/platform-apps/tests/AddressBookTestingTool/src/test/java/com/swirlds/demo/addressbook/AddressBookTestingToolStateTest.java b/platform-sdk/platform-apps/tests/AddressBookTestingTool/src/test/java/com/swirlds/demo/addressbook/AddressBookTestingToolStateTest.java index 8c1475fd9a5f..fb94ae6d3330 100644 --- a/platform-sdk/platform-apps/tests/AddressBookTestingTool/src/test/java/com/swirlds/demo/addressbook/AddressBookTestingToolStateTest.java +++ b/platform-sdk/platform-apps/tests/AddressBookTestingTool/src/test/java/com/swirlds/demo/addressbook/AddressBookTestingToolStateTest.java @@ -29,7 +29,6 @@ import com.swirlds.config.api.Configuration; import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.config.AddressBookConfig; -import com.swirlds.platform.state.PlatformStateModifier; import com.swirlds.platform.system.BasicSoftwareVersion; import com.swirlds.platform.system.InitTrigger; import com.swirlds.platform.system.Platform; @@ -57,9 +56,9 @@ class AddressBookTestingToolStateTest { private static final int RUNNING_SUM_INDEX = 3; private static AddressBookTestingToolState state; + private static AddressBookTestingToolStateLifecycles stateLifecycles; private AddressBookTestingToolMain main; private Random random; - private PlatformStateModifier platformStateModifier; private Platform platform; private PlatformContext platformContext; private Round round; @@ -76,7 +75,8 @@ class AddressBookTestingToolStateTest { @BeforeAll static void initState() { - state = new AddressBookTestingToolState(FAKE_MERKLE_STATE_LIFECYCLES, mock(Function.class)); + state = new AddressBookTestingToolState(mock(Function.class)); + stateLifecycles = new AddressBookTestingToolStateLifecycles(); FAKE_MERKLE_STATE_LIFECYCLES.initStates(state); } @@ -99,11 +99,10 @@ void setUp() { when(addressBookTestingToolConfig.testScenario()) .thenReturn(String.valueOf(AddressBookTestScenario.GENESIS_NORMAL)); - state.init(platform, initTrigger, softwareVersion); + stateLifecycles.onStateInitialized(state, platform, initTrigger, softwareVersion); main = mock(AddressBookTestingToolMain.class); random = new Random(); - platformStateModifier = mock(PlatformStateModifier.class); round = mock(Round.class); event = mock(ConsensusEvent.class); @@ -136,7 +135,7 @@ void handleConsensusRoundWithApplicationTransaction() { when(consensusTransaction.getApplicationTransaction()).thenReturn(bytes); // When - state.handleConsensusRound(round, platformStateModifier, consumer); + stateLifecycles.onHandleConsensusRound(round, state, consumer); // Then verify(round, times(1)).iterator(); @@ -158,7 +157,7 @@ void handleConsensusRoundWithSystemTransaction() { when(consensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); // When - state.handleConsensusRound(round, platformStateModifier, consumer); + stateLifecycles.onHandleConsensusRound(round, state, consumer); // Then verify(round, times(1)).iterator(); @@ -191,7 +190,7 @@ void handleConsensusRoundWithMultipleSystemTransaction() { when(thirdConsensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); // When - state.handleConsensusRound(round, platformStateModifier, consumer); + stateLifecycles.onHandleConsensusRound(round, state, consumer); // Then verify(round, times(1)).iterator(); @@ -211,7 +210,7 @@ void handleConsensusRoundWithDeprecatedSystemTransaction() { when(consensusTransaction.isSystem()).thenReturn(true); // When - state.handleConsensusRound(round, platformStateModifier, consumer); + stateLifecycles.onHandleConsensusRound(round, state, consumer); // Then verify(round, times(1)).iterator(); @@ -236,7 +235,7 @@ void handleConsensusRoundWithEmptyTransaction() { when(consensusTransaction.getApplicationTransaction()).thenReturn(emptyStateSignatureTransactionBytes); // When - state.handleConsensusRound(round, platformStateModifier, consumer); + stateLifecycles.onHandleConsensusRound(round, state, consumer); // Then verify(round, times(1)).iterator(); @@ -264,7 +263,7 @@ void preHandleEventWithMultipleSystemTransaction() { when(thirdConsensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); // When - state.preHandle(event, consumer); + stateLifecycles.onPreHandle(event, state, consumer); // Then assertThat(consumedTransactions).hasSize(3); @@ -280,7 +279,7 @@ void preHandleEventWithSystemTransaction() { when(consensusTransaction.getApplicationTransaction()).thenReturn(emptyStateSignatureBytes); // When - state.preHandle(event, consumer); + stateLifecycles.onPreHandle(event, state, consumer); // Then assertThat(consumedTransactions).hasSize(1); @@ -295,7 +294,7 @@ void preHandleEventWithDeprecatedSystemTransaction() { when(consensusTransaction.isSystem()).thenReturn(true); // When - state.preHandle(event, consumer); + stateLifecycles.onPreHandle(event, state, consumer); // Then assertThat(consumedTransactions).isEmpty(); @@ -312,7 +311,7 @@ void preHandleEventWithEmptyTransaction() { when(consensusTransaction.getApplicationTransaction()).thenReturn(emptyStateSignatureBytes); // When - state.preHandle(event, consumer); + stateLifecycles.onPreHandle(event, state, consumer); // Then assertThat(consumedTransactions).isEmpty(); diff --git a/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/main/java/com/swirlds/demo/consistency/ConsistencyTestingToolMain.java b/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/main/java/com/swirlds/demo/consistency/ConsistencyTestingToolMain.java index 54b70d5fe38c..80978065e009 100644 --- a/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/main/java/com/swirlds/demo/consistency/ConsistencyTestingToolMain.java +++ b/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/main/java/com/swirlds/demo/consistency/ConsistencyTestingToolMain.java @@ -26,7 +26,7 @@ import com.swirlds.common.constructable.ConstructableRegistry; import com.swirlds.common.constructable.ConstructableRegistryException; import com.swirlds.common.platform.NodeId; -import com.swirlds.platform.state.PlatformMerkleStateRoot; +import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.system.BasicSoftwareVersion; import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.SoftwareVersion; @@ -42,7 +42,7 @@ /** * A testing app for guaranteeing proper handling of transactions after a restart */ -public class ConsistencyTestingToolMain implements SwirldMain { +public class ConsistencyTestingToolMain implements SwirldMain { private static final Logger logger = LogManager.getLogger(ConsistencyTestingToolMain.class); @@ -57,8 +57,8 @@ public class ConsistencyTestingToolMain implements SwirldMain { ConstructableRegistry constructableRegistry = ConstructableRegistry.getInstance(); constructableRegistry.registerConstructable( new ClassConstructorPair(ConsistencyTestingToolState.class, () -> { - ConsistencyTestingToolState consistencyTestingToolState = new ConsistencyTestingToolState( - FAKE_MERKLE_STATE_LIFECYCLES, version -> new BasicSoftwareVersion(version.major())); + ConsistencyTestingToolState consistencyTestingToolState = + new ConsistencyTestingToolState(version -> new BasicSoftwareVersion(version.major())); // Don't call FAKE_MERKLE_STATE_LIFECYCLES.initStates(consistencyTestingToolState) here. // The stub states are automatically initialized upon loading the state from disk, // or after finishing a reconnect. @@ -115,14 +115,23 @@ public void run() { */ @Override @NonNull - public PlatformMerkleStateRoot newMerkleStateRoot() { - final PlatformMerkleStateRoot state = new ConsistencyTestingToolState( - FAKE_MERKLE_STATE_LIFECYCLES, version -> new BasicSoftwareVersion(softwareVersion.getVersion())); + public ConsistencyTestingToolState newMerkleStateRoot() { + final ConsistencyTestingToolState state = + new ConsistencyTestingToolState(version -> new BasicSoftwareVersion(softwareVersion.getVersion())); FAKE_MERKLE_STATE_LIFECYCLES.initStates(state); return state; } + /** + * {@inheritDoc} + */ + @Override + @NonNull + public StateLifecycles newStateLifecycles() { + return new ConsistencyTestingToolStateLifecycles(); + } + /** * {@inheritDoc} */ diff --git a/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/main/java/com/swirlds/demo/consistency/ConsistencyTestingToolState.java b/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/main/java/com/swirlds/demo/consistency/ConsistencyTestingToolState.java index 78117e97e578..668c8e0529c5 100644 --- a/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/main/java/com/swirlds/demo/consistency/ConsistencyTestingToolState.java +++ b/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/main/java/com/swirlds/demo/consistency/ConsistencyTestingToolState.java @@ -19,20 +19,14 @@ import static com.swirlds.common.utility.ByteUtils.byteArrayToLong; import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; import static com.swirlds.logging.legacy.LogMarker.STARTUP; -import static com.swirlds.platform.test.fixtures.state.FakeStateLifecycles.FAKE_MERKLE_STATE_LIFECYCLES; import com.hedera.hapi.node.base.SemanticVersion; import com.hedera.hapi.platform.event.StateSignatureTransaction; import com.hedera.pbj.runtime.ParseException; -import com.swirlds.common.config.StateCommonConfig; import com.swirlds.common.constructable.ConstructableIgnored; import com.swirlds.common.utility.NonCryptographicHashing; import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.state.PlatformMerkleStateRoot; -import com.swirlds.platform.state.PlatformStateModifier; -import com.swirlds.platform.state.StateLifecycles; -import com.swirlds.platform.system.InitTrigger; -import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.Round; import com.swirlds.platform.system.SoftwareVersion; import com.swirlds.platform.system.events.Event; @@ -40,12 +34,7 @@ import com.swirlds.platform.system.transaction.Transaction; import com.swirlds.state.merkle.singleton.StringLeaf; import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.Nullable; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.file.Files; import java.nio.file.Path; -import java.time.Duration; import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -70,14 +59,6 @@ private static class ClassVersion { private static final int STATE_LONG_INDEX = 3; private static final int ROUND_HANDLED_INDEX = 4; - /** - * The history of transactions that have been handled by this app. - *

    - * A deep copy of this object is NOT created when this state is copied. This object does not affect the hash of this - * node. - */ - private final TransactionHandlingHistory transactionHandlingHistory; - /** * The true "state" of this app. This long value is updated with every transaction, and with every round. *

    @@ -87,8 +68,8 @@ private static class ClassVersion { /** * The number of rounds handled by this app. Is incremented each time - * {@link #handleConsensusRound(Round, PlatformStateModifier)} is called. Note that this may not actually equal the round - * number, since we don't call {@link #handleConsensusRound(Round, PlatformStateModifier)} for rounds with no events. + * {@link ConsistencyTestingToolStateLifecycles#onHandleConsensusRound(Round, ConsistencyTestingToolState, Consumer < ScopedSystemTransaction < StateSignatureTransaction >>)} is called. Note that this may not actually equal the round + * number, since we don't call {@link ConsistencyTestingToolStateLifecycles#onHandleConsensusRound(Round, ConsistencyTestingToolState, Consumer>)} for rounds with no events. * *

    * Affects the hash of this node. @@ -96,12 +77,12 @@ private static class ClassVersion { private long roundsHandled = 0; /** - * If not zero, and we are handling the first round after genesis, configure a freeze this duration later. + * The history of transactions that have been handled by this app. *

    - * Does not affect the hash of this node (although actions may be taken based on this info that DO affect the - * hash). + * A deep copy of this object is NOT created when this state is copied. This object does not affect the hash of this + * node. */ - private Duration freezeAfterGenesis = null; + private final TransactionHandlingHistory transactionHandlingHistory; /** * The set of transactions that have been preconsensus-handled by this app, but haven't yet been @@ -110,65 +91,22 @@ private static class ClassVersion { *

    * Does not affect the hash of this node. */ - private Set transactionsAwaitingPostHandle = ConcurrentHashMap.newKeySet(); + private final Set transactionsAwaitingPostHandle; /** * Constructor */ - public ConsistencyTestingToolState( - @NonNull final StateLifecycles lifecycles, - @NonNull final Function versionFactory) { - super(lifecycles, versionFactory); + public ConsistencyTestingToolState(@NonNull final Function versionFactory) { + super(versionFactory); + transactionHandlingHistory = new TransactionHandlingHistory(); + transactionsAwaitingPostHandle = ConcurrentHashMap.newKeySet(); logger.info(STARTUP.getMarker(), "New State Constructed."); - - this.transactionHandlingHistory = new TransactionHandlingHistory(); } /** - * Copy constructor - * - * @param that the state to copy + * Initialize the state */ - private ConsistencyTestingToolState(@NonNull final ConsistencyTestingToolState that) { - super(Objects.requireNonNull(that)); - - this.transactionHandlingHistory = that.transactionHandlingHistory; - this.stateLong = that.stateLong; - this.roundsHandled = that.roundsHandled; - this.freezeAfterGenesis = that.freezeAfterGenesis; - this.transactionsAwaitingPostHandle = that.transactionsAwaitingPostHandle; - } - - /** - * {@inheritDoc} - */ - @Override - public void init( - @NonNull final Platform platform, - @NonNull final InitTrigger trigger, - @Nullable final SoftwareVersion previousSoftwareVersion) { - - Objects.requireNonNull(platform); - Objects.requireNonNull(trigger); - - final StateCommonConfig stateConfig = - platform.getContext().getConfiguration().getConfigData(StateCommonConfig.class); - final ConsistencyTestingToolConfig testingToolConfig = - platform.getContext().getConfiguration().getConfigData(ConsistencyTestingToolConfig.class); - - final Path logFileDirectory = stateConfig - .savedStateDirectory() - .resolve(testingToolConfig.logfileDirectory()) - .resolve(Long.toString(platform.getSelfId().id())); - try { - Files.createDirectories(logFileDirectory); - } catch (final IOException e) { - throw new UncheckedIOException("unable to set up file system for consistency data", e); - } - final Path logFilePath = logFileDirectory.resolve("ConsistencyTestLog.csv"); - - this.freezeAfterGenesis = testingToolConfig.freezeAfterGenesis(); - + void initState(Path logFilePath) { final StringLeaf stateLongLeaf = getChild(STATE_LONG_INDEX); if (stateLongLeaf != null && stateLongLeaf.getLabel() != null) { this.stateLong = Long.parseLong(stateLongLeaf.getLabel()); @@ -179,9 +117,51 @@ public void init( this.roundsHandled = Long.parseLong(roundsHandledLeaf.getLabel()); logger.info(STARTUP.getMarker(), "State initialized with {} rounds handled.", roundsHandled); } - transactionHandlingHistory.init(logFilePath); - FAKE_MERKLE_STATE_LIFECYCLES.initStates(this); + } + + /** + * @return the number of rounds handled + */ + long getRoundsHandled() { + return roundsHandled; + } + + /** + * Increment the number of rounds handled + */ + void incrementRoundsHandled() { + roundsHandled++; + setChild(ROUND_HANDLED_INDEX, new StringLeaf(Long.toString(roundsHandled))); + } + + /** + * @return the state represented by a long + */ + long getStateLong() { + return stateLong; + } + + /** + * Sets the state + * @param stateLong state represtented by a long + */ + void setStateLong(final long stateLong) { + this.stateLong = stateLong; + setChild(STATE_LONG_INDEX, new StringLeaf(Long.toString(stateLong))); + } + + /** + * Copy constructor + * + * @param that the state to copy + */ + private ConsistencyTestingToolState(@NonNull final ConsistencyTestingToolState that) { + super(Objects.requireNonNull(that)); + this.stateLong = that.stateLong; + this.roundsHandled = that.roundsHandled; + this.transactionHandlingHistory = that.transactionHandlingHistory; + this.transactionsAwaitingPostHandle = that.transactionsAwaitingPostHandle; } /** @@ -208,19 +188,37 @@ public int getMinimumSupportedVersion() { return ClassVersion.ORIGINAL; } - /** - * {@inheritDoc} - */ - @Override - @NonNull - public synchronized ConsistencyTestingToolState copy() { - throwIfImmutable(); - setImmutable(true); - return new ConsistencyTestingToolState(this); + private void processRound(Round round) { + stateLong = NonCryptographicHashing.hash64(stateLong, round.getRoundNum()); + transactionHandlingHistory.processRound(ConsistencyTestingToolRound.fromRound(round, stateLong)); + setChild(STATE_LONG_INDEX, new StringLeaf(Long.toString(stateLong))); + } + + void processTransactions( + Round round, + @NonNull final Consumer> stateSignatureTransaction) { + incrementRoundsHandled(); + + round.forEachEventTransaction((ev, tx) -> { + if (isSystemTransaction(tx)) { + consumeSystemTransaction(tx, ev, stateSignatureTransaction); + } else { + applyTransactionToState(tx); + } + }); + + processRound(round); + } + + void processPrehandle(Transaction transaction) { + final long transactionContents = getTransactionContents(transaction); + if (!transactionsAwaitingPostHandle.add(transactionContents)) { + logger.error(EXCEPTION.getMarker(), "Transaction {} was prehandled more than once.", transactionContents); + } } /** - * Sets the new {@link #stateLong} to the non-cryptographic hash of the existing state, and the contents of the + * Sets the new {@link ConsistencyTestingToolState#stateLong} to the non-cryptographic hash of the existing state, and the contents of the * transaction being handled * * @param transaction the transaction to apply to the state @@ -231,79 +229,18 @@ private void applyTransactionToState(final @NonNull ConsensusTransaction transac return; } - final long transactionContents = - byteArrayToLong(transaction.getApplicationTransaction().toByteArray(), 0); + final long transactionContents = getTransactionContents(transaction); if (!transactionsAwaitingPostHandle.remove(transactionContents)) { logger.error(EXCEPTION.getMarker(), "Transaction {} was not prehandled.", transactionContents); } stateLong = NonCryptographicHashing.hash64(stateLong, transactionContents); + setChild(STATE_LONG_INDEX, new StringLeaf(Long.toString(stateLong))); } - /** - * Keeps track of which transactions have been prehandled. - */ - @Override - public void preHandle( - @NonNull final Event event, - @NonNull final Consumer> stateSignatureTransaction) { - event.forEachTransaction(transaction -> { - if (transaction.isSystem()) { - return; - } - - if (isSystemTransaction(transaction)) { - consumeSystemTransaction(transaction, event, stateSignatureTransaction); - return; - } - final long transactionContents = - byteArrayToLong(transaction.getApplicationTransaction().toByteArray(), 0); - - if (!transactionsAwaitingPostHandle.add(transactionContents)) { - logger.error( - EXCEPTION.getMarker(), "Transaction {} was prehandled more than once.", transactionContents); - } - }); - } - - /** - * Modifies the state based on each transaction in the round - *

    - * Writes the round and its contents to a log on disk - */ - @Override - public void handleConsensusRound( - final @NonNull Round round, - final @NonNull PlatformStateModifier platformState, - @NonNull final Consumer> stateSignatureTransaction) { - Objects.requireNonNull(round); - Objects.requireNonNull(platformState); - - if (roundsHandled == 0 && !freezeAfterGenesis.equals(Duration.ZERO)) { - // This is the first round after genesis. - logger.info( - STARTUP.getMarker(), - "Setting freeze time to {} seconds after genesis.", - freezeAfterGenesis.getSeconds()); - platformState.setFreezeTime(round.getConsensusTimestamp().plus(freezeAfterGenesis)); - } - - roundsHandled++; - - round.forEachEventTransaction((ev, tx) -> { - if (isSystemTransaction(tx)) { - consumeSystemTransaction(tx, ev, stateSignatureTransaction); - } else { - applyTransactionToState(tx); - } - }); - stateLong = NonCryptographicHashing.hash64(stateLong, round.getRoundNum()); - - transactionHandlingHistory.processRound(ConsistencyTestingToolRound.fromRound(round, stateLong)); - - setChild(ROUND_HANDLED_INDEX, new StringLeaf(Long.toString(roundsHandled))); - setChild(STATE_LONG_INDEX, new StringLeaf(Long.toString(stateLong))); + private static long getTransactionContents(Transaction transaction) { + return byteArrayToLong(transaction.getApplicationTransaction().toByteArray(), 0); } /** @@ -312,11 +249,11 @@ public void handleConsensusRound( * @param transaction the transaction to check * @return true if the transaction is a system transaction, false otherwise */ - private boolean isSystemTransaction(final @NonNull Transaction transaction) { + boolean isSystemTransaction(final @NonNull Transaction transaction) { return transaction.getApplicationTransaction().length() > 8; } - private void consumeSystemTransaction( + void consumeSystemTransaction( final @NonNull Transaction transaction, final @NonNull Event event, final @NonNull Consumer> @@ -330,4 +267,15 @@ private void consumeSystemTransaction( logger.error("Failed to parse StateSignatureTransaction", e); } } + + /** + * {@inheritDoc} + */ + @Override + @NonNull + public synchronized ConsistencyTestingToolState copy() { + throwIfImmutable(); + setImmutable(true); + return new ConsistencyTestingToolState(this); + } } diff --git a/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/main/java/com/swirlds/demo/consistency/ConsistencyTestingToolStateLifecycles.java b/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/main/java/com/swirlds/demo/consistency/ConsistencyTestingToolStateLifecycles.java new file mode 100644 index 000000000000..395aa13c2ab4 --- /dev/null +++ b/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/main/java/com/swirlds/demo/consistency/ConsistencyTestingToolStateLifecycles.java @@ -0,0 +1,168 @@ +/* + * 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.swirlds.demo.consistency; + +import static com.swirlds.logging.legacy.LogMarker.STARTUP; +import static com.swirlds.platform.test.fixtures.state.FakeStateLifecycles.FAKE_MERKLE_STATE_LIFECYCLES; +import static java.util.Objects.requireNonNull; + +import com.hedera.hapi.platform.event.StateSignatureTransaction; +import com.swirlds.common.config.StateCommonConfig; +import com.swirlds.common.context.PlatformContext; +import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; +import com.swirlds.platform.state.PlatformStateModifier; +import com.swirlds.platform.state.StateLifecycles; +import com.swirlds.platform.system.InitTrigger; +import com.swirlds.platform.system.Platform; +import com.swirlds.platform.system.Round; +import com.swirlds.platform.system.SoftwareVersion; +import com.swirlds.platform.system.address.AddressBook; +import com.swirlds.platform.system.events.Event; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.function.Consumer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * This class handles lifecycle events for the {@link ConsistencyTestingToolState} + */ +public class ConsistencyTestingToolStateLifecycles implements StateLifecycles { + + private static final Logger logger = LogManager.getLogger(ConsistencyTestingToolState.class); + + /** + * If not zero, and we are handling the first round after genesis, configure a freeze this duration later. + *

    + * Does not affect the hash of this node (although actions may be taken based on this info that DO affect the + * hash). + */ + private Duration freezeAfterGenesis = null; + + @Override + public void onStateInitialized( + @NonNull ConsistencyTestingToolState state, + @NonNull Platform platform, + @NonNull InitTrigger trigger, + @Nullable SoftwareVersion previousVersion) { + requireNonNull(platform); + requireNonNull(trigger); + + final StateCommonConfig stateConfig = + platform.getContext().getConfiguration().getConfigData(StateCommonConfig.class); + final ConsistencyTestingToolConfig testingToolConfig = + platform.getContext().getConfiguration().getConfigData(ConsistencyTestingToolConfig.class); + + final Path logFileDirectory = stateConfig + .savedStateDirectory() + .resolve(testingToolConfig.logfileDirectory()) + .resolve(Long.toString(platform.getSelfId().id())); + try { + Files.createDirectories(logFileDirectory); + } catch (final IOException e) { + throw new UncheckedIOException("unable to set up file system for consistency data", e); + } + final Path logFilePath = logFileDirectory.resolve("ConsistencyTestLog.csv"); + + this.freezeAfterGenesis = testingToolConfig.freezeAfterGenesis(); + + state.initState(logFilePath); + + FAKE_MERKLE_STATE_LIFECYCLES.initStates(state); + } + + /** + * Modifies the state based on each transaction in the round + *

    + * Writes the round and its contents to a log on disk + */ + @Override + public void onHandleConsensusRound( + @NonNull Round round, + @NonNull ConsistencyTestingToolState state, + @NonNull Consumer> stateSignatureTransactionCallback) { + requireNonNull(round); + requireNonNull(state); + PlatformStateModifier platformState = state.getWritablePlatformState(); + requireNonNull(platformState); + + if (state.getRoundsHandled() == 0 && !freezeAfterGenesis.equals(Duration.ZERO)) { + // This is the first round after genesis. + logger.info( + STARTUP.getMarker(), + "Setting freeze time to {} seconds after genesis.", + freezeAfterGenesis.getSeconds()); + platformState.setFreezeTime(round.getConsensusTimestamp().plus(freezeAfterGenesis)); + } + + state.processTransactions(round, stateSignatureTransactionCallback); + } + + /** + * Keeps track of which transactions have been prehandled. + */ + @Override + public void onPreHandle( + @NonNull Event event, + @NonNull ConsistencyTestingToolState state, + @NonNull Consumer> stateSignatureTransactionCallback) { + event.forEachTransaction(transaction -> { + if (transaction.isSystem()) { + return; + } + + if (state.isSystemTransaction(transaction)) { + state.consumeSystemTransaction(transaction, event, stateSignatureTransactionCallback); + return; + } + + state.processPrehandle(transaction); + }); + } + + /** + * {@inheritDoc} + */ + @Override + public void onSealConsensusRound(@NonNull Round round, @NonNull ConsistencyTestingToolState state) { + // no-op + } + + /** + * {@inheritDoc} + */ + @Override + public void onUpdateWeight( + @NonNull ConsistencyTestingToolState state, + @NonNull AddressBook configAddressBook, + @NonNull PlatformContext context) { + // no-op + } + + /** + * {@inheritDoc} + */ + @Override + public void onNewRecoveredState(@NonNull ConsistencyTestingToolState recoveredState) { + // no-op + } +} diff --git a/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/main/java/com/swirlds/demo/consistency/TransactionHandlingHistory.java b/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/main/java/com/swirlds/demo/consistency/TransactionHandlingHistory.java index 0a9840d3c4a3..db356fb0f407 100644 --- a/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/main/java/com/swirlds/demo/consistency/TransactionHandlingHistory.java +++ b/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/main/java/com/swirlds/demo/consistency/TransactionHandlingHistory.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. @@ -23,6 +23,7 @@ import edu.umd.cs.findbugs.annotations.Nullable; import java.io.BufferedReader; import java.io.BufferedWriter; +import java.io.Closeable; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; @@ -48,7 +49,7 @@ * can be confirmed to match the original handling. NOTE: Partially written log lines are simply ignored by this tool, * so it is NOT verifying handling of transactions in a partially handled round at the time of a crash. */ -public class TransactionHandlingHistory { +public class TransactionHandlingHistory implements Closeable { private static final Logger logger = LogManager.getLogger(TransactionHandlingHistory.class); /** @@ -118,7 +119,8 @@ private void tryReadLog() { logger.info(STARTUP.getMarker(), "Log file found. Parsing previous history"); - try (final BufferedReader reader = new BufferedReader(new FileReader(logFilePath.toFile()))) { + try (FileReader in = new FileReader(logFilePath.toFile()); + final BufferedReader reader = new BufferedReader(in)) { reader.lines().forEach(line -> { final ConsistencyTestingToolRound parsedRound = ConsistencyTestingToolRound.fromString(line); @@ -272,4 +274,12 @@ public List processRound(final @NonNull ConsistencyTestingToolRound roun return errors; } + + public void close() { + try { + writer.close(); + } catch (final IOException e) { + throw new UncheckedIOException("Failed to close writer for transaction handling history", e); + } + } } diff --git a/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/test/java/com/swirlds/demo/consistency/ConsistencyTestingToolStateTest.java b/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/test/java/com/swirlds/demo/consistency/ConsistencyTestingToolStateTest.java index 23a06e6bb7ad..070d2a5643bb 100644 --- a/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/test/java/com/swirlds/demo/consistency/ConsistencyTestingToolStateTest.java +++ b/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/test/java/com/swirlds/demo/consistency/ConsistencyTestingToolStateTest.java @@ -30,7 +30,6 @@ import com.swirlds.common.platform.NodeId; import com.swirlds.config.api.Configuration; import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; -import com.swirlds.platform.state.PlatformStateModifier; import com.swirlds.platform.system.BasicSoftwareVersion; import com.swirlds.platform.system.InitTrigger; import com.swirlds.platform.system.Platform; @@ -54,8 +53,8 @@ public class ConsistencyTestingToolStateTest { private static ConsistencyTestingToolState state; + private static ConsistencyTestingToolStateLifecycles stateLifecycle; private Random random; - private PlatformStateModifier platformStateModifier; private Platform platform; private PlatformContext platformContext; private Round round; @@ -72,7 +71,8 @@ public class ConsistencyTestingToolStateTest { @BeforeAll static void initState() { - state = new ConsistencyTestingToolState(FAKE_MERKLE_STATE_LIFECYCLES, mock(Function.class)); + state = new ConsistencyTestingToolState(mock(Function.class)); + stateLifecycle = new ConsistencyTestingToolStateLifecycles(); FAKE_MERKLE_STATE_LIFECYCLES.initStates(state); } @@ -95,10 +95,9 @@ void setUp() { when(stateCommonConfig.savedStateDirectory()).thenReturn(Path.of("consistency-test")); when(consistencyTestingToolConfig.logfileDirectory()).thenReturn("consistency-test"); - state.init(platform, initTrigger, softwareVersion); + stateLifecycle.onStateInitialized(state, platform, initTrigger, softwareVersion); random = new Random(); - platformStateModifier = mock(PlatformStateModifier.class); round = mock(Round.class); event = mock(ConsensusEvent.class); @@ -130,7 +129,7 @@ void handleConsensusRoundWithApplicationTransaction() { .when(round) .forEachEventTransaction(any()); - state.handleConsensusRound(round, platformStateModifier, consumer); + stateLifecycle.onHandleConsensusRound(round, state, consumer); assertThat(consumedTransactions).isEmpty(); } @@ -149,7 +148,7 @@ void handleConsensusRoundWithSystemTransaction() { .when(round) .forEachEventTransaction(any()); - state.handleConsensusRound(round, platformStateModifier, consumer); + stateLifecycle.onHandleConsensusRound(round, state, consumer); assertThat(consumedTransactions).hasSize(1); } @@ -176,7 +175,7 @@ void handleConsensusRoundWithMultipleSystemTransactions() { .forEachEventTransaction(any()); // When - state.handleConsensusRound(round, platformStateModifier, consumer); + stateLifecycle.onHandleConsensusRound(round, state, consumer); assertThat(consumedTransactions).hasSize(3); } @@ -194,7 +193,7 @@ void handleConsensusRoundWithDeprecatedSystemTransaction() { .when(round) .forEachEventTransaction(any()); - state.handleConsensusRound(round, platformStateModifier, consumer); + stateLifecycle.onHandleConsensusRound(round, state, consumer); assertThat(consumedTransactions).isEmpty(); } @@ -219,7 +218,7 @@ void preHandleEventWithMultipleSystemTransactions() { .when(event) .forEachTransaction(any()); - state.preHandle(event, consumer); + stateLifecycle.onPreHandle(event, state, consumer); assertThat(consumedTransactions).hasSize(3); } @@ -237,7 +236,7 @@ void preHandleEventWithSystemTransaction() { .when(event) .forEachTransaction(any()); - state.preHandle(event, consumer); + stateLifecycle.onPreHandle(event, state, consumer); assertThat(consumedTransactions).hasSize(1); } @@ -255,7 +254,7 @@ void preHandleEventWithApplicationTransaction() { .when(event) .forEachTransaction(any()); - state.preHandle(event, consumer); + stateLifecycle.onPreHandle(event, state, consumer); assertThat(consumedTransactions).isEmpty(); } @@ -272,7 +271,7 @@ void preHandleEventWithDeprecatedSystemTransaction() { .when(event) .forEachTransaction(any()); - state.preHandle(event, consumer); + stateLifecycle.onPreHandle(event, state, consumer); assertThat(consumedTransactions).isEmpty(); } diff --git a/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/test/java/com/swirlds/demo/consistency/TransactionHandlingHistoryTests.java b/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/test/java/com/swirlds/demo/consistency/TransactionHandlingHistoryTests.java index a3141825e612..10158852cd3f 100644 --- a/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/test/java/com/swirlds/demo/consistency/TransactionHandlingHistoryTests.java +++ b/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/test/java/com/swirlds/demo/consistency/TransactionHandlingHistoryTests.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. @@ -64,141 +64,150 @@ private String getLogFileContents() { @DisplayName("New rounds are added to history and logged to file") void newRoundsHandled() { assertFalse(Files.exists(logFilePath), "Log file shouldn't exist yet"); + try (final TransactionHandlingHistory history = new TransactionHandlingHistory()) { + history.init(logFilePath); - final TransactionHandlingHistory history = new TransactionHandlingHistory(); - history.init(logFilePath); + final ConsistencyTestingToolRound round1 = new ConsistencyTestingToolRound(1, 1, List.of(1L, 2L, 3L)); + final ConsistencyTestingToolRound round2 = new ConsistencyTestingToolRound(2, 22, List.of(6L, 5L, 4L)); - final ConsistencyTestingToolRound round1 = new ConsistencyTestingToolRound(1, 1, List.of(1L, 2L, 3L)); - final ConsistencyTestingToolRound round2 = new ConsistencyTestingToolRound(2, 22, List.of(6L, 5L, 4L)); + assertTrue(history.processRound(round1).isEmpty(), "No errors should have occurred"); + assertTrue(Files.exists(logFilePath), "Log file should exist"); + assertEquals(1, getLogFileContents().lines().count(), "Log file should have 1 round recorded"); - assertTrue(history.processRound(round1).isEmpty(), "No errors should have occurred"); - assertTrue(Files.exists(logFilePath), "Log file should exist"); - assertEquals(1, getLogFileContents().lines().count(), "Log file should have 1 round recorded"); - - assertTrue(history.processRound(round2).isEmpty(), "No errors should have occurred"); - assertEquals(2, getLogFileContents().lines().count(), "Log file should have 2 rounds recorded"); + assertTrue(history.processRound(round2).isEmpty(), "No errors should have occurred"); + assertEquals(2, getLogFileContents().lines().count(), "Log file should have 2 rounds recorded"); + } } @Test @DisplayName("Duplicate transactions are detected") void duplicateTransaction() { - final TransactionHandlingHistory history = new TransactionHandlingHistory(); - history.init(logFilePath); + try (final TransactionHandlingHistory history = new TransactionHandlingHistory()) { + history.init(logFilePath); - final ConsistencyTestingToolRound round1 = new ConsistencyTestingToolRound(1, 1, List.of(1L, 2L, 3L)); - final ConsistencyTestingToolRound round2 = new ConsistencyTestingToolRound(2, 22, List.of(3L, 5L, 4L)); + final ConsistencyTestingToolRound round1 = new ConsistencyTestingToolRound(1, 1, List.of(1L, 2L, 3L)); + final ConsistencyTestingToolRound round2 = new ConsistencyTestingToolRound(2, 22, List.of(3L, 5L, 4L)); - assertTrue(history.processRound(round1).isEmpty(), "No errors should have occurred"); - assertEquals(1, history.processRound(round2).size(), "An error should have occurred"); + assertTrue(history.processRound(round1).isEmpty(), "No errors should have occurred"); + assertEquals(1, history.processRound(round2).size(), "An error should have occurred"); - assertEquals(2, getLogFileContents().lines().count(), "Log file should have 2 rounds recorded"); + assertEquals(2, getLogFileContents().lines().count(), "Log file should have 2 rounds recorded"); + } } @Test @DisplayName("A new round that matches a historical round is handled correctly") void historicalRound() { - final TransactionHandlingHistory history = new TransactionHandlingHistory(); - history.init(logFilePath); + try (final TransactionHandlingHistory history = new TransactionHandlingHistory()) { + history.init(logFilePath); - final ConsistencyTestingToolRound round1 = new ConsistencyTestingToolRound(1, 1, List.of(1L, 2L, 3L)); + final ConsistencyTestingToolRound round1 = new ConsistencyTestingToolRound(1, 1, List.of(1L, 2L, 3L)); - // exactly the same as the first round - final ConsistencyTestingToolRound round2 = new ConsistencyTestingToolRound(1, 1, List.of(1L, 2L, 3L)); + // exactly the same as the first round + final ConsistencyTestingToolRound round2 = new ConsistencyTestingToolRound(1, 1, List.of(1L, 2L, 3L)); - assertTrue(history.processRound(round1).isEmpty(), "No errors should have occurred"); - assertTrue(history.processRound(round2).isEmpty(), "No errors should have occurred"); + assertTrue(history.processRound(round1).isEmpty(), "No errors should have occurred"); + assertTrue(history.processRound(round2).isEmpty(), "No errors should have occurred"); - assertEquals(1, getLogFileContents().lines().count(), "Log file should have only have 1 round recorded"); + assertEquals(1, getLogFileContents().lines().count(), "Log file should have only have 1 round recorded"); + } } @Test @DisplayName("A new round that matches a historical round but with incorrect state is detected") void historicalRoundWithIncorrectState() { - final TransactionHandlingHistory history = new TransactionHandlingHistory(); - history.init(logFilePath); + try (final TransactionHandlingHistory history = new TransactionHandlingHistory()) { + history.init(logFilePath); - final ConsistencyTestingToolRound round1 = new ConsistencyTestingToolRound(1, 1, List.of(1L, 2L, 3L)); + final ConsistencyTestingToolRound round1 = new ConsistencyTestingToolRound(1, 1, List.of(1L, 2L, 3L)); - // exactly the same as the first round, but with incorrect state - final ConsistencyTestingToolRound round2 = new ConsistencyTestingToolRound(1, 66, List.of(1L, 2L, 3L)); + // exactly the same as the first round, but with incorrect state + final ConsistencyTestingToolRound round2 = new ConsistencyTestingToolRound(1, 66, List.of(1L, 2L, 3L)); - assertTrue(history.processRound(round1).isEmpty(), "No errors should have occurred"); - assertEquals(1, history.processRound(round2).size(), "An error should have occurred"); + assertTrue(history.processRound(round1).isEmpty(), "No errors should have occurred"); + assertEquals(1, history.processRound(round2).size(), "An error should have occurred"); - assertEquals(1, getLogFileContents().lines().count(), "Log file should have only have 1 round recorded"); + assertEquals(1, getLogFileContents().lines().count(), "Log file should have only have 1 round recorded"); + } } @Test @DisplayName("A new round that matches a historical round but with incorrect transactions is detected") void historicalRoundWithIncorrectTransactions() { - final TransactionHandlingHistory history = new TransactionHandlingHistory(); - history.init(logFilePath); + try (final TransactionHandlingHistory history = new TransactionHandlingHistory()) { + history.init(logFilePath); - final ConsistencyTestingToolRound round1 = new ConsistencyTestingToolRound(1, 1, List.of(1L, 2L, 3L)); + final ConsistencyTestingToolRound round1 = new ConsistencyTestingToolRound(1, 1, List.of(1L, 2L, 3L)); - // exactly the same as the first round, but with an extra transaction - final ConsistencyTestingToolRound round2 = new ConsistencyTestingToolRound(1, 1, List.of(1L, 2L, 3L, 4L)); + // exactly the same as the first round, but with an extra transaction + final ConsistencyTestingToolRound round2 = new ConsistencyTestingToolRound(1, 1, List.of(1L, 2L, 3L, 4L)); - assertTrue(history.processRound(round1).isEmpty(), "No errors should have occurred"); - assertEquals(1, history.processRound(round2).size(), "An error should have occurred"); + assertTrue(history.processRound(round1).isEmpty(), "No errors should have occurred"); + assertEquals(1, history.processRound(round2).size(), "An error should have occurred"); - assertEquals(1, getLogFileContents().lines().count(), "Log file should have only have 1 round recorded"); + assertEquals(1, getLogFileContents().lines().count(), "Log file should have only have 1 round recorded"); + } } @Test @DisplayName("A new round with non-increasing round number is detected") void nonIncreasingRoundNumber() { - final TransactionHandlingHistory history = new TransactionHandlingHistory(); - history.init(logFilePath); + try (final TransactionHandlingHistory history = new TransactionHandlingHistory()) { + history.init(logFilePath); - final ConsistencyTestingToolRound round1 = new ConsistencyTestingToolRound(3, 1, List.of(1L, 2L, 3L)); - final ConsistencyTestingToolRound round2 = new ConsistencyTestingToolRound(2, 22, List.of(6L, 5L, 4L)); + final ConsistencyTestingToolRound round1 = new ConsistencyTestingToolRound(3, 1, List.of(1L, 2L, 3L)); + final ConsistencyTestingToolRound round2 = new ConsistencyTestingToolRound(2, 22, List.of(6L, 5L, 4L)); - assertTrue(history.processRound(round1).isEmpty(), "No errors should have occurred"); - assertEquals(1, history.processRound(round2).size(), "An error should have occurred"); + assertTrue(history.processRound(round1).isEmpty(), "No errors should have occurred"); + assertEquals(1, history.processRound(round2).size(), "An error should have occurred"); - assertEquals(2, getLogFileContents().lines().count(), "Log file should have 2 rounds recorded"); + assertEquals(2, getLogFileContents().lines().count(), "Log file should have 2 rounds recorded"); + } } @Test @DisplayName("History that has been parsed from file is equivalent to the normal history") void parsedHistory() { - final TransactionHandlingHistory history = new TransactionHandlingHistory(); - history.init(logFilePath); - - final ConsistencyTestingToolRound round1 = new ConsistencyTestingToolRound(1, 1, List.of(1L, 2L, 3L)); - final ConsistencyTestingToolRound round2 = new ConsistencyTestingToolRound(2, 22, List.of(6L, 5L, 4L)); - final ConsistencyTestingToolRound round3 = new ConsistencyTestingToolRound(3, 33, List.of()); - - assertTrue(history.processRound(round1).isEmpty(), "No errors should have occurred"); - assertTrue(history.processRound(round2).isEmpty(), "No errors should have occurred"); - assertTrue(history.processRound(round3).isEmpty(), "No errors should have occurred"); - - assertEquals(3, getLogFileContents().lines().count(), "Log file should have 3 rounds recorded"); - - final TransactionHandlingHistory parsedHistory = new TransactionHandlingHistory(); - parsedHistory.init(logFilePath); - - // rounds matching historical rounds are handled correctly - final ConsistencyTestingToolRound round2Duplicate = new ConsistencyTestingToolRound(2, 22, List.of(6L, 5L, 4L)); - assertTrue(parsedHistory.processRound(round2Duplicate).isEmpty(), "No errors should have occurred"); - assertEquals(3, getLogFileContents().lines().count(), "Log file should have 4 rounds recorded"); - - // rounds that don't match historical rounds are handled correctly - final ConsistencyTestingToolRound incorrectRound1Duplicate = - new ConsistencyTestingToolRound(1, 65, List.of(1L, 2L, 3L)); - assertEquals(1, parsedHistory.processRound(incorrectRound1Duplicate).size(), "An error should have occurred"); - assertEquals(3, getLogFileContents().lines().count(), "Log file should have 4 rounds recorded"); - - final ConsistencyTestingToolRound incorrectRound3Duplicate = - new ConsistencyTestingToolRound(3, 33, List.of(1L, 2L, 3L)); - assertEquals(1, parsedHistory.processRound(incorrectRound3Duplicate).size(), "An error should have occurred"); - assertEquals(3, getLogFileContents().lines().count(), "Log file should have 4 rounds recorded"); - - // new rounds can be recorded - final ConsistencyTestingToolRound round4 = new ConsistencyTestingToolRound(4, 44, List.of(7L, 8L, 9L)); - - assertTrue(parsedHistory.processRound(round4).isEmpty(), "No errors should have occurred"); - assertEquals(4, getLogFileContents().lines().count(), "Log file should have 4 rounds recorded"); + try (final TransactionHandlingHistory history = new TransactionHandlingHistory(); + final TransactionHandlingHistory parsedHistory = new TransactionHandlingHistory()) { + history.init(logFilePath); + + final ConsistencyTestingToolRound round1 = new ConsistencyTestingToolRound(1, 1, List.of(1L, 2L, 3L)); + final ConsistencyTestingToolRound round2 = new ConsistencyTestingToolRound(2, 22, List.of(6L, 5L, 4L)); + final ConsistencyTestingToolRound round3 = new ConsistencyTestingToolRound(3, 33, List.of()); + + assertTrue(history.processRound(round1).isEmpty(), "No errors should have occurred"); + assertTrue(history.processRound(round2).isEmpty(), "No errors should have occurred"); + assertTrue(history.processRound(round3).isEmpty(), "No errors should have occurred"); + + assertEquals(3, getLogFileContents().lines().count(), "Log file should have 3 rounds recorded"); + + parsedHistory.init(logFilePath); + + // rounds matching historical rounds are handled correctly + final ConsistencyTestingToolRound round2Duplicate = + new ConsistencyTestingToolRound(2, 22, List.of(6L, 5L, 4L)); + assertTrue(parsedHistory.processRound(round2Duplicate).isEmpty(), "No errors should have occurred"); + assertEquals(3, getLogFileContents().lines().count(), "Log file should have 4 rounds recorded"); + + // rounds that don't match historical rounds are handled correctly + final ConsistencyTestingToolRound incorrectRound1Duplicate = + new ConsistencyTestingToolRound(1, 65, List.of(1L, 2L, 3L)); + assertEquals( + 1, parsedHistory.processRound(incorrectRound1Duplicate).size(), "An error should have occurred"); + assertEquals(3, getLogFileContents().lines().count(), "Log file should have 4 rounds recorded"); + + final ConsistencyTestingToolRound incorrectRound3Duplicate = + new ConsistencyTestingToolRound(3, 33, List.of(1L, 2L, 3L)); + assertEquals( + 1, parsedHistory.processRound(incorrectRound3Duplicate).size(), "An error should have occurred"); + assertEquals(3, getLogFileContents().lines().count(), "Log file should have 4 rounds recorded"); + + // new rounds can be recorded + final ConsistencyTestingToolRound round4 = new ConsistencyTestingToolRound(4, 44, List.of(7L, 8L, 9L)); + + assertTrue(parsedHistory.processRound(round4).isEmpty(), "No errors should have occurred"); + assertEquals(4, getLogFileContents().lines().count(), "Log file should have 4 rounds recorded"); + } } } diff --git a/platform-sdk/platform-apps/tests/ISSTestingTool/src/main/java/com/swirlds/demo/iss/ISSTestingToolMain.java b/platform-sdk/platform-apps/tests/ISSTestingTool/src/main/java/com/swirlds/demo/iss/ISSTestingToolMain.java index 8caa91b9e9d4..ca511f9eb093 100644 --- a/platform-sdk/platform-apps/tests/ISSTestingTool/src/main/java/com/swirlds/demo/iss/ISSTestingToolMain.java +++ b/platform-sdk/platform-apps/tests/ISSTestingTool/src/main/java/com/swirlds/demo/iss/ISSTestingToolMain.java @@ -26,7 +26,7 @@ import com.swirlds.common.constructable.ConstructableRegistry; import com.swirlds.common.constructable.ConstructableRegistryException; import com.swirlds.common.platform.NodeId; -import com.swirlds.platform.state.PlatformMerkleStateRoot; +import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.system.BasicSoftwareVersion; import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.SwirldMain; @@ -47,7 +47,7 @@ * peers stop gossiping. Therefore, we can validate that a scheduled log error doesn't occur, due to consensus coming to * a halt, even if an ISS isn't detected. */ -public class ISSTestingToolMain implements SwirldMain { +public class ISSTestingToolMain implements SwirldMain { private static final Logger logger = LogManager.getLogger(ISSTestingToolMain.class); @@ -58,8 +58,8 @@ public class ISSTestingToolMain implements SwirldMain { logger.info(STARTUP.getMarker(), "Registering ISSTestingToolState with ConstructableRegistry"); ConstructableRegistry constructableRegistry = ConstructableRegistry.getInstance(); constructableRegistry.registerConstructable(new ClassConstructorPair(ISSTestingToolState.class, () -> { - ISSTestingToolState issTestingToolState = new ISSTestingToolState( - FAKE_MERKLE_STATE_LIFECYCLES, version -> new BasicSoftwareVersion(version.major())); + ISSTestingToolState issTestingToolState = + new ISSTestingToolState(version -> new BasicSoftwareVersion(version.major())); return issTestingToolState; })); registerMerkleStateRootClassIds(); @@ -112,14 +112,18 @@ public void run() { */ @Override @NonNull - public PlatformMerkleStateRoot newMerkleStateRoot() { - final PlatformMerkleStateRoot state = new ISSTestingToolState( - FAKE_MERKLE_STATE_LIFECYCLES, - version -> new BasicSoftwareVersion(softwareVersion.getSoftwareVersion())); + public ISSTestingToolState newMerkleStateRoot() { + final ISSTestingToolState state = + new ISSTestingToolState(version -> new BasicSoftwareVersion(softwareVersion.getSoftwareVersion())); FAKE_MERKLE_STATE_LIFECYCLES.initStates(state); return state; } + @Override + public StateLifecycles newStateLifecycles() { + return new ISSTestingToolStateLifecycles(); + } + /** * {@inheritDoc} */ diff --git a/platform-sdk/platform-apps/tests/ISSTestingTool/src/main/java/com/swirlds/demo/iss/ISSTestingToolState.java b/platform-sdk/platform-apps/tests/ISSTestingTool/src/main/java/com/swirlds/demo/iss/ISSTestingToolState.java index 9ecda4703be1..5257a5c0d807 100644 --- a/platform-sdk/platform-apps/tests/ISSTestingTool/src/main/java/com/swirlds/demo/iss/ISSTestingToolState.java +++ b/platform-sdk/platform-apps/tests/ISSTestingTool/src/main/java/com/swirlds/demo/iss/ISSTestingToolState.java @@ -26,59 +26,28 @@ * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. */ -import static com.swirlds.common.utility.CompareTo.isGreaterThan; -import static com.swirlds.common.utility.CompareTo.isLessThan; -import static com.swirlds.common.utility.NonCryptographicHashing.hash64; -import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; -import static com.swirlds.logging.legacy.LogMarker.STARTUP; - import com.hedera.hapi.node.base.SemanticVersion; -import com.hedera.hapi.node.state.roster.Roster; -import com.hedera.hapi.node.state.roster.RosterEntry; -import com.hedera.hapi.platform.event.StateSignatureTransaction; -import com.hedera.pbj.runtime.ParseException; import com.swirlds.common.constructable.ConstructableIgnored; import com.swirlds.common.io.SelfSerializable; import com.swirlds.common.io.streams.SerializableDataInputStream; import com.swirlds.common.io.streams.SerializableDataOutputStream; -import com.swirlds.common.merkle.utility.SerializableLong; -import com.swirlds.common.platform.NodeId; -import com.swirlds.common.utility.ByteUtils; -import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; -import com.swirlds.platform.scratchpad.Scratchpad; import com.swirlds.platform.state.PlatformMerkleStateRoot; -import com.swirlds.platform.state.PlatformStateModifier; -import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.system.InitTrigger; import com.swirlds.platform.system.Platform; -import com.swirlds.platform.system.Round; import com.swirlds.platform.system.SoftwareVersion; import com.swirlds.platform.system.events.ConsensusEvent; -import com.swirlds.platform.system.events.Event; -import com.swirlds.platform.system.transaction.ConsensusTransaction; -import com.swirlds.platform.system.transaction.Transaction; import com.swirlds.platform.test.fixtures.state.FakeStateLifecycles; import com.swirlds.state.merkle.singleton.StringLeaf; import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.Nullable; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.time.Duration; import java.time.Instant; -import java.util.HashMap; -import java.util.Iterator; import java.util.LinkedList; import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Random; -import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; /** * State for the ISSTestingTool. @@ -86,8 +55,6 @@ @ConstructableIgnored public class ISSTestingToolState extends PlatformMerkleStateRoot { - private static final Logger logger = LogManager.getLogger(ISSTestingToolState.class); - private static class ClassVersion { public static final int ORIGINAL = 1; } @@ -98,20 +65,12 @@ private static class ClassVersion { private static final long CLASS_ID = 0xf059378c7764ef47L; - /** - * Only trigger an incident if consensus time is within this time window of the scheduled time. If consensus time - * "skips" forward longer than this window then the scheduled incident will be ignored. - */ - private static final Duration INCIDENT_WINDOW = Duration.ofSeconds(10); - // 0 is PLATFORM_STATE, 1 is ROSTERS, 2 is ROSTER_STATE private static final int RUNNING_SUM_INDEX = 3; private static final int GENESIS_TIMESTAMP_INDEX = 4; private static final int PLANNED_ISS_LIST_INDEX = 5; private static final int PLANNED_LOG_ERROR_LIST_INDEX = 6; - private NodeId selfId; - /** * The true "state" of this app. Each transaction is just an integer that gets added to this value. */ @@ -132,60 +91,11 @@ private static class ClassVersion { */ private List plannedLogErrorList = new LinkedList<>(); - private boolean immutable; - - private Scratchpad scratchPad; - - public ISSTestingToolState( - @NonNull final StateLifecycles lifecycles, - @NonNull final Function versionFactory) { - super(lifecycles, versionFactory); - } - - /** - * Copy constructor. - */ - private ISSTestingToolState(final ISSTestingToolState that) { - super(that); - this.runningSum = that.runningSum; - this.plannedIssList = new LinkedList<>(that.plannedIssList); - this.plannedLogErrorList = new LinkedList<>(that.plannedLogErrorList); - this.genesisTimestamp = that.genesisTimestamp; - this.selfId = that.selfId; - this.scratchPad = that.scratchPad; - that.immutable = true; - } - - /** - * {@inheritDoc} - */ - @Override - public boolean isImmutable() { - return immutable; + public ISSTestingToolState(@NonNull final Function versionFactory) { + super(versionFactory); } - /** - * {@inheritDoc} - */ - @Override - public synchronized ISSTestingToolState copy() { - throwIfImmutable(); - setImmutable(true); - return new ISSTestingToolState(this); - } - - /** - * {@inheritDoc} - */ - @Override - public void init( - @NonNull final Platform platform, - @NonNull final InitTrigger trigger, - @Nullable final SoftwareVersion previousSoftwareVersion) { - super.init(platform, trigger, previousSoftwareVersion); - - throwIfImmutable(); - + public void initState(InitTrigger trigger, Platform platform) { // since the test occurrences are relative to the genesis timestamp, the data only needs to be parsed at genesis if (trigger == InitTrigger.GENESIS) { final ISSTestingToolConfig testingToolConfig = @@ -207,10 +117,6 @@ public void init( plannedIssList = readObjectByChildIndex(PLANNED_ISS_LIST_INDEX, PlannedIss::new); plannedLogErrorList = readObjectByChildIndex(PLANNED_LOG_ERROR_LIST_INDEX, PlannedLogError::new); } - - this.selfId = platform.getSelfId(); - this.scratchPad = - Scratchpad.create(platform.getContext(), selfId, IssTestingToolScratchpad.class, "ISSTestingTool"); } List readObjectByChildIndex(final int index, final Supplier factory) { @@ -239,300 +145,48 @@ void writeObjectByChildIndex(final int index, final } } - @Override - public void preHandle( - @NonNull final Event event, - @NonNull - final Consumer> - stateSignatureTransactionCallback) { - event.forEachTransaction(transaction -> { - // We are not interested in pre-handling any system transactions, as they are - // specific for the platform only.We also don't want to consume deprecated - // EventTransaction.STATE_SIGNATURE_TRANSACTION system transactions in the - // callback,since it's intended to be used only for the new form of encoded system - // transactions in Bytes.Thus, we can directly skip the current - // iteration, if it processes a deprecated system transaction with the - // EventTransaction.STATE_SIGNATURE_TRANSACTION type. - if (transaction.isSystem()) { - return; - } - - // We should consume in the callback the new form of system transactions in Bytes - if (areTransactionBytesSystemOnes(transaction)) { - consumeSystemTransaction(transaction, event, stateSignatureTransactionCallback); - } - }); - } - - /** - * {@inheritDoc} - */ - @Override - public void handleConsensusRound( - @NonNull final Round round, - @NonNull final PlatformStateModifier platformState, - @NonNull - final Consumer> - stateSignatureTransactionCallback) { - throwIfImmutable(); - final Iterator eventIterator = round.iterator(); - - while (eventIterator.hasNext()) { - final var event = eventIterator.next(); - captureTimestamp(event); - event.consensusTransactionIterator().forEachRemaining(transaction -> { - // We are not interested in handling any system transactions, as they are specific - // for the platform only.We also don't want to consume deprecated - // EventTransaction.STATE_SIGNATURE_TRANSACTION system transactions in the - // callback,since it's intended to be used only for the new form of encoded system - // transactions in Bytes.Thus, we can directly skip the current - // iteration, if it processes a deprecated system transaction with the - // EventTransaction.STATE_SIGNATURE_TRANSACTION type. - if (transaction.isSystem()) { - return; - } - - // We should consume in the callback the new form of system transactions in Bytes - if (areTransactionBytesSystemOnes(transaction)) { - consumeSystemTransaction(transaction, event, stateSignatureTransactionCallback); - } else { - handleTransaction(transaction); - } - }); - if (!eventIterator.hasNext()) { - final Instant currentTimestamp = event.getConsensusTimestamp(); - final Duration elapsedSinceGenesis = Duration.between(genesisTimestamp, currentTimestamp); - - final PlannedIss plannedIss = - shouldTriggerIncident(elapsedSinceGenesis, currentTimestamp, plannedIssList); - - if (plannedIss != null) { - triggerISS(round, plannedIss, elapsedSinceGenesis, currentTimestamp); - // Record the consensus time at which this ISS was provoked - scratchPad.set( - IssTestingToolScratchpad.PROVOKED_ISS, - new SerializableLong(currentTimestamp.toEpochMilli())); - } - - final PlannedLogError plannedLogError = - shouldTriggerIncident(elapsedSinceGenesis, currentTimestamp, plannedLogErrorList); - if (plannedLogError != null) { - triggerLogError(plannedLogError, elapsedSinceGenesis); - } - } - } - } - /** * Save the event's timestamp, if needed. */ - private void captureTimestamp(final ConsensusEvent event) { + void captureTimestamp(@NonNull final ConsensusEvent event) { if (genesisTimestamp == null) { genesisTimestamp = event.getConsensusTimestamp(); setChild(GENESIS_TIMESTAMP_INDEX, new StringLeaf(genesisTimestamp.toString())); } } - /** - * Apply a transaction to the state. - * - * @param transaction the transaction to apply - */ - private void handleTransaction(final ConsensusTransaction transaction) { - final int delta = - ByteUtils.byteArrayToInt(transaction.getApplicationTransaction().toByteArray(), 0); + void incrementRunningSum(long delta) { runningSum += delta; setChild(RUNNING_SUM_INDEX, new StringLeaf(Long.toString(runningSum))); } - /** - * Checks if the transaction bytes are system ones. The test creates application transactions - * with max length of 4. System transactions will be always bigger than that. - * - * @param transaction the consensus transaction to check - * @return true if the transaction bytes are system ones, false otherwise - */ - private boolean areTransactionBytesSystemOnes(final Transaction transaction) { - return transaction.getApplicationTransaction().length() > 4; - } - - private void consumeSystemTransaction( - final Transaction transaction, - final Event event, - final Consumer> stateSignatureTransactionCallback) { - try { - final var stateSignatureTransaction = - StateSignatureTransaction.PROTOBUF.parse(transaction.getApplicationTransaction()); - stateSignatureTransactionCallback.accept(new ScopedSystemTransaction<>( - event.getCreatorId(), event.getSoftwareVersion(), stateSignatureTransaction)); - } catch (final ParseException e) { - logger.error("Failed to parse StateSignatureTransaction", e); - } + Instant getGenesisTimestamp() { + return genesisTimestamp; } - /** - * Iterate over a list of planned incidents, and return the first one that should be triggered. If no incident from - * the list should be triggered, return null - * - * @param elapsedSinceGenesis the amount of time that has elapsed since genesis - * @param currentTimestamp the current consensus timestamp - * @param plannedIncidentList the list of planned incidents to iterate over - * @param the type of incident in the list - * @return the first incident that should be triggered, or null if no incident should be triggered - */ - @Nullable - private T shouldTriggerIncident( - @NonNull final Duration elapsedSinceGenesis, - @NonNull final Instant currentTimestamp, - @NonNull final List plannedIncidentList) { - - Objects.requireNonNull(elapsedSinceGenesis); - Objects.requireNonNull(currentTimestamp); - Objects.requireNonNull(plannedIncidentList); - - final Iterator plannedIncidentIterator = plannedIncidentList.listIterator(); - while (plannedIncidentIterator.hasNext()) { - final T plannedIncident = plannedIncidentIterator.next(); - - if (isLessThan(elapsedSinceGenesis, plannedIncident.getTimeAfterGenesis())) { - // The next planned incident is for some time in the future, so return null - return null; - } - - // If we reach this point then we are ready to trigger the incident. - // Once triggered, the same incident is not triggered again. - plannedIncidentIterator.remove(); - - if (isGreaterThan( - elapsedSinceGenesis, plannedIncident.getTimeAfterGenesis().plus(INCIDENT_WINDOW))) { - - // Consensus time has skipped forward, possibly because this node was restarted. - // We are outside the allowable window for the scheduled incident, so do not trigger this one. - logger.info( - STARTUP.getMarker(), - "Planned {} skipped at {}. Planned time after genesis: {}. " - + "Elapsed time since genesis at skip: {}", - plannedIncident.getDescriptor(), - currentTimestamp, - plannedIncident.getTimeAfterGenesis(), - elapsedSinceGenesis); - - continue; - } - - final SerializableLong issLong = scratchPad.get(IssTestingToolScratchpad.PROVOKED_ISS); - if (issLong != null) { - final Instant lastProvokedIssTime = Instant.ofEpochMilli(issLong.getValue()); - if (lastProvokedIssTime.equals(currentTimestamp)) { - logger.info( - STARTUP.getMarker(), - "Planned {} skipped at {} because this ISS was already invoked (likely before a restart).", - plannedIncident.getDescriptor(), - currentTimestamp); - } - continue; - } - - return plannedIncident; - } - - return null; + List getPlannedIssList() { + return plannedIssList; } - /** - * Determine which hash partition in a planned ISS is the largest (by consensus weight). If there is a tie, returns - * the smaller partition index. - * - * @return the index of the largest hash partition - */ - private int findLargestPartition(@NonNull final Roster roster, @NonNull final PlannedIss plannedIss) { - - final Map partitionWeights = new HashMap<>(); - for (final RosterEntry entry : roster.rosterEntries()) { - final int partition = plannedIss.getPartitionOfNode(NodeId.of(entry.nodeId())); - final long newWeight = partitionWeights.getOrDefault(partition, 0L) + entry.weight(); - partitionWeights.put(partition, newWeight); - } - - int largestPartition = 0; - long largestPartitionWeight = 0; - for (int partition = 0; partition < plannedIss.getPartitionCount(); partition++) { - if (partitionWeights.get(partition) != null && partitionWeights.get(partition) > largestPartitionWeight) { - largestPartition = partition; - largestPartitionWeight = partitionWeights.getOrDefault(partition, 0L); - } - } - - return largestPartition; + List getPlannedLogErrorList() { + return plannedLogErrorList; } /** - * Trigger an ISS - * - * @param round the current round - * @param plannedIss the planned ISS to trigger - * @param elapsedSinceGenesis the amount of time that has elapsed since genesis - * @param currentTimestamp the current consensus timestamp + * Copy constructor. */ - private void triggerISS( - @NonNull final Round round, - @NonNull final PlannedIss plannedIss, - @NonNull final Duration elapsedSinceGenesis, - @NonNull final Instant currentTimestamp) { - - Objects.requireNonNull(plannedIss); - Objects.requireNonNull(elapsedSinceGenesis); - Objects.requireNonNull(currentTimestamp); - - final int hashPartitionIndex = plannedIss.getPartitionOfNode(selfId); - if (hashPartitionIndex == findLargestPartition(round.getConsensusRoster(), plannedIss)) { - // If we are in the largest partition then don't bother modifying the state. - return; - } - - // Randomly mutate the state. Each node in the same partition will get the same random mutation. - // Nodes in different partitions will get a different random mutation with high probability. - final long hashPartitionSeed = hash64(currentTimestamp.toEpochMilli(), hashPartitionIndex); - final Random random = new Random(hashPartitionSeed); - runningSum += random.nextInt(); - - logger.info( - STARTUP.getMarker(), - "ISS intentionally provoked. This ISS was planned to occur at time after genesis {}, " - + "and actually occurred at time after genesis {} in round {}. This node ({}) is in partition {} and will " - + "agree with the hashes of all other nodes in partition {}. Nodes in other partitions " - + "are expected to have divergent hashes.", - plannedIss.getTimeAfterGenesis(), - elapsedSinceGenesis, - round.getRoundNum(), - selfId, - hashPartitionIndex, - hashPartitionIndex); + private ISSTestingToolState(final ISSTestingToolState that) { + super(that); } /** - * Trigger a log error - * - * @param plannedLogError the planned log error to trigger - * @param elapsedSinceGenesis the amount of time that has elapsed since genesis + * {@inheritDoc} */ - private void triggerLogError( - @NonNull final PlannedLogError plannedLogError, @NonNull final Duration elapsedSinceGenesis) { - - Objects.requireNonNull(plannedLogError); - Objects.requireNonNull(elapsedSinceGenesis); - - if (!plannedLogError.getNodeIds().contains(selfId)) { - // don't log if this node isn't in the list of nodes that should log - return; - } - - logger.error( - EXCEPTION.getMarker(), - "This error was scheduled to be logged at time after genesis {}, and actually was logged " - + "at time after genesis {}.", - plannedLogError.getTimeAfterGenesis(), - elapsedSinceGenesis); + @Override + public synchronized ISSTestingToolState copy() { + throwIfImmutable(); + setImmutable(true); + return new ISSTestingToolState(this); } /** diff --git a/platform-sdk/platform-apps/tests/ISSTestingTool/src/main/java/com/swirlds/demo/iss/ISSTestingToolStateLifecycles.java b/platform-sdk/platform-apps/tests/ISSTestingTool/src/main/java/com/swirlds/demo/iss/ISSTestingToolStateLifecycles.java new file mode 100644 index 000000000000..c6fd14814255 --- /dev/null +++ b/platform-sdk/platform-apps/tests/ISSTestingTool/src/main/java/com/swirlds/demo/iss/ISSTestingToolStateLifecycles.java @@ -0,0 +1,391 @@ +/* + * 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.swirlds.demo.iss; + +import static com.swirlds.common.utility.CompareTo.isGreaterThan; +import static com.swirlds.common.utility.CompareTo.isLessThan; +import static com.swirlds.common.utility.NonCryptographicHashing.hash64; +import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; +import static com.swirlds.logging.legacy.LogMarker.STARTUP; + +import com.hedera.hapi.node.state.roster.Roster; +import com.hedera.hapi.node.state.roster.RosterEntry; +import com.hedera.hapi.platform.event.StateSignatureTransaction; +import com.hedera.pbj.runtime.ParseException; +import com.swirlds.common.context.PlatformContext; +import com.swirlds.common.merkle.utility.SerializableLong; +import com.swirlds.common.platform.NodeId; +import com.swirlds.common.utility.ByteUtils; +import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; +import com.swirlds.platform.scratchpad.Scratchpad; +import com.swirlds.platform.state.StateLifecycles; +import com.swirlds.platform.system.InitTrigger; +import com.swirlds.platform.system.Platform; +import com.swirlds.platform.system.Round; +import com.swirlds.platform.system.SoftwareVersion; +import com.swirlds.platform.system.address.AddressBook; +import com.swirlds.platform.system.events.ConsensusEvent; +import com.swirlds.platform.system.events.Event; +import com.swirlds.platform.system.transaction.ConsensusTransaction; +import com.swirlds.platform.system.transaction.Transaction; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.time.Duration; +import java.time.Instant; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Random; +import java.util.function.Consumer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * This class handles the lifecycle events for the {@link ISSTestingToolState}. + */ +public class ISSTestingToolStateLifecycles implements StateLifecycles { + + private static final Logger logger = LogManager.getLogger(ISSTestingToolStateLifecycles.class); + + /** + * Only trigger an incident if consensus time is within this time window of the scheduled time. If consensus time + * "skips" forward longer than this window then the scheduled incident will be ignored. + */ + private static final Duration INCIDENT_WINDOW = Duration.ofSeconds(10); + + private NodeId selfId; + private Scratchpad scratchPad; + + @Override + public void onStateInitialized( + @NonNull ISSTestingToolState state, + @NonNull Platform platform, + @NonNull InitTrigger trigger, + @Nullable SoftwareVersion previousVersion) { + state.throwIfImmutable(); + + state.initState(trigger, platform); + + this.selfId = platform.getSelfId(); + this.scratchPad = + Scratchpad.create(platform.getContext(), selfId, IssTestingToolScratchpad.class, "ISSTestingTool"); + } + + /** + * Apply a transaction to the state. + * + * @param transaction the transaction to apply + */ + private void handleTransaction( + @NonNull final ISSTestingToolState state, @NonNull final ConsensusTransaction transaction) { + if (transaction.isSystem()) { + return; + } + final int delta = + ByteUtils.byteArrayToInt(transaction.getApplicationTransaction().toByteArray(), 0); + state.incrementRunningSum(delta); + } + + @Override + public void onHandleConsensusRound( + @NonNull Round round, + @NonNull ISSTestingToolState state, + @NonNull Consumer> stateSignatureTransactionCallback) { + state.throwIfImmutable(); + final Iterator eventIterator = round.iterator(); + + while (eventIterator.hasNext()) { + final var event = eventIterator.next(); + state.captureTimestamp(event); + event.consensusTransactionIterator().forEachRemaining(transaction -> { + // We are not interested in handling any system transactions, as they are specific + // for the platform only.We also don't want to consume deprecated + // EventTransaction.STATE_SIGNATURE_TRANSACTION system transactions in the + // callback,since it's intended to be used only for the new form of encoded system + // transactions in Bytes.Thus, we can directly skip the current + // iteration, if it processes a deprecated system transaction with the + // EventTransaction.STATE_SIGNATURE_TRANSACTION type. + if (transaction.isSystem()) { + return; + } + + // We should consume in the callback the new form of system transactions in Bytes + if (areTransactionBytesSystemOnes(transaction)) { + consumeSystemTransaction(transaction, event, stateSignatureTransactionCallback); + } else { + handleTransaction(state, transaction); + } + }); + if (!eventIterator.hasNext()) { + final Instant currentTimestamp = event.getConsensusTimestamp(); + final Duration elapsedSinceGenesis = Duration.between(state.getGenesisTimestamp(), currentTimestamp); + + final PlannedIss plannedIss = + shouldTriggerIncident(elapsedSinceGenesis, currentTimestamp, state.getPlannedIssList()); + + if (plannedIss != null) { + triggerISS(round, plannedIss, elapsedSinceGenesis, currentTimestamp, state); + // Record the consensus time at which this ISS was provoked + scratchPad.set( + IssTestingToolScratchpad.PROVOKED_ISS, + new SerializableLong(currentTimestamp.toEpochMilli())); + } + + final PlannedLogError plannedLogError = + shouldTriggerIncident(elapsedSinceGenesis, currentTimestamp, state.getPlannedLogErrorList()); + if (plannedLogError != null) { + triggerLogError(plannedLogError, elapsedSinceGenesis); + } + } + } + } + + /** + * Iterate over a list of planned incidents, and return the first one that should be triggered. If no incident from + * the list should be triggered, return null + * + * @param elapsedSinceGenesis the amount of time that has elapsed since genesis + * @param currentTimestamp the current consensus timestamp + * @param plannedIncidentList the list of planned incidents to iterate over + * @param the type of incident in the list + * @return the first incident that should be triggered, or null if no incident should be triggered + */ + @Nullable + private T shouldTriggerIncident( + @NonNull final Duration elapsedSinceGenesis, + @NonNull final Instant currentTimestamp, + @NonNull final List plannedIncidentList) { + + Objects.requireNonNull(elapsedSinceGenesis); + Objects.requireNonNull(currentTimestamp); + Objects.requireNonNull(plannedIncidentList); + + final Iterator plannedIncidentIterator = plannedIncidentList.listIterator(); + while (plannedIncidentIterator.hasNext()) { + final T plannedIncident = plannedIncidentIterator.next(); + + if (isLessThan(elapsedSinceGenesis, plannedIncident.getTimeAfterGenesis())) { + // The next planned incident is for some time in the future, so return null + return null; + } + + // If we reach this point then we are ready to trigger the incident. + // Once triggered, the same incident is not triggered again. + plannedIncidentIterator.remove(); + + if (isGreaterThan( + elapsedSinceGenesis, plannedIncident.getTimeAfterGenesis().plus(INCIDENT_WINDOW))) { + + // Consensus time has skipped forward, possibly because this node was restarted. + // We are outside the allowable window for the scheduled incident, so do not trigger this one. + logger.info( + STARTUP.getMarker(), + "Planned {} skipped at {}. Planned time after genesis: {}. " + + "Elapsed time since genesis at skip: {}", + plannedIncident.getDescriptor(), + currentTimestamp, + plannedIncident.getTimeAfterGenesis(), + elapsedSinceGenesis); + + continue; + } + + final SerializableLong issLong = scratchPad.get(IssTestingToolScratchpad.PROVOKED_ISS); + if (issLong != null) { + final Instant lastProvokedIssTime = Instant.ofEpochMilli(issLong.getValue()); + if (lastProvokedIssTime.equals(currentTimestamp)) { + logger.info( + STARTUP.getMarker(), + "Planned {} skipped at {} because this ISS was already invoked (likely before a restart).", + plannedIncident.getDescriptor(), + currentTimestamp); + } + continue; + } + + return plannedIncident; + } + + return null; + } + + /** + * Trigger an ISS + * + * @param round the current round + * @param plannedIss the planned ISS to trigger + * @param elapsedSinceGenesis the amount of time that has elapsed since genesis + * @param currentTimestamp the current consensus timestamp + */ + private void triggerISS( + @NonNull final Round round, + @NonNull final PlannedIss plannedIss, + @NonNull final Duration elapsedSinceGenesis, + @NonNull final Instant currentTimestamp, + @NonNull final ISSTestingToolState state) { + + Objects.requireNonNull(plannedIss); + Objects.requireNonNull(elapsedSinceGenesis); + Objects.requireNonNull(currentTimestamp); + + final int hashPartitionIndex = plannedIss.getPartitionOfNode(selfId); + if (hashPartitionIndex == findLargestPartition(round.getConsensusRoster(), plannedIss)) { + // If we are in the largest partition then don't bother modifying the state. + return; + } + + // Randomly mutate the state. Each node in the same partition will get the same random mutation. + // Nodes in different partitions will get a different random mutation with high probability. + final long hashPartitionSeed = hash64(currentTimestamp.toEpochMilli(), hashPartitionIndex); + final Random random = new Random(hashPartitionSeed); + state.incrementRunningSum(random.nextInt()); + + logger.info( + STARTUP.getMarker(), + "ISS intentionally provoked. This ISS was planned to occur at time after genesis {}, " + + "and actually occurred at time after genesis {} in round {}. This node ({}) is in partition {} and will " + + "agree with the hashes of all other nodes in partition {}. Nodes in other partitions " + + "are expected to have divergent hashes.", + plannedIss.getTimeAfterGenesis(), + elapsedSinceGenesis, + round.getRoundNum(), + selfId, + hashPartitionIndex, + hashPartitionIndex); + } + + /** + * Determine which hash partition in a planned ISS is the largest (by consensus weight). If there is a tie, returns + * the smaller partition index. + * + * @return the index of the largest hash partition + */ + private int findLargestPartition(@NonNull final Roster roster, @NonNull final PlannedIss plannedIss) { + + final Map partitionWeights = new HashMap<>(); + for (final RosterEntry entry : roster.rosterEntries()) { + final int partition = plannedIss.getPartitionOfNode(NodeId.of(entry.nodeId())); + final long newWeight = partitionWeights.getOrDefault(partition, 0L) + entry.weight(); + partitionWeights.put(partition, newWeight); + } + + int largestPartition = 0; + long largestPartitionWeight = 0; + for (int partition = 0; partition < plannedIss.getPartitionCount(); partition++) { + if (partitionWeights.get(partition) != null && partitionWeights.get(partition) > largestPartitionWeight) { + largestPartition = partition; + largestPartitionWeight = partitionWeights.getOrDefault(partition, 0L); + } + } + + return largestPartition; + } + + /** + * Trigger a log error + * + * @param plannedLogError the planned log error to trigger + * @param elapsedSinceGenesis the amount of time that has elapsed since genesis + */ + private void triggerLogError( + @NonNull final PlannedLogError plannedLogError, @NonNull final Duration elapsedSinceGenesis) { + + Objects.requireNonNull(plannedLogError); + Objects.requireNonNull(elapsedSinceGenesis); + + if (!plannedLogError.getNodeIds().contains(selfId)) { + // don't log if this node isn't in the list of nodes that should log + return; + } + + logger.error( + EXCEPTION.getMarker(), + "This error was scheduled to be logged at time after genesis {}, and actually was logged " + + "at time after genesis {}.", + plannedLogError.getTimeAfterGenesis(), + elapsedSinceGenesis); + } + + @Override + public void onPreHandle( + @NonNull Event event, + @NonNull ISSTestingToolState state, + @NonNull Consumer> stateSignatureTransactionCallback) { + event.forEachTransaction(transaction -> { + // We are not interested in pre-handling any system transactions, as they are + // specific for the platform only.We also don't want to consume deprecated + // EventTransaction.STATE_SIGNATURE_TRANSACTION system transactions in the + // callback,since it's intended to be used only for the new form of encoded system + // transactions in Bytes.Thus, we can directly skip the current + // iteration, if it processes a deprecated system transaction with the + // EventTransaction.STATE_SIGNATURE_TRANSACTION type. + if (transaction.isSystem()) { + return; + } + + // We should consume in the callback the new form of system transactions in Bytes + if (areTransactionBytesSystemOnes(transaction)) { + consumeSystemTransaction(transaction, event, stateSignatureTransactionCallback); + } + }); + } + + /** + * Checks if the transaction bytes are system ones. The test creates application transactions + * with max length of 4. System transactions will be always bigger than that. + * + * @param transaction the consensus transaction to check + * @return true if the transaction bytes are system ones, false otherwise + */ + private boolean areTransactionBytesSystemOnes(final Transaction transaction) { + return transaction.getApplicationTransaction().length() > 4; + } + + private void consumeSystemTransaction( + final Transaction transaction, + final Event event, + final Consumer> stateSignatureTransactionCallback) { + try { + final var stateSignatureTransaction = + StateSignatureTransaction.PROTOBUF.parse(transaction.getApplicationTransaction()); + stateSignatureTransactionCallback.accept(new ScopedSystemTransaction<>( + event.getCreatorId(), event.getSoftwareVersion(), stateSignatureTransaction)); + } catch (final ParseException e) { + logger.error("Failed to parse StateSignatureTransaction", e); + } + } + + @Override + public void onSealConsensusRound(@NonNull Round round, @NonNull ISSTestingToolState state) { + // no-op + } + + @Override + public void onUpdateWeight( + @NonNull ISSTestingToolState state, + @NonNull AddressBook configAddressBook, + @NonNull PlatformContext context) { + // no-op + } + + @Override + public void onNewRecoveredState(@NonNull ISSTestingToolState recoveredState) { + // no-op + } +} diff --git a/platform-sdk/platform-apps/tests/ISSTestingTool/src/test/java/com/swirlds/demo/iss/ISSTestingToolStateTest.java b/platform-sdk/platform-apps/tests/ISSTestingTool/src/test/java/com/swirlds/demo/iss/ISSTestingToolStateTest.java index 0bdecce5accd..45ae16b689b1 100644 --- a/platform-sdk/platform-apps/tests/ISSTestingTool/src/test/java/com/swirlds/demo/iss/ISSTestingToolStateTest.java +++ b/platform-sdk/platform-apps/tests/ISSTestingTool/src/test/java/com/swirlds/demo/iss/ISSTestingToolStateTest.java @@ -32,8 +32,6 @@ import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.event.PlatformEvent; -import com.swirlds.platform.state.PlatformStateModifier; -import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.system.Round; import com.swirlds.platform.system.events.ConsensusEvent; import com.swirlds.platform.system.transaction.ConsensusTransaction; @@ -55,7 +53,7 @@ class ISSTestingToolStateTest { private static final int RUNNING_SUM_INDEX = 3; private ISSTestingToolMain main; private ISSTestingToolState state; - private PlatformStateModifier platformStateModifier; + private ISSTestingToolStateLifecycles stateLifecycles; private Round round; private ConsensusEvent event; private List> consumedTransactions; @@ -65,10 +63,10 @@ class ISSTestingToolStateTest { @BeforeEach void setUp() { - state = new ISSTestingToolState(mock(StateLifecycles.class), mock(Function.class)); + state = new ISSTestingToolState(mock(Function.class)); + stateLifecycles = new ISSTestingToolStateLifecycles(); main = mock(ISSTestingToolMain.class); final var random = new Random(); - platformStateModifier = mock(PlatformStateModifier.class); round = mock(Round.class); event = mock(ConsensusEvent.class); @@ -96,7 +94,7 @@ void handleConsensusRoundWithApplicationTransaction() { when(transaction.getApplicationTransaction()).thenReturn(bytes); // When - state.handleConsensusRound(round, platformStateModifier, consumer); + stateLifecycles.onHandleConsensusRound(round, state, consumer); // Then verify(round, times(1)).iterator(); @@ -119,7 +117,7 @@ void handleConsensusRoundWithSystemTransaction() { when(transaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); // When - state.handleConsensusRound(round, platformStateModifier, consumer); + stateLifecycles.onHandleConsensusRound(round, state, consumer); // Then verify(round, times(1)).iterator(); @@ -152,7 +150,7 @@ void handleConsensusRoundWithMultipleSystemTransaction() { when(thirdConsensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); // When - state.handleConsensusRound(round, platformStateModifier, consumer); + stateLifecycles.onHandleConsensusRound(round, state, consumer); // Then verify(round, times(1)).iterator(); @@ -172,7 +170,7 @@ void handleConsensusRoundWithDeprecatedSystemTransaction() { when(transaction.isSystem()).thenReturn(true); // When - state.handleConsensusRound(round, platformStateModifier, consumer); + stateLifecycles.onHandleConsensusRound(round, state, consumer); // Then verify(round, times(1)).iterator(); @@ -196,7 +194,7 @@ void handleConsensusRoundWithEmptyTransaction() { when(transaction.getApplicationTransaction()).thenReturn(emptyStateSignatureTransactionBytes); // When - state.handleConsensusRound(round, platformStateModifier, consumer); + stateLifecycles.onHandleConsensusRound(round, state, consumer); // Then verify(round, times(1)).iterator(); @@ -221,7 +219,7 @@ void handleConsensusRoundWithNullTransaction() { when(transaction.getApplicationTransaction()).thenReturn(emptyStateSignatureTransactionBytes); // When - state.handleConsensusRound(round, platformStateModifier, consumer); + stateLifecycles.onHandleConsensusRound(round, state, consumer); // Then verify(round, times(1)).iterator(); @@ -268,7 +266,7 @@ void preHandleEventWithMultipleSystemTransaction() { when(thirdEventTransaction.applicationTransaction()).thenReturn(transactionBytes); // When - state.preHandle(event, consumer); + stateLifecycles.onPreHandle(event, state, consumer); // Then assertThat(consumedTransactions).hasSize(3); @@ -302,7 +300,7 @@ void preHandleEventWithSystemTransaction() { when(transaction.getApplicationTransaction()).thenReturn(transactionBytes); // When - state.preHandle(event, consumer); + stateLifecycles.onPreHandle(event, state, consumer); // Then assertThat(consumedTransactions).hasSize(1); @@ -317,7 +315,7 @@ void preHandleEventWithDeprecatedSystemTransaction() { when(transaction.isSystem()).thenReturn(true); // When - state.preHandle(event, consumer); + stateLifecycles.onPreHandle(event, state, consumer); // Then assertThat(consumedTransactions).isEmpty(); @@ -350,7 +348,7 @@ void preHandleEventWithEmptyTransaction() { when(transaction.getApplicationTransaction()).thenReturn(transactionBytes); // When - state.preHandle(event, consumer); + stateLifecycles.onPreHandle(event, state, consumer); // Then assertThat(consumedTransactions).isEmpty(); diff --git a/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/MigrationTestToolStateLifecycles.java b/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/MigrationTestToolStateLifecycles.java new file mode 100644 index 000000000000..9ca65c1721af --- /dev/null +++ b/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/MigrationTestToolStateLifecycles.java @@ -0,0 +1,170 @@ +/* + * 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.swirlds.demo.migration; + +import static com.swirlds.demo.migration.MigrationTestingToolMain.PREVIOUS_SOFTWARE_VERSION; +import static com.swirlds.demo.migration.TransactionUtils.isSystemTransaction; +import static com.swirlds.logging.legacy.LogMarker.STARTUP; + +import com.hedera.hapi.platform.event.StateSignatureTransaction; +import com.hedera.pbj.runtime.ParseException; +import com.swirlds.common.context.PlatformContext; +import com.swirlds.common.platform.NodeId; +import com.swirlds.merkle.map.MerkleMap; +import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; +import com.swirlds.platform.state.StateLifecycles; +import com.swirlds.platform.system.InitTrigger; +import com.swirlds.platform.system.Platform; +import com.swirlds.platform.system.Round; +import com.swirlds.platform.system.SoftwareVersion; +import com.swirlds.platform.system.address.AddressBook; +import com.swirlds.platform.system.events.ConsensusEvent; +import com.swirlds.platform.system.events.Event; +import com.swirlds.platform.system.transaction.ConsensusTransaction; +import com.swirlds.platform.system.transaction.Transaction; +import com.swirlds.virtualmap.VirtualMap; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Iterator; +import java.util.function.Consumer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * This class handles the lifecycle events for the {@link MigrationTestingToolState}. + */ +public class MigrationTestToolStateLifecycles implements StateLifecycles { + private static final Logger logger = LogManager.getLogger(MigrationTestToolStateLifecycles.class); + + public NodeId selfId; + + @Override + public void onStateInitialized( + @NonNull MigrationTestingToolState state, + @NonNull Platform platform, + @NonNull InitTrigger trigger, + @Nullable SoftwareVersion previousVersion) { + final MerkleMap merkleMap = state.getMerkleMap(); + if (merkleMap != null) { + logger.info(STARTUP.getMarker(), "MerkleMap initialized with {} values", merkleMap.size()); + } + final VirtualMap virtualMap = state.getVirtualMap(); + if (virtualMap != null) { + logger.info(STARTUP.getMarker(), "VirtualMap initialized with {} values", virtualMap.size()); + } + selfId = platform.getSelfId(); + + if (trigger == InitTrigger.GENESIS) { + logger.warn(STARTUP.getMarker(), "InitTrigger was {} when expecting RESTART or RECONNECT", trigger); + selfId = platform.getSelfId(); + } + + if (previousVersion == null || previousVersion.compareTo(PREVIOUS_SOFTWARE_VERSION) != 0) { + logger.warn( + STARTUP.getMarker(), + "previousSoftwareVersion was {} when expecting it to be {}", + previousVersion, + PREVIOUS_SOFTWARE_VERSION); + } + + if (trigger == InitTrigger.GENESIS) { + logger.info(STARTUP.getMarker(), "Doing genesis initialization"); + state.genesisInit(); + } + } + + @Override + public void onHandleConsensusRound( + @NonNull Round round, + @NonNull MigrationTestingToolState state, + @NonNull Consumer> stateSignatureTransactionCallback) { + state.throwIfImmutable(); + for (final Iterator eventIt = round.iterator(); eventIt.hasNext(); ) { + final ConsensusEvent event = eventIt.next(); + for (final Iterator transIt = event.consensusTransactionIterator(); + transIt.hasNext(); ) { + final ConsensusTransaction trans = transIt.next(); + if (trans.isSystem()) { + continue; + } + if (isSystemTransaction(trans.getApplicationTransaction())) { + consumeSystemTransaction(trans, event, stateSignatureTransactionCallback); + continue; + } + + final MigrationTestingToolTransaction mTrans = + TransactionUtils.parseTransaction(trans.getApplicationTransaction()); + mTrans.applyTo(state); + } + } + } + + @Override + public void onPreHandle( + @NonNull Event event, + @NonNull MigrationTestingToolState state, + @NonNull Consumer> stateSignatureTransactionCallback) { + event.forEachTransaction(transaction -> { + + // We don't want to consume deprecated EventTransaction.STATE_SIGNATURE_TRANSACTION system transactions in + // the callback, since it's intended to be used only + // for the new form of encoded system transactions in Bytes. + // We skip the current iteration, if it processes a deprecated system transaction with the + // EventTransaction.STATE_SIGNATURE_TRANSACTION type. + if (transaction.isSystem()) { + return; + } + + if (isSystemTransaction(transaction.getApplicationTransaction())) { + consumeSystemTransaction(transaction, event, stateSignatureTransactionCallback); + } + }); + } + + @Override + public void onSealConsensusRound(@NonNull Round round, @NonNull MigrationTestingToolState state) { + // no-op + } + + @Override + public void onUpdateWeight( + @NonNull MigrationTestingToolState state, + @NonNull AddressBook configAddressBook, + @NonNull PlatformContext context) { + // no-op + } + + @Override + public void onNewRecoveredState(@NonNull MigrationTestingToolState recoveredState) { + // no-op + } + + private void consumeSystemTransaction( + final @NonNull Transaction transaction, + final @NonNull Event event, + final @NonNull Consumer> + stateSignatureTransactionCallback) { + try { + final var stateSignatureTransaction = + StateSignatureTransaction.PROTOBUF.parse(transaction.getApplicationTransaction()); + stateSignatureTransactionCallback.accept(new ScopedSystemTransaction<>( + event.getCreatorId(), event.getSoftwareVersion(), stateSignatureTransaction)); + } catch (final ParseException e) { + logger.error("Failed to parse StateSignatureTransaction", e); + } + } +} diff --git a/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/MigrationTestingToolMain.java b/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/MigrationTestingToolMain.java index cea14826eea1..224875345ded 100644 --- a/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/MigrationTestingToolMain.java +++ b/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/MigrationTestingToolMain.java @@ -33,7 +33,7 @@ import com.swirlds.merkle.map.MerkleMapMetrics; import com.swirlds.platform.ParameterProvider; import com.swirlds.platform.roster.RosterUtils; -import com.swirlds.platform.state.PlatformMerkleStateRoot; +import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.system.BasicSoftwareVersion; import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.SwirldMain; @@ -47,7 +47,7 @@ *

    * Command line arguments: Seed(long), TransactionsPerNode(int) */ -public class MigrationTestingToolMain implements SwirldMain { +public class MigrationTestingToolMain implements SwirldMain { private static final Logger logger = LogManager.getLogger(MigrationTestingToolMain.class); @@ -55,12 +55,9 @@ public class MigrationTestingToolMain implements SwirldMain { try { logger.info(STARTUP.getMarker(), "Registering MigrationTestingToolState with ConstructableRegistry"); ConstructableRegistry constructableRegistry = ConstructableRegistry.getInstance(); - constructableRegistry.registerConstructable( - new ClassConstructorPair(MigrationTestingToolState.class, () -> { - MigrationTestingToolState migrationTestingToolState = new MigrationTestingToolState( - FAKE_MERKLE_STATE_LIFECYCLES, version -> new BasicSoftwareVersion(version.major())); - return migrationTestingToolState; - })); + constructableRegistry.registerConstructable(new ClassConstructorPair( + MigrationTestingToolState.class, + () -> new MigrationTestingToolState(version -> new BasicSoftwareVersion(version.major())))); registerMerkleStateRootClassIds(); logger.info(STARTUP.getMarker(), "MigrationTestingToolState is registered with ConstructableRegistry"); } catch (ConstructableRegistryException e) { @@ -194,14 +191,18 @@ private void throttleTransactionCreation() throws InterruptedException { */ @NonNull @Override - public PlatformMerkleStateRoot newMerkleStateRoot() { - final PlatformMerkleStateRoot state = new MigrationTestingToolState( - FAKE_MERKLE_STATE_LIFECYCLES, + public MigrationTestingToolState newMerkleStateRoot() { + final MigrationTestingToolState state = new MigrationTestingToolState( version -> new BasicSoftwareVersion(softwareVersion.getSoftwareVersion())); FAKE_MERKLE_STATE_LIFECYCLES.initStates(state); return state; } + @Override + public StateLifecycles newStateLifecycles() { + return new MigrationTestToolStateLifecycles(); + } + /** * {@inheritDoc} */ diff --git a/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/MigrationTestingToolState.java b/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/MigrationTestingToolState.java index db801e25980a..d3a87079a46a 100644 --- a/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/MigrationTestingToolState.java +++ b/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/MigrationTestingToolState.java @@ -16,18 +16,12 @@ package com.swirlds.demo.migration; -import static com.swirlds.demo.migration.MigrationTestingToolMain.PREVIOUS_SOFTWARE_VERSION; -import static com.swirlds.demo.migration.TransactionUtils.isSystemTransaction; -import static com.swirlds.logging.legacy.LogMarker.STARTUP; import static com.swirlds.platform.test.fixtures.state.FakeStateLifecycles.FAKE_MERKLE_STATE_LIFECYCLES; import com.hedera.hapi.node.base.SemanticVersion; -import com.hedera.hapi.platform.event.StateSignatureTransaction; -import com.hedera.pbj.runtime.ParseException; import com.swirlds.common.constructable.ConstructableIgnored; import com.swirlds.common.crypto.DigestType; import com.swirlds.common.merkle.MerkleNode; -import com.swirlds.common.platform.NodeId; import com.swirlds.config.api.Configuration; import com.swirlds.config.api.ConfigurationBuilder; import com.swirlds.demo.migration.virtual.AccountVirtualMapKey; @@ -39,26 +33,13 @@ import com.swirlds.merkledb.MerkleDbDataSourceBuilder; import com.swirlds.merkledb.MerkleDbTableConfig; import com.swirlds.merkledb.config.MerkleDbConfig; -import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.state.PlatformMerkleStateRoot; -import com.swirlds.platform.state.PlatformStateModifier; -import com.swirlds.platform.state.StateLifecycles; -import com.swirlds.platform.system.InitTrigger; -import com.swirlds.platform.system.Platform; -import com.swirlds.platform.system.Round; import com.swirlds.platform.system.SoftwareVersion; import com.swirlds.platform.system.address.AddressBook; -import com.swirlds.platform.system.events.ConsensusEvent; -import com.swirlds.platform.system.events.Event; -import com.swirlds.platform.system.transaction.ConsensusTransaction; -import com.swirlds.platform.system.transaction.Transaction; import com.swirlds.virtualmap.VirtualMap; import com.swirlds.virtualmap.datasource.VirtualDataSourceBuilder; import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.Nullable; -import java.util.Iterator; import java.util.List; -import java.util.function.Consumer; import java.util.function.Function; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -114,12 +95,8 @@ private static class ChildIndices { public static final int OLD_CHILD_COUNT = 3; } - public NodeId selfId; - - public MigrationTestingToolState( - @NonNull final StateLifecycles lifecycles, - @NonNull final Function versionFactory) { - super(lifecycles, versionFactory); + public MigrationTestingToolState(@NonNull final Function versionFactory) { + super(versionFactory); allocateSpaceForChild(ChildIndices.CHILD_COUNT); } @@ -127,7 +104,6 @@ private MigrationTestingToolState(final MigrationTestingToolState that) { super(that); that.setImmutable(true); this.setImmutable(false); - this.selfId = that.selfId; } /** @@ -192,14 +168,14 @@ public void addDeserializedChildren(final List children, final int v /** * Get a {@link MerkleMap} that contains various data. */ - protected MerkleMap getMerkleMap() { + MerkleMap getMerkleMap() { return getChild(ChildIndices.MERKLE_MAP); } /** * Set a {@link MerkleMap} that contains various data. */ - protected void setMerkleMap(final MerkleMap map) { + void setMerkleMap(final MerkleMap map) { throwIfImmutable(); setChild(ChildIndices.MERKLE_MAP, map); } @@ -207,7 +183,7 @@ protected void setMerkleMap(final MerkleMap map) { /** * Get a {@link VirtualMap} that contains various data. */ - protected VirtualMap getVirtualMap() { + VirtualMap getVirtualMap() { return getChild(ChildIndices.VIRTUAL_MAP); } @@ -221,7 +197,7 @@ protected void setVirtualMap(final VirtualMap()); @@ -241,95 +217,6 @@ private void genesisInit(final Platform platform) { new AccountVirtualMapValueSerializer(), dsBuilder, configuration)); - selfId = platform.getSelfId(); - } - - /** - * {@inheritDoc} - */ - @Override - public void init( - @NonNull final Platform platform, - @NonNull final InitTrigger trigger, - @Nullable final SoftwareVersion previousSoftwareVersion) { - super.init(platform, trigger, previousSoftwareVersion); - - final MerkleMap merkleMap = getMerkleMap(); - if (merkleMap != null) { - logger.info(STARTUP.getMarker(), "MerkleMap initialized with {} values", merkleMap.size()); - } - final VirtualMap virtualMap = getVirtualMap(); - if (virtualMap != null) { - logger.info(STARTUP.getMarker(), "VirtualMap initialized with {} values", virtualMap.size()); - } - selfId = platform.getSelfId(); - - if (trigger == InitTrigger.GENESIS) { - logger.warn(STARTUP.getMarker(), "InitTrigger was {} when expecting RESTART or RECONNECT", trigger); - } - - if (previousSoftwareVersion == null || previousSoftwareVersion.compareTo(PREVIOUS_SOFTWARE_VERSION) != 0) { - logger.warn( - STARTUP.getMarker(), - "previousSoftwareVersion was {} when expecting it to be {}", - previousSoftwareVersion, - PREVIOUS_SOFTWARE_VERSION); - } - - if (trigger == InitTrigger.GENESIS) { - logger.info(STARTUP.getMarker(), "Doing genesis initialization"); - genesisInit(platform); - } - } - - @Override - public void preHandle( - final @NonNull Event event, - final @NonNull Consumer> stateSignatureTransaction) { - event.forEachTransaction(transaction -> { - - // We don't want to consume deprecated EventTransaction.STATE_SIGNATURE_TRANSACTION system transactions in - // the callback, since it's intended to be used only - // for the new form of encoded system transactions in Bytes. - // We skip the current iteration, if it processes a deprecated system transaction with the - // EventTransaction.STATE_SIGNATURE_TRANSACTION type. - if (transaction.isSystem()) { - return; - } - - if (isSystemTransaction(transaction.getApplicationTransaction())) { - consumeSystemTransaction(transaction, event, stateSignatureTransaction); - } - }); - } - - /** - * {@inheritDoc} - */ - @Override - public void handleConsensusRound( - @NonNull final Round round, - @NonNull final PlatformStateModifier platformState, - @NonNull final Consumer> stateSignatureTransaction) { - throwIfImmutable(); - for (final Iterator eventIt = round.iterator(); eventIt.hasNext(); ) { - final ConsensusEvent event = eventIt.next(); - for (final Iterator transIt = event.consensusTransactionIterator(); - transIt.hasNext(); ) { - final ConsensusTransaction trans = transIt.next(); - if (trans.isSystem()) { - continue; - } - if (isSystemTransaction(trans.getApplicationTransaction())) { - consumeSystemTransaction(trans, event, stateSignatureTransaction); - continue; - } - - final MigrationTestingToolTransaction mTrans = - TransactionUtils.parseTransaction(trans.getApplicationTransaction()); - mTrans.applyTo(this); - } - } } /** @@ -365,19 +252,4 @@ public int getVersion() { public int getMinimumSupportedVersion() { return ClassVersion.VIRTUAL_MAP; } - - private void consumeSystemTransaction( - final @NonNull Transaction transaction, - final @NonNull Event event, - final @NonNull Consumer> - stateSignatureTransactionCallback) { - try { - final var stateSignatureTransaction = - StateSignatureTransaction.PROTOBUF.parse(transaction.getApplicationTransaction()); - stateSignatureTransactionCallback.accept(new ScopedSystemTransaction<>( - event.getCreatorId(), event.getSoftwareVersion(), stateSignatureTransaction)); - } catch (final ParseException e) { - logger.error("Failed to parse StateSignatureTransaction", e); - } - } } diff --git a/platform-sdk/platform-apps/tests/MigrationTestingTool/src/test/java/com/swirlds/demo/migration/MigrationTestingToolStateTest.java b/platform-sdk/platform-apps/tests/MigrationTestingTool/src/test/java/com/swirlds/demo/migration/MigrationTestingToolStateTest.java index eff503460703..23ab24b373b7 100644 --- a/platform-sdk/platform-apps/tests/MigrationTestingTool/src/test/java/com/swirlds/demo/migration/MigrationTestingToolStateTest.java +++ b/platform-sdk/platform-apps/tests/MigrationTestingTool/src/test/java/com/swirlds/demo/migration/MigrationTestingToolStateTest.java @@ -31,8 +31,6 @@ import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.event.PlatformEvent; -import com.swirlds.platform.state.PlatformStateModifier; -import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.system.Round; import com.swirlds.platform.system.events.ConsensusEvent; import com.swirlds.platform.system.transaction.ConsensusTransaction; @@ -53,8 +51,8 @@ class MigrationTestingToolStateTest { private MigrationTestingToolState state; + private MigrationTestToolStateLifecycles stateLifecycles; private Random random; - private PlatformStateModifier platformStateModifier; private Round round; private ConsensusEvent event; private List> consumedTransactions; @@ -64,7 +62,8 @@ class MigrationTestingToolStateTest { @BeforeEach void setUp() { - state = new MigrationTestingToolState(mock(StateLifecycles.class), mock(Function.class)); + state = new MigrationTestingToolState(mock(Function.class)); + stateLifecycles = new MigrationTestToolStateLifecycles(); random = new Random(); round = mock(Round.class); event = mock(ConsensusEvent.class); @@ -97,7 +96,7 @@ void handleConsensusRoundWithApplicationTransaction() throws SignatureException MigrationTestingToolTransaction migrationTestingToolTransaction = Mockito.spy(tr); utilities.when(() -> TransactionUtils.parseTransaction(any())).thenReturn(migrationTestingToolTransaction); Mockito.doNothing().when(migrationTestingToolTransaction).applyTo(state); - state.handleConsensusRound(round, platformStateModifier, consumer); + stateLifecycles.onHandleConsensusRound(round, state, consumer); } assertThat(consumedTransactions).isEmpty(); @@ -110,7 +109,7 @@ void handleConsensusRoundWithSystemTransaction() { StateSignatureTransaction.PROTOBUF.toBytes(stateSignatureTransaction); when(transaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); - state.handleConsensusRound(round, platformStateModifier, consumer); + stateLifecycles.onHandleConsensusRound(round, state, consumer); assertThat(consumedTransactions).hasSize(1); } @@ -134,7 +133,7 @@ void handleConsensusRoundWithMultipleSystemTransactions() { when(secondConsensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); when(thirdConsensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); - state.handleConsensusRound(round, platformStateModifier, consumer); + stateLifecycles.onHandleConsensusRound(round, state, consumer); assertThat(consumedTransactions).hasSize(3); } @@ -145,7 +144,7 @@ void handleConsensusRoundWithDeprecatedSystemTransaction() { when(transaction.getApplicationTransaction()).thenReturn(Bytes.EMPTY); when(transaction.isSystem()).thenReturn(true); - state.handleConsensusRound(round, platformStateModifier, consumer); + stateLifecycles.onHandleConsensusRound(round, state, consumer); assertThat(consumedTransactions).isEmpty(); } @@ -177,7 +176,7 @@ void preHandleEventWithMultipleSystemTransactions() { .thenReturn(List.of(eventTransaction, secondEventTransaction, thirdEventTransaction)); event = new PlatformEvent(gossipEvent); - state.preHandle(event, consumer); + stateLifecycles.onPreHandle(event, state, consumer); assertThat(consumedTransactions).hasSize(3); } @@ -202,7 +201,7 @@ void preHandleEventWithSystemTransaction() { when(eventTransaction.transaction()).thenReturn(systemTransactionWithType); event = new PlatformEvent(gossipEvent); - state.preHandle(event, consumer); + stateLifecycles.onPreHandle(event, state, consumer); assertThat(consumedTransactions).hasSize(1); } @@ -214,7 +213,7 @@ void preHandleEventWithDeprecatedSystemTransaction() { when(round.iterator()).thenReturn(Collections.singletonList(event).iterator()); when(transaction.isSystem()).thenReturn(true); - state.preHandle(event, consumer); + stateLifecycles.onPreHandle(event, state, consumer); assertThat(consumedTransactions).isEmpty(); } diff --git a/platform-sdk/platform-apps/tests/PlatformTestingTool/src/main/java/com/swirlds/demo/platform/PlatformTestingToolMain.java b/platform-sdk/platform-apps/tests/PlatformTestingTool/src/main/java/com/swirlds/demo/platform/PlatformTestingToolMain.java index a3abf9366686..6cb8ff38796b 100644 --- a/platform-sdk/platform-apps/tests/PlatformTestingTool/src/main/java/com/swirlds/demo/platform/PlatformTestingToolMain.java +++ b/platform-sdk/platform-apps/tests/PlatformTestingTool/src/main/java/com/swirlds/demo/platform/PlatformTestingToolMain.java @@ -86,7 +86,7 @@ import com.swirlds.platform.listeners.ReconnectCompleteListener; import com.swirlds.platform.listeners.StateWriteToDiskCompleteListener; import com.swirlds.platform.roster.RosterUtils; -import com.swirlds.platform.state.PlatformMerkleStateRoot; +import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.system.BasicSoftwareVersion; import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.SwirldMain; @@ -127,7 +127,7 @@ * writes them to the screen, and also saves them to disk in a comma separated value (.csv) file. * Each transaction consists of an optional sequence number and random bytes. */ -public class PlatformTestingToolMain implements SwirldMain { +public class PlatformTestingToolMain implements SwirldMain { /** * use this for all logging @@ -441,7 +441,7 @@ private void initAppStat() { FCQueueStatistics.register(metrics); // Register PTT statistics - PlatformTestingToolState.initStatistics(platform); + PlatformTestingToolStateLifecycles.initStatistics(platform); final int SAMPLING_PERIOD = 5000; /* millisecond */ Timer statTimer = new Timer("stat timer" + selfId, true); @@ -711,7 +711,7 @@ public void init(Platform platform, NodeId id) { platform.getNotificationEngine().register(NewSignedStateListener.class, notification -> { if (timeToCheckBalances(notification.getConsensusTimestamp())) { - checkBalances(notification.getSwirldState()); + checkBalances(notification.getStateRoot()); } }); } @@ -862,16 +862,27 @@ public void run() { } } + /** + * {@inheritDoc} + */ @Override @NonNull - public PlatformMerkleStateRoot newMerkleStateRoot() { - final PlatformMerkleStateRoot state = new PlatformTestingToolState( - FAKE_MERKLE_STATE_LIFECYCLES, - version -> new BasicSoftwareVersion(softwareVersion.getSoftwareVersion())); + public PlatformTestingToolState newMerkleStateRoot() { + final PlatformTestingToolState state = + new PlatformTestingToolState(version -> new BasicSoftwareVersion(softwareVersion.getSoftwareVersion())); FAKE_MERKLE_STATE_LIFECYCLES.initStates(state); return state; } + /** + * {@inheritDoc} + */ + @Override + @NonNull + public StateLifecycles newStateLifecycles() { + return new PlatformTestingToolStateLifecycles(); + } + private void platformStatusChange(final PlatformStatusChangeNotification notification) { final PlatformStatus newStatus = notification.getNewStatus(); // set isActive diff --git a/platform-sdk/platform-apps/tests/PlatformTestingTool/src/main/java/com/swirlds/demo/platform/PlatformTestingToolState.java b/platform-sdk/platform-apps/tests/PlatformTestingTool/src/main/java/com/swirlds/demo/platform/PlatformTestingToolState.java index 07ed2a0d0abd..a0c4db6e17d3 100644 --- a/platform-sdk/platform-apps/tests/PlatformTestingTool/src/main/java/com/swirlds/demo/platform/PlatformTestingToolState.java +++ b/platform-sdk/platform-apps/tests/PlatformTestingTool/src/main/java/com/swirlds/demo/platform/PlatformTestingToolState.java @@ -16,37 +16,16 @@ package com.swirlds.demo.platform; -import static com.swirlds.base.units.UnitConstants.MICROSECONDS_TO_NANOSECONDS; -import static com.swirlds.base.units.UnitConstants.NANOSECONDS_TO_MICROSECONDS; import static com.swirlds.common.io.streams.SerializableStreamConstants.NULL_CLASS_ID; -import static com.swirlds.common.utility.CommonUtils.hex; -import static com.swirlds.demo.platform.fs.stresstest.proto.TestTransaction.BodyCase.FCMTRANSACTION; -import static com.swirlds.logging.legacy.LogMarker.DEMO_INFO; import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; -import static com.swirlds.logging.legacy.LogMarker.TESTING_EXCEPTIONS_ACCEPTABLE_RECONNECT; -import static com.swirlds.merkle.test.fixtures.map.lifecycle.SaveExpectedMapHandler.STORAGE_DIRECTORY; -import static com.swirlds.merkle.test.fixtures.map.lifecycle.SaveExpectedMapHandler.createExpectedMapName; -import static com.swirlds.merkle.test.fixtures.map.lifecycle.SaveExpectedMapHandler.serialize; -import static com.swirlds.metrics.api.FloatFormats.FORMAT_11_0; -import static com.swirlds.platform.test.fixtures.state.FakeStateLifecycles.FAKE_MERKLE_STATE_LIFECYCLES; import com.fasterxml.jackson.annotation.JsonIgnore; -import com.google.protobuf.InvalidProtocolBufferException; import com.hedera.hapi.node.base.SemanticVersion; -import com.hedera.hapi.platform.event.StateSignatureTransaction; -import com.swirlds.common.constructable.*; -import com.swirlds.common.crypto.CryptographyHolder; -import com.swirlds.common.crypto.SignatureType; -import com.swirlds.common.crypto.TransactionSignature; -import com.swirlds.common.crypto.VerificationStatus; +import com.swirlds.common.constructable.ConstructableIgnored; import com.swirlds.common.merkle.MerkleNode; -import com.swirlds.common.metrics.RunningAverageMetric; -import com.swirlds.common.platform.NodeId; import com.swirlds.common.utility.ThresholdLimitingHandler; import com.swirlds.demo.merkle.map.FCMConfig; import com.swirlds.demo.merkle.map.FCMFamily; -import com.swirlds.demo.merkle.map.FCMTransactionHandler; -import com.swirlds.demo.merkle.map.FCMTransactionUtils; import com.swirlds.demo.merkle.map.internal.ExpectedFCMFamily; import com.swirlds.demo.merkle.map.internal.ExpectedFCMFamilyImpl; import com.swirlds.demo.platform.actions.Action; @@ -54,22 +33,10 @@ import com.swirlds.demo.platform.actions.QuorumTriggeredAction; import com.swirlds.demo.platform.expiration.ExpirationRecordEntry; import com.swirlds.demo.platform.expiration.ExpirationUtils; -import com.swirlds.demo.platform.freeze.FreezeTransactionHandler; -import com.swirlds.demo.platform.fs.stresstest.proto.Activity; -import com.swirlds.demo.platform.fs.stresstest.proto.AppTransactionSignatureType; import com.swirlds.demo.platform.fs.stresstest.proto.ControlTransaction; -import com.swirlds.demo.platform.fs.stresstest.proto.ControlType; -import com.swirlds.demo.platform.fs.stresstest.proto.FCMTransaction; -import com.swirlds.demo.platform.fs.stresstest.proto.FreezeTransaction; -import com.swirlds.demo.platform.fs.stresstest.proto.RandomBytesTransaction; -import com.swirlds.demo.platform.fs.stresstest.proto.SimpleAction; -import com.swirlds.demo.platform.fs.stresstest.proto.TestTransaction; -import com.swirlds.demo.platform.fs.stresstest.proto.TestTransactionWrapper; -import com.swirlds.demo.platform.fs.stresstest.proto.VirtualMerkleTransaction; import com.swirlds.demo.platform.iss.IssLeaf; import com.swirlds.demo.platform.nft.NftId; import com.swirlds.demo.platform.nft.NftLedger; -import com.swirlds.demo.platform.nft.NftLedgerStatistics; import com.swirlds.demo.platform.nft.ReferenceNftLedger; import com.swirlds.demo.virtualmerkle.map.account.AccountVirtualMapKey; import com.swirlds.demo.virtualmerkle.map.account.AccountVirtualMapValue; @@ -77,48 +44,23 @@ import com.swirlds.demo.virtualmerkle.map.smartcontracts.bytecode.SmartContractByteCodeMapValue; import com.swirlds.demo.virtualmerkle.map.smartcontracts.data.SmartContractMapKey; import com.swirlds.demo.virtualmerkle.map.smartcontracts.data.SmartContractMapValue; -import com.swirlds.demo.virtualmerkle.transaction.handler.VirtualMerkleTransactionHandler; -import com.swirlds.logging.legacy.payload.SoftwareVersionPayload; -import com.swirlds.merkle.test.fixtures.map.lifecycle.EntityType; -import com.swirlds.merkle.test.fixtures.map.lifecycle.TransactionState; -import com.swirlds.merkle.test.fixtures.map.lifecycle.TransactionType; import com.swirlds.merkle.test.fixtures.map.pta.MapKey; -import com.swirlds.platform.ParameterProvider; -import com.swirlds.platform.Utilities; -import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.roster.RosterUtils; import com.swirlds.platform.state.PlatformMerkleStateRoot; -import com.swirlds.platform.state.PlatformStateModifier; -import com.swirlds.platform.state.StateLifecycles; -import com.swirlds.platform.system.*; +import com.swirlds.platform.system.BasicSoftwareVersion; +import com.swirlds.platform.system.Platform; +import com.swirlds.platform.system.SoftwareVersion; import com.swirlds.platform.system.address.AddressBook; -import com.swirlds.platform.system.events.ConsensusEvent; -import com.swirlds.platform.system.events.Event; -import com.swirlds.platform.system.transaction.ConsensusTransaction; -import com.swirlds.platform.system.transaction.Transaction; import com.swirlds.virtualmap.VirtualMap; import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.Nullable; -import java.io.File; -import java.nio.ByteBuffer; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.time.Instant; -import java.util.Arrays; import java.util.HashSet; import java.util.List; -import java.util.Objects; -import java.util.Optional; import java.util.Set; -import java.util.Timer; -import java.util.TimerTask; import java.util.concurrent.BlockingQueue; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; import java.util.concurrent.PriorityBlockingQueue; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Consumer; import java.util.function.Function; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -136,80 +78,17 @@ public class PlatformTestingToolState extends PlatformMerkleStateRoot { static final long CLASS_ID = 0xc0900cfa7a24db76L; private static final Logger logger = LogManager.getLogger(PlatformTestingToolState.class); private static final Marker LOGM_DEMO_INFO = MarkerManager.getMarker("DEMO_INFO"); - private static final Marker LOGM_EXCEPTION = MarkerManager.getMarker("EXCEPTION"); - private static final Marker LOGM_EXPIRATION = MarkerManager.getMarker("EXPIRATION"); - private static final Marker LOGM_STARTUP = MarkerManager.getMarker("STARTUP"); private static final long EXCEPTION_RATE_THRESHOLD = 10; - static final String STAT_TIMER_THREAD_NAME = "stat timer PTTState"; - /** * The fraction (out of 1.0) of NFT tokens to track in the reference data structure. */ private static final double NFT_TRACKING_FRACTION = 0.001; - /** - * Defined in settings. the consensus timestamp of a transaction is guaranteed to be at least this many nanoseconds - * later than that of the transaction immediately before it in consensus order, and to be a multiple of this - */ - private static final long minTransTimestampIncrNanos = 1_000; - - /** - * statistics for handleTransaction - */ - private static final String HANDLE_TRANSACTION_CATEGORY = "HandleTransaction"; - - private static long htCountFCM; - private static long htFCMSumNano; - private static final RunningAverageMetric.Config HT_FCM_MICRO_SEC_CONFIG = new RunningAverageMetric.Config( - HANDLE_TRANSACTION_CATEGORY, "htFCMTimeMicroSec") - .withDescription("average handleTransaction (FCM) Time, microseconds"); - private static RunningAverageMetric htFCMMicroSec; - private static long htCountFCQ; - private static long htFCQSumNano; - - private static final RunningAverageMetric.Config HT_FCQ_MICRO_SEC_CONFIG = new RunningAverageMetric.Config( - HANDLE_TRANSACTION_CATEGORY, "htFCQTimeMicroSec") - .withDescription("average handleTransaction (FCQ) Time, microseconds"); - private static RunningAverageMetric htFCQMicroSec; - /** * Has init() been called on this copy or an ancestor copy of this object? */ private final AtomicBoolean initialized = new AtomicBoolean(false); - private static long htCountExpiration; - private static long htFCQExpirationSumMicro; - - private static final RunningAverageMetric.Config HT_FCQ_EXPIRATION_MICRO_SEC_CONFIG = - new RunningAverageMetric.Config(HANDLE_TRANSACTION_CATEGORY, "htFCQExpirationMicroSec") - .withDescription("FCQ Expiration Time per call, microseconds"); - private static RunningAverageMetric htFCQExpirationMicroSec; - - private static final RunningAverageMetric.Config HT_FCM_SIZE_CONFIG = new RunningAverageMetric.Config( - HANDLE_TRANSACTION_CATEGORY, "htFCMSize") - .withDescription("FCM Tree Size (accounts)") - .withFormat(FORMAT_11_0); - private static RunningAverageMetric htFCMSize; - - /////////////////////////////////////////// - // Transaction Handlers - private static long htFCMAccounts; - - private static final RunningAverageMetric.Config HT_FCQ_SIZE_CONFIG = new RunningAverageMetric.Config( - HANDLE_TRANSACTION_CATEGORY, "htFCQSize") - .withDescription("FCQ Tree Size (accounts)") - .withFormat(FORMAT_11_0); - private static RunningAverageMetric htFCQSize; - - private static long htFCQAccounts; - - private static final RunningAverageMetric.Config HT_FCQ_RECORDS_CONFIG = new RunningAverageMetric.Config( - HANDLE_TRANSACTION_CATEGORY, "htFCQRecords") - .withDescription("FCQ Transaction Records") - .withFormat(FORMAT_11_0); - private static RunningAverageMetric htFCQRecords; - - private static long htFCQRecordsCount; /** * Used for validation of part (or all) of the data in the NFT ledger. */ @@ -234,30 +113,18 @@ public class PlatformTestingToolState extends PlatformMerkleStateRoot { private BlockingQueue expirationQueue; // contains MapKeys which has an entry in ExpirationQueue private Set accountsWithExpiringRecords; - // last timestamp purging records - private long lastPurgeTimestamp = 0; - /** - * The instant of the previously handled transaction. Null if no transactions have yet been handled by this state - * instance. Used to verify that each transaction happens at a later instant than its predecessor. - */ - private Instant previousTimestamp; /** * Handles quorum determinations for all {@link ControlTransaction} processed by the handle method. */ private QuorumTriggeredAction controlQuorum; - private long transactionsIgnoredByExpectedMap = 0; - public PlatformTestingToolState() { - this(FAKE_MERKLE_STATE_LIFECYCLES, version -> new BasicSoftwareVersion(version.major())); + this(version -> new BasicSoftwareVersion(version.major())); } - public PlatformTestingToolState( - @NonNull final StateLifecycles lifecycles, - @NonNull final Function versionFactory) { - super(lifecycles, versionFactory); + public PlatformTestingToolState(@NonNull final Function versionFactory) { + super(versionFactory); expectedFCMFamily = new ExpectedFCMFamilyImpl(); - referenceNftLedger = new ReferenceNftLedger(NFT_TRACKING_FRACTION); } @@ -298,76 +165,6 @@ protected PlatformTestingToolState(final PlatformTestingToolState sourceState) { sourceState.setImmutable(true); } - /** - * startup any statistics - * - * @param platform Platform - */ - public static void initStatistics(final Platform platform) { - if (htFCMMicroSec != null) { - return; - } - - /* Add handleTransaction statistics */ - htFCMMicroSec = platform.getContext().getMetrics().getOrCreate(HT_FCM_MICRO_SEC_CONFIG); - htFCQMicroSec = platform.getContext().getMetrics().getOrCreate(HT_FCQ_MICRO_SEC_CONFIG); - htFCQExpirationMicroSec = platform.getContext().getMetrics().getOrCreate(HT_FCQ_EXPIRATION_MICRO_SEC_CONFIG); - htFCMSize = platform.getContext().getMetrics().getOrCreate(HT_FCM_SIZE_CONFIG); - htFCQSize = platform.getContext().getMetrics().getOrCreate(HT_FCQ_SIZE_CONFIG); - htFCQRecords = platform.getContext().getMetrics().getOrCreate(HT_FCQ_RECORDS_CONFIG); - - NftLedgerStatistics.register(platform); - - // timer to update output stats - final int SAMPLING_PERIOD = 5000; // millisecond - final Timer statTimer = new Timer(STAT_TIMER_THREAD_NAME, true); - statTimer.schedule( - new TimerTask() { - @Override - public void run() { - getCurrentTransactionStat(); - } - }, - 0, - SAMPLING_PERIOD); - } - - private static void getCurrentTransactionStat() { - if (htFCMMicroSec == null) { - return; - } - - // fcm, nsec - if (htCountFCM > 0) { - htFCMMicroSec.update((double) htFCMSumNano / (double) htCountFCM * NANOSECONDS_TO_MICROSECONDS); - } else { - htFCMMicroSec.update(0); - } - htFCMSumNano = 0; - htCountFCM = 0; - // fcq, nsec - if (htCountFCQ > 0) { - htFCQMicroSec.update((double) htFCQSumNano / (double) htCountFCQ * NANOSECONDS_TO_MICROSECONDS); - } else { - htFCQMicroSec.update(0); - } - htFCQSumNano = 0; - htCountFCQ = 0; - // expiration, in microseconds - if (htCountExpiration > 0) { - htFCQExpirationMicroSec.update((double) htFCQExpirationSumMicro / (double) htCountExpiration); - } else { - htFCQExpirationMicroSec.update(0); - } - htFCQExpirationSumMicro = 0; - htCountExpiration = 0; - - // datastructure sizes - htFCMSize.update(htFCMAccounts); - htFCQSize.update(htFCQAccounts); - htFCQRecords.update(htFCQRecordsCount); - } - // count invalid signature ratio static AtomicLong totalTransactionSignatureCount = new AtomicLong(0); static AtomicLong expectedInvalidSignatureCount = new AtomicLong(0); @@ -439,11 +236,11 @@ public void addDeserializedChildren(final List children, final int v super.addDeserializedChildren(children, version); } - private PayloadCfgSimple getConfig() { + PayloadCfgSimple getConfig() { return getChild(ChildIndices.CONFIG); } - private void setConfig(final PayloadCfgSimple config) { + void setConfig(final PayloadCfgSimple config) { setChild(ChildIndices.CONFIG, config); } @@ -455,7 +252,7 @@ public NextSeqConsList getNextSeqCons() { return getChild(ChildIndices.NEXT_SEQUENCE_CONSENSUS); } - private void setNextSeqCons(final NextSeqConsList nextSeqCons) { + void setNextSeqCons(final NextSeqConsList nextSeqCons) { setChild(ChildIndices.NEXT_SEQUENCE_CONSENSUS, nextSeqCons); } @@ -502,15 +299,15 @@ public TransactionCounterList getTransactionCounter() { return getChild(ChildIndices.TRANSACTION_COUNTER); } - private void setTransactionCounter(final TransactionCounterList transactionCounter) { + void setTransactionCounter(final TransactionCounterList transactionCounter) { setChild(ChildIndices.TRANSACTION_COUNTER, transactionCounter); } - private IssLeaf getIssLeaf() { + IssLeaf getIssLeaf() { return getChild(ChildIndices.ISS_LEAF); } - private void setIssLeaf(final IssLeaf leaf) { + void setIssLeaf(final IssLeaf leaf) { setChild(ChildIndices.ISS_LEAF, leaf); } @@ -548,10 +345,6 @@ public Set getAccountsWithExpiringRecords() { return accountsWithExpiringRecords; } - public long getLastPurgeTimestamp() { - return lastPurgeTimestamp; - } - public synchronized void setPayloadConfig(final FCMConfig fcmConfig) { expectedFCMFamily.setNodeId(platform.getSelfId().id()); expectedFCMFamily.setFcmConfig(fcmConfig); @@ -613,15 +406,6 @@ public synchronized PlatformTestingToolState copy() { setImmutable(true); roundCounter++; - if (transactionsIgnoredByExpectedMap > 0) { - logger.info( - DEMO_INFO.getMarker(), - "Copying round {}, transactions ignored by expected map: {}." - + " This log is added to debug #11254", - roundCounter, - transactionsIgnoredByExpectedMap); - } - final PlatformTestingToolState mutableCopy = new PlatformTestingToolState(this); if (platform != null) { @@ -631,831 +415,6 @@ public synchronized PlatformTestingToolState copy() { return mutableCopy; } - /** - * Make sure that the timestamp for the submitted transaction is not earlier than (the timestamp for the previous - * transaction + minTransTimestampIncrNanos). - */ - private void validateTimestamp(final Instant timestamp) { - if (previousTimestamp != null) { - final Instant previousTransTimestampPlusIncr = previousTimestamp.plusNanos(minTransTimestampIncrNanos); - if (timestamp.isBefore(previousTransTimestampPlusIncr)) { - logger.error( - EXCEPTION.getMarker(), - "Transaction has timestamp {} which is earlier than previous timestamp {} plus {} nanos: " - + "{}", - timestamp, - previousTimestamp, - minTransTimestampIncrNanos, - previousTransTimestampPlusIncr); - } - } - previousTimestamp = timestamp; - } - - /** - * Check if the signatures on the transaction are invalid. - * - * @return true if the signature is invalid - */ - private boolean checkIfSignatureIsInvalid(final Transaction trans) { - if (getConfig().isAppendSig()) { - return validateSignatures(trans); - } - return false; - } - - /** - * If configured, delay this transaction. - */ - private void delay() { - if (getConfig().getDelayCfg() != null) { - final int delay = getConfig().getDelayCfg().getRandomDelay(); - try { - Thread.sleep(delay); - } catch (final InterruptedException e) { - logger.info(LOGM_DEMO_INFO, "", e); - } - } - SyntheticBottleneckConfig.getActiveConfig() - .throttleIfNeeded(platform.getSelfId().id()); - } - - /** - * Instantiate TestTransaction object from transaction byte array. - * - * @return the instantiated transaction if no errors, otherwise null - */ - private Optional unpackTransaction(final Transaction trans) { - try { - final byte[] payloadBytes = trans.getApplicationTransaction().toByteArray(); - if (getConfig().isAppendSig()) { - final byte[] testTransactionRawBytes = TestTransactionWrapper.parseFrom(payloadBytes) - .getTestTransactionRawBytes() - .toByteArray(); - return Optional.of(TestTransaction.parseFrom(testTransactionRawBytes)); - } else { - return Optional.of(TestTransaction.parseFrom(payloadBytes)); - } - } catch (final InvalidProtocolBufferException ex) { - exceptionRateLimiter.handle( - ex, (error) -> logger.error(EXCEPTION.getMarker(), "InvalidProtocolBufferException", error)); - return Optional.empty(); - } - } - - /** - * If configured to do so, purge expired records as needed. - */ - private void purgeExpiredRecordsIfNeeded(final TestTransaction testTransaction, final Instant timestamp) { - if (!testTransaction.hasControlTransaction() - || testTransaction.getControlTransaction().getType() != ControlType.EXIT_VALIDATION) { - - if (getFcmFamily().getAccountFCQMap().size() > 0) { - // remove expired records from FCQs before handling transaction if accountFCQMap has any entities - try { - purgeExpiredRecords(timestamp.getEpochSecond()); - } catch (final Throwable ex) { - exceptionRateLimiter.handle( - ex, - (error) -> logger.error(EXCEPTION.getMarker(), "Failed to purge expired records", error)); - } - } - } - } - - /** - * Write a special log message if this is the first transaction and total fcm and any file statistics are all - * zeroes. - */ - private void logIfFirstTransaction(final NodeId id) { - final int nodeIndex = RosterUtils.getIndex(platform.getRoster(), id.id()); - if (progressCfg != null - && progressCfg.getProgressMarker() > 0 - && getTransactionCounter().get(nodeIndex).getAllTransactionAmount() == 0) { - logger.info(LOGM_DEMO_INFO, "PlatformTestingDemo HANDLE ALL START"); - } - } - - /** - * Handle the random bytes transaction type. - */ - private void handleBytesTransaction(@NonNull final TestTransaction testTransaction, @NonNull final NodeId id) { - Objects.requireNonNull(testTransaction, "testTransaction must not be null"); - Objects.requireNonNull(id, "id must not be null"); - final int nodeIndex = RosterUtils.getIndex(platform.getRoster(), id.id()); - final RandomBytesTransaction bytesTransaction = testTransaction.getBytesTransaction(); - if (bytesTransaction.getIsInserSeq()) { - final long seq = Utilities.toLong(bytesTransaction.getData().toByteArray()); - if (getNextSeqCons().get(nodeIndex).getValue() != seq) { - logger.error( - EXCEPTION.getMarker(), - platform.getSelfId() + " error, new (id=" + id - + ") seq should be " + getNextSeqCons().get(nodeIndex) - + " but is " + seq); - } - getNextSeqCons().get(nodeIndex).getAndIncrement(); - } - } - - /** - * Handle the Virtual Merkle transaction type. - */ - private void handleVirtualMerkleTransaction( - @NonNull final VirtualMerkleTransaction virtualMerkleTransaction, - @NonNull final NodeId id, - @NonNull final Instant consensusTimestamp) { - Objects.requireNonNull(virtualMerkleTransaction, "virtualMerkleTransaction must not be null"); - Objects.requireNonNull(id, "id must not be null"); - Objects.requireNonNull(consensusTimestamp, "consensusTimestamp must not be null"); - final int nodeIndex = RosterUtils.getIndex(platform.getRoster(), id.id()); - VirtualMerkleTransactionHandler.handle( - consensusTimestamp, - virtualMerkleTransaction, - expectedFCMFamily, - getVirtualMap(), - getVirtualMapForSmartContracts(), - getVirtualMapForSmartContractsByteCode()); - - if (virtualMerkleTransaction.hasCreateAccount()) { - getTransactionCounter().get(nodeIndex).vmCreateAmount++; - } else if (virtualMerkleTransaction.hasUpdateAccount()) { - getTransactionCounter().get(nodeIndex).vmUpdateAmount++; - } else if (virtualMerkleTransaction.hasDeleteAccount()) { - getTransactionCounter().get(nodeIndex).vmDeleteAmount++; - } else if (virtualMerkleTransaction.hasSmartContract()) { - getTransactionCounter().get(nodeIndex).vmContractCreateAmount++; - } else if (virtualMerkleTransaction.hasMethodExecution()) { - getTransactionCounter().get(nodeIndex).vmContractExecutionAmount++; - } - } - - /** - * Handle the FCM transaction type. - */ - private void handleFCMTransaction( - @NonNull final TestTransaction testTransaction, - @NonNull final NodeId id, - @NonNull final Instant timestamp, - final boolean invalidSig) { - Objects.requireNonNull(testTransaction, "testTransaction must not be null"); - Objects.requireNonNull(id, "id must not be null"); - Objects.requireNonNull(timestamp, "timestamp must not be null"); - - final int nodeIndex = RosterUtils.getIndex(platform.getRoster(), id.id()); - final FCMTransaction fcmTransaction = testTransaction.getFcmTransaction(); - - // Handle Activity transaction, which doesn't effect any entity's lifecyle - if (fcmTransaction.hasActivity()) { - final Activity.ActivityType activityType = - fcmTransaction.getActivity().getType(); - if (nodeIndex == 0 && activityType == Activity.ActivityType.SAVE_EXPECTED_MAP) { - // Serialize ExpectedMap to disk in JSON format - TransactionSubmitter.setForcePauseCanSubmitMore(new AtomicBoolean(true)); - serialize( - expectedFCMFamily.getExpectedMap(), - new File(STORAGE_DIRECTORY), - createExpectedMapName(platform.getSelfId().id(), timestamp), - false); - TransactionSubmitter.setForcePauseCanSubmitMore(new AtomicBoolean(false)); - logger.info(LOGM_DEMO_INFO, "handling SAVE_EXPECTED_MAP"); - - } else if (activityType == Activity.ActivityType.SAVE_EXPECTED_MAP) { - logger.info(LOGM_DEMO_INFO, "Received SAVE_EXPECTED_MAP transaction from node {}", id); - } else { - logger.info(EXCEPTION.getMarker(), "unknown Activity type"); - } - return; - } - - if (fcmTransaction.hasDummyTransaction()) { - return; - } - - // Extract MapKeys, TransactionType, and EntityType from FCMTransaction - // which might effect entity's lifecycle status - final List keys = FCMTransactionUtils.getMapKeys(fcmTransaction); - final TransactionType transactionType = FCMTransactionUtils.getTransactionType(fcmTransaction); - final EntityType entityType = FCMTransactionUtils.getEntityType(fcmTransaction); - final long epochMillis = timestamp.toEpochMilli(); - - final long originId = fcmTransaction.getOriginNode(); - - if ((keys.isEmpty() && entityType != EntityType.NFT) || transactionType == null || entityType == null) { - logger.error( - EXCEPTION.getMarker(), - "Invalid Transaction: keys: {}, transactionType: {}, entityType: {}", - keys, - transactionType, - entityType); - return; - } - - // if there is any error, we don't handle this transaction - if (!expectedFCMFamily.shouldHandleForKeys( - keys, transactionType, getConfig(), entityType, epochMillis, originId) - && entityType != EntityType.NFT) { - transactionsIgnoredByExpectedMap++; - return; - } - - // If signature verification result doesn't match expected result - // which is set when generating this FCMTransaction, it denotes an error - if (getConfig().isAppendSig() && invalidSig != fcmTransaction.getInvalidSig()) { - logger.error( - EXCEPTION.getMarker(), - "Unexpected signature verification result: " + "actual: {}; expected:{}", - () -> (invalidSig ? "INVALID" : "VALID"), - () -> (fcmTransaction.getInvalidSig() ? "INVALID" : "VALID")); - - // if the entity doesn't exist, put it to expectedMap; - // set its ExpectedValues's isErrored to be true; - // set its latestHandledStatus to be INVALID_SIG - expectedFCMFamily.setLatestHandledStatusForKey( - keys.get(0), - entityType, - null, - TransactionState.INVALID_SIG, - transactionType, - epochMillis, - originId, - true); - return; - } - - // for expected invalid sig, - // set expectedValue's latestHandledStatus to be EXPECTED_INVALID_SIG - // and handle this transaction - if (invalidSig) { - expectedFCMFamily.setLatestHandledStatusForKey( - keys.get(0), - entityType, - null, - TransactionState.EXPECTED_INVALID_SIG, - transactionType, - epochMillis, - originId, - false); - } - - // Handle the transaction and set latestHandledStatus - try { - FCMTransactionHandler.performOperation( - fcmTransaction, - this, - this.expectedFCMFamily, - originId, - epochMillis, - entityType, - timestamp.getEpochSecond() + getConfig().getFcqTtl(), - expirationQueue, - accountsWithExpiringRecords); - } catch (final Exception ex) { - exceptionRateLimiter.handle( - ex, - (error) -> logger.error( - EXCEPTION.getMarker(), - "Exceptions while handling transaction: {} {} for {}, originId:{}", - transactionType, - entityType, - keys, - originId, - error)); - - // if the entity doesn't exist, put it to expectedMap; - // set its ExpectedValues's isErrored to be true; - // set its latestHandledStatus to be HANDLE_FAILED - for (final MapKey key : keys) { - expectedFCMFamily.setLatestHandledStatusForKey( - key, - entityType, - null, - TransactionState.HANDLE_FAILED, - transactionType, - timestamp.toEpochMilli(), - originId, - true); - } - } - if (fcmTransaction.hasCreateAccount()) { - getTransactionCounter().get(nodeIndex).fcmCreateAmount++; - if (progressCfg != null) { - logProgress( - id, - progressCfg.getProgressMarker(), - PAYLOAD_TYPE.TYPE_FCM_CREATE, - progressCfg.getExpectedFCMCreateAmount(), - getTransactionCounter().get(nodeIndex).fcmCreateAmount); - } - } else if (fcmTransaction.hasTransferBalance()) { - getTransactionCounter().get(nodeIndex).fcmTransferAmount++; - if (progressCfg != null) { - logProgress( - id, - progressCfg.getProgressMarker(), - PAYLOAD_TYPE.TYPE_FCM_TRANSFER, - progressCfg.getExpectedFCMTransferAmount(), - getTransactionCounter().get(nodeIndex).fcmTransferAmount); - } - } else if (fcmTransaction.hasDeleteAccount()) { - getTransactionCounter().get(nodeIndex).fcmDeleteAmount++; - if (progressCfg != null) { - logProgress( - id, - progressCfg.getProgressMarker(), - PAYLOAD_TYPE.TYPE_FCM_DELETE, - progressCfg.getExpectedFCMDeleteAmount(), - getTransactionCounter().get(nodeIndex).fcmDeleteAmount); - } - } else if (fcmTransaction.hasUpdateAccount()) { - getTransactionCounter().get(nodeIndex).fcmUpdateAmount++; - if (progressCfg != null) { - logProgress( - id, - progressCfg.getProgressMarker(), - PAYLOAD_TYPE.TYPE_FCM_UPDATE, - progressCfg.getExpectedFCMUpdateAmount(), - getTransactionCounter().get(nodeIndex).fcmUpdateAmount); - } - } else if (fcmTransaction.hasAssortedAccount()) { - getTransactionCounter().get(nodeIndex).fcmAssortedAmount++; - if (progressCfg != null) { - logProgress( - id, - progressCfg.getProgressMarker(), - PAYLOAD_TYPE.TYPE_FCM_ASSORTED, - progressCfg.getExpectedFCMAssortedAmount(), - getTransactionCounter().get(nodeIndex).fcmAssortedAmount); - } - } else if (fcmTransaction.hasAssortedFCQ()) { - getTransactionCounter().get(nodeIndex).fcmFCQAssortedAmount++; - } else if (fcmTransaction.hasCreateAccountFCQ()) { - getTransactionCounter().get(nodeIndex).fcmFCQCreateAmount++; - } else if (fcmTransaction.hasUpdateAccountFCQ()) { - getTransactionCounter().get(nodeIndex).fcmFCQUpdateAmount++; - } else if (fcmTransaction.hasTransferBalanceFCQ()) { - getTransactionCounter().get(nodeIndex).fcmFCQTransferAmount++; - } else if (fcmTransaction.hasDeleteFCQNode()) { - getTransactionCounter().get(nodeIndex).fcmFCQDeleteAmount++; - } - } - - /** - * Handle the control transaction type. - */ - private void handleControlTransaction( - @NonNull final TestTransaction testTransaction, - @NonNull final NodeId id, - @NonNull final Instant timestamp) { - Objects.requireNonNull(testTransaction, "testTransaction must not be null"); - Objects.requireNonNull(id, "id must not be null"); - Objects.requireNonNull(timestamp, "timestamp must not be null"); - - final long nodeIndex = RosterUtils.getIndex(platform.getRoster(), id.id()); - final ControlTransaction msg = testTransaction.getControlTransaction(); - logger.info( - DEMO_INFO.getMarker(), - "Handling Control Transaction [ originatingNodeId = {}, type = {}, consensusTimestamp = {} ]", - () -> id, - msg::getType, - () -> timestamp); - - // Must use auto reset here, otherwise if reached quorum EXIT_VALIDATION then PTT restart, QuorumResult would be - // reloaded from saved state with reaching quorum state EXIT_VALIDATION as true, - // then TransactionSubmitter won't be able to submit transaction due to some check mechanism. - controlQuorum.withAutoReset().check(nodeIndex, new ControlAction(timestamp, msg.getType())); - // updating quorum result after handling control transactions - setQuorumResult(controlQuorum.getQuorumResult().copy()); - } - - /** - * Handle the freeze transaction type. - */ - private void handleFreezeTransaction( - final TestTransaction testTransaction, final PlatformStateModifier platformState) { - final FreezeTransaction freezeTx = testTransaction.getFreezeTransaction(); - FreezeTransactionHandler.freeze(freezeTx, platformState); - } - - /** - * Handle a simple action transaction type - */ - private void handleSimpleAction(final SimpleAction simpleAction) { - if (simpleAction == SimpleAction.CAUSE_ISS) { - getIssLeaf().setWriteRandom(true); - } - } - - protected void preHandleTransaction(final Transaction transaction) { - if (transaction.isSystem()) { - return; - } - expandSignatures(transaction); - } - - @Override - public synchronized void handleConsensusRound( - @NonNull final Round round, - @NonNull final PlatformStateModifier platformState, - @NonNull final Consumer> stateSignatureTransaction) { - throwIfImmutable(); - if (!initialized.get()) { - throw new IllegalStateException("handleConsensusRound() called before init()"); - } - delay(); - updateTransactionCounters(); - round.forEachEventTransaction((event, transaction) -> - handleConsensusTransaction(event, transaction, platformState, round.getRoundNum())); - } - - /** - * If the size of the address book has changed, zero out the transaction counters and resize, as needed. - */ - private void updateTransactionCounters() { - if (getTransactionCounter() == null - || getTransactionCounter().size() - != platform.getRoster().rosterEntries().size()) { - setNextSeqCons( - new NextSeqConsList(platform.getRoster().rosterEntries().size())); - - logger.info(DEMO_INFO.getMarker(), "resetting transaction counters"); - - setTransactionCounter(new TransactionCounterList( - platform.getRoster().rosterEntries().size())); - for (int id = 0; id < platform.getRoster().rosterEntries().size(); id++) { - getTransactionCounter().add(new TransactionCounter(id)); - } - } - } - - private void handleConsensusTransaction( - final ConsensusEvent event, - final ConsensusTransaction trans, - final PlatformStateModifier platformState, - final long roundNum) { - if (trans.isSystem()) { - return; - } - try { - waitForSignatureValidation(trans); - handleTransaction( - event.getCreatorId(), event.getTimeCreated(), trans.getConsensusTimestamp(), trans, platformState); - } catch (final InterruptedException e) { - logger.info( - TESTING_EXCEPTIONS_ACCEPTABLE_RECONNECT.getMarker(), - "handleConsensusRound Interrupted [ nodeId = {}, round = {} ]. " - + "This should happen only during a reconnect", - platform.getSelfId().id(), - roundNum); - Thread.currentThread().interrupt(); - } catch (final ExecutionException e) { - logger.error(EXCEPTION.getMarker(), "Exception while handling transaction", e); - } - } - - private static void waitForSignatureValidation(final ConsensusTransaction transaction) - throws InterruptedException, ExecutionException { - final TransactionSignature sig = transaction.getMetadata(); - if (sig == null) { - return; - } - final Future future = sig.waitForFuture(); - - // Block & Ignore the Void return - future.get(); - } - - private void handleTransaction( - @NonNull final NodeId id, - @NonNull final Instant timeCreated, - @NonNull final Instant timestamp, - @NonNull final ConsensusTransaction trans, - @NonNull final PlatformStateModifier platformState) { - if (getConfig().isAppendSig()) { - try { - final TestTransactionWrapper testTransactionWrapper = TestTransactionWrapper.parseFrom( - trans.getApplicationTransaction().toByteArray()); - final byte[] testTransactionRawBytes = - testTransactionWrapper.getTestTransactionRawBytes().toByteArray(); - final byte[] publicKey = - testTransactionWrapper.getPublicKeyRawBytes().toByteArray(); - final byte[] signature = - testTransactionWrapper.getSignaturesRawBytes().toByteArray(); - - // if this is expected manually inject invalid signature - boolean expectingInvalidSignature = false; - final TestTransaction testTransaction = TestTransaction.parseFrom(testTransactionRawBytes); - if (testTransaction.getBodyCase() == FCMTRANSACTION) { - final FCMTransaction fcmTransaction = testTransaction.getFcmTransaction(); - if (fcmTransaction.getInvalidSig()) { - expectingInvalidSignature = true; - } - } - totalTransactionSignatureCount.incrementAndGet(); - final TransactionSignature s = trans.getMetadata(); - if (s != null && s.getSignatureStatus() != VerificationStatus.VALID && (!expectingInvalidSignature)) { - logger.error( - EXCEPTION.getMarker(), - "Invalid Transaction Signature [status = {}, signatureType = {}, " - + "publicKey = {}, signature = {}, data = {}, " - + "actualPublicKey = {}, actualSignature = {}, actualData = {} ]", - s.getSignatureStatus(), - s.getSignatureType(), - hex(publicKey), - hex(signature), - hex(testTransactionRawBytes), - hex(Arrays.copyOfRange( - s.getContentsDirect(), - s.getPublicKeyOffset(), - s.getPublicKeyOffset() + s.getPublicKeyLength())), - hex(Arrays.copyOfRange( - s.getContentsDirect(), - s.getSignatureOffset(), - s.getSignatureOffset() + s.getSignatureLength())), - hex(Arrays.copyOfRange( - s.getContentsDirect(), - s.getMessageOffset(), - s.getMessageOffset() + s.getMessageLength()))); - } else if (s != null - && s.getSignatureStatus() != VerificationStatus.VALID - && expectingInvalidSignature) { - expectedInvalidSignatureCount.incrementAndGet(); - } - - } catch (final InvalidProtocolBufferException ex) { - exceptionRateLimiter.handle( - ex, - (error) -> logger.error( - EXCEPTION.getMarker(), - "" + "InvalidProtocolBufferException while chekcing signature", - error)); - } - } - - //////////// start timing///////////// - final long startTime = System.nanoTime(); - validateTimestamp(timestamp); - lastTranTimeStamp = System.currentTimeMillis(); - - final Optional testTransaction = unpackTransaction(trans); - if (testTransaction.isEmpty()) { - return; - } - final long splitTime1 = System.nanoTime(); - // omit entityExpiration from handleTransaction timing - purgeExpiredRecordsIfNeeded(testTransaction.get(), timestamp); - final long splitTime2 = System.nanoTime(); - - logIfFirstTransaction(id); - - // Handle based on transaction type - switch (testTransaction.get().getBodyCase()) { - case BYTESTRANSACTION: - handleBytesTransaction(testTransaction.get(), id); - break; - case FCMTRANSACTION: - handleFCMTransaction(testTransaction.get(), id, timestamp, checkIfSignatureIsInvalid(trans)); - break; - case CONTROLTRANSACTION: - handleControlTransaction(testTransaction.get(), id, timestamp); - break; - case FREEZETRANSACTION: - handleFreezeTransaction(testTransaction.get(), platformState); - break; - case SIMPLEACTION: - handleSimpleAction(testTransaction.get().getSimpleAction()); - break; - case VIRTUALMERKLETRANSACTION: - handleVirtualMerkleTransaction(testTransaction.get().getVirtualMerkleTransaction(), id, timeCreated); - break; - default: - logger.error(EXCEPTION.getMarker(), "Unrecognized transaction!"); - } - - //////////// end timing///////////// - final long htNetTime = System.nanoTime() - splitTime2 + (splitTime1 - startTime); - if (testTransaction.get().hasFcmTransaction()) { - final FCMTransaction fcmTransaction = testTransaction.get().getFcmTransaction(); - if (!fcmTransaction.hasActivity() && !fcmTransaction.hasDummyTransaction()) { - switch (Objects.requireNonNull(FCMTransactionUtils.getEntityType(fcmTransaction))) { - case Crypto: - htFCMSumNano += htNetTime; - htCountFCM++; - htFCMAccounts = getFcmFamily().getMap().size(); - break; - case FCQ: - htFCQSumNano += htNetTime; - htCountFCQ++; - htFCQAccounts = getFcmFamily().getAccountFCQMap().size(); - break; - } - } - } - } - - /** - * Do initial genesis setup. - */ - private void genesisInit() { - logger.info(LOGM_STARTUP, "Set QuorumResult from genesisInit()"); - setQuorumResult(new QuorumResult<>(platform.getRoster().rosterEntries().size())); - - setIssLeaf(new IssLeaf()); - } - - /** - * {@inheritDoc} - */ - @Override - public void init( - @NonNull final Platform platform, - @NonNull final InitTrigger trigger, - @Nullable final SoftwareVersion previousSoftwareVersion) { - if (trigger == InitTrigger.RESTART) { - rebuildExpectedMapFromState(Instant.EPOCH, true); - rebuildExpirationQueue(); - } - - this.platform = platform; - UnsafeMutablePTTStateAccessor.getInstance().setMutableState(platform.getSelfId(), this); - - initialized.set(true); - - TransactionSubmitter.setForcePauseCanSubmitMore(new AtomicBoolean(false)); - - // If parameter exists, load PayloadCfgSimple from top level json configuration file - // Otherwise, load the default setting - final String[] parameters = ParameterProvider.getInstance().getParameters(); - if (parameters != null && parameters.length > 0) { - final String jsonFileName = parameters[0]; - final PayloadCfgSimple payloadCfgSimple = PlatformTestingToolMain.getPayloadCfgSimple(jsonFileName); - setConfig(payloadCfgSimple); - } else { - setConfig(new PayloadCfgSimple()); - } - - expectedFCMFamily.setNodeId(platform.getSelfId().id()); - expectedFCMFamily.setWeightedNodeNum(RosterUtils.getNumberWithWeight(platform.getRoster())); - - // initialize data structures used for FCQueue transaction records expiration - initializeExpirationQueueAndAccountsSet(); - logger.info(LOGM_STARTUP, () -> new SoftwareVersionPayload( - "Trigger and PreviousSoftwareVersion state received in init function", - trigger.toString(), - Objects.toString(previousSoftwareVersion)) - .toString()); - - if (trigger == InitTrigger.GENESIS) { - genesisInit(); - } - this.invalidateHash(); - FAKE_MERKLE_STATE_LIFECYCLES.initStates(this); - - // compute hash - try { - platform.getContext().getMerkleCryptography().digestTreeAsync(this).get(); - } catch (final ExecutionException e) { - logger.error(EXCEPTION.getMarker(), "Exception occurred during hashing", e); - } catch (final InterruptedException e) { - logger.error(EXCEPTION.getMarker(), "Interrupted while hashing state. Expect buggy behavior."); - Thread.currentThread().interrupt(); - } - } - - private MessageDigest createKeccakDigest() { - MessageDigest digest; - try { - digest = MessageDigest.getInstance("KECCAK-256"); - } catch (NoSuchAlgorithmException ignored) { - try { - digest = MessageDigest.getInstance("SHA3-256"); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException(e); - } - } - - return digest; - } - - private byte[] keccak256(final byte[] bytes) { - final MessageDigest keccakDigest = createKeccakDigest(); - keccakDigest.update(bytes); - return keccakDigest.digest(); - } - - private void expandSignatures(final Transaction trans) { - if (getConfig().isAppendSig()) { - try { - final byte[] payloadBytes = trans.getApplicationTransaction().toByteArray(); - final TestTransactionWrapper testTransactionWrapper = TestTransactionWrapper.parseFrom(payloadBytes); - final byte[] testTransactionRawBytes = - testTransactionWrapper.getTestTransactionRawBytes().toByteArray(); - final byte[] publicKey = - testTransactionWrapper.getPublicKeyRawBytes().toByteArray(); - final byte[] signature = - testTransactionWrapper.getSignaturesRawBytes().toByteArray(); - final AppTransactionSignatureType AppSignatureType = testTransactionWrapper.getSignatureType(); - - final SignatureType signatureType; - byte[] signaturePayload = testTransactionRawBytes; - - if (AppSignatureType == AppTransactionSignatureType.ED25519) { - signatureType = SignatureType.ED25519; - } else if (AppSignatureType == AppTransactionSignatureType.ECDSA_SECP256K1) { - signatureType = SignatureType.ECDSA_SECP256K1; - signaturePayload = keccak256(testTransactionRawBytes); - } else if (AppSignatureType == AppTransactionSignatureType.RSA) { - signatureType = SignatureType.RSA; - } else { - throw new UnsupportedOperationException("Unknown application signature type " + AppSignatureType); - } - - final int msgLen = signaturePayload.length; - final int sigOffset = msgLen + publicKey.length; - - // concatenate payload with public key and signature - final byte[] contents = ByteBuffer.allocate( - signaturePayload.length + publicKey.length + signature.length) - .put(signaturePayload) - .put(publicKey) - .put(signature) - .array(); - - final TransactionSignature transactionSignature = new TransactionSignature( - contents, sigOffset, signature.length, msgLen, publicKey.length, 0, msgLen, signatureType); - trans.setMetadata(transactionSignature); - - CryptographyHolder.get().verifySync(List.of(transactionSignature)); - - } catch (final InvalidProtocolBufferException ex) { - exceptionRateLimiter.handle( - ex, (error) -> logger.error(EXCEPTION.getMarker(), "InvalidProtocolBufferException", error)); - } - } - } - - /** - * Report current progress, if currentAmount equals multiple times of markerPercentage of expectedAmount For example - * if expectedAmount = 88, markerPercentage = 20, the progress will be reported if current progress percentage is - * 20%, 40%, 60%, 80%, 100%. Accordingly, currentAmount equals to 17, 35, 52, 70, and 88 - *

    - * If markerPercentage is 15, then should report progress at 15%, 30%, 45%, 60%, 75%, 90% and 100% - */ - private void logProgress( - @NonNull final NodeId id, - final int markerPercentage, - @NonNull final PAYLOAD_TYPE type, - final long expectedAmount, - final long currentAmount) { - if (markerPercentage != 0 && currentAmount != 0 && expectedAmount != 0) { - if (currentAmount == 1) { - logger.info(LOGM_DEMO_INFO, "PlatformTestingDemo id {} HANDLE {} START", id, type); - } else if (currentAmount == expectedAmount) { - logger.info(LOGM_DEMO_INFO, "PlatformTestingDemo id {} HANDLE {} END", id, type); - } else { - final int reportTimes = (int) Math.ceil(((double) 100) / markerPercentage); - for (int i = 1; i <= reportTimes; i++) { // check currentAmount match which marker value - final int percentage = Math.min(100, i * markerPercentage); - final long reportNumber = expectedAmount * percentage / 100; - if (currentAmount == reportNumber) { - logger.info( - LOGM_DEMO_INFO, - "PlatformTestingDemo id {} HANDLE {} {}% currentAmount {} ", - id, - type, - percentage, - currentAmount); - } - } - } - } - } - - /** - * Validate signatures when appendSig is true, if signature status is INVALID set invalidSig flag - * - * @param trans Transaction whose signatures needs to be validated - * @return boolean that shows if the signature status is INVALID - */ - private static boolean validateSignatures(final Transaction trans) { - // Verify signatures if appendSig is true - boolean invalidSig = false; - final TransactionSignature signature = trans.getMetadata(); - if (signature != null) { - if (VerificationStatus.UNKNOWN.equals(signature.getSignatureStatus())) { - try { - final Future future = signature.waitForFuture(); - future.get(); - } catch (final ExecutionException | InterruptedException ex) { - logger.info(EXCEPTION.getMarker(), "Error when verifying signature", ex); - } - } - if (VerificationStatus.INVALID.equals(signature.getSignatureStatus())) { - invalidSig = true; - } - } - return invalidSig; - } - /** * after reconnect/restart, rebuild ExpectedMap from state * @@ -1522,33 +481,7 @@ public synchronized void rebuild() { getReferenceNftLedger().reload(getNftLedger()); } - /** - * Remove expired records from FCQs - * - * @param consensusCurrentTimestamp transaction records whose expiration time is below or equal to this consensus - * time will be removed - * @return number of removed records - */ - public long purgeExpiredRecords(final long consensusCurrentTimestamp) { - lastPurgeTimestamp = consensusCurrentTimestamp; - - final long startTime = System.nanoTime(); - final long removedNum = removeExpiredRecordsInExpirationQueue(consensusCurrentTimestamp); - - if (removedNum > 0) { - final long timeTakenMicro = (System.nanoTime() - startTime) / MICROSECONDS_TO_NANOSECONDS; - logger.info( - LOGM_EXPIRATION, - "Finish removing expired records from FCQs. Has removed: {} in {} ms", - removedNum, - timeTakenMicro); - htCountExpiration += removedNum; - htFCQExpirationSumMicro += timeTakenMicro; - } - return removedNum; - } - - private long removeExpiredRecordsInExpirationQueue(final long consensusCurrentTimestamp) { + long removeExpiredRecordsInExpirationQueue(final long consensusCurrentTimestamp) { long removedNumOfRecords = 0; while (!expirationQueue.isEmpty() && (expirationQueue.peek().getEarliestExpiry() <= consensusCurrentTimestamp)) { @@ -1563,7 +496,6 @@ private long removeExpiredRecordsInExpirationQueue(final long consensusCurrentTi fcmFamily, expectedFCMFamily); } - htFCQRecordsCount = expirationQueue.size(); return removedNumOfRecords; } @@ -1576,7 +508,7 @@ public void rebuildExpirationQueue() { logger.error(EXCEPTION.getMarker(), "FCMFamily is null, so could not rebuild Expiration Queue"); return; } - if (fcmFamily.getAccountFCQMap().size() == 0) { + if (fcmFamily.getAccountFCQMap().isEmpty()) { return; } expirationQueue = new PriorityBlockingQueue<>(); @@ -1656,11 +588,4 @@ private static class ChildIndices { public static final int CHILD_COUNT = 13; } - - @Override - public void preHandle( - @NonNull final Event event, - @NonNull final Consumer> stateSignatureTransaction) { - event.forEachTransaction(this::preHandleTransaction); - } } diff --git a/platform-sdk/platform-apps/tests/PlatformTestingTool/src/main/java/com/swirlds/demo/platform/PlatformTestingToolStateLifecycles.java b/platform-sdk/platform-apps/tests/PlatformTestingTool/src/main/java/com/swirlds/demo/platform/PlatformTestingToolStateLifecycles.java new file mode 100644 index 000000000000..baca7ca948e8 --- /dev/null +++ b/platform-sdk/platform-apps/tests/PlatformTestingTool/src/main/java/com/swirlds/demo/platform/PlatformTestingToolStateLifecycles.java @@ -0,0 +1,1186 @@ +/* + * 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.swirlds.demo.platform; + +import static com.swirlds.base.units.UnitConstants.MICROSECONDS_TO_NANOSECONDS; +import static com.swirlds.base.units.UnitConstants.NANOSECONDS_TO_MICROSECONDS; +import static com.swirlds.common.utility.CommonUtils.hex; +import static com.swirlds.demo.platform.fs.stresstest.proto.TestTransaction.BodyCase.FCMTRANSACTION; +import static com.swirlds.logging.legacy.LogMarker.DEMO_INFO; +import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; +import static com.swirlds.logging.legacy.LogMarker.TESTING_EXCEPTIONS_ACCEPTABLE_RECONNECT; +import static com.swirlds.merkle.test.fixtures.map.lifecycle.SaveExpectedMapHandler.STORAGE_DIRECTORY; +import static com.swirlds.merkle.test.fixtures.map.lifecycle.SaveExpectedMapHandler.createExpectedMapName; +import static com.swirlds.merkle.test.fixtures.map.lifecycle.SaveExpectedMapHandler.serialize; +import static com.swirlds.metrics.api.FloatFormats.FORMAT_11_0; +import static com.swirlds.platform.test.fixtures.state.FakeStateLifecycles.FAKE_MERKLE_STATE_LIFECYCLES; + +import com.google.protobuf.InvalidProtocolBufferException; +import com.hedera.hapi.platform.event.StateSignatureTransaction; +import com.swirlds.common.context.PlatformContext; +import com.swirlds.common.crypto.CryptographyHolder; +import com.swirlds.common.crypto.SignatureType; +import com.swirlds.common.crypto.TransactionSignature; +import com.swirlds.common.crypto.VerificationStatus; +import com.swirlds.common.metrics.RunningAverageMetric; +import com.swirlds.common.platform.NodeId; +import com.swirlds.common.utility.ThresholdLimitingHandler; +import com.swirlds.demo.merkle.map.FCMTransactionHandler; +import com.swirlds.demo.merkle.map.FCMTransactionUtils; +import com.swirlds.demo.merkle.map.internal.ExpectedFCMFamily; +import com.swirlds.demo.platform.actions.Action; +import com.swirlds.demo.platform.actions.QuorumResult; +import com.swirlds.demo.platform.actions.QuorumTriggeredAction; +import com.swirlds.demo.platform.freeze.FreezeTransactionHandler; +import com.swirlds.demo.platform.fs.stresstest.proto.Activity; +import com.swirlds.demo.platform.fs.stresstest.proto.AppTransactionSignatureType; +import com.swirlds.demo.platform.fs.stresstest.proto.ControlTransaction; +import com.swirlds.demo.platform.fs.stresstest.proto.ControlType; +import com.swirlds.demo.platform.fs.stresstest.proto.FCMTransaction; +import com.swirlds.demo.platform.fs.stresstest.proto.FreezeTransaction; +import com.swirlds.demo.platform.fs.stresstest.proto.RandomBytesTransaction; +import com.swirlds.demo.platform.fs.stresstest.proto.SimpleAction; +import com.swirlds.demo.platform.fs.stresstest.proto.TestTransaction; +import com.swirlds.demo.platform.fs.stresstest.proto.TestTransactionWrapper; +import com.swirlds.demo.platform.fs.stresstest.proto.VirtualMerkleTransaction; +import com.swirlds.demo.platform.iss.IssLeaf; +import com.swirlds.demo.platform.nft.NftLedgerStatistics; +import com.swirlds.demo.virtualmerkle.transaction.handler.VirtualMerkleTransactionHandler; +import com.swirlds.logging.legacy.payload.SoftwareVersionPayload; +import com.swirlds.merkle.test.fixtures.map.lifecycle.EntityType; +import com.swirlds.merkle.test.fixtures.map.lifecycle.TransactionState; +import com.swirlds.merkle.test.fixtures.map.lifecycle.TransactionType; +import com.swirlds.merkle.test.fixtures.map.pta.MapKey; +import com.swirlds.platform.ParameterProvider; +import com.swirlds.platform.Utilities; +import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; +import com.swirlds.platform.roster.RosterUtils; +import com.swirlds.platform.state.PlatformStateModifier; +import com.swirlds.platform.state.StateLifecycles; +import com.swirlds.platform.system.InitTrigger; +import com.swirlds.platform.system.Platform; +import com.swirlds.platform.system.Round; +import com.swirlds.platform.system.SoftwareVersion; +import com.swirlds.platform.system.address.AddressBook; +import com.swirlds.platform.system.events.ConsensusEvent; +import com.swirlds.platform.system.events.Event; +import com.swirlds.platform.system.transaction.ConsensusTransaction; +import com.swirlds.platform.system.transaction.Transaction; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.io.File; +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.MarkerManager; + +public class PlatformTestingToolStateLifecycles implements StateLifecycles { + + private static final Logger logger = LogManager.getLogger(PlatformTestingToolState.class); + private static final Marker LOGM_DEMO_INFO = MarkerManager.getMarker("DEMO_INFO"); + private static final Marker LOGM_EXPIRATION = MarkerManager.getMarker("EXPIRATION"); + private static final Marker LOGM_STARTUP = MarkerManager.getMarker("STARTUP"); + private static final long EXCEPTION_RATE_THRESHOLD = 10; + + static final String STAT_TIMER_THREAD_NAME = "stat timer PTTState"; + + /** + * Defined in settings. the consensus timestamp of a transaction is guaranteed to be at least this many nanoseconds + * later than that of the transaction immediately before it in consensus order, and to be a multiple of this + */ + private static final long minTransTimestampIncrNanos = 1_000; + + /** + * statistics for handleTransaction + */ + private static final String HANDLE_TRANSACTION_CATEGORY = "HandleTransaction"; + + private static long htCountFCM; + private static long htFCMSumNano; + private static final RunningAverageMetric.Config HT_FCM_MICRO_SEC_CONFIG = new RunningAverageMetric.Config( + HANDLE_TRANSACTION_CATEGORY, "htFCMTimeMicroSec") + .withDescription("average handleTransaction (FCM) Time, microseconds"); + private static RunningAverageMetric htFCMMicroSec; + private static long htCountFCQ; + private static long htFCQSumNano; + + private static final RunningAverageMetric.Config HT_FCQ_MICRO_SEC_CONFIG = new RunningAverageMetric.Config( + HANDLE_TRANSACTION_CATEGORY, "htFCQTimeMicroSec") + .withDescription("average handleTransaction (FCQ) Time, microseconds"); + private static RunningAverageMetric htFCQMicroSec; + + /** + * Has init() been called on this copy or an ancestor copy of this object? + */ + private final AtomicBoolean initialized = new AtomicBoolean(false); + + private static long htCountExpiration; + private static long htFCQExpirationSumMicro; + + private static final RunningAverageMetric.Config HT_FCQ_EXPIRATION_MICRO_SEC_CONFIG = + new RunningAverageMetric.Config(HANDLE_TRANSACTION_CATEGORY, "htFCQExpirationMicroSec") + .withDescription("FCQ Expiration Time per call, microseconds"); + private static RunningAverageMetric htFCQExpirationMicroSec; + + private static final RunningAverageMetric.Config HT_FCM_SIZE_CONFIG = new RunningAverageMetric.Config( + HANDLE_TRANSACTION_CATEGORY, "htFCMSize") + .withDescription("FCM Tree Size (accounts)") + .withFormat(FORMAT_11_0); + private static RunningAverageMetric htFCMSize; + + /////////////////////////////////////////// + // Transaction Handlers + private static long htFCMAccounts; + + private static final RunningAverageMetric.Config HT_FCQ_SIZE_CONFIG = new RunningAverageMetric.Config( + HANDLE_TRANSACTION_CATEGORY, "htFCQSize") + .withDescription("FCQ Tree Size (accounts)") + .withFormat(FORMAT_11_0); + private static RunningAverageMetric htFCQSize; + + private static long htFCQAccounts; + + private static final RunningAverageMetric.Config HT_FCQ_RECORDS_CONFIG = new RunningAverageMetric.Config( + HANDLE_TRANSACTION_CATEGORY, "htFCQRecords") + .withDescription("FCQ Transaction Records") + .withFormat(FORMAT_11_0); + private static RunningAverageMetric htFCQRecords; + + private static long htFCQRecordsCount; + /////////////////////////////////////////// + // Non copyable shared variables + private Platform platform; + /////////////////////////////////////////// + // Copyable variables + private ThresholdLimitingHandler exceptionRateLimiter; + private ProgressCfg progressCfg; + /////////////////////////////////////////// + // Variables not used for state copyTo + protected long roundCounter = 0; + + // last timestamp purging records + private long lastPurgeTimestamp = 0; + /** + * The instant of the previously handled transaction. Null if no transactions have yet been handled by this state + * instance. Used to verify that each transaction happens at a later instant than its predecessor. + */ + private Instant previousTimestamp; + /** + * Handles quorum determinations for all {@link ControlTransaction} processed by the handle method. + */ + private QuorumTriggeredAction controlQuorum; + + /** + * startup any statistics + * + * @param platform Platform + */ + public static void initStatistics(final Platform platform) { + if (htFCMMicroSec != null) { + return; + } + + /* Add handleTransaction statistics */ + htFCMMicroSec = platform.getContext().getMetrics().getOrCreate(HT_FCM_MICRO_SEC_CONFIG); + htFCQMicroSec = platform.getContext().getMetrics().getOrCreate(HT_FCQ_MICRO_SEC_CONFIG); + htFCQExpirationMicroSec = platform.getContext().getMetrics().getOrCreate(HT_FCQ_EXPIRATION_MICRO_SEC_CONFIG); + htFCMSize = platform.getContext().getMetrics().getOrCreate(HT_FCM_SIZE_CONFIG); + htFCQSize = platform.getContext().getMetrics().getOrCreate(HT_FCQ_SIZE_CONFIG); + htFCQRecords = platform.getContext().getMetrics().getOrCreate(HT_FCQ_RECORDS_CONFIG); + + NftLedgerStatistics.register(platform); + + // timer to update output stats + final int SAMPLING_PERIOD = 5000; // millisecond + final Timer statTimer = new Timer(STAT_TIMER_THREAD_NAME, true); + statTimer.schedule( + new TimerTask() { + @Override + public void run() { + getCurrentTransactionStat(); + } + }, + 0, + SAMPLING_PERIOD); + } + + private static void getCurrentTransactionStat() { + if (htFCMMicroSec == null) { + return; + } + + // fcm, nsec + if (htCountFCM > 0) { + htFCMMicroSec.update((double) htFCMSumNano / (double) htCountFCM * NANOSECONDS_TO_MICROSECONDS); + } else { + htFCMMicroSec.update(0); + } + htFCMSumNano = 0; + htCountFCM = 0; + // fcq, nsec + if (htCountFCQ > 0) { + htFCQMicroSec.update((double) htFCQSumNano / (double) htCountFCQ * NANOSECONDS_TO_MICROSECONDS); + } else { + htFCQMicroSec.update(0); + } + htFCQSumNano = 0; + htCountFCQ = 0; + // expiration, in microseconds + if (htCountExpiration > 0) { + htFCQExpirationMicroSec.update((double) htFCQExpirationSumMicro / (double) htCountExpiration); + } else { + htFCQExpirationMicroSec.update(0); + } + htFCQExpirationSumMicro = 0; + htCountExpiration = 0; + + // datastructure sizes + htFCMSize.update(htFCMAccounts); + htFCQSize.update(htFCQAccounts); + htFCQRecords.update(htFCQRecordsCount); + } + + // count invalid signature ratio + static AtomicLong totalTransactionSignatureCount = new AtomicLong(0); + static AtomicLong expectedInvalidSignatureCount = new AtomicLong(0); + + QuorumTriggeredAction getControlQuorum() { + return controlQuorum; + } + + void initControlStructures(final Action action) { + final int nodeIndex = + RosterUtils.getIndex(platform.getRoster(), platform.getSelfId().id()); + this.controlQuorum = new QuorumTriggeredAction<>( + () -> nodeIndex, + () -> platform.getRoster().rosterEntries().size(), + () -> RosterUtils.getNumberWithWeight(platform.getRoster()), + action); + + this.exceptionRateLimiter = new ThresholdLimitingHandler<>(EXCEPTION_RATE_THRESHOLD); + } + + /** + * Make sure that the timestamp for the submitted transaction is not earlier than (the timestamp for the previous + * transaction + minTransTimestampIncrNanos). + */ + private void validateTimestamp(final Instant timestamp) { + if (previousTimestamp != null) { + final Instant previousTransTimestampPlusIncr = previousTimestamp.plusNanos(minTransTimestampIncrNanos); + if (timestamp.isBefore(previousTransTimestampPlusIncr)) { + logger.error( + EXCEPTION.getMarker(), + "Transaction has timestamp {} which is earlier than previous timestamp {} plus {} nanos: " + + "{}", + timestamp, + previousTimestamp, + minTransTimestampIncrNanos, + previousTransTimestampPlusIncr); + } + } + previousTimestamp = timestamp; + } + + /** + * Check if the signatures on the transaction are invalid. + * + * @return true if the signature is invalid + */ + private boolean checkIfSignatureIsInvalid(final Transaction trans, @NonNull final PlatformTestingToolState state) { + if (state.getConfig().isAppendSig()) { + return validateSignatures(trans); + } + return false; + } + + /** + * If configured, delay this transaction. + */ + private void delay(@NonNull final PlatformTestingToolState state) { + if (state.getConfig().getDelayCfg() != null) { + final int delay = state.getConfig().getDelayCfg().getRandomDelay(); + try { + Thread.sleep(delay); + } catch (final InterruptedException e) { + logger.info(LOGM_DEMO_INFO, "", e); + } + } + SyntheticBottleneckConfig.getActiveConfig() + .throttleIfNeeded(platform.getSelfId().id()); + } + + /** + * Instantiate TestTransaction object from transaction byte array. + * + * @return the instantiated transaction if no errors, otherwise null + */ + private Optional unpackTransaction( + final Transaction trans, @NonNull final PlatformTestingToolState state) { + try { + final byte[] payloadBytes = trans.getApplicationTransaction().toByteArray(); + if (state.getConfig().isAppendSig()) { + final byte[] testTransactionRawBytes = TestTransactionWrapper.parseFrom(payloadBytes) + .getTestTransactionRawBytes() + .toByteArray(); + return Optional.of(TestTransaction.parseFrom(testTransactionRawBytes)); + } else { + return Optional.of(TestTransaction.parseFrom(payloadBytes)); + } + } catch (final InvalidProtocolBufferException ex) { + exceptionRateLimiter.handle( + ex, (error) -> logger.error(EXCEPTION.getMarker(), "InvalidProtocolBufferException", error)); + return Optional.empty(); + } + } + + /** + * If configured to do so, purge expired records as needed. + */ + private void purgeExpiredRecordsIfNeeded( + final TestTransaction testTransaction, + final Instant timestamp, + @NonNull final PlatformTestingToolState state) { + if (!testTransaction.hasControlTransaction() + || testTransaction.getControlTransaction().getType() != ControlType.EXIT_VALIDATION) { + + if (!state.getFcmFamily().getAccountFCQMap().isEmpty()) { + // remove expired records from FCQs before handling transaction if accountFCQMap has any entities + try { + purgeExpiredRecords(state, timestamp.getEpochSecond()); + } catch (final Throwable ex) { + exceptionRateLimiter.handle( + ex, + (error) -> logger.error(EXCEPTION.getMarker(), "Failed to purge expired records", error)); + } + } + } + } + + /** + * Write a special log message if this is the first transaction and total fcm and any file statistics are all + * zeroes. + */ + private void logIfFirstTransaction(final NodeId id, @NonNull final PlatformTestingToolState state) { + final int nodeIndex = RosterUtils.getIndex(platform.getRoster(), id.id()); + if (progressCfg != null + && progressCfg.getProgressMarker() > 0 + && state.getTransactionCounter().get(nodeIndex).getAllTransactionAmount() == 0) { + logger.info(LOGM_DEMO_INFO, "PlatformTestingDemo HANDLE ALL START"); + } + } + + /** + * Handle the random bytes transaction type. + */ + private void handleBytesTransaction( + @NonNull final TestTransaction testTransaction, + @NonNull final NodeId id, + @NonNull final PlatformTestingToolState state) { + Objects.requireNonNull(testTransaction, "testTransaction must not be null"); + Objects.requireNonNull(id, "id must not be null"); + final int nodeIndex = RosterUtils.getIndex(platform.getRoster(), id.id()); + final RandomBytesTransaction bytesTransaction = testTransaction.getBytesTransaction(); + if (bytesTransaction.getIsInserSeq()) { + final long seq = Utilities.toLong(bytesTransaction.getData().toByteArray()); + if (state.getNextSeqCons().get(nodeIndex).getValue() != seq) { + logger.error( + EXCEPTION.getMarker(), + platform.getSelfId() + " error, new (id=" + id + + ") seq should be " + state.getNextSeqCons().get(nodeIndex) + + " but is " + seq); + } + state.getNextSeqCons().get(nodeIndex).getAndIncrement(); + } + } + + /** + * Handle the Virtual Merkle transaction type. + */ + private void handleVirtualMerkleTransaction( + @NonNull final VirtualMerkleTransaction virtualMerkleTransaction, + @NonNull final NodeId id, + @NonNull final Instant consensusTimestamp, + @NonNull final PlatformTestingToolState state) { + Objects.requireNonNull(virtualMerkleTransaction, "virtualMerkleTransaction must not be null"); + Objects.requireNonNull(id, "id must not be null"); + Objects.requireNonNull(consensusTimestamp, "consensusTimestamp must not be null"); + final int nodeIndex = RosterUtils.getIndex(platform.getRoster(), id.id()); + VirtualMerkleTransactionHandler.handle( + consensusTimestamp, + virtualMerkleTransaction, + state.getStateExpectedMap(), + state.getVirtualMap(), + state.getVirtualMapForSmartContracts(), + state.getVirtualMapForSmartContractsByteCode()); + + if (virtualMerkleTransaction.hasCreateAccount()) { + state.getTransactionCounter().get(nodeIndex).vmCreateAmount++; + } else if (virtualMerkleTransaction.hasUpdateAccount()) { + state.getTransactionCounter().get(nodeIndex).vmUpdateAmount++; + } else if (virtualMerkleTransaction.hasDeleteAccount()) { + state.getTransactionCounter().get(nodeIndex).vmDeleteAmount++; + } else if (virtualMerkleTransaction.hasSmartContract()) { + state.getTransactionCounter().get(nodeIndex).vmContractCreateAmount++; + } else if (virtualMerkleTransaction.hasMethodExecution()) { + state.getTransactionCounter().get(nodeIndex).vmContractExecutionAmount++; + } + } + + /** + * Handle the FCM transaction type. + */ + private void handleFCMTransaction( + @NonNull final TestTransaction testTransaction, + @NonNull final NodeId id, + @NonNull final Instant timestamp, + final boolean invalidSig, + @NonNull final PlatformTestingToolState state) { + Objects.requireNonNull(testTransaction, "testTransaction must not be null"); + Objects.requireNonNull(id, "id must not be null"); + Objects.requireNonNull(timestamp, "timestamp must not be null"); + + final int nodeIndex = RosterUtils.getIndex(platform.getRoster(), id.id()); + final FCMTransaction fcmTransaction = testTransaction.getFcmTransaction(); + final ExpectedFCMFamily expectedFCMFamily = state.getStateExpectedMap(); + + // Handle Activity transaction, which doesn't affect any entity's lifecyle + if (fcmTransaction.hasActivity()) { + final Activity.ActivityType activityType = + fcmTransaction.getActivity().getType(); + if (nodeIndex == 0 && activityType == Activity.ActivityType.SAVE_EXPECTED_MAP) { + // Serialize ExpectedMap to disk in JSON format + TransactionSubmitter.setForcePauseCanSubmitMore(new AtomicBoolean(true)); + serialize( + expectedFCMFamily.getExpectedMap(), + new File(STORAGE_DIRECTORY), + createExpectedMapName(platform.getSelfId().id(), timestamp), + false); + TransactionSubmitter.setForcePauseCanSubmitMore(new AtomicBoolean(false)); + logger.info(LOGM_DEMO_INFO, "handling SAVE_EXPECTED_MAP"); + + } else if (activityType == Activity.ActivityType.SAVE_EXPECTED_MAP) { + logger.info(LOGM_DEMO_INFO, "Received SAVE_EXPECTED_MAP transaction from node {}", id); + } else { + logger.info(EXCEPTION.getMarker(), "unknown Activity type"); + } + return; + } + + if (fcmTransaction.hasDummyTransaction()) { + return; + } + + // Extract MapKeys, TransactionType, and EntityType from FCMTransaction + // which might affect entity's lifecycle status + final List keys = FCMTransactionUtils.getMapKeys(fcmTransaction); + final TransactionType transactionType = FCMTransactionUtils.getTransactionType(fcmTransaction); + final EntityType entityType = FCMTransactionUtils.getEntityType(fcmTransaction); + final long epochMillis = timestamp.toEpochMilli(); + + final long originId = fcmTransaction.getOriginNode(); + + if ((keys.isEmpty() && entityType != EntityType.NFT) || transactionType == null || entityType == null) { + logger.error( + EXCEPTION.getMarker(), + "Invalid Transaction: keys: {}, transactionType: {}, entityType: {}", + keys, + transactionType, + entityType); + return; + } + + // if there is any error, we don't handle this transaction + if (!expectedFCMFamily.shouldHandleForKeys( + keys, transactionType, state.getConfig(), entityType, epochMillis, originId) + && entityType != EntityType.NFT) { + logger.info(DEMO_INFO.getMarker(), "A transaction ignored by expected map: {}.", roundCounter); + return; + } + + // If signature verification result doesn't match expected result + // which is set when generating this FCMTransaction, it denotes an error + if (state.getConfig().isAppendSig() && invalidSig != fcmTransaction.getInvalidSig()) { + logger.error( + EXCEPTION.getMarker(), + "Unexpected signature verification result: " + "actual: {}; expected:{}", + () -> (invalidSig ? "INVALID" : "VALID"), + () -> (fcmTransaction.getInvalidSig() ? "INVALID" : "VALID")); + + // if the entity doesn't exist, put it to expectedMap; + // set its ExpectedValues's isErrored to be true; + // set its latestHandledStatus to be INVALID_SIG + expectedFCMFamily.setLatestHandledStatusForKey( + keys.getFirst(), + entityType, + null, + TransactionState.INVALID_SIG, + transactionType, + epochMillis, + originId, + true); + return; + } + + // for expected invalid sig, + // set expectedValue's latestHandledStatus to be EXPECTED_INVALID_SIG + // and handle this transaction + if (invalidSig) { + expectedFCMFamily.setLatestHandledStatusForKey( + keys.getFirst(), + entityType, + null, + TransactionState.EXPECTED_INVALID_SIG, + transactionType, + epochMillis, + originId, + false); + } + + // Handle the transaction and set latestHandledStatus + try { + FCMTransactionHandler.performOperation( + fcmTransaction, + state, + expectedFCMFamily, + originId, + epochMillis, + entityType, + timestamp.getEpochSecond() + state.getConfig().getFcqTtl(), + state.getExpirationQueue(), + state.getAccountsWithExpiringRecords()); + } catch (final Exception ex) { + exceptionRateLimiter.handle( + ex, + (error) -> logger.error( + EXCEPTION.getMarker(), + "Exceptions while handling transaction: {} {} for {}, originId:{}", + transactionType, + entityType, + keys, + originId, + error)); + + // if the entity doesn't exist, put it to expectedMap; + // set its ExpectedValues's isErrored to be true; + // set its latestHandledStatus to be HANDLE_FAILED + for (final MapKey key : keys) { + expectedFCMFamily.setLatestHandledStatusForKey( + key, + entityType, + null, + TransactionState.HANDLE_FAILED, + transactionType, + timestamp.toEpochMilli(), + originId, + true); + } + } + if (fcmTransaction.hasCreateAccount()) { + state.getTransactionCounter().get(nodeIndex).fcmCreateAmount++; + if (progressCfg != null) { + logProgress( + id, + progressCfg.getProgressMarker(), + PAYLOAD_TYPE.TYPE_FCM_CREATE, + progressCfg.getExpectedFCMCreateAmount(), + state.getTransactionCounter().get(nodeIndex).fcmCreateAmount); + } + } else if (fcmTransaction.hasTransferBalance()) { + state.getTransactionCounter().get(nodeIndex).fcmTransferAmount++; + if (progressCfg != null) { + logProgress( + id, + progressCfg.getProgressMarker(), + PAYLOAD_TYPE.TYPE_FCM_TRANSFER, + progressCfg.getExpectedFCMTransferAmount(), + state.getTransactionCounter().get(nodeIndex).fcmTransferAmount); + } + } else if (fcmTransaction.hasDeleteAccount()) { + state.getTransactionCounter().get(nodeIndex).fcmDeleteAmount++; + if (progressCfg != null) { + logProgress( + id, + progressCfg.getProgressMarker(), + PAYLOAD_TYPE.TYPE_FCM_DELETE, + progressCfg.getExpectedFCMDeleteAmount(), + state.getTransactionCounter().get(nodeIndex).fcmDeleteAmount); + } + } else if (fcmTransaction.hasUpdateAccount()) { + state.getTransactionCounter().get(nodeIndex).fcmUpdateAmount++; + if (progressCfg != null) { + logProgress( + id, + progressCfg.getProgressMarker(), + PAYLOAD_TYPE.TYPE_FCM_UPDATE, + progressCfg.getExpectedFCMUpdateAmount(), + state.getTransactionCounter().get(nodeIndex).fcmUpdateAmount); + } + } else if (fcmTransaction.hasAssortedAccount()) { + state.getTransactionCounter().get(nodeIndex).fcmAssortedAmount++; + if (progressCfg != null) { + logProgress( + id, + progressCfg.getProgressMarker(), + PAYLOAD_TYPE.TYPE_FCM_ASSORTED, + progressCfg.getExpectedFCMAssortedAmount(), + state.getTransactionCounter().get(nodeIndex).fcmAssortedAmount); + } + } else if (fcmTransaction.hasAssortedFCQ()) { + state.getTransactionCounter().get(nodeIndex).fcmFCQAssortedAmount++; + } else if (fcmTransaction.hasCreateAccountFCQ()) { + state.getTransactionCounter().get(nodeIndex).fcmFCQCreateAmount++; + } else if (fcmTransaction.hasUpdateAccountFCQ()) { + state.getTransactionCounter().get(nodeIndex).fcmFCQUpdateAmount++; + } else if (fcmTransaction.hasTransferBalanceFCQ()) { + state.getTransactionCounter().get(nodeIndex).fcmFCQTransferAmount++; + } else if (fcmTransaction.hasDeleteFCQNode()) { + state.getTransactionCounter().get(nodeIndex).fcmFCQDeleteAmount++; + } + } + + /** + * Handle the control transaction type. + */ + private void handleControlTransaction( + @NonNull final TestTransaction testTransaction, + @NonNull final NodeId id, + @NonNull final Instant timestamp, + @NonNull final PlatformTestingToolState state) { + Objects.requireNonNull(testTransaction, "testTransaction must not be null"); + Objects.requireNonNull(id, "id must not be null"); + Objects.requireNonNull(timestamp, "timestamp must not be null"); + + final long nodeIndex = RosterUtils.getIndex(platform.getRoster(), id.id()); + final ControlTransaction msg = testTransaction.getControlTransaction(); + logger.info( + DEMO_INFO.getMarker(), + "Handling Control Transaction [ originatingNodeId = {}, type = {}, consensusTimestamp = {} ]", + () -> id, + msg::getType, + () -> timestamp); + + // Must use auto reset here, otherwise if reached quorum EXIT_VALIDATION then PTT restart, QuorumResult would be + // reloaded from saved state with reaching quorum state EXIT_VALIDATION as true, + // then TransactionSubmitter won't be able to submit transaction due to some check mechanism. + controlQuorum.withAutoReset().check(nodeIndex, new ControlAction(timestamp, msg.getType())); + // updating quorum result after handling control transactions + state.setQuorumResult(controlQuorum.getQuorumResult().copy()); + } + + /** + * Handle the freeze transaction type. + */ + private void handleFreezeTransaction( + final TestTransaction testTransaction, final PlatformStateModifier platformState) { + final FreezeTransaction freezeTx = testTransaction.getFreezeTransaction(); + FreezeTransactionHandler.freeze(freezeTx, platformState); + } + + /** + * Handle a simple action transaction type + */ + private void handleSimpleAction(final SimpleAction simpleAction, final PlatformTestingToolState state) { + if (simpleAction == SimpleAction.CAUSE_ISS) { + state.getIssLeaf().setWriteRandom(true); + } + } + + private void preHandleTransaction(final Transaction transaction, final PlatformTestingToolState state) { + if (transaction.isSystem()) { + return; + } + expandSignatures(transaction, state); + } + + @Override + public void onHandleConsensusRound( + @NonNull Round round, + @NonNull PlatformTestingToolState state, + @NonNull Consumer> stateSignatureTransactionCallback) { + state.throwIfImmutable(); + if (!initialized.get()) { + throw new IllegalStateException("onHandleConsensusRound() called before init()"); + } + delay(state); + updateTransactionCounters(state); + round.forEachEventTransaction( + (event, transaction) -> handleConsensusTransaction(event, transaction, round.getRoundNum(), state)); + } + + /** + * If the size of the address book has changed, zero out the transaction counters and resize, as needed. + */ + private void updateTransactionCounters(PlatformTestingToolState state) { + if (state.getTransactionCounter() == null + || state.getTransactionCounter().size() + != platform.getRoster().rosterEntries().size()) { + state.setNextSeqCons( + new NextSeqConsList(platform.getRoster().rosterEntries().size())); + + logger.info(DEMO_INFO.getMarker(), "resetting transaction counters"); + + state.setTransactionCounter(new TransactionCounterList( + platform.getRoster().rosterEntries().size())); + for (int id = 0; id < platform.getRoster().rosterEntries().size(); id++) { + state.getTransactionCounter().add(new TransactionCounter(id)); + } + } + } + + private void handleConsensusTransaction( + final ConsensusEvent event, + final ConsensusTransaction trans, + final long roundNum, + final PlatformTestingToolState state) { + if (trans.isSystem()) { + return; + } + try { + waitForSignatureValidation(trans); + handleTransaction( + event.getCreatorId(), event.getTimeCreated(), trans.getConsensusTimestamp(), trans, state); + } catch (final InterruptedException e) { + logger.info( + TESTING_EXCEPTIONS_ACCEPTABLE_RECONNECT.getMarker(), + "onHandleConsensusRound Interrupted [ nodeId = {}, round = {} ]. " + + "This should happen only during a reconnect", + platform.getSelfId().id(), + roundNum); + Thread.currentThread().interrupt(); + } catch (final ExecutionException e) { + logger.error(EXCEPTION.getMarker(), "Exception while handling transaction", e); + } + } + + private static void waitForSignatureValidation(final ConsensusTransaction transaction) + throws InterruptedException, ExecutionException { + final TransactionSignature sig = transaction.getMetadata(); + if (sig == null) { + return; + } + final Future future = sig.waitForFuture(); + + // Block & Ignore the Void return + future.get(); + } + + private void handleTransaction( + @NonNull final NodeId id, + @NonNull final Instant timeCreated, + @NonNull final Instant timestamp, + @NonNull final ConsensusTransaction trans, + @NonNull final PlatformTestingToolState state) { + if (state.getConfig().isAppendSig()) { + try { + final TestTransactionWrapper testTransactionWrapper = TestTransactionWrapper.parseFrom( + trans.getApplicationTransaction().toByteArray()); + final byte[] testTransactionRawBytes = + testTransactionWrapper.getTestTransactionRawBytes().toByteArray(); + final byte[] publicKey = + testTransactionWrapper.getPublicKeyRawBytes().toByteArray(); + final byte[] signature = + testTransactionWrapper.getSignaturesRawBytes().toByteArray(); + + // if this is expected manually inject invalid signature + boolean expectingInvalidSignature = false; + final TestTransaction testTransaction = TestTransaction.parseFrom(testTransactionRawBytes); + if (testTransaction.getBodyCase() == FCMTRANSACTION) { + final FCMTransaction fcmTransaction = testTransaction.getFcmTransaction(); + if (fcmTransaction.getInvalidSig()) { + expectingInvalidSignature = true; + } + } + totalTransactionSignatureCount.incrementAndGet(); + final TransactionSignature s = trans.getMetadata(); + if (s != null && s.getSignatureStatus() != VerificationStatus.VALID && (!expectingInvalidSignature)) { + logger.error( + EXCEPTION.getMarker(), + "Invalid Transaction Signature [status = {}, signatureType = {}, " + + "publicKey = {}, signature = {}, data = {}, " + + "actualPublicKey = {}, actualSignature = {}, actualData = {} ]", + s.getSignatureStatus(), + s.getSignatureType(), + hex(publicKey), + hex(signature), + hex(testTransactionRawBytes), + hex(Arrays.copyOfRange( + s.getContentsDirect(), + s.getPublicKeyOffset(), + s.getPublicKeyOffset() + s.getPublicKeyLength())), + hex(Arrays.copyOfRange( + s.getContentsDirect(), + s.getSignatureOffset(), + s.getSignatureOffset() + s.getSignatureLength())), + hex(Arrays.copyOfRange( + s.getContentsDirect(), + s.getMessageOffset(), + s.getMessageOffset() + s.getMessageLength()))); + } else if (s != null + && s.getSignatureStatus() != VerificationStatus.VALID + && expectingInvalidSignature) { + expectedInvalidSignatureCount.incrementAndGet(); + } + + } catch (final InvalidProtocolBufferException ex) { + exceptionRateLimiter.handle( + ex, + (error) -> logger.error( + EXCEPTION.getMarker(), + "" + "InvalidProtocolBufferException while chekcing signature", + error)); + } + } + + //////////// start timing///////////// + final long startTime = System.nanoTime(); + validateTimestamp(timestamp); + + final Optional testTransaction = unpackTransaction(trans, state); + if (testTransaction.isEmpty()) { + return; + } + final long splitTime1 = System.nanoTime(); + // omit entityExpiration from handleTransaction timing + purgeExpiredRecordsIfNeeded(testTransaction.get(), timestamp, state); + final long splitTime2 = System.nanoTime(); + + logIfFirstTransaction(id, state); + + // Handle based on transaction type + switch (testTransaction.get().getBodyCase()) { + case BYTESTRANSACTION: + handleBytesTransaction(testTransaction.get(), id, state); + break; + case FCMTRANSACTION: + handleFCMTransaction( + testTransaction.get(), id, timestamp, checkIfSignatureIsInvalid(trans, state), state); + break; + case CONTROLTRANSACTION: + handleControlTransaction(testTransaction.get(), id, timestamp, state); + break; + case FREEZETRANSACTION: + handleFreezeTransaction(testTransaction.get(), state.getWritablePlatformState()); + break; + case SIMPLEACTION: + handleSimpleAction(testTransaction.get().getSimpleAction(), state); + break; + case VIRTUALMERKLETRANSACTION: + handleVirtualMerkleTransaction( + testTransaction.get().getVirtualMerkleTransaction(), id, timeCreated, state); + break; + default: + logger.error(EXCEPTION.getMarker(), "Unrecognized transaction!"); + } + + //////////// end timing///////////// + final long htNetTime = System.nanoTime() - splitTime2 + (splitTime1 - startTime); + if (testTransaction.get().hasFcmTransaction()) { + final FCMTransaction fcmTransaction = testTransaction.get().getFcmTransaction(); + if (!fcmTransaction.hasActivity() && !fcmTransaction.hasDummyTransaction()) { + switch (Objects.requireNonNull(FCMTransactionUtils.getEntityType(fcmTransaction))) { + case Crypto: + htFCMSumNano += htNetTime; + htCountFCM++; + htFCMAccounts = state.getFcmFamily().getMap().size(); + break; + case FCQ: + htFCQSumNano += htNetTime; + htCountFCQ++; + htFCQAccounts = state.getFcmFamily().getAccountFCQMap().size(); + break; + } + } + } + } + + /** + * Do initial genesis setup. + */ + private void genesisInit(PlatformTestingToolState state) { + logger.info(LOGM_STARTUP, "Set QuorumResult from genesisInit()"); + state.setQuorumResult( + new QuorumResult<>(platform.getRoster().rosterEntries().size())); + + state.setIssLeaf(new IssLeaf()); + } + + private MessageDigest createKeccakDigest() { + MessageDigest digest; + try { + digest = MessageDigest.getInstance("KECCAK-256"); + } catch (NoSuchAlgorithmException ignored) { + try { + digest = MessageDigest.getInstance("SHA3-256"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + } + + return digest; + } + + private byte[] keccak256(final byte[] bytes) { + final MessageDigest keccakDigest = createKeccakDigest(); + keccakDigest.update(bytes); + return keccakDigest.digest(); + } + + private void expandSignatures(final Transaction trans, PlatformTestingToolState state) { + if (state.getConfig().isAppendSig()) { + try { + final byte[] payloadBytes = trans.getApplicationTransaction().toByteArray(); + final TestTransactionWrapper testTransactionWrapper = TestTransactionWrapper.parseFrom(payloadBytes); + final byte[] testTransactionRawBytes = + testTransactionWrapper.getTestTransactionRawBytes().toByteArray(); + final byte[] publicKey = + testTransactionWrapper.getPublicKeyRawBytes().toByteArray(); + final byte[] signature = + testTransactionWrapper.getSignaturesRawBytes().toByteArray(); + final AppTransactionSignatureType AppSignatureType = testTransactionWrapper.getSignatureType(); + + final SignatureType signatureType; + byte[] signaturePayload = testTransactionRawBytes; + + if (AppSignatureType == AppTransactionSignatureType.ED25519) { + signatureType = SignatureType.ED25519; + } else if (AppSignatureType == AppTransactionSignatureType.ECDSA_SECP256K1) { + signatureType = SignatureType.ECDSA_SECP256K1; + signaturePayload = keccak256(testTransactionRawBytes); + } else if (AppSignatureType == AppTransactionSignatureType.RSA) { + signatureType = SignatureType.RSA; + } else { + throw new UnsupportedOperationException("Unknown application signature type " + AppSignatureType); + } + + final int msgLen = signaturePayload.length; + final int sigOffset = msgLen + publicKey.length; + + // concatenate payload with public key and signature + final byte[] contents = ByteBuffer.allocate( + signaturePayload.length + publicKey.length + signature.length) + .put(signaturePayload) + .put(publicKey) + .put(signature) + .array(); + + final TransactionSignature transactionSignature = new TransactionSignature( + contents, sigOffset, signature.length, msgLen, publicKey.length, 0, msgLen, signatureType); + trans.setMetadata(transactionSignature); + + CryptographyHolder.get().verifySync(List.of(transactionSignature)); + + } catch (final InvalidProtocolBufferException ex) { + exceptionRateLimiter.handle( + ex, (error) -> logger.error(EXCEPTION.getMarker(), "InvalidProtocolBufferException", error)); + } + } + } + + /** + * Report current progress, if currentAmount equals multiple times of markerPercentage of expectedAmount For example + * if expectedAmount = 88, markerPercentage = 20, the progress will be reported if current progress percentage is + * 20%, 40%, 60%, 80%, 100%. Accordingly, currentAmount equals to 17, 35, 52, 70, and 88 + *

    + * If markerPercentage is 15, then should report progress at 15%, 30%, 45%, 60%, 75%, 90% and 100% + */ + private void logProgress( + @NonNull final NodeId id, + final int markerPercentage, + @NonNull final PAYLOAD_TYPE type, + final long expectedAmount, + final long currentAmount) { + if (markerPercentage != 0 && currentAmount != 0 && expectedAmount != 0) { + if (currentAmount == 1) { + logger.info(LOGM_DEMO_INFO, "PlatformTestingDemo id {} HANDLE {} START", id, type); + } else if (currentAmount == expectedAmount) { + logger.info(LOGM_DEMO_INFO, "PlatformTestingDemo id {} HANDLE {} END", id, type); + } else { + final int reportTimes = (int) Math.ceil(((double) 100) / markerPercentage); + for (int i = 1; i <= reportTimes; i++) { // check currentAmount match which marker value + final int percentage = Math.min(100, i * markerPercentage); + final long reportNumber = expectedAmount * percentage / 100; + if (currentAmount == reportNumber) { + logger.info( + LOGM_DEMO_INFO, + "PlatformTestingDemo id {} HANDLE {} {}% currentAmount {} ", + id, + type, + percentage, + currentAmount); + } + } + } + } + } + + /** + * Validate signatures when appendSig is true, if signature status is INVALID set invalidSig flag + * + * @param trans Transaction whose signatures needs to be validated + * @return boolean that shows if the signature status is INVALID + */ + private static boolean validateSignatures(final Transaction trans) { + // Verify signatures if appendSig is true + boolean invalidSig = false; + final TransactionSignature signature = trans.getMetadata(); + if (signature != null) { + if (VerificationStatus.UNKNOWN.equals(signature.getSignatureStatus())) { + try { + final Future future = signature.waitForFuture(); + future.get(); + } catch (final ExecutionException | InterruptedException ex) { + logger.info(EXCEPTION.getMarker(), "Error when verifying signature", ex); + } + } + if (VerificationStatus.INVALID.equals(signature.getSignatureStatus())) { + invalidSig = true; + } + } + return invalidSig; + } + + /** + * Remove expired records from FCQs + * + * @param consensusCurrentTimestamp transaction records whose expiration time is below or equal to this consensus + * time will be removed + * @return number of removed records + */ + public long purgeExpiredRecords(PlatformTestingToolState state, final long consensusCurrentTimestamp) { + lastPurgeTimestamp = consensusCurrentTimestamp; + + final long startTime = System.nanoTime(); + final long removedNum = removeExpiredRecordsInExpirationQueue(state, consensusCurrentTimestamp); + + if (removedNum > 0) { + final long timeTakenMicro = (System.nanoTime() - startTime) / MICROSECONDS_TO_NANOSECONDS; + logger.info( + LOGM_EXPIRATION, + "Finish removing expired records from FCQs. Has removed: {} in {} ms", + removedNum, + timeTakenMicro); + htCountExpiration += removedNum; + htFCQExpirationSumMicro += timeTakenMicro; + } + return removedNum; + } + + private long removeExpiredRecordsInExpirationQueue( + PlatformTestingToolState state, final long consensusCurrentTimestamp) { + long removedNumOfRecords = state.removeExpiredRecordsInExpirationQueue(consensusCurrentTimestamp); + htFCQRecordsCount = state.getExpirationQueue().size(); + return removedNumOfRecords; + } + + @Override + public void onPreHandle( + @NonNull Event event, + @NonNull PlatformTestingToolState state, + @NonNull Consumer> stateSignatureTransactionCallback) { + event.forEachTransaction(v -> preHandleTransaction(v, state)); + } + + @Override + public void onStateInitialized( + @NonNull PlatformTestingToolState state, + @NonNull Platform platform, + @NonNull InitTrigger trigger, + @Nullable SoftwareVersion previousVersion) { + if (trigger == InitTrigger.RESTART) { + state.rebuildExpectedMapFromState(Instant.EPOCH, true); + state.rebuildExpirationQueue(); + } + + this.platform = platform; + UnsafeMutablePTTStateAccessor.getInstance().setMutableState(platform.getSelfId(), state); + + initialized.set(true); + + TransactionSubmitter.setForcePauseCanSubmitMore(new AtomicBoolean(false)); + + // If parameter exists, load PayloadCfgSimple from top level json configuration file + // Otherwise, load the default setting + final String[] parameters = ParameterProvider.getInstance().getParameters(); + if (parameters != null && parameters.length > 0) { + final String jsonFileName = parameters[0]; + final PayloadCfgSimple payloadCfgSimple = PlatformTestingToolMain.getPayloadCfgSimple(jsonFileName); + state.setConfig(payloadCfgSimple); + } else { + state.setConfig(new PayloadCfgSimple()); + } + + state.getStateExpectedMap().setNodeId(platform.getSelfId().id()); + state.getStateExpectedMap().setWeightedNodeNum(RosterUtils.getNumberWithWeight(platform.getRoster())); + + // initialize data structures used for FCQueue transaction records expiration + state.initializeExpirationQueueAndAccountsSet(); + logger.info(LOGM_STARTUP, () -> new SoftwareVersionPayload( + "Trigger and PreviousSoftwareVersion state received in init function", + trigger.toString(), + Objects.toString(previousVersion)) + .toString()); + + if (trigger == InitTrigger.GENESIS) { + genesisInit(state); + } + state.invalidateHash(); + FAKE_MERKLE_STATE_LIFECYCLES.initStates(state); + + // compute hash + try { + platform.getContext().getMerkleCryptography().digestTreeAsync(state).get(); + } catch (final ExecutionException e) { + logger.error(EXCEPTION.getMarker(), "Exception occurred during hashing", e); + } catch (final InterruptedException e) { + logger.error(EXCEPTION.getMarker(), "Interrupted while hashing state. Expect buggy behavior."); + Thread.currentThread().interrupt(); + } + } + + @Override + public void onSealConsensusRound(@NonNull Round round, @NonNull PlatformTestingToolState state) { + // no-op + } + + @Override + public void onUpdateWeight( + @NonNull PlatformTestingToolState state, + @NonNull AddressBook configAddressBook, + @NonNull PlatformContext context) { + // no-op + } + + @Override + public void onNewRecoveredState(@NonNull PlatformTestingToolState recoveredState) { + // no-op + } +} diff --git a/platform-sdk/platform-apps/tests/PlatformTestingTool/src/timingSensitive/java/com/swirlds/demo/merkle/map/MapValueFCQTests.java b/platform-sdk/platform-apps/tests/PlatformTestingTool/src/timingSensitive/java/com/swirlds/demo/merkle/map/MapValueFCQTests.java index 25e70c3a5676..5701721522e2 100644 --- a/platform-sdk/platform-apps/tests/PlatformTestingTool/src/timingSensitive/java/com/swirlds/demo/merkle/map/MapValueFCQTests.java +++ b/platform-sdk/platform-apps/tests/PlatformTestingTool/src/timingSensitive/java/com/swirlds/demo/merkle/map/MapValueFCQTests.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2024 Hedera Hashgraph, LLC + * Copyright (C) 2022-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. @@ -32,6 +32,7 @@ import com.swirlds.common.platform.NodeId; import com.swirlds.common.test.fixtures.io.InputOutputStream; import com.swirlds.demo.platform.PlatformTestingToolState; +import com.swirlds.demo.platform.PlatformTestingToolStateLifecycles; import com.swirlds.demo.platform.TestUtil; import com.swirlds.demo.platform.expiration.ExpirationUtils; import com.swirlds.merkle.map.MerkleMap; @@ -60,6 +61,7 @@ public class MapValueFCQTests { private static final Random RANDOM = new Random(); private static final Random random = new Random(); static PlatformTestingToolState state; + static PlatformTestingToolStateLifecycles lifecycles; private static MerkleCryptography cryptography; private static MapValueFCQ mapValueFCQ; private static MapKey mapKey; @@ -77,6 +79,7 @@ public static void setUp() throws ConstructableRegistryException { mapKey = new MapKey(0, 0, random.nextLong()); state = Mockito.spy(PlatformTestingToolState.class); + lifecycles = new PlatformTestingToolStateLifecycles(); final Platform platform = Mockito.mock(Platform.class); when(platform.getSelfId()).thenReturn(NodeId.of(0L)); final Roster roster = RandomRosterBuilder.create(RANDOM).withSize(4).build(); @@ -166,7 +169,7 @@ public void purgeExpirationTest() throws Exception { generateRecords(500, 0, 500); final long purgeTime = Instant.now().getEpochSecond(); - final long removedNum = state.purgeExpiredRecords(purgeTime); + final long removedNum = lifecycles.purgeExpiredRecords(state, purgeTime); int expectedNumberOfPurgedRecords = 2001; diff --git a/platform-sdk/platform-apps/tests/PlatformTestingTool/src/timingSensitive/java/com/swirlds/demo/platform/PlatformTestingToolStateTests.java b/platform-sdk/platform-apps/tests/PlatformTestingTool/src/timingSensitive/java/com/swirlds/demo/platform/PlatformTestingToolStateTests.java deleted file mode 100644 index a5a3f0688aef..000000000000 --- a/platform-sdk/platform-apps/tests/PlatformTestingTool/src/timingSensitive/java/com/swirlds/demo/platform/PlatformTestingToolStateTests.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2022-2024 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.swirlds.demo.platform; - -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; - -import com.swirlds.demo.platform.nft.NftLedgerStatistics; -import com.swirlds.platform.system.Platform; -import java.lang.management.ManagementFactory; -import java.lang.management.ThreadInfo; -import java.util.Optional; -import java.util.concurrent.TimeUnit; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -class PlatformTestingToolStateTests { - - @AfterAll - static void cleanup() { - NftLedgerStatistics.unregister(); - } - - @Test - @Disabled("This test needs to be investigated") - void initStatistics() throws InterruptedException { - final long delay = 16; - final Platform platform = mock(Platform.class); - PlatformTestingToolState.initStatistics(platform); - final Optional threadInfo = getThread(PlatformTestingToolState.STAT_TIMER_THREAD_NAME); - TimeUnit.SECONDS.sleep(delay); - assertTrue(threadInfo.isPresent(), "Thread shouldn't die due to NPE"); - final Thread.State state = threadInfo.get().getThreadState(); - final boolean isInExpectedState = Thread.State.TIMED_WAITING == state || Thread.State.RUNNABLE == state; - assertTrue(isInExpectedState, "The timer task is either running or waiting to be run"); - } - - private static Optional getThread(final String threadName) { - final long[] threadIds = ManagementFactory.getThreadMXBean().getAllThreadIds(); - final ThreadInfo[] threadInfo = ManagementFactory.getThreadMXBean().getThreadInfo(threadIds); - - for (final ThreadInfo info : threadInfo) { - if (info != null) { - if (info.getThreadName().equals(threadName)) { - return Optional.of(info); - } - } - } - - return Optional.empty(); - } -} diff --git a/platform-sdk/platform-apps/tests/PlatformTestingTool/src/timingSensitive/java/com/swirlds/demo/platform/PttTransactionPoolTest.java b/platform-sdk/platform-apps/tests/PlatformTestingTool/src/timingSensitive/java/com/swirlds/demo/platform/PttTransactionPoolTest.java index 5feecea05ded..9e24b483a1e8 100644 --- a/platform-sdk/platform-apps/tests/PlatformTestingTool/src/timingSensitive/java/com/swirlds/demo/platform/PttTransactionPoolTest.java +++ b/platform-sdk/platform-apps/tests/PlatformTestingTool/src/timingSensitive/java/com/swirlds/demo/platform/PttTransactionPoolTest.java @@ -22,7 +22,6 @@ import static com.swirlds.merkle.test.fixtures.map.lifecycle.TransactionType.Delete; import static com.swirlds.merkle.test.fixtures.map.lifecycle.TransactionType.Update; import static com.swirlds.merkle.test.fixtures.map.pta.TransactionRecord.DEFAULT_EXPIRATION_TIME; -import static com.swirlds.platform.test.fixtures.state.FakeStateLifecycles.FAKE_MERKLE_STATE_LIFECYCLES; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -182,8 +181,7 @@ public void fcqDeleteTest() { .setOriginNode(otherID) .build(); try { - final PlatformTestingToolState state = - new PlatformTestingToolState(FAKE_MERKLE_STATE_LIFECYCLES, version -> new BasicSoftwareVersion(1)); + final PlatformTestingToolState state = new PlatformTestingToolState(version -> new BasicSoftwareVersion(1)); state.setFcmFamily(fCMFamily); handler.performOperation( trans, @@ -240,8 +238,7 @@ public void fcqDeleteNodeTest() { .setOriginNode(otherID) .build(); try { - final PlatformTestingToolState state = - new PlatformTestingToolState(FAKE_MERKLE_STATE_LIFECYCLES, version -> new BasicSoftwareVersion(1)); + final PlatformTestingToolState state = new PlatformTestingToolState(version -> new BasicSoftwareVersion(1)); state.setFcmFamily(fCMFamily); handler.performOperation( trans, diff --git a/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolMain.java b/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolMain.java index 8ceffb7bc04b..fb8870d32cd5 100644 --- a/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolMain.java +++ b/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolMain.java @@ -47,7 +47,7 @@ import com.swirlds.metrics.api.Metrics; import com.swirlds.platform.Browser; import com.swirlds.platform.ParameterProvider; -import com.swirlds.platform.state.PlatformMerkleStateRoot; +import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.system.BasicSoftwareVersion; import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.SwirldMain; @@ -60,7 +60,7 @@ * also saves them to disk in a comma separated value (.csv) file. Each transaction is 100 random bytes. So * StatsSigningDemoState.handleTransaction doesn't actually do anything. */ -public class StatsSigningTestingToolMain implements SwirldMain { +public class StatsSigningTestingToolMain implements SwirldMain { // the first four come from the parameters in the config.txt file private static final Logger logger = LogManager.getLogger(StatsSigningTestingToolMain.class); @@ -71,10 +71,8 @@ public class StatsSigningTestingToolMain implements SwirldMain { ConstructableRegistry constructableRegistry = ConstructableRegistry.getInstance(); constructableRegistry.registerConstructable( new ClassConstructorPair(StatsSigningTestingToolState.class, () -> { - StatsSigningTestingToolState statsSigningTestingToolState = new StatsSigningTestingToolState( - FAKE_MERKLE_STATE_LIFECYCLES, - version -> new BasicSoftwareVersion(version.major()), - () -> null); + StatsSigningTestingToolState statsSigningTestingToolState = + new StatsSigningTestingToolState(version -> new BasicSoftwareVersion(version.major())); return statsSigningTestingToolState; })); registerMerkleStateRootClassIds(); @@ -297,15 +295,18 @@ private synchronized void generateTransactions() { @Override @NonNull - public PlatformMerkleStateRoot newMerkleStateRoot() { - final PlatformMerkleStateRoot state = new StatsSigningTestingToolState( - FAKE_MERKLE_STATE_LIFECYCLES, - version -> new BasicSoftwareVersion(softwareVersion.getSoftwareVersion()), - () -> sttTransactionPool); + public StatsSigningTestingToolState newMerkleStateRoot() { + final StatsSigningTestingToolState state = new StatsSigningTestingToolState( + version -> new BasicSoftwareVersion(softwareVersion.getSoftwareVersion())); FAKE_MERKLE_STATE_LIFECYCLES.initStates(state); return state; } + @Override + public StateLifecycles newStateLifecycles() { + return new StatsSigningTestingToolStateLifecycles(() -> sttTransactionPool); + } + /** * {@inheritDoc} */ diff --git a/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolState.java b/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolState.java index fbc2d5f12b78..40336d322b21 100644 --- a/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolState.java +++ b/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolState.java @@ -26,36 +26,12 @@ * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. */ -import static com.swirlds.common.utility.CommonUtils.hex; -import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; -import static com.swirlds.logging.legacy.LogMarker.TESTING_EXCEPTIONS_ACCEPTABLE_RECONNECT; - import com.hedera.hapi.node.base.SemanticVersion; -import com.hedera.hapi.platform.event.StateSignatureTransaction; import com.swirlds.common.constructable.ConstructableIgnored; -import com.swirlds.common.crypto.CryptographyHolder; -import com.swirlds.common.crypto.TransactionSignature; -import com.swirlds.common.crypto.VerificationStatus; -import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.state.PlatformMerkleStateRoot; -import com.swirlds.platform.state.PlatformStateModifier; -import com.swirlds.platform.state.StateLifecycles; -import com.swirlds.platform.system.Round; import com.swirlds.platform.system.SoftwareVersion; -import com.swirlds.platform.system.events.Event; -import com.swirlds.platform.system.transaction.ConsensusTransaction; -import com.swirlds.platform.system.transaction.Transaction; import edu.umd.cs.findbugs.annotations.NonNull; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.function.Consumer; import java.util.function.Function; -import java.util.function.Supplier; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; /** * This demo collects statistics on the running of the network and consensus systems. It writes them to the @@ -68,37 +44,24 @@ public class StatsSigningTestingToolState extends PlatformMerkleStateRoot { private static final long CLASS_ID = 0x79900efa3127b6eL; - /** - * use this for all logging, as controlled by the optional data/log4j2.xml file - */ - private static final Logger logger = LogManager.getLogger(StatsSigningTestingToolState.class); - - private final Supplier transactionPoolSupplier; /** A running sum of transaction contents */ private long runningSum = 0; - /** if true, artificially take {@link #HANDLE_MICROS} to handle each consensus transaction */ - private static final boolean SYNTHETIC_HANDLE_TIME = false; - - /** the number of microseconds to wait before returning from the handle method */ - private static final int HANDLE_MICROS = 100; - - public StatsSigningTestingToolState( - @NonNull final StateLifecycles lifecycles, - @NonNull final Function versionFactory, - @NonNull final Supplier transactionPoolSupplier) { - super(lifecycles, versionFactory); - this.transactionPoolSupplier = Objects.requireNonNull(transactionPoolSupplier); + public StatsSigningTestingToolState(@NonNull final Function versionFactory) { + super(versionFactory); } private StatsSigningTestingToolState(@NonNull final StatsSigningTestingToolState sourceState) { super(sourceState); - this.transactionPoolSupplier = sourceState.transactionPoolSupplier; setImmutable(false); sourceState.setImmutable(true); } + void incrementRunningSum(final long delta) { + runningSum += delta; + } + /** * {@inheritDoc} */ @@ -109,102 +72,6 @@ public synchronized StatsSigningTestingToolState copy() { return new StatsSigningTestingToolState(this); } - /** - * {@inheritDoc} - */ - @Override - public void preHandle( - @NonNull final Event event, - @NonNull final Consumer> stateSignatureTransaction) { - final SttTransactionPool sttTransactionPool = transactionPoolSupplier.get(); - if (sttTransactionPool != null) { - event.forEachTransaction(transaction -> { - if (transaction.isSystem()) { - return; - } - final TransactionSignature transactionSignature = - sttTransactionPool.expandSignatures(transaction.getApplicationTransaction()); - if (transactionSignature != null) { - transaction.setMetadata(transactionSignature); - CryptographyHolder.get().verifySync(List.of(transactionSignature)); - } - }); - } - } - - /** - * {@inheritDoc} - */ - @Override - public void handleConsensusRound( - @NonNull final Round round, - @NonNull final PlatformStateModifier platformState, - @NonNull final Consumer> stateSignatureTransaction) { - throwIfImmutable(); - round.forEachTransaction(this::handleTransaction); - } - - private void handleTransaction(final ConsensusTransaction trans) { - if (trans.isSystem()) { - return; - } - final TransactionSignature s = trans.getMetadata(); - - if (s != null && validateSignature(s, trans) && s.getSignatureStatus() != VerificationStatus.VALID) { - logger.error( - EXCEPTION.getMarker(), - "Invalid Transaction Signature [ transactionId = {}, status = {}, signatureType = {}," - + " publicKey = {}, signature = {}, data = {} ]", - TransactionCodec.txId(trans.getApplicationTransaction()), - s.getSignatureStatus(), - s.getSignatureType(), - hex(Arrays.copyOfRange( - s.getContentsDirect(), - s.getPublicKeyOffset(), - s.getPublicKeyOffset() + s.getPublicKeyLength())), - hex(Arrays.copyOfRange( - s.getContentsDirect(), - s.getSignatureOffset(), - s.getSignatureOffset() + s.getSignatureLength())), - hex(Arrays.copyOfRange( - s.getContentsDirect(), s.getMessageOffset(), s.getMessageOffset() + s.getMessageLength()))); - } - - runningSum += TransactionCodec.txId(trans.getApplicationTransaction()); - - maybeDelay(); - } - - private void maybeDelay() { - if (SYNTHETIC_HANDLE_TIME) { - final long start = System.nanoTime(); - while (System.nanoTime() - start < (HANDLE_MICROS * 1_000)) { - // busy wait - } - } - } - - private boolean validateSignature(final TransactionSignature signature, final Transaction transaction) { - try { - final Future future = signature.waitForFuture(); - // Block & Ignore the Void return - future.get(); - return true; - } catch (final InterruptedException e) { - logger.info( - TESTING_EXCEPTIONS_ACCEPTABLE_RECONNECT.getMarker(), - "handleTransaction Interrupted. This should happen only during a reconnect"); - Thread.currentThread().interrupt(); - } catch (final ExecutionException e) { - logger.error( - EXCEPTION.getMarker(), - "error while validating transaction signature for transaction {}", - transaction, - e); - } - return false; - } - /** * {@inheritDoc} */ diff --git a/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolStateLifecycles.java b/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolStateLifecycles.java new file mode 100644 index 000000000000..5e7d7e93b880 --- /dev/null +++ b/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolStateLifecycles.java @@ -0,0 +1,177 @@ +/* + * 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.swirlds.demo.stats.signing; + +import static com.swirlds.common.utility.CommonUtils.hex; +import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; +import static com.swirlds.logging.legacy.LogMarker.TESTING_EXCEPTIONS_ACCEPTABLE_RECONNECT; + +import com.hedera.hapi.platform.event.StateSignatureTransaction; +import com.swirlds.common.context.PlatformContext; +import com.swirlds.common.crypto.CryptographyHolder; +import com.swirlds.common.crypto.TransactionSignature; +import com.swirlds.common.crypto.VerificationStatus; +import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; +import com.swirlds.platform.state.StateLifecycles; +import com.swirlds.platform.system.InitTrigger; +import com.swirlds.platform.system.Platform; +import com.swirlds.platform.system.Round; +import com.swirlds.platform.system.SoftwareVersion; +import com.swirlds.platform.system.address.AddressBook; +import com.swirlds.platform.system.events.Event; +import com.swirlds.platform.system.transaction.ConsensusTransaction; +import com.swirlds.platform.system.transaction.Transaction; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.function.Consumer; +import java.util.function.Supplier; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class StatsSigningTestingToolStateLifecycles implements StateLifecycles { + + /** + * use this for all logging, as controlled by the optional data/log4j2.xml file + */ + private static final Logger logger = LogManager.getLogger(StatsSigningTestingToolStateLifecycles.class); + + /** if true, artificially take {@link #HANDLE_MICROS} to handle each consensus transaction */ + private static final boolean SYNTHETIC_HANDLE_TIME = false; + + /** the number of microseconds to wait before returning from the handle method */ + private static final int HANDLE_MICROS = 100; + + private final Supplier transactionPoolSupplier; + + public StatsSigningTestingToolStateLifecycles(Supplier transactionPoolSupplier) { + this.transactionPoolSupplier = transactionPoolSupplier; + } + + @Override + public void onStateInitialized( + @NonNull StatsSigningTestingToolState state, + @NonNull Platform platform, + @NonNull InitTrigger trigger, + @Nullable SoftwareVersion previousVersion) {} + + @Override + public void onPreHandle( + @NonNull Event event, + @NonNull StatsSigningTestingToolState state, + @NonNull Consumer> stateSignatureTransactionCallback) { + final SttTransactionPool sttTransactionPool = transactionPoolSupplier.get(); + if (sttTransactionPool != null) { + event.forEachTransaction(transaction -> { + if (transaction.isSystem()) { + return; + } + final TransactionSignature transactionSignature = + sttTransactionPool.expandSignatures(transaction.getApplicationTransaction()); + if (transactionSignature != null) { + transaction.setMetadata(transactionSignature); + CryptographyHolder.get().verifySync(List.of(transactionSignature)); + } + }); + } + } + + @Override + public void onHandleConsensusRound( + @NonNull Round round, + @NonNull StatsSigningTestingToolState state, + @NonNull Consumer> stateSignatureTransactionCallback) { + state.throwIfImmutable(); + round.forEachTransaction(v -> handleTransaction(v, state)); + } + + private void handleTransaction(final ConsensusTransaction trans, final StatsSigningTestingToolState state) { + if (trans.isSystem()) { + return; + } + final TransactionSignature s = trans.getMetadata(); + + if (s != null && validateSignature(s, trans) && s.getSignatureStatus() != VerificationStatus.VALID) { + logger.error( + EXCEPTION.getMarker(), + "Invalid Transaction Signature [ transactionId = {}, status = {}, signatureType = {}," + + " publicKey = {}, signature = {}, data = {} ]", + TransactionCodec.txId(trans.getApplicationTransaction()), + s.getSignatureStatus(), + s.getSignatureType(), + hex(Arrays.copyOfRange( + s.getContentsDirect(), + s.getPublicKeyOffset(), + s.getPublicKeyOffset() + s.getPublicKeyLength())), + hex(Arrays.copyOfRange( + s.getContentsDirect(), + s.getSignatureOffset(), + s.getSignatureOffset() + s.getSignatureLength())), + hex(Arrays.copyOfRange( + s.getContentsDirect(), s.getMessageOffset(), s.getMessageOffset() + s.getMessageLength()))); + } + + state.incrementRunningSum(TransactionCodec.txId(trans.getApplicationTransaction())); + + maybeDelay(); + } + + private void maybeDelay() { + if (SYNTHETIC_HANDLE_TIME) { + final long start = System.nanoTime(); + while (System.nanoTime() - start < (HANDLE_MICROS * 1_000)) { + // busy wait + } + } + } + + private boolean validateSignature(final TransactionSignature signature, final Transaction transaction) { + try { + final Future future = signature.waitForFuture(); + // Block & Ignore the Void return + future.get(); + return true; + } catch (final InterruptedException e) { + logger.info( + TESTING_EXCEPTIONS_ACCEPTABLE_RECONNECT.getMarker(), + "handleTransaction Interrupted. This should happen only during a reconnect"); + Thread.currentThread().interrupt(); + } catch (final ExecutionException e) { + logger.error( + EXCEPTION.getMarker(), + "error while validating transaction signature for transaction {}", + transaction, + e); + } + return false; + } + + @Override + public void onSealConsensusRound(@NonNull Round round, @NonNull StatsSigningTestingToolState state) {} + + @Override + public void onUpdateWeight( + @NonNull StatsSigningTestingToolState state, + @NonNull AddressBook configAddressBook, + @NonNull PlatformContext context) {} + + @Override + public void onNewRecoveredState(@NonNull StatsSigningTestingToolState recoveredState) {} +} diff --git a/platform-sdk/platform-apps/tests/StressTestingTool/src/main/java/com/swirlds/demo/stress/StressTestingToolMain.java b/platform-sdk/platform-apps/tests/StressTestingTool/src/main/java/com/swirlds/demo/stress/StressTestingToolMain.java index 5c5632c8681a..76780ac5be9e 100644 --- a/platform-sdk/platform-apps/tests/StressTestingTool/src/main/java/com/swirlds/demo/stress/StressTestingToolMain.java +++ b/platform-sdk/platform-apps/tests/StressTestingTool/src/main/java/com/swirlds/demo/stress/StressTestingToolMain.java @@ -46,7 +46,7 @@ import com.swirlds.common.threading.framework.config.ThreadConfiguration; import com.swirlds.metrics.api.Metrics; import com.swirlds.platform.Browser; -import com.swirlds.platform.state.PlatformMerkleStateRoot; +import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.system.BasicSoftwareVersion; import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.SwirldMain; @@ -59,19 +59,17 @@ /** * A testing tool which generates a number of transactions per second, and simulates handling them */ -public class StressTestingToolMain implements SwirldMain { +public class StressTestingToolMain implements SwirldMain { private static final Logger logger = LogManager.getLogger(StressTestingToolMain.class); private static final BasicSoftwareVersion SOFTWARE_VERSION = new BasicSoftwareVersion(1); static { try { logger.info(STARTUP.getMarker(), "Registering StressTestingToolState with ConstructableRegistry"); - final ConstructableRegistry constructableRegistry = ConstructableRegistry.getInstance(); - constructableRegistry.registerConstructable(new ClassConstructorPair(StressTestingToolState.class, () -> { - final StressTestingToolState stressTestingToolState = new StressTestingToolState( - FAKE_MERKLE_STATE_LIFECYCLES, version -> new BasicSoftwareVersion(version.major())); - return stressTestingToolState; - })); + ConstructableRegistry constructableRegistry = ConstructableRegistry.getInstance(); + constructableRegistry.registerConstructable(new ClassConstructorPair( + StressTestingToolState.class, + () -> new StressTestingToolState(version -> new BasicSoftwareVersion(version.major())))); registerMerkleStateRootClassIds(); logger.info(STARTUP.getMarker(), "StressTestingToolState is registered with ConstructableRegistry"); } catch (final ConstructableRegistryException e) { @@ -255,14 +253,18 @@ private synchronized void generateTransactions() { } @Override - public PlatformMerkleStateRoot newMerkleStateRoot() { - final PlatformMerkleStateRoot state = new StressTestingToolState( - FAKE_MERKLE_STATE_LIFECYCLES, - version -> new BasicSoftwareVersion(SOFTWARE_VERSION.getSoftwareVersion())); + public StressTestingToolState newMerkleStateRoot() { + final StressTestingToolState state = + new StressTestingToolState(version -> new BasicSoftwareVersion(SOFTWARE_VERSION.getSoftwareVersion())); FAKE_MERKLE_STATE_LIFECYCLES.initStates(state); return state; } + @Override + public StateLifecycles newStateLifecycles() { + return new StressTestingToolStateLifecycles(); + } + /** * {@inheritDoc} */ diff --git a/platform-sdk/platform-apps/tests/StressTestingTool/src/main/java/com/swirlds/demo/stress/StressTestingToolState.java b/platform-sdk/platform-apps/tests/StressTestingTool/src/main/java/com/swirlds/demo/stress/StressTestingToolState.java index c9ded890232b..4dfa62d62d08 100644 --- a/platform-sdk/platform-apps/tests/StressTestingTool/src/main/java/com/swirlds/demo/stress/StressTestingToolState.java +++ b/platform-sdk/platform-apps/tests/StressTestingTool/src/main/java/com/swirlds/demo/stress/StressTestingToolState.java @@ -26,30 +26,12 @@ * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. */ -import static com.swirlds.demo.stress.TransactionPool.APPLICATION_TRANSACTION_MARKER; - import com.hedera.hapi.node.base.SemanticVersion; -import com.hedera.hapi.platform.event.StateSignatureTransaction; import com.swirlds.common.constructable.ConstructableIgnored; -import com.swirlds.common.utility.ByteUtils; -import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.state.PlatformMerkleStateRoot; -import com.swirlds.platform.state.PlatformStateModifier; -import com.swirlds.platform.state.StateLifecycles; -import com.swirlds.platform.system.InitTrigger; -import com.swirlds.platform.system.Platform; -import com.swirlds.platform.system.Round; import com.swirlds.platform.system.SoftwareVersion; -import com.swirlds.platform.system.events.Event; -import com.swirlds.platform.system.transaction.ConsensusTransaction; -import com.swirlds.platform.system.transaction.Transaction; import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.Nullable; -import java.time.Duration; -import java.util.function.Consumer; import java.util.function.Function; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; /** * This testing tool simulates configurable processing times for both preHandling and handling for stress testing @@ -59,28 +41,24 @@ public class StressTestingToolState extends PlatformMerkleStateRoot { private static final long CLASS_ID = 0x79900efa3127b6eL; - private static final Logger logger = LogManager.getLogger(StressTestingToolState.class); - /** A running sum of transaction contents */ private long runningSum = 0; - /** supplies the app config */ - public StressTestingToolConfig config; - - public StressTestingToolState( - @NonNull final StateLifecycles lifecycles, - @NonNull final Function versionFactory) { - super(lifecycles, versionFactory); + public StressTestingToolState(@NonNull final Function versionFactory) { + super(versionFactory); } private StressTestingToolState(@NonNull final StressTestingToolState sourceState) { super(sourceState); runningSum = sourceState.runningSum; - config = sourceState.config; setImmutable(false); sourceState.setImmutable(true); } + void incrementRunningSum(final long delta) { + runningSum += delta; + } + /** * {@inheritDoc} */ @@ -91,138 +69,6 @@ public synchronized StressTestingToolState copy() { return new StressTestingToolState(this); } - /** - * {@inheritDoc} - */ - @Override - public void init( - @NonNull final Platform platform, - @NonNull final InitTrigger trigger, - @Nullable final SoftwareVersion previousSoftwareVersion) { - super.init(platform, trigger, previousSoftwareVersion); - - this.config = platform.getContext().getConfiguration().getConfigData(StressTestingToolConfig.class); - } - - /** - * {@inheritDoc} - */ - @Override - public void preHandle( - @NonNull final Event event, - @NonNull - final Consumer> - stateSignatureTransactionCallback) { - event.forEachTransaction(transaction -> { - // We are not interested in pre-handling any system transactions, as they are - // specific for the platform only.We also don't want to consume deprecated - // EventTransaction.STATE_SIGNATURE_TRANSACTION system transactions in the - // callback,since it's intended to be used only for the new form of encoded system - // transactions in Bytes.Thus, we can directly skip the current - // iteration, if it processes a deprecated system transaction with the - // EventTransaction.STATE_SIGNATURE_TRANSACTION type. - if (transaction.isSystem()) { - return; - } - - if (areTransactionBytesSystemOnes(transaction)) { - consumeSystemTransaction(transaction, event, stateSignatureTransactionCallback); - } - }); - - busyWait(config.preHandleTime()); - } - - /** - * {@inheritDoc} - */ - @Override - public void handleConsensusRound( - @NonNull final Round round, - @NonNull final PlatformStateModifier platformState, - @NonNull - final Consumer> - stateSignatureTransactionCallback) { - throwIfImmutable(); - - for (final var event : round) { - event.consensusTransactionIterator().forEachRemaining(transaction -> { - // We are not interested in handling any system transactions, as they are - // specific for the platform only.We also don't want to consume deprecated - // EventTransaction.STATE_SIGNATURE_TRANSACTION system transactions in the - // callback,since it's intended to be used only for the new form of encoded system - // transactions in Bytes.Thus, we can directly skip the current - // iteration, if it processes a deprecated system transaction with the - // EventTransaction.STATE_SIGNATURE_TRANSACTION type. - if (transaction.isSystem()) { - return; - } - - if (areTransactionBytesSystemOnes(transaction)) { - consumeSystemTransaction(transaction, event, stateSignatureTransactionCallback); - } else { - handleTransaction(transaction); - } - }); - } - } - - private void handleTransaction(@NonNull final ConsensusTransaction trans) { - runningSum += - ByteUtils.byteArrayToLong(trans.getApplicationTransaction().toByteArray(), 0); - busyWait(config.handleTime()); - } - - /** - * Checks if the transaction bytes are system ones. The test creates application transactions with max length of 4. - * System transactions will be always bigger than that. - * - * @param transaction the consensus transaction to check - * @return true if the transaction bytes are system ones, false otherwise - */ - private boolean areTransactionBytesSystemOnes(@NonNull final Transaction transaction) { - final var transactionBytes = transaction.getApplicationTransaction(); - - if (transactionBytes.length() == 0) { - return false; - } - - return transactionBytes.getByte(0) != APPLICATION_TRANSACTION_MARKER; - } - - /** - * Converts a transaction to a {@link StateSignatureTransaction} and then consumes it into a callback. - * - * @param transaction the transaction to consume - * @param event the event that contains the transaction - * @param stateSignatureTransactionCallback the callback to call with the system transaction - */ - private void consumeSystemTransaction( - final @NonNull Transaction transaction, - final @NonNull Event event, - final @NonNull Consumer> - stateSignatureTransactionCallback) { - try { - final var stateSignatureTransaction = - StateSignatureTransaction.PROTOBUF.parse(transaction.getApplicationTransaction()); - stateSignatureTransactionCallback.accept(new ScopedSystemTransaction<>( - event.getCreatorId(), event.getSoftwareVersion(), stateSignatureTransaction)); - } catch (final com.hedera.pbj.runtime.ParseException e) { - logger.error("Failed to parse StateSignatureTransaction", e); - } - } - - @SuppressWarnings("all") - private void busyWait(@NonNull final Duration duration) { - if (!duration.isZero() && !duration.isNegative()) { - final long start = System.nanoTime(); - final long nanos = duration.toNanos(); - while (System.nanoTime() - start < nanos) { - // busy wait - } - } - } - /** * {@inheritDoc} */ diff --git a/platform-sdk/platform-apps/tests/StressTestingTool/src/main/java/com/swirlds/demo/stress/StressTestingToolStateLifecycles.java b/platform-sdk/platform-apps/tests/StressTestingTool/src/main/java/com/swirlds/demo/stress/StressTestingToolStateLifecycles.java new file mode 100644 index 000000000000..5e7916ecde8d --- /dev/null +++ b/platform-sdk/platform-apps/tests/StressTestingTool/src/main/java/com/swirlds/demo/stress/StressTestingToolStateLifecycles.java @@ -0,0 +1,186 @@ +/* + * 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.swirlds.demo.stress; + +import static com.swirlds.demo.stress.TransactionPool.APPLICATION_TRANSACTION_MARKER; + +import com.hedera.hapi.platform.event.StateSignatureTransaction; +import com.swirlds.common.context.PlatformContext; +import com.swirlds.common.utility.ByteUtils; +import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; +import com.swirlds.platform.state.StateLifecycles; +import com.swirlds.platform.system.InitTrigger; +import com.swirlds.platform.system.Platform; +import com.swirlds.platform.system.Round; +import com.swirlds.platform.system.SoftwareVersion; +import com.swirlds.platform.system.address.AddressBook; +import com.swirlds.platform.system.events.Event; +import com.swirlds.platform.system.transaction.ConsensusTransaction; +import com.swirlds.platform.system.transaction.Transaction; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.time.Duration; +import java.util.function.Consumer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class StressTestingToolStateLifecycles implements StateLifecycles { + private static final Logger logger = LogManager.getLogger(StressTestingToolStateLifecycles.class); + + /** supplies the app config */ + private StressTestingToolConfig config; + + @Override + public void onStateInitialized( + @NonNull StressTestingToolState state, + @NonNull Platform platform, + @NonNull InitTrigger trigger, + @Nullable SoftwareVersion previousVersion) { + this.config = platform.getContext().getConfiguration().getConfigData(StressTestingToolConfig.class); + } + + @Override + public void onPreHandle( + @NonNull Event event, + @NonNull StressTestingToolState state, + @NonNull Consumer> stateSignatureTransactionCallback) { + event.forEachTransaction(transaction -> { + // We are not interested in pre-handling any system transactions, as they are + // specific for the platform only.We also don't want to consume deprecated + // EventTransaction.STATE_SIGNATURE_TRANSACTION system transactions in the + // callback,since it's intended to be used only for the new form of encoded system + // transactions in Bytes.Thus, we can directly skip the current + // iteration, if it processes a deprecated system transaction with the + // EventTransaction.STATE_SIGNATURE_TRANSACTION type. + if (transaction.isSystem()) { + return; + } + + if (areTransactionBytesSystemOnes(transaction)) { + consumeSystemTransaction(transaction, event, stateSignatureTransactionCallback); + } + }); + + busyWait(config.preHandleTime()); + } + + @SuppressWarnings("StatementWithEmptyBody") + private void busyWait(@NonNull final Duration duration) { + if (!duration.isZero() && !duration.isNegative()) { + final long start = System.nanoTime(); + final long nanos = duration.toNanos(); + while (System.nanoTime() - start < nanos) { + // busy wait + } + } + } + + @Override + public void onHandleConsensusRound( + @NonNull Round round, + @NonNull StressTestingToolState state, + @NonNull Consumer> stateSignatureTransactionCallback) { + state.throwIfImmutable(); + for (final var event : round) { + event.consensusTransactionIterator().forEachRemaining(transaction -> { + // We are not interested in handling any system transactions, as they are + // specific for the platform only.We also don't want to consume deprecated + // EventTransaction.STATE_SIGNATURE_TRANSACTION system transactions in the + // callback,since it's intended to be used only for the new form of encoded system + // transactions in Bytes.Thus, we can directly skip the current + // iteration, if it processes a deprecated system transaction with the + // EventTransaction.STATE_SIGNATURE_TRANSACTION type. + if (transaction.isSystem()) { + return; + } + + if (areTransactionBytesSystemOnes(transaction)) { + consumeSystemTransaction(transaction, event, stateSignatureTransactionCallback); + } else { + handleTransaction(transaction, state); + } + }); + } + } + + private void handleTransaction(@NonNull final ConsensusTransaction trans, StressTestingToolState state) { + if (trans.isSystem()) { + return; + } + + state.incrementRunningSum( + ByteUtils.byteArrayToLong(trans.getApplicationTransaction().toByteArray(), 0)); + busyWait(config.handleTime()); + } + + @Override + public void onSealConsensusRound(@NonNull Round round, @NonNull StressTestingToolState state) { + // no-op + } + + @Override + public void onUpdateWeight( + @NonNull StressTestingToolState state, + @NonNull AddressBook configAddressBook, + @NonNull PlatformContext context) { + // no-op + } + + @Override + public void onNewRecoveredState(@NonNull StressTestingToolState recoveredState) { + // no-op + } + + /** + * Checks if the transaction bytes are system ones. The test creates application transactions with max length of 4. + * System transactions will be always bigger than that. + * + * @param transaction the consensus transaction to check + * @return true if the transaction bytes are system ones, false otherwise + */ + private boolean areTransactionBytesSystemOnes(@NonNull final Transaction transaction) { + final var transactionBytes = transaction.getApplicationTransaction(); + + if (transactionBytes.length() == 0) { + return false; + } + + return transactionBytes.getByte(0) != APPLICATION_TRANSACTION_MARKER; + } + + /** + * Converts a transaction to a {@link StateSignatureTransaction} and then consumes it into a callback. + * + * @param transaction the transaction to consume + * @param event the event that contains the transaction + * @param stateSignatureTransactionCallback the callback to call with the system transaction + */ + private void consumeSystemTransaction( + final @NonNull Transaction transaction, + final @NonNull Event event, + final @NonNull Consumer> + stateSignatureTransactionCallback) { + try { + final var stateSignatureTransaction = + StateSignatureTransaction.PROTOBUF.parse(transaction.getApplicationTransaction()); + stateSignatureTransactionCallback.accept(new ScopedSystemTransaction<>( + event.getCreatorId(), event.getSoftwareVersion(), stateSignatureTransaction)); + } catch (final com.hedera.pbj.runtime.ParseException e) { + logger.error("Failed to parse StateSignatureTransaction", e); + } + } +} diff --git a/platform-sdk/platform-apps/tests/StressTestingTool/src/test/java/com/swirlds/demo/stress/StressTestingToolStateTest.java b/platform-sdk/platform-apps/tests/StressTestingTool/src/test/java/com/swirlds/demo/stress/StressTestingToolStateTest.java index d2da88c45171..f707a1bfae83 100644 --- a/platform-sdk/platform-apps/tests/StressTestingTool/src/test/java/com/swirlds/demo/stress/StressTestingToolStateTest.java +++ b/platform-sdk/platform-apps/tests/StressTestingTool/src/test/java/com/swirlds/demo/stress/StressTestingToolStateTest.java @@ -44,7 +44,6 @@ import com.swirlds.platform.crypto.PublicStores; import com.swirlds.platform.event.PlatformEvent; import com.swirlds.platform.state.PlatformStateModifier; -import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.system.InitTrigger; import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.Round; @@ -72,6 +71,7 @@ class StressTestingToolStateTest { private static final byte[] EMPTY_ARRAY = new byte[] {}; private StressTestingToolState state; private StressTestingToolMain main; + private StressTestingToolStateLifecycles stateLifecycles; private PlatformStateModifier platformStateModifier; private Round round; private ConsensusEvent event; @@ -82,7 +82,8 @@ class StressTestingToolStateTest { @BeforeEach void setUp() throws NoSuchAlgorithmException, KeyStoreException, KeyGeneratingException, NoSuchProviderException { - state = new StressTestingToolState(mock(StateLifecycles.class), mock(Function.class)); + state = new StressTestingToolState(mock(Function.class)); + stateLifecycles = new StressTestingToolStateLifecycles(); main = new StressTestingToolMain(); platformStateModifier = mock(PlatformStateModifier.class); event = mock(PlatformEvent.class); @@ -123,7 +124,7 @@ void setUp() throws NoSuchAlgorithmException, KeyStoreException, KeyGeneratingEx when(platformContext.getMetrics()).thenReturn(metrics); when(platformContext.getMerkleCryptography()).thenReturn(cryptography); - state.init(platform, initTrigger, softwareVersion); + stateLifecycles.onStateInitialized(state, platform, initTrigger, softwareVersion); } @ParameterizedTest @@ -137,7 +138,7 @@ void handleConsensusRoundWithApplicationTransaction(final Integer transactionSiz when(transaction.getApplicationTransaction()).thenReturn(Bytes.wrap(pool.transaction())); // When - state.handleConsensusRound(round, platformStateModifier, consumer); + stateLifecycles.onHandleConsensusRound(round, state, consumer); // Then assertThat(consumedSystemTransactions.size()).isZero(); @@ -152,7 +153,7 @@ void handleConsensusRoundWithSystemTransaction() { when(transaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); // When - state.handleConsensusRound(round, platformStateModifier, consumer); + stateLifecycles.onHandleConsensusRound(round, state, consumer); // Then assertThat(consumedSystemTransactions.size()).isEqualTo(1); @@ -178,7 +179,7 @@ void handleConsensusRoundWithMultipleSystemTransaction() { when(thirdConsensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); // When - state.handleConsensusRound(round, platformStateModifier, consumer); + stateLifecycles.onHandleConsensusRound(round, state, consumer); // Then assertThat(consumedSystemTransactions.size()).isEqualTo(3); @@ -193,7 +194,7 @@ void handleConsensusRoundWithDeprecatedSystemTransaction() { when(transaction.isSystem()).thenReturn(true); // When - state.handleConsensusRound(round, platformStateModifier, consumer); + stateLifecycles.onHandleConsensusRound(round, state, consumer); // Then assertThat(consumedSystemTransactions.size()).isZero(); @@ -215,7 +216,7 @@ void preHandleConsensusRoundWithApplicationTransaction(final Integer transaction event = new PlatformEvent(gossipEvent); // When - state.preHandle(event, consumer); + stateLifecycles.onPreHandle(event, state, consumer); // Then assertThat(consumedSystemTransactions.size()).isZero(); @@ -235,7 +236,7 @@ void preHandleConsensusRoundWithSystemTransaction() { event = new PlatformEvent(gossipEvent); // When - state.preHandle(event, consumer); + stateLifecycles.onPreHandle(event, state, consumer); // Then assertThat(consumedSystemTransactions.size()).isEqualTo(1); @@ -264,7 +265,7 @@ void preHandleConsensusRoundWithMultipleSystemTransaction() { event = new PlatformEvent(gossipEvent); // When - state.preHandle(event, consumer); + stateLifecycles.onPreHandle(event, state, consumer); // Then assertThat(consumedSystemTransactions.size()).isEqualTo(3); @@ -285,7 +286,7 @@ void preHandleConsensusRoundWithDeprecatedSystemTransaction() { event = new PlatformEvent(gossipEvent); // When - state.preHandle(event, consumer); + stateLifecycles.onPreHandle(event, state, consumer); // Then assertThat(consumedSystemTransactions.size()).isZero(); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/Browser.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/Browser.java index bd7094699d29..ed653033da2f 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/Browser.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/Browser.java @@ -70,13 +70,13 @@ import com.swirlds.platform.gui.model.InfoMember; import com.swirlds.platform.gui.model.InfoSwirld; import com.swirlds.platform.roster.RosterUtils; +import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.state.signed.HashedReservedSignedState; import com.swirlds.platform.system.SwirldMain; import com.swirlds.platform.system.SystemExitCode; import com.swirlds.platform.system.address.AddressBook; import com.swirlds.platform.system.address.AddressBookUtils; import com.swirlds.platform.util.BootstrapUtils; -import com.swirlds.state.State; import edu.umd.cs.findbugs.annotations.NonNull; import java.awt.GraphicsEnvironment; import java.util.ArrayList; @@ -272,6 +272,7 @@ private static void launchUnhandled(@NonNull final CommandLineArgs commandLineAr // Each platform needs a different temporary state on disk. MerkleDb.resetDefaultInstancePath(); // Create the initial state for the platform + StateLifecycles stateLifecycles = appMain.newStateLifecycles(); final HashedReservedSignedState reservedState = getInitialState( configuration, recycleBin, @@ -289,7 +290,8 @@ private static void launchUnhandled(@NonNull final CommandLineArgs commandLineAr appMain.getSoftwareVersion(), initialState, appDefinition.getConfigAddressBook(), - platformContext); + platformContext, + stateLifecycles); // Build the platform with the given values final PlatformBuilder builder = PlatformBuilder.create( @@ -297,9 +299,10 @@ private static void launchUnhandled(@NonNull final CommandLineArgs commandLineAr appDefinition.getSwirldName(), appMain.getSoftwareVersion(), initialState, + stateLifecycles, nodeId, AddressBookUtils.formatConsensusEventStreamName(addressBook, nodeId), - RosterUtils.buildRosterHistory((State) initialState.get().getState())); + RosterUtils.buildRosterHistory(initialState.get().getState())); if (showUi && index == 0) { builder.withPreconsensusEventCallback(guiEventStorage::handlePreconsensusEvent); builder.withConsensusSnapshotOverrideCallback(guiEventStorage::handleSnapshotOverride); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/ReconnectStateLoader.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/ReconnectStateLoader.java index 944ef5e116b6..62175b827599 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/ReconnectStateLoader.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/ReconnectStateLoader.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. @@ -31,11 +31,13 @@ import com.swirlds.platform.eventhandling.EventConfig; import com.swirlds.platform.listeners.ReconnectCompleteNotification; import com.swirlds.platform.roster.RosterRetriever; +import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.state.SwirldStateManager; import com.swirlds.platform.state.nexus.SignedStateNexus; import com.swirlds.platform.state.signed.SignedState; import com.swirlds.platform.system.InitTrigger; import com.swirlds.platform.system.Platform; +import com.swirlds.platform.system.SoftwareVersion; import com.swirlds.platform.system.status.actions.ReconnectCompleteAction; import com.swirlds.platform.wiring.PlatformWiring; import com.swirlds.state.State; @@ -59,6 +61,7 @@ public class ReconnectStateLoader { private final SignedStateNexus latestImmutableStateNexus; private final SavedStateController savedStateController; private final Roster roster; + private final StateLifecycles stateLifecycles; /** * Constructor. @@ -70,6 +73,7 @@ public class ReconnectStateLoader { * @param latestImmutableStateNexus holds the latest immutable state * @param savedStateController manages how states are saved * @param roster the current roster + * @param stateLifecycles state lifecycle event handler */ public ReconnectStateLoader( @NonNull final Platform platform, @@ -78,7 +82,8 @@ public ReconnectStateLoader( @NonNull final SwirldStateManager swirldStateManager, @NonNull final SignedStateNexus latestImmutableStateNexus, @NonNull final SavedStateController savedStateController, - @NonNull final Roster roster) { + @NonNull final Roster roster, + @NonNull final StateLifecycles stateLifecycles) { this.platform = Objects.requireNonNull(platform); this.platformContext = Objects.requireNonNull(platformContext); this.platformWiring = Objects.requireNonNull(platformWiring); @@ -86,6 +91,7 @@ public ReconnectStateLoader( this.latestImmutableStateNexus = Objects.requireNonNull(latestImmutableStateNexus); this.savedStateController = Objects.requireNonNull(savedStateController); this.roster = Objects.requireNonNull(roster); + this.stateLifecycles = stateLifecycles; } /** @@ -103,12 +109,12 @@ public void loadReconnectState(@NonNull final SignedState signedState) { // It's important to call init() before loading the signed state. The loading process makes copies // of the state, and we want to be sure that the first state in the chain of copies has been initialized. final Hash reconnectHash = signedState.getState().getHash(); - signedState - .getSwirldState() - .init( - platform, - InitTrigger.RECONNECT, - signedState.getState().getReadablePlatformState().getCreationSoftwareVersion()); + SoftwareVersion creationSoftwareVersion = + signedState.getState().getReadablePlatformState().getCreationSoftwareVersion(); + signedState.init(platformContext); + stateLifecycles.onStateInitialized( + signedState.getState(), platform, InitTrigger.RECONNECT, creationSoftwareVersion); + if (!Objects.equals(signedState.getState().getHash(), reconnectHash)) { throw new IllegalStateException( "State hash is not permitted to change during a reconnect init() call. Previous hash was " diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/StateInitializer.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/StateInitializer.java index 8db8764cbaeb..4c5030461fd9 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/StateInitializer.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/StateInitializer.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. @@ -26,10 +26,12 @@ import com.swirlds.common.merkle.crypto.MerkleCryptoFactory; import com.swirlds.platform.config.StateConfig; import com.swirlds.platform.state.PlatformMerkleStateRoot; +import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.state.signed.SignedState; import com.swirlds.platform.system.InitTrigger; import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.SoftwareVersion; +import com.swirlds.state.merkle.MerkleStateRoot; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.concurrent.ExecutionException; import org.apache.logging.log4j.LogManager; @@ -37,7 +39,7 @@ /** * Encapsulates the logic for calling - * {@link com.swirlds.platform.system.SwirldState#init(Platform, InitTrigger, SoftwareVersion)} + * {@link StateLifecycles#onStateInitialized(MerkleStateRoot, Platform, InitTrigger, SoftwareVersion)} * startup time. */ public final class StateInitializer { @@ -56,7 +58,8 @@ private StateInitializer() {} public static void initializeState( @NonNull final Platform platform, @NonNull final PlatformContext platformContext, - @NonNull final SignedState signedState) { + @NonNull final SignedState signedState, + @NonNull final StateLifecycles stateLifecycles) { final SoftwareVersion previousSoftwareVersion; final InitTrigger trigger; @@ -74,14 +77,13 @@ public static void initializeState( // Although the state from disk / genesis state is initially hashed, we are actually dealing with a copy // of that state here. That copy should have caused the hash to be cleared. + if (initialState.getHash() != null) { throw new IllegalStateException("Expected initial state to be unhashed"); } - if (initialState.getHash() != null) { - throw new IllegalStateException("Expected initial swirld state to be unhashed"); - } - initialState.init(platform, trigger, previousSoftwareVersion); + signedState.init(platformContext); + stateLifecycles.onStateInitialized(signedState.getState(), platform, trigger, previousSoftwareVersion); abortAndThrowIfInterrupted( () -> { diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/SwirldsPlatform.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/SwirldsPlatform.java index 80e812b199fa..6400c84ca4d7 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/SwirldsPlatform.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/SwirldsPlatform.java @@ -60,6 +60,7 @@ import com.swirlds.platform.publisher.PlatformPublisher; import com.swirlds.platform.state.PlatformMerkleStateRoot; import com.swirlds.platform.state.PlatformStateAccessor; +import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.state.SwirldStateManager; import com.swirlds.platform.state.nexus.DefaultLatestCompleteStateNexus; import com.swirlds.platform.state.nexus.LatestCompleteStateNexus; @@ -75,12 +76,12 @@ import com.swirlds.platform.state.snapshot.StateToDiskReason; import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.SoftwareVersion; -import com.swirlds.platform.system.SwirldState; import com.swirlds.platform.system.events.BirthRoundMigrationShim; import com.swirlds.platform.system.events.DefaultBirthRoundMigrationShim; import com.swirlds.platform.system.status.actions.DoneReplayingEventsAction; import com.swirlds.platform.system.status.actions.StartedReplayingEventsAction; import com.swirlds.platform.wiring.PlatformWiring; +import com.swirlds.state.merkle.MerkleStateRoot; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.io.IOException; @@ -134,11 +135,6 @@ public class SwirldsPlatform implements Platform { */ private final SignedStateNexus latestImmutableStateNexus = new LockFreeStateNexus(); - /** - * Handles all interaction with {@link SwirldState} - */ - private final SwirldStateManager swirldStateManager; - /** * For passing notifications between the platform and the application. */ @@ -178,6 +174,7 @@ public class SwirldsPlatform implements Platform { public SwirldsPlatform(@NonNull final PlatformComponentBuilder builder) { final PlatformBuildingBlocks blocks = builder.getBuildingBlocks(); platformContext = blocks.platformContext(); + final StateLifecycles stateLifecycles = blocks.stateLifecycles(); final AncientMode ancientMode = platformContext .getConfiguration() @@ -247,10 +244,13 @@ public SwirldsPlatform(@NonNull final PlatformComponentBuilder builder) { () -> latestImmutableStateNexus.getState("PCES replay"), () -> isLessThan(blocks.model().getUnhealthyDuration(), replayHealthThreshold)); - initializeState(this, platformContext, initialState); + initializeState(this, platformContext, initialState, stateLifecycles); // This object makes a copy of the state. After this point, initialState becomes immutable. - swirldStateManager = blocks.swirldStateManager(); + /** + * Handles all interaction with {@link StateLifecycles} + */ + SwirldStateManager swirldStateManager = blocks.swirldStateManager(); swirldStateManager.setInitialState(initialState.getState()); final EventWindowManager eventWindowManager = new DefaultEventWindowManager(); @@ -343,7 +343,8 @@ public SwirldsPlatform(@NonNull final PlatformComponentBuilder builder) { swirldStateManager, latestImmutableStateNexus, savedStateController, - currentRoster); + currentRoster, + stateLifecycles); blocks.loadReconnectStateReference().set(reconnectStateLoader::loadReconnectState); blocks.clearAllPipelinesForReconnectReference().set(platformWiring::clear); @@ -512,7 +513,7 @@ public Roster getRoster() { @SuppressWarnings("unchecked") @Override @NonNull - public AutoCloseableWrapper getLatestImmutableState(@NonNull final String reason) { + public AutoCloseableWrapper getLatestImmutableState(@NonNull final String reason) { final ReservedSignedState wrapper = latestImmutableStateNexus.getState(reason); return wrapper == null ? AutoCloseableWrapper.empty() diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/PlatformBuilder.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/PlatformBuilder.java index 8f04b8794476..74a13be51991 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/PlatformBuilder.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/PlatformBuilder.java @@ -53,6 +53,8 @@ import com.swirlds.platform.pool.TransactionPoolNexus; import com.swirlds.platform.roster.RosterHistory; import com.swirlds.platform.scratchpad.Scratchpad; +import com.swirlds.platform.state.PlatformMerkleStateRoot; +import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.state.SwirldStateManager; import com.swirlds.platform.state.iss.IssScratchpad; import com.swirlds.platform.state.signed.ReservedSignedState; @@ -82,6 +84,9 @@ public final class PlatformBuilder { private final String appName; private final SoftwareVersion softwareVersion; private final ReservedSignedState initialState; + + private final StateLifecycles stateLifecycles; + private final NodeId selfId; private final String swirldName; @@ -143,14 +148,15 @@ public final class PlatformBuilder { * the app will pass the loaded state via the initialState argument to this method. If the snapshot doesn't exist, * then the app will create a new genesis state and pass it via the same initialState argument. * - * @param appName the name of the application, currently used for deciding where to store states on - * disk - * @param swirldName the name of the swirld, currently used for deciding where to store states on disk - * @param selfId the ID of this node - * @param softwareVersion the software version of the application - * @param initialState the initial state supplied by the application + * @param appName the name of the application, currently used for deciding where to store states on + * disk + * @param swirldName the name of the swirld, currently used for deciding where to store states on disk + * @param softwareVersion the software version of the application + * @param initialState the initial state supplied by the application + * @param stateLifecycles the state lifecycle events handler + * @param selfId the ID of this node * @param consensusEventStreamName a part of the name of the directory where the consensus event stream is written - * @param rosterHistory the roster history provided by the application to use at startup + * @param rosterHistory the roster history provided by the application to use at startup */ @NonNull public static PlatformBuilder create( @@ -158,30 +164,40 @@ public static PlatformBuilder create( @NonNull final String swirldName, @NonNull final SoftwareVersion softwareVersion, @NonNull final ReservedSignedState initialState, + @NonNull final StateLifecycles stateLifecycles, @NonNull final NodeId selfId, @NonNull final String consensusEventStreamName, @NonNull final RosterHistory rosterHistory) { return new PlatformBuilder( - appName, swirldName, softwareVersion, initialState, selfId, consensusEventStreamName, rosterHistory); + appName, + swirldName, + softwareVersion, + initialState, + stateLifecycles, + selfId, + consensusEventStreamName, + rosterHistory); } /** * Constructor. * - * @param appName the name of the application, currently used for deciding where to store states on - * disk - * @param swirldName the name of the swirld, currently used for deciding where to store states on disk - * @param softwareVersion the software version of the application - * @param initialState the genesis state supplied by application - * @param selfId the ID of this node + * @param appName the name of the application, currently used for deciding where to store states on + * disk + * @param swirldName the name of the swirld, currently used for deciding where to store states on disk + * @param softwareVersion the software version of the application + * @param initialState the genesis state supplied by application + * @param stateLifecycles the state lifecycle events handler + * @param selfId the ID of this node * @param consensusEventStreamName a part of the name of the directory where the consensus event stream is written - * @param rosterHistory the roster history provided by the application to use at startup + * @param rosterHistory the roster history provided by the application to use at startup */ private PlatformBuilder( @NonNull final String appName, @NonNull final String swirldName, @NonNull final SoftwareVersion softwareVersion, @NonNull final ReservedSignedState initialState, + @NonNull final StateLifecycles stateLifecycles, @NonNull final NodeId selfId, @NonNull final String consensusEventStreamName, @NonNull final RosterHistory rosterHistory) { @@ -190,6 +206,7 @@ private PlatformBuilder( this.swirldName = Objects.requireNonNull(swirldName); this.softwareVersion = Objects.requireNonNull(softwareVersion); this.initialState = Objects.requireNonNull(initialState); + this.stateLifecycles = Objects.requireNonNull(stateLifecycles); this.selfId = Objects.requireNonNull(selfId); this.consensusEventStreamName = Objects.requireNonNull(consensusEventStreamName); this.rosterHistory = Objects.requireNonNull(rosterHistory); @@ -413,7 +430,8 @@ public PlatformComponentBuilder buildComponentBuilder() { currentRoster, selfId, x -> statusActionSubmitterAtomicReference.get().submitStatusAction(x), - softwareVersion); + softwareVersion, + stateLifecycles); if (model == null) { final WiringConfig wiringConfig = platformContext.getConfiguration().getConfigData(WiringConfig.class); @@ -468,7 +486,8 @@ public PlatformComponentBuilder buildComponentBuilder() { new AtomicReference<>(), new AtomicReference<>(), new AtomicReference<>(), - firstPlatform); + firstPlatform, + stateLifecycles); return new PlatformComponentBuilder(buildingBlocks); } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/PlatformBuildingBlocks.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/PlatformBuildingBlocks.java index 73287f4aff02..8b8b9e2bafcf 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/PlatformBuildingBlocks.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/PlatformBuildingBlocks.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. @@ -30,6 +30,7 @@ import com.swirlds.platform.pool.TransactionPoolNexus; import com.swirlds.platform.roster.RosterHistory; import com.swirlds.platform.scratchpad.Scratchpad; +import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.state.SwirldStateManager; import com.swirlds.platform.state.iss.IssScratchpad; import com.swirlds.platform.state.signed.ReservedSignedState; @@ -122,7 +123,8 @@ public record PlatformBuildingBlocks( @NonNull AtomicReference> getLatestCompleteStateReference, @NonNull AtomicReference> loadReconnectStateReference, @NonNull AtomicReference clearAllPipelinesForReconnectReference, - boolean firstPlatform) { + boolean firstPlatform, + @NonNull StateLifecycles stateLifecycles) { public PlatformBuildingBlocks { requireNonNull(platformContext); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/PlatformComponentBuilder.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/PlatformComponentBuilder.java index 2647672872eb..53415c3ee9e3 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/PlatformComponentBuilder.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/PlatformComponentBuilder.java @@ -740,7 +740,8 @@ public TransactionPrehandler buildTransactionPrehandler() { if (transactionPrehandler == null) { transactionPrehandler = new DefaultTransactionPrehandler( blocks.platformContext(), - () -> blocks.latestImmutableStateProviderReference().get().apply("transaction prehandle")); + () -> blocks.latestImmutableStateProviderReference().get().apply("transaction prehandle"), + blocks.stateLifecycles()); } return transactionPrehandler; } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/CompareStatesCommand.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/CompareStatesCommand.java index ce5ac3f94c56..e5f66aa27dcd 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/CompareStatesCommand.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/CompareStatesCommand.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2024 Hedera Hashgraph, LLC + * Copyright (C) 2022-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. diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/GenesisPlatformStateCommand.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/GenesisPlatformStateCommand.java index 88566146dc68..034c3a7de019 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/GenesisPlatformStateCommand.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/GenesisPlatformStateCommand.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. diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/ValidateAddressBookStateCommand.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/ValidateAddressBookStateCommand.java index 43be509a695d..7c212efae795 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/ValidateAddressBookStateCommand.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/ValidateAddressBookStateCommand.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. diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/appcomm/DefaultLatestCompleteStateNotifier.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/appcomm/DefaultLatestCompleteStateNotifier.java index 6d684e6de3bd..826970f8c7ec 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/appcomm/DefaultLatestCompleteStateNotifier.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/appcomm/DefaultLatestCompleteStateNotifier.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. @@ -45,7 +45,7 @@ public CompleteStateNotificationWithCleanup latestCompleteStateHandler( } latestStateProvidedRound = reservedSignedState.get().getRound(); final NewSignedStateNotification notification = new NewSignedStateNotification( - reservedSignedState.get().getSwirldState(), + reservedSignedState.get().getState(), reservedSignedState.get().getRound(), reservedSignedState.get().getConsensusTimestamp()); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/eventhandling/DefaultTransactionHandler.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/eventhandling/DefaultTransactionHandler.java index c01d2b661f9c..16c3b2386083 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/eventhandling/DefaultTransactionHandler.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/eventhandling/DefaultTransactionHandler.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. @@ -208,7 +208,7 @@ public StateAndRound handleConsensusRound(@NonNull final ConsensusRound consensu return createSignedState(consensusRound, systemTransactions); } catch (final InterruptedException e) { - logger.error(EXCEPTION.getMarker(), "handleConsensusRound interrupted"); + logger.error(EXCEPTION.getMarker(), "onHandleConsensusRound interrupted"); Thread.currentThread().interrupt(); return null; diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/eventhandling/DefaultTransactionPrehandler.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/eventhandling/DefaultTransactionPrehandler.java index 884991c5eb80..8e0a5983c6a9 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/eventhandling/DefaultTransactionPrehandler.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/eventhandling/DefaultTransactionPrehandler.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. @@ -25,6 +25,7 @@ import com.swirlds.common.context.PlatformContext; import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.event.PlatformEvent; +import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.state.signed.ReservedSignedState; import com.swirlds.platform.stats.AverageTimeStat; import edu.umd.cs.findbugs.annotations.NonNull; @@ -55,6 +56,8 @@ public class DefaultTransactionPrehandler implements TransactionPrehandler { */ private final AverageTimeStat preHandleTime; + private final StateLifecycles stateLifecycles; + private final Time time; /** @@ -63,10 +66,12 @@ public class DefaultTransactionPrehandler implements TransactionPrehandler { * @param platformContext the platform context * @param latestStateSupplier provides access to the latest immutable state, may return null (implementation detail * of locking mechanism within the supplier) + * @param stateLifecycles the state lifecycles */ public DefaultTransactionPrehandler( @NonNull final PlatformContext platformContext, - @NonNull final Supplier latestStateSupplier) { + @NonNull final Supplier latestStateSupplier, + @NonNull StateLifecycles stateLifecycles) { this.time = platformContext.getTime(); this.latestStateSupplier = Objects.requireNonNull(latestStateSupplier); @@ -76,6 +81,7 @@ public DefaultTransactionPrehandler( INTERNAL_CATEGORY, "preHandleMicros", "average time it takes to perform preHandle (in microseconds)"); + this.stateLifecycles = stateLifecycles; } /** @@ -94,9 +100,10 @@ public List> prehandleApplica } try { - latestImmutableState.get().getSwirldState().preHandle(event, NO_OP_CONSUMER); + stateLifecycles.onPreHandle(event, latestImmutableState.get().getState(), NO_OP_CONSUMER); } catch (final Throwable t) { - logger.error(EXCEPTION.getMarker(), "error invoking SwirldState.preHandle() for event {}", event, t); + logger.error( + EXCEPTION.getMarker(), "error invoking StateLifecycles.onPreHandle() for event {}", event, t); } } finally { event.signalPrehandleCompletion(); @@ -105,7 +112,8 @@ public List> prehandleApplica preHandleTime.update(startTime, time.nanoTime()); } - // TODO adapt this logic to read transactions directly from the callback passed in SwirldState.preHandle() when + // TODO adapt this logic to read transactions directly from the callback passed in StateLifecycles.onOreHandle() + // when // implemented return extractFromEvent(event, StateSignatureTransaction.class); } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/SyncGossip.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/SyncGossip.java index 9d74a46998eb..bbc127c84dac 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/SyncGossip.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/SyncGossip.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. diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/listeners/ReconnectCompleteNotification.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/listeners/ReconnectCompleteNotification.java index 1051eb61317f..22b8a69d455a 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/listeners/ReconnectCompleteNotification.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/listeners/ReconnectCompleteNotification.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2024 Hedera Hashgraph, LLC + * Copyright (C) 2016-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,7 @@ package com.swirlds.platform.listeners; import com.swirlds.common.notification.AbstractNotification; -import com.swirlds.platform.system.SwirldState; +import com.swirlds.platform.state.PlatformMerkleStateRoot; import java.time.Instant; /** @@ -27,17 +27,17 @@ public class ReconnectCompleteNotification extends AbstractNotification { private long roundNumber; private Instant consensusTimestamp; - private SwirldState state; + private PlatformMerkleStateRoot state; public ReconnectCompleteNotification( - final long roundNumber, final Instant consensusTimestamp, final SwirldState state) { + final long roundNumber, final Instant consensusTimestamp, final PlatformMerkleStateRoot state) { this.roundNumber = roundNumber; this.consensusTimestamp = consensusTimestamp; this.state = state; } /** - * get round number from the {@link SwirldState} + * get round number from the {@link PlatformMerkleStateRoot} * * @return round number */ @@ -55,11 +55,11 @@ public Instant getConsensusTimestamp() { } /** - * get the {@link SwirldState} instance + * get the {@link PlatformMerkleStateRoot} instance * - * @return SwirldState + * @return PlatformMerkleStateRoot */ - public SwirldState getState() { + public PlatformMerkleStateRoot getState() { return state; } } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/listeners/StateWriteToDiskCompleteNotification.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/listeners/StateWriteToDiskCompleteNotification.java index 3cc795592cd8..19f85809be16 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/listeners/StateWriteToDiskCompleteNotification.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/listeners/StateWriteToDiskCompleteNotification.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2024 Hedera Hashgraph, LLC + * Copyright (C) 2016-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,7 @@ package com.swirlds.platform.listeners; import com.swirlds.common.notification.AbstractNotification; -import com.swirlds.platform.system.SwirldState; +import com.swirlds.platform.state.PlatformMerkleStateRoot; import java.nio.file.Path; import java.time.Instant; @@ -61,7 +61,7 @@ public Instant getConsensusTimestamp() { * @deprecated used by PTT for an obsolete feature */ @Deprecated(forRemoval = true) - public SwirldState getState() { + public PlatformMerkleStateRoot getState() { return null; } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/metrics/SwirldStateMetrics.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/metrics/StateMetrics.java similarity index 81% rename from platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/metrics/SwirldStateMetrics.java rename to platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/metrics/StateMetrics.java index 3ca545d9d6b7..e95672dbaef1 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/metrics/SwirldStateMetrics.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/metrics/StateMetrics.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2024 Hedera Hashgraph, LLC + * Copyright (C) 2022-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. @@ -26,18 +26,19 @@ import com.swirlds.common.metrics.RunningAverageMetric; import com.swirlds.common.metrics.SpeedometerMetric; import com.swirlds.metrics.api.Metrics; +import com.swirlds.platform.state.PlatformMerkleStateRoot; import com.swirlds.platform.system.PlatformStatNames; -import com.swirlds.platform.system.SwirldState; +import com.swirlds.state.merkle.MerkleStateRoot; /** - * Collection of metrics related to SwirldState + * Collection of metrics related to the state lifecycle */ -public class SwirldStateMetrics { +public class StateMetrics { private static final RunningAverageMetric.Config AVG_SEC_TRANS_HANDLED_CONFIG = new RunningAverageMetric.Config( INTERNAL_CATEGORY, "secTransH") - .withDescription( - "avg time to handle a consensus transaction in SwirldState.handleTransaction " + "(in seconds)") + .withDescription("avg time to handle a consensus transaction in StateLifecycles.onHandleTransaction " + + "(in seconds)") .withFormat(FORMAT_10_6); private final RunningAverageMetric avgSecTransHandled; @@ -50,26 +51,26 @@ public class SwirldStateMetrics { private static final SpeedometerMetric.Config TRANS_HANDLED_PER_SECOND_CONFIG = new SpeedometerMetric.Config( INTERNAL_CATEGORY, PlatformStatNames.TRANSACTIONS_HANDLED_PER_SECOND) .withDescription( - "number of consensus transactions per second handled " + "by SwirldState.handleTransaction()") + "number of consensus transactions per second handled " + "by StateLifecycles.onHandleTransaction()") .withFormat(FORMAT_9_6); private final SpeedometerMetric transHandledPerSecond; private static final RunningAverageMetric.Config AVG_STATE_COPY_MICROS_CONFIG = new RunningAverageMetric.Config( INTERNAL_CATEGORY, "stateCopyMicros") - .withDescription("average time it takes the SwirldState.copy() method in SwirldState to finish " - + "(in microseconds)") + .withDescription( + "average time it takes the State.copy() method in StateLifecycles to finish " + "(in microseconds)") .withFormat(FORMAT_16_2); private final RunningAverageMetric avgStateCopyMicros; /** - * Constructor of {@code SwirldStateMetrics} + * Constructor of {@code StateMetrics} * * @param metrics * a reference to the metrics-system * @throws IllegalArgumentException * if {@code metrics} is {@code null} */ - public SwirldStateMetrics(final Metrics metrics) { + public StateMetrics(final Metrics metrics) { avgSecTransHandled = metrics.getOrCreate(AVG_SEC_TRANS_HANDLED_CONFIG); avgConsHandleTime = metrics.getOrCreate(AVG_CONS_HANDLE_TIME_CONFIG); transHandledPerSecond = metrics.getOrCreate(TRANS_HANDLED_PER_SECOND_CONFIG); @@ -77,7 +78,7 @@ public SwirldStateMetrics(final Metrics metrics) { } /** - * Records the amount of time to handle a consensus transaction in {@link SwirldState}. + * Records the amount of time to handle a consensus transaction in {@link PlatformMerkleStateRoot}. * * @param seconds * the amount of time in seconds @@ -87,7 +88,7 @@ public void consensusTransHandleTime(final double seconds) { } /** - * Records the amount of time between a transaction reaching consensus and being handled in {@link SwirldState}. + * Records the amount of time between a transaction reaching consensus and being handled in {@link PlatformMerkleStateRoot}. * * @param seconds * the amount of time in seconds @@ -97,14 +98,14 @@ public void consensusToHandleTime(final double seconds) { } /** - * Records the fact that consensus transactions were handled by {@link SwirldState}. + * Records the fact that consensus transactions were handled by {@link PlatformMerkleStateRoot}. */ public void consensusTransHandled(final int numTrans) { transHandledPerSecond.update(numTrans); } /** - * Records the time it takes {@link SwirldState#copy()} to finish (in microseconds) + * Records the time it takes {@link MerkleStateRoot#copy()} to finish (in microseconds) * * @param micros * the amount of time in microseconds diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/reconnect/ReconnectLearner.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/reconnect/ReconnectLearner.java index 67a2b6c4459b..d067ebf041f3 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/reconnect/ReconnectLearner.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/reconnect/ReconnectLearner.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. @@ -61,6 +61,7 @@ public class ReconnectLearner { private final Duration reconnectSocketTimeout; private final ReconnectMetrics statistics; private final SignedStateValidationData stateValidationData; + private SigSet sigSet; private final PlatformContext platformContext; /** diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/reconnect/ReconnectLearnerFactory.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/reconnect/ReconnectLearnerFactory.java index 47fe3b7bfb34..a00f9c2f64da 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/reconnect/ReconnectLearnerFactory.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/reconnect/ReconnectLearnerFactory.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. diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/recovery/EventRecoveryWorkflow.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/recovery/EventRecoveryWorkflow.java index 90a8f4952096..8baf5022561f 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/recovery/EventRecoveryWorkflow.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/recovery/EventRecoveryWorkflow.java @@ -55,14 +55,15 @@ import com.swirlds.platform.state.PlatformMerkleStateRoot; import com.swirlds.platform.state.PlatformStateAccessor; import com.swirlds.platform.state.PlatformStateModifier; +import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.state.signed.ReservedSignedState; import com.swirlds.platform.state.signed.SignedState; import com.swirlds.platform.state.snapshot.SignedStateFileReader; import com.swirlds.platform.state.snapshot.SignedStateFileWriter; import com.swirlds.platform.system.InitTrigger; import com.swirlds.platform.system.Round; +import com.swirlds.platform.system.SoftwareVersion; import com.swirlds.platform.system.SwirldMain; -import com.swirlds.platform.system.SwirldState; import com.swirlds.platform.system.events.CesEvent; import com.swirlds.platform.system.events.ConsensusEvent; import com.swirlds.platform.system.state.notifications.NewRecoveredStateListener; @@ -261,7 +262,7 @@ public static void updateEmergencyRecoveryFile( private static void notifyStateRecovered( final NotificationEngine notificationEngine, final SignedState recoveredState) { final NewRecoveredStateNotification notification = new NewRecoveredStateNotification( - recoveredState.getSwirldState(), recoveredState.getRound(), recoveredState.getConsensusTimestamp()); + recoveredState.getState(), recoveredState.getRound(), recoveredState.getConsensusTimestamp()); notificationEngine.dispatch(NewRecoveredStateListener.class, notification); } @@ -305,14 +306,16 @@ public static RecoveredState reapplyTransactions( final RecoveryPlatform platform = new RecoveryPlatform(configuration, initialState.get(), selfId, loadSigningKeys); - initialState - .get() - .getSwirldState() - .init( - platform, - InitTrigger.EVENT_STREAM_RECOVERY, - initialState.get().getState().getReadablePlatformState().getCreationSoftwareVersion()); - + StateLifecycles stateLifecycles = appMain.newStateLifecycles(); + SoftwareVersion softwareVersion = + initialState.get().getState().getReadablePlatformState().getCreationSoftwareVersion(); + initialState.get().init(platformContext); + final var notificationEngine = platform.getNotificationEngine(); + notificationEngine.register( + NewRecoveredStateListener.class, + notification -> stateLifecycles.onNewRecoveredState(notification.getState())); + stateLifecycles.onStateInitialized( + initialState.get().getState(), platform, InitTrigger.EVENT_STREAM_RECOVERY, softwareVersion); appMain.init(platform, platform.getSelfId()); ReservedSignedState signedState = initialState; @@ -330,7 +333,11 @@ public static RecoveredState reapplyTransactions( round.getRoundNum()); signedState = handleNextRound( - platformContext, signedState, round, configuration.getConfigData(ConsensusConfig.class)); + stateLifecycles, + platformContext, + signedState, + round, + configuration.getConfigData(ConsensusConfig.class)); platform.setLatestState(signedState.get()); lastEvent = getLastEvent(round); } @@ -365,6 +372,7 @@ public static RecoveredState reapplyTransactions( * @return the resulting signed state */ private static ReservedSignedState handleNextRound( + @NonNull final StateLifecycles stateLifecycles, @NonNull final PlatformContext platformContext, @NonNull final ReservedSignedState previousState, @NonNull final StreamedRound round, @@ -391,11 +399,7 @@ private static ReservedSignedState handleNextRound( v.setCreationSoftwareVersion(previousReadablePlatformState.getCreationSoftwareVersion()); }); - applyTransactions( - previousState.get().getSwirldState().cast(), - newState.cast(), - newState.getWritablePlatformState(), - round); + applyTransactions(stateLifecycles, previousState.get().getState(), newState.cast(), round); final boolean isFreezeState = isFreezeState( previousState.get().getConsensusTimestamp(), @@ -469,22 +473,21 @@ static ConsensusEvent getLastEvent(final Round round) { * * @param immutableState the immutable swirld state for the previous round * @param mutableState the swirld state for the current round - * @param platformState the platform state for the current round * @param round the current round */ static void applyTransactions( - final SwirldState immutableState, - final SwirldState mutableState, - final PlatformStateModifier platformState, + final StateLifecycles stateLifecycles, + final PlatformMerkleStateRoot immutableState, + final PlatformMerkleStateRoot mutableState, final Round round) { mutableState.throwIfImmutable(); for (final ConsensusEvent event : round) { - immutableState.preHandle(event, NO_OP_CONSUMER); + stateLifecycles.onPreHandle(event, immutableState, NO_OP_CONSUMER); } - mutableState.handleConsensusRound(round, platformState, NO_OP_CONSUMER); + stateLifecycles.onHandleConsensusRound(round, mutableState, NO_OP_CONSUMER); // FUTURE WORK: there are currently no system transactions that are capable of modifying // the state. If/when system transactions capable of modifying state are added, this workflow diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/recovery/internal/RecoveryPlatform.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/recovery/internal/RecoveryPlatform.java index c5ff6b2063b2..76b8fdfb0b83 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/recovery/internal/RecoveryPlatform.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/recovery/internal/RecoveryPlatform.java @@ -34,8 +34,8 @@ import com.swirlds.platform.state.signed.SignedState; import com.swirlds.platform.state.signed.SignedStateReference; import com.swirlds.platform.system.Platform; -import com.swirlds.platform.system.SwirldState; import com.swirlds.platform.system.address.AddressBook; +import com.swirlds.state.merkle.MerkleStateRoot; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.Collections; import java.util.Objects; @@ -155,13 +155,12 @@ public NodeId getSelfId() { @SuppressWarnings("unchecked") @Override @NonNull - public synchronized AutoCloseableWrapper getLatestImmutableState( - @NonNull final String reason) { + public AutoCloseableWrapper getLatestImmutableState(@NonNull String reason) { final ReservedSignedState reservedSignedState = immutableState.getAndReserve(reason); return new AutoCloseableWrapper<>( reservedSignedState.isNull() ? null - : (T) reservedSignedState.get().getSwirldState(), + : (T) reservedSignedState.get().getState(), reservedSignedState::close); } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/NoOpStateLifecycles.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/NoOpStateLifecycles.java new file mode 100644 index 000000000000..cb0a53f705a1 --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/NoOpStateLifecycles.java @@ -0,0 +1,81 @@ +/* + * 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.swirlds.platform.state; + +import com.hedera.hapi.platform.event.StateSignatureTransaction; +import com.swirlds.common.context.PlatformContext; +import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; +import com.swirlds.platform.system.InitTrigger; +import com.swirlds.platform.system.Platform; +import com.swirlds.platform.system.Round; +import com.swirlds.platform.system.SoftwareVersion; +import com.swirlds.platform.system.address.AddressBook; +import com.swirlds.platform.system.events.Event; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.function.Consumer; + +/** + * A no-op implementation of {@link StateLifecycles} that does nothing. + * It's useful for auxiliary code that doesn't handle new transactions (State Editor, State commands, Event Recovery workflow, etc.). + */ +public enum NoOpStateLifecycles implements StateLifecycles { + NO_OP_STATE_LIFECYCLES; + + @Override + public void onPreHandle( + @NonNull Event event, + @NonNull PlatformMerkleStateRoot state, + @NonNull Consumer> stateSignatureTransactionCallback) { + // no-op + } + + @Override + public void onHandleConsensusRound( + @NonNull Round round, + @NonNull PlatformMerkleStateRoot state, + @NonNull Consumer> stateSignatureTransactionCallback) { + // no-op + } + + @Override + public void onSealConsensusRound(@NonNull Round round, @NonNull PlatformMerkleStateRoot state) { + // no-op + } + + @Override + public void onStateInitialized( + @NonNull PlatformMerkleStateRoot state, + @NonNull Platform platform, + @NonNull InitTrigger trigger, + @Nullable SoftwareVersion previousVersion) { + // no-op + } + + @Override + public void onUpdateWeight( + @NonNull PlatformMerkleStateRoot state, + @NonNull AddressBook configAddressBook, + @NonNull PlatformContext context) { + // no-op + } + + @Override + public void onNewRecoveredState(@NonNull PlatformMerkleStateRoot recoveredState) { + // no-op + } +} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/PlatformMerkleStateRoot.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/PlatformMerkleStateRoot.java index 64d3c18b3980..33254e4e34f1 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/PlatformMerkleStateRoot.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/PlatformMerkleStateRoot.java @@ -18,43 +18,30 @@ import static com.swirlds.platform.state.MerkleStateUtils.createInfoString; import static com.swirlds.platform.state.service.schemas.V0540PlatformStateSchema.PLATFORM_STATE_KEY; -import static com.swirlds.platform.system.InitTrigger.EVENT_STREAM_RECOVERY; import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.SemanticVersion; -import com.hedera.hapi.platform.event.StateSignatureTransaction; import com.hedera.hapi.platform.state.PlatformState; import com.swirlds.common.constructable.ConstructableIgnored; -import com.swirlds.common.context.PlatformContext; -import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.state.service.PlatformStateService; import com.swirlds.platform.state.service.ReadablePlatformStateStore; import com.swirlds.platform.state.service.SnapshotPlatformStateAccessor; import com.swirlds.platform.state.service.WritablePlatformStateStore; import com.swirlds.platform.state.service.schemas.V0540PlatformStateSchema; -import com.swirlds.platform.system.InitTrigger; -import com.swirlds.platform.system.Platform; -import com.swirlds.platform.system.Round; import com.swirlds.platform.system.SoftwareVersion; import com.swirlds.platform.system.SwirldMain; -import com.swirlds.platform.system.SwirldState; -import com.swirlds.platform.system.address.AddressBook; -import com.swirlds.platform.system.events.Event; -import com.swirlds.platform.system.state.notifications.NewRecoveredStateListener; import com.swirlds.state.State; import com.swirlds.state.merkle.MerkleStateRoot; import com.swirlds.state.merkle.singleton.SingletonNode; import com.swirlds.state.spi.ReadableStates; import com.swirlds.state.spi.WritableStates; import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.Nullable; -import java.util.function.Consumer; import java.util.function.Function; /** - * An implementation of {@link SwirldState} and {@link State}. The Hashgraph Platform + * An implementation of {@link State}. The Hashgraph Platform * communicates with the application through {@link SwirldMain} and {@link - * SwirldState}. The Hedera application, after startup, only needs the ability to get {@link + * StateLifecycles}. The Hedera application, after startup, only needs the ability to get {@link * ReadableStates} and {@link WritableStates} from this object. * *

    Among {@link MerkleStateRoot}'s child nodes are the various {@link @@ -70,111 +57,31 @@ * consider nesting service nodes in a MerkleMap, or some other such approach to get a binary tree. */ @ConstructableIgnored -public class PlatformMerkleStateRoot extends MerkleStateRoot implements SwirldState { +public class PlatformMerkleStateRoot extends MerkleStateRoot { private static final long CLASS_ID = 0x8e300b0dfdafbb1aL; - /** - * The callbacks for Hedera lifecycle events. - */ - private final StateLifecycles lifecycles; private final Function versionFactory; /** * Create a new instance. This constructor must be used for all creations of this class. * - * @param lifecycles The lifecycle callbacks. Cannot be null. * @param versionFactory a factory for creating {@link SoftwareVersion} based on provided {@link SemanticVersion} */ - public PlatformMerkleStateRoot( - @NonNull StateLifecycles lifecycles, @NonNull Function versionFactory) { - this.lifecycles = requireNonNull(lifecycles); + public PlatformMerkleStateRoot(@NonNull Function versionFactory) { this.versionFactory = requireNonNull(versionFactory); } protected PlatformMerkleStateRoot(@NonNull PlatformMerkleStateRoot from) { super(from); - this.lifecycles = from.lifecycles; this.versionFactory = from.versionFactory; } - /** - * {@inheritDoc} - *

    - * Called by the platform whenever the state should be initialized. This can happen at genesis startup, - * on restart, on reconnect, or any other time indicated by the {@code trigger}. - */ - @Override - public void init( - @NonNull final Platform platform, - @NonNull final InitTrigger trigger, - @Nullable final SoftwareVersion deserializedVersion) { - final PlatformContext platformContext = platform.getContext(); - super.init(platformContext.getTime(), platformContext.getMetrics(), platformContext.getMerkleCryptography()); - - // If we are initialized for event stream recovery, we have to register an - // extra listener to make sure we call all the required Hedera lifecycles - if (trigger == EVENT_STREAM_RECOVERY) { - final var notificationEngine = platform.getNotificationEngine(); - notificationEngine.register( - NewRecoveredStateListener.class, - notification -> lifecycles.onNewRecoveredState(notification.getSwirldState())); - } - // At some point this method will no longer be defined on SwirldState2, because we want to move - // to a model where SwirldState/SwirldState2 are simply data objects, without this lifecycle. - // Instead, this method will be a callback the app registers with the platform. So for now, - // we simply call the callback handler, which is implemented by the app. - lifecycles.onStateInitialized(this, platform, trigger, deserializedVersion); - } - - /** - * {@inheritDoc} - */ - @NonNull - @Override - public AddressBook updateWeight( - @NonNull final AddressBook configAddressBook, @NonNull final PlatformContext context) { - lifecycles.onUpdateWeight(this, configAddressBook, context); - return configAddressBook; - } - @Override protected PlatformMerkleStateRoot copyingConstructor() { return new PlatformMerkleStateRoot(this); } - /** - * {@inheritDoc} - */ - @Override - public void handleConsensusRound( - @NonNull final Round round, - @NonNull final PlatformStateModifier platformState, - @NonNull final Consumer> stateSignatureTransaction) { - throwIfImmutable(); - lifecycles.onHandleConsensusRound(round, this, stateSignatureTransaction); - } - - /** - * {@inheritDoc} - */ - @Override - public void sealConsensusRound(@NonNull final Round round) { - requireNonNull(round); - throwIfImmutable(); - lifecycles.onSealConsensusRound(round, this); - } - - /** - * {@inheritDoc} - */ - @Override - public void preHandle( - @NonNull final Event event, - @NonNull final Consumer> stateSignatureTransaction) { - lifecycles.onPreHandle(event, this, stateSignatureTransaction); - } - /** * Returns a factory constructing instances of {@link SoftwareVersion} based on provided {@link SemanticVersion}. */ diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/StateLifecycles.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/StateLifecycles.java index cd3e404ef24d..3bf625aeb108 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/StateLifecycles.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/StateLifecycles.java @@ -25,18 +25,18 @@ import com.swirlds.platform.system.SoftwareVersion; import com.swirlds.platform.system.address.AddressBook; import com.swirlds.platform.system.events.Event; -import com.swirlds.state.State; +import com.swirlds.state.merkle.MerkleStateRoot; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.util.function.Consumer; /** - * Implements the major lifecycle events for the state. + * Implements the major lifecycle events for the state. Normally, the implementation of this interface should be + * stateless or effectively immutable, unless it's a special test implementation. An instance of this class is + * meant to be created once at the start of the application and then used for the lifetime of the application. * - *

    Currently these are implied by the {@link com.swirlds.platform.system.SwirldState} - * interface; but in the future will be callbacks registered with a platform builder. */ -public interface StateLifecycles { +public interface StateLifecycles { /** * Called when an event is added to the hashgraph used to compute consensus ordering * for this node. @@ -47,7 +47,7 @@ public interface StateLifecycles { */ void onPreHandle( @NonNull Event event, - @NonNull State state, + @NonNull T state, @NonNull Consumer> stateSignatureTransactionCallback); /** @@ -59,14 +59,14 @@ void onPreHandle( */ void onHandleConsensusRound( @NonNull Round round, - @NonNull State state, + @NonNull T state, @NonNull Consumer> stateSignatureTransactionCallback); /** * Called by the platform after it has made all its changes to this state for the given round. * @param round the round whose platform state changes are completed */ - void onSealConsensusRound(@NonNull Round round, @NonNull State state); + void onSealConsensusRound(@NonNull Round round, @NonNull T state); /** * Called when the platform is initializing the network state. @@ -77,7 +77,7 @@ void onHandleConsensusRound( * @param previousVersion if non-null, the network version that was previously in use */ void onStateInitialized( - @NonNull State state, + @NonNull T state, @NonNull Platform platform, @NonNull InitTrigger trigger, @Nullable SoftwareVersion previousVersion); @@ -92,12 +92,12 @@ void onStateInitialized( * @param context the current platform context */ @Deprecated(forRemoval = true) - void onUpdateWeight(@NonNull State state, @NonNull AddressBook configAddressBook, @NonNull PlatformContext context); + void onUpdateWeight(@NonNull T state, @NonNull AddressBook configAddressBook, @NonNull PlatformContext context); /** * Called when event stream recovery finishes. * * @param recoveredState the recovered state after reapplying all events */ - void onNewRecoveredState(@NonNull State recoveredState); + void onNewRecoveredState(@NonNull T recoveredState); } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/SwirldStateManager.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/SwirldStateManager.java index 7630af120a77..0b4048a10fb1 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/SwirldStateManager.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/SwirldStateManager.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. @@ -18,6 +18,7 @@ import static com.swirlds.platform.components.transaction.system.SystemTransactionExtractionUtils.extractFromRound; import static com.swirlds.platform.state.SwirldStateManagerUtils.fastCopy; +import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.state.roster.Roster; import com.hedera.hapi.platform.event.StateSignatureTransaction; @@ -26,29 +27,28 @@ import com.swirlds.platform.FreezePeriodChecker; import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.internal.ConsensusRound; -import com.swirlds.platform.metrics.SwirldStateMetrics; +import com.swirlds.platform.metrics.StateMetrics; import com.swirlds.platform.state.signed.SignedState; import com.swirlds.platform.system.Round; import com.swirlds.platform.system.SoftwareVersion; -import com.swirlds.platform.system.SwirldState; import com.swirlds.platform.system.status.StatusActionSubmitter; import com.swirlds.platform.uptime.UptimeTracker; +import com.swirlds.state.merkle.MerkleStateRoot; import edu.umd.cs.findbugs.annotations.NonNull; import java.time.Instant; import java.util.List; -import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; /** - * Manages all interactions with the state object required by {@link SwirldState}. + * Manages all interactions with the state object required by {@link StateLifecycles}. */ public class SwirldStateManager implements FreezePeriodChecker { /** - * Stats relevant to SwirldState operations. + * Stats relevant to the state operations. */ - private final SwirldStateMetrics stats; + private final StateMetrics stats; /** * reference to the state that reflects all known consensus transactions @@ -75,6 +75,8 @@ public class SwirldStateManager implements FreezePeriodChecker { */ private final SoftwareVersion softwareVersion; + private final StateLifecycles stateLifecycles; + /** * Constructor. * @@ -83,47 +85,53 @@ public class SwirldStateManager implements FreezePeriodChecker { * @param selfId this node's id * @param statusActionSubmitter enables submitting platform status actions * @param softwareVersion the current software version + * @param stateLifecycles the state lifecycles */ public SwirldStateManager( @NonNull final PlatformContext platformContext, @NonNull final Roster roster, @NonNull final NodeId selfId, @NonNull final StatusActionSubmitter statusActionSubmitter, - @NonNull final SoftwareVersion softwareVersion) { - - Objects.requireNonNull(platformContext); - Objects.requireNonNull(roster); - Objects.requireNonNull(selfId); - this.stats = new SwirldStateMetrics(platformContext.getMetrics()); - Objects.requireNonNull(statusActionSubmitter); - this.softwareVersion = Objects.requireNonNull(softwareVersion); + @NonNull final SoftwareVersion softwareVersion, + @NonNull final StateLifecycles stateLifecycles) { + requireNonNull(platformContext); + requireNonNull(roster); + requireNonNull(selfId); + requireNonNull(stateLifecycles); + + this.stateLifecycles = stateLifecycles; + this.stats = new StateMetrics(platformContext.getMetrics()); + requireNonNull(statusActionSubmitter); + this.softwareVersion = requireNonNull(softwareVersion); this.transactionHandler = new TransactionHandler(selfId, stats); this.uptimeTracker = new UptimeTracker(platformContext, roster, statusActionSubmitter, selfId, platformContext.getTime()); } /** - * Set the initial state for the platform. This method should only be called once. + * Set the initial eventHandler for the platform. This method should only be called once. * * @param state the initial state */ public void setInitialState(@NonNull final PlatformMerkleStateRoot state) { - Objects.requireNonNull(state); + requireNonNull(state); + state.throwIfDestroyed("state must not be destroyed"); state.throwIfImmutable("state must be mutable"); if (stateRef.get() != null) { - throw new IllegalStateException("Attempt to set initial state when there is already a state reference."); + throw new IllegalStateException( + "Attempt to set initial eventHandler when there is already a eventHandler reference."); } - // Create a fast copy so there is always an immutable state to + // Create a fast copy so there is always an immutable eventHandler to // invoke handleTransaction on for pre-consensus transactions fastCopyAndUpdateRefs(state); } /** * Handles the events in a consensus round. Implementations are responsible for invoking - * {@link SwirldState#handleConsensusRound(Round, PlatformStateModifier, Consumer>)}. + * {@link StateLifecycles#onHandleConsensusRound(Round, MerkleStateRoot, Consumer)} . * * @param round the round to handle */ @@ -131,10 +139,10 @@ public List> handleConsensusR final PlatformMerkleStateRoot state = stateRef.get(); uptimeTracker.handleRound(round); - transactionHandler.handleRound(round, state); + transactionHandler.handleRound(round, stateLifecycles, state); // TODO update this logic to return the transactions from the callback consumer passed in - // state.getSwirldState().handleConsensusRound, when it is implemented + // stateLifecycles.onHandleConsensusRound, when it is implemented return extractFromRound(round, StateSignatureTransaction.class); } @@ -143,9 +151,9 @@ public List> handleConsensusR * @param round the round to seal */ public void sealConsensusRound(@NonNull final Round round) { - Objects.requireNonNull(round); + requireNonNull(round); final PlatformMerkleStateRoot state = stateRef.get(); - state.sealConsensusRound(round); + stateLifecycles.onSealConsensusRound(round, state); } /** @@ -176,7 +184,7 @@ public void savedStateInFreezePeriod() { * @param signedState the signed state to load */ public void loadFromSignedState(@NonNull final SignedState signedState) { - final PlatformMerkleStateRoot state = signedState.getState(); + PlatformMerkleStateRoot state = signedState.getState(); state.throwIfDestroyed("state must not be destroyed"); state.throwIfImmutable("state must be mutable"); @@ -185,21 +193,21 @@ public void loadFromSignedState(@NonNull final SignedState signedState) { } private void fastCopyAndUpdateRefs(final PlatformMerkleStateRoot state) { - final PlatformMerkleStateRoot consState = fastCopy(state, stats, softwareVersion); + PlatformMerkleStateRoot newState = fastCopy(state, stats, softwareVersion); - // Set latest immutable first to prevent the newly immutable state from being deleted between setting the + // Set latest immutable first to prevent the newly immutable stateRoot from being deleted between setting the // stateRef and the latestImmutableState setLatestImmutableState(state); - setState(consState); + setState(newState); } /** * Sets the consensus state to the state provided. Must be mutable and have a reference count of at least 1. * - * @param state the new mutable state + * @param state a new mutable state */ private void setState(final PlatformMerkleStateRoot state) { - final PlatformMerkleStateRoot currVal = stateRef.get(); + final var currVal = stateRef.get(); if (currVal != null) { currVal.release(); } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/SwirldStateManagerUtils.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/SwirldStateManagerUtils.java index efbff35b36a4..27236b84749d 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/SwirldStateManagerUtils.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/SwirldStateManagerUtils.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. @@ -18,7 +18,7 @@ import static com.swirlds.base.units.UnitConstants.NANOSECONDS_TO_MICROSECONDS; -import com.swirlds.platform.metrics.SwirldStateMetrics; +import com.swirlds.platform.metrics.StateMetrics; import com.swirlds.platform.system.SoftwareVersion; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; @@ -43,7 +43,7 @@ private SwirldStateManagerUtils() {} */ public static PlatformMerkleStateRoot fastCopy( @NonNull final PlatformMerkleStateRoot state, - @NonNull final SwirldStateMetrics stats, + @NonNull final StateMetrics stats, @NonNull final SoftwareVersion softwareVersion) { Objects.requireNonNull(softwareVersion); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/TransactionHandler.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/TransactionHandler.java index 9f40a4671f86..79aedca8590a 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/TransactionHandler.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/TransactionHandler.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. @@ -22,7 +22,7 @@ import com.swirlds.common.platform.NodeId; import com.swirlds.platform.internal.ConsensusRound; -import com.swirlds.platform.metrics.SwirldStateMetrics; +import com.swirlds.platform.metrics.StateMetrics; import java.time.Instant; import java.time.temporal.ChronoUnit; import org.apache.logging.log4j.LogManager; @@ -36,28 +36,30 @@ public class TransactionHandler { /** The id of this node. */ private final NodeId selfId; - /** Stats relevant to SwirldState operations. */ - private final SwirldStateMetrics stats; + /** Stats relevant to the state operations. */ + private final StateMetrics stats; - public TransactionHandler(final NodeId selfId, final SwirldStateMetrics stats) { + public TransactionHandler(final NodeId selfId, final StateMetrics stats) { this.selfId = selfId; this.stats = stats; } /** - * Applies a consensus round to SwirldState, handles any exceptions gracefully, and updates relevant statistics. + * Applies a consensus round to the state, handles any exceptions gracefully, and updates relevant statistics. * * @param round * the round to apply - * @param state - * the state to apply {@code round} to + * @param stateLifecycles + * the stateLifecycles to apply {@code round} to + * @param stateRoot the state root to apply {@code round} to */ - public void handleRound(final ConsensusRound round, final PlatformMerkleStateRoot state) { + public void handleRound( + final ConsensusRound round, final StateLifecycles stateLifecycles, final T stateRoot) { try { final Instant timeOfHandle = Instant.now(); final long startTime = System.nanoTime(); - state.handleConsensusRound(round, state.getWritablePlatformState(), NO_OP_CONSUMER); + stateLifecycles.onHandleConsensusRound(round, stateRoot, NO_OP_CONSUMER); final double secondsElapsed = (System.nanoTime() - startTime) * NANOSECONDS_TO_SECONDS; @@ -73,7 +75,7 @@ public void handleRound(final ConsensusRound round, final PlatformMerkleStateRoo } catch (final Throwable t) { logger.error( EXCEPTION.getMarker(), - "error invoking SwirldState.handleConsensusRound() [ nodeId = {} ] with round {}", + "error invoking StateLifecycles.onHandleConsensusRound() [ nodeId = {} ] with round {}", selfId, round.getRoundNum(), t); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/address/AddressBookInitializer.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/address/AddressBookInitializer.java index 9e174784b375..da35174f9827 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/address/AddressBookInitializer.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/address/AddressBookInitializer.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. @@ -24,6 +24,7 @@ import com.swirlds.common.context.PlatformContext; import com.swirlds.common.platform.NodeId; import com.swirlds.platform.config.AddressBookConfig; +import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.state.signed.SignedState; import com.swirlds.platform.system.SoftwareVersion; import com.swirlds.platform.system.address.Address; @@ -75,6 +76,9 @@ public class AddressBookInitializer { /** The context for the platform */ @NonNull private final PlatformContext platformContext; + + @NonNull + private final StateLifecycles stateLifecycles; /** The current version of the application from config.txt. */ @NonNull private final SoftwareVersion currentVersion; @@ -107,7 +111,7 @@ public class AddressBookInitializer { /** * Constructs an AddressBookInitializer to initialize an address book from config.txt, the saved state from disk, or - * the SwirldState on upgrade. + * the state on upgrade. * * @param selfId The id of this node. * @param currentVersion The current version of the application. @@ -122,12 +126,14 @@ public AddressBookInitializer( final boolean softwareUpgrade, @NonNull final SignedState initialState, @NonNull final AddressBook configAddressBook, - @NonNull final PlatformContext platformContext) { + @NonNull final PlatformContext platformContext, + @NonNull final StateLifecycles stateLifecycles) { this.selfId = Objects.requireNonNull(selfId, "The selfId must not be null."); this.currentVersion = Objects.requireNonNull(currentVersion, "The currentVersion must not be null."); this.softwareUpgrade = softwareUpgrade; this.configAddressBook = Objects.requireNonNull(configAddressBook, "The configAddressBook must not be null."); this.platformContext = Objects.requireNonNull(platformContext, "The platformContext must not be null."); + this.stateLifecycles = stateLifecycles; final AddressBookConfig addressBookConfig = platformContext.getConfiguration().getConfigData(AddressBookConfig.class); this.initialState = Objects.requireNonNull(initialState, "The initialState must not be null."); @@ -229,10 +235,10 @@ private InitializedAddressBooks initialize() { logger.info( STARTUP.getMarker(), "The address book weight may be updated by the application using data from the state snapshot."); - candidateAddressBook = initialState - .getSwirldState() - .updateWeight(configAddressBook.copy(), platformContext) - .copy(); + + AddressBook configAddressBookCopy = configAddressBook.copy(); + stateLifecycles.onUpdateWeight(initialState.getState(), configAddressBookCopy, platformContext); + candidateAddressBook = configAddressBookCopy; candidateAddressBook = checkCandidateAddressBookValidity(candidateAddressBook); previousAddressBook = stateAddressBook; copyCertsIfAbsent(configAddressBook, stateAddressBook); @@ -306,7 +312,7 @@ private AddressBook checkCandidateAddressBookValidity(@Nullable final AddressBoo private synchronized void recordAddressBooks(@NonNull final AddressBook usedAddressBook) { final String date = DATE_TIME_FORMAT.format(Instant.now()); final String addressBookFileName = - "%s_v%s_%s_node_%s.txt".formatted(ADDRESS_BOOK_FILE_PREFIX, currentVersion, date, selfId); + "%s_v%s_%s_node_%s.txt".formatted(ADDRESS_BOOK_FILE_PREFIX, currentVersion.getVersion(), date, selfId); final String addressBookDebugFileName = addressBookFileName + ".debug"; try { final File debugFile = Path.of(this.pathToAddressBookDirectory.toString(), addressBookDebugFileName) diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/editor/StateEditor.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/editor/StateEditor.java index 0125ceadbeb8..926051745262 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/editor/StateEditor.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/editor/StateEditor.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2024 Hedera Hashgraph, LLC + * Copyright (C) 2016-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. diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/signed/SignedState.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/signed/SignedState.java index 0f1da7772bf9..b2fbeebb182c 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/signed/SignedState.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/signed/SignedState.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. @@ -28,6 +28,7 @@ import com.hedera.hapi.node.state.roster.Roster; import com.hedera.hapi.node.state.roster.RosterEntry; import com.swirlds.base.time.Time; +import com.swirlds.common.context.PlatformContext; import com.swirlds.common.crypto.Signature; import com.swirlds.common.platform.NodeId; import com.swirlds.common.utility.ReferenceCounter; @@ -42,7 +43,6 @@ import com.swirlds.platform.state.PlatformMerkleStateRoot; import com.swirlds.platform.state.signed.SignedStateHistory.SignedStateAction; import com.swirlds.platform.state.snapshot.StateToDiskReason; -import com.swirlds.platform.system.SwirldState; import com.swirlds.platform.system.address.Address; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; @@ -192,11 +192,10 @@ public SignedState( final boolean freezeState, final boolean deleteOnBackgroundThread, final boolean pcesRound) { - state.reserve(); this.signatureVerifier = requireNonNull(signatureVerifier); - this.state = state; + this.state = requireNonNull(state); final StateConfig stateConfig = configuration.getConfigData(StateConfig.class); if (stateConfig.stateHistoryEnabled()) { @@ -214,6 +213,10 @@ public SignedState( this.pcesRound = pcesRound; } + public void init(@NonNull PlatformContext platformContext) { + state.init(platformContext.getTime(), platformContext.getMetrics(), platformContext.getMerkleCryptography()); + } + /** * {@inheritDoc} */ @@ -476,15 +479,6 @@ public String toString() { return creationTimestamp; } - /** - * Get the root node of the application's state - * - * @return the root node of the application's state. - */ - public @NonNull SwirldState getSwirldState() { - return state; - } - /** * Check if this is a state that needs to be eventually written to disk. * diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/signed/StartupStateUtils.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/signed/StartupStateUtils.java index f6414825dac2..d04397673803 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/signed/StartupStateUtils.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/signed/StartupStateUtils.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. diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/SignedStateFileReader.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/SignedStateFileReader.java index e98b289cea32..6463868c1cda 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/SignedStateFileReader.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/SignedStateFileReader.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. diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/Platform.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/Platform.java index e496b1da80e5..901abaaeb517 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/Platform.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/Platform.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2024 Hedera Hashgraph, LLC + * Copyright (C) 2019-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. @@ -22,6 +22,7 @@ import com.swirlds.common.notification.NotificationEngine; import com.swirlds.common.platform.NodeId; import com.swirlds.common.utility.AutoCloseableWrapper; +import com.swirlds.state.merkle.MerkleStateRoot; import edu.umd.cs.findbugs.annotations.NonNull; /** @@ -71,7 +72,7 @@ public interface Platform { * @return a wrapper around the most recent immutable state */ @NonNull - AutoCloseableWrapper getLatestImmutableState(@NonNull final String reason); + AutoCloseableWrapper getLatestImmutableState(@NonNull final String reason); /** * This method can be called to create a new transaction. If accepted by this method, the newly-created transaction diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/PlatformStatNames.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/PlatformStatNames.java index 8fbca3208f2b..b165d9b2fc5c 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/PlatformStatNames.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/PlatformStatNames.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2024 Hedera Hashgraph, LLC + * Copyright (C) 2016-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. @@ -20,7 +20,7 @@ * A class that holds all the user readable names */ public abstract class PlatformStatNames { - /** number of consensus transactions per second handled by SwirldState.handleConsensusRound() */ + /** number of consensus transactions per second handled by StateLifecycles.onHandleConsensusRound() */ public static final String TRANSACTIONS_HANDLED_PER_SECOND = "transH_per_sec"; /** number of consensus events in queue waiting to be handled */ public static final String CONSENSUS_QUEUE_SIZE = "consEvents"; diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/SwirldMain.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/SwirldMain.java index 6bd799f9348f..895e28b25dd9 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/SwirldMain.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/SwirldMain.java @@ -20,6 +20,8 @@ import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.common.platform.NodeId; import com.swirlds.platform.state.PlatformMerkleStateRoot; +import com.swirlds.platform.state.StateLifecycles; +import com.swirlds.state.merkle.MerkleStateRoot; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.List; @@ -27,7 +29,7 @@ * To implement a swirld, create a class that implements SwirldMain. Its constructor should have no parameters, and its * run() method should run until the user quits the swirld. */ -public interface SwirldMain extends Runnable { +public interface SwirldMain extends Runnable { /** * Get configuration types to be registered. @@ -47,8 +49,8 @@ default List> getConfigDataTypes() { *

    * *

    - * Any changes necessary to initialize {@link SwirldState} should be made in - * {@link SwirldState#init(Platform, InitTrigger, SoftwareVersion)} + * Any changes necessary to initialize {@link PlatformMerkleStateRoot} should be made in + * {@link StateLifecycles#onStateInitialized(MerkleStateRoot, Platform, InitTrigger, SoftwareVersion)} *

    * * @param platform the Platform that instantiated this SwirldMain @@ -69,7 +71,13 @@ default List> getConfigDataTypes() { * @return merkle state tree root node */ @NonNull - PlatformMerkleStateRoot newMerkleStateRoot(); + T newMerkleStateRoot(); + + /** + * Instantiate and return a new instance of the state lifecycles for this SwirldMain object. + * @return state lifecycles + */ + StateLifecycles newStateLifecycles(); /** *

    diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/SwirldState.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/SwirldState.java deleted file mode 100644 index d2d09585d282..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/SwirldState.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (C) 2016-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.swirlds.platform.system; - -import com.hedera.hapi.platform.event.StateSignatureTransaction; -import com.swirlds.common.context.PlatformContext; -import com.swirlds.common.merkle.MerkleNode; -import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; -import com.swirlds.platform.state.PlatformStateModifier; -import com.swirlds.platform.system.address.AddressBook; -import com.swirlds.platform.system.events.Event; -import com.swirlds.platform.system.transaction.Transaction; -import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.Nullable; -import java.util.Objects; -import java.util.function.Consumer; - -/** - * A Swirld app is defined by creating two classes, one implementing {@link SwirldMain}, and the other - * {@link SwirldState}. The class that implements the SwirldState should have a zero-argument constructor. - */ -public interface SwirldState extends MerkleNode { - - /** - *

    - * Initialize a state. Called exactly once each time a node creates/recreates a state (e.g. restart, reconnect, - * genesis). - *

    - * - *

    - * If applicable, the application should check to see if the address book has changed, and if so those changes - * should be handled (in the future the address book will not be changed in this method). It may also be convenient - * for the application to initialize internal data structures at this time. - *

    - * - * @param platform the Platform that instantiated this state - * @param trigger describes the reason why the state was created/recreated - * @param previousSoftwareVersion the previous version of the software, {@link SoftwareVersion#NO_VERSION} if this - * is genesis or if migrating from code from before the concept of an application - * software version - */ - default void init( - @NonNull final Platform platform, - @NonNull final InitTrigger trigger, - @Nullable final SoftwareVersion previousSoftwareVersion) { - // Override if needed - } - - /** - * Provides the application an opportunity to perform operations on transactions in an event prior to handling. - * Called against a given {@link Event} only once, globally (not once per state instance) This method may modify the - * {@link Transaction}s in the event by doing nothing, adding additional signatures, removing existing signatures, - * replacing signatures with versions that expand the public key from an application specific identifier to an - * actual public key, or attaching metadata. Additional signatures extracted from the transaction payload can also - * be added to the list of signatures to be verified. - *

    - * This method is always invoked on an immutable state. - * - * @param event the event to perform pre-handling on - * @param stateSignatureTransaction a consumer that accepts a {@link ScopedSystemTransaction} that - * will be used for callbacks - */ - default void preHandle( - final Event event, - final Consumer> stateSignatureTransaction) {} - - /** - * This method should apply the transactions in the provided round to the state. Only called on mutable states. - * - * @param round the round to apply - * @param platformState the platform state - * @param stateSignatureTransaction a consumer that accepts a {@link ScopedSystemTransaction} that - * will be used for callbacks - */ - void handleConsensusRound( - final Round round, - final PlatformStateModifier platformState, - final Consumer> stateSignatureTransaction); - - /** - * Called by the platform after it has made all its changes to this state for the given round. - * @param round the round whose platform state changes are completed - */ - default void sealConsensusRound(@NonNull final Round round) { - // No-op, only implemented by applications that externalize state changes - } - - /** - * Implementations of the SwirldState should always override this method in production. The AddressBook returned - * should have the same Address entries as the configuration AddressBook, but with the weight values updated. - *

    - * The default implementation of this method is provided for use in testing and to prevent compilation failure of - * implementing classes that have not yet implemented this method. - * - * @param configAddressBook the address book as loaded from config.txt. This address book may contain new nodes not - * present in the stateAddressBook. Must not be null. - * @param context the platform context. Must not be null. - * @return a copy of the configuration address book with updated weight. - */ - @NonNull - default AddressBook updateWeight( - @NonNull final AddressBook configAddressBook, @NonNull final PlatformContext context) { - Objects.requireNonNull(configAddressBook, "configAddressBook must not be null"); - Objects.requireNonNull(context, "context must not be null"); - return configAddressBook; - } - - /** - * {@inheritDoc} - */ - @Override - SwirldState copy(); -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/address/AddressBookUtils.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/address/AddressBookUtils.java index 8a1d5fd6ed23..377cfe8f6016 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/address/AddressBookUtils.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/address/AddressBookUtils.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. @@ -26,6 +26,7 @@ import com.swirlds.common.platform.NodeId; import com.swirlds.platform.roster.RosterRetriever; import com.swirlds.platform.roster.RosterUtils; +import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.state.address.AddressBookInitializer; import com.swirlds.platform.state.signed.ReservedSignedState; import com.swirlds.platform.system.SoftwareVersion; @@ -246,11 +247,18 @@ public static ServiceEndpoint endpointFor(@NonNull final String host, final int @NonNull final SoftwareVersion version, @NonNull final ReservedSignedState initialState, @NonNull final AddressBook bootstrapAddressBook, - @NonNull final PlatformContext platformContext) { + @NonNull final PlatformContext platformContext, + @NonNull final StateLifecycles stateLifecycles) { final boolean softwareUpgrade = detectSoftwareUpgrade(version, initialState.get()); // Initialize the address book from the configuration and platform saved state. final AddressBookInitializer addressBookInitializer = new AddressBookInitializer( - selfId, version, softwareUpgrade, initialState.get(), bootstrapAddressBook.copy(), platformContext); + selfId, + version, + softwareUpgrade, + initialState.get(), + bootstrapAddressBook.copy(), + platformContext, + stateLifecycles); final State state = initialState.get().getState(); if (addressBookInitializer.hasAddressBookChanged()) { diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/state/notifications/NewRecoveredStateNotification.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/state/notifications/NewRecoveredStateNotification.java index f676dd84332a..40804145baef 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/state/notifications/NewRecoveredStateNotification.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/state/notifications/NewRecoveredStateNotification.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2024 Hedera Hashgraph, LLC + * Copyright (C) 2016-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,7 @@ package com.swirlds.platform.system.state.notifications; import com.swirlds.common.notification.AbstractNotification; -import com.swirlds.platform.system.SwirldState; +import com.swirlds.platform.state.PlatformMerkleStateRoot; import java.time.Instant; /** @@ -28,32 +28,30 @@ */ public class NewRecoveredStateNotification extends AbstractNotification { - private final SwirldState swirldState; + private final PlatformMerkleStateRoot state; private final long round; private final Instant consensusTimestamp; /** * Create a notification for a state created as a result of event recovery. * - * @param swirldState the swirld state from the recovered state + * @param state the swirld state from the recovered state * @param round the round of the recovered state * @param consensusTimestamp the consensus timestamp of the recovered state round */ public NewRecoveredStateNotification( - final SwirldState swirldState, final long round, final Instant consensusTimestamp) { - - this.swirldState = swirldState; + final PlatformMerkleStateRoot state, final long round, final Instant consensusTimestamp) { + this.state = state; this.round = round; this.consensusTimestamp = consensusTimestamp; } /** - * Get the swirld state from the recovered state. Guaranteed to hold a reservation in the scope of this + * Get the state from the recovered state. Guaranteed to hold a reservation in the scope of this * notification. */ - @SuppressWarnings("unchecked") - public T getSwirldState() { - return (T) swirldState; + public PlatformMerkleStateRoot getState() { + return state; } /** diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/state/notifications/NewSignedStateNotification.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/state/notifications/NewSignedStateNotification.java index 84f5844a40b9..c84aaf007dfa 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/state/notifications/NewSignedStateNotification.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/state/notifications/NewSignedStateNotification.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2024 Hedera Hashgraph, LLC + * Copyright (C) 2016-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,7 @@ package com.swirlds.platform.system.state.notifications; import com.swirlds.common.notification.AbstractNotification; -import com.swirlds.platform.system.SwirldState; +import com.swirlds.platform.state.PlatformMerkleStateRoot; import java.time.Instant; /** @@ -27,21 +27,20 @@ */ public class NewSignedStateNotification extends AbstractNotification { - private final SwirldState swirldState; + private final PlatformMerkleStateRoot stateRoot; private final long round; private final Instant consensusTimestamp; /** * Create a notification for a newly signed state. * - * @param swirldState the swirld state from the round that is now fully signed + * @param stateRoot the swirld state from the round that is now fully signed * @param round the round that is now fully signed * @param consensusTimestamp the consensus timestamp of the round that is now fully signed */ public NewSignedStateNotification( - final SwirldState swirldState, final long round, final Instant consensusTimestamp) { - - this.swirldState = swirldState; + final PlatformMerkleStateRoot stateRoot, final long round, final Instant consensusTimestamp) { + this.stateRoot = stateRoot; this.round = round; this.consensusTimestamp = consensusTimestamp; } @@ -51,8 +50,8 @@ public NewSignedStateNotification( * this notification. */ @SuppressWarnings("unchecked") - public T getSwirldState() { - return (T) swirldState; + public T getStateRoot() { + return (T) stateRoot; } /** diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/AddressBookInitializerTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/AddressBookInitializerTest.java index 4db717f954be..51da2075091e 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/AddressBookInitializerTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/AddressBookInitializerTest.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. @@ -28,9 +28,12 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.openMocks; import com.hedera.hapi.node.state.roster.Roster; import com.swirlds.common.context.PlatformContext; @@ -43,6 +46,7 @@ import com.swirlds.platform.roster.RosterRetriever; import com.swirlds.platform.state.PlatformMerkleStateRoot; import com.swirlds.platform.state.PlatformStateAccessor; +import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.state.address.AddressBookInitializer; import com.swirlds.platform.state.signed.SignedState; import com.swirlds.platform.system.SoftwareVersion; @@ -61,17 +65,26 @@ import java.util.Objects; import java.util.Random; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Supplier; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import org.mockito.stubbing.OngoingStubbing; +import org.mockito.Mock; +import org.mockito.stubbing.Stubber; class AddressBookInitializerTest { @TempDir Path testDirectory; + @Mock + StateLifecycles stateLifecycles; + + @BeforeEach + public void setUp() { + openMocks(this); + } + @Test @DisplayName("Force the use of the config address book") void forceUseOfConfigAddressBook() throws IOException { @@ -86,7 +99,8 @@ void forceUseOfConfigAddressBook() throws IOException { false, signedState, configAddressBook, - getPlatformContext(true)); + getPlatformContext(true), + stateLifecycles); final AddressBook inititializedAddressBook = initializer.getCurrentAddressBook(); final AddressBook signedStateAddressBook = buildAddressBook(signedState.getRoster()); assertEquals( @@ -116,7 +130,8 @@ void noStateLoadedFromDisk() throws IOException { false, signedState, configAddressBook, - getPlatformContext(false)); + getPlatformContext(false), + stateLifecycles); final AddressBook inititializedAddressBook = initializer.getCurrentAddressBook(); final AddressBook signedStateAddressBook = buildAddressBook(signedState.getRoster()); assertEquals( @@ -143,7 +158,8 @@ void noStateLoadedFromDiskGenesisStateSetZeroWeight() throws IOException { false, signedState, configAddressBook, - getPlatformContext(false)); + getPlatformContext(false), + stateLifecycles); final AddressBook inititializedAddressBook = initializer.getCurrentAddressBook(); final AddressBook signedStateAddressBook = buildAddressBook(signedState.getRoster()); assertEquals( @@ -170,7 +186,8 @@ void noStateLoadedFromDiskGenesisStateChangedAddressBook() throws IOException { false, signedState, configAddressBook, - getPlatformContext(false)); + getPlatformContext(false), + stateLifecycles); final AddressBook inititializedAddressBook = initializer.getCurrentAddressBook(); final AddressBook signedStateAddressBook = buildAddressBook(signedState.getRoster()); assertEquals( @@ -200,7 +217,8 @@ void currentVersionEqualsStateVersion() throws IOException { false, signedState, configAddressBook, - getPlatformContext(false)); + getPlatformContext(false), + stateLifecycles); final AddressBook inititializedAddressBook = initializer.getCurrentAddressBook(); final AddressBook signedStateAddressBook = buildAddressBook(signedState.getRoster()); assertEquals( @@ -231,8 +249,8 @@ void assertEqualsAsRosters(final AddressBook expected, final AddressBook actual, } @Test - @DisplayName("Version upgrade, SwirldState set 0 weight.") - void versionUpgradeSwirldStateZeroWeight() throws IOException { + @DisplayName("Version upgrade, state set 0 weight.") + void versionUpgradeStateZeroWeight() throws IOException { final Randotron randotron = Randotron.create(); clearTestDirectory(); final Roster roster = getRandomRoster(randotron); @@ -245,7 +263,8 @@ void versionUpgradeSwirldStateZeroWeight() throws IOException { true, signedState, configAddressBook, - getPlatformContext(false)); + getPlatformContext(false), + stateLifecycles); final AddressBook inititializedAddressBook = initializer.getCurrentAddressBook(); final AddressBook signedStateAddressBook = buildAddressBook(signedState.getRoster()); assertEquals( @@ -261,8 +280,8 @@ void versionUpgradeSwirldStateZeroWeight() throws IOException { } @Test - @DisplayName("Version upgrade, Swirld State modified the address book.") - void versionUpgradeSwirldStateModifiedAddressBook() throws IOException { + @DisplayName("Version upgrade, state modified the address book.") + void versionUpgradeStateModifiedAddressBook() throws IOException { final Randotron randotron = Randotron.create(); clearTestDirectory(); final Roster roster = getRandomRoster(randotron); @@ -276,7 +295,8 @@ void versionUpgradeSwirldStateModifiedAddressBook() throws IOException { true, signedState, configAddressBook, - getPlatformContext(false)); + getPlatformContext(false), + stateLifecycles); final AddressBook inititializedAddressBook = initializer.getCurrentAddressBook(); assertEquals( configAddressBook, @@ -291,8 +311,8 @@ void versionUpgradeSwirldStateModifiedAddressBook() throws IOException { } @Test - @DisplayName("Version upgrade, Swirld State updates weight successfully.") - void versionUpgradeSwirldStateWeightUpdateWorks() throws IOException { + @DisplayName("Version upgrade, State updates weight successfully.") + void versionUpgradeStateWeightUpdateWorks() throws IOException { final Randotron randotron = Randotron.create(); clearTestDirectory(); final SignedState signedState = getMockSignedState7WeightRandomAddressBook(randotron); @@ -305,7 +325,8 @@ void versionUpgradeSwirldStateWeightUpdateWorks() throws IOException { true, signedState, configAddressBook, - getPlatformContext(false)); + getPlatformContext(false), + stateLifecycles); final AddressBook inititializedAddressBook = initializer.getCurrentAddressBook(); assertNotEquals( configAddressBook, @@ -338,6 +359,24 @@ private AddressBook copyWithWeightChanges(AddressBook addressBook, int weightVal return newAddressBook; } + /** + * Copies the address book while setting weight per address to the given weight value. + * + * @param addressBook The address book to copy + * @param weightValue The new weight value per address. + * @return the copy of the input address book with the given weight value per address. + */ + private void updateWithWeightChanges(AddressBook addressBook, int weightValue) { + final AddressBook temp = new AddressBook(); + for (Address address : addressBook) { + temp.add(address.copySetWeight(weightValue)); + } + addressBook.clear(); + for (Address address : temp) { + addressBook.add(address); + } + } + /** * Creates a mock SoftwareVersion matching the input version number. * @@ -360,11 +399,12 @@ private SoftwareVersion getMockSoftwareVersion(int version) { return Integer.compare(softwareVersion.getVersion(), other.getVersion()); } }); + when(softwareVersion.toString()).thenReturn(Integer.toString(version)); return softwareVersion; } /** - * Creates a mock signed state and a SwirldState that sets all addresses to have weight = 7. + * Creates a mock signed state and a State that sets all addresses to have weight = 7. * * @return The mock SignedState. */ @@ -373,16 +413,16 @@ private SignedState getMockSignedState7WeightRandomAddressBook(@NonNull final Ra } /** - * Creates a mock signed state and a SwirldState that sets all addresses to the given weightValue. + * Creates a mock signed state and a State that sets all addresses to the given weightValue. * - * @param weightValue The weight value that the SwirldState should set all addresses to in its updateWeight + * @param weightValue The weight value that the State should set all addresses to in its updateWeight * method. * @param currentRoster The roster that should be returned by {@link SignedState#getRoster()} and used to * derive the address book for {@link PlatformStateAccessor#getAddressBook()} * @param previousAddressBook The address book that should be returned by * {@link PlatformStateAccessor#getPreviousAddressBook()} * @param fromGenesis Whether the state should be from genesis or not. - * @return The mock SignedState and SwirldState configured to set all addresses with given weightValue. + * @return The mock SignedState and State configured to set all addresses with given weightValue. */ private SignedState getMockSignedState( final int weightValue, @@ -391,9 +431,8 @@ private SignedState getMockSignedState( boolean fromGenesis) { final SignedState signedState = mock(SignedState.class); final SoftwareVersion softwareVersion = getMockSoftwareVersion(2); - final PlatformMerkleStateRoot state = - getMockSwirldStateSupplier(weightValue).get(); - when(signedState.getSwirldState()).thenReturn(state); + configureUpdateWeightForStateLifecycles(weightValue); + final PlatformMerkleStateRoot state = mock(PlatformMerkleStateRoot.class); final PlatformStateAccessor platformState = mock(PlatformStateAccessor.class); when(platformState.getCreationSoftwareVersion()).thenReturn(softwareVersion); RosterServiceStateMock.setup(state, currentRoster, 1L, RosterRetriever.buildRoster(previousAddressBook)); @@ -409,29 +448,25 @@ private SignedState getMockSignedState( * Creates a mock swirld state with the given scenario. * * @param scenario The scenario to load. - * @return A SwirldState which behaves according to the input scenario. */ - private Supplier getMockSwirldStateSupplier(int scenario) { + private void configureUpdateWeightForStateLifecycles(int scenario) { final AtomicReference configAddressBook = new AtomicReference<>(); - final PlatformMerkleStateRoot swirldState = mock(PlatformMerkleStateRoot.class); - final OngoingStubbing stub = when(swirldState.updateWeight( - argThat(confAB -> { - configAddressBook.set(confAB); - return true; - }), - argThat(context -> true))); + final Stubber stubber; switch (scenario) { case 0: - stub.thenAnswer(foo -> copyWithWeightChanges(configAddressBook.get(), 0)); + stubber = doAnswer(foo -> { + updateWithWeightChanges(configAddressBook.get(), 0); + return null; + }); break; case 1: - stub.thenAnswer(foo -> configAddressBook.get()); + stubber = doAnswer(foo -> configAddressBook.get()); break; case 2: - stub.thenAnswer(foo -> configAddressBook + stubber = doAnswer(foo -> configAddressBook .get() .add(configAddressBook .get() @@ -439,13 +474,23 @@ private Supplier getMockSwirldStateSupplier(int scenari .copySetNodeId(configAddressBook.get().getNextAvailableNodeId()))); break; case 7: - stub.thenAnswer(foo -> copyWithWeightChanges(configAddressBook.get(), 7)); + stubber = doAnswer(foo -> { + updateWithWeightChanges(configAddressBook.get(), 7); + return null; + }); break; default: - stub.thenAnswer(foo -> copyWithWeightChanges(configAddressBook.get(), 10)); + stubber = doAnswer(foo -> copyWithWeightChanges(configAddressBook.get(), 10)); } - return () -> swirldState; + stubber.when(stateLifecycles) + .onUpdateWeight( + any(), + argThat(confAB -> { + configAddressBook.set(confAB); + return true; + }), + argThat(context -> true)); } /** diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/SignedStateFileReadWriteTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/SignedStateFileReadWriteTest.java index b1731d4fdc2b..fe89fbabef9e 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/SignedStateFileReadWriteTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/SignedStateFileReadWriteTest.java @@ -184,7 +184,7 @@ void writeSavedStateToDiskTest() throws IOException { .build(); // make immutable - signedState.getSwirldState().copy(); + signedState.getState().copy(); writeSignedStateToDisk( platformContext, NodeId.of(0), directory, signedState, StateToDiskReason.PERIODIC_SNAPSHOT); diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/SwirldStateManagerFreezePeriodCheckerTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/StateEventHandlerManagerFreezePeriodCheckerTest.java similarity index 95% rename from platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/SwirldStateManagerFreezePeriodCheckerTest.java rename to platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/StateEventHandlerManagerFreezePeriodCheckerTest.java index d788ff530a1f..eed593b3f0f5 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/SwirldStateManagerFreezePeriodCheckerTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/StateEventHandlerManagerFreezePeriodCheckerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2024 Hedera Hashgraph, LLC + * Copyright (C) 2021-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. @@ -23,7 +23,7 @@ import java.time.Instant; import org.junit.jupiter.api.Test; -class SwirldStateManagerFreezePeriodCheckerTest { +class StateEventHandlerManagerFreezePeriodCheckerTest { @Test void isInFreezePeriodTest() { diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/StateFileManagerTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/StateFileManagerTests.java index 865b90e7ee6e..4ddc18b9994c 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/StateFileManagerTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/StateFileManagerTests.java @@ -62,7 +62,7 @@ import com.swirlds.platform.state.snapshot.StateDumpRequest; import com.swirlds.platform.state.snapshot.StateSavingResult; import com.swirlds.platform.state.snapshot.StateSnapshotManager; -import com.swirlds.platform.test.fixtures.state.BlockingSwirldState; +import com.swirlds.platform.test.fixtures.state.BlockingState; import com.swirlds.platform.test.fixtures.state.FakeStateLifecycles; import com.swirlds.platform.test.fixtures.state.RandomSignedStateGenerator; import com.swirlds.platform.wiring.components.StateAndRound; @@ -210,7 +210,7 @@ void standardOperationTest(final boolean successExpected) throws IOException { void saveFatalSignedState() throws InterruptedException, IOException { final SignedState signedState = new RandomSignedStateGenerator().setUseBlockingState(true).build(); - ((BlockingSwirldState) signedState.getSwirldState()).enableBlockingSerialization(); + ((BlockingState) signedState.getState()).enableBlockingSerialization(); final StateSnapshotManager manager = new DefaultStateSnapshotManager(context, MAIN_CLASS_NAME, SELF_ID, SWIRLD_NAME); @@ -227,7 +227,7 @@ void saveFatalSignedState() throws InterruptedException, IOException { // shouldn't be finished yet assertTrue(thread.isAlive(), "thread should still be blocked"); - ((BlockingSwirldState) signedState.getSwirldState()).unblockSerialization(); + ((BlockingState) signedState.getState()).unblockSerialization(); thread.join(1000); final Path stateDirectory = testDirectory.resolve("fatal").resolve("node1234_round" + signedState.getRound()); diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/components/DefaultAppNotifierTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/components/DefaultAppNotifierTest.java index a036a3aa8568..fd79f78f93f8 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/components/DefaultAppNotifierTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/components/DefaultAppNotifierTest.java @@ -37,7 +37,7 @@ import com.swirlds.platform.listeners.ReconnectCompleteNotification; import com.swirlds.platform.listeners.StateWriteToDiskCompleteListener; import com.swirlds.platform.listeners.StateWriteToDiskCompleteNotification; -import com.swirlds.platform.system.SwirldState; +import com.swirlds.platform.state.PlatformMerkleStateRoot; import com.swirlds.platform.system.state.notifications.AsyncFatalIssListener; import com.swirlds.platform.system.state.notifications.IssListener; import com.swirlds.platform.system.state.notifications.IssNotification; @@ -88,7 +88,7 @@ void testStateHashNotificationSent() { @Test void testReconnectCompleteNotificationSent() { - final SwirldState state = mock(SwirldState.class); + final PlatformMerkleStateRoot state = mock(PlatformMerkleStateRoot.class); final ReconnectCompleteNotification notification = new ReconnectCompleteNotification(100L, Instant.now(), state); @@ -114,7 +114,7 @@ void testPlatformStatusChangeNotificationSent() { @Test void testLatestCompleteStateNotificationSent() { - final SwirldState state = mock(SwirldState.class); + final PlatformMerkleStateRoot state = mock(PlatformMerkleStateRoot.class); final CompletionCallback> cleanup = mock(CompletionCallback.class); final NewSignedStateNotification signedStateNotification = diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/eventhandling/DefaultTransactionHandlerTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/eventhandling/DefaultTransactionHandlerTests.java index 037f5c12b3d4..92bfb34df668 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/eventhandling/DefaultTransactionHandlerTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/eventhandling/DefaultTransactionHandlerTests.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. @@ -156,7 +156,9 @@ void normalOperation(final boolean pcesRound) throws InterruptedException { pcesRound, handlerOutput.reservedSignedState().get().isPcesRound(), "the state should match the PCES boolean"); - verify(tester.getSwirldStateManager().getConsensusState()).sealConsensusRound(consensusRound); + verify(tester.getStateLifecycles()) + .onSealConsensusRound( + consensusRound, tester.getSwirldStateManager().getConsensusState()); } @Test diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/eventhandling/TransactionHandlerTester.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/eventhandling/TransactionHandlerTester.java index 2fac032faca8..17af88b26124 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/eventhandling/TransactionHandlerTester.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/eventhandling/TransactionHandlerTester.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. @@ -17,6 +17,7 @@ package com.swirlds.platform.eventhandling; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -27,12 +28,12 @@ import com.swirlds.platform.roster.RosterRetriever; import com.swirlds.platform.state.PlatformMerkleStateRoot; import com.swirlds.platform.state.PlatformStateModifier; +import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.state.SwirldStateManager; import com.swirlds.platform.state.service.PlatformStateValueAccumulator; import com.swirlds.platform.system.BasicSoftwareVersion; import com.swirlds.platform.system.Round; import com.swirlds.platform.system.SoftwareVersion; -import com.swirlds.platform.system.SwirldState; import com.swirlds.platform.system.address.AddressBook; import com.swirlds.platform.system.status.StatusActionSubmitter; import com.swirlds.platform.system.status.actions.PlatformStatusAction; @@ -48,6 +49,7 @@ public class TransactionHandlerTester { private final DefaultTransactionHandler defaultTransactionHandler; private final List submittedActions = new ArrayList<>(); private final List handledRounds = new ArrayList<>(); + private final StateLifecycles stateLifecycles; /** * Constructs a new {@link TransactionHandlerTester} with the given {@link AddressBook}. @@ -60,6 +62,8 @@ public TransactionHandlerTester(final AddressBook addressBook) { platformState = new PlatformStateValueAccumulator(); final PlatformMerkleStateRoot consensusState = mock(PlatformMerkleStateRoot.class); + stateLifecycles = mock(StateLifecycles.class); + ; when(consensusState.copy()).thenReturn(consensusState); when(consensusState.getReadablePlatformState()).thenReturn(platformState); when(consensusState.getWritablePlatformState()).thenReturn(platformState); @@ -67,15 +71,16 @@ public TransactionHandlerTester(final AddressBook addressBook) { handledRounds.add(i.getArgument(0)); return null; }) - .when(consensusState) - .handleConsensusRound(any(), any(), any()); + .when(stateLifecycles) + .onHandleConsensusRound(any(), same(consensusState), any()); final StatusActionSubmitter statusActionSubmitter = submittedActions::add; swirldStateManager = new SwirldStateManager( platformContext, RosterRetriever.buildRoster(addressBook), NodeId.FIRST_NODE_ID, statusActionSubmitter, - new BasicSoftwareVersion(1)); + new BasicSoftwareVersion(1), + stateLifecycles); swirldStateManager.setInitialState(consensusState); defaultTransactionHandler = new DefaultTransactionHandler( platformContext, swirldStateManager, statusActionSubmitter, mock(SoftwareVersion.class)); @@ -103,7 +108,7 @@ public List getSubmittedActions() { } /** - * @return a list of all {@link Round}s that have been provided to the {@link SwirldState} for handling + * @return a list of all {@link Round}s that have been provided to the {@link PlatformMerkleStateRoot} for handling */ public List getHandledRounds() { return handledRounds; @@ -115,4 +120,11 @@ public List getHandledRounds() { public SwirldStateManager getSwirldStateManager() { return swirldStateManager; } + + /** + * @return the {@link StateLifecycles} used by this tester + */ + public StateLifecycles getStateLifecycles() { + return stateLifecycles; + } } diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/eventhandling/TransactionPrehandlerTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/eventhandling/TransactionPrehandlerTests.java index 8239fef9f361..69146f3179b5 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/eventhandling/TransactionPrehandlerTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/eventhandling/TransactionPrehandlerTests.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. @@ -27,8 +27,11 @@ import com.swirlds.common.test.fixtures.RandomUtils; import com.swirlds.common.test.fixtures.platform.TestPlatformContextBuilder; import com.swirlds.platform.event.PlatformEvent; +import com.swirlds.platform.state.PlatformMerkleStateRoot; +import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.state.nexus.SignedStateNexus; import com.swirlds.platform.state.signed.ReservedSignedState; +import com.swirlds.platform.state.signed.SignedState; import com.swirlds.platform.test.fixtures.event.TestingEventBuilder; import java.time.Duration; import java.util.Random; @@ -58,7 +61,12 @@ void normalOperation() { .when(state) .close(); + final SignedState signedState = mock(SignedState.class); + final PlatformMerkleStateRoot merkleStateRoot = mock(PlatformMerkleStateRoot.class); + when(signedState.getState()).thenReturn(merkleStateRoot); + final SignedStateNexus latestImmutableStateNexus = mock(SignedStateNexus.class); + final StateLifecycles stateLifecycles = mock(StateLifecycles.class); // return null until returnValidState is set to true. keep track of when the first state retrieval is attempted, // so we can assert that prehandle hasn't happened before the state is available when(latestImmutableStateNexus.getState(any())).thenAnswer(i -> { @@ -68,8 +76,8 @@ void normalOperation() { final PlatformContext platformContext = TestPlatformContextBuilder.create().build(); - final TransactionPrehandler transactionPrehandler = - new DefaultTransactionPrehandler(platformContext, () -> latestImmutableStateNexus.getState("test")); + final TransactionPrehandler transactionPrehandler = new DefaultTransactionPrehandler( + platformContext, () -> latestImmutableStateNexus.getState("test"), stateLifecycles); final PlatformEvent platformEvent = new TestingEventBuilder(random).build(); diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventRecoveryWorkflowTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventRecoveryWorkflowTests.java index ff137598029d..fd018c4e0b27 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventRecoveryWorkflowTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventRecoveryWorkflowTests.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2024 Hedera Hashgraph, LLC + * Copyright (C) 2022-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. @@ -28,6 +28,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -40,9 +41,9 @@ import com.swirlds.platform.event.PlatformEvent; import com.swirlds.platform.recovery.emergencyfile.EmergencyRecoveryFile; import com.swirlds.platform.recovery.internal.StreamedRound; -import com.swirlds.platform.state.PlatformStateModifier; +import com.swirlds.platform.state.PlatformMerkleStateRoot; +import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.system.Round; -import com.swirlds.platform.system.SwirldState; import com.swirlds.platform.system.events.CesEvent; import com.swirlds.platform.system.events.ConsensusEvent; import java.io.IOException; @@ -107,8 +108,6 @@ void isFreezeStateTest() { @Test @DisplayName("applyTransactions() Test") void applyTransactionsTest() { - final PlatformStateModifier platformState = mock(PlatformStateModifier.class); - final List events = new ArrayList<>(); for (int i = 0; i < 100; i++) { events.add(mock(PlatformEvent.class)); @@ -120,39 +119,39 @@ void applyTransactionsTest() { final List preHandleList = new ArrayList<>(); final AtomicBoolean roundHandled = new AtomicBoolean(false); - final SwirldState immutableState = mock(SwirldState.class); + final StateLifecycles stateLifecycles = mock(StateLifecycles.class); + final PlatformMerkleStateRoot immutableState = mock(PlatformMerkleStateRoot.class); doAnswer(invocation -> { assertFalse(roundHandled.get(), "round should not have been handled yet"); preHandleList.add(invocation.getArgument(0)); return null; }) - .when(immutableState) - .preHandle(any(), any()); + .when(stateLifecycles) + .onPreHandle(any(), same(immutableState), any()); doAnswer(invocation -> { fail("mutable state should handle transactions"); return null; }) - .when(immutableState) - .handleConsensusRound(any(), any(), any()); + .when(stateLifecycles) + .onHandleConsensusRound(any(), same(immutableState), any()); - final SwirldState mutableState = mock(SwirldState.class); + final PlatformMerkleStateRoot mutableState = mock(PlatformMerkleStateRoot.class); doAnswer(invocation -> { fail("immutable state should pre-handle transactions"); return null; }) - .when(mutableState) - .preHandle(any(), any()); + .when(stateLifecycles) + .onPreHandle(any(), same(mutableState), any()); doAnswer(invocation -> { assertFalse(roundHandled.get(), "round should only be handled once"); assertSame(round, invocation.getArgument(0), "unexpected round"); - assertSame(platformState, invocation.getArgument(1), "unexpected dual state"); roundHandled.set(true); return null; }) - .when(mutableState) - .handleConsensusRound(any(), any(), any()); + .when(stateLifecycles) + .onHandleConsensusRound(any(), same(mutableState), any()); - EventRecoveryWorkflow.applyTransactions(immutableState, mutableState, platformState, round); + EventRecoveryWorkflow.applyTransactions(stateLifecycles, immutableState, mutableState, round); assertEquals(events.size(), preHandleList.size(), "incorrect number of pre-handle calls"); for (int index = 0; index < events.size(); index++) { diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/PlatformMerkleStateRootTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/PlatformMerkleStateRootTest.java index 116e6c9236b2..b529892fa775 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/PlatformMerkleStateRootTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/PlatformMerkleStateRootTest.java @@ -37,7 +37,6 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; -import com.hedera.hapi.platform.event.StateSignatureTransaction; import com.hedera.hapi.platform.state.PlatformState; import com.swirlds.base.state.MutabilityException; import com.swirlds.common.context.PlatformContext; @@ -50,18 +49,10 @@ import com.swirlds.common.metrics.noop.NoOpMetrics; import com.swirlds.config.api.ConfigurationBuilder; import com.swirlds.merkle.map.MerkleMap; -import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.state.service.PlatformStateService; import com.swirlds.platform.state.service.schemas.V0540PlatformStateSchema; -import com.swirlds.platform.system.InitTrigger; -import com.swirlds.platform.system.Platform; -import com.swirlds.platform.system.Round; -import com.swirlds.platform.system.SoftwareVersion; -import com.swirlds.platform.system.address.AddressBook; -import com.swirlds.platform.system.events.Event; import com.swirlds.platform.test.fixtures.state.FakeStateLifecycles; import com.swirlds.platform.test.fixtures.state.MerkleTestBase; -import com.swirlds.state.State; import com.swirlds.state.StateChangeListener; import com.swirlds.state.lifecycle.StateDefinition; import com.swirlds.state.merkle.StateMetadata; @@ -74,8 +65,6 @@ import com.swirlds.state.spi.WritableSingletonState; import com.swirlds.state.spi.WritableStates; import com.swirlds.state.test.fixtures.merkle.TestSchema; -import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; @@ -83,8 +72,6 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -99,52 +86,6 @@ class PlatformMerkleStateRootTest extends MerkleTestBase { /** The merkle tree we will test with */ private PlatformMerkleStateRoot stateRoot; - private final AtomicBoolean onPreHandleCalled = new AtomicBoolean(false); - private final AtomicBoolean onHandleCalled = new AtomicBoolean(false); - private final AtomicBoolean onUpdateWeightCalled = new AtomicBoolean(false); - - private final StateLifecycles lifecycles = new StateLifecycles() { - - @Override - public void onSealConsensusRound(@NonNull Round round, @NonNull State state) { - // No-op - } - - @Override - public void onPreHandle( - @NonNull Event event, - @NonNull State state, - @NonNull Consumer> stateSignatureTransactions) { - onPreHandleCalled.set(true); - } - - @Override - public void onNewRecoveredState(@NonNull State recoveredState) { - // No-op - } - - @Override - public void onHandleConsensusRound( - @NonNull Round round, - @NonNull State state, - @NonNull Consumer> stateSignatureTransactions) { - onHandleCalled.set(true); - } - - @Override - public void onStateInitialized( - @NonNull State state, - @NonNull Platform platform, - @NonNull InitTrigger trigger, - @Nullable SoftwareVersion previousVersion) {} - - @Override - public void onUpdateWeight( - @NonNull State state, @NonNull AddressBook configAddressBook, @NonNull PlatformContext context) { - onUpdateWeightCalled.set(true); - } - }; - /** * Start with an empty Merkle Tree, but with the "fruit" map and metadata created and ready to * be added. @@ -154,7 +95,7 @@ void setUp() { setupConstructableRegistry(); FakeStateLifecycles.registerMerkleStateRootClassIds(); setupFruitMerkleMap(); - stateRoot = new PlatformMerkleStateRoot(lifecycles, softwareVersionSupplier); + stateRoot = new PlatformMerkleStateRoot(softwareVersionSupplier); FAKE_MERKLE_STATE_LIFECYCLES.initPlatformState(stateRoot); } @@ -759,53 +700,9 @@ void knownServiceNameUsingWritableStates() { } } - @Nested - @DisplayName("Handling Pre-Handle Tests") - final class PreHandleTest { - @Test - @DisplayName("The onPreHandle handler is called when a pre-handle happens") - void onPreHandleCalled() { - assertThat(onPreHandleCalled).isFalse(); - stateRoot.preHandle(Mockito.mock(Event.class), systemTransactions -> {}); - assertThat(onPreHandleCalled).isTrue(); - } - } - - @Nested - @DisplayName("Handling Consensus Rounds Tests") - final class ConsensusRoundTest { - @Test - @DisplayName("Notifications are sent to onHandleConsensusRound when handleConsensusRound is called") - void handleConsensusRoundCallback() { - final var round = Mockito.mock(Round.class); - final var platformState = Mockito.mock(PlatformStateModifier.class); - final var state = new PlatformMerkleStateRoot(lifecycles, softwareVersionSupplier); - - state.handleConsensusRound(round, platformState, systemTransactions -> {}); - assertThat(onHandleCalled).isTrue(); - } - } - @Nested @DisplayName("Copy Tests") final class CopyTest { - @Test - @DisplayName("When a copy is made, the original loses the onConsensusRoundCallback, and the copy gains it") - void originalLosesConsensusRoundCallbackAfterCopy() { - final var copy = stateRoot.copy(); - - // The original no longer has the listener - final var round = Mockito.mock(Round.class); - final var platformState = Mockito.mock(PlatformStateModifier.class); - assertThrows( - MutabilityException.class, - () -> stateRoot.handleConsensusRound(round, platformState, systemTransactions -> {})); - - // But the copy does - copy.handleConsensusRound(round, platformState, systemTransactions -> {}); - assertThat(onHandleCalled).isTrue(); - } - @Test @DisplayName("Cannot call copy on original after copy") void callCopyTwiceOnOriginalThrows() { @@ -841,18 +738,6 @@ void createWritableStatesOnOriginalAfterCopyThrows() { } } - @Nested - @DisplayName("Handling updateWeight Tests") - final class UpdateWeightTest { - @Test - @DisplayName("The onUpdateWeight handler is called when a updateWeight is called") - void onUpdateWeightCalled() { - assertThat(onUpdateWeightCalled).isFalse(); - stateRoot.updateWeight(Mockito.mock(AddressBook.class), Mockito.mock(PlatformContext.class)); - assertThat(onUpdateWeightCalled).isTrue(); - } - } - @Nested @DisplayName("with registered listeners") class WithRegisteredListeners { @@ -1027,17 +912,16 @@ void setUp() { stateRoot.putServiceStateIfAbsent(countryMetadata, () -> countrySingleton); stateRoot.putServiceStateIfAbsent(steamMetadata, () -> steamQueue); - final Platform platform = mock(Platform.class); merkleCryptography = MerkleCryptographyFactory.create( ConfigurationBuilder.create() .withConfigDataType(CryptoConfig.class) .build(), CryptographyFactory.create()); final PlatformContext platformContext = mock(PlatformContext.class); - when(platform.getContext()).thenReturn(platformContext); when(platformContext.getMerkleCryptography()).thenReturn(merkleCryptography); when(platformContext.getMetrics()).thenReturn(new NoOpMetrics()); - stateRoot.init(platform, InitTrigger.GENESIS, mock(SoftwareVersion.class)); + stateRoot.init( + platformContext.getTime(), platformContext.getMetrics(), platformContext.getMerkleCryptography()); } @Test diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/SignedStateTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/SignedStateTests.java index a7270b8c3453..51e553087fd4 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/SignedStateTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/SignedStateTests.java @@ -76,8 +76,7 @@ void tearDown() { */ private PlatformMerkleStateRoot buildMockState( final Random random, final Runnable reserveCallback, final Runnable releaseCallback) { - final var real = new PlatformMerkleStateRoot( - FAKE_MERKLE_STATE_LIFECYCLES, version -> new BasicSoftwareVersion(version.major())); + final var real = new PlatformMerkleStateRoot(version -> new BasicSoftwareVersion(version.major())); FAKE_MERKLE_STATE_LIFECYCLES.initStates(real); RosterUtils.setActiveRoster(real, RandomRosterBuilder.create(random).build(), 0L); final PlatformMerkleStateRoot state = spy(real); @@ -225,8 +224,8 @@ void noGarbageCollectorTest() { @Test @DisplayName("Alternate Constructor Reservations Test") void alternateConstructorReservationsTest() { - final PlatformMerkleStateRoot state = spy(new PlatformMerkleStateRoot( - FAKE_MERKLE_STATE_LIFECYCLES, version -> new BasicSoftwareVersion(version.major()))); + final PlatformMerkleStateRoot state = + spy(new PlatformMerkleStateRoot(version -> new BasicSoftwareVersion(version.major()))); final PlatformStateModifier platformState = mock(PlatformStateModifier.class); FAKE_MERKLE_STATE_LIFECYCLES.initPlatformState(state); when(state.getReadablePlatformState()).thenReturn(platformState); diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/SwirldStateManagerUtilsTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/StateEventHandlerManagerUtilsTests.java similarity index 83% rename from platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/SwirldStateManagerUtilsTests.java rename to platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/StateEventHandlerManagerUtilsTests.java index b6e3916de2d9..ad6cde86a25f 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/SwirldStateManagerUtilsTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/StateEventHandlerManagerUtilsTests.java @@ -21,12 +21,12 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.mockito.Mockito.mock; -import com.swirlds.platform.metrics.SwirldStateMetrics; +import com.swirlds.platform.metrics.StateMetrics; import com.swirlds.platform.system.BasicSoftwareVersion; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -public class SwirldStateManagerUtilsTests { +public class StateEventHandlerManagerUtilsTests { @BeforeEach void setup() {} @@ -34,11 +34,11 @@ void setup() {} @Test void testFastCopyIsMutable() { - final PlatformMerkleStateRoot state = new PlatformMerkleStateRoot( - FAKE_MERKLE_STATE_LIFECYCLES, version -> new BasicSoftwareVersion(version.major())); + final PlatformMerkleStateRoot state = + new PlatformMerkleStateRoot(version -> new BasicSoftwareVersion(version.major())); FAKE_MERKLE_STATE_LIFECYCLES.initPlatformState(state); state.reserve(); - final SwirldStateMetrics stats = mock(SwirldStateMetrics.class); + final StateMetrics stats = mock(StateMetrics.class); final PlatformMerkleStateRoot result = SwirldStateManagerUtils.fastCopy(state, stats, new BasicSoftwareVersion(1)); diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/StateRegistryTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/StateRegistryTests.java index 77466062d72c..ce1817bdfd6e 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/StateRegistryTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/StateRegistryTests.java @@ -86,7 +86,7 @@ void activeStateCountTest() throws IOException { final List states = new LinkedList<>(); // Create a bunch of states for (int i = 0; i < 100; i++) { - states.add(new PlatformMerkleStateRoot(FAKE_MERKLE_STATE_LIFECYCLES, softwareVersionSupplier)); + states.add(new PlatformMerkleStateRoot(softwareVersionSupplier)); assertEquals( states.size(), RuntimeObjectRegistry.getActiveObjectsCount(PlatformMerkleStateRoot.class), @@ -94,8 +94,7 @@ void activeStateCountTest() throws IOException { } // Fast copy a state - final PlatformMerkleStateRoot stateToCopy = - new PlatformMerkleStateRoot(FAKE_MERKLE_STATE_LIFECYCLES, softwareVersionSupplier); + final PlatformMerkleStateRoot stateToCopy = new PlatformMerkleStateRoot(softwareVersionSupplier); states.add(stateToCopy); final PlatformMerkleStateRoot copyOfStateToCopy = stateToCopy.copy(); states.add(copyOfStateToCopy); @@ -107,8 +106,7 @@ void activeStateCountTest() throws IOException { final Path dir = testDirectory; // Deserialize a state - final PlatformMerkleStateRoot stateToSerialize = - new PlatformMerkleStateRoot(FAKE_MERKLE_STATE_LIFECYCLES, softwareVersionSupplier); + final PlatformMerkleStateRoot stateToSerialize = new PlatformMerkleStateRoot(softwareVersionSupplier); FAKE_MERKLE_STATE_LIFECYCLES.initPlatformState(stateToSerialize); final var platformState = stateToSerialize.getWritablePlatformState(); platformState.bulkUpdate(v -> { diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/SwirldStateManagerTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/SwirldsStateManagerTests.java similarity index 92% rename from platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/SwirldStateManagerTests.java rename to platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/SwirldsStateManagerTests.java index 1c2947a6f04d..8caa733019a2 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/SwirldStateManagerTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/SwirldsStateManagerTests.java @@ -41,7 +41,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -class SwirldStateManagerTests { +class SwirldsStateManagerTests { private SwirldStateManager swirldStateManager; private PlatformMerkleStateRoot initialState; @@ -57,7 +57,12 @@ void setup() { TestPlatformContextBuilder.create().build(); swirldStateManager = new SwirldStateManager( - platformContext, roster, NodeId.of(0L), mock(StatusActionSubmitter.class), new BasicSoftwareVersion(1)); + platformContext, + roster, + NodeId.of(0L), + mock(StatusActionSubmitter.class), + new BasicSoftwareVersion(1), + FAKE_MERKLE_STATE_LIFECYCLES); swirldStateManager.setInitialState(initialState); } @@ -126,8 +131,8 @@ void loadFromSignedStateRefCount() { } private static PlatformMerkleStateRoot newState() { - final PlatformMerkleStateRoot state = new PlatformMerkleStateRoot( - FAKE_MERKLE_STATE_LIFECYCLES, version -> new BasicSoftwareVersion(version.major())); + final PlatformMerkleStateRoot state = + new PlatformMerkleStateRoot(version -> new BasicSoftwareVersion(version.major())); FAKE_MERKLE_STATE_LIFECYCLES.initPlatformState(state); final PlatformStateModifier platformState = mock(PlatformStateModifier.class); @@ -143,7 +148,7 @@ private static SignedState newSignedState() { final SignedState ss = new RandomSignedStateGenerator().build(); assertEquals( 1, - ss.getSwirldState().getReservationCount(), + ss.getState().getReservationCount(), "Creating a signed state should increment the state reference count."); return ss; } diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/hashlogger/HashLoggerTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/hashlogger/HashLoggerTest.java index 3b56272a8e6f..5fb6492d9ec7 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/hashlogger/HashLoggerTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/hashlogger/HashLoggerTest.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. @@ -39,7 +39,6 @@ import com.swirlds.platform.state.PlatformStateAccessor; import com.swirlds.platform.state.signed.ReservedSignedState; import com.swirlds.platform.state.signed.SignedState; -import com.swirlds.platform.system.SwirldState; import com.swirlds.state.State; import java.util.ArrayList; import java.util.List; @@ -151,7 +150,7 @@ private ReservedSignedState createSignedState(final long round) { MerkleCryptoFactory.getInstance().digestTreeSync(merkleNode); final SignedState signedState = mock(SignedState.class); final PlatformMerkleStateRoot state = - mock(PlatformMerkleStateRoot.class, withSettings().extraInterfaces(State.class, SwirldState.class)); + mock(PlatformMerkleStateRoot.class, withSettings().extraInterfaces(State.class)); final PlatformStateAccessor platformState = mock(PlatformStateAccessor.class); when(platformState.getRound()).thenReturn(round); diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/signed/StartupStateUtilsTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/signed/StartupStateUtilsTests.java index 95a8574241b7..8c3009fa3b7f 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/signed/StartupStateUtilsTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/signed/StartupStateUtilsTests.java @@ -19,7 +19,6 @@ import static com.swirlds.common.test.fixtures.RandomUtils.getRandomPrintSeed; import static com.swirlds.common.threading.manager.AdHocThreadManager.getStaticThreadManager; import static com.swirlds.platform.state.snapshot.SignedStateFileWriter.writeSignedStateToDisk; -import static com.swirlds.platform.test.fixtures.state.FakeStateLifecycles.FAKE_MERKLE_STATE_LIFECYCLES; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -106,8 +105,7 @@ static void beforeAll() throws ConstructableRegistryException { ConstructableRegistry.getInstance() .registerConstructable(new ClassConstructorPair( PlatformMerkleStateRoot.class, - () -> new PlatformMerkleStateRoot( - FAKE_MERKLE_STATE_LIFECYCLES, version -> new BasicSoftwareVersion(version.major())))); + () -> new PlatformMerkleStateRoot(version -> new BasicSoftwareVersion(version.major())))); } @NonNull diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/turtle/runner/Turtle.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/turtle/runner/Turtle.java index 9ee203eba715..2acd65b0b9a6 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/turtle/runner/Turtle.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/turtle/runner/Turtle.java @@ -16,8 +16,6 @@ package com.swirlds.platform.turtle.runner; -import static com.swirlds.platform.test.fixtures.state.FakeStateLifecycles.FAKE_MERKLE_STATE_LIFECYCLES; - import com.swirlds.base.test.fixtures.time.FakeTime; import com.swirlds.common.constructable.ClassConstructorPair; import com.swirlds.common.constructable.ConstructableRegistry; @@ -105,8 +103,7 @@ public class Turtle { ConstructableRegistry.getInstance() .registerConstructable(new ClassConstructorPair( MerkleStateRoot.class, - () -> new PlatformMerkleStateRoot( - FAKE_MERKLE_STATE_LIFECYCLES, version -> new BasicSoftwareVersion(1)))); + () -> new PlatformMerkleStateRoot(version -> new BasicSoftwareVersion(1)))); } catch (final ConstructableRegistryException e) { throw new RuntimeException(e); } diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/turtle/runner/TurtleNode.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/turtle/runner/TurtleNode.java index 227ef6dd2588..aef2e6852293 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/turtle/runner/TurtleNode.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/turtle/runner/TurtleNode.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. @@ -20,6 +20,7 @@ import static com.swirlds.platform.builder.internal.StaticPlatformBuilder.getMetricsProvider; import static com.swirlds.platform.builder.internal.StaticPlatformBuilder.setupGlobalMetrics; import static com.swirlds.platform.state.signed.StartupStateUtils.getInitialState; +import static com.swirlds.platform.turtle.runner.TurtleStateLifecycles.TURTLE_STATE_LIFECYCLES; import com.swirlds.base.time.Time; import com.swirlds.common.context.PlatformContext; @@ -46,7 +47,6 @@ import com.swirlds.platform.test.fixtures.turtle.gossip.SimulatedNetwork; import com.swirlds.platform.util.RandomBuilder; import com.swirlds.platform.wiring.PlatformSchedulersConfig_; -import com.swirlds.state.State; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.function.Supplier; @@ -119,10 +119,10 @@ public class TurtleNode { "bar", new BasicSoftwareVersion(1), initialState, + TURTLE_STATE_LIFECYCLES, nodeId, AddressBookUtils.formatConsensusEventStreamName(addressBook, nodeId), - RosterUtils.buildRosterHistory( - (State) initialState.get().getState())) + RosterUtils.buildRosterHistory(initialState.get().getState())) .withModel(model) .withRandomBuilder(new RandomBuilder(randotron.nextLong())) .withKeysAndCerts(privateKeys) diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/turtle/runner/TurtleStateLifecycles.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/turtle/runner/TurtleStateLifecycles.java new file mode 100644 index 000000000000..bf15206cd89b --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/turtle/runner/TurtleStateLifecycles.java @@ -0,0 +1,86 @@ +/* + * 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.swirlds.platform.turtle.runner; + +import com.hedera.hapi.platform.event.StateSignatureTransaction; +import com.swirlds.common.context.PlatformContext; +import com.swirlds.common.utility.NonCryptographicHashing; +import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; +import com.swirlds.platform.state.StateLifecycles; +import com.swirlds.platform.system.InitTrigger; +import com.swirlds.platform.system.Platform; +import com.swirlds.platform.system.Round; +import com.swirlds.platform.system.SoftwareVersion; +import com.swirlds.platform.system.address.AddressBook; +import com.swirlds.platform.system.events.Event; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.function.Consumer; + +/** + * This class handles the lifecycle events for the {@link TurtleTestingToolState}. + */ +enum TurtleStateLifecycles implements StateLifecycles { + TURTLE_STATE_LIFECYCLES; + + @Override + public void onPreHandle( + @NonNull Event event, + @NonNull TurtleTestingToolState state, + @NonNull Consumer> stateSignatureTransactionCallback) { + // no op + } + + @Override + public void onHandleConsensusRound( + @NonNull Round round, + @NonNull TurtleTestingToolState turtleTestingToolState, + @NonNull Consumer> stateSignatureTransactionCallback) { + turtleTestingToolState.state = NonCryptographicHashing.hash64( + turtleTestingToolState.state, + round.getRoundNum(), + round.getConsensusTimestamp().getNano(), + round.getConsensusTimestamp().getEpochSecond()); + } + + @Override + public void onSealConsensusRound(@NonNull Round round, @NonNull TurtleTestingToolState state) { + // no op + } + + @Override + public void onStateInitialized( + @NonNull TurtleTestingToolState state, + @NonNull Platform platform, + @NonNull InitTrigger trigger, + @Nullable SoftwareVersion previousVersion) { + // no op + } + + @Override + public void onUpdateWeight( + @NonNull TurtleTestingToolState state, + @NonNull AddressBook configAddressBook, + @NonNull PlatformContext context) { + // no op + } + + @Override + public void onNewRecoveredState(@NonNull TurtleTestingToolState recoveredState) { + // no op + } +} diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/turtle/runner/TurtleTestingToolState.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/turtle/runner/TurtleTestingToolState.java index 70b4b6998af5..9eebc8284bf0 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/turtle/runner/TurtleTestingToolState.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/turtle/runner/TurtleTestingToolState.java @@ -18,14 +18,9 @@ import static com.swirlds.platform.test.fixtures.state.FakeStateLifecycles.FAKE_MERKLE_STATE_LIFECYCLES; -import com.hedera.hapi.platform.event.StateSignatureTransaction; -import com.swirlds.common.utility.NonCryptographicHashing; -import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.state.*; import com.swirlds.platform.system.BasicSoftwareVersion; -import com.swirlds.platform.system.Round; import edu.umd.cs.findbugs.annotations.NonNull; -import java.util.function.Consumer; /** * A simple testing application intended for use with TURTLE. @@ -44,10 +39,10 @@ private static final class ClassVersion { public static final int ORIGINAL = 1; } - private long state; + long state; public TurtleTestingToolState() { - super(FAKE_MERKLE_STATE_LIFECYCLES, version -> new BasicSoftwareVersion(1)); + super(version -> new BasicSoftwareVersion(1)); } /** @@ -76,21 +71,6 @@ public int getVersion() { return ClassVersion.ORIGINAL; } - /** - * {@inheritDoc} - */ - @Override - public void handleConsensusRound( - @NonNull final Round round, - @NonNull final PlatformStateModifier platformState, - @NonNull final Consumer> stateSignatureTransaction) { - state = NonCryptographicHashing.hash64( - state, - round.getRoundNum(), - round.getConsensusTimestamp().getNano(), - round.getConsensusTimestamp().getEpochSecond()); - } - /** * {@inheritDoc} */ diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/wiring/SignedStateReserverTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/wiring/SignedStateReserverTest.java index a1acab1071d8..9a9cecda9089 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/wiring/SignedStateReserverTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/wiring/SignedStateReserverTest.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. diff --git a/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/state/BlockingSwirldState.java b/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/state/BlockingState.java similarity index 78% rename from platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/state/BlockingSwirldState.java rename to platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/state/BlockingState.java index fffe529c16ff..2821f6caa655 100644 --- a/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/state/BlockingSwirldState.java +++ b/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/state/BlockingState.java @@ -17,34 +17,27 @@ package com.swirlds.platform.test.fixtures.state; import static com.swirlds.common.threading.interrupt.Uninterruptable.abortAndThrowIfInterrupted; -import static com.swirlds.platform.test.fixtures.state.FakeStateLifecycles.FAKE_MERKLE_STATE_LIFECYCLES; -import com.hedera.hapi.platform.event.StateSignatureTransaction; import com.swirlds.common.constructable.ClassConstructorPair; import com.swirlds.common.constructable.ConstructableRegistry; import com.swirlds.common.constructable.ConstructableRegistryException; import com.swirlds.common.io.streams.SerializableDataInputStream; import com.swirlds.common.io.streams.SerializableDataOutputStream; -import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.state.PlatformMerkleStateRoot; -import com.swirlds.platform.state.PlatformStateModifier; import com.swirlds.platform.system.BasicSoftwareVersion; -import com.swirlds.platform.system.Round; -import com.swirlds.platform.system.SwirldState; import com.swirlds.state.merkle.MerkleStateRoot; import com.swirlds.state.merkle.singleton.StringLeaf; import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; import java.util.Objects; import java.util.concurrent.CountDownLatch; -import java.util.function.Consumer; /** - * A test implementation of {@link PlatformMerkleStateRoot} and {@link SwirldState} state for SignedStateManager unit tests. + * A test implementation of {@link PlatformMerkleStateRoot} state for SignedStateManager unit tests. * Node that some of the {@link PlatformMerkleStateRoot} methods are intentionally not implemented. If a test needs these methods, * {@link MerkleStateRoot} should be used instead. */ -public class BlockingSwirldState extends PlatformMerkleStateRoot { +public class BlockingState extends PlatformMerkleStateRoot { static { try { @@ -66,37 +59,29 @@ public class BlockingSwirldState extends PlatformMerkleStateRoot { private final BlockingStringLeaf value; /** - * Constructs a new instance of {@link BlockingSwirldState}. + * Constructs a new instance of {@link BlockingState}. */ - public BlockingSwirldState() { - super(FAKE_MERKLE_STATE_LIFECYCLES, version -> new BasicSoftwareVersion(version.major())); + public BlockingState() { + super(version -> new BasicSoftwareVersion(version.major())); value = new BlockingStringLeaf(); setChild(1, value); } - private BlockingSwirldState(final BlockingSwirldState that) { + private BlockingState(final BlockingState that) { super(that); this.value = that.value; setChild(1, value); } - @Override - public void handleConsensusRound( - @NonNull final Round round, - @NonNull final PlatformStateModifier platformState, - @NonNull final Consumer> stateSignatureTransaction) { - // intentionally does nothing - } - /** * {@inheritDoc} */ @NonNull @Override - public BlockingSwirldState copy() { + public BlockingState copy() { throwIfImmutable(); setImmutable(true); - return new BlockingSwirldState(this); + return new BlockingState(this); } /** @@ -107,7 +92,7 @@ public boolean equals(final Object obj) { if (obj == this) { return true; } - if (!(obj instanceof final BlockingSwirldState that)) { + if (!(obj instanceof final BlockingState that)) { return false; } return Objects.equals(this.getReadablePlatformState(), that.getReadablePlatformState()); diff --git a/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/state/FakeStateLifecycles.java b/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/state/FakeStateLifecycles.java index 3a179535bd9e..268872bdf0d0 100644 --- a/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/state/FakeStateLifecycles.java +++ b/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/state/FakeStateLifecycles.java @@ -74,7 +74,7 @@ import java.util.List; import java.util.function.Consumer; -public enum FakeStateLifecycles implements StateLifecycles { +public enum FakeStateLifecycles implements StateLifecycles { FAKE_MERKLE_STATE_LIFECYCLES; public static final Configuration CONFIGURATION = ConfigurationBuilder.create() @@ -96,8 +96,7 @@ public static void registerMerkleStateRootClassIds() { ConstructableRegistry registry = ConstructableRegistry.getInstance(); registry.registerConstructable(new ClassConstructorPair( PlatformMerkleStateRoot.class, - () -> new PlatformMerkleStateRoot( - FAKE_MERKLE_STATE_LIFECYCLES, version -> new BasicSoftwareVersion(version.major())))); + () -> new PlatformMerkleStateRoot(version -> new BasicSoftwareVersion(version.major())))); registry.registerConstructable(new ClassConstructorPair(SingletonNode.class, SingletonNode::new)); registry.registerConstructable(new ClassConstructorPair(StringLeaf.class, StringLeaf::new)); registry.registerConstructable(new ClassConstructorPair( @@ -129,7 +128,7 @@ public List initStates(@NonNull final State state) { } public List initPlatformState(@NonNull final State state) { - if (!(state instanceof MerkleStateRoot merkleStateRoot)) { + if (!(state instanceof PlatformMerkleStateRoot merkleStateRoot)) { throw new IllegalArgumentException("Can only be used with MerkleStateRoot instances"); } final var schema = new V0540PlatformStateSchema(config -> new BasicSoftwareVersion(1)); @@ -210,7 +209,7 @@ public List initRosterState(@NonNull final State state) { @Override public void onPreHandle( @NonNull Event event, - @NonNull State state, + @NonNull PlatformMerkleStateRoot state, @NonNull Consumer> stateSignatureTransactionCallback) { // no-op } @@ -218,20 +217,20 @@ public void onPreHandle( @Override public void onHandleConsensusRound( @NonNull Round round, - @NonNull State state, + @NonNull PlatformMerkleStateRoot state, @NonNull Consumer> stateSignatureTransactionCallback) { // no-op } @Override - public void onSealConsensusRound(@NonNull Round round, @NonNull State state) { + public void onSealConsensusRound(@NonNull Round round, @NonNull PlatformMerkleStateRoot state) { // Touch this round round.getRoundNum(); } @Override public void onStateInitialized( - @NonNull State state, + @NonNull PlatformMerkleStateRoot state, @NonNull Platform platform, @NonNull InitTrigger trigger, @Nullable SoftwareVersion previousVersion) { @@ -240,12 +239,14 @@ public void onStateInitialized( @Override public void onUpdateWeight( - @NonNull State state, @NonNull AddressBook configAddressBook, @NonNull PlatformContext context) { + @NonNull PlatformMerkleStateRoot state, + @NonNull AddressBook configAddressBook, + @NonNull PlatformContext context) { // no-op } @Override - public void onNewRecoveredState(@NonNull State recoveredState) { + public void onNewRecoveredState(@NonNull PlatformMerkleStateRoot recoveredState) { // no-op } } diff --git a/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/state/RandomSignedStateGenerator.java b/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/state/RandomSignedStateGenerator.java index 88cb53aa9c2e..e6178ebe5fd4 100644 --- a/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/state/RandomSignedStateGenerator.java +++ b/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/state/RandomSignedStateGenerator.java @@ -141,10 +141,9 @@ public SignedState build() { registerMerkleStateRootClassIds(); if (state == null) { if (useBlockingState) { - stateInstance = new BlockingSwirldState(); + stateInstance = new BlockingState(); } else { - stateInstance = new PlatformMerkleStateRoot( - FAKE_MERKLE_STATE_LIFECYCLES, version -> new BasicSoftwareVersion(version.major())); + stateInstance = new PlatformMerkleStateRoot(version -> new BasicSoftwareVersion(version.major())); } stateInstance.setTime(Time.getCurrent()); } else { @@ -199,7 +198,7 @@ public SignedState build() { } else { consensusSnapshotInstance = consensusSnapshot; } - FAKE_MERKLE_STATE_LIFECYCLES.initPlatformState((MerkleStateRoot) stateInstance); + FAKE_MERKLE_STATE_LIFECYCLES.initPlatformState(stateInstance); final PlatformStateModifier platformState = stateInstance.getWritablePlatformState(); platformState.bulkUpdate(v -> { @@ -210,7 +209,7 @@ public SignedState build() { v.setConsensusTimestamp(consensusTimestampInstance); }); - FAKE_MERKLE_STATE_LIFECYCLES.initRosterState((MerkleStateRoot) stateInstance); + FAKE_MERKLE_STATE_LIFECYCLES.initRosterState(stateInstance); RosterUtils.setActiveRoster((State) stateInstance, rosterInstance, roundInstance); if (signatureVerifier == null) { @@ -456,10 +455,10 @@ public RandomSignedStateGenerator setPcesRound(final boolean pcesRound) { } /** - * Set if this state should use a {@link BlockingSwirldState} instead of a {@link MerkleStateRoot}. + * Set if this state should use a {@link BlockingState} instead of a {@link MerkleStateRoot}. * This flag is fasle by default. * - * @param useBlockingState true if this state should use {@link BlockingSwirldState} + * @param useBlockingState true if this state should use {@link BlockingState} * @return this object */ public RandomSignedStateGenerator setUseBlockingState(boolean useBlockingState) { diff --git a/platform-sdk/swirlds-platform-core/src/timingSensitive/java/com/swirlds/platform/components/appcomm/LatestCompleteStateNotifierTests.java b/platform-sdk/swirlds-platform-core/src/timingSensitive/java/com/swirlds/platform/components/appcomm/LatestCompleteStateNotifierTests.java index 362e031bf41c..48141e8e660a 100644 --- a/platform-sdk/swirlds-platform-core/src/timingSensitive/java/com/swirlds/platform/components/appcomm/LatestCompleteStateNotifierTests.java +++ b/platform-sdk/swirlds-platform-core/src/timingSensitive/java/com/swirlds/platform/components/appcomm/LatestCompleteStateNotifierTests.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. @@ -54,9 +54,9 @@ void testNewLatestCompleteStateEventNotification() throws InterruptedException { try { assertThat(senderLatch.await(1, TimeUnit.SECONDS)).isTrue(); assertFalse( - n.getSwirldState().isDestroyed(), - "SwirldState should not be destroyed until the callback has completed"); - assertEquals(signedState.getSwirldState(), n.getSwirldState(), "Unexpected SwirldState"); + n.getStateRoot().isDestroyed(), + "State should not be destroyed until the callback has completed"); + assertEquals(signedState.getState(), n.getStateRoot(), "Unexpected State"); listenerLatch.countDown(); } catch (InterruptedException e) { throw new RuntimeException(e); diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/SignedStateUtils.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/SignedStateUtils.java index 6375f86ccc70..0fedf0e1a1e8 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/SignedStateUtils.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/SignedStateUtils.java @@ -34,8 +34,8 @@ public static SignedState randomSignedState(long seed) { } public static SignedState randomSignedState(Random random) { - PlatformMerkleStateRoot root = new PlatformMerkleStateRoot( - FAKE_MERKLE_STATE_LIFECYCLES, version -> new BasicSoftwareVersion(version.minor())); + PlatformMerkleStateRoot root = + new PlatformMerkleStateRoot(version -> new BasicSoftwareVersion(version.minor())); FAKE_MERKLE_STATE_LIFECYCLES.initPlatformState(root); randomPlatformState(random, root.getWritablePlatformState()); boolean shouldSaveToDisk = random.nextBoolean(); diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/StateTest.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/StateTest.java index 211bf9a06d54..1ed3fe4f3233 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/StateTest.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/StateTest.java @@ -16,7 +16,6 @@ package com.swirlds.platform.test; -import static com.swirlds.platform.test.fixtures.state.FakeStateLifecycles.FAKE_MERKLE_STATE_LIFECYCLES; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotSame; @@ -84,8 +83,8 @@ void tryReserveTest() { private static SignedState randomSignedState() { Random random = new Random(0); - PlatformMerkleStateRoot merkleStateRoot = new PlatformMerkleStateRoot( - FAKE_MERKLE_STATE_LIFECYCLES, version -> new BasicSoftwareVersion(version.major())); + PlatformMerkleStateRoot merkleStateRoot = + new PlatformMerkleStateRoot(version -> new BasicSoftwareVersion(version.major())); boolean shouldSaveToDisk = random.nextBoolean(); SignedState signedState = new SignedState( TestPlatformContextBuilder.create().build().getConfiguration(), diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/StateTests.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/StateTests.java index f06478b51f4a..592130444220 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/StateTests.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/StateTests.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. @@ -27,7 +27,7 @@ import com.swirlds.common.test.fixtures.junit.tags.TestComponentTags; import com.swirlds.config.extensions.test.fixtures.TestConfigBuilder; import com.swirlds.platform.state.PlatformMerkleStateRoot; -import com.swirlds.platform.test.fixtures.state.BlockingSwirldState; +import com.swirlds.platform.test.fixtures.state.BlockingState; import com.swirlds.state.merkle.MerkleStateRoot; import java.io.IOException; import java.nio.file.Path; @@ -53,7 +53,7 @@ static void setUp() throws ConstructableRegistryException { ConstructableRegistry.getInstance().registerConstructables("com.swirlds"); - merkleStateRoot = new BlockingSwirldState(); + merkleStateRoot = new BlockingState(); merkleStateRoot.invalidateHash(); MerkleCryptoFactory.getInstance().digestTreeSync(merkleStateRoot); From 418e6d3b1ee421df71e7baf1d3f44382dc3cec74 Mon Sep 17 00:00:00 2001 From: Michael Tinker Date: Tue, 14 Jan 2025 19:17:44 -0600 Subject: [PATCH 16/19] chore: Add placeholder `HintsService`, `HistoryService`, integrate with app infrastructure (#17222) Signed-off-by: Michael Tinker Co-authored-by: Neeharika Sompalli <52669918+Neeharika-Sompalli@users.noreply.github.com> --- .../main/java/com/hedera/node/app/Hedera.java | 45 +++- .../node/app/HederaInjectionComponent.java | 8 + .../com/hedera/node/app/ServicesMain.java | 4 + .../hedera/node/app/hints/HintsLibrary.java | 136 +++++++++++ .../hedera/node/app/hints/HintsService.java | 118 ++++++++++ .../node/app/hints/ReadableHintsStore.java | 34 +++ .../node/app/hints/WritableHintsStore.java | 22 ++ .../app/hints/impl/HintsLibraryCodec.java | 103 +++++++++ .../node/app/hints/impl/HintsServiceImpl.java | 73 ++++++ .../hints/impl/ReadableHintsStoreImpl.java | 40 ++++ .../hints/impl/WritableHintsStoreImpl.java | 30 +++ .../node/app/history/HistoryLibrary.java | 100 ++++++++ .../node/app/history/HistoryService.java | 81 +++++++ .../app/history/ReadableHistoryStore.java | 22 ++ .../app/history/WritableHistoryStore.java | 22 ++ .../app/history/impl/HistoryLibraryCodec.java | 64 ++++++ .../app/history/impl/HistoryServiceImpl.java | 67 ++++++ .../impl/ReadableHistoryStoreImpl.java | 32 +++ .../impl/WritableHistoryStoreImpl.java | 27 +++ .../hedera/node/app/roster/ActiveRosters.java | 206 +++++++++++++++++ .../app/roster/RosterTransitionWeights.java | 215 ++++++++++++++++++ .../node/app/tss/TssBlockHashSigner.java | 62 ++++- .../app/workflows/handle/HandleWorkflow.java | 54 ++++- .../hedera-app/src/main/java/module-info.java | 20 ++ .../app/components/IngestComponentTest.java | 6 + .../app/hints/impl/HintsLibraryCodecTest.java | 36 +++ .../app/hints/impl/HintsServiceImplTest.java | 79 +++++++ .../impl/ReadableHintsStoreImplTest.java | 45 ++++ .../impl/WritableHintsStoreImplTest.java | 44 ++++ .../history/impl/HistoryLibraryCodecTest.java | 34 +++ .../history/impl/HistoryServiceImplTest.java | 74 ++++++ .../impl/ReadableHistoryStoreImplTest.java | 44 ++++ .../impl/WritableHistoryStoreImplTest.java | 45 ++++ .../node/app/roster/ActiveRostersTest.java | 150 ++++++++++++ .../node/app/tss/TssBlockHashSignerTest.java | 146 ++++++++++++ .../workflows/handle/HandleWorkflowTest.java | 12 +- .../hedera/node/config/data/TssConfig.java | 6 +- .../src/main/java/module-info.java | 16 ++ .../embedded/AbstractEmbeddedHedera.java | 7 +- .../fakes/LapsingBlockHashSigner.java | 10 +- .../suites/records/RecordCreationSuite.java | 7 +- .../state/service/ReadableRosterStore.java | 8 +- .../service/ReadableRosterStoreImpl.java | 11 +- .../src/main/java/module-info.java | 16 ++ .../service/ReadableRosterStoreImplTest.java | 65 ++++++ 45 files changed, 2424 insertions(+), 22 deletions(-) create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/HintsLibrary.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/HintsService.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/ReadableHintsStore.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/WritableHintsStore.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/impl/HintsLibraryCodec.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/impl/HintsServiceImpl.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/impl/ReadableHintsStoreImpl.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/impl/WritableHintsStoreImpl.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/HistoryLibrary.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/HistoryService.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/ReadableHistoryStore.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/WritableHistoryStore.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/impl/HistoryLibraryCodec.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/impl/HistoryServiceImpl.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/impl/ReadableHistoryStoreImpl.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/impl/WritableHistoryStoreImpl.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/roster/ActiveRosters.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/roster/RosterTransitionWeights.java create mode 100644 hedera-node/hedera-app/src/test/java/com/hedera/node/app/hints/impl/HintsLibraryCodecTest.java create mode 100644 hedera-node/hedera-app/src/test/java/com/hedera/node/app/hints/impl/HintsServiceImplTest.java create mode 100644 hedera-node/hedera-app/src/test/java/com/hedera/node/app/hints/impl/ReadableHintsStoreImplTest.java create mode 100644 hedera-node/hedera-app/src/test/java/com/hedera/node/app/hints/impl/WritableHintsStoreImplTest.java create mode 100644 hedera-node/hedera-app/src/test/java/com/hedera/node/app/history/impl/HistoryLibraryCodecTest.java create mode 100644 hedera-node/hedera-app/src/test/java/com/hedera/node/app/history/impl/HistoryServiceImplTest.java create mode 100644 hedera-node/hedera-app/src/test/java/com/hedera/node/app/history/impl/ReadableHistoryStoreImplTest.java create mode 100644 hedera-node/hedera-app/src/test/java/com/hedera/node/app/history/impl/WritableHistoryStoreImplTest.java create mode 100644 hedera-node/hedera-app/src/test/java/com/hedera/node/app/roster/ActiveRostersTest.java create mode 100644 hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/TssBlockHashSignerTest.java create mode 100644 platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/service/ReadableRosterStoreImplTest.java diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/Hedera.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/Hedera.java index 41cfa0572179..22e4e95cd276 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/Hedera.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/Hedera.java @@ -72,6 +72,8 @@ import com.hedera.node.app.config.BootstrapConfigProviderImpl; import com.hedera.node.app.config.ConfigProviderImpl; import com.hedera.node.app.fees.FeeService; +import com.hedera.node.app.hints.HintsService; +import com.hedera.node.app.history.HistoryService; import com.hedera.node.app.ids.EntityIdService; import com.hedera.node.app.info.CurrentPlatformStatusImpl; import com.hedera.node.app.info.GenesisNetworkInfo; @@ -268,6 +270,18 @@ public final class Hedera */ private final BlockStreamService blockStreamService; + /** + * The hinTS service singleton, kept as a field here to avoid constructing twice + * (once in constructor to register schemas, again inside Dagger component). + */ + private final HintsService hintsService; + + /** + * The history service singleton, kept as a field here to avoid constructing twice + * (once in constructor to register schemas, again inside Dagger component). + */ + private final HistoryService historyService; + /** * The block hash signer factory. */ @@ -366,10 +380,25 @@ public interface StartupNetworksFactory { StartupNetworks apply(@NonNull ConfigProvider configProvider); } + @FunctionalInterface + public interface HintsServiceFactory { + @NonNull + HintsService apply(@NonNull AppContext appContext); + } + + @FunctionalInterface + public interface HistoryServiceFactory { + @NonNull + HistoryService apply(@NonNull AppContext appContext); + } + @FunctionalInterface public interface BlockHashSignerFactory { @NonNull - BlockHashSigner apply(); + BlockHashSigner apply( + @NonNull HintsService hintsService, + @NonNull HistoryService historyService, + @NonNull ConfigProvider configProvider); } /*================================================================================================================== @@ -390,6 +419,8 @@ public interface BlockHashSignerFactory { * @param migrator the migrator to use with the services * @param startupNetworksFactory the factory for the startup networks * @param blockHashSignerFactory the factory for the block hash signer + * @param hintsServiceFactory the factory for the hints service + * @param historyServiceFactory the factory for the history service * @param metrics the metrics object to use for reporting */ public Hedera( @@ -399,6 +430,8 @@ public Hedera( @NonNull final InstantSource instantSource, @NonNull final StartupNetworksFactory startupNetworksFactory, @NonNull final BlockHashSignerFactory blockHashSignerFactory, + @NonNull final HintsServiceFactory hintsServiceFactory, + @NonNull final HistoryServiceFactory historyServiceFactory, @NonNull final Metrics metrics) { requireNonNull(registryFactory); requireNonNull(constructableRegistry); @@ -444,6 +477,8 @@ public Hedera( () -> daggerApp.workingStateAccessor().getState(), () -> daggerApp.throttleServiceManager().activeThrottleDefinitionsOrThrow(), ThrottleAccumulator::new)); + hintsService = hintsServiceFactory.apply(appContext); + historyService = historyServiceFactory.apply(appContext); contractServiceImpl = new ContractServiceImpl(appContext); scheduleServiceImpl = new ScheduleServiceImpl(); blockStreamService = new BlockStreamService(); @@ -454,6 +489,8 @@ public Hedera( contractServiceImpl, fileServiceImpl, new TssBaseServiceImpl(), + hintsService, + historyService, new FreezeServiceImpl(), scheduleServiceImpl, new TokenServiceImpl(), @@ -1066,6 +1103,7 @@ private void initializeDagger(@NonNull final State state, @NonNull final InitTri final var networkInfo = new StateNetworkInfo(platform.getSelfId().id(), state, platform.getRoster(), configProvider); + final var blockHashSigner = blockHashSignerFactory.apply(hintsService, historyService, configProvider); // Fully qualified so as to not confuse javadoc daggerApp = com.hedera.node.app.DaggerHederaInjectionComponent.builder() .configProviderImpl(configProvider) @@ -1090,8 +1128,9 @@ private void initializeDagger(@NonNull final State state, @NonNull final InitTri .initialStateHash(initialStateHash) .networkInfo(networkInfo) .startupNetworks(startupNetworks) - // (FUTURE) Pass the HintsService and HistoryService to this factory - .blockHashSigner(blockHashSignerFactory.apply()) + .hintsService(hintsService) + .historyService(historyService) + .blockHashSigner(blockHashSigner) .build(); // Initialize infrastructure for fees, exchange rates, and throttles from the working state daggerApp.initializer().accept(state); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/HederaInjectionComponent.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/HederaInjectionComponent.java index f2b45fca06c5..47c3e1af9525 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/HederaInjectionComponent.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/HederaInjectionComponent.java @@ -33,6 +33,8 @@ import com.hedera.node.app.fees.FeeManager; import com.hedera.node.app.grpc.GrpcInjectionModule; import com.hedera.node.app.grpc.GrpcServerManager; +import com.hedera.node.app.hints.HintsService; +import com.hedera.node.app.history.HistoryService; import com.hedera.node.app.info.CurrentPlatformStatus; import com.hedera.node.app.info.InfoInjectionModule; import com.hedera.node.app.metrics.MetricsInjectionModule; @@ -190,6 +192,12 @@ interface Builder { @BindsInstance Builder blockHashSigner(BlockHashSigner blockHashSigner); + @BindsInstance + Builder hintsService(HintsService hintsService); + + @BindsInstance + Builder historyService(HistoryService historyService); + @BindsInstance Builder instantSource(InstantSource instantSource); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/ServicesMain.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/ServicesMain.java index 52baf92d6418..1a3232752911 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/ServicesMain.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/ServicesMain.java @@ -41,6 +41,8 @@ import static java.util.Objects.requireNonNull; import com.google.common.annotations.VisibleForTesting; +import com.hedera.node.app.hints.impl.HintsServiceImpl; +import com.hedera.node.app.history.impl.HistoryServiceImpl; import com.hedera.node.app.info.DiskStartupNetworks; import com.hedera.node.app.roster.RosterService; import com.hedera.node.app.service.addressbook.AddressBookService; @@ -410,6 +412,8 @@ public static Hedera newHedera(@NonNull final Metrics metrics) { InstantSource.system(), DiskStartupNetworks::new, TssBlockHashSigner::new, + HintsServiceImpl::new, + HistoryServiceImpl::new, metrics); } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/HintsLibrary.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/HintsLibrary.java new file mode 100644 index 000000000000..bf9a7854d100 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/HintsLibrary.java @@ -0,0 +1,136 @@ +/* + * 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. + * 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.hints; + +import com.hedera.node.app.hints.impl.HintsLibraryCodec; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Map; + +/** + * The cryptographic operations required by the {@link HintsService}. The relationship between the hinTS algorithms + * and these operations are as follows: + *

      + *
    • Key generation ({@code KGen}) - Implemented by {@link HintsLibrary#newBlsKeyPair()}.
    • + *
    • Hint generation ({@code HintGen}) - Implemented by {@link HintsLibrary#computeHints(Bytes, int, int)}.
    • + *
    • Preprocessing ({@code Preprocess}) - Implemented by using {@link HintsLibrary#preprocess(Map, Map, int)} + * to select the hinTS keys to use as input to {@link HintsLibrary#preprocess(Map, Map, int)}.
    • + *
    • Partial signatures ({@code Sign}) - Implemented by {@link HintsLibrary#signBls(Bytes, Bytes)}.
    • + *
    • Verifying partial signatures ({@code PartialVerify}) - Implemented by using + * {@link HintsLibrary#verifyBls(Bytes, Bytes, Bytes)} with public keys extracted from the + * aggregation key in the active hinTS scheme via {@link HintsLibraryCodec#extractPublicKey(Bytes, int)}.
    • + *
    • Signature aggregation ({@code SignAggr}) - Implemented by {@link HintsLibrary#aggregateSignatures(Bytes, Bytes, Map)} + * with partial signatures verified as above with weights extracted from the aggregation key in the active hinTS + * scheme via {@link HintsLibraryCodec#extractWeight(Bytes, int)} and {@link HintsLibraryCodec#extractTotalWeight(Bytes)}.
    • + *
    • Verifying aggregate signatures ({@code Verify}) - Implemented by + * {@link HintsLibrary#verifyAggregate(Bytes, Bytes, Bytes, long, long)}.
    • + *
    + */ +public interface HintsLibrary { + /** + * Generates a new BLS key pair. + * @return the key pair + */ + Bytes newBlsKeyPair(); + + /** + * Computes the hints for the given public key and number of parties. + * + * @param blsPrivateKey the private key + * @param partyId the party id + * @param n the number of parties + * @return the hints + */ + Bytes computeHints(@NonNull Bytes blsPrivateKey, int partyId, int n); + + /** + * Validates the hinTS public key for the given number of parties. + * + * @param hintsKey the hinTS key + * @param partyId the party id + * @param n the number of parties + * @return true if the hints are valid; false otherwise + */ + boolean validateHintsKey(@NonNull Bytes hintsKey, int partyId, int n); + + /** + * Runs the hinTS preprocessing algorithm on the given validated hint keys and party weights for the given number + * of parties. The output includes, + *
      + *
    1. The linear size aggregation key to use in combining partial signatures on a message with a provably + * well-formed aggregate public key.
    2. + *
    3. The succinct verification key to use when verifying an aggregate signature.
    4. + *
    + * Both maps given must have the same key set; in particular, a subset of {@code [0, n)}. + * @param hintsKeys the valid hinTS keys by party id + * @param weights the weights by party id + * @param n the number of parties + * @return the preprocessed keys + */ + Bytes preprocess(@NonNull Map hintsKeys, @NonNull Map weights, int n); + + /** + * Signs a message with a BLS private key. + * + * @param message the message + * @param privateKey the private key + * @return the signature + */ + Bytes signBls(@NonNull Bytes message, @NonNull Bytes privateKey); + + /** + * Checks that a signature on a message verifies under a BLS public key. + * + * @param signature the signature + * @param message the message + * @param publicKey the public key + * @return true if the signature is valid; false otherwise + */ + boolean verifyBls(@NonNull Bytes signature, @NonNull Bytes message, @NonNull Bytes publicKey); + + /** + * Aggregates the signatures for party ids using hinTS aggregation and verification keys. + * + * @param aggregationKey the aggregation key + * @param verificationKey the verification key + * @param partialSignatures the partial signatures by party id + * @return the aggregated signature + */ + Bytes aggregateSignatures( + @NonNull Bytes aggregationKey, + @NonNull Bytes verificationKey, + @NonNull Map partialSignatures); + + /** + * Checks an aggregate signature on a message verifies under a hinTS verification key, where + * this is only true if the aggregate signature has weight exceeding the specified threshold + * or total weight stipulated in the verification key. + * + * @param signature the aggregate signature + * @param message the message + * @param verificationKey the verification key + * @param thresholdNumerator the numerator of a fraction of total weight the signature must have + * @param thresholdDenominator the denominator of a fraction of total weight the signature must have + * @return true if the signature is valid; false otherwise + */ + boolean verifyAggregate( + @NonNull Bytes signature, + @NonNull Bytes message, + @NonNull Bytes verificationKey, + long thresholdNumerator, + long thresholdDenominator); +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/HintsService.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/HintsService.java new file mode 100644 index 000000000000..d63ed8f2604b --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/HintsService.java @@ -0,0 +1,118 @@ +/* + * 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. + * 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.hints; + +import com.hedera.node.app.blocks.BlockHashSigner; +import com.hedera.node.app.roster.ActiveRosters; +import com.hedera.node.app.roster.RosterService; +import com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory; +import com.hedera.node.config.data.TssConfig; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.state.lifecycle.SchemaRegistry; +import com.swirlds.state.lifecycle.Service; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.time.Instant; + +/** + * Orchestrates the hinTS algorithms for, + *
      + *
    1. Silent setup of party keys and hints.
    2. + *
    3. Preprocessing and acceptance of aggregation and verification keys.
    4. + *
    5. Aggregating partial signatures under a provably well-formed aggregate public key.
    6. + *
    + * Note that only the first two of these require deterministic orchestration, since any + * valid aggregation of partial signatures that exceeds the threshold works equally well. But a + * strong minority of the current roster must reach deterministic consensus on the first two + * steps before the third can be attempted. + *

    + * All the expensive cryptographic work being orchestrated happens in threads spawned by the + * service, and not in the {@code preHandle} or {@code handleTransaction} threads as + * they dispatch to the service's {@link com.hedera.node.app.spi.workflows.TransactionHandler}s. + * When a background thread finishes some costly cryptographic work for a node, it gossips + * the result to the rest of the network via a {@link TransactionCategory#NODE} transaction. + *

    + * All honest nodes will incorporate results deterministically, depending on the type of + * result. If it is an individual result like a hint, then all honest nodes will adhere to + * a deterministic policy for adopting a particular set of valid results. If it is an + * aggregate result like a verification key, all honest nodes will wait to adopt it until + * a strong minority of the current roster has approved that exact result. + *

    + * The service only supports orchestration of one ongoing hinTS construction at a time, + * and if requested to orchestrate a different construction, will abandon all in-progress + * work. + */ +public interface HintsService extends Service, BlockHashSigner { + String NAME = "HintsService"; + + /** + * Since the roster service has to decide to adopt the candidate roster + * at an upgrade boundary based on availability of hinTS preprocessed + * keys, the hinTS service must be migrated before the roster service + * in the node's setup phase. (Contrast with the reverse order of + * dependency in the runtime phase; then the hinTS service depends + * on the roster service to know how to set up preprocessing work.) + */ + int MIGRATION_ORDER = RosterService.MIGRATION_ORDER - 1; + + /** + * Takes any actions needed to advance the state of the {@link HintsService} toward + * having completed the most up-to-date hinTS construction for the given {@link ActiveRosters}. + *

    + * Given active rosters with a source/target transition, this method will, + *

      + *
    1. Do nothing if a completed construction for the transition already exists in {@link HintsService}.
    2. + *
    3. If there is no active controller for the transition, create one based on the given consensus time and + * {@link HintsService} states; and save the created construction in network state if this is the first time + * the network ever began reconciling a hinTS construction for the transition.
    4. + *
    5. Use the resolved controller do advance progress toward the transition's completion.
    6. + *
    + *

    + * IMPORTANT: Note that whether a new controller, or an appropriate one already exists, its subsequent + * behavior will be a deterministic function of the given consensus time and {@link HintsService} states. That is, + * controllers are persistent objects onlydue to performance considerations, but are logically + * functions of just the network state and consensus time. + * @param activeRosters the active rosters + * @param hintsStore the hints store, for recording progress if needed + * @param now the current consensus time + * @param tssConfig the TSS configuration + */ + void reconcile( + @NonNull ActiveRosters activeRosters, + @NonNull WritableHintsStore hintsStore, + @NonNull Instant now, + @NonNull TssConfig tssConfig); + + /** + * Returns the current verification key for the active hinTS construction, or throws if it is incomplete. + * @throws IllegalStateException if the active hinTS construction is incomplete + */ + @NonNull + Bytes currentVerificationKeyOrThrow(); + + @Override + default int migrationOrder() { + return MIGRATION_ORDER; + } + + @Override + default @NonNull String getServiceName() { + return NAME; + } + + @Override + void registerSchemas(@NonNull SchemaRegistry registry); +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/ReadableHintsStore.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/ReadableHintsStore.java new file mode 100644 index 000000000000..c3ede9cdc5f4 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/ReadableHintsStore.java @@ -0,0 +1,34 @@ +/* + * 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.hints; + +import com.hedera.pbj.runtime.io.buffer.Bytes; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** + * Encapsulates information about the hints in state that are not directly scoped to a particular maximum party size. + */ +public interface ReadableHintsStore { + /** + * Returns the verification key for the given roster hash, if it exists. + * @param rosterHash the roster hash + * @return the verification key, or null if it does not exist + */ + @Nullable + Bytes getVerificationKeyFor(@NonNull Bytes rosterHash); +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/WritableHintsStore.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/WritableHintsStore.java new file mode 100644 index 000000000000..5e475426872f --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/WritableHintsStore.java @@ -0,0 +1,22 @@ +/* + * 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.hints; + +/** + * Provides write access to {@link HintsService} state. + */ +public interface WritableHintsStore extends ReadableHintsStore {} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/impl/HintsLibraryCodec.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/impl/HintsLibraryCodec.java new file mode 100644 index 000000000000..fdb0dc67b84e --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/impl/HintsLibraryCodec.java @@ -0,0 +1,103 @@ +/* + * 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.hints.impl; + +import static java.util.Objects.requireNonNull; + +import com.hedera.node.app.hints.HintsLibrary; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * Utility to extract information from byte arrays returned by the {@link HintsLibrary}, encode protobuf + * messages in the form the library expects, and so on. + */ +@Singleton +public class HintsLibraryCodec { + @Inject + public HintsLibraryCodec() { + // Dagger2 + } + + /** + * Encodes the given public key and hints into a hinTS key for use with the {@link HintsLibrary}. + * @param blsPublicKey the BLS public key + * @param hints the hints for the corresponding BLS private key + * @return the hinTS key + */ + public Bytes encodeHintsKey(@NonNull final Bytes blsPublicKey, @NonNull final Bytes hints) { + requireNonNull(blsPublicKey); + requireNonNull(hints); + throw new UnsupportedOperationException("Not implemented"); + } + + /** + * Extracts the aggregation key from the given preprocessed keys. + * @param preprocessedKeys the preprocessed keys + * @return the aggregation key + */ + public Bytes extractAggregationKey(@NonNull final Bytes preprocessedKeys) { + requireNonNull(preprocessedKeys); + throw new UnsupportedOperationException("Not implemented"); + } + + /** + * Extracts the verification key from the given preprocessed keys. + * @param preprocessedKeys the preprocessed keys + * @return the verification key + */ + public Bytes extractVerificationKey(@NonNull final Bytes preprocessedKeys) { + requireNonNull(preprocessedKeys); + throw new UnsupportedOperationException("Not implemented"); + } + + /** + * Extracts the public key for the given party id from the given aggregation key. + * @param aggregationKey the aggregation key + * @param partyId the party id + * @return the public key, or null if the party id is not present + */ + @Nullable + public Bytes extractPublicKey(@NonNull final Bytes aggregationKey, final int partyId) { + requireNonNull(aggregationKey); + throw new UnsupportedOperationException("Not implemented"); + } + + /** + * Extracts the weight of the given party id from the given aggregation key. + * @param aggregationKey the aggregation key + * @param partyId the party id + * @return the weight + */ + public long extractWeight(@NonNull final Bytes aggregationKey, final int partyId) { + requireNonNull(aggregationKey); + throw new UnsupportedOperationException("Not implemented"); + } + + /** + * Extracts the total weight of all parties from the given verification key. + * @param verificationKey the verification key + * @return the total weight + */ + public long extractTotalWeight(@NonNull final Bytes verificationKey) { + requireNonNull(verificationKey); + throw new UnsupportedOperationException("Not implemented"); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/impl/HintsServiceImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/impl/HintsServiceImpl.java new file mode 100644 index 000000000000..3db7dd4b6def --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/impl/HintsServiceImpl.java @@ -0,0 +1,73 @@ +/* + * 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.hints.impl; + +import static java.util.Objects.requireNonNull; + +import com.hedera.node.app.hints.HintsService; +import com.hedera.node.app.hints.WritableHintsStore; +import com.hedera.node.app.roster.ActiveRosters; +import com.hedera.node.app.spi.AppContext; +import com.hedera.node.config.data.TssConfig; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.state.lifecycle.SchemaRegistry; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.time.Instant; +import java.util.concurrent.CompletableFuture; + +/** + * Placeholder implementation of the {@link HintsService}. + */ +public class HintsServiceImpl implements HintsService { + public HintsServiceImpl(@NonNull final AppContext appContext) { + requireNonNull(appContext); + } + + @Override + public void reconcile( + @NonNull final ActiveRosters activeRosters, + @NonNull final WritableHintsStore hintsStore, + @NonNull final Instant now, + @NonNull final TssConfig tssConfig) { + requireNonNull(activeRosters); + requireNonNull(hintsStore); + requireNonNull(now); + requireNonNull(tssConfig); + throw new UnsupportedOperationException(); + } + + @Override + public @NonNull Bytes currentVerificationKeyOrThrow() { + throw new UnsupportedOperationException(); + } + + @Override + public void registerSchemas(@NonNull final SchemaRegistry registry) { + requireNonNull(registry); + } + + @Override + public boolean isReady() { + throw new UnsupportedOperationException(); + } + + @Override + public CompletableFuture signFuture(@NonNull final Bytes blockHash) { + requireNonNull(blockHash); + throw new UnsupportedOperationException(); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/impl/ReadableHintsStoreImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/impl/ReadableHintsStoreImpl.java new file mode 100644 index 000000000000..78c55ab1b2e1 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/impl/ReadableHintsStoreImpl.java @@ -0,0 +1,40 @@ +/* + * 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.hints.impl; + +import static java.util.Objects.requireNonNull; + +import com.hedera.node.app.hints.ReadableHintsStore; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.state.spi.ReadableStates; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** + * Placeholder implementation of the {@link ReadableHintsStore}. + */ +public class ReadableHintsStoreImpl implements ReadableHintsStore { + public ReadableHintsStoreImpl(@NonNull final ReadableStates states) { + requireNonNull(states); + } + + @Override + public @Nullable Bytes getVerificationKeyFor(@NonNull final Bytes rosterHash) { + requireNonNull(rosterHash); + throw new UnsupportedOperationException(); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/impl/WritableHintsStoreImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/impl/WritableHintsStoreImpl.java new file mode 100644 index 000000000000..5076167a1e42 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/hints/impl/WritableHintsStoreImpl.java @@ -0,0 +1,30 @@ +/* + * 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.hints.impl; + +import com.hedera.node.app.hints.WritableHintsStore; +import com.swirlds.state.spi.WritableStates; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Placeholder implementation of the {@link WritableHintsStore}. + */ +public class WritableHintsStoreImpl extends ReadableHintsStoreImpl implements WritableHintsStore { + public WritableHintsStoreImpl(@NonNull WritableStates states) { + super(states); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/HistoryLibrary.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/HistoryLibrary.java new file mode 100644 index 000000000000..883cd49aaf71 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/HistoryLibrary.java @@ -0,0 +1,100 @@ +/* + * 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.history; + +import com.hedera.pbj.runtime.io.buffer.Bytes; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Map; + +/** + * The cryptographic operations required by the {@link HistoryService}. + */ +public interface HistoryLibrary { + /** + * Returns the SNARK verification key in use by this library. + *

    + * Important: If this changes, the ledger id must also change. + */ + Bytes snarkVerificationKey(); + + /** + * Returns a new Schnorr key pair. + */ + Bytes newSchnorrKeyPair(); + + /** + * Signs a message with a Schnorr private key. In Hiero TSS, this will always be the concatenation + * of an address book hash and the associated metadata. + * + * @param message the message + * @param privateKey the private key + * @return the signature + */ + Bytes signSchnorr(@NonNull Bytes message, @NonNull Bytes privateKey); + + /** + * Checks that a signature on a message verifies under a Schnorr public key. + * + * @param signature the signature + * @param message the message + * @param publicKey the public key + * @return true if the signature is valid; false otherwise + */ + boolean verifySchnorr(@NonNull Bytes signature, @NonNull Bytes message, @NonNull Bytes publicKey); + + /** + * Computes the hash of the given address book with the same algorithm used by the SNARK circuit. + * @param addressBook the address book + * @return the hash of the address book + */ + Bytes hashAddressBook(@NonNull Bytes addressBook); + + /** + * Returns a SNARK recursively proving the target address book and associated metadata belong to the given ledger + * id's chain of trust that includes the given source address book, based on its own proof of belonging. (Unless the + * source address book hash is the ledger id, which is the base case of the recursion). + * + * @param ledgerId the ledger id, the concatenation of the genesis address book hash and the SNARK verification key + * @param sourceProof if not null, the proof the source address book is in the ledger id's chain of trust + * @param sourceAddressBook the source roster + * @param sourceSignatures the source address book signatures on the target address book hash and its metadata + * @param targetAddressBookHash the hash of the target address book + * @param targetMetadata the metadata of the target address book + * @return the SNARK proving the target address book and metadata belong to the ledger id's chain of trust + */ + @NonNull + Bytes proveChainOfTrust( + @NonNull Bytes ledgerId, + @Nullable Bytes sourceProof, + @NonNull Bytes sourceAddressBook, + @NonNull Map sourceSignatures, + @NonNull Bytes targetAddressBookHash, + @NonNull Bytes targetMetadata); + + /** + * Verifies the given SNARK proves the given address book hash and associated metadata belong to the given + * ledger id's chain of trust + * @param ledgerId the ledger id + * @param addressBookHash the hash of the address book + * @param metadata the metadata associated to the address book + * @param proof the SNARK proving the address book hash and metadata belong to the ledger id's chain of trust + * @return true if the proof is valid; false otherwise + */ + boolean verifyChainOfTrust( + @NonNull Bytes ledgerId, @NonNull Bytes addressBookHash, @NonNull Bytes metadata, @NonNull Bytes proof); +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/HistoryService.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/HistoryService.java new file mode 100644 index 000000000000..385c331ddeaf --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/HistoryService.java @@ -0,0 +1,81 @@ +/* + * 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. + * 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.history; + +import com.hedera.node.app.roster.ActiveRosters; +import com.hedera.node.app.roster.RosterService; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.state.lifecycle.Service; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.time.Instant; + +/** + * Proves inclusion of metadata in a history of rosters. + */ +public interface HistoryService extends Service { + String NAME = "HistoryService"; + + @Override + default @NonNull String getServiceName() { + return NAME; + } + + /** + * Since the roster service has to decide to adopt the candidate roster + * at an upgrade boundary based on availability of TSS signatures on + * blocks produced by that roster, the history service must be migrated + * before the roster service in the node's setup phase. (Contrast + * with the reverse order of dependency in the runtime phase; then + * the history service depends on the roster service to know how to set up + * its ongoing construction work for roster transitions.) + */ + int MIGRATION_ORDER = RosterService.MIGRATION_ORDER - 1; + + @Override + default int migrationOrder() { + return MIGRATION_ORDER; + } + + /** + * Whether this service is ready to provide metadata-enriched proofs. + */ + boolean isReady(); + + /** + * Reconciles the history of roster proofs with the given active rosters and metadata, if known. + * @param activeRosters the active rosters + * @param currentMetadata the current metadata, if known + * @param historyStore the history store + * @param now the current time + */ + void reconcile( + @NonNull ActiveRosters activeRosters, + @Nullable Bytes currentMetadata, + @NonNull WritableHistoryStore historyStore, + @NonNull Instant now); + + /** + * Returns a proof of inclusion of the given metadata for the current roster. + * @param metadata the metadata that must be included in the proof + * @return the proof + * @throws IllegalStateException if the service is not ready + * @throws IllegalArgumentException if the metadata for the current roster does not match the given metadata + */ + @NonNull + Bytes getCurrentProof(@NonNull Bytes metadata); +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/ReadableHistoryStore.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/ReadableHistoryStore.java new file mode 100644 index 000000000000..3aec09e501ef --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/ReadableHistoryStore.java @@ -0,0 +1,22 @@ +/* + * 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.history; + +/** + * Provides read access to the {@link HistoryService} state. + */ +public interface ReadableHistoryStore {} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/WritableHistoryStore.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/WritableHistoryStore.java new file mode 100644 index 000000000000..690cdb0b7ff6 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/WritableHistoryStore.java @@ -0,0 +1,22 @@ +/* + * 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.history; + +/** + * Provides write access to the {@link HistoryService} state. + */ +public interface WritableHistoryStore extends ReadableHistoryStore {} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/impl/HistoryLibraryCodec.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/impl/HistoryLibraryCodec.java new file mode 100644 index 000000000000..32f66d776865 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/impl/HistoryLibraryCodec.java @@ -0,0 +1,64 @@ +/* + * 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.history.impl; + +import static java.util.Objects.requireNonNull; + +import com.hedera.hapi.node.state.roster.Roster; +import com.hedera.node.app.history.HistoryLibrary; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Map; +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * Utility to extract information from byte arrays returned by the {@link HistoryLibrary}, encode protobuf + * messages in the form the library expects, and so on. + */ +@Singleton +public class HistoryLibraryCodec { + @Inject + public HistoryLibraryCodec() { + // Dagger2 + } + + /** + * Encodes the given address book hash and metadata into a history record to be signed via + * {@link HistoryLibrary#signSchnorr(Bytes, Bytes)}. + * @param addressBookHash an address book hash + * @param metadata the metadata for the address book + * @return the bytes for signing + */ + public @NonNull Bytes encodeHistory(@NonNull final Bytes addressBookHash, @NonNull final Bytes metadata) { + requireNonNull(addressBookHash); + requireNonNull(metadata); + throw new UnsupportedOperationException("Not implemented"); + } + + /** + * Encodes the given roster and public keys into an address book for use with the {@link HistoryLibrary}. + * @param roster the roster + * @param publicKeys the available Schnorr public keys for the nodes in the roster + * @return the history address book + */ + public @NonNull Bytes encodeAddressBook(@NonNull final Roster roster, @NonNull final Map publicKeys) { + requireNonNull(roster); + requireNonNull(publicKeys); + throw new UnsupportedOperationException("Not implemented"); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/impl/HistoryServiceImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/impl/HistoryServiceImpl.java new file mode 100644 index 000000000000..338beeccf4b1 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/impl/HistoryServiceImpl.java @@ -0,0 +1,67 @@ +/* + * 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.history.impl; + +import static java.util.Objects.requireNonNull; + +import com.hedera.node.app.history.HistoryService; +import com.hedera.node.app.history.WritableHistoryStore; +import com.hedera.node.app.roster.ActiveRosters; +import com.hedera.node.app.spi.AppContext; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.state.lifecycle.SchemaRegistry; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.time.Instant; + +/** + * Placeholder implementation of the {@link HistoryService}. + */ +public class HistoryServiceImpl implements HistoryService { + public HistoryServiceImpl(@NonNull final AppContext appContext) { + requireNonNull(appContext); + } + + @Override + public boolean isReady() { + throw new UnsupportedOperationException(); + } + + @Override + public void reconcile( + @NonNull ActiveRosters activeRosters, + @Nullable Bytes currentMetadata, + @NonNull WritableHistoryStore historyStore, + @NonNull Instant now) { + requireNonNull(activeRosters); + requireNonNull(historyStore); + requireNonNull(now); + throw new UnsupportedOperationException(); + } + + @NonNull + @Override + public Bytes getCurrentProof(@NonNull final Bytes metadata) { + requireNonNull(metadata); + throw new UnsupportedOperationException(); + } + + @Override + public void registerSchemas(@NonNull final SchemaRegistry registry) { + requireNonNull(registry); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/impl/ReadableHistoryStoreImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/impl/ReadableHistoryStoreImpl.java new file mode 100644 index 000000000000..6d40769c4032 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/impl/ReadableHistoryStoreImpl.java @@ -0,0 +1,32 @@ +/* + * 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.history.impl; + +import static java.util.Objects.requireNonNull; + +import com.hedera.node.app.history.ReadableHistoryStore; +import com.swirlds.state.spi.ReadableStates; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Placeholder implementation of the {@link ReadableHistoryStore}. + */ +public class ReadableHistoryStoreImpl implements ReadableHistoryStore { + public ReadableHistoryStoreImpl(@NonNull final ReadableStates states) { + requireNonNull(states); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/impl/WritableHistoryStoreImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/impl/WritableHistoryStoreImpl.java new file mode 100644 index 000000000000..0b52489f6420 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/history/impl/WritableHistoryStoreImpl.java @@ -0,0 +1,27 @@ +/* + * 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.history.impl; + +import com.hedera.node.app.history.WritableHistoryStore; +import com.swirlds.state.spi.WritableStates; +import edu.umd.cs.findbugs.annotations.NonNull; + +public class WritableHistoryStoreImpl extends ReadableHistoryStoreImpl implements WritableHistoryStore { + public WritableHistoryStoreImpl(@NonNull final WritableStates states) { + super(states); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/roster/ActiveRosters.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/roster/ActiveRosters.java new file mode 100644 index 000000000000..30773bc958d1 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/roster/ActiveRosters.java @@ -0,0 +1,206 @@ +/* + * 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.roster; + +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.toMap; + +import com.hedera.hapi.node.state.roster.Roster; +import com.hedera.hapi.node.state.roster.RosterEntry; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.platform.state.service.ReadableRosterStore; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +/** + * Contains the active rosters for the {@link RosterService}'s current phase; and the transition from a + * source roster to a target roster, if applicable. + *

    + * Recall the {@link RosterService} has three phases, each of which involves either one or two active rosters; and + * possibly implies a roster transition: + *

      + *
    1. {@link Phase#BOOTSTRAP} - There is a single active roster, the genesis roster, with an implied + * special transition in which the genesis roster serves as both source and target.
    2. + *
    3. {@link Phase#TRANSITION} - There are two active rosters, the current roster and the + * candidate roster; and an explicit transition from the former to the latter.
    4. + *
    5. {@link Phase#HANDOFF} - There is only one active roster (the current roster), and there is no transition.
    6. + *
    + */ +public class ActiveRosters { + private static final int HEX_PREFIX_LENGTH = 6; + + private final Phase phase; + + @Nullable + private final Bytes sourceRosterHash; + + private final Bytes targetRosterHash; + private final Function lookup; + + /** + * The phase of the {@link RosterService} in which these active rosters are being used. + */ + public enum Phase { + /** + * The {@link RosterService} is in the bootstrap phase. + */ + BOOTSTRAP, + /** + * The {@link RosterService} is in a transition phase. + */ + TRANSITION, + /** + * The {@link RosterService} is in a handoff phase. + */ + HANDOFF, + } + + /** + * Returns the active rosters for a given {@link ReadableRosterStore}. + * + * @param rosterStore the roster store + * @return the active rosters for the given roster store + */ + public static ActiveRosters from(@NonNull final ReadableRosterStore rosterStore) { + final var currentRosterHash = requireNonNull(rosterStore.getCurrentRosterHash()); + final var candidateRosterHash = rosterStore.getCandidateRosterHash(); + if (candidateRosterHash == null) { + if (rosterStore.getPreviousRosterHash() == null) { + return new ActiveRosters(Phase.BOOTSTRAP, currentRosterHash, currentRosterHash, rosterStore::get); + } else { + return new ActiveRosters(Phase.HANDOFF, null, currentRosterHash, rosterStore::get); + } + } else { + return new ActiveRosters(Phase.TRANSITION, currentRosterHash, candidateRosterHash, rosterStore::get); + } + } + + private ActiveRosters( + @NonNull final Phase phase, + @Nullable final Bytes sourceRosterHash, + @NonNull final Bytes targetRosterHash, + @NonNull final Function lookup) { + this.phase = requireNonNull(phase); + this.lookup = requireNonNull(lookup); + this.sourceRosterHash = sourceRosterHash; + this.targetRosterHash = requireNonNull(targetRosterHash); + } + + /** + * Returns the phase of the {@link RosterService} in which these active rosters are being used. + */ + public Phase phase() { + return phase; + } + + /** + * Returns the related roster with the given hash, if one exists. + * + * @param rosterHash the hash of the roster to find + */ + public @Nullable Roster findRelatedRoster(@NonNull final Bytes rosterHash) { + return lookup.apply(rosterHash); + } + + /** + * Returns the current roster hash. + */ + public @NonNull Bytes currentRosterHash() { + return switch (phase) { + case BOOTSTRAP, HANDOFF -> targetRosterHash; + case TRANSITION -> requireNonNull(sourceRosterHash); + }; + } + + /** + * Assuming the {@link RosterService} is in a transition phase, returns the source roster hash. + * + * @throws IllegalStateException if the {@link RosterService} is not in a transition phase + */ + public @NonNull Bytes sourceRosterHash() { + return switch (phase) { + case BOOTSTRAP, TRANSITION -> requireNonNull(sourceRosterHash); + case HANDOFF -> throw new IllegalStateException("No source roster hash in handoff phase"); + }; + } + + /** + * Assuming the {@link RosterService} is in a transition phase, returns the target roster hash. + * + * @throws IllegalStateException if the {@link RosterService} is not in a transition phase + */ + public @NonNull Bytes targetRosterHash() { + return switch (phase) { + case BOOTSTRAP, TRANSITION -> targetRosterHash; + case HANDOFF -> throw new IllegalStateException("No target roster hash in handoff phase"); + }; + } + + /** + * Assuming the {@link RosterService} is in a transition phase, returns the target roster. + * + * @throws IllegalStateException if the {@link RosterService} is not in a transition phase + */ + public @NonNull Roster targetRoster() { + return switch (phase) { + case BOOTSTRAP, TRANSITION -> lookup.apply(targetRosterHash); + case HANDOFF -> throw new IllegalStateException("No target roster in handoff phase"); + }; + } + + /** + * Returns the current roster. + */ + public @NonNull Roster currentRoster() { + return lookup.apply(currentRosterHash()); + } + + /** + * Assuming the {@link RosterService} is in a transition phase, returns the transition weights + * from the source roster to the target roster. + * + * @throws IllegalStateException if the {@link RosterService} is not in a transition phase + */ + public RosterTransitionWeights transitionWeights() { + return switch (phase) { + case BOOTSTRAP, TRANSITION -> new RosterTransitionWeights( + weightsFrom(lookup.apply(sourceRosterHash)), weightsFrom(lookup.apply(targetRosterHash))); + case HANDOFF -> throw new IllegalStateException("No target roster in handoff phase"); + }; + } + + @Override + public String toString() { + return "ActiveRosters{" + "phase=" + + phase + ", source=" + + Optional.ofNullable(sourceRosterHash) + .map(ActiveRosters::toHex) + .orElse("") + ", target=" + + toHex(targetRosterHash) + '}'; + } + + private static String toHex(@NonNull final Bytes bytes) { + return bytes.toHex().substring(0, HEX_PREFIX_LENGTH) + "..."; + } + + private static @NonNull Map weightsFrom(@NonNull final Roster roster) { + return requireNonNull(roster).rosterEntries().stream().collect(toMap(RosterEntry::nodeId, RosterEntry::weight)); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/roster/RosterTransitionWeights.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/roster/RosterTransitionWeights.java new file mode 100644 index 000000000000..fb96fdbac467 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/roster/RosterTransitionWeights.java @@ -0,0 +1,215 @@ +/* + * 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. + * 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.roster; + +import static java.util.Objects.requireNonNull; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.math.BigInteger; +import java.util.Comparator; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +/** + * Represents the weights of the nodes in a roster transition. + * @param sourceNodeWeights the weights of the nodes in the source roster + * @param targetNodeWeights the weights of the nodes in the target roster + * @param sourceWeightThreshold the weight required for a strong minority in the source roster + * @param targetWeightThreshold the weight required for a strong minority in the target roster + */ +public record RosterTransitionWeights( + @NonNull Map sourceNodeWeights, + @NonNull Map targetNodeWeights, + long sourceWeightThreshold, + long targetWeightThreshold) { + + public RosterTransitionWeights( + @NonNull final Map sourceNodeWeights, @NonNull final Map targetNodeWeights) { + this( + requireNonNull(sourceNodeWeights), + requireNonNull(targetNodeWeights), + atLeastOneThirdOfTotal(sourceNodeWeights), + moreThanTwoThirdsOfTotal(targetNodeWeights)); + } + + /** + * Represents the weight of a node in a roster. + * @param nodeId the ID of the node + * @param weight the weight of the node + */ + public record NodeWeight(long nodeId, long weight) implements Comparable { + private static final Comparator NODE_ID_COMPARATOR = Comparator.comparingLong(NodeWeight::nodeId); + + @Override + public int compareTo(@NonNull final NodeWeight that) { + return NODE_ID_COMPARATOR.compare(this, that); + } + } + + /** + * Returns the source node ids in the target roster. + * @return the source node ids in the target roster + */ + public Set sourceNodeIds() { + return sourceNodeWeights.keySet(); + } + + /** + * Returns the target node ids in the source roster. + * @return the target node ids in the source roster + */ + public Set targetNodeIds() { + return targetNodeWeights.keySet(); + } + + /** + * Returns whether the source roster has a strict majority of weight in the target roster. + */ + public boolean sourceNodesHaveTargetThreshold() { + return sourceNodeWeights.keySet().stream() + .filter(targetNodeWeights::containsKey) + .mapToLong(targetNodeWeights::get) + .sum() + >= targetWeightThreshold; + } + + /** + * Returns the weights of the nodes in the source roster in ascending order of node ID. + * @return the weights of the nodes in the source roster + */ + public Stream orderedSourceWeights() { + return sourceNodeWeights.entrySet().stream() + .map(entry -> new NodeWeight(entry.getKey(), entry.getValue())) + .sorted(); + } + + /** + * Returns the weights of the nodes in the source roster in ascending order of node ID. + * @return the weights of the nodes in the source roster + */ + public Stream orderedTargetWeights() { + return targetNodeWeights.entrySet().stream() + .map(entry -> new NodeWeight(entry.getKey(), entry.getValue())) + .sorted(); + } + + /** + * Returns the weight of a node in the source roster. + * @param nodeId the ID of the node + * @return the weight of the node + */ + public long sourceWeightOf(final long nodeId) { + return sourceNodeWeights.getOrDefault(nodeId, 0L); + } + + /** + * Returns whether given node has an explicit weight in the target roster. + * @param nodeId the ID of the node + * @return whether the node has an explicit weight + */ + public boolean targetIncludes(final long nodeId) { + return targetNodeWeights.containsKey(nodeId); + } + + /** + * Returns the weight of a node in the target roster. + * @param nodeId the ID of the node + * @return the weight of the node + */ + public long targetWeightOf(final long nodeId) { + return targetNodeWeights.getOrDefault(nodeId, 0L); + } + + /** + * Returns the size of the target roster. + */ + public int targetRosterSize() { + return targetNodeWeights.size(); + } + + /** + * Returns the number of target node ids in the source roster. + * @return the number of target node ids in the source roster + */ + public int numTargetNodesInSource() { + return numTargetNodesIn(sourceNodeWeights.keySet()); + } + + /** + * Returns the number of target node ids in a given set of node ids. + * @param nodeIds the set of node ids + * @return the number of target node ids in the set + */ + public int numTargetNodesIn(@NonNull final Set nodeIds) { + return targetNodeWeights.keySet().stream() + .filter(nodeIds::contains) + .mapToInt(i -> 1) + .sum(); + } + + /** + * Returns the weight that would constitute a strong minority of the network weight for a roster. + * + * @param weights the weights of the nodes in the roster + * @return the weight required for a strong minority + */ + public static long atLeastOneThirdOfTotal(@NonNull final Map weights) { + requireNonNull(weights); + return atLeastOneThirdOfTotal( + weights.values().stream().mapToLong(Long::longValue).sum()); + } + + /** + * Returns a weight that, assuming no corruption beyond the Byzantine threshold, guarantees agreement + * from an honest node holding at non-zero weight. + * @param totalWeight the total weight of the network + * @return the weight required to guarantee agreement with an honest node holding non-zero weight + */ + public static long atLeastOneThirdOfTotal(final long totalWeight) { + // Since aBFT is unachievable with n/3 malicious weight, using the conclusion of n/3 weight + // ensures it the conclusion overlaps with the weight held by at least one honest node + return (totalWeight + 2) / 3; + } + + /** + * Returns the weight that, assuming no corruption beyond the Byzantine threshold, guarantees sufficient + * honest nodes to reach agreement backed by at least 1/3 of the total network weight. + * @param weights the weights of the nodes in the roster + * @return the weight required for consensus progress even with maximum Byzantine faults + */ + public static long moreThanTwoThirdsOfTotal(@NonNull final Map weights) { + requireNonNull(weights); + return moreThanTwoThirdsOfTotal( + weights.values().stream().mapToLong(Long::longValue).sum()); + } + + /** + * Returns the weight that, assuming no corruption beyond the Byzantine threshold, guarantees sufficient + * honest nodes to reach agreement backed by at least 1/3 of the total network weight. + * @param totalWeight the total weight of the network + * @return the weight required for consensus progress even with maximum Byzantine faults + */ + public static long moreThanTwoThirdsOfTotal(final long totalWeight) { + // Calculate (2 * totalWeight) / 3 + 1 with BigInteger + return BigInteger.valueOf(totalWeight) + .multiply(BigInteger.TWO) + .divide(BigInteger.valueOf(3)) + .add(BigInteger.ONE) + .longValueExact(); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssBlockHashSigner.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssBlockHashSigner.java index 7de486798567..c6824cbf8673 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssBlockHashSigner.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssBlockHashSigner.java @@ -20,15 +20,18 @@ import static java.util.Objects.requireNonNull; import com.hedera.node.app.blocks.BlockHashSigner; +import com.hedera.node.app.hints.HintsService; +import com.hedera.node.app.history.HistoryService; +import com.hedera.node.config.ConfigProvider; +import com.hedera.node.config.data.TssConfig; import com.hedera.pbj.runtime.io.buffer.Bytes; import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; import java.util.concurrent.CompletableFuture; -import javax.inject.Inject; import javax.inject.Singleton; /** - * FUTURE - * ------ * A {@link BlockHashSigner} that uses whatever parts of the TSS protocol are enabled to sign blocks. * That is, *
      @@ -71,19 +74,62 @@ */ @Singleton public class TssBlockHashSigner implements BlockHashSigner { - @Inject - public TssBlockHashSigner() { - // Dagger2 + @Nullable + private final HintsService hintsService; + + @Nullable + private final HistoryService historyService; + + public TssBlockHashSigner( + @NonNull final HintsService hintsService, + @NonNull final HistoryService historyService, + @NonNull final ConfigProvider configProvider) { + final var tssConfig = configProvider.getConfiguration().getConfigData(TssConfig.class); + this.hintsService = tssConfig.hintsEnabled() ? hintsService : null; + this.historyService = tssConfig.historyEnabled() ? historyService : null; } @Override public boolean isReady() { - return true; + return (hintsService == null || hintsService.isReady()) && (historyService == null || historyService.isReady()); } @Override public CompletableFuture signFuture(@NonNull final Bytes blockHash) { requireNonNull(blockHash); - return CompletableFuture.supplyAsync(() -> noThrowSha384HashOf(blockHash)); + if (!isReady()) { + throw new IllegalStateException("TSS protocol not ready to sign block hash " + blockHash); + } + if (historyService == null) { + if (hintsService == null) { + return CompletableFuture.supplyAsync(() -> noThrowSha384HashOf(blockHash)); + } else { + return hintsService.signFuture(blockHash); + } + } else { + final var vk = hintsService == null ? Bytes.EMPTY : hintsService.currentVerificationKeyOrThrow(); + final var proof = historyService.getCurrentProof(vk); + if (hintsService == null) { + return CompletableFuture.supplyAsync(() -> assemble(noThrowSha384HashOf(blockHash), vk, proof)); + } else { + return hintsService.signFuture(blockHash).thenApply(signature -> assemble(signature, vk, proof)); + } + } + } + + /** + * Assembles the ledger signature from the given components. + * + * @param signature the hinTS aggregate signature + * @param vk the hinTS verification key + * @param proof the recursive proof that the verification key was honestly assigned to the current roster + * @return the assembled signature + */ + static Bytes assemble(@NonNull final Bytes signature, @NonNull final Bytes vk, @NonNull final Bytes proof) { + final var buffer = ByteBuffer.wrap(new byte[(int) (signature.length() + vk.length() + proof.length())]); + signature.writeTo(buffer); + vk.writeTo(buffer); + proof.writeTo(buffer); + return Bytes.wrap(buffer.array()); } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleWorkflow.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleWorkflow.java index 8c1fe670ff5b..82af3b0d4dd0 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleWorkflow.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleWorkflow.java @@ -59,8 +59,15 @@ import com.hedera.node.app.blocks.impl.BoundaryStateChangeListener; import com.hedera.node.app.blocks.impl.KVStateChangeListener; import com.hedera.node.app.fees.ExchangeRateManager; +import com.hedera.node.app.hints.HintsService; +import com.hedera.node.app.hints.impl.ReadableHintsStoreImpl; +import com.hedera.node.app.hints.impl.WritableHintsStoreImpl; +import com.hedera.node.app.history.HistoryService; +import com.hedera.node.app.history.impl.WritableHistoryStoreImpl; import com.hedera.node.app.records.BlockRecordManager; import com.hedera.node.app.records.BlockRecordService; +import com.hedera.node.app.roster.ActiveRosters; +import com.hedera.node.app.roster.RosterService; import com.hedera.node.app.service.addressbook.AddressBookService; import com.hedera.node.app.service.addressbook.impl.WritableNodeStore; import com.hedera.node.app.service.addressbook.impl.helpers.AddressBookHelper; @@ -97,9 +104,11 @@ import com.hedera.node.config.data.BlockStreamConfig; import com.hedera.node.config.data.ConsensusConfig; import com.hedera.node.config.data.SchedulingConfig; +import com.hedera.node.config.data.TssConfig; import com.hedera.node.config.types.StreamMode; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; +import com.swirlds.platform.state.service.ReadableRosterStoreImpl; import com.swirlds.platform.system.InitTrigger; import com.swirlds.platform.system.Round; import com.swirlds.platform.system.transaction.ConsensusTransaction; @@ -153,6 +162,8 @@ public class HandleWorkflow { private final KVStateChangeListener kvStateChangeListener; private final BoundaryStateChangeListener boundaryStateChangeListener; private final ScheduleService scheduleService; + private final HintsService hintsService; + private final HistoryService historyService; // The last second since the epoch at which the metrics were updated; this does not affect transaction handling private long lastMetricUpdateSecond; @@ -184,7 +195,9 @@ public HandleWorkflow( @NonNull final AddressBookHelper addressBookHelper, @NonNull final KVStateChangeListener kvStateChangeListener, @NonNull final BoundaryStateChangeListener boundaryStateChangeListener, - @NonNull final ScheduleService scheduleService) { + @NonNull final ScheduleService scheduleService, + @NonNull final HintsService hintsService, + @NonNull final HistoryService historyService) { this.networkInfo = requireNonNull(networkInfo); this.stakePeriodChanges = requireNonNull(stakePeriodChanges); this.dispatchProcessor = requireNonNull(dispatchProcessor); @@ -213,6 +226,8 @@ public HandleWorkflow( .getConfiguration() .getConfigData(BlockStreamConfig.class) .streamMode(); + this.hintsService = requireNonNull(hintsService); + this.historyService = requireNonNull(historyService); } /** @@ -857,6 +872,43 @@ private void processStakePeriodChanges(@NonNull final UserTxn userTxn, @NonNull } } + /** + * Reconciles the state of the TSS system with the active rosters in the given state at the current time. + * @param tssConfig the TSS configuration + * @param state the state to use when reconciling the TSS system state with the active rosters + * @param now the current consensus time + */ + private void reconcileTssState( + @NonNull final TssConfig tssConfig, @NonNull final State state, @NonNull final Instant now) { + if (tssConfig.hintsEnabled() || tssConfig.historyEnabled()) { + final var rosterStore = new ReadableRosterStoreImpl(state.getReadableStates(RosterService.NAME)); + final var activeRosters = ActiveRosters.from(rosterStore); + if (tssConfig.hintsEnabled()) { + final var hintsWritableStates = state.getWritableStates(HintsService.NAME); + final var hintsStore = new WritableHintsStoreImpl(hintsWritableStates); + doStreamingKVChanges( + hintsWritableStates, + now, + () -> hintsService.reconcile(activeRosters, hintsStore, now, tssConfig)); + } + if (tssConfig.historyEnabled()) { + final Bytes currentMetadata; + if (tssConfig.hintsEnabled()) { + final var hintsStore = new ReadableHintsStoreImpl(state.getReadableStates(HintsService.NAME)); + currentMetadata = hintsStore.getVerificationKeyFor(activeRosters.currentRosterHash()); + } else { + currentMetadata = null; + } + final var historyWritableStates = state.getWritableStates(HistoryService.NAME); + final var historyStore = new WritableHistoryStoreImpl(historyWritableStates); + doStreamingKVChanges( + historyWritableStates, + now, + () -> historyService.reconcile(activeRosters, currentMetadata, historyStore, now)); + } + } + } + private static void logPreDispatch(@NonNull final UserTxn userTxn) { if (logger.isDebugEnabled()) { logStartUserTransaction( 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 8b618d827ca5..6c6f411fdbf5 100644 --- a/hedera-node/hedera-app/src/main/java/module-info.java +++ b/hedera-node/hedera-app/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.config.ServicesConfigExtension; import com.swirlds.config.api.ConfigurationExtension; @@ -72,6 +88,10 @@ exports com.hedera.node.app.signature; exports com.hedera.node.app.info; exports com.hedera.node.app.grpc; + exports com.hedera.node.app.hints; + exports com.hedera.node.app.hints.impl; + exports com.hedera.node.app.history; + exports com.hedera.node.app.history.impl; exports com.hedera.node.app.metrics; exports com.hedera.node.app.authorization; exports com.hedera.node.app.platform; diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/components/IngestComponentTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/components/IngestComponentTest.java index 94f581cb92a8..3a7abdb5b5a2 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/components/IngestComponentTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/components/IngestComponentTest.java @@ -36,6 +36,8 @@ import com.hedera.node.app.config.BootstrapConfigProviderImpl; import com.hedera.node.app.config.ConfigProviderImpl; import com.hedera.node.app.fixtures.state.FakeState; +import com.hedera.node.app.hints.impl.HintsServiceImpl; +import com.hedera.node.app.history.impl.HistoryServiceImpl; import com.hedera.node.app.info.NodeInfoImpl; import com.hedera.node.app.service.contract.impl.ContractServiceImpl; import com.hedera.node.app.service.file.impl.FileServiceImpl; @@ -115,6 +117,8 @@ void setUp() { () -> DEFAULT_NODE_INFO, () -> NO_OP_METRICS, throttleFactory); + final var hintsService = new HintsServiceImpl(appContext); + final var historyService = new HistoryServiceImpl(appContext); app = DaggerHederaInjectionComponent.builder() .configProviderImpl(configProvider) .bootstrapConfigProviderImpl(new BootstrapConfigProviderImpl()) @@ -139,6 +143,8 @@ void setUp() { .startupNetworks(startupNetworks) .throttleFactory(throttleFactory) .blockHashSigner(blockHashSigner) + .hintsService(hintsService) + .historyService(historyService) .build(); final var state = new FakeState(); diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/hints/impl/HintsLibraryCodecTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/hints/impl/HintsLibraryCodecTest.java new file mode 100644 index 000000000000..1ab9c0ceace0 --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/hints/impl/HintsLibraryCodecTest.java @@ -0,0 +1,36 @@ +/* + * 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.hints.impl; + +import static org.junit.jupiter.api.Assertions.*; + +import com.hedera.pbj.runtime.io.buffer.Bytes; +import org.junit.jupiter.api.Test; + +class HintsLibraryCodecTest { + private final HintsLibraryCodec subject = new HintsLibraryCodec(); + + @Test + void nothingSupportedYet() { + assertThrows(UnsupportedOperationException.class, () -> subject.encodeHintsKey(Bytes.EMPTY, Bytes.EMPTY)); + assertThrows(UnsupportedOperationException.class, () -> subject.extractAggregationKey(Bytes.EMPTY)); + assertThrows(UnsupportedOperationException.class, () -> subject.extractVerificationKey(Bytes.EMPTY)); + assertThrows(UnsupportedOperationException.class, () -> subject.extractPublicKey(Bytes.EMPTY, 0)); + assertThrows(UnsupportedOperationException.class, () -> subject.extractWeight(Bytes.EMPTY, 0)); + assertThrows(UnsupportedOperationException.class, () -> subject.extractTotalWeight(Bytes.EMPTY)); + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/hints/impl/HintsServiceImplTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/hints/impl/HintsServiceImplTest.java new file mode 100644 index 000000000000..ac9386dffbbd --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/hints/impl/HintsServiceImplTest.java @@ -0,0 +1,79 @@ +/* + * 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.hints.impl; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.hedera.node.app.hints.HintsService; +import com.hedera.node.app.hints.WritableHintsStore; +import com.hedera.node.app.roster.ActiveRosters; +import com.hedera.node.app.spi.AppContext; +import com.hedera.node.config.data.TssConfig; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.state.lifecycle.SchemaRegistry; +import java.time.Instant; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class HintsServiceImplTest { + private static final Instant CONSENSUS_NOW = Instant.ofEpochSecond(1_234_567L, 890); + + @Mock + private AppContext appContext; + + @Mock + private TssConfig tssConfig; + + @Mock + private ActiveRosters activeRosters; + + @Mock + private WritableHintsStore hintsStore; + + @Mock + private SchemaRegistry schemaRegistry; + + private HintsServiceImpl subject; + + @BeforeEach + void setUp() { + subject = new HintsServiceImpl(appContext); + } + + @Test + void metadataAsExpected() { + assertEquals(HintsService.NAME, subject.getServiceName()); + assertEquals(HintsService.MIGRATION_ORDER, subject.migrationOrder()); + } + + @Test + void nothingSupportedExceptRegisteringSchemas() { + assertThrows( + UnsupportedOperationException.class, + () -> subject.reconcile(activeRosters, hintsStore, CONSENSUS_NOW, tssConfig)); + assertThrows(UnsupportedOperationException.class, subject::isReady); + assertThrows(UnsupportedOperationException.class, subject::currentVerificationKeyOrThrow); + assertThrows(UnsupportedOperationException.class, () -> subject.signFuture(Bytes.EMPTY)); + assertDoesNotThrow(() -> subject.registerSchemas(schemaRegistry)); + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/hints/impl/ReadableHintsStoreImplTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/hints/impl/ReadableHintsStoreImplTest.java new file mode 100644 index 000000000000..d8ab03498581 --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/hints/impl/ReadableHintsStoreImplTest.java @@ -0,0 +1,45 @@ +/* + * 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.hints.impl; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.state.spi.ReadableStates; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ReadableHintsStoreImplTest { + @Mock + private ReadableStates states; + + private ReadableHintsStoreImpl subject; + + @BeforeEach + void setUp() { + subject = new ReadableHintsStoreImpl(states); + } + + @Test + void everythingUnsupportedForNow() { + assertThrows(UnsupportedOperationException.class, () -> subject.getVerificationKeyFor(Bytes.EMPTY)); + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/hints/impl/WritableHintsStoreImplTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/hints/impl/WritableHintsStoreImplTest.java new file mode 100644 index 000000000000..0ad0a2a73933 --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/hints/impl/WritableHintsStoreImplTest.java @@ -0,0 +1,44 @@ +/* + * 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.hints.impl; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.swirlds.state.spi.WritableStates; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class WritableHintsStoreImplTest { + @Mock + private WritableStates states; + + private WritableHintsStoreImpl subject; + + @BeforeEach + void setUp() { + subject = new WritableHintsStoreImpl(states); + } + + @Test + void constructorWorks() { + assertNotNull(subject); + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/history/impl/HistoryLibraryCodecTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/history/impl/HistoryLibraryCodecTest.java new file mode 100644 index 000000000000..62ee50f9bbe2 --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/history/impl/HistoryLibraryCodecTest.java @@ -0,0 +1,34 @@ +/* + * 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.history.impl; + +import static org.junit.jupiter.api.Assertions.*; + +import com.hedera.hapi.node.state.roster.Roster; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class HistoryLibraryCodecTest { + private final HistoryLibraryCodec subject = new HistoryLibraryCodec(); + + @Test + void nothingSupportedYet() { + assertThrows(UnsupportedOperationException.class, () -> subject.encodeHistory(Bytes.EMPTY, Bytes.EMPTY)); + assertThrows(UnsupportedOperationException.class, () -> subject.encodeAddressBook(Roster.DEFAULT, Map.of())); + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/history/impl/HistoryServiceImplTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/history/impl/HistoryServiceImplTest.java new file mode 100644 index 000000000000..4cfe81d8e4b5 --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/history/impl/HistoryServiceImplTest.java @@ -0,0 +1,74 @@ +/* + * 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.history.impl; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.hedera.node.app.history.HistoryService; +import com.hedera.node.app.history.WritableHistoryStore; +import com.hedera.node.app.roster.ActiveRosters; +import com.hedera.node.app.spi.AppContext; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.state.lifecycle.SchemaRegistry; +import java.time.Instant; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class HistoryServiceImplTest { + private static final Instant CONSENSUS_NOW = Instant.ofEpochSecond(1_234_567L, 890); + + @Mock + private AppContext appContext; + + @Mock + private ActiveRosters activeRosters; + + @Mock + private WritableHistoryStore historyStore; + + @Mock + private SchemaRegistry schemaRegistry; + + private HistoryServiceImpl subject; + + @BeforeEach + void setUp() { + subject = new HistoryServiceImpl(appContext); + } + + @Test + void metadataAsExpected() { + assertEquals(HistoryService.NAME, subject.getServiceName()); + assertEquals(HistoryService.MIGRATION_ORDER, subject.migrationOrder()); + } + + @Test + void nothingSupportedExceptRegisteringSchemas() { + assertThrows( + UnsupportedOperationException.class, + () -> subject.reconcile(activeRosters, null, historyStore, CONSENSUS_NOW)); + assertThrows(UnsupportedOperationException.class, subject::isReady); + assertThrows(UnsupportedOperationException.class, () -> subject.getCurrentProof(Bytes.EMPTY)); + assertDoesNotThrow(() -> subject.registerSchemas(schemaRegistry)); + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/history/impl/ReadableHistoryStoreImplTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/history/impl/ReadableHistoryStoreImplTest.java new file mode 100644 index 000000000000..89f444b8f599 --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/history/impl/ReadableHistoryStoreImplTest.java @@ -0,0 +1,44 @@ +/* + * 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.history.impl; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.swirlds.state.spi.ReadableStates; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ReadableHistoryStoreImplTest { + @Mock + private ReadableStates states; + + private ReadableHistoryStoreImpl subject; + + @BeforeEach + void setUp() { + subject = new ReadableHistoryStoreImpl(states); + } + + @Test + void constructorWorks() { + assertNotNull(subject); + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/history/impl/WritableHistoryStoreImplTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/history/impl/WritableHistoryStoreImplTest.java new file mode 100644 index 000000000000..0d58270325bd --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/history/impl/WritableHistoryStoreImplTest.java @@ -0,0 +1,45 @@ +/* + * 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.history.impl; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.hedera.node.app.hints.impl.WritableHintsStoreImpl; +import com.swirlds.state.spi.WritableStates; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class WritableHistoryStoreImplTest { + @Mock + private WritableStates states; + + private WritableHintsStoreImpl subject; + + @BeforeEach + void setUp() { + subject = new WritableHintsStoreImpl(states); + } + + @Test + void constructorWorks() { + assertNotNull(subject); + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/roster/ActiveRostersTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/roster/ActiveRostersTest.java new file mode 100644 index 000000000000..1b55e2d0c241 --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/roster/ActiveRostersTest.java @@ -0,0 +1,150 @@ +/* + * 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.roster; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.given; + +import com.hedera.hapi.node.state.roster.Roster; +import com.hedera.hapi.node.state.roster.RosterEntry; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.platform.state.service.ReadableRosterStore; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ActiveRostersTest { + private static final long MISSING_NODE_ID = 666L; + private static final List A_ROSTER_NODE_WEIGHTS = List.of( + new RosterTransitionWeights.NodeWeight(1L, 1L), + new RosterTransitionWeights.NodeWeight(2L, 2L), + new RosterTransitionWeights.NodeWeight(3L, 3L)); + private static final List B_ROSTER_NODE_WEIGHTS = List.of( + new RosterTransitionWeights.NodeWeight(1L, 2L), + new RosterTransitionWeights.NodeWeight(2L, 4L), + new RosterTransitionWeights.NodeWeight(3L, 6L)); + private static final Map A_ROSTER_WEIGHTS = A_ROSTER_NODE_WEIGHTS.stream() + .collect(Collectors.toMap( + RosterTransitionWeights.NodeWeight::nodeId, RosterTransitionWeights.NodeWeight::weight)); + private static final Map B_ROSTER_WEIGHTS = B_ROSTER_NODE_WEIGHTS.stream() + .collect(Collectors.toMap( + RosterTransitionWeights.NodeWeight::nodeId, RosterTransitionWeights.NodeWeight::weight)); + private static final Bytes A_ROSTER_HASH = Bytes.wrap("A_ROSTER_HASH"); + private static final Bytes B_ROSTER_HASH = Bytes.wrap("B_ROSTER_HASH"); + private static final Roster A_ROSTER = new Roster(A_ROSTER_WEIGHTS.entrySet().stream() + .map(entry -> new RosterEntry(entry.getKey(), entry.getValue(), Bytes.EMPTY, List.of())) + .toList()); + private static final Roster B_ROSTER = new Roster(B_ROSTER_WEIGHTS.entrySet().stream() + .map(entry -> new RosterEntry(entry.getKey(), entry.getValue(), Bytes.EMPTY, List.of())) + .toList()); + private static final long A_ROSTER_STRONG_MINORITY_WEIGHT = RosterTransitionWeights.atLeastOneThirdOfTotal( + A_ROSTER_WEIGHTS.values().stream().mapToLong(Long::longValue).sum()); + private static final long A_ROSTER_STRICT_MAJORITY_WEIGHT = RosterTransitionWeights.moreThanTwoThirdsOfTotal( + A_ROSTER_WEIGHTS.values().stream().mapToLong(Long::longValue).sum()); + private static final long B_ROSTER_STRICT_MAJORITY_WEIGHT = RosterTransitionWeights.moreThanTwoThirdsOfTotal( + B_ROSTER_WEIGHTS.values().stream().mapToLong(Long::longValue).sum()); + + @Mock + private ReadableRosterStore rosterStore; + + @Test + void detectsBootstrap() { + given(rosterStore.getCurrentRosterHash()).willReturn(A_ROSTER_HASH); + given(rosterStore.get(A_ROSTER_HASH)).willReturn(A_ROSTER); + + final var activeRosters = ActiveRosters.from(rosterStore); + + assertEquals(ActiveRosters.Phase.BOOTSTRAP, activeRosters.phase()); + assertEquals(A_ROSTER_HASH, activeRosters.currentRosterHash()); + assertEquals(A_ROSTER_HASH, activeRosters.sourceRosterHash()); + assertEquals(A_ROSTER_HASH, activeRosters.targetRosterHash()); + assertSame(A_ROSTER, activeRosters.findRelatedRoster(A_ROSTER_HASH)); + assertSame(A_ROSTER, activeRosters.targetRoster()); + final var weights = activeRosters.transitionWeights(); + assertEquals(A_ROSTER_WEIGHTS, weights.sourceNodeWeights()); + assertEquals(A_ROSTER_WEIGHTS, weights.targetNodeWeights()); + assertEquals(A_ROSTER_STRONG_MINORITY_WEIGHT, weights.sourceWeightThreshold()); + assertEquals(A_ROSTER_STRICT_MAJORITY_WEIGHT, weights.targetWeightThreshold()); + assertEquals(A_ROSTER_NODE_WEIGHTS, weights.orderedSourceWeights().toList()); + assertEquals(A_ROSTER_NODE_WEIGHTS, weights.orderedTargetWeights().toList()); + assertTrue(A_ROSTER_NODE_WEIGHTS.stream().allMatch(nw -> weights.sourceWeightOf(nw.nodeId()) == nw.weight())); + assertTrue(A_ROSTER_NODE_WEIGHTS.stream().allMatch(nw -> weights.targetWeightOf(nw.nodeId()) == nw.weight())); + assertTrue(A_ROSTER_NODE_WEIGHTS.stream().allMatch(nw -> weights.targetIncludes(nw.nodeId()))); + assertEquals(A_ROSTER.rosterEntries().size(), weights.numTargetNodesIn(A_ROSTER_WEIGHTS.keySet())); + assertEquals( + 1, + weights.numTargetNodesIn( + Set.of(A_ROSTER_WEIGHTS.keySet().iterator().next()))); + assertEquals(0L, weights.sourceWeightOf(MISSING_NODE_ID)); + assertEquals(0L, weights.targetWeightOf(MISSING_NODE_ID)); + } + + @Test + void detectsHandoff() { + given(rosterStore.getCurrentRosterHash()).willReturn(A_ROSTER_HASH); + given(rosterStore.getPreviousRosterHash()).willReturn(B_ROSTER_HASH); + given(rosterStore.get(A_ROSTER_HASH)).willReturn(A_ROSTER); + + final var activeRosters = ActiveRosters.from(rosterStore); + + assertEquals(ActiveRosters.Phase.HANDOFF, activeRosters.phase()); + assertEquals(A_ROSTER_HASH, activeRosters.currentRosterHash()); + assertThrows(IllegalStateException.class, activeRosters::targetRoster); + assertThrows(IllegalStateException.class, activeRosters::sourceRosterHash); + assertThrows(IllegalStateException.class, activeRosters::targetRosterHash); + assertSame(A_ROSTER, activeRosters.findRelatedRoster(A_ROSTER_HASH)); + assertThrows(IllegalStateException.class, activeRosters::transitionWeights); + } + + @Test + void detectsTransition() { + given(rosterStore.getCurrentRosterHash()).willReturn(A_ROSTER_HASH); + given(rosterStore.getCandidateRosterHash()).willReturn(B_ROSTER_HASH); + given(rosterStore.get(A_ROSTER_HASH)).willReturn(A_ROSTER); + given(rosterStore.get(B_ROSTER_HASH)).willReturn(B_ROSTER); + + final var activeRosters = ActiveRosters.from(rosterStore); + + assertEquals(ActiveRosters.Phase.TRANSITION, activeRosters.phase()); + assertEquals(A_ROSTER_HASH, activeRosters.currentRosterHash()); + assertEquals(A_ROSTER_HASH, activeRosters.sourceRosterHash()); + assertEquals(B_ROSTER_HASH, activeRosters.targetRosterHash()); + assertSame(A_ROSTER, activeRosters.findRelatedRoster(A_ROSTER_HASH)); + assertSame(B_ROSTER, activeRosters.targetRoster()); + final var weights = activeRosters.transitionWeights(); + assertEquals(A_ROSTER_WEIGHTS, weights.sourceNodeWeights()); + assertEquals(B_ROSTER_WEIGHTS, weights.targetNodeWeights()); + assertEquals(A_ROSTER_STRONG_MINORITY_WEIGHT, weights.sourceWeightThreshold()); + assertEquals(B_ROSTER_STRICT_MAJORITY_WEIGHT, weights.targetWeightThreshold()); + assertEquals(A_ROSTER_NODE_WEIGHTS, weights.orderedSourceWeights().toList()); + assertEquals(B_ROSTER_NODE_WEIGHTS, weights.orderedTargetWeights().toList()); + assertTrue(A_ROSTER_NODE_WEIGHTS.stream().allMatch(nw -> weights.sourceWeightOf(nw.nodeId()) == nw.weight())); + assertTrue(B_ROSTER_NODE_WEIGHTS.stream().allMatch(nw -> weights.targetWeightOf(nw.nodeId()) == nw.weight())); + assertTrue(B_ROSTER_NODE_WEIGHTS.stream().allMatch(nw -> weights.targetIncludes(nw.nodeId()))); + final var allNodeIds = B_ROSTER_WEIGHTS.keySet().stream().toList(); + final var someNodeIds = Set.copyOf(allNodeIds.subList(1, allNodeIds.size())); + assertEquals(B_ROSTER.rosterEntries().size() - 1, weights.numTargetNodesIn(someNodeIds)); + assertEquals(0L, weights.sourceWeightOf(MISSING_NODE_ID)); + assertEquals(0L, weights.targetWeightOf(MISSING_NODE_ID)); + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/TssBlockHashSignerTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/TssBlockHashSignerTest.java new file mode 100644 index 000000000000..8e2a7811a03f --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/TssBlockHashSignerTest.java @@ -0,0 +1,146 @@ +/* + * 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.tss; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.BDDMockito.given; + +import com.hedera.node.app.hapi.utils.CommonUtils; +import com.hedera.node.app.hints.HintsService; +import com.hedera.node.app.history.HistoryService; +import com.hedera.node.config.ConfigProvider; +import com.hedera.node.config.VersionedConfigImpl; +import com.hedera.node.config.testfixtures.HederaTestConfigBuilder; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.config.api.Configuration; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.concurrent.CompletableFuture; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class TssBlockHashSignerTest { + private static final Bytes FAKE_BLOCK_HASH = Bytes.wrap("FAKE_BLOCK_HASH"); + private static final Bytes FAKE_HINTS_SIGNATURE = CommonUtils.noThrowSha384HashOf(FAKE_BLOCK_HASH); + private static final Bytes FAKE_PROOF = Bytes.wrap("FAKE_PROOF"); + private static final Bytes FAKE_VK = Bytes.wrap("FAKE_VK"); + + @Mock + private HintsService hintsService; + + @Mock + private HistoryService historyService; + + @Mock + private ConfigProvider configProvider; + + private TssBlockHashSigner subject; + + enum HintsEnabled { + YES, + NO + } + + enum HistoryEnabled { + YES, + NO + } + + @Test + void asExpectedWithNothingEnabled() { + givenSubjectWith(HintsEnabled.NO, HistoryEnabled.NO); + + assertTrue(subject.isReady()); + + final var signature = subject.signFuture(FAKE_BLOCK_HASH).join(); + + assertEquals(FAKE_HINTS_SIGNATURE, signature); + } + + @Test + void asExpectedWithJustHintsEnabled() { + givenSubjectWith(HintsEnabled.YES, HistoryEnabled.NO); + + assertFalse(subject.isReady()); + assertThrows(IllegalStateException.class, () -> subject.signFuture(FAKE_BLOCK_HASH)); + given(hintsService.isReady()).willReturn(true); + assertTrue(subject.isReady()); + given(hintsService.signFuture(FAKE_BLOCK_HASH)) + .willReturn(CompletableFuture.completedFuture(FAKE_HINTS_SIGNATURE)); + + final var signature = subject.signFuture(FAKE_BLOCK_HASH).join(); + + assertSame(FAKE_HINTS_SIGNATURE, signature); + } + + @Test + void asExpectedWithJustHistoryEnabled() { + givenSubjectWith(HintsEnabled.NO, HistoryEnabled.YES); + + assertFalse(subject.isReady()); + assertThrows(IllegalStateException.class, () -> subject.signFuture(FAKE_BLOCK_HASH)); + given(historyService.isReady()).willReturn(true); + assertTrue(subject.isReady()); + given(historyService.getCurrentProof(Bytes.EMPTY)).willReturn(FAKE_PROOF); + + final var signature = subject.signFuture(FAKE_BLOCK_HASH).join(); + + assertEquals(TssBlockHashSigner.assemble(FAKE_HINTS_SIGNATURE, Bytes.EMPTY, FAKE_PROOF), signature); + } + + @Test + void asExpectedWithBothEnabled() { + givenSubjectWith(HintsEnabled.YES, HistoryEnabled.YES); + + assertFalse(subject.isReady()); + assertThrows(IllegalStateException.class, () -> subject.signFuture(FAKE_BLOCK_HASH)); + given(historyService.isReady()).willReturn(true); + assertFalse(subject.isReady()); + assertThrows(IllegalStateException.class, () -> subject.signFuture(FAKE_BLOCK_HASH)); + given(hintsService.isReady()).willReturn(true); + assertTrue(subject.isReady()); + given(hintsService.currentVerificationKeyOrThrow()).willReturn(FAKE_VK); + given(hintsService.signFuture(FAKE_BLOCK_HASH)) + .willReturn(CompletableFuture.completedFuture(FAKE_HINTS_SIGNATURE)); + given(historyService.getCurrentProof(FAKE_VK)).willReturn(FAKE_PROOF); + + final var signature = subject.signFuture(FAKE_BLOCK_HASH).join(); + + assertEquals(TssBlockHashSigner.assemble(FAKE_HINTS_SIGNATURE, FAKE_VK, FAKE_PROOF), signature); + } + + private void givenSubjectWith( + @NonNull final HintsEnabled hintsEnabled, @NonNull final HistoryEnabled historyEnabled) { + given(configProvider.getConfiguration()) + .willReturn(new VersionedConfigImpl(configWith(hintsEnabled, historyEnabled), 123)); + subject = new TssBlockHashSigner(hintsService, historyService, configProvider); + } + + private Configuration configWith( + @NonNull final HintsEnabled hintsEnabled, @NonNull final HistoryEnabled historyEnabled) { + return HederaTestConfigBuilder.create() + .withValue("tss.hintsEnabled", "" + (hintsEnabled == HintsEnabled.YES)) + .withValue("tss.historyEnabled", "" + (historyEnabled == HistoryEnabled.YES)) + .getOrCreateConfig(); + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/HandleWorkflowTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/HandleWorkflowTest.java index e003b8c686f4..445c9bd02e07 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/HandleWorkflowTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/HandleWorkflowTest.java @@ -34,6 +34,8 @@ import com.hedera.node.app.blocks.impl.BoundaryStateChangeListener; import com.hedera.node.app.blocks.impl.KVStateChangeListener; import com.hedera.node.app.fees.ExchangeRateManager; +import com.hedera.node.app.hints.HintsService; +import com.hedera.node.app.history.HistoryService; import com.hedera.node.app.records.BlockRecordManager; import com.hedera.node.app.service.addressbook.impl.helpers.AddressBookHelper; import com.hedera.node.app.service.schedule.ScheduleService; @@ -144,6 +146,12 @@ class HandleWorkflowTest { @Mock private UserTxnFactory userTxnFactory; + @Mock + private HintsService hintsService; + + @Mock + private HistoryService historyService; + private HandleWorkflow subject; @BeforeEach @@ -222,6 +230,8 @@ private void givenSubjectWith( new AddressBookHelper(), kvStateChangeListener, boundaryStateChangeListener, - scheduleService); + scheduleService, + hintsService, + historyService); } } diff --git a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/TssConfig.java b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/TssConfig.java index 7510132f445b..b92543522b75 100644 --- a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/TssConfig.java +++ b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/TssConfig.java @@ -16,10 +16,14 @@ package com.hedera.node.config.data; +import com.hedera.node.config.NetworkProperty; import com.swirlds.config.api.ConfigData; +import com.swirlds.config.api.ConfigProperty; /** * Configuration for the TSS subsystem. */ @ConfigData("tss") -public record TssConfig() {} +public record TssConfig( + @ConfigProperty(defaultValue = "false") @NetworkProperty boolean hintsEnabled, + @ConfigProperty(defaultValue = "false") @NetworkProperty boolean historyEnabled) {} diff --git a/hedera-node/hedera-file-service-impl/src/main/java/module-info.java b/hedera-node/hedera-file-service-impl/src/main/java/module-info.java index 31058e857c4f..1d5446027af9 100644 --- a/hedera-node/hedera-file-service-impl/src/main/java/module-info.java +++ b/hedera-node/hedera-file-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. + */ + module com.hedera.node.app.service.file.impl { requires transitive com.hedera.node.app.hapi.fees; requires transitive com.hedera.node.app.hapi.utils; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/AbstractEmbeddedHedera.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/AbstractEmbeddedHedera.java index ae2130851850..9218a55b6803 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/AbstractEmbeddedHedera.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/AbstractEmbeddedHedera.java @@ -40,6 +40,8 @@ import com.hedera.node.app.fixtures.state.FakeServiceMigrator; import com.hedera.node.app.fixtures.state.FakeServicesRegistry; import com.hedera.node.app.fixtures.state.FakeState; +import com.hedera.node.app.hints.impl.HintsServiceImpl; +import com.hedera.node.app.history.impl.HistoryServiceImpl; import com.hedera.node.app.info.DiskStartupNetworks; import com.hedera.node.app.version.ServicesSoftwareVersion; import com.hedera.node.internal.network.Network; @@ -163,7 +165,10 @@ protected AbstractEmbeddedHedera(@NonNull final EmbeddedNode node) { new FakeServiceMigrator(), this::now, DiskStartupNetworks::new, - () -> this.blockHashSigner = new LapsingBlockHashSigner(), + (hintsService, historyService, configProvider) -> + this.blockHashSigner = new LapsingBlockHashSigner(hintsService, historyService, configProvider), + HintsServiceImpl::new, + HistoryServiceImpl::new, metrics); version = (ServicesSoftwareVersion) hedera.getSoftwareVersion(); blockStreamEnabled = hedera.isBlockStreamEnabled(); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/LapsingBlockHashSigner.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/LapsingBlockHashSigner.java index 1c0cff2c2dda..fa961de7876d 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/LapsingBlockHashSigner.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/LapsingBlockHashSigner.java @@ -17,7 +17,10 @@ package com.hedera.services.bdd.junit.hedera.embedded.fakes; import com.hedera.node.app.blocks.BlockHashSigner; +import com.hedera.node.app.hints.HintsService; +import com.hedera.node.app.history.HistoryService; import com.hedera.node.app.tss.TssBlockHashSigner; +import com.hedera.node.config.ConfigProvider; import com.hedera.pbj.runtime.io.buffer.Bytes; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.concurrent.CompletableFuture; @@ -30,8 +33,11 @@ public class LapsingBlockHashSigner implements BlockHashSigner { private boolean ignoreRequests = false; - public LapsingBlockHashSigner() { - this.delegate = new TssBlockHashSigner(); + public LapsingBlockHashSigner( + @NonNull final HintsService hintsService, + @NonNull final HistoryService historyService, + @NonNull final ConfigProvider configProvider) { + this.delegate = new TssBlockHashSigner(hintsService, historyService, configProvider); } /** diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/records/RecordCreationSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/records/RecordCreationSuite.java index d3b88bfb58b9..31ed87489eda 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/records/RecordCreationSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/records/RecordCreationSuite.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2024 Hedera Hashgraph, LLC + * Copyright (C) 2020-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,6 +17,7 @@ package com.hedera.services.bdd.suites.records; import static com.hedera.services.bdd.junit.ContextRequirement.SYSTEM_ACCOUNT_BALANCES; +import static com.hedera.services.bdd.junit.RepeatableReason.NEEDS_SYNCHRONOUS_HANDLE_WORKFLOW; import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; import static com.hedera.services.bdd.spec.assertions.AccountInfoAsserts.changeFromSnapshot; import static com.hedera.services.bdd.spec.assertions.AssertUtils.inOrder; @@ -42,6 +43,7 @@ import com.hedera.node.app.hapi.utils.fee.FeeObject; import com.hedera.services.bdd.junit.HapiTest; import com.hedera.services.bdd.junit.LeakyHapiTest; +import com.hedera.services.bdd.junit.RepeatableHapiTest; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import org.junit.jupiter.api.DynamicTest; @@ -108,7 +110,7 @@ final Stream submittingNodeStillPaidIfServiceFeesOmitted() { .logged())); } - @LeakyHapiTest(requirement = SYSTEM_ACCOUNT_BALANCES) + @RepeatableHapiTest(NEEDS_SYNCHRONOUS_HANDLE_WORKFLOW) final Stream submittingNodeChargedNetworkFeeForLackOfDueDiligence() { final String comfortingMemo = THIS_IS_OK_IT_S_FINE_IT_S_WHATEVER; final String disquietingMemo = "\u0000his is ok, it's fine, it's whatever."; @@ -131,7 +133,6 @@ final Stream submittingNodeChargedNetworkFeeForLackOfDueDiligence() .payingWith(PAYER) .txnId(TXN_ID)) .payingWith(GENESIS), - sleepFor(SLEEP_MS), sourcing(() -> getAccountBalance(TO_ACCOUNT) .hasTinyBars(changeFromSnapshot(BEFORE, -feeObs.get().networkFee()))), sourcing(() -> getAccountBalance(FOR_ACCOUNT_FUNDING) diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/ReadableRosterStore.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/ReadableRosterStore.java index aa04b4119bce..cd2e4409b694 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/ReadableRosterStore.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/ReadableRosterStore.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. @@ -85,4 +85,10 @@ public interface ReadableRosterStore { */ @Nullable Bytes getPreviousRosterHash(); + + /** + * Gets the candidate roster hash, if present. If none is set, returns null; + */ + @Nullable + Bytes getCandidateRosterHash(); } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/ReadableRosterStoreImpl.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/ReadableRosterStoreImpl.java index 6003100ee9b2..b78aa1edd678 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/ReadableRosterStoreImpl.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/ReadableRosterStoreImpl.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. @@ -29,6 +29,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.util.List; +import java.util.Optional; /** * Provides read-only methods for interacting with the underlying data storage mechanisms for @@ -128,4 +129,12 @@ public Bytes getPreviousRosterHash() { .filter(pair -> rosterMap.contains(new ProtoBytes(pair.activeRosterHash()))) .toList(); } + + @Override + public @Nullable Bytes getCandidateRosterHash() { + return Optional.ofNullable(rosterState.get()) + .map(RosterState::candidateRosterHash) + .filter(bytes -> bytes.length() > 0) + .orElse(null); + } } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/module-info.java b/platform-sdk/swirlds-platform-core/src/main/java/module-info.java index b1e6a8cb2b05..4ecc278eedb0 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/module-info.java +++ b/platform-sdk/swirlds-platform-core/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. + */ + /** * The Swirlds public API module used by platform applications. */ diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/service/ReadableRosterStoreImplTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/service/ReadableRosterStoreImplTest.java new file mode 100644 index 000000000000..cc351da4af2e --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/service/ReadableRosterStoreImplTest.java @@ -0,0 +1,65 @@ +/* + * 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.swirlds.platform.state.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.BDDMockito.given; + +import com.hedera.hapi.node.state.roster.RosterState; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.state.spi.ReadableSingletonState; +import com.swirlds.state.spi.ReadableStates; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ReadableRosterStoreImplTest { + @Mock + private ReadableStates readableStates; + + @Mock + private ReadableSingletonState rosterState; + + private ReadableRosterStoreImpl subject; + + @BeforeEach + void setUp() { + given(readableStates.getSingleton(WritableRosterStore.ROSTER_STATES_KEY)) + .willReturn(rosterState); + subject = new ReadableRosterStoreImpl(readableStates); + } + + @Test + void nullCandidateRosterCasesPass() { + assertNull(subject.getCandidateRosterHash()); + given(rosterState.get()).willReturn(RosterState.DEFAULT); + assertNull(subject.getCandidateRosterHash()); + } + + @Test + void nonNullCandidateRosterIsReturned() { + final var fakeHash = Bytes.wrap("PRETEND"); + given(rosterState.get()) + .willReturn( + RosterState.newBuilder().candidateRosterHash(fakeHash).build()); + assertEquals(fakeHash, subject.getCandidateRosterHash()); + } +} From 30ae2c8cd263229d55178a279f946c2f137710a7 Mon Sep 17 00:00:00 2001 From: Mihail Mihov Date: Wed, 15 Jan 2025 09:04:50 +0200 Subject: [PATCH 17/19] ci: Clean up Node: Build Application workflow calls (#17310) Signed-off-by: Mihail Mihov --- .../node-flow-build-application.yaml | 29 +----------- .../node-flow-deploy-release-artifact.yaml | 25 ++--------- .../node-zxf-deploy-integration.yaml | 45 +++++++++++++------ 3 files changed, 35 insertions(+), 64 deletions(-) diff --git a/.github/workflows/node-flow-build-application.yaml b/.github/workflows/node-flow-build-application.yaml index cd5ffd66a46b..1120f19e1084 100644 --- a/.github/workflows/node-flow-build-application.yaml +++ b/.github/workflows/node-flow-build-application.yaml @@ -94,30 +94,6 @@ jobs: with: egress-policy: audit - - name: Configure Workflow Inputs - id: workflow-inputs - env: - REFERENCE: ${{ github.ref }} - AUTHOR: ${{ github.event.head_commit.author.name }} - MESSAGE: ${{ github.event.head_commit.message }} - COMMIT_SHA: ${{ github.sha }} - run: | - # massage the message to remove invalid control characters - MASSAGED_MESSAGE="${MESSAGE//[$'\t\r\n']/' '}" - - # Assign github step outputs - echo "input-ref=${REFERENCE}" >> $GITHUB_OUTPUT - echo "input-author=${AUTHOR}" >> $GITHUB_OUTPUT - echo "input-msg=${MASSAGED_MESSAGE}" >> $GITHUB_OUTPUT - echo "input-sha=${COMMIT_SHA}" >> $GITHUB_OUTPUT - - # Preview the input values - echo "### Workflow Dispatch Inputs" >> $GITHUB_STEP_SUMMARY - echo "input-ref=${REFERENCE}" >> $GITHUB_STEP_SUMMARY - echo "input-author=${AUTHOR}" >> $GITHUB_STEP_SUMMARY - echo "input-msg=${MASSAGED_MESSAGE}" >> $GITHUB_STEP_SUMMARY - echo "input-sha=${COMMIT_SHA}" >> $GITHUB_STEP_SUMMARY - - name: Trigger ZXF Deploy Production Release uses: step-security/workflow-dispatch@4d1049025980f72b1327cbfdeecb07fe7a20f577 # v1.2.4 with: @@ -126,8 +102,5 @@ jobs: ref: main # ensure we are always using the workflow definition from the main branch token: ${{ secrets.GH_ACCESS_TOKEN }} inputs: '{ - "ref": "${{ steps.workflow-inputs.outputs.input-ref }}", - "author": "${{ steps.workflow-inputs.outputs.input-author }}", - "msg": "${{ steps.workflow-inputs.outputs.input-msg }}", - "sha": "${{ steps.workflow-inputs.outputs.input-sha }}" + "ref": "${{ github.ref }}" }' diff --git a/.github/workflows/node-flow-deploy-release-artifact.yaml b/.github/workflows/node-flow-deploy-release-artifact.yaml index 3ec560f1710d..ad3089601588 100644 --- a/.github/workflows/node-flow-deploy-release-artifact.yaml +++ b/.github/workflows/node-flow-deploy-release-artifact.yaml @@ -24,18 +24,6 @@ on: ref: required: true description: "The github branch or tag that triggered the workflow" - author: - required: false - description: "The author of the commit" - default: "" - msg: - required: false - description: "The message on the head commit" - default: "" - sha: - required: false - description: "The commit ID of the commit that triggered the workflow" - default: "" defaults: run: @@ -181,9 +169,7 @@ jobs: - name: Check Integration Job State id: check-integration-job - if: ${{ needs.release-branch.result == 'success' && - (inputs.author != '' && inputs.msg != '' && inputs.sha != '') && - !cancelled() }} + if: ${{ needs.release-branch.result == 'success' && !cancelled() }} env: GH_TOKEN: ${{ github.token }} run: | @@ -193,9 +179,7 @@ jobs: echo "enabled=${JOB_ENABLED}" >> $GITHUB_OUTPUT - name: Trigger ZXF Deploy Integration - if: ${{ needs.release-branch.result == 'success' && steps.check-integration-job.outputs.enabled == 'true' && - (inputs.author != '' && inputs.msg != '' && inputs.sha != '') && - !cancelled() }} + if: ${{ needs.release-branch.result == 'success' && steps.check-integration-job.outputs.enabled == 'true' && !cancelled() }} uses: step-security/workflow-dispatch@4d1049025980f72b1327cbfdeecb07fe7a20f577 # v1.2.4 with: workflow: .github/workflows/node-zxf-deploy-integration.yaml @@ -203,10 +187,7 @@ jobs: ref: main # ensure we are always using the workflow definition from the main branch token: ${{ secrets.GH_ACCESS_TOKEN }} inputs: '{ - "ref": "${{ inputs.ref }}", - "author": "${{ inputs.author }}", - "msg": "${{ inputs.msg }}", - "sha": "${{ inputs.sha }}" + "ref": "${{ inputs.ref }}" }' update-hedera-protobufs: diff --git a/.github/workflows/node-zxf-deploy-integration.yaml b/.github/workflows/node-zxf-deploy-integration.yaml index 3420ae57d9be..d1858dff06f8 100644 --- a/.github/workflows/node-zxf-deploy-integration.yaml +++ b/.github/workflows/node-zxf-deploy-integration.yaml @@ -21,18 +21,6 @@ on: ref: required: true description: "The github branch or tag that triggered the workflow" - author: - required: true - description: "The author of the commit" - default: "" - msg: - required: true - description: "The message on the head commit" - default: "" - sha: - required: true - description: "The commit ID of the commit that triggered the workflow" - default: "" permissions: contents: read @@ -48,12 +36,41 @@ jobs: with: egress-policy: audit + - name: Check out code at the specified ref + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ inputs.ref }} + + - name: Gather commit info + id: gather-commit-info + run: | + # Fetch info from the most recent commit for the given ref + COMMIT_AUTHOR="$(git log -1 --pretty=format:'%an')" + COMMIT_MSG="$(git log -1 --pretty=format:'%s')" + COMMIT_SHA="$(git rev-parse HEAD)" + + # Print them to the console + echo "Commit Author: $COMMIT_AUTHOR" + echo "Commit Message: $COMMIT_MSG" + echo "Commit SHA: $COMMIT_SHA" + + # Expose them as outputs for the next steps + echo "author=$COMMIT_AUTHOR" >> $GITHUB_OUTPUT + echo "msg=$COMMIT_MSG" >> $GITHUB_OUTPUT + echo "sha=$COMMIT_SHA" >> $GITHUB_OUTPUT + - name: Notify Jenkins of Release (Integration) id: jenkins-integration uses: fjogeleit/http-request-action@23ad54bcd1178fcff6a0d17538fa09de3a7f0a4d # v1.16.4 with: - url: ${{ secrets.RELEASE_JENKINS_INTEGRATION_URL }} - data: ${{ toJSON(inputs) }} + url: ${{ secrets.RELEASE_JENKINS_INTEGRATION_URL }} + data: | + '{ + "ref": "${{ inputs.ref }}", + "author": "${{ steps.gather-commit-info.outputs.author }}", + "msg": "${{ steps.gather-commit-info.outputs.msg }}", + "sha": "${{ steps.gather-commit-info.outputs.sha }}" + }' - name: Display Jenkins Payload env: From 307b4698f99e72221ec52555dafbd18768430180 Mon Sep 17 00:00:00 2001 From: Lazar Petrovic Date: Wed, 15 Jan 2025 10:31:40 +0100 Subject: [PATCH 18/19] chore: new wiring diagram with inline PCES (#17372) Signed-off-by: Lazar Petrovic --- platform-sdk/docs/core/wiring-diagram.svg | 2 +- ...erate-platform-diagram-with-inline-pces.sh | 65 ------------------- .../wiring/generate-platform-diagram.sh | 18 ++--- 3 files changed, 7 insertions(+), 78 deletions(-) delete mode 100755 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/generate-platform-diagram-with-inline-pces.sh diff --git a/platform-sdk/docs/core/wiring-diagram.svg b/platform-sdk/docs/core/wiring-diagram.svg index 098ffc251b92..7f3d5fc58aa4 100644 --- a/platform-sdk/docs/core/wiring-diagram.svg +++ b/platform-sdk/docs/core/wiring-diagram.svg @@ -1 +1 @@ -
      Transaction Handling
      State Verification
      State Signature Collection
      State File Management
      Preconsensus Event Stream
      PCES Replay
      Miscellaneous
      Event Intake
      Event Creation
      Consensus
      AppNotifier
      ❔💢💥🚦
      Branch Detection
      ❔🌀
      Consensus Engine
      ❔🚦
      🌀
      🚽
      EventCreationManager
      ❔❤️🌀🏥🚦
      TransactionPool
      ♻️❔🏥🖋️🚦
      ♻️
      ⚰️
      PostHashCollector
      Mystery Input
      ❤️
      🏥
      💨
      🚦
      PcesWriter
      ✅❔🌀📀🚽
      💾
      📀
      State Signature Collector
      ❔🔰
      💢
      ISS Detector
      💀
      💥
      🖋️
      Transaction Handler
      💨🔮
      gossip
      ❔🌀🏥📬🚦
      🍎
      📝
      📬
      🔮
      🔰
      consensus events
      rounds
      rounds
      event window
      flush request
      future hash
      self events
      get transactions
      GossipEvent
      GossipEvent
      unordered events
      health info
      check system health
      checkSignedStates
      evaluate status
      heartbeat
      PlatformStatusAction
      IssNotification
      non-deduplicated events
      mystery data
      checkForBranches
      GossipEvent
      unsequenced event
      GossipEvent
      GossipEvent
      events to gossip
      preconsensus signatures
      GossipEvent
      events to write
      durable event info
      non-validated events
      handleConsensusRound
      hash override
      self events
      stale events
      publishStaleEvent
      non-validated events
      hashed states
      handleStateAndRound
      consensus events
      hashed states
      signState
      complete state
      states
      complete state notification
      state written notification
      PlatformStatusAction
      minimum identifier to store
      submit transaction
      PlatformStatus
      setState
      unhashed state and round
      registerState
      submit transaction
      futures
      unhashed event
      unhashed event
      done streaming pces
      \ No newline at end of file +
      Transaction Handling
      State Verification
      State Signature Collection
      State File Management
      PCES Replay
      Miscellaneous
      Event Intake
      Event Creation
      Consensus
      AppNotifier
      ❔💢💥🚦
      Branch Detection
      ❔🌀
      Consensus Engine
      ❔🚦
      🌀
      EventCreationManager
      ❔❤️🌀🏥🚦
      TransactionPool
      ♻️❔🏥🖋️🚦
      ♻️
      ⚰️
      InlinePcesWriter
      ✅❔🌀📀
      Mystery Input
      ❤️
      🏥
      💨
      🚦
      💾
      📀
      State Signature Collector
      💢
      ISS Detector
      💀
      💥
      🖋️
      Transaction Handler
      💨🔮
      getSystemTransactions
      gossip
      ❔🌀🏥📬🚦
      postHasher_notifier
      🍎
      📬
      🔮
      consensus events
      rounds
      handleConsensusRound
      event window
      future hash
      self events
      get transactions
      PlatformEvent
      non-validated events
      unordered events
      health info
      check system health
      checkSignedStates
      evaluate status
      heartbeat
      PlatformStatusAction
      IssNotification
      PlatformEvent
      PlatformEvent
      events to gossip
      non-deduplicated events
      mystery data
      checkForBranches
      events to write
      PlatformEvent
      PlatformEvent
      hash override
      self events
      stale events
      publishStaleEvent
      non-validated events
      hashed states
      handleStateAndRound
      hashed states
      signState
      state and round
      complete state
      states
      complete state notification
      state written notification
      PlatformStatusAction
      minimum identifier to store
      submit transaction
      PlatformStatus
      setState
      unhashed state and round
      registerState
      stateAndRound with system transactions
      submit transaction
      preconsensus state signatures
      futures
      post consensus state signatures
      unhashed event
      unhashed event
      done streaming pces
      state hashed notification
      \ No newline at end of file diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/generate-platform-diagram-with-inline-pces.sh b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/generate-platform-diagram-with-inline-pces.sh deleted file mode 100755 index d988f6b115b9..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/generate-platform-diagram-with-inline-pces.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env bash - -# The location were this script can be found. -SCRIPT_PATH="$(dirname "$(readlink -f "$0")")" - -# You must install mermaid to use this script. -# npm install -g @mermaid-js/mermaid-cli - -# Add the flag "--less-mystery" to add back labels for mystery input wires (noisy diagram warning) - -pcli diagram \ - -l 'TransactionPrehandler:futures:TransactionHandler' \ - -l 'EventCreationManager:get transactions:TransactionPool' \ - -l 'ConsensusEventStream:future hash:TransactionHandler' \ - -s 'EventWindowManager:event window:🌀' \ - -s 'Heartbeat:heartbeat:❤️' \ - -s 'TransactionPrehandler:futures:🔮' \ - -s 'pcesReplayer:done streaming pces:✅' \ - -s 'InlinePcesWriter:events to gossip:📬' \ - -s 'extractOldestMinimumGenerationOnDisk:minimum identifier to store:📀' \ - -s 'StaleEventDetectorRouter:non-validated events:🍎' \ - -s 'Mystery Input:mystery data:❔' \ - -s 'StateSigner:submit transaction:🖋️' \ - -s 'StateSigner:signature transactions:🖋️' \ - -s 'IssDetectorSplitter:IssNotification:💥' \ - -s 'getStatusAction:PlatformStatusAction:💀' \ - -s 'LatestCompleteStateNotifier:complete state notification:💢' \ - -s 'OrphanBufferSplitter:preconsensus signatures:🔰' \ - -s 'RunningEventHashOverride:hash override:💨' \ - -s 'TransactionResubmitterSplitter:submit transaction:♻️' \ - -s 'StaleEventDetectorRouter:publishStaleEvent:⚰️' \ - -s 'toStateWrittenToDiskAction:PlatformStatusAction:💾' \ - -s 'StatusStateMachine:PlatformStatus:🚦' \ - -s 'HealthMonitor:health info:🏥' \ - -g 'Orphan Buffer:OrphanBuffer,OrphanBufferSplitter' \ - -g 'Event Intake:EventHasher,InternalEventValidator,EventDeduplicator,EventSignatureValidator,Orphan Buffer' \ - -g 'Consensus Engine:ConsensusEngine,ConsensusEngineSplitter,EventWindowManager,getCesEvents' \ - -g 'State Snapshot Manager:saveToDiskFilter,StateSnapshotManager,extractOldestMinimumGenerationOnDisk,toStateWrittenToDiskAction,toNotification' \ - -g 'State File Management:State Snapshot Manager,📀,💾' \ - -g 'State Signature Collector:StateSignatureCollector,reservedStateSplitter,allStatesReserver,completeStateFilter,completeStatesReserver,extractConsensusSignatureTransactions,extractPreconsensusSignatureTransactions,LatestCompleteStateNotifier' \ - -g 'State Signature Collection:State Signature Collector,LatestCompleteStateNexus,💢' \ - -g 'Transaction Resubmitter:TransactionResubmitter,TransactionResubmitterSplitter' \ - -g 'Stale Event Detector:StaleEventDetector,StaleEventDetectorSplitter,StaleEventDetectorRouter' \ - -g 'Event Creation:EventCreationManager,TransactionPool,SelfEventSigner,Stale Event Detector,Transaction Resubmitter,⚰️,♻️' \ - -g 'ISS Detector:IssDetector,IssDetectorSplitter,IssHandler,getStatusAction' \ - -g 'PCES Replay:pcesReplayer,✅' \ - -g 'Transaction Handler:TransactionHandler,postHandler_stateAndRoundReserver,getState,SavedStateController' \ - -g 'State Hasher:StateHasher,postHasher_stateAndRoundReserver,postHasher_getConsensusRound,postHasher_stateReserver' \ - -g 'Consensus:Consensus Engine,🌀' \ - -g 'State Verification:StateSigner,HashLogger,ISS Detector,🖋️,💥,💀' \ - -g 'Transaction Handling:Transaction Handler,LatestImmutableStateNexus' \ - -g 'Branch Detection:BranchDetector,BranchReporter' \ - -g 'Miscellaneous:Mystery Input,RunningEventHashOverride,HealthMonitor,SignedStateSentinel,StatusStateMachine,Heartbeat,❔,🏥,❤️,💨,🚦' \ - -c 'Orphan Buffer' \ - -c 'Consensus Engine' \ - -c 'State Signature Collector' \ - -c 'State Snapshot Manager' \ - -c 'Transaction Handler' \ - -c 'State Hasher' \ - -c 'ISS Detector' \ - -c 'Wait For Crash Durability' \ - -c 'Stale Event Detector' \ - -c 'Transaction Resubmitter' \ - -c 'Branch Detection' \ - -o "${SCRIPT_PATH}/../../../../../../../../docs/core/wiring-diagram.svg" diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/generate-platform-diagram.sh b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/generate-platform-diagram.sh index 27fc6e12ef7f..aa128129a4d8 100755 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/generate-platform-diagram.sh +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/generate-platform-diagram.sh @@ -16,8 +16,7 @@ pcli diagram \ -s 'Heartbeat:heartbeat:❤️' \ -s 'TransactionPrehandler:futures:🔮' \ -s 'pcesReplayer:done streaming pces:✅' \ - -s 'OrphanBufferSplitter:events to gossip:📬' \ - -s 'getKeystoneEventSequenceNumber:flush request:🚽' \ + -s 'InlinePcesWriter:events to gossip:📬' \ -s 'extractOldestMinimumGenerationOnDisk:minimum identifier to store:📀' \ -s 'StaleEventDetectorRouter:non-validated events:🍎' \ -s 'Mystery Input:mystery data:❔' \ @@ -26,33 +25,29 @@ pcli diagram \ -s 'IssDetectorSplitter:IssNotification:💥' \ -s 'getStatusAction:PlatformStatusAction:💀' \ -s 'LatestCompleteStateNotifier:complete state notification:💢' \ - -s 'OrphanBufferSplitter:preconsensus signatures:🔰' \ -s 'RunningEventHashOverride:hash override:💨' \ -s 'TransactionResubmitterSplitter:submit transaction:♻️' \ -s 'StaleEventDetectorRouter:publishStaleEvent:⚰️' \ -s 'toStateWrittenToDiskAction:PlatformStatusAction:💾' \ -s 'StatusStateMachine:PlatformStatus:🚦' \ - -s 'PcesWriter:durable event info:📝' \ -s 'HealthMonitor:health info:🏥' \ -g 'Orphan Buffer:OrphanBuffer,OrphanBufferSplitter' \ - -g 'Event Intake:EventHasher,InternalEventValidator,EventDeduplicator,EventSignatureValidator,Orphan Buffer,PostHashCollector' \ - -g 'Consensus Engine:ConsensusEngine,ConsensusEngineSplitter,EventWindowManager,getKeystoneEventSequenceNumber,getCesEvents' \ + -g 'Event Intake:EventHasher,InternalEventValidator,EventDeduplicator,EventSignatureValidator,Orphan Buffer' \ + -g 'Consensus Engine:ConsensusEngine,ConsensusEngineSplitter,EventWindowManager,getCesEvents' \ -g 'State Snapshot Manager:saveToDiskFilter,StateSnapshotManager,extractOldestMinimumGenerationOnDisk,toStateWrittenToDiskAction,toNotification' \ -g 'State File Management:State Snapshot Manager,📀,💾' \ - -g 'State Signature Collector:StateSignatureCollector,reservedStateSplitter,allStatesReserver,completeStateFilter,completeStatesReserver,extractConsensusSignatureTransactions,extractPreconsensusSignatureTransactions,LatestCompleteStateNotifier' \ + -g 'State Signature Collector:StateSignatureCollector,reservedStateSplitter,allStatesReserver,completeStateFilter,completeStatesReserver,LatestCompleteStateNotifier' \ -g 'State Signature Collection:State Signature Collector,LatestCompleteStateNexus,💢' \ - -g 'Preconsensus Event Stream:PcesSequencer,PcesWriter' \ -g 'Transaction Resubmitter:TransactionResubmitter,TransactionResubmitterSplitter' \ -g 'Stale Event Detector:StaleEventDetector,StaleEventDetectorSplitter,StaleEventDetectorRouter' \ -g 'Event Creation:EventCreationManager,TransactionPool,SelfEventSigner,Stale Event Detector,Transaction Resubmitter,⚰️,♻️' \ -g 'ISS Detector:IssDetector,IssDetectorSplitter,IssHandler,getStatusAction' \ -g 'PCES Replay:pcesReplayer,✅' \ -g 'Transaction Handler:TransactionHandler,postHandler_stateAndRoundReserver,getState,SavedStateController' \ - -g 'State Hasher:StateHasher,postHasher_stateAndRoundReserver,postHasher_getConsensusRound,postHasher_stateReserver' \ - -g 'Consensus:Consensus Engine,🚽,🌀' \ + -g 'State Hasher:StateHasher,postHasher_stateAndRoundReserver,postHasher_stateReserver' \ + -g 'Consensus:Consensus Engine,🌀' \ -g 'State Verification:StateSigner,HashLogger,ISS Detector,🖋️,💥,💀' \ -g 'Transaction Handling:Transaction Handler,LatestImmutableStateNexus' \ - -g 'Round Durability Buffer:RoundDurabilityBuffer,RoundDurabilityBufferSplitter' \ -g 'Branch Detection:BranchDetector,BranchReporter' \ -g 'Miscellaneous:Mystery Input,RunningEventHashOverride,HealthMonitor,SignedStateSentinel,StatusStateMachine,Heartbeat,❔,🏥,❤️,💨,🚦' \ -c 'Orphan Buffer' \ @@ -62,7 +57,6 @@ pcli diagram \ -c 'Transaction Handler' \ -c 'State Hasher' \ -c 'ISS Detector' \ - -c 'Round Durability Buffer' \ -c 'Wait For Crash Durability' \ -c 'Stale Event Detector' \ -c 'Transaction Resubmitter' \ From 84fb83b7ad2b4bf954c9d51e24aa8bcc9ff79d4f Mon Sep 17 00:00:00 2001 From: IvanKavaldzhiev Date: Wed, 15 Jan 2025 15:40:07 +0200 Subject: [PATCH 19/19] refactor: adapt StatsSigningTestingTool to work with Bytes wrapper for transactions (#17144) Signed-off-by: Mustafa Uzun Signed-off-by: Ivan Kavaldzhiev Co-authored-by: Mustafa Uzun Co-authored-by: Roger Barker --- .../StatsSigningTestingTool/build.gradle.kts | 24 +- .../signing/StatsSigningTestingToolMain.java | 29 +- ...tatsSigningTestingToolStateLifecycles.java | 76 ++++- .../stats/signing/SttTransactionPool.java | 6 +- .../demo/stats/signing/TransactionCodec.java | 20 +- .../StatsSigningTestingToolStateTest.java | 305 ++++++++++++++++++ 6 files changed, 441 insertions(+), 19 deletions(-) create mode 100644 platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/test/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolStateTest.java diff --git a/platform-sdk/platform-apps/tests/StatsSigningTestingTool/build.gradle.kts b/platform-sdk/platform-apps/tests/StatsSigningTestingTool/build.gradle.kts index 2a4f4cf630fa..c960b7ec7229 100644 --- a/platform-sdk/platform-apps/tests/StatsSigningTestingTool/build.gradle.kts +++ b/platform-sdk/platform-apps/tests/StatsSigningTestingTool/build.gradle.kts @@ -1,4 +1,19 @@ -// SPDX-License-Identifier: Apache-2.0 +/* + * 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. + * 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. + */ + plugins { id("org.hiero.gradle.module.application") } // Remove the following line to enable all 'javac' lint checks that we have turned on by default @@ -6,3 +21,10 @@ plugins { id("org.hiero.gradle.module.application") } tasks.withType().configureEach { options.compilerArgs.add("-Xlint:-cast") } application.mainClass = "com.swirlds.demo.stats.signing.StatsSigningTestingToolMain" + +testModuleInfo { + requires("org.assertj.core") + requires("org.junit.jupiter.api") + requires("org.mockito") + requires("org.junit.jupiter.params") +} diff --git a/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolMain.java b/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolMain.java index fb8870d32cd5..de987153daab 100644 --- a/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolMain.java +++ b/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolMain.java @@ -34,6 +34,9 @@ import static com.swirlds.platform.test.fixtures.state.FakeStateLifecycles.FAKE_MERKLE_STATE_LIFECYCLES; import static com.swirlds.platform.test.fixtures.state.FakeStateLifecycles.registerMerkleStateRootClassIds; +import com.hedera.hapi.platform.event.StateSignatureTransaction; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.hedera.pbj.runtime.io.stream.WritableStreamingData; import com.swirlds.common.constructable.ClassConstructorPair; import com.swirlds.common.constructable.ConstructableRegistry; import com.swirlds.common.constructable.ConstructableRegistryException; @@ -52,6 +55,8 @@ import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.SwirldMain; import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -63,12 +68,13 @@ public class StatsSigningTestingToolMain implements SwirldMain { // the first four come from the parameters in the config.txt file + public static final byte SYSTEM_TRANSACTION_MARKER = 0; private static final Logger logger = LogManager.getLogger(StatsSigningTestingToolMain.class); static { try { logger.info(STARTUP.getMarker(), "Registering StatsSigningTestingToolState with ConstructableRegistry"); - ConstructableRegistry constructableRegistry = ConstructableRegistry.getInstance(); + final ConstructableRegistry constructableRegistry = ConstructableRegistry.getInstance(); constructableRegistry.registerConstructable( new ClassConstructorPair(StatsSigningTestingToolState.class, () -> { StatsSigningTestingToolState statsSigningTestingToolState = @@ -77,7 +83,7 @@ public class StatsSigningTestingToolMain implements SwirldMain -1) { // if not unlimited (-1 means unlimited) // ramp up the TPS to the expected value - long elapsedTime = now / MILLISECONDS_TO_NANOSECONDS - rampUpStartTimeMilliSeconds; + final long elapsedTime = now / MILLISECONDS_TO_NANOSECONDS - rampUpStartTimeMilliSeconds; double rampUpTPS = 0; if (elapsedTime < TPS_RAMP_UP_WINDOW_MILLISECONDS) { rampUpTPS = expectedTPS * elapsedTime / ((double) (TPS_RAMP_UP_WINDOW_MILLISECONDS)); @@ -314,4 +320,21 @@ public StateLifecycles newStateLifecycles() { public BasicSoftwareVersion getSoftwareVersion() { return softwareVersion; } + + @Override + @NonNull + public Bytes encodeSystemTransaction(@NonNull final StateSignatureTransaction transaction) { + final var bytes = new ByteArrayOutputStream(); + final var out = new WritableStreamingData(bytes); + + // Add a marker to indicate the start of a system transaction. This is used + // to later differentiate between application transactions and system transactions. + out.writeByte(SYSTEM_TRANSACTION_MARKER); + try { + StateSignatureTransaction.PROTOBUF.write(transaction, out); + return Bytes.wrap(bytes.toByteArray()); + } catch (final IOException e) { + throw new IllegalStateException("Failed to encode a system transaction.", e); + } + } } diff --git a/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolStateLifecycles.java b/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolStateLifecycles.java index 5e7d7e93b880..8bccbc435d42 100644 --- a/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolStateLifecycles.java +++ b/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolStateLifecycles.java @@ -17,10 +17,13 @@ package com.swirlds.demo.stats.signing; import static com.swirlds.common.utility.CommonUtils.hex; +import static com.swirlds.demo.stats.signing.StatsSigningTestingToolMain.SYSTEM_TRANSACTION_MARKER; import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; import static com.swirlds.logging.legacy.LogMarker.TESTING_EXCEPTIONS_ACCEPTABLE_RECONNECT; import com.hedera.hapi.platform.event.StateSignatureTransaction; +import com.hedera.pbj.runtime.ParseException; +import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.common.context.PlatformContext; import com.swirlds.common.crypto.CryptographyHolder; import com.swirlds.common.crypto.TransactionSignature; @@ -77,12 +80,27 @@ public void onPreHandle( @NonNull Event event, @NonNull StatsSigningTestingToolState state, @NonNull Consumer> stateSignatureTransactionCallback) { + final SttTransactionPool sttTransactionPool = transactionPoolSupplier.get(); if (sttTransactionPool != null) { event.forEachTransaction(transaction -> { + // We are not interested in pre-handling any system transactions, as they are + // specific for the platform only.We also don't want to consume deprecated + // EventTransaction.STATE_SIGNATURE_TRANSACTION system transactions in the + // callback,since it's intended to be used only for the new form of encoded system + // transactions in Bytes.Thus, we can directly skip the current + // iteration, if it processes a deprecated system transaction with the + // EventTransaction.STATE_SIGNATURE_TRANSACTION type. if (transaction.isSystem()) { return; } + + // We should consume in the callback the new form of system transactions in Bytes + if (areTransactionBytesSystemOnes(transaction)) { + consumeSystemTransaction(transaction, event, stateSignatureTransactionCallback); + return; + } + final TransactionSignature transactionSignature = sttTransactionPool.expandSignatures(transaction.getApplicationTransaction()); if (transactionSignature != null) { @@ -99,13 +117,29 @@ public void onHandleConsensusRound( @NonNull StatsSigningTestingToolState state, @NonNull Consumer> stateSignatureTransactionCallback) { state.throwIfImmutable(); - round.forEachTransaction(v -> handleTransaction(v, state)); + + round.forEachEventTransaction((event, transaction) -> { + // We are not interested in handling any system transactions, as they are + // specific for the platform only.We also don't want to consume deprecated + // EventTransaction.STATE_SIGNATURE_TRANSACTION system transactions in the + // callback,since it's intended to be used only for the new form of encoded system + // transactions in Bytes.Thus, we can directly skip the current + // iteration, if it processes a deprecated system transaction with the + // EventTransaction.STATE_SIGNATURE_TRANSACTION type. + if (transaction.isSystem()) { + return; + } + + // We should consume in the callback the new form of system transactions in Bytes + if (areTransactionBytesSystemOnes(transaction)) { + consumeSystemTransaction(transaction, event, stateSignatureTransactionCallback); + } else { + handleTransaction(transaction, state); + } + }); } private void handleTransaction(final ConsensusTransaction trans, final StatsSigningTestingToolState state) { - if (trans.isSystem()) { - return; - } final TransactionSignature s = trans.getMetadata(); if (s != null && validateSignature(s, trans) && s.getSignatureStatus() != VerificationStatus.VALID) { @@ -133,6 +167,40 @@ private void handleTransaction(final ConsensusTransaction trans, final StatsSign maybeDelay(); } + /** + * Checks if the transaction bytes are system ones. + * + * @param transaction the transaction to check + * @return true if the transaction bytes are system ones, false otherwise + */ + private boolean areTransactionBytesSystemOnes(@NonNull final Transaction transaction) { + final var transactionBytes = transaction.getApplicationTransaction(); + + if (transactionBytes.length() == 0) { + return false; + } + + return transactionBytes.getByte(0) == SYSTEM_TRANSACTION_MARKER; + } + + private void consumeSystemTransaction( + @NonNull final Transaction transaction, + @NonNull final Event event, + @NonNull + final Consumer> + stateSignatureTransactionCallback) { + try { + final Bytes transactionBytes = transaction.getApplicationTransaction(); + final Bytes strippedSystemTransactionBytes = transactionBytes.slice(1, transactionBytes.length() - 1); + final StateSignatureTransaction stateSignatureTransaction = + StateSignatureTransaction.PROTOBUF.parse(strippedSystemTransactionBytes); + stateSignatureTransactionCallback.accept(new ScopedSystemTransaction<>( + event.getCreatorId(), event.getSoftwareVersion(), stateSignatureTransaction)); + } catch (final ParseException e) { + logger.error("Failed to parse StateSignatureTransaction", e); + } + } + private void maybeDelay() { if (SYNTHETIC_HANDLE_TIME) { final long start = System.nanoTime(); diff --git a/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/SttTransactionPool.java b/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/SttTransactionPool.java index 604a66727370..51dee62964d7 100644 --- a/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/SttTransactionPool.java +++ b/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/SttTransactionPool.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2024 Hedera Hashgraph, LLC + * Copyright (C) 2022-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. @@ -165,7 +165,7 @@ private void init() { final byte[] sig = exSig.getSignature(); transactions[i] = TransactionCodec.encode(alg, transactionId, sig, data); - } catch (SignatureException e) { + } catch (final SignatureException e) { // If we are unable to sign the transaction then log the failure and create an unsigned transaction logger.error( EXCEPTION.getMarker(), @@ -202,7 +202,7 @@ private void tryAcquirePrimitives() { if (algorithm.isAvailable()) { activeAlgorithms.put(algorithm.getId(), algorithm); } - } catch (Exception ex) { + } catch (final Exception ex) { logger.error( EXCEPTION.getMarker(), "Failed to Activate Signing Algorithm [ id = {}, class = {} ]", diff --git a/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/TransactionCodec.java b/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/TransactionCodec.java index f7d27a361477..abbc068a47c1 100644 --- a/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/TransactionCodec.java +++ b/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/TransactionCodec.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2024 Hedera Hashgraph, LLC + * Copyright (C) 2022-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. @@ -30,14 +30,15 @@ * The core transaction encoder and decoder implementation. See below for the binary transaction format specification. *

      * Transaction Structure: - * ------------------------------------------------------------------------------------------------------------ - * | 8 bytes | 1 byte | 1 byte | 4 bytes | pklen bytes | 4 bytes | siglen bytes | 4 bytes | datalen bytes | - * |---------|--------|----------|-----------|-------------|---------|--------------|---------|---------------| - * | id | signed | sigAlgId | pklen | pk | siglen | sig | datalen | data | - * ------------------------------------------------------------------------------------------------------------ + * --------------------------------------------------------------------------------------------------------------------- + * | 1 byte | 8 bytes | 1 byte | 1 byte | 4 bytes | pklen bytes | 4 bytes | siglen bytes | 4 bytes | datalen bytes | + * |--------|---------|--------|----------|-----------|-------------|---------|--------------|---------|---------------| + * | marker | id | signed | sigAlgId | pklen | pk | siglen | sig | datalen | data | + * --------------------------------------------------------------------------------------------------------------------- */ final class TransactionCodec { + public static final byte APPLICATION_TRANSACTION_MARKER = 1; public static final byte NO_ALGORITHM_PRESENT = -1; private static final int PREAMBLE_SIZE = Long.BYTES + (Byte.BYTES * 2); @@ -78,11 +79,14 @@ public static int overheadSize(final SigningAlgorithm algorithm) { public static byte[] encode( final SigningAlgorithm algorithm, final long transactionId, final byte[] signature, final byte[] data) { - final ByteBuffer buffer = ByteBuffer.allocate(bufferSize(algorithm, (data != null) ? data.length : 0)); + final ByteBuffer buffer = ByteBuffer.allocate(1 + bufferSize(algorithm, (data != null) ? data.length : 0)); final boolean signed = algorithm != null && algorithm.isAvailable() && signature != null && signature.length > 0; - buffer.putLong(transactionId) + // Add a marker byte in the very beginning to indicate the start of an application transaction. This is used + // to later differentiate between application transactions and system transactions. + buffer.put(APPLICATION_TRANSACTION_MARKER) + .putLong(transactionId) .put((signed) ? (byte) 1 : 0) .put((signed) ? algorithm.getId() : NO_ALGORITHM_PRESENT) .putInt((signed) ? algorithm.getPublicKeyLength() : 0); diff --git a/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/test/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolStateTest.java b/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/test/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolStateTest.java new file mode 100644 index 000000000000..705d431da603 --- /dev/null +++ b/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/test/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolStateTest.java @@ -0,0 +1,305 @@ +/* + * 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. + * 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.swirlds.demo.stats.signing; + +import static com.hedera.hapi.platform.event.EventTransaction.TransactionOneOfType.APPLICATION_TRANSACTION; +import static com.hedera.hapi.platform.event.EventTransaction.TransactionOneOfType.STATE_SIGNATURE_TRANSACTION; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.hedera.hapi.node.base.SemanticVersion; +import com.hedera.hapi.node.base.Timestamp; +import com.hedera.hapi.node.state.roster.Roster; +import com.hedera.hapi.platform.event.EventCore; +import com.hedera.hapi.platform.event.EventTransaction; +import com.hedera.hapi.platform.event.GossipEvent; +import com.hedera.hapi.platform.event.StateSignatureTransaction; +import com.hedera.pbj.runtime.OneOf; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.common.platform.NodeId; +import com.swirlds.demo.stats.signing.algorithms.X25519SigningAlgorithm; +import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; +import com.swirlds.platform.consensus.ConsensusSnapshot; +import com.swirlds.platform.consensus.EventWindow; +import com.swirlds.platform.event.AncientMode; +import com.swirlds.platform.event.PlatformEvent; +import com.swirlds.platform.gossip.shadowgraph.Generations; +import com.swirlds.platform.internal.ConsensusRound; +import com.swirlds.platform.system.Round; +import com.swirlds.platform.system.SoftwareVersion; +import com.swirlds.platform.system.transaction.ConsensusTransaction; +import com.swirlds.platform.system.transaction.TransactionWrapper; +import java.security.SignatureException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class StatsSigningTestingToolStateTest { + + private static final int transactionSize = 100; + private Random random; + private StatsSigningTestingToolState state; + private StatsSigningTestingToolStateLifecycles stateLifecycles; + private StatsSigningTestingToolMain main; + private Round round; + private PlatformEvent event; + private Consumer> consumer; + private List> consumedSystemTransactions; + private ConsensusTransaction consensusTransaction; + private StateSignatureTransaction stateSignatureTransaction; + + @BeforeEach + void setUp() { + final SttTransactionPool transactionPool = mock(SttTransactionPool.class); + final Supplier transactionPoolSupplier = mock(Supplier.class); + final Function versionFactory = mock(Function.class); + state = new StatsSigningTestingToolState(versionFactory); + stateLifecycles = new StatsSigningTestingToolStateLifecycles(transactionPoolSupplier); + main = new StatsSigningTestingToolMain(); + random = new Random(); + event = mock(PlatformEvent.class); + + final var eventWindow = new EventWindow(10, 5, 20, AncientMode.BIRTH_ROUND_THRESHOLD); + final var roster = new Roster(Collections.EMPTY_LIST); + when(event.transactionIterator()).thenReturn(Collections.emptyIterator()); + round = new ConsensusRound( + roster, + List.of(event), + event, + new Generations(), + eventWindow, + new ConsensusSnapshot(), + false, + Instant.now()); + + consumedSystemTransactions = new ArrayList<>(); + consumer = systemTransaction -> consumedSystemTransactions.add(systemTransaction); + consensusTransaction = mock(TransactionWrapper.class); + + final byte[] signature = new byte[384]; + random.nextBytes(signature); + final byte[] hash = new byte[48]; + random.nextBytes(hash); + stateSignatureTransaction = StateSignatureTransaction.newBuilder() + .signature(Bytes.wrap(signature)) + .hash(Bytes.wrap(hash)) + .round(round.getRoundNum()) + .build(); + + when(transactionPoolSupplier.get()).thenReturn(transactionPool); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void handleConsensusRoundWithApplicationTransaction(final boolean signedTransaction) throws SignatureException { + // Given + givenRoundAndEvent(); + + final var transactionBytes = + signedTransaction ? getSignedApplicationTransaction() : getUnsignedApplicationTransaction(); + + when(consensusTransaction.getApplicationTransaction()).thenReturn(transactionBytes); + + // When + stateLifecycles.onHandleConsensusRound(round, state, consumer); + + // Then + assertThat(consumedSystemTransactions.size()).isZero(); + } + + @Test + void handleConsensusRoundWithSystemTransaction() { + // Given + givenRoundAndEvent(); + + final var stateSignatureTransactionBytes = main.encodeSystemTransaction(stateSignatureTransaction); + when(consensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + + // When + stateLifecycles.onHandleConsensusRound(round, state, consumer); + + // Then + assertThat(consumedSystemTransactions.size()).isEqualTo(1); + } + + @Test + void handleConsensusRoundWithMultipleSystemTransaction() { + // Given + final var secondConsensusTransaction = mock(TransactionWrapper.class); + final var thirdConsensusTransaction = mock(TransactionWrapper.class); + when(event.getConsensusTimestamp()).thenReturn(Instant.now()); + when(event.consensusTransactionIterator()) + .thenReturn(List.of(consensusTransaction, secondConsensusTransaction, thirdConsensusTransaction) + .iterator()); + + final var stateSignatureTransactionBytes = main.encodeSystemTransaction(stateSignatureTransaction); + + when(consensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + when(secondConsensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + when(thirdConsensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + + // When + stateLifecycles.onHandleConsensusRound(round, state, consumer); + + // Then + assertThat(consumedSystemTransactions.size()).isEqualTo(3); + } + + @Test + void handleConsensusRoundWithDeprecatedSystemTransaction() { + // Given + givenRoundAndEvent(); + + when(consensusTransaction.getApplicationTransaction()).thenReturn(Bytes.EMPTY); + when(consensusTransaction.isSystem()).thenReturn(true); + + // When + stateLifecycles.onHandleConsensusRound(round, state, consumer); + + // Then + assertThat(consumedSystemTransactions.size()).isZero(); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void preHandleConsensusRoundWithApplicationTransaction(final boolean signedTransaction) throws SignatureException { + // Given + givenRoundAndEvent(); + + final var bytes = signedTransaction ? getSignedApplicationTransaction() : getUnsignedApplicationTransaction(); + + final var eventTransaction = new EventTransaction(new OneOf<>(APPLICATION_TRANSACTION, bytes)); + final var eventCore = mock(EventCore.class); + final var gossipEvent = new GossipEvent(eventCore, null, List.of(eventTransaction), Collections.emptyList()); + when(eventCore.timeCreated()).thenReturn(Timestamp.DEFAULT); + event = new PlatformEvent(gossipEvent); + + // When + stateLifecycles.onPreHandle(event, state, consumer); + + // Then + assertThat(consumedSystemTransactions.size()).isZero(); + } + + @Test + void preHandleConsensusRoundWithSystemTransaction() { + // Given + givenRoundAndEvent(); + + final var stateSignatureTransactionBytes = main.encodeSystemTransaction(stateSignatureTransaction); + final var eventTransaction = + new EventTransaction(new OneOf<>(APPLICATION_TRANSACTION, stateSignatureTransactionBytes)); + final var eventCore = mock(EventCore.class); + final var gossipEvent = new GossipEvent(eventCore, null, List.of(eventTransaction), Collections.emptyList()); + when(eventCore.timeCreated()).thenReturn(Timestamp.DEFAULT); + event = new PlatformEvent(gossipEvent); + + // When + stateLifecycles.onPreHandle(event, state, consumer); + + // Then + assertThat(consumedSystemTransactions.size()).isEqualTo(1); + } + + @Test + void preHandleConsensusRoundWithMultipleSystemTransaction() { + // Given + when(event.getConsensusTimestamp()).thenReturn(Instant.now()); + + final var stateSignatureTransactionBytes = main.encodeSystemTransaction(stateSignatureTransaction); + + final var eventTransaction = + new EventTransaction(new OneOf<>(APPLICATION_TRANSACTION, stateSignatureTransactionBytes)); + final var secondEventTransaction = + new EventTransaction(new OneOf<>(APPLICATION_TRANSACTION, stateSignatureTransactionBytes)); + final var thirdEventTransaction = + new EventTransaction(new OneOf<>(APPLICATION_TRANSACTION, stateSignatureTransactionBytes)); + final var eventCore = mock(EventCore.class); + final var gossipEvent = new GossipEvent( + eventCore, + null, + List.of(eventTransaction, secondEventTransaction, thirdEventTransaction), + Collections.emptyList()); + when(eventCore.timeCreated()).thenReturn(Timestamp.DEFAULT); + event = new PlatformEvent(gossipEvent); + + // When + stateLifecycles.onPreHandle(event, state, consumer); + + // Then + assertThat(consumedSystemTransactions.size()).isEqualTo(3); + } + + @Test + void preHandleConsensusRoundWithDeprecatedSystemTransaction() { + // Given + givenRoundAndEvent(); + + final var stateSignatureTransactionBytes = + StateSignatureTransaction.PROTOBUF.toBytes(stateSignatureTransaction); + final var eventTransaction = + new EventTransaction(new OneOf<>(STATE_SIGNATURE_TRANSACTION, stateSignatureTransactionBytes)); + final var eventCore = mock(EventCore.class); + final var gossipEvent = new GossipEvent(eventCore, null, List.of(eventTransaction), Collections.emptyList()); + when(eventCore.timeCreated()).thenReturn(Timestamp.DEFAULT); + event = new PlatformEvent(gossipEvent); + + // When + stateLifecycles.onPreHandle(event, state, consumer); + + // Then + assertThat(consumedSystemTransactions.size()).isZero(); + } + + private void givenRoundAndEvent() { + when(event.getCreatorId()).thenReturn(new NodeId()); + when(event.getSoftwareVersion()).thenReturn(new SemanticVersion(1, 1, 1, "", "")); + when(event.getConsensusTimestamp()).thenReturn(Instant.now()); + when(event.consensusTransactionIterator()) + .thenReturn(Collections.singletonList(consensusTransaction).iterator()); + } + + private Bytes getSignedApplicationTransaction() throws SignatureException { + final byte[] data = new byte[transactionSize]; + random.nextBytes(data); + + final var alg = new X25519SigningAlgorithm(); + alg.tryAcquirePrimitives(); + final var exSig = alg.signEx(data, 0, data.length); + final var sig = exSig.getSignature(); + final var transactionId = 80_000L; + return Bytes.wrap(TransactionCodec.encode(alg, transactionId, sig, data)); + } + + private Bytes getUnsignedApplicationTransaction() { + final byte[] data = new byte[transactionSize]; + random.nextBytes(data); + + final var transactionId = 80_000L; + return Bytes.wrap(TransactionCodec.encode(null, transactionId, null, data)); + } +}

    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≶;-Y-6Vn`s6kwY@Grwb>!`!jH8(-K+E6}7 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

    `8FI;E7#ZO{P+)-R_ zV>JT6{a+oen%t5p#Z)*rTC%n4(D@OWdb%O3{n zU0-R@+CDs;qfQVInCRwa1X2KLd$Htys0P?$^jbU3^zM=G7aM^k$jqKkLMVxg;{l;=_?Go+F8|=>D9Tg&~R|d)Y?)FO=yUD z9z&gP#Nmcl&;56cPQY}th!}Q$Ik*E9*&|G`*pw4I|XFw_xyU_26DW* z)$Ssi`Jj)>R^@`Y_eB-M84Jlj(6(J3$Z#z9P@{=^Qne0viM}q9+Hv&az?#-X+CfOW zK()TLgC#H7w(la`<+CC7qlNS}I`wiqoMQNp^&!FbrPXJKqp`f3Sqiq1l*?2W&Z&$k zZ)X%$>1f)5-Q;UMew&?Ac=1RB_U*@#*5NUe4a+{6oOyVtG!k8tQg5XA5P}%p-iVUmd7ZQ(eY(WxjcDb-|%5NE^yUB+Al;$x~!DdpLnh z%(-RyR}4-#4D&DEeVoN=ojA1VTz@fNS~J;sJwlLFeQ+i}JZfMx4XZRv*wrS?cb$9; zS7z18icwmKV+rqIqh3e=P|x4;%~Auf><{|g_fPSSEXC85;JEBbU>RTD*f8iwyw!x# z^DZQ2_jN7|Q&G=8dt(ogSW))Z-0PrKlN1B;=W2bY?>pZbfVw^(fcwAT`&NnTq%;x& z9VLLzIM^*=On~2(v=R9NJD)me_bp+c9WydI&i<9gUQ^}#DE%D&*1w+wc;fev#E&Ha z?*CPT?-LsT&d2zhZzE{|CcdLdud|J@g&mkI7H%&44hA@qNG_wIEqxw1NfhQ>()(h2 zXH1pOR?DXDMBg5!eYUi11wPNfc8CqZ2_$!?5(&&mcdNaHvW$;BeF33cZ zCdc#WM$Su$nI^c?5@*CfIsK29ZcI?EJz1TwmJ0f~acALF2S|><%Cw=^a0L;0NP_kn zWzb?+dq@W)OrFJ8vSe5(5$%#UGPOfDGJ#U&O*hiHW0w8q(ce6A#TkZJ-g#)gka!$A zmtin#+_`Llr5n6*;}K~DH6B@n5{89uIBCzlKQ5q)5w(KsI3EdPR2mg_p_4I2C7xMG z)K|>j;WW{VbN1h^%=E}r$7G+J4P|6JaX>L*m)wcOnCi|_X#@HeLepOd%UboKa?51k zl}E6^2IW|NYKT$;S3?lZh7qT?7DR80#XF)hGJk>j{%?p6-q!A4xTG0;d%}a^A5bKjs>H2lR_q)aZ#f~_8D!`0C@ZJB1 z_y+vCe)3QL@NoVYe3RR?C^2*Hn-Z|0i;BIn-u%d8Jfs^J+*k>UdjOATe}zw-u-VTG zq08-6?xsXB4Oo%;uU2?&06;7JN5~Cu>w9Yfu7Mc;wPx;bzWMtu_lQ#-G9E!dQ8MYA z={l@7{itfQhiauOC4#7CRCDN0CB$ZiH$Jvx!&o0xoJzKYVkI5Ku!nZ_)$f}e5O^O= z9kC>L9tQ%W58s=V7r!;ZhrC=wex3fwb#)Ad*lGxdjSpjXR9}x2 zw*+)43yVRnMv?`j&sB%+XfoMbgwzG^e=+IH`FyR%Sq={9NBM z-<@Rf#lg3O6)$$j*u7`foIZBj|Iop1XwBa@_@T9&RvcLl!;x6 zu?rcGL!@H&Y?9yUcM~)nz;e%ge*m1H-y}Eihxi7N@e|*Fcn$e4`2MH;Z))8g)Mw;> z9WV&fP~WQBwoK=|Kx4EIhMewm7>_)reM8>@t!ZATS4{lmjvgWP;jLt+%N1d>yWW2E zAmcF_xl9mMmhgwdy?lcwX+mY4+UCNDZLXJ566>8I@tD@sHEG8%%SK(Y*XaJ8>f`Q?L`t%;z}qSNVD4~l#Q`t>^o*6NviXfQtGUH= zm&=FO19*4qXLu3ua~10@BH+Pa7>pdK5>(GZ)bt%seLM@?E1-ytVMR9!6th#DWa&#i z?t7w{wPq0Cax|1LHn)buynomo)!B!+^M1zd{~R#r1nO<$c`OslDoOu5bd=*gp+Z)R z-so+r81&@Zdim1lMM6`N}eFDEpQ zRW?Ttk5u1%+>$wJz8w*K?8!&Xu1?&6|5^ruQz1Pwj!j+U-JR-%YfjV&2YSi0RKJ;) z#W*F-e#ZpfH!^~SZ5zUFCW^T`NyZELn)954mcbAajjA@z7d8eipS$C!5Y=oVD3w!Tzq?XxIp0og5H^#%FDQScH-8UQW;k8 z6hIlz#8X~q)$pV=2M$(ogzLW~3Ly%McHoa)&klv?oV&t^&7QHn8_#dr0(;VF1G0Fh zR`+bf-v|~6?Nj>mr+yTy!GB7y8gh3eZdOW-GWJ-PVlNOHTm5pW=H`KVwk9#jZ~Zb$ z#-u(E__RaWI=o!_#M$}i_*|{;-Q{2B=kD^y`O#kp);}HLq%)|`eXiBt9daFg9d|2I zP#*srMnBN1f4fxcgY6JIq%5?zu-Mo=t7WHA+L}MXq?2pXr$* z`as?Fc`cA^ocH<3QZ>=RExfu9_hHPk*yrSmdJREnPZa@*di+GOV}A`O$TJmc15+f! zX>w`eLxP&@4i1{O*Pi7HbP~au5&GFFCbC^l$=8;wqg%{$Q3#bJrt%=^HG?WONjw&s zga!Ge$l#VR>n$!zDMMF)45;7nO$QEeOMm2h=nwG?c8&20uHT z|JsD)Z@v=?*kidnnC3}T$5JrfxRhwd4dWKclJ}^GEDf26M1lpulPkFIL$eUpxEQ&z zWq#2^jm0zyI?0b+(YGz)QD zS_JwsA!{A`hIiA~LgeJ0z1tE#Add^iXk2w6-t=;u%BlT8KA}%DZkB}6Kzwo@Rs5+u zVzrABejw-^+Quf=n>WaZLj#S=pl zrK>E^J04gJkLt%5Y$rCgtfT3aXeXw8;=DF&rAT|fFukAXgUBFrIn(Mneai8S5Meb?{5>iDeRKMv=A z!S`@EBh|rpYER1Z?3{a=L}GR>(lPREyq7?x{b3cZ=$Oj^Q4YL)a02*^G8mnujoc3< z{Kf2Z34m??0mt`}f8v|0=-g?zM~ zwVUXrS`{Slz}ScBL?uwY%#ui^_5gYdHMf~$E`<594^4d7deQBAaKsiu(@G{|mnVaE{LN7x-DfnN+q&pYQ$6x7wvh&VDad=4iVk z4P7^6IctpH&V_9-onf^FWT4@kAt-HH?{TeZh<<@05iA48|Y(&W6b{^?_w$;T=6v?grI!JSk$&~tBO`aW39Yo0^9N&k2LR1qqIG&5ryJ#u36x!fOp!sO9 zIg-s8^c?f(ZkB5YDbEB!_bh^`A z2L?6^thL)DzfFQIRZTqK<>OE$&=>f;?{opa#q*#4%rE~1-;WhpGpPg)+O5d~!96zQ zUvYvUW-)v`Kh}8Z6F%l`jkNiYyy{nVv_M})Y87+$W&5E7Kt9B817fV3ZGf8m&9|6Y zXqEg7QzJdiRIk*xev=z-g*O3#!EQU0>&^^S-O-F? z^}wLVs^<85n-gOkgHJn1Dz=}KG%v6?_JtVSjiSf{F2%v-NFkap$>yd^I^SSEHg$1g zn&;yzT{vY>OkS3d!*XFgSy-sqzg6YEgw^H6iJfD+gBhPT zCUpI4YljsM*9HeN^Bfg{elYxCyZxH4dbD(=@2IiFo`Mj+ul|Jci%StR9+i_7(Z{5mu41=E2<*`D7<3dLUOWAVCO!0rjKmxwK zxnpjwy%|8kJz>R57FL@bt>E5`Km>M-uPCWyCD5|%8 zN{5gcK<>F%ULv;riKM6LDowNX*%rU$nL*njrAFqlfH<>`9liWFw=i)%9W$|=R;Y{asB|=W3g~pzBBriq}9WeyFa442z zlTmO_U7Cy|?Lee)hs{K9`-ZxEAx(bh&IoNQLKD;?*oCh%o0}Syw?D1)l!gzH&+7Al z22QVU;&I#X!BzdUqv&!^6U@gPO=bo4;LVp|vZM4#2T{G~LKIwmtKfcA@5^iL8DFMG zE4`!I@Hm0Xfo5y#0jjk>QBIV?-)uPKz5+5`dDZ_~nt~n(U&nlXmPPlN5Cc<_0J5SK`qvX00ILB#PeJ`6cwA-(|={KWSk_KttSx6bTms7cwNCc3IBzVNB7IHeMYNwErkCYsxOAQmF= zVLxObnM&0i9C)p^^yeD>)e8TvDL}t}^zff=|IGKl^P&Id`_*BDwfQLS!}u`W{p)WG zLvWUE{nSAZt}>rHq}`<@r4?eIM(e-|Q;Tm2#A>~=A885^u)^wD3K6`4#M2dqTHg4D zfA(>&u_LY+W?{yVem^EAVZ8kqvmRF@ZWi@k;wl+FNb5ErZK;pHeHUV*iu+@#(lpGb zbwog&O}!X-s(86d$Q@)Y7FlkJyT&W~!SG#N$T&J~$_`=I_%O#7Dw^yfzrvvOF0;hO z`vn^okI<-o}; z0Yk{!sf9`#mA!X6RF4BTvt-+|FlNV*Pc=M2ke8hp>Q1yz8)1qI zxn$XOuLKo+w$JbMd*=Lm!o_$0Wa1C;4d}^#;+yXV(O>6aydCgxeAa6ZPwFM9Oq z_LVF?^m&BsRB{l8t;m*WbPfv%Vz;XI;+H3b!9};xPewCp{hd7k#2R!Bo ziPW||Ao?-7Qcnhv4=Sx%T&6!S4w-P(xb=WlaAwRdgC|eCa?S2jTw1Iumd%ZVm&!f_ zf|Xo4yZToJ|9<@g|xgnUr8ygsFA{_21x1xue8Oq!L0Z!PN|}#_j#8 ztzAYM485AbR+7?;C4c>XRAUiy4o7nsPmB=9J?=gyLubE0Eqn4b2F0r3s*BR6{VbO6 zUUs?MIChAF)h-gJ_2E=PLDWxbaXeR-6QhKZ56yAmwi(41oYxJ3Y2Dvh&c~oMGd#1f z$_fLlC59Vb*zAm-w`ao^&LqLYC6=7=@jV0*W*t$Y5q!%F*!XKnn>6o(C*K>Pj;~G= zK%;WO?+Uy{LRi0oWD+nW5)o|O*;Y(~_o)K1t$)k+^P%o9&khl_e@E%=J%Zzb+xYe6f$x(WXa!bfZVE(b+*w9l(g+7Dws?5SvWk>r*!a1ln znmZt}rBDMprqh|04Ih}&OUvtqfaI znwP>AMgl!Qnt3O}cRRYYWJCrFlM|F^6xmcJAKUa~qms>ANscKts9f)>>Y^n9S?8x9 z0;HMQtdvMWxInrOeUr?x>gVqFL2hFFjzWkxAT>mE0!}=ny7lmO1Gj8qQct`OOQ0is zzACgW`VUT@jsmOD)NCRlONoNqqW91>s*o6Syo#j6>-y)j`pRo380~#1Q-H4c1 zjKGS%{!kztWcbyWFr6i_^;XmQ!VH(GY^tr5h#N5t7<2YqB4Dj-nqh!)@!1x?;TyaW zkLmeSKl&$A{}kUc^83+o)G<{~{k)A&^4vVtaWjFn*iksh+4*L(O7In{k6&EHi$X=N zQwk3&Jv*+RtM#3}@9922M(=*Xcj$R=neNiS1lx48P0gzs&;!#?CRC_JLel<-$wSPq z8n8lGuT2DNI}q9z#^RRy4Sy)%FJ^zM-~XDv|C?`Vz7LW?aFX(~(KbV;L_NiE2ydox zW}f2$W)ZWC$R*3I@-TxQL&8k_DmzJ?gfFHT!jq1nRhVej;602lI*GyN@~lxFI3dPr z4+MOnB5V&}UZ0(F?HP|B6rvVj^;MhaRdak>Ez9V+6jfU>$u*`XOiYo0pVHd|zpf$T z40_o3m8csG)umP8Bkw1)tqHn%$)nWJX?E$Ot+_*y}NT3Plpm zNsc1rz0->4C=z5v6qx214T?gK<>5dJ9!EIq&v$Cc*3-FB+(EcqR2l~t0zYxN?bjFA zrBkKqU?Zfrmtawt5P3ZR?nCwpy{lwlR;4~oAUpl1jCdSJ6{KslrZMXIF!k6J7uH$* z`Lce_X$lXCWQLrl{F%&V!x-NWs!ZO+a$tdKD8LOL^BdZG-#~%gQZiEifC>Jpzx?O{&- z|6~3Vypb^HKlq;hLwo}s`;&hEVejD=d>=15Zl&po9?!gy8}b>qMS^;M#|_UBm1rnK z0h>HWY}31*AzUy^7wS76KX$(c8?zty{_B_Y%=eFL_yFJFyMF?>24ZJ_T75qK&A00C zU>8XrQ8NXKi4e`bmT`#T97*g0lD*~5nN+lw^#qT?U|rZb9U?Y4&Fz2}58+m-U)OBi znypv53~UsS>?eB&;s;(5KS^%NCz80e@kd}s+!6IC`Um8(e2E$bG~Oakxyz8n>g*HB z?2(O^VkLKmFsQRSsv1MXBG_3X2WTiwnwY82XvZu=}u&LpI7nSeS#X zX{L(O?Fl6H8>oE>W|5UBSGpX2eeXKcYr3Fu&sE#nET-dV3OVK9tkmB$)*(_VL@kOA zh#!I269YB2?>co{zAZ3waPV~`lqwZ{02Q6x+fHN+hM22+4_rHij1oRt-EFXhS!tjE z4TBwbXtz)uk?<6f!Tq#OOk(=_AeB-%*{Bpxv45o`c?X1R!}CLjhqc!zwDs2(P4t92 zbnnF`>KFV1v^UNzBsU7hE=5=T&$jrDenaww{pE4~;GfL=A-(|={KWSkR@Yzf{iZT3 zt6a<(XI7*lObrY_5Y%>QsxozXma^;E<>^~(4cQk*ZXo_rLS~*ZJSs41e?OLrRx>YfNHR%9FJ>T1?uer+L~t%et$aAP#gL(k zgHW#?0UkHxp19HA50cL)uquTOckCnD5Qt?!7s|nilh|!2ah>#eh$)J{d*VrU{_b4v zHXGx8tGVoek)Ekc7^d(kgPG{eNGu@|VRR`Jw@7 zkw5w;vww(h!1I6N`wy$@FZlk$832HW`Ky(_Cpr8s)fRj|DVq2e;0%@go0N-NE1%Xufp%md@5!rELRM@-(%PtKYvZn39NTS;(dC*Hf034 zP)zIenyL&T_siQ0Pm|gS^Oyc44Yf%=eR2Vi{N&HGz$CUduIs?qsctQ~L0D0lF zyv1QIH1DwiFD8?KbYl^;G4D@}cTx&VcXO*RRBkWrty*c>-a@dt>I3UgJBg2c_IR)e z!LL33_z}PV7y#~nU$6ox%>fB8;}7~h_lNlYALC0Hhfq5@6oN4jTz>8gsygDB7#MP& z7;F%MG%yj<+3V`z={R00$QGXsH`y>*pn zJlB-?qP~n+>xyLTmxm?`iFh?Uh77wuKY*$}$HNUI5OsDPJ15-6uB&QvQ)AC^qyl{T)*u8o5 z>Z&Z0b{R5(B%uHf#l)IKaQ?1gTEE1DSGy{azXv;SuFWh@n zzeHaEO)O>;9>T64eNZ%i|M68wh#Yavl^6Fq2bl>aP^HdN(g)O<13&@`0Da9bfQ#qX zUfA4E%SpZPF0ZZmC`e3ap(^Lh9SHx9odMM1wKff!1haRap~Vh$1z$GQ%v*f+?NE?r zzOt_OFm6nrF=lQw1jvwYRz5wtqL>}|pQcT$S0Od5SSg{4n1MPhd@}+sP_Iv$huE#zf8T`5&X=%B)v87S;B%>(|})O?0ub<0RB6 zxLsZwSKchcFJZLxzkuBgm2r(=Jusd-YSqkq3aZ>mK_X|VlWUpjV@3AFXcu}cMDGFN zio9!G=&)+D*osNC2j$G1B^?&_*l?S_^wzzFik{}pIev0Uu|pbTw!~=PLNzvd@WWOp zD=0)*QmkNG<10NRu!~nCg|>9V#SJ5JYLM5gStK6QNt7!Zv*3~580;hiR$qQ;O|P8a zTjS{`-$WQ%Xf*544>GJV1q;GQQS>J1*$BT8EEwITb%3pY5Ulw>Bv}7r!tM`;t9~I^ z?;u8d8dk8SJaJ<}N<7x47e22pC@qi{q*Dg)`eei{-21mon3s#t=3ubktTi$KLft2-|MPc;EVwhbQn1kt97Ml8LQC-KbREY+P;UY zNKQl;pMg6K^B6|$;68GA_Sd}5#9-d**k)Tx8$-AmLJ8x;jTebCdrBFrM%$nGz&F+! z#vG5EKQhYiBo0>KEk!}?&Wj4D=#@6ccgK^YDLHMN zCw~Q}KPdkYCQw(4Wg2P&;x-6Fh~O8tes#X#`0(Y%2R3dDuQG#pg^uremHZkMhA{4{ zWN(N{!z34avcq0lL}3PI1Eh81VfrPR9B|7BcRAi0@uu^jg($tJAL27vA+q3{eu&B|EftjFyPG!fAbwo5}mp$NpZTMyG~`w7@k)tLX)m~z4wTY z7}F(;bV$9QZOj?%aDU+7OgiGq7=+&E%zU&_f-(IvxXQ$JRHCL-KVMh%+o$_#I`2H& z;?%quyUHHkw2Izb4wSa(a+MT{BjkKTZZaS9lsNJk&r@-@+6{6$a8uE}k5}0(G67B> z`Tdw*t2PSd^LOiwYi1{9?Wf+zMi^Nnl?@LIT6%_@AM%c_HAe1V$|=NoBG( z(}OEeOToWjR*UwRdMOz*W{@g`-#V$aB;3j)&rkD#>)IPCt%88-0oQ5!1Z3U5ZS{!J zVh5fMUmb0a@ybzxFO4$XQL%?Q?PWZELw~&F^6kLw|tQ@ zZS*0s-c!b;&(-=*zJHuf`-Of(j^APHC_kc(tPRhbbYIEOka{&*ETN7FoE=L~L z5@=qB2~kyvU~+#;)_zp`LkZuf1ht;;0gmq_|8@S`;ruK8>VI)KfjAXOjm8Wl%O(BB zoVhTb6u1Rm^$5Z+D9>-_6pyW%bomhZpk|LOtqIOdTJ^po4TLxq4rDZ+fS#s5ZRulG zx?AUs{FKNKw_c}{y)9P?$0@A-W?JYj)H>v`XF0^8q)kBdL-A>Xcc5-C0E~o@K{jxiP3Opl>mF&NHO6DAEx&B zx@z(R;hfwNfH}kHU#66waPu^NwVPx2CsYvT0Wpgpez(DDKduHIcI$nQ<5G}CaFVqe zc;WQef|M2$3;LcCHojJM3_Mk_3rH!To03ZnpKio^2;S%ga8U4aMC{E5t6ub?5_&~U zRLWAGMx!?F@!Ye~{?864J^twq7_6`QtJT-7xCIiy_5{p3J@iem_)t!mRbyGk^q%OA z0^}rEZ6Hv{K%F*P8sSYYF^mvvZWy=4BRvTOlQDGSm++?O(pkCL8n$(MgfU4t#EM>= zYu0$(EitndB6u8!!T4(3n0*-8XpTxB5Tu-irM#e}IacQ_UomFKMSlj79BA*9pfW^I z%O_T;j$s|{5oj>WKbn;Hrk|EG=j@r*W1=y7Siue*%4q|6cruw{Rc~We>u~9(xDkH; z)?`|CBTT1$f3Ygv;hubN|ENXAcGv_8=@RRK>hP#gdN71J#+-7**P`-D1PW4C^XWG7 zqU>z&V|$|MN|_?wNm#QCVZ1wt=R{$MB(b2J^Qg$(k<;5;TAwc0fQ}Nhux&5Kk5+!_ z)Uj@G5r&l?acDc%?#gtnsg6ymFG7u-1+g$MYhWs>+vOj?GpA6&J|Q^ zH?l}$!5)@BOB7U@Z#V3C_6TNL_!LL?^gN7x;x!U(sGA^cSH-!yA8lah`I*&q_Gm;n z7Wq8R>zw2=R9XSQH2;xi<4fYx^TKle2-^j07z~-v`xA**gdHwcYh!9B5bb*5{SYl~ z3w(5&QMEeoi4kJC%KD}vjxEZic`_$AkYYSGrWb|X8KN@+5e;;AIqiEsOt_q=mtSZW z>mwD*6%<9}Q?XITgiW~<4^T&`)rRGz3b!|-2a-4Xk4aZu5KhJ^DMpls-k}JFG+Mt@ z;@YnP2isD0|6*jWLQSj8&sanF!V&5dP4z<+eGIXZqPy;)-0UJ>Cy_Dc_9TW$ea{ve zeE#IpntMB=o>)1QR-N6sw>Ec4Ck{c}W{5VrA=de^^9P)XqGfjfIGgH89)_T4U}ZWN zKqBw|S+K&^GWvW=VJ2(TS{qe*2sSdW(`49dsLcEJ2oPnuGVzj*vX?8wCAto*}1|cyV6$) zG^d+l^w6Ph5_5UO%L97&4j5l27-|9~8)?tsCsf08i+ST4XpcL7bf^f=wm^p3SwBJ* zHf-^Vh@Fc+L9N&Y$saRN(BKyMBxLleeZLyz2`btn89t9E>@3yd7Q@dLO2h zWBjTk@)j$rX+j?O2Kgn~C>Y0qFcU!`GLl}4@>z>wDnj}HLT2>`dCc~d&Pz-zL(2tL}S`5U`LgxgAHq!)pNLe zAW!aK0#+iq!-*$ECN(oTkSpm81XTD5g&@{P%?XqqlN>va9s8I8nXL(f?IV!j2Xx-K zS0-kK@!sMB#r9!&q!h$tv#eBlZLs3^gsdatA>QmwyRa^p%YL|4g;g^bJ&?;Cg#|C* zHC53t&}zw*i&sdP4v2E}fO>6W2BknDLt5~-F z?YO%O@ep?-LfqY5iMvDGohWf5?(W3hJtQQ=-JQ7dWuJTCg?qU#|EiZ$-&F0INoViw zu3pdldUen0o?>6PV2IkzJ29A!%~N#Li->VoGoVjj1-L+%8ST_sY2=!>pKAE+4gxd+ zAms1II|z{U6W{*|u=y_?@^cjNabKnC~BVOJ{FTWpJqn6}-uF#Yx4XHk5U#Byo_ke%=810{7AHlI? z)zjCaROrp*bTq_^>WDi6-)Nq^I_V8;{fe*TcEjajg8GuKzkqM`$yopA4jJK;xj+;7 zyiF_2*1+FVvU>P=p{&{S3{YQo22w_whGXjKN@(OQ-I~!1QL_a_DojP+yFeO(x&Z}d z>Gsh+X7zR9Go54#xW1kvD_FLdR1y7I4(q>&ZV5BmUKiVdt$9#!f`j_*v6Q0%3)w4UfzCnbTuZ!zWKhTv(Q7 zpeifgIMva@#GDNV8r%7PlW3k!spx(sE&WZ<+XGff!*J12;Ok6{0d^pHAPX)yJP5iG zIPY0lzizd-gJ%08J2)rOaQrA7KscbhhDT(F#b zjCrw?v(OQT_|M&3C`?Q;wq6^#2=?!a?Yv;lM3Z@yx@$=AE#D+~cKKNMfN_kT(1-xB zC}J#O2EatL#$_ay*OtYk6|P%O!`QrlKx$TB=1i-43!9p95U7T0=gGEY$xz39gUQ*+ z*9&W`E3#L8oTp!Soq8_mhK)ll`cY6s+#8%5yAj;oLyk5HmYo>RB6&noV0 z`+xM`rn4(y@cvX47EbowiU@JBOwhiZS@$8^!4aY+&Y1V&*+FD0B+o^?@eJnD#_CYw zc9kMsjl6-K?Liw9Y5HdyO_-n8J?nJG#Fn&@J=!?NjDB9&cR5+y_XVdO%v+RjDiRR%1Z3DksKhh1FQG|_gi$Kf6aP`9gDytfmDE*CSB+|pny>=h5R_qVBu#hX)=g2;5&!8sr8?>Re*&zF4_T(!e4Yf zodNd=pu7Cdl=UBuq5~BA6W@RDN&cO0&|dV86Pn(2f+M3X-DN5nrfJ6RhTd@O-FN7& zqdx8y4#}^3daR#QDY?+|N*yY2LBcgQZU?BsUO2rE(D3}CnCxNDt*SPK9J|74t^}MN z+OqyZu=Y9{2anx0{NQ+Tozps0_Ex;3zDcY))fC<@i2bv=W*SPPa9J$J1s9OWX)Y;l z#R<<;;_H#zu{samzB&zLC5;qML-KwGq+;CaZLjMCQl8Ao^r zn{yfg+`ympr6_GEXuBy+p_UF8lO4wn&4sCozCHQ1gl7jTxVc1qnLEfJ`vp^#QkmrI zI!YShvcyNlhG)EfMOeNis$At=FRk%Lg(Z{=oP8KgGB60o#O`%SS0bX=hY5@}#?bn{``Ed{P!R_BC)2RqYu;&U(X9eZ`JX z38S&zvPi+-=z2PT<6H0P8gKxp`-Oi0<5SkW`+y1l?|i!p_YP`RJmb(N&~`S)n)M2+ zb)rT0uIws%rF<<69UrN) z3(QD2&$5a0*l&q#_T_Sct6bPBc>VKb6$u)>KSJ%jE{2$suPHo?CTdAVGD;YE$h1Obv{V0-XfqIcK>g5~3 z+Q3X7l67dl2SLnAcs%1NG!~Rsn=A=mRBni{B)#K`n?#?#@Zx1!GLE0Ds8JOj1lH{I z*~b%1z;?^O<+}#y>03YYz41@+Z7;hB`Nl@d*N97*O%Vg?`JEE9z!9`fWC0cu zkMmrbEZ=20h-hDJy`PZ_-&3{zUB6!e>iTs27ry`L8BPd(>&U0Vf4wOj>_V#H72?Mh zISL;g-sg2AuD~hE`-Pv@s)*H3xKBxjTku$e@Em56hGvA^mpy_oV;v2}b_s-| z3sQR*m?DOE9Pzafj1_IuLTzCf3>}rz6zuiT4$(E179*CoOt zbssi5*lWl$yx)u!p&@u5j2E1K)Ri`yCl3#c&@V|BMI;;3WDXy?SSMc4XK5pEZWNds z-yVjYW_zS#eBNbU00=n$KM$y=PD8yX`(L&h3=5JBVQ{5<-t-m87O(Rr-}0tKiVql_ zz~oQ9Adu`G`Km`oQJ+{E;EWnJ(J0`#*M590>gn+?jK$+>M1O?p)?UdL%S8ULK_(Fg zDgCkD)A@nbM%|B@(|}daiFQRec`_z-L$(R6Alw!x-bFrtJ?myUwp>ZpAl(Jo2&rJK zh;S_)&#NHXlN&kEspgM7+wBZAx(u=oVO?Jbe_9&<6BhKuU`C^Z& z540lZFjsXfINL>XoGHHWbKO9@J{{A`y8T1iNr$MgrjH2F$vhj1z-P9026F2X7E=%> zdjo!JJ}}@5={O}lZc%wY3|91SE@RdiI`X{oIs30G@;_iJY4f`&KiTlNg7x7Vpa4H~ z$eVviumGa+(}3y^N9g>bL;l0tx&RvHFUkBCl>6cm*#9Z*dxspb`Yv1P6}Q3U*1hT@ z9k5PFQaa9v?#OQUvivEIZl5fCtJg7?EV|T9jdd@ck%cyH+={R-NZybL-XTYATPUdftozf=op(@t(I`w94-w)WzA* z1k=le-3pOh*|_%5gPM)We90+usQ@-_SLYC`JfxZ}dw-i{noBXTZ%7=!q;sUq;nd(- zT9LTure8)*st-i^aRCZ9f?IZfbQKIuZtrMX@Xq!k?y1K^xzDfgy_c5{xC6`Q&t-Nf zdB=6;*5_AKh4}{e_Npx8)5;k`{?)Rv;qu3rXW3PXjE#C)jMu*2OJ_B_`hldpWII+V zw=z@jM#bYKo%*A%J8bjEXBg>(_!cMnw(%e#Y(vJn@ujDq7k!0ScvP#wZA7wJ6;8oQ zf_)zZB$zq<9l5n5W4##-&_9_-^nm~646XkE-(fb3#JT<^YRm3bwQ6;3s|TF z6|_q-EMl)u2|Lb9BMCo9OL>h=iak~9FS@>+e?Or5!^@@t4f7MpBf#CN{ILopCL ztLv!0R56@4#e0VmH%x9d5Z^W&O&En)&$Gg0%!y&yD$a|aunnUu5_IelZKh^EW6NE5 z-PyMcu>cugkkmJp7}Qn*8F`Jzy}iFxm)<&dXnvvk8iOXT_`U=vDSEmqFaKVOi#219xj4rc$G-SSr=rj78_b))P zQ5rNv8oOJj74X*kiuK?YTPYlj2?e{YhwMZ(@we}E-=ljXgS!}9>^G2#Ih^@}dRn!6a6o^lE!4WVp*P7bj#uH_X~D7F6tNtRf}0=blY|Of1K_6q6T* zcIn6a5!iBBYLGlxUH=%%6w+%P(;DnqOQ4lb61=)j<4ncZF?LQbQ@h!X>LMI}P?j4s zgd5>ue7ifw%Bh?2h}synugPVnJHhLq?JxAVbW83`E>o6h#&ulGfCY44ZTC2UTL35L3f20-XQf@@p?T&kP$B>fajYXj~R&fVkHV61EA2qMn?5F*S>4Q^aFye$S1bxbum_yA8hnf{91*y-(ygPXKc0fV3l`j#2K&l$FnCs<81w4U>u+c zUi{_-4@{CQ8PIz^5%m{!3a>k8w?!Z`UKAroO5$U(YIDViyBHqf=Y-3uhMtnalH}>M zw@Zi%u}GoZKIU~UZtvl}+@g>-Q`}!m6=+n!flQFH5X!S1!02_yQ6RLAYyTwW-qddO zz;a~fEo=LQ*d)C*-?J6f=huaMpFBXF}u}~c-*pl z)&X5%ba($|X|^!QyRzkbvHWiGrl_e3$+>yqG)I^iT{K*xB~{QPse@{;7V+$@xu)Lg zbp=+(YAq4>yhsO97l%%+2HKkNf}$xUc)aJ?N_V5~lnX?(0w%T_ch3!cvLUpjTR5!(<8@w&@K&d#oqmkh8CX<4y@A z{y6Wjgd8OnG2S%2o54keEYUsMxC#2I2P*)6nfB4Y9EU0g>=Z?H==SZR0%XVZBBi=SF+79JLl#Z-N_Kh6=89Y?lfcVv8;bH!lXZ(xx(g<=9 zUT>P{UE|@iq=Y_kljSDT+>)GZn;jExdD&l$^c$!N+GDQd7Tj!zylbe3Mhbk2H*#g6 zyp}kD+Jd{o^K`9r>_yurE7jsYW_=sLr9W)*%97UHN!JNhEVWjWmRd453eiS{G@T#g zrF^A=b)$@adhRu87QU@$O{IaYXAWb=m8`;L*;N9q^y_hK^;caY!DQ9tJMy17U#Im# zPs7Usu_QMfq54I*_jIi<(i=H@s-ythD2#-6ypkH<&1PR!poczI8$&#&>7Qw47;FEC zuzO|gZmMQt^EplR>;+UNW{h(=Yb-M~ z(t7QMj@EIN1ObPA{GzVya$mHiOix&wlnkjQ@385jMlI530{jMxRtZ^7m}&MlOV4cj2k4nx0@2JI=Glt6p9(jx0nWl*DJUgl+q=@B@tW<+xT@ z6PJvXq`UPlcK)Q8Tu7OPWl+VeHU6)19U5h?bL1u7M$z~ox*sC;v|jR&tirYnkwhX* zEFDlt-6axnO)xNM)2b@YzBwLZ3G#6Lpcj8_^MYh$j9TB7E_`wEc~Zydg2saty(fxp z{#2f^rrd55eW@bX0tDPxQ|D-!zUds;Mymv5i!#;%Zpy%7fwzppI^^Oh^_ArQGW%p!m?X? z<29g3V=^wfrQrL*s@WW5d+~~LTUZ_n-lrJGI^4soAXDi1jXJ$vC!qlR=_gQyH{OIt zcG1S9hiM-S&ybm47@zu8zP3xXqT1YT4#j=x%TQVlP@?}m${K#%2H=PNC|J9HNU;9H z!&0K%w@-JG9NA-*vMrM0BV*KBw~*VLK3;il=imLRLFCdqPp=UlX_;616_1rd>F1|v zeG{y2G0?97b$vPj>A&!>;xU$nYf}?)&A7*T(9;^v#D<9Z#gw769L{++3A)VGj@G(n z5Tp5Rw$d3E@XrhYF;d@3`0M<;7zv13WWea*`v^jqKR8_Yi@JgP-8wD@`N|ywNO691 zER!9TXW_k7XK^LG=gYeJke!qM1_V`RZ*7=oOUWooS;spB0ci&Da1@{jJA`(}i9AcD zd1CmoZ}-D4(fASg^%9aX1CeSuyPEV%3QXsr5nd;<+yU*d^;@a{A(Mk+&1Vpc@Hm%u z^#ZTY3RzB@({@auhj3j-?ogIh&e_;oiP=-+hKO{30y|~ESE`_p16ez3Nuf})p%ZnN zd?D^!g(4Sf^6IoehbM?D{z3{vGfaR}Ayy3HenroBh#+2;TpXyeqL?f$VdHaur~s{+ zFDn?hV1l1|bH%wY$P(OpRJIac+}0!e2N>@@QTeP-rbDXt9%m3JypJb9gGr0qBIe04ct@@9h`-|lV1<2QluT;kGiW+trxrHJwtej#beZd*W{lL_A{5(d_ zGLYfAGTpymUOs30h^6oQs?B9V$HnSnOZOrjV|M&G@&zpQEszP>Hw1RWokw^h6rN*j_GEh^KJ@) zQx)`zqyZPxVC=qr1n%&e$)!ol<6L09u38WlX-~XqrfYF`72Y9BHLWn&RYhR$)R_ua zt4>dUuHzz2Qd&ci&~z2&phg1ad~vo2NX+`;6h|mZeilUFXn_F=ES$(GK5?YDcKf7e zweU7Q%ny!VGMrJj22>!_dKQQ|3eCpqIAso8*qR8}SGgu8!>PcS_ThP%w!8OiT`XYT zYq&wNi}Uf?L|KwdTu{M6dwHGJ7GX8#imwFwwq!dT&S&4UGMQJt=;x?|;~U9xw)68K z5lNr0P;*wmo7!o!l?LlKNKUQwT?Ed`Gvjr5k>yb0IPxG5znDs2{_z|tIb?8!3b*&w zS_!z!1_NQ}oO<(I?~)>0fASZ}9?#Qzw^2w~E?z4uS3WD4JC^m?+T*QB%uG{msAU8!2H1Fw^p*e((Pwz5y!t(+sEXTd3GwgGK9-VLnjUf^d`p)7c&jfnm4@_KHkl~&awShQG`FbXab;c&!kBDW z;q8d%dAi}IqG_DmMyI_#Ikk}Wdx>mpv)7dwo?zh>)*RQE9(cgxsJB~FaHyF4f_wnf z2~pn8S+1PW-_@UlKFF4x|HbZcZbO_Lwh#<|g{8aSz)C^{y^LX}lBAxBpXiwOo4HK1 z?RyPDybsT?%{LXJ^E2NmJ|5~Lucar~HM67`jiG=1q|5PY!(p;xYr(!fwnh*^?@e-E)tox2bNUJM(2tTf}mY~=QtD^DuX1Ugc$P(7253v zd+jmr+X>^LS!DqpaT%!AS)W8u95vrov{N&#(BN2pP?V_;MfIqi)uJE*kfxm-v+Xk| z6jB%$FW}$E5H+Hi8gg&FJ%q4VGN?HAXT!vLXqMq&C7=c@m-vl-BPA>nK7H#)z7PHo z-+*j?;`=;!?<|BY`zUVxAhgTF5RmHrF9iz2ELd5s4(CmSMQCP3OkG<4Qb4&%IIv60MiTQRFo z=bKUCfta<)jbz;2o>!)1KjrqVgl|#MKb8O}?63cS;~TnBAK=q`=Q}CNwoDxoJ&0(2 z2K5=~jno0Gls_1v88+Q31r8_0VYV10k=T{R-S_N<=B@d7^nRJvp$^Nm`G+{~i<(v; zDGjgp(H<3-XzA^|)3~#VdB5V!jh{0IbWc4lyy--JIOJYuPojVQ`E~FjHD94j$$p`Z zNL&HNHm7Wx5IKQ)ZIgYEB_l9Fz5;w{`3+xzNG-e8BUL!2Ay&d_P2dM_M^`yxZ{+>` zrfyW!A(HF^|5;uEi(53u2%rV>(svSq_&MODuaMOiKc}`?BTnNm)60U;M5Ix=Od-GoOVP7A0`s4r?ql&>IlF)%{( z`Bw!-pn#L~5>Rbr8G-J#Pba?QF$Rsa`VCe;*yvVXqBY0hyq?^yw<8sa58`o3%bZ<8 z4Bm83^gmQ+!}N`=da}iD`Cf$rnDGby$oy)OmG!8Olc4j$wARUqlJpe+4 zbQGHsAot<@{&>sVByGdBkIaMeqeeHYXN=n;Pu2QH-#7jKk-lH>9ad&!h1xWfqL3}a z@ID-w>V+YV5{v-z84Xr$LaJaWy!j9vEQhq|oZEET@=17m+YcoG8Ub(z8UA(gKPCUf z_umoS-}#ogixF}YVPHvQrT#+JT6TWHbImk}p&8qY6&=Are1l53U)oESv3v78CzaBg zt8-}urgi<`4I1gg+lI5~K=>TzEowV`^*qJ_TSD=rM#IjYD*Yit;sJ$hS1*c}=eaa9 zu0qrn-YLftFLVTg1!eGPEhAM1#|c$Z3>Ln|>8`zTw^e^&cCPV912 zWQ&)Zn;Ii>!<}=(x{cKF`i`8XCu4qH>imwjnBeYUdIA=12IVr+v&p!r-`K}(rIgjt zC_j>tD&qi%MRF5IZ}TDDl&|!w>0RVLNl*5J+9;`@+uTM15%T*tE4EQA<+T#rx>6(Q zbKXqh9!p841=cQb>r|eT;(Pw#Tn+Va9A^DY2sFBFosI`OvxwS5Ao(T{!)M%xhf-3BXI=4zN&Gr;b`iOb+7A+;vl&cQBh_EO(saC4}CiR_Sx zTF@PDHqdp}q6QClGLa9|nm3ux!2U`NuE8OGbPhKBTp8ie(&FmwG!}dc++-!nB?K9R zY6^O}JRJnagUvn0i-9Z%*TI4~>kS4>AU1AOFRhgp`G#@XN45V4zQ1VrKYi;*{XYIf zd;_utefj{aef7b8Iy3xXb^Qh3exG(D+n0J9v?DueROmo`b3>R_5Tuc767p@k((PnQ z(RH+j4MI6kJWr?#_O^2Y^Go0O{)^cs{RZ$0IKHL-tl$3%aR1IX>RU#A>0a_4K~j%% zNPAB{zas@hNyV-v%LmYSsX5yN%RLYee9?}{iqEJgkDX7wH5P0Us~^FA^>rqjqhYK_ ziUPa5+)-f;luYh1dx$Tnr4z76K!y#+?7OwFn3wG>nMg zjWg^UOb&9sHjj)Jdf|8T{(vM5yB&in##;&^#K zEqekK7!h_sBrlPRoSvf4k+-odDfLrBLAI}r0*&)k{sZpq`9tr%*;yMf*ij^O3_MP7 zV_O;nuT{%YG}{H=uC8{0tPZ7m!9lxXhNzoC z@IvuhLTBk90=_8x&H1cguawNK?o2zA3{JXsO^8ZC_N?MvaLo${lxeuaGiq9VF`59R z_;o=PZ+2CCx!p2t;oXjn)>zC1F8XF^;q@l(LMB&A*gRjm6i1cx_lJz@is5%b7+!&3 z`H)s7^Jcja3-Zh9%GB59mXEnX2qR6X#B=0TLFvwgEf?hpROZlAZb2RH2-^?Xc?+1v z`x`H5!eGIA$wTG}zXZ%+I|SD!r%=rAK(6eEKPY`39w8HNn$#vB@G=@!1%55+k5kuxFOGieefo#^{ty4e{wb6-l4;(v zAum+5ZxeWdL7ZhFA-5x~d-GMen7cGWOBLA^bynABJ$8kX6c^cJ^uA_BTpo$B{7?em zPW<-~EFkG8zTy8B;Qn2|&B@-#UcmQVqgzs|W0+cA&S_krTb*$sA|6;}>*5c7@PM+_ zy}XZzsup`!Nb6--&mJuV*#Mni2L(HZKgZMeX&NqqN^+7ezs;Y!@N|thQ1$lRjDKHj z=L{Z)Tw_msgB^51a{Q2kOLLNs>&q7zO5`63eW(m2{j?dFK;$$y458AFy%W|Z-y=di z@SkTfL_8ed6CnGHz9Sr}(96bkC%V?OPY5Lwi_tPRP8DF9qy*xS)sd=tT1Z>pNR`T2#t`?=H8Z z*n$iBt9|35SvE#AY?aR7z23x0mReyf?96BCK&j~lynA|*M$-hxe9efeZH47<@^PCC zl3(;IS|$UTG*ng55PL_iKrI~5cv1|XOI5bhCJCyDFIu%@2hXAsYk6`oH5l*(MlZ#vl0p z@`v~aknxj#|KX=Ezu@~1KS2UC%ujqnINklUOOzo#0j2?)g1(r3*`8A9IQg2wa`u)_ zWdT;#=xIq9yIg7*Q;IM5qzX+$LhYKgAm6yAwh&t{{(Pp4MbUj{ZvvYoqw$48QH7Ut zvp8#CyoIu?Lqxd=8Pg+tF38nx+^>-ICO!&4*nqfGv(Wc# z${fYOBQS34!>6@MvFu?qur(TMtXaHhx~4|W0$#6_b4W)(iFL`t`a1OP%noM+_f*)u zwGX-XrnSoXk%1j@YON@BuZ`u(Z)TlA`Lj|>y>hXysKQ73n=wYBSJu@OG z81IF6IU)qpjCw7V)i;WHrpsgwuD+sgqAGrccx*}{u-nJH*cT>jc4&cFv+CRC-HPzc zXrizgRc2z{WGz+CWr!2xZPI6PAhF6KJIEJno?wUb#?n`|!(bk%QgC>c*6aqKE-d|Jd2KyYP+db^|4CvNKDKJ3PUg0h^QE0p8L#QQIc!Bxp7?|?c8VW(7gd3 z%5`RMf&T~gb=_C@_+ITu-f<6&Y=S(^Z@>T;@bdl4w06<_(OaH zvipf|p{N`Ik=_B0TGL}q4Y8wC=Z1qDst>nf8a=+H`3l#V{DfiFlcaHvM)5j$ah|nh zPrv_*E^o#Ew%srOM@ui0ZYO z(o8xsTYIm)$5o{So0dG)Q*Pf%_>0+pD*;g0&wMwJ18VS{Z$gU`%!4s#OeXe?*3B3-ZBcj`#XuFqXzCR6xr;(`5Be2MH-UF?IsgPkgM$dNHbW$h^?4S_K?c<3eRzeL%pc_O%ES(P~elK9qn7VYjs#cSAK3 z0ZM4D9SeMgP_pI%-w)E9DP*HZLnIA~6qCRnVYQ)U@4w7TWQh!H%?Jj$i;B>U+J?cy z?c^)!Cy+Q*C`DF83;-jhm3A(NABUH;Eb*o7bif&{KWU)o6LXbo9I|Y zv`bvh6f^+eV`&*jQ;A*9^_7doYRn}4gZMZ*q3cqgzE74G zK2URf{gV9*-_O!#;CQHI^G4QXR$Hads5-KvuAc!czpI{o_{D_gb=u)5+njT{Q5t^P zI2KNIv(nP5gJ+8ZH9#B-5pF8GO3(WpHUghRWzCOaTR!@6rFTip-sY}Toa;IE$z41)zXO_ea;LF$3iV)onvX@(UwD>#P z67FKmqYI`~EL`yVcMFcR0x#c$0Kom<{cjBM@>+lyf8hJ-pW^!^o(Tg5I>gLk zlwqxeR_Ami+8JdW`R)Z;kM$i|Y%=>W6&8Qdd;%S8fs_4jbUmE`_sYM|Zv2Ape|mlp z=3hImedilYNoUC9G{&qH%lUv?A%cffV{%-hq@b}FtiL%kr)`O#gNhxk9IA!XH&S?8 z&3i%`6z18HMZb1MhJ!_Squ7MRT<;@+>-0cI6^8{6Lf>Ked+8FpBiKmM<2sWDaSU9N zQkEG}e-|?53HB1Zsn9or8no`qD1+QGZ&L`6HS%gb@@FuZiYZi7MdTL}>fWpCVjFg+ zbaY8T34fqX^}!6C*l}Nb);LTT<>@+%(tMzdcz)XQ21Gr4F+?~`Q7PG|OAv@;*EDHw zO#6Mab5J4C)UX6ANw;ze>nYLdLvgq?Sk|_-orP(4FQ}# z9ROAR1>c;*);K(i(r0@z+!;!{btiAm*`=K-g`on*&0#fb_>CJr)%B}DCMH(i!k}VU z$^2@`z+bKKcM0E0`tHra|7$(scfOr0ibjSBZQZhYKqyUa89i#AHk;hkx3;DVmTD(; zxE!%HW6{XH5w1zZ?#Jyk+RnputojR87{)NfN#4-y`)vlso9EX@FgbTV+J1$37{yhNLoA)+EdPvTtoP2_s!Y z9uly$buPR&M6S)M3N_u^;QJE&Yn|@RK>5WO!&^s_YWq_2MVgVQ8m1k3WFVU`&*ute z6H{r(@1kF2tG!9ezQt+0B)9K&JA8>6#tuKb|Hl1#jD^(f<84+6>Wu+#RSRp~f1ykg80%V>HdL&yJmhbw1 z_fKyA5a0hX|Gg)*%|Z!l|8Cv&b1-Ukb8mCb=gnxMPTX-yrj{TndQYN3Z`raO;AV-= zc)E7e_8)h!`!@e=1VHZT_%D2m6+fF_#6Ou7F!G=uS*;nLs5LIKO8rC(hJAtrk+hwI zyWvPHe?hNM98h;6OqCS&cTRvDzuiGUmHOF$S{b0Y|07?v@ zSVD(mMc*UAGUux6J4~`{)1h0R?0{_F@@VS;Wer^*!Ls6DU1IB=*bQx`5ld8Pe)%%y z?5Dliu}6tu5XV3-2uGzlHOKbuU(FZ-@t+yzaKHo)-~>=$gVL8Lj$abELlrrP)$7~% z)9*h}L3`~J2OJS_UK^-MewMU2q(Elv**9%!3=Fu?=wKlbZ%BeG@6o-tpL+mRxX4y?uyJ7!Eep8B};H$6qksAXA zN0nSoRp!Z%Yj||PNMv#ygMn>y*^vKSI4T^G{-Y(TCX3dg5NW!i)}v0}%w_8}xk$N$ zwqx!`P1DnZ zeKx5mKV8O}At{$YZf&+46vATnUKHL6$;iHJ$tz`jd-%*h@LzR6gkXDUJ&o|st&SY|AWhtau)%?+d5Yh?~UlsYo$9=WbrPnK?62qaDP-VRt&MJg^O_PFZXIp3` zo2O*}D!iQS$n^2RwCufNz1Wp3p@^L6MFBu-e#18w7(O6Mq}N&CRP!tNeL=Pa|cvzVcF8?J3i&hjbFVb z;YSY$&@eyo4G)dO^OW|TZ^@zON-NOaqi7wDn_;17>GP9WxAUpw6S9o<9P%oK1Wfs@ z9MVjfbc!Ust9uCyCJFxY80UKBFz;wKc5u7x&4)G}5uKYDlIP;ykS;YKGokZ>PGSZz zbC)S6gQRHPtu(!%rva+Q9sI<+>Fcf?T3!6e165X42 z&A(%Jr<4GCOV0-)H7KXJEykD>ziqvJBBbx+Y~;AOA4{oa(OcFj9!H#qqvTlut==iO z&JA(t_O5NYHr#dXeq^w|E%5-=j%uVCA6&%OWCXNoF}`cg^-3|KuzeEQ|LDWojWey) zF>@1Ae=#}O6-=ub`EXrJY`9I4YNfes!!WYLHAY}Pmy3YJu&}W7#HJZNAGxCjttV{2 zr@CcB&J^{S5CvMf9VLm6U}j(FeB(oajQy}Sjf1OXHR^0dm0d3qCLG9^I_9|u2;lSYAo8BtjA$3=(_ptjTIJP|w_?5An2> zI@nHZb(_0y?bLX;g{EWkQCo*(?>ijucA#6j>a#(zbLeneBxi7viv%KU`C|K@DX2v; z&p5Ueb#xa^JO_1#De%RVhL0xo*7GaLXcUw)`LI8D{pgQtdO|oBu7B`#y2H9oLQ`)^ z1g&Ud(}953V4ICcz@nc$M5XOLQLO8}Ir31;Wu#3B^}&lW{jN+_0;1F!W?L>-IX3RO-a%TRgSJu^J=R{BzE@aN zg%0(?IcTEF!~m-8=02*Ch@)hSzfv<`t9L2Qnf4U+_qguZH9#D<;J|6QsLGR4k5qZZ zO#L4Etej}d&+i=vMbdIi&s%9T6UOd}Iof_NkHdv$cVE$RW2rJk%mdk7(^obYU)-4V zuITeTy<0sWnDw+UMfnquvsfZC<-FMWl?4hrQ)Uvea82Z+x@%}0$K~thMYr@7yC?hn zmT$@mfEj<_`{57q4aoK<{SLK5?gkej@J3kH(p9)^Ek(J zR!@cY8_02FyjGPai08JiBBS@Wz(*AYhgd;S%T1PG_o=-uE}J1xYI-%PaDLtM>Xxf%tiiqLqefGBpC7kQ zNk>i$#>^KVKT?{ef%E(PIjA^_*}gf>)ShV0^UxM6&QtqD4w0&K#{i9*SmjD2%3&xp z^(9N`mwa&MT(_bQs9TZP$Z6=Ga|qB~mkLI$tVqS7Y_hFZhjSlEt^0k_F^oO$c?^o^7_OY3t4do;=(rMHhXJF*i1*PO?&4=FCNPi zVua;t*tW~yfk_si27F8O^_}UfC_7JH#PhXc>~Y*6VPm=uS6A2eqw@~yq2;&RA5iXi zo;MR}U3mEcb^3s;#3ayGp}k^uZ8-iSX*pC{wd8<{ZVIky@B6)Ltw0 z{62pI4XgJ1FIaijh08$%$a@`oLwzI_io`5b-1XKHOW){vI)58q8UyP3bO5OP1>cGm zD^=(2QI6hO%Y)fi`KZIDlKeF;QSj`i%Gu%sq|53ZVloz-AMUbi*^06Hf zeg}O||F(Vy%|LwqDe*hs;0Q4DN^OJzS%uv;^w{I35@iC&5G#3uw7k{hxSQ3^T(Vm| z`Oo7`=x%aS3r|Xu0(7;4?)-q*^?LT>O^{=c?EGyvaC=GR1!hoVC}$lhUXy|81icAr z>$`cbFx)m3`Wi?VgV|9O_S)B!x<$&=>jlg9R^<&y zdT~}8>{xO$Yh$K9f+;P~$*W>KCS9chyo!%yuduq86cqKkbU6f?9)Y18Fo^?6PxSn+ zUqGSRe#Jn$YwlwcYk!U#f_6N3ge;P4o%@Pv_|s zQ`ff{2X1zm%%BOb6s6DVI~vfFM?WTo_-D$Q$A*&z1x5mXH7CnE59uIh9;_vwWubk#ip_ z4f?Rkx?0by^p@>BZrD%ZF2HJjOq+6|x-ySS!I7?}&B>v0-Z-c_wdn{4zIl@XIF*gu za(GfMxS^pB(++<7X4D9#XFkR6>8zvJ+M3E7R_#_=+;!M zv@LwlUyD5@>`^*kV5><9c8okNsiv1ZI-v>F^ju>nGn%>y#cVk6HGC)fOw6m_>w4f! zd@$a^-yMc)MOOIGyyC7aQ=_@G>3g6{$JxfpXV%K;8n}bXd$tASpRj;yQ9cnX8aYy- zvD;kJ)2kO8ljmh%lG6@0#bDScOOU&iKhoIoUbUv8+m{x8*`kWi(#p!G%h`^pP)Do+yLPI z@6UIzKe_&!|1AgfPw^ev!(QqlqhvusKj2NGFL{C5Of8QeWj}3OXxEa;3q831Mjpy8 z+z!!W@6nP(p$PE4(r^CBx7oYj_$LK8pBsfV{gkyp7 z9PdHJt!5DMD~xJ)=3;CFf^QW-Rmx9rFl#9M=aFTXFzwyT5y4)`-G2mou?}=9fzTD- znzJ+y1=28^Lz}WUV=?c$N`BH$Gi)HMqjI&$h*Xy`x;qP2`f1ZH3Nccc%5+t)jsy`l zUBt1+CE8eeyNTZ{#|tTk+&W8Udh2xU76t?(=W{QlXR~;1!?o(lb%Po4CT$?6-3smY zOv6$VcTFjpCR`}Wr)4;x1#DYny;Ct6%YN}JpDo`g+qhG;+%E~0`Y+S(BG^#)`71@! z^ei${IwRe1MwaSoA&MhKtq}`ja3LJEdF)9&BW2o^55}^dx6jtR=nD-e5?oI2?bB7L zN?d$&er?0q$!dpjWOHJN^VoEE0Lg1yj>Lj-)9Df5IPi`rBo^9h8iim%eEt6;?k#}w zShoFb+}&M+yE_DT*FbOyPJ(N2cXtnN!6mpmL4vzWa1tE;y!-6rOU}u;xm7p2zNv!Y z?HZ>0?e+ApZOs}}pZ#8=Sa@S~U3M7!0n*02FUsj2{##iH8Y)^hjVL)pLk(D95N|sP z%~Kjb%A%+4irmVpV?-ETt^i$WkjQU*ZE~=XnSHX=Z~1=vZ{u$T;6KDSK)Zj^??3FF z{R{p6r`HGJbcpMozSVcWNt!zl`19EiaLvm;q)6>HV8^YSW+iAp!!pp|k1EObtHd!$ zd}YAHWINzP`&N1ve5;ZW=boA;#X`}PH_A@Pr`K8#x^28NM%R`--eiixf0=uE-PyhE zQ)zm<5{{D`;o=?^&;h(WFC%DuOeP=Rz98etZM_edv4=aNAR!$io#$l=4$Up5Uy7AM z66UAJ*H%;>X`zIi$2gnsDLs%*?T!m*p_p_xxLwQwHOK}MY_J_r=ML6;UW1TZ>sNMt zmima$SKDt72CRX}L{24zNYXDkDnBpM zKVUEm7#=&nvHmhI9fXrXF?>g{?ZTPrBAv>}Aktl<%o$c1BfI``Ho)Yb&erb0IayL9 zc=u%jbQTQoB6n_kVy^=K>NBDNNS_&btx9;!4Hq`ANh4Pi&GPl%^Nr(+23RHgVSMrw z4@RNrcCov%=_whYUSKvfxScCuEpUL#o_+ua+rLhK{|0>eOJV=|{7n)6gKyZ|wHH(; z392#NvlIfBpba1|UAB?&P&GpCwS<~iCa6KqXJ*=!ViJ1`{np?raRJ^lKi&&4IyU+{ zM?m^7`2NElih#`iD#5q$H{$R>)KgmbU+I~b(E%6T3yFVUR6$(|3?l@C4g&}EHSwVi z0?Et&I2aTK94ju&2LuHK3j)ilbB2Kg1j^YK4+aMY76u%v@c0ZE>EoKgUmpOMA&spW zpqRiI!4NIQ2jxjI^9q7iJoCer5wgo1661_gxMpkWUeBNnfm6YGekOyZMo?u4;cG)W@VefV!JlK7>uXYL3y)h%TqpblMDmZ@ZMWK|0&_$3H%K<(=%t{mG zTN(Tk+pU+60wXgt4Jm%xrI9;po#y|>bCMd*6%s}ftb~7KgZf3IC@JOcn5Z1a8Re!GapPF2)YcBqs>iJ zM1`8MrNXAGdg_lvx5?xD@KPj~thTZvB@P86TQbZ~y^>*xY#ITFU6hD$zF>JsPLQh$ z_EiGaygqqkoW{w*0byVer$?@(xud-mw%QvJl#7!1jo(4@{j zyeL}wZ0GJtlBG&UM7OjidYj);YsSO!z19;tzv+K)eDR(x`l)?_{!{IQ&$ZFTxxBNpAHRv6T=&Tm1L-`J4IiA6{Vs zRLoE96IHVr_9^Xq|09Z*C_YJiV}!;O)i9c~gkfZST*H{nWke2cn`5J|!K`0>h+sTU zoxyhNUUj~*lB^HmoM6=v(V3y1bhy~mGtzv#D}SN4hCwAaot4amb$@N`VVgeK7^5;G)RMTJtsb@p3mS3NK*0#<*y#_PdrBpK70+cDf|6 z$L_e{I|z5)E)3ufV(XI5>0sWfeGcqVIDA3gk)x#Kg-WPJaCZ$-MJCjZPLKse8ZQo! zHQ|#<#)|`u!0;7i{06dm7`qcFet?ggA+)fmNZbMurH&CcPo#Fw4OV@@G}28rU;}g1 z8be50-FP-BqTA4zJuNd^_@(9-T3;QVSM!|ItrX!x|^mm{&5p(2f06 z<+9keQ^DpOIIPc9p5iHr5gDr#%zbvfne-TEJCdIUnXdt8bize2)%mh!gDiJU zoIYagIkTK`4F#Ut4B&5eB?}amwOp47Ld;h~)*VTpE4R z*MMkACY9!y@IoL$pPdRjF~Y^$P_vN&h#qC%f@`jRmP2B+a*Z3M2JG{0W94a3)CgzKBC0&0qw*p9k~aBq}$lL#T*X%v5G!|Mr;#-V6X7A=xVzw zq2wUhuoByMV0%QAHg8@;p*^SHielgilr;QA74Tt^ zS8=BMktwLs+G0Rd^w)K@Ns4z^;?l+`Ur&84e2JvBdb4{BmtWhXC(FnDo^RZkKD2-1 z8~mT*d+;iiV=fD(mGuaivek_;hxkoVG+(<{W)w~er1lQw3j~bixV4dYm|Z=T^q=fa z^qS=uSbQBm@$YiP?P7BWCQQ9PM~uIu;+)aud#Me1^k@aq`vk9rMeUS3sKxO7TNxwWA7# zpUiyTz1;x8vWFkJynge#q$gn*&%OI2=$#G8-ZKN9YTrB4YimW}Rzdbr`0FvXXU1!A zxF#Tv%qL%wup0y`%?odH&7v=2)zW+AYgNP${n_r|5nf-{Kvva%<)fLeS^lCI#X*9J zXoB0TS}{gqlSG{jM0UVXNm)ZH90(i$%=QOWvD|zSEQGi4m$hI z{Sg7(jgQ7AcigT9n3qL4(ulKfgCkKEK0>W<+k>;qMz#t1K3{X+%w74g5lg$BES#H4 zmkCiVYh&7h!5a6BP|^;F;@<`;Y)fft^AUZPsLm!DG9x1#QqA&Cgu z3D>L{M6G?UH$rAbaL(iz#Bn(CZcHzbj7Q|#3<*%uZ!O3S@ZstB*0O%$`)_aP?|l0w z>sgT@wpHH77|q4`+hxCSaHz3O-vL|hhn2%iNV)byDsYSAkY!+$BajvHRL)>+t-Vto z^-p-kiOWu2VaDrU$J-HHAF(fiP-e=az3+Zi<&XnwCNQoc$a2oPr{5d@!0tl%SU^`V zw3K>@lCM+pISL``?rULCpg!3;hCm`@UIv5N8wtec)G;-&yBOuyrcTUjcOcs|S@!rR zwu9>6??omY+MJ$^lB3)grwx&_S1T+h#~|ckHqx1Ha# zV7_&FJP{Ap4=zEpinx!0gLq}C_Pk{DSmJJhCSQ{tQOr={B?Tpf6zTbD6-nqg2l8X;H=rAoM@|K-stT%^&Ie1>b6W z&jhHPT+yv?3_~qMQD-2+Vo-LqvOitu1s@zEzO#-xpe?oIt8wjegXXamF>;C#00Gm~ckPd4nnRr~w;YFzaA>oWNEwqHf|*BZ8&aBMiS{ zq?~6y{w%ly(TKI0v;wULvmTY=2$_7o|8-!)wy@>&*yI!M0O?g&ILratmA*{`c|_(w zxi0V3bX?QRYy;v6bIsr|EYy@>FqDiLdJ|!>^+a#^l6Cn0c404r-OOU|AM?w+K7+Iq;CBlso1@Gk86u$>VhRc`D|&g2 zb;9PDkWO)Dt`-GXP#$(bzRYTzLKB|t1R8d7zGRju!Ax_oB#hsknn4oJVVLcupMW8D z>E$_1+&621#7E8D;IHH%1I3<8W-6+toHi4?WQNEGnlP~hEb}ntdASYKiVVFPW9Jd& z27jpzf%8^I6mj_W$S!7q+p2!dsbodgcj$R$y=(huRFNs*0S^HBdR~A(o)!$$+KH1s zBZngGs?)1m#t>Nd6vVOm7l(cdB<6uO9?xs)-=9AVe%RRD=tFguSr3QW=K{pl)HgdS4LlvLLQ~$zo}NlK90fVNtt--|~%MZPZ*( zaWhW%OC7)LT7(ZESQUpZ$$)1p-?~M>y#R7z{?{M> zO8hi{VjV-A4KeiAI~zqV)pi(oPI4N*!WzIeXF_-V}8urY_#045K^orb)e%J78X?tGDb$gDNMn~3e|hYMz*-zWKj z8aWGmM6`;`U*Fp7gb@I;5cfY8@q8zyvVYuh2eckM0zu>GFf};bY*k5Gf+LY-;Lj5Y z$-8uM&H5(P_g$~leKT#EltJXv`&9F$GszB&6Wf)fAtz6LwNtkgUz~tCt_5?S_21fq zB)b$7T4`CPc6N;Qz4TDii|!$V&3s!!aB7o=_Gx6!>}CYcza^Xwdjzjk8|`DV9{o;P z_}llW-BI<3EX`*Dl(Czj1cVf({mk$4A%>`s{KyyTq%8Gw3c=a+`03M*srg7CV@ZFqM?Vri`@JA z{G>6#={yFX46mPTjk0^mgQw@Y9J!QeZYQ^^#b-P!nZ6}MlmsF>W+^=R(~b)C8(Gy? zOA2-W3x(4!k*Ztey~2o;uN~{QdL~)3aRjbBGdwfVfvYZPhi)n-Il|+i+$IGMUG%3w3dM{J-L_)T@ za-QvC&Mk5ur={dV%E^s6?en~(rd)(Mzb-v9{ao8}{D7nz7OAVxu8`02{Osm^g!Ovo zBn|Lc<_`HM5#55$NmAt7)uBa-13;7itzhXE0@~#df(8AD1nWO`An0d5Ho9Hkp6N z0Ga+ISQtB(EKh0QXAq|OA6r$FxTJU)&=K1}tHdG>Ygd7_d3?ZP(_eiAHhz^@o0bw! ze3O`P`5>kf34O3olE!2i(e!R6Z*l9?W*)2r{19KVSxkHTBhF%V9Z^_wghn~&k(^@> zsbpeUAlS_9O@kM4jqb*=IhH}lil0S}?jFeSmf9tul#=dEs(#LTSx2_9b%I!jIERct z+7v_%GW1%&h!B^N8+1GT7Nm^MqS@%1gZlWkoWrlE@4DLLQd+dVbLzR0`JQErJ7%gz z7;c)fKp>3d@~7=^pcG2p)n1m-YDI**dA^+kVIjil9fj>rl|!CFMWftyq3XX@@|SOP zwqCp`p-;THV`U$T8SK<&oU({QZ}5}GtvU&4w+zCs&3d0UmEjsrJ3DVsV#S=WVhm2J0=}95*EQ;aPG1FJ=$~UY6tlGycFg%pcOEdqPa>J2eCQah%!@_LES{X?^QhaTiEwf-3}&S5o|-9fDh#Oa3ItJ$9a({Gr-` zd^1jcS*pENtDQFvUcH6hpHWeAn|Eln#fsMr3&c!C3`Xnw(zv_C_b$W&4t#6&CE7=i zouT6+(7ULE5D4fm+d;f@hQz@a-X63<2JHHLWXqs5a%)aWBLamXUSp_a!fK`(YY2Qh zcW)ywdC4A#Fq&XmuIy6$r6cKa@;R;!?g4Gv$Ob=+tGHDABzO-Obmz5d1PyO%u-l>) z6k%hHQvN>aXJ_8YCiI(EC#Mn&1Q_kOuM-iy``8l6Q#or(PoWJKjfA|8MD?Fl2%c&9 zl{llwB_W!_Nk6;Uxg2sn)}P~I+ zfC%DU0ek*G@(ue>@x2T;jbQekRc^2xX*Of??{Vg%+v#elZZ59~re&$BksT3wH3`#VAKG_1xQ7c8=pSB$ z$<%>4Dg!@hLwbiMZ0wjep6#T$U5L_5iJ!NyVU5%FxqGnNgh?C3Lg@SmE z{YNkVXG*ZW=PwW~Lb)nF`{#x%mQd*+QfSdArH_H(foRon zlM$Yq$@aD6e56$nE4PnBbHt$28jHnnCB6NiDYxq7UypBCKDW0#1IaQQTb*O_MwP6u z%4r~BVxAIQ`xQys#rqtpwJ}y9sApOaIo|r@oDHxJlY=CLC~#ZtE-4X`(#ax0Ln`jF zSE1x(u?cFoIv0vB2*-6VCuqy3V$;8>2C8P!BB;uqmy&AVJ?l*fY~H-gD%DpkU6>n5 zYv8w{4V{H*g#knu|1IC1fPJhV`G)&Ld;=K#X+rgf{fB?SH`uBN`IdL}s$W|8kTL4X zjORs_Y^k_*CgyU~BilqfR6gkkxchEgYBFcxJ@A^|?XxbpbFaswbETF&m zz<_8ZD>o&$h^wkp-R|1Hi5bZyr3cK(R)hSjp867&cFU>e*17l+Q>Yv4IEbt`Qs|EA2cu|Rr zl9l5y%;8tShkfeNM42UIBsIeJcLN&A4uC*z6(8KLVK{C4n?Y?3Ki28TA*6MYbe({q zl3G{*jj~&$J!86rCv3qV;Hw1INa{3}eH?b#zo(xc0qV9Khq%L2z#5&NqlDhXr=l7a z*fOAgK-`CViP*Bd`m5>Bk@6-H>PB{qM#w&<+>LZQ&!oVootIXF-iZEt!A3K2v zqr>xujl`v2^CmD#xCa+JBqo)vTX?nfph-lo!gIdlZU*DrcGxX zd~x?KOVIVa1q@1S4Cy>W>l8t)Q^@n(XtvKKz4U@KS5}Ezm(;dsOSMoRWYVCM&dN%z z7m)-3+Ev^1Ndt_#Ei=9phDmKkDR!&~?nQ*1q=jUQ7+ja+zyh5lP`(b)mra%rv3OE* z7hO4gz`F~V`O`SMkcZlq+JdYwM7M46SzoP>%{h8k;D}|wqep$Yd_8`R zZRz`%OVfyGj)NEnS>I{788%eJQ#=eG-=HsIbPv?^!Gy6H>q6U<&Uk>NiG@v6DiUdL z|HAFn00wD#>dP0lmQ4YgF?MUP9ni1Yqi7k$d%DNlH7$ILX-=xknU>tV;Aw|s9vIlH zPrTMzj>0?mBfRPczm9^3f`tvubG#5p9FKfhp;>`aJDe8+gy#G|FBlZSH%1Y;!lU%- z^$A-lGQ|ZKa;v;P1!sjdm1PR#4;ChYWJPA265fSE&$V;$Ak;gNGQLuG&`PGBbf&Y* zl?=ABir`}6pqKm7gJA8esOgEp0Tv4c8f!_LkwbtedxmRTwwOT+UWwVPTRheHQ6W(M ze1P&Z+1gx*kQQcgnqsaZFEXgQi!(;!U44)LJ4;1-xfH)n!v0OM>6jg`g2U+#e24*A zy{!%9k;q$h`y}ibUNbhscF5&=TC845m6BG*Ntg$^Qn~20GB)RCqnE8E zrjLsfSg`;_!PxY1DK9@ry^}mK5g(hlbv6$Ru+jw*iQ)(K@%gD1D;47SXB|WhGxZ6C zrmb=0S#2OF*N0W@xW^>WD^DvDg|o$DJYIeTY^zrZh)`a%t~jHzqjJ$ozO4c|xwRhh zmjXoDC?UFxdZks@guoQ1K19(|5?sX&oIVKpP{v-wwp7G96B`7=lM9;DIJ_ZWTF*ySi=!~p9Kp$f`cDY zm8F4GsG-qBzs7f^Xx^1Gnr0OXslO~7)S(OkCrZQ(E_SFRcgw@qx*f;4-l zE^c-k(Tvmy5gB^n;sYa}&Nu%UX-|c=d?T^DchYom!a;3-I9E`Rmo4UDa2u%-MsB8T zuWMcZf)zEHiiW!a5w1?x;*2ZVy;m5;;ZYwo*^I&o#o8%@U4&tLFkwJMvOCc{$7FyNi4bS=gzic^kA1*>_)W``6O~} zDVZ4TdTvB$dO^5l0qGr~GcDcf{qG!Ehd#a3utm-mMyfzr8}GJ>bq^{pj1bpS!KGoG zDzZb;IfbmI-zuJHM=99(U-)nm8OT}z+T{-;EX03Gu(Fo;*!<=4C&nP7;Hiu6JiKYu zm@|%5J#9pYN181M#AZ_)+6SegRG~~#VlCi1rJsuRZF~<9Kfw6@H+#sv0e-ivckvpY zo^mJ1W#N9=^$-nHUI~@fpdH8z8ys%la^(H)r3yoXgps)oy9pgXND(6X=zPL5%|rn27)`63u~zbE4s)4uTrz` z!`j2hFrORk2z7;oz-LC8ZjSZ%zh~0v=u8O1)0k|~oh<6MXPw6Ops(xQllvSQ0Swi>pHOaxYiiYe_kk(CYrEFi z$JVV!`nVF;>fo`Hx8uB?E+%sLO&6Y?9>C;CKzb2dq#j&ssA?Z(*TUsqOTK2=N$yWa zklFy#S8V{H^yN5Uqk|byjAnpd$9e(Me}5z)7NzuLi{Es}#7Ic;05kr;H_{*C8_*g5 z)FBh*Bqo7*^>B(NFKOHb75f2?(brT)f_vo_X=$P?K44l<*zb#;MbVq4wx4!1CqH?) zf9D%;9{}G!ZV3LOL;k~?nt)7y;v0MK@F}j;f9a4r#lnCXET9*`O}?eu9f z^vy`iEw%1px%kzD>u0HHNp6qm@tqm^iC-*{K?i^z+Q7(R=5;t^`n~1HnZM!>MtzWG z^y8`}6H0GoYlhu%t+KDcbqFU<4rc`|rSA7U3d_+V*yh8^XPl9Gdr_gbDO!U?%a3u= z07AS5c11zRQRX#y9IYxr%pB5c`(nY-u{`jx$JDeX^9^{`IqYhJc>q09b{rd=0V$;y z!G}j-16i9kf=J07(!J19@&T><0VTqEyAkP#&$hGHq{kcw;5glHHd|REfWdO@vfxHLCkWCd0xf&+#yP1_eP$=-7F9 z+FNJpd9Hphfm=`HTJSb6+w)}dGf~OcO*Gb6JHsi4#=T-^_vcV>_%I{EZutd{8V75- zsoPJ+`agHbWM=P|whgG`$KA>Jf?-&?+n^#S4J{C&6t%LS7e-PEM$7e?yo27As%LIu zLk+go`_KbFxt9OI7*<~}op*hl@wQ>+)Ybnpo-zC&M7NuIIbqA^WiD&q*bg^WEW)b1 zUjD}Fii*|FLV=|DN!$5^dIi`qoT#{p?Rbu**`-2KFjmguwl9Z^L|NXT9A0n_m_06K zNxtJ1DovCYo>AfUsdJJ!k5nD-!MMbZ-KO*p^uO#12Cd_yJx!kCdqEOIvwBYckcB2S zxmrzk`fLR)lT5bI0DIKY?H)>2so#4a9Si45Pey9+)MZIiHPXmom9z{hCe9O;E@a@A zJhwWC=OMB0v`i6mMGwlTC%(h#s9}S6Z=VusRR&!@O!fnq(nZa-VZWU#r=BF?b|961 z%8`CXS%Eena#?9 zro~dN6iu{@Rdidu0-}p*mw=|UOHHJD+!2njDW&>k!`}&(bT2>{eiSU^KO|UyZtEw( z`ok`TzYwfHEQx@M`B||33eoUi1dB|MEj{5)_GS()gz6ZY^#l5{06nsI2`*snG4e%+eYP|=stErL+DcDPb zL`7yN&QAg(&sF*P2Ng2O)I70G3Gu^dYg9sfJ*jfuEG`A*)qCzJ%Cu|kz0^7#Pi~X1 zP-$-5N=((WrN7h#gPgy59S5|d^Q=UsX6+^NY+fTx;+itiEnI8!q><#qK^nvT^AfFE zVt9)TSR>w9*vq4r-p{EElYA{3bxK)ao*@*yJ~#ro4w3+?5xhG>hV$R(Tnr48>DS09 zoy-#*%WzGZC+S?Yw2%MLBZVR>yCsNm)Cb4lDV#O)25t{xe8b)DP7Olue!HBf&@V}y zN;#6x)WcrzNMhcv?Nb^fa`vV;e~CZCGVLCJmAQ4OI=MP+7Az?$ z1>`Xc%hb(F)jAMMX#q=9+qa^mDH8^c;KJ(Or8Ti$C$mD@&1xx7|4xf1PE?YWbG4w6mrS+9rFx^$`0O#;*fDWBE?GU7* zmVI7CK(HM_r-pv33U5!QK^d27O@pnau zpS)@%A=%%TGaOdenk$w?e~NkbJQ^|349XrUSuf=J?idz$HW~OZX2{b7D-m8IKkJ$B za0Wh@louOIfK19Nh3WP#SwK%?i@nHQEUVG>ZaR_x@d ziB+Q;wsnd$?v7d1ESvw^_i_)^>sq4(4b?sf{ue~3{&s>1PsRF6E}qVRjoF_@SQdb? zJ{^GcUkH}Trpzbb)ZzC_)hpq3Tu7pJ&}Jm$g9&f^*y3nS;x*&w(uO+pw?Ze#52F&!~*2*)ILf zSBV^uK~ibik0tVzg+RKFZ^F#V?#)NsDk(Mh1w98P;=M!SpF)}xe4*-CEJK5-A5U$9 z8sK2m#>Je1m7qw;Nq~?K3Fum#A$mSta(O0>AZAJ5@5w$JtR3DZuR%2liED_p ztA=3wrRnsX$6aCuR`lgFf^NC07_xB;6WjZKcp@0%OY@96ZM;qzutP!ilNWQK*QJ{X z_jeo5biGfPDbJ5+j4FJ$UZ2d*I57Beqw;DXvChs&a?>Y61nOv~h>3oV1xX6%)Pr~`5auY_09+C7vvkE_C+u)}8g#D~QGf@DI-E}3$$J5N``or znJX{IaUTTbO-^enTok!D%z(hYm|F?;{-`Cr6z^I$EFNo*qFq8&{n}?7^g9-Kpk3i!Y%m7~SD~eA~+yU3C_m^+jw=yRB zdN^nsmYIsA&LytWYhT>OHU8jW|LDieN?7?~A_ALJ%W_&rni?d7xoGe6Hb}BHQU+zq z^8`c}&1x2M;wbR)_g)7}pcC!YlaZvkP?-<$`s`P`90PS(#pnTLC5lF!@CkV^Ll0DW zIJu#yLl`gZpjBXy>XFKWI`|Z=v$}jrR%VJXa)ZRuU`b>5G05v7LD#|Y-0%YE z`_nPUj6EDQbKoneg%_sxzc}|8Ibv;sROr9d1Q)XIesCkr0h%JfgFsdr7gm;IxP2n* zH~RfdUL+16Q$Of8+CRm&@l?BKzJSo90+LaZndClR zP4T5A^vCW8ikShx(>vev`&jqRKwPPaic@$NP2Po??F4`|l zxrp%Wh1)3c2U$J&q9yf`YE%{a2@FT#f8Pmk$8T#)KhD7c=|AcB-yS93`3@HZiT0%j z<~GO7L29zNwSWsdbBCQ4W(-2fb1iEiV73adCd;{^e9U6hk<$p;x)yXdQ|MV`-8?G! zvZbM99Ps)8RVRls`NWQ5^>T^#?Fl)Sb@$2g2YrNmg?$EQN(G4dpg8Z$M+jxqV$lhO zVTyBa9x;T(d@`_glvJxP97nryP_53BeSXWgWIMo&Kk$wIhxi8E_9y-R!=r~^==Y^lhJJOFi`)%4 zK^#dHEw!_Dc~9`dxR*ta=Nr$8znd-2x}00u&aMQ1VAV#xF8j8`@q=vX)N9FSqE|o+lwbCGK`K$j z2Uhv7hNvp!fJZI_fG3x!DLrf-$J*sq9}BX-mQ+VL)6?;r7GcW5a^&XP~0G&p&8EY3MC% z35D0EW>WyoEfOi*9ec@J>SRW=vX7x(=LHdOuo769dqnttv7twpkFjAW3~nFn z=o<86tbrkL5gFUZd%PBzM=M@zpV-+PwP;Af$GM&Hw) zf30x-wcGG_`Y-sVk?o#-E4lC$wB$-a=gD2YBF(n$f`D|_i~?&Zp?BN;K62xhq2Uux z#JXP|H6qB+-**Ds{96z4V-A4ypZNZJzvDaKZ}*o#jL5b{Yoqx3;m`0^D!*KAAiW5F zoHne%m?J&Ssqn0eN|Wpf$iQv8_^n$a7*8GZq`9bEh2 zY5|Ga{%UB^B01FKxr$MtO2Vl9V|IrfyAe-NyL!$;RaN1x?x-=1N4nv=RFK=(tB=CY zP#PV{{>rb+fj)ncE%hW}6O=cqERcrI1__vyN*^V2h{b-hEh3VCd3sS#WK8dPFChzS zQ1)Wep|)XTb=|)MefQuilNeIAbN&|*l;)XSi+UHXVku1(EfVF8)PWNSD(|{>iqO}m zK9f!eZG(=Fp=OyC3ei>*G|2E>{I5QLjCz2X!QsM_G)m>VR9bMH5D-aUOy`@L>r#)C zqiD0+fT4s@VQQGF+uok3pcQt(C96hD$pd7FxpWGg>tCH{S zTPJM@71fl2k4AaxZb;$7?h6mO%!Ou?5=HLCfeIsd4qxV`lK2n@dCY%lw=qP9G|u#7 zpWpJ0-vBV<4}4?(A-?}(OZ^Xz9)7`hMTtxLd;=SCvg9W!s5J#&)5)f(CmUO0uJWhC zBnTFH=i!&M`pkIx?}!l^Px~l*srbe6Yv8^xb!t2^g=2I8><^#b#;K=W{xEkrk3bHN)PlGN*N|?zZ1Z2-lD*_E;OA z&9V1mcWm+?I}x5eN9VtCTO7yBg7clG%aJ!lFomvBLj^k>P7v)>qY#92FEeRK8&J0s z(Oh8-!g79%(Q)SU3PlW3KpmHfzIgG|{B)GmH6|N|zP4jj1L#=~QVz)^=1VA4&30FW zexVHxIKi>Z&l@(3(ndP`R})=qy-tC984K5bM3NdpE4?JPKk-f zIY#>&5L0eDn>@Lz7T@<9$V{$FX2-H!Q*m>mGnJ1eZSRtJ#$!TK>|<~WW^89fJG}He zL$0LS$sNq?;%)6^&XB4EbEpfWkOZ;;jZIFs&TQ$4Uy^yRZu`dTK**=LGM|&%a~Qu# z)4KUMk%qd-uPL>!N1@YUMqB^np|+g{YAoXaeN z^pe#Z+EtH9@K3j+AN_S?CSuG~Rm(UcML1k_=tJzbW!M~N@&@4Qk*};hJLB8&cHVFZvz%Y2`ymtuk5mFWwU0BQQ3bl5u*j2V? z&FWG2z5F|kKzALN{t%ji!Dkf9kbMM3qLcn~)0rfRD4*^1_H7NSTmp~<^Kn#RSogx= zD=^4%%9T1+!a)d}mR}c-kP)N0ja^OawjV&DHOmKO-$}=3r8BGau;^o>#2yij86K58 z)txQltGwHoQU=ZzfpNEv<%y)N)P#Dr9%&k&3mAF)C*J_8eSfbKd8iK*;PVsTMu2m$ ztOE8Q;u}E0PkjI3(eW?%?tI%Sa5bte@th5m6s$~(5xx> zXg7j#@=o&Qa23Qe4Tfoo;;6C8!kr5xYn#{tv;&T4*~xWRkYO&uT_1r4RJ#43A*aLA zsgW=)gSlW2dys4jtRh0(zf8!~t>1T!~4=mLy6Tt|20qcW&C z?UmSp-#}jk`2)EecfEj~*2-0B*Fn0tFL)DE>_@}F7Q_oM4}Y&~hNPpMc|7T;W=HUd zDEthdtzf!S^5b@&{DRPx_7euXmn*=iB`(eXPZ< zmm<4UNjWF|argMFktCvGSq&i20RvdM~SM8}wjGqW}h zsLSmij{`ONu-cterhDyMMPCmr=fxu6&%qio@2Rak(!$U(b<7>0eeDQXv~|Fz1v89E zLv2!c?LI3KVxoyq0NTh^NiQ?a3(QE>HCJEIfpq#XfsHGk|5~!nfpQRmQVrxSdzjA# zzQMZu`;xc46`&cn+iROF+xzx>wDETz8tnQ8cD9nj1V|$USJoJ>*bKoO zab?(Z$WUqsKRl-(^JdI!YiKvP>H`wIC>0POzNDg5+){r#hUsFgJJm5KG9eQu?p~qw z94eWGKz%|Wn{4P2!yxCyHrKlpRu!B~Ib!uHRBMvkV~0C4~3XQ;?Sg8}onANj`pr}$n6KYJ-*?nH-gE$1IsVmW7$ z3LoYh_wh!f#K|32Qp0R$7?K{knqObf-KGYd0|~I5{!Lx~ngc(n>u;h&{b8qBK*jvT zH>QuV<5SYFU0OMa2;W%P>6&03;hsDsZ)9&dy7>q_c*(fJF|hIlWsp!^?vG-}U$gRJ zB!0qm8LXuGnB>MtXh=%`ahmy+(4+iCBE;28y)4%m$bBc33U$+*S8s|0mck)znBNAN zWPYldKnD@n&cN2YGPv*sDPVhSygH7sAsb>o!%@>Yz!Bdg$1RRxB9$^skqWYbxz`*^ zu|i@-x6Y?Maivq{$`xaAnV;A%zX^&meCK{BxfM}=-eiAZ(YQ2wUxZ-1f|Lr^#Al9^ z#3TT=WzdKq0ylgQv3prB-RLjx=Xz7c{u0R}EVm^_c2dou9$Hr#I4%QzKP{y??;2WM zygwYBVnCoEli73WL(c-pUDHAmYg5B$vqGZP3ZwK6Q|+a$Wdqv=;`l+;g7)&_ z05DVt=H2CqV@}~gaA+T;9M$Y097Pn%!USVYJlS+}M6t_DI8j-b4XRy*bvpj7;NEG^ z6|MU>Nl_vnNn1)E5W}qpqxK%CA)%oz&1UWpw!rJFpN#c?-jK#V=7fZp#+rQ3s^g## z6ql@bN%bL78X?m0y<0+Xv+uCaE&uV@xk`yJYDFb#6@5^MG9(7vT9?w$C=iVO$F^J6 z1*x=PY2XrwLZpf{2eGf`U%XVNsz$@Y6dMV&BF;{z?@OX#OO}%652~vQKUEy=nMIPW zbG#9soyu@z#tARU2T=%fxwk>Eim{g!@(qaGf}=axW;Sp#8LIKxZF22b*tbIU56nn= z^W>C#=!&k}w`M0?0`Y}&`)0&bW%{lBWG4jnc7=lmx@0iPoWW`F_(6Wg^E!}T;p6A) zlqoWrx^7KrFU47I2aq{e4ks7gTBIHKwb`iy$au~zci0wCeVMQe&E~n>s-tW1o>i0O zB%2ZhzGn#<#_s@ICtHV)N*n8LuQf*=iYK6G~-5Gd$7suVV^ z?pLoA7d}pfXL_9JQC4^x6m!-BHWL3`uzusx`cM1d{~cucdxuQ!JYn=EJP4$uZy^E& zG$dARrYYk*xK{pS?xl`qS1cca&LbDF&%IUImyZJrRip(S&bBM*r7(QNGXB-lr~OND zd90DTxg$c47X&Lul?K*oq(&1y=`#_E>j^goO(uinugBRJ7^lB>1t=2F!=OxknQwfB z1sm>wFZmp8&UnfF#&X>}#Z-#iH{#UP+9(#9#_EGf^a)kHAYUEzODnb^c@^nB7F)U@h-M{vKPp&@OoAQ5(Btr~MV6(9 z2962MLMy`!Z(V$0ccg;QQNl+K<}}zZh`38Z=|vtp;OD@Ak$n zEl-6LSj%rM=r3lUy8Qof!IuQC zjQr_;zVn?nbk~j~Mk{wD(y3bt+Jm?j_n}DqO(k^uxY`ybiWBO`5y;Qd-3Qn-AlTjMi}`vWR@j`Hd~ga4p*sNy0kou5 zPCPl9Nj&A*ZEI2zTpPRd>NNF@%^Bd299-CxxGnrsyq-7-);kemw>n|)tO}W+E)-LU zV(*zqEh{=>zV=9I;m z!1?2L@h|xP!w)?GD(0^ed<*+dfbdxLG)n|94h-~j*mqE5>Iev!TyPjP7+9#wf5m+d zyQO*x`+isN3;Y!J9URNcuR<3P_MI6~TvSC=PE<+!1)YkLpor)^@B!FU6!^-yrwiA> z+(@j9?do#QJpvlk39 z`obLlkGZ!1>!REGzv=Go?(S~s?w0QEZjh4h1_?o=1f{zhq>+{`=?3}pyU)=_&+$2) z>w3K3>zzv(nBOq7+3Pdky=Lt-Yb=%2-C3OEjhr~t+0|^#RT!mByu2*EjO;v&WE9o7 z#Ki1`4LKyGS!8VFMGd*_)vP4A#l$^@Rh$jARmB+9v`me(Sh*cMolISo)U7O-)kU48 zrI|!5Smcz|xkXjj8I??&JQT!bg_)mPYA>S3!78VrZs_F9sAy`Zpu*(p$ST8ZXUNRx z=G%_tbH=_D{X`FWvj}fw7IHshx|I$>Ur~O^*9W0Ii`UNHiCI%K} zc1}(<_NVR6E`~0y&QHe?R#a40P!|&gY%_IovotnU^>hHVPTmedJ_s1K6gl zASh{2q==X2pHoWZMgNe9k!^07} zm!E^>Jeqw^7sT!@_^P!gZG#GXORl42nqORBpOshSWHr?0;bd*4CITXW)Is*3Y_#I} ziym&ScwT=?KKBHKr;8*&GOa8jE4;;Km%OPYC^EXIn_<^WgEo=GQ8bi8$g| zN0VpP61>Pa?&zjz^Ug{>b`f<>*;^12C9N$zuC#-FkkXa#~6WYzl?US|3K$f@cD<<%*xz{JZ zKXv}_YJsF*CSZBku{Jaf(R~q`@vsF|Q@pm#b>{vgTv!77A}bkUMrih^P;c(F(f0Ck zoVB0>j~Yulj6YGt5gBkWq2%j9fjlQ`VcUiZ_{0d7meu}-+O#8q!{+*vO)l+gwhbqR z%8xT^FB4h<_Rh|ptK`w#h(&_-%#o<>$2aAcL7SMAs{ohv&#OKu)$c}t<>PNT@ZUTf z!oO<-P&%6BQkt_Dq`2aGAjJ}0u}jwjAFAz-wG#|#46ahJ#2TGbHt-krzDRT^+JhJB z|E9lyzvAgX;GrFB!1`$c=<+W{fIsX*eLK_l0Q)~5ar*Azm_D-|yxM)IG6vJ&wFnEu zgeHq^Nrlb~qEk*kJ7&W8p-Lr{Lh921iaVhe3YQDLgR96x@DQ`xQTAGzcuUDP)B;<=FvJ(CdS z=)@B1I7CGAbru)zB*DiBQg+0@AF~bGtifPk@1GOnVnTGThv9Rvhh?-D-|$P7zBuE4 z?%J5!3IDPOE7za74R2=2)o81+F)PqvBZu8a^G9i&9tvoyGXV9DdH8 zpUWYo96f*7x@=&8vhz_@U@wF)&DS$e1%04Hki96V0(qO^P4U2FwADf&tJ^yl{mjn- zve(L$IpWV1eZI`76pfylu`lgDro9fi<2WbX+0 zYGz398=$PbfK6e_TM6zH$1CD{)4}_i6+|u%obt+yh|7A40}Ne~9mYODX-s2^GKKn`OXx;p2|W%tvZjiUUa@jv;45 zad5VZmAr=<+N?L|$g)!n&`lKS<+nRtJXBP7fUD)34+ZE1z!hZo*Ej#O{U;CSp9$vQ z`S$#JzC6g3xF$TT&d;}D?I;U>81c3=cOp(Nel6pX0q zjAIKAHutH4&N-?D0*=~oZTe+|T@{RFj)7zwtvcK%wYOt)r6@DX1Yk(>90*!_DTUE_ z8UYx3_A7zBhJBuKdYshi=oPU3s@OFji7cf-de`O)*eh15mx-*PfS)r|O>j!soG;4Y z1&YroX0#v{cNu}N&bToN;MG{BXq<(}KYLk?2IfeaexOo+j2qF7Mvm zpDN*$7(L{8CblOe!uZ0#M+YSL)*sq=n=ze9%6Y?{5F;5Ke z3hq{8!Bc&Y>lj^^CJ>!4GFb^OPYC2#Kh*F-n38rxy6$$^sqLZ6N8Zu^Za7+fnM14X zQ7P|LC){zHAf;QE#$f~*^SI3tmlxcBk#a{JwFXE$`;l+rzl(3O5=mb1u(P-f(x7k7iXx}%J zuub$3Rf5vnrwGRP#+udVY8~PE1Tj?LvCpj%ppYy2TJ$D zGTD*t2^-7T4+Hvi0?xu+L_NA)w}=F9ozatPZ|6Rp!UQH}y%$Ozo;QmbR3t%I<=bOg z%9Tn1cfFR<=bND&=88;L+AtL(akIjJCr6N4%J1(cEH}Dz$)9F^fUlH~@;gguurE|l zb%=u**2w)j#S*9$+*Y7Oh7jNE_?83SHK&tXrUSEi_Q@8%(QoRgt<|T!e&n0v5Ah8+ z>`zmEe^_0A!S~-Cfgv@1CiV1JedqfbOp5yzIeg|!v&>Cieemr*&UB}CS^}XXS#CmsD-teG%!tkH zz9bOoP3`t4TyX0y7j*eh)pq&r@L1m_$hRwdYYc|4i;1-(R9iS%G--FLXGH3Yz;UG* zCCe5pbm#1p``hgv2ABK3GZhQtkDN?(Y{!K4Weu)-NOH7|O~*%xVT*~BMYU>&JNu+a zQjZp~(Zhy6n5XKYwH4-u3WwllDa`8^1dL3ew1SJ!uPoMaa&&x}6 zvn7P6k3Z7Ay+cXpif@dVkRPN%TDeIDSJLTmk~}WrVP`YmNURigB13kC1c3XWr~GJg zPU8R|{@|aG{vo~rZ2!dfA5Mt-1>b)-DI3r+KlvyBtZM$9@6uAi0qOnntxwL7Tvc}( zZ>pNl+I81j?=n@3(FTAZ*DDAY*JaT3+CD|TvQFr91P!{Uq@0PpMeT}b;;Z3RtTwq z2bX&&zSwPscYg;XtE!T8KUQ&*bvOK>pFIGd&0?0`TZUj|1my0sGwSr4k!K|cjWA%e z{*o)KcroMr2#j#!};c@9X21bMbXXtl0;h`92o zt@R*yjxdTk1q-r=sAZaY`o(*U>aDiax7Exaw+7IXUthldQWBUa;Aga#{}#P`ZeP-S z%cpAI8U-^L0PcV0oA%XUAi#`2@J;r2@hy&>lCPySDZgEvqT6nR8Gthjo3V3_F8{SA z=Q{G;=xLSWv3#A!xPFcOzET%L*Q;-IJ*@%P3BWu4O$7b#jxZ8=5Xd|=Pi) zK&!6$+ST8e5P=rE}o)Vm=}r=XibKDxmALG7+HOr&$eHA=hTz%+yxS&I33CeO;vRR zvx)~r+2WIyThUegU{<1(9*9`tWzJ3R&iOULf!X~2WMSq|93~w@#Y2cadM*u2_1W7& zBx74oGF?-{3s^Efl`{ST_b+y3YnkJ1$R9tcL}|(nb0bTn9c#SU(t4|AcN4*e>4n1j zMP`iS^G2AdD@l8Kl#E4C1KN`b70dCx7<-p>B4hhWOF+1Y+n6Gtp}+Ld9I3cRlbZ<@pILd!D!qkPdfStu z(g0U~IYjt0vferz-+ZQkH~&^6yx`SY;sk*hyTYY)agy^L-zOxVcZo|J0i#*Zfi=M? zK0N}9TsCKN5o{;RNVqF~U_Z=aQV302eW3wMuybndL`=Vt{Dx?YB<D1n;=SfOdfMP7_K^aGZ3O9`axEF95-Ep z7IRG#6q*$M7B$4%L!ly@G0d2t9&H(k^uOVI>RJR)On8~+RFlYygR6m^I>D#|Xh#&7@0h@mE zPbeV)*+~DR-3} zI#@c<#3`N?c68s2B5j5}+|^Z#JU}B=$fHRT8%+tLTCT@d<8=Mj6;M-Fh0vMa67X+T2q8++yMR^GEKS; z!Gl7LCnx_gQ#vp2gUDF6G#RX0y7>t-4`jMDClegjE7$u+clgKos~r6Y_+Cjc{2Cxh zGdJ=;&M?%qaLj^-kOHGT`1BGyTKE!Gs-V=$O>4X=-@m_us{XOm!0QN&nkU7^Q=E?`uz%}-it$%qHCKBxIVNZQgrPX9J+a- z=QKGQyn(m_a}!3>ui<AQVI&=D`r>fGll8 zlHOv2*E7|%uG;Df4YI6gq=otJ*(#yS4FqqWQ9R`nr)?2RV$KQ;eVfnR;WX{U}KqBx3vCHg`l- z=#c>fzcMD@K-@XnvE4I2xAfOB32DO`Q!cEUAu+71q$5}Og6zU9AwbJfZ#-5X`eJ7q z!J@@aFNIS19t2-vc`97+;mg_ORre_3<8MKby|&=nAb@F?-oN-13<}mQPvMD^u64!z zjEEZ+;sBF;JM4OyIj5Mp7Svf&-!B>i+amzJtR+fBQAW_wZ zRmXc3FG5nboKne;nJC_ldXh&U9vx|f#w4p--*xgL98SDvq3>UiftW`@o8aE%x{ENgRA0ft#?#~q^%cnW z;KOO9fy41+_Cd5d>F((Q>6fo3M6QMgN_uMHHxKCB4APGt5TIjz(rl)+&- zyP$pJBlr+*nc;3#{%@=I3Qgz1@9hr{!s^H;vA1hzuIR4&W+_X)mX0k8g7a>kkyK|R zPVYZZR9=5|nj*>5`nUm7QV&!l7Di|o#_RRC9q0v9D-^9sfN)K2IeyWFbFtBf5hbI$ zq2gquq9>d2-o4s*N~W#WGV=33G&UsB)_$e={mtxB*sk9DVto zjiQabIiY+>>Z0z_G3&Ld+wNeCnH3#x?T4#&XnUnx>C%MaCY1vwoZPQ(=Y1UypjX3i zQrLt-VfXlUIBqdKR%00Gi&rNdMC1M1LTS(@~N6%*-LI= z=S3#&bHS#9;#EOe@hHQy@=zmQ!*edHz~zm#*o@b)$;MAD{KogU z2)hHI*-y*2?LYBNafJBvn9To@GHu^)DOV$*fNn`v{>tq{2BYxPCYW~Vfj-n zR*w4Ql*?qPD(Z>6?m5lESBkK?v(9xa)<{Gz54Iz?3ps{KixQ8redyrA_u-p+zkc!( zCwt*FsTFyYHR5SZxMc4eHmWjVe9U-%aI9;zW@$IXJ#pgDIgn#sYFNk<*$J{uxdS<$34MuUE0Dkve zEc~IE;jFgvi+Jw`Q~vNr{y5#D-W)di0>e}8u}xCf@anxWZHpQr*dyTfi=;y`+VfyX za#~@#zES!YSF1UzqzBu`ODHB62B@yvL{1;H!=n7CSOnRR2~;fAH7eO~In&4wJd(C@ zp{yGT6*__^E!Uc}{F&!?zy~}%Hy$^*B>J$@j~R@RYlcSYE>mzUUbrx~g<(au#OsM> zFIZ}0_oh5r6ylMec{`YTN5;9%p}atTy-uG&K*kyk?TSScqZctj#a!10^JFxpYr+~< zs78*p1U{tIIZ!7WF8^8F$$*eN%rxvIG5o__Uu$R8Ev?ZzJMG!(z|9rFJD>pQ8wLQr zc}lp5dk+P{tvW)XxuFYTv|KFL+q#Ct)WkpbA&EFa}J5_Qz(A{rYV*smGMdHM@|EyGXEUj&(E<@Bm*$f$ znc|`Vg@w7W43S9j?avzuS2LDWMr(ypwW6^#kS)?mo_k7J;wavied;v5nKk+rTHzUzh~Fm|Jf11hSO1gDDdm zj^wq0AEo8)u#;RrmoZwEdqE}1#kI1FI%t^BBQ`$K;|`q_i(9BL?@<0^%>T1ssfu~Q zDS|-c(k12&>DRj_%v3NC3*3eD*zu`B1}=VuWr0xM=&?F}ZdjqlT!L}~-Faf~Z7c?P z!5uh42ZO#+?@5nW3Nfe7gQlAlV#EwY8lfUZ;fFQ z%B>c|R2e!nOtxHF>_kaW5Lv;@>I77(+ycbC|6()%?OCDw0{3E>2q4UL%4>3ZWn8zQ}HMH`RYRWqhrh3_lu8Z|<9to+7kZ{p~AXF2k6(LZ_V1 zSHWnjxg-6>M-r``_dQa2!Ac|40Al z7lH+GQjX?~l-KF^vHrE|XS72{C6+R}GQn6vE)$bp4Gt?HR|yltk7?fR zvE@Tz28kfos%T)jZF7s8k?`b8Q9))B4 z$9Q_eB^)O7=Zf$N_`zF!Hb5J?)#T8eH$4S=aaZTp_=cQc2(A>!B3A|0Uw&R*TWCR# z%52zr$(4hI9;daxTCQS`g>k`T8 zoW(FBYmAL3`isEoE6q={(g=lu84=Ibpe-`b-wyII*K;HJDajrrajravOU3%t zdK9s&PyFz09#MO$t4EiJV#QIm$M6!Z|J(Qzb#=%2vT0dN*%ULj#C=*A_6Jb`L>z|& zgyJU3;jNPR6pm>3M1yP3OAjo46oaTO)Po}~nnKf(pl;EmMT%gO{z5xXX#>%Mz!V+U z++@p!w6MtQ7gURbZ-~e`ql;DT?+-qIeFYaY8u2;7xGF4<**(!wv$VdbyRYgUT3m>p za*WLjL|#R7=huyf{L;NMO~%mPBVw_;{q%vsaWz&K+sB^sE*w$4Y&2_$WTM8p&ZVy* z-6rq7nnL~GfsWO}jcA$)U0ATmtH2@8m3pT8dK(YH^nKFNXzh+ErIoaEXmy$ zg10Eam9QpxL-~cV#(%7@8)6N)nxw7y-QEyBze1%T5aCp9IHU;8A{;29Ht+IdB!+U0 z>yqD13~4uQ^9YL&LYl1GX4v}V#0+}09=5J`%}H zTL>KQ&Y4tX?oCO}#WLfs4kq)>d|Rc^3;|$NL$ZCAM5!6>O~|G~A3SmT-}fHIt3ZTU zk_;1>9Xz>hjRPkQ>yXMN8ZPJ=<0pBg7&AU6ZH(zQUzA;gM22Xr;TE+u-cBL%o8ucK zbhQt>O3gk$)}h3iTB&WrRn+1W9P`$bg-pqxJ)yw4p(lWH=`C`{qm%HiGi~+1$GPp9 zez-Ym#&2b+_U>Agn%LqYYV172?4L?@e!T9P3k4a$X9k3k$D+v7 zCzJV$ZhY)SC;ZZ5>k8sC)8Jnp!Ibd1-wfAILvi1GzHln!Q@Z%tu&DfEB5?>Yx?HEh zbx-<;fU)kQ2M)q^&zH{p$`z80N_d3StjP$S3FCn>!A>!h+9e=ov9)DyW5E+s}Z26U>CH4VP_FtlKB`qRB&r+wcS3=NpdCkG-*0edo*vmY0KN;5wve~11D;X z)|uw=3W&0%lEgvN^1?6*LVcHpAGNa7&c};xS7l5=P8`7H3w2?hExGpF`Y0}u0l@vw z4kt626)nJwKk&`)ckvC2da}@G!JfYGVV5(<%>UXVTw@CiM^Nw6nkRpDy!S=T?3a@I zc=>jj?MOLh$P7S?<}bRwt$&{~@`sbc0Uh%b-~X&B`<-ui4a`CCFM;mVOWk#z`bdn5 ziRV!aE;Cbs=o%F=loi2RgE5w=1;j<$BzE?|=cL3~77SKP5@_UU40jCp%zF~~$3{K@ zL7!dVuG+A7w{6%zypfZ)26k-yTw&+HtRd(zfO(jSD$r?mh6RPBn=&nuVa|JqCLlmd zvvxZ1Vd`YAMuDElrC+DB)z|ANSraLvu7@Z*hI~I`Tc) zzD16LnmQcCYFTtEyhiY1lGxr!iOy)s3{;RGM3#H~y4vwktesZeD;$~SnC9842l4FU zRuDKOIBZ>pWu?`~^~XME)o@E5ttl&a^gJ|kwTtik(;)0O;DHj;V!NIk1wKJ#?F#%eWw?%-U09Nbk zP_BXq6yR3rw|qOc1I+jX-;95V?|+K`c@r;C#NJvt0@HH#F|d#TjT(SU@ueNT*HFt^ zoDG*iAfLF}yKd4k@^|j92m_P(v5Nb*fHGjh53m5J`-OgEMKyTy@h+%$Z);?ybC@PS z`v@0kHnZ3C=1Ljt!JykDWNVb(P(`UrqNfnCDyZYfDVcA#Y(Jji+nK&Ua`Vp#tnYlw z@u@FNUvov|vdp`{0dp{X-F36(?#!zzzl{^Bm|nN?dKcyU+{?yHmG9VO>u~JKo@yt8 zE9oFnZHr#aJ;#d#bT`{NfI&jEI<|q;ZK}VP-&h-rw{WM?-(K6)k0jXjf756vT@Jk6CkN zty6GG!A&T82JVC@*gpi6de3Xx5yM|4%!Rktuw=Po!6 zD5G0zP`h9B{Mv{(6mO+=vTm<;(+lTYHJ9#)LG#x0PReX60Sp5Wds_ZlKk4^xQbs=F zU|TezUL;M#Y+n-Qb0RoLQMHejUZ=Hdo}SH&p?~$ySwzKZ36-M=7|?83Ab9w8-S!T8gWgSoE8Zufi7uBL4V{OT9v7?ap)XOA8qQkSI1v+ldSH)b3x z9N$E8cD*%xb#a)1m%sxuU>6eTL&a`b-xIT*+oMibwRHC2+(?AvfT6)*yC1IdYOh4h z$LpLN@uj#z$dW2#gHl9`HU-UTjq}8TX&KdHL9dSvCf9-*202aplTrRp0fV0(5(k>0 znFNNhgu#V@gS!0d8S$;X++z?FXeb~gAgqq%U|=jD6o|im0K*E1TWOd6>v?e?9uAm) z>=OI61{f)za0irx4F90vuUCOQweZ&q(UI^!p7c#Dq4ld9>{E$+ajFvB60(N_ zQDQcBJBL$AN4(yg9;LLcyLD?=Zc&}+Qy?EW;Qbz3FH9K!oshz$8Ig6y75A+3fwNHw zSqO#Z8wl@5{r6mj%Sh}z_zWcP(fYjRLADEVs?G?9S17x38zoh)tb6?q4Roay8i~&a z6RyLsGApM!TMbYTUJRpARom-Zm-H#KP+3r;2!%yL-s#eNa=%xv73EQ9VYmefYE0Sr zlxb@z9-og&1WeoZ0nx+@SRrEp|Mf?vGDb+f5*p#r*S4#<$3??_c+C;(t?+?i;V#Mt zu@^m-uruP|to!+0Mu|bkRG56K0YM%5Z{%@yN)1NAk`at`XC^R*$slP)F!D7u464cw z9L(Zi3VO>7mUzMAjX94`n?5(GQG*cWxKhRJTBoQD+xD}n zXy$g%;_E==^W%Dt9H(dEi;*jcA+y}TRv&Rj(xSS$kYxGZcWr{DaPP-^scyM_N_3$K zoU_nMxnp}*$5}zkz2$yY{o$ih{NB54gdO#zxm96*;F*CA%{Ttun~nzdk5j2qVph>x zPZ0c8mOcM9laKlDl4bvs1y)*t>_E+8w%es44B%}W54h&+p=_GcS9s2$4QP{;K_v>? zRvB+a%=ZqJ*Z`$l|5cWM6UY6-kz&9#^j8bM#c^rvOpKnkeRn(5cv)H37gfQj%4ZtY z)*|Bv&-hN@{s0pni*zXPU(x@qj$TMx{%c(ZoJ$2K;w&4{B7vDVBr7*=jbu&`Q z-6xsj;?W_C+Wk!U@snsV6+Ip7$XQm?YQD3es3VN>;R}BoVIN76M2f`v0G#r0E$dG+ z_|OI1vAV@5;GmkF3#36d$hV`(OAwyaZ;6zzQ?=E#)!n&jyZ4Cku6w@_-~O_@oA)fE(?9 zt7U>glL!G*0uF=zeVhu|4xa=4pJFDE|EZYC(f48|n?DsZVSlD(BqJ&(L+hxlsIBOr zZoz3M>dqwQY{|{+Zm+KFE+fzB$f~3)EGuKnM$0Bm%Wdf(uH?!h;pxF9Vxpoh=gFyL zD#otLr73A^1b`t z#OS5K#UgB{O)I0uX``g#%B3yEqUGvfu4d+_rKD~l?d9ZdAt$TuqAJYp!enZtAmgk+ zYb_Au$rWtp|gmV zya|glm!z1aq$C@wim9QZlP5D5r-z{^qm`MOr-q>;yQPhWvzmo7ld7w@xU(>mshEVN zn0U-83AP4cR5Eq*>WbyhK_4e_70got2%LjqRzb$p7cXOjLwr#r~z5iHe${ zn6kdGsGOAie_zl9o(xlq$?|1SX5ptgin*CViyKRDo9;xJ`kTkm^9~K2{Ptt%G)jWy z4BPsi4?>XHlP`TTTdAL3lUwJ-gv3P%4RCXWF&!Q>DBo2OPcB^QFnk~!S5~+S((mMQ zJ7$x@p#_&AQhvr%O+|0c^|t1N`01lAxqh>RX|-VSUQzZ#wIMk&^9go`6y z3bgtTFf4{?nF$(Z+SXg7-cO}(#erHyE^%x0##`)`%bkyaX(BSdWYX((H>+iS?H>kv z+j<*OGr75_vWcc*u^631lcE<`ieMl$4R)I8C3a`E$H#KxuYox;u%iK_DS?83D-f*Q z?M!VaG5&5od*qpLv?MwQq7w*&c{rMw3AS6@v#g52$A@YR%G0Yhk%#lIZ{DYoEc zD0^-&wvgn$$BaCQDR8uw4f;_2@5$9uzH3c4A=D1MPT#IJU=ONLLlCkE_YZx4{wE z6+1j&MZK+(?xP*n{v~k2A~10qc)i{PxRYJcqaRBW0y+VZ@bMp`hyN&#@xSDc zIoTq@PBN`azX>)G9tWlifLJ>lIkb;0f1bD6t}~uXLlMaf&D(i7UkJNzcd(CC*1buN z)d#%DO^?|*EZgiBV4_~0cj-|*BUVm~-CNYeqdVe!DsP3W0=~#Wcj8nze@X2UTqy~C z5(XWu+%;6$46-&m6Dx`yJi6p^g1Q#cnV&dsizejv7RNM46KP*-ytoz&9s+?WT-NbZ?M(!g3)3 z5_764Y|%#Na!7*YEnag>AgZ@JCtwS3u}Ln}nbPodC$YT}6SN}ReCRz#>r#q6V%g)N zVxAEc2!yMtaiL+7fP+z0TaGtZ1(0sq{;f1g?&vHq_W{wryZ5SnEJC3E6?`K?Vpa>O%!^t{K8fYF=#V+e}p| z-hEV-04KfUo|Xm)zo;U0{^6C~Kwx|@>QJ5;A=wZ%L7=!OY9QU`^D4n1D>vS~+YxuS zETBpi^mkCZ&GAR~5ygHN?si;iH@d*i*=En;5WNR?v!A`CcEIF;8#peaZx^?}wl0nb z>m_)gm7%g?BvpnQiL(@zvXDbiF0YR~14d}72i>)*eeYu_Szm1{J?k*!u*R#C(96=x zW8BX>{(;>j>7~h07ZoZHfp2Usow6Miy&QD#sI6Y2Ru%Fvj_u>B2%OtV*#;!$j)1v8 z$f_H9Tv*5ZK!>+oeAvA+cXcQ!E*2f}Nf5Q1sqnkaSn0TuG9`_ zD&4JEOtV--Z5S)$7TgIKPtE!kclb8l;RI;w(*oH3ivd}gI!1j(LFf5%hC3U?l-Zb9 zQ2_*Cuo)2xM-15ZI$6?4t*j*%P1vGuoJx^P&0Yh<>Dw9pTK`V47{ydBo_^>1fJ|LP z(U3U!hHDpioiP9T4Lfq6W?2iMbE;6{j0@A00@1Nb{HHDsNH9>|! z%q4(A+_uAXac!XaIUnByYNhJyRk&_>fPd_wAUER9@Qh@sV)uG331&Q?z>hZ>0FR!v z0baNZjyGM_1@91DbtDide4M58xOSG+mww_%v2|#6uNZ>DEwAn$?{xkAGH3RvHuPDe3@pnw$1}z$ z#YGbijr%B%sNT}x2NGdy<;fi8my@<*8aUYsQ?@LAYmjeZBEUN`5T+YoPR_BZJ!>+rSZ)I@sz(<^ zl%Y2B;m`OBpgQ+6UhQ@t6q56S5+Hes8pWHgh(T$v=v;bHa}itcb{?=OZaVt8eWJ#+ z_2nnL10mv&2_l2-aCokNPgAy7IXl#Fh`35h9O!Nkkt$c*YAlw5N^NrFnXqxsoMKOC z=)^0DEv_Ipazcw>9;?mj(V(BW=r`xz#kbKsk$#W$p+nA|u&&p(QNH$s+ej_m!28-4ar~VK6mjY?=AC3$K$>?{ ze69vF;vrAX`bHn%Isv$Y&VcpP0@(fwzR&mtqnEf?U#)i`r_8KO=MS9<=(^uwzqW04 zVqk&g7M=BENvO&xKiXHNeJ9>4itx`P0gm`>g!u6cfbBo={mhmjQ8jBcOh z#ln$7N)9|yHQdeUp3#o;#0KZij7A@*h9B9pe;`;)1%&fO z;^CBSPJb2mqEuo(|2A_{W@{M!TxTvRQD(7*0cC@s^aEmxe%rDxw3%4F#9V&qEl@*= zMsS`gJVMjf9!d}k&z4D+W^IaMDf*Q3XrcMzqJeN=;@u3k2KSNQ+Vy8Ecx|&-3pG)h zJv~%$E4tJRIr?Vy_C(?sI`z!cp=A|Nbx+qYi44PC%R7Y&Aq6j*j9Fw>M)2$x-dkc6 zQpPx9v8XF;pA1=~OMpNdE0N>iP@5|8T+wpksdG`=8HS ze&?G`;T=eu*cK;A&UL>Wv_7X~cCchY61e*Yq+~Z*(Rt9`c>p z-Sx;~WLxNMGYIp_$`Gg9i54Mmmt;KMlkn9fFc z9+f>uT)&mBbjkJ|fNyKl=-;HVBk6wPEFR|qYj0*o$A+VmbkBCCGcb*f0+Kf-d zq6K1?A++d)CLN4^&S)BoVn(Nnijr^W>U6r#bia$3WI>o&zJ>LpHQgTP+*)fP;)v8IEk`;OR>YzDv<|J=iDzbQFeb zo8LGe;E{%Pf1d_>Mez*N<%_R(X-@>cwW7Mapv1eNtY*D;M_AQFhvTNs@nv@>@AWCk zImPS`d@yT*p=&kyI~Sg6CT+}Xye4hK_f+8yUdwJ~!VjKtWJun`Lc4J7LlyzcLw_&$ zq9fpBe51lwe}!b!=E_Ww4#D2w5+h1FFd|QyDkL?gaZs6@F|iV}u~FDc+hrj<&h|~S znBNUop5IpzX|EDuG_Mt*sQ^-qYrb7Lj>;pl*cmuSa>&|dG_l>J#ToI5aYfBnRptD= zFDLTAAqs*0vzFFxaCuXf{R+$M@T`v%>G@-oo3I7ph7hNF-fjr9T13&6>%X^z|3JdG z;Jsj^86p|oGw5-L&avRmEYgd#pT9uP!V@4zrpV6~2KMd&RbtTde%~CtLi6Z_fG~aex_r z;G5^~;@jX;_X#DkN58r(Hh5!Wcc@Am1vd%K#xh11ytx*tlySnj$p8rDba_A}#j}iY z@V%#I{Y4j`eSqc3Kl#x+{srHEw|>)|b^-25zw;e%6^iSmL*JIC^2&Q3-=ps+cwHlo zHg(V~%8{mve5z&WMr4Pgs*CpCaY31p8-zh&i$&wksQ#-&rLO+k1mov+C)g!Sv=p*0 z(E0n|?$+j}{!yvDp|Jbe-6=4eXig{VL>_%;GPhNcOpcyPYJTvadyen>RmjkRcm1ql zo2^oJtmZ45zu1@KteqDkVP=e|(dD`GzM5FGoK?^987yQQ)pOvL=pWp9VX9NGXy?uD zD8dV$(7#H#V+$#Sety5Y@Ucl$Au;Dv3{83z>gaPr;E}VY=cYbY8OiJA1oIOaL#v8F z7L!Mq*rm4v5pkGjFfveT68)!5&9x>TJa~k-!;jhv^D5zez1gN8fM4*}Z#p~!4`5Sd zqaI>9F}D~pZ$yuL9xA%#_k#4fF!cg~O0VtbhzG{k7agAUA*Z8!c=Z^TAQgp-CSi}n42FNbT6PosYHd-@psGK$FM%f&;kkWa1g0tfd8uIk&XIdD{BiBcNEPS*6H zw_D~O7gmz3x3~o88&A#pSH6Grj(@@T-yJ{s=fe@-`8JYXxvAwW?BRi0JajjOv~&U* zV!EY7N*(^vXwBl6^5XRZ!U?)A#>UA7DY2X_W(3AGi2vC+??{Y8B+|4GYpu32^i3?S z!0AkDX`=^YL`lx45=Jrp@RRm41rPVo-l`6D@w8k|7jXLL&onQqO+ceR1>Wq6(BLrG zO|aLGm0MJWjVt@(gO)%_Zmgx#)=Pq}U%VCd_u3Uv$Qlt1pPng~0%ceV-JKuTA3!e`TT75X88#igf0ZS)| zXlKm-|A;&5s4lv-@6+8OEt1k99n#$`-3`(p9RiZl9n#$m(jwj6-AH%y^7GvK=zToL zXRXJ3z4OnoHuKxFXJ4QB?rYaI*`Ol5+3+M5_awO;wo1ory9D!++`t&v2rr{vmeyxJ zYqh~b_I%&(%0FXcq5VNMZ_N3@!5^UsQv*AICQ2Sj3F~?v=lxs45U9+Jy7npT+iqrv zhoz06*oR(sD-5!c#0mJ&Cu+iHj8i%iu^R%Tc8tS;4tpvhX5+hAK;T!75pWR;=~vgu z+yPT0c|^rC_HRVr4e6Rx!9fywb*R z{;(RG`7y{-hXuDyt3bFDl9qA*Lq`sB0?9Cppmx5ZjY$pOAs)NsQEryoaUY!RKMkh;hQq zXqLGDwAq}U@3ND5X<^lL96QU0G_zzI%WLD%x3@f!$hV=?9KwK|&+VmzL_f-8XSrEURO2fl@YoAqIvtHvHs5!8W#91oJ-qE zGXnfw#-e69LtHEEIahPLzIsvVLnzON*^_M6d&%b+cUr@9(JJ+))W_eNV?_+c@wC}! zRMur=b?9m5k*t%|_E68;9^j$Lj%ga{l~3GtdkfLxsnMq|V1ya7hCEbLvMB}Y*T$ha z?Y^p=F}`R$!Rhy_-A0}Xq#fnjtR$V>7LeLWB2Nl)mb=aGswwW@tw%AKWEU(TfWXP2XtGwljUhvtQ zjVJx&NrUOFU=6Cr=n+egHvElXv4-sf_DTFGSTFx6!CGe=hN`X#AA1QY-UK8x(Yy(Y z*9(j+q_c*~tqZ!i2$5rV#lkeHx4KafZqc#EBKugZZw)d){Qf>a`U}DO#}`umD**8K z2KikD43(X$)ve^zKEsQ4kfL*EV0;JZ73U`|lZBr`;2O`)Hs>~hUln?c=@vGB7G-z?Tlvt`pCS%xq_;!)M7H>(zAlzEQD4bk(X2-`X7@CNB`^kyq)fTO$h-ApDL281kXSg% zD-&7$=RTm|32LAUUInaMTvsp1%+SzTS#Lb+72b=&LN*}B%uVDOT%HV}CRA9DhQfs4 zPbd)!ZpSM{SiHu!c0Oz#($_81HZ&isB?!{0&pd?J^j&0765x|Nd>IG4TdG#URP|O0 zj9cgaNpE~Y-ZNPQT1G~C^s7%BC}I{C zm9=}7Fie`0g+lva=ukN;mgWP}Fbu^1i-p3hVSpK^pZMnbr}(}HztNFO+{nh`v%kWG z!85K>%`wTUcA`F%HYyILR~p5DjCcn-A|be?!o=iA?E1K5@!$CVO^50aPYD7l=BEbv zukaY(`DVCkPg^D{)AP|zaG&wSr{5)|%HpOnh;k+xhx~$_5IztSP1D~|EgFwe&@I*c zLhgXT4%up$03PQd7ZoXZhV|2xp33K@<~}j|RIQx_*)&<36n*J}Oh4b5K7*4vLuDZ9 zcU&e=?RBP3X~*;>u!DV>D!vdpErFj#RN+E&S)#YPbi>h=42QNq%y(}$;(62C@IjBA zPk_qsf2iSNJRKR*980+9tz|&+O2q46_CS)tCrpA7u3Cy*o34QahE(2Zg||9_CqN`GfH|I(isl?d zzc1t!|1!^0eQc4Nctz6)JfKH*0E(XRh2vbKIHhkKJW*T@R3xN`q8c77+uq04{h6)Y zlgJ_gI|h5N*Ug20C4~NfEa7o_e;e(7aIjz={m^pcu(+X{a&+GL-GJ(mv+nH^VC# zI>x?gh-)rpFhsq|h``Uu`X02NuBsvkg}PHUdzN==(G=yncG^fOEFqTXcZ^2fCG}}6%i|`wl~@kf1Y^x zxE1f|p;F$;VPn0SU9lETbn9wDiEUQf7dfY(b*cF`Vxp~u3^DE{puY0%`BJjBA8o=C z88Z?w^lI{36Ct!0kK!5;5gVwUT24v4c0tZ=Xe3d8@@(uiPs-Fjm~O3d9cEANdt{rn zniC*@pITqo?705VVSTHfhouRtVN{%ktpP%jELDkIs}lNk(BLI_=`n%+tW$oyi$F*q z$$eHE)3+bn1@ImsUgtXqIWjp`I>M)TuheDg-_SMtN46<42V}9Lp2^bpM00xx6 zi!(kK)if!0`y3y6`Do^cEf5Fyw8*;;P|ohaz@<^^hiC0eab_so?59O*<7d+4zvr7x z>WuAgeDnSxz5zQnf8zTO@BaJ+-+wr0E}&w5@=yNtiQoS)V1UBjk%>im-{H0n_Fj$6 z!w8sx6G!l*az04!dAg~Ygw%>G(J1J$&dJ7%XGb2GBPNpylCz{_p0~O6)MhvHK>hotdlEB|U zcd#_5wxUlpe{vT6kf_n4LOH9ltE|x2GHc~b!7*7}O)@JVC z9YtD-eS+M1(p86`6|cqz1dXQH(%8)+u z4#s@Q^Ahap5T%gMm?car!oskS_i2{*O1##%?S{t=wop5iz^mFikBca{oPx2vt}7$E zlb%g$I!Qqp|+a0|sEG*t}o7%e)WnYzi0+JQ!sT&YF|4^?)6VHqq3QY7o{Vy3<>x zdx3{r(dz@HRHsj18ltQ8-?J{Vp4fGsBoYggcscrS%Cl>W(@@lU?}rO$!_H3hIQVJ| z`{Z=`XYJRWh_Y629H9yN@dn6r-@#`p0#|Ll0oF@XRHUJm&$mVMqUvPhYbrw_y{@cI z8+Zn_n7`}YwT~N}-;ZPMm3B(yU=wQb*{G^wkI7b2x{EEf_H7x-wGW?tI=tu8j*T%{ zn+;{)XK)|==I$Lrv*VEv9TG1gQt6BJ4Imt_cr{+?X`?tIfIQ6z_!JgZph@t`W1=Dv zE)Tq3ApNBfwBj>E#J%J-y=Xa=6(DwU3(#5nXz9aTP^&usbN{=!1B~7mB{-s&^u3`9 z31H_|y^SQr^?r;W1KAFDrpw7RY@tt)7F{>WpT6j*Ek!iNWe@}HKTT+=vg9;y2SZ!9 zgvhVXG$ov9!%B?6d$WP=J+5Ffj+W{~ba{*%j0-%@FIL61@My!|G|23U6@buVKQ_pG z|CC^X72ob_ltFVSB<|OGJC;gjL3-MkO<__%U-``Tzu@0_?Nj}vTwSYIhV~(h4Fk;d zu~^>*3;^N>7%;E`oF5NB`Y#0QpB^yy>x)?58{{|IEkcKwrlWc}q(BiNrF$SBlAeJ1 zw0=HVxP*mGWW4W0)Pl9O>sCr~5ZwyDNHMq|RI!!hO9ky$ ztm6GggHi4wnkcp`e-C>|Sn_j%N#r&3MD!T5R76CKP#Cx5GcV8;g z)g%}4RUR+|HBSzG2{_#?(zKHNC&;R}6TOp5jaIDUu3g~e$d?uveV)G36U1QP8aH2} zsG3K=_p07-&lftAcuFiAKTbO9XPF6knUfve`;JS~S+vqcaC-2f`HPIHkZPsGn-wx7 zd+PI8c0Odih)`%5QBKtmg~mF9ZJ%|?5ye?vTff0iT$6WcV22n zvW^+C#cqXDwiyPqwXv;K0-n34E48_ACb$-H(LbN1AaPcCowrY3VSOm%seU5a2fnWC zosc3wf^jM5yIRmTe`;Go5shtV2*sc1JjYjo?KJ3=dpAftFfz`>J%H0WzY!vHZ{bN2 zfkA~ck&5Vz0y0i_zN6K!`E*y)q$W%aA6o&TWs$yhtBdft#tdxCKa5S`tI3+0Aa|fOH#PN0wzCqD-KaJJ1uUkvFoJbU5f-2TA?tU z2}m1rgbnX85AD=UU*=A8%2EGXm= zo@I0*PIR=W!$~+sv=TE@zpsE4_^0@eYFMN`wk)`ufeXvqJHVP`YlL@>GR)S+g{H;M zql6o-(r?>&pH3-yZSC{f^=9V&V%{LshJeCL~iimm95YAgm` z=dCr19pwj#C2bg9<&)3FaJ-rl9do3?ijj6&%Jd|IkihLAR|C;GPIzf#^bHZTjpWR+ zmsH>fyzbWMHmsk;J^O+|uY>AvcELVRFK%^u^Ku>;r*?Apln}hE6cHaMM(ALPD$LgCfr@o2Nm&VBQj!cd>#hrndKytMXB4 zq@^}EJ!|_kzh*Ba3LxXZ4~VfRwj6#1=+;ahaDH6DF8ELJeKG}Q+Caj(964}0-Pu~- z8v$HM5f005$Lz~sA|V=j#-Qk}+;U!M!yXt@<#WE}^;n$0>o?%LW`N^A=r=xF8(Y=p z%y7qa&``$<@H6TA%0>C7l9g(8gi>PK5gMN&lI&Oa(CW|wYLr{ZWHN0Vj$ya^BG?R8HcE%WaNd(u? zQEjTy9pZ%*`w&3?obLhq<3kkTrB%*mVp4=)0K5-`;k-ieFlg&NV+Bv>kd}o)8VA)h z>MK^2=#Tb@!j*vsGRkpJ7*s?0o)BAsCK{JMrM6F9qSOTDk@g%y6=l$NN=d+HOMU&h zLYKnP>q*f2Y+q+qEa{X_4gGsBAGq2hx6UxfSaOhq6FpaS-RvH!ZPMM&mo8lhR{9QP z8XD)Xb@2HJ6*g{M0%uLmmS3;KnnxhKYnutfJdEfE<-3|RXvu3#NFPvB_F?X|B(-~^)ID60Ws*N$(ZYA74;v9faP&TYKOlI}YsT#JpF$2CGBu zq3LtYT^lZPeX9u^2xzsLjkT_HZlv1%MjMmymh;Pjjw<_8C$-YirpiG@!BT^YN0a?K z-(M~HO_A6WyIKGVj|092fb*k%3;!X$!5_2xiSIwWf8rN>|KU9#fC~9Z4PK0tC_W~+ zeog;5Ko_tP#Tp3ACGgkCCzoF%bj?h*4geybOl>BCpnzaPeUE%H13Ux_1CBWhNY4Xy zAvCwt)3GzLv>;@E{9DJ4ke!i^<^=;Q9UC*%_^koble@4izM#wm=HXU$9@#J%-PV zAu*yZb4oAT!1MQctbfnf{*0__WF!Jv?~ak;4Q!rtaZd4g*-etY_HlV3EuCi{Anvb# z0hHKmwwfGK-nfG5Ad=GIIh%qm2?hb3HS!EIq(#&;mt5G>s3>I4kcd((psO9mbrH~CZUT3J3Z`IUvxQvV33%Ts*F6ha&I^q5E@=NU9dedvXR%B z@n_6al+3)R;^L8=O<^0wXi|GqY2VIr(fE81l0(xU!lD7MbyF{61-2}%PxDk_(7eJ;;2Exk6aU@9=?oJ9tpQE{PkvmO_ zvDE4G`hgyS=*p`q^5xXx8b(_5GaaZH3RUS3-l>|VsjO+pd3ulh|4vsb)Gh$8&kwpH z@=xi?{eB5aBAO%X^m$g<;IO`OMRyn1%XV4cqoEc7rIiZ=|f@qe(_aq;xtFV8$&5Fh|?mkk$Cy;g)K}ejJb#;t{qYWfJO89Bf9mBS9ddaQy)WOO3 zgHtr-ijDXZoxB3TLzQIH5s}8AIk`GM;YJuMmx5gFenH_ZqoOj)R>yBqlBn>vB(}oS z@AXMY7tkg8BVIi)i^6?WjC0Z~PdmM2DqMUvR5aNwhJn_}tG-x7uv=nfRW6oz|GE0|nbCFCO^0R5`bPbrui|1iu!n+j#Mrd(+N?+4BeoNhwqn; z0QrVnC&tt+vSDa~lTwRMPop@`QrZc3*~lx$!A34*yi7}wx7w)RF_~;%L=<44?=7jlQo${jFE>3%>ub=Lx8opZI3TE`RZu_MLCP zuC*WP4Z;AEoeBe@%1A{rgX6>z{j^oI%-9%^lJ^O1tIsA&g144xHo;j1>>VOc z!5zry@f>y0F5zY0Tpyv3!ct{OeBPvnbe4bi`9kb0f#^ec3Dme{81P4MOws^@6})mz zgoXK3y5;)#t)c7GO1G9;x{{hfH0QzQ_bLO|A7HsSaK}w65+xWb9B=97(+rog@ssz9 zS&M`Z`^e8|B%w%`h=CZJ(Vh&jVepH2|YQQtP=IXn)15`Z)VqAdZCZb<0)S*h@tixwV7;DIOoBL6}Iwog`xN{{*@CT zN5Gp1==c112SCzKd^6{+wmqhO=UaZ#%U~`A@enNgCn~WKC#*LF*yWzZNVWdE{FSEqK>LvSe*DGs#$x@b2K6)M}sPBXmA!;48 z#@>5(RLkXJU^r7b`G$j9(_5G|r$w$LD$7uj{FBNUu7PH0wg$Nl+}qfPZ0_j_zajhT z{PnEk3RUmx=gJINPtBlexHXG=Pv12w784-gY<=?b^zM48dRpIntC@)UzX+txA&_DW zFyjw=i~k|M0onef-+#E*{R_VTa0>x=f}i+i@H7X^RetB2)?9@AzB)C}I=DZJ>R|an zIMmzL+@CsaA$M8z!&}^hXGP&#My|0#84;FDdLaVMFtrMkDdZD9F?nYfcxsH0baVY|CjrbxBBOElp2a8`oX8KS`%GLOmoJtAoX5-NKPv8K7|H{x}dpbISPrmc`?dTFQyLdkhY5QZi##hN@fpN^>OBvu{aKp z+gu~EkWC~)D*`;od{7AJ{Y{3{Ybf13Q1M3cW*gy$o7h-_x~zFus_P|)CxgNoq>jAH zG7GLbKwX0!XvTO2htUEiF~b6&sk?kKdS0tH7MQ5BSZif&#@s zh}eHja2DU9^bsNN1E)RBY-6tRv{(gZux6NJd|Uu?iBH4OC_ssKMJNy!1aozzD% zN@PaxRi3V&n1%5B@bS+}LCvA-a@`O!3)-f6WWOLD<$F`?Adj0N*hkgYN&Z0DU)oQ_ z^TBM%T{*!OOFzhyxmNm#KCTZ^$r;3!hod%mdb!xeek7}{GvSs*x03&ai|j)OFfbZ3 zk$wMXuZKg+FFuVt@fNzsbT_-L9yuD>aQi$}*hU8t3>(MwIdkL=g@L&d>h05Y*o!ry z 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�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Ⓢy|@QUk{F&$_5(axs$ZQz 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