diff --git a/hedera-node/hedera-app/build.gradle.kts b/hedera-node/hedera-app/build.gradle.kts index e8ea30e9bf45..44776cc290de 100644 --- a/hedera-node/hedera-app/build.gradle.kts +++ b/hedera-node/hedera-app/build.gradle.kts @@ -165,7 +165,10 @@ val cleanRun = tasks.clean { dependsOn(cleanRun) } -tasks.register("showHapiVersion") { doLast { println(libs.versions.hapi.proto.get()) } } +tasks.register("showHapiVersion") { + inputs.property("version", project.version) + doLast { println(inputs.properties["version"]) } +} var updateDockerEnvTask = tasks.register("updateDockerEnv") { 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 f06d69f3c321..2f936bddc0aa 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 @@ -16,6 +16,7 @@ package com.hedera.node.app; +import static com.hedera.node.app.blocks.BlockStreamService.FAKE_RESTART_BLOCK_HASH; import static com.hedera.node.app.info.UnavailableNetworkInfo.UNAVAILABLE_NETWORK_INFO; import static com.hedera.node.app.records.schemas.V0490BlockRecordSchema.BLOCK_INFO_STATE_KEY; import static com.hedera.node.app.state.merkle.VersionUtils.isSoOrdered; @@ -36,6 +37,7 @@ import com.hedera.hapi.node.base.SemanticVersion; import com.hedera.hapi.node.state.blockrecords.BlockInfo; import com.hedera.hapi.util.HapiUtils; +import com.hedera.node.app.blocks.BlockStreamManager; import com.hedera.node.app.blocks.BlockStreamService; import com.hedera.node.app.blocks.impl.BoundaryStateChangeListener; import com.hedera.node.app.blocks.impl.KVStateChangeListener; @@ -69,6 +71,8 @@ import com.hedera.node.app.statedumpers.MerkleStateChild; import com.hedera.node.app.store.ReadableStoreFactory; import com.hedera.node.app.throttle.CongestionThrottleService; +import com.hedera.node.app.tss.TssBaseService; +import com.hedera.node.app.tss.impl.PlaceholderTssBaseService; import com.hedera.node.app.version.HederaSoftwareVersion; import com.hedera.node.app.version.ServicesSoftwareVersion; import com.hedera.node.app.workflows.handle.HandleWorkflow; @@ -190,6 +194,11 @@ public final class Hedera implements SwirldMain, PlatformStatusChangeListener { */ private final InstantSource instantSource; + /** + * The supplier for the TSS base service. + */ + private final Supplier tssBaseServiceSupplier; + /** * The contract service singleton, kept as a field here to avoid constructing twice * (once in constructor to register schemas, again inside Dagger component). @@ -202,6 +211,12 @@ public final class Hedera implements SwirldMain, PlatformStatusChangeListener { */ private final FileServiceImpl fileServiceImpl; + /** + * The block stream service singleton, kept as a field here to reuse information learned + * during the state migration phase in the later initialization phase. + */ + private final BlockStreamService blockStreamService; + /** * The bootstrap configuration provider for the network. */ @@ -268,14 +283,17 @@ public final class Hedera implements SwirldMain, PlatformStatusChangeListener { * @param constructableRegistry the registry to register {@link RuntimeConstructable} factories with * @param registryFactory the factory to use for creating the services registry * @param migrator the migrator to use with the services + * @param tssBaseServiceSupplier the supplier for the TSS base service */ public Hedera( @NonNull final ConstructableRegistry constructableRegistry, @NonNull final ServicesRegistry.Factory registryFactory, @NonNull final ServiceMigrator migrator, - @NonNull final InstantSource instantSource) { + @NonNull final InstantSource instantSource, + @NonNull final Supplier tssBaseServiceSupplier) { requireNonNull(registryFactory); requireNonNull(constructableRegistry); + this.tssBaseServiceSupplier = requireNonNull(tssBaseServiceSupplier); this.serviceMigrator = requireNonNull(migrator); this.instantSource = requireNonNull(instantSource); logger.info( @@ -306,6 +324,7 @@ public Hedera( new SignatureExpanderImpl(), new SignatureVerifierImpl(CryptographyHolder.get()))); contractServiceImpl = new ContractServiceImpl(appContext); + blockStreamService = new BlockStreamService(bootstrapConfig); // Register all service schema RuntimeConstructable factories before platform init Set.of( new EntityIdService(), @@ -318,7 +337,7 @@ public Hedera( new UtilServiceImpl(), new RecordCacheService(), new BlockRecordService(), - new BlockStreamService(bootstrapConfig), + blockStreamService, new FeeService(), new CongestionThrottleService(), new NetworkServiceImpl(), @@ -775,6 +794,7 @@ private void initializeDagger( @NonNull final InitTrigger trigger, @NonNull final List migrationStateChanges) { final var notifications = platform.getNotificationEngine(); + final var blockStreamEnabled = isBlockStreamEnabled(); // The Dagger component should be constructed every time we reach this point, even if // it exists (this avoids any problems with mutable singleton state by reconstructing // everything); but we must ensure the gRPC server in the old component is fully stopped, @@ -784,6 +804,9 @@ private void initializeDagger( notifications.unregister(PlatformStatusChangeListener.class, this); notifications.unregister(ReconnectCompleteListener.class, daggerApp.reconnectListener()); notifications.unregister(StateWriteToDiskCompleteListener.class, daggerApp.stateWriteToDiskListener()); + if (blockStreamEnabled) { + daggerApp.tssBaseService().unregisterLedgerSignatureConsumer(daggerApp.blockStreamManager()); + } } // Fully qualified so as to not confuse javadoc daggerApp = com.hedera.node.app.DaggerHederaInjectionComponent.builder() @@ -804,12 +827,36 @@ private void initializeDagger( .kvStateChangeListener(kvStateChangeListener) .boundaryStateChangeListener(boundaryStateChangeListener) .migrationStateChanges(migrationStateChanges) + .tssBaseService(tssBaseServiceSupplier.get()) .build(); // Initialize infrastructure for fees, exchange rates, and throttles from the working state daggerApp.initializer().accept(state); notifications.register(PlatformStatusChangeListener.class, this); notifications.register(ReconnectCompleteListener.class, daggerApp.reconnectListener()); notifications.register(StateWriteToDiskCompleteListener.class, daggerApp.stateWriteToDiskListener()); + if (blockStreamEnabled) { + daggerApp + .blockStreamManager() + .initLastBlockHash( + switch (trigger) { + case GENESIS -> BlockStreamManager.ZERO_BLOCK_HASH; + // FUTURE - get the actual last block hash from e.g. a reconnect teacher or disk + default -> blockStreamService + .migratedLastBlockHash() + .orElse(FAKE_RESTART_BLOCK_HASH); + }); + daggerApp.tssBaseService().registerLedgerSignatureConsumer(daggerApp.blockStreamManager()); + if (daggerApp.tssBaseService() instanceof PlaceholderTssBaseService placeholderTssBaseService) { + daggerApp.inject(placeholderTssBaseService); + } + } + } + + private boolean isBlockStreamEnabled() { + return bootstrapConfigProvider + .getConfiguration() + .getConfigData(BlockStreamConfig.class) + .streamBlocks(); } private static ServicesSoftwareVersion getNodeStartupVersion(@NonNull final Configuration config) { 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 d956e8fd7936..6895096b58a7 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 @@ -47,6 +47,8 @@ import com.hedera.node.app.state.WorkingStateAccessor; import com.hedera.node.app.throttle.ThrottleServiceManager; import com.hedera.node.app.throttle.ThrottleServiceModule; +import com.hedera.node.app.tss.TssBaseService; +import com.hedera.node.app.tss.impl.PlaceholderTssBaseService; import com.hedera.node.app.workflows.FacilityInitModule; import com.hedera.node.app.workflows.WorkflowsInjectionModule; import com.hedera.node.app.workflows.handle.HandleWorkflow; @@ -132,6 +134,10 @@ public interface HederaInjectionComponent { StoreMetricsService storeMetricsService(); + TssBaseService tssBaseService(); + + void inject(PlaceholderTssBaseService placeholderTssBaseService); + @Component.Builder interface Builder { @BindsInstance @@ -185,6 +191,9 @@ interface Builder { @BindsInstance Builder migrationStateChanges(List migrationStateChanges); + @BindsInstance + Builder tssBaseService(TssBaseService tssBaseService); + HederaInjectionComponent build(); } } 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 459128029091..11124585fd48 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 @@ -18,29 +18,45 @@ import static com.swirlds.common.io.utility.FileUtils.getAbsolutePath; import static com.swirlds.common.io.utility.FileUtils.rethrowIO; +import static com.swirlds.common.threading.manager.AdHocThreadManager.getStaticThreadManager; import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; import static com.swirlds.platform.builder.PlatformBuildConstants.DEFAULT_CONFIG_FILE_NAME; import static com.swirlds.platform.builder.PlatformBuildConstants.DEFAULT_SETTINGS_FILE_NAME; +import static com.swirlds.platform.builder.internal.StaticPlatformBuilder.getMetricsProvider; +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.state.signed.StartupStateUtils.getInitialState; import static com.swirlds.platform.system.SystemExitCode.CONFIGURATION_ERROR; import static com.swirlds.platform.system.SystemExitCode.NODE_ADDRESS_MISMATCH; import static com.swirlds.platform.system.SystemExitUtils.exitSystem; +import static com.swirlds.platform.system.address.AddressBookUtils.createRoster; +import static com.swirlds.platform.system.address.AddressBookUtils.initializeAddressBook; import static com.swirlds.platform.util.BootstrapUtils.checkNodesToRun; import static com.swirlds.platform.util.BootstrapUtils.getNodesToRun; import static java.util.Objects.requireNonNull; import com.hedera.node.app.services.OrderedServiceMigrator; import com.hedera.node.app.services.ServicesRegistryImpl; +import com.hedera.node.app.tss.impl.PlaceholderTssBaseService; import com.swirlds.base.time.Time; import com.swirlds.common.constructable.ConstructableRegistry; import com.swirlds.common.constructable.RuntimeConstructable; +import com.swirlds.common.context.PlatformContext; import com.swirlds.common.crypto.CryptographyFactory; +import com.swirlds.common.crypto.CryptographyHolder; +import com.swirlds.common.io.filesystem.FileSystemManager; import com.swirlds.common.io.utility.FileUtils; +import com.swirlds.common.io.utility.RecycleBin; +import com.swirlds.common.merkle.crypto.MerkleCryptoFactory; +import com.swirlds.common.merkle.crypto.MerkleCryptographyFactory; import com.swirlds.common.platform.NodeId; import com.swirlds.config.api.Configuration; import com.swirlds.config.api.ConfigurationBuilder; import com.swirlds.config.extensions.sources.SystemEnvironmentConfigSource; import com.swirlds.config.extensions.sources.SystemPropertiesConfigSource; import com.swirlds.platform.CommandLineArgs; +import com.swirlds.platform.ParameterProvider; import com.swirlds.platform.builder.PlatformBuilder; import com.swirlds.platform.config.legacy.ConfigurationException; import com.swirlds.platform.config.legacy.LegacyConfigProperties; @@ -154,7 +170,7 @@ public static void main(final String... args) throws Exception { // Determine which node to run locally // Load config.txt address book file and parse address book - final AddressBook addressBook = loadAddressBook(DEFAULT_CONFIG_FILE_NAME); + final AddressBook bootstrapAddressBook = loadAddressBook(DEFAULT_CONFIG_FILE_NAME); // parse command line arguments final CommandLineArgs commandLineArgs = CommandLineArgs.parse(args); @@ -169,7 +185,7 @@ public static void main(final String... args) throws Exception { // get the list of configured nodes from the address book // for each node in the address book, check if it has a local IP (local to this computer) // additionally if a command line arg is supplied then limit matching nodes to that node id - final List nodesToRun = getNodesToRun(addressBook, commandLineArgs.localNodesToStart()); + final List nodesToRun = getNodesToRun(bootstrapAddressBook, commandLineArgs.localNodesToStart()); // hard exit if no nodes are configured to run checkNodesToRun(nodesToRun); @@ -178,19 +194,58 @@ public static void main(final String... args) throws Exception { final SoftwareVersion version = hedera.getSoftwareVersion(); logger.info("Starting node {} with version {}", selfId, version); - final PlatformBuilder platformBuilder = PlatformBuilder.create( - Hedera.APP_NAME, - Hedera.SWIRLD_NAME, + final var configuration = buildConfiguration(); + final var keysAndCerts = + initNodeSecurity(bootstrapAddressBook, configuration).get(selfId); + + setupGlobalMetrics(configuration); + final var metrics = getMetricsProvider().createPlatformMetrics(selfId); + final var time = Time.getCurrent(); + final var fileSystemManager = FileSystemManager.create(configuration); + final var recycleBin = + RecycleBin.create(metrics, configuration, getStaticThreadManager(), time, fileSystemManager, selfId); + + final var cryptography = CryptographyFactory.create(); + CryptographyHolder.set(cryptography); + // the AddressBook is not changed after this point, so we calculate the hash now + cryptography.digestSync(bootstrapAddressBook); + + // Initialize the Merkle cryptography + final var merkleCryptography = MerkleCryptographyFactory.create(configuration, cryptography); + MerkleCryptoFactory.set(merkleCryptography); + + // Create the platform context + final var platformContext = PlatformContext.create( + configuration, + Time.getCurrent(), + metrics, + cryptography, + FileSystemManager.create(configuration), + recycleBin, + merkleCryptography); + // Create initial state for the platform + final var initialState = getInitialState( + platformContext, version, hedera::newMerkleStateRoot, SignedStateFileUtils::readState, - selfId); + Hedera.APP_NAME, + Hedera.SWIRLD_NAME, + selfId, + bootstrapAddressBook); + + // Initialize the address book and set on platform builder + final var addressBook = + initializeAddressBook(selfId, version, initialState, bootstrapAddressBook, platformContext); - // Add additional configuration to the platform - final Configuration configuration = buildConfiguration(); - platformBuilder.withConfiguration(configuration); - platformBuilder.withCryptography(CryptographyFactory.create()); - platformBuilder.withTime(Time.getCurrent()); + // Follow the Inversion of Control pattern by injecting all needed dependencies into the PlatformBuilder. + final var platformBuilder = PlatformBuilder.create( + Hedera.APP_NAME, Hedera.SWIRLD_NAME, version, initialState, selfId) + .withPlatformContext(platformContext) + .withConfiguration(configuration) + .withAddressBook(addressBook) + .withRoster(createRoster(addressBook)) + .withKeysAndCerts(keysAndCerts); // IMPORTANT: A surface-level reading of this method will undersell the centrality // of the Hedera instance. It is actually omnipresent throughout both the startup @@ -233,13 +288,15 @@ private static Configuration buildConfiguration() { .withSource(SystemPropertiesConfigSource.getInstance()); rethrowIO(() -> BootstrapUtils.setupConfigBuilder(configurationBuilder, getAbsolutePath(DEFAULT_SETTINGS_FILE_NAME))); - return configurationBuilder.build(); + final Configuration configuration = configurationBuilder.build(); + checkConfiguration(configuration); + return configuration; } /** * Selects the node to run locally from either the command line arguments or the address book. * - * @param nodesToRun the list of nodes configured to run based on the address book. + * @param nodesToRun the list of nodes configured to run based on the address book. * @param localNodesToStart the node ids specified on the command line. * @return the node which should be run locally. * @throws ConfigurationException if more than one node would be started or the requested node is not configured. @@ -288,6 +345,7 @@ private static AddressBook loadAddressBook(@NonNull final String addressBookPath try { final LegacyConfigProperties props = LegacyConfigPropertiesLoader.loadConfigFile(FileUtils.getAbsolutePath(addressBookPath)); + 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); @@ -301,6 +359,7 @@ private static Hedera newHedera() { ConstructableRegistry.getInstance(), ServicesRegistryImpl::new, new OrderedServiceMigrator(), - InstantSource.system()); + InstantSource.system(), + PlaceholderTssBaseService::new); } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/BlockStreamManager.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/BlockStreamManager.java index 6f156bfd349c..6b052a3aaf0d 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/BlockStreamManager.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/BlockStreamManager.java @@ -22,6 +22,7 @@ import com.swirlds.platform.system.Round; import com.swirlds.state.State; import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.function.BiConsumer; /** * Maintains the state and process objects needed to produce the block stream. @@ -34,11 +35,22 @@ * Items written to the stream will be produced in the order they are written. The leaves of the input and output item * Merkle trees will be in the order they are written. */ -public interface BlockStreamManager extends BlockRecordInfo { +public interface BlockStreamManager extends BlockRecordInfo, BiConsumer { + Bytes ZERO_BLOCK_HASH = Bytes.wrap(new byte[48]); + + /** + * Initializes the block stream manager after a restart with the hash of the last block incorporated + * in the state used in the restart. If the restart was from genesis, this hash should be the + * {@link #ZERO_BLOCK_HASH}. + * @param blockHash the hash of the last block + */ + void initLastBlockHash(@NonNull Bytes blockHash); + /** * Updates the internal state of the block stream manager to reflect the start of a new round. * @param round the round that has just started * @param state the state of the network at the beginning of the round + * @throws IllegalStateException if the last block hash was not explicitly initialized */ void startRound(@NonNull Round round, @NonNull State state); @@ -57,11 +69,4 @@ public interface BlockStreamManager extends BlockRecordInfo { * @throws IllegalStateException if the stream is closed */ void writeItem(@NonNull BlockItem item); - - /** - * Completes the block proof for the given block with the given signature. - * @param blockNumber the number of the block to finish - * @param signature the signature to use in the block proof - */ - void finishBlockProof(long blockNumber, @NonNull Bytes signature); } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/BlockStreamService.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/BlockStreamService.java index c9ea329603a6..91eb6ed62279 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/BlockStreamService.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/BlockStreamService.java @@ -21,20 +21,28 @@ import com.hedera.node.app.blocks.schemas.V0540BlockStreamSchema; import com.hedera.node.config.data.BlockStreamConfig; +import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.config.api.Configuration; import com.swirlds.state.spi.SchemaRegistry; import com.swirlds.state.spi.Service; import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Optional; /** * Service for BlockStreams implementation responsible for tracking state changes * and writing them to a block */ public class BlockStreamService implements Service { + public static final Bytes FAKE_RESTART_BLOCK_HASH = Bytes.fromHex("abcd".repeat(24)); + public static final String NAME = "BlockStreamService"; private final boolean enabled; + @Nullable + private Bytes migratedLastBlockHash; + /** * Service constructor. */ @@ -52,7 +60,20 @@ public String getServiceName() { public void registerSchemas(@NonNull final SchemaRegistry registry) { requireNonNull(registry); if (enabled) { - registry.register(new V0540BlockStreamSchema()); + registry.register(new V0540BlockStreamSchema(this::setMigratedLastBlockHash)); } } + + /** + * Returns the last block hash as migrated from a state that used record streams, or empty + * if there was no such hash observed during migration. + * @return the last block hash + */ + public Optional migratedLastBlockHash() { + return Optional.ofNullable(migratedLastBlockHash); + } + + private void setMigratedLastBlockHash(@NonNull final Bytes migratedLastBlockHash) { + this.migratedLastBlockHash = requireNonNull(migratedLastBlockHash); + } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/BlockImplUtils.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/BlockImplUtils.java index 834aaa8ce3e2..050bc0e045ca 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/BlockImplUtils.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/BlockImplUtils.java @@ -215,6 +215,22 @@ public static Bytes appendHash(@NonNull final Bytes hash, @NonNull final Bytes h return Bytes.wrap(newBytes); } + /** + * Hashes the given left and right hashes. + * @param leftHash the left hash + * @param rightHash the right hash + * @return the combined hash + */ + public static Bytes combine(@NonNull final Bytes leftHash, @NonNull final Bytes rightHash) { + return Bytes.wrap(combine(leftHash.toByteArray(), rightHash.toByteArray())); + } + + /** + * Hashes the given left and right hashes. + * @param leftHash the left hash + * @param rightHash the right hash + * @return the combined hash + */ public static byte[] combine(final byte[] leftHash, final byte[] rightHash) { try { final var digest = MessageDigest.getInstance(DigestType.SHA_384.algorithmName()); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/BlockStreamManagerImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/BlockStreamManagerImpl.java index ebf4e495684f..8211b2fe68a7 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/BlockStreamManagerImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/BlockStreamManagerImpl.java @@ -17,6 +17,8 @@ package com.hedera.node.app.blocks.impl; import static com.hedera.hapi.node.base.BlockHashAlgorithm.SHA2_384; +import static com.hedera.hapi.util.HapiUtils.asInstant; +import static com.hedera.node.app.blocks.impl.BlockImplUtils.appendHash; import static com.hedera.node.app.blocks.impl.BlockImplUtils.combine; import static com.hedera.node.app.blocks.schemas.V0540BlockStreamSchema.BLOCK_STREAM_INFO_KEY; import static com.hedera.node.app.hapi.utils.CommonUtils.noThrowSha384HashOf; @@ -28,25 +30,27 @@ import com.hedera.hapi.block.stream.BlockItem; import com.hedera.hapi.block.stream.BlockProof; +import com.hedera.hapi.block.stream.MerkleSiblingHash; import com.hedera.hapi.block.stream.output.BlockHeader; import com.hedera.hapi.block.stream.output.TransactionResult; import com.hedera.hapi.node.base.SemanticVersion; import com.hedera.hapi.node.base.Timestamp; import com.hedera.hapi.node.state.blockstream.BlockStreamInfo; +import com.hedera.hapi.platform.state.PlatformState; import com.hedera.node.app.blocks.BlockItemWriter; import com.hedera.node.app.blocks.BlockStreamManager; import com.hedera.node.app.blocks.BlockStreamService; import com.hedera.node.app.blocks.StreamingTreeHasher; import com.hedera.node.app.records.impl.BlockRecordInfoUtils; +import com.hedera.node.app.tss.TssBaseService; import com.hedera.node.config.ConfigProvider; import com.hedera.node.config.data.BlockRecordStreamConfig; import com.hedera.node.config.data.BlockStreamConfig; -import com.hedera.node.config.data.HederaConfig; import com.hedera.node.config.data.VersionConfig; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.config.api.Configuration; import com.swirlds.platform.state.service.PlatformStateService; -import com.swirlds.platform.state.service.ReadablePlatformStateStore; +import com.swirlds.platform.state.service.schemas.V0540PlatformStateSchema; import com.swirlds.platform.system.Round; import com.swirlds.state.State; import com.swirlds.state.spi.CommittableWritableStates; @@ -69,26 +73,25 @@ public class BlockStreamManagerImpl implements BlockStreamManager { private static final Logger log = LogManager.getLogger(BlockStreamManagerImpl.class); - private static final Bytes MOCK_HASH = Bytes.wrap(new byte[48]); private static final int CHUNK_SIZE = 8; private static final CompletableFuture MOCK_START_STATE_ROOT_HASH_FUTURE = completedFuture(Bytes.wrap(new byte[48])); private final int roundsPerBlock; + private final TssBaseService tssBaseService; private final SemanticVersion hapiVersion; - private final SemanticVersion nodeVersion; private final ExecutorService executor; - private final BlockHashManager blockHashManager; - private final RunningHashManager runningHashManager; private final Supplier writerSupplier; private final BoundaryStateChangeListener boundaryStateChangeListener; - // All this state is scoped to producing the block for the last-started round + private final BlockHashManager blockHashManager; + private final RunningHashManager runningHashManager; + + // All this state is scoped to producing the current block private long blockNumber; // Set to the round number of the last round handled before entering a freeze period private long freezeRoundNumber = -1; - // FUTURE - initialize to the actual last block hash (this is only correct at genesis) - private Bytes lastBlockHash = Bytes.wrap(new byte[48]); + private Bytes lastBlockHash; private Instant blockTimestamp; private BlockItemWriter writer; private List pendingItems; @@ -101,15 +104,24 @@ public class BlockStreamManagerImpl implements BlockStreamManager { */ private CompletableFuture writeFuture = completedFuture(null); + // (FUTURE) Remove this once reconnect protocol also transmits the last block hash + private boolean appendRealHashes = false; + /** * Represents a block pending completion by the block hash signature needed for its block proof. * - * @param blockNumber the block number + * @param number the block number + * @param blockHash the block hash * @param proofBuilder the block proof builder * @param writer the block item writer + * @param siblingHashes the sibling hashes needed for an indirect block proof of an earlier block */ private record PendingBlock( - long blockNumber, @NonNull BlockProof.Builder proofBuilder, @NonNull BlockItemWriter writer) {} + long number, + @NonNull Bytes blockHash, + @NonNull BlockProof.Builder proofBuilder, + @NonNull BlockItemWriter writer, + @NonNull MerkleSiblingHash... siblingHashes) {} /** * A queue of blocks pending completion by the block hash signature needed for their block proofs. @@ -121,23 +133,36 @@ public BlockStreamManagerImpl( @NonNull final Supplier writerSupplier, @NonNull final ExecutorService executor, @NonNull final ConfigProvider configProvider, + @NonNull final TssBaseService tssBaseService, @NonNull final BoundaryStateChangeListener boundaryStateChangeListener) { this.writerSupplier = requireNonNull(writerSupplier); this.executor = requireNonNull(executor); + this.tssBaseService = requireNonNull(tssBaseService); this.boundaryStateChangeListener = requireNonNull(boundaryStateChangeListener); - final var config = requireNonNull(configProvider).getConfiguration(); + requireNonNull(configProvider); + final var config = configProvider.getConfiguration(); this.hapiVersion = hapiVersionFrom(config); - this.nodeVersion = nodeVersionFrom(config); this.roundsPerBlock = config.getConfigData(BlockStreamConfig.class).roundsPerBlock(); this.blockHashManager = new BlockHashManager(config); this.runningHashManager = new RunningHashManager(); } + @Override + public void initLastBlockHash(@NonNull final Bytes blockHash) { + lastBlockHash = requireNonNull(blockHash); + } + @Override public void startRound(@NonNull final Round round, @NonNull final State state) { - // We will always close the block at the end of the freeze round, even if - // its number would not otherwise trigger a block closing - if (isFreezeRound(state, round)) { + if (lastBlockHash == null) { + throw new IllegalStateException("Last block hash must be initialized before starting a round"); + } + final var platformState = state.getReadableStates(PlatformStateService.NAME) + .getSingleton(V0540PlatformStateSchema.PLATFORM_STATE_KEY) + .get(); + requireNonNull(platformState); + if (isFreezeRound(platformState, round)) { + // Track freeze round numbers because they always end a block freezeRoundNumber = round.getRoundNum(); } if (writer == null) { @@ -159,7 +184,7 @@ public void startRound(@NonNull final Round round, @NonNull final State state) { .number(blockNumber) .previousBlockHash(lastBlockHash) .hashAlgorithm(SHA2_384) - .softwareVersion(nodeVersion) + .softwareVersion(platformState.creationSoftwareVersionOrThrow()) .hapiProtoVersion(hapiVersion)) .build()); @@ -172,7 +197,8 @@ public void endRound(@NonNull final State state, final long roundNum) { if (shouldCloseBlock(roundNum, roundsPerBlock)) { final var writableState = state.getWritableStates(BlockStreamService.NAME); final var blockStreamInfoState = writableState.getSingleton(BLOCK_STREAM_INFO_KEY); - // Ensure all runningHashManager futures are complete + // Ensure runningHashManager futures include all result items and are completed + schedulePendingWork(); writeFuture.join(); // Commit the block stream info to state before flushing the boundary state changes blockStreamInfoState.put(new BlockStreamInfo( @@ -184,53 +210,28 @@ public void endRound(@NonNull final State state, final long roundNum) { schedulePendingWork(); writeFuture.join(); - final var inputRootHash = inputTreeHasher.rootHash().join(); - final var outputRootHash = outputTreeHasher.rootHash().join(); + final var inputHash = inputTreeHasher.rootHash().join(); + final var outputHash = outputTreeHasher.rootHash().join(); final var blockStartStateHash = MOCK_START_STATE_ROOT_HASH_FUTURE.join(); - final var blockHash = computeBlockHash(lastBlockHash, inputRootHash, outputRootHash, blockStartStateHash); - // FUTURE: sign the block hash and gossip our signature - - final var blockProofBuilder = BlockProof.newBuilder() + final var leftParent = combine(lastBlockHash, inputHash); + final var rightParent = combine(outputHash, blockStartStateHash); + final var blockHash = combine(leftParent, rightParent); + final var pendingProof = BlockProof.newBuilder() .block(blockNumber) .previousBlockRootHash(lastBlockHash) .startOfBlockStateRootHash(blockStartStateHash); - pendingBlocks.add(new PendingBlock(blockNumber, blockProofBuilder, writer)); + pendingBlocks.add(new PendingBlock( + blockNumber, + blockHash, + pendingProof, + writer, + new MerkleSiblingHash(false, inputHash), + new MerkleSiblingHash(false, rightParent))); // Update in-memory state to prepare for the next block lastBlockHash = blockHash; writer = null; - // Simulate the completion of the block proof - final long blockNumberToComplete = this.blockNumber; - CompletableFuture.runAsync( - () -> { - try { - finishBlockProof(blockNumberToComplete, Bytes.wrap(new byte[48])); - } catch (Exception e) { - log.error("Failed to finish proof for block {}", blockNumberToComplete, e); - } - }, - executor); - } - } - - /** - * {@inheritDoc} - * Synchronized to ensure that block proofs are always written in order, even in edge cases where multiple - * pending block proofs become available at the same time. - * @param blockNumber the number of the block to finish - * @param signature the signature to use in the block proof - */ - @Override - public synchronized void finishBlockProof(final long blockNumber, @NonNull final Bytes signature) { - requireNonNull(signature); - while (!pendingBlocks.isEmpty() && pendingBlocks.peek().blockNumber() <= blockNumber) { - final var block = pendingBlocks.poll(); - // Note the actual proof for an earlier block number awaiting proof will be more complicated than this - final var proof = block.proofBuilder().blockSignature(signature).build(); - block.writer() - .writeItem(BlockItem.PROTOBUF.toBytes( - BlockItem.newBuilder().blockProof(proof).build())) - .closeBlock(); + tssBaseService.requestLedgerSignature(blockHash.toByteArray()); } } @@ -244,6 +245,10 @@ public void writeItem(@NonNull final BlockItem item) { @Override public @Nullable Bytes prngSeed() { + // Incorporate all pending results before returning the seed to guarantee + // no two consecutive transactions ever get the same seed + schedulePendingWork(); + writeFuture.join(); final var seed = runningHashManager.nMinus3HashFuture.join(); return seed == null ? null : Bytes.wrap(seed); } @@ -263,6 +268,59 @@ public long blockNo() { return blockHashManager.hashOfBlock(blockNo); } + /** + * Synchronized to ensure that block proofs are always written in order, even in edge cases where multiple + * pending block proofs become available at the same time. + * + * @param message the number of the block to finish + * @param signature the signature to use in the block proof + */ + @Override + public synchronized void accept(@NonNull final byte[] message, @NonNull final byte[] signature) { + // Find the block whose hash as the signed message, tracking any sibling hashes + // needed for indirect proofs of earlier blocks along the way + long blockNumber = Long.MIN_VALUE; + boolean impliesIndirectProof = false; + final List> siblingHashes = new ArrayList<>(); + final var blockHash = Bytes.wrap(message); + for (final var block : pendingBlocks) { + if (impliesIndirectProof) { + siblingHashes.add(List.of(block.siblingHashes())); + } + if (block.blockHash().equals(blockHash)) { + blockNumber = block.number(); + break; + } + impliesIndirectProof = true; + } + if (blockNumber == Long.MIN_VALUE) { + log.info("Ignoring signature on already proven block hash '{}'", blockHash); + return; + } + // Write proofs for all pending blocks up to and including the signed block number + final var blockSignature = Bytes.wrap(signature); + while (!pendingBlocks.isEmpty() && pendingBlocks.peek().number() <= blockNumber) { + final var block = pendingBlocks.poll(); + final var proof = block.proofBuilder() + .blockSignature(blockSignature) + .siblingHashes(siblingHashes.stream().flatMap(List::stream).toList()); + block.writer() + .writeItem(BlockItem.PROTOBUF.toBytes( + BlockItem.newBuilder().blockProof(proof).build())) + .closeBlock(); + if (block.number() != blockNumber) { + siblingHashes.removeFirst(); + } + } + } + + /** + * (FUTURE) Remove this after reconnect protocol also transmits the last block hash. + */ + public void appendRealHashes() { + this.appendRealHashes = true; + } + private void schedulePendingWork() { final var scheduledWork = new ScheduledWork(pendingItems); final var pendingSerialization = CompletableFuture.supplyAsync(scheduledWork::serializeItems, executor); @@ -270,16 +328,6 @@ private void schedulePendingWork() { pendingItems = new ArrayList<>(); } - private Bytes computeBlockHash( - @NonNull final Bytes prevBlockHash, - @NonNull final Bytes inputRootHash, - @NonNull final Bytes outputRootHash, - @NonNull final Bytes stateRootHash) { - final var leftParent = combine(prevBlockHash.toByteArray(), inputRootHash.toByteArray()); - final var rightParent = combine(outputRootHash.toByteArray(), stateRootHash.toByteArray()); - return Bytes.wrap(combine(leftParent, rightParent)); - } - private @NonNull BlockStreamInfo blockStreamInfoFrom(@NonNull final State state) { final var blockStreamInfoState = state.getReadableStates(BlockStreamService.NAME).getSingleton(BLOCK_STREAM_INFO_KEY); @@ -290,10 +338,11 @@ private boolean shouldCloseBlock(final long roundNumber, final int roundsPerBloc return roundNumber % roundsPerBlock == 0 || roundNumber == freezeRoundNumber; } - private boolean isFreezeRound(@NonNull final State state, @NonNull final Round round) { - final var platformState = new ReadablePlatformStateStore(state.getReadableStates(PlatformStateService.NAME)); + private boolean isFreezeRound(@NonNull final PlatformState platformState, @NonNull final Round round) { return isInFreezePeriod( - round.getConsensusTimestamp(), platformState.getFreezeTime(), platformState.getLastFrozenTime()); + round.getConsensusTimestamp(), + platformState.freezeTime() == null ? null : asInstant(platformState.freezeTime()), + platformState.lastFrozenTime() == null ? null : asInstant(platformState.lastFrozenTime())); } /** @@ -361,18 +410,6 @@ public Void combineSerializedItems(@Nullable Void ignore, @NonNull final List + *
  • We never know the hash of the {@code N+1} block currently being created.
  • + *
  • We start every block {@code N} by concatenating the {@code N-1} block hash to the trailing + * hashes up to block {@code N-2} that were in state at the end of block {@code N-1}. + * * * @param blockNo the block number * @return the hash of the block with the given number, or null if it is not available diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/schemas/V0540BlockStreamSchema.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/schemas/V0540BlockStreamSchema.java index 2f248cf545d6..5659885d460b 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/schemas/V0540BlockStreamSchema.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/schemas/V0540BlockStreamSchema.java @@ -17,6 +17,7 @@ package com.hedera.node.app.blocks.schemas; import static com.hedera.node.app.blocks.impl.BlockImplUtils.appendHash; +import static com.hedera.node.app.records.impl.BlockRecordInfoUtils.blockHashByBlockNumber; import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.SemanticVersion; @@ -30,25 +31,20 @@ import com.swirlds.state.spi.StateDefinition; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.Set; +import java.util.function.Consumer; /** - * Defines the schema for two forms of state, + * Defines the schema for state with two notable properties: *
      - *
    1. State needed for a new or reconnected node to construct the next block exactly as will + *
    2. It is needed for a new or reconnected node to construct the next block exactly as will * nodes already in the network.
    3. - *
    4. State derived from the block stream, and hence the natural provenance of the same service + *
    5. It is derived from the block stream, and hence the natural provenance of the same service * that is managing and producing blocks.
    6. *
    *

    - * The two pieces of state in the first category are, + * The particular items with these properties are, *

      *
    1. The number of the last completed block, which each node must increment in the next block.
    2. - *
    3. The hash of the last completed block, which each node must include in the header and proof - * of the next block.
    4. - *
    - *

    - * State in the second category has three parts, - *

      *
    1. The first consensus time of the last finished block, for comparison with the consensus * time at the start of the current block. Depending on the elapsed period between these times, * the network may deterministically choose to purge expired entities, adjust node stakes and @@ -69,11 +65,14 @@ public class V0540BlockStreamSchema extends Schema { private static final SemanticVersion VERSION = SemanticVersion.newBuilder().major(0).minor(54).patch(0).build(); + private final Consumer migratedBlockHashConsumer; + /** * Schema constructor. */ - public V0540BlockStreamSchema() { + public V0540BlockStreamSchema(@NonNull final Consumer migratedBlockHashConsumer) { super(VERSION); + this.migratedBlockHashConsumer = requireNonNull(migratedBlockHashConsumer); } @Override @@ -94,10 +93,22 @@ public void migrate(@NonNull final MigrationContext ctx) { (BlockInfo) requireNonNull(ctx.sharedValues().get(SHARED_BLOCK_RECORD_INFO)); final RunningHashes runningHashes = (RunningHashes) requireNonNull(ctx.sharedValues().get(SHARED_RUNNING_HASHES)); + // Note that it is impossible to put the hash of block N into a state that includes + // the state changes from block N, because the hash of block N is a function of exactly + // those state changes---so act of putting the hash in state would change it; as a result, + // the correct way to migrate from a record stream-based state is to save its last + // block hash as the last block hash of the new state; and create a BlockStreamInfo with + // the remaining block hashes + final var lastBlockHash = + requireNonNull(blockHashByBlockNumber(blockInfo, blockInfo.lastBlockNumber())); + migratedBlockHashConsumer.accept(lastBlockHash); + final var trailingBlockHashes = blockInfo + .blockHashes() + .slice(lastBlockHash.length(), blockInfo.blockHashes().length() - lastBlockHash.length()); state.put(BlockStreamInfo.newBuilder() .blockTime(blockInfo.firstConsTimeOfLastBlock()) .blockNumber(blockInfo.lastBlockNumber()) - .trailingBlockHashes(blockInfo.blockHashes()) + .trailingBlockHashes(trailingBlockHashes) .trailingOutputHashes(appendedHashes(runningHashes)) .build()); } @@ -105,10 +116,10 @@ public void migrate(@NonNull final MigrationContext ctx) { } private Bytes appendedHashes(final RunningHashes runningHashes) { - Bytes appendedHashes = Bytes.EMPTY; - appendedHashes = appendHash(runningHashes.nMinus3RunningHash(), appendedHashes, 4); - appendedHashes = appendHash(runningHashes.nMinus2RunningHash(), appendedHashes, 4); - appendedHashes = appendHash(runningHashes.nMinus1RunningHash(), appendedHashes, 4); - return appendHash(runningHashes.runningHash(), appendedHashes, 4); + var hashes = Bytes.EMPTY; + hashes = appendHash(runningHashes.nMinus3RunningHash(), hashes, 4); + hashes = appendHash(runningHashes.nMinus2RunningHash(), hashes, 4); + hashes = appendHash(runningHashes.nMinus1RunningHash(), hashes, 4); + return appendHash(runningHashes.runningHash(), hashes, 4); } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssBaseService.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssBaseService.java new file mode 100644 index 000000000000..b131df78c200 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssBaseService.java @@ -0,0 +1,61 @@ +/* + * 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.hedera.node.app.tss; + +import com.swirlds.state.spi.Service; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.function.BiConsumer; + +/** + * The TssBaseService will attempt to generate TSS key material for any set candidate roster, giving it a ledger id and + * the ability to generate ledger signatures that can be verified by the ledger id. Once the candidate roster has + * received its full TSS key material, it can be made available for adoption by the platform. + *

      + * The TssBaseService will also attempt to generate ledger signatures by aggregating share signatures produced by + * calling {@link #requestLedgerSignature(byte[])}. + */ +public interface TssBaseService extends Service { + String NAME = "TssBaseService"; + + @NonNull + @Override + default String getServiceName() { + return NAME; + } + + /** + * Requests a ledger signature on a message hash. The ledger signature is computed asynchronously and returned + * to all consumers that have been registered through {@link #registerLedgerSignatureConsumer}. + * + * @param messageHash The hash of the message to be signed by the ledger. + */ + void requestLedgerSignature(@NonNull byte[] messageHash); + + /** + * Registers a consumer of the message hash and the ledger signature on the message hash. + * + * @param consumer the consumer of ledger signatures and message hashes. + */ + void registerLedgerSignatureConsumer(@NonNull BiConsumer consumer); + + /** + * Unregisters a consumer of the message hash and the ledger signature on the message hash. + * + * @param consumer the consumer of ledger signatures and message hashes to unregister. + */ + void unregisterLedgerSignatureConsumer(@NonNull BiConsumer consumer); +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/impl/PlaceholderTssBaseService.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/impl/PlaceholderTssBaseService.java new file mode 100644 index 000000000000..6c0017c91a1d --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/impl/PlaceholderTssBaseService.java @@ -0,0 +1,94 @@ +/* + * 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.hedera.node.app.tss.impl; + +import static com.hedera.node.app.hapi.utils.CommonUtils.noThrowSha384HashOf; +import static java.util.Objects.requireNonNull; + +import com.hedera.node.app.tss.TssBaseService; +import com.swirlds.common.utility.CommonUtils; +import com.swirlds.state.spi.SchemaRegistry; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; +import java.util.function.BiConsumer; +import javax.inject.Inject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Placeholder for the TSS base service, added to support testing production of indirect block proofs, + * c.f. this issue. + */ +public class PlaceholderTssBaseService implements TssBaseService { + private static final Logger log = LogManager.getLogger(PlaceholderTssBaseService.class); + + /** + * Copy-on-write list to avoid concurrent modification exceptions if a consumer unregisters + * itself in its callback. + */ + private final List> consumers = new CopyOnWriteArrayList<>(); + + private ExecutorService executor; + + @Inject + public void setExecutor(@NonNull final ExecutorService executor) { + this.executor = requireNonNull(executor); + } + + @Override + public void registerSchemas(@NonNull final SchemaRegistry registry) { + // FUTURE - add required schemas + } + + @Override + public void requestLedgerSignature(@NonNull final byte[] messageHash) { + requireNonNull(messageHash); + requireNonNull(executor); + // The "signature" is a hash of the message hash + final var mockSignature = noThrowSha384HashOf(messageHash); + // Simulate asynchronous completion of the ledger signature + CompletableFuture.runAsync( + () -> consumers.forEach(consumer -> { + try { + consumer.accept(messageHash, mockSignature); + } catch (Exception e) { + log.error( + "Failed to provide signature {} on message {} to consumer {}", + CommonUtils.hex(mockSignature), + CommonUtils.hex(messageHash), + consumer, + e); + } + }), + executor); + } + + @Override + public void registerLedgerSignatureConsumer(@NonNull final BiConsumer consumer) { + requireNonNull(consumer); + consumers.add(consumer); + } + + @Override + public void unregisterLedgerSignatureConsumer(@NonNull final BiConsumer consumer) { + requireNonNull(consumer); + consumers.remove(consumer); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/version/HederaSoftwareVersion.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/version/HederaSoftwareVersion.java index ae87b7764428..c8f44252f68c 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/version/HederaSoftwareVersion.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/version/HederaSoftwareVersion.java @@ -39,7 +39,9 @@ * completely different from each other. * *

      The Services version is the version of the node software itself. + * This will be removed once we stop supporting 0.53.0 and earlier versions. */ +@Deprecated(forRemoval = true) public class HederaSoftwareVersion implements SoftwareVersion { public static final long CLASS_ID = 0x6f2b1bc2df8cbd0cL; 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 96e414535a47..738b27c3ce5d 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 @@ -363,7 +363,8 @@ private HandleOutput execute(@NonNull final UserTxn userTxn) { dispatchProcessor.processDispatch(dispatch); updateWorkflowMetrics(userTxn); } - final var handleOutput = userTxn.stack().buildHandleOutput(userTxn.consensusNow()); + final var handleOutput = + userTxn.stack().buildHandleOutput(userTxn.consensusNow(), exchangeRateManager.exchangeRates()); // Note that we don't yet support producing ONLY blocks, because we haven't integrated // translators from block items to records for answering queries if (blockStreamConfig.streamRecords()) { diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/stack/SavepointStackImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/stack/SavepointStackImpl.java index 63c6ad4a8bba..78dd643535c2 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/stack/SavepointStackImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/stack/SavepointStackImpl.java @@ -29,6 +29,7 @@ import com.hedera.hapi.block.stream.BlockItem; import com.hedera.hapi.node.base.TransactionID; +import com.hedera.hapi.node.transaction.ExchangeRateSet; import com.hedera.node.app.blocks.impl.BoundaryStateChangeListener; import com.hedera.node.app.blocks.impl.KVStateChangeListener; import com.hedera.node.app.blocks.impl.PairedStreamBuilder; @@ -59,8 +60,6 @@ import java.util.List; import java.util.Map; import java.util.function.Consumer; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; /** * A stack of savepoints scoped to a dispatch. Each savepoint captures the state of the {@link State} at the time @@ -68,7 +67,6 @@ * the stream builders created in the savepoint. */ public class SavepointStackImpl implements HandleContext.SavepointStack, State { - private static final Logger log = LogManager.getLogger(SavepointStackImpl.class); private final State state; private final Deque stack = new ArrayDeque<>(); private final Map writableStatesMap = new HashMap<>(); @@ -444,9 +442,11 @@ Savepoint peek() { * Builds all the records for the user transaction. * * @param consensusTime consensus time of the transaction + * @param exchangeRates the active exchange rates * @return the stream of records */ - public HandleOutput buildHandleOutput(@NonNull final Instant consensusTime) { + public HandleOutput buildHandleOutput( + @NonNull final Instant consensusTime, @NonNull final ExchangeRateSet exchangeRates) { final List blockItems; Instant lastAssignedConsenusTime = consensusTime; if (streamMode == RECORDS) { @@ -481,10 +481,16 @@ public HandleOutput buildHandleOutput(@NonNull final Instant consensusTime) { final var consensusNow = consensusTime.plusNanos((long) i - indexOfUserRecord); lastAssignedConsenusTime = consensusNow; builder.consensusTimestamp(consensusNow); - if (i > indexOfUserRecord && builder.category() != SCHEDULED) { - // Only set exchange rates on transactions preceding the user transaction, since - // no subsequent child can change the exchange rate - builder.parentConsensus(consensusTime).exchangeRate(null); + if (i > indexOfUserRecord) { + if (builder.category() != SCHEDULED) { + // Only set exchange rates on transactions preceding the user transaction, since + // no subsequent child can change the exchange rate + builder.parentConsensus(consensusTime).exchangeRate(null); + } else { + // But for backward compatibility keep setting rates on scheduled receipts, c.f. + // https://github.com/hashgraph/hedera-services/issues/15393 + builder.exchangeRate(exchangeRates); + } } switch (streamMode) { case RECORDS -> records.add(((RecordStreamBuilder) builder).build()); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/ExecutorComponent.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/ExecutorComponent.java index 7b50038e619f..1eb3dc722971 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/ExecutorComponent.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/ExecutorComponent.java @@ -19,6 +19,7 @@ import com.hedera.node.app.authorization.AuthorizerInjectionModule; import com.hedera.node.app.config.BootstrapConfigProviderImpl; import com.hedera.node.app.config.ConfigProviderImpl; +import com.hedera.node.app.fees.ExchangeRateManager; import com.hedera.node.app.service.contract.impl.ContractServiceImpl; import com.hedera.node.app.service.file.impl.FileServiceImpl; import com.hedera.node.app.services.ServicesInjectionModule; @@ -81,5 +82,7 @@ interface Builder { StateNetworkInfo stateNetworkInfo(); + ExchangeRateManager exchangeRateManager(); + StandaloneDispatchFactory standaloneDispatchFactory(); } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/TransactionExecutors.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/TransactionExecutors.java index 7f09f43fb1dd..fd2d7df81f39 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/TransactionExecutors.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/TransactionExecutors.java @@ -16,6 +16,8 @@ package com.hedera.node.app.workflows.standalone; +import static com.hedera.node.app.workflows.standalone.impl.NoopVerificationStrategies.NOOP_VERIFICATION_STRATEGIES; + import com.hedera.node.app.config.BootstrapConfigProviderImpl; import com.hedera.node.app.config.ConfigProviderImpl; import com.hedera.node.app.service.contract.impl.ContractServiceImpl; @@ -52,11 +54,14 @@ public TransactionExecutor newExecutor(@NonNull final State state, @NonNull fina final var executor = newExecutorComponent(properties); executor.initializer().accept(state); executor.stateNetworkInfo().initFrom(state); + final var exchangeRateManager = executor.exchangeRateManager(); return (transactionBody, consensusNow, operationTracers) -> { final var dispatch = executor.standaloneDispatchFactory().newDispatch(state, transactionBody, consensusNow); OPERATION_TRACERS.set(List.of(operationTracers)); executor.dispatchProcessor().processDispatch(dispatch); - return dispatch.stack().buildHandleOutput(consensusNow).recordsOrThrow(); + return dispatch.stack() + .buildHandleOutput(consensusNow, exchangeRateManager.exchangeRates()) + .recordsOrThrow(); }; } @@ -68,7 +73,8 @@ private ExecutorComponent newExecutorComponent(@NonNull final Map Decision.VALID; + + @Override + public VerificationStrategy activatingOnlyContractKeysFor( + @NonNull final Address sender, + final boolean requiresDelegatePermission, + @NonNull final HederaNativeOperations nativeOperations) { + return NOOP_VERIFICATION_STRATEGY; + } +} 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 dc8c5c2fcab5..eeeb435abd80 100644 --- a/hedera-node/hedera-app/src/main/java/module-info.java +++ b/hedera-node/hedera-app/src/main/java/module-info.java @@ -41,6 +41,7 @@ requires com.swirlds.merkledb; requires com.swirlds.virtualmap; requires com.google.common; + requires com.google.errorprone.annotations; requires com.google.protobuf; requires io.grpc.netty; requires io.grpc; @@ -48,6 +49,7 @@ requires io.netty.transport.classes.epoll; requires io.netty.transport; requires org.apache.commons.lang3; + requires org.hyperledger.besu.datatypes; requires static com.github.spotbugs.annotations; requires static com.google.auto.service; requires static java.compiler; @@ -106,6 +108,8 @@ exports com.hedera.node.app.blocks.impl; exports com.hedera.node.app.workflows.handle.metric; exports com.hedera.node.app.roster; + exports com.hedera.node.app.tss; + exports com.hedera.node.app.tss.impl; provides ConfigurationExtension with ServicesConfigExtension; 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 cbb7c9add4e0..b366ba087f40 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 @@ -20,21 +20,36 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.instanceOf; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mock.Strictness.LENIENT; import static org.mockito.Mockito.mockStatic; import com.hedera.node.app.version.ServicesSoftwareVersion; +import com.swirlds.common.context.PlatformContext; +import com.swirlds.common.crypto.Cryptography; +import com.swirlds.common.io.filesystem.FileSystemManager; +import com.swirlds.common.io.utility.RecycleBin; +import com.swirlds.common.merkle.crypto.MerkleCryptography; +import com.swirlds.common.metrics.platform.DefaultMetricsProvider; import com.swirlds.common.platform.NodeId; +import com.swirlds.config.api.Configuration; +import com.swirlds.metrics.api.Metrics; +import com.swirlds.platform.builder.PlatformBuilder; import com.swirlds.platform.config.legacy.ConfigurationException; import com.swirlds.platform.config.legacy.LegacyConfigProperties; import com.swirlds.platform.config.legacy.LegacyConfigPropertiesLoader; import com.swirlds.platform.state.MerkleStateRoot; +import com.swirlds.platform.state.signed.ReservedSignedState; +import com.swirlds.platform.state.signed.SignedState; +import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.SystemExitUtils; +import com.swirlds.platform.system.address.AddressBook; import com.swirlds.platform.util.BootstrapUtils; import java.util.ArrayList; import java.util.List; -import org.junit.jupiter.api.Assertions; +import java.util.function.BiFunction; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -47,9 +62,48 @@ final class ServicesMainTest { mockStatic(LegacyConfigPropertiesLoader.class); private static final MockedStatic bootstrapUtilsMockedStatic = mockStatic(BootstrapUtils.class); - @Mock + @Mock(strictness = LENIENT) private LegacyConfigProperties legacyConfigProperties; + @Mock(strictness = LENIENT) + private AddressBook addressBook; + + @Mock(strictness = LENIENT) + private DefaultMetricsProvider metricsProvider; + + @Mock(strictness = LENIENT) + private Metrics metrics; + + @Mock(strictness = LENIENT) + private FileSystemManager fileSystemManager; + + @Mock(strictness = LENIENT) + private RecycleBin recycleBin; + + @Mock(strictness = LENIENT) + private MerkleCryptography merkleCryptography; + + @Mock(strictness = LENIENT) + BiFunction merkleCryptographyFn; + + @Mock(strictness = LENIENT) + private PlatformContext platformContext; + + @Mock(strictness = LENIENT) + private PlatformBuilder platformBuilder; + + @Mock(strictness = LENIENT) + private ReservedSignedState reservedSignedState; + + @Mock(strictness = LENIENT) + private SignedState signedState; + + @Mock(strictness = LENIENT) + private Platform platform; + + @Mock(strictness = LENIENT) + private Hedera hedera; + private final ServicesMain subject = new ServicesMain(); // no local nodes specified but more than one match in address book @@ -102,7 +156,7 @@ void returnsSerializableVersion() { @Test void noopsAsExpected() { // expect: - Assertions.assertDoesNotThrow(subject::run); + assertDoesNotThrow(subject::run); } @Test diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/BlockStreamServiceTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/BlockStreamServiceTest.java index 28e6cc7d06e0..d164fc526ab2 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/BlockStreamServiceTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/BlockStreamServiceTest.java @@ -16,83 +16,62 @@ package com.hedera.node.app.blocks; -import static com.hedera.node.app.blocks.schemas.V0540BlockStreamSchema.BLOCK_STREAM_INFO_KEY; +import static com.hedera.node.app.fixtures.AppTestBase.DEFAULT_CONFIG; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verifyNoInteractions; -import com.hedera.hapi.node.state.blockstream.BlockStreamInfo; import com.hedera.node.app.blocks.schemas.V0540BlockStreamSchema; import com.hedera.node.config.testfixtures.HederaTestConfigBuilder; -import com.swirlds.config.api.Configuration; -import com.swirlds.state.spi.MigrationContext; -import com.swirlds.state.spi.Schema; import com.swirlds.state.spi.SchemaRegistry; -import com.swirlds.state.spi.StateDefinition; -import com.swirlds.state.spi.WritableSingletonState; -import com.swirlds.state.spi.WritableStates; -import java.util.Set; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -@SuppressWarnings({"rawtypes", "unchecked"}) @ExtendWith(MockitoExtension.class) final class BlockStreamServiceTest { - @Mock(strictness = LENIENT) + @Mock private SchemaRegistry schemaRegistry; - @Mock(strictness = LENIENT) - private MigrationContext migrationContext; + private BlockStreamService subject; - @Mock(strictness = LENIENT) - private WritableSingletonState blockStreamState; - - @Mock(strictness = LENIENT) - private WritableStates writableStates; + @Test + void serviceNameAsExpected() { + givenDisabledSubject(); - public static final Configuration DEFAULT_CONFIG = HederaTestConfigBuilder.createConfig(); + assertThat(subject.getServiceName()).isEqualTo("BlockStreamService"); + } @Test - void testGetServiceName() { - BlockStreamService blockRecordService = new BlockStreamService(DEFAULT_CONFIG); - assertEquals(BlockStreamService.NAME, blockRecordService.getServiceName()); + void enabledSubjectRegistersV0540Schema() { + givenEnabledSubject(); + + subject.registerSchemas(schemaRegistry); + + verify(schemaRegistry).register(argThat(s -> s instanceof V0540BlockStreamSchema)); } @Test - void testRegisterSchemas() { - when(schemaRegistry.register(any())).then(invocation -> { - Object[] args = invocation.getArguments(); - assertEquals(1, args.length); - Schema schema = (Schema) args[0]; - assertThat(schema).isInstanceOf(V0540BlockStreamSchema.class); - Set states = schema.statesToCreate(DEFAULT_CONFIG); - assertEquals(1, states.size()); - assertTrue(states.contains(StateDefinition.singleton(BLOCK_STREAM_INFO_KEY, BlockStreamInfo.PROTOBUF))); - - when(migrationContext.newStates()).thenReturn(writableStates); - when(migrationContext.previousVersion()).thenReturn(null); - when(writableStates.getSingleton(BLOCK_STREAM_INFO_KEY)).thenReturn(blockStreamState); - - // FINISH: - ArgumentCaptor blockInfoCapture = ArgumentCaptor.forClass(BlockStreamInfo.class); - - schema.migrate(migrationContext); - - verify(blockStreamState).put(blockInfoCapture.capture()); - assertEquals(BlockStreamInfo.DEFAULT, blockInfoCapture.getValue()); - return null; - }); + void disabledSubjectDoesNotRegisterSchema() { + givenDisabledSubject(); + + subject.registerSchemas(schemaRegistry); + + verifyNoInteractions(schemaRegistry); + + assertThat(subject.migratedLastBlockHash()).isEmpty(); + } + + private void givenEnabledSubject() { final var testConfig = HederaTestConfigBuilder.create() .withValue("blockStream.streamMode", "BOTH") .getOrCreateConfig(); - BlockStreamService blockStreamService = new BlockStreamService(testConfig); - blockStreamService.registerSchemas(schemaRegistry); + subject = new BlockStreamService(testConfig); + } + + private void givenDisabledSubject() { + subject = new BlockStreamService(DEFAULT_CONFIG); } } diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/impl/BlockStreamManagerImplTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/impl/BlockStreamManagerImplTest.java new file mode 100644 index 000000000000..723c40667b4f --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/impl/BlockStreamManagerImplTest.java @@ -0,0 +1,432 @@ +/* + * 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.hedera.node.app.blocks.impl; + +import static com.hedera.hapi.util.HapiUtils.asTimestamp; +import static com.hedera.node.app.blocks.BlockStreamManager.ZERO_BLOCK_HASH; +import static com.hedera.node.app.blocks.BlockStreamService.FAKE_RESTART_BLOCK_HASH; +import static com.hedera.node.app.blocks.impl.BlockImplUtils.appendHash; +import static com.hedera.node.app.blocks.impl.BlockImplUtils.combine; +import static com.hedera.node.app.blocks.schemas.V0540BlockStreamSchema.BLOCK_STREAM_INFO_KEY; +import static com.hedera.node.app.fixtures.AppTestBase.DEFAULT_CONFIG; +import static com.hedera.node.app.hapi.utils.CommonUtils.noThrowSha384HashOf; +import static com.swirlds.platform.state.service.schemas.V0540PlatformStateSchema.PLATFORM_STATE_KEY; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.withSettings; + +import com.hedera.hapi.block.stream.BlockItem; +import com.hedera.hapi.block.stream.RecordFileItem; +import com.hedera.hapi.block.stream.output.StateChanges; +import com.hedera.hapi.block.stream.output.TransactionResult; +import com.hedera.hapi.node.base.SemanticVersion; +import com.hedera.hapi.node.state.blockstream.BlockStreamInfo; +import com.hedera.hapi.platform.event.EventTransaction; +import com.hedera.hapi.platform.state.PlatformState; +import com.hedera.node.app.blocks.BlockItemWriter; +import com.hedera.node.app.blocks.BlockStreamService; +import com.hedera.node.app.tss.TssBaseService; +import com.hedera.node.config.ConfigProvider; +import com.hedera.node.config.VersionedConfigImpl; +import com.hedera.node.config.testfixtures.HederaTestConfigBuilder; +import com.hedera.pbj.runtime.ParseException; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.platform.state.service.PlatformStateService; +import com.swirlds.platform.system.Round; +import com.swirlds.state.State; +import com.swirlds.state.spi.CommittableWritableStates; +import com.swirlds.state.spi.ReadableStates; +import com.swirlds.state.spi.WritableSingletonStateBase; +import com.swirlds.state.spi.WritableStates; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.time.Instant; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class BlockStreamManagerImplTest { + private static final SemanticVersion CREATION_VERSION = new SemanticVersion(1, 2, 3, "alpha.1", "2"); + private static final long ROUND_NO = 123L; + private static final long N_MINUS_2_BLOCK_NO = 664L; + private static final long N_MINUS_1_BLOCK_NO = 665L; + private static final long N_BLOCK_NO = 666L; + private static final Instant CONSENSUS_NOW = Instant.ofEpochSecond(1_234_567L); + private static final Bytes N_MINUS_2_BLOCK_HASH = Bytes.wrap(noThrowSha384HashOf(new byte[] {(byte) 0xAA})); + private static final Bytes FIRST_FAKE_SIGNATURE = Bytes.fromHex("ff".repeat(48)); + private static final Bytes SECOND_FAKE_SIGNATURE = Bytes.fromHex("ee".repeat(48)); + private static final BlockItem FAKE_EVENT_TRANSACTION = + BlockItem.newBuilder().eventTransaction(EventTransaction.DEFAULT).build(); + private static final BlockItem FAKE_TRANSACTION_RESULT = + BlockItem.newBuilder().transactionResult(TransactionResult.DEFAULT).build(); + private static final Bytes FAKE_RESULT_HASH = noThrowSha384HashOfItem(FAKE_TRANSACTION_RESULT); + private static final BlockItem FAKE_STATE_CHANGES = + BlockItem.newBuilder().stateChanges(StateChanges.DEFAULT).build(); + private static final BlockItem FAKE_RECORD_FILE_ITEM = + BlockItem.newBuilder().recordFile(RecordFileItem.DEFAULT).build(); + + @Mock + private TssBaseService tssBaseService; + + @Mock + private ConfigProvider configProvider; + + @Mock + private BoundaryStateChangeListener boundaryStateChangeListener; + + @Mock + private BlockItemWriter aWriter; + + @Mock + private BlockItemWriter bWriter; + + @Mock + private ReadableStates readableStates; + + private WritableStates writableStates; + + @Mock + private Round round; + + @Mock + private State state; + + private final AtomicReference lastAItem = new AtomicReference<>(); + private final AtomicReference lastBItem = new AtomicReference<>(); + private final AtomicReference stateRef = new AtomicReference<>(); + private final AtomicReference infoRef = new AtomicReference<>(); + + private WritableSingletonStateBase blockStreamInfoState; + + private BlockStreamManagerImpl subject; + + @BeforeEach + void setUp() { + writableStates = mock(WritableStates.class, withSettings().extraInterfaces(CommittableWritableStates.class)); + } + + @Test + void requiresLastHashToBeInitialized() { + given(configProvider.getConfiguration()).willReturn(new VersionedConfigImpl(DEFAULT_CONFIG, 1)); + subject = new BlockStreamManagerImpl( + () -> aWriter, ForkJoinPool.commonPool(), configProvider, tssBaseService, boundaryStateChangeListener); + assertThrows(IllegalStateException.class, () -> subject.startRound(round, state)); + } + + @Test + void startsAndEndsBlockWithSingleRoundPerBlockAsExpected() throws ParseException { + givenSubjectWith( + 1, + blockStreamInfoWith(N_MINUS_1_BLOCK_NO, N_MINUS_2_BLOCK_HASH, Bytes.EMPTY), + platformStateWith(null), + aWriter); + givenEndOfRoundSetup(); + final ArgumentCaptor blockHashCaptor = ArgumentCaptor.forClass(byte[].class); + + // Initialize the last (N-1) block hash + subject.initLastBlockHash(FAKE_RESTART_BLOCK_HASH); + + // Start the round that will be block N + subject.startRound(round, state); + + // Assert the internal state of the subject has changed as expected and the writer has been opened + verify(boundaryStateChangeListener).setLastUsedConsensusTime(CONSENSUS_NOW); + verify(aWriter).openBlock(N_BLOCK_NO); + assertEquals(N_MINUS_2_BLOCK_HASH, subject.blockHashByBlockNumber(N_MINUS_2_BLOCK_NO)); + assertEquals(FAKE_RESTART_BLOCK_HASH, subject.blockHashByBlockNumber(N_MINUS_1_BLOCK_NO)); + assertNull(subject.prngSeed()); + assertEquals(N_BLOCK_NO, subject.blockNo()); + + // Write some items to the block + subject.writeItem(FAKE_EVENT_TRANSACTION); + subject.writeItem(FAKE_TRANSACTION_RESULT); + subject.writeItem(FAKE_STATE_CHANGES); + subject.writeItem(FAKE_RECORD_FILE_ITEM); + + // End the round + subject.endRound(state, ROUND_NO); + + // Assert the internal state of the subject has changed as expected and the writer has been closed + final var expectedBlockInfo = new BlockStreamInfo( + N_BLOCK_NO, + asTimestamp(CONSENSUS_NOW), + appendHash(combine(ZERO_BLOCK_HASH, FAKE_RESULT_HASH), appendHash(ZERO_BLOCK_HASH, Bytes.EMPTY, 4), 4), + appendHash(FAKE_RESTART_BLOCK_HASH, appendHash(N_MINUS_2_BLOCK_HASH, Bytes.EMPTY, 256), 256)); + final var actualBlockInfo = infoRef.get(); + assertEquals(expectedBlockInfo, actualBlockInfo); + verify(tssBaseService).requestLedgerSignature(blockHashCaptor.capture()); + + // Provide the ledger signature to the subject + subject.accept(blockHashCaptor.getValue(), FIRST_FAKE_SIGNATURE.toByteArray()); + + // Assert the block proof was written + final var proofItem = lastAItem.get(); + assertNotNull(proofItem); + final var item = BlockItem.PROTOBUF.parse(proofItem); + assertTrue(item.hasBlockProof()); + final var proof = item.blockProofOrThrow(); + assertEquals(N_BLOCK_NO, proof.block()); + assertEquals(FIRST_FAKE_SIGNATURE, proof.blockSignature()); + } + + @Test + void doesNotEndBlockWithMultipleRoundPerBlockIfNotModZero() { + givenSubjectWith( + 7, + blockStreamInfoWith(N_MINUS_1_BLOCK_NO, N_MINUS_2_BLOCK_HASH, Bytes.EMPTY), + platformStateWith(null), + aWriter); + + // Initialize the last (N-1) block hash + subject.initLastBlockHash(FAKE_RESTART_BLOCK_HASH); + + // Start the round that will be block N + subject.startRound(round, state); + + // Assert the internal state of the subject has changed as expected and the writer has been opened + verify(boundaryStateChangeListener).setLastUsedConsensusTime(CONSENSUS_NOW); + verify(aWriter).openBlock(N_BLOCK_NO); + assertEquals(N_MINUS_2_BLOCK_HASH, subject.blockHashByBlockNumber(N_MINUS_2_BLOCK_NO)); + assertEquals(FAKE_RESTART_BLOCK_HASH, subject.blockHashByBlockNumber(N_MINUS_1_BLOCK_NO)); + + // Write some items to the block + subject.writeItem(FAKE_EVENT_TRANSACTION); + subject.writeItem(FAKE_TRANSACTION_RESULT); + subject.writeItem(FAKE_STATE_CHANGES); + subject.writeItem(FAKE_RECORD_FILE_ITEM); + + // End the round + subject.endRound(state, ROUND_NO); + + // Assert the internal state of the subject has changed as expected and the writer has been closed + verify(tssBaseService, never()).requestLedgerSignature(any()); + } + + @Test + void alwaysEndsBlockOnFreezeRoundPerBlockAsExpected() throws ParseException { + final var resultHashes = Bytes.fromHex("aa".repeat(48) + "bb".repeat(48) + "cc".repeat(48) + "dd".repeat(48)); + givenSubjectWith( + 7, + blockStreamInfoWith(N_MINUS_1_BLOCK_NO, N_MINUS_2_BLOCK_HASH, resultHashes), + platformStateWith(CONSENSUS_NOW.minusSeconds(1)), + aWriter); + givenEndOfRoundSetup(); + given(round.getRoundNum()).willReturn(ROUND_NO); + final ArgumentCaptor blockHashCaptor = ArgumentCaptor.forClass(byte[].class); + + // Initialize the last (N-1) block hash + subject.initLastBlockHash(FAKE_RESTART_BLOCK_HASH); + + // Start the round that will be block N + subject.startRound(round, state); + + // Assert the internal state of the subject has changed as expected and the writer has been opened + verify(boundaryStateChangeListener).setLastUsedConsensusTime(CONSENSUS_NOW); + verify(aWriter).openBlock(N_BLOCK_NO); + assertEquals(N_MINUS_2_BLOCK_HASH, subject.blockHashByBlockNumber(N_MINUS_2_BLOCK_NO)); + assertEquals(FAKE_RESTART_BLOCK_HASH, subject.blockHashByBlockNumber(N_MINUS_1_BLOCK_NO)); + assertEquals(N_BLOCK_NO, subject.blockNo()); + + // Write some items to the block + subject.writeItem(FAKE_EVENT_TRANSACTION); + assertEquals(Bytes.fromHex("aa".repeat(48)), subject.prngSeed()); + subject.writeItem(FAKE_TRANSACTION_RESULT); + assertEquals(Bytes.fromHex("bb".repeat(48)), subject.prngSeed()); + subject.writeItem(FAKE_STATE_CHANGES); + for (int i = 0; i < 8; i++) { + subject.writeItem(FAKE_RECORD_FILE_ITEM); + } + + // End the round + subject.endRound(state, ROUND_NO); + + // Assert the internal state of the subject has changed as expected and the writer has been closed + final var expectedBlockInfo = new BlockStreamInfo( + N_BLOCK_NO, + asTimestamp(CONSENSUS_NOW), + appendHash(combine(Bytes.fromHex("dd".repeat(48)), FAKE_RESULT_HASH), resultHashes, 4), + appendHash(FAKE_RESTART_BLOCK_HASH, appendHash(N_MINUS_2_BLOCK_HASH, Bytes.EMPTY, 256), 256)); + final var actualBlockInfo = infoRef.get(); + assertEquals(expectedBlockInfo, actualBlockInfo); + verify(tssBaseService).requestLedgerSignature(blockHashCaptor.capture()); + + // Provide the ledger signature to the subject + subject.accept(blockHashCaptor.getValue(), FIRST_FAKE_SIGNATURE.toByteArray()); + + // Assert the block proof was written + final var proofItem = lastAItem.get(); + assertNotNull(proofItem); + final var item = BlockItem.PROTOBUF.parse(proofItem); + assertTrue(item.hasBlockProof()); + final var proof = item.blockProofOrThrow(); + assertEquals(N_BLOCK_NO, proof.block()); + assertEquals(FIRST_FAKE_SIGNATURE, proof.blockSignature()); + } + + @Test + void supportsMultiplePendingBlocksWithIndirectProofAsExpected() throws ParseException { + givenSubjectWith( + 1, + blockStreamInfoWith(N_MINUS_1_BLOCK_NO, N_MINUS_2_BLOCK_HASH, Bytes.EMPTY), + platformStateWith(null), + aWriter, + bWriter); + givenEndOfRoundSetup(); + doAnswer(invocationOnMock -> { + lastBItem.set(invocationOnMock.getArgument(0)); + return bWriter; + }) + .when(bWriter) + .writeItem(any()); + final ArgumentCaptor blockHashCaptor = ArgumentCaptor.forClass(byte[].class); + + // Initialize the last (N-1) block hash + subject.initLastBlockHash(FAKE_RESTART_BLOCK_HASH); + + // Start the round that will be block N + subject.startRound(round, state); + // Write some items to the block + subject.writeItem(FAKE_EVENT_TRANSACTION); + subject.writeItem(FAKE_TRANSACTION_RESULT); + subject.writeItem(FAKE_STATE_CHANGES); + subject.writeItem(FAKE_RECORD_FILE_ITEM); + // End the round in block N + subject.endRound(state, ROUND_NO); + + // Start the round that will be block N+1 + subject.startRound(round, state); + // Write some items to the block + subject.writeItem(FAKE_EVENT_TRANSACTION); + subject.writeItem(FAKE_TRANSACTION_RESULT); + subject.writeItem(FAKE_STATE_CHANGES); + subject.writeItem(FAKE_RECORD_FILE_ITEM); + // End the round in block N+1 + subject.endRound(state, ROUND_NO + 1); + + verify(tssBaseService, times(2)).requestLedgerSignature(blockHashCaptor.capture()); + final var allBlockHashes = blockHashCaptor.getAllValues(); + assertEquals(2, allBlockHashes.size()); + + // Provide the N+1 ledger signature to the subject first + subject.accept(allBlockHashes.getLast(), FIRST_FAKE_SIGNATURE.toByteArray()); + subject.accept(allBlockHashes.getFirst(), SECOND_FAKE_SIGNATURE.toByteArray()); + + // Assert both block proofs were written, but with the proof for N using an indirect proof + final var aProofItem = lastAItem.get(); + assertNotNull(aProofItem); + final var aItem = BlockItem.PROTOBUF.parse(aProofItem); + assertTrue(aItem.hasBlockProof()); + final var aProof = aItem.blockProofOrThrow(); + assertEquals(N_BLOCK_NO, aProof.block()); + assertEquals(FIRST_FAKE_SIGNATURE, aProof.blockSignature()); + assertEquals(2, aProof.siblingHashes().size()); + // And the proof for N+1 using a direct proof + final var bProofItem = lastBItem.get(); + assertNotNull(bProofItem); + final var bItem = BlockItem.PROTOBUF.parse(bProofItem); + assertTrue(bItem.hasBlockProof()); + final var bProof = bItem.blockProofOrThrow(); + assertEquals(N_BLOCK_NO + 1, bProof.block()); + assertEquals(FIRST_FAKE_SIGNATURE, bProof.blockSignature()); + assertTrue(bProof.siblingHashes().isEmpty()); + } + + private void givenSubjectWith( + final int roundsPerBlock, + @NonNull final BlockStreamInfo blockStreamInfo, + @NonNull final PlatformState platformState, + @NonNull final BlockItemWriter... writers) { + given(round.getConsensusTimestamp()).willReturn(CONSENSUS_NOW); + final AtomicInteger nextWriter = new AtomicInteger(0); + final var config = HederaTestConfigBuilder.create() + .withValue("blockStream.roundsPerBlock", roundsPerBlock) + .getOrCreateConfig(); + given(configProvider.getConfiguration()).willReturn(new VersionedConfigImpl(config, 1L)); + subject = new BlockStreamManagerImpl( + () -> writers[nextWriter.getAndIncrement()], + ForkJoinPool.commonPool(), + configProvider, + tssBaseService, + boundaryStateChangeListener); + subject.appendRealHashes(); + given(state.getReadableStates(BlockStreamService.NAME)).willReturn(readableStates); + given(state.getReadableStates(PlatformStateService.NAME)).willReturn(readableStates); + infoRef.set(blockStreamInfo); + stateRef.set(platformState); + blockStreamInfoState = new WritableSingletonStateBase<>(BLOCK_STREAM_INFO_KEY, infoRef::get, infoRef::set); + given(readableStates.getSingleton(BLOCK_STREAM_INFO_KEY)) + .willReturn(blockStreamInfoState); + given(readableStates.getSingleton(PLATFORM_STATE_KEY)) + .willReturn(new WritableSingletonStateBase<>(PLATFORM_STATE_KEY, stateRef::get, stateRef::set)); + } + + private void givenEndOfRoundSetup() { + given(boundaryStateChangeListener.flushChanges()).willReturn(FAKE_STATE_CHANGES); + doAnswer(invocationOnMock -> { + lastAItem.set(invocationOnMock.getArgument(0)); + return aWriter; + }) + .when(aWriter) + .writeItem(any()); + given(state.getWritableStates(BlockStreamService.NAME)).willReturn(writableStates); + given(writableStates.getSingleton(BLOCK_STREAM_INFO_KEY)) + .willReturn(blockStreamInfoState); + doAnswer(invocationOnMock -> { + blockStreamInfoState.commit(); + return null; + }) + .when((CommittableWritableStates) writableStates) + .commit(); + } + + private BlockStreamInfo blockStreamInfoWith( + final long blockNumber, @NonNull final Bytes nMinus2Hash, @NonNull final Bytes resultHashes) { + return BlockStreamInfo.newBuilder() + .blockNumber(blockNumber) + .trailingBlockHashes(appendHash(nMinus2Hash, Bytes.EMPTY, 256)) + .trailingOutputHashes(resultHashes) + .build(); + } + + private PlatformState platformStateWith(@Nullable final Instant freezeTime) { + return PlatformState.newBuilder() + .creationSoftwareVersion(CREATION_VERSION) + .freezeTime(freezeTime == null ? null : asTimestamp(freezeTime)) + .build(); + } + + private static Bytes noThrowSha384HashOfItem(@NonNull final BlockItem item) { + return Bytes.wrap(noThrowSha384HashOf(BlockItem.PROTOBUF.toBytes(item).toByteArray())); + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/schemas/V0540BlockStreamSchemaTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/schemas/V0540BlockStreamSchemaTest.java index 7371a94270f3..2679d1b72ed9 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/schemas/V0540BlockStreamSchemaTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/schemas/V0540BlockStreamSchemaTest.java @@ -17,75 +17,121 @@ package com.hedera.node.app.blocks.schemas; import static com.hedera.node.app.blocks.schemas.V0540BlockStreamSchema.BLOCK_STREAM_INFO_KEY; +import static com.hedera.node.app.fixtures.AppTestBase.DEFAULT_CONFIG; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verifyNoInteractions; +import com.hedera.hapi.node.base.SemanticVersion; +import com.hedera.hapi.node.base.Timestamp; +import com.hedera.hapi.node.state.blockrecords.BlockInfo; +import com.hedera.hapi.node.state.blockrecords.RunningHashes; import com.hedera.hapi.node.state.blockstream.BlockStreamInfo; -import com.hedera.node.config.testfixtures.HederaTestConfigBuilder; -import com.swirlds.config.api.Configuration; +import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.state.spi.MigrationContext; -import com.swirlds.state.spi.StateDefinition; import com.swirlds.state.spi.WritableSingletonState; import com.swirlds.state.spi.WritableStates; -import java.util.Set; +import java.util.Map; +import java.util.function.Consumer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) public class V0540BlockStreamSchemaTest { - @Mock(strictness = LENIENT) - private MigrationContext mockCtx; + @Mock + private MigrationContext migrationContext; - @Mock(strictness = LENIENT) - private WritableSingletonState mockBlockStreamInfo; + @Mock + private WritableStates writableStates; - @Mock(strictness = LENIENT) - private WritableStates mockWritableStates; + @Mock + private Consumer migratedBlockHashConsumer; - public static final Configuration DEFAULT_CONFIG = HederaTestConfigBuilder.createConfig(); + @Mock + private WritableSingletonState state; - private V0540BlockStreamSchema schema; + private V0540BlockStreamSchema subject; @BeforeEach void setUp() { - schema = new V0540BlockStreamSchema(); - when(mockCtx.newStates()).thenReturn(mockWritableStates); - when(mockWritableStates.getSingleton(BLOCK_STREAM_INFO_KEY)).thenReturn(mockBlockStreamInfo); + subject = new V0540BlockStreamSchema(migratedBlockHashConsumer); } @Test - void testVersion() { - assertEquals(0, schema.getVersion().major()); - assertEquals(54, schema.getVersion().minor()); - assertEquals(0, schema.getVersion().patch()); + void versionIsV0540() { + assertEquals(new SemanticVersion(0, 54, 0, "", ""), subject.getVersion()); } @Test - void testStatesToCreate() { - Set statesToCreate = schema.statesToCreate(DEFAULT_CONFIG); - assertNotNull(statesToCreate); - assertEquals(1, statesToCreate.size()); - assertTrue(statesToCreate.stream().anyMatch(state -> state.stateKey().equals(BLOCK_STREAM_INFO_KEY))); + void createsOneSingleton() { + final var stateDefs = subject.statesToCreate(DEFAULT_CONFIG); + assertEquals(1, stateDefs.size()); + final var def = stateDefs.iterator().next(); + assertTrue(def.singleton()); + assertEquals(BLOCK_STREAM_INFO_KEY, def.stateKey()); } @Test - void testMigration() { - when(mockCtx.previousVersion()).thenReturn(null); + void createsDefaultInfoAtGenesis() { + given(migrationContext.newStates()).willReturn(writableStates); + given(writableStates.getSingleton(BLOCK_STREAM_INFO_KEY)) + .willReturn(state); - schema.migrate(mockCtx); + subject.migrate(migrationContext); - ArgumentCaptor captor = ArgumentCaptor.forClass(BlockStreamInfo.class); - verify(mockBlockStreamInfo).put(captor.capture()); + verify(state).put(BlockStreamInfo.DEFAULT); + } + + @Test + void assumesMigrationIfNotGenesisAndStateIsNull() { + final var blockInfo = new BlockInfo( + 666L, + new Timestamp(1_234_567L, 0), + Bytes.fromHex("abcd".repeat(24 * 256)), + new Timestamp(1_234_567L, 890), + false, + new Timestamp(1_234_567L, 123)); + final var sharedValues = Map.of( + "SHARED_BLOCK_RECORD_INFO", + blockInfo, + "SHARED_RUNNING_HASHES", + new RunningHashes( + Bytes.fromHex("aa".repeat(48)), + Bytes.fromHex("bb".repeat(48)), + Bytes.fromHex("cc".repeat(48)), + Bytes.fromHex("dd".repeat(48)))); + given(migrationContext.newStates()).willReturn(writableStates); + given(migrationContext.previousVersion()).willReturn(SemanticVersion.DEFAULT); + given(writableStates.getSingleton(BLOCK_STREAM_INFO_KEY)) + .willReturn(state); + given(migrationContext.sharedValues()).willReturn(sharedValues); + + subject.migrate(migrationContext); + + verify(migratedBlockHashConsumer).accept(Bytes.fromHex("abcd".repeat(24))); + final var expectedInfo = new BlockStreamInfo( + blockInfo.lastBlockNumber(), + blockInfo.firstConsTimeOfLastBlock(), + Bytes.fromHex("dd".repeat(48) + "cc".repeat(48) + "bb".repeat(48) + "aa".repeat(48)), + Bytes.fromHex("abcd".repeat(24 * 255))); + verify(state).put(expectedInfo); + } + + @Test + void migrationIsNoopIfNotGenesisAndInfoIsNonNull() { + given(migrationContext.newStates()).willReturn(writableStates); + given(migrationContext.previousVersion()).willReturn(SemanticVersion.DEFAULT); + given(writableStates.getSingleton(BLOCK_STREAM_INFO_KEY)) + .willReturn(state); + given(state.get()).willReturn(BlockStreamInfo.DEFAULT); + + subject.migrate(migrationContext); - BlockStreamInfo blockInfoCapture = captor.getValue(); - assertEquals(BlockStreamInfo.DEFAULT, blockInfoCapture); + verifyNoInteractions(migratedBlockHashConsumer); } } 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 dd5ff90e2b29..3c4c27a32850 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 @@ -39,6 +39,7 @@ import com.hedera.node.app.signature.impl.SignatureExpanderImpl; import com.hedera.node.app.signature.impl.SignatureVerifierImpl; import com.hedera.node.app.state.recordcache.RecordCacheService; +import com.hedera.node.app.tss.TssBaseService; import com.hedera.node.config.data.HederaConfig; import com.hedera.node.config.testfixtures.HederaTestConfigBuilder; import com.hedera.pbj.runtime.io.buffer.Bytes; @@ -66,6 +67,9 @@ class IngestComponentTest { @Mock private Platform platform; + @Mock + private TssBaseService tssBaseService; + private HederaInjectionComponent app; @BeforeEach @@ -114,6 +118,7 @@ void setUp() { .kvStateChangeListener(new KVStateChangeListener()) .boundaryStateChangeListener(new BoundaryStateChangeListener()) .migrationStateChanges(List.of()) + .tssBaseService(tssBaseService) .build(); final var state = new FakeState(); 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 00c09d0c1f1c..7d99d2473d59 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 @@ -208,7 +208,6 @@ void dualReadAndWrite() throws IOException, ConstructableRegistryException { final var originalTree = createMerkleHederaState(schemaV1); MerkleStateRoot copy = originalTree.copy(); // make a copy to make VM flushable - ; forceFlush(originalTree.getReadableStates(FIRST_SERVICE).get(ANIMAL_STATE_KEY)); copy.copy(); // make a fast copy because we can only write to disk an immutable copy diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/TssBaseServiceTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/TssBaseServiceTest.java new file mode 100644 index 000000000000..45b095dfdc83 --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/TssBaseServiceTest.java @@ -0,0 +1,32 @@ +/* + * 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.hedera.node.app.tss; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.mock; + +import org.junit.jupiter.api.Test; + +class TssBaseServiceTest { + @Test + void nameIsAsExpected() { + final var subject = mock(TssBaseService.class); + doCallRealMethod().when(subject).getServiceName(); + assertEquals(TssBaseService.NAME, subject.getServiceName()); + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/impl/PlaceholderTssBaseServiceTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/impl/PlaceholderTssBaseServiceTest.java new file mode 100644 index 000000000000..a5f5ffdc64e1 --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/impl/PlaceholderTssBaseServiceTest.java @@ -0,0 +1,90 @@ +/* + * 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.hedera.node.app.tss.impl; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import com.swirlds.state.spi.SchemaRegistry; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +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 PlaceholderTssBaseServiceTest { + private CountDownLatch latch; + private final List receivedMessageHashes = new ArrayList<>(); + private final List receivedSignatures = new ArrayList<>(); + private final BiConsumer trackingConsumer = (a, b) -> { + receivedMessageHashes.add(a); + receivedSignatures.add(b); + latch.countDown(); + }; + + @Mock + private BiConsumer mockConsumer; + + @Mock + private SchemaRegistry registry; + + private final PlaceholderTssBaseService subject = new PlaceholderTssBaseService(); + + @BeforeEach + void setUp() { + subject.setExecutor(ForkJoinPool.commonPool()); + } + + @Test + void onlyRegisteredConsumerReceiveCallbacks() throws InterruptedException { + final var firstMessage = new byte[] {(byte) 0x01}; + final var secondMessage = new byte[] {(byte) 0x02}; + latch = new CountDownLatch(1); + + subject.registerLedgerSignatureConsumer(trackingConsumer); + subject.registerLedgerSignatureConsumer(mockConsumer); + + subject.requestLedgerSignature(firstMessage); + assertTrue(latch.await(1, TimeUnit.SECONDS)); + subject.unregisterLedgerSignatureConsumer(mockConsumer); + latch = new CountDownLatch(1); + subject.requestLedgerSignature(secondMessage); + assertTrue(latch.await(1, TimeUnit.SECONDS)); + + assertEquals(2, receivedMessageHashes.size()); + assertEquals(2, receivedSignatures.size()); + assertArrayEquals(firstMessage, receivedMessageHashes.getFirst()); + assertArrayEquals(secondMessage, receivedMessageHashes.getLast()); + verify(mockConsumer).accept(firstMessage, receivedSignatures.getFirst()); + verifyNoMoreInteractions(mockConsumer); + } + + @Test + void placeholderRegistersNoSchemasYet() { + subject.registerSchemas(registry); + verifyNoInteractions(registry); + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/standalone/impl/NoopVerificationStrategiesTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/standalone/impl/NoopVerificationStrategiesTest.java new file mode 100644 index 000000000000..50a3cec3eb56 --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/standalone/impl/NoopVerificationStrategiesTest.java @@ -0,0 +1,42 @@ +/* + * 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.hedera.node.app.workflows.standalone.impl; + +import static com.hedera.node.app.workflows.standalone.impl.NoopVerificationStrategies.NOOP_VERIFICATION_STRATEGIES; +import static org.junit.jupiter.api.Assertions.*; + +import com.hedera.hapi.node.base.Key; +import com.hedera.node.app.service.contract.impl.exec.scope.HederaNativeOperations; +import com.hedera.node.app.service.contract.impl.exec.scope.VerificationStrategy; +import org.hyperledger.besu.datatypes.Address; +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 NoopVerificationStrategiesTest { + @Mock + private HederaNativeOperations nativeOperations; + + @Test + void allKeysAreValid() { + final var subject = NOOP_VERIFICATION_STRATEGIES.activatingOnlyContractKeysFor( + Address.ALTBN128_ADD, true, nativeOperations); + assertSame(VerificationStrategy.Decision.VALID, subject.decideForPrimitive(Key.DEFAULT)); + } +} diff --git a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/ContractsConfig.java b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/ContractsConfig.java index 62c9dedae00a..032f68ffac72 100644 --- a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/ContractsConfig.java +++ b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/ContractsConfig.java @@ -87,8 +87,8 @@ public record ContractsConfig( boolean evmVersionDynamic, @ConfigProperty(value = "evm.allowCallsToNonContractAccounts", defaultValue = "true") @NetworkProperty boolean evmAllowCallsToNonContractAccounts, - @ConfigProperty(value = "evm.chargeGasOnPreEvmException", defaultValue = "true") @NetworkProperty - boolean chargeGasOnPreEvmException, + @ConfigProperty(value = "evm.chargeGasOnEvmHandleException", defaultValue = "true") @NetworkProperty + boolean chargeGasOnEvmHandleException, @ConfigProperty(value = "evm.nonExtantContractsFail", defaultValue = "0") @NetworkProperty Set evmNonExtantContractsFail, @ConfigProperty(value = "evm.version", defaultValue = "v0.50") @NetworkProperty String evmVersion) {} diff --git a/hedera-node/hedera-network-admin-service-impl/build.gradle.kts b/hedera-node/hedera-network-admin-service-impl/build.gradle.kts index ef0b562a944b..07590e13359a 100644 --- a/hedera-node/hedera-network-admin-service-impl/build.gradle.kts +++ b/hedera-node/hedera-network-admin-service-impl/build.gradle.kts @@ -23,7 +23,7 @@ description = "Default Hedera Network Admin Service Implementation" val writeSemanticVersionProperties = tasks.register("writeSemanticVersionProperties") { - property("hapi.proto.version", libs.versions.hapi.proto.get()) + property("hapi.proto.version", project.version) property("hedera.services.version", project.version) destinationFile.set( diff --git a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/handlers/UnzipUtility.java b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/handlers/UnzipUtility.java index 6e032e22d15b..053815a35299 100644 --- a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/handlers/UnzipUtility.java +++ b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/handlers/UnzipUtility.java @@ -46,7 +46,7 @@ public final class UnzipUtility { private static final int THRESHOLD_ZIP_SIZE = 1000000000; // 1 GB - max allowed total size of all uncompressed files private static final int THRESHOLD_ENTRY_SIZE = 100000000; // 100 MB - max allowed size of one uncompressed file // max allowed ratio between uncompressed and compressed file size - private static final double THRESHOLD_RATIO = 10; + private static final double THRESHOLD_RATIO = 100; private UnzipUtility() {} diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/ContractServiceComponent.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/ContractServiceComponent.java index 678231f53943..a0c3510a09e9 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/ContractServiceComponent.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/ContractServiceComponent.java @@ -16,6 +16,7 @@ package com.hedera.node.app.service.contract.impl; +import com.hedera.node.app.service.contract.impl.exec.scope.VerificationStrategies; import com.hedera.node.app.service.contract.impl.handlers.ContractHandlers; import com.hedera.node.app.spi.signatures.SignatureVerifier; import dagger.BindsInstance; @@ -35,6 +36,7 @@ interface Factory { ContractServiceComponent create( @BindsInstance InstantSource instantSource, @BindsInstance SignatureVerifier signatureVerifier, + @BindsInstance VerificationStrategies verificationStrategies, @BindsInstance @Nullable Supplier> addOnTracers); } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/ContractServiceImpl.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/ContractServiceImpl.java index 9bd30be61527..8ed4849de690 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/ContractServiceImpl.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/ContractServiceImpl.java @@ -19,6 +19,8 @@ import static java.util.Objects.requireNonNull; import com.hedera.node.app.service.contract.ContractService; +import com.hedera.node.app.service.contract.impl.exec.scope.DefaultVerificationStrategies; +import com.hedera.node.app.service.contract.impl.exec.scope.VerificationStrategies; import com.hedera.node.app.service.contract.impl.handlers.ContractHandlers; import com.hedera.node.app.service.contract.impl.schemas.V0490ContractSchema; import com.hedera.node.app.service.contract.impl.schemas.V0500ContractSchema; @@ -27,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; import java.util.function.Supplier; import org.hyperledger.besu.evm.tracing.OperationTracer; @@ -39,11 +42,13 @@ public class ContractServiceImpl implements ContractService { private final ContractServiceComponent component; public ContractServiceImpl(@NonNull final AppContext appContext) { - this(appContext, null); + this(appContext, null, null); } public ContractServiceImpl( - @NonNull final AppContext appContext, @Nullable final Supplier> addOnTracers) { + @NonNull final AppContext appContext, + @Nullable final VerificationStrategies verificationStrategies, + @Nullable final Supplier> addOnTracers) { requireNonNull(appContext); this.component = DaggerContractServiceComponent.factory() .create( @@ -51,6 +56,7 @@ public ContractServiceImpl( // (FUTURE) Inject the signature verifier instance into the IsAuthorizedSystemContract // C.f. https://github.com/hashgraph/hedera-services/issues/14248 appContext.signatureVerifier(), + Optional.ofNullable(verificationStrategies).orElseGet(DefaultVerificationStrategies::new), addOnTracers); } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/ContextTransactionProcessor.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/ContextTransactionProcessor.java index 49a56f2cbed6..b43d83f4de49 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/ContextTransactionProcessor.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/ContextTransactionProcessor.java @@ -19,11 +19,11 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_CONTRACT_ID; import static java.util.Objects.requireNonNull; +import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.ContractID; import com.hedera.hapi.streams.ContractBytecode; import com.hedera.node.app.hapi.utils.ethereum.EthTxData; import com.hedera.node.app.service.contract.impl.annotations.TransactionScope; -import com.hedera.node.app.service.contract.impl.exec.failure.AbortException; import com.hedera.node.app.service.contract.impl.exec.gas.CustomGasCharging; import com.hedera.node.app.service.contract.impl.exec.tracers.AddOnEvmActionTracer; import com.hedera.node.app.service.contract.impl.exec.tracers.EvmActionTracer; @@ -33,6 +33,7 @@ import com.hedera.node.app.service.contract.impl.hevm.HederaWorldUpdater; import com.hedera.node.app.service.contract.impl.hevm.HydratedEthTxData; import com.hedera.node.app.service.contract.impl.infra.HevmTransactionFactory; +import com.hedera.node.app.service.contract.impl.state.HederaEvmAccount; import com.hedera.node.app.service.contract.impl.state.RootProxyWorldUpdater; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.spi.workflows.HandleException; @@ -109,8 +110,12 @@ public CallOutcome call() { // if an exception occurs during a ContractCall, charge fees to the sender and return a CallOutcome reflecting // the error. final var hevmTransaction = safeCreateHevmTransaction(); - if (hevmTransaction.isException() && contractsConfig.chargeGasOnPreEvmException()) { - return chargeFeesAndReturnOutcome(hevmTransaction); + if (hevmTransaction.isException()) { + return maybeChargeFeesAndReturnOutcome( + hevmTransaction, + context.body().transactionIDOrThrow().accountIDOrThrow(), + null, + contractsConfig.chargeGasOnEvmHandleException()); } // Process the transaction and return its outcome @@ -135,30 +140,15 @@ public CallOutcome call() { } return CallOutcome.fromResultsWithMaybeSidecars( result.asProtoResultOf(ethTxDataIfApplicable(), rootProxyWorldUpdater), result); - } catch (AbortException e) { - if (e.isChargeable() && contractsConfig.chargeGasOnPreEvmException()) { - gasCharging.chargeGasForAbortedTransaction( - requireNonNull(e.senderId()), hederaEvmContext, rootProxyWorldUpdater, hevmTransaction); - } - // Commit any HAPI fees that were charged before aborting - rootProxyWorldUpdater.commit(); - - ContractID recipientId = null; - if (!INVALID_CONTRACT_ID.equals(e.getStatus())) { - recipientId = hevmTransaction.contractId(); - } - - var result = HederaEvmTransactionResult.fromAborted(e.senderId(), recipientId, e.getStatus()); - - if (context.body().hasEthereumTransaction()) { - final var sender = rootProxyWorldUpdater.getHederaAccount(e.senderId()); - if (sender != null) { - result = result.withSignerNonce(sender.getNonce()); - } - } - - return CallOutcome.fromResultsWithoutSidecars( - result.asProtoResultOf(ethTxDataIfApplicable(), rootProxyWorldUpdater), result); + } catch (HandleException e) { + final var sender = rootProxyWorldUpdater.getHederaAccount(hevmTransaction.senderId()); + final var senderId = sender != null ? sender.hederaId() : hevmTransaction.senderId(); + + return maybeChargeFeesAndReturnOutcome( + hevmTransaction.withException(e), + senderId, + sender, + hevmTransaction.isContractCall() && contractsConfig.chargeGasOnEvmHandleException()); } } @@ -171,16 +161,28 @@ private HederaEvmTransaction safeCreateHevmTransaction() { } } - private CallOutcome chargeFeesAndReturnOutcome(@NonNull final HederaEvmTransaction hevmTransaction) { - // If there was an exception while creating the HederaEvmTransaction and the transaction is a ContractCall - // charge fees to the sender and return a CallOutcome reflecting the error. - final var senderId = context.body().transactionIDOrThrow().accountIDOrThrow(); - gasCharging.chargeGasForAbortedTransaction(senderId, hederaEvmContext, rootProxyWorldUpdater, hevmTransaction); + private CallOutcome maybeChargeFeesAndReturnOutcome( + @NonNull final HederaEvmTransaction hevmTransaction, + @NonNull final AccountID senderId, + @Nullable final HederaEvmAccount sender, + final boolean chargeGas) { + final var status = requireNonNull(hevmTransaction.exception()).getStatus(); + if (chargeGas) { + gasCharging.chargeGasForAbortedTransaction( + senderId, hederaEvmContext, rootProxyWorldUpdater, hevmTransaction); + } rootProxyWorldUpdater.commit(); - final var result = HederaEvmTransactionResult.fromAborted( - senderId, - hevmTransaction.contractId(), - hevmTransaction.exception().getStatus()); + ContractID recipientId = null; + if (!INVALID_CONTRACT_ID.equals(status)) { + recipientId = hevmTransaction.contractId(); + } + + var result = HederaEvmTransactionResult.fromAborted(senderId, recipientId, status); + + if (context.body().hasEthereumTransaction() && sender != null) { + result = result.withSignerNonce(sender.getNonce()); + } + return CallOutcome.fromResultsWithoutSidecars( result.asProtoResultOf(ethTxDataIfApplicable(), rootProxyWorldUpdater), result); } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/TransactionProcessor.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/TransactionProcessor.java index 51e9a508d60a..aa2b3cac65e9 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/TransactionProcessor.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/TransactionProcessor.java @@ -19,7 +19,6 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ACCOUNT_ID; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_CONTRACT_ID; import static com.hedera.hapi.node.base.ResponseCodeEnum.WRONG_NONCE; -import static com.hedera.node.app.service.contract.impl.exec.failure.AbortException.validateTrueOrAbort; import static com.hedera.node.app.service.contract.impl.hevm.HederaEvmTransactionResult.resourceExhaustionFrom; import static com.hedera.node.app.service.contract.impl.utils.ConversionUtils.contractIDToBesuAddress; import static com.hedera.node.app.service.contract.impl.utils.ConversionUtils.isEvmAddress; @@ -31,7 +30,6 @@ import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.hapi.node.contract.ContractCreateTransactionBody; -import com.hedera.node.app.service.contract.impl.exec.failure.AbortException; import com.hedera.node.app.service.contract.impl.exec.gas.CustomGasCharging; import com.hedera.node.app.service.contract.impl.exec.processors.CustomMessageCallProcessor; import com.hedera.node.app.service.contract.impl.exec.utils.FrameBuilder; @@ -40,7 +38,6 @@ import com.hedera.node.app.service.contract.impl.hevm.HederaEvmTransactionResult; import com.hedera.node.app.service.contract.impl.hevm.HederaWorldUpdater; import com.hedera.node.app.service.contract.impl.state.HederaEvmAccount; -import com.hedera.node.app.spi.workflows.HandleException; import com.hedera.node.app.spi.workflows.ResourceExhaustedException; import com.hedera.node.config.data.ContractsConfig; import com.swirlds.config.api.Configuration; @@ -113,7 +110,6 @@ public FeatureFlags featureFlags() { * @param tracer the tracer to use * @param config the node configuration * @return the result of running the transaction to completion - * @throws AbortException if processing failed before initiating the EVM transaction */ public HederaEvmTransactionResult processTransaction( @NonNull final HederaEvmTransaction transaction, @@ -123,14 +119,7 @@ public HederaEvmTransactionResult processTransaction( @NonNull final ActionSidecarContentTracer tracer, @NonNull final Configuration config) { final var parties = computeInvolvedPartiesOrAbort(transaction, updater, config); - try { - return processTransactionWithParties( - transaction, updater, feesOnlyUpdater, context, tracer, config, parties); - } catch (HandleException e) { - final var sender = updater.getHederaAccount(transaction.senderId()); - final var senderId = sender != null ? sender.hederaId() : transaction.senderId(); - throw new AbortException(e.getStatus(), senderId); - } + return processTransactionWithParties(transaction, updater, feesOnlyUpdater, context, tracer, config, parties); } private HederaEvmTransactionResult processTransactionWithParties( @@ -175,13 +164,7 @@ private InvolvedParties computeInvolvedPartiesOrAbort( @NonNull final HederaEvmTransaction transaction, @NonNull final HederaWorldUpdater updater, @NonNull final Configuration config) { - try { - return computeInvolvedParties(transaction, updater, config); - } catch (AbortException e) { - throw e; - } catch (HandleException e) { - throw new AbortException(e.getStatus(), transaction.senderId(), null, true); - } + return computeInvolvedParties(transaction, updater, config); } private HederaEvmTransactionResult safeCommit( @@ -243,12 +226,11 @@ private InvolvedParties computeInvolvedParties( @NonNull final HederaWorldUpdater updater, @NonNull final Configuration config) { final var sender = updater.getHederaAccount(transaction.senderId()); - validateTrueOrAbort(sender != null, INVALID_ACCOUNT_ID, transaction.senderId()); - final var senderId = sender.hederaId(); + validateTrue(sender != null, INVALID_ACCOUNT_ID); HederaEvmAccount relayer = null; if (transaction.isEthereumTransaction()) { relayer = updater.getHederaAccount(requireNonNull(transaction.relayerId())); - validateTrueOrAbort(relayer != null, INVALID_ACCOUNT_ID, senderId); + validateTrue(relayer != null, INVALID_ACCOUNT_ID); } final InvolvedParties parties; if (transaction.isCreate()) { @@ -270,7 +252,7 @@ private InvolvedParties computeInvolvedParties( } } if (transaction.isEthereumTransaction()) { - validateTrueOrAbort(transaction.nonce() == parties.sender().getNonce(), WRONG_NONCE, senderId); + validateTrue(transaction.nonce() == parties.sender().getNonce(), WRONG_NONCE); } return parties; } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/failure/AbortException.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/failure/AbortException.java deleted file mode 100644 index 7186be142f3d..000000000000 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/failure/AbortException.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (C) 2023-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.hedera.node.app.service.contract.impl.exec.failure; - -import static java.util.Objects.requireNonNull; - -import com.hedera.hapi.node.base.AccountID; -import com.hedera.hapi.node.base.ResponseCodeEnum; -import com.hedera.node.app.spi.workflows.HandleException; -import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.Nullable; - -/** - * An exception thrown when a transaction is aborted before entering the EVM. - * - *

      Includes the effective Hedera id of the sender. - */ -public class AbortException extends HandleException { - private final AccountID senderId; - - @Nullable - private final AccountID relayerId; - // Whether gas can still be charged for the transaction - private final boolean isChargeable; - - public AbortException(@NonNull final ResponseCodeEnum status, @NonNull final AccountID senderId) { - super(status); - this.senderId = requireNonNull(senderId); - this.relayerId = null; - this.isChargeable = false; - } - - public AbortException( - @NonNull final ResponseCodeEnum status, - @NonNull final AccountID senderId, - @Nullable final AccountID relayerId, - final boolean isChargeable) { - super(status); - this.senderId = requireNonNull(senderId); - this.relayerId = relayerId; - this.isChargeable = isChargeable; - } - - /** - * Returns the effective Hedera id of the sender. - * - * @return the effective Hedera id of the sender - */ - public AccountID senderId() { - return senderId; - } - - /** - * Returns whether the transaction can still be charged for gas. - * - * @return whether the transaction can still be charged for gas. - */ - public boolean isChargeable() { - return isChargeable; - } - - /** - * Returns the relayer id. - * - * @return the relayer id. - */ - @Nullable - public AccountID relayerId() { - return relayerId; - } - - /** - * Throws an {@code AbortException} if the given flag is {@code false}. - * - * @param flag the flag to check - * @param status the status to use if the flag is {@code false} - * @param senderId the effective Hedera id of the sender - */ - public static void validateTrueOrAbort( - final boolean flag, @NonNull final ResponseCodeEnum status, @NonNull final AccountID senderId) { - if (!flag) { - throw new AbortException(status, senderId); - } - } -} diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/DefaultVerificationStrategies.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/DefaultVerificationStrategies.java new file mode 100644 index 000000000000..6104cca14758 --- /dev/null +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/DefaultVerificationStrategies.java @@ -0,0 +1,58 @@ +/* + * 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.hedera.node.app.service.contract.impl.exec.scope; + +import static com.hedera.node.app.service.contract.impl.exec.scope.HederaNativeOperations.MISSING_ENTITY_NUMBER; +import static com.hedera.node.app.service.contract.impl.utils.ConversionUtils.maybeMissingNumberOf; +import static com.hedera.node.app.service.contract.impl.utils.ConversionUtils.tuweniToPbjBytes; + +import com.hedera.hapi.node.base.ContractID; +import edu.umd.cs.findbugs.annotations.NonNull; +import org.hyperledger.besu.datatypes.Address; + +/** + * Implements the default {@link VerificationStrategies} for use in signature activation tests. + */ +public class DefaultVerificationStrategies implements VerificationStrategies { + /** + * Returns a {@link VerificationStrategy} that will activate only delegatable contract id and + * contract id keys (the latter if delegatable permissions are not required). + * + *

      This is the standard strategy under the approval-based security model, where a contract gains + * authorization for an entity only by having its id or address added to that entity's controlling + * key structure. + * + * @param sender the contract whose keys are to be activated + * @param requiresDelegatePermission whether the strategy should require a delegatable contract id key + * @param nativeOperations the operations to use for looking up the contract's number + * @return a {@link VerificationStrategy} that will activate only delegatable contract id and contract id keys + */ + public VerificationStrategy activatingOnlyContractKeysFor( + @NonNull final Address sender, + final boolean requiresDelegatePermission, + @NonNull final HederaNativeOperations nativeOperations) { + final var contractNum = maybeMissingNumberOf(sender, nativeOperations); + if (contractNum == MISSING_ENTITY_NUMBER) { + throw new IllegalArgumentException("Cannot verify against missing contract " + sender); + } + return new ActiveContractVerificationStrategy( + ContractID.newBuilder().contractNum(contractNum).build(), + tuweniToPbjBytes(sender), + requiresDelegatePermission, + ActiveContractVerificationStrategy.UseTopLevelSigs.NO); + } +} diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/VerificationStrategies.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/VerificationStrategies.java index a9bfa7963466..3991e1e552ab 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/VerificationStrategies.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/VerificationStrategies.java @@ -16,52 +16,24 @@ package com.hedera.node.app.service.contract.impl.exec.scope; -import static com.hedera.node.app.service.contract.impl.exec.scope.HederaNativeOperations.MISSING_ENTITY_NUMBER; -import static com.hedera.node.app.service.contract.impl.utils.ConversionUtils.maybeMissingNumberOf; -import static com.hedera.node.app.service.contract.impl.utils.ConversionUtils.tuweniToPbjBytes; - -import com.hedera.hapi.node.base.ContractID; -import com.hedera.node.app.service.contract.impl.exec.scope.ActiveContractVerificationStrategy.UseTopLevelSigs; import edu.umd.cs.findbugs.annotations.NonNull; -import javax.inject.Inject; -import javax.inject.Singleton; import org.hyperledger.besu.datatypes.Address; /** * Provides {@link VerificationStrategy} instances for use in signature activation tests. */ -@Singleton -public class VerificationStrategies { - @Inject - public VerificationStrategies() { - // Dagger2 - } - +public interface VerificationStrategies { /** - * Returns a {@link VerificationStrategy} that will activate only delegatable contract id and - * contract id keys (the latter if delegatable permissions are not required). - * - *

      This is the standard strategy under the approval-based security model, where a contract gains - * authorization for an entity only by having its id or address added to that entity's controlling - * key structure. + * Returns a {@link VerificationStrategy} to use based on the given sender address, delegate + * permissions requirements, and Hedera native operations. * - * @param sender the contract whose keys are to be activated - * @param requiresDelegatePermission whether the strategy should require a delegatable contract id key - * @param nativeOperations the operations to use for looking up the contract's number - * @return a {@link VerificationStrategy} that will activate only delegatable contract id and contract id keys + * @param sender the sender address + * @param requiresDelegatePermission whether the sender is using {@code DELEGATECALL} + * @param nativeOperations the native Hedera operations + * @return the {@link VerificationStrategy} to use */ - public VerificationStrategy activatingOnlyContractKeysFor( - @NonNull final Address sender, - final boolean requiresDelegatePermission, - @NonNull final HederaNativeOperations nativeOperations) { - final var contractNum = maybeMissingNumberOf(sender, nativeOperations); - if (contractNum == MISSING_ENTITY_NUMBER) { - throw new IllegalArgumentException("Cannot verify against missing contract " + sender); - } - return new ActiveContractVerificationStrategy( - ContractID.newBuilder().contractNum(contractNum).build(), - tuweniToPbjBytes(sender), - requiresDelegatePermission, - UseTopLevelSigs.NO); - } + VerificationStrategy activatingOnlyContractKeysFor( + @NonNull Address sender, + boolean requiresDelegatePermission, + @NonNull HederaNativeOperations nativeOperations); } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/create/ClassicCreatesCall.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/create/ClassicCreatesCall.java index 2fd1b17cf2e8..2586626c845d 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/create/ClassicCreatesCall.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/create/ClassicCreatesCall.java @@ -189,7 +189,7 @@ private VerificationStrategy verificationStrategyFor( ? new EitherOrVerificationStrategy( verificationStrategy, new SpecificCryptoVerificationStrategy(op.adminKeyOrThrow())) : verificationStrategy; - // And our final dispatch verification strategy must very depending on if + // And our final dispatch verification strategy must vary depending on if // a legacy activation address is active (somewhere on the stack) return stackIncludesActiveAddress(frame, legacyActivation.besuAddress()) ? new EitherOrVerificationStrategy( diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/v046/Version046FeatureFlags.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/v046/Version046FeatureFlags.java index c961bf7e5cfc..20af04cf60ce 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/v046/Version046FeatureFlags.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/v046/Version046FeatureFlags.java @@ -41,6 +41,6 @@ public boolean isAllowCallsToNonContractAccountsEnabled( @Override public boolean isChargeGasOnPreEvmException(@NonNull Configuration config) { - return config.getConfigData(ContractsConfig.class).chargeGasOnPreEvmException(); + return config.getConfigData(ContractsConfig.class).chargeGasOnEvmHandleException(); } } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/hevm/HederaEvmTransaction.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/hevm/HederaEvmTransaction.java index 596f57240845..56964caa7407 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/hevm/HederaEvmTransaction.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/hevm/HederaEvmTransaction.java @@ -127,4 +127,23 @@ public long offeredGasCost() { public boolean requiresFullRelayerAllowance() { return offeredGasPrice == 0L; } + + /** + * @return a copy of this transaction with the given {@code exception} + */ + public HederaEvmTransaction withException(@NonNull final HandleException exception) { + return new HederaEvmTransaction( + this.senderId, + this.relayerId, + this.contractId, + this.nonce, + this.payload, + this.chainId, + this.value, + this.gasLimit, + this.offeredGasPrice, + this.maxGasAllowance, + this.hapiCreation, + exception); + } } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/ContextTransactionProcessorTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/ContextTransactionProcessorTest.java index cb4f3f553772..0f5ec2a45555 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/ContextTransactionProcessorTest.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/ContextTransactionProcessorTest.java @@ -31,6 +31,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import com.hedera.hapi.node.base.TransactionID; @@ -38,7 +39,6 @@ import com.hedera.node.app.service.contract.impl.exec.CallOutcome; import com.hedera.node.app.service.contract.impl.exec.ContextTransactionProcessor; import com.hedera.node.app.service.contract.impl.exec.TransactionProcessor; -import com.hedera.node.app.service.contract.impl.exec.failure.AbortException; import com.hedera.node.app.service.contract.impl.exec.gas.CustomGasCharging; import com.hedera.node.app.service.contract.impl.exec.tracers.EvmActionTracer; import com.hedera.node.app.service.contract.impl.hevm.HederaEvmContext; @@ -61,6 +61,9 @@ @ExtendWith(MockitoExtension.class) class ContextTransactionProcessorTest { private static final Configuration CONFIGURATION = HederaTestConfigBuilder.createConfig(); + private static final Configuration CONFIG_NO_CHARGE_ON_EXCEPTION = HederaTestConfigBuilder.create() + .withValue("contracts.evm.chargeGasOnEvmHandleException", false) + .getOrCreateConfig(); @Mock private HandleContext context; @@ -224,10 +227,11 @@ void stillChargesHapiFeesOnAbort() { .willReturn(HEVM_CREATION); given(processor.processTransaction( HEVM_CREATION, rootProxyWorldUpdater, feesOnlyUpdater, hederaEvmContext, tracer, CONFIGURATION)) - .willThrow(new AbortException(INVALID_CONTRACT_ID, SENDER_ID)); + .willThrow(new HandleException(INVALID_CONTRACT_ID)); subject.call(); + verify(customGasCharging).chargeGasForAbortedTransaction(any(), any(), any(), any()); verify(rootProxyWorldUpdater).commit(); } @@ -255,6 +259,36 @@ void stillChargesHapiFeesOnHevmException() { final var outcome = subject.call(); + verify(customGasCharging).chargeGasForAbortedTransaction(any(), any(), any(), any()); + verify(rootProxyWorldUpdater).commit(); + assertEquals(INVALID_CONTRACT_ID, outcome.status()); + } + + @Test + void doesNotChargeHapiFeesOnHevmExceptionIfSoConfiggured() { + final var contractsConfig = CONFIG_NO_CHARGE_ON_EXCEPTION.getConfigData(ContractsConfig.class); + final var subject = new ContextTransactionProcessor( + null, + context, + contractsConfig, + CONFIG_NO_CHARGE_ON_EXCEPTION, + hederaEvmContext, + null, + tracer, + rootProxyWorldUpdater, + hevmTransactionFactory, + feesOnlyUpdater, + processor, + customGasCharging); + + given(context.body()).willReturn(transactionBody); + given(hevmTransactionFactory.fromHapiTransaction(transactionBody)).willReturn(HEVM_Exception); + given(transactionBody.transactionIDOrThrow()).willReturn(transactionID); + given(transactionID.accountIDOrThrow()).willReturn(SENDER_ID); + + final var outcome = subject.call(); + + verify(customGasCharging, never()).chargeGasForAbortedTransaction(any(), any(), any(), any()); verify(rootProxyWorldUpdater).commit(); assertEquals(INVALID_CONTRACT_ID, outcome.status()); } @@ -285,6 +319,38 @@ void stillChargesHapiFeesOnExceptionThrown() { final var outcome = subject.call(); + verify(customGasCharging).chargeGasForAbortedTransaction(any(), any(), any(), any()); + verify(rootProxyWorldUpdater).commit(); + assertEquals(INVALID_CONTRACT_ID, outcome.status()); + } + + @Test + void doesNotChargeHapiFeesOnExceptionThrownIfSoConfigured() { + final var contractsConfig = CONFIG_NO_CHARGE_ON_EXCEPTION.getConfigData(ContractsConfig.class); + final var subject = new ContextTransactionProcessor( + null, + context, + contractsConfig, + CONFIG_NO_CHARGE_ON_EXCEPTION, + hederaEvmContext, + null, + tracer, + rootProxyWorldUpdater, + hevmTransactionFactory, + feesOnlyUpdater, + processor, + customGasCharging); + + given(context.body()).willReturn(transactionBody); + given(hevmTransactionFactory.fromHapiTransaction(transactionBody)) + .willThrow(new HandleException(INVALID_CONTRACT_ID)); + given(hevmTransactionFactory.fromContractTxException(any(), any())).willReturn(HEVM_Exception); + given(transactionBody.transactionIDOrThrow()).willReturn(transactionID); + given(transactionID.accountIDOrThrow()).willReturn(SENDER_ID); + + final var outcome = subject.call(); + + verify(customGasCharging, never()).chargeGasForAbortedTransaction(any(), any(), any(), any()); verify(rootProxyWorldUpdater).commit(); assertEquals(INVALID_CONTRACT_ID, outcome.status()); } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/TransactionProcessorTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/TransactionProcessorTest.java index 657d8c2e079b..ae7110c37a1a 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/TransactionProcessorTest.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/TransactionProcessorTest.java @@ -65,7 +65,6 @@ import com.hedera.node.app.service.contract.impl.exec.FeatureFlags; import com.hedera.node.app.service.contract.impl.exec.FrameRunner; import com.hedera.node.app.service.contract.impl.exec.TransactionProcessor; -import com.hedera.node.app.service.contract.impl.exec.failure.AbortException; import com.hedera.node.app.service.contract.impl.exec.gas.CustomGasCharging; import com.hedera.node.app.service.contract.impl.exec.gas.SystemContractGasCalculator; import com.hedera.node.app.service.contract.impl.exec.gas.TinybarValues; @@ -78,6 +77,7 @@ import com.hedera.node.app.service.contract.impl.records.ContractOperationStreamBuilder; import com.hedera.node.app.service.contract.impl.state.HederaEvmAccount; import com.hedera.node.app.service.contract.impl.utils.ConversionUtils; +import com.hedera.node.app.spi.workflows.HandleException; import com.hedera.node.app.spi.workflows.ResourceExhaustedException; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.config.api.Configuration; @@ -163,7 +163,7 @@ void abortsOnMissingSender() { @Test void lazyCreationAttemptWithNoValueFailsFast() { - givenSenderAccount(); + givenSenderAccountWithNoHederaAccount(); givenRelayerAccount(); given(messageCallProcessor.isImplicitCreationEnabled(config)).willReturn(true); assertAbortsWith(wellKnownRelayedHapiCall(0), INVALID_CONTRACT_ID); @@ -171,7 +171,7 @@ void lazyCreationAttemptWithNoValueFailsFast() { @Test void lazyCreationAttemptWithInvalidAddress() { - givenSenderAccount(); + givenSenderAccountWithNoHederaAccount(); givenRelayerAccount(); final var invalidCreation = new HederaEvmTransaction( SENDER_ID, @@ -363,22 +363,22 @@ void callWhenComputePartiesThrowsException() { final var context = wellKnownContextWith(blocks, tinybarValues, systemContractGasCalculator); given(worldUpdater.getHederaAccount(SENDER_ID)).willReturn(null); - final var abortException = catchThrowableOfType( + final var handleException = catchThrowableOfType( () -> subject.processTransaction( transaction, worldUpdater, () -> feesOnlyUpdater, context, tracer, config), - AbortException.class); - assertThat(abortException.isChargeable()).isFalse(); + HandleException.class); + assertThat(handleException.getStatus()).isEqualTo(INVALID_ACCOUNT_ID); } @Test void requiresEthTxToHaveNonNullRelayer() { - givenSenderAccount(); + givenSenderAccountWithNoHederaAccount(); assertAbortsWith(wellKnownRelayedHapiCall(0), INVALID_ACCOUNT_ID); } @Test void nonLazyCreateCandidateMustHaveReceiver() { - givenSenderAccount(); + givenSenderAccountWithNoHederaAccount(); givenRelayerAccount(); assertAbortsWith(wellKnownRelayedHapiCall(0), INVALID_CONTRACT_ID); } @@ -756,8 +756,12 @@ private void givenFeeOnlyParties() { given(feesOnlyUpdater.getHederaAccount(CALLED_CONTRACT_ID)).willReturn(receiverAccount); } - private void givenSenderAccount() { + private void givenSenderAccountWithNoHederaAccount() { given(worldUpdater.getHederaAccount(SENDER_ID)).willReturn(senderAccount); + } + + private void givenSenderAccount() { + givenSenderAccountWithNoHederaAccount(); given(senderAccount.hederaId()).willReturn(SENDER_ID); } diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/BaseTokenHandler.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/BaseTokenHandler.java index d346996d1ccc..8e477646ee35 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/BaseTokenHandler.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/BaseTokenHandler.java @@ -42,7 +42,9 @@ import com.hedera.node.app.service.token.impl.WritableAccountStore; import com.hedera.node.app.service.token.impl.WritableTokenRelationStore; import com.hedera.node.app.service.token.impl.WritableTokenStore; +import com.hedera.node.app.service.token.impl.util.TokenHandlerHelper; import com.hedera.node.app.service.token.impl.util.TokenKey; +import com.hedera.node.app.spi.validation.ExpiryValidator; import com.hedera.node.config.data.EntitiesConfig; import com.hedera.node.config.data.TokensConfig; import com.swirlds.config.api.Configuration; @@ -77,12 +79,14 @@ public class BaseTokenHandler { /** * Mints fungible tokens. This method is called in both token create and mint. - * @param token the new or existing token to mint - * @param treasuryRel the treasury relation for the token - * @param amount the amount to mint - * @param accountStore the account store - * @param tokenStore the token store + * + * @param token the new or existing token to mint + * @param treasuryRel the treasury relation for the token + * @param amount the amount to mint + * @param accountStore the account store + * @param tokenStore the token store * @param tokenRelationStore the token relation store + * @param expiryValidator the expiry validator */ protected long mintFungible( @NonNull final Token token, @@ -90,12 +94,20 @@ protected long mintFungible( final long amount, @NonNull final WritableAccountStore accountStore, @NonNull final WritableTokenStore tokenStore, - @NonNull final WritableTokenRelationStore tokenRelationStore) { + @NonNull final WritableTokenRelationStore tokenRelationStore, + @NonNull final ExpiryValidator expiryValidator) { requireNonNull(token); requireNonNull(treasuryRel); return changeSupply( - token, treasuryRel, +amount, INVALID_TOKEN_MINT_AMOUNT, accountStore, tokenStore, tokenRelationStore); + token, + treasuryRel, + +amount, + INVALID_TOKEN_MINT_AMOUNT, + accountStore, + tokenStore, + tokenRelationStore, + expiryValidator); } /** @@ -105,13 +117,14 @@ protected long mintFungible( *

      * Note: This method assumes the given token has a non-null supply key! * - * @param token the token that is minted or burned - * @param treasuryRel the treasury relation for the token - * @param amount the amount to mint or burn - * @param invalidSupplyCode the invalid supply code to use if the supply is invalid - * @param accountStore the account store - * @param tokenStore the token store + * @param token the token that is minted or burned + * @param treasuryRel the treasury relation for the token + * @param amount the amount to mint or burn + * @param invalidSupplyCode the invalid supply code to use if the supply is invalid + * @param accountStore the account store + * @param tokenStore the token store * @param tokenRelationStore the token relation store + * @param expiryValidator the expiry validator */ protected long changeSupply( @NonNull final Token token, @@ -120,7 +133,8 @@ protected long changeSupply( @NonNull final ResponseCodeEnum invalidSupplyCode, @NonNull final WritableAccountStore accountStore, @NonNull final WritableTokenStore tokenStore, - @NonNull final WritableTokenRelationStore tokenRelationStore) { + @NonNull final WritableTokenRelationStore tokenRelationStore, + @NonNull final ExpiryValidator expiryValidator) { requireNonNull(token); requireNonNull(treasuryRel); requireNonNull(invalidSupplyCode); @@ -140,8 +154,8 @@ protected long changeSupply( validateTrue(token.maxSupply() >= newTotalSupply, TOKEN_MAX_SUPPLY_REACHED); } - final var treasuryAccount = accountStore.get(treasuryRel.accountId()); - validateTrue(treasuryAccount != null, INVALID_TREASURY_ACCOUNT_FOR_TOKEN); + final var treasuryAccount = TokenHandlerHelper.getIfUsable( + treasuryRel.accountId(), accountStore, expiryValidator, INVALID_TREASURY_ACCOUNT_FOR_TOKEN); final long newTreasuryBalance = treasuryRel.balance() + amount; validateTrue(newTreasuryBalance >= 0, INSUFFICIENT_TOKEN_BALANCE); diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoApproveAllowanceHandler.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoApproveAllowanceHandler.java index c93d986638e3..51a843f38b09 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoApproveAllowanceHandler.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoApproveAllowanceHandler.java @@ -41,6 +41,7 @@ import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.HederaFunctionality; +import com.hedera.hapi.node.base.NftID; import com.hedera.hapi.node.base.SubType; import com.hedera.hapi.node.base.TokenID; import com.hedera.hapi.node.state.token.Account; @@ -56,9 +57,11 @@ import com.hedera.node.app.service.token.impl.WritableAccountStore; import com.hedera.node.app.service.token.impl.WritableNftStore; import com.hedera.node.app.service.token.impl.WritableTokenStore; +import com.hedera.node.app.service.token.impl.util.TokenHandlerHelper; import com.hedera.node.app.service.token.impl.validators.ApproveAllowanceValidator; import com.hedera.node.app.spi.fees.FeeContext; import com.hedera.node.app.spi.fees.Fees; +import com.hedera.node.app.spi.validation.ExpiryValidator; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.spi.workflows.HandleException; import com.hedera.node.app.spi.workflows.PreCheckException; @@ -186,8 +189,8 @@ public void handle(@NonNull final HandleContext context) throws HandleException final var accountStore = context.storeFactory().writableStore(WritableAccountStore.class); // Validate payer account exists - final var payerAccount = accountStore.getAccountById(payer); - validateTrue(payerAccount != null, INVALID_PAYER_ACCOUNT_ID); + final var payerAccount = TokenHandlerHelper.getIfUsable( + payer, accountStore, context.expiryValidator(), INVALID_PAYER_ACCOUNT_ID); // Validate the transaction body fields that include state or configuration. validateSemantics(context, payerAccount, accountStore); @@ -241,26 +244,36 @@ private void approveAllowance( /* --- Apply changes to state --- */ final var allowanceMaxAccountLimit = hederaConfig.allowancesMaxAccountLimit(); - applyCryptoAllowances(cryptoAllowances, payerId, accountStore, allowanceMaxAccountLimit); - applyFungibleTokenAllowances(tokenAllowances, payerId, accountStore, allowanceMaxAccountLimit); + final var expiryValidator = context.expiryValidator(); + applyCryptoAllowances(cryptoAllowances, payerId, accountStore, allowanceMaxAccountLimit, expiryValidator); + applyFungibleTokenAllowances(tokenAllowances, payerId, accountStore, allowanceMaxAccountLimit, expiryValidator); applyNftAllowances( - nftAllowances, payerId, accountStore, tokenStore, uniqueTokenStore, allowanceMaxAccountLimit); + nftAllowances, + payerId, + accountStore, + tokenStore, + uniqueTokenStore, + allowanceMaxAccountLimit, + expiryValidator); } /** * Applies all changes needed for Crypto allowances from the transaction. * If the spender already has an allowance, the allowance value will be replaced with values * from transaction. If the amount specified is 0, the allowance will be removed. - * @param cryptoAllowances the list of crypto allowances - * @param payerId the payer account id - * @param accountStore the account store + * + * @param cryptoAllowances the list of crypto allowances + * @param payerId the payer account id + * @param accountStore the account store * @param allowanceMaxAccountLimit the {@link HederaConfig} + * @param expiryValidator the expiry validator */ private void applyCryptoAllowances( @NonNull final List cryptoAllowances, @NonNull final AccountID payerId, @NonNull final WritableAccountStore accountStore, - final int allowanceMaxAccountLimit) { + final int allowanceMaxAccountLimit, + @NonNull final ExpiryValidator expiryValidator) { requireNonNull(cryptoAllowances); requireNonNull(payerId); requireNonNull(accountStore); @@ -268,7 +281,7 @@ private void applyCryptoAllowances( for (final var allowance : cryptoAllowances) { final var owner = allowance.owner(); final var spender = allowance.spenderOrThrow(); - final var effectiveOwner = getEffectiveOwnerAccount(owner, payerId, accountStore); + final var effectiveOwner = getEffectiveOwnerAccount(owner, payerId, accountStore, expiryValidator); final var mutableAllowances = new ArrayList<>(effectiveOwner.cryptoAllowances()); final var amount = allowance.amount(); @@ -318,23 +331,25 @@ private void updateCryptoAllowance( * Applies all changes needed for fungible token allowances from the transaction. If the key * {token, spender} already has an allowance, the allowance value will be replaced with values * from transaction. - * @param tokenAllowances the list of token allowances - * @param payerId the payer account id - * @param accountStore the account store + * @param tokenAllowances the list of token allowances + * @param payerId the payer account id + * @param accountStore the account store * @param allowanceMaxAccountLimit the {@link HederaConfig} allowance max account limit + * @param expiryValidator the expiry validator */ private void applyFungibleTokenAllowances( @NonNull final List tokenAllowances, @NonNull final AccountID payerId, @NonNull final WritableAccountStore accountStore, - final int allowanceMaxAccountLimit) { + final int allowanceMaxAccountLimit, + @NonNull final ExpiryValidator expiryValidator) { for (final var allowance : tokenAllowances) { final var owner = allowance.owner(); final var amount = allowance.amount(); final var tokenId = allowance.tokenIdOrThrow(); final var spender = allowance.spenderOrThrow(); - final var effectiveOwner = getEffectiveOwnerAccount(owner, payerId, accountStore); + final var effectiveOwner = getEffectiveOwnerAccount(owner, payerId, accountStore, expiryValidator); final var mutableTokenAllowances = new ArrayList<>(effectiveOwner.tokenAllowances()); updateTokenAllowance(mutableTokenAllowances, amount, spender, tokenId); @@ -387,12 +402,13 @@ private void updateTokenAllowance( * spenderNum} doesn't exist in the map the allowance will be inserted. If the key exists, * existing allowance values will be replaced with new allowances given in operation * - * @param nftAllowances the list of nft allowances - * @param payerId the payer account id - * @param accountStore the account store - * @param tokenStore the token store - * @param uniqueTokenStore the unique token store + * @param nftAllowances the list of nft allowances + * @param payerId the payer account id + * @param accountStore the account store + * @param tokenStore the token store + * @param uniqueTokenStore the unique token store * @param allowanceMaxAccountLimit the {@link HederaConfig} config allowance max account limit + * @param expiryValidator the expiry validator */ protected void applyNftAllowances( final List nftAllowances, @@ -400,13 +416,14 @@ protected void applyNftAllowances( @NonNull final WritableAccountStore accountStore, @NonNull final WritableTokenStore tokenStore, @NonNull final WritableNftStore uniqueTokenStore, - final int allowanceMaxAccountLimit) { + final int allowanceMaxAccountLimit, + @NonNull final ExpiryValidator expiryValidator) { for (final var allowance : nftAllowances) { final var owner = allowance.owner(); final var tokenId = allowance.tokenIdOrThrow(); final var spender = allowance.spenderOrThrow(); - final var effectiveOwner = getEffectiveOwnerAccount(owner, payerId, accountStore); + final var effectiveOwner = getEffectiveOwnerAccount(owner, payerId, accountStore, expiryValidator); final var mutableNftAllowances = new ArrayList<>(effectiveOwner.approveForAllNftAllowances()); if (allowance.hasApprovedForAll()) { @@ -456,8 +473,11 @@ public void updateSpender( final var serialsSet = new HashSet<>(serialNums); for (final var serialNum : serialsSet) { - final var nft = uniqueTokenStore.get(tokenId, serialNum); - final var token = tokenStore.get(tokenId); + final var nftId = + NftID.newBuilder().serialNumber(serialNum).tokenId(tokenId).build(); + final var nft = TokenHandlerHelper.getIfUsable(nftId, uniqueTokenStore); + final var token = TokenHandlerHelper.getIfUsable( + tokenId, tokenStore, TokenHandlerHelper.TokenValidations.PERMIT_PAUSED); final AccountID accountOwner = owner.accountId(); validateTrue(isValidOwner(nft, accountOwner, token), SENDER_DOES_NOT_OWN_NFT_SERIAL_NO); @@ -508,15 +528,18 @@ private int lookupSpenderAndToken( * Returns the effective owner account. If the owner is not present or owner is same as payer. * Since we are modifying the payer account in the same transaction for each allowance if owner is not specified, * we need to get the payer account each time from the modifications map. - * @param owner owner of the allowance - * @param payerId payer of the transaction - * @param accountStore account store + * + * @param owner owner of the allowance + * @param payerId payer of the transaction + * @param accountStore account store + * @param expiryValidator the expiry validator * @return effective owner account */ private static Account getEffectiveOwnerAccount( @Nullable final AccountID owner, @NonNull final AccountID payerId, - @NonNull final ReadableAccountStore accountStore) { + @NonNull final ReadableAccountStore accountStore, + @NonNull final ExpiryValidator expiryValidator) { final var ownerNum = owner != null ? owner.accountNumOrElse(0L) : 0L; if (ownerNum == 0 || ownerNum == payerId.accountNumOrThrow()) { // The payer would have been modified in the same transaction for previous allowances @@ -524,8 +547,8 @@ private static Account getEffectiveOwnerAccount( return accountStore.getAccountById(payerId); } else { // If owner is in modifications get the modified account from state - final var ownerAccount = accountStore.getAccountById(owner); - validateTrue(ownerAccount != null, INVALID_ALLOWANCE_OWNER_ID); + final var ownerAccount = + TokenHandlerHelper.getIfUsable(owner, accountStore, expiryValidator, INVALID_ALLOWANCE_OWNER_ID); return ownerAccount; } } diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoDeleteAllowanceHandler.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoDeleteAllowanceHandler.java index 14684156873c..78812d9b856c 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoDeleteAllowanceHandler.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoDeleteAllowanceHandler.java @@ -18,7 +18,6 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.EMPTY_ALLOWANCES; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ALLOWANCE_OWNER_ID; -import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_NFT_ID; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_PAYER_ACCOUNT_ID; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOKEN_ID; import static com.hedera.hapi.node.base.ResponseCodeEnum.SENDER_DOES_NOT_OWN_NFT_SERIAL_NO; @@ -33,6 +32,7 @@ import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.HederaFunctionality; +import com.hedera.hapi.node.base.NftID; import com.hedera.hapi.node.base.SubType; import com.hedera.hapi.node.base.TokenID; import com.hedera.hapi.node.state.token.Account; @@ -42,6 +42,7 @@ import com.hedera.node.app.service.token.impl.WritableAccountStore; import com.hedera.node.app.service.token.impl.WritableNftStore; import com.hedera.node.app.service.token.impl.WritableTokenStore; +import com.hedera.node.app.service.token.impl.util.TokenHandlerHelper; import com.hedera.node.app.service.token.impl.validators.DeleteAllowanceValidator; import com.hedera.node.app.spi.fees.FeeContext; import com.hedera.node.app.spi.fees.Fees; @@ -113,8 +114,8 @@ public void handle(@NonNull final HandleContext context) throws HandleException final var accountStore = context.storeFactory().writableStore(WritableAccountStore.class); // validate payer account exists - final var payerAccount = accountStore.getAccountById(payer); - validateTrue(payerAccount != null, INVALID_PAYER_ACCOUNT_ID); + final var payerAccount = TokenHandlerHelper.getIfUsable( + payer, accountStore, context.expiryValidator(), INVALID_PAYER_ACCOUNT_ID); // validate the transaction body fields that include state or configuration // We can use payerAccount for validations since it's not mutated in validateSemantics @@ -180,9 +181,10 @@ private void deleteNftSerials( final var owner = getEffectiveOwner(allowance.owner(), payerAccount, accountStore, expiryValidator); final var token = tokenStore.get(tokenId); for (final var serial : serialNums) { - final var nft = nftStore.get(tokenId, serial); + final var nftId = + NftID.newBuilder().serialNumber(serial).tokenId(tokenId).build(); + final var nft = TokenHandlerHelper.getIfUsable(nftId, nftStore); - validateTrue(nft != null, INVALID_NFT_ID); final AccountID accountOwner = owner.accountId(); validateTrue(isValidOwner(nft, accountOwner, token), SENDER_DOES_NOT_OWN_NFT_SERIAL_NO); diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoUpdateHandler.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoUpdateHandler.java index ea67624613a8..4cd8e94db1f7 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoUpdateHandler.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoUpdateHandler.java @@ -55,6 +55,7 @@ import com.hedera.node.app.service.token.CryptoSignatureWaivers; import com.hedera.node.app.service.token.ReadableAccountStore; import com.hedera.node.app.service.token.impl.WritableAccountStore; +import com.hedera.node.app.service.token.impl.util.TokenHandlerHelper; import com.hedera.node.app.service.token.impl.validators.StakingValidator; import com.hedera.node.app.service.token.records.CryptoUpdateStreamBuilder; import com.hedera.node.app.spi.fees.FeeCalculator; @@ -147,8 +148,8 @@ public void handle(@NonNull final HandleContext context) { // validate update account exists final var accountStore = context.storeFactory().writableStore(WritableAccountStore.class); - final var targetAccount = accountStore.get(target); - validateTrue(targetAccount != null, INVALID_ACCOUNT_ID); + final var targetAccount = + TokenHandlerHelper.getIfUsable(target, accountStore, context.expiryValidator(), INVALID_ACCOUNT_ID); context.attributeValidator().validateMemo(op.memo()); // Customize the account based on fields set in transaction body diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenAccountWipeHandler.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenAccountWipeHandler.java index eca905121322..63c9074cff55 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenAccountWipeHandler.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenAccountWipeHandler.java @@ -19,7 +19,6 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_DOES_NOT_OWN_WIPED_NFT; import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_KYC_NOT_GRANTED_FOR_TOKEN; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ACCOUNT_ID; -import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_NFT_ID; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOKEN_ID; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_WIPING_AMOUNT; import static com.hedera.node.app.hapi.fees.usage.SingletonUsageProperties.USAGE_PROPERTIES; @@ -169,8 +168,11 @@ public void handle(@NonNull final HandleContext context) throws HandleException // Load and validate the nfts for (final Long nftSerial : nftSerialNums) { - final var nft = nftStore.get(tokenId, nftSerial); - validateTrue(nft != null, INVALID_NFT_ID); + final var nftId = NftID.newBuilder() + .serialNumber(nftSerial) + .tokenId(tokenId) + .build(); + final var nft = TokenHandlerHelper.getIfUsable(nftId, nftStore); final var nftOwner = nft.ownerId(); validateTrue(Objects.equals(nftOwner, unaliasedId), ACCOUNT_DOES_NOT_OWN_WIPED_NFT); diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenAssociateToAccountHandler.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenAssociateToAccountHandler.java index 1a51c5521298..288a1577a1b2 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenAssociateToAccountHandler.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenAssociateToAccountHandler.java @@ -16,7 +16,6 @@ package com.hedera.node.app.service.token.impl.handlers; -import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_DELETED; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ACCOUNT_ID; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOKEN_ID; import static com.hedera.hapi.node.base.ResponseCodeEnum.MAX_ENTITIES_IN_PRICE_REGIME_HAVE_BEEN_CREATED; @@ -29,7 +28,6 @@ import static com.hedera.node.app.service.token.impl.handlers.BaseCryptoHandler.hasAccountNumOrAlias; import static com.hedera.node.app.service.token.impl.util.TokenHandlerHelper.getIfUsable; import static com.hedera.node.app.spi.fees.Fees.CONSTANT_FEE_DATA; -import static com.hedera.node.app.spi.workflows.HandleException.validateFalse; import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; import static com.hedera.node.app.spi.workflows.PreCheckException.validateFalsePreCheck; import static com.hedera.node.app.spi.workflows.PreCheckException.validateTruePreCheck; @@ -50,9 +48,11 @@ import com.hedera.node.app.service.token.ReadableTokenStore; import com.hedera.node.app.service.token.impl.WritableAccountStore; import com.hedera.node.app.service.token.impl.WritableTokenRelationStore; +import com.hedera.node.app.service.token.impl.util.TokenHandlerHelper; import com.hedera.node.app.service.token.impl.validators.TokenListChecks; import com.hedera.node.app.spi.fees.FeeContext; import com.hedera.node.app.spi.fees.Fees; +import com.hedera.node.app.spi.validation.ExpiryValidator; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.spi.workflows.HandleException; import com.hedera.node.app.spi.workflows.PreCheckException; @@ -102,7 +102,14 @@ public void handle(@NonNull final HandleContext context) throws HandleException final var accountStore = storeFactory.writableStore(WritableAccountStore.class); final var tokenRelStore = storeFactory.writableStore(WritableTokenRelationStore.class); final var validated = validateSemantics( - tokenIds, op.accountOrThrow(), tokensConfig, entitiesConfig, accountStore, tokenStore, tokenRelStore); + tokenIds, + op.accountOrThrow(), + tokensConfig, + entitiesConfig, + accountStore, + tokenStore, + tokenRelStore, + context.expiryValidator()); // Now that we've validated we can link all the new token IDs to the account, // create the corresponding token relations and update the account @@ -133,7 +140,8 @@ private Validated validateSemantics( @NonNull final EntitiesConfig entitiesConfig, @NonNull final WritableAccountStore accountStore, @NonNull final ReadableTokenStore tokenStore, - @NonNull final WritableTokenRelationStore tokenRelStore) { + @NonNull final WritableTokenRelationStore tokenRelStore, + @NonNull final ExpiryValidator expiryValidator) { requireNonNull(tokenConfig); requireNonNull(entitiesConfig); @@ -142,10 +150,9 @@ private Validated validateSemantics( isTotalNumTokenRelsWithinMax(tokenIds.size(), tokenRelStore, tokenConfig.maxAggregateRels()), MAX_ENTITIES_IN_PRICE_REGIME_HAVE_BEEN_CREATED); - // Check that the account exists - final var account = accountStore.get(accountId); - validateTrue(account != null, INVALID_ACCOUNT_ID); - validateFalse(account.deleted(), ACCOUNT_DELETED); + // Check that the account is usable + final var account = + TokenHandlerHelper.getIfUsable(accountId, accountStore, expiryValidator, INVALID_ACCOUNT_ID); // Check that the given tokens exist and are usable final var tokens = new ArrayList(); diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenBurnHandler.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenBurnHandler.java index 4942d0fce953..2c8ad612ef74 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenBurnHandler.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenBurnHandler.java @@ -136,7 +136,8 @@ public void handle(@NonNull final HandleContext context) throws HandleException INVALID_TOKEN_BURN_AMOUNT, accountStore, tokenStore, - tokenRelStore); + tokenRelStore, + context.expiryValidator()); record.newTotalSupply(newTotalSupply); } else { validateTrue(!nftSerialNums.isEmpty(), INVALID_TOKEN_BURN_METADATA); @@ -152,7 +153,14 @@ public void handle(@NonNull final HandleContext context) throws HandleException // Update counts for accounts and token rels final var newTotalSupply = changeSupply( - token, treasuryRel, -nftSerialNums.size(), FAIL_INVALID, accountStore, tokenStore, tokenRelStore); + token, + treasuryRel, + -nftSerialNums.size(), + FAIL_INVALID, + accountStore, + tokenStore, + tokenRelStore, + context.expiryValidator()); // Update treasury's NFT count final var treasuryAcct = accountStore.get(token.treasuryAccountIdOrThrow()); diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenCreateHandler.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenCreateHandler.java index 280cee2a2fc8..7f464a7c8cc1 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenCreateHandler.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenCreateHandler.java @@ -163,7 +163,14 @@ public void handle(@NonNull final HandleContext context) { final var treasuryRel = requireNonNull(tokenRelationStore.get(op.treasuryOrThrow(), newTokenId)); if (op.initialSupply() > 0) { // This keeps modified token with minted balance into modifications in token store - mintFungible(newToken, treasuryRel, op.initialSupply(), accountStore, tokenStore, tokenRelationStore); + mintFungible( + newToken, + treasuryRel, + op.initialSupply(), + accountStore, + tokenStore, + tokenRelationStore, + context.expiryValidator()); } // Increment treasury's title count final var treasuryAccount = requireNonNull(accountStore.get(treasuryRel.accountIdOrThrow())); diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenFreezeAccountHandler.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenFreezeAccountHandler.java index dc83931d7d05..cf0f278d802d 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenFreezeAccountHandler.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenFreezeAccountHandler.java @@ -19,7 +19,6 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ACCOUNT_ID; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOKEN_ID; import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKEN_HAS_NO_FREEZE_KEY; -import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKEN_NOT_ASSOCIATED_TO_ACCOUNT; import static com.hedera.node.app.hapi.fees.usage.token.TokenOpsUsageUtils.TOKEN_OPS_USAGE_UTILS; import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; import static java.util.Objects.requireNonNull; @@ -143,12 +142,8 @@ private TokenRelation validateSemantics( // Check that token exists TokenHandlerHelper.getIfUsable(tokenId, tokenStore); - // Check that the token is associated to the account - final var tokenRel = tokenRelStore.getForModify(accountId, tokenId); - validateTrue(tokenRel != null, TOKEN_NOT_ASSOCIATED_TO_ACCOUNT); - - // Return the token relation - return tokenRel; + // Check that the token is associated to the account, and return it + return TokenHandlerHelper.getIfUsable(accountId, tokenId, tokenRelStore); } @NonNull diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenMintHandler.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenMintHandler.java index 43f6f7d83b04..75c467ff1324 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenMintHandler.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenMintHandler.java @@ -57,6 +57,7 @@ import com.hedera.node.app.service.token.records.TokenMintStreamBuilder; import com.hedera.node.app.spi.fees.FeeContext; import com.hedera.node.app.spi.fees.Fees; +import com.hedera.node.app.spi.validation.ExpiryValidator; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.spi.workflows.HandleException; import com.hedera.node.app.spi.workflows.PreCheckException; @@ -138,8 +139,14 @@ public void handle(@NonNull final HandleContext context) throws HandleException if (token.tokenType() == TokenType.FUNGIBLE_COMMON) { validateTrue(op.amount() >= 0, INVALID_TOKEN_MINT_AMOUNT); // we need to know if treasury mint while creation to ignore supply key exist or not. - long newTotalSupply = - mintFungible(token, treasuryRel, op.amount(), accountStore, tokenStore, tokenRelStore); + long newTotalSupply = mintFungible( + token, + treasuryRel, + op.amount(), + accountStore, + tokenStore, + tokenRelStore, + context.expiryValidator()); recordBuilder.newTotalSupply(newTotalSupply); } else { // get the config needed for validation @@ -159,7 +166,8 @@ public void handle(@NonNull final HandleContext context) throws HandleException accountStore, tokenStore, tokenRelStore, - nftStore); + nftStore, + context.expiryValidator()); recordBuilder.newTotalSupply(tokenStore.get(tokenId).totalSupply()); recordBuilder.serialNumbers(mintedSerials); } @@ -182,14 +190,15 @@ private void validateSemantics(final HandleContext context) { * serial number of the given base unique token, and increments total owned nfts of the * non-fungible token. * - * @param token - the token to mint nfts for - * @param treasuryRel - the treasury relation of the token - * @param metadata - the metadata of the nft to be minted - * @param consensusTime - the consensus time of the transaction - * @param accountStore - the account store - * @param tokenStore - the token store - * @param tokenRelStore - the token relation store - * @param nftStore - the nft store + * @param token - the token to mint nfts for + * @param treasuryRel - the treasury relation of the token + * @param metadata - the metadata of the nft to be minted + * @param consensusTime - the consensus time of the transaction + * @param accountStore - the account store + * @param tokenStore - the token store + * @param tokenRelStore - the token relation store + * @param nftStore - the nft store + * @param expiryValidator - the expiry validator */ private List mintNonFungible( final Token token, @@ -199,7 +208,8 @@ private List mintNonFungible( @NonNull final WritableAccountStore accountStore, @NonNull final WritableTokenStore tokenStore, @NonNull final WritableTokenRelationStore tokenRelStore, - @NonNull final WritableNftStore nftStore) { + @NonNull final WritableNftStore nftStore, + @NonNull final ExpiryValidator expiryValidator) { final var metadataCount = metadata.size(); validateFalse(metadata.isEmpty(), INVALID_TOKEN_MINT_METADATA); @@ -207,15 +217,23 @@ private List mintNonFungible( final var tokenId = treasuryRel.tokenId(); // get the treasury account - var treasuryAccount = accountStore.get(treasuryRel.accountIdOrThrow()); - validateTrue(treasuryAccount != null, INVALID_TREASURY_ACCOUNT_FOR_TOKEN); + var treasuryAccount = TokenHandlerHelper.getIfUsable( + treasuryRel.accountIdOrThrow(), accountStore, expiryValidator, INVALID_TREASURY_ACCOUNT_FOR_TOKEN); // get the latest serial number minted for the token var currentSerialNumber = token.lastUsedSerialNumber(); validateTrue((currentSerialNumber + metadataCount) <= MAX_SERIAL_NO_ALLOWED, SERIAL_NUMBER_LIMIT_REACHED); // Change the supply on token - changeSupply(token, treasuryRel, metadataCount, FAIL_INVALID, accountStore, tokenStore, tokenRelStore); + changeSupply( + token, + treasuryRel, + metadataCount, + FAIL_INVALID, + accountStore, + tokenStore, + tokenRelStore, + expiryValidator); // Since changeSupply call above modifies the treasuryAccount, we need to get the modified treasuryAccount treasuryAccount = accountStore.get(treasuryRel.accountIdOrThrow()); // The token is modified in previous step, so we need to get the modified token diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenUpdateHandler.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenUpdateHandler.java index 2752d16a73ed..8977c733730a 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenUpdateHandler.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenUpdateHandler.java @@ -58,6 +58,7 @@ import com.hedera.node.app.service.token.impl.WritableAccountStore; import com.hedera.node.app.service.token.impl.WritableTokenRelationStore; import com.hedera.node.app.service.token.impl.WritableTokenStore; +import com.hedera.node.app.service.token.impl.util.TokenHandlerHelper; import com.hedera.node.app.service.token.impl.util.TokenKey; import com.hedera.node.app.service.token.impl.validators.TokenUpdateValidator; import com.hedera.node.app.service.token.records.TokenUpdateStreamBuilder; @@ -170,8 +171,8 @@ public void handle(@NonNull final HandleContext context) throws HandleException // Treasury can be modified when it owns NFTs when the property "tokens.nfts.useTreasuryWildcards" // is enabled. if (!tokensConfig.nftsUseTreasuryWildcards() && token.tokenType().equals(NON_FUNGIBLE_UNIQUE)) { - final var existingTreasuryRel = tokenRelStore.get(existingTreasury, tokenId); - validateTrue(existingTreasuryRel != null, INVALID_TREASURY_ACCOUNT_FOR_TOKEN); + final var existingTreasuryRel = + TokenHandlerHelper.getIfUsable(existingTreasury, tokenId, tokenRelStore); final var tokenRelBalance = existingTreasuryRel.balance(); validateTrue(tokenRelBalance == 0, CURRENT_TREASURY_STILL_OWNS_NFTS); } diff --git a/hedera-node/hedera-token-service-impl/src/main/java/module-info.java b/hedera-node/hedera-token-service-impl/src/main/java/module-info.java index 8b2c1e55c8b1..e5ba7bd85c1e 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/module-info.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/module-info.java @@ -28,7 +28,8 @@ exports com.hedera.node.app.service.token.impl.handlers to com.hedera.node.app, - com.hedera.node.app.service.token.impl.test; + com.hedera.node.app.service.token.impl.test, + com.hedera.node.test.clients; exports com.hedera.node.app.service.token.impl; exports com.hedera.node.app.service.token.impl.api to com.hedera.node.app, diff --git a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoDeleteAllowanceHandlerTest.java b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoDeleteAllowanceHandlerTest.java index 5536c049a51d..fe206a575965 100644 --- a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoDeleteAllowanceHandlerTest.java +++ b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoDeleteAllowanceHandlerTest.java @@ -71,6 +71,7 @@ public void setUp() { given(handleContext.configuration()).willReturn(configuration); given(handleContext.expiryValidator()).willReturn(expiryValidator); + given(expiryValidator.expirationStatus(any(), anyBoolean(), anyLong())).willReturn(OK); } @Test diff --git a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoUpdateHandlerTest.java b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoUpdateHandlerTest.java index fff176cb6b2a..f29e8e781472 100644 --- a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoUpdateHandlerTest.java +++ b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoUpdateHandlerTest.java @@ -27,6 +27,7 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_EXPIRATION_TIME; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_STAKING_ID; import static com.hedera.hapi.node.base.ResponseCodeEnum.MEMO_TOO_LONG; +import static com.hedera.hapi.node.base.ResponseCodeEnum.OK; import static com.hedera.hapi.node.base.ResponseCodeEnum.PROXY_ACCOUNT_ID_FIELD_IS_DEPRECATED; import static com.hedera.hapi.node.base.ResponseCodeEnum.REQUESTED_NUM_AUTOMATIC_ASSOCIATIONS_EXCEEDS_ASSOCIATION_LIMIT; import static com.hedera.hapi.node.base.ResponseCodeEnum.STAKING_NOT_ENABLED; @@ -146,6 +147,9 @@ public void setUp() { updateReadableAccountStore(Map.of(updateAccountId.accountNum(), updateAccount, accountNum, account)); lenient().when(handleContext.savepointStack()).thenReturn(stack); lenient().when(stack.getBaseBuilder(CryptoUpdateStreamBuilder.class)).thenReturn(streamBuilder); + lenient() + .when(expiryValidator.expirationStatus(any(), anyBoolean(), anyLong())) + .thenReturn(OK); subject = new CryptoUpdateHandler(waivers); } diff --git a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/TokenAssociateToAccountHandlerTest.java b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/TokenAssociateToAccountHandlerTest.java index a13e38f7a1fa..22bcb3d20355 100644 --- a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/TokenAssociateToAccountHandlerTest.java +++ b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/TokenAssociateToAccountHandlerTest.java @@ -18,6 +18,7 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ACCOUNT_ID; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOKEN_ID; +import static com.hedera.hapi.node.base.ResponseCodeEnum.OK; import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKENS_PER_ACCOUNT_LIMIT_EXCEEDED; import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKEN_ID_REPEATED_IN_TOKEN_LIST; import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKEN_IS_PAUSED; @@ -33,7 +34,11 @@ import static com.hedera.node.app.service.token.impl.test.keys.KeysAndIds.MISC_ACCOUNT; import static com.hedera.node.app.spi.fixtures.workflows.ExceptionConditions.responseCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; @@ -53,6 +58,7 @@ import com.hedera.node.app.service.token.impl.test.handlers.util.ParityTestBase; import com.hedera.node.app.spi.fixtures.workflows.FakePreHandleContext; import com.hedera.node.app.spi.store.StoreFactory; +import com.hedera.node.app.spi.validation.ExpiryValidator; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.spi.workflows.HandleException; import com.hedera.node.app.spi.workflows.PreCheckException; @@ -86,8 +92,14 @@ class TokenAssociateToAccountHandlerTest { @Mock private StoreFactory storeFactory; + @Mock(strictness = LENIENT) + private ExpiryValidator expiryValidator; + @BeforeEach void setUp() { + lenient().when(context.expiryValidator()).thenReturn(expiryValidator); + given(expiryValidator.expirationStatus(any(), anyBoolean(), anyLong())).willReturn(OK); + subject = new TokenAssociateToAccountHandler(); } diff --git a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/TokenBurnHandlerTest.java b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/TokenBurnHandlerTest.java index 8ea4083b7937..42e7614403c9 100644 --- a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/TokenBurnHandlerTest.java +++ b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/TokenBurnHandlerTest.java @@ -26,6 +26,7 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TRANSACTION_BODY; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TREASURY_ACCOUNT_FOR_TOKEN; import static com.hedera.hapi.node.base.ResponseCodeEnum.NOT_SUPPORTED; +import static com.hedera.hapi.node.base.ResponseCodeEnum.OK; import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKEN_HAS_NO_SUPPLY_KEY; import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKEN_IS_PAUSED; import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKEN_NOT_ASSOCIATED_TO_ACCOUNT; @@ -42,6 +43,9 @@ import static com.hedera.node.app.spi.workflows.record.StreamBuilder.ReversingBehavior.REVERSIBLE; import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; @@ -72,6 +76,7 @@ import com.hedera.node.app.spi.fixtures.state.MapWritableStates; import com.hedera.node.app.spi.metrics.StoreMetricsService; import com.hedera.node.app.spi.store.StoreFactory; +import com.hedera.node.app.spi.validation.ExpiryValidator; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.spi.workflows.HandleException; import com.hedera.node.app.spi.workflows.PreCheckException; @@ -868,6 +873,11 @@ private HandleContext mockContext(TransactionBody txn) { given(storeFactory.writableStore(WritableNftStore.class)).willReturn(writableNftStore); given(context.configuration()).willReturn(configuration); lenient().when(context.savepointStack()).thenReturn(stack); + final var expiryValidator = mock(ExpiryValidator.class); + lenient().when(context.expiryValidator()).thenReturn(expiryValidator); + lenient() + .when(expiryValidator.expirationStatus(any(), anyBoolean(), anyLong())) + .thenReturn(OK); return context; } diff --git a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/TokenFreezeAccountHandlerTest.java b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/TokenFreezeAccountHandlerTest.java index ec9a9964c522..17eaed27a331 100644 --- a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/TokenFreezeAccountHandlerTest.java +++ b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/TokenFreezeAccountHandlerTest.java @@ -253,7 +253,7 @@ void tokenRelNotFound() throws HandleException { given(readableTokenStore.getTokenMeta(token)).willReturn(tokenMetaWithFreezeKey()); given(readableAccountStore.getAccountById(ACCOUNT_13257)) .willReturn(Account.newBuilder().accountId(ACCOUNT_13257).build()); - given(tokenRelStore.getForModify(ACCOUNT_13257, token)).willReturn(null); + given(tokenRelStore.get(ACCOUNT_13257, token)).willReturn(null); given(expiryValidator.expirationStatus(EntityType.ACCOUNT, false, 0)) .willReturn(OK); final var txn = newFreezeTxn(token); @@ -273,7 +273,7 @@ void tokenRelFreezeSuccessful() { given(readableTokenStore.getTokenMeta(token)).willReturn(tokenMetaWithFreezeKey()); given(readableAccountStore.getAccountById(ACCOUNT_13257)) .willReturn(Account.newBuilder().accountId(ACCOUNT_13257).build()); - given(tokenRelStore.getForModify(ACCOUNT_13257, token)) + given(tokenRelStore.get(ACCOUNT_13257, token)) .willReturn(TokenRelation.newBuilder() .tokenId(token) .accountId(ACCOUNT_13257) diff --git a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/transfer/AssociateTokenRecipientsStepTest.java b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/transfer/AssociateTokenRecipientsStepTest.java index 213b502b6bec..a27935c60d8a 100644 --- a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/transfer/AssociateTokenRecipientsStepTest.java +++ b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/transfer/AssociateTokenRecipientsStepTest.java @@ -21,6 +21,7 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.lenient; import com.hedera.hapi.node.base.AccountAmount; import com.hedera.hapi.node.base.AccountID; @@ -115,7 +116,9 @@ void givenValidTxn() { .build(); given(handleContext.configuration()).willReturn(configuration); given(handleContext.expiryValidator()).willReturn(expiryValidator); - given(expiryValidator.expirationStatus(any(), anyBoolean(), anyLong())).willReturn(ResponseCodeEnum.OK); + lenient() + .when(expiryValidator.expirationStatus(any(), anyBoolean(), anyLong())) + .thenReturn(ResponseCodeEnum.OK); given(handleContext.savepointStack()).willReturn(stack); } diff --git a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/util/CryptoTokenHandlerTestBase.java b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/util/CryptoTokenHandlerTestBase.java index f98ce345e12d..f573577db529 100644 --- a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/util/CryptoTokenHandlerTestBase.java +++ b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/util/CryptoTokenHandlerTestBase.java @@ -26,8 +26,12 @@ import static java.util.Collections.emptyList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.ContractID; @@ -37,6 +41,7 @@ import com.hedera.hapi.node.base.NftID; import com.hedera.hapi.node.base.PendingAirdropId; import com.hedera.hapi.node.base.PendingAirdropValue; +import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.hapi.node.base.Timestamp; import com.hedera.hapi.node.base.TokenID; import com.hedera.hapi.node.base.TokenSupplyType; @@ -86,6 +91,7 @@ import com.hedera.node.app.spi.fees.Fees; import com.hedera.node.app.spi.metrics.StoreMetricsService; import com.hedera.node.app.spi.store.StoreFactory; +import com.hedera.node.app.spi.validation.ExpiryValidator; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.spi.workflows.PreHandleContext; import com.hedera.node.config.VersionedConfigImpl; @@ -1173,7 +1179,13 @@ protected void givenStoresAndConfig(final HandleContext context) { .willReturn(readableRewardsStore); given(storeFactory.writableStore(WritableNetworkStakingRewardsStore.class)) .willReturn(writableRewardsStore); - given(context.dispatchComputeFees(any(), any(), any())).willReturn(new Fees(1l, 2l, 3l)); + given(context.dispatchComputeFees(any(), any(), any())).willReturn(new Fees(1L, 2L, 3L)); + + final var expiryValidator = mock(ExpiryValidator.class); + lenient().when(context.expiryValidator()).thenReturn(expiryValidator); + lenient() + .when(expiryValidator.expirationStatus(any(), anyBoolean(), anyLong())) + .thenReturn(ResponseCodeEnum.OK); } protected void givenStoresAndConfig(final FinalizeContext context) { diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/RepeatableReason.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/RepeatableReason.java index 35f671274748..778cbdc5471e 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/RepeatableReason.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/RepeatableReason.java @@ -33,4 +33,8 @@ public enum RepeatableReason { * The test needs the handle workflow to be synchronous. */ NEEDS_SYNCHRONOUS_HANDLE_WORKFLOW, + /** + * The test needs to control behavior of the TSS subsystem. + */ + NEEDS_TSS_CONTROL, } 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 e6890865ca81..ccb3046321db 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 @@ -36,6 +36,7 @@ import com.hedera.pbj.runtime.io.buffer.BufferedData; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.hedera.services.bdd.junit.hedera.embedded.fakes.AbstractFakePlatform; +import com.hedera.services.bdd.junit.hedera.embedded.fakes.FakeTssBaseService; import com.hederahashgraph.api.proto.java.AccountID; import com.hederahashgraph.api.proto.java.Query; import com.hederahashgraph.api.proto.java.Response; @@ -93,6 +94,7 @@ public abstract class AbstractEmbeddedHedera implements EmbeddedHedera { protected final AtomicInteger nextNano = new AtomicInteger(0); protected final Hedera hedera; protected final ServicesSoftwareVersion version; + protected final FakeTssBaseService tssBaseService; protected final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); protected AbstractEmbeddedHedera(@NonNull final EmbeddedNode node) { @@ -104,11 +106,13 @@ protected AbstractEmbeddedHedera(@NonNull final EmbeddedNode node) { .collect(toMap(Address::getNodeId, address -> parseAccount(address.getMemo()))); defaultNodeId = addressBook.getNodeId(0); defaultNodeAccountId = fromPbj(accountIds.get(defaultNodeId)); + tssBaseService = new FakeTssBaseService(); hedera = new Hedera( ConstructableRegistry.getInstance(), FakeServicesRegistry.FACTORY, new FakeServiceMigrator(), - this::now); + this::now, + () -> tssBaseService); version = (ServicesSoftwareVersion) hedera.getSoftwareVersion(); Runtime.getRuntime().addShutdownHook(new Thread(executorService::shutdownNow)); } @@ -133,6 +137,11 @@ public FakeState state() { return state; } + @Override + public FakeTssBaseService tssBaseService() { + return tssBaseService; + } + @Override public SoftwareVersion version() { return version; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/EmbeddedHedera.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/EmbeddedHedera.java index 06db83141fdc..1ba65a9d44c3 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/EmbeddedHedera.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/EmbeddedHedera.java @@ -17,6 +17,7 @@ package com.hedera.services.bdd.junit.hedera.embedded; import com.hedera.node.app.fixtures.state.FakeState; +import com.hedera.services.bdd.junit.hedera.embedded.fakes.FakeTssBaseService; import com.hederahashgraph.api.proto.java.AccountID; import com.hederahashgraph.api.proto.java.Query; import com.hederahashgraph.api.proto.java.Response; @@ -42,11 +43,16 @@ public interface EmbeddedHedera { /** * Returns the fake state of the embedded Hedera node. - * * @return the fake state of the embedded Hedera node */ FakeState state(); + /** + * Returns the fake TSS base service of the embedded Hedera node. + * @return the fake TSS base service of the embedded Hedera node + */ + FakeTssBaseService tssBaseService(); + /** * Returns the software version of the embedded Hedera node. * @return the software version of the embedded Hedera node diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/FakeTssBaseService.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/FakeTssBaseService.java new file mode 100644 index 000000000000..0ce0657779e5 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/FakeTssBaseService.java @@ -0,0 +1,106 @@ +/* + * 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.hedera.services.bdd.junit.hedera.embedded.fakes; + +import static com.hedera.node.app.hapi.utils.CommonUtils.noThrowSha384HashOf; +import static java.util.Objects.requireNonNull; + +import com.hedera.node.app.tss.TssBaseService; +import com.hedera.services.bdd.junit.HapiTest; +import com.swirlds.common.utility.CommonUtils; +import com.swirlds.state.spi.SchemaRegistry; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.BiConsumer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * A fake implementation of the {@link TssBaseService} that, + *

        + *
      • Lets the author of an embedded {@link HapiTest} control whether the TSS base service ignores + * signature requests; and, when requests are not ignored,
      • + *
      • "Signs" messages by scheduling callback to its consumers using the SHA-384 hash of the + * message as the signature.
      • + *
      + */ +public class FakeTssBaseService implements TssBaseService { + private static final Logger log = LogManager.getLogger(FakeTssBaseService.class); + + /** + * Copy-on-write list to avoid concurrent modification exceptions if a consumer unregisters + * itself in its callback. + */ + private final List> consumers = new CopyOnWriteArrayList<>(); + + private boolean ignoreRequests = false; + + /** + * When called, will start ignoring any requests for ledger signatures. + */ + public void startIgnoringRequests() { + ignoreRequests = true; + } + + /** + * When called, will stop ignoring any requests for ledger signatures. + */ + public void stopIgnoringRequests() { + ignoreRequests = false; + } + + @Override + public void registerSchemas(@NonNull final SchemaRegistry registry) { + // No-op for now + } + + @Override + public void requestLedgerSignature(@NonNull final byte[] messageHash) { + requireNonNull(messageHash); + if (ignoreRequests) { + return; + } + final var mockSignature = noThrowSha384HashOf(messageHash); + // Simulate asynchronous completion of the ledger signature + CompletableFuture.runAsync(() -> consumers.forEach(consumer -> { + try { + consumer.accept(messageHash, mockSignature); + } catch (Exception e) { + log.error( + "Failed to provide signature {} on message {} to consumer {}", + CommonUtils.hex(mockSignature), + CommonUtils.hex(messageHash), + consumer, + e); + } + })); + } + + @Override + public void registerLedgerSignatureConsumer(@NonNull final BiConsumer consumer) { + requireNonNull(consumer); + consumers.add(consumer); + } + + @Override + public void unregisterLedgerSignatureConsumer(@NonNull final BiConsumer consumer) { + requireNonNull(consumer); + consumers.remove(consumer); + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/BlockStreamAccess.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/BlockStreamAccess.java index 6e67899419bb..5dbcb4867f89 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/BlockStreamAccess.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/BlockStreamAccess.java @@ -34,6 +34,7 @@ import com.hedera.pbj.runtime.io.buffer.Bytes; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; +import java.io.File; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Files; @@ -219,23 +220,29 @@ private Block blockFrom(@NonNull final Path path) { private List orderedBlocksFrom(@NonNull final Path path) throws IOException { try (final var stream = Files.walk(path)) { - return stream.filter(this::isBlockFile) - .sorted(comparing(this::extractBlockNumber)) + return stream.filter(BlockStreamAccess::isBlockFile) + .sorted(comparing(BlockStreamAccess::extractBlockNumber)) .toList(); } } - private boolean isBlockFile(@NonNull final Path path) { + private static boolean isBlockFile(@NonNull final Path path) { return path.toFile().isFile() && extractBlockNumber(path) != -1; } - private long extractBlockNumber(@NonNull final Path path) { - final var fileName = path.getFileName().toString(); + private static long extractBlockNumber(@NonNull final Path path) { + return extractBlockNumber(path.getFileName().toString()); + } + + public static boolean isBlockFile(@NonNull final File file) { + return file.isFile() && extractBlockNumber(file.getName()) != -1; + } + + private static long extractBlockNumber(@NonNull final String fileName) { try { final var blockNumber = fileName.substring(0, fileName.indexOf(UNCOMPRESSED_FILE_EXT)); return Long.parseLong(blockNumber); } catch (Exception ignore) { - log.info("Ignoring non-block file {}", path); } return -1; } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/BlockStreamValidator.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/BlockStreamValidator.java index 2a1b400ebc0c..f6d9924430f6 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/BlockStreamValidator.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/BlockStreamValidator.java @@ -46,14 +46,14 @@ default boolean appliesTo(@NonNull final HapiSpec spec) { } /** - * Validate the given {@link Block}s in the context of the given {@link RecordStreamAccess.Data} and + * Validate the given {@link Block}s in the context of the given {@link StreamFileAccess.RecordStreamData} and * returns a {@link Stream} of {@link Throwable}s representing any validation errors. * @param blocks the blocks to validate * @param data the record stream data * @return a stream of validation errors */ default Stream validationErrorsIn( - @NonNull final List blocks, @NonNull final RecordStreamAccess.Data data) { + @NonNull final List blocks, @NonNull final StreamFileAccess.RecordStreamData data) { try { validateBlockVsRecords(blocks, data); } catch (final Throwable t) { @@ -63,12 +63,12 @@ default Stream validationErrorsIn( } /** - * Validate the given {@link Block}s in the context of the given {@link RecordStreamAccess.Data}. + * Validate the given {@link Block}s in the context of the given {@link StreamFileAccess.RecordStreamData}. * @param blocks the blocks to validate * @param data the record stream data */ default void validateBlockVsRecords( - @NonNull final List blocks, @NonNull final RecordStreamAccess.Data data) { + @NonNull final List blocks, @NonNull final StreamFileAccess.RecordStreamData data) { validateBlocks(blocks); } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/RecordStreamValidator.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/RecordStreamValidator.java index a59c46ba6507..493eb2171f6c 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/RecordStreamValidator.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/RecordStreamValidator.java @@ -22,7 +22,7 @@ import java.util.stream.Stream; public interface RecordStreamValidator { - default Stream validationErrorsIn(@NonNull final RecordStreamAccess.Data data) { + default Stream validationErrorsIn(@NonNull final StreamFileAccess.RecordStreamData data) { try { validateFiles(data.files()); validateRecordsAndSidecars(data.records()); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/StreamDataListener.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/StreamDataListener.java index ed78d084d62e..5df8d3dbbe80 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/StreamDataListener.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/StreamDataListener.java @@ -16,13 +16,21 @@ package com.hedera.services.bdd.junit.support; +import com.hedera.hapi.block.stream.Block; import com.hedera.services.stream.proto.RecordStreamItem; import com.hedera.services.stream.proto.TransactionSidecarRecord; +import edu.umd.cs.findbugs.annotations.NonNull; /** * A listener that receives record stream items and transaction sidecar records. */ public interface StreamDataListener { + /** + * Called when a new block is received. + * @param block the new block + */ + default void onNewBlock(@NonNull final Block block) {} + default void onNewItem(RecordStreamItem item) {} default void onNewSidecar(TransactionSidecarRecord sidecar) {} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/RecordStreamAccess.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/StreamFileAccess.java similarity index 79% rename from hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/RecordStreamAccess.java rename to hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/StreamFileAccess.java index eb8945986591..651f522949c1 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/RecordStreamAccess.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/StreamFileAccess.java @@ -50,24 +50,26 @@ * A singleton that provides near real-time access to the record stream files for all concurrently * executing {@link com.hedera.services.bdd.spec.HapiSpec}'s. */ -public enum RecordStreamAccess { - RECORD_STREAM_ACCESS; +public enum StreamFileAccess { + STREAM_FILE_ACCESS; - private static final Logger log = LogManager.getLogger(RecordStreamAccess.class); + private static final Logger log = LogManager.getLogger(StreamFileAccess.class); private static final int MONITOR_INTERVAL_MS = 250; /** - * A map of record stream file locations to the listeners that are watching them. (In general we - * only validate records from the single node0, but this could change?) + * A map of stream file locations to the listeners that are watching them. + *

      + * Note that in general we only validate stream files from {@code node0}, since if other nodes are producing + * different files they are certain to hit an ISS in very short order. */ - private final Map validatingListeners = new ConcurrentHashMap<>(); + private final Map locationListeners = new ConcurrentHashMap<>(); /** A bit of infrastructure that runs the polling loop for all the listeners. */ private final FileAlterationMonitor monitor = new FileAlterationMonitor(MONITOR_INTERVAL_MS); - public record Data(List records, List files) { - public static Data EMPTY_DATA = new Data(List.of(), List.of()); + public record RecordStreamData(List records, List files) { + public static RecordStreamData EMPTY_DATA = new RecordStreamData(List.of(), List.of()); } /** @@ -82,9 +84,9 @@ public synchronized Runnable subscribe(@NonNull final Path path, @NonNull final requireNonNull(path); requireNonNull(listener); try { - final var unsubscribe = getValidatingListener( - path.toAbsolutePath().normalize().toString()) - .subscribe(listener); + final var alterationListener = + getOrCreateListener(path.toAbsolutePath().normalize().toString()); + final var unsubscribe = alterationListener.subscribe(listener); return () -> { try { unsubscribe.run(); @@ -103,14 +105,14 @@ public synchronized Runnable subscribe(@NonNull final Path path, @NonNull final */ public synchronized void stopMonitorIfNoSubscribers() { // Count the number of subscribers (could derive from more than one concurrent HapiSpec) - final var numSubscribers = validatingListeners.values().stream() - .mapToInt(BroadcastingRecordStreamListener::numListeners) + final var numSubscribers = locationListeners.values().stream() + .mapToInt(StreamFileAlterationListener::numListeners) .sum(); if (numSubscribers == 0) { try { - if (!validatingListeners.isEmpty()) { - log.info("Stopping record stream access monitor (locations were {})", validatingListeners.keySet()); - validatingListeners.clear(); + if (!locationListeners.isEmpty()) { + log.info("Stopping record stream access monitor (locations were {})", locationListeners.keySet()); + locationListeners.clear(); } // Remove all observers and stop the monitor monitor.getObservers().forEach(monitor::removeObserver); @@ -121,26 +123,6 @@ public synchronized void stopMonitorIfNoSubscribers() { } } - /** - * If the given location is not already being watched, starts a new listener for it and returns - * the listener. - * - * @param loc the record stream file location to watch - * @return the listener for the given location - * @throws Exception if there is an error starting the listener - */ - public synchronized BroadcastingRecordStreamListener getValidatingListener(final String loc) throws Exception { - if (!validatingListeners.containsKey(loc)) { - var fAtLoc = relocatedIfNotPresentWithCurrentPathPrefix(new File(loc), "..", TEST_CLIENTS_PREFIX); - if (!fAtLoc.exists()) { - Files.createDirectories(fAtLoc.toPath()); - } - validatingListeners.put(loc, newValidatingListener(fAtLoc.getAbsolutePath())); - log.info("Started record stream listener for {}", loc); - } - return validatingListeners.get(loc); - } - /** * Reads the record and sidecar stream files from a given directory. * @@ -149,7 +131,7 @@ public synchronized BroadcastingRecordStreamListener getValidatingListener(final * @return the list of record and sidecar files * @throws IOException if there is an error reading the files */ - public Data readStreamDataFrom(String loc, final String relativeSidecarLoc) throws IOException { + public RecordStreamData readStreamDataFrom(String loc, final String relativeSidecarLoc) throws IOException { return readStreamDataFrom(loc, relativeSidecarLoc, f -> true); } @@ -163,7 +145,7 @@ public Data readStreamDataFrom(String loc, final String relativeSidecarLoc) thro * @return the list of record and sidecar files * @throws IOException if there is an error reading the files */ - public Data readStreamDataFrom( + public RecordStreamData readStreamDataFrom( @NonNull String loc, @NonNull final String relativeSidecarLoc, @NonNull final Predicate inclusionTest) @@ -191,11 +173,11 @@ public Data readStreamDataFrom( sidecarFilesByRecordFile .getOrDefault(parseRecordFileConsensusTime(f), Collections.emptyList()) .stream() - .map(RecordStreamAccess::ensurePresentSidecarFile) + .map(StreamFileAccess::ensurePresentSidecarFile) .toList()); }) .toList(); - return new Data(recordsWithSideCars, fullRecordFiles); + return new RecordStreamData(recordsWithSideCars, fullRecordFiles); } public static RecordStreamFile ensurePresentRecordFile(final String f) { @@ -218,9 +200,29 @@ public static SidecarFile ensurePresentSidecarFile(final String f) { } } - private BroadcastingRecordStreamListener newValidatingListener(final String loc) throws Exception { + /** + * If the given location is not already being watched, starts a new listener for it and returns + * the listener. + * + * @param loc the record stream file location to watch + * @return the listener for the given location + * @throws Exception if there is an error starting the listener + */ + private StreamFileAlterationListener getOrCreateListener(final String loc) throws Exception { + if (!locationListeners.containsKey(loc)) { + final var fAtLoc = relocatedIfNotPresentWithCurrentPathPrefix(new File(loc), "..", TEST_CLIENTS_PREFIX); + if (!fAtLoc.exists()) { + Files.createDirectories(fAtLoc.toPath()); + } + locationListeners.put(loc, newValidatingListener(fAtLoc.getAbsolutePath())); + log.info("Started stream file listener for {}", loc); + } + return locationListeners.get(loc); + } + + private StreamFileAlterationListener newValidatingListener(final String loc) throws Exception { final var observer = new FileAlterationObserver(loc); - final var listener = new BroadcastingRecordStreamListener(); + final var listener = new StreamFileAlterationListener(); observer.addListener(listener); monitor.addObserver(observer); try { diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/BroadcastingRecordStreamListener.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/StreamFileAlterationListener.java similarity index 82% rename from hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/BroadcastingRecordStreamListener.java rename to hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/StreamFileAlterationListener.java index d68400d72884..b3259c7f6967 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/BroadcastingRecordStreamListener.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/StreamFileAlterationListener.java @@ -18,11 +18,11 @@ import static com.hedera.node.app.hapi.utils.exports.recordstreaming.RecordStreamingUtils.isRecordFile; import static com.hedera.node.app.hapi.utils.exports.recordstreaming.RecordStreamingUtils.isSidecarFile; +import static com.hedera.services.bdd.junit.support.BlockStreamAccess.isBlockFile; import static java.util.concurrent.TimeUnit.MILLISECONDS; import edu.umd.cs.findbugs.annotations.NonNull; import java.io.File; -import java.io.UncheckedIOException; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Consumer; @@ -34,8 +34,8 @@ * A small utility class that listens for record stream files and provides them to any subscribed * listeners. */ -public class BroadcastingRecordStreamListener extends FileAlterationListenerAdaptor { - private static final Logger log = LogManager.getLogger(BroadcastingRecordStreamListener.class); +public class StreamFileAlterationListener extends FileAlterationListenerAdaptor { + private static final Logger log = LogManager.getLogger(StreamFileAlterationListener.class); private static final int NUM_RETRIES = 32; private static final long RETRY_BACKOFF_MS = 500L; @@ -60,6 +60,7 @@ public Runnable subscribe(final StreamDataListener listener) { enum FileType { RECORD_STREAM_FILE, SIDE_CAR_FILE, + BLOCK_FILE, OTHER } @@ -68,6 +69,7 @@ public void onFileCreate(final File file) { switch (typeOf(file)) { case RECORD_STREAM_FILE -> retryExposingVia(this::exposeItems, "record", file); case SIDE_CAR_FILE -> retryExposingVia(this::exposeSidecars, "sidecar", file); + case BLOCK_FILE -> retryExposingVia(this::exposeBlock, "block", file); case OTHER -> { // Nothing to expose } @@ -87,7 +89,7 @@ private void retryExposingVia( fileType, f.getAbsolutePath()); return; - } catch (UncheckedIOException e) { + } catch (Exception e) { if (retryCount < NUM_RETRIES) { try { MILLISECONDS.sleep(RETRY_BACKOFF_MS); @@ -103,13 +105,19 @@ private void retryExposingVia( } } + private void exposeBlock(@NonNull final File file) { + final var block = + BlockStreamAccess.BLOCK_STREAM_ACCESS.readBlocks(file.toPath()).getFirst(); + listeners.forEach(l -> l.onNewBlock(block)); + } + private void exposeSidecars(final File file) { - final var contents = RecordStreamAccess.ensurePresentSidecarFile(file.getAbsolutePath()); + final var contents = StreamFileAccess.ensurePresentSidecarFile(file.getAbsolutePath()); contents.getSidecarRecordsList().forEach(sidecar -> listeners.forEach(l -> l.onNewSidecar(sidecar))); } private void exposeItems(final File file) { - final var contents = RecordStreamAccess.ensurePresentRecordFile(file.getAbsolutePath()); + final var contents = StreamFileAccess.ensurePresentRecordFile(file.getAbsolutePath()); contents.getRecordStreamItemsList().forEach(item -> listeners.forEach(l -> l.onNewItem(item))); } @@ -122,6 +130,8 @@ private FileType typeOf(final File file) { return FileType.RECORD_STREAM_FILE; } else if (isSidecarFile(file.getName())) { return FileType.SIDE_CAR_FILE; + } else if (isBlockFile(file)) { + return FileType.BLOCK_FILE; } else { return FileType.OTHER; } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/translators/BaseTranslator.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/translators/BaseTranslator.java index d7b8f63edec2..911d1ff70bc2 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/translators/BaseTranslator.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/translators/BaseTranslator.java @@ -27,6 +27,7 @@ import static com.hedera.node.config.types.EntityType.SCHEDULE; import static com.hedera.node.config.types.EntityType.TOKEN; import static com.hedera.node.config.types.EntityType.TOPIC; +import static com.hedera.services.bdd.junit.support.translators.impl.FileUpdateTranslator.EXCHANGE_RATES_FILE_NUM; import static java.util.Collections.emptyList; import static java.util.Objects.requireNonNull; @@ -77,8 +78,6 @@ public class BaseTranslator { private static final Logger log = LogManager.getLogger(BaseTranslator.class); - private static final long EXCHANGE_RATES_FILE_NUM = 112L; - /** * These fields are context maintained for the full lifetime of the translator. */ @@ -330,7 +329,7 @@ public SingleTransactionRecord recordFrom(@NonNull final BlockTransactionParts p if (followsUserRecord && !parts.transactionIdOrThrow().scheduled()) { recordBuilder.parentConsensusTimestamp(asTimestamp(userTimestamp)); } - if (!followsUserRecord) { + if (!followsUserRecord || parts.transactionIdOrThrow().scheduled()) { // Only preceding and user transactions get exchange rates in their receipts; note that // auto-account creations are always preceding dispatches and so get exchange rates receiptBuilder.exchangeRate(activeRates); @@ -350,6 +349,29 @@ public SingleTransactionRecord recordFrom(@NonNull final BlockTransactionParts p new SingleTransactionRecord.TransactionOutputs(null)); } + /** + * Updates the active exchange rates with the contents of the given state change. + * @param change the state change to update from + */ + public void updateActiveRates(@NonNull final StateChange change) { + final var contents = + change.mapUpdateOrThrow().valueOrThrow().fileValueOrThrow().contents(); + try { + activeRates = ExchangeRateSet.PROTOBUF.parse(contents); + log.info("Updated active exchange rates to {}", activeRates); + } catch (ParseException e) { + throw new IllegalStateException("Rates file updated with unparseable contents", e); + } + } + + /** + * Returns the active exchange rates. + * @return the active exchange rates + */ + public ExchangeRateSet activeRates() { + return activeRates; + } + /** * Returns the modified schedule id for the ongoing transactional unit. * @@ -389,8 +411,6 @@ private void scanUnit(@NonNull final BlockTransactionalUnit unit) { nextCreatedNums .computeIfAbsent(FILE, ignore -> new LinkedList<>()) .add(num); - } else if (num == EXCHANGE_RATES_FILE_NUM) { - updateActiveRates(stateChange); } } else if (key.hasScheduleIdKey()) { final var num = key.scheduleIdKeyOrThrow().scheduleNum(); @@ -448,16 +468,6 @@ private void scanUnit(@NonNull final BlockTransactionalUnit unit) { }); } - private void updateActiveRates(@NonNull final StateChange change) { - final var contents = - change.mapUpdateOrThrow().valueOrThrow().fileValueOrThrow().contents(); - try { - activeRates = ExchangeRateSet.PROTOBUF.parse(contents); - } catch (ParseException e) { - throw new IllegalStateException("Rates file updated with unparseable contents", e); - } - } - private static boolean isContractOp(@NonNull final BlockTransactionParts parts) { final var function = parts.functionality(); return function == CONTRACT_CALL || function == CONTRACT_CREATE || function == ETHEREUM_TRANSACTION; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/translators/BlockTransactionalUnitTranslator.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/translators/BlockTransactionalUnitTranslator.java index c395f67a6d0e..e434f90e0fff 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/translators/BlockTransactionalUnitTranslator.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/translators/BlockTransactionalUnitTranslator.java @@ -83,6 +83,7 @@ import com.hedera.services.bdd.junit.support.translators.impl.CryptoUpdateTranslator; import com.hedera.services.bdd.junit.support.translators.impl.EthereumTransactionTranslator; import com.hedera.services.bdd.junit.support.translators.impl.FileCreateTranslator; +import com.hedera.services.bdd.junit.support.translators.impl.FileUpdateTranslator; import com.hedera.services.bdd.junit.support.translators.impl.NodeCreateTranslator; import com.hedera.services.bdd.junit.support.translators.impl.ScheduleCreateTranslator; import com.hedera.services.bdd.junit.support.translators.impl.ScheduleDeleteTranslator; @@ -142,7 +143,7 @@ public class BlockTransactionalUnitTranslator { put(FILE_APPEND, NO_EXPLICIT_SIDE_EFFECTS_TRANSLATOR); put(FILE_CREATE, new FileCreateTranslator()); put(FILE_DELETE, NO_EXPLICIT_SIDE_EFFECTS_TRANSLATOR); - put(FILE_UPDATE, NO_EXPLICIT_SIDE_EFFECTS_TRANSLATOR); + put(FILE_UPDATE, new FileUpdateTranslator()); put(FREEZE, NO_EXPLICIT_SIDE_EFFECTS_TRANSLATOR); put(NODE_CREATE, new NodeCreateTranslator()); put(NODE_DELETE, NO_EXPLICIT_SIDE_EFFECTS_TRANSLATOR); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/translators/impl/FileUpdateTranslator.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/translators/impl/FileUpdateTranslator.java new file mode 100644 index 000000000000..0c7462c18b08 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/translators/impl/FileUpdateTranslator.java @@ -0,0 +1,61 @@ +/* + * 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.hedera.services.bdd.junit.support.translators.impl; + +import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS; +import static java.util.Objects.requireNonNull; + +import com.hedera.hapi.block.stream.output.StateChange; +import com.hedera.node.app.state.SingleTransactionRecord; +import com.hedera.services.bdd.junit.support.translators.BaseTranslator; +import com.hedera.services.bdd.junit.support.translators.BlockTransactionPartsTranslator; +import com.hedera.services.bdd.junit.support.translators.inputs.BlockTransactionParts; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.List; + +/** + * Translates a file update transaction into a {@link SingleTransactionRecord}, updating + * {@link BaseTranslator} context when a special file is changed. + */ +public class FileUpdateTranslator implements BlockTransactionPartsTranslator { + public static final long EXCHANGE_RATES_FILE_NUM = 112L; + + @Override + public SingleTransactionRecord translate( + @NonNull final BlockTransactionParts parts, + @NonNull final BaseTranslator baseTranslator, + @NonNull final List remainingStateChanges) { + requireNonNull(parts); + requireNonNull(baseTranslator); + requireNonNull(remainingStateChanges); + return baseTranslator.recordFrom(parts, (receiptBuilder, recordBuilder) -> { + if (parts.status() == SUCCESS) { + for (final var stateChange : remainingStateChanges) { + if (stateChange.hasMapUpdate() + && stateChange.mapUpdateOrThrow().keyOrThrow().hasFileIdKey()) { + final var fileId = + stateChange.mapUpdateOrThrow().keyOrThrow().fileIdKeyOrThrow(); + if (fileId.fileNum() == EXCHANGE_RATES_FILE_NUM) { + baseTranslator.updateActiveRates(stateChange); + receiptBuilder.exchangeRate(baseTranslator.activeRates()); + } + } + } + } + }); + } +} 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 d878531ac85f..cddecb100712 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 @@ -236,7 +236,8 @@ public StateChangesValidator( final var currentVersion = new ServicesSoftwareVersion(servicesVersion, configVersion); final var lifecycles = newPlatformInitLifecycle(bootstrapConfig, currentVersion, migrator, servicesRegistry); state = new MerkleStateRoot(lifecycles, version -> new ServicesSoftwareVersion(version, configVersion)); - state.getPlatformState(); + // initialize the platform state + state.getWritablePlatformState(); migrator.doMigrations( state, servicesRegistry, diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/validators/block/TransactionRecordParityValidator.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/validators/block/TransactionRecordParityValidator.java index ecb003cc38cc..9465c10628ed 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/validators/block/TransactionRecordParityValidator.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/validators/block/TransactionRecordParityValidator.java @@ -30,7 +30,7 @@ import com.hedera.node.app.state.SingleTransactionRecord; import com.hedera.services.bdd.junit.support.BlockStreamAccess; import com.hedera.services.bdd.junit.support.BlockStreamValidator; -import com.hedera.services.bdd.junit.support.RecordStreamAccess; +import com.hedera.services.bdd.junit.support.StreamFileAccess; import com.hedera.services.bdd.junit.support.translators.BlockTransactionalUnitTranslator; import com.hedera.services.bdd.junit.support.translators.BlockUnitSplit; import com.hedera.services.bdd.spec.HapiSpec; @@ -87,15 +87,15 @@ public static void main(@NonNull final String[] args) throws IOException { final var blocks = BlockStreamAccess.BLOCK_STREAM_ACCESS.readBlocks(blocksLoc); final var recordsLoc = node0Data.resolve("recordStreams/record0.0.3").toAbsolutePath().normalize(); - final var records = - RecordStreamAccess.RECORD_STREAM_ACCESS.readStreamDataFrom(recordsLoc.toString(), "sidecar"); + final var records = StreamFileAccess.STREAM_FILE_ACCESS.readStreamDataFrom(recordsLoc.toString(), "sidecar"); final var validator = new TransactionRecordParityValidator(); validator.validateBlockVsRecords(blocks, records); } @Override - public void validateBlockVsRecords(@NonNull final List blocks, @NonNull final RecordStreamAccess.Data data) { + public void validateBlockVsRecords( + @NonNull final List blocks, @NonNull final StreamFileAccess.RecordStreamData data) { requireNonNull(blocks); requireNonNull(data); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpec.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpec.java index 19479fd5f5ca..1cc569862fac 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpec.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpec.java @@ -22,7 +22,7 @@ import static com.hedera.services.bdd.junit.extensions.NetworkTargetingExtension.REPEATABLE_KEY_GENERATOR; import static com.hedera.services.bdd.junit.extensions.NetworkTargetingExtension.SHARED_NETWORK; import static com.hedera.services.bdd.junit.hedera.ExternalPath.RECORD_STREAMS_DIR; -import static com.hedera.services.bdd.junit.support.RecordStreamAccess.RECORD_STREAM_ACCESS; +import static com.hedera.services.bdd.junit.support.StreamFileAccess.STREAM_FILE_ACCESS; import static com.hedera.services.bdd.spec.HapiSpec.SpecStatus.ERROR; import static com.hedera.services.bdd.spec.HapiSpec.SpecStatus.FAILED; import static com.hedera.services.bdd.spec.HapiSpec.SpecStatus.FAILED_AS_EXPECTED; @@ -75,6 +75,7 @@ import com.hedera.services.bdd.junit.hedera.HederaNetwork; import com.hedera.services.bdd.junit.hedera.HederaNode; import com.hedera.services.bdd.junit.hedera.NodeSelector; +import com.hedera.services.bdd.junit.hedera.embedded.EmbeddedHedera; import com.hedera.services.bdd.junit.hedera.embedded.EmbeddedNetwork; import com.hedera.services.bdd.junit.hedera.remote.RemoteNetwork; import com.hedera.services.bdd.junit.support.TestLifecycle; @@ -93,7 +94,7 @@ import com.hedera.services.bdd.spec.utilops.records.AutoSnapshotModeOp; import com.hedera.services.bdd.spec.utilops.records.SnapshotMatchMode; import com.hedera.services.bdd.spec.utilops.records.SnapshotModeOp; -import com.hedera.services.bdd.spec.utilops.streams.assertions.EventualRecordStreamAssertion; +import com.hedera.services.bdd.spec.utilops.streams.assertions.AbstractEventualStreamAssertion; import com.hedera.services.bdd.spec.verification.traceability.SidecarWatcher; import com.hederahashgraph.api.proto.java.Key; import com.hederahashgraph.api.proto.java.ResponseCodeEnum; @@ -117,6 +118,7 @@ import java.util.SplittableRandom; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; import java.util.concurrent.PriorityBlockingQueue; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; @@ -459,6 +461,15 @@ public SidecarWatcher getSidecarWatcher() { } } + /** + * Returns the {@link EmbeddedHedera} for a spec in embedded mode, or throws if the spec is not in embedded mode. + * @return the embedded Hedera + * @throws IllegalStateException if the spec is not in embedded mode + */ + public EmbeddedHedera embeddedHederaOrThrow() { + return embeddedNetworkOrThrow().embeddedHederaOrThrow(); + } + /** * Sleeps for the approximate wall clock time it will take for the spec's target * network to advance consensus time by the given duration. @@ -725,7 +736,7 @@ private void exec(@NonNull List ops) { if (!autoScheduled.isEmpty()) { log.info("Auto-scheduling {}", autoScheduled); } - @Nullable List assertions = null; + @Nullable List streamAssertions = null; var snapshotOp = AutoSnapshotModeOp.from(this); if (snapshotOp != null) { // Ensure a mutable list @@ -736,11 +747,11 @@ private void exec(@NonNull List ops) { if (!autoScheduled.isEmpty() && op.shouldSkipWhenAutoScheduling(autoScheduled)) { continue; } - if (op instanceof EventualRecordStreamAssertion recordStreamAssertion) { - if (assertions == null) { - assertions = new ArrayList<>(); + if (op instanceof AbstractEventualStreamAssertion streamAssertion) { + if (streamAssertions == null) { + streamAssertions = new ArrayList<>(); } - assertions.add(recordStreamAssertion); + streamAssertions.add(streamAssertion); } else if (op instanceof HapiTxnOp txn && autoScheduled.contains(txn.type())) { op = autoScheduledSequenceFor(txn); } else if (op instanceof SnapshotModeOp snapshotModeOp) { @@ -799,10 +810,10 @@ private void exec(@NonNull List ops) { failure = new Failure(t, "Record snapshot fuzzy-match"); } } - final var maybeRecordStreamError = checkRecordStream(assertions); - if (maybeRecordStreamError.isPresent()) { + final var maybeStreamFileError = checkStream(streamAssertions); + if (maybeStreamFileError.isPresent()) { status = FAILED; - failure = maybeRecordStreamError.get(); + failure = maybeStreamFileError.get(); } if (sidecarWatcher != null) { try { @@ -813,9 +824,9 @@ private void exec(@NonNull List ops) { failure = new Failure(t, "Sidecar assertion"); } } - } else if (assertions != null) { - assertions.forEach(EventualRecordStreamAssertion::unsubscribe); - RECORD_STREAM_ACCESS.stopMonitorIfNoSubscribers(); + } else if (streamAssertions != null) { + streamAssertions.forEach(AbstractEventualStreamAssertion::unsubscribe); + STREAM_FILE_ACCESS.stopMonitorIfNoSubscribers(); } tearDown(); @@ -950,30 +961,34 @@ private static List createAndSignIndicesGiven(final int numKeys, final return endIndices; } - private Optional checkRecordStream(@Nullable final List assertions) { - if (assertions == null) { + private Optional checkStream(@Nullable final List streamAssertions) { + if (streamAssertions == null) { return Optional.empty(); } if (!quietMode) { - log.info("Checking record stream for {} assertions", assertions.size()); + log.info("Checking stream files for {} assertions", streamAssertions.size()); } + final var needsTraffic = + streamAssertions.stream().anyMatch(AbstractEventualStreamAssertion::needsBackgroundTraffic); Optional answer = Optional.empty(); - // Keep submitting transactions to close record files (in almost every case, just + // Keep submitting transactions to close stream files (in almost every case, just // one file will need to be closed, since it's very rare to have a long-running spec) - final var backgroundTraffic = THREAD_POOL.submit(() -> { - while (true) { - try { - TxnUtils.triggerAndCloseAtLeastOneFile(this); - if (!quietMode) { - log.info("Closed at least one record file via background traffic"); + final Future backgroundTraffic = needsTraffic + ? THREAD_POOL.submit(() -> { + while (true) { + try { + TxnUtils.triggerAndCloseAtLeastOneFile(this); + if (!quietMode) { + log.info("Closed at least one record file via background traffic"); + } + } catch (final InterruptedException ignore) { + Thread.currentThread().interrupt(); + return; + } } - } catch (final InterruptedException ignore) { - Thread.currentThread().interrupt(); - return; - } - } - }); - for (final var assertion : assertions) { + }) + : null; + for (final var assertion : streamAssertions) { if (!quietMode) { log.info("Checking record stream for {}", assertion); } @@ -987,8 +1002,10 @@ private Optional checkRecordStream(@Nullable final List observer) { return new ViewNodeOp(name, observer); } /*** - * `ViewPendingAirdropOp` is an operation that allows the test author to view the pending airdrop of an account. - * @param tokenName - * @param senderName - * @param receiverName - * @param observer - * @return + * Returns an operation that allows the test author to view the pending airdrop of an account. + * @param tokenName the name of the token + * @param senderName the name of the sender + * @param receiverName the name of the receiver + * @param observer the observer to apply to the account + * @return the operation that will expose the pending airdrop of the account */ public static ViewPendingAirdropOp viewAccountPendingAirdrop( @NonNull final String tokenName, diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/TssVerbs.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/TssVerbs.java new file mode 100644 index 000000000000..ed7e53e865aa --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/TssVerbs.java @@ -0,0 +1,49 @@ +/* + * 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.hedera.services.bdd.spec.utilops; + +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.doingContextual; + +import com.hedera.node.app.tss.TssBaseService; +import com.hedera.services.bdd.spec.SpecOperation; + +/** + * Factory for spec operations that support exercising TSS, especially in embedded mode. + */ +public class TssVerbs { + private TssVerbs() { + throw new UnsupportedOperationException("Utility class"); + } + + /** + * Returns an operation that instructs the embedded {@link TssBaseService} to ignoring TSS signature requests. + * @return the operation that will ignore TSS signature requests + */ + public static SpecOperation startIgnoringTssSignatureRequests() { + return doingContextual( + spec -> spec.embeddedHederaOrThrow().tssBaseService().startIgnoringRequests()); + } + + /** + * Returns an operation that instructs the embedded {@link TssBaseService} to stop ignoring TSS signature requests. + * @return the operation that will stop ignoring TSS signature requests + */ + public static SpecOperation stopIgnoringTssSignatureRequests() { + return doingContextual( + spec -> spec.embeddedHederaOrThrow().tssBaseService().stopIgnoringRequests()); + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/UtilVerbs.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/UtilVerbs.java index fb7c907e9c87..2e2e943bac9f 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/UtilVerbs.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/UtilVerbs.java @@ -159,8 +159,10 @@ import com.hedera.services.bdd.spec.utilops.streams.LogContainmentOp; import com.hedera.services.bdd.spec.utilops.streams.LogValidationOp; import com.hedera.services.bdd.spec.utilops.streams.StreamValidationOp; +import com.hedera.services.bdd.spec.utilops.streams.assertions.AbstractEventualStreamAssertion; import com.hedera.services.bdd.spec.utilops.streams.assertions.AssertingBiConsumer; -import com.hedera.services.bdd.spec.utilops.streams.assertions.EventualAssertion; +import com.hedera.services.bdd.spec.utilops.streams.assertions.BlockStreamAssertion; +import com.hedera.services.bdd.spec.utilops.streams.assertions.EventualBlockStreamAssertion; import com.hedera.services.bdd.spec.utilops.streams.assertions.EventualRecordStreamAssertion; import com.hedera.services.bdd.spec.utilops.streams.assertions.RecordStreamAssertion; import com.hedera.services.bdd.spec.utilops.streams.assertions.SelectedItemsAssertion; @@ -1065,20 +1067,38 @@ public static HapiSpecOperation remembering(final Map props, fin } /* Stream validation. */ - public static EventualAssertion streamMustInclude(final Function assertion) { - return new EventualRecordStreamAssertion(assertion); - } - - public static EventualAssertion streamMustIncludeNoFailuresFrom( - final Function assertion) { + public static EventualRecordStreamAssertion recordStreamMustIncludeNoFailuresFrom( + @NonNull final Function assertion) { return EventualRecordStreamAssertion.eventuallyAssertingNoFailures(assertion); } - public static EventualAssertion streamMustIncludePassFrom( - final Function assertion) { + public static EventualRecordStreamAssertion recordStreamMustIncludePassFrom( + @NonNull final Function assertion) { return EventualRecordStreamAssertion.eventuallyAssertingExplicitPass(assertion); } + /** + * Returns an operation that asserts that the block stream must include no failures from the given assertion + * before its timeout elapses. + * @param assertion the assertion to apply to the block stream + * @return the operation that asserts no block stream problems + */ + public static EventualBlockStreamAssertion blockStreamMustIncludeNoFailuresFrom( + @NonNull final Function assertion) { + return EventualBlockStreamAssertion.eventuallyAssertingNoFailures(assertion); + } + + /** + * Returns an operation that asserts that the block stream must include a pass from the given assertion + * before its timeout elapses. + * @param assertion the assertion to apply to the block stream + * @return the operation that asserts a passing block stream + */ + public static AbstractEventualStreamAssertion blockStreamMustIncludePassFrom( + @NonNull final Function assertion) { + return EventualBlockStreamAssertion.eventuallyAssertingExplicitPass(assertion); + } + public static RunnableOp verify(@NonNull final Runnable runnable) { return new RunnableOp(runnable); } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/records/SnapshotModeOp.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/records/SnapshotModeOp.java index b696a528dfcb..5416f24ea31f 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/records/SnapshotModeOp.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/records/SnapshotModeOp.java @@ -17,7 +17,7 @@ package com.hedera.services.bdd.spec.utilops.records; import static com.hedera.node.app.hapi.utils.exports.recordstreaming.RecordStreamingUtils.parseRecordFileConsensusTime; -import static com.hedera.services.bdd.junit.support.RecordStreamAccess.RECORD_STREAM_ACCESS; +import static com.hedera.services.bdd.junit.support.StreamFileAccess.STREAM_FILE_ACCESS; 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.utilops.CustomSpecAssert.allRunFor; @@ -43,7 +43,7 @@ import com.google.protobuf.Descriptors; import com.google.protobuf.GeneratedMessageV3; import com.hedera.services.bdd.junit.SharedNetworkLauncherSessionListener; -import com.hedera.services.bdd.junit.support.RecordStreamAccess; +import com.hedera.services.bdd.junit.support.StreamFileAccess; import com.hedera.services.bdd.spec.HapiSpec; import com.hedera.services.bdd.spec.utilops.UtilOp; import com.hedera.services.bdd.spec.utilops.domain.ParsedItem; @@ -260,11 +260,11 @@ public void finishLifecycle(@NonNull final HapiSpec spec) { return; } try { - RecordStreamAccess.Data data = RecordStreamAccess.Data.EMPTY_DATA; + StreamFileAccess.RecordStreamData data = StreamFileAccess.RecordStreamData.EMPTY_DATA; for (final var recordLoc : recordLocs) { try { log.info("Trying to read post-placeholder items from {}", recordLoc); - data = RECORD_STREAM_ACCESS.readStreamDataFrom(recordLoc, "sidecar", f -> { + data = STREAM_FILE_ACCESS.readStreamDataFrom(recordLoc, "sidecar", f -> { final var fileConsTime = parseRecordFileConsensusTime(f); return fileConsTime.isAfter(lowerBoundConsensusStartTime) && new File(f).length() > MIN_GZIP_SIZE_IN_BYTES; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/EventualAssertionResult.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/EventualAssertionResult.java index cdb042f87405..6609c698cc76 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/EventualAssertionResult.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/EventualAssertionResult.java @@ -16,7 +16,10 @@ package com.hedera.services.bdd.spec.utilops.streams; +import static java.util.Objects.requireNonNull; + import com.hedera.services.bdd.spec.utilops.streams.assertions.AssertionResult; +import edu.umd.cs.findbugs.annotations.NonNull; import java.time.Duration; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -29,13 +32,13 @@ public class EventualAssertionResult { private AssertionResult result; - public EventualAssertionResult(final Duration timeout) { + public EventualAssertionResult(@NonNull final Duration timeout) { this(false, timeout); } - public EventualAssertionResult(boolean hasPassedIfNothingFailed, final Duration timeout) { + public EventualAssertionResult(final boolean hasPassedIfNothingFailed, @NonNull final Duration timeout) { this.hasPassedIfNothingFailed = hasPassedIfNothingFailed; - this.timeout = timeout; + this.timeout = requireNonNull(timeout); } public AssertionResult get() throws InterruptedException { 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 0486a5fe9f85..b3ebd6cea1ce 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 @@ -19,7 +19,7 @@ import static com.hedera.services.bdd.junit.hedera.ExternalPath.BLOCK_STREAMS_DIR; import static com.hedera.services.bdd.junit.hedera.ExternalPath.RECORD_STREAMS_DIR; import static com.hedera.services.bdd.junit.support.BlockStreamAccess.BLOCK_STREAM_ACCESS; -import static com.hedera.services.bdd.junit.support.RecordStreamAccess.RECORD_STREAM_ACCESS; +import static com.hedera.services.bdd.junit.support.StreamFileAccess.STREAM_FILE_ACCESS; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoTransfer; import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.freezeOnly; @@ -30,8 +30,8 @@ import com.hedera.hapi.block.stream.Block; import com.hedera.services.bdd.junit.support.BlockStreamValidator; -import com.hedera.services.bdd.junit.support.RecordStreamAccess; import com.hedera.services.bdd.junit.support.RecordStreamValidator; +import com.hedera.services.bdd.junit.support.StreamFileAccess; import com.hedera.services.bdd.junit.support.validators.BalanceReconciliationValidator; import com.hedera.services.bdd.junit.support.validators.BlockNoValidator; import com.hedera.services.bdd.junit.support.validators.ExpiryRecordsValidator; @@ -88,7 +88,7 @@ protected boolean submitOp(@NonNull final HapiSpec spec) throws Throwable { // Wait for the final record file to be created sleepFor(2 * BUFFER_MS)); // Validate the record streams - final AtomicReference dataRef = new AtomicReference<>(); + final AtomicReference dataRef = new AtomicReference<>(); readMaybeRecordStreamDataFor(spec) .ifPresentOrElse( data -> { @@ -154,8 +154,9 @@ private static Optional> readMaybeBlockStreamsFor(@NonNull final Hap return Optional.ofNullable(blocks); } - private static Optional readMaybeRecordStreamDataFor(@NonNull final HapiSpec spec) { - RecordStreamAccess.Data data = null; + private static Optional readMaybeRecordStreamDataFor( + @NonNull final HapiSpec spec) { + StreamFileAccess.RecordStreamData data = null; final var streamLocs = spec.getNetworkNodes().stream() .map(node -> node.getExternalPath(RECORD_STREAMS_DIR)) .map(Path::toAbsolutePath) @@ -164,7 +165,7 @@ private static Optional readMaybeRecordStreamDataFor(@N for (final var loc : streamLocs) { try { log.info("Trying to read record files from {}", loc); - data = RECORD_STREAM_ACCESS.readStreamDataFrom( + data = STREAM_FILE_ACCESS.readStreamDataFrom( loc, "sidecar", f -> new File(f).length() > MIN_GZIP_SIZE_IN_BYTES); log.info("Read {} record files from {}", data.records().size(), loc); } catch (Exception ignore) { diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/AbstractEventualStreamAssertion.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/AbstractEventualStreamAssertion.java new file mode 100644 index 000000000000..abdd80dfee49 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/AbstractEventualStreamAssertion.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2020-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.hedera.services.bdd.spec.utilops.streams.assertions; + +import com.hedera.services.bdd.spec.utilops.UtilOp; +import com.hedera.services.bdd.spec.utilops.streams.EventualAssertionResult; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.time.Duration; +import org.junit.jupiter.api.Assertions; + +/** + * Important: {@code HapiSpec#exec()} recognizes {@link AbstractEventualStreamAssertion} + * operations as a special case, in two ways. + *

        + *
      1. If a spec includes at least one {@link AbstractEventualStreamAssertion}, and all other + * operations have passed, it starts running "background traffic" to ensure record stream + * files are being written. + *
      2. For each {@link AbstractEventualStreamAssertion}, the spec then calls its + * {@link #assertHasPassed()}, method which blocks until the assertion has either passed or timed + * out. (The default timeout is 5 seconds, since generally we expect the assertion to apply to + * the contents of a single record stream file, which are created every 2 seconds given steady + * background traffic.) + *
      + */ +public abstract class AbstractEventualStreamAssertion extends UtilOp { + private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(5L); + + protected final EventualAssertionResult result; + + /** + * Once this op is submitted, the function to unsubscribe from the stream. + */ + @Nullable + protected Runnable unsubscribe; + + protected AbstractEventualStreamAssertion(final boolean hasPassedIfNothingFailed) { + result = new EventualAssertionResult(hasPassedIfNothingFailed, DEFAULT_TIMEOUT); + } + + /** + * Returns true if this assertion needs background traffic to be running in order to pass. + * @return true if this assertion needs background traffic + */ + public boolean needsBackgroundTraffic() { + return true; + } + + /** + * If this assertion has subscribed to a stream, this method unsubscribes from it. + */ + public void unsubscribe() { + if (unsubscribe != null) { + unsubscribe.run(); + } + } + + /** + * Blocks until the assertion has passed, fail, or timed out. + * @throws AssertionError if the assertion has failed + */ + public void assertHasPassed() { + try { + final var eventualResult = result.get(); + unsubscribe(); + if (!eventualResult.passed()) { + Assertions.fail(eventualResult.getErrorDetails()); + } + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + Assertions.fail("Interrupted while waiting for " + this + " to pass"); + } + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/BlockStreamAssertion.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/BlockStreamAssertion.java new file mode 100644 index 000000000000..54b7e2d9bb04 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/BlockStreamAssertion.java @@ -0,0 +1,49 @@ +/* + * 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.hedera.services.bdd.spec.utilops.streams.assertions; + +import com.hedera.hapi.block.stream.Block; +import com.hedera.services.bdd.spec.HapiSpec; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Implements an assertion about one or more {@link com.hedera.hapi.block.stream.Block}'s that should appear in the + * block stream during---or shortly after---execution of a {@link HapiSpec}. + * + *

      Typical implementations will be stateful, and will be constructed with their "parent" {@link HapiSpec}. + */ +public interface BlockStreamAssertion { + /** + * Updates the assertion's state based on a relevant {@link Block}, throwing an {@link AssertionError} if a + * failure state is reached; or returning true if the assertion has reached a success state. + * + * @param block the block to test + * @throws AssertionError if the assertion has failed + * @return true if the assertion has succeeded + */ + default boolean test(@NonNull final Block block) throws AssertionError { + return true; + } + + /** + * Hint to implementers to return a string that describes the assertion. + * + * @return a string that describes the assertion + */ + @Override + String toString(); +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/EventualAssertion.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/EventualAssertion.java deleted file mode 100644 index 96213f9ae17a..000000000000 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/EventualAssertion.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 2020-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.hedera.services.bdd.spec.utilops.streams.assertions; - -import com.hedera.services.bdd.spec.utilops.UtilOp; -import com.hedera.services.bdd.spec.utilops.streams.EventualAssertionResult; -import java.time.Duration; - -public abstract class EventualAssertion extends UtilOp { - private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(5L); - - protected final EventualAssertionResult result; - - protected EventualAssertion() { - this(DEFAULT_TIMEOUT); - } - - protected EventualAssertion(final Duration timeout) { - result = new EventualAssertionResult(timeout); - } - - protected EventualAssertion(final boolean hasPassedIfNothingFailed) { - result = new EventualAssertionResult(hasPassedIfNothingFailed, DEFAULT_TIMEOUT); - } -} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/EventualBlockStreamAssertion.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/EventualBlockStreamAssertion.java new file mode 100644 index 000000000000..1434dd376e91 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/EventualBlockStreamAssertion.java @@ -0,0 +1,111 @@ +/* + * 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.hedera.services.bdd.spec.utilops.streams.assertions; + +import static com.hedera.services.bdd.junit.hedera.ExternalPath.BLOCK_STREAMS_DIR; +import static com.hedera.services.bdd.junit.support.StreamFileAccess.STREAM_FILE_ACCESS; +import static java.util.Objects.requireNonNull; + +import com.hedera.hapi.block.stream.Block; +import com.hedera.services.bdd.junit.support.StreamDataListener; +import com.hedera.services.bdd.spec.HapiSpec; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.file.Path; +import java.util.function.Function; + +public class EventualBlockStreamAssertion extends AbstractEventualStreamAssertion { + /** + * The factory for the assertion to be tested. + */ + private final Function assertionFactory; + /** + * Once this op is submitted, the assertion to be tested. + */ + @Nullable + private BlockStreamAssertion assertion; + + /** + * Returns an {@link EventualBlockStreamAssertion} that will pass as long as the given assertion does not + * throw an {@link AssertionError} before its timeout. + * @param assertionFactory the assertion factory + * @return the eventual block stream assertion that must not fail + */ + public static EventualBlockStreamAssertion eventuallyAssertingNoFailures( + @NonNull final Function assertionFactory) { + return new EventualBlockStreamAssertion(assertionFactory, true); + } + + /** + * Returns an {@link EventualBlockStreamAssertion} that will pass only if the given assertion explicitly + * passes within the default timeout. + * @param assertionFactory the assertion factory + * @return the eventual block stream assertion that must pass + */ + public static EventualBlockStreamAssertion eventuallyAssertingExplicitPass( + @NonNull final Function assertionFactory) { + return new EventualBlockStreamAssertion(assertionFactory, false); + } + + private EventualBlockStreamAssertion( + @NonNull final Function assertionFactory, + final boolean hasPassedIfNothingFailed) { + super(hasPassedIfNothingFailed); + this.assertionFactory = requireNonNull(assertionFactory); + } + + @Override + public boolean needsBackgroundTraffic() { + return false; + } + + @Override + protected boolean submitOp(@NonNull final HapiSpec spec) throws Throwable { + requireNonNull(spec); + assertion = requireNonNull(assertionFactory.apply(spec)); + unsubscribe = STREAM_FILE_ACCESS.subscribe(blockStreamLocFor(spec), new StreamDataListener() { + @Override + public void onNewBlock(@NonNull final Block block) { + requireNonNull(block); + try { + if (assertion.test(block)) { + result.pass(); + } + } catch (final AssertionError e) { + result.fail(e.getMessage()); + } + } + + @Override + public String name() { + return assertion.toString(); + } + }); + return false; + } + + /** + * Returns the block stream location for the first listed node in the network targeted + * by the given spec. + * + * @param spec the spec + * @return a record stream location for the first listed node in the network + */ + private static Path blockStreamLocFor(@NonNull final HapiSpec spec) { + return spec.targetNetworkOrThrow().nodes().getFirst().getExternalPath(BLOCK_STREAMS_DIR); + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/EventualRecordStreamAssertion.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/EventualRecordStreamAssertion.java index f388fe8f973d..5e63b339eba8 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/EventualRecordStreamAssertion.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/EventualRecordStreamAssertion.java @@ -17,38 +17,25 @@ package com.hedera.services.bdd.spec.utilops.streams.assertions; import static com.hedera.services.bdd.junit.hedera.ExternalPath.RECORD_STREAMS_DIR; -import static com.hedera.services.bdd.junit.support.RecordStreamAccess.RECORD_STREAM_ACCESS; +import static com.hedera.services.bdd.junit.support.StreamFileAccess.STREAM_FILE_ACCESS; +import static java.util.Objects.requireNonNull; -import com.hedera.services.bdd.junit.support.RecordStreamAccess; import com.hedera.services.bdd.junit.support.StreamDataListener; +import com.hedera.services.bdd.junit.support.StreamFileAccess; import com.hedera.services.bdd.spec.HapiSpec; import com.hedera.services.stream.proto.RecordStreamItem; import com.hedera.services.stream.proto.TransactionSidecarRecord; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import java.util.Objects; +import java.nio.file.Path; import java.util.function.Function; -import org.junit.jupiter.api.Assertions; /** * A {@link com.hedera.services.bdd.spec.utilops.UtilOp} that registers itself with {@link - * RecordStreamAccess} and continually updates the {@link RecordStreamAssertion} yielded by a given + * StreamFileAccess} and continually updates the {@link RecordStreamAssertion} yielded by a given * factory with each new {@link RecordStreamItem}. - * - *

      Important: {@code HapiSpec#exec()} recognizes {@link EventualRecordStreamAssertion} - * operations as a special case, in two ways. - *

        - *
      1. If a spec includes at least one {@link EventualRecordStreamAssertion}, and all other - * operations have passed, it starts running "background traffic" to ensure record stream - * files are being written. - *
      2. For each {@link EventualRecordStreamAssertion}, the spec then calls its {@link - * #assertHasPassed()}, method which blocks until the assertion has either passed or timed - * out. (The default timeout is 3 seconds, since generally we expect the assertion to apply to - * the contents of a single record stream file, which are created every 2 seconds given steady - * background traffic.) - *
      */ -public class EventualRecordStreamAssertion extends EventualAssertion { +public class EventualRecordStreamAssertion extends AbstractEventualStreamAssertion { /** * The factory for the assertion to be tested. */ @@ -59,21 +46,6 @@ public class EventualRecordStreamAssertion extends EventualAssertion { */ @Nullable private RecordStreamAssertion assertion; - /** - * Once this op is submitted, the function to unsubscribe from the record stream. - */ - @Nullable - private Runnable unsubscribe; - - public EventualRecordStreamAssertion(final Function assertionFactory) { - this.assertionFactory = assertionFactory; - } - - private EventualRecordStreamAssertion( - final Function assertionFactory, final boolean hasPassedIfNothingFailed) { - super(hasPassedIfNothingFailed); - this.assertionFactory = assertionFactory; - } /** * Returns an {@link EventualRecordStreamAssertion} that will pass as long as the given assertion does not @@ -97,28 +69,17 @@ public static EventualRecordStreamAssertion eventuallyAssertingExplicitPass( return new EventualRecordStreamAssertion(assertionFactory, false); } - /** - * Returns the record stream location for the first listed node in the network targeted - * by the given spec. - * - * @param spec the spec - * @return a record stream location for the first listed node in the network - */ - public static String recordStreamLocFor(@NonNull final HapiSpec spec) { - Objects.requireNonNull(spec); - return spec.targetNetworkOrThrow() - .nodes() - .getFirst() - .getExternalPath(RECORD_STREAMS_DIR) - .toString(); + private EventualRecordStreamAssertion( + @NonNull final Function assertionFactory, + final boolean hasPassedIfNothingFailed) { + super(hasPassedIfNothingFailed); + this.assertionFactory = requireNonNull(assertionFactory); } @Override protected boolean submitOp(final HapiSpec spec) throws Throwable { - final var locToUse = recordStreamLocFor(spec); - final var validatingListener = RECORD_STREAM_ACCESS.getValidatingListener(locToUse); - assertion = Objects.requireNonNull(assertionFactory.apply(spec)); - unsubscribe = validatingListener.subscribe(new StreamDataListener() { + assertion = requireNonNull(assertionFactory.apply(spec)); + unsubscribe = STREAM_FILE_ACCESS.subscribe(recordStreamLocFor(spec), new StreamDataListener() { @Override public void onNewItem(RecordStreamItem item) { if (assertion.isApplicableTo(item)) { @@ -153,29 +114,19 @@ public String name() { return false; } - public void assertHasPassed() { - try { - final var eventualResult = result.get(); - if (unsubscribe != null) { - unsubscribe.run(); - } - if (!eventualResult.passed()) { - Assertions.fail(eventualResult.getErrorDetails()); - } - } catch (final InterruptedException e) { - Thread.currentThread().interrupt(); - Assertions.fail("Interrupted while waiting for " + assertion + " to pass"); - } - } - - public void unsubscribe() { - if (unsubscribe != null) { - unsubscribe.run(); - } - } - @Override public String toString() { - return "Eventually{" + assertion + "}"; + return "EventuallyRecordStream{" + assertion + "}"; + } + + /** + * Returns the record stream location for the first listed node in the network targeted + * by the given spec. + * + * @param spec the spec + * @return a record stream location for the first listed node in the network + */ + private static Path recordStreamLocFor(@NonNull final HapiSpec spec) { + return spec.targetNetworkOrThrow().nodes().getFirst().getExternalPath(RECORD_STREAMS_DIR); } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/IndirectProofsAssertion.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/IndirectProofsAssertion.java new file mode 100644 index 000000000000..cf75149ccf04 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/IndirectProofsAssertion.java @@ -0,0 +1,73 @@ +/* + * 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.hedera.services.bdd.spec.utilops.streams.assertions; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.hedera.hapi.block.stream.Block; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A {@link BlockStreamAssertion} used to verify the presence of some number {@code n} of expected indirect proofs + * in the block stream. When constructed, it assumes proof construction is paused, and fails if any block + * is written in this stage. + *

      + * After {@link #startExpectingBlocks()} is called, the assertion will verify that the next {@code n} proofs are + * indirect proofs with the correct number of sibling hashes; and are followed by a direct proof, at which point + * it passes. + */ +public class IndirectProofsAssertion implements BlockStreamAssertion { + private boolean proofsArePaused; + private int remainingIndirectProofs; + + public IndirectProofsAssertion(final int remainingIndirectProofs) { + this.proofsArePaused = true; + this.remainingIndirectProofs = remainingIndirectProofs; + } + + /** + * Signals that the assertion should now expect proofs to be created, hence blocks to be written. + */ + public void startExpectingBlocks() { + proofsArePaused = false; + } + + @Override + public boolean test(@NonNull final Block block) throws AssertionError { + if (proofsArePaused) { + throw new AssertionError("No blocks should be written when proofs are unavailable"); + } else { + final var items = block.items(); + final var proofItem = items.getLast(); + assertTrue(proofItem.hasBlockProof(), "Block proof is expected as the last item"); + final var proof = proofItem.blockProofOrThrow(); + if (remainingIndirectProofs == 0) { + assertTrue(proof.siblingHashes().isEmpty(), "No sibling hashes should be present on a direct proof"); + return true; + } else { + assertEquals( + // Two sibling hashes per indirection level + 2 * remainingIndirectProofs, + proof.siblingHashes().size(), + "Wrong number of sibling hashes for indirect proof"); + } + remainingIndirectProofs--; + return false; + } + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/RecordStreamAssertion.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/RecordStreamAssertion.java index d7d5b0f718da..765f34afbaa4 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/RecordStreamAssertion.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/RecordStreamAssertion.java @@ -22,8 +22,7 @@ /** * Implements an assertion about one or more {@link RecordStreamItem}'s that should appear in the - * record stream during---or shortly after---execution of a {@link - * HapiSpec}. + * record stream during---or shortly after---execution of a {@link HapiSpec}. * *

      Typical implementations will be stateful, and will be constructed with their "parent" {@link HapiSpec}. */ diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/verification/traceability/SidecarWatcher.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/verification/traceability/SidecarWatcher.java index 5e4035e95c4f..186f100cc4b6 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/verification/traceability/SidecarWatcher.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/verification/traceability/SidecarWatcher.java @@ -17,13 +17,13 @@ package com.hedera.services.bdd.spec.verification.traceability; import static com.hedera.services.bdd.junit.hedera.utils.WorkingDirUtils.guaranteedExtantDir; -import static com.hedera.services.bdd.junit.support.RecordStreamAccess.RECORD_STREAM_ACCESS; +import static com.hedera.services.bdd.junit.support.StreamFileAccess.STREAM_FILE_ACCESS; import static com.hedera.services.bdd.spec.transactions.TxnUtils.triggerAndCloseAtLeastOneFileIfNotInterrupted; import static java.nio.charset.StandardCharsets.US_ASCII; import static org.junit.jupiter.api.Assertions.assertTrue; -import com.hedera.services.bdd.junit.support.RecordStreamAccess; import com.hedera.services.bdd.junit.support.StreamDataListener; +import com.hedera.services.bdd.junit.support.StreamFileAccess; import com.hedera.services.bdd.spec.HapiSpec; import com.hedera.services.stream.proto.TransactionSidecarRecord; import edu.umd.cs.findbugs.annotations.NonNull; @@ -43,7 +43,7 @@ * A class that simultaneously, *

        *
      1. Listens for the actual sidecars written at the given location via - * the {@link RecordStreamAccess#RECORD_STREAM_ACCESS} utility; and,
      2. + * the {@link StreamFileAccess#STREAM_FILE_ACCESS} utility; and, *
      3. Registers expected sidecars.
      4. *
      * When a client has registered all its expectations with a {@link SidecarWatcher} @@ -71,7 +71,7 @@ public class SidecarWatcher { private record ConstructionDetails(String creatingThread, String stackTrace) {} public SidecarWatcher(@NonNull final Path path) { - this.unsubscribe = RECORD_STREAM_ACCESS.subscribe(guaranteedExtantDir(path), new StreamDataListener() { + this.unsubscribe = STREAM_FILE_ACCESS.subscribe(guaranteedExtantDir(path), new StreamDataListener() { @Override public void onNewSidecar(@NonNull final TransactionSidecarRecord sidecar) { actualSidecars.add(sidecar); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/compose/PerpetualLocalCalls.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/compose/PerpetualLocalCalls.java deleted file mode 100644 index f559d6dbd46e..000000000000 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/compose/PerpetualLocalCalls.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (C) 2020-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.hedera.services.bdd.suites.compose; - -import static com.hedera.services.bdd.spec.HapiSpec.defaultHapiSpec; -import static com.hedera.services.bdd.spec.assertions.ContractFnResultAsserts.isLiteralResult; -import static com.hedera.services.bdd.spec.assertions.ContractFnResultAsserts.resultWith; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.contractCallLocal; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.uploadInitCode; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.runWithProvider; -import static com.hedera.services.bdd.suites.contract.Utils.FunctionType.FUNCTION; -import static com.hedera.services.bdd.suites.contract.Utils.getABIFor; -import static java.util.concurrent.TimeUnit.MINUTES; - -import com.hedera.services.bdd.spec.HapiSpec; -import com.hedera.services.bdd.spec.HapiSpecOperation; -import com.hedera.services.bdd.spec.SpecOperation; -import com.hedera.services.bdd.spec.infrastructure.OpProvider; -import com.hedera.services.bdd.suites.HapiSuite; -import java.math.BigInteger; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Function; -import java.util.stream.Stream; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.junit.jupiter.api.DynamicTest; - -public class PerpetualLocalCalls extends HapiSuite { - - private static final Logger log = LogManager.getLogger(PerpetualLocalCalls.class); - public static final String CHILD_STORAGE = "ChildStorage"; - - private AtomicLong duration = new AtomicLong(Long.MAX_VALUE); - private AtomicReference unit = new AtomicReference<>(MINUTES); - private AtomicInteger maxOpsPerSec = new AtomicInteger(100); - private AtomicInteger totalBeforeFailure = new AtomicInteger(0); - - public static void main(String... args) { - new PerpetualLocalCalls().runSuiteSync(); - } - - @Override - public List> getSpecsInSuite() { - return List.of(localCallsForever()); - } - - final Stream localCallsForever() { - return defaultHapiSpec("LocalCallsForever") - .given() - .when() - .then(runWithProvider(localCallsFactory()) - .lasting(duration::get, unit::get) - .maxOpsPerSec(maxOpsPerSec::get)); - } - - private Function localCallsFactory() { - return spec -> new OpProvider() { - @Override - public List suggestedInitializers() { - return List.of(uploadInitCode(CHILD_STORAGE), contractCreate(CHILD_STORAGE)); - } - - @Override - public Optional get() { - var op = contractCallLocal(CHILD_STORAGE, "getMyValue") - .noLogging() - .has(resultWith() - .resultThruAbi( - getABIFor(FUNCTION, "getMyValue", CHILD_STORAGE), - isLiteralResult(new Object[] {BigInteger.valueOf(73)}))); - var soFar = totalBeforeFailure.getAndIncrement(); - if (soFar % 1000 == 0) { - log.info("--- {}", soFar); - } - return Optional.of(op); - } - }; - } - - @Override - protected Logger getResultsLogger() { - return log; - } -} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/hapi/ContractCallSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/hapi/ContractCallSuite.java index 9b93a3a48dad..5a691d768d2d 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/hapi/ContractCallSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/hapi/ContractCallSuite.java @@ -17,6 +17,7 @@ package com.hedera.services.bdd.suites.contract.hapi; import static com.hedera.node.app.hapi.utils.EthSigsUtils.recoverAddressFromPubKey; +import static com.hedera.services.bdd.junit.TestTags.ADHOC; import static com.hedera.services.bdd.junit.TestTags.SMART_CONTRACT; import static com.hedera.services.bdd.spec.HapiPropertySource.asContract; import static com.hedera.services.bdd.spec.HapiPropertySource.asContractString; @@ -71,9 +72,9 @@ import static com.hedera.services.bdd.spec.utilops.UtilVerbs.logIt; 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.recordStreamMustIncludeNoFailuresFrom; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sidecarIdValidator; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sourcing; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.streamMustIncludeNoFailuresFrom; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.submitModified; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; import static com.hedera.services.bdd.spec.utilops.mod.ModificationUtils.withSuccessivelyVariedBodyIds; @@ -164,8 +165,7 @@ import org.junit.jupiter.api.Tag; @Tag(SMART_CONTRACT) -// @Tag(ADHOC) -@Tag("ONEOFF") +@Tag(ADHOC) public class ContractCallSuite { public static final String TOKEN = "yahcliToken"; @@ -245,7 +245,7 @@ final Stream repeatedCreate2FailsWithInterpretableActionSidecars() final var secondCreation = "secondCreation"; return defaultHapiSpec("repeatedCreate2FailsWithInterpretableActionSidecars", NONDETERMINISTIC_TRANSACTION_FEES) .given( - streamMustIncludeNoFailuresFrom(sidecarIdValidator()), + recordStreamMustIncludeNoFailuresFrom(sidecarIdValidator()), cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS), uploadInitCode(contract), contractCreate(contract)) @@ -275,7 +275,7 @@ final Stream insufficientGasToPrecompileFailsWithInterpretableActio final var tokenInfoFn = new Function("getTokenInfo(address)"); return defaultHapiSpec("insufficientGasToPrecompileFailsWithInterpretableActionSidecars") .given( - streamMustIncludeNoFailuresFrom(sidecarIdValidator()), + recordStreamMustIncludeNoFailuresFrom(sidecarIdValidator()), uploadInitCode(contract), contractCreate(contract)) .when(tokenCreate("someToken").exposingAddressTo(someTokenAddress::set)) @@ -307,7 +307,7 @@ final Stream hollowCreationFailsCleanly() { final var contract = "HollowAccountCreator"; return defaultHapiSpec("HollowCreationFailsCleanly", FULLY_NONDETERMINISTIC) .given( - streamMustIncludeNoFailuresFrom(sidecarIdValidator()), + recordStreamMustIncludeNoFailuresFrom(sidecarIdValidator()), uploadInitCode(contract), contractCreate(contract)) .when(contractCall(contract, "testCallFoo", randomHeadlongAddress(), BigInteger.valueOf(500_000L)) @@ -1971,7 +1971,7 @@ final Stream hscsEvm010ReceiverMustSignContractTx() { NONDETERMINISTIC_FUNCTION_PARAMETERS, NONDETERMINISTIC_TRANSACTION_FEES) .given( - streamMustIncludeNoFailuresFrom(sidecarIdValidator()), + recordStreamMustIncludeNoFailuresFrom(sidecarIdValidator()), newKeyNamed(RECEIVER_KEY), cryptoCreate(ACC) .balance(5 * ONE_HUNDRED_HBARS) @@ -2569,7 +2569,7 @@ final Stream callToNonExtantLongZeroAddressUsesTargetedAddress() { final var nonExtantMirrorAddress = asHeadlongAddress("0xE8D4A50FFF"); return defaultHapiSpec("callToNonExtantLongZeroAddressUsesTargetedAddress") .given( - streamMustIncludeNoFailuresFrom(sidecarIdValidator()), + recordStreamMustIncludeNoFailuresFrom(sidecarIdValidator()), uploadInitCode(contract), contractCreate(contract)) .when() @@ -2583,7 +2583,7 @@ final Stream callToNonExtantEvmAddressUsesTargetedAddress() { final var nonExtantEvmAddress = asHeadlongAddress(TxnUtils.randomUtf8Bytes(20)); return defaultHapiSpec("callToNonExtantEvmAddressUsesTargetedAddress") .given( - streamMustIncludeNoFailuresFrom(sidecarIdValidator()), + recordStreamMustIncludeNoFailuresFrom(sidecarIdValidator()), uploadInitCode(contract), contractCreate(contract)) .when() diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractKeysStillWorkAsExpectedSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractKeysStillWorkAsExpectedSuite.java index c38ee70f02aa..cf9beecc0905 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractKeysStillWorkAsExpectedSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractKeysStillWorkAsExpectedSuite.java @@ -44,9 +44,9 @@ import static com.hedera.services.bdd.spec.utilops.UtilVerbs.childRecordsCheck; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.noOp; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.recordStreamMustIncludePassFrom; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.recordedChildBodyWithId; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sourcing; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.streamMustInclude; import static com.hedera.services.bdd.spec.utilops.records.SnapshotMatchMode.NONDETERMINISTIC_FUNCTION_PARAMETERS; import static com.hedera.services.bdd.spec.utilops.records.SnapshotMatchMode.NONDETERMINISTIC_NONCE; import static com.hedera.services.bdd.suites.HapiSuite.ONE_HBAR; @@ -85,21 +85,22 @@ final Stream approvalFallbacksRequiredWithoutTopLevelSigAccess() { final AtomicReference
      bReceiverAddr = new AtomicReference<>(); return hapiTest( - streamMustInclude(recordedChildBodyWithId(TOKEN_UNIT_FROM_TO_OTHERS_TXN, 1, (spec, txn) -> { - if (txn.hasNodeStakeUpdate()) { - // Avoid asserting something about an end-of-staking-period NodeStakeUpdate in CI - return; - } - final var tokenTransfers = txn.getCryptoTransfer().getTokenTransfersList(); - assertEquals(1, tokenTransfers.size()); - final var tokenTransfer = tokenTransfers.getFirst(); - for (final var adjust : tokenTransfer.getTransfersList()) { - if (adjust.getAmount() < 0) { - // The debit should have been automatically converted to an approval - assertTrue(adjust.getIsApproval()); - } - } - })), + recordStreamMustIncludePassFrom( + recordedChildBodyWithId(TOKEN_UNIT_FROM_TO_OTHERS_TXN, 1, (spec, txn) -> { + if (txn.hasNodeStakeUpdate()) { + // Avoid asserting something about an end-of-staking-period NodeStakeUpdate in CI + return; + } + final var tokenTransfers = txn.getCryptoTransfer().getTokenTransfersList(); + assertEquals(1, tokenTransfers.size()); + final var tokenTransfer = tokenTransfers.getFirst(); + for (final var adjust : tokenTransfer.getTransfersList()) { + if (adjust.getAmount() < 0) { + // The debit should have been automatically converted to an approval + assertTrue(adjust.getIsApproval()); + } + } + })), someWellKnownTokensAndAccounts( fungibleTokenMirrorAddr, nonFungibleTokenMirrorAddr, diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/CryptoUpdateSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/CryptoUpdateSuite.java index 04c5f8d0271e..4b038e755376 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/CryptoUpdateSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/CryptoUpdateSuite.java @@ -35,6 +35,7 @@ import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractUpdate; 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.cryptoUpdate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenAssociate; @@ -50,6 +51,7 @@ import static com.hedera.services.bdd.spec.utilops.mod.ModificationUtils.withSuccessivelyVariedBodyIds; import static com.hedera.services.bdd.spec.utilops.records.SnapshotMatchMode.EXPECT_STREAMLINED_INGEST_RECORDS; import static com.hedera.services.bdd.spec.utilops.records.SnapshotMatchMode.NONDETERMINISTIC_TRANSACTION_FEES; +import static com.hedera.services.bdd.suites.HapiSuite.DEFAULT_PAYER; 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_HUNDRED_HBARS; @@ -57,6 +59,7 @@ import static com.hedera.services.bdd.suites.HapiSuite.ZERO_BYTE_MEMO; import static com.hedera.services.bdd.suites.contract.hapi.ContractUpdateSuite.ADMIN_KEY; import static com.hederahashgraph.api.proto.java.HederaFunctionality.CryptoUpdate; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_DELETED; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.EXISTING_AUTOMATIC_ASSOCIATIONS_EXCEED_GIVEN_LIMIT; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_ADMIN_KEY; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_EXPIRATION_TIME; @@ -549,4 +552,16 @@ final Stream updateMaxAutoAssociationsWorks() { contractUpdate(CONTRACT).newMaxAutomaticAssociations(-1).hasKnownStatus(SUCCESS), getContractInfo(CONTRACT).has(contractWith().maxAutoAssociations(-1))); } + + @HapiTest + final Stream deletedAccountCannotBeUpdated() { + final var accountToDelete = "accountToDelete"; + return hapiTest( + cryptoCreate(accountToDelete).declinedReward(false), + cryptoDelete(accountToDelete), + cryptoUpdate(accountToDelete) + .payingWith(DEFAULT_PAYER) + .newDeclinedReward(true) + .hasKnownStatus(ACCOUNT_DELETED)); + } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip993/NaturalDispatchOrderingTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip993/NaturalDispatchOrderingTest.java index a6418db0ea2e..906f44b67b4b 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip993/NaturalDispatchOrderingTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip993/NaturalDispatchOrderingTest.java @@ -18,7 +18,7 @@ import static com.hedera.services.bdd.junit.hedera.NodeSelector.byNodeId; import static com.hedera.services.bdd.junit.hedera.utils.WorkingDirUtils.guaranteedExtantDir; -import static com.hedera.services.bdd.junit.support.RecordStreamAccess.RECORD_STREAM_ACCESS; +import static com.hedera.services.bdd.junit.support.StreamFileAccess.STREAM_FILE_ACCESS; import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; import static com.hedera.services.bdd.spec.dsl.operations.transactions.TouchBalancesOperation.touchBalanceOf; import static com.hedera.services.bdd.spec.keys.TrieSigMapGenerator.uniqueWithFullPrefixesFor; @@ -30,7 +30,7 @@ import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.createHollow; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.streamMustIncludeNoFailuresFrom; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.recordStreamMustIncludeNoFailuresFrom; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.visibleNonSyntheticItems; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; import static com.hedera.services.bdd.suites.HapiSuite.DEFAULT_PAYER; @@ -51,6 +51,7 @@ import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import com.esaulpaugh.headlong.abi.Function; import com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory; @@ -107,7 +108,7 @@ public class NaturalDispatchOrderingTest { @BeforeAll static void setUp(@NonNull final TestLifecycle testLifecycle) { testLifecycle.doAdhoc(withOpContext((spec, opLog) -> { - unsubscribe = RECORD_STREAM_ACCESS.subscribe( + unsubscribe = STREAM_FILE_ACCESS.subscribe( guaranteedExtantDir(spec.streamsLoc(byNodeId(0))), new StreamDataListener() {}); triggerAndCloseAtLeastOneFile(spec); })); @@ -133,7 +134,7 @@ static void cleanUp() { @DisplayName("reversible user stream items are as expected") final Stream reversibleUserItemsAsExpected() { return hapiTest( - streamMustIncludeNoFailuresFrom( + recordStreamMustIncludeNoFailuresFrom( visibleNonSyntheticItems(reversibleUserValidator(), "firstCreation", "duplicateCreation")), scheduleCreate( "scheduledTxn", @@ -172,7 +173,7 @@ final Stream reversibleChildAndRemovablePrecedingItemsAsExpected( @Contract(contract = "LowLevelCall") SpecContract lowLevelCallContract) { final var transferFunction = new Function("transferNFTThanRevertCall(address,address,address,int64)"); return hapiTest( - streamMustIncludeNoFailuresFrom(visibleNonSyntheticItems( + recordStreamMustIncludeNoFailuresFrom(visibleNonSyntheticItems( reversibleChildValidator(), "fullSuccess", "containedRevert", "fullRevert")), nonFungibleToken.treasury().authorizeContract(transferContract), transferContract @@ -231,7 +232,7 @@ final Stream reversibleScheduleAndRemovablePrecedingItemsAsExpected @Account(centBalance = 7, maxAutoAssociations = UNLIMITED_AUTO_ASSOCIATION_SLOTS) SpecAccount insolventPayer) { return hapiTest( - streamMustIncludeNoFailuresFrom( + recordStreamMustIncludeNoFailuresFrom( visibleNonSyntheticItems(reversibleScheduleValidator(), "committed", "rolledBack")), firstToken.treasury().transferUnitsTo(solventPayer, 10, firstToken), secondToken.treasury().transferUnitsTo(insolventPayer, 10, secondToken), @@ -281,7 +282,7 @@ final Stream removableChildItemsAsExpected( final var startChainFn = new Function("startChain(bytes)"); final var emptyMessage = new byte[0]; return hapiTest( - streamMustIncludeNoFailuresFrom( + recordStreamMustIncludeNoFailuresFrom( visibleNonSyntheticItems(removableChildValidator(), "nestedCreations", "revertedCreations")), outerCreatorContract.call("startChain", emptyMessage).with(txn -> txn.gas(2_000_000) .via("nestedCreations")), @@ -315,7 +316,7 @@ final Stream removableChildItemsAsExpected( @DisplayName("irreversible preceding stream items are as expected") final Stream irreversiblePrecedingItemsAsExpected() { return hapiTest( - streamMustIncludeNoFailuresFrom(visibleNonSyntheticItems( + recordStreamMustIncludeNoFailuresFrom(visibleNonSyntheticItems( irreversiblePrecedingValidator(), "finalizationBySuccess", "finalizationByFailure")), tokenCreate("unassociatedToken"), // Create two hollow accounts to finalize, first by a top-level success and second by failure @@ -455,6 +456,9 @@ private static void assertParentChildStructure( withNonce(userTransactionID, nextExpectedNonce++ - postTriggeredOffset), following.txnId()); assertEquals(userConsensusTime, following.parentConsensusTimestamp()); } + if (following.txnId().getScheduled()) { + assertTrue(following.txnRecord().getReceipt().hasExchangeRate()); + } } } 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 8ffad8f00109..ea11adb35e7b 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 @@ -40,12 +40,12 @@ import static com.hedera.services.bdd.spec.utilops.UtilVerbs.nOps; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.overriding; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.overridingTwo; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.recordStreamMustIncludeNoFailuresFrom; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.recordStreamMustIncludePassFrom; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.selectedItems; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.simulatePostUpgradeTransaction; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sourcing; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sourcingContextual; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.streamMustIncludeNoFailuresFrom; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.streamMustIncludePassFrom; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.validateChargedUsdWithin; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.visibleItems; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; @@ -124,7 +124,7 @@ final Stream syntheticNodeDetailsUpdateHappensAtUpgradeBoundary() { }; final AtomicReference> gossipCertificates = new AtomicReference<>(); return hapiTest( - streamMustIncludePassFrom(selectedItems( + recordStreamMustIncludePassFrom(selectedItems( addressBookExportValidator(grpcCertHashes, gossipCertificates), 1, this::isSysFileUpdate)), given(() -> gossipCertificates.set(generateCertificates(CLASSIC_HAPI_TEST_NETWORK_SIZE))), // This is the genesis transaction @@ -148,7 +148,7 @@ final Stream syntheticFeeSchedulesUpdateHappensAtUpgradeBoundary() final var upgradeFeeSchedules = CurrentAndNextFeeSchedule.parseFrom(SYS_FILE_SERDES.get(111L).toRawFile(feeSchedulesJson, null)); return hapiTest( - streamMustIncludePassFrom(selectedItems( + recordStreamMustIncludePassFrom(selectedItems( sysFileExportValidator( "files.feeSchedules", upgradeFeeSchedules, SystemFileExportsTest::parseFeeSchedule), 2, @@ -191,7 +191,7 @@ final Stream syntheticThrottlesUpdateHappensAtUpgradeBoundary() thr final var upgradeThrottleDefs = ThrottleDefinitions.parseFrom(SYS_FILE_SERDES.get(123L).toRawFile(throttlesJson, null)); return hapiTest( - streamMustIncludePassFrom(selectedItems( + recordStreamMustIncludePassFrom(selectedItems( sysFileExportValidator( "files.throttleDefinitions", upgradeThrottleDefs, @@ -232,7 +232,7 @@ final Stream syntheticPropertyOverridesUpdateHappensAtUpgradeBounda final var upgradePropOverrides = ServicesConfigurationList.parseFrom(SYS_FILE_SERDES.get(121L).toRawFile(overrideProperties, null)); return hapiTest( - streamMustIncludePassFrom(selectedItems( + recordStreamMustIncludePassFrom(selectedItems( sysFileExportValidator( "files.networkProperties", upgradePropOverrides, @@ -267,7 +267,7 @@ final Stream syntheticPropertyOverridesUpdateHappensAtUpgradeBounda @GenesisHapiTest final Stream syntheticPropertyOverridesUpdateCanBeEmptyFile() { return hapiTest( - streamMustIncludePassFrom(selectedItems( + recordStreamMustIncludePassFrom(selectedItems( sysFileExportValidator( "files.networkProperties", ServicesConfigurationList.getDefaultInstance(), @@ -308,7 +308,7 @@ final Stream syntheticPermissionOverridesUpdateHappensAtUpgradeBoun final var upgradePermissionOverrides = ServicesConfigurationList.parseFrom(SYS_FILE_SERDES.get(122L).toRawFile(overridePermissions, null)); return hapiTest( - streamMustIncludePassFrom(selectedItems( + recordStreamMustIncludePassFrom(selectedItems( sysFileExportValidator( "files.hapiPermissions", upgradePermissionOverrides, @@ -347,7 +347,7 @@ final Stream syntheticPermissionOverridesUpdateHappensAtUpgradeBoun final Stream syntheticFileCreationsMatchQueries() { final AtomicReference> preGenesisContents = new AtomicReference<>(); return hapiTest( - streamMustIncludeNoFailuresFrom(visibleItems(validatorFor(preGenesisContents), "genesisTxn")), + recordStreamMustIncludeNoFailuresFrom(visibleItems(validatorFor(preGenesisContents), "genesisTxn")), getSystemFiles(preGenesisContents::set), cryptoCreate("firstUser").via("genesisTxn"), // Assert the first created entity still has the expected number diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/token/TokenAssociationSpecs.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/token/TokenAssociationSpecs.java index 5a7201250925..135dcb515a0c 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/token/TokenAssociationSpecs.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/token/TokenAssociationSpecs.java @@ -71,6 +71,7 @@ import static com.hederahashgraph.api.proto.java.TokenFreezeStatus.Unfrozen; import static com.hederahashgraph.api.proto.java.TokenKycStatus.Granted; import static com.hederahashgraph.api.proto.java.TokenKycStatus.KycNotApplicable; +import static com.hederahashgraph.api.proto.java.TokenType.FUNGIBLE_COMMON; import static com.hederahashgraph.api.proto.java.TokenType.NON_FUNGIBLE_UNIQUE; import com.google.protobuf.ByteString; @@ -583,4 +584,22 @@ public static HapiSpecOperation[] basicKeysAndTokens() { tokenCreate(VANILLA_TOKEN).treasury(TOKEN_TREASURY) }; } + + @HapiTest + final Stream deletedAccountCannotBeAssociatedToToken() { + final var accountToDelete = "accountToDelete"; + final var token = "anyToken"; + final var supplyKey = "supplyKey"; + return hapiTest( + newKeyNamed(supplyKey), + cryptoCreate(accountToDelete), + cryptoDelete(accountToDelete), + tokenCreate(token) + .treasury(DEFAULT_PAYER) + .tokenType(FUNGIBLE_COMMON) + .initialSupply(1000L) + .supplyKey(supplyKey) + .hasKnownStatus(SUCCESS), + tokenAssociate(accountToDelete, token).hasKnownStatus(ACCOUNT_DELETED)); + } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/token/TokenTransactSpecs.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/token/TokenTransactSpecs.java index 08e6edd9fd18..d304c11f5383 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/token/TokenTransactSpecs.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/token/TokenTransactSpecs.java @@ -20,6 +20,7 @@ import static com.hedera.services.bdd.junit.TestTags.ADHOC; import static com.hedera.services.bdd.junit.TestTags.TOKEN; import static com.hedera.services.bdd.spec.HapiSpec.defaultHapiSpec; +import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; import static com.hedera.services.bdd.spec.assertions.AccountInfoAsserts.accountWith; import static com.hedera.services.bdd.spec.assertions.AccountInfoAsserts.changeFromSnapshot; import static com.hedera.services.bdd.spec.assertions.AutoAssocAsserts.accountTokenPairs; @@ -44,6 +45,7 @@ import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenDelete; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenDissociate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenFreeze; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenUpdate; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedHbarFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedHbarFeeInheritingRoyaltyCollector; @@ -2038,6 +2040,25 @@ final Stream collectorIsChargedRoyaltyFallbackFeeUnlessExempt() { .logged()); } + @HapiTest + final Stream tokenFrozenOnTreasuryCannotBeFrozenAgain() { + final var alice = "alice"; + final var token = "token"; + final var freezeKey = "freezeKey"; + return hapiTest( + newKeyNamed(freezeKey), + cryptoCreate(alice), + tokenCreate(token) + .treasury(DEFAULT_PAYER) + .tokenType(FUNGIBLE_COMMON) + .initialSupply(1000L) + .freezeKey(freezeKey) + .hasKnownStatus(SUCCESS), + tokenAssociate(alice, token), + tokenFreeze(token, alice).hasKnownStatus(SUCCESS), + tokenFreeze(token, alice).hasKnownStatus(ACCOUNT_FROZEN_FOR_TOKEN)); + } + private static final String TXN_TRIGGERING_COLLECTOR_EXEMPT_FEE = "collectorExempt"; private static final String TXN_TRIGGERING_COLLECTOR_NON_EXEMPT_FEE = "collectorNonExempt"; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/token/TokenUpdateSpecs.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/token/TokenUpdateSpecs.java index 45335e53072d..0d00678b0d0c 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/token/TokenUpdateSpecs.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/token/TokenUpdateSpecs.java @@ -34,6 +34,7 @@ import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenDelete; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenFeeScheduleUpdate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenFreeze; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenUnfreeze; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenUpdate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.wipeTokenAccount; @@ -47,6 +48,7 @@ import static com.hedera.services.bdd.spec.utilops.UtilVerbs.doSeveralWithStartupConfigNow; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.doWithStartupConfigNow; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.overriding; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.specOps; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; import static com.hedera.services.bdd.suites.HapiSuite.ADDRESS_BOOK_CONTROL; @@ -59,6 +61,7 @@ import static com.hedera.services.bdd.suites.HapiSuite.ZERO_BYTE_MEMO; import static com.hedera.services.bdd.suites.HapiSuite.salted; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_DELETED; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_FROZEN_FOR_TOKEN; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_ADMIN_KEY; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_AUTORENEW_ACCOUNT; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_CUSTOM_FEE_SCHEDULE_KEY; @@ -68,6 +71,7 @@ import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_TOKEN_ID; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_ZERO_BYTE_IN_STRING; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.NO_REMAINING_AUTOMATIC_ASSOCIATIONS; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_HAS_NO_FEE_SCHEDULE_KEY; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_HAS_NO_FREEZE_KEY; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_HAS_NO_KYC_KEY; @@ -85,6 +89,7 @@ import com.google.protobuf.ByteString; import com.hedera.services.bdd.junit.HapiTest; +import com.hedera.services.bdd.junit.LeakyHapiTest; import com.hedera.services.bdd.spec.queries.crypto.ExpectedTokenRel; import com.hedera.services.bdd.spec.transactions.TxnUtils; import com.hederahashgraph.api.proto.java.TokenFreezeStatus; @@ -1014,4 +1019,29 @@ final Stream updateUniqueTreasuryWithNfts() { getAccountInfo("newTreasury").logged(), getTokenInfo("tbu").hasTreasury("newTreasury")); } + + @LeakyHapiTest(overrides = {"tokens.nfts.useTreasuryWildcards"}) + final Stream tokenFrozenOnTreasuryCannotBeUpdated() { + final var accountToFreeze = "account"; + final var adminKey = "adminKey"; + final var tokenToFreeze = "token"; + return hapiTest( + overriding("tokens.nfts.useTreasuryWildcards", "false"), + newKeyNamed(adminKey), + cryptoCreate(accountToFreeze), + tokenCreate(tokenToFreeze) + .treasury(accountToFreeze) + .tokenType(NON_FUNGIBLE_UNIQUE) + .initialSupply(0) + .supplyKey(adminKey) + .freezeKey(adminKey) + .adminKey(adminKey) + .hasKnownStatus(SUCCESS), + tokenFreeze(tokenToFreeze, accountToFreeze), + tokenAssociate(DEFAULT_PAYER, tokenToFreeze), + tokenUpdate(tokenToFreeze) + .treasury(DEFAULT_PAYER) + .signedBy(DEFAULT_PAYER, adminKey) + .hasKnownStatus(ACCOUNT_FROZEN_FOR_TOKEN)); + } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/tss/RepeatableTssTests.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/tss/RepeatableTssTests.java new file mode 100644 index 000000000000..cec1d2c1e7f4 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/tss/RepeatableTssTests.java @@ -0,0 +1,70 @@ +/* + * 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.hedera.services.bdd.suites.tss; + +import static com.hedera.services.bdd.junit.RepeatableReason.NEEDS_TSS_CONTROL; +import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; +import static com.hedera.services.bdd.spec.utilops.TssVerbs.startIgnoringTssSignatureRequests; +import static com.hedera.services.bdd.spec.utilops.TssVerbs.stopIgnoringTssSignatureRequests; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.blockStreamMustIncludePassFrom; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.doAdhoc; + +import com.hedera.node.app.blocks.BlockStreamManager; +import com.hedera.services.bdd.junit.RepeatableHapiTest; +import com.hedera.services.bdd.junit.hedera.embedded.fakes.FakeTssBaseService; +import com.hedera.services.bdd.spec.utilops.streams.assertions.IndirectProofsAssertion; +import java.util.stream.Stream; +import org.junit.jupiter.api.DynamicTest; + +/** + * TSS tests that require repeatable mode to run. + */ +public class RepeatableTssTests { + /** + * A test that simulates the behavior of the {@link FakeTssBaseService} under specific conditions + * related to signature requests and block creation. + * + *

      This test follows three main steps:

      + *
        + *
      • Instructs the {@link FakeTssBaseService} to start ignoring signature requests and + * produces several blocks. In this scenario, each transaction is placed into its own round + * since the service is operating in repeatable mode.
      • + *
      • Verifies that no blocks are written, as no block proofs are available, which is the + * expected behavior when the service is ignoring signature requests.
      • + *
      • Reactivates the {@link FakeTssBaseService}, creates another block, and verifies that + * the {@link BlockStreamManager} processes pending block proofs. It checks that the expected + * blocks are written within a brief period after the service resumes normal behavior.
      • + *
      + * + *

      The test ensures that block production halts when block proofs are unavailable and + * verifies that the system can catch up on pending proofs when the service resumes.

      + */ + @RepeatableHapiTest(NEEDS_TSS_CONTROL) + Stream blockStreamManagerCatchesUpWithIndirectProofs() { + final var indirectProofsAssertion = new IndirectProofsAssertion(2); + return hapiTest( + startIgnoringTssSignatureRequests(), + blockStreamMustIncludePassFrom(spec -> indirectProofsAssertion), + // Each transaction is placed into its own round and hence block with default config + cryptoCreate("firstIndirectProof"), + cryptoCreate("secondIndirectProof"), + stopIgnoringTssSignatureRequests(), + doAdhoc(indirectProofsAssertion::startExpectingBlocks), + cryptoCreate("directProof")); + } +} diff --git a/platform-sdk/swirlds-merkledb/build.gradle.kts b/platform-sdk/swirlds-merkledb/build.gradle.kts index a17fc9fa72ee..1c613517ef36 100644 --- a/platform-sdk/swirlds-merkledb/build.gradle.kts +++ b/platform-sdk/swirlds-merkledb/build.gradle.kts @@ -72,5 +72,6 @@ hammerModuleInfo { requires("org.apache.logging.log4j.core") requires("org.junit.jupiter.api") requires("org.junit.jupiter.params") + runtimeOnly("com.swirlds.common.test.fixtures") runtimeOnly("com.swirlds.config.impl") } diff --git a/platform-sdk/swirlds-merkledb/src/hammer/java/com/swirlds/merkledb/files/DataFileCollectionCompactionHammerTest.java b/platform-sdk/swirlds-merkledb/src/hammer/java/com/swirlds/merkledb/files/DataFileCollectionCompactionHammerTest.java index 9141662de5ff..ecd08443013a 100644 --- a/platform-sdk/swirlds-merkledb/src/hammer/java/com/swirlds/merkledb/files/DataFileCollectionCompactionHammerTest.java +++ b/platform-sdk/swirlds-merkledb/src/hammer/java/com/swirlds/merkledb/files/DataFileCollectionCompactionHammerTest.java @@ -39,6 +39,7 @@ import org.apache.logging.log4j.core.config.Configurator; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Tags; import org.junit.jupiter.api.Test; @@ -49,6 +50,7 @@ /** * Hammer the compaction subsystem with as many small compactions as possible to try to overwhelm it. */ +@Disabled("This test needs to be investigated") class DataFileCollectionCompactionHammerTest { @BeforeAll diff --git a/platform-sdk/swirlds-merkledb/src/hammer/java/com/swirlds/merkledb/files/DataFileReaderHammerTest.java b/platform-sdk/swirlds-merkledb/src/hammer/java/com/swirlds/merkledb/files/DataFileReaderHammerTest.java index 125a58dd770a..9361370f4aad 100644 --- a/platform-sdk/swirlds-merkledb/src/hammer/java/com/swirlds/merkledb/files/DataFileReaderHammerTest.java +++ b/platform-sdk/swirlds-merkledb/src/hammer/java/com/swirlds/merkledb/files/DataFileReaderHammerTest.java @@ -36,9 +36,11 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReferenceArray; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +@Disabled("This test needs to be investigated") public class DataFileReaderHammerTest { @Test diff --git a/platform-sdk/swirlds-merkledb/src/hammer/java/com/swirlds/merkledb/files/MemoryIndexDiskKeyValueStoreCompactionHammerTest.java b/platform-sdk/swirlds-merkledb/src/hammer/java/com/swirlds/merkledb/files/MemoryIndexDiskKeyValueStoreCompactionHammerTest.java index 382268ea1212..d2981e928992 100644 --- a/platform-sdk/swirlds-merkledb/src/hammer/java/com/swirlds/merkledb/files/MemoryIndexDiskKeyValueStoreCompactionHammerTest.java +++ b/platform-sdk/swirlds-merkledb/src/hammer/java/com/swirlds/merkledb/files/MemoryIndexDiskKeyValueStoreCompactionHammerTest.java @@ -42,6 +42,7 @@ import org.apache.logging.log4j.core.config.Configurator; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.function.ThrowingSupplier; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; @@ -51,6 +52,7 @@ * Hammers the {@link MemoryIndexDiskKeyValueStore} with a ton of concurrent changes to validate the * index and the data files are in sync and have all the data they should have. */ +@Disabled("This test needs to be investigated") class MemoryIndexDiskKeyValueStoreCompactionHammerTest { /** Temporary directory provided by JUnit */ 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 4bf43ce66f31..03ff19a9fc37 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 @@ -23,17 +23,31 @@ import static com.swirlds.platform.builder.PlatformBuildConstants.DEFAULT_CONFIG_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.setupGlobalMetrics; +import static com.swirlds.platform.crypto.CryptoStatic.initNodeSecurity; import static com.swirlds.platform.gui.internal.BrowserWindowManager.addPlatforms; import static com.swirlds.platform.gui.internal.BrowserWindowManager.getStateHierarchy; import static com.swirlds.platform.gui.internal.BrowserWindowManager.moveBrowserWindowToFront; import static com.swirlds.platform.gui.internal.BrowserWindowManager.setBrowserWindow; import static com.swirlds.platform.gui.internal.BrowserWindowManager.setStateHierarchy; import static com.swirlds.platform.gui.internal.BrowserWindowManager.showBrowserWindow; +import static com.swirlds.platform.state.signed.StartupStateUtils.getInitialState; +import static com.swirlds.platform.system.address.AddressBookUtils.createRoster; +import static com.swirlds.platform.system.address.AddressBookUtils.initializeAddressBook; import static com.swirlds.platform.util.BootstrapUtils.checkNodesToRun; import static com.swirlds.platform.util.BootstrapUtils.getNodesToRun; import static com.swirlds.platform.util.BootstrapUtils.loadSwirldMains; import static com.swirlds.platform.util.BootstrapUtils.setupBrowserWindow; +import com.swirlds.base.time.Time; +import com.swirlds.common.context.PlatformContext; +import com.swirlds.common.crypto.CryptographyFactory; +import com.swirlds.common.crypto.CryptographyHolder; +import com.swirlds.common.io.filesystem.FileSystemManager; +import com.swirlds.common.io.utility.RecycleBin; +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; @@ -44,6 +58,7 @@ import com.swirlds.platform.builder.PlatformBuilder; import com.swirlds.platform.config.PathsConfig; import com.swirlds.platform.crypto.CryptoConstants; +import com.swirlds.platform.crypto.KeysAndCerts; import com.swirlds.platform.gui.GuiEventStorage; import com.swirlds.platform.gui.hashgraph.HashgraphGuiSource; import com.swirlds.platform.gui.hashgraph.internal.StandardGuiSource; @@ -52,10 +67,12 @@ import com.swirlds.platform.gui.model.InfoApp; import com.swirlds.platform.gui.model.InfoMember; import com.swirlds.platform.gui.model.InfoSwirld; +import com.swirlds.platform.state.signed.ReservedSignedState; import com.swirlds.platform.state.snapshot.SignedStateFileUtils; import com.swirlds.platform.system.SwirldMain; import com.swirlds.platform.system.SystemExitCode; import com.swirlds.platform.system.SystemExitUtils; +import com.swirlds.platform.system.address.AddressBook; import com.swirlds.platform.util.BootstrapUtils; import edu.umd.cs.findbugs.annotations.NonNull; import java.awt.GraphicsEnvironment; @@ -70,8 +87,12 @@ import org.apache.logging.log4j.Logger; /** - * The Browser that launches the Platforms that run the apps. + * The Browser that launches the Platforms that run the apps. This is used by the demo apps to launch the + * Platforms. + * This class will be removed once the demo apps moved to Inversion of Control pattern to build and start platform + * directly. */ +@Deprecated(forRemoval = true) public class Browser { // Each member is represented by an AddressBook entry in config.txt. On a given computer, a single java // process runs all members whose listed internal IP address matches some address on that computer. That @@ -213,21 +234,75 @@ private static void launchUnhandled(@NonNull final CommandLineArgs commandLineAr BootstrapUtils.setupConfigBuilder(configBuilder, getAbsolutePath(DEFAULT_SETTINGS_FILE_NAME))); final Configuration configuration = configBuilder.build(); + setupGlobalMetrics(configuration); + guiMetrics = getMetricsProvider().createPlatformMetrics(nodeId); + + final var recycleBin = RecycleBin.create( + guiMetrics, + configuration, + getStaticThreadManager(), + Time.getCurrent(), + FileSystemManager.create(configuration), + nodeId); + final var cryptography = CryptographyFactory.create(); + CryptographyHolder.set(cryptography); + final KeysAndCerts keysAndCerts = initNodeSecurity(appDefinition.getConfigAddressBook(), configuration) + .get(nodeId); + + // the AddressBook is not changed after this point, so we calculate the hash now + cryptography.digestSync(appDefinition.getConfigAddressBook()); + + // Set the MerkleCryptography instance for this node + final var merkleCryptography = MerkleCryptographyFactory.create(configuration, CryptographyHolder.get()); + MerkleCryptoFactory.set(merkleCryptography); + + // Create platform context + final var platformContext = PlatformContext.create( + configuration, + Time.getCurrent(), + guiMetrics, + cryptography, + FileSystemManager.create(configuration), + recycleBin, + MerkleCryptographyFactory.create(configuration, CryptographyHolder.get())); + // Create the initial state for the platform + final ReservedSignedState initialState = getInitialState( + platformContext, + appMain.getSoftwareVersion(), + appMain::newMerkleStateRoot, + SignedStateFileUtils::readState, + appMain.getClass().getName(), + appDefinition.getSwirldName(), + nodeId, + appDefinition.getConfigAddressBook()); + // Initialize the address book + final AddressBook addressBook = initializeAddressBook( + nodeId, + appMain.getSoftwareVersion(), + initialState, + appDefinition.getConfigAddressBook(), + platformContext); + + // Build the platform with the given values final PlatformBuilder builder = PlatformBuilder.create( appMain.getClass().getName(), appDefinition.getSwirldName(), appMain.getSoftwareVersion(), - appMain::newMerkleStateRoot, - SignedStateFileUtils::readState, + initialState, nodeId); - if (showUi && index == 0) { builder.withPreconsensusEventCallback(guiEventStorage::handlePreconsensusEvent); builder.withConsensusSnapshotOverrideCallback(guiEventStorage::handleSnapshotOverride); } - - final SwirldsPlatform platform = - (SwirldsPlatform) builder.withConfiguration(configuration).build(); + // Build platform using the Inversion of Control pattern by injecting all needed + // dependencies into the PlatformBuilder. + final SwirldsPlatform platform = (SwirldsPlatform) builder.withConfiguration(configuration) + .withPlatformContext(platformContext) + .withConfiguration(configuration) + .withAddressBook(addressBook) + .withRoster(createRoster(appDefinition.getConfigAddressBook())) + .withKeysAndCerts(keysAndCerts) + .build(); platforms.put(nodeId, platform); if (showUi) { 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 9b35b63716ba..d13802ff269c 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 @@ -107,7 +107,7 @@ public void loadReconnectState(@NonNull final SignedState signedState) { .init( platform, InitTrigger.RECONNECT, - signedState.getState().getPlatformState().getCreationSoftwareVersion()); + signedState.getState().getReadablePlatformState().getCreationSoftwareVersion()); 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 " @@ -136,13 +136,13 @@ public void loadReconnectState(@NonNull final SignedState signedState) { .getSignatureCollectorStateInput() .put(signedState.reserve("loading reconnect state into sig collector")); platformWiring.consensusSnapshotOverride(Objects.requireNonNull( - signedState.getState().getPlatformState().getSnapshot())); + signedState.getState().getReadablePlatformState().getSnapshot())); platformWiring .getAddressBookUpdateInput() .inject(new AddressBookUpdate( - signedState.getState().getPlatformState().getPreviousAddressBook(), - signedState.getState().getPlatformState().getAddressBook())); + signedState.getState().getReadablePlatformState().getPreviousAddressBook(), + signedState.getState().getReadablePlatformState().getAddressBook())); final AncientMode ancientMode = platformContext .getConfiguration() @@ -151,12 +151,12 @@ public void loadReconnectState(@NonNull final SignedState signedState) { platformWiring.updateEventWindow(new EventWindow( signedState.getRound(), - signedState.getState().getPlatformState().getAncientThreshold(), - signedState.getState().getPlatformState().getAncientThreshold(), + signedState.getState().getReadablePlatformState().getAncientThreshold(), + signedState.getState().getReadablePlatformState().getAncientThreshold(), ancientMode)); final RunningEventHashOverride runningEventHashOverride = new RunningEventHashOverride( - signedState.getState().getPlatformState().getLegacyRunningEventHash(), true); + signedState.getState().getReadablePlatformState().getLegacyRunningEventHash(), true); platformWiring.updateRunningHash(runningEventHashOverride); platformWiring.getPcesWriterRegisterDiscontinuityInput().inject(signedState.getRound()); 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 294343656360..743639e48fd4 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 @@ -65,7 +65,8 @@ public static void initializeState( previousSoftwareVersion = NO_VERSION; trigger = GENESIS; } else { - previousSoftwareVersion = signedState.getState().getPlatformState().getCreationSoftwareVersion(); + previousSoftwareVersion = + signedState.getState().getReadablePlatformState().getCreationSoftwareVersion(); trigger = RESTART; } 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 bd100f9eb929..e72df9a6ad31 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 @@ -200,7 +200,10 @@ public SwirldsPlatform(@NonNull final PlatformComponentBuilder builder) { platformContext, blocks.selfId(), initialState.getRound(), - initialState.getState().getPlatformState().getLowestJudgeGenerationBeforeBirthRoundMode()); + initialState + .getState() + .getReadablePlatformState() + .getLowestJudgeGenerationBeforeBirthRoundMode()); } catch (final IOException e) { throw new UncheckedIOException("Birth round migration failed during PCES migration.", e); } @@ -288,9 +291,9 @@ public SwirldsPlatform(@NonNull final PlatformComponentBuilder builder) { publisher); final Hash legacyRunningEventHash = - initialState.getState().getPlatformState().getLegacyRunningEventHash() == null + initialState.getState().getReadablePlatformState().getLegacyRunningEventHash() == null ? platformContext.getCryptography().getNullHash() - : initialState.getState().getPlatformState().getLegacyRunningEventHash(); + : initialState.getState().getReadablePlatformState().getLegacyRunningEventHash(); final RunningEventHashOverride runningEventHashOverride = new RunningEventHashOverride(legacyRunningEventHash, false); platformWiring.updateRunningHash(runningEventHashOverride); @@ -320,7 +323,8 @@ public SwirldsPlatform(@NonNull final PlatformComponentBuilder builder) { startingRound = 0; platformWiring.updateEventWindow(EventWindow.getGenesisEventWindow(ancientMode)); } else { - initialAncientThreshold = initialState.getState().getPlatformState().getAncientThreshold(); + initialAncientThreshold = + initialState.getState().getReadablePlatformState().getAncientThreshold(); startingRound = initialState.getRound(); platformWiring.sendStateToHashLogger(initialState); @@ -331,7 +335,7 @@ public SwirldsPlatform(@NonNull final PlatformComponentBuilder builder) { savedStateController.registerSignedStateFromDisk(initialState); platformWiring.consensusSnapshotOverride(Objects.requireNonNull( - initialState.getState().getPlatformState().getSnapshot())); + initialState.getState().getReadablePlatformState().getSnapshot())); // We only load non-ancient events during start up, so the initial expired threshold will be // equal to the ancient threshold when the system first starts. Over time as we get more events, @@ -378,7 +382,7 @@ private BirthRoundMigrationShim buildBirthRoundMigrationShim( } final MerkleRoot state = initialState.getState(); - final PlatformStateAccessor platformState = state.getPlatformState(); + final PlatformStateAccessor platformState = state.getReadablePlatformState(); return new DefaultBirthRoundMigrationShim( platformContext, diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/Utilities.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/Utilities.java index f3e7529722d2..3926f4e6ddc3 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/Utilities.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/Utilities.java @@ -379,7 +379,6 @@ public static boolean hasAnyCauseSuppliedType( .filter(address -> !address.getNodeId().equals(selfId)) .map(address -> new PeerInfo( address.getNodeId(), - address.getSelfName(), Objects.requireNonNull(address.getHostnameExternal()), Objects.requireNonNull(address.getSigCert()))) .toList(); 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 cf1efa69e374..14929e4471e8 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 @@ -17,50 +17,25 @@ package com.swirlds.platform.builder; import static com.swirlds.common.io.utility.FileUtils.getAbsolutePath; -import static com.swirlds.common.io.utility.FileUtils.rethrowIO; import static com.swirlds.common.threading.manager.AdHocThreadManager.getStaticThreadManager; import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; import static com.swirlds.logging.legacy.LogMarker.STARTUP; import static com.swirlds.platform.builder.PlatformBuildConstants.DEFAULT_CONFIG_FILE_NAME; -import static com.swirlds.platform.builder.PlatformBuildConstants.DEFAULT_SETTINGS_FILE_NAME; import static com.swirlds.platform.builder.internal.StaticPlatformBuilder.doStaticSetup; -import static com.swirlds.platform.builder.internal.StaticPlatformBuilder.getMetricsProvider; -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.event.preconsensus.PcesUtilities.getDatabaseDirectory; -import static com.swirlds.platform.state.signed.StartupStateUtils.getInitialState; -import static com.swirlds.platform.system.address.AddressBookUtils.createRoster; import static com.swirlds.platform.util.BootstrapUtils.checkNodesToRun; -import static com.swirlds.platform.util.BootstrapUtils.detectSoftwareUpgrade; import com.hedera.hapi.node.state.roster.Roster; -import com.swirlds.base.function.CheckedBiFunction; -import com.swirlds.base.time.Time; import com.swirlds.common.concurrent.ExecutorFactory; -import com.swirlds.common.context.DefaultPlatformContext; import com.swirlds.common.context.PlatformContext; -import com.swirlds.common.crypto.Cryptography; -import com.swirlds.common.crypto.CryptographyFactory; -import com.swirlds.common.crypto.CryptographyHolder; -import com.swirlds.common.io.filesystem.FileSystemManager; -import com.swirlds.common.io.streams.MerkleDataInputStream; -import com.swirlds.common.io.utility.RecycleBin; -import com.swirlds.common.merkle.crypto.MerkleCryptoFactory; -import com.swirlds.common.merkle.crypto.MerkleCryptography; -import com.swirlds.common.merkle.crypto.MerkleCryptographyFactory; import com.swirlds.common.notification.NotificationEngine; import com.swirlds.common.platform.NodeId; import com.swirlds.common.wiring.WiringConfig; import com.swirlds.common.wiring.model.WiringModel; import com.swirlds.common.wiring.model.WiringModelBuilder; import com.swirlds.config.api.Configuration; -import com.swirlds.config.api.ConfigurationBuilder; -import com.swirlds.metrics.api.Metrics; -import com.swirlds.platform.ParameterProvider; import com.swirlds.platform.SwirldsPlatform; -import com.swirlds.platform.config.legacy.LegacyConfigProperties; -import com.swirlds.platform.config.legacy.LegacyConfigPropertiesLoader; import com.swirlds.platform.consensus.ConsensusSnapshot; import com.swirlds.platform.crypto.KeysAndCerts; import com.swirlds.platform.event.PlatformEvent; @@ -74,10 +49,7 @@ import com.swirlds.platform.gossip.sync.config.SyncConfig; import com.swirlds.platform.pool.TransactionPoolNexus; import com.swirlds.platform.scratchpad.Scratchpad; -import com.swirlds.platform.state.MerkleRoot; -import com.swirlds.platform.state.PlatformStateAccessor; import com.swirlds.platform.state.SwirldStateManager; -import com.swirlds.platform.state.address.AddressBookInitializer; import com.swirlds.platform.state.iss.IssScratchpad; import com.swirlds.platform.state.signed.ReservedSignedState; import com.swirlds.platform.system.Platform; @@ -85,7 +57,6 @@ import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.system.address.AddressBook; import com.swirlds.platform.system.status.StatusActionSubmitter; -import com.swirlds.platform.util.BootstrapUtils; import com.swirlds.platform.util.RandomBuilder; import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; @@ -97,7 +68,6 @@ import java.util.concurrent.ForkJoinPool; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; -import java.util.function.Supplier; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -110,17 +80,11 @@ public final class PlatformBuilder { private final String appName; private final SoftwareVersion softwareVersion; - private final Supplier genesisStateBuilder; - private final CheckedBiFunction snapshotStateReader; + private final ReservedSignedState initialState; private final NodeId selfId; private final String swirldName; private Configuration configuration; - private Cryptography cryptography; - private Metrics metrics; - private Time time; - private FileSystemManager fileSystemManager; - private RecycleBin recycleBin; private ExecutorFactory executorFactory; private static final UncaughtExceptionHandler DEFAULT_UNCAUGHT_EXCEPTION_HANDLER = @@ -129,7 +93,7 @@ public final class PlatformBuilder { /** * An address book that is used to bootstrap the system. Traditionally read from config.txt. */ - private AddressBook bootstrapAddressBook; + private AddressBook addressBook; private Roster roster; @@ -152,6 +116,10 @@ public final class PlatformBuilder { * The source of non-cryptographic randomness for this platform. */ private RandomBuilder randomBuilder; + /** + * The platform context for this platform. + */ + private PlatformContext platformContext; private Consumer preconsensusEventConsumer; private Consumer snapshotOverrideConsumer; @@ -178,19 +146,16 @@ public final class PlatformBuilder { * @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 genesisStateBuilder a supplier that will be called to create the genesis state, if necessary - * @param snapshotStateReader a function to read an existing state snapshot, if exists + * @param initialState the genesis state supplied by the application */ @NonNull public static PlatformBuilder create( @NonNull final String appName, @NonNull final String swirldName, @NonNull final SoftwareVersion softwareVersion, - @NonNull final Supplier genesisStateBuilder, - @NonNull final CheckedBiFunction snapshotStateReader, + @NonNull final ReservedSignedState initialState, @NonNull final NodeId selfId) { - return new PlatformBuilder( - appName, swirldName, softwareVersion, genesisStateBuilder, snapshotStateReader, selfId); + return new PlatformBuilder(appName, swirldName, softwareVersion, initialState, selfId); } /** @@ -200,23 +165,20 @@ public static PlatformBuilder create( * 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 genesisStateBuilder a supplier that will be called to create the genesis state, if necessary - * @param snapshotStateReader a function to read an existing state snapshot, if exists + * @param initialState the genesis state supplied by application * @param selfId the ID of this node */ private PlatformBuilder( @NonNull final String appName, @NonNull final String swirldName, @NonNull final SoftwareVersion softwareVersion, - @NonNull final Supplier genesisStateBuilder, - @NonNull final CheckedBiFunction snapshotStateReader, + @NonNull final ReservedSignedState initialState, @NonNull final NodeId selfId) { this.appName = Objects.requireNonNull(appName); this.swirldName = Objects.requireNonNull(swirldName); this.softwareVersion = Objects.requireNonNull(softwareVersion); - this.genesisStateBuilder = Objects.requireNonNull(genesisStateBuilder); - this.snapshotStateReader = Objects.requireNonNull(snapshotStateReader); + this.initialState = Objects.requireNonNull(initialState); this.selfId = Objects.requireNonNull(selfId); StaticSoftwareVersion.setSoftwareVersion(softwareVersion); @@ -237,80 +199,6 @@ public PlatformBuilder withConfiguration(@NonNull final Configuration configurat return this; } - /** - * Provide the cryptography to use for this platform. If not provided then the default cryptography is used. - * - * @param cryptography the cryptography to use - * @return this - */ - @NonNull - public PlatformBuilder withCryptography(@NonNull final Cryptography cryptography) { - this.cryptography = Objects.requireNonNull(cryptography); - return this; - } - - /** - * Provide the metrics to use for this platform. If not provided then default metrics are created. - * - * @param metrics the metrics to use - * @return this - */ - @NonNull - public PlatformBuilder withMetrics(@NonNull final Metrics metrics) { - this.metrics = Objects.requireNonNull(metrics); - return this; - } - - /** - * Provide the time to use for this platform. If not provided then the default wall clock time is used. - * - * @param time the time to use - * @return this - */ - @NonNull - public PlatformBuilder withTime(@NonNull final Time time) { - this.time = Objects.requireNonNull(time); - return this; - } - - /** - * Provide the file system manager to use for this platform. If not provided then the default file system manager is - * used. - * - * @param fileSystemManager the file system manager to use - * @return this - */ - @NonNull - public PlatformBuilder withFileSystemManager(@NonNull final FileSystemManager fileSystemManager) { - this.fileSystemManager = Objects.requireNonNull(fileSystemManager); - return this; - } - - /** - * Provide the recycle bin to use for this platform. If not provided then the default recycle bin is used. - * - * @param recycleBin the recycle bin to use - * @return this - */ - @NonNull - public PlatformBuilder withRecycleBin(@NonNull final RecycleBin recycleBin) { - this.recycleBin = Objects.requireNonNull(recycleBin); - return this; - } - - /** - * Provide the executor factory to use for this platform. If not provided then the default executor factory is - * used. - * - * @param executorFactory the executor factory to use - * @return this - */ - @NonNull - public PlatformBuilder withExecutorFactory(@NonNull final ExecutorFactory executorFactory) { - this.executorFactory = Objects.requireNonNull(executorFactory); - return this; - } - /** * Registers a callback that is called for each valid non-ancient preconsensus event in topological order (i.e. * after each event exits the orphan buffer). Useful for scenarios where access to this internal stream of events is @@ -385,9 +273,9 @@ public PlatformBuilder withStaleEventCallback(@NonNull final Consumer ParameterProvider.getInstance().setParameters(c.params())); - return legacyConfig.getAddressBook(); + public PlatformBuilder withPlatformContext(@NonNull final PlatformContext platformContext) { + throwIfAlreadyUsed(); + this.platformContext = Objects.requireNonNull(platformContext); + return this; } /** @@ -476,116 +365,14 @@ public PlatformComponentBuilder buildComponentBuilder() { throwIfAlreadyUsed(); used = true; - if (configuration == null) { - final ConfigurationBuilder configurationBuilder = ConfigurationBuilder.create(); - rethrowIO(() -> BootstrapUtils.setupConfigBuilder( - configurationBuilder, getAbsolutePath(DEFAULT_SETTINGS_FILE_NAME))); - configuration = configurationBuilder.build(); - checkConfiguration(configuration); - } - - if (time == null) { - time = Time.getCurrent(); - } - - if (metrics == null) { - setupGlobalMetrics(configuration); - metrics = getMetricsProvider().createPlatformMetrics(selfId); - } - - if (cryptography == null) { - cryptography = CryptographyFactory.create(); - } - final MerkleCryptography merkleCryptography = MerkleCryptographyFactory.create(configuration, cryptography); - CryptographyHolder.set(cryptography); - MerkleCryptoFactory.set(merkleCryptography); - - if (fileSystemManager == null) { - fileSystemManager = FileSystemManager.create(configuration); - } - - if (recycleBin == null) { - recycleBin = RecycleBin.create( - metrics, configuration, getStaticThreadManager(), time, fileSystemManager, selfId); - } - if (executorFactory == null) { executorFactory = ExecutorFactory.create("platform", null, DEFAULT_UNCAUGHT_EXCEPTION_HANDLER); } - final PlatformContext platformContext = new DefaultPlatformContext( - configuration, - metrics, - cryptography, - time, - executorFactory, - fileSystemManager, - recycleBin, - merkleCryptography); - final boolean firstPlatform = doStaticSetup(configuration, configPath); - final AddressBook boostrapAddressBook = - this.bootstrapAddressBook == null ? loadConfigAddressBook() : this.bootstrapAddressBook; - checkNodesToRun(List.of(selfId)); - final KeysAndCerts keysAndCerts = this.keysAndCerts == null - ? initNodeSecurity(boostrapAddressBook, configuration).get(selfId) - : this.keysAndCerts; - - // the AddressBook is not changed after this point, so we calculate the hash now - platformContext.getCryptography().digestSync(boostrapAddressBook); - - final ReservedSignedState initialState = getInitialState( - platformContext, - softwareVersion, - genesisStateBuilder, - snapshotStateReader, - appName, - swirldName, - selfId, - boostrapAddressBook); - - final boolean softwareUpgrade = detectSoftwareUpgrade(softwareVersion, initialState.get()); - - // Initialize the address book from the configuration and platform saved state. - final AddressBookInitializer addressBookInitializer = new AddressBookInitializer( - selfId, - softwareVersion, - softwareUpgrade, - initialState.get(), - boostrapAddressBook.copy(), - platformContext); - - if (addressBookInitializer.hasAddressBookChanged()) { - final MerkleRoot state = initialState.get().getState(); - // Update the address book with the current address book read from config.txt. - // Eventually we will not do this, and only transactions will be capable of - // modifying the address book. - final PlatformStateAccessor platformState = state.getPlatformState(); - platformState.bulkUpdate(v -> { - v.setAddressBook(addressBookInitializer.getCurrentAddressBook().copy()); - v.setPreviousAddressBook( - addressBookInitializer.getPreviousAddressBook() == null - ? null - : addressBookInitializer - .getPreviousAddressBook() - .copy()); - }); - } - - // At this point the initial state must have the current address book set. If not, something is wrong. - final AddressBook addressBook = - initialState.get().getState().getPlatformState().getAddressBook(); - if (addressBook == null) { - throw new IllegalStateException("The current address book of the initial state is null."); - } - - if (roster == null) { - roster = createRoster(boostrapAddressBook); - } - final SyncConfig syncConfig = platformContext.getConfiguration().getConfigData(SyncConfig.class); final IntakeEventCounter intakeEventCounter; if (syncConfig.waitForEventsInIntake()) { 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 498b07089d33..bc74e1d6c31f 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 @@ -16,6 +16,8 @@ package com.swirlds.platform.builder; +import static java.util.Objects.requireNonNull; + import com.swirlds.common.context.PlatformContext; import com.swirlds.common.notification.NotificationEngine; import com.swirlds.common.platform.NodeId; @@ -122,6 +124,32 @@ public record PlatformBuildingBlocks( @NonNull AtomicReference clearAllPipelinesForReconnectReference, boolean firstPlatform) { + public PlatformBuildingBlocks { + requireNonNull(platformContext); + requireNonNull(model); + requireNonNull(keysAndCerts); + requireNonNull(selfId); + requireNonNull(mainClassName); + requireNonNull(swirldName); + requireNonNull(appVersion); + requireNonNull(initialState); + requireNonNull(applicationCallbacks); + requireNonNull(intakeEventCounter); + requireNonNull(randomBuilder); + requireNonNull(transactionPoolNexus); + requireNonNull(intakeQueueSizeSupplierSupplier); + requireNonNull(isInFreezePeriodReference); + requireNonNull(latestImmutableStateProviderReference); + requireNonNull(initialPcesFiles); + requireNonNull(issScratchpad); + requireNonNull(notificationEngine); + requireNonNull(statusActionSubmitterReference); + requireNonNull(swirldStateManager); + requireNonNull(getLatestCompleteStateReference); + requireNonNull(loadReconnectStateReference); + requireNonNull(clearAllPipelinesForReconnectReference); + } + /** * Get the address book from the initial state. * @@ -129,6 +157,6 @@ public record PlatformBuildingBlocks( */ @NonNull public AddressBook initialAddressBook() { - return initialState.get().getState().getPlatformState().getAddressBook(); + return initialState.get().getState().getReadablePlatformState().getAddressBook(); } } 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 ca6289066c4a..88e9f3cd898e 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 @@ -360,7 +360,11 @@ public EventSignatureValidator buildEventSignatureValidator() { blocks.platformContext(), CryptoStatic::verifySignature, blocks.appVersion().getPbjSemanticVersion(), - blocks.initialState().get().getState().getPlatformState().getPreviousAddressBook(), + blocks.initialState() + .get() + .getState() + .getReadablePlatformState() + .getPreviousAddressBook(), blocks.initialAddressBook(), blocks.intakeEventCounter()); } @@ -859,7 +863,11 @@ public IssDetector buildIssDetector() { issDetector = new DefaultIssDetector( blocks.platformContext(), - blocks.initialState().get().getState().getPlatformState().getAddressBook(), + blocks.initialState() + .get() + .getState() + .getReadablePlatformState() + .getAddressBook(), blocks.appVersion().getPbjSemanticVersion(), ignorePreconsensusSignatures, roundToIgnore); 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 d6cd90976a94..572a55e2a720 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 @@ -76,7 +76,7 @@ public Integer call() throws IOException, ExecutionException, InterruptedExcepti SignedStateFileReader.readStateFile(platformContext, statePath, SignedStateFileUtils::readState); try (final ReservedSignedState reservedSignedState = deserializedSignedState.reservedSignedState()) { final PlatformStateAccessor platformState = - reservedSignedState.get().getState().getPlatformState(); + reservedSignedState.get().getState().getWritablePlatformState(); platformState.bulkUpdate(v -> { System.out.printf("Replacing platform data %n"); v.setRound(PlatformStateAccessor.GENESIS_ROUND); 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 42d4ef68afda..295bd8934cd4 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 @@ -82,7 +82,7 @@ public Integer call() throws IOException, ExecutionException, InterruptedExcepti final AddressBook stateAddressBook; try (final ReservedSignedState reservedSignedState = deserializedSignedState.reservedSignedState()) { final PlatformStateAccessor platformState = - reservedSignedState.get().getState().getPlatformState(); + reservedSignedState.get().getState().getReadablePlatformState(); System.out.printf("Extracting the state address book for comparison %n"); stateAddressBook = platformState.getAddressBook(); } 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 e972df500e8d..42371c32f49f 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 @@ -221,7 +221,7 @@ public StateAndRound handleConsensusRound(@NonNull final ConsensusRound consensu */ private void updatePlatformState(@NonNull final ConsensusRound round) { final PlatformStateAccessor platformState = - swirldStateManager.getConsensusState().getPlatformState(); + swirldStateManager.getConsensusState().getWritablePlatformState(); platformState.bulkUpdate(v -> { v.setRound(round.getRoundNum()); v.setConsensusTimestamp(round.getConsensusTimestamp()); @@ -239,7 +239,7 @@ private void updatePlatformState(@NonNull final ConsensusRound round) { */ private void updateRunningEventHash(@NonNull final ConsensusRound round) throws InterruptedException { final PlatformStateAccessor platformState = - swirldStateManager.getConsensusState().getPlatformState(); + swirldStateManager.getConsensusState().getWritablePlatformState(); if (writeLegacyRunningEventHash) { // Update the running hash object. If there are no events, the running hash does not change. diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/PeerInfo.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/PeerInfo.java index 39873c2f5052..c3d69d66737f 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/PeerInfo.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/PeerInfo.java @@ -17,6 +17,7 @@ package com.swirlds.platform.network; import com.swirlds.common.platform.NodeId; +import com.swirlds.platform.roster.RosterUtils; import edu.umd.cs.findbugs.annotations.NonNull; import java.security.cert.Certificate; @@ -24,12 +25,16 @@ * A record representing a peer's network information. * * @param nodeId the ID of the peer - * @param nodeName the name of the peer * @param hostname the hostname (or IP address) of the peer * @param signingCertificate the certificate used to validate the peer's TLS certificate */ -public record PeerInfo( - @NonNull NodeId nodeId, - @NonNull String nodeName, - @NonNull String hostname, - @NonNull Certificate signingCertificate) {} +public record PeerInfo(@NonNull NodeId nodeId, @NonNull String hostname, @NonNull Certificate signingCertificate) { + /** + * Return a "node name" for the peer, e.g. "node1" for a peer with NodeId == 0. + * @return a "node name" + */ + @NonNull + public String nodeName() { + return RosterUtils.formatNodeName(nodeId.id()); + } +} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/reconnect/DefaultSignedStateValidator.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/reconnect/DefaultSignedStateValidator.java index 1db603c57fd1..25b8a8ebb364 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/reconnect/DefaultSignedStateValidator.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/reconnect/DefaultSignedStateValidator.java @@ -67,10 +67,10 @@ public void validate( private void throwIfOld(final SignedState signedState, final SignedStateValidationData previousStateData) throws SignedStateInvalidException { - if (signedState.getState().getPlatformState().getRound() < previousStateData.round() + if (signedState.getState().getReadablePlatformState().getRound() < previousStateData.round() || signedState .getState() - .getPlatformState() + .getReadablePlatformState() .getConsensusTimestamp() .isBefore(previousStateData.consensusTimestamp())) { logger.error( 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 c4366cb9eb2c..055b9c77e750 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 @@ -104,7 +104,7 @@ public ReconnectLearner( this.statistics = Objects.requireNonNull(statistics); // Save some of the current state data for validation - this.stateValidationData = new SignedStateValidationData(currentState.getPlatformState(), addressBook); + this.stateValidationData = new SignedStateValidationData(currentState.getReadablePlatformState(), addressBook); } /** 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 fd7308f5f91b..fc3e2ca68636 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 @@ -153,7 +153,7 @@ public static void recoverState( platformContext, signedStateFile, SignedStateFileUtils::readState) .reservedSignedState()) { StaticSoftwareVersion.setSoftwareVersion( - initialState.get().getState().getPlatformState().getCreationSoftwareVersion()); + initialState.get().getState().getReadablePlatformState().getCreationSoftwareVersion()); logger.info( STARTUP.getMarker(), @@ -314,7 +314,7 @@ public static RecoveredState reapplyTransactions( .init( platform, InitTrigger.EVENT_STREAM_RECOVERY, - initialState.get().getState().getPlatformState().getCreationSoftwareVersion()); + initialState.get().getState().getReadablePlatformState().getCreationSoftwareVersion()); appMain.init(platform, platform.getSelfId()); @@ -379,32 +379,33 @@ private static ReservedSignedState handleNextRound( final PlatformEvent lastEvent = ((CesEvent) getLastEvent(round)).getPlatformEvent(); new DefaultEventHasher().hashEvent(lastEvent); - final PlatformStateAccessor platformState = newState.getPlatformState(); + final PlatformStateAccessor newReadablePlatformState = newState.getReadablePlatformState(); + final PlatformStateAccessor newWritablePlatformState = newState.getWritablePlatformState(); + final PlatformStateAccessor previousReadablePlatformState = + previousState.get().getState().getReadablePlatformState(); - platformState.bulkUpdate(v -> { + newWritablePlatformState.bulkUpdate(v -> { v.setRound(round.getRoundNum()); - v.setLegacyRunningEventHash(getHashEventsCons( - previousState.get().getState().getPlatformState().getLegacyRunningEventHash(), round)); + v.setLegacyRunningEventHash( + getHashEventsCons(previousReadablePlatformState.getLegacyRunningEventHash(), round)); v.setConsensusTimestamp(currentRoundTimestamp); v.setSnapshot(SyntheticSnapshot.generateSyntheticSnapshot( round.getRoundNum(), lastEvent.getConsensusOrder(), currentRoundTimestamp, config, lastEvent)); - v.setCreationSoftwareVersion( - previousState.get().getState().getPlatformState().getCreationSoftwareVersion()); + v.setCreationSoftwareVersion(previousReadablePlatformState.getCreationSoftwareVersion()); }); applyTransactions( previousState.get().getSwirldState().cast(), newState.getSwirldState().cast(), - newState.getPlatformState(), + newState.getWritablePlatformState(), round); final boolean isFreezeState = isFreezeState( previousState.get().getConsensusTimestamp(), currentRoundTimestamp, - newState.getPlatformState().getFreezeTime()); + newReadablePlatformState.getFreezeTime()); if (isFreezeState) { - newState.getPlatformState() - .setLastFrozenTime(newState.getPlatformState().getFreezeTime()); + newWritablePlatformState.setLastFrozenTime(newReadablePlatformState.getFreezeTime()); } final ReservedSignedState signedState = new SignedState( diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/roster/RosterUtils.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/roster/RosterUtils.java new file mode 100644 index 000000000000..837b9526151b --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/roster/RosterUtils.java @@ -0,0 +1,39 @@ +/* + * 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.roster; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A utility class to help use Rooster and RosterEntry instances. + */ +public final class RosterUtils { + private RosterUtils() {} + + /** + * Formats a "node name" for a given node id, e.g. "node1" for nodeId == 0. + * This name can be used for logging purposes, or to support code that + * uses strings to identify nodes. + * + * @param nodeId a node id + * @return a "node name" + */ + @NonNull + public static String formatNodeName(final long nodeId) { + return "node" + (nodeId + 1); + } +} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/BirthRoundStateMigration.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/BirthRoundStateMigration.java index 7a5ed7b9acaf..7dde25310efc 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/BirthRoundStateMigration.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/BirthRoundStateMigration.java @@ -53,7 +53,7 @@ public static void modifyStateForBirthRoundMigration( @NonNull final SoftwareVersion appVersion) { if (ancientMode == AncientMode.GENERATION_THRESHOLD) { - if (initialState.getState().getPlatformState().getFirstVersionInBirthRoundMode() != null) { + if (initialState.getState().getReadablePlatformState().getFirstVersionInBirthRoundMode() != null) { throw new IllegalStateException( "Cannot revert to generation mode after birth round migration has been completed."); } @@ -64,18 +64,18 @@ public static void modifyStateForBirthRoundMigration( } final MerkleRoot state = initialState.getState(); - final PlatformStateAccessor platformState = state.getPlatformState(); + final PlatformStateAccessor writablePlatformState = state.getWritablePlatformState(); - final boolean alreadyMigrated = platformState.getFirstVersionInBirthRoundMode() != null; + final boolean alreadyMigrated = writablePlatformState.getFirstVersionInBirthRoundMode() != null; if (alreadyMigrated) { // Birth round migration was completed at a prior time, no action needed. logger.info(STARTUP.getMarker(), "Birth round state migration has already been completed."); return; } - final long lastRoundBeforeMigration = platformState.getRound(); + final long lastRoundBeforeMigration = writablePlatformState.getRound(); - final ConsensusSnapshot consensusSnapshot = Objects.requireNonNull(platformState.getSnapshot()); + final ConsensusSnapshot consensusSnapshot = Objects.requireNonNull(writablePlatformState.getSnapshot()); final List judgeInfoList = consensusSnapshot.getMinimumJudgeInfoList(); final long lowestJudgeGenerationBeforeMigration = judgeInfoList.getLast().minimumJudgeAncientThreshold(); @@ -88,9 +88,9 @@ public static void modifyStateForBirthRoundMigration( lastRoundBeforeMigration, lowestJudgeGenerationBeforeMigration); - platformState.setFirstVersionInBirthRoundMode(appVersion); - platformState.setLastRoundBeforeBirthRoundMode(lastRoundBeforeMigration); - platformState.setLowestJudgeGenerationBeforeBirthRoundMode(lowestJudgeGenerationBeforeMigration); + writablePlatformState.setFirstVersionInBirthRoundMode(appVersion); + writablePlatformState.setLastRoundBeforeBirthRoundMode(lastRoundBeforeMigration); + writablePlatformState.setLowestJudgeGenerationBeforeBirthRoundMode(lowestJudgeGenerationBeforeMigration); final List modifiedJudgeInfoList = new ArrayList<>(judgeInfoList.size()); for (final MinimumJudgeInfo judgeInfo : judgeInfoList) { @@ -102,7 +102,7 @@ public static void modifyStateForBirthRoundMigration( modifiedJudgeInfoList, consensusSnapshot.nextConsensusNumber(), consensusSnapshot.consensusTimestamp()); - platformState.setSnapshot(modifiedConsensusSnapshot); + writablePlatformState.setSnapshot(modifiedConsensusSnapshot); state.invalidateHash(); MerkleCryptoFactory.getInstance().digestTreeSync(state); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/GenesisStateBuilder.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/GenesisStateBuilder.java index df09891cf934..a37b0aeb83ba 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/GenesisStateBuilder.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/GenesisStateBuilder.java @@ -73,7 +73,7 @@ public static ReservedSignedState buildGenesisState( @NonNull final SoftwareVersion appVersion, @NonNull final MerkleRoot stateRoot) { - initGenesisPlatformState(platformContext, stateRoot.getPlatformState(), addressBook, appVersion); + initGenesisPlatformState(platformContext, stateRoot.getWritablePlatformState(), addressBook, appVersion); final SignedState signedState = new SignedState( platformContext, CryptoStatic::verifySignature, stateRoot, "genesis state", false, false, false); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/MerkleRoot.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/MerkleRoot.java index 82812057e5c2..74244ab4323a 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/MerkleRoot.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/MerkleRoot.java @@ -33,12 +33,23 @@ public interface MerkleRoot extends MerkleInternal { SwirldState getSwirldState(); /** - * Get the platform state. + * Get readable platform state. + * Works on both - mutable and immutable {@link MerkleRoot} and, therefore, this method should be preferred. * - * @return the platform state + * @return immutable platform state */ @NonNull - PlatformStateAccessor getPlatformState(); + PlatformStateAccessor getReadablePlatformState(); + + /** + * Get writable platform state. Works only on mutable {@link MerkleRoot}. + * Call this method only if you need to modify the platform state. + * + * @return mutable platform state + */ + @NonNull + PlatformStateAccessor getWritablePlatformState(); + /** * Set the platform state. * diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/MerkleStateRoot.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/MerkleStateRoot.java index 37d84a0d0f19..039aa89c5b82 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/MerkleStateRoot.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/MerkleStateRoot.java @@ -1005,8 +1005,20 @@ public SwirldState getSwirldState() { */ @NonNull @Override - public PlatformStateAccessor getPlatformState() { - return !isImmutable() ? writablePlatformStateStore() : readablePlatformStateStore(); + public PlatformStateAccessor getReadablePlatformState() { + return readablePlatformStateStore(); + } + + /** + * {@inheritDoc} + */ + @NonNull + @Override + public PlatformStateAccessor getWritablePlatformState() { + if (isImmutable()) { + throw new IllegalStateException("Cannot get writable platform state when state is immutable"); + } + return writablePlatformStateStore(); } /** @@ -1025,7 +1037,7 @@ public void updatePlatformState(@NonNull final PlatformStateAccessor accessor) { @NonNull @Override public String getInfoString(final int hashDepth) { - return createInfoString(hashDepth, getPlatformState(), getHash(), this); + return createInfoString(hashDepth, readablePlatformStateStore(), getHash(), this); } private ReadablePlatformStateStore readablePlatformStateStore() { diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/State.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/State.java index d49e3f0af1b1..d7b589ec798d 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/State.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/State.java @@ -84,8 +84,8 @@ private State(final State that) { if (that.getSwirldState() != null) { this.setSwirldState(that.getSwirldState().copy()); } - if (that.getPlatformState() != null) { - this.updatePlatformState(that.getPlatformState().copy()); + if (that.getWritablePlatformState() != null) { + this.updatePlatformState(that.getWritablePlatformState().copy()); } } @@ -101,7 +101,7 @@ public MerkleNode migrate(final int version) { if (version < ClassVersion.MIGRATE_PLATFORM_STATE && getSwirldState() instanceof MerkleStateRoot merkleStateRoot) { - PlatformState platformState = getPlatformState().copy(); + PlatformState platformState = getWritablePlatformState().copy(); setChild(ChildIndices.PLATFORM_STATE, null); merkleStateRoot.updatePlatformState(platformState); merkleStateRoot.setRoute(MerkleRouteFactory.getEmptyRoute()); @@ -139,14 +139,22 @@ public void setSwirldState(final SwirldState state) { setChild(ChildIndices.SWIRLD_STATE, state); } + /** + * Immutable platform state is not supported by this class. + */ + @NonNull + @Override + public PlatformStateAccessor getReadablePlatformState() { + return getChild(ChildIndices.PLATFORM_STATE); + } + /** * Get the platform state. - * * @return the platform state */ @NonNull @Override - public PlatformState getPlatformState() { + public PlatformState getWritablePlatformState() { return getChild(ChildIndices.PLATFORM_STATE); } @@ -214,7 +222,7 @@ public boolean equals(final Object other) { return false; } final MerkleRoot state = (MerkleRoot) other; - return Objects.equals(getPlatformState(), state.getPlatformState()) + return Objects.equals(getReadablePlatformState(), state.getReadablePlatformState()) && Objects.equals(getSwirldState(), state.getSwirldState()); } @@ -223,7 +231,7 @@ public boolean equals(final Object other) { */ @Override public int hashCode() { - return Objects.hash(getPlatformState(), getSwirldState()); + return Objects.hash(getReadablePlatformState(), getSwirldState()); } /** @@ -234,7 +242,7 @@ public int hashCode() { @NonNull @Override public String getInfoString(final int hashDepth) { - final PlatformStateAccessor platformState = getPlatformState(); + final PlatformStateAccessor platformState = getReadablePlatformState(); return createInfoString(hashDepth, platformState, getHash(), this); } @@ -244,7 +252,7 @@ public String getInfoString(final int hashDepth) { @Override public String toString() { return new ToStringBuilder(this) - .append("platformState", getPlatformState()) + .append("platformState", getReadablePlatformState()) .append("swirldState", getSwirldState()) .toString(); } 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 cd9df967bd00..e0fad3358917 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 @@ -125,7 +125,7 @@ public void setInitialState(@NonNull final MerkleRoot state) { public void handleConsensusRound(final ConsensusRound round) { final MerkleRoot state = stateRef.get(); - uptimeTracker.handleRound(round, state.getPlatformState().getAddressBook()); + uptimeTracker.handleRound(round, state.getReadablePlatformState().getAddressBook()); transactionHandler.handleRound(round, state); } @@ -157,8 +157,8 @@ public MerkleRoot getConsensusState() { public void savedStateInFreezePeriod() { // set current DualState's lastFrozenTime to be current freezeTime stateRef.get() - .getPlatformState() - .setLastFrozenTime(stateRef.get().getPlatformState().getFreezeTime()); + .getWritablePlatformState() + .setLastFrozenTime(stateRef.get().getReadablePlatformState().getFreezeTime()); } /** @@ -213,7 +213,7 @@ private void setLatestImmutableState(final MerkleRoot immutableState) { */ @Override public boolean isInFreezePeriod(final Instant timestamp) { - final PlatformStateAccessor platformState = getConsensusState().getPlatformState(); + final PlatformStateAccessor platformState = getConsensusState().getReadablePlatformState(); return SwirldStateManagerUtils.isInFreezePeriod( timestamp, platformState.getFreezeTime(), platformState.getLastFrozenTime()); } 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 f872f1885156..bb42bcd48af6 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 @@ -52,7 +52,7 @@ public static MerkleRoot fastCopy( // Create a fast copy final MerkleRoot copy = state.copy(); - final var platformState = copy.getPlatformState(); + final var platformState = copy.getWritablePlatformState(); platformState.setCreationSoftwareVersion(softwareVersion); // Increment the reference count because this reference becomes the new value 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 24063ee819f9..7075b59c8ede 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 @@ -56,7 +56,7 @@ public void handleRound(final ConsensusRound round, final MerkleRoot state) { final Instant timeOfHandle = Instant.now(); final long startTime = System.nanoTime(); - state.getSwirldState().handleConsensusRound(round, state.getPlatformState()); + state.getSwirldState().handleConsensusRound(round, state.getWritablePlatformState()); final double secondsElapsed = (System.nanoTime() - startTime) * NANOSECONDS_TO_SECONDS; 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 f5b4fcb82c38..41b4643e7c80 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 @@ -130,7 +130,8 @@ public AddressBookInitializer( platformContext.getConfiguration().getConfigData(AddressBookConfig.class); this.initialState = Objects.requireNonNull(initialState, "The initialState must not be null."); - this.stateAddressBook = initialState.getState().getPlatformState().getAddressBook(); + this.stateAddressBook = + initialState.getState().getReadablePlatformState().getAddressBook(); if (stateAddressBook == null && !initialState.isGenesisState()) { throw new IllegalStateException("Only genesis states can have null address books."); } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/PlatformStateService.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/PlatformStateService.java index bee02252192e..765f1f15828f 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/PlatformStateService.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/PlatformStateService.java @@ -21,7 +21,6 @@ import com.hedera.hapi.node.base.SemanticVersion; import com.hedera.hapi.platform.state.PlatformState; -import com.swirlds.platform.state.MerkleRoot; import com.swirlds.platform.state.MerkleStateRoot; import com.swirlds.platform.state.service.schemas.V0540PlatformStateSchema; import com.swirlds.state.merkle.singleton.SingletonNode; @@ -33,8 +32,8 @@ import java.util.List; /** - * A service that provides the schema for the platform state, used by {@link MerkleStateRoot} to implement - * {@link MerkleRoot#getPlatformState()}. + * A service that provides the schema for the platform state, used by {@link MerkleStateRoot} + * to implement accessors to the platform state. */ public enum PlatformStateService implements Service { PLATFORM_STATE_SERVICE; 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 ea956efd3a11..cc859ccb3260 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 @@ -213,7 +213,7 @@ public SignedState( */ @Override public long getRound() { - return state.getPlatformState().getRound(); + return state.getReadablePlatformState().getRound(); } /** @@ -222,7 +222,7 @@ public long getRound() { * @return true if this is the genesis state */ public boolean isGenesisState() { - return state.getPlatformState().getRound() == GENESIS_ROUND; + return state.getReadablePlatformState().getRound() == GENESIS_ROUND; } /** @@ -241,6 +241,8 @@ public boolean isGenesisState() { public void setSigSet(@NonNull final SigSet sigSet) { this.sigSet = Objects.requireNonNull(sigSet); signingWeight = 0; + // init + state.getWritablePlatformState(); if (!isGenesisState()) { // Only non-genesis states will have signing weight final AddressBook addressBook = getAddressBook(); @@ -258,7 +260,7 @@ public void setSigSet(@NonNull final SigSet sigSet) { @Override public @NonNull AddressBook getAddressBook() { return Objects.requireNonNull( - getState().getPlatformState().getAddressBook(), + getState().getReadablePlatformState().getAddressBook(), "address book stored in this signed state is null, this should never happen"); } @@ -455,7 +457,7 @@ public String toString() { * @return the consensus timestamp for this signed state. */ public @NonNull Instant getConsensusTimestamp() { - return state.getPlatformState().getConsensusTimestamp(); + return state.getReadablePlatformState().getConsensusTimestamp(); } /** 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 8d27c8cd4983..60ad8d4c47f4 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 @@ -273,7 +273,7 @@ private static ReservedSignedState loadStateFile( final Hash oldHash = deserializedSignedState.originalHash(); final Hash newHash = rehashTree(state); - final SoftwareVersion loadedVersion = state.getPlatformState().getCreationSoftwareVersion(); + final SoftwareVersion loadedVersion = state.getReadablePlatformState().getCreationSoftwareVersion(); if (oldHash.equals(newHash)) { logger.info(STARTUP.getMarker(), "Loaded state's hash is the same as when it was saved."); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/SavedStateMetadata.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/SavedStateMetadata.java index ba6f69d7dde5..ddc2560fe64c 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/SavedStateMetadata.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/SavedStateMetadata.java @@ -167,7 +167,7 @@ public static SavedStateMetadata create( Objects.requireNonNull(signedState.getState().getHash(), "state must be hashed"); Objects.requireNonNull(now, "now must not be null"); - final PlatformStateAccessor platformState = signedState.getState().getPlatformState(); + final PlatformStateAccessor platformState = signedState.getState().getReadablePlatformState(); final List signingNodes = signedState.getSigSet().getSigningNodes(); Collections.sort(signingNodes); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/SignedStateFileWriter.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/SignedStateFileWriter.java index 5f56891a5a8b..b79c5087a73a 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/SignedStateFileWriter.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/SignedStateFileWriter.java @@ -169,7 +169,7 @@ public static void writeSignedStateFilesToDirectory( platformContext, selfId, directory, - signedState.getState().getPlatformState().getAncientThreshold(), + signedState.getState().getReadablePlatformState().getAncientThreshold(), signedState.getRound()); } } 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 e0d9147f39fc..4cf817fecc50 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 @@ -16,12 +16,20 @@ package com.swirlds.platform.system.address; +import static com.swirlds.platform.util.BootstrapUtils.detectSoftwareUpgrade; + import com.hedera.hapi.node.base.ServiceEndpoint; 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.common.context.PlatformContext; import com.swirlds.common.formatting.TextTable; import com.swirlds.common.platform.NodeId; +import com.swirlds.platform.state.MerkleRoot; +import com.swirlds.platform.state.PlatformStateAccessor; +import com.swirlds.platform.state.address.AddressBookInitializer; +import com.swirlds.platform.state.signed.ReservedSignedState; +import com.swirlds.platform.system.SoftwareVersion; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.net.InetAddress; @@ -372,4 +380,49 @@ private static RosterEntry toRosterEntry(@NonNull final Address address, @NonNul .gossipEndpoint(serviceEndpoints) .build(); } + /** + * Initializes the address book from the configuration and platform saved state. + * @param selfId the node ID of the current node + * @param version the software version of the current node + * @param initialState the initial state of the platform + * @param bootstrapAddressBook the bootstrap address book + * @param platformContext the platform context + * @return the initialized address book + */ + public static @NonNull AddressBook initializeAddressBook( + @NonNull final NodeId selfId, + @NonNull final SoftwareVersion version, + @NonNull final ReservedSignedState initialState, + @NonNull final AddressBook bootstrapAddressBook, + @NonNull final PlatformContext platformContext) { + 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); + + if (addressBookInitializer.hasAddressBookChanged()) { + final MerkleRoot state = initialState.get().getState(); + // Update the address book with the current address book read from config.txt. + // Eventually we will not do this, and only transactions will be capable of + // modifying the address book. + final PlatformStateAccessor platformState = state.getWritablePlatformState(); + platformState.bulkUpdate(v -> { + v.setAddressBook(addressBookInitializer.getCurrentAddressBook().copy()); + v.setPreviousAddressBook( + addressBookInitializer.getPreviousAddressBook() == null + ? null + : addressBookInitializer + .getPreviousAddressBook() + .copy()); + }); + } + + // At this point the initial state must have the current address book set. If not, something is wrong. + final AddressBook addressBook = + initialState.get().getState().getReadablePlatformState().getAddressBook(); + if (addressBook == null) { + throw new IllegalStateException("The current address book of the initial state is null."); + } + return addressBook; + } } 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 78ba2f901de1..73a32aedefcf 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 @@ -45,6 +45,7 @@ import com.swirlds.platform.health.entropy.OSEntropyChecker; import com.swirlds.platform.health.filesystem.OSFileSystemChecker; import com.swirlds.platform.network.Network; +import com.swirlds.platform.state.MerkleRoot; import com.swirlds.platform.state.address.AddressBookNetworkUtils; import com.swirlds.platform.state.signed.SignedState; import com.swirlds.platform.swirldapp.AppLoaderException; @@ -192,9 +193,13 @@ public static boolean detectSoftwareUpgrade( @NonNull final SoftwareVersion appVersion, @Nullable final SignedState loadedSignedState) { Objects.requireNonNull(appVersion, "The app version must not be null."); - final SoftwareVersion loadedSoftwareVersion = loadedSignedState == null - ? null - : loadedSignedState.getState().getPlatformState().getCreationSoftwareVersion(); + final SoftwareVersion loadedSoftwareVersion; + if (loadedSignedState == null) { + loadedSoftwareVersion = null; + } else { + MerkleRoot state = loadedSignedState.getState(); + loadedSoftwareVersion = state.getReadablePlatformState().getCreationSoftwareVersion(); + } final int versionComparison = loadedSoftwareVersion == null ? 1 : appVersion.compareTo(loadedSoftwareVersion); final boolean softwareUpgrade; if (versionComparison < 0) { 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 f2f7f8777331..d8738b47f8d9 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 @@ -75,7 +75,8 @@ com.swirlds.platform.test, com.hedera.node.test.clients, com.swirlds.platform.core.test.fixtures, - com.hedera.node.app.test.fixtures; + com.hedera.node.app.test.fixtures, + com.hedera.node.app; exports com.swirlds.platform.event.linking to com.swirlds.common, com.swirlds.platform.test, @@ -123,6 +124,8 @@ exports com.swirlds.platform.state.snapshot; exports com.swirlds.platform.state.service.schemas; exports com.swirlds.platform.state.service; + exports com.swirlds.platform.builder.internal; + exports com.swirlds.platform.config.internal; requires transitive com.hedera.node.hapi; requires transitive com.swirlds.base; 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 ae95551d83df..5db9809cfc6d 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 @@ -379,7 +379,7 @@ private SignedState getMockSignedState( when(platformState.getAddressBook()).thenReturn(currentAddressBook); when(platformState.getPreviousAddressBook()).thenReturn(previousAddressBook); final MerkleRoot state = mock(MerkleRoot.class); - when(state.getPlatformState()).thenReturn(platformState); + when(state.getReadablePlatformState()).thenReturn(platformState); when(signedState.getState()).thenReturn(state); when(signedState.isGenesisState()).thenReturn(fromGenesis); when(signedState.getAddressBook()).thenReturn(currentAddressBook); diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/SavedStateMetadataTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/SavedStateMetadataTests.java index 44af71144e78..6e2cae0a7713 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/SavedStateMetadataTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/SavedStateMetadataTests.java @@ -213,7 +213,7 @@ void signingNodesSortedTest() { final AddressBook addressBook = mock(AddressBook.class); when(signedState.getState()).thenReturn(state); - when(state.getPlatformState()).thenReturn(platformState); + when(state.getReadablePlatformState()).thenReturn(platformState); when(platformState.getAddressBook()).thenReturn(addressBook); when(signedState.getSigSet()).thenReturn(sigSet); when(sigSet.getSigningNodes()) diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/consensus/RoundCalculationUtilsTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/consensus/RoundCalculationUtilsTest.java index 439c8c1b5ad7..9786788ee916 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/consensus/RoundCalculationUtilsTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/consensus/RoundCalculationUtilsTest.java @@ -72,7 +72,7 @@ void getMinGenNonAncientFromSignedState() { final MerkleRoot state = mock(MerkleRoot.class); final PlatformStateAccessor platformState = mock(PlatformStateAccessor.class); when(signedState.getState()).thenReturn(state); - when(state.getPlatformState()).thenReturn(platformState); + when(state.getReadablePlatformState()).thenReturn(platformState); final AtomicLong lastRoundDecided = new AtomicLong(); when(signedState.getRound()).thenAnswer(a -> lastRoundDecided.get()); 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 0f4c0ebe31b6..d14a1e74ec91 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 @@ -63,7 +63,8 @@ public TransactionHandlerTester(final AddressBook addressBook) { final SwirldState swirldState = mock(SwirldState.class); when(consensusState.getSwirldState()).thenReturn(swirldState); when(consensusState.copy()).thenReturn(consensusState); - when(consensusState.getPlatformState()).thenReturn(platformState); + when(consensusState.getReadablePlatformState()).thenReturn(platformState); + when(consensusState.getWritablePlatformState()).thenReturn(platformState); doAnswer(i -> { handledRounds.add(i.getArgument(0)); return null; diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/network/NetworkPeerIdentifierTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/network/NetworkPeerIdentifierTest.java index f3011e2f39fe..575ed2a0888c 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/network/NetworkPeerIdentifierTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/network/NetworkPeerIdentifierTest.java @@ -47,13 +47,16 @@ import java.util.HashSet; import java.util.List; import java.util.Objects; -import java.util.Random; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class NetworkPeerIdentifierTest { + private static final Pattern NODE_NAME_PATTERN = Pattern.compile("^node(\\d+)$"); + final PlatformContext platformContext = mock(PlatformContext.class); List peerInfoList = null; PublicStores publicStores = null; @@ -74,15 +77,17 @@ void setUp() throws URISyntaxException, KeyLoadingException, KeyStoreException { publicStores = PublicStores.fromAllPublic(publicKeys, names.stream().toList()); peerInfoList = new ArrayList<>(); - final Random random = new Random(); names.forEach(name -> { - final int id = random.nextInt(names.size()); + final Matcher nameMatcher = NODE_NAME_PATTERN.matcher(name); + if (!nameMatcher.matches()) { + throw new RuntimeException("Invalid node name " + name); + } + final int id = Integer.parseInt(nameMatcher.group(1)) - 1; final NodeId node = new NodeId(id); final PeerInfo peer; try { peer = new PeerInfo( node, - name, "127.0.0.1", Objects.requireNonNull(publicStores.getCertificate(KeyCertPurpose.SIGNING, name))); } catch (final KeyLoadingException e) { diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/reconnect/DefaultSignedStateValidatorTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/reconnect/DefaultSignedStateValidatorTests.java index 9fd083d6a271..e0fd1ded3c6b 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/reconnect/DefaultSignedStateValidatorTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/reconnect/DefaultSignedStateValidatorTests.java @@ -258,7 +258,7 @@ void testSignedStateValidationRandom(final String desc, final List nodes, final SignedState signedState = stateSignedByNodes(signingNodes); final SignedStateValidationData originalData = - new SignedStateValidationData(signedState.getState().getPlatformState(), addressBook); + new SignedStateValidationData(signedState.getState().getReadablePlatformState(), addressBook); final boolean shouldSucceed = stateHasEnoughWeight(nodes, signingNodes); if (shouldSucceed) { diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/BirthRoundStateMigrationTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/BirthRoundStateMigrationTests.java index 7ee81b537b4c..d7e993850fb8 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/BirthRoundStateMigrationTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/BirthRoundStateMigrationTests.java @@ -84,7 +84,7 @@ void generationModeTest() { final Hash originalHash = signedState.getState().getHash(); final SoftwareVersion previousSoftwareVersion = - signedState.getState().getPlatformState().getCreationSoftwareVersion(); + signedState.getState().getWritablePlatformState().getCreationSoftwareVersion(); final SoftwareVersion newSoftwareVersion = createNextVersion(previousSoftwareVersion); @@ -108,14 +108,17 @@ void alreadyMigratedTest() { final SignedState signedState = generateSignedState(random, platformContext); final SoftwareVersion previousSoftwareVersion = - signedState.getState().getPlatformState().getCreationSoftwareVersion(); + signedState.getState().getReadablePlatformState().getCreationSoftwareVersion(); ; final SoftwareVersion newSoftwareVersion = createNextVersion(previousSoftwareVersion); - signedState.getState().getPlatformState().setLastRoundBeforeBirthRoundMode(signedState.getRound() - 100); - signedState.getState().getPlatformState().setFirstVersionInBirthRoundMode(previousSoftwareVersion); - signedState.getState().getPlatformState().setLowestJudgeGenerationBeforeBirthRoundMode(100); + signedState + .getState() + .getWritablePlatformState() + .setLastRoundBeforeBirthRoundMode(signedState.getRound() - 100); + signedState.getState().getWritablePlatformState().setFirstVersionInBirthRoundMode(previousSoftwareVersion); + signedState.getState().getWritablePlatformState().setLowestJudgeGenerationBeforeBirthRoundMode(100); rehashTree(signedState.getState()); final Hash originalHash = signedState.getState().getHash(); @@ -145,13 +148,13 @@ void migrationTest() { final Hash originalHash = signedState.getState().getHash(); final SoftwareVersion previousSoftwareVersion = - signedState.getState().getPlatformState().getCreationSoftwareVersion(); + signedState.getState().getReadablePlatformState().getCreationSoftwareVersion(); final SoftwareVersion newSoftwareVersion = createNextVersion(previousSoftwareVersion); final long lastRoundMinimumJudgeGeneration = signedState .getState() - .getPlatformState() + .getReadablePlatformState() .getSnapshot() .getMinimumJudgeInfoList() .getLast() @@ -167,19 +170,19 @@ void migrationTest() { newSoftwareVersion.getPbjSemanticVersion(), signedState .getState() - .getPlatformState() + .getReadablePlatformState() .getFirstVersionInBirthRoundMode() .getPbjSemanticVersion()); assertEquals( lastRoundMinimumJudgeGeneration, - signedState.getState().getPlatformState().getLowestJudgeGenerationBeforeBirthRoundMode()); + signedState.getState().getReadablePlatformState().getLowestJudgeGenerationBeforeBirthRoundMode()); assertEquals( signedState.getRound(), - signedState.getState().getPlatformState().getLastRoundBeforeBirthRoundMode()); + signedState.getState().getReadablePlatformState().getLastRoundBeforeBirthRoundMode()); // All of the judge info objects should now be using a birth round equal to the round of the state for (final MinimumJudgeInfo minimumJudgeInfo : - signedState.getState().getPlatformState().getSnapshot().getMinimumJudgeInfoList()) { + signedState.getState().getReadablePlatformState().getSnapshot().getMinimumJudgeInfoList()) { assertEquals(signedState.getRound(), minimumJudgeInfo.minimumJudgeAncientThreshold()); } } diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/MerkleStateRootTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/MerkleStateRootTest.java index 71af42111a33..f8e2cd6c5a9a 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/MerkleStateRootTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/MerkleStateRootTest.java @@ -53,6 +53,7 @@ import com.swirlds.config.api.ConfigurationBuilder; import com.swirlds.merkle.map.MerkleMap; import com.swirlds.platform.state.service.PlatformStateService; +import com.swirlds.platform.state.service.ReadablePlatformStateStore; import com.swirlds.platform.state.service.WritablePlatformStateStore; import com.swirlds.platform.state.service.schemas.V0540PlatformStateSchema; import com.swirlds.platform.system.InitTrigger; @@ -926,13 +927,13 @@ class PlatformStateTests { @Test @DisplayName("Platform state should be registered by default") void platformStateIsRegisteredByDefault() { - assertThat(stateRoot.getPlatformState()).isNotNull(); + assertThat(stateRoot.getWritablePlatformState()).isNotNull(); } @Test @DisplayName("Test access to the platform state") void testAccessToPlatformStateData() { - PlatformStateAccessor randomPlatformState = randomPlatformState(stateRoot.getPlatformState()); + PlatformStateAccessor randomPlatformState = randomPlatformState(stateRoot.getWritablePlatformState()); stateRoot.updatePlatformState(randomPlatformState); ReadableSingletonState readableSingletonState = stateRoot .getReadableStates(PlatformStateService.NAME) @@ -948,16 +949,16 @@ void testAccessToPlatformStateData() { @Test @DisplayName("Test update of the platform state") void testUpdatePlatformStateData() { - PlatformStateAccessor randomPlatformState = randomPlatformState(stateRoot.getPlatformState()); + PlatformStateAccessor randomPlatformState = randomPlatformState(stateRoot.getWritablePlatformState()); stateRoot.updatePlatformState(randomPlatformState); WritableStates writableStates = stateRoot.getWritableStates(PlatformStateService.NAME); WritableSingletonState writableSingletonState = writableStates.getSingleton(V0540PlatformStateSchema.PLATFORM_STATE_KEY); - PlatformStateAccessor newPlatformState = randomPlatformState(stateRoot.getPlatformState()); + PlatformStateAccessor newPlatformState = randomPlatformState(stateRoot.getWritablePlatformState()); writableSingletonState.put(toPbjPlatformState(newPlatformState)); ((CommittableWritableStates) writableStates).commit(); - PlatformStateAccessor stateAccessor = stateRoot.getPlatformState(); + PlatformStateAccessor stateAccessor = stateRoot.getReadablePlatformState(); assertThat(stateAccessor.getAddressBook()).isEqualTo(newPlatformState.getAddressBook()); assertThat(stateAccessor.getRound()) .isEqualTo(newPlatformState.getSnapshot().round()); @@ -1020,8 +1021,10 @@ void migrate_platform_state_zeroth_child() { assertFalse(stateRoot.isImmutable()); // MerkleStateRoot registers the platform state as a singleton upon the first request to it - assertInstanceOf(WritablePlatformStateStore.class, stateRoot.getPlatformState()); - assertEquals(toPbjPlatformState(platformState), toPbjPlatformState(stateRoot.getPlatformState())); + assertInstanceOf(WritablePlatformStateStore.class, stateRoot.getWritablePlatformState()); + assertInstanceOf(ReadablePlatformStateStore.class, stateRoot.getReadablePlatformState()); + assertEquals(toPbjPlatformState(platformState), toPbjPlatformState(stateRoot.getWritablePlatformState())); + assertEquals(toPbjPlatformState(platformState), toPbjPlatformState(stateRoot.getReadablePlatformState())); } @Test @@ -1059,8 +1062,8 @@ void migrate_platform_state_last_child() { assertFalse(stateRoot.isImmutable()); // MerkleStateRoot registers the platform state as a singleton upon the first request to it - assertInstanceOf(WritablePlatformStateStore.class, stateRoot.getPlatformState()); - assertEquals(toPbjPlatformState(platformState), toPbjPlatformState(stateRoot.getPlatformState())); + assertInstanceOf(WritablePlatformStateStore.class, stateRoot.getWritablePlatformState()); + assertEquals(toPbjPlatformState(platformState), toPbjPlatformState(stateRoot.getWritablePlatformState())); } @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 1811d7686101..4db93d4e6f51 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 @@ -66,7 +66,7 @@ private MerkleStateRoot buildMockState(final Runnable reserveCallback, final Run final PlatformStateAccessor platformState = new PlatformState(); platformState.setAddressBook(mock(AddressBook.class)); - when(state.getPlatformState()).thenReturn(platformState); + when(state.getWritablePlatformState()).thenReturn(platformState); if (reserveCallback != null) { doAnswer(invocation -> { reserveCallback.run(); @@ -209,7 +209,9 @@ void alternateConstructorReservationsTest() { final MerkleRoot state = spy(new MerkleStateRoot( FAKE_MERKLE_STATE_LIFECYCLES, version -> new BasicSoftwareVersion(version.major()))); final PlatformStateAccessor platformState = mock(PlatformStateAccessor.class); - when(state.getPlatformState()).thenReturn(platformState); + // init state first + state.getWritablePlatformState(); + when(state.getReadablePlatformState()).thenReturn(platformState); when(platformState.getRound()).thenReturn(0L); final SignedState signedState = new SignedState( TestPlatformContextBuilder.create().build(), 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 ba038d34694f..666e81660311 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 @@ -108,7 +108,7 @@ void activeStateCountTest() throws IOException { // Deserialize a state final MerkleStateRoot stateToSerialize = new MerkleStateRoot(FAKE_MERKLE_STATE_LIFECYCLES, softwareVersionSupplier); - final var platformState = stateToSerialize.getPlatformState(); + final var platformState = stateToSerialize.getWritablePlatformState(); platformState.bulkUpdate(v -> { v.setCreationSoftwareVersion(new BasicSoftwareVersion(version.minor())); v.setLegacyRunningEventHash(new Hash()); diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/StateSigningTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/StateSigningTests.java index 4ff4500f69f8..0b16a114f10a 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/StateSigningTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/StateSigningTests.java @@ -318,7 +318,7 @@ void signatureBecomesInvalidTest(final boolean evenWeighting) { final NodeId nodeRemovedFromAddressBook = nodes.get(0).getNodeId(); final long weightRemovedFromAddressBook = nodes.get(0).getWeight(); final AddressBook updatedAddressBook = signedState.getAddressBook().remove(nodeRemovedFromAddressBook); - signedState.getState().getPlatformState().setAddressBook(updatedAddressBook); + signedState.getState().getWritablePlatformState().setAddressBook(updatedAddressBook); // Tamper with a node's signature final long weightWithModifiedSignature = nodes.get(1).getWeight(); 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 c4cfd203bb89..cc42a4cac511 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 @@ -157,7 +157,7 @@ private ReservedSignedState createSignedState(final long round) { when(platformState.getRound()).thenReturn(round); when(platformState.getAddressBook()).thenReturn(addressBook); - when(state.getPlatformState()).thenReturn(platformState); + when(state.getReadablePlatformState()).thenReturn(platformState); when(state.getRoute()).thenReturn(merkleNode.getRoute()); when(state.getHash()).thenReturn(merkleNode.getHash()); 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 ae84e1ba974b..32681c30e042 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 @@ -16,6 +16,7 @@ package com.swirlds.platform.turtle.runner; +import static com.swirlds.platform.state.signed.StartupStateUtils.getInitialState; import static com.swirlds.platform.system.address.AddressBookUtils.createRoster; import com.swirlds.base.time.Time; @@ -32,6 +33,7 @@ import com.swirlds.platform.config.BasicConfig_; import com.swirlds.platform.crypto.KeysAndCerts; import com.swirlds.platform.eventhandling.EventConfig_; +import com.swirlds.platform.state.MerkleRoot; import com.swirlds.platform.state.snapshot.SignedStateFileUtils; import com.swirlds.platform.system.BasicSoftwareVersion; import com.swirlds.platform.system.Platform; @@ -41,6 +43,7 @@ import com.swirlds.platform.util.RandomBuilder; import com.swirlds.platform.wiring.PlatformSchedulersConfig_; import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.function.Supplier; /** * Encapsulates a single node running in a TURTLE network. @@ -94,25 +97,26 @@ public class TurtleNode { model = WiringModelBuilder.create(platformContext) .withDeterministicModeEnabled(true) .build(); - + final Supplier genesisStateSupplier = TurtleTestingToolState::getStateRootNode; + final var version = new BasicSoftwareVersion(1); + final var initialState = getInitialState( + platformContext, + version, + genesisStateSupplier, + SignedStateFileUtils::readState, + "foo", + "bar", + nodeId, + addressBook); final PlatformBuilder platformBuilder = PlatformBuilder.create( - "foo", - "bar", - new BasicSoftwareVersion(1), - TurtleTestingToolState::getStateRootNode, - SignedStateFileUtils::readState, - nodeId) + "foo", "bar", new BasicSoftwareVersion(1), initialState, nodeId) .withModel(model) - .withCryptography(platformContext.getCryptography()) - .withMetrics(platformContext.getMetrics()) - .withTime(platformContext.getTime()) - .withFileSystemManager(platformContext.getFileSystemManager()) - .withRecycleBin(platformContext.getRecycleBin()) - .withExecutorFactory(platformContext.getExecutorFactory()) .withRandomBuilder(new RandomBuilder(randotron.nextLong())) - .withBootstrapAddressBook(addressBook) + .withAddressBook(addressBook) .withRoster(createRoster(addressBook)) - .withKeysAndCerts(privateKeys); + .withKeysAndCerts(privateKeys) + .withPlatformContext(platformContext) + .withConfiguration(configuration); final PlatformComponentBuilder platformComponentBuilder = platformBuilder.buildComponentBuilder(); diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/util/AddressBookNetworkUtilsTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/util/AddressBookNetworkUtilsTests.java index 29a5b4d8a346..89c4fa4f7ce9 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/util/AddressBookNetworkUtilsTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/util/AddressBookNetworkUtilsTests.java @@ -32,6 +32,7 @@ import com.swirlds.platform.builder.PlatformBuilder; import com.swirlds.platform.network.Network; import com.swirlds.platform.state.address.AddressBookNetworkUtils; +import com.swirlds.platform.state.signed.ReservedSignedState; import com.swirlds.platform.system.BasicSoftwareVersion; import com.swirlds.platform.system.address.Address; import com.swirlds.platform.system.address.AddressBook; @@ -97,8 +98,7 @@ void testCreateRosterFromNonEmptyAddressBook() { "name", "swirldName", new BasicSoftwareVersion(1), - () -> null, - (inputStream, path) -> null, + ReservedSignedState.createNullReservation(), new NodeId(0)); final Address address1 = new Address(new NodeId(1), "", "", 10, null, 77, null, 88, null, null, ""); @@ -106,7 +106,7 @@ void testCreateRosterFromNonEmptyAddressBook() { final AddressBook addressBook = new AddressBook(); addressBook.add(address1); addressBook.add(address2); - platformBuilder.withBootstrapAddressBook(addressBook); + platformBuilder.withAddressBook(addressBook); final Roster roster = AddressBookUtils.createRoster(addressBook); assertNotNull(roster); @@ -129,11 +129,10 @@ void testCreateRosterFromEmptyAddressBook() { "name", "swirldName", new BasicSoftwareVersion(1), - () -> null, - (inputStream, path) -> null, + ReservedSignedState.createNullReservation(), new NodeId(0)); final AddressBook addressBook = new AddressBook(); - platformBuilder.withBootstrapAddressBook(addressBook); + platformBuilder.withAddressBook(addressBook); final Roster roster = AddressBookUtils.createRoster(addressBook); assertNotNull(roster); 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/BlockingSwirldState.java index 7a3ddc4056ec..20ccc2648240 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/BlockingSwirldState.java @@ -102,8 +102,8 @@ public boolean equals(final Object obj) { return false; } return Objects.equals( - this.getPlatformState().getAddressBook(), - that.getPlatformState().getAddressBook()); + this.getReadablePlatformState().getAddressBook(), + that.getReadablePlatformState().getAddressBook()); } /** 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 7d257c7e77e2..1d6b0487ef31 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 @@ -186,7 +186,7 @@ public SignedState build() { consensusSnapshotInstance = consensusSnapshot; } - final PlatformStateAccessor platformState = stateInstance.getPlatformState(); + final PlatformStateAccessor platformState = stateInstance.getWritablePlatformState(); platformState.bulkUpdate(v -> { v.setSnapshot(consensusSnapshotInstance); diff --git a/platform-sdk/swirlds-state-api/src/main/java/com/swirlds/state/merkle/StateUtils.java b/platform-sdk/swirlds-state-api/src/main/java/com/swirlds/state/merkle/StateUtils.java index 8b81f88ba05f..c27999baa925 100644 --- a/platform-sdk/swirlds-state-api/src/main/java/com/swirlds/state/merkle/StateUtils.java +++ b/platform-sdk/swirlds-state-api/src/main/java/com/swirlds/state/merkle/StateUtils.java @@ -30,14 +30,10 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.Objects; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; /** Utility class for working with states. */ public final class StateUtils { - private static final Logger logger = LogManager.getLogger(); - /** Prevent instantiation */ private StateUtils() {} 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 5ba41ddb5aac..cc6cd99e73b0 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 @@ -36,7 +36,7 @@ public static SignedState randomSignedState(long seed) { public static SignedState randomSignedState(Random random) { MerkleStateRoot root = new MerkleStateRoot(FAKE_MERKLE_STATE_LIFECYCLES, version -> new BasicSoftwareVersion(version.minor())); - randomPlatformState(random, root.getPlatformState()); + randomPlatformState(random, root.getWritablePlatformState()); boolean shouldSaveToDisk = random.nextBoolean(); SignedState signedState = new SignedState( TestPlatformContextBuilder.create().build(), diff --git a/settings.gradle.kts b/settings.gradle.kts index 1af074a0ae1f..015ca3a49c55 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -86,15 +86,11 @@ javaModules { versions("hedera-dependency-versions") } -// The HAPI API version to use for Protobuf sources. -val hapiProtoVersion = "0.54.0" - dependencyResolutionManagement { // Protobuf tool versions versionCatalogs.create("libs") { version("google-proto", "3.25.4") version("grpc-proto", "1.66.0") - version("hapi-proto", hapiProtoVersion) plugin("pbj", "com.hedera.pbj.pbj-compiler").version("0.9.2") }