From df139dd9a0bbdc52f2d0f07a888d4f01ae9ac115 Mon Sep 17 00:00:00 2001 From: Cody Littley <56973212+cody-littley@users.noreply.github.com> Date: Wed, 28 Feb 2024 16:19:20 -0600 Subject: [PATCH 001/115] feat: diagram tweaks (#11801) Signed-off-by: Cody Littley --- .../com/swirlds/platform/wiring/PlatformSchedulers.java | 6 +++--- .../java/com/swirlds/platform/wiring/PlatformWiring.java | 6 +++--- .../components/LatestCompleteStateNotifierWiring.java | 3 +-- .../swirlds/platform/wiring/generate-platform-diagram.sh | 6 ++++-- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformSchedulers.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformSchedulers.java index 292d4c6992fe..cb58578774d2 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformSchedulers.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformSchedulers.java @@ -59,7 +59,7 @@ * @param issDetectorScheduler the scheduler for the iss detector * @param issHandlerScheduler the scheduler for the iss handler * @param hashLoggerScheduler the scheduler for the hash logger - * @param latestCompleteStateScheduler the scheduler for the latest complete state notifier + * @param latestCompleteStateNotificationScheduler the scheduler for the latest complete state notifier */ public record PlatformSchedulers( @NonNull TaskScheduler eventHasherScheduler, @@ -87,7 +87,7 @@ public record PlatformSchedulers( @NonNull TaskScheduler> issDetectorScheduler, @NonNull TaskScheduler issHandlerScheduler, @NonNull TaskScheduler hashLoggerScheduler, - @NonNull TaskScheduler latestCompleteStateScheduler) { + @NonNull TaskScheduler latestCompleteStateNotificationScheduler) { /** * Instantiate the schedulers for the platform, for the given wiring model @@ -270,7 +270,7 @@ public static PlatformSchedulers create( .withMetricsBuilder(model.metricsBuilder().withUnhandledTaskMetricEnabled(true)) .build() .cast(), - model.schedulerBuilder("latestCompleteStateScheduler") + model.schedulerBuilder("latestCompleteStateNotification") .withType(TaskSchedulerType.SEQUENTIAL_THREAD) .withUnhandledTaskCapacity(config.completeStateNotifierUnhandledCapacity()) .withMetricsBuilder(model.metricsBuilder().withUnhandledTaskMetricEnabled(true)) diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformWiring.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformWiring.java index 81dbf3f1f03d..a5672433e098 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformWiring.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformWiring.java @@ -226,7 +226,7 @@ public PlatformWiring(@NonNull final PlatformContext platformContext, @NonNull f hashLoggerWiring = HashLoggerWiring.create(schedulers.hashLoggerScheduler()); latestCompleteStateNotifierWiring = - LatestCompleteStateNotifierWiring.create(schedulers.latestCompleteStateScheduler()); + LatestCompleteStateNotifierWiring.create(schedulers.latestCompleteStateNotificationScheduler()); wire(); } @@ -352,11 +352,11 @@ public void wireExternalComponents( stateSignerWiring .stateSignature() - .solderTo("transactionPool", "state signature transaction", transactionPool::submitSystemTransaction); + .solderTo("transactionPool", "state signature transactions", transactionPool::submitSystemTransaction); stateSignatureCollectorWiring .getCompleteStatesOutput() - .solderTo("latestCompleteStateNexus", "complete state", latestCompleteStateNexus::setStateIfNewer); + .solderTo("latestCompleteStateNexus", "complete states", latestCompleteStateNexus::setStateIfNewer); issDetectorWiring .issNotificationOutput() diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/LatestCompleteStateNotifierWiring.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/LatestCompleteStateNotifierWiring.java index 68f36d283431..5b475a78b237 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/LatestCompleteStateNotifierWiring.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/LatestCompleteStateNotifierWiring.java @@ -37,8 +37,7 @@ public record LatestCompleteStateNotifierWiring( * @return the new wiring instance */ public static LatestCompleteStateNotifierWiring create(@NonNull final TaskScheduler taskScheduler) { - return new LatestCompleteStateNotifierWiring( - taskScheduler.buildInputWire("completed reserved signed state to notify")); + return new LatestCompleteStateNotifierWiring(taskScheduler.buildInputWire("complete states")); } /** diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/generate-platform-diagram.sh b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/generate-platform-diagram.sh index c9405e9be924..58b12b57d7af 100755 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/generate-platform-diagram.sh +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/generate-platform-diagram.sh @@ -3,6 +3,7 @@ pcli diagram \ -l 'applicationTransactionPrehandler:futures:consensusRoundHandler' \ -l 'eventDurabilityNexus:wait for durability:consensusRoundHandler' \ + -l 'eventCreationManager:get transactions:transactionPool' \ -s 'eventWindowManager:non-ancient event window:🌀' \ -s 'heartbeat:heartbeat:❤️' \ -s 'applicationTransactionPrehandler:futures:🔮' \ @@ -11,6 +12,7 @@ pcli diagram \ -s 'inOrderLinker:events to gossip:📬' \ -s 'getKeystoneEventSequenceNumber:flush request:🚽' \ -s 'extractOldestMinimumGenerationOnDisk:minimum identifier to store:📀' \ + -s 'eventCreationManager:non-validated events:🍎' \ -g 'Event Validation:internalEventValidator,eventDeduplicator,eventSignatureValidator' \ -g 'Event Hashing:eventHasher,postHashCollector' \ -g 'Orphan Buffer:orphanBuffer,orphanBufferSplitter' \ @@ -20,13 +22,13 @@ pcli diagram \ -g 'Preconsensus Event Stream:pcesSequencer,pcesWriter,eventDurabilityNexus,🕑' \ -g 'Consensus Event Stream:getEvents,eventStreamManager' \ -g 'Consensus Pipeline:inOrderLinker,Consensus Engine,📬,🌀,🚽' \ - -g 'Event Creation:futureEventBuffer,futureEventBufferSplitter,eventCreationManager' \ + -g 'Event Creation:futureEventBuffer,futureEventBufferSplitter,eventCreationManager,transactionPool,🍎' \ -g 'Gossip:gossip,shadowgraph' \ -g 'Iss Detector:extractSignaturesForIssDetector,issDetector,issNotificationSplitter,issHandler,issNotificationEngine,statusManager_submitCatastrophicFailure' \ -g 'Heartbeat:heartbeat,❤️' \ -g 'PCES Replay:pcesReplayer,✅' \ -g 'Transaction Prehandling:applicationTransactionPrehandler,🔮' \ - -g 'Signature Management:State Signature Collection,stateSigner,Iss Detector' \ + -g 'Signature Management:State Signature Collection,stateSigner,Iss Detector,latestCompleteStateNotification,latestCompleteStateNexus' \ -g 'State Modification:consensusRoundHandler,runningHashUpdate' \ -c 'Consensus Event Stream' \ -c 'Orphan Buffer' \ From de7b0ebeab352a6f558f0fdff6a6b79dfa6fcf7d Mon Sep 17 00:00:00 2001 From: JeffreyDallas <39912573+JeffreyDallas@users.noreply.github.com> Date: Wed, 28 Feb 2024 23:04:50 -0600 Subject: [PATCH 002/115] fix: wait longer for freeze transaction to be handled (#11790) --- .../com/hedera/services/bdd/suites/freeze/SimpleFreezeOnly.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/freeze/SimpleFreezeOnly.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/freeze/SimpleFreezeOnly.java index 9911faf1516d..e3ddc898a2fc 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/freeze/SimpleFreezeOnly.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/freeze/SimpleFreezeOnly.java @@ -59,7 +59,7 @@ public boolean canRunConcurrent() { final HapiSpec simpleFreezeWithTimestamp() { return defaultHapiSpec("SimpleFreezeWithTimeStamp") .given(freezeOnly().payingWith(GENESIS).startingAt(Instant.now().plusSeconds(10))) - .when(sleepFor(11000)) + .when(sleepFor(40000)) .then(cryptoCreate("not_going_to_happen").hasPrecheck(ResponseCodeEnum.PLATFORM_NOT_ACTIVE)); } } From b58eb1a2033e2ee30718744692491c03eab21f70 Mon Sep 17 00:00:00 2001 From: Valentin Tronkov Date: Thu, 29 Feb 2024 07:12:21 +0200 Subject: [PATCH 003/115] feat: Differential testing: Enhance contract bytecode dumper to handle modular representation (#11523) Signed-off-by: Valentin Tronkov <99957253+vtronkov@users.noreply.github.com> --- .../com/hedera/node/app/bbm/StateDumper.java | 17 ++ .../app/bbm/accounts/AccountDumpUtils.java | 2 +- .../app/bbm/contracts/ByteArrayAsKey.java | 39 +++ .../node/app/bbm/contracts/Contract.java | 87 +++++++ .../contracts/ContractBytecodesDumpUtils.java | 231 ++++++++++++++++++ .../node/app/bbm/contracts/ContractUtils.java | 105 ++++++++ .../node/app/bbm/contracts/Contracts.java | 34 +++ .../node/app/bbm/contracts/Validity.java | 22 ++ .../com/hedera/node/app/bbm/files/FileId.java | 2 +- .../node/app/bbm/files/FilesDumpUtils.java | 2 +- .../hedera/node/app/bbm/files/HederaFile.java | 2 +- 11 files changed, 539 insertions(+), 4 deletions(-) create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/contracts/ByteArrayAsKey.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/contracts/Contract.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/contracts/ContractBytecodesDumpUtils.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/contracts/ContractUtils.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/contracts/Contracts.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/contracts/Validity.java diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/StateDumper.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/StateDumper.java index 7027b37a363a..36be4652d718 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/StateDumper.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/StateDumper.java @@ -20,6 +20,7 @@ import static com.hedera.node.app.bbm.accounts.AccountDumpUtils.dumpMonoAccounts; import static com.hedera.node.app.bbm.associations.TokenAssociationsDumpUtils.dumpModTokenRelations; import static com.hedera.node.app.bbm.associations.TokenAssociationsDumpUtils.dumpMonoTokenRelations; +import static com.hedera.node.app.bbm.contracts.ContractBytecodesDumpUtils.dumpModContractBytecodes; import static com.hedera.node.app.bbm.files.FilesDumpUtils.dumpModFiles; import static com.hedera.node.app.bbm.files.FilesDumpUtils.dumpMonoFiles; import static com.hedera.node.app.bbm.nfts.UniqueTokenDumpUtils.dumpModUniqueTokens; @@ -36,14 +37,19 @@ import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.ContractID; import com.hedera.hapi.node.base.FileID; import com.hedera.hapi.node.base.NftID; import com.hedera.hapi.node.base.TokenAssociation; import com.hedera.hapi.node.state.blockrecords.BlockInfo; +import com.hedera.hapi.node.state.contract.Bytecode; import com.hedera.hapi.node.state.token.Account; import com.hedera.hapi.node.state.token.Nft; import com.hedera.hapi.node.state.token.TokenRelation; +import com.hedera.node.app.bbm.contracts.ContractBytecodesDumpUtils; import com.hedera.node.app.records.BlockRecordService; +import com.hedera.node.app.service.contract.ContractService; +import com.hedera.node.app.service.contract.impl.state.InitialModServiceContractSchema; import com.hedera.node.app.service.file.FileService; import com.hedera.node.app.service.file.impl.FileServiceImpl; import com.hedera.node.app.service.mono.state.merkle.MerkleNetworkContext; @@ -68,6 +74,7 @@ public class StateDumper { private static final String SEMANTIC_TOKEN_RELATIONS = "tokenRelations.txt"; private static final String SEMANTIC_FILES = "files.txt"; private static final String SEMANTIC_ACCOUNTS = "accounts.txt"; + private static final String SEMANTIC_CONTRACT_BYTECODES = "contractBytecodes.txt"; public static void dumpMonoChildrenFrom( @NonNull final MerkleHederaState state, @NonNull final DumpCheckpoint checkpoint) { @@ -78,6 +85,12 @@ public static void dumpMonoChildrenFrom( Paths.get(dumpLoc, SEMANTIC_TOKEN_RELATIONS), state.getChild(TOKEN_ASSOCIATIONS), checkpoint); dumpMonoFiles(Paths.get(dumpLoc, SEMANTIC_FILES), state.getChild(STORAGE), checkpoint); dumpMonoAccounts(Paths.get(dumpLoc, SEMANTIC_TOKEN_RELATIONS), state.getChild(ACCOUNTS), checkpoint); + + ContractBytecodesDumpUtils.dumpMonoContractBytecodes( + Paths.get(dumpLoc, SEMANTIC_CONTRACT_BYTECODES), + state.getChild(ACCOUNTS), + state.getChild(STORAGE), + checkpoint); } public static void dumpModChildrenFrom( @@ -106,6 +119,10 @@ public static void dumpModChildrenFrom( final VirtualMap, OnDiskValue> accounts = requireNonNull(state.getChild(state.findNodeIndex(TokenService.NAME, ACCOUNTS_KEY))); dumpModAccounts(Paths.get(dumpLoc, SEMANTIC_ACCOUNTS), accounts, checkpoint); + + final VirtualMap, OnDiskValue> contracts = requireNonNull(state.getChild( + state.findNodeIndex(ContractService.NAME, InitialModServiceContractSchema.BYTECODE_KEY))); + dumpModContractBytecodes(Paths.get(dumpLoc, SEMANTIC_CONTRACT_BYTECODES), contracts, checkpoint); } private static String getExtantDumpLoc( diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/accounts/AccountDumpUtils.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/accounts/AccountDumpUtils.java index 7afbc858010a..c6d9661ece43 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/accounts/AccountDumpUtils.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/accounts/AccountDumpUtils.java @@ -98,7 +98,7 @@ public static void dumpModAccounts( } @NonNull - private static HederaAccount[] gatherAccounts( + public static HederaAccount[] gatherAccounts( @NonNull VirtualMap accounts, @NonNull Function mapper) { final var accountsToReturn = new ConcurrentLinkedQueue(); final var threadCount = 8; diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/contracts/ByteArrayAsKey.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/contracts/ByteArrayAsKey.java new file mode 100644 index 000000000000..820138112ca0 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/contracts/ByteArrayAsKey.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.hedera.node.app.bbm.contracts; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Arrays; + +/** Implements equality-of-content on a byte array so it can be used as a map key */ +public record ByteArrayAsKey(@NonNull byte[] array) { + + @Override + public boolean equals(final Object obj) { + return obj instanceof ByteArrayAsKey other && Arrays.equals(array, other.array); + } + + @Override + public int hashCode() { + return Arrays.hashCode(array); + } + + @Override + public String toString() { + return "ByteArrayAsKey{" + "array=" + Arrays.toString(array) + '}'; + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/contracts/Contract.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/contracts/Contract.java new file mode 100644 index 000000000000..bf2ed6bec265 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/contracts/Contract.java @@ -0,0 +1,87 @@ +/* + * 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.bbm.contracts; + +import com.hedera.hapi.node.base.ContractID; +import com.hedera.hapi.node.state.contract.Bytecode; +import com.hedera.node.app.state.merkle.disk.OnDiskKey; +import com.hedera.node.app.state.merkle.disk.OnDiskValue; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Arrays; +import java.util.TreeSet; +import java.util.stream.Collectors; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +/** + * A contract - some bytecode associated with its contract id(s) + * + * @param ids - direct from the signed state file there's one contract id for each bytecode, but + * there are duplicates which can be coalesced and then there's a set of ids for the single + * contract; kept in sorted order by the container `TreeSet` so it's easy to get the canonical + * id for the contract, and also you can't forget to process them in a deterministic order + * @param bytecode - bytecode of the contract + * @param validity - whether the contract is valid or note, aka active or deleted + */ +public record Contract( + @NonNull TreeSet ids, @NonNull byte[] bytecode, @NonNull Validity validity) { + + public static Contract fromMod(OnDiskKey id, OnDiskValue bytecode) { + final var c = new Contract(new TreeSet<>(), bytecode.getValue().code().toByteArray(), Validity.ACTIVE); + if (id.getKey().contractNum() != null) { + c.ids().add(id.getKey().contractNum().intValue()); + } + return c; + } + + // For any set of contract ids with the same bytecode, the lowest contract id is used as the "canonical" + // id for that bytecode (useful for ordering contracts deterministically) + public int canonicalId() { + return ids.first(); + } + + @Override + public boolean equals(final Object o) { + if (o == null) { + return false; + } + if (o == this) { + return true; + } + return o instanceof Contract other + && new EqualsBuilder() + .append(ids, other.ids) + .append(bytecode, other.bytecode) + .append(validity, other.validity) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .append(ids) + .append(bytecode) + .append(validity) + .toHashCode(); + } + + @Override + public String toString() { + var csvIds = ids.stream().map(Object::toString).collect(Collectors.joining(",")); + return "Contract{ids=(%s), %s, bytecode=%s}".formatted(csvIds, validity, Arrays.toString(bytecode)); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/contracts/ContractBytecodesDumpUtils.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/contracts/ContractBytecodesDumpUtils.java new file mode 100644 index 000000000000..e11420439bbe --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/contracts/ContractBytecodesDumpUtils.java @@ -0,0 +1,231 @@ +/* + * 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.bbm.contracts; + +import static com.hedera.node.app.bbm.contracts.ContractUtils.ESTIMATED_NUMBER_OF_CONTRACTS; +import static com.swirlds.common.threading.manager.AdHocThreadManager.getStaticThreadManager; + +import com.hedera.hapi.node.base.ContractID; +import com.hedera.hapi.node.state.contract.Bytecode; +import com.hedera.node.app.bbm.DumpCheckpoint; +import com.hedera.node.app.bbm.utils.Writer; +import com.hedera.node.app.service.mono.state.adapters.VirtualMapLike; +import com.hedera.node.app.service.mono.state.migration.AccountStorageAdapter; +import com.hedera.node.app.service.mono.state.virtual.EntityNumVirtualKey; +import com.hedera.node.app.service.mono.state.virtual.VirtualBlobKey; +import com.hedera.node.app.service.mono.state.virtual.VirtualBlobValue; +import com.hedera.node.app.service.mono.state.virtual.entities.OnDiskAccount; +import com.hedera.node.app.state.merkle.disk.OnDiskKey; +import com.hedera.node.app.state.merkle.disk.OnDiskValue; +import com.swirlds.virtualmap.VirtualMap; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HexFormat; +import java.util.List; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import org.apache.commons.lang3.tuple.Pair; + +public class ContractBytecodesDumpUtils { + + private ContractBytecodesDumpUtils() { + // Utility class + } + + public static void dumpModContractBytecodes( + @NonNull final Path path, + @NonNull final VirtualMap, OnDiskValue> contracts, + @NonNull final DumpCheckpoint checkpoint) { + final var dumpableAccounts = gatherModContracts(contracts); + final var sb = generateReport(dumpableAccounts); + try (@NonNull final var writer = new Writer(path)) { + writer.writeln(sb.toString()); + System.out.printf( + "=== contract bytecodes report is %d bytes at checkpoint %s%n", + writer.getSize(), checkpoint.name()); + } + } + + @NonNull + public static Contracts gatherModContracts(VirtualMap, OnDiskValue> contracts) { + final var contractsToReturn = new ConcurrentLinkedQueue(); + final var threadCount = 8; + final var processed = new AtomicInteger(); + + try { + VirtualMapLike.from(contracts) + .extractVirtualMapData( + getStaticThreadManager(), + p -> { + processed.incrementAndGet(); + contractsToReturn.add(Contract.fromMod(p.left(), p.right())); + }, + threadCount); + } catch (final InterruptedException ex) { + System.err.println("*** Traversal of contracts virtual map interrupted!"); + Thread.currentThread().interrupt(); + } + + final var contractArr = contractsToReturn.toArray(new Contract[0]); + System.out.printf("=== %d contracts iterated over (%d saved)%n", processed.get(), contractArr.length); + return new Contracts(List.of(contractArr), List.of(), contractArr.length); + } + + public static void dumpMonoContractBytecodes( + @NonNull final Path path, + @NonNull final VirtualMap accounts, + @NonNull final VirtualMapLike files, + @NonNull final DumpCheckpoint checkpoint) { + final var accountAdapter = AccountStorageAdapter.fromOnDisk(VirtualMapLike.from(accounts)); + final var knownContracts = ContractUtils.getMonoContracts(files, accountAdapter); + final var sb = generateReport(knownContracts); + try (@NonNull final var writer = new Writer(path)) { + writer.writeln(sb.toString()); + System.out.printf( + "=== contract bytecodes report is %d bytes at checkpoint %s%n", + writer.getSize(), checkpoint.name()); + } + } + + private static StringBuilder generateReport(Contracts knownContracts) { + if (knownContracts.contracts().isEmpty()) { + return new StringBuilder(); + } + + var r = getNonTrivialContracts(knownContracts); + var contractsWithBytecode = r.getLeft(); + var zeroLengthContracts = r.getRight(); + + final var totalContractsRegisteredWithAccounts = contractsWithBytecode.registeredContractsCount(); + final var totalContractsPresentInFileStore = + contractsWithBytecode.contracts().size(); + int totalUniqueContractsPresentInFileStore = totalContractsPresentInFileStore; + + r = uniqifyContracts(contractsWithBytecode, zeroLengthContracts); + contractsWithBytecode = r.getLeft(); + zeroLengthContracts = r.getRight(); + totalUniqueContractsPresentInFileStore = + contractsWithBytecode.contracts().size(); + + // emitSummary + final var sb = new StringBuilder(estimateReportSize(contractsWithBytecode)); + sb.append("%d registered contracts, %d with bytecode (%d are zero-length)%s, %d deleted contracts%n" + .formatted( + totalContractsRegisteredWithAccounts, + totalContractsPresentInFileStore + zeroLengthContracts.size(), + zeroLengthContracts.size(), + ", %d unique (by bytecode)".formatted(totalUniqueContractsPresentInFileStore), + contractsWithBytecode.deletedContracts().size())); + + appendFormattedContractLines(sb, contractsWithBytecode); + return sb; + } + + /** Returns all contracts with bytecodes from the signed state, plus the ids of contracts with 0-length bytecodes. + * + * Returns both the set of all contract ids with their bytecode, and the total number of contracts registered + * in the signed state file. The latter number may be larger than the number of contracts-with-bytecodes + * returned because some contracts known to accounts are not present in the file store. + */ + @NonNull + private static Pair> getNonTrivialContracts(Contracts knownContracts) { + final var zeroLengthContracts = new ArrayList(10000); + knownContracts.contracts().removeIf(contract -> { + if (0 == contract.bytecode().length) { + zeroLengthContracts.addAll(contract.ids()); + return true; + } + return false; + }); + return Pair.of(knownContracts, zeroLengthContracts); + } + + /** Returns all _unique_ contracts (by their bytecode) from the signed state. + * + * Returns the set of all unique contracts (by their bytecode), each contract bytecode with _all_ of the + * contract ids that have that bytecode. Also returns the total number of contracts registered in the signed + * state. The latter number may be larger than the number of contracts-with-bytecodes because some contracts + * known to accounts are not present in the file store. Deleted contracts are _omitted_. + */ + @NonNull + private static Pair> uniqifyContracts( + @NonNull final Contracts contracts, @NonNull final List zeroLengthContracts) { + // First create a map where the bytecode is the key (have to wrap the byte[] for that) and the value is + // the set of all contract ids that have that bytecode + final var contractsByBytecode = new HashMap>(ESTIMATED_NUMBER_OF_CONTRACTS); + for (var contract : contracts.contracts()) { + if (contract.validity() == Validity.DELETED) { + continue; + } + final var bytecode = contract.bytecode(); + final var cids = contract.ids(); + contractsByBytecode.compute(new ByteArrayAsKey(bytecode), (k, v) -> { + if (v == null) { + v = new TreeSet<>(); + } + v.addAll(cids); + return v; + }); + } + + // Second, flatten that map into a collection + final var uniqueContracts = new ArrayList(contractsByBytecode.size()); + for (final var kv : contractsByBytecode.entrySet()) { + uniqueContracts.add(new Contract(kv.getValue(), kv.getKey().array(), Validity.ACTIVE)); + } + + return Pair.of( + new Contracts(uniqueContracts, contracts.deletedContracts(), contracts.registeredContractsCount()), + zeroLengthContracts); + } + + private static int estimateReportSize(@NonNull Contracts contracts) { + int totalBytecodeSize = contracts.contracts().stream() + .map(Contract::bytecode) + .mapToInt(bc -> bc.length) + .sum(); + // Make a swag based on how many contracts there are plus bytecode size - each line has not just the bytecode + // but the list of contract ids, so the estimated size of the file accounts for the bytecodes (as hex) and the + // contract ids (as decimal) + return contracts.registeredContractsCount() * 20 + totalBytecodeSize * 2; + } + + /** Format a collection of pairs of a set of contract ids with their associated bytecode */ + private static void appendFormattedContractLines( + @NonNull final StringBuilder sb, @NonNull final Contracts contracts) { + contracts.contracts().stream() + .sorted(Comparator.comparingInt(Contract::canonicalId)) + .forEach(contract -> appendFormattedContractLine(sb, contract)); + } + + private static final HexFormat hexer = HexFormat.of().withUpperCase(); + + /** Format a single contract line - may want any id, may want _all_ ids */ + private static void appendFormattedContractLine(@NonNull final StringBuilder sb, @NonNull final Contract contract) { + sb.append(hexer.formatHex(contract.bytecode())); + sb.append('\t'); + sb.append(contract.canonicalId()); + sb.append('\t'); + sb.append(contract.ids().stream().map(Object::toString).collect(Collectors.joining(","))); + sb.append('\n'); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/contracts/ContractUtils.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/contracts/ContractUtils.java new file mode 100644 index 000000000000..747f66a91aab --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/contracts/ContractUtils.java @@ -0,0 +1,105 @@ +/* + * 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.bbm.contracts; + +import com.hedera.node.app.service.mono.state.adapters.VirtualMapLike; +import com.hedera.node.app.service.mono.state.migration.AccountStorageAdapter; +import com.hedera.node.app.service.mono.state.virtual.VirtualBlobKey; +import com.hedera.node.app.service.mono.state.virtual.VirtualBlobKey.Type; +import com.hedera.node.app.service.mono.state.virtual.VirtualBlobValue; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; + +public class ContractUtils { + + static final int ESTIMATED_NUMBER_OF_CONTRACTS = 100_000; + static final int ESTIMATED_NUMBER_OF_DELETED_CONTRACTS = 10_000; + + private ContractUtils() { + // Utility class + } + + /** + * Return all the bytecodes for all the contracts in this state. + */ + @NonNull + public static Contracts getMonoContracts( + VirtualMapLike files, AccountStorageAdapter accountAdapter) { + final var contractIds = getAllKnownContracts(accountAdapter); + final var deletedContractIds = getAllDeletedContracts(accountAdapter); + final var contractContents = getAllContractContents(files, contractIds, deletedContractIds); + return new Contracts(contractContents, deletedContractIds, contractIds.size()); + } + + /** + * Returns all contracts known via Hedera accounts, by their contract id (lowered to an Integer) + */ + @NonNull + private static Set getAllKnownContracts(AccountStorageAdapter accounts) { + final var ids = new HashSet(ESTIMATED_NUMBER_OF_CONTRACTS); + accounts.forEach((k, v) -> { + if (null != k && null != v && v.isSmartContract()) { + ids.add(k.intValue()); + } + }); + return ids; + } + + /** Returns the ids of all deleted contracts ("self-destructed") */ + @NonNull + private static Set getAllDeletedContracts(AccountStorageAdapter accounts) { + final var ids = new HashSet(ESTIMATED_NUMBER_OF_DELETED_CONTRACTS); + accounts.forEach((k, v) -> { + if (null != k && null != v && v.isSmartContract() && v.isDeleted()) { + ids.add(k.intValue()); + } + }); + return ids; + } + + /** Returns the bytecodes for all the requested contracts */ + @NonNull + private static Collection getAllContractContents( + @NonNull final VirtualMapLike fileStore, + @NonNull final Collection contractIds, + @NonNull final Collection deletedContractIds) { + Objects.requireNonNull(contractIds); + Objects.requireNonNull(deletedContractIds); + + final var codes = new ArrayList(ESTIMATED_NUMBER_OF_CONTRACTS); + for (final var cid : contractIds) { + final var vbk = new VirtualBlobKey(Type.CONTRACT_BYTECODE, cid); + if (fileStore.containsKey(vbk)) { + final var blob = fileStore.get(vbk); + if (null != blob) { + final var c = new Contract( + new TreeSet<>(), + blob.getData(), + deletedContractIds.contains(cid) ? Validity.DELETED : Validity.ACTIVE); + c.ids().add(cid); + codes.add(c); + } + } + } + return codes; + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/contracts/Contracts.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/contracts/Contracts.java new file mode 100644 index 000000000000..e8add3c4131c --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/contracts/Contracts.java @@ -0,0 +1,34 @@ +/* + * 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.bbm.contracts; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Collection; + +/** + * All contracts extracted from a signed state file + * + * @param contracts - dictionary of contract bytecodes indexed by their contract id (as a Long) + * @param deletedContracts - collection of ids of deleted contracts + * @param registeredContractsCount - total #contracts known to the _accounts_ in the signed + * state file (not all actually have bytecodes in the file store, and of those, some have + * 0-length bytecode files) + */ +public record Contracts( + @NonNull Collection contracts, + @NonNull Collection deletedContracts, + int registeredContractsCount) {} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/contracts/Validity.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/contracts/Validity.java new file mode 100644 index 000000000000..df0e607fc65d --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/contracts/Validity.java @@ -0,0 +1,22 @@ +/* + * 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.bbm.contracts; + +public enum Validity { + ACTIVE, + DELETED +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/files/FileId.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/files/FileId.java index 502aa3c3622d..a7001a305b1e 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/files/FileId.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/files/FileId.java @@ -21,7 +21,7 @@ import com.hedera.node.app.bbm.utils.Writer; import edu.umd.cs.findbugs.annotations.NonNull; -record FileId(long shardNum, long realmNum, long fileNum) implements Comparable { +public record FileId(long shardNum, long realmNum, long fileNum) implements Comparable { static FileId fromMod(@NonNull final FileID fileID) { return new FileId(fileID.shardNum(), fileID.realmNum(), fileID.fileNum()); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/files/FilesDumpUtils.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/files/FilesDumpUtils.java index eeb552565441..f202e8a560f6 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/files/FilesDumpUtils.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/files/FilesDumpUtils.java @@ -76,7 +76,7 @@ public static void dumpMonoFiles( } @NonNull - private static Map gatherModFiles(VirtualMap, OnDiskValue> source) { + public static Map gatherModFiles(VirtualMap, OnDiskValue> source) { final var r = new HashMap(); final var threadCount = 8; final var files = new ConcurrentLinkedQueue>(); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/files/HederaFile.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/files/HederaFile.java index 6bb722457821..007d9f8f9f89 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/files/HederaFile.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/files/HederaFile.java @@ -25,7 +25,7 @@ /** Holds the content and the metadata for a single data file in the store */ @SuppressWarnings("java:S6218") // "Equals/hashcode methods should be overridden in records containing array fields" // not using this with equals -record HederaFile( +public record HederaFile( @NonNull FileStore fileStore, @NonNull Integer fileId, @NonNull byte[] contents, From 48f9b4174ffe72afcd761c0de4e273a5dc9fa1a6 Mon Sep 17 00:00:00 2001 From: Kore Aguda <157432197+kfa-aguda@users.noreply.github.com> Date: Thu, 29 Feb 2024 12:04:36 -0600 Subject: [PATCH 004/115] feat: Remove unidirectional networks - no longer supported (#11798) Signed-off-by: Kore Aguda --- platform-sdk/docs/core/network/network.md | 7 -- .../docs/core/network/unidirectional.md | 15 ---- .../VirtualMapLargeReconnectTest.java | 3 + .../platform/gossip/AbstractGossip.java | 8 +- .../gossip/sync/SingleNodeSyncGossip.java | 8 -- .../platform/gossip/sync/SyncGossip.java | 8 -- .../network/topology/StaticTopology.java | 18 +--- .../network/StaticConnectionManagersTest.java | 90 ++++++++++--------- .../platform/test/network/TopologyTest.java | 9 +- 9 files changed, 63 insertions(+), 103 deletions(-) delete mode 100644 platform-sdk/docs/core/network/unidirectional.md diff --git a/platform-sdk/docs/core/network/network.md b/platform-sdk/docs/core/network/network.md index 307904e9fb10..763380551422 100644 --- a/platform-sdk/docs/core/network/network.md +++ b/platform-sdk/docs/core/network/network.md @@ -14,11 +14,6 @@ network. The platform networking functionality aims to accomplish these goals: - has simple, loosely coupled, unit tested components ## Design -The functionality is split up into multiple layers, each with its own responsibilities. See diagram below. -**Note:** The aim is to transition away from unidirectional networks to bidirectional, so less consideration is given -to the former. We plan to support both until we are ready to transition. More details about each can be found in the -following docs: -- [Unidirectional](unidirectional.md) - [Bidirectional](bidirectional.md) ![](network.png) @@ -30,8 +25,6 @@ number of connections each node has. So a node will not be directly connected to will receive events from non-neighbors through neighbors - **Outbound connection** - a connection to a neighbor that has been initiated by me - **Inbound connection** - a connection to a neighbor that has been initiated by the neighbor -- **Unidirectional network** - a network where we have 2 connections per neighbor, 1 inbound and 1 outbound. A node can -only initiate a protocol request through its outbound connection in this type of network. - **Bidirectional network** - a network where we have 1 connections per neighbor, that can be either inbound or outbound, depending on the topology. In this type of network, both nodes can initiate a protocol over the same connection. This means that both sides could initiate a protocol in parallel, so it is slightly more complex to decide diff --git a/platform-sdk/docs/core/network/unidirectional.md b/platform-sdk/docs/core/network/unidirectional.md deleted file mode 100644 index bb79c2136be9..000000000000 --- a/platform-sdk/docs/core/network/unidirectional.md +++ /dev/null @@ -1,15 +0,0 @@ -# Unidirectional network -A network where we have 2 connections per neighbor, 1 inbound and 1 outbound. A node can only initiate a protocol -request through its outbound connection. Each peer in a connection has a role: -- **Caller** - initiated the connection, outbound for him -- **Listener** - accepted the connection, inbound for him - -## Communication -A protocol is always initiated by the caller, the listener only responds: -[![](https://mermaid.ink/img/pako:eNp10LFqAzEMBuBXMZoSSF_AQ8IdLXTqkE4FL4r9pzX45KstDyXk3eujaeAo9SR-PslIF_I5gCxVfDaIx2Pk98KTE9PfzEWjjzOLmsFwNUOKHhvPKaFs_5pxMWM-bVKsCrmb4WG_H615_RJ_-EnGngzWvKGaXMxLXrln9JknsK7sPTUFdc5SsWo6wmcReP3nB9rRhDJxDH3by2Ic6QcmOLK9DDhzS-rIybXTNgdWPIWouZA9c6rYETfNyw5ktTT8otvFbur6DcsdbCU)](https://mermaid-js.github.io/mermaid-live-editor/edit/#pako:eNp10LFqAzEMBuBXMZoSSF_AQ8IdLXTqkE4FL4r9pzX45KstDyXk3eujaeAo9SR-PslIF_I5gCxVfDaIx2Pk98KTE9PfzEWjjzOLmsFwNUOKHhvPKaFs_5pxMWM-bVKsCrmb4WG_H615_RJ_-EnGngzWvKGaXMxLXrln9JknsK7sPTUFdc5SsWo6wmcReP3nB9rRhDJxDH3by2Ic6QcmOLK9DDhzS-rIybXTNgdWPIWouZA9c6rYETfNyw5ktTT8otvFbur6DcsdbCU) - -### Listener state diagram -[![](https://mermaid.ink/img/pako:eNpdkLFOwzAQhl_ldCNqFkarylKQoqwMGTCD8V3AKLHL5VKpqvruOE0sCp6s___On-0L-kSMBid1yk_BfYgbq9OjjZDXAQx0Lij0ScCnGNlrSHEtu_vy_ay8xk2OGxdpYFmD28nQwn7_lUKsa7vNvz68QVXVcDALIlqUS9YZcP57DsL0x1vMC9OYmxWEPYcT033XGmCRJGWi-Zf-ZtlEKXIB23IlCtMmhjTQUuMOR5bRBcrfdVlwi_rJI1s0eUvcu3lQizZeMzofKT_7mYImQdO7YeIdulnTyzl6NCozF2j79Y26_gB77X0Y)](https://mermaid-js.github.io/mermaid-live-editor/edit/#pako:eNpdkLFOwzAQhl_ldCNqFkarylKQoqwMGTCD8V3AKLHL5VKpqvruOE0sCp6s___On-0L-kSMBid1yk_BfYgbq9OjjZDXAQx0Lij0ScCnGNlrSHEtu_vy_ay8xk2OGxdpYFmD28nQwn7_lUKsa7vNvz68QVXVcDALIlqUS9YZcP57DsL0x1vMC9OYmxWEPYcT033XGmCRJGWi-Zf-ZtlEKXIB23IlCtMmhjTQUuMOR5bRBcrfdVlwi_rJI1s0eUvcu3lQizZeMzofKT_7mYImQdO7YeIdulnTyzl6NCozF2j79Y26_gB77X0Y) - -## Implementation overview -![](unidirectional-outline.png) \ No newline at end of file diff --git a/platform-sdk/swirlds-merkle/src/test/java/com/swirlds/virtual/merkle/reconnect/VirtualMapLargeReconnectTest.java b/platform-sdk/swirlds-merkle/src/test/java/com/swirlds/virtual/merkle/reconnect/VirtualMapLargeReconnectTest.java index 885f84fb56ed..4266a5c2723e 100644 --- a/platform-sdk/swirlds-merkle/src/test/java/com/swirlds/virtual/merkle/reconnect/VirtualMapLargeReconnectTest.java +++ b/platform-sdk/swirlds-merkle/src/test/java/com/swirlds/virtual/merkle/reconnect/VirtualMapLargeReconnectTest.java @@ -25,6 +25,7 @@ import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Tags; @@ -41,6 +42,8 @@ class VirtualMapLargeReconnectTest extends VirtualMapReconnectTestBase { @Tags({@Tag("VirtualMerkle"), @Tag("Reconnect"), @Tag("VMAP-003"), @Tag("VMAP-003.14")}) @Tag(TIME_CONSUMING) @DisplayName("Permutations of very large trees reconnecting") + // FUTURE WORK: https://github.com/hashgraph/hedera-services/issues/11507 + @Disabled void largeTeacherLargerLearnerPermutations(int teacherStart, int teacherEnd, int learnerStart, int learnerEnd) { for (int i = teacherStart; i < teacherEnd; i++) { diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/AbstractGossip.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/AbstractGossip.java index 9d286e4100f2..e70a09fe8ae1 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/AbstractGossip.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/AbstractGossip.java @@ -141,8 +141,7 @@ protected AbstractGossip( final CryptoConfig cryptoConfig = platformContext.getConfiguration().getConfigData(CryptoConfig.class); final SocketConfig socketConfig = platformContext.getConfiguration().getConfigData(SocketConfig.class); - topology = new StaticTopology( - addressBook, selfId, basicConfig.numConnections(), unidirectionalConnectionsEnabled()); + topology = new StaticTopology(addressBook, selfId, basicConfig.numConnections()); final SocketFactory socketFactory = socketFactory(keysAndCerts, cryptoConfig, socketConfig); // create an instance that can create new outbound connections @@ -237,11 +236,6 @@ private static SocketFactory socketFactory( @NonNull protected abstract FallenBehindManagerImpl buildFallenBehindManager(); - /** - * If true, use unidirectional connections between nodes. - */ - protected abstract boolean unidirectionalConnectionsEnabled(); - /** * {@inheritDoc} */ diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/sync/SingleNodeSyncGossip.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/sync/SingleNodeSyncGossip.java index 8ff6a21a65c2..8c08022c7d7e 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/sync/SingleNodeSyncGossip.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/sync/SingleNodeSyncGossip.java @@ -93,14 +93,6 @@ public SingleNodeSyncGossip( clearAllPipelinesForReconnect); } - /** - * {@inheritDoc} - */ - @Override - protected boolean unidirectionalConnectionsEnabled() { - return false; - } - /** * {@inheritDoc} */ diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/sync/SyncGossip.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/sync/SyncGossip.java index c1610a03127a..86808421c57d 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/sync/SyncGossip.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/sync/SyncGossip.java @@ -266,14 +266,6 @@ public SyncGossip( thingsToStart.add(() -> syncProtocolThreads.forEach(StoppableThread::start)); } - /** - * {@inheritDoc} - */ - @Override - protected boolean unidirectionalConnectionsEnabled() { - return false; - } - /** * {@inheritDoc} */ diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/topology/StaticTopology.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/topology/StaticTopology.java index c2e531f6dccd..632159e097aa 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/topology/StaticTopology.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/topology/StaticTopology.java @@ -26,7 +26,7 @@ import java.util.function.Predicate; /** - * A topology that never changes. Can be either unidirectional or bidirectional. + * A bidirectional topology that never changes. */ public class StaticTopology implements NetworkTopology { private static final long SEED = 0; @@ -38,21 +38,11 @@ public class StaticTopology implements NetworkTopology { private final AddressBook addressBook; private final RandomGraph connectionGraph; - private final boolean unidirectional; public StaticTopology( @NonNull final AddressBook addressBook, @NonNull final NodeId selfId, final int numberOfNeighbors) { - this(addressBook, selfId, numberOfNeighbors, true); - } - - public StaticTopology( - @NonNull final AddressBook addressBook, - @NonNull final NodeId selfId, - final int numberOfNeighbors, - final boolean unidirectional) { this.addressBook = Objects.requireNonNull(addressBook, "addressBook must not be null"); this.selfId = Objects.requireNonNull(selfId, "selfId must not be null"); - this.unidirectional = unidirectional; this.connectionGraph = new RandomGraph(addressBook.getSize(), numberOfNeighbors, SEED); if (!addressBook.contains(selfId)) { @@ -85,8 +75,7 @@ public List getNeighbors(final Predicate filter) { */ @Override public boolean shouldConnectToMe(final NodeId nodeId) { - return isNeighbor(nodeId) - && (unidirectional || addressBook.getIndexOfNodeId(nodeId) < addressBook.getIndexOfNodeId(selfId)); + return isNeighbor(nodeId) && addressBook.getIndexOfNodeId(nodeId) < addressBook.getIndexOfNodeId(selfId); } /** @@ -110,8 +99,7 @@ private boolean isNeighbor(final NodeId nodeId) { */ @Override public boolean shouldConnectTo(final NodeId nodeId) { - return isNeighbor(nodeId) - && (unidirectional || addressBook.getIndexOfNodeId(nodeId) > addressBook.getIndexOfNodeId(selfId)); + return isNeighbor(nodeId) && addressBook.getIndexOfNodeId(nodeId) > addressBook.getIndexOfNodeId(selfId); } /** diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/StaticConnectionManagersTest.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/StaticConnectionManagersTest.java index 53579dcf1ef4..bae285017b9e 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/StaticConnectionManagersTest.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/StaticConnectionManagersTest.java @@ -52,52 +52,62 @@ private static List topologicalVariations() { @ParameterizedTest @MethodSource("topologicalVariations") - void test(final int numNodes, final int numNeighbors) throws Exception { + void testShouldConnectToMe(final int numNodes, final int numNeighbors) throws Exception { final Random r = RandomUtils.getRandomPrintSeed(); final AddressBook addressBook = new RandomAddressBookGenerator(r).setSize(numNodes).build(); final NodeId selfId = addressBook.getNodeId(r.nextInt(numNodes)); - Mockito.when(connectionCreator.createConnection(Mockito.any())).thenAnswer(inv -> { - final NodeId peerId = inv.getArgument(0, NodeId.class); - return new FakeConnection(selfId, peerId); - }); - for (final Boolean unidirectional : List.of(true, false)) { - final StaticTopology topology = new StaticTopology(addressBook, selfId, numNeighbors, unidirectional); - final StaticConnectionManagers managers = new StaticConnectionManagers(topology, connectionCreator); - final List neighbors = topology.getNeighbors(); - final NodeId neighbor = neighbors.get(r.nextInt(neighbors.size())); + final StaticTopology topology = new StaticTopology(addressBook, selfId, numNeighbors); + final StaticConnectionManagers managers = new StaticConnectionManagers(topology, connectionCreator); + final List neighbors = topology.getNeighbors(); + final NodeId neighbor = neighbors.get(r.nextInt(neighbors.size())); - if (topology.shouldConnectToMe(neighbor)) { - final ConnectionManager manager = managers.getManager(neighbor, false); - assertNotNull(manager, "should have a manager for this connection"); - final Connection c1 = new FakeConnection(selfId, neighbor); - managers.newConnection(c1); - assertSame(c1, manager.waitForConnection(), "the manager should have received the connection supplied"); - assertTrue(c1.connected(), "a new inbound connection should be connected"); - final Connection c2 = new FakeConnection(selfId, neighbor); - managers.newConnection(c2); - assertFalse(c1.connected(), "the new connection should have disconnected the old one"); - assertSame(c2, manager.waitForConnection(), "c2 should have replaced c1"); - } else { - final ConnectionManager manager = managers.getManager(neighbor, false); - assertNull(manager, "should not have a manager for this connection"); - final Connection c = new FakeConnection(selfId, neighbor); - managers.newConnection(c); - assertFalse( - c.connected(), - "if an illegal connection is established, it should be disconnected immediately"); - } + if (topology.shouldConnectToMe(neighbor)) { + final ConnectionManager manager = managers.getManager(neighbor, false); + assertNotNull(manager, "should have a manager for this connection"); + final Connection c1 = new FakeConnection(selfId, neighbor); + managers.newConnection(c1); + assertSame(c1, manager.waitForConnection(), "the manager should have received the connection supplied"); + assertTrue(c1.connected(), "a new inbound connection should be connected"); + final Connection c2 = new FakeConnection(selfId, neighbor); + managers.newConnection(c2); + assertFalse(c1.connected(), "the new connection should have disconnected the old one"); + assertSame(c2, manager.waitForConnection(), "c2 should have replaced c1"); + } else { + final ConnectionManager manager = managers.getManager(neighbor, false); + assertNull(manager, "should not have a manager for this connection"); + final Connection c = new FakeConnection(selfId, neighbor); + managers.newConnection(c); + assertFalse( + c.connected(), "if an illegal connection is established, it should be disconnected immediately"); + } + } + + @ParameterizedTest + @MethodSource("topologicalVariations") + void testShouldConnectTo(final int numNodes, final int numNeighbors) throws Exception { + final Random r = RandomUtils.getRandomPrintSeed(); + final AddressBook addressBook = + new RandomAddressBookGenerator(r).setSize(numNodes).build(); + final NodeId selfId = addressBook.getNodeId(r.nextInt(numNodes)); + final StaticTopology topology = new StaticTopology(addressBook, selfId, numNeighbors); + final StaticConnectionManagers managers = new StaticConnectionManagers(topology, connectionCreator); + final List neighbors = topology.getNeighbors(); + final NodeId neighbor = neighbors.get(r.nextInt(neighbors.size())); - if (topology.shouldConnectTo(neighbor)) { - final ConnectionManager manager = managers.getManager(neighbor, true); - assertNotNull(manager, "should have a manager for this connection"); - assertTrue( - manager.waitForConnection().connected(), - "outbound connections should be esablished by the manager"); - } else { - final ConnectionManager manager = managers.getManager(neighbor, true); - assertNull(manager, "should not have a manager for this connection"); - } + if (topology.shouldConnectTo(neighbor)) { + Mockito.when(connectionCreator.createConnection(Mockito.any())).thenAnswer(inv -> { + final NodeId peerId = inv.getArgument(0, NodeId.class); + return new FakeConnection(selfId, peerId); + }); + final ConnectionManager manager = managers.getManager(neighbor, true); + assertNotNull(manager, "should have a manager for this connection"); + assertTrue( + manager.waitForConnection().connected(), + "outbound connections should be esablished by the manager"); + } else { + final ConnectionManager manager = managers.getManager(neighbor, true); + assertNull(manager, "should not have a manager for this connection"); } } } diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/TopologyTest.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/TopologyTest.java index 979db04535cf..16511af2665b 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/TopologyTest.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/TopologyTest.java @@ -115,7 +115,7 @@ void testRandomGraphs(final int numNodes, final int numNeighbors, final long see @ParameterizedTest @MethodSource("fullyConnected") - void testFullyConnectedUnidirectionalTopology(final int numNodes, final int numNeighbors, final long ignoredSeed) { + void testFullyConnectedTopology(final int numNodes, final int numNeighbors, final long ignoredSeed) { final AddressBook addressBook = new RandomAddressBookGenerator().setSize(numNodes).build(); for (int thisNode = 0; thisNode < numNodes; thisNode++) { @@ -129,8 +129,11 @@ void testFullyConnectedUnidirectionalTopology(final int numNodes, final int numN .toList(); assertEquals(expected, neighbors, "all should be neighbors except me"); for (final NodeId neighbor : neighbors) { - assertTrue(topology.shouldConnectTo(neighbor), "I should connect to all neighbors"); - assertTrue(topology.shouldConnectToMe(neighbor), "all neighbors should connect to me"); + assertTrue( + topology.shouldConnectTo(neighbor) ^ topology.shouldConnectToMe(neighbor), + String.format( + "Exactly one connection should be specified between nodes %s and %s%n", + thisNodeId, neighbor)); } assertFalse(topology.shouldConnectTo(thisNodeId), "I should not connect to myself"); assertFalse(topology.shouldConnectToMe(thisNodeId), "I should not connect to myself"); From ad241ac6ea7647d369c4a600c4ebd776acd93a82 Mon Sep 17 00:00:00 2001 From: JivkoKelchev Date: Fri, 1 Mar 2024 04:09:51 +0700 Subject: [PATCH 005/115] feat: Differential testing: Enhance token (type) dumper to handle modular representation (#11543) Signed-off-by: Zhivko Kelchev Co-authored-by: Valentin Tronkov <99957253+vtronkov@users.noreply.github.com> --- .../hedera/node/app/bbm/tokentypes/Token.java | 255 ++++++++++++++ .../bbm/tokentypes/TokenTypesDumpUtils.java | 330 ++++++++++++++++++ .../src/main/java/module-info.java | 3 +- 3 files changed, 587 insertions(+), 1 deletion(-) create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/tokentypes/Token.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/tokentypes/TokenTypesDumpUtils.java diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/tokentypes/Token.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/tokentypes/Token.java new file mode 100644 index 000000000000..5f63237d22a5 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/tokentypes/Token.java @@ -0,0 +1,255 @@ +/* + * 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.bbm.tokentypes; + +import static com.hedera.node.app.bbm.tokentypes.TokenTypesDumpUtils.jkeyDeepEqualsButBothNullIsFalse; +import static com.hedera.node.app.bbm.tokentypes.TokenTypesDumpUtils.jkeyIsComplex; +import static com.hedera.node.app.bbm.tokentypes.TokenTypesDumpUtils.jkeyPresentAndOk; +import static com.hedera.node.app.bbm.utils.ThingsToStrings.toStructureSummaryOfJKey; + +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.Key; +import com.hedera.hapi.node.base.TokenSupplyType; +import com.hedera.hapi.node.transaction.CustomFee; +import com.hedera.node.app.service.evm.store.tokens.TokenType; +import com.hedera.node.app.service.mono.legacy.core.jproto.JKey; +import com.hedera.node.app.service.mono.pbj.PbjConverter; +import com.hedera.node.app.service.mono.state.merkle.MerkleToken; +import com.hedera.node.app.service.mono.state.submerkle.EntityId; +import com.hedera.node.app.service.mono.state.submerkle.FcCustomFee; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.security.InvalidKeyException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.TreeMap; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; + +record Token( + @NonNull TokenType tokenType, + @NonNull TokenSupplyType tokenSupplyType, + long tokenTypeId, // this is the field `number` with setter/getter `getKey/setKey` + @NonNull String symbol, + @NonNull String name, + @NonNull String memo, + boolean deleted, + boolean paused, + long decimals, + long maxSupply, + long totalSupply, + long lastUsedSerialNumber, + long expiry, + @NonNull Optional autoRenewPeriod, + boolean accountsFrozenByDefault, + boolean accountsKycGrantedByDefault, + @Nullable EntityId treasury, + @Nullable EntityId autoRenewAccount, + @Nullable List feeSchedule, + @NonNull Optional adminKey, + @NonNull Optional feeScheduleKey, + @NonNull Optional freezeKey, + @NonNull Optional kycKey, + @NonNull Optional pauseKey, + @NonNull Optional supplyKey, + @NonNull Optional wipeKey) { + + static Token fromMono(@NonNull final MerkleToken token) { + var tokenRes = new Token( + token.tokenType(), + supplyTypeFromMono(token.supplyType()), + token.getKey().longValue(), + token.symbol(), + token.name(), + token.memo(), + token.isDeleted(), + token.isPaused(), + token.decimals(), + token.maxSupply(), + token.totalSupply(), + token.getLastUsedSerialNumber(), + token.expiry(), + token.autoRenewPeriod() == -1L ? Optional.empty() : Optional.of(token.autoRenewPeriod()), + token.accountsAreFrozenByDefault(), + token.accountsKycGrantedByDefault(), + token.treasury(), + token.autoRenewAccount(), + token.customFeeSchedule(), + token.adminKey(), + token.feeScheduleKey(), + token.freezeKey(), + token.kycKey(), + token.pauseKey(), + token.supplyKey(), + token.wipeKey()); + Objects.requireNonNull(tokenRes.tokenType, "tokenType"); + Objects.requireNonNull(tokenRes.tokenSupplyType, "tokenSupplyType"); + Objects.requireNonNull(tokenRes.symbol, "symbol"); + Objects.requireNonNull(tokenRes.name, "name"); + Objects.requireNonNull(tokenRes.memo, "memo"); + Objects.requireNonNull(tokenRes.adminKey, "adminKey"); + Objects.requireNonNull(tokenRes.feeScheduleKey, "feeScheduleKey"); + Objects.requireNonNull(tokenRes.freezeKey, "freezeKey"); + Objects.requireNonNull(tokenRes.kycKey, "kycKey"); + Objects.requireNonNull(tokenRes.pauseKey, "pauseKey"); + Objects.requireNonNull(tokenRes.supplyKey, "supplyKey"); + Objects.requireNonNull(tokenRes.wipeKey, "wipeKey"); + + return tokenRes; + } + + static Token fromMod(@NonNull final com.hedera.hapi.node.state.token.Token token) { + Token tokenRes; + + tokenRes = new Token( + TokenType.valueOf(token.tokenType().protoName()), + token.supplyType(), + token.tokenId().tokenNum(), + token.symbol(), + token.name(), + token.memo(), + token.deleted(), + token.paused(), + token.decimals(), + token.maxSupply(), + token.totalSupply(), + token.lastUsedSerialNumber(), + token.expirationSecond(), + token.autoRenewSeconds() == -1L ? Optional.empty() : Optional.of(token.autoRenewSeconds()), + token.accountsFrozenByDefault(), + token.accountsKycGrantedByDefault(), + idFromMod(token.treasuryAccountId()), + idFromMod(token.autoRenewAccountId()), + customFeesFromMod(token.customFees()), + keyFromMod(token.adminKey()), + keyFromMod(token.feeScheduleKey()), + keyFromMod(token.freezeKey()), + keyFromMod(token.kycKey()), + keyFromMod(token.pauseKey()), + keyFromMod(token.supplyKey()), + keyFromMod(token.wipeKey())); + + Objects.requireNonNull(tokenRes.tokenType, "tokenType"); + Objects.requireNonNull(tokenRes.tokenSupplyType, "tokenSupplyType"); + Objects.requireNonNull(tokenRes.symbol, "symbol"); + Objects.requireNonNull(tokenRes.name, "name"); + Objects.requireNonNull(tokenRes.memo, "memo"); + Objects.requireNonNull(tokenRes.adminKey, "adminKey"); + Objects.requireNonNull(tokenRes.feeScheduleKey, "feeScheduleKey"); + Objects.requireNonNull(tokenRes.freezeKey, "freezeKey"); + Objects.requireNonNull(tokenRes.kycKey, "kycKey"); + Objects.requireNonNull(tokenRes.pauseKey, "pauseKey"); + Objects.requireNonNull(tokenRes.supplyKey, "supplyKey"); + Objects.requireNonNull(tokenRes.wipeKey, "wipeKey"); + + return tokenRes; + } + + private static EntityId idFromMod(@Nullable final AccountID accountId) { + return null == accountId ? EntityId.MISSING_ENTITY_ID : new EntityId(0L, 0L, accountId.accountNumOrThrow()); + } + + private static List customFeesFromMod(List customFees) { + List fcCustomFees = new ArrayList<>(); + customFees.stream().forEach(fee -> { + var fcCustomFee = FcCustomFee.fromGrpc(PbjConverter.fromPbj(fee)); + fcCustomFees.add(fcCustomFee); + }); + return fcCustomFees; + } + + static TokenSupplyType supplyTypeFromMono( + @NonNull com.hedera.node.app.service.mono.state.enums.TokenSupplyType tokenSupplyType) { + return (tokenSupplyType.equals(com.hedera.node.app.service.mono.state.enums.TokenSupplyType.INFINITE)) + ? TokenSupplyType.INFINITE + : TokenSupplyType.FINITE; + } + + private static Optional keyFromMod(@Nullable Key key) { + try { + return key == null ? Optional.empty() : Optional.ofNullable(JKey.mapKey(key)); + } catch (InvalidKeyException invalidKeyException) { + // return invalid JKey + return Optional.of(new JKey() { + @Override + public boolean isEmpty() { + return true; + } + + @Override + public boolean isValid() { + return false; + } + }); + } + } + + @NonNull + String getKeyProfile() { + final var adminKeyOk = jkeyPresentAndOk(adminKey); + + return getKeyDescription((c, ojk) -> { + if (!jkeyPresentAndOk(ojk)) return " "; + if (!adminKeyOk) return c + " "; + if (c == 'A') return "A "; + if (jkeyDeepEqualsButBothNullIsFalse(ojk.get(), adminKey.get())) return c + "=A "; + return c + " "; + }); + } + + String getKeyComplexity() { + return getKeyDescription((c, ojk) -> { + if (!jkeyPresentAndOk(ojk)) return " "; + if (jkeyIsComplex(ojk.get())) return c + "! "; + return c + " "; + }); + } + + String getKeyStructure() { + final var r = getKeyDescription((c, ojk) -> { + if (!jkeyPresentAndOk(ojk)) return ""; + final var sb = new StringBuilder(); + final var b = toStructureSummaryOfJKey(sb, ojk.get()); + if (!b) return ""; + return c + ":" + sb + "; "; + }); + return r.isEmpty() ? "" : r.substring(0, r.length() - 2); + } + + // spotless:off + @NonNull + private static final Map>> KEYS = new TreeMap<>(Map.of( + 'A', Token::adminKey, + 'F', Token::feeScheduleKey, + 'K', Token::kycKey, + 'P', Token::pauseKey, + 'S', Token::supplyKey, + 'W', Token::wipeKey, + 'Z', Token::freezeKey)); + // spotless:on + + @NonNull + private String getKeyDescription(@NonNull final BiFunction, String> map) { + return KEYS.entrySet().stream() + .map(e -> map.apply(e.getKey(), e.getValue().apply(this))) + .collect(Collectors.joining()); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/tokentypes/TokenTypesDumpUtils.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/tokentypes/TokenTypesDumpUtils.java new file mode 100644 index 000000000000..fd60cb42f31c --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/tokentypes/TokenTypesDumpUtils.java @@ -0,0 +1,330 @@ +/* + * 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.bbm.tokentypes; + +import static com.hedera.node.app.bbm.utils.ThingsToStrings.quoteForCsv; +import static com.swirlds.common.threading.manager.AdHocThreadManager.getStaticThreadManager; + +import com.hedera.hapi.node.base.TokenID; +import com.hedera.hapi.node.base.TokenSupplyType; +import com.hedera.hapi.node.base.TokenType; +import com.hedera.node.app.bbm.DumpCheckpoint; +import com.hedera.node.app.bbm.utils.FieldBuilder; +import com.hedera.node.app.bbm.utils.ThingsToStrings; +import com.hedera.node.app.bbm.utils.Writer; +import com.hedera.node.app.service.mono.legacy.core.jproto.JKey; +import com.hedera.node.app.service.mono.state.adapters.MerkleMapLike; +import com.hedera.node.app.service.mono.state.adapters.VirtualMapLike; +import com.hedera.node.app.service.mono.state.merkle.MerkleToken; +import com.hedera.node.app.service.mono.utils.EntityNum; +import com.hedera.node.app.state.merkle.disk.OnDiskKey; +import com.hedera.node.app.state.merkle.disk.OnDiskValue; +import com.swirlds.base.utility.Pair; +import com.swirlds.merkle.map.MerkleMap; +import com.swirlds.virtualmap.VirtualMap; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class TokenTypesDumpUtils { + + /** String that separates all fields in the CSV format */ + static final String FIELD_SEPARATOR = ";"; + /** String that separates sub-fields (e.g., in lists). */ + static final String SUBFIELD_SEPARATOR = ","; + + static Function booleanFormatter = b -> b ? "T" : ""; + static Function csvQuote = s -> quoteForCsv(FIELD_SEPARATOR, s); + // spotless:off + @NonNull + static List>> fieldFormatters = List.of( + Pair.of("tokenType", getFieldFormatter(Token::tokenType, com.hedera.node.app.service.evm.store.tokens.TokenType::name)), + Pair.of("tokenSupplyType", getFieldFormatter(Token::tokenSupplyType, TokenSupplyType::name)), + Pair.of("tokenTypeId", getFieldFormatter(Token::tokenTypeId, Object::toString)), + Pair.of("symbol", getFieldFormatter(Token::symbol, csvQuote)), + Pair.of("name", getFieldFormatter(Token::name, csvQuote)), + Pair.of("memo", getFieldFormatter(Token::memo, csvQuote)), + Pair.of("isDeleted", getFieldFormatter(Token::deleted, booleanFormatter)), + Pair.of("isPaused", getFieldFormatter(Token::paused, booleanFormatter)), + Pair.of("decimals", getFieldFormatter(Token::decimals, Object::toString)), + Pair.of("maxSupply", getFieldFormatter(Token::maxSupply, Object::toString)), + Pair.of("totalSupply", getFieldFormatter(Token::totalSupply, Object::toString)), + Pair.of("lastUsedSerialNumber", getFieldFormatter(Token::lastUsedSerialNumber, Object::toString)), + Pair.of("expiry", getFieldFormatter(Token::expiry, Object::toString)), + Pair.of("autoRenewPeriod", getFieldFormatter(Token::autoRenewPeriod, getOptionalFormatter(Object::toString))), + Pair.of("accountsFrozenByDefault", getFieldFormatter(Token::accountsFrozenByDefault, booleanFormatter)), + Pair.of("accountsKycGrantedByDefault", getFieldFormatter(Token::accountsKycGrantedByDefault, booleanFormatter)), + Pair.of("treasuryAccount", getFieldFormatter(Token::treasury, getNullableFormatter(ThingsToStrings::toStringOfEntityId))), + Pair.of("autoRenewAccount", getFieldFormatter(Token::autoRenewAccount, getNullableFormatter(ThingsToStrings::toStringOfEntityId))), + Pair.of("feeSchedule", getFieldFormatter(Token::feeSchedule, + getNullableFormatter(getListFormatter(ThingsToStrings::toStringOfFcCustomFee, SUBFIELD_SEPARATOR)))), + Pair.of("adminKey", getFieldFormatter(Token::adminKey, getOptionalJKeyFormatter(ThingsToStrings::toStringOfJKey))), + Pair.of("feeScheduleKey", getFieldFormatter(Token::feeScheduleKey, getOptionalJKeyFormatter(ThingsToStrings::toStringOfJKey))), + Pair.of("frezeKey", getFieldFormatter(Token::freezeKey, getOptionalJKeyFormatter(ThingsToStrings::toStringOfJKey))), + Pair.of("kycKey", getFieldFormatter(Token::kycKey, getOptionalJKeyFormatter(ThingsToStrings::toStringOfJKey))), + Pair.of("pauseKey", getFieldFormatter(Token::pauseKey, getOptionalJKeyFormatter(ThingsToStrings::toStringOfJKey))), + Pair.of("supplyKey", getFieldFormatter(Token::supplyKey, getOptionalJKeyFormatter(ThingsToStrings::toStringOfJKey))), + Pair.of("wipeKey", getFieldFormatter(Token::wipeKey, getOptionalJKeyFormatter(ThingsToStrings::toStringOfJKey)))); + // spotless:on + + public static void dumpModTokenType( + @NonNull final Path path, + @NonNull final VirtualMap, OnDiskValue> tokens, + @NonNull final DumpCheckpoint checkpoint) { + + try (@NonNull final var writer = new Writer(path)) { + final var allTokens = gatherTokensFromMod(tokens, Token::fromMod); + dump(writer, allTokens); + System.out.printf( + "=== mod tokens report is %d bytes at checkpoint %s%n", writer.getSize(), checkpoint.name()); + } + } + + public static void dumpMonoTokenType( + @NonNull final Path path, + @NonNull final MerkleMap tokens, + @NonNull final DumpCheckpoint checkpoint) { + try (@NonNull final var writer = new Writer(path)) { + final var allTokens = gatherTokensFromMono(tokens); + dump(writer, allTokens); + System.out.printf( + "=== mono tokens report is %d bytes at checkpoint %s%n", writer.getSize(), checkpoint.name()); + } + } + + @NonNull + private static Map> gatherTokensFromMono( + @NonNull final MerkleMap source) { + + final var allTokens = new HashMap>(); + + allTokens.put(TokenType.FUNGIBLE_COMMON, new HashMap<>()); + allTokens.put(TokenType.NON_FUNGIBLE_UNIQUE, new HashMap<>()); + + // todo check if it is possible to use multi threading with MerkleMaps like VirtualMaps + MerkleMapLike.from(source).forEachNode((en, mt) -> allTokens + .get(TokenType.fromProtobufOrdinal(mt.tokenType().ordinal())) + .put(en.longValue(), Token.fromMono(mt))); + return allTokens; + } + + @NonNull + private static Map> gatherTokensFromMod( + @NonNull final VirtualMap, OnDiskValue> source, + @NonNull final Function valueMapper) { + final var r = new HashMap>(); + + r.put(TokenType.FUNGIBLE_COMMON, new HashMap<>()); + r.put(TokenType.NON_FUNGIBLE_UNIQUE, new HashMap<>()); + + final var threadCount = 8; + final var allMappings = new ConcurrentLinkedQueue>>(); + try { + + VirtualMapLike.from(source) + .extractVirtualMapDataC( + getStaticThreadManager(), + p -> { + var tokenId = p.left().getKey(); + var currentToken = p.right().getValue(); + var tokenMap = new HashMap(); + tokenMap.put(tokenId.tokenNum(), valueMapper.apply(currentToken)); + allMappings.add(Pair.of(currentToken.tokenType(), tokenMap)); + }, + threadCount); + + } catch (final InterruptedException ex) { + System.err.println("*** Traversal of uniques virtual map interrupted!"); + Thread.currentThread().interrupt(); + } + + while (!allMappings.isEmpty()) { + final var mapping = allMappings.poll(); + r.get(mapping.left()).putAll(mapping.value()); + } + return r; + } + + private static void dump(@NonNull Writer writer, @NonNull Map> allTokens) { + reportSummary(writer, allTokens); + + reportOnTokens(writer, "fungible", allTokens.get(TokenType.FUNGIBLE_COMMON)); + reportOnTokens(writer, "non-fungible", allTokens.get(TokenType.NON_FUNGIBLE_UNIQUE)); + + reportOnKeyStructure(writer, "fungible", allTokens.get(TokenType.FUNGIBLE_COMMON)); + reportOnKeyStructure(writer, "non-fungible", allTokens.get(TokenType.NON_FUNGIBLE_UNIQUE)); + + reportOnFees(writer, "fungible", allTokens.get(TokenType.FUNGIBLE_COMMON)); + reportOnFees(writer, "non-fungible", allTokens.get(TokenType.NON_FUNGIBLE_UNIQUE)); + } + + private static void reportSummary(@NonNull Writer writer, @NonNull Map> allTokens) { + writer.writeln("=== %7d: fungible token types" + .formatted(allTokens.get(TokenType.FUNGIBLE_COMMON).size())); + writer.writeln("=== %7d: non-fungible token types" + .formatted(allTokens.get(TokenType.NON_FUNGIBLE_UNIQUE).size())); + writer.writeln(""); + } + + private static void reportOnTokens( + @NonNull final Writer writer, @NonNull final String type, @NonNull final Map tokens) { + writer.writeln("=== %s token types%n".formatted(type)); + writer.writeln(formatHeader()); + tokens.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(e -> formatToken(writer, e.getValue())); + writer.writeln(""); + } + + private static void reportOnKeyStructure( + @NonNull final Writer writer, @NonNull final String type, @NonNull final Map tokens) { + + final BiConsumer> map = (title, fun) -> { + final var histogram = new HashMap(); + + for (@NonNull var e : tokens.entrySet()) { + histogram.merge(fun.apply(e.getValue()), 1, Integer::sum); + } + + writer.writeln("=== %s %s (%d distinct)%n".formatted(type, title, histogram.size())); + histogram.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .forEachOrdered(e -> writer.writeln("%7d: %s".formatted(e.getValue(), e.getKey()))); + writer.writeln(""); + }; + + map.accept("key structures", Token::getKeyStructure); + map.accept("key role profiles", Token::getKeyProfile); + map.accept("key complexity", Token::getKeyComplexity); + } + + private static void reportOnFees( + @NonNull final Writer writer, @NonNull final String type, @NonNull final Map tokens) { + final var histogram = new HashMap(); + for (@NonNull var token : tokens.values()) { + final var fees = token.feeSchedule(); + if (null == fees || fees.isEmpty()) continue; + final var feeProfile = fees.stream() + .map(ThingsToStrings::toSketchyStringOfFcCustomFee) + .sorted() + .collect(Collectors.joining(SUBFIELD_SEPARATOR)); + histogram.merge(feeProfile, 1, Integer::sum); + } + + writer.writeln("=== %s fee schedules (%d distinct)%n".formatted(type, histogram.size())); + histogram.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .forEachOrdered(e -> writer.writeln("%7d: %s".formatted(e.getValue(), e.getKey()))); + writer.writeln(""); + } + + @NonNull + static BiConsumer getFieldFormatter( + @NonNull final Function fun, @NonNull final Function formatter) { + return (fb, t) -> formatField(fb, t, fun, formatter); + } + + static void formatField( + @NonNull final FieldBuilder fb, + @NonNull final Token token, + @NonNull final Function fun, + @NonNull final Function formatter) { + fb.append(formatter.apply(fun.apply(token))); + } + + static Function getNullableFormatter(@NonNull final Function formatter) { + return t -> null != t ? formatter.apply(t) : ""; + } + + static Function, String> getListFormatter( + @NonNull final Function formatter, @NonNull final String subfieldSeparator) { + return lt -> { + if (!lt.isEmpty()) { + final var sb = new StringBuilder(); + for (@NonNull final var e : lt) { + final var v = formatter.apply(e); + sb.append(v); + sb.append(subfieldSeparator); + } + // Remove last subfield separator + if (sb.length() >= subfieldSeparator.length()) sb.setLength(sb.length() - subfieldSeparator.length()); + return sb.toString(); + } else return ""; + }; + } + + static void formatToken(@NonNull final Writer writer, @NonNull final Token token) { + final var fb = new FieldBuilder(FIELD_SEPARATOR); + fieldFormatters.stream().map(Pair::right).forEach(ff -> ff.accept(fb, token)); + writer.writeln(fb); + } + + static Function, String> getOptionalFormatter(@NonNull final Function formatter) { + return ot -> ot.isPresent() ? formatter.apply(ot.get()) : ""; + } + + static Function, String> getOptionalJKeyFormatter(@NonNull final Function formatter) { + return ot -> { + if (ot.isPresent()) { + return ot.get().isValid() ? formatter.apply(ot.get()) : ""; + } + return ""; + }; + } + + @NonNull + static String formatHeader() { + return fieldFormatters.stream().map(Pair::left).collect(Collectors.joining(FIELD_SEPARATOR)); + } + + public static boolean jkeyPresentAndOk(@NonNull Optional ojkey) { + if (ojkey.isEmpty()) return false; + if (ojkey.get().isEmpty()) return false; + return ojkey.get().isValid(); + } + + static boolean jkeyDeepEqualsButBothNullIsFalse(final JKey left, final JKey right) { + if (left == null || right == null) return false; + return left.equals(right); + } + + /** A "complex" key is a keylist with >1 key or a threshold key with >1 key. If a keylist has one key or if a + * threshold key is 1-of-1 then the complexity is the complexity of the contained key. Otherwise, it is not + * complex. */ + static boolean jkeyIsComplex(final JKey jkey) { + if (jkey == null) return false; + if (jkey.isEmpty()) return false; + if (!jkey.isValid()) return false; + if (jkey.hasThresholdKey()) { + final var jThresholdKey = jkey.getThresholdKey(); + final var th = jThresholdKey.getThreshold(); + final var n = jThresholdKey.getKeys().getKeysList().size(); + if (th == 1 && n == 1) + return jkeyIsComplex(jThresholdKey.getKeys().getKeysList().get(0)); + return true; + } else if (jkey.hasKeyList()) { + final var n = jkey.getKeyList().getKeysList().size(); + if (n == 1) return jkeyIsComplex(jkey.getKeyList().getKeysList().get(0)); + return true; + } else return false; + } +} diff --git a/hedera-node/hedera-mono-service/src/main/java/module-info.java b/hedera-node/hedera-mono-service/src/main/java/module-info.java index 73e046de80c9..57ccace105d1 100644 --- a/hedera-node/hedera-mono-service/src/main/java/module-info.java +++ b/hedera-node/hedera-mono-service/src/main/java/module-info.java @@ -76,7 +76,8 @@ exports com.hedera.node.app.service.mono.context.properties; exports com.hedera.node.app.service.mono.state.enums to com.hedera.node.app.service.mono.test.fixtures, - com.hedera.node.services.cli; + com.hedera.node.services.cli, + com.hedera.node.app; exports com.hedera.node.app.service.mono.state.exports to com.hedera.node.app; exports com.hedera.node.app.service.mono.records; From 3f17a6d786b2f856dc9c3da3e7d89e14f906f0ec Mon Sep 17 00:00:00 2001 From: Deyan Dimitrov Date: Thu, 29 Feb 2024 23:10:08 +0200 Subject: [PATCH 006/115] feat: scheduled txs modularization signed state dumper (#11524) Signed-off-by: dikel Signed-off-by: Valentin Tronkov <99957253+vtronkov@users.noreply.github.com> Co-authored-by: Valentin Tronkov <99957253+vtronkov@users.noreply.github.com> Co-authored-by: Valentin Tronkov --- .../ScheduledTransaction.java | 116 +++++++++ .../ScheduledTransactionId.java | 42 ++++ .../ScheduledTransactionsDumpUtils.java | 236 ++++++++++++++++++ .../app/service/mono/pbj/PbjConverter.java | 11 + 4 files changed, 405 insertions(+) create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/scheduledtransactions/ScheduledTransaction.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/scheduledtransactions/ScheduledTransactionId.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/scheduledtransactions/ScheduledTransactionsDumpUtils.java diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/scheduledtransactions/ScheduledTransaction.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/scheduledtransactions/ScheduledTransaction.java new file mode 100644 index 000000000000..0e8fee1987c6 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/scheduledtransactions/ScheduledTransaction.java @@ -0,0 +1,116 @@ +/* + * 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.bbm.scheduledtransactions; + +import com.hedera.hapi.node.state.schedule.Schedule; +import com.hedera.node.app.service.mono.legacy.core.jproto.JKey; +import com.hedera.node.app.service.mono.pbj.PbjConverter; +import com.hedera.node.app.service.mono.state.submerkle.EntityId; +import com.hedera.node.app.service.mono.state.submerkle.RichInstant; +import com.hedera.node.app.service.mono.state.virtual.schedule.ScheduleVirtualValue; +import com.hederahashgraph.api.proto.java.SchedulableTransactionBody; +import com.hederahashgraph.api.proto.java.TransactionBody; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.security.InvalidKeyException; +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +@SuppressWarnings("java:S6218") // "Equals/hashcode methods should be overridden in records containing array fields" +record ScheduledTransaction( + long number, + @NonNull Optional adminKey, + @Nullable String memo, + boolean deleted, + boolean executed, + boolean calculatedWaitForExpiry, + boolean waitForExpiryProvided, + @Nullable EntityId payer, + @NonNull EntityId schedulingAccount, + @NonNull RichInstant schedulingTXValidStart, + @Nullable RichInstant expirationTimeProvided, + @Nullable RichInstant calculatedExpirationTime, + @Nullable RichInstant resolutionTime, + @NonNull byte[] bodyBytes, + @Nullable TransactionBody ordinaryScheduledTxn, + @Nullable SchedulableTransactionBody scheduledTxn, + @Nullable List signatories) { + + static ScheduledTransaction fromMod(@NonNull final Schedule value) throws InvalidKeyException { + return new ScheduledTransaction( + value.scheduleId().scheduleNum(), + value.adminKey() != null ? Optional.of(JKey.mapKey(value.adminKey())) : Optional.empty(), + value.memo(), + value.deleted(), + value.executed(), + // calculatedWaitForExpiry is the same as waitForExpiryProvided; + // see ScheduleVirtualValue::from` - to.calculatedWaitForExpiry = to.waitForExpiryProvided; + value.waitForExpiry(), + value.waitForExpiry(), + entityIdFrom(value.payerAccountId().accountNum()), + entityIdFrom(value.schedulerAccountId().accountNum()), + RichInstant.fromJava(Instant.ofEpochSecond( + value.scheduleValidStart().seconds(), + value.scheduleValidStart().nanos())), + RichInstant.fromJava(Instant.ofEpochSecond(value.providedExpirationSecond())), + RichInstant.fromJava(Instant.ofEpochSecond(value.calculatedExpirationSecond())), + RichInstant.fromJava(Instant.ofEpochSecond( + value.resolutionTime().seconds(), value.resolutionTime().nanos())), + PbjConverter.fromPbj(value.originalCreateTransaction()).toByteArray(), + PbjConverter.fromPbj(value.originalCreateTransaction()), + PbjConverter.fromPbj(value.scheduledTransaction()), + value.signatories().stream() + .map(ScheduledTransaction::toPrimitiveKey) + .toList()); + } + + static ScheduledTransaction fromMono(@NonNull final ScheduleVirtualValue scheduleVirtualValue) { + return new ScheduledTransaction( + scheduleVirtualValue.getKey().getKeyAsLong(), + scheduleVirtualValue.adminKey(), + scheduleVirtualValue.memo().orElse(""), + scheduleVirtualValue.isDeleted(), + scheduleVirtualValue.isExecuted(), + scheduleVirtualValue.calculatedWaitForExpiry(), + scheduleVirtualValue.waitForExpiryProvided(), + scheduleVirtualValue.payer(), + scheduleVirtualValue.schedulingAccount(), + scheduleVirtualValue.schedulingTXValidStart(), + scheduleVirtualValue.expirationTimeProvided(), + scheduleVirtualValue.calculatedExpirationTime(), + scheduleVirtualValue.getResolutionTime(), + scheduleVirtualValue.bodyBytes(), + scheduleVirtualValue.ordinaryViewOfScheduledTxn(), + scheduleVirtualValue.scheduledTxn(), + scheduleVirtualValue.signatories()); + } + + static EntityId entityIdFrom(long num) { + return new EntityId(0L, 0L, num); + } + + static byte[] toPrimitiveKey(com.hedera.hapi.node.base.Key key) { + if (key.hasEd25519()) { + return key.ed25519().toByteArray(); + } else if (key.hasEcdsaSecp256k1()) { + return key.ecdsaSecp256k1().toByteArray(); + } else { + return new byte[] {}; + } + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/scheduledtransactions/ScheduledTransactionId.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/scheduledtransactions/ScheduledTransactionId.java new file mode 100644 index 000000000000..6a8aad442b33 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/scheduledtransactions/ScheduledTransactionId.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.bbm.scheduledtransactions; + +import com.google.common.collect.ComparisonChain; +import com.hedera.hapi.node.base.ScheduleID; +import com.hedera.node.app.service.mono.state.virtual.EntityNumVirtualKey; +import edu.umd.cs.findbugs.annotations.NonNull; + +record ScheduledTransactionId(long num) implements Comparable { + static ScheduledTransactionId fromMod(@NonNull final ScheduleID scheduleID) { + return new ScheduledTransactionId(scheduleID.scheduleNum()); + } + + static ScheduledTransactionId fromMono(@NonNull final EntityNumVirtualKey key) { + return new ScheduledTransactionId(key.getKeyAsLong()); + } + + @Override + public String toString() { + return "%d".formatted(num); + } + + @Override + public int compareTo(ScheduledTransactionId o) { + return ComparisonChain.start().compare(this.num, o.num).result(); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/scheduledtransactions/ScheduledTransactionsDumpUtils.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/scheduledtransactions/ScheduledTransactionsDumpUtils.java new file mode 100644 index 000000000000..04b4ec85fe4c --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/scheduledtransactions/ScheduledTransactionsDumpUtils.java @@ -0,0 +1,236 @@ +/* + * 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.bbm.scheduledtransactions; + +import static com.hedera.node.app.bbm.utils.ThingsToStrings.quoteForCsv; +import static com.swirlds.common.threading.manager.AdHocThreadManager.getStaticThreadManager; + +import com.hedera.hapi.node.base.ScheduleID; +import com.hedera.hapi.node.state.schedule.Schedule; +import com.hedera.node.app.bbm.DumpCheckpoint; +import com.hedera.node.app.bbm.utils.FieldBuilder; +import com.hedera.node.app.bbm.utils.ThingsToStrings; +import com.hedera.node.app.bbm.utils.Writer; +import com.hedera.node.app.service.mono.state.adapters.VirtualMapLike; +import com.hedera.node.app.service.mono.state.virtual.EntityNumVirtualKey; +import com.hedera.node.app.service.mono.state.virtual.schedule.ScheduleVirtualValue; +import com.hedera.node.app.state.merkle.disk.OnDiskKey; +import com.hedera.node.app.state.merkle.memory.InMemoryKey; +import com.hedera.node.app.state.merkle.memory.InMemoryValue; +import com.swirlds.base.utility.Pair; +import com.swirlds.merkle.map.MerkleMap; +import com.swirlds.virtualmap.VirtualMap; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.file.Path; +import java.security.InvalidKeyException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class ScheduledTransactionsDumpUtils { + + public static void dumpModScheduledTransactions( + @NonNull final Path path, + @NonNull + final MerkleMap, InMemoryValue> scheduledTransactions, + @NonNull final DumpCheckpoint checkpoint) { + try (@NonNull final var writer = new Writer(path)) { + final var dumpableScheduledTransactions = gatherModScheduledTransactions(scheduledTransactions); + reportOnScheduledTransactions(writer, dumpableScheduledTransactions); + System.out.printf( + "=== mod scheduled transactions report is %d bytes at checkpoint %s%n", + writer.getSize(), checkpoint.name()); + } + } + + public static void dumpMonoScheduledTransactions( + @NonNull final Path path, + @NonNull final VirtualMap, ScheduleVirtualValue> scheduledTransactions, + @NonNull final DumpCheckpoint checkpoint) { + try (@NonNull final var writer = new Writer(path)) { + final var dumpableScheduledTransactions = gatherMonoScheduledTransactions(scheduledTransactions); + reportOnScheduledTransactions(writer, dumpableScheduledTransactions); + System.out.printf( + "=== mono scheduled transactions report is %d bytes at checkpoint %s%n", + writer.getSize(), checkpoint.name()); + } + } + + @NonNull + private static Map gatherModScheduledTransactions( + MerkleMap, InMemoryValue> source) { + final var r = new HashMap(); + final var scheduledTransactions = + new ConcurrentLinkedQueue>(); + source.forEach((key, value) -> { + try { + scheduledTransactions.add(Pair.of( + ScheduledTransactionId.fromMod(key.key()), ScheduledTransaction.fromMod(value.getValue()))); + } catch (InvalidKeyException e) { + throw new RuntimeException(e); + } + }); + scheduledTransactions.forEach(filePair -> r.put(filePair.key(), filePair.value())); + return r; + } + + @NonNull + private static Map gatherMonoScheduledTransactions( + VirtualMap, ScheduleVirtualValue> source) { + final var r = new HashMap(); + final var threadCount = 8; + final var scheduledTransactions = + new ConcurrentLinkedQueue>(); + try { + VirtualMapLike.from(source) + .extractVirtualMapData( + getStaticThreadManager(), + p -> scheduledTransactions.add(Pair.of( + ScheduledTransactionId.fromMono(p.left().getKey()), + ScheduledTransaction.fromMono(p.right()))), + threadCount); + } catch (final InterruptedException ex) { + System.err.println("*** Traversal of files virtual map interrupted!"); + Thread.currentThread().interrupt(); + } + scheduledTransactions.forEach(filePair -> r.put(filePair.key(), filePair.value())); + return r; + } + + private static void reportOnScheduledTransactions( + @NonNull final Writer writer, + @NonNull final Map scheduledTransactions) { + writer.writeln(formatHeader()); + scheduledTransactions.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .forEach(e -> formatTokenAssociation(writer, e.getValue())); + writer.writeln(""); + } + + @NonNull + private static String formatHeader() { + return fieldFormatters.stream().map(Pair::left).collect(Collectors.joining(FIELD_SEPARATOR)); + } + + static final String FIELD_SEPARATOR = ";"; + static final String SUBFIELD_SEPARATOR = ","; + static Function booleanFormatter = b -> b ? "T" : ""; + static Function csvQuote = s -> quoteForCsv(FIELD_SEPARATOR, (s == null) ? "" : s.toString()); + + static Function, String> getOptionalFormatter(@NonNull final Function formatter) { + return ot -> ot.isPresent() ? formatter.apply(ot.get()) : ""; + } + + static Function getNullableFormatter(@NonNull final Function formatter) { + return t -> null != t ? formatter.apply(t) : ""; + } + + static Function, String> getListFormatter( + @NonNull final Function formatter, @NonNull final String subfieldSeparator) { + return lt -> { + if (!lt.isEmpty()) { + final var sb = new StringBuilder(); + for (@NonNull final var e : lt) { + final var v = formatter.apply(e); + sb.append(v); + sb.append(subfieldSeparator); + } + // Remove last subfield separator + if (sb.length() >= subfieldSeparator.length()) sb.setLength(sb.length() - subfieldSeparator.length()); + return sb.toString(); + } else return ""; + }; + } + + // spotless:off + @NonNull + private static final List>> fieldFormatters = List.of( + Pair.of("number", getFieldFormatter(ScheduledTransaction::number, Object::toString)), + Pair.of( + "adminKey", + getFieldFormatter( + ScheduledTransaction::adminKey, getOptionalFormatter(ThingsToStrings::toStringOfJKey))), + Pair.of("memo", getFieldFormatter(ScheduledTransaction::memo, csvQuote)), + Pair.of("isDeleted", getFieldFormatter(ScheduledTransaction::deleted, booleanFormatter)), + Pair.of("isExecuted", getFieldFormatter(ScheduledTransaction::executed, booleanFormatter)), + Pair.of( + "calculatedWaitForExpiry", + getFieldFormatter(ScheduledTransaction::calculatedWaitForExpiry, booleanFormatter)), + Pair.of( + "waitForExpiryProvided", + getFieldFormatter(ScheduledTransaction::waitForExpiryProvided, booleanFormatter)), + Pair.of("payer", getFieldFormatter(ScheduledTransaction::payer, ThingsToStrings::toStringOfEntityId)), + Pair.of( + "schedulingAccount", + getFieldFormatter(ScheduledTransaction::schedulingAccount, ThingsToStrings::toStringOfEntityId)), + Pair.of( + "schedulingTXValidStart", + getFieldFormatter( + ScheduledTransaction::schedulingTXValidStart, ThingsToStrings::toStringOfRichInstant)), + Pair.of( + "expirationTimeProvided", + getFieldFormatter( + ScheduledTransaction::expirationTimeProvided, + getNullableFormatter(ThingsToStrings::toStringOfRichInstant))), + Pair.of( + "calculatedExpirationTime", + getFieldFormatter( + ScheduledTransaction::calculatedExpirationTime, + getNullableFormatter(ThingsToStrings::toStringOfRichInstant))), + Pair.of( + "resolutionTime", + getFieldFormatter( + ScheduledTransaction::resolutionTime, + getNullableFormatter(ThingsToStrings::toStringOfRichInstant))), + Pair.of( + "bodyBytes", + getFieldFormatter(ScheduledTransaction::bodyBytes, ThingsToStrings::toStringOfByteArray)), + Pair.of("ordinaryScheduledTxn", getFieldFormatter(ScheduledTransaction::ordinaryScheduledTxn, csvQuote)), + Pair.of("scheduledTxn", getFieldFormatter(ScheduledTransaction::scheduledTxn, csvQuote)), + Pair.of( + "signatories", + getFieldFormatter( + ScheduledTransaction::signatories, + getListFormatter(ThingsToStrings::toStringOfByteArray, SUBFIELD_SEPARATOR)))); + // spotless:on + + @NonNull + static BiConsumer getFieldFormatter( + @NonNull final Function fun, @NonNull final Function formatter) { + return (fb, u) -> formatField(fb, u, fun, formatter); + } + + static void formatField( + @NonNull final FieldBuilder fb, + @NonNull final ScheduledTransaction scheduledTransaction, + @NonNull final Function fun, + @NonNull final Function formatter) { + fb.append(formatter.apply(fun.apply(scheduledTransaction))); + } + + private static void formatTokenAssociation( + @NonNull final Writer writer, @NonNull final ScheduledTransaction scheduledTransaction) { + final var fb = new FieldBuilder(FIELD_SEPARATOR); + fieldFormatters.stream().map(Pair::right).forEach(ff -> ff.accept(fb, scheduledTransaction)); + writer.writeln(fb); + } +} diff --git a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/pbj/PbjConverter.java b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/pbj/PbjConverter.java index e066ef5503ad..f7a6938d634d 100644 --- a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/pbj/PbjConverter.java +++ b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/pbj/PbjConverter.java @@ -1452,6 +1452,17 @@ private static R explicitPbjToP } } + public static @NonNull com.hederahashgraph.api.proto.java.SchedulableTransactionBody fromPbj( + @NonNull SchedulableTransactionBody tx) { + requireNonNull(tx); + try { + final var bytes = asBytes(SchedulableTransactionBody.PROTOBUF, tx); + return com.hederahashgraph.api.proto.java.SchedulableTransactionBody.parseFrom(bytes); + } catch (InvalidProtocolBufferException e) { + throw new RuntimeException(e); + } + } + public static Key asPbjKey(@NonNull final JKey jKey) { requireNonNull(jKey); try { From d9097486df3db4e2daf1b27a7af2a2e0491db4f5 Mon Sep 17 00:00:00 2001 From: Valentin Tronkov Date: Thu, 29 Feb 2024 23:11:11 +0200 Subject: [PATCH 007/115] feat: Differential testing: Enhance topics store dumper to handle modular representation (#11601) Signed-off-by: Valentin Tronkov <99957253+vtronkov@users.noreply.github.com> --- .../com/hedera/node/app/bbm/topics/Topic.java | 58 ++++++++ .../node/app/bbm/topics/TopicDumpUtils.java | 134 ++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/topics/Topic.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/topics/TopicDumpUtils.java diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/topics/Topic.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/topics/Topic.java new file mode 100644 index 000000000000..6ddfee272215 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/topics/Topic.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.bbm.topics; + +import com.hedera.node.app.service.mono.legacy.core.jproto.JKey; +import com.hedera.node.app.service.mono.state.merkle.MerkleTopic; +import com.hedera.node.app.service.mono.state.submerkle.EntityId; +import com.hedera.node.app.service.mono.state.submerkle.RichInstant; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Objects; + +record Topic( + int number, + @NonNull String memo, + @NonNull RichInstant expirationTimestamp, + boolean deleted, + @NonNull JKey adminKey, + @NonNull JKey submitKey, + @NonNull byte[] runningHash, + long sequenceNumber, + long autoRenewDurationSeconds, + @Nullable EntityId autoRenewAccountId) { + + Topic(@NonNull final MerkleTopic topic) { + this( + topic.getKey().intValue(), + topic.getMemo(), + topic.getExpirationTimestamp(), + topic.isDeleted(), + topic.getAdminKey(), + topic.getSubmitKey(), + null != topic.getRunningHash() ? topic.getRunningHash() : EMPTY_BYTES, + topic.getSequenceNumber(), + topic.getAutoRenewDurationSeconds(), + topic.getAutoRenewAccountId()); + Objects.requireNonNull(memo, "memo"); + Objects.requireNonNull(adminKey, "adminKey"); + Objects.requireNonNull(submitKey, "submitKey"); + Objects.requireNonNull(runningHash, "runningHash"); + } + + static final byte[] EMPTY_BYTES = new byte[0]; +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/topics/TopicDumpUtils.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/topics/TopicDumpUtils.java new file mode 100644 index 000000000000..f7a6484297f2 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/topics/TopicDumpUtils.java @@ -0,0 +1,134 @@ +/* + * 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.bbm.topics; + +import static com.hedera.node.app.bbm.utils.ThingsToStrings.getMaybeStringifyByteString; +import static com.hedera.node.app.bbm.utils.ThingsToStrings.quoteForCsv; + +import com.hedera.node.app.bbm.DumpCheckpoint; +import com.hedera.node.app.bbm.utils.FieldBuilder; +import com.hedera.node.app.bbm.utils.ThingsToStrings; +import com.hedera.node.app.bbm.utils.Writer; +import com.hedera.node.app.service.mono.state.adapters.MerkleMapLike; +import com.hedera.node.app.service.mono.state.merkle.MerkleTopic; +import com.hedera.node.app.service.mono.utils.EntityNum; +import com.swirlds.base.utility.Pair; +import com.swirlds.merkle.map.MerkleMap; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class TopicDumpUtils { + + private static final String FIELD_SEPARATOR = ";"; + private static final Function booleanFormatter = b -> b ? "T" : ""; + private static final Function csvQuote = s -> quoteForCsv(FIELD_SEPARATOR, s); + + private TopicDumpUtils() { + // Utility class + } + + public static void dumpModTopics( + @NonNull final Path path, + @NonNull final MerkleMap topics, + @NonNull final DumpCheckpoint checkpoint) { + + try (@NonNull final var writer = new Writer(path)) { + final var dumpableTopics = gatherTopics(MerkleMapLike.from(topics)); + reportOnTopics(writer, dumpableTopics); + System.out.printf( + "=== mod topics report is %d bytes at checkpoint %s%n", writer.getSize(), checkpoint.name()); + } + } + + public static void dumpMonoTopics( + @NonNull final Path path, + @NonNull final MerkleMap topics, + @NonNull final DumpCheckpoint checkpoint) { + try (@NonNull final var writer = new Writer(path)) { + final var dumpableTopics = gatherTopics(MerkleMapLike.from(topics)); + reportOnTopics(writer, dumpableTopics); + System.out.printf( + "=== mono topics report is %d bytes at checkpoint %s%n", writer.getSize(), checkpoint.name()); + } + } + + private static Map gatherTopics(@NonNull final MerkleMapLike topicsStore) { + final var allTopics = new TreeMap(); + topicsStore.forEachNode((en, mt) -> allTopics.put(en.longValue(), new Topic(mt))); + return allTopics; + } + + private static void reportOnTopics(@NonNull Writer writer, @NonNull Map topics) { + writer.writeln(formatHeader()); + topics.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(e -> formatTopic(writer, e.getValue())); + writer.writeln(""); + } + + @NonNull + private static String formatHeader() { + return fieldFormatters.stream().map(Pair::left).collect(Collectors.joining(FIELD_SEPARATOR)); + } + + @NonNull + private static List>> fieldFormatters = List.of( + Pair.of("number", getFieldFormatter(Topic::number, Object::toString)), + Pair.of("memo", getFieldFormatter(Topic::memo, csvQuote)), + Pair.of("expiry", getFieldFormatter(Topic::expirationTimestamp, ThingsToStrings::toStringOfRichInstant)), + Pair.of("deleted", getFieldFormatter(Topic::deleted, booleanFormatter)), + Pair.of( + "adminKey", + getFieldFormatter(Topic::adminKey, getNullableFormatter(ThingsToStrings::toStringOfJKey))), + Pair.of( + "submitKey", + getFieldFormatter(Topic::submitKey, getNullableFormatter(ThingsToStrings::toStringOfJKey))), + Pair.of("runningHash", getFieldFormatter(Topic::runningHash, getMaybeStringifyByteString(FIELD_SEPARATOR))), + Pair.of("sequenceNumber", getFieldFormatter(Topic::sequenceNumber, Object::toString)), + Pair.of("autoRenewSecs", getFieldFormatter(Topic::autoRenewDurationSeconds, Object::toString)), + Pair.of( + "autoRenewAccount", + getFieldFormatter( + Topic::autoRenewAccountId, getNullableFormatter(ThingsToStrings::toStringOfEntityId)))); + + private static BiConsumer getFieldFormatter( + @NonNull final Function fun, @NonNull final Function formatter) { + return (fb, t) -> formatField(fb, t, fun, formatter); + } + + private static void formatField( + @NonNull final FieldBuilder fb, + @NonNull final Topic topic, + @NonNull final Function fun, + @NonNull final Function formatter) { + fb.append(formatter.apply(fun.apply(topic))); + } + + private static Function getNullableFormatter(@NonNull final Function formatter) { + return t -> null != t ? formatter.apply(t) : ""; + } + + private static void formatTopic(@NonNull final Writer writer, @NonNull final Topic topic) { + final var fb = new FieldBuilder(FIELD_SEPARATOR); + fieldFormatters.stream().map(Pair::right).forEach(ff -> ff.accept(fb, topic)); + writer.writeln(fb); + } +} From da234395fdd72b6c5f220d1c11a6c8d239f15a7e Mon Sep 17 00:00:00 2001 From: Ivan Malygin Date: Thu, 29 Feb 2024 16:32:44 -0500 Subject: [PATCH 008/115] chore: 11770 Removed unused adapter classes. (#11807) Signed-off-by: Ivan Malygin --- .../merkle/adapters/MerkleMapLikeAdapter.java | 140 --------------- .../ScheduledTransactionsAdapter.java | 84 --------- .../adapters/VirtualMapLikeAdapter.java | 151 ---------------- .../hedera-app/src/main/java/module-info.java | 2 - .../ScheduledTransactionsAdapterTest.java | 86 --------- .../adapters/VirtualMapLikeAdapterTest.java | 169 ------------------ .../node/app/test/UtilsConstructorTest.java | 58 ------ 7 files changed, 690 deletions(-) delete mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/merkle/adapters/MerkleMapLikeAdapter.java delete mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/merkle/adapters/ScheduledTransactionsAdapter.java delete mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/merkle/adapters/VirtualMapLikeAdapter.java delete mode 100644 hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/merkle/adapters/ScheduledTransactionsAdapterTest.java delete mode 100644 hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/merkle/adapters/VirtualMapLikeAdapterTest.java delete mode 100644 hedera-node/hedera-app/src/test/java/com/hedera/node/app/test/UtilsConstructorTest.java diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/merkle/adapters/MerkleMapLikeAdapter.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/merkle/adapters/MerkleMapLikeAdapter.java deleted file mode 100644 index f1ebf4e6603a..000000000000 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/merkle/adapters/MerkleMapLikeAdapter.java +++ /dev/null @@ -1,140 +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.state.merkle.adapters; - -import com.hedera.node.app.HederaInjectionComponent; -import com.hedera.node.app.service.mono.context.StateChildrenProvider; -import com.hedera.node.app.service.mono.state.adapters.MerkleMapLike; -import com.hedera.node.app.state.merkle.MerkleHederaState; -import com.hedera.node.app.state.merkle.StateMetadata; -import com.hedera.node.app.state.merkle.memory.InMemoryKey; -import com.hedera.node.app.state.merkle.memory.InMemoryValue; -import com.swirlds.common.crypto.Hash; -import com.swirlds.common.merkle.MerkleNode; -import com.swirlds.common.merkle.utility.Keyed; -import com.swirlds.merkle.map.MerkleMap; -import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.Nullable; -import java.util.Objects; -import java.util.Set; -import java.util.function.BiConsumer; -import java.util.stream.Collectors; - -/** - * Adapts a {@link MerkleMap} constructed by {@code MerkleHederaState#MerkleStates} by "unwrapping" its - * {@link InMemoryKey} and {@link InMemoryValue} containers, so that a - * {@code MerkleMap, InMemoryValue>} appears as a {@code MerkleMapLike}. - * - *

This allows us to use a {@link MerkleHederaState} as a {@link StateChildrenProvider} binding - * within a {@link HederaInjectionComponent} instance, which is important while we are relying heavily on adapters - * around {@code mono-service} components. - */ -public final class MerkleMapLikeAdapter { - private MerkleMapLikeAdapter() { - throw new UnsupportedOperationException("Utility Class"); - } - - public static > MerkleMapLike unwrapping( - final StateMetadata md, final MerkleMap, InMemoryValue> real) { - return new MerkleMapLike<>() { - @Override - public void forEachNode(final BiConsumer action) { - real.forEachNode((final MerkleNode node) -> { - if (node instanceof Keyed) { - final InMemoryValue leaf = node.cast(); - action.accept(leaf.getKey().key(), leaf.getValue()); - } - }); - } - - @Override - public boolean isEmpty() { - return real.isEmpty(); - } - - @Override - public Hash getHash() { - return real.getHash(); - } - - @Override - @SuppressWarnings("unchecked") - public V remove(final Object key) { - final var removed = real.remove(new InMemoryKey<>((K) key)); - return removed != null ? removed.getValue() : null; - } - - @Override - @SuppressWarnings("unchecked") - public V get(final Object key) { - return withKeyIfPresent((K) key, real.get(new InMemoryKey<>((K) key))); - } - - @Override - public V getForModify(final K key) { - return withKeyIfPresent(key, real.getForModify(new InMemoryKey<>(key))); - } - - @Override - public V put(final K key, final V value) { - final var wrappedKey = new InMemoryKey<>(key); - final var replaced = real.put(wrappedKey, new InMemoryValue<>(md, wrappedKey, value)); - return replaced != null ? replaced.getValue() : null; - } - - @Override - public int size() { - return real.size(); - } - - @Override - public Set keySet() { - return real.keySet().stream().map(InMemoryKey::key).collect(Collectors.toSet()); - } - - @Override - @SuppressWarnings("unchecked") - public boolean containsKey(final Object key) { - return real.containsKey(new InMemoryKey<>((K) key)); - } - - @Override - @SuppressWarnings("unchecked") - public V getOrDefault(final Object key, final V defaultValue) { - final var wrappedKey = new InMemoryKey<>((K) key); - final var wrappedDefaultValue = new InMemoryValue<>(md, wrappedKey, defaultValue); - return real.getOrDefault(wrappedKey, wrappedDefaultValue).getValue(); - } - - @Override - public void forEach(final BiConsumer action) { - real.forEach((k, v) -> action.accept(k.key(), v.getValue())); - } - - @Nullable - private V withKeyIfPresent(final @NonNull K key, final @Nullable InMemoryValue present) { - if (present != null) { - final var answer = present.getValue(); - Objects.requireNonNull(answer).setKey(key); - return answer; - } else { - return null; - } - } - }; - } -} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/merkle/adapters/ScheduledTransactionsAdapter.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/merkle/adapters/ScheduledTransactionsAdapter.java deleted file mode 100644 index 3a89879a2c99..000000000000 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/merkle/adapters/ScheduledTransactionsAdapter.java +++ /dev/null @@ -1,84 +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.state.merkle.adapters; - -import com.hedera.node.app.service.mono.state.adapters.MerkleMapLike; -import com.hedera.node.app.service.mono.state.logic.ScheduledTransactions; -import com.hedera.node.app.service.mono.state.merkle.MerkleScheduledTransactionsState; -import com.hedera.node.app.service.mono.state.virtual.EntityNumVirtualKey; -import com.hedera.node.app.service.mono.state.virtual.schedule.ScheduleEqualityVirtualKey; -import com.hedera.node.app.service.mono.state.virtual.schedule.ScheduleEqualityVirtualValue; -import com.hedera.node.app.service.mono.state.virtual.schedule.ScheduleSecondVirtualValue; -import com.hedera.node.app.service.mono.state.virtual.schedule.ScheduleVirtualValue; -import com.hedera.node.app.service.mono.state.virtual.temporal.SecondSinceEpocVirtualKey; - -/** - * A trivial non-Merkle implementation of {@link ScheduledTransactions}. (The {@code mono-service} - * version has Merkle baggage that doesn't make sense here.) - */ -public final class ScheduledTransactionsAdapter implements ScheduledTransactions { - private final MerkleScheduledTransactionsState state; - private final MerkleMapLike byId; - private final MerkleMapLike byExpirySec; - private final MerkleMapLike byEquality; - - public ScheduledTransactionsAdapter( - MerkleScheduledTransactionsState state, - MerkleMapLike byId, - MerkleMapLike byExpirySec, - MerkleMapLike byEquality) { - this.state = state; - this.byId = byId; - this.byExpirySec = byExpirySec; - this.byEquality = byEquality; - } - - @Override - public void setCurrentMinSecond(final long currentMinSecond) { - state.setCurrentMinSecond(currentMinSecond); - } - - @Override - public long getCurrentMinSecond() { - return state.currentMinSecond(); - } - - @Override - public long getNumSchedules() { - return byId.size(); - } - - @Override - public MerkleMapLike byEquality() { - return byEquality; - } - - @Override - public MerkleMapLike byExpirationSecond() { - return byExpirySec; - } - - @Override - public MerkleMapLike byId() { - return byId; - } - - @Override - public MerkleScheduledTransactionsState state() { - return state; - } -} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/merkle/adapters/VirtualMapLikeAdapter.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/merkle/adapters/VirtualMapLikeAdapter.java deleted file mode 100644 index 7627b781f2f8..000000000000 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/merkle/adapters/VirtualMapLikeAdapter.java +++ /dev/null @@ -1,151 +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.state.merkle.adapters; - -import com.hedera.node.app.HederaInjectionComponent; -import com.hedera.node.app.service.mono.context.StateChildrenProvider; -import com.hedera.node.app.service.mono.state.adapters.VirtualMapLike; -import com.hedera.node.app.state.merkle.MerkleHederaState; -import com.hedera.node.app.state.merkle.StateMetadata; -import com.hedera.node.app.state.merkle.disk.OnDiskKey; -import com.hedera.node.app.state.merkle.disk.OnDiskValue; -import com.swirlds.base.utility.Pair; -import com.swirlds.common.crypto.Hash; -import com.swirlds.common.threading.interrupt.InterruptableConsumer; -import com.swirlds.common.threading.manager.ThreadManager; -import com.swirlds.metrics.api.Metrics; -import com.swirlds.virtualmap.VirtualKey; -import com.swirlds.virtualmap.VirtualMap; -import com.swirlds.virtualmap.VirtualMapMigration; -import com.swirlds.virtualmap.VirtualValue; -import com.swirlds.virtualmap.datasource.VirtualDataSource; - -/** - * Adapts a {@link VirtualMap} constructed by {@code MerkleHederaState#MerkleStates} by "unwrapping" its - * {@link OnDiskKey} and {@link OnDiskValue} containers, so that a {@code VirtualMap, OnDiskValue>} - * appears as a {@code VirtualMapLike}. - * - *

This allows us to use a {@link MerkleHederaState} as a {@link StateChildrenProvider} binding - * within a {@link HederaInjectionComponent} instance, which is important while we are relying heavily on adapters - * around {@code mono-service} components. - */ -public final class VirtualMapLikeAdapter { - private VirtualMapLikeAdapter() { - throw new UnsupportedOperationException("Utility Class"); - } - - public static VirtualMapLike unwrapping( - final StateMetadata md, final VirtualMap, OnDiskValue> real) { - return new VirtualMapLike<>() { - @Override - public boolean release() { - return real.release(); - } - - @Override - public Hash getHash() { - return real.getHash(); - } - - @Override - public VirtualDataSource, OnDiskValue> getDataSource() { - return real.getDataSource(); - } - - @Override - public void extractVirtualMapData( - final ThreadManager threadManager, - final InterruptableConsumer> handler, - final int threadCount) - throws InterruptedException { - - final var unwrappingHandler = new InterruptableConsumer, OnDiskValue>>() { - @Override - public void accept(final Pair, OnDiskValue> pair) throws InterruptedException { - handler.accept( - Pair.of(pair.left().getKey(), pair.right().getValue())); - } - }; - VirtualMapMigration.extractVirtualMapData(threadManager, real, unwrappingHandler, threadCount); - } - - @Override - public void extractVirtualMapDataC( - final ThreadManager threadManager, - final InterruptableConsumer> handler, - final int threadCount) - throws InterruptedException { - VirtualMapMigration.extractVirtualMapDataC( - threadManager, - real, - pair -> handler.accept( - Pair.of(pair.left().getKey(), pair.right().getValue())), - threadCount); - } - - @Override - public void registerMetrics(final Metrics metrics) { - real.registerMetrics(metrics); - } - - @Override - public long size() { - return real.size(); - } - - @Override - public void put(final K key, final V value) { - final var onDiskKey = new OnDiskKey<>(md, key); - final var onDiskValue = new OnDiskValue<>(md, value); - real.put(onDiskKey, onDiskValue); - } - - @Override - public V get(final K key) { - final var found = real.get(new OnDiskKey<>(md, key)); - return found != null ? found.getValue() : null; - } - - @Override - public V getForModify(final K key) { - final var mutable = real.getForModify(new OnDiskKey<>(md, key)); - return mutable != null ? mutable.getValue() : null; - } - - @Override - public boolean containsKey(final K key) { - return real.containsKey(new OnDiskKey<>(md, key)); - } - - @Override - public boolean isEmpty() { - return real.isEmpty(); - } - - @Override - public V remove(final K key) { - final var removed = real.remove(new OnDiskKey<>(md, key)); - return removed != null ? removed.getValue() : null; - } - - @Override - public void warm(final K key) { - real.warm(new OnDiskKey<>(md, key)); - } - }; - } -} 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 040c9a5cb83e..7fe840e1ebbf 100644 --- a/hedera-node/hedera-app/src/main/java/module-info.java +++ b/hedera-node/hedera-app/src/main/java/module-info.java @@ -68,8 +68,6 @@ com.swirlds.common; exports com.hedera.node.app.authorization to com.swirlds.platform.core; - exports com.hedera.node.app.state.merkle.adapters to - com.swirlds.platform.core; exports com.hedera.node.app.fees to com.swirlds.platform.core; exports com.hedera.node.app.fees.congestion to diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/merkle/adapters/ScheduledTransactionsAdapterTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/merkle/adapters/ScheduledTransactionsAdapterTest.java deleted file mode 100644 index 2fc57d0fa8f4..000000000000 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/merkle/adapters/ScheduledTransactionsAdapterTest.java +++ /dev/null @@ -1,86 +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.state.merkle.adapters; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.BDDMockito.given; - -import com.hedera.node.app.service.mono.state.adapters.MerkleMapLike; -import com.hedera.node.app.service.mono.state.merkle.MerkleScheduledTransactionsState; -import com.hedera.node.app.service.mono.state.virtual.EntityNumVirtualKey; -import com.hedera.node.app.service.mono.state.virtual.schedule.ScheduleEqualityVirtualKey; -import com.hedera.node.app.service.mono.state.virtual.schedule.ScheduleEqualityVirtualValue; -import com.hedera.node.app.service.mono.state.virtual.schedule.ScheduleSecondVirtualValue; -import com.hedera.node.app.service.mono.state.virtual.schedule.ScheduleVirtualValue; -import com.hedera.node.app.service.mono.state.virtual.temporal.SecondSinceEpocVirtualKey; -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.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -class ScheduledTransactionsAdapterTest { - - @Mock - private MerkleScheduledTransactionsState state; - - @Mock - private MerkleMapLike byId; - - @Mock - private MerkleMapLike byExpirySec; - - @Mock - private MerkleMapLike byEquality; - - private ScheduledTransactionsAdapter subject; - - @BeforeEach - void setUp() { - subject = new ScheduledTransactionsAdapter(state, byId, byExpirySec, byEquality); - } - - @Test - void delegatesSetsMinSecond() { - subject.setCurrentMinSecond(1234L); - Mockito.verify(state).setCurrentMinSecond(1234L); - } - - @Test - void delegatesGetsMinSecond() { - given(state.currentMinSecond()).willReturn(1234L); - - assertEquals(1234L, subject.getCurrentMinSecond()); - } - - @Test - void delegatesNumSchedulesToIds() { - given(byId.size()).willReturn(1234); - - assertEquals(1234, subject.getNumSchedules()); - } - - @Test - void gettersWork() { - assertSame(byEquality, subject.byEquality()); - assertSame(byExpirySec, subject.byExpirationSecond()); - assertSame(byId, subject.byId()); - assertSame(state, subject.state()); - } -} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/merkle/adapters/VirtualMapLikeAdapterTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/merkle/adapters/VirtualMapLikeAdapterTest.java deleted file mode 100644 index 795bfdb31273..000000000000 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/merkle/adapters/VirtualMapLikeAdapterTest.java +++ /dev/null @@ -1,169 +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.state.merkle.adapters; - -import static com.hedera.node.app.service.token.impl.TokenServiceImpl.NFTS_KEY; -import static com.swirlds.common.io.utility.TemporaryFileBuilder.buildTemporaryDirectory; -import static com.swirlds.common.threading.manager.AdHocThreadManager.getStaticThreadManager; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.verify; - -import com.hedera.hapi.node.base.SemanticVersion; -import com.hedera.node.app.service.mono.state.adapters.VirtualMapLike; -import com.hedera.node.app.service.mono.state.codec.MonoMapCodecAdapter; -import com.hedera.node.app.service.mono.state.submerkle.EntityId; -import com.hedera.node.app.service.mono.state.submerkle.RichInstant; -import com.hedera.node.app.service.mono.state.virtual.UniqueTokenKey; -import com.hedera.node.app.service.mono.state.virtual.UniqueTokenKeySerializer; -import com.hedera.node.app.service.mono.state.virtual.UniqueTokenValue; -import com.hedera.node.app.service.mono.state.virtual.UniqueTokenValueSerializer; -import com.hedera.node.app.spi.state.Schema; -import com.hedera.node.app.spi.state.StateDefinition; -import com.hedera.node.app.state.merkle.StateMetadata; -import com.hedera.node.app.state.merkle.disk.OnDiskKey; -import com.hedera.node.app.state.merkle.disk.OnDiskKeySerializer; -import com.hedera.node.app.state.merkle.disk.OnDiskValue; -import com.hedera.node.app.state.merkle.disk.OnDiskValueSerializer; -import com.swirlds.base.utility.Pair; -import com.swirlds.common.crypto.DigestType; -import com.swirlds.common.threading.interrupt.InterruptableConsumer; -import com.swirlds.merkledb.MerkleDbDataSourceBuilder; -import com.swirlds.merkledb.MerkleDbTableConfig; -import com.swirlds.metrics.api.Metrics; -import com.swirlds.virtualmap.VirtualMap; -import edu.umd.cs.findbugs.annotations.NonNull; -import java.io.IOException; -import java.util.Set; -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 VirtualMapLikeAdapterTest { - private static final UniqueTokenKey A_KEY = new UniqueTokenKey(1234L, 5678L); - private static final UniqueTokenKey B_KEY = new UniqueTokenKey(2345L, 6789L); - private static final UniqueTokenKey C_KEY = new UniqueTokenKey(3456L, 7890L); - private static final UniqueTokenKey D_KEY = new UniqueTokenKey(4567L, 8901L); - private static final UniqueTokenKey Z_KEY = new UniqueTokenKey(7890L, 1234L); - private static final UniqueTokenValue A_VALUE = - new UniqueTokenValue(1L, 2L, "A".getBytes(), new RichInstant(1L, 2)); - private static final UniqueTokenValue B_VALUE = - new UniqueTokenValue(2L, 3L, "B".getBytes(), new RichInstant(2L, 3)); - private static final UniqueTokenValue C_VALUE = - new UniqueTokenValue(3L, 4L, "C".getBytes(), new RichInstant(3L, 4)); - private static final UniqueTokenValue D_VALUE = - new UniqueTokenValue(4L, 5L, "D".getBytes(), new RichInstant(4L, 5)); - - private VirtualMap, OnDiskValue> real; - - private VirtualMapLike subject; - - private StateMetadata metadata; - - @Mock - private Metrics metrics; - - @Mock - private InterruptableConsumer> consumer; - - @Test - void methodsDelegateAsExpected() throws IOException, InterruptedException { - setupRealAndSubject(); - - assertSame(real.getDataSource(), subject.getDataSource()); - assertSame(real.getHash(), subject.getHash()); - assertTrue(subject.isEmpty()); - - putToReal(A_KEY, A_VALUE); - putToReal(B_KEY, B_VALUE); - putToReal(C_KEY, C_VALUE); - - assertNull(subject.get(Z_KEY)); - assertNull(subject.remove(Z_KEY)); - assertNull(subject.getForModify(Z_KEY)); - - subject.extractVirtualMapData(getStaticThreadManager(), consumer, 1); - verify(consumer).accept(Pair.of(A_KEY, A_VALUE)); - verify(consumer).accept(Pair.of(B_KEY, B_VALUE)); - verify(consumer).accept(Pair.of(C_KEY, C_VALUE)); - - assertEquals(3, subject.size()); - - assertFalse(subject.containsKey(D_KEY)); - subject.put(D_KEY, D_VALUE); - assertTrue(subject.containsKey(D_KEY)); - assertEquals(D_VALUE, subject.get(D_KEY)); - subject.remove(B_KEY); - assertFalse(subject.containsKey(B_KEY)); - - final var mutableA = subject.getForModify(A_KEY); - mutableA.setOwner(EntityId.fromNum(666L)); - - assertDoesNotThrow(() -> subject.registerMetrics(metrics)); - - real.copy(); - subject.release(); - } - - @SuppressWarnings({"rawtypes", "unchecked"}) - private void setupRealAndSubject() throws IOException { - final var schema = justNftsSchema(); - final var nftsDef = schema.statesToCreate().iterator().next(); - metadata = new StateMetadata<>("REAL", schema, nftsDef); - - final var keySerializer = new OnDiskKeySerializer(metadata); - final var valueSerializer = new OnDiskValueSerializer(metadata); - final var tableConfig = new MerkleDbTableConfig<>( - (short) 1, DigestType.SHA_384, (short) 1, keySerializer, (short) 1, valueSerializer) - .maxNumberOfKeys(1_024); - - final var dsBuilder = new MerkleDbDataSourceBuilder<>(buildTemporaryDirectory("merkledb"), tableConfig); - real = new VirtualMap<>("REAL", dsBuilder); - subject = VirtualMapLikeAdapter.unwrapping(metadata, real); - } - - private static final SemanticVersion CURRENT_VERSION = - SemanticVersion.newBuilder().minor(34).build(); - - private Schema justNftsSchema() { - return new Schema(CURRENT_VERSION) { - @NonNull - @Override - public Set statesToCreate() { - return Set.of(onDiskNftsDef()); - } - }; - } - - private void putToReal(final UniqueTokenKey key, final UniqueTokenValue value) { - real.put(new OnDiskKey<>(metadata, key), new OnDiskValue<>(metadata, value)); - } - - private StateDefinition onDiskNftsDef() { - final var keySerdes = MonoMapCodecAdapter.codecForVirtualKey( - UniqueTokenKey.CURRENT_VERSION, UniqueTokenKey::new, new UniqueTokenKeySerializer()); - final var valueSerdes = MonoMapCodecAdapter.codecForVirtualValue( - UniqueTokenValue.CURRENT_VERSION, UniqueTokenValue::new, new UniqueTokenValueSerializer()); - return StateDefinition.onDisk(NFTS_KEY, keySerdes, valueSerdes, 1_024); - } -} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/test/UtilsConstructorTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/test/UtilsConstructorTest.java deleted file mode 100644 index c04d47ab7229..000000000000 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/test/UtilsConstructorTest.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (C) 2021-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.test; - -import com.hedera.node.app.state.merkle.adapters.MerkleMapLikeAdapter; -import com.hedera.node.app.state.merkle.adapters.VirtualMapLikeAdapter; -import java.lang.reflect.InvocationTargetException; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -class UtilsConstructorTest { - private static final Set> toBeTested = - new HashSet<>(Arrays.asList(MerkleMapLikeAdapter.class, VirtualMapLikeAdapter.class)); - - @Test - void throwsInConstructor() { - for (final var clazz : toBeTested) { - assertFor(clazz); - } - } - - private static final String UNEXPECTED_THROW = "Unexpected `%s` was thrown in `%s` constructor!"; - private static final String NO_THROW = "No exception was thrown in `%s` constructor!"; - - private void assertFor(final Class clazz) { - try { - final var constructor = clazz.getDeclaredConstructor(); - constructor.setAccessible(true); - - constructor.newInstance(); - } catch (final InvocationTargetException expected) { - final var cause = expected.getCause(); - Assertions.assertTrue( - cause instanceof UnsupportedOperationException, String.format(UNEXPECTED_THROW, cause, clazz)); - return; - } catch (final Exception e) { - Assertions.fail(String.format(UNEXPECTED_THROW, e, clazz)); - } - Assertions.fail(String.format(NO_THROW, clazz)); - } -} From ab5c2a4e683cd5a4396eb88dd906f38af281c040 Mon Sep 17 00:00:00 2001 From: Lazar Petrovic Date: Fri, 1 Mar 2024 11:01:15 +0100 Subject: [PATCH 009/115] fix: consensus test flake (#11817) Signed-off-by: Lazar Petrovic --- .../platform/test/consensus/ConsensusTestDefinitions.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/ConsensusTestDefinitions.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/ConsensusTestDefinitions.java index afacaa5663f5..b258a47b28b0 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/ConsensusTestDefinitions.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/ConsensusTestDefinitions.java @@ -573,7 +573,9 @@ public static void removeNode(@NonNull final TestInput input) { orchestrator2.generateEvents(0.5); orchestrator2.validate( - Validations.standard().ratios(EventRatioValidation.blank().setMinimumConsensusRatio(0.5))); + // this used to be set to 0.5, but then a test failed because it had a ratio of 0.4999 + // the number are a bit arbitrary, but the goal is to validate that events are reaching consensus + Validations.standard().ratios(EventRatioValidation.blank().setMinimumConsensusRatio(0.4))); } public static void syntheticSnapshot(@NonNull final TestInput input) { From 56530bf32e8324dcfda93786c76449ddd55b58d7 Mon Sep 17 00:00:00 2001 From: Kore Aguda <157432197+kfa-aguda@users.noreply.github.com> Date: Fri, 1 Mar 2024 08:19:56 -0600 Subject: [PATCH 010/115] refactor: Flatten gossip class 11506 (#11827) Signed-off-by: Kore Aguda --- .../com/swirlds/platform/SwirldsPlatform.java | 11 +- .../platform/gossip/AbstractGossip.java | 308 ----------------- .../com/swirlds/platform/gossip/Gossip.java | 57 ---- .../platform/gossip/GossipFactory.java | 170 ---------- .../gossip/{sync => }/SyncGossip.java | 310 ++++++++++++++--- .../gossip/chatter/ChatterGossip.java | 316 ------------------ .../gossip/sync/SingleNodeSyncGossip.java | 140 -------- 7 files changed, 265 insertions(+), 1047 deletions(-) delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/AbstractGossip.java delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/Gossip.java delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/GossipFactory.java rename platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/{sync => }/SyncGossip.java (58%) delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/chatter/ChatterGossip.java delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/sync/SingleNodeSyncGossip.java 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 8b2c9b0483ee..0c4c886d8883 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 @@ -100,10 +100,9 @@ import com.swirlds.platform.eventhandling.EventConfig; import com.swirlds.platform.eventhandling.TransactionPool; import com.swirlds.platform.gossip.DefaultIntakeEventCounter; -import com.swirlds.platform.gossip.Gossip; -import com.swirlds.platform.gossip.GossipFactory; import com.swirlds.platform.gossip.IntakeEventCounter; import com.swirlds.platform.gossip.NoOpIntakeEventCounter; +import com.swirlds.platform.gossip.SyncGossip; import com.swirlds.platform.gossip.shadowgraph.Shadowgraph; import com.swirlds.platform.gossip.sync.config.SyncConfig; import com.swirlds.platform.gui.GuiPlatformAccessor; @@ -269,7 +268,7 @@ public class SwirldsPlatform implements Platform { /** * Responsible for transmitting and receiving events from the network. */ - private final Gossip gossip; + private final SyncGossip gossip; /** * The round of the most recent reconnect state received, or {@link UptimeData#NO_ROUND} if no reconnect state has @@ -720,7 +719,7 @@ public class SwirldsPlatform implements Platform { } }; - gossip = GossipFactory.buildGossip( + gossip = new SyncGossip( platformContext, threadManager, time, @@ -741,7 +740,7 @@ public class SwirldsPlatform implements Platform { this::loadReconnectState, this::clearAllPipelines, intakeEventCounter, - () -> emergencyState.getState("emergency reconnect")); + () -> emergencyState.getState("emergency reconnect")) {}; consensusRef.set(new ConsensusImpl( platformContext.getConfiguration().getConfigData(ConsensusConfig.class), @@ -898,8 +897,6 @@ private void loadStateIntoConsensus(@NonNull final SignedState signedState) { ancientMode); shadowGraph.startWithEventWindow(eventWindow); - - gossip.loadFromSignedState(signedState); } /** diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/AbstractGossip.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/AbstractGossip.java deleted file mode 100644 index e70a09fe8ae1..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/AbstractGossip.java +++ /dev/null @@ -1,308 +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.swirlds.platform.gossip; - -import static com.swirlds.platform.SwirldsPlatform.PLATFORM_THREAD_POOL_NAME; - -import com.swirlds.base.state.LifecyclePhase; -import com.swirlds.base.state.Startable; -import com.swirlds.base.time.Time; -import com.swirlds.common.context.PlatformContext; -import com.swirlds.common.crypto.config.CryptoConfig; -import com.swirlds.common.merkle.synchronization.config.ReconnectConfig; -import com.swirlds.common.platform.NodeId; -import com.swirlds.common.threading.framework.config.StoppableThreadConfiguration; -import com.swirlds.common.threading.manager.ThreadManager; -import com.swirlds.platform.config.BasicConfig; -import com.swirlds.platform.config.StateConfig; -import com.swirlds.platform.config.ThreadConfig; -import com.swirlds.platform.crypto.KeysAndCerts; -import com.swirlds.platform.eventhandling.EventConfig; -import com.swirlds.platform.gossip.sync.SyncManagerImpl; -import com.swirlds.platform.metrics.ReconnectMetrics; -import com.swirlds.platform.network.Connection; -import com.swirlds.platform.network.ConnectionTracker; -import com.swirlds.platform.network.NetworkMetrics; -import com.swirlds.platform.network.SocketConfig; -import com.swirlds.platform.network.connectivity.ConnectionServer; -import com.swirlds.platform.network.connectivity.InboundConnectionHandler; -import com.swirlds.platform.network.connectivity.OutboundConnectionCreator; -import com.swirlds.platform.network.connectivity.SocketFactory; -import com.swirlds.platform.network.connectivity.TcpFactory; -import com.swirlds.platform.network.connectivity.TlsFactory; -import com.swirlds.platform.network.topology.NetworkTopology; -import com.swirlds.platform.network.topology.StaticConnectionManagers; -import com.swirlds.platform.network.topology.StaticTopology; -import com.swirlds.platform.reconnect.ReconnectHelper; -import com.swirlds.platform.reconnect.ReconnectLearnerFactory; -import com.swirlds.platform.reconnect.ReconnectLearnerThrottle; -import com.swirlds.platform.reconnect.ReconnectThrottle; -import com.swirlds.platform.state.SwirldStateManager; -import com.swirlds.platform.state.nexus.SignedStateNexus; -import com.swirlds.platform.state.signed.SignedState; -import com.swirlds.platform.system.PlatformConstructionException; -import com.swirlds.platform.system.SoftwareVersion; -import com.swirlds.platform.system.address.Address; -import com.swirlds.platform.system.address.AddressBook; -import com.swirlds.platform.system.status.StatusActionSubmitter; -import edu.umd.cs.findbugs.annotations.NonNull; -import java.io.IOException; -import java.security.KeyManagementException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.UnrecoverableKeyException; -import java.security.cert.CertificateException; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.function.Consumer; -import java.util.function.LongSupplier; - -/** - * Boilerplate code for gossip. - */ -public abstract class AbstractGossip implements ConnectionTracker, Gossip { - private LifecyclePhase lifecyclePhase = LifecyclePhase.NOT_STARTED; - - protected final PlatformContext platformContext; - protected final AddressBook addressBook; - protected final NodeId selfId; - protected final NetworkTopology topology; - protected final NetworkMetrics networkMetrics; - protected final ReconnectHelper reconnectHelper; - protected final StaticConnectionManagers connectionManagers; - protected final FallenBehindManagerImpl fallenBehindManager; - protected final SyncManagerImpl syncManager; - protected final ReconnectThrottle reconnectThrottle; - protected final ReconnectMetrics reconnectMetrics; - - /** - * Enables submitting platform status actions - */ - protected final StatusActionSubmitter statusActionSubmitter; - - protected final List thingsToStart = new ArrayList<>(); - - /** - * Builds the gossip engine, depending on which flavor is requested in the configuration. - * - * @param platformContext the platform context - * @param threadManager the thread manager - * @param time the time object used to get the current time - * @param keysAndCerts private keys and public certificates - * @param addressBook the current address book - * @param selfId this node's ID - * @param appVersion the version of the app - * @param intakeQueueSizeSupplier a supplier for the size of the event intake queue - * @param swirldStateManager manages the mutable state - * @param latestCompleteState holds the latest signed state that has enough signatures to be verifiable - * @param statusActionSubmitter enables submitting platform status actions - * @param loadReconnectState a method that should be called when a state from reconnect is obtained - * @param clearAllPipelinesForReconnect this method should be called to clear all pipelines prior to a reconnect - */ - protected AbstractGossip( - @NonNull final PlatformContext platformContext, - @NonNull final ThreadManager threadManager, - @NonNull final Time time, - @NonNull final KeysAndCerts keysAndCerts, - @NonNull final AddressBook addressBook, - @NonNull final NodeId selfId, - @NonNull final SoftwareVersion appVersion, - @NonNull final LongSupplier intakeQueueSizeSupplier, - @NonNull final SwirldStateManager swirldStateManager, - @NonNull final SignedStateNexus latestCompleteState, - @NonNull final StatusActionSubmitter statusActionSubmitter, - @NonNull final Consumer loadReconnectState, - @NonNull final Runnable clearAllPipelinesForReconnect) { - - this.platformContext = Objects.requireNonNull(platformContext); - this.addressBook = Objects.requireNonNull(addressBook); - this.selfId = Objects.requireNonNull(selfId); - this.statusActionSubmitter = Objects.requireNonNull(statusActionSubmitter); - Objects.requireNonNull(time); - - final ThreadConfig threadConfig = platformContext.getConfiguration().getConfigData(ThreadConfig.class); - - final BasicConfig basicConfig = platformContext.getConfiguration().getConfigData(BasicConfig.class); - final CryptoConfig cryptoConfig = platformContext.getConfiguration().getConfigData(CryptoConfig.class); - final SocketConfig socketConfig = platformContext.getConfiguration().getConfigData(SocketConfig.class); - - topology = new StaticTopology(addressBook, selfId, basicConfig.numConnections()); - - final SocketFactory socketFactory = socketFactory(keysAndCerts, cryptoConfig, socketConfig); - // create an instance that can create new outbound connections - final OutboundConnectionCreator connectionCreator = new OutboundConnectionCreator( - platformContext, selfId, this, socketFactory, addressBook, shouldDoVersionCheck(), appVersion); - connectionManagers = new StaticConnectionManagers(topology, connectionCreator); - final InboundConnectionHandler inboundConnectionHandler = new InboundConnectionHandler( - platformContext, - this, - selfId, - addressBook, - connectionManagers::newConnection, - shouldDoVersionCheck(), - appVersion, - time); - // allow other members to create connections to me - final Address address = addressBook.getAddress(selfId); - final ConnectionServer connectionServer = new ConnectionServer( - threadManager, - address.getListenAddressIpv4(), - address.getListenPort(), - socketFactory, - inboundConnectionHandler::handle); - thingsToStart.add(new StoppableThreadConfiguration<>(threadManager) - .setPriority(threadConfig.threadPrioritySync()) - .setNodeId(selfId) - .setComponent(PLATFORM_THREAD_POOL_NAME) - .setThreadName("connectionServer") - .setWork(connectionServer) - .build()); - - fallenBehindManager = buildFallenBehindManager(); - - syncManager = new SyncManagerImpl( - platformContext, - intakeQueueSizeSupplier, - fallenBehindManager, - platformContext.getConfiguration().getConfigData(EventConfig.class)); - - final ReconnectConfig reconnectConfig = - platformContext.getConfiguration().getConfigData(ReconnectConfig.class); - reconnectThrottle = new ReconnectThrottle(reconnectConfig, time); - - networkMetrics = new NetworkMetrics(platformContext.getMetrics(), selfId, addressBook); - platformContext.getMetrics().addUpdater(networkMetrics::update); - - reconnectMetrics = new ReconnectMetrics(platformContext.getMetrics(), addressBook); - - final StateConfig stateConfig = platformContext.getConfiguration().getConfigData(StateConfig.class); - reconnectHelper = new ReconnectHelper( - this::pause, - clearAllPipelinesForReconnect::run, - swirldStateManager::getConsensusState, - latestCompleteState::getRound, - new ReconnectLearnerThrottle(time, selfId, reconnectConfig), - loadReconnectState, - new ReconnectLearnerFactory( - platformContext, - threadManager, - addressBook, - reconnectConfig.asyncStreamTimeout(), - reconnectMetrics), - stateConfig); - } - - private static SocketFactory socketFactory( - @NonNull final KeysAndCerts keysAndCerts, - @NonNull final CryptoConfig cryptoConfig, - @NonNull final SocketConfig socketConfig) { - Objects.requireNonNull(keysAndCerts); - Objects.requireNonNull(cryptoConfig); - Objects.requireNonNull(socketConfig); - - if (!socketConfig.useTLS()) { - return new TcpFactory(socketConfig); - } - try { - return new TlsFactory(keysAndCerts, socketConfig, cryptoConfig); - } catch (final NoSuchAlgorithmException - | UnrecoverableKeyException - | KeyStoreException - | KeyManagementException - | CertificateException - | IOException e) { - throw new PlatformConstructionException("A problem occurred while creating the SocketFactory", e); - } - } - - /** - * Build the fallen behind manager. - */ - @NonNull - protected abstract FallenBehindManagerImpl buildFallenBehindManager(); - - /** - * {@inheritDoc} - */ - @NonNull - @Override - public LifecyclePhase getLifecyclePhase() { - return lifecyclePhase; - } - - /** - * {@inheritDoc} - */ - @Override - public void start() { - throwIfNotInPhase(LifecyclePhase.NOT_STARTED); - lifecyclePhase = LifecyclePhase.STARTED; - thingsToStart.forEach(Startable::start); - } - - /** - * {@inheritDoc} - */ - @Override - public void stop() { - throwIfNotInPhase(LifecyclePhase.STARTED); - lifecyclePhase = LifecyclePhase.STOPPED; - syncManager.haltRequestedObserver("stopping gossip"); - } - - /** - * {@inheritDoc} - */ - @Override - public void resetFallenBehind() { - syncManager.resetFallenBehind(); - } - - /** - * {@inheritDoc} - */ - @Override - public boolean hasFallenBehind() { - return syncManager.hasFallenBehind(); - } - - /** - * {@inheritDoc} - */ - @Override - public void newConnectionOpened(@NonNull final Connection sc) { - Objects.requireNonNull(sc); - networkMetrics.connectionEstablished(sc); - } - - /** - * {@inheritDoc} - */ - @Override - public void connectionClosed(final boolean outbound, @NonNull final Connection conn) { - Objects.requireNonNull(conn); - networkMetrics.recordDisconnect(conn); - } - - /** - * Should the network layer do a version check prior to initiating a connection? - * - * @return true if a version check should be done - */ - protected abstract boolean shouldDoVersionCheck(); -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/Gossip.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/Gossip.java deleted file mode 100644 index b86506392510..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/Gossip.java +++ /dev/null @@ -1,57 +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.swirlds.platform.gossip; - -import com.swirlds.base.state.Lifecycle; -import com.swirlds.platform.network.ConnectionTracker; -import com.swirlds.platform.state.signed.SignedState; -import edu.umd.cs.findbugs.annotations.NonNull; - -/** - * This object is responsible for talking to other nodes and distributing events. - */ -public interface Gossip extends ConnectionTracker, Lifecycle { - - /** - * Load data from a signed state. - * - * @param signedState the signed state to load from - */ - void loadFromSignedState(@NonNull final SignedState signedState); - - /** - * This method is called when the node has finished a reconnect. - */ - void resetFallenBehind(); - - /** - * Check if we have fallen behind. - * - * @return true if we have fallen behind - */ - boolean hasFallenBehind(); - - /** - * Stop gossiping until {@link #resume()} is called. If called when already paused then this has no effect. - */ - void pause(); - - /** - * Resume gossiping. If called when already running then this has no effect. - */ - void resume(); -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/GossipFactory.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/GossipFactory.java deleted file mode 100644 index 07f070083a44..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/GossipFactory.java +++ /dev/null @@ -1,170 +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.swirlds.platform.gossip; - -import static com.swirlds.logging.legacy.LogMarker.STARTUP; - -import com.swirlds.base.time.Time; -import com.swirlds.common.context.PlatformContext; -import com.swirlds.common.crypto.Hash; -import com.swirlds.common.notification.NotificationEngine; -import com.swirlds.common.platform.NodeId; -import com.swirlds.common.threading.manager.ThreadManager; -import com.swirlds.platform.crypto.KeysAndCerts; -import com.swirlds.platform.event.GossipEvent; -import com.swirlds.platform.gossip.shadowgraph.Shadowgraph; -import com.swirlds.platform.gossip.sync.SingleNodeSyncGossip; -import com.swirlds.platform.gossip.sync.SyncGossip; -import com.swirlds.platform.metrics.SyncMetrics; -import com.swirlds.platform.recovery.EmergencyRecoveryManager; -import com.swirlds.platform.state.SwirldStateManager; -import com.swirlds.platform.state.nexus.SignedStateNexus; -import com.swirlds.platform.state.signed.ReservedSignedState; -import com.swirlds.platform.state.signed.SignedState; -import com.swirlds.platform.system.SoftwareVersion; -import com.swirlds.platform.system.address.AddressBook; -import com.swirlds.platform.system.status.PlatformStatusManager; -import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.Nullable; -import java.util.Objects; -import java.util.function.Consumer; -import java.util.function.LongSupplier; -import java.util.function.Supplier; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -/** - * Builds the gossip engine, depending on which flavor is requested in the configuration. - */ -public final class GossipFactory { - - private static final Logger logger = LogManager.getLogger(GossipFactory.class); - - private GossipFactory() {} - - /** - * Builds the gossip engine, depending on which flavor is requested in the configuration. - * - * @param platformContext the platform context - * @param threadManager the thread manager - * @param time the wall clock time - * @param keysAndCerts private keys and public certificates - * @param notificationEngine used to send notifications to the app - * @param addressBook the current address book - * @param selfId this node's ID - * @param appVersion the version of the app - * @param epochHash the epoch hash of the initial state - * @param shadowGraph contains non-ancient events - * @param emergencyRecoveryManager handles emergency recovery - * @param receivedEventHandler handles events received from other nodes - * @param intakeQueueSizeSupplier a supplier for the size of the event intake queue - * @param swirldStateManager manages the mutable state - * @param latestCompleteState holds the latest signed state that has enough signatures to be verifiable - * @param syncMetrics metrics for sync - * @param platformStatusManager the platform status manager - * @param loadReconnectState a method that should be called when a state from reconnect is obtained - * @param clearAllPipelinesForReconnect this method should be called to clear all pipelines prior to a reconnect - * @param intakeEventCounter keeps track of the number of events in the intake pipeline from each peer - * @param emergencyStateSupplier returns the emergency state if available - * @return the gossip engine - */ - public static Gossip buildGossip( - @NonNull final PlatformContext platformContext, - @NonNull final ThreadManager threadManager, - @NonNull final Time time, - @NonNull final KeysAndCerts keysAndCerts, - @NonNull final NotificationEngine notificationEngine, - @NonNull final AddressBook addressBook, - @NonNull final NodeId selfId, - @NonNull final SoftwareVersion appVersion, - @Nullable final Hash epochHash, - @NonNull final Shadowgraph shadowGraph, - @NonNull final EmergencyRecoveryManager emergencyRecoveryManager, - @NonNull final Consumer receivedEventHandler, - @NonNull final LongSupplier intakeQueueSizeSupplier, - @NonNull final SwirldStateManager swirldStateManager, - @NonNull final SignedStateNexus latestCompleteState, - @NonNull final SyncMetrics syncMetrics, - @NonNull final PlatformStatusManager platformStatusManager, - @NonNull final Consumer loadReconnectState, - @NonNull final Runnable clearAllPipelinesForReconnect, - @NonNull final IntakeEventCounter intakeEventCounter, - @NonNull final Supplier emergencyStateSupplier) { - - Objects.requireNonNull(platformContext); - Objects.requireNonNull(threadManager); - Objects.requireNonNull(time); - Objects.requireNonNull(keysAndCerts); - Objects.requireNonNull(notificationEngine); - Objects.requireNonNull(addressBook); - Objects.requireNonNull(selfId); - Objects.requireNonNull(appVersion); - Objects.requireNonNull(shadowGraph); - Objects.requireNonNull(emergencyRecoveryManager); - Objects.requireNonNull(receivedEventHandler); - Objects.requireNonNull(intakeQueueSizeSupplier); - Objects.requireNonNull(swirldStateManager); - Objects.requireNonNull(latestCompleteState); - Objects.requireNonNull(syncMetrics); - Objects.requireNonNull(platformStatusManager); - Objects.requireNonNull(loadReconnectState); - Objects.requireNonNull(clearAllPipelinesForReconnect); - Objects.requireNonNull(intakeEventCounter); - - if (addressBook.getSize() == 1) { - logger.info(STARTUP.getMarker(), "Using SingleNodeSyncGossip"); - return new SingleNodeSyncGossip( - platformContext, - threadManager, - time, - keysAndCerts, - addressBook, - selfId, - appVersion, - intakeQueueSizeSupplier, - swirldStateManager, - latestCompleteState, - platformStatusManager, - loadReconnectState, - clearAllPipelinesForReconnect); - } else { - logger.info(STARTUP.getMarker(), "Using SyncGossip"); - return new SyncGossip( - platformContext, - threadManager, - time, - keysAndCerts, - notificationEngine, - addressBook, - selfId, - appVersion, - epochHash, - shadowGraph, - emergencyRecoveryManager, - receivedEventHandler, - intakeQueueSizeSupplier, - swirldStateManager, - latestCompleteState, - syncMetrics, - platformStatusManager, - loadReconnectState, - clearAllPipelinesForReconnect, - intakeEventCounter, - emergencyStateSupplier); - } - } -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/sync/SyncGossip.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/SyncGossip.java similarity index 58% rename from platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/sync/SyncGossip.java rename to platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/SyncGossip.java index 86808421c57d..3176e641bbbe 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/sync/SyncGossip.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/SyncGossip.java @@ -14,14 +14,17 @@ * limitations under the License. */ -package com.swirlds.platform.gossip.sync; +package com.swirlds.platform.gossip; import static com.swirlds.platform.SwirldsPlatform.PLATFORM_THREAD_POOL_NAME; +import com.swirlds.base.state.Lifecycle; import com.swirlds.base.state.LifecyclePhase; +import com.swirlds.base.state.Startable; import com.swirlds.base.time.Time; import com.swirlds.common.context.PlatformContext; import com.swirlds.common.crypto.Hash; +import com.swirlds.common.crypto.config.CryptoConfig; import com.swirlds.common.merkle.synchronization.config.ReconnectConfig; import com.swirlds.common.notification.NotificationEngine; import com.swirlds.common.platform.NodeId; @@ -31,38 +34,62 @@ import com.swirlds.common.threading.pool.CachedPoolParallelExecutor; import com.swirlds.common.threading.pool.ParallelExecutor; import com.swirlds.platform.config.BasicConfig; +import com.swirlds.platform.config.StateConfig; +import com.swirlds.platform.config.ThreadConfig; import com.swirlds.platform.crypto.KeysAndCerts; import com.swirlds.platform.event.GossipEvent; import com.swirlds.platform.eventhandling.EventConfig; -import com.swirlds.platform.gossip.AbstractGossip; -import com.swirlds.platform.gossip.FallenBehindManagerImpl; -import com.swirlds.platform.gossip.IntakeEventCounter; -import com.swirlds.platform.gossip.ProtocolConfig; -import com.swirlds.platform.gossip.SyncPermitProvider; import com.swirlds.platform.gossip.shadowgraph.Shadowgraph; import com.swirlds.platform.gossip.shadowgraph.ShadowgraphSynchronizer; +import com.swirlds.platform.gossip.sync.SyncManagerImpl; import com.swirlds.platform.gossip.sync.config.SyncConfig; import com.swirlds.platform.gossip.sync.protocol.SyncProtocol; import com.swirlds.platform.heartbeats.HeartbeatProtocol; +import com.swirlds.platform.metrics.ReconnectMetrics; import com.swirlds.platform.metrics.SyncMetrics; +import com.swirlds.platform.network.Connection; +import com.swirlds.platform.network.ConnectionTracker; +import com.swirlds.platform.network.NetworkMetrics; +import com.swirlds.platform.network.SocketConfig; import com.swirlds.platform.network.communication.NegotiationProtocols; import com.swirlds.platform.network.communication.NegotiatorThread; import com.swirlds.platform.network.communication.handshake.HashCompareHandshake; import com.swirlds.platform.network.communication.handshake.VersionCompareHandshake; +import com.swirlds.platform.network.connectivity.ConnectionServer; +import com.swirlds.platform.network.connectivity.InboundConnectionHandler; +import com.swirlds.platform.network.connectivity.OutboundConnectionCreator; +import com.swirlds.platform.network.connectivity.SocketFactory; +import com.swirlds.platform.network.connectivity.TcpFactory; +import com.swirlds.platform.network.connectivity.TlsFactory; +import com.swirlds.platform.network.topology.NetworkTopology; +import com.swirlds.platform.network.topology.StaticConnectionManagers; +import com.swirlds.platform.network.topology.StaticTopology; import com.swirlds.platform.reconnect.DefaultSignedStateValidator; import com.swirlds.platform.reconnect.ReconnectController; +import com.swirlds.platform.reconnect.ReconnectHelper; +import com.swirlds.platform.reconnect.ReconnectLearnerFactory; +import com.swirlds.platform.reconnect.ReconnectLearnerThrottle; import com.swirlds.platform.reconnect.ReconnectProtocol; +import com.swirlds.platform.reconnect.ReconnectThrottle; import com.swirlds.platform.reconnect.emergency.EmergencyReconnectProtocol; import com.swirlds.platform.recovery.EmergencyRecoveryManager; import com.swirlds.platform.state.SwirldStateManager; import com.swirlds.platform.state.nexus.SignedStateNexus; import com.swirlds.platform.state.signed.ReservedSignedState; import com.swirlds.platform.state.signed.SignedState; +import com.swirlds.platform.system.PlatformConstructionException; import com.swirlds.platform.system.SoftwareVersion; +import com.swirlds.platform.system.address.Address; import com.swirlds.platform.system.address.AddressBook; import com.swirlds.platform.system.status.PlatformStatusManager; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; +import java.io.IOException; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; import java.time.Duration; import java.util.ArrayList; import java.util.List; @@ -73,11 +100,13 @@ import java.util.function.Supplier; /** - * Sync gossip using the protocol negotiator. + * Boilerplate code for gossip. */ -public class SyncGossip extends AbstractGossip { +public class SyncGossip implements ConnectionTracker, Lifecycle { + private LifecyclePhase lifecyclePhase = LifecyclePhase.NOT_STARTED; private final ReconnectController reconnectController; + private final AtomicBoolean gossipHalted = new AtomicBoolean(false); private final SyncPermitProvider syncPermitProvider; protected final SyncConfig syncConfig; @@ -93,12 +122,27 @@ public class SyncGossip extends AbstractGossip { */ private final List syncProtocolThreads = new ArrayList<>(); + protected final PlatformContext platformContext; + protected final AddressBook addressBook; + protected final NodeId selfId; + protected final NetworkTopology topology; + protected final NetworkMetrics networkMetrics; + protected final ReconnectHelper reconnectHelper; + protected final StaticConnectionManagers connectionManagers; + protected final FallenBehindManagerImpl fallenBehindManager; + protected final SyncManagerImpl syncManager; + protected final ReconnectThrottle reconnectThrottle; + protected final ReconnectMetrics reconnectMetrics; + protected final PlatformStatusManager platformStatusManager; + + protected final List thingsToStart = new ArrayList<>(); + /** * Builds the gossip engine, depending on which flavor is requested in the configuration. * * @param platformContext the platform context * @param threadManager the thread manager - * @param time the wall clock time + * @param time the time object used to get the current time * @param keysAndCerts private keys and public certificates * @param notificationEngine used to send notifications to the app * @param addressBook the current address book @@ -118,7 +162,7 @@ public class SyncGossip extends AbstractGossip { * @param intakeEventCounter keeps track of the number of events in the intake pipeline from each peer * @param emergencyStateSupplier returns the emergency state if available */ - public SyncGossip( + protected SyncGossip( @NonNull final PlatformContext platformContext, @NonNull final ThreadManager threadManager, @NonNull final Time time, @@ -140,21 +184,86 @@ public SyncGossip( @NonNull final Runnable clearAllPipelinesForReconnect, @NonNull final IntakeEventCounter intakeEventCounter, @NonNull final Supplier emergencyStateSupplier) { - super( + + this.platformContext = Objects.requireNonNull(platformContext); + this.addressBook = Objects.requireNonNull(addressBook); + this.selfId = Objects.requireNonNull(selfId); + this.platformStatusManager = Objects.requireNonNull(platformStatusManager); + + Objects.requireNonNull(time); + + final ThreadConfig threadConfig = platformContext.getConfiguration().getConfigData(ThreadConfig.class); + + final BasicConfig basicConfig = platformContext.getConfiguration().getConfigData(BasicConfig.class); + + final CryptoConfig cryptoConfig = platformContext.getConfiguration().getConfigData(CryptoConfig.class); + final SocketConfig socketConfig = platformContext.getConfiguration().getConfigData(SocketConfig.class); + + topology = new StaticTopology(addressBook, selfId, basicConfig.numConnections()); + + final SocketFactory socketFactory = socketFactory(keysAndCerts, cryptoConfig, socketConfig); + // create an instance that can create new outbound connections + final OutboundConnectionCreator connectionCreator = new OutboundConnectionCreator( + platformContext, selfId, this, socketFactory, addressBook, shouldDoVersionCheck(), appVersion); + connectionManagers = new StaticConnectionManagers(topology, connectionCreator); + final InboundConnectionHandler inboundConnectionHandler = new InboundConnectionHandler( platformContext, - threadManager, - time, - keysAndCerts, - addressBook, + this, selfId, + addressBook, + connectionManagers::newConnection, + shouldDoVersionCheck(), appVersion, + time); + // allow other members to create connections to me + final Address address = addressBook.getAddress(selfId); + final ConnectionServer connectionServer = new ConnectionServer( + threadManager, + address.getListenAddressIpv4(), + address.getListenPort(), + socketFactory, + inboundConnectionHandler::handle); + thingsToStart.add(new StoppableThreadConfiguration<>(threadManager) + .setPriority(threadConfig.threadPrioritySync()) + .setNodeId(selfId) + .setComponent(PLATFORM_THREAD_POOL_NAME) + .setThreadName("connectionServer") + .setWork(connectionServer) + .build()); + + fallenBehindManager = buildFallenBehindManager(); + + syncManager = new SyncManagerImpl( + platformContext, intakeQueueSizeSupplier, - swirldStateManager, - latestCompleteState, - platformStatusManager, - loadReconnectState, - clearAllPipelinesForReconnect); + fallenBehindManager, + platformContext.getConfiguration().getConfigData(EventConfig.class)); + + final ReconnectConfig reconnectConfig = + platformContext.getConfiguration().getConfigData(ReconnectConfig.class); + + reconnectThrottle = new ReconnectThrottle(reconnectConfig, time); + + networkMetrics = new NetworkMetrics(platformContext.getMetrics(), selfId, addressBook); + platformContext.getMetrics().addUpdater(networkMetrics::update); + reconnectMetrics = new ReconnectMetrics(platformContext.getMetrics(), addressBook); + + final StateConfig stateConfig = platformContext.getConfiguration().getConfigData(StateConfig.class); + reconnectHelper = new ReconnectHelper( + this::pause, + clearAllPipelinesForReconnect::run, + swirldStateManager::getConsensusState, + latestCompleteState::getRound, + new ReconnectLearnerThrottle(time, selfId, reconnectConfig), + loadReconnectState, + new ReconnectLearnerFactory( + platformContext, + threadManager, + addressBook, + reconnectConfig.asyncStreamTimeout(), + reconnectMetrics), + stateConfig); this.intakeEventCounter = Objects.requireNonNull(intakeEventCounter); final EventConfig eventConfig = platformContext.getConfiguration().getConfigData(EventConfig.class); @@ -173,12 +282,8 @@ public SyncGossip( intakeEventCounter, shadowgraphExecutor); - final ReconnectConfig reconnectConfig = - platformContext.getConfiguration().getConfigData(ReconnectConfig.class); - reconnectController = new ReconnectController(reconnectConfig, threadManager, reconnectHelper, this::resume); - final BasicConfig basicConfig = platformContext.getConfiguration().getConfigData(BasicConfig.class); final ProtocolConfig protocolConfig = platformContext.getConfiguration().getConfigData(ProtocolConfig.class); final Duration hangingThreadDuration = basicConfig.hangingThreadDuration(); @@ -196,9 +301,49 @@ public SyncGossip( // If we still need an emergency recovery state, we need it via emergency reconnect. // Start the helper first so that it is ready to receive a connection to perform reconnect with when the // protocol is initiated. - thingsToStart.add(0, reconnectController::start); + thingsToStart.addFirst(reconnectController::start); } + buildSyncProtocolThreads( + platformContext, + threadManager, + time, + notificationEngine, + selfId, + appVersion, + epochHash, + emergencyRecoveryManager, + intakeQueueSizeSupplier, + latestCompleteState, + syncMetrics, + platformStatusManager, + emergencyStateSupplier, + hangingThreadDuration, + protocolConfig, + reconnectConfig, + eventConfig); + + thingsToStart.add(() -> syncProtocolThreads.forEach(StoppableThread::start)); + } + + private void buildSyncProtocolThreads( + final PlatformContext platformContext, + final ThreadManager threadManager, + final Time time, + final NotificationEngine notificationEngine, + final NodeId selfId, + final SoftwareVersion appVersion, + final Hash epochHash, + final EmergencyRecoveryManager emergencyRecoveryManager, + final LongSupplier intakeQueueSizeSupplier, + final SignedStateNexus latestCompleteState, + final SyncMetrics syncMetrics, + final PlatformStatusManager platformStatusManager, + final Supplier emergencyStateSupplier, + final Duration hangingThreadDuration, + final ProtocolConfig protocolConfig, + final ReconnectConfig reconnectConfig, + final EventConfig eventConfig) { for (final NodeId otherId : topology.getNeighbors()) { syncProtocolThreads.add(new StoppableThreadConfiguration<>(threadManager) .setPriority(Thread.NORM_PRIORITY) @@ -262,8 +407,72 @@ public SyncGossip( platformStatusManager))))) .build()); } + } - thingsToStart.add(() -> syncProtocolThreads.forEach(StoppableThread::start)); + private static SocketFactory socketFactory( + @NonNull final KeysAndCerts keysAndCerts, + @NonNull final CryptoConfig cryptoConfig, + @NonNull final SocketConfig socketConfig) { + Objects.requireNonNull(keysAndCerts); + Objects.requireNonNull(cryptoConfig); + Objects.requireNonNull(socketConfig); + + if (!socketConfig.useTLS()) { + return new TcpFactory(socketConfig); + } + try { + return new TlsFactory(keysAndCerts, socketConfig, cryptoConfig); + } catch (final NoSuchAlgorithmException + | UnrecoverableKeyException + | KeyStoreException + | KeyManagementException + | CertificateException + | IOException e) { + throw new PlatformConstructionException("A problem occurred while creating the SocketFactory", e); + } + } + + /** + * Build the fallen behind manager. + */ + @NonNull + protected FallenBehindManagerImpl buildFallenBehindManager() { + return new FallenBehindManagerImpl( + addressBook, + selfId, + topology.getConnectionGraph(), + platformStatusManager, + // this fallen behind impl is different from that of + // SingleNodeSyncGossip which was a no-op. Same for the pause/resume impls + // which only logged (but they do more here) + () -> getReconnectController().start(), + platformContext.getConfiguration().getConfigData(ReconnectConfig.class)); + } + + /** + * Get the reconnect controller. This method is needed to break a circular dependency. + */ + private ReconnectController getReconnectController() { + return reconnectController; + } + + /** + * {@inheritDoc} + */ + @NonNull + @Override + public LifecyclePhase getLifecyclePhase() { + return lifecyclePhase; + } + + /** + * {@inheritDoc} + */ + @Override + public void start() { + throwIfNotInPhase(LifecyclePhase.NOT_STARTED); + lifecyclePhase = LifecyclePhase.STARTED; + thingsToStart.forEach(Startable::start); } /** @@ -271,7 +480,9 @@ public SyncGossip( */ @Override public void stop() { - super.stop(); + throwIfNotInPhase(LifecyclePhase.STARTED); + lifecyclePhase = LifecyclePhase.STOPPED; + syncManager.haltRequestedObserver("stopping gossip"); gossipHalted.set(true); // wait for all existing syncs to stop. no new ones will be started, since gossip has been halted, and // we've fallen behind @@ -282,58 +493,59 @@ public void stop() { } /** - * Get the reconnect controller. This method is needed to break a circular dependency. + * This method is called when the node has finished a reconnect */ - public ReconnectController getReconnectController() { - return reconnectController; + public void resetFallenBehind() { + syncManager.resetFallenBehind(); } /** - * {@inheritDoc} + * Check if we have fallen behind. */ - @NonNull - @Override - protected FallenBehindManagerImpl buildFallenBehindManager() { - return new FallenBehindManagerImpl( - addressBook, - selfId, - topology.getConnectionGraph(), - statusActionSubmitter, - () -> getReconnectController().start(), - platformContext.getConfiguration().getConfigData(ReconnectConfig.class)); + public boolean hasFallenBehind() { + return syncManager.hasFallenBehind(); } /** * {@inheritDoc} */ @Override - public void loadFromSignedState(@NonNull SignedState signedState) { - // intentional no-op + public void newConnectionOpened(@NonNull final Connection sc) { + Objects.requireNonNull(sc); + networkMetrics.connectionEstablished(sc); } /** * {@inheritDoc} */ @Override + public void connectionClosed(final boolean outbound, @NonNull final Connection conn) { + Objects.requireNonNull(conn); + networkMetrics.recordDisconnect(conn); + } + + /** + * Should the network layer do a version check prior to initiating a connection? + * + * @return true if a version check should be done + */ protected boolean shouldDoVersionCheck() { return false; } /** - * {@inheritDoc} + * Stop gossiping until {@link #resume()} is called. If called when already paused then this has no effect. */ - @Override - public void pause() { + protected void pause() { throwIfNotInPhase(LifecyclePhase.STARTED); gossipHalted.set(true); syncPermitProvider.waitForAllSyncsToFinish(); } /** - * {@inheritDoc} + * Resume gossiping. If called when already running then this has no effect. */ - @Override - public void resume() { + protected void resume() { throwIfNotInPhase(LifecyclePhase.STARTED); intakeEventCounter.reset(); gossipHalted.set(false); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/chatter/ChatterGossip.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/chatter/ChatterGossip.java deleted file mode 100644 index 976c60ab7bba..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/chatter/ChatterGossip.java +++ /dev/null @@ -1,316 +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.swirlds.platform.gossip.chatter; - -/** - * Gossip implemented with the chatter protocol. - */ -public class ChatterGossip /*extends AbstractGossip*/ { - - // private final ReconnectController reconnectController; - // private final ChatterCore chatterCore; - // private final List chatterThreads = new LinkedList<>(); - // private final SequenceCycle intakeCycle; - // - // /** - // * Holds a list of objects that need to be cleared when {@link #clear()} is called on this object. - // */ - // private final Clearable clearAllInternalPipelines; - // - // /** - // * Builds the gossip engine that implements the chatter v1 algorithm. - // * - // * @param platformContext the platform context - // * @param threadManager the thread manager - // * @param time the wall clock time - // * @param keysAndCerts private keys and public certificates - // * @param notificationEngine used to send notifications to the app - // * @param addressBook the current address book - // * @param selfId this node's ID - // * @param appVersion the version of the app - // * @param epochHash the epoch hash of the initial state - // * @param shadowGraph contains non-ancient events - // * @param emergencyRecoveryManager handles emergency recovery - // * @param consensusRef a pointer to consensus - // * @param intakeQueue the event intake queue - // * @param swirldStateManager manages the mutable state - // * @param latestCompleteState holds the latest signed state that has enough signatures to be - // verifiable - // * @param eventValidator validates events and passes valid events along the intake pipeline - // * @param eventObserverDispatcher the object used to wire event intake - // * @param syncMetrics metrics for sync - // * @param eventLinker links together events, if chatter is enabled will also buffer orphans - // * @param platformStatusManager the platform status manager - // * @param loadReconnectState a method that should be called when a state from reconnect is obtained - // * @param clearAllPipelinesForReconnect this method should be called to clear all pipelines prior to a - // reconnect - // * @param emergencyStateSupplier returns the emergency state if available - // */ - // public ChatterGossip( - // @NonNull final PlatformContext platformContext, - // @NonNull final ThreadManager threadManager, - // @NonNull final Time time, - // @NonNull final KeysAndCerts keysAndCerts, - // @NonNull final NotificationEngine notificationEngine, - // @NonNull final AddressBook addressBook, - // @NonNull final NodeId selfId, - // @NonNull final SoftwareVersion appVersion, - // @Nullable final Hash epochHash, - // @NonNull final ShadowGraph shadowGraph, - // @NonNull final EmergencyRecoveryManager emergencyRecoveryManager, - // @NonNull final AtomicReference consensusRef, - // @NonNull final QueueThread intakeQueue, - // @NonNull final SwirldStateManager swirldStateManager, - // @NonNull final SignedStateNexus latestCompleteState, - // @NonNull final EventValidator eventValidator, - // @NonNull final EventObserverDispatcher eventObserverDispatcher, - // @NonNull final SyncMetrics syncMetrics, - // @NonNull final EventLinker eventLinker, - // @NonNull final PlatformStatusManager platformStatusManager, - // @NonNull final Consumer loadReconnectState, - // @NonNull final Runnable clearAllPipelinesForReconnect, - // @NonNull final Supplier emergencyStateSupplier) { - // super( - // platformContext, - // threadManager, - // time, - // keysAndCerts, - // addressBook, - // selfId, - // appVersion, - // intakeQueue, - // swirldStateManager, - // latestCompleteState, - // syncMetrics, - // platformStatusManager, - // loadReconnectState, - // clearAllPipelinesForReconnect); - // - // final BasicConfig basicConfig = platformContext.getConfiguration().getConfigData(BasicConfig.class); - // final ChatterConfig chatterConfig = platformContext.getConfiguration().getConfigData(ChatterConfig.class); - // final ProtocolConfig protocolConfig = - // platformContext.getConfiguration().getConfigData(ProtocolConfig.class); - // - // chatterCore = new ChatterCore<>( - // time, - // GossipEvent.class, - // new PrepareChatterEvent(CryptographyHolder.get()), - // chatterConfig, - // networkMetrics::recordPingTime, - // platformContext.getMetrics()); - // - // final ReconnectConfig reconnectConfig = - // platformContext.getConfiguration().getConfigData(ReconnectConfig.class); - // - // reconnectController = new ReconnectController(reconnectConfig, threadManager, reconnectHelper, - // this::resume); - // - // // first create all instances because of thread safety - // for (final NodeId otherId : topology.getNeighbors()) { - // chatterCore.newPeerInstance(otherId, intakeQueue::add); - // } - // - // if (emergencyRecoveryManager.isEmergencyStateRequired()) { - // // If we still need an emergency recovery state, we need it via emergency reconnect. - // // Start the helper first so that it is ready to receive a connection to perform reconnect with when - // the - // // protocol is initiated. - // thingsToStart.add(0, reconnectController::start); - // } - // - // intakeCycle = new SequenceCycle<>(eventValidator::validateEvent); - // - // final ParallelExecutor parallelExecutor = new CachedPoolParallelExecutor(threadManager, "chatter"); - // parallelExecutor.start(); - // for (final NodeId otherId : topology.getNeighbors()) { - // final PeerInstance chatterPeer = chatterCore.getPeerInstance(otherId); - // final ParallelExecutor shadowgraphExecutor = new CachedPoolParallelExecutor(threadManager, - // "node-sync"); - // shadowgraphExecutor.start(); - // final ShadowGraphSynchronizer chatterSynchronizer = new ShadowGraphSynchronizer( - // platformContext, - // time, - // shadowGraph, - // null, - // addressBook.getSize(), - // syncMetrics, - // consensusRef::get, - // intakeQueue, - // syncManager, - // new NoOpIntakeEventCounter(), - // shadowgraphExecutor, - // false, - // () -> { - // // start accepting events into the chatter queue - // chatterPeer.communicationState().chatterSyncStartingPhase3(); - // // wait for any intake event currently being processed to finish - // intakeCycle.waitForCurrentSequenceEnd(); - // }); - // - // chatterThreads.add(new StoppableThreadConfiguration<>(threadManager) - // .setPriority(Thread.NORM_PRIORITY) - // .setNodeId(selfId) - // .setComponent(PLATFORM_THREAD_POOL_NAME) - // .setOtherNodeId(otherId) - // .setThreadName("ChatterReader") - // .setHangingThreadPeriod(basicConfig.hangingThreadDuration()) - // .setWork(new NegotiatorThread( - // connectionManagers.getManager(otherId, topology.shouldConnectTo(otherId)), - // chatterConfig.sleepAfterFailedNegotiation(), - // List.of( - // new VersionCompareHandshake( - // appVersion, !protocolConfig.tolerateMismatchedVersion()), - // new HashCompareHandshake(epochHash, - // !protocolConfig.tolerateMismatchedEpochHash())), - // new NegotiationProtocols(List.of( - // new EmergencyReconnectProtocol( - // time, - // threadManager, - // notificationEngine, - // otherId, - // emergencyRecoveryManager, - // reconnectThrottle, - // emergencyStateSupplier, - // reconnectConfig.asyncStreamTimeout(), - // reconnectMetrics, - // reconnectController, - // platformStatusManager, - // platformContext.getConfiguration()), - // new ReconnectProtocol( - // threadManager, - // otherId, - // reconnectThrottle, - // () -> latestCompleteState.getState("SwirldsPlatform: - // ReconnectProtocol"), - // reconnectConfig.asyncStreamTimeout(), - // reconnectMetrics, - // reconnectController, - // new DefaultSignedStateValidator(platformContext), - // fallenBehindManager, - // platformStatusManager, - // platformContext.getConfiguration(), - // time), - // new ChatterSyncProtocol( - // platformContext, - // otherId, - // chatterPeer.communicationState(), - // chatterPeer.outputAggregator(), - // chatterSynchronizer, - // fallenBehindManager), - // new ChatterProtocol(chatterPeer, parallelExecutor))))) - // .build()); - // } - // - // thingsToStart.add(() -> chatterThreads.forEach(StoppableThread::start)); - // - // eventObserverDispatcher.addObserver(new ChatterNotifier(selfId, chatterCore)); - // - // clearAllInternalPipelines = new LoggingClearables( - // RECONNECT.getMarker(), - // List.of( - // Pair.of(intakeQueue, "intakeQueue"), - // // eventLinker is not thread safe, so the intake thread needs to be paused while it's - // being - // // cleared - // Pair.of(new PauseAndClear(intakeQueue, eventLinker), "eventLinker"), - // Pair.of(shadowGraph, "shadowGraph"))); - // } - // - // /** - // * {@inheritDoc} - // */ - // @Override - // protected boolean unidirectionalConnectionsEnabled() { - // return false; - // } - // - // /** - // * {@inheritDoc} - // */ - // @NonNull - // @Override - // protected FallenBehindManagerImpl buildFallenBehindManager() { - // return new FallenBehindManagerImpl( - // addressBook, - // selfId, - // topology.getConnectionGraph(), - // statusActionSubmitter, - // () -> getReconnectController().start(), - // platformContext.getConfiguration().getConfigData(ReconnectConfig.class)); - // } - // - // /** - // * Get the reconnect controller. This method is needed to break a circular dependency. - // */ - // public ReconnectController getReconnectController() { - // return reconnectController; - // } - // - // /** - // * {@inheritDoc} - // */ - // @Override - // public void loadFromSignedState(@NonNull SignedState signedState) { - // chatterCore.loadFromSignedState(signedState); - // } - // - // /** - // * {@inheritDoc} - // */ - // @Override - // public void stop() { - // super.stop(); - // chatterCore.stopChatter(); - // for (final StoppableThread thread : chatterThreads) { - // thread.stop(); - // } - // } - // - // /** - // * {@inheritDoc} - // */ - // @Override - // protected boolean shouldDoVersionCheck() { - // return false; - // } - // - // /** - // * {@inheritDoc} - // */ - // @Override - // public void clear() { - // clearAllInternalPipelines.clear(); - // } - // - // /** - // * {@inheritDoc} - // */ - // @Override - // public void pause() { - // throwIfNotInPhase(LifecyclePhase.STARTED); - // chatterCore.stopChatter(); - // } - // - // /** - // * {@inheritDoc} - // */ - // @Override - // public void resume() { - // throwIfNotInPhase(LifecyclePhase.STARTED); - // chatterCore.startChatter(); - // } -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/sync/SingleNodeSyncGossip.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/sync/SingleNodeSyncGossip.java deleted file mode 100644 index 8c08022c7d7e..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/sync/SingleNodeSyncGossip.java +++ /dev/null @@ -1,140 +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.swirlds.platform.gossip.sync; - -import static com.swirlds.logging.legacy.LogMarker.RECONNECT; - -import com.swirlds.base.time.Time; -import com.swirlds.common.context.PlatformContext; -import com.swirlds.common.merkle.synchronization.config.ReconnectConfig; -import com.swirlds.common.platform.NodeId; -import com.swirlds.common.threading.manager.ThreadManager; -import com.swirlds.platform.crypto.KeysAndCerts; -import com.swirlds.platform.gossip.AbstractGossip; -import com.swirlds.platform.gossip.FallenBehindManagerImpl; -import com.swirlds.platform.state.SwirldStateManager; -import com.swirlds.platform.state.nexus.SignedStateNexus; -import com.swirlds.platform.state.signed.SignedState; -import com.swirlds.platform.system.SoftwareVersion; -import com.swirlds.platform.system.address.AddressBook; -import com.swirlds.platform.system.status.StatusActionSubmitter; -import edu.umd.cs.findbugs.annotations.NonNull; -import java.util.function.Consumer; -import java.util.function.LongSupplier; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -/** - * Sync gossip using the protocol negotiator. - */ -public class SingleNodeSyncGossip extends AbstractGossip { - - private static final Logger logger = LogManager.getLogger(SingleNodeSyncGossip.class); - - /** - * Builds the gossip engine, depending on which flavor is requested in the configuration. - * - * @param platformContext the platform context - * @param threadManager the thread manager - * @param time the time object used to get the current time - * @param keysAndCerts private keys and public certificates - * @param addressBook the current address book - * @param selfId this node's ID - * @param appVersion the version of the app - * @param intakeQueueSizeSupplier supplies the event intake queue size - * @param swirldStateManager manages the mutable state - * @param latestCompleteState holds the latest signed state that has enough signatures to be verifiable - * @param statusActionSubmitter enables submitting platform status actions - * @param loadReconnectState a method that should be called when a state from reconnect is obtained - * @param clearAllPipelinesForReconnect this method should be called to clear all pipelines prior to a reconnect - */ - public SingleNodeSyncGossip( - @NonNull final PlatformContext platformContext, - @NonNull final ThreadManager threadManager, - @NonNull final Time time, - @NonNull final KeysAndCerts keysAndCerts, - @NonNull final AddressBook addressBook, - @NonNull final NodeId selfId, - @NonNull final SoftwareVersion appVersion, - @NonNull final LongSupplier intakeQueueSizeSupplier, - @NonNull final SwirldStateManager swirldStateManager, - @NonNull final SignedStateNexus latestCompleteState, - @NonNull final StatusActionSubmitter statusActionSubmitter, - @NonNull final Consumer loadReconnectState, - @NonNull final Runnable clearAllPipelinesForReconnect) { - - super( - platformContext, - threadManager, - time, - keysAndCerts, - addressBook, - selfId, - appVersion, - intakeQueueSizeSupplier, - swirldStateManager, - latestCompleteState, - statusActionSubmitter, - loadReconnectState, - clearAllPipelinesForReconnect); - } - - /** - * {@inheritDoc} - */ - @NonNull - @Override - protected FallenBehindManagerImpl buildFallenBehindManager() { - return new FallenBehindManagerImpl( - addressBook, - selfId, - topology.getConnectionGraph(), - statusActionSubmitter, - // Fallen behind callback is intentional no-op, is impossible to fall behind - () -> {}, - platformContext.getConfiguration().getConfigData(ReconnectConfig.class)); - } - - /** - * {@inheritDoc} - */ - @Override - public void loadFromSignedState(@NonNull final SignedState signedState) { - // intentional no-op - } - - /** - * {@inheritDoc} - */ - @Override - protected boolean shouldDoVersionCheck() { - return false; - } - - /** - * {@inheritDoc} - */ - @Override - public void pause() { - logger.info(RECONNECT.getMarker(), "pause() requested for SingleNodeSyncGossip, this should not be possible"); - } - - @Override - public void resume() { - logger.info(RECONNECT.getMarker(), "resume() requested for SingleNodeSyncGossip, this should not be possible"); - } -} From 6954892bcfcd17ba2b4f9876d496bacdb524b469 Mon Sep 17 00:00:00 2001 From: Ivan Malygin Date: Fri, 1 Mar 2024 10:06:33 -0500 Subject: [PATCH 011/115] chore: 11767 Removed state classes that are no longer in use (#11811) Signed-off-by: Ivan Malygin --- .../cli/GenesisPlatformStateCommand.java | 3 +- .../swirlds/platform/state/DualStateImpl.java | 136 ----- .../platform/state/LegacyPlatformState.java | 137 ----- .../swirlds/platform/state/PlatformData.java | 510 ------------------ .../com/swirlds/platform/state/State.java | 38 +- .../platform/state/signed/SignedState.java | 2 +- .../state/signed/SignedStateValidator.java | 2 +- .../platform/test/PlatformDataTests.java | 150 ------ .../platform/test/PlatformStateTests.java | 38 ++ 9 files changed, 44 insertions(+), 972 deletions(-) delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/DualStateImpl.java delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/LegacyPlatformState.java delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/PlatformData.java delete mode 100644 platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/PlatformDataTests.java 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 37afb1f907b1..3dd150159f5a 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 @@ -31,7 +31,6 @@ import com.swirlds.config.api.Configuration; import com.swirlds.platform.config.DefaultConfiguration; import com.swirlds.platform.consensus.SyntheticSnapshot; -import com.swirlds.platform.state.PlatformData; import com.swirlds.platform.state.PlatformState; import com.swirlds.platform.state.signed.DeserializedSignedState; import com.swirlds.platform.state.signed.ReservedSignedState; @@ -82,7 +81,7 @@ public Integer call() throws IOException, ExecutionException, InterruptedExcepti final PlatformState platformState = reservedSignedState.get().getState().getPlatformState(); System.out.printf("Replacing platform data %n"); - platformState.setRound(PlatformData.GENESIS_ROUND); + platformState.setRound(PlatformState.GENESIS_ROUND); platformState.setSnapshot(SyntheticSnapshot.getGenesisSnapshot()); System.out.printf("Nullifying Address Books %n"); platformState.setAddressBook(null); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/DualStateImpl.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/DualStateImpl.java deleted file mode 100644 index 7a6d2b094545..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/DualStateImpl.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright (C) 2016-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.state; - -import com.swirlds.common.io.streams.SerializableDataInputStream; -import com.swirlds.common.io.streams.SerializableDataOutputStream; -import com.swirlds.common.merkle.MerkleLeaf; -import com.swirlds.common.merkle.impl.PartialMerkleLeaf; -import com.swirlds.platform.uptime.UptimeDataImpl; -import edu.umd.cs.findbugs.annotations.NonNull; -import java.io.IOException; -import java.time.Instant; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -/** - * Contains any data that is either read or written by the platform and the application - * @deprecated can be removed after we don't need it for migration - */ -@Deprecated(forRemoval = true) -public class DualStateImpl extends PartialMerkleLeaf implements MerkleLeaf { - private static final Logger logger = LogManager.getLogger(DualStateImpl.class); - - public static final long CLASS_ID = 0x565e2e04ce3782b8L; - - private static final class ClassVersion { - private static final int ORIGINAL = 1; - private static final int UPTIME_DATA = 2; - } - - /** - * the time when the freeze starts - */ - private Instant freezeTime; - - /** - * the last freezeTime based on which the nodes were frozen - */ - private Instant lastFrozenTime; - - /** - * Data on node uptime. - */ - private UptimeDataImpl uptimeData = new UptimeDataImpl(); - - public DualStateImpl() {} - - private DualStateImpl(@NonNull final DualStateImpl that) { - super(that); - this.freezeTime = that.freezeTime; - this.lastFrozenTime = that.lastFrozenTime; - this.uptimeData = that.uptimeData.copy(); - } - - /** - * {@inheritDoc} - */ - @Override - public void serialize(SerializableDataOutputStream out) throws IOException { - out.writeInstant(freezeTime); - out.writeInstant(lastFrozenTime); - out.writeSerializable(uptimeData, false); - } - - /** - * {@inheritDoc} - */ - @Override - public void deserialize(SerializableDataInputStream in, int version) throws IOException { - freezeTime = in.readInstant(); - lastFrozenTime = in.readInstant(); - if (version >= ClassVersion.UPTIME_DATA) { - uptimeData = in.readSerializable(false, UptimeDataImpl::new); - } - } - - /** - * Get the node uptime data. - */ - @NonNull - public UptimeDataImpl getUptimeData() { - return uptimeData; - } - - /** - * Get the freeze time. - */ - public Instant getFreezeTime() { - return freezeTime; - } - - /** - * Get the last frozen time. - */ - public Instant getLastFrozenTime() { - return lastFrozenTime; - } - - /** - * {@inheritDoc} - */ - @Override - public long getClassId() { - return CLASS_ID; - } - - /** - * {@inheritDoc} - */ - @Override - public int getVersion() { - return ClassVersion.UPTIME_DATA; - } - - /** - * {@inheritDoc} - */ - @Override - public DualStateImpl copy() { - return new DualStateImpl(this); - } -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/LegacyPlatformState.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/LegacyPlatformState.java deleted file mode 100644 index 1bf3d439087c..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/LegacyPlatformState.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright (C) 2016-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.state; - -import com.swirlds.common.merkle.MerkleInternal; -import com.swirlds.common.merkle.impl.PartialNaryMerkleInternal; -import com.swirlds.platform.system.address.AddressBook; - -/** - * This subtree contains state data which is managed and used exclusively by the platform. - * - * @deprecated This class is deprecated and will be removed in a future release. Use {@link PlatformState} instead. - */ -@Deprecated(forRemoval = true) -public class LegacyPlatformState extends PartialNaryMerkleInternal implements MerkleInternal { - - public static final long CLASS_ID = 0x483ae5404ad0d0bfL; - - private static final class ClassVersion { - public static final int ORIGINAL = 1; - public static final int ADDED_PREVIOUS_ADDRESS_BOOK = 2; - } - - private static final class ChildIndices { - public static final int PLATFORM_DATA = 0; - public static final int ADDRESS_BOOK = 1; - public static final int PREVIOUS_ADDRESS_BOOK = 2; - } - - public LegacyPlatformState() {} - - /** - * Copy constructor. - * - * @param that the node to copy - */ - private LegacyPlatformState(final LegacyPlatformState that) { - super(that); - if (that.getPlatformData() != null) { - this.setPlatformData(that.getPlatformData().copy()); - } - if (that.getAddressBook() != null) { - this.setAddressBook(that.getAddressBook().copy()); - } - if (that.getPreviousAddressBook() != null) { - this.setPreviousAddressBook(that.getPreviousAddressBook().copy()); - } - } - - /** - * {@inheritDoc} - */ - @Override - public long getClassId() { - return CLASS_ID; - } - - /** - * {@inheritDoc} - */ - @Override - public int getVersion() { - return ClassVersion.ADDED_PREVIOUS_ADDRESS_BOOK; - } - - /** - * {@inheritDoc} - */ - @Override - public LegacyPlatformState copy() { - return new LegacyPlatformState(this); - } - - /** - * Get the address book. - */ - public AddressBook getAddressBook() { - return getChild(ChildIndices.ADDRESS_BOOK); - } - - /** - * Set the address book. - * - * @param addressBook an address book - */ - public void setAddressBook(final AddressBook addressBook) { - setChild(ChildIndices.ADDRESS_BOOK, addressBook); - } - - /** - * Get the object containing miscellaneous round information. - * - * @return round data - */ - public PlatformData getPlatformData() { - return getChild(ChildIndices.PLATFORM_DATA); - } - - /** - * Set the object containing miscellaneous platform information. - * - * @param round round data - */ - public void setPlatformData(final PlatformData round) { - setChild(ChildIndices.PLATFORM_DATA, round); - } - - /** - * Get the previous address book. - */ - public AddressBook getPreviousAddressBook() { - return getChild(ChildIndices.PREVIOUS_ADDRESS_BOOK); - } - - /** - * Set the previous address book. - * - * @param addressBook an address book - */ - public void setPreviousAddressBook(final AddressBook addressBook) { - setChild(ChildIndices.PREVIOUS_ADDRESS_BOOK, addressBook); - } -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/PlatformData.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/PlatformData.java deleted file mode 100644 index b980762fdcc2..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/PlatformData.java +++ /dev/null @@ -1,510 +0,0 @@ -/* - * Copyright (C) 2016-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.state; - -import com.swirlds.base.utility.ToStringBuilder; -import com.swirlds.common.crypto.Hash; -import com.swirlds.common.io.streams.SerializableDataInputStream; -import com.swirlds.common.io.streams.SerializableDataOutputStream; -import com.swirlds.common.merkle.MerkleLeaf; -import com.swirlds.common.merkle.impl.PartialMerkleLeaf; -import com.swirlds.common.utility.NonCryptographicHashing; -import com.swirlds.platform.consensus.ConsensusSnapshot; -import com.swirlds.platform.consensus.RoundCalculationUtils; -import com.swirlds.platform.internal.EventImpl; -import com.swirlds.platform.system.SoftwareVersion; -import edu.umd.cs.findbugs.annotations.Nullable; -import java.io.IOException; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.Objects; - -/** - * A collection of miscellaneous platform data. - * - * @deprecated this class is no longer used and is kept for migration purposes only - */ -@Deprecated(forRemoval = true) -public class PlatformData extends PartialMerkleLeaf implements MerkleLeaf { - - private static final long CLASS_ID = 0x1f89d0c43a8c08bdL; - - /** - * The round of the genesis state. - */ - public static final long GENESIS_ROUND = 0; - - private static final class ClassVersion { - public static final int EPOCH_HASH = 2; - public static final int ROUNDS_NON_ANCIENT = 3; - /** - * - Events are no longer serialized, the field is kept for migration purposes - Mingen is no longer stored - * directly, its part of the snapshot - restart/reconnect now uses a snapshot - lastTransactionTimestamp is no - * longer stored directly, its part of the snapshot - numEventsCons is no longer stored directly, its part of - * the snapshot - */ - public static final int CONSENSUS_SNAPSHOT = 4; - } - - /** - * The round of this state. This state represents the handling of all transactions that have reached consensus in - * all previous rounds. All transactions from this round will eventually be applied to this state. The first state - * (genesis state) has a round of 0 because the first round is defined as round 1, and the genesis state is before - * any transactions are handled. - */ - private long round = GENESIS_ROUND; - - /** - * running hash of the hashes of all consensus events have there been throughout all of history, up through the - * round received that this SignedState represents. - */ - private Hash hashEventsCons; - - /** - * contains events for the round that is being signed and the preceding rounds - */ - private EventImpl[] events; - - /** - * the consensus timestamp for this signed state - */ - private Instant consensusTimestamp; - - /** - * the minimum ancient indicators of the judges for each round - */ - private List minimumJudgeInfoList; - - /** - * The version of the application software that was responsible for creating this state. - */ - private SoftwareVersion creationSoftwareVersion; - - /** - * The epoch hash of this state. Updated every time emergency recovery is performed. - */ - private Hash epochHash; - - /** - * The next epoch hash, used to update the epoch hash at the next round boundary. This field is not part of the hash - * and is not serialized. - */ - private Hash nextEpochHash; - - /** - * The number of non-ancient rounds. - */ - private int roundsNonAncient; - - /** A snapshot of the consensus state at the end of the round, used for restart/reconnect */ - private ConsensusSnapshot snapshot; - - public PlatformData() {} - - /** - * Copy constructor. - * - * @param that the object to copy - */ - private PlatformData(final PlatformData that) { - super(that); - this.round = that.round; - this.hashEventsCons = that.hashEventsCons; - if (that.events != null) { - this.events = Arrays.copyOf(that.events, that.events.length); - } - this.consensusTimestamp = that.consensusTimestamp; - if (that.minimumJudgeInfoList != null) { - this.minimumJudgeInfoList = new ArrayList<>(that.minimumJudgeInfoList); - } - this.creationSoftwareVersion = that.creationSoftwareVersion; - this.epochHash = that.epochHash; - this.nextEpochHash = that.nextEpochHash; - this.roundsNonAncient = that.roundsNonAncient; - this.snapshot = that.snapshot; - } - - /** - * Update the epoch hash if the next epoch hash is non-null and different from the current epoch hash. - */ - public void updateEpochHash() { - throwIfImmutable(); - if (nextEpochHash != null && !nextEpochHash.equals(epochHash)) { - // This is the first round after an emergency recovery round - // Set the epoch hash to the next value - epochHash = nextEpochHash; - - // set this to null so the value is consistent with a - // state loaded from disk or received via reconnect - nextEpochHash = null; - } - } - - /** - * {@inheritDoc} - */ - @Override - public long getClassId() { - return CLASS_ID; - } - - /** - * {@inheritDoc} - */ - @Override - public void serialize(final SerializableDataOutputStream out) throws IOException { - out.writeLong(round); - out.writeSerializable(hashEventsCons, false); - - out.writeInstant(consensusTimestamp); - - out.writeSerializable(creationSoftwareVersion, true); - out.writeSerializable(epochHash, false); - out.writeInt(roundsNonAncient); - out.writeSerializable(snapshot, false); - } - - /** - * {@inheritDoc} - */ - @Override - public void deserialize(final SerializableDataInputStream in, final int version) throws IOException { - - if (version < ClassVersion.ROUNDS_NON_ANCIENT) { - throw new IOException("Unsupported version " + version); - } - - round = in.readLong(); - if (version < ClassVersion.CONSENSUS_SNAPSHOT) { - // numEventsCons - in.readLong(); - } - - hashEventsCons = in.readSerializable(false, Hash::new); - - if (version < ClassVersion.CONSENSUS_SNAPSHOT) { - int eventNum = in.readInt(); - events = new EventImpl[eventNum]; - for (int i = 0; i < eventNum; i++) { - events[i] = in.readSerializable(false, EventImpl::new); - events[i].getBaseEventHashedData().setHash(in.readSerializable(false, Hash::new)); - } - State.linkParents(events); - } - - consensusTimestamp = in.readInstant(); - - if (version < ClassVersion.CONSENSUS_SNAPSHOT) { - minimumJudgeInfoList = MinimumJudgeInfo.deserializeList(in); - - // previously this was the last transaction timestamp - in.readInstant(); - } - - creationSoftwareVersion = in.readSerializable(); - epochHash = in.readSerializable(false, Hash::new); - roundsNonAncient = in.readInt(); - - if (version >= ClassVersion.CONSENSUS_SNAPSHOT) { - snapshot = in.readSerializable(false, ConsensusSnapshot::new); - } - } - - /** - * {@inheritDoc} - */ - @Override - public int getVersion() { - return ClassVersion.CONSENSUS_SNAPSHOT; - } - - /** - * {@inheritDoc} - */ - @Override - public PlatformData copy() { - return new PlatformData(this); - } - - /** - * Get the software version of the application that created this state. - * - * @return the creation version - */ - public SoftwareVersion getCreationSoftwareVersion() { - return creationSoftwareVersion; - } - - /** - * Set the software version of the application that created this state. - * - * @param creationVersion the creation version - * @return this object - */ - public PlatformData setCreationSoftwareVersion(final SoftwareVersion creationVersion) { - this.creationSoftwareVersion = creationVersion; - return this; - } - - /** - * Get the round when this state was generated. - * - * @return a round number - */ - public long getRound() { - return round; - } - - /** - * Set the round when this state was generated. - * - * @param round a round number - * @return this object - */ - public PlatformData setRound(final long round) { - this.round = round; - return this; - } - - /** - * Get the running hash of all events that have been applied to this state since the beginning of time. - * - * @return a running hash of events - */ - public Hash getHashEventsCons() { - return hashEventsCons; - } - - /** - * Set the running hash of all events that have been applied to this state since the beginning of time. - * - * @param hashEventsCons a running hash of events - * @return this object - */ - public PlatformData setHashEventsCons(final Hash hashEventsCons) { - this.hashEventsCons = hashEventsCons; - return this; - } - - /** - * Get the events stored in this state. - * - * @return an array of events - */ - public EventImpl[] getEvents() { - return events; - } - - /** - * Get the consensus timestamp for this state, defined as the timestamp of the first transaction that was applied in - * the round that created the state. - * - * @return a consensus timestamp - */ - public Instant getConsensusTimestamp() { - return consensusTimestamp; - } - - /** - * Set the consensus timestamp for this state, defined as the timestamp of the first transaction that was applied in - * the round that created the state. - * - * @param consensusTimestamp a consensus timestamp - * @return this object - */ - public PlatformData setConsensusTimestamp(final Instant consensusTimestamp) { - this.consensusTimestamp = consensusTimestamp; - return this; - } - - /** - * Get the minimum event generation for each node within this state. - * - * @return minimum generation info list - */ - public List getMinimumJudgeInfoList() { - if (snapshot != null) { - return snapshot.getMinimumJudgeInfoList(); - } - return minimumJudgeInfoList; - } - - /** - * The minimum generation of famous witnesses for the round specified. This method only looks at non-ancient rounds - * contained within this state. - * - * @param round the round whose minimum generation will be returned - * @return the minimum generation for the round specified - * @throws NoSuchElementException if the generation information for this round is not contained withing this state - */ - public long getMinGen(final long round) { - for (final MinimumJudgeInfo info : getMinimumJudgeInfoList()) { - if (info.round() == round) { - return info.minimumJudgeAncientThreshold(); - } - } - throw new NoSuchElementException("No minimum generation found for round: " + round); - } - - /** - * Return the round generation of the oldest round in this state - * - * @return the generation of the oldest round - */ - public long getMinRoundGeneration() { - return getMinimumJudgeInfoList().stream() - .findFirst() - .orElseThrow(() -> new IllegalStateException("No MinGen info found in state")) - .minimumJudgeAncientThreshold(); - } - - /** - * Sets the epoch hash of this state. - * - * @param epochHash the epoch hash of this state - * @return this object - */ - public PlatformData setEpochHash(final Hash epochHash) { - this.epochHash = epochHash; - return this; - } - - /** - * Gets the epoch hash of this state. - * - * @return the epoch hash of this state - */ - @Nullable - public Hash getEpochHash() { - return epochHash; - } - - /** - * Sets the next epoch hash of this state. - * - * @param nextEpochHash the next epoch hash of this state - * @return this object - */ - public PlatformData setNextEpochHash(final Hash nextEpochHash) { - this.nextEpochHash = nextEpochHash; - return this; - } - - /** - * Gets the next epoch hash of this state. - * - * @return the next epoch hash of this state - */ - public Hash getNextEpochHash() { - return nextEpochHash; - } - - /** - * Sets the number of non-ancient rounds. - * - * @param roundsNonAncient the number of non-ancient rounds - * @return this object - */ - public PlatformData setRoundsNonAncient(final int roundsNonAncient) { - this.roundsNonAncient = roundsNonAncient; - return this; - } - - /** - * Gets the number of non-ancient rounds. - * - * @return the number of non-ancient rounds - */ - public int getRoundsNonAncient() { - return roundsNonAncient; - } - - /** - * Gets the minimum generation of non-ancient events. - * - * @return the minimum generation of non-ancient events - */ - public long getMinimumGenerationNonAncient() { - return RoundCalculationUtils.getMinGenNonAncient(roundsNonAncient, round, this::getMinGen); - } - - /** - * @return the consensus snapshot for this round - */ - public ConsensusSnapshot getSnapshot() { - return snapshot; - } - - /** - * @param snapshot the consensus snapshot for this round - * @return this object - */ - public PlatformData setSnapshot(final ConsensusSnapshot snapshot) { - this.snapshot = snapshot; - return this; - } - - /** - * {@inheritDoc} - */ - @Override - public boolean equals(final Object other) { - if (this == other) { - return true; - } - if (other == null || getClass() != other.getClass()) { - return false; - } - final PlatformData that = (PlatformData) other; - return round == that.round - && Objects.equals(hashEventsCons, that.hashEventsCons) - && Arrays.equals(events, that.events) - && Objects.equals(consensusTimestamp, that.consensusTimestamp) - && Objects.equals(minimumJudgeInfoList, that.minimumJudgeInfoList) - && Objects.equals(epochHash, that.epochHash) - && Objects.equals(roundsNonAncient, that.roundsNonAncient) - && Objects.equals(snapshot, that.snapshot); - } - - /** - * {@inheritDoc} - */ - @Override - public int hashCode() { - return NonCryptographicHashing.hash32(round); - } - - /** - * {@inheritDoc} - */ - @Override - public String toString() { - return new ToStringBuilder(this) - .append("round", round) - .append("hashEventsCons", hashEventsCons) - .append("events", events) - .append("consensusTimestamp", consensusTimestamp) - .append("minimumJudgeInfoList", minimumJudgeInfoList) - .append("epochHash", epochHash) - .append("roundsNonAncient", roundsNonAncient) - .append("snapshot", snapshot) - .toString(); - } -} 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 59088b79f21d..4470abe81e22 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 @@ -16,8 +16,6 @@ package com.swirlds.platform.state; -import static com.swirlds.logging.legacy.LogMarker.STARTUP; - import com.swirlds.base.utility.ToStringBuilder; import com.swirlds.common.crypto.Hash; import com.swirlds.common.formatting.TextTable; @@ -99,38 +97,8 @@ private State(final State that) { @Override public MerkleNode migrate(final int version) { if (version < ClassVersion.REMOVE_DUAL_STATE) { - logger.info( - STARTUP.getMarker(), - "Migrating legacy platform state to new platform state at version (State version {} -> {}).", - version, - getVersion()); - - final State newState = new State(); - - final PlatformState newPlatformState = new PlatformState(); - - final LegacyPlatformState platformState = getChild(ChildIndices.PLATFORM_STATE); - final PlatformData platformData = platformState.getPlatformData(); - final DualStateImpl dualState = getChild(ChildIndices.DUAL_STATE); - - newPlatformState.setAddressBook(platformState.getAddressBook()); - newPlatformState.setPreviousAddressBook(platformState.getPreviousAddressBook()); - newPlatformState.setRound(platformData.getRound()); - newPlatformState.setRunningEventHash(platformData.getHashEventsCons()); - newPlatformState.setConsensusTimestamp(platformData.getConsensusTimestamp()); - newPlatformState.setCreationSoftwareVersion(platformData.getCreationSoftwareVersion()); - newPlatformState.setEpochHash(platformData.getEpochHash()); - newPlatformState.setNextEpochHash(platformData.getNextEpochHash()); - newPlatformState.setRoundsNonAncient(platformData.getRoundsNonAncient()); - newPlatformState.setSnapshot(platformData.getSnapshot()); - newPlatformState.setFreezeTime(dualState.getFreezeTime()); - newPlatformState.setLastFrozenTime(dualState.getLastFrozenTime()); - newPlatformState.setUptimeData(dualState.getUptimeData()); - - newState.setPlatformState(newPlatformState); - newState.setSwirldState(getSwirldState()); - - return newState; + throw new UnsupportedOperationException("State migration from version " + version + " is not supported." + + " The minimum supported version is " + getMinimumSupportedVersion()); } return this; } @@ -140,7 +108,7 @@ public MerkleNode migrate(final int version) { */ @Override public int getMinimumSupportedVersion() { - return ClassVersion.ADD_DUAL_STATE; + return ClassVersion.REMOVE_DUAL_STATE; } /** 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 6520d6d6eeb1..0e88fb080316 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 @@ -20,7 +20,7 @@ import static com.swirlds.common.utility.Threshold.SUPER_MAJORITY; import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; import static com.swirlds.logging.legacy.LogMarker.SIGNED_STATE; -import static com.swirlds.platform.state.PlatformData.GENESIS_ROUND; +import static com.swirlds.platform.state.PlatformState.GENESIS_ROUND; import static com.swirlds.platform.state.signed.SignedStateHistory.SignedStateAction.CREATION; import static com.swirlds.platform.state.signed.SignedStateHistory.SignedStateAction.RELEASE; import static com.swirlds.platform.state.signed.SignedStateHistory.SignedStateAction.RESERVE; diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/signed/SignedStateValidator.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/signed/SignedStateValidator.java index 0c318dffc58b..685560b94b42 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/signed/SignedStateValidator.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/signed/SignedStateValidator.java @@ -31,7 +31,7 @@ public interface SignedStateValidator { * @param signedState the signed state to validate * @param addressBook the address book used for this signed state * @param previousStateData A {@link SignedStateValidationData} containing data from the - * {@link com.swirlds.platform.state.PlatformData} in the state before the signed state to be validated. + * {@link com.swirlds.platform.state.PlatformState} in the state before the signed state to be validated. * This may be used to ensure signed state is usable and valid, and also contains useful information for * diagnostics produced when the signed state is not considered valid. * @throws SignedStateInvalidException if the signed state is not valid diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/PlatformDataTests.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/PlatformDataTests.java deleted file mode 100644 index 39ca6707d8b4..000000000000 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/PlatformDataTests.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright (C) 2016-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.test; - -import static com.swirlds.common.test.fixtures.RandomUtils.getRandomPrintSeed; -import static com.swirlds.common.test.fixtures.RandomUtils.randomHash; -import static com.swirlds.common.test.fixtures.RandomUtils.randomInstant; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - -import com.swirlds.common.constructable.ConstructableRegistry; -import com.swirlds.common.constructable.ConstructableRegistryException; -import com.swirlds.common.crypto.CryptographyHolder; -import com.swirlds.common.crypto.Hash; -import com.swirlds.common.io.streams.SerializableDataInputStream; -import com.swirlds.common.io.streams.SerializableDataOutputStream; -import com.swirlds.config.extensions.test.fixtures.TestConfigBuilder; -import com.swirlds.platform.consensus.ConsensusSnapshot; -import com.swirlds.platform.state.MinimumJudgeInfo; -import com.swirlds.platform.state.PlatformData; -import com.swirlds.platform.system.BasicSoftwareVersion; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.time.Instant; -import java.util.LinkedList; -import java.util.List; -import java.util.Random; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -@DisplayName("PlatformData Tests") -class PlatformDataTests { - - @BeforeAll - static void beforeAll() throws ConstructableRegistryException { - new TestConfigBuilder().getOrCreateConfig(); - ConstructableRegistry.getInstance().registerConstructables("com.swirlds"); - } - - private static PlatformData generateRandomPlatformData(final Random random) { - final int randomBound = 10_000; - - final List minimumJudgeInfo = new LinkedList<>(); - final int minimumJudgeInfoSize = random.nextInt(1, MinimumJudgeInfo.MAX_MINIMUM_JUDGE_INFO_SIZE); - for (int i = 0; i < minimumJudgeInfoSize; i++) { - minimumJudgeInfo.add(new MinimumJudgeInfo(random.nextLong(randomBound), random.nextLong(randomBound))); - } - - return new PlatformData() - .setRound(random.nextLong(randomBound)) - .setHashEventsCons(randomHash(random)) - .setConsensusTimestamp(Instant.ofEpochSecond(random.nextInt(randomBound))) - .setCreationSoftwareVersion(new BasicSoftwareVersion(random.nextInt(randomBound))) - .setEpochHash(randomHash(random)) - .setSnapshot(new ConsensusSnapshot( - random.nextLong(), - List.of(randomHash(random), randomHash(random), randomHash(random)), - minimumJudgeInfo, - random.nextLong(), - randomInstant(random))); - } - - @Test - @DisplayName("Serialization Test") - void serializationTest() throws IOException { - final Random random = getRandomPrintSeed(); - final PlatformData platformData = generateRandomPlatformData(random); - - final ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); - final SerializableDataOutputStream out = new SerializableDataOutputStream(byteOut); - - out.writeSerializable(platformData, true); - - final SerializableDataInputStream in = - new SerializableDataInputStream(new ByteArrayInputStream(byteOut.toByteArray())); - - final PlatformData deserialized = in.readSerializable(); - - CryptographyHolder.get().digestSync(platformData); - CryptographyHolder.get().digestSync(deserialized); - - assertEquals(platformData.getHash(), deserialized.getHash(), "hash should match"); - } - - @Test - @DisplayName("Serialization Test") - void equalityTest() { - final Random random = getRandomPrintSeed(); - - final long seed1 = random.nextLong(); - final long seed2 = random.nextLong(); - - final PlatformData platformData1 = generateRandomPlatformData(new Random(seed1)); - final PlatformData platformData1Duplicate = generateRandomPlatformData(new Random(seed1)); - final PlatformData platformData2 = generateRandomPlatformData(new Random(seed2)); - - assertEquals(platformData1, platformData1Duplicate, "should be equal"); - assertNotEquals(platformData1, platformData2, "should not be equal"); - } - - @Test - @DisplayName("Update Epoch Hash Test") - void updateEpochHashTest() { - final Random random = getRandomPrintSeed(); - final PlatformData platformData = generateRandomPlatformData(random); - final Hash hash = randomHash(random); - - platformData.setEpochHash(null); - platformData.setNextEpochHash(null); - assertDoesNotThrow(platformData::updateEpochHash); - assertNull(platformData.getEpochHash(), "epoch hash should not change"); - assertNull(platformData.getNextEpochHash(), "next epoch hash should not change"); - - platformData.setEpochHash(hash); - platformData.setNextEpochHash(null); - assertDoesNotThrow(platformData::updateEpochHash); - assertEquals(hash, platformData.getEpochHash(), "epoch hash should not change"); - assertNull(platformData.getNextEpochHash(), "next epoch hash should not change"); - - platformData.setEpochHash(null); - platformData.setNextEpochHash(hash); - assertDoesNotThrow(platformData::updateEpochHash); - assertEquals(hash, platformData.getEpochHash(), "epoch hash should be updated"); - assertNull(platformData.getNextEpochHash(), "next epoch hash should be set to null"); - - platformData.setEpochHash(randomHash(random)); - platformData.setNextEpochHash(hash); - assertDoesNotThrow(platformData::updateEpochHash); - assertEquals(hash, platformData.getEpochHash(), "epoch hash should be updated"); - assertNull(platformData.getNextEpochHash(), "next epoch hash should be set to null"); - } -} diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/PlatformStateTests.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/PlatformStateTests.java index e402b3a3ecba..7465e0f51f26 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/PlatformStateTests.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/PlatformStateTests.java @@ -16,14 +16,19 @@ package com.swirlds.platform.test; +import static com.swirlds.common.test.fixtures.RandomUtils.getRandomPrintSeed; +import static com.swirlds.common.test.fixtures.RandomUtils.randomHash; import static com.swirlds.platform.test.PlatformStateUtils.randomPlatformState; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import com.swirlds.common.constructable.ConstructableRegistry; import com.swirlds.common.constructable.ConstructableRegistryException; +import com.swirlds.common.crypto.Hash; import com.swirlds.common.merkle.crypto.MerkleCryptoFactory; import com.swirlds.common.test.fixtures.io.InputOutputStream; import com.swirlds.common.test.fixtures.junit.tags.TestComponentTags; @@ -32,6 +37,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.Path; +import java.util.Random; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Tag; @@ -93,4 +99,36 @@ void platformStateSerializationTest() throws IOException, ConstructableRegistryE assertEquals(state.getHash(), decodedState.getHash(), "expected deserialized object to be equal"); } + + @Test + @DisplayName("Update Epoch Hash Test") + void updateEpochHashTest() { + final Random random = getRandomPrintSeed(); + final PlatformState platformData = randomPlatformState(random); + final Hash hash = randomHash(random); + + platformData.setEpochHash(null); + platformData.setNextEpochHash(null); + assertDoesNotThrow(platformData::updateEpochHash); + assertNull(platformData.getEpochHash(), "epoch hash should not change"); + assertNull(platformData.getNextEpochHash(), "next epoch hash should not change"); + + platformData.setEpochHash(hash); + platformData.setNextEpochHash(null); + assertDoesNotThrow(platformData::updateEpochHash); + assertEquals(hash, platformData.getEpochHash(), "epoch hash should not change"); + assertNull(platformData.getNextEpochHash(), "next epoch hash should not change"); + + platformData.setEpochHash(null); + platformData.setNextEpochHash(hash); + assertDoesNotThrow(platformData::updateEpochHash); + assertEquals(hash, platformData.getEpochHash(), "epoch hash should be updated"); + assertNull(platformData.getNextEpochHash(), "next epoch hash should be set to null"); + + platformData.setEpochHash(randomHash(random)); + platformData.setNextEpochHash(hash); + assertDoesNotThrow(platformData::updateEpochHash); + assertEquals(hash, platformData.getEpochHash(), "epoch hash should be updated"); + assertNull(platformData.getNextEpochHash(), "next epoch hash should be set to null"); + } } From e408ffc75f27f91187689b54cd92caba623aa18a Mon Sep 17 00:00:00 2001 From: Michael Tinker Date: Fri, 1 Mar 2024 10:43:30 -0600 Subject: [PATCH 012/115] fix: fix ERC-20 log events and custom fee calculations (#11789) Signed-off-by: Michael Tinker --- .../state/logic/StandardProcessLogic.java | 11 +++++- .../precompile/codec/DecodingFacade.java | 7 ++-- .../impl/ERCTransferPrecompile.java | 9 ++--- .../precompile/impl/TransferPrecompile.java | 2 +- .../mono/txns/span/SpanMapManager.java | 8 +++- .../state/logic/StandardProcessLogicTest.java | 7 +++- .../mono/txns/span/SpanMapManagerTest.java | 3 -- .../scope/HandleSystemContractOperations.java | 1 - .../hts/transfer/ClassicTransfersCall.java | 8 ++-- .../hts/transfer/Erc20TransfersCall.java | 37 ++++++------------- .../transfer/TransferEventLoggingUtils.java | 20 ++++------ .../hts/transfer/Erc20TransfersCallTest.java | 20 ++++++++-- 12 files changed, 70 insertions(+), 63 deletions(-) diff --git a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/state/logic/StandardProcessLogic.java b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/state/logic/StandardProcessLogic.java index a92f8df3d80d..f99910234fa9 100644 --- a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/state/logic/StandardProcessLogic.java +++ b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/state/logic/StandardProcessLogic.java @@ -33,8 +33,10 @@ import com.hedera.node.app.service.mono.txns.ProcessLogic; import com.hedera.node.app.service.mono.txns.schedule.ScheduleProcessing; import com.hedera.node.app.service.mono.txns.span.ExpandHandleSpan; +import com.hedera.node.app.service.mono.txns.span.SpanMapManager; import com.hedera.node.app.service.mono.utils.accessors.SwirldsTxnAccessor; import com.hedera.node.app.service.mono.utils.accessors.TxnAccessor; +import com.hederahashgraph.api.proto.java.HederaFunctionality; import com.swirlds.platform.system.InitTrigger; import com.swirlds.platform.system.SoftwareVersion; import com.swirlds.platform.system.transaction.ConsensusTransaction; @@ -63,6 +65,7 @@ public class StandardProcessLogic implements ProcessLogic { private final RecordStreaming recordStreaming; private final RecordCache recordCache; private final InitTrigger initTrigger; + private final SpanMapManager spanMapManager; @Inject public StandardProcessLogic( @@ -79,7 +82,8 @@ public StandardProcessLogic( final RecordStreaming recordStreaming, final StateView workingView, final RecordCache recordCache, - @NonNull final InitTrigger initTrigger) { + @NonNull final InitTrigger initTrigger, + SpanMapManager spanMapManager) { this.expiries = expiries; this.invariantChecks = invariantChecks; this.expandHandleSpan = expandHandleSpan; @@ -94,6 +98,7 @@ public StandardProcessLogic( this.workingView = workingView; this.recordCache = recordCache; this.initTrigger = requireNonNull(initTrigger); + this.spanMapManager = spanMapManager; } @Override @@ -185,6 +190,10 @@ private void doProcess(final long submittingMember, final Instant consensusTime, txnManager.process(accessor, consensusTime, submittingMember); final var triggeredAccessor = txnCtx.triggeredTxn(); if (triggeredAccessor != null) { + // Ensure we take custom fees into account when charging fees + if (triggeredAccessor.getFunction() == HederaFunctionality.CryptoTransfer) { + spanMapManager.expandImpliedTransfers(triggeredAccessor); + } txnManager.process(triggeredAccessor, consensusTimeTracker.nextTransactionTime(false), submittingMember); } executionTimeTracker.stop(); diff --git a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/codec/DecodingFacade.java b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/codec/DecodingFacade.java index d267ad84b7e7..960fd16968bd 100644 --- a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/codec/DecodingFacade.java +++ b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/codec/DecodingFacade.java @@ -217,7 +217,7 @@ public static List bindFungibleTransf // otherwise default to false in order to preserve the existing behaviour. // The isApproval parameter only exists in the new form of cryptoTransfer final boolean isApproval = (transfer.size() > 2) && (boolean) transfer.get(2); - addSignedAdjustment(fungibleTransfers, tokenType, accountID, amount, isApproval); + addSignedAdjustment(fungibleTransfers, tokenType, accountID, amount, isApproval, false); } return fungibleTransfers; } @@ -250,8 +250,9 @@ public static void addSignedAdjustment( final TokenID tokenType, final AccountID accountID, final long amount, - final boolean isApproval) { - if (amount > 0) { + final boolean isApproval, + final boolean zeroAmountIsReceiver) { + if (amount > 0 || (amount == 0 && zeroAmountIsReceiver)) { fungibleTransfers.add( new SyntheticTxnFactory.FungibleTokenTransfer(amount, isApproval, tokenType, null, accountID)); } else { diff --git a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/ERCTransferPrecompile.java b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/ERCTransferPrecompile.java index ba87277c5508..f74d258992c2 100644 --- a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/ERCTransferPrecompile.java +++ b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/ERCTransferPrecompile.java @@ -224,8 +224,8 @@ public static CryptoTransferWrapper decodeERCTransfer( final var amount = (BigInteger) decodedArguments.get(1); final List fungibleTransfers = new ArrayList<>(); - addSignedAdjustment(fungibleTransfers, token, recipient, amount.longValueExact(), false); - addSignedAdjustment(fungibleTransfers, token, caller, -amount.longValueExact(), false); + addSignedAdjustment(fungibleTransfers, token, recipient, amount.longValueExact(), false, true); + addSignedAdjustment(fungibleTransfers, token, caller, -amount.longValueExact(), false, false); final var tokenTransferWrappers = Collections.singletonList(new TokenTransferWrapper(NO_NFT_EXCHANGES, fungibleTransfers)); @@ -265,9 +265,9 @@ public static CryptoTransferWrapper decodeERCTransferFrom( final List fungibleTransfers = new ArrayList<>(); final var amount = (BigInteger) decodedArguments.get(offset + 2); - addSignedAdjustment(fungibleTransfers, token, to, amount.longValueExact(), false); + addSignedAdjustment(fungibleTransfers, token, to, amount.longValueExact(), false, true); - addSignedAdjustment(fungibleTransfers, token, from, -amount.longValueExact(), true); + addSignedAdjustment(fungibleTransfers, token, from, -amount.longValueExact(), true, false); final var tokenTransferWrappers = Collections.singletonList(new TokenTransferWrapper(NO_NFT_EXCHANGES, fungibleTransfers)); @@ -303,7 +303,6 @@ private Log getLogForFungibleTransfer(final Address logger) { amount = BigInteger.valueOf(fungibleTransfer.amount()); } } - return EncodingFacade.LogBuilder.logBuilder() .forLogger(logger) .forEventSignature(AbiConstants.TRANSFER_EVENT) diff --git a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/TransferPrecompile.java b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/TransferPrecompile.java index 53826e51c8f1..888174554aad 100644 --- a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/TransferPrecompile.java +++ b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/TransferPrecompile.java @@ -541,7 +541,7 @@ public static void addSignedAdjustments( accountID = generateAccountIDWithAliasCalculatedFrom(accountID); } - DecodingFacade.addSignedAdjustment(fungibleTransfers, tokenType, accountID, amount, false); + DecodingFacade.addSignedAdjustment(fungibleTransfers, tokenType, accountID, amount, false, false); } } diff --git a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/txns/span/SpanMapManager.java b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/txns/span/SpanMapManager.java index b57ff2c258f1..906f6155f40e 100644 --- a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/txns/span/SpanMapManager.java +++ b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/txns/span/SpanMapManager.java @@ -318,7 +318,7 @@ private void rationalizeImpliedTransfers(final TxnAccessor accessor) { } } - private void expandImpliedTransfers(final TxnAccessor accessor) { + public void expandImpliedTransfers(final TxnAccessor accessor) { final var op = accessor.getTxn().getCryptoTransfer(); final var impliedTransfers = impliedTransfersMarshal.unmarshalFromGrpc(op, accessor.getPayer()); reCalculateXferMeta(accessor, impliedTransfers); @@ -328,12 +328,16 @@ private void expandImpliedTransfers(final TxnAccessor accessor) { } public static void reCalculateXferMeta(final TxnAccessor accessor, final ImpliedTransfers impliedTransfers) { + final var maybeAssessedCustomFees = impliedTransfers.getAssessedCustomFeeWrappers(); + if (maybeAssessedCustomFees.isEmpty()) { + return; + } final var xferMeta = accessor.availXferUsageMeta(); var customFeeTokenTransfers = 0; var customFeeHbarTransfers = 0; final Set involvedTokens = new HashSet<>(); - for (final var assessedFeeWrapper : impliedTransfers.getAssessedCustomFeeWrappers()) { + for (final var assessedFeeWrapper : maybeAssessedCustomFees) { if (assessedFeeWrapper.isForHbar()) { customFeeHbarTransfers++; } else { diff --git a/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/state/logic/StandardProcessLogicTest.java b/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/state/logic/StandardProcessLogicTest.java index cd3bed92e060..5a7b3c93f4e8 100644 --- a/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/state/logic/StandardProcessLogicTest.java +++ b/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/state/logic/StandardProcessLogicTest.java @@ -40,6 +40,7 @@ import com.hedera.node.app.service.mono.stats.ExecutionTimeTracker; import com.hedera.node.app.service.mono.txns.schedule.ScheduleProcessing; import com.hedera.node.app.service.mono.txns.span.ExpandHandleSpan; +import com.hedera.node.app.service.mono.txns.span.SpanMapManager; import com.hedera.node.app.service.mono.utils.accessors.PlatformTxnAccessor; import com.hedera.node.app.service.mono.utils.accessors.TxnAccessor; import com.hedera.test.extensions.LogCaptor; @@ -71,6 +72,9 @@ class StandardProcessLogicTest { @Mock private ExpiryManager expiries; + @Mock + private SpanMapManager spanMapManager; + @Mock private InvariantChecks invariantChecks; @@ -140,7 +144,8 @@ void setUp() { recordStreaming, workingView, recordCache, - InitTrigger.GENESIS); + InitTrigger.GENESIS, + spanMapManager); } @Test diff --git a/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/txns/span/SpanMapManagerTest.java b/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/txns/span/SpanMapManagerTest.java index 51a34c4c37c1..dada4607a5ee 100644 --- a/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/txns/span/SpanMapManagerTest.java +++ b/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/txns/span/SpanMapManagerTest.java @@ -198,7 +198,6 @@ void expandsImpliedTransfersForCryptoTransfer() { given(accessor.getTxn()).willReturn(pretendXferTxn); given(accessor.getSpanMap()).willReturn(span); given(accessor.getFunction()).willReturn(CryptoTransfer); - given(accessor.availXferUsageMeta()).willReturn(xferMeta); given(impliedTransfersMarshal.unmarshalFromGrpc(pretendXferTxn.getCryptoTransfer(), payer)) .willReturn(someImpliedXfers); @@ -215,7 +214,6 @@ void setsNumImplicitCreationsOnExpanding() { given(accessor.getTxn()).willReturn(pretendXferTxn); given(accessor.getSpanMap()).willReturn(span); given(accessor.getFunction()).willReturn(CryptoTransfer); - given(accessor.availXferUsageMeta()).willReturn(xferMeta); given(impliedTransfersMarshal.unmarshalFromGrpc(pretendXferTxn.getCryptoTransfer(), payer)) .willReturn(someValidImpliedXfers); @@ -274,7 +272,6 @@ void recomputesImpliedTransfersIfMetaNotMatches() { given(accessor.getTxn()).willReturn(pretendXferTxn); given(accessor.getSpanMap()).willReturn(span); given(accessor.getFunction()).willReturn(CryptoTransfer); - given(accessor.availXferUsageMeta()).willReturn(xferMeta); given(dynamicProperties.maxTransferListSize()).willReturn(maxHbarAdjusts); given(dynamicProperties.maxTokenTransferListSize()).willReturn(maxTokenAdjusts + 1); spanMapAccessor.setImpliedTransfers(accessor, someImpliedXfers); diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/HandleSystemContractOperations.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/HandleSystemContractOperations.java index 5ad9856338a8..8789b0e7f09c 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/HandleSystemContractOperations.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/HandleSystemContractOperations.java @@ -76,7 +76,6 @@ public HandleSystemContractOperations(@NonNull final HandleContext context) { requireNonNull(strategy); requireNonNull(syntheticPayerId); requireNonNull(recordBuilderClass); - return context.dispatchChildTransaction( syntheticBody, recordBuilderClass, activeSignatureTestWith(strategy), syntheticPayerId, CHILD); } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/ClassicTransfersCall.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/ClassicTransfersCall.java index 358f837b870a..dbc01b171918 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/ClassicTransfersCall.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/ClassicTransfersCall.java @@ -168,7 +168,7 @@ public ClassicTransfersCall( * @param systemContractGasCalculator the gas calculator to use * @param enhancement the enhancement to use * @param payerId the payer of the transaction - * @param selector + * @param selector the selector of the call * @return the gas requirement for the transaction to be dispatched */ public static long transferGasRequirement( @@ -263,17 +263,17 @@ private boolean executionIsNotSupported() { private void maybeEmitErcLogsFor( @NonNull final CryptoTransferTransactionBody op, @NonNull final MessageFrame frame) { if (Arrays.equals(ClassicTransfersTranslator.TRANSFER_FROM.selector(), selector)) { - final var fungibleTransfers = op.tokenTransfersOrThrow().get(0); + final var fungibleTransfers = op.tokenTransfersOrThrow().getFirst(); logSuccessfulFungibleTransfer( fungibleTransfers.tokenOrThrow(), fungibleTransfers.transfersOrThrow(), readableAccountStore(), frame); } else if (Arrays.equals(ClassicTransfersTranslator.TRANSFER_NFT_FROM.selector(), selector)) { - final var nftTransfers = op.tokenTransfersOrThrow().get(0); + final var nftTransfers = op.tokenTransfersOrThrow().getFirst(); logSuccessfulNftTransfer( nftTransfers.tokenOrThrow(), - nftTransfers.nftTransfersOrThrow().get(0), + nftTransfers.nftTransfersOrThrow().getFirst(), readableAccountStore(), frame); } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/Erc20TransfersCall.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/Erc20TransfersCall.java index bdba49ef488c..3477afb0b154 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/Erc20TransfersCall.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/Erc20TransfersCall.java @@ -93,7 +93,7 @@ public Erc20TransfersCall( * {@inheritDoc} */ @Override - public @NonNull PricedResult execute() { + public @NonNull PricedResult execute(@NonNull final MessageFrame frame) { // https://eips.ethereum.org/EIPS/eip-20 final var syntheticTransfer = syntheticTransferOrTransferFrom(senderId); final var selector = (from == null) ? ERC_20_TRANSFER.selector() : ERC_20_TRANSFER_FROM.selector(); @@ -103,11 +103,7 @@ public Erc20TransfersCall( return reversionWith(INVALID_TOKEN_ID, gasRequirement); } final var recordBuilder = systemContractOperations() - .dispatch( - syntheticTransferOrTransferFrom(senderId), - verificationStrategy, - senderId, - ContractCallRecordBuilder.class); + .dispatch(syntheticTransfer, verificationStrategy, senderId, ContractCallRecordBuilder.class); final var status = recordBuilder.status(); if (status != SUCCESS) { if (status == NOT_SUPPORTED) { @@ -118,25 +114,8 @@ public Erc20TransfersCall( return gasOnly(revertResult(recordBuilder, gasRequirement), status, false); } } else { - final var encodedOutput = (from == null) - ? ERC_20_TRANSFER.getOutputs().encodeElements(true) - : ERC_20_TRANSFER_FROM.getOutputs().encodeElements(true); - recordBuilder.contractCallResult(ContractFunctionResult.newBuilder() - .contractCallResult(Bytes.wrap(encodedOutput.array())) - .build()); - return gasOnly(successResult(encodedOutput, gasRequirement, recordBuilder), status, false); - } - } - - @NonNull - @Override - public PricedResult execute(final MessageFrame frame) { - final var result = execute(); - - if (result.fullResult().result().getState().equals(MessageFrame.State.COMPLETED_SUCCESS)) { - final var tokenTransferLists = syntheticTransferOrTransferFrom(senderId) - .cryptoTransferOrThrow() - .tokenTransfersOrThrow(); + final var tokenTransferLists = + syntheticTransfer.cryptoTransferOrThrow().tokenTransfersOrThrow(); for (final var fungibleTransfers : tokenTransferLists) { TransferEventLoggingUtils.logSuccessfulFungibleTransfer( requireNonNull(tokenId), @@ -144,8 +123,14 @@ public PricedResult execute(final MessageFrame frame) { enhancement.nativeOperations().readableAccountStore(), frame); } + final var encodedOutput = (from == null) + ? ERC_20_TRANSFER.getOutputs().encodeElements(true) + : ERC_20_TRANSFER_FROM.getOutputs().encodeElements(true); + recordBuilder.contractCallResult(ContractFunctionResult.newBuilder() + .contractCallResult(Bytes.wrap(encodedOutput.array())) + .build()); + return gasOnly(successResult(encodedOutput, gasRequirement, recordBuilder), status, false); } - return result; } private TransactionBody syntheticTransferOrTransferFrom(@NonNull final AccountID spenderId) { diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/TransferEventLoggingUtils.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/TransferEventLoggingUtils.java index e8e7c11e4386..4765cb383b2b 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/TransferEventLoggingUtils.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/TransferEventLoggingUtils.java @@ -44,6 +44,9 @@ private TransferEventLoggingUtils() { * Logs a successful ERC-20 transfer event based on the Hedera-style representation of the fungible * balance adjustments. * + *

IMPORTANT: The adjusts list must be length two and the credit adjustment + * must appear first. + * * @param tokenId the token ID * @param adjusts the Hedera-style representation of the fungible balance adjustments * @param accountStore the account store to get account addresses from @@ -58,19 +61,12 @@ public static void logSuccessfulFungibleTransfer( requireNonNull(frame); requireNonNull(adjusts); requireNonNull(accountStore); - var senderId = AccountID.DEFAULT; - var receiverId = AccountID.DEFAULT; - long amount = 0L; - for (final var adjust : adjusts) { - amount = Math.abs(adjust.amount()); - if (adjust.amount() > 0) { - receiverId = adjust.accountIDOrThrow(); - } else { - senderId = adjust.accountIDOrThrow(); - } + final var credit = adjusts.getFirst(); + if (credit.amount() < 0) { + throw new IllegalArgumentException("Credit adjustment must appear first"); } - frame.addLog(builderFor(tokenId, senderId, receiverId, accountStore) - .forDataItem(amount) + frame.addLog(builderFor(tokenId, adjusts.getLast().accountIDOrThrow(), credit.accountIDOrThrow(), accountStore) + .forDataItem(credit.amount()) .build()); } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/transfer/Erc20TransfersCallTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/transfer/Erc20TransfersCallTest.java index b4690caf4c90..8cdd0e78f59f 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/transfer/Erc20TransfersCallTest.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/transfer/Erc20TransfersCallTest.java @@ -20,10 +20,12 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOKEN_ID; import static com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.transfer.Erc20TransfersTranslator.ERC_20_TRANSFER; import static com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.transfer.Erc20TransfersTranslator.ERC_20_TRANSFER_FROM; +import static com.hedera.node.app.service.contract.impl.test.TestHelpers.ALIASED_RECEIVER; import static com.hedera.node.app.service.contract.impl.test.TestHelpers.A_NEW_ACCOUNT_ID; import static com.hedera.node.app.service.contract.impl.test.TestHelpers.B_NEW_ACCOUNT_ID; import static com.hedera.node.app.service.contract.impl.test.TestHelpers.EIP_1014_ADDRESS; import static com.hedera.node.app.service.contract.impl.test.TestHelpers.FUNGIBLE_TOKEN_ID; +import static com.hedera.node.app.service.contract.impl.test.TestHelpers.OWNER_ACCOUNT; import static com.hedera.node.app.service.contract.impl.test.TestHelpers.SENDER_ID; import static com.hedera.node.app.service.contract.impl.test.TestHelpers.asBytesResult; import static com.hedera.node.app.service.contract.impl.test.TestHelpers.readableRevertReason; @@ -43,6 +45,7 @@ import com.hedera.node.app.service.contract.impl.records.ContractCallRecordBuilder; import com.hedera.node.app.service.contract.impl.test.exec.systemcontracts.hts.HtsCallTestBase; import com.hedera.node.app.service.contract.impl.utils.ConversionUtils; +import com.hedera.node.app.service.token.ReadableAccountStore; import org.apache.tuweni.bytes.Bytes; import org.hyperledger.besu.evm.frame.MessageFrame; import org.junit.jupiter.api.Test; @@ -56,6 +59,9 @@ class Erc20TransfersCallTest extends HtsCallTestBase { @Mock private AddressIdConverter addressIdConverter; + @Mock + private ReadableAccountStore readableAccountStore; + @Mock private VerificationStrategy verificationStrategy; @@ -81,7 +87,7 @@ void revertsOnMissingToken() { addressIdConverter, false); - final var result = subject.execute().fullResult().result(); + final var result = subject.execute(frame).fullResult().result(); assertEquals(MessageFrame.State.REVERT, result.getState()); assertEquals(Bytes.wrap(INVALID_TOKEN_ID.protoName().getBytes()), result.getOutput()); @@ -97,10 +103,13 @@ void transferHappyPathSucceedsWithTrue() { eq(ContractCallRecordBuilder.class))) .willReturn(recordBuilder); given(recordBuilder.status()).willReturn(ResponseCodeEnum.SUCCESS); + given(nativeOperations.readableAccountStore()).willReturn(readableAccountStore); + given(readableAccountStore.getAccountById(SENDER_ID)).willReturn(OWNER_ACCOUNT); + given(readableAccountStore.getAccountById(B_NEW_ACCOUNT_ID)).willReturn(ALIASED_RECEIVER); subject = subjectForTransfer(1L); - final var result = subject.execute().fullResult().result(); + final var result = subject.execute(frame).fullResult().result(); assertEquals(MessageFrame.State.COMPLETED_SUCCESS, result.getState()); assertEquals(asBytesResult(ERC_20_TRANSFER.getOutputs().encodeElements(true)), result.getOutput()); @@ -115,11 +124,14 @@ void transferFromHappyPathSucceedsWithTrue() { eq(SENDER_ID), eq(ContractCallRecordBuilder.class))) .willReturn(recordBuilder); + given(nativeOperations.readableAccountStore()).willReturn(readableAccountStore); + given(readableAccountStore.getAccountById(A_NEW_ACCOUNT_ID)).willReturn(OWNER_ACCOUNT); + given(readableAccountStore.getAccountById(B_NEW_ACCOUNT_ID)).willReturn(ALIASED_RECEIVER); given(recordBuilder.status()).willReturn(ResponseCodeEnum.SUCCESS); subject = subjectForTransferFrom(1L); - final var result = subject.execute().fullResult().result(); + final var result = subject.execute(frame).fullResult().result(); assertEquals(MessageFrame.State.COMPLETED_SUCCESS, result.getState()); assertEquals(asBytesResult(ERC_20_TRANSFER_FROM.getOutputs().encodeElements(true)), result.getOutput()); @@ -138,7 +150,7 @@ void unhappyPathRevertsWithReason() { subject = subjectForTransfer(1L); - final var result = subject.execute().fullResult().result(); + final var result = subject.execute(frame).fullResult().result(); assertEquals(MessageFrame.State.REVERT, result.getState()); assertEquals(readableRevertReason(INSUFFICIENT_ACCOUNT_BALANCE), result.getOutput()); From fa950dd99fb5f6e34a604ff3be1ada4a36bf2570 Mon Sep 17 00:00:00 2001 From: anthony-swirldslabs <152534762+anthony-swirldslabs@users.noreply.github.com> Date: Fri, 1 Mar 2024 11:10:43 -0800 Subject: [PATCH 013/115] feat: pbj-208: upgrade PBJ dependency to 0.7.20 (#11829) Signed-off-by: Anthony Petrov --- hedera-dependency-versions/build.gradle.kts | 2 +- settings.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hedera-dependency-versions/build.gradle.kts b/hedera-dependency-versions/build.gradle.kts index 60165dd4252b..acecd85136bc 100644 --- a/hedera-dependency-versions/build.gradle.kts +++ b/hedera-dependency-versions/build.gradle.kts @@ -58,7 +58,7 @@ moduleInfo { version("com.google.jimfs", "1.2") version("com.google.protobuf", protobufVersion) version("com.google.protobuf.util", protobufVersion) - version("com.hedera.pbj.runtime", "0.7.19") + version("com.hedera.pbj.runtime", "0.7.20") version("com.squareup.javapoet", "1.13.0") version("com.sun.jna", "5.12.1") version("dagger", daggerVersion) diff --git a/settings.gradle.kts b/settings.gradle.kts index 0f6b51255e69..1dd165decb97 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -150,6 +150,6 @@ dependencyResolutionManagement { version("grpc-proto", "1.45.1") version("hapi-proto", hapiProtoVersion) - plugin("pbj", "com.hedera.pbj.pbj-compiler").version("0.7.19") + plugin("pbj", "com.hedera.pbj.pbj-compiler").version("0.7.20") } } From 6e093ca00efb736218c870e146606f49830a9fb9 Mon Sep 17 00:00:00 2001 From: Jendrik Johannes Date: Mon, 4 Mar 2024 13:45:52 +0100 Subject: [PATCH 014/115] build: fix visibility of JMH classes in IDEA (#11792) Signed-off-by: Jendrik Johannes --- ....hashgraph.benchmark-conventions.gradle.kts | 8 ++++---- platform-sdk/swirlds-base/gradle.properties | 18 ------------------ 2 files changed, 4 insertions(+), 22 deletions(-) delete mode 100644 platform-sdk/swirlds-base/gradle.properties diff --git a/build-logic/project-plugins/src/main/kotlin/com.hedera.hashgraph.benchmark-conventions.gradle.kts b/build-logic/project-plugins/src/main/kotlin/com.hedera.hashgraph.benchmark-conventions.gradle.kts index c167c70a8512..1da2467ccc7e 100644 --- a/build-logic/project-plugins/src/main/kotlin/com.hedera.hashgraph.benchmark-conventions.gradle.kts +++ b/build-logic/project-plugins/src/main/kotlin/com.hedera.hashgraph.benchmark-conventions.gradle.kts @@ -19,7 +19,7 @@ import me.champeau.jmh.JMHTask plugins { id("me.champeau.jmh") } jmh { - jmhVersion = "1.36" + jmhVersion = "1.37" includeTests = false } @@ -38,9 +38,9 @@ configurations { // The way the JMH plugin interacts with this in the 'jmhJar' task triggers this Gradle issue: // https://github.com/gradle/gradle/issues/27372 // And since 'jmhJar' builds a fat jar, module information is not needed here anyway. - jmhRuntimeClasspath { - attributes { attribute(Attribute.of("javaModule", Boolean::class.javaObjectType), false) } - } + val javaModule = Attribute.of("javaModule", Boolean::class.javaObjectType) + jmhRuntimeClasspath { attributes { attribute(javaModule, false) } } + jmhCompileClasspath { attributes { attribute(javaModule, false) } } } tasks.assemble { diff --git a/platform-sdk/swirlds-base/gradle.properties b/platform-sdk/swirlds-base/gradle.properties deleted file mode 100644 index 351bac2b9245..000000000000 --- a/platform-sdk/swirlds-base/gradle.properties +++ /dev/null @@ -1,18 +0,0 @@ -# -# Copyright 2016-2022 Hedera Hashgraph, LLC -# -# This software is the confidential and proprietary information of -# Hedera Hashgraph, LLC. ("Confidential Information"). You shall not -# disclose such Confidential Information and shall use it only in -# accordance with the terms of the license agreement you entered into -# with Hedera Hashgraph. -# -# HEDERA HASHGRAPH MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY OF -# THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -# TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -# PARTICULAR PURPOSE, OR NON-INFRINGEMENT. HEDERA HASHGRAPH SHALL NOT BE LIABLE FOR -# ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR -# DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. -# - -includesRegex=.* From 42264750661403c58c8e722e2cf72ec544195607 Mon Sep 17 00:00:00 2001 From: Jendrik Johannes Date: Mon, 4 Mar 2024 19:43:24 +0100 Subject: [PATCH 015/115] build: rename 'com.hedera.node.blocknode' -> 'com.hedera.storage.blocknode' (#11675) Signed-off-by: Jendrik Johannes --- .../src/main/java/module-info.java | 2 +- .../src/test/java/module-info.java | 4 ++-- .../src/main/java/module-info.java | 16 ++++++++-------- .../src/test/java/module-info.java | 4 ++-- .../src/main/java/module-info.java | 2 +- .../src/test/java/module-info.java | 4 ++-- .../src/main/java/module-info.java | 10 +++++----- .../src/test/java/module-info.java | 4 ++-- .../src/main/java/module-info.java | 10 +++++----- .../src/test/java/module-info.java | 4 ++-- .../src/main/java/module-info.java | 2 +- .../src/test/java/module-info.java | 4 ++-- .../src/main/java/module-info.java | 4 ++-- .../src/test/java/module-info.java | 4 ++-- build-logic/project-plugins/build.gradle.kts | 2 +- ...hashgraph.jpms-module-dependencies.gradle.kts | 14 +++++++++++--- 16 files changed, 49 insertions(+), 41 deletions(-) diff --git a/block-node/blocknode-core-spi/src/main/java/module-info.java b/block-node/blocknode-core-spi/src/main/java/module-info.java index 3a555f8a75eb..5ec946ff6cad 100644 --- a/block-node/blocknode-core-spi/src/main/java/module-info.java +++ b/block-node/blocknode-core-spi/src/main/java/module-info.java @@ -1,4 +1,4 @@ -module com.hedera.node.blocknode.core.spi { +module com.hedera.storage.blocknode.core.spi { // Export packages with public interfaces to the world as needed. exports com.hedera.node.blocknode.core.spi; } diff --git a/block-node/blocknode-core-spi/src/test/java/module-info.java b/block-node/blocknode-core-spi/src/test/java/module-info.java index 316a96959728..f3fd340b46c2 100644 --- a/block-node/blocknode-core-spi/src/test/java/module-info.java +++ b/block-node/blocknode-core-spi/src/test/java/module-info.java @@ -1,9 +1,9 @@ -module com.hedera.node.blocknode.core.spi.test { +module com.hedera.storage.blocknode.core.spi.test { // Open test packages to JUnit 5 and Mockito as required. opens com.hedera.node.blocknode.core.spi.test to org.junit.platform.commons; // Require other modules needed for the unit tests to compile. - requires com.hedera.node.blocknode.core.spi; + requires com.hedera.storage.blocknode.core.spi; requires org.junit.jupiter.api; } diff --git a/block-node/blocknode-core/src/main/java/module-info.java b/block-node/blocknode-core/src/main/java/module-info.java index c1d4f3f490a4..7813bfcf3868 100644 --- a/block-node/blocknode-core/src/main/java/module-info.java +++ b/block-node/blocknode-core/src/main/java/module-info.java @@ -1,17 +1,17 @@ -module com.hedera.node.blocknode.core { +module com.hedera.storage.blocknode.core { // Selectively export non-public packages to the test module. exports com.hedera.node.blocknode.core to - com.hedera.node.blocknode.core.test; + com.hedera.storage.blocknode.core.test; // Require the modules needed for compilation. - requires com.hedera.node.blocknode.filesystem.local; - requires com.hedera.node.blocknode.filesystem.s3; + requires com.hedera.storage.blocknode.filesystem.local; + requires com.hedera.storage.blocknode.filesystem.s3; // Require modules which are needed for compilation and should be available to all modules that depend on this // module (including tests and other source sets). - requires transitive com.hedera.node.blocknode.core.spi; - requires transitive com.hedera.node.blocknode.filesystem.api; - requires transitive com.hedera.node.blocknode.grpc.api; - requires transitive com.hedera.node.blocknode.state; + requires transitive com.hedera.storage.blocknode.core.spi; + requires transitive com.hedera.storage.blocknode.filesystem.api; + requires transitive com.hedera.storage.blocknode.grpc.api; + requires transitive com.hedera.storage.blocknode.state; requires transitive com.hedera.node.hapi; } diff --git a/block-node/blocknode-core/src/test/java/module-info.java b/block-node/blocknode-core/src/test/java/module-info.java index f978525558bd..822ec0bb156c 100644 --- a/block-node/blocknode-core/src/test/java/module-info.java +++ b/block-node/blocknode-core/src/test/java/module-info.java @@ -1,10 +1,10 @@ -module com.hedera.node.blocknode.core.test { +module com.hedera.storage.blocknode.core.test { // Open test packages to JUnit 5 and Mockito as required. opens com.hedera.node.blocknode.core.test to org.junit.platform.commons; // Require other modules needed for the unit tests to compile. - requires com.hedera.node.blocknode.core; + requires com.hedera.storage.blocknode.core; requires com.swirlds.platform.core; requires org.junit.jupiter.api; } diff --git a/block-node/blocknode-filesystem-api/src/main/java/module-info.java b/block-node/blocknode-filesystem-api/src/main/java/module-info.java index 4b4228379b1d..d1b5a820fc35 100644 --- a/block-node/blocknode-filesystem-api/src/main/java/module-info.java +++ b/block-node/blocknode-filesystem-api/src/main/java/module-info.java @@ -1,4 +1,4 @@ -module com.hedera.node.blocknode.filesystem.api { +module com.hedera.storage.blocknode.filesystem.api { // Export packages with public interfaces to the world as needed. exports com.hedera.node.blocknode.filesystem.api; } diff --git a/block-node/blocknode-filesystem-api/src/test/java/module-info.java b/block-node/blocknode-filesystem-api/src/test/java/module-info.java index 755f04491629..f1a6ca8b380a 100644 --- a/block-node/blocknode-filesystem-api/src/test/java/module-info.java +++ b/block-node/blocknode-filesystem-api/src/test/java/module-info.java @@ -1,9 +1,9 @@ -module com.hedera.node.blocknode.filesystem.api.test { +module com.hedera.storage.blocknode.filesystem.api.test { // Open test packages to JUnit 5 and Mockito as required. opens com.hedera.node.blocknode.filesystem.api.test to org.junit.platform.commons; // Require other modules needed for the unit tests to compile. - requires com.hedera.node.blocknode.filesystem.api; + requires com.hedera.storage.blocknode.filesystem.api; requires org.junit.jupiter.api; } diff --git a/block-node/blocknode-filesystem-local/src/main/java/module-info.java b/block-node/blocknode-filesystem-local/src/main/java/module-info.java index a752695b8b98..105019e078ea 100644 --- a/block-node/blocknode-filesystem-local/src/main/java/module-info.java +++ b/block-node/blocknode-filesystem-local/src/main/java/module-info.java @@ -1,13 +1,13 @@ -module com.hedera.node.blocknode.filesystem.local { +module com.hedera.storage.blocknode.filesystem.local { // Selectively export non-public packages to the test module. exports com.hedera.node.blocknode.filesystem.local to - com.hedera.node.blocknode.filesystem.local.test, - com.hedera.node.blocknode.core; + com.hedera.storage.blocknode.filesystem.local.test, + com.hedera.storage.blocknode.core; // Require the modules needed for compilation. - requires com.hedera.node.blocknode.core.spi; + requires com.hedera.storage.blocknode.core.spi; // Require modules which are needed for compilation and should be available to all modules that depend on this // module (including tests and other source sets). - requires transitive com.hedera.node.blocknode.filesystem.api; + requires transitive com.hedera.storage.blocknode.filesystem.api; } diff --git a/block-node/blocknode-filesystem-local/src/test/java/module-info.java b/block-node/blocknode-filesystem-local/src/test/java/module-info.java index 7d5561fb7c50..baf25f0d73a0 100644 --- a/block-node/blocknode-filesystem-local/src/test/java/module-info.java +++ b/block-node/blocknode-filesystem-local/src/test/java/module-info.java @@ -1,9 +1,9 @@ -module com.hedera.node.blocknode.filesystem.local.test { +module com.hedera.storage.blocknode.filesystem.local.test { // Open test packages to JUnit 5 and Mockito as required. opens com.hedera.node.blocknode.filesystem.local.test to org.junit.platform.commons; // Require other modules needed for the unit tests to compile. - requires com.hedera.node.blocknode.filesystem.local; + requires com.hedera.storage.blocknode.filesystem.local; requires org.junit.jupiter.api; } diff --git a/block-node/blocknode-filesystem-s3/src/main/java/module-info.java b/block-node/blocknode-filesystem-s3/src/main/java/module-info.java index 50c1d39d1039..8ebea648d375 100644 --- a/block-node/blocknode-filesystem-s3/src/main/java/module-info.java +++ b/block-node/blocknode-filesystem-s3/src/main/java/module-info.java @@ -1,13 +1,13 @@ -module com.hedera.node.blocknode.filesystem.s3 { +module com.hedera.storage.blocknode.filesystem.s3 { // Selectively export non-public packages to the test module. exports com.hedera.node.blocknode.filesystem.s3 to - com.hedera.node.blocknode.filesystem.s3.test, - com.hedera.node.blocknode.core; + com.hedera.storage.blocknode.filesystem.s3.test, + com.hedera.storage.blocknode.core; // Require the modules needed for compilation. - requires com.hedera.node.blocknode.core.spi; + requires com.hedera.storage.blocknode.core.spi; // Require modules which are needed for compilation and should be available to all modules that depend on this // module (including tests and other source sets). - requires transitive com.hedera.node.blocknode.filesystem.api; + requires transitive com.hedera.storage.blocknode.filesystem.api; } diff --git a/block-node/blocknode-filesystem-s3/src/test/java/module-info.java b/block-node/blocknode-filesystem-s3/src/test/java/module-info.java index c6dedff11135..40e9d6293ac1 100644 --- a/block-node/blocknode-filesystem-s3/src/test/java/module-info.java +++ b/block-node/blocknode-filesystem-s3/src/test/java/module-info.java @@ -1,9 +1,9 @@ -module com.hedera.node.blocknode.filesystem.s3.test { +module com.hedera.storage.blocknode.filesystem.s3.test { // Open test packages to JUnit 5 and Mockito as required. opens com.hedera.node.blocknode.filesystem.s3.test to org.junit.platform.commons; // Require other modules needed for the unit tests to compile. - requires com.hedera.node.blocknode.filesystem.s3; + requires com.hedera.storage.blocknode.filesystem.s3; requires org.junit.jupiter.api; } diff --git a/block-node/blocknode-grpc-api/src/main/java/module-info.java b/block-node/blocknode-grpc-api/src/main/java/module-info.java index 6c9454932248..d6c30311f51c 100644 --- a/block-node/blocknode-grpc-api/src/main/java/module-info.java +++ b/block-node/blocknode-grpc-api/src/main/java/module-info.java @@ -1,4 +1,4 @@ -module com.hedera.node.blocknode.grpc.api { +module com.hedera.storage.blocknode.grpc.api { // Export packages with public interfaces to the world as needed. exports com.hedera.node.blocknode.grpc.api; } diff --git a/block-node/blocknode-grpc-api/src/test/java/module-info.java b/block-node/blocknode-grpc-api/src/test/java/module-info.java index 6533146309fe..00b03d3be57d 100644 --- a/block-node/blocknode-grpc-api/src/test/java/module-info.java +++ b/block-node/blocknode-grpc-api/src/test/java/module-info.java @@ -1,9 +1,9 @@ -module com.hedera.node.blocknode.grpc.api.test { +module com.hedera.storage.blocknode.grpc.api.test { // Open test packages to JUnit 5 and Mockito as required. opens com.hedera.node.blocknode.core.grpc.api.test to org.junit.platform.commons; // Require other modules needed for the unit tests to compile. - requires com.hedera.node.blocknode.grpc.api; + requires com.hedera.storage.blocknode.grpc.api; requires org.junit.jupiter.api; } diff --git a/block-node/blocknode-state/src/main/java/module-info.java b/block-node/blocknode-state/src/main/java/module-info.java index bf7daf58bf97..f5c47ff784e3 100644 --- a/block-node/blocknode-state/src/main/java/module-info.java +++ b/block-node/blocknode-state/src/main/java/module-info.java @@ -1,9 +1,9 @@ -module com.hedera.node.blocknode.state { +module com.hedera.storage.blocknode.state { // Export the packages that should be available to other modules. exports com.hedera.node.blocknode.state; // Require the modules needed for compilation. - requires com.hedera.node.blocknode.core.spi; + requires com.hedera.storage.blocknode.core.spi; // Require modules which are needed for compilation and should be available to all modules that depend on this // module (including tests and other source sets diff --git a/block-node/blocknode-state/src/test/java/module-info.java b/block-node/blocknode-state/src/test/java/module-info.java index a17d288dd201..3fba97935f8b 100644 --- a/block-node/blocknode-state/src/test/java/module-info.java +++ b/block-node/blocknode-state/src/test/java/module-info.java @@ -1,9 +1,9 @@ -module com.hedera.node.blocknode.state.test { +module com.hedera.storage.blocknode.state.test { // Open test packages to JUnit 5 and Mockito as required. opens com.hedera.node.blocknode.state.test to org.junit.platform.commons; // Require other modules needed for the unit tests to compile. - requires com.hedera.node.blocknode.state; + requires com.hedera.storage.blocknode.state; requires org.junit.jupiter.api; } diff --git a/build-logic/project-plugins/build.gradle.kts b/build-logic/project-plugins/build.gradle.kts index d036d31d4598..3864f1d4be7b 100644 --- a/build-logic/project-plugins/build.gradle.kts +++ b/build-logic/project-plugins/build.gradle.kts @@ -33,6 +33,6 @@ dependencies { implementation("net.swiftzer.semver:semver:1.3.0") implementation("org.gradlex:extra-java-module-info:1.8") implementation("org.gradlex:java-ecosystem-capabilities:1.5.1") - implementation("org.gradlex:java-module-dependencies:1.6.1") + implementation("org.gradlex:java-module-dependencies:1.6.2") implementation("org.owasp:dependency-check-gradle:9.0.9") } diff --git a/build-logic/project-plugins/src/main/kotlin/com.hedera.hashgraph.jpms-module-dependencies.gradle.kts b/build-logic/project-plugins/src/main/kotlin/com.hedera.hashgraph.jpms-module-dependencies.gradle.kts index a52ca403189a..820d2f956976 100644 --- a/build-logic/project-plugins/src/main/kotlin/com.hedera.hashgraph.jpms-module-dependencies.gradle.kts +++ b/build-logic/project-plugins/src/main/kotlin/com.hedera.hashgraph.jpms-module-dependencies.gradle.kts @@ -16,6 +16,14 @@ plugins { id("org.gradlex.java-module-dependencies") } -// The following is required as long as we use different Module Name prefixes in the project. Right -// now we have 'com.hedera.node.' (works automatically) and 'com.' (for 'com.swirlds...' modules). -javaModuleDependencies { moduleNamePrefixToGroup.put("com.", "com.swirlds") } +// The following is required as we use different Module Name prefixes. Right now we have: +// - 'com.' for 'com.swirlds' modules +// - 'com.hedera.node.' for 'com.hedera.hashgraph' modules +// - 'com.hedera.storage' for 'com.hedera.storage.blocknode' modules +// If one of the module groups has 'requires' to modules of another group, we need to register +// that module group here. +javaModuleDependencies { + moduleNamePrefixToGroup.put("com.", "com.swirlds") + moduleNamePrefixToGroup.put("com.hedera.node.", "com.hedera.hashgraph") + moduleNamePrefixToGroup.put("com.hedera.storage.", "com.hedera.storage.blocknode") +} From 5decfe9e7c1f524a51b3c09a9133eb2d1791426f Mon Sep 17 00:00:00 2001 From: Lazar Petrovic Date: Tue, 5 Mar 2024 10:16:21 +0100 Subject: [PATCH 016/115] chore: decouple socket factory (#11836) Signed-off-by: Lazar Petrovic --- .../benchmark/reconnect/PairedStreams.java | 2 +- .../java/com/swirlds/platform/Utilities.java | 31 ++++++++++ .../swirlds/platform/crypto/CryptoStatic.java | 23 +++++++- .../swirlds/platform/gossip/SyncGossip.java | 47 ++------------- .../platform/network/NetworkUtils.java | 59 +++++++++++++++++++ .../swirlds/platform/network/PeerInfo.java | 35 +++++++++++ .../connectivity/ConnectionServer.java | 16 +---- .../network/connectivity/SocketFactory.java | 31 ++++++---- .../network/connectivity/TcpFactory.java | 7 ++- .../network/connectivity/TlsFactory.java | 48 +++++++++------ .../platform/system/address/Address.java | 11 ---- .../connectivity/SocketFactoryTest.java | 50 ++++++++++------ .../test/network/ConnectionServerTest.java | 5 +- 13 files changed, 242 insertions(+), 123 deletions(-) create mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/PeerInfo.java diff --git a/platform-sdk/swirlds-benchmarks/src/jmh/java/com/swirlds/benchmark/reconnect/PairedStreams.java b/platform-sdk/swirlds-benchmarks/src/jmh/java/com/swirlds/benchmark/reconnect/PairedStreams.java index 303650277a63..7569b7bd1cee 100644 --- a/platform-sdk/swirlds-benchmarks/src/jmh/java/com/swirlds/benchmark/reconnect/PairedStreams.java +++ b/platform-sdk/swirlds-benchmarks/src/jmh/java/com/swirlds/benchmark/reconnect/PairedStreams.java @@ -48,7 +48,7 @@ public class PairedStreams implements AutoCloseable { public PairedStreams(final SocketConfig socketConfig) throws IOException { - server = new TcpFactory(socketConfig).createServerSocket(new byte[] {127, 0, 0, 1}, 0); + server = new TcpFactory(socketConfig).createServerSocket(0); teacherSocket = new Socket("127.0.0.1", server.getLocalPort()); learnerSocket = server.accept(); 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 9a6172b74ddb..91a45f6a6c1e 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 @@ -18,14 +18,21 @@ import com.swirlds.common.io.streams.SerializableDataInputStream; import com.swirlds.common.io.streams.SerializableDataOutputStream; +import com.swirlds.common.platform.NodeId; import com.swirlds.platform.internal.Deserializer; import com.swirlds.platform.internal.Serializer; +import com.swirlds.platform.network.PeerInfo; +import com.swirlds.platform.system.address.AddressBook; import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; import java.net.SocketException; import java.util.Arrays; import java.util.List; +import java.util.Objects; +import java.util.Spliterator; +import java.util.Spliterators; import java.util.function.Supplier; +import java.util.stream.StreamSupport; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -353,4 +360,28 @@ public static boolean hasAnyCauseSuppliedType( } return false; } + + /** + * Create a list of PeerInfos from the address book. The list will contain information about all peers but not us. + * + * @param addressBook + * the address book to create the list from + * @param selfId + * our ID + * @return a list of PeerInfo + */ + public static @NonNull List createPeerInfoList( + @NonNull final AddressBook addressBook, @NonNull final NodeId selfId) { + Objects.requireNonNull(addressBook); + Objects.requireNonNull(selfId); + return StreamSupport.stream( + Spliterators.spliteratorUnknownSize(addressBook.iterator(), Spliterator.ORDERED), false) + .filter(address -> !address.getNodeId().equals(selfId)) + .map(address -> new PeerInfo( + address.getNodeId(), + address.getSelfName(), + address.getHostnameExternal(), + address.getSigCert())) + .toList(); + } } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/crypto/CryptoStatic.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/crypto/CryptoStatic.java index 45d42cbf4fe0..9c29d64d73ce 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/crypto/CryptoStatic.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/crypto/CryptoStatic.java @@ -22,6 +22,7 @@ import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; import static com.swirlds.logging.legacy.LogMarker.STARTUP; import static com.swirlds.platform.crypto.CryptoConstants.PUBLIC_KEYS_FILE; +import static com.swirlds.platform.crypto.KeyCertPurpose.SIGNING; import com.swirlds.common.crypto.CryptographyException; import com.swirlds.common.crypto.config.CryptoConfig; @@ -33,6 +34,7 @@ import com.swirlds.platform.Utilities; import com.swirlds.platform.config.BasicConfig; import com.swirlds.platform.config.PathsConfig; +import com.swirlds.platform.network.PeerInfo; import com.swirlds.platform.state.address.AddressBookNetworkUtils; import com.swirlds.platform.system.SystemExitCode; import com.swirlds.platform.system.SystemExitUtils; @@ -56,6 +58,7 @@ import java.security.Signature; import java.security.SignatureException; import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayList; @@ -448,7 +451,7 @@ static void copyPublicKeys(final PublicStores publicStores, final AddressBook ad final NodeId nodeId = addressBook.getNodeId(i); final Address add = addressBook.getAddress(nodeId); final String name = nameToAlias(add.getSelfName()); - final X509Certificate sigCert = publicStores.getCertificate(KeyCertPurpose.SIGNING, name); + final X509Certificate sigCert = publicStores.getCertificate(SIGNING, name); final X509Certificate agrCert = publicStores.getCertificate(KeyCertPurpose.AGREEMENT, name); addressBook.add( addressBook.getAddress(nodeId).copySetSigCert(sigCert).copySetAgreeCert(agrCert)); @@ -577,4 +580,22 @@ public static Map initNodeSecurity( return keysAndCerts; } + + /** + * Create a trust store that contains the public keys of all the members in the peer list + * + * @param peers all the peers in the network + * @return a trust store containing the public keys of all the members + * @throws KeyStoreException if there is no provider that supports {@link CryptoConstants#KEYSTORE_TYPE} + */ + public static @NonNull KeyStore createPublicKeyStore(@NonNull final List peers) throws KeyStoreException { + Objects.requireNonNull(peers); + final KeyStore store = CryptoStatic.createEmptyTrustStore(); + for (final PeerInfo peer : peers) { + final String name = nameToAlias(peer.nodeName()); + final Certificate sigCert = peer.signingCertificate(); + store.setCertificateEntry(SIGNING.storeName(name), sigCert); + } + return store; + } } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/SyncGossip.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/SyncGossip.java index 3176e641bbbe..198a042a88db 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/SyncGossip.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/SyncGossip.java @@ -24,7 +24,6 @@ import com.swirlds.base.time.Time; import com.swirlds.common.context.PlatformContext; import com.swirlds.common.crypto.Hash; -import com.swirlds.common.crypto.config.CryptoConfig; import com.swirlds.common.merkle.synchronization.config.ReconnectConfig; import com.swirlds.common.notification.NotificationEngine; import com.swirlds.common.platform.NodeId; @@ -50,7 +49,7 @@ import com.swirlds.platform.network.Connection; import com.swirlds.platform.network.ConnectionTracker; import com.swirlds.platform.network.NetworkMetrics; -import com.swirlds.platform.network.SocketConfig; +import com.swirlds.platform.network.NetworkUtils; import com.swirlds.platform.network.communication.NegotiationProtocols; import com.swirlds.platform.network.communication.NegotiatorThread; import com.swirlds.platform.network.communication.handshake.HashCompareHandshake; @@ -59,8 +58,6 @@ import com.swirlds.platform.network.connectivity.InboundConnectionHandler; import com.swirlds.platform.network.connectivity.OutboundConnectionCreator; import com.swirlds.platform.network.connectivity.SocketFactory; -import com.swirlds.platform.network.connectivity.TcpFactory; -import com.swirlds.platform.network.connectivity.TlsFactory; import com.swirlds.platform.network.topology.NetworkTopology; import com.swirlds.platform.network.topology.StaticConnectionManagers; import com.swirlds.platform.network.topology.StaticTopology; @@ -77,19 +74,12 @@ import com.swirlds.platform.state.nexus.SignedStateNexus; import com.swirlds.platform.state.signed.ReservedSignedState; import com.swirlds.platform.state.signed.SignedState; -import com.swirlds.platform.system.PlatformConstructionException; import com.swirlds.platform.system.SoftwareVersion; import com.swirlds.platform.system.address.Address; import com.swirlds.platform.system.address.AddressBook; import com.swirlds.platform.system.status.PlatformStatusManager; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import java.io.IOException; -import java.security.KeyManagementException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.UnrecoverableKeyException; -import java.security.cert.CertificateException; import java.time.Duration; import java.util.ArrayList; import java.util.List; @@ -196,12 +186,10 @@ protected SyncGossip( final BasicConfig basicConfig = platformContext.getConfiguration().getConfigData(BasicConfig.class); - final CryptoConfig cryptoConfig = platformContext.getConfiguration().getConfigData(CryptoConfig.class); - final SocketConfig socketConfig = platformContext.getConfiguration().getConfigData(SocketConfig.class); - topology = new StaticTopology(addressBook, selfId, basicConfig.numConnections()); - final SocketFactory socketFactory = socketFactory(keysAndCerts, cryptoConfig, socketConfig); + final SocketFactory socketFactory = + NetworkUtils.createSocketFactory(selfId, addressBook, keysAndCerts, platformContext.getConfiguration()); // create an instance that can create new outbound connections final OutboundConnectionCreator connectionCreator = new OutboundConnectionCreator( platformContext, selfId, this, socketFactory, addressBook, shouldDoVersionCheck(), appVersion); @@ -218,11 +206,7 @@ protected SyncGossip( // allow other members to create connections to me final Address address = addressBook.getAddress(selfId); final ConnectionServer connectionServer = new ConnectionServer( - threadManager, - address.getListenAddressIpv4(), - address.getListenPort(), - socketFactory, - inboundConnectionHandler::handle); + threadManager, address.getListenPort(), socketFactory, inboundConnectionHandler::handle); thingsToStart.add(new StoppableThreadConfiguration<>(threadManager) .setPriority(threadConfig.threadPrioritySync()) .setNodeId(selfId) @@ -409,29 +393,6 @@ private void buildSyncProtocolThreads( } } - private static SocketFactory socketFactory( - @NonNull final KeysAndCerts keysAndCerts, - @NonNull final CryptoConfig cryptoConfig, - @NonNull final SocketConfig socketConfig) { - Objects.requireNonNull(keysAndCerts); - Objects.requireNonNull(cryptoConfig); - Objects.requireNonNull(socketConfig); - - if (!socketConfig.useTLS()) { - return new TcpFactory(socketConfig); - } - try { - return new TlsFactory(keysAndCerts, socketConfig, cryptoConfig); - } catch (final NoSuchAlgorithmException - | UnrecoverableKeyException - | KeyStoreException - | KeyManagementException - | CertificateException - | IOException e) { - throw new PlatformConstructionException("A problem occurred while creating the SocketFactory", e); - } - } - /** * Build the fallen behind manager. */ diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/NetworkUtils.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/NetworkUtils.java index 38fce025bad8..7e944c65440f 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/NetworkUtils.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/NetworkUtils.java @@ -19,10 +19,26 @@ import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; import static com.swirlds.logging.legacy.LogMarker.SOCKET_EXCEPTIONS; +import com.swirlds.common.crypto.config.CryptoConfig; +import com.swirlds.common.platform.NodeId; +import com.swirlds.config.api.Configuration; import com.swirlds.platform.Utilities; +import com.swirlds.platform.crypto.KeysAndCerts; import com.swirlds.platform.gossip.shadowgraph.SyncTimeoutException; +import com.swirlds.platform.network.connectivity.SocketFactory; +import com.swirlds.platform.network.connectivity.TcpFactory; +import com.swirlds.platform.network.connectivity.TlsFactory; +import com.swirlds.platform.system.PlatformConstructionException; +import com.swirlds.platform.system.address.AddressBook; +import edu.umd.cs.findbugs.annotations.NonNull; import java.io.Closeable; import java.io.IOException; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.Objects; import javax.net.ssl.SSLException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -112,4 +128,47 @@ public static String formatException(final Throwable e) { return "Caused by exception: " + e.getClass().getSimpleName() + " Message: " + e.getMessage() + " " + formatException(e.getCause()); } + + /** + * Create a {@link SocketFactory} based on the configuration and the provided keys and certificates. + * NOTE: This method is a stepping stone to decoupling the networking from the platform. + * + * @param selfId the ID of the node + * @param addressBook the address book of the network + * @param keysAndCerts the keys and certificates to use for the TLS connections + * @param configuration the configuration of the network + * @return the created {@link SocketFactory} + */ + public static @NonNull SocketFactory createSocketFactory( + @NonNull final NodeId selfId, + @NonNull final AddressBook addressBook, + @NonNull final KeysAndCerts keysAndCerts, + @NonNull final Configuration configuration) { + Objects.requireNonNull(selfId); + Objects.requireNonNull(addressBook); + Objects.requireNonNull(keysAndCerts); + Objects.requireNonNull(configuration); + + final CryptoConfig cryptoConfig = configuration.getConfigData(CryptoConfig.class); + final SocketConfig socketConfig = configuration.getConfigData(SocketConfig.class); + + if (!socketConfig.useTLS()) { + return new TcpFactory(socketConfig); + } + try { + return new TlsFactory( + keysAndCerts.agrCert(), + keysAndCerts.agrKeyPair().getPrivate(), + Utilities.createPeerInfoList(addressBook, selfId), + socketConfig, + cryptoConfig); + } catch (final NoSuchAlgorithmException + | UnrecoverableKeyException + | KeyStoreException + | KeyManagementException + | CertificateException + | IOException e) { + throw new PlatformConstructionException("A problem occurred while creating the SocketFactory", e); + } + } } 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 new file mode 100644 index 000000000000..39873c2f5052 --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/PeerInfo.java @@ -0,0 +1,35 @@ +/* + * 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.network; + +import com.swirlds.common.platform.NodeId; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.security.cert.Certificate; + +/** + * 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) {} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/connectivity/ConnectionServer.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/connectivity/ConnectionServer.java index cd4ba07d504d..b9012dfa8178 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/connectivity/ConnectionServer.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/connectivity/ConnectionServer.java @@ -39,11 +39,6 @@ public class ConnectionServer implements InterruptableRunnable { private static final int SLEEP_AFTER_BIND_FAILED_MS = 100; /** use this for all logging, as controlled by the optional data/log4j2.xml file */ private static final Logger logger = LogManager.getLogger(ConnectionServer.class); - /** overrides ip if null */ - private static final byte[] LISTEN_IP = new byte[] {0, 0, 0, 0}; - - /** the IP address that this server listens on for establishing new connections */ - private final byte[] ip; /** the port that this server listens on for establishing new connections */ private final int port; /** responsible for creating and binding the server socket */ @@ -54,11 +49,8 @@ public class ConnectionServer implements InterruptableRunnable { private final ExecutorService incomingConnPool; /** - * * @param threadManager - * * responsible for managing thread lifecycles - * - * @param ip - * the IP address to use + * @param threadManager + * responsible for managing thread lifecycles * @param port * the port ot use * @param socketFactory @@ -68,11 +60,9 @@ public class ConnectionServer implements InterruptableRunnable { */ public ConnectionServer( final ThreadManager threadManager, - final byte[] ip, final int port, final SocketFactory socketFactory, final Consumer newConnectionHandler) { - this.ip = (ip != null) ? ip : LISTEN_IP; this.port = port; this.newConnectionHandler = newConnectionHandler; this.socketFactory = socketFactory; @@ -83,7 +73,7 @@ public ConnectionServer( @Override public void run() throws InterruptedException { - try (ServerSocket serverSocket = socketFactory.createServerSocket(ip, port)) { + try (ServerSocket serverSocket = socketFactory.createServerSocket(port)) { listen(serverSocket); } catch (final RuntimeException | IOException e) { logger.error(EXCEPTION.getMarker(), "Cannot bind ServerSocket", e); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/connectivity/SocketFactory.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/connectivity/SocketFactory.java index 708128732585..4bda65e0499e 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/connectivity/SocketFactory.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/connectivity/SocketFactory.java @@ -17,16 +17,21 @@ package com.swirlds.platform.network.connectivity; import com.swirlds.platform.network.SocketConfig; +import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; +import java.util.Objects; /** * Creates, binds and connects server and client sockets */ public interface SocketFactory { + /** The IPv4 address to listen all interface: [0.0.0.0]. */ + byte[] ALL_INTERFACES = new byte[] {0, 0, 0, 0}; + int IP_TOP_MIN = 0; int IP_TOP_MAX = 255; @@ -41,21 +46,21 @@ static boolean isIpTopInRange(final int ipTos) { * the socket to configure and bind * @param socketConfig * the configuration for the socket - * @param ipAddress - * the IP address to bind * @param port * the TCP port to bind * @throws IOException * if the bind is unsuccessful */ static void configureAndBind( - final ServerSocket serverSocket, final SocketConfig socketConfig, final byte[] ipAddress, final int port) + @NonNull final ServerSocket serverSocket, @NonNull final SocketConfig socketConfig, final int port) throws IOException { + Objects.requireNonNull(serverSocket); + Objects.requireNonNull(socketConfig); if (isIpTopInRange(socketConfig.ipTos())) { // set the IP_TOS option serverSocket.setOption(java.net.StandardSocketOptions.IP_TOS, socketConfig.ipTos()); } - InetSocketAddress endpoint = new InetSocketAddress(InetAddress.getByAddress(ipAddress), port); + final InetSocketAddress endpoint = new InetSocketAddress(InetAddress.getByAddress(ALL_INTERFACES), port); serverSocket.bind(endpoint); // try to grab a port on this computer serverSocket.setReuseAddress(true); // do NOT do clientSocket.setSendBufferSize or clientSocket.setReceiveBufferSize @@ -65,7 +70,7 @@ static void configureAndBind( } /** - * Configures and connects the provided Socket + * Configures and connects the provided client Socket * * @param clientSocket * the socket to configure and connect @@ -79,7 +84,10 @@ static void configureAndBind( * if the connections fails */ static void configureAndConnect( - final Socket clientSocket, final SocketConfig socketConfig, final String hostname, final int port) + @NonNull final Socket clientSocket, + @NonNull final SocketConfig socketConfig, + @NonNull final String hostname, + final int port) throws IOException { if (isIpTopInRange(socketConfig.ipTos())) { // set the IP_TOS option @@ -94,18 +102,16 @@ static void configureAndConnect( } /** - * Create a new ServerSocket, then binds it to the given ip and port. - *

+ * Create a new ServerSocket, then binds it to the given port on all interfaces * - * @param ipAddress - * the ip address to bind to * @param port * the port to bind to * @return a new server socket * @throws IOException * if the socket cannot be created */ - ServerSocket createServerSocket(final byte[] ipAddress, final int port) throws IOException; + @NonNull + ServerSocket createServerSocket(final int port) throws IOException; /** * Create a new Socket, then connect to the given ip and port. @@ -118,5 +124,6 @@ static void configureAndConnect( * @throws IOException * if the connection cannot be made */ - Socket createClientSocket(final String hostname, final int port) throws IOException; + @NonNull + Socket createClientSocket(@NonNull final String hostname, final int port) throws IOException; } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/connectivity/TcpFactory.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/connectivity/TcpFactory.java index ca1883de5885..b39c5be87357 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/connectivity/TcpFactory.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/connectivity/TcpFactory.java @@ -34,14 +34,15 @@ public TcpFactory(@NonNull final SocketConfig socketConfig) { } @Override - public ServerSocket createServerSocket(final byte[] ipAddress, final int port) throws IOException { + public @NonNull ServerSocket createServerSocket(final int port) throws IOException { final ServerSocket serverSocket = new ServerSocket(); - SocketFactory.configureAndBind(serverSocket, socketConfig, ipAddress, port); + SocketFactory.configureAndBind(serverSocket, socketConfig, port); return serverSocket; } @Override - public Socket createClientSocket(final String hostname, final int port) throws IOException { + public @NonNull Socket createClientSocket(@NonNull final String hostname, final int port) throws IOException { + Objects.requireNonNull(hostname); final Socket clientSocket = new Socket(); SocketFactory.configureAndConnect(clientSocket, socketConfig, hostname, port); return clientSocket; diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/connectivity/TlsFactory.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/connectivity/TlsFactory.java index 54d0d289eaa6..de5d14d5d2b7 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/connectivity/TlsFactory.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/connectivity/TlsFactory.java @@ -19,7 +19,7 @@ import com.swirlds.common.crypto.config.CryptoConfig; import com.swirlds.platform.crypto.CryptoConstants; import com.swirlds.platform.crypto.CryptoStatic; -import com.swirlds.platform.crypto.KeysAndCerts; +import com.swirlds.platform.network.PeerInfo; import com.swirlds.platform.network.SocketConfig; import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; @@ -29,10 +29,12 @@ import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; import java.security.SecureRandom; import java.security.UnrecoverableKeyException; import java.security.cert.Certificate; import java.security.cert.CertificateException; +import java.util.List; import java.util.Objects; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; @@ -51,21 +53,28 @@ public class TlsFactory implements SocketFactory { private final SSLSocketFactory sslSocketFactory; /** - * Construct this object to create and receive TLS connections. This is done using the trustStore - * whose reference was passed in as an argument. That trustStore must contain certs for all - * the members before calling this constructor. This method will then create the appropriate - * KeyManagerFactory, TrustManagerFactory, SSLContext, SSLServerSocketFactory, and SSLSocketFactory, so - * that it can later create the TLS sockets. + * Construct this object to create and receive TLS connections. + * @param agrCert the TLS certificate to use + * @param agrKey the private key corresponding to the public key in the certificate + * @param peers the list of peers to allow connections with + * @param socketConfig the configuration for the sockets + * @param cryptoConfig the configuration for the cryptography */ public TlsFactory( - @NonNull final KeysAndCerts keysAndCerts, + @NonNull final Certificate agrCert, + @NonNull final PrivateKey agrKey, + @NonNull final List peers, @NonNull final SocketConfig socketConfig, @NonNull final CryptoConfig cryptoConfig) throws NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException, KeyManagementException, CertificateException, IOException { - Objects.requireNonNull(keysAndCerts); - Objects.requireNonNull(cryptoConfig); + Objects.requireNonNull(agrCert); + Objects.requireNonNull(agrKey); + Objects.requireNonNull(peers); this.socketConfig = Objects.requireNonNull(socketConfig); + Objects.requireNonNull(cryptoConfig); + + final KeyStore signingTrustStore = CryptoStatic.createPublicKeyStore(peers); final char[] password = cryptoConfig.keystorePassword().toCharArray(); /* nondeterministic CSPRNG */ @@ -75,17 +84,17 @@ public TlsFactory( // PKCS12 uses file extension .p12 or .pfx final KeyStore agrKeyStore = KeyStore.getInstance(CryptoConstants.KEYSTORE_TYPE); agrKeyStore.load(null, null); // initialize - agrKeyStore.setKeyEntry( - "key", keysAndCerts.agrKeyPair().getPrivate(), password, new Certificate[] {keysAndCerts.agrCert()}); + agrKeyStore.setKeyEntry("key", agrKey, password, new Certificate[] {agrCert}); // "PKIX" may be more interoperable than KeyManagerFactory.getDefaultAlgorithm or // TrustManagerFactory.getDefaultAlgorithm(), which was "SunX509" on one system tested - KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(CryptoConstants.KEY_MANAGER_FACTORY_TYPE); + final KeyManagerFactory keyManagerFactory = + KeyManagerFactory.getInstance(CryptoConstants.KEY_MANAGER_FACTORY_TYPE); keyManagerFactory.init(agrKeyStore, password); - TrustManagerFactory trustManagerFactory = + final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(CryptoConstants.TRUST_MANAGER_FACTORY_TYPE); - trustManagerFactory.init(keysAndCerts.publicStores().sigTrustStore()); - SSLContext sslContext = SSLContext.getInstance(CryptoConstants.SSL_VERSION); + trustManagerFactory.init(signingTrustStore); + final SSLContext sslContext = SSLContext.getInstance(CryptoConstants.SSL_VERSION); SSLContext.setDefault(sslContext); sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), nonDetRandom); sslServerSocketFactory = sslContext.getServerSocketFactory(); @@ -93,18 +102,19 @@ public TlsFactory( } @Override - public ServerSocket createServerSocket(final byte[] ipAddress, final int port) throws IOException { + public @NonNull ServerSocket createServerSocket(final int port) throws IOException { final SSLServerSocket serverSocket = (SSLServerSocket) sslServerSocketFactory.createServerSocket(); serverSocket.setEnabledCipherSuites(new String[] {CryptoConstants.TLS_SUITE}); serverSocket.setWantClientAuth(true); serverSocket.setNeedClientAuth(true); - SocketFactory.configureAndBind(serverSocket, socketConfig, ipAddress, port); + SocketFactory.configureAndBind(serverSocket, socketConfig, port); return serverSocket; } @Override - public Socket createClientSocket(final String hostname, final int port) throws IOException { - SSLSocket clientSocket = (SSLSocket) sslSocketFactory.createSocket(); + public @NonNull Socket createClientSocket(@NonNull final String hostname, final int port) throws IOException { + Objects.requireNonNull(hostname); + final SSLSocket clientSocket = (SSLSocket) sslSocketFactory.createSocket(); // ensure the connection is ALWAYS the exact cipher suite we've chosen clientSocket.setEnabledCipherSuites(new String[] {CryptoConstants.TLS_SUITE}); clientSocket.setWantClientAuth(true); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/address/Address.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/address/Address.java index eba414194724..00094bfd188c 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/address/Address.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/address/Address.java @@ -67,7 +67,6 @@ private static class ClassVersion { public static final int X509_CERT_SUPPORT = 6; } - private static final byte[] ALL_INTERFACES = new byte[] {0, 0, 0, 0}; private static final int MAX_IP_LENGTH = 16; private static final int STRING_MAX_BYTES = 512; @@ -275,16 +274,6 @@ public boolean isLocalTo(Address a) { return Objects.equals(getHostnameExternal(), a.getHostnameExternal()); } - /** - * Get the IPv4 address for listening all interfaces, [0.0.0.0]. - * - * @return The IPv4 address to listen all interface: [0.0.0.0]. - */ - @NonNull - public byte[] getListenAddressIpv4() { - return ALL_INTERFACES; - } - /** * Get listening port used on the local network. * diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/network/connectivity/SocketFactoryTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/network/connectivity/SocketFactoryTest.java index baf99a5269de..478c447a9f5b 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/network/connectivity/SocketFactoryTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/network/connectivity/SocketFactoryTest.java @@ -17,35 +17,46 @@ package com.swirlds.platform.network.connectivity; import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; -import com.swirlds.common.crypto.config.CryptoConfig; -import com.swirlds.common.test.fixtures.junit.tags.TestQualifierTags; +import com.swirlds.common.platform.NodeId; import com.swirlds.config.api.Configuration; import com.swirlds.config.extensions.test.fixtures.TestConfigBuilder; import com.swirlds.platform.crypto.KeysAndCerts; +import com.swirlds.platform.network.NetworkUtils; import com.swirlds.platform.network.SocketConfig; import com.swirlds.platform.network.SocketConfig_; import com.swirlds.platform.system.address.AddressBook; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; +import java.util.List; +import java.util.Map; import java.util.Random; import java.util.concurrent.atomic.AtomicReference; -import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; class SocketFactoryTest { private static final byte[] DATA = {1, 2, 3}; - private static final byte[] BYTES_IP = {127, 0, 0, 1}; private static final String STRING_IP = "127.0.0.1"; private static final int PORT = 30_000; private static final SocketConfig NO_IP_TOS; private static final SocketConfig IP_TOS; - private static final CryptoConfig CRYPTO_CONFIG; + private static final Configuration TLS_NO_IP_TOS_CONFIG; + private static final Configuration TLS_IP_TOS_CONFIG; static { + TLS_NO_IP_TOS_CONFIG = new TestConfigBuilder() + .withValue(SocketConfig_.IP_TOS, "-1") + .withValue(SocketConfig_.USE_T_L_S, true) + .getOrCreateConfig(); + TLS_IP_TOS_CONFIG = new TestConfigBuilder() + .withValue(SocketConfig_.IP_TOS, "100") + .withValue(SocketConfig_.USE_T_L_S, true) + .getOrCreateConfig(); + final Configuration configurationNoIpTos = new TestConfigBuilder().withValue(SocketConfig_.IP_TOS, "-1").getOrCreateConfig(); NO_IP_TOS = configurationNoIpTos.getConfigData(SocketConfig.class); @@ -53,8 +64,6 @@ class SocketFactoryTest { final Configuration configurationIpTos = new TestConfigBuilder().withValue(SocketConfig_.IP_TOS, "100").getOrCreateConfig(); IP_TOS = configurationIpTos.getConfigData(SocketConfig.class); - - CRYPTO_CONFIG = configurationIpTos.getConfigData(CryptoConfig.class); } /** @@ -85,7 +94,7 @@ private static void testSocketsBoth(final SocketFactory socketFactory1, final So private static void testSockets(final SocketFactory serverFactory, final SocketFactory clientFactory) throws Throwable { - final ServerSocket serverSocket = serverFactory.createServerSocket(BYTES_IP, PORT); + final ServerSocket serverSocket = serverFactory.createServerSocket(PORT); final Thread server = new Thread(() -> { try { @@ -123,7 +132,7 @@ private static void testSockets(final SocketFactory serverFactory, final SocketF * Tests the functionality {@link KeysAndCerts} are currently used for, signing and establishing TLS connections. * * @param addressBook - * address book of the network + * the address book of the network * @param keysAndCerts * keys and certificates to use for testing * @throws Throwable @@ -131,19 +140,26 @@ private static void testSockets(final SocketFactory serverFactory, final SocketF */ @ParameterizedTest @MethodSource({"com.swirlds.platform.crypto.CryptoArgsProvider#basicTestArgs"}) - @Tag(TestQualifierTags.TIME_CONSUMING) - void tlsFactoryTest(final AddressBook addressBook, final KeysAndCerts[] keysAndCerts) throws Throwable { + void tlsFactoryTest(final AddressBook addressBook, final Map keysAndCerts) throws Throwable { + assertTrue(addressBook.getSize() > 1, "Address book must contain at least 2 nodes"); // choose 2 random nodes to test final Random random = new Random(); - final int node1 = random.nextInt(addressBook.getSize()); - final int node2 = random.nextInt(addressBook.getSize()); + final List nodeIndexes = random.ints(0, addressBook.getSize()) + .distinct() + .limit(2) + .boxed() + .toList(); + final NodeId node1 = addressBook.getNodeId(nodeIndexes.get(0)); + final NodeId node2 = addressBook.getNodeId(nodeIndexes.get(1)); + final KeysAndCerts keysAndCerts1 = keysAndCerts.get(node1); + final KeysAndCerts keysAndCerts2 = keysAndCerts.get(node2); testSocketsBoth( - new TlsFactory(keysAndCerts[node1], NO_IP_TOS, CRYPTO_CONFIG), - new TlsFactory(keysAndCerts[node2], NO_IP_TOS, CRYPTO_CONFIG)); + NetworkUtils.createSocketFactory(node1, addressBook, keysAndCerts1, TLS_NO_IP_TOS_CONFIG), + NetworkUtils.createSocketFactory(node2, addressBook, keysAndCerts2, TLS_NO_IP_TOS_CONFIG)); testSocketsBoth( - new TlsFactory(keysAndCerts[node1], IP_TOS, CRYPTO_CONFIG), - new TlsFactory(keysAndCerts[node2], IP_TOS, CRYPTO_CONFIG)); + NetworkUtils.createSocketFactory(node1, addressBook, keysAndCerts1, TLS_IP_TOS_CONFIG), + NetworkUtils.createSocketFactory(node2, addressBook, keysAndCerts2, TLS_IP_TOS_CONFIG)); } @Test diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/ConnectionServerTest.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/ConnectionServerTest.java index 5b1589b0e693..3316d33b39aa 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/ConnectionServerTest.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/ConnectionServerTest.java @@ -17,7 +17,6 @@ package com.swirlds.platform.test.network; import static com.swirlds.common.threading.manager.AdHocThreadManager.getStaticThreadManager; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; @@ -48,11 +47,11 @@ void createConnectionTest() throws IOException, InterruptedException { .when(serverSocket) .accept(); final SocketFactory socketFactory = mock(SocketFactory.class); - doAnswer(i -> serverSocket).when(socketFactory).createServerSocket(any(), anyInt()); + doAnswer(i -> serverSocket).when(socketFactory).createServerSocket(anyInt()); final AtomicReference connectionHandler = new AtomicReference<>(null); final ConnectionServer server = - new ConnectionServer(getStaticThreadManager(), null, 0, socketFactory, connectionHandler::set); + new ConnectionServer(getStaticThreadManager(), 0, socketFactory, connectionHandler::set); server.run(); Assertions.assertSame( From f68ad942d79030660956f924bd907e50b81a6a60 Mon Sep 17 00:00:00 2001 From: Jendrik Johannes Date: Tue, 5 Mar 2024 11:40:26 +0100 Subject: [PATCH 017/115] build: support running selected JMH tests (#11865) Signed-off-by: Jendrik Johannes --- ...hedera.hashgraph.benchmark-conventions.gradle.kts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/build-logic/project-plugins/src/main/kotlin/com.hedera.hashgraph.benchmark-conventions.gradle.kts b/build-logic/project-plugins/src/main/kotlin/com.hedera.hashgraph.benchmark-conventions.gradle.kts index 1da2467ccc7e..83a76eddae42 100644 --- a/build-logic/project-plugins/src/main/kotlin/com.hedera.hashgraph.benchmark-conventions.gradle.kts +++ b/build-logic/project-plugins/src/main/kotlin/com.hedera.hashgraph.benchmark-conventions.gradle.kts @@ -21,6 +21,17 @@ plugins { id("me.champeau.jmh") } jmh { jmhVersion = "1.37" includeTests = false + // Filter JMH tests from command line via -PjmhTests=... + val commandLineIncludes = providers.gradleProperty("jmhTests") + if (commandLineIncludes.isPresent) { + includes.add(commandLineIncludes.get()) + } +} + +dependencies { + // Required for the JMH IDEA plugin: + // https://plugins.jetbrains.com/plugin/7529-jmh-java-microbenchmark-harness + jmhAnnotationProcessor("org.openjdk.jmh:jmh-generator-annprocess:${jmh.jmhVersion.get()}") } tasks.jmh { outputs.upToDateWhen { false } } @@ -41,6 +52,7 @@ configurations { val javaModule = Attribute.of("javaModule", Boolean::class.javaObjectType) jmhRuntimeClasspath { attributes { attribute(javaModule, false) } } jmhCompileClasspath { attributes { attribute(javaModule, false) } } + jmhAnnotationProcessor { attributes { attribute(javaModule, false) } } } tasks.assemble { From 3aee6fed8e3beec94d41ee1bdad0d61e6e56392b Mon Sep 17 00:00:00 2001 From: Ivan Malygin Date: Tue, 5 Mar 2024 14:07:22 -0500 Subject: [PATCH 018/115] fix: 11636 VirtualHasher performance improvements (#11787) Signed-off-by: Ivan Malygin Co-authored-by: Oleg Mazurov Co-authored-by: Artem Ananev --- .../schedulers/internal/ConcurrentTask.java | 9 -- .../common/wiring/tasks/AbstractTask.java | 2 +- .../virtualmap/config/VirtualMapConfig.java | 2 +- .../internal/hash/VirtualHasher.java | 147 ++++++++++-------- 4 files changed, 87 insertions(+), 73 deletions(-) diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/schedulers/internal/ConcurrentTask.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/schedulers/internal/ConcurrentTask.java index 9a197ff53828..fea8b381e6dd 100644 --- a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/schedulers/internal/ConcurrentTask.java +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/schedulers/internal/ConcurrentTask.java @@ -71,13 +71,4 @@ protected boolean exec() { } return true; } - - /** - * {@inheritDoc} - */ - @Override - public void send() { - // Expose this method to the scheduler - super.send(); - } } diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/tasks/AbstractTask.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/tasks/AbstractTask.java index b67ed87ebded..cba3654d27de 100644 --- a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/tasks/AbstractTask.java +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/tasks/AbstractTask.java @@ -67,7 +67,7 @@ protected final void setRawResult(Void value) {} * If the task has no dependencies then execute it. If the task has dependencies, decrement the dependency count and * execute it if the resulting number of dependencies is zero. */ - protected void send() { + public void send() { if (dependencyCount == null || dependencyCount.decrementAndGet() == 0) { if ((Thread.currentThread() instanceof ForkJoinWorkerThread t) && (t.getPool() == pool)) { fork(); diff --git a/platform-sdk/swirlds-virtualmap/src/main/java/com/swirlds/virtualmap/config/VirtualMapConfig.java b/platform-sdk/swirlds-virtualmap/src/main/java/com/swirlds/virtualmap/config/VirtualMapConfig.java index 046d5e2b50f8..3fe2349e2f42 100644 --- a/platform-sdk/swirlds-virtualmap/src/main/java/com/swirlds/virtualmap/config/VirtualMapConfig.java +++ b/platform-sdk/swirlds-virtualmap/src/main/java/com/swirlds/virtualmap/config/VirtualMapConfig.java @@ -89,7 +89,7 @@ public record VirtualMapConfig( @Min(0) @Max(100) @ConfigProperty(defaultValue = "50.0") double percentHashThreads, // FUTURE WORK: We need to add min/max support for double values @Min(-1) @ConfigProperty(defaultValue = "-1") int numHashThreads, - @Min(1) @Max(64) @ConfigProperty(defaultValue = "6") int virtualHasherChunkHeight, + @Min(1) @Max(64) @ConfigProperty(defaultValue = "3") int virtualHasherChunkHeight, @Min(0) @ConfigProperty(defaultValue = "500000") int reconnectFlushInterval, @Min(0) @Max(100) @ConfigProperty(defaultValue = "25.0") double percentCleanerThreads, // FUTURE WORK: We need to add min/max support for double values diff --git a/platform-sdk/swirlds-virtualmap/src/main/java/com/swirlds/virtualmap/internal/hash/VirtualHasher.java b/platform-sdk/swirlds-virtualmap/src/main/java/com/swirlds/virtualmap/internal/hash/VirtualHasher.java index 67d0fd677e35..a1dfaf109c8c 100644 --- a/platform-sdk/swirlds-virtualmap/src/main/java/com/swirlds/virtualmap/internal/hash/VirtualHasher.java +++ b/platform-sdk/swirlds-virtualmap/src/main/java/com/swirlds/virtualmap/internal/hash/VirtualHasher.java @@ -37,6 +37,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; +import java.util.Objects; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.LongFunction; @@ -145,51 +146,58 @@ public Hash hash( return hash(hashReader, sortedDirtyLeaves, firstLeafPath, lastLeafPath, null); } - class ChunkHashTask extends AbstractTask { + class HashHoldingTask extends AbstractTask { + + // Input hashes. Some hashes may be null, which indicates they should be loaded from disk + protected final Hash[] ins; + + HashHoldingTask(final ForkJoinPool pool, final int dependencies, final int numHashes) { + super(pool, dependencies); + ins = numHashes > 0 ? new Hash[numHashes] : null; + } + + @Override + protected boolean exec() { + return true; + } + + void setHash(final int index, final Hash hash) { + ins[index] = hash; + send(); + } + } + + class ChunkHashTask extends HashHoldingTask { private final long path; private final int height; // 1 for 3-node chunk, 2 for 7-node chunk, and so on - private ChunkHashTask out; - - // Input hashes. Some hashes may be null, which indicates they should be loaded from disk - private final Hash[] ins; + private HashHoldingTask out; // If not null, the task hashes the leaf. If null, the task processes the input hashes private VirtualLeafRecord leaf; ChunkHashTask(final ForkJoinPool pool, final long path, final int height) { - super(pool, 1 + (1 << height)); + super(pool, 1 + (1 << height), height > 0 ? 1 << height : 0); this.height = height; this.path = path; - this.ins = new Hash[1 << height]; } - void setOut(final ChunkHashTask out) { + void setOut(final HashHoldingTask out) { this.out = out; - assert path == 0 || Path.getRank(path) - out.height == Path.getRank(out.path) - : "setOut " + path + " " + height + " " + out.path; send(); } - void setData(final VirtualLeafRecord leaf) { - assert leaf == null || path == leaf.getPath(); - assert leaf == null || height == 1; - assert leaf != null || out != null; - if (leaf == null) { - out.setHash(getIndexInOut(), null); - } else { - this.leaf = leaf; - send(); // left hash dependency - send(); // right hash dependency - } + void setLeaf(final VirtualLeafRecord leaf) { + assert leaf != null && path == leaf.getPath() && height == 0; + this.leaf = leaf; + send(); } - void setHash(final int index, final Hash hash) { - assert index >= 0 && index < (1 << height); - ins[index] = hash; - send(); + void complete() { + assert (leaf == null) && (ins == null || Arrays.stream(ins).allMatch(Objects::isNull)); + out.send(); } @Override @@ -210,10 +218,7 @@ protected boolean exec() { listener.onNodeHashed(path, hash); } else { int len = 1 << height; - long rankPath = path; - for (int i = 0; i < height; i++) { - rankPath = Path.getLeftChildPath(rankPath); - } + long rankPath = Path.getLeftGrandChildPath(path, height); while (len > 1) { for (int i = 0; i < len / 2; i++) { final long hashedPath = Path.getParentPath(rankPath + i * 2); @@ -260,11 +265,12 @@ static Hash hash(final long path, final Hash left, final Hash right) { } private int getIndexInOut() { - if (path == 0) { + if (out instanceof ChunkHashTask t) { + final long firstInPathInOut = Path.getLeftGrandChildPath(t.path, t.height); + return (int) (path - firstInPathInOut); + } else { return 0; } - final long firstInPathInOut = Path.getLeftGrandChildPath(out.path, out.height); - return (int) (path - firstInPathInOut); } } @@ -309,7 +315,7 @@ public Hash hash( // Each chunk is processed in a separate task. Tasks have dependencies. Once all task // dependencies are met, the task is scheduled for execution in the pool. Each task // has N input dependencies, where N is the number of nodes at the lowest chunk rank, - // i.e. 2^height. Every input depenency is either set to a hash from another task, + // i.e. 2^height. Every input dependency is either set to a hash from another task, // or a null value, which indicates that the input hash needs not to be recalculated, // but loaded from disk. A special case of a task is leaf tasks, they are all of // height 1, both input dependencies are null, but they are given a leaf instead. For @@ -334,9 +340,12 @@ public Hash hash( // the root task below. When the root task is done executing, that is it produced // a root hash, this hash is set as an input dependency for this result task, where // it's read and returned in the end of this method - ChunkHashTask resultTask = new ChunkHashTask(HASHING_POOL, INVALID_PATH, 1); - int rootTaskHeight = Math.min(firstLeafRank, chunkHeight); - ChunkHashTask rootTask = new ChunkHashTask(HASHING_POOL, ROOT_PATH, rootTaskHeight); + /** + * A task holding the resulting hash. Used to synchronize all parallel computations. + */ + final HashHoldingTask resultTask = new HashHoldingTask(HASHING_POOL, 1, 1); + final int rootTaskHeight = Math.min(firstLeafRank, chunkHeight); + final ChunkHashTask rootTask = new ChunkHashTask(HASHING_POOL, ROOT_PATH, rootTaskHeight); rootTask.setOut(resultTask); map.put(ROOT_PATH, rootTask); @@ -347,7 +356,7 @@ public Hash hash( // Tasks may have different heights. The root task has a default height. If the whole // virtual tree has fewer ranks than the default height, the root task will cover all // the tree (almost all, see comments below about leaf task heights) - final int[] parentRankHeights = new int[256]; // assuming there may be no more than 256 ranks in the tree + final int[] parentRankHeights = new int[lastLeafRank + 1]; parentRankHeights[0] = 1; for (int i = 1; i <= firstLeafRank; i++) { parentRankHeights[i] = Math.min((i - 1) % chunkHeight + 1, i); @@ -374,9 +383,9 @@ public Hash hash( long curPath = leaf.getPath(); ChunkHashTask curTask = map.remove(curPath); if (curTask == null) { - curTask = new ChunkHashTask(HASHING_POOL, curPath, 1); + curTask = new ChunkHashTask(HASHING_POOL, curPath, 0); } - curTask.setData(leaf); + curTask.setLeaf(leaf); // The next step is to iterate over parent tasks, until an already created task // is met (e.g. the root task). For every parent task, check all already created @@ -400,10 +409,22 @@ public Hash hash( while (curStackPath < Math.min(curPath, lastPathInCurStackChunk)) { final ChunkHashTask t = map.remove(curStackPath); assert t != null; - t.setData(null); + t.complete(); curStackPath++; } - stack[curRank] = INVALID_PATH; + + // It may happen that curPath is actually in the same chunk as stack[curRank]. + // In this case, stack[curRank] should be set to curPath + 1 to prevent a situation in which all + // existing tasks between curPath and the end of the chunk will hang in the tasks map and will be + // processed only after the last leaf (in the loop to set null data for all tasks remaining in the + // map), + // despite these tasks being known to be clear. + + if (curPath > curStackPath && curPath < lastPathInCurStackChunk) { + stack[curRank] = curPath + 1; + } else { + stack[curRank] = INVALID_PATH; + } } // If the out is already set at this rank, all parent tasks and siblings are already @@ -436,29 +457,31 @@ public Hash hash( continue; } if (siblingPath > lastLeafPath) { + assert siblingPath == 2; parentTask.setHash((int) (siblingPath - firstSiblingPath), NULL_HASH); - continue; - } - // Get or create the sibling task - ChunkHashTask siblingTask = map.remove(siblingPath); - if (siblingTask == null) { - siblingTask = new ChunkHashTask(HASHING_POOL, siblingPath, curTask.height); - } - // Set sibling task output to the same parent - siblingTask.setOut(parentTask); - // Mark the sibling as clean if: it's to the left AND this is not the very first leaf - if ((siblingPath < curPath) && !firstLeaf) { - siblingTask.setData(null); + } else if ((siblingPath < curPath) && !firstLeaf) { + // Mark the sibling as clean, reducing the number of dependencies + parentTask.send(); } else { - map.put(siblingPath, siblingTask); - } - // Now update the stack to the first sibling to the right. When the next node - // at the same rank is processed, all tasks starting from this sibling are - // guaranteed to be clean - if ((curPath != lastSiblingPath) && !firstLeaf) { - stack[curRank] = curPath + 1; + // Get or create a sibling task + final int siblingHeight; + if (curTask.height == 0) { + siblingHeight = siblingPath < firstLeafPath ? 1 : 0; + } else { + siblingHeight = curTask.height; + } + ChunkHashTask siblingTask = map.computeIfAbsent( + siblingPath, path -> new ChunkHashTask(HASHING_POOL, path, siblingHeight)); + // Set sibling task output to the same parent + siblingTask.setOut(parentTask); } } + // Now update the stack to the first sibling to the right. When the next node + // at the same rank is processed, all tasks starting from this sibling are + // guaranteed to be clean + if ((curPath != lastSiblingPath) && !firstLeaf) { + stack[curRank] = curPath + 1; + } curPath = parentPath; curTask = parentTask; @@ -472,11 +495,11 @@ public Hash hash( // created during walking from the last leaf on the last leaf rank to the root; sibling // tasks to the left of the very first route to the root. There are no more dirty leaves, // all these tasks may be marked as clean now - map.forEach((path, task) -> task.setData(null)); + map.forEach((path, task) -> task.complete()); map.clear(); try { - rootTask.join(); + resultTask.join(); } catch (final Exception e) { if (shutdown.get()) { return null; From c92de0026ec8079d45e480b0eb3b450fbd06f198 Mon Sep 17 00:00:00 2001 From: Michael Tinker Date: Tue, 5 Mar 2024 17:01:10 -0600 Subject: [PATCH 019/115] fix: fix schedules-by-equality migration, fractional custom fees, and special reward situations (#11823) Signed-off-by: Michael Tinker --- .../app/workflows/handle/HandleWorkflow.java | 66 ++++++++- .../SingleTransactionRecordBuilderImpl.java | 25 ++++ .../schedule/impl/ScheduleStoreUtility.java | 4 +- .../InitialModServiceScheduleSchema.java | 24 ++-- .../CustomMessageCallProcessor.java | 21 ++- .../hts/transfer/ClassicTransfersCall.java | 7 +- .../transfer/ClassicTransfersTranslator.java | 4 +- .../hts/transfer/Erc20TransfersCall.java | 11 +- .../transfer/Erc20TransfersTranslator.java | 4 +- .../hts/transfer/Erc721TransferFromCall.java | 5 +- .../Erc721TransferFromTranslator.java | 4 +- .../hts/transfer/SpecialRewardReceivers.java | 62 +++++++++ .../contract/impl/exec/utils/FrameUtils.java | 22 +++ .../handlers/ContractGetBytecodeHandler.java | 5 +- .../records/ContractCallRecordBuilder.java | 8 ++ .../ContractOperationRecordBuilder.java | 18 +++ .../contract/impl/state/ProxyEvmAccount.java | 2 +- .../transfer/ClassicTransfersCallTest.java | 16 ++- .../hts/transfer/Erc20TransfersCallTest.java | 13 +- .../transfer/Erc721TransferFromCallTest.java | 7 +- .../transfer/SpecialRewardReceiversTest.java | 127 ++++++++++++++++++ .../ContractOperationRecordBuilderTest.java | 10 ++ .../handlers/FinalizeParentRecordHandler.java | 6 +- .../staking/EndOfStakingPeriodUpdater.java | 10 +- .../staking/StakingRewardsDistributor.java | 3 + .../staking/StakingRewardsHandler.java | 12 +- .../staking/StakingRewardsHandlerImpl.java | 16 ++- .../staking/StakingRewardsHelper.java | 63 ++++++--- .../CustomFractionalFeeAssessor.java | 31 +++-- .../validators/CryptoTransferValidator.java | 2 +- .../FakeNodeStakeUpdateRecordBuilder.java | 6 + .../FinalizeParentRecordHandlerTest.java | 63 +++++---- .../StakingRewardsHandlerImplTest.java | 42 +++--- .../records/NodeStakeUpdateRecordBuilder.java | 8 ++ .../token/records/ParentRecordFinalizer.java | 6 +- 35 files changed, 607 insertions(+), 126 deletions(-) create mode 100644 hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/SpecialRewardReceivers.java create mode 100644 hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/transfer/SpecialRewardReceiversTest.java 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 26a8a15fe86a..7d96f8bb934b 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 @@ -41,14 +41,17 @@ import static com.hedera.node.app.workflows.prehandle.PreHandleResult.Status.PRE_HANDLE_FAILURE; import static com.hedera.node.app.workflows.prehandle.PreHandleResult.Status.SO_FAR_SO_GOOD; import static java.util.Collections.emptyList; +import static java.util.Collections.emptySet; import static java.util.Objects.requireNonNull; +import com.hedera.hapi.node.base.AccountAmount; import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.HederaFunctionality; import com.hedera.hapi.node.base.Key; import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.hapi.node.base.SignatureMap; import com.hedera.hapi.node.base.Transaction; +import com.hedera.hapi.node.base.TransferList; import com.hedera.hapi.node.state.token.Account; import com.hedera.hapi.node.token.CryptoUpdateTransactionBody; import com.hedera.hapi.node.transaction.TransactionBody; @@ -116,6 +119,7 @@ import java.time.Instant; import java.util.EnumSet; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import javax.inject.Inject; @@ -594,7 +598,9 @@ private void handleUserTransaction( } throttleServiceManager.saveThrottleSnapshotsAndCongestionLevelStartsTo(stack); - transactionFinalizer.finalizeParentRecord(payer, tokenServiceContext, transactionInfo.functionality()); + final var function = transactionInfo.functionality(); + transactionFinalizer.finalizeParentRecord( + payer, tokenServiceContext, function, extraRewardReceivers(transactionInfo, recordBuilder)); // Commit all state changes stack.commitFullStack(); @@ -609,6 +615,64 @@ private void handleUserTransaction( handleWorkflowMetrics.update(transactionInfo.functionality(), handleDuration); } + /** + * Returns a set of "extra" account ids that should be considered as eligible for + * collecting their accrued staking rewards with the given transaction info and + * record builder. + * + *

IMPORTANT: Needed only for mono-service fidelity. + * + *

There are three cases, none of which HIP-406 defined as a reward situation; + * but were "false positives" in the original mono-service implementation: + *

    + *
  1. For a crypto transfer, any account explicitly listed in the HBAR + * transfer list, even with a zero balance adjustment.
  2. + *
  3. For a contract operation, any called contract.
  4. + *
  5. For a contract operation, any account loaded in a child + * transaction (primarily, any account involved in a child + * token transfer).
  6. + *
+ * + * @param transactionInfo the transaction info + * @param recordBuilder the record builder + * @return the set of extra account ids + */ + private Set extraRewardReceivers( + @NonNull final TransactionInfo transactionInfo, + @NonNull final SingleTransactionRecordBuilderImpl recordBuilder) { + if (recordBuilder.status() != SUCCESS) { + return emptySet(); + } + return switch (transactionInfo.functionality()) { + case CRYPTO_TRANSFER -> zeroAdjustIdsFrom(transactionInfo + .txBody() + .cryptoTransferOrThrow() + .transfersOrElse(TransferList.DEFAULT) + .accountAmountsOrElse(emptyList())); + case ETHEREUM_TRANSACTION, CONTRACT_CALL, CONTRACT_CREATE -> recordBuilder.explicitRewardSituationIds(); + default -> emptySet(); + }; + } + + /** + * Returns any ids from the given list of explicit hbar adjustments that have a zero amount. + * + * @param explicitHbarAdjustments the list of explicit hbar adjustments + * @return the set of account ids that have a zero amount + */ + private @NonNull Set zeroAdjustIdsFrom(@NonNull final List explicitHbarAdjustments) { + Set zeroAdjustmentAccounts = null; + for (final var aa : explicitHbarAdjustments) { + if (aa.amount() == 0) { + if (zeroAdjustmentAccounts == null) { + zeroAdjustmentAccounts = new LinkedHashSet<>(); + } + zeroAdjustmentAccounts.add(aa.accountID()); + } + } + return zeroAdjustmentAccounts == null ? emptySet() : zeroAdjustmentAccounts; + } + /** * Updates key on the hollow accounts that need to be finalized. This is done by dispatching a preceding * synthetic update transaction. The ksy is derived from the signature expansion, by looking up the ECDSA key diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/SingleTransactionRecordBuilderImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/SingleTransactionRecordBuilderImpl.java index 139d2178896d..fa0a5c940469 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/SingleTransactionRecordBuilderImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/SingleTransactionRecordBuilderImpl.java @@ -18,6 +18,7 @@ import static com.hedera.node.app.spi.workflows.record.ExternalizedRecordCustomizer.NOOP_EXTERNALIZED_RECORD_CUSTOMIZER; import static com.hedera.node.app.state.logging.TransactionStateLogger.logEndTransactionRecord; +import static java.util.Collections.emptySet; import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.AccountAmount; @@ -87,9 +88,11 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Set; /** * A custom builder for create a {@link SingleTransactionRecord}. @@ -165,6 +168,9 @@ public class SingleTransactionRecordBuilderImpl // These are not persisted to the record file private final Map deletedAccountBeneficiaries = new HashMap<>(); + @Nullable + private Set calledContractIds; + // While the fee is sent to the underlying builder all the time, it is also cached here because, as of today, // there is no way to get the transaction fee from the PBJ object. private long transactionFee; @@ -448,6 +454,7 @@ public SingleTransactionRecordBuilderImpl memo(@NonNull final String memo) { public Transaction transaction() { return transaction; } + /** * Gets the consensus instant. * @@ -487,6 +494,19 @@ public SingleTransactionRecordBuilderImpl transactionFee(final long transactionF return this; } + @Override + public void trackExplicitRewardSituation(@NonNull final AccountID contractId) { + if (calledContractIds == null) { + calledContractIds = new LinkedHashSet<>(); + } + calledContractIds.add(contractId); + } + + @Override + public Set explicitRewardSituationIds() { + return calledContractIds != null ? calledContractIds : emptySet(); + } + /** * Sets the body to contractCall result. * @@ -745,6 +765,11 @@ public SingleTransactionRecordBuilderImpl evmAddress(@NonNull final Bytes evmAdd return this; } + @Override + public @NonNull List getAssessedCustomFees() { + return assessedCustomFees; + } + // ------------------------------------------------------------------------------------------------------------------------ // fields needed for TransactionReceipt diff --git a/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/ScheduleStoreUtility.java b/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/ScheduleStoreUtility.java index 9887e1c46391..6eaa8433a25d 100644 --- a/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/ScheduleStoreUtility.java +++ b/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/ScheduleStoreUtility.java @@ -26,13 +26,13 @@ import java.nio.charset.StandardCharsets; import java.util.Objects; -final class ScheduleStoreUtility { +public final class ScheduleStoreUtility { private ScheduleStoreUtility() {} // @todo('7773') This requires rebuilding the equality virtual map on migration, // because it's different from ScheduleVirtualValue (and must be, due to PBJ shift) @SuppressWarnings("UnstableApiUsage") - static String calculateStringHash(@NonNull final Schedule scheduleToHash) { + public static String calculateStringHash(@NonNull final Schedule scheduleToHash) { Objects.requireNonNull(scheduleToHash); final Hasher hasher = Hashing.sha256().newHasher(); if (scheduleToHash.memo() != null) { diff --git a/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/schemas/InitialModServiceScheduleSchema.java b/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/schemas/InitialModServiceScheduleSchema.java index 0fd5330dc595..4c62d6f5baa8 100644 --- a/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/schemas/InitialModServiceScheduleSchema.java +++ b/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/schemas/InitialModServiceScheduleSchema.java @@ -30,6 +30,7 @@ import com.hedera.node.app.service.mono.state.submerkle.RichInstant; import com.hedera.node.app.service.mono.state.virtual.schedule.ScheduleSecondVirtualValue; import com.hedera.node.app.service.mono.state.virtual.temporal.SecondSinceEpocVirtualKey; +import com.hedera.node.app.service.schedule.impl.ScheduleStoreUtility; import com.hedera.node.app.service.schedule.impl.codec.ScheduleServiceStateTranslator; import com.hedera.node.app.spi.state.MigrationContext; import com.hedera.node.app.spi.state.Schema; @@ -40,6 +41,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Set; import java.util.function.BiConsumer; @@ -144,26 +146,30 @@ public void value(long scheduleId) { final WritableKVState schedulesByEquality = ctx.newStates().get(SCHEDULES_BY_EQUALITY_KEY); fs.byEquality().forEachNode((scheduleEqualityVirtualKey, sevv) -> { - List schedules = new ArrayList<>(); sevv.getIds().forEach(new BiConsumer() { @Override public void accept(String scheduleObjHash, Long scheduleId) { var schedule = schedulesById.get( ScheduleID.newBuilder().scheduleNum(scheduleId).build()); - if (schedule != null) schedules.add(schedule); - else { + if (schedule != null) { + final var equalityKey = new ProtoString(ScheduleStoreUtility.calculateStringHash(schedule)); + final var existingList = schedulesByEquality.get(equalityKey); + final List existingSchedules = existingList == null + ? new ArrayList<>() + : new ArrayList<>(existingList.schedulesOrElse(Collections.emptyList())); + existingSchedules.add(schedule); + schedulesByEquality.put( + equalityKey, + ScheduleList.newBuilder() + .schedules(existingSchedules) + .build()); + } else { log.error("BBM: ERROR: no schedule for scheduleObjHash->id " + scheduleObjHash + " -> " + scheduleId); } } }); - - schedulesByEquality.put( - ProtoString.newBuilder() - .value(String.valueOf(scheduleEqualityVirtualKey.getKeyAsLong())) - .build(), - ScheduleList.newBuilder().schedules(schedules).build()); }); if (schedulesByEquality.isModified()) ((WritableKVStateBase) schedulesByEquality).commit(); log.info("BBM: finished schedule by equality migration"); diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/processors/CustomMessageCallProcessor.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/processors/CustomMessageCallProcessor.java index a8adf8b887e7..ac319f340180 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/processors/CustomMessageCallProcessor.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/processors/CustomMessageCallProcessor.java @@ -23,6 +23,9 @@ import static com.hedera.node.app.service.contract.impl.exec.failure.CustomExceptionalHaltReason.INVALID_SIGNATURE; import static com.hedera.node.app.service.contract.impl.exec.utils.FrameUtils.acquiredSenderAuthorizationViaDelegateCall; import static com.hedera.node.app.service.contract.impl.exec.utils.FrameUtils.alreadyHalted; +import static com.hedera.node.app.service.contract.impl.exec.utils.FrameUtils.isTopLevelTransaction; +import static com.hedera.node.app.service.contract.impl.exec.utils.FrameUtils.proxyUpdaterFor; +import static com.hedera.node.app.service.contract.impl.exec.utils.FrameUtils.recordBuilderFor; import static com.hedera.node.app.service.contract.impl.exec.utils.FrameUtils.setPropagatedCallFailure; import static com.hedera.node.app.service.contract.impl.exec.utils.FrameUtils.transfersValue; import static com.hedera.node.app.service.contract.impl.hevm.HevmPropagatedCallFailure.MISSING_RECEIVER_SIGNATURE; @@ -37,6 +40,7 @@ import com.hedera.node.app.service.contract.impl.exec.systemcontracts.HederaSystemContract; import com.hedera.node.app.service.contract.impl.hevm.ActionSidecarContentTracer; import com.hedera.node.app.service.contract.impl.hevm.HederaWorldUpdater; +import com.hedera.node.app.service.contract.impl.state.ProxyEvmAccount; import com.hedera.node.app.service.contract.impl.state.ProxyWorldUpdater; import com.swirlds.config.api.Configuration; import edu.umd.cs.findbugs.annotations.NonNull; @@ -103,7 +107,7 @@ public CustomMessageCallProcessor( *
  • An existing account.
  • * * - * @param frame the frame to start + * @param frame the frame to start * @param tracer the operation tracer */ @Override @@ -144,6 +148,15 @@ public void start(@NonNull final MessageFrame frame, @NonNull final OperationTra return; } + // For mono-service fidelity, we need to consider called contracts + // as a special case eligible for staking rewards + if (isTopLevelTransaction(frame)) { + final var maybeCalledContract = proxyUpdaterFor(frame).get(codeAddress); + if (maybeCalledContract instanceof ProxyEvmAccount a && a.isContract()) { + recordBuilderFor(frame).trackExplicitRewardSituation(a.hederaId()); + } + } + frame.setState(MessageFrame.State.CODE_EXECUTING); } @@ -174,9 +187,9 @@ private void doExecutePrecompile( * the call to computePrecompile. Thus, the logic for checking for sufficient gas must be done in a different * order vs normal precompiles. * - * @param systemContract the system contract to execute - * @param frame the current frame - * @param tracer the operation tracer + * @param systemContract the system contract to execute + * @param frame the current frame + * @param tracer the operation tracer */ private void doExecuteSystemContract( @NonNull final HederaSystemContract systemContract, diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/ClassicTransfersCall.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/ClassicTransfersCall.java index dbc01b171918..1bcc2b790a43 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/ClassicTransfersCall.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/ClassicTransfersCall.java @@ -81,8 +81,8 @@ public class ClassicTransfersCall extends AbstractHtsCall { private final CallStatusStandardizer callStatusStandardizer; private final SystemAccountCreditScreen systemAccountCreditScreen; - private final VerificationStrategy verificationStrategy; + private final SpecialRewardReceivers specialRewardReceivers; // too many parameters @SuppressWarnings("java:S107") @@ -97,7 +97,8 @@ public ClassicTransfersCall( @Nullable ApprovalSwitchHelper approvalSwitchHelper, @NonNull final CallStatusStandardizer callStatusStandardizer, @NonNull final VerificationStrategy verificationStrategy, - @NonNull final SystemAccountCreditScreen systemAccountCreditScreen) { + @NonNull final SystemAccountCreditScreen systemAccountCreditScreen, + @NonNull final SpecialRewardReceivers specialRewardReceivers) { super(gasCalculator, enhancement, false); this.selector = requireNonNull(selector); this.senderId = requireNonNull(senderId); @@ -108,6 +109,7 @@ public ClassicTransfersCall( this.callStatusStandardizer = requireNonNull(callStatusStandardizer); this.systemAccountCreditScreen = systemAccountCreditScreen; this.verificationStrategy = requireNonNull(verificationStrategy); + this.specialRewardReceivers = requireNonNull(specialRewardReceivers); } /** @@ -154,6 +156,7 @@ public ClassicTransfersCall( final var op = transferToDispatch.cryptoTransferOrThrow(); if (recordBuilder.status() == SUCCESS) { maybeEmitErcLogsFor(op, frame); + specialRewardReceivers.addInFrame(frame, op, recordBuilder.getAssessedCustomFees()); } else { recordBuilder.status(callStatusStandardizer.codeForFailure(recordBuilder.status(), frame, op)); } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/ClassicTransfersTranslator.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/ClassicTransfersTranslator.java index bcc65c03174f..da171978991f 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/ClassicTransfersTranslator.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/ClassicTransfersTranslator.java @@ -18,6 +18,7 @@ import static com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.transfer.ApprovalSwitchHelper.APPROVAL_SWITCH_HELPER; import static com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.transfer.CallStatusStandardizer.CALL_STATUS_STANDARDIZER; +import static com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.transfer.SpecialRewardReceivers.SPECIAL_REWARD_RECEIVERS; import static com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.transfer.SystemAccountCreditScreen.SYSTEM_ACCOUNT_CREDIT_SCREEN; import com.esaulpaugh.headlong.abi.Function; @@ -87,7 +88,8 @@ public ClassicTransfersCall callFrom(@NonNull final HtsCallAttempt attempt) { isClassicCall(selector) ? APPROVAL_SWITCH_HELPER : null, CALL_STATUS_STANDARDIZER, attempt.defaultVerificationStrategy(), - SYSTEM_ACCOUNT_CREDIT_SCREEN); + SYSTEM_ACCOUNT_CREDIT_SCREEN, + SPECIAL_REWARD_RECEIVERS); } private @Nullable TransactionBody nominalBodyFor(@NonNull final HtsCallAttempt attempt) { diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/Erc20TransfersCall.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/Erc20TransfersCall.java index 3477afb0b154..71cd2aa287f3 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/Erc20TransfersCall.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/Erc20TransfersCall.java @@ -64,6 +64,7 @@ public class Erc20TransfersCall extends AbstractHtsCall { private final AccountID senderId; private final AddressIdConverter addressIdConverter; private final boolean requiresApproval; + private final SpecialRewardReceivers specialRewardReceivers; // too many parameters @SuppressWarnings("java:S107") @@ -77,7 +78,8 @@ public Erc20TransfersCall( @NonNull final VerificationStrategy verificationStrategy, @NonNull final AccountID senderId, @NonNull final AddressIdConverter addressIdConverter, - final boolean requiresApproval) { + final boolean requiresApproval, + @NonNull final SpecialRewardReceivers specialRewardReceivers) { super(gasCalculator, enhancement, false); this.amount = amount; this.from = from; @@ -87,6 +89,7 @@ public Erc20TransfersCall( this.senderId = requireNonNull(senderId); this.addressIdConverter = requireNonNull(addressIdConverter); this.requiresApproval = requiresApproval; + this.specialRewardReceivers = requireNonNull(specialRewardReceivers); } /** @@ -114,15 +117,15 @@ public Erc20TransfersCall( return gasOnly(revertResult(recordBuilder, gasRequirement), status, false); } } else { - final var tokenTransferLists = - syntheticTransfer.cryptoTransferOrThrow().tokenTransfersOrThrow(); - for (final var fungibleTransfers : tokenTransferLists) { + final var op = syntheticTransfer.cryptoTransferOrThrow(); + for (final var fungibleTransfers : op.tokenTransfersOrThrow()) { TransferEventLoggingUtils.logSuccessfulFungibleTransfer( requireNonNull(tokenId), fungibleTransfers.transfersOrThrow(), enhancement.nativeOperations().readableAccountStore(), frame); } + specialRewardReceivers.addInFrame(frame, op, recordBuilder.getAssessedCustomFees()); final var encodedOutput = (from == null) ? ERC_20_TRANSFER.getOutputs().encodeElements(true) : ERC_20_TRANSFER_FROM.getOutputs().encodeElements(true); diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/Erc20TransfersTranslator.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/Erc20TransfersTranslator.java index 62f2da3470fe..b56ef823943b 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/Erc20TransfersTranslator.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/Erc20TransfersTranslator.java @@ -17,6 +17,7 @@ package com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.transfer; import static com.hedera.hapi.node.base.TokenType.NON_FUNGIBLE_UNIQUE; +import static com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.transfer.SpecialRewardReceivers.SPECIAL_REWARD_RECEIVERS; import static java.util.Objects.requireNonNull; import com.esaulpaugh.headlong.abi.Address; @@ -83,7 +84,8 @@ private Erc20TransfersCall callFrom( attempt.defaultVerificationStrategy(), attempt.senderId(), attempt.addressIdConverter(), - requiresApproval); + requiresApproval, + SPECIAL_REWARD_RECEIVERS); } private boolean selectorsInclude(@NonNull final byte[] selector) { diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/Erc721TransferFromCall.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/Erc721TransferFromCall.java index 10102c503bd2..8391808973ee 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/Erc721TransferFromCall.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/Erc721TransferFromCall.java @@ -55,6 +55,7 @@ public class Erc721TransferFromCall extends AbstractHtsCall { private final VerificationStrategy verificationStrategy; private final AccountID senderId; private final AddressIdConverter addressIdConverter; + private final SpecialRewardReceivers specialRewardReceivers; // too many parameters @SuppressWarnings("java:S107") @@ -67,7 +68,8 @@ public Erc721TransferFromCall( @NonNull final HederaWorldUpdater.Enhancement enhancement, @NonNull final SystemContractGasCalculator gasCalculator, @NonNull final AccountID senderId, - @NonNull final AddressIdConverter addressIdConverter) { + @NonNull final AddressIdConverter addressIdConverter, + @NonNull final SpecialRewardReceivers specialRewardReceivers) { super(gasCalculator, enhancement, false); this.from = requireNonNull(from); this.to = requireNonNull(to); @@ -76,6 +78,7 @@ public Erc721TransferFromCall( this.senderId = requireNonNull(senderId); this.addressIdConverter = requireNonNull(addressIdConverter); this.serialNo = serialNo; + this.specialRewardReceivers = requireNonNull(specialRewardReceivers); } @NonNull diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/Erc721TransferFromTranslator.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/Erc721TransferFromTranslator.java index c97221b4c487..c5722da4d25b 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/Erc721TransferFromTranslator.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/Erc721TransferFromTranslator.java @@ -17,6 +17,7 @@ package com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.transfer; import static com.hedera.hapi.node.base.TokenType.NON_FUNGIBLE_UNIQUE; +import static com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.transfer.SpecialRewardReceivers.SPECIAL_REWARD_RECEIVERS; import static java.util.Objects.requireNonNull; import com.esaulpaugh.headlong.abi.Function; @@ -63,6 +64,7 @@ public HtsCall callFrom(@NonNull final HtsCallAttempt attempt) { attempt.enhancement(), attempt.systemContractGasCalculator(), attempt.senderId(), - attempt.addressIdConverter()); + attempt.addressIdConverter(), + SPECIAL_REWARD_RECEIVERS); } } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/SpecialRewardReceivers.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/SpecialRewardReceivers.java new file mode 100644 index 000000000000..82c9b41ce94f --- /dev/null +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/SpecialRewardReceivers.java @@ -0,0 +1,62 @@ +/* + * 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.systemcontracts.hts.transfer; + +import static com.hedera.node.app.service.contract.impl.exec.utils.FrameUtils.recordBuilderFor; +import static java.util.Collections.emptyList; + +import com.hedera.hapi.node.base.TransferList; +import com.hedera.hapi.node.token.CryptoTransferTransactionBody; +import com.hedera.hapi.node.transaction.AssessedCustomFee; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.List; +import org.hyperledger.besu.evm.frame.MessageFrame; + +/** + * Provides logic to detect account ids that need to be treated as in special + * reward situations for mono-service fidelity. + */ +public class SpecialRewardReceivers { + public static final SpecialRewardReceivers SPECIAL_REWARD_RECEIVERS = new SpecialRewardReceivers(); + + /** + * Adds any special reward receivers to the given frame for the given {@link CryptoTransferTransactionBody}. + * + * @param frame the frame to add to + * @param body the body to inspect + */ + public void addInFrame( + @NonNull final MessageFrame frame, + @NonNull final CryptoTransferTransactionBody body, + @NonNull final List assessedCustomFees) { + final var recordBuilder = recordBuilderFor(frame); + body.transfersOrElse(TransferList.DEFAULT) + .accountAmountsOrElse(emptyList()) + .forEach(adjustment -> recordBuilder.trackExplicitRewardSituation(adjustment.accountIDOrThrow())); + body.tokenTransfersOrElse(emptyList()).forEach(transfers -> { + transfers + .transfersOrElse(emptyList()) + .forEach(adjustment -> recordBuilder.trackExplicitRewardSituation(adjustment.accountIDOrThrow())); + transfers.nftTransfersOrElse(emptyList()).forEach(transfer -> { + recordBuilder.trackExplicitRewardSituation(transfer.senderAccountIDOrThrow()); + recordBuilder.trackExplicitRewardSituation(transfer.receiverAccountIDOrThrow()); + }); + }); + assessedCustomFees.forEach( + fee -> recordBuilder.trackExplicitRewardSituation(fee.feeCollectorAccountIdOrThrow())); + } +} diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/utils/FrameUtils.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/utils/FrameUtils.java index 5c1b62682727..2ae02090120d 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/utils/FrameUtils.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/utils/FrameUtils.java @@ -30,6 +30,7 @@ import com.hedera.node.app.service.contract.impl.exec.processors.CustomMessageCallProcessor; import com.hedera.node.app.service.contract.impl.hevm.HevmPropagatedCallFailure; import com.hedera.node.app.service.contract.impl.infra.StorageAccessTracker; +import com.hedera.node.app.service.contract.impl.records.ContractOperationRecordBuilder; import com.hedera.node.app.service.contract.impl.state.ProxyWorldUpdater; import com.hedera.node.app.service.contract.impl.utils.ConversionUtils; import com.hedera.node.app.spi.workflows.record.DeleteCapableTransactionRecordBuilder; @@ -147,6 +148,27 @@ public static void setPropagatedCallFailure( return requireNonNull(initialFrameOf(frame).getContextVariable(HAPI_RECORD_BUILDER_CONTEXT_VARIABLE)); } + /** + * Returns true if the given frame has a record builder. + * + * @param frame the frame to check + * @return true if the frame has a record builder + */ + public static boolean isTopLevelTransaction(@NonNull final MessageFrame frame) { + return initialFrameOf(frame).hasContextVariable(HAPI_RECORD_BUILDER_CONTEXT_VARIABLE); + } + + /** + * Returns a record builder able to track the contracts called in the frame's + * EVM transaction. + * + * @param frame the frame whose EVM transaction we are tracking called contracts in + * @return the record builder + */ + public static @NonNull ContractOperationRecordBuilder recordBuilderFor(@NonNull final MessageFrame frame) { + return requireNonNull(initialFrameOf(frame).getContextVariable(HAPI_RECORD_BUILDER_CONTEXT_VARIABLE)); + } + public static @NonNull SystemContractGasCalculator systemContractGasCalculatorOf( @NonNull final MessageFrame frame) { return initialFrameOf(frame).getContextVariable(SYSTEM_CONTRACT_GAS_CALCULATOR_CONTEXT_VARIABLE); diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/handlers/ContractGetBytecodeHandler.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/handlers/ContractGetBytecodeHandler.java index 38d4ecaf6f3c..3f95d147c57a 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/handlers/ContractGetBytecodeHandler.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/handlers/ContractGetBytecodeHandler.java @@ -69,8 +69,9 @@ public Response createEmptyResponse(@NonNull final ResponseHeader header) { @Override public void validate(@NonNull final QueryContext context) throws PreCheckException { requireNonNull(context); - validateFalsePreCheck(contractFrom(context) == null, INVALID_CONTRACT_ID); - validateFalsePreCheck(contractFrom(context).deleted(), CONTRACT_DELETED); + final var contract = contractFrom(context); + validateFalsePreCheck(contract == null, INVALID_CONTRACT_ID); + validateFalsePreCheck(requireNonNull(contract).deleted(), CONTRACT_DELETED); } @Override diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/records/ContractCallRecordBuilder.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/records/ContractCallRecordBuilder.java index f5e15a7cf5e1..a2093e429255 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/records/ContractCallRecordBuilder.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/records/ContractCallRecordBuilder.java @@ -21,6 +21,7 @@ import com.hedera.hapi.node.base.TokenID; import com.hedera.hapi.node.base.Transaction; import com.hedera.hapi.node.contract.ContractFunctionResult; +import com.hedera.hapi.node.transaction.AssessedCustomFee; import com.hedera.pbj.runtime.io.buffer.Bytes; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; @@ -30,6 +31,13 @@ * Exposes the record customizations needed for a HAPI contract call transaction. */ public interface ContractCallRecordBuilder extends ContractOperationRecordBuilder { + /** + * Returns all assessed custom fees for this call. + * + * @return the assessed custom fees + */ + @NonNull + List getAssessedCustomFees(); /** * Tracks the final status of a top-level contract call. diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/records/ContractOperationRecordBuilder.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/records/ContractOperationRecordBuilder.java index 65726523906d..2131f821773b 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/records/ContractOperationRecordBuilder.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/records/ContractOperationRecordBuilder.java @@ -18,12 +18,14 @@ import static java.util.Objects.requireNonNull; +import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.streams.ContractActions; import com.hedera.hapi.streams.ContractBytecode; import com.hedera.hapi.streams.ContractStateChanges; import com.hedera.node.app.service.contract.impl.exec.CallOutcome; import com.hedera.node.app.spi.workflows.record.DeleteCapableTransactionRecordBuilder; import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Set; public interface ContractOperationRecordBuilder extends DeleteCapableTransactionRecordBuilder { /** @@ -34,6 +36,22 @@ public interface ContractOperationRecordBuilder extends DeleteCapableTransaction */ ContractOperationRecordBuilder transactionFee(long transactionFee); + /** + * Tracks the ID of an account that should be explicitly considered + * as in a "reward situation"; that is, to collect any pending native + * staking rewards it has accrued. + * + * @param accountId the account ID + */ + void trackExplicitRewardSituation(@NonNull AccountID accountId); + + /** + * Gets the set of contract IDs called during the transaction. + * + * @return the set of contract IDs called during the transaction + */ + Set explicitRewardSituationIds(); + /** * Updates this record builder to include the standard contract fields from the given outcome. * diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/state/ProxyEvmAccount.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/state/ProxyEvmAccount.java index c97767ef0030..097dc389e2ec 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/state/ProxyEvmAccount.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/state/ProxyEvmAccount.java @@ -101,7 +101,7 @@ public Wei getBalance() { @Override public void setNonce(final long value) { - state.setNonce(accountID.accountNumOrElse(AccountID.DEFAULT.accountNum()), value); + state.setNonce(accountID.accountNumOrThrow(), value); } @Override diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/transfer/ClassicTransfersCallTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/transfer/ClassicTransfersCallTest.java index 988cd5dc515e..9a38a050547d 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/transfer/ClassicTransfersCallTest.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/transfer/ClassicTransfersCallTest.java @@ -45,6 +45,7 @@ import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.transfer.CallStatusStandardizer; import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.transfer.ClassicTransfersCall; import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.transfer.ClassicTransfersTranslator; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.transfer.SpecialRewardReceivers; import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.transfer.SystemAccountCreditScreen; import com.hedera.node.app.service.contract.impl.records.ContractCallRecordBuilder; import com.hedera.node.app.service.contract.impl.test.exec.systemcontracts.hts.HtsCallTestBase; @@ -79,6 +80,9 @@ class ClassicTransfersCallTest extends HtsCallTestBase { @Mock private SystemContractGasCalculator systemContractGasCalculator; + @Mock + private SpecialRewardReceivers specialRewardReceivers; + private ClassicTransfersCall subject; @Test @@ -232,7 +236,8 @@ private void givenRetryingSubject() { approvalSwitchHelper, callStatusStandardizer, verificationStrategy, - systemAccountCreditScreen); + systemAccountCreditScreen, + specialRewardReceivers); } private void givenHaltingSubject() { @@ -247,7 +252,8 @@ private void givenHaltingSubject() { approvalSwitchHelper, callStatusStandardizer, verificationStrategy, - systemAccountCreditScreen); + systemAccountCreditScreen, + specialRewardReceivers); } private void givenV2SubjectWithV2Enabled() { @@ -265,7 +271,8 @@ private void givenV2SubjectWithV2Enabled() { null, callStatusStandardizer, verificationStrategy, - systemAccountCreditScreen); + systemAccountCreditScreen, + specialRewardReceivers); } private void givenV2SubjectWithV2Disabled() { @@ -280,6 +287,7 @@ private void givenV2SubjectWithV2Disabled() { null, callStatusStandardizer, verificationStrategy, - systemAccountCreditScreen); + systemAccountCreditScreen, + specialRewardReceivers); } } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/transfer/Erc20TransfersCallTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/transfer/Erc20TransfersCallTest.java index 8cdd0e78f59f..bc8b2fe12ce3 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/transfer/Erc20TransfersCallTest.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/transfer/Erc20TransfersCallTest.java @@ -42,6 +42,7 @@ import com.hedera.node.app.service.contract.impl.exec.scope.VerificationStrategy; import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.AddressIdConverter; import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.transfer.Erc20TransfersCall; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.transfer.SpecialRewardReceivers; import com.hedera.node.app.service.contract.impl.records.ContractCallRecordBuilder; import com.hedera.node.app.service.contract.impl.test.exec.systemcontracts.hts.HtsCallTestBase; import com.hedera.node.app.service.contract.impl.utils.ConversionUtils; @@ -71,6 +72,9 @@ class Erc20TransfersCallTest extends HtsCallTestBase { @Mock private SystemContractGasCalculator systemContractGasCalculator; + @Mock + private SpecialRewardReceivers specialRewardReceivers; + private Erc20TransfersCall subject; @Test @@ -85,7 +89,8 @@ void revertsOnMissingToken() { verificationStrategy, SENDER_ID, addressIdConverter, - false); + false, + specialRewardReceivers); final var result = subject.execute(frame).fullResult().result(); @@ -176,7 +181,8 @@ private Erc20TransfersCall subjectForTransfer(final long amount) { verificationStrategy, SENDER_ID, addressIdConverter, - false); + false, + specialRewardReceivers); } private Erc20TransfersCall subjectForTransferFrom(final long amount) { @@ -190,6 +196,7 @@ private Erc20TransfersCall subjectForTransferFrom(final long amount) { verificationStrategy, SENDER_ID, addressIdConverter, - false); + false, + specialRewardReceivers); } } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/transfer/Erc721TransferFromCallTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/transfer/Erc721TransferFromCallTest.java index 4de43e69c7ee..7749ac9a44dc 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/transfer/Erc721TransferFromCallTest.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/transfer/Erc721TransferFromCallTest.java @@ -36,6 +36,7 @@ import com.hedera.node.app.service.contract.impl.exec.scope.VerificationStrategy; import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.AddressIdConverter; import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.transfer.Erc721TransferFromCall; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.transfer.SpecialRewardReceivers; import com.hedera.node.app.service.contract.impl.records.ContractCallRecordBuilder; import com.hedera.node.app.service.contract.impl.test.exec.systemcontracts.hts.HtsCallTestBase; import com.hedera.node.app.service.contract.impl.utils.ConversionUtils; @@ -59,6 +60,9 @@ class Erc721TransferFromCallTest extends HtsCallTestBase { @Mock private VerificationStrategy verificationStrategy; + @Mock + private SpecialRewardReceivers specialRewardReceivers; + @Mock private ContractCallRecordBuilder recordBuilder; @@ -123,6 +127,7 @@ private Erc721TransferFromCall subjectFor(final long serialNo) { mockEnhancement(), gasCalculator, SENDER_ID, - addressIdConverter); + addressIdConverter, + specialRewardReceivers); } } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/transfer/SpecialRewardReceiversTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/transfer/SpecialRewardReceiversTest.java new file mode 100644 index 000000000000..6ffcc3b40ef6 --- /dev/null +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/transfer/SpecialRewardReceiversTest.java @@ -0,0 +1,127 @@ +/* + * 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.test.exec.systemcontracts.hts.transfer; + +import static com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.transfer.SpecialRewardReceivers.SPECIAL_REWARD_RECEIVERS; +import static com.hedera.node.app.service.contract.impl.exec.utils.FrameUtils.HAPI_RECORD_BUILDER_CONTEXT_VARIABLE; +import static com.hedera.node.app.service.contract.impl.test.TestHelpers.A_NEW_ACCOUNT_ID; +import static com.hedera.node.app.service.contract.impl.test.TestHelpers.B_NEW_ACCOUNT_ID; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +import com.hedera.hapi.node.base.AccountAmount; +import com.hedera.hapi.node.base.NftTransfer; +import com.hedera.hapi.node.base.TokenTransferList; +import com.hedera.hapi.node.base.TransferList; +import com.hedera.hapi.node.token.CryptoTransferTransactionBody; +import com.hedera.hapi.node.transaction.AssessedCustomFee; +import com.hedera.node.app.service.contract.impl.records.ContractOperationRecordBuilder; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.List; +import org.hyperledger.besu.evm.frame.MessageFrame; +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 SpecialRewardReceiversTest { + @Mock + private MessageFrame frame; + + @Mock + private MessageFrame initialFrame; + + @Mock + private ContractOperationRecordBuilder recordBuilder; + + private final Deque stack = new ArrayDeque<>(); + + @BeforeEach + void setUp() { + stack.push(initialFrame); + stack.addFirst(frame); + given(frame.getMessageFrameStack()).willReturn(stack); + given(initialFrame.getContextVariable(HAPI_RECORD_BUILDER_CONTEXT_VARIABLE)) + .willReturn(recordBuilder); + } + + @Test + void addsFungibleTokenTransfers() { + final var body = CryptoTransferTransactionBody.newBuilder() + .tokenTransfers(TokenTransferList.newBuilder() + .transfers(List.of( + AccountAmount.newBuilder() + .accountID(A_NEW_ACCOUNT_ID) + .build(), + AccountAmount.newBuilder() + .accountID(B_NEW_ACCOUNT_ID) + .build())) + .build()) + .build(); + SPECIAL_REWARD_RECEIVERS.addInFrame(frame, body, List.of()); + + verify(recordBuilder).trackExplicitRewardSituation(A_NEW_ACCOUNT_ID); + verify(recordBuilder).trackExplicitRewardSituation(B_NEW_ACCOUNT_ID); + } + + @Test + void addsNftOwnershipChanges() { + final var body = CryptoTransferTransactionBody.newBuilder() + .tokenTransfers(TokenTransferList.newBuilder() + .nftTransfers(new NftTransfer(A_NEW_ACCOUNT_ID, B_NEW_ACCOUNT_ID, 123L, true)) + .build()) + .build(); + SPECIAL_REWARD_RECEIVERS.addInFrame(frame, body, List.of()); + + verify(recordBuilder).trackExplicitRewardSituation(A_NEW_ACCOUNT_ID); + verify(recordBuilder).trackExplicitRewardSituation(B_NEW_ACCOUNT_ID); + } + + @Test + void addsHbarTransfers() { + final var body = CryptoTransferTransactionBody.newBuilder() + .transfers(TransferList.newBuilder() + .accountAmounts(List.of( + AccountAmount.newBuilder() + .accountID(A_NEW_ACCOUNT_ID) + .build(), + AccountAmount.newBuilder() + .accountID(B_NEW_ACCOUNT_ID) + .build())) + .build()) + .build(); + SPECIAL_REWARD_RECEIVERS.addInFrame(frame, body, List.of()); + + verify(recordBuilder).trackExplicitRewardSituation(A_NEW_ACCOUNT_ID); + verify(recordBuilder).trackExplicitRewardSituation(B_NEW_ACCOUNT_ID); + } + + @Test + void tracksFeeCollectionAccounts() { + SPECIAL_REWARD_RECEIVERS.addInFrame( + frame, + CryptoTransferTransactionBody.DEFAULT, + List.of(AssessedCustomFee.newBuilder() + .feeCollectorAccountId(A_NEW_ACCOUNT_ID) + .build())); + verify(recordBuilder).trackExplicitRewardSituation(A_NEW_ACCOUNT_ID); + } +} diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/records/ContractOperationRecordBuilderTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/records/ContractOperationRecordBuilderTest.java index 25da8a219ce4..b83d1869943f 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/records/ContractOperationRecordBuilderTest.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/records/ContractOperationRecordBuilderTest.java @@ -34,7 +34,9 @@ import com.hedera.node.app.spi.workflows.record.SingleTransactionRecordBuilder; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Collections; import java.util.List; +import java.util.Set; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; @@ -42,6 +44,14 @@ class ContractOperationRecordBuilderTest { @Test void withGasFeeWorksAsExpected() { final var subject = new ContractOperationRecordBuilder() { + @Override + public void trackExplicitRewardSituation(@NotNull AccountID accountId) {} + + @Override + public Set explicitRewardSituationIds() { + return Collections.emptySet(); + } + private long totalFee = 456L; private ContractActions actions = null; private ContractStateChanges stateChanges = null; diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/FinalizeParentRecordHandler.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/FinalizeParentRecordHandler.java index 493a78819516..49215193664c 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/FinalizeParentRecordHandler.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/FinalizeParentRecordHandler.java @@ -50,6 +50,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Singleton; @@ -74,7 +75,8 @@ public FinalizeParentRecordHandler(@NonNull final StakingRewardsHandler stakingR public void finalizeParentRecord( @NonNull final AccountID payer, @NonNull final FinalizeContext context, - @NonNull final HederaFunctionality functionality) { + @NonNull final HederaFunctionality functionality, + @NonNull final Set explicitRewardReceivers) { final var recordBuilder = context.userTransactionRecordBuilder(CryptoTransferRecordBuilder.class); // This handler won't ask the context for its transaction, but instead will determine the net hbar transfers and @@ -93,7 +95,7 @@ public void finalizeParentRecord( // a node. They are also triggered if staking related fields are modified // Calculate staking rewards and add them also to hbarChanges here, before assessing // net changes for transaction record - final var rewardsPaid = stakingRewardsHandler.applyStakingRewards(context); + final var rewardsPaid = stakingRewardsHandler.applyStakingRewards(context, explicitRewardReceivers); if (requiresExternalization(rewardsPaid)) { recordBuilder.paidStakingRewards(asAccountAmounts(rewardsPaid)); } diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/EndOfStakingPeriodUpdater.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/EndOfStakingPeriodUpdater.java index 56f45699ffb1..3153a35394a5 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/EndOfStakingPeriodUpdater.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/EndOfStakingPeriodUpdater.java @@ -16,6 +16,7 @@ package com.hedera.node.app.service.token.impl.handlers.staking; +import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS; import static com.hedera.node.app.service.mono.utils.Units.HBARS_TO_TINYBARS; import static com.hedera.node.app.service.token.impl.handlers.BaseCryptoHandler.asAccount; import static com.hedera.node.app.service.token.impl.handlers.staking.EndOfStakingPeriodUtils.calculateRewardSumHistory; @@ -116,6 +117,7 @@ public void updateNodes(@NonNull final TokenContext context) { long newTotalStakedRewardStart = 0L; long maxStakeOfAllNodes = 0L; final Map updatedNodeInfos = new HashMap<>(); + final Map newPendingRewardRates = new HashMap<>(); for (final var nodeNum : nodeIds.stream().sorted().toList()) { var currStakingInfo = stakingInfoStore.getForModify(nodeNum); @@ -128,6 +130,7 @@ public void updateNodes(@NonNull final TokenContext context) { stakingConfig.perHbarRewardRate(), stakingConfig.requireMinStakeToReward()); final var newPendingRewardRate = newRewardSumHistory.pendingRewardRate(); + newPendingRewardRates.put(nodeNum, newPendingRewardRate); currStakingInfo = currStakingInfo .copyBuilder() .rewardSumHistory(newRewardSumHistory.rewardSumHistory()) @@ -206,6 +209,7 @@ public void updateNodes(@NonNull final TokenContext context) { newTotalStakedStart, sumOfConsensusWeights); finalNodeStakes.add(fromStakingInfo( + newPendingRewardRates.get(nodeNum), entry.getValue().copyBuilder().stake(scaledWeightToStake).build())); // Persist the updated staking info @@ -246,7 +250,8 @@ public void updateNodes(@NonNull final TokenContext context) { context.addUncheckedPrecedingChildRecordBuilder(NodeStakeUpdateRecordBuilder.class); nodeStakeUpdateBuilder .transaction(transactionWith(syntheticNodeStakeUpdateTxn.build())) - .memo("End of staking period calculation record"); + .memo("End of staking period calculation record") + .status(SUCCESS); } /** @@ -422,10 +427,11 @@ private static NetworkStakingRewards.Builder copy(final ReadableNetworkStakingRe .totalStakedStart(networkRewardsStore.totalStakedStart()); } - private static NodeStake fromStakingInfo(StakingNodeInfo stakingNodeInfo) { + private static NodeStake fromStakingInfo(final long rewardRate, StakingNodeInfo stakingNodeInfo) { return NodeStake.newBuilder() .nodeId(stakingNodeInfo.nodeNumber()) .stake(stakingNodeInfo.stake()) + .rewardRate(rewardRate) .minStake(stakingNodeInfo.minStake()) .maxStake(stakingNodeInfo.maxStake()) .stakeRewarded(stakingNodeInfo.stakeToReward()) diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/StakingRewardsDistributor.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/StakingRewardsDistributor.java index fe4cb4ff6ee1..2c6437a01acb 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/StakingRewardsDistributor.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/StakingRewardsDistributor.java @@ -65,6 +65,9 @@ public Map payRewardsIfPending( final Map rewardsPaid = new HashMap<>(); for (final var receiver : possibleRewardReceivers) { final var originalAccount = writableStore.getOriginalValue(receiver); + if (originalAccount == null) { + continue; + } final var modifiedAccount = writableStore.get(receiver); final var reward = rewardCalculator.computePendingReward( originalAccount, stakingInfoStore, stakingRewardsStore, consensusNow); diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/StakingRewardsHandler.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/StakingRewardsHandler.java index 4c32a9946a7a..8dfbfff91996 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/StakingRewardsHandler.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/StakingRewardsHandler.java @@ -21,7 +21,9 @@ import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.state.token.Account; import com.hedera.node.app.service.token.records.FinalizeContext; +import edu.umd.cs.findbugs.annotations.NonNull; import java.util.Map; +import java.util.Set; /** * On each transaction, before finalizing the state to a transaction record, goes through all the modified accounts @@ -38,10 +40,18 @@ public interface StakingRewardsHandler { /** * Goes through all the modified accounts and pays out the staking rewards if any and returns the map of account id * to the amount of rewards paid out. + * + *

    For mono-service fidelity, also supports taking an extra set of accounts + * to explicitly consider for staking rewards, even if they do not appear to be + * in a reward situation. This is needed to trigger rewards for accounts that + * are listed in the HBAR adjustments of a {@code CryptoTransfer}; but with a + * zero adjustment amount. + * * @param context the context of the transaction + * @param explicitRewardReceivers a set of accounts that must be considered for rewards independent of the context * @return a map of account id to the amount of rewards paid out */ - Map applyStakingRewards(final FinalizeContext context); + Map applyStakingRewards(FinalizeContext context, @NonNull Set explicitRewardReceivers); /** * Checks if the account has been rewarded since the last staking metadata change. diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/StakingRewardsHandlerImpl.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/StakingRewardsHandlerImpl.java index 5f99f8f7a392..08daf4eeb4aa 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/StakingRewardsHandlerImpl.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/StakingRewardsHandlerImpl.java @@ -68,7 +68,10 @@ public StakingRewardsHandlerImpl( /** {@inheritDoc} */ @Override - public Map applyStakingRewards(final FinalizeContext context) { + public Map applyStakingRewards( + final FinalizeContext context, @NonNull final Set explicitRewardReceivers) { + requireNonNull(context); + requireNonNull(explicitRewardReceivers); final var writableStore = context.writableStore(WritableAccountStore.class); final var stakingRewardsStore = context.writableStore(WritableNetworkStakingRewardsStore.class); final var stakingInfoStore = context.writableStore(WritableStakingInfoStore.class); @@ -77,10 +80,11 @@ public Map applyStakingRewards(final FinalizeContext context) { final var consensusNow = context.consensusTime(); // When an account StakedIdType is FROM_ACCOUNT or TO_ACCOUNT, we need to assess if the staked accountId // could be in a reward situation. So add those staked accountIds to the list of possible reward receivers - final var specialRewardReceivers = getStakedToMeRewardReceivers(writableStore); + final var stakedToMeRewardReceivers = getStakedToMeRewardReceivers(writableStore); // In addition to the above set, iterate through all modifications in state and // get list of possible reward receivers which are staked to node - final var rewardReceivers = getAllRewardReceivers(writableStore, specialRewardReceivers); + final var rewardReceivers = + getAllRewardReceivers(writableStore, stakedToMeRewardReceivers, explicitRewardReceivers); // Pay rewards to all possible reward receivers, returns all rewards paid final var recordBuilder = context.userTransactionRecordBuilder(DeleteCapableTransactionRecordBuilder.class); final var rewardsPaid = rewardsPayer.payRewardsIfPending( @@ -163,7 +167,7 @@ public Set getStakedToMeRewardReceivers(@NonNull final WritableAccoun Set specialRewardReceivers = null; for (final var id : modifiedAccounts) { final var originalAccount = writableStore.getOriginalValue(id); - final var modifiedAccount = writableStore.get(id); + final var modifiedAccount = requireNonNull(writableStore.get(id)); // check if stakedId has changed final var scenario = StakeIdChangeType.forCase(originalAccount, modifiedAccount); @@ -172,7 +176,9 @@ public Set getStakedToMeRewardReceivers(@NonNull final WritableAccoun // stakedToMe balance of new account. This is needed in order to trigger next level rewards // if the account is staked to node if (scenario.equals(FROM_ACCOUNT_TO_ACCOUNT) - && originalAccount.stakedAccountId().equals(modifiedAccount.stakedAccountId())) { + && requireNonNull(originalAccount) + .stakedAccountIdOrThrow() + .equals(modifiedAccount.stakedAccountId())) { // Even if the stakee's total stake hasn't changed, we still want to // trigger a reward situation whenever the staker balance changes if (modifiedAccount.tinybarBalance() != originalAccount.tinybarBalance()) { diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/StakingRewardsHelper.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/StakingRewardsHelper.java index f2afadeabb7c..dbb3d0e8116e 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/StakingRewardsHelper.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/StakingRewardsHelper.java @@ -32,6 +32,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.util.ArrayList; +import java.util.Collection; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -59,25 +60,48 @@ public StakingRewardsHelper() { * and has stakedId or stakedToMe or balance or declineReward changed in this transaction. * * @param writableAccountStore The store to write to for updated values and original values - * @param specialRewardReceivers The accounts which are staked to a node and are special reward receivers + * @param stakeToMeRewardReceivers The accounts which are staked to a node and are special reward receivers + * @param explicitRewardReceivers Extra accounts to consider for rewards * @return A list of accounts which are staked to a node and could possibly receive a reward */ public static Set getAllRewardReceivers( - final WritableAccountStore writableAccountStore, final Set specialRewardReceivers) { - final var possibleRewardReceivers = new LinkedHashSet<>(specialRewardReceivers); - for (final AccountID id : writableAccountStore.modifiedAccountsInState()) { - final var modifiedAcct = writableAccountStore.get(id); - final var originalAcct = writableAccountStore.getOriginalValue(id); - // It is possible that original account is null if the account was created in this transaction - // In that case it is not a reward situation - // If the account existed before this transaction and is staked to a node, - // and the current transaction modified the stakedToMe field or declineReward or - // the stakedId field, then it is a reward situation - if (isRewardSituation(modifiedAcct, originalAcct)) { - possibleRewardReceivers.add(id); + final WritableAccountStore writableAccountStore, + final Set stakeToMeRewardReceivers, + @NonNull final Set explicitRewardReceivers) { + final var possibleRewardReceivers = new LinkedHashSet<>(stakeToMeRewardReceivers); + addIdsInRewardSituation( + writableAccountStore, + writableAccountStore.modifiedAccountsInState(), + possibleRewardReceivers, + FilterType.IS_CANONICAL_REWARD_SITUATION); + addIdsInRewardSituation( + writableAccountStore, explicitRewardReceivers, possibleRewardReceivers, FilterType.IS_STAKED_TO_NODE); + return possibleRewardReceivers; + } + + private enum FilterType { + IS_CANONICAL_REWARD_SITUATION, + IS_STAKED_TO_NODE + } + + private static void addIdsInRewardSituation( + @NonNull final WritableAccountStore writableAccountStore, + @NonNull final Collection ids, + @NonNull final Set possibleRewardReceivers, + @NonNull final FilterType filterType) { + for (final AccountID id : ids) { + if (filterType == FilterType.IS_CANONICAL_REWARD_SITUATION) { + final var modifiedAcct = requireNonNull(writableAccountStore.get(id)); + final var originalAcct = writableAccountStore.getOriginalValue(id); + if (isRewardSituation(modifiedAcct, originalAcct)) { + possibleRewardReceivers.add(id); + } + } else { + if (isCurrentlyStakedToNode(writableAccountStore.get(id))) { + possibleRewardReceivers.add(id); + } } } - return possibleRewardReceivers; } /** @@ -100,9 +124,7 @@ private static boolean isRewardSituation( // in previous step final var hasBalanceChange = modifiedAccount.tinybarBalance() != originalAccount.tinybarBalance(); final var hasStakeMetaChanges = hasStakeMetaChanges(originalAccount, modifiedAccount); - // We do this for backward compatibility with mono-service - final var isCalledContract = modifiedAccount.smartContract(); - return (isCalledContract || hasBalanceChange || hasStakeMetaChanges); + return hasBalanceChange || hasStakeMetaChanges; } /** @@ -245,4 +267,11 @@ public static List asAccountAmounts(@NonNull final Map> mutableInputTokenAdjustments, @NonNull final TokenID denom, - @NonNull final Map filteredCredits) { + @NonNull final Map filteredCredits, + @NonNull final Map creditsForToken) { // if we reached here it means there are credits for the token final var map = mutableInputTokenAdjustments.get(denom); for (final var entry : filteredCredits.entrySet()) { final var account = entry.getKey(); final var amount = entry.getValue(); - map.put(account, amount); + // Further reduce the credit to an effective payer account + // by the amount that was redirected to fee collector accounts + map.merge(account, amount - creditsForToken.get(account), Long::sum); } mutableInputTokenAdjustments.put(denom, map); } diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/validators/CryptoTransferValidator.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/validators/CryptoTransferValidator.java index eebd03ce522c..3ca233d97be1 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/validators/CryptoTransferValidator.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/validators/CryptoTransferValidator.java @@ -168,7 +168,7 @@ public void validateSemantics( // Verify that the current total number of (counted) fungible transfers does not exceed the limit validateTrue( - totalFungibleTransfers < ledgerConfig.tokenTransfersMaxLen(), + totalFungibleTransfers <= ledgerConfig.tokenTransfersMaxLen(), TOKEN_TRANSFER_LIST_SIZE_LIMIT_EXCEEDED); // Verify that the current total number of (counted) nft transfers does not exceed the limit validateTrue(totalNftTransfers <= ledgerConfig.nftTransfersMaxLen(), BATCH_SIZE_LIMIT_EXCEEDED); diff --git a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/fixtures/FakeNodeStakeUpdateRecordBuilder.java b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/fixtures/FakeNodeStakeUpdateRecordBuilder.java index 42125f13de06..1b615e960d12 100644 --- a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/fixtures/FakeNodeStakeUpdateRecordBuilder.java +++ b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/fixtures/FakeNodeStakeUpdateRecordBuilder.java @@ -16,6 +16,7 @@ package com.hedera.node.app.service.token.impl.test.fixtures; +import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.hapi.node.base.Transaction; import com.hedera.node.app.service.token.records.NodeStakeUpdateRecordBuilder; import org.jetbrains.annotations.NotNull; @@ -27,6 +28,11 @@ public NodeStakeUpdateRecordBuilder create() { private String memo; private Transaction txn; + @Override + public NodeStakeUpdateRecordBuilder status(@NotNull ResponseCodeEnum status) { + return null; + } + @NotNull @Override public NodeStakeUpdateRecordBuilder transaction(@NotNull final Transaction txn) { diff --git a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/FinalizeParentRecordHandlerTest.java b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/FinalizeParentRecordHandlerTest.java index 16ce4943a379..0301a33c09f5 100644 --- a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/FinalizeParentRecordHandlerTest.java +++ b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/FinalizeParentRecordHandlerTest.java @@ -119,8 +119,8 @@ public void setUp() { @Test void handleNullArg() { - assertThatThrownBy( - () -> subject.finalizeParentRecord(ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE)) + assertThatThrownBy(() -> subject.finalizeParentRecord( + ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE, Collections.emptySet())) .isInstanceOf(NullPointerException.class); } @@ -137,8 +137,8 @@ void handleHbarNetTransferAmountIsNotZero() { given(context.userTransactionRecordBuilder(SingleTransactionRecordBuilder.class)) .willReturn(mock(SingleTransactionRecordBuilder.class)); - assertThatThrownBy( - () -> subject.finalizeParentRecord(ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE)) + assertThatThrownBy(() -> subject.finalizeParentRecord( + ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE, Collections.emptySet())) .isInstanceOf(HandleException.class) .has(responseCode(FAIL_INVALID)); } @@ -162,8 +162,8 @@ void handleHbarAccountBalanceIsNegative() { given(context.userTransactionRecordBuilder(SingleTransactionRecordBuilder.class)) .willReturn(mock(SingleTransactionRecordBuilder.class)); - assertThatThrownBy( - () -> subject.finalizeParentRecord(ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE)) + assertThatThrownBy(() -> subject.finalizeParentRecord( + ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE, Collections.emptySet())) .isInstanceOf(HandleException.class) .has(responseCode(FAIL_INVALID)); } @@ -183,7 +183,8 @@ void handleHbarAccountBalanceDoesntChange() { context = mockContext(); given(context.configuration()).willReturn(configuration); - subject.finalizeParentRecord(ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE); + subject.finalizeParentRecord( + ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE, Collections.emptySet()); BDDMockito.verifyNoInteractions(recordBuilder); } @@ -210,7 +211,8 @@ void handleHbarTransfersToNewAccountSuccess() { context = mockContext(); given(context.configuration()).willReturn(configuration); - subject.finalizeParentRecord(ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE); + subject.finalizeParentRecord( + ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE, Collections.emptySet()); BDDMockito.verify(recordBuilder) .transferList(TransferList.newBuilder() @@ -275,7 +277,8 @@ void handleHbarTransfersToAccountDeductsFromChildRecordsSuccess() { .when(context) .forEachChildRecord(any(), any()); - subject.finalizeParentRecord(ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE); + subject.finalizeParentRecord( + ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE, Collections.emptySet()); final var transferAmount1212 = -amountToTransfer + childRecordTransfer; final var transferAmount3434 = amountToTransfer - childRecordTransfer; @@ -351,7 +354,8 @@ void handleFungibleTokenTransfersToAccountDeductsFromChildRecordsSuccess() { .build()) .build())); - subject.finalizeParentRecord(ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE); + subject.finalizeParentRecord( + ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE, Collections.emptySet()); BDDMockito.verify(recordBuilder) .tokenTransferLists(List.of(TokenTransferList.newBuilder() @@ -399,7 +403,8 @@ void accountsForDissociatedTokenRelations() { context = mockContext(); given(context.configuration()).willReturn(configuration); - subject.finalizeParentRecord(ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE); + subject.finalizeParentRecord( + ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE, Collections.emptySet()); BDDMockito.verify(recordBuilder) .tokenTransferLists(List.of(TokenTransferList.newBuilder() @@ -438,7 +443,8 @@ void nftBurnsOrWipesAreAccounted() { context = mockContext(); given(context.configuration()).willReturn(configuration); - subject.finalizeParentRecord(ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE); + subject.finalizeParentRecord( + ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE, Collections.emptySet()); BDDMockito.verify(recordBuilder) .tokenTransferLists(List.of(TokenTransferList.newBuilder() @@ -476,7 +482,8 @@ void handleHbarTransfersToExistingAccountSuccess() { context = mockContext(); given(context.configuration()).willReturn(configuration); - subject.finalizeParentRecord(ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE); + subject.finalizeParentRecord( + ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE, Collections.emptySet()); BDDMockito.verify(recordBuilder) .transferList(TransferList.newBuilder() @@ -506,8 +513,8 @@ void handleFungibleTokenBalanceIsNegative() { context = mockContext(); given(context.configuration()).willReturn(configuration); - assertThatThrownBy( - () -> subject.finalizeParentRecord(ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE)) + assertThatThrownBy(() -> subject.finalizeParentRecord( + ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE, Collections.emptySet())) .isInstanceOf(HandleException.class) .has(responseCode(FAIL_INVALID)); } @@ -528,7 +535,8 @@ void handleFungibleTransferTokenBalancesDontChange() { context = mockContext(); given(context.configuration()).willReturn(configuration); - subject.finalizeParentRecord(ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE); + subject.finalizeParentRecord( + ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE, Collections.emptySet()); BDDMockito.verifyNoInteractions(recordBuilder); } @@ -569,7 +577,8 @@ void handleFungibleTransfersToNewAccountSuccess() { context = mockContext(); given(context.configuration()).willReturn(configuration); - subject.finalizeParentRecord(ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE); + subject.finalizeParentRecord( + ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE, Collections.emptySet()); BDDMockito.verify(recordBuilder) .tokenTransferLists(List.of(TokenTransferList.newBuilder() @@ -660,7 +669,8 @@ void handleFungibleTransfersToExistingAccountsSuccess() { context = mockContext(); given(context.configuration()).willReturn(configuration); - subject.finalizeParentRecord(ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE); + subject.finalizeParentRecord( + ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE, Collections.emptySet()); BDDMockito.verify(recordBuilder) .tokenTransferLists(List.of( @@ -725,7 +735,8 @@ void handleNftTransfersToNewAccountSuccess() { context = mockContext(); given(context.configuration()).willReturn(configuration); - subject.finalizeParentRecord(ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE); + subject.finalizeParentRecord( + ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE, Collections.emptySet()); BDDMockito.verify(recordBuilder) .tokenTransferLists(List.of(TokenTransferList.newBuilder() @@ -763,7 +774,8 @@ void handleNewNftTransferToAccountSuccess() { context = mockContext(); given(context.configuration()).willReturn(configuration); - subject.finalizeParentRecord(ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE); + subject.finalizeParentRecord( + ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE, Collections.emptySet()); BDDMockito.verify(recordBuilder) .tokenTransferLists(List.of(TokenTransferList.newBuilder() @@ -837,7 +849,8 @@ void handleNftTransfersToExistingAccountSuccess() { .getOrCreateConfig(); given(context.configuration()).willReturn(config); - subject.finalizeParentRecord(ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE); + subject.finalizeParentRecord( + ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE, Collections.emptySet()); // The transfer list should be sorted by token ID, then by serial number BDDMockito.verify(recordBuilder) @@ -873,8 +886,9 @@ void handleNftTransfersToExistingAccountSuccess() { .build()) .build())); - subject.finalizeParentRecord(ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE); - verify(stakingRewardsHandler, never()).applyStakingRewards(context); + subject.finalizeParentRecord( + ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE, Collections.emptySet()); + verify(stakingRewardsHandler, never()).applyStakingRewards(context, Collections.emptySet()); } @Test @@ -927,7 +941,8 @@ void handleCombinedHbarAndTokenTransfersSuccess() { context = mockContext(); given(context.configuration()).willReturn(configuration); - subject.finalizeParentRecord(ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE); + subject.finalizeParentRecord( + ACCOUNT_1212_ID, context, HederaFunctionality.CRYPTO_DELETE, Collections.emptySet()); BDDMockito.verify(recordBuilder) .transferList(TransferList.newBuilder() diff --git a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/staking/StakingRewardsHandlerImplTest.java b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/staking/StakingRewardsHandlerImplTest.java index 3ee42ac36b20..e17f5f7daca9 100644 --- a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/staking/StakingRewardsHandlerImplTest.java +++ b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/staking/StakingRewardsHandlerImplTest.java @@ -45,6 +45,7 @@ import java.time.LocalDate; import java.time.ZoneOffset; import java.time.temporal.ChronoUnit; +import java.util.Collections; import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -99,7 +100,7 @@ void changingKeyOnlyIsNotRewardSituation() { noStakeChanges(); - final var rewards = subject.applyStakingRewards(context); + final var rewards = subject.applyStakingRewards(context, Collections.emptySet()); assertThat(rewards).isEmpty(); final var modifiedAccount = writableAccountStore.get(payerId); @@ -120,7 +121,7 @@ void rewardsWhenStakingFieldsModified() { randomStakeNodeChanges(); - final var rewards = subject.applyStakingRewards(context); + final var rewards = subject.applyStakingRewards(context, Collections.emptySet()); // earned zero rewards due to zero stake assertThat(rewards).hasSize(1); @@ -169,7 +170,7 @@ void anAccountThatStartedStakingBeforeCurrentPeriodAndHasntBeenRewardedUnclaimsS given(context.consensusTime()).willReturn(nextDayInstant); given(context.writableStore(WritableAccountStore.class)).willReturn(writableAccountStore); - subject.applyStakingRewards(context); + subject.applyStakingRewards(context, Collections.emptySet()); final var payerAfter = writableAccountStore.get(payerId); final var node1Info = writableStakingInfoState.get(node1Id); @@ -201,7 +202,7 @@ void anAccountThatStartedStakingBeforeCurrentPeriodAndWasRewardedDaysAgoUnclaims given(context.consensusTime()).willReturn(nextDayInstant); - subject.applyStakingRewards(context); + subject.applyStakingRewards(context, Collections.emptySet()); final var node1Info = writableStakingInfoState.get(node1Id); // Since the node is rewarded in last period the unclaimed reward will be stakeAtStartOfLastRewardPeriod. @@ -233,7 +234,7 @@ void anAccountThatStartedStakingBeforeCurrentPeriodAndWasRewardedTodayUnclaimsSt .atStartOfDay(ZoneOffset.UTC) .toInstant()); - subject.applyStakingRewards(context); + subject.applyStakingRewards(context, Collections.emptySet()); final var node1Info = writableStakingInfoState.get(node1Id); // Since the node is rewarded in last period and stakePeriodStart is the previous period @@ -260,7 +261,7 @@ void anAccountThatStartedStakingAtCurrentPeriodDoesntUnclaimStakeWhenChangingEle given(context.consensusTime()).willReturn(stakePeriodStartInstant); given(context.writableStore(WritableAccountStore.class)).willReturn(writableAccountStore); - subject.applyStakingRewards(context); + subject.applyStakingRewards(context, Collections.emptySet()); final var node1Info = writableStakingInfoState.get(node1Id); @@ -287,7 +288,7 @@ void anAccountThatDeclineRewardsDoesntUnclaimStakeWhenChangingElection() { given(context.consensusTime()).willReturn(originalInstant); given(context.writableStore(WritableAccountStore.class)).willReturn(writableAccountStore); - subject.applyStakingRewards(context); + subject.applyStakingRewards(context, Collections.emptySet()); final var node1Info = writableStakingInfoState.get(node1Id); @@ -345,7 +346,7 @@ void anAccountWithAlreadyCollectedRewardShouldNotHaveStakeStartUpdated() { given(context.consensusTime()).willReturn(stakePeriodStartInstant); given(context.writableStore(WritableAccountStore.class)).willReturn(writableAccountStore); - subject.applyStakingRewards(context); + subject.applyStakingRewards(context, Collections.emptySet()); final var node1Info = writableStakingInfoState.get(node1Id); @@ -399,7 +400,7 @@ void calculatesRewardIfNeededStakingToNode() { .toInstant()); given(context.writableStore(WritableAccountStore.class)).willReturn(writableAccountStore); - final var rewards = subject.applyStakingRewards(context); + final var rewards = subject.applyStakingRewards(context, Collections.emptySet()); final var node1InfoAfter = writableStakingInfoState.get(node1Id); final var node0InfoAfter = writableStakingInfoState.get(node0Id); @@ -467,7 +468,7 @@ void doesNotAwardStakeFromDeletedAccount() { given(recordBuilder.getNumberOfDeletedAccounts()).willReturn(1); given(recordBuilder.getDeletedAccountBeneficiaryFor(payerId)).willReturn(ownerId); - final var rewards = subject.applyStakingRewards(context); + final var rewards = subject.applyStakingRewards(context, Collections.emptySet()); assertThat(rewards).hasSize(1); // because the transferId is owner for the deleted payer account assertThat(rewards).containsEntry(ownerId, 178900L); @@ -499,7 +500,7 @@ void stakingEffectsWorkAsExpectedWhenStakingToNodeWithNoStakingMetaChanges() { .atStartOfDay(ZoneOffset.UTC) .toInstant()); - final var rewards = subject.applyStakingRewards(context); + final var rewards = subject.applyStakingRewards(context, Collections.emptySet()); final var node1InfoAfter = writableStakingInfoState.get(node1Id); @@ -542,7 +543,7 @@ void stakingEffectsWorkAsExpectedWhenStakingToNodeWithNoStakingMetaChangesAndNoR .toInstant()); given(context.writableStore(WritableAccountStore.class)).willReturn(writableAccountStore); - final var rewards = subject.applyStakingRewards(context); + final var rewards = subject.applyStakingRewards(context, Collections.emptySet()); final var node1InfoAfter = writableStakingInfoState.get(node1Id); @@ -588,7 +589,7 @@ void sasolarpMgmtWorksAsExpectedWhenStakingToNodeWithNoStakingMetaChangesAndNoRe .toInstant()); // No rewards rewarded - final var rewards = subject.applyStakingRewards(context); + final var rewards = subject.applyStakingRewards(context, Collections.emptySet()); final var node1InfoAfter = writableStakingInfoState.get(node1Id); @@ -644,7 +645,7 @@ void stakingEffectsWorkAsExpectedWhenStakingToAccount() { .atStartOfDay(ZoneOffset.UTC) .toInstant()); - final var rewards = subject.applyStakingRewards(context); + final var rewards = subject.applyStakingRewards(context, Collections.emptySet()); final var node1InfoAfter = writableStakingInfoState.get(node1Id); @@ -709,7 +710,7 @@ void rewardsUltimateBeneficiaryInsteadOfDeletedAccount() { given(recordBuilder.getNumberOfDeletedAccounts()).willReturn(1); given(recordBuilder.getDeletedAccountBeneficiaryFor(payerId)).willReturn(ownerId); - final var rewards = subject.applyStakingRewards(context); + final var rewards = subject.applyStakingRewards(context, Collections.emptySet()); assertThat(rewards).hasSize(1); // because the transferId is owner for the deleted payer account assertThat(rewards).containsEntry(ownerId, 178900L); @@ -761,7 +762,7 @@ void doesntTrackAnythingIfRedirectBeneficiaryDeclinedReward() { given(recordBuilder.getNumberOfDeletedAccounts()).willReturn(1); given(recordBuilder.getDeletedAccountBeneficiaryFor(payerId)).willReturn(ownerId); - final var rewards = subject.applyStakingRewards(context); + final var rewards = subject.applyStakingRewards(context, Collections.emptySet()); // because the transferId is owner and it declined reward assertThat(rewards).hasSize(1); } @@ -819,7 +820,8 @@ void failsHardIfMoreRedirectsThanDeletedEntitiesAreNeeded() { given(recordBuilder.getDeletedAccountBeneficiaryFor(payerId)).willReturn(ownerId); given(recordBuilder.getDeletedAccountBeneficiaryFor(ownerId)).willReturn(spenderId); - assertThatThrownBy(() -> subject.applyStakingRewards(context)).isInstanceOf(IllegalStateException.class); + assertThatThrownBy(() -> subject.applyStakingRewards(context, Collections.emptySet())) + .isInstanceOf(IllegalStateException.class); } @Test @@ -862,7 +864,7 @@ void updatesStakedToMeSideEffects() { given(context.writableStore(WritableAccountStore.class)).willReturn(writableAccountStore); final var originalPayer = writableAccountStore.get(payerId); - final var rewards = subject.applyStakingRewards(context); + final var rewards = subject.applyStakingRewards(context, Collections.emptySet()); // even though only payer account has changed, since staked to me of owner changes, // it will trigger reward for owner @@ -926,7 +928,7 @@ void doesntUpdateStakedToMeIfStakerBalanceIsExactlyTheSame() { final var originalPayer = writableAccountStore.get(payerId); // This should not change anything - final var rewards = subject.applyStakingRewards(context); + final var rewards = subject.applyStakingRewards(context, Collections.emptySet()); // No rewards should be paid assertThat(rewards).isEmpty(); @@ -983,7 +985,7 @@ void stakePeriodStartUpdatedWhenStakedToAccount() { given(context.writableStore(WritableAccountStore.class)).willReturn(writableAccountStore); final var originalPayer = writableAccountStore.get(payerId); - final var rewards = subject.applyStakingRewards(context); + final var rewards = subject.applyStakingRewards(context, Collections.emptySet()); assertThat(rewards).hasSize(1).containsEntry(ownerId, 6600L); diff --git a/hedera-node/hedera-token-service/src/main/java/com/hedera/node/app/service/token/records/NodeStakeUpdateRecordBuilder.java b/hedera-node/hedera-token-service/src/main/java/com/hedera/node/app/service/token/records/NodeStakeUpdateRecordBuilder.java index a29c9bdf7d67..9441eb63b49a 100644 --- a/hedera-node/hedera-token-service/src/main/java/com/hedera/node/app/service/token/records/NodeStakeUpdateRecordBuilder.java +++ b/hedera-node/hedera-token-service/src/main/java/com/hedera/node/app/service/token/records/NodeStakeUpdateRecordBuilder.java @@ -16,6 +16,7 @@ package com.hedera.node.app.service.token.records; +import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.hapi.node.base.Transaction; import edu.umd.cs.findbugs.annotations.NonNull; @@ -23,6 +24,13 @@ * A {@code RecordBuilder} specialization for tracking {@code NodeStakeUpdate} at midnight UTC every day. */ public interface NodeStakeUpdateRecordBuilder { + /** + * Sets the status. + * + * @param status the status + */ + NodeStakeUpdateRecordBuilder status(@NonNull ResponseCodeEnum status); + /** * Sets the transaction. * diff --git a/hedera-node/hedera-token-service/src/main/java/com/hedera/node/app/service/token/records/ParentRecordFinalizer.java b/hedera-node/hedera-token-service/src/main/java/com/hedera/node/app/service/token/records/ParentRecordFinalizer.java index e1dc380f795d..4ee80074c906 100644 --- a/hedera-node/hedera-token-service/src/main/java/com/hedera/node/app/service/token/records/ParentRecordFinalizer.java +++ b/hedera-node/hedera-token-service/src/main/java/com/hedera/node/app/service/token/records/ParentRecordFinalizer.java @@ -19,6 +19,7 @@ import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.HederaFunctionality; import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Set; /** * This class is used to "finalize" hbar and token transfers for the parent transaction record. @@ -42,5 +43,8 @@ */ public interface ParentRecordFinalizer { void finalizeParentRecord( - @NonNull AccountID payer, @NonNull FinalizeContext context, final HederaFunctionality functionality); + @NonNull AccountID payer, + @NonNull FinalizeContext context, + HederaFunctionality functionality, + @NonNull Set explicitRewardReceivers); } From b8cf8dfc2b718d8c378ca22cb10876111bc096b7 Mon Sep 17 00:00:00 2001 From: Michael Heinrichs Date: Wed, 6 Mar 2024 08:16:52 +0100 Subject: [PATCH 020/115] chore: Enable modularization (#11753) Signed-off-by: Neeharika-Sompalli Signed-off-by: Michael Tinker Signed-off-by: Michael Heinrichs Co-authored-by: Neeharika-Sompalli Co-authored-by: Michael Tinker Co-authored-by: Neeharika Sompalli <52669918+Neeharika-Sompalli@users.noreply.github.com> --- .../node-flow-pull-request-checks.yaml | 2 ++ .../data/config/application.properties | 16 ++++++++++++++ hedera-node/data/config/genesis.properties | 19 +++++++++++++++++ .../{ => data/config}/application.properties | 0 .../{ => data/config}/genesis.properties | 0 .../node/app/config/ConfigProviderBase.java | 4 ++-- .../app/state/merkle/MerkleHederaState.java | 4 ++-- .../com/hedera/node/app/ServicesMainTest.java | 21 ++++++++++++++----- .../formats/BlockRecordFactoryImplTest.java | 6 +++++- .../config/data/BlockRecordStreamConfig.java | 2 +- .../hedera/node/config/data/HederaConfig.java | 2 +- .../node/app/service/mono/ServicesState.java | 4 ++-- .../app/service/mono/ServicesStateTest.java | 2 +- .../impl/handlers/FreezeUpgradeActions.java | 14 ++----------- .../src/main/java/module-info.java | 1 + .../InitialModServiceContractSchema.java | 2 +- .../schemas/InitialModServiceTokenSchema.java | 8 +++---- .../services/bdd/junit/HapiTestEnv.java | 12 ++++++++++- settings.gradle.kts | 2 +- 19 files changed, 87 insertions(+), 34 deletions(-) create mode 100644 hedera-node/data/config/application.properties create mode 100644 hedera-node/data/config/genesis.properties rename hedera-node/hedera-app/{ => data/config}/application.properties (100%) rename hedera-node/hedera-app/{ => data/config}/genesis.properties (100%) diff --git a/.github/workflows/node-flow-pull-request-checks.yaml b/.github/workflows/node-flow-pull-request-checks.yaml index c62d27a93a72..636870ac8295 100644 --- a/.github/workflows/node-flow-pull-request-checks.yaml +++ b/.github/workflows/node-flow-pull-request-checks.yaml @@ -90,6 +90,7 @@ jobs: eet-tests: name: E2E Tests + if: ${{ false }} uses: ./.github/workflows/node-zxc-compile-application-code.yaml needs: - dependency-check @@ -107,6 +108,7 @@ jobs: integration-tests: name: Integration Tests + if: ${{ false }} uses: ./.github/workflows/node-zxc-compile-application-code.yaml needs: - dependency-check diff --git a/hedera-node/data/config/application.properties b/hedera-node/data/config/application.properties new file mode 100644 index 000000000000..6164bb5f6a0a --- /dev/null +++ b/hedera-node/data/config/application.properties @@ -0,0 +1,16 @@ +bootstrap.throttleDefsJson.resource=throttles-dev.json +ledger.id=0x03 +staking.periodMins=1 +scheduling.whitelist=ConsensusSubmitMessage,CryptoTransfer,TokenMint,TokenBurn,CryptoApproveAllowance,CryptoUpdate +# Uncomment a line below to test HAPI operations via workflows +hedera.workflows.enabled=true +#hedera.workflows.enabled=ConsensusCreateTopic,ConsensusUpdateTopic,ConsensusDeleteTopic,ConsensusSubmitMessage,ConsensusGetTopicInfo +# Initial Service workflow schemas require on-disk storage, so all these need to be true if enabling workflows +accounts.storeOnDisk=true +accounts.releaseAliasAfterDeletion=true +tokens.storeRelsOnDisk=true +tokens.nfts.useVirtualMerkle=true +records.useConsolidatedFcq=true +cache.cryptoTransfer.warmThreads=30 +contracts.maxNumWithHapiSigsAccess=0 +tokens.balancesInQueries.enabled=true \ No newline at end of file diff --git a/hedera-node/data/config/genesis.properties b/hedera-node/data/config/genesis.properties new file mode 100644 index 000000000000..6eb28390c65d --- /dev/null +++ b/hedera-node/data/config/genesis.properties @@ -0,0 +1,19 @@ +# +# Copyright (C) 2023 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. +# +# +# This file is needed for JRS tests of the configuration and should not be deleted until we can change it to look +# for the file in a different location +bar.test=genesis \ No newline at end of file diff --git a/hedera-node/hedera-app/application.properties b/hedera-node/hedera-app/data/config/application.properties similarity index 100% rename from hedera-node/hedera-app/application.properties rename to hedera-node/hedera-app/data/config/application.properties diff --git a/hedera-node/hedera-app/genesis.properties b/hedera-node/hedera-app/data/config/genesis.properties similarity index 100% rename from hedera-node/hedera-app/genesis.properties rename to hedera-node/hedera-app/data/config/genesis.properties diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/config/ConfigProviderBase.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/config/ConfigProviderBase.java index 88dd6e716c49..760f2377b8bf 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/config/ConfigProviderBase.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/config/ConfigProviderBase.java @@ -44,9 +44,9 @@ public abstract class ConfigProviderBase implements ConfigProvider { */ public static final String APPLICATION_PROPERTIES_PATH_ENV = "HEDERA_APP_PROPERTIES_PATH"; /** Default path to the genesis.properties file. */ - public static final String GENESIS_PROPERTIES_DEFAULT_PATH = "genesis.properties"; + public static final String GENESIS_PROPERTIES_DEFAULT_PATH = "data/config/genesis.properties"; /** Default path to the application.properties file. */ - public static final String APPLICATION_PROPERTIES_DEFAULT_PATH = "application.properties"; + public static final String APPLICATION_PROPERTIES_DEFAULT_PATH = "data/config/application.properties"; private static final Logger logger = LogManager.getLogger(ConfigProviderBase.class); /** Default path to the semantic-version.properties file. */ diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/merkle/MerkleHederaState.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/merkle/MerkleHederaState.java index d496ce8c64d1..478278702f4e 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/merkle/MerkleHederaState.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/merkle/MerkleHederaState.java @@ -109,10 +109,10 @@ public class MerkleHederaState extends PartialNaryMerkleInternal implements Merk */ private static final long DO_NOT_USE_IN_REAL_LIFE_CLASS_ID = 0x0000deadbeef0000L; - private static final long CLASS_ID = 0x2de3ead3caf06392L; + // private static final long CLASS_ID = 0x2de3ead3caf06392L; // Uncomment the following class ID to run a mono -> modular state migration // NOTE: also change class ID of ServicesState - // private static final long CLASS_ID = 0x8e300b0dfdafbb1aL; + private static final long CLASS_ID = 0x8e300b0dfdafbb1aL; private static final int VERSION_1 = 30; private static final int CURRENT_VERSION = VERSION_1; 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 c8b1369b35a7..82e597a795ea 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 @@ -18,7 +18,9 @@ import static com.hedera.node.app.service.mono.context.AppsManager.APPS; import static com.swirlds.platform.system.SystemExitCode.NODE_ADDRESS_MISMATCH; -import static com.swirlds.platform.system.status.PlatformStatus.*; +import static com.swirlds.platform.system.status.PlatformStatus.ACTIVE; +import static com.swirlds.platform.system.status.PlatformStatus.FREEZE_COMPLETE; +import static com.swirlds.platform.system.status.PlatformStatus.STARTING_UP; import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.hamcrest.MatcherAssert.assertThat; @@ -30,11 +32,9 @@ import static org.mockito.Mockito.verify; import com.hedera.node.app.service.mono.ServicesApp; -import com.hedera.node.app.service.mono.ServicesState; import com.hedera.node.app.service.mono.context.CurrentPlatformStatus; import com.hedera.node.app.service.mono.context.MutableStateChildren; import com.hedera.node.app.service.mono.context.NodeInfo; -import com.hedera.node.app.service.mono.context.properties.SerializableSemVers; import com.hedera.node.app.service.mono.grpc.GrpcStarter; import com.hedera.node.app.service.mono.state.exports.AccountsExporter; import com.hedera.node.app.service.mono.state.logic.StatusChangeListener; @@ -44,6 +44,8 @@ import com.hedera.node.app.service.mono.stream.RecordStreamManager; import com.hedera.node.app.service.mono.utils.NamedDigestFactory; import com.hedera.node.app.service.mono.utils.SystemExits; +import com.hedera.node.app.state.merkle.MerkleHederaState; +import com.hedera.node.app.version.HederaSoftwareVersion; import com.swirlds.common.notification.NotificationEngine; import com.swirlds.common.platform.NodeId; import com.swirlds.platform.config.legacy.ConfigurationException; @@ -67,6 +69,7 @@ import java.util.Optional; import java.util.function.Supplier; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -151,6 +154,7 @@ final class ServicesMainTest { private final ServicesMain subject = new ServicesMain(); @Test + @Disabled("Mono-specific behavior") void throwsErrorOnMissingApp() { // expect: Assertions.assertThrows(AssertionError.class, () -> subject.init(platform, unselfId)); @@ -200,10 +204,11 @@ void hardExitOnTooManyMatchingNodes() { @Test void returnsSerializableVersion() { - assertInstanceOf(SerializableSemVers.class, subject.getSoftwareVersion()); + assertInstanceOf(HederaSoftwareVersion.class, subject.getSoftwareVersion()); } @Test + @Disabled("Mono-specific behavior") void failsOnWrongNativeCharset() { withDoomedApp(); @@ -217,6 +222,7 @@ void failsOnWrongNativeCharset() { } @Test + @Disabled("Mono-specific behavior") void failsOnUnavailableDigest() throws NoSuchAlgorithmException { withDoomedApp(); @@ -232,6 +238,7 @@ void failsOnUnavailableDigest() throws NoSuchAlgorithmException { } @Test + @Disabled("Mono-specific behavior") void doesAppDrivenInit() throws NoSuchAlgorithmException { withRunnableApp(app); withChangeableApp(); @@ -262,10 +269,11 @@ void noopsAsExpected() { @Test void createsNewState() { // expect: - assertThat(subject.newState(), instanceOf(ServicesState.class)); + assertThat(subject.newState(), instanceOf(MerkleHederaState.class)); } @Test + @Disabled("Mono-specific behavior") void updatesCurrentMiscPlatformStatus() throws NoSuchAlgorithmException { final var listener = new StatusChangeListener(currentPlatformStatus, selfId, recordStreamManager); withRunnableApp(app); @@ -279,6 +287,7 @@ void updatesCurrentMiscPlatformStatus() throws NoSuchAlgorithmException { } @Test + @Disabled("Mono-specific behavior") void updatesCurrentActivePlatformStatus() throws NoSuchAlgorithmException { final var listener = new StatusChangeListener(currentPlatformStatus, selfId, recordStreamManager); withRunnableApp(app); @@ -293,6 +302,7 @@ void updatesCurrentActivePlatformStatus() throws NoSuchAlgorithmException { } @Test + @Disabled("Mono-specific behavior") void updatesCurrentMaintenancePlatformStatus() throws NoSuchAlgorithmException { final var listener = new StatusChangeListener(currentPlatformStatus, selfId, recordStreamManager); withRunnableApp(app); @@ -307,6 +317,7 @@ void updatesCurrentMaintenancePlatformStatus() throws NoSuchAlgorithmException { } @Test + @Disabled("Mono-specific behavior") void failsHardIfCannotInit() throws NoSuchAlgorithmException { withFailingApp(); diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/impl/producers/formats/BlockRecordFactoryImplTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/impl/producers/formats/BlockRecordFactoryImplTest.java index 0c345f4c6690..467fdc7a05e5 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/impl/producers/formats/BlockRecordFactoryImplTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/impl/producers/formats/BlockRecordFactoryImplTest.java @@ -28,7 +28,9 @@ final class BlockRecordFactoryImplTest extends AppTestBase { @Test void createV6BasedOnConfig() throws Exception { - final var app = appBuilder().build(); + final var app = appBuilder() + .withConfigValue("hedera.recordStream.logDir", "hedera-node/data/recordStreams") + .build(); final var factory = new BlockRecordWriterFactoryImpl(app.configProvider(), selfNodeInfo, SIGNER, FileSystems.getDefault()); final var writer = factory.create(); @@ -39,6 +41,7 @@ void createV6BasedOnConfig() throws Exception { void createV7BasedOnConfigThrows() throws Exception { final var app = appBuilder() .withConfigValue("hedera.recordStream.recordFileVersion", 7) + .withConfigValue("hedera.recordStream.logDir", "hedera-node/data/recordStreams") .build(); final var factory = @@ -52,6 +55,7 @@ void createV7BasedOnConfigThrows() throws Exception { void createUnknownVersionBasedOnConfigThrows() throws Exception { final var app = appBuilder() .withConfigValue("hedera.recordStream.recordFileVersion", 99999) + .withConfigValue("hedera.recordStream.logDir", "hedera-node/data/recordStreams") .build(); final var factory = diff --git a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/BlockRecordStreamConfig.java b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/BlockRecordStreamConfig.java index ac66370e26f5..082bca8ffa04 100644 --- a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/BlockRecordStreamConfig.java +++ b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/BlockRecordStreamConfig.java @@ -41,7 +41,7 @@ @ConfigData("hedera.recordStream") public record BlockRecordStreamConfig( @ConfigProperty(defaultValue = "true") @NodeProperty boolean enabled, - @ConfigProperty(defaultValue = "hedera-node/data/recordStreams") @NodeProperty String logDir, + @ConfigProperty(defaultValue = "/opt/hgcapp/recordStreams") @NodeProperty String logDir, @ConfigProperty(defaultValue = "sidecar") @NodeProperty String sidecarDir, @ConfigProperty(defaultValue = "2") @Min(1) @NodeProperty int logPeriod, @ConfigProperty(defaultValue = "5000") @Min(1) @NodeProperty int queueCapacity, diff --git a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/HederaConfig.java b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/HederaConfig.java index e5040579e9a8..10e41b13dfef 100644 --- a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/HederaConfig.java +++ b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/HederaConfig.java @@ -56,4 +56,4 @@ public record HederaConfig( @ConfigProperty(value = "workflow.verificationTimeoutMS", defaultValue = "20000") @NetworkProperty long workflowVerificationTimeoutMS, // FUTURE: Set. - @ConfigProperty(value = "workflows.enabled", defaultValue = "") @NetworkProperty String workflowsEnabled) {} + @ConfigProperty(value = "workflows.enabled", defaultValue = "true") @NetworkProperty String workflowsEnabled) {} diff --git a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/ServicesState.java b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/ServicesState.java index 81dd3dcbbd3a..333022ae9d50 100644 --- a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/ServicesState.java +++ b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/ServicesState.java @@ -115,10 +115,10 @@ public class ServicesState extends PartialNaryMerkleInternal implements MerkleInternal, SwirldState, StateChildrenProvider { private static final Logger log = LogManager.getLogger(ServicesState.class); - private static final long RUNTIME_CONSTRUCTABLE_ID = 0x8e300b0dfdafbb1aL; + // private static final long RUNTIME_CONSTRUCTABLE_ID = 0x8e300b0dfdafbb1aL; // Uncomment the following class ID to run a mono -> modular state migration // NOTE: also change class ID of MerkleHederaState - // private static final long RUNTIME_CONSTRUCTABLE_ID = 0x8e300b0dfdafbb1bL; + private static final long RUNTIME_CONSTRUCTABLE_ID = 0x8e300b0dfdafbb1bL; public static final ImmutableHash EMPTY_HASH = new ImmutableHash(new byte[DigestType.SHA_384.digestLength()]); // Only over-written when Platform deserializes a legacy version of the state diff --git a/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/ServicesStateTest.java b/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/ServicesStateTest.java index bec0e02f1895..eb32a9d12be0 100644 --- a/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/ServicesStateTest.java +++ b/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/ServicesStateTest.java @@ -404,7 +404,7 @@ void minimumChildCountsAsExpected() { @Test void merkleMetaAsExpected() { // expect: - assertEquals(0x8e300b0dfdafbb1aL, subject.getClassId()); + assertEquals(0x8e300b0dfdafbb1bL, subject.getClassId()); assertEquals(StateVersions.CURRENT_VERSION, subject.getVersion()); } diff --git a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/handlers/FreezeUpgradeActions.java b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/handlers/FreezeUpgradeActions.java index 7885eee90bf4..e662fb1172c5 100644 --- a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/handlers/FreezeUpgradeActions.java +++ b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/handlers/FreezeUpgradeActions.java @@ -30,11 +30,10 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Comparator; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Stream; +import org.apache.commons.io.FileUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -139,16 +138,7 @@ private CompletableFuture extractNow( private void extractAndReplaceArtifacts( String artifactsLoc, Bytes archiveData, long size, String desc, String marker, Timestamp now) { try { - try (Stream paths = Files.walk(Paths.get(artifactsLoc))) { - // delete any existing files in the artifacts directory - paths.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); - } - } catch (final IOException e) { - // above is a best-effort delete - // if it fails, we log the error and continue - log.error("Failed to delete existing files in {}", artifactsLoc, e); - } - try { + FileUtils.cleanDirectory(new File(artifactsLoc)); UnzipUtility.unzip(archiveData.toByteArray(), Paths.get(artifactsLoc)); log.info("Finished unzipping {} bytes for {} update into {}", size, desc, artifactsLoc); writeSecondMarker(marker, now); diff --git a/hedera-node/hedera-network-admin-service-impl/src/main/java/module-info.java b/hedera-node/hedera-network-admin-service-impl/src/main/java/module-info.java index 97c725552f68..44729e6babc3 100644 --- a/hedera-node/hedera-network-admin-service-impl/src/main/java/module-info.java +++ b/hedera-node/hedera-network-admin-service-impl/src/main/java/module-info.java @@ -17,6 +17,7 @@ requires com.google.common; requires com.swirlds.common; requires com.swirlds.config.api; + requires org.apache.commons.io; requires org.apache.logging.log4j; requires static com.github.spotbugs.annotations; requires static java.compiler; // javax.annotation.processing.Generated diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/state/InitialModServiceContractSchema.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/state/InitialModServiceContractSchema.java index ea2d58666654..fa9841436760 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/state/InitialModServiceContractSchema.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/state/InitialModServiceContractSchema.java @@ -55,7 +55,7 @@ public class InitialModServiceContractSchema extends Schema { public static final String STORAGE_KEY = "STORAGE"; public static final String BYTECODE_KEY = "BYTECODE"; private static final int MAX_BYTECODES = 50_000_000; - private static final int MAX_STORAGE_ENTRIES = 500_000_000; + private static final int MAX_STORAGE_ENTRIES = 1_000_000_000; private VirtualMapLike storageFromState; private Supplier> contractBytecodeFromState; diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/schemas/InitialModServiceTokenSchema.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/schemas/InitialModServiceTokenSchema.java index 97b28107efcb..525088d599f1 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/schemas/InitialModServiceTokenSchema.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/schemas/InitialModServiceTokenSchema.java @@ -104,10 +104,10 @@ public class InitialModServiceTokenSchema extends Schema { // These need to be big so databases are created at right scale. If they are too small then the on disk hash map // buckets will be too full which results in very poor performance. Have chosen 10 billion as should give us // plenty of runway. - private static final long MAX_TOKENS = 10_000_000_000L; - private static final long MAX_ACCOUNTS = 10_000_000_000L; - private static final long MAX_TOKEN_RELS = 10_000_000_000L; - private static final long MAX_MINTABLE_NFTS = 10_000_000_000L; + private static final long MAX_TOKENS = 1_000_000_000L; + private static final long MAX_ACCOUNTS = 1_000_000_000L; + private static final long MAX_TOKEN_RELS = 1_000_000_000L; + private static final long MAX_MINTABLE_NFTS = 1_000_000_000L; private static final long FIRST_RESERVED_SYSTEM_CONTRACT = 350L; private static final long LAST_RESERVED_SYSTEM_CONTRACT = 399L; private static final long FIRST_POST_SYSTEM_FILE_ENTITY = 200L; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/HapiTestEnv.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/HapiTestEnv.java index 4828851817e9..91cab11bcc00 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/HapiTestEnv.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/HapiTestEnv.java @@ -168,6 +168,7 @@ private void setupWorkingDirectory(@NonNull final Path workingDir, @NonNull fina Files.createDirectories(workingDir); Files.createDirectories(workingDir.resolve("data").resolve("keys")); + Files.createDirectories(workingDir.resolve("data").resolve("config")); final var configTextFile = workingDir.resolve("config.txt"); Files.writeString(configTextFile, configText); @@ -176,7 +177,16 @@ private void setupWorkingDirectory(@NonNull final Path workingDir, @NonNull fina Path.of("../configuration/dev").toAbsolutePath().normalize(); Files.walk(configDir).filter(file -> !file.equals(configDir)).forEach(file -> { try { - Files.copy(file, workingDir.resolve(file.getFileName().toString())); + if (file.getFileName().toString().contains(".properties")) { + Files.copy( + file, + workingDir + .resolve("data") + .resolve("config") + .resolve(file.getFileName().toString())); + } else { + Files.copy(file, workingDir.resolve(file.getFileName().toString())); + } } catch (Exception e) { throw new RuntimeException(e); } diff --git a/settings.gradle.kts b/settings.gradle.kts index 1dd165decb97..3a583bd2a1c4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -141,7 +141,7 @@ fun includeAllProjects(containingFolder: String) { } // The HAPI API version to use for Protobuf sources. -val hapiProtoVersion = "0.47.0" +val hapiProtoVersion = "0.47.3" dependencyResolutionManagement { // Protobuf tool versions From 18583b74d09ed795a01bf7bf8f03ed9baf3424f4 Mon Sep 17 00:00:00 2001 From: Cody Littley <56973212+cody-littley@users.noreply.github.com> Date: Wed, 6 Mar 2024 07:48:40 -0600 Subject: [PATCH 021/115] fix: bug in future event buffer (#11875) Signed-off-by: Cody Littley --- .../platform/event/FutureEventBuffer.java | 7 ++- .../event/FutureEventBufferTests.java | 63 +++++++++++++++++-- 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/FutureEventBuffer.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/FutureEventBuffer.java index 52fb513ad127..affc07b55fed 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/FutureEventBuffer.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/FutureEventBuffer.java @@ -112,8 +112,13 @@ public List addEvent(@NonNull final GossipEvent event) { public List updateEventWindow(@NonNull final NonAncientEventWindow eventWindow) { this.eventWindow = Objects.requireNonNull(eventWindow); + // We want to release all events with birth rounds less than or equal to the pending consensus round. + // In order to do that, we tell the sequence map to shift its window to the oldest round that we want + // to keep within the buffer. + final long oldestRoundToBuffer = eventWindow.getPendingConsensusRound() + 1; + final List events = new ArrayList<>(); - futureEvents.shiftWindow(eventWindow.getPendingConsensusRound(), (round, roundEvents) -> { + futureEvents.shiftWindow(oldestRoundToBuffer, (round, roundEvents) -> { for (final GossipEvent event : roundEvents) { if (!eventWindow.isAncient(event)) { events.add(event); diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/FutureEventBufferTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/FutureEventBufferTests.java index 8210b09766bf..df0694eefd41 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/FutureEventBufferTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/FutureEventBufferTests.java @@ -131,8 +131,8 @@ void futureEventsBufferedTest() { newPendingConsensusRound <= maxFutureRound; newPendingConsensusRound++) { - final NonAncientEventWindow newEventWindow = - new NonAncientEventWindow(newPendingConsensusRound, nonAncientBirthRound, 1, BIRTH_ROUND_THRESHOLD); + final NonAncientEventWindow newEventWindow = new NonAncientEventWindow( + newPendingConsensusRound - 1, nonAncientBirthRound, 1, BIRTH_ROUND_THRESHOLD); final List bufferedEvents = futureEventBuffer.updateEventWindow(newEventWindow); @@ -153,8 +153,8 @@ void futureEventsBufferedTest() { } /** - * It is plausible that we have a big jump in rounds due to a reconnect. Verify that we don't emit events - * if they become ancient while buffered. + * It is plausible that we have a big jump in rounds due to a reconnect. Verify that we don't emit events if they + * become ancient while buffered. */ @Test void eventsGoAncientWhileBufferedTest() { @@ -210,4 +210,59 @@ void eventsGoAncientWhileBufferedTest() { final List bufferedEvents = futureEventBuffer.updateEventWindow(newEventWindow); assertTrue(bufferedEvents.isEmpty()); } + + /** + * Verify that an event that is buffered gets released at the exact moment we expect. + */ + @Test + void eventInBufferIsReleasedOnTimeTest() { + final Random random = getRandomPrintSeed(); + + final Configuration configuration = new TestConfigBuilder() + .withValue(EventConfig_.USE_BIRTH_ROUND_ANCIENT_THRESHOLD, true) + .getOrCreateConfig(); + + final PlatformContext platformContext = TestPlatformContextBuilder.create() + .withConfiguration(configuration) + .build(); + + final FutureEventBuffer futureEventBuffer = new FutureEventBuffer(platformContext); + + final long pendingConsensusRound = random.nextLong(100, 1_000); + final long nonAncientBirthRound = pendingConsensusRound / 2; + + final NonAncientEventWindow eventWindow = + new NonAncientEventWindow(pendingConsensusRound - 1, nonAncientBirthRound, 1, BIRTH_ROUND_THRESHOLD); + futureEventBuffer.updateEventWindow(eventWindow); + + final long roundsUntilRelease = random.nextLong(10, 20); + final long eventBirthRound = pendingConsensusRound + roundsUntilRelease; + final GossipEvent event = generateEvent(random, eventBirthRound); + + // Event is from the future, we can't release it yet + assertNull(futureEventBuffer.addEvent(event)); + + // While the (newPendingConsensusRound-1) is less than the event's birth round, the event should be buffered + for (long currentConsensusRound = pendingConsensusRound - 1; + currentConsensusRound < eventBirthRound - 1; + currentConsensusRound++) { + + final NonAncientEventWindow newEventWindow = + new NonAncientEventWindow(currentConsensusRound, nonAncientBirthRound, 1, BIRTH_ROUND_THRESHOLD); + final List bufferedEvents = futureEventBuffer.updateEventWindow(newEventWindow); + assertTrue(bufferedEvents.isEmpty()); + } + + // When the pending consensus round is equal to the event's birth round, the event should be released + // Note: the pending consensus round is equal to the current consensus round + 1, but the argument + // for an event window takes the current consensus round, not the pending consensus round. + // To land with the pending consensus round at the exact value as the event's birth round, we need to + // set the current consensus round to the event's birth round - 1. + + final NonAncientEventWindow newEventWindow = + new NonAncientEventWindow(eventBirthRound - 1, nonAncientBirthRound, 1, BIRTH_ROUND_THRESHOLD); + final List bufferedEvents = futureEventBuffer.updateEventWindow(newEventWindow); + assertEquals(1, bufferedEvents.size()); + assertSame(event, bufferedEvents.getFirst()); + } } From 3977983fe6198308fb5feb8b6db5ce1519b345f6 Mon Sep 17 00:00:00 2001 From: Cody Littley <56973212+cody-littley@users.noreply.github.com> Date: Wed, 6 Mar 2024 08:10:33 -0600 Subject: [PATCH 022/115] feat: wiring proxy (#11846) Signed-off-by: Cody Littley --- .../wiring/component/ComponentWiring.java | 239 ++++++++++++++++++ .../wiring/component/InputWireLabel.java | 39 +++ .../component/internal/WireBindInfo.java | 33 +++ .../internal/WiringComponentProxy.java | 54 ++++ .../WiringComponentPerformanceTests.java | 131 ++++++++++ .../component/WiringComponentTests.java | 147 +++++++++++ 6 files changed, 643 insertions(+) create mode 100644 platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/ComponentWiring.java create mode 100644 platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/InputWireLabel.java create mode 100644 platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/internal/WireBindInfo.java create mode 100644 platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/internal/WiringComponentProxy.java create mode 100644 platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/component/WiringComponentPerformanceTests.java create mode 100644 platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/component/WiringComponentTests.java diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/ComponentWiring.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/ComponentWiring.java new file mode 100644 index 000000000000..0f52fab26181 --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/ComponentWiring.java @@ -0,0 +1,239 @@ +/* + * 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.common.wiring.component; + +import com.swirlds.common.wiring.component.internal.WireBindInfo; +import com.swirlds.common.wiring.component.internal.WiringComponentProxy; +import com.swirlds.common.wiring.schedulers.TaskScheduler; +import com.swirlds.common.wiring.wires.input.BindableInputWire; +import com.swirlds.common.wiring.wires.input.InputWire; +import com.swirlds.common.wiring.wires.output.OutputWire; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; + +/** + * Builds and manages input/output wires for a component. + * + * @param the type of the component + * @param the output type of the component + */ +public class ComponentWiring { + + private final TaskScheduler scheduler; + + private final WiringComponentProxy proxy = new WiringComponentProxy(); + private final COMPONENT_TYPE proxyComponent; + + private COMPONENT_TYPE component; + + private final Map bindInfo = new HashMap<>(); + private final Map> inputWires = new HashMap<>(); + + /** + * Create a new component wiring. + * + * @param clazz the interface class of the component + * @param scheduler the task scheduler that will run the component + */ + @SuppressWarnings("unchecked") + public ComponentWiring( + @NonNull final Class clazz, @NonNull final TaskScheduler scheduler) { + + this.scheduler = Objects.requireNonNull(scheduler); + if (!clazz.isInterface()) { + throw new IllegalArgumentException("Component class " + clazz.getName() + " is not an interface."); + } + + proxyComponent = (COMPONENT_TYPE) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] {clazz}, proxy); + } + + /** + * Get the output wire of this component. + * + * @return the output wire + */ + @NonNull + public OutputWire getOutputWire() { + return scheduler.getOutputWire(); + } + + /** + * Get an input wire for this component. + * + * @param handler the component method that will handle the input, e.g. "MyComponent::handleInput". Should be a + * method on the class, not a method on a specific instance. + * @param the type of the input + * @return the input wire + */ + public InputWire getInputWire( + @NonNull final BiFunction handler) { + + Objects.requireNonNull(handler); + + try { + handler.apply(proxyComponent, null); + } catch (final NullPointerException e) { + throw new IllegalStateException( + "Component wiring does not support primitive input types or return types. Use a boxed primitive instead."); + } + + return getOrBuildInputWire(proxy.getMostRecentlyInvokedMethod(), handler, null); + } + + /** + * Get an input wire for this component. + * + * @param handler the component method that will handle the input, e.g. "MyComponent::handleInput". Should be a + * method on the class, not a method on a specific instance. + * @param the input type + * @return the input wire + */ + public InputWire getInputWire( + @NonNull final BiConsumer handler) { + + Objects.requireNonNull(handler); + + try { + handler.accept(proxyComponent, null); + } catch (final NullPointerException e) { + throw new IllegalStateException( + "Component wiring does not support primitive input types. Use a boxed primitive instead."); + } + + return getOrBuildInputWire(proxy.getMostRecentlyInvokedMethod(), null, handler); + } + + /** + * Get the input wire for a specified method. + * + * @param method the method that will handle data on the input wire + * @param handlerWithReturn the handler for the method if it has a return type + * @param handlerWithoutReturn the handler for the method if it does not have a return type + * @param the input type + * @return the input wire + */ + @SuppressWarnings("unchecked") + private InputWire getOrBuildInputWire( + @NonNull final Method method, + @Nullable final BiFunction handlerWithReturn, + @Nullable final BiConsumer handlerWithoutReturn) { + + if (inputWires.containsKey(method)) { + // We've already created this wire + return (InputWire) inputWires.get(method); + } + + final String label; + final InputWireLabel inputWireLabel = method.getAnnotation(InputWireLabel.class); + if (inputWireLabel == null) { + label = method.getName(); + } else { + label = inputWireLabel.value(); + } + + final BindableInputWire inputWire = scheduler.buildInputWire(label); + inputWires.put(method, (BindableInputWire) inputWire); + + if (component == null) { + // we will bind this later + bindInfo.put(method, new WireBindInfo((BiFunction) handlerWithReturn, (BiConsumer< + Object, Object>) + handlerWithoutReturn)); + } else { + // bind this now + if (handlerWithReturn != null) { + inputWire.bind(x -> { + return handlerWithReturn.apply(component, x); + }); + } else { + inputWire.bind(x -> { + assert handlerWithoutReturn != null; + handlerWithoutReturn.accept(component, x); + }); + } + } + + return inputWire; + } + + /** + * Flush all data in the task scheduler. Blocks until all data currently in flight has been processed. + * + * @throws UnsupportedOperationException if the scheduler does not support flushing + */ + public void flush() { + scheduler.flush(); + } + + /** + * Start squelching the output of this component. + * + * @throws UnsupportedOperationException if the scheduler does not support squelching + * @throws IllegalStateException if the scheduler is already squelching + */ + public void startSquelching() { + scheduler.startSquelching(); + } + + /** + * Stop squelching the output of this component. + * + * @throws UnsupportedOperationException if the scheduler does not support squelching + * @throws IllegalStateException if the scheduler is not squelching + */ + public void stopSquelching() { + scheduler.stopSquelching(); + } + + /** + * Bind the component to the input wires. + * + * @param component the component to bind + */ + public void bind(@NonNull final COMPONENT_TYPE component) { + Objects.requireNonNull(component); + + this.component = component; + + for (final Map.Entry entry : bindInfo.entrySet()) { + + final Method method = entry.getKey(); + final WireBindInfo wireBindInfo = entry.getValue(); + final BindableInputWire wire = inputWires.get(method); + + if (wireBindInfo.handlerWithReturn() != null) { + final BiFunction handlerWithReturn = wireBindInfo.handlerWithReturn(); + wire.bind(x -> { + return handlerWithReturn.apply(component, x); + }); + } else { + final BiConsumer handlerWithoutReturn = + Objects.requireNonNull(wireBindInfo.handlerWithoutReturn()); + wire.bind(x -> { + handlerWithoutReturn.accept(component, x); + }); + } + } + } +} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/InputWireLabel.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/InputWireLabel.java new file mode 100644 index 000000000000..808b852719fa --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/InputWireLabel.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.common.wiring.component; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotates a method parameter to indicate that the parameter is an input wire label. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface InputWireLabel { + + /** + * The label of the input wire. + * + * @return the label of the input wire + */ + @NonNull + String value(); +} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/internal/WireBindInfo.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/internal/WireBindInfo.java new file mode 100644 index 000000000000..c7b46fc693a3 --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/internal/WireBindInfo.java @@ -0,0 +1,33 @@ +/* + * 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.common.wiring.component.internal; + +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; + +/** + * Contains information necessary to bind an input wire when we eventually get the implementation of the component. + * + * @param handlerWithReturn null if initially bound. If not initially bound, will be non-null if the method has a + * non-void return type. + * @param handlerWithoutReturn null if initially bound. If not initially bound, will be non-null if the method has a + * void return type + */ +public record WireBindInfo( + @Nullable BiFunction handlerWithReturn, + @Nullable BiConsumer handlerWithoutReturn) {} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/internal/WiringComponentProxy.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/internal/WiringComponentProxy.java new file mode 100644 index 000000000000..66555ac01814 --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/internal/WiringComponentProxy.java @@ -0,0 +1,54 @@ +/* + * 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.common.wiring.component.internal; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.util.Objects; + +/** + * This dynamic proxy is used by the {@link com.swirlds.common.wiring.component.ComponentWiring} to capture the most + * recently invoked method. + */ +public class WiringComponentProxy implements InvocationHandler { + + private Method mostRecentlyInvokedMethod = null; + + /** + * {@inheritDoc} + */ + @Override + public Object invoke(@NonNull final Object proxy, @NonNull final Method method, @NonNull final Object[] args) + throws Throwable { + mostRecentlyInvokedMethod = Objects.requireNonNull(method); + return null; + } + + /** + * Get the most recently invoked method. + * + * @return the most recently invoked method + */ + @NonNull + public Method getMostRecentlyInvokedMethod() { + if (mostRecentlyInvokedMethod == null) { + throw new IllegalStateException("No method has been invoked yet"); + } + return mostRecentlyInvokedMethod; + } +} diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/component/WiringComponentPerformanceTests.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/component/WiringComponentPerformanceTests.java new file mode 100644 index 000000000000..014d2cadabf8 --- /dev/null +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/component/WiringComponentPerformanceTests.java @@ -0,0 +1,131 @@ +/* + * 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.common.wiring.component; + +import com.swirlds.base.time.Time; +import com.swirlds.common.test.fixtures.platform.TestPlatformContextBuilder; +import com.swirlds.common.wiring.model.WiringModel; +import com.swirlds.common.wiring.schedulers.TaskScheduler; +import com.swirlds.common.wiring.schedulers.builders.TaskSchedulerType; +import com.swirlds.common.wiring.wires.input.BindableInputWire; +import com.swirlds.common.wiring.wires.input.InputWire; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.ForkJoinPool; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +@Disabled // Do not merge with this class enabled +class WiringComponentPerformanceTests { + + private interface SimpleComponent { + void handleInput(@NonNull Long input); + } + + private static class SimpleComponentImpl implements SimpleComponent { + private long runningValue = 0; + + @Override + public void handleInput(@NonNull final Long input) { + runningValue += input; + } + + public long getRunningValue() { + return runningValue; + } + } + + @NonNull + private InputWire buildOldStyleComponent(@NonNull final SimpleComponent component) { + final WiringModel model = WiringModel.create( + TestPlatformContextBuilder.create().build(), Time.getCurrent(), ForkJoinPool.commonPool()); + + final TaskScheduler scheduler = model.schedulerBuilder("test") + .withType(TaskSchedulerType.DIRECT) + .build(); + + final BindableInputWire inputWire = scheduler.buildInputWire("input"); + inputWire.bind(component::handleInput); + + return inputWire; + } + + @NonNull + private InputWire buildAutomaticComponent(@NonNull final SimpleComponent component) { + + final WiringModel model = WiringModel.create( + TestPlatformContextBuilder.create().build(), Time.getCurrent(), ForkJoinPool.commonPool()); + + final TaskScheduler scheduler = model.schedulerBuilder("test") + .withType(TaskSchedulerType.DIRECT) + .build() + .cast(); + + final ComponentWiring componentWiring = + new ComponentWiring<>(SimpleComponent.class, scheduler); + final InputWire inputWire = componentWiring.getInputWire(SimpleComponent::handleInput); + componentWiring.bind(component); + + return inputWire; + } + + // When testing locally on my macbook (m1), the old style component took 0.76s to run 100,000,000 iterations, + // and the automatic component took 0.79s to run 100,000,000 iterations. + + @Test + void oldStylePerformanceTest() { + final long iterations = 100_000_000; + + final SimpleComponentImpl component = new SimpleComponentImpl(); + final InputWire inputWire = buildOldStyleComponent(component); + + final Instant start = Instant.now(); + + for (long i = 0; i < iterations; i++) { + inputWire.put(i); + } + + final Instant end = Instant.now(); + final Duration duration = Duration.between(start, end); + System.out.println("Time required: " + duration.toMillis() + "ms"); + + // Just in case the compiler wants to get cheeky and avoid doing computation + System.out.println("value = " + component.getRunningValue()); + } + + @Test + void automaticComponentPerformanceTest() { + final long iterations = 100_000_000; + + final SimpleComponentImpl component = new SimpleComponentImpl(); + final InputWire inputWire = buildAutomaticComponent(component); + + final Instant start = Instant.now(); + + for (long i = 0; i < iterations; i++) { + inputWire.put(i); + } + + final Instant end = Instant.now(); + final Duration duration = Duration.between(start, end); + System.out.println("Time required: " + duration.toMillis() + "ms"); + + // Just in case the compiler wants to get cheeky and avoid doing computation + System.out.println("value = " + component.getRunningValue()); + } +} diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/component/WiringComponentTests.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/component/WiringComponentTests.java new file mode 100644 index 000000000000..28c45bf821d1 --- /dev/null +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/component/WiringComponentTests.java @@ -0,0 +1,147 @@ +/* + * 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.common.wiring.component; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +import com.swirlds.common.context.PlatformContext; +import com.swirlds.common.test.fixtures.platform.TestPlatformContextBuilder; +import com.swirlds.common.wiring.model.WiringModel; +import com.swirlds.common.wiring.schedulers.TaskScheduler; +import com.swirlds.common.wiring.schedulers.builders.TaskSchedulerType; +import com.swirlds.common.wiring.wires.input.InputWire; +import com.swirlds.common.wiring.wires.output.OutputWire; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +public class WiringComponentTests { + + private interface FooBarBaz { + Long handleFoo(@NonNull Integer foo); + + @InputWireLabel("bar") + Long handleBar(@NonNull Boolean bar); + + void handleBaz(@NonNull String baz); + } + + private static class FooBarBazImpl implements FooBarBaz { + private long runningValue = 0; + + @Override + public Long handleFoo(@NonNull final Integer foo) { + runningValue += foo; + return runningValue; + } + + @Override + public Long handleBar(@NonNull final Boolean bar) { + runningValue *= bar ? 1 : -1; + return runningValue; + } + + @Override + public void handleBaz(@NonNull final String baz) { + runningValue *= baz.hashCode(); + } + + public long getRunningValue() { + return runningValue; + } + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 3}) + void simpleComponentTest(final int bindLocation) { + final PlatformContext platformContext = + TestPlatformContextBuilder.create().build(); + + final WiringModel wiringModel = + WiringModel.create(platformContext, platformContext.getTime(), ForkJoinPool.commonPool()); + + final TaskScheduler scheduler = wiringModel + .schedulerBuilder("test") + .withType(TaskSchedulerType.DIRECT) + .build() + .cast(); + + final ComponentWiring fooBarBazWiring = new ComponentWiring<>(FooBarBaz.class, scheduler); + + final FooBarBazImpl fooBarBazImpl = new FooBarBazImpl(); + + if (bindLocation == 0) { + fooBarBazWiring.bind(fooBarBazImpl); + } + + final InputWire fooInput = fooBarBazWiring.getInputWire(FooBarBaz::handleFoo); + assertEquals("handleFoo", fooInput.getName()); + final InputWire barInput = fooBarBazWiring.getInputWire(FooBarBaz::handleBar); + assertEquals("bar", barInput.getName()); + + if (bindLocation == 1) { + fooBarBazWiring.bind(fooBarBazImpl); + } + + final InputWire bazInput = fooBarBazWiring.getInputWire(FooBarBaz::handleBaz); + assertEquals("handleBaz", bazInput.getName()); + final OutputWire output = fooBarBazWiring.getOutputWire(); + + if (bindLocation == 2) { + fooBarBazWiring.bind(fooBarBazImpl); + } + + final AtomicLong outputValue = new AtomicLong(); + output.solderTo("outputHandler", "output", outputValue::set); + + // Getting the same input wire multiple times should yield the same instance + assertSame(fooInput, fooBarBazWiring.getInputWire(FooBarBaz::handleFoo)); + assertSame(barInput, fooBarBazWiring.getInputWire(FooBarBaz::handleBar)); + assertSame(bazInput, fooBarBazWiring.getInputWire(FooBarBaz::handleBaz)); + + // Getting the output wire multiple times should yield the same instance + assertSame(output, fooBarBazWiring.getOutputWire()); + + if (bindLocation == 3) { + fooBarBazWiring.bind(fooBarBazImpl); + } + + long expectedRunningValue = 0; + for (int i = 0; i < 1000; i++) { + if (i % 3 == 0) { + expectedRunningValue += i; + fooInput.put(i); + assertEquals(expectedRunningValue, fooBarBazImpl.getRunningValue()); + assertEquals(expectedRunningValue, outputValue.get()); + } else if (i % 3 == 1) { + final boolean choice = i % 7 == 0; + expectedRunningValue *= choice ? 1 : -1; + barInput.put(choice); + assertEquals(expectedRunningValue, fooBarBazImpl.getRunningValue()); + assertEquals(expectedRunningValue, outputValue.get()); + } else { + final String value = "value" + i; + expectedRunningValue *= value.hashCode(); + bazInput.put(value); + assertEquals(expectedRunningValue, fooBarBazImpl.getRunningValue()); + } + } + } +} From 1cac73e2d90498fa0ad0871b26e800e4a0dbbaec Mon Sep 17 00:00:00 2001 From: Cody Littley <56973212+cody-littley@users.noreply.github.com> Date: Wed, 6 Mar 2024 08:27:40 -0600 Subject: [PATCH 023/115] chore: serializable list tweaks (#11825) Signed-off-by: Cody Littley --- .../cli/signedstate/SignedStateHolder.java | 3 + .../DebuggableMerkleDataInputStream.java | 69 +-- .../streams/SerializableDataInputStream.java | 404 +++++++++++------- .../internal/SerializationOperation.java | 3 +- .../merkle/utility/SerializableLong.java | 2 +- .../threading/StoppableThreadTests.java | 1 + .../com/swirlds/platform/PlatformBuilder.java | 3 + .../handshake/VersionCompareHandshake.java | 3 +- .../InboundConnectionHandler.java | 3 +- .../OutboundConnectionCreator.java | 3 +- .../recovery/EventRecoveryWorkflow.java | 3 + .../system/StaticSoftwareVersion.java | 67 +++ .../system/events/BaseEventHashedData.java | 19 +- .../StateSignatureTransaction.java | 2 +- .../system/transaction/SwirldTransaction.java | 2 +- .../platform/SerializableStreamTests.java | 147 +++++++ .../event/DetailedConsensusEventTest.java | 12 +- .../EventStreamMultiFileIteratorTest.java | 9 + .../recovery/EventStreamPathIteratorTest.java | 14 + .../EventStreamRoundIteratorTest.java | 14 + .../EventStreamSingleFileIteratorTest.java | 14 + .../recovery/ObjectStreamIteratorTest.java | 14 + .../util/EventStreamSigningUtilsTests.java | 10 + .../platform/test/SerializationTests.java | 9 + .../cli/EventStreamReportingToolTest.java | 14 + .../cli/EventStreamSingleFileRepairTest.java | 14 + .../TransactionHandlingTestUtils.java | 31 -- .../platform/test/event/GossipEventTest.java | 9 + .../EventCreationExpectedResults.java | 102 ----- .../EventCreationSimulationParams.java | 48 --- .../EventCreationSimulationResults.java | 88 ---- .../preconsensus/PcesReadWriteTests.java | 9 + .../event/preconsensus/PcesWriterTests.java | 9 + .../handshake/VersionHandshakeTests.java | 10 +- .../swirlds/platform/test/sync/SyncTests.java | 13 + 35 files changed, 713 insertions(+), 464 deletions(-) create mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/StaticSoftwareVersion.java delete mode 100644 platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/creation/EventCreationExpectedResults.java delete mode 100644 platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/creation/EventCreationSimulationParams.java delete mode 100644 platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/creation/EventCreationSimulationResults.java diff --git a/hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/SignedStateHolder.java b/hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/SignedStateHolder.java index 05188838b408..34a559793399 100644 --- a/hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/SignedStateHolder.java +++ b/hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/SignedStateHolder.java @@ -46,6 +46,7 @@ import com.swirlds.config.extensions.sources.LegacyFileConfigSource; import com.swirlds.platform.state.signed.ReservedSignedState; import com.swirlds.platform.state.signed.SignedStateFileReader; +import com.swirlds.platform.system.StaticSoftwareVersion; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.io.IOException; @@ -327,6 +328,8 @@ private Pair dehydrate(@NonNull final List

    T recordStringRepresentation(final T value) { @@ -648,7 +648,7 @@ public T readSerializable() throws IOException { */ @Override public T readSerializable( - final boolean readClassId, final Supplier serializableConstructor) throws IOException { + final boolean readClassId, @NonNull final Supplier serializableConstructor) throws IOException { startOperation(SerializationOperation.READ_SERIALIZABLE); try { return super.readSerializable(readClassId, serializableConstructor); @@ -662,10 +662,11 @@ public T readSerializable( */ @Override public void readSerializableIterableWithSize( - final int maxSize, final Consumer callback) throws IOException { + final int maxSize, @NonNull final Consumer callback, @Nullable final Set permissibleClassIds) + throws IOException { startOperation(SerializationOperation.READ_SERIALIZABLE_LIST); try { - super.readSerializableIterableWithSize(maxSize, callback); + super.readSerializableIterableWithSize(maxSize, callback, permissibleClassIds); } finally { finishOperation(); } @@ -678,12 +679,14 @@ public void readSerializableIterableWithSize( public void readSerializableIterableWithSize( final int size, final boolean readClassId, - final Supplier serializableConstructor, - final Consumer callback) + @NonNull final Supplier serializableConstructor, + @NonNull final Consumer callback, + @Nullable final Set permissibleClassIds) throws IOException { startOperation(SerializationOperation.READ_SERIALIZABLE_LIST); try { - super.readSerializableIterableWithSize(size, readClassId, serializableConstructor, callback); + super.readSerializableIterableWithSize( + size, readClassId, serializableConstructor, callback, permissibleClassIds); } finally { finishOperation(); } @@ -696,14 +699,15 @@ public void readSerializableIterableWithSize( protected T readNextSerializableIteration( final boolean allSameClass, final boolean readClassId, - final ValueReference classId, - final ValueReference version, - final CheckedFunction serializableConstructor) + @NonNull final ValueReference classId, + @NonNull final ValueReference version, + @NonNull final CheckedFunction serializableConstructor, + @Nullable final Set permissibleClassIds) throws IOException { startOperation(SerializationOperation.READ_SERIALIZABLE); try { return super.readNextSerializableIteration( - allSameClass, readClassId, classId, version, serializableConstructor); + allSameClass, readClassId, classId, version, serializableConstructor, permissibleClassIds); } finally { finishOperation(); } @@ -713,10 +717,11 @@ protected T readNextSerializableIteration( * {@inheritDoc} */ @Override - public List readSerializableList(final int maxListSize) throws IOException { + public List readSerializableList( + final int maxListSize, @Nullable final Set permissibleClassIds) throws IOException { startOperation(SerializationOperation.READ_SERIALIZABLE_LIST); try { - return super.readSerializableList(maxListSize); + return super.readSerializableList(maxListSize, permissibleClassIds); } finally { finishOperation(); } @@ -727,12 +732,15 @@ public List readSerializableList(final int maxLi */ @Override public List readSerializableList( - final int maxListSize, final boolean readClassId, final Supplier serializableConstructor) + final int maxListSize, + final boolean readClassId, + @NonNull final Supplier serializableConstructor, + @Nullable final Set permissibleClassIds) throws IOException { startOperation(SerializationOperation.READ_SERIALIZABLE_LIST); try { - return super.readSerializableList(maxListSize, readClassId, serializableConstructor); + return super.readSerializableList(maxListSize, readClassId, serializableConstructor, permissibleClassIds); } finally { finishOperation(); } @@ -743,12 +751,15 @@ public List readSerializableList( */ @Override public T[] readSerializableArray( - final IntFunction arrayConstructor, final int maxListSize, final boolean readClassId) + @NonNull final IntFunction arrayConstructor, + final int maxListSize, + final boolean readClassId, + @Nullable final Set permissibleClassIds) throws IOException { startOperation(SerializationOperation.READ_SERIALIZABLE_LIST); try { - return super.readSerializableArray(arrayConstructor, maxListSize, readClassId); + return super.readSerializableArray(arrayConstructor, maxListSize, readClassId, permissibleClassIds); } finally { finishOperation(); } @@ -759,15 +770,17 @@ public T[] readSerializableArray( */ @Override public T[] readSerializableArray( - final IntFunction arrayConstructor, + @NonNull final IntFunction arrayConstructor, final int maxListSize, final boolean readClassId, - final Supplier serializableConstructor) + @NonNull final Supplier serializableConstructor, + @Nullable final Set permissibleClassIds) throws IOException { startOperation(SerializationOperation.READ_SERIALIZABLE_LIST); try { - return super.readSerializableArray(arrayConstructor, maxListSize, readClassId, serializableConstructor); + return super.readSerializableArray( + arrayConstructor, maxListSize, readClassId, serializableConstructor, permissibleClassIds); } finally { finishOperation(); } diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/io/streams/SerializableDataInputStream.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/io/streams/SerializableDataInputStream.java index e7e89aac68e2..597844eea463 100644 --- a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/io/streams/SerializableDataInputStream.java +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/io/streams/SerializableDataInputStream.java @@ -29,19 +29,21 @@ import com.swirlds.common.io.exceptions.InvalidVersionException; import com.swirlds.common.utility.ValueReference; import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.function.Consumer; import java.util.function.IntFunction; import java.util.function.Supplier; /** - * A drop-in replacement for {@link DataInputStream}, which handles SerializableDet classes specially. - * It is designed for use with the SerializableDet interface, and its use is described there. + * A drop-in replacement for {@link DataInputStream}, which handles SerializableDet classes specially. It is designed + * for use with the SerializableDet interface, and its use is described there. */ public class SerializableDataInputStream extends AugmentedDataInputStream { @@ -50,8 +52,7 @@ public class SerializableDataInputStream extends AugmentedDataInputStream { /** * Creates a stream capable of deserializing serializable objects. * - * @param in - * the specified input stream + * @param in the specified input stream */ public SerializableDataInputStream(final InputStream in) { super(in); @@ -61,8 +62,7 @@ public SerializableDataInputStream(final InputStream in) { * Reads the protocol version written by {@link SerializableDataOutputStream#writeProtocolVersion()} and saves it * internally. From this point on, it will use this version number to deserialize. * - * @throws IOException - * thrown if any IO problems occur + * @throws IOException thrown if any IO problems occur */ public void readProtocolVersion() throws IOException { final int protocolVersion = readInt(); @@ -75,37 +75,67 @@ public void readProtocolVersion() throws IOException { * Reads a {@link SerializableDet} from a stream and returns it. The instance will be created using the * {@link ConstructableRegistry}. The instance must have previously been written using * {@link SerializableDataOutputStream#writeSerializable(SelfSerializable, boolean)} (SerializableDet, boolean)} - * with {@code writeClassId} set to true, otherwise we - * cannot know what the class written is. + * with {@code writeClassId} set to true, otherwise we cannot know what the class written is. * - * @param - * the implementation of {@link SelfSerializable} used + * @param permissibleClassIds a set of class IDs that are allowed to be read, will throw an IOException if asked to + * deserialize a class not in this set, all class IDs are permitted if null + * @param the implementation of {@link SelfSerializable} used * @return An instance of the class previously written - * @throws IOException - * thrown if any IO problems occur + * @throws IOException thrown if any IO problems occur + */ + public T readSerializable(@Nullable final Set permissibleClassIds) + throws IOException { + return readSerializable(true, SerializableDataInputStream::registryConstructor, permissibleClassIds); + } + + /** + * Reads a {@link SerializableDet} from a stream and returns it. The instance will be created using the + * {@link ConstructableRegistry}. The instance must have previously been written using + * {@link SerializableDataOutputStream#writeSerializable(SelfSerializable, boolean)} (SerializableDet, boolean)} + * with {@code writeClassId} set to true, otherwise we cannot know what the class written is. + * + * @param the implementation of {@link SelfSerializable} used + * @return An instance of the class previously written + * @throws IOException thrown if any IO problems occur */ public T readSerializable() throws IOException { - return readSerializable(true, SerializableDataInputStream::registryConstructor); + return readSerializable(null); } /** * Uses the provided {@code serializable} to read its data from the stream. * - * @param serializableConstructor - * a constructor for the instance written in the stream - * @param readClassId - * set to true if the class ID was written to the stream - * @param - * the implementation of {@link SelfSerializable} used + * @param serializableConstructor a constructor for the instance written in the stream + * @param readClassId set to true if the class ID was written to the stream + * @param permissibleClassIds a set of class IDs that are allowed to be read, will throw an IOException if asked + * to deserialize a class not in this set, all class IDs are permitted if null. + * Ignored if readClassId is false. + * @param the implementation of {@link SelfSerializable} used * @return the same object that was passed in, returned for convenience - * @throws IOException - * thrown if any IO problems occur + * @throws IOException thrown if any IO problems occur */ public T readSerializable( - final boolean readClassId, @NonNull final Supplier serializableConstructor) throws IOException { + final boolean readClassId, + @NonNull final Supplier serializableConstructor, + @Nullable final Set permissibleClassIds) + throws IOException { Objects.requireNonNull(serializableConstructor, "serializableConstructor must not be null"); - return readSerializable(readClassId, id -> serializableConstructor.get()); + return readSerializable(readClassId, id -> serializableConstructor.get(), permissibleClassIds); + } + + /** + * Uses the provided {@code serializable} to read its data from the stream. + * + * @param serializableConstructor a constructor for the instance written in the stream + * @param readClassId set to true if the class ID was written to the stream + * @param the implementation of {@link SelfSerializable} used + * @return the same object that was passed in, returned for convenience + * @throws IOException thrown if any IO problems occur + */ + public T readSerializable( + final boolean readClassId, @NonNull final Supplier serializableConstructor) throws IOException { + return readSerializable(readClassId, serializableConstructor, null); } /** @@ -120,8 +150,7 @@ protected void validateVersion(final SerializableDet object, final int version) /** * Called when the class ID of an object becomes known. This method is a hook for the debug stream. * - * @param classId - * the class ID of the current object being deserialized + * @param classId the class ID of the current object being deserialized */ protected void recordClassId(final long classId) { // debug framework can override @@ -130,8 +159,7 @@ protected void recordClassId(final long classId) { /** * Called when the class ID of an object becomes known. This method is a hook for the debug stream. * - * @param o - * the object that is being deserialized + * @param o the object that is being deserialized */ protected void recordClass(final Object o) { // debug framework can override @@ -141,12 +169,19 @@ protected void recordClass(final Object o) { * Same as {@link #readSerializable(boolean, Supplier)} except that the constructor takes a class ID */ private T readSerializable( - final boolean readClassId, final CheckedFunction serializableConstructor) + final boolean readClassId, + @NonNull final CheckedFunction serializableConstructor, + @Nullable final Set permissibleClassIds) throws IOException { final Long classId; if (readClassId) { classId = readLong(); + if (permissibleClassIds != null && !permissibleClassIds.contains(classId)) { + throw new IOException( + "Class ID " + classId + " is not in the set of permissible class IDs: " + permissibleClassIds); + } + recordClassId(classId); if (classId == NULL_CLASS_ID) { return null; @@ -171,66 +206,96 @@ private T readSerializable( /** * Read a sequence of serializable objects and pass them to a callback method. * - * @param maxSize - * the maximum allowed size - * @param callback - * this method is passed each object in the sequence - * @param - * the type of the objects in the sequence + * @param maxSize the maximum allowed size + * @param callback this method is passed each object in the sequence + * @param permissibleClassIds a set of class IDs that are allowed to be read, will throw an IOException if asked to + * deserialize a class not in this set, all class IDs are permitted if null + * @param the type of the objects in the sequence */ public void readSerializableIterableWithSize( - final int maxSize, final Consumer callback) throws IOException { + final int maxSize, @NonNull final Consumer callback, @Nullable final Set permissibleClassIds) + throws IOException { final int size = readInt(); checkLengthLimit(size, maxSize); readSerializableIterableWithSizeInternal( - size, true, SerializableDataInputStream::registryConstructor, callback); + size, true, SerializableDataInputStream::registryConstructor, callback, permissibleClassIds); + } + + /** + * Read a sequence of serializable objects and pass them to a callback method. + * + * @param maxSize the maximum allowed size + * @param callback this method is passed each object in the sequence + * @param the type of the objects in the sequence + */ + public void readSerializableIterableWithSize( + final int maxSize, @NonNull final Consumer callback) throws IOException { + + readSerializableIterableWithSize(maxSize, callback, null); } /** * Read a sequence of serializable objects and pass them to a callback method. * - * @param maxSize - * the maximum number of objects to read - * @param readClassId - * if true then the class ID needs to be read - * @param serializableConstructor - * a method that takes a class ID and provides a constructor - * @param callback - * the callback method where each object is passed when it is deserialized - * @param - * the type of the objects being deserialized + * @param maxSize the maximum number of objects to read + * @param readClassId if true then the class ID needs to be read + * @param serializableConstructor a method that takes a class ID and provides a constructor + * @param callback the callback method where each object is passed when it is deserialized + * @param permissibleClassIds a set of class IDs that are allowed to be read, will throw an IOException if asked + * to deserialize a class not in this set, all class IDs are permitted if null. + * Ignored if readClassId is false. + * @param the type of the objects being deserialized */ public void readSerializableIterableWithSize( final int maxSize, final boolean readClassId, - final Supplier serializableConstructor, - final Consumer callback) + @NonNull final Supplier serializableConstructor, + @NonNull final Consumer callback, + @Nullable final Set permissibleClassIds) throws IOException { final int size = readInt(); checkLengthLimit(size, maxSize); - readSerializableIterableWithSizeInternal(size, readClassId, id -> serializableConstructor.get(), callback); + readSerializableIterableWithSizeInternal( + size, readClassId, id -> serializableConstructor.get(), callback, permissibleClassIds); } /** * Read a sequence of serializable objects and pass them to a callback method. * - * @param size - * the number of objects to read - * @param readClassId - * if true then the class ID needs to be read - * @param serializableConstructor - * a method that takes a class ID and provides a constructor - * @param callback - * the callback method where each object is passed when it is deserialized - * @param - * the type of the objects being deserialized + * @param maxSize the maximum number of objects to read + * @param readClassId if true then the class ID needs to be read + * @param serializableConstructor a method that takes a class ID and provides a constructor + * @param callback the callback method where each object is passed when it is deserialized + * @param the type of the objects being deserialized + */ + public void readSerializableIterableWithSize( + final int maxSize, + final boolean readClassId, + @NonNull final Supplier serializableConstructor, + @NonNull final Consumer callback) + throws IOException { + readSerializableIterableWithSize(maxSize, readClassId, serializableConstructor, callback, null); + } + + /** + * Read a sequence of serializable objects and pass them to a callback method. + * + * @param size the number of objects to read + * @param readClassId if true then the class ID needs to be read + * @param serializableConstructor a method that takes a class ID and provides a constructor + * @param callback the callback method where each object is passed when it is deserialized + * @param permissibleClassIds a set of class IDs that are allowed to be read, will throw an IOException if asked + * to deserialize a class not in this set, all class IDs are permitted if null. + * Ignored if readClassId is false. + * @param the type of the objects being deserialized */ private void readSerializableIterableWithSizeInternal( final int size, final boolean readClassId, - final CheckedFunction serializableConstructor, - final Consumer callback) + @NonNull final CheckedFunction serializableConstructor, + @NonNull final Consumer callback, + @Nullable final Set permissibleClassIds) throws IOException { if (serializableConstructor == null) { @@ -248,41 +313,39 @@ private void readSerializableIterableWithSizeIntern final ValueReference version = new ValueReference<>(); for (int i = 0; i < size; i++) { - final T next = - readNextSerializableIteration(allSameClass, readClassId, classId, version, serializableConstructor); + final T next = readNextSerializableIteration( + allSameClass, readClassId, classId, version, serializableConstructor, permissibleClassIds); callback.accept(next); } } /** - * Helper method for {@link #readSerializableIterableWithSize(int, Consumer)}. Protected instead of - * private to allow debug framework to intercept this method. + * Helper method for {@link #readSerializableIterableWithSizeInternal(int, boolean, CheckedFunction, Consumer, Set)} + * . Protected instead of private to allow debug framework to intercept this method. * - * @param allSameClass - * true if the elements all have the same class - * @param readClassId - * if true then the class ID needs to be read, ignored if allSameClass is true - * @param classId - * the class ID if known, otherwise null - * @param version - * the version if known, otherwise ignored - * @param serializableConstructor - * given a class ID, returns a constructor for that class - * @param - * the type of the elements in the sequence + * @param allSameClass true if the elements all have the same class + * @param readClassId if true then the class ID needs to be read, ignored if allSameClass is true + * @param classId the class ID if known, otherwise null + * @param version the version if known, otherwise ignored + * @param serializableConstructor given a class ID, returns a constructor for that class + * @param permissibleClassIds a set of class IDs that are allowed to be read, will throw an IOException if asked + * to deserialize a class not in this set, all class IDs are permitted if null. + * Ignored if readClassId is false. + * @param the type of the elements in the sequence * @return true if the class ID has already been read */ protected T readNextSerializableIteration( final boolean allSameClass, final boolean readClassId, - final ValueReference classId, - final ValueReference version, - final CheckedFunction serializableConstructor) + @NonNull final ValueReference classId, + @NonNull final ValueReference version, + @NonNull final CheckedFunction serializableConstructor, + @Nullable final Set permissibleClassIds) throws IOException { if (!allSameClass) { // if classes are different, we just read each object one by one - return readSerializable(readClassId, serializableConstructor); + return readSerializable(readClassId, serializableConstructor, permissibleClassIds); } final boolean isNull = readBoolean(); @@ -294,6 +357,10 @@ protected T readNextSerializableIteration( // this is the first non-null member, so we read the ID and version if (readClassId) { classId.setValue(readLong()); + if (permissibleClassIds != null && !permissibleClassIds.contains(classId.getValue())) { + throw new IOException("Class ID " + classId + " is not in the set of permissible class IDs: " + + permissibleClassIds); + } } version.setValue(readInt()); } @@ -308,59 +375,88 @@ protected T readNextSerializableIteration( /** * Read a list of serializable objects from the stream * - * @param maxListSize - * maximal number of object to read - * @param - * the implementation of {@link SelfSerializable} used + * @param maxListSize maximal number of object to read + * @param permissibleClassIds a set of class IDs that are allowed to be read, will throw an IOException if asked to + * deserialize a class not in this set, all class IDs are permitted if null + * @param the implementation of {@link SelfSerializable} used + * @return A list of the instances of the class previously written + * @throws IOException thrown if any IO problems occur + */ + public List readSerializableList( + final int maxListSize, @Nullable final Set permissibleClassIds) throws IOException { + return readSerializableList( + maxListSize, true, SerializableDataInputStream::registryConstructor, permissibleClassIds); + } + + /** + * Read a list of serializable objects from the stream + * + * @param maxListSize maximal number of object to read + * @param the implementation of {@link SelfSerializable} used * @return A list of the instances of the class previously written - * @throws IOException - * thrown if any IO problems occur + * @throws IOException thrown if any IO problems occur */ public List readSerializableList(final int maxListSize) throws IOException { - return readSerializableList(maxListSize, true, SerializableDataInputStream::registryConstructor); + return readSerializableList(maxListSize, null); } /** * Read a list of serializable objects from the stream * - * @param maxListSize - * maximal number of object to read - * @param readClassId - * set to true if the class ID was written to the stream - * @param serializableConstructor - * the constructor to use when instantiating list elements - * @param - * the implementation of {@link SelfSerializable} used + * @param maxListSize maximal number of object to read + * @param readClassId set to true if the class ID was written to the stream + * @param serializableConstructor the constructor to use when instantiating list elements + * @param permissibleClassIds a set of class IDs that are allowed to be read, will throw an IOException if asked + * to deserialize a class not in this set, all class IDs are permitted if null. + * Ignored if readClassId is false. + * @param the implementation of {@link SelfSerializable} used * @return A list of the instances of the class previously written - * @throws IOException - * thrown if any IO problems occur + * @throws IOException thrown if any IO problems occur */ public List readSerializableList( - final int maxListSize, final boolean readClassId, @NonNull final Supplier serializableConstructor) + final int maxListSize, + final boolean readClassId, + @NonNull final Supplier serializableConstructor, + @Nullable final Set permissibleClassIds) throws IOException { Objects.requireNonNull(serializableConstructor, "serializableConstructor must not be null"); - return readSerializableList(maxListSize, readClassId, id -> serializableConstructor.get()); + return readSerializableList(maxListSize, readClassId, id -> serializableConstructor.get(), permissibleClassIds); + } + + /** + * Read a list of serializable objects from the stream + * + * @param maxListSize maximal number of object to read + * @param readClassId set to true if the class ID was written to the stream + * @param serializableConstructor the constructor to use when instantiating list elements + * @param the implementation of {@link SelfSerializable} used + * @return A list of the instances of the class previously written + * @throws IOException thrown if any IO problems occur + */ + public List readSerializableList( + final int maxListSize, final boolean readClassId, @NonNull final Supplier serializableConstructor) + throws IOException { + return readSerializableList(maxListSize, readClassId, serializableConstructor, null); } /** * Read a list of serializable objects from the stream * - * @param maxListSize - * maximal number of object to read - * @param readClassId - * set to true if the class ID was written to the stream - * @param serializableConstructor - * a method that takes a class ID and returns a constructor - * @param - * the implementation of {@link SelfSerializable} used + * @param maxListSize maximal number of object to read + * @param readClassId set to true if the class ID was written to the stream + * @param serializableConstructor a method that takes a class ID and returns a constructor + * @param permissibleClassIds a set of class IDs that are allowed to be read, will throw an IOException if asked + * to deserialize a class not in this set, all class IDs are permitted if null. + * Ignored if readClassId is false. + * @param the implementation of {@link SelfSerializable} used * @return A list of the instances of the class previously written - * @throws IOException - * thrown if any IO problems occur + * @throws IOException thrown if any IO problems occur */ private List readSerializableList( final int maxListSize, final boolean readClassId, - final CheckedFunction serializableConstructor) + @NonNull final CheckedFunction serializableConstructor, + @Nullable final Set permissibleClassIds) throws IOException { final int length = readInt(); @@ -374,31 +470,33 @@ private List readSerializableList( if (length == 0) { return list; } - readSerializableIterableWithSizeInternal(length, readClassId, serializableConstructor, list::add); + readSerializableIterableWithSizeInternal( + length, readClassId, serializableConstructor, list::add, permissibleClassIds); return list; } /** * Read an array of serializable objects from the stream. * - * @param arrayConstructor - * a method that returns an array of the requested size - * @param maxListSize - * maximal number of object should read - * @param readClassId - * set to true if the class ID was written to the stream - * @param - * the implementation of {@link SelfSerializable} used + * @param arrayConstructor a method that returns an array of the requested size + * @param maxListSize maximal number of object should read + * @param readClassId set to true if the class ID was written to the stream + * @param permissibleClassIds a set of class IDs that are allowed to be read, will throw an IOException if asked to + * deserialize a class not in this set, all class IDs are permitted if null. Ignored if + * readClassId is false. + * @param the implementation of {@link SelfSerializable} used * @return An array of the instances of the class previously written - * @throws IOException - * thrown if any IO problems occur + * @throws IOException thrown if any IO problems occur */ public T[] readSerializableArray( - final IntFunction arrayConstructor, final int maxListSize, final boolean readClassId) + @NonNull final IntFunction arrayConstructor, + final int maxListSize, + final boolean readClassId, + @Nullable final Set permissibleClassIds) throws IOException { - final List list = - readSerializableList(maxListSize, readClassId, SerializableDataInputStream::registryConstructor); + final List list = readSerializableList( + maxListSize, readClassId, SerializableDataInputStream::registryConstructor, permissibleClassIds); if (list == null) { return null; } @@ -409,41 +507,59 @@ public T[] readSerializableArray( /** * Read an array of serializable objects from the stream. * - * @param arrayConstructor - * a method that returns an array of the requested size - * @param maxListSize - * maximal number of object should read - * @param readClassId - * set to true if the class ID was written to the stream - * @param serializableConstructor - * an object that returns new instances of the class - * @param - * the implementation of {@link SelfSerializable} used + * @param arrayConstructor a method that returns an array of the requested size + * @param maxListSize maximal number of object should read + * @param readClassId set to true if the class ID was written to the stream + * @param permissibleClassIds a set of class IDs that are allowed to be read, will throw an IOException if asked + * to deserialize a class not in this, all class IDs are permitted if null. Ignored + * if readClassId is false. + * @param serializableConstructor an object that returns new instances of the class + * @param the implementation of {@link SelfSerializable} used * @return An array of the instances of the class previously written - * @throws IOException - * thrown if any IO problems occur + * @throws IOException thrown if any IO problems occur */ public T[] readSerializableArray( - final IntFunction arrayConstructor, + @NonNull final IntFunction arrayConstructor, final int maxListSize, final boolean readClassId, - final Supplier serializableConstructor) + @NonNull final Supplier serializableConstructor, + @Nullable final Set permissibleClassIds) throws IOException { - final List list = readSerializableList(maxListSize, readClassId, id -> serializableConstructor.get()); + final List list = readSerializableList( + maxListSize, readClassId, id -> serializableConstructor.get(), permissibleClassIds); if (list == null) { return null; } return list.toArray(arrayConstructor.apply(list.size())); } + /** + * Read an array of serializable objects from the stream. + * + * @param arrayConstructor a method that returns an array of the requested size + * @param maxListSize maximal number of object we are willing to read + * @param readClassId set to true if the class ID was written to the stream + * @param serializableConstructor an object that returns new instances of the class + * @param the implementation of {@link SelfSerializable} used + * @return An array of the instances of the class previously written + * @throws IOException thrown if any IO problems occur + */ + public T[] readSerializableArray( + @NonNull final IntFunction arrayConstructor, + final int maxListSize, + final boolean readClassId, + @NonNull final Supplier serializableConstructor) + throws IOException { + + return readSerializableArray(arrayConstructor, maxListSize, readClassId, serializableConstructor, null); + } + /** * Looks up a constructor given a class ID. * - * @param classId - * a requested class ID - * @param - * the type of the class + * @param classId a requested class ID + * @param the type of the class * @return a constructor for the class * @throws ClassNotFoundException if the class ID is not registered */ diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/io/streams/internal/SerializationOperation.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/io/streams/internal/SerializationOperation.java index 6f818a03492f..bf09b7576696 100644 --- a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/io/streams/internal/SerializationOperation.java +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/io/streams/internal/SerializationOperation.java @@ -22,6 +22,7 @@ import java.io.DataInputStream; import java.io.InputStream; import java.nio.file.Path; +import java.util.Set; import java.util.function.IntFunction; import java.util.function.Supplier; @@ -198,7 +199,7 @@ public enum SerializationOperation { /** * All variants of {@link SerializableDataInputStream#readSerializableList(int, boolean, Supplier)} - * and {@link SerializableDataInputStream#readSerializableArray(IntFunction, int, boolean, Supplier)} + * and {@link SerializableDataInputStream#readSerializableArray(IntFunction, int, boolean, Set)} */ READ_SERIALIZABLE_LIST, diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/merkle/utility/SerializableLong.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/merkle/utility/SerializableLong.java index 532a8c2ebac6..bcbe5350623c 100644 --- a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/merkle/utility/SerializableLong.java +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/merkle/utility/SerializableLong.java @@ -28,7 +28,7 @@ */ public class SerializableLong implements Comparable, FastCopyable, SelfSerializable { - private static final long CLASS_ID = 0x70deca6058a40bc6L; + public static final long CLASS_ID = 0x70deca6058a40bc6L; private static class ClassVersion { diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/threading/StoppableThreadTests.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/threading/StoppableThreadTests.java index f24658c38722..2a796768d92f 100644 --- a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/threading/StoppableThreadTests.java +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/threading/StoppableThreadTests.java @@ -54,6 +54,7 @@ import org.junit.jupiter.params.provider.ValueSource; @DisplayName("Stoppable Thread Tests") +@Tag(TIMING_SENSITIVE) class StoppableThreadTests { @Test diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/PlatformBuilder.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/PlatformBuilder.java index 10e50988f66b..49ffc518bfbe 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/PlatformBuilder.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/PlatformBuilder.java @@ -56,6 +56,7 @@ import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.Shutdown; import com.swirlds.platform.system.SoftwareVersion; +import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.system.SwirldState; import com.swirlds.platform.system.address.AddressBook; import com.swirlds.platform.util.BootstrapUtils; @@ -119,6 +120,8 @@ public PlatformBuilder( this.softwareVersion = Objects.requireNonNull(softwareVersion); this.genesisStateBuilder = Objects.requireNonNull(genesisStateBuilder); this.selfId = Objects.requireNonNull(selfId); + + StaticSoftwareVersion.setSoftwareVersion(softwareVersion); } /** diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/communication/handshake/VersionCompareHandshake.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/communication/handshake/VersionCompareHandshake.java index 2fff3977cf8e..2ad517f0c01d 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/communication/handshake/VersionCompareHandshake.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/communication/handshake/VersionCompareHandshake.java @@ -25,6 +25,7 @@ import com.swirlds.platform.system.SoftwareVersion; import java.io.IOException; import java.util.Objects; +import java.util.Set; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -63,7 +64,7 @@ public void runProtocol(final Connection connection) throws NetworkProtocolException, IOException, InterruptedException { connection.getDos().writeSerializable(version, true); connection.getDos().flush(); - final SelfSerializable peerVersion = connection.getDis().readSerializable(); + final SelfSerializable peerVersion = connection.getDis().readSerializable(Set.of(version.getClassId())); if (!(peerVersion instanceof SoftwareVersion sv) || version.compareTo(sv) != 0) { final String message = String.format( "Incompatible versions. Self version is '%s', peer version is '%s'", version, peerVersion); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/connectivity/InboundConnectionHandler.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/connectivity/InboundConnectionHandler.java index 2cd535d642a0..781fe9a7e176 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/connectivity/InboundConnectionHandler.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/connectivity/InboundConnectionHandler.java @@ -41,6 +41,7 @@ import java.net.Socket; import java.time.Duration; import java.util.Objects; +import java.util.Set; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -110,7 +111,7 @@ public void handle(final Socket clientSocket) { dos.writeSerializable(softwareVersion, true); dos.flush(); - final SoftwareVersion otherVersion = dis.readSerializable(); + final SoftwareVersion otherVersion = dis.readSerializable(Set.of(softwareVersion.getClassId())); if (otherVersion == null || otherVersion.getClass() != softwareVersion.getClass() || otherVersion.compareTo(softwareVersion) != 0) { diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/connectivity/OutboundConnectionCreator.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/connectivity/OutboundConnectionCreator.java index 476df0838242..090c48eb9e15 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/connectivity/OutboundConnectionCreator.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/connectivity/OutboundConnectionCreator.java @@ -43,6 +43,7 @@ import java.net.SocketException; import java.net.SocketTimeoutException; import java.util.Objects; +import java.util.Set; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -108,7 +109,7 @@ public Connection createConnection(final NodeId otherId) { dos.writeSerializable(softwareVersion, true); dos.flush(); - final SoftwareVersion otherVersion = dis.readSerializable(); + final SoftwareVersion otherVersion = dis.readSerializable(Set.of(softwareVersion.getClassId())); if (otherVersion == null || otherVersion.getClass() != softwareVersion.getClass() || otherVersion.compareTo(softwareVersion) != 0) { 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 56d84393c1f8..717562212afb 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 @@ -58,6 +58,7 @@ import com.swirlds.platform.state.signed.SignedStateFileWriter; import com.swirlds.platform.system.InitTrigger; import com.swirlds.platform.system.Round; +import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.system.SwirldMain; import com.swirlds.platform.system.SwirldState; import com.swirlds.platform.system.events.ConsensusEvent; @@ -148,6 +149,8 @@ public static void recoverState( try (final ReservedSignedState initialState = SignedStateFileReader.readStateFile( platformContext, signedStateFile) .reservedSignedState()) { + StaticSoftwareVersion.setSoftwareVersion( + initialState.get().getState().getPlatformState().getCreationSoftwareVersion()); logger.info( STARTUP.getMarker(), diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/StaticSoftwareVersion.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/StaticSoftwareVersion.java new file mode 100644 index 000000000000..64d25dbb76ab --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/StaticSoftwareVersion.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.swirlds.platform.system; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Set; + +/** + * Holds a static reference to information about the current software version. Needed due to inability to cleanly inject + * contextual data during deserialization. + * + * @deprecated this class is a short term work around, do not add new dependencies on this class + */ +@Deprecated +public final class StaticSoftwareVersion { + + /** + * The current software version. + */ + private static Set softwareVersionClassIdSet; + + private StaticSoftwareVersion() {} + + /** + * Get the current software version. + * + * @param softwareVersion the current software version + */ + public static void setSoftwareVersion(@NonNull final SoftwareVersion softwareVersion) { + softwareVersionClassIdSet = Set.of(softwareVersion.getClassId()); + } + + /** + * Reset this object. Required for testing. + */ + public static void reset() { + softwareVersionClassIdSet = null; + } + + /** + * Get a set that contains the class ID of the current software version. A convenience method that avoids the + * recreation of the set every time it is needed. + * + * @return a set that contains the class ID of the current software version + */ + @NonNull + public static Set getSoftwareVersionClassIdSet() { + if (softwareVersionClassIdSet == null) { + throw new IllegalStateException("Software version not set"); + } + return softwareVersionClassIdSet; + } +} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/BaseEventHashedData.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/BaseEventHashedData.java index b6651a4f8ca3..9b0f60ec4d6c 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/BaseEventHashedData.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/BaseEventHashedData.java @@ -29,8 +29,11 @@ import com.swirlds.common.utility.CommonUtils; import com.swirlds.platform.config.TransactionConfig; import com.swirlds.platform.system.SoftwareVersion; +import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.system.address.AddressBook; import com.swirlds.platform.system.transaction.ConsensusTransactionImpl; +import com.swirlds.platform.system.transaction.StateSignatureTransaction; +import com.swirlds.platform.system.transaction.SwirldTransaction; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.io.IOException; @@ -39,6 +42,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Set; /** * A class used to store base event data that is used to create the hash of that event. @@ -105,6 +109,12 @@ public static class ClassVersion { /** the payload: an array of transactions */ private ConsensusTransactionImpl[] transactions; + /** + * Class IDs of permitted transaction types. + */ + private static final Set TRANSACTION_TYPES = + Set.of(StateSignatureTransaction.CLASS_ID, SwirldTransaction.CLASS_ID); + public BaseEventHashedData() {} /** @@ -193,11 +203,7 @@ public void deserialize( throws IOException { Objects.requireNonNull(in, "The input stream must not be null"); serializedVersion = version; - if (version >= ClassVersion.SOFTWARE_VERSION) { - softwareVersion = in.readSerializable(); - } else { - softwareVersion = SoftwareVersion.NO_VERSION; - } + softwareVersion = in.readSerializable(StaticSoftwareVersion.getSoftwareVersionClassIdSet()); if (version < ClassVersion.BIRTH_ROUND) { // FUTURE WORK: The creatorId should be a selfSerializable NodeId at some point. // Changing the event format may require a HIP. The old format is preserved for now. @@ -227,7 +233,8 @@ public void deserialize( } timeCreated = in.readInstant(); in.readInt(); // read serialized length - transactions = in.readSerializableArray(ConsensusTransactionImpl[]::new, maxTransactionCount, true); + transactions = + in.readSerializableArray(ConsensusTransactionImpl[]::new, maxTransactionCount, true, TRANSACTION_TYPES); } @Override diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/transaction/StateSignatureTransaction.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/transaction/StateSignatureTransaction.java index 3d430a8bb90a..9ff7ae4e51e7 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/transaction/StateSignatureTransaction.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/transaction/StateSignatureTransaction.java @@ -39,7 +39,7 @@ public final class StateSignatureTransaction extends SystemTransaction { /** * class identifier for the purposes of serialization */ - private static final long CLASS_ID = 0xaf7024c653caabf4L; + public static final long CLASS_ID = 0xaf7024c653caabf4L; private static class ClassVersion { public static final int ORIGINAL = 1; diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/transaction/SwirldTransaction.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/transaction/SwirldTransaction.java index 8f8ab910d67f..8b6bb7b354fe 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/transaction/SwirldTransaction.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/transaction/SwirldTransaction.java @@ -44,7 +44,7 @@ */ public class SwirldTransaction extends ConsensusTransactionImpl implements Comparable { /** class identifier for the purposes of serialization */ - private static final long CLASS_ID = 0x9ff79186f4c4db97L; + public static final long CLASS_ID = 0x9ff79186f4c4db97L; /** current class version */ private static final int CLASS_VERSION = 1; diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/SerializableStreamTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/SerializableStreamTests.java index 3eeae9ac21fb..7a778013a6f9 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/SerializableStreamTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/SerializableStreamTests.java @@ -16,9 +16,11 @@ package com.swirlds.platform; +import static com.swirlds.common.test.fixtures.RandomUtils.getRandomPrintSeed; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertThrows; import com.swirlds.common.constructable.ClassConstructorPair; @@ -28,22 +30,28 @@ import com.swirlds.common.io.streams.AugmentedDataOutputStream; import com.swirlds.common.io.streams.SerializableDataInputStream; import com.swirlds.common.io.streams.SerializableDataOutputStream; +import com.swirlds.common.merkle.utility.SerializableLong; import com.swirlds.common.test.fixtures.TransactionUtils; import com.swirlds.common.test.fixtures.io.InputOutputStream; import com.swirlds.common.test.fixtures.io.SelfSerializableExample; import com.swirlds.common.test.fixtures.junit.tags.TestComponentTags; import com.swirlds.platform.system.transaction.Transaction; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.text.Normalizer; import java.time.Instant; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Random; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; @@ -66,6 +74,7 @@ static void setUp() throws ConstructableRegistryException { final ConstructableRegistry registry = ConstructableRegistry.getInstance(); registry.registerConstructables(PACKAGE_PREFIX); + registry.registerConstructables("com.swirlds.common.merkle.utility"); registry.registerConstructable( new ClassConstructorPair(SelfSerializableExample.class, SelfSerializableExample::new)); @@ -755,4 +764,142 @@ void serializedLengthArrayDiffClass(int tranAmount) throws IOException { private void checkExpectedSize(int actualWrittenBytes, int calculatedBytes) { assertEquals(actualWrittenBytes, calculatedBytes, "length mismatch"); } + + /** + * Tests class ID restrictions for {@link SerializableDataInputStream#readSerializable(Set)} + */ + @Test + void testRestrictedReadSerializable() throws IOException { + final Random random = getRandomPrintSeed(); + + final SerializableLong data = new SerializableLong(random.nextLong()); + + final ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); + final SerializableDataOutputStream out = new SerializableDataOutputStream(byteOut); + + out.writeSerializable(data, true); + final byte[] bytes = byteOut.toByteArray(); + + // Should work if the class id is not restricted + final SerializableDataInputStream in1 = new SerializableDataInputStream(new ByteArrayInputStream(bytes)); + final SerializableLong deserialized1 = in1.readSerializable(null); + assertEquals(data, deserialized1); + assertNotSame(data, deserialized1); + + // Should not work if the class id is restricted to other classIDs + final SerializableDataInputStream in2 = new SerializableDataInputStream(new ByteArrayInputStream(bytes)); + assertThrows(IOException.class, () -> in2.readSerializable(Set.of(1L, 2L, 3L, 4L))); + + // Should work if class ID is in the restricted set + final SerializableDataInputStream in3 = new SerializableDataInputStream(new ByteArrayInputStream(bytes)); + final SerializableLong deserialized3 = in3.readSerializable(Set.of(1L, 2L, 3L, 4L, SerializableLong.CLASS_ID)); + assertEquals(data, deserialized3); + assertNotSame(data, deserialized3); + } + + /** + * Tests class ID restrictions for + * {@link SerializableDataInputStream#readSerializableIterableWithSize(int, boolean, Supplier, Consumer, Set)}. + */ + @Test + void testRestrictedReadSerializableIterableWithSize() throws IOException { + final Random random = getRandomPrintSeed(); + + final List data = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + data.add(new SerializableLong(random.nextLong())); + } + + final ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); + final SerializableDataOutputStream out = new SerializableDataOutputStream(byteOut); + + out.writeSerializableIterableWithSize(data.iterator(), data.size(), true, false); + final byte[] bytes = byteOut.toByteArray(); + + // Should work if the class id is not restricted + final SerializableDataInputStream in1 = new SerializableDataInputStream(new ByteArrayInputStream(bytes)); + final List deserialized1 = new ArrayList<>(); + in1.readSerializableIterableWithSize(data.size(), x -> deserialized1.add((SerializableLong) x), null); + assertEquals(data, deserialized1); + + // Should not work if the class id is restricted to other classIDs + final SerializableDataInputStream in2 = new SerializableDataInputStream(new ByteArrayInputStream(bytes)); + assertThrows( + IOException.class, + () -> in2.readSerializableIterableWithSize(data.size(), x -> {}, Set.of(1L, 2L, 3L, 4L))); + + // Should work if class ID is in the restricted set + final SerializableDataInputStream in3 = new SerializableDataInputStream(new ByteArrayInputStream(bytes)); + final List deserialized3 = new ArrayList<>(); + in3.readSerializableIterableWithSize( + data.size(), + x -> deserialized3.add((SerializableLong) x), + Set.of(1L, 2L, 3L, 4L, SerializableLong.CLASS_ID)); + assertEquals(data, deserialized3); + } + + @Test + void testRestrictedReadSerializableList() throws IOException { + final Random random = getRandomPrintSeed(); + + final List data = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + data.add(new SerializableLong(random.nextLong())); + } + + final ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); + final SerializableDataOutputStream out = new SerializableDataOutputStream(byteOut); + + out.writeSerializableList(data, true, false); + final byte[] bytes = byteOut.toByteArray(); + + // Should work if the class id is not restricted + final SerializableDataInputStream in1 = new SerializableDataInputStream(new ByteArrayInputStream(bytes)); + final List deserialized1 = in1.readSerializableList(data.size(), null); + assertEquals(data, deserialized1); + + // Should not work if the class id is restricted to other classIDs + final SerializableDataInputStream in2 = new SerializableDataInputStream(new ByteArrayInputStream(bytes)); + assertThrows(IOException.class, () -> in2.readSerializableList(data.size(), Set.of(1L, 2L, 3L, 4L))); + + // Should work if class ID is in the restricted set + final SerializableDataInputStream in3 = new SerializableDataInputStream(new ByteArrayInputStream(bytes)); + final List deserialized3 = + in3.readSerializableList(data.size(), Set.of(1L, 2L, 3L, 4L, SerializableLong.CLASS_ID)); + assertEquals(data, deserialized3); + } + + @Test + void testRestrictedReadSerializableArray() throws IOException { + final Random random = getRandomPrintSeed(); + + final SerializableLong[] data = new SerializableLong[10]; + for (int i = 0; i < 10; i++) { + data[i] = new SerializableLong(random.nextLong()); + } + + final ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); + final SerializableDataOutputStream out = new SerializableDataOutputStream(byteOut); + + out.writeSerializableArray(data, true, false); + final byte[] bytes = byteOut.toByteArray(); + + // Should work if the class id is not restricted + final SerializableDataInputStream in1 = new SerializableDataInputStream(new ByteArrayInputStream(bytes)); + final SerializableLong[] deserialized1 = + in1.readSerializableArray(SerializableLong[]::new, data.length, true, (Set) null); + assertArrayEquals(data, deserialized1); + + // Should not work if the class id is restricted to other classIDs + final SerializableDataInputStream in2 = new SerializableDataInputStream(new ByteArrayInputStream(bytes)); + assertThrows( + IOException.class, + () -> in2.readSerializableArray(SerializableLong[]::new, data.length, true, Set.of(1L, 2L, 3L, 4L))); + + // Should work if class ID is in the restricted set + final SerializableDataInputStream in3 = new SerializableDataInputStream(new ByteArrayInputStream(bytes)); + final SerializableLong[] deserialized3 = in3.readSerializableArray( + SerializableLong[]::new, data.length, true, Set.of(1L, 2L, 3L, 4L, SerializableLong.CLASS_ID)); + assertArrayEquals(data, deserialized3); + } } diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/DetailedConsensusEventTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/DetailedConsensusEventTest.java index e54550a264cc..d541e1830760 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/DetailedConsensusEventTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/DetailedConsensusEventTest.java @@ -24,12 +24,15 @@ import com.swirlds.common.crypto.Hash; import com.swirlds.common.test.fixtures.io.InputOutputStream; import com.swirlds.platform.internal.EventImpl; +import com.swirlds.platform.system.BasicSoftwareVersion; +import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.system.events.BaseEventHashedData; import com.swirlds.platform.system.events.BaseEventUnhashedData; import com.swirlds.platform.system.events.ConsensusData; import com.swirlds.platform.system.events.DetailedConsensusEvent; import java.io.IOException; import java.util.Random; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -37,8 +40,13 @@ public class DetailedConsensusEventTest { @BeforeAll public static void setUp() throws ConstructableRegistryException { final ConstructableRegistry registry = ConstructableRegistry.getInstance(); - registry.registerConstructables("com.swirlds.common"); - registry.registerConstructables("com.swirlds.common.events"); + registry.registerConstructables("com.swirlds.platform"); + StaticSoftwareVersion.setSoftwareVersion(new BasicSoftwareVersion(1)); + } + + @AfterAll + static void afterAll() { + StaticSoftwareVersion.reset(); } @Test diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamMultiFileIteratorTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamMultiFileIteratorTest.java index 5d60a0a2f986..da8e5e3cf1b4 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamMultiFileIteratorTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamMultiFileIteratorTest.java @@ -37,6 +37,8 @@ import com.swirlds.platform.recovery.internal.EventStreamMultiFileIterator; import com.swirlds.platform.recovery.internal.EventStreamRoundLowerBound; import com.swirlds.platform.recovery.internal.EventStreamTimestampLowerBound; +import com.swirlds.platform.system.BasicSoftwareVersion; +import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.system.events.DetailedConsensusEvent; import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; @@ -51,6 +53,7 @@ import java.util.NoSuchElementException; import java.util.Objects; import java.util.Random; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -61,6 +64,12 @@ class EventStreamMultiFileIteratorTest { @BeforeAll static void beforeAll() throws ConstructableRegistryException { ConstructableRegistry.getInstance().registerConstructables("com.swirlds"); + StaticSoftwareVersion.setSoftwareVersion(new BasicSoftwareVersion(1)); + } + + @AfterAll + static void afterAll() { + StaticSoftwareVersion.reset(); } public static void assertEventsAreEqual(final EventImpl expected, final EventImpl actual) { diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamPathIteratorTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamPathIteratorTest.java index c5bbe6ababe3..3d8e5807ef4a 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamPathIteratorTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamPathIteratorTest.java @@ -33,6 +33,8 @@ import com.swirlds.platform.recovery.internal.EventStreamPathIterator; import com.swirlds.platform.recovery.internal.EventStreamRoundLowerBound; import com.swirlds.platform.recovery.internal.EventStreamTimestampLowerBound; +import com.swirlds.platform.system.BasicSoftwareVersion; +import com.swirlds.platform.system.StaticSoftwareVersion; import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; import java.nio.file.Path; @@ -46,12 +48,24 @@ import java.util.NoSuchElementException; import java.util.Objects; import java.util.Random; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @DisplayName("EventStreamPathIterator Test") class EventStreamPathIteratorTest { + @BeforeAll + static void beforeAll() { + StaticSoftwareVersion.setSoftwareVersion(new BasicSoftwareVersion(1)); + } + + @AfterAll + static void afterAll() { + StaticSoftwareVersion.reset(); + } + @Test @DisplayName("Starting From First Event Test") void startingFromFirstEventTest() throws IOException, NoSuchAlgorithmException, ConstructableRegistryException { diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamRoundIteratorTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamRoundIteratorTest.java index bca08617a710..252ed198fd30 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamRoundIteratorTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamRoundIteratorTest.java @@ -36,7 +36,9 @@ import com.swirlds.platform.internal.EventImpl; import com.swirlds.platform.recovery.internal.EventStreamPathIterator; import com.swirlds.platform.recovery.internal.EventStreamRoundIterator; +import com.swirlds.platform.system.BasicSoftwareVersion; import com.swirlds.platform.system.Round; +import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.system.address.AddressBook; import java.io.IOException; import java.nio.file.Files; @@ -47,6 +49,8 @@ import java.util.List; import java.util.NoSuchElementException; import java.util.Random; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -54,6 +58,16 @@ @DisplayName("EventStreamRoundIterator Test") class EventStreamRoundIteratorTest { + @BeforeAll + static void beforeAll() { + StaticSoftwareVersion.setSoftwareVersion(new BasicSoftwareVersion(1)); + } + + @AfterAll + static void afterAll() { + StaticSoftwareVersion.reset(); + } + public static void assertEventsAreEqual(final EventImpl expected, final EventImpl actual) { assertEquals(expected.getBaseEvent(), actual.getBaseEvent()); assertEquals(expected.getConsensusData(), actual.getConsensusData()); diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamSingleFileIteratorTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamSingleFileIteratorTest.java index 4e37cf375059..10e54dbd5563 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamSingleFileIteratorTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/EventStreamSingleFileIteratorTest.java @@ -34,6 +34,8 @@ import com.swirlds.common.io.utility.TemporaryFileBuilder; import com.swirlds.platform.internal.EventImpl; import com.swirlds.platform.recovery.internal.EventStreamSingleFileIterator; +import com.swirlds.platform.system.BasicSoftwareVersion; +import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.system.events.DetailedConsensusEvent; import java.io.IOException; import java.nio.file.Path; @@ -42,6 +44,8 @@ import java.util.List; import java.util.NoSuchElementException; import java.util.Random; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -49,6 +53,16 @@ @DisplayName("EventStreamSingleFileIterator Test") class EventStreamSingleFileIteratorTest { + @BeforeAll + static void beforeAll() { + StaticSoftwareVersion.setSoftwareVersion(new BasicSoftwareVersion(1)); + } + + @AfterAll + static void afterAll() { + StaticSoftwareVersion.reset(); + } + public static void assertEventsAreEqual(final EventImpl expected, final EventImpl actual) { assertEquals(expected.getBaseEvent(), actual.getBaseEvent()); assertEquals(expected.getConsensusData(), actual.getConsensusData()); diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/ObjectStreamIteratorTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/ObjectStreamIteratorTest.java index b4d5a1830730..e2cf2693365e 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/ObjectStreamIteratorTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/ObjectStreamIteratorTest.java @@ -37,6 +37,8 @@ import com.swirlds.common.io.utility.TemporaryFileBuilder; import com.swirlds.platform.internal.EventImpl; import com.swirlds.platform.recovery.internal.ObjectStreamIterator; +import com.swirlds.platform.system.BasicSoftwareVersion; +import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.system.events.DetailedConsensusEvent; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -46,12 +48,24 @@ import java.util.List; import java.util.NoSuchElementException; import java.util.Random; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @DisplayName("ObjectStreamIterator Test") class ObjectStreamIteratorTest { + @BeforeAll + static void beforeAll() { + StaticSoftwareVersion.setSoftwareVersion(new BasicSoftwareVersion(1)); + } + + @AfterAll + static void afterAll() { + StaticSoftwareVersion.reset(); + } + public static void assertEventsAreEqual(final EventImpl expected, final EventImpl actual) { assertEquals(expected.getBaseEvent(), actual.getBaseEvent()); assertEquals(expected.getConsensusData(), actual.getConsensusData()); diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/util/EventStreamSigningUtilsTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/util/EventStreamSigningUtilsTests.java index 06ba6c0b2090..b08e4aa79141 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/util/EventStreamSigningUtilsTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/util/EventStreamSigningUtilsTests.java @@ -30,6 +30,8 @@ import com.swirlds.common.stream.EventStreamType; import com.swirlds.platform.internal.EventImpl; import com.swirlds.platform.recovery.RecoveryTestUtils; +import com.swirlds.platform.system.BasicSoftwareVersion; +import com.swirlds.platform.system.StaticSoftwareVersion; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Files; @@ -41,6 +43,7 @@ import java.util.Objects; import java.util.Random; import java.util.stream.Stream; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -80,6 +83,13 @@ void setup() { // the utility method being leveraged saves stream files to a directory "events_test" toSignDirectory = testDirectoryPath.resolve("events_test"); + + StaticSoftwareVersion.setSoftwareVersion(new BasicSoftwareVersion(1)); + } + + @AfterAll + static void afterAll() { + StaticSoftwareVersion.reset(); } /** diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/SerializationTests.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/SerializationTests.java index 24fd1c44942e..6828ebe9146a 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/SerializationTests.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/SerializationTests.java @@ -24,9 +24,12 @@ import com.swirlds.common.io.SelfSerializable; import com.swirlds.common.test.fixtures.io.SerializationUtils; import com.swirlds.config.extensions.test.fixtures.TestConfigBuilder; +import com.swirlds.platform.system.BasicSoftwareVersion; +import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.test.fixtures.event.TestingEventBuilder; import java.io.IOException; import java.util.stream.Stream; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; @@ -40,6 +43,12 @@ public static void setUp() throws ConstructableRegistryException { new TestConfigBuilder().withValue("transactionMaxBytes", 1_000_000).getOrCreateConfig(); ConstructableRegistry.getInstance().registerConstructables("com.swirlds"); + StaticSoftwareVersion.setSoftwareVersion(new BasicSoftwareVersion(1)); + } + + @AfterAll + static void afterAll() { + StaticSoftwareVersion.reset(); } @ParameterizedTest diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/cli/EventStreamReportingToolTest.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/cli/EventStreamReportingToolTest.java index 2083fdba62c9..ab75c2db222e 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/cli/EventStreamReportingToolTest.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/cli/EventStreamReportingToolTest.java @@ -25,6 +25,8 @@ import com.swirlds.platform.internal.EventImpl; import com.swirlds.platform.recovery.internal.EventStreamRoundLowerBound; import com.swirlds.platform.recovery.internal.EventStreamTimestampLowerBound; +import com.swirlds.platform.system.BasicSoftwareVersion; +import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.test.consensus.GenerateConsensus; import com.swirlds.platform.test.fixtures.stream.StreamUtils; import com.swirlds.platform.test.simulated.RandomSigner; @@ -37,7 +39,9 @@ import java.util.Optional; import java.util.Random; import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -46,6 +50,16 @@ class EventStreamReportingToolTest { @TempDir Path tmpDir; + @BeforeAll + static void beforeAll() { + StaticSoftwareVersion.setSoftwareVersion(new BasicSoftwareVersion(1)); + } + + @AfterAll + static void afterAll() { + StaticSoftwareVersion.reset(); + } + /** * Generates events, feeds them to consensus, then writes these consensus events to stream files. One the files a * written, it generates a report and checks the values. diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/cli/EventStreamSingleFileRepairTest.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/cli/EventStreamSingleFileRepairTest.java index 75e06cf794f0..9e182d761860 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/cli/EventStreamSingleFileRepairTest.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/cli/EventStreamSingleFileRepairTest.java @@ -30,6 +30,8 @@ import com.swirlds.platform.internal.ConsensusRound; import com.swirlds.platform.recovery.internal.EventStreamSingleFileIterator; import com.swirlds.platform.recovery.internal.EventStreamSingleFileRepairer; +import com.swirlds.platform.system.BasicSoftwareVersion; +import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.test.consensus.GenerateConsensus; import com.swirlds.platform.test.fixtures.stream.StreamUtils; import com.swirlds.platform.test.simulated.RandomSigner; @@ -43,7 +45,9 @@ import java.util.Deque; import java.util.Random; import java.util.stream.Stream; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -58,6 +62,16 @@ class EventStreamSingleFileRepairTest { @TempDir Path tmpDir; + @BeforeAll + static void beforeAll() { + StaticSoftwareVersion.setSoftwareVersion(new BasicSoftwareVersion(1)); + } + + @AfterAll + static void afterAll() { + StaticSoftwareVersion.reset(); + } + /** * Generates events, feeds them to consensus, then writes these consensus events to stream files. Once the files are * written, it picks the last file, attempts to repair it with no effect, truncates the file, and then repairs it. diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/components/TransactionHandlingTestUtils.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/components/TransactionHandlingTestUtils.java index 47ec987b75d2..0cdff9f8a842 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/components/TransactionHandlingTestUtils.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/components/TransactionHandlingTestUtils.java @@ -16,27 +16,18 @@ package com.swirlds.platform.test.components; -import static org.mockito.Mockito.mock; - import com.swirlds.common.crypto.CryptographyHolder; import com.swirlds.common.platform.NodeId; import com.swirlds.common.test.fixtures.DummySystemTransaction; -import com.swirlds.platform.consensus.ConsensusSnapshot; -import com.swirlds.platform.consensus.GraphGenerations; -import com.swirlds.platform.consensus.NonAncientEventWindow; -import com.swirlds.platform.internal.ConsensusRound; import com.swirlds.platform.internal.EventImpl; import com.swirlds.platform.system.BasicSoftwareVersion; -import com.swirlds.platform.system.address.AddressBook; import com.swirlds.platform.system.events.BaseEventHashedData; import com.swirlds.platform.system.events.BaseEventUnhashedData; import com.swirlds.platform.system.events.EventConstants; import com.swirlds.platform.system.events.EventDescriptor; import com.swirlds.platform.system.transaction.SystemTransaction; import java.time.Instant; -import java.util.ArrayList; import java.util.Collections; -import java.util.List; /** * Utility functions for testing system transaction handling @@ -71,26 +62,4 @@ public static EventImpl newDummyEvent(final int transactionCount) { transactions), new BaseEventUnhashedData(new NodeId(0L), new byte[0])); } - - /** - * Generates a new round, with specified number of events, containing DummySystemTransactions - * - * @param roundContents a list of integers, where each list element results in an event being added the output - * round, and the element value specifies number of transactions to include in the event - * @return a new round, with specified contents - */ - public static ConsensusRound newDummyRound(final List roundContents) { - final List events = new ArrayList<>(); - for (Integer transactionCount : roundContents) { - events.add(newDummyEvent(transactionCount)); - } - - return new ConsensusRound( - mock(AddressBook.class), - events, - mock(EventImpl.class), - mock(GraphGenerations.class), - mock(NonAncientEventWindow.class), - mock(ConsensusSnapshot.class)); - } } diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/GossipEventTest.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/GossipEventTest.java index b74e8dc70806..eb9d7a505a92 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/GossipEventTest.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/GossipEventTest.java @@ -27,6 +27,8 @@ import com.swirlds.common.test.fixtures.io.SerializationUtils; import com.swirlds.config.extensions.test.fixtures.TestConfigBuilder; import com.swirlds.platform.event.GossipEvent; +import com.swirlds.platform.system.BasicSoftwareVersion; +import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.test.fixtures.event.TestingEventBuilder; import com.swirlds.platform.test.utils.EqualsVerifier; import java.io.ByteArrayOutputStream; @@ -34,6 +36,7 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -43,6 +46,12 @@ class GossipEventTest { @BeforeAll public static void setup() throws FileNotFoundException, ConstructableRegistryException { new TestConfigBuilder().getOrCreateConfig(); + StaticSoftwareVersion.setSoftwareVersion(new BasicSoftwareVersion(1)); + } + + @AfterAll + static void afterAll() { + StaticSoftwareVersion.reset(); } @Test diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/creation/EventCreationExpectedResults.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/creation/EventCreationExpectedResults.java deleted file mode 100644 index 315b4ec3bc27..000000000000 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/creation/EventCreationExpectedResults.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (C) 2016-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.test.event.creation; - -import com.swirlds.common.utility.DurationUtils; -import java.time.Duration; -import org.junit.jupiter.api.Assertions; - -/** - * Used to store and expected results and validate {@link EventCreationExpectedResults} - */ -public class EventCreationExpectedResults { - private int numEventsCreatedMax = Integer.MAX_VALUE; - private int numEventsCreatedMin = 0; - private int numConsEventsMin = 0; - private boolean consensusExpected = true; - private Duration maxC2CMax = Duration.ofSeconds(Long.MAX_VALUE); - private Duration avgC2CMax = Duration.ofSeconds(Long.MAX_VALUE); - private int maxRoundSizeMax = Integer.MAX_VALUE; - - public static EventCreationExpectedResults get() { - return new EventCreationExpectedResults(); - } - - public EventCreationExpectedResults setNumEventsCreatedMax(final int numEventsCreatedMax) { - this.numEventsCreatedMax = numEventsCreatedMax; - return this; - } - - public EventCreationExpectedResults setNumEventsCreatedMin(final int numEventsCreatedMin) { - this.numEventsCreatedMin = numEventsCreatedMin; - return this; - } - - public EventCreationExpectedResults setNumConsEventsMin(final int numConsEventsMin) { - this.numConsEventsMin = numConsEventsMin; - return this; - } - - public EventCreationExpectedResults setConsensusExpected(final boolean consensusExpected) { - this.consensusExpected = consensusExpected; - return this; - } - - public EventCreationExpectedResults setMaxC2CMax(final Duration maxC2CMax) { - this.maxC2CMax = maxC2CMax; - return this; - } - - public EventCreationExpectedResults setAvgC2CMax(final Duration avgC2CMax) { - this.avgC2CMax = avgC2CMax; - return this; - } - - public EventCreationExpectedResults setMaxRoundSizeMax(final int maxRoundSizeMax) { - this.maxRoundSizeMax = maxRoundSizeMax; - return this; - } - - public void validate(final EventCreationSimulationResults r) { - Assertions.assertTrue( - numEventsCreatedMax >= r.numEventsCreated(), - "Number of events created should be less than or equal to %d but was %d" - .formatted(numEventsCreatedMax, r.numEventsCreated())); - Assertions.assertTrue(numEventsCreatedMin <= r.numEventsCreated()); - if (!consensusExpected) { - Assertions.assertEquals(0, r.numConsEvents()); - return; - } - Assertions.assertTrue( - numConsEventsMin <= r.numConsEvents(), - "Number of consensus events should be greater than or equal to %d but was %d" - .formatted(numConsEventsMin, r.numConsEvents())); - Assertions.assertNotNull(r.avgC2C()); - Assertions.assertNotNull(r.maxC2C()); - Assertions.assertFalse( - DurationUtils.isLonger(r.maxC2C(), maxC2CMax), - "Max C2C should be less than or equal to %s but was %s".formatted(maxC2CMax, r.maxC2C())); - Assertions.assertFalse( - DurationUtils.isLonger(r.avgC2C(), avgC2CMax), - "Avg C2C should be less than or equal to %s but was %s".formatted(avgC2CMax, r.avgC2C())); - Assertions.assertNotNull(r.maxRoundSize()); - Assertions.assertTrue( - maxRoundSizeMax >= r.maxRoundSize(), - "Max round size should be less than or equal to %d but was %d" - .formatted(maxRoundSizeMax, r.maxRoundSize())); - } -} diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/creation/EventCreationSimulationParams.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/creation/EventCreationSimulationParams.java deleted file mode 100644 index 0d7e9288d89c..000000000000 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/creation/EventCreationSimulationParams.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2016-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.test.event.creation; - -import com.swirlds.common.platform.NodeId; -import com.swirlds.platform.test.simulated.config.NodeConfig; -import java.time.Duration; -import java.util.Map; - -/** - * Parameters for an event creation simulation - * - * @param seed - * the seed to use for randomness - * @param nodeConfigs - * configuration for each node, the number of nodes is determined by the list size - * @param maxDelay - * the maximum delay between 2 nodes - * @param simulatedTime - * the amount of time to simulate - * @param simulationStep - * the step size of the fake clock - */ -public record EventCreationSimulationParams( - long seed, - Map nodeConfigs, - Duration maxDelay, - Duration simulatedTime, - Duration simulationStep, - EventCreationExpectedResults expectedResults) { - public int numNodes() { - return nodeConfigs.size(); - } -} diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/creation/EventCreationSimulationResults.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/creation/EventCreationSimulationResults.java deleted file mode 100644 index d88e9da5bdec..000000000000 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/creation/EventCreationSimulationResults.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (C) 2022-2024 Hedera Hashgraph, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.swirlds.platform.test.event.creation; - -import com.swirlds.common.utility.DurationUtils; -import com.swirlds.platform.internal.ConsensusRound; -import com.swirlds.platform.internal.EventImpl; -import java.time.Duration; -import java.util.Collection; - -/** - * Stores results of a event creation simulation - * - * @param numEventsCreated - * the number of events created in total - * @param numConsEvents - * the number of events that reached consensus - * @param maxC2C - * the maximum C2C of all consensus events, null if none reached consensus - * @param avgC2C - * the average C2C of all consensus events, null if none reached consensus - * @param maxRoundSize - * the maximum round size of all consensus rounds, null if none reached consensus - * @param avgRoundSize - * the average round size of all consensus rounds, null if none reached consensus - */ -public record EventCreationSimulationResults( - int numEventsCreated, - int numConsEvents, - Duration maxC2C, - Duration avgC2C, - Integer maxRoundSize, - Double avgRoundSize) { - public static EventCreationSimulationResults calculateResults( - final int numEventsCreated, final Collection consensusRounds) { - int numConsEvents = 0; - Duration maxC2Ctmp = Duration.ZERO; - Duration sumC2Ctmp = Duration.ZERO; - int maxRoundTmp = -1; - int sumRoundSize = 0; - - for (final ConsensusRound consensusRound : consensusRounds) { - maxRoundTmp = Math.max(maxRoundTmp, consensusRound.getNumEvents()); - sumRoundSize += consensusRound.getNumEvents(); - - for (final EventImpl event : consensusRound.getConsensusEvents()) { - final Duration c2c = Duration.between(event.getTimeCreated(), event.getReachedConsTimestamp()); - maxC2Ctmp = DurationUtils.max(maxC2Ctmp, c2c); - sumC2Ctmp = sumC2Ctmp.plus(c2c); - numConsEvents++; - } - } - - final Duration maxC2C = maxC2Ctmp == Duration.ZERO ? null : maxC2Ctmp; - final Duration avgC2C = sumC2Ctmp == Duration.ZERO ? null : sumC2Ctmp.dividedBy(numConsEvents); - final Integer maxRoundSize = maxRoundTmp < 0 ? null : maxRoundTmp; - final Double avgRoundSize = - consensusRounds.size() == 0 ? null : ((double) sumRoundSize) / consensusRounds.size(); - - return new EventCreationSimulationResults( - numEventsCreated, numConsEvents, maxC2C, avgC2C, maxRoundSize, avgRoundSize); - } - - public void printResults() { - System.out.println("num events created = " + numEventsCreated); - System.out.println("num consensus events = " + numConsEvents); - System.out.println("maxC2C = " - + (maxC2C == null ? "N/A" : String.format("%.3f seconds", ((double) maxC2C.toMillis()) / 1000))); - System.out.println("avgC2C = " - + (avgC2C == null ? "N/A" : String.format("%.3f seconds", ((double) avgC2C.toMillis()) / 1000))); - System.out.println("maxRoundSize = " + (maxRoundSize == null ? "N/A" : maxRoundSize)); - System.out.println("avgRoundSize = " + (avgRoundSize == null ? "N/A" : avgRoundSize)); - } -} diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/preconsensus/PcesReadWriteTests.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/preconsensus/PcesReadWriteTests.java index c697243b0e95..2309552ccbc1 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/preconsensus/PcesReadWriteTests.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/preconsensus/PcesReadWriteTests.java @@ -36,6 +36,8 @@ import com.swirlds.platform.event.preconsensus.PcesFile; import com.swirlds.platform.event.preconsensus.PcesFileIterator; import com.swirlds.platform.event.preconsensus.PcesMutableFile; +import com.swirlds.platform.system.BasicSoftwareVersion; +import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.test.fixtures.event.generator.StandardGraphGenerator; import com.swirlds.platform.test.fixtures.event.source.StandardEventSource; import edu.umd.cs.findbugs.annotations.NonNull; @@ -51,6 +53,7 @@ import java.util.NoSuchElementException; import java.util.Random; import java.util.stream.Stream; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -72,6 +75,12 @@ class PcesReadWriteTests { @BeforeAll static void beforeAll() throws ConstructableRegistryException { ConstructableRegistry.getInstance().registerConstructables(""); + StaticSoftwareVersion.setSoftwareVersion(new BasicSoftwareVersion(1)); + } + + @AfterAll + static void afterAll() { + StaticSoftwareVersion.reset(); } @BeforeEach diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/preconsensus/PcesWriterTests.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/preconsensus/PcesWriterTests.java index c798b1a04b4e..27d5496149c6 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/preconsensus/PcesWriterTests.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/preconsensus/PcesWriterTests.java @@ -61,6 +61,8 @@ import com.swirlds.platform.event.preconsensus.PcesUtilities; import com.swirlds.platform.event.preconsensus.PcesWriter; import com.swirlds.platform.eventhandling.EventConfig_; +import com.swirlds.platform.system.BasicSoftwareVersion; +import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.system.transaction.ConsensusTransactionImpl; import com.swirlds.platform.system.transaction.SwirldTransaction; import com.swirlds.platform.test.fixtures.event.generator.StandardGraphGenerator; @@ -82,6 +84,7 @@ import java.util.Random; import java.util.Set; import java.util.stream.Stream; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -239,6 +242,12 @@ static StandardGraphGenerator buildGraphGenerator(final Random random) { @BeforeAll static void beforeAll() throws ConstructableRegistryException { ConstructableRegistry.getInstance().registerConstructables(""); + StaticSoftwareVersion.setSoftwareVersion(new BasicSoftwareVersion(1)); + } + + @AfterAll + static void afterAll() { + StaticSoftwareVersion.reset(); } @BeforeEach diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/communication/handshake/VersionHandshakeTests.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/communication/handshake/VersionHandshakeTests.java index f95e4b064574..3118aa4b517f 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/communication/handshake/VersionHandshakeTests.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/communication/handshake/VersionHandshakeTests.java @@ -99,19 +99,13 @@ void differentVersion() throws IOException { @DisplayName("Their software version is null") void nullVersion() throws IOException { clearWriteFlush(theirConnection, null); - assertThrows(HandshakeException.class, () -> protocolThrowingOnMismatch.runProtocol(myConnection)); - - clearWriteFlush(theirConnection, null); - assertDoesNotThrow(() -> protocolToleratingMismatch.runProtocol(myConnection)); + assertThrows(IOException.class, () -> protocolThrowingOnMismatch.runProtocol(myConnection)); } @Test @DisplayName("Their software version is a different class") void differentClass() throws IOException { clearWriteFlush(theirConnection, new SerializableLong(5)); - assertThrows(HandshakeException.class, () -> protocolThrowingOnMismatch.runProtocol(myConnection)); - - clearWriteFlush(theirConnection, new SerializableLong(5)); - assertDoesNotThrow(() -> protocolToleratingMismatch.runProtocol(myConnection)); + assertThrows(IOException.class, () -> protocolThrowingOnMismatch.runProtocol(myConnection)); } } diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/sync/SyncTests.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/sync/SyncTests.java index 7e23542e0b2f..2582013378d0 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/sync/SyncTests.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/sync/SyncTests.java @@ -41,6 +41,8 @@ import com.swirlds.platform.consensus.NonAncientEventWindow; import com.swirlds.platform.event.AncientMode; import com.swirlds.platform.gossip.shadowgraph.ShadowEvent; +import com.swirlds.platform.system.BasicSoftwareVersion; +import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.system.events.EventConstants; import com.swirlds.platform.test.event.emitter.EventEmitterFactory; import com.swirlds.platform.test.event.emitter.StandardEventEmitter; @@ -60,6 +62,7 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; @@ -75,6 +78,16 @@ private static Stream bothAncientModes() { return Stream.of(Arguments.of(GENERATION_THRESHOLD), Arguments.of(BIRTH_ROUND_THRESHOLD)); } + @BeforeAll + static void beforeAll() { + StaticSoftwareVersion.setSoftwareVersion(new BasicSoftwareVersion(1)); + } + + @AfterAll + static void afterAll() { + StaticSoftwareVersion.reset(); + } + private static Stream fourNodeGraphParams() { return Stream.of( Arguments.of(new SyncTestParams(4, 100, 20, 0, GENERATION_THRESHOLD)), From f68ac1d24ac5da695df73bdeaa5c6cf373db57dd Mon Sep 17 00:00:00 2001 From: Iris Simon <122310714+iwsimon@users.noreply.github.com> Date: Wed, 6 Mar 2024 12:36:42 -0500 Subject: [PATCH 024/115] fix: cherry-pick fixed diff test issue 11822 (#11907) Signed-off-by: Iris Simon --- .../impl/handlers/CryptoTransferHandler.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoTransferHandler.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoTransferHandler.java index db6447df0d9a..d42b4f6ce0b8 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoTransferHandler.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoTransferHandler.java @@ -16,6 +16,7 @@ package com.hedera.node.app.service.token.impl.handlers; +import static com.hedera.hapi.node.base.ResponseCodeEnum.AMOUNT_EXCEEDS_ALLOWANCE; import static com.hedera.hapi.node.base.ResponseCodeEnum.CUSTOM_FEE_CHARGING_EXCEEDED_MAX_ACCOUNT_AMOUNTS; import static com.hedera.hapi.node.base.ResponseCodeEnum.INSUFFICIENT_PAYER_BALANCE_FOR_CUSTOM_FEE; import static com.hedera.hapi.node.base.ResponseCodeEnum.INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE; @@ -25,6 +26,7 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TRANSFER_ACCOUNT_ID; 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.SPENDER_DOES_NOT_HAVE_ALLOWANCE; import static com.hedera.hapi.node.base.SubType.DEFAULT; import static com.hedera.hapi.node.base.SubType.TOKEN_FUNGIBLE_COMMON; import static com.hedera.hapi.node.base.SubType.TOKEN_FUNGIBLE_COMMON_WITH_CUSTOM_FEES; @@ -36,9 +38,11 @@ import static com.hedera.node.app.hapi.fees.usage.token.entities.TokenEntitySizes.TOKEN_ENTITY_SIZES; import static com.hedera.node.app.service.token.AliasUtils.isAlias; import static com.hedera.node.app.service.token.impl.handlers.BaseCryptoHandler.isStakingAccount; +import static com.hedera.node.app.service.token.impl.util.TokenHandlerHelper.getIfUsable; import static com.hedera.node.app.spi.HapiUtils.isHollow; import static com.hedera.node.app.spi.key.KeyUtils.isValid; import static com.hedera.node.app.spi.validation.Validations.validateAccountID; +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.validateTruePreCheck; import static java.util.Collections.emptyList; @@ -53,6 +57,7 @@ import com.hedera.hapi.node.base.TokenTransferList; import com.hedera.hapi.node.base.TransferList; import com.hedera.hapi.node.state.token.Account; +import com.hedera.hapi.node.state.token.AccountFungibleTokenAllowance; import com.hedera.hapi.node.state.token.Nft; import com.hedera.hapi.node.token.CryptoTransferTransactionBody; import com.hedera.hapi.node.transaction.AssessedCustomFee; @@ -88,6 +93,7 @@ import com.hedera.node.config.data.TokensConfig; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; @@ -221,6 +227,8 @@ public void handle(@NonNull final HandleContext context) throws HandleException validator.validateSemantics(op, ledgerConfig, hederaConfig, tokensConfig); + validateTopLevelAllowances(context, op, topLevelPayer); + // create a new transfer context that is specific only for this transaction final var transferContext = new TransferContextImpl(context, enforceMonoServiceRestrictionsOnAutoCreationCustomFeePayments); @@ -637,4 +645,43 @@ private SubType getSubType( } return DEFAULT; } + + private void validateTopLevelAllowances( + @NonNull final HandleContext context, + @NonNull final CryptoTransferTransactionBody op, + @NonNull final AccountID topLevelPayer) { + final var accountStore = context.readableStore(ReadableAccountStore.class); + TokenID tokenId; + AccountID accountId; + long amount; + Account account; + List tokenAllowances; + + for (final var xfers : op.tokenTransfersOrElse(emptyList())) { + tokenId = xfers.tokenOrThrow(); + + for (final var aa : xfers.transfersOrElse(emptyList())) { + accountId = aa.accountID(); + amount = aa.amount(); + + if (amount < 0 && aa.isApproval()) { + account = getIfUsable(accountId, accountStore, context.expiryValidator(), INVALID_ACCOUNT_ID); + tokenAllowances = account.tokenAllowancesOrElse(Collections.emptyList()); + + for (int i = 0; i < tokenAllowances.size(); i++) { + final var allowance = tokenAllowances.get(i); + + validateFalse( + !topLevelPayer.equals(allowance.spenderId()) || allowance.amount() == 0, + SPENDER_DOES_NOT_HAVE_ALLOWANCE); + + if (topLevelPayer.equals(allowance.spenderId()) && tokenId.equals(allowance.tokenId())) { + final var newAllowanceAmount = allowance.amount() + amount; + validateTrue(newAllowanceAmount >= 0, AMOUNT_EXCEEDS_ALLOWANCE); + } + } + } + } + } + } } From 7dc01fb93528b967fecf56fc2defdd6d866e830e Mon Sep 17 00:00:00 2001 From: Austin Littley <102969658+alittley@users.noreply.github.com> Date: Wed, 6 Mar 2024 13:02:31 -0500 Subject: [PATCH 025/115] fix: Log correct PCES lower bound (#11922) Signed-off-by: Austin Littley --- .../swirlds/platform/event/preconsensus/PcesFileTracker.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/preconsensus/PcesFileTracker.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/preconsensus/PcesFileTracker.java index 7812db42a27f..c264a3700c8a 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/preconsensus/PcesFileTracker.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/preconsensus/PcesFileTracker.java @@ -210,7 +210,7 @@ public Iterator getFileIterator(final long lowerBound, final long orig "The preconsensus event stream has insufficient data to guarantee that all events with the " + "requested lower bound of {} are present, the first file has a lower bound of {}", lowerBound, - files.getFirst().getLowerBound()); + files.get(firstFileIndex).getLowerBound()); return new UnmodifiableIterator<>(files.iterator(firstFileIndex)); } From a8e1470997879eb1c394ba56b04fb4ea3645a2cf Mon Sep 17 00:00:00 2001 From: Ivan Malygin Date: Wed, 6 Mar 2024 15:32:23 -0500 Subject: [PATCH 026/115] fix: 11507 Temporary disabled test to prevent non-deterministic failures. (#11929) Signed-off-by: Ivan Malygin --- .../virtual/merkle/reconnect/VirtualMapLargeReconnectTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/platform-sdk/swirlds-merkle/src/test/java/com/swirlds/virtual/merkle/reconnect/VirtualMapLargeReconnectTest.java b/platform-sdk/swirlds-merkle/src/test/java/com/swirlds/virtual/merkle/reconnect/VirtualMapLargeReconnectTest.java index 4266a5c2723e..72ebf4c6c468 100644 --- a/platform-sdk/swirlds-merkle/src/test/java/com/swirlds/virtual/merkle/reconnect/VirtualMapLargeReconnectTest.java +++ b/platform-sdk/swirlds-merkle/src/test/java/com/swirlds/virtual/merkle/reconnect/VirtualMapLargeReconnectTest.java @@ -62,6 +62,8 @@ void largeTeacherLargerLearnerPermutations(int teacherStart, int teacherEnd, int @Tags({@Tag("VirtualMerkle"), @Tag("Reconnect"), @Tag("VMAP-005"), @Tag("VMAP-006")}) @Tag(TIME_CONSUMING) @DisplayName("Reconnect aborts 3 times before success") + // FUTURE WORK: https://github.com/hashgraph/hedera-services/issues/11507 + @Disabled void multipleAbortedReconnectsCanSucceed(int teacherStart, int teacherEnd, int learnerStart, int learnerEnd) { for (int i = teacherStart; i < teacherEnd; i++) { teacherMap.put(new TestKey(i), new TestValue(i)); From 484426838071eb3650c4704f9b234764e49be4e1 Mon Sep 17 00:00:00 2001 From: lukelee-sl <109538178+lukelee-sl@users.noreply.github.com> Date: Wed, 6 Mar 2024 12:33:34 -0800 Subject: [PATCH 027/115] chore: update design doc for atomic crypto transfer (#11918) Signed-off-by: lukelee-sl --- .../services/smart-contract-service/atomic-crypto-transfer.md | 1 - 1 file changed, 1 deletion(-) diff --git a/hedera-node/docs/design/services/smart-contract-service/atomic-crypto-transfer.md b/hedera-node/docs/design/services/smart-contract-service/atomic-crypto-transfer.md index e9251d57da27..53447d66eed0 100644 --- a/hedera-node/docs/design/services/smart-contract-service/atomic-crypto-transfer.md +++ b/hedera-node/docs/design/services/smart-contract-service/atomic-crypto-transfer.md @@ -66,7 +66,6 @@ are already implemented and need not be repeated as XTests. - Successful transfer of hbars only and HTS tokens only between accounts from sender account via contract. - Successful transfer of hbars and HTS tokens with a custom fees (including fallback fee scenarios). - Successful transfer of hbars and HTS tokens with available auto token association slots on the receiver. -- Successful transfer of hbars and HTS tokens from EOA account given approval to another EOA. - Successful transfer of hbars and HTS tokens from EOA account given approval to a caller contract. - Successful transfer of hbars and HTS tokens from owner contract via transfer contract. From fd5d21dcd9b4137f32d9d065f8b902d3410a3557 Mon Sep 17 00:00:00 2001 From: Matt Hess Date: Wed, 6 Mar 2024 15:17:01 -0600 Subject: [PATCH 028/115] chore: Bump services version to current cycle (0.49.x) (#11931) Signed-off-by: Matt Hess --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 864059b0c03f..76c89d759fcd 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.47.0-SNAPSHOT +0.49.0-SNAPSHOT From 9c2e7eff4c905ad2fc50344983bc6aad126cf6de Mon Sep 17 00:00:00 2001 From: anthony-swirldslabs <152534762+anthony-swirldslabs@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:12:16 -0800 Subject: [PATCH 029/115] fix: pbj-221: upgrade PBJ dependency to 0.7.22 (#11934) Signed-off-by: Anthony Petrov --- hedera-dependency-versions/build.gradle.kts | 2 +- settings.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hedera-dependency-versions/build.gradle.kts b/hedera-dependency-versions/build.gradle.kts index acecd85136bc..fbda7d9a5ecd 100644 --- a/hedera-dependency-versions/build.gradle.kts +++ b/hedera-dependency-versions/build.gradle.kts @@ -58,7 +58,7 @@ moduleInfo { version("com.google.jimfs", "1.2") version("com.google.protobuf", protobufVersion) version("com.google.protobuf.util", protobufVersion) - version("com.hedera.pbj.runtime", "0.7.20") + version("com.hedera.pbj.runtime", "0.7.22") version("com.squareup.javapoet", "1.13.0") version("com.sun.jna", "5.12.1") version("dagger", daggerVersion) diff --git a/settings.gradle.kts b/settings.gradle.kts index 3a583bd2a1c4..9676b6923fa8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -150,6 +150,6 @@ dependencyResolutionManagement { version("grpc-proto", "1.45.1") version("hapi-proto", hapiProtoVersion) - plugin("pbj", "com.hedera.pbj.pbj-compiler").version("0.7.20") + plugin("pbj", "com.hedera.pbj.pbj-compiler").version("0.7.22") } } From f961b52e8c0b67a7af4efe2c428b555f193bc7dc Mon Sep 17 00:00:00 2001 From: Neeharika Sompalli <52669918+Neeharika-Sompalli@users.noreply.github.com> Date: Thu, 7 Mar 2024 06:32:10 -0600 Subject: [PATCH 030/115] chore: Fix issues related to upgrade (#11884) Signed-off-by: Neeharika-Sompalli Signed-off-by: Michael Tinker Co-authored-by: Michael Tinker --- .../node/app/spi/state/MigrationContext.java | 9 + .../node/app/spi/workflows/HandleContext.java | 7 + hedera-node/hedera-app/build.gradle.kts | 1 + .../main/java/com/hedera/node/app/Hedera.java | 51 ++-- .../node/app/HederaInjectionComponent.java | 9 + .../com/hedera/node/app/fees/FeeService.java | 2 +- .../hedera/node/app/ids/EntityIdService.java | 2 +- .../node/app/records/BlockRecordService.java | 2 +- .../app/services/ServicesRegistryImpl.java | 9 +- .../app/state/HederaStateInjectionModule.java | 12 + .../node/app/state/PlatformStateAccessor.java | 39 +++ .../state/listeners/ReconnectListener.java | 80 ++++++ .../listeners/WriteStateToDiskListener.java | 89 +++++++ .../state/merkle/MerkleSchemaRegistry.java | 24 +- .../throttle/CongestionThrottleService.java | 2 +- .../workflows/handle/HandleContextImpl.java | 16 +- .../app/workflows/handle/HandleWorkflow.java | 3 +- .../handle/record/MigrationContextImpl.java | 11 +- .../hedera-app/src/main/java/module-info.java | 31 +-- .../node/app/PlatformStateAccessorTest.java | 48 ++++ .../merkle/MerkleSchemaRegistryTest.java | 2 +- .../handle/HandleContextImplTest.java | 71 +++++- .../handle/record/BlockRecordServiceTest.java | 2 +- .../fixtures/state/FakeSchemaRegistry.java | 8 + .../java/common/BaseScaffoldingModule.java | 7 +- .../schemas/InitialModFileGenesisSchema.java | 2 +- .../impl/test/schemas/FileSchemaTest.java | 3 +- .../app/service/mono/pbj/PbjConverter.java | 4 + .../build.gradle.kts | 1 + .../impl/WritableFreezeStore.java | 14 +- .../impl/handlers/FreezeHandler.java | 9 +- .../impl/handlers/FreezeUpgradeActions.java | 134 +--------- .../ReadableFreezeUpgradeActions.java | 226 +++++++++++++++++ .../schemas/InitialModServiceAdminSchema.java | 2 +- .../src/main/java/module-info.java | 2 +- .../impl/test/FreezeServiceImplTest.java | 3 +- .../impl/test/WritableFreezeStoreTest.java | 6 +- .../impl/test/handlers/FreezeHandlerTest.java | 3 + .../handlers/FreezeUpgradeActionsTest.java | 104 ++------ .../ReadableFreezeUpgradeActionsTest.java | 232 ++++++++++++++++++ .../schemas/InitialModServiceTokenSchema.java | 34 ++- .../InitialModServiceTokenSchemaTest.java | 102 ++++++-- 42 files changed, 1093 insertions(+), 325 deletions(-) create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/PlatformStateAccessor.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/listeners/ReconnectListener.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/listeners/WriteStateToDiskListener.java create mode 100644 hedera-node/hedera-app/src/test/java/com/hedera/node/app/PlatformStateAccessorTest.java create mode 100644 hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/handlers/ReadableFreezeUpgradeActions.java create mode 100644 hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/handlers/ReadableFreezeUpgradeActionsTest.java diff --git a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/state/MigrationContext.java b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/state/MigrationContext.java index 65dff2dd69e3..9fc02bcca894 100644 --- a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/state/MigrationContext.java +++ b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/state/MigrationContext.java @@ -16,10 +16,12 @@ package com.hedera.node.app.spi.state; +import com.hedera.hapi.node.base.SemanticVersion; import com.hedera.node.app.spi.info.NetworkInfo; import com.hedera.node.app.spi.workflows.record.GenesisRecordsBuilder; import com.swirlds.config.api.Configuration; import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; /** * Provides the context for a migration of state from one {@link Schema} version to another. @@ -88,4 +90,11 @@ public interface MigrationContext { * @param stateKey the key of the state to copy and release */ void copyAndReleaseOnDiskState(String stateKey); + + /** + * Provides the previous version of the schema. This is useful to know if this is genesis restart + * @return the previous version of the schema. Previous version will be null if this is genesis restart + */ + @Nullable + SemanticVersion previousVersion(); } diff --git a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/workflows/HandleContext.java b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/workflows/HandleContext.java index 6cd3b6e5c201..20b843069dfa 100644 --- a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/workflows/HandleContext.java +++ b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/workflows/HandleContext.java @@ -763,4 +763,11 @@ static void throwIfMissingPayerId(@NonNull final TransactionBody body) { throw new IllegalArgumentException("Transaction id must be set if dispatching without an explicit payer"); } } + + /** + * Returns the freeze time from state, if it is set. + * @return the freeze time, if it is set + */ + @Nullable + Instant freezeTime(); } diff --git a/hedera-node/hedera-app/build.gradle.kts b/hedera-node/hedera-app/build.gradle.kts index e824670f52b3..f0a41ee87c2a 100644 --- a/hedera-node/hedera-app/build.gradle.kts +++ b/hedera-node/hedera-app/build.gradle.kts @@ -100,6 +100,7 @@ xtestModuleInfo { requires("com.swirlds.config.api") requires("com.swirlds.config.extensions.test.fixtures") requires("com.swirlds.metrics.api") + requires("com.swirlds.platform.core") requires("dagger") requires("headlong") requires("javax.inject") 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 12cb65ce38fe..f2d5d4807e79 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 @@ -112,6 +112,8 @@ import com.swirlds.fcqueue.FCQueue; import com.swirlds.merkle.map.MerkleMap; import com.swirlds.platform.listeners.PlatformStatusChangeListener; +import com.swirlds.platform.listeners.ReconnectCompleteListener; +import com.swirlds.platform.listeners.StateWriteToDiskCompleteListener; import com.swirlds.platform.state.PlatformState; import com.swirlds.platform.system.InitTrigger; import com.swirlds.platform.system.Platform; @@ -585,9 +587,9 @@ public void onStateInitialized( // here. This is intentional so as to avoid forgetting to handle a new trigger. try { switch (trigger) { - case GENESIS -> genesis(state); - case RECONNECT -> reconnect(state, deserializedVersion); - case RESTART, EVENT_STREAM_RECOVERY -> restart(state, deserializedVersion, trigger); + case GENESIS -> genesis(state, platformState); + case RECONNECT -> reconnect(state, deserializedVersion, platformState); + case RESTART, EVENT_STREAM_RECOVERY -> restart(state, deserializedVersion, trigger, platformState); } } catch (final Throwable th) { logger.fatal("Critical failure during initialization", th); @@ -760,8 +762,6 @@ public void init(@NonNull final Platform platform, @NonNull final NodeId nodeId) } } }); - - // TBD: notifications.register(ReconnectCompleteListener.class, daggerApp.reconnectListener()); // The main job of the reconnect listener (com.hedera.node.app.service.mono.state.logic.ReconnectListener) // is to log some output (including hashes from the tree for the main state per service) and then to // "catchUpOnMissedSideEffects". This last part worries me, because it looks like it invades into the space @@ -772,13 +772,9 @@ public void init(@NonNull final Platform platform, @NonNull final NodeId nodeId) // ANSWER: We need to look and see if there is an update to the upgrade file that happened on other nodes // that we reconnected with. In that case, we need to save the file to disk. Similar to how we have to hook // for all the other special files on restart / genesis / reconnect. - - // TBD: notifications.register(StateWriteToDiskCompleteListener.class, - // It looks like this notification is handled by - // com.hedera.node.app.service.mono.state.logic.StateWriteToDiskListener - // which looks like it is related to freeze / upgrade. - // daggerApp.stateWriteToDiskListener()); - // see issue #8660 + notifications.register(ReconnectCompleteListener.class, daggerApp.reconnectListener()); + // This notifaction is needed for freeze / upgrade. + notifications.register(StateWriteToDiskCompleteListener.class, daggerApp.stateWriteToDiskListener()); // TBD: notifications.register(NewSignedStateListener.class, daggerApp.newSignedStateListener()); // com.hedera.node.app.service.mono.state.exports.NewSignedStateListener @@ -896,6 +892,7 @@ public void onNewRecoveredState(@NonNull final MerkleHederaState recoveredState) public void onHandleConsensusRound( @NonNull final Round round, @NonNull final PlatformState platformState, @NonNull final HederaState state) { daggerApp.workingStateAccessor().setHederaState(state); + daggerApp.platformStateAccessor().setPlatformState(platformState); daggerApp.handleWorkflow().handleRound(state, platformState, round); } @@ -930,12 +927,12 @@ public void shutdownGrpcServer() { /** * Implements the code flow for initializing the state of a new Hedera node with NO SAVED STATE. */ - private void genesis(@NonNull final MerkleHederaState state) { + private void genesis(@NonNull final MerkleHederaState state, @NonNull final PlatformState platformState) { logger.debug("Genesis Initialization"); // Create all the nodes in the merkle tree for all the services onMigrate(state, null, GENESIS); // Now that we have the state created, we are ready to create the dependency graph with Dagger - initializeDagger(state, GENESIS); + initializeDagger(state, GENESIS, platformState); // And now that the entire dependency graph has been initialized, and we have config, and all migration has // been completed, we are prepared to initialize in-memory data structures. These specifically are loaded // from information held in state (especially those in special files). @@ -955,8 +952,9 @@ private void genesis(@NonNull final MerkleHederaState state) { private void restart( @NonNull final MerkleHederaState state, @Nullable final HederaSoftwareVersion deserializedVersion, - @NonNull final InitTrigger trigger) { - initializeForTrigger(state, deserializedVersion, trigger); + @NonNull final InitTrigger trigger, + @NonNull final PlatformState platformState) { + initializeForTrigger(state, deserializedVersion, trigger, platformState); } /*================================================================================================================== @@ -968,18 +966,23 @@ private void restart( /** * The initialization needed for reconnect. It constructs all schemas appropriately. * These are exactly the same steps done as restart trigger. - * @param state The current state + * + * @param state The current state * @param deserializedVersion version of deserialized state + * @param platformState platform state */ private void reconnect( - @NonNull final MerkleHederaState state, @Nullable final HederaSoftwareVersion deserializedVersion) { - initializeForTrigger(state, deserializedVersion, RECONNECT); + @NonNull final MerkleHederaState state, + @Nullable final HederaSoftwareVersion deserializedVersion, + @NonNull final PlatformState platformState) { + initializeForTrigger(state, deserializedVersion, RECONNECT, platformState); } private void initializeForTrigger( @NonNull final MerkleHederaState state, @Nullable final HederaSoftwareVersion deserializedVersion, - @NonNull final InitTrigger trigger) { + @NonNull final InitTrigger trigger, + @NonNull final PlatformState platformState) { logger.info(trigger + " Initialization"); // The deserialized version can ONLY be null if we are in genesis, otherwise something is wrong with the state @@ -1002,7 +1005,7 @@ private void initializeForTrigger( } // Now that we have the state created, we are ready to create the dependency graph with Dagger - initializeDagger(state, trigger); + initializeDagger(state, trigger, platformState); // And now that the entire dependency graph has been initialized, and we have config, and all migration has // been completed, we are prepared to initialize in-memory data structures. These specifically are loaded @@ -1020,7 +1023,10 @@ private void initializeForTrigger( * =================================================================================================================*/ - private void initializeDagger(@NonNull final MerkleHederaState state, @NonNull final InitTrigger trigger) { + private void initializeDagger( + @NonNull final MerkleHederaState state, + @NonNull final InitTrigger trigger, + final PlatformState platformState) { logger.debug("Initializing dagger"); final var selfId = platform.getSelfId(); final var nodeAddress = platform.getAddressBook().getAddress(selfId); @@ -1042,6 +1048,7 @@ private void initializeDagger(@NonNull final MerkleHederaState state, @NonNull f .build(); daggerApp.workingStateAccessor().setHederaState(state); + daggerApp.platformStateAccessor().setPlatformState(platformState); } private boolean isDowngrade( 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 2925c8e4fac8..5b6d1de15b9d 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/HederaInjectionComponent.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/HederaInjectionComponent.java @@ -33,6 +33,7 @@ import com.hedera.node.app.records.BlockRecordManager; import com.hedera.node.app.service.mono.context.annotations.BootstrapProps; import com.hedera.node.app.service.mono.context.properties.PropertySource; +import com.hedera.node.app.service.mono.state.PlatformStateAccessor; import com.hedera.node.app.service.mono.utils.NamedDigestFactory; import com.hedera.node.app.service.mono.utils.SystemExits; import com.hedera.node.app.services.ServicesInjectionModule; @@ -53,6 +54,8 @@ import com.hedera.node.config.ConfigProvider; import com.swirlds.common.crypto.Cryptography; import com.swirlds.common.platform.NodeId; +import com.swirlds.platform.listeners.ReconnectCompleteListener; +import com.swirlds.platform.listeners.StateWriteToDiskCompleteListener; import com.swirlds.platform.system.InitTrigger; import com.swirlds.platform.system.Platform; import dagger.BindsInstance; @@ -124,6 +127,12 @@ public interface HederaInjectionComponent { ThrottleServiceManager throttleServiceManager(); + ReconnectCompleteListener reconnectListener(); + + StateWriteToDiskCompleteListener stateWriteToDiskListener(); + + PlatformStateAccessor platformStateAccessor(); + @Component.Builder interface Builder { diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/fees/FeeService.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/fees/FeeService.java index 4c00e6822cea..444c7cce3d85 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/fees/FeeService.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/fees/FeeService.java @@ -62,7 +62,7 @@ public Set statesToCreate() { @Override public void migrate(@NonNull final MigrationContext ctx) { - final var isGenesis = ctx.previousStates().isEmpty(); + final var isGenesis = ctx.previousVersion() == null; final var midnightRatesState = ctx.newStates().getSingleton(MIDNIGHT_RATES_STATE_KEY); if (isGenesis) { // Set the initial exchange rates (from the bootstrap config) as the midnight rates diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/ids/EntityIdService.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/ids/EntityIdService.java index 42b77749970d..dd000ffc39fd 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/ids/EntityIdService.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/ids/EntityIdService.java @@ -82,7 +82,7 @@ public void migrate(@NonNull MigrationContext ctx) { final var entityIdState = ctx.newStates().getSingleton(ENTITY_ID_STATE_KEY); final var config = ctx.configuration().getConfigData(HederaConfig.class); - final var isGenesis = ctx.previousStates().isEmpty(); + final var isGenesis = ctx.previousVersion() == null; if (isGenesis) { // Set the initial entity id to the first user entity minus one final var entityNum = config.firstUserEntity() - 1; diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/BlockRecordService.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/BlockRecordService.java index 2d4e4d1dd113..35a0f1e20f43 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/BlockRecordService.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/BlockRecordService.java @@ -82,7 +82,7 @@ public Set statesToCreate() { public void migrate(@NonNull final MigrationContext ctx) { final var runningHashState = ctx.newStates().getSingleton(RUNNING_HASHES_STATE_KEY); final var blocksState = ctx.newStates().getSingleton(BLOCK_INFO_STATE_KEY); - final var isGenesis = ctx.previousStates().isEmpty(); + final var isGenesis = ctx.previousVersion() == null; if (isGenesis) { final var blocks = new BlockInfo(-1, EPOCH, Bytes.EMPTY, EPOCH, false, EPOCH); blocksState.put(blocks); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/ServicesRegistryImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/ServicesRegistryImpl.java index 0e4d3985b662..11ec0c8f6990 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/ServicesRegistryImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/ServicesRegistryImpl.java @@ -18,6 +18,7 @@ import static java.util.Objects.requireNonNull; +import com.hedera.hapi.node.base.SemanticVersion; import com.hedera.node.app.spi.Service; import com.hedera.node.app.spi.workflows.record.GenesisRecordsBuilder; import com.hedera.node.app.state.merkle.MerkleSchemaRegistry; @@ -38,6 +39,12 @@ @Singleton public final class ServicesRegistryImpl implements ServicesRegistry { private static final Logger logger = LogManager.getLogger(ServicesRegistryImpl.class); + /** + * Use a constant version to be passed to the schema registration. + * If the version changes the class id will be different and the upgrade will have issues. + */ + private final SemanticVersion VERSION = + SemanticVersion.newBuilder().major(0).minor(48).patch(0).build(); /** We have to register with the {@link ConstructableRegistry} based on the schemas of the services */ private final ConstructableRegistry constructableRegistry; /** The set of registered services */ @@ -67,7 +74,7 @@ public void register(@NonNull final Service service, final HederaSoftwareVersion logger.debug("Registering schemas for service {}", serviceName); final var registry = new MerkleSchemaRegistry(constructableRegistry, serviceName, genesisRecords); - service.registerSchemas(registry, version.getServicesVersion()); + service.registerSchemas(registry, VERSION); entries.add(new Registration(service, registry)); logger.info("Registered service {} with implementation {}", service.getServiceName(), service.getClass()); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/HederaStateInjectionModule.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/HederaStateInjectionModule.java index 68384ceb8bcc..9f2acd4f4514 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/HederaStateInjectionModule.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/HederaStateInjectionModule.java @@ -17,8 +17,12 @@ package com.hedera.node.app.state; import com.hedera.node.app.spi.records.RecordCache; +import com.hedera.node.app.state.listeners.ReconnectListener; +import com.hedera.node.app.state.listeners.WriteStateToDiskListener; import com.hedera.node.app.state.recordcache.DeduplicationCacheImpl; import com.hedera.node.app.state.recordcache.RecordCacheImpl; +import com.swirlds.platform.listeners.ReconnectCompleteListener; +import com.swirlds.platform.listeners.StateWriteToDiskCompleteListener; import dagger.Binds; import dagger.Module; import dagger.Provides; @@ -43,4 +47,12 @@ public interface HederaStateInjectionModule { static WorkingStateAccessor provideWorkingStateAccessor() { return new WorkingStateAccessor(); } + + @Binds + @Singleton + ReconnectCompleteListener bindReconnectListener(ReconnectListener reconnectListener); + + @Binds + @Singleton + StateWriteToDiskCompleteListener bindStateWrittenToDiskListener(WriteStateToDiskListener writeStateToDiskListener); } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/PlatformStateAccessor.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/PlatformStateAccessor.java new file mode 100644 index 000000000000..d357b482d2f6 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/PlatformStateAccessor.java @@ -0,0 +1,39 @@ +/* + * 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.node.app.state; + +import com.swirlds.platform.state.PlatformState; +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +public class PlatformStateAccessor { + private PlatformState platformState = null; + + @Inject + public PlatformStateAccessor() { + // Default constructor + } + + public PlatformState getPlatformState() { + return platformState; + } + + public void setPlatformState(PlatformState platformState) { + this.platformState = platformState; + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/listeners/ReconnectListener.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/listeners/ReconnectListener.java new file mode 100644 index 000000000000..28e1626dc1ed --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/listeners/ReconnectListener.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2021-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.state.listeners; + +import com.hedera.node.app.service.file.ReadableUpgradeFileStore; +import com.hedera.node.app.service.networkadmin.ReadableFreezeStore; +import com.hedera.node.app.service.networkadmin.impl.handlers.ReadableFreezeUpgradeActions; +import com.hedera.node.app.state.HederaState; +import com.hedera.node.app.state.PlatformStateAccessor; +import com.hedera.node.app.workflows.dispatcher.ReadableStoreFactory; +import com.hedera.node.config.ConfigProvider; +import com.hedera.node.config.data.NetworkAdminConfig; +import com.swirlds.common.utility.AutoCloseableWrapper; +import com.swirlds.platform.listeners.ReconnectCompleteListener; +import com.swirlds.platform.listeners.ReconnectCompleteNotification; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.concurrent.Executor; +import java.util.function.Supplier; +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +@Singleton +public class ReconnectListener implements ReconnectCompleteListener { + private static final Logger log = LogManager.getLogger(ReconnectListener.class); + + private final Supplier> stateAccessor; + private final Executor executor; + private final ConfigProvider configProvider; + private final PlatformStateAccessor platformStateAccessor; + + @Inject + public ReconnectListener( + @NonNull final Supplier> stateAccessor, + @NonNull @Named("FreezeService") final Executor executor, + @NonNull final ConfigProvider configProvider, + @NonNull final PlatformStateAccessor platformStateAccessor) { + + this.stateAccessor = stateAccessor; + this.executor = executor; + this.configProvider = configProvider; + this.platformStateAccessor = platformStateAccessor; + } + + @Override + public void notify(final ReconnectCompleteNotification notification) { + log.info( + "ReconnectCompleteNotification Received: Reconnect Finished. " + + "consensusTimestamp: {}, roundNumber: {}, sequence: {}", + notification.getConsensusTimestamp(), + notification.getRoundNumber(), + notification.getSequence()); + try (final var wrappedState = stateAccessor.get()) { + final var readableStoreFactory = new ReadableStoreFactory(wrappedState.get()); + final var networkAdminConfig = configProvider.getConfiguration().getConfigData(NetworkAdminConfig.class); + final var freezeStore = readableStoreFactory.getStore(ReadableFreezeStore.class); + final var upgradeFileStore = readableStoreFactory.getStore(ReadableUpgradeFileStore.class); + final var upgradeActions = + new ReadableFreezeUpgradeActions(networkAdminConfig, freezeStore, executor, upgradeFileStore); + upgradeActions.catchUpOnMissedSideEffects( + platformStateAccessor.getPlatformState().getFreezeTime()); + } + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/listeners/WriteStateToDiskListener.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/listeners/WriteStateToDiskListener.java new file mode 100644 index 000000000000..234cb2a6ef63 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/listeners/WriteStateToDiskListener.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2021-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.state.listeners; + +import static java.util.Objects.requireNonNull; + +import com.hedera.node.app.service.file.ReadableUpgradeFileStore; +import com.hedera.node.app.service.networkadmin.ReadableFreezeStore; +import com.hedera.node.app.service.networkadmin.impl.handlers.ReadableFreezeUpgradeActions; +import com.hedera.node.app.state.HederaState; +import com.hedera.node.app.workflows.dispatcher.ReadableStoreFactory; +import com.hedera.node.config.ConfigProvider; +import com.hedera.node.config.data.NetworkAdminConfig; +import com.swirlds.common.utility.AutoCloseableWrapper; +import com.swirlds.platform.listeners.StateWriteToDiskCompleteListener; +import com.swirlds.platform.listeners.StateWriteToDiskCompleteNotification; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.concurrent.Executor; +import java.util.function.Supplier; +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Listener that will be notified with {@link + * StateWriteToDiskCompleteNotification} when state is + * written to disk. This writes {@code NOW_FROZEN_MARKER} to disk when upgrade is pending + */ +@Singleton +public class WriteStateToDiskListener implements StateWriteToDiskCompleteListener { + private static final Logger log = LogManager.getLogger(WriteStateToDiskListener.class); + + private final Supplier> stateAccessor; + private final Executor executor; + private final ConfigProvider configProvider; + + @Inject + public WriteStateToDiskListener( + @NonNull final Supplier> stateAccessor, + @NonNull @Named("FreezeService") final Executor executor, + @NonNull final ConfigProvider configProvider) { + requireNonNull(stateAccessor); + requireNonNull(executor); + requireNonNull(configProvider); + this.stateAccessor = stateAccessor; + this.executor = executor; + this.configProvider = configProvider; + } + + @Override + public void notify(final StateWriteToDiskCompleteNotification notification) { + if (notification.isFreezeState()) { + log.info( + "StateWriteToDiskCompleteNotification Received : Freeze State Finished. " + + "consensusTimestamp: {}, roundNumber: {}, sequence: {}", + notification.getConsensusTimestamp(), + notification.getRoundNumber(), + notification.getSequence()); + try (final var wrappedState = stateAccessor.get()) { + final var readableStoreFactory = new ReadableStoreFactory(wrappedState.get()); + final var readableFreezeStore = readableStoreFactory.getStore(ReadableFreezeStore.class); + final var readableUpgradeFileStore = readableStoreFactory.getStore(ReadableUpgradeFileStore.class); + final var networkAdminConfig = + configProvider.getConfiguration().getConfigData(NetworkAdminConfig.class); + + final var upgradeActions = new ReadableFreezeUpgradeActions( + networkAdminConfig, readableFreezeStore, executor, readableUpgradeFileStore); + log.info("Externalizing freeze if upgrade is pending"); + upgradeActions.externalizeFreezeIfUpgradePending(); + } + } + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/merkle/MerkleSchemaRegistry.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/merkle/MerkleSchemaRegistry.java index 252550f7fc5a..c32384f935b3 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/merkle/MerkleSchemaRegistry.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/merkle/MerkleSchemaRegistry.java @@ -27,6 +27,7 @@ import com.hedera.node.app.spi.state.FilteredReadableStates; import com.hedera.node.app.spi.state.FilteredWritableStates; import com.hedera.node.app.spi.state.MigrationContext; +import com.hedera.node.app.spi.state.ReadableStates; import com.hedera.node.app.spi.state.Schema; import com.hedera.node.app.spi.state.SchemaRegistry; import com.hedera.node.app.spi.state.StateDefinition; @@ -57,7 +58,7 @@ import java.util.Comparator; import java.util.HashSet; import java.util.List; -import java.util.Set; +import java.util.SortedSet; import java.util.TreeSet; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -89,7 +90,7 @@ public class MerkleSchemaRegistry implements SchemaRegistry { /** * The ordered set of all schemas registered by the service */ - private final Set schemas = new TreeSet<>(); + private final SortedSet schemas = new TreeSet<>(); /** * Stores system entities created during genesis until the node can build synthetic records */ @@ -183,6 +184,7 @@ public void migrate( for (final var schema : schemasToApply) { final var applicationType = checkApplicationType(previousVersion, latestVersion, schema); logger.info("Applying {} schema {} ({})", serviceName, schema.getVersion(), applicationType); + // Now we can migrate the schema and then commit all the changes // We just have one merkle tree -- the just-loaded working tree -- to work from. // We get a ReadableStates for everything in the current tree, but then wrap @@ -190,8 +192,11 @@ public void migrate( // available at this moment in time. This is done to make sure that even after we // add new states into the tree, it doesn't increase the number of states that can // be seen by the schema migration code - final var readableStates = hederaState.getReadableStates(serviceName); - final var previousStates = new FilteredReadableStates(readableStates, readableStates.stateKeys()); + ReadableStates previousStatesIfNeeded = null; + if (applicationType != SchemaApplicationType.ONLY_STATE_MANAGEMENT) { + final var readableStates = hederaState.getReadableStates(serviceName); + previousStatesIfNeeded = new FilteredReadableStates(readableStates, readableStates.stateKeys()); + } // Create the new states (based on the schema) which, thanks to the above, does not // expand the set of states that the migration code will see @@ -245,7 +250,13 @@ public void migrate( // MigrationContext API so that only changes explicitly specified in the // interface can be made (instead of allowing any arbitrary state change). final var migrationContext = new MigrationContextImpl( - previousStates, newStates, config, networkInfo, genesisRecordsBuilder, entityIdStore); + requireNonNull(previousStatesIfNeeded), + newStates, + config, + networkInfo, + genesisRecordsBuilder, + entityIdStore, + previousVersion); if (applicationType != SchemaApplicationType.RESTART_ONLY) { schema.migrate(migrationContext); } @@ -337,7 +348,8 @@ private List computeApplicableSchemas( applicableSchemas.add(schema); } } - return applicableSchemas; + final List registeredSchemas = schemas.isEmpty() ? List.of() : List.of(schemas.getLast()); + return applicableSchemas.isEmpty() ? registeredSchemas : applicableSchemas; } /** diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/throttle/CongestionThrottleService.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/throttle/CongestionThrottleService.java index a51d896f02c6..ca8ee20b2421 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/throttle/CongestionThrottleService.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/throttle/CongestionThrottleService.java @@ -59,7 +59,7 @@ public Set statesToCreate() { /** {@inheritDoc} */ @Override public void migrate(@NonNull final MigrationContext ctx) { - if (ctx.previousStates().isEmpty()) { + if (ctx.previousVersion() == null) { // At genesis we put empty throttle usage snapshots and // congestion level starts into their respective singleton // states just to ensure they exist diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleContextImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleContextImpl.java index 418e6eae1b08..0e8eb8b703df 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleContextImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleContextImpl.java @@ -96,6 +96,7 @@ import com.hedera.node.app.workflows.prehandle.PreHandleContextImpl; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.config.api.Configuration; +import com.swirlds.platform.state.PlatformState; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.time.Instant; @@ -148,6 +149,7 @@ public class HandleContextImpl implements HandleContext, FeeContext { private AttributeValidator attributeValidator; private ExpiryValidator expiryValidator; private ExchangeRateInfo exchangeRateInfo; + private PlatformState platformState; /** * Constructs a {@link HandleContextImpl}. @@ -175,6 +177,7 @@ public class HandleContextImpl implements HandleContext, FeeContext { * @param childRecordFinalizer The {@link ChildRecordFinalizer} used to finalize child records * @param networkUtilizationManager The {@link NetworkUtilizationManager} used to manage the tracking of backend network throttling * @param synchronizedThrottleAccumulator The {@link SynchronizedThrottleAccumulator} used to manage the tracking of frontend network throttling + * @param platformState The {@link PlatformState} of the node */ public HandleContextImpl( @NonNull final TransactionBody txBody, @@ -201,7 +204,8 @@ public HandleContextImpl( @NonNull final SolvencyPreCheck solvencyPreCheck, @NonNull final ChildRecordFinalizer childRecordFinalizer, @NonNull final NetworkUtilizationManager networkUtilizationManager, - @NonNull final SynchronizedThrottleAccumulator synchronizedThrottleAccumulator) { + @NonNull final SynchronizedThrottleAccumulator synchronizedThrottleAccumulator, + @NonNull final PlatformState platformState) { this.txBody = requireNonNull(txBody, "txBody must not be null"); this.functionality = requireNonNull(functionality, "functionality must not be null"); this.payer = requireNonNull(payer, "payer must not be null"); @@ -253,6 +257,7 @@ public HandleContextImpl( this.exchangeRateManager = requireNonNull(exchangeRateManager, "exchangeRateManager must not be null"); this.solvencyPreCheck = requireNonNull(solvencyPreCheck, "solvencyPreCheck must not be null"); + this.platformState = requireNonNull(platformState, "platformState must not be null"); } private WrappedHederaState current() { @@ -718,7 +723,8 @@ private void dispatchSyntheticTxn( solvencyPreCheck, childRecordFinalizer, networkUtilizationManager, - synchronizedThrottleAccumulator); + synchronizedThrottleAccumulator, + platformState); // in order to work correctly isSuperUser(), we need to keep track of top level payer in child context childContext.setTopLevelPayer(topLevelPayer); @@ -1011,4 +1017,10 @@ private record DispatchValidationResult(@NonNull Key key, @NonNull Fees fees) { private void setTopLevelPayer(@NonNull AccountID topLevelPayer) { this.topLevelPayer = requireNonNull(topLevelPayer, "payer must not be null"); } + + @Nullable + @Override + public Instant freezeTime() { + return platformState.getFreezeTime(); + } } 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 7d96f8bb934b..6573a749e564 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 @@ -419,7 +419,8 @@ private void handleUserTransaction( solvencyPreCheck, childRecordFinalizer, networkUtilizationManager, - synchronizedThrottleAccumulator); + synchronizedThrottleAccumulator, + platformState); // Calculate the fee fees = dispatcher.dispatchComputeFees(context); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/MigrationContextImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/MigrationContextImpl.java index 194f27b4d11f..159e6164ee25 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/MigrationContextImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/MigrationContextImpl.java @@ -18,6 +18,7 @@ import static java.util.Objects.requireNonNull; +import com.hedera.hapi.node.base.SemanticVersion; import com.hedera.node.app.ids.WritableEntityIdStore; import com.hedera.node.app.spi.info.NetworkInfo; import com.hedera.node.app.spi.state.FilteredWritableStates; @@ -33,13 +34,14 @@ /** * An implementation of {@link MigrationContext}. * - * @param previousStates The previous states. - * @param newStates The new states, preloaded with any new state definitions. - * @param configuration The configuration to use + * @param previousStates The previous states. + * @param newStates The new states, preloaded with any new state definitions. + * @param configuration The configuration to use * @param genesisRecordsBuilder The instance responsible for genesis records * @param writableEntityIdStore The instance responsible for generating new entity IDs (ONLY during * migrations). Note that this is nullable only because it cannot exist * when the entity ID service itself is being migrated + * @param previousVersion */ public record MigrationContextImpl( @NonNull ReadableStates previousStates, @@ -47,7 +49,8 @@ public record MigrationContextImpl( @NonNull Configuration configuration, @NonNull NetworkInfo networkInfo, @NonNull GenesisRecordsBuilder genesisRecordsBuilder, - @Nullable WritableEntityIdStore writableEntityIdStore) + @Nullable WritableEntityIdStore writableEntityIdStore, + @Nullable SemanticVersion previousVersion) implements MigrationContext { public MigrationContextImpl { 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 7fe840e1ebbf..72a5201b56ad 100644 --- a/hedera-node/hedera-app/src/main/java/module-info.java +++ b/hedera-node/hedera-app/src/main/java/module-info.java @@ -48,55 +48,34 @@ requires static java.compiler; // javax.annotation.processing.Generated exports com.hedera.node.app to - com.swirlds.platform.core, com.hedera.node.test.clients; exports com.hedera.node.app.state to - com.swirlds.common, com.hedera.node.app.test.fixtures; exports com.hedera.node.app.workflows to com.hedera.node.app.test.fixtures; exports com.hedera.node.app.state.merkle to - com.hedera.node.services.cli, - com.swirlds.common; + com.hedera.node.services.cli; exports com.hedera.node.app.state.merkle.disk to - com.swirlds.common, com.hedera.node.services.cli; exports com.hedera.node.app.state.merkle.memory to - com.hedera.node.services.cli, - com.swirlds.common; - exports com.hedera.node.app.state.merkle.singleton to - com.swirlds.common; - exports com.hedera.node.app.authorization to - com.swirlds.platform.core; - exports com.hedera.node.app.fees to - com.swirlds.platform.core; - exports com.hedera.node.app.fees.congestion to - com.swirlds.platform.core; - exports com.hedera.node.app.throttle to - com.swirlds.platform.core; + com.hedera.node.services.cli; exports com.hedera.node.app.workflows.dispatcher; exports com.hedera.node.app.config; exports com.hedera.node.app.workflows.handle.validation; - exports com.hedera.node.app.state.recordcache to - com.swirlds.common; - exports com.hedera.node.app.services to - com.swirlds.platform.core; exports com.hedera.node.app.signature to com.hedera.node.app.test.fixtures; exports com.hedera.node.app.info to - com.hedera.node.app.test.fixtures, - com.swirlds.common, - com.swirlds.platform.core; + com.hedera.node.app.test.fixtures; exports com.hedera.node.app.workflows.handle to com.hedera.node.app.test.fixtures; exports com.hedera.node.app.workflows.handle.record to com.hedera.node.app.test.fixtures; exports com.hedera.node.app.state.merkle.queue to - com.swirlds.common, com.swirlds.platform; exports com.hedera.node.app.version to com.hedera.node.app.test.fixtures, - com.swirlds.common, com.swirlds.platform; exports com.hedera.node.app.validation; + exports com.hedera.node.app.state.listeners to + com.hedera.node.app.test.fixtures; } diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/PlatformStateAccessorTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/PlatformStateAccessorTest.java new file mode 100644 index 000000000000..0e381b89767f --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/PlatformStateAccessorTest.java @@ -0,0 +1,48 @@ +/* + * 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.node.app; + +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.hedera.node.app.state.PlatformStateAccessor; +import com.swirlds.platform.state.PlatformState; +import org.junit.jupiter.api.Assertions; +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 PlatformStateAccessorTest { + @Mock + private PlatformState platformState; + + @Test + void beanMethodsWork() { + // setup: + final var subject = new PlatformStateAccessor(); + + // expect: + assertNull(subject.getPlatformState()); + + // and when: + subject.setPlatformState(platformState); + + // expect: + Assertions.assertSame(platformState, subject.getPlatformState()); + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/merkle/MerkleSchemaRegistryTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/merkle/MerkleSchemaRegistryTest.java index 531be90d0a38..850adb907967 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/merkle/MerkleSchemaRegistryTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/merkle/MerkleSchemaRegistryTest.java @@ -374,7 +374,7 @@ public Set statesToCreate() { @Override public void migrate(@NonNull final MigrationContext ctx) { assertThat(ctx).isNotNull(); - assertThat(ctx.previousStates().isEmpty()).isTrue(); + assertThat(ctx.previousVersion()).isNull(); assertThat(ctx.newStates().size()).isEqualTo(1); final WritableKVState fruit = ctx.newStates().get(FRUIT_STATE_KEY); diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/HandleContextImplTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/HandleContextImplTest.java index fcf40caefd99..638306de5120 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/HandleContextImplTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/HandleContextImplTest.java @@ -100,6 +100,7 @@ import com.hedera.node.config.testfixtures.HederaTestConfigBuilder; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.config.api.Configuration; +import com.swirlds.platform.state.PlatformState; import java.lang.reflect.InvocationTargetException; import java.time.Instant; import java.util.Arrays; @@ -189,6 +190,9 @@ class HandleContextImplTest extends StateTestBase implements Scenarios { @Mock private SelfNodeInfo selfNodeInfo; + @Mock + private PlatformState platformState; + @BeforeEach void setup() { when(serviceScopeLookup.getServiceName(any())).thenReturn(TokenService.NAME); @@ -240,7 +244,8 @@ private HandleContextImpl createContext(final TransactionBody txBody) { solvencyPreCheck, childRecordFinalizer, networkUtilizationManager, - synchronizedThrottleAccumulator); + synchronizedThrottleAccumulator, + platformState); } @SuppressWarnings("ConstantConditions") @@ -271,7 +276,8 @@ void testConstructorWithInvalidArguments() { solvencyPreCheck, childRecordFinalizer, networkUtilizationManager, - synchronizedThrottleAccumulator + synchronizedThrottleAccumulator, + platformState }; final var constructor = HandleContextImpl.class.getConstructors()[0]; @@ -401,7 +407,8 @@ void setUp() { solvencyPreCheck, childRecordFinalizer, networkUtilizationManager, - synchronizedThrottleAccumulator); + synchronizedThrottleAccumulator, + platformState); } @Test @@ -457,6 +464,61 @@ void testPeekingAtNewEntityNum() { } } + @Nested + @DisplayName("Getters work as expected") + final class GettersWork { + + @Mock + private WritableStates writableStates; + + private HandleContext handleContext; + + @BeforeEach + void setUp() { + final var payer = ALICE.accountID(); + final var payerKey = ALICE.account().keyOrThrow(); + when(stack.getWritableStates(EntityIdService.NAME)).thenReturn(writableStates); + when(stack.getWritableStates(TokenService.NAME)) + .thenReturn(MapWritableStates.builder() + .state(MapWritableKVState.builder("ACCOUNTS").build()) + .state(MapWritableKVState.builder("ALIASES").build()) + .build()); + handleContext = new HandleContextImpl( + defaultTransactionBody(), + HederaFunctionality.CRYPTO_TRANSFER, + 0, + payer, + payerKey, + networkInfo, + TransactionCategory.USER, + recordBuilder, + stack, + DEFAULT_CONFIGURATION, + verifier, + recordListBuilder, + checker, + dispatcher, + serviceScopeLookup, + blockRecordInfo, + recordCache, + feeManager, + exchangeRateManager, + DEFAULT_CONSENSUS_NOW, + authorizer, + solvencyPreCheck, + childRecordFinalizer, + networkUtilizationManager, + synchronizedThrottleAccumulator, + platformState); + } + + @Test + void getsFreezeTime() { + given(platformState.getFreezeTime()).willReturn(DEFAULT_CONSENSUS_NOW.plusSeconds(1)); + assertThat(handleContext.freezeTime()).isEqualTo(DEFAULT_CONSENSUS_NOW.plusSeconds(1)); + } + } + @Nested @DisplayName("Handling of transaction data") final class TransactionDataTest { @@ -934,7 +996,8 @@ private HandleContextImpl createContext(final TransactionBody txBody, final Tran solvencyPreCheck, childRecordFinalizer, networkUtilizationManager, - synchronizedThrottleAccumulator); + synchronizedThrottleAccumulator, + platformState); } @SuppressWarnings("ConstantConditions") diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/BlockRecordServiceTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/BlockRecordServiceTest.java index 90e0cb9b99fd..3873d82b8b13 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/BlockRecordServiceTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/BlockRecordServiceTest.java @@ -66,7 +66,7 @@ void testRegisterSchemas() { assertTrue(states.contains(StateDefinition.singleton("BLOCKS", BlockInfo.PROTOBUF))); when(migrationContext.newStates()).thenReturn(writableStates); - when(migrationContext.previousStates()).thenReturn(EmptyReadableStates.INSTANCE); + when(migrationContext.previousVersion()).thenReturn(null); when(writableStates.getSingleton(BLOCK_INFO_STATE_KEY)).thenReturn(blockInfoState); when(writableStates.getSingleton(RUNNING_HASHES_STATE_KEY)).thenReturn(runningHashesState); diff --git a/hedera-node/hedera-app/src/testFixtures/java/com/hedera/node/app/fixtures/state/FakeSchemaRegistry.java b/hedera-node/hedera-app/src/testFixtures/java/com/hedera/node/app/fixtures/state/FakeSchemaRegistry.java index df651f4f0d99..e536baa41865 100644 --- a/hedera-node/hedera-app/src/testFixtures/java/com/hedera/node/app/fixtures/state/FakeSchemaRegistry.java +++ b/hedera-node/hedera-app/src/testFixtures/java/com/hedera/node/app/fixtures/state/FakeSchemaRegistry.java @@ -16,6 +16,9 @@ package com.hedera.node.app.fixtures.state; +import static com.hedera.node.app.spi.fixtures.state.TestSchema.CURRENT_VERSION; + +import com.hedera.hapi.node.base.SemanticVersion; import com.hedera.node.app.spi.fixtures.state.ListWritableQueueState; import com.hedera.node.app.spi.fixtures.state.MapWritableKVState; import com.hedera.node.app.spi.fixtures.state.MapWritableStates; @@ -80,6 +83,11 @@ public void copyAndReleaseOnDiskState(String stateKey) { // No-op } + @Override + public SemanticVersion previousVersion() { + return CURRENT_VERSION; + } + @NonNull @Override public ReadableStates previousStates() { diff --git a/hedera-node/hedera-app/src/xtest/java/common/BaseScaffoldingModule.java b/hedera-node/hedera-app/src/xtest/java/common/BaseScaffoldingModule.java index b92ee42e9c74..b9943da4c15c 100644 --- a/hedera-node/hedera-app/src/xtest/java/common/BaseScaffoldingModule.java +++ b/hedera-node/hedera-app/src/xtest/java/common/BaseScaffoldingModule.java @@ -67,6 +67,7 @@ import com.hedera.node.app.state.DeduplicationCache; import com.hedera.node.app.state.HederaRecordCache; import com.hedera.node.app.state.HederaState; +import com.hedera.node.app.state.PlatformStateAccessor; import com.hedera.node.app.state.recordcache.DeduplicationCacheImpl; import com.hedera.node.app.state.recordcache.RecordCacheImpl; import com.hedera.node.app.throttle.NetworkUtilizationManager; @@ -250,7 +251,8 @@ static Function provideHandleContextCreator( @NonNull final Authorizer authorizer, @NonNull final ChildRecordFinalizer childRecordFinalizer, @NonNull final NetworkUtilizationManager networkUtilizationManager, - @NonNull final SynchronizedThrottleAccumulator synchronizedThrottleAccumulator) { + @NonNull final SynchronizedThrottleAccumulator synchronizedThrottleAccumulator, + @NonNull final PlatformStateAccessor platformState) { final var consensusTime = Instant.now(); final var recordListBuilder = new RecordListBuilder(consensusTime); final var parentRecordBuilder = recordListBuilder.userTransactionRecordBuilder(); @@ -290,7 +292,8 @@ static Function provideHandleContextCreator( solvencyPreCheck, childRecordFinalizer, networkUtilizationManager, - synchronizedThrottleAccumulator); + synchronizedThrottleAccumulator, + platformState.getPlatformState()); }; } diff --git a/hedera-node/hedera-file-service-impl/src/main/java/com/hedera/node/app/service/file/impl/schemas/InitialModFileGenesisSchema.java b/hedera-node/hedera-file-service-impl/src/main/java/com/hedera/node/app/service/file/impl/schemas/InitialModFileGenesisSchema.java index e715f5cd4399..c799dc2ab101 100644 --- a/hedera-node/hedera-file-service-impl/src/main/java/com/hedera/node/app/service/file/impl/schemas/InitialModFileGenesisSchema.java +++ b/hedera-node/hedera-file-service-impl/src/main/java/com/hedera/node/app/service/file/impl/schemas/InitialModFileGenesisSchema.java @@ -145,7 +145,7 @@ public void setFs(@Nullable final Supplierget(FileServiceImpl.BLOBS_KEY); diff --git a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/pbj/PbjConverter.java b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/pbj/PbjConverter.java index f7a6938d634d..88b862a88c61 100644 --- a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/pbj/PbjConverter.java +++ b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/pbj/PbjConverter.java @@ -1530,6 +1530,10 @@ public static com.hederahashgraph.api.proto.java.Fraction fromPbj(@NonNull final return builder.build(); } + public static FileID toPbj(com.hederahashgraph.api.proto.java.FileID fileID) { + return protoToPbj(fileID, FileID.class); + } + @NonNull public static com.hederahashgraph.api.proto.java.File fromPbj(@Nullable File file) { var builder = com.hederahashgraph.api.proto.java.File.newBuilder(); 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 6207ff99e753..f78e7773347a 100644 --- a/hedera-node/hedera-network-admin-service-impl/build.gradle.kts +++ b/hedera-node/hedera-network-admin-service-impl/build.gradle.kts @@ -22,6 +22,7 @@ mainModuleInfo { annotationProcessor("dagger.compiler") } testModuleInfo { requires("com.hedera.node.app") + requires("com.hedera.node.app.service.file.impl") requires("com.hedera.node.app.service.network.admin.impl") requires("com.hedera.node.app.service.token.impl") requires("com.hedera.node.app.spi.test.fixtures") diff --git a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/WritableFreezeStore.java b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/WritableFreezeStore.java index add91afc8f41..e4631c0f1997 100644 --- a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/WritableFreezeStore.java +++ b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/WritableFreezeStore.java @@ -57,9 +57,9 @@ public WritableFreezeStore(@NonNull final WritableStates states) { * * @param freezeTime the freeze time to set; if null, clears the freeze time */ - public void freezeTime(@Nullable final Timestamp freezeTime) { + public void freezeTime(@NonNull final Timestamp freezeTime) { freezeTimeState.put(freezeTime); - if (freezeTime != null) { + if (!freezeTime.equals(Timestamp.DEFAULT)) { lastFrozenTimeState.put(freezeTime); } } @@ -70,7 +70,7 @@ public void freezeTime(@Nullable final Timestamp freezeTime) { * Gets the scheduled freeze time. If no freeze has been scheduled, returns null. */ public Timestamp freezeTime() { - return freezeTimeState.get(); + return freezeTimeState.get() == Timestamp.DEFAULT ? null : freezeTimeState.get(); } @Override @@ -87,7 +87,8 @@ public Timestamp lastFrozenTime() { * * @param updateFileHash The update file hash to set. If null, clears the update file hash. */ - public void updateFileHash(@Nullable final Bytes updateFileHash) { + public void updateFileHash(@NonNull final Bytes updateFileHash) { + requireNonNull(updateFileHash); this.updateFileHash.put(new ProtoBytes(updateFileHash)); } @@ -95,6 +96,9 @@ public void updateFileHash(@Nullable final Bytes updateFileHash) { @Nullable public Bytes updateFileHash() { ProtoBytes fileHash = updateFileHash.get(); - return (fileHash == null ? null : fileHash.value()); + if (fileHash == null) { + return null; + } + return fileHash.value() == Bytes.EMPTY ? null : fileHash.value(); } } diff --git a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/handlers/FreezeHandler.java b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/handlers/FreezeHandler.java index 75c474a285c0..c86f436483b4 100644 --- a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/handlers/FreezeHandler.java +++ b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/handlers/FreezeHandler.java @@ -55,12 +55,15 @@ import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; /** * This class contains all workflow-related functionality regarding {@link HederaFunctionality#FREEZE}. */ @Singleton public class FreezeHandler implements TransactionHandler { + private static final Logger log = LogManager.getLogger(FreezeHandler.class); // length of the hash of the update file included in the FreezeTransactionBody // used for a quick sanity check that the file hash is not invalid public static final int UPDATE_FILE_HASH_LEN = 48; @@ -143,13 +146,14 @@ public void handle(@NonNull final HandleContext context) throws HandleException final var filesConfig = context.configuration().getConfigData(FilesConfig.class); final FreezeUpgradeActions upgradeActions = - new FreezeUpgradeActions(adminServiceConfig, freezeStore, freezeExecutor); + new FreezeUpgradeActions(adminServiceConfig, freezeStore, freezeExecutor, upgradeFileStore); final Timestamp freezeStartTime = freezeTxn.startTime(); // may be null for some freeze types switch (freezeTxn.freezeType()) { case PREPARE_UPGRADE -> { // by the time we get here, we've already checked that fileHash is non-null in preHandle() freezeStore.updateFileHash(freezeTxn.fileHash()); + log.info("Preparing upgrade with file {}, hash {}", updateFileID, freezeTxn.fileHash()); try { if (updateFileID != null && updateFileID.fileNum() @@ -165,7 +169,8 @@ public void handle(@NonNull final HandleContext context) throws HandleException case FREEZE_UPGRADE -> upgradeActions.scheduleFreezeUpgradeAt(requireNonNull(freezeStartTime)); case FREEZE_ABORT -> { upgradeActions.abortScheduledFreeze(); - freezeStore.updateFileHash(null); + freezeStore.updateFileHash(Bytes.EMPTY); + log.info("Preparing freeze abort with file {}, hash null", updateFileID); } case TELEMETRY_UPGRADE -> { try { diff --git a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/handlers/FreezeUpgradeActions.java b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/handlers/FreezeUpgradeActions.java index e662fb1172c5..a9d0d0a780de 100644 --- a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/handlers/FreezeUpgradeActions.java +++ b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/handlers/FreezeUpgradeActions.java @@ -17,75 +17,30 @@ package com.hedera.node.app.service.networkadmin.impl.handlers; import static java.util.Objects.requireNonNull; -import static java.util.concurrent.CompletableFuture.runAsync; import com.hedera.hapi.node.base.Timestamp; +import com.hedera.node.app.service.file.ReadableUpgradeFileStore; import com.hedera.node.app.service.networkadmin.impl.WritableFreezeStore; import com.hedera.node.config.data.NetworkAdminConfig; -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.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicBoolean; -import org.apache.commons.io.FileUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -public class FreezeUpgradeActions { +/** + * Provides all the needed actions that need to take place during upgrade + */ +public class FreezeUpgradeActions extends ReadableFreezeUpgradeActions { private static final Logger log = LogManager.getLogger(FreezeUpgradeActions.class); - - private static final String PREPARE_UPGRADE_DESC = "software"; - private static final String TELEMETRY_UPGRADE_DESC = "telemetry"; - private static final String MANUAL_REMEDIATION_ALERT = "Manual remediation may be necessary to avoid node ISS"; - - public static final String NOW_FROZEN_MARKER = "now_frozen.mf"; - public static final String EXEC_IMMEDIATE_MARKER = "execute_immediate.mf"; - public static final String EXEC_TELEMETRY_MARKER = "execute_telemetry.mf"; - public static final String FREEZE_SCHEDULED_MARKER = "freeze_scheduled.mf"; - public static final String FREEZE_ABORTED_MARKER = "freeze_aborted.mf"; - - public static final String MARK = "✓"; - - private final NetworkAdminConfig adminServiceConfig; private final WritableFreezeStore freezeStore; - private final Executor executor; - public FreezeUpgradeActions( @NonNull final NetworkAdminConfig adminServiceConfig, @NonNull final WritableFreezeStore freezeStore, - @NonNull final Executor executor) { - requireNonNull(adminServiceConfig); - requireNonNull(freezeStore); - requireNonNull(executor); - - this.adminServiceConfig = adminServiceConfig; + @NonNull final Executor executor, + @NonNull final ReadableUpgradeFileStore upgradeFileStore) { + super(adminServiceConfig, freezeStore, executor, upgradeFileStore); this.freezeStore = freezeStore; - this.executor = executor; - } - - public void externalizeFreezeIfUpgradePending() { - // @todo('Issue #8660') this code is not currently triggered anywhere - if (freezeStore.updateFileHash() != null) { - writeCheckMarker(NOW_FROZEN_MARKER); - } - } - - public CompletableFuture extractTelemetryUpgrade( - @NonNull final Bytes archiveData, @Nullable final Timestamp now) { - requireNonNull(archiveData); - return extractNow(archiveData, TELEMETRY_UPGRADE_DESC, EXEC_TELEMETRY_MARKER, now); - } - - public CompletableFuture extractSoftwareUpgrade(@NonNull final Bytes archiveData) { - requireNonNull(archiveData); - return extractNow(archiveData, PREPARE_UPGRADE_DESC, EXEC_IMMEDIATE_MARKER, null); } public void scheduleFreezeOnlyAt(@NonNull final Timestamp freezeTime) { @@ -103,78 +58,7 @@ public void scheduleFreezeUpgradeAt(@NonNull final Timestamp freezeTime) { public void abortScheduledFreeze() { requireNonNull(freezeStore, "Cannot abort freeze without access to the dual state"); - freezeStore.freezeTime(null); + freezeStore.freezeTime(Timestamp.DEFAULT); writeCheckMarker(FREEZE_ABORTED_MARKER); } - - public boolean isFreezeScheduled() { - final var ans = new AtomicBoolean(); - requireNonNull(freezeStore, "Cannot check freeze schedule without access to the dual state"); - final var freezeTime = freezeStore.freezeTime(); - ans.set(freezeTime != null && !freezeTime.equals(freezeStore.lastFrozenTime())); - return ans.get(); - } - - /* --- Internal methods --- */ - - private CompletableFuture extractNow( - @NonNull final Bytes archiveData, - @NonNull final String desc, - @NonNull final String marker, - @Nullable final Timestamp now) { - requireNonNull(archiveData); - requireNonNull(desc); - requireNonNull(marker); - - final long size = archiveData.length(); - final String artifactsLoc = adminServiceConfig.upgradeArtifactsPath(); - requireNonNull(artifactsLoc); - log.info("About to unzip {} bytes for {} update into {}", size, desc, artifactsLoc); - // we spin off a separate thread to avoid blocking handleTransaction - // if we block handle, there could be a dramatic spike in E2E latency at the time of PREPARE_UPGRADE - return runAsync(() -> extractAndReplaceArtifacts(artifactsLoc, archiveData, size, desc, marker, now), executor); - } - - private void extractAndReplaceArtifacts( - String artifactsLoc, Bytes archiveData, long size, String desc, String marker, Timestamp now) { - try { - FileUtils.cleanDirectory(new File(artifactsLoc)); - UnzipUtility.unzip(archiveData.toByteArray(), Paths.get(artifactsLoc)); - log.info("Finished unzipping {} bytes for {} update into {}", size, desc, artifactsLoc); - writeSecondMarker(marker, now); - } catch (final IOException e) { - // catch and log instead of throwing because upgrade process looks at the presence or absence - // of marker files to determine whether to proceed with the upgrade - // if second marker is present, that means the zip file was successfully extracted - log.error("Failed to unzip archive for NMT consumption", e); - log.error(MANUAL_REMEDIATION_ALERT); - } - } - - private void writeCheckMarker(@NonNull final String file) { - requireNonNull(file); - writeMarker(file, null); - } - - private void writeSecondMarker(@NonNull final String file, @Nullable final Timestamp now) { - requireNonNull(file); - writeMarker(file, now); - } - - private void writeMarker(@NonNull final String file, @Nullable final Timestamp now) { - requireNonNull(file); - final Path artifactsDirPath = Paths.get(adminServiceConfig.upgradeArtifactsPath()); - final var filePath = artifactsDirPath.resolve(file); - try { - if (!artifactsDirPath.toFile().exists()) { - Files.createDirectories(artifactsDirPath); - } - final var contents = (now == null) ? MARK : (String.valueOf(now.seconds())); - Files.writeString(filePath, contents); - log.info("Wrote marker {}", filePath); - } catch (final IOException e) { - log.error("Failed to write NMT marker {}", filePath, e); - log.error(MANUAL_REMEDIATION_ALERT); - } - } } diff --git a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/handlers/ReadableFreezeUpgradeActions.java b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/handlers/ReadableFreezeUpgradeActions.java new file mode 100644 index 000000000000..8346c4ddd44f --- /dev/null +++ b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/handlers/ReadableFreezeUpgradeActions.java @@ -0,0 +1,226 @@ +/* + * 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.networkadmin.impl.handlers; + +import static com.hedera.node.app.hapi.utils.CommonUtils.noThrowSha384HashOf; +import static com.hedera.node.app.service.mono.context.properties.StaticPropertiesHolder.STATIC_PROPERTIES; +import static com.hedera.node.app.service.mono.pbj.PbjConverter.toPbj; +import static com.hedera.node.app.service.mono.utils.EntityIdUtils.readableId; +import static java.util.Objects.requireNonNull; +import static java.util.concurrent.CompletableFuture.runAsync; + +import com.hedera.hapi.node.base.Timestamp; +import com.hedera.node.app.service.file.ReadableUpgradeFileStore; +import com.hedera.node.app.service.networkadmin.ReadableFreezeStore; +import com.hedera.node.config.data.NetworkAdminConfig; +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.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.commons.io.FileUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Provides all the read-only actions that need to take place during upgrade + */ +public class ReadableFreezeUpgradeActions { + private static final Logger log = LogManager.getLogger(ReadableFreezeUpgradeActions.class); + private final NetworkAdminConfig adminServiceConfig; + private final ReadableFreezeStore freezeStore; + private final ReadableUpgradeFileStore upgradeFileStore; + + private final Executor executor; + + public static final String PREPARE_UPGRADE_DESC = "software"; + public static final String TELEMETRY_UPGRADE_DESC = "telemetry"; + public static final String MANUAL_REMEDIATION_ALERT = "Manual remediation may be necessary to avoid node ISS"; + + public static final String NOW_FROZEN_MARKER = "now_frozen.mf"; + public static final String EXEC_IMMEDIATE_MARKER = "execute_immediate.mf"; + public static final String EXEC_TELEMETRY_MARKER = "execute_telemetry.mf"; + public static final String FREEZE_SCHEDULED_MARKER = "freeze_scheduled.mf"; + public static final String FREEZE_ABORTED_MARKER = "freeze_aborted.mf"; + + public static final String MARK = "✓"; + + public ReadableFreezeUpgradeActions( + @NonNull final NetworkAdminConfig adminServiceConfig, + @NonNull final ReadableFreezeStore freezeStore, + @NonNull final Executor executor, + @NonNull final ReadableUpgradeFileStore upgradeFileStore) { + requireNonNull(adminServiceConfig, "Admin service config is required for freeze upgrade actions"); + requireNonNull(freezeStore, "Freeze store is required for freeze upgrade actions"); + requireNonNull(executor, "Executor is required for freeze upgrade actions"); + requireNonNull(upgradeFileStore, "Upgrade file store is required for freeze upgrade actions"); + + this.adminServiceConfig = adminServiceConfig; + this.freezeStore = freezeStore; + this.executor = executor; + this.upgradeFileStore = upgradeFileStore; + } + + public void externalizeFreezeIfUpgradePending() { + log.info( + "Externalizing freeze if upgrade pending, freezeStore: {}, updateFileHash: {}", + freezeStore, + freezeStore.updateFileHash()); + if (freezeStore.updateFileHash() != null) { + writeCheckMarker(NOW_FROZEN_MARKER); + } + } + + protected void writeMarker(@NonNull final String file, @Nullable final Timestamp now) { + requireNonNull(file); + final Path artifactsDirPath = Paths.get(adminServiceConfig.upgradeArtifactsPath()); + final var filePath = artifactsDirPath.resolve(file); + try { + if (!artifactsDirPath.toFile().exists()) { + Files.createDirectories(artifactsDirPath); + } + final var contents = (now == null) ? MARK : (String.valueOf(now.seconds())); + Files.writeString(filePath, contents); + log.info("Wrote marker {}", filePath); + } catch (final IOException e) { + log.error("Failed to write NMT marker {}", filePath, e); + log.error(MANUAL_REMEDIATION_ALERT); + } + } + + protected void writeCheckMarker(@NonNull final String file) { + requireNonNull(file); + writeMarker(file, null); + } + + protected void writeSecondMarker(@NonNull final String file, @Nullable final Timestamp now) { + requireNonNull(file); + writeMarker(file, now); + } + + public void catchUpOnMissedSideEffects(final Instant freezeTime) { + catchUpOnMissedFreezeScheduling(freezeTime); + catchUpOnMissedUpgradePrep(); + } + + private void catchUpOnMissedFreezeScheduling(final Instant freezeTime) { + final var isUpgradePrepared = freezeStore.updateFileHash() != null; + if (isFreezeScheduled() && isUpgradePrepared) { + writeMarker( + FREEZE_SCHEDULED_MARKER, + Timestamp.newBuilder() + .nanos(freezeTime.getNano()) + .seconds(freezeTime.getEpochSecond()) + .build()); + } + /* If we missed a FREEZE_ABORT, we are at risk of having a problem down the road. + But writing a "defensive" freeze_aborted.mf is itself too risky, as it will keep + us from correctly (1) catching up on a missed PREPARE_UPGRADE; or (2) handling an + imminent PREPARE_UPGRADE. */ + } + + private void catchUpOnMissedUpgradePrep() { + if (freezeStore.updateFileHash() == null) { + return; + } + + final var upgradeFileId = STATIC_PROPERTIES.scopedFileWith(150); + try { + final var curSpecialFileContents = upgradeFileStore.getFull(toPbj(upgradeFileId)); + if (!isPreparedFileHashValidGiven( + noThrowSha384HashOf(curSpecialFileContents.toByteArray()), + freezeStore.updateFileHash().toByteArray())) { + log.error( + "Cannot redo NMT upgrade prep, file {} changed since FREEZE_UPGRADE", + () -> readableId(upgradeFileId)); + log.error(MANUAL_REMEDIATION_ALERT); + return; + } + extractSoftwareUpgrade(curSpecialFileContents).join(); + } catch (final IOException e) { + log.error( + "Cannot redo NMT upgrade prep, file {} changed since FREEZE_UPGRADE", readableId(upgradeFileId), e); + log.error(MANUAL_REMEDIATION_ALERT); + } + } + + public boolean isPreparedFileHashValidGiven(final byte[] curSpecialFilesHash, final byte[] hashFromTxnBody) { + return Arrays.equals(curSpecialFilesHash, hashFromTxnBody); + } + + public CompletableFuture extractTelemetryUpgrade( + @NonNull final Bytes archiveData, @Nullable final Timestamp now) { + requireNonNull(archiveData); + return extractNow(archiveData, TELEMETRY_UPGRADE_DESC, EXEC_TELEMETRY_MARKER, now); + } + + public CompletableFuture extractSoftwareUpgrade(@NonNull final Bytes archiveData) { + requireNonNull(archiveData); + return extractNow(archiveData, PREPARE_UPGRADE_DESC, EXEC_IMMEDIATE_MARKER, null); + } + + public boolean isFreezeScheduled() { + final var ans = new AtomicBoolean(); + requireNonNull(freezeStore, "Cannot check freeze schedule without access to the dual state"); + final var freezeTime = freezeStore.freezeTime(); + ans.set(freezeTime != null && !freezeTime.equals(freezeStore.lastFrozenTime())); + return ans.get(); + } + + /* -------- Internal Methods */ + private CompletableFuture extractNow( + @NonNull final Bytes archiveData, + @NonNull final String desc, + @NonNull final String marker, + @Nullable final Timestamp now) { + requireNonNull(archiveData); + requireNonNull(desc); + requireNonNull(marker); + + final long size = archiveData.length(); + final String artifactsLoc = adminServiceConfig.upgradeArtifactsPath(); + requireNonNull(artifactsLoc); + log.info("About to unzip {} bytes for {} update into {}", size, desc, artifactsLoc); + // we spin off a separate thread to avoid blocking handleTransaction + // if we block handle, there could be a dramatic spike in E2E latency at the time of PREPARE_UPGRADE + return runAsync(() -> extractAndReplaceArtifacts(artifactsLoc, archiveData, size, desc, marker, now), executor); + } + + private void extractAndReplaceArtifacts( + String artifactsLoc, Bytes archiveData, long size, String desc, String marker, Timestamp now) { + try { + FileUtils.cleanDirectory(new File(artifactsLoc)); + UnzipUtility.unzip(archiveData.toByteArray(), Paths.get(artifactsLoc)); + log.info("Finished unzipping {} bytes for {} update into {}", size, desc, artifactsLoc); + writeSecondMarker(marker, now); + } catch (final IOException e) { + // catch and log instead of throwing because upgrade process looks at the presence or absence + // of marker files to determine whether to proceed with the upgrade + // if second marker is present, that means the zip file was successfully extracted + log.error("Failed to unzip archive for NMT consumption", e); + log.error(MANUAL_REMEDIATION_ALERT); + } + } +} diff --git a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/schemas/InitialModServiceAdminSchema.java b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/schemas/InitialModServiceAdminSchema.java index 4f9eaf7a9c6a..fc58be72b7e6 100644 --- a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/schemas/InitialModServiceAdminSchema.java +++ b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/schemas/InitialModServiceAdminSchema.java @@ -56,7 +56,7 @@ public void migrate(@NonNull final MigrationContext ctx) { // Reset the upgrade file hash to empty // It should always be empty at genesis or after an upgrade, to indicate that no upgrade is in progress // Nothing in state can ever be null, so use Type.DEFAULT to indicate an empty hash - final var isGenesis = ctx.previousStates().isEmpty(); + final var isGenesis = ctx.previousVersion() == null; final var upgradeFileHashKeyState = ctx.newStates().getSingleton(FreezeServiceImpl.UPGRADE_FILE_HASH_KEY); diff --git a/hedera-node/hedera-network-admin-service-impl/src/main/java/module-info.java b/hedera-node/hedera-network-admin-service-impl/src/main/java/module-info.java index 44729e6babc3..aa01345757c0 100644 --- a/hedera-node/hedera-network-admin-service-impl/src/main/java/module-info.java +++ b/hedera-node/hedera-network-admin-service-impl/src/main/java/module-info.java @@ -3,6 +3,7 @@ module com.hedera.node.app.service.network.admin.impl { requires transitive com.hedera.node.app.hapi.fees; + requires transitive com.hedera.node.app.service.file; requires transitive com.hedera.node.app.service.mono; requires transitive com.hedera.node.app.service.network.admin; requires transitive com.hedera.node.app.spi; @@ -12,7 +13,6 @@ requires transitive dagger; requires transitive javax.inject; requires com.hedera.node.app.hapi.utils; - requires com.hedera.node.app.service.file; requires com.hedera.node.app.service.token; requires com.google.common; requires com.swirlds.common; diff --git a/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/FreezeServiceImplTest.java b/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/FreezeServiceImplTest.java index 9a12212668d9..be5c70b74feb 100644 --- a/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/FreezeServiceImplTest.java +++ b/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/FreezeServiceImplTest.java @@ -22,6 +22,7 @@ import static com.hedera.node.app.spi.fixtures.state.TestSchema.CURRENT_VERSION; 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.mockito.Mockito.verify; import com.hedera.node.app.fixtures.state.FakeHederaState; @@ -88,6 +89,6 @@ void migratesAsExpected() { registry.migrate(FreezeService.NAME, state, networkInfo); final var upgradeFileHashKeyState = state.getReadableStates(FreezeService.NAME).getSingleton(UPGRADE_FILE_HASH_KEY); - assertNotNull(upgradeFileHashKeyState.get()); + assertNull(upgradeFileHashKeyState.get()); } } diff --git a/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/WritableFreezeStoreTest.java b/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/WritableFreezeStoreTest.java index 1bc1001abdfc..1e18ee62264f 100644 --- a/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/WritableFreezeStoreTest.java +++ b/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/WritableFreezeStoreTest.java @@ -16,6 +16,7 @@ package com.hedera.node.app.service.networkadmin.impl.test; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -97,7 +98,8 @@ void testUpdateFileHash() { store.updateFileHash(Bytes.wrap("test hash")); assertEquals(Bytes.wrap("test hash"), store.updateFileHash()); - store.updateFileHash(null); - assertNull(store.updateFileHash()); + // test with file hash set + assertThatThrownBy(() -> store.updateFileHash(null)).isInstanceOf(NullPointerException.class); + assertEquals(Bytes.wrap("test hash"), store.updateFileHash()); } } diff --git a/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/handlers/FreezeHandlerTest.java b/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/handlers/FreezeHandlerTest.java index e9ad9c5cef00..18c5d277e91c 100644 --- a/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/handlers/FreezeHandlerTest.java +++ b/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/handlers/FreezeHandlerTest.java @@ -28,6 +28,7 @@ import static com.hedera.hapi.node.freeze.FreezeType.UNKNOWN_FREEZE_TYPE; import static com.hedera.node.app.spi.fixtures.Assertions.assertThrowsPreCheck; import static com.hedera.node.app.spi.fixtures.workflows.ExceptionConditions.responseCode; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.mockito.BDDMockito.given; @@ -332,6 +333,8 @@ void happyPathFreezeAbort() { given(handleContext.body()).willReturn(txn); assertDoesNotThrow(() -> subject.handle(handleContext)); + + assertThat(freezeStore.updateFileHash()).isNull(); } @Test diff --git a/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/handlers/FreezeUpgradeActionsTest.java b/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/handlers/FreezeUpgradeActionsTest.java index d350b36d8996..985d9757ff85 100644 --- a/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/handlers/FreezeUpgradeActionsTest.java +++ b/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/handlers/FreezeUpgradeActionsTest.java @@ -20,28 +20,26 @@ import static com.hedera.node.app.service.networkadmin.impl.handlers.FreezeUpgradeActions.EXEC_TELEMETRY_MARKER; import static com.hedera.node.app.service.networkadmin.impl.handlers.FreezeUpgradeActions.FREEZE_ABORTED_MARKER; import static com.hedera.node.app.service.networkadmin.impl.handlers.FreezeUpgradeActions.FREEZE_SCHEDULED_MARKER; -import static com.hedera.node.app.service.networkadmin.impl.handlers.FreezeUpgradeActions.NOW_FROZEN_MARKER; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; import com.hedera.hapi.node.base.Timestamp; +import com.hedera.node.app.service.file.impl.WritableUpgradeFileStore; import com.hedera.node.app.service.networkadmin.impl.WritableFreezeStore; import com.hedera.node.app.service.networkadmin.impl.handlers.FreezeUpgradeActions; +import com.hedera.node.app.service.networkadmin.impl.handlers.ReadableFreezeUpgradeActions; import com.hedera.node.app.spi.fixtures.util.LogCaptor; import com.hedera.node.app.spi.fixtures.util.LogCaptureExtension; import com.hedera.node.app.spi.fixtures.util.LoggingSubject; import com.hedera.node.app.spi.fixtures.util.LoggingTarget; import com.hedera.node.config.data.NetworkAdminConfig; -import com.hedera.pbj.runtime.io.buffer.Bytes; import edu.umd.cs.findbugs.annotations.Nullable; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.ForkJoinPool; import java.util.zip.ZipEntry; @@ -57,7 +55,6 @@ class FreezeUpgradeActionsTest { private static final Timestamp then = Timestamp.newBuilder().seconds(1_234_567L).nanos(890).build(); - private Path noiseFileLoc; private Path noiseSubFileLoc; private Path zipArchivePath; // path to valid.zip test zip file (in zipSourceDir directory) @@ -76,17 +73,22 @@ class FreezeUpgradeActionsTest { @LoggingTarget private LogCaptor logCaptor; - @LoggingSubject private FreezeUpgradeActions subject; + // Since all logs are moved to base class + @LoggingSubject + private ReadableFreezeUpgradeActions loggingSubject; + + @Mock + private WritableUpgradeFileStore upgradeFileStore; + @BeforeEach void setUp() throws IOException { - noiseFileLoc = zipOutputDir.toPath().resolve("forgotten.cfg"); noiseSubFileLoc = zipOutputDir.toPath().resolve("edargpu"); final Executor freezeExectuor = new ForkJoinPool( 1, ForkJoinPool.defaultForkJoinWorkerThreadFactory, Thread.getDefaultUncaughtExceptionHandler(), true); - subject = new FreezeUpgradeActions(adminServiceConfig, freezeStore, freezeExectuor); + subject = new FreezeUpgradeActions(adminServiceConfig, freezeStore, freezeExectuor, upgradeFileStore); // set up test zip zipSourceDir = Files.createTempDirectory("zipSourceDir"); @@ -102,60 +104,6 @@ void setUp() throws IOException { } } - @Test - void complainsLoudlyWhenUnableToUnzipArchive() { - rmIfPresent(EXEC_IMMEDIATE_MARKER); - - given(adminServiceConfig.upgradeArtifactsPath()).willReturn(zipOutputDir.toString()); - - final Bytes invalidArchive = Bytes.wrap("Not a valid zip archive".getBytes(StandardCharsets.UTF_8)); - subject.extractSoftwareUpgrade(invalidArchive).join(); - - assertThat(logCaptor.errorLogs()) - .anyMatch(l -> l.startsWith("Failed to unzip archive for NMT consumption java.io.IOException:" + " ")); - assertThat(logCaptor.errorLogs()) - .anyMatch(l -> l.equals("Manual remediation may be necessary to avoid node ISS")); - - assertThat(new File(zipOutputDir, EXEC_IMMEDIATE_MARKER)).doesNotExist(); - } - - @Test - void preparesForUpgrade() throws IOException { - setupNoiseFiles(); - rmIfPresent(EXEC_IMMEDIATE_MARKER); - - given(adminServiceConfig.upgradeArtifactsPath()).willReturn(zipOutputDir.toString()); - - final Bytes realArchive = Bytes.wrap(Files.readAllBytes(zipArchivePath)); - subject.extractSoftwareUpgrade(realArchive).join(); - - assertMarkerCreated(EXEC_IMMEDIATE_MARKER, null); - } - - @Test - void upgradesTelemetry() throws IOException { - rmIfPresent(EXEC_TELEMETRY_MARKER); - - given(adminServiceConfig.upgradeArtifactsPath()).willReturn(zipOutputDir.toString()); - - final Bytes realArchive = Bytes.wrap(Files.readAllBytes(zipArchivePath)); - subject.extractTelemetryUpgrade(realArchive, then).join(); - - assertMarkerCreated(EXEC_TELEMETRY_MARKER, then); - } - - @Test - void externalizesFreeze() throws IOException { - rmIfPresent(NOW_FROZEN_MARKER); - - given(adminServiceConfig.upgradeArtifactsPath()).willReturn(zipOutputDir.toString()); - given(freezeStore.updateFileHash()).willReturn(Bytes.wrap("fake hash")); - - subject.externalizeFreezeIfUpgradePending(); - - assertMarkerCreated(NOW_FROZEN_MARKER, null); - } - @Test void setsExpectedFreezeAndWritesMarkerForFreezeUpgrade() throws IOException { rmIfPresent(FREEZE_SCHEDULED_MARKER); @@ -186,9 +134,9 @@ void nullsOutDualOnAborting() throws IOException { subject.abortScheduledFreeze(); - verify(freezeStore).freezeTime(null); + verify(freezeStore).freezeTime(Timestamp.DEFAULT); - assertMarkerCreated(FREEZE_ABORTED_MARKER, null); + assertMarkerCreated(FREEZE_ABORTED_MARKER, Timestamp.DEFAULT); } @Test @@ -204,18 +152,9 @@ void canStillWriteMarkersEvenIfDirDoesntExist() throws IOException { subject.abortScheduledFreeze(); - verify(freezeStore).freezeTime(null); - - assertMarkerCreated(FREEZE_ABORTED_MARKER, null, otherMarkerFilesLoc); - } - - @Test - void determinesIfFreezeIsScheduled() { - assertThat(subject.isFreezeScheduled()).isFalse(); - - given(freezeStore.freezeTime()).willReturn(then); + verify(freezeStore).freezeTime(Timestamp.DEFAULT); - assertThat(subject.isFreezeScheduled()).isTrue(); + assertMarkerCreated(FREEZE_ABORTED_MARKER, Timestamp.DEFAULT, otherMarkerFilesLoc); } private void rmIfPresent(final String file) { @@ -260,24 +199,11 @@ private void assertMarkerCreated(final String file, final @Nullable Timestamp wh } else { assertThat(logCaptor.infoLogs()).anyMatch(l -> (l.contains("Wrote marker " + filePath))); } - if (when != null) { + if (when != null && !when.equals(Timestamp.DEFAULT)) { final var writtenEpochSecond = Long.parseLong(contents); assertThat(when.seconds()).isEqualTo(writtenEpochSecond); } else { assertThat(contents).isEqualTo(FreezeUpgradeActions.MARK); } } - - private void setupNoiseFiles() throws IOException { - Files.write( - noiseFileLoc, - List.of("There, the eyes are", "Sunlight on a broken column", "There, is a tree swinging")); - Files.write( - noiseSubFileLoc, - List.of( - "And voices are", - "In the wind's singing", - "More distant and more solemn", - "Than a fading star")); - } } diff --git a/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/handlers/ReadableFreezeUpgradeActionsTest.java b/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/handlers/ReadableFreezeUpgradeActionsTest.java new file mode 100644 index 000000000000..29e7e6475fb9 --- /dev/null +++ b/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/handlers/ReadableFreezeUpgradeActionsTest.java @@ -0,0 +1,232 @@ +/* + * 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.networkadmin.impl.test.handlers; + +import static com.hedera.node.app.service.networkadmin.impl.handlers.FreezeUpgradeActions.EXEC_IMMEDIATE_MARKER; +import static com.hedera.node.app.service.networkadmin.impl.handlers.FreezeUpgradeActions.EXEC_TELEMETRY_MARKER; +import static com.hedera.node.app.service.networkadmin.impl.handlers.FreezeUpgradeActions.NOW_FROZEN_MARKER; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; + +import com.hedera.hapi.node.base.Timestamp; +import com.hedera.node.app.service.file.impl.WritableUpgradeFileStore; +import com.hedera.node.app.service.networkadmin.impl.WritableFreezeStore; +import com.hedera.node.app.service.networkadmin.impl.handlers.FreezeUpgradeActions; +import com.hedera.node.app.service.networkadmin.impl.handlers.ReadableFreezeUpgradeActions; +import com.hedera.node.app.spi.fixtures.util.LogCaptor; +import com.hedera.node.app.spi.fixtures.util.LogCaptureExtension; +import com.hedera.node.app.spi.fixtures.util.LoggingSubject; +import com.hedera.node.app.spi.fixtures.util.LoggingTarget; +import com.hedera.node.config.data.NetworkAdminConfig; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.ForkJoinPool; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith({MockitoExtension.class, LogCaptureExtension.class}) +class ReadableFreezeUpgradeActionsTest { + private static final Timestamp then = + Timestamp.newBuilder().seconds(1_234_567L).nanos(890).build(); + private Path noiseFileLoc; + private Path noiseSubFileLoc; + private Path zipArchivePath; // path to valid.zip test zip file (in zipSourceDir directory) + + @TempDir + private Path zipSourceDir; // temp directory to place test zip files + + @TempDir + private File zipOutputDir; // temp directory to place marker files and output of zip extraction + + @Mock + private WritableFreezeStore freezeStore; + + @Mock + private NetworkAdminConfig adminServiceConfig; + + @LoggingTarget + private LogCaptor logCaptor; + + @LoggingSubject + private ReadableFreezeUpgradeActions subject; + + @Mock + private WritableUpgradeFileStore upgradeFileStore; + + @BeforeEach + void setUp() throws IOException { + noiseFileLoc = zipOutputDir.toPath().resolve("forgotten.cfg"); + noiseSubFileLoc = zipOutputDir.toPath().resolve("edargpu"); + + final Executor freezeExectuor = new ForkJoinPool( + 1, ForkJoinPool.defaultForkJoinWorkerThreadFactory, Thread.getDefaultUncaughtExceptionHandler(), true); + subject = new FreezeUpgradeActions(adminServiceConfig, freezeStore, freezeExectuor, upgradeFileStore); + + // set up test zip + zipSourceDir = Files.createTempDirectory("zipSourceDir"); + zipArchivePath = Path.of(zipSourceDir + "/valid.zip"); + try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(zipArchivePath.toFile()))) { + ZipEntry e = new ZipEntry("garden_path_sentence.txt"); + out.putNextEntry(e); + + String fileContent = "The old man the boats"; + byte[] data = fileContent.getBytes(); + out.write(data, 0, data.length); + out.closeEntry(); + } + } + + @Test + void complainsLoudlyWhenUnableToUnzipArchive() { + rmIfPresent(EXEC_IMMEDIATE_MARKER); + + given(adminServiceConfig.upgradeArtifactsPath()).willReturn(zipOutputDir.toString()); + + final Bytes invalidArchive = Bytes.wrap("Not a valid zip archive".getBytes(StandardCharsets.UTF_8)); + subject.extractSoftwareUpgrade(invalidArchive).join(); + + assertThat(logCaptor.errorLogs()) + .anyMatch(l -> l.startsWith("Failed to unzip archive for NMT consumption java.io.IOException:" + " ")); + assertThat(logCaptor.errorLogs()) + .anyMatch(l -> l.equals("Manual remediation may be necessary to avoid node ISS")); + + assertThat(new File(zipOutputDir, EXEC_IMMEDIATE_MARKER)).doesNotExist(); + } + + @Test + void preparesForUpgrade() throws IOException { + setupNoiseFiles(); + rmIfPresent(EXEC_IMMEDIATE_MARKER); + + given(adminServiceConfig.upgradeArtifactsPath()).willReturn(zipOutputDir.toString()); + + final Bytes realArchive = Bytes.wrap(Files.readAllBytes(zipArchivePath)); + subject.extractSoftwareUpgrade(realArchive).join(); + + assertMarkerCreated(EXEC_IMMEDIATE_MARKER, null); + } + + @Test + void upgradesTelemetry() throws IOException { + rmIfPresent(EXEC_TELEMETRY_MARKER); + + given(adminServiceConfig.upgradeArtifactsPath()).willReturn(zipOutputDir.toString()); + + final Bytes realArchive = Bytes.wrap(Files.readAllBytes(zipArchivePath)); + subject.extractTelemetryUpgrade(realArchive, then).join(); + + assertMarkerCreated(EXEC_TELEMETRY_MARKER, then); + } + + @Test + void externalizesFreeze() throws IOException { + rmIfPresent(NOW_FROZEN_MARKER); + + given(adminServiceConfig.upgradeArtifactsPath()).willReturn(zipOutputDir.toString()); + given(freezeStore.updateFileHash()).willReturn(Bytes.wrap("fake hash")); + + subject.externalizeFreezeIfUpgradePending(); + + assertMarkerCreated(NOW_FROZEN_MARKER, null); + } + + @Test + void determinesIfFreezeIsScheduled() { + assertThat(subject.isFreezeScheduled()).isFalse(); + + given(freezeStore.freezeTime()).willReturn(then); + + assertThat(subject.isFreezeScheduled()).isTrue(); + } + + private void rmIfPresent(final String file) { + rmIfPresent(zipOutputDir.toPath(), file); + } + + private static void rmIfPresent(final Path baseDir, final String file) { + final File f = baseDir.resolve(file).toFile(); + if (f.exists()) { + boolean deleted = f.delete(); + assert (deleted); + } + } + + private void assertMarkerCreated(final String file, final @Nullable Timestamp when) throws IOException { + assertMarkerCreated(file, when, zipOutputDir.toPath()); + } + + private void assertMarkerCreated(final String file, final @Nullable Timestamp when, final Path baseDir) + throws IOException { + final Path filePath = baseDir.resolve(file); + assertThat(filePath.toFile()).exists(); + final var contents = Files.readString(filePath); + assertThat(filePath.toFile().delete()).isTrue(); + + if (file.equals(EXEC_IMMEDIATE_MARKER)) { + assertThat(logCaptor.infoLogs()) + .anyMatch(l -> (l.startsWith("About to unzip ") + && l.contains(" bytes for software update into " + baseDir))); + assertThat(logCaptor.infoLogs()) + .anyMatch(l -> (l.startsWith("Finished unzipping ") + && l.contains(" bytes for software update into " + baseDir))); + assertThat(logCaptor.infoLogs()).anyMatch(l -> (l.contains("Wrote marker " + filePath))); + } else if (file.equals(EXEC_TELEMETRY_MARKER)) { + assertThat(logCaptor.infoLogs()) + .anyMatch(l -> (l.startsWith("About to unzip ") + && l.contains(" bytes for telemetry update into " + baseDir))); + assertThat(logCaptor.infoLogs()) + .anyMatch(l -> + (l.startsWith("Finished unzipping ") && l.contains(" bytes for telemetry update into "))); + assertThat(logCaptor.infoLogs()).anyMatch(l -> (l.contains("Wrote marker " + filePath))); + } else { + assertThat(logCaptor.infoLogs()).anyMatch(l -> (l.contains("Wrote marker " + filePath))); + } + if (when != null) { + final var writtenEpochSecond = Long.parseLong(contents); + assertThat(when.seconds()).isEqualTo(writtenEpochSecond); + } else { + assertThat(contents).isEqualTo(FreezeUpgradeActions.MARK); + } + } + + private void setupNoiseFiles() throws IOException { + Files.write( + noiseFileLoc, + List.of("There, the eyes are", "Sunlight on a broken column", "There, is a tree swinging")); + Files.write( + noiseSubFileLoc, + List.of( + "And voices are", + "In the wind's singing", + "More distant and more solemn", + "Than a fading star")); + } +} diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/schemas/InitialModServiceTokenSchema.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/schemas/InitialModServiceTokenSchema.java index 525088d599f1..456a1e84bd67 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/schemas/InitialModServiceTokenSchema.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/schemas/InitialModServiceTokenSchema.java @@ -68,6 +68,7 @@ import com.hedera.node.app.spi.state.MigrationContext; import com.hedera.node.app.spi.state.Schema; import com.hedera.node.app.spi.state.StateDefinition; +import com.hedera.node.app.spi.state.WritableKVState; import com.hedera.node.app.spi.state.WritableKVStateBase; import com.hedera.node.app.spi.state.WritableSingletonStateBase; import com.hedera.node.config.data.AccountsConfig; @@ -186,7 +187,7 @@ public void setStakingFs( @Override public void migrate(@NonNull final MigrationContext ctx) { - final var isGenesis = ctx.previousStates().isEmpty(); + final var isGenesis = ctx.previousVersion() == null; if (isGenesis) { createGenesisSchema(ctx); } @@ -521,13 +522,7 @@ private void createGenesisSchema(@NonNull final MigrationContext ctx) { // ---------- Balances Safety Check ------------------------- // Aadd up the balances of all accounts, they must match 50,000,000,000 HBARs (config) - var totalBalance = 0L; - for (int i = 1; i < hederaConfig.firstUserEntity(); i++) { - final var account = accounts.get(asAccountId(i, hederaConfig)); - if (account != null) { - totalBalance += account.tinybarBalance(); - } - } + final var totalBalance = getTotalBalanceOfAllAccounts(accounts, hederaConfig); if (totalBalance != ledgerConfig.totalTinyBarFloat()) { throw new IllegalStateException("Total balance of all accounts does not match the total float: actual: " + totalBalance + " vs expected: " + ledgerConfig.totalTinyBarFloat()); @@ -538,6 +533,29 @@ private void createGenesisSchema(@NonNull final MigrationContext ctx) { accounts.modifiedKeys().size()); } + /** + * Get the total balance of all accounts. Since we cannot iterate over the accounts in VirtualMap, + * we have to do this manually. + * @param accounts The accounts map + * @param hederaConfig The Hedera configuration + * @return The total balance of all accounts + */ + public long getTotalBalanceOfAllAccounts( + @NonNull final WritableKVState accounts, @NonNull final HederaConfig hederaConfig) { + long totalBalance = 0; + long i = 1; // Start with the first account ID + long totalAccounts = accounts.size(); + do { + Account account = accounts.get(asAccountId(i, hederaConfig)); + if (account != null) { + totalBalance += account.tinybarBalance(); + totalAccounts--; + } + i++; + } while (totalAccounts > 0); + return totalBalance; + } + @VisibleForTesting public static long[] nonContractSystemNums(final long numReservedSystemEntities) { return LongStream.rangeClosed(FIRST_POST_SYSTEM_FILE_ENTITY, numReservedSystemEntities) diff --git a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/schemas/InitialModServiceTokenSchemaTest.java b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/schemas/InitialModServiceTokenSchemaTest.java index e587aa1446a3..b3cbf567a79d 100644 --- a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/schemas/InitialModServiceTokenSchemaTest.java +++ b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/schemas/InitialModServiceTokenSchemaTest.java @@ -137,7 +137,13 @@ void nonGenesisDoesntCreate() { newWritableEntityIdState()); final var schema = newSubjectWithAllExpected(); final var migrationContext = new MigrationContextImpl( - nonEmptyPrevStates, newStates, config, networkInfo, genesisRecordsBuilder, entityIdStore); + nonEmptyPrevStates, + newStates, + config, + networkInfo, + genesisRecordsBuilder, + entityIdStore, + CURRENT_VERSION); schema.migrate(migrationContext); @@ -150,10 +156,16 @@ void nonGenesisDoesntCreate() { } @Test - void initializesStakingData() { + void initializesStakingDataOnGenesisStart() { final var schema = newSubjectWithAllExpected(); final var migrationContext = new MigrationContextImpl( - EmptyReadableStates.INSTANCE, newStates, config, networkInfo, genesisRecordsBuilder, entityIdStore); + EmptyReadableStates.INSTANCE, + newStates, + config, + networkInfo, + genesisRecordsBuilder, + entityIdStore, + null); schema.migrate(migrationContext); @@ -164,10 +176,16 @@ void initializesStakingData() { } @Test - void createsAllAccounts() { + void createsAllAccountsOnGenesisStart() { final var schema = newSubjectWithAllExpected(); final var migrationContext = new MigrationContextImpl( - EmptyReadableStates.INSTANCE, newStates, config, networkInfo, genesisRecordsBuilder, entityIdStore); + EmptyReadableStates.INSTANCE, + newStates, + config, + networkInfo, + genesisRecordsBuilder, + entityIdStore, + null); schema.migrate(migrationContext); @@ -276,7 +294,13 @@ void someAccountsAlreadyExist() { new MapWritableKVState<>(ALIASES_KEY, blocklistAccts), newWritableEntityIdState()); final var migrationContext = new MigrationContextImpl( - EmptyReadableStates.INSTANCE, newStates, config, networkInfo, genesisRecordsBuilder, entityIdStore); + EmptyReadableStates.INSTANCE, + newStates, + config, + networkInfo, + genesisRecordsBuilder, + entityIdStore, + null); schema.migrate(migrationContext); @@ -396,7 +420,13 @@ void allAccountsAlreadyExist() { new MapWritableKVState<>(ALIASES_KEY, blocklistEvmAliasMappings), newWritableEntityIdState()); final var migrationContext = new MigrationContextImpl( - EmptyReadableStates.INSTANCE, newStates, config, networkInfo, genesisRecordsBuilder, entityIdStore); + EmptyReadableStates.INSTANCE, + newStates, + config, + networkInfo, + genesisRecordsBuilder, + entityIdStore, + CURRENT_VERSION); schema.migrate(migrationContext); @@ -435,7 +465,13 @@ void blocklistNotEnabled() { // None of the blocklist accounts will exist, but they shouldn't be created since blocklists aren't enabled config = buildConfig(DEFAULT_NUM_SYSTEM_ACCOUNTS, false); final var migrationContext = new MigrationContextImpl( - EmptyReadableStates.INSTANCE, newStates, config, networkInfo, genesisRecordsBuilder, entityIdStore); + EmptyReadableStates.INSTANCE, + newStates, + config, + networkInfo, + genesisRecordsBuilder, + entityIdStore, + CURRENT_VERSION); schema.migrate(migrationContext); @@ -451,7 +487,7 @@ void blocklistNotEnabled() { } @Test - void createsSystemAccountsOnly() { + void createsSystemAccountsOnlyOnGenesisStart() { final var schema = new InitialModServiceTokenSchema( this::allDefaultSysAccts, Collections::emptySortedSet, @@ -460,7 +496,13 @@ void createsSystemAccountsOnly() { Collections::emptySortedSet, CURRENT_VERSION); schema.migrate(new MigrationContextImpl( - EmptyReadableStates.INSTANCE, newStates, config, networkInfo, genesisRecordsBuilder, entityIdStore)); + EmptyReadableStates.INSTANCE, + newStates, + config, + networkInfo, + genesisRecordsBuilder, + entityIdStore, + null)); final var acctsStateResult = newStates.get(ACCOUNTS_KEY); for (int i = 1; i < DEFAULT_NUM_SYSTEM_ACCOUNTS; i++) { @@ -484,7 +526,13 @@ void createsStakingRewardAccountsOnly() { Collections::emptySortedSet, CURRENT_VERSION); schema.migrate(new MigrationContextImpl( - EmptyReadableStates.INSTANCE, newStates, config, networkInfo, genesisRecordsBuilder, entityIdStore)); + EmptyReadableStates.INSTANCE, + newStates, + config, + networkInfo, + genesisRecordsBuilder, + entityIdStore, + null)); final var acctsStateResult = newStates.get(ACCOUNTS_KEY); final var stakingRewardAccount = acctsStateResult.get(ACCT_IDS[800]); @@ -510,7 +558,13 @@ void createsTreasuryAccountsOnly() { Collections::emptySortedSet, CURRENT_VERSION); schema.migrate(new MigrationContextImpl( - EmptyReadableStates.INSTANCE, newStates, config, networkInfo, genesisRecordsBuilder, entityIdStore)); + EmptyReadableStates.INSTANCE, + newStates, + config, + networkInfo, + genesisRecordsBuilder, + entityIdStore, + null)); final var acctsStateResult = newStates.get(ACCOUNTS_KEY); for (final long reservedNum : NON_CONTRACT_RESERVED_NUMS) { @@ -534,7 +588,13 @@ void createsMiscAccountsOnly() { Collections::emptySortedSet, CURRENT_VERSION); schema.migrate(new MigrationContextImpl( - EmptyReadableStates.INSTANCE, newStates, config, networkInfo, genesisRecordsBuilder, entityIdStore)); + EmptyReadableStates.INSTANCE, + newStates, + config, + networkInfo, + genesisRecordsBuilder, + entityIdStore, + null)); final var acctsStateResult = newStates.get(ACCOUNTS_KEY); @@ -557,7 +617,13 @@ void createsBlocklistAccountsOnly() { this::allBlocklistAccts, CURRENT_VERSION); schema.migrate(new MigrationContextImpl( - EmptyReadableStates.INSTANCE, newStates, config, networkInfo, genesisRecordsBuilder, entityIdStore)); + EmptyReadableStates.INSTANCE, + newStates, + config, + networkInfo, + genesisRecordsBuilder, + entityIdStore, + null)); // Verify that the assigned account ID matches the expected entity IDs for (int i = 0; i < EVM_ADDRESSES.length; i++) { @@ -570,7 +636,13 @@ void createsBlocklistAccountsOnly() { void onlyExpectedIdsUsed() { final var schema = newSubjectWithAllExpected(); schema.migrate(new MigrationContextImpl( - EmptyReadableStates.INSTANCE, newStates, config, networkInfo, genesisRecordsBuilder, entityIdStore)); + EmptyReadableStates.INSTANCE, + newStates, + config, + networkInfo, + genesisRecordsBuilder, + entityIdStore, + null)); // Verify contract entity IDs aren't used for (int i = 350; i < 400; i++) { From ebcedb6c3453b968cbc544d8b2a0ad4dbd6f7e96 Mon Sep 17 00:00:00 2001 From: Cody Littley <56973212+cody-littley@users.noreply.github.com> Date: Thu, 7 Mar 2024 07:35:52 -0600 Subject: [PATCH 031/115] feat: event br migration (#11842) Signed-off-by: Cody Littley --- .../com/swirlds/platform/SwirldsPlatform.java | 35 +++- .../swirlds/platform/cli/DiagramCommand.java | 2 +- .../state/BirthRoundStateMigration.java | 112 ++++++++++ .../swirlds/platform/state/PlatformState.java | 94 ++++++++- .../com/swirlds/platform/state/State.java | 3 + .../system/events/BaseEventHashedData.java | 60 +++--- .../events/BirthRoundMigrationShim.java | 128 ++++++++++++ .../platform/wiring/PlatformWiring.java | 42 +++- .../BirthRoundMigrationShimWiring.java | 63 ++++++ .../state/BirthRoundStateMigrationTests.java | 176 ++++++++++++++++ .../state/RandomSignedStateGenerator.java | 30 ++- .../events/BirthRoundMigrationShimTests.java | 196 ++++++++++++++++++ .../platform/wiring/PlatformWiringTests.java | 5 +- .../platform/test/event/GossipEventTest.java | 28 --- .../sampleGossipEvent.evts | Bin 947 -> 0 bytes 15 files changed, 894 insertions(+), 80 deletions(-) create mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/BirthRoundStateMigration.java create mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/BirthRoundMigrationShim.java create mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/BirthRoundMigrationShimWiring.java create mode 100644 platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/BirthRoundStateMigrationTests.java create mode 100644 platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/system/events/BirthRoundMigrationShimTests.java delete mode 100644 platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/resources/eventFiles/eventSerializationV45/sampleGossipEvent.evts 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 0c4c886d8883..08bd6c19c342 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 @@ -24,6 +24,7 @@ import static com.swirlds.logging.legacy.LogMarker.STATE_TO_DISK; import static com.swirlds.platform.event.creation.EventCreationManagerFactory.buildEventCreationManager; import static com.swirlds.platform.event.preconsensus.PcesUtilities.getDatabaseDirectory; +import static com.swirlds.platform.state.BirthRoundStateMigration.modifyStateForBirthRoundMigration; import static com.swirlds.platform.state.address.AddressBookMetrics.registerAddressBookMetrics; import static com.swirlds.platform.state.iss.IssDetector.DO_NOT_IGNORE_ROUNDS; import static com.swirlds.platform.state.signed.SignedStateFileReader.getSavedStateFiles; @@ -123,6 +124,7 @@ import com.swirlds.platform.metrics.SyncMetrics; import com.swirlds.platform.metrics.TransactionMetrics; import com.swirlds.platform.recovery.EmergencyRecoveryManager; +import com.swirlds.platform.state.PlatformState; import com.swirlds.platform.state.State; import com.swirlds.platform.state.SwirldStateManager; import com.swirlds.platform.state.iss.IssDetector; @@ -155,6 +157,7 @@ import com.swirlds.platform.system.address.Address; import com.swirlds.platform.system.address.AddressBook; import com.swirlds.platform.system.address.AddressBookUtils; +import com.swirlds.platform.system.events.BirthRoundMigrationShim; import com.swirlds.platform.system.status.PlatformStatus; import com.swirlds.platform.system.status.PlatformStatusManager; import com.swirlds.platform.system.status.actions.DoneReplayingEventsAction; @@ -322,6 +325,9 @@ public class SwirldsPlatform implements Platform { .getConfigData(EventConfig.class) .getAncientMode(); + // This method is a no-op if we are not in birth round mode, or if we have already migrated. + modifyStateForBirthRoundMigration(initialState, ancientMode, appVersion); + this.emergencyRecoveryManager = Objects.requireNonNull(emergencyRecoveryManager, "emergencyRecoveryManager"); final Time time = Time.getCurrent(); @@ -468,7 +474,7 @@ public class SwirldsPlatform implements Platform { final LatestCompleteStateNexus latestCompleteState = new LatestCompleteStateNexus(stateConfig, platformContext.getMetrics()); - platformWiring = components.add(new PlatformWiring(platformContext, time)); + platformWiring = components.add(new PlatformWiring(platformContext)); final boolean useOldStyleIntakeQueue = eventConfig.useOldStyleIntakeQueue(); @@ -663,6 +669,8 @@ public class SwirldsPlatform implements Platform { final HashLogger hashLogger = new HashLogger(platformContext.getConfiguration().getConfigData(StateConfig.class)); + final BirthRoundMigrationShim birthRoundMigrationShim = buildBirthRoundMigrationShim(initialState); + platformWiring.bind( eventHasher, internalEventValidator, @@ -687,6 +695,7 @@ public class SwirldsPlatform implements Platform { issDetector, issHandler, hashLogger, + birthRoundMigrationShim, latestCompleteStateNotifier); // Load the minimum generation into the pre-consensus event writer @@ -809,6 +818,30 @@ public class SwirldsPlatform implements Platform { GuiPlatformAccessor.getInstance().setLatestImmutableStateComponent(selfId, latestImmutableState); } + /** + * Builds the birth round migration shim if necessary. + * + * @param initialState the initial state + * @return the birth round migration shim, or null if it is not needed + */ + @Nullable + private BirthRoundMigrationShim buildBirthRoundMigrationShim(@NonNull final SignedState initialState) { + + if (ancientMode == AncientMode.GENERATION_THRESHOLD) { + // We don't need the shim if we haven't migrated to birth round mode. + return null; + } + + final State state = initialState.getState(); + final PlatformState platformState = state.getPlatformState(); + + return new BirthRoundMigrationShim( + platformContext, + platformState.getFirstVersionInBirthRoundMode(), + platformState.getLastRoundBeforeBirthRoundMode(), + platformState.getLowestJudgeGenerationBeforeBirthRoundMode()); + } + /** * Clears all pipelines in preparation for a reconnect. This method is needed to break a circular dependency. */ diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/DiagramCommand.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/DiagramCommand.java index fcb6b5f123e1..85fbf758473d 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/DiagramCommand.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/DiagramCommand.java @@ -99,7 +99,7 @@ public Integer call() throws IOException { final PlatformContext platformContext = new DefaultPlatformContext( configuration, new NoOpMetrics(), CryptographyHolder.get(), Time.getCurrent()); - final PlatformWiring platformWiring = new PlatformWiring(platformContext, Time.getCurrent()); + final PlatformWiring platformWiring = new PlatformWiring(platformContext); final ThreadManager threadManager = getStaticThreadManager(); final NotificationEngine notificationEngine = NotificationEngine.buildEngine(threadManager); 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 new file mode 100644 index 000000000000..a65d0fbaafb7 --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/BirthRoundStateMigration.java @@ -0,0 +1,112 @@ +/* + * 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.state; + +import static com.swirlds.logging.legacy.LogMarker.STARTUP; + +import com.swirlds.common.merkle.crypto.MerkleCryptoFactory; +import com.swirlds.platform.consensus.ConsensusSnapshot; +import com.swirlds.platform.event.AncientMode; +import com.swirlds.platform.state.signed.SignedState; +import com.swirlds.platform.system.SoftwareVersion; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * A utility for migrating the state when birth round mode is first enabled. + */ +public final class BirthRoundStateMigration { + + private static final Logger logger = LogManager.getLogger(BirthRoundStateMigration.class); + + private BirthRoundStateMigration() {} + + /** + * Perform required state changes for the migration to birth round mode. This method is a no-op if it is not yet + * time to migrate, or if the migration has already been completed. + * + * @param initialState the initial state the platform is starting with + * @param ancientMode the current ancient mode + * @param appVersion the current application version + */ + public static void modifyStateForBirthRoundMigration( + @NonNull final SignedState initialState, + @NonNull final AncientMode ancientMode, + @NonNull final SoftwareVersion appVersion) { + + if (ancientMode == AncientMode.GENERATION_THRESHOLD) { + if (initialState.getState().getPlatformState().getFirstVersionInBirthRoundMode() != null) { + throw new IllegalStateException( + "Cannot revert to generation mode after birth round migration has been completed."); + } + + logger.info( + STARTUP.getMarker(), "Birth round state migration is not yet needed, still in generation mode."); + return; + } + + final State state = initialState.getState(); + final PlatformState platformState = state.getPlatformState(); + + final boolean alreadyMigrated = platformState.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 ConsensusSnapshot consensusSnapshot = Objects.requireNonNull(platformState.getSnapshot()); + final List judgeInfoList = consensusSnapshot.getMinimumJudgeInfoList(); + final long lowestJudgeGenerationBeforeMigration = + judgeInfoList.getLast().minimumJudgeAncientThreshold(); + + logger.info( + STARTUP.getMarker(), + "Birth round state migration in progress. First version in birth round mode: {}, " + + "last round before migration: {}, lowest judge generation before migration: {}", + appVersion, + lastRoundBeforeMigration, + lowestJudgeGenerationBeforeMigration); + + platformState.setFirstVersionInBirthRoundMode(appVersion); + platformState.setLastRoundBeforeBirthRoundMode(lastRoundBeforeMigration); + platformState.setLowestJudgeGenerationBeforeBirthRoundMode(lowestJudgeGenerationBeforeMigration); + + final List modifiedJudgeInfoList = new ArrayList<>(judgeInfoList.size()); + for (final MinimumJudgeInfo judgeInfo : judgeInfoList) { + modifiedJudgeInfoList.add(new MinimumJudgeInfo(judgeInfo.round(), lastRoundBeforeMigration)); + } + final ConsensusSnapshot modifiedConsensusSnapshot = new ConsensusSnapshot( + consensusSnapshot.round(), + consensusSnapshot.judgeHashes(), + modifiedJudgeInfoList, + consensusSnapshot.nextConsensusNumber(), + consensusSnapshot.consensusTimestamp()); + platformState.setSnapshot(modifiedConsensusSnapshot); + + // rehash the state + platformState.invalidateHash(); + state.invalidateHash(); + MerkleCryptoFactory.getInstance().digestTreeSync(state); + } +} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/PlatformState.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/PlatformState.java index 661c0a5a86a6..8573bef872a6 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/PlatformState.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/PlatformState.java @@ -47,6 +47,10 @@ public class PlatformState extends PartialMerkleLeaf implements MerkleLeaf { private static final class ClassVersion { public static final int ORIGINAL = 1; + /** + * Added state to allow for birth round migration. + */ + public static final int BIRTH_ROUND_MIGRATION_PATHWAY = 2; } /** @@ -119,6 +123,23 @@ private static final class ClassVersion { */ private UptimeDataImpl uptimeData = new UptimeDataImpl(); + /** + * Null if birth round migration has not yet happened, otherwise the software version that was first used when the + * birth round migration was performed. + */ + private SoftwareVersion firstVersionInBirthRoundMode; + + /** + * The last round before the birth round mode was enabled, or -1 if birth round mode has not yet been enabled. + */ + private long lastRoundBeforeBirthRoundMode = -1; + + /** + * The lowest judge generation before the birth round mode was enabled, or -1 if birth round mode has not yet been + * enabled. + */ + private long lowestJudgeGenerationBeforeBirthRoundMode = -1; + public PlatformState() {} /** @@ -141,6 +162,9 @@ private PlatformState(final PlatformState that) { this.freezeTime = that.freezeTime; this.lastFrozenTime = that.lastFrozenTime; this.uptimeData = that.uptimeData.copy(); + this.firstVersionInBirthRoundMode = that.firstVersionInBirthRoundMode; + this.lastRoundBeforeBirthRoundMode = that.lastRoundBeforeBirthRoundMode; + this.lowestJudgeGenerationBeforeBirthRoundMode = that.lowestJudgeGenerationBeforeBirthRoundMode; } /** @@ -184,6 +208,9 @@ public void serialize(final SerializableDataOutputStream out) throws IOException out.writeInstant(freezeTime); out.writeInstant(lastFrozenTime); out.writeSerializable(uptimeData, false); + out.writeSerializable(firstVersionInBirthRoundMode, true); + out.writeLong(lastRoundBeforeBirthRoundMode); + out.writeLong(lowestJudgeGenerationBeforeBirthRoundMode); } /** @@ -203,6 +230,11 @@ public void deserialize(final SerializableDataInputStream in, final int version) freezeTime = in.readInstant(); lastFrozenTime = in.readInstant(); uptimeData = in.readSerializable(false, UptimeDataImpl::new); + if (version >= ClassVersion.BIRTH_ROUND_MIGRATION_PATHWAY) { + firstVersionInBirthRoundMode = in.readSerializable(); + lastRoundBeforeBirthRoundMode = in.readLong(); + lowestJudgeGenerationBeforeBirthRoundMode = in.readLong(); + } } /** @@ -210,7 +242,7 @@ public void deserialize(final SerializableDataInputStream in, final int version) */ @Override public int getVersion() { - return ClassVersion.ORIGINAL; + return ClassVersion.BIRTH_ROUND_MIGRATION_PATHWAY; } /** @@ -226,7 +258,7 @@ public PlatformState copy() { * * @return the creation version */ - @Nullable + @NonNull public SoftwareVersion getCreationSoftwareVersion() { return creationSoftwareVersion; } @@ -489,4 +521,62 @@ public UptimeDataImpl getUptimeData() { public void setUptimeData(@NonNull final UptimeDataImpl uptimeData) { this.uptimeData = Objects.requireNonNull(uptimeData); } + + /** + * Get the first software version where the birth round migration happened, or null if birth round migration has not + * yet happened. + * + * @return the first software version where the birth round migration happened + */ + @Nullable + public SoftwareVersion getFirstVersionInBirthRoundMode() { + return firstVersionInBirthRoundMode; + } + + /** + * Set the first software version where the birth round migration happened. + * + * @param firstVersionInBirthRoundMode the first software version where the birth round migration happened + */ + public void setFirstVersionInBirthRoundMode(final SoftwareVersion firstVersionInBirthRoundMode) { + this.firstVersionInBirthRoundMode = firstVersionInBirthRoundMode; + } + + /** + * Get the last round before the birth round mode was enabled, or -1 if birth round mode has not yet been enabled. + * + * @return the last round before the birth round mode was enabled + */ + public long getLastRoundBeforeBirthRoundMode() { + return lastRoundBeforeBirthRoundMode; + } + + /** + * Set the last round before the birth round mode was enabled. + * + * @param lastRoundBeforeBirthRoundMode the last round before the birth round mode was enabled + */ + public void setLastRoundBeforeBirthRoundMode(final long lastRoundBeforeBirthRoundMode) { + this.lastRoundBeforeBirthRoundMode = lastRoundBeforeBirthRoundMode; + } + + /** + * Get the lowest judge generation before the birth round mode was enabled, or -1 if birth round mode has not yet + * been enabled. + * + * @return the lowest judge generation before the birth round mode was enabled + */ + public long getLowestJudgeGenerationBeforeBirthRoundMode() { + return lowestJudgeGenerationBeforeBirthRoundMode; + } + + /** + * Set the lowest judge generation before the birth round mode was enabled. + * + * @param lowestJudgeGenerationBeforeBirthRoundMode the lowest judge generation before the birth round mode was + * enabled + */ + public void setLowestJudgeGenerationBeforeBirthRoundMode(final long lowestJudgeGenerationBeforeBirthRoundMode) { + this.lowestJudgeGenerationBeforeBirthRoundMode = lowestJudgeGenerationBeforeBirthRoundMode; + } } 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 4470abe81e22..46aed4aa7813 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 @@ -243,6 +243,9 @@ public String getInfoString(final int hashDepth) { .addRow("Epoch hash:", epochHash) .addRow("Minimum judge hash code:", minimumJudgeInfo == null ? "null" : minimumJudgeInfo.hashCode()) .addRow("Root hash:", getHash()) + .addRow("First BR Version:", platformState.getFirstVersionInBirthRoundMode()) + .addRow("Last round before BR:", platformState.getLastRoundBeforeBirthRoundMode()) + .addRow("Lowest Judge Gen before BR", platformState.getLowestJudgeGenerationBeforeBirthRoundMode()) .render(sb); sb.append("\n"); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/BaseEventHashedData.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/BaseEventHashedData.java index 9b0f60ec4d6c..9a83578152e2 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/BaseEventHashedData.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/BaseEventHashedData.java @@ -29,7 +29,6 @@ import com.swirlds.common.utility.CommonUtils; import com.swirlds.platform.config.TransactionConfig; import com.swirlds.platform.system.SoftwareVersion; -import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.system.address.AddressBook; import com.swirlds.platform.system.transaction.ConsensusTransactionImpl; import com.swirlds.platform.system.transaction.StateSignatureTransaction; @@ -39,7 +38,6 @@ import java.io.IOException; import java.time.Instant; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Set; @@ -109,6 +107,12 @@ public static class ClassVersion { /** the payload: an array of transactions */ private ConsensusTransactionImpl[] transactions; + /** + * The actual birth round to return. May not be the original birth round if this event was created in the software + * version right before the birth round migration. + */ + private long birthRoundOverride; + /** * Class IDs of permitted transaction types. */ @@ -143,13 +147,14 @@ public BaseEventHashedData( otherParents.forEach(Objects::requireNonNull); this.otherParents = otherParents; this.birthRound = birthRound; + this.birthRoundOverride = birthRound; this.timeCreated = Objects.requireNonNull(timeCreated, "The timeCreated must not be null"); this.transactions = transactions; } @Override public int getMinimumSupportedVersion() { - return ClassVersion.TRANSACTION_SUBCLASSES; + return ClassVersion.BIRTH_ROUND; } @Override @@ -203,34 +208,17 @@ public void deserialize( throws IOException { Objects.requireNonNull(in, "The input stream must not be null"); serializedVersion = version; - softwareVersion = in.readSerializable(StaticSoftwareVersion.getSoftwareVersionClassIdSet()); - if (version < ClassVersion.BIRTH_ROUND) { - // FUTURE WORK: The creatorId should be a selfSerializable NodeId at some point. - // Changing the event format may require a HIP. The old format is preserved for now. - creatorId = NodeId.deserializeLong(in, false); - final long selfParentGen = in.readLong(); - final long otherParentGen = in.readLong(); - final Hash selfParentHash = in.readSerializable(false, Hash::new); - final Hash otherParentHash = in.readSerializable(false, Hash::new); - selfParent = selfParentHash == null - ? null - : new EventDescriptor( - selfParentHash, creatorId, selfParentGen, EventConstants.BIRTH_ROUND_UNDEFINED); - // The creator for the other parent descriptor is not here and should be retrieved from the unhashed data. - otherParents = otherParentHash == null - ? Collections.emptyList() - : Collections.singletonList( - new EventDescriptor(otherParentHash, otherParentGen, EventConstants.BIRTH_ROUND_UNDEFINED)); - birthRound = EventConstants.BIRTH_ROUND_UNDEFINED; - } else { - creatorId = in.readSerializable(false, NodeId::new); - if (creatorId == null) { - throw new IOException("creatorId is null"); - } - selfParent = in.readSerializable(false, EventDescriptor::new); - otherParents = in.readSerializableList(AddressBook.MAX_ADDRESSES, false, EventDescriptor::new); - birthRound = in.readLong(); + softwareVersion = in.readSerializable(); + + creatorId = in.readSerializable(false, NodeId::new); + if (creatorId == null) { + throw new IOException("creatorId is null"); } + selfParent = in.readSerializable(false, EventDescriptor::new); + otherParents = in.readSerializableList(AddressBook.MAX_ADDRESSES, false, EventDescriptor::new); + birthRound = in.readLong(); + birthRoundOverride = birthRound; + timeCreated = in.readInstant(); in.readInt(); // read serialized length transactions = @@ -314,13 +302,23 @@ public NodeId getCreatorId() { return creatorId; } + /** + * Override the birth round for this event. This will only be called for events created in the software version + * right before the birth round migration. + * + * @param birthRoundOverride the birth round that has been assigned to this event + */ + public void setBirthRoundOverride(final long birthRoundOverride) { + this.birthRoundOverride = birthRoundOverride; + } + /** * Get the birth round of the event. * * @return the birth round of the event */ public long getBirthRound() { - return birthRound; + return birthRoundOverride; } /** diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/BirthRoundMigrationShim.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/BirthRoundMigrationShim.java new file mode 100644 index 000000000000..29582b8eb00a --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/BirthRoundMigrationShim.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.swirlds.platform.system.events; + +import static com.swirlds.logging.legacy.LogMarker.STARTUP; +import static com.swirlds.platform.consensus.ConsensusConstants.ROUND_FIRST; + +import com.swirlds.common.context.PlatformContext; +import com.swirlds.common.metrics.SpeedometerMetric; +import com.swirlds.common.utility.CompareTo; +import com.swirlds.platform.event.GossipEvent; +import com.swirlds.platform.system.SoftwareVersion; +import edu.umd.cs.findbugs.annotations.NonNull; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Performs special migration on events during the birth round migration pathway. + */ +public class BirthRoundMigrationShim { + + private static final SpeedometerMetric.Config SHIM_ANCIENT_EVENTS = new SpeedometerMetric.Config( + "platform", "shimAncientEvents") + .withDescription("Events that the BirthRoundMigrationShim gave an ancient birth round override") + .withUnit("hz"); + + private final SpeedometerMetric shimAncientEvents; + + private static final SpeedometerMetric.Config SHIM_BARELY_NON_ANCIENT_EVENTS = new SpeedometerMetric.Config( + "platform", "shimBarelyNonAncientEvents") + .withDescription("Events that the BirthRoundMigrationShim gave a barely non-ancient birth round override") + .withUnit("hz"); + + private final SpeedometerMetric shimBarelyNonAncientEvents; + + private static final Logger logger = LogManager.getLogger(BirthRoundMigrationShim.class); + + /** + * The first software version where the birth round mode is enabled. Events from this software version and later are + * not modified by this object. Events from earlier software versions have their birth rounds modified by this + * object. + */ + private final SoftwareVersion firstVersionInBirthRoundMode; + + /** + * The last round before the birth round mode was enabled. + */ + private final long lastRoundBeforeBirthRoundMode; + + /** + * The lowest judge generation before the birth round mode was enabled. + */ + private final long lowestJudgeGenerationBeforeBirthRoundMode; + + /** + * Constructs a new BirthRoundMigrationShim. + * + * @param platformContext the platform context + * @param firstVersionInBirthRoundMode the first software version where the birth round mode is + * enabled + * @param lastRoundBeforeBirthRoundMode the last round before the birth round mode was enabled + * @param lowestJudgeGenerationBeforeBirthRoundMode the lowest judge generation before the birth round mode was + * enabled + */ + public BirthRoundMigrationShim( + @NonNull final PlatformContext platformContext, + @NonNull final SoftwareVersion firstVersionInBirthRoundMode, + final long lastRoundBeforeBirthRoundMode, + final long lowestJudgeGenerationBeforeBirthRoundMode) { + + logger.info( + STARTUP.getMarker(), + "BirthRoundMigrationShim initialized with firstVersionInBirthRoundMode={}, " + + "lastRoundBeforeBirthRoundMode={}, lowestJudgeGenerationBeforeBirthRoundMode={}", + firstVersionInBirthRoundMode, + lastRoundBeforeBirthRoundMode, + lowestJudgeGenerationBeforeBirthRoundMode); + + this.firstVersionInBirthRoundMode = firstVersionInBirthRoundMode; + this.lastRoundBeforeBirthRoundMode = lastRoundBeforeBirthRoundMode; + this.lowestJudgeGenerationBeforeBirthRoundMode = lowestJudgeGenerationBeforeBirthRoundMode; + + shimAncientEvents = platformContext.getMetrics().getOrCreate(SHIM_ANCIENT_EVENTS); + shimBarelyNonAncientEvents = platformContext.getMetrics().getOrCreate(SHIM_BARELY_NON_ANCIENT_EVENTS); + } + + /** + * Migrate an event's birth round, if needed. + * + * @param event the event to migrate + * @return the migrated event + */ + @NonNull + public GossipEvent migrateEvent(@NonNull final GossipEvent event) { + if (CompareTo.isLessThan(event.getHashedData().getSoftwareVersion(), firstVersionInBirthRoundMode)) { + // The event was created before the birth round mode was enabled. + // We need to migrate the event's birth round. + + if (event.getGeneration() >= lowestJudgeGenerationBeforeBirthRoundMode) { + // Any event with a generation greater than or equal to the lowest pre-migration judge generation + // is given a birth round that will be non-ancient at migration time. + event.getHashedData().setBirthRoundOverride(lastRoundBeforeBirthRoundMode); + shimBarelyNonAncientEvents.cycle(); + } else { + // All other pre-migration events are given a birth round that will + // cause them to be immediately ancient. + event.getHashedData().setBirthRoundOverride(ROUND_FIRST); + shimAncientEvents.cycle(); + } + } + + return event; + } +} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformWiring.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformWiring.java index a5672433e098..a30c6e105751 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformWiring.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformWiring.java @@ -21,7 +21,6 @@ import com.swirlds.base.state.Startable; import com.swirlds.base.state.Stoppable; -import com.swirlds.base.time.Time; import com.swirlds.common.context.PlatformContext; import com.swirlds.common.io.IOIterator; import com.swirlds.common.notification.NotificationEngine; @@ -39,6 +38,7 @@ import com.swirlds.platform.components.ConsensusEngine; import com.swirlds.platform.components.appcomm.LatestCompleteStateNotifier; import com.swirlds.platform.consensus.NonAncientEventWindow; +import com.swirlds.platform.event.AncientMode; import com.swirlds.platform.event.FutureEventBuffer; import com.swirlds.platform.event.GossipEvent; import com.swirlds.platform.event.creation.EventCreationManager; @@ -54,6 +54,7 @@ import com.swirlds.platform.event.validation.EventSignatureValidator; import com.swirlds.platform.event.validation.InternalEventValidator; import com.swirlds.platform.eventhandling.ConsensusRoundHandler; +import com.swirlds.platform.eventhandling.EventConfig; import com.swirlds.platform.eventhandling.TransactionPool; import com.swirlds.platform.gossip.shadowgraph.Shadowgraph; import com.swirlds.platform.internal.ConsensusRound; @@ -67,12 +68,14 @@ import com.swirlds.platform.state.signed.StateDumpRequest; import com.swirlds.platform.state.signed.StateSavingResult; import com.swirlds.platform.state.signed.StateSignatureCollector; +import com.swirlds.platform.system.events.BirthRoundMigrationShim; import com.swirlds.platform.system.state.notifications.IssListener; import com.swirlds.platform.system.state.notifications.IssNotification; import com.swirlds.platform.system.status.PlatformStatusManager; import com.swirlds.platform.system.status.actions.CatastrophicFailureAction; import com.swirlds.platform.util.HashLogger; import com.swirlds.platform.wiring.components.ApplicationTransactionPrehandlerWiring; +import com.swirlds.platform.wiring.components.BirthRoundMigrationShimWiring; import com.swirlds.platform.wiring.components.ConsensusRoundHandlerWiring; import com.swirlds.platform.wiring.components.EventCreationManagerWiring; import com.swirlds.platform.wiring.components.EventDurabilityNexusWiring; @@ -93,8 +96,10 @@ import com.swirlds.platform.wiring.components.ShadowgraphWiring; import com.swirlds.platform.wiring.components.StateSignatureCollectorWiring; import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; import java.time.Duration; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.concurrent.ForkJoinPool; import java.util.function.LongSupplier; @@ -138,16 +143,15 @@ public class PlatformWiring implements Startable, Stoppable, Clearable { private final IssHandlerWiring issHandlerWiring; private final HashLoggerWiring hashLoggerWiring; private final LatestCompleteStateNotifierWiring latestCompleteStateNotifierWiring; - private final PlatformCoordinator platformCoordinator; + private final BirthRoundMigrationShimWiring birthRoundMigrationShimWiring; /** * Constructor. * * @param platformContext the platform context - * @param time provides wall clock time */ - public PlatformWiring(@NonNull final PlatformContext platformContext, @NonNull final Time time) { + public PlatformWiring(@NonNull final PlatformContext platformContext) { final PlatformSchedulersConfig schedulersConfig = platformContext.getConfiguration().getConfigData(PlatformSchedulersConfig.class); @@ -158,7 +162,7 @@ public PlatformWiring(@NonNull final PlatformContext platformContext, @NonNull f final ForkJoinPool defaultPool = new ForkJoinPool(parallelism); logger.info(STARTUP.getMarker(), "Default platform pool parallelism: {}", parallelism); - model = WiringModel.create(platformContext, time, defaultPool); + model = WiringModel.create(platformContext, platformContext.getTime(), defaultPool); // This counter spans both the event hasher and the post hash collector. This is a workaround for the current // inability of concurrent schedulers to handle backpressure from an immediately subsequent scheduler. @@ -173,6 +177,16 @@ public PlatformWiring(@NonNull final PlatformContext platformContext, @NonNull f final PlatformSchedulers schedulers = PlatformSchedulers.create(platformContext, model, hashingObjectCounter); + final AncientMode ancientMode = platformContext + .getConfiguration() + .getConfigData(EventConfig.class) + .getAncientMode(); + if (ancientMode == AncientMode.BIRTH_ROUND_THRESHOLD) { + birthRoundMigrationShimWiring = BirthRoundMigrationShimWiring.create(model); + } else { + birthRoundMigrationShimWiring = null; + } + eventHasherWiring = EventHasherWiring.create(schedulers.eventHasherScheduler()); postHashCollectorWiring = PostHashCollectorWiring.create(schedulers.postHashCollectorScheduler()); internalEventValidatorWiring = @@ -262,7 +276,15 @@ private void solderNonAncientEventWindow() { * Wire the components together. */ private void wire() { - gossipWiring.eventOutput().solderTo(eventHasherWiring.eventInput()); + final InputWire pipelineInputWire; + if (birthRoundMigrationShimWiring != null) { + birthRoundMigrationShimWiring.eventOutput().solderTo(eventHasherWiring.eventInput()); + pipelineInputWire = birthRoundMigrationShimWiring.eventInput(); + } else { + pipelineInputWire = eventHasherWiring.eventInput(); + } + + gossipWiring.eventOutput().solderTo(pipelineInputWire); eventHasherWiring.eventOutput().solderTo(postHashCollectorWiring.eventInput()); postHashCollectorWiring.eventOutput().solderTo(internalEventValidatorWiring.eventInput()); internalEventValidatorWiring.eventOutput().solderTo(eventDeduplicatorWiring.eventInput()); @@ -285,7 +307,7 @@ private void wire() { solderNonAncientEventWindow(); pcesReplayerWiring.doneStreamingPcesOutputWire().solderTo(pcesWriterWiring.doneStreamingPcesInputWire()); - pcesReplayerWiring.eventOutput().solderTo(eventHasherWiring.eventInput()); + pcesReplayerWiring.eventOutput().solderTo(pipelineInputWire); // Create the transformer that extracts keystone event sequence number from consensus rounds. // This is done here instead of in ConsensusEngineWiring, since the transformer needs to be soldered with @@ -400,6 +422,8 @@ public void wireExternalComponents( * @param issDetector the ISS detector to bind * @param issHandler the ISS handler to bind * @param hashLogger the hash logger to bind + * @param birthRoundMigrationShim the birth round migration shim to bind, ignored if birth round migration has not + * yet happened, must not be null if birth round migration has happened * @param completeStateNotifier the latest complete state notifier to bind */ public void bind( @@ -426,6 +450,7 @@ public void bind( @NonNull final IssDetector issDetector, @NonNull final IssHandler issHandler, @NonNull final HashLogger hashLogger, + @Nullable final BirthRoundMigrationShim birthRoundMigrationShim, @NonNull final LatestCompleteStateNotifier completeStateNotifier) { eventHasherWiring.bind(eventHasher); @@ -451,6 +476,9 @@ public void bind( issDetectorWiring.bind(issDetector); issHandlerWiring.bind(issHandler); hashLoggerWiring.bind(hashLogger); + if (birthRoundMigrationShimWiring != null) { + birthRoundMigrationShimWiring.bind(Objects.requireNonNull(birthRoundMigrationShim)); + } latestCompleteStateNotifierWiring.bind(completeStateNotifier); } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/BirthRoundMigrationShimWiring.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/BirthRoundMigrationShimWiring.java new file mode 100644 index 000000000000..785ac34049d3 --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/BirthRoundMigrationShimWiring.java @@ -0,0 +1,63 @@ +/* + * 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.wiring.components; + +import com.swirlds.common.wiring.model.WiringModel; +import com.swirlds.common.wiring.schedulers.TaskScheduler; +import com.swirlds.common.wiring.schedulers.builders.TaskSchedulerType; +import com.swirlds.common.wiring.wires.input.BindableInputWire; +import com.swirlds.common.wiring.wires.input.InputWire; +import com.swirlds.common.wiring.wires.output.OutputWire; +import com.swirlds.platform.event.GossipEvent; +import com.swirlds.platform.system.events.BirthRoundMigrationShim; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Wiring for the {@link com.swirlds.platform.system.events.BirthRoundMigrationShim}. + * + * @param eventInput the input wire for events to be migrated + * @param eventOutput the output wire for migrated events + */ +public record BirthRoundMigrationShimWiring( + @NonNull InputWire eventInput, @NonNull OutputWire eventOutput) { + + /** + * Create a new instance of this wiring. + * + * @param model the wiring model + * @return the new wiring instance + */ + public static BirthRoundMigrationShimWiring create(@NonNull final WiringModel model) { + + final TaskScheduler scheduler = model.schedulerBuilder("birthRoundMigrationShim") + .withType(TaskSchedulerType.DIRECT_THREADSAFE) + .build() + .cast(); + + return new BirthRoundMigrationShimWiring( + scheduler.buildInputWire("un-migrated events"), scheduler.getOutputWire()); + } + + /** + * Bind a birth round migration shim to this wiring. + * + * @param shim the birth round migration shim to bind + */ + public void bind(@NonNull final BirthRoundMigrationShim shim) { + ((BindableInputWire) eventInput).bind(shim::migrateEvent); + } +} 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 new file mode 100644 index 000000000000..4dc63001877d --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/BirthRoundStateMigrationTests.java @@ -0,0 +1,176 @@ +/* + * 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.state; + +import static com.swirlds.common.merkle.utility.MerkleUtils.rehashTree; +import static com.swirlds.common.test.fixtures.RandomUtils.getRandomPrintSeed; +import static com.swirlds.common.test.fixtures.RandomUtils.randomHash; +import static com.swirlds.common.test.fixtures.RandomUtils.randomInstant; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import com.swirlds.common.context.PlatformContext; +import com.swirlds.common.crypto.Hash; +import com.swirlds.common.test.fixtures.platform.TestPlatformContextBuilder; +import com.swirlds.platform.consensus.ConsensusSnapshot; +import com.swirlds.platform.event.AncientMode; +import com.swirlds.platform.state.signed.SignedState; +import com.swirlds.platform.system.BasicSoftwareVersion; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.junit.jupiter.api.Test; + +class BirthRoundStateMigrationTests { + + @NonNull + private SignedState generateSignedState( + @NonNull final Random random, @NonNull final PlatformContext platformContext) { + + final long round = random.nextLong(1, 1_000_000); + + final List judgeHashes = new ArrayList<>(); + final int judgeHashCount = random.nextInt(5, 10); + for (int i = 0; i < judgeHashCount; i++) { + judgeHashes.add(randomHash(random)); + } + + final Instant consensusTimestamp = randomInstant(random); + + final long nextConsensusNumber = random.nextLong(0, Long.MAX_VALUE); + + final List minimumJudgeInfoList = new ArrayList<>(); + long generation = random.nextLong(1, 1_000_000); + for (int i = 0; i < 26; i++) { + final long judgeRound = round - 25 + i; + minimumJudgeInfoList.add(new MinimumJudgeInfo(judgeRound, generation)); + generation += random.nextLong(1, 100); + } + + final ConsensusSnapshot snapshot = new ConsensusSnapshot( + round, judgeHashes, minimumJudgeInfoList, nextConsensusNumber, consensusTimestamp); + + return new RandomSignedStateGenerator(random) + .setConsensusSnapshot(snapshot) + .setRound(round) + .build(); + } + + @Test + void generationModeTest() { + final Random random = getRandomPrintSeed(); + final PlatformContext platformContext = + TestPlatformContextBuilder.create().build(); + + final SignedState signedState = generateSignedState(random, platformContext); + final Hash originalHash = signedState.getState().getHash(); + + final BasicSoftwareVersion previousSoftwareVersion = + (BasicSoftwareVersion) signedState.getState().getPlatformState().getCreationSoftwareVersion(); + + final BasicSoftwareVersion newSoftwareVersion = + new BasicSoftwareVersion(previousSoftwareVersion.getSoftwareVersion() + 1); + + BirthRoundStateMigration.modifyStateForBirthRoundMigration( + signedState, AncientMode.GENERATION_THRESHOLD, newSoftwareVersion); + + assertEquals(originalHash, signedState.getState().getHash()); + + // Rehash the state, just in case + rehashTree(signedState.getState()); + + assertEquals(originalHash, signedState.getState().getHash()); + } + + @Test + void alreadyMigratedTest() { + final Random random = getRandomPrintSeed(); + final PlatformContext platformContext = + TestPlatformContextBuilder.create().build(); + + final SignedState signedState = generateSignedState(random, platformContext); + + final BasicSoftwareVersion previousSoftwareVersion = + (BasicSoftwareVersion) signedState.getState().getPlatformState().getCreationSoftwareVersion(); + + final BasicSoftwareVersion newSoftwareVersion = + new BasicSoftwareVersion(previousSoftwareVersion.getSoftwareVersion() + 1); + + signedState.getState().getPlatformState().setLastRoundBeforeBirthRoundMode(signedState.getRound() - 100); + signedState.getState().getPlatformState().setFirstVersionInBirthRoundMode(previousSoftwareVersion); + signedState.getState().getPlatformState().setLowestJudgeGenerationBeforeBirthRoundMode(100); + rehashTree(signedState.getState()); + final Hash originalHash = signedState.getState().getHash(); + + BirthRoundStateMigration.modifyStateForBirthRoundMigration( + signedState, AncientMode.BIRTH_ROUND_THRESHOLD, newSoftwareVersion); + + assertEquals(originalHash, signedState.getState().getHash()); + + // Rehash the state, just in case + rehashTree(signedState.getState()); + + assertEquals(originalHash, signedState.getState().getHash()); + } + + @Test + void migrationTest() { + final Random random = getRandomPrintSeed(); + final PlatformContext platformContext = + TestPlatformContextBuilder.create().build(); + + final SignedState signedState = generateSignedState(random, platformContext); + final Hash originalHash = signedState.getState().getHash(); + + final BasicSoftwareVersion previousSoftwareVersion = + (BasicSoftwareVersion) signedState.getState().getPlatformState().getCreationSoftwareVersion(); + + final BasicSoftwareVersion newSoftwareVersion = + new BasicSoftwareVersion(previousSoftwareVersion.getSoftwareVersion() + 1); + + final long lastRoundMinimumJudgeGeneration = signedState + .getState() + .getPlatformState() + .getSnapshot() + .getMinimumJudgeInfoList() + .getLast() + .minimumJudgeAncientThreshold(); + + BirthRoundStateMigration.modifyStateForBirthRoundMigration( + signedState, AncientMode.BIRTH_ROUND_THRESHOLD, newSoftwareVersion); + + assertNotEquals(originalHash, signedState.getState().getHash()); + + // We expect these fields to be populated at the migration boundary + assertEquals( + newSoftwareVersion, signedState.getState().getPlatformState().getFirstVersionInBirthRoundMode()); + assertEquals( + lastRoundMinimumJudgeGeneration, + signedState.getState().getPlatformState().getLowestJudgeGenerationBeforeBirthRoundMode()); + assertEquals( + signedState.getRound(), + signedState.getState().getPlatformState().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()) { + assertEquals(signedState.getRound(), minimumJudgeInfo.minimumJudgeAncientThreshold()); + } + } +} diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/RandomSignedStateGenerator.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/RandomSignedStateGenerator.java index 7cf6dc7cf18b..86ec321403eb 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/RandomSignedStateGenerator.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/RandomSignedStateGenerator.java @@ -69,6 +69,7 @@ public class RandomSignedStateGenerator { private Hash stateHash = null; private Integer roundsNonAncient = null; private Hash epoch = null; + private ConsensusSnapshot consensusSnapshot; /** * Create a new signed state generator with a random seed. @@ -162,6 +163,20 @@ public SignedState build() { softwareVersionInstance = softwareVersion; } + final ConsensusSnapshot consensusSnapshotInstance; + if (consensusSnapshot == null) { + consensusSnapshotInstance = new ConsensusSnapshot( + roundInstance, + Stream.generate(() -> randomHash(random)).limit(10).toList(), + IntStream.range(0, roundsNonAncientInstance) + .mapToObj(i -> new MinimumJudgeInfo(roundInstance - i, 0L)) + .toList(), + roundInstance, + consensusTimestampInstance); + } else { + consensusSnapshotInstance = consensusSnapshot; + } + final PlatformState platformState = stateInstance.getPlatformState(); platformState.setRound(roundInstance); @@ -169,14 +184,7 @@ public SignedState build() { platformState.setConsensusTimestamp(consensusTimestampInstance); platformState.setCreationSoftwareVersion(softwareVersionInstance); platformState.setRoundsNonAncient(roundsNonAncientInstance); - platformState.setSnapshot(new ConsensusSnapshot( - roundInstance, - Stream.generate(() -> randomHash(random)).limit(10).toList(), - IntStream.range(0, roundsNonAncientInstance) - .mapToObj(i -> new MinimumJudgeInfo(roundInstance - i, 0L)) - .toList(), - roundInstance, - consensusTimestampInstance)); + platformState.setSnapshot(consensusSnapshotInstance); final SignedState signedState = new SignedState( new TestConfigBuilder() @@ -392,4 +400,10 @@ public RandomSignedStateGenerator setEpoch(Hash epoch) { this.epoch = epoch; return this; } + + @NonNull + public RandomSignedStateGenerator setConsensusSnapshot(@NonNull final ConsensusSnapshot consensusSnapshot) { + this.consensusSnapshot = consensusSnapshot; + return this; + } } diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/system/events/BirthRoundMigrationShimTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/system/events/BirthRoundMigrationShimTests.java new file mode 100644 index 000000000000..bd7124c0431b --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/system/events/BirthRoundMigrationShimTests.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.swirlds.platform.system.events; + +import static com.swirlds.common.test.fixtures.RandomUtils.getRandomPrintSeed; +import static com.swirlds.common.test.fixtures.RandomUtils.randomHash; +import static com.swirlds.common.test.fixtures.RandomUtils.randomInstant; +import static com.swirlds.platform.consensus.ConsensusConstants.ROUND_FIRST; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +import com.swirlds.common.context.PlatformContext; +import com.swirlds.common.crypto.Hash; +import com.swirlds.common.platform.NodeId; +import com.swirlds.common.test.fixtures.platform.TestPlatformContextBuilder; +import com.swirlds.platform.event.GossipEvent; +import com.swirlds.platform.system.BasicSoftwareVersion; +import com.swirlds.platform.system.SoftwareVersion; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.List; +import java.util.Random; +import org.junit.jupiter.api.Test; + +class BirthRoundMigrationShimTests { + + @NonNull + private GossipEvent buildEvent( + @NonNull final Random random, + @NonNull final PlatformContext platformContext, + @NonNull final SoftwareVersion softwareVersion, + final long generation, + final long birthRound) { + + final GossipEvent event = new GossipEvent( + new BaseEventHashedData( + softwareVersion, + new NodeId(random.nextLong(1, 10)), + new EventDescriptor( + randomHash(random), + new NodeId(random.nextInt(1, 10)), + generation - 1 /* chose parent generation to yield desired self generation */, + random.nextLong(birthRound - 2, birthRound + 1)) /* realistic range */, + List.of() /* don't bother with other parents, unimportant for this test */, + birthRound, + randomInstant(random), + null), + new BaseEventUnhashedData()); + + platformContext.getCryptography().digestSync(event.getHashedData()); + + return event; + } + + @Test + void ancientEventsTest() { + final Random random = getRandomPrintSeed(); + + final PlatformContext platformContext = + TestPlatformContextBuilder.create().build(); + + final BasicSoftwareVersion firstVersionInBirthRoundMode = new BasicSoftwareVersion(random.nextInt(10, 100)); + final long lastRoundBeforeBirthRoundMode = random.nextLong(100, 1_000); + final long lowestJudgeGenerationBeforeBirthRoundMode = random.nextLong(100, 1_000); + + final BirthRoundMigrationShim shim = new BirthRoundMigrationShim( + platformContext, + firstVersionInBirthRoundMode, + lastRoundBeforeBirthRoundMode, + lowestJudgeGenerationBeforeBirthRoundMode); + + // Any event with a software version less than firstVersionInBirthRoundMode and a generation less than + // lowestJudgeGenerationBeforeBirthRoundMode should have its birth round set to ROUND_FIRST. + + for (int i = 0; i < 100; i++) { + final long birthRound = random.nextLong(100, 1000); + final GossipEvent event = buildEvent( + random, + platformContext, + new BasicSoftwareVersion( + firstVersionInBirthRoundMode.getSoftwareVersion() - random.nextInt(1, 100)), + lowestJudgeGenerationBeforeBirthRoundMode - random.nextInt(1, 100), + birthRound); + + assertEquals(birthRound, event.getHashedData().getBirthRound()); + final Hash originalHash = event.getHashedData().getHash(); + + assertSame(event, shim.migrateEvent(event)); + assertEquals(ROUND_FIRST, event.getHashedData().getBirthRound()); + + // The hash of the event should not have changed + event.getHashedData().invalidateHash(); + platformContext.getCryptography().digestSync(event.getHashedData()); + assertEquals(originalHash, event.getHashedData().getHash()); + } + } + + @Test + void barelyNonAncientEventsTest() { + final Random random = getRandomPrintSeed(); + + final PlatformContext platformContext = + TestPlatformContextBuilder.create().build(); + + final BasicSoftwareVersion firstVersionInBirthRoundMode = new BasicSoftwareVersion(random.nextInt(10, 100)); + final long lastRoundBeforeBirthRoundMode = random.nextLong(100, 1_000); + final long lowestJudgeGenerationBeforeBirthRoundMode = random.nextLong(100, 1_000); + + final BirthRoundMigrationShim shim = new BirthRoundMigrationShim( + platformContext, + firstVersionInBirthRoundMode, + lastRoundBeforeBirthRoundMode, + lowestJudgeGenerationBeforeBirthRoundMode); + + // Any event with a software version less than firstVersionInBirthRoundMode and a generation greater than + // or equal to lowestJudgeGenerationBeforeBirthRoundMode should have its birth round set to + // lastRoundBeforeBirthRoundMode. + + for (int i = 0; i < 100; i++) { + final long birthRound = random.nextLong(100, 1000); + final GossipEvent event = buildEvent( + random, + platformContext, + new BasicSoftwareVersion( + firstVersionInBirthRoundMode.getSoftwareVersion() - random.nextInt(1, 100)), + lowestJudgeGenerationBeforeBirthRoundMode + random.nextInt(0, 10), + birthRound); + + assertEquals(birthRound, event.getHashedData().getBirthRound()); + final Hash originalHash = event.getHashedData().getHash(); + + assertSame(event, shim.migrateEvent(event)); + assertEquals(lastRoundBeforeBirthRoundMode, event.getHashedData().getBirthRound()); + + // The hash of the event should not have changed + event.getHashedData().invalidateHash(); + platformContext.getCryptography().digestSync(event.getHashedData()); + assertEquals(originalHash, event.getHashedData().getHash()); + } + } + + @Test + void unmodifiedEventsTest() { + final Random random = getRandomPrintSeed(); + + final PlatformContext platformContext = + TestPlatformContextBuilder.create().build(); + + final BasicSoftwareVersion firstVersionInBirthRoundMode = new BasicSoftwareVersion(random.nextInt(10, 100)); + final long lastRoundBeforeBirthRoundMode = random.nextLong(100, 1_000); + final long lowestJudgeGenerationBeforeBirthRoundMode = random.nextLong(100, 1_000); + + final BirthRoundMigrationShim shim = new BirthRoundMigrationShim( + platformContext, + firstVersionInBirthRoundMode, + lastRoundBeforeBirthRoundMode, + lowestJudgeGenerationBeforeBirthRoundMode); + + // Any event with a software greater than or equal to firstVersionInBirthRoundMode should not have its birth + // round modified. + + for (int i = 0; i < 100; i++) { + final long birthRound = random.nextLong(100, 1000); + final GossipEvent event = buildEvent( + random, + platformContext, + new BasicSoftwareVersion(firstVersionInBirthRoundMode.getSoftwareVersion() + random.nextInt(0, 10)), + lowestJudgeGenerationBeforeBirthRoundMode - random.nextInt(-100, 100), + birthRound); + + assertEquals(birthRound, event.getHashedData().getBirthRound()); + final Hash originalHash = event.getHashedData().getHash(); + + assertSame(event, shim.migrateEvent(event)); + assertEquals(birthRound, event.getHashedData().getBirthRound()); + + // The hash of the event should not have changed + event.getHashedData().invalidateHash(); + platformContext.getCryptography().digestSync(event.getHashedData()); + assertEquals(originalHash, event.getHashedData().getHash()); + } + } +} diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/wiring/PlatformWiringTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/wiring/PlatformWiringTests.java index 3ca100c6dbff..fce529e48776 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/wiring/PlatformWiringTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/wiring/PlatformWiringTests.java @@ -19,7 +19,6 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.mockito.Mockito.mock; -import com.swirlds.base.test.fixtures.time.FakeTime; import com.swirlds.common.context.PlatformContext; import com.swirlds.common.stream.EventStreamManager; import com.swirlds.common.test.fixtures.platform.TestPlatformContextBuilder; @@ -45,6 +44,7 @@ import com.swirlds.platform.state.iss.IssHandler; import com.swirlds.platform.state.signed.SignedStateFileManager; import com.swirlds.platform.state.signed.StateSignatureCollector; +import com.swirlds.platform.system.events.BirthRoundMigrationShim; import com.swirlds.platform.util.HashLogger; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -59,7 +59,7 @@ void testBindings() { final PlatformContext platformContext = TestPlatformContextBuilder.create().build(); - final PlatformWiring wiring = new PlatformWiring(platformContext, new FakeTime()); + final PlatformWiring wiring = new PlatformWiring(platformContext); wiring.bind( mock(EventHasher.class), @@ -85,6 +85,7 @@ void testBindings() { mock(IssDetector.class), mock(IssHandler.class), mock(HashLogger.class), + mock(BirthRoundMigrationShim.class), mock(LatestCompleteStateNotifier.class)); assertFalse(wiring.getModel().checkForUnboundInputWires()); diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/GossipEventTest.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/GossipEventTest.java index eb9d7a505a92..f8b4ea2861c1 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/GossipEventTest.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/GossipEventTest.java @@ -16,14 +16,11 @@ package com.swirlds.platform.test.event; -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.swirlds.common.constructable.ConstructableRegistry; import com.swirlds.common.constructable.ConstructableRegistryException; -import com.swirlds.common.io.streams.SerializableDataInputStream; -import com.swirlds.common.io.streams.SerializableDataOutputStream; import com.swirlds.common.test.fixtures.io.SerializationUtils; import com.swirlds.config.extensions.test.fixtures.TestConfigBuilder; import com.swirlds.platform.event.GossipEvent; @@ -31,9 +28,6 @@ import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.test.fixtures.event.TestingEventBuilder; import com.swirlds.platform.test.utils.EqualsVerifier; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import org.junit.jupiter.api.AfterAll; @@ -63,28 +57,6 @@ void serializeDeserialize() throws IOException, ConstructableRegistryException { assertEquals(gossipEvent, copy, "deserialized version should be the same"); } - @Test - @DisplayName("Deserialize prior version of event") - void deserializePriorVersion() throws IOException, ConstructableRegistryException { - ConstructableRegistry.getInstance().registerConstructables("com.swirlds"); - final File file = new File("src/test/resources/eventFiles/eventSerializationV45/sampleGossipEvent.evts"); - final SerializableDataInputStream in = new SerializableDataInputStream(new FileInputStream(file)); - final GossipEvent gossipEvent = in.readSerializable(false, GossipEvent::new); - assertEquals(3, gossipEvent.getHashedData().getVersion()); - assertEquals(1, gossipEvent.getUnhashedData().getVersion()); - final GossipEvent copy = SerializationUtils.serializeDeserialize(gossipEvent); - assertEquals(gossipEvent, copy, "deserialized version should be the same"); - assertEquals( - gossipEvent.getHashedData().getVersion(), copy.getHashedData().getVersion()); - - final byte[] original = new FileInputStream(file).readAllBytes(); - final ByteArrayOutputStream outBytes = new ByteArrayOutputStream(); - final SerializableDataOutputStream out = new SerializableDataOutputStream(outBytes); - out.writeSerializable(gossipEvent, false); - final byte[] serialized = outBytes.toByteArray(); - assertArrayEquals(original, serialized, "serialized bytes should be the same"); - } - @Test void validateEqualsHashCode() { assertTrue(EqualsVerifier.verify( diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/resources/eventFiles/eventSerializationV45/sampleGossipEvent.evts b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/resources/eventFiles/eventSerializationV45/sampleGossipEvent.evts deleted file mode 100644 index 530f5f39aea5246eda4bfb1ce3bc37b0e5a7d277..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 947 zcmZQzU|?ckU|=qD~V$dg|t=WzAjM32Sm?wFyOEp}L__e$Q| zrr8^EUFOf2x;%1+XKm%XIy6-`g2|Mm1wiqky#M}2lu?RvU|z9_ionDQ=jS% z%;uVQA|2^X|uju0Yla6hB_i)}RY53mwTOt2g%{k_< z-#XFNeHB9Y6B6ED`o^Yv>}7;)@`;%uMauG59aAdhm3ML9n`o~1kA-=E+{4=ouebH_ zFZjQ>dUKe`#qyWGi?{F7c% zwI7~R_xN)xNk6_`|GTMr*SqlRq9L=_AMsT9`%wRR<(+~XN$GypYaRGjzZOybx=t+A z@N>0Y{~p;!wVmN}UR&0!RpR|Vxv}x%-TG%LFLv*($v<`9$?2+RiLcrXQ;n7qt~p;s n<(1~fly3?QT)6whhk1)nuJkSo`RTp5a2hZIP1dp3-+BxHQFqVh From b7f0f0ffa0f6f7a72c5fb00280cb669e581efbbc Mon Sep 17 00:00:00 2001 From: Cody Littley <56973212+cody-littley@users.noreply.github.com> Date: Thu, 7 Mar 2024 07:36:38 -0600 Subject: [PATCH 032/115] feat: faster wiring backpressure (#11690) Signed-off-by: Cody Littley --- .../wiring/counters/BackpressureBlocker.java | 26 ++++-------- .../counters/BackpressureObjectCounter.java | 42 +++++++++++-------- .../wiring/counters/NoCapacityException.java | 22 ---------- 3 files changed, 33 insertions(+), 57 deletions(-) delete mode 100644 platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/NoCapacityException.java diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/BackpressureBlocker.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/BackpressureBlocker.java index 10a671ed5c79..37f00849b3f1 100644 --- a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/BackpressureBlocker.java +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/BackpressureBlocker.java @@ -90,23 +90,15 @@ public boolean block() throws InterruptedException { */ @Override public boolean isReleasable() { - while (true) { - final long currentCount = count.get(); - - if (currentCount >= capacity) { - // We've reached capacity, so we need to block. - return false; - } - - final boolean success = count.compareAndSet(currentCount, currentCount + 1); - if (success) { - // We've successfully incremented the count, so we're done. - return true; - } - - // We were unable to increment the count because another thread concurrently modified it. - // Try again. We will keep trying until we are either successful or we observe there is - // insufficient capacity. + final long resultingCount = count.incrementAndGet(); + if (resultingCount <= capacity) { + // We didn't violate capacity by incrementing the count, so we're done. + return true; + } else { + // We may have violated capacity restrictions by incrementing the count. + // Decrement count and take the slow pathway. + count.decrementAndGet(); + return false; } } } diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/BackpressureObjectCounter.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/BackpressureObjectCounter.java index cea374c1c2ce..d88dd57ef901 100644 --- a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/BackpressureObjectCounter.java +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/BackpressureObjectCounter.java @@ -27,6 +27,10 @@ /** * A utility for counting the number of objects in various parts of the pipeline. Will apply backpressure if the number * of objects exceeds a specified capacity. + *

    + * In order to achieve higher performance in high contention environments, this class allows the count returned by + * {@link #getCount()} to temporarily exceed the capacity even if {@link #forceOnRamp()} is not used. This doesn't allow + * objects to be on-ramped in excess of the capacity, but it may add some slight fuzziness to the count. */ public class BackpressureObjectCounter extends ObjectCounter { @@ -73,17 +77,19 @@ public BackpressureObjectCounter( */ @Override public void onRamp() { - while (true) { - final long currentCount = count.get(); - if (currentCount < capacity) { - final boolean success = count.compareAndSet(currentCount, currentCount + 1); - if (success) { - return; - } - } + final long resultingCount = count.incrementAndGet(); + if (resultingCount <= capacity) { + // We didn't violate capacity by incrementing the count, so we're done. + return; + } else { + // We may have violated capacity restrictions by incrementing the count. + // Decrement count and take the slow pathway. + count.decrementAndGet(); + } - // Slow case. Capacity wasn't reserved, so we may need to block. + // Slow case. Capacity wasn't reserved, so we need to block. + while (true) { try { // This will block until capacity is available and the count has been incremented. // @@ -119,15 +125,15 @@ public void onRamp() { */ @Override public boolean attemptOnRamp() { - while (true) { - final long currentCount = count.get(); - if (currentCount >= capacity) { - return false; - } - - if (count.compareAndSet(currentCount, currentCount + 1)) { - return true; - } + final long resultingCount = count.incrementAndGet(); + if (resultingCount <= capacity) { + // We didn't violate capacity by incrementing the count, so we're done. + return true; + } else { + // We may have violated capacity restrictions by incrementing the count. + // Decrement count and return failure. + count.decrementAndGet(); + return false; } } diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/NoCapacityException.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/NoCapacityException.java deleted file mode 100644 index 5c0aff957952..000000000000 --- a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/counters/NoCapacityException.java +++ /dev/null @@ -1,22 +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.swirlds.common.wiring.counters; - -/** - * An exception thrown when an attempt is made to increment a counter that is already at capacity. - */ -class NoCapacityException extends RuntimeException {} From 27cdfb7781ca348365eca02063f41624f725cccb Mon Sep 17 00:00:00 2001 From: Michael Heinrichs Date: Thu, 7 Mar 2024 17:01:26 +0100 Subject: [PATCH 033/115] chore: Remove explicit fuzzy matching (cherry-pick) (#11943) --- .../precompile/LazyCreateThroughPrecompileSuite.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/LazyCreateThroughPrecompileSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/LazyCreateThroughPrecompileSuite.java index 6d7f15f6077c..c5ce7d0a2647 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/LazyCreateThroughPrecompileSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/LazyCreateThroughPrecompileSuite.java @@ -49,7 +49,6 @@ import static com.hedera.services.bdd.spec.utilops.UtilVerbs.ifNotHapiTest; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.inParallel; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.snapshotMode; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sourcing; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; import static com.hedera.services.bdd.spec.utilops.records.SnapshotMatchMode.ACCEPTED_MONO_GAS_CALCULATION_DIFFERENCE; @@ -59,7 +58,6 @@ 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.spec.utilops.records.SnapshotMatchMode.NONDETERMINISTIC_TRANSACTION_FEES; -import static com.hedera.services.bdd.spec.utilops.records.SnapshotMode.FUZZY_MATCH_AGAINST_HAPI_TEST_STREAMS; import static com.hedera.services.bdd.suites.contract.Utils.asAddress; import static com.hedera.services.bdd.suites.contract.Utils.headlongFromHexed; import static com.hedera.services.bdd.suites.contract.Utils.mirrorAddrWith; @@ -698,12 +696,11 @@ final HapiSpec htsTransferFromForNFTLazyCreate() { @HapiTest final HapiSpec revertedAutoCreationRollsBackEvenIfTopLevelSucceeds() { - return defaultHapiSpec("revertedAutoCreationRollsBackEvenIfTopLevelSucceeds") + return defaultHapiSpec( + "revertedAutoCreationRollsBackEvenIfTopLevelSucceeds", + NONDETERMINISTIC_TRANSACTION_FEES, + ACCEPTED_MONO_GAS_CALCULATION_DIFFERENCE) .given( - snapshotMode( - FUZZY_MATCH_AGAINST_HAPI_TEST_STREAMS, - NONDETERMINISTIC_FUNCTION_PARAMETERS, - ACCEPTED_MONO_GAS_CALCULATION_DIFFERENCE), newKeyNamed(ECDSA_KEY).shape(SECP_256K1_SHAPE), newKeyNamed(MULTI_KEY), cryptoCreate(OWNER).balance(100 * ONE_HUNDRED_HBARS).maxAutomaticTokenAssociations(5), From b5a6b840c0936b895dedf616d7fecc075eb71859 Mon Sep 17 00:00:00 2001 From: Cody Littley <56973212+cody-littley@users.noreply.github.com> Date: Thu, 7 Mar 2024 12:39:14 -0600 Subject: [PATCH 034/115] feat: use automatic wiring for event deduplicator (#11936) Signed-off-by: Cody Littley --- platform-sdk/docs/core/wiringDiagramLink.md | 6 + .../src/main/java/module-info.java | 1 + .../com/swirlds/platform/SwirldsPlatform.java | 3 +- .../deduplication/EventDeduplicator.java | 135 +------------- .../StandardEventDeduplicator.java | 172 ++++++++++++++++++ .../wiring/EventDeduplicatorWiring.java | 70 ------- .../platform/wiring/PlatformCoordinator.java | 11 +- .../platform/wiring/PlatformWiring.java | 15 +- .../event/EventDeduplicatorTests.java | 3 +- 9 files changed, 207 insertions(+), 209 deletions(-) create mode 100644 platform-sdk/docs/core/wiringDiagramLink.md create mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/deduplication/StandardEventDeduplicator.java delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/EventDeduplicatorWiring.java diff --git a/platform-sdk/docs/core/wiringDiagramLink.md b/platform-sdk/docs/core/wiringDiagramLink.md new file mode 100644 index 000000000000..a6567fc1e117 --- /dev/null +++ b/platform-sdk/docs/core/wiringDiagramLink.md @@ -0,0 +1,6 @@ +# Pre-Generated Wiring Diagram + +[Click here for the wiring diagram](https://mermaid.ink/svg/JSV7aW5pdDogeydmbG93Y2hhcnQnOiB7J2RlZmF1bHRSZW5kZXJlcic6ICdlbGsnfX19JSUKZmxvd2NoYXJ0IFRECnYwWy8iQ29uc2Vuc3VzIEV2ZW50IFN0cmVhbSIvXQpzdWJncmFwaCB2MVsiQ29uc2Vuc3VzIFBpcGVsaW5lIl0KdjJbIkNvbnNlbnN1cyBFbmdpbmUiXQp2M1siaW5PcmRlckxpbmtlcjxiciAvPvCfjIAiXQp2NCgoIvCfjIAiKSkKdjUoKCLwn5OsIikpCnY2KCgi8J+avSIpKQplbmQKc3ViZ3JhcGggdjdbIkV2ZW50IENyZWF0aW9uIl0KdjhbImV2ZW50Q3JlYXRpb25NYW5hZ2VyPGJyIC8+4p2k77iP8J+MgCJdCnY5WyJmdXR1cmVFdmVudEJ1ZmZlcjxiciAvPvCfjIAiXQp2MTB7eyJmdXR1cmVFdmVudEJ1ZmZlclNwbGl0dGVyIn19CnYxMVsvInRyYW5zYWN0aW9uUG9vbCIvXQp2MTIoKCLwn42OIikpCmVuZApzdWJncmFwaCB2MTNbIkV2ZW50IEhhc2hpbmciXQp2MTRbWyJldmVudEhhc2hlciJdXQp2MTVbInBvc3RIYXNoQ29sbGVjdG9yIl0KZW5kCnN1YmdyYXBoIHYxNlsiRXZlbnQgVmFsaWRhdGlvbiJdCnYxN1siZXZlbnREZWR1cGxpY2F0b3I8YnIgLz7wn4yAIl0KdjE4WyJldmVudFNpZ25hdHVyZVZhbGlkYXRvcjxiciAvPvCfjIAiXQp2MTlbImludGVybmFsRXZlbnRWYWxpZGF0b3I8YnIgLz7wn42OIl0KZW5kCnN1YmdyYXBoIHYyMFsiR29zc2lwIl0KdjIxe3siZ29zc2lwIn19CnYyMlsic2hhZG93Z3JhcGg8YnIgLz7wn4yA8J+TrCJdCmVuZApzdWJncmFwaCB2MjNbIkhlYXJ0YmVhdCJdCnYyNFsiaGVhcnRiZWF0Il0KdjI1KCgi4p2k77iPIikpCmVuZAp2MjZbIk9ycGhhbiBCdWZmZXI8YnIgLz7wn4yAIl0Kc3ViZ3JhcGggdjI3WyJQQ0VTIFJlcGxheSJdCnYyOFsvInBjZXNSZXBsYXllciIvXQp2MjkoKCLinIUiKSkKZW5kCnN1YmdyYXBoIHYzMFsiUHJlY29uc2Vuc3VzIEV2ZW50IFN0cmVhbSJdCnYzMVsvImV2ZW50RHVyYWJpbGl0eU5leHVzIi9dCnYzMlsvInBjZXNTZXF1ZW5jZXIiL10KdjMzWyJwY2VzV3JpdGVyPGJyIC8+4pyF8J+MgPCfk4Dwn5q9Il0KdjM0KCgi8J+VkSIpKQplbmQKc3ViZ3JhcGggdjM1WyJTaWduYXR1cmUgTWFuYWdlbWVudCJdCnN1YmdyYXBoIHYzNlsiSXNzIERldGVjdG9yIl0KdjM3e3siZXh0cmFjdFNpZ25hdHVyZXNGb3JJc3NEZXRlY3RvciJ9fQp2MzhbImlzc0RldGVjdG9yIl0KdjM5Wy8iaXNzSGFuZGxlciIvXQp2NDBbLyJpc3NOb3RpZmljYXRpb25FbmdpbmUiL10KdjQxe3siaXNzTm90aWZpY2F0aW9uU3BsaXR0ZXIifX0KdjQyWy8ic3RhdHVzTWFuYWdlcl9zdWJtaXRDYXRhc3Ryb3BoaWNGYWlsdXJlIi9dCmVuZAp2NDNbIlN0YXRlIFNpZ25hdHVyZSBDb2xsZWN0aW9uIl0KdjQ0Wy8ibGF0ZXN0Q29tcGxldGVTdGF0ZU5leHVzIi9dCnY0NVsibGF0ZXN0Q29tcGxldGVTdGF0ZU5vdGlmaWNhdGlvbiJdCnY0Nlsic3RhdGVTaWduZXIiXQplbmQKdjQ3WyJTdGF0ZSBGaWxlIE1hbmFnZW1lbnQiXQpzdWJncmFwaCB2NDhbIlN0YXRlIE1vZGlmaWNhdGlvbiJdCnY0OVsiY29uc2Vuc3VzUm91bmRIYW5kbGVyPGJyIC8+8J+UrvCflZEiXQp2NTB7eyJydW5uaW5nSGFzaFVwZGF0ZSJ9fQplbmQKc3ViZ3JhcGggdjUxWyJUcmFuc2FjdGlvbiBQcmVoYW5kbGluZyJdCnY1MltbImFwcGxpY2F0aW9uVHJhbnNhY3Rpb25QcmVoYW5kbGVyIl1dCnY1MygoIvCflK4iKSkKZW5kCnY1NFsiaGFzaExvZ2dlciJdCnY1NSgoIvCfk4AiKSkKdjIgLS0gInJvdW5kcyIgLS0+IHYwCnYyIC0tICJyb3VuZHMiIC0tPiB2NDkKdjIgLS4gIm5vbi1hbmNpZW50IGV2ZW50IHdpbmRvdyIgLi0+IHY0CnYyIC0tICJmbHVzaCByZXF1ZXN0IiAtLT4gdjYKdjI2IC0tICJwcmVjb25zZW5zdXMgZXZlbnRzIiAtLT4gdjQzCnYyNiAtLSAicHJlY29uc2Vuc3VzIGV2ZW50cyIgLS0+IHY1Mgp2MjYgLS0gInByZWNvbnNlbnN1cyBldmVudHMiIC0tPiB2OQp2MjYgLS0gImV2ZW50cyB0byBzZXF1ZW5jZSIgLS0+IHYzMgp2NDcgLS0gIm1pbmltdW0gaWRlbnRpZmllciB0byBzdG9yZSIgLS0+IHY1NQp2NDMgLS0gInN0YXRlcyIgLS0+IHY0Nwp2NDMgLS0gImNvbXBsZXRlIHN0YXRlcyIgLS0+IHY0NAp2NDMgLS0gImNvbXBsZXRlIHN0YXRlcyIgLS0+IHY0NQp2NTIgLS4gImZ1dHVyZXMiIC4tbyB2NTMKdjggLS4gImdldCB0cmFuc2FjdGlvbnMiIC4tbyB2MTEKdjggLS4gIm5vbi12YWxpZGF0ZWQgZXZlbnRzIiAuLT4gdjEyCnYxNyAtLSAiZXZlbnRzIHdpdGggdW52YWxpZGF0ZWQgc2lnbmF0dXJlcyIgLS0+IHYxOAp2MzEgLS4gIndhaXQgZm9yIGR1cmFiaWxpdHkiIC4tbyB2MzQKdjE0IC0tICJoYXNoZWQgZXZlbnRzIiAtLT4gdjE1CnYxOCAtLSAidW5vcmRlcmVkIGV2ZW50cyIgLS0+IHYyNgp2MzcgLS0gInBvc3QgY29uc2Vuc3VzIHNpZ25hdHVyZXMiIC0tPiB2MzgKdjkgLS0gInBvc3NpYmxlIHBhcmVudCBsaXN0cyIgLS0+IHYxMAp2MTAgLS0gInBvc3NpYmxlIHBhcmVudHMiIC0tPiB2OAp2MjEgLS0gImV2ZW50cyB0byBoYXNoIiAtLT4gdjE0CnYyNCAtLSAiaGVhcnRiZWF0IiAtLT4gdjI1CnYzIC0tICJsaW5rZWQgZXZlbnRzIiAtLT4gdjIKdjMgLS0gImV2ZW50cyB0byBnb3NzaXAiIC0tPiB2NQp2MTkgLS0gIm5vbi1kZWR1cGxpY2F0ZWQgZXZlbnRzIiAtLT4gdjE3CnYzOCAtLSAiaXNzIG5vdGlmaWNhdGlvbnMiIC0tPiB2NDEKdjQxIC0tICJpc3Mgbm90aWZpY2F0aW9uIiAtLT4gdjM5CnY0MSAtLSAiSVNTIG5vdGlmaWNhdGlvbiIgLS0+IHY0MAp2NDEgLS0gIklTUyBub3RpZmljYXRpb24iIC0tPiB2NDIKdjI4IC0tICJldmVudHMgdG8gaGFzaCIgLS0+IHYxNAp2MjggLS0gImRvbmUgc3RyZWFtaW5nIHBjZXMiIC0tPiB2MjkKdjMyIC0tICJ1bmxpbmtlZCBldmVudHMiIC0tPiB2Mwp2MzIgLS0gImV2ZW50cyB0byB3cml0ZSIgLS0+IHYzMwp2MzMgLS0gImxhdGVzdCBkdXJhYmxlIHNlcXVlbmNlIG51bWJlciIgLS0+IHYzMQp2MTUgLS0gIm5vbi12YWxpZGF0ZWQgZXZlbnRzIiAtLT4gdjE5CnY1MCAtLSAicnVubmluZyBoYXNoIHVwZGF0ZSIgLS0+IHYwCnY1MCAtLSAicnVubmluZyBoYXNoIHVwZGF0ZSIgLS0+IHY0OQp2NDYgLS0gInN0YXRlIHNpZ25hdHVyZSB0cmFuc2FjdGlvbnMiIC0tPiB2MTEKY2xhc3NEZWYgczAgZmlsbDojY2NjLHN0cm9rZTojMDAwLHN0cm9rZS13aWR0aDoycHgKY2xhc3MgdjAsdjEwLHYxMSx2MjEsdjI4LHYzMSx2MzIsdjM3LHYzOSx2NDAsdjQxLHY0Mix2NDQsdjUwIHMwCmNsYXNzRGVmIHMxIGZpbGw6IzlDRixzdHJva2U6IzAwMCxzdHJva2Utd2lkdGg6MnB4CmNsYXNzIHYxLHYxMyx2MTYsdjIwLHYyMyx2MjcsdjMwLHYzNSx2NDgsdjUxLHY3IHMxCmNsYXNzRGVmIHMyIGZpbGw6I2ZmOSxzdHJva2U6IzAwMCxzdHJva2Utd2lkdGg6MnB4CmNsYXNzIHYxNCx2MTUsdjE3LHYxOCx2MTksdjIsdjIyLHYyNCx2MjYsdjMsdjMzLHYzOCx2NDMsdjQ1LHY0Nix2NDcsdjQ5LHY1Mix2NTQsdjgsdjkgczIKY2xhc3NEZWYgczMgZmlsbDojZjg4LHN0cm9rZTojMDAwLHN0cm9rZS13aWR0aDoycHgKY2xhc3MgdjEyLHYyNSx2MjksdjM0LHY0LHY1LHY1Myx2NTUsdjYgczMKY2xhc3NEZWYgczQgZmlsbDojQkVGLHN0cm9rZTojMDAwLHN0cm9rZS13aWR0aDoycHgKY2xhc3MgdjM2IHM0Cg==?bgColor=e8e8e8) + +When making any change that modifies the wiring diagram, +please regenerate the diagram and update this page with the new diagram. \ No newline at end of file diff --git a/platform-sdk/swirlds-common/src/main/java/module-info.java b/platform-sdk/swirlds-common/src/main/java/module-info.java index 232de92be40d..c1f95a986910 100644 --- a/platform-sdk/swirlds-common/src/main/java/module-info.java +++ b/platform-sdk/swirlds-common/src/main/java/module-info.java @@ -66,6 +66,7 @@ exports com.swirlds.common.utility.throttle; exports com.swirlds.common.jackson; exports com.swirlds.common.units; + exports com.swirlds.common.wiring.component; exports com.swirlds.common.wiring.counters; exports com.swirlds.common.wiring.model; exports com.swirlds.common.wiring.schedulers; 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 08bd6c19c342..c2fbad13d5ef 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 @@ -83,6 +83,7 @@ import com.swirlds.platform.event.GossipEvent; import com.swirlds.platform.event.creation.EventCreationManager; import com.swirlds.platform.event.deduplication.EventDeduplicator; +import com.swirlds.platform.event.deduplication.StandardEventDeduplicator; import com.swirlds.platform.event.hashing.EventHasher; import com.swirlds.platform.event.linking.InOrderLinker; import com.swirlds.platform.event.orphan.OrphanBuffer; @@ -619,7 +620,7 @@ public class SwirldsPlatform implements Platform { final InternalEventValidator internalEventValidator = new InternalEventValidator( platformContext, time, currentAddressBook.getSize() == 1, intakeEventCounter); - final EventDeduplicator eventDeduplicator = new EventDeduplicator(platformContext, intakeEventCounter); + final EventDeduplicator eventDeduplicator = new StandardEventDeduplicator(platformContext, intakeEventCounter); final EventSignatureValidator eventSignatureValidator = new EventSignatureValidator( platformContext, time, diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/deduplication/EventDeduplicator.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/deduplication/EventDeduplicator.java index ccac5b4fafde..e15917976c50 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/deduplication/EventDeduplicator.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/deduplication/EventDeduplicator.java @@ -16,30 +16,12 @@ package com.swirlds.platform.event.deduplication; -import static com.swirlds.metrics.api.FloatFormats.FORMAT_10_2; -import static com.swirlds.metrics.api.Metrics.PLATFORM_CATEGORY; - -import com.swirlds.common.context.PlatformContext; -import com.swirlds.common.metrics.RunningAverageMetric; -import com.swirlds.common.metrics.extensions.CountPerSecond; -import com.swirlds.common.sequence.map.SequenceMap; -import com.swirlds.common.sequence.map.StandardSequenceMap; -import com.swirlds.metrics.api.LongAccumulator; -import com.swirlds.metrics.api.Metrics; +import com.swirlds.common.wiring.component.InputWireLabel; import com.swirlds.platform.consensus.NonAncientEventWindow; -import com.swirlds.platform.event.AncientMode; import com.swirlds.platform.event.GossipEvent; -import com.swirlds.platform.eventhandling.EventConfig; -import com.swirlds.platform.gossip.IntakeEventCounter; -import com.swirlds.platform.system.events.EventDescriptor; import com.swirlds.platform.wiring.ClearTrigger; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import java.nio.ByteBuffer; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; -import java.util.function.Function; /** * Deduplicates events. @@ -54,79 +36,7 @@ * deduplicator lets all versions of the event through that have a unique descriptor/signature pair, and the signature * validator further along the pipeline will handle discarding bad versions. */ -public class EventDeduplicator { - /** - * Avoid the creation of lambdas for Map.computeIfAbsent() by reusing this lambda. - */ - private static final Function> NEW_HASH_SET = ignored -> new HashSet<>(); - - /** - * Initial capacity of {@link #observedEvents}. - */ - private static final int INITIAL_CAPACITY = 1024; - - /** - * The current non-ancient event window. - */ - private NonAncientEventWindow nonAncientEventWindow; - - /** - * Keeps track of the number of events in the intake pipeline from each peer - */ - private final IntakeEventCounter intakeEventCounter; - - /** - * A map from event descriptor to a set of signatures that have been received for that event. - */ - private final SequenceMap> observedEvents; - - private static final LongAccumulator.Config DISPARATE_SIGNATURE_CONFIG = new LongAccumulator.Config( - PLATFORM_CATEGORY, "eventsWithDisparateSignature") - .withDescription( - "Events received that match a descriptor of a previous event, but with a different signature") - .withUnit("events"); - private final LongAccumulator disparateSignatureAccumulator; - - private final CountPerSecond duplicateEventsPerSecond; - - private static final RunningAverageMetric.Config AVG_DUPLICATE_PERCENT_CONFIG = new RunningAverageMetric.Config( - PLATFORM_CATEGORY, "dupEvPercent") - .withDescription("percentage of events received that are already known") - .withFormat(FORMAT_10_2); - private final RunningAverageMetric avgDuplicatePercent; - - /** - * Constructor - * - * @param platformContext the platform context - * @param intakeEventCounter keeps track of the number of events in the intake pipeline from each peer - */ - public EventDeduplicator( - @NonNull final PlatformContext platformContext, @NonNull final IntakeEventCounter intakeEventCounter) { - - this.intakeEventCounter = Objects.requireNonNull(intakeEventCounter); - - final Metrics metrics = platformContext.getMetrics(); - - this.disparateSignatureAccumulator = metrics.getOrCreate(DISPARATE_SIGNATURE_CONFIG); - this.duplicateEventsPerSecond = new CountPerSecond( - metrics, - new CountPerSecond.Config(PLATFORM_CATEGORY, "dupEv_per_sec") - .withDescription("number of events received per second that are already known") - .withUnit("hz")); - this.avgDuplicatePercent = metrics.getOrCreate(AVG_DUPLICATE_PERCENT_CONFIG); - - final AncientMode ancientMode = platformContext - .getConfiguration() - .getConfigData(EventConfig.class) - .getAncientMode(); - this.nonAncientEventWindow = NonAncientEventWindow.getGenesisNonAncientEventWindow(ancientMode); - if (ancientMode == AncientMode.BIRTH_ROUND_THRESHOLD) { - observedEvents = new StandardSequenceMap<>(0, INITIAL_CAPACITY, true, EventDescriptor::getBirthRound); - } else { - observedEvents = new StandardSequenceMap<>(0, INITIAL_CAPACITY, true, EventDescriptor::getGeneration); - } - } +public interface EventDeduplicator { /** * Handle a potentially duplicate event @@ -138,52 +48,21 @@ public EventDeduplicator( * @return the event if it is not a duplicate, or null if it is a duplicate */ @Nullable - public GossipEvent handleEvent(@NonNull final GossipEvent event) { - if (nonAncientEventWindow.isAncient(event)) { - // Ancient events can be safely ignored. - intakeEventCounter.eventExitedIntakePipeline(event.getSenderId()); - return null; - } - - final Set signatures = observedEvents.computeIfAbsent(event.getDescriptor(), NEW_HASH_SET); - if (signatures.add(ByteBuffer.wrap(event.getUnhashedData().getSignature()))) { - if (signatures.size() != 1) { - // signature is unique, but descriptor is not - disparateSignatureAccumulator.update(1); - } - - // move toward 0% - avgDuplicatePercent.update(0); - - return event; - } else { - // duplicate descriptor and signature - duplicateEventsPerSecond.count(1); - // move toward 100% - avgDuplicatePercent.update(100); - intakeEventCounter.eventExitedIntakePipeline(event.getSenderId()); - - return null; - } - } + @InputWireLabel("non-deduplicated events") + GossipEvent handleEvent(@NonNull GossipEvent event); /** * Set the NonAncientEventWindow, defines the minimum threshold for an event to be non-ancient. * * @param nonAncientEventWindow the non-ancient event window */ - public void setNonAncientEventWindow(@NonNull final NonAncientEventWindow nonAncientEventWindow) { - this.nonAncientEventWindow = Objects.requireNonNull(nonAncientEventWindow); - - observedEvents.shiftWindow(nonAncientEventWindow.getAncientThreshold()); - } + @InputWireLabel("non-ancient event window") + void setNonAncientEventWindow(@NonNull NonAncientEventWindow nonAncientEventWindow); /** * Clear the internal state of this deduplicator. * * @param ignored ignored trigger object */ - public void clear(@NonNull final ClearTrigger ignored) { - observedEvents.clear(); - } + void clear(@NonNull final ClearTrigger ignored); } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/deduplication/StandardEventDeduplicator.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/deduplication/StandardEventDeduplicator.java new file mode 100644 index 000000000000..dbf025f14d94 --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/deduplication/StandardEventDeduplicator.java @@ -0,0 +1,172 @@ +/* + * 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.swirlds.platform.event.deduplication; + +import static com.swirlds.metrics.api.FloatFormats.FORMAT_10_2; +import static com.swirlds.metrics.api.Metrics.PLATFORM_CATEGORY; + +import com.swirlds.common.context.PlatformContext; +import com.swirlds.common.metrics.RunningAverageMetric; +import com.swirlds.common.metrics.extensions.CountPerSecond; +import com.swirlds.common.sequence.map.SequenceMap; +import com.swirlds.common.sequence.map.StandardSequenceMap; +import com.swirlds.metrics.api.LongAccumulator; +import com.swirlds.metrics.api.Metrics; +import com.swirlds.platform.consensus.NonAncientEventWindow; +import com.swirlds.platform.event.AncientMode; +import com.swirlds.platform.event.GossipEvent; +import com.swirlds.platform.eventhandling.EventConfig; +import com.swirlds.platform.gossip.IntakeEventCounter; +import com.swirlds.platform.system.events.EventDescriptor; +import com.swirlds.platform.wiring.ClearTrigger; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; + +/** + * A standard implementation of an {@link EventDeduplicator}. + */ +public class StandardEventDeduplicator implements EventDeduplicator { + /** + * Avoid the creation of lambdas for Map.computeIfAbsent() by reusing this lambda. + */ + private static final Function> NEW_HASH_SET = ignored -> new HashSet<>(); + + /** + * Initial capacity of {@link #observedEvents}. + */ + private static final int INITIAL_CAPACITY = 1024; + + /** + * The current non-ancient event window. + */ + private NonAncientEventWindow nonAncientEventWindow; + + /** + * Keeps track of the number of events in the intake pipeline from each peer + */ + private final IntakeEventCounter intakeEventCounter; + + /** + * A map from event descriptor to a set of signatures that have been received for that event. + */ + private final SequenceMap> observedEvents; + + private static final LongAccumulator.Config DISPARATE_SIGNATURE_CONFIG = new LongAccumulator.Config( + PLATFORM_CATEGORY, "eventsWithDisparateSignature") + .withDescription( + "Events received that match a descriptor of a previous event, but with a different signature") + .withUnit("events"); + private final LongAccumulator disparateSignatureAccumulator; + + private final CountPerSecond duplicateEventsPerSecond; + + private static final RunningAverageMetric.Config AVG_DUPLICATE_PERCENT_CONFIG = new RunningAverageMetric.Config( + PLATFORM_CATEGORY, "dupEvPercent") + .withDescription("percentage of events received that are already known") + .withFormat(FORMAT_10_2); + private final RunningAverageMetric avgDuplicatePercent; + + /** + * Constructor + * + * @param platformContext the platform context + * @param intakeEventCounter keeps track of the number of events in the intake pipeline from each peer + */ + public StandardEventDeduplicator( + @NonNull final PlatformContext platformContext, @NonNull final IntakeEventCounter intakeEventCounter) { + + this.intakeEventCounter = Objects.requireNonNull(intakeEventCounter); + + final Metrics metrics = platformContext.getMetrics(); + + this.disparateSignatureAccumulator = metrics.getOrCreate(DISPARATE_SIGNATURE_CONFIG); + this.duplicateEventsPerSecond = new CountPerSecond( + metrics, + new CountPerSecond.Config(PLATFORM_CATEGORY, "dupEv_per_sec") + .withDescription("number of events received per second that are already known") + .withUnit("hz")); + this.avgDuplicatePercent = metrics.getOrCreate(AVG_DUPLICATE_PERCENT_CONFIG); + + final AncientMode ancientMode = platformContext + .getConfiguration() + .getConfigData(EventConfig.class) + .getAncientMode(); + this.nonAncientEventWindow = NonAncientEventWindow.getGenesisNonAncientEventWindow(ancientMode); + if (ancientMode == AncientMode.BIRTH_ROUND_THRESHOLD) { + observedEvents = new StandardSequenceMap<>(0, INITIAL_CAPACITY, true, EventDescriptor::getBirthRound); + } else { + observedEvents = new StandardSequenceMap<>(0, INITIAL_CAPACITY, true, EventDescriptor::getGeneration); + } + } + + /** + * {@inheritDoc} + */ + @Override + @Nullable + public GossipEvent handleEvent(@NonNull final GossipEvent event) { + if (nonAncientEventWindow.isAncient(event)) { + // Ancient events can be safely ignored. + intakeEventCounter.eventExitedIntakePipeline(event.getSenderId()); + return null; + } + + final Set signatures = observedEvents.computeIfAbsent(event.getDescriptor(), NEW_HASH_SET); + if (signatures.add(ByteBuffer.wrap(event.getUnhashedData().getSignature()))) { + if (signatures.size() != 1) { + // signature is unique, but descriptor is not + disparateSignatureAccumulator.update(1); + } + + // move toward 0% + avgDuplicatePercent.update(0); + + return event; + } else { + // duplicate descriptor and signature + duplicateEventsPerSecond.count(1); + // move toward 100% + avgDuplicatePercent.update(100); + intakeEventCounter.eventExitedIntakePipeline(event.getSenderId()); + + return null; + } + } + + /** + * {@inheritDoc} + */ + @Override + public void setNonAncientEventWindow(@NonNull final NonAncientEventWindow nonAncientEventWindow) { + this.nonAncientEventWindow = Objects.requireNonNull(nonAncientEventWindow); + + observedEvents.shiftWindow(nonAncientEventWindow.getAncientThreshold()); + } + + /** + * {@inheritDoc} + */ + @Override + public void clear(@NonNull final ClearTrigger ignored) { + observedEvents.clear(); + } +} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/EventDeduplicatorWiring.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/EventDeduplicatorWiring.java deleted file mode 100644 index ddf94ca8cfd5..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/EventDeduplicatorWiring.java +++ /dev/null @@ -1,70 +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.swirlds.platform.wiring; - -import com.swirlds.common.wiring.schedulers.TaskScheduler; -import com.swirlds.common.wiring.wires.input.BindableInputWire; -import com.swirlds.common.wiring.wires.input.InputWire; -import com.swirlds.common.wiring.wires.output.OutputWire; -import com.swirlds.platform.consensus.NonAncientEventWindow; -import com.swirlds.platform.event.GossipEvent; -import com.swirlds.platform.event.deduplication.EventDeduplicator; -import edu.umd.cs.findbugs.annotations.NonNull; - -/** - * Wiring for the {@link EventDeduplicator}. - * - * @param eventInput the input wire for events to be deduplicated - * @param nonAncientEventWindowInput the input wire for the minimum non-ancient threshold - * @param clearInput the input wire to clear the internal state of the deduplicator - * @param eventOutput the output wire for deduplicated events - * @param flushRunnable the runnable to flush the deduplicator - */ -public record EventDeduplicatorWiring( - @NonNull InputWire eventInput, - @NonNull InputWire nonAncientEventWindowInput, - @NonNull InputWire clearInput, - @NonNull OutputWire eventOutput, - @NonNull Runnable flushRunnable) { - - /** - * Create a new instance of this wiring. - * - * @param taskScheduler the task scheduler for this deduplicator - * @return the new wiring instance - */ - public static EventDeduplicatorWiring create(@NonNull final TaskScheduler taskScheduler) { - return new EventDeduplicatorWiring( - taskScheduler.buildInputWire("non-deduplicated events"), - taskScheduler.buildInputWire("non-ancient event window"), - taskScheduler.buildInputWire("clear"), - taskScheduler.getOutputWire(), - taskScheduler::flush); - } - - /** - * Bind a deduplicator to this wiring. - * - * @param deduplicator the deduplicator to bind - */ - public void bind(@NonNull final EventDeduplicator deduplicator) { - ((BindableInputWire) eventInput).bind(deduplicator::handleEvent); - ((BindableInputWire) nonAncientEventWindowInput) - .bind(deduplicator::setNonAncientEventWindow); - ((BindableInputWire) clearInput).bind(deduplicator::clear); - } -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformCoordinator.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformCoordinator.java index 4b8a9b9eaf83..be4b962c5839 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformCoordinator.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformCoordinator.java @@ -16,7 +16,10 @@ package com.swirlds.platform.wiring; +import com.swirlds.common.wiring.component.ComponentWiring; import com.swirlds.common.wiring.counters.ObjectCounter; +import com.swirlds.platform.event.GossipEvent; +import com.swirlds.platform.event.deduplication.EventDeduplicator; import com.swirlds.platform.wiring.components.ApplicationTransactionPrehandlerWiring; import com.swirlds.platform.wiring.components.ConsensusRoundHandlerWiring; import com.swirlds.platform.wiring.components.EventCreationManagerWiring; @@ -39,7 +42,7 @@ public class PlatformCoordinator { private final ObjectCounter hashingObjectCounter; private final InternalEventValidatorWiring internalEventValidatorWiring; - private final EventDeduplicatorWiring eventDeduplicatorWiring; + private final ComponentWiring eventDeduplicatorWiring; private final EventSignatureValidatorWiring eventSignatureValidatorWiring; private final OrphanBufferWiring orphanBufferWiring; private final InOrderLinkerWiring inOrderLinkerWiring; @@ -69,7 +72,7 @@ public class PlatformCoordinator { public PlatformCoordinator( @NonNull final ObjectCounter hashingObjectCounter, @NonNull final InternalEventValidatorWiring internalEventValidatorWiring, - @NonNull final EventDeduplicatorWiring eventDeduplicatorWiring, + @NonNull final ComponentWiring eventDeduplicatorWiring, @NonNull final EventSignatureValidatorWiring eventSignatureValidatorWiring, @NonNull final OrphanBufferWiring orphanBufferWiring, @NonNull final InOrderLinkerWiring inOrderLinkerWiring, @@ -104,7 +107,7 @@ public void flushIntakePipeline() { hashingObjectCounter.waitUntilEmpty(); internalEventValidatorWiring.flushRunnable().run(); - eventDeduplicatorWiring.flushRunnable().run(); + eventDeduplicatorWiring.flush(); eventSignatureValidatorWiring.flushRunnable().run(); orphanBufferWiring.flushRunnable().run(); eventCreationManagerWiring.flush(); @@ -146,7 +149,7 @@ public void clear() { // Phase 4: clear // Data is no longer moving through the system. Clear all the internal data structures in the wiring objects. - eventDeduplicatorWiring.clearInput().inject(new ClearTrigger()); + eventDeduplicatorWiring.getInputWire(EventDeduplicator::clear).inject(new ClearTrigger()); orphanBufferWiring.clearInput().inject(new ClearTrigger()); inOrderLinkerWiring.clearInput().inject(new ClearTrigger()); stateSignatureCollectorWiring.getClearInput().inject(new ClearTrigger()); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformWiring.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformWiring.java index a30c6e105751..37cee0989f07 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformWiring.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformWiring.java @@ -27,6 +27,7 @@ import com.swirlds.common.stream.EventStreamManager; import com.swirlds.common.stream.RunningEventHashUpdate; import com.swirlds.common.utility.Clearable; +import com.swirlds.common.wiring.component.ComponentWiring; import com.swirlds.common.wiring.counters.BackpressureObjectCounter; import com.swirlds.common.wiring.counters.ObjectCounter; import com.swirlds.common.wiring.model.WiringModel; @@ -118,7 +119,7 @@ public class PlatformWiring implements Startable, Stoppable, Clearable { private final EventHasherWiring eventHasherWiring; private final PostHashCollectorWiring postHashCollectorWiring; private final InternalEventValidatorWiring internalEventValidatorWiring; - private final EventDeduplicatorWiring eventDeduplicatorWiring; + private final ComponentWiring eventDeduplicatorWiring; private final EventSignatureValidatorWiring eventSignatureValidatorWiring; private final OrphanBufferWiring orphanBufferWiring; private final InOrderLinkerWiring inOrderLinkerWiring; @@ -191,7 +192,8 @@ public PlatformWiring(@NonNull final PlatformContext platformContext) { postHashCollectorWiring = PostHashCollectorWiring.create(schedulers.postHashCollectorScheduler()); internalEventValidatorWiring = InternalEventValidatorWiring.create(schedulers.internalEventValidatorScheduler()); - eventDeduplicatorWiring = EventDeduplicatorWiring.create(schedulers.eventDeduplicatorScheduler()); + eventDeduplicatorWiring = + new ComponentWiring<>(EventDeduplicator.class, schedulers.eventDeduplicatorScheduler()); eventSignatureValidatorWiring = EventSignatureValidatorWiring.create(schedulers.eventSignatureValidatorScheduler()); orphanBufferWiring = OrphanBufferWiring.create(schedulers.orphanBufferScheduler()); @@ -262,7 +264,8 @@ private void solderNonAncientEventWindow() { final OutputWire nonAncientEventWindowOutputWire = eventWindowManagerWiring.nonAncientEventWindowOutput(); - nonAncientEventWindowOutputWire.solderTo(eventDeduplicatorWiring.nonAncientEventWindowInput(), INJECT); + nonAncientEventWindowOutputWire.solderTo( + eventDeduplicatorWiring.getInputWire(EventDeduplicator::setNonAncientEventWindow), INJECT); nonAncientEventWindowOutputWire.solderTo(eventSignatureValidatorWiring.nonAncientEventWindowInput(), INJECT); nonAncientEventWindowOutputWire.solderTo(orphanBufferWiring.nonAncientEventWindowInput(), INJECT); nonAncientEventWindowOutputWire.solderTo(inOrderLinkerWiring.nonAncientEventWindowInput(), INJECT); @@ -287,8 +290,10 @@ private void wire() { gossipWiring.eventOutput().solderTo(pipelineInputWire); eventHasherWiring.eventOutput().solderTo(postHashCollectorWiring.eventInput()); postHashCollectorWiring.eventOutput().solderTo(internalEventValidatorWiring.eventInput()); - internalEventValidatorWiring.eventOutput().solderTo(eventDeduplicatorWiring.eventInput()); - eventDeduplicatorWiring.eventOutput().solderTo(eventSignatureValidatorWiring.eventInput()); + internalEventValidatorWiring + .eventOutput() + .solderTo(eventDeduplicatorWiring.getInputWire(EventDeduplicator::handleEvent)); + eventDeduplicatorWiring.getOutputWire().solderTo(eventSignatureValidatorWiring.eventInput()); eventSignatureValidatorWiring.eventOutput().solderTo(orphanBufferWiring.eventInput()); orphanBufferWiring.eventOutput().solderTo(pcesSequencerWiring.eventInput()); pcesSequencerWiring.eventOutput().solderTo(inOrderLinkerWiring.eventInput()); diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/EventDeduplicatorTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/EventDeduplicatorTests.java index 87275d26402f..929d7f60d1c6 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/EventDeduplicatorTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/EventDeduplicatorTests.java @@ -34,6 +34,7 @@ import com.swirlds.platform.consensus.ConsensusConstants; import com.swirlds.platform.consensus.NonAncientEventWindow; import com.swirlds.platform.event.deduplication.EventDeduplicator; +import com.swirlds.platform.event.deduplication.StandardEventDeduplicator; import com.swirlds.platform.eventhandling.EventConfig_; import com.swirlds.platform.gossip.IntakeEventCounter; import com.swirlds.platform.system.events.BaseEventUnhashedData; @@ -148,7 +149,7 @@ void standardOperation(final boolean useBirthRoundForAncientThreshold) { .when(intakeEventCounter) .eventExitedIntakePipeline(any()); - final EventDeduplicator deduplicator = new EventDeduplicator( + final EventDeduplicator deduplicator = new StandardEventDeduplicator( TestPlatformContextBuilder.create() .withConfiguration(new TestConfigBuilder() .withValue( From f5aa4b37ee0a378a1682d793f24a067b76594c75 Mon Sep 17 00:00:00 2001 From: Maxi Tartaglia <152629744+mxtartaglia-sl@users.noreply.github.com> Date: Thu, 7 Mar 2024 15:40:07 -0300 Subject: [PATCH 035/115] feat: 11830 performance improvements log fwk (#11831) Signed-off-by: mxtartaglia --- .../com.hedera.hashgraph.java.gradle.kts | 8 + hedera-node/test-clients/build.gradle.kts | 6 - platform-sdk/swirlds-logging/build.gradle.kts | 15 +- .../com/swirlds/logging/LoggingBenchmark.java | 299 ------------------ .../benchmark/config/Configuration.java | 33 ++ .../logging/benchmark/config/Constants.java | 45 +++ .../benchmark/log4j2/Log4J2Benchmark.java | 99 ++++++ .../log4j2/Log4J2FineGraneBenchmark.java | 280 ++++++++++++++++ .../benchmark/log4j2/Log4JConfiguration.java | 117 +++++++ .../logging/benchmark/log4j2/Log4JRunner.java | 68 ++++ .../swirldslog/SwirldsLogBenchmark.java | 99 ++++++ .../swirldslog/SwirldsLogConfiguration.java | 106 +++++++ .../SwirldsLogFineGraneBenchmark.java | 275 ++++++++++++++++ .../swirldslog/SwirldsLogRunner.java | 53 ++++ .../benchmark/util/ConfigManagement.java | 61 ++++ .../logging/benchmark/util/LogFiles.java | 80 +++++ .../logging/benchmark/util/Throwables.java | 67 ++++ .../java/com/swirlds/logging/api/Level.java | 18 ++ .../api/extensions/event/LogEvent.java | 1 - .../handler/AbstractAsyncHandler.java | 81 +++++ .../handler/AbstractSyncedHandler.java | 33 +- .../extensions/handler/LogHandlerFactory.java | 4 +- .../emergency/EmergencyLoggerImpl.java | 31 +- .../api/internal/event/MutableLogEvent.java | 3 +- .../event/ParameterizedLogMessage.java | 32 +- .../api/internal/format/EpochFormatUtils.java | 85 +++++ .../internal/format/FormattedLinePrinter.java | 178 +++++++++++ .../api/internal/format/LineBasedFormat.java | 174 ---------- .../internal/format/StackTracePrinter.java | 77 ++++- .../level/HandlerLoggingLevelConfig.java | 14 +- .../logging/buffer/BufferedOutputStream.java | 139 ++++++++ .../logging/console/ConsoleHandler.java | 52 ++- .../console/ConsoleHandlerFactory.java | 13 +- .../com/swirlds/logging/file/FileHandler.java | 76 ++--- .../logging/file/FileHandlerFactory.java | 19 +- .../swirlds/logging/EmergencyLoggerTest.java | 2 +- .../com/swirlds/logging/LogLevelTest.java | 2 +- .../com/swirlds/logging/LoggerImplTest.java | 29 ++ .../logging/LoggingSystemStressTest.java | 77 ++++- .../swirlds/logging/LoggingSystemTest.java | 80 +++++ .../internal/format/EpochFormatUtilsTest.java | 72 +++++ .../buffer/BufferedOutputStreamTest.java | 149 +++++++++ .../logging/util/LoggingTestUtils.java | 184 +++++++++++ .../swirlds/logging/util/LoggingUtils.java | 69 ---- 44 files changed, 2720 insertions(+), 685 deletions(-) delete mode 100644 platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/LoggingBenchmark.java create mode 100644 platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/config/Configuration.java create mode 100644 platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/config/Constants.java create mode 100644 platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/log4j2/Log4J2Benchmark.java create mode 100644 platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/log4j2/Log4J2FineGraneBenchmark.java create mode 100644 platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/log4j2/Log4JConfiguration.java create mode 100644 platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/log4j2/Log4JRunner.java create mode 100644 platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/swirldslog/SwirldsLogBenchmark.java create mode 100644 platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/swirldslog/SwirldsLogConfiguration.java create mode 100644 platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/swirldslog/SwirldsLogFineGraneBenchmark.java create mode 100644 platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/swirldslog/SwirldsLogRunner.java create mode 100644 platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/util/ConfigManagement.java create mode 100644 platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/util/LogFiles.java create mode 100644 platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/util/Throwables.java create mode 100644 platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/extensions/handler/AbstractAsyncHandler.java create mode 100644 platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/format/EpochFormatUtils.java create mode 100644 platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/format/FormattedLinePrinter.java delete mode 100644 platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/format/LineBasedFormat.java create mode 100644 platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/buffer/BufferedOutputStream.java create mode 100644 platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/LoggingSystemTest.java create mode 100644 platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/api/internal/format/EpochFormatUtilsTest.java create mode 100644 platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/buffer/BufferedOutputStreamTest.java create mode 100644 platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/util/LoggingTestUtils.java delete mode 100644 platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/util/LoggingUtils.java diff --git a/build-logic/project-plugins/src/main/kotlin/com.hedera.hashgraph.java.gradle.kts b/build-logic/project-plugins/src/main/kotlin/com.hedera.hashgraph.java.gradle.kts index f3174eaaefb3..70eca7642b48 100644 --- a/build-logic/project-plugins/src/main/kotlin/com.hedera.hashgraph.java.gradle.kts +++ b/build-logic/project-plugins/src/main/kotlin/com.hedera.hashgraph.java.gradle.kts @@ -331,3 +331,11 @@ tasks.test { excludeTags("TIME_CONSUMING") } } + +tasks.withType().configureEach { + // Do not yet run things on the '--module-path' + modularity.inferModulePath = false + if (name.endsWith("main()")) { + notCompatibleWithConfigurationCache("JavaExec created by IntelliJ") + } +} diff --git a/hedera-node/test-clients/build.gradle.kts b/hedera-node/test-clients/build.gradle.kts index 21ca239a808b..a5788651e44b 100644 --- a/hedera-node/test-clients/build.gradle.kts +++ b/hedera-node/test-clients/build.gradle.kts @@ -51,12 +51,6 @@ sourceSets { create("yahcli") } -// IntelliJ uses adhoc-created JavaExec tasks when running a 'main()' method. -tasks.withType { - // Do not yet run things on the '--module-path' - modularity.inferModulePath.set(false) -} - // The following tasks run the 'HapiTestEngine' tests (residing in src/main/java). // IntelliJ picks up this task when running tests through in the IDE. diff --git a/platform-sdk/swirlds-logging/build.gradle.kts b/platform-sdk/swirlds-logging/build.gradle.kts index b8ca4d37ed6a..891b47ae13ce 100644 --- a/platform-sdk/swirlds-logging/build.gradle.kts +++ b/platform-sdk/swirlds-logging/build.gradle.kts @@ -23,11 +23,6 @@ plugins { mainModuleInfo { annotationProcessor("com.google.auto.service.processor") } -jmhModuleInfo { - requires("com.swirlds.config.api") - runtimeOnly("com.swirlds.config.impl") -} - testModuleInfo { requires("org.apache.logging.log4j.core") requires("com.swirlds.config.extensions.test.fixtures") @@ -38,3 +33,13 @@ testModuleInfo { requires("com.swirlds.common.test.fixtures") requires("jakarta.inject") } + +jmhModuleInfo { + requires("com.swirlds.logging") + requires("org.apache.logging.log4j") + requires("com.swirlds.config.api") + runtimeOnly("com.swirlds.config.impl") + requires("org.apache.logging.log4j.core") + requires("com.github.spotbugs.annotations") + requires("jmh.core") +} diff --git a/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/LoggingBenchmark.java b/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/LoggingBenchmark.java deleted file mode 100644 index dbaac111b4f6..000000000000 --- a/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/LoggingBenchmark.java +++ /dev/null @@ -1,299 +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.swirlds.logging; - -import com.swirlds.config.api.Configuration; -import com.swirlds.config.api.ConfigurationBuilder; -import com.swirlds.logging.api.Logger; -import com.swirlds.logging.api.extensions.handler.LogHandler; -import com.swirlds.logging.api.internal.LoggingSystem; -import com.swirlds.logging.api.internal.configuration.ConfigLevelConverter; -import com.swirlds.logging.api.internal.configuration.MarkerStateConverter; -import com.swirlds.logging.console.ConsoleHandler; -import com.swirlds.logging.file.FileHandlerFactory; -import java.io.IOException; -import java.net.URISyntaxException; -import java.util.Objects; -import java.util.concurrent.TimeUnit; -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.BenchmarkMode; -import org.openjdk.jmh.annotations.Fork; -import org.openjdk.jmh.annotations.Level; -import org.openjdk.jmh.annotations.Measurement; -import org.openjdk.jmh.annotations.Mode; -import org.openjdk.jmh.annotations.OutputTimeUnit; -import org.openjdk.jmh.annotations.Param; -import org.openjdk.jmh.annotations.Scope; -import org.openjdk.jmh.annotations.Setup; -import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.annotations.Threads; -import org.openjdk.jmh.annotations.Warmup; - -@State(Scope.Benchmark) -@Fork(1) -@Warmup(iterations = 3, time = 2) -@Measurement(iterations = 5, time = 2) -@Threads(5) -@OutputTimeUnit(TimeUnit.MILLISECONDS) -@BenchmarkMode(Mode.Throughput) -public class LoggingBenchmark { - - private static final String MESSAGE = "This is a simple log message"; - - private static final String MESSAGE_WITH_PLACEHOLDER = "This is a {} log message"; - - private static final String MESSAGE_WITH_MANY_PLACEHOLDERS = - "This is a {} log message that counts up: one, {},{},{},{},{},{},{},{},{},{},{}"; - - private static final String PLACEHOLDER_1 = "combined"; - - private static final String PLACEHOLDER_2 = "two"; - - private static final String PLACEHOLDER_3 = "three"; - - private static final String PLACEHOLDER_4 = "four"; - - private static final String PLACEHOLDER_5 = "five"; - - private static final String PLACEHOLDER_6 = "six"; - - private static final String PLACEHOLDER_7 = "seven"; - - private static final String PLACEHOLDER_8 = "eight"; - - private static final String PLACEHOLDER_9 = "nine"; - - private static final String PLACEHOLDER_10 = "ten"; - - private static final String PLACEHOLDER_11 = "eleven"; - - private static final String PLACEHOLDER_12 = "twelve"; - - private static final String EXCEPTION_MESSAGE = "Error while doing something"; - - private static final String LONG_MESSAGE = - """ - Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. - Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. - Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. - Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. - Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. - At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. - Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. - Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. - Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. - Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. - Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo"""; - - private static final String MARKER_1 = "MARKER_1"; - - private static final String MARKER_2 = "MARKER_2"; - - private static final String MARKER_3 = "MARKER_3"; - - private static final String CONTEXT_1_KEY = "name"; - - private static final String CONTEXT_1_VALUE = "benchmark"; - - private static final String CONTEXT_2_KEY = "type"; - - private static final String CONTEXT_2_VALUE = "jmh"; - - private static final String CONTEXT_3_KEY = "state"; - - private static final String CONTEXT_3_VALUE = "running"; - - private static void createDeepStackTrace(int levelsToGo, String exceptionMessage) { - if (levelsToGo <= 0) { - throw new RuntimeException(exceptionMessage); - } else { - createDeepStackTrace(levelsToGo - 1, exceptionMessage); - } - } - - private static void createRecursiveDeepStackTrace(int levelsToGo, int throwModulo, String exceptionMessage) { - if (levelsToGo <= 0) { - throw new RuntimeException(exceptionMessage); - } else { - if (levelsToGo % throwModulo == 0) { - try { - createRecursiveDeepStackTrace(levelsToGo - 1, throwModulo, exceptionMessage); - } catch (Exception e) { - throw new RuntimeException(exceptionMessage + "in level " + levelsToGo, e); - } - } else { - createRecursiveDeepStackTrace(levelsToGo - 1, throwModulo, exceptionMessage); - } - } - } - - private Logger logger; - - private Exception exceptionWithNormalStackTrace; - - private Exception exceptionWithNormalStackTraceAndLongMessage; - - private Exception exceptionWithDeepStackTrace; - - private Exception exceptionWithDeepStackTraceAndDeepCause; - - @Param({"ONLY_EMERGENCY", "CONSOLE_HANDLER", "NOOP_HANDLER", "LEVEL_OFF", "FILE_HANDLER"}) - public String setup; - - @Setup(Level.Iteration) - public void setup() throws IOException, URISyntaxException { - final LoggingSystem loggingSystem; - if (Objects.equals(setup, "ONLY_EMERGENCY")) { - final Configuration configuration = ConfigurationBuilder.create() - .withConverter(new ConfigLevelConverter()) - .withConverter(new MarkerStateConverter()) - .withValue("logging.level", "trace") - .build(); - loggingSystem = new LoggingSystem(configuration); - } else if (Objects.equals(setup, "CONSOLE_HANDLER")) { - final Configuration configuration = ConfigurationBuilder.create() - .withConverter(new ConfigLevelConverter()) - .withConverter(new MarkerStateConverter()) - .withValue("logging.level", "trace") - .withValue("logging.handler.console.type", "console") - .withValue("logging.handler.console.active", "true") - .withValue("logging.handler.console.level", "trace") - .build(); - loggingSystem = new LoggingSystem(configuration); - loggingSystem.addHandler(new ConsoleHandler("console", configuration)); - } else if (Objects.equals(setup, "NOOP_HANDLER")) { - final Configuration configuration = ConfigurationBuilder.create() - .withConverter(new ConfigLevelConverter()) - .withConverter(new MarkerStateConverter()) - .withValue("logging.level", "trace") - .build(); - loggingSystem = new LoggingSystem(configuration); - loggingSystem.addHandler(logEvent -> { - // NOOP - }); - } else if (Objects.equals(setup, "FILE_HANDLER")) { - final Configuration configuration = ConfigurationBuilder.create() - .withConverter(new ConfigLevelConverter()) - .withConverter(new MarkerStateConverter()) - .withValue("logging.level", "trace") - .withValue("logging.handler.file.type", "file") - .withValue("logging.handler.file.active", "true") - .withValue("logging.handler.file.level", "trace") - .withValue("logging.handler.file.file", "benchmark.log") - .build(); - final LogHandler fileHandler = new FileHandlerFactory().create("file", configuration); - loggingSystem = new LoggingSystem(configuration); - loggingSystem.addHandler(fileHandler); - } else { - final Configuration configuration = ConfigurationBuilder.create() - .withConverter(new ConfigLevelConverter()) - .withConverter(new MarkerStateConverter()) - .withValue("logging.level", "off") - .build(); - loggingSystem = new LoggingSystem(configuration); - } - logger = loggingSystem.getLogger(LoggingBenchmark.class.getName() + "." + setup.substring(0, 9)); - exceptionWithNormalStackTrace = new RuntimeException(EXCEPTION_MESSAGE); - exceptionWithNormalStackTraceAndLongMessage = new RuntimeException(LONG_MESSAGE); - try { - createDeepStackTrace(200, EXCEPTION_MESSAGE); - } catch (final RuntimeException e) { - exceptionWithDeepStackTrace = e; - } - try { - createRecursiveDeepStackTrace(200, 10, EXCEPTION_MESSAGE); - } catch (final RuntimeException e) { - exceptionWithDeepStackTraceAndDeepCause = e; - } - } - - @Benchmark - public void executeSimpleLog() { - logger.info(MESSAGE); - } - - @Benchmark - public void executeSimpleLogWithMarker() { - logger.withMarker(MARKER_1).info(MESSAGE); - } - - @Benchmark - public void executeSimpleLogWithMultipleMarkers() { - logger.withMarker(MARKER_1).withMarker(MARKER_2).withMarker(MARKER_3).info(MESSAGE); - } - - @Benchmark - public void executeSimpleLogWithLongMessage() { - logger.info(LONG_MESSAGE); - } - - @Benchmark - public void executeSimpleLogWithException() { - logger.info(MESSAGE, exceptionWithNormalStackTrace); - } - - @Benchmark - public void executeSimpleLogWithExceptionWithLongMessage() { - logger.info(MESSAGE, exceptionWithNormalStackTraceAndLongMessage); - } - - @Benchmark - public void executeSimpleLogWithExceptionWithDeepStackTrace() { - logger.info(MESSAGE, exceptionWithDeepStackTrace); - } - - @Benchmark - public void executeSimpleLogWithExceptionWithDeepStackTraceAndDeepCause() { - logger.info(MESSAGE, exceptionWithDeepStackTraceAndDeepCause); - } - - @Benchmark - public void executeSimpleLogWithMessageWithPlaceholder() { - logger.info(MESSAGE_WITH_PLACEHOLDER, PLACEHOLDER_1); - } - - @Benchmark - public void executeSimpleLogWithMessageWithMultiplePlaceholders() { - logger.info( - MESSAGE_WITH_MANY_PLACEHOLDERS, - PLACEHOLDER_1, - PLACEHOLDER_2, - PLACEHOLDER_3, - PLACEHOLDER_4, - PLACEHOLDER_5, - PLACEHOLDER_6, - PLACEHOLDER_7, - PLACEHOLDER_8, - PLACEHOLDER_9, - PLACEHOLDER_10, - PLACEHOLDER_11, - PLACEHOLDER_12); - } - - @Benchmark - public void executeSimpleLogWithContextValue() { - logger.withContext(CONTEXT_1_KEY, CONTEXT_1_VALUE).info(MESSAGE); - } - - @Benchmark - public void executeSimpleLogWithMultiplyContextValues() { - logger.withContext(CONTEXT_1_KEY, CONTEXT_1_VALUE) - .withContext(CONTEXT_2_KEY, CONTEXT_3_VALUE) - .withContext(CONTEXT_3_KEY, CONTEXT_3_VALUE) - .info(MESSAGE); - } -} diff --git a/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/config/Configuration.java b/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/config/Configuration.java new file mode 100644 index 000000000000..41c9bdcb949c --- /dev/null +++ b/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/config/Configuration.java @@ -0,0 +1,33 @@ +/* + * 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.logging.benchmark.config; + +import edu.umd.cs.findbugs.annotations.NonNull; + +public interface Configuration { + + @NonNull + T configureFileLogging(); + + @NonNull + T configureConsoleLogging(); + + @NonNull + T configureFileAndConsoleLogging(); + + void tierDown(); +} diff --git a/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/config/Constants.java b/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/config/Constants.java new file mode 100644 index 000000000000..b05778c4db1e --- /dev/null +++ b/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/config/Constants.java @@ -0,0 +1,45 @@ +/* + * 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.logging.benchmark.config; + +public class Constants { + public static final String CONSOLE_TYPE = "CONSOLE"; + public static final String FILE_TYPE = "FILE"; + public static final String CONSOLE_AND_FILE_TYPE = "CONSOLE_AND_FILE"; + public static final String SWIRLDS = "SWIRLDS"; + public static final String LOG4J2 = "LOG4J2"; + + public static final int WARMUP_ITERATIONS = 10; + + public static final int WARMUP_TIME_IN_SECONDS_PER_ITERATION = 20; + + public static final int MEASUREMENT_ITERATIONS = 20; + + public static final int MEASUREMENT_TIME_IN_SECONDS_PER_ITERATION = 200; + + public static final int PARALLEL_THREAD_COUNT = 1; + + public static final int FORK_COUNT = 1; + public static final String ENABLE_TIME_FORMATTING_ENV = "ENABLE_TIME_FORMATTING"; + public static final String DELETE_OUTPUT_FILES_ENV = "DELETE_OUTPUT_FILES"; + public static final String DELETE_OUTPUT_FOLDER_ENV = "DELETE_OUTPUT_FOLDER"; + public static final boolean ENABLE_TIME_FORMATTING = false; + public static final boolean DELETE_OUTPUT_FILES = true; + public static final boolean DELETE_OUTPUT_FOLDER = true; + + private Constants() {} +} diff --git a/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/log4j2/Log4J2Benchmark.java b/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/log4j2/Log4J2Benchmark.java new file mode 100644 index 000000000000..54e815cfc159 --- /dev/null +++ b/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/log4j2/Log4J2Benchmark.java @@ -0,0 +1,99 @@ +/* + * 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.logging.benchmark.log4j2; + +import static com.swirlds.logging.benchmark.config.Constants.CONSOLE_AND_FILE_TYPE; +import static com.swirlds.logging.benchmark.config.Constants.CONSOLE_TYPE; +import static com.swirlds.logging.benchmark.config.Constants.FILE_TYPE; +import static com.swirlds.logging.benchmark.config.Constants.FORK_COUNT; +import static com.swirlds.logging.benchmark.config.Constants.MEASUREMENT_ITERATIONS; +import static com.swirlds.logging.benchmark.config.Constants.MEASUREMENT_TIME_IN_SECONDS_PER_ITERATION; +import static com.swirlds.logging.benchmark.config.Constants.PARALLEL_THREAD_COUNT; +import static com.swirlds.logging.benchmark.config.Constants.WARMUP_ITERATIONS; +import static com.swirlds.logging.benchmark.config.Constants.WARMUP_TIME_IN_SECONDS_PER_ITERATION; + +import com.swirlds.logging.benchmark.config.Configuration; +import com.swirlds.logging.benchmark.config.Constants; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.spi.LoggerContext; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +@State(Scope.Benchmark) +public class Log4J2Benchmark { + private static final String LOGGER_NAME = Constants.LOG4J2 + "Benchmark"; + + @Param({CONSOLE_TYPE, FILE_TYPE, CONSOLE_AND_FILE_TYPE}) + public String loggingType; + + private Logger logger; + private Log4JRunner logRunner; + + private Configuration config; + ; + + @Setup(Level.Trial) + public void init() { + config = new Log4JConfiguration(); + if (Objects.equals(loggingType, FILE_TYPE)) { + logger = config.configureFileLogging().getLogger(LOGGER_NAME); + } else if (Objects.equals(loggingType, CONSOLE_TYPE)) { + logger = config.configureConsoleLogging().getLogger(LOGGER_NAME); + } else if (Objects.equals(loggingType, CONSOLE_AND_FILE_TYPE)) { + logger = config.configureFileAndConsoleLogging().getLogger(LOGGER_NAME); + } + logRunner = new Log4JRunner(logger); + } + + @Benchmark + @Fork(value = FORK_COUNT) + @Threads(PARALLEL_THREAD_COUNT) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + @Warmup( + iterations = WARMUP_ITERATIONS, + time = WARMUP_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + @Measurement( + iterations = MEASUREMENT_ITERATIONS, + time = MEASUREMENT_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + public void log4J() { + logRunner.run(); + } + + @TearDown(Level.Iteration) + public void tearDown() { + LogManager.shutdown(); + config.tierDown(); + } +} diff --git a/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/log4j2/Log4J2FineGraneBenchmark.java b/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/log4j2/Log4J2FineGraneBenchmark.java new file mode 100644 index 000000000000..604b5d1c2408 --- /dev/null +++ b/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/log4j2/Log4J2FineGraneBenchmark.java @@ -0,0 +1,280 @@ +/* + * 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.logging.benchmark.log4j2; + +import static com.swirlds.logging.benchmark.config.Constants.CONSOLE_AND_FILE_TYPE; +import static com.swirlds.logging.benchmark.config.Constants.CONSOLE_TYPE; +import static com.swirlds.logging.benchmark.config.Constants.FILE_TYPE; +import static com.swirlds.logging.benchmark.config.Constants.FORK_COUNT; +import static com.swirlds.logging.benchmark.config.Constants.MEASUREMENT_ITERATIONS; +import static com.swirlds.logging.benchmark.config.Constants.MEASUREMENT_TIME_IN_SECONDS_PER_ITERATION; +import static com.swirlds.logging.benchmark.config.Constants.PARALLEL_THREAD_COUNT; +import static com.swirlds.logging.benchmark.config.Constants.WARMUP_ITERATIONS; +import static com.swirlds.logging.benchmark.config.Constants.WARMUP_TIME_IN_SECONDS_PER_ITERATION; + +import com.swirlds.logging.benchmark.config.Configuration; +import com.swirlds.logging.benchmark.config.Constants; +import com.swirlds.logging.benchmark.util.Throwables; +import java.math.BigDecimal; +import java.util.Collections; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.MarkerManager; +import org.apache.logging.log4j.ThreadContext; +import org.apache.logging.log4j.spi.LoggerContext; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +@State(Scope.Benchmark) +public class Log4J2FineGraneBenchmark { + private static final String LOGGER_NAME = Constants.LOG4J2 + "Benchmark"; + + @Param({CONSOLE_TYPE, FILE_TYPE, CONSOLE_AND_FILE_TYPE}) + public String loggingType; + + private Logger logger; + private Configuration config; + + private static final Marker MARKER = MarkerManager.getMarker("marker"); + + @Setup(Level.Trial) + public void init() { + config = new Log4JConfiguration(); + if (Objects.equals(loggingType, FILE_TYPE)) { + logger = config.configureFileLogging().getLogger(LOGGER_NAME); + } else if (Objects.equals(loggingType, CONSOLE_TYPE)) { + logger = config.configureConsoleLogging().getLogger(LOGGER_NAME); + } else if (Objects.equals(loggingType, CONSOLE_AND_FILE_TYPE)) { + logger = config.configureFileAndConsoleLogging().getLogger(LOGGER_NAME); + } + } + + @Benchmark + @Fork(value = FORK_COUNT) + @Threads(PARALLEL_THREAD_COUNT) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + @Warmup( + iterations = WARMUP_ITERATIONS, + time = WARMUP_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + @Measurement( + iterations = MEASUREMENT_ITERATIONS, + time = MEASUREMENT_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + public void logSimpleStatement() { + logger.log(org.apache.logging.log4j.Level.INFO, "logSimpleStatement, Hello world!"); + } + + @Benchmark + @Fork(value = FORK_COUNT) + @Threads(PARALLEL_THREAD_COUNT) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + @Warmup( + iterations = WARMUP_ITERATIONS, + time = WARMUP_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + @Measurement( + iterations = MEASUREMENT_ITERATIONS, + time = MEASUREMENT_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + public void logOffStatement() { + logger.log(org.apache.logging.log4j.Level.OFF, "logSimpleStatement, Hello world!"); + } + + @Benchmark + @Fork(value = FORK_COUNT) + @Threads(PARALLEL_THREAD_COUNT) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + @Warmup( + iterations = WARMUP_ITERATIONS, + time = WARMUP_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + @Measurement( + iterations = MEASUREMENT_ITERATIONS, + time = MEASUREMENT_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + public void logLargeStatement() { + + String logMessage = + """ + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus aliquam dolor placerat, efficitur erat a, iaculis lectus. Vestibulum lectus diam, dapibus sed porta eget, posuere ac mauris. Suspendisse nec dolor vel purus dignissim dignissim sed sed magna. Sed eu dignissim leo, ut volutpat lacus. Donec gravida ultricies dolor. Suspendisse pharetra egestas tortor, sit amet mattis tellus elementum eget. Integer eget nisl massa. In feugiat nisl ut mi tristique vulputate. Donec bibendum purus gravida massa blandit maximus. In blandit sem a malesuada pharetra. Fusce lectus erat, vulputate et tristique ac, ultricies a ex. + + Duis non nisi rutrum metus maximus fringilla. Cras nibh leo, convallis ut dignissim eget, aliquam sit amet justo. Vivamus condimentum aliquet aliquam. Nulla facilisi. Pellentesque malesuada felis mauris, sed convallis ex convallis vel. Mauris libero nibh, faucibus eget erat at, sagittis consectetur purus. Ut ac massa maximus, vulputate justo lacinia, accumsan dolor. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Mauris eget condimentum dolor. Nunc lacinia, lacus quis blandit aliquet, odio ex aliquet purus, et pretium urna ligula at ipsum. + + Suspendisse sollicitudin rhoncus sem, ut pulvinar nisi porttitor et. Vestibulum vehicula arcu ex, id eleifend felis rhoncus non. Quisque a arcu ullamcorper, fermentum mi in, bibendum libero. Donec dignissim ut purus et porttitor. Suspendisse ac tellus eu arcu condimentum rhoncus. Curabitur cursus blandit vulputate. Duis imperdiet velit tortor, non mollis elit rutrum a. Praesent nibh neque, condimentum id lorem et, fringilla varius mi. Donec eget varius tortor. Vestibulum vehicula leo vel tincidunt scelerisque. Proin laoreet vitae nisi auctor varius. Sed imperdiet tortor justo. Proin gravida vehicula nisl. Suspendisse elit nunc, blandit vel semper ut, tristique quis quam. Vivamus nec bibendum est. Aenean maximus, augue non ornare ornare, dui metus gravida mi, nec lacinia massa massa eu eros. + + Donec faucibus laoreet ipsum ut viverra. Ut molestie, urna nec tincidunt pretium, mauris ipsum consequat velit, mollis aliquam ipsum lorem consequat nisi. Suspendisse eros orci, luctus non scelerisque sit amet, aliquam ac sem. Etiam pellentesque eleifend ligula. Phasellus elementum auctor dui, at venenatis nibh elementum in. Duis venenatis tempus ex sit amet commodo. Fusce ut erat sit amet enim convallis pellentesque quis sit amet nisi. Sed nec ligula bibendum, volutpat dolor sit amet, maximus magna. Nam fermentum volutpat metus vitae tempus. Maecenas tempus iaculis tristique. Aenean a lobortis nisl. In auctor id ex sit amet ultrices. Vivamus at ante nec ex ultricies sagittis. Praesent odio ante, ultricies vel ante sed, mollis laoreet lectus. Aenean sagittis justo eu sapien ullamcorper commodo. + """; + logger.log(org.apache.logging.log4j.Level.INFO, "logLargeStatement, " + logMessage); + } + + @Benchmark + @Fork(value = FORK_COUNT) + @Threads(PARALLEL_THREAD_COUNT) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + @Warmup( + iterations = WARMUP_ITERATIONS, + time = WARMUP_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + @Measurement( + iterations = MEASUREMENT_ITERATIONS, + time = MEASUREMENT_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + public void logWithPlaceholders() { + logger.log( + org.apache.logging.log4j.Level.INFO, + "logWithPlaceholders, Hello {}, {}, {}, {}, {}, {}, {}, {}, {}!", + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9); + } + + @Benchmark + @Fork(value = FORK_COUNT) + @Threads(PARALLEL_THREAD_COUNT) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + @Warmup( + iterations = WARMUP_ITERATIONS, + time = WARMUP_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + @Measurement( + iterations = MEASUREMENT_ITERATIONS, + time = MEASUREMENT_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + public void logWithMarker() { + logger.log(org.apache.logging.log4j.Level.INFO, MARKER, "logWithMarker, Hello world!"); + } + + @Benchmark + @Fork(value = FORK_COUNT) + @Threads(PARALLEL_THREAD_COUNT) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + @Warmup( + iterations = WARMUP_ITERATIONS, + time = WARMUP_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + @Measurement( + iterations = MEASUREMENT_ITERATIONS, + time = MEASUREMENT_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + public void logWithContext() { + ThreadContext.put("user-id", Throwables.USER_1); + logger.log(org.apache.logging.log4j.Level.INFO, "logWithContext, Hello world!"); + ThreadContext.clearAll(); + } + + @Benchmark + @Fork(value = FORK_COUNT) + @Threads(PARALLEL_THREAD_COUNT) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + @Warmup( + iterations = WARMUP_ITERATIONS, + time = WARMUP_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + @Measurement( + iterations = MEASUREMENT_ITERATIONS, + time = MEASUREMENT_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + public void logWithThrowable() { + logger.log(org.apache.logging.log4j.Level.INFO, "logWithThrowable, Hello world!", Throwables.THROWABLE); + } + + @Benchmark + @Fork(value = FORK_COUNT) + @Threads(PARALLEL_THREAD_COUNT) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + @Warmup( + iterations = WARMUP_ITERATIONS, + time = WARMUP_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + @Measurement( + iterations = MEASUREMENT_ITERATIONS, + time = MEASUREMENT_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + public void logWithDeepThrowable() { + logger.log( + org.apache.logging.log4j.Level.INFO, "logWithDeepThrowable, Hello world!", Throwables.DEEP_THROWABLE); + } + + @Benchmark + @Fork(value = FORK_COUNT) + @Threads(PARALLEL_THREAD_COUNT) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + @Warmup( + iterations = WARMUP_ITERATIONS, + time = WARMUP_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + @Measurement( + iterations = MEASUREMENT_ITERATIONS, + time = MEASUREMENT_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + public void logWorstCase() { + + String logMessage = + """ + Lorem ipsum dolor sit amet, {} adipiscing elit. Vivamus aliquam dolor placerat, efficitur erat a, iaculis lectus. Vestibulum lectus diam, dapibus sed porta eget, posuere ac mauris. Suspendisse nec dolor vel purus dignissim dignissim sed sed magna. Sed eu dignissim leo, ut volutpat lacus. Donec gravida ultricies dolor. Suspendisse pharetra egestas tortor, sit amet mattis tellus elementum eget. Integer eget nisl massa. In feugiat nisl ut mi tristique vulputate. Donec bibendum purus gravida massa blandit maximus. In blandit sem a malesuada pharetra. Fusce lectus erat, vulputate et tristique ac, ultricies a ex. + + Duis non nisi rutrum metus maximus fringilla. Cras nibh leo, {} ut dignissim eget, aliquam sit amet justo. Vivamus condimentum aliquet aliquam. Nulla facilisi. Pellentesque malesuada felis mauris, sed convallis ex convallis vel. Mauris libero nibh, faucibus eget erat at, sagittis consectetur purus. Ut ac massa maximus, vulputate justo lacinia, accumsan dolor. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Mauris eget condimentum dolor. Nunc lacinia, lacus quis blandit aliquet, odio ex aliquet purus, et pretium urna ligula at ipsum. + + Suspendisse sollicitudin rhoncus sem, ut pulvinar nisi porttitor et. Vestibulum vehicula arcu ex, id eleifend felis rhoncus non. Quisque a arcu ullamcorper, fermentum mi in, bibendum libero. Donec dignissim ut purus et porttitor. Suspendisse ac tellus eu arcu condimentum rhoncus. Curabitur cursus blandit vulputate. Duis imperdiet velit tortor, non mollis elit rutrum a. Praesent nibh neque, condimentum id lorem et, fringilla varius mi. Donec eget varius tortor. Vestibulum vehicula leo vel tincidunt scelerisque. Proin laoreet vitae nisi auctor varius. Sed imperdiet tortor justo. Proin gravida vehicula nisl. Suspendisse elit nunc, blandit vel semper ut, tristique quis quam. Vivamus nec bibendum est. Aenean maximus, augue non ornare ornare, dui metus gravida mi, nec lacinia massa massa eu eros. + + Donec faucibus laoreet ipsum ut viverra. Ut molestie, urna nec tincidunt pretium, mauris ipsum {} velit, mollis aliquam ipsum lorem consequat nisi. Suspendisse eros orci, luctus non scelerisque sit amet, {} ac sem. Etiam pellentesque eleifend ligula. Phasellus elementum auctor dui, at venenatis nibh elementum in. Duis venenatis tempus ex sit amet commodo. Fusce ut erat sit amet enim convallis pellentesque quis sit amet nisi. Sed nec ligula bibendum, volutpat dolor sit amet, maximus magna. Nam fermentum volutpat metus vitae tempus. Maecenas tempus iaculis tristique. Aenean a lobortis nisl. In auctor id ex sit amet ultrices. Vivamus at ante nec ex ultricies sagittis. Praesent odio ante, ultricies vel ante sed, mollis laoreet lectus. Aenean sagittis justo eu sapien ullamcorper {}. + """; + logger.log( + org.apache.logging.log4j.Level.INFO, + "logLargeStatement, " + logMessage, + new Object(), + Collections.emptyList(), + new BigDecimal("10.1"), + "comodo", + Throwables.DEEP_THROWABLE); + } + + @TearDown(Level.Iteration) + public void tearDown() { + config.tierDown(); + } +} diff --git a/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/log4j2/Log4JConfiguration.java b/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/log4j2/Log4JConfiguration.java new file mode 100644 index 000000000000..5fac5cb438fc --- /dev/null +++ b/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/log4j2/Log4JConfiguration.java @@ -0,0 +1,117 @@ +/* + * 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.logging.benchmark.log4j2; + +import com.swirlds.logging.benchmark.config.Configuration; +import com.swirlds.logging.benchmark.config.Constants; +import com.swirlds.logging.benchmark.util.ConfigManagement; +import com.swirlds.logging.benchmark.util.LogFiles; +import edu.umd.cs.findbugs.annotations.NonNull; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.appender.ConsoleAppender; +import org.apache.logging.log4j.core.config.Configurator; +import org.apache.logging.log4j.core.config.builder.api.AppenderComponentBuilder; +import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilder; +import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory; +import org.apache.logging.log4j.core.config.builder.api.LayoutComponentBuilder; +import org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration; +import org.apache.logging.log4j.spi.LoggerContext; + +/** + * Convenience methods for configuring log4j logger + */ +public class Log4JConfiguration implements Configuration { + + private static final String PATTERN = + (ConfigManagement.formatTimestamp() ? "%d{yyyy-MM-dd HH:mm:ss.SSS}" : "%d{UNIX_MILLIS}") + + " %-5level [%t] %c - %msg - [%marker] %X %n%throwable"; + public static final String CONSOLE_APPENDER_NAME = "console"; + public static final String FILE_APPENDER_NAME = "file"; + + public @NonNull LoggerContext configureConsoleLogging() { + System.clearProperty("log4j2.contextSelector"); + final ConfigurationBuilder builder = ConfigurationBuilderFactory.newConfigurationBuilder(); + builder.setStatusLevel(Level.ERROR); + builder.setConfigurationName("consoleLoggingConfig"); + builder.add(createConsoleAppender(builder)); + builder.add(builder.newRootLogger(Level.DEBUG).add(builder.newAppenderRef(CONSOLE_APPENDER_NAME))); + return create(builder); + } + + public @NonNull LoggerContext configureFileLogging() { + final String logFile = LogFiles.provideLogFilePath(Constants.LOG4J2, Constants.FILE_TYPE); + System.clearProperty("log4j2.contextSelector"); + final ConfigurationBuilder builder = ConfigurationBuilderFactory.newConfigurationBuilder(); + builder.setStatusLevel(Level.DEBUG); + builder.setConfigurationName("fileLoggingConfig"); + builder.add(createFileAppender(builder, logFile)); + builder.add(builder.newRootLogger(Level.DEBUG).add(builder.newAppenderRef(FILE_APPENDER_NAME))); + return create(builder); + } + + public @NonNull LoggerContext configureFileAndConsoleLogging() { + final String logFile = LogFiles.provideLogFilePath(Constants.LOG4J2, Constants.CONSOLE_AND_FILE_TYPE); + System.clearProperty("log4j2.contextSelector"); + final ConfigurationBuilder builder = ConfigurationBuilderFactory.newConfigurationBuilder(); + builder.setStatusLevel(Level.ERROR); + builder.setConfigurationName("fileAndConsoleLoggingConfig"); + builder.add(createFileAppender(builder, logFile)); + builder.add(createConsoleAppender(builder)); + builder.add(builder.newRootLogger(Level.DEBUG) + .add(builder.newAppenderRef(FILE_APPENDER_NAME)) + .add(builder.newAppenderRef(CONSOLE_APPENDER_NAME))); + return create(builder); + } + + private static @NonNull LoggerContext create(final @NonNull ConfigurationBuilder builder) { + final org.apache.logging.log4j.core.config.Configuration configuration = builder.build(); + final org.apache.logging.log4j.core.LoggerContext context = Configurator.initialize(configuration); + LogManager.getFactory().removeContext(context); + return Configurator.initialize(configuration); + } + + private static AppenderComponentBuilder createConsoleAppender( + final @NonNull ConfigurationBuilder builder) { + final LayoutComponentBuilder layoutComponentBuilder = + builder.newLayout("PatternLayout").addAttribute("pattern", PATTERN); + return builder.newAppender(Log4JConfiguration.CONSOLE_APPENDER_NAME, "CONSOLE") + .addAttribute("target", ConsoleAppender.Target.SYSTEM_OUT) + .add(layoutComponentBuilder); + } + + private static AppenderComponentBuilder createFileAppender( + final @NonNull ConfigurationBuilder builder, final @NonNull String path) { + final LayoutComponentBuilder layoutBuilder = + builder.newLayout("PatternLayout").addAttribute("pattern", PATTERN); + return builder.newAppender(Log4JConfiguration.FILE_APPENDER_NAME, "File") + .addAttribute("fileName", path) + .addAttribute("append", true) + .add(layoutBuilder); + } + + @Override + public void tierDown() { + if (ConfigManagement.deleteOutputFiles()) { + LogFiles.deleteFile(LogFiles.provideLogFilePath(Constants.SWIRLDS, Constants.FILE_TYPE)); + LogFiles.deleteFile(LogFiles.provideLogFilePath(Constants.SWIRLDS, Constants.CONSOLE_AND_FILE_TYPE)); + } + if (ConfigManagement.deleteOutputFolder()) { + LogFiles.tryForceDeleteDir(); + } + } +} diff --git a/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/log4j2/Log4JRunner.java b/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/log4j2/Log4JRunner.java new file mode 100644 index 000000000000..664dad04bcae --- /dev/null +++ b/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/log4j2/Log4JRunner.java @@ -0,0 +1,68 @@ +/* + * 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.logging.benchmark.log4j2; + +import com.swirlds.logging.benchmark.util.Throwables; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.MarkerManager; +import org.apache.logging.log4j.ThreadContext; + +public class Log4JRunner implements Runnable { + + private final Logger logger; + + private final Marker marker1 = MarkerManager.getMarker("marker"); + private final Marker marker2 = MarkerManager.getMarker("marker2", marker1); + + public Log4JRunner(Logger logger) { + this.logger = logger; + } + + @Override + public void run() { + logger.log(Level.INFO, "L0, Hello world!"); + logger.log(Level.INFO, "L1, A quick brown fox jumps over the lazy dog."); + logger.log(Level.INFO, "L2, Hello world!", Throwables.THROWABLE); + logger.log(Level.INFO, "L3, Hello {}!", "placeholder"); + + ThreadContext.put("key", "value"); + logger.log(Level.INFO, "L4, Hello world!"); + ThreadContext.clearAll(); + + logger.log(Level.INFO, marker1, "L5, Hello world!"); + + ThreadContext.put("user-id", Throwables.USER_1); + logger.log(Level.INFO, "L6, Hello world!"); + + ThreadContext.put("user-id", Throwables.USER_2); + logger.log(Level.INFO, "L7, Hello {}, {}, {}, {}, {}, {}, {}, {}, {}!", 1, 2, 3, 4, 5, 6, 7, 8, 9); + + ThreadContext.put("key", "value"); + ThreadContext.put("user-id", Throwables.USER_3); + logger.log(Level.INFO, "L8, Hello world!"); + + logger.log(Level.INFO, marker1, "L9, Hello world!"); + logger.log(Level.INFO, marker2, "L10, Hello world!"); + + ThreadContext.put("key", "value"); + logger.log(Level.INFO, marker2, "L11, Hello {}, {}, {}, {}, {}, {}, {}, {}, {}!", 1, 2, 3, 4, 5, 6, 7, 8, 9); + + logger.log(Level.INFO, "L12, Hello world!", Throwables.DEEP_THROWABLE); + } +} diff --git a/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/swirldslog/SwirldsLogBenchmark.java b/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/swirldslog/SwirldsLogBenchmark.java new file mode 100644 index 000000000000..b9a147645f2a --- /dev/null +++ b/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/swirldslog/SwirldsLogBenchmark.java @@ -0,0 +1,99 @@ +/* + * 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.logging.benchmark.swirldslog; + +import static com.swirlds.logging.benchmark.config.Constants.CONSOLE_AND_FILE_TYPE; +import static com.swirlds.logging.benchmark.config.Constants.CONSOLE_TYPE; +import static com.swirlds.logging.benchmark.config.Constants.FILE_TYPE; +import static com.swirlds.logging.benchmark.config.Constants.FORK_COUNT; +import static com.swirlds.logging.benchmark.config.Constants.MEASUREMENT_ITERATIONS; +import static com.swirlds.logging.benchmark.config.Constants.MEASUREMENT_TIME_IN_SECONDS_PER_ITERATION; +import static com.swirlds.logging.benchmark.config.Constants.PARALLEL_THREAD_COUNT; +import static com.swirlds.logging.benchmark.config.Constants.WARMUP_ITERATIONS; +import static com.swirlds.logging.benchmark.config.Constants.WARMUP_TIME_IN_SECONDS_PER_ITERATION; + +import com.swirlds.logging.api.Logger; +import com.swirlds.logging.api.internal.LoggingSystem; +import com.swirlds.logging.benchmark.config.Configuration; +import com.swirlds.logging.benchmark.config.Constants; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +@State(Scope.Benchmark) +public class SwirldsLogBenchmark { + + @Param({CONSOLE_TYPE, FILE_TYPE, CONSOLE_AND_FILE_TYPE}) + public String loggingType; + + private static final String LOGGER_NAME = Constants.SWIRLDS + "Benchmark"; + private Logger logger; + private SwirldsLogRunner logRunner; + private LoggingSystem loggingSystem; + + private Configuration config; + + @Setup(Level.Trial) + public void init() { + config = new SwirldsLogConfiguration(); + if (Objects.equals(loggingType, FILE_TYPE)) { + loggingSystem = config.configureFileLogging(); + } else if (Objects.equals(loggingType, CONSOLE_TYPE)) { + loggingSystem = config.configureConsoleLogging(); + } else if (Objects.equals(loggingType, CONSOLE_AND_FILE_TYPE)) { + loggingSystem = config.configureFileAndConsoleLogging(); + } + logger = loggingSystem.getLogger(LOGGER_NAME); + logRunner = new SwirldsLogRunner(logger); + } + + @Benchmark + @Fork(value = FORK_COUNT) + @Threads(PARALLEL_THREAD_COUNT) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + @Warmup( + iterations = WARMUP_ITERATIONS, + time = WARMUP_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + @Measurement( + iterations = MEASUREMENT_ITERATIONS, + time = MEASUREMENT_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + public void swirldsLogging() { + logRunner.run(); + } + + @TearDown(Level.Trial) + public void tearDown() { + // loggingSystem.stopAndFinalize(); + config.tierDown(); + } +} diff --git a/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/swirldslog/SwirldsLogConfiguration.java b/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/swirldslog/SwirldsLogConfiguration.java new file mode 100644 index 000000000000..9eb36a9c6a0f --- /dev/null +++ b/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/swirldslog/SwirldsLogConfiguration.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.swirlds.logging.benchmark.swirldslog; + +import com.swirlds.config.api.ConfigurationBuilder; +import com.swirlds.logging.api.extensions.handler.LogHandler; +import com.swirlds.logging.api.internal.LoggingSystem; +import com.swirlds.logging.api.internal.configuration.ConfigLevelConverter; +import com.swirlds.logging.api.internal.configuration.MarkerStateConverter; +import com.swirlds.logging.benchmark.config.Configuration; +import com.swirlds.logging.benchmark.config.Constants; +import com.swirlds.logging.benchmark.util.ConfigManagement; +import com.swirlds.logging.benchmark.util.LogFiles; +import com.swirlds.logging.console.ConsoleHandlerFactory; +import com.swirlds.logging.file.FileHandlerFactory; +import edu.umd.cs.findbugs.annotations.NonNull; + +public class SwirldsLogConfiguration implements Configuration { + + private static final FileHandlerFactory FILE_HANDLER_FACTORY = new FileHandlerFactory(); + private static final ConsoleHandlerFactory CONSOLE_HANDLER_FACTORY = new ConsoleHandlerFactory(); + + public @NonNull LoggingSystem configureFileLogging() { + final String logFile = LogFiles.provideLogFilePath(Constants.SWIRLDS, Constants.FILE_TYPE); + final com.swirlds.config.api.Configuration configuration = ConfigurationBuilder.create() + .withConverter(new ConfigLevelConverter()) + .withConverter(new MarkerStateConverter()) + .withValue("logging.level", "trace") + .withValue("logging.handler.file.type", "file") + .withValue("logging.handler.file.active", "true") + .withValue("logging.handler.file.formatTimestamp", ConfigManagement.formatTimestamp() + "") + .withValue("logging.handler.file.level", "trace") + .withValue("logging.handler.file.file", logFile) + .build(); + final LogHandler fileHandler = FILE_HANDLER_FACTORY.create("file", configuration); + LoggingSystem loggingSystem = new LoggingSystem(configuration); + loggingSystem.addHandler(fileHandler); + return loggingSystem; + } + + public @NonNull LoggingSystem configureConsoleLogging() { + final com.swirlds.config.api.Configuration configuration = ConfigurationBuilder.create() + .withConverter(new ConfigLevelConverter()) + .withConverter(new MarkerStateConverter()) + .withValue("logging.level", "trace") + .withValue("logging.handler.console.type", "console") + .withValue("logging.handler.console.active", "true") + .withValue("logging.handler.console.formatTimestamp", ConfigManagement.formatTimestamp() + "") + .withValue("logging.handler.console.level", "trace") + .build(); + final LogHandler consoleHandler = CONSOLE_HANDLER_FACTORY.create("console", configuration); + LoggingSystem loggingSystem = new LoggingSystem(configuration); + loggingSystem.addHandler(consoleHandler); + return loggingSystem; + } + + public @NonNull LoggingSystem configureFileAndConsoleLogging() { + final String logFile = LogFiles.provideLogFilePath(Constants.SWIRLDS, Constants.CONSOLE_AND_FILE_TYPE); + final com.swirlds.config.api.Configuration configuration = ConfigurationBuilder.create() + .withConverter(new ConfigLevelConverter()) + .withConverter(new MarkerStateConverter()) + .withValue("logging.level", "trace") + .withValue("logging.handler.file.type", "file") + .withValue("logging.handler.file.active", "true") + .withValue("logging.handler.file.formatTimestamp", ConfigManagement.formatTimestamp() + "") + .withValue("logging.handler.file.level", "trace") + .withValue("logging.handler.file.file", logFile) + .withValue("logging.handler.console.type", "console") + .withValue("logging.handler.console.active", "true") + .withValue("logging.handler.console.formatTimestamp", ConfigManagement.formatTimestamp() + "") + .withValue("logging.handler.console.level", "trace") + .build(); + final LogHandler fileHandler = FILE_HANDLER_FACTORY.create("file", configuration); + final LogHandler consoleHandler = CONSOLE_HANDLER_FACTORY.create("console", configuration); + LoggingSystem loggingSystem = new LoggingSystem(configuration); + loggingSystem.addHandler(fileHandler); + loggingSystem.addHandler(consoleHandler); + return loggingSystem; + } + + @Override + public void tierDown() { + + if (ConfigManagement.deleteOutputFiles()) { + LogFiles.deleteFile(LogFiles.provideLogFilePath(Constants.SWIRLDS, Constants.FILE_TYPE)); + LogFiles.deleteFile(LogFiles.provideLogFilePath(Constants.SWIRLDS, Constants.CONSOLE_AND_FILE_TYPE)); + } + if (ConfigManagement.deleteOutputFolder()) { + LogFiles.tryForceDeleteDir(); + } + } +} diff --git a/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/swirldslog/SwirldsLogFineGraneBenchmark.java b/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/swirldslog/SwirldsLogFineGraneBenchmark.java new file mode 100644 index 000000000000..ccee0cdc3346 --- /dev/null +++ b/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/swirldslog/SwirldsLogFineGraneBenchmark.java @@ -0,0 +1,275 @@ +/* + * 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.logging.benchmark.swirldslog; + +import static com.swirlds.logging.benchmark.config.Constants.CONSOLE_AND_FILE_TYPE; +import static com.swirlds.logging.benchmark.config.Constants.CONSOLE_TYPE; +import static com.swirlds.logging.benchmark.config.Constants.FILE_TYPE; +import static com.swirlds.logging.benchmark.config.Constants.FORK_COUNT; +import static com.swirlds.logging.benchmark.config.Constants.MEASUREMENT_ITERATIONS; +import static com.swirlds.logging.benchmark.config.Constants.MEASUREMENT_TIME_IN_SECONDS_PER_ITERATION; +import static com.swirlds.logging.benchmark.config.Constants.PARALLEL_THREAD_COUNT; +import static com.swirlds.logging.benchmark.config.Constants.WARMUP_ITERATIONS; +import static com.swirlds.logging.benchmark.config.Constants.WARMUP_TIME_IN_SECONDS_PER_ITERATION; + +import com.swirlds.logging.api.Logger; +import com.swirlds.logging.api.internal.LoggingSystem; +import com.swirlds.logging.benchmark.config.Configuration; +import com.swirlds.logging.benchmark.config.Constants; +import com.swirlds.logging.benchmark.util.Throwables; +import java.math.BigDecimal; +import java.util.Collections; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +@State(Scope.Benchmark) +public class SwirldsLogFineGraneBenchmark { + + @Param({CONSOLE_TYPE, FILE_TYPE, CONSOLE_AND_FILE_TYPE}) + public String loggingType; + + private static final String LOGGER_NAME = Constants.SWIRLDS + "Benchmark"; + private Logger logger; + private LoggingSystem loggingSystem; + private Configuration config; + + @Setup(Level.Trial) + public void init() { + config = new SwirldsLogConfiguration(); + if (Objects.equals(loggingType, FILE_TYPE)) { + loggingSystem = config.configureFileLogging(); + } else if (Objects.equals(loggingType, CONSOLE_TYPE)) { + loggingSystem = config.configureConsoleLogging(); + } else if (Objects.equals(loggingType, CONSOLE_AND_FILE_TYPE)) { + loggingSystem = config.configureFileAndConsoleLogging(); + } + logger = loggingSystem.getLogger(LOGGER_NAME); + } + + @Benchmark + @Fork(value = FORK_COUNT) + @Threads(PARALLEL_THREAD_COUNT) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + @Warmup( + iterations = WARMUP_ITERATIONS, + time = WARMUP_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + @Measurement( + iterations = MEASUREMENT_ITERATIONS, + time = MEASUREMENT_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + public void logSimpleStatement() { + logger.log(com.swirlds.logging.api.Level.INFO, "logSimpleStatement, Hello world!"); + } + + @Benchmark + @Fork(value = FORK_COUNT) + @Threads(PARALLEL_THREAD_COUNT) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + @Warmup( + iterations = WARMUP_ITERATIONS, + time = WARMUP_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + @Measurement( + iterations = MEASUREMENT_ITERATIONS, + time = MEASUREMENT_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + public void logOffStatement() { + logger.log(com.swirlds.logging.api.Level.OFF, "logSimpleStatement, Hello world!"); + } + + @Benchmark + @Fork(value = FORK_COUNT) + @Threads(PARALLEL_THREAD_COUNT) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + @Warmup( + iterations = WARMUP_ITERATIONS, + time = WARMUP_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + @Measurement( + iterations = MEASUREMENT_ITERATIONS, + time = MEASUREMENT_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + public void logLargeStatement() { + + String logMessage = + """ + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus aliquam dolor placerat, efficitur erat a, iaculis lectus. Vestibulum lectus diam, dapibus sed porta eget, posuere ac mauris. Suspendisse nec dolor vel purus dignissim dignissim sed sed magna. Sed eu dignissim leo, ut volutpat lacus. Donec gravida ultricies dolor. Suspendisse pharetra egestas tortor, sit amet mattis tellus elementum eget. Integer eget nisl massa. In feugiat nisl ut mi tristique vulputate. Donec bibendum purus gravida massa blandit maximus. In blandit sem a malesuada pharetra. Fusce lectus erat, vulputate et tristique ac, ultricies a ex. + + Duis non nisi rutrum metus maximus fringilla. Cras nibh leo, convallis ut dignissim eget, aliquam sit amet justo. Vivamus condimentum aliquet aliquam. Nulla facilisi. Pellentesque malesuada felis mauris, sed convallis ex convallis vel. Mauris libero nibh, faucibus eget erat at, sagittis consectetur purus. Ut ac massa maximus, vulputate justo lacinia, accumsan dolor. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Mauris eget condimentum dolor. Nunc lacinia, lacus quis blandit aliquet, odio ex aliquet purus, et pretium urna ligula at ipsum. + + Suspendisse sollicitudin rhoncus sem, ut pulvinar nisi porttitor et. Vestibulum vehicula arcu ex, id eleifend felis rhoncus non. Quisque a arcu ullamcorper, fermentum mi in, bibendum libero. Donec dignissim ut purus et porttitor. Suspendisse ac tellus eu arcu condimentum rhoncus. Curabitur cursus blandit vulputate. Duis imperdiet velit tortor, non mollis elit rutrum a. Praesent nibh neque, condimentum id lorem et, fringilla varius mi. Donec eget varius tortor. Vestibulum vehicula leo vel tincidunt scelerisque. Proin laoreet vitae nisi auctor varius. Sed imperdiet tortor justo. Proin gravida vehicula nisl. Suspendisse elit nunc, blandit vel semper ut, tristique quis quam. Vivamus nec bibendum est. Aenean maximus, augue non ornare ornare, dui metus gravida mi, nec lacinia massa massa eu eros. + + Donec faucibus laoreet ipsum ut viverra. Ut molestie, urna nec tincidunt pretium, mauris ipsum consequat velit, mollis aliquam ipsum lorem consequat nisi. Suspendisse eros orci, luctus non scelerisque sit amet, aliquam ac sem. Etiam pellentesque eleifend ligula. Phasellus elementum auctor dui, at venenatis nibh elementum in. Duis venenatis tempus ex sit amet commodo. Fusce ut erat sit amet enim convallis pellentesque quis sit amet nisi. Sed nec ligula bibendum, volutpat dolor sit amet, maximus magna. Nam fermentum volutpat metus vitae tempus. Maecenas tempus iaculis tristique. Aenean a lobortis nisl. In auctor id ex sit amet ultrices. Vivamus at ante nec ex ultricies sagittis. Praesent odio ante, ultricies vel ante sed, mollis laoreet lectus. Aenean sagittis justo eu sapien ullamcorper commodo. + """; + logger.log(com.swirlds.logging.api.Level.INFO, "logLargeStatement, " + logMessage); + } + + @Benchmark + @Fork(value = FORK_COUNT) + @Threads(PARALLEL_THREAD_COUNT) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + @Warmup( + iterations = WARMUP_ITERATIONS, + time = WARMUP_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + @Measurement( + iterations = MEASUREMENT_ITERATIONS, + time = MEASUREMENT_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + public void logWithPlaceholders() { + logger.log( + com.swirlds.logging.api.Level.INFO, + "logWithPlaceholders, Hello {}, {}, {}, {}, {}, {}, {}, {}, {}!", + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9); + } + + @Benchmark + @Fork(value = FORK_COUNT) + @Threads(PARALLEL_THREAD_COUNT) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + @Warmup( + iterations = WARMUP_ITERATIONS, + time = WARMUP_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + @Measurement( + iterations = MEASUREMENT_ITERATIONS, + time = MEASUREMENT_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + public void logWithMarker() { + logger.withMarker("marker").log(com.swirlds.logging.api.Level.INFO, "logWithMarker, Hello world!"); + } + + @Benchmark + @Fork(value = FORK_COUNT) + @Threads(PARALLEL_THREAD_COUNT) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + @Warmup( + iterations = WARMUP_ITERATIONS, + time = WARMUP_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + @Measurement( + iterations = MEASUREMENT_ITERATIONS, + time = MEASUREMENT_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + public void logWithContext() { + logger.withContext("user-id", Throwables.USER_1) + .log(com.swirlds.logging.api.Level.INFO, "logWithContext, Hello world!"); + } + + @Benchmark + @Fork(value = FORK_COUNT) + @Threads(PARALLEL_THREAD_COUNT) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + @Warmup( + iterations = WARMUP_ITERATIONS, + time = WARMUP_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + @Measurement( + iterations = MEASUREMENT_ITERATIONS, + time = MEASUREMENT_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + public void logWithThrowable() { + logger.log(com.swirlds.logging.api.Level.INFO, "logWithThrowable, Hello world!", Throwables.THROWABLE); + } + + @Benchmark + @Fork(value = FORK_COUNT) + @Threads(PARALLEL_THREAD_COUNT) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + @Warmup( + iterations = WARMUP_ITERATIONS, + time = WARMUP_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + @Measurement( + iterations = MEASUREMENT_ITERATIONS, + time = MEASUREMENT_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + public void logWithDeepThrowable() { + logger.log(com.swirlds.logging.api.Level.INFO, "logWithDeepThrowable, Hello world!", Throwables.DEEP_THROWABLE); + } + + @Benchmark + @Fork(value = FORK_COUNT) + @Threads(PARALLEL_THREAD_COUNT) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + @Warmup( + iterations = WARMUP_ITERATIONS, + time = WARMUP_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + @Measurement( + iterations = MEASUREMENT_ITERATIONS, + time = MEASUREMENT_TIME_IN_SECONDS_PER_ITERATION, + timeUnit = TimeUnit.MILLISECONDS) + public void logWorstCase() { + + String logMessage = + """ + Lorem ipsum dolor sit amet, {} adipiscing elit. Vivamus aliquam dolor placerat, efficitur erat a, iaculis lectus. Vestibulum lectus diam, dapibus sed porta eget, posuere ac mauris. Suspendisse nec dolor vel purus dignissim dignissim sed sed magna. Sed eu dignissim leo, ut volutpat lacus. Donec gravida ultricies dolor. Suspendisse pharetra egestas tortor, sit amet mattis tellus elementum eget. Integer eget nisl massa. In feugiat nisl ut mi tristique vulputate. Donec bibendum purus gravida massa blandit maximus. In blandit sem a malesuada pharetra. Fusce lectus erat, vulputate et tristique ac, ultricies a ex. + + Duis non nisi rutrum metus maximus fringilla. Cras nibh leo, {} ut dignissim eget, aliquam sit amet justo. Vivamus condimentum aliquet aliquam. Nulla facilisi. Pellentesque malesuada felis mauris, sed convallis ex convallis vel. Mauris libero nibh, faucibus eget erat at, sagittis consectetur purus. Ut ac massa maximus, vulputate justo lacinia, accumsan dolor. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Mauris eget condimentum dolor. Nunc lacinia, lacus quis blandit aliquet, odio ex aliquet purus, et pretium urna ligula at ipsum. + + Suspendisse sollicitudin rhoncus sem, ut pulvinar nisi porttitor et. Vestibulum vehicula arcu ex, id eleifend felis rhoncus non. Quisque a arcu ullamcorper, fermentum mi in, bibendum libero. Donec dignissim ut purus et porttitor. Suspendisse ac tellus eu arcu condimentum rhoncus. Curabitur cursus blandit vulputate. Duis imperdiet velit tortor, non mollis elit rutrum a. Praesent nibh neque, condimentum id lorem et, fringilla varius mi. Donec eget varius tortor. Vestibulum vehicula leo vel tincidunt scelerisque. Proin laoreet vitae nisi auctor varius. Sed imperdiet tortor justo. Proin gravida vehicula nisl. Suspendisse elit nunc, blandit vel semper ut, tristique quis quam. Vivamus nec bibendum est. Aenean maximus, augue non ornare ornare, dui metus gravida mi, nec lacinia massa massa eu eros. + + Donec faucibus laoreet ipsum ut viverra. Ut molestie, urna nec tincidunt pretium, mauris ipsum {} velit, mollis aliquam ipsum lorem consequat nisi. Suspendisse eros orci, luctus non scelerisque sit amet, {} ac sem. Etiam pellentesque eleifend ligula. Phasellus elementum auctor dui, at venenatis nibh elementum in. Duis venenatis tempus ex sit amet commodo. Fusce ut erat sit amet enim convallis pellentesque quis sit amet nisi. Sed nec ligula bibendum, volutpat dolor sit amet, maximus magna. Nam fermentum volutpat metus vitae tempus. Maecenas tempus iaculis tristique. Aenean a lobortis nisl. In auctor id ex sit amet ultrices. Vivamus at ante nec ex ultricies sagittis. Praesent odio ante, ultricies vel ante sed, mollis laoreet lectus. Aenean sagittis justo eu sapien ullamcorper {}. + """; + logger.log( + com.swirlds.logging.api.Level.INFO, + "logLargeStatement, " + logMessage, + new Object(), + Collections.emptyList(), + new BigDecimal("10.1"), + "comodo", + Throwables.DEEP_THROWABLE); + } + + @TearDown(Level.Trial) + public void tearDown() { + config.tierDown(); + } +} diff --git a/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/swirldslog/SwirldsLogRunner.java b/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/swirldslog/SwirldsLogRunner.java new file mode 100644 index 000000000000..ca5add1e04ff --- /dev/null +++ b/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/swirldslog/SwirldsLogRunner.java @@ -0,0 +1,53 @@ +/* + * 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.logging.benchmark.swirldslog; + +import com.swirlds.logging.api.Level; +import com.swirlds.logging.api.Logger; +import com.swirlds.logging.benchmark.util.Throwables; + +public class SwirldsLogRunner implements Runnable { + + private final Logger logger; + + public SwirldsLogRunner(Logger logger) { + this.logger = logger; + } + + @Override + public void run() { + logger.log(Level.INFO, "L0, Hello world!"); + logger.log(Level.INFO, "L1, A quick brown fox jumps over the lazy dog."); + logger.log(Level.INFO, "L2, Hello world!", Throwables.THROWABLE); + logger.log(Level.INFO, "L3, Hello {}!", "placeholder"); + logger.withContext("key", "value").log(Level.INFO, "L4, Hello world!"); + logger.withMarker("marker").log(Level.INFO, "L5, Hello world!"); + logger.withContext("user-id", Throwables.USER_1).log(Level.INFO, "L6, Hello world!"); + logger.withContext("user-id", Throwables.USER_2) + .log(Level.INFO, "L7, Hello {}, {}, {}, {}, {}, {}, {}, {}, {}!", 1, 2, 3, 4, 5, 6, 7, 8, 9); + logger.withContext("user-id", Throwables.USER_3) + .withContext("key", "value") + .log(Level.INFO, "L8, Hello world!"); + logger.withMarker("marker").log(Level.INFO, "L9, Hello world!"); + logger.withMarker("marker1").withMarker("marker2").log(Level.INFO, "L10, Hello world!"); + logger.withContext("key", "value") + .withMarker("marker1") + .withMarker("marker2") + .log(Level.INFO, "L11, Hello {}, {}, {}, {}, {}, {}, {}, {}, {}!", 1, 2, 3, 4, 5, 6, 7, 8, 9); + logger.log(Level.INFO, "L12, Hello world!", Throwables.DEEP_THROWABLE); + } +} diff --git a/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/util/ConfigManagement.java b/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/util/ConfigManagement.java new file mode 100644 index 000000000000..e1838defcf44 --- /dev/null +++ b/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/util/ConfigManagement.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.swirlds.logging.benchmark.util; + +import com.swirlds.logging.benchmark.config.Constants; + +/** + * Utility class for configuring benchmark handling of + *

  • timestamp formatting + *
  • outputFile deletion + */ +public class ConfigManagement { + + private ConfigManagement() {} + + /** + * Reads the value from ENABLE_TIME_FORMATTING system variable or returns {@link Constants#ENABLE_TIME_FORMATTING} + */ + public static boolean formatTimestamp() { + return getEnvOrElse(Constants.ENABLE_TIME_FORMATTING_ENV, Constants.ENABLE_TIME_FORMATTING); + } + + /** + * Reads the value from DELETE_OUTPUT_FILES system variable or returns {@link Constants#DELETE_OUTPUT_FILES} + */ + public static boolean deleteOutputFiles() { + return getEnvOrElse(Constants.DELETE_OUTPUT_FILES_ENV, Constants.DELETE_OUTPUT_FILES); + } + + /** + * Reads the value from DELETE_OUTPUT_FOLDER system variable or returns {@link Constants#DELETE_OUTPUT_FOLDER} + */ + public static boolean deleteOutputFolder() { + return getEnvOrElse(Constants.DELETE_OUTPUT_FOLDER_ENV, Constants.DELETE_OUTPUT_FOLDER); + } + + private static boolean getEnvOrElse(final String deleteOutputFilesEnv, final boolean deleteOutputFiles) { + try { + final String enableTimestampFormatting = System.getenv(deleteOutputFilesEnv); + return enableTimestampFormatting != null + ? Boolean.parseBoolean(enableTimestampFormatting) + : deleteOutputFiles; + } catch (Exception e) { + return deleteOutputFiles; + } + } +} diff --git a/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/util/LogFiles.java b/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/util/LogFiles.java new file mode 100644 index 000000000000..c3eeb90447bc --- /dev/null +++ b/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/util/LogFiles.java @@ -0,0 +1,80 @@ +/* + * 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.logging.benchmark.util; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.stream.Stream; + +/** + * Convenience methods for handling logFiles pre and after benchmark runs + */ +public class LogFiles { + + public static final String LOGGING_FOLDER = "logging-out"; + + private LogFiles() {} + + /** + * Provides the path to the log file based on the implementationName of the logging system under benchmark + * {@code implementationName} and the type of benchmark {@code type}. Previously deleting the file if exists in the + * FS. + */ + @NonNull + public static String provideLogFilePath(final @NonNull String implementationName, final @NonNull String type) { + final String path = getPath(implementationName, type); + deleteFile(path); + return path; + } + + /** + * Provides the path to the log file based on the implementation of the logging system under benchmark + * {@code implementationName} and the type of benchmark {@code type} + */ + @NonNull + public static String getPath(final @NonNull String implementation, final @NonNull String type) { + final long pid = ProcessHandle.current().pid(); + return LOGGING_FOLDER + File.separator + "benchmark-" + implementation + "-" + pid + "-" + type + ".log"; + } + + /** + * Deletes the file + */ + public static void deleteFile(final @NonNull String logFile) { + try { + Files.deleteIfExists(Path.of(logFile)); + } catch (IOException e) { + throw new RuntimeException("Can not delete old log file", e); + } + } + + public static void tryForceDeleteDir() { + final Path path = Path.of(LOGGING_FOLDER); + try (Stream walk = Files.walk(path)) { + walk.sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .map(File::getAbsolutePath) + .forEach(LogFiles::deleteFile); + } catch (IOException e) { + // do nothing + } + } +} diff --git a/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/util/Throwables.java b/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/util/Throwables.java new file mode 100644 index 000000000000..6320f33d2088 --- /dev/null +++ b/platform-sdk/swirlds-logging/src/jmh/java/com/swirlds/logging/benchmark/util/Throwables.java @@ -0,0 +1,67 @@ +/* + * 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.logging.benchmark.util; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.UUID; + +/** + * Convenience methods for creating exceptions with stacktrace as big as requested + */ +public class Throwables { + public static final Throwable THROWABLE = createThrowable(); + + public static final Throwable DEEP_THROWABLE = createThrowableWithDeepCause(20, 20); + + public static final String USER_1 = UUID.randomUUID().toString(); + public static final String USER_2 = UUID.randomUUID().toString(); + public static final String USER_3 = UUID.randomUUID().toString(); + + private Throwables() {} + + /** + * Creates a throwable with a {@code myDepth} stacktrace call and with cause having {@code causeDepth} nested + * exceptions. + */ + public static @NonNull Throwable createThrowableWithDeepCause(final int myDepth, final int causeDepth) { + if (myDepth > 0) { + return createThrowableWithDeepCause(myDepth - 1, causeDepth); + } + try { + throw createDeepThrowable(causeDepth); + } catch (Throwable t) { + return new RuntimeException("test", t); + } + } + + /** + * Creates a throwable with cause having {@code depth} nested exceptions. + */ + public static @NonNull Throwable createDeepThrowable(final int depth) { + if (depth <= 0) { + return new RuntimeException("test"); + } + return createDeepThrowable(depth - 1); + } + + /** + * Creates a throwable. + */ + public static @NonNull Throwable createThrowable() { + return new RuntimeException("test"); + } +} diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/Level.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/Level.java index aaee04640080..ee218cf31bf0 100644 --- a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/Level.java +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/Level.java @@ -84,4 +84,22 @@ public static Level valueOfOrElse(@Nullable final String value, @NonNull final L return defaultLevel; } } + + public String nameWithFixedSize() { + if (this == OFF) { + return "OFF "; + } else if (this == ERROR) { + return "ERROR"; + } else if (this == WARN) { + return "WARN "; + } else if (this == INFO) { + return "INFO "; + } else if (this == DEBUG) { + return "DEBUG"; + } else if (this == TRACE) { + return "TRACE"; + } else { + return " "; + } + } } diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/extensions/event/LogEvent.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/extensions/event/LogEvent.java index 94c3146b0041..a1065f3723db 100644 --- a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/extensions/event/LogEvent.java +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/extensions/event/LogEvent.java @@ -57,7 +57,6 @@ public interface LogEvent { * * @return the timestamp */ - @NonNull long timestamp(); /** diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/extensions/handler/AbstractAsyncHandler.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/extensions/handler/AbstractAsyncHandler.java new file mode 100644 index 000000000000..65ffc0b109a2 --- /dev/null +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/extensions/handler/AbstractAsyncHandler.java @@ -0,0 +1,81 @@ +/* + * 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.logging.api.extensions.handler; + +import com.swirlds.config.api.Configuration; +import com.swirlds.logging.api.extensions.event.LogEvent; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; + +public abstract class AbstractAsyncHandler extends AbstractLogHandler { + /** + * The executor service that is used to handle log events asynchronously. + */ + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + + /** + * True if the log handler is stopped, false otherwise. + */ + private final AtomicBoolean stopped = new AtomicBoolean(false); + + /** + * Creates a new log handler. + * + * @param configKey the configuration key + * @param configuration the configuration + */ + public AbstractAsyncHandler(@NonNull final String configKey, @NonNull final Configuration configuration) { + super(configKey, configuration); + } + + @Override + public void accept(@NonNull LogEvent event) { + if (stopped.get()) { + EMERGENCY_LOGGER.log(event); + return; + } + executorService.submit(() -> { + if (stopped.get()) { + EMERGENCY_LOGGER.log(event); + } else { + handleEvent(event); + } + }); + } + + /** + * Handles the log event asynchronously. + * + * @param event the log event + */ + protected abstract void handleEvent(@NonNull LogEvent event); + + @Override + public void stopAndFinalize() { + if (!stopped.getAndSet(true)) { + executorService.shutdown(); + handleStopAndFinalize(); + } + } + + /** + * Implementations can override this method to handle the stop and finalize of the handler. + */ + protected void handleStopAndFinalize() {} +} diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/extensions/handler/AbstractSyncedHandler.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/extensions/handler/AbstractSyncedHandler.java index a064d2c198f8..f9640ec6a965 100644 --- a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/extensions/handler/AbstractSyncedHandler.java +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/extensions/handler/AbstractSyncedHandler.java @@ -19,8 +19,6 @@ import com.swirlds.config.api.Configuration; import com.swirlds.logging.api.extensions.event.LogEvent; import edu.umd.cs.findbugs.annotations.NonNull; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; /** * An abstract log handler that synchronizes the handling of log events. This handler is used as a base class for all @@ -29,11 +27,6 @@ */ public abstract class AbstractSyncedHandler extends AbstractLogHandler { - /** - * The write lock that is used to synchronize the handling of log events. - */ - private final Lock writeLock = new ReentrantLock(); - /** * True if the log handler is stopped, false otherwise. */ @@ -51,17 +44,12 @@ public AbstractSyncedHandler(@NonNull final String configKey, @NonNull final Con @Override public final void accept(@NonNull LogEvent event) { - try { - writeLock.lock(); - if (stopped) { - // FUTURE: is the emergency logger really the best idea in that case? If multiple handlers are stopped, - // the emergency logger will be called multiple times. - EMERGENCY_LOGGER.log(event); - } else { - handleEvent(event); - } - } finally { - writeLock.unlock(); + if (stopped) { + // FUTURE: is the emergency logger really the best idea in that case? If multiple handlers are stopped, + // the emergency logger will be called multiple times. + EMERGENCY_LOGGER.log(event); + } else { + handleEvent(event); } } @@ -74,13 +62,8 @@ public final void accept(@NonNull LogEvent event) { @Override public final void stopAndFinalize() { - try { - writeLock.lock(); - stopped = true; - handleStopAndFinalize(); - } finally { - writeLock.unlock(); - } + stopped = true; + handleStopAndFinalize(); } /** diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/extensions/handler/LogHandlerFactory.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/extensions/handler/LogHandlerFactory.java index aadbe32decee..845bda11a939 100644 --- a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/extensions/handler/LogHandlerFactory.java +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/extensions/handler/LogHandlerFactory.java @@ -30,12 +30,12 @@ public interface LogHandlerFactory { /** * Creates a new log handler. * - * @param configKey the configuration key for the log handler + * @param handlerName the configuration key for the log handler * @param configuration the configuration * @return the log handler */ @NonNull - LogHandler create(@NonNull String configKey, @NonNull Configuration configuration); + LogHandler create(@NonNull String handlerName, @NonNull Configuration configuration); /** * Name used to reference a handler type in the configuration. If the name is "console", then the configuration diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/emergency/EmergencyLoggerImpl.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/emergency/EmergencyLoggerImpl.java index e55c8f01aab7..690153bbf352 100644 --- a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/emergency/EmergencyLoggerImpl.java +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/emergency/EmergencyLoggerImpl.java @@ -21,7 +21,7 @@ import com.swirlds.logging.api.extensions.event.LogEvent; import com.swirlds.logging.api.extensions.event.LogEventFactory; import com.swirlds.logging.api.internal.event.SimpleLogEventFactory; -import com.swirlds.logging.api.internal.format.LineBasedFormat; +import com.swirlds.logging.api.internal.format.FormattedLinePrinter; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.io.PrintStream; @@ -42,17 +42,18 @@ * The logger is defined as a singleton. */ public class EmergencyLoggerImpl implements EmergencyLogger { + private static class InstanceHolder { + /** + * The singleton instance of the logger. + */ + private static final EmergencyLoggerImpl INSTANCE = new EmergencyLoggerImpl(); + } /** * The name of the emergency logger. */ private static final String EMERGENCY_LOGGER_NAME = "EMERGENCY-LOGGER"; - /** - * The message that is used when the message is undefined. - */ - private static final String UNDEFINED_MESSAGE = "UNDEFINED-MESSAGE"; - /** * The size of the queue that is used to store the log events. */ @@ -63,11 +64,6 @@ public class EmergencyLoggerImpl implements EmergencyLogger { */ private static final String LEVEL_PROPERTY_NAME = "com.swirlds.logging.emergency.level"; - /** - * The singleton instance of the logger. - */ - private static final EmergencyLoggerImpl INSTANCE = new EmergencyLoggerImpl(); - public static final Level DEFAULT_LEVEL = Level.DEBUG; /** @@ -98,6 +94,8 @@ public class EmergencyLoggerImpl implements EmergencyLogger { private final Lock handleLock; + private final AtomicReference lineBasedFormat = new AtomicReference<>(); + /** * Creates the singleton instance of the logger. */ @@ -251,7 +249,7 @@ private void handle(@NonNull final LogEvent logEvent) { if (printStream != null) { handleLock.lock(); try { - LineBasedFormat.print(printStream, logEvent); + getLineBasedFormat().print(printStream, logEvent); } finally { handleLock.unlock(); } @@ -273,6 +271,13 @@ private void handle(@NonNull final LogEvent logEvent) { } } + private FormattedLinePrinter getLineBasedFormat() { + if (lineBasedFormat.get() == null) { + lineBasedFormat.compareAndSet(null, new FormattedLinePrinter(false)); + } + return lineBasedFormat.get(); + } + /** * Returns the list of logged events and clears the list. * @@ -292,6 +297,6 @@ public List publishLoggedEvents() { */ @NonNull public static EmergencyLoggerImpl getInstance() { - return INSTANCE; + return InstanceHolder.INSTANCE; } } diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/event/MutableLogEvent.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/event/MutableLogEvent.java index a1b9df14861f..f4845feb93f2 100644 --- a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/event/MutableLogEvent.java +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/event/MutableLogEvent.java @@ -141,7 +141,7 @@ public void update( @NonNull final Level level, @NonNull final String loggerName, @NonNull final String threadName, - @NonNull final long timestamp, + final long timestamp, @NonNull final LogMessage message, @Nullable final Throwable throwable, @Nullable final Marker marker, @@ -175,7 +175,6 @@ public String threadName() { } @Override - @NonNull public long timestamp() { return timestamp; } diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/event/ParameterizedLogMessage.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/event/ParameterizedLogMessage.java index 55c7e10e028e..c7c7bbda75a7 100644 --- a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/event/ParameterizedLogMessage.java +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/event/ParameterizedLogMessage.java @@ -25,21 +25,41 @@ * of the {} placeholders. *

    * The implementation is copied from slf4j for first tests. need to be replaced in future. SLF4J is using MIT license - * (https://github.com/qos-ch/slf4j/blob/master/LICENSE.txt). Based on that we can use it in our project for now + * (...). Based on that we can use it in our project for now * - * @param messagePattern the message pattern - * @param args the arguments * @see LogMessage */ -public record ParameterizedLogMessage(@Nullable String messagePattern, @Nullable Object... args) implements LogMessage { +public class ParameterizedLogMessage implements LogMessage { - static final char DELIM_START = '{'; - static final String DELIM_STR = "{}"; + private static final char DELIM_START = '{'; + private static final String DELIM_STR = "{}"; private static final char ESCAPE_CHAR = '\\'; + private final String messagePattern; + + private final Object[] args; + + private volatile String message = null; + + /** + * @param messagePattern the message pattern + * @param args the arguments + */ + public ParameterizedLogMessage(final @NonNull String messagePattern, final @NonNull Object... args) { + this.messagePattern = messagePattern; + this.args = args; + } + @NonNull @Override public String getMessage() { + if (message == null) { + message = createMessage(); + } + return message; + } + + private @NonNull String createMessage() { if (messagePattern == null) { return ""; } diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/format/EpochFormatUtils.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/format/EpochFormatUtils.java new file mode 100644 index 000000000000..6b4385459ac0 --- /dev/null +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/format/EpochFormatUtils.java @@ -0,0 +1,85 @@ +/* + * 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.logging.api.internal.format; + +import static java.time.ZoneOffset.UTC; + +import com.swirlds.logging.api.Level; +import com.swirlds.logging.api.extensions.emergency.EmergencyLogger; +import com.swirlds.logging.api.extensions.emergency.EmergencyLoggerProvider; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.time.Instant; +import java.time.format.DateTimeFormatter; + +/** + * Utility class to help formatting epoc milliseconds like those coming from ({@link System#currentTimeMillis()}) to a + * String representation matching {@link DateTimeFormatter#ISO_LOCAL_DATE_TIME} + */ +public class EpochFormatUtils { + + /** + * The emergency logger. + */ + private static final EmergencyLogger EMERGENCY_LOGGER = EmergencyLoggerProvider.getEmergencyLogger(); + + /** + * Space filler values, so we can return a fixed size string + */ + private static final String[] FILLERS = prepareFillers(); + + /** + * The formatter for the timestamp. + */ + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME.withZone(UTC); + + private static final String BROKEN_TIMESTAMP = "BROKEN-TIMESTAMP "; + public static final int DATE_FIELD_MAX_SIZE = 26; + + private EpochFormatUtils() {} + + /** + * Returns the String representation matching {@link DateTimeFormatter#ISO_LOCAL_DATE_TIME} for the epoc value + * {@code timestamp} + */ + public static @NonNull String timestampAsString(final long timestamp) { + try { + final StringBuilder sb = new StringBuilder(DATE_FIELD_MAX_SIZE); + sb.append(FORMATTER.format(Instant.ofEpochMilli(timestamp))); + sb.append(getFiller(DATE_FIELD_MAX_SIZE - sb.length())); + return sb.toString(); + } catch (final Throwable e) { + EMERGENCY_LOGGER.log(Level.ERROR, "Failed to format instant", e); + return BROKEN_TIMESTAMP; + } + } + + private static @NonNull String getFiller(final int length) { + if (length < 0 || length > DATE_FIELD_MAX_SIZE) { + throw new IllegalArgumentException("Unsupported length: " + length); + } + return FILLERS[length]; + } + + private static String[] prepareFillers() { + final String[] fillers = new String[DATE_FIELD_MAX_SIZE + 1]; + fillers[0] = ""; + for (int i = 1; i <= DATE_FIELD_MAX_SIZE; i++) { + fillers[i] = " ".repeat(i); + } + return fillers; + } +} diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/format/FormattedLinePrinter.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/format/FormattedLinePrinter.java new file mode 100644 index 000000000000..4fc3d73e6fb6 --- /dev/null +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/format/FormattedLinePrinter.java @@ -0,0 +1,178 @@ +/* + * 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.swirlds.logging.api.internal.format; + +import static java.util.Objects.requireNonNullElse; + +import com.swirlds.config.api.Configuration; +import com.swirlds.logging.api.Level; +import com.swirlds.logging.api.Marker; +import com.swirlds.logging.api.extensions.emergency.EmergencyLogger; +import com.swirlds.logging.api.extensions.emergency.EmergencyLoggerProvider; +import com.swirlds.logging.api.extensions.event.LogEvent; +import com.swirlds.logging.api.extensions.event.LogMessage; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Map; +import java.util.Objects; + +/** + * Formats a {@link LogEvent} as a {@link String} and prints it to a given {@link Appendable} + */ +public class FormattedLinePrinter { + + private static final String THREAD_SUFFIX = "UNDEFINED-THREAD"; + private static final String LOGGER_SUFFIX = "UNDEFINED-LOGGER"; + private static final String UNDEFINED_MESSAGE = "UNDEFINED-MESSAGE"; + private static final String BROKEN_MESSAGE = "BROKEN-MESSAGE"; + /** + * The emergency logger. + */ + private static final EmergencyLogger EMERGENCY_LOGGER = EmergencyLoggerProvider.getEmergencyLogger(); + + /** + * Defines whether timestamps should be formatted as string or raw epoc values. + */ + private final boolean formatTimestamp; + + /** + * Creates a format + * + * @param formatTimestamp if true, timestamps will be converted to a human-readable format defined by + * {@link EpochFormatUtils} + */ + public FormattedLinePrinter(boolean formatTimestamp) { + this.formatTimestamp = formatTimestamp; + } + + /** + * Formats a {@link LogEvent} as a {@link String} and prints it to a given {@link Appendable} + * + * @param appendable Non-null appendable. Destination to write into. + * @param event Non-null event to write. + */ + public void print(@NonNull final Appendable appendable, @NonNull final LogEvent event) { + if (appendable == null) { + EMERGENCY_LOGGER.logNPE("printer"); + return; + } + if (event == null) { + EMERGENCY_LOGGER.logNPE("event"); + return; + } + try { + if (formatTimestamp) { + appendable.append(EpochFormatUtils.timestampAsString(event.timestamp())); + } else { + appendable.append(Long.toString(event.timestamp())); + } + appendable.append(' '); + appendable.append(asString(event.level())); + appendable.append(" ["); + appendable.append(requireNonNullElse(event.threadName(), THREAD_SUFFIX)); + appendable.append("] "); + appendable.append(requireNonNullElse(event.loggerName(), LOGGER_SUFFIX)); + appendable.append(" - "); + appendable.append(asString(event.message())); + + Marker marker = event.marker(); + if (marker != null) { + appendable.append(" - ["); + appendable.append(asString(marker)); + appendable.append("]"); + } + + final Map context = event.context(); + if (context != null && !context.isEmpty()) { + appendable.append(" - "); + appendable.append(context.toString()); + } + appendable.append(System.lineSeparator()); + + Throwable throwable = event.throwable(); + if (throwable != null) { + StackTracePrinter.print(appendable, throwable); + } + } catch (final Throwable e) { + EMERGENCY_LOGGER.log(Level.ERROR, "Failed to format and print event", e); + } + } + + /** + * Converts the given {@link Level} object to a string. + * + * @param level The level + * @return The string + */ + private static String asString(@Nullable final Level level) { + if (level == null) { + EMERGENCY_LOGGER.logNPE("level"); + return "NO_LV"; // Must be 5 chars long to fit in pattern + } else { + return level.nameWithFixedSize(); + } + } + + /** + * Converts the given object to a string. + * + * @param message The message + * @return The string + */ + private static String asString(@Nullable final LogMessage message) { + if (message == null) { + EMERGENCY_LOGGER.logNPE("message"); + return UNDEFINED_MESSAGE; + } else { + try { + return message.getMessage(); + } catch (final Throwable e) { + EMERGENCY_LOGGER.log(Level.ERROR, "Failed to format message", e); + return BROKEN_MESSAGE; + } + } + } + + /** + * Converts the given object to a string. + * + * @param marker The marker + * @return The string + */ + private static String asString(@Nullable final Marker marker) { + if (marker == null) { + EMERGENCY_LOGGER.logNPE("marker"); + return "null"; + } else { + return String.join(", ", marker.getAllMarkerNames()); + } + } + + /** + * Creates in instance of {@link FormattedLinePrinter} + * + * @throws NullPointerException if any of the arguments is {@code null} + */ + public static @NonNull FormattedLinePrinter createForHandler( + @NonNull final String handlerName, @NonNull final Configuration configuration) { + Objects.requireNonNull(handlerName, "handlerName must not be null"); + Objects.requireNonNull(configuration, "configuration must not be null"); + final String formatTimestampKey = "logging.handler." + handlerName + ".formatTimestamp"; + final Boolean formatTimestamp = configuration.getValue(formatTimestampKey, Boolean.class, true); + return new FormattedLinePrinter(formatTimestamp != null && formatTimestamp); + } +} diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/format/LineBasedFormat.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/format/LineBasedFormat.java deleted file mode 100644 index 2787e259fe56..000000000000 --- a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/format/LineBasedFormat.java +++ /dev/null @@ -1,174 +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.swirlds.logging.api.internal.format; - -import com.swirlds.logging.api.Level; -import com.swirlds.logging.api.Marker; -import com.swirlds.logging.api.extensions.emergency.EmergencyLogger; -import com.swirlds.logging.api.extensions.emergency.EmergencyLoggerProvider; -import com.swirlds.logging.api.extensions.event.LogEvent; -import com.swirlds.logging.api.extensions.event.LogMessage; -import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.Nullable; -import java.time.Instant; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.util.Map; - -/** - * A utility class that formats a {@link LogEvent} as a line based format. - */ -public class LineBasedFormat { - - /** - * The emergency logger. - */ - private static final EmergencyLogger EMERGENCY_LOGGER = EmergencyLoggerProvider.getEmergencyLogger(); - - /** - * The formatter for the timestamp. - */ - private static final DateTimeFormatter FORMATTER = - DateTimeFormatter.ISO_LOCAL_DATE_TIME.withZone(ZoneId.systemDefault()); - - /** - * Converts the given object to a string. If the object is {@code null}, the given default value is used. - * - * @param event - */ - public static void print(@NonNull final Appendable writer, @NonNull final LogEvent event) { - if (writer == null) { - EMERGENCY_LOGGER.logNPE("printer"); - return; - } - if (event == null) { - EMERGENCY_LOGGER.logNPE("event"); - return; - } - try { - writer.append(timestampAsString(event.timestamp())); - writer.append(' '); - writer.append(asString(event.level())); - writer.append(' '); - writer.append('['); - writer.append(asString(event.threadName(), "THREAD")); - writer.append(']'); - writer.append(' '); - writer.append(asString(event.loggerName(), "LOGGER")); - writer.append(" - "); - writer.append(asString(event.message())); - - Marker marker = event.marker(); - if (marker != null) { - writer.append(" - [M:"); - writer.append(asString(marker)); - writer.append("]"); - } - - final Map context = event.context(); - if (context != null && !context.isEmpty()) { - writer.append(" - C:"); - writer.append(context.toString()); - } - writer.append(System.lineSeparator()); - - Throwable throwable = event.throwable(); - if (throwable != null) { - StackTracePrinter.print(writer, throwable); - } - } catch (final Throwable e) { - EMERGENCY_LOGGER.log(Level.ERROR, "Failed to format and print event", e); - } - } - - /** - * Converts the given object to a string. - * - * @param str The string - * @param suffix The suffix that is used if the string is {@code null} - * @return The string - */ - private static String asString(String str, String suffix) { - if (str == null) { - return "UNDEFINED-" + suffix; - } else { - return str; - } - } - - /** - * Converts the given object to a string. - * - * @param level The level - * @return The string - */ - private static String asString(Level level) { - if (level == null) { - return "UNDEFINED"; - } else { - return "%-5s".formatted(level.name()); - } - } - - /** - * Converts the given object to a string. - * - * @param message The message - * @return The string - */ - private static String asString(LogMessage message) { - if (message == null) { - return "UNDEFINED-MESSAGE"; - } else { - try { - return message.getMessage(); - } catch (final Throwable e) { - EMERGENCY_LOGGER.log(Level.ERROR, "Failed to format message", e); - return "BROKEN-MESSAGE"; - } - } - } - - /** - * Converts the given object to a string. - * - * @param timestamp The timestamp - * @return The string - */ - private static String timestampAsString(long timestamp) { - try { - return "%-26s".formatted(FORMATTER.format(Instant.ofEpochMilli(timestamp))); - } catch (final Throwable e) { - EMERGENCY_LOGGER.log(Level.ERROR, "Failed to format instant", e); - return "BROKEN-TIMESTAMP "; - } - } - - /** - * Converts the given object to a string. - * - * @param marker The marker - * @return The string - */ - private static String asString(@Nullable final Marker marker) { - if (marker == null) { - return "null"; - } else { - return String.join(", ", marker.getAllMarkerNames()); - } - } -} diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/format/StackTracePrinter.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/format/StackTracePrinter.java index 5d86c9ab7234..4eb6ee4f25e1 100644 --- a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/format/StackTracePrinter.java +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/format/StackTracePrinter.java @@ -36,6 +36,8 @@ public class StackTracePrinter { */ private static final EmergencyLogger EMERGENCY_LOGGER = EmergencyLoggerProvider.getEmergencyLogger(); + private static final int MAX_STACK_TRACE_DEPTH = -1; + /** * Prints the stack trace of a throwable to a provided Appendable writer. * Avoids printing circular references and handles already printed traces. @@ -52,7 +54,80 @@ private static void print( @NonNull final Set alreadyPrinted, @NonNull final StackTraceElement[] enclosingTrace) throws IOException { - // Method implementation + if (writer == null) { + EMERGENCY_LOGGER.logNPE("printWriter"); + return; + } + if (throwable == null) { + EMERGENCY_LOGGER.logNPE("throwable"); + writer.append("[NULL REFERENCE]"); + return; + } + if (alreadyPrinted == null) { + EMERGENCY_LOGGER.logNPE("alreadyPrinted"); + writer.append("[INVALID REFERENCE]"); + return; + } + if (enclosingTrace == null) { + EMERGENCY_LOGGER.logNPE("enclosingTrace"); + writer.append("[INVALID REFERENCE]"); + return; + } + if (alreadyPrinted.contains(throwable)) { + writer.append("[CIRCULAR REFERENCE: " + throwable + "]"); + return; + } + alreadyPrinted.add(throwable); + if (alreadyPrinted.size() > 1) { + writer.append("Cause: "); + } + writer.append(throwable.getClass().getName()); + writer.append(": "); + writer.append(throwable.getMessage()); + writer.append(System.lineSeparator()); + + final StackTraceElement[] stackTrace = throwable.getStackTrace(); + int m = stackTrace.length - 1; + int n = enclosingTrace.length - 1; + while (m >= 0 && n >= 0 && stackTrace[m].equals(enclosingTrace[n])) { + m--; + n--; + } + if (MAX_STACK_TRACE_DEPTH >= 0) { + m = Math.min(m, MAX_STACK_TRACE_DEPTH); + } + final int skippedFrames = stackTrace.length - 1 - m; + for (int i = 0; i <= m; i++) { + final StackTraceElement stackTraceElement = stackTrace[i]; + final String moduleName = stackTraceElement.getModuleName(); + final String className = stackTraceElement.getClassName(); + final String methodName = stackTraceElement.getMethodName(); + final String fileName = stackTraceElement.getFileName(); + final int line = stackTraceElement.getLineNumber(); + writer.append("\tat "); + if (moduleName != null) { + writer.append(moduleName); + writer.append("/"); + } + writer.append(className); + writer.append("."); + writer.append(methodName); + writer.append("("); + writer.append(fileName); + writer.append(Integer.toString(line)); + writer.append(")"); + writer.append(System.lineSeparator()); + } + if (skippedFrames != 0) { + writer.append("\t... "); + writer.append(Integer.toString(skippedFrames)); + writer.append(" more"); + writer.append(System.lineSeparator()); + } + final Throwable cause = throwable.getCause(); + if (cause != null) { + print(writer, cause, alreadyPrinted, stackTrace); + } } /** diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/level/HandlerLoggingLevelConfig.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/level/HandlerLoggingLevelConfig.java index 587dc5eec36d..e0ecab9e675b 100644 --- a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/level/HandlerLoggingLevelConfig.java +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/level/HandlerLoggingLevelConfig.java @@ -207,20 +207,20 @@ private boolean containsUpperCase(@NonNull final String name) { } /** - * Returns true if the given level is enabled for the given handler. + * Returns true if the given level is enabled for the given name. * - * @param handler The handler name. + * @param name The name of the logger. * @param level The level. * - * @return True if the given level is enabled for the given handler. + * @return True if the given level is enabled for the given name. */ - public boolean isEnabled(@NonNull final String handler, @NonNull final Level level, @Nullable final Marker marker) { + public boolean isEnabled(@NonNull final String name, @NonNull final Level level, @Nullable final Marker marker) { if (level == null) { EMERGENCY_LOGGER.logNPE("level"); return true; } - if (handler == null) { - EMERGENCY_LOGGER.logNPE("handler"); + if (name == null) { + EMERGENCY_LOGGER.logNPE("name"); return true; } if (marker != null) { @@ -238,7 +238,7 @@ public boolean isEnabled(@NonNull final String handler, @NonNull final Level lev } } - final ConfigLevel enabledLevel = levelCache.computeIfAbsent(handler.trim(), this::getConfiguredLevel); + final ConfigLevel enabledLevel = levelCache.computeIfAbsent(name.trim(), this::getConfiguredLevel); return enabledLevel.enabledLoggingOfLevel(level); } diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/buffer/BufferedOutputStream.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/buffer/BufferedOutputStream.java new file mode 100644 index 000000000000..170024b29726 --- /dev/null +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/buffer/BufferedOutputStream.java @@ -0,0 +1,139 @@ +/* + * 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.logging.buffer; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.util.Objects; + +/** + * An OutputStream that uses {@link ByteBuffer} before writing to an underlying {@link OutputStream} + */ +public class BufferedOutputStream extends OutputStream { + private final ByteBuffer buffer; + private final OutputStream outputStream; + + /** + * Creates a Writer that uses an internal {@link ByteBuffer} to buffer writes to the given {@code outputStream}. + * + * @param outputStream the underlying {@link OutputStream} to write to + * @param bufferCapacity the capacity of the buffer has to be grater than 0 + * @throws IllegalArgumentException in case {@code bufferCapacity} is less or equals to 0 + * @throws NullPointerException in case {@code outputStream} is null + */ + public BufferedOutputStream(@NonNull final OutputStream outputStream, final int bufferCapacity) { + if (bufferCapacity <= 0) { + throw new IllegalArgumentException("bufferCapacity must be > than 0"); + } + this.outputStream = Objects.requireNonNull(outputStream, "outputStream must not be null"); + this.buffer = ByteBuffer.wrap(new byte[bufferCapacity]); + } + + /** + * if {@code length} is less than the remaining capacity of the buffer, buffers the {@code bytes} and eventually + * writes it to the underlying stream. if the buffer is full or {@code length} is greater than buffers capacity, + * writes bytes and the buffer content to the underlying output stream + * + * @param bytes information to write + * @throws IOException in case there was an error writing to the underlying outputStream + */ + @Override + public synchronized void write(@NonNull final byte[] bytes, final int offset, final int length) throws IOException { + internalWrite(bytes, offset, length); + } + + /** + * if {@code bytes} length is less than the remaining capacity of the buffer, buffers the {@code bytes} and + * eventually writes it to the underlying stream. if the buffer is full or {@code buffer} length is greater than + * buffers capacity, writes bytes and the buffer content to the underlying output stream + * + * @param bytes information to write + * @throws IOException in case there was an error writing to the underlying outputStream + */ + @Override + public synchronized void write(@NonNull final byte[] bytes) throws IOException { + internalWrite(bytes, 0, bytes.length); + } + + /** + * {@inheritDoc} + */ + @Override + public synchronized void write(final int b) throws IOException { + if (buffer.remaining() >= 1) { + buffer.put((byte) b); + } else { + // if request length exceeds buffer capacity, + // flush the buffer and write the data directly + flush(); + outputStream.write(b); + } + } + + /** + * {@inheritDoc} + */ + @Override + public synchronized void flush() throws IOException { + flushBuffer(buffer); + flushDestination(); + } + + /** + * {@inheritDoc} + */ + @Override + public void close() throws IOException { + flush(); + outputStream.close(); + } + + private void internalWrite(final @NonNull byte[] bytes, final int offset, final int length) throws IOException { + if (length >= buffer.capacity()) { + // if request length exceeds buffer capacity, flush the buffer and write the data directly + flush(); + writeToDestination(bytes, offset, length); + } else { + if (length > buffer.remaining()) { + flush(); + } + buffer.put(bytes, offset, length); + } + } + + private void writeToDestination(final byte[] bytes, final int offset, final int length) throws IOException { + outputStream.write(bytes, offset, length); + } + + private void flushDestination() throws IOException { + outputStream.flush(); + } + + private void flushBuffer(final ByteBuffer buf) throws IOException { + ((Buffer) buf).flip(); + try { + if (buf.remaining() > 0) { + writeToDestination(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining()); + } + } finally { + buf.clear(); + } + } +} diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/console/ConsoleHandler.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/console/ConsoleHandler.java index 98176b742585..04840fc62b4e 100644 --- a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/console/ConsoleHandler.java +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/console/ConsoleHandler.java @@ -17,40 +17,70 @@ package com.swirlds.logging.console; import com.swirlds.config.api.Configuration; +import com.swirlds.logging.api.Level; import com.swirlds.logging.api.extensions.event.LogEvent; import com.swirlds.logging.api.extensions.handler.AbstractSyncedHandler; -import com.swirlds.logging.api.internal.format.LineBasedFormat; +import com.swirlds.logging.api.internal.format.FormattedLinePrinter; +import com.swirlds.logging.buffer.BufferedOutputStream; import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; /** * A handler that logs events to the console. - * - * This class extends the {@link AbstractSyncedHandler} and provides a simple way to log - * {@link LogEvent}s to the console using a {@link LineBasedFormat}. + *

    + * This class extends the {@link AbstractSyncedHandler} and provides a simple way to log {@link LogEvent}s to the + * console using a {@link FormattedLinePrinter}. * * @see AbstractSyncedHandler - * @see LineBasedFormat + * @see FormattedLinePrinter */ public class ConsoleHandler extends AbstractSyncedHandler { + private static final int BUFFER_CAPACITY = 8192; + private final FormattedLinePrinter format; + private final OutputStream outputStream; + /** * Constructs a new ConsoleHandler with the specified configuration. * + * @param handlerName The unique name of this handler. * @param configuration The configuration for this handler. */ - public ConsoleHandler(@NonNull final String configKey, @NonNull final Configuration configuration) { - super(configKey, configuration); + public ConsoleHandler( + @NonNull final String handlerName, @NonNull final Configuration configuration, final boolean buffered) { + super(handlerName, configuration); + this.format = FormattedLinePrinter.createForHandler(handlerName, configuration); + this.outputStream = buffered ? new BufferedOutputStream(System.out, BUFFER_CAPACITY) : System.out; } /** - * Handles a log event by printing it to the console using the {@link LineBasedFormat}, - * followed by flushing the console output. + * Handles a log event by printing it to the console using the {@link FormattedLinePrinter}. May be buffered and not + * immediately flushed. * * @param event The log event to be printed. */ @Override protected void handleEvent(@NonNull final LogEvent event) { - LineBasedFormat.print(System.out, event); - System.out.flush(); + StringBuilder builder = new StringBuilder(); + format.print(builder, event); + try { + outputStream.write(builder.toString().getBytes(StandardCharsets.UTF_8)); + } catch (IOException exception) { // Should not happen + EMERGENCY_LOGGER.log(Level.ERROR, "Failed to write to console", exception); + } + } + + /** + * {@inheritDoc} + */ + @Override + protected void handleStopAndFinalize() { + try { + outputStream.flush(); + } catch (IOException exception) { // Should Not happen + EMERGENCY_LOGGER.log(Level.ERROR, "Failed to close file output stream", exception); + } } } diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/console/ConsoleHandlerFactory.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/console/ConsoleHandlerFactory.java index eb5a39495c47..06c16c26c2fe 100644 --- a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/console/ConsoleHandlerFactory.java +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/console/ConsoleHandlerFactory.java @@ -24,9 +24,9 @@ /** * A factory for creating {@link ConsoleHandler} instances. - * - * This class implements the {@link LogHandlerFactory} interface and is responsible for creating - * instances of the {@link ConsoleHandler} class with the provided {@link Configuration}. + *

    + * This class implements the {@link LogHandlerFactory} interface and is responsible for creating instances of the + * {@link ConsoleHandler} class with the provided {@link Configuration}. * * @see LogHandlerFactory * @see ConsoleHandler @@ -40,16 +40,15 @@ public class ConsoleHandlerFactory implements LogHandlerFactory { /** * Creates a new {@link ConsoleHandler} instance with the specified {@link Configuration}. * - * @param configKey The name of the handler instance. + * @param handlerName The name of the handler instance. * @param configuration The configuration for the new handler instance. * @return A new {@link ConsoleHandler} instance. - * * @throws NullPointerException if the provided {@code configuration} is {@code null}. */ @Override @NonNull - public LogHandler create(@NonNull final String configKey, @NonNull final Configuration configuration) { - return new ConsoleHandler(configKey, configuration); + public LogHandler create(@NonNull final String handlerName, @NonNull final Configuration configuration) { + return new ConsoleHandler(handlerName, configuration, true); } @NonNull diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/file/FileHandler.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/file/FileHandler.java index e6f92b589a97..1e69322ea7ab 100644 --- a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/file/FileHandler.java +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/file/FileHandler.java @@ -20,91 +20,93 @@ import com.swirlds.logging.api.Level; import com.swirlds.logging.api.extensions.event.LogEvent; import com.swirlds.logging.api.extensions.handler.AbstractSyncedHandler; -import com.swirlds.logging.api.internal.format.LineBasedFormat; +import com.swirlds.logging.api.internal.format.FormattedLinePrinter; +import com.swirlds.logging.buffer.BufferedOutputStream; import edu.umd.cs.findbugs.annotations.NonNull; -import java.io.BufferedWriter; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardOpenOption; import java.util.Objects; /** * A file handler that writes log events to a file. *

    - * This handler use a {@link BufferedWriter} to write {@link LogEvent}s to a file. - * You can configure the following properties: + * This handler use a {@link BufferedOutputStream} to write {@link LogEvent}s to a file. You can configure the following + * properties: *

      *
    • {@code file} - the {@link Path} of the file
    • *
    • {@code append} - whether to append to the file or not
    • *
    - * */ public class FileHandler extends AbstractSyncedHandler { private static final String FILE_NAME_PROPERTY = "%s.file"; private static final String APPEND_PROPERTY = "%s.append"; private static final String DEFAULT_FILE_NAME = "swirlds-log.log"; - private final BufferedWriter bufferedWriter; + private static final int BUFFER_CAPACITY = 8192 * 8; + private final OutputStream outputStream; + private final FormattedLinePrinter format; /** * Creates a new file handler. * - * @param configKey the configuration key + * @param handlerName the unique handler name * @param configuration the configuration + * @param buffered if true a buffer is used in between the file writing */ - public FileHandler(@NonNull final String configKey, @NonNull final Configuration configuration) { - super(configKey, configuration); + public FileHandler( + @NonNull final String handlerName, @NonNull final Configuration configuration, final boolean buffered) + throws IOException { + super(handlerName, configuration); + + this.format = FormattedLinePrinter.createForHandler(handlerName, configuration); - final String propertyPrefix = PROPERTY_HANDLER.formatted(configKey); + final String propertyPrefix = PROPERTY_HANDLER.formatted(handlerName); final Path filePath = Objects.requireNonNullElse( configuration.getValue(FILE_NAME_PROPERTY.formatted(propertyPrefix), Path.class, null), Path.of(DEFAULT_FILE_NAME)); final boolean append = Objects.requireNonNullElse( configuration.getValue(APPEND_PROPERTY.formatted(propertyPrefix), Boolean.class, null), true); - - BufferedWriter bufferedWriter = null; try { - if (!Files.exists(filePath) || Files.isWritable(filePath)) { - if (append) { - bufferedWriter = Files.newBufferedWriter( - filePath, - StandardOpenOption.CREATE, - StandardOpenOption.APPEND, - StandardOpenOption.WRITE, - StandardOpenOption.DSYNC); - } else { - bufferedWriter = Files.newBufferedWriter( - filePath, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.DSYNC); - } - } else { - EMERGENCY_LOGGER.log(Level.ERROR, "Log file could not be created or written to"); + if (Files.exists(filePath) && !(append && Files.isWritable(filePath))) { + throw new IOException("Log file exist and is not writable or is not append mode"); } - } catch (final Exception exception) { - EMERGENCY_LOGGER.log(Level.ERROR, "Failed to create FileHandler", exception); + if (filePath.getParent() != null) Files.createDirectories(filePath.getParent()); + final OutputStream fileOutputStream = new FileOutputStream(filePath.toFile(), append); + this.outputStream = + buffered ? new BufferedOutputStream(fileOutputStream, BUFFER_CAPACITY) : fileOutputStream; + } catch (IOException e) { + throw new IOException("Could not create log file " + filePath.toAbsolutePath(), e); } - this.bufferedWriter = bufferedWriter; } /** - * Handles a log event by appending it to the file using the {@link LineBasedFormat}. + * Handles a log event by appending it to the file using the {@link FormattedLinePrinter}. * * @param event The log event to be printed. */ @Override protected void handleEvent(@NonNull final LogEvent event) { - if (bufferedWriter != null) { - LineBasedFormat.print(bufferedWriter, event); + final StringBuilder writer = new StringBuilder(4 * 1024); + format.print(writer, event); + try { + this.outputStream.write(writer.toString().getBytes(StandardCharsets.UTF_8)); + } catch (final Exception exception) { + EMERGENCY_LOGGER.log(Level.ERROR, "Failed to write to file output stream", exception); } } + /** + * Stops the handler and no further events are processed + */ @Override protected void handleStopAndFinalize() { super.handleStopAndFinalize(); try { - if (bufferedWriter != null) { - bufferedWriter.flush(); - bufferedWriter.close(); - } + outputStream.close(); } catch (final Exception exception) { EMERGENCY_LOGGER.log(Level.ERROR, "Failed to close file output stream", exception); } diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/file/FileHandlerFactory.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/file/FileHandlerFactory.java index 20378a3d03e9..93417ce8fd5b 100644 --- a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/file/FileHandlerFactory.java +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/file/FileHandlerFactory.java @@ -21,13 +21,14 @@ import com.swirlds.logging.api.extensions.handler.LogHandler; import com.swirlds.logging.api.extensions.handler.LogHandlerFactory; import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.IOException; import java.util.ServiceLoader; /** * A factory for creating new {@link FileHandler} instances. - * - * This is a {@link LogHandlerFactory} and is discovered by the {@link ServiceLoader} at runtime. - * The factory creates new {@link FileHandler} instances with the specified {@link Configuration}. + *

    + * This is a {@link LogHandlerFactory} and is discovered by the {@link ServiceLoader} at runtime. The factory creates + * new {@link FileHandler} instances with the specified {@link Configuration}. * * @see LogHandlerFactory * @see FileHandler @@ -45,16 +46,20 @@ public class FileHandlerFactory implements LogHandlerFactory { /** * Creates a new {@link FileHandler} instance with the specified {@link Configuration}. * - * @param configKey The name of the handler instance. + * @param handlerName The name of the handler instance. * @param configuration The configuration for the new handler instance. * @return A new {@link FileHandler} instance. - * * @throws NullPointerException if the provided {@code configuration} is {@code null}. + * @throws RuntimeException if there was an error trying to create the {@link FileHandler}. */ @NonNull @Override - public LogHandler create(@NonNull final String configKey, @NonNull final Configuration configuration) { - return new FileHandler(configKey, configuration); + public LogHandler create(@NonNull final String handlerName, @NonNull final Configuration configuration) { + try { + return new FileHandler(handlerName, configuration, true); + } catch (IOException e) { + throw new RuntimeException("Unable to create FileHandler", e); + } } /** diff --git a/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/EmergencyLoggerTest.java b/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/EmergencyLoggerTest.java index 14d196d3fec5..50d7e37d0399 100644 --- a/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/EmergencyLoggerTest.java +++ b/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/EmergencyLoggerTest.java @@ -46,7 +46,7 @@ void testLog1Line() { // then Assertions.assertEquals(1, systemErrProvider.getLines().count()); - Assertions.assertTrue(systemErrProvider.getLines().toList().get(0).endsWith("EMERGENCY-LOGGER - test")); + Assertions.assertTrue(systemErrProvider.getLines().toList().getFirst().endsWith("EMERGENCY-LOGGER - test")); } @Test diff --git a/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/LogLevelTest.java b/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/LogLevelTest.java index 0237eaea1280..45e5e7523d06 100644 --- a/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/LogLevelTest.java +++ b/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/LogLevelTest.java @@ -187,7 +187,7 @@ void nameNull() { // then assertThat(result).isTrue(); - assertThat(systemErrProvider.getLines()).anyMatch(s -> s.contains("Null parameter: handler")); + assertThat(systemErrProvider.getLines()).anyMatch(s -> s.contains("Null parameter: name")); } @Test diff --git a/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/LoggerImplTest.java b/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/LoggerImplTest.java index 59e2421bd673..90278b1a3332 100644 --- a/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/LoggerImplTest.java +++ b/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/LoggerImplTest.java @@ -16,8 +16,16 @@ package com.swirlds.logging; +import com.swirlds.config.api.Configuration; +import com.swirlds.config.api.ConfigurationBuilder; +import com.swirlds.logging.api.Logger; +import com.swirlds.logging.api.extensions.handler.LogHandler; import com.swirlds.logging.api.internal.LoggerImpl; +import com.swirlds.logging.api.internal.LoggingSystem; +import com.swirlds.logging.api.internal.configuration.ConfigLevelConverter; +import com.swirlds.logging.api.internal.configuration.MarkerStateConverter; import com.swirlds.logging.api.internal.event.SimpleLogEventFactory; +import com.swirlds.logging.file.FileHandlerFactory; import com.swirlds.logging.util.DummyConsumer; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -98,6 +106,27 @@ void testSpecWithSimpleLogger() { LoggerApiSpecTest.testSpec(logger); } + @Test + void testSpecWithFileLogHandler() { + // given + final Configuration configuration = ConfigurationBuilder.create() + .withConverter(new ConfigLevelConverter()) + .withConverter(new MarkerStateConverter()) + .withValue("logging.level", "trace") + .withValue("logging.handler.file.type", "file") + .withValue("logging.handler.file.active", "true") + .withValue("logging.handler.file.level", "trace") + .withValue("logging.handler.file.file", "benchmark.log") + .build(); + final LogHandler fileHandler = new FileHandlerFactory().create("file", configuration); + final LoggingSystem loggingSystem = new LoggingSystem(configuration); + loggingSystem.addHandler(fileHandler); + final Logger logger = loggingSystem.getLogger("test-name"); + + // then + LoggerApiSpecTest.testSpec(logger); + } + @Test void testSpecWithDifferentLoggers() { LoggerApiSpecTest.testSpec(new LoggerImpl("test-name", new SimpleLogEventFactory(), new DummyConsumer())); diff --git a/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/LoggingSystemStressTest.java b/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/LoggingSystemStressTest.java index d28e1af5d615..5443d15ad877 100644 --- a/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/LoggingSystemStressTest.java +++ b/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/LoggingSystemStressTest.java @@ -17,6 +17,10 @@ package com.swirlds.logging; import static com.swirlds.common.test.fixtures.junit.tags.TestQualifierTags.TIMING_SENSITIVE; +import static com.swirlds.logging.util.LoggingTestUtils.EXPECTED_STATEMENTS; +import static com.swirlds.logging.util.LoggingTestUtils.countLinesInStatements; +import static com.swirlds.logging.util.LoggingTestUtils.getLines; +import static com.swirlds.logging.util.LoggingTestUtils.linesToStatements; import com.swirlds.base.test.fixtures.concurrent.TestExecutor; import com.swirlds.base.test.fixtures.concurrent.WithTestExecutor; @@ -24,10 +28,16 @@ import com.swirlds.config.extensions.test.fixtures.TestConfigBuilder; import com.swirlds.logging.api.Logger; import com.swirlds.logging.api.internal.LoggingSystem; +import com.swirlds.logging.file.FileHandler; +import com.swirlds.logging.test.fixtures.internal.LoggingMirrorImpl; import com.swirlds.logging.util.InMemoryHandler; -import com.swirlds.logging.util.LoggingUtils; +import com.swirlds.logging.util.LoggingTestUtils; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; import java.util.Objects; +import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.junit.jupiter.api.Assertions; @@ -38,6 +48,9 @@ @Tag(TIMING_SENSITIVE) public class LoggingSystemStressTest { + private static final int TOTAL_RUNNABLE = 100; + private static final String LOG_FILE = "log-files/logging.log"; + @Test void testMultipleLoggersInParallel(TestExecutor testExecutor) { // given @@ -45,19 +58,20 @@ void testMultipleLoggersInParallel(TestExecutor testExecutor) { final LoggingSystem loggingSystem = new LoggingSystem(configuration); final InMemoryHandler handler = new InMemoryHandler(configuration); loggingSystem.addHandler(handler); - final List runnables = IntStream.range(0, 100) + final List runnables = IntStream.range(0, TOTAL_RUNNABLE) .mapToObj(i -> loggingSystem.getLogger("logger-" + i)) - .map(l -> (Runnable) () -> LoggingUtils.generateExtensiveLogMessages(l)) + .map(l -> (Runnable) () -> LoggingTestUtils.loggExtensively(l)) .collect(Collectors.toList()); // when testExecutor.executeAndWait(runnables); // then - Assertions.assertEquals(140000, handler.getEvents().size()); - IntStream.range(0, 100) + Assertions.assertEquals( + EXPECTED_STATEMENTS * TOTAL_RUNNABLE, handler.getEvents().size()); + IntStream.range(0, TOTAL_RUNNABLE) .forEach(i -> Assertions.assertEquals( - 1400, + EXPECTED_STATEMENTS, handler.getEvents().stream() .filter(e -> Objects.equals(e.loggerName(), "logger-" + i)) .count())); @@ -67,18 +81,59 @@ void testMultipleLoggersInParallel(TestExecutor testExecutor) { void testOneLoggerInParallel(TestExecutor testExecutor) { // given final Configuration configuration = new TestConfigBuilder().getOrCreateConfig(); + final LoggingSystem loggingSystem = new LoggingSystem(configuration); final Logger logger = loggingSystem.getLogger("logger"); final InMemoryHandler handler = new InMemoryHandler(configuration); loggingSystem.addHandler(handler); - final List runnables = IntStream.range(0, 100) - .mapToObj(l -> (Runnable) () -> LoggingUtils.generateExtensiveLogMessages(logger)) - .collect(Collectors.toList()); // when - testExecutor.executeAndWait(runnables); + doLog(testExecutor, logger, TOTAL_RUNNABLE); // then - Assertions.assertEquals(140000, handler.getEvents().size()); + Assertions.assertEquals( + EXPECTED_STATEMENTS * TOTAL_RUNNABLE, handler.getEvents().size()); + } + + @Test + void testFileLoggingFileMultipleEventsInParallel(TestExecutor testExecutor) throws IOException { + + // given + final String logFile = LoggingTestUtils.prepareLoggingFile(LOG_FILE); + final String fileHandlerName = "file"; + final Configuration configuration = LoggingTestUtils.prepareConfiguration(logFile, fileHandlerName); + final LoggingSystem loggingSystem = new LoggingSystem(configuration); + final FileHandler handler = new FileHandler(fileHandlerName, configuration, true); + final LoggingMirrorImpl mirror = new LoggingMirrorImpl(); + loggingSystem.addHandler(handler); + loggingSystem.addHandler(mirror); + // A random log name, so it's easier to combine lines after + final String loggerName = UUID.randomUUID().toString(); + final Logger logger = loggingSystem.getLogger(loggerName); + + // when + doLog(testExecutor, logger, 10); + loggingSystem.stopAndFinalize(); + + try { + final List statementsInMirror = LoggingTestUtils.mirrorToStatements(mirror); + final List logLines = getLines(logFile); + final List statementsInFile = linesToStatements(logLines); + + // then + Assertions.assertEquals(EXPECTED_STATEMENTS * 10, statementsInFile.size()); + final int expectedLineCountInFile = countLinesInStatements(statementsInMirror); + Assertions.assertEquals(expectedLineCountInFile, (long) logLines.size()); + org.assertj.core.api.Assertions.assertThat(statementsInMirror).isSubsetOf(statementsInFile); + + } finally { + Files.deleteIfExists(Path.of(logFile)); + } + } + + private static void doLog(final TestExecutor testExecutor, final Logger logger, final int totalRunnable) { + testExecutor.executeAndWait(IntStream.range(0, totalRunnable) + .mapToObj(l -> (Runnable) () -> LoggingTestUtils.loggExtensively(logger)) + .collect(Collectors.toList())); } } diff --git a/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/LoggingSystemTest.java b/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/LoggingSystemTest.java new file mode 100644 index 000000000000..076e4028e5fa --- /dev/null +++ b/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/LoggingSystemTest.java @@ -0,0 +1,80 @@ +/* + * 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.swirlds.logging; + +import static com.swirlds.logging.util.LoggingTestUtils.EXPECTED_STATEMENTS; +import static com.swirlds.logging.util.LoggingTestUtils.countLinesInStatements; +import static com.swirlds.logging.util.LoggingTestUtils.getLines; +import static com.swirlds.logging.util.LoggingTestUtils.linesToStatements; + +import com.swirlds.base.test.fixtures.concurrent.WithTestExecutor; +import com.swirlds.config.api.Configuration; +import com.swirlds.logging.api.Logger; +import com.swirlds.logging.api.internal.LoggingSystem; +import com.swirlds.logging.file.FileHandler; +import com.swirlds.logging.test.fixtures.internal.LoggingMirrorImpl; +import com.swirlds.logging.util.LoggingTestUtils; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.UUID; +import org.junit.jupiter.api.Test; + +@WithTestExecutor +public class LoggingSystemTest { + + private static final String LOG_FILE = "log-files/logging.log"; + + @Test + void testFileHandlerLogging() throws IOException { + + // given + final String logFile = LoggingTestUtils.prepareLoggingFile(LOG_FILE); + final String fileHandlerName = "file"; + final Configuration configuration = LoggingTestUtils.prepareConfiguration(logFile, fileHandlerName); + final LoggingSystem loggingSystem = new LoggingSystem(configuration); + final FileHandler handler = new FileHandler(fileHandlerName, configuration, true); + final LoggingMirrorImpl mirror = new LoggingMirrorImpl(); + loggingSystem.addHandler(handler); + loggingSystem.addHandler(mirror); + // A random log name, so it's easier to combine lines after + final String loggerName = UUID.randomUUID().toString(); + final Logger logger = loggingSystem.getLogger(loggerName); + + // when + LoggingTestUtils.loggExtensively(logger); + loggingSystem.stopAndFinalize(); + + try { + final List statementsInMirror = LoggingTestUtils.mirrorToStatements(mirror); + final List logLines = getLines(logFile); + final List statementsInFile = linesToStatements(logLines); + + // then + org.assertj.core.api.Assertions.assertThat(statementsInFile.size()).isEqualTo(EXPECTED_STATEMENTS); + + // Loglines should be 1 per statement in mirror + 1 for each stament + final int expectedLineCountInFile = countLinesInStatements(statementsInMirror); + org.assertj.core.api.Assertions.assertThat((long) logLines.size()).isEqualTo(expectedLineCountInFile); + org.assertj.core.api.Assertions.assertThat(statementsInFile).isSubsetOf(statementsInMirror); + + } finally { + Files.deleteIfExists(Path.of(logFile)); + } + } +} diff --git a/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/api/internal/format/EpochFormatUtilsTest.java b/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/api/internal/format/EpochFormatUtilsTest.java new file mode 100644 index 000000000000..a99b6a2e126c --- /dev/null +++ b/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/api/internal/format/EpochFormatUtilsTest.java @@ -0,0 +1,72 @@ +/* + * 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.logging.api.internal.format; + +import static java.time.ZoneOffset.UTC; +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import org.junit.jupiter.api.Test; + +public class EpochFormatUtilsTest { + + @Test + void testTimestampAsString() { + // given + long timestamp = Instant.parse("2024-03-06T12:00:00Z").toEpochMilli(); + + // when + String formattedTimestamp = EpochFormatUtils.timestampAsString(timestamp); + + String expectedTimestamp = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS ") + .withZone(UTC) + .format(Instant.ofEpochMilli(timestamp)); + + // then + assertThat(formattedTimestamp.length()).isEqualTo(26); + // assertThat(formattedTimestamp).isEqualTo(expectedTimestamp); FUTURE WORK: FIX this scenario + } + + @Test + void testTimestampAsStringWithNegativeTimestamp() { + // given + long timestamp = -1; // Negative timestamp + + // when + String formattedTimestamp = EpochFormatUtils.timestampAsString(timestamp); + + // then + assertThat(formattedTimestamp).isEqualTo("1969-12-31T23:59:59.999 "); + } + + @Test + void testNegativeTimestampOverflowed26() { + // given + String formattedTimestamp = EpochFormatUtils.timestampAsString(Long.MIN_VALUE); + // then + assertThat(formattedTimestamp).isEqualTo("BROKEN-TIMESTAMP "); + } + + @Test + void testPositiveTimestampOverflowed26() { + // given + String formattedTimestamp = EpochFormatUtils.timestampAsString(Long.MAX_VALUE); + // then + assertThat(formattedTimestamp).isEqualTo("BROKEN-TIMESTAMP "); + } +} diff --git a/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/buffer/BufferedOutputStreamTest.java b/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/buffer/BufferedOutputStreamTest.java new file mode 100644 index 000000000000..a1c7c67d53b4 --- /dev/null +++ b/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/buffer/BufferedOutputStreamTest.java @@ -0,0 +1,149 @@ +/* + * 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.logging.buffer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.Test; + +public class BufferedOutputStreamTest { + + @Test + void testWriteSingleByte() throws IOException { + // Given + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream, 10)) { + // When + bufferedOutputStream.write('A'); + } + + // Then + assertThat(outputStream.toString()).isEqualTo("A"); + } + + @Test + void testWriteByteArray() throws IOException { + // Given + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + byte[] bytes = "Hello, Swirlds!".getBytes(StandardCharsets.UTF_8); + try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream, 10)) { + // When + bufferedOutputStream.write(bytes); + } + + // Then + assertThat(outputStream.toString()).isEqualTo("Hello, Swirlds!"); + } + + @Test + void testWriteByteArrayWithOffsetAndLength() throws IOException { + // Given + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + byte[] bytes = "Hello, Swirlds!".getBytes(StandardCharsets.UTF_8); + try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream, 10)) { + // When + bufferedOutputStream.write(bytes, 7, 7); + } + + // Then + assertThat(outputStream.toString()).isEqualTo("Swirlds"); + } + + @Test + void testFlush() throws IOException { + // Given + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream, 10)) { + + // When + bufferedOutputStream.write('A'); + bufferedOutputStream.flush(); + + // Then + assertThat(outputStream.toString()).isEqualTo("A"); + } + } + + @Test + void testWriteNoFlush() throws IOException { + // Given + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream, 1); ) { + + // When / Then + bufferedOutputStream.write('A'); + assertThat(outputStream.size()).isEqualTo(0); + bufferedOutputStream.write('B'); + assertThat(outputStream.size()).isEqualTo(2); + assertThat(outputStream.toString()).isEqualTo("AB"); + } + } + + @Test + void testWriteNoFlushArray() throws IOException { + // Given + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream, 7); ) { + + // When / Then + final byte[] a = "Hello ".getBytes(StandardCharsets.UTF_8); + final byte[] b = "Swirlds!".getBytes(StandardCharsets.UTF_8); + bufferedOutputStream.write(a); + assertThat(outputStream.size()).isEqualTo(0); + bufferedOutputStream.write(b); + assertThat(outputStream.size()).isEqualTo(14); + assertThat(outputStream.toString()).isEqualTo("Hello Swirlds!"); + } + } + + @Test + void testClose() throws IOException { + // Given + final AtomicBoolean underlyingCloseWasCalled = new AtomicBoolean(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream() { + // Small hack so we don't add mockito dependency + @Override + public void close() throws IOException { + super.close(); + underlyingCloseWasCalled.set(true); + } + }; + BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream, 10); + + // When + bufferedOutputStream.close(); + + // Then + assertThat(outputStream.toString()).isEmpty(); + assertTrue(underlyingCloseWasCalled.get()); + } + + @Test + void testInvalidBufferCapacity() { + // Given + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + // When / Then + assertThrows(IllegalArgumentException.class, () -> new BufferedOutputStream(outputStream, 0)); + } +} diff --git a/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/util/LoggingTestUtils.java b/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/util/LoggingTestUtils.java new file mode 100644 index 000000000000..1d771564d5d2 --- /dev/null +++ b/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/util/LoggingTestUtils.java @@ -0,0 +1,184 @@ +/* + * 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.logging.util; + +import com.swirlds.config.api.Configuration; +import com.swirlds.config.extensions.test.fixtures.TestConfigBuilder; +import com.swirlds.logging.api.Logger; +import com.swirlds.logging.api.internal.configuration.ConfigLevelConverter; +import com.swirlds.logging.api.internal.configuration.MarkerStateConverter; +import com.swirlds.logging.api.internal.format.FormattedLinePrinter; +import com.swirlds.logging.api.internal.level.ConfigLevel; +import com.swirlds.logging.api.internal.level.MarkerState; +import com.swirlds.logging.test.fixtures.internal.LoggingMirrorImpl; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * Utility class for logging related operations. + */ +public final class LoggingTestUtils { + public static final int EXPECTED_STATEMENTS = 14 * 100; + + private static boolean checkIsLogLine(String inputString) { + + for (ConfigLevel logLevel : ConfigLevel.values()) { + if (inputString.contains(logLevel.name())) { + return true; + } + } + return false; + } + + public static List getLines(String path) throws IOException { + List lines = new ArrayList<>(); + + try (BufferedReader reader = new BufferedReader(new FileReader(path))) { + String line; + while ((line = reader.readLine()) != null) { + lines.add(line); + } + } + return lines; + } + + /** + * Converts a list of log lines into a list of statements. Stacktrace and multiple-line logs are grouped together to + * form statements based on the presence of log levels determined by {@link ConfigLevel}. + * + * @param logLines a list of log lines to be converted into statements. + * @return a list of statements derived from the provided log lines. + */ + public static List linesToStatements(List logLines) { + List result = new ArrayList<>(); + StringBuilder previousLine = new StringBuilder(); + + for (String line : logLines) { + if (checkIsLogLine(line)) { + if (!previousLine.isEmpty()) { + result.add(previousLine.toString()); + previousLine.setLength(0); + } + previousLine.append(line); + } else if (!line.isEmpty()) { + previousLine.append("\n").append(line); + } + } + if (!previousLine.isEmpty()) { + result.add(previousLine.toString()); + } + + return result; + } + + /** + * Counts the total new line chars in each element of the list and returns number of new line chars in each line + * plus 1 for each element on the collection + */ + public static int countLinesInStatements(List strings) { + int count = strings.size(); + for (String str : strings) { + for (int i = 0; i < str.length(); i++) { + if (str.charAt(i) == '\n') { + count++; + } + } + } + return count; + } + + /** + * extensively log messages into {@code logger} for testing and debugging purposes. + * + * @param logger the logger instance to use logging messages + */ + public static void loggExtensively(Logger logger) { + IntStream.range(0, 100).forEach(i -> { + logger.info("L0, Hello world!"); + logger.info("L1, A quick brown fox jumps over the lazy dog."); + logger.info("L2, Hello world!", new RuntimeException("test")); + logger.info("L3, Hello {}!", "placeholder"); + logger.info("L4, Hello {}!", new RuntimeException("test"), "placeholder"); + logger.withContext("key", "value").info("L5, Hello world!"); + logger.withMarker("marker").info("L6, Hello world!"); + logger.withContext("user-id", UUID.randomUUID().toString()).info("L7, Hello world!"); + logger.withContext("user-id", UUID.randomUUID().toString()) + .info("L8, Hello {}, {}, {}, {}, {}, {}, {}, {}, {}!", 1, 2, 3, 4, 5, 6, 7, 8, 9); + logger.withContext("user-id", UUID.randomUUID().toString()) + .info( + "L9, Hello {}, {}, {}, {}, {}, {}, {}, {}, {}!", + new RuntimeException("test"), + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9); + logger.withContext("user-id", UUID.randomUUID().toString()) + .withContext("key", "value") + .info("L10, Hello world!"); + logger.withMarker("marker").info("L11, Hello world!"); + logger.withMarker("marker1").withMarker("marker2").info("L12, Hello world!"); + logger.withContext("key", "value") + .withMarker("marker1") + .withMarker("marker2") + .info("L13, Hello {}, {}, {}, {}, {}, {}, {}, {}, {}!", 1, 2, 3, 4, 5, 6, 7, 8, 9); + }); + } + + public static List mirrorToStatements(final LoggingMirrorImpl mirror) { + final FormattedLinePrinter formattedEvents = new FormattedLinePrinter(false); + return mirror.getEvents().stream() + .map(e -> { + final StringBuilder stringBuilder = new StringBuilder(); + formattedEvents.print(stringBuilder, e); + stringBuilder.setLength(stringBuilder.length() - 1); + return stringBuilder.toString(); + }) + .collect(Collectors.toList()); + } + + public static Configuration prepareConfiguration(final String logFile, final String fileHandlerName) { + return new TestConfigBuilder() + .withConverter(ConfigLevel.class, new ConfigLevelConverter()) + .withConverter(MarkerState.class, new MarkerStateConverter()) + .withValue("logging.level", "trace") + .withValue("logging.handler.%s.type".formatted(fileHandlerName), "file") + .withValue("logging.handler.%s.active".formatted(fileHandlerName), "true") + .withValue("logging.handler.%s.formatTimestamp".formatted(fileHandlerName), "false") + .withValue("logging.handler.%s.level".formatted(fileHandlerName), "trace") + .withValue("logging.handler.%s.file".formatted(fileHandlerName), logFile) + .getOrCreateConfig(); + } + + public static String prepareLoggingFile(final String logFile) throws IOException { + final File testMultipleLoggersInParallel = new File(logFile); + Files.deleteIfExists(testMultipleLoggersInParallel.toPath()); + return testMultipleLoggersInParallel.getAbsolutePath(); + } +} diff --git a/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/util/LoggingUtils.java b/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/util/LoggingUtils.java deleted file mode 100644 index 7b97d5141507..000000000000 --- a/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/util/LoggingUtils.java +++ /dev/null @@ -1,69 +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.swirlds.logging.util; - -import com.swirlds.logging.api.Logger; -import java.util.UUID; -import java.util.stream.IntStream; - -/** - * Utility class for logging related operations. - */ -public final class LoggingUtils { - - /** - * Generates extensive log messages for testing and debugging purposes. - * - * @param logger the logger instance to use for generating log messages - */ - public static void generateExtensiveLogMessages(Logger logger) { - IntStream.range(0, 100).forEach(i -> { - logger.info("L0, Hello world!"); - logger.info("L1, A quick brown fox jumps over the lazy dog."); - logger.info("L2, Hello world!", new RuntimeException("test")); - logger.info("L3, Hello {}!", "placeholder"); - logger.info("L4, Hello {}!", new RuntimeException("test"), "placeholder"); - logger.withContext("key", "value").info("L5, Hello world!"); - logger.withMarker("marker").info("L6, Hello world!"); - logger.withContext("user-id", UUID.randomUUID().toString()).info("L7, Hello world!"); - logger.withContext("user-id", UUID.randomUUID().toString()) - .info("L8, Hello {}, {}, {}, {}, {}, {}, {}, {}, {}!", 1, 2, 3, 4, 5, 6, 7, 8, 9); - logger.withContext("user-id", UUID.randomUUID().toString()) - .info( - "L9, Hello {}, {}, {}, {}, {}, {}, {}, {}, {}!", - new RuntimeException("test"), - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9); - logger.withContext("user-id", UUID.randomUUID().toString()) - .withContext("key", "value") - .info("L10, Hello world!"); - logger.withMarker("marker").info("L11, Hello world!"); - logger.withMarker("marker1").withMarker("marker2").info("L12, Hello world!"); - logger.withContext("key", "value") - .withMarker("marker1") - .withMarker("marker2") - .info("L13, Hello {}, {}, {}, {}, {}, {}, {}, {}, {}!", 1, 2, 3, 4, 5, 6, 7, 8, 9); - }); - } -} From 8335bda2582e1a366bd0f52ac9c0ea5abf45409d Mon Sep 17 00:00:00 2001 From: Cody Littley <56973212+cody-littley@users.noreply.github.com> Date: Thu, 7 Mar 2024 13:33:21 -0600 Subject: [PATCH 036/115] feat: dispatch the dispatcher (#11950) Signed-off-by: Cody Littley --- .../swirlds/common/io/utility/RecycleBin.java | 3 +- .../common/io/utility/RecycleBinImpl.java | 3 +- .../common/test/fixtures/TestRecycleBin.java | 6 + .../com/swirlds/platform/PlatformBuilder.java | 5 +- .../com/swirlds/platform/SwirldsPlatform.java | 43 +- .../platform/dispatch/DispatchBuilder.java | 522 -------- .../dispatch/DispatchConfiguration.java | 89 -- .../swirlds/platform/dispatch/Observer.java | 40 - .../platform/dispatch/ObserverComment.java | 35 - .../swirlds/platform/dispatch/Trigger.java | 45 - .../dispatch/flowchart/CommentedTrigger.java | 47 - .../dispatch/flowchart/DispatchFlowchart.java | 312 ----- .../control/HaltRequestedConsumer.java | 34 - .../control/ShutdownRequestedTrigger.java | 40 - .../triggers/error/DeadlockTrigger.java | 32 - .../platform/dispatch/types/TriggerEight.java | 65 - .../platform/dispatch/types/TriggerFive.java | 53 - .../platform/dispatch/types/TriggerFour.java | 49 - .../platform/dispatch/types/TriggerNine.java | 70 - .../platform/dispatch/types/TriggerOne.java | 37 - .../platform/dispatch/types/TriggerSeven.java | 61 - .../platform/dispatch/types/TriggerSix.java | 57 - .../platform/dispatch/types/TriggerTen.java | 74 -- .../platform/dispatch/types/TriggerThree.java | 45 - .../platform/dispatch/types/TriggerTwo.java | 41 - .../platform/dispatch/types/TriggerZero.java | 31 - .../recovery/EmergencyRecoveryManager.java | 17 +- .../platform/state/iss/IssHandler.java | 12 +- .../com/swirlds/platform/system/Shutdown.java | 3 - .../swirlds/platform/util/BootstrapUtils.java | 2 - .../platform/util/DeadlockSentinel.java | 137 -- ...formComponents.java => ThingsToStart.java} | 23 +- .../src/main/java/module-info.java | 12 - .../platform/DispatchBuilderUtils.java | 36 - .../platform/DispatchFlowchartTests.java | 341 ----- .../com/swirlds/platform/DispatchTests.java | 1167 ----------------- .../state/StateManagementComponentTests.java | 9 - .../platform/util/DeadlockSentinelTests.java | 114 -- .../platform/test/DispatchBuilderUtils.java | 36 - .../platform/test/state/IssHandlerTests.java | 18 +- 40 files changed, 54 insertions(+), 3712 deletions(-) delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/DispatchBuilder.java delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/DispatchConfiguration.java delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/Observer.java delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/ObserverComment.java delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/Trigger.java delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/flowchart/CommentedTrigger.java delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/flowchart/DispatchFlowchart.java delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/control/HaltRequestedConsumer.java delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/control/ShutdownRequestedTrigger.java delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/error/DeadlockTrigger.java delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerEight.java delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerFive.java delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerFour.java delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerNine.java delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerOne.java delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerSeven.java delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerSix.java delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerTen.java delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerThree.java delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerTwo.java delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerZero.java delete mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/util/DeadlockSentinel.java rename platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/util/{PlatformComponents.java => ThingsToStart.java} (71%) delete mode 100644 platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/DispatchBuilderUtils.java delete mode 100644 platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/DispatchFlowchartTests.java delete mode 100644 platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/DispatchTests.java delete mode 100644 platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/util/DeadlockSentinelTests.java delete mode 100644 platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/DispatchBuilderUtils.java diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/io/utility/RecycleBin.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/io/utility/RecycleBin.java index 72683fb24495..f2b6a18b2ec7 100644 --- a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/io/utility/RecycleBin.java +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/io/utility/RecycleBin.java @@ -16,6 +16,7 @@ package com.swirlds.common.io.utility; +import com.swirlds.base.state.Startable; import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; import java.nio.file.Path; @@ -29,7 +30,7 @@ * code that depends on the existence of files in this temporary location. Files in this temporary location should be * treated as deleted by java code, and only used for debugging purposes. */ -public interface RecycleBin { +public interface RecycleBin extends Startable { /** * Remove a file or directory tree from its current location and move it to a temporary location. diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/io/utility/RecycleBinImpl.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/io/utility/RecycleBinImpl.java index 0260f20f2bd8..1189a8a961cb 100644 --- a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/io/utility/RecycleBinImpl.java +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/io/utility/RecycleBinImpl.java @@ -20,7 +20,6 @@ import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; import static com.swirlds.logging.legacy.LogMarker.STARTUP; -import com.swirlds.base.state.Startable; import com.swirlds.base.state.Stoppable; import com.swirlds.base.time.Time; import com.swirlds.common.config.StateCommonConfig; @@ -51,7 +50,7 @@ /** * A standard implementation of a {@link RecycleBin}. */ -public class RecycleBinImpl implements RecycleBin, Startable, Stoppable { +public class RecycleBinImpl implements RecycleBin, Stoppable { private static final Logger logger = LogManager.getLogger(RecycleBinImpl.class); diff --git a/platform-sdk/swirlds-common/src/testFixtures/java/com/swirlds/common/test/fixtures/TestRecycleBin.java b/platform-sdk/swirlds-common/src/testFixtures/java/com/swirlds/common/test/fixtures/TestRecycleBin.java index 6f8094f40818..ddfbfcf348c7 100644 --- a/platform-sdk/swirlds-common/src/testFixtures/java/com/swirlds/common/test/fixtures/TestRecycleBin.java +++ b/platform-sdk/swirlds-common/src/testFixtures/java/com/swirlds/common/test/fixtures/TestRecycleBin.java @@ -47,4 +47,10 @@ private TestRecycleBin() {} public void recycle(@NonNull final Path path) throws IOException { FileUtils.deleteDirectory(path); } + + /** + * {@inheritDoc} + */ + @Override + public void start() {} } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/PlatformBuilder.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/PlatformBuilder.java index 49ffc518bfbe..e81530f3b713 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/PlatformBuilder.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/PlatformBuilder.java @@ -54,7 +54,6 @@ import com.swirlds.platform.state.address.AddressBookInitializer; import com.swirlds.platform.state.signed.ReservedSignedState; import com.swirlds.platform.system.Platform; -import com.swirlds.platform.system.Shutdown; import com.swirlds.platform.system.SoftwareVersion; import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.system.SwirldState; @@ -232,8 +231,8 @@ public Platform build() { // time this class is used. final BasicConfig basicConfig = configuration.getConfigData(BasicConfig.class); final StateConfig stateConfig = configuration.getConfigData(StateConfig.class); - final EmergencyRecoveryManager emergencyRecoveryManager = new EmergencyRecoveryManager( - stateConfig, new Shutdown()::shutdown, basicConfig.getEmergencyRecoveryFileLoadDir()); + final EmergencyRecoveryManager emergencyRecoveryManager = + new EmergencyRecoveryManager(stateConfig, basicConfig.getEmergencyRecoveryFileLoadDir()); try (final ReservedSignedState initialState = getInitialState( platformContext, 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 c2fbad13d5ef..9f5953565d18 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 @@ -75,8 +75,6 @@ import com.swirlds.platform.crypto.CryptoStatic; import com.swirlds.platform.crypto.KeysAndCerts; import com.swirlds.platform.crypto.PlatformSigner; -import com.swirlds.platform.dispatch.DispatchBuilder; -import com.swirlds.platform.dispatch.DispatchConfiguration; import com.swirlds.platform.event.AncientMode; import com.swirlds.platform.event.EventCounter; import com.swirlds.platform.event.FutureEventBuffer; @@ -149,7 +147,6 @@ import com.swirlds.platform.stats.StatConstructor; import com.swirlds.platform.system.InitTrigger; import com.swirlds.platform.system.Platform; -import com.swirlds.platform.system.Shutdown; import com.swirlds.platform.system.SoftwareVersion; import com.swirlds.platform.system.SwirldState; import com.swirlds.platform.system.SystemExitCode; @@ -166,7 +163,7 @@ import com.swirlds.platform.system.status.actions.StartedReplayingEventsAction; import com.swirlds.platform.system.transaction.SwirldTransaction; import com.swirlds.platform.util.HashLogger; -import com.swirlds.platform.util.PlatformComponents; +import com.swirlds.platform.util.ThingsToStart; import com.swirlds.platform.wiring.NoInput; import com.swirlds.platform.wiring.PlatformWiring; import com.swirlds.platform.wiring.components.IssDetectorWiring; @@ -245,9 +242,9 @@ public class SwirldsPlatform implements Platform { private final Clearable clearAllPipelines; /** - * All components that need to be started or that have dispatch observers. + * All things that need to be started when the platform is started. */ - private final PlatformComponents components; + private final ThingsToStart thingsToStart; /** * For passing notifications between the platform and the application. @@ -332,18 +329,13 @@ public class SwirldsPlatform implements Platform { this.emergencyRecoveryManager = Objects.requireNonNull(emergencyRecoveryManager, "emergencyRecoveryManager"); final Time time = Time.getCurrent(); - final DispatchBuilder dispatchBuilder = - new DispatchBuilder(platformContext.getConfiguration().getConfigData(DispatchConfiguration.class)); - - components = new PlatformComponents(dispatchBuilder); + thingsToStart = new ThingsToStart(); // FUTURE WORK: use a real thread manager here final ThreadManager threadManager = getStaticThreadManager(); notificationEngine = NotificationEngine.buildEngine(threadManager); - dispatchBuilder.registerObservers(this); - final StateConfig stateConfig = platformContext.getConfiguration().getConfigData(StateConfig.class); final String actualMainClassName = stateConfig.getMainClassName(mainClassName); @@ -358,8 +350,10 @@ public class SwirldsPlatform implements Platform { notificationEngine.dispatch(PlatformStatusChangeListener.class, new PlatformStatusChangeNotification(s)); emergencyState.platformStatusChanged(s); }; - platformStatusManager = - components.add(new PlatformStatusManager(platformContext, time, threadManager, statusChangeConsumer)); + platformStatusManager = thingsToStart.add( + new PlatformStatusManager(platformContext, time, threadManager, statusChangeConsumer)); + + thingsToStart.add(Objects.requireNonNull(recycleBin)); this.metrics = platformContext.getMetrics(); @@ -371,8 +365,6 @@ public class SwirldsPlatform implements Platform { registerAddressBookMetrics(metrics, currentAddressBook, selfId); - components.add(Objects.requireNonNull(recycleBin)); - final ConsensusMetrics consensusMetrics = new ConsensusMetricsImpl(this.selfId, metrics); final SyncMetrics syncMetrics = new SyncMetrics(metrics); @@ -475,7 +467,7 @@ public class SwirldsPlatform implements Platform { final LatestCompleteStateNexus latestCompleteState = new LatestCompleteStateNexus(stateConfig, platformContext.getMetrics()); - platformWiring = components.add(new PlatformWiring(platformContext)); + platformWiring = thingsToStart.add(new PlatformWiring(platformContext)); final boolean useOldStyleIntakeQueue = eventConfig.useOldStyleIntakeQueue(); @@ -488,7 +480,7 @@ public class SwirldsPlatform implements Platform { .setHandler(event -> platformWiring.getGossipEventInput().put(event)) .setMetricsConfiguration(new QueueThreadMetricsConfiguration(metrics).enableMaxSizeMetric()) .build(); - components.add(oldStyleIntakeQueue); + thingsToStart.add(oldStyleIntakeQueue); } else { oldStyleIntakeQueue = null; @@ -522,10 +514,7 @@ public class SwirldsPlatform implements Platform { () -> latestImmutableState.getState("PCES replay")); final EventDurabilityNexus eventDurabilityNexus = new EventDurabilityNexus(); - components.add(stateManagementComponent); - - // FUTURE WORK remove this when there are no more ShutdownRequestedTriggers being dispatched - components.add(new Shutdown()); + thingsToStart.add(stateManagementComponent); final Address address = getSelfAddress(); final String eventStreamManagerName; @@ -591,7 +580,7 @@ public class SwirldsPlatform implements Platform { stateAndRound.reservedSignedState().close(); }; - stateHashSignQueue = components.add(new QueueThreadConfiguration(threadManager) + stateHashSignQueue = thingsToStart.add(new QueueThreadConfiguration(threadManager) .setNodeId(selfId) .setComponent(PLATFORM_THREAD_POOL_NAME) .setThreadName("state_hash_sign") @@ -784,7 +773,7 @@ public class SwirldsPlatform implements Platform { platformWiring.getIssDetectorWiring().overridingState().put(initialState.reserve("initialize issDetector")); // We don't want to invoke these callbacks until after we are starting up. - components.add((Startable) () -> { + thingsToStart.add((Startable) () -> { // If we loaded from disk then call the appropriate dispatch. // Let the app know that a state was loaded. notificationEngine.dispatch( @@ -809,7 +798,7 @@ public class SwirldsPlatform implements Platform { Pair.of(transactionPool, "transactionPool"))); if (platformContext.getConfiguration().getConfigData(ThreadConfig.class).jvmAnchor()) { - components.add(new JvmAnchor(threadManager)); + thingsToStart.add(new JvmAnchor(threadManager)); } // To be removed once the GUI component is better integrated with the platform. @@ -1034,7 +1023,7 @@ private void haltRequested(final String reason) { public void start() { logger.info(STARTUP.getMarker(), "Starting platform {}", selfId); - components.start(); + thingsToStart.start(); metrics.start(); @@ -1052,7 +1041,7 @@ public void start() { * */ public void performPcesRecovery() { - components.start(); + thingsToStart.start(); replayPreconsensusEvents(); try (final ReservedSignedState reservedState = latestImmutableState.getState("Get PCES recovery state")) { diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/DispatchBuilder.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/DispatchBuilder.java deleted file mode 100644 index e181eb582fcf..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/DispatchBuilder.java +++ /dev/null @@ -1,522 +0,0 @@ -/* - * Copyright (C) 2022-2024 Hedera Hashgraph, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.swirlds.platform.dispatch; - -import com.swirlds.base.state.MutabilityException; -import com.swirlds.base.state.Mutable; -import com.swirlds.base.state.Startable; -import com.swirlds.platform.dispatch.flowchart.DispatchFlowchart; -import com.swirlds.platform.dispatch.types.TriggerEight; -import com.swirlds.platform.dispatch.types.TriggerFive; -import com.swirlds.platform.dispatch.types.TriggerFour; -import com.swirlds.platform.dispatch.types.TriggerNine; -import com.swirlds.platform.dispatch.types.TriggerOne; -import com.swirlds.platform.dispatch.types.TriggerSeven; -import com.swirlds.platform.dispatch.types.TriggerSix; -import com.swirlds.platform.dispatch.types.TriggerTen; -import com.swirlds.platform.dispatch.types.TriggerThree; -import com.swirlds.platform.dispatch.types.TriggerTwo; -import com.swirlds.platform.dispatch.types.TriggerZero; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.lang.invoke.LambdaMetafactory; -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; -import java.lang.reflect.Method; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -/** - * Manages the construction of dispatch methods. Useful for linking together various platform - * components with minimal performance overhead. - */ -public class DispatchBuilder implements Mutable, Startable { - - private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); - - private static final Runnable MUTABILITY_GUARD = () -> { - throw new MutabilityException("no dispatch is permitted prior to the dispatcher being started"); - }; - - private boolean immutable = false; - - private final Map>, List>> observers = new HashMap<>(); - - private final DispatchFlowchart flowchart; - - private static final Path FLOWCHART_LOCATION = Path.of("platform-components.mermaid"); - - /** - * Create a new dispatch builder. - * - * @param configuration - * dispatch configuration - * @throws NullPointerException in case {@code configuration} parameter is {@code null} - */ - public DispatchBuilder(final DispatchConfiguration configuration) { - Objects.requireNonNull(configuration, "configuration must not be null"); - if (configuration.flowchartEnabled()) { - flowchart = new DispatchFlowchart(configuration); - } else { - flowchart = null; - } - } - - /** - *

    - * Register a new observer. Multiple observers for the same type of dispatch event may be registered. - *

    - * - *

    - * May only be called before {@link #start()} is invoked. - *

    - * - *

    - * It is thread safe to leak a reference to "this" in a constructor via this method, since observers are not - * permitted to be used until after the dispatch builder has been sealed. - *

    - * - * @param owner - * the object (or the class of the object) that "owns" the observer. This - * information is used only for generating documentation, and does not affect the routing of dispatches. - * It is safe to pass "this" in a constructor for this parameter, as only the class if the object is used. - * @param triggerClass - * the type of the trigger - * @param observer - * the observer - * @param - * the base functional interface for the trigger, - * e.g. {@link TriggerZero}, {@link TriggerOne}, etc. - * @param - * a specific trigger type, should inherit from the BASE_INTERFACE - * @return this object - * @throws MutabilityException - * if called after {@link #start()} - */ - public , TRIGGER_CLASS extends BASE_INTERFACE> - DispatchBuilder registerObserver( - final Object owner, final Class triggerClass, final BASE_INTERFACE observer) { - - registerObserver(owner, triggerClass, observer, null); - return this; - } - - /** - *

    - * Register a new observer. Multiple observers for the same type of dispatch event may be registered. - *

    - * - *

    - * May only be called before {@link #start()} is invoked. - *

    - * - *

    - * It is thread safe to leak a reference to "this" in a constructor via this method, since observers are not - * permitted to be used until after the dispatch builder has been sealed. - *

    - * - * @param owner - * the object (or the class of the object) that "owns" the observer. This - * information is used only for generating documentation, and does not affect the routing of dispatches. - * It is safe to pass "this" in a constructor for this parameter, as only the class if the object is used. - * @param triggerClass - * the type of the trigger - * @param observer - * the observer - * @param comment - * a comment used to enhance the dispatch flowchart - * @param - * the base functional interface for the trigger, - * e.g. {@link TriggerZero}, {@link TriggerOne}, etc. - * @param - * a specific trigger type, should inherit from the BASE_INTERFACE - * @return this object - * @throws MutabilityException - * if called after {@link #start()} - * @throws NullPointerException if any of the following parameters are {@code null}. - *
      - *
    • {@code triggerClass}
    • - *
    • {@code observer}
    • - *
    - * - */ - public , TRIGGER_CLASS extends BASE_INTERFACE> - DispatchBuilder registerObserver( - final Object owner, - final Class triggerClass, - final BASE_INTERFACE observer, - final String comment) { - - throwIfImmutable("observer can only be registered while this object is mutable"); - Objects.requireNonNull(triggerClass, "triggerClass must not be null"); - Objects.requireNonNull(observer, "observer must not be null"); - - if (flowchart != null && isMutable()) { - flowchart.registerObserver(owner, triggerClass, comment); - } - - getObserverList(triggerClass).add(observer); - - return this; - } - - /** - * Register all of an object's public observer methods annotated with {@link Observer}. - * This is a convenience method -- it's perfectly acceptable to register each observer - * one at a time via {@link #registerObserver(Object, Class, Trigger)}. - * - * @param object - * the object with observers - * @return this object - */ - public DispatchBuilder registerObservers(final Object object) { - throwIfImmutable("observers can only be registered while this object is mutable"); - Objects.requireNonNull(object, "object must not be null"); - - for (final Method method : object.getClass().getDeclaredMethods()) { - - final Observer annotation = method.getAnnotation(Observer.class); - if (annotation == null) { - continue; - } - - if (annotation.value().length == 0) { - throw new IllegalArgumentException("No triggers specified. At least one trigger type " - + "must be passed to each @Observer annotation."); - } - - final String comment = annotation.comment(); - - for (final Class> triggerClass : annotation.value()) { - registerAnnotatedClassMethod(object, method, triggerClass, comment); - } - } - - return this; - } - - /** - * Register an annotated class member function as an observer. - * - * @param object - * the object that is the observer - * @param method - * the method that should be called when the dispatch is triggered - * @param triggerClass - * the type of the trigger - * @param comment - * a comment used to enhance the dispatch flowchart - */ - private void registerAnnotatedClassMethod( - final Object object, - final Method method, - final Class> triggerClass, - final String comment) { - try { - final MethodType factoryType = MethodType.methodType(triggerClass, object.getClass()); - final MethodType methodType = MethodType.methodType(method.getReturnType(), method.getParameterTypes()); - final MethodType genericMethodType = methodType.generic().changeReturnType(methodType.returnType()); - final MethodHandle target = LOOKUP.unreflect(method); - - final Trigger trigger = (Trigger) LambdaMetafactory.metafactory( - LOOKUP, "dispatch", factoryType, genericMethodType, target, methodType) - .getTarget() - .bindTo(object) - .invoke(); - - getObserverList(triggerClass).add(trigger); - - if (flowchart != null && isMutable()) { - flowchart.registerObserver(object, triggerClass, comment); - } - - } catch (final Throwable e) { - // factoryHandle.invoke() forces us to catch Throwable. >:( It doesn't really matter - // what this throws, if anything at all fails we can't recover. - throw new RuntimeException("unable to register observer " + object.getClass() + "." + method.getName(), e); - } - } - - /** - * Get a dispatcher method for a given type. Will call into all registered observers for this type, - * even those registered after this dispatcher is returned. Method returned is a no-op if no observers - * are ever registered. The dispatcher returned will throw a mutability exception if invoked prior to - * {@link #start()} being called. - * - * @param owner - * the object (or the class of the object) that "owns" the dispatcher. - * This information is used only for generating documentation, and does not affect the routing of dispatches. - * It is safe to pass "this" in a constructor for this parameter, as only the class if the object is used. - * @param triggerClass - * the type of the dispatch event - * @param - * the base functional interface for the dispatcher, - * e.g. {@link TriggerZero}, {@link TriggerOne}, etc. - * @param - * a specific dispatcher type, should inherit from the BASE_INTERFACE - * @return a dispatch method, not null even if no observers have been registered for this event - */ - public , DISPATCHER_TYPE extends BASE_INTERFACE> - BASE_INTERFACE getDispatcher(final Object owner, final Class triggerClass) { - return getDispatcher(owner, triggerClass, null); - } - - /** - * Get a dispatcher method for a given type. Will call into all registered observers for this type, - * even those registered after this dispatcher is returned. Method returned is a no-op if no observers - * are ever registered. The dispatcher returned will throw a mutability exception if invoked prior to - * {@link #start()} being called. - * - * @param owner - * the object (or the class of the object) that "owns" the dispatcher. - * This information is used only for generating documentation, and does not affect the routing of dispatches. - * It is safe to pass "this" in a constructor for this parameter, as only the class if the object is used. - * @param triggerClass - * the type of the dispatch event - * @param - * the base functional interface for the dispatcher, - * e.g. {@link TriggerZero}, {@link TriggerOne}, etc. - * @param - * a specific dispatcher type, should inherit from the BASE_INTERFACE - * @param comment - * a comment on how the dispatch is being used, used to enhance the dispatch flowchart - * @return a dispatch method, not null even if no observers have been registered for this event - */ - @SuppressWarnings("unchecked") - public , DISPATCHER_TYPE extends BASE_INTERFACE> - BASE_INTERFACE getDispatcher( - final Object owner, final Class triggerClass, final String comment) { - - Objects.requireNonNull(owner, "owner must not be null"); - Objects.requireNonNull(triggerClass, "dispatchType must not be null"); - - if (flowchart != null && isMutable()) { - flowchart.registerDispatcher(owner, triggerClass, comment); - } - - final List> observerList = getObserverList(triggerClass); - - if (TriggerZero.class.isAssignableFrom(triggerClass)) { - return (BASE_INTERFACE) (TriggerZero) () -> { - for (final Trigger observer : observerList) { - ((TriggerZero) observer).dispatch(); - } - }; - } else if (TriggerOne.class.isAssignableFrom(triggerClass)) { - return (BASE_INTERFACE) (TriggerOne) (a) -> { - for (final Trigger observer : observerList) { - ((TriggerOne) observer).dispatch(a); - } - }; - } else if (TriggerTwo.class.isAssignableFrom(triggerClass)) { - return (BASE_INTERFACE) (TriggerTwo) (a, b) -> { - for (final Trigger observer : observerList) { - ((TriggerTwo) observer).dispatch(a, b); - } - }; - } else if (TriggerThree.class.isAssignableFrom(triggerClass)) { - return (BASE_INTERFACE) (TriggerThree) (a, b, c) -> { - for (final Trigger observer : observerList) { - ((TriggerThree) observer).dispatch(a, b, c); - } - }; - } else if (TriggerFour.class.isAssignableFrom(triggerClass)) { - return (BASE_INTERFACE) (TriggerFour) (a, b, c, d) -> { - for (final Trigger observer : observerList) { - ((TriggerFour) observer).dispatch(a, b, c, d); - } - }; - } else if (TriggerFive.class.isAssignableFrom(triggerClass)) { - return (BASE_INTERFACE) (TriggerFive) (a, b, c, d, e) -> { - for (final Trigger observer : observerList) { - ((TriggerFive) observer).dispatch(a, b, c, d, e); - } - }; - } else if (TriggerSix.class.isAssignableFrom(triggerClass)) { - return (BASE_INTERFACE) (TriggerSix) (a, b, c, d, e, f) -> { - for (final Trigger observer : observerList) { - ((TriggerSix) observer).dispatch(a, b, c, d, e, f); - } - }; - } else if (TriggerSeven.class.isAssignableFrom(triggerClass)) { - return (BASE_INTERFACE) - (TriggerSeven) (a, b, c, d, e, f, g) -> { - for (final Trigger observer : observerList) { - ((TriggerSeven) observer) - .dispatch(a, b, c, d, e, f, g); - } - }; - } else if (TriggerEight.class.isAssignableFrom(triggerClass)) { - return (BASE_INTERFACE) (TriggerEight) - (a, b, c, d, e, f, g, h) -> { - for (final Trigger observer : observerList) { - ((TriggerEight) observer) - .dispatch(a, b, c, d, e, f, g, h); - } - }; - } else if (TriggerNine.class.isAssignableFrom(triggerClass)) { - return (BASE_INTERFACE) (TriggerNine< - Object, Object, Object, Object, Object, Object, Object, Object, Object>) - (a, b, c, d, e, f, g, h, i) -> { - for (final Trigger observer : observerList) { - ((TriggerNine) - observer) - .dispatch(a, b, c, d, e, f, g, h, i); - } - }; - } else if (TriggerTen.class.isAssignableFrom(triggerClass)) { - return (BASE_INTERFACE) - (TriggerTen) - (a, b, c, d, e, f, g, h, i, j) -> { - for (final Trigger observer : observerList) { - ((TriggerTen< - Object, - Object, - Object, - Object, - Object, - Object, - Object, - Object, - Object, - Object>) - observer) - .dispatch(a, b, c, d, e, f, g, h, i, j); - } - }; - } else { - throw new IllegalStateException("unhandled dispatch type " + triggerClass); - } - } - - /** - * Get a list of observers for a given trigger type. If that dispatch type does not yet have a list of observers - * then create one and insert it into the map of observer lists. - * - * @param triggerClass - * the type of trigger - * @return a list of observers for the trigger type, calling this method more than once for the same - * trigger type always returns the same list instance - */ - private List> getObserverList(final Class> triggerClass) { - final List> observerList = observers.get(triggerClass); - if (observerList != null) { - return observerList; - } - - final List> newObserverList = new ArrayList<>(); - - if (isMutable()) { - // Add a special observer that will cause premature dispatch to throw. This observer - // is removed when the dispatch builder is started. Performance wise, this is superior - // to the addition of an "if (boolean)" guard, since this extra lambda function has - // zero performance impact after boot time. - addMutabilityGuard(triggerClass, newObserverList); - } - - observers.put(triggerClass, newObserverList); - return newObserverList; - } - - /** - * Add a special observer that will cause premature dispatch to throw. This observer - * is removed when the dispatch builder is started. Performance wise, this is superior - * to the addition of an "if (boolean)" guard, since this extra lambda function has - * zero performance impact after boot time. - * - * @param triggerClass - * the trigger class - * @param newObserverList - * the list of observers for the dispatcher - */ - @SuppressWarnings("Convert2MethodRef") - private static void addMutabilityGuard( - final Class> triggerClass, final List> newObserverList) { - - if (TriggerZero.class.isAssignableFrom(triggerClass)) { - newObserverList.add((TriggerZero) () -> MUTABILITY_GUARD.run()); - } else if (TriggerOne.class.isAssignableFrom(triggerClass)) { - newObserverList.add((TriggerOne) (a) -> MUTABILITY_GUARD.run()); - } else if (TriggerTwo.class.isAssignableFrom(triggerClass)) { - newObserverList.add((TriggerTwo) (a, b) -> MUTABILITY_GUARD.run()); - } else if (TriggerThree.class.isAssignableFrom(triggerClass)) { - newObserverList.add((TriggerThree) (a, b, c) -> MUTABILITY_GUARD.run()); - } else if (TriggerFour.class.isAssignableFrom(triggerClass)) { - newObserverList.add((TriggerFour) (a, b, c, d) -> MUTABILITY_GUARD.run()); - } else if (TriggerFive.class.isAssignableFrom(triggerClass)) { - newObserverList.add((TriggerFive) (a, b, c, d, e) -> MUTABILITY_GUARD.run()); - } else if (TriggerSix.class.isAssignableFrom(triggerClass)) { - newObserverList.add((TriggerSix) (a, b, c, d, e, f) -> MUTABILITY_GUARD.run()); - } else if (TriggerSeven.class.isAssignableFrom(triggerClass)) { - newObserverList.add((TriggerSeven) (a, b, c, d, e, f, g) -> MUTABILITY_GUARD.run()); - } else if (TriggerEight.class.isAssignableFrom(triggerClass)) { - newObserverList.add( - (TriggerEight) (a, b, c, d, e, f, g, h) -> MUTABILITY_GUARD.run()); - } else if (TriggerNine.class.isAssignableFrom(triggerClass)) { - newObserverList.add( - (TriggerNine) (a, b, c, d, e, f, g, h, i) -> MUTABILITY_GUARD.run()); - } else if (TriggerTen.class.isAssignableFrom(triggerClass)) { - newObserverList.add((TriggerTen) - (a, b, c, d, e, f, g, h, i, j) -> MUTABILITY_GUARD.run()); - } else { - throw new IllegalStateException("unhandled dispatch type " + triggerClass); - } - } - - /** - * Once started, dispatchers are permitted to start invoking callbacks. - * - * @throws MutabilityException - * if called more than once - */ - @Override - public void start() { - throwIfImmutable("start() should only be called once"); - immutable = true; - - // Remove the preventPrematureDispatch() lambda that was added to each observer list. - // The implementation for observers is ArrayList. It's mildly less efficient to remove the first - // element of an array list, for lists of this size. However, since we only pay that cost at boot time, - // it's much better to go with an array list over a linked list for the enhanced runtime performance. - // Iterating over an array list is more efficient than iterating over a linked list. Although the - // difference may seem small, this code is used in performance critical areas. - for (final List> observerList : observers.values()) { - observerList.remove(0); - } - - if (flowchart != null) { - try { - flowchart.writeFlowchart(FLOWCHART_LOCATION); - } catch (final IOException e) { - throw new UncheckedIOException("unable to generate dispatch flowchart", e); - } - } - } - - /** - * {@inheritDoc} - */ - @Override - public boolean isImmutable() { - return immutable; - } -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/DispatchConfiguration.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/DispatchConfiguration.java deleted file mode 100644 index 1a8c704898af..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/DispatchConfiguration.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (C) 2022-2024 Hedera Hashgraph, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.swirlds.platform.dispatch; - -import com.swirlds.config.api.ConfigData; -import com.swirlds.config.api.ConfigProperty; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; - -/** - * Configuration for dispatches and the dispatch builder. - * - * @param flowchartEnabled - * if true then generate a visual flowchart showing the dispatch configuration of platform components - * @param flowchartTriggerWhitelist - * a whitelist of trigger types when building the dispatch flowchart, ":" separated - * @param flowchartTriggerBlacklist - * a blacklist of trigger types when building the dispatch flowchart, ":" separated - * @param flowchartObjectWhitelist - * a whitelist of observer/dispatcher types when building the dispatch flowchart, ":" separated - * @param flowchartObjectBlacklist - * a blacklist of observer/dispatcher types when building the dispatch flowchart, ":" separated - */ -@ConfigData("dispatch") -public record DispatchConfiguration( - @ConfigProperty(defaultValue = "false") boolean flowchartEnabled, - @ConfigProperty(defaultValue = "") String flowchartTriggerWhitelist, - @ConfigProperty(defaultValue = "") String flowchartTriggerBlacklist, - @ConfigProperty(defaultValue = "") String flowchartObjectWhitelist, - @ConfigProperty(defaultValue = "") String flowchartObjectBlacklist) { - - /** - * @return a set of all whitelisted flowchart triggers - */ - public Set getFlowchartTriggerWhitelistSet() { - return parseStringList(flowchartTriggerWhitelist); - } - - /** - * @return a set of all blacklisted flowchart triggers - */ - public Set getFlowchartTriggerBlacklistSet() { - return parseStringList(flowchartTriggerBlacklist); - } - - /** - * @return a set of all whitelisted flowchart objects - */ - public Set getFlowchartObjectWhitelistSet() { - return parseStringList(flowchartObjectWhitelist); - } - - /** - * @return a set of all blacklisted flowchart objects - */ - public Set getFlowchartObjectBlacklistSet() { - return parseStringList(flowchartObjectBlacklist); - } - - /** - * Parse a ":" delimited list of strings. - */ - private static Set parseStringList(final String commaSeparatedStrings) { - final Set strings = new HashSet<>(); - if (!commaSeparatedStrings.equals("")) { - for (final String string : commaSeparatedStrings.split(":")) { - if (!Objects.equals(string, "")) { - strings.add(string); - } - } - } - return strings; - } -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/Observer.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/Observer.java deleted file mode 100644 index 78b0b8085167..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/Observer.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (C) 2016-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.dispatch; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Use this annotation to signal that a method should be called when a dispatch is triggered. - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.METHOD) -public @interface Observer { - - /** - * The type of the dispatcher(s). Minimum one trigger must be supplied. - */ - Class>[] value(); - - /** - * An optional comment describing what the observer is doing. - */ - String comment() default ""; -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/ObserverComment.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/ObserverComment.java deleted file mode 100644 index 887902a0e7ce..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/ObserverComment.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (C) 2016-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.dispatch; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Use this annotation to add a comment to an observer, used when generating the dispatcher flowchart. - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.METHOD) -public @interface ObserverComment { - - /** - * A comment used to enhance the dispatcher flowchart. - */ - String value(); -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/Trigger.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/Trigger.java deleted file mode 100644 index f0e680babb8d..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/Trigger.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2018-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.dispatch; - -import com.swirlds.platform.dispatch.types.TriggerEight; -import com.swirlds.platform.dispatch.types.TriggerFive; -import com.swirlds.platform.dispatch.types.TriggerFour; -import com.swirlds.platform.dispatch.types.TriggerNine; -import com.swirlds.platform.dispatch.types.TriggerOne; -import com.swirlds.platform.dispatch.types.TriggerSeven; -import com.swirlds.platform.dispatch.types.TriggerSix; -import com.swirlds.platform.dispatch.types.TriggerTen; -import com.swirlds.platform.dispatch.types.TriggerThree; -import com.swirlds.platform.dispatch.types.TriggerTwo; -import com.swirlds.platform.dispatch.types.TriggerZero; - -/** - * The base interface for all dispatcher types. - */ -public sealed interface Trigger> - permits TriggerZero, - TriggerOne, - TriggerTwo, - TriggerThree, - TriggerFour, - TriggerFive, - TriggerSix, - TriggerSeven, - TriggerEight, - TriggerNine, - TriggerTen {} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/flowchart/CommentedTrigger.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/flowchart/CommentedTrigger.java deleted file mode 100644 index 63aca0e8e05d..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/flowchart/CommentedTrigger.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2016-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.dispatch.flowchart; - -/** - * A trigger and a comment about how the trigger is being used. - * - * @param trigger - * the trigger - * @param comment - * the comment on how the trigger is being used, or null if there is no comment - */ -public record CommentedTrigger(Class trigger, String comment) { - - /** - * {@inheritDoc} - */ - @Override - public boolean equals(final Object obj) { - if (obj == null || obj.getClass() != CommentedTrigger.class) { - return false; - } - return trigger == ((CommentedTrigger) obj).trigger; - } - - /** - * {@inheritDoc} - */ - @Override - public int hashCode() { - return trigger.hashCode(); - } -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/flowchart/DispatchFlowchart.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/flowchart/DispatchFlowchart.java deleted file mode 100644 index 7d9193805ac1..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/flowchart/DispatchFlowchart.java +++ /dev/null @@ -1,312 +0,0 @@ -/* - * Copyright (C) 2022-2024 Hedera Hashgraph, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.swirlds.platform.dispatch.flowchart; - -import com.swirlds.platform.dispatch.DispatchConfiguration; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Objects; -import java.util.Set; - -/** - * This class builds a mermaid flowchart showing dispatch configuration. - */ -public class DispatchFlowchart { - - private static final String INDENTATION = " "; - private static final String COMMENT = "%%"; - - private final Set> uniqueObjects = new HashSet<>(); - private final Set> uniqueTriggers = new HashSet<>(); - private final Map, Set> dispatcherMap = new HashMap<>(); - private final Map, Set> observerMap = new HashMap<>(); - - private final Set triggerWhitelist; - private final Set triggerBlacklist; - - private final Set objectWhitelist; - private final Set objectBlacklist; - - public DispatchFlowchart(final DispatchConfiguration dispatchConfiguration) { - - triggerWhitelist = dispatchConfiguration.getFlowchartTriggerWhitelistSet(); - triggerBlacklist = dispatchConfiguration.getFlowchartTriggerBlacklistSet(); - objectWhitelist = dispatchConfiguration.getFlowchartObjectWhitelistSet(); - objectBlacklist = dispatchConfiguration.getFlowchartObjectBlacklistSet(); - - if (!triggerWhitelist.isEmpty() && !triggerBlacklist.isEmpty()) { - throw new IllegalStateException( - "Either trigger whitelist or trigger blacklist may be specified, but not both"); - } - - if (!objectWhitelist.isEmpty() && !objectBlacklist.isEmpty()) { - throw new IllegalStateException( - "Either object whitelist or object blacklist may be specified, but not both"); - } - } - - /** - * Check if a trigger is restricted by a whitelist or a blacklist. - */ - private boolean isTriggerRestricted(final Class triggerClass) { - if (!triggerWhitelist.isEmpty()) { - return !triggerWhitelist.contains(triggerClass.getSimpleName()); - } else if (!triggerBlacklist.isEmpty()) { - return triggerBlacklist.contains(triggerClass.getSimpleName()); - } else { - return false; - } - } - - /** - * Check if an object is restricted by a whitelist or a blacklist. - */ - private boolean isObjectRestricted(final Class objectClass) { - if (!objectWhitelist.isEmpty()) { - return !objectWhitelist.contains(objectClass.getSimpleName()); - } else if (!objectBlacklist.isEmpty()) { - return objectBlacklist.contains(objectClass.getSimpleName()); - } else { - return false; - } - } - - /** - * Register a dispatcher. - * - * @param owner - * the object or the class of the object that "owns" the dispatcher. It is safe to pass "this" - * in a constructor for this parameter, as only the class if the object is used. - * @param triggerClass - * the trigger class of the dispatch - * @param comment - * an optional comment used to enhance the flowchart - */ - public void registerDispatcher(final Object owner, final Class triggerClass, final String comment) { - - registerTriggerLinkage(owner, triggerClass, comment, dispatcherMap); - } - - /** - * Register a dispatch observer. - * - * @param owner - * the object or the class of the object that "owns" the observer. It is safe to pass "this" - * in a constructor for this parameter, as only the class if the object is used. - * @param triggerClass - * the trigger class of the dispatch - * @param comment - * an optional comment used to enhance the flowchart - */ - public void registerObserver(final Object owner, final Class triggerClass, final String comment) { - - registerTriggerLinkage(owner, triggerClass, comment, observerMap); - } - - /** - * Register a linkage between an observer/dispatcher and a trigger. - * - * @param owner - * the object or the class of the object that "owns" the observer or the dispatcher. - * It is safe to pass "this" in a constructor for this parameter, as only the class if the object is used. - * @param triggerClass - * the trigger class - * @param comment - * an optional comment on the linkage - * @param map - * a map containing linkages for observers or dispatchers - * @throws NullPointerException in case {@code owner} parameter is {@code null} - */ - private void registerTriggerLinkage( - final Object owner, - final Class triggerClass, - final String comment, - final Map, Set> map) { - - Objects.requireNonNull(owner, "owner must not be null"); - - final Class ownerClass; - if (owner instanceof final Class cls) { - ownerClass = cls; - } else { - ownerClass = owner.getClass(); - } - - if (isObjectRestricted(ownerClass) || isTriggerRestricted(triggerClass)) { - return; - } - - uniqueObjects.add(ownerClass); - uniqueTriggers.add(triggerClass); - - final Set triggersForOwner = map.computeIfAbsent(ownerClass, k -> new HashSet<>()); - - triggersForOwner.add(new CommentedTrigger(triggerClass, comment)); - } - - /** - * Draw an object (either an observer or a dispatcher, or both). - * - * @param sb - * a string builder where the mermaid file is being assembled - * @param objectClass - * the class of the object - */ - private static void drawObject(final StringBuilder sb, final Class objectClass) { - sb.append(INDENTATION) - .append(COMMENT) - .append(" ") - .append(objectClass.getName()) - .append("\n"); - sb.append(INDENTATION).append(objectClass.getSimpleName()).append("\n"); - sb.append(INDENTATION) - .append("style ") - .append(objectClass.getSimpleName()) - .append(" fill:#362,stroke:#000,stroke-width:2px,color:#fff\n"); - } - - /** - * Draw a trigger. - * - * @param sb - * a string builder where the mermaid file is being assembled - * @param triggerClass - * the class of the trigger - */ - private static void drawTrigger(final StringBuilder sb, final Class triggerClass) { - final String name = triggerClass.getSimpleName(); - final String fullName = triggerClass.getName(); - - sb.append(INDENTATION).append(COMMENT).append(" ").append(fullName).append("\n"); - sb.append(INDENTATION).append(name).append("{{").append(name).append("}}\n"); - sb.append(INDENTATION) - .append("style ") - .append(name) - .append(" fill:#36a,stroke:#000,stroke-width:2px,color:#fff\n"); - } - - /** - * Draw an arrow from a dispatcher to a trigger. - * - * @param sb - * a string builder where the mermaid file is being assembled - * @param dispatchClass - * the dispatching class - * @param trigger - * the trigger that is being dispatched - */ - private static void drawDispatchArrow( - final StringBuilder sb, final Class dispatchClass, final CommentedTrigger trigger) { - - sb.append(INDENTATION).append(dispatchClass.getSimpleName()); - - final String comment = trigger.comment(); - if (trigger.comment() == null || trigger.comment().equals("")) { - sb.append(" --> "); - } else { - validateComment(dispatchClass, comment); - sb.append(" -- \"").append(comment).append("\" --> "); - } - - sb.append(trigger.trigger().getSimpleName()).append("\n"); - } - - /** - * Draw an arrow from a trigger to an observer. - * - * @param sb - * a string builder where the mermaid file is being assembled - * @param observerClass - * the class observing the trigger - * @param trigger - * the trigger being observed - */ - private static void drawObserverArrow( - final StringBuilder sb, final Class observerClass, final CommentedTrigger trigger) { - - sb.append(INDENTATION).append(trigger.trigger().getSimpleName()); - - final String comment = trigger.comment(); - if (comment == null || comment.equals("")) { - sb.append(" -.-> "); - } else { - validateComment(observerClass, comment); - sb.append(" -. \"").append(comment).append("\" .-> "); - } - - sb.append(observerClass.getSimpleName()).append("\n"); - } - - private static void validateComment(final Class clazz, final String comment) { - if (comment.contains("\"")) { - throw new IllegalArgumentException( - "Dispatcher comments for class " + clazz + " contain illegal \" character(s)."); - } - } - - /** - * Build a mermaid flowchart. - * - * @return a string containing a flowchart in mermaid format - */ - public String buildFlowchart() { - final StringBuilder sb = new StringBuilder(); - - sb.append("flowchart TD\n"); - - sb.append("\n").append(INDENTATION).append(COMMENT).append(" observing and dispatching objects\n"); - uniqueObjects.stream() - .sorted(Comparator.comparing(Class::getSimpleName)) - .forEachOrdered(object -> drawObject(sb, object)); - - sb.append("\n").append(INDENTATION).append(COMMENT).append(" triggers\n"); - uniqueTriggers.stream() - .sorted(Comparator.comparing(Class::getSimpleName)) - .forEachOrdered(object -> drawTrigger(sb, object)); - - sb.append("\n").append(INDENTATION).append(COMMENT).append(" links from dispatchers to triggers\n"); - dispatcherMap.keySet().stream() - .sorted(Comparator.comparing(Class::getSimpleName)) - .forEach(dispatcher -> dispatcherMap.get(dispatcher).stream() - .sorted(Comparator.comparing(a -> a.trigger().getSimpleName())) - .forEach(trigger -> drawDispatchArrow(sb, dispatcher, trigger))); - - sb.append("\n").append(INDENTATION).append(COMMENT).append(" links from triggers to observers\n"); - observerMap.keySet().stream() - .sorted(Comparator.comparing(Class::getSimpleName)) - .forEach(observer -> observerMap.get(observer).stream() - .sorted(Comparator.comparing(a -> a.trigger().getSimpleName())) - .forEach(trigger -> drawObserverArrow(sb, observer, trigger))); - - return sb.toString(); - } - - /** - * Write a mermaid flowchart to a file. - * - * @param file - * the location of the file - */ - public void writeFlowchart(final Path file) throws IOException { - Files.writeString(file, buildFlowchart()); - } -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/control/HaltRequestedConsumer.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/control/HaltRequestedConsumer.java deleted file mode 100644 index a489bc9f07b9..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/control/HaltRequestedConsumer.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) 2016-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.dispatch.triggers.control; - -/** - * Sends dispatches when a halt is requested. A halt causes the node to stop doing work without stopping the JVM. - * Once halted, all work permanently stops until the node is rebooted. - */ -@FunctionalInterface -public interface HaltRequestedConsumer { - - /** - * The system has been asked to halt. A halt causes the node to stop doing work without stopping the JVM. - * Once halted, all work permanently stops until the node is rebooted. - * - * @param reason - * the reason why the halt is being requested - */ - void haltRequested(String reason); -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/control/ShutdownRequestedTrigger.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/control/ShutdownRequestedTrigger.java deleted file mode 100644 index 145417f14c8e..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/control/ShutdownRequestedTrigger.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (C) 2016-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.dispatch.triggers.control; - -import com.swirlds.platform.dispatch.types.TriggerTwo; -import com.swirlds.platform.system.SystemExitCode; -import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.Nullable; - -/** - * Sends dispatches when a shutdown is requested. - */ -@FunctionalInterface -public interface ShutdownRequestedTrigger extends TriggerTwo { - - /** - * Send a dispatch requesting that the system shut down immediately. - * - * @param reason - * A human-readable reason why the shutdown is being requested - * @param exitCode - * the exit code to return - */ - @Override - void dispatch(@Nullable String reason, @NonNull SystemExitCode exitCode); -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/error/DeadlockTrigger.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/error/DeadlockTrigger.java deleted file mode 100644 index 10caa2855873..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/error/DeadlockTrigger.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2018-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.dispatch.triggers.error; - -import com.swirlds.platform.dispatch.types.TriggerZero; - -/** - * Triggered when a deadlock is detected. - */ -@FunctionalInterface -public interface DeadlockTrigger extends TriggerZero { - - /** - * This method is called when a deadlock is detected. - */ - @Override - void dispatch(); -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerEight.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerEight.java deleted file mode 100644 index 815f5e0bc49f..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerEight.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2016-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.dispatch.types; - -import com.swirlds.platform.dispatch.Trigger; - -/** - * A trigger that accepts eight arguments. - * - * @param - * the type of the first argument - * @param - * the type of the second argument - * @param - * the type of the third argument - * @param - * the type of the fourth argument - * @param - * the type of the fifth argument - * @param - * the type of the sixth argument - * @param - * the type of the seventh argument - * @param - * the type of the eighth argument - */ -@FunctionalInterface -public non-sealed interface TriggerEight extends Trigger> { - - /** - * Dispatch a trigger event. - * - * @param a - * the first argument - * @param b - * the second argument - * @param c - * the third argument - * @param d - * the fourth argument - * @param e - * the fifth argument - * @param f - * the sixth argument - * @param g - * the seventh argument - * @param h - * the eighth argument - */ - void dispatch(A a, B b, C c, D d, E e, F f, G g, H h); -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerFive.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerFive.java deleted file mode 100644 index 8dad7295b176..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerFive.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2016-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.dispatch.types; - -import com.swirlds.platform.dispatch.Trigger; - -/** - * A trigger that accepts five arguments. - * - * @param - * the type of the first argument - * @param - * the type of the second argument - * @param - * the type of the third argument - * @param - * the type of the fourth argument - * @param - * the type of the fifth argument - */ -@FunctionalInterface -public non-sealed interface TriggerFive extends Trigger> { - - /** - * Dispatch a trigger event. - * - * @param a - * the first argument - * @param b - * the second argument - * @param c - * the third argument - * @param d - * the fourth argument - * @param e - * the fifth argument - */ - void dispatch(A a, B b, C c, D d, E e); -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerFour.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerFour.java deleted file mode 100644 index 365e994ec3a4..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerFour.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2016-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.dispatch.types; - -import com.swirlds.platform.dispatch.Trigger; - -/** - * A trigger that accepts four arguments. - * - * @param - * the type of the first argument - * @param - * the type of the second argument - * @param - * the type of the third argument - * @param - * the type of the fourth argument - */ -@FunctionalInterface -public non-sealed interface TriggerFour extends Trigger> { - - /** - * Dispatch a trigger event. - * - * @param a - * the first argument - * @param b - * the second argument - * @param c - * the third argument - * @param d - * the fourth argument - */ - void dispatch(A a, B b, C c, D d); -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerNine.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerNine.java deleted file mode 100644 index 94f0f428e51e..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerNine.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2016-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.dispatch.types; - -import com.swirlds.platform.dispatch.Trigger; - -/** - * A trigger that accepts nine arguments. - * - * @param - * the type of the first argument - * @param - * the type of the second argument - * @param - * the type of the third argument - * @param - * the type of the fourth argument - * @param - * the type of the fifth argument - * @param - * the type of the sixth argument - * @param - * the type of the seventh argument - * @param - * the type of the eighth argument - * @param - * the type of the ninth argument - */ -@FunctionalInterface -public non-sealed interface TriggerNine - extends Trigger> { - - /** - * Dispatch a trigger event. - * - * @param a - * the first argument - * @param b - * the second argument - * @param c - * the third argument - * @param d - * the fourth argument - * @param e - * the fifth argument - * @param f - * the sixth argument - * @param g - * the seventh argument - * @param h - * the eighth argument - * @param i - * the ninth argument - */ - void dispatch(A a, B b, C c, D d, E e, F f, G g, H h, I i); -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerOne.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerOne.java deleted file mode 100644 index 68c9674d6150..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerOne.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2016-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.dispatch.types; - -import com.swirlds.platform.dispatch.Trigger; - -/** - * A trigger that accepts a single argument. - * - * @param - * the type of the argument - */ -@FunctionalInterface -public non-sealed interface TriggerOne extends Trigger> { - - /** - * Dispatch a trigger event. - * - * @param a - * the argument - */ - void dispatch(A a); -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerSeven.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerSeven.java deleted file mode 100644 index 5b1ee92bc0ce..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerSeven.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2016-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.dispatch.types; - -import com.swirlds.platform.dispatch.Trigger; - -/** - * A trigger that accepts seven arguments. - * - * @param - * the type of the first argument - * @param - * the type of the second argument - * @param - * the type of the third argument - * @param - * the type of the fourth argument - * @param - * the type of the fifth argument - * @param - * the type of the sixth argument - * @param - * the type of the seventh argument - */ -@FunctionalInterface -public non-sealed interface TriggerSeven extends Trigger> { - - /** - * Dispatch a trigger event. - * - * @param a - * the first argument - * @param b - * the second argument - * @param c - * the third argument - * @param d - * the fourth argument - * @param e - * the fifth argument - * @param f - * the sixth argument - * @param g - * the seventh argument - */ - void dispatch(A a, B b, C c, D d, E e, F f, G g); -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerSix.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerSix.java deleted file mode 100644 index e85d6d8dd340..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerSix.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (C) 2016-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.dispatch.types; - -import com.swirlds.platform.dispatch.Trigger; - -/** - * A trigger that accepts six arguments. - * - * @param - * the type of the first argument - * @param - * the type of the second argument - * @param - * the type of the third argument - * @param - * the type of the fourth argument - * @param - * the type of the fifth argument - * @param - * the type of the sixth argument - */ -@FunctionalInterface -public non-sealed interface TriggerSix extends Trigger> { - - /** - * Dispatch a trigger event. - * - * @param a - * the first argument - * @param b - * the second argument - * @param c - * the third argument - * @param d - * the fourth argument - * @param e - * the fifth argument - * @param f - * the sixth argument - */ - void dispatch(A a, B b, C c, D d, E e, F f); -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerTen.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerTen.java deleted file mode 100644 index 9bd587c547c0..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerTen.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (C) 2016-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.dispatch.types; - -import com.swirlds.platform.dispatch.Trigger; - -/** - * A trigger that accepts ten arguments. - * - * @param - * the type of the first argument - * @param - * the type of the second argument - * @param - * the type of the third argument - * @param - * the type of the fourth argument - * @param - * the type of the fifth argument - * @param - * the type of the sixth argument - * @param - * the type of the seventh argument - * @param - * the type of the eighth argument - * @param - * the type of the ninth argument - * @param - * the type of the tenth argument - */ -@FunctionalInterface -public non-sealed interface TriggerTen - extends Trigger> { - - /** - * Dispatch a trigger event. - * - * @param a - * the first argument - * @param b - * the second argument - * @param c - * the third argument - * @param d - * the fourth argument - * @param e - * the fifth argument - * @param f - * the sixth argument - * @param g - * the seventh argument - * @param h - * the eighth argument - * @param i - * the ninth argument - * @param j - * the tenth argument - */ - void dispatch(A a, B b, C c, D d, E e, F f, G g, H h, I i, J j); -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerThree.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerThree.java deleted file mode 100644 index 2b04e59348d7..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerThree.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2016-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.dispatch.types; - -import com.swirlds.platform.dispatch.Trigger; - -/** - * A trigger that accepts three arguments. - * - * @param - * the type of the first argument - * @param - * the type of the second argument - * @param - * the type of the third argument - */ -@FunctionalInterface -public non-sealed interface TriggerThree extends Trigger> { - - /** - * Dispatch a trigger. - * - * @param a - * the first argument - * @param b - * the second argument - * @param c - * the third argument - */ - void dispatch(A a, B b, C c); -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerTwo.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerTwo.java deleted file mode 100644 index bd4d59c77edb..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerTwo.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2016-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.dispatch.types; - -import com.swirlds.platform.dispatch.Trigger; - -/** - * A trigger that accepts two arguments. - * - * @param - * the type of the first argument - * @param - * the type of the second argument - */ -@FunctionalInterface -public non-sealed interface TriggerTwo extends Trigger> { - - /** - * Dispatch a trigger event. - * - * @param a - * the first argument - * @param b - * the second argument - */ - void dispatch(A a, B b); -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerZero.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerZero.java deleted file mode 100644 index c6ef86cde13e..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/types/TriggerZero.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (C) 2018-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.dispatch.types; - -import com.swirlds.platform.dispatch.Trigger; - -/** - * A dispatcher for that accepts zero arguments. - */ -@FunctionalInterface -public non-sealed interface TriggerZero extends Trigger { - - /** - * Dispatch a trigger event. - */ - void dispatch(); -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/recovery/EmergencyRecoveryManager.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/recovery/EmergencyRecoveryManager.java index eb8d42728bb7..1ff56efc81bf 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/recovery/EmergencyRecoveryManager.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/recovery/EmergencyRecoveryManager.java @@ -20,9 +20,9 @@ import static com.swirlds.platform.system.SystemExitCode.EMERGENCY_RECOVERY_ERROR; import com.swirlds.platform.config.StateConfig; -import com.swirlds.platform.dispatch.triggers.control.ShutdownRequestedTrigger; import com.swirlds.platform.recovery.emergencyfile.EmergencyRecoveryFile; import com.swirlds.platform.state.signed.SignedState; +import com.swirlds.platform.system.Shutdown; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.io.IOException; @@ -36,23 +36,17 @@ public class EmergencyRecoveryManager { private static final Logger logger = LogManager.getLogger(EmergencyRecoveryManager.class); - private final ShutdownRequestedTrigger shutdownRequestedTrigger; private final EmergencyRecoveryFile emergencyRecoveryFile; private final StateConfig stateConfig; private volatile boolean emergencyStateRequired; /** - * @param stateConfig the state configuration from the platform - * @param shutdownRequestedTrigger a trigger that requests the platform to shut down - * @param emergencyRecoveryDir the directory to look for an emergency recovery file in + * @param stateConfig the state configuration from the platform + * @param emergencyRecoveryDir the directory to look for an emergency recovery file in */ - public EmergencyRecoveryManager( - @NonNull final StateConfig stateConfig, - @NonNull final ShutdownRequestedTrigger shutdownRequestedTrigger, - @NonNull final Path emergencyRecoveryDir) { + public EmergencyRecoveryManager(@NonNull final StateConfig stateConfig, @NonNull final Path emergencyRecoveryDir) { this.stateConfig = stateConfig; - this.shutdownRequestedTrigger = shutdownRequestedTrigger; this.emergencyRecoveryFile = readEmergencyRecoveryFile(emergencyRecoveryDir); emergencyStateRequired = emergencyRecoveryFile != null; } @@ -107,7 +101,8 @@ public boolean isEmergencyState(@NonNull final SignedState state) { "Detected an emergency recovery file at {} but was unable to read it", dir, e); - shutdownRequestedTrigger.dispatch("Emergency Recovery Error", EMERGENCY_RECOVERY_ERROR); + + new Shutdown().shutdown("Emergency Recovery Error", EMERGENCY_RECOVERY_ERROR); return null; } } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/iss/IssHandler.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/iss/IssHandler.java index b28a8d8ab543..e187e3589d15 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/iss/IssHandler.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/iss/IssHandler.java @@ -20,18 +20,18 @@ import com.swirlds.common.scratchpad.Scratchpad; import com.swirlds.platform.components.common.output.FatalErrorConsumer; import com.swirlds.platform.config.StateConfig; -import com.swirlds.platform.dispatch.triggers.control.HaltRequestedConsumer; import com.swirlds.platform.system.SystemExitCode; import com.swirlds.platform.system.state.notifications.IssNotification; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.Objects; +import java.util.function.Consumer; /** * This class is responsible for handling the response to an ISS event. */ public class IssHandler { private final StateConfig stateConfig; - private final HaltRequestedConsumer haltRequestedConsumer; + private final Consumer haltRequestedConsumer; private final FatalErrorConsumer fatalErrorConsumer; private final Scratchpad issScratchpad; @@ -47,7 +47,7 @@ public class IssHandler { */ public IssHandler( @NonNull final StateConfig stateConfig, - @NonNull final HaltRequestedConsumer haltRequestedConsumer, + @NonNull final Consumer haltRequestedConsumer, @NonNull final FatalErrorConsumer fatalErrorConsumer, @NonNull final Scratchpad issScratchpad) { this.haltRequestedConsumer = @@ -79,7 +79,7 @@ private void otherIss() { return; } if (stateConfig.haltOnAnyIss()) { - haltRequestedConsumer.haltRequested("other node observed with ISS"); + haltRequestedConsumer.accept("other node observed with ISS"); halted = true; } } @@ -120,7 +120,7 @@ private void selfIssObserver(@NonNull final Long round) { updateIssRoundInScratchpad(round); if (stateConfig.haltOnAnyIss()) { - haltRequestedConsumer.haltRequested("self ISS observed"); + haltRequestedConsumer.accept("self ISS observed"); halted = true; } else if (stateConfig.automatedSelfIssRecovery()) { // Automated recovery is a fancy way of saying "turn it off and on again". @@ -195,7 +195,7 @@ private void catastrophicIssObserver(@NonNull final Long round) { updateIssRoundInScratchpad(round); if (stateConfig.haltOnAnyIss() || stateConfig.haltOnCatastrophicIss()) { - haltRequestedConsumer.haltRequested("catastrophic ISS observed"); + haltRequestedConsumer.accept("catastrophic ISS observed"); halted = true; } } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/Shutdown.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/Shutdown.java index 5908f8ec2fe3..1988286ed1cd 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/Shutdown.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/Shutdown.java @@ -16,8 +16,6 @@ package com.swirlds.platform.system; -import com.swirlds.platform.dispatch.Observer; -import com.swirlds.platform.dispatch.triggers.control.ShutdownRequestedTrigger; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.util.Objects; @@ -35,7 +33,6 @@ public Shutdown() {} * @param reason the reason the JVM is being shut down * @param exitCode the exit code to return when the JVM has been shut down */ - @Observer(ShutdownRequestedTrigger.class) public void shutdown(@Nullable final String reason, @NonNull final SystemExitCode exitCode) { Objects.requireNonNull(exitCode); SystemExitUtils.exitSystem(exitCode, reason); 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 052d47095a63..2cad12e468b6 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 @@ -55,7 +55,6 @@ import com.swirlds.platform.config.internal.ConfigMappings; import com.swirlds.platform.config.internal.PlatformConfigUtils; import com.swirlds.platform.consensus.ConsensusConfig; -import com.swirlds.platform.dispatch.DispatchConfiguration; import com.swirlds.platform.event.creation.EventCreationConfig; import com.swirlds.platform.event.preconsensus.PcesConfig; import com.swirlds.platform.eventhandling.EventConfig; @@ -148,7 +147,6 @@ public static void setupConfigBuilder( .withConfigDataType(VirtualMapConfig.class) .withConfigDataType(ConsensusConfig.class) .withConfigDataType(ThreadConfig.class) - .withConfigDataType(DispatchConfiguration.class) .withConfigDataType(MetricsConfig.class) .withConfigDataType(PrometheusConfig.class) .withConfigDataType(OSHealthCheckConfig.class) diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/util/DeadlockSentinel.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/util/DeadlockSentinel.java deleted file mode 100644 index b8123b602059..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/util/DeadlockSentinel.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright (C) 2022-2024 Hedera Hashgraph, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.swirlds.platform.util; - -import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; - -import com.swirlds.base.state.Startable; -import com.swirlds.common.AutoCloseableNonThrowing; -import com.swirlds.common.threading.framework.StoppableThread; -import com.swirlds.common.threading.framework.config.StoppableThreadConfiguration; -import com.swirlds.common.threading.manager.ThreadManager; -import com.swirlds.common.utility.StackTrace; -import com.swirlds.platform.dispatch.DispatchBuilder; -import com.swirlds.platform.dispatch.triggers.error.DeadlockTrigger; -import java.lang.management.LockInfo; -import java.lang.management.ManagementFactory; -import java.lang.management.ThreadInfo; -import java.lang.management.ThreadMXBean; -import java.time.Duration; -import java.util.concurrent.locks.StampedLock; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -/** - * This class watches for deadlocks and logs debug messages if deadlocks are detected. - */ -public class DeadlockSentinel implements Startable, AutoCloseableNonThrowing { - - private static final Logger logger = LogManager.getLogger(DeadlockSentinel.class); - private static final int STACK_TRACE_MAX_DEPTH = 16; - - private final ThreadMXBean mxBean = ManagementFactory.getThreadMXBean(); - private final StoppableThread thread; - private final DeadlockTrigger deadlockDispatcher; - - /** - * Create a new deadlock sentinel, but do not start it. - * - * @param threadManager - * responsible for managing thread lifecycles - * @param dispatchBuilder - * builds dispatchers - * @param period - * the minimum amount of time that must pass between checking for deadlocks - */ - public DeadlockSentinel( - final ThreadManager threadManager, final DispatchBuilder dispatchBuilder, final Duration period) { - thread = new StoppableThreadConfiguration<>(threadManager) - .setComponent("platform") - .setThreadName("deadlock-sentinel") - .setMinimumPeriod(period) - .setWork(this::lookForDeadlocks) - .build(); - deadlockDispatcher = dispatchBuilder.getDispatcher(this, DeadlockTrigger.class)::dispatch; - } - - /** - * {@inheritDoc} - */ - @Override - public void start() { - thread.start(); - } - - /** - * {@inheritDoc} - */ - @Override - public void close() { - thread.stop(); - } - - /** - * Look for deadlocks, and log if deadlocks are discovered. - */ - private void lookForDeadlocks() { - final long[] deadlockedThreads = mxBean.findDeadlockedThreads(); - - final StampedLock stampedLock = new StampedLock(); - - if (deadlockedThreads == null || deadlockedThreads.length == 0) { - // No threads are currently deadlocked. - return; - } - - final StringBuilder sb = new StringBuilder(); - sb.append("Deadlocked threads detected:\n"); - - for (final long threadId : deadlockedThreads) { - captureDeadlockedThreadData(sb, threadId); - } - - logger.error(EXCEPTION.getMarker(), sb); - deadlockDispatcher.dispatch(); - } - - /** - * Write information about a deadlocked thread to a string builder. - */ - private void captureDeadlockedThreadData(final StringBuilder sb, final long threadId) { - final ThreadInfo blocked = mxBean.getThreadInfo(threadId, STACK_TRACE_MAX_DEPTH); - final String blockedName = blocked.getThreadName(); - - final ThreadInfo blocker = mxBean.getThreadInfo(blocked.getLockOwnerId()); - final String blockingName = blocker.getThreadName(); - - final LockInfo lock = blocked.getLockInfo(); - final String lockName = lock.getClassName(); - final int lockId = lock.getIdentityHashCode(); - - sb.append("Thread ") - .append(blockedName) - .append(" blocked waiting on ") - .append(blockingName) - .append(", lock = ") - .append(lockName) - .append("(") - .append(lockId) - .append(")\n"); - - sb.append(new StackTrace(blocked.getStackTrace())).append("\n"); - } -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/util/PlatformComponents.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/util/ThingsToStart.java similarity index 71% rename from platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/util/PlatformComponents.java rename to platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/util/ThingsToStart.java index 7ee90161ba36..916d9e7664ad 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/util/PlatformComponents.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/util/ThingsToStart.java @@ -18,47 +18,37 @@ import com.swirlds.base.state.Mutable; import com.swirlds.base.state.Startable; -import com.swirlds.platform.dispatch.DispatchBuilder; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.LinkedList; import java.util.List; import java.util.Objects; /** - * A helper class for wiring platform components together. + * A helper class for holding things we want to start. */ -public class PlatformComponents implements Mutable, Startable { +public class ThingsToStart implements Mutable, Startable { private final List components = new LinkedList<>(); - private final DispatchBuilder dispatchBuilder; private boolean immutable = false; /** * Create a new container for platform components. - * - * @param dispatchBuilder - * the dispatch builder used by this platform instance. */ - public PlatformComponents(final DispatchBuilder dispatchBuilder) { - this.dispatchBuilder = dispatchBuilder; - } + public ThingsToStart() {} /** * Add a platform component that needs to be wired and/or started. * - * @param component - * the component - * @param - * the type of the component + * @param component the component + * @param the type of the component * @return the component */ @NonNull - public T add(@NonNull final T component) { + public T add(@NonNull final T component) { throwIfImmutable(); Objects.requireNonNull(component); components.add(component); - dispatchBuilder.registerObservers(component); return component; } @@ -69,7 +59,6 @@ public T add(@NonNull final T component) { public void start() { throwIfImmutable(); immutable = true; - dispatchBuilder.start(); for (final Object component : components) { if (component instanceof final Startable startable) { startable.start(); 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 0e5c094fea05..1d496777c6cf 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 @@ -89,18 +89,6 @@ exports com.swirlds.platform.state.iss.internal to com.swirlds.platform.test; exports com.swirlds.platform.gossip.chatter.protocol.processing; - exports com.swirlds.platform.dispatch to - com.swirlds.platform.test, - com.swirlds.config.impl, - com.swirlds.common, - com.hedera.node.test.clients; - exports com.swirlds.platform.dispatch.types to - com.swirlds.platform.test; - exports com.swirlds.platform.dispatch.triggers.control to - com.swirlds.platform.test, - com.hedera.node.test.clients; - exports com.swirlds.platform.dispatch.triggers.error to - com.swirlds.platform.test; exports com.swirlds.platform.reconnect.emergency to com.swirlds.platform.test; exports com.swirlds.platform.recovery.internal to diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/DispatchBuilderUtils.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/DispatchBuilderUtils.java deleted file mode 100644 index 0dfb88360ee5..000000000000 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/DispatchBuilderUtils.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2018-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; - -import com.swirlds.platform.dispatch.DispatchConfiguration; - -/** - * Utilities for tests utilizing the {@link com.swirlds.platform.dispatch.DispatchBuilder}. - */ -public final class DispatchBuilderUtils { - - private DispatchBuilderUtils() {} - - private static DispatchConfiguration defaultConfiguration = new DispatchConfiguration(false, "", "", "", ""); - - /** - * Get a default configuration for the dispatch builder. - */ - public static DispatchConfiguration getDefaultDispatchConfiguration() { - return defaultConfiguration; - } -} diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/DispatchFlowchartTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/DispatchFlowchartTests.java deleted file mode 100644 index 654e83dfaea5..000000000000 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/DispatchFlowchartTests.java +++ /dev/null @@ -1,341 +0,0 @@ -/* - * Copyright (C) 2022-2024 Hedera Hashgraph, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.swirlds.platform; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.swirlds.platform.dispatch.DispatchBuilder; -import com.swirlds.platform.dispatch.DispatchConfiguration; -import com.swirlds.platform.dispatch.flowchart.DispatchFlowchart; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -@DisplayName("Dispatch Flowchart Tests") -class DispatchFlowchartTests { - - private final class Class1 {} - - private final class Class2 {} - - private final class Trigger1 {} - - private final class Trigger2 {} - - @Test - @DisplayName("Linkage Test") - void linkageTest() { - final DispatchFlowchart flowchart = new DispatchFlowchart(new DispatchConfiguration(true, "", "", "", "")); - - flowchart.registerDispatcher(Class1.class, Trigger1.class, null); - - flowchart.registerObserver(Class2.class, Trigger1.class, null); - - final String data = flowchart.buildFlowchart(); - - // Header - assertTrue(data.contains("flowchart TD\n"), "data lacking expected line"); - - // Class definitions - assertTrue(data.contains(" Class1\n"), "data lacking expected line"); - assertTrue(data.contains(" Class2\n"), "data lacking expected line"); - - // Trigger definitions - assertTrue(data.contains(" Trigger1{{Trigger1}}\n"), "data lacking expected line"); - assertFalse(data.contains(" Trigger2{{Trigger2}}\n"), "data lacking expected line"); - - // Links from dispatchers to triggers - assertTrue(data.contains(" Class1 --> Trigger1\n"), "data lacking expected line"); - assertFalse(data.contains(" Class1 --> Trigger2\n"), "data lacking expected line"); - assertFalse(data.contains(" Class2 --> Trigger1\n"), "data lacking expected line"); - assertFalse(data.contains(" Class2 --> Trigger2\n"), "data lacking expected line"); - - // Links from triggers to observers - assertFalse(data.contains("Trigger1 -.-> Class1\n"), " data lacking expected line"); - assertFalse(data.contains("Trigger2 -.-> Class1\n"), " data lacking expected line"); - assertTrue(data.contains("Trigger1 -.-> Class2\n"), " data lacking expected line"); - assertFalse(data.contains("Trigger2 -.-> Class2\n"), " data lacking expected line"); - } - - @Test - @DisplayName("Linkage With Comments Test") - void linkageWithCommentsTest() { - - final DispatchFlowchart flowchart = new DispatchFlowchart(new DispatchConfiguration(true, "", "", "", "")); - - final String dispatchComment = "this is a comment"; - flowchart.registerDispatcher(Class1.class, Trigger1.class, dispatchComment); - - final String observerComment = "this is another comment"; - flowchart.registerObserver(Class2.class, Trigger1.class, observerComment); - - final String data = flowchart.buildFlowchart(); - - // Header - assertTrue(data.contains("flowchart TD\n"), "data lacking expected line"); - - // Class definitions - assertTrue(data.contains(" Class1\n"), "data lacking expected line"); - assertTrue(data.contains(" Class2\n"), "data lacking expected line"); - - // Trigger definitions - assertTrue(data.contains(" Trigger1{{Trigger1}}\n"), "data lacking expected line"); - - // Links from dispatchers to triggers - assertTrue(data.contains("flowchart TD\n"), " Class1 -- \"this is a comment\" --> Trigger1\n"); - - // Links from triggers to observers - assertTrue( - data.contains(" Trigger1 -. \"this is another comment\" .-> Class2\n"), - "data lacking expected line"); - } - - @Test - @DisplayName("Multi-Linkage Test") - void multiLinkageTest() { - final DispatchFlowchart flowchart = new DispatchFlowchart(new DispatchConfiguration(true, "", "", "", "")); - - flowchart.registerDispatcher(Class1.class, Trigger1.class, null); - flowchart.registerDispatcher(Class1.class, Trigger2.class, null); - flowchart.registerDispatcher(Class2.class, Trigger1.class, null); - flowchart.registerDispatcher(Class2.class, Trigger2.class, null); - - flowchart.registerObserver(Class1.class, Trigger1.class, null); - flowchart.registerObserver(Class2.class, Trigger1.class, null); - flowchart.registerObserver(Class1.class, Trigger2.class, null); - flowchart.registerObserver(Class2.class, Trigger2.class, null); - - final String data = flowchart.buildFlowchart(); - - // Header - assertTrue(data.contains("flowchart TD\n"), "data lacking expected line"); - - // Class definitions - assertTrue(data.contains(" Class1\n"), "data lacking expected line"); - assertTrue(data.contains(" Class2\n"), "data lacking expected line"); - - // Trigger definitions - assertTrue(data.contains(" Trigger1{{Trigger1}}\n"), "data lacking expected line"); - assertTrue(data.contains(" Trigger2{{Trigger2}}\n"), "data lacking expected line"); - - // Links from dispatchers to triggers - assertTrue(data.contains(" Class1 --> Trigger1\n"), "data lacking expected line"); - assertTrue(data.contains(" Class1 --> Trigger2\n"), "data lacking expected line"); - assertTrue(data.contains(" Class2 --> Trigger1\n"), "data lacking expected line"); - assertTrue(data.contains(" Class2 --> Trigger2\n"), "data lacking expected line"); - - // Links from triggers to observers - assertTrue(data.contains("Trigger1 -.-> Class1\n"), " data lacking expected line"); - assertTrue(data.contains("Trigger2 -.-> Class1\n"), " data lacking expected line"); - assertTrue(data.contains("Trigger1 -.-> Class2\n"), " data lacking expected line"); - assertTrue(data.contains("Trigger2 -.-> Class2\n"), " data lacking expected line"); - } - - @Test - @DisplayName("Whitelist Trigger Test") - void whitelistTriggerTest() { - - final DispatchFlowchart flowchart = - new DispatchFlowchart(new DispatchConfiguration(true, "Trigger1", "", "", "")); - - flowchart.registerDispatcher(Class1.class, Trigger1.class, null); - flowchart.registerDispatcher(Class1.class, Trigger2.class, null); - flowchart.registerDispatcher(Class2.class, Trigger1.class, null); - flowchart.registerDispatcher(Class2.class, Trigger2.class, null); - - flowchart.registerObserver(Class1.class, Trigger1.class, null); - flowchart.registerObserver(Class2.class, Trigger1.class, null); - flowchart.registerObserver(Class1.class, Trigger2.class, null); - flowchart.registerObserver(Class2.class, Trigger2.class, null); - - final String data = flowchart.buildFlowchart(); - System.out.println(data); - - // Header - assertTrue(data.contains("flowchart TD\n"), "data lacking expected line"); - - // Class definitions - assertTrue(data.contains(" Class1\n"), "data lacking expected line"); - assertTrue(data.contains(" Class2\n"), "data lacking expected line"); - - // Trigger definitions - assertTrue(data.contains(" Trigger1{{Trigger1}}\n"), "data lacking expected line"); - assertFalse(data.contains(" Trigger2{{Trigger2}}\n"), "data lacking expected line"); - - // Links from dispatchers to triggers - assertTrue(data.contains(" Class1 --> Trigger1\n"), "data lacking expected line"); - assertFalse(data.contains(" Class1 --> Trigger2\n"), "data lacking expected line"); - assertTrue(data.contains(" Class2 --> Trigger1\n"), "data lacking expected line"); - assertFalse(data.contains(" Class2 --> Trigger2\n"), "data lacking expected line"); - - // Links from triggers to observers - assertTrue(data.contains("Trigger1 -.-> Class1\n"), " data lacking expected line"); - assertFalse(data.contains("Trigger2 -.-> Class1\n"), " data lacking expected line"); - assertTrue(data.contains("Trigger1 -.-> Class2\n"), " data lacking expected line"); - assertFalse(data.contains("Trigger2 -.-> Class2\n"), " data lacking expected line"); - } - - @Test - @DisplayName("Whitelist Trigger Test") - void blacklistTriggerTest() { - - final DispatchFlowchart flowchart = - new DispatchFlowchart(new DispatchConfiguration(true, "", "Trigger2", "", "")); - - flowchart.registerDispatcher(Class1.class, Trigger1.class, null); - flowchart.registerDispatcher(Class1.class, Trigger2.class, null); - flowchart.registerDispatcher(Class2.class, Trigger1.class, null); - flowchart.registerDispatcher(Class2.class, Trigger2.class, null); - - flowchart.registerObserver(Class1.class, Trigger1.class, null); - flowchart.registerObserver(Class2.class, Trigger1.class, null); - flowchart.registerObserver(Class1.class, Trigger2.class, null); - flowchart.registerObserver(Class2.class, Trigger2.class, null); - - final String data = flowchart.buildFlowchart(); - - // Header - assertTrue(data.contains("flowchart TD\n"), "data lacking expected line"); - - // Class definitions - assertTrue(data.contains(" Class1\n"), "data lacking expected line"); - assertTrue(data.contains(" Class2\n"), "data lacking expected line"); - - // Trigger definitions - assertTrue(data.contains(" Trigger1{{Trigger1}}\n"), "data lacking expected line"); - assertFalse(data.contains(" Trigger2{{Trigger2}}\n"), "data lacking expected line"); - - // Links from dispatchers to triggers - assertTrue(data.contains(" Class1 --> Trigger1\n"), "data lacking expected line"); - assertFalse(data.contains(" Class1 --> Trigger2\n"), "data lacking expected line"); - assertTrue(data.contains(" Class2 --> Trigger1\n"), "data lacking expected line"); - assertFalse(data.contains(" Class2 --> Trigger2\n"), "data lacking expected line"); - - // Links from triggers to observers - assertTrue(data.contains("Trigger1 -.-> Class1\n"), " data lacking expected line"); - assertFalse(data.contains("Trigger2 -.-> Class1\n"), " data lacking expected line"); - assertTrue(data.contains("Trigger1 -.-> Class2\n"), " data lacking expected line"); - assertFalse(data.contains("Trigger2 -.-> Class2\n"), " data lacking expected line"); - } - - @Test - @DisplayName("Whitelist Object Test") - void whitelistObjectTest() { - - final DispatchFlowchart flowchart = - new DispatchFlowchart(new DispatchConfiguration(true, "", "", "Class1", "")); - - flowchart.registerDispatcher(Class1.class, Trigger1.class, null); - flowchart.registerDispatcher(Class1.class, Trigger2.class, null); - flowchart.registerDispatcher(Class2.class, Trigger1.class, null); - flowchart.registerDispatcher(Class2.class, Trigger2.class, null); - - flowchart.registerObserver(Class1.class, Trigger1.class, null); - flowchart.registerObserver(Class2.class, Trigger1.class, null); - flowchart.registerObserver(Class1.class, Trigger2.class, null); - flowchart.registerObserver(Class2.class, Trigger2.class, null); - - final String data = flowchart.buildFlowchart(); - - // Header - assertTrue(data.contains("flowchart TD\n"), "data lacking expected line"); - - // Class definitions - assertTrue(data.contains(" Class1\n"), "data lacking expected line"); - assertFalse(data.contains(" Class2\n"), "data lacking expected line"); - - // Trigger definitions - assertTrue(data.contains(" Trigger1{{Trigger1}}\n"), "data lacking expected line"); - assertTrue(data.contains(" Trigger2{{Trigger2}}\n"), "data lacking expected line"); - - // Links from dispatchers to triggers - assertTrue(data.contains(" Class1 --> Trigger1\n"), "data lacking expected line"); - assertTrue(data.contains(" Class1 --> Trigger2\n"), "data lacking expected line"); - assertFalse(data.contains(" Class2 --> Trigger1\n"), "data lacking expected line"); - assertFalse(data.contains(" Class2 --> Trigger2\n"), "data lacking expected line"); - - // Links from triggers to observers - assertTrue(data.contains("Trigger1 -.-> Class1\n"), " data lacking expected line"); - assertTrue(data.contains("Trigger2 -.-> Class1\n"), " data lacking expected line"); - assertFalse(data.contains("Trigger1 -.-> Class2\n"), " data lacking expected line"); - assertFalse(data.contains("Trigger2 -.-> Class2\n"), " data lacking expected line"); - } - - @Test - @DisplayName("Blacklist Object Test") - void blacklistObjectTest() { - - final DispatchFlowchart flowchart = - new DispatchFlowchart(new DispatchConfiguration(true, "", "", "", "Class2")); - - flowchart.registerDispatcher(Class1.class, Trigger1.class, null); - flowchart.registerDispatcher(Class1.class, Trigger2.class, null); - flowchart.registerDispatcher(Class2.class, Trigger1.class, null); - flowchart.registerDispatcher(Class2.class, Trigger2.class, null); - - flowchart.registerObserver(Class1.class, Trigger1.class, null); - flowchart.registerObserver(Class2.class, Trigger1.class, null); - flowchart.registerObserver(Class1.class, Trigger2.class, null); - flowchart.registerObserver(Class2.class, Trigger2.class, null); - - final String data = flowchart.buildFlowchart(); - - // Header - assertTrue(data.contains("flowchart TD\n"), "data lacking expected line"); - - // Class definitions - assertTrue(data.contains(" Class1\n"), "data lacking expected line"); - assertFalse(data.contains(" Class2\n"), "data lacking expected line"); - - // Trigger definitions - assertTrue(data.contains(" Trigger1{{Trigger1}}\n"), "data lacking expected line"); - assertTrue(data.contains(" Trigger2{{Trigger2}}\n"), "data lacking expected line"); - - // Links from dispatchers to triggers - assertTrue(data.contains(" Class1 --> Trigger1\n"), "data lacking expected line"); - assertTrue(data.contains(" Class1 --> Trigger2\n"), "data lacking expected line"); - assertFalse(data.contains(" Class2 --> Trigger1\n"), "data lacking expected line"); - assertFalse(data.contains(" Class2 --> Trigger2\n"), "data lacking expected line"); - - // Links from triggers to observers - assertTrue(data.contains("Trigger1 -.-> Class1\n"), " data lacking expected line"); - assertTrue(data.contains("Trigger2 -.-> Class1\n"), " data lacking expected line"); - assertFalse(data.contains("Trigger1 -.-> Class2\n"), " data lacking expected line"); - assertFalse(data.contains("Trigger2 -.-> Class2\n"), " data lacking expected line"); - } - - @Test - @DisplayName("Catch Illegal Object Whitelist & Blacklist") - void catchIllegalObjectWhitelistAndBlacklistTest() { - // It's illegal to define a simultaneous whitelist and blacklist - assertThrows( - IllegalStateException.class, - () -> new DispatchBuilder(new DispatchConfiguration(true, "", "", "Class1", "Class2")), - "should be unable to construct flowchart with given configuration"); - } - - @Test - @DisplayName("Catch Illegal Trigger Whitelist & Blacklist") - void catchTriggerObjectWhitelistAndBlacklistTest() { - // It's illegal to define a simultaneous whitelist and blacklist - assertThrows( - IllegalStateException.class, - () -> new DispatchBuilder(new DispatchConfiguration(true, "Trigger1", "Trigger2", "", "")), - "should be unable to construct flowchart with given configuration"); - } -} diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/DispatchTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/DispatchTests.java deleted file mode 100644 index 4a19dff9b57a..000000000000 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/DispatchTests.java +++ /dev/null @@ -1,1167 +0,0 @@ -/* - * Copyright (C) 2022-2024 Hedera Hashgraph, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.swirlds.platform; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import com.swirlds.base.state.MutabilityException; -import com.swirlds.platform.dispatch.DispatchBuilder; -import com.swirlds.platform.dispatch.DispatchConfiguration; -import com.swirlds.platform.dispatch.Observer; -import com.swirlds.platform.dispatch.types.TriggerEight; -import com.swirlds.platform.dispatch.types.TriggerFive; -import com.swirlds.platform.dispatch.types.TriggerFour; -import com.swirlds.platform.dispatch.types.TriggerNine; -import com.swirlds.platform.dispatch.types.TriggerOne; -import com.swirlds.platform.dispatch.types.TriggerSeven; -import com.swirlds.platform.dispatch.types.TriggerSix; -import com.swirlds.platform.dispatch.types.TriggerTen; -import com.swirlds.platform.dispatch.types.TriggerThree; -import com.swirlds.platform.dispatch.types.TriggerTwo; -import com.swirlds.platform.dispatch.types.TriggerZero; -import java.util.concurrent.atomic.AtomicInteger; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -@DisplayName("Dispatch Test") -class DispatchTests { - - private static final DispatchConfiguration config = new DispatchConfiguration(true, "", "", "", ""); - - @FunctionalInterface - public interface TestDispatchZero extends TriggerZero {} - - @FunctionalInterface - public interface TestDispatchOne extends TriggerOne {} - - @FunctionalInterface - public interface TestDispatchOneB extends TriggerOne {} - - @FunctionalInterface - public interface TestDispatchOneC extends TriggerOne {} - - @FunctionalInterface - public interface TestDispatchOneD extends TriggerOne {} - - @FunctionalInterface - public interface TestDispatchTwo extends TriggerTwo {} - - @FunctionalInterface - public interface TestDispatchThree extends TriggerThree {} - - @FunctionalInterface - public interface TestDispatchFour extends TriggerFour {} - - @FunctionalInterface - public interface TestDispatchFive extends TriggerFive {} - - @FunctionalInterface - public interface TestDispatchSix extends TriggerSix {} - - @FunctionalInterface - public interface TestDispatchSeven - extends TriggerSeven {} - - @FunctionalInterface - public interface TestDispatchEight - extends TriggerEight {} - - @FunctionalInterface - public interface TestDispatchNine - extends TriggerNine {} - - @FunctionalInterface - public interface TestDispatchTen - extends TriggerTen< - Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer> {} - - public static class ObserverClass { - - private final AtomicInteger count = new AtomicInteger(0); - - public int getCount() { - return count.get(); - } - - @DisplayName("bogus annotation that should be ignored") - @Observer(value = TestDispatchZero.class) - public void observeZero() { - count.getAndIncrement(); - } - - @Tag("bogus annotation") - @Tag("should be ignored") - @Observer(TestDispatchOne.class) - public void observeOne(final Integer a) { - count.getAndAdd(a); - } - - @Observer(value = TestDispatchTwo.class) - public void observeTwo(final Integer a, final Integer b) { - count.getAndAdd(a); - count.getAndAdd(b); - } - - @Observer(value = TestDispatchThree.class) - public void observeThree(final Integer a, final Integer b, final Integer c) { - count.getAndAdd(a); - count.getAndAdd(b); - count.getAndAdd(c); - } - - @Observer(value = TestDispatchFour.class) - public void observeFour(final Integer a, final Integer b, final Integer c, final Integer d) { - count.getAndAdd(a); - count.getAndAdd(b); - count.getAndAdd(c); - count.getAndAdd(d); - } - - @Observer(value = TestDispatchFive.class) - public void observeFive(final Integer a, final Integer b, final Integer c, final Integer d, final Integer e) { - count.getAndAdd(a); - count.getAndAdd(b); - count.getAndAdd(c); - count.getAndAdd(d); - count.getAndAdd(e); - } - - @Observer(value = TestDispatchSix.class) - public void observeSix( - final Integer a, final Integer b, final Integer c, final Integer d, final Integer e, final Integer f) { - count.getAndAdd(a); - count.getAndAdd(b); - count.getAndAdd(c); - count.getAndAdd(d); - count.getAndAdd(e); - count.getAndAdd(f); - } - - @Observer(value = TestDispatchSeven.class) - public void observeSeven( - final Integer a, - final Integer b, - final Integer c, - final Integer d, - final Integer e, - final Integer f, - final Integer g) { - count.getAndAdd(a); - count.getAndAdd(b); - count.getAndAdd(c); - count.getAndAdd(d); - count.getAndAdd(e); - count.getAndAdd(f); - count.getAndAdd(g); - } - - @Observer(value = TestDispatchEight.class) - public void observeEight( - final Integer a, - final Integer b, - final Integer c, - final Integer d, - final Integer e, - final Integer f, - final Integer g, - final Integer h) { - count.getAndAdd(a); - count.getAndAdd(b); - count.getAndAdd(c); - count.getAndAdd(d); - count.getAndAdd(e); - count.getAndAdd(f); - count.getAndAdd(g); - count.getAndAdd(h); - } - - @Observer(value = TestDispatchNine.class) - public void observeNine( - final Integer a, - final Integer b, - final Integer c, - final Integer d, - final Integer e, - final Integer f, - final Integer g, - final Integer h, - final Integer i) { - count.getAndAdd(a); - count.getAndAdd(b); - count.getAndAdd(c); - count.getAndAdd(d); - count.getAndAdd(e); - count.getAndAdd(f); - count.getAndAdd(g); - count.getAndAdd(h); - count.getAndAdd(i); - } - - @Observer(value = TestDispatchTen.class) - public void observeTen( - final Integer a, - final Integer b, - final Integer c, - final Integer d, - final Integer e, - final Integer f, - final Integer g, - final Integer h, - final Integer i, - final Integer j) { - count.getAndAdd(a); - count.getAndAdd(b); - count.getAndAdd(c); - count.getAndAdd(d); - count.getAndAdd(e); - count.getAndAdd(f); - count.getAndAdd(g); - count.getAndAdd(h); - count.getAndAdd(i); - count.getAndAdd(j); - } - } - - public class ObserverClassMultipleTriggersOnMethod { - - private final AtomicInteger count = new AtomicInteger(0); - - public int getCount() { - return count.get(); - } - - @Observer( - value = {TestDispatchOne.class, TestDispatchOneB.class, TestDispatchOneC.class, TestDispatchOneD.class}) - public void observeOne(final Integer a) { - count.getAndAdd(a); - } - } - - public class ObserverClassNoTriggers { - - /** - * This is an illegal way of using the annotation, at least one observer must be specified. - */ - @Observer(value = {}) - public void observeZero() {} - } - - @Test - @DisplayName("Illegal Annotation Arguments Test") - void illegalAnnotationArgumentsTest() { - final DispatchBuilder builder = new DispatchBuilder(config); - - final ObserverClassNoTriggers observerClassNoTriggers = new ObserverClassNoTriggers(); - assertThrows(IllegalArgumentException.class, () -> builder.registerObservers(observerClassNoTriggers)); - } - - @Test - @DisplayName("Double Start test") - void doubleStartTest() { - final DispatchBuilder builder = new DispatchBuilder(config); - builder.start(); - assertThrows(MutabilityException.class, builder::start, "should only be able to start once"); - } - - @Test - @DisplayName("Null Argument Test") - void nullArgumentTest() { - final DispatchBuilder builder = new DispatchBuilder(config); - - assertThrows( - NullPointerException.class, - () -> builder.registerObserver(null, null, null), - "null arguments not allowed"); - assertThrows( - NullPointerException.class, - () -> builder.registerObserver(DispatchTests.class, TestDispatchOne.class, null), - "null arguments not allowed"); - assertThrows( - NullPointerException.class, - () -> builder.registerObserver(TestDispatchOne.class, null, (TestDispatchOne) x -> {}), - "null arguments not allowed"); - assertThrows( - NullPointerException.class, - () -> builder.registerObserver(DispatchTests.class, null, (TestDispatchOne) x -> {}), - "null arguments not allowed"); - assertThrows(NullPointerException.class, () -> builder.registerObservers(null), "null arguments not allowed"); - - builder.start(); - - assertThrows( - NullPointerException.class, - () -> builder.getDispatcher(null, TestDispatchOne.class), - "null arguments not allowed"); - assertThrows(NullPointerException.class, () -> builder.getDispatcher(this, null), "null arguments not allowed"); - } - - @Test - @DisplayName("Early Dispatch Test") - void earlyDispatchTest() { - final DispatchBuilder builder = new DispatchBuilder(config); - - final TestDispatchZero d0 = builder.getDispatcher(this, TestDispatchZero.class)::dispatch; - final TestDispatchOne d1 = builder.getDispatcher(this, TestDispatchOne.class)::dispatch; - final TestDispatchTwo d2 = builder.getDispatcher(this, TestDispatchTwo.class)::dispatch; - final TestDispatchThree d3 = builder.getDispatcher(this, TestDispatchThree.class)::dispatch; - final TestDispatchFour d4 = builder.getDispatcher(this, TestDispatchFour.class)::dispatch; - final TestDispatchFive d5 = builder.getDispatcher(this, TestDispatchFive.class)::dispatch; - final TestDispatchSix d6 = builder.getDispatcher(this, TestDispatchSix.class)::dispatch; - final TestDispatchSeven d7 = builder.getDispatcher(this, TestDispatchSeven.class)::dispatch; - final TestDispatchEight d8 = builder.getDispatcher(this, TestDispatchEight.class)::dispatch; - final TestDispatchNine d9 = builder.getDispatcher(this, TestDispatchNine.class)::dispatch; - final TestDispatchTen d10 = builder.getDispatcher(this, TestDispatchTen.class)::dispatch; - - assertThrows( - MutabilityException.class, d0::dispatch, "shouldn't be able to dispatch before builder is started"); - assertThrows( - MutabilityException.class, - () -> d1.dispatch(0), - "shouldn't be able to dispatch before builder is started"); - assertThrows( - MutabilityException.class, - () -> d2.dispatch(0, 0), - "shouldn't be able to dispatch before builder is started"); - assertThrows( - MutabilityException.class, - () -> d3.dispatch(0, 0, 0), - "shouldn't be able to dispatch before builder is started"); - assertThrows( - MutabilityException.class, - () -> d4.dispatch(0, 0, 0, 0), - "shouldn't be able to dispatch before builder is started"); - assertThrows( - MutabilityException.class, - () -> d5.dispatch(0, 0, 0, 0, 0), - "shouldn't be able to dispatch before builder is started"); - assertThrows( - MutabilityException.class, - () -> d6.dispatch(0, 0, 0, 0, 0, 0), - "shouldn't be able to dispatch before builder is started"); - assertThrows( - MutabilityException.class, - () -> d7.dispatch(0, 0, 0, 0, 0, 0, 0), - "shouldn't be able to dispatch before builder is started"); - assertThrows( - MutabilityException.class, - () -> d8.dispatch(0, 0, 0, 0, 0, 0, 0, 0), - "shouldn't be able to dispatch before builder is started"); - assertThrows( - MutabilityException.class, - () -> d9.dispatch(0, 0, 0, 0, 0, 0, 0, 0, 0), - "shouldn't be able to dispatch before builder is started"); - assertThrows( - MutabilityException.class, - () -> d10.dispatch(0, 0, 0, 0, 0, 0, 0, 0, 0, 0), - "shouldn't be able to dispatch before builder is started"); - } - - @Test - @DisplayName("Late Registration Test") - void lateRegistrationTest() { - final DispatchBuilder builder = new DispatchBuilder(config); - builder.start(); - - assertThrows( - MutabilityException.class, - () -> builder.registerObserver(DispatchTests.class, TestDispatchZero.class, () -> {}), - "should not be able to register new observers after start"); - assertThrows( - MutabilityException.class, - () -> builder.registerObserver(DispatchTests.class, TestDispatchOne.class, (a) -> {}), - "should not be able to register new observers after start"); - assertThrows( - MutabilityException.class, - () -> builder.registerObserver(DispatchTests.class, TestDispatchTwo.class, (a, b) -> {}), - "should not be able to register new observers after start"); - assertThrows( - MutabilityException.class, - () -> builder.registerObserver(DispatchTests.class, TestDispatchThree.class, (a, b, c) -> {}), - "should not be able to register new observers after start"); - assertThrows( - MutabilityException.class, - () -> builder.registerObserver(DispatchTests.class, TestDispatchFour.class, (a, b, c, d) -> {}), - "should not be able to register new observers after start"); - assertThrows( - MutabilityException.class, - () -> builder.registerObserver(DispatchTests.class, TestDispatchFive.class, (a, b, c, d, e) -> {}), - "should not be able to register new observers after start"); - assertThrows( - MutabilityException.class, - () -> builder.registerObserver(DispatchTests.class, TestDispatchSix.class, (a, b, c, d, e, f) -> {}), - "should not be able to register new observers after start"); - assertThrows( - MutabilityException.class, - () -> builder.registerObserver( - DispatchTests.class, TestDispatchSeven.class, (a, b, c, d, e, f, g) -> {}), - "should not be able to register new observers after start"); - assertThrows( - MutabilityException.class, - () -> builder.registerObserver( - DispatchTests.class, TestDispatchEight.class, (a, b, c, d, e, f, g, h) -> {}), - "should not be able to register new observers after start"); - assertThrows( - MutabilityException.class, - () -> builder.registerObserver( - DispatchTests.class, TestDispatchNine.class, (a, b, c, d, e, f, g, h, i) -> {}), - "should not be able to register new observers after start"); - assertThrows( - MutabilityException.class, - () -> builder.registerObserver( - DispatchTests.class, TestDispatchTen.class, (a, b, c, d, e, f, g, h, i, j) -> {}), - "should not be able to register new observers after start"); - assertThrows( - MutabilityException.class, - () -> builder.registerObservers(new ObserverClass()), - "should not be able to register new observers after start"); - } - - @Test - @DisplayName("No Observer Test") - void noObserverTest() { - final DispatchBuilder builder = new DispatchBuilder(config); - builder.start(); - - final TestDispatchZero d0 = builder.getDispatcher(this, TestDispatchZero.class)::dispatch; - final TestDispatchOne d1 = builder.getDispatcher(this, TestDispatchOne.class)::dispatch; - final TestDispatchTwo d2 = builder.getDispatcher(this, TestDispatchTwo.class)::dispatch; - final TestDispatchThree d3 = builder.getDispatcher(this, TestDispatchThree.class)::dispatch; - final TestDispatchFour d4 = builder.getDispatcher(this, TestDispatchFour.class)::dispatch; - final TestDispatchFive d5 = builder.getDispatcher(this, TestDispatchFive.class)::dispatch; - final TestDispatchSix d6 = builder.getDispatcher(this, TestDispatchSix.class)::dispatch; - final TestDispatchSeven d7 = builder.getDispatcher(this, TestDispatchSeven.class)::dispatch; - final TestDispatchEight d8 = builder.getDispatcher(this, TestDispatchEight.class)::dispatch; - final TestDispatchNine d9 = builder.getDispatcher(this, TestDispatchNine.class)::dispatch; - final TestDispatchTen d10 = builder.getDispatcher(this, TestDispatchTen.class)::dispatch; - - assertDoesNotThrow(d0::dispatch, "no observers should be supported"); - assertDoesNotThrow(() -> d1.dispatch(0), "no observers should be supported"); - assertDoesNotThrow(() -> d2.dispatch(0, 0), "no observers should be supported"); - assertDoesNotThrow(() -> d3.dispatch(0, 0, 0), "no observers should be supported"); - assertDoesNotThrow(() -> d4.dispatch(0, 0, 0, 0), "no observers should be supported"); - assertDoesNotThrow(() -> d5.dispatch(0, 0, 0, 0, 0), "no observers should be supported"); - assertDoesNotThrow(() -> d6.dispatch(0, 0, 0, 0, 0, 0), "no observers should be supported"); - assertDoesNotThrow(() -> d7.dispatch(0, 0, 0, 0, 0, 0, 0), "no observers should be supported"); - assertDoesNotThrow(() -> d8.dispatch(0, 0, 0, 0, 0, 0, 0, 0), "no observers should be supported"); - assertDoesNotThrow(() -> d9.dispatch(0, 0, 0, 0, 0, 0, 0, 0, 0), "no observers should be supported"); - assertDoesNotThrow(() -> d10.dispatch(0, 0, 0, 0, 0, 0, 0, 0, 0, 0), "no observers should be supported"); - } - - @ParameterizedTest - @ValueSource(ints = {0, 1, 2}) - @DisplayName("One-To-One Dispatch Test") - void oneToOneDispatchTest(final int dispatchBuildLocation) { - final DispatchBuilder builder = new DispatchBuilder(config); - - TestDispatchZero d0 = null; - TestDispatchOne d1 = null; - TestDispatchTwo d2 = null; - TestDispatchThree d3 = null; - TestDispatchFour d4 = null; - TestDispatchFive d5 = null; - TestDispatchSix d6 = null; - TestDispatchSeven d7 = null; - TestDispatchEight d8 = null; - TestDispatchNine d9 = null; - TestDispatchTen d10 = null; - - if (dispatchBuildLocation == 0) { - d0 = builder.getDispatcher(this, TestDispatchZero.class)::dispatch; - d1 = builder.getDispatcher(this, TestDispatchOne.class)::dispatch; - d2 = builder.getDispatcher(this, TestDispatchTwo.class)::dispatch; - d3 = builder.getDispatcher(this, TestDispatchThree.class)::dispatch; - d4 = builder.getDispatcher(this, TestDispatchFour.class)::dispatch; - d5 = builder.getDispatcher(this, TestDispatchFive.class)::dispatch; - d6 = builder.getDispatcher(this, TestDispatchSix.class)::dispatch; - d7 = builder.getDispatcher(this, TestDispatchSeven.class)::dispatch; - d8 = builder.getDispatcher(this, TestDispatchEight.class)::dispatch; - d9 = builder.getDispatcher(this, TestDispatchNine.class)::dispatch; - d10 = builder.getDispatcher(this, TestDispatchTen.class)::dispatch; - } - - final AtomicInteger sum = new AtomicInteger(); - assertSame( - builder, - builder.registerObserver(DispatchTests.class, TestDispatchZero.class, sum::getAndIncrement), - "should have returned self"); - assertSame( - builder, - builder.registerObserver(DispatchTests.class, TestDispatchOne.class, sum::getAndAdd), - "should have returned self"); - assertSame( - builder, - builder.registerObserver(DispatchTests.class, TestDispatchTwo.class, (a, b) -> sum.getAndAdd(a + b)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, TestDispatchThree.class, (a, b, c) -> sum.getAndAdd(a + b + c)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, TestDispatchFour.class, (a, b, c, d) -> sum.getAndAdd(a + b + c + d)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, - TestDispatchFive.class, - (a, b, c, d, e) -> sum.getAndAdd(a + b + c + d + e)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, - TestDispatchSix.class, - (a, b, c, d, e, f) -> sum.getAndAdd(a + b + c + d + e + f)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, - TestDispatchSeven.class, - (a, b, c, d, e, f, g) -> sum.getAndAdd(a + b + c + d + e + f + g)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, - TestDispatchEight.class, - (a, b, c, d, e, f, g, h) -> sum.getAndAdd(a + b + c + d + e + f + g + h)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, - TestDispatchNine.class, - (a, b, c, d, e, f, g, h, i) -> sum.getAndAdd(a + b + c + d + e + f + g + h + i)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, - TestDispatchTen.class, - (a, b, c, d, e, f, g, h, i, j) -> sum.getAndAdd(a + b + c + d + e + f + g + h + i + j)), - "should have returned self"); - - if (dispatchBuildLocation == 1) { - d0 = builder.getDispatcher(this, TestDispatchZero.class)::dispatch; - d1 = builder.getDispatcher(this, TestDispatchOne.class)::dispatch; - d2 = builder.getDispatcher(this, TestDispatchTwo.class)::dispatch; - d3 = builder.getDispatcher(this, TestDispatchThree.class)::dispatch; - d4 = builder.getDispatcher(this, TestDispatchFour.class)::dispatch; - d5 = builder.getDispatcher(this, TestDispatchFive.class)::dispatch; - d6 = builder.getDispatcher(this, TestDispatchSix.class)::dispatch; - d7 = builder.getDispatcher(this, TestDispatchSeven.class)::dispatch; - d8 = builder.getDispatcher(this, TestDispatchEight.class)::dispatch; - d9 = builder.getDispatcher(this, TestDispatchNine.class)::dispatch; - d10 = builder.getDispatcher(this, TestDispatchTen.class)::dispatch; - } - - builder.start(); - - if (dispatchBuildLocation == 2) { - d0 = builder.getDispatcher(this, TestDispatchZero.class)::dispatch; - d1 = builder.getDispatcher(this, TestDispatchOne.class)::dispatch; - d2 = builder.getDispatcher(this, TestDispatchTwo.class)::dispatch; - d3 = builder.getDispatcher(this, TestDispatchThree.class)::dispatch; - d4 = builder.getDispatcher(this, TestDispatchFour.class)::dispatch; - d5 = builder.getDispatcher(this, TestDispatchFive.class)::dispatch; - d6 = builder.getDispatcher(this, TestDispatchSix.class)::dispatch; - d7 = builder.getDispatcher(this, TestDispatchSeven.class)::dispatch; - d8 = builder.getDispatcher(this, TestDispatchEight.class)::dispatch; - d9 = builder.getDispatcher(this, TestDispatchNine.class)::dispatch; - d10 = builder.getDispatcher(this, TestDispatchTen.class)::dispatch; - } - - assertNotNull(d0, "dispatcher should have been initialized by now"); - assertNotNull(d1, "dispatcher should have been initialized by now"); - assertNotNull(d2, "dispatcher should have been initialized by now"); - assertNotNull(d3, "dispatcher should have been initialized by now"); - assertNotNull(d4, "dispatcher should have been initialized by now"); - assertNotNull(d5, "dispatcher should have been initialized by now"); - assertNotNull(d6, "dispatcher should have been initialized by now"); - assertNotNull(d7, "dispatcher should have been initialized by now"); - assertNotNull(d8, "dispatcher should have been initialized by now"); - assertNotNull(d9, "dispatcher should have been initialized by now"); - assertNotNull(d10, "dispatcher should have been initialized by now"); - - int expectedSum = 0; - for (int i = 0; i < 100; i++) { - expectedSum += 1; - d0.dispatch(); - - expectedSum += i; - d1.dispatch(i); - - expectedSum += 2 * i; - d2.dispatch(i, i); - - expectedSum += 3 * i; - d3.dispatch(i, i, i); - - expectedSum += 4 * i; - d4.dispatch(i, i, i, i); - - expectedSum += 5 * i; - d5.dispatch(i, i, i, i, i); - - expectedSum += 6 * i; - d6.dispatch(i, i, i, i, i, i); - - expectedSum += 7 * i; - d7.dispatch(i, i, i, i, i, i, i); - - expectedSum += 8 * i; - d8.dispatch(i, i, i, i, i, i, i, i); - - expectedSum += 9 * i; - d9.dispatch(i, i, i, i, i, i, i, i, i); - } - - assertEquals(expectedSum, sum.get(), "callbacks not invoked correctly"); - } - - @ParameterizedTest - @ValueSource(ints = {0, 1, 2, 3, 4}) - @DisplayName("One-To-Many Dispatch Test") - void oneToManyDispatchTest(final int dispatchBuildLocation) { - final DispatchBuilder builder = new DispatchBuilder(config); - - TestDispatchZero d0 = null; - TestDispatchOne d1 = null; - TestDispatchTwo d2 = null; - TestDispatchThree d3 = null; - TestDispatchFour d4 = null; - TestDispatchFive d5 = null; - TestDispatchSix d6 = null; - TestDispatchSeven d7 = null; - TestDispatchEight d8 = null; - TestDispatchNine d9 = null; - TestDispatchTen d10 = null; - - if (dispatchBuildLocation == 0) { - d0 = builder.getDispatcher(this, TestDispatchZero.class)::dispatch; - d1 = builder.getDispatcher(this, TestDispatchOne.class)::dispatch; - d2 = builder.getDispatcher(this, TestDispatchTwo.class)::dispatch; - d3 = builder.getDispatcher(this, TestDispatchThree.class)::dispatch; - d4 = builder.getDispatcher(this, TestDispatchFour.class)::dispatch; - d5 = builder.getDispatcher(this, TestDispatchFive.class)::dispatch; - d6 = builder.getDispatcher(this, TestDispatchSix.class)::dispatch; - d7 = builder.getDispatcher(this, TestDispatchSeven.class)::dispatch; - d8 = builder.getDispatcher(this, TestDispatchEight.class)::dispatch; - d9 = builder.getDispatcher(this, TestDispatchNine.class)::dispatch; - d10 = builder.getDispatcher(this, TestDispatchTen.class)::dispatch; - } - - final AtomicInteger sum1 = new AtomicInteger(); - assertSame( - builder, - builder.registerObserver(DispatchTests.class, TestDispatchZero.class, sum1::getAndIncrement), - "should have returned self"); - assertSame( - builder, - builder.registerObserver(DispatchTests.class, TestDispatchOne.class, sum1::getAndAdd), - "should have returned self"); - assertSame( - builder, - builder.registerObserver(DispatchTests.class, TestDispatchTwo.class, (a, b) -> sum1.getAndAdd(a + b)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, TestDispatchThree.class, (a, b, c) -> sum1.getAndAdd(a + b + c)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, TestDispatchFour.class, (a, b, c, d) -> sum1.getAndAdd(a + b + c + d)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, - TestDispatchFive.class, - (a, b, c, d, e) -> sum1.getAndAdd(a + b + c + d + e)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, - TestDispatchSix.class, - (a, b, c, d, e, f) -> sum1.getAndAdd(a + b + c + d + e + f)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, - TestDispatchSeven.class, - (a, b, c, d, e, f, g) -> sum1.getAndAdd(a + b + c + d + e + f + g)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, - TestDispatchEight.class, - (a, b, c, d, e, f, g, h) -> sum1.getAndAdd(a + b + c + d + e + f + g + h)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, - TestDispatchNine.class, - (a, b, c, d, e, f, g, h, i) -> sum1.getAndAdd(a + b + c + d + e + f + g + h + i)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, - TestDispatchTen.class, - (a, b, c, d, e, f, g, h, i, j) -> sum1.getAndAdd(a + b + c + d + e + f + g + h + i + j)), - "should have returned self"); - - if (dispatchBuildLocation == 1) { - d0 = builder.getDispatcher(this, TestDispatchZero.class)::dispatch; - d1 = builder.getDispatcher(this, TestDispatchOne.class)::dispatch; - d2 = builder.getDispatcher(this, TestDispatchTwo.class)::dispatch; - d3 = builder.getDispatcher(this, TestDispatchThree.class)::dispatch; - d4 = builder.getDispatcher(this, TestDispatchFour.class)::dispatch; - d5 = builder.getDispatcher(this, TestDispatchFive.class)::dispatch; - d6 = builder.getDispatcher(this, TestDispatchSix.class)::dispatch; - d7 = builder.getDispatcher(this, TestDispatchSeven.class)::dispatch; - d8 = builder.getDispatcher(this, TestDispatchEight.class)::dispatch; - d9 = builder.getDispatcher(this, TestDispatchNine.class)::dispatch; - d10 = builder.getDispatcher(this, TestDispatchTen.class)::dispatch; - } - - final AtomicInteger sum2 = new AtomicInteger(); - assertSame( - builder, - builder.registerObserver(DispatchTests.class, TestDispatchZero.class, sum2::getAndIncrement), - "should have returned self"); - assertSame( - builder, - builder.registerObserver(DispatchTests.class, TestDispatchOne.class, sum2::getAndAdd), - "should have returned self"); - assertSame( - builder, - builder.registerObserver(DispatchTests.class, TestDispatchTwo.class, (a, b) -> sum2.getAndAdd(a + b)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, TestDispatchThree.class, (a, b, c) -> sum2.getAndAdd(a + b + c)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, TestDispatchFour.class, (a, b, c, d) -> sum2.getAndAdd(a + b + c + d)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, - TestDispatchFive.class, - (a, b, c, d, e) -> sum2.getAndAdd(a + b + c + d + e)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, - TestDispatchSix.class, - (a, b, c, d, e, f) -> sum2.getAndAdd(a + b + c + d + e + f)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, - TestDispatchSeven.class, - (a, b, c, d, e, f, g) -> sum2.getAndAdd(a + b + c + d + e + f + g)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, - TestDispatchEight.class, - (a, b, c, d, e, f, g, h) -> sum2.getAndAdd(a + b + c + d + e + f + g + h)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, - TestDispatchNine.class, - (a, b, c, d, e, f, g, h, i) -> sum2.getAndAdd(a + b + c + d + e + f + g + h + i)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, - TestDispatchTen.class, - (a, b, c, d, e, f, g, h, i, j) -> sum2.getAndAdd(a + b + c + d + e + f + g + h + i + j)), - "should have returned self"); - - if (dispatchBuildLocation == 2) { - d0 = builder.getDispatcher(this, TestDispatchZero.class)::dispatch; - d1 = builder.getDispatcher(this, TestDispatchOne.class)::dispatch; - d2 = builder.getDispatcher(this, TestDispatchTwo.class)::dispatch; - d3 = builder.getDispatcher(this, TestDispatchThree.class)::dispatch; - d4 = builder.getDispatcher(this, TestDispatchFour.class)::dispatch; - d5 = builder.getDispatcher(this, TestDispatchFive.class)::dispatch; - d6 = builder.getDispatcher(this, TestDispatchSix.class)::dispatch; - d7 = builder.getDispatcher(this, TestDispatchSeven.class)::dispatch; - d8 = builder.getDispatcher(this, TestDispatchEight.class)::dispatch; - d9 = builder.getDispatcher(this, TestDispatchNine.class)::dispatch; - d10 = builder.getDispatcher(this, TestDispatchTen.class)::dispatch; - } - - final AtomicInteger sum3 = new AtomicInteger(); - assertSame( - builder, - builder.registerObserver(DispatchTests.class, TestDispatchZero.class, sum3::getAndIncrement), - "should have returned self"); - assertSame( - builder, - builder.registerObserver(DispatchTests.class, TestDispatchOne.class, sum3::getAndAdd), - "should have returned self"); - assertSame( - builder, - builder.registerObserver(DispatchTests.class, TestDispatchTwo.class, (a, b) -> sum3.getAndAdd(a + b)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, TestDispatchThree.class, (a, b, c) -> sum3.getAndAdd(a + b + c)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, TestDispatchFour.class, (a, b, c, d) -> sum3.getAndAdd(a + b + c + d)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, - TestDispatchFive.class, - (a, b, c, d, e) -> sum3.getAndAdd(a + b + c + d + e)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, - TestDispatchSix.class, - (a, b, c, d, e, f) -> sum3.getAndAdd(a + b + c + d + e + f)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, - TestDispatchSeven.class, - (a, b, c, d, e, f, g) -> sum3.getAndAdd(a + b + c + d + e + f + g)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, - TestDispatchEight.class, - (a, b, c, d, e, f, g, h) -> sum3.getAndAdd(a + b + c + d + e + f + g + h)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, - TestDispatchNine.class, - (a, b, c, d, e, f, g, h, i) -> sum3.getAndAdd(a + b + c + d + e + f + g + h + i)), - "should have returned self"); - assertSame( - builder, - builder.registerObserver( - DispatchTests.class, - TestDispatchTen.class, - (a, b, c, d, e, f, g, h, i, j) -> sum3.getAndAdd(a + b + c + d + e + f + g + h + i + j)), - "should have returned self"); - - if (dispatchBuildLocation == 3) { - d0 = builder.getDispatcher(this, TestDispatchZero.class)::dispatch; - d1 = builder.getDispatcher(this, TestDispatchOne.class)::dispatch; - d2 = builder.getDispatcher(this, TestDispatchTwo.class)::dispatch; - d3 = builder.getDispatcher(this, TestDispatchThree.class)::dispatch; - d4 = builder.getDispatcher(this, TestDispatchFour.class)::dispatch; - d5 = builder.getDispatcher(this, TestDispatchFive.class)::dispatch; - d6 = builder.getDispatcher(this, TestDispatchSix.class)::dispatch; - d7 = builder.getDispatcher(this, TestDispatchSeven.class)::dispatch; - d8 = builder.getDispatcher(this, TestDispatchEight.class)::dispatch; - d9 = builder.getDispatcher(this, TestDispatchNine.class)::dispatch; - d10 = builder.getDispatcher(this, TestDispatchTen.class)::dispatch; - } - - builder.start(); - - if (dispatchBuildLocation == 4) { - d0 = builder.getDispatcher(this, TestDispatchZero.class)::dispatch; - d1 = builder.getDispatcher(this, TestDispatchOne.class)::dispatch; - d2 = builder.getDispatcher(this, TestDispatchTwo.class)::dispatch; - d3 = builder.getDispatcher(this, TestDispatchThree.class)::dispatch; - d4 = builder.getDispatcher(this, TestDispatchFour.class)::dispatch; - d5 = builder.getDispatcher(this, TestDispatchFive.class)::dispatch; - d6 = builder.getDispatcher(this, TestDispatchSix.class)::dispatch; - d7 = builder.getDispatcher(this, TestDispatchSeven.class)::dispatch; - d8 = builder.getDispatcher(this, TestDispatchEight.class)::dispatch; - d9 = builder.getDispatcher(this, TestDispatchNine.class)::dispatch; - d10 = builder.getDispatcher(this, TestDispatchTen.class)::dispatch; - } - - assertNotNull(d0, "dispatcher should have been initialized by now"); - assertNotNull(d1, "dispatcher should have been initialized by now"); - assertNotNull(d2, "dispatcher should have been initialized by now"); - assertNotNull(d3, "dispatcher should have been initialized by now"); - assertNotNull(d4, "dispatcher should have been initialized by now"); - assertNotNull(d5, "dispatcher should have been initialized by now"); - assertNotNull(d6, "dispatcher should have been initialized by now"); - assertNotNull(d7, "dispatcher should have been initialized by now"); - assertNotNull(d8, "dispatcher should have been initialized by now"); - assertNotNull(d9, "dispatcher should have been initialized by now"); - assertNotNull(d10, "dispatcher should have been initialized by now"); - - int expectedSum = 0; - for (int i = 0; i < 100; i++) { - expectedSum += 1; - d0.dispatch(); - - expectedSum += i; - d1.dispatch(i); - - expectedSum += 2 * i; - d2.dispatch(i, i); - - expectedSum += 3 * i; - d3.dispatch(i, i, i); - - expectedSum += 4 * i; - d4.dispatch(i, i, i, i); - - expectedSum += 5 * i; - d5.dispatch(i, i, i, i, i); - - expectedSum += 6 * i; - d6.dispatch(i, i, i, i, i, i); - - expectedSum += 7 * i; - d7.dispatch(i, i, i, i, i, i, i); - - expectedSum += 8 * i; - d8.dispatch(i, i, i, i, i, i, i, i); - - expectedSum += 9 * i; - d9.dispatch(i, i, i, i, i, i, i, i, i); - } - - assertEquals(expectedSum, sum1.get(), "callbacks not invoked correctly"); - assertEquals(expectedSum, sum2.get(), "callbacks not invoked correctly"); - assertEquals(expectedSum, sum3.get(), "callbacks not invoked correctly"); - } - - @ParameterizedTest - @ValueSource(ints = {0, 1, 2, 3, 4}) - @DisplayName("Auto-Register Test") - void autoRegisterTest(final int dispatchBuildLocation) { - final DispatchBuilder builder = new DispatchBuilder(config); - - TestDispatchZero d0 = null; - TestDispatchOne d1 = null; - TestDispatchTwo d2 = null; - TestDispatchThree d3 = null; - TestDispatchFour d4 = null; - TestDispatchFive d5 = null; - TestDispatchSix d6 = null; - TestDispatchSeven d7 = null; - TestDispatchEight d8 = null; - TestDispatchNine d9 = null; - TestDispatchTen d10 = null; - - if (dispatchBuildLocation == 0) { - d0 = builder.getDispatcher(this, TestDispatchZero.class)::dispatch; - d1 = builder.getDispatcher(this, TestDispatchOne.class)::dispatch; - d2 = builder.getDispatcher(this, TestDispatchTwo.class)::dispatch; - d3 = builder.getDispatcher(this, TestDispatchThree.class)::dispatch; - d4 = builder.getDispatcher(this, TestDispatchFour.class)::dispatch; - d5 = builder.getDispatcher(this, TestDispatchFive.class)::dispatch; - d6 = builder.getDispatcher(this, TestDispatchSix.class)::dispatch; - d7 = builder.getDispatcher(this, TestDispatchSeven.class)::dispatch; - d8 = builder.getDispatcher(this, TestDispatchEight.class)::dispatch; - d9 = builder.getDispatcher(this, TestDispatchNine.class)::dispatch; - d10 = builder.getDispatcher(this, TestDispatchTen.class)::dispatch; - } - - final ObserverClass observerClass1 = new ObserverClass(); - assertSame(builder, builder.registerObservers(observerClass1), "builder should return itself"); - - if (dispatchBuildLocation == 1) { - d0 = builder.getDispatcher(this, TestDispatchZero.class)::dispatch; - d1 = builder.getDispatcher(this, TestDispatchOne.class)::dispatch; - d2 = builder.getDispatcher(this, TestDispatchTwo.class)::dispatch; - d3 = builder.getDispatcher(this, TestDispatchThree.class)::dispatch; - d4 = builder.getDispatcher(this, TestDispatchFour.class)::dispatch; - d5 = builder.getDispatcher(this, TestDispatchFive.class)::dispatch; - d6 = builder.getDispatcher(this, TestDispatchSix.class)::dispatch; - d7 = builder.getDispatcher(this, TestDispatchSeven.class)::dispatch; - d8 = builder.getDispatcher(this, TestDispatchEight.class)::dispatch; - d9 = builder.getDispatcher(this, TestDispatchNine.class)::dispatch; - d10 = builder.getDispatcher(this, TestDispatchTen.class)::dispatch; - } - - final ObserverClass observerClass2 = new ObserverClass(); - assertSame(builder, builder.registerObservers(observerClass2), "builder should return itself"); - - if (dispatchBuildLocation == 2) { - d0 = builder.getDispatcher(this, TestDispatchZero.class)::dispatch; - d1 = builder.getDispatcher(this, TestDispatchOne.class)::dispatch; - d2 = builder.getDispatcher(this, TestDispatchTwo.class)::dispatch; - d3 = builder.getDispatcher(this, TestDispatchThree.class)::dispatch; - d4 = builder.getDispatcher(this, TestDispatchFour.class)::dispatch; - d5 = builder.getDispatcher(this, TestDispatchFive.class)::dispatch; - d6 = builder.getDispatcher(this, TestDispatchSix.class)::dispatch; - d7 = builder.getDispatcher(this, TestDispatchSeven.class)::dispatch; - d8 = builder.getDispatcher(this, TestDispatchEight.class)::dispatch; - d9 = builder.getDispatcher(this, TestDispatchNine.class)::dispatch; - d10 = builder.getDispatcher(this, TestDispatchTen.class)::dispatch; - } - - final ObserverClass observerClass3 = new ObserverClass(); - assertSame(builder, builder.registerObservers(observerClass3), "builder should return itself"); - - if (dispatchBuildLocation == 3) { - d0 = builder.getDispatcher(this, TestDispatchZero.class)::dispatch; - d1 = builder.getDispatcher(this, TestDispatchOne.class)::dispatch; - d2 = builder.getDispatcher(this, TestDispatchTwo.class)::dispatch; - d3 = builder.getDispatcher(this, TestDispatchThree.class)::dispatch; - d4 = builder.getDispatcher(this, TestDispatchFour.class)::dispatch; - d5 = builder.getDispatcher(this, TestDispatchFive.class)::dispatch; - d6 = builder.getDispatcher(this, TestDispatchSix.class)::dispatch; - d7 = builder.getDispatcher(this, TestDispatchSeven.class)::dispatch; - d8 = builder.getDispatcher(this, TestDispatchEight.class)::dispatch; - d9 = builder.getDispatcher(this, TestDispatchNine.class)::dispatch; - d10 = builder.getDispatcher(this, TestDispatchTen.class)::dispatch; - } - - builder.start(); - - if (dispatchBuildLocation == 4) { - d0 = builder.getDispatcher(this, TestDispatchZero.class)::dispatch; - d1 = builder.getDispatcher(this, TestDispatchOne.class)::dispatch; - d2 = builder.getDispatcher(this, TestDispatchTwo.class)::dispatch; - d3 = builder.getDispatcher(this, TestDispatchThree.class)::dispatch; - d4 = builder.getDispatcher(this, TestDispatchFour.class)::dispatch; - d5 = builder.getDispatcher(this, TestDispatchFive.class)::dispatch; - d6 = builder.getDispatcher(this, TestDispatchSix.class)::dispatch; - d7 = builder.getDispatcher(this, TestDispatchSeven.class)::dispatch; - d8 = builder.getDispatcher(this, TestDispatchEight.class)::dispatch; - d9 = builder.getDispatcher(this, TestDispatchNine.class)::dispatch; - d10 = builder.getDispatcher(this, TestDispatchTen.class)::dispatch; - } - - assertNotNull(d0, "dispatcher should have been initialized by now"); - assertNotNull(d1, "dispatcher should have been initialized by now"); - assertNotNull(d2, "dispatcher should have been initialized by now"); - assertNotNull(d3, "dispatcher should have been initialized by now"); - assertNotNull(d4, "dispatcher should have been initialized by now"); - assertNotNull(d5, "dispatcher should have been initialized by now"); - assertNotNull(d6, "dispatcher should have been initialized by now"); - assertNotNull(d7, "dispatcher should have been initialized by now"); - assertNotNull(d8, "dispatcher should have been initialized by now"); - assertNotNull(d9, "dispatcher should have been initialized by now"); - assertNotNull(d10, "dispatcher should have been initialized by now"); - - int expectedSum = 0; - for (int i = 0; i < 100; i++) { - expectedSum += 1; - d0.dispatch(); - - expectedSum += i; - d1.dispatch(i); - - expectedSum += 2 * i; - d2.dispatch(i, i); - - expectedSum += 3 * i; - d3.dispatch(i, i, i); - - expectedSum += 4 * i; - d4.dispatch(i, i, i, i); - - expectedSum += 5 * i; - d5.dispatch(i, i, i, i, i); - - expectedSum += 6 * i; - d6.dispatch(i, i, i, i, i, i); - - expectedSum += 7 * i; - d7.dispatch(i, i, i, i, i, i, i); - - expectedSum += 8 * i; - d8.dispatch(i, i, i, i, i, i, i, i); - - expectedSum += 9 * i; - d9.dispatch(i, i, i, i, i, i, i, i, i); - } - - assertEquals(expectedSum, observerClass1.getCount(), "callbacks not invoked correctly"); - assertEquals(expectedSum, observerClass1.getCount(), "callbacks not invoked correctly"); - assertEquals(expectedSum, observerClass1.getCount(), "callbacks not invoked correctly"); - } - - @Test - @DisplayName("Multiple Triggers On Single Method Test") - void multipleTriggersOnSingleMethodTest() { - final DispatchBuilder dispatchBuilder = new DispatchBuilder(config); - - final ObserverClassMultipleTriggersOnMethod object = new ObserverClassMultipleTriggersOnMethod(); - dispatchBuilder.registerObservers(object); - - dispatchBuilder.start(); - - final TestDispatchOne dispatcherA = dispatchBuilder.getDispatcher(this, TestDispatchOne.class)::dispatch; - final TestDispatchOneB dispatcherB = dispatchBuilder.getDispatcher(this, TestDispatchOneB.class)::dispatch; - final TestDispatchOneC dispatcherC = dispatchBuilder.getDispatcher(this, TestDispatchOneC.class)::dispatch; - final TestDispatchOneD dispatcherD = dispatchBuilder.getDispatcher(this, TestDispatchOneD.class)::dispatch; - - int expectedSum = 0; - for (int i = 0; i < 100; i++) { - - expectedSum += i; - dispatcherA.dispatch(i); - - expectedSum += i * 2; - dispatcherB.dispatch(i * 2); - - expectedSum += i * 3; - dispatcherC.dispatch(i * 3); - - expectedSum += i * 4; - dispatcherD.dispatch(i * 4); - } - - assertEquals(expectedSum, object.getCount(), "callbacks not invoked correctly"); - } -} diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/components/state/StateManagementComponentTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/components/state/StateManagementComponentTests.java index f2d6f0718dc6..e870e55b3d28 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/components/state/StateManagementComponentTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/components/state/StateManagementComponentTests.java @@ -32,8 +32,6 @@ import com.swirlds.common.threading.manager.AdHocThreadManager; import com.swirlds.config.extensions.test.fixtures.TestConfigBuilder; import com.swirlds.platform.config.StateConfig_; -import com.swirlds.platform.dispatch.DispatchBuilder; -import com.swirlds.platform.dispatch.DispatchConfiguration; import com.swirlds.platform.state.RandomSignedStateGenerator; import com.swirlds.platform.state.signed.ReservedSignedState; import com.swirlds.platform.state.signed.SignedState; @@ -213,11 +211,6 @@ private DefaultStateManagementComponent newStateManagementComponent( } }; - final DispatchConfiguration dispatchConfiguration = - platformContext.getConfiguration().getConfigData(DispatchConfiguration.class); - - final DispatchBuilder dispatchBuilder = new DispatchBuilder(dispatchConfiguration); - final DefaultStateManagementComponent stateManagementComponent = new DefaultStateManagementComponent( platformContext, AdHocThreadManager.getStaticThreadManager(), @@ -227,8 +220,6 @@ private DefaultStateManagementComponent newStateManagementComponent( new SignedStateMetrics(new NoOpMetrics()), x -> true); - dispatchBuilder.start(); - return stateManagementComponent; } } diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/util/DeadlockSentinelTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/util/DeadlockSentinelTests.java deleted file mode 100644 index cfd0f8b5e944..000000000000 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/util/DeadlockSentinelTests.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (C) 2022-2024 Hedera Hashgraph, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.swirlds.platform.util; - -import static com.swirlds.common.test.fixtures.AssertionUtils.assertEventuallyTrue; -import static com.swirlds.common.threading.interrupt.Uninterruptable.abortAndLogIfInterrupted; -import static com.swirlds.common.threading.manager.AdHocThreadManager.getStaticThreadManager; -import static com.swirlds.platform.DispatchBuilderUtils.getDefaultDispatchConfiguration; -import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import com.swirlds.common.threading.framework.config.ThreadConfiguration; -import com.swirlds.platform.dispatch.DispatchBuilder; -import com.swirlds.platform.dispatch.triggers.error.DeadlockTrigger; -import java.time.Duration; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -@DisplayName("DeadlockSentinel Tests") -class DeadlockSentinelTests { - - /** - * Start deadlocked threads. Returns an auto-closeable object that stopps the deadlocked threads. - */ - private AutoCloseable startDeadlock() throws InterruptedException { - final Lock lock1 = new ReentrantLock(); - final Lock lock2 = new ReentrantLock(); - - final CountDownLatch waitForThreads = new CountDownLatch(2); - final CountDownLatch waitForDeadlock = new CountDownLatch(1); - - final Thread thread1 = new ThreadConfiguration(getStaticThreadManager()) - .setThreadName("thread1") - .setRunnable(() -> { - lock1.lock(); - waitForThreads.countDown(); - abortAndLogIfInterrupted(waitForDeadlock::await, "test thread interrupted"); - try { - lock2.lockInterruptibly(); - } catch (InterruptedException e) { - // ignored - } - }) - .build(true); - final Thread thread2 = new ThreadConfiguration(getStaticThreadManager()) - .setThreadName("thread2") - .setRunnable(() -> { - lock2.lock(); - waitForThreads.countDown(); - abortAndLogIfInterrupted(waitForDeadlock::await, "test thread interrupted"); - try { - lock1.lockInterruptibly(); - } catch (InterruptedException e) { - // ignored - } - }) - .build(true); - - waitForThreads.await(); - waitForDeadlock.countDown(); - - return () -> { - thread1.interrupt(); - thread2.interrupt(); - }; - } - - @Test - @DisplayName("Basic Deadlock Test") - void basicDeadlockTest() throws InterruptedException { - - final DispatchBuilder dispatchBuilder = new DispatchBuilder(getDefaultDispatchConfiguration()); - try (final DeadlockSentinel sentinel = - new DeadlockSentinel(getStaticThreadManager(), dispatchBuilder, Duration.ofMillis(50))) { - - final AtomicInteger deadlockCount = new AtomicInteger(0); - dispatchBuilder.registerObserver(this, DeadlockTrigger.class, deadlockCount::getAndIncrement); - - dispatchBuilder.start(); - sentinel.start(); - - // Wait a little while. Sentinel should not detect any deadlocks yet. - MILLISECONDS.sleep(200); - assertEquals(0, deadlockCount.get(), "no deadlocks should have been collected"); - - try (final AutoCloseable deadlock = startDeadlock()) { - - assertEventuallyTrue( - () -> deadlockCount.get() > 0, Duration.ofSeconds(1), "should have detected deadlock by now"); - - } catch (final Exception e) { - throw new RuntimeException(e); - } - } - } -} diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/DispatchBuilderUtils.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/DispatchBuilderUtils.java deleted file mode 100644 index d4ab3ecb84fc..000000000000 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/DispatchBuilderUtils.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2016-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.test; - -import com.swirlds.platform.dispatch.DispatchConfiguration; - -/** - * Utilities for tests utilizing the {@link com.swirlds.platform.dispatch.DispatchBuilder}. - */ -public final class DispatchBuilderUtils { - - private DispatchBuilderUtils() {} - - private static DispatchConfiguration defaultConfiguration = new DispatchConfiguration(false, "", "", "", ""); - - /** - * Get a default configuration for the dispatch builder. - */ - public static DispatchConfiguration getDefaultDispatchConfiguration() { - return defaultConfiguration; - } -} diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/IssHandlerTests.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/IssHandlerTests.java index 17b693ce3f8a..56f634c92388 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/IssHandlerTests.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/IssHandlerTests.java @@ -26,13 +26,13 @@ import com.swirlds.config.extensions.test.fixtures.TestConfigBuilder; import com.swirlds.platform.components.common.output.FatalErrorConsumer; import com.swirlds.platform.config.StateConfig; -import com.swirlds.platform.dispatch.triggers.control.HaltRequestedConsumer; import com.swirlds.platform.state.iss.IssHandler; import com.swirlds.platform.state.iss.IssScratchpad; import com.swirlds.platform.system.state.notifications.IssNotification; import com.swirlds.platform.system.state.notifications.IssNotification.IssType; import com.swirlds.platform.test.fixtures.SimpleScratchpad; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -48,7 +48,7 @@ void otherIssAlwaysFreeze() { final AtomicInteger freezeCount = new AtomicInteger(); final AtomicInteger shutdownCount = new AtomicInteger(); - final HaltRequestedConsumer haltRequestedConsumer = (final String reason) -> freezeCount.getAndIncrement(); + final Consumer haltRequestedConsumer = (final String reason) -> freezeCount.getAndIncrement(); final FatalErrorConsumer fatalErrorConsumer = (msg, t, code) -> shutdownCount.getAndIncrement(); @@ -81,7 +81,7 @@ void otherIssNoAction() { final AtomicInteger freezeCount = new AtomicInteger(); final AtomicInteger shutdownCount = new AtomicInteger(); - final HaltRequestedConsumer haltRequestedConsumer = (final String reason) -> freezeCount.getAndIncrement(); + final Consumer haltRequestedConsumer = (final String reason) -> freezeCount.getAndIncrement(); final FatalErrorConsumer fatalErrorConsumer = (msg, t, code) -> shutdownCount.getAndIncrement(); @@ -109,7 +109,7 @@ void selfIssAutomatedRecovery() { final AtomicInteger freezeCount = new AtomicInteger(); final AtomicInteger shutdownCount = new AtomicInteger(); - final HaltRequestedConsumer haltRequestedConsumer = (final String reason) -> freezeCount.getAndIncrement(); + final Consumer haltRequestedConsumer = (final String reason) -> freezeCount.getAndIncrement(); final FatalErrorConsumer fatalErrorConsumer = (msg, t, code) -> shutdownCount.getAndIncrement(); @@ -139,7 +139,7 @@ void selfIssNoAction() { final AtomicInteger freezeCount = new AtomicInteger(); final AtomicInteger shutdownCount = new AtomicInteger(); - final HaltRequestedConsumer haltRequestedConsumer = (final String reason) -> freezeCount.getAndIncrement(); + final Consumer haltRequestedConsumer = (final String reason) -> freezeCount.getAndIncrement(); final FatalErrorConsumer fatalErrorConsumer = (msg, t, code) -> shutdownCount.getAndIncrement(); @@ -169,7 +169,7 @@ void selfIssAlwaysFreeze() { final AtomicInteger freezeCount = new AtomicInteger(); final AtomicInteger shutdownCount = new AtomicInteger(); - final HaltRequestedConsumer haltRequestedConsumer = (final String reason) -> freezeCount.getAndIncrement(); + final Consumer haltRequestedConsumer = (final String reason) -> freezeCount.getAndIncrement(); final FatalErrorConsumer fatalErrorConsumer = (msg, t, code) -> shutdownCount.getAndIncrement(); @@ -205,7 +205,7 @@ void catastrophicIssNoAction() { final AtomicInteger freezeCount = new AtomicInteger(); final AtomicInteger shutdownCount = new AtomicInteger(); - final HaltRequestedConsumer haltRequestedConsumer = (final String reason) -> freezeCount.getAndIncrement(); + final Consumer haltRequestedConsumer = (final String reason) -> freezeCount.getAndIncrement(); final FatalErrorConsumer fatalErrorConsumer = (msg, t, code) -> shutdownCount.getAndIncrement(); @@ -235,7 +235,7 @@ void catastrophicIssAlwaysFreeze() { final AtomicInteger freezeCount = new AtomicInteger(); final AtomicInteger shutdownCount = new AtomicInteger(); - final HaltRequestedConsumer haltRequestedConsumer = (final String reason) -> freezeCount.getAndIncrement(); + final Consumer haltRequestedConsumer = (final String reason) -> freezeCount.getAndIncrement(); final FatalErrorConsumer fatalErrorConsumer = (msg, t, code) -> shutdownCount.getAndIncrement(); @@ -271,7 +271,7 @@ void catastrophicIssFreezeOnCatastrophic() { final AtomicInteger freezeCount = new AtomicInteger(); final AtomicInteger shutdownCount = new AtomicInteger(); - final HaltRequestedConsumer haltRequestedConsumer = (final String reason) -> freezeCount.getAndIncrement(); + final Consumer haltRequestedConsumer = (final String reason) -> freezeCount.getAndIncrement(); final FatalErrorConsumer fatalErrorConsumer = (msg, t, code) -> shutdownCount.getAndIncrement(); From 2e7682d69bf5346cc2a3b1ba2bae52c7587dda32 Mon Sep 17 00:00:00 2001 From: Cody Littley <56973212+cody-littley@users.noreply.github.com> Date: Thu, 7 Mar 2024 13:57:43 -0600 Subject: [PATCH 037/115] fix: properly handle services software version migration (#11957) Signed-off-by: Cody Littley --- .../java/com/hedera/node/app/ServicesMain.java | 1 + .../com/swirlds/platform/PlatformBuilder.java | 16 ++++++++++++++++ .../platform/system/StaticSoftwareVersion.java | 12 +++++++++++- .../system/events/BaseEventHashedData.java | 3 ++- 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/ServicesMain.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/ServicesMain.java index 49cd06c98107..028f76d6a2c6 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 @@ -153,6 +153,7 @@ public static void main(final String... args) throws Exception { logger.info("Starting node {} with version {}", selfId, version); final PlatformBuilder builder = new PlatformBuilder( Hedera.APP_NAME, Hedera.SWIRLD_NAME, version, hedera::newState, selfId) + .withPreviousSoftwareVersionClassId(0x6f2b1bc2df8cbd0bL /* SerializableSemVers.CLASS_ID */) .withConfigurationBuilder(config); final Platform platform = builder.build(); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/PlatformBuilder.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/PlatformBuilder.java index e81530f3b713..2bc27c30eeb5 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/PlatformBuilder.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/PlatformBuilder.java @@ -63,6 +63,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.nio.file.Path; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -163,6 +164,21 @@ public PlatformBuilder withConfigPath(@NonNull final Path path) { return this; } + /** + * Provide the platform with the class ID of the previous software version. Needed at migration boundaries if the + * class ID of the software version has changed. + * + * @param previousSoftwareVersionClassId the class ID of the previous software version + * @return this + */ + public PlatformBuilder withPreviousSoftwareVersionClassId(final long previousSoftwareVersionClassId) { + final Set softwareVersions = new HashSet<>(); + softwareVersions.add(softwareVersion.getClassId()); + softwareVersions.add(previousSoftwareVersionClassId); + StaticSoftwareVersion.setSoftwareVersion(softwareVersions); + return this; + } + /** * Build the configuration for the node. * diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/StaticSoftwareVersion.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/StaticSoftwareVersion.java index 64d25dbb76ab..f152729fd9f6 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/StaticSoftwareVersion.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/StaticSoftwareVersion.java @@ -17,6 +17,7 @@ package com.swirlds.platform.system; import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Objects; import java.util.Set; /** @@ -36,7 +37,7 @@ public final class StaticSoftwareVersion { private StaticSoftwareVersion() {} /** - * Get the current software version. + * Set the current software version. * * @param softwareVersion the current software version */ @@ -44,6 +45,15 @@ public static void setSoftwareVersion(@NonNull final SoftwareVersion softwareVer softwareVersionClassIdSet = Set.of(softwareVersion.getClassId()); } + /** + * Set the current software version. + * + * @param softwareVersions the current software versions (there may be multiple versions during a migration) + */ + public static void setSoftwareVersion(@NonNull final Set softwareVersions) { + softwareVersionClassIdSet = Objects.requireNonNull(softwareVersions); + } + /** * Reset this object. Required for testing. */ diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/BaseEventHashedData.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/BaseEventHashedData.java index 9a83578152e2..0ef792985019 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/BaseEventHashedData.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/BaseEventHashedData.java @@ -29,6 +29,7 @@ import com.swirlds.common.utility.CommonUtils; import com.swirlds.platform.config.TransactionConfig; import com.swirlds.platform.system.SoftwareVersion; +import com.swirlds.platform.system.StaticSoftwareVersion; import com.swirlds.platform.system.address.AddressBook; import com.swirlds.platform.system.transaction.ConsensusTransactionImpl; import com.swirlds.platform.system.transaction.StateSignatureTransaction; @@ -208,7 +209,7 @@ public void deserialize( throws IOException { Objects.requireNonNull(in, "The input stream must not be null"); serializedVersion = version; - softwareVersion = in.readSerializable(); + softwareVersion = in.readSerializable(StaticSoftwareVersion.getSoftwareVersionClassIdSet()); creatorId = in.readSerializable(false, NodeId::new); if (creatorId == null) { From 0f7fa2fda618aa7215ea5eb8c98a464f0ed61ab1 Mon Sep 17 00:00:00 2001 From: Austin Littley <102969658+alittley@users.noreply.github.com> Date: Thu, 7 Mar 2024 17:16:07 -0500 Subject: [PATCH 038/115] feat: Rework extraction utils (#11962) Signed-off-by: Austin Littley --- ... => SystemTransactionExtractionUtils.java} | 45 +++++++++---------- .../wiring/components/IssDetectorWiring.java | 5 ++- .../StateSignatureCollectorWiring.java | 8 ++-- ...ystemTransactionExtractionUtilsTests.java} | 38 +++++++++++----- .../TransactionHandlingTestUtils.java | 30 ++++++++++++- 5 files changed, 85 insertions(+), 41 deletions(-) rename platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/transaction/system/{SystemTransactionExtractor.java => SystemTransactionExtractionUtils.java} (60%) rename platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/components/{SystemTransactionExtractorTests.java => SystemTransactionExtractionUtilsTests.java} (50%) diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/transaction/system/SystemTransactionExtractor.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/transaction/system/SystemTransactionExtractionUtils.java similarity index 60% rename from platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/transaction/system/SystemTransactionExtractor.java rename to platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/transaction/system/SystemTransactionExtractionUtils.java index a8202c08f506..a89a3b3664ee 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/transaction/system/SystemTransactionExtractor.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/transaction/system/SystemTransactionExtractionUtils.java @@ -30,47 +30,44 @@ import java.util.Objects; /** - * Extracts a particular type of system transaction from an event or a round. + * Contains utility methods for extracting a particular type of system transaction from an event or a round. */ -public class SystemTransactionExtractor { - /** the system transaction type to extract */ - private final Class systemTransactionType; - +public class SystemTransactionExtractionUtils { /** - * Constructs a new extractor for the given system transaction type. - * - * @param systemTransactionType - * the system transaction type to extract + * Hidden constructor. */ - public SystemTransactionExtractor(@NonNull final Class systemTransactionType) { - this.systemTransactionType = Objects.requireNonNull(systemTransactionType); - } + private SystemTransactionExtractionUtils() {} /** - * Extracts the system transactions from the given round. + * Extracts system transactions of a given type from a round. * - * @param round - * the round to extract from + * @param round the round to extract from + * @param systemTransactionTypeClass the class of system transaction to extract + * @param the type of system transaction to extract * @return the extracted system transactions, or {@code null} if there are none */ - public @Nullable List> handleRound(@NonNull final ConsensusRound round) { + public static @Nullable List> extractFromRound( + @NonNull final ConsensusRound round, @NonNull final Class systemTransactionTypeClass) { + return round.getConsensusEvents().stream() - .map(this::handleEvent) + .map(event -> extractFromEvent(event, systemTransactionTypeClass)) .filter(Objects::nonNull) .flatMap(List::stream) - .collect(collectingAndThen(toList(), l -> l.isEmpty() ? null : l)); + .collect(collectingAndThen(toList(), list -> list.isEmpty() ? null : list)); } /** - * Extracts the system transactions from the given event. + * Extracts system transactions of a given type from an event. * - * @param event - * the event to extract from + * @param event the event to extract from + * @param systemTransactionTypeClass the class of system transaction to extract + * @param the type of system transaction to extract * @return the extracted system transactions, or {@code null} if there are none */ @SuppressWarnings("unchecked") - public @Nullable List> handleEvent(@NonNull final BaseEvent event) { - // no transactions to transform + public static @Nullable List> extractFromEvent( + @NonNull final BaseEvent event, @NonNull final Class systemTransactionTypeClass) { + final var transactions = event.getHashedData().getTransactions(); if (transactions == null) { return null; @@ -79,7 +76,7 @@ public SystemTransactionExtractor(@NonNull final Class systemTransactionType) final List> scopedTransactions = new ArrayList<>(); for (final Transaction transaction : event.getHashedData().getTransactions()) { - if (systemTransactionType.isInstance(transaction)) { + if (systemTransactionTypeClass.isInstance(transaction)) { scopedTransactions.add(new ScopedSystemTransaction<>( event.getHashedData().getCreatorId(), event.getHashedData().getSoftwareVersion(), diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/IssDetectorWiring.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/IssDetectorWiring.java index 363afc880fb5..fbef264399b5 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/IssDetectorWiring.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/IssDetectorWiring.java @@ -23,7 +23,7 @@ import com.swirlds.common.wiring.wires.input.InputWire; import com.swirlds.common.wiring.wires.output.OutputWire; import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; -import com.swirlds.platform.components.transaction.system.SystemTransactionExtractor; +import com.swirlds.platform.components.transaction.system.SystemTransactionExtractionUtils; import com.swirlds.platform.internal.ConsensusRound; import com.swirlds.platform.state.iss.IssDetector; import com.swirlds.platform.state.signed.ReservedSignedState; @@ -67,7 +67,8 @@ public static IssDetectorWiring create( model, "extractSignaturesForIssDetector", "consensus round", - new SystemTransactionExtractor<>(StateSignatureTransaction.class)::handleRound); + round -> SystemTransactionExtractionUtils.extractFromRound( + round, StateSignatureTransaction.class)); final InputWire>> sigInput = taskScheduler.buildInputWire("post consensus signatures"); roundTransformer.getOutputWire().solderTo(sigInput); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/StateSignatureCollectorWiring.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/StateSignatureCollectorWiring.java index 744870c83aeb..0dc65395969b 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/StateSignatureCollectorWiring.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/StateSignatureCollectorWiring.java @@ -23,7 +23,7 @@ import com.swirlds.common.wiring.wires.input.InputWire; import com.swirlds.common.wiring.wires.output.OutputWire; import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; -import com.swirlds.platform.components.transaction.system.SystemTransactionExtractor; +import com.swirlds.platform.components.transaction.system.SystemTransactionExtractionUtils; import com.swirlds.platform.event.GossipEvent; import com.swirlds.platform.internal.ConsensusRound; import com.swirlds.platform.state.signed.ReservedSignedState; @@ -76,7 +76,8 @@ private StateSignatureCollectorWiring( model, "extractPreconsensusSignatureTransactions", "preconsensus events", - new SystemTransactionExtractor<>(StateSignatureTransaction.class)::handleEvent); + event -> SystemTransactionExtractionUtils.extractFromEvent( + event, StateSignatureTransaction.class)); preConsensusEventInput = preConsensusTransformer.getInputWire(); preConsSigInput = taskScheduler.buildInputWire("preconsensus signature transactions"); preConsensusTransformer.getOutputWire().solderTo(preConsSigInput); @@ -87,7 +88,8 @@ private StateSignatureCollectorWiring( model, "extractConsensusSignatureTransactions", "consensus events", - new SystemTransactionExtractor<>(StateSignatureTransaction.class)::handleRound); + round -> SystemTransactionExtractionUtils.extractFromRound( + round, StateSignatureTransaction.class)); postConsensusEventInput = postConsensusTransformer.getInputWire(); postConsSigInput = taskScheduler.buildInputWire("consensus signature transactions"); postConsensusTransformer.getOutputWire().solderTo(postConsSigInput); diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/components/SystemTransactionExtractorTests.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/components/SystemTransactionExtractionUtilsTests.java similarity index 50% rename from platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/components/SystemTransactionExtractorTests.java rename to platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/components/SystemTransactionExtractionUtilsTests.java index c69af18063af..d556636c2984 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/components/SystemTransactionExtractorTests.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/components/SystemTransactionExtractionUtilsTests.java @@ -17,30 +17,46 @@ package com.swirlds.platform.test.components; import static com.swirlds.platform.test.components.TransactionHandlingTestUtils.newDummyEvent; +import static com.swirlds.platform.test.components.TransactionHandlingTestUtils.newDummyRound; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import com.swirlds.common.test.fixtures.DummySystemTransaction; import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; -import com.swirlds.platform.components.transaction.system.SystemTransactionExtractor; +import com.swirlds.platform.components.transaction.system.SystemTransactionExtractionUtils; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -class SystemTransactionExtractorTests { - +/** + * Tests for {@link SystemTransactionExtractionUtils} + */ +class SystemTransactionExtractionUtilsTests { @Test - @DisplayName("tests handling system transactions") - void testHandle() { - final SystemTransactionExtractor manager = - new SystemTransactionExtractor<>(DummySystemTransaction.class); - + @DisplayName("Handle event") + void testHandleEvent() { final List> transactions = new ArrayList<>(); - assertNull(manager.handleEvent(newDummyEvent(0))); - transactions.addAll(manager.handleEvent(newDummyEvent(1))); - transactions.addAll(manager.handleEvent(newDummyEvent(2))); + assertNull(SystemTransactionExtractionUtils.extractFromEvent(newDummyEvent(0), DummySystemTransaction.class)); + transactions.addAll( + SystemTransactionExtractionUtils.extractFromEvent(newDummyEvent(1), DummySystemTransaction.class)); + transactions.addAll( + SystemTransactionExtractionUtils.extractFromEvent(newDummyEvent(2), DummySystemTransaction.class)); assertEquals(3, transactions.size(), "incorrect number of transactions returned"); } + + @Test + @DisplayName("Handle round") + void testHandleRound() { + final List> transactions = new ArrayList<>(); + assertNull( + SystemTransactionExtractionUtils.extractFromRound(newDummyRound(0, 0), DummySystemTransaction.class)); + transactions.addAll( + SystemTransactionExtractionUtils.extractFromRound(newDummyRound(1, 1), DummySystemTransaction.class)); + transactions.addAll( + SystemTransactionExtractionUtils.extractFromRound(newDummyRound(2, 2), DummySystemTransaction.class)); + + assertEquals(5, transactions.size(), "incorrect number of transactions returned"); + } } diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/components/TransactionHandlingTestUtils.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/components/TransactionHandlingTestUtils.java index 0cdff9f8a842..0ed877fb937d 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/components/TransactionHandlingTestUtils.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/components/TransactionHandlingTestUtils.java @@ -16,9 +16,13 @@ package com.swirlds.platform.test.components; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import com.swirlds.common.crypto.CryptographyHolder; import com.swirlds.common.platform.NodeId; import com.swirlds.common.test.fixtures.DummySystemTransaction; +import com.swirlds.platform.internal.ConsensusRound; import com.swirlds.platform.internal.EventImpl; import com.swirlds.platform.system.BasicSoftwareVersion; import com.swirlds.platform.system.events.BaseEventHashedData; @@ -27,12 +31,16 @@ import com.swirlds.platform.system.events.EventDescriptor; import com.swirlds.platform.system.transaction.SystemTransaction; import java.time.Instant; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; /** * Utility functions for testing system transaction handling */ public final class TransactionHandlingTestUtils { + private TransactionHandlingTestUtils() {} + /** * Generate a new bare-bones event, containing DummySystemTransactions * @@ -40,7 +48,7 @@ public final class TransactionHandlingTestUtils { * @return the new event */ public static EventImpl newDummyEvent(final int transactionCount) { - SystemTransaction[] transactions = new SystemTransaction[transactionCount]; + final SystemTransaction[] transactions = new SystemTransaction[transactionCount]; for (int index = 0; index < transactionCount; index++) { transactions[index] = new DummySystemTransaction(); @@ -62,4 +70,24 @@ public static EventImpl newDummyEvent(final int transactionCount) { transactions), new BaseEventUnhashedData(new NodeId(0L), new byte[0])); } + + /** + * Generate a new bare-bones consensus round, containing DummySystemTransactions + * + * @param eventCount the number of events to include in the round + * @param transactionsPerEvent the number of transactions to include in each event + * @return a bare-bones consensus round + */ + public static ConsensusRound newDummyRound(final int eventCount, final int transactionsPerEvent) { + final ConsensusRound round = mock(ConsensusRound.class); + + final List events = new ArrayList<>(); + for (int index = 0; index < eventCount; index++) { + events.add(newDummyEvent(transactionsPerEvent)); + } + + when(round.getConsensusEvents()).thenReturn(events); + + return round; + } } From 5e1097dd44a98ae14bc7bc67504f670ddcd80626 Mon Sep 17 00:00:00 2001 From: Cody Littley <56973212+cody-littley@users.noreply.github.com> Date: Fri, 8 Mar 2024 07:44:01 -0600 Subject: [PATCH 039/115] fix: flaky backpressure unit test (#11963) Signed-off-by: Cody Littley --- .../wiring/counters/BackpressureObjectCounterTests.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/counters/BackpressureObjectCounterTests.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/counters/BackpressureObjectCounterTests.java index fe3aeb498d95..3d7e3a837366 100644 --- a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/counters/BackpressureObjectCounterTests.java +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/counters/BackpressureObjectCounterTests.java @@ -105,13 +105,15 @@ void onRampTest(final int sleepMillis) throws InterruptedException { assertEquals(10, counter.getCount()); // Sleep for a little while. Thread should be unable to on ramp another element. + // Count can briefly overflow to 11, but should quickly return to 10. MILLISECONDS.sleep(50); - assertEquals(10, counter.getCount()); + assertTrue(counter.getCount() == 10 || counter.getCount() == 11); // Interrupting the thread should not unblock us. thread.interrupt(); MILLISECONDS.sleep(50); - assertEquals(10, counter.getCount()); + // Count can briefly overflow to 11, but should quickly return to 10. + assertTrue(counter.getCount() == 10 || counter.getCount() == 11); // Off ramp one element. Thread should become unblocked. counter.offRamp(); From c6ea693c84e39b95b613fbdf1e4ebe5592a9965d Mon Sep 17 00:00:00 2001 From: Edward Wertz <123979964+edward-swirldslabs@users.noreply.github.com> Date: Fri, 8 Mar 2024 09:35:26 -0600 Subject: [PATCH 040/115] refactor: PlatformContext to configure Consensus (#11961) Signed-off-by: Edward Wertz --- .../swirlds-platform-core/build.gradle.kts | 2 +- .../platform/core/jmh/ConsensusBenchmark.java | 14 +- .../com/swirlds/platform/ConsensusImpl.java | 25 ++-- .../com/swirlds/platform/SwirldsPlatform.java | 7 +- .../consensus/ThreadSafeConsensusInfo.java | 38 +++--- .../test/consensus/GenerateConsensus.java | 12 +- .../platform/test/consensus/TestIntake.java | 14 +- .../framework/ConsensusTestNode.java | 25 ++-- .../framework/ConsensusTestOrchestrator.java | 20 ++- .../framework/OrchestratorBuilder.java | 36 +++-- .../test/consensus/framework/TestInput.java | 13 +- .../platform/test/gui/TestGuiSource.java | 16 ++- .../cli/EventStreamReportingToolTest.java | 10 +- .../cli/EventStreamSingleFileRepairTest.java | 5 +- .../test/consensus/ConsensusTestArgs.java | 128 ++++++++++++------ .../consensus/ConsensusTestDefinitions.java | 2 +- .../test/consensus/ConsensusTestParams.java | 9 +- .../test/consensus/ConsensusTestRunner.java | 6 +- .../test/consensus/ConsensusTests.java | 3 +- .../consensus/IntakeAndConsensusTests.java | 19 ++- .../platform/test/gui/HashgraphGuiTest.java | 6 +- 21 files changed, 243 insertions(+), 167 deletions(-) diff --git a/platform-sdk/swirlds-platform-core/build.gradle.kts b/platform-sdk/swirlds-platform-core/build.gradle.kts index 9e9d62534fe8..52fb926dbe75 100644 --- a/platform-sdk/swirlds-platform-core/build.gradle.kts +++ b/platform-sdk/swirlds-platform-core/build.gradle.kts @@ -28,7 +28,7 @@ mainModuleInfo { jmhModuleInfo { requires("com.swirlds.base") - requires("com.swirlds.config.api") + requires("com.swirlds.common") requires("com.swirlds.platform.core") requires("com.swirlds.platform.test") requires("com.swirlds.common.test.fixtures") diff --git a/platform-sdk/swirlds-platform-core/src/jmh/java/com/swirlds/platform/core/jmh/ConsensusBenchmark.java b/platform-sdk/swirlds-platform-core/src/jmh/java/com/swirlds/platform/core/jmh/ConsensusBenchmark.java index 50d753692671..1c738c2c5ffd 100644 --- a/platform-sdk/swirlds-platform-core/src/jmh/java/com/swirlds/platform/core/jmh/ConsensusBenchmark.java +++ b/platform-sdk/swirlds-platform-core/src/jmh/java/com/swirlds/platform/core/jmh/ConsensusBenchmark.java @@ -17,13 +17,11 @@ package com.swirlds.platform.core.jmh; import com.swirlds.base.utility.Pair; +import com.swirlds.common.context.PlatformContext; import com.swirlds.common.test.fixtures.WeightGenerators; -import com.swirlds.config.api.Configuration; +import com.swirlds.common.test.fixtures.platform.TestPlatformContextBuilder; import com.swirlds.platform.Consensus; import com.swirlds.platform.ConsensusImpl; -import com.swirlds.platform.config.DefaultConfiguration; -import com.swirlds.platform.consensus.ConsensusConfig; -import com.swirlds.platform.eventhandling.EventConfig; import com.swirlds.platform.test.NoOpConsensusMetrics; import com.swirlds.platform.test.event.emitter.StandardEventEmitter; import com.swirlds.platform.test.event.source.EventSourceFactory; @@ -77,12 +75,12 @@ public void setup() throws Exception { final StandardEventEmitter emitter = new StandardEventEmitter(generator); events = emitter.emitEvents(numEvents); - final Configuration configuration = DefaultConfiguration.buildBasicConfiguration(); + final PlatformContext platformContext = + TestPlatformContextBuilder.create().build(); consensus = new ConsensusImpl( - configuration.getConfigData(ConsensusConfig.class), + platformContext, new NoOpConsensusMetrics(), - emitter.getGraphGenerator().getAddressBook(), - configuration.getConfigData(EventConfig.class).getAncientMode()); + emitter.getGraphGenerator().getAddressBook()); } @Benchmark diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/ConsensusImpl.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/ConsensusImpl.java index b3bc93ad8630..7dbd873c95ee 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/ConsensusImpl.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/ConsensusImpl.java @@ -20,11 +20,11 @@ import static com.swirlds.logging.legacy.LogMarker.STARTUP; import static com.swirlds.platform.consensus.ConsensusConstants.FIRST_CONSENSUS_NUMBER; +import com.swirlds.common.context.PlatformContext; import com.swirlds.common.platform.NodeId; import com.swirlds.common.utility.Threshold; import com.swirlds.platform.consensus.AncestorSearch; import com.swirlds.platform.consensus.CandidateWitness; -import com.swirlds.platform.consensus.ConsensusConfig; import com.swirlds.platform.consensus.ConsensusConstants; import com.swirlds.platform.consensus.ConsensusRounds; import com.swirlds.platform.consensus.ConsensusSnapshot; @@ -34,9 +34,9 @@ import com.swirlds.platform.consensus.InitJudges; import com.swirlds.platform.consensus.NonAncientEventWindow; import com.swirlds.platform.consensus.RoundElections; -import com.swirlds.platform.consensus.SequentialRingBuffer; import com.swirlds.platform.consensus.ThreadSafeConsensusInfo; import com.swirlds.platform.event.AncientMode; +import com.swirlds.platform.eventhandling.EventConfig; import com.swirlds.platform.gossip.shadowgraph.Generations; import com.swirlds.platform.internal.ConsensusRound; import com.swirlds.platform.internal.EventImpl; @@ -52,7 +52,6 @@ import java.util.Iterator; import java.util.LinkedList; import java.util.List; -import java.util.Objects; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -144,8 +143,6 @@ public class ConsensusImpl extends ThreadSafeConsensusInfo implements Consensus { private static final Logger logger = LogManager.getLogger(ConsensusImpl.class); - /** consensus configuration */ - private final ConsensusConfig config; /** the only address book currently, until address book changes are implemented */ private final AddressBook addressBook; /** metrics related to consensus */ @@ -195,25 +192,25 @@ public class ConsensusImpl extends ThreadSafeConsensusInfo implements Consensus /** * Constructs an empty object (no events) to keep track of elections and calculate consensus. * - * @param config consensus configuration + * @param platformContext the platform context containing configuration * @param consensusMetrics metrics related to consensus - * @param addressBook the global address book, which never changes - * @param ancientMode describes how we are currently computing "ancientness" of events + * @param addressBook the global address book, which never changes */ public ConsensusImpl( - @NonNull final ConsensusConfig config, + @NonNull final PlatformContext platformContext, @NonNull final ConsensusMetrics consensusMetrics, - @NonNull final AddressBook addressBook, - @NonNull final AncientMode ancientMode) { - super(config, new SequentialRingBuffer<>(ConsensusConstants.ROUND_FIRST, config.roundsExpired() * 2)); - this.config = config; + @NonNull final AddressBook addressBook) { + super(platformContext); this.consensusMetrics = consensusMetrics; // until we implement address book changes, we will just use the use this address book this.addressBook = addressBook; this.rounds = new ConsensusRounds(config, getStorage(), addressBook); - this.ancientMode = Objects.requireNonNull(ancientMode); + this.ancientMode = platformContext + .getConfiguration() + .getConfigData(EventConfig.class) + .getAncientMode(); } @Override 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 9f5953565d18..f4a7d3e02d48 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 @@ -70,7 +70,6 @@ import com.swirlds.platform.config.StateConfig; import com.swirlds.platform.config.ThreadConfig; import com.swirlds.platform.config.TransactionConfig; -import com.swirlds.platform.consensus.ConsensusConfig; import com.swirlds.platform.consensus.NonAncientEventWindow; import com.swirlds.platform.crypto.CryptoStatic; import com.swirlds.platform.crypto.KeysAndCerts; @@ -741,11 +740,7 @@ public class SwirldsPlatform implements Platform { intakeEventCounter, () -> emergencyState.getState("emergency reconnect")) {}; - consensusRef.set(new ConsensusImpl( - platformContext.getConfiguration().getConfigData(ConsensusConfig.class), - consensusMetrics, - getAddressBook(), - ancientMode)); + consensusRef.set(new ConsensusImpl(platformContext, consensusMetrics, getAddressBook())); if (startedFromGenesis) { initialAncientThreshold = 0; diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/consensus/ThreadSafeConsensusInfo.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/consensus/ThreadSafeConsensusInfo.java index dd5455a3f71c..d9ce1c3428b4 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/consensus/ThreadSafeConsensusInfo.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/consensus/ThreadSafeConsensusInfo.java @@ -16,6 +16,8 @@ package com.swirlds.platform.consensus; +import com.swirlds.common.context.PlatformContext; +import com.swirlds.config.api.Configuration; import com.swirlds.logging.legacy.LogMarker; import com.swirlds.platform.state.MinimumJudgeInfo; import edu.umd.cs.findbugs.annotations.NonNull; @@ -23,18 +25,18 @@ import org.apache.logging.log4j.Logger; /** - * All information provided by {@link com.swirlds.platform.Consensus} that needs to be accessed at - * any time by any thread. + * All information provided by {@link com.swirlds.platform.Consensus} that needs to be accessed at any time by any + * thread. */ public class ThreadSafeConsensusInfo implements GraphGenerations, RoundNumberProvider { private static final Logger LOG = LogManager.getLogger(ThreadSafeConsensusInfo.class); - private final ConsensusConfig config; + protected final ConsensusConfig config; private final SequentialRingBuffer storage; /** - * The minimum judge generation number from the oldest non-expired round, if we have expired any - * rounds. Else, this is {@link GraphGenerations#FIRST_GENERATION}. + * The minimum judge generation number from the oldest non-expired round, if we have expired any rounds. Else, this + * is {@link GraphGenerations#FIRST_GENERATION}. * *

    Updated only on consensus thread, read concurrently from gossip threads. */ @@ -44,35 +46,35 @@ public class ThreadSafeConsensusInfo implements GraphGenerations, RoundNumberPro private volatile long minGenNonAncient = GraphGenerations.FIRST_GENERATION; /** - * The minimum judge generation number from the most recent fame-decided round, if there is one. - * Else, this is {@link GraphGenerations#FIRST_GENERATION}. + * The minimum judge generation number from the most recent fame-decided round, if there is one. Else, this is + * {@link GraphGenerations#FIRST_GENERATION}. * *

    Updated only on consensus thread, read concurrently from gossip threads. */ private volatile long maxRoundGeneration = GraphGenerations.FIRST_GENERATION; /** - * maximum round number of all events stored in "storage", or -1 if none. This is the max round - * created of all events ever added to the hashgraph. + * maximum round number of all events stored in "storage", or -1 if none. This is the max round created of all + * events ever added to the hashgraph. */ private volatile long maxRound = ConsensusConstants.ROUND_UNDEFINED; /** - * minimum round number of all events stored in "storage", or -1 if none. This may not be the min - * round created of all events ever added to the hashgraph, since some of the older rounds may - * have been decided and discarded. + * minimum round number of all events stored in "storage", or -1 if none. This may not be the min round created of + * all events ever added to the hashgraph, since some of the older rounds may have been decided and discarded. */ private volatile long minRound = ConsensusConstants.ROUND_UNDEFINED; /** fame has been decided for all rounds less than this, but not for this round. */ private volatile long fameDecidedBelow = ConsensusConstants.ROUND_FIRST; /** - * @param config consensus configuration - * @param storage round storage + * @param platformContext platform context */ - public ThreadSafeConsensusInfo( - @NonNull final ConsensusConfig config, @NonNull final SequentialRingBuffer storage) { - this.config = config; - this.storage = storage; + public ThreadSafeConsensusInfo(@NonNull final PlatformContext platformContext) { + final Configuration config = platformContext.getConfiguration(); + this.config = config.getConfigData(ConsensusConfig.class); + this.storage = new SequentialRingBuffer<>( + ConsensusConstants.ROUND_FIRST, + config.getConfigData(ConsensusConfig.class).roundsExpired() * 2); } /** diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/GenerateConsensus.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/GenerateConsensus.java index 10cd766f0b73..7e787f0b2930 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/GenerateConsensus.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/GenerateConsensus.java @@ -16,15 +16,16 @@ package com.swirlds.platform.test.consensus; -import com.swirlds.config.extensions.test.fixtures.TestConfigBuilder; -import com.swirlds.platform.consensus.ConsensusConfig; +import com.swirlds.common.context.PlatformContext; import com.swirlds.platform.internal.ConsensusRound; import com.swirlds.platform.test.fixtures.event.generator.StandardGraphGenerator; import com.swirlds.platform.test.fixtures.event.source.EventSource; import com.swirlds.platform.test.fixtures.event.source.StandardEventSource; +import edu.umd.cs.findbugs.annotations.NonNull; import java.util.ArrayList; import java.util.Deque; import java.util.List; +import java.util.Objects; import java.util.stream.IntStream; /** @@ -45,13 +46,12 @@ private GenerateConsensus() {} * @return consensus rounds */ public static Deque generateConsensusRounds( - final int numNodes, final int numEvents, final long seed) { + @NonNull PlatformContext platformContext, final int numNodes, final int numEvents, final long seed) { + Objects.requireNonNull(platformContext); final List> eventSources = new ArrayList<>(); IntStream.range(0, numNodes).forEach(i -> eventSources.add(new StandardEventSource(false))); final StandardGraphGenerator generator = new StandardGraphGenerator(seed, eventSources); - final TestIntake intake = new TestIntake( - generator.getAddressBook(), - new TestConfigBuilder().getOrCreateConfig().getConfigData(ConsensusConfig.class)); + final TestIntake intake = new TestIntake(platformContext, generator.getAddressBook()); // generate events and feed them to consensus for (int i = 0; i < numEvents; i++) { diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/TestIntake.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/TestIntake.java index 8f1528e019c0..c3d41bbb19d4 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/TestIntake.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/TestIntake.java @@ -24,15 +24,12 @@ import com.swirlds.base.time.Time; import com.swirlds.common.context.PlatformContext; import com.swirlds.common.platform.NodeId; -import com.swirlds.common.test.fixtures.platform.TestPlatformContextBuilder; import com.swirlds.common.wiring.model.WiringModel; import com.swirlds.common.wiring.schedulers.TaskScheduler; import com.swirlds.common.wiring.schedulers.builders.TaskSchedulerType; -import com.swirlds.config.extensions.test.fixtures.TestConfigBuilder; import com.swirlds.platform.Consensus; import com.swirlds.platform.ConsensusImpl; import com.swirlds.platform.components.ConsensusEngine; -import com.swirlds.platform.consensus.ConsensusConfig; import com.swirlds.platform.consensus.ConsensusSnapshot; import com.swirlds.platform.consensus.NonAncientEventWindow; import com.swirlds.platform.event.GossipEvent; @@ -76,21 +73,16 @@ public class TestIntake implements LoadableFromSignedState { private final WiringModel model; /** + * @param platformContext the platform context used to configure this intake. * @param addressBook the address book used by this intake */ - public TestIntake(@NonNull final AddressBook addressBook, @NonNull final ConsensusConfig consensusConfig) { + public TestIntake(@NonNull PlatformContext platformContext, @NonNull final AddressBook addressBook) { final NodeId selfId = new NodeId(0); final Time time = Time.getCurrent(); output = new ConsensusOutput(time); - // FUTURE WORK: Broaden this test sweet to include testing ancient threshold via birth round. - consensus = new ConsensusImpl( - consensusConfig, ConsensusUtils.NOOP_CONSENSUS_METRICS, addressBook, GENERATION_THRESHOLD); - - final PlatformContext platformContext = TestPlatformContextBuilder.create() - .withConfiguration(new TestConfigBuilder().getOrCreateConfig()) - .build(); + consensus = new ConsensusImpl(platformContext, ConsensusUtils.NOOP_CONSENSUS_METRICS, addressBook); shadowGraph = new Shadowgraph(platformContext, mock(AddressBook.class)); diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/framework/ConsensusTestNode.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/framework/ConsensusTestNode.java index d481afcdbb40..13f6f20a2bee 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/framework/ConsensusTestNode.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/framework/ConsensusTestNode.java @@ -18,9 +18,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue; -import com.swirlds.config.extensions.test.fixtures.TestConfigBuilder; +import com.swirlds.common.context.PlatformContext; import com.swirlds.platform.Consensus; -import com.swirlds.platform.consensus.ConsensusConfig; import com.swirlds.platform.consensus.ConsensusSnapshot; import com.swirlds.platform.test.consensus.TestIntake; import com.swirlds.platform.test.event.emitter.EventEmitter; @@ -42,7 +41,7 @@ public class ConsensusTestNode { * Creates a new instance. * * @param eventEmitter the emitter of events - * @param intake the instance to apply events to + * @param intake the instance to apply events to */ public ConsensusTestNode(@NonNull final EventEmitter eventEmitter, @NonNull final TestIntake intake) { this.eventEmitter = eventEmitter; @@ -53,14 +52,16 @@ public ConsensusTestNode(@NonNull final EventEmitter eventEmitter, @NonNull f /** * Creates a new instance with a freshly seeded {@link EventEmitter}. * - * @param eventEmitter the emitter of events + * @param platformContext the platform context + * @param eventEmitter the emitter of events */ - public static @NonNull ConsensusTestNode genesisContext(@NonNull final EventEmitter eventEmitter) { + public static @NonNull ConsensusTestNode genesisContext( + @NonNull final PlatformContext platformContext, @NonNull final EventEmitter eventEmitter) { return new ConsensusTestNode( eventEmitter, new TestIntake( - eventEmitter.getGraphGenerator().getAddressBook(), - new TestConfigBuilder().getOrCreateConfig().getConfigData(ConsensusConfig.class))); + Objects.requireNonNull(platformContext), + eventEmitter.getGraphGenerator().getAddressBook())); } /** Simulates a restart on a node */ @@ -75,21 +76,19 @@ public void restart() { } /** - * Create a new {@link ConsensusTestNode} that will be created by simulating a reconnect with - * this context + * Create a new {@link ConsensusTestNode} that will be created by simulating a reconnect with this context * + * @param platformContext the platform context * @return a new {@link ConsensusTestNode} */ - public @NonNull ConsensusTestNode reconnect() { + public @NonNull ConsensusTestNode reconnect(@NonNull final PlatformContext platformContext) { // create a new context final EventEmitter newEmitter = eventEmitter.cleanCopy(random.nextLong()); newEmitter.reset(); final ConsensusTestNode consensusTestNode = new ConsensusTestNode( newEmitter, - new TestIntake( - newEmitter.getGraphGenerator().getAddressBook(), - new TestConfigBuilder().getOrCreateConfig().getConfigData(ConsensusConfig.class))); + new TestIntake(platformContext, newEmitter.getGraphGenerator().getAddressBook())); consensusTestNode.intake.loadSnapshot( Objects.requireNonNull(getOutput().getConsensusRounds().peekLast()) .getSnapshot()); diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/framework/ConsensusTestOrchestrator.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/framework/ConsensusTestOrchestrator.java index 3d36d1c89587..533db914830a 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/framework/ConsensusTestOrchestrator.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/framework/ConsensusTestOrchestrator.java @@ -16,12 +16,14 @@ package com.swirlds.platform.test.consensus.framework; +import com.swirlds.common.context.PlatformContext; import com.swirlds.platform.system.address.AddressBook; import com.swirlds.platform.system.events.EventConstants; import com.swirlds.platform.test.consensus.framework.validation.ConsensusOutputValidation; import com.swirlds.platform.test.consensus.framework.validation.Validations; import com.swirlds.platform.test.fixtures.event.generator.GraphGenerator; import com.swirlds.platform.test.gui.TestGuiSource; +import edu.umd.cs.findbugs.annotations.NonNull; import java.util.List; import java.util.function.Consumer; @@ -39,9 +41,13 @@ public ConsensusTestOrchestrator( this.totalEventNum = totalEventNum; } - /** Adds a new node to the test context by simulating a reconnect */ - public void addReconnectNode() { - final ConsensusTestNode node = nodes.get(0).reconnect(); + /** + * Adds a new node to the test context by simulating a reconnect + * + * @param platformContext the platform context to use for the new node + */ + public void addReconnectNode(@NonNull PlatformContext platformContext) { + final ConsensusTestNode node = nodes.get(0).reconnect(platformContext); node.getEventEmitter().setCheckpoint(currentSequence); node.addEvents(currentSequence); nodes.add(node); @@ -116,8 +122,8 @@ public void clearOutput() { } /** - * Restarts all nodes with events and generations stored in the signed state. This is the - * currently implemented restart, it discards all non-consensus events. + * Restarts all nodes with events and generations stored in the signed state. This is the currently implemented + * restart, it discards all non-consensus events. */ public void restartAllNodes() { final long lastRoundDecided = nodes.get(0).getConsensus().getLastRoundDecided(); @@ -134,8 +140,8 @@ public void restartAllNodes() { } /** - * Configures the graph generators of all nodes with the given configurator. This must be done - * for all nodes so that the generators generate the same graphs + * Configures the graph generators of all nodes with the given configurator. This must be done for all nodes so that + * the generators generate the same graphs */ public ConsensusTestOrchestrator configGenerators(final Consumer> configurator) { for (final ConsensusTestNode node : nodes) { diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/framework/OrchestratorBuilder.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/framework/OrchestratorBuilder.java index ef2f3c7fd08c..b287f775008e 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/framework/OrchestratorBuilder.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/framework/OrchestratorBuilder.java @@ -18,9 +18,11 @@ import static com.swirlds.common.test.fixtures.WeightGenerators.BALANCED; +import com.swirlds.common.context.PlatformContext; import com.swirlds.common.test.fixtures.RandomUtils; import com.swirlds.common.test.fixtures.ResettableRandom; import com.swirlds.common.test.fixtures.WeightGenerator; +import com.swirlds.common.test.fixtures.platform.TestPlatformContextBuilder; import com.swirlds.platform.test.event.emitter.EventEmitter; import com.swirlds.platform.test.event.emitter.EventEmitterGenerator; import com.swirlds.platform.test.event.emitter.ShuffledEventEmitter; @@ -30,6 +32,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.function.Consumer; import java.util.function.Function; @@ -41,14 +44,14 @@ public class OrchestratorBuilder { private int totalEventNum = 10_000; private Function, List>> eventSourceBuilder = EventSourceFactory::newStandardEventSources; private Consumer> eventSourceConfigurator = es -> {}; + private PlatformContext platformContext = + TestPlatformContextBuilder.create().build(); /** - * A function that creates an event emitter based on a graph generator and a seed. They should - * produce emitters that will emit events in different orders. For example, nothing would be - * tested if both returned a {@link - * com.swirlds.platform.test.event.emitter.StandardEventEmitter}. It is for both to return - * {@link ShuffledEventEmitter} because they will be seeded with different values and therefore - * emit events in different orders. Each instance of consensus should receive the same events, - * but in a different order. + * A function that creates an event emitter based on a graph generator and a seed. They should produce emitters that + * will emit events in different orders. For example, nothing would be tested if both returned a + * {@link com.swirlds.platform.test.event.emitter.StandardEventEmitter}. It is for both to return + * {@link ShuffledEventEmitter} because they will be seeded with different values and therefore emit events in + * different orders. Each instance of consensus should receive the same events, but in a different order. */ private EventEmitterGenerator node1EventEmitterGenerator = ShuffledEventEmitter::new; @@ -64,11 +67,23 @@ public class OrchestratorBuilder { return this; } + /** + * Set the {@link PlatformContext} to use. If not set, uses a default context. + * + * @param platformContext + * @return this OrchestratorBuilder + */ + public @NonNull OrchestratorBuilder setPlatformContext(@NonNull final PlatformContext platformContext) { + this.platformContext = Objects.requireNonNull(platformContext); + return this; + } + public @NonNull OrchestratorBuilder setTestInput(@NonNull final TestInput testInput) { numberOfNodes = testInput.numberOfNodes(); weightGenerator = testInput.weightGenerator(); seed = testInput.seed(); totalEventNum = testInput.eventsToGenerate(); + platformContext = testInput.platformContext(); return this; } @@ -113,10 +128,9 @@ public class OrchestratorBuilder { final List nodes = new ArrayList<>(); // Create two instances to run consensus on. Each instance reseeds the emitter so that they - // emit - // events in different orders. - nodes.add(ConsensusTestNode.genesisContext(node1Emitter)); - nodes.add(ConsensusTestNode.genesisContext(node2Emitter)); + // emit events in different orders. + nodes.add(ConsensusTestNode.genesisContext(platformContext, node1Emitter)); + nodes.add(ConsensusTestNode.genesisContext(platformContext, node2Emitter)); return new ConsensusTestOrchestrator(nodes, weights, totalEventNum); } diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/framework/TestInput.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/framework/TestInput.java index dade62625bbc..f05e2c4bbf86 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/framework/TestInput.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/framework/TestInput.java @@ -16,11 +16,20 @@ package com.swirlds.platform.test.consensus.framework; +import com.swirlds.common.context.PlatformContext; import com.swirlds.common.test.fixtures.WeightGenerator; import edu.umd.cs.findbugs.annotations.NonNull; -public record TestInput(int numberOfNodes, @NonNull WeightGenerator weightGenerator, long seed, int eventsToGenerate) { +/** + * Holds the input to a consensus test. + */ +public record TestInput( + @NonNull PlatformContext platformContext, + int numberOfNodes, + @NonNull WeightGenerator weightGenerator, + long seed, + int eventsToGenerate) { public @NonNull TestInput setNumberOfNodes(int numberOfNodes) { - return new TestInput(numberOfNodes, weightGenerator, seed, eventsToGenerate); + return new TestInput(platformContext, numberOfNodes, weightGenerator, seed, eventsToGenerate); } } diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/gui/TestGuiSource.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/gui/TestGuiSource.java index d707b330ec8e..8d5926e6c78f 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/gui/TestGuiSource.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/gui/TestGuiSource.java @@ -16,8 +16,7 @@ package com.swirlds.platform.test.gui; -import com.swirlds.config.extensions.test.fixtures.TestConfigBuilder; -import com.swirlds.platform.consensus.ConsensusConfig; +import com.swirlds.common.context.PlatformContext; import com.swirlds.platform.consensus.ConsensusSnapshot; import com.swirlds.platform.gui.hashgraph.HashgraphGuiSource; import com.swirlds.platform.gui.hashgraph.internal.FinalShadowgraphGuiSource; @@ -43,13 +42,18 @@ public class TestGuiSource { private final HashgraphGuiSource guiSource; private ConsensusSnapshot savedSnapshot; - public TestGuiSource(final long seed, final int numNodes) { + /** + * Construct a {@link TestGuiSource} with the given platform context, seed, and number of nodes. + * + * @param platformContext the platform context + * @param seed the seed + * @param numNodes the number of nodes + */ + public TestGuiSource(@NonNull final PlatformContext platformContext, final long seed, final int numNodes) { graphGenerator = new StandardGraphGenerator(seed, generateSources(numNodes)); graphGenerator.reset(); - intake = new TestIntake( - graphGenerator.getAddressBook(), - new TestConfigBuilder().getOrCreateConfig().getConfigData(ConsensusConfig.class)); + intake = new TestIntake(platformContext, graphGenerator.getAddressBook()); guiSource = new FinalShadowgraphGuiSource(intake.getShadowGraph(), graphGenerator.getAddressBook()); } diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/cli/EventStreamReportingToolTest.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/cli/EventStreamReportingToolTest.java index ab75c2db222e..5108ece4f780 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/cli/EventStreamReportingToolTest.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/cli/EventStreamReportingToolTest.java @@ -16,6 +16,8 @@ package com.swirlds.platform.test.cli; +import static com.swirlds.platform.test.consensus.ConsensusTestArgs.DEFAULT_PLATFORM_CONTEXT; + import com.swirlds.common.constructable.ConstructableRegistry; import com.swirlds.common.constructable.ConstructableRegistryException; import com.swirlds.common.test.fixtures.RandomUtils; @@ -75,8 +77,8 @@ void createReportTest() throws IOException, ConstructableRegistryException { ConstructableRegistry.getInstance().registerConstructables("com.swirlds"); // generate consensus events - final Deque rounds = - GenerateConsensus.generateConsensusRounds(numNodes, numEvents, random.nextLong()); + final Deque rounds = GenerateConsensus.generateConsensusRounds( + DEFAULT_PLATFORM_CONTEXT, numNodes, numEvents, random.nextLong()); if (rounds.isEmpty()) { Assertions.fail("events are excepted to reach consensus"); } @@ -120,8 +122,8 @@ void createTimeBoundReportTest() throws IOException, ConstructableRegistryExcept ConstructableRegistry.getInstance().registerConstructables("com.swirlds"); // generate consensus events - final Deque rounds = - GenerateConsensus.generateConsensusRounds(numNodes, numEvents, random.nextLong()); + final Deque rounds = GenerateConsensus.generateConsensusRounds( + DEFAULT_PLATFORM_CONTEXT, numNodes, numEvents, random.nextLong()); if (rounds.isEmpty()) { Assertions.fail("events are excepted to reach consensus"); } diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/cli/EventStreamSingleFileRepairTest.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/cli/EventStreamSingleFileRepairTest.java index 9e182d761860..e875c364ce9b 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/cli/EventStreamSingleFileRepairTest.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/cli/EventStreamSingleFileRepairTest.java @@ -18,6 +18,7 @@ import static com.swirlds.platform.recovery.internal.EventStreamSingleFileRepairer.DAMAGED_SUFFIX; import static com.swirlds.platform.recovery.internal.EventStreamSingleFileRepairer.REPAIRED_SUFFIX; +import static com.swirlds.platform.test.consensus.ConsensusTestArgs.DEFAULT_PLATFORM_CONTEXT; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -120,8 +121,8 @@ private void createEventStreamFiles() throws ConstructableRegistryException { ConstructableRegistry.getInstance().registerConstructables("com.swirlds"); // generate consensus events - final Deque rounds = - GenerateConsensus.generateConsensusRounds(numNodes, numEvents, random.nextLong()); + final Deque rounds = GenerateConsensus.generateConsensusRounds( + DEFAULT_PLATFORM_CONTEXT, numNodes, numEvents, random.nextLong()); if (rounds.isEmpty()) { Assertions.fail("events are excepted to reach consensus"); } diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/ConsensusTestArgs.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/ConsensusTestArgs.java index 79ca872265dd..8434ab2a149b 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/ConsensusTestArgs.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/ConsensusTestArgs.java @@ -24,6 +24,8 @@ import static com.swirlds.common.test.fixtures.WeightGenerators.RANDOM_REAL_WEIGHT; import static com.swirlds.common.test.fixtures.WeightGenerators.SINGLE_NODE_STRONG_MINORITY; +import com.swirlds.common.context.PlatformContext; +import com.swirlds.common.test.fixtures.platform.TestPlatformContextBuilder; import java.util.stream.Stream; import org.junit.jupiter.params.provider.Arguments; @@ -35,41 +37,56 @@ public class ConsensusTestArgs { public static final String SINGLE_NODE_STRONG_MINORITY_DESC = "Single Node With Strong Minority Weight"; public static final String ONE_THIRD_NODES_ZERO_WEIGHT_DESC = "One Third of Nodes Have Zero Weight"; public static final String RANDOM_WEIGHT_DESC = "Random Weight, Real Total Weight Value"; + public static final PlatformContext DEFAULT_PLATFORM_CONTEXT = + TestPlatformContextBuilder.create().build(); static Stream orderInvarianceTests() { return Stream.of( - Arguments.of(new ConsensusTestParams(2, BALANCED, BALANCED_WEIGHT_DESC)), - Arguments.of(new ConsensusTestParams(2, INCREMENTING, INCREMENTAL_NODE_WEIGHT_DESC)), - Arguments.of(new ConsensusTestParams(4, INCREMENTING, INCREMENTAL_NODE_WEIGHT_DESC)), - Arguments.of(new ConsensusTestParams(9, ONE_THIRD_ZERO_WEIGHT, ONE_THIRD_NODES_ZERO_WEIGHT_DESC)), - Arguments.of(new ConsensusTestParams(50, RANDOM_REAL_WEIGHT, RANDOM_WEIGHT_DESC)), - Arguments.of(new ConsensusTestParams(50, RANDOM, RANDOM_WEIGHT_DESC))); + Arguments.of(new ConsensusTestParams(DEFAULT_PLATFORM_CONTEXT, 2, BALANCED, BALANCED_WEIGHT_DESC)), + Arguments.of(new ConsensusTestParams( + DEFAULT_PLATFORM_CONTEXT, 2, INCREMENTING, INCREMENTAL_NODE_WEIGHT_DESC)), + Arguments.of(new ConsensusTestParams( + DEFAULT_PLATFORM_CONTEXT, 4, INCREMENTING, INCREMENTAL_NODE_WEIGHT_DESC)), + Arguments.of(new ConsensusTestParams( + DEFAULT_PLATFORM_CONTEXT, 9, ONE_THIRD_ZERO_WEIGHT, ONE_THIRD_NODES_ZERO_WEIGHT_DESC)), + Arguments.of( + new ConsensusTestParams(DEFAULT_PLATFORM_CONTEXT, 50, RANDOM_REAL_WEIGHT, RANDOM_WEIGHT_DESC)), + Arguments.of(new ConsensusTestParams(DEFAULT_PLATFORM_CONTEXT, 50, RANDOM, RANDOM_WEIGHT_DESC))); } static Stream reconnectSimulation() { return Stream.of( - Arguments.of(new ConsensusTestParams(4, BALANCED, BALANCED_WEIGHT_DESC)), - Arguments.of(new ConsensusTestParams(4, INCREMENTING, INCREMENTAL_NODE_WEIGHT_DESC)), - Arguments.of(new ConsensusTestParams(4, ONE_THIRD_ZERO_WEIGHT, ONE_THIRD_NODES_ZERO_WEIGHT_DESC)), - Arguments.of(new ConsensusTestParams(4, RANDOM_REAL_WEIGHT, RANDOM_WEIGHT_DESC)), + Arguments.of(new ConsensusTestParams(DEFAULT_PLATFORM_CONTEXT, 4, BALANCED, BALANCED_WEIGHT_DESC)), + Arguments.of(new ConsensusTestParams( + DEFAULT_PLATFORM_CONTEXT, 4, INCREMENTING, INCREMENTAL_NODE_WEIGHT_DESC)), + Arguments.of(new ConsensusTestParams( + DEFAULT_PLATFORM_CONTEXT, 4, ONE_THIRD_ZERO_WEIGHT, ONE_THIRD_NODES_ZERO_WEIGHT_DESC)), + Arguments.of( + new ConsensusTestParams(DEFAULT_PLATFORM_CONTEXT, 4, RANDOM_REAL_WEIGHT, RANDOM_WEIGHT_DESC)), + Arguments.of(new ConsensusTestParams( + DEFAULT_PLATFORM_CONTEXT, 10, SINGLE_NODE_STRONG_MINORITY, SINGLE_NODE_STRONG_MINORITY_DESC)), + Arguments.of(new ConsensusTestParams( + DEFAULT_PLATFORM_CONTEXT, 10, ONE_THIRD_ZERO_WEIGHT, ONE_THIRD_NODES_ZERO_WEIGHT_DESC)), Arguments.of( - new ConsensusTestParams(10, SINGLE_NODE_STRONG_MINORITY, SINGLE_NODE_STRONG_MINORITY_DESC)), - Arguments.of(new ConsensusTestParams(10, ONE_THIRD_ZERO_WEIGHT, ONE_THIRD_NODES_ZERO_WEIGHT_DESC)), - Arguments.of(new ConsensusTestParams(10, RANDOM_REAL_WEIGHT, RANDOM_WEIGHT_DESC))); + new ConsensusTestParams(DEFAULT_PLATFORM_CONTEXT, 10, RANDOM_REAL_WEIGHT, RANDOM_WEIGHT_DESC))); } static Stream staleEvent() { return Stream.of( - Arguments.of(new ConsensusTestParams(6, BALANCED, BALANCED_WEIGHT_DESC)), - Arguments.of(new ConsensusTestParams(6, INCREMENTING, INCREMENTAL_NODE_WEIGHT_DESC)), - Arguments.of(new ConsensusTestParams(6, ONE_THIRD_ZERO_WEIGHT, ONE_THIRD_NODES_ZERO_WEIGHT_DESC))); + Arguments.of(new ConsensusTestParams(DEFAULT_PLATFORM_CONTEXT, 6, BALANCED, BALANCED_WEIGHT_DESC)), + Arguments.of(new ConsensusTestParams( + DEFAULT_PLATFORM_CONTEXT, 6, INCREMENTING, INCREMENTAL_NODE_WEIGHT_DESC)), + Arguments.of(new ConsensusTestParams( + DEFAULT_PLATFORM_CONTEXT, 6, ONE_THIRD_ZERO_WEIGHT, ONE_THIRD_NODES_ZERO_WEIGHT_DESC))); } static Stream forkingTests() { return Stream.of( - Arguments.of(new ConsensusTestParams(2, BALANCED, BALANCED_WEIGHT_DESC)), - Arguments.of(new ConsensusTestParams(4, INCREMENTING, INCREMENTAL_NODE_WEIGHT_DESC)), - Arguments.of(new ConsensusTestParams(9, RANDOM_REAL_WEIGHT, RANDOM_WEIGHT_DESC))); + Arguments.of(new ConsensusTestParams(DEFAULT_PLATFORM_CONTEXT, 2, BALANCED, BALANCED_WEIGHT_DESC)), + Arguments.of(new ConsensusTestParams( + DEFAULT_PLATFORM_CONTEXT, 4, INCREMENTING, INCREMENTAL_NODE_WEIGHT_DESC)), + Arguments.of( + new ConsensusTestParams(DEFAULT_PLATFORM_CONTEXT, 9, RANDOM_REAL_WEIGHT, RANDOM_WEIGHT_DESC))); } static Stream partitionTests() { @@ -85,14 +102,18 @@ static Stream partitionTests() { // of the test, not the consensus algorithm. // Arguments.of(new ConsensusTestParams(5, INCREMENTING, // INCREMENTAL_NODE_WEIGHT_DESC)), - Arguments.of(new ConsensusTestParams(9, INCREMENTING, INCREMENTAL_NODE_WEIGHT_DESC))); + Arguments.of(new ConsensusTestParams( + DEFAULT_PLATFORM_CONTEXT, 9, INCREMENTING, INCREMENTAL_NODE_WEIGHT_DESC))); } static Stream subQuorumPartitionTests() { return Stream.of( - Arguments.of(new ConsensusTestParams(7, BALANCED_REAL_WEIGHT, BALANCED_REAL_WEIGHT_DESC)), - Arguments.of(new ConsensusTestParams(9, INCREMENTING, INCREMENTAL_NODE_WEIGHT_DESC)), Arguments.of(new ConsensusTestParams( + DEFAULT_PLATFORM_CONTEXT, 7, BALANCED_REAL_WEIGHT, BALANCED_REAL_WEIGHT_DESC)), + Arguments.of(new ConsensusTestParams( + DEFAULT_PLATFORM_CONTEXT, 9, INCREMENTING, INCREMENTAL_NODE_WEIGHT_DESC)), + Arguments.of(new ConsensusTestParams( + DEFAULT_PLATFORM_CONTEXT, 9, ONE_THIRD_ZERO_WEIGHT, ONE_THIRD_NODES_ZERO_WEIGHT_DESC, @@ -104,22 +125,27 @@ static Stream subQuorumPartitionTests() { static Stream cliqueTests() { return Stream.of( - Arguments.of(new ConsensusTestParams(4, BALANCED, BALANCED_WEIGHT_DESC)), - Arguments.of(new ConsensusTestParams(9, INCREMENTING, INCREMENTAL_NODE_WEIGHT_DESC)), - Arguments.of(new ConsensusTestParams(9, RANDOM_REAL_WEIGHT, RANDOM_WEIGHT_DESC))); + Arguments.of(new ConsensusTestParams(DEFAULT_PLATFORM_CONTEXT, 4, BALANCED, BALANCED_WEIGHT_DESC)), + Arguments.of(new ConsensusTestParams( + DEFAULT_PLATFORM_CONTEXT, 9, INCREMENTING, INCREMENTAL_NODE_WEIGHT_DESC)), + Arguments.of( + new ConsensusTestParams(DEFAULT_PLATFORM_CONTEXT, 9, RANDOM_REAL_WEIGHT, RANDOM_WEIGHT_DESC))); } static Stream variableRateTests() { return Stream.of( - Arguments.of(new ConsensusTestParams(2, BALANCED, BALANCED_WEIGHT_DESC)), - Arguments.of(new ConsensusTestParams(4, INCREMENTING, INCREMENTAL_NODE_WEIGHT_DESC)), - Arguments.of(new ConsensusTestParams(9, RANDOM_REAL_WEIGHT, RANDOM_WEIGHT_DESC))); + Arguments.of(new ConsensusTestParams(DEFAULT_PLATFORM_CONTEXT, 2, BALANCED, BALANCED_WEIGHT_DESC)), + Arguments.of(new ConsensusTestParams( + DEFAULT_PLATFORM_CONTEXT, 4, INCREMENTING, INCREMENTAL_NODE_WEIGHT_DESC)), + Arguments.of( + new ConsensusTestParams(DEFAULT_PLATFORM_CONTEXT, 9, RANDOM_REAL_WEIGHT, RANDOM_WEIGHT_DESC))); } static Stream nodeUsesStaleOtherParents() { return Stream.of( - Arguments.of(new ConsensusTestParams(2, BALANCED, BALANCED_WEIGHT_DESC)), + Arguments.of(new ConsensusTestParams(DEFAULT_PLATFORM_CONTEXT, 2, BALANCED, BALANCED_WEIGHT_DESC)), Arguments.of(new ConsensusTestParams( + DEFAULT_PLATFORM_CONTEXT, 4, INCREMENTING, INCREMENTAL_NODE_WEIGHT_DESC, @@ -127,46 +153,58 @@ static Stream nodeUsesStaleOtherParents() { // than what was previously // set 458078453642476240L)), - Arguments.of(new ConsensusTestParams(4, INCREMENTING, INCREMENTAL_NODE_WEIGHT_DESC)), - Arguments.of(new ConsensusTestParams(9, RANDOM_REAL_WEIGHT, RANDOM_WEIGHT_DESC))); + Arguments.of(new ConsensusTestParams( + DEFAULT_PLATFORM_CONTEXT, 4, INCREMENTING, INCREMENTAL_NODE_WEIGHT_DESC)), + Arguments.of( + new ConsensusTestParams(DEFAULT_PLATFORM_CONTEXT, 9, RANDOM_REAL_WEIGHT, RANDOM_WEIGHT_DESC))); } static Stream nodeProvidesStaleOtherParents() { return Stream.of( - Arguments.of(new ConsensusTestParams(4, INCREMENTING, INCREMENTAL_NODE_WEIGHT_DESC)), - Arguments.of(new ConsensusTestParams(9, RANDOM_REAL_WEIGHT, RANDOM_WEIGHT_DESC))); + Arguments.of(new ConsensusTestParams( + DEFAULT_PLATFORM_CONTEXT, 4, INCREMENTING, INCREMENTAL_NODE_WEIGHT_DESC)), + Arguments.of( + new ConsensusTestParams(DEFAULT_PLATFORM_CONTEXT, 9, RANDOM_REAL_WEIGHT, RANDOM_WEIGHT_DESC))); } static Stream quorumOfNodesGoDownTests() { return Stream.of( - Arguments.of(new ConsensusTestParams(2, BALANCED, BALANCED_WEIGHT_DESC)), - Arguments.of(new ConsensusTestParams(4, INCREMENTING, INCREMENTAL_NODE_WEIGHT_DESC)), - Arguments.of(new ConsensusTestParams(9, RANDOM_REAL_WEIGHT, RANDOM_WEIGHT_DESC))); + Arguments.of(new ConsensusTestParams(DEFAULT_PLATFORM_CONTEXT, 2, BALANCED, BALANCED_WEIGHT_DESC)), + Arguments.of(new ConsensusTestParams( + DEFAULT_PLATFORM_CONTEXT, 4, INCREMENTING, INCREMENTAL_NODE_WEIGHT_DESC)), + Arguments.of( + new ConsensusTestParams(DEFAULT_PLATFORM_CONTEXT, 9, RANDOM_REAL_WEIGHT, RANDOM_WEIGHT_DESC))); } static Stream subQuorumOfNodesGoDownTests() { return Stream.of( - Arguments.of(new ConsensusTestParams(2, BALANCED, BALANCED_WEIGHT_DESC)), - Arguments.of(new ConsensusTestParams(4, INCREMENTING, INCREMENTAL_NODE_WEIGHT_DESC)), - Arguments.of(new ConsensusTestParams(9, RANDOM_REAL_WEIGHT, RANDOM_WEIGHT_DESC))); + Arguments.of(new ConsensusTestParams(DEFAULT_PLATFORM_CONTEXT, 2, BALANCED, BALANCED_WEIGHT_DESC)), + Arguments.of(new ConsensusTestParams( + DEFAULT_PLATFORM_CONTEXT, 4, INCREMENTING, INCREMENTAL_NODE_WEIGHT_DESC)), + Arguments.of( + new ConsensusTestParams(DEFAULT_PLATFORM_CONTEXT, 9, RANDOM_REAL_WEIGHT, RANDOM_WEIGHT_DESC))); } static Stream ancientEventTests() { - return Stream.of(Arguments.of(new ConsensusTestParams(4, BALANCED, BALANCED_WEIGHT_DESC))); + return Stream.of( + Arguments.of(new ConsensusTestParams(DEFAULT_PLATFORM_CONTEXT, 4, BALANCED, BALANCED_WEIGHT_DESC))); } public static Stream restartWithEventsParams() { return Stream.of( - Arguments.of(new ConsensusTestParams(5, INCREMENTING, INCREMENTAL_NODE_WEIGHT_DESC)), - Arguments.of(new ConsensusTestParams(10, RANDOM, RANDOM_WEIGHT_DESC)), - Arguments.of(new ConsensusTestParams(20, RANDOM, RANDOM_WEIGHT_DESC))); + Arguments.of(new ConsensusTestParams( + DEFAULT_PLATFORM_CONTEXT, 5, INCREMENTING, INCREMENTAL_NODE_WEIGHT_DESC)), + Arguments.of(new ConsensusTestParams(DEFAULT_PLATFORM_CONTEXT, 10, RANDOM, RANDOM_WEIGHT_DESC)), + Arguments.of(new ConsensusTestParams(DEFAULT_PLATFORM_CONTEXT, 20, RANDOM, RANDOM_WEIGHT_DESC))); } public static Stream migrationTestParams() { - return Stream.of(Arguments.of(new ConsensusTestParams(27, RANDOM, RANDOM_WEIGHT_DESC))); + return Stream.of( + Arguments.of(new ConsensusTestParams(DEFAULT_PLATFORM_CONTEXT, 27, RANDOM, RANDOM_WEIGHT_DESC))); } public static Stream nodeRemoveTestParams() { - return Stream.of(Arguments.of(new ConsensusTestParams(4, RANDOM, RANDOM_WEIGHT_DESC))); + return Stream.of( + Arguments.of(new ConsensusTestParams(DEFAULT_PLATFORM_CONTEXT, 4, RANDOM, RANDOM_WEIGHT_DESC))); } } diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/ConsensusTestDefinitions.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/ConsensusTestDefinitions.java index b258a47b28b0..b247cbf30252 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/ConsensusTestDefinitions.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/ConsensusTestDefinitions.java @@ -529,7 +529,7 @@ public static void reconnect(@NonNull final TestInput input) { orchestrator.generateEvents(0.5); orchestrator.validate( Validations.standard().ratios(EventRatioValidation.blank().setMinimumConsensusRatio(0.5))); - orchestrator.addReconnectNode(); + orchestrator.addReconnectNode(input.platformContext()); orchestrator.clearOutput(); orchestrator.generateEvents(0.5); diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/ConsensusTestParams.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/ConsensusTestParams.java index 5788575cfc95..e2c0b90e53ed 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/ConsensusTestParams.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/ConsensusTestParams.java @@ -16,9 +16,16 @@ package com.swirlds.platform.test.consensus; +import com.swirlds.common.context.PlatformContext; import com.swirlds.common.test.fixtures.WeightGenerator; +import edu.umd.cs.findbugs.annotations.NonNull; -public record ConsensusTestParams(int numNodes, WeightGenerator weightGenerator, String weightDesc, long... seeds) { +public record ConsensusTestParams( + @NonNull PlatformContext platformContext, + int numNodes, + @NonNull WeightGenerator weightGenerator, + @NonNull String weightDesc, + long... seeds) { @Override public String toString() { return numNodes + " nodes, " + weightDesc; diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/ConsensusTestRunner.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/ConsensusTestRunner.java index 6a777c280700..85de64d16759 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/ConsensusTestRunner.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/ConsensusTestRunner.java @@ -51,13 +51,15 @@ public void run() { for (final long seed : params.seeds()) { System.out.println("Running seed: " + seed); - test.accept(new TestInput(params.numNodes(), params.weightGenerator(), seed, eventsToGenerate)); + test.accept(new TestInput( + params.platformContext(), params.numNodes(), params.weightGenerator(), seed, eventsToGenerate)); } for (int i = 0; i < iterations; i++) { final long seed = new Random().nextLong(); System.out.println("Running seed: " + seed); - test.accept(new TestInput(params.numNodes(), params.weightGenerator(), seed, eventsToGenerate)); + test.accept(new TestInput( + params.platformContext(), params.numNodes(), params.weightGenerator(), seed, eventsToGenerate)); } } catch (final Throwable e) { throw new RuntimeException(e); diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/ConsensusTests.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/ConsensusTests.java index 128be26a7bb6..720a34518a5d 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/ConsensusTests.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/ConsensusTests.java @@ -17,6 +17,7 @@ package com.swirlds.platform.test.consensus; import static com.swirlds.common.test.fixtures.WeightGenerators.RANDOM; +import static com.swirlds.platform.test.consensus.ConsensusTestArgs.DEFAULT_PLATFORM_CONTEXT; import static com.swirlds.platform.test.consensus.ConsensusTestArgs.RANDOM_WEIGHT_DESC; import com.swirlds.common.test.fixtures.junit.tags.TestComponentTags; @@ -253,7 +254,7 @@ void nodeRemoveTest(final ConsensusTestParams params) { void syntheticSnapshotTest() { ConsensusTestRunner.create() .setTest(ConsensusTestDefinitions::syntheticSnapshot) - .setParams(new ConsensusTestParams(4, RANDOM, RANDOM_WEIGHT_DESC)) + .setParams(new ConsensusTestParams(DEFAULT_PLATFORM_CONTEXT, 4, RANDOM, RANDOM_WEIGHT_DESC)) .setIterations(NUM_ITER) .run(); } diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/IntakeAndConsensusTests.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/IntakeAndConsensusTests.java index 3054bdecbcda..70b8f5a21efb 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/IntakeAndConsensusTests.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/IntakeAndConsensusTests.java @@ -18,10 +18,11 @@ import static com.swirlds.common.test.fixtures.junit.tags.TestQualifierTags.TIME_CONSUMING; +import com.swirlds.common.context.PlatformContext; import com.swirlds.common.platform.NodeId; +import com.swirlds.common.test.fixtures.platform.TestPlatformContextBuilder; import com.swirlds.config.api.Configuration; import com.swirlds.config.extensions.test.fixtures.TestConfigBuilder; -import com.swirlds.platform.consensus.ConsensusConfig; import com.swirlds.platform.consensus.ConsensusConfig_; import com.swirlds.platform.system.address.AddressBook; import com.swirlds.platform.system.events.EventConstants; @@ -71,13 +72,15 @@ void nonAncientEventWithMissingParents() { .withValue(ConsensusConfig_.ROUNDS_EXPIRED, 25) .getOrCreateConfig(); - final ConsensusConfig consensusConfig = configuration.getConfigData(ConsensusConfig.class); + final PlatformContext platformContext = TestPlatformContextBuilder.create() + .withConfiguration(configuration) + .build(); // the generated events are first fed into consensus so that round created is calculated before we start // using them - final GeneratorWithConsensus generator = new GeneratorWithConsensus(seed, numNodes, consensusConfig); - final TestIntake node1 = new TestIntake(generator.getAddressBook(), consensusConfig); - final TestIntake node2 = new TestIntake(generator.getAddressBook(), consensusConfig); + final GeneratorWithConsensus generator = new GeneratorWithConsensus(platformContext, seed, numNodes); + final TestIntake node1 = new TestIntake(platformContext, generator.getAddressBook()); + final TestIntake node2 = new TestIntake(platformContext, generator.getAddressBook()); // first we generate events regularly, until we have some ancient rounds final int firstBatchSize = 5000; @@ -184,11 +187,13 @@ private static class GeneratorWithConsensus implements GraphGenerator eventSources = Stream.generate(StandardEventSource::new).limit(numNodes).toList(); generator = new StandardGraphGenerator(seed, (List>) (List) eventSources); - intake = new TestIntake(generator.getAddressBook(), consensusConfig); + intake = new TestIntake(platformContext, generator.getAddressBook()); } @Override diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/gui/HashgraphGuiTest.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/gui/HashgraphGuiTest.java index 7927d9f6fcb4..cf78e9865da2 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/gui/HashgraphGuiTest.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/gui/HashgraphGuiTest.java @@ -16,6 +16,8 @@ package com.swirlds.platform.test.gui; +import com.swirlds.common.context.PlatformContext; +import com.swirlds.common.test.fixtures.platform.TestPlatformContextBuilder; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -27,7 +29,9 @@ void runGuiWithControls() { final int numNodes = 4; final int initialEvents = 0; - final TestGuiSource guiSource = new TestGuiSource(seed, numNodes); + final PlatformContext platformContext = + TestPlatformContextBuilder.create().build(); + final TestGuiSource guiSource = new TestGuiSource(platformContext, seed, numNodes); guiSource.generateEvents(initialEvents); guiSource.runGui(); } From cf856f8de33e266924b8b5edb5359bd6cf49309c Mon Sep 17 00:00:00 2001 From: Michael Tinker Date: Fri, 8 Mar 2024 10:11:14 -0600 Subject: [PATCH 041/115] fix: ensure ECDSA `ExpandedSigPair` always has EVM alias (#11955) Signed-off-by: Michael Tinker Signed-off-by: Cody Littley Co-authored-by: Cody Littley --- .../app/signature/ExpandedSignaturePair.java | 31 +++++++++++++++++++ .../signature/impl/SignatureExpanderImpl.java | 22 ++++++------- .../app/workflows/handle/HandleWorkflow.java | 7 +++++ .../prehandle/PreHandleWorkflowImpl.java | 4 +-- .../impl/SignatureExpanderImplTest.java | 19 ++++++++++-- 5 files changed, 65 insertions(+), 18 deletions(-) diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/signature/ExpandedSignaturePair.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/signature/ExpandedSignaturePair.java index 760a302cacf4..461c1b30cf73 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/signature/ExpandedSignaturePair.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/signature/ExpandedSignaturePair.java @@ -16,6 +16,9 @@ package com.hedera.node.app.signature; +import static com.hedera.node.app.service.mono.sigs.utils.MiscCryptoUtils.extractEvmAddressFromDecompressedECDSAKey; +import static com.hedera.node.app.signature.impl.SignatureExpanderImpl.decompressKey; + import com.hedera.hapi.node.base.Key; import com.hedera.hapi.node.base.SignaturePair; import com.hedera.pbj.runtime.io.buffer.Bytes; @@ -36,10 +39,38 @@ public record ExpandedSignaturePair( /** * Gets the {@link Bytes} representing the signature signed by the private key matching the fully expanded public * key. + * * @return The signature bytes. */ @NonNull public Bytes signature() { return sigPair.signature().as(); } + + /** + * Given a (putative) compressed ECDSA public key and a {@link SignaturePair}, + * returns the implied {@link ExpandedSignaturePair} if the key can be decompressed. + * Returns null if the key is not a valid compressed ECDSA public key. + * + * @param compressedEcdsaPubKey the compressed ECDSA public key + * @param sigPair the signature pair + * @return the expanded signature pair, or null if the key is not a valid compressed ECDSA public key + */ + public static @Nullable ExpandedSignaturePair maybeFrom( + @NonNull final Bytes compressedEcdsaPubKey, @NonNull final SignaturePair sigPair) { + final var ecdsaPubKey = decompressKey(compressedEcdsaPubKey); + return ecdsaPubKey != null ? from(ecdsaPubKey, compressedEcdsaPubKey, sigPair) : null; + } + + private static @NonNull ExpandedSignaturePair from( + @NonNull final Bytes ecdsaPubKey, + @NonNull final Bytes compressedEcdsaPubKey, + @NonNull final SignaturePair sigPair) { + final var evmAddress = extractEvmAddressFromDecompressedECDSAKey(ecdsaPubKey.toByteArray()); + return new ExpandedSignaturePair( + Key.newBuilder().ecdsaSecp256k1(compressedEcdsaPubKey).build(), + ecdsaPubKey, + Bytes.wrap(evmAddress), + sigPair); + } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/signature/impl/SignatureExpanderImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/signature/impl/SignatureExpanderImpl.java index 6ddb9e414787..3fade4e01023 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/signature/impl/SignatureExpanderImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/signature/impl/SignatureExpanderImpl.java @@ -78,14 +78,9 @@ public void expand( // hollow accounts it is needed, but otherwise it can typically not be the full prefix. In that case, // we won't waste much work. And the payer pays for the whole thing anyway, so we're compensated for the // CPU cycles in any event. Doing it in the background threads seems to be a better tradeoff. - final var decompressed = decompressKey(pair.pubKeyPrefix()); - if (decompressed != null) { - final var decompressedByteArray = new byte[(int) decompressed.length()]; - decompressed.getBytes(0, decompressedByteArray); - final var hashedPrefixByteArray = - MiscCryptoUtils.extractEvmAddressFromDecompressedECDSAKey(decompressedByteArray); - final var emvAlias = Bytes.wrap(hashedPrefixByteArray); - expanded.add(new ExpandedSignaturePair(asKey(pair), decompressed, emvAlias, pair)); + final var maybeExpandedSigPair = ExpandedSignaturePair.maybeFrom(pair.pubKeyPrefix(), pair); + if (maybeExpandedSigPair != null) { + expanded.add(maybeExpandedSigPair); } } } @@ -130,9 +125,10 @@ public void expand( case ECDSA_SECP256K1 -> { final var match = findMatch(key, originals); if (match != null) { - final var decompressed = decompressKey(key.ecdsaSecp256k1OrThrow()); - if (decompressed != null) { - expanded.add(new ExpandedSignaturePair(key, decompressed, null, match)); + final var maybeExpandedSigPair = + ExpandedSignaturePair.maybeFrom(key.ecdsaSecp256k1OrThrow(), match); + if (maybeExpandedSigPair != null) { + expanded.add(maybeExpandedSigPair); } } } @@ -160,7 +156,7 @@ public void expand( * @return The decompressed key bytes, or null if the key was not a valid compressed ECDSA_SECP256K1 key */ @Nullable - private Bytes decompressKey(@Nullable final Bytes keyBytes) { + public static Bytes decompressKey(@Nullable final Bytes keyBytes) { if (keyBytes == null) return null; // If the compressed key begins with a prefix byte other than 0x02 or 0x03, decompressing will throw. // We don't want it to throw, because that is a waste of CPU cycles. So we'll check the first byte @@ -236,7 +232,7 @@ private SignaturePair findMatch(@NonNull final Key key, @NonNull final List Key.newBuilder().ed25519(pair.pubKeyPrefix()).build(); case ECDSA_SECP256K1 -> Key.newBuilder() 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 6573a749e564..e4f0f515dcd0 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 @@ -700,6 +700,13 @@ private void finalizeHollowAccounts( throw new HandleException(MAX_CHILD_RECORDS_EXCEEDED); } else { for (final var hollowAccount : accounts) { + if (hollowAccount.accountIdOrElse(AccountID.DEFAULT).equals(AccountID.DEFAULT)) { + // The CryptoCreateHandler uses a "hack" to validate that a CryptoCreate with + // an EVM address has signed with that alias's ECDSA key; that is, it adds a + // dummy "hollow account" with the EVM address as an alias. But we don't want + // to try to finalize such a dummy account, so skip it here. + continue; + } // get the verified key for this hollow account final var verification = ethTxVerification != null && hollowAccount.alias().equals(ethTxVerification.evmAlias()) diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleWorkflowImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleWorkflowImpl.java index 1737791c083b..e65f99740a78 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleWorkflowImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleWorkflowImpl.java @@ -53,7 +53,7 @@ import com.swirlds.platform.system.transaction.Transaction; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.stream.Stream; @@ -356,7 +356,7 @@ private Map verifySignatures( } // If not, bootstrap the expanded signature pairs by grabbing all prefixes that are "full" keys already final var originals = txInfo.signatureMap().sigPairOrElse(emptyList()); - final var expanded = new HashSet(); + final var expanded = new LinkedHashSet(); signatureExpander.expand(originals, expanded); // Expand the payer account key signatures if it is not a hollow account if (payerIsHollow == PayerIsHollow.NO) { diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/signature/impl/SignatureExpanderImplTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/signature/impl/SignatureExpanderImplTest.java index 040ec387eea4..3734db1626dd 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/signature/impl/SignatureExpanderImplTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/signature/impl/SignatureExpanderImplTest.java @@ -16,6 +16,7 @@ package com.hedera.node.app.signature.impl; +import static com.hedera.node.app.service.mono.sigs.utils.MiscCryptoUtils.extractEvmAddressFromDecompressedECDSAKey; import static java.util.Collections.emptyList; import static java.util.Collections.emptySet; import static org.assertj.core.api.Assertions.assertThat; @@ -410,21 +411,33 @@ void expansion() { FAKE_ECDSA_WITH_ALIAS_KEY_INFOS[0] .uncompressedPublicKey() .ecdsaSecp256k1OrThrow(), - null, + Bytes.wrap( + extractEvmAddressFromDecompressedECDSAKey(FAKE_ECDSA_WITH_ALIAS_KEY_INFOS[0] + .uncompressedPublicKey() + .ecdsaSecp256k1OrThrow() + .toByteArray())), sigList.get(5)), new ExpandedSignaturePair( FAKE_ECDSA_WITH_ALIAS_KEY_INFOS[1].publicKey(), FAKE_ECDSA_WITH_ALIAS_KEY_INFOS[1] .uncompressedPublicKey() .ecdsaSecp256k1OrThrow(), - null, + Bytes.wrap( + extractEvmAddressFromDecompressedECDSAKey(FAKE_ECDSA_WITH_ALIAS_KEY_INFOS[1] + .uncompressedPublicKey() + .ecdsaSecp256k1OrThrow() + .toByteArray())), sigList.get(6)), new ExpandedSignaturePair( FAKE_ECDSA_WITH_ALIAS_KEY_INFOS[2].publicKey(), FAKE_ECDSA_WITH_ALIAS_KEY_INFOS[2] .uncompressedPublicKey() .ecdsaSecp256k1OrThrow(), - null, + Bytes.wrap( + extractEvmAddressFromDecompressedECDSAKey(FAKE_ECDSA_WITH_ALIAS_KEY_INFOS[2] + .uncompressedPublicKey() + .ecdsaSecp256k1OrThrow() + .toByteArray())), sigList.get(7))); } } From c37a921ec4f3c91cf9e40fbad896ed1407a2b652 Mon Sep 17 00:00:00 2001 From: Michael Heinrichs Date: Fri, 8 Mar 2024 18:23:20 +0100 Subject: [PATCH 042/115] perf: Move remaining in-memory maps on disk (#11974) Signed-off-by: Michael Heinrichs --- .../schemas/InitialModServiceConsensusSchema.java | 5 ++++- .../schemas/InitialModServiceScheduleSchema.java | 15 ++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/schemas/InitialModServiceConsensusSchema.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/schemas/InitialModServiceConsensusSchema.java index 97132fb42942..584bca404ab4 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/schemas/InitialModServiceConsensusSchema.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/schemas/InitialModServiceConsensusSchema.java @@ -43,6 +43,9 @@ */ public class InitialModServiceConsensusSchema extends Schema { private static final Logger log = LogManager.getLogger(InitialModServiceConsensusSchema.class); + + private static final long MAX_TOPICS = 1_000_000_000L; + private MerkleMap fs; public InitialModServiceConsensusSchema(@NonNull final SemanticVersion version) { @@ -52,7 +55,7 @@ public InitialModServiceConsensusSchema(@NonNull final SemanticVersion version) @NonNull @Override public Set statesToCreate() { - return Set.of(StateDefinition.inMemory(TOPICS_KEY, TopicID.PROTOBUF, Topic.PROTOBUF)); + return Set.of(StateDefinition.onDisk(TOPICS_KEY, TopicID.PROTOBUF, Topic.PROTOBUF, MAX_TOPICS)); } public void setFromState(@Nullable final MerkleMap fs) { diff --git a/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/schemas/InitialModServiceScheduleSchema.java b/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/schemas/InitialModServiceScheduleSchema.java index 4c62d6f5baa8..2af8a3ae2867 100644 --- a/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/schemas/InitialModServiceScheduleSchema.java +++ b/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/schemas/InitialModServiceScheduleSchema.java @@ -58,6 +58,9 @@ */ public final class InitialModServiceScheduleSchema extends Schema { private static final Logger log = LogManager.getLogger(InitialModServiceScheduleSchema.class); + private static final long MAX_SCHEDULES_BY_ID_KEY = 50_000_000L; + private static final long MAX_SCHEDULES_BY_EXPIRY_SEC_KEY = 50_000_000L; + private static final long MAX_SCHEDULES_BY_EQUALITY = 50_000_000L; private MerkleScheduledTransactions fs; public InitialModServiceScheduleSchema(@NonNull final SemanticVersion version) { @@ -183,14 +186,20 @@ public void accept(String scheduleObjHash, Long scheduleId) { } private static StateDefinition schedulesByIdDef() { - return StateDefinition.inMemory(SCHEDULES_BY_ID_KEY, ScheduleID.PROTOBUF, Schedule.PROTOBUF); + return StateDefinition.onDisk( + SCHEDULES_BY_ID_KEY, ScheduleID.PROTOBUF, Schedule.PROTOBUF, MAX_SCHEDULES_BY_ID_KEY); } private static StateDefinition schedulesByExpirySec() { - return StateDefinition.inMemory(SCHEDULES_BY_EXPIRY_SEC_KEY, ProtoLong.PROTOBUF, ScheduleList.PROTOBUF); + return StateDefinition.onDisk( + SCHEDULES_BY_EXPIRY_SEC_KEY, + ProtoLong.PROTOBUF, + ScheduleList.PROTOBUF, + MAX_SCHEDULES_BY_EXPIRY_SEC_KEY); } private static StateDefinition schedulesByEquality() { - return StateDefinition.inMemory(SCHEDULES_BY_EQUALITY_KEY, ProtoString.PROTOBUF, ScheduleList.PROTOBUF); + return StateDefinition.onDisk( + SCHEDULES_BY_EQUALITY_KEY, ProtoString.PROTOBUF, ScheduleList.PROTOBUF, MAX_SCHEDULES_BY_EQUALITY); } } From 173f419619af3a289d2be114568e3665d0416827 Mon Sep 17 00:00:00 2001 From: anthony-swirldslabs <152534762+anthony-swirldslabs@users.noreply.github.com> Date: Fri, 8 Mar 2024 09:57:03 -0800 Subject: [PATCH 043/115] fix: pbj-220: upgrade PBJ dependency to 0.7.23 (#11958) Signed-off-by: Anthony Petrov --- hedera-dependency-versions/build.gradle.kts | 2 +- .../StreamFileProducerConcurrent.java | 5 ++- .../StreamFileProducerSingleThreaded.java | 4 +- .../handle/record/BlockRecordManagerTest.java | 2 +- .../producers/StreamFileProducerTest.java | 4 +- .../codec/FileServiceStateTranslatorTest.java | 43 +++++++++---------- .../hevm/HederaEvmTransactionResultTest.java | 6 +-- settings.gradle.kts | 2 +- 8 files changed, 33 insertions(+), 35 deletions(-) diff --git a/hedera-dependency-versions/build.gradle.kts b/hedera-dependency-versions/build.gradle.kts index fbda7d9a5ecd..49bc13ef8d4b 100644 --- a/hedera-dependency-versions/build.gradle.kts +++ b/hedera-dependency-versions/build.gradle.kts @@ -58,7 +58,7 @@ moduleInfo { version("com.google.jimfs", "1.2") version("com.google.protobuf", protobufVersion) version("com.google.protobuf.util", protobufVersion) - version("com.hedera.pbj.runtime", "0.7.22") + version("com.hedera.pbj.runtime", "0.7.23") version("com.squareup.javapoet", "1.13.0") version("com.sun.jna", "5.12.1") version("dagger", daggerVersion) diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/impl/producers/StreamFileProducerConcurrent.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/impl/producers/StreamFileProducerConcurrent.java index dabb9545da29..d94cf3e0a869 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/impl/producers/StreamFileProducerConcurrent.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/impl/producers/StreamFileProducerConcurrent.java @@ -116,8 +116,9 @@ public void initRunningHash(@NonNull final RunningHashes runningHashes) { throw new IllegalStateException("initRunningHash() can only be called once"); } - if (runningHashes.runningHash() == null) { - throw new IllegalArgumentException("The initial running hash cannot be null"); + if (runningHashes.runningHash() == null + || runningHashes.runningHash().equals(Bytes.EMPTY)) { + throw new IllegalArgumentException("The initial running hash cannot be null or empty"); } lastRecordHashingResult = completedFuture(runningHashes.runningHash()); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/impl/producers/StreamFileProducerSingleThreaded.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/impl/producers/StreamFileProducerSingleThreaded.java index e872352df1e6..a860a4245d27 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/impl/producers/StreamFileProducerSingleThreaded.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/impl/producers/StreamFileProducerSingleThreaded.java @@ -124,8 +124,8 @@ public void initRunningHash(@NonNull final RunningHashes runningHashes) { throw new IllegalStateException("initRunningHash() must only be called once"); } - if (runningHashes.runningHash() == null) { - throw new IllegalArgumentException("The initial running hash cannot be null"); + if (runningHashes.runningHash() == null || runningHashes.runningHash().equals(Bytes.EMPTY)) { + throw new IllegalArgumentException("The initial running hash cannot be null or empty"); } runningHash = runningHashes.runningHash(); diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/BlockRecordManagerTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/BlockRecordManagerTest.java index 159cd99b9d37..db6ba779602a 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/BlockRecordManagerTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/BlockRecordManagerTest.java @@ -301,7 +301,7 @@ void testBlockInfoMethods() throws Exception { } else { // check nulls as well assertThat(blockRecordManager.getNMinus3RunningHash()) - .isNull(); + .isEqualTo(Bytes.EMPTY); } } j += batchSize; diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/impl/producers/StreamFileProducerTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/impl/producers/StreamFileProducerTest.java index b21c91f5113b..944f7007deb5 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/impl/producers/StreamFileProducerTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/impl/producers/StreamFileProducerTest.java @@ -108,7 +108,7 @@ void nullOK() { final var runningHashes = new RunningHashes(randomBytes(32), null, null, null); subject.initRunningHash(runningHashes); assertThat(subject.getRunningHash()).isEqualTo(runningHashes.runningHash()); - assertThat(subject.getNMinus3RunningHash()).isNull(); + assertThat(subject.getNMinus3RunningHash()).isEqualTo(Bytes.EMPTY); } } @@ -247,7 +247,7 @@ void multipleRecords() throws Exception { // Submitting a batch of records only moves the running hash forward once, because it moves forward once // PER USER TRANSACTION, not per record. - assertThat(subject.getNMinus3RunningHash()).isNull(); + assertThat(subject.getNMinus3RunningHash()).isEqualTo(Bytes.EMPTY); final var finalRunningHash = subject.getRunningHash(); subject.close(); diff --git a/hedera-node/hedera-file-service-impl/src/test/java/com/hedera/node/app/service/file/impl/test/codec/FileServiceStateTranslatorTest.java b/hedera-node/hedera-file-service-impl/src/test/java/com/hedera/node/app/service/file/impl/test/codec/FileServiceStateTranslatorTest.java index 85eb0f126aa8..06be45d7957c 100644 --- a/hedera-node/hedera-file-service-impl/src/test/java/com/hedera/node/app/service/file/impl/test/codec/FileServiceStateTranslatorTest.java +++ b/hedera-node/hedera-file-service-impl/src/test/java/com/hedera/node/app/service/file/impl/test/codec/FileServiceStateTranslatorTest.java @@ -76,28 +76,27 @@ void createFileMetadataAndContentFromFileWithEmptyKeysAndMemo() throws InvalidKe final FileMetadataAndContent convertedFile = FileServiceStateTranslator.pbjToState(fileWithNoKeysAndMemo); assertArrayEquals( - convertedFile.data(), - getExpectedMonoFileMetaAndContentWithEmptyMemoAndKeys().data()); + getExpectedMonoFileMetaAndContentWithEmptyMemoAndKeys().data(), convertedFile.data()); assertEquals( - convertedFile.metadata().getExpiry(), getExpectedMonoFileMetaAndContentWithEmptyMemoAndKeys() .metadata() - .getExpiry()); + .getExpiry(), + convertedFile.metadata().getExpiry()); assertEquals( - convertedFile.metadata().getMemo(), getExpectedMonoFileMetaAndContentWithEmptyMemoAndKeys() .metadata() - .getMemo()); + .getMemo(), + convertedFile.metadata().getMemo()); assertEquals( - MiscUtils.describe(convertedFile.metadata().getWacl()), MiscUtils.describe(getExpectedMonoFileMetaAndContentWithEmptyMemoAndKeys() .metadata() - .getWacl())); + .getWacl()), + MiscUtils.describe(convertedFile.metadata().getWacl())); assertEquals( - convertedFile.metadata().isDeleted(), getExpectedMonoFileMetaAndContentWithEmptyMemoAndKeys() .metadata() - .isDeleted()); + .isDeleted(), + convertedFile.metadata().isDeleted()); } @Test @@ -105,23 +104,21 @@ void createFileMetadataAndContentFromFileWithEmptyConentForDeletedFile() throws final FileMetadataAndContent convertedFile = FileServiceStateTranslator.pbjToState(fileWithNoContent); - assertArrayEquals( - convertedFile.data(), - getExpectedMonoFileMetaAndContentEmptyContent().data()); + assertArrayEquals(getExpectedMonoFileMetaAndContentEmptyContent().data(), convertedFile.data()); assertEquals( - convertedFile.metadata().getExpiry(), - getExpectedMonoFileMetaAndContentEmptyContent().metadata().getExpiry()); + getExpectedMonoFileMetaAndContentEmptyContent().metadata().getExpiry(), + convertedFile.metadata().getExpiry()); assertEquals( - convertedFile.metadata().getMemo(), - getExpectedMonoFileMetaAndContentEmptyContent().metadata().getMemo()); + getExpectedMonoFileMetaAndContentEmptyContent().metadata().getMemo(), + convertedFile.metadata().getMemo()); assertEquals( - MiscUtils.describe(convertedFile.metadata().getWacl()), MiscUtils.describe(getExpectedMonoFileMetaAndContentEmptyContent() .metadata() - .getWacl())); + .getWacl()), + MiscUtils.describe(convertedFile.metadata().getWacl())); assertEquals( - convertedFile.metadata().isDeleted(), - getExpectedMonoFileMetaAndContentEmptyContent().metadata().isDeleted()); + getExpectedMonoFileMetaAndContentEmptyContent().metadata().isDeleted(), + convertedFile.metadata().isDeleted()); } @Test @@ -210,7 +207,7 @@ private FileMetadataAndContent getExpectedMonoFileMetaAndContent() throws Invali private FileMetadataAndContent getExpectedMonoFileMetaAndContentWithEmptyMemoAndKeys() { com.hedera.node.app.service.mono.files.HFileMeta hFileMeta = - new HFileMeta(file.deleted(), null, file.expirationSecond(), null); + new HFileMeta(file.deleted(), null, file.expirationSecond(), ""); return new FileMetadataAndContent(file.contents().toByteArray(), hFileMeta); } @@ -219,6 +216,6 @@ private FileMetadataAndContent getExpectedMonoFileMetaAndContentEmptyContent() t Key.newBuilder().keyList(file.keys()).build(), 1); com.hedera.node.app.service.mono.files.HFileMeta hFileMeta = new HFileMeta(file.deleted(), keys, file.expirationSecond(), file.memo()); - return new FileMetadataAndContent(null, hFileMeta); + return new FileMetadataAndContent(new byte[] {}, hFileMeta); } } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/hevm/HederaEvmTransactionResultTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/hevm/HederaEvmTransactionResultTest.java index 82ccfa6bbe91..a5bfdc4826a5 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/hevm/HederaEvmTransactionResultTest.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/hevm/HederaEvmTransactionResultTest.java @@ -178,7 +178,7 @@ void givenAccessTrackerIncludesFullContractStorageChangesAndNonNullNoncesOnSucce assertEquals(GAS_LIMIT / 2, protoResult.gasUsed()); assertEquals(bloomForAll(BESU_LOGS), protoResult.bloom()); assertEquals(OUTPUT_DATA, protoResult.contractCallResult()); - assertNull(protoResult.errorMessage()); + assertEquals("", protoResult.errorMessage()); assertNull(protoResult.senderId()); assertEquals(CALLED_CONTRACT_ID, protoResult.contractID()); assertEquals(pbjLogsFrom(BESU_LOGS), protoResult.logInfo()); @@ -217,7 +217,7 @@ void givenEthTxDataIncludesSpecialFields() { assertEquals(GAS_LIMIT / 2, protoResult.gasUsed()); assertEquals(bloomForAll(BESU_LOGS), protoResult.bloom()); assertEquals(OUTPUT_DATA, protoResult.contractCallResult()); - assertNull(protoResult.errorMessage()); + assertEquals("", protoResult.errorMessage()); assertEquals(CALLED_CONTRACT_ID, protoResult.contractID()); assertEquals(pbjLogsFrom(BESU_LOGS), protoResult.logInfo()); assertEquals(createdIds, protoResult.createdContractIDs()); @@ -267,7 +267,7 @@ void QueryResultOnSuccess() { assertEquals(GAS_LIMIT / 2, queryResult.gasUsed()); assertEquals(bloomForAll(BESU_LOGS), queryResult.bloom()); assertEquals(OUTPUT_DATA, queryResult.contractCallResult()); - assertNull(queryResult.errorMessage()); + assertEquals("", queryResult.errorMessage()); assertNull(queryResult.senderId()); assertEquals(CALLED_CONTRACT_ID, queryResult.contractID()); assertEquals(pbjLogsFrom(BESU_LOGS), queryResult.logInfo()); diff --git a/settings.gradle.kts b/settings.gradle.kts index 9676b6923fa8..ad384728d9ef 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -150,6 +150,6 @@ dependencyResolutionManagement { version("grpc-proto", "1.45.1") version("hapi-proto", hapiProtoVersion) - plugin("pbj", "com.hedera.pbj.pbj-compiler").version("0.7.22") + plugin("pbj", "com.hedera.pbj.pbj-compiler").version("0.7.23") } } From b42024fa92cba5a72712a8f1d069b1009059567d Mon Sep 17 00:00:00 2001 From: Iris Simon <122310714+iwsimon@users.noreply.github.com> Date: Fri, 8 Mar 2024 15:55:40 -0500 Subject: [PATCH 044/115] chore: cherry pick pr 11944, added unit tests to validateTopLevelAllowances() (#11976) Signed-off-by: Iris Simon --- .../handlers/CryptoTransferHandlerTest.java | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoTransferHandlerTest.java b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoTransferHandlerTest.java index f7f9ef8b2ed8..0a84447f0fb4 100644 --- a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoTransferHandlerTest.java +++ b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoTransferHandlerTest.java @@ -18,8 +18,10 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_KYC_NOT_GRANTED_FOR_TOKEN; import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_REPEATED_IN_ACCOUNT_AMOUNTS; +import static com.hedera.hapi.node.base.ResponseCodeEnum.AMOUNT_EXCEEDS_ALLOWANCE; import static com.hedera.hapi.node.base.ResponseCodeEnum.BATCH_SIZE_LIMIT_EXCEEDED; import static com.hedera.hapi.node.base.ResponseCodeEnum.NOT_SUPPORTED; +import static com.hedera.hapi.node.base.ResponseCodeEnum.SPENDER_DOES_NOT_HAVE_ALLOWANCE; import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKEN_TRANSFER_LIST_SIZE_LIMIT_EXCEEDED; import static com.hedera.hapi.node.base.ResponseCodeEnum.TRANSFER_LIST_SIZE_LIMIT_EXCEEDED; import static com.hedera.node.app.service.mono.context.properties.PropertyNames.HEDERA_ALLOWANCES_IS_ENABLED; @@ -39,12 +41,17 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; +import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.Key; import com.hedera.hapi.node.base.TokenID; import com.hedera.hapi.node.base.TokenTransferList; import com.hedera.hapi.node.base.TransferList; +import com.hedera.hapi.node.state.token.Account; +import com.hedera.hapi.node.state.token.AccountFungibleTokenAllowance; import com.hedera.hapi.node.token.CryptoTransferTransactionBody; import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.node.app.service.token.ReadableAccountStore; +import com.hedera.node.app.service.token.impl.ReadableAccountStoreImpl; import com.hedera.node.app.service.token.impl.WritableAccountStore; import com.hedera.node.app.service.token.impl.handlers.CryptoTransferHandler; import com.hedera.node.app.service.token.records.CryptoCreateRecordBuilder; @@ -411,6 +418,76 @@ void failsOnRepeatedAliasAndCorrespondingNumberInTokenTransferList() { .has(responseCode(ACCOUNT_REPEATED_IN_ACCOUNT_AMOUNTS)); } + @Test + void validateTopLevelAllowancesWithPayer() { + givenTxnWithAllowances(); + + final var allowance = AccountFungibleTokenAllowance.newBuilder() + .tokenId(fungibleTokenId) + .spenderId(ownerId) + .build(); + ownerAccount = + ownerAccount.copyBuilder().tokenAllowances(List.of(allowance)).build(); + readableAccounts = + emptyReadableAccountStateBuilder().value(ownerId, ownerAccount).build(); + given(readableStates.get(ACCOUNTS)).willReturn(readableAccounts); + readableAccountStore = new ReadableAccountStoreImpl(readableStates); + + given(handleContext.readableStore(ReadableAccountStore.class)).willReturn(readableAccountStore); + + Assertions.assertThatThrownBy(() -> subject.handle(handleContext)) + .isInstanceOf(HandleException.class) + .has(responseCode(SPENDER_DOES_NOT_HAVE_ALLOWANCE)); + } + + @Test + void validateTopLevelAllowancesWithAmount0() { + givenTxnWithAllowances(); + + final var allowance = AccountFungibleTokenAllowance.newBuilder() + .tokenId(fungibleTokenId) + .spenderId(spenderId) + .amount(0) + .build(); + ownerAccount = + ownerAccount.copyBuilder().tokenAllowances(List.of(allowance)).build(); + readableAccounts = + emptyReadableAccountStateBuilder().value(ownerId, ownerAccount).build(); + given(readableStates.get(ACCOUNTS)).willReturn(readableAccounts); + readableAccountStore = new ReadableAccountStoreImpl(readableStates); + + given(handleContext.payer()).willReturn(spenderId); + given(handleContext.readableStore(ReadableAccountStore.class)).willReturn(readableAccountStore); + + Assertions.assertThatThrownBy(() -> subject.handle(handleContext)) + .isInstanceOf(HandleException.class) + .has(responseCode(SPENDER_DOES_NOT_HAVE_ALLOWANCE)); + } + + @Test + void validateTopLevelAllowancesWithAmountLess() { + givenTxnWithAllowances(); + + final var allowance = AccountFungibleTokenAllowance.newBuilder() + .tokenId(fungibleTokenId) + .spenderId(spenderId) + .amount(10) + .build(); + ownerAccount = + ownerAccount.copyBuilder().tokenAllowances(List.of(allowance)).build(); + readableAccounts = + emptyReadableAccountStateBuilder().value(ownerId, ownerAccount).build(); + given(readableStates.get(ACCOUNTS)).willReturn(readableAccounts); + readableAccountStore = new ReadableAccountStoreImpl(readableStates); + + given(handleContext.payer()).willReturn(spenderId); + given(handleContext.readableStore(ReadableAccountStore.class)).willReturn(readableAccountStore); + + Assertions.assertThatThrownBy(() -> subject.handle(handleContext)) + .isInstanceOf(HandleException.class) + .has(responseCode(AMOUNT_EXCEEDS_ALLOWANCE)); + } + private HandleContext mockContext(final TransactionBody txn) { final var context = mock(HandleContext.class); given(context.configuration()).willReturn(config); From cfc76ac5beff5edd70705aaeda1857a5985ef632 Mon Sep 17 00:00:00 2001 From: Iris Simon <122310714+iwsimon@users.noreply.github.com> Date: Fri, 8 Mar 2024 15:57:00 -0500 Subject: [PATCH 045/115] fix: cherry-pick fixed diff test issue 11952 (#11965) (#11990) Signed-off-by: Iris Simon --- .../node/app/service/token/impl/util/TokenHandlerHelper.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/util/TokenHandlerHelper.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/util/TokenHandlerHelper.java index 79d9aa8b06d0..49c4537281cf 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/util/TokenHandlerHelper.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/util/TokenHandlerHelper.java @@ -34,7 +34,6 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_DELETED; import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_FROZEN_FOR_TOKEN; -import static com.hedera.hapi.node.base.ResponseCodeEnum.CONTRACT_DELETED; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_AUTORENEW_ACCOUNT; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOKEN_ID; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TREASURY_ACCOUNT_FOR_TOKEN; @@ -152,7 +151,7 @@ public static Account getIfUsable( validateTrue(acct != null, errorIfNotUsable); final var isContract = acct.smartContract(); - validateFalse(acct.deleted(), isContract ? CONTRACT_DELETED : errorOnAccountDeleted); + validateFalse(acct.deleted(), errorOnAccountDeleted); final var type = isContract ? EntityType.CONTRACT : EntityType.ACCOUNT; final var expiryStatus = From 9ee469d1ba3fd978e8a71abfd97d4c242baa0759 Mon Sep 17 00:00:00 2001 From: Neeharika Sompalli <52669918+Neeharika-Sompalli@users.noreply.github.com> Date: Fri, 8 Mar 2024 15:31:24 -0600 Subject: [PATCH 046/115] fix: avoid NPE in `ThrottlesManager` post-BBM (#11970) Signed-off-by: Neeharika-Sompalli --- .../main/java/com/hedera/node/app/Hedera.java | 17 +++++++++++++- .../state/listeners/ReconnectListener.java | 3 +-- .../throttle/CongestionThrottleService.java | 16 +++++++++++--- .../networkadmin/impl/FreezeServiceImpl.java | 1 - .../impl/ReadableFreezeStoreImpl.java | 10 --------- .../impl/WritableFreezeStore.java | 15 ------------- .../ReadableFreezeUpgradeActions.java | 22 +++++++++---------- .../schemas/InitialModServiceAdminSchema.java | 18 +++++++++------ .../src/main/java/module-info.java | 1 + .../impl/test/FreezeServiceImplTest.java | 4 +--- .../impl/test/WritableFreezeStoreTest.java | 9 -------- .../ReadableFreezeUpgradeActionsTest.java | 9 -------- .../networkadmin/ReadableFreezeStore.java | 6 ----- .../bdd/suites/freeze/SimpleFreezeOnly.java | 4 +--- 14 files changed, 54 insertions(+), 81 deletions(-) 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 f2d5d4807e79..a4ea2adcaf0a 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 @@ -24,6 +24,7 @@ import static com.hedera.node.app.bbm.StateDumper.dumpMonoChildrenFrom; import static com.hedera.node.app.records.impl.BlockRecordManagerImpl.isDefaultConsTimeOfLastHandledTxn; import static com.hedera.node.app.service.contract.impl.ContractServiceImpl.CONTRACT_SERVICE; +import static com.hedera.node.app.service.mono.pbj.PbjConverter.toPbj; import static com.hedera.node.app.service.mono.state.migration.StateChildIndices.ACCOUNTS; import static com.hedera.node.app.service.mono.state.migration.StateChildIndices.CONTRACT_STORAGE; import static com.hedera.node.app.service.mono.state.migration.StateChildIndices.NETWORK_CTX; @@ -62,6 +63,7 @@ import com.hedera.node.app.service.file.ReadableFileStore; import com.hedera.node.app.service.file.impl.FileServiceImpl; import com.hedera.node.app.service.mono.context.properties.BootstrapProperties; +import com.hedera.node.app.service.mono.context.properties.SerializableSemVers; import com.hedera.node.app.service.mono.state.adapters.VirtualMapLike; import com.hedera.node.app.service.mono.state.merkle.MerkleNetworkContext; import com.hedera.node.app.service.mono.state.merkle.MerkleScheduledTransactions; @@ -83,6 +85,7 @@ import com.hedera.node.app.service.mono.utils.NamedDigestFactory; import com.hedera.node.app.service.networkadmin.impl.FreezeServiceImpl; import com.hedera.node.app.service.networkadmin.impl.NetworkServiceImpl; +import com.hedera.node.app.service.networkadmin.impl.schemas.InitialModServiceAdminSchema; import com.hedera.node.app.service.schedule.impl.ScheduleServiceImpl; import com.hedera.node.app.service.token.impl.TokenServiceImpl; import com.hedera.node.app.service.token.impl.schemas.SyntheticRecordsGenerator; @@ -225,6 +228,7 @@ public final class Hedera implements SwirldMain { private static RecordCacheService RECORD_SERVICE; private static BlockRecordService BLOCK_SERVICE; private static FeeService FEE_SERVICE; + private static CongestionThrottleService CONGESTION_THROTTLE_SERVICE; /*================================================================================================================== * @@ -288,6 +292,7 @@ public Hedera(@NonNull final ConstructableRegistry constructableRegistry) { RECORD_SERVICE = new RecordCacheService(); BLOCK_SERVICE = new BlockRecordService(); FEE_SERVICE = new FeeService(); + CONGESTION_THROTTLE_SERVICE = new CongestionThrottleService(); // FUTURE: Use the service loader framework to load these services! this.servicesRegistry = new ServicesRegistryImpl(constructableRegistry, genesisRecordsBuilder); @@ -303,7 +308,7 @@ public Hedera(@NonNull final ConstructableRegistry constructableRegistry) { RECORD_SERVICE, BLOCK_SERVICE, FEE_SERVICE, - new CongestionThrottleService(), + CONGESTION_THROTTLE_SERVICE, new NetworkServiceImpl()) .forEach(service -> servicesRegistry.register(service, version)); @@ -545,6 +550,12 @@ public void onStateInitialized( ENTITY_SERVICE.setFs(fromNetworkContext.seqNo().current()); } + // --------------------- CONGESTION THROTTLE SERVICE (14) + if (fromNetworkContext != null) { + CONGESTION_THROTTLE_SERVICE.setFs(fromNetworkContext); + InitialModServiceAdminSchema.setFs(fromNetworkContext); + } + // Here we release all mono children so that we don't have a bunch of null routes in state state.addDeserializedChildren(List.of(), 0); @@ -577,6 +588,10 @@ public void onStateInitialized( version); System.exit(1); } + } else if (previousVersion instanceof SerializableSemVers) { + deserializedVersion = new HederaSoftwareVersion( + toPbj(((SerializableSemVers) previousVersion).getProto()), + toPbj(((SerializableSemVers) previousVersion).getServices())); } else { deserializedVersion = new HederaSoftwareVersion(SemanticVersion.DEFAULT, SemanticVersion.DEFAULT); } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/listeners/ReconnectListener.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/listeners/ReconnectListener.java index 28e1626dc1ed..d566755dfa52 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/listeners/ReconnectListener.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/listeners/ReconnectListener.java @@ -73,8 +73,7 @@ public void notify(final ReconnectCompleteNotification notification) { final var upgradeFileStore = readableStoreFactory.getStore(ReadableUpgradeFileStore.class); final var upgradeActions = new ReadableFreezeUpgradeActions(networkAdminConfig, freezeStore, executor, upgradeFileStore); - upgradeActions.catchUpOnMissedSideEffects( - platformStateAccessor.getPlatformState().getFreezeTime()); + upgradeActions.catchUpOnMissedSideEffects(platformStateAccessor.getPlatformState()); } } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/throttle/CongestionThrottleService.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/throttle/CongestionThrottleService.java index ca8ee20b2421..9b44cea8ebfe 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/throttle/CongestionThrottleService.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/throttle/CongestionThrottleService.java @@ -19,12 +19,14 @@ import com.hedera.hapi.node.base.SemanticVersion; import com.hedera.hapi.node.state.congestion.CongestionLevelStarts; import com.hedera.hapi.node.state.throttles.ThrottleUsageSnapshots; +import com.hedera.node.app.service.mono.state.merkle.MerkleNetworkContext; import com.hedera.node.app.spi.Service; import com.hedera.node.app.spi.state.MigrationContext; import com.hedera.node.app.spi.state.Schema; import com.hedera.node.app.spi.state.SchemaRegistry; import com.hedera.node.app.spi.state.StateDefinition; import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; import java.util.Set; import javax.inject.Singleton; import org.apache.logging.log4j.LogManager; @@ -37,6 +39,7 @@ public class CongestionThrottleService implements Service { public static final String NAME = "CongestionThrottleService"; public static final String THROTTLE_USAGE_SNAPSHOTS_STATE_KEY = "THROTTLE_USAGE_SNAPSHOTS"; public static final String CONGESTION_LEVEL_STARTS_STATE_KEY = "CONGESTION_LEVEL_STARTS"; + private MerkleNetworkContext mnc; @NonNull @Override @@ -59,17 +62,24 @@ public Set statesToCreate() { /** {@inheritDoc} */ @Override public void migrate(@NonNull final MigrationContext ctx) { - if (ctx.previousVersion() == null) { + log.info("BBM: migrating congestion throttle service"); + final var throttleSnapshots = ctx.newStates().getSingleton(THROTTLE_USAGE_SNAPSHOTS_STATE_KEY); + final var congestionLevelStarts = ctx.newStates().getSingleton(CONGESTION_LEVEL_STARTS_STATE_KEY); + if (ctx.previousVersion() == null || mnc != null) { // At genesis we put empty throttle usage snapshots and // congestion level starts into their respective singleton // states just to ensure they exist log.info("Creating genesis throttle snapshots and congestion level starts"); - final var throttleSnapshots = ctx.newStates().getSingleton(THROTTLE_USAGE_SNAPSHOTS_STATE_KEY); throttleSnapshots.put(ThrottleUsageSnapshots.DEFAULT); - final var congestionLevelStarts = ctx.newStates().getSingleton(CONGESTION_LEVEL_STARTS_STATE_KEY); congestionLevelStarts.put(CongestionLevelStarts.DEFAULT); } + mnc = null; + log.info("BBM: finished migrating congestion throttle service"); } }); } + + public void setFs(@Nullable final MerkleNetworkContext mnc) { + this.mnc = mnc; + } } diff --git a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/FreezeServiceImpl.java b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/FreezeServiceImpl.java index 6d7501c1c0e1..b8a84794372b 100644 --- a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/FreezeServiceImpl.java +++ b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/FreezeServiceImpl.java @@ -26,7 +26,6 @@ public final class FreezeServiceImpl implements FreezeService { public static final String UPGRADE_FILE_HASH_KEY = "UPGRADE_FILE_HASH"; public static final String FREEZE_TIME_KEY = "FREEZE_TIME"; - public static final String LAST_FROZEN_TIME_KEY = "LAST_FROZEN_TIME"; @Override public void registerSchemas(@NonNull final SchemaRegistry registry, @NonNull final SemanticVersion version) { diff --git a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/ReadableFreezeStoreImpl.java b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/ReadableFreezeStoreImpl.java index 2c36829cdc7a..508e2312fc5a 100644 --- a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/ReadableFreezeStoreImpl.java +++ b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/ReadableFreezeStoreImpl.java @@ -17,7 +17,6 @@ package com.hedera.node.app.service.networkadmin.impl; import static com.hedera.node.app.service.networkadmin.impl.FreezeServiceImpl.FREEZE_TIME_KEY; -import static com.hedera.node.app.service.networkadmin.impl.FreezeServiceImpl.LAST_FROZEN_TIME_KEY; import static com.hedera.node.app.service.networkadmin.impl.FreezeServiceImpl.UPGRADE_FILE_HASH_KEY; import static java.util.Objects.requireNonNull; @@ -37,8 +36,6 @@ public class ReadableFreezeStoreImpl implements ReadableFreezeStore { /** The underlying data storage classes that hold the freeze state data. */ private final ReadableSingletonState freezeTime; - private final ReadableSingletonState lastFrozenTime; - /** The underlying data storage class that holds the prepared update file hash. * May be null if no prepared update file has been set. */ private final ReadableSingletonState updateFileHash; @@ -50,7 +47,6 @@ public class ReadableFreezeStoreImpl implements ReadableFreezeStore { public ReadableFreezeStoreImpl(@NonNull final ReadableStates states) { requireNonNull(states); this.freezeTime = states.getSingleton(FREEZE_TIME_KEY); - this.lastFrozenTime = states.getSingleton(LAST_FROZEN_TIME_KEY); this.updateFileHash = states.getSingleton(UPGRADE_FILE_HASH_KEY); } @@ -60,12 +56,6 @@ public Timestamp freezeTime() { return freezeTime.get(); } - @Override - @Nullable - public Timestamp lastFrozenTime() { - return lastFrozenTime.get(); - } - @Override @Nullable public Bytes updateFileHash() { diff --git a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/WritableFreezeStore.java b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/WritableFreezeStore.java index e4631c0f1997..434b0f035020 100644 --- a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/WritableFreezeStore.java +++ b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/WritableFreezeStore.java @@ -34,8 +34,6 @@ public class WritableFreezeStore extends ReadableFreezeStoreImpl { /** The underlying data storage classes that hold the freeze state data. */ private final WritableSingletonState freezeTimeState; - private final WritableSingletonState lastFrozenTimeState; - /** The underlying data storage class that holds the update file hash. */ private final WritableSingletonState updateFileHash; @@ -48,7 +46,6 @@ public WritableFreezeStore(@NonNull final WritableStates states) { super(states); requireNonNull(states); freezeTimeState = states.getSingleton(FreezeServiceImpl.FREEZE_TIME_KEY); - lastFrozenTimeState = states.getSingleton(FreezeServiceImpl.LAST_FROZEN_TIME_KEY); updateFileHash = states.getSingleton(FreezeServiceImpl.UPGRADE_FILE_HASH_KEY); } @@ -59,9 +56,6 @@ public WritableFreezeStore(@NonNull final WritableStates states) { */ public void freezeTime(@NonNull final Timestamp freezeTime) { freezeTimeState.put(freezeTime); - if (!freezeTime.equals(Timestamp.DEFAULT)) { - lastFrozenTimeState.put(freezeTime); - } } @Override @@ -73,15 +67,6 @@ public Timestamp freezeTime() { return freezeTimeState.get() == Timestamp.DEFAULT ? null : freezeTimeState.get(); } - @Override - @Nullable - /** - * Gets the last frozen time. If no freeze has occurred, returns null. - */ - public Timestamp lastFrozenTime() { - return lastFrozenTimeState.get(); - } - /** * Sets or clears the update file hash. * diff --git a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/handlers/ReadableFreezeUpgradeActions.java b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/handlers/ReadableFreezeUpgradeActions.java index 8346c4ddd44f..76e84b1de861 100644 --- a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/handlers/ReadableFreezeUpgradeActions.java +++ b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/handlers/ReadableFreezeUpgradeActions.java @@ -28,6 +28,7 @@ import com.hedera.node.app.service.networkadmin.ReadableFreezeStore; import com.hedera.node.config.data.NetworkAdminConfig; import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.platform.state.PlatformState; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.io.File; @@ -35,11 +36,9 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.time.Instant; import java.util.Arrays; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.io.FileUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -120,14 +119,15 @@ protected void writeSecondMarker(@NonNull final String file, @Nullable final Tim writeMarker(file, now); } - public void catchUpOnMissedSideEffects(final Instant freezeTime) { - catchUpOnMissedFreezeScheduling(freezeTime); + public void catchUpOnMissedSideEffects(final PlatformState platformState) { + catchUpOnMissedFreezeScheduling(platformState); catchUpOnMissedUpgradePrep(); } - private void catchUpOnMissedFreezeScheduling(final Instant freezeTime) { + private void catchUpOnMissedFreezeScheduling(final PlatformState platformState) { final var isUpgradePrepared = freezeStore.updateFileHash() != null; - if (isFreezeScheduled() && isUpgradePrepared) { + if (isFreezeScheduled(platformState) && isUpgradePrepared) { + final var freezeTime = platformState.getFreezeTime(); writeMarker( FREEZE_SCHEDULED_MARKER, Timestamp.newBuilder() @@ -181,12 +181,10 @@ public CompletableFuture extractSoftwareUpgrade(@NonNull final Bytes archi return extractNow(archiveData, PREPARE_UPGRADE_DESC, EXEC_IMMEDIATE_MARKER, null); } - public boolean isFreezeScheduled() { - final var ans = new AtomicBoolean(); - requireNonNull(freezeStore, "Cannot check freeze schedule without access to the dual state"); - final var freezeTime = freezeStore.freezeTime(); - ans.set(freezeTime != null && !freezeTime.equals(freezeStore.lastFrozenTime())); - return ans.get(); + public boolean isFreezeScheduled(final PlatformState platformState) { + requireNonNull(platformState, "Cannot check freeze schedule without access to the dual state"); + final var freezeTime = platformState.getFreezeTime(); + return freezeTime != null && !freezeTime.equals(platformState.getLastFrozenTime()); } /* -------- Internal Methods */ diff --git a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/schemas/InitialModServiceAdminSchema.java b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/schemas/InitialModServiceAdminSchema.java index fc58be72b7e6..f47f7483d082 100644 --- a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/schemas/InitialModServiceAdminSchema.java +++ b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/schemas/InitialModServiceAdminSchema.java @@ -19,11 +19,13 @@ import com.hedera.hapi.node.base.SemanticVersion; import com.hedera.hapi.node.base.Timestamp; import com.hedera.hapi.node.state.primitives.ProtoBytes; +import com.hedera.node.app.service.mono.state.merkle.MerkleNetworkContext; import com.hedera.node.app.service.networkadmin.impl.FreezeServiceImpl; import com.hedera.node.app.spi.state.MigrationContext; import com.hedera.node.app.spi.state.Schema; import com.hedera.node.app.spi.state.StateDefinition; import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; import java.util.Set; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -36,6 +38,7 @@ */ public class InitialModServiceAdminSchema extends Schema { private static final Logger log = LogManager.getLogger(InitialModServiceAdminSchema.class); + private static MerkleNetworkContext mnc; public InitialModServiceAdminSchema(@NonNull final SemanticVersion version) { super(version); @@ -47,12 +50,12 @@ public InitialModServiceAdminSchema(@NonNull final SemanticVersion version) { public Set statesToCreate() { return Set.of( StateDefinition.singleton(FreezeServiceImpl.UPGRADE_FILE_HASH_KEY, ProtoBytes.PROTOBUF), - StateDefinition.singleton(FreezeServiceImpl.FREEZE_TIME_KEY, Timestamp.PROTOBUF), - StateDefinition.singleton(FreezeServiceImpl.LAST_FROZEN_TIME_KEY, Timestamp.PROTOBUF)); + StateDefinition.singleton(FreezeServiceImpl.FREEZE_TIME_KEY, Timestamp.PROTOBUF)); } @Override public void migrate(@NonNull final MigrationContext ctx) { + log.info("BBM: migrating Admin service"); // Reset the upgrade file hash to empty // It should always be empty at genesis or after an upgrade, to indicate that no upgrade is in progress // Nothing in state can ever be null, so use Type.DEFAULT to indicate an empty hash @@ -61,15 +64,16 @@ public void migrate(@NonNull final MigrationContext ctx) { final var upgradeFileHashKeyState = ctx.newStates().getSingleton(FreezeServiceImpl.UPGRADE_FILE_HASH_KEY); final var freezeTimeKeyState = ctx.newStates().getSingleton(FreezeServiceImpl.FREEZE_TIME_KEY); - final var lastFrozenTimeKeyState = - ctx.newStates().getSingleton(FreezeServiceImpl.LAST_FROZEN_TIME_KEY); - if (isGenesis) { + if (isGenesis || mnc != null) { upgradeFileHashKeyState.put(ProtoBytes.DEFAULT); freezeTimeKeyState.put(Timestamp.DEFAULT); - lastFrozenTimeKeyState.put(Timestamp.DEFAULT); } + mnc = null; + log.info("BBM: finished migrating Admin service"); + } - log.info("BBM: no migration actions necessary for admin service"); + public static void setFs(@Nullable final MerkleNetworkContext mn) { + mnc = mn; } } diff --git a/hedera-node/hedera-network-admin-service-impl/src/main/java/module-info.java b/hedera-node/hedera-network-admin-service-impl/src/main/java/module-info.java index aa01345757c0..62b5c0a7fb2b 100644 --- a/hedera-node/hedera-network-admin-service-impl/src/main/java/module-info.java +++ b/hedera-node/hedera-network-admin-service-impl/src/main/java/module-info.java @@ -10,6 +10,7 @@ requires transitive com.hedera.node.config; requires transitive com.hedera.node.hapi; requires transitive com.hedera.pbj.runtime; + requires transitive com.swirlds.platform.core; requires transitive dagger; requires transitive javax.inject; requires com.hedera.node.app.hapi.utils; diff --git a/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/FreezeServiceImplTest.java b/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/FreezeServiceImplTest.java index be5c70b74feb..540334a6282f 100644 --- a/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/FreezeServiceImplTest.java +++ b/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/FreezeServiceImplTest.java @@ -17,7 +17,6 @@ package com.hedera.node.app.service.networkadmin.impl.test; import static com.hedera.node.app.service.networkadmin.impl.FreezeServiceImpl.FREEZE_TIME_KEY; -import static com.hedera.node.app.service.networkadmin.impl.FreezeServiceImpl.LAST_FROZEN_TIME_KEY; import static com.hedera.node.app.service.networkadmin.impl.FreezeServiceImpl.UPGRADE_FILE_HASH_KEY; import static com.hedera.node.app.spi.fixtures.state.TestSchema.CURRENT_VERSION; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -71,11 +70,10 @@ void registersExpectedSchema() { final var schema = schemaCaptor.getValue(); final var statesToCreate = schema.statesToCreate(); - assertEquals(3, statesToCreate.size()); + assertEquals(2, statesToCreate.size()); final var iter = statesToCreate.stream().map(StateDefinition::stateKey).sorted().iterator(); assertEquals(FREEZE_TIME_KEY, iter.next()); - assertEquals(LAST_FROZEN_TIME_KEY, iter.next()); assertEquals(UPGRADE_FILE_HASH_KEY, iter.next()); } diff --git a/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/WritableFreezeStoreTest.java b/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/WritableFreezeStoreTest.java index 1e18ee62264f..f562ec171426 100644 --- a/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/WritableFreezeStoreTest.java +++ b/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/WritableFreezeStoreTest.java @@ -62,25 +62,16 @@ void testFreezeTime() { .then(invocation -> new WritableSingletonStateBase<>( FreezeServiceImpl.FREEZE_TIME_KEY, freezeTimeBackingStore::get, freezeTimeBackingStore::set)); final AtomicReference lastFrozenBackingStore = new AtomicReference<>(null); - when(writableStates.getSingleton(FreezeServiceImpl.LAST_FROZEN_TIME_KEY)) - .then(invocation -> new WritableSingletonStateBase<>( - FreezeServiceImpl.LAST_FROZEN_TIME_KEY, - lastFrozenBackingStore::get, - lastFrozenBackingStore::set)); final WritableFreezeStore store = new WritableFreezeStore(writableStates); // test with no freeze time set assertNull(store.freezeTime()); - assertNull(store.lastFrozenTime()); // test with freeze time set final Timestamp freezeTime = Timestamp.newBuilder().seconds(1_234_567L).nanos(890).build(); store.freezeTime(freezeTime); assertEquals(freezeTime, store.freezeTime()); - - // test last frozen time - assertEquals(freezeTime, store.lastFrozenTime()); } @Test diff --git a/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/handlers/ReadableFreezeUpgradeActionsTest.java b/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/handlers/ReadableFreezeUpgradeActionsTest.java index 29e7e6475fb9..3fda13927536 100644 --- a/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/handlers/ReadableFreezeUpgradeActionsTest.java +++ b/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/handlers/ReadableFreezeUpgradeActionsTest.java @@ -158,15 +158,6 @@ void externalizesFreeze() throws IOException { assertMarkerCreated(NOW_FROZEN_MARKER, null); } - @Test - void determinesIfFreezeIsScheduled() { - assertThat(subject.isFreezeScheduled()).isFalse(); - - given(freezeStore.freezeTime()).willReturn(then); - - assertThat(subject.isFreezeScheduled()).isTrue(); - } - private void rmIfPresent(final String file) { rmIfPresent(zipOutputDir.toPath(), file); } diff --git a/hedera-node/hedera-network-admin-service/src/main/java/com/hedera/node/app/service/networkadmin/ReadableFreezeStore.java b/hedera-node/hedera-network-admin-service/src/main/java/com/hedera/node/app/service/networkadmin/ReadableFreezeStore.java index 446fbbe7713f..befab9473389 100644 --- a/hedera-node/hedera-network-admin-service/src/main/java/com/hedera/node/app/service/networkadmin/ReadableFreezeStore.java +++ b/hedera-node/hedera-network-admin-service/src/main/java/com/hedera/node/app/service/networkadmin/ReadableFreezeStore.java @@ -38,10 +38,4 @@ public interface ReadableFreezeStore { */ @Nullable Timestamp freezeTime(); - - /** - * Returns the last time a freeze was successfully completed - */ - @Nullable - Timestamp lastFrozenTime(); } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/freeze/SimpleFreezeOnly.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/freeze/SimpleFreezeOnly.java index e3ddc898a2fc..ea2b3ad05f80 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/freeze/SimpleFreezeOnly.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/freeze/SimpleFreezeOnly.java @@ -17,13 +17,11 @@ package com.hedera.services.bdd.suites.freeze; import static com.hedera.services.bdd.spec.HapiSpec.defaultHapiSpec; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.freezeOnly; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sleepFor; import com.hedera.services.bdd.spec.HapiSpec; import com.hedera.services.bdd.suites.HapiSuite; -import com.hederahashgraph.api.proto.java.ResponseCodeEnum; import java.time.Instant; import java.util.Arrays; import java.util.List; @@ -60,6 +58,6 @@ final HapiSpec simpleFreezeWithTimestamp() { return defaultHapiSpec("SimpleFreezeWithTimeStamp") .given(freezeOnly().payingWith(GENESIS).startingAt(Instant.now().plusSeconds(10))) .when(sleepFor(40000)) - .then(cryptoCreate("not_going_to_happen").hasPrecheck(ResponseCodeEnum.PLATFORM_NOT_ACTIVE)); + .then(); } } From a3b64270684b0d10081a2c3420442b9f82ddbc14 Mon Sep 17 00:00:00 2001 From: anthony-swirldslabs <152534762+anthony-swirldslabs@users.noreply.github.com> Date: Fri, 8 Mar 2024 15:37:54 -0800 Subject: [PATCH 047/115] feat: pbj-212: upgrade PBJ dependency to 0.8.1 (#11914) Signed-off-by: Anthony Petrov --- hedera-dependency-versions/build.gradle.kts | 2 +- .../formats/v7/BlockRecordFormatV7.java | 7 ++--- .../node/app/state/merkle/TestLongCodec.java | 17 ++++-------- .../app/state/merkle/TestStringCodec.java | 13 +++------ .../consensus/impl/codecs/EntityNumCodec.java | 9 ++----- .../mono/state/codec/CodecFactory.java | 3 ++- .../mono/state/codec/MonoMapCodecAdapter.java | 27 +++++-------------- .../codec/MonoSpecialFilesAdapterCodec.java | 4 ++- .../impl/serdes/EntityNumCodec.java | 3 ++- .../impl/serdes/MonoContextAdapterCodec.java | 4 ++- .../serdes/MonoRunningHashesAdapterCodec.java | 4 ++- .../token/impl/serdes/EntityNumCodec.java | 9 ++----- settings.gradle.kts | 2 +- 13 files changed, 36 insertions(+), 68 deletions(-) diff --git a/hedera-dependency-versions/build.gradle.kts b/hedera-dependency-versions/build.gradle.kts index 49bc13ef8d4b..e9e3f40c08fc 100644 --- a/hedera-dependency-versions/build.gradle.kts +++ b/hedera-dependency-versions/build.gradle.kts @@ -58,7 +58,7 @@ moduleInfo { version("com.google.jimfs", "1.2") version("com.google.protobuf", protobufVersion) version("com.google.protobuf.util", protobufVersion) - version("com.hedera.pbj.runtime", "0.7.23") + version("com.hedera.pbj.runtime", "0.8.1") version("com.squareup.javapoet", "1.13.0") version("com.sun.jna", "5.12.1") version("dagger", daggerVersion) diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/impl/producers/formats/v7/BlockRecordFormatV7.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/impl/producers/formats/v7/BlockRecordFormatV7.java index 8d99dc2a77e7..209ae29bbd66 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/impl/producers/formats/v7/BlockRecordFormatV7.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/impl/producers/formats/v7/BlockRecordFormatV7.java @@ -191,11 +191,8 @@ public record RecordStreamItemV7( } public static final class RecordStreamItemV7ProtoCodec implements Codec { - public @NonNull RecordStreamItemV7 parse(@NonNull ReadableSequentialData input) { - return new RecordStreamItemV7(null, null, null, null, 0, 0); - } - - public @NonNull RecordStreamItemV7 parseStrict(@NonNull ReadableSequentialData input) { + public @NonNull RecordStreamItemV7 parse( + @NonNull final ReadableSequentialData input, final boolean strictMode, final int maxDepth) { return new RecordStreamItemV7(null, null, null, null, 0, 0); } diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/merkle/TestLongCodec.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/merkle/TestLongCodec.java index 63377c5926cd..643074c10316 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/merkle/TestLongCodec.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/merkle/TestLongCodec.java @@ -17,6 +17,7 @@ package com.hedera.node.app.state.merkle; import com.hedera.pbj.runtime.Codec; +import com.hedera.pbj.runtime.ParseException; import com.hedera.pbj.runtime.io.ReadableSequentialData; import com.hedera.pbj.runtime.io.WritableSequentialData; import edu.umd.cs.findbugs.annotations.NonNull; @@ -39,17 +40,8 @@ private TestLongCodec() {} @NonNull @Override - public Long parse(@NonNull ReadableSequentialData input) { - Objects.requireNonNull(input); - return Long.valueOf(input.readLong()); - } - - @NonNull - @Override - // Suppressing the warning that this method is the same as requiresNodePayment. - // To be removed if that changes - @SuppressWarnings("java:S4144") - public Long parseStrict(@NonNull ReadableSequentialData input) { + public Long parse(@NonNull final ReadableSequentialData input, final boolean strictMode, final int maxDepth) + throws ParseException { Objects.requireNonNull(input); return Long.valueOf(input.readLong()); } @@ -72,7 +64,8 @@ public int measureRecord(@Nullable Long aLong) { } @Override - public boolean fastEquals(@NonNull Long value, @NonNull ReadableSequentialData input) { + public boolean fastEquals(@NonNull final Long value, @NonNull final ReadableSequentialData input) + throws ParseException { Objects.requireNonNull(value); Objects.requireNonNull(input); return value.equals(parse(input)); diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/merkle/TestStringCodec.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/merkle/TestStringCodec.java index 0823bf53cc0c..f0b72e621638 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/merkle/TestStringCodec.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/merkle/TestStringCodec.java @@ -17,6 +17,7 @@ package com.hedera.node.app.state.merkle; import com.hedera.pbj.runtime.Codec; +import com.hedera.pbj.runtime.ParseException; import com.hedera.pbj.runtime.io.ReadableSequentialData; import com.hedera.pbj.runtime.io.WritableSequentialData; import edu.umd.cs.findbugs.annotations.NonNull; @@ -39,19 +40,12 @@ private TestStringCodec() {} @NonNull @Override - public String parse(final @NonNull ReadableSequentialData input) { + public String parse(final @NonNull ReadableSequentialData input, final boolean strictMode, final int maxDepth) { Objects.requireNonNull(input); final var len = input.readInt(); return len == 0 ? "" : input.readBytes(len).asUtf8String(); } - @NonNull - @Override - public String parseStrict(final @NonNull ReadableSequentialData dataInput) { - Objects.requireNonNull(dataInput); - return parse(dataInput); - } - @Override public void write(final @NonNull String value, final @NonNull WritableSequentialData output) { Objects.requireNonNull(value); @@ -67,7 +61,8 @@ public int measure(final @NonNull ReadableSequentialData input) { } @Override - public boolean fastEquals(final @NonNull String value, final @NonNull ReadableSequentialData input) { + public boolean fastEquals(final @NonNull String value, final @NonNull ReadableSequentialData input) + throws ParseException { Objects.requireNonNull(value); Objects.requireNonNull(input); return value.equals(parse(input)); diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/codecs/EntityNumCodec.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/codecs/EntityNumCodec.java index 077b24b66a58..5fbe3da2ee41 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/codecs/EntityNumCodec.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/codecs/EntityNumCodec.java @@ -27,16 +27,11 @@ public class EntityNumCodec implements Codec { @NonNull @Override - public EntityNum parse(final @NonNull ReadableSequentialData input) throws ParseException { + public EntityNum parse(final @NonNull ReadableSequentialData input, final boolean strictMode, final int maxDepth) + throws ParseException { return new EntityNum(input.readInt()); } - @NonNull - @Override - public EntityNum parseStrict(@NonNull ReadableSequentialData dataInput) throws ParseException { - return parse(dataInput); - } - @Override public void write(final @NonNull EntityNum item, final @NonNull WritableSequentialData output) throws IOException { output.writeInt(item.intValue()); diff --git a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/state/codec/CodecFactory.java b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/state/codec/CodecFactory.java index 7878d1095c52..3b4aa9e3ec84 100644 --- a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/state/codec/CodecFactory.java +++ b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/state/codec/CodecFactory.java @@ -40,7 +40,8 @@ public static Codec newInMemoryCodec(final PbjParser parser, final Pbj return new Codec<>() { @NonNull @Override - public T parse(final @NonNull ReadableSequentialData input) throws ParseException { + public T parse(final @NonNull ReadableSequentialData input, final boolean strictMode, final int maxDepth) + throws ParseException { if (input instanceof ReadableStreamingData in) { return parser.parse(in); } else { diff --git a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/state/codec/MonoMapCodecAdapter.java b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/state/codec/MonoMapCodecAdapter.java index 43f50457a723..483048f4e0f9 100644 --- a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/state/codec/MonoMapCodecAdapter.java +++ b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/state/codec/MonoMapCodecAdapter.java @@ -58,7 +58,8 @@ public static Codec codecForSelfSerializable( return new Codec<>() { @NonNull @Override - public T parse(final @NonNull ReadableSequentialData input) throws ParseException { + public T parse(final @NonNull ReadableSequentialData input, final boolean strictMode, final int maxDepth) + throws ParseException { final var buffer = new byte[input.readInt()]; input.readBytes(buffer); final var bais = new ByteArrayInputStream(buffer); @@ -71,12 +72,6 @@ public T parse(final @NonNull ReadableSequentialData input) throws ParseExceptio return item; } - @NonNull - @Override - public T parseStrict(@NonNull ReadableSequentialData dataInput) throws ParseException { - return parse(dataInput); - } - @Override public void write(final @NonNull T item, final @NonNull WritableSequentialData output) throws IOException { final var baos = new ByteArrayOutputStream(); @@ -110,7 +105,8 @@ public static Codec codecForVirtualKey( return new Codec<>() { @NonNull @Override - public T parse(final @NonNull ReadableSequentialData input) throws ParseException { + public T parse(final @NonNull ReadableSequentialData input, final boolean strictMode, final int maxDepth) + throws ParseException { try { if (input instanceof ReadableStreamingData in) { final var item = factory.get(); @@ -136,12 +132,6 @@ public T parse(final @NonNull ReadableSequentialData input) throws ParseExceptio } } - @NonNull - @Override - public T parseStrict(@NonNull ReadableSequentialData dataInput) throws ParseException { - return parse(dataInput); - } - @Override public void write(final @NonNull T item, final @NonNull WritableSequentialData output) throws IOException { if (output instanceof WritableStreamingData out) { @@ -186,7 +176,8 @@ public static Codec codecForVirtualValue( return new Codec<>() { @NonNull @Override - public T parse(final @NonNull ReadableSequentialData input) throws ParseException { + public T parse(final @NonNull ReadableSequentialData input, final boolean strictMode, final int maxDepth) + throws ParseException { try { if (input instanceof ReadableStreamingData in) { final var item = factory.get(); @@ -212,12 +203,6 @@ public T parse(final @NonNull ReadableSequentialData input) throws ParseExceptio } } - @NonNull - @Override - public T parseStrict(@NonNull ReadableSequentialData dataInput) throws ParseException { - return parse(dataInput); - } - @Override public void write(final @NonNull T item, final @NonNull WritableSequentialData output) throws IOException { if (output instanceof WritableStreamingData out) { diff --git a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/codec/MonoSpecialFilesAdapterCodec.java b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/codec/MonoSpecialFilesAdapterCodec.java index 783d64414ed0..f6bcf4f8adfc 100644 --- a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/codec/MonoSpecialFilesAdapterCodec.java +++ b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/codec/MonoSpecialFilesAdapterCodec.java @@ -31,7 +31,9 @@ public class MonoSpecialFilesAdapterCodec implements Codec { @NonNull @Override - public MerkleSpecialFiles parse(final @NonNull ReadableSequentialData input) throws ParseException { + public MerkleSpecialFiles parse( + final @NonNull ReadableSequentialData input, final boolean strictMode, final int maxDepth) + throws ParseException { try { final var length = input.readInt(); final var javaIn = new byte[length]; diff --git a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/serdes/EntityNumCodec.java b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/serdes/EntityNumCodec.java index 99d337804a0f..fd4512031463 100644 --- a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/serdes/EntityNumCodec.java +++ b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/serdes/EntityNumCodec.java @@ -27,7 +27,8 @@ public class EntityNumCodec implements Codec { @NonNull @Override - public EntityNum parse(final @NonNull ReadableSequentialData input) throws ParseException { + public EntityNum parse(final @NonNull ReadableSequentialData input, final boolean strictMode, final int maxDepth) + throws ParseException { return new EntityNum(input.readInt()); } diff --git a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/serdes/MonoContextAdapterCodec.java b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/serdes/MonoContextAdapterCodec.java index 2021e10c43f2..38a841c237f4 100644 --- a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/serdes/MonoContextAdapterCodec.java +++ b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/serdes/MonoContextAdapterCodec.java @@ -31,7 +31,9 @@ public class MonoContextAdapterCodec implements Codec { @NonNull @Override - public MerkleNetworkContext parse(final @NonNull ReadableSequentialData input) throws ParseException { + public MerkleNetworkContext parse( + final @NonNull ReadableSequentialData input, final boolean strictMode, final int maxDepth) + throws ParseException { try { final var length = input.readInt(); final var javaIn = new byte[length]; diff --git a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/serdes/MonoRunningHashesAdapterCodec.java b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/serdes/MonoRunningHashesAdapterCodec.java index 3de6a232f1dc..9a9fe180fb19 100644 --- a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/serdes/MonoRunningHashesAdapterCodec.java +++ b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/serdes/MonoRunningHashesAdapterCodec.java @@ -31,7 +31,9 @@ public class MonoRunningHashesAdapterCodec implements Codec { @NonNull @Override - public RecordsRunningHashLeaf parse(final @NonNull ReadableSequentialData input) throws ParseException { + public RecordsRunningHashLeaf parse( + final @NonNull ReadableSequentialData input, final boolean strictMode, final int maxDepth) + throws ParseException { try { final var length = input.readInt(); final var javaIn = new byte[length]; diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/serdes/EntityNumCodec.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/serdes/EntityNumCodec.java index 99aa67b87a9e..d16d2b9fd4b6 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/serdes/EntityNumCodec.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/serdes/EntityNumCodec.java @@ -29,17 +29,12 @@ public class EntityNumCodec implements Codec { @NonNull @Override - public EntityNum parse(final @NonNull ReadableSequentialData input) throws ParseException { + public EntityNum parse(final @NonNull ReadableSequentialData input, final boolean strictMode, final int maxDepth) + throws ParseException { requireNonNull(input); return new EntityNum(input.readInt()); } - @NonNull - @Override - public EntityNum parseStrict(final @NonNull ReadableSequentialData dataInput) throws ParseException { - return parse(requireNonNull(dataInput)); - } - @Override public void write(final @NonNull EntityNum item, final @NonNull WritableSequentialData output) throws IOException { requireNonNull(item); diff --git a/settings.gradle.kts b/settings.gradle.kts index ad384728d9ef..c607a7f1d0ce 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -150,6 +150,6 @@ dependencyResolutionManagement { version("grpc-proto", "1.45.1") version("hapi-proto", hapiProtoVersion) - plugin("pbj", "com.hedera.pbj.pbj-compiler").version("0.7.23") + plugin("pbj", "com.hedera.pbj.pbj-compiler").version("0.8.1") } } From d4b9f7ca35eeadf5f4f2486e1e7a2532d7aa4d91 Mon Sep 17 00:00:00 2001 From: Cody Littley <56973212+cody-littley@users.noreply.github.com> Date: Fri, 8 Mar 2024 18:10:01 -0600 Subject: [PATCH 048/115] fix: bug in backpressure test (#11994) Signed-off-by: Cody Littley --- .../wiring/counters/BackpressureObjectCounterTests.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/counters/BackpressureObjectCounterTests.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/counters/BackpressureObjectCounterTests.java index 3d7e3a837366..e1bbee44c45d 100644 --- a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/counters/BackpressureObjectCounterTests.java +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/counters/BackpressureObjectCounterTests.java @@ -107,13 +107,15 @@ void onRampTest(final int sleepMillis) throws InterruptedException { // Sleep for a little while. Thread should be unable to on ramp another element. // Count can briefly overflow to 11, but should quickly return to 10. MILLISECONDS.sleep(50); - assertTrue(counter.getCount() == 10 || counter.getCount() == 11); + final long count1 = counter.getCount(); + assertTrue(count1 == 10 || count1 == 11, "unexpected count " + count1); // Interrupting the thread should not unblock us. thread.interrupt(); MILLISECONDS.sleep(50); // Count can briefly overflow to 11, but should quickly return to 10. - assertTrue(counter.getCount() == 10 || counter.getCount() == 11); + final long count2 = counter.getCount(); + assertTrue(count2 == 10 || count2 == 11, "unexpected count " + count2); // Off ramp one element. Thread should become unblocked. counter.offRamp(); From 5fb5ab55950fa04cdd2bbf11c35eeec8ccba919e Mon Sep 17 00:00:00 2001 From: artemananiev <33361937+artemananiev@users.noreply.github.com> Date: Fri, 8 Mar 2024 16:40:21 -0800 Subject: [PATCH 049/115] fix: 11996: The fix for #11498 doesn't cover generic object keys (#11998) Fixes: https://github.com/hashgraph/hedera-services/issues/11996 Reviewed-by: Anthony Petrov , Ivan Malygin , Oleg Mazurov Signed-off-by: Artem Ananev --- .../swirlds/merkledb/MerkleDbDataSource.java | 6 +- .../swirlds/virtual/merkle/TestObjectKey.java | 113 ++++++++++++++++++ .../merkle/TestObjectKeySerializer.java | 84 +++++++++++++ .../swirlds/virtual/merkle/map/MapTest.java | 74 +++++++++++- 4 files changed, 272 insertions(+), 5 deletions(-) create mode 100644 platform-sdk/swirlds-merkle/src/test/java/com/swirlds/virtual/merkle/TestObjectKey.java create mode 100644 platform-sdk/swirlds-merkle/src/test/java/com/swirlds/virtual/merkle/TestObjectKeySerializer.java diff --git a/platform-sdk/swirlds-jasperdb/src/main/java/com/swirlds/merkledb/MerkleDbDataSource.java b/platform-sdk/swirlds-jasperdb/src/main/java/com/swirlds/merkledb/MerkleDbDataSource.java index a3196f660714..3a442346e524 100644 --- a/platform-sdk/swirlds-jasperdb/src/main/java/com/swirlds/merkledb/MerkleDbDataSource.java +++ b/platform-sdk/swirlds-jasperdb/src/main/java/com/swirlds/merkledb/MerkleDbDataSource.java @@ -1217,7 +1217,11 @@ private void writeLeavesToPathToKeyValue( longKeyToPath.put(key, INVALID_PATH); } } else { - objectKeyToPath.deleteIfEqual(leafRecord.getKey(), path); + if (isReconnect) { + objectKeyToPath.deleteIfEqual(leafRecord.getKey(), path); + } else { + objectKeyToPath.delete(leafRecord.getKey()); + } } statisticsUpdater.countFlushLeavesDeleted(); diff --git a/platform-sdk/swirlds-merkle/src/test/java/com/swirlds/virtual/merkle/TestObjectKey.java b/platform-sdk/swirlds-merkle/src/test/java/com/swirlds/virtual/merkle/TestObjectKey.java new file mode 100644 index 000000000000..5cddda518095 --- /dev/null +++ b/platform-sdk/swirlds-merkle/src/test/java/com/swirlds/virtual/merkle/TestObjectKey.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2016-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.virtual.merkle; + +import com.hedera.pbj.runtime.io.ReadableSequentialData; +import com.hedera.pbj.runtime.io.WritableSequentialData; +import com.swirlds.common.io.streams.SerializableDataInputStream; +import com.swirlds.common.io.streams.SerializableDataOutputStream; +import com.swirlds.virtualmap.VirtualKey; +import java.io.IOException; +import java.nio.ByteBuffer; + +public final class TestObjectKey implements VirtualKey { + + public static final int BYTES = Long.BYTES * 2; + + private long k; + + public TestObjectKey() {} + + public TestObjectKey(long value) { + this.k = value; + } + + public TestObjectKey copy() { + return new TestObjectKey(k); + } + + @Override + public int getVersion() { + return 1; + } + + long getValue() { + return k; + } + + @Override + public void serialize(SerializableDataOutputStream out) throws IOException { + out.writeLong(k); + out.writeLong(k); + } + + void serialize(final WritableSequentialData out) { + out.writeLong(k); + out.writeLong(k); + } + + void serialize(final ByteBuffer buffer) { + buffer.putLong(k); + buffer.putLong(k); + } + + @Override + public void deserialize(SerializableDataInputStream in, int version) throws IOException { + k = in.readLong(); + long kk = in.readLong(); + assert k == kk : "Malformed TestObjectKey"; + } + + void deserialize(final ReadableSequentialData in) { + k = in.readLong(); + long kk = in.readLong(); + assert k == kk : "Malformed TestObjectKey"; + } + + void deserialize(final ByteBuffer buffer) { + k = buffer.getLong(); + long kk = buffer.getLong(); + assert k == kk : "Malformed TestObjectKey"; + } + + @Override + public int hashCode() { + return Long.hashCode(k); + } + + @Override + public String toString() { + if (Character.isAlphabetic((char) k)) { + return "TestObjectKey{ " + ((char) k) + " }"; + } else { + return "TestObjectKey{ " + k + " }"; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TestObjectKey other = (TestObjectKey) o; + return k == other.k; + } + + @Override + public long getClassId() { + return 0x255bb9565ebfad4bL; + } +} diff --git a/platform-sdk/swirlds-merkle/src/test/java/com/swirlds/virtual/merkle/TestObjectKeySerializer.java b/platform-sdk/swirlds-merkle/src/test/java/com/swirlds/virtual/merkle/TestObjectKeySerializer.java new file mode 100644 index 000000000000..118244d7e398 --- /dev/null +++ b/platform-sdk/swirlds-merkle/src/test/java/com/swirlds/virtual/merkle/TestObjectKeySerializer.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2016-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.virtual.merkle; + +import com.hedera.pbj.runtime.io.ReadableSequentialData; +import com.hedera.pbj.runtime.io.WritableSequentialData; +import com.hedera.pbj.runtime.io.buffer.BufferedData; +import com.swirlds.merkledb.serialize.KeySerializer; +import java.nio.ByteBuffer; + +public class TestObjectKeySerializer implements KeySerializer { + + public TestObjectKeySerializer() { + // required for deserialization + } + + @Override + public long getClassId() { + return 8838922; + } + + @Override + public int getVersion() { + return 1; + } + + @Override + public int getSerializedSize() { + return TestObjectKey.BYTES; + } + + @Override + public long getCurrentDataVersion() { + return 1; + } + + @Override + public void serialize(final TestObjectKey data, final WritableSequentialData out) { + data.serialize(out); + } + + @Override + public void serialize(TestObjectKey data, ByteBuffer buffer) { + data.serialize(buffer); + } + + @Override + public TestObjectKey deserialize(final ReadableSequentialData in) { + final TestObjectKey key = new TestObjectKey(); + key.deserialize(in); + return key; + } + + @Override + public TestObjectKey deserialize(final ByteBuffer buffer, final long dataVersion) { + final TestObjectKey key = new TestObjectKey(); + key.deserialize(buffer); + return key; + } + + @Override + public boolean equals(final BufferedData buffer, final TestObjectKey keyToCompare) { + return (buffer.readLong() == keyToCompare.getValue()) && (buffer.readLong() == keyToCompare.getValue()); + } + + @Override + public boolean equals(final ByteBuffer buffer, final int dataVersion, final TestObjectKey keyToCompare) { + return (buffer.getLong() == keyToCompare.getValue()) && (buffer.getLong() == keyToCompare.getValue()); + } +} diff --git a/platform-sdk/swirlds-merkle/src/test/java/com/swirlds/virtual/merkle/map/MapTest.java b/platform-sdk/swirlds-merkle/src/test/java/com/swirlds/virtual/merkle/map/MapTest.java index dac9be213d1c..fba8bfd0560a 100644 --- a/platform-sdk/swirlds-merkle/src/test/java/com/swirlds/virtual/merkle/map/MapTest.java +++ b/platform-sdk/swirlds-merkle/src/test/java/com/swirlds/virtual/merkle/map/MapTest.java @@ -19,6 +19,8 @@ import static com.swirlds.common.test.fixtures.junit.tags.TestQualifierTags.TIME_CONSUMING; import static com.swirlds.common.test.fixtures.junit.tags.TestQualifierTags.TIMING_SENSITIVE; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -27,10 +29,15 @@ import com.swirlds.merkledb.MerkleDbTableConfig; import com.swirlds.virtual.merkle.TestKey; import com.swirlds.virtual.merkle.TestKeySerializer; +import com.swirlds.virtual.merkle.TestObjectKey; +import com.swirlds.virtual.merkle.TestObjectKeySerializer; import com.swirlds.virtual.merkle.TestValue; import com.swirlds.virtual.merkle.TestValueSerializer; import com.swirlds.virtualmap.VirtualMap; import com.swirlds.virtualmap.datasource.VirtualDataSourceBuilder; +import com.swirlds.virtualmap.datasource.VirtualLeafRecord; +import com.swirlds.virtualmap.internal.RecordAccessor; +import com.swirlds.virtualmap.internal.merkle.VirtualRootNode; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Tags; @@ -39,7 +46,7 @@ @Tag(TIMING_SENSITIVE) final class MapTest { - VirtualDataSourceBuilder createBuilder() { + VirtualDataSourceBuilder createLongBuilder() { final MerkleDbTableConfig tableConfig = new MerkleDbTableConfig<>( (short) 1, DigestType.SHA_384, (short) 1, new TestKeySerializer(), @@ -47,8 +54,20 @@ VirtualDataSourceBuilder createBuilder() { return new MerkleDbDataSourceBuilder<>(tableConfig); } - VirtualMap createMap(String label) { - return new VirtualMap<>(label, createBuilder()); + VirtualDataSourceBuilder createGenericBuilder() { + final MerkleDbTableConfig tableConfig = new MerkleDbTableConfig<>( + (short) 1, DigestType.SHA_384, + (short) 1, new TestObjectKeySerializer(), + (short) 1, new TestValueSerializer()); + return new MerkleDbDataSourceBuilder<>(tableConfig); + } + + VirtualMap createLongMap(String label) { + return new VirtualMap<>(label, createLongBuilder()); + } + + VirtualMap createObjectMap(String label) { + return new VirtualMap<>(label, createGenericBuilder()); } @Test @@ -58,7 +77,7 @@ VirtualMap createMap(String label) { void insertRemoveAndModifyOneMillion() throws InterruptedException { final int changesPerBatch = 15_432; // Some unexpected size just to be crazy final int max = 1_000_000; - VirtualMap map = createMap("insertRemoveAndModifyOneMillion"); + VirtualMap map = createLongMap("insertRemoveAndModifyOneMillion"); try { for (int i = 0; i < max; i++) { if (i > 0 && i % changesPerBatch == 0) { @@ -104,4 +123,51 @@ void insertRemoveAndModifyOneMillion() throws InterruptedException { map.release(); } } + + @Test + @Tags({@Tag("VirtualMerkle")}) + @DisplayName("Delete a value that was moved to a different virtual path") + void deletedObjectLeavesOnFlush() throws InterruptedException { + VirtualMap map = createObjectMap("deletedObjectLeavesOnFlush"); + for (int i = 0; i < 8; i++) { + map.put(new TestObjectKey(i), new TestValue(i)); + } + + VirtualRootNode rootNode = map.getRight(); + rootNode.enableFlush(); + + RecordAccessor records = rootNode.getRecords(); + // Check that key/value 0 is at path 7 + VirtualLeafRecord leaf = records.findLeafRecord(7, false); + assertNotNull(leaf); + assertEquals(new TestObjectKey(0), leaf.getKey()); + assertEquals(new TestValue(0), leaf.getValue()); + + VirtualMap copy = map.copy(); + map.release(); + map = copy; + rootNode.waitUntilFlushed(); + + // Move key/value to a different path, then delete + map.remove(new TestObjectKey(0)); + map.remove(new TestObjectKey(2)); + map.put(new TestObjectKey(8), new TestValue(8)); + map.put(new TestObjectKey(0), new TestValue(0)); + map.remove(new TestObjectKey(0)); + + rootNode = map.getRight(); + rootNode.enableFlush(); + + copy = map.copy(); + map.release(); + map = copy; + rootNode.waitUntilFlushed(); + + // During this second flush, key/value 0 must be deleted from the map despite it's + // path the virtual tree doesn't match the path in the data source + assertFalse(map.containsKey(new TestObjectKey(0))); + assertNull(map.get(new TestObjectKey(0))); + + map.release(); + } } From 5dd1481ce36396dc96f6ab0ee9aa69b9c69deccb Mon Sep 17 00:00:00 2001 From: Michael Tinker Date: Sat, 9 Mar 2024 19:52:33 -0600 Subject: [PATCH 050/115] fix: use `signedTransactionBytes` in synthetic record builders (#12008) Signed-off-by: Michael Tinker --- .../handle/record/GenesisRecordsConsensusHook.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/GenesisRecordsConsensusHook.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/GenesisRecordsConsensusHook.java index 19a21a98234a..4d86233ab8d8 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/GenesisRecordsConsensusHook.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/GenesisRecordsConsensusHook.java @@ -19,11 +19,11 @@ import static com.hedera.node.app.service.token.impl.handlers.staking.StakingRewardsHelper.asAccountAmounts; import static com.hedera.node.app.spi.HapiUtils.ACCOUNT_ID_COMPARATOR; import static com.hedera.node.app.spi.HapiUtils.FUNDING_ACCOUNT_EXPIRY; +import static com.hedera.node.app.spi.workflows.record.SingleTransactionRecordBuilder.transactionWith; import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.Duration; -import com.hedera.hapi.node.base.Transaction; import com.hedera.hapi.node.base.TransferList; import com.hedera.hapi.node.state.token.Account; import com.hedera.hapi.node.token.CryptoCreateTransactionBody; @@ -170,15 +170,15 @@ private void createAccountRecordBuilders( recordBuilder.memo(recordMemo); } - var txnBody = newCryptoCreate(account); + final var op = newCryptoCreate(account); if (overrideAutoRenewPeriod != null) { - txnBody.autoRenewPeriod(Duration.newBuilder().seconds(overrideAutoRenewPeriod)); + op.autoRenewPeriod(Duration.newBuilder().seconds(overrideAutoRenewPeriod)); } - var txnBuilder = - Transaction.newBuilder().body(TransactionBody.newBuilder().cryptoCreateAccount(txnBody)); - recordBuilder.transaction(txnBuilder.build()); + final var body = + TransactionBody.newBuilder().cryptoCreateAccount(op).build(); + recordBuilder.transaction(transactionWith(body)); - var balance = account.tinybarBalance(); + final var balance = account.tinybarBalance(); if (balance != 0) { var accountID = AccountID.newBuilder() .accountNum(account.accountId().accountNumOrElse(0L)) From c6363be45239ea880efbedfda62b798eef68b0bf Mon Sep 17 00:00:00 2001 From: Lazar Petrovic Date: Mon, 11 Mar 2024 15:16:12 +0100 Subject: [PATCH 051/115] chore: add JMH event serialization benchmark (#12014) Signed-off-by: Lazar Petrovic --- .../platform/core/jmh/EventSerialization.java | 81 +++++++++++++++++++ .../fixtures/event/TestingEventBuilder.java | 26 +++++- 2 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 platform-sdk/swirlds-platform-core/src/jmh/java/com/swirlds/platform/core/jmh/EventSerialization.java diff --git a/platform-sdk/swirlds-platform-core/src/jmh/java/com/swirlds/platform/core/jmh/EventSerialization.java b/platform-sdk/swirlds-platform-core/src/jmh/java/com/swirlds/platform/core/jmh/EventSerialization.java new file mode 100644 index 000000000000..0ee414494e7b --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/jmh/java/com/swirlds/platform/core/jmh/EventSerialization.java @@ -0,0 +1,81 @@ +/* + * 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.core.jmh; + +import com.swirlds.common.constructable.ConstructableRegistry; +import com.swirlds.common.constructable.ConstructableRegistryException; +import com.swirlds.common.io.streams.MerkleDataInputStream; +import com.swirlds.common.io.streams.MerkleDataOutputStream; +import com.swirlds.platform.event.GossipEvent; +import com.swirlds.platform.system.StaticSoftwareVersion; +import com.swirlds.platform.test.fixtures.event.TestingEventBuilder; +import java.io.IOException; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +@State(Scope.Benchmark) +@Fork(value = 1) +@Warmup(iterations = 1, time = 1) +@Measurement(iterations = 3, time = 10) +public class EventSerialization { + + @Param({"0"}) + public long seed; + + private GossipEvent event; + private MerkleDataOutputStream outStream; + private MerkleDataInputStream inStream; + + @Setup + public void setup() throws IOException, ConstructableRegistryException { + event = TestingEventBuilder.builder() + .setNumberOfSystemTransactions(1) + .setSeed(seed) + .buildEvent(); + StaticSoftwareVersion.setSoftwareVersion(event.getHashedData().getSoftwareVersion()); + ConstructableRegistry.getInstance().registerConstructables("com.swirlds.platform.system"); + final PipedInputStream inputStream = new PipedInputStream(); + final PipedOutputStream outputStream = new PipedOutputStream(inputStream); + outStream = new MerkleDataOutputStream(outputStream); + inStream = new MerkleDataInputStream(inputStream); + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public void serializeDeserialize(final Blackhole bh) throws IOException { + // results on Lazar's M1 Max MacBook Pro: + // + // Benchmark (seed) Mode Cnt Score Error Units + // EventSerialization.serializeDeserialize 0 thrpt 3 962.486 ± 29.252 ops/ms + outStream.writeSerializable(event, false); + bh.consume(inStream.readSerializable(false, GossipEvent::new)); + } +} diff --git a/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/event/TestingEventBuilder.java b/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/event/TestingEventBuilder.java index 45dfa48cb88c..4582e8c25c9f 100644 --- a/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/event/TestingEventBuilder.java +++ b/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/event/TestingEventBuilder.java @@ -27,6 +27,7 @@ import com.swirlds.platform.system.events.EventConstants; import com.swirlds.platform.system.events.EventDescriptor; import com.swirlds.platform.system.transaction.ConsensusTransactionImpl; +import com.swirlds.platform.system.transaction.StateSignatureTransaction; import com.swirlds.platform.system.transaction.SwirldTransaction; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; @@ -48,6 +49,8 @@ public class TestingEventBuilder { private Instant timeCreated; /** the number of transactions an event should contain */ private int numberOfTransactions; + /** the number of system transactions an event should contain */ + private int numberOfSystemTransactions; /** the transaction size */ private int transactionSize; /** the transactions of an event */ @@ -80,6 +83,7 @@ private TestingEventBuilder() {} creatorId = new NodeId(0); timeCreated = null; numberOfTransactions = 2; + numberOfSystemTransactions = 0; transactionSize = 4; transactions = null; selfParent = null; @@ -164,6 +168,18 @@ private TestingEventBuilder() {} return this; } + /** + * Set the number of system transactions an event should contain. If {@link #setTransactions(ConsensusTransactionImpl[])} + * is called with a non-null value, this setting will be ignored. + * + * @param numberOfSystemTransactions the number of system transactions + * @return this instance + */ + public @NonNull TestingEventBuilder setNumberOfSystemTransactions(final int numberOfSystemTransactions) { + this.numberOfSystemTransactions = numberOfSystemTransactions; + return this; + } + /** * Set the transaction size. If {@link #setTransactions(ConsensusTransactionImpl[])} is called with a non-null * value, this setting will be ignored. @@ -299,12 +315,18 @@ private TestingEventBuilder() {} public @NonNull GossipEvent buildGossipEvent() { final ConsensusTransactionImpl[] tr; if (transactions == null) { - tr = new ConsensusTransactionImpl[numberOfTransactions]; - for (int i = 0; i < tr.length; ++i) { + tr = new ConsensusTransactionImpl[numberOfTransactions + numberOfSystemTransactions]; + for (int i = 0; i < numberOfTransactions; ++i) { final byte[] bytes = new byte[transactionSize]; random.nextBytes(bytes); tr[i] = new SwirldTransaction(bytes); } + for (int i = numberOfTransactions; i < numberOfTransactions + numberOfSystemTransactions; ++i) { + tr[i] = new StateSignatureTransaction( + random.nextLong(0, Long.MAX_VALUE), + RandomUtils.randomSignature(random), + RandomUtils.randomHash(random)); + } } else { tr = transactions; } From 47b3721c52ca9fe30a37110e72e42b742aa5b6b1 Mon Sep 17 00:00:00 2001 From: Austin Littley <102969658+alittley@users.noreply.github.com> Date: Mon, 11 Mar 2024 10:27:40 -0400 Subject: [PATCH 052/115] feat: Combine ISS detector inputs (#11902) Signed-off-by: Austin Littley --- .../com/swirlds/platform/SwirldsPlatform.java | 11 +- .../platform/state/iss/IssDetector.java | 196 +++-- .../platform/wiring/PlatformWiring.java | 2 +- .../wiring/components/IssDetectorWiring.java | 54 +- .../wiring/components/StateAndRound.java | 13 +- .../test/state/IssDetectorTestHelper.java | 99 ++- .../platform/test/state/IssDetectorTests.java | 745 +++++++++--------- .../test/state/RoundHashValidatorTests.java | 19 +- 8 files changed, 612 insertions(+), 527 deletions(-) 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 f4a7d3e02d48..e2331db5a5ef 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 @@ -165,7 +165,6 @@ import com.swirlds.platform.util.ThingsToStart; import com.swirlds.platform.wiring.NoInput; import com.swirlds.platform.wiring.PlatformWiring; -import com.swirlds.platform.wiring.components.IssDetectorWiring; import com.swirlds.platform.wiring.components.StateAndRound; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; @@ -568,12 +567,10 @@ public class SwirldsPlatform implements Platform { stateManagementComponent.newSignedStateFromTransactions( state.getAndReserve("stateManagementComponent.newSignedStateFromTransactions")); - final IssDetectorWiring issDetectorWiring = platformWiring.getIssDetectorWiring(); - // FUTURE WORK: these three method calls will be combined into a single method call - issDetectorWiring.roundCompletedInput().put(roundNumber); - issDetectorWiring.newStateHashed().put(state.getAndReserve("issDetector")); - issDetectorWiring.handleConsensusRound().put(consensusRound); - + platformWiring + .getIssDetectorWiring() + .stateAndRoundInput() + .put(stateAndRound.makeAdditionalReservation("issDetector")); platformWiring.getSignatureCollectorConsensusInput().put(consensusRound); stateAndRound.reservedSignedState().close(); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/iss/IssDetector.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/iss/IssDetector.java index defca9b1c9a6..8e992c16c3e8 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/iss/IssDetector.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/iss/IssDetector.java @@ -19,8 +19,6 @@ import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; import static com.swirlds.logging.legacy.LogMarker.STARTUP; import static com.swirlds.logging.legacy.LogMarker.STATE_HASH; -import static java.util.stream.Collectors.collectingAndThen; -import static java.util.stream.Collectors.toList; import com.swirlds.common.context.PlatformContext; import com.swirlds.common.crypto.Hash; @@ -30,8 +28,10 @@ import com.swirlds.common.utility.throttle.RateLimiter; import com.swirlds.logging.legacy.payload.IssPayload; import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; +import com.swirlds.platform.components.transaction.system.SystemTransactionExtractionUtils; import com.swirlds.platform.config.StateConfig; import com.swirlds.platform.consensus.ConsensusConfig; +import com.swirlds.platform.internal.ConsensusRound; import com.swirlds.platform.metrics.IssMetrics; import com.swirlds.platform.state.iss.internal.ConsensusHashFinder; import com.swirlds.platform.state.iss.internal.HashValidityStatus; @@ -42,6 +42,7 @@ import com.swirlds.platform.system.state.notifications.IssNotification; import com.swirlds.platform.system.state.notifications.IssNotification.IssType; import com.swirlds.platform.system.transaction.StateSignatureTransaction; +import com.swirlds.platform.wiring.components.StateAndRound; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.time.Duration; @@ -66,9 +67,13 @@ public class IssDetector { * The address book of this network. */ private final AddressBook addressBook; - /** The current epoch hash */ + /** + * The current epoch hash + */ private final Hash currentEpochHash; - /** The current software version */ + /** + * The current software version + */ private final SoftwareVersion currentSoftwareVersion; /** @@ -105,7 +110,9 @@ public class IssDetector { * A round that should not be validated. Set to {@link #DO_NOT_IGNORE_ROUNDS} if all rounds should be validated. */ private final long ignoredRound; - /** ISS related metrics */ + /** + * ISS related metrics + */ private final IssMetrics issMetrics; /** @@ -165,26 +172,39 @@ public void signalEndOfPreconsensusReplay(@Nullable final Object ignored) { } /** - * Called when a round has been completed. + * Create an ISS notification if the round shouldn't be ignored * - * @param round the round that was just completed - * @return a list of ISS notifications, or null if no ISS occurred + * @param roundNumber the round number of the ISS + * @param issType the type of the ISS + * @return an ISS notification, or null if the round of the ISS should be ignored */ - public @Nullable List roundCompleted(final long round) { - if (round <= previousRound) { - throw new IllegalArgumentException( - "previous round was " + previousRound + ", can't decrease round to " + round); + private @Nullable IssNotification maybeCreateIssNotification( + final long roundNumber, @NonNull final IssType issType) { + if (roundNumber == ignoredRound) { + return null; } + return new IssNotification(roundNumber, issType); + } - if (round == ignoredRound) { - // This round is intentionally ignored. - return null; + /** + * Shift the round data window when a new round is completed. + *

    + * If any round that is removed by shifting the window hasn't already had its hash decided, then this method will + * force a decision on the hash, and handle any ISS events that result. + * + * @param roundNumber the round that was just completed + * @return a list of ISS notifications, which may be empty, but will not contain null + */ + private @NonNull List shiftRoundDataWindow(final long roundNumber) { + if (roundNumber <= previousRound) { + throw new IllegalArgumentException( + "previous round was " + previousRound + ", can't decrease round to " + roundNumber); } - final long oldestRoundToValidate = round - roundData.getSequenceNumberCapacity() + 1; + final long oldestRoundToValidate = roundNumber - roundData.getSequenceNumberCapacity() + 1; final List removedRounds = new ArrayList<>(); - if (round != previousRound + 1) { + if (roundNumber != previousRound + 1) { // We are either loading the first state at boot time, or we had a reconnect that caused us to skip some // rounds. Rounds that have not yet been validated at this point in time should not be considered // evidence of a catastrophic ISS. @@ -193,11 +213,41 @@ public void signalEndOfPreconsensusReplay(@Nullable final Object ignored) { roundData.shiftWindow(oldestRoundToValidate, (k, v) -> removedRounds.add(v)); } - final long roundWeight = addressBook.getTotalWeight(); - previousRound = round; + previousRound = roundNumber; + + roundData.put(roundNumber, new RoundHashValidator(roundNumber, addressBook.getTotalWeight(), issMetrics)); - roundData.put(round, new RoundHashValidator(round, roundWeight, issMetrics)); - return listOrNull(removedRounds.stream().map(this::handleRemovedRound).toList()); + return removedRounds.stream() + .map(this::handleRemovedRound) + .filter(Objects::nonNull) + .toList(); + } + + /** + * Called when a round has been completed. + *

    + * Expects the contained state to have been reserved by the caller for this method. This method will release the + * state reservation when it is done with it. + * + * @param stateAndRound the round and state to be handled + * @return a list of ISS notifications, or null if no ISS occurred + */ + public @Nullable List handleStateAndRound(@NonNull final StateAndRound stateAndRound) { + try (final ReservedSignedState state = stateAndRound.reservedSignedState()) { + final long roundNumber = stateAndRound.round().getRoundNum(); + + final List issNotifications = new ArrayList<>(shiftRoundDataWindow(roundNumber)); + + final IssNotification selfHashCheckResult = + checkSelfStateHash(roundNumber, state.get().getState().getHash()); + if (selfHashCheckResult != null) { + issNotifications.add(selfHashCheckResult); + } + + issNotifications.addAll(handlePostconsensusSignatures(stateAndRound.round())); + + return issNotifications.isEmpty() ? null : issNotifications; + } } /** @@ -217,8 +267,14 @@ public void signalEndOfPreconsensusReplay(@Nullable final Object ignored) { final HashValidityStatus status = roundHashValidator.getStatus(); if (status == HashValidityStatus.CATASTROPHIC_ISS || status == HashValidityStatus.CATASTROPHIC_LACK_OF_DATA) { - handleCatastrophic(roundHashValidator); - return new IssNotification(roundHashValidator.getRound(), IssType.CATASTROPHIC_ISS); + + final IssNotification notification = + maybeCreateIssNotification(roundHashValidator.getRound(), IssType.CATASTROPHIC_ISS); + if (notification != null) { + handleCatastrophic(roundHashValidator); + } + + return notification; } else if (status == HashValidityStatus.LACK_OF_DATA) { handleLackOfData(roundHashValidator); } else { @@ -232,13 +288,21 @@ public void signalEndOfPreconsensusReplay(@Nullable final Object ignored) { /** * Handle postconsensus state signatures. * - * @param transactions the signature transactions to handle - * @return a list of ISS notifications, or null if no ISS occurred + * @param round the round that may contain state signatures + * @return a list of ISS notifications, which may be empty, but will not contain null */ - public @Nullable List handlePostconsensusSignatures( - @NonNull final List> transactions) { - return listOrNull( - transactions.stream().map(this::handlePostconsensusSignature).toList()); + private @NonNull List handlePostconsensusSignatures(@NonNull final ConsensusRound round) { + final List> stateSignatureTransactions = + SystemTransactionExtractionUtils.extractFromRound(round, StateSignatureTransaction.class); + + if (stateSignatureTransactions == null) { + return List.of(); + } + + return stateSignatureTransactions.stream() + .map(this::handlePostconsensusSignature) + .filter(Objects::nonNull) + .toList(); } /** @@ -266,7 +330,7 @@ public void signalEndOfPreconsensusReplay(@Nullable final Object ignored) { } if (ignorePreconsensusSignatures && replayingPreconsensusStream) { - // We are still replaying preconsensus events and we are configured to ignore signatures during replay + // We are still replaying preconsensus events, and we are configured to ignore signatures during replay return null; } @@ -308,31 +372,13 @@ public void signalEndOfPreconsensusReplay(@Nullable final Object ignored) { } /** - * Called when this node finishes hashing a state. - * - * @param state the state that was hashed - * @return a list of ISS notifications, or null if no ISS occurred - */ - public @Nullable List newStateHashed(@NonNull final ReservedSignedState state) { - try (state) { - return listOrNull(newStateHashed( - state.get().getRound(), state.get().getState().getHash())); - } - } - - /** - * Called when this node finishes hashing a state. + * Checks the validity of the self state hash for a round. * * @param round the round of the state * @param hash the hash of the state * @return an ISS notification, or null if no ISS occurred */ - private @Nullable IssNotification newStateHashed(final long round, @NonNull final Hash hash) { - if (round == ignoredRound) { - // This round is intentionally ignored. - return null; - } - + private @Nullable IssNotification checkSelfStateHash(final long round, @NonNull final Hash hash) { final RoundHashValidator roundHashValidator = roundData.get(round); if (roundHashValidator == null) { throw new IllegalStateException( @@ -348,19 +394,25 @@ public void signalEndOfPreconsensusReplay(@Nullable final Object ignored) { /** * Called when an overriding state is obtained, i.e. via reconnect or state loading. + *

    + * Expects the input state to have been reserved by the caller for this method. This method will release the state + * reservation when it is done with it. * * @param state the state that was loaded * @return a list of ISS notifications, or null if no ISS occurred */ public @Nullable List overridingState(@NonNull final ReservedSignedState state) { try (state) { - final long round = state.get().getRound(); - final Hash stateHash = state.get().getState().getHash(); + final long roundNumber = state.get().getRound(); // this is not practically possible for this to happen. Even if it were to happen, on a reconnect, // we are receiving a new state that is fully signed, so any ISSs in the past should be ignored. // so we will ignore any ISSs from removed rounds - roundCompleted(round); - return listOrNull(newStateHashed(round, stateHash)); + shiftRoundDataWindow(roundNumber); + + final Hash stateHash = state.get().getState().getHash(); + final IssNotification issNotification = checkSelfStateHash(roundNumber, stateHash); + + return issNotification == null ? null : List.of(issNotification); } } @@ -376,17 +428,23 @@ public void signalEndOfPreconsensusReplay(@Nullable final Object ignored) { return switch (roundValidator.getStatus()) { case VALID -> { if (roundValidator.hasDisagreement()) { - yield new IssNotification(round, IssType.OTHER_ISS); + yield maybeCreateIssNotification(round, IssType.OTHER_ISS); } yield null; } case SELF_ISS -> { - handleSelfIss(roundValidator); - yield new IssNotification(round, IssType.SELF_ISS); + final IssNotification notification = maybeCreateIssNotification(round, IssType.SELF_ISS); + if (notification != null) { + handleSelfIss(roundValidator); + } + yield notification; } case CATASTROPHIC_ISS -> { - handleCatastrophic(roundValidator); - yield new IssNotification(round, IssType.CATASTROPHIC_ISS); + final IssNotification notification = maybeCreateIssNotification(round, IssType.CATASTROPHIC_ISS); + if (notification != null) { + handleCatastrophic(roundValidator); + } + yield notification; } case UNDECIDED -> throw new IllegalStateException( "status is undecided, but method reported a decision, round = " + round); @@ -490,24 +548,4 @@ private static void writeSkippedLogCount(@NonNull final StringBuilder sb, final .append("seconds."); } } - - /** - * @param n the notification to wrap - * @return a list containing the notification, or null if the notification is null - */ - private static List listOrNull(@Nullable final IssNotification n) { - return n == null ? null : List.of(n); - } - - /** - * @param list the list to filter - * @return the list, or null if the list is null or empty - */ - private static List listOrNull(@Nullable final List list) { - return list == null - ? null - : list.stream() - .filter(Objects::nonNull) - .collect(collectingAndThen(toList(), l -> l.isEmpty() ? null : l)); - } } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformWiring.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformWiring.java index 37cee0989f07..ccc0db360279 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformWiring.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformWiring.java @@ -237,7 +237,7 @@ public PlatformWiring(@NonNull final PlatformContext platformContext) { gossipWiring = GossipWiring.create(model); eventWindowManagerWiring = EventWindowManagerWiring.create(model); - issDetectorWiring = IssDetectorWiring.create(model, schedulers.issDetectorScheduler()); + issDetectorWiring = IssDetectorWiring.create(schedulers.issDetectorScheduler()); issHandlerWiring = IssHandlerWiring.create(schedulers.issHandlerScheduler()); hashLoggerWiring = HashLoggerWiring.create(schedulers.hashLoggerScheduler()); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/IssDetectorWiring.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/IssDetectorWiring.java index fbef264399b5..c4aa4d77f347 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/IssDetectorWiring.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/IssDetectorWiring.java @@ -16,19 +16,13 @@ package com.swirlds.platform.wiring.components; -import com.swirlds.common.wiring.model.WiringModel; import com.swirlds.common.wiring.schedulers.TaskScheduler; -import com.swirlds.common.wiring.transformers.WireTransformer; import com.swirlds.common.wiring.wires.input.BindableInputWire; import com.swirlds.common.wiring.wires.input.InputWire; import com.swirlds.common.wiring.wires.output.OutputWire; -import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; -import com.swirlds.platform.components.transaction.system.SystemTransactionExtractionUtils; -import com.swirlds.platform.internal.ConsensusRound; import com.swirlds.platform.state.iss.IssDetector; import com.swirlds.platform.state.signed.ReservedSignedState; import com.swirlds.platform.system.state.notifications.IssNotification; -import com.swirlds.platform.system.transaction.StateSignatureTransaction; import com.swirlds.platform.wiring.NoInput; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.List; @@ -36,49 +30,29 @@ /** * Wiring for the {@link IssDetector}. * - * @param endOfPcesReplay the input wire for the end of the PCES replay - * @param roundCompletedInput the input wire for completed rounds - * @param handleConsensusRound the input wire for consensus rounds - * @param handlePostconsensusSignatures the input wire for postconsensus signatures - * @param newStateHashed the input wire for new hashed states - * @param overridingState the input wire for overriding states - * @param issNotificationOutput the output wire for ISS notifications + * @param endOfPcesReplay the input wire for the end of the PCES replay + * @param stateAndRoundInput the input wire for completed rounds and their corresponding states + * @param overridingState the input wire for overriding states + * @param issNotificationOutput the output wire for ISS notifications */ public record IssDetectorWiring( @NonNull InputWire endOfPcesReplay, - @NonNull InputWire roundCompletedInput, - @NonNull InputWire handleConsensusRound, - @NonNull InputWire>> handlePostconsensusSignatures, - @NonNull InputWire newStateHashed, + @NonNull InputWire stateAndRoundInput, @NonNull InputWire overridingState, @NonNull OutputWire issNotificationOutput) { /** * Create a new instance of this wiring. * - * @param model the wiring model * @param taskScheduler the task scheduler that will detect ISSs * @return the new wiring instance */ @NonNull - public static IssDetectorWiring create( - @NonNull final WiringModel model, @NonNull final TaskScheduler> taskScheduler) { - final WireTransformer>> - roundTransformer = new WireTransformer<>( - model, - "extractSignaturesForIssDetector", - "consensus round", - round -> SystemTransactionExtractionUtils.extractFromRound( - round, StateSignatureTransaction.class)); - final InputWire>> sigInput = - taskScheduler.buildInputWire("post consensus signatures"); - roundTransformer.getOutputWire().solderTo(sigInput); + public static IssDetectorWiring create(@NonNull final TaskScheduler> taskScheduler) { + return new IssDetectorWiring( - taskScheduler.buildInputWire("endOfPcesReplay"), - taskScheduler.buildInputWire("roundCompleted"), - roundTransformer.getInputWire(), - sigInput, - taskScheduler.buildInputWire("newStateHashed"), - taskScheduler.buildInputWire("overridingState"), + taskScheduler.buildInputWire("end of PCES replay"), + taskScheduler.buildInputWire("state and round"), + taskScheduler.buildInputWire("overriding state"), taskScheduler.getOutputWire().buildSplitter("issNotificationSplitter", "iss notifications")); } @@ -89,12 +63,8 @@ public static IssDetectorWiring create( */ public void bind(@NonNull final IssDetector issDetector) { ((BindableInputWire) endOfPcesReplay).bind(issDetector::signalEndOfPreconsensusReplay); - ((BindableInputWire>) roundCompletedInput).bind(issDetector::roundCompleted); - ((BindableInputWire>, List>) - handlePostconsensusSignatures) - .bind(issDetector::handlePostconsensusSignatures); - ((BindableInputWire>) newStateHashed) - .bind(issDetector::newStateHashed); + ((BindableInputWire>) stateAndRoundInput) + .bind(issDetector::handleStateAndRound); ((BindableInputWire>) overridingState) .bind(issDetector::overridingState); } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/StateAndRound.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/StateAndRound.java index 0917b026b9b1..c929d3c9303e 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/StateAndRound.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/StateAndRound.java @@ -26,4 +26,15 @@ * @param reservedSignedState the state * @param round the round that caused the state to be created */ -public record StateAndRound(@NonNull ReservedSignedState reservedSignedState, @NonNull ConsensusRound round) {} +public record StateAndRound(@NonNull ReservedSignedState reservedSignedState, @NonNull ConsensusRound round) { + /** + * Make an additional reservation on the reserved signed state + * + * @param reservationReason the reason for the reservation + * @return a copy of this object, which has its own new reservation on the state + */ + @NonNull + public StateAndRound makeAdditionalReservation(@NonNull final String reservationReason) { + return new StateAndRound(reservedSignedState.getAndReserve(reservationReason), round); + } +} diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/IssDetectorTestHelper.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/IssDetectorTestHelper.java index 3b3834dc49ec..3603a4c472cc 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/IssDetectorTestHelper.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/IssDetectorTestHelper.java @@ -16,71 +16,84 @@ package com.swirlds.platform.test.state; -import com.swirlds.common.context.PlatformContext; -import com.swirlds.common.crypto.Hash; -import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.state.iss.IssDetector; import com.swirlds.platform.state.signed.ReservedSignedState; -import com.swirlds.platform.system.BasicSoftwareVersion; -import com.swirlds.platform.system.address.AddressBook; import com.swirlds.platform.system.state.notifications.IssNotification; -import com.swirlds.platform.system.state.notifications.IssNotification.IssType; -import com.swirlds.platform.system.transaction.StateSignatureTransaction; +import com.swirlds.platform.wiring.components.StateAndRound; import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; import java.util.ArrayList; import java.util.List; -import java.util.Optional; -import java.util.Set; +import java.util.Objects; -public class IssDetectorTestHelper extends IssDetector { - /** the default epoch hash to use */ - private static final Hash DEFAULT_EPOCH_HASH = null; +/** + * A helper class for testing the {@link com.swirlds.platform.state.iss.IssDetector}. + */ +public class IssDetectorTestHelper { + private int selfIssCount = 0; + private int catastrophicIssCount = 0; - private final List issList = new ArrayList<>(); + private final List issNotificationList = new ArrayList<>(); - public IssDetectorTestHelper( - @NonNull final PlatformContext platformContext, final AddressBook addressBook, final long ignoredRound) { - super(platformContext, addressBook, DEFAULT_EPOCH_HASH, new BasicSoftwareVersion(1), false, ignoredRound); - } + private final IssDetector issDetector; - @Override - public List roundCompleted(final long round) { - return processList(super.roundCompleted(round)); + public IssDetectorTestHelper(@NonNull final IssDetector issDetector) { + this.issDetector = Objects.requireNonNull(issDetector); } - @Override - public List handlePostconsensusSignatures( - @NonNull final List> transactions) { - return processList(super.handlePostconsensusSignatures(transactions)); + public void handleStateAndRound(@NonNull final StateAndRound stateAndRound) { + trackIssNotifications(issDetector.handleStateAndRound(stateAndRound)); } - @Override - public List newStateHashed(@NonNull final ReservedSignedState state) { - return processList(super.newStateHashed(state)); + public void overridingState(@NonNull final ReservedSignedState state) { + trackIssNotifications(issDetector.overridingState(state)); } - @Override - public List overridingState(@NonNull final ReservedSignedState state) { - return processList(super.overridingState(state)); - } + /** + * Keeps track of all ISS notifications passed to this method over the course of a test, for the sake of validation + * + * @param notifications the list of ISS notifications to track. permitted to be null. + */ + private void trackIssNotifications(@Nullable final List notifications) { + if (notifications == null) { + return; + } + + notifications.forEach(notification -> { + if (notification.getIssType() == IssNotification.IssType.SELF_ISS) { + selfIssCount++; + } else if (notification.getIssType() == IssNotification.IssType.CATASTROPHIC_ISS) { + catastrophicIssCount++; + } - public List getIssList() { - return issList; + issNotificationList.add(notification); + }); } - public int getIssCount() { - return issList.size(); + /** + * Get the number of self ISS notifications that have been observed. + * + * @return the number of self ISS notifications + */ + public int getSelfIssCount() { + return selfIssCount; } - public long getIssCount(final IssType... types) { - return issList.stream() - .map(IssNotification::getIssType) - .filter(Set.of(types)::contains) - .count(); + /** + * Get the number of catastrophic ISS notifications that have been observed. + * + * @return the number of catastrophic ISS notifications + */ + public int getCatastrophicIssCount() { + return catastrophicIssCount; } - private List processList(final List list) { - Optional.ofNullable(list).ifPresent(issList::addAll); - return list; + /** + * Get the list of all ISS notifications that have been observed. + * + * @return the list of all ISS notifications + */ + public List getIssNotificationList() { + return issNotificationList; } } diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/IssDetectorTests.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/IssDetectorTests.java index 14305d27bdcc..8d10b730ae94 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/IssDetectorTests.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/IssDetectorTests.java @@ -22,50 +22,144 @@ import static com.swirlds.common.utility.Threshold.SUPER_MAJORITY; import static com.swirlds.platform.state.iss.IssDetector.DO_NOT_IGNORE_ROUNDS; import static com.swirlds.platform.test.state.RoundHashValidatorTests.generateCatastrophicNodeHashes; -import static com.swirlds.platform.test.state.RoundHashValidatorTests.generateNodeHashes; import static com.swirlds.platform.test.state.RoundHashValidatorTests.generateRegularNodeHashes; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import com.swirlds.common.context.PlatformContext; import com.swirlds.common.crypto.Hash; import com.swirlds.common.crypto.Signature; import com.swirlds.common.platform.NodeId; import com.swirlds.common.test.fixtures.platform.TestPlatformContextBuilder; -import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.consensus.ConsensusConfig; +import com.swirlds.platform.internal.ConsensusRound; +import com.swirlds.platform.internal.EventImpl; import com.swirlds.platform.state.State; +import com.swirlds.platform.state.iss.IssDetector; import com.swirlds.platform.state.iss.internal.HashValidityStatus; import com.swirlds.platform.state.signed.ReservedSignedState; import com.swirlds.platform.state.signed.SignedState; import com.swirlds.platform.system.BasicSoftwareVersion; import com.swirlds.platform.system.address.Address; import com.swirlds.platform.system.address.AddressBook; -import com.swirlds.platform.system.state.notifications.IssNotification.IssType; +import com.swirlds.platform.system.events.BaseEventHashedData; +import com.swirlds.platform.system.state.notifications.IssNotification; +import com.swirlds.platform.system.transaction.ConsensusTransactionImpl; import com.swirlds.platform.system.transaction.StateSignatureTransaction; import com.swirlds.platform.test.fixtures.addressbook.RandomAddressBookGenerator; +import com.swirlds.platform.wiring.components.StateAndRound; +import edu.umd.cs.findbugs.annotations.NonNull; import java.util.ArrayList; +import java.util.Collection; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Random; -import java.util.Set; -import java.util.stream.StreamSupport; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; -@DisplayName("ConsensusHashManager Tests") +@DisplayName("IssDetector Tests") class IssDetectorTests { + private static final Hash DEFAULT_EPOCH_HASH = null; + + /** + * Generates a list of events, with each event containing a signature transaction from a node for the given round. + * + * @param roundNumber the round that signature transactions will be for + * @param hashGenerationData the data to use to generate the signature transactions + * @return a list of events, each containing a signature transaction from a node for the given round + */ + private static List generateEventsContainingSignatures( + final long roundNumber, @NonNull final RoundHashValidatorTests.HashGenerationData hashGenerationData) { + + return hashGenerationData.nodeList().stream() + .map(nodeHashInfo -> { + final StateSignatureTransaction signatureTransaction = new StateSignatureTransaction( + roundNumber, mock(Signature.class), nodeHashInfo.nodeStateHash()); + + final BaseEventHashedData hashedData = mock(BaseEventHashedData.class); + when(hashedData.getCreatorId()).thenReturn(nodeHashInfo.nodeId()); + when(hashedData.getSoftwareVersion()).thenReturn(new BasicSoftwareVersion(1)); + when(hashedData.getTransactions()) + .thenReturn(new ConsensusTransactionImpl[] {signatureTransaction}); + + final EventImpl event = mock(EventImpl.class); + when(event.getHashedData()).thenReturn(hashedData); + when(event.getCreatorId()).thenReturn(nodeHashInfo.nodeId()); + + return event; + }) + .toList(); + } + + /** + * Generates a list of events, with each event containing a signature transaction from a node for the given round. + *

    + * One event will be created for each node in the address book, and all signatures will be made on a single + * consistent hash. + * + * @param addressBook the address book to use to generate the signature transactions + * @param roundNumber the round that signature transactions will be for + * @param roundHash the hash that all signature transactions will be made on + * @return a list of events, each containing a signature transaction from a node for the given round + */ + private static List generateEventsWithConsistentSignatures( + @NonNull final AddressBook addressBook, final long roundNumber, @NonNull final Hash roundHash) { + final List nodeHashInfos = new ArrayList<>(); + + addressBook.forEach(address -> nodeHashInfos.add( + new RoundHashValidatorTests.NodeHashInfo(address.getNodeId(), roundHash, roundNumber))); + + // create signature transactions for this round + return generateEventsContainingSignatures( + roundNumber, new RoundHashValidatorTests.HashGenerationData(nodeHashInfos, roundHash)); + } + + /** + * Randomly selects ~50% of a collection of candidate events to include in a round, and removes them from the + * candidate events collection. + * + * @param random a source of randomness + * @param candidateEvents the collection of candidate events to select from + * @return a list of events to include in a round + */ + private static List selectRandomEvents( + @NonNull final Random random, @NonNull final Collection candidateEvents) { + + final List eventsToInclude = new ArrayList<>(); + candidateEvents.forEach(event -> { + if (random.nextBoolean()) { + eventsToInclude.add(event); + } + }); + candidateEvents.removeAll(eventsToInclude); + + return eventsToInclude; + } + + /** + * Creates a mock consensus round, which includes a given list of events. + * + * @param roundNumber the round number + * @param eventsToInclude the events to include in the round + * @return a mock consensus round + */ + private static ConsensusRound createRoundWithSignatureEvents( + final long roundNumber, @NonNull final List eventsToInclude) { + final ConsensusRound consensusRound = mock(ConsensusRound.class); + when(consensusRound.getConsensusEvents()).thenReturn(eventsToInclude); + when(consensusRound.getRoundNum()).thenReturn(roundNumber); + + return consensusRound; + } @Test - @DisplayName("Valid Signatures After Hash Test") - void validSignaturesAfterHashTest() { + @DisplayName("No ISSes Test") + void noIss() { final Random random = getRandomPrintSeed(); - final AddressBook addressBook = new RandomAddressBookGenerator(random) .setSize(100) .setAverageWeight(100) @@ -75,30 +169,54 @@ void validSignaturesAfterHashTest() { final PlatformContext platformContext = TestPlatformContextBuilder.create().build(); - final IssDetectorTestHelper manager = - new IssDetectorTestHelper(platformContext, addressBook, DO_NOT_IGNORE_ROUNDS); + final IssDetector issDetector = new IssDetector( + platformContext, + addressBook, + DEFAULT_EPOCH_HASH, + new BasicSoftwareVersion(1), + false, + DO_NOT_IGNORE_ROUNDS); + final IssDetectorTestHelper issDetectorTestHelper = new IssDetectorTestHelper(issDetector); + + // signature events are generated for each round when that round is handled, and then are included randomly + // in subsequent rounds + final List signatureEvents = new ArrayList<>(); + + long currentRound = 0; + + issDetectorTestHelper.overridingState(mockState(currentRound, randomHash())); - final int rounds = 1_000; - for (long round = 1; round <= rounds; round++) { + for (currentRound++; currentRound <= 1_000; currentRound++) { final Hash roundHash = randomHash(random); - if (round == 1) { - manager.overridingState(mockState(round, roundHash)); - } else { - manager.roundCompleted(round); - manager.newStateHashed(mockState(round, roundHash)); - } - final long r = round; - StreamSupport.stream(addressBook.spliterator(), false) - .map(a -> new ScopedSystemTransaction<>( - a.getNodeId(), - new BasicSoftwareVersion(1), - new StateSignatureTransaction(r, mock(Signature.class), roundHash))) - .forEach(t -> manager.handlePostconsensusSignatures(List.of(t))); + // create signature transactions for this round + signatureEvents.addAll(generateEventsWithConsistentSignatures(addressBook, currentRound, roundHash)); + + // randomly select half of unsubmitted signature events to include in this round + final List eventsToInclude = selectRandomEvents(random, signatureEvents); + final ConsensusRound consensusRound = createRoundWithSignatureEvents(currentRound, eventsToInclude); + + issDetectorTestHelper.handleStateAndRound( + new StateAndRound(mockState(currentRound, roundHash), consensusRound)); } - assertTrue(manager.getIssList().isEmpty(), "there should be no ISS notifications"); + + // Add all remaining unsubmitted signature events + final ConsensusRound consensusRound = createRoundWithSignatureEvents(currentRound, signatureEvents); + issDetectorTestHelper.handleStateAndRound( + new StateAndRound(mockState(currentRound, randomHash(random)), consensusRound)); + + assertEquals(0, issDetectorTestHelper.getSelfIssCount(), "there should be no ISS notifications"); + assertEquals( + 0, + issDetectorTestHelper.getCatastrophicIssCount(), + "there should be no catastrophic ISS notifications"); + assertEquals(0, issDetectorTestHelper.getIssNotificationList().size(), "there should be no ISS notifications"); } + /** + * This test goes through a series of rounds, some of which experience ISSes. The test verifies that the expected + * number of ISSes are registered by the ISS detector. + */ @Test @DisplayName("Mixed Order Test") void mixedOrderTest() { @@ -171,234 +289,86 @@ void mixedOrderTest() { } } - final IssDetectorTestHelper manager = - new IssDetectorTestHelper(platformContext, addressBook, DO_NOT_IGNORE_ROUNDS); - - manager.overridingState(mockState(0L, selfHashes.getFirst())); + final IssDetector issDetector = new IssDetector( + platformContext, + addressBook, + DEFAULT_EPOCH_HASH, + new BasicSoftwareVersion(1), + false, + DO_NOT_IGNORE_ROUNDS); + final IssDetectorTestHelper issDetectorTestHelper = new IssDetectorTestHelper(issDetector); - // Start collecting data for rounds. - for (long round = 1; round < roundsNonAncient; round++) { - manager.roundCompleted(round); - } + long currentRound = 0; - // Add all the self hashes. - for (long round = 1; round < roundsNonAncient; round++) { - manager.newStateHashed(mockState(round, selfHashes.get((int) round))); - } + issDetectorTestHelper.overridingState(mockState(currentRound, selfHashes.getFirst())); - // Report hashes from the network in random order - final List operations = new ArrayList<>(); - while (!roundData.isEmpty()) { - final int index = random.nextInt(roundData.size()); - operations.add(roundData.get(index).nodeList().removeFirst()); - if (roundData.get(index).nodeList().isEmpty()) { - roundData.remove(index); - } - } + // signature events are generated for each round when that round is handled, and then are included randomly + // in subsequent rounds + final List signatureEvents = + new ArrayList<>(generateEventsContainingSignatures(0, roundData.getFirst())); - assertEquals(roundsNonAncient * addressBook.getSize(), operations.size(), "unexpected number of operations"); + for (currentRound++; currentRound < roundsNonAncient; currentRound++) { + // create signature transactions for this round + signatureEvents.addAll(generateEventsContainingSignatures(currentRound, roundData.get((int) currentRound))); - operations.stream() - .map(nhi -> new ScopedSystemTransaction<>( - nhi.nodeId(), - new BasicSoftwareVersion(1), - new StateSignatureTransaction(nhi.round(), mock(Signature.class), nhi.nodeStateHash()))) - .forEach(t -> manager.handlePostconsensusSignatures(List.of(t))); + // randomly select half of unsubmitted signature events to include in this round + final List eventsToInclude = selectRandomEvents(random, signatureEvents); - // Shifting after completion should have no side effects - for (long i = roundsNonAncient; i < 2L * roundsNonAncient - 1; i++) { - manager.roundCompleted(i); + final ConsensusRound consensusRound = createRoundWithSignatureEvents(currentRound, eventsToInclude); + issDetectorTestHelper.handleStateAndRound( + new StateAndRound(mockState(currentRound, selfHashes.get((int) currentRound)), consensusRound)); } + // Add all remaining signature events + final ConsensusRound consensusRound = createRoundWithSignatureEvents(roundsNonAncient, signatureEvents); + issDetectorTestHelper.handleStateAndRound( + new StateAndRound(mockState(roundsNonAncient, randomHash(random)), consensusRound)); + assertEquals( expectedSelfIssCount, - manager.getIssList().stream() - .filter(n -> n.getIssType() == IssType.SELF_ISS) - .count(), - "unexpected number of ISS callbacks"); + issDetectorTestHelper.getSelfIssCount(), + "unexpected number of self ISS notifications"); assertEquals( expectedCatastrophicIssCount, - manager.getIssList().stream() - .filter(n -> n.getIssType() == IssType.CATASTROPHIC_ISS) - .count(), - "unexpected number of catastrophic ISS callbacks"); - manager.getIssList().forEach(n -> { - final IssType expectedType = - switch (expectedRoundStatus.get((int) n.getRound())) { - case SELF_ISS -> IssType.SELF_ISS; - case CATASTROPHIC_ISS -> IssType.CATASTROPHIC_ISS; + issDetectorTestHelper.getCatastrophicIssCount(), + "unexpected number of catastrophic ISS notifications"); + + final Collection observedRounds = new HashSet<>(); + issDetectorTestHelper.getIssNotificationList().forEach(notification -> { + assertTrue( + observedRounds.add(notification.getRound()), "rounds should trigger a notification at most once"); + + final IssNotification.IssType expectedType = + switch (expectedRoundStatus.get((int) notification.getRound())) { + case SELF_ISS -> IssNotification.IssType.SELF_ISS; + case CATASTROPHIC_ISS -> IssNotification.IssType.CATASTROPHIC_ISS; // if there was an other-ISS, then the round should still be valid - case VALID -> IssType.OTHER_ISS; + case VALID -> IssNotification.IssType.OTHER_ISS; default -> throw new IllegalStateException( - "Unexpected value: " + expectedRoundStatus.get((int) n.getRound())); + "Unexpected value: " + expectedRoundStatus.get((int) notification.getRound())); }; assertEquals( expectedType, - n.getIssType(), + notification.getIssType(), "Expected status for round %d to be %s but was %s" - .formatted(n.getRound(), expectedRoundStatus.get((int) n.getRound()), n.getIssType())); + .formatted( + notification.getRound(), + expectedRoundStatus.get((int) notification.getRound()), + notification.getIssType())); }); - final Set observedRounds = new HashSet<>(); - manager.getIssList() - .forEach(n -> assertTrue( - observedRounds.add(n.getRound()), "rounds should trigger a notification at most once")); } /** - * The method generateNodeHashes() doesn't account for self ID, and therefore doesn't guarantee that any particular - * node will have an ISS. Regenerate data until we find a data set that results in a self ISS. + * Handles additional rounds after an ISS occurred, but before all signatures have been submitted. Validates + * that the ISS is detected after enough signatures are submitted, and not before. */ - private static RoundHashValidatorTests.HashGenerationData generateDataWithSelfIss( - final Random random, final AddressBook addressBook, final NodeId selfId, final long targetRound) { - - int triesRemaining = 1000; - - while (triesRemaining > 0) { - triesRemaining--; - - final RoundHashValidatorTests.HashGenerationData data = - generateNodeHashes(random, addressBook, HashValidityStatus.SELF_ISS, targetRound); - - for (final RoundHashValidatorTests.NodeHashInfo info : data.nodeList()) { - if (info.nodeId() == selfId) { - if (!info.nodeStateHash().equals(data.consensusHash())) { - return data; - } - break; - } - } - } - throw new IllegalStateException("unable to generate data with a self ISS"); - } - - @Test - @SuppressWarnings("UnnecessaryLocalVariable") - @DisplayName("Early Add Test") - void earlyAddTest() { - final Random random = getRandomPrintSeed(); - - final PlatformContext platformContext = - TestPlatformContextBuilder.create().build(); - - final int roundsNonAncient = platformContext - .getConfiguration() - .getConfigData(ConsensusConfig.class) - .roundsNonAncient(); - final AddressBook addressBook = new RandomAddressBookGenerator(random) - .setSize(100) - .setAverageWeight(100) - .setWeightStandardDeviation(50) - .build(); - final NodeId selfId = addressBook.getNodeId(0); - - final IssDetectorTestHelper manager = - new IssDetectorTestHelper(platformContext, addressBook, DO_NOT_IGNORE_ROUNDS); - - // Start collecting data for rounds. - for (long round = 0; round < roundsNonAncient; round++) { - manager.roundCompleted(round); - } - - // We are not yet collecting data for this round - final long targetRound = roundsNonAncient; - - // Add data. Should be ignored since we are not processing data for this round yet. - final RoundHashValidatorTests.HashGenerationData ignoredData = - generateCatastrophicNodeHashes(random, addressBook, targetRound); - for (final RoundHashValidatorTests.NodeHashInfo info : ignoredData.nodeList()) { - if (info.nodeId() == selfId) { - assertThrows( - IllegalStateException.class, - () -> manager.newStateHashed(mockState(targetRound, info.nodeStateHash())), - "should not be able to add hash for round not being tracked"); - } - manager.handlePostconsensusSignatures(List.of(new ScopedSystemTransaction<>( - info.nodeId(), - new BasicSoftwareVersion(1), - new StateSignatureTransaction(targetRound, mock(Signature.class), info.nodeStateHash())))); - } - - assertEquals(0, manager.getIssList().size(), "all data should have been ignored"); - - // Move forward to the next round. Data should no longer be ignored. - // Use a different data set so we can know if old data was fully ignored. - final RoundHashValidatorTests.HashGenerationData data = - generateDataWithSelfIss(random, addressBook, selfId, targetRound); - manager.roundCompleted(targetRound); - for (final RoundHashValidatorTests.NodeHashInfo info : data.nodeList()) { - if (info.nodeId() == selfId) { - manager.newStateHashed(mockState(targetRound, info.nodeStateHash())); - } - manager.handlePostconsensusSignatures(List.of(new ScopedSystemTransaction<>( - info.nodeId(), - new BasicSoftwareVersion(1), - new StateSignatureTransaction(targetRound, mock(Signature.class), info.nodeStateHash())))); - } - - assertEquals(1, manager.getIssList().size(), "data should not have been ignored"); - } - - @Test - @DisplayName("Late Add Test") - void lateAddTest() { - final Random random = getRandomPrintSeed(); - - final PlatformContext platformContext = - TestPlatformContextBuilder.create().build(); - - final int roundsNonAncient = platformContext - .getConfiguration() - .getConfigData(ConsensusConfig.class) - .roundsNonAncient(); - final AddressBook addressBook = new RandomAddressBookGenerator(random) - .setSize(100) - .setAverageWeight(100) - .setWeightStandardDeviation(50) - .build(); - final NodeId selfId = addressBook.getNodeId(0); - - final IssDetectorTestHelper manager = - new IssDetectorTestHelper(platformContext, addressBook, DO_NOT_IGNORE_ROUNDS); - - // Start collecting data for rounds. - // After this method, round 0 will be too old and will not be tracked. - for (long round = 0; round <= roundsNonAncient; round++) { - manager.roundCompleted(round); - } - - final long targetRound = 0; - - // Add data. Should be ignored since we are not processing data for this round anymore. - final RoundHashValidatorTests.HashGenerationData ignoredData = - generateCatastrophicNodeHashes(random, addressBook, targetRound); - for (final RoundHashValidatorTests.NodeHashInfo info : ignoredData.nodeList()) { - if (info.nodeId() == selfId) { - assertThrows( - IllegalStateException.class, - () -> manager.newStateHashed(mockState(targetRound, info.nodeStateHash())), - "should not be able to add hash for round not being tracked"); - } - manager.handlePostconsensusSignatures(List.of(new ScopedSystemTransaction<>( - info.nodeId(), - new BasicSoftwareVersion(1), - new StateSignatureTransaction(targetRound, mock(Signature.class), info.nodeStateHash())))); - } - - assertEquals(0, manager.getIssCount(), "all data should have been ignored"); - } - @Test - @DisplayName("Shift Before Complete Test") - void shiftBeforeCompleteTest() { + @DisplayName("Decide hash for catastrophic ISS") + void decideForCatastrophicIss() { final Random random = getRandomPrintSeed(); - final PlatformContext platformContext = TestPlatformContextBuilder.create().build(); - final int roundsNonAncient = platformContext - .getConfiguration() - .getConfigData(ConsensusConfig.class) - .roundsNonAncient(); final AddressBook addressBook = new RandomAddressBookGenerator(random) .setSize(100) .setAverageWeight(100) @@ -406,54 +376,81 @@ void shiftBeforeCompleteTest() { .build(); final NodeId selfId = addressBook.getNodeId(0); - final IssDetectorTestHelper manager = - new IssDetectorTestHelper(platformContext, addressBook, DO_NOT_IGNORE_ROUNDS); - - // Start collecting data for rounds. - for (long round = 0; round < roundsNonAncient; round++) { - manager.roundCompleted(round); - } - - final long targetRound = 0; - - // Add data, but not enough to be certain of an ISS. - final RoundHashValidatorTests.HashGenerationData data = - generateCatastrophicNodeHashes(random, addressBook, targetRound); - - for (final RoundHashValidatorTests.NodeHashInfo info : data.nodeList()) { - if (info.nodeId() == selfId) { - manager.newStateHashed(mockState(0L, info.nodeStateHash())); - } + final IssDetector issDetector = new IssDetector( + platformContext, + addressBook, + DEFAULT_EPOCH_HASH, + new BasicSoftwareVersion(1), + false, + DO_NOT_IGNORE_ROUNDS); + final IssDetectorTestHelper issDetectorTestHelper = new IssDetectorTestHelper(issDetector); + + long currentRound = 0; + + // start with an initial state + issDetectorTestHelper.overridingState(mockState(currentRound, randomHash())); + currentRound++; + + // the round after the initial state will have a catastrophic iss + final RoundHashValidatorTests.HashGenerationData catastrophicHashData = + generateCatastrophicNodeHashes(random, addressBook, currentRound); + final Hash selfHashForCatastrophicRound = catastrophicHashData.nodeList().stream() + .filter(info -> info.nodeId() == selfId) + .findFirst() + .map(RoundHashValidatorTests.NodeHashInfo::nodeStateHash) + .orElseThrow(); + final List signaturesOnCatastrophicRound = + generateEventsContainingSignatures(currentRound, catastrophicHashData); + + // handle the catastrophic round, but don't submit any signatures yet, so it won't be detected + issDetectorTestHelper.handleStateAndRound(new StateAndRound( + mockState(currentRound, selfHashForCatastrophicRound), + createRoundWithSignatureEvents(currentRound, List.of()))); + + // handle some more rounds on top of the catastrophic round + for (currentRound++; currentRound < 10; currentRound++) { + // don't include any signatures + issDetectorTestHelper.handleStateAndRound(new StateAndRound( + mockState(currentRound, randomHash()), createRoundWithSignatureEvents(currentRound, List.of()))); } + // submit signatures on the ISS round that represent a minority of the weight long submittedWeight = 0; - for (final RoundHashValidatorTests.NodeHashInfo info : data.nodeList()) { - final long weight = addressBook.getAddress(info.nodeId()).getWeight(); + final List signaturesToSubmit = new ArrayList<>(); + for (final EventImpl signatureEvent : signaturesOnCatastrophicRound) { + final long weight = + addressBook.getAddress(signatureEvent.getCreatorId()).getWeight(); if (MAJORITY.isSatisfiedBy(submittedWeight + weight, addressBook.getTotalWeight())) { // If we add less than a majority then we won't be able to detect the ISS no matter what break; } submittedWeight += weight; - - manager.handlePostconsensusSignatures(List.of(new ScopedSystemTransaction<>( - info.nodeId(), - new BasicSoftwareVersion(1), - new StateSignatureTransaction(targetRound, mock(Signature.class), info.nodeStateHash())))); + signaturesToSubmit.add(signatureEvent); } - // Shift the window even though we have not added enough data for a decision - manager.roundCompleted(roundsNonAncient); + issDetectorTestHelper.handleStateAndRound(new StateAndRound( + mockState(currentRound, randomHash()), + createRoundWithSignatureEvents(currentRound, signaturesToSubmit))); + assertEquals( + 0, + issDetectorTestHelper.getIssNotificationList().size(), + "there shouldn't have been enough data submitted to observe the ISS"); + + currentRound++; - System.out.println(manager.getIssList()); + // submit the remaining signatures in the next round + issDetectorTestHelper.handleStateAndRound(new StateAndRound( + mockState(currentRound, randomHash()), + createRoundWithSignatureEvents(currentRound, signaturesOnCatastrophicRound))); - assertEquals(0, manager.getIssCount(), "there wasn't enough data submitted to observe the ISS"); + assertEquals( + 1, issDetectorTestHelper.getCatastrophicIssCount(), "the catastrophic round should have caused an ISS"); } /** - * Generate data in an order that will cause a catastrophic ISS after the timeout, assuming the bare minimum to meet - * ≥2/3 has been met. + * Generate data in an order that will cause a catastrophic ISS after the timeout, but without a supermajority of + * signatures being on an incorrect hash. */ - @SuppressWarnings("SameParameterValue") private static List generateCatastrophicTimeoutIss( final Random random, final AddressBook addressBook, final long targetRound) { @@ -479,11 +476,15 @@ private static List generateCatastrophicTi return data; } + /** + * Causes a catastrophic ISS, but shifts the window before deciding on a consensus hash. Even though we don't get + * enough signatures to "decide", there will be enough signatures to declare a catastrophic ISS when shifting + * the window past the ISS round. + */ @Test @DisplayName("Catastrophic Shift Before Complete Test") void catastrophicShiftBeforeCompleteTest() { final Random random = getRandomPrintSeed(); - final PlatformContext platformContext = TestPlatformContextBuilder.create().build(); @@ -498,34 +499,34 @@ void catastrophicShiftBeforeCompleteTest() { .build(); final NodeId selfId = addressBook.getNodeId(0); - final IssDetectorTestHelper manager = - new IssDetectorTestHelper(platformContext, addressBook, DO_NOT_IGNORE_ROUNDS); - - // Start collecting data for rounds. - for (long round = 0; round < roundsNonAncient; round++) { - manager.roundCompleted(round); - } - - final long targetRound = 0; - - // Add data, but not enough to be certain of an ISS. - final List data = - generateCatastrophicTimeoutIss(random, addressBook, targetRound); - - for (final RoundHashValidatorTests.NodeHashInfo info : data) { - if (info.nodeId() == selfId) { - manager.newStateHashed(mockState(0L, info.nodeStateHash())); - } - } + final IssDetector issDetector = new IssDetector( + platformContext, + addressBook, + DEFAULT_EPOCH_HASH, + new BasicSoftwareVersion(1), + false, + DO_NOT_IGNORE_ROUNDS); + final IssDetectorTestHelper issDetectorTestHelper = new IssDetectorTestHelper(issDetector); + + long currentRound = 0; + + final List catastrophicData = + generateCatastrophicTimeoutIss(random, addressBook, currentRound); + final Hash selfHashForCatastrophicRound = catastrophicData.stream() + .filter(info -> info.nodeId() == selfId) + .findFirst() + .map(RoundHashValidatorTests.NodeHashInfo::nodeStateHash) + .orElseThrow(); + final List signaturesOnCatastrophicRound = generateEventsContainingSignatures( + currentRound, new RoundHashValidatorTests.HashGenerationData(catastrophicData, null)); long submittedWeight = 0; - for (final RoundHashValidatorTests.NodeHashInfo info : data) { - final long weight = addressBook.getAddress(info.nodeId()).getWeight(); + final List signaturesToSubmit = new ArrayList<>(); + for (final EventImpl signatureEvent : signaturesOnCatastrophicRound) { + final long weight = + addressBook.getAddress(signatureEvent.getCreatorId()).getWeight(); - manager.handlePostconsensusSignatures(List.of(new ScopedSystemTransaction<>( - info.nodeId(), - new BasicSoftwareVersion(1), - new StateSignatureTransaction(targetRound, mock(Signature.class), info.nodeStateHash())))); + signaturesToSubmit.add(signatureEvent); // Stop once we have added >2/3. We should not have decided yet, but will // have gathered enough to declare a catastrophic ISS @@ -535,13 +536,36 @@ void catastrophicShiftBeforeCompleteTest() { } } - // Shift the window even though we have not added enough data for a decision. - // But we will have added enough to lead to a catastrophic ISS when the timeout is triggered. - manager.roundCompleted(roundsNonAncient); + // handle the catastrophic round, but it won't be decided yet, since there aren't enough signatures + issDetectorTestHelper.handleStateAndRound(new StateAndRound( + mockState(currentRound, selfHashForCatastrophicRound), + createRoundWithSignatureEvents(currentRound, signaturesToSubmit))); + + // shift through until the catastrophic round is almost ready to be cleaned up + for (currentRound++; currentRound < roundsNonAncient; currentRound++) { + issDetectorTestHelper.handleStateAndRound(new StateAndRound( + mockState(currentRound, randomHash()), createRoundWithSignatureEvents(currentRound, List.of()))); + } + + assertEquals( + 0, + issDetectorTestHelper.getIssNotificationList().size(), + "no ISS should be detected prior to shifting"); - assertEquals(1, manager.getIssCount(), "shifting should have caused an ISS"); + // Shift the window. Even though we have not added enough data for a decision, we will have added enough to lead + // to a catastrophic ISS when the timeout is triggered. + issDetectorTestHelper.handleStateAndRound(new StateAndRound( + mockState(currentRound, randomHash()), createRoundWithSignatureEvents(currentRound, List.of()))); + + assertEquals(1, issDetectorTestHelper.getIssNotificationList().size(), "shifting should have caused an ISS"); + assertEquals( + 1, issDetectorTestHelper.getCatastrophicIssCount(), "shifting should have caused a catastrophic ISS"); } + /** + * Causes a catastrophic ISS, but shifts the window by a large amount past the ISS round. This causes the + * catastrophic ISS to not be registered. + */ @Test @DisplayName("Big Shift Test") void bigShiftTest() { @@ -561,49 +585,66 @@ void bigShiftTest() { .build(); final NodeId selfId = addressBook.getNodeId(0); - final IssDetectorTestHelper manager = - new IssDetectorTestHelper(platformContext, addressBook, DO_NOT_IGNORE_ROUNDS); - - // Start collecting data for rounds. - for (long round = 0; round < roundsNonAncient; round++) { - manager.roundCompleted(round); - } - - final long targetRound = 0; - - // Add data, but not enough to be certain of an ISS. - final List data = - generateCatastrophicTimeoutIss(random, addressBook, targetRound); - - for (final RoundHashValidatorTests.NodeHashInfo info : data) { - if (info.nodeId() == selfId) { - manager.newStateHashed(mockState(0L, info.nodeStateHash())); - } - } + final IssDetector issDetector = new IssDetector( + platformContext, + addressBook, + DEFAULT_EPOCH_HASH, + new BasicSoftwareVersion(1), + false, + DO_NOT_IGNORE_ROUNDS); + final IssDetectorTestHelper issDetectorTestHelper = new IssDetectorTestHelper(issDetector); + + long currentRound = 0; + + // start with an initial state + issDetectorTestHelper.overridingState(mockState(currentRound, randomHash())); + currentRound++; + + final List catastrophicData = + generateCatastrophicTimeoutIss(random, addressBook, currentRound); + final Hash selfHashForCatastrophicRound = catastrophicData.stream() + .filter(info -> info.nodeId() == selfId) + .findFirst() + .map(RoundHashValidatorTests.NodeHashInfo::nodeStateHash) + .orElseThrow(); + final List signaturesOnCatastrophicRound = generateEventsContainingSignatures( + currentRound, new RoundHashValidatorTests.HashGenerationData(catastrophicData, null)); + + // handle the catastrophic round, but don't submit any signatures yet, so it won't be detected + issDetectorTestHelper.handleStateAndRound(new StateAndRound( + mockState(currentRound, selfHashForCatastrophicRound), + createRoundWithSignatureEvents(currentRound, List.of()))); long submittedWeight = 0; - for (final RoundHashValidatorTests.NodeHashInfo info : data) { - final long weight = addressBook.getAddress(info.nodeId()).getWeight(); - - manager.handlePostconsensusSignatures(List.of(new ScopedSystemTransaction<>( - info.nodeId(), - new BasicSoftwareVersion(1), - new StateSignatureTransaction(targetRound, mock(Signature.class), info.nodeStateHash())))); + final List signaturesToSubmit = new ArrayList<>(); + for (final EventImpl signatureEvent : signaturesOnCatastrophicRound) { + final long weight = + addressBook.getAddress(signatureEvent.getCreatorId()).getWeight(); - // Stop once we have added >2/3. We should not have decided yet, but will - // have gathered enough to declare a catastrophic ISS + // Stop once we have added >2/3. We should not have decided yet, but will have gathered enough to declare a + // catastrophic ISS submittedWeight += weight; - if (SUPER_MAJORITY.isSatisfiedBy(submittedWeight, addressBook.getTotalWeight())) { + signaturesToSubmit.add(signatureEvent); + if (SUPER_MAJORITY.isSatisfiedBy(submittedWeight + weight, addressBook.getTotalWeight())) { break; } } + currentRound++; + // submit the supermajority of signatures + issDetectorTestHelper.handleStateAndRound(new StateAndRound( + mockState(currentRound, randomHash()), + createRoundWithSignatureEvents(currentRound, signaturesToSubmit))); + // Shifting the window a great distance should not trigger the ISS. - manager.overridingState(mockState(roundsNonAncient + 100L, randomHash(random))); + issDetectorTestHelper.overridingState(mockState(roundsNonAncient + 100L, randomHash(random))); - assertEquals(0, manager.getIssCount(), "there wasn't enough data submitted to observe the ISS"); + assertEquals(0, issDetectorTestHelper.getSelfIssCount(), "there should be no ISS notifications"); } + /** + * Causes a catastrophic ISS, but specifies that round to be ignored. This should cause the ISS to not be detected. + */ @Test @DisplayName("Ignored Round Test") void ignoredRoundTest() { @@ -617,46 +658,48 @@ void ignoredRoundTest() { final PlatformContext platformContext = TestPlatformContextBuilder.create().build(); + final int roundsNonAncient = platformContext + .getConfiguration() + .getConfigData(ConsensusConfig.class) + .roundsNonAncient(); - final IssDetectorTestHelper manager = new IssDetectorTestHelper(platformContext, addressBook, 1); + final IssDetector issDetector = new IssDetector( + platformContext, addressBook, DEFAULT_EPOCH_HASH, new BasicSoftwareVersion(1), false, 1); + final IssDetectorTestHelper issDetectorTestHelper = new IssDetectorTestHelper(issDetector); - final int rounds = 1_000; - for (long round = 1; round <= rounds; round++) { - final Hash roundHash = randomHash(random); + long currentRound = 0; - if (round == 1) { - manager.overridingState(mockState(round, roundHash)); - } else { - manager.roundCompleted(round); - manager.newStateHashed(mockState(round, roundHash)); - } + issDetectorTestHelper.overridingState(mockState(currentRound, randomHash())); + currentRound++; - for (final Address address : addressBook) { - if (round == 1) { - // Intentionally send bad hashes in the first round. We are configured to ignore this round. - manager.handlePostconsensusSignatures(List.of(new ScopedSystemTransaction<>( - address.getNodeId(), - new BasicSoftwareVersion(1), - new StateSignatureTransaction(round, mock(Signature.class), randomHash(random))))); - } else { - manager.handlePostconsensusSignatures(List.of(new ScopedSystemTransaction<>( - address.getNodeId(), - new BasicSoftwareVersion(1), - new StateSignatureTransaction(round, mock(Signature.class), roundHash)))); - } - } + final List catastrophicData = + generateCatastrophicTimeoutIss(random, addressBook, currentRound); + final List signaturesOnCatastrophicRound = generateEventsContainingSignatures( + currentRound, new RoundHashValidatorTests.HashGenerationData(catastrophicData, null)); + + // handle the round and all signatures. + // The round has a catastrophic ISS, but should be ignored + issDetectorTestHelper.handleStateAndRound(new StateAndRound( + mockState(currentRound, randomHash()), + createRoundWithSignatureEvents(currentRound, signaturesOnCatastrophicRound))); + + // shift through some rounds, to make sure nothing unexpected happens + for (currentRound++; currentRound <= roundsNonAncient; currentRound++) { + issDetectorTestHelper.handleStateAndRound(new StateAndRound( + mockState(currentRound, randomHash()), createRoundWithSignatureEvents(currentRound, List.of()))); } - assertEquals(0, manager.getIssCount(), "ISS should have been ignored"); + + assertEquals(0, issDetectorTestHelper.getIssNotificationList().size(), "ISS should have been ignored"); } private static ReservedSignedState mockState(final long round, final Hash hash) { final ReservedSignedState rs = mock(ReservedSignedState.class); final SignedState ss = mock(SignedState.class); final State s = mock(State.class); - Mockito.when(rs.get()).thenReturn(ss); - Mockito.when(ss.getState()).thenReturn(s); - Mockito.when(ss.getRound()).thenReturn(round); - Mockito.when(s.getHash()).thenReturn(hash); + when(rs.get()).thenReturn(ss); + when(ss.getState()).thenReturn(s); + when(ss.getRound()).thenReturn(round); + when(s.getHash()).thenReturn(hash); return rs; } } diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/RoundHashValidatorTests.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/RoundHashValidatorTests.java index 31df692001b4..5b0dd90970b8 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/RoundHashValidatorTests.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/RoundHashValidatorTests.java @@ -54,8 +54,21 @@ static Stream args() { Arguments.of(HashValidityStatus.CATASTROPHIC_ISS)); } + /** + * Describes a node's hash and the round it was generated in. + * + * @param nodeId the node ID + * @param nodeStateHash the hash the node will report + * @param round the round the hash was generated in + */ record NodeHashInfo(NodeId nodeId, Hash nodeStateHash, long round) {} + /** + * Holds a list of {@link NodeHashInfo} for a given round, and that round's consensus hash. + * + * @param nodeList the node hash info list + * @param consensusHash the consensus hash of the round + */ record HashGenerationData(List nodeList, Hash consensusHash) {} /** @@ -70,7 +83,7 @@ static HashGenerationData generateNodeHashes( final Random random, final AddressBook addressBook, final HashValidityStatus desiredValidityStatus, - long round) { + final long round) { if (desiredValidityStatus == HashValidityStatus.VALID || desiredValidityStatus == HashValidityStatus.SELF_ISS) { return generateRegularNodeHashes(random, addressBook, round); } else if (desiredValidityStatus == HashValidityStatus.CATASTROPHIC_ISS) { @@ -84,7 +97,7 @@ static HashGenerationData generateNodeHashes( * Generate node hashes without there being a catastrophic ISS. */ static HashGenerationData generateRegularNodeHashes( - final Random random, final AddressBook addressBook, long round) { + final Random random, final AddressBook addressBook, final long round) { // Greater than 1/2 must have the same hash. But all other nodes are free to take whatever other hash // they want. Choose that fraction randomly. @@ -177,7 +190,7 @@ static HashGenerationData generateRegularNodeHashes( * Generate node hashes that result in a catastrophic ISS. */ static HashGenerationData generateCatastrophicNodeHashes( - final Random random, final AddressBook addressBook, long round) { + final Random random, final AddressBook addressBook, final long round) { // There should exist no group of nodes with the same hash that >1/2 From c1d3177d733eb3e848ed3617a955ff207e834c2f Mon Sep 17 00:00:00 2001 From: Ivan Malygin Date: Mon, 11 Mar 2024 13:25:01 -0400 Subject: [PATCH 053/115] fix: 12000 Increased timeout for `ConcurrentNodeStatusTrackerTests.setsBoundValue` (#12001) Signed-off-by: Ivan Malygin --- .../virtualmap/internal/ConcurrentNodeStatusTrackerTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform-sdk/swirlds-virtualmap/src/test/java/com/swirlds/virtualmap/internal/ConcurrentNodeStatusTrackerTests.java b/platform-sdk/swirlds-virtualmap/src/test/java/com/swirlds/virtualmap/internal/ConcurrentNodeStatusTrackerTests.java index 1435c7a37629..8e02a6d0f443 100644 --- a/platform-sdk/swirlds-virtualmap/src/test/java/com/swirlds/virtualmap/internal/ConcurrentNodeStatusTrackerTests.java +++ b/platform-sdk/swirlds-virtualmap/src/test/java/com/swirlds/virtualmap/internal/ConcurrentNodeStatusTrackerTests.java @@ -143,7 +143,7 @@ void setsBoundValue() throws InterruptedException, ExecutionException, TimeoutEx final ConcurrentNodeStatusTracker tracker = new ConcurrentNodeStatusTracker(capacity); final ExecutorService executor = Executors.newSingleThreadExecutor(); final Future future = executor.submit(() -> producer(tracker, value, value + 1)); - future.get(500, TimeUnit.MILLISECONDS); + future.get(1, TimeUnit.SECONDS); assertEquals(KNOWN, tracker.getStatus(value), "The capacity - 1 is a valid value"); executor.shutdown(); From 849aaa750ca63e7bdd5151668c528a577372374d Mon Sep 17 00:00:00 2001 From: Austin Littley <102969658+alittley@users.noreply.github.com> Date: Mon, 11 Mar 2024 14:04:18 -0400 Subject: [PATCH 054/115] feat: Extract interface from `LatestCompleteStateNexus` (#11988) Signed-off-by: Austin Littley --- .../com/swirlds/platform/SwirldsPlatform.java | 3 +- .../swirlds/platform/cli/DiagramCommand.java | 4 +- .../DefaultLatestCompleteStateNexus.java | 117 ++++++++++++++++++ .../state/nexus/LatestCompleteStateNexus.java | 97 ++------------- .../state/StateSignatureCollectorTester.java | 4 +- .../state/nexus/SignedStateNexusTest.java | 2 +- 6 files changed, 137 insertions(+), 90 deletions(-) create mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/nexus/DefaultLatestCompleteStateNexus.java 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 e2331db5a5ef..6cbfe12eb8e1 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 @@ -128,6 +128,7 @@ import com.swirlds.platform.state.iss.IssDetector; import com.swirlds.platform.state.iss.IssHandler; import com.swirlds.platform.state.iss.IssScratchpad; +import com.swirlds.platform.state.nexus.DefaultLatestCompleteStateNexus; import com.swirlds.platform.state.nexus.EmergencyStateNexus; import com.swirlds.platform.state.nexus.LatestCompleteStateNexus; import com.swirlds.platform.state.nexus.LockFreeStateNexus; @@ -463,7 +464,7 @@ public class SwirldsPlatform implements Platform { transactionPool = new TransactionPool(platformContext); final LatestCompleteStateNexus latestCompleteState = - new LatestCompleteStateNexus(stateConfig, platformContext.getMetrics()); + new DefaultLatestCompleteStateNexus(stateConfig, platformContext.getMetrics()); platformWiring = thingsToStart.add(new PlatformWiring(platformContext)); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/DiagramCommand.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/DiagramCommand.java index 85fbf758473d..b720273811e8 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/DiagramCommand.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/DiagramCommand.java @@ -35,7 +35,7 @@ import com.swirlds.platform.config.DefaultConfiguration; import com.swirlds.platform.config.StateConfig; import com.swirlds.platform.eventhandling.TransactionPool; -import com.swirlds.platform.state.nexus.LatestCompleteStateNexus; +import com.swirlds.platform.state.nexus.DefaultLatestCompleteStateNexus; import com.swirlds.platform.system.status.PlatformStatusManager; import com.swirlds.platform.wiring.PlatformWiring; import edu.umd.cs.findbugs.annotations.NonNull; @@ -106,7 +106,7 @@ public Integer call() throws IOException { platformWiring.wireExternalComponents( new PlatformStatusManager(platformContext, platformContext.getTime(), threadManager, a -> {}), new TransactionPool(platformContext), - new LatestCompleteStateNexus( + new DefaultLatestCompleteStateNexus( platformContext.getConfiguration().getConfigData(StateConfig.class), platformContext.getMetrics()), notificationEngine); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/nexus/DefaultLatestCompleteStateNexus.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/nexus/DefaultLatestCompleteStateNexus.java new file mode 100644 index 000000000000..0c662576fc41 --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/nexus/DefaultLatestCompleteStateNexus.java @@ -0,0 +1,117 @@ +/* + * 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.swirlds.platform.state.nexus; + +import static com.swirlds.metrics.api.Metrics.PLATFORM_CATEGORY; + +import com.swirlds.common.metrics.RunningAverageMetric; +import com.swirlds.metrics.api.Metrics; +import com.swirlds.platform.config.StateConfig; +import com.swirlds.platform.consensus.ConsensusConstants; +import com.swirlds.platform.state.signed.ReservedSignedState; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Objects; + +/** + * The default implementation of {@link LatestCompleteStateNexus}. + */ +public class DefaultLatestCompleteStateNexus implements LatestCompleteStateNexus { + private static final RunningAverageMetric.Config AVG_ROUND_SUPERMAJORITY_CONFIG = new RunningAverageMetric.Config( + PLATFORM_CATEGORY, "roundSup") + .withDescription("latest round with state signed by a supermajority") + .withUnit("round"); + + private final StateConfig stateConfig; + private ReservedSignedState currentState; + + /** + * Create a new nexus that holds the latest complete signed state. + * + * @param stateConfig the state configuration + * @param metrics the metrics object to update + */ + public DefaultLatestCompleteStateNexus(@NonNull final StateConfig stateConfig, @NonNull final Metrics metrics) { + this.stateConfig = Objects.requireNonNull(stateConfig); + Objects.requireNonNull(metrics); + + final RunningAverageMetric avgRoundSupermajority = metrics.getOrCreate(AVG_ROUND_SUPERMAJORITY_CONFIG); + metrics.addUpdater(() -> avgRoundSupermajority.update(getRound())); + } + + /** + * {@inheritDoc} + */ + @Override + public synchronized void setState(@Nullable final ReservedSignedState reservedSignedState) { + if (currentState != null) { + currentState.close(); + } + currentState = reservedSignedState; + } + + /** + * {@inheritDoc} + */ + @Override + public synchronized void setStateIfNewer(@NonNull final ReservedSignedState reservedSignedState) { + if (reservedSignedState.isNotNull() + && getRound() < reservedSignedState.get().getRound()) { + setState(reservedSignedState); + } else { + reservedSignedState.close(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public synchronized void newIncompleteState(@NonNull final Long newStateRound) { + // Any state older than this is unconditionally removed, even if it is the latest + final long earliestPermittedRound = newStateRound - stateConfig.roundsToKeepForSigning() + 1; + + // Is the latest complete round older than the earliest permitted round? + if (getRound() < earliestPermittedRound) { + // Yes, so remove it + clear(); + } + } + + /** + * {@inheritDoc} + */ + @Nullable + @Override + public synchronized ReservedSignedState getState(@NonNull final String reason) { + if (currentState == null) { + return null; + } + return currentState.tryGetAndReserve(reason); + } + + /** + * {@inheritDoc} + */ + @Override + public synchronized long getRound() { + if (currentState == null) { + return ConsensusConstants.ROUND_UNDEFINED; + } + return currentState.get().getRound(); + } +} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/nexus/LatestCompleteStateNexus.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/nexus/LatestCompleteStateNexus.java index 74174c2ba761..5633d55a7fd3 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/nexus/LatestCompleteStateNexus.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/nexus/LatestCompleteStateNexus.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023-2024 Hedera Hashgraph, LLC + * 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. @@ -16,67 +16,14 @@ package com.swirlds.platform.state.nexus; -import static com.swirlds.metrics.api.Metrics.PLATFORM_CATEGORY; - -import com.swirlds.common.metrics.RunningAverageMetric; -import com.swirlds.metrics.api.Metrics; -import com.swirlds.platform.config.StateConfig; -import com.swirlds.platform.consensus.ConsensusConstants; +import com.swirlds.common.wiring.component.InputWireLabel; import com.swirlds.platform.state.signed.ReservedSignedState; import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.Nullable; -import java.util.Objects; /** * A nexus that holds the latest complete signed state. */ -public class LatestCompleteStateNexus implements SignedStateNexus { - private static final RunningAverageMetric.Config AVG_ROUND_SUPERMAJORITY_CONFIG = new RunningAverageMetric.Config( - PLATFORM_CATEGORY, "roundSup") - .withDescription("latest round with state signed by a supermajority") - .withUnit("round"); - - private final StateConfig stateConfig; - private ReservedSignedState currentState; - - /** - * Create a new nexus that holds the latest complete signed state. - * - * @param stateConfig the state configuration - * @param metrics the metrics object to update - */ - public LatestCompleteStateNexus(@NonNull final StateConfig stateConfig, @NonNull final Metrics metrics) { - this.stateConfig = Objects.requireNonNull(stateConfig); - Objects.requireNonNull(metrics); - - final RunningAverageMetric avgRoundSupermajority = metrics.getOrCreate(AVG_ROUND_SUPERMAJORITY_CONFIG); - metrics.addUpdater(() -> avgRoundSupermajority.update(getRound())); - } - - @Override - public synchronized void setState(@Nullable final ReservedSignedState reservedSignedState) { - if (currentState != null) { - currentState.close(); - } - currentState = reservedSignedState; - } - - /** - * Replace the current state with the given state if the given state is newer than the current state. - * @param reservedSignedState the new state - */ - public synchronized void setStateIfNewer(@Nullable final ReservedSignedState reservedSignedState) { - if (reservedSignedState == null) { - return; - } - if (reservedSignedState.isNotNull() - && getRound() < reservedSignedState.get().getRound()) { - setState(reservedSignedState); - } else { - reservedSignedState.close(); - } - } - +public interface LatestCompleteStateNexus extends SignedStateNexus { /** * Notify the nexus that a new signed state has been created. This is useful for the nexus to know when it should * clear the latest complete state. This is used so that we don't hold the latest complete state forever in case we @@ -84,34 +31,14 @@ && getRound() < reservedSignedState.get().getRound()) { * * @param newStateRound a new signed state round that is not yet complete */ - public synchronized void newIncompleteState(final long newStateRound) { - // NOTE: This logic is duplicated in SignedStateManager, but will be removed from the signed state manager - // once its refactor is done - - // Any state older than this is unconditionally removed, even if it is the latest - final long earliestPermittedRound = newStateRound - stateConfig.roundsToKeepForSigning() + 1; - - // Is the latest complete round older than the earliest permitted round? - if (getRound() < earliestPermittedRound) { - // Yes, so remove it - clear(); - } - } - - @Nullable - @Override - public synchronized ReservedSignedState getState(@NonNull final String reason) { - if (currentState == null) { - return null; - } - return currentState.tryGetAndReserve(reason); - } + @InputWireLabel("incomplete state") + void newIncompleteState(@NonNull final Long newStateRound); - @Override - public synchronized long getRound() { - if (currentState == null) { - return ConsensusConstants.ROUND_UNDEFINED; - } - return currentState.get().getRound(); - } + /** + * Replace the current state with the given state if the given state is newer than the current state. + * + * @param reservedSignedState the new state + */ + @InputWireLabel("complete state") + void setStateIfNewer(@NonNull final ReservedSignedState reservedSignedState); } diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/StateSignatureCollectorTester.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/StateSignatureCollectorTester.java index 7dc500ffc2da..935b666a812d 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/StateSignatureCollectorTester.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/StateSignatureCollectorTester.java @@ -22,6 +22,7 @@ import com.swirlds.platform.components.state.output.StateLacksSignaturesConsumer; import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.config.StateConfig; +import com.swirlds.platform.state.nexus.DefaultLatestCompleteStateNexus; import com.swirlds.platform.state.nexus.LatestCompleteStateNexus; import com.swirlds.platform.state.signed.ReservedSignedState; import com.swirlds.platform.state.signed.SignedStateMetrics; @@ -58,7 +59,8 @@ public static StateSignatureCollectorTester create( @NonNull final SignedStateMetrics signedStateMetrics, @NonNull final StateHasEnoughSignaturesConsumer stateHasEnoughSignaturesConsumer, @NonNull final StateLacksSignaturesConsumer stateLacksSignaturesConsumer) { - final LatestCompleteStateNexus latestSignedState = new LatestCompleteStateNexus(stateConfig, new NoOpMetrics()); + final LatestCompleteStateNexus latestSignedState = + new DefaultLatestCompleteStateNexus(stateConfig, new NoOpMetrics()); return new StateSignatureCollectorTester( stateConfig, signedStateMetrics, diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/nexus/SignedStateNexusTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/nexus/SignedStateNexusTest.java index d29ab3c98a7e..51710c440300 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/nexus/SignedStateNexusTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/nexus/SignedStateNexusTest.java @@ -44,7 +44,7 @@ class SignedStateNexusTest { private static Stream allInstances() { return Stream.concat( raceConditionInstances(), - Stream.of(new LatestCompleteStateNexus( + Stream.of(new DefaultLatestCompleteStateNexus( ConfigurationBuilder.create() .withConfigDataType(StateConfig.class) .build() From d289bde941bdffabd486411e90a29fd74940b1b9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 14:11:37 -0500 Subject: [PATCH 055/115] chore(deps): bump crazy-max/ghaction-import-gpg from 6.0.0 to 6.1.0 (#11175) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/node-zxc-build-release-artifact.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/node-zxc-build-release-artifact.yaml b/.github/workflows/node-zxc-build-release-artifact.yaml index 628570eededf..9c665bdde338 100644 --- a/.github/workflows/node-zxc-build-release-artifact.yaml +++ b/.github/workflows/node-zxc-build-release-artifact.yaml @@ -554,7 +554,7 @@ jobs: - name: Import GPG key id: gpg_key - uses: crazy-max/ghaction-import-gpg@82a020f1f7f605c65dd2449b392a52c3fcfef7ef # v6.0.0 + uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0 if: ${{ inputs.dry-run-enabled != true && !cancelled() && !failure() }} with: gpg_private_key: ${{ secrets.svcs-gpg-key-contents }} @@ -646,7 +646,7 @@ jobs: - name: Import GPG key id: gpg_key - uses: crazy-max/ghaction-import-gpg@82a020f1f7f605c65dd2449b392a52c3fcfef7ef # v6.0.0 + uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0 with: gpg_private_key: ${{ secrets.sdk-gpg-key-contents }} passphrase: ${{ secrets.sdk-gpg-key-passphrase }} From 5a154ac42683e331cfb9cb5a4d9c83c6b815f440 Mon Sep 17 00:00:00 2001 From: Maxi Tartaglia <152629744+mxtartaglia-sl@users.noreply.github.com> Date: Mon, 11 Mar 2024 17:12:54 -0300 Subject: [PATCH 056/115] fix: 12032 temporally disable log tests (#12033) Signed-off-by: mxtartaglia --- .../src/test/java/com/swirlds/logging/LoggerImplTest.java | 2 ++ .../src/test/java/com/swirlds/logging/LoggingSystemTest.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/LoggerImplTest.java b/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/LoggerImplTest.java index 90278b1a3332..eda425f137d6 100644 --- a/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/LoggerImplTest.java +++ b/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/LoggerImplTest.java @@ -28,8 +28,10 @@ import com.swirlds.logging.file.FileHandlerFactory; import com.swirlds.logging.util.DummyConsumer; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +@Disabled public class LoggerImplTest { @Test diff --git a/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/LoggingSystemTest.java b/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/LoggingSystemTest.java index 076e4028e5fa..46596d85993e 100644 --- a/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/LoggingSystemTest.java +++ b/platform-sdk/swirlds-logging/src/test/java/com/swirlds/logging/LoggingSystemTest.java @@ -33,9 +33,11 @@ import java.nio.file.Path; import java.util.List; import java.util.UUID; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @WithTestExecutor +@Disabled public class LoggingSystemTest { private static final String LOG_FILE = "log-files/logging.log"; From 6fe79de12f0b4f17b3f88e7798815867d38f1f42 Mon Sep 17 00:00:00 2001 From: anthony-swirldslabs <152534762+anthony-swirldslabs@users.noreply.github.com> Date: Mon, 11 Mar 2024 13:20:01 -0700 Subject: [PATCH 057/115] fix: remove null checks for non-nullable runningHash() (#12036) Signed-off-by: Anthony Petrov --- .../records/impl/producers/StreamFileProducerConcurrent.java | 5 ++--- .../impl/producers/StreamFileProducerSingleThreaded.java | 4 ++-- .../app/workflows/handle/record/BlockRecordManagerTest.java | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/impl/producers/StreamFileProducerConcurrent.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/impl/producers/StreamFileProducerConcurrent.java index d94cf3e0a869..7b4da4b8ea61 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/impl/producers/StreamFileProducerConcurrent.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/impl/producers/StreamFileProducerConcurrent.java @@ -116,9 +116,8 @@ public void initRunningHash(@NonNull final RunningHashes runningHashes) { throw new IllegalStateException("initRunningHash() can only be called once"); } - if (runningHashes.runningHash() == null - || runningHashes.runningHash().equals(Bytes.EMPTY)) { - throw new IllegalArgumentException("The initial running hash cannot be null or empty"); + if (runningHashes.runningHash().equals(Bytes.EMPTY)) { + throw new IllegalArgumentException("The initial running hash cannot be empty"); } lastRecordHashingResult = completedFuture(runningHashes.runningHash()); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/impl/producers/StreamFileProducerSingleThreaded.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/impl/producers/StreamFileProducerSingleThreaded.java index a860a4245d27..d5ad007a8d9e 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/impl/producers/StreamFileProducerSingleThreaded.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/impl/producers/StreamFileProducerSingleThreaded.java @@ -124,8 +124,8 @@ public void initRunningHash(@NonNull final RunningHashes runningHashes) { throw new IllegalStateException("initRunningHash() must only be called once"); } - if (runningHashes.runningHash() == null || runningHashes.runningHash().equals(Bytes.EMPTY)) { - throw new IllegalArgumentException("The initial running hash cannot be null or empty"); + if (runningHashes.runningHash().equals(Bytes.EMPTY)) { + throw new IllegalArgumentException("The initial running hash cannot be empty"); } runningHash = runningHashes.runningHash(); diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/BlockRecordManagerTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/BlockRecordManagerTest.java index db6ba779602a..636a1a4d6ef1 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/BlockRecordManagerTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/BlockRecordManagerTest.java @@ -299,7 +299,7 @@ void testBlockInfoMethods() throws Exception { .getNMinus3RunningHash() .toHex()); } else { - // check nulls as well + // check empty as well assertThat(blockRecordManager.getNMinus3RunningHash()) .isEqualTo(Bytes.EMPTY); } From fbcf79677a39447e51d4767bedcdc3328a08e0e6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 18:25:15 -0500 Subject: [PATCH 058/115] chore(deps): bump peter-evans/repository-dispatch from 2.1.2 to 3.0.0 (#11176) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/node-zxcron-release-branching.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/node-zxcron-release-branching.yaml b/.github/workflows/node-zxcron-release-branching.yaml index e4be60013b19..89a5d7f6e6c2 100644 --- a/.github/workflows/node-zxcron-release-branching.yaml +++ b/.github/workflows/node-zxcron-release-branching.yaml @@ -212,7 +212,7 @@ jobs: printf "## Dispatch Payload\n\`\`\`json\n%s\n\`\`\`\n" "$(jq '.' <<<"${REQ_JSON}")" >>"${GITHUB_STEP_SUMMARY}" - name: Repository Dispatch - uses: peter-evans/repository-dispatch@bf47d102fdb849e755b0b0023ea3e81a44b6f570 # v2.1.2 + uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0 with: token: ${{ secrets.GH_ACCESS_TOKEN }} repository: hashgraph/hedera-internal-workflows From cec69da6183c983e49561c3aadd14e35429cee1a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 20:52:19 -0500 Subject: [PATCH 059/115] chore(deps): bump aslafy-z/conventional-pr-title-action from 3.1.1 to 3.2.0 (#11177) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/flow-pull-request-formatting.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flow-pull-request-formatting.yaml b/.github/workflows/flow-pull-request-formatting.yaml index 43f4516341f0..08e418a3413d 100644 --- a/.github/workflows/flow-pull-request-formatting.yaml +++ b/.github/workflows/flow-pull-request-formatting.yaml @@ -38,6 +38,6 @@ jobs: runs-on: [self-hosted, Linux, medium, ephemeral] steps: - name: Check PR Title - uses: aslafy-z/conventional-pr-title-action@2ce59b07f86bd51b521dd088f0acfb0d7fdac55e # v3.1.1 + uses: aslafy-z/conventional-pr-title-action@a0b851005a0f82ac983a56ead5a8111c0d8e044a # v3.2.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 9f6b68f6465f83c7cd5559269b4dc201589fa9c1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 21:12:48 -0500 Subject: [PATCH 060/115] chore(deps): bump actions/upload-artifact from 4.3.0 to 4.3.1 (#11375) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../node-zxc-compile-application-code.yaml | 20 +++++++++---------- .../zxc-verify-docker-build-determinism.yaml | 2 +- .../zxc-verify-gradle-build-determinism.yaml | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/node-zxc-compile-application-code.yaml b/.github/workflows/node-zxc-compile-application-code.yaml index 4a27bab84919..09d51ea0b315 100644 --- a/.github/workflows/node-zxc-compile-application-code.yaml +++ b/.github/workflows/node-zxc-compile-application-code.yaml @@ -260,7 +260,7 @@ jobs: junit_files: "**/build/test-results/itest/TEST-*.xml" - name: Publish Integration Test Network Logs - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 if: ${{ inputs.enable-integration-tests && inputs.enable-network-log-capture && !cancelled() }} with: name: Integration Test Network Logs @@ -285,7 +285,7 @@ jobs: junit_files: "**/build/test-results/hapiTestMisc/TEST-*.xml" - name: Publish HAPI Test (Misc) Network Logs - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 if: ${{ inputs.enable-hapi-tests-misc && inputs.enable-network-log-capture && !cancelled() }} with: name: HAPI Test (Misc) Network Logs @@ -310,7 +310,7 @@ jobs: junit_files: "**/build/test-results/hapiTestCrypto/TEST-*.xml" - name: Publish HAPI Test (crypto) Network Logs - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 if: ${{ inputs.enable-hapi-tests-crypto && inputs.enable-network-log-capture && !cancelled() }} with: name: HAPI Test (Crypto) Network Logs @@ -335,7 +335,7 @@ jobs: junit_files: "**/build/test-results/hapiTestToken/TEST-*.xml" - name: Publish HAPI Test (Token) Network Logs - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 if: ${{ inputs.enable-hapi-tests-token && inputs.enable-network-log-capture && !cancelled() }} with: name: HAPI Test (Token) Network Logs @@ -360,7 +360,7 @@ jobs: junit_files: "**/build/test-results/hapiTestSmartContract/TEST-*.xml" - name: Publish HAPI Test (Smart Contract) Network Logs - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 if: ${{ inputs.enable-hapi-tests-smart-contract && inputs.enable-network-log-capture && !cancelled() }} with: name: HAPI Test (Smart Contract) Network Logs @@ -385,7 +385,7 @@ jobs: junit_files: "**/build/test-results/hapiTestTimeConsuming/TEST-*.xml" - name: Publish HAPI Test (Time Consuming) Network Logs - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 if: ${{ inputs.enable-hapi-tests-time-consuming && inputs.enable-network-log-capture && !cancelled() }} with: name: HAPI Test (Time Consuming) Network Logs @@ -410,7 +410,7 @@ jobs: junit_files: "**/build/test-results/hapiTestRestart/TEST-*.xml" - name: Publish HAPI Test (Restart) Network Logs - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 if: ${{ inputs.enable-hapi-tests-restart && inputs.enable-network-log-capture && !cancelled() }} with: name: HAPI Test (Restart) Network Logs @@ -436,7 +436,7 @@ jobs: junit_files: "**/build/test-results/hapiTestNDReconnect/TEST-*.xml" - name: Publish HAPI Test (Node Death Reconnect) Network Logs - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 if: ${{ inputs.enable-hapi-tests-nd-reconnect && inputs.enable-network-log-capture && !cancelled() }} with: name: HAPI Test (Node Death Reconnect) Network Logs @@ -459,7 +459,7 @@ jobs: junit_files: "**/build/test-results/eet/TEST-*.xml" - name: Publish E2E Test Network Logs - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 if: ${{ inputs.enable-e2e-tests && inputs.enable-network-log-capture && !cancelled() }} with: name: E2E Test Network Logs @@ -477,7 +477,7 @@ jobs: run: bash <(curl -Ls https://coverage.codacy.com/get.sh) report -l Java $(find . -name 'jacoco*.xml' -printf '-r %p ') - name: Publish Test Reports - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 if: ${{ inputs.enable-unit-tests && !cancelled() }} with: name: Test Reports diff --git a/.github/workflows/zxc-verify-docker-build-determinism.yaml b/.github/workflows/zxc-verify-docker-build-determinism.yaml index 0902b9074d91..dd92bee1c9f6 100644 --- a/.github/workflows/zxc-verify-docker-build-determinism.yaml +++ b/.github/workflows/zxc-verify-docker-build-determinism.yaml @@ -466,7 +466,7 @@ jobs: fi - name: Publish Manifests - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 if: ${{ steps.regen-manifest.conclusion == 'success' && failure() && !cancelled() }} with: name: Docker Manifests [${{ join(matrix.os, ', ') }}] diff --git a/.github/workflows/zxc-verify-gradle-build-determinism.yaml b/.github/workflows/zxc-verify-gradle-build-determinism.yaml index b320ac8bce6e..22a698b9d780 100644 --- a/.github/workflows/zxc-verify-gradle-build-determinism.yaml +++ b/.github/workflows/zxc-verify-gradle-build-determinism.yaml @@ -236,7 +236,7 @@ jobs: fi - name: Publish Manifests - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 if: ${{ steps.regen-manifest.conclusion == 'success' && failure() && !cancelled() }} with: name: Gradle Manifests [${{ join(matrix.os, ', ') }}] From 40546345ef7aa59ed48d016a89356b088ba9f8c0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 21:12:56 -0500 Subject: [PATCH 061/115] chore(deps): bump actions/setup-node from 4.0.1 to 4.0.2 (#11437) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/node-zxc-compile-application-code.yaml | 2 +- .github/workflows/node-zxf-snyk-monitor.yaml | 2 +- .github/workflows/zxc-jrs-regression.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/node-zxc-compile-application-code.yaml b/.github/workflows/node-zxc-compile-application-code.yaml index 09d51ea0b315..a26ee07b3c94 100644 --- a/.github/workflows/node-zxc-compile-application-code.yaml +++ b/.github/workflows/node-zxc-compile-application-code.yaml @@ -168,7 +168,7 @@ jobs: cache-read-only: false - name: Setup NodeJS - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: ${{ inputs.node-version }} diff --git a/.github/workflows/node-zxf-snyk-monitor.yaml b/.github/workflows/node-zxf-snyk-monitor.yaml index 22eda6ada9c8..948c564a4e7a 100644 --- a/.github/workflows/node-zxf-snyk-monitor.yaml +++ b/.github/workflows/node-zxf-snyk-monitor.yaml @@ -56,7 +56,7 @@ jobs: run: sed -i 's/^org.gradle.configuration-cache=.*$/org.gradle.configuration-cache=false/' gradle.properties - name: Setup NodeJS - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: 16 diff --git a/.github/workflows/zxc-jrs-regression.yaml b/.github/workflows/zxc-jrs-regression.yaml index ae4205e34ac4..64a923eb8785 100644 --- a/.github/workflows/zxc-jrs-regression.yaml +++ b/.github/workflows/zxc-jrs-regression.yaml @@ -323,7 +323,7 @@ jobs: http://localhost:9427/metrics - name: Setup NodeJS Environment - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: 18 From 3a8feda7e3557d059bb0854bd89be52c25c65c6d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 21:13:06 -0500 Subject: [PATCH 062/115] build(deps): bump gradle/gradle-build-action from 2.12.0 to 3.1.0 (#11515) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../node-zxc-build-release-artifact.yaml | 32 +++++++++---------- .../node-zxc-compile-application-code.yaml | 2 +- .github/workflows/node-zxf-snyk-monitor.yaml | 4 +-- .github/workflows/zxc-jrs-regression.yaml | 6 ++-- .../zxc-verify-docker-build-determinism.yaml | 2 +- .../zxc-verify-gradle-build-determinism.yaml | 4 +-- 6 files changed, 25 insertions(+), 25 deletions(-) diff --git a/.github/workflows/node-zxc-build-release-artifact.yaml b/.github/workflows/node-zxc-build-release-artifact.yaml index 9c665bdde338..ff5061ca89ad 100644 --- a/.github/workflows/node-zxc-build-release-artifact.yaml +++ b/.github/workflows/node-zxc-build-release-artifact.yaml @@ -170,19 +170,19 @@ jobs: java-version: ${{ inputs.java-version }} - name: Setup Gradle - uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + uses: gradle/gradle-build-action@29c0906b64b8fc82467890bfb7a0a7ef34bda89e # v3.1.0 with: gradle-version: ${{ inputs.gradle-version }} - name: Gradle Update Version (As Specified) - uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + uses: gradle/gradle-build-action@29c0906b64b8fc82467890bfb7a0a7ef34bda89e # v3.1.0 if: ${{ inputs.version-policy == 'specified' && !cancelled() && !failure() }} with: gradle-version: ${{ inputs.gradle-version }} arguments: versionAsSpecified -PnewVersion=${{ inputs.new-version }} --scan - name: Gradle Update Version (Branch Commit) - uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + uses: gradle/gradle-build-action@29c0906b64b8fc82467890bfb7a0a7ef34bda89e # v3.1.0 if: ${{ inputs.version-policy != 'specified' && !cancelled() && !failure() }} with: gradle-version: ${{ inputs.gradle-version }} @@ -276,7 +276,7 @@ jobs: java-version: ${{ inputs.java-version }} - name: Setup Gradle - uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + uses: gradle/gradle-build-action@29c0906b64b8fc82467890bfb7a0a7ef34bda89e # v3.1.0 with: gradle-version: ${{ inputs.gradle-version }} @@ -295,13 +295,13 @@ jobs: - name: Gradle Assemble id: gradle-build - uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + uses: gradle/gradle-build-action@29c0906b64b8fc82467890bfb7a0a7ef34bda89e # v3.1.0 with: gradle-version: ${{ inputs.gradle-version }} arguments: assemble --scan - name: Gradle Version Summary - uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + uses: gradle/gradle-build-action@29c0906b64b8fc82467890bfb7a0a7ef34bda89e # v3.1.0 with: gradle-version: ${{ inputs.gradle-version }} arguments: githubVersionSummary --scan @@ -571,7 +571,7 @@ jobs: java-version: ${{ inputs.java-version }} - name: Setup Gradle - uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + uses: gradle/gradle-build-action@29c0906b64b8fc82467890bfb7a0a7ef34bda89e # v3.1.0 with: gradle-version: ${{ inputs.gradle-version }} @@ -583,7 +583,7 @@ jobs: key: node-build-version-${{ needs.validate.outputs.version }}-${{ github.sha }} - name: Gradle Update Version (Snapshot) - uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + uses: gradle/gradle-build-action@29c0906b64b8fc82467890bfb7a0a7ef34bda89e # v3.1.0 if: ${{ inputs.version-policy != 'specified' && !cancelled() && !failure() }} with: gradle-version: ${{ inputs.gradle-version }} @@ -591,19 +591,19 @@ jobs: - name: Gradle Assemble id: gradle-build - uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + uses: gradle/gradle-build-action@29c0906b64b8fc82467890bfb7a0a7ef34bda89e # v3.1.0 with: gradle-version: ${{ inputs.gradle-version }} arguments: assemble --scan - name: Gradle Version Summary - uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + uses: gradle/gradle-build-action@29c0906b64b8fc82467890bfb7a0a7ef34bda89e # v3.1.0 with: gradle-version: ${{ inputs.gradle-version }} arguments: githubVersionSummary --scan - name: Gradle Maven Central Release - uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + uses: gradle/gradle-build-action@29c0906b64b8fc82467890bfb7a0a7ef34bda89e # v3.1.0 if: ${{ inputs.dry-run-enabled != true && inputs.version-policy == 'specified' && !cancelled() && !failure() }} env: OSSRH_USERNAME: ${{ secrets.svcs-ossrh-username }} @@ -613,7 +613,7 @@ jobs: arguments: "releaseEvmMavenCentral --scan -PpublishSigningEnabled=true --no-configuration-cache --no-parallel" - name: Gradle Maven Central Snapshot - uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + uses: gradle/gradle-build-action@29c0906b64b8fc82467890bfb7a0a7ef34bda89e # v3.1.0 if: ${{ inputs.dry-run-enabled != true && inputs.version-policy != 'specified' && !cancelled() && !failure() }} env: OSSRH_USERNAME: ${{ secrets.svcs-ossrh-username }} @@ -673,7 +673,7 @@ jobs: java-version: ${{ inputs.java-version }} - name: Setup Gradle - uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + uses: gradle/gradle-build-action@29c0906b64b8fc82467890bfb7a0a7ef34bda89e # v3.1.0 with: gradle-version: ${{ inputs.gradle-version }} @@ -685,13 +685,13 @@ jobs: key: node-build-version-${{ needs.validate.outputs.version }}-${{ github.sha }} - name: Gradle Assemble - uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + uses: gradle/gradle-build-action@29c0906b64b8fc82467890bfb7a0a7ef34bda89e # v3.1.0 with: gradle-version: ${{ inputs.gradle-version }} arguments: assemble --scan - name: Gradle Version Summary - uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + uses: gradle/gradle-build-action@29c0906b64b8fc82467890bfb7a0a7ef34bda89e # v3.1.0 with: gradle-version: ${{ inputs.gradle-version }} arguments: githubVersionSummary --scan @@ -769,7 +769,7 @@ jobs: echo "::endgroup::" - name: Gradle Publish to ${{ inputs.version-policy == 'specified' && 'Maven Central' || 'Google Artifact Registry' }} (${{ inputs.sdk-release-profile }}) - uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + uses: gradle/gradle-build-action@29c0906b64b8fc82467890bfb7a0a7ef34bda89e # v3.1.0 if: ${{ inputs.dry-run-enabled != true && inputs.sdk-release-profile != 'none' && !cancelled() && !failure() }} env: OSSRH_USERNAME: ${{ secrets.sdk-ossrh-username }} diff --git a/.github/workflows/node-zxc-compile-application-code.yaml b/.github/workflows/node-zxc-compile-application-code.yaml index a26ee07b3c94..9fdb96982298 100644 --- a/.github/workflows/node-zxc-compile-application-code.yaml +++ b/.github/workflows/node-zxc-compile-application-code.yaml @@ -163,7 +163,7 @@ jobs: java-version: ${{ inputs.java-version }} - name: Setup Gradle - uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + uses: gradle/gradle-build-action@29c0906b64b8fc82467890bfb7a0a7ef34bda89e # v3.1.0 with: cache-read-only: false diff --git a/.github/workflows/node-zxf-snyk-monitor.yaml b/.github/workflows/node-zxf-snyk-monitor.yaml index 948c564a4e7a..a32bb27de10c 100644 --- a/.github/workflows/node-zxf-snyk-monitor.yaml +++ b/.github/workflows/node-zxf-snyk-monitor.yaml @@ -41,13 +41,13 @@ jobs: java-version: 21.0.1 - name: Setup Gradle - uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + uses: gradle/gradle-build-action@29c0906b64b8fc82467890bfb7a0a7ef34bda89e # v3.1.0 with: build-root-directory: hedera-node gradle-version: wrapper - name: Compile - uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + uses: gradle/gradle-build-action@29c0906b64b8fc82467890bfb7a0a7ef34bda89e # v3.1.0 with: gradle-version: wrapper arguments: assemble --scan diff --git a/.github/workflows/zxc-jrs-regression.yaml b/.github/workflows/zxc-jrs-regression.yaml index 64a923eb8785..a3111bf43ed7 100644 --- a/.github/workflows/zxc-jrs-regression.yaml +++ b/.github/workflows/zxc-jrs-regression.yaml @@ -334,7 +334,7 @@ jobs: java-version: ${{ inputs.java-version }} - name: Setup Gradle - uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + uses: gradle/gradle-build-action@29c0906b64b8fc82467890bfb7a0a7ef34bda89e # v3.1.0 with: gradle-version: ${{ inputs.gradle-version }} gradle-home-cache-strict-match: false @@ -405,14 +405,14 @@ jobs: - name: Gradle Assemble id: gradle-build - uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + uses: gradle/gradle-build-action@29c0906b64b8fc82467890bfb7a0a7ef34bda89e # v3.1.0 with: gradle-version: ${{ inputs.gradle-version }} arguments: assemble --scan - name: Regression Gradle Assemble id: regression-gradle-build - uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + uses: gradle/gradle-build-action@29c0906b64b8fc82467890bfb7a0a7ef34bda89e # v3.1.0 with: build-root-directory: platform-sdk/regression gradle-version: ${{ inputs.gradle-version }} diff --git a/.github/workflows/zxc-verify-docker-build-determinism.yaml b/.github/workflows/zxc-verify-docker-build-determinism.yaml index dd92bee1c9f6..fe59291f7a87 100644 --- a/.github/workflows/zxc-verify-docker-build-determinism.yaml +++ b/.github/workflows/zxc-verify-docker-build-determinism.yaml @@ -119,7 +119,7 @@ jobs: java-version: ${{ inputs.java-version }} - name: Setup Gradle - uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + uses: gradle/gradle-build-action@29c0906b64b8fc82467890bfb7a0a7ef34bda89e # v3.1.0 if: ${{ steps.baseline.outputs.exists == 'false' && !failure() && !cancelled() }} with: cache-disabled: true diff --git a/.github/workflows/zxc-verify-gradle-build-determinism.yaml b/.github/workflows/zxc-verify-gradle-build-determinism.yaml index 22a698b9d780..1f124ca1e421 100644 --- a/.github/workflows/zxc-verify-gradle-build-determinism.yaml +++ b/.github/workflows/zxc-verify-gradle-build-determinism.yaml @@ -78,7 +78,7 @@ jobs: java-version: ${{ inputs.java-version }} - name: Setup Gradle - uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + uses: gradle/gradle-build-action@29c0906b64b8fc82467890bfb7a0a7ef34bda89e # v3.1.0 with: cache-disabled: true @@ -170,7 +170,7 @@ jobs: java-version: ${{ inputs.java-version }} - name: Setup Gradle - uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + uses: gradle/gradle-build-action@29c0906b64b8fc82467890bfb7a0a7ef34bda89e # v3.1.0 with: cache-disabled: true From 3de98181087e87d058f5ecca8c34133ada2e75ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 21:13:21 -0500 Subject: [PATCH 063/115] chore(deps): bump slackapi/slack-github-action from 1.24.0 to 1.25.0 (#11210) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/node-zxc-build-release-artifact.yaml | 2 +- .github/workflows/node-zxcron-release-branching.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/node-zxc-build-release-artifact.yaml b/.github/workflows/node-zxc-build-release-artifact.yaml index ff5061ca89ad..d9a0c4508c12 100644 --- a/.github/workflows/node-zxc-build-release-artifact.yaml +++ b/.github/workflows/node-zxc-build-release-artifact.yaml @@ -814,7 +814,7 @@ jobs: echo "artifact-registry=${ARTIFACT_REGISTRY}" >>"${GITHUB_OUTPUT}" - name: Send Slack Notification (Maven Central) - uses: slackapi/slack-github-action@e28cf165c92ffef168d23c5c9000cffc8a25e117 # v1.24.0 + uses: slackapi/slack-github-action@6c661ce58804a1a20f6dc5fbee7f0381b469e001 # v1.25.0 env: SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK SLACK_WEBHOOK_URL: ${{ secrets.slack-webhook-url }} diff --git a/.github/workflows/node-zxcron-release-branching.yaml b/.github/workflows/node-zxcron-release-branching.yaml index 89a5d7f6e6c2..7af9f9088179 100644 --- a/.github/workflows/node-zxcron-release-branching.yaml +++ b/.github/workflows/node-zxcron-release-branching.yaml @@ -121,7 +121,7 @@ jobs: run: echo "short-id=$(echo -n "${{ github.sha }}" | tr -d '[:space:]' | cut -c1-8)" >> "${GITHUB_OUTPUT}" - name: Send Slack Notification - uses: slackapi/slack-github-action@e28cf165c92ffef168d23c5c9000cffc8a25e117 # v1.24.0 + uses: slackapi/slack-github-action@6c661ce58804a1a20f6dc5fbee7f0381b469e001 # v1.25.0 env: SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK SLACK_WEBHOOK_URL: ${{ secrets.SLACK_RELEASE_WEBHOOK }} From 8d204a3b25807713f80a7a7c14c07957b17deb73 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 21:36:55 -0500 Subject: [PATCH 064/115] build(deps): bump google-github-actions/auth from 2.1.0 to 2.1.2 (#11757) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/flow-node-performance-tests.yaml | 2 +- .github/workflows/node-zxc-build-release-artifact.yaml | 6 +++--- .github/workflows/node-zxc-deploy-preview.yaml | 2 +- .github/workflows/zxc-jrs-regression.yaml | 2 +- .github/workflows/zxc-verify-docker-build-determinism.yaml | 4 ++-- .github/workflows/zxc-verify-gradle-build-determinism.yaml | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/flow-node-performance-tests.yaml b/.github/workflows/flow-node-performance-tests.yaml index a3c633afd6f9..50bfb0491d2e 100644 --- a/.github/workflows/flow-node-performance-tests.yaml +++ b/.github/workflows/flow-node-performance-tests.yaml @@ -51,7 +51,7 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Authenticate to Google Cloud - uses: google-github-actions/auth@5a50e581162a13f4baa8916d01180d2acbc04363 # v2.1.0 + uses: google-github-actions/auth@55bd3a7c6e2ae7cf1877fd1ccb9d54c0503c457c # v2.1.2 with: workload_identity_provider: "projects/235822363393/locations/global/workloadIdentityPools/hedera-builds-pool/providers/hedera-builds-gh-actions" service_account: "hedera-artifact-builds@devops-1-254919.iam.gserviceaccount.com" diff --git a/.github/workflows/node-zxc-build-release-artifact.yaml b/.github/workflows/node-zxc-build-release-artifact.yaml index d9a0c4508c12..bed43fc20886 100644 --- a/.github/workflows/node-zxc-build-release-artifact.yaml +++ b/.github/workflows/node-zxc-build-release-artifact.yaml @@ -259,7 +259,7 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Authenticate to Google Cloud - uses: google-github-actions/auth@5a50e581162a13f4baa8916d01180d2acbc04363 # v2.1.0 + uses: google-github-actions/auth@55bd3a7c6e2ae7cf1877fd1ccb9d54c0503c457c # v2.1.2 if: ${{ inputs.dry-run-enabled != true && !cancelled() && !failure() }} with: workload_identity_provider: "projects/235822363393/locations/global/workloadIdentityPools/hedera-builds-pool/providers/hedera-builds-gh-actions" @@ -414,7 +414,7 @@ jobs: - name: Authenticate to Google Cloud id: google-auth - uses: google-github-actions/auth@5a50e581162a13f4baa8916d01180d2acbc04363 # v2.1.0 + uses: google-github-actions/auth@55bd3a7c6e2ae7cf1877fd1ccb9d54c0503c457c # v2.1.2 if: ${{ inputs.dry-run-enabled != true && !cancelled() && !failure() }} with: token_format: 'access_token' @@ -657,7 +657,7 @@ jobs: - name: Authenticate to Google Cloud id: google-auth - uses: google-github-actions/auth@5a50e581162a13f4baa8916d01180d2acbc04363 # v2.1.0 + uses: google-github-actions/auth@55bd3a7c6e2ae7cf1877fd1ccb9d54c0503c457c # v2.1.2 with: workload_identity_provider: "projects/229164983194/locations/global/workloadIdentityPools/registry-identity-pool/providers/gh-provider" service_account: "artifact-deployer@swirlds-registry.iam.gserviceaccount.com" diff --git a/.github/workflows/node-zxc-deploy-preview.yaml b/.github/workflows/node-zxc-deploy-preview.yaml index 02b46909c061..83beb136a952 100644 --- a/.github/workflows/node-zxc-deploy-preview.yaml +++ b/.github/workflows/node-zxc-deploy-preview.yaml @@ -112,7 +112,7 @@ jobs: fi - name: Authenticate to Google Cloud - uses: google-github-actions/auth@5a50e581162a13f4baa8916d01180d2acbc04363 # v2.1.0 + uses: google-github-actions/auth@55bd3a7c6e2ae7cf1877fd1ccb9d54c0503c457c # v2.1.2 if: ${{ inputs.dry-run-enabled != true && !cancelled() && !failure() }} with: workload_identity_provider: "projects/235822363393/locations/global/workloadIdentityPools/hedera-builds-pool/providers/hedera-builds-gh-actions" diff --git a/.github/workflows/zxc-jrs-regression.yaml b/.github/workflows/zxc-jrs-regression.yaml index a3111bf43ed7..e1dab26c6d68 100644 --- a/.github/workflows/zxc-jrs-regression.yaml +++ b/.github/workflows/zxc-jrs-regression.yaml @@ -377,7 +377,7 @@ jobs: - name: Authenticate to Google Cloud id: google-auth - uses: google-github-actions/auth@5a50e581162a13f4baa8916d01180d2acbc04363 # v2.1.0 + uses: google-github-actions/auth@55bd3a7c6e2ae7cf1877fd1ccb9d54c0503c457c # v2.1.2 with: workload_identity_provider: 'projects/785813846068/locations/global/workloadIdentityPools/jrs-identity-pool/providers/gh-provider' service_account: 'swirlds-automation@swirlds-regression.iam.gserviceaccount.com' diff --git a/.github/workflows/zxc-verify-docker-build-determinism.yaml b/.github/workflows/zxc-verify-docker-build-determinism.yaml index fe59291f7a87..9e1bd1777d49 100644 --- a/.github/workflows/zxc-verify-docker-build-determinism.yaml +++ b/.github/workflows/zxc-verify-docker-build-determinism.yaml @@ -79,7 +79,7 @@ jobs: - name: Authenticate to Google Cloud id: google-auth - uses: google-github-actions/auth@5a50e581162a13f4baa8916d01180d2acbc04363 # v2.1.0 + uses: google-github-actions/auth@55bd3a7c6e2ae7cf1877fd1ccb9d54c0503c457c # v2.1.2 with: workload_identity_provider: "projects/235822363393/locations/global/workloadIdentityPools/hedera-builds-pool/providers/hedera-builds-gh-actions" service_account: "swirlds-automation@hedera-registry.iam.gserviceaccount.com" @@ -316,7 +316,7 @@ jobs: - name: Authenticate to Google Cloud id: google-auth - uses: google-github-actions/auth@5a50e581162a13f4baa8916d01180d2acbc04363 # v2.1.0 + uses: google-github-actions/auth@55bd3a7c6e2ae7cf1877fd1ccb9d54c0503c457c # v2.1.2 with: workload_identity_provider: "projects/235822363393/locations/global/workloadIdentityPools/hedera-builds-pool/providers/hedera-builds-gh-actions" service_account: "swirlds-automation@hedera-registry.iam.gserviceaccount.com" diff --git a/.github/workflows/zxc-verify-gradle-build-determinism.yaml b/.github/workflows/zxc-verify-gradle-build-determinism.yaml index 1f124ca1e421..f2c96d7fe96f 100644 --- a/.github/workflows/zxc-verify-gradle-build-determinism.yaml +++ b/.github/workflows/zxc-verify-gradle-build-determinism.yaml @@ -84,7 +84,7 @@ jobs: - name: Authenticate to Google Cloud id: google-auth - uses: google-github-actions/auth@5a50e581162a13f4baa8916d01180d2acbc04363 # v2.1.0 + uses: google-github-actions/auth@55bd3a7c6e2ae7cf1877fd1ccb9d54c0503c457c # v2.1.2 with: workload_identity_provider: "projects/235822363393/locations/global/workloadIdentityPools/hedera-builds-pool/providers/hedera-builds-gh-actions" service_account: "swirlds-automation@hedera-registry.iam.gserviceaccount.com" @@ -180,7 +180,7 @@ jobs: - name: Authenticate to Google Cloud id: google-auth - uses: google-github-actions/auth@5a50e581162a13f4baa8916d01180d2acbc04363 # v2.1.0 + uses: google-github-actions/auth@55bd3a7c6e2ae7cf1877fd1ccb9d54c0503c457c # v2.1.2 with: workload_identity_provider: "projects/235822363393/locations/global/workloadIdentityPools/hedera-builds-pool/providers/hedera-builds-gh-actions" service_account: "swirlds-automation@hedera-registry.iam.gserviceaccount.com" From 85b4ffa94225910a642c0ad154d72a6ea003a21b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 21:37:05 -0500 Subject: [PATCH 065/115] build(deps): bump codecov/codecov-action from 3.1.4 to 4.1.0 (#11758) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/node-zxc-compile-application-code.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/node-zxc-compile-application-code.yaml b/.github/workflows/node-zxc-compile-application-code.yaml index 9fdb96982298..7c6dae40f21b 100644 --- a/.github/workflows/node-zxc-compile-application-code.yaml +++ b/.github/workflows/node-zxc-compile-application-code.yaml @@ -468,7 +468,7 @@ jobs: - name: Publish To Codecov if: ${{ inputs.enable-unit-tests && !cancelled() }} - uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4 + uses: codecov/codecov-action@54bcd8715eee62d40e33596ef5e8f0f48dbbccab # v4.1.0 - name: Publish to Codacy env: From 700d382f0a52aef95772fd0b905de0d5d1559aef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 21:37:15 -0500 Subject: [PATCH 066/115] build(deps): bump actions/cache from 3.3.2 to 4.0.1 (#11828) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../workflows/node-zxc-build-release-artifact.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/node-zxc-build-release-artifact.yaml b/.github/workflows/node-zxc-build-release-artifact.yaml index bed43fc20886..3fab69b4b1b2 100644 --- a/.github/workflows/node-zxc-build-release-artifact.yaml +++ b/.github/workflows/node-zxc-build-release-artifact.yaml @@ -204,7 +204,7 @@ jobs: echo "prerelease=${PRERELEASE}" >>"${GITHUB_OUTPUT}" - name: Cache Build Version - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 + uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 with: path: version.txt key: node-build-version-${{ steps.effective-version.outputs.number }}-${{ github.sha }} @@ -281,14 +281,14 @@ jobs: gradle-version: ${{ inputs.gradle-version }} - name: Restore Build Version - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 + uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 with: fail-on-cache-miss: true path: version.txt key: node-build-version-${{ needs.validate.outputs.version }}-${{ github.sha }} - name: Cache Build Artifacts - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 + uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 with: path: ~/artifact-build key: node-build-artifacts-${{ needs.validate.outputs.version }}-${{ github.sha }} @@ -458,7 +458,7 @@ jobs: password: ${{ steps.google-auth.outputs.access_token }} - name: Restore Build Artifacts - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 + uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 with: fail-on-cache-miss: true path: ~/artifact-build @@ -576,7 +576,7 @@ jobs: gradle-version: ${{ inputs.gradle-version }} - name: Restore Build Version - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 + uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 with: fail-on-cache-miss: true path: version.txt @@ -678,7 +678,7 @@ jobs: gradle-version: ${{ inputs.gradle-version }} - name: Restore Build Version - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 + uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 with: fail-on-cache-miss: true path: version.txt From 420637899b1d698a2f5991d011fcc0ace91934d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 21:37:24 -0500 Subject: [PATCH 067/115] chore(deps): bump mikefarah/yq from 4.40.5 to 4.42.1 (#12043) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/node-zxcron-release-branching.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/node-zxcron-release-branching.yaml b/.github/workflows/node-zxcron-release-branching.yaml index 7af9f9088179..484eacad416e 100644 --- a/.github/workflows/node-zxcron-release-branching.yaml +++ b/.github/workflows/node-zxcron-release-branching.yaml @@ -45,7 +45,7 @@ jobs: - name: Read Trigger Time id: time - uses: mikefarah/yq@dd648994340a5d03225d97abf19c9bf1086c3f07 # v4.40.5 + uses: mikefarah/yq@9adde1ac14bb283b8955d2b0d567bcaf3c69e639 # v4.42.1 with: cmd: yq '.release.branching.execution.time' '${{ env.WORKFLOW_CONFIG_FILE }}' From e98e0edb5c4050533b686f5cd86abf026338c62f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 21:37:33 -0500 Subject: [PATCH 068/115] chore(deps): bump peterjgrainger/action-create-branch from 2.4.0 to 3.0.0 (#12044) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/node-zxcron-release-branching.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/node-zxcron-release-branching.yaml b/.github/workflows/node-zxcron-release-branching.yaml index 484eacad416e..7364cc971ea3 100644 --- a/.github/workflows/node-zxcron-release-branching.yaml +++ b/.github/workflows/node-zxcron-release-branching.yaml @@ -112,7 +112,7 @@ jobs: - name: Create Branch id: branch - uses: peterjgrainger/action-create-branch@08259812c8ebdbf1973747f9297e332fa078d3c1 # v2.4.0 + uses: peterjgrainger/action-create-branch@10c7d268152480ae859347db45dc69086cef1d9c # v3.0.0 with: branch: refs/heads/${{ needs.check-branch.outputs.branch-name }} From 7545dd24b7ef8221747fd8cb6682e952306f5ac6 Mon Sep 17 00:00:00 2001 From: Matt Hess Date: Tue, 12 Mar 2024 08:27:19 -0500 Subject: [PATCH 069/115] chore: Configure maxAggregateRels to 15 million (all envs) (#12051) Signed-off-by: Matt Hess --- hedera-node/hedera-app/src/test/resources/bootstrap.properties | 2 +- .../src/main/java/com/hedera/node/config/data/TokensConfig.java | 2 +- .../hedera-mono-service/src/main/resources/bootstrap.properties | 2 +- .../mono/context/properties/BootstrapPropertiesTest.java | 2 +- .../hedera-mono-service/src/test/resources/bootstrap.properties | 2 +- .../src/test/resources/bootstrap/standard.properties | 2 +- hedera-node/test-clients/src/main/resource/bootstrap.properties | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/hedera-node/hedera-app/src/test/resources/bootstrap.properties b/hedera-node/hedera-app/src/test/resources/bootstrap.properties index 97532e89cae3..2fda18f67452 100644 --- a/hedera-node/hedera-app/src/test/resources/bootstrap.properties +++ b/hedera-node/hedera-app/src/test/resources/bootstrap.properties @@ -155,7 +155,7 @@ staking.isEnabled=true staking.perHbarRewardRate=6_849 staking.requireMinStakeToReward=false staking.startThreshold=250_000_000_00_000_000 -tokens.maxAggregateRels=10_000_000 +tokens.maxAggregateRels=15_000_000 tokens.maxNumber=1_000_000 tokens.maxPerAccount=1000 tokens.maxRelsPerInfoQuery=1000 diff --git a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/TokensConfig.java b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/TokensConfig.java index bbe215f70f0a..ee47b1038bf4 100644 --- a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/TokensConfig.java +++ b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/TokensConfig.java @@ -23,7 +23,7 @@ @ConfigData("tokens") public record TokensConfig( - @ConfigProperty(defaultValue = "10000000") @NetworkProperty long maxAggregateRels, + @ConfigProperty(defaultValue = "15000000") @NetworkProperty long maxAggregateRels, @ConfigProperty(defaultValue = "true") @NetworkProperty boolean storeRelsOnDisk, @ConfigProperty(defaultValue = "1000000") @NetworkProperty long maxNumber, @ConfigProperty(defaultValue = "1000") @NetworkProperty int maxPerAccount, diff --git a/hedera-node/hedera-mono-service/src/main/resources/bootstrap.properties b/hedera-node/hedera-mono-service/src/main/resources/bootstrap.properties index 64be4527e7d0..1a0ee2664d5e 100644 --- a/hedera-node/hedera-mono-service/src/main/resources/bootstrap.properties +++ b/hedera-node/hedera-mono-service/src/main/resources/bootstrap.properties @@ -159,7 +159,7 @@ staking.isEnabled=true staking.perHbarRewardRate=6_849 staking.requireMinStakeToReward=false staking.startThreshold=250_000_000_00_000_000 -tokens.maxAggregateRels=10_000_000 +tokens.maxAggregateRels=15_000_000 tokens.maxNumber=1_000_000 tokens.maxPerAccount=1000 tokens.maxRelsPerInfoQuery=1000 diff --git a/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/context/properties/BootstrapPropertiesTest.java b/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/context/properties/BootstrapPropertiesTest.java index be28d9dbe29b..cc4deddc1789 100644 --- a/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/context/properties/BootstrapPropertiesTest.java +++ b/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/context/properties/BootstrapPropertiesTest.java @@ -549,7 +549,7 @@ class BootstrapPropertiesTest { entry(SCHEDULING_MAX_NUM, 10_000_000L), entry(TOKENS_MAX_NUM, 1_000_000L), entry(TOPICS_MAX_NUM, 1_000_000L), - entry(TOKENS_MAX_AGGREGATE_RELS, 10_000_000L), + entry(TOKENS_MAX_AGGREGATE_RELS, 15_000_000L), entry(UTIL_PRNG_IS_ENABLED, true), entry(CONTRACTS_SIDECARS, EnumSet.of(SidecarType.CONTRACT_STATE_CHANGE, SidecarType.CONTRACT_BYTECODE)), entry(CONTRACTS_SIDECAR_VALIDATION_ENABLED, false), diff --git a/hedera-node/hedera-mono-service/src/test/resources/bootstrap.properties b/hedera-node/hedera-mono-service/src/test/resources/bootstrap.properties index a012de195704..b8a515aaa921 100644 --- a/hedera-node/hedera-mono-service/src/test/resources/bootstrap.properties +++ b/hedera-node/hedera-mono-service/src/test/resources/bootstrap.properties @@ -156,7 +156,7 @@ staking.isEnabled=true staking.perHbarRewardRate=6_849 staking.requireMinStakeToReward=false staking.startThreshold=250_000_000_00_000_000 -tokens.maxAggregateRels=10_000_000 +tokens.maxAggregateRels=15_000_000 tokens.maxNumber=1_000_000 tokens.maxPerAccount=1000 tokens.maxRelsPerInfoQuery=1000 diff --git a/hedera-node/hedera-mono-service/src/test/resources/bootstrap/standard.properties b/hedera-node/hedera-mono-service/src/test/resources/bootstrap/standard.properties index 55b80a026325..a00af390f409 100644 --- a/hedera-node/hedera-mono-service/src/test/resources/bootstrap/standard.properties +++ b/hedera-node/hedera-mono-service/src/test/resources/bootstrap/standard.properties @@ -155,7 +155,7 @@ staking.isEnabled=true staking.perHbarRewardRate=6_849 staking.requireMinStakeToReward=false staking.startThreshold=250_000_000_00_000_000 -tokens.maxAggregateRels=10_000_000 +tokens.maxAggregateRels=15_000_000 tokens.maxNumber=1_000_000 tokens.maxPerAccount=1000 tokens.maxRelsPerInfoQuery=1000 diff --git a/hedera-node/test-clients/src/main/resource/bootstrap.properties b/hedera-node/test-clients/src/main/resource/bootstrap.properties index 788d86db052f..00017a7a81d2 100644 --- a/hedera-node/test-clients/src/main/resource/bootstrap.properties +++ b/hedera-node/test-clients/src/main/resource/bootstrap.properties @@ -156,7 +156,7 @@ staking.isEnabled=true staking.perHbarRewardRate=6849 staking.requireMinStakeToReward=false staking.startThreshold=100000000 -tokens.maxAggregateRels=10000000 +tokens.maxAggregateRels=15000000 tokens.maxNumber=1000000 tokens.maxPerAccount=1000 tokens.maxRelsPerInfoQuery=1000 From f0b74d27e0de994a5298ff352fe850a7958794ef Mon Sep 17 00:00:00 2001 From: Austin Littley <102969658+alittley@users.noreply.github.com> Date: Tue, 12 Mar 2024 10:13:59 -0400 Subject: [PATCH 070/115] feat: Extract interface from SignedStateHasher (#12035) Signed-off-by: Austin Littley --- .../com/swirlds/platform/SwirldsPlatform.java | 4 +- ...ewSignedStateFromTransactionsConsumer.java | 6 +- .../DefaultStateManagementComponent.java | 10 +- .../signed/DefaultSignedStateHasher.java | 92 +++++++++++++++++++ .../state/signed/SignedStateHasher.java | 76 ++------------- .../state/StateManagementComponentTests.java | 5 +- 6 files changed, 116 insertions(+), 77 deletions(-) create mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/signed/DefaultSignedStateHasher.java 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 6cbfe12eb8e1..18380616126c 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 @@ -565,8 +565,8 @@ public class SwirldsPlatform implements Platform { // FUTURE WORK: this is where the state is currently being hashed. State hashing will be moved into a // separate component. At that time, all subsequent method calls in this lambda will be wired to receive // data from the hasher, since they require a strong guarantee that the state has been hashed. - stateManagementComponent.newSignedStateFromTransactions( - state.getAndReserve("stateManagementComponent.newSignedStateFromTransactions")); + stateManagementComponent.newSignedStateFromTransactions(stateAndRound.makeAdditionalReservation( + ("stateManagementComponent.newSignedStateFromTransactions"))); platformWiring .getIssDetectorWiring() diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/common/output/NewSignedStateFromTransactionsConsumer.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/common/output/NewSignedStateFromTransactionsConsumer.java index a9eb85309410..6b5d16bb5b11 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/common/output/NewSignedStateFromTransactionsConsumer.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/common/output/NewSignedStateFromTransactionsConsumer.java @@ -16,7 +16,7 @@ package com.swirlds.platform.components.common.output; -import com.swirlds.platform.state.signed.ReservedSignedState; +import com.swirlds.platform.wiring.components.StateAndRound; import edu.umd.cs.findbugs.annotations.NonNull; /** @@ -29,7 +29,7 @@ public interface NewSignedStateFromTransactionsConsumer { * A new signed state has been created. The state holds a single reservation. It is the responsibility of the * consumer to release the reservation when appropriate. * - * @param signedState the newly created signed state + * @param stateAndRound the newly created signed state, with its associated round */ - void newSignedStateFromTransactions(@NonNull final ReservedSignedState signedState); + void newSignedStateFromTransactions(@NonNull final StateAndRound stateAndRound); } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/state/DefaultStateManagementComponent.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/state/DefaultStateManagementComponent.java index 0c8504820354..3e73e6c14a81 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/state/DefaultStateManagementComponent.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/state/DefaultStateManagementComponent.java @@ -20,6 +20,7 @@ import com.swirlds.common.context.PlatformContext; import com.swirlds.common.threading.manager.ThreadManager; import com.swirlds.platform.components.common.output.FatalErrorConsumer; +import com.swirlds.platform.state.signed.DefaultSignedStateHasher; import com.swirlds.platform.state.signed.ReservedSignedState; import com.swirlds.platform.state.signed.SignedState; import com.swirlds.platform.state.signed.SignedStateGarbageCollector; @@ -27,6 +28,7 @@ import com.swirlds.platform.state.signed.SignedStateMetrics; import com.swirlds.platform.state.signed.SignedStateSentinel; import com.swirlds.platform.state.signed.SourceOfSignedState; +import com.swirlds.platform.wiring.components.StateAndRound; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.Objects; import java.util.function.Consumer; @@ -91,7 +93,7 @@ public DefaultStateManagementComponent( this.sigCollector = Objects.requireNonNull(sigCollector); this.offerToHashLogger = Objects.requireNonNull(offerToHashLogger); - signedStateHasher = new SignedStateHasher(signedStateMetrics, fatalErrorConsumer); + signedStateHasher = new DefaultSignedStateHasher(signedStateMetrics, fatalErrorConsumer); } private void logHashes(@NonNull final SignedState signedState) { @@ -105,10 +107,10 @@ private void logHashes(@NonNull final SignedState signedState) { } @Override - public void newSignedStateFromTransactions(@NonNull final ReservedSignedState signedState) { - try (signedState) { + public void newSignedStateFromTransactions(@NonNull final StateAndRound stateAndRound) { + try (final ReservedSignedState signedState = stateAndRound.reservedSignedState()) { signedState.get().setGarbageCollector(signedStateGarbageCollector); - signedStateHasher.hashState(signedState.get()); + signedStateHasher.hashState(stateAndRound); logHashes(signedState.get()); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/signed/DefaultSignedStateHasher.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/signed/DefaultSignedStateHasher.java new file mode 100644 index 000000000000..a6651b6d5cf2 --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/signed/DefaultSignedStateHasher.java @@ -0,0 +1,92 @@ +/* + * 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.swirlds.platform.state.signed; + +import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; +import static com.swirlds.platform.system.SystemExitCode.FATAL_ERROR; + +import com.swirlds.common.merkle.crypto.MerkleCryptoFactory; +import com.swirlds.platform.components.common.output.FatalErrorConsumer; +import com.swirlds.platform.wiring.components.StateAndRound; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.time.Duration; +import java.time.Instant; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Hashes signed states after all modifications for a round have been completed. + */ +public class DefaultSignedStateHasher implements SignedStateHasher { + /** + * The logger for the SignedStateHasher class. + */ + private static final Logger logger = LogManager.getLogger(DefaultSignedStateHasher.class); + /** + * The SignedStateMetrics object to record time spent hashing. May be null. + */ + private final SignedStateMetrics signedStateMetrics; + + /** + * The FatalErrorConsumer to notify with any fatal errors that occur during hashing. + */ + private final FatalErrorConsumer fatalErrorConsumer; + + /** + * Constructs a SignedStateHasher to hash SignedStates. If the signedStateMetrics object is not null, the time + * spent hashing is recorded. Any fatal errors that occur are passed to the provided FatalErrorConsumer. The hash is + * dispatched to the provided StateHashedTrigger. + * + * @param signedStateMetrics the SignedStateMetrics instance to record time spent hashing. + * @param fatalErrorConsumer the FatalErrorConsumer to consume any fatal errors during hashing. + * + * @throws NullPointerException if any of the {@code fatalErrorConsumer} parameter is {@code null}. + */ + public DefaultSignedStateHasher( + @Nullable final SignedStateMetrics signedStateMetrics, + @NonNull final FatalErrorConsumer fatalErrorConsumer) { + this.fatalErrorConsumer = Objects.requireNonNull(fatalErrorConsumer, "fatalErrorConsumer must not be null"); + this.signedStateMetrics = signedStateMetrics; + } + + /** + * {@inheritDoc} + */ + @Override + public void hashState(@NonNull final StateAndRound stateAndRound) { + final Instant start = Instant.now(); + try { + MerkleCryptoFactory.getInstance() + .digestTreeAsync(stateAndRound.reservedSignedState().get().getState()) + .get(); + + if (signedStateMetrics != null) { + signedStateMetrics + .getSignedStateHashingTimeMetric() + .update(Duration.between(start, Instant.now()).toMillis()); + } + } catch (final ExecutionException e) { + fatalErrorConsumer.fatalError("Exception occurred during SignedState hashing", e, FATAL_ERROR); + } catch (final InterruptedException e) { + logger.error(EXCEPTION.getMarker(), "Interrupted while hashing state. Expect buggy behavior."); + Thread.currentThread().interrupt(); + } + } +} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/signed/SignedStateHasher.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/signed/SignedStateHasher.java index 4f81eadd333a..11b83fd2334d 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/signed/SignedStateHasher.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/signed/SignedStateHasher.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023-2024 Hedera Hashgraph, LLC + * 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. @@ -16,78 +16,20 @@ package com.swirlds.platform.state.signed; -import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; -import static com.swirlds.platform.system.SystemExitCode.FATAL_ERROR; - -import com.swirlds.common.merkle.crypto.MerkleCryptoFactory; -import com.swirlds.platform.components.common.output.FatalErrorConsumer; +import com.swirlds.common.wiring.component.InputWireLabel; +import com.swirlds.platform.wiring.components.StateAndRound; import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.Nullable; -import java.time.Duration; -import java.time.Instant; -import java.util.Objects; -import java.util.concurrent.ExecutionException; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; /** - * Hashes signed states after all modifications for a round have been completed. + * Hashes signed states */ -public class SignedStateHasher { - /** - * The logger for the SignedStateHasher class. - */ - private static final Logger logger = LogManager.getLogger(SignedStateHasher.class); - /** - * The SignedStateMetrics object to record time spent hashing. May be null. - */ - private final SignedStateMetrics signedStateMetrics; - - /** - * The FatalErrorConsumer to notify with any fatal errors that occur during hashing. - */ - private final FatalErrorConsumer fatalErrorConsumer; - - /** - * Constructs a SignedStateHasher to hash SignedStates. If the signedStateMetrics object is not null, the time - * spent hashing is recorded. Any fatal errors that occur are passed to the provided FatalErrorConsumer. The hash is - * dispatched to the provided StateHashedTrigger. - * - * @param signedStateMetrics the SignedStateMetrics instance to record time spent hashing. - * @param fatalErrorConsumer the FatalErrorConsumer to consume any fatal errors during hashing. - * - * @throws NullPointerException if any of the {@code fatalErrorConsumer} parameter is {@code null}. - */ - public SignedStateHasher( - @Nullable final SignedStateMetrics signedStateMetrics, - @NonNull final FatalErrorConsumer fatalErrorConsumer) { - this.fatalErrorConsumer = Objects.requireNonNull(fatalErrorConsumer, "fatalErrorConsumer must not be null"); - this.signedStateMetrics = signedStateMetrics; - } - +@FunctionalInterface +public interface SignedStateHasher { /** * Hashes a SignedState. * - * @param signedState the SignedState to hash. + * @param stateAndRound the state and round, which contains the state to hash */ - public void hashState(final SignedState signedState) { - final Instant start = Instant.now(); - try { - MerkleCryptoFactory.getInstance() - .digestTreeAsync(signedState.getState()) - .get(); - - if (signedStateMetrics != null) { - signedStateMetrics - .getSignedStateHashingTimeMetric() - .update(Duration.between(start, Instant.now()).toMillis()); - } - - } catch (final ExecutionException e) { - fatalErrorConsumer.fatalError("Exception occurred during SignedState hashing", e, FATAL_ERROR); - } catch (final InterruptedException e) { - logger.error(EXCEPTION.getMarker(), "Interrupted while hashing state. Expect buggy behavior."); - Thread.currentThread().interrupt(); - } - } + @InputWireLabel("unhashed state and round") + void hashState(@NonNull StateAndRound stateAndRound); } diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/components/state/StateManagementComponentTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/components/state/StateManagementComponentTests.java index e870e55b3d28..889a088a2210 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/components/state/StateManagementComponentTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/components/state/StateManagementComponentTests.java @@ -32,12 +32,14 @@ import com.swirlds.common.threading.manager.AdHocThreadManager; import com.swirlds.config.extensions.test.fixtures.TestConfigBuilder; import com.swirlds.platform.config.StateConfig_; +import com.swirlds.platform.internal.ConsensusRound; import com.swirlds.platform.state.RandomSignedStateGenerator; import com.swirlds.platform.state.signed.ReservedSignedState; import com.swirlds.platform.state.signed.SignedState; import com.swirlds.platform.state.signed.SignedStateMetrics; import com.swirlds.platform.system.address.AddressBook; import com.swirlds.platform.system.transaction.StateSignatureTransaction; +import com.swirlds.platform.wiring.components.StateAndRound; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.util.HashMap; @@ -90,7 +92,8 @@ void newStateFromTransactionsSubmitsSystemTransaction() { final Hash hash = getHash(signedState); signedState.getState().setHash(null); // we expect this to trigger hashing the state. - component.newSignedStateFromTransactions(signedState.reserve("test")); + component.newSignedStateFromTransactions( + new StateAndRound(signedState.reserve("test"), mock(ConsensusRound.class))); final Hash hash2 = getHash(signedState); assertEquals(hash, hash2, "The same hash must be computed and added to the state."); From 99c57c1a6f048da95e7f545a3bdccbca2845fe13 Mon Sep 17 00:00:00 2001 From: Austin Littley <102969658+alittley@users.noreply.github.com> Date: Tue, 12 Mar 2024 10:27:34 -0400 Subject: [PATCH 071/115] feat: Extract interface from `SavedStateController` (#11986) Signed-off-by: Austin Littley --- .../com/swirlds/platform/SwirldsPlatform.java | 3 +- .../DefaultSavedStateController.java | 154 ++++++++++++++++++ .../components/SavedStateController.java | 118 +------------- .../platform/SignedStateFileManagerTests.java | 3 +- .../state/TestSavedStateController.java | 48 ------ 5 files changed, 167 insertions(+), 159 deletions(-) create mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/DefaultSavedStateController.java delete mode 100644 platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/components/state/TestSavedStateController.java 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 18380616126c..8da884195fdf 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 @@ -63,6 +63,7 @@ import com.swirlds.logging.legacy.payload.FatalErrorPayload; import com.swirlds.metrics.api.Metrics; import com.swirlds.platform.components.ConsensusEngine; +import com.swirlds.platform.components.DefaultSavedStateController; import com.swirlds.platform.components.SavedStateController; import com.swirlds.platform.components.appcomm.LatestCompleteStateNotifier; import com.swirlds.platform.components.state.DefaultStateManagementComponent; @@ -485,7 +486,7 @@ public class SwirldsPlatform implements Platform { oldStyleIntakeQueue = null; } - savedStateController = new SavedStateController(stateConfig); + savedStateController = new DefaultSavedStateController(stateConfig); final SignedStateMetrics signedStateMetrics = new SignedStateMetrics(platformContext.getMetrics()); final StateSignatureCollector stateSignatureCollector = new StateSignatureCollector( diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/DefaultSavedStateController.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/DefaultSavedStateController.java new file mode 100644 index 000000000000..8987cfba80c8 --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/DefaultSavedStateController.java @@ -0,0 +1,154 @@ +/* + * 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.swirlds.platform.components; + +import static com.swirlds.logging.legacy.LogMarker.STATE_TO_DISK; +import static com.swirlds.platform.state.signed.StateToDiskReason.FIRST_ROUND_AFTER_GENESIS; +import static com.swirlds.platform.state.signed.StateToDiskReason.FREEZE_STATE; +import static com.swirlds.platform.state.signed.StateToDiskReason.PERIODIC_SNAPSHOT; +import static com.swirlds.platform.state.signed.StateToDiskReason.RECONNECT; + +import com.swirlds.platform.config.StateConfig; +import com.swirlds.platform.state.signed.ReservedSignedState; +import com.swirlds.platform.state.signed.SignedState; +import com.swirlds.platform.state.signed.StateToDiskReason; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.time.Instant; +import java.util.Objects; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * The default implementation of {@link SavedStateController}. + */ +public class DefaultSavedStateController implements SavedStateController { + private static final Logger logger = LogManager.getLogger(DefaultSavedStateController.class); + /** + * The timestamp of the signed state that was most recently written to disk, or null if no timestamp was recently + * written to disk. + */ + private Instant previousSavedStateTimestamp; + + private final StateConfig stateConfig; + + /** + * Constructor + * + * @param stateConfig the state config + */ + public DefaultSavedStateController(@NonNull final StateConfig stateConfig) { + this.stateConfig = Objects.requireNonNull(stateConfig); + } + + /** + * {@inheritDoc} + */ + @Override + public synchronized void markSavedState(@NonNull final ReservedSignedState reservedSignedState) { + try (reservedSignedState) { + final SignedState signedState = reservedSignedState.get(); + final StateToDiskReason reason = shouldSaveToDisk(signedState, previousSavedStateTimestamp); + + if (reason != null) { + markSavingToDisk(reservedSignedState, reason); + } + // if a null reason is returned, then there isn't anything to do, since the state shouldn't be saved + } + } + + /** + * {@inheritDoc} + */ + @Override + public synchronized void reconnectStateReceived(@NonNull final ReservedSignedState reservedSignedState) { + try (reservedSignedState) { + markSavingToDisk(reservedSignedState, RECONNECT); + } + } + + /** + * {@inheritDoc} + */ + @Override + public synchronized void registerSignedStateFromDisk(@NonNull final SignedState signedState) { + previousSavedStateTimestamp = signedState.getConsensusTimestamp(); + } + + /** + * Marks a signed state with a reason why it should eventually be written to disk + * + * @param state the state to mark + * @param reason the reason why the state should be written to disk + */ + private void markSavingToDisk(@NonNull final ReservedSignedState state, @NonNull final StateToDiskReason reason) { + final SignedState signedState = state.get(); + logger.info( + STATE_TO_DISK.getMarker(), + "Signed state from round {} created, will eventually be written to disk, for reason: {}", + signedState.getRound(), + reason); + + previousSavedStateTimestamp = signedState.getConsensusTimestamp(); + signedState.markAsStateToSave(reason); + } + + /** + * Determines whether a signed state should eventually be written to disk + *

    + * If it is determined that the state should be written to disk, this method returns the reason why + *

    + * If it is determined that the state shouldn't be written to disk, then this method returns null + * + * @param signedState the state in question + * @param previousTimestamp the timestamp of the previous state that was saved to disk, or null if no previous state + * was saved to disk + * @return the reason why the state should be written to disk, or null if it shouldn't be written to disk + */ + @Nullable + private StateToDiskReason shouldSaveToDisk( + @NonNull final SignedState signedState, @Nullable final Instant previousTimestamp) { + + if (signedState.isFreezeState()) { + // the state right before a freeze should be written to disk + return FREEZE_STATE; + } + + final int saveStatePeriod = stateConfig.saveStatePeriod(); + if (saveStatePeriod <= 0) { + // periodic state saving is disabled + return null; + } + + // FUTURE WORK: writing genesis state to disk is currently disabled if the saveStatePeriod is 0. + // This is for testing purposes, to have a method of disabling state saving for tests. + // Once a feature to disable all state saving has been added, this block should be moved in front of the + // saveStatePeriod <=0 block, so that saveStatePeriod doesn't impact the saving of genesis state. + if (previousTimestamp == null) { + // the first round should be saved + return FIRST_ROUND_AFTER_GENESIS; + } + + if ((signedState.getConsensusTimestamp().getEpochSecond() / saveStatePeriod) + > (previousTimestamp.getEpochSecond() / saveStatePeriod)) { + return PERIODIC_SNAPSHOT; + } else { + // the period hasn't yet elapsed + return null; + } + } +} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/SavedStateController.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/SavedStateController.java index 0c6ef702a4ff..9224e195af8f 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/SavedStateController.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/SavedStateController.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023-2024 Hedera Hashgraph, LLC + * 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. @@ -16,62 +16,23 @@ package com.swirlds.platform.components; -import static com.swirlds.logging.legacy.LogMarker.STATE_TO_DISK; -import static com.swirlds.platform.state.signed.StateToDiskReason.FIRST_ROUND_AFTER_GENESIS; -import static com.swirlds.platform.state.signed.StateToDiskReason.FREEZE_STATE; -import static com.swirlds.platform.state.signed.StateToDiskReason.PERIODIC_SNAPSHOT; -import static com.swirlds.platform.state.signed.StateToDiskReason.RECONNECT; - -import com.swirlds.platform.config.StateConfig; +import com.swirlds.common.wiring.component.InputWireLabel; import com.swirlds.platform.state.signed.ReservedSignedState; import com.swirlds.platform.state.signed.SignedState; -import com.swirlds.platform.state.signed.StateToDiskReason; import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.Nullable; -import java.time.Instant; -import java.util.Objects; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; /** * Controls which signed states should be written to disk based on input from other components */ -public class SavedStateController { - private static final Logger logger = LogManager.getLogger(SavedStateController.class); - /** - * The timestamp of the signed state that was most recently written to disk, or null if no timestamp was recently - * written to disk. - */ - private Instant previousSavedStateTimestamp; - /** the state config */ - private final StateConfig stateConfig; - - /** - * Create a new SavedStateController - * - * @param stateConfig the state config - */ - public SavedStateController(@NonNull final StateConfig stateConfig) { - this.stateConfig = Objects.requireNonNull(stateConfig); - } - +public interface SavedStateController { /** * Determine if a signed state should be written to disk. If the state should be written, the state will be marked * and then written to disk outside the scope of this class. * * @param reservedSignedState the signed state in question */ - public synchronized void markSavedState(@NonNull final ReservedSignedState reservedSignedState) { - try (reservedSignedState) { - final SignedState signedState = reservedSignedState.get(); - final StateToDiskReason reason = shouldSaveToDisk(signedState, previousSavedStateTimestamp); - - if (reason != null) { - markSavingToDisk(reservedSignedState, reason); - } - // if a null reason is returned, then there isn't anything to do, since the state shouldn't be saved - } - } + @InputWireLabel("state to mark") + void markSavedState(@NonNull ReservedSignedState reservedSignedState); /** * Notifies the controller that a signed state was received from another node during reconnect. The controller saves @@ -79,75 +40,14 @@ public synchronized void markSavedState(@NonNull final ReservedSignedState reser * * @param reservedSignedState the signed state that was received from another node during reconnect */ - public synchronized void reconnectStateReceived(@NonNull final ReservedSignedState reservedSignedState) { - try (reservedSignedState) { - markSavingToDisk(reservedSignedState, RECONNECT); - } - } + @InputWireLabel("reconnect state") + void reconnectStateReceived(@NonNull ReservedSignedState reservedSignedState); /** * This should be called at boot time when a signed state is read from the disk. * * @param signedState the signed state that was read from file at boot time */ - public synchronized void registerSignedStateFromDisk(@NonNull final SignedState signedState) { - previousSavedStateTimestamp = signedState.getConsensusTimestamp(); - } - - private void markSavingToDisk(@NonNull final ReservedSignedState state, @NonNull final StateToDiskReason reason) { - final SignedState signedState = state.get(); - logger.info( - STATE_TO_DISK.getMarker(), - "Signed state from round {} created, will eventually be written to disk, for reason: {}", - signedState.getRound(), - reason); - - previousSavedStateTimestamp = signedState.getConsensusTimestamp(); - signedState.markAsStateToSave(reason); - } - - /** - * Determines whether a signed state should eventually be written to disk - *

    - * If it is determined that the state should be written to disk, this method returns the reason why - *

    - * If it is determined that the state shouldn't be written to disk, then this method returns null - * - * @param signedState the state in question - * @param previousTimestamp the timestamp of the previous state that was saved to disk, or null if no previous state - * was saved to disk - * @return the reason why the state should be written to disk, or null if it shouldn't be written to disk - */ - @Nullable - private StateToDiskReason shouldSaveToDisk( - @NonNull final SignedState signedState, @Nullable final Instant previousTimestamp) { - - if (signedState.isFreezeState()) { - // the state right before a freeze should be written to disk - return FREEZE_STATE; - } - - final int saveStatePeriod = stateConfig.saveStatePeriod(); - if (saveStatePeriod <= 0) { - // periodic state saving is disabled - return null; - } - - // FUTURE WORK: writing genesis state to disk is currently disabled if the saveStatePeriod is 0. - // This is for testing purposes, to have a method of disabling state saving for tests. - // Once a feature to disable all state saving has been added, this block should be moved in front of the - // saveStatePeriod <=0 block, so that saveStatePeriod doesn't impact the saving of genesis state. - if (previousTimestamp == null) { - // the first round should be saved - return FIRST_ROUND_AFTER_GENESIS; - } - - if ((signedState.getConsensusTimestamp().getEpochSecond() / saveStatePeriod) - > (previousTimestamp.getEpochSecond() / saveStatePeriod)) { - return PERIODIC_SNAPSHOT; - } else { - // the period hasn't yet elapsed - return null; - } - } + @InputWireLabel("state from disk") + void registerSignedStateFromDisk(@NonNull SignedState signedState); } diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/SignedStateFileManagerTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/SignedStateFileManagerTests.java index d1f3f22a1bf7..a3e473a27b5f 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/SignedStateFileManagerTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/SignedStateFileManagerTests.java @@ -49,6 +49,7 @@ import com.swirlds.common.utility.CompareTo; import com.swirlds.config.extensions.test.fixtures.TestConfigBuilder; import com.swirlds.metrics.api.Counter; +import com.swirlds.platform.components.DefaultSavedStateController; import com.swirlds.platform.components.SavedStateController; import com.swirlds.platform.config.StateConfig; import com.swirlds.platform.config.StateConfig_; @@ -273,7 +274,7 @@ void sequenceOfStatesTest(final boolean startAtGenesis) throws IOException { final SignedStateFileManager manager = new SignedStateFileManager( context, buildMockMetrics(), new FakeTime(), MAIN_CLASS_NAME, SELF_ID, SWIRLD_NAME); final SavedStateController controller = - new SavedStateController(context.getConfiguration().getConfigData(StateConfig.class)); + new DefaultSavedStateController(context.getConfiguration().getConfigData(StateConfig.class)); Instant timestamp; final long firstRound; diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/components/state/TestSavedStateController.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/components/state/TestSavedStateController.java deleted file mode 100644 index 1330f875692f..000000000000 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/components/state/TestSavedStateController.java +++ /dev/null @@ -1,48 +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.swirlds.platform.components.state; - -import com.swirlds.config.extensions.test.fixtures.TestConfigBuilder; -import com.swirlds.platform.components.SavedStateController; -import com.swirlds.platform.config.StateConfig; -import com.swirlds.platform.state.signed.ReservedSignedState; -import com.swirlds.platform.state.signed.SignedState; -import edu.umd.cs.findbugs.annotations.NonNull; -import java.util.Deque; -import java.util.LinkedList; - -public class TestSavedStateController extends SavedStateController { - private final Deque queue = new LinkedList<>(); - - public TestSavedStateController() { - super(new TestConfigBuilder().getOrCreateConfig().getConfigData(StateConfig.class)); - } - - @Override - public synchronized void reconnectStateReceived(@NonNull final ReservedSignedState reservedSignedState) { - queue.add(reservedSignedState.get()); - } - - @Override - public synchronized void registerSignedStateFromDisk(@NonNull final SignedState signedState) { - queue.add(signedState); - } - - public @NonNull Deque getStatesQueue() { - return queue; - } -} From f9214317852d76f3d98043fda17770992940b853 Mon Sep 17 00:00:00 2001 From: Michael Tinker Date: Tue, 12 Mar 2024 10:12:43 -0500 Subject: [PATCH 072/115] fix: set `SUCCESS` status in genesis records (#12038) Signed-off-by: Michael Tinker --- .../handle/record/GenesisRecordsConsensusHook.java | 2 ++ .../service/token/records/GenesisAccountRecordBuilder.java | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/GenesisRecordsConsensusHook.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/GenesisRecordsConsensusHook.java index 4d86233ab8d8..c3e39b70ddb1 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/GenesisRecordsConsensusHook.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/GenesisRecordsConsensusHook.java @@ -16,6 +16,7 @@ package com.hedera.node.app.workflows.handle.record; +import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS; import static com.hedera.node.app.service.token.impl.handlers.staking.StakingRewardsHelper.asAccountAmounts; import static com.hedera.node.app.spi.HapiUtils.ACCOUNT_ID_COMPARATOR; import static com.hedera.node.app.spi.HapiUtils.FUNDING_ACCOUNT_EXPIRY; @@ -190,6 +191,7 @@ private void createAccountRecordBuilders( .accountAmounts(asAccountAmounts(Map.of(accountID, balance))) .build()); } + recordBuilder.status(SUCCESS); log.debug("Queued synthetic CryptoCreate for {} account {}", recordMemo, account); } diff --git a/hedera-node/hedera-token-service/src/main/java/com/hedera/node/app/service/token/records/GenesisAccountRecordBuilder.java b/hedera-node/hedera-token-service/src/main/java/com/hedera/node/app/service/token/records/GenesisAccountRecordBuilder.java index ba63fb089246..b55e6affdd80 100644 --- a/hedera-node/hedera-token-service/src/main/java/com/hedera/node/app/service/token/records/GenesisAccountRecordBuilder.java +++ b/hedera-node/hedera-token-service/src/main/java/com/hedera/node/app/service/token/records/GenesisAccountRecordBuilder.java @@ -17,6 +17,7 @@ package com.hedera.node.app.service.token.records; import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.hapi.node.base.Transaction; import com.hedera.hapi.node.base.TransferList; import edu.umd.cs.findbugs.annotations.NonNull; @@ -39,6 +40,12 @@ public interface GenesisAccountRecordBuilder { @NonNull GenesisAccountRecordBuilder transaction(@NonNull final Transaction txn); + /** + * Tracks the synthetic transaction that represents the created system account + */ + @NonNull + GenesisAccountRecordBuilder status(@NonNull ResponseCodeEnum status); + /** * Tracks the memo for the synthetic record */ From 653bbf4bd3d8bfd0fff1140b7eaa01e5a9cf4bff Mon Sep 17 00:00:00 2001 From: Petar Tonev Date: Tue, 12 Mar 2024 17:52:17 +0200 Subject: [PATCH 073/115] fix: add positive suite tests for fixed custom fee (#11795) --- .../suites/crypto/TransferWithCustomFees.java | 927 +++++++++++++++++- 1 file changed, 884 insertions(+), 43 deletions(-) diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/TransferWithCustomFees.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/TransferWithCustomFees.java index c5f30517ea4e..22fb01c3431f 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/TransferWithCustomFees.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/TransferWithCustomFees.java @@ -18,21 +18,37 @@ import static com.hedera.services.bdd.junit.TestTags.CRYPTO; import static com.hedera.services.bdd.spec.HapiSpec.defaultHapiSpec; +import static com.hedera.services.bdd.spec.assertions.AccountDetailsAsserts.accountDetailsWith; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountDetails; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTxnRecord; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoApproveAllowance; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoTransfer; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.mintToken; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenAssociate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenCreate; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedHbarFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedHtsFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fractionalFee; import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; +import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.movingUnique; +import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.movingUniqueWithAllowance; +import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.movingWithAllowance; +import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; +import com.hedera.node.app.hapi.utils.ByteStringUtils; import com.hedera.services.bdd.junit.HapiTest; import com.hedera.services.bdd.junit.HapiTestSuite; import com.hedera.services.bdd.spec.HapiSpec; +import com.hedera.services.bdd.spec.HapiSpecOperation; import com.hedera.services.bdd.suites.HapiSuite; import com.hederahashgraph.api.proto.java.ResponseCodeEnum; +import com.hederahashgraph.api.proto.java.TokenSupplyType; +import com.hederahashgraph.api.proto.java.TokenType; +import java.util.ArrayList; import java.util.List; import java.util.OptionalLong; import org.apache.logging.log4j.LogManager; @@ -43,22 +59,33 @@ @Tag(CRYPTO) public class TransferWithCustomFees extends HapiSuite { private static final Logger log = LogManager.getLogger(TransferWithCustomFees.class); - private final long hbarFee = 1_000L; - private final long htsFee = 100L; - private final long tokenTotal = 1_000L; - private final long numerator = 1L; - private final long denominator = 10L; - private final long minHtsFee = 2L; - private final long maxHtsFee = 10L; - - private final String token = "withCustomSchedules"; - private final String feeDenom = "denom"; - private final String hbarCollector = "hbarFee"; - private final String htsCollector = "denomFee"; - private final String tokenReceiver = "receiver"; - private final String tokenTreasury = "tokenTreasury"; - - private final String tokenOwner = "tokenOwner"; + private static final long hbarFee = 1_000L; + private static final long htsFee = 100L; + private static final long tokenTotal = 1_000L; + private static final long numerator = 1L; + private static final long denominator = 10L; + private static final long minHtsFee = 2L; + private static final long maxHtsFee = 10L; + + private static final String fungibleToken = "fungibleWithCustomFees"; + private static final String fungibleToken2 = "fungibleWithCustomFees2"; + private static final String nonFungibleToken = "nonFungibleWithCustomFees"; + private static final String feeDenom = "denom"; + private static final String feeDenom2 = "denom2"; + private static final String hbarCollector = "hbarFee"; + private static final String htsCollector = "denomFee"; + private static final String htsCollector2 = "denomFee2"; + private static final String tokenReceiver = "receiver"; + private static final String tokenTreasury = "tokenTreasury"; + private static final String spender = "spender"; + private static final String NFT_KEY = "nftKey"; + private static final String tokenOwner = "tokenOwner"; + private static final String alice = "alice"; + private static final long aliceFee = 100L; + private static final String bob = "bob"; + private static final long bobFee = 200L; + private static final String carol = "carol"; + private static final long carolFee = 300L; public static void main(String... args) { new TransferWithCustomFees().runSuiteAsync(); @@ -67,44 +94,858 @@ public static void main(String... args) { @Override public List getSpecsInSuite() { return List.of(new HapiSpec[] { - transferWithFixedCustomFeeSchedule(), - transferWithFractinalCustomFeeSchedule(), + transferFungibleTokenWithFixedHbarCustomFees(), + transferFungibleTokenWithFixedHtsCustomFees(), + transferNonFungibleTokenWithFixedHbarCustomFees(), + transferNonFungibleTokenWithFixedHtsCustomFees(), + transferApprovedFungibleTokenWithFixedHbarCustomFee(), + transferApprovedFungibleTokenWithFixedHtsCustomFeeAsOwner(), + transferApprovedFungibleTokenWithFixedHtsCustomFeeAsSpender(), + transferApprovedNonFungibleTokenWithFixedHbarCustomFee(), + transferApprovedNonFungibleTokenWithFixedHtsCustomFeesAsOwner(), + transferApprovedNonFungibleTokenWithFixedHtsCustomFeeAsSpender(), + transferFungibleTokenWithThreeFixedHtsCustomFeesWithoutAllCollectorsExempt(), + transferFungibleTokenWithThreeFixedHtsCustomFeesWithAllCollectorsExempt(), + transferFungibleTokenWithFixedHtsCustomFees2Layers(), + transferNonFungibleTokenWithFixedHtsCustomFees2Layers(), + transferMaxFungibleTokenWith10FixedHtsCustomFees2Layers(), + multipleTransfersWithMultipleCustomFees(), + transferWithFractionalCustomFee(), transferWithInsufficientCustomFees() }); } @HapiTest - public HapiSpec transferWithFixedCustomFeeSchedule() { - return defaultHapiSpec("transferWithFixedCustomFeeSchedule") + public HapiSpec transferFungibleTokenWithFixedHbarCustomFees() { + return defaultHapiSpec("transferFungibleTokenWithFixedHbarCustomFees") + .given( + cryptoCreate(hbarCollector).balance(0L), + cryptoCreate(tokenOwner).balance(ONE_MILLION_HBARS), + cryptoCreate(tokenReceiver), + cryptoCreate(tokenTreasury), + tokenCreate(fungibleToken) + .treasury(tokenTreasury) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(tokenTotal) + .withCustom(fixedHbarFee(hbarFee, hbarCollector)), + tokenAssociate(tokenReceiver, fungibleToken), + tokenAssociate(tokenOwner, fungibleToken), + cryptoTransfer(moving(1000, fungibleToken).between(tokenTreasury, tokenOwner))) + .when(cryptoTransfer(moving(1, fungibleToken).between(tokenOwner, tokenReceiver)) + .fee(ONE_HUNDRED_HBARS) + .via("hbarFixedFee") + .payingWith(tokenOwner)) + .then(withOpContext((spec, log) -> { + final var record = getTxnRecord("hbarFixedFee"); + allRunFor(spec, record); + final var txFee = record.getResponseRecord().getTransactionFee(); + + getAccountBalance(tokenOwner) + .hasTinyBars(ONE_MILLION_HBARS - (txFee + hbarFee)) + .hasTokenBalance(fungibleToken, 999); + getAccountBalance(tokenReceiver).hasTokenBalance(fungibleToken, 1); + getAccountBalance(hbarCollector).hasTinyBars(hbarFee); + })); + } + + @HapiTest + public HapiSpec transferFungibleTokenWithFixedHtsCustomFees() { + return defaultHapiSpec("transferFungibleTokenWithFixedHtsCustomFees") .given( cryptoCreate(htsCollector), + cryptoCreate(tokenOwner).balance(ONE_MILLION_HBARS), + cryptoCreate(tokenReceiver), + cryptoCreate(tokenTreasury), + tokenCreate(feeDenom).treasury(tokenOwner).initialSupply(tokenTotal), + tokenAssociate(htsCollector, feeDenom), + tokenCreate(fungibleToken) + .treasury(tokenTreasury) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(tokenTotal) + .withCustom(fixedHtsFee(htsFee, feeDenom, htsCollector)), + tokenAssociate(tokenReceiver, fungibleToken), + tokenAssociate(tokenOwner, fungibleToken), + cryptoTransfer(moving(1000, fungibleToken).between(tokenTreasury, tokenOwner))) + .when(cryptoTransfer(moving(1, fungibleToken).between(tokenOwner, tokenReceiver)) + .fee(ONE_HUNDRED_HBARS) + .payingWith(tokenOwner)) + .then( + getAccountBalance(tokenOwner) + .hasTokenBalance(fungibleToken, 999) + .hasTokenBalance(feeDenom, tokenTotal - htsFee), + getAccountBalance(tokenReceiver).hasTokenBalance(fungibleToken, 1), + getAccountBalance(htsCollector).hasTokenBalance(feeDenom, htsFee)); + } + + @HapiTest + public HapiSpec transferNonFungibleTokenWithFixedHbarCustomFees() { + return defaultHapiSpec("transferNonFungibleTokenWithFixedHbarCustomFees") + .given( + newKeyNamed(NFT_KEY), cryptoCreate(hbarCollector).balance(0L), cryptoCreate(tokenOwner).balance(ONE_MILLION_HBARS), cryptoCreate(tokenReceiver), cryptoCreate(tokenTreasury), + tokenCreate(nonFungibleToken) + .treasury(tokenTreasury) + .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .supplyKey(NFT_KEY) + .supplyType(TokenSupplyType.INFINITE) + .initialSupply(0) + .withCustom(fixedHbarFee(hbarFee, hbarCollector)), + mintToken(nonFungibleToken, List.of(ByteStringUtils.wrapUnsafely("meta1".getBytes()))), + tokenAssociate(tokenReceiver, nonFungibleToken), + tokenAssociate(tokenOwner, nonFungibleToken), + cryptoTransfer(movingUnique(nonFungibleToken, 1L).between(tokenTreasury, tokenOwner))) + .when(cryptoTransfer(movingUnique(nonFungibleToken, 1L).between(tokenOwner, tokenReceiver)) + .fee(ONE_HUNDRED_HBARS) + .via("hbarFixedFee") + .payingWith(tokenOwner)) + .then(withOpContext((spec, log) -> { + final var record = getTxnRecord("hbarFixedFee"); + allRunFor(spec, record); + final var txFee = record.getResponseRecord().getTransactionFee(); + + getAccountBalance(tokenOwner) + .hasTinyBars(ONE_MILLION_HBARS - (txFee + hbarFee)) + .hasTokenBalance(nonFungibleToken, 0); + getAccountBalance(tokenReceiver).hasTokenBalance(nonFungibleToken, 1); + getAccountBalance(hbarCollector).hasTinyBars(hbarFee); + })); + } + + @HapiTest + public HapiSpec transferNonFungibleTokenWithFixedHtsCustomFees() { + return defaultHapiSpec("transferNonFungibleTokenWithFixedHtsCustomFees") + .given( + newKeyNamed(NFT_KEY), + cryptoCreate(htsCollector), + cryptoCreate(tokenOwner).balance(ONE_MILLION_HBARS), + cryptoCreate(tokenReceiver), + cryptoCreate(tokenTreasury), tokenCreate(feeDenom).treasury(tokenOwner).initialSupply(tokenTotal), tokenAssociate(htsCollector, feeDenom), - tokenCreate(token) + tokenCreate(nonFungibleToken) .treasury(tokenTreasury) + .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .supplyKey(NFT_KEY) + .supplyType(TokenSupplyType.INFINITE) + .initialSupply(0) + .withCustom(fixedHtsFee(htsFee, feeDenom, htsCollector)), + mintToken(nonFungibleToken, List.of(ByteStringUtils.wrapUnsafely("meta1".getBytes()))), + tokenAssociate(tokenReceiver, nonFungibleToken), + tokenAssociate(tokenOwner, nonFungibleToken), + cryptoTransfer(movingUnique(nonFungibleToken, 1L).between(tokenTreasury, tokenOwner))) + .when(cryptoTransfer(movingUnique(nonFungibleToken, 1L).between(tokenOwner, tokenReceiver)) + .fee(ONE_HUNDRED_HBARS) + .payingWith(tokenOwner)) + .then( + getAccountBalance(tokenOwner).hasTokenBalance(nonFungibleToken, 0), + getAccountBalance(tokenReceiver).hasTokenBalance(nonFungibleToken, 1), + getAccountBalance(htsCollector).hasTokenBalance(feeDenom, htsFee)); + } + + @HapiTest + public HapiSpec transferApprovedFungibleTokenWithFixedHbarCustomFee() { + return defaultHapiSpec("transferApprovedFungibleTokenWithFixedHbarCustomFee") + .given( + cryptoCreate(hbarCollector).balance(0L), + cryptoCreate(tokenOwner).balance(ONE_MILLION_HBARS), + cryptoCreate(spender).balance(ONE_MILLION_HBARS), + cryptoCreate(tokenReceiver), + cryptoCreate(tokenTreasury), + tokenCreate(fungibleToken) + .treasury(tokenTreasury) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(tokenTotal) + .withCustom(fixedHbarFee(hbarFee, hbarCollector)), + tokenAssociate(tokenReceiver, fungibleToken), + tokenAssociate(tokenOwner, fungibleToken), + tokenAssociate(spender, fungibleToken), + cryptoTransfer(moving(tokenTotal, fungibleToken).between(tokenTreasury, tokenOwner))) + .when( + cryptoApproveAllowance() + .addTokenAllowance(tokenOwner, fungibleToken, spender, 10L) + .fee(ONE_HUNDRED_HBARS) + .payingWith(tokenOwner), + getAccountDetails(tokenOwner) + .has(accountDetailsWith().tokenAllowancesContaining(fungibleToken, spender, 10L)), + cryptoTransfer(movingWithAllowance(1L, fungibleToken).between(tokenOwner, tokenReceiver)) + .fee(ONE_HUNDRED_HBARS) + .via("hbarFixedFee") + .payingWith(spender) + .signedBy(spender)) + .then(withOpContext((spec, log) -> { + final var record = getTxnRecord("hbarFixedFee"); + allRunFor(spec, record); + final var txFee = record.getResponseRecord().getTransactionFee(); + + getAccountBalance(tokenOwner) + .hasTinyBars(ONE_MILLION_HBARS - (txFee + hbarFee)) + .hasTokenBalance(fungibleToken, 999); + getAccountBalance(spender).hasTokenBalance(fungibleToken, 0); + getAccountBalance(tokenReceiver).hasTokenBalance(fungibleToken, 1); + getAccountBalance(hbarCollector).hasTinyBars(hbarFee); + getAccountDetails(tokenOwner) + .has(accountDetailsWith().tokenAllowancesContaining(fungibleToken, spender, 9L)); + })); + } + + @HapiTest + public HapiSpec transferApprovedFungibleTokenWithFixedHtsCustomFeeAsOwner() { + return defaultHapiSpec("transferApprovedFungibleTokenWithFixedHtsCustomFeeAsOwner") + .given( + cryptoCreate(htsCollector), + cryptoCreate(tokenOwner).balance(ONE_MILLION_HBARS), + cryptoCreate(spender).balance(ONE_MILLION_HBARS), + cryptoCreate(tokenReceiver), + cryptoCreate(tokenTreasury), + tokenCreate(feeDenom).treasury(spender).initialSupply(tokenTotal), + tokenAssociate(htsCollector, feeDenom), + tokenAssociate(tokenOwner, feeDenom), + tokenCreate(fungibleToken) + .treasury(tokenTreasury) + .tokenType(TokenType.FUNGIBLE_COMMON) .initialSupply(tokenTotal) - .withCustom(fixedHbarFee(hbarFee, hbarCollector)) .withCustom(fixedHtsFee(htsFee, feeDenom, htsCollector)), - tokenAssociate(tokenReceiver, token), - tokenAssociate(tokenOwner, token), - cryptoTransfer(moving(1000, token).between(tokenTreasury, tokenOwner))) - .when(cryptoTransfer(moving(1, token).between(tokenOwner, tokenReceiver)) + tokenAssociate(tokenReceiver, fungibleToken), + tokenAssociate(tokenOwner, fungibleToken), + tokenAssociate(spender, fungibleToken), + cryptoTransfer(moving(tokenTotal, fungibleToken).between(tokenTreasury, tokenOwner)), + cryptoTransfer(moving(200L, feeDenom).between(spender, tokenOwner))) + .when( + cryptoApproveAllowance() + .addTokenAllowance(tokenOwner, fungibleToken, spender, 10L) + .fee(ONE_HUNDRED_HBARS) + .payingWith(tokenOwner), + getAccountDetails(tokenOwner) + .has(accountDetailsWith().tokenAllowancesContaining(fungibleToken, spender, 10L)), + cryptoTransfer(movingWithAllowance(1L, fungibleToken).between(tokenOwner, tokenReceiver)) + .fee(ONE_HUNDRED_HBARS) + .payingWith(spender) + .signedBy(spender)) + .then( + getAccountDetails(tokenOwner) + .has(accountDetailsWith().tokenAllowancesContaining(fungibleToken, spender, 9L)), + getAccountBalance(tokenOwner) + .hasTokenBalance(fungibleToken, 999) + .hasTokenBalance(feeDenom, 200L - htsFee), + getAccountBalance(spender) + .hasTokenBalance(fungibleToken, 0) + .hasTokenBalance(feeDenom, 800L), + getAccountBalance(tokenReceiver).hasTokenBalance(fungibleToken, 1), + getAccountBalance(htsCollector).hasTokenBalance(feeDenom, htsFee)); + } + + @HapiTest + public HapiSpec transferApprovedFungibleTokenWithFixedHtsCustomFeeAsSpender() { + return defaultHapiSpec("transferApprovedFungibleTokenWithFixedHtsCustomFeeAsSpender") + .given( + cryptoCreate(htsCollector), + cryptoCreate(tokenOwner).balance(ONE_MILLION_HBARS), + cryptoCreate(spender).balance(ONE_MILLION_HBARS), + cryptoCreate(tokenReceiver), + cryptoCreate(tokenTreasury), + tokenCreate(feeDenom).treasury(tokenOwner).initialSupply(tokenTotal), + tokenAssociate(htsCollector, feeDenom), + tokenAssociate(spender, feeDenom), + tokenCreate(fungibleToken) + .treasury(tokenTreasury) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(tokenTotal) + .withCustom(fixedHtsFee(htsFee, feeDenom, htsCollector)), + tokenAssociate(tokenReceiver, fungibleToken), + tokenAssociate(tokenOwner, fungibleToken), + tokenAssociate(spender, fungibleToken), + cryptoTransfer(moving(tokenTotal, fungibleToken).between(tokenTreasury, tokenOwner))) + .when( + cryptoApproveAllowance() + .addTokenAllowance(tokenOwner, fungibleToken, spender, 10L) + .fee(ONE_HUNDRED_HBARS) + .signedBy(tokenOwner) + .payingWith(tokenOwner), + getAccountDetails(tokenOwner) + .has(accountDetailsWith().tokenAllowancesContaining(fungibleToken, spender, 10L)), + cryptoTransfer(movingWithAllowance(1L, fungibleToken).between(tokenOwner, tokenReceiver)) + .fee(ONE_HUNDRED_HBARS) + .payingWith(spender) + .signedBy(spender)) + .then( + getAccountDetails(tokenOwner) + .has(accountDetailsWith().tokenAllowancesContaining(fungibleToken, spender, 9L)), + getAccountBalance(tokenOwner) + .hasTokenBalance(fungibleToken, 999) + .hasTokenBalance(feeDenom, tokenTotal - htsFee), + getAccountBalance(spender) + .hasTokenBalance(fungibleToken, 0) + .hasTokenBalance(feeDenom, 0), + getAccountBalance(tokenReceiver).hasTokenBalance(fungibleToken, 1), + getAccountBalance(htsCollector).hasTokenBalance(feeDenom, htsFee)); + } + + @HapiTest + public HapiSpec transferApprovedNonFungibleTokenWithFixedHbarCustomFee() { + return defaultHapiSpec("transferApprovedNonFungibleTokenWithFixedHbarCustomFee") + .given( + newKeyNamed(NFT_KEY), + cryptoCreate(hbarCollector).balance(0L), + cryptoCreate(tokenOwner).balance(ONE_MILLION_HBARS), + cryptoCreate(spender).balance(ONE_MILLION_HBARS), + cryptoCreate(tokenReceiver), + cryptoCreate(tokenTreasury), + tokenCreate(nonFungibleToken) + .treasury(tokenTreasury) + .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .supplyKey(NFT_KEY) + .supplyType(TokenSupplyType.INFINITE) + .initialSupply(0) + .withCustom(fixedHbarFee(hbarFee, hbarCollector)), + mintToken(nonFungibleToken, List.of(ByteStringUtils.wrapUnsafely("meta1".getBytes()))), + tokenAssociate(tokenReceiver, nonFungibleToken), + tokenAssociate(tokenOwner, nonFungibleToken), + tokenAssociate(spender, nonFungibleToken), + cryptoTransfer(movingUnique(nonFungibleToken, 1L).between(tokenTreasury, tokenOwner))) + .when( + cryptoApproveAllowance() + .addNftAllowance(tokenOwner, nonFungibleToken, spender, false, List.of(1L)) + .fee(ONE_HUNDRED_HBARS) + .payingWith(tokenOwner), + cryptoTransfer(movingUniqueWithAllowance(nonFungibleToken, 1L) + .between(tokenOwner, tokenReceiver)) + .fee(ONE_HUNDRED_HBARS) + .via("hbarFixedFee") + .payingWith(spender) + .signedBy(spender)) + .then(withOpContext((spec, log) -> { + final var record = getTxnRecord("hbarFixedFee"); + allRunFor(spec, record); + final var txFee = record.getResponseRecord().getTransactionFee(); + + getAccountBalance(tokenOwner) + .hasTinyBars(ONE_MILLION_HBARS - (txFee + hbarFee)) + .hasTokenBalance(nonFungibleToken, 0); + getAccountBalance(spender).hasTokenBalance(nonFungibleToken, 0); + getAccountBalance(tokenReceiver).hasTokenBalance(nonFungibleToken, 1); + getAccountBalance(hbarCollector).hasTinyBars(hbarFee); + })); + } + + @HapiTest + public HapiSpec transferApprovedNonFungibleTokenWithFixedHtsCustomFeesAsOwner() { + return defaultHapiSpec("transferApprovedNonFungibleTokenWithFixedHtsCustomFeesAsOwner") + .given( + newKeyNamed(NFT_KEY), + cryptoCreate(htsCollector), + cryptoCreate(tokenOwner).balance(ONE_MILLION_HBARS), + cryptoCreate(spender).balance(ONE_MILLION_HBARS), + cryptoCreate(tokenReceiver), + cryptoCreate(tokenTreasury), + tokenCreate(feeDenom).treasury(spender).initialSupply(tokenTotal), + tokenAssociate(htsCollector, feeDenom), + tokenAssociate(tokenOwner, feeDenom), + tokenCreate(nonFungibleToken) + .treasury(tokenTreasury) + .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .supplyKey(NFT_KEY) + .supplyType(TokenSupplyType.INFINITE) + .initialSupply(0) + .withCustom(fixedHtsFee(htsFee, feeDenom, htsCollector)), + mintToken(nonFungibleToken, List.of(ByteStringUtils.wrapUnsafely("meta1".getBytes()))), + tokenAssociate(tokenReceiver, nonFungibleToken), + tokenAssociate(tokenOwner, nonFungibleToken), + tokenAssociate(spender, nonFungibleToken), + cryptoTransfer(movingUnique(nonFungibleToken, 1L).between(tokenTreasury, tokenOwner)), + cryptoTransfer(moving(200L, feeDenom).between(spender, tokenOwner))) + .when( + cryptoApproveAllowance() + .addNftAllowance(tokenOwner, nonFungibleToken, spender, false, List.of(1L)) + .fee(ONE_HUNDRED_HBARS) + .payingWith(tokenOwner), + cryptoTransfer(movingUniqueWithAllowance(nonFungibleToken, 1L) + .between(tokenOwner, tokenReceiver)) + .fee(ONE_HUNDRED_HBARS) + .payingWith(spender) + .signedBy(spender)) + .then( + getAccountBalance(tokenOwner) + .hasTokenBalance(nonFungibleToken, 0) + .hasTokenBalance(feeDenom, 200L - htsFee), + getAccountBalance(spender) + .hasTokenBalance(nonFungibleToken, 0) + .hasTokenBalance(feeDenom, 800L), + getAccountBalance(tokenReceiver).hasTokenBalance(nonFungibleToken, 1), + getAccountBalance(htsCollector).hasTokenBalance(feeDenom, htsFee)); + } + + @HapiTest + public HapiSpec transferApprovedNonFungibleTokenWithFixedHtsCustomFeeAsSpender() { + return defaultHapiSpec("transferApprovedNonFungibleTokenWithFixedHtsCustomFeeAsSpender") + .given( + newKeyNamed(NFT_KEY), + cryptoCreate(htsCollector), + cryptoCreate(tokenOwner).balance(ONE_MILLION_HBARS), + cryptoCreate(spender).balance(ONE_MILLION_HBARS), + cryptoCreate(tokenReceiver), + cryptoCreate(tokenTreasury), + tokenCreate(feeDenom).treasury(tokenOwner).initialSupply(tokenTotal), + tokenAssociate(htsCollector, feeDenom), + tokenAssociate(spender, feeDenom), + tokenCreate(nonFungibleToken) + .treasury(tokenTreasury) + .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .supplyKey(NFT_KEY) + .supplyType(TokenSupplyType.INFINITE) + .initialSupply(0) + .withCustom(fixedHtsFee(htsFee, feeDenom, htsCollector)), + mintToken(nonFungibleToken, List.of(ByteStringUtils.wrapUnsafely("meta1".getBytes()))), + tokenAssociate(tokenReceiver, nonFungibleToken), + tokenAssociate(tokenOwner, nonFungibleToken), + tokenAssociate(spender, nonFungibleToken), + cryptoTransfer(movingUnique(nonFungibleToken, 1L).between(tokenTreasury, tokenOwner))) + .when( + cryptoApproveAllowance() + .addNftAllowance(tokenOwner, nonFungibleToken, spender, false, List.of(1L)) + .fee(ONE_HUNDRED_HBARS) + .payingWith(tokenOwner), + cryptoTransfer(movingUniqueWithAllowance(nonFungibleToken, 1L) + .between(tokenOwner, tokenReceiver)) + .fee(ONE_HUNDRED_HBARS) + .payingWith(spender) + .signedBy(spender)) + .then( + getAccountBalance(tokenOwner) + .hasTokenBalance(nonFungibleToken, 0) + .hasTokenBalance(feeDenom, tokenTotal - htsFee), + getAccountBalance(spender) + .hasTokenBalance(nonFungibleToken, 0) + .hasTokenBalance(feeDenom, 0), + getAccountBalance(tokenReceiver).hasTokenBalance(nonFungibleToken, 1), + getAccountBalance(htsCollector).hasTokenBalance(feeDenom, htsFee)); + } + + @HapiTest + public HapiSpec transferFungibleTokenWithThreeFixedHtsCustomFeesWithoutAllCollectorsExempt() { + final long amountToSend = 400L; + return defaultHapiSpec("transferFungibleTokenWithThreeFixedHtsCustomFeesWithoutAllCollectorsExempt") + .given( + cryptoCreate(alice).balance(0L), + cryptoCreate(bob).balance(0L), + cryptoCreate(carol).balance(0L), + cryptoCreate(tokenOwner).balance(ONE_MILLION_HBARS), + cryptoCreate(tokenReceiver), + cryptoCreate(tokenTreasury), + tokenCreate(feeDenom).treasury(tokenOwner).initialSupply(tokenTotal * 10L), + tokenAssociate(alice, feeDenom), + tokenAssociate(bob, feeDenom), + tokenAssociate(carol, feeDenom), + tokenCreate(fungibleToken) + .treasury(tokenTreasury) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(tokenTotal) + .withCustom(fixedHtsFee(aliceFee, feeDenom, alice)) + .withCustom(fixedHtsFee(bobFee, feeDenom, bob)) + .withCustom(fixedHtsFee(carolFee, feeDenom, carol)), + tokenAssociate(tokenReceiver, fungibleToken), + tokenAssociate(tokenOwner, fungibleToken), + tokenAssociate(alice, fungibleToken), + tokenAssociate(bob, fungibleToken), + tokenAssociate(carol, fungibleToken), + cryptoTransfer(moving(1000, feeDenom).between(tokenOwner, alice)), + cryptoTransfer(moving(1000, feeDenom).between(tokenOwner, bob)), + cryptoTransfer(moving(1000, feeDenom).between(tokenOwner, carol))) + .when( + cryptoTransfer(moving(amountToSend, fungibleToken).between(tokenTreasury, alice)), + cryptoTransfer(moving(amountToSend / 2, fungibleToken).between(alice, bob)), + cryptoTransfer(moving(amountToSend / 4, fungibleToken).between(bob, carol))) + .then( + getAccountBalance(alice) + .hasTokenBalance(fungibleToken, 200) + .hasTokenBalance(feeDenom, 600), + getAccountBalance(bob) + .hasTokenBalance(fungibleToken, 100) + .hasTokenBalance(feeDenom, 800), + getAccountBalance(carol) + .hasTokenBalance(fungibleToken, 100) + .hasTokenBalance(feeDenom, 1600)); + } + + @HapiTest + public HapiSpec transferFungibleTokenWithThreeFixedHtsCustomFeesWithAllCollectorsExempt() { + final long amountToSend = 400L; + return defaultHapiSpec("transferFungibleTokenWithThreeFixedHtsCustomFeesWithAllCollectorsExempt") + .given( + cryptoCreate(alice).balance(0L), + cryptoCreate(bob).balance(0L), + cryptoCreate(carol).balance(0L), + cryptoCreate(tokenOwner).balance(ONE_MILLION_HBARS), + cryptoCreate(tokenReceiver), + cryptoCreate(tokenTreasury), + tokenCreate(feeDenom).treasury(tokenOwner).initialSupply(tokenTotal * 10L), + tokenAssociate(alice, feeDenom), + tokenAssociate(bob, feeDenom), + tokenAssociate(carol, feeDenom), + tokenCreate(fungibleToken) + .treasury(tokenTreasury) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(tokenTotal) + .withCustom(fixedHtsFee(aliceFee, feeDenom, alice, true)) + .withCustom(fixedHtsFee(bobFee, feeDenom, bob, true)) + .withCustom(fixedHtsFee(carolFee, feeDenom, carol, true)), + tokenAssociate(tokenReceiver, fungibleToken), + tokenAssociate(tokenOwner, fungibleToken), + tokenAssociate(alice, fungibleToken), + tokenAssociate(bob, fungibleToken), + tokenAssociate(carol, fungibleToken), + cryptoTransfer(moving(1000, feeDenom).between(tokenOwner, alice)), + cryptoTransfer(moving(1000, feeDenom).between(tokenOwner, bob)), + cryptoTransfer(moving(1000, feeDenom).between(tokenOwner, carol))) + .when( + cryptoTransfer(moving(amountToSend, fungibleToken).between(tokenTreasury, alice)), + cryptoTransfer(moving(amountToSend / 2, fungibleToken).between(alice, bob)), + cryptoTransfer(moving(amountToSend / 4, fungibleToken).between(bob, carol))) + .then( + getAccountBalance(alice) + .hasTokenBalance(fungibleToken, 200) + .hasTokenBalance(feeDenom, 1000), + getAccountBalance(bob) + .hasTokenBalance(fungibleToken, 100) + .hasTokenBalance(feeDenom, 1000), + getAccountBalance(carol) + .hasTokenBalance(fungibleToken, 100) + .hasTokenBalance(feeDenom, 1000)); + } + + @HapiTest + public HapiSpec transferFungibleTokenWithFixedHtsCustomFees2Layers() { + return defaultHapiSpec("transferFungibleTokenWithFixedHtsCustomFees2Layers") + .given( + cryptoCreate(htsCollector), + cryptoCreate(htsCollector2), + cryptoCreate(tokenOwner).balance(ONE_MILLION_HBARS), + cryptoCreate(tokenReceiver), + cryptoCreate(tokenTreasury), + tokenCreate(feeDenom).treasury(tokenOwner).initialSupply(tokenTotal), + tokenAssociate(htsCollector, feeDenom), + tokenCreate(fungibleToken) + .treasury(tokenTreasury) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(tokenTotal) + .withCustom(fixedHtsFee(htsFee, feeDenom, htsCollector)), + tokenAssociate(htsCollector2, fungibleToken), + tokenCreate(fungibleToken2) + .treasury(tokenTreasury) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(tokenTotal) + .withCustom(fixedHtsFee(htsFee, fungibleToken, htsCollector2)), + tokenAssociate(tokenReceiver, fungibleToken), + tokenAssociate(tokenOwner, fungibleToken), + tokenAssociate(tokenReceiver, fungibleToken2), + tokenAssociate(tokenOwner, fungibleToken2), + cryptoTransfer( + moving(tokenTotal, fungibleToken2).between(tokenTreasury, tokenOwner), + moving(tokenTotal, fungibleToken).between(tokenTreasury, tokenOwner))) + .when(cryptoTransfer(moving(1, fungibleToken2).between(tokenOwner, tokenReceiver)) .fee(ONE_HUNDRED_HBARS) .payingWith(tokenOwner)) .then( getAccountBalance(tokenOwner) - .hasTokenBalance(token, 999) - .hasTokenBalance(feeDenom, 900), - getAccountBalance(hbarCollector).hasTinyBars(hbarFee)); + .hasTokenBalance(fungibleToken2, 999) + .hasTokenBalance(fungibleToken, tokenTotal - htsFee) + .hasTokenBalance(feeDenom, tokenTotal - htsFee), + getAccountBalance(tokenReceiver).hasTokenBalance(fungibleToken2, 1), + getAccountBalance(htsCollector).hasTokenBalance(feeDenom, htsFee), + getAccountBalance(htsCollector2).hasTokenBalance(fungibleToken, htsFee)); + } + + @HapiTest + public HapiSpec transferNonFungibleTokenWithFixedHtsCustomFees2Layers() { + return defaultHapiSpec("transferNonFungibleTokenWithFixedHtsCustomFees2Layers") + .given( + newKeyNamed(NFT_KEY), + cryptoCreate(htsCollector), + cryptoCreate(htsCollector2), + cryptoCreate(tokenOwner).balance(ONE_MILLION_HBARS), + cryptoCreate(tokenReceiver), + cryptoCreate(tokenTreasury), + tokenCreate(feeDenom).treasury(tokenOwner).initialSupply(tokenTotal), + tokenAssociate(htsCollector, feeDenom), + tokenCreate(fungibleToken) + .treasury(tokenTreasury) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(tokenTotal) + .withCustom(fixedHtsFee(htsFee, feeDenom, htsCollector)), + tokenAssociate(htsCollector2, fungibleToken), + tokenCreate(nonFungibleToken) + .treasury(tokenTreasury) + .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .supplyKey(NFT_KEY) + .supplyType(TokenSupplyType.INFINITE) + .initialSupply(0) + .withCustom(fixedHtsFee(htsFee, fungibleToken, htsCollector2)), + mintToken(nonFungibleToken, List.of(ByteStringUtils.wrapUnsafely("meta1".getBytes()))), + tokenAssociate(tokenReceiver, fungibleToken), + tokenAssociate(tokenOwner, fungibleToken), + tokenAssociate(tokenReceiver, nonFungibleToken), + tokenAssociate(tokenOwner, nonFungibleToken), + cryptoTransfer( + movingUnique(nonFungibleToken, 1L).between(tokenTreasury, tokenOwner), + moving(tokenTotal, fungibleToken).between(tokenTreasury, tokenOwner))) + .when(cryptoTransfer(movingUnique(nonFungibleToken, 1L).between(tokenOwner, tokenReceiver)) + .fee(ONE_HUNDRED_HBARS) + .payingWith(tokenOwner)) + .then( + getAccountBalance(tokenOwner) + .hasTokenBalance(nonFungibleToken, 0) + .hasTokenBalance(fungibleToken, tokenTotal - htsFee) + .hasTokenBalance(feeDenom, tokenTotal - htsFee), + getAccountBalance(tokenReceiver).hasTokenBalance(nonFungibleToken, 1), + getAccountBalance(htsCollector).hasTokenBalance(feeDenom, htsFee), + getAccountBalance(htsCollector2).hasTokenBalance(fungibleToken, htsFee)); + } + + @HapiTest + public HapiSpec transferMaxFungibleTokenWith10FixedHtsCustomFees2Layers() { + final String fungibleToken3 = "fungibleWithCustomFees3"; + final String fungibleToken4 = "fungibleWithCustomFees4"; + final String fungibleToken5 = "fungibleWithCustomFees5"; + final String fungibleToken6 = "fungibleWithCustomFees6"; + final String fungibleToken7 = "fungibleWithCustomFees7"; + final String fungibleToken8 = "fungibleWithCustomFees8"; + final String fungibleToken9 = "fungibleWithCustomFees9"; + final String fungibleToken10 = "fungibleWithCustomFees10"; + final String fungibleToken11 = "fungibleWithCustomFees11"; + final String fungibleToken12 = "fungibleWithCustomFees12"; + final String fungibleToken13 = "fungibleWithCustomFees13"; + final String fungibleToken14 = "fungibleWithCustomFees14"; + final String fungibleToken15 = "fungibleWithCustomFees15"; + final String fungibleToken16 = "fungibleWithCustomFees16"; + final String fungibleToken17 = "fungibleWithCustomFees17"; + final String fungibleToken18 = "fungibleWithCustomFees18"; + final String fungibleToken19 = "fungibleWithCustomFees19"; + final String fungibleToken20 = "fungibleWithCustomFees20"; + final var specificTokenTotal = 2 * tokenTotal; + + List firstLayerCustomFees = List.of( + fungibleToken2, + fungibleToken3, + fungibleToken4, + fungibleToken5, + fungibleToken6, + fungibleToken7, + fungibleToken8, + fungibleToken9, + fungibleToken10); + List secondLayerCustomFees = List.of( + fungibleToken11, + fungibleToken12, + fungibleToken13, + fungibleToken14, + fungibleToken15, + fungibleToken16, + fungibleToken17, + fungibleToken18, + fungibleToken19, + fungibleToken20); + + return defaultHapiSpec("transferMaxFungibleTokenWith10FixedHtsCustomFees2Layers") + .given(withOpContext((spec, log) -> { + ArrayList ops = new ArrayList<>(); + var collectorCreate = cryptoCreate(htsCollector); + var collector2Create = cryptoCreate(htsCollector2); + var tokenOwnerCreate = cryptoCreate(tokenOwner).balance(ONE_MILLION_HBARS); + var tokenReceiverCreate = cryptoCreate(tokenReceiver); + var tokenTreasuryCreate = cryptoCreate(tokenTreasury); + allRunFor( + spec, + collectorCreate, + collector2Create, + tokenOwnerCreate, + tokenReceiverCreate, + tokenTreasuryCreate); + + // create all second layer custom fee hts tokens + for (String secondLayerCustomFee : secondLayerCustomFees) { + ops.add(tokenCreate(secondLayerCustomFee) + .treasury(tokenOwner) + .initialSupply(specificTokenTotal)); + ops.add(tokenAssociate(htsCollector, secondLayerCustomFee)); + } + // create all first layer custom fee hts tokens + for (String firstLayerCustomFee : firstLayerCustomFees) { + ops.add(tokenCreate(firstLayerCustomFee) + .treasury(tokenOwner) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(specificTokenTotal) + .withCustom(fixedHtsFee(htsFee, fungibleToken11, htsCollector)) + .withCustom(fixedHtsFee(htsFee, fungibleToken12, htsCollector)) + .withCustom(fixedHtsFee(htsFee, fungibleToken13, htsCollector)) + .withCustom(fixedHtsFee(htsFee, fungibleToken14, htsCollector)) + .withCustom(fixedHtsFee(htsFee, fungibleToken15, htsCollector)) + .withCustom(fixedHtsFee(htsFee, fungibleToken16, htsCollector)) + .withCustom(fixedHtsFee(htsFee, fungibleToken17, htsCollector)) + .withCustom(fixedHtsFee(htsFee, fungibleToken18, htsCollector)) + .withCustom(fixedHtsFee(htsFee, fungibleToken19, htsCollector)) + .withCustom(fixedHtsFee(htsFee, fungibleToken20, htsCollector))); + + ops.add(tokenAssociate(htsCollector2, firstLayerCustomFee)); + } + allRunFor(spec, ops); + + var fungibleToTransfer = tokenCreate(fungibleToken) + .treasury(tokenTreasury) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(9223372036854775807L) + .withCustom(fixedHtsFee(htsFee, fungibleToken2, htsCollector2)) + .withCustom(fixedHtsFee(htsFee, fungibleToken3, htsCollector2)) + .withCustom(fixedHtsFee(htsFee, fungibleToken4, htsCollector2)) + .withCustom(fixedHtsFee(htsFee, fungibleToken5, htsCollector2)) + .withCustom(fixedHtsFee(htsFee, fungibleToken6, htsCollector2)) + .withCustom(fixedHtsFee(htsFee, fungibleToken7, htsCollector2)) + .withCustom(fixedHtsFee(htsFee, fungibleToken8, htsCollector2)) + .withCustom(fixedHtsFee(htsFee, fungibleToken9, htsCollector2)) + .withCustom(fixedHtsFee(htsFee, fungibleToken10, htsCollector2)); + var ownerAssociate = tokenAssociate(tokenOwner, fungibleToken); + var receiverAssociate = tokenAssociate(tokenReceiver, fungibleToken); + var transferToOwner = cryptoTransfer( + moving(9223372036854775807L, fungibleToken).between(tokenTreasury, tokenOwner)); + allRunFor(spec, fungibleToTransfer, ownerAssociate, receiverAssociate, transferToOwner); + })) + .when(cryptoTransfer(moving(1, fungibleToken).between(tokenOwner, tokenReceiver)) + .fee(ONE_HUNDRED_HBARS) + .payingWith(tokenOwner)) + .then( + getAccountBalance(tokenOwner) + .hasTokenBalance(fungibleToken, 9223372036854775806L) + .hasTokenBalance(fungibleToken2, specificTokenTotal - htsFee) + .hasTokenBalance(fungibleToken3, specificTokenTotal - htsFee) + .hasTokenBalance(fungibleToken4, specificTokenTotal - htsFee) + .hasTokenBalance(fungibleToken5, specificTokenTotal - htsFee) + .hasTokenBalance(fungibleToken6, specificTokenTotal - htsFee) + .hasTokenBalance(fungibleToken7, specificTokenTotal - htsFee) + .hasTokenBalance(fungibleToken8, specificTokenTotal - htsFee) + .hasTokenBalance(fungibleToken9, specificTokenTotal - htsFee) + .hasTokenBalance(fungibleToken10, specificTokenTotal - htsFee), + getAccountBalance(tokenReceiver).hasTokenBalance(fungibleToken, 1), + getAccountBalance(htsCollector2) + .hasTokenBalance(fungibleToken2, htsFee) + .hasTokenBalance(fungibleToken3, htsFee) + .hasTokenBalance(fungibleToken4, htsFee) + .hasTokenBalance(fungibleToken5, htsFee) + .hasTokenBalance(fungibleToken6, htsFee) + .hasTokenBalance(fungibleToken7, htsFee) + .hasTokenBalance(fungibleToken8, htsFee) + .hasTokenBalance(fungibleToken9, htsFee) + .hasTokenBalance(fungibleToken10, htsFee)); + } + + @HapiTest + public HapiSpec multipleTransfersWithMultipleCustomFees() { + return defaultHapiSpec("multipleTransfersWithMultipleCustomFees") + .given( + newKeyNamed(NFT_KEY), + cryptoCreate(hbarCollector).balance(0L), + cryptoCreate(htsCollector).balance(ONE_MILLION_HBARS), + cryptoCreate(htsCollector2), + cryptoCreate(tokenOwner).balance(ONE_MILLION_HBARS), + cryptoCreate(tokenTreasury), + cryptoCreate(alice).balance(ONE_MILLION_HBARS), + cryptoCreate(bob).balance(ONE_MILLION_HBARS), + cryptoCreate(carol), + tokenCreate(feeDenom).treasury(tokenOwner).initialSupply(tokenTotal), + tokenCreate(feeDenom2).treasury(tokenOwner).initialSupply(tokenTotal), + tokenAssociate(htsCollector, feeDenom), + tokenAssociate(htsCollector2, feeDenom2), + tokenAssociate(alice, feeDenom), + tokenAssociate(alice, feeDenom2), + tokenAssociate(bob, feeDenom), + tokenAssociate(bob, feeDenom2), + tokenCreate(fungibleToken) + .treasury(tokenTreasury) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(tokenTotal) + .payingWith(htsCollector) + .withCustom(fixedHbarFee(hbarFee, hbarCollector)) + .withCustom(fixedHtsFee(htsFee, feeDenom, htsCollector)) + .withCustom(fractionalFee( + numerator, denominator, minHtsFee, OptionalLong.of(maxHtsFee), htsCollector)), + tokenAssociate(alice, fungibleToken), + tokenAssociate(bob, fungibleToken), + tokenCreate(nonFungibleToken) + .treasury(tokenTreasury) + .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .supplyKey(NFT_KEY) + .supplyType(TokenSupplyType.INFINITE) + .initialSupply(0) + .withCustom(fixedHtsFee(50L, feeDenom, htsCollector)) + .withCustom(fixedHtsFee(htsFee, feeDenom2, htsCollector2)), + mintToken(nonFungibleToken, List.of(ByteStringUtils.wrapUnsafely("meta1".getBytes()))), + tokenAssociate(alice, nonFungibleToken), + tokenAssociate(bob, nonFungibleToken), + tokenAssociate(carol, nonFungibleToken), + cryptoTransfer( + moving(tokenTotal, feeDenom).between(tokenOwner, alice), + moving(tokenTotal, feeDenom2).between(tokenOwner, alice), + moving(tokenTotal, fungibleToken).between(tokenTreasury, alice), + movingUnique(nonFungibleToken, 1L).between(tokenTreasury, alice)), + // make 2 transfers - one with the same HTS token as custom fee and one with different HTS token + // as custom fee + cryptoTransfer(moving(10L, fungibleToken).between(alice, bob)) + .fee(ONE_HUNDRED_HBARS) + .payingWith(alice), + cryptoTransfer(movingUnique(nonFungibleToken, 1L).between(alice, bob)) + .fee(ONE_HUNDRED_HBARS) + .payingWith(alice), + // check balances + getAccountBalance(alice) + .hasTokenBalance(fungibleToken, tokenTotal - 10L) + .hasTokenBalance(nonFungibleToken, 0) + .hasTokenBalance(feeDenom, tokenTotal - htsFee - 50L) + .hasTokenBalance(feeDenom2, tokenTotal - htsFee), + getAccountBalance(bob) + .hasTokenBalance(fungibleToken, 8L) + .hasTokenBalance(nonFungibleToken, 1) + .hasTokenBalance(feeDenom, 0) + .hasTokenBalance(feeDenom2, 0), + getAccountBalance(hbarCollector).hasTinyBars(hbarFee), + getAccountBalance(htsCollector).hasTokenBalance(feeDenom, htsFee + 50L), + getAccountBalance(htsCollector2).hasTokenBalance(feeDenom2, htsFee)) + .when( + // transfer some hts custom fee tokens to bob as he is a sender and needs to pay with them + cryptoTransfer( + moving(100L, feeDenom).between(alice, bob), + moving(200L, feeDenom2).between(alice, bob)), + // make 2 transfers in a single tx with different senders and receivers + cryptoTransfer( + moving(10L, fungibleToken).between(alice, bob), + movingUnique(nonFungibleToken, 1L).between(bob, carol)) + .fee(ONE_HUNDRED_HBARS) + .signedBy(alice, bob) + .payingWith(alice)) + .then( + // check balances + getAccountBalance(alice) + .hasTokenBalance(fungibleToken, tokenTotal - 2 * 10L) + .hasTokenBalance(nonFungibleToken, 0) + .hasTokenBalance(feeDenom, tokenTotal - 2 * htsFee - 50L - 100L) + .hasTokenBalance(feeDenom2, tokenTotal - htsFee - 200L), + getAccountBalance(bob) + .hasTokenBalance(fungibleToken, 16L) + .hasTokenBalance(nonFungibleToken, 0) + .hasTokenBalance(feeDenom, 50L) + .hasTokenBalance(feeDenom2, 100L), + getAccountBalance(carol) + .hasTokenBalance(fungibleToken, 0L) + .hasTokenBalance(nonFungibleToken, 1) + .hasTokenBalance(feeDenom, 0) + .hasTokenBalance(feeDenom2, 0), + getAccountBalance(hbarCollector).hasTinyBars(2 * hbarFee), + getAccountBalance(htsCollector).hasTokenBalance(feeDenom, 2 * htsFee + 2 * 50L), + getAccountBalance(htsCollector2).hasTokenBalance(feeDenom2, 2 * htsFee)); } @HapiTest - public HapiSpec transferWithFractinalCustomFeeSchedule() { - return defaultHapiSpec("transferWithCustomFeeScheduleHappyPath") + public HapiSpec transferWithFractionalCustomFee() { + return defaultHapiSpec("transferWithFractionalCustomFee") .given( cryptoCreate(htsCollector).balance(ONE_HUNDRED_HBARS), cryptoCreate(hbarCollector).balance(0L), @@ -113,29 +954,29 @@ public HapiSpec transferWithFractinalCustomFeeSchedule() { cryptoCreate(tokenOwner).balance(ONE_MILLION_HBARS), tokenCreate(feeDenom).treasury(tokenOwner).initialSupply(tokenTotal), tokenAssociate(htsCollector, feeDenom), - tokenCreate(token) + tokenCreate(fungibleToken) .treasury(tokenTreasury) .initialSupply(tokenTotal) .payingWith(htsCollector) .withCustom(fixedHbarFee(hbarFee, hbarCollector)) .withCustom(fractionalFee( numerator, denominator, minHtsFee, OptionalLong.of(maxHtsFee), htsCollector)), - tokenAssociate(tokenReceiver, token), - tokenAssociate(tokenOwner, token), - cryptoTransfer(moving(tokenTotal, token).between(tokenTreasury, tokenOwner))) - .when(cryptoTransfer(moving(3, token).between(tokenOwner, tokenReceiver)) + tokenAssociate(tokenReceiver, fungibleToken), + tokenAssociate(tokenOwner, fungibleToken), + cryptoTransfer(moving(tokenTotal, fungibleToken).between(tokenTreasury, tokenOwner))) + .when(cryptoTransfer(moving(3, fungibleToken).between(tokenOwner, tokenReceiver)) .fee(ONE_HUNDRED_HBARS) .payingWith(tokenOwner)) .then( getAccountBalance(tokenOwner) - .hasTokenBalance(token, 997) + .hasTokenBalance(fungibleToken, 997) .hasTokenBalance(feeDenom, tokenTotal), getAccountBalance(hbarCollector).hasTinyBars(hbarFee)); } @HapiTest public HapiSpec transferWithInsufficientCustomFees() { - return defaultHapiSpec("transferWithFixedCustomFeeSchedule") + return defaultHapiSpec("transferWithInsufficientCustomFees") .given( cryptoCreate(htsCollector), cryptoCreate(hbarCollector).balance(0L), @@ -144,15 +985,15 @@ public HapiSpec transferWithInsufficientCustomFees() { cryptoCreate(tokenOwner).balance(ONE_MILLION_HBARS), tokenCreate(feeDenom).treasury(tokenOwner).initialSupply(10), tokenAssociate(htsCollector, feeDenom), - tokenCreate(token) + tokenCreate(fungibleToken) .treasury(tokenTreasury) .initialSupply(tokenTotal) .withCustom(fixedHtsFee(htsFee, feeDenom, htsCollector)), - tokenAssociate(tokenReceiver, token), - tokenAssociate(tokenOwner, token), - cryptoTransfer(moving(tokenTotal, token).between(tokenTreasury, tokenOwner))) + tokenAssociate(tokenReceiver, fungibleToken), + tokenAssociate(tokenOwner, fungibleToken), + cryptoTransfer(moving(tokenTotal, fungibleToken).between(tokenTreasury, tokenOwner))) .when() - .then(cryptoTransfer(moving(1, token).between(tokenOwner, tokenReceiver)) + .then(cryptoTransfer(moving(1, fungibleToken).between(tokenOwner, tokenReceiver)) .fee(ONE_HUNDRED_HBARS) .payingWith(tokenOwner) .hasKnownStatus(ResponseCodeEnum.INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE)); From 128974b28c39aefe24a67e5427115065a11b777b Mon Sep 17 00:00:00 2001 From: Kim Rader Date: Tue, 12 Mar 2024 09:37:09 -0700 Subject: [PATCH 074/115] fix: use Bytes instead of String for SCHEDULES_BY_EQUALITY_KEY (#11995) (#12030) Signed-off-by: Kim Rader --- .../impl/ReadableScheduleStoreImpl.java | 9 +- .../schedule/impl/ScheduleStoreUtility.java | 13 +- .../impl/WritableScheduleStoreImpl.java | 8 +- .../InitialModServiceScheduleSchema.java | 128 ++++++++---------- .../impl/ScheduleStoreUtilityTest.java | 105 +++++++------- .../schedule/impl/ScheduleTestBase.java | 25 +--- .../schedule/ReadableScheduleStore.java | 6 +- 7 files changed, 123 insertions(+), 171 deletions(-) diff --git a/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/ReadableScheduleStoreImpl.java b/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/ReadableScheduleStoreImpl.java index 0e01d3e92bc2..6f3638a156a3 100644 --- a/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/ReadableScheduleStoreImpl.java +++ b/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/ReadableScheduleStoreImpl.java @@ -17,13 +17,14 @@ package com.hedera.node.app.service.schedule.impl; import com.hedera.hapi.node.base.ScheduleID; +import com.hedera.hapi.node.state.primitives.ProtoBytes; import com.hedera.hapi.node.state.primitives.ProtoLong; -import com.hedera.hapi.node.state.primitives.ProtoString; import com.hedera.hapi.node.state.schedule.Schedule; import com.hedera.hapi.node.state.schedule.ScheduleList; import com.hedera.node.app.service.schedule.ReadableScheduleStore; import com.hedera.node.app.spi.state.ReadableKVState; import com.hedera.node.app.spi.state.ReadableStates; +import com.hedera.pbj.runtime.io.buffer.Bytes; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.util.List; @@ -40,7 +41,7 @@ public class ReadableScheduleStoreImpl implements ReadableScheduleStore { private final ReadableKVState schedulesById; private final ReadableKVState schedulesByExpirationSecond; - private final ReadableKVState schedulesByStringHash; + private final ReadableKVState schedulesByStringHash; /** * Create a new {@link ReadableScheduleStore} instance. @@ -71,8 +72,8 @@ public Schedule get(@Nullable final ScheduleID id) { @Override @Nullable public List getByEquality(final @NonNull Schedule scheduleToMatch) { - String stringHash = ScheduleStoreUtility.calculateStringHash(scheduleToMatch); - final ScheduleList inStateValue = schedulesByStringHash.get(new ProtoString(stringHash)); + Bytes bytesHash = ScheduleStoreUtility.calculateBytesHash(scheduleToMatch); + final ScheduleList inStateValue = schedulesByStringHash.get(new ProtoBytes(bytesHash)); return inStateValue != null ? inStateValue.schedules() : null; } diff --git a/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/ScheduleStoreUtility.java b/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/ScheduleStoreUtility.java index 6eaa8433a25d..b170fcd8692f 100644 --- a/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/ScheduleStoreUtility.java +++ b/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/ScheduleStoreUtility.java @@ -18,10 +18,10 @@ import com.google.common.hash.Hasher; import com.google.common.hash.Hashing; -import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.Key; import com.hedera.hapi.node.scheduled.SchedulableTransactionBody; import com.hedera.hapi.node.state.schedule.Schedule; +import com.hedera.pbj.runtime.io.buffer.Bytes; import edu.umd.cs.findbugs.annotations.NonNull; import java.nio.charset.StandardCharsets; import java.util.Objects; @@ -32,7 +32,7 @@ private ScheduleStoreUtility() {} // @todo('7773') This requires rebuilding the equality virtual map on migration, // because it's different from ScheduleVirtualValue (and must be, due to PBJ shift) @SuppressWarnings("UnstableApiUsage") - public static String calculateStringHash(@NonNull final Schedule scheduleToHash) { + public static Bytes calculateBytesHash(@NonNull final Schedule scheduleToHash) { Objects.requireNonNull(scheduleToHash); final Hasher hasher = Hashing.sha256().newHasher(); if (scheduleToHash.memo() != null) { @@ -49,7 +49,7 @@ public static String calculateStringHash(@NonNull final Schedule scheduleToHash) // differential testing completes hasher.putLong(scheduleToHash.providedExpirationSecond()); hasher.putBoolean(scheduleToHash.waitForExpiry()); - return hasher.hash().toString(); + return Bytes.wrap(hasher.hash().asBytes()); } @SuppressWarnings("UnstableApiUsage") @@ -59,13 +59,6 @@ private static void addToHash(final Hasher hasher, final Key keyToAdd) { hasher.putBytes(keyBytes); } - @SuppressWarnings("UnstableApiUsage") - private static void addToHash(final Hasher hasher, final AccountID accountToAdd) { - final byte[] accountIdBytes = AccountID.PROTOBUF.toBytes(accountToAdd).toByteArray(); - hasher.putInt(accountIdBytes.length); - hasher.putBytes(accountIdBytes); - } - @SuppressWarnings("UnstableApiUsage") private static void addToHash(final Hasher hasher, final SchedulableTransactionBody transactionToAdd) { final byte[] bytes = diff --git a/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/WritableScheduleStoreImpl.java b/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/WritableScheduleStoreImpl.java index 266f0c8e3f53..32a7fe265e6e 100644 --- a/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/WritableScheduleStoreImpl.java +++ b/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/WritableScheduleStoreImpl.java @@ -20,8 +20,8 @@ import com.hedera.hapi.node.base.ScheduleID; import com.hedera.hapi.node.base.Timestamp; +import com.hedera.hapi.node.state.primitives.ProtoBytes; import com.hedera.hapi.node.state.primitives.ProtoLong; -import com.hedera.hapi.node.state.primitives.ProtoString; import com.hedera.hapi.node.state.schedule.Schedule; import com.hedera.hapi.node.state.schedule.ScheduleList; import com.hedera.node.app.service.schedule.WritableScheduleStore; @@ -49,7 +49,7 @@ public class WritableScheduleStoreImpl extends ReadableScheduleStoreImpl impleme private static final String SCHEDULE_MISSING_FOR_DELETE_MESSAGE = "Schedule to be deleted, %1$s, not found in state."; private final WritableKVState schedulesByIdMutable; - private final WritableKVState schedulesByEqualityMutable; + private final WritableKVState schedulesByEqualityMutable; private final WritableKVState schedulesByExpirationMutable; /** @@ -107,7 +107,7 @@ public Schedule getForModify(@Nullable final ScheduleID idToFind) { @Override public void put(@NonNull final Schedule scheduleToAdd) { schedulesByIdMutable.put(scheduleToAdd.scheduleIdOrThrow(), scheduleToAdd); - final ProtoString newHash = new ProtoString(ScheduleStoreUtility.calculateStringHash(scheduleToAdd)); + final ProtoBytes newHash = new ProtoBytes(ScheduleStoreUtility.calculateBytesHash(scheduleToAdd)); final ScheduleList inStateEquality = schedulesByEqualityMutable.get(newHash); List byEquality = inStateEquality != null ? new LinkedList<>(inStateEquality.schedulesOrElse(emptyList())) : null; @@ -159,7 +159,7 @@ public void purgeExpiredSchedulesBetween(long firstSecondToExpire, long lastSeco for (final var schedule : scheduleList.schedules()) { schedulesByIdMutable.remove(schedule.scheduleIdOrThrow()); - final ProtoString hash = new ProtoString(ScheduleStoreUtility.calculateStringHash(schedule)); + final ProtoBytes hash = new ProtoBytes(ScheduleStoreUtility.calculateBytesHash(schedule)); schedulesByEqualityMutable.remove(hash); logger.info("Purging expired schedule {} from state.", schedule.scheduleIdOrThrow()); } diff --git a/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/schemas/InitialModServiceScheduleSchema.java b/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/schemas/InitialModServiceScheduleSchema.java index 2af8a3ae2867..f9073e6e097a 100644 --- a/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/schemas/InitialModServiceScheduleSchema.java +++ b/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/schemas/InitialModServiceScheduleSchema.java @@ -22,14 +22,11 @@ import com.hedera.hapi.node.base.ScheduleID; import com.hedera.hapi.node.base.SemanticVersion; +import com.hedera.hapi.node.state.primitives.ProtoBytes; import com.hedera.hapi.node.state.primitives.ProtoLong; -import com.hedera.hapi.node.state.primitives.ProtoString; import com.hedera.hapi.node.state.schedule.Schedule; import com.hedera.hapi.node.state.schedule.ScheduleList; import com.hedera.node.app.service.mono.state.merkle.MerkleScheduledTransactions; -import com.hedera.node.app.service.mono.state.submerkle.RichInstant; -import com.hedera.node.app.service.mono.state.virtual.schedule.ScheduleSecondVirtualValue; -import com.hedera.node.app.service.mono.state.virtual.temporal.SecondSinceEpocVirtualKey; import com.hedera.node.app.service.schedule.impl.ScheduleStoreUtility; import com.hedera.node.app.service.schedule.impl.codec.ScheduleServiceStateTranslator; import com.hedera.node.app.spi.state.MigrationContext; @@ -48,7 +45,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.eclipse.collections.api.block.procedure.primitive.LongProcedure; -import org.eclipse.collections.api.list.primitive.ImmutableLongList; /** * General schema for the schedule service @@ -99,82 +95,68 @@ public void migrate(@NonNull final MigrationContext ctx) { throw new RuntimeException(e); } }); - if (schedulesById.isModified()) ((WritableKVStateBase) schedulesById).commit(); + if (schedulesById.isModified()) ((WritableKVStateBase) schedulesById).commit(); log.info("BBM: finished schedule by id migration"); log.info("BBM: doing schedule by expiration migration"); final WritableKVState schedulesByExpiration = ctx.newStates().get(SCHEDULES_BY_EXPIRY_SEC_KEY); - fs.byExpirationSecond() - .forEachNode(new BiConsumer() { - @Override - public void accept( - SecondSinceEpocVirtualKey secondSinceEpocVirtualKey, ScheduleSecondVirtualValue sVv) { - sVv.getIds().forEach(new BiConsumer() { - @Override - public void accept(RichInstant richInstant, ImmutableLongList scheduleIds) { - - List schedules = new ArrayList<>(); - scheduleIds.forEach(new LongProcedure() { - @Override - public void value(long scheduleId) { - var schedule = schedulesById.get(ScheduleID.newBuilder() - .scheduleNum(scheduleId) - .build()); - if (schedule != null) schedules.add(schedule); - else { - log.info("BBM: ERROR: no schedule for expiration->id " - + richInstant - + " -> " - + scheduleId); - } - } - }); - - schedulesByExpiration.put( - ProtoLong.newBuilder() - .value(secondSinceEpocVirtualKey.getKeyAsLong()) - .build(), - ScheduleList.newBuilder() - .schedules(schedules) - .build()); - } - }); - } - }); - if (schedulesByExpiration.isModified()) ((WritableKVStateBase) schedulesByExpiration).commit(); + fs.byExpirationSecond().forEachNode((secondSinceEpocVirtualKey, sVv) -> sVv.getIds() + .forEach((richInstant, scheduleIds) -> { + List schedules = new ArrayList<>(); + scheduleIds.forEach((LongProcedure) scheduleId -> { + var schedule = schedulesById.get(ScheduleID.newBuilder() + .scheduleNum(scheduleId) + .build()); + if (schedule != null) schedules.add(schedule); + else { + log.info("BBM: ERROR: no schedule for expiration->id " + + richInstant + + " -> " + + scheduleId); + } + }); + + schedulesByExpiration.put( + ProtoLong.newBuilder() + .value(secondSinceEpocVirtualKey.getKeyAsLong()) + .build(), + ScheduleList.newBuilder().schedules(schedules).build()); + })); + if (schedulesByExpiration.isModified()) ((WritableKVStateBase) schedulesByExpiration).commit(); log.info("BBM: finished schedule by expiration migration"); log.info("BBM: doing schedule by equality migration"); - final WritableKVState schedulesByEquality = + final WritableKVState schedulesByEquality = ctx.newStates().get(SCHEDULES_BY_EQUALITY_KEY); - fs.byEquality().forEachNode((scheduleEqualityVirtualKey, sevv) -> { - sevv.getIds().forEach(new BiConsumer() { - @Override - public void accept(String scheduleObjHash, Long scheduleId) { - var schedule = schedulesById.get( - ScheduleID.newBuilder().scheduleNum(scheduleId).build()); - if (schedule != null) { - final var equalityKey = new ProtoString(ScheduleStoreUtility.calculateStringHash(schedule)); - final var existingList = schedulesByEquality.get(equalityKey); - final List existingSchedules = existingList == null - ? new ArrayList<>() - : new ArrayList<>(existingList.schedulesOrElse(Collections.emptyList())); - existingSchedules.add(schedule); - schedulesByEquality.put( - equalityKey, - ScheduleList.newBuilder() - .schedules(existingSchedules) - .build()); - } else { - log.error("BBM: ERROR: no schedule for scheduleObjHash->id " - + scheduleObjHash + " -> " - + scheduleId); + fs.byEquality().forEachNode((scheduleEqualityVirtualKey, sevv) -> sevv.getIds() + .forEach(new BiConsumer() { + @Override + public void accept(String scheduleObjHash, Long scheduleId) { + var schedule = schedulesById.get(ScheduleID.newBuilder() + .scheduleNum(scheduleId) + .build()); + if (schedule != null) { + final var equalityKey = + new ProtoBytes(ScheduleStoreUtility.calculateBytesHash(schedule)); + final var existingList = schedulesByEquality.get(equalityKey); + final List existingSchedules = existingList == null + ? new ArrayList<>() + : new ArrayList<>(existingList.schedulesOrElse(Collections.emptyList())); + existingSchedules.add(schedule); + schedulesByEquality.put( + equalityKey, + ScheduleList.newBuilder() + .schedules(existingSchedules) + .build()); + } else { + log.error("BBM: ERROR: no schedule for scheduleObjHash->id " + + scheduleObjHash + " -> " + + scheduleId); + } } - } - }); - }); - if (schedulesByEquality.isModified()) ((WritableKVStateBase) schedulesByEquality).commit(); + })); + if (schedulesByEquality.isModified()) ((WritableKVStateBase) schedulesByEquality).commit(); log.info("BBM: finished schedule by equality migration"); log.info("BBM: finished schedule service migration migration"); @@ -198,8 +180,8 @@ private static StateDefinition schedulesByExpirySec() { MAX_SCHEDULES_BY_EXPIRY_SEC_KEY); } - private static StateDefinition schedulesByEquality() { + private static StateDefinition schedulesByEquality() { return StateDefinition.onDisk( - SCHEDULES_BY_EQUALITY_KEY, ProtoString.PROTOBUF, ScheduleList.PROTOBUF, MAX_SCHEDULES_BY_EQUALITY); + SCHEDULES_BY_EQUALITY_KEY, ProtoBytes.PROTOBUF, ScheduleList.PROTOBUF, MAX_SCHEDULES_BY_EQUALITY); } } diff --git a/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/ScheduleStoreUtilityTest.java b/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/ScheduleStoreUtilityTest.java index 4f3562b4032a..316efa10199a 100644 --- a/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/ScheduleStoreUtilityTest.java +++ b/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/ScheduleStoreUtilityTest.java @@ -21,6 +21,7 @@ import com.hedera.hapi.node.base.ScheduleID; import com.hedera.hapi.node.state.schedule.Schedule; import com.hedera.node.app.spi.workflows.PreCheckException; +import com.hedera.pbj.runtime.io.buffer.Bytes; import java.security.InvalidKeyException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -35,107 +36,97 @@ void setUp() throws PreCheckException, InvalidKeyException { setUpBase(); } - @Test - void verifyHashCalculationNormalFunction() { - final String hashValue = ScheduleStoreUtility.calculateStringHash(scheduleInState); - assertThat(hashValue).isEqualTo(SCHEDULE_IN_STATE_SHA256); - assertThat(hashValue).isNotEqualTo(SCHEDULE_IN_STATE_0_EXPIRE_SHA256); - } - @Test void verifyIncludedFieldsChangeHash() { Schedule.Builder testSchedule = scheduleInState.copyBuilder(); - String hashValue = ScheduleStoreUtility.calculateStringHash(scheduleInState); - assertThat(hashValue).isEqualTo(SCHEDULE_IN_STATE_SHA256); - assertThat(hashValue).isNotEqualTo(SCHEDULE_IN_STATE_0_EXPIRE_SHA256); + Bytes origHashValue = ScheduleStoreUtility.calculateBytesHash(scheduleInState); + // change the expiration time and verify that hash changes testSchedule.providedExpirationSecond(0L); - hashValue = ScheduleStoreUtility.calculateStringHash(testSchedule.build()); - assertThat(hashValue).isNotEqualTo(SCHEDULE_IN_STATE_SHA256); - assertThat(hashValue).isEqualTo(SCHEDULE_IN_STATE_0_EXPIRE_SHA256); + Bytes hashValue = ScheduleStoreUtility.calculateBytesHash(testSchedule.build()); + assertThat(hashValue).isNotEqualTo(origHashValue); testSchedule.providedExpirationSecond(scheduleInState.providedExpirationSecond()); + hashValue = ScheduleStoreUtility.calculateBytesHash(testSchedule.build()); + assertThat(hashValue).isEqualTo(origHashValue); + // change the admin key and verify that hash changes testSchedule.adminKey(payerKey); - hashValue = ScheduleStoreUtility.calculateStringHash(testSchedule.build()); - assertThat(hashValue).isNotEqualTo(SCHEDULE_IN_STATE_SHA256); - assertThat(hashValue).isEqualTo(SCHEDULE_IN_STATE_PAYER_IS_ADMIN_SHA256); + hashValue = ScheduleStoreUtility.calculateBytesHash(testSchedule.build()); + assertThat(hashValue).isNotEqualTo(origHashValue); testSchedule.adminKey(scheduleInState.adminKey()); + hashValue = ScheduleStoreUtility.calculateBytesHash(testSchedule.build()); + assertThat(hashValue).isEqualTo(origHashValue); + // change the scheduled transaction and verify that hash changes testSchedule.scheduledTransaction(createAlternateScheduled()); - hashValue = ScheduleStoreUtility.calculateStringHash(testSchedule.build()); - assertThat(hashValue).isNotEqualTo(SCHEDULE_IN_STATE_SHA256); - assertThat(hashValue).isEqualTo(SCHEDULE_IN_STATE_ALTERNATE_SCHEDULED_SHA256); + hashValue = ScheduleStoreUtility.calculateBytesHash(testSchedule.build()); + assertThat(hashValue).isNotEqualTo(origHashValue); testSchedule.scheduledTransaction(scheduleInState.scheduledTransaction()); + hashValue = ScheduleStoreUtility.calculateBytesHash(testSchedule.build()); + assertThat(hashValue).isEqualTo(origHashValue); + // change the memo and verify that hash changes testSchedule.memo(ODD_MEMO); - hashValue = ScheduleStoreUtility.calculateStringHash(testSchedule.build()); - assertThat(hashValue).isNotEqualTo(SCHEDULE_IN_STATE_SHA256); - assertThat(hashValue).isEqualTo(SCHEDULE_IN_STATE_ODD_MEMO_SHA256); + hashValue = ScheduleStoreUtility.calculateBytesHash(testSchedule.build()); + assertThat(hashValue).isNotEqualTo(origHashValue); testSchedule.memo(scheduleInState.memo()); + hashValue = ScheduleStoreUtility.calculateBytesHash(testSchedule.build()); + assertThat(hashValue).isEqualTo(origHashValue); + // change the wait for expiry and verify that hash changes testSchedule.waitForExpiry(!scheduleInState.waitForExpiry()); - hashValue = ScheduleStoreUtility.calculateStringHash(testSchedule.build()); - assertThat(hashValue).isNotEqualTo(SCHEDULE_IN_STATE_SHA256); - assertThat(hashValue).isEqualTo(SCHEDULE_IN_STATE_WAIT_EXPIRE_SHA256); + hashValue = ScheduleStoreUtility.calculateBytesHash(testSchedule.build()); + assertThat(hashValue).isNotEqualTo(origHashValue); testSchedule.waitForExpiry(scheduleInState.waitForExpiry()); + hashValue = ScheduleStoreUtility.calculateBytesHash(testSchedule.build()); + assertThat(hashValue).isEqualTo(origHashValue); } @Test void verifyExcludedAttributesHaveNoEffect() { Schedule.Builder testSchedule = scheduleInState.copyBuilder(); - String hashValue = ScheduleStoreUtility.calculateStringHash(scheduleInState); - assertThat(hashValue).isEqualTo(SCHEDULE_IN_STATE_SHA256); + Bytes origHashValue = ScheduleStoreUtility.calculateBytesHash(scheduleInState); testSchedule.scheduleId(new ScheduleID(42L, 444L, 22740229L)); - hashValue = ScheduleStoreUtility.calculateStringHash(testSchedule.build()); - assertThat(hashValue).isEqualTo(SCHEDULE_IN_STATE_SHA256); - testSchedule.scheduleId(scheduleInState.scheduleId()); + Bytes hashValue = ScheduleStoreUtility.calculateBytesHash(testSchedule.build()); + assertThat(hashValue).isEqualTo(origHashValue); testSchedule.calculatedExpirationSecond(18640811L); - hashValue = ScheduleStoreUtility.calculateStringHash(testSchedule.build()); - assertThat(hashValue).isEqualTo(SCHEDULE_IN_STATE_SHA256); - testSchedule.calculatedExpirationSecond(scheduleInState.calculatedExpirationSecond()); + hashValue = ScheduleStoreUtility.calculateBytesHash(testSchedule.build()); + assertThat(hashValue).isEqualTo(origHashValue); testSchedule.deleted(!scheduleInState.deleted()); - hashValue = ScheduleStoreUtility.calculateStringHash(testSchedule.build()); - assertThat(hashValue).isEqualTo(SCHEDULE_IN_STATE_SHA256); - testSchedule.deleted(scheduleInState.deleted()); + hashValue = ScheduleStoreUtility.calculateBytesHash(testSchedule.build()); + assertThat(hashValue).isEqualTo(origHashValue); testSchedule.executed(!scheduleInState.executed()); - hashValue = ScheduleStoreUtility.calculateStringHash(testSchedule.build()); - assertThat(hashValue).isEqualTo(SCHEDULE_IN_STATE_SHA256); - testSchedule.executed(scheduleInState.executed()); + hashValue = ScheduleStoreUtility.calculateBytesHash(testSchedule.build()); + assertThat(hashValue).isEqualTo(origHashValue); testSchedule.payerAccountId(admin); - hashValue = ScheduleStoreUtility.calculateStringHash(testSchedule.build()); - assertThat(hashValue).isEqualTo(SCHEDULE_IN_STATE_SHA256); - testSchedule.payerAccountId(scheduleInState.payerAccountId()); + hashValue = ScheduleStoreUtility.calculateBytesHash(testSchedule.build()); + assertThat(hashValue).isEqualTo(origHashValue); testSchedule.schedulerAccountId(payer); - hashValue = ScheduleStoreUtility.calculateStringHash(testSchedule.build()); - assertThat(hashValue).isEqualTo(SCHEDULE_IN_STATE_SHA256); - testSchedule.schedulerAccountId(scheduleInState.schedulerAccountId()); + hashValue = ScheduleStoreUtility.calculateBytesHash(testSchedule.build()); + assertThat(hashValue).isEqualTo(origHashValue); testSchedule.resolutionTime(modifiedResolutionTime); - hashValue = ScheduleStoreUtility.calculateStringHash(testSchedule.build()); - assertThat(hashValue).isEqualTo(SCHEDULE_IN_STATE_SHA256); - testSchedule.resolutionTime(scheduleInState.resolutionTime()); + hashValue = ScheduleStoreUtility.calculateBytesHash(testSchedule.build()); + assertThat(hashValue).isEqualTo(origHashValue); testSchedule.scheduleValidStart(modifiedStartTime); - hashValue = ScheduleStoreUtility.calculateStringHash(testSchedule.build()); - assertThat(hashValue).isEqualTo(SCHEDULE_IN_STATE_SHA256); - testSchedule.scheduleValidStart(scheduleInState.scheduleValidStart()); + hashValue = ScheduleStoreUtility.calculateBytesHash(testSchedule.build()); + assertThat(hashValue).isEqualTo(origHashValue); testSchedule.originalCreateTransaction(alternateCreateTransaction); - hashValue = ScheduleStoreUtility.calculateStringHash(testSchedule.build()); - assertThat(hashValue).isEqualTo(SCHEDULE_IN_STATE_SHA256); - testSchedule.originalCreateTransaction(scheduleInState.originalCreateTransaction()); + hashValue = ScheduleStoreUtility.calculateBytesHash(testSchedule.build()); + assertThat(hashValue).isEqualTo(origHashValue); testSchedule.signatories(alternateSignatories); - hashValue = ScheduleStoreUtility.calculateStringHash(testSchedule.build()); - assertThat(hashValue).isEqualTo(SCHEDULE_IN_STATE_SHA256); - testSchedule.signatories(scheduleInState.signatories()); + hashValue = ScheduleStoreUtility.calculateBytesHash(testSchedule.build()); + assertThat(hashValue).isEqualTo(origHashValue); } } diff --git a/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/ScheduleTestBase.java b/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/ScheduleTestBase.java index 04ee350a8f39..219160a1e751 100644 --- a/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/ScheduleTestBase.java +++ b/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/ScheduleTestBase.java @@ -47,7 +47,6 @@ import com.hedera.hapi.node.scheduled.ScheduleDeleteTransactionBody; import com.hedera.hapi.node.state.primitives.ProtoBytes; import com.hedera.hapi.node.state.primitives.ProtoLong; -import com.hedera.hapi.node.state.primitives.ProtoString; import com.hedera.hapi.node.state.schedule.Schedule; import com.hedera.hapi.node.state.schedule.ScheduleList; import com.hedera.hapi.node.state.token.Account; @@ -132,19 +131,7 @@ public class ScheduleTestBase { Bytes.fromHex("9834701927540926570495640961948794713207439248567184729049081327"); protected static final Bytes OTHER_KEY_HEX = Bytes.fromHex("983470192754092657adbdbeef61948794713207439248567184729049081327"); - // A few random values for fake schedule hashes - protected static final String SCHEDULE_IN_STATE_SHA256 = - "5a89e2e2ef363aa047e2ca032cc8fbff02cf64f5536e350bc252dcbc6e76fd76"; - protected static final String SCHEDULE_IN_STATE_0_EXPIRE_SHA256 = - "4a9a1bd0a6487bac0924da771d4bb62ed72391c15602eaee394991929bdde427"; - protected static final String SCHEDULE_IN_STATE_PAYER_IS_ADMIN_SHA256 = - "ef76f6e13f805b9ab5ae0e6fd9fd5976aba14e1b1f7cb4ecbd07fbc298f90ee5"; - protected static final String SCHEDULE_IN_STATE_ALTERNATE_SCHEDULED_SHA256 = - "1e2ec1fa33fce66166497aeffa8a0af690e257cafad02063a13191cabc2c2de3"; - protected static final String SCHEDULE_IN_STATE_ODD_MEMO_SHA256 = - "7788f7de741c9ef2e7bf371095ea497a0eaed1ad9b6cba2489f34f565ee70556"; - protected static final String SCHEDULE_IN_STATE_WAIT_EXPIRE_SHA256 = - "87cfae9fd8f15126af9ba8be0155c57f1cf0838c4915f4e0ea297a15d5d3a3b9"; + protected static final String SCHEDULED_TRANSACTION_MEMO = "Les ħ2ᛏᚺᛂ🌕 goo"; protected static final String ODD_MEMO = "she had marvelous judgement, Don... if not particularly good taste."; // a few typed null values to avoid casting null @@ -200,10 +187,10 @@ public class ScheduleTestBase { protected WritableKVState accountAliases; protected Map accountsMapById; protected Map scheduleMapById; - protected Map scheduleMapByEquality; + protected Map scheduleMapByEquality; protected Map scheduleMapByExpiration; protected WritableKVState writableById; - protected WritableKVState writableByEquality; + protected WritableKVState writableByEquality; protected WritableKVState writableByExpiration; protected Map> writableStatesMap; protected ReadableStates states; @@ -252,10 +239,9 @@ protected void commitScheduleStores() { // ConsensusSubmitMessage,CryptoTransfer,TokenMint,TokenBurn,CryptoApproveAllowance protected SchedulableTransactionBody createAlternateScheduled() { - final SchedulableTransactionBody scheduledTxn = SchedulableTransactionBody.newBuilder() + return SchedulableTransactionBody.newBuilder() .tokenBurn(TokenBurnTransactionBody.newBuilder()) .build(); - return scheduledTxn; } /** @@ -485,10 +471,9 @@ private void setUpStates() { } private SchedulableTransactionBody createSampleScheduled() { - final SchedulableTransactionBody scheduledTxn = SchedulableTransactionBody.newBuilder() + return SchedulableTransactionBody.newBuilder() .cryptoCreateAccount(CryptoCreateTransactionBody.newBuilder()) .build(); - return scheduledTxn; } private TransactionBody alternateCreateTransaction(final TransactionBody originalTransaction) { diff --git a/hedera-node/hedera-schedule-service/src/main/java/com/hedera/node/app/service/schedule/ReadableScheduleStore.java b/hedera-node/hedera-schedule-service/src/main/java/com/hedera/node/app/service/schedule/ReadableScheduleStore.java index 3a45d131b0cb..8ae0ec0c1f8f 100644 --- a/hedera-node/hedera-schedule-service/src/main/java/com/hedera/node/app/service/schedule/ReadableScheduleStore.java +++ b/hedera-node/hedera-schedule-service/src/main/java/com/hedera/node/app/service/schedule/ReadableScheduleStore.java @@ -42,7 +42,7 @@ public interface ReadableScheduleStore { * @return the schedule with the given id */ @Nullable - Schedule get(final @Nullable ScheduleID id); + Schedule get(@Nullable ScheduleID id); /** * Get a set of schedules that are "hash equal" to the provided Schedule. @@ -57,7 +57,7 @@ public interface ReadableScheduleStore { * These may not actually be equal to the provided schedule, and further comparison should be performed. */ @Nullable - public List getByEquality(final @NonNull Schedule scheduleToMatch); + List getByEquality(final @NonNull Schedule scheduleToMatch); /** * Given a time as seconds since the epoch, find all schedules currently in state that expire at that time. @@ -70,7 +70,7 @@ public interface ReadableScheduleStore { * @return a {@link List} of entries that have expiration times within the requested second. */ @Nullable - public List getByExpirationSecond(final long expirationTime); + List getByExpirationSecond(final long expirationTime); /** * Returns the number of schedules in state, for use in enforcing creation limits. From d025ceb31db00cd4305bce665a9e438130eaff5d Mon Sep 17 00:00:00 2001 From: Cody Littley <56973212+cody-littley@users.noreply.github.com> Date: Tue, 12 Mar 2024 11:54:06 -0500 Subject: [PATCH 075/115] feat: automatic transformers (#11992) Signed-off-by: Cody Littley --- .../wiring/component/ComponentWiring.java | 134 +++++++++++++++--- .../wiring/component/InputWireLabel.java | 2 +- .../wiring/component/SchedulerLabel.java | 40 ++++++ ...WireBindInfo.java => InputWireToBind.java} | 13 +- .../component/internal/TransformerToBind.java | 34 +++++ .../internal/WiringComponentProxy.java | 11 +- .../wiring/transformers/WireTransformer.java | 34 ++++- .../WiringComponentPerformanceTests.java | 2 +- .../component/WiringComponentTests.java | 105 +++++++++++++- .../platform/wiring/PlatformWiring.java | 2 +- 10 files changed, 348 insertions(+), 29 deletions(-) create mode 100644 platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/SchedulerLabel.java rename platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/internal/{WireBindInfo.java => InputWireToBind.java} (65%) create mode 100644 platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/internal/TransformerToBind.java diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/ComponentWiring.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/ComponentWiring.java index 0f52fab26181..b5cdbea3499f 100644 --- a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/ComponentWiring.java +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/ComponentWiring.java @@ -16,9 +16,12 @@ package com.swirlds.common.wiring.component; -import com.swirlds.common.wiring.component.internal.WireBindInfo; +import com.swirlds.common.wiring.component.internal.InputWireToBind; +import com.swirlds.common.wiring.component.internal.TransformerToBind; import com.swirlds.common.wiring.component.internal.WiringComponentProxy; +import com.swirlds.common.wiring.model.WiringModel; import com.swirlds.common.wiring.schedulers.TaskScheduler; +import com.swirlds.common.wiring.transformers.WireTransformer; import com.swirlds.common.wiring.wires.input.BindableInputWire; import com.swirlds.common.wiring.wires.input.InputWire; import com.swirlds.common.wiring.wires.output.OutputWire; @@ -26,7 +29,9 @@ import edu.umd.cs.findbugs.annotations.Nullable; import java.lang.reflect.Method; import java.lang.reflect.Proxy; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.function.BiConsumer; @@ -40,27 +45,53 @@ */ public class ComponentWiring { + private final WiringModel model; private final TaskScheduler scheduler; private final WiringComponentProxy proxy = new WiringComponentProxy(); private final COMPONENT_TYPE proxyComponent; + /** + * The component that implements the business logic. Will be null until {@link #bind(Object)} is called. + */ private COMPONENT_TYPE component; - private final Map bindInfo = new HashMap<>(); + /** + * Input wires that have been created for this component. + */ private final Map> inputWires = new HashMap<>(); + /** + * Input wires that need to be bound. + */ + private final List> inputsToBind = new ArrayList<>(); + + /** + * Previously created transformers/splitters/filters. + */ + private final Map> alternateOutputs = new HashMap<>(); + + /** + * Transformers that need to be bound. + */ + private final List> transformersToBind = new ArrayList<>(); + /** * Create a new component wiring. * + * @param model the wiring model that will contain the component * @param clazz the interface class of the component * @param scheduler the task scheduler that will run the component */ @SuppressWarnings("unchecked") public ComponentWiring( - @NonNull final Class clazz, @NonNull final TaskScheduler scheduler) { + @NonNull final WiringModel model, + @NonNull final Class clazz, + @NonNull final TaskScheduler scheduler) { + this.model = Objects.requireNonNull(model); this.scheduler = Objects.requireNonNull(scheduler); + if (!clazz.isInterface()) { throw new IllegalArgumentException("Component class " + clazz.getName() + " is not an interface."); } @@ -124,6 +155,68 @@ public InputWire getInputWire( return getOrBuildInputWire(proxy.getMostRecentlyInvokedMethod(), null, handler); } + /** + * Get the output wire of this component, transformed by a function. + * + * @param transformation the function that will transform the output, must be a static method on the component + * @param the type of the transformed output + * @return the transformed output wire + */ + @SuppressWarnings("unchecked") + @NonNull + public OutputWire getTransformedOutput( + @NonNull final BiFunction transformation) { + + Objects.requireNonNull(transformation); + try { + transformation.apply(proxyComponent, null); + } catch (final NullPointerException e) { + throw new IllegalStateException("Component wiring does not support primitive input types or return types. " + + "Use a boxed primitive instead."); + } + + final Method method = proxy.getMostRecentlyInvokedMethod(); + if (!method.isDefault()) { + throw new IllegalArgumentException("Method " + method.getName() + " does not have a default."); + } + + if (alternateOutputs.containsKey(method)) { + // We've already created this transformer. + return (OutputWire) alternateOutputs.get(method); + } + + final String wireLabel; + final InputWireLabel inputWireLabel = method.getAnnotation(InputWireLabel.class); + if (inputWireLabel == null) { + wireLabel = "data to transform"; + } else { + wireLabel = inputWireLabel.value(); + } + + final String schedulerLabel; + final SchedulerLabel schedulerLabelAnnotation = method.getAnnotation(SchedulerLabel.class); + if (schedulerLabelAnnotation == null) { + schedulerLabel = method.getName(); + } else { + schedulerLabel = schedulerLabelAnnotation.value(); + } + + final WireTransformer transformer = + new WireTransformer<>(model, schedulerLabel, wireLabel); + getOutputWire().solderTo(transformer.getInputWire()); + alternateOutputs.put(method, transformer.getOutputWire()); + + if (component == null) { + // we will bind this later + transformersToBind.add(new TransformerToBind<>(transformer, transformation)); + } else { + // bind this now + transformer.bind(x -> transformation.apply(component, x)); + } + + return transformer.getOutputWire(); + } + /** * Get the input wire for a specified method. * @@ -157,9 +250,7 @@ private InputWire getOrBuildInputWire( if (component == null) { // we will bind this later - bindInfo.put(method, new WireBindInfo((BiFunction) handlerWithReturn, (BiConsumer< - Object, Object>) - handlerWithoutReturn)); + inputsToBind.add(new InputWireToBind<>(inputWire, handlerWithReturn, handlerWithoutReturn)); } else { // bind this now if (handlerWithReturn != null) { @@ -211,29 +302,36 @@ public void stopSquelching() { * * @param component the component to bind */ + @SuppressWarnings("unchecked") public void bind(@NonNull final COMPONENT_TYPE component) { Objects.requireNonNull(component); this.component = component; - for (final Map.Entry entry : bindInfo.entrySet()) { - - final Method method = entry.getKey(); - final WireBindInfo wireBindInfo = entry.getValue(); - final BindableInputWire wire = inputWires.get(method); - - if (wireBindInfo.handlerWithReturn() != null) { - final BiFunction handlerWithReturn = wireBindInfo.handlerWithReturn(); - wire.bind(x -> { + // Bind input wires + for (final InputWireToBind wireToBind : inputsToBind) { + if (wireToBind.handlerWithReturn() != null) { + final BiFunction handlerWithReturn = + (BiFunction) wireToBind.handlerWithReturn(); + wireToBind.inputWire().bind(x -> { return handlerWithReturn.apply(component, x); }); } else { - final BiConsumer handlerWithoutReturn = - Objects.requireNonNull(wireBindInfo.handlerWithoutReturn()); - wire.bind(x -> { + final BiConsumer handlerWithoutReturn = + (BiConsumer) Objects.requireNonNull(wireToBind.handlerWithoutReturn()); + wireToBind.inputWire().bind(x -> { handlerWithoutReturn.accept(component, x); }); } } + + // Bind transformers + for (final TransformerToBind transformerToBind : transformersToBind) { + final WireTransformer transformer = + (WireTransformer) transformerToBind.transformer(); + final BiFunction transformation = + (BiFunction) transformerToBind.transformation(); + transformer.bind(x -> transformation.apply(component, x)); + } } } diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/InputWireLabel.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/InputWireLabel.java index 808b852719fa..6fd770311f38 100644 --- a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/InputWireLabel.java +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/InputWireLabel.java @@ -23,7 +23,7 @@ import java.lang.annotation.Target; /** - * Annotates a method parameter to indicate that the parameter is an input wire label. + * Label the input wire that a method is associated with. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/SchedulerLabel.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/SchedulerLabel.java new file mode 100644 index 000000000000..d8edf25f97b4 --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/SchedulerLabel.java @@ -0,0 +1,40 @@ +/* + * 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.common.wiring.component; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotates a method parameter used to implement a transformer/filter. Use this to override the name of the task + * scheduler used to operate the transformer/filter. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface SchedulerLabel { + + /** + * The label of the task scheduler that will operate the transformer/filter. + * + * @return the label of the task scheduler that will operate the transformer/filter + */ + @NonNull + String value(); +} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/internal/WireBindInfo.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/internal/InputWireToBind.java similarity index 65% rename from platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/internal/WireBindInfo.java rename to platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/internal/InputWireToBind.java index c7b46fc693a3..05a1d2caadab 100644 --- a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/internal/WireBindInfo.java +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/internal/InputWireToBind.java @@ -16,6 +16,8 @@ package com.swirlds.common.wiring.component.internal; +import com.swirlds.common.wiring.wires.input.BindableInputWire; +import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.util.function.BiConsumer; import java.util.function.BiFunction; @@ -23,11 +25,16 @@ /** * Contains information necessary to bind an input wire when we eventually get the implementation of the component. * + * @param inputWire the input wire to bind * @param handlerWithReturn null if initially bound. If not initially bound, will be non-null if the method has a * non-void return type. * @param handlerWithoutReturn null if initially bound. If not initially bound, will be non-null if the method has a * void return type + * @param the type of the component + * @param the input type of the input wire + * @param the output type of the component */ -public record WireBindInfo( - @Nullable BiFunction handlerWithReturn, - @Nullable BiConsumer handlerWithoutReturn) {} +public record InputWireToBind( + @NonNull BindableInputWire inputWire, + @Nullable BiFunction handlerWithReturn, + @Nullable BiConsumer handlerWithoutReturn) {} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/internal/TransformerToBind.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/internal/TransformerToBind.java new file mode 100644 index 000000000000..982099c8c47d --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/internal/TransformerToBind.java @@ -0,0 +1,34 @@ +/* + * 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.common.wiring.component.internal; + +import com.swirlds.common.wiring.transformers.WireTransformer; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.function.BiFunction; + +/** + * A transformer and the transformation to bind to a component. + * + * @param transformer the transformer we eventually want to bind + * @param transformation the transformation method + * @param the type of the component + * @param the input type of the transformer (equal to the output type of the base output wire) + * @param the output type of the transformer + */ +public record TransformerToBind( + @NonNull WireTransformer transformer, + @NonNull BiFunction transformation) {} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/internal/WiringComponentProxy.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/internal/WiringComponentProxy.java index 66555ac01814..79482d0f4f3a 100644 --- a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/internal/WiringComponentProxy.java +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/component/internal/WiringComponentProxy.java @@ -40,15 +40,20 @@ public Object invoke(@NonNull final Object proxy, @NonNull final Method method, } /** - * Get the most recently invoked method. + * Get the most recently invoked method. Calling this method resets the most recently invoked method to null + * as a safety measure. * * @return the most recently invoked method */ @NonNull public Method getMostRecentlyInvokedMethod() { if (mostRecentlyInvokedMethod == null) { - throw new IllegalStateException("No method has been invoked yet"); + throw new IllegalArgumentException("Provided lambda is not a method on the component interface."); + } + try { + return mostRecentlyInvokedMethod; + } finally { + mostRecentlyInvokedMethod = null; } - return mostRecentlyInvokedMethod; } } diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/transformers/WireTransformer.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/transformers/WireTransformer.java index d32047315e29..4bd174a722d6 100644 --- a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/transformers/WireTransformer.java +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/transformers/WireTransformer.java @@ -38,7 +38,7 @@ public class WireTransformer { private final OutputWire outputWire; /** - * Constructor. + * Constructor. Immediately binds the transformation function to the input wire. * * @param model the wiring model containing this output channel * @param transformerName the name of the transformer @@ -65,6 +65,27 @@ public WireTransformer( outputWire = taskScheduler.getOutputWire(); } + /** + * Constructor. Requires the input wire to be bound later. + * + * @param model the wiring model containing this output channel + * @param transformerName the name of the transformer + * @param transformerInputName the label for the input wire going into the transformer + */ + public WireTransformer( + @NonNull final WiringModel model, + @NonNull final String transformerName, + @NonNull final String transformerInputName) { + + final TaskScheduler taskScheduler = model.schedulerBuilder(transformerName) + .withType(TaskSchedulerType.DIRECT_THREADSAFE) + .build() + .cast(); + + inputWire = taskScheduler.buildInputWire(transformerInputName); + outputWire = taskScheduler.getOutputWire(); + } + /** * Get the input wire for this transformer. * @@ -84,4 +105,15 @@ public InputWire getInputWire() { public OutputWire getOutputWire() { return outputWire; } + + /** + * Bind the transformation function to the input wire. Do not call this if the transformation function was provided + * in the constructor. Must be called prior to use if the transformation function was not provided in the + * constructor. + * + * @param transformer the transformation function + */ + public void bind(@NonNull final Function transformer) { + inputWire.bind(transformer); + } } diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/component/WiringComponentPerformanceTests.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/component/WiringComponentPerformanceTests.java index 014d2cadabf8..fe195d656826 100644 --- a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/component/WiringComponentPerformanceTests.java +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/component/WiringComponentPerformanceTests.java @@ -77,7 +77,7 @@ private InputWire buildAutomaticComponent(@NonNull final SimpleComponent c .cast(); final ComponentWiring componentWiring = - new ComponentWiring<>(SimpleComponent.class, scheduler); + new ComponentWiring<>(model, SimpleComponent.class, scheduler); final InputWire inputWire = componentWiring.getInputWire(SimpleComponent::handleInput); componentWiring.bind(component); diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/component/WiringComponentTests.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/component/WiringComponentTests.java index 28c45bf821d1..fae71bd03aa9 100644 --- a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/component/WiringComponentTests.java +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/component/WiringComponentTests.java @@ -18,6 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; import com.swirlds.common.context.PlatformContext; import com.swirlds.common.test.fixtures.platform.TestPlatformContextBuilder; @@ -29,18 +30,30 @@ import edu.umd.cs.findbugs.annotations.NonNull; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; public class WiringComponentTests { private interface FooBarBaz { + @NonNull Long handleFoo(@NonNull Integer foo); @InputWireLabel("bar") + @NonNull Long handleBar(@NonNull Boolean bar); void handleBaz(@NonNull String baz); + + @InputWireLabel("data to be transformed") + @SchedulerLabel("transformer") + @NonNull + default String transformer(@NonNull final Long baseOutput) { + handleBar(true); + return "" + baseOutput; + } } private static class FooBarBazImpl implements FooBarBaz { @@ -68,6 +81,33 @@ public long getRunningValue() { } } + /** + * The framework should not permit methods that aren't on the component to be wired. + */ + @Test + void methodNotOnComponentTest() { + final PlatformContext platformContext = + TestPlatformContextBuilder.create().build(); + + final WiringModel wiringModel = + WiringModel.create(platformContext, platformContext.getTime(), ForkJoinPool.commonPool()); + + final TaskScheduler scheduler = wiringModel + .schedulerBuilder("test") + .withType(TaskSchedulerType.DIRECT) + .build() + .cast(); + + final ComponentWiring fooBarBazWiring = + new ComponentWiring<>(wiringModel, FooBarBaz.class, scheduler); + + assertThrows(IllegalArgumentException.class, () -> fooBarBazWiring.getInputWire((x, y) -> 0L)); + + assertThrows(IllegalArgumentException.class, () -> fooBarBazWiring.getInputWire((x, y) -> {})); + + assertThrows(IllegalArgumentException.class, () -> fooBarBazWiring.getTransformedOutput((x, y) -> 0L)); + } + @ParameterizedTest @ValueSource(ints = {0, 1, 2, 3}) void simpleComponentTest(final int bindLocation) { @@ -83,7 +123,8 @@ void simpleComponentTest(final int bindLocation) { .build() .cast(); - final ComponentWiring fooBarBazWiring = new ComponentWiring<>(FooBarBaz.class, scheduler); + final ComponentWiring fooBarBazWiring = + new ComponentWiring<>(wiringModel, FooBarBaz.class, scheduler); final FooBarBazImpl fooBarBazImpl = new FooBarBazImpl(); @@ -144,4 +185,66 @@ void simpleComponentTest(final int bindLocation) { } } } + + @ParameterizedTest + @ValueSource(ints = {0, 1}) + void transformerTest(final int bindLocation) { + final PlatformContext platformContext = + TestPlatformContextBuilder.create().build(); + + final WiringModel wiringModel = + WiringModel.create(platformContext, platformContext.getTime(), ForkJoinPool.commonPool()); + + final TaskScheduler scheduler = wiringModel + .schedulerBuilder("test") + .withType(TaskSchedulerType.DIRECT) + .build() + .cast(); + + final FooBarBazImpl fooBarBazImpl = new FooBarBazImpl(); + + final ComponentWiring fooBarBazWiring = + new ComponentWiring<>(wiringModel, FooBarBaz.class, scheduler); + + if (bindLocation == 0) { + fooBarBazWiring.bind(fooBarBazImpl); + } + + final InputWire fooInput = fooBarBazWiring.getInputWire(FooBarBaz::handleFoo); + final InputWire barInput = fooBarBazWiring.getInputWire(FooBarBaz::handleBar); + final InputWire bazInput = fooBarBazWiring.getInputWire(FooBarBaz::handleBaz); + + final OutputWire output = fooBarBazWiring.getTransformedOutput(FooBarBaz::transformer); + + // Getting the same transformer multiple times should yield the same instance + assertSame(output, fooBarBazWiring.getTransformedOutput(FooBarBaz::transformer)); + + if (bindLocation == 1) { + fooBarBazWiring.bind(fooBarBazImpl); + } + + final AtomicReference outputValue = new AtomicReference<>("0"); + output.solderTo("outputHandler", "output", outputValue::set); + + long expectedRunningValue = 0; + for (int i = 0; i < 1000; i++) { + if (i % 3 == 0) { + expectedRunningValue += i; + fooInput.put(i); + assertEquals(expectedRunningValue, fooBarBazImpl.getRunningValue()); + assertEquals("" + expectedRunningValue, outputValue.get()); + } else if (i % 3 == 1) { + final boolean choice = i % 7 == 0; + expectedRunningValue *= choice ? 1 : -1; + barInput.put(choice); + assertEquals(expectedRunningValue, fooBarBazImpl.getRunningValue()); + assertEquals("" + expectedRunningValue, outputValue.get()); + } else { + final String value = "value" + i; + expectedRunningValue *= value.hashCode(); + bazInput.put(value); + assertEquals(expectedRunningValue, fooBarBazImpl.getRunningValue()); + } + } + } } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformWiring.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformWiring.java index ccc0db360279..a9d73a710560 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformWiring.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformWiring.java @@ -193,7 +193,7 @@ public PlatformWiring(@NonNull final PlatformContext platformContext) { internalEventValidatorWiring = InternalEventValidatorWiring.create(schedulers.internalEventValidatorScheduler()); eventDeduplicatorWiring = - new ComponentWiring<>(EventDeduplicator.class, schedulers.eventDeduplicatorScheduler()); + new ComponentWiring<>(model, EventDeduplicator.class, schedulers.eventDeduplicatorScheduler()); eventSignatureValidatorWiring = EventSignatureValidatorWiring.create(schedulers.eventSignatureValidatorScheduler()); orphanBufferWiring = OrphanBufferWiring.create(schedulers.orphanBufferScheduler()); From fc69352cd889e1959717be5b7d971a27b114f3c4 Mon Sep 17 00:00:00 2001 From: anthony-swirldslabs <152534762+anthony-swirldslabs@users.noreply.github.com> Date: Tue, 12 Mar 2024 12:43:16 -0700 Subject: [PATCH 076/115] fix(pbj): stabilize hashCode() for Enums (#12045) Signed-off-by: Anthony Petrov --- hedera-dependency-versions/build.gradle.kts | 2 +- settings.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hedera-dependency-versions/build.gradle.kts b/hedera-dependency-versions/build.gradle.kts index e9e3f40c08fc..d9e6610b0fac 100644 --- a/hedera-dependency-versions/build.gradle.kts +++ b/hedera-dependency-versions/build.gradle.kts @@ -58,7 +58,7 @@ moduleInfo { version("com.google.jimfs", "1.2") version("com.google.protobuf", protobufVersion) version("com.google.protobuf.util", protobufVersion) - version("com.hedera.pbj.runtime", "0.8.1") + version("com.hedera.pbj.runtime", "0.8.3") version("com.squareup.javapoet", "1.13.0") version("com.sun.jna", "5.12.1") version("dagger", daggerVersion) diff --git a/settings.gradle.kts b/settings.gradle.kts index c607a7f1d0ce..6b2f34c4c55b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -150,6 +150,6 @@ dependencyResolutionManagement { version("grpc-proto", "1.45.1") version("hapi-proto", hapiProtoVersion) - plugin("pbj", "com.hedera.pbj.pbj-compiler").version("0.8.1") + plugin("pbj", "com.hedera.pbj.pbj-compiler").version("0.8.3") } } From f53ab4e379e2fddce177bfd90b03365f33884fc9 Mon Sep 17 00:00:00 2001 From: Cody Littley <56973212+cody-littley@users.noreply.github.com> Date: Tue, 12 Mar 2024 15:35:28 -0500 Subject: [PATCH 077/115] fix: future event buffer bug (#12066) Signed-off-by: Cody Littley --- .../creation/tipset/TipsetEventCreator.java | 2 +- .../platform/wiring/PlatformCoordinator.java | 25 ++++++++++++++++--- .../platform/wiring/PlatformWiring.java | 3 ++- .../components/FutureEventBufferWiring.java | 2 +- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/creation/tipset/TipsetEventCreator.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/creation/tipset/TipsetEventCreator.java index a9b4a0cdde60..1a3fdae274b5 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/creation/tipset/TipsetEventCreator.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/creation/tipset/TipsetEventCreator.java @@ -212,7 +212,7 @@ public void registerEvent(@NonNull final GossipEvent event) { * {@inheritDoc} */ @Override - public void setNonAncientEventWindow(@NonNull NonAncientEventWindow nonAncientEventWindow) { + public void setNonAncientEventWindow(@NonNull final NonAncientEventWindow nonAncientEventWindow) { this.nonAncientEventWindow = Objects.requireNonNull(nonAncientEventWindow); tipsetTracker.setNonAncientEventWindow(nonAncientEventWindow); childlessOtherEventTracker.pruneOldEvents(nonAncientEventWindow); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformCoordinator.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformCoordinator.java index be4b962c5839..c2c509e3413e 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformCoordinator.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformCoordinator.java @@ -24,6 +24,7 @@ import com.swirlds.platform.wiring.components.ConsensusRoundHandlerWiring; import com.swirlds.platform.wiring.components.EventCreationManagerWiring; import com.swirlds.platform.wiring.components.EventHasherWiring; +import com.swirlds.platform.wiring.components.FutureEventBufferWiring; import com.swirlds.platform.wiring.components.PostHashCollectorWiring; import com.swirlds.platform.wiring.components.ShadowgraphWiring; import com.swirlds.platform.wiring.components.StateSignatureCollectorWiring; @@ -48,6 +49,7 @@ public class PlatformCoordinator { private final InOrderLinkerWiring inOrderLinkerWiring; private final ShadowgraphWiring shadowgraphWiring; private final ConsensusEngineWiring consensusEngineWiring; + private final FutureEventBufferWiring futureEventBufferWiring; private final EventCreationManagerWiring eventCreationManagerWiring; private final ApplicationTransactionPrehandlerWiring applicationTransactionPrehandlerWiring; private final StateSignatureCollectorWiring stateSignatureCollectorWiring; @@ -64,6 +66,7 @@ public class PlatformCoordinator { * @param inOrderLinkerWiring the in order linker wiring * @param shadowgraphWiring the shadowgraph wiring * @param consensusEngineWiring the consensus engine wiring + * @param futureEventBufferWiring the future event buffer wiring * @param eventCreationManagerWiring the event creation manager wiring * @param applicationTransactionPrehandlerWiring the application transaction prehandler wiring * @param stateSignatureCollectorWiring the system transaction prehandler wiring @@ -78,6 +81,7 @@ public PlatformCoordinator( @NonNull final InOrderLinkerWiring inOrderLinkerWiring, @NonNull final ShadowgraphWiring shadowgraphWiring, @NonNull final ConsensusEngineWiring consensusEngineWiring, + @NonNull final FutureEventBufferWiring futureEventBufferWiring, @NonNull final EventCreationManagerWiring eventCreationManagerWiring, @NonNull final ApplicationTransactionPrehandlerWiring applicationTransactionPrehandlerWiring, @NonNull final StateSignatureCollectorWiring stateSignatureCollectorWiring, @@ -91,6 +95,7 @@ public PlatformCoordinator( this.inOrderLinkerWiring = Objects.requireNonNull(inOrderLinkerWiring); this.shadowgraphWiring = Objects.requireNonNull(shadowgraphWiring); this.consensusEngineWiring = Objects.requireNonNull(consensusEngineWiring); + this.futureEventBufferWiring = Objects.requireNonNull(futureEventBufferWiring); this.eventCreationManagerWiring = Objects.requireNonNull(eventCreationManagerWiring); this.applicationTransactionPrehandlerWiring = Objects.requireNonNull(applicationTransactionPrehandlerWiring); this.stateSignatureCollectorWiring = Objects.requireNonNull(stateSignatureCollectorWiring); @@ -98,9 +103,16 @@ public PlatformCoordinator( } /** - * Flushes the intake pipeline + * Flushes the intake pipeline. After this method is called, all components in the intake pipeline (i.e. components + * prior to the consensus engine) will have been flushed. Additionally, things will be flushed an order that + * guarantees that there will be no remaining work in the intake pipeline (as long as there are no additional events + * added to the intake pipeline, and as long as there are no events released by the orphan buffer). */ public void flushIntakePipeline() { + // Important: the order of the lines within this function matters. Do not alter the order of these + // lines without understanding the implications of doing so. Consult the wiring diagram when deciding + // whether to change the order of these lines. + // it isn't possible to flush the event hasher and the post hash collector independently, since the framework // currently doesn't support flushing if multiple components share the same object counter. As a workaround, // we just wait for the shared object counter to be empty, which is equivalent to flushing both components. @@ -110,17 +122,24 @@ public void flushIntakePipeline() { eventDeduplicatorWiring.flush(); eventSignatureValidatorWiring.flushRunnable().run(); orphanBufferWiring.flushRunnable().run(); - eventCreationManagerWiring.flush(); inOrderLinkerWiring.flushRunnable().run(); shadowgraphWiring.flushRunnable().run(); consensusEngineWiring.flushRunnable().run(); applicationTransactionPrehandlerWiring.flushRunnable().run(); + futureEventBufferWiring.flushRunnable().run(); + eventCreationManagerWiring.flush(); } /** - * Safely clears the system in preparation for reconnect + * Safely clears the system in preparation for reconnect. After this method is called, there should be no work + * sitting in any of the wiring queues, and all internal data structures within wiring components that need to be + * cleared to prepare for a reconnect should be cleared. */ public void clear() { + // Important: the order of the lines within this function are important. Do not alter the order of these + // lines without understanding the implications of doing so. Consult the wiring diagram when deciding + // whether to change the order of these lines. + // Phase 1: squelch // Break cycles in the system. Flush squelched components just in case there is a task being executed when // squelch is activated. diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformWiring.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformWiring.java index a9d73a710560..694fc96ee282 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformWiring.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformWiring.java @@ -199,6 +199,7 @@ public PlatformWiring(@NonNull final PlatformContext platformContext) { orphanBufferWiring = OrphanBufferWiring.create(schedulers.orphanBufferScheduler()); inOrderLinkerWiring = InOrderLinkerWiring.create(schedulers.inOrderLinkerScheduler()); consensusEngineWiring = ConsensusEngineWiring.create(schedulers.consensusEngineScheduler()); + futureEventBufferWiring = FutureEventBufferWiring.create(schedulers.futureEventBufferScheduler()); eventCreationManagerWiring = EventCreationManagerWiring.create(platformContext, schedulers.eventCreationManagerScheduler()); pcesSequencerWiring = PcesSequencerWiring.create(schedulers.pcesSequencerScheduler()); @@ -224,6 +225,7 @@ public PlatformWiring(@NonNull final PlatformContext platformContext) { inOrderLinkerWiring, shadowgraphWiring, consensusEngineWiring, + futureEventBufferWiring, eventCreationManagerWiring, applicationTransactionPrehandlerWiring, stateSignatureCollectorWiring, @@ -233,7 +235,6 @@ public PlatformWiring(@NonNull final PlatformContext platformContext) { pcesWriterWiring = PcesWriterWiring.create(schedulers.pcesWriterScheduler()); eventDurabilityNexusWiring = EventDurabilityNexusWiring.create(schedulers.eventDurabilityNexusScheduler()); - futureEventBufferWiring = FutureEventBufferWiring.create(schedulers.futureEventBufferScheduler()); gossipWiring = GossipWiring.create(model); eventWindowManagerWiring = EventWindowManagerWiring.create(model); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/FutureEventBufferWiring.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/FutureEventBufferWiring.java index 6e6eefe8fe4d..1a3434e1c45b 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/FutureEventBufferWiring.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/FutureEventBufferWiring.java @@ -67,7 +67,7 @@ public static FutureEventBufferWiring create(@NonNull final TaskScheduler>) eventInput).bind(futureEventBuffer::addEvent); - ((BindableInputWire) eventWindowInput) + ((BindableInputWire>) eventWindowInput) .bind(futureEventBuffer::updateEventWindow); } } From 1186447682c0b4fec5f054c1b67242bdd981f3dd Mon Sep 17 00:00:00 2001 From: Austin Littley <102969658+alittley@users.noreply.github.com> Date: Tue, 12 Mar 2024 16:52:51 -0400 Subject: [PATCH 078/115] fix: invalid wiring diagram (#12074) Signed-off-by: Austin Littley --- platform-sdk/docs/core/wiringDiagramLink.md | 2 +- .../com/swirlds/platform/wiring/generate-platform-diagram.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/platform-sdk/docs/core/wiringDiagramLink.md b/platform-sdk/docs/core/wiringDiagramLink.md index a6567fc1e117..69669a126267 100644 --- a/platform-sdk/docs/core/wiringDiagramLink.md +++ b/platform-sdk/docs/core/wiringDiagramLink.md @@ -1,6 +1,6 @@ # Pre-Generated Wiring Diagram -[Click here for the wiring diagram](https://mermaid.ink/svg/JSV7aW5pdDogeydmbG93Y2hhcnQnOiB7J2RlZmF1bHRSZW5kZXJlcic6ICdlbGsnfX19JSUKZmxvd2NoYXJ0IFRECnYwWy8iQ29uc2Vuc3VzIEV2ZW50IFN0cmVhbSIvXQpzdWJncmFwaCB2MVsiQ29uc2Vuc3VzIFBpcGVsaW5lIl0KdjJbIkNvbnNlbnN1cyBFbmdpbmUiXQp2M1siaW5PcmRlckxpbmtlcjxiciAvPvCfjIAiXQp2NCgoIvCfjIAiKSkKdjUoKCLwn5OsIikpCnY2KCgi8J+avSIpKQplbmQKc3ViZ3JhcGggdjdbIkV2ZW50IENyZWF0aW9uIl0KdjhbImV2ZW50Q3JlYXRpb25NYW5hZ2VyPGJyIC8+4p2k77iP8J+MgCJdCnY5WyJmdXR1cmVFdmVudEJ1ZmZlcjxiciAvPvCfjIAiXQp2MTB7eyJmdXR1cmVFdmVudEJ1ZmZlclNwbGl0dGVyIn19CnYxMVsvInRyYW5zYWN0aW9uUG9vbCIvXQp2MTIoKCLwn42OIikpCmVuZApzdWJncmFwaCB2MTNbIkV2ZW50IEhhc2hpbmciXQp2MTRbWyJldmVudEhhc2hlciJdXQp2MTVbInBvc3RIYXNoQ29sbGVjdG9yIl0KZW5kCnN1YmdyYXBoIHYxNlsiRXZlbnQgVmFsaWRhdGlvbiJdCnYxN1siZXZlbnREZWR1cGxpY2F0b3I8YnIgLz7wn4yAIl0KdjE4WyJldmVudFNpZ25hdHVyZVZhbGlkYXRvcjxiciAvPvCfjIAiXQp2MTlbImludGVybmFsRXZlbnRWYWxpZGF0b3I8YnIgLz7wn42OIl0KZW5kCnN1YmdyYXBoIHYyMFsiR29zc2lwIl0KdjIxe3siZ29zc2lwIn19CnYyMlsic2hhZG93Z3JhcGg8YnIgLz7wn4yA8J+TrCJdCmVuZApzdWJncmFwaCB2MjNbIkhlYXJ0YmVhdCJdCnYyNFsiaGVhcnRiZWF0Il0KdjI1KCgi4p2k77iPIikpCmVuZAp2MjZbIk9ycGhhbiBCdWZmZXI8YnIgLz7wn4yAIl0Kc3ViZ3JhcGggdjI3WyJQQ0VTIFJlcGxheSJdCnYyOFsvInBjZXNSZXBsYXllciIvXQp2MjkoKCLinIUiKSkKZW5kCnN1YmdyYXBoIHYzMFsiUHJlY29uc2Vuc3VzIEV2ZW50IFN0cmVhbSJdCnYzMVsvImV2ZW50RHVyYWJpbGl0eU5leHVzIi9dCnYzMlsvInBjZXNTZXF1ZW5jZXIiL10KdjMzWyJwY2VzV3JpdGVyPGJyIC8+4pyF8J+MgPCfk4Dwn5q9Il0KdjM0KCgi8J+VkSIpKQplbmQKc3ViZ3JhcGggdjM1WyJTaWduYXR1cmUgTWFuYWdlbWVudCJdCnN1YmdyYXBoIHYzNlsiSXNzIERldGVjdG9yIl0KdjM3e3siZXh0cmFjdFNpZ25hdHVyZXNGb3JJc3NEZXRlY3RvciJ9fQp2MzhbImlzc0RldGVjdG9yIl0KdjM5Wy8iaXNzSGFuZGxlciIvXQp2NDBbLyJpc3NOb3RpZmljYXRpb25FbmdpbmUiL10KdjQxe3siaXNzTm90aWZpY2F0aW9uU3BsaXR0ZXIifX0KdjQyWy8ic3RhdHVzTWFuYWdlcl9zdWJtaXRDYXRhc3Ryb3BoaWNGYWlsdXJlIi9dCmVuZAp2NDNbIlN0YXRlIFNpZ25hdHVyZSBDb2xsZWN0aW9uIl0KdjQ0Wy8ibGF0ZXN0Q29tcGxldGVTdGF0ZU5leHVzIi9dCnY0NVsibGF0ZXN0Q29tcGxldGVTdGF0ZU5vdGlmaWNhdGlvbiJdCnY0Nlsic3RhdGVTaWduZXIiXQplbmQKdjQ3WyJTdGF0ZSBGaWxlIE1hbmFnZW1lbnQiXQpzdWJncmFwaCB2NDhbIlN0YXRlIE1vZGlmaWNhdGlvbiJdCnY0OVsiY29uc2Vuc3VzUm91bmRIYW5kbGVyPGJyIC8+8J+UrvCflZEiXQp2NTB7eyJydW5uaW5nSGFzaFVwZGF0ZSJ9fQplbmQKc3ViZ3JhcGggdjUxWyJUcmFuc2FjdGlvbiBQcmVoYW5kbGluZyJdCnY1MltbImFwcGxpY2F0aW9uVHJhbnNhY3Rpb25QcmVoYW5kbGVyIl1dCnY1MygoIvCflK4iKSkKZW5kCnY1NFsiaGFzaExvZ2dlciJdCnY1NSgoIvCfk4AiKSkKdjIgLS0gInJvdW5kcyIgLS0+IHYwCnYyIC0tICJyb3VuZHMiIC0tPiB2NDkKdjIgLS4gIm5vbi1hbmNpZW50IGV2ZW50IHdpbmRvdyIgLi0+IHY0CnYyIC0tICJmbHVzaCByZXF1ZXN0IiAtLT4gdjYKdjI2IC0tICJwcmVjb25zZW5zdXMgZXZlbnRzIiAtLT4gdjQzCnYyNiAtLSAicHJlY29uc2Vuc3VzIGV2ZW50cyIgLS0+IHY1Mgp2MjYgLS0gInByZWNvbnNlbnN1cyBldmVudHMiIC0tPiB2OQp2MjYgLS0gImV2ZW50cyB0byBzZXF1ZW5jZSIgLS0+IHYzMgp2NDcgLS0gIm1pbmltdW0gaWRlbnRpZmllciB0byBzdG9yZSIgLS0+IHY1NQp2NDMgLS0gInN0YXRlcyIgLS0+IHY0Nwp2NDMgLS0gImNvbXBsZXRlIHN0YXRlcyIgLS0+IHY0NAp2NDMgLS0gImNvbXBsZXRlIHN0YXRlcyIgLS0+IHY0NQp2NTIgLS4gImZ1dHVyZXMiIC4tbyB2NTMKdjggLS4gImdldCB0cmFuc2FjdGlvbnMiIC4tbyB2MTEKdjggLS4gIm5vbi12YWxpZGF0ZWQgZXZlbnRzIiAuLT4gdjEyCnYxNyAtLSAiZXZlbnRzIHdpdGggdW52YWxpZGF0ZWQgc2lnbmF0dXJlcyIgLS0+IHYxOAp2MzEgLS4gIndhaXQgZm9yIGR1cmFiaWxpdHkiIC4tbyB2MzQKdjE0IC0tICJoYXNoZWQgZXZlbnRzIiAtLT4gdjE1CnYxOCAtLSAidW5vcmRlcmVkIGV2ZW50cyIgLS0+IHYyNgp2MzcgLS0gInBvc3QgY29uc2Vuc3VzIHNpZ25hdHVyZXMiIC0tPiB2MzgKdjkgLS0gInBvc3NpYmxlIHBhcmVudCBsaXN0cyIgLS0+IHYxMAp2MTAgLS0gInBvc3NpYmxlIHBhcmVudHMiIC0tPiB2OAp2MjEgLS0gImV2ZW50cyB0byBoYXNoIiAtLT4gdjE0CnYyNCAtLSAiaGVhcnRiZWF0IiAtLT4gdjI1CnYzIC0tICJsaW5rZWQgZXZlbnRzIiAtLT4gdjIKdjMgLS0gImV2ZW50cyB0byBnb3NzaXAiIC0tPiB2NQp2MTkgLS0gIm5vbi1kZWR1cGxpY2F0ZWQgZXZlbnRzIiAtLT4gdjE3CnYzOCAtLSAiaXNzIG5vdGlmaWNhdGlvbnMiIC0tPiB2NDEKdjQxIC0tICJpc3Mgbm90aWZpY2F0aW9uIiAtLT4gdjM5CnY0MSAtLSAiSVNTIG5vdGlmaWNhdGlvbiIgLS0+IHY0MAp2NDEgLS0gIklTUyBub3RpZmljYXRpb24iIC0tPiB2NDIKdjI4IC0tICJldmVudHMgdG8gaGFzaCIgLS0+IHYxNAp2MjggLS0gImRvbmUgc3RyZWFtaW5nIHBjZXMiIC0tPiB2MjkKdjMyIC0tICJ1bmxpbmtlZCBldmVudHMiIC0tPiB2Mwp2MzIgLS0gImV2ZW50cyB0byB3cml0ZSIgLS0+IHYzMwp2MzMgLS0gImxhdGVzdCBkdXJhYmxlIHNlcXVlbmNlIG51bWJlciIgLS0+IHYzMQp2MTUgLS0gIm5vbi12YWxpZGF0ZWQgZXZlbnRzIiAtLT4gdjE5CnY1MCAtLSAicnVubmluZyBoYXNoIHVwZGF0ZSIgLS0+IHYwCnY1MCAtLSAicnVubmluZyBoYXNoIHVwZGF0ZSIgLS0+IHY0OQp2NDYgLS0gInN0YXRlIHNpZ25hdHVyZSB0cmFuc2FjdGlvbnMiIC0tPiB2MTEKY2xhc3NEZWYgczAgZmlsbDojY2NjLHN0cm9rZTojMDAwLHN0cm9rZS13aWR0aDoycHgKY2xhc3MgdjAsdjEwLHYxMSx2MjEsdjI4LHYzMSx2MzIsdjM3LHYzOSx2NDAsdjQxLHY0Mix2NDQsdjUwIHMwCmNsYXNzRGVmIHMxIGZpbGw6IzlDRixzdHJva2U6IzAwMCxzdHJva2Utd2lkdGg6MnB4CmNsYXNzIHYxLHYxMyx2MTYsdjIwLHYyMyx2MjcsdjMwLHYzNSx2NDgsdjUxLHY3IHMxCmNsYXNzRGVmIHMyIGZpbGw6I2ZmOSxzdHJva2U6IzAwMCxzdHJva2Utd2lkdGg6MnB4CmNsYXNzIHYxNCx2MTUsdjE3LHYxOCx2MTksdjIsdjIyLHYyNCx2MjYsdjMsdjMzLHYzOCx2NDMsdjQ1LHY0Nix2NDcsdjQ5LHY1Mix2NTQsdjgsdjkgczIKY2xhc3NEZWYgczMgZmlsbDojZjg4LHN0cm9rZTojMDAwLHN0cm9rZS13aWR0aDoycHgKY2xhc3MgdjEyLHYyNSx2MjksdjM0LHY0LHY1LHY1Myx2NTUsdjYgczMKY2xhc3NEZWYgczQgZmlsbDojQkVGLHN0cm9rZTojMDAwLHN0cm9rZS13aWR0aDoycHgKY2xhc3MgdjM2IHM0Cg==?bgColor=e8e8e8) +[Click here for the wiring diagram](https://mermaid.ink/svg/JSV7aW5pdDogeydmbG93Y2hhcnQnOiB7J2RlZmF1bHRSZW5kZXJlcic6ICdlbGsnfX19JSUKZmxvd2NoYXJ0IFRECnYwWy8iQ29uc2Vuc3VzIEV2ZW50IFN0cmVhbSIvXQpzdWJncmFwaCB2MVsiQ29uc2Vuc3VzIFBpcGVsaW5lIl0KdjJbIkNvbnNlbnN1cyBFbmdpbmUiXQp2M1siaW5PcmRlckxpbmtlcjxiciAvPvCfjIAiXQp2NCgoIvCfjIAiKSkKdjUoKCLwn5OsIikpCnY2KCgi8J+avSIpKQplbmQKc3ViZ3JhcGggdjdbIkV2ZW50IENyZWF0aW9uIl0KdjhbImV2ZW50Q3JlYXRpb25NYW5hZ2VyPGJyIC8+4p2k77iP8J+MgCJdCnY5WyJmdXR1cmVFdmVudEJ1ZmZlcjxiciAvPvCfjIAiXQp2MTB7eyJmdXR1cmVFdmVudEJ1ZmZlclNwbGl0dGVyIn19CnYxMVsvInRyYW5zYWN0aW9uUG9vbCIvXQp2MTIoKCLwn42OIikpCmVuZApzdWJncmFwaCB2MTNbIkV2ZW50IEhhc2hpbmciXQp2MTRbWyJldmVudEhhc2hlciJdXQp2MTVbInBvc3RIYXNoQ29sbGVjdG9yIl0KZW5kCnN1YmdyYXBoIHYxNlsiRXZlbnQgVmFsaWRhdGlvbiJdCnYxN1siZXZlbnREZWR1cGxpY2F0b3I8YnIgLz7wn4yAIl0KdjE4WyJldmVudFNpZ25hdHVyZVZhbGlkYXRvcjxiciAvPvCfjIAiXQp2MTlbImludGVybmFsRXZlbnRWYWxpZGF0b3I8YnIgLz7wn42OIl0KZW5kCnN1YmdyYXBoIHYyMFsiR29zc2lwIl0KdjIxe3siZ29zc2lwIn19CnYyMlsic2hhZG93Z3JhcGg8YnIgLz7wn4yA8J+TrCJdCmVuZApzdWJncmFwaCB2MjNbIkhlYXJ0YmVhdCJdCnYyNFsiaGVhcnRiZWF0Il0KdjI1KCgi4p2k77iPIikpCmVuZAp2MjZbIk9ycGhhbiBCdWZmZXI8YnIgLz7wn4yAIl0Kc3ViZ3JhcGggdjI3WyJQQ0VTIFJlcGxheSJdCnYyOFsvInBjZXNSZXBsYXllciIvXQp2MjkoKCLinIUiKSkKZW5kCnN1YmdyYXBoIHYzMFsiUHJlY29uc2Vuc3VzIEV2ZW50IFN0cmVhbSJdCnYzMVsvImV2ZW50RHVyYWJpbGl0eU5leHVzIi9dCnYzMlsvInBjZXNTZXF1ZW5jZXIiL10KdjMzWyJwY2VzV3JpdGVyPGJyIC8+4pyF8J+MgPCfk4Dwn5q9Il0KdjM0KCgi8J+VkSIpKQplbmQKc3ViZ3JhcGggdjM1WyJTaWduYXR1cmUgTWFuYWdlbWVudCJdCnN1YmdyYXBoIHYzNlsiSXNzIERldGVjdG9yIl0KdjM3WyJpc3NEZXRlY3RvciJdCnYzOFsvImlzc0hhbmRsZXIiL10KdjM5Wy8iaXNzTm90aWZpY2F0aW9uRW5naW5lIi9dCnY0MHt7Imlzc05vdGlmaWNhdGlvblNwbGl0dGVyIn19CnY0MVsvInN0YXR1c01hbmFnZXJfc3VibWl0Q2F0YXN0cm9waGljRmFpbHVyZSIvXQplbmQKdjQyWyJTdGF0ZSBTaWduYXR1cmUgQ29sbGVjdGlvbiJdCnY0M1svImxhdGVzdENvbXBsZXRlU3RhdGVOZXh1cyIvXQp2NDRbImxhdGVzdENvbXBsZXRlU3RhdGVOb3RpZmljYXRpb24iXQp2NDVbInN0YXRlU2lnbmVyIl0KZW5kCnY0NlsiU3RhdGUgRmlsZSBNYW5hZ2VtZW50Il0Kc3ViZ3JhcGggdjQ3WyJTdGF0ZSBNb2RpZmljYXRpb24iXQp2NDhbImNvbnNlbnN1c1JvdW5kSGFuZGxlcjxiciAvPvCflK7wn5WRIl0KdjQ5e3sicnVubmluZ0hhc2hVcGRhdGUifX0KZW5kCnN1YmdyYXBoIHY1MFsiVHJhbnNhY3Rpb24gUHJlaGFuZGxpbmciXQp2NTFbWyJhcHBsaWNhdGlvblRyYW5zYWN0aW9uUHJlaGFuZGxlciJdXQp2NTIoKCLwn5SuIikpCmVuZAp2NTNbImhhc2hMb2dnZXIiXQp2NTQoKCLwn5OAIikpCnYyIC0tICJyb3VuZHMiIC0tPiB2MAp2MiAtLSAicm91bmRzIiAtLT4gdjQ4CnYyIC0uICJub24tYW5jaWVudCBldmVudCB3aW5kb3ciIC4tPiB2NAp2MiAtLSAiZmx1c2ggcmVxdWVzdCIgLS0+IHY2CnYyNiAtLSAicHJlY29uc2Vuc3VzIGV2ZW50cyIgLS0+IHY0Mgp2MjYgLS0gInByZWNvbnNlbnN1cyBldmVudHMiIC0tPiB2NTEKdjI2IC0tICJwcmVjb25zZW5zdXMgZXZlbnRzIiAtLT4gdjkKdjI2IC0tICJldmVudHMgdG8gc2VxdWVuY2UiIC0tPiB2MzIKdjQ2IC0tICJtaW5pbXVtIGlkZW50aWZpZXIgdG8gc3RvcmUiIC0tPiB2NTQKdjQyIC0tICJzdGF0ZXMiIC0tPiB2NDYKdjQyIC0tICJjb21wbGV0ZSBzdGF0ZXMiIC0tPiB2NDMKdjQyIC0tICJjb21wbGV0ZSBzdGF0ZXMiIC0tPiB2NDQKdjUxIC0uICJmdXR1cmVzIiAuLW8gdjUyCnY4IC0uICJnZXQgdHJhbnNhY3Rpb25zIiAuLW8gdjExCnY4IC0uICJub24tdmFsaWRhdGVkIGV2ZW50cyIgLi0+IHYxMgp2MTcgLS0gImV2ZW50cyB3aXRoIHVudmFsaWRhdGVkIHNpZ25hdHVyZXMiIC0tPiB2MTgKdjMxIC0uICJ3YWl0IGZvciBkdXJhYmlsaXR5IiAuLW8gdjM0CnYxNCAtLSAiaGFzaGVkIGV2ZW50cyIgLS0+IHYxNQp2MTggLS0gInVub3JkZXJlZCBldmVudHMiIC0tPiB2MjYKdjkgLS0gInBvc3NpYmxlIHBhcmVudCBsaXN0cyIgLS0+IHYxMAp2MTAgLS0gInBvc3NpYmxlIHBhcmVudHMiIC0tPiB2OAp2MjEgLS0gImV2ZW50cyB0byBoYXNoIiAtLT4gdjE0CnYyNCAtLSAiaGVhcnRiZWF0IiAtLT4gdjI1CnYzIC0tICJsaW5rZWQgZXZlbnRzIiAtLT4gdjIKdjMgLS0gImV2ZW50cyB0byBnb3NzaXAiIC0tPiB2NQp2MTkgLS0gIm5vbi1kZWR1cGxpY2F0ZWQgZXZlbnRzIiAtLT4gdjE3CnYzNyAtLSAiaXNzIG5vdGlmaWNhdGlvbnMiIC0tPiB2NDAKdjQwIC0tICJpc3Mgbm90aWZpY2F0aW9uIiAtLT4gdjM4CnY0MCAtLSAiSVNTIG5vdGlmaWNhdGlvbiIgLS0+IHYzOQp2NDAgLS0gIklTUyBub3RpZmljYXRpb24iIC0tPiB2NDEKdjI4IC0tICJldmVudHMgdG8gaGFzaCIgLS0+IHYxNAp2MjggLS0gImRvbmUgc3RyZWFtaW5nIHBjZXMiIC0tPiB2MjkKdjMyIC0tICJ1bmxpbmtlZCBldmVudHMiIC0tPiB2Mwp2MzIgLS0gImV2ZW50cyB0byB3cml0ZSIgLS0+IHYzMwp2MzMgLS0gImxhdGVzdCBkdXJhYmxlIHNlcXVlbmNlIG51bWJlciIgLS0+IHYzMQp2MTUgLS0gIm5vbi12YWxpZGF0ZWQgZXZlbnRzIiAtLT4gdjE5CnY0OSAtLSAicnVubmluZyBoYXNoIHVwZGF0ZSIgLS0+IHYwCnY0OSAtLSAicnVubmluZyBoYXNoIHVwZGF0ZSIgLS0+IHY0OAp2NDUgLS0gInN0YXRlIHNpZ25hdHVyZSB0cmFuc2FjdGlvbnMiIC0tPiB2MTEKY2xhc3NEZWYgczAgZmlsbDojY2NjLHN0cm9rZTojMDAwLHN0cm9rZS13aWR0aDoycHgKY2xhc3MgdjAsdjEwLHYxMSx2MjEsdjI4LHYzMSx2MzIsdjM4LHYzOSx2NDAsdjQxLHY0Myx2NDkgczAKY2xhc3NEZWYgczEgZmlsbDojOUNGLHN0cm9rZTojMDAwLHN0cm9rZS13aWR0aDoycHgKY2xhc3MgdjEsdjEzLHYxNix2MjAsdjIzLHYyNyx2MzAsdjM1LHY0Nyx2NTAsdjcgczEKY2xhc3NEZWYgczIgZmlsbDojZmY5LHN0cm9rZTojMDAwLHN0cm9rZS13aWR0aDoycHgKY2xhc3MgdjE0LHYxNSx2MTcsdjE4LHYxOSx2Mix2MjIsdjI0LHYyNix2Myx2MzMsdjM3LHY0Mix2NDQsdjQ1LHY0Nix2NDgsdjUxLHY1Myx2OCx2OSBzMgpjbGFzc0RlZiBzMyBmaWxsOiNmODgsc3Ryb2tlOiMwMDAsc3Ryb2tlLXdpZHRoOjJweApjbGFzcyB2MTIsdjI1LHYyOSx2MzQsdjQsdjUsdjUyLHY1NCx2NiBzMwpjbGFzc0RlZiBzNCBmaWxsOiNCRUYsc3Ryb2tlOiMwMDAsc3Ryb2tlLXdpZHRoOjJweApjbGFzcyB2MzYgczQK?bgColor=e8e8e8) When making any change that modifies the wiring diagram, please regenerate the diagram and update this page with the new diagram. \ No newline at end of file diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/generate-platform-diagram.sh b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/generate-platform-diagram.sh index 58b12b57d7af..c4de749b0a63 100755 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/generate-platform-diagram.sh +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/generate-platform-diagram.sh @@ -24,7 +24,7 @@ pcli diagram \ -g 'Consensus Pipeline:inOrderLinker,Consensus Engine,📬,🌀,🚽' \ -g 'Event Creation:futureEventBuffer,futureEventBufferSplitter,eventCreationManager,transactionPool,🍎' \ -g 'Gossip:gossip,shadowgraph' \ - -g 'Iss Detector:extractSignaturesForIssDetector,issDetector,issNotificationSplitter,issHandler,issNotificationEngine,statusManager_submitCatastrophicFailure' \ + -g 'Iss Detector:issDetector,issNotificationSplitter,issHandler,issNotificationEngine,statusManager_submitCatastrophicFailure' \ -g 'Heartbeat:heartbeat,❤️' \ -g 'PCES Replay:pcesReplayer,✅' \ -g 'Transaction Prehandling:applicationTransactionPrehandler,🔮' \ From 0b91e89714418026b98b421282342de16d56273e Mon Sep 17 00:00:00 2001 From: Nathan Klick Date: Tue, 12 Mar 2024 19:49:21 -0500 Subject: [PATCH 079/115] fix(ci): issue with improperly generated artifact file names (#12088) Signed-off-by: Nathan Klick --- .github/workflows/node-zxc-build-release-artifact.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/node-zxc-build-release-artifact.yaml b/.github/workflows/node-zxc-build-release-artifact.yaml index 3fab69b4b1b2..9d053a03ebec 100644 --- a/.github/workflows/node-zxc-build-release-artifact.yaml +++ b/.github/workflows/node-zxc-build-release-artifact.yaml @@ -334,7 +334,7 @@ jobs: mkdir -p "${ARTIFACT_BASE_DIR}" if [[ "${POLICY}" == "branch-commit" ]]; then - ARTIFACT_NAME="build-${{ needs.validate.outputs.branch-name-lower }}-${{ needs.validate.outputs.commit-id-short }}" + ARTIFACT_NAME="build-${{ needs.validate.outputs.branch-name-safe }}-${{ needs.validate.outputs.commit-id-short }}" else ARTIFACT_NAME="build-v${{ needs.validate.outputs.version }}" fi From 7c67d9f3024d0638879534f6f0ecbae90b4035aa Mon Sep 17 00:00:00 2001 From: Petar Tonev Date: Wed, 13 Mar 2024 08:53:55 +0200 Subject: [PATCH 080/115] fix: differential testing: add singleton stores mono and modular representation (#11275) Signed-off-by: Zhivko Kelchev Co-authored-by: Zhivko Kelchev --- .../signedstate/DumpBlockInfoSubcommand.java | 166 ++++++++++++++++ .../signedstate/DumpCongestionSubcommand.java | 153 ++++++++++++++ .../DumpPayerRecordsSubcommand.java | 137 +++++++++++++ .../DumpStakingInfoSubcommand.java | 188 ++++++++++++++++++ .../DumpStakingRewardsSubcommand.java | 128 ++++++++++++ .../cli/signedstate/DumpStateCommand.java | 85 ++++++++ .../cli/signedstate/SignedStateHolder.java | 34 ++++ .../singleton/BlockInfoAndRunningHashes.java | 127 ++++++++++++ .../app/bbm/singleton/BlockInfoDumpUtils.java | 131 ++++++++++++ .../node/app/bbm/singleton/Congestion.java | 79 ++++++++ .../bbm/singleton/CongestionDumpUtils.java | 111 +++++++++++ .../node/app/bbm/singleton/PayerRecord.java | 52 +++++ .../bbm/singleton/PayerRecordsDumpUtils.java | 126 ++++++++++++ .../node/app/bbm/singleton/StakingInfo.java | 68 +++++++ .../bbm/singleton/StakingInfoDumpUtils.java | 175 ++++++++++++++++ .../app/bbm/singleton/StakingRewards.java | 42 ++++ .../singleton/StakingRewardsDumpUtils.java | 104 ++++++++++ .../node/app/bbm/utils/ThingsToStrings.java | 5 + .../state/recordcache/RecordCacheService.java | 2 +- .../state/merkle/MerkleNetworkContext.java | 12 +- .../mono/state/merkle/MerkleStakingInfo.java | 2 +- 21 files changed, 1921 insertions(+), 6 deletions(-) create mode 100644 hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/DumpBlockInfoSubcommand.java create mode 100644 hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/DumpCongestionSubcommand.java create mode 100644 hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/DumpPayerRecordsSubcommand.java create mode 100644 hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/DumpStakingInfoSubcommand.java create mode 100644 hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/DumpStakingRewardsSubcommand.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/BlockInfoAndRunningHashes.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/BlockInfoDumpUtils.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/Congestion.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/CongestionDumpUtils.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/PayerRecord.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/PayerRecordsDumpUtils.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/StakingInfo.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/StakingInfoDumpUtils.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/StakingRewards.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/StakingRewardsDumpUtils.java diff --git a/hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/DumpBlockInfoSubcommand.java b/hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/DumpBlockInfoSubcommand.java new file mode 100644 index 000000000000..d621ddccd29f --- /dev/null +++ b/hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/DumpBlockInfoSubcommand.java @@ -0,0 +1,166 @@ +/* + * 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.cli.signedstate; + +import static com.hedera.services.cli.utils.ThingsToStrings.quoteForCsv; +import static java.util.Objects.requireNonNull; + +import com.hedera.node.app.service.mono.state.merkle.MerkleNetworkContext; +import com.hedera.node.app.service.mono.state.submerkle.RichInstant; +import com.hedera.node.app.service.mono.stream.RecordsRunningHashLeaf; +import com.hedera.services.cli.utils.FieldBuilder; +import com.hedera.services.cli.utils.ThingsToStrings; +import com.hedera.services.cli.utils.Writer; +import com.swirlds.base.utility.Pair; +import com.swirlds.common.crypto.Hash; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.file.Path; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** Dump block info from a signed state file to a text file in a deterministic order */ +public class DumpBlockInfoSubcommand { + + static void doit(@NonNull final SignedStateHolder state, @NonNull final Path blockInfoPath) { + new DumpBlockInfoSubcommand(state, blockInfoPath).doit(); + } + + @NonNull + final SignedStateHolder state; + + @NonNull + final Path blockInfoPath; + + DumpBlockInfoSubcommand(@NonNull final SignedStateHolder state, @NonNull final Path blockInfoPath) { + requireNonNull(state, "state"); + requireNonNull(blockInfoPath, "blockInfoPath"); + + this.state = state; + this.blockInfoPath = blockInfoPath; + } + + void doit() { + final var networkContext = state.getNetworkContext(); + System.out.printf("=== block info ===%n"); + + final var runningHashLeaf = state.getRunningHashLeaf(); + final var blockInfo = + BlockInfo.combineFromMerkleNetworkContextAndRunningHashLeaf(networkContext, runningHashLeaf); + + int reportSize; + try (@NonNull final var writer = new Writer(blockInfoPath)) { + reportOnBlockInfo(writer, blockInfo); + reportSize = writer.getSize(); + } + + System.out.printf("=== block info report is %d bytes%n", reportSize); + } + + @SuppressWarnings( + "java:S6218") // "Equals/hashcode method should be overridden in records containing array fields" - this + record BlockInfo( + long lastBlockNumber, + @NonNull String blockHashes, + @Nullable RichInstant consTimeOfLastHandledTxn, + boolean migrationRecordsStreamed, + @Nullable RichInstant firstConsTimeOfCurrentBlock, + long entityId, + @NonNull Hash runningHash, + @NonNull Hash nMinus1RunningHash, + @NonNull Hash nMinus2RunningHash, + @NonNull Hash nMinus3RunningHash) { + static BlockInfo combineFromMerkleNetworkContextAndRunningHashLeaf( + @NonNull final MerkleNetworkContext networkContext, + @NonNull RecordsRunningHashLeaf recordsRunningHashLeaf) { + return new BlockInfo( + networkContext.getAlignmentBlockNo(), + networkContext.stringifiedBlockHashes(), + RichInstant.fromJava(networkContext.consensusTimeOfLastHandledTxn()), + networkContext.areMigrationRecordsStreamed(), + RichInstant.fromJava(networkContext.firstConsTimeOfCurrentBlock()), + networkContext.seqNo().current(), + recordsRunningHashLeaf.getRunningHash().getHash(), + recordsRunningHashLeaf.getNMinus1RunningHash().getHash(), + recordsRunningHashLeaf.getNMinus2RunningHash().getHash(), + recordsRunningHashLeaf.getNMinus3RunningHash().getHash()); + } + } + + void reportOnBlockInfo(@NonNull Writer writer, @NonNull BlockInfo blockInfo) { + writer.writeln(formatHeader()); + formatBlockInfo(writer, blockInfo); + writer.writeln(""); + } + + void formatBlockInfo(@NonNull final Writer writer, @NonNull final BlockInfo blockInfo) { + final var fb = new FieldBuilder(FIELD_SEPARATOR); + fieldFormatters.stream().map(Pair::right).forEach(ff -> ff.accept(fb, blockInfo)); + writer.writeln(fb); + } + + @NonNull + String formatHeader() { + return fieldFormatters.stream().map(Pair::left).collect(Collectors.joining(FIELD_SEPARATOR)); + } + + static final String FIELD_SEPARATOR = ";"; + static Function booleanFormatter = b -> b ? "T" : ""; + static Function csvQuote = s -> quoteForCsv(FIELD_SEPARATOR, s); + + @NonNull + static List>> fieldFormatters = List.of( + Pair.of("lastBlockNumber", getFieldFormatter(BlockInfo::lastBlockNumber, Object::toString)), + Pair.of("blockHashes", getFieldFormatter(BlockInfo::blockHashes, Object::toString)), + Pair.of( + "consTimeOfLastHandledTxn", + getFieldFormatter( + BlockInfo::consTimeOfLastHandledTxn, + getNullableFormatter(ThingsToStrings::toStringOfRichInstant))), + Pair.of( + "migrationRecordsStreamed", + getFieldFormatter(BlockInfo::migrationRecordsStreamed, booleanFormatter)), + Pair.of( + "firstConsTimeOfCurrentBlock", + getFieldFormatter( + BlockInfo::firstConsTimeOfCurrentBlock, + getNullableFormatter(ThingsToStrings::toStringOfRichInstant))), + Pair.of("entityId", getFieldFormatter(BlockInfo::entityId, Object::toString)), + Pair.of("runningHash", getFieldFormatter(BlockInfo::runningHash, Object::toString)), + Pair.of("nMinus1RunningHash", getFieldFormatter(BlockInfo::nMinus1RunningHash, Object::toString)), + Pair.of("nMinus2RunningHash", getFieldFormatter(BlockInfo::nMinus2RunningHash, Object::toString)), + Pair.of("nMinus3RunningHas", getFieldFormatter(BlockInfo::nMinus3RunningHash, Object::toString))); + + static BiConsumer getFieldFormatter( + @NonNull final Function fun, @NonNull final Function formatter) { + return (fb, t) -> formatField(fb, t, fun, formatter); + } + + static Function getNullableFormatter(@NonNull final Function formatter) { + return t -> null != t ? formatter.apply(t) : ""; + } + + static void formatField( + @NonNull final FieldBuilder fb, + @NonNull final BlockInfo blockInfo, + @NonNull final Function fun, + @NonNull final Function formatter) { + fb.append(formatter.apply(fun.apply(blockInfo))); + } +} diff --git a/hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/DumpCongestionSubcommand.java b/hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/DumpCongestionSubcommand.java new file mode 100644 index 000000000000..838d44c239d6 --- /dev/null +++ b/hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/DumpCongestionSubcommand.java @@ -0,0 +1,153 @@ +/* + * 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.cli.signedstate; + +import static java.util.Objects.requireNonNull; + +import com.hedera.hapi.node.state.throttles.ThrottleUsageSnapshot; +import com.hedera.node.app.service.mono.pbj.PbjConverter; +import com.hedera.node.app.service.mono.state.merkle.MerkleNetworkContext; +import com.hedera.node.app.service.mono.state.submerkle.RichInstant; +import com.hedera.services.cli.utils.FieldBuilder; +import com.hedera.services.cli.utils.ThingsToStrings; +import com.hedera.services.cli.utils.Writer; +import com.swirlds.base.utility.Pair; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** Dump congestion from a signed state file to a text file in a deterministic order */ +public class DumpCongestionSubcommand { + + static void doit(@NonNull final SignedStateHolder state, @NonNull final Path congestionInfoPath) { + new DumpCongestionSubcommand(state, congestionInfoPath).doit(); + } + + @NonNull + final SignedStateHolder state; + + @NonNull + final Path congestionInfoPath; + + DumpCongestionSubcommand(@NonNull final SignedStateHolder state, @NonNull final Path congestionInfoPath) { + requireNonNull(state, "state"); + requireNonNull(congestionInfoPath, "congestionInfoPath"); + + this.state = state; + this.congestionInfoPath = congestionInfoPath; + } + + void doit() { + final var networkContext = state.getNetworkContext(); + System.out.printf("=== congestion ===%n"); + + final var congestion = Congestion.fromMerkleNetworkContext(networkContext); + + int reportSize; + try (@NonNull final var writer = new Writer(congestionInfoPath)) { + reportOnCongestion(writer, congestion); + reportSize = writer.getSize(); + } + + System.out.printf("=== congestion report is %d bytes%n", reportSize); + } + + @SuppressWarnings( + "java:S6218") // "Equals/hashcode method should be overridden in records containing array fields" - this + record Congestion( + @Nullable List tpsThrottles, + @Nullable ThrottleUsageSnapshot gasThrottle, + + // last two represented as Strings already formatted from List + @Nullable String genericLevelStarts, + @Nullable String gasLevelStarts) { + static Congestion fromMerkleNetworkContext(@NonNull final MerkleNetworkContext networkContext) { + final var tpsThrottleUsageSnapshots = Arrays.stream(networkContext.usageSnapshots()) + .map(PbjConverter::toPbj) + .toList(); + final var gasThrottleUsageSnapshot = PbjConverter.toPbj(networkContext.getGasThrottleUsageSnapshot()); + // format the following two from `List` to String + final var gasCongestionStarts = Arrays.stream( + networkContext.getMultiplierSources().gasCongestionStarts()) + .map(RichInstant::fromJava) + .map(ThingsToStrings::toStringOfRichInstant) + .collect(Collectors.joining(", ")); + final var genericCongestionStarts = Arrays.stream( + networkContext.getMultiplierSources().genericCongestionStarts()) + .map(RichInstant::fromJava) + .map(ThingsToStrings::toStringOfRichInstant) + .collect(Collectors.joining(", ")); + + return new Congestion( + tpsThrottleUsageSnapshots, gasThrottleUsageSnapshot, genericCongestionStarts, gasCongestionStarts); + } + } + + void reportOnCongestion(@NonNull Writer writer, @NonNull Congestion congestion) { + writer.writeln(formatHeader()); + formatCongestion(writer, congestion); + writer.writeln(""); + } + + void formatCongestion(@NonNull final Writer writer, @NonNull final Congestion congestion) { + final var fb = new FieldBuilder(FIELD_SEPARATOR); + fieldFormatters.stream().map(Pair::right).forEach(ff -> ff.accept(fb, congestion)); + writer.writeln(fb); + } + + @NonNull + String formatHeader() { + return fieldFormatters.stream().map(Pair::left).collect(Collectors.joining(FIELD_SEPARATOR)); + } + + static final String FIELD_SEPARATOR = ";"; + + @NonNull + static List>> fieldFormatters = List.of( + Pair.of( + "tpsThrottles", + getFieldFormatter(Congestion::tpsThrottles, getNullableFormatter(Object::toString))), + Pair.of("gasThrottle", getFieldFormatter(Congestion::gasThrottle, getNullableFormatter(Object::toString))), + Pair.of( + "genericLevelStarts", + getFieldFormatter(Congestion::genericLevelStarts, getNullableFormatter(Object::toString))), + Pair.of( + "gasLevelStarts", + getFieldFormatter(Congestion::gasLevelStarts, getNullableFormatter(Object::toString)))); + + static BiConsumer getFieldFormatter( + @NonNull final Function fun, @NonNull final Function formatter) { + return (fb, t) -> formatField(fb, t, fun, formatter); + } + + static Function getNullableFormatter(@NonNull final Function formatter) { + return t -> null != t ? formatter.apply(t) : ""; + } + + static void formatField( + @NonNull final FieldBuilder fb, + @NonNull final Congestion congestion, + @NonNull final Function fun, + @NonNull final Function formatter) { + fb.append(formatter.apply(fun.apply(congestion))); + } +} diff --git a/hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/DumpPayerRecordsSubcommand.java b/hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/DumpPayerRecordsSubcommand.java new file mode 100644 index 000000000000..9d35435ca592 --- /dev/null +++ b/hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/DumpPayerRecordsSubcommand.java @@ -0,0 +1,137 @@ +/* + * 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.cli.signedstate; + +import static java.util.Objects.requireNonNull; + +import com.hedera.node.app.service.mono.state.migration.RecordsStorageAdapter; +import com.hedera.node.app.service.mono.state.submerkle.EntityId; +import com.hedera.node.app.service.mono.state.submerkle.ExpirableTxnRecord; +import com.hedera.node.app.service.mono.state.submerkle.RichInstant; +import com.hedera.node.app.service.mono.state.submerkle.TxnId; +import com.hedera.services.cli.utils.FieldBuilder; +import com.hedera.services.cli.utils.ThingsToStrings; +import com.hedera.services.cli.utils.Writer; +import com.swirlds.base.utility.Pair; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** Dump payer records from a signed state file to a text file in a deterministic order */ +public class DumpPayerRecordsSubcommand { + + static void doit(@NonNull final SignedStateHolder state, @NonNull final Path payerRecordsPath) { + new DumpPayerRecordsSubcommand(state, payerRecordsPath).doit(); + } + + @NonNull + final SignedStateHolder state; + + @NonNull + final Path payerRecordsPath; + + DumpPayerRecordsSubcommand(@NonNull final SignedStateHolder state, @NonNull final Path payerRecordsPath) { + requireNonNull(state, "state"); + requireNonNull(payerRecordsPath, "payerRecordsPath"); + + this.state = state; + this.payerRecordsPath = payerRecordsPath; + } + + void doit() { + final var payerRecordsQueue = state.getPayerRecords(); + System.out.printf("=== payer records ===%n"); + + final var records = gatherTxnRecordsFromMono(payerRecordsQueue); + + int reportSize; + try (@NonNull final var writer = new Writer(payerRecordsPath)) { + reportOnTxnRecords(writer, records); + reportSize = writer.getSize(); + } + + System.out.printf("=== payer records report is %d bytes%n", reportSize); + } + + private static List gatherTxnRecordsFromMono(RecordsStorageAdapter recordsStorageAdapter) { + final var listTxnRecords = new ArrayList(); + recordsStorageAdapter.doForEach((payer, fcq) -> { + fcq.stream().forEach(p -> listTxnRecords.add(PayerRecord.fromExpirableTxnRecord(p))); + }); + return listTxnRecords; + } + + @SuppressWarnings( + "java:S6218") // "Equals/hashcode method should be overridden in records containing array fields" - this + public record PayerRecord( + @NonNull TxnId transactionId, @NonNull RichInstant consensusTime, @NonNull EntityId payer) { + + public static PayerRecord fromExpirableTxnRecord(@NonNull ExpirableTxnRecord record) { + return new PayerRecord( + record.getTxnId(), + record.getConsensusTime(), + record.getTxnId().getPayerAccount()); + } + } + + static void reportOnTxnRecords(@NonNull Writer writer, @NonNull List records) { + writer.writeln(formatHeader()); + records.stream() + .sorted(Comparator.comparing(PayerRecord::consensusTime)) + .forEach(e -> formatRecords(writer, e)); + writer.writeln(""); + } + + static void formatRecords(@NonNull final Writer writer, @NonNull final PayerRecord record) { + final var fb = new FieldBuilder(FIELD_SEPARATOR); + fieldFormatters.stream().map(Pair::right).forEach(ff -> ff.accept(fb, record)); + writer.writeln(fb); + } + + @NonNull + static String formatHeader() { + return fieldFormatters.stream().map(Pair::left).collect(Collectors.joining(FIELD_SEPARATOR)); + } + + static final String FIELD_SEPARATOR = ";"; + + @NonNull + static List>> fieldFormatters = List.of( + Pair.of("txnId", getFieldFormatter(PayerRecord::transactionId, Object::toString)), + Pair.of( + "consensusTime", + getFieldFormatter(PayerRecord::consensusTime, ThingsToStrings::toStringOfRichInstant)), + Pair.of("payer", getFieldFormatter(PayerRecord::payer, ThingsToStrings::toStringOfEntityId))); + + static BiConsumer getFieldFormatter( + @NonNull final Function fun, @NonNull final Function formatter) { + return (fb, t) -> formatField(fb, t, fun, formatter); + } + + static void formatField( + @NonNull final FieldBuilder fb, + @NonNull final PayerRecord record, + @NonNull final Function fun, + @NonNull final Function formatter) { + fb.append(formatter.apply(fun.apply(record))); + } +} diff --git a/hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/DumpStakingInfoSubcommand.java b/hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/DumpStakingInfoSubcommand.java new file mode 100644 index 000000000000..eee139caf730 --- /dev/null +++ b/hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/DumpStakingInfoSubcommand.java @@ -0,0 +1,188 @@ +/* + * 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.cli.signedstate; + +import static java.util.Objects.requireNonNull; + +import com.hedera.node.app.service.mono.state.adapters.MerkleMapLike; +import com.hedera.node.app.service.mono.state.merkle.MerkleStakingInfo; +import com.hedera.node.app.service.mono.utils.EntityNum; +import com.hedera.services.cli.signedstate.DumpStateCommand.EmitSummary; +import com.hedera.services.cli.signedstate.SignedStateCommand.Verbosity; +import com.hedera.services.cli.utils.FieldBuilder; +import com.hedera.services.cli.utils.Writer; +import com.swirlds.base.utility.Pair; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** Dump staking info from a signed state file to a text file in a deterministic order */ +public class DumpStakingInfoSubcommand { + + static void doit( + @NonNull final SignedStateHolder state, + @NonNull final Path stakingInfoPath, + @NonNull final EmitSummary emitSummary, + @NonNull final Verbosity verbosity) { + new DumpStakingInfoSubcommand(state, stakingInfoPath, emitSummary, verbosity).doit(); + } + + @NonNull + final SignedStateHolder state; + + @NonNull + final Path stakingInfoPath; + + @NonNull + final EmitSummary emitSummary; + + @NonNull + final Verbosity verbosity; + + DumpStakingInfoSubcommand( + @NonNull final SignedStateHolder state, + @NonNull final Path stakingInfoPath, + @NonNull final EmitSummary emitSummary, + @NonNull final Verbosity verbosity) { + requireNonNull(state, "state"); + requireNonNull(stakingInfoPath, "stakingInfoPath"); + requireNonNull(emitSummary, "emitSummary"); + requireNonNull(verbosity, "verbosity"); + + this.state = state; + this.stakingInfoPath = stakingInfoPath; + this.emitSummary = emitSummary; + this.verbosity = verbosity; + } + + void doit() { + final var stakingInfoStore = state.getStakingInfo(); + System.out.printf("=== %d staking info ===%n", stakingInfoStore.size()); + + final var allStakingInfo = gatherStakingInfo(stakingInfoStore); + + int reportSize; + try (@NonNull final var writer = new Writer(stakingInfoPath)) { + if (emitSummary == EmitSummary.YES) reportSummary(writer, allStakingInfo); + reportOnStakingInfo(writer, allStakingInfo); + reportSize = writer.getSize(); + } + + System.out.printf("=== staking info report is %d bytes%n", reportSize); + } + + @SuppressWarnings( + "java:S6218") // "Equals/hashcode method should be overridden in records containing array fields" - this + record StakingInfo( + int number, + long minStake, + long maxStake, + long stakeToReward, + long stakeToNotReward, + long stakeRewardStart, + long unclaimedStakeRewardStart, + long stake, + @NonNull long[] rewardSumHistory, + int weight) { + StakingInfo(@NonNull final MerkleStakingInfo stakingInfo) { + this( + stakingInfo.getKey().intValue(), + stakingInfo.getMinStake(), + stakingInfo.getMaxStake(), + stakingInfo.getStakeToReward(), + stakingInfo.getStakeToNotReward(), + stakingInfo.getStakeRewardStart(), + stakingInfo.getUnclaimedStakeRewardStart(), + stakingInfo.getStake(), + stakingInfo.getRewardSumHistory(), + stakingInfo.getWeight()); + Objects.requireNonNull(rewardSumHistory, "rewardSumHistory"); + } + } + + @NonNull + Map gatherStakingInfo( + @NonNull final MerkleMapLike stakingInfoStore) { + final var allStakingInfo = new TreeMap(); + stakingInfoStore.forEachNode((en, mt) -> allStakingInfo.put(en.longValue(), new StakingInfo(mt))); + return allStakingInfo; + } + + void reportSummary(@NonNull Writer writer, @NonNull Map stakingInfo) { + writer.writeln("=== %7d: staking info".formatted(stakingInfo.size())); + writer.writeln(""); + } + + void reportOnStakingInfo(@NonNull Writer writer, @NonNull Map stakingInfo) { + writer.writeln(formatHeader()); + stakingInfo.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .forEach(e -> formatStakingInfo(writer, e.getValue())); + writer.writeln(""); + } + + void formatStakingInfo(@NonNull final Writer writer, @NonNull final StakingInfo stakingInfo) { + final var fb = new FieldBuilder(FIELD_SEPARATOR); + fieldFormatters.stream().map(Pair::right).forEach(ff -> ff.accept(fb, stakingInfo)); + writer.writeln(fb); + } + + @NonNull + String formatHeader() { + return fieldFormatters.stream().map(Pair::left).collect(Collectors.joining(FIELD_SEPARATOR)); + } + + static final String FIELD_SEPARATOR = ";"; + + @NonNull + static List>> fieldFormatters = List.of( + Pair.of("number", getFieldFormatter(StakingInfo::number, Object::toString)), + Pair.of("minStake", getFieldFormatter(StakingInfo::minStake, Object::toString)), + Pair.of("maxStake", getFieldFormatter(StakingInfo::maxStake, Object::toString)), + Pair.of("stakeToReward", getFieldFormatter(StakingInfo::stakeToReward, Object::toString)), + Pair.of("stakeToNotReward", getFieldFormatter(StakingInfo::stakeToNotReward, Object::toString)), + Pair.of("stakeRewardStart", getFieldFormatter(StakingInfo::stakeRewardStart, Object::toString)), + Pair.of( + "unclaimedStakeRewardStart", + getFieldFormatter(StakingInfo::unclaimedStakeRewardStart, Object::toString)), + Pair.of("stake", getFieldFormatter(StakingInfo::stake, Object::toString)), + Pair.of("rewardSumHistory", getFieldFormatter(StakingInfo::rewardSumHistory, Object::toString)), + Pair.of("weight", getFieldFormatter(StakingInfo::weight, Object::toString))); + + static BiConsumer getFieldFormatter( + @NonNull final Function fun, @NonNull final Function formatter) { + return (fb, t) -> formatField(fb, t, fun, formatter); + } + + static Function getNullableFormatter(@NonNull final Function formatter) { + return t -> null != t ? formatter.apply(t) : ""; + } + + static void formatField( + @NonNull final FieldBuilder fb, + @NonNull final StakingInfo stakingInfo, + @NonNull final Function fun, + @NonNull final Function formatter) { + fb.append(formatter.apply(fun.apply(stakingInfo))); + } +} diff --git a/hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/DumpStakingRewardsSubcommand.java b/hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/DumpStakingRewardsSubcommand.java new file mode 100644 index 000000000000..0bd189ba8aa5 --- /dev/null +++ b/hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/DumpStakingRewardsSubcommand.java @@ -0,0 +1,128 @@ +/* + * 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.cli.signedstate; + +import static java.util.Objects.requireNonNull; + +import com.hedera.node.app.service.mono.state.merkle.MerkleNetworkContext; +import com.hedera.services.cli.utils.FieldBuilder; +import com.hedera.services.cli.utils.Writer; +import com.swirlds.base.utility.Pair; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.file.Path; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** Dump staking rewards from a signed state file to a text file in a deterministic order */ +public class DumpStakingRewardsSubcommand { + + static void doit(@NonNull final SignedStateHolder state, @NonNull final Path stakingRewardsPath) { + new DumpStakingRewardsSubcommand(state, stakingRewardsPath).doit(); + } + + @NonNull + final SignedStateHolder state; + + @NonNull + final Path stakingRewardsPath; + + DumpStakingRewardsSubcommand(@NonNull final SignedStateHolder state, @NonNull final Path stakingRewardsPath) { + requireNonNull(state, "state"); + requireNonNull(stakingRewardsPath, "stakingRewardsPath"); + + this.state = state; + this.stakingRewardsPath = stakingRewardsPath; + } + + void doit() { + final var networkContext = state.getNetworkContext(); + System.out.printf("=== staking rewards ===%n"); + + final var stakingRewards = StakingRewards.fromMerkleNetworkContext(networkContext); + + int reportSize; + try (@NonNull final var writer = new Writer(stakingRewardsPath)) { + reportOnStakingRewards(writer, stakingRewards); + reportSize = writer.getSize(); + } + + System.out.printf("=== staking rewards report is %d bytes%n", reportSize); + } + + @SuppressWarnings( + "java:S6218") // "Equals/hashcode method should be overridden in records containing array fields" - this + public record StakingRewards( + boolean stakingRewardsActivated, long totalStakedRewardStart, long totalStakedStart, long pendingRewards) { + + public static StakingRewards fromMerkleNetworkContext( + @NonNull final MerkleNetworkContext merkleNetworkContext) { + + return new StakingRewards( + merkleNetworkContext.areRewardsActivated(), + merkleNetworkContext.getTotalStakedRewardStart(), + merkleNetworkContext.getTotalStakedStart(), + merkleNetworkContext.pendingRewards()); + } + } + + void reportOnStakingRewards(@NonNull Writer writer, @NonNull StakingRewards stakingRewards) { + writer.writeln(formatHeader()); + formatStakingRewards(writer, stakingRewards); + writer.writeln(""); + } + + void formatStakingRewards(@NonNull final Writer writer, @NonNull final StakingRewards stakingRewards) { + final var fb = new FieldBuilder(FIELD_SEPARATOR); + fieldFormatters.stream().map(Pair::right).forEach(ff -> ff.accept(fb, stakingRewards)); + writer.writeln(fb); + } + + @NonNull + String formatHeader() { + return fieldFormatters.stream().map(Pair::left).collect(Collectors.joining(FIELD_SEPARATOR)); + } + + static final String FIELD_SEPARATOR = ";"; + + static Function booleanFormatter = b -> b ? "T" : ""; + + @NonNull + static List>> fieldFormatters = List.of( + Pair.of( + "stakingRewardsActivated", + getFieldFormatter(StakingRewards::stakingRewardsActivated, booleanFormatter)), + Pair.of( + "totalStakedRewardStart", + getFieldFormatter(StakingRewards::totalStakedRewardStart, Object::toString)), + Pair.of("totalStakedStart", getFieldFormatter(StakingRewards::totalStakedStart, Object::toString)), + Pair.of("pendingRewards", getFieldFormatter(StakingRewards::pendingRewards, Object::toString))); + + static BiConsumer getFieldFormatter( + @NonNull final Function fun, @NonNull final Function formatter) { + return (fb, t) -> formatField(fb, t, fun, formatter); + } + + static void formatField( + @NonNull final FieldBuilder fb, + @NonNull final StakingRewards stakingRewards, + @NonNull final Function fun, + @NonNull final Function formatter) { + fb.append(formatter.apply(fun.apply(stakingRewards))); + } +} diff --git a/hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/DumpStateCommand.java b/hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/DumpStateCommand.java index 769d49ee3f87..0655cebf9075 100644 --- a/hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/DumpStateCommand.java +++ b/hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/DumpStateCommand.java @@ -334,6 +334,91 @@ void associations( finish(); } + @Command(name = "block-info", description = "Dump block info") + void blockInfo( + @Option( + names = {"--block-info"}, + required = true, + arity = "1", + description = "Output file for block info dump") + @NonNull + final Path blockInfoPath) { + Objects.requireNonNull(blockInfoPath); + init(); + System.out.println("=== Block info ==="); + DumpBlockInfoSubcommand.doit(parent.signedState, blockInfoPath); + finish(); + } + + @Command(name = "staking-info", description = "Dump staking info") + void stakingInfo( + @Option( + names = {"--staking-info"}, + required = true, + arity = "1", + description = "Output file for staking info dump") + @NonNull + final Path stakingInfoPath, + @Option( + names = {"-s", "--summary"}, + description = "Emit summary information") + final boolean emitSummary) { + Objects.requireNonNull(stakingInfoPath); + init(); + System.out.println("=== Staking info ==="); + DumpStakingInfoSubcommand.doit( + parent.signedState, stakingInfoPath, emitSummary ? EmitSummary.YES : EmitSummary.NO, parent.verbosity); + finish(); + } + + @Command(name = "staking-rewards", description = "Dump staking rewards") + void stakingRewards( + @Option( + names = {"--staking-rewards"}, + required = true, + arity = "1", + description = "Output file for staking rewards dump") + @NonNull + final Path stakingRewardsPath) { + Objects.requireNonNull(stakingRewardsPath); + init(); + System.out.println("=== Staking rewards ==="); + DumpStakingRewardsSubcommand.doit(parent.signedState, stakingRewardsPath); + finish(); + } + + @Command(name = "payer-records", description = "Dump payer records") + void payerRecords( + @Option( + names = {"--payer-records"}, + required = true, + arity = "1", + description = "Output file for payer records dump") + @NonNull + final Path payerRecordsPath) { + Objects.requireNonNull(payerRecordsPath); + init(); + System.out.println("=== Payer records ==="); + DumpPayerRecordsSubcommand.doit(parent.signedState, payerRecordsPath); + finish(); + } + + @Command(name = "congestion", description = "Dump congestion") + void congestion( + @Option( + names = {"--congestion"}, + required = true, + arity = "1", + description = "Output file for congestion dump") + @NonNull + final Path congestionPath) { + Objects.requireNonNull(congestionPath); + init(); + System.out.println("=== Congestion ==="); + DumpCongestionSubcommand.doit(parent.signedState, congestionPath); + finish(); + } + @Command(name = "topics", description = "Dump topics") void topics( @Option( diff --git a/hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/SignedStateHolder.java b/hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/SignedStateHolder.java index 34a559793399..d818e82ebb18 100644 --- a/hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/SignedStateHolder.java +++ b/hedera-node/cli-clients/src/main/java/com/hedera/services/cli/signedstate/SignedStateHolder.java @@ -19,11 +19,14 @@ import com.hedera.node.app.service.mono.ServicesState; import com.hedera.node.app.service.mono.state.adapters.MerkleMapLike; import com.hedera.node.app.service.mono.state.adapters.VirtualMapLike; +import com.hedera.node.app.service.mono.state.merkle.MerkleNetworkContext; import com.hedera.node.app.service.mono.state.merkle.MerkleScheduledTransactions; import com.hedera.node.app.service.mono.state.merkle.MerkleSpecialFiles; +import com.hedera.node.app.service.mono.state.merkle.MerkleStakingInfo; import com.hedera.node.app.service.mono.state.merkle.MerkleToken; import com.hedera.node.app.service.mono.state.merkle.MerkleTopic; import com.hedera.node.app.service.mono.state.migration.AccountStorageAdapter; +import com.hedera.node.app.service.mono.state.migration.RecordsStorageAdapter; import com.hedera.node.app.service.mono.state.migration.TokenRelStorageAdapter; import com.hedera.node.app.service.mono.state.migration.UniqueTokenMapAdapter; import com.hedera.node.app.service.mono.state.virtual.ContractKey; @@ -31,6 +34,7 @@ import com.hedera.node.app.service.mono.state.virtual.VirtualBlobKey; import com.hedera.node.app.service.mono.state.virtual.VirtualBlobKey.Type; import com.hedera.node.app.service.mono.state.virtual.VirtualBlobValue; +import com.hedera.node.app.service.mono.stream.RecordsRunningHashLeaf; import com.hedera.node.app.service.mono.utils.EntityNum; import com.swirlds.base.time.Time; import com.swirlds.common.AutoCloseableNonThrowing; @@ -315,6 +319,36 @@ public MerkleScheduledTransactions getScheduledTransactions() { return scheduledTransactions; } + // Returns the network context store from the state + @NonNull + public MerkleNetworkContext getNetworkContext() { + final var networkContext = servicesState.networkCtx(); + assertSignedStateComponentExists(networkContext, "networkContext"); + return networkContext; + } + + // Returns the staking info store from the state + @NonNull + public MerkleMapLike getStakingInfo() { + final var stakingInfo = servicesState.stakingInfo(); + assertSignedStateComponentExists(stakingInfo, "stakingInfo"); + return stakingInfo; + } + + @NonNull + public RecordsRunningHashLeaf getRunningHashLeaf() { + final var runningHashLeaf = servicesState.runningHashLeaf(); + assertSignedStateComponentExists(runningHashLeaf, "runningHashLeaf"); + return runningHashLeaf; + } + + @NonNull + public RecordsStorageAdapter getPayerRecords() { + final var payerRecords = servicesState.payerRecords(); + assertSignedStateComponentExists(payerRecords, "payerRecords"); + return payerRecords; + } + /** Deserialize the signed state file into an in-memory data structure. */ @NonNull private Pair dehydrate(@NonNull final List configurationPaths) { diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/BlockInfoAndRunningHashes.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/BlockInfoAndRunningHashes.java new file mode 100644 index 000000000000..6760aa42cd4f --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/BlockInfoAndRunningHashes.java @@ -0,0 +1,127 @@ +/* + * 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.bbm.singleton; + +import static java.util.Objects.requireNonNull; + +import com.hedera.hapi.node.state.blockrecords.BlockInfo; +import com.hedera.hapi.node.state.blockrecords.RunningHashes; +import com.hedera.node.app.records.impl.BlockRecordInfoUtils; +import com.hedera.node.app.service.mono.state.merkle.MerkleNetworkContext; +import com.hedera.node.app.service.mono.state.submerkle.RichInstant; +import com.hedera.node.app.service.mono.stream.RecordsRunningHashLeaf; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.common.crypto.Hash; +import com.swirlds.common.utility.CommonUtils; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.time.Instant; + +record BlockInfoAndRunningHashes( + long lastBlockNumber, + @NonNull String blockHashes, + @Nullable RichInstant consTimeOfLastHandledTxn, + boolean migrationRecordsStreamed, + @Nullable RichInstant firstConsTimeOfCurrentBlock, + long entityId, + @Nullable Hash runningHash, + @Nullable Hash nMinus1RunningHash, + @Nullable Hash nMinus2RunningHash, + @Nullable Hash nMinus3RunningHash) { + + public static BlockInfoAndRunningHashes combineFromMono( + @NonNull final MerkleNetworkContext merkleNetworkContext, + @NonNull final RecordsRunningHashLeaf recordsRunningHashLeaf) { + requireNonNull(merkleNetworkContext); + requireNonNull(recordsRunningHashLeaf); + return new BlockInfoAndRunningHashes( + merkleNetworkContext.getAlignmentBlockNo(), + merkleNetworkContext.stringifiedBlockHashes(), + RichInstant.fromJava(merkleNetworkContext.consensusTimeOfLastHandledTxn()), + merkleNetworkContext.areMigrationRecordsStreamed(), + RichInstant.fromJava(merkleNetworkContext.firstConsTimeOfCurrentBlock()), + merkleNetworkContext.seqNo().current(), + recordsRunningHashLeaf.getRunningHash().getHash(), + recordsRunningHashLeaf.getNMinus1RunningHash().getHash(), + recordsRunningHashLeaf.getNMinus2RunningHash().getHash(), + recordsRunningHashLeaf.getNMinus3RunningHash().getHash()); + } + + public static BlockInfoAndRunningHashes combineFromMod( + @NonNull final BlockInfo blockInfo, @NonNull final RunningHashes runningHashes, final long entityId) { + + // convert all TimeStamps fields from blockInfo to RichInstant + var consTimeOfLastHandledTxn = blockInfo.consTimeOfLastHandledTxn() == null + ? RichInstant.fromJava(Instant.EPOCH) + : new RichInstant( + blockInfo.consTimeOfLastHandledTxn().seconds(), + blockInfo.consTimeOfLastHandledTxn().nanos()); + var firstConsTimeOfCurrentBlock = blockInfo.firstConsTimeOfCurrentBlock() == null + ? RichInstant.fromJava(Instant.EPOCH) + : new RichInstant( + blockInfo.firstConsTimeOfCurrentBlock().seconds(), + blockInfo.firstConsTimeOfCurrentBlock().nanos()); + + var runningHash = Bytes.EMPTY.equals(runningHashes.runningHash()) + ? null + : new Hash(runningHashes.runningHash().toByteArray()); + var nMinus1RunningHash = Bytes.EMPTY.equals(runningHashes.nMinus1RunningHash()) + ? null + : new Hash(runningHashes.nMinus1RunningHash().toByteArray()); + var nMinus2RunningHash = Bytes.EMPTY.equals(runningHashes.nMinus2RunningHash()) + ? null + : new Hash(runningHashes.nMinus2RunningHash().toByteArray()); + var nMinus3RunningHash = Bytes.EMPTY.equals(runningHashes.nMinus3RunningHash()) + ? null + : new Hash(runningHashes.nMinus3RunningHash().toByteArray()); + + return new BlockInfoAndRunningHashes( + blockInfo.lastBlockNumber(), + stringifiedBlockHashes(blockInfo), + consTimeOfLastHandledTxn, + blockInfo.migrationRecordsStreamed(), + firstConsTimeOfCurrentBlock, + entityId, + runningHash, + nMinus1RunningHash, + nMinus2RunningHash, + nMinus3RunningHash); + } + + // generate same string format for hashes, as MerkelNetworkContext.stringifiedBlockHashes() for mod + static String stringifiedBlockHashes(BlockInfo blockInfo) { + final var jsonSb = new StringBuilder("["); + final var blockNo = blockInfo.lastBlockNumber(); + final var blockHashes = blockInfo.blockHashes(); + final var availableBlocksCount = blockHashes.length() / BlockRecordInfoUtils.HASH_SIZE; + final var firstAvailable = blockNo - availableBlocksCount; + + for (int i = 0; i < availableBlocksCount; i++) { + final var nextBlockNo = firstAvailable + i; + final var blockHash = + blockHashes.toByteArray(i * BlockRecordInfoUtils.HASH_SIZE, BlockRecordInfoUtils.HASH_SIZE); + jsonSb.append("{\"num\": ") + .append(nextBlockNo + 1) + .append(", ") + .append("\"hash\": \"") + .append(CommonUtils.hex(blockHash)) + .append("\"}") + .append(i < availableBlocksCount ? ", " : ""); + } + return jsonSb.append("]").toString(); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/BlockInfoDumpUtils.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/BlockInfoDumpUtils.java new file mode 100644 index 000000000000..ea74f42ef648 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/BlockInfoDumpUtils.java @@ -0,0 +1,131 @@ +/* + * 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.bbm.singleton; + +import com.hedera.hapi.node.state.blockrecords.BlockInfo; +import com.hedera.hapi.node.state.blockrecords.RunningHashes; +import com.hedera.hapi.node.state.common.EntityNumber; +import com.hedera.node.app.bbm.DumpCheckpoint; +import com.hedera.node.app.bbm.utils.FieldBuilder; +import com.hedera.node.app.bbm.utils.ThingsToStrings; +import com.hedera.node.app.bbm.utils.Writer; +import com.hedera.node.app.service.mono.state.merkle.MerkleNetworkContext; +import com.hedera.node.app.service.mono.stream.RecordsRunningHashLeaf; +import com.swirlds.base.utility.Pair; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.file.Path; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class BlockInfoDumpUtils { + static Function booleanFormatter = b -> b ? "T" : ""; + + // spotless:off + @NonNull + static List>> fieldFormatters = List.of( + Pair.of("lastBlockNumber", getFieldFormatter(BlockInfoAndRunningHashes::lastBlockNumber, Object::toString)), + Pair.of("blockHashes", getFieldFormatter(BlockInfoAndRunningHashes::blockHashes, Object::toString)), + Pair.of( + "consTimeOfLastHandledTxn", + getFieldFormatter( + BlockInfoAndRunningHashes::consTimeOfLastHandledTxn, + getNullableFormatter(ThingsToStrings::toStringOfRichInstant))), + Pair.of( + "migrationRecordsStreamed", + getFieldFormatter(BlockInfoAndRunningHashes::migrationRecordsStreamed, booleanFormatter)), + Pair.of( + "firstConsTimeOfCurrentBlock", + getFieldFormatter( + BlockInfoAndRunningHashes::firstConsTimeOfCurrentBlock, + getNullableFormatter(ThingsToStrings::toStringOfRichInstant))), + Pair.of("entityId", getFieldFormatter(BlockInfoAndRunningHashes::entityId, Object::toString)), + Pair.of("runningHash", getFieldFormatter(BlockInfoAndRunningHashes::runningHash, getNullableFormatter(Object::toString))), + Pair.of("nMinus1RunningHash", getFieldFormatter(BlockInfoAndRunningHashes::nMinus1RunningHash, getNullableFormatter(Object::toString))), + Pair.of("nMinus2RunningHash", getFieldFormatter(BlockInfoAndRunningHashes::nMinus2RunningHash, getNullableFormatter(Object::toString))), + Pair.of("nMinus3RunningHas", getFieldFormatter(BlockInfoAndRunningHashes::nMinus3RunningHash, getNullableFormatter(Object::toString)))); + // spotless:on + + public static void dumpModBlockInfo( + @NonNull final Path path, + @NonNull final RunningHashes runningHashes, + @NonNull final BlockInfo blockInfo, + @NonNull final EntityNumber entityNumber, + @NonNull final DumpCheckpoint checkpoint) { + try (@NonNull final var writer = new Writer(path)) { + var combined = BlockInfoAndRunningHashes.combineFromMod(blockInfo, runningHashes, entityNumber.number()); + reportOnBlockInfo(writer, combined); + System.out.printf( + "=== mod running hashes and block info report is %d bytes at checkpoint %s%n", + writer.getSize(), checkpoint.name()); + } + } + + public static void dumpMonoBlockInfo( + @NonNull final Path path, + @NonNull final MerkleNetworkContext merkleNetworkContext, + @NonNull final RecordsRunningHashLeaf recordsRunningHashLeaf, + @NonNull final DumpCheckpoint checkpoint) { + try (@NonNull final var writer = new Writer(path)) { + final var combined = + BlockInfoAndRunningHashes.combineFromMono(merkleNetworkContext, recordsRunningHashLeaf); + reportOnBlockInfo(writer, combined); + + System.out.printf( + "=== mono running hashes and block info report is %d bytes at checkpoint %s%n", + writer.getSize(), checkpoint.name()); + } + } + + private static void reportOnBlockInfo( + @NonNull final Writer writer, @NonNull final BlockInfoAndRunningHashes combinedBlockInfoAndRunningHashes) { + writer.writeln(formatHeaderForBlockInfo()); + formatBlockInfo(writer, combinedBlockInfoAndRunningHashes); + writer.writeln(""); + } + + @NonNull + private static String formatHeaderForBlockInfo() { + return fieldFormatters.stream().map(Pair::left).collect(Collectors.joining(Writer.FIELD_SEPARATOR)); + } + + @NonNull + static BiConsumer getFieldFormatter( + @NonNull final Function fun, @NonNull final Function formatter) { + return (fb, u) -> formatField(fb, u, fun, formatter); + } + + static void formatField( + @NonNull final FieldBuilder fb, + @NonNull final BlockInfoAndRunningHashes info, + @NonNull final Function fun, + @NonNull final Function formatter) { + fb.append(formatter.apply(fun.apply(info))); + } + + private static void formatBlockInfo( + @NonNull final Writer writer, @NonNull final BlockInfoAndRunningHashes combinedBlockInfoAndRunningHashes) { + final var fb = new FieldBuilder(Writer.FIELD_SEPARATOR); + fieldFormatters.stream().map(Pair::right).forEach(ff -> ff.accept(fb, combinedBlockInfoAndRunningHashes)); + writer.writeln(fb); + } + + static Function getNullableFormatter(@NonNull final Function formatter) { + return t -> null != t ? formatter.apply(t) : ""; + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/Congestion.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/Congestion.java new file mode 100644 index 000000000000..53f12b4a3ec9 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/Congestion.java @@ -0,0 +1,79 @@ +/* + * 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.bbm.singleton; + +import com.hedera.hapi.node.state.congestion.CongestionLevelStarts; +import com.hedera.hapi.node.state.throttles.ThrottleUsageSnapshot; +import com.hedera.hapi.node.state.throttles.ThrottleUsageSnapshots; +import com.hedera.node.app.bbm.utils.ThingsToStrings; +import com.hedera.node.app.service.mono.pbj.PbjConverter; +import com.hedera.node.app.service.mono.state.merkle.MerkleNetworkContext; +import com.hedera.node.app.service.mono.state.submerkle.RichInstant; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +record Congestion( + @Nullable List tpsThrottles, + @Nullable ThrottleUsageSnapshot gasThrottle, + + // last two represented as Strings already formatted from List + @Nullable String genericLevelStarts, + @Nullable String gasLevelStarts) { + static Congestion fromMerkleNetworkContext(@NonNull final MerkleNetworkContext networkContext) { + final var tpsThrottleUsageSnapshots = Arrays.stream(networkContext.usageSnapshots()) + .map(PbjConverter::toPbj) + .toList(); + final var gasThrottleUsageSnapshot = PbjConverter.toPbj(networkContext.getGasThrottleUsageSnapshot()); + // format the following two from `List` to String + final var gasCongestionStarts = Arrays.stream( + networkContext.getMultiplierSources().gasCongestionStarts()) + .map(RichInstant::fromJava) + .map(ThingsToStrings::toStringOfRichInstant) + .collect(Collectors.joining(", ")); + final var genericCongestionStarts = Arrays.stream( + networkContext.getMultiplierSources().genericCongestionStarts()) + .map(RichInstant::fromJava) + .map(ThingsToStrings::toStringOfRichInstant) + .collect(Collectors.joining(", ")); + + return new Congestion( + tpsThrottleUsageSnapshots, gasThrottleUsageSnapshot, genericCongestionStarts, gasCongestionStarts); + } + + static Congestion fromMod( + @NonNull final CongestionLevelStarts congestionLevelStarts, + @NonNull final ThrottleUsageSnapshots throttleUsageSnapshots) { + + final var tpsThrottleUsageSnapshots = throttleUsageSnapshots.tpsThrottles(); + + final var gasThrottleUsageSnapshot = throttleUsageSnapshots.gasThrottle(); + + // format the following two from `List` to String + final var gasCongestionStarts = congestionLevelStarts.gasLevelStarts().stream() + .map(ThingsToStrings::toStringOfTimestamp) + .collect(Collectors.joining(", ")); + final var genericCongestionStarts = congestionLevelStarts.genericLevelStarts().stream() + .map(ThingsToStrings::toStringOfTimestamp) + .collect(Collectors.joining(", ")); + + return new Congestion( + tpsThrottleUsageSnapshots, gasThrottleUsageSnapshot, genericCongestionStarts, gasCongestionStarts); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/CongestionDumpUtils.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/CongestionDumpUtils.java new file mode 100644 index 000000000000..6af8e8980c2c --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/CongestionDumpUtils.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.node.app.bbm.singleton; + +import com.hedera.hapi.node.state.congestion.CongestionLevelStarts; +import com.hedera.hapi.node.state.throttles.ThrottleUsageSnapshots; +import com.hedera.node.app.bbm.DumpCheckpoint; +import com.hedera.node.app.bbm.utils.FieldBuilder; +import com.hedera.node.app.bbm.utils.Writer; +import com.hedera.node.app.service.mono.state.merkle.MerkleNetworkContext; +import com.swirlds.base.utility.Pair; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.file.Path; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class CongestionDumpUtils { + + static final String FIELD_SEPARATOR = ";"; + + @NonNull + static List>> fieldFormatters = List.of( + Pair.of( + "tpsThrottles", + getFieldFormatter(Congestion::tpsThrottles, getNullableFormatter(Object::toString))), + Pair.of("gasThrottle", getFieldFormatter(Congestion::gasThrottle, getNullableFormatter(Object::toString))), + Pair.of( + "genericLevelStarts", + getFieldFormatter(Congestion::genericLevelStarts, getNullableFormatter(Object::toString))), + Pair.of( + "gasLevelStarts", + getFieldFormatter(Congestion::gasLevelStarts, getNullableFormatter(Object::toString)))); + + public static void dumpMonoCongestion( + @NonNull final Path path, + @NonNull final MerkleNetworkContext merkleNetworkContext, + @NonNull final DumpCheckpoint checkpoint) { + + int reportSize; + try (@NonNull final var writer = new Writer(path)) { + reportOnCongestion(writer, Congestion.fromMerkleNetworkContext(merkleNetworkContext)); + reportSize = writer.getSize(); + } + + System.out.printf("=== staking rewards report is %d bytes %n", reportSize); + } + + public static void dumpModCongestion( + @NonNull final Path path, + @NonNull final CongestionLevelStarts congestionLevelStarts, + @NonNull final ThrottleUsageSnapshots throttleUsageSnapshots, + @NonNull final DumpCheckpoint checkpoint) { + int reportSize; + try (@NonNull final var writer = new Writer(path)) { + reportOnCongestion(writer, Congestion.fromMod(congestionLevelStarts, throttleUsageSnapshots)); + reportSize = writer.getSize(); + } + + System.out.printf("=== staking rewards report is %d bytes %n", reportSize); + } + + static void reportOnCongestion(@NonNull Writer writer, @NonNull Congestion congestion) { + writer.writeln(formatHeader()); + formatCongestion(writer, congestion); + writer.writeln(""); + } + + static void formatCongestion(@NonNull final Writer writer, @NonNull final Congestion congestion) { + final var fb = new FieldBuilder(FIELD_SEPARATOR); + fieldFormatters.stream().map(Pair::right).forEach(ff -> ff.accept(fb, congestion)); + writer.writeln(fb); + } + + @NonNull + static String formatHeader() { + return fieldFormatters.stream().map(Pair::left).collect(Collectors.joining(FIELD_SEPARATOR)); + } + + private static BiConsumer getFieldFormatter( + @NonNull final Function fun, @NonNull final Function formatter) { + return (fb, t) -> formatField(fb, t, fun, formatter); + } + + static void formatField( + @NonNull final FieldBuilder fb, + @NonNull final Congestion stakingRewards, + @NonNull final Function fun, + @NonNull final Function formatter) { + fb.append(formatter.apply(fun.apply(stakingRewards))); + } + + static Function getNullableFormatter(@NonNull final Function formatter) { + return t -> null != t ? formatter.apply(t) : ""; + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/PayerRecord.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/PayerRecord.java new file mode 100644 index 000000000000..bdb01fb45d43 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/PayerRecord.java @@ -0,0 +1,52 @@ +/* + * 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.bbm.singleton; + +import com.hedera.hapi.node.state.recordcache.TransactionRecordEntry; +import com.hedera.node.app.service.mono.state.submerkle.EntityId; +import com.hedera.node.app.service.mono.state.submerkle.ExpirableTxnRecord; +import com.hedera.node.app.service.mono.state.submerkle.RichInstant; +import com.hedera.node.app.service.mono.state.submerkle.TxnId; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Objects; + +public record PayerRecord(TxnId transactionId, RichInstant consensusTime, EntityId payer) { + + public static PayerRecord fromMod(@NonNull TransactionRecordEntry recordEntry) { + Objects.requireNonNull(recordEntry.transactionRecord(), "Record is null"); + + var modTransactionId = recordEntry.transactionRecord().transactionID(); + var accountId = EntityId.fromPbjAccountId(modTransactionId.accountID()); + var validStartTimestamp = modTransactionId.transactionValidStart(); + var txnId = new TxnId( + accountId, + new RichInstant(validStartTimestamp.seconds(), validStartTimestamp.nanos()), + modTransactionId.scheduled(), + modTransactionId.nonce()); + var consensusTimestamp = recordEntry.transactionRecord().consensusTimestamp(); + + return new PayerRecord( + txnId, + new RichInstant(consensusTimestamp.seconds(), consensusTimestamp.nanos()), + EntityId.fromPbjAccountId(recordEntry.payerAccountId())); + } + + public static PayerRecord fromMono(@NonNull ExpirableTxnRecord record) { + return new PayerRecord( + record.getTxnId(), record.getConsensusTime(), record.getTxnId().getPayerAccount()); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/PayerRecordsDumpUtils.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/PayerRecordsDumpUtils.java new file mode 100644 index 000000000000..6d2ea3e23ccc --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/PayerRecordsDumpUtils.java @@ -0,0 +1,126 @@ +/* + * 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.bbm.singleton; + +import com.hedera.hapi.node.state.recordcache.TransactionRecordEntry; +import com.hedera.node.app.bbm.DumpCheckpoint; +import com.hedera.node.app.bbm.utils.FieldBuilder; +import com.hedera.node.app.bbm.utils.ThingsToStrings; +import com.hedera.node.app.bbm.utils.Writer; +import com.hedera.node.app.service.mono.state.submerkle.ExpirableTxnRecord; +import com.hedera.node.app.state.merkle.queue.QueueNode; +import com.swirlds.base.utility.Pair; +import com.swirlds.fcqueue.FCQueue; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class PayerRecordsDumpUtils { + + static final String FIELD_SEPARATOR = ";"; + + @NonNull + static List>> fieldFormatters = List.of( + Pair.of("txnId", getFieldFormatter(PayerRecord::transactionId, Object::toString)), + Pair.of( + "consensusTime", + getFieldFormatter(PayerRecord::consensusTime, ThingsToStrings::toStringOfRichInstant)), + Pair.of("payer", getFieldFormatter(PayerRecord::payer, ThingsToStrings::toStringOfEntityId))); + + public static void dumpMonoPayerRecords( + @NonNull final Path path, + @NonNull final FCQueue records, + @NonNull final DumpCheckpoint checkpoint) { + var transactionRecords = gatherTxnRecordsFromMono(records); + int reportSize; + try (@NonNull final var writer = new Writer(path)) { + reportOnTxnRecords(writer, transactionRecords); + reportSize = writer.getSize(); + } + System.out.printf("=== payer records report is %d bytes %n", reportSize); + } + + public static void dumpModTxnRecordQueue( + @NonNull final Path path, + @NonNull final QueueNode queue, + @NonNull final DumpCheckpoint checkpoint) { + var transactionRecords = gatherTxnRecordsFromMod(queue); + int reportSize; + try (@NonNull final var writer = new Writer(path)) { + reportOnTxnRecords(writer, transactionRecords); + reportSize = writer.getSize(); + } + System.out.printf("=== payer records report is %d bytes %n", reportSize); + } + + private static List gatherTxnRecordsFromMod(QueueNode queue) { + var iterator = queue.iterator(); + var records = new ArrayList(); + while (iterator.hasNext()) { + records.add(PayerRecord.fromMod(iterator.next())); + } + + return records; + } + + private static List gatherTxnRecordsFromMono(FCQueue records) { + var listTxnRecords = new ArrayList(); + records.stream().forEach(p -> listTxnRecords.add(PayerRecord.fromMono(p))); + return listTxnRecords; + } + + static void reportOnTxnRecords(@NonNull Writer writer, @NonNull List records) { + writer.writeln(formatHeader()); + records.stream() + .sorted(Comparator.comparing(PayerRecord::consensusTime)) + .forEach(e -> formatRecords(writer, e)); + writer.writeln(""); + } + + static void formatRecords(@NonNull final Writer writer, @NonNull final PayerRecord record) { + final var fb = new FieldBuilder(FIELD_SEPARATOR); + fieldFormatters.stream().map(Pair::right).forEach(ff -> ff.accept(fb, record)); + writer.writeln(fb); + } + + @NonNull + static String formatHeader() { + return fieldFormatters.stream().map(Pair::left).collect(Collectors.joining(FIELD_SEPARATOR)); + } + + private static BiConsumer getFieldFormatter( + @NonNull final Function fun, @NonNull final Function formatter) { + return (fb, t) -> formatField(fb, t, fun, formatter); + } + + static Function getNullableFormatter(@NonNull final Function formatter) { + return t -> null != t ? formatter.apply(t) : ""; + } + + static void formatField( + @NonNull final FieldBuilder fb, + @NonNull final PayerRecord transaction, + @NonNull final Function fun, + @NonNull final Function formatter) { + fb.append(formatter.apply(fun.apply(transaction))); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/StakingInfo.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/StakingInfo.java new file mode 100644 index 000000000000..9f1f589629d3 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/StakingInfo.java @@ -0,0 +1,68 @@ +/* + * 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.bbm.singleton; + +import com.hedera.hapi.node.state.token.StakingNodeInfo; +import com.hedera.node.app.service.mono.state.merkle.MerkleStakingInfo; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Objects; + +public record StakingInfo( + int number, + long minStake, + long maxStake, + long stakeToReward, + long stakeToNotReward, + long stakeRewardStart, + long unclaimedStakeRewardStart, + long stake, + @NonNull long[] rewardSumHistory, + int weight) { + public static StakingInfo fromMono(@NonNull final MerkleStakingInfo stakingInfo) { + Objects.requireNonNull(stakingInfo.getRewardSumHistory(), "rewardSumHistory"); + return new StakingInfo( + stakingInfo.getKey().intValue(), + stakingInfo.getMinStake(), + stakingInfo.getMaxStake(), + stakingInfo.getStakeToReward(), + stakingInfo.getStakeToNotReward(), + stakingInfo.getStakeRewardStart(), + stakingInfo.getUnclaimedStakeRewardStart(), + stakingInfo.getStake(), + stakingInfo.getRewardSumHistory(), + stakingInfo.getWeight()); + } + + public static StakingInfo fromMod(@NonNull final StakingNodeInfo stakingInfo) { + Objects.requireNonNull(stakingInfo.rewardSumHistory(), "rewardSumHistory"); + return new StakingInfo( + Long.valueOf(stakingInfo.nodeNumber()).intValue(), + stakingInfo.minStake(), + stakingInfo.maxStake(), + stakingInfo.stakeToReward(), + stakingInfo.stakeToNotReward(), + stakingInfo.stakeRewardStart(), + stakingInfo.unclaimedStakeRewardStart(), + stakingInfo.stake(), + stakingInfo.rewardSumHistory().stream() + .mapToLong(Long::longValue) + .toArray(), + stakingInfo.weight()); + } + + static final byte[] EMPTY_BYTES = new byte[0]; +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/StakingInfoDumpUtils.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/StakingInfoDumpUtils.java new file mode 100644 index 000000000000..42aa8b6332df --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/StakingInfoDumpUtils.java @@ -0,0 +1,175 @@ +/* + * 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.bbm.singleton; + +import com.hedera.hapi.node.state.common.EntityNumber; +import com.hedera.hapi.node.state.token.StakingNodeInfo; +import com.hedera.node.app.bbm.DumpCheckpoint; +import com.hedera.node.app.bbm.utils.FieldBuilder; +import com.hedera.node.app.bbm.utils.Writer; +import com.hedera.node.app.service.mono.state.adapters.MerkleMapLike; +import com.hedera.node.app.service.mono.state.merkle.MerkleStakingInfo; +import com.hedera.node.app.service.mono.utils.EntityNum; +import com.hedera.node.app.state.merkle.memory.InMemoryKey; +import com.hedera.node.app.state.merkle.memory.InMemoryValue; +import com.swirlds.base.utility.Pair; +import com.swirlds.merkle.map.MerkleMap; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class StakingInfoDumpUtils { + + static final String FIELD_SEPARATOR = ";"; + + @NonNull + static List>> fieldFormatters = List.of( + Pair.of("number", getFieldFormatter(StakingInfo::number, Object::toString)), + Pair.of("minStake", getFieldFormatter(StakingInfo::minStake, Object::toString)), + Pair.of("maxStake", getFieldFormatter(StakingInfo::maxStake, Object::toString)), + Pair.of("stakeToReward", getFieldFormatter(StakingInfo::stakeToReward, Object::toString)), + Pair.of("stakeToNotReward", getFieldFormatter(StakingInfo::stakeToNotReward, Object::toString)), + Pair.of("stakeRewardStart", getFieldFormatter(StakingInfo::stakeRewardStart, Object::toString)), + Pair.of( + "unclaimedStakeRewardStart", + getFieldFormatter(StakingInfo::unclaimedStakeRewardStart, Object::toString)), + Pair.of("stake", getFieldFormatter(StakingInfo::stake, Object::toString)), + Pair.of("rewardSumHistory", getFieldFormatter(StakingInfo::rewardSumHistory, Arrays::toString)), + Pair.of("weight", getFieldFormatter(StakingInfo::weight, Object::toString))); + + public static void dumpMonoStakingInfo( + @NonNull final Path path, + @NonNull final MerkleMap stakingInfoMerkleMap, + @NonNull final DumpCheckpoint checkpoint) { + System.out.printf("=== %d staking info ===%n", stakingInfoMerkleMap.size()); + + final var allStakingInfo = gatherStakingInfoFromMono(MerkleMapLike.from(stakingInfoMerkleMap)); + + int reportSize; + try (@NonNull final var writer = new Writer(path)) { + reportSummary(writer, allStakingInfo); + reportOnStakingInfo(writer, allStakingInfo); + reportSize = writer.getSize(); + } + + System.out.printf("=== staking info report is %d bytes %n", reportSize); + } + + public static void dumpModStakingInfo( + @NonNull final Path path, + @NonNull + final MerkleMap, InMemoryValue> + stakingInfoVirtualMap, + @NonNull final DumpCheckpoint checkpoint) { + System.out.printf("=== %d staking info ===%n", stakingInfoVirtualMap.size()); + + final var allStakingInfo = gatherStakingInfoFromMod(stakingInfoVirtualMap); + + int reportSize; + try (@NonNull final var writer = new Writer(path)) { + reportSummary(writer, allStakingInfo); + reportOnStakingInfo(writer, allStakingInfo); + reportSize = writer.getSize(); + } + + System.out.printf("=== staking info report is %d bytes %n", reportSize); + } + + @NonNull + static Map gatherStakingInfoFromMono( + @NonNull final MerkleMapLike stakingInfoStore) { + final var allStakingInfo = new TreeMap(); + stakingInfoStore.forEachNode((en, mt) -> allStakingInfo.put(en.longValue(), StakingInfo.fromMono(mt))); + return allStakingInfo; + } + + @NonNull + static Map gatherStakingInfoFromMod( + @NonNull + final MerkleMap, InMemoryValue> + stakingInfoMap) { + final var r = new HashMap(); + MerkleMapLike.from(stakingInfoMap) + .forEach((k, v) -> r.put(k.key().number(), StakingInfo.fromMod(v.getValue()))); + return r; + } + + static void reportSummary(@NonNull Writer writer, @NonNull Map stakingInfo) { + writer.writeln("=== %7d: staking info".formatted(stakingInfo.size())); + writer.writeln(""); + } + + static void reportOnStakingInfo(@NonNull Writer writer, @NonNull Map stakingInfo) { + writer.writeln(formatHeader()); + stakingInfo.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .forEach(e -> formatStakingInfo(writer, e.getValue())); + writer.writeln(""); + } + + static void formatStakingInfo(@NonNull final Writer writer, @NonNull final StakingInfo stakingInfo) { + final var fb = new FieldBuilder(FIELD_SEPARATOR); + fieldFormatters.stream().map(Pair::right).forEach(ff -> ff.accept(fb, stakingInfo)); + writer.writeln(fb); + } + + @NonNull + static String formatHeader() { + return fieldFormatters.stream().map(Pair::left).collect(Collectors.joining(FIELD_SEPARATOR)); + } + + static BiConsumer getFieldFormatter( + @NonNull final Function fun, @NonNull final Function formatter) { + return (fb, t) -> formatField(fb, t, fun, formatter); + } + + static Function getNullableFormatter(@NonNull final Function formatter) { + return t -> null != t ? formatter.apply(t) : ""; + } + + static void formatField( + @NonNull final FieldBuilder fb, + @NonNull final StakingInfo stakingInfo, + @NonNull final Function fun, + @NonNull final Function formatter) { + fb.append(formatter.apply(fun.apply(stakingInfo))); + } + + static Function, String> getListFormatter( + @NonNull final Function formatter, @NonNull final String subfieldSeparator) { + return lt -> { + if (!lt.isEmpty()) { + final var sb = new StringBuilder(); + for (@NonNull final var e : lt) { + final var v = formatter.apply(e); + sb.append(v); + sb.append(subfieldSeparator); + } + // Remove last subfield separator + if (sb.length() >= subfieldSeparator.length()) sb.setLength(sb.length() - subfieldSeparator.length()); + return sb.toString(); + } else return ""; + }; + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/StakingRewards.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/StakingRewards.java new file mode 100644 index 000000000000..ab953ea112c4 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/StakingRewards.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.bbm.singleton; + +import com.hedera.hapi.node.state.token.NetworkStakingRewards; +import com.hedera.node.app.service.mono.state.merkle.MerkleNetworkContext; +import edu.umd.cs.findbugs.annotations.NonNull; + +public record StakingRewards( + boolean stakingRewardsActivated, long totalStakedRewardStart, long totalStakedStart, long pendingRewards) { + + public static StakingRewards fromMono(@NonNull final MerkleNetworkContext merkleNetworkContext) { + + return new StakingRewards( + merkleNetworkContext.areRewardsActivated(), + merkleNetworkContext.getTotalStakedRewardStart(), + merkleNetworkContext.getTotalStakedStart(), + merkleNetworkContext.pendingRewards()); + } + + public static StakingRewards fromMod(@NonNull final NetworkStakingRewards networkStakingRewards) { + return new StakingRewards( + networkStakingRewards.stakingRewardsActivated(), + networkStakingRewards.totalStakedRewardStart(), + networkStakingRewards.totalStakedStart(), + networkStakingRewards.pendingRewards()); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/StakingRewardsDumpUtils.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/StakingRewardsDumpUtils.java new file mode 100644 index 000000000000..d034585978f0 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/singleton/StakingRewardsDumpUtils.java @@ -0,0 +1,104 @@ +/* + * 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.bbm.singleton; + +import com.hedera.hapi.node.state.token.NetworkStakingRewards; +import com.hedera.node.app.bbm.DumpCheckpoint; +import com.hedera.node.app.bbm.utils.FieldBuilder; +import com.hedera.node.app.bbm.utils.Writer; +import com.hedera.node.app.service.mono.state.merkle.MerkleNetworkContext; +import com.swirlds.base.utility.Pair; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.file.Path; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class StakingRewardsDumpUtils { + + static final String FIELD_SEPARATOR = ";"; + static Function booleanFormatter = b -> b ? "T" : ""; + + @NonNull + static List>> fieldFormatters = List.of( + Pair.of( + "stakingRewardsActivated", + getFieldFormatter(StakingRewards::stakingRewardsActivated, booleanFormatter)), + Pair.of( + "totalStakedRewardStart", + getFieldFormatter(StakingRewards::totalStakedRewardStart, Object::toString)), + Pair.of("totalStakedStart", getFieldFormatter(StakingRewards::totalStakedStart, Object::toString)), + Pair.of("pendingRewards", getFieldFormatter(StakingRewards::pendingRewards, Object::toString))); + + public static void dumpMonoStakingRewards( + @NonNull final Path path, + @NonNull final MerkleNetworkContext merkleNetworkContext, + @NonNull final DumpCheckpoint checkpoint) { + + int reportSize; + try (@NonNull final var writer = new Writer(path)) { + reportOnStakingRewards(writer, StakingRewards.fromMono(merkleNetworkContext)); + reportSize = writer.getSize(); + } + + System.out.printf("=== staking rewards report is %d bytes %n", reportSize); + } + + public static void dumpModStakingRewards( + @NonNull final Path path, + @NonNull final NetworkStakingRewards stakingRewards, + @NonNull final DumpCheckpoint checkpoint) { + int reportSize; + try (@NonNull final var writer = new Writer(path)) { + reportOnStakingRewards(writer, StakingRewards.fromMod(stakingRewards)); + reportSize = writer.getSize(); + } + + System.out.printf("=== staking rewards report is %d bytes %n", reportSize); + } + + static void reportOnStakingRewards(@NonNull Writer writer, @NonNull StakingRewards stakingRewards) { + writer.writeln(formatHeader()); + formatStakingRewards(writer, stakingRewards); + writer.writeln(""); + } + + static void formatStakingRewards(@NonNull final Writer writer, @NonNull final StakingRewards stakingRewards) { + final var fb = new FieldBuilder(FIELD_SEPARATOR); + fieldFormatters.stream().map(Pair::right).forEach(ff -> ff.accept(fb, stakingRewards)); + writer.writeln(fb); + } + + @NonNull + static String formatHeader() { + return fieldFormatters.stream().map(Pair::left).collect(Collectors.joining(FIELD_SEPARATOR)); + } + + static BiConsumer getFieldFormatter( + @NonNull final Function fun, @NonNull final Function formatter) { + return (fb, t) -> formatField(fb, t, fun, formatter); + } + + static void formatField( + @NonNull final FieldBuilder fb, + @NonNull final StakingRewards stakingRewards, + @NonNull final Function fun, + @NonNull final Function formatter) { + fb.append(formatter.apply(fun.apply(stakingRewards))); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/utils/ThingsToStrings.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/utils/ThingsToStrings.java index a8dfb55a9089..3109b2744398 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/utils/ThingsToStrings.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/utils/ThingsToStrings.java @@ -18,6 +18,7 @@ import com.google.protobuf.ByteString; import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.Timestamp; import com.hedera.hapi.node.base.TokenID; import com.hedera.hapi.node.state.token.AccountApprovalForAllAllowance; import com.hedera.hapi.node.state.token.AccountCryptoAllowance; @@ -525,6 +526,10 @@ public static String toStringOfRichInstant(@NonNull final RichInstant instant) { return "%d.%d".formatted(instant.getSeconds(), instant.getNanos()); } + public static String toStringOfTimestamp(@NonNull final Timestamp timestamp) { + return "%d.%d".formatted(timestamp.seconds(), timestamp.nanos()); + } + public static boolean is7BitAscii(@NonNull final byte[] bs) { for (byte b : bs) if (b < 0) return false; return true; diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/recordcache/RecordCacheService.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/recordcache/RecordCacheService.java index 5b027678759b..5e6fa6779bdb 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/recordcache/RecordCacheService.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/recordcache/RecordCacheService.java @@ -47,7 +47,7 @@ public class RecordCacheService implements Service { /** The record cache service name */ public static final String NAME = "RecordCache"; /** The name of the queue that stores the transaction records */ - static final String TXN_RECORD_QUEUE = "TransactionRecordQueue"; + public static final String TXN_RECORD_QUEUE = "TransactionRecordQueue"; private List fromRecs; diff --git a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/state/merkle/MerkleNetworkContext.java b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/state/merkle/MerkleNetworkContext.java index 9751035378cd..2f54d47635dd 100644 --- a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/state/merkle/MerkleNetworkContext.java +++ b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/state/merkle/MerkleNetworkContext.java @@ -819,7 +819,8 @@ private String reprOf(final Instant consensusTime) { return consensusTime == null ? NOT_AVAILABLE : consensusTime.toString(); } - private String stringifiedBlockHashes() { + @VisibleForTesting + public String stringifiedBlockHashes() { final var jsonSb = new StringBuilder("["); final var firstAvailable = blockNo - blockHashes.size(); final var hashIter = blockHashes.iterator(); @@ -991,7 +992,8 @@ Instant getConsensusTimeOfLastHandledTxn() { return consensusTimeOfLastHandledTxn; } - DeterministicThrottle.UsageSnapshot[] usageSnapshots() { + @VisibleForTesting + public DeterministicThrottle.UsageSnapshot[] usageSnapshots() { return usageSnapshots; } @@ -1008,7 +1010,8 @@ public void setSeqNo(final SequenceNumber seqNo) { } @Nullable - MultiplierSources getMultiplierSources() { + @VisibleForTesting + public MultiplierSources getMultiplierSources() { return multiplierSources; } @@ -1016,7 +1019,8 @@ FunctionalityThrottling getThrottling() { return throttling; } - DeterministicThrottle.UsageSnapshot getGasThrottleUsageSnapshot() { + @VisibleForTesting + public DeterministicThrottle.UsageSnapshot getGasThrottleUsageSnapshot() { return gasThrottleUsageSnapshot; } diff --git a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/state/merkle/MerkleStakingInfo.java b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/state/merkle/MerkleStakingInfo.java index 0ce7dfa8a0d1..661c0e63817c 100644 --- a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/state/merkle/MerkleStakingInfo.java +++ b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/state/merkle/MerkleStakingInfo.java @@ -525,7 +525,7 @@ public MerkleStakingInfo( @Nullable @VisibleForTesting - byte[] getHistoryHash() { + public byte[] getHistoryHash() { return historyHash; } From 0b5a10a390ec9c1b5b34c82f39cbf8a68314e592 Mon Sep 17 00:00:00 2001 From: Lazar Petrovic Date: Wed, 13 Mar 2024 11:00:19 +0100 Subject: [PATCH 081/115] chore: add mainnet event migration test (#12048) Signed-off-by: Lazar Petrovic --- .../platform/event/EventMigrationTest.java | 94 ++++++++++++++++++ .../2024-03-05T00_10_55.002129867Z.events | Bin 0 -> 3595541 bytes 2 files changed, 94 insertions(+) create mode 100644 hedera-node/hedera-app/src/test/java/com/hedera/node/app/platform/event/EventMigrationTest.java create mode 100644 hedera-node/hedera-app/src/test/resources/eventFiles/sdk0.46.3/2024-03-05T00_10_55.002129867Z.events diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/platform/event/EventMigrationTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/platform/event/EventMigrationTest.java new file mode 100644 index 000000000000..0e9fdf2d5691 --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/platform/event/EventMigrationTest.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.platform.event; + +import com.hedera.node.app.service.mono.context.properties.SerializableSemVers; +import com.swirlds.common.constructable.ConstructableRegistry; +import com.swirlds.common.constructable.ConstructableRegistryException; +import com.swirlds.common.crypto.CryptographyHolder; +import com.swirlds.common.crypto.Hash; +import com.swirlds.platform.recovery.internal.EventStreamSingleFileIterator; +import com.swirlds.platform.system.StaticSoftwareVersion; +import com.swirlds.platform.system.events.BaseEventHashedData; +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Stream; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class EventMigrationTest { + + @BeforeAll + public static void setUp() throws ConstructableRegistryException { + ConstructableRegistry.getInstance().registerConstructables(""); + StaticSoftwareVersion.setSoftwareVersion(Set.of(SerializableSemVers.CLASS_ID)); + } + + /** + * Tests the migration of events as we are switching events to protobuf. The main thing we are testing is that the + * hashes of old events can still be calculated when the code changes. This is done by calculating the hashes of the + * events that are read and matching them to the parent descriptors inside the events. The parents of most events + * will be present in the file, except for a few events at the beginning of the file. + *

    + * The file being read is from mainnet written by the SDK 0.46.3. + *

    + * Even though this could be considered a platform test, it needs to be in the services module because the event + * contains a {@link com.hedera.node.app.service.mono.context.properties.SerializableSemVers} which is a services + * class + */ + @Test + public void migration() throws URISyntaxException, IOException { + final Set eventHashes = new HashSet<>(); + final Set parentHashes = new HashSet<>(); + int numEvents = 0; + + try (final EventStreamSingleFileIterator iterator = new EventStreamSingleFileIterator( + new File(this.getClass() + .getClassLoader() + .getResource("eventFiles/sdk0.46.3/2024-03-05T00_10_55.002129867Z.events") + .toURI()) + .toPath(), + false)) { + while (iterator.hasNext()) { + final BaseEventHashedData hashedData = iterator.next().getBaseEventHashedData(); + numEvents++; + CryptographyHolder.get().digestSync(hashedData); + eventHashes.add(hashedData.getHash()); + Stream.of(hashedData.getSelfParentHash(), hashedData.getOtherParentHash()) + .filter(Objects::nonNull) + .forEach(parentHashes::add); + } + } + + Assertions.assertEquals(2417, numEvents, "this file is expected to have 2417 events but has " + numEvents); + Assertions.assertEquals( + 2417, + eventHashes.size(), + "we expected to have 2417 hashes (one for each event) but have " + eventHashes.size()); + eventHashes.removeAll(parentHashes); + Assertions.assertEquals( + 9, + eventHashes.size(), + "the hashes of most parents are expected to match the hashes of events." + + " Number of unmatched hashes: " + eventHashes.size()); + } +} diff --git a/hedera-node/hedera-app/src/test/resources/eventFiles/sdk0.46.3/2024-03-05T00_10_55.002129867Z.events b/hedera-node/hedera-app/src/test/resources/eventFiles/sdk0.46.3/2024-03-05T00_10_55.002129867Z.events new file mode 100644 index 0000000000000000000000000000000000000000..2bdb5f7ba1346414ad47c13179ee6b36c689cd2e GIT binary patch literal 3595541 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 z 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ΝCF*S5z^@z!I$U+Xh)8%M7AMhrU0u2peb4ca#u-7P*VVf zQjnqpicIlw9TT3lnXH+i^2d))Rd?J z0b?FhDHMD;1R(h4Hsp8U^LxWr@A+>R z$hNjVxqZ>*NJGC@u>PB(-0Q2tWQ98G??@vmcLp#%zuc)tp7(+Sb@9AHI7Q#{`#-{% zE%hw7ZAs-lpZSF_y3L~Ca{^fs1LR66@b3Tu6YR96u!llZ175;nAdG8 z;{&~^reb%fFO4QEblhgNDYeuX6cUwOD{mU#Rt96WsyOnu&lC(3;gleV_G>_##A7fp z6$W1wP&AYP4+NYfS^^kc#O@7N5(|z10K12?2A3$v&5Hrdd#l3e>hQd&?9`+2;-bKk zIf6S{+tNq#u%+%fdaD+;DdyW|Du=ReR6e;Gnp?QEE=(18bYmJlc>7s5H=(J0Lylea z4{qh7mhWGpQ@oxJ%y9dguq4iD)7+k7wdjdHe4L!;CKcrlq&)4bEQP0YuK<0kRy4!&Z;I$x6?J$-UXU8DQe zPPa7MYr+d&g=vBcX$o1=ZZ0gLH7TFPRM}d*mrZh&0*tPTicz$p-qA=WNYR{oRi!Mm z7%vN#sbUX1IxZSD!7-y#=91JSAycb zSYBr&Tok9p4fyYloRU=_o0!l)r#tXNF!8P5*qx-8UYcJIf2#B2dOwir|9#NLRjb!R z+rGh0BmSFKWBn~F@|SYE5S{6J1pD@OKg}IobWe8j)46s5`GS1jk^U~fSv(`lyK!sU zRP5B(!xfgF6SdD~+tjxgA85PBcx^}E>%tJa9Gs|;y7<*$_wxZuEfN>^*+=SQ^jkVj`YsQo7Tjg!sk*u$r)9~@I*r1=Dv(W%y?%Wwy2^6G!j$2lTqqIT zxw)NhiV#Y*l$@X0PnPU1fanD5itIvy3%0*jcoWmVy6clr0!%Cc6RO{iKJM|PInNQ z?OhqB{^o8*%*yqadt6Dg{c$;~6%_WGv5zZ`4ev`X4mzAJ-09nb!LsSv1eaesognhl zx1uL3S;@s$uRQ5y-E!dRk2{0i&$^KtE7jbft;-vH!!`4$?!DtplCre8X%6Ft!!8HU z$*3PX;p5mX_BfI~aMrCQG-Zdl$r3dePB3c4UeW&UQ?XBtSB+3uPxQw-U+ekZL(V_n zk(%x`t#_#ZQu&w?k}&)7`XTO{g^78c_eNeke}5x6+_&eIt?F@o-ob2sh3uEyF=ue& zV;e5JQ;smpcKMCn(!7A<5i4mH8nmEmH=Bwky)YBy>c_1+-7Zc-u{P#!HZ!SLLelUs ziFRdv(8+dkc(1{AgI5AdtVb~P?oy;i$G_~Hq{y|Lak2N|Ag>;O-OYV(oM~QkHzz%J z*t=s~C2^N2EfT3Rf5zp>-7HSN{l)rjW7$bjj59u)j6E?gN?OPhU;j}h5kD6^{&*iqRgSy=@#7y~ z{>`<$TjT-uyr(hYXUtolVCN^cR|`|u`Z6TsUxc16C9g}l`;do2OU2#N_SG7$i^Ul= z2=B9bZErG@w~p}3k+zZ{#Kc;~Zzn^+Q>DM~hIjqvnhXqgAm-qpIReQJUIK@efS#8m z0XQmXaC$|XBEdNnoHdaMBv8MgiG(L6MZl&a1}9S~$*uCmwR`-4{%z|@|AH6p1Zo|^ zinY!s$(Zl^L{+AQE1x}D>CgFOKl-(@rOEGvswfAaB|nFytFM&qzaZ@y*|s%Q z)D`pW&k(!KqW%YXfS?L^9>@xz z$b{aP=~jqUq}QfJ-=&Ijb3im4tl9sNY;IIoL8X@ADP^3`nl zg$vZKlNQR&YCZ_>2eCH<`?XeFx21tSdh((4w-<$!Aj(VNF)+pg!F)+H4)8g^TO$B{ z76)!Zq9_Rrj2y+Fc19EXebBfBpE_WG!-$dqdDNB_YQoXu;k)eW@5vr~NheUGXRm+k zou2#`#dC#M#5g!YqlEH`I#<<(#3Sw#I5nnFuTK#+G9BuS?HOVZy(eUi4Q@*Vt1nEi z{Ov^%HMyaAM*wIXIJN!3GlT`AQ5euXgT}E0G3%3(0>&^9prPF?1_>Y}<5C1X3Il&& zx&@7sq^bFD(OJ4hk*Q)s zD?6Q1_^rCFC+y_jVT_1V?Z`lU?*)O)&9zCmW9#b@OCCFnyY`li5vs>G|6{5yDO;| z`+BE%p-PfV!y}uDMfDqYDn$Wjo)jp0~Dh;%k5G^?wOTlpgfA8kSB;AU+cgMf6W0QyZlP%d1U>;;stH zrSi*}OYQ1ZbGqnI(`4VD^+fMIG~~7d22{m=Btu*ExjQ_u`Pi(UQ8(v+>++k`bM5oN zPRy9pJ zS!PgsGQB!hguKO^ug;nG-Swo`+Xf8Zh@8ydUKFeez>i`C1PW>|QBci*ejR8@0hibm zDt<|EEEt=CFf|&u(4e7#Mqq)g2@Z9z2csl#l7y|!3A4hOVgsFfwa|A1)e4oFw%u1X^DIAZ4&WHZ1e0GvI=2skXD zKY-@FSv~jI)_r$5cHV2|yad_OrP%e*L= zt$4nE>{xTTKwRx)-nBQqaWs_!q?m#de>63dg?OX!EtQ*OiY4s@3zfrAWstfHd^q)y z$b=}f13tM^0o9ASCsiaJ%h>702 zFN4OAt@AcPLvaXc|KRoZw$Z8|w{CUE4%a~Nk@U+?0n%etXBztmC;9t5l%+!Xf&e5{~{rf^@*cLCBB%R2_xqtScAI-&5aWi!K=bd-=XEW}vb2hPr{h&)1E&2Acab3n-i{<&O(WXYvxs!JGL%@x6%6 z(5>2*`18L844tSOpIDMdb?*%xveA!l`>9z(H^Q)iVpT|PQ9QOL4bb|Vr45B$3zd_U zMJ%f!OTQd}`5#C4k5eDMUUNOrm>>qJUE5B5Y9oq5|GZ=O{O1Y-1?&J&uEF4O;M+;m zUVxS)vB*G(5+tSYXpn<5gOG=K z$L_{R9X`vNKElgfgw)}eTI0wsRc7ry<8^k>*ko6^axl+Y=F7;Y8E zG;`N&C+a`e`on&EQR0ANK%k(YfVF`ICLT@HTYxql9;AE#y+Q0oO2F6$#Cjomh>L-$ z1`s@?@S-581|~D8t$=}if4EwspdkfkJ~53(Dc-w#A%Zz1Q8}|q`Pmygt&NMpUV(w0 zu^y^^HYL>hlB&zSWnY6@?<~bS$5roJ^;+F_QE`ny(Z9VYQ50xm5Gy6(VCfcI(vGX02mfb{H9LdiIhGCZ^W(DQxZ62daD5(qjwiO*L znk#+y+lvATDexT=1@R;#v3!7TDwxawbPgb!fSQ96LQ)hj0Z2fsC^Y#&ZV6t(6Yv;3 z==O@^VM@8VD8}Q5WBXiEnbT_L;=2@N9cY7=M7tEM+FqC_uBX+oTlblbHZ-wfn3=;4 zd=q)mlI7s%UO9&9HxenosNQqw>9#}1qWI~g-(D1iIFRlF2}qbpfRGZ3s4@Y84~cX2kp*=8OeG*ug9HZbfuux1dvJ46^)kFyHGKlonFVB1>s}lPVY%c| zRP^=cc$G>{LZhi!bD7OE(s-t#`>OgPq;%>-1s)Yw`=&5Qj2)M3*%%!O+a_VzcRuC+ z_M*Ua2?YaGBo2u|L-i+ykOXp(1aupqzYor8rg)Sn4h>RRXgF~=kdB1rI1H5W5cpx3 zA%@?&ryS{L_lY`bFRJ@r_CyDpR|YUw#!#TiYCh^0ZS1_s^H@J-m&qAU>&!ROE^;Di zJH7S`aa`~D+D{^@kW9rQ5=GoC-6d5kEstnWU$~vSKV!`}u~IR_El`NYMI}?^k~(MU z=jU^H68o~qQgJ!*{)gmQTOqVf1)OGR0WZeDCZV>ZVf) zW$r%)qVABenrn*HXkCvTzri)1d)EEpZSzpWfzB#+D(k>#Zw#PnWd}q0y-1UUj zWPGjf=bu9P;=FqzS&r;pUfO{ue-@%vP~&#_yYO>Y6N&p7f}TPf6zcR&u?|0Ze9x7A zEKf3Z#k4!fAd-F_D6~G8c<9^>tc~vwUEtFVj!R++cM|yD&Ia9^iH`Ly8!a&nVkujl zdE2M*zS~$eo~xMwmHxVVv0r0i%9AEGFz>r*RMo?#+h!JQyLcH};#D52Q&F|J?0t3wjEyIT z(`!%-4eLjAo1AM3(z+t1%^8o#S2oxjcHvjHuit^iU($SV|3PSi31^UQnun+(2f0v9 zc19U~V(5A0Mez?90ZZ-WmXMGhLID+w3gDwa`!H9(>)J;Cbtn(vu;7^QSIbM(&73+ z`mPWS<9E%^6<)jY2iG03A1RMk361ZW%Nx3{3n%4owH;W>%q{v8pu_r~Q#J5bgJcEd zc`@REp96pj5vl{^Ml?wE13VteMpNkfLKhe`3&D*|91mkx3?Kx>vG6nC<~PF%wFzq= zL;IzUUO&#}}}cqVo^9SP5_}ZJn%Wi z@nE9>C}ChP-~reGOk*(sCBa-Aa8oGg)Wbr-_Yg0R#1gFy0qXguSKg=h4AC#wPgw-{ z@8^w(usuUCzV&AG@{%bp?Y_@>F>8Bg5RZSJC9CItqIXg*rL@5_-G#JZoOLmix>^NI z`Y>kO4tGj@;rriS6u`kGr2v!xq+4-uQ$XMnpsvS3UIU?D0$NA}6u7L4B0x0^4VDTL z&=ZDC0O+|yLMuptZZ#}4I4tdNIz6aAA{CFTuR7GV9vOJ-oJgt?g_E?qiQ3IE!?vKt zhQwRcZGAjO^qP`L<;zTowC9JK88yk?JM0$w=cGF2Xd)>Unu?K%X7~NN=-gdOMykD3yQtu)J$tAqsEDucp=Tga;i>lXD}HcPm6}p` zaK?w{LN!Uz5@k!j{Bu;Hbg@vd*h!AF5C;dh~SOK9Ufsa-a#QBOn0z`shn=U zhgn$m$8jDKI>EdPGaYi!r3!hq(bXiTwJ~w*tLx!b$It@@2u1Y7q=nNbz>rhvZE2*i z5P<_-ME;~{6y-=y>US}-c<$X#KbgsvKGCwk^SgI4w<*fbuqt3Op81~TQ5QXY_6MI` zix$spthDD4S~0X|y8m=Utk~N|v-kXiQUTJJEefm2R~IzGP3H7XV+LNLYk2eGti-&i zTY73U9w+VG!FXrJYTrDmpF>WnUkHJI{*j!0zT5lD8{bN4=TU&Y(~HVe(4bfsmx@!JmrrdBdeNC6Cu}L_F79QQv=MEa=qB zZsYf1DWm<8bKf+pd2CPDlinsOXl#?T?39iwU5`8`ThEbH*mJy)_J2C7X?cdvh=wC`-+5(X>wU8yv6-}>-H9Lce;)rP zX{k_29N!TntTeb2G4}d!2v^5(TtS!=4`s)r?t?nfvDfy^J0iVPvN(409Kls9ef<@M zx+#mabp-d#oFM*0;vP$k%5q-(tGE7AG(|_$UFEM20vr#w>Q@+Nk>p5yFeE0116YYizTxkTje6u;VVk%Z*lT{FVP zsyo{H=C$u=elptY1s)2G=DoC*bVzvp#1+5SUoLy|E%4lFr=yuO%%UrmH^82nZOUo5 zf9pVeh%^~{TmP4i`u7{XAr__lNqMF5t0a7ay@r>51{%nX7F9hceXO-oQuZyacNk zY3XSX_6FS^s@(d`q(%>aM<%HeqFZbdS!@m5bVuM%BXNGrwBr$cSbO`L)+x~8C?xKl9cb1#qN)Nwzp`>1cB&(hI z`i@;KludgH49kaf9T?{-!*&~UM(+DS6-d!Y=zj(GN z)b&Gi@kndOk9UW4_fjjFEXk(PPhbp>g)m;vRgFWw6OXdBQADejNrNSH8m#6o* zXK2Lj{I+MR$#3u%NBC`WW|*7+D;6y-23oSvyFer1A7Gq9CW8Qm3LdDmXfbexf(Z=3 zG9^Hb9mXvPz`hgLBnnN$%|#u3?aWd?hH5j)=kvO1(4v<)#A|iN%bD-^qoueH=j8{O zrhmr#R6W=Hz@A)$aZHZ0C7aduKxQMqNM3Vrle#M0N2KnKyF&@hX5@!5MGa219vXhf zu+VV(`HlNmZnU^D75=1nRCVTKq;=jqN$Hy$OgWUznK|#>FgZNa``L4Zt;eF&xuj2> zn*T;n5E!VfW4X0*)<74neL{e*6*xN{T!t1*H*u#`@W&| zKi0C?Ca6uoNt?8zeGJ2IiC70VoeQXWiakAYk9(7=G&p3}lOxOE@(|LPB zb(mAR`Utef*501)#PWDGQ;mk_TjnoH3Kd>W`!W-keCd*E(pVFLl)C(lq(+RA$kj#) z#R`?>Xg<`h5e+u7E@c{w7l_l)#ToC62}E) zN$Z=+f%S_i``!2O!(O&+0-)mrzx8VZ@cr*d0Hkal?pbGiV*i+%KSTR73903+zF6zY zwHCB-kyV_8kg{A%k#-GN)O&g>qXwDQ{9hqYn+d=+1BnfTTJlKNLkoz-rd_uVu%wl^ zo!g}mo`kXQaCoHxyJD1rXk*Po-X=*<*kGXaotC;nH!!~Ku1|MO)t z0pRnj?Snu0JsOI8e%dcIlx^-vA0WMu$R}4=}%A$YAkASmMJQ!{HHn+Ao+;)?DWs zW6nCNhpy~ZH8`Tg>0WBeK>k#K>?G>8qRPv$_NyF4t|KYi4ttA!!Gi z^di9^0L;^oK(hfoez2K?tq3YWuss52dmI#ql49ae;G$tLiUEaU2rCjqd4fM30|`eG z)@;wAfM7A?}w^j)D0V|jT=RavX&*S{^SO~%}Ey9P> zp-<*qvZSY95r7j&-3`t;sTGHevY)CIp^bP(eomHj$^Oe}k`g_`R-JS=E!xl0__%wI zbMBEoP0I_O!Nugg@-?Ekw9EaWd+NgkQLNX;K@<76)JI*&>=?WGYP;TMv70Q{>W32ISop z*qsv;oACN(a(QyYKIP--In$jl_sZn#^x_Eq5hWmQ^bA`NHOn^Gd+V8XH;qQ}j`dVt zk^^$pWGH--SIOBKw2-An=to|a2!4aqQ&-nrN6Yj^Ivw_8*VUZRltw$_DzTMEc6^Z4-wt&l`T> z0c>-}VlaR`M4$niCk4|P6znf(;2Dd<=nw~lI6Smzpy7cth1(PGd?=WG0DBBnM*wbv zCjfhVYuxeU*$;=dfcbsWBXW+(TyYDRojs#S&aSbn)i1`)PYfERPg_PhT1aL&g=iZh zFL$|0>VLbqWXNYacS!m2>EPmRX()k%Q4xR6aJE|%?iES`swVq+s7g6GvDC3Bd6IMiGpqh;F8!O}3d0C5E%rnpPcTpD(j}?x#MgEfj_| zkGdaY(=K$o)FWH#y(tSb*8xWk>QFRw8n>UlMfFXa9|LL!DS7j5e>1%@x@})-xMA>r zai+jjjsPZ~C=^Bv@>7%;n4AFU1YjiufCs@uP4bWVHspcC<`hik@Zj%4045I5s)5_T zwWtt_OC{@P%J-h9zcyFpd{}sJSc8-LnmKQo&J~aE)go@|f}8@h4YnOF$38yBOVeEE z4y>Kfvk&l7lV23%{Q70vfHB_q^%vmpHfIVX`LGBCn5^QYz$F0-R7B7pK;zIj1VEjz zz~G0WHmn;kn7~$nixC4ZF2Em$p`0kOy4o7{?i6kV7@AWg-{MscF;z|VQ~^*#0m{YanS97DKAVB;fjS*N1Xd^ zUVbBl^cpsAzv%9fs*XDO)xlT8+IN{}H%capPpnI7=nt)Sj_=jjlf4j{@tE>l?&Zom z0UuH)v@FcNKe4!eo|$CZMa{Lu{r;i|z*i?K4kAH(5a3#(C_piT)&Nw!Ky?Q{Cn-o< z01^d-2ylo%+(tqQ&}-mU?g9c(bj+;c8%i;cuTQDI`)9 zkjq3sffNQWiUWUR5E&9Tg#s50=fE!r0fLg?bSw$ZPk`f=M1X$+++cwB!iq_VqP8Rn z9JL3`HP2D(wik@R6xUK`I+maRsC8h@s?hCl@+ukCSlP-YADy*g*^@PMpWHcfluk60 z5f(FaDjC<>iyZyAwjDLZbyWP~SGCPO1(i5hG(w9SD=9(10`da{Kj3shY6cJx;yfPs zgkWSL4)_odV}MK(G?a+=b6_44h{j!;m*0n|6a1AAN?kho(vRf7+vUtB@S>^yp~4tx zU3}+@qx!bFFYe->*!1nJIV*5K^aX2P`tw^^Sb^au)&W@bsgYy5h)I}e-;)Guj)&i( zRLD#2g&+6b$=8$e^^jVTFHhi9ydL|h<>3nSiMBiG)&;0M?hFw1>%vwXhubStw&{m`cmmJ89cmZ=tj6%?$x7lf;> z-S)V1KMm<CQ$L%mH2hQ+nMol1W@Q|y^osV=g((!6tKZX-F)id{ z$Eo=o9rGWgQ0pI9vFALT_9O)ohHaBD>tL8)E*?6HlPT5+zVU(FF*!FxLbR9T=AZObv|8 zKp_?IX#iPB3)n|Kic4*&NoY`=_eUe;+XH)V1nAi^w z`2%v3@2pC!-RBHeGHh3?Jnq--ou}>oaktT9%RMA-y_X&+2UKg$HmK=sOR8|sFRTCU zMFB?y2^1dCSAgjmv_MTkaTpdLMdUO~g4U2IRJ+ihLW7GQWG50(Gy-@VvQcpTMB-pf zxVa4pKg%!{I#{@)ZB@Q@CF#ca)1Wu#@TVF5pRJp+k29*?5$Nhwvr^S>&MJKn)yVky zyY<+f);z2B2m8bsO$`ypx5aTarF?()+lzvn12Eb!1w=}Mi#!P00#}xR0b?o%??mTy zDO1p~fdMuG3+C}e02?$ELGlKAZm?2NuiISI6i>8?SHNrGIRf9XD?5|*8_`+M&$VSU zCWWYLo^S1+i$2hAOg@^j!`T1!B&pGK(Q$5G^Nf%vnbVf|dtcX|Y@05Jl@50N_M)&j z1Ri?Oun5p=Bc3i(7n{ceWyCoyNmJ#*vyDDG#PK8Q{~s$*i@yYLQePswS<7o$(DQ+V6h z=H+3ZN#hdsws(_Le@OF6a_gHsr!gq?dZ-<0`9WrO-R-TwxcR=kI8p75Aw!Z1#PQK+ zi)It2tdwUXwD_N<=P7qd$edL{3@bZ6$jhhSfix~Z9Jmm3OjO(#7SPRn0y<9jkZGdX%`P-Q>`os=y{u@HhfoxpqaoL~}H;BrFP7fvY_9mZ$r z-bjTVqQsy;{9N$Z3>aJF6nO661dQ?d6S@b_YhW%YpHiheY(C2nAZA}1uhNyYU~+-U zamR`W$%n*GW?Bcm>OS`G3>-@(z6^~lxRXbwm*$4mM>ERBi zu17y7J%(p$T&luOJ)0&YztxNmo1S*G>H5;Pp_4eWLOcDG$tuT(gq=!i}~obOzPaMdP``)zZQ7B#X`7b$vZ{1DQ45wGVQFc=!qZ0kK-wWqQc_ zKSi2xT+>%;K5{pchl{eJIXRFe@Z;3j6W2S=mj{|gA9B2;NWLfJapvN#4pH$5K8q}O zadEXbI+-!F`s_Pd!jHZmo0!IVC12c*cq`8e(sB(SL_R2#pOJ8!zI_$zhZbKjsyzkiBhVpvQSg| zB+&&Ey0Rp3N4jYS)Vq{XmSr^5&2A@`2hqbkXot)P1+gn9*~crOYRO`Rzp^u+Wr;_6iOQLt7}TAWcJq z<1b`4K<&W*A`QNP{4U_OiDDpPK#v7YJ5aaB0P#r@=vl z`}^wuczizR@hHwY-1oWN_kCU0>$;wUs%5xYyeaznstPArckfL6@j(Hs35LQYeCK>rEYufV4RAzpBfz@ffyaU{ciV5 z-$=TvxyUu`FkEPSv~iaY@BT@a0U1Hpg)v&UtM4Ayf#Hyc=3Y2XX=*l8f^~FJ29_kT zoBwgn8i=XUWbMZz%q}9w_X{?hO%cyb_$B5te0I@fOd!awFV0bS?bSU}qDun&wl8~% zWu|309(0Ip`pV0GL8qZ5)2O*WvtmSBb55p^Y*?ZiuQ@ew^>cb2Z>-5$(oaF&dk+XF zb~xso_hsMGuIutdPDZcYDkFGZX4e(d4yOvWR8_u&GpRaf7UPbo?0x>g!B5Wn>B|Yj z*Q%OK)66`BgRb(Vp7r4$IfmE9+oz7Fo6_HAq&y|$-1jntQZ`ej-t~ZWf$NyZm+o=a zRg<1<3l;Nb-*v-tRQnQ3Z}k2a!Lq>8k=yMYC+}ce>m;15&Fb*!w!<;iW}9B}MZRch z5HCC|JXNURY9GEOLNMW2P2}xb!DpAy9KAiR_vhG*PS4aVor(Qa9gGkd5X%l3j2NFg z+r0L<+o3%#JY+T=cq!TR?P|2`7lq-oeOKx~e~tSZCBLt;E(qe{ss+HMn~Y5N+lq1N z-%tQBTJ8T>F%E7D-k;VId~H9NDQk$5 zfAoXJezQxX&BJ)*gWJr0+ru(pYGn`6@aFPkxd4z?a?WOA;sj?k5CmBbZgR8m;e)^L z*j0KwXplGsjc6>itAL#VJwxbP;TX(TSVux18}JIyO+)9zkA@EZ|O)? zc%>%1Ws6ysv&q$L&Yf~SdNhx;yrGj1c6>bA%|Eqt{5jV}N&bU32(8*Eoqr+8uq(+_ z@bCe2$%Ae)22>#+cDDES!gaq&SJ>q0>C2>D@S+a)_(Sw4tpvda@fvYTCSKW<@vO)$s zgnGGq+UU|vTwU!6p0X$ftS#OQf!Fh}SMadWrWu-{X}VNIAO%l!_Xu%A>B#z;d0D8N z6LB7vbaz8@62{fiT;JURz#hIKG&`z|zm9?#)k{Ic&cGl*TR{_Tk7!HR4M&3$(>Gf-A=Gc>TEn%LNcxZ4|BX=7=w z;Q{s@G|#}maO+S{tKe{gSvY|jYU-!wNzowrsB3EkdFh!DX$I=r_`qP4CDq*9Fwk2! zh(IAIm{P+`6kPq)jnz#w?D3xd=4L26bA4kCAJ-5qtgE)Aeu$A3z|8T4a9`eR~aUdyNn)R~*UQ01*_ZZR2gL4emO@7~^0n z#>3mfTFcuy1Z3Aj%(1rMou?NL^m%={f~JqVQK+v!32#hLur#rB2n?nw(Ea=Zbpw&c z>e{YEUDpsRw*YmLPN;DZR*U4R=NW8eYD2&(7zJV!^n$fzbqP3vv1=gRLLCu=)HF8N z#OqSLLOrZBLXC0mCWam~gsh3If(Moos^g^@8j4m2I=HK+s~6SQ+yZZoQP2u-@DA4V zm!%R6WKI1{a0+35t~x$OZbs$^D=k-ZgqDFiTAvOV5uI+RKr;f&HO z(e|eP1VztKT}yMT@KA3A*3HyQKg=V{$k*D{HrQU%Ho#jk#L~iuY)TK))WX`EqR_U6 z0gB!<&tUP0En@#?4B=$y`Tw^i`~Ta}^h3qj=C;X9?{A!naw^pO^6p>bj$1f6(b&+lpFl!|zj15|{ZMbR|A(%` zf#disY7<>+hvwNmb3c7>&zF4~)sMK~wPxab4oO(@@$+lnkV?1`)#MBI%6f^M3oZJm zEBS70F=6XDso(V|iY3V{Wp!KiW9?P8gsJZ>}NvB`z^O|R_v#xWU?h5O$vEh4bEje$!)qCNz=^?*{~PaeVMcS z-DSNT&uf;~Lk}Z=Wu1#``F08?JR&Z7>JxX{S)a=!WK5sH`B&C7^G&MF;VM_+O|gVX*P59#GgN(EhRg?e8Lj+;n6JX_@X!Y?$c;O`4rvJecs6VM~Alc zE%P|Hmzz{{X|jQ)l9j_M`g4Y|A!+UQr)S>_8&4l1{tz}4o)SK4_d@VsR*5_6$DHeA z@|SPi(S1cPvrcV3_wtjO_y$^dxCiUu3!Kt{Cs;Q7iQcshRx_!;zm%9OB~tzTBl<3J zY(27M;JtkBx!R?35#J2O%LYsH#Cw?|7R!^i>&VmfJYuD@9m?{J@q6y5XC);mXHzBm zl{T*XYW*T`?DpDyL0lnA(Gu))!6&f?F_gX`#f^B|htZ4ji!67dg0`g;(C$6i%zl#l z=E==&-}6RX^J2r)Q$$2M_CNYLt~tV+1Qpe4RjPJaKcgP|txEm%pQuv#FC>2A@=iRs ze||jewvhF+)0sy(v^#rf^%Lfk^u9-_v zJbui%;KW#p-+|VcWBgfzL+jIAYB6eLKC4w0X=fi*#{&H6=XpHxdzAuBslef`a7BgQ3s9}hsObzi%38LlL!uxhJ|Q_ zRwS&1p_%3~L`LJoe7Q=dYw8oAEz6q_Yu_|z$Sf!DKn>~@>H|Xv)#ButSn{qMPJMFC-8ci zg4)&Q_FkLZM!yMg`Lu6K^|TMzF1XHC_K?^ESsmtK$;k%!z^bx#fvgs{V;mo^ozmRf zf8eP{*n0j=zTv%F8Vo3Mk2}7+F!v09_Zsr2;E+a*?K1!u$FVwy`bS17Jqa#noH=<|0mbQI*tfG^<`6EsoHBUp}(Xaw~{K&JU+?Ec|Z1-y4Z)? ztYeOR@5_?Lol;NqAS=I@wffr|3~F3whIb48OH=l0FoU#5b@2}Uh?w`2Rx@ke>e zFE(S>4i^_~i5m@na$8GX<=)Hm^TYR)i#nVVenc#tx^~Shfa&yy6m;!l11%|b+Y_XX z$4YekqC>;Ju{+){urtnj)F;!5TpD=_abVThnuupak-uYW6w|*Uwlbtt{?P#-=>$he zG+%pVCwB69jtYg1a6*a0KL6VGH@mEKmv)~e@Trgq7csH3L7(ZZX>`pH`2FSB%GlOF z)SVg6|9bNud#DjREhj1S;?bL&3F-p2aU^a}*7v#R;g%zJclrfK3p$m%<&2_a;^)O~ zfBvF;348UoJuF)l|DOaRxf61Ae{AORsWs*|N|u_O(Z}V-e>Hi; z7c9LsXX%pz`d%&H#=h!Fx8x)1Ma+xdd_8#1x!LPdrCqljO7X|=xN;Q}8-3fVjmXxU zS1bMwL#rGV2|A=GA_6p(5dioC@dYZFWdLs!>OLIwB0;+a)CExp1Q?+(oM}N>2?M1c z@KeYn=#>9WV_5oFp~GIZzr%c1&hS=YICJ}`mSwB~8MAJCs>-v%*N0BzH{EvO88A|N z>rtmKI&W5O(_j%#{(#!CaM0{r^nT{m+8V8s*?0bb925b%_7osAgKHV+S3(4Z6avzZ zBpM^e5&$xVfmjNG^e-kYf`|mYC=iYU1ROx5|L%joA5YD$RQbU2{NOJ|`s69zIoRsrq=g$St9Ra$Rd{Ca51(s8r3Z*NQI+`wbjiy!6)~oV`O?n&A3G z*So3olz|d!z@7DZ5lwwN!&G#-_426dVL#nUM$m@EA+x(iRA`shVn5j&EV+d;tl#$T z@d+E{nYA}-3N+K!=fA&sYklc1j?KL&bF&Y#E;`5Q@oeV&nlCO5Z5ptb?=|av@y+Jk zMSc_FR2WHoQ8HGTIMPt+xZ!BkHDLi}cDMAjp0#0*cD>ZQHMSRw*HL(e=7$K%}c(bdPNT3l4dg;aYawiS%D`O8JcAGBZ* zDLlVDT=U%oNmiBL$EZtY4W&=}$#{prb%$NI0{8e;R$U7;c2~>Za>IvIb=W6KqP3Lz zotN1&bSiaQh+@km3p2;I*t;}oZtg7Y$J@Nj_N)y}^i-Ej77sIY+P>k}Gf50;F}-=l zAU{`CCGwMevuoSdx-}Q$nNPc}OZt-)hV}M*$Ope?LFRu$7F-XT{6~(W9ep#*TftLN zYk2s<_Hy-x$)oDDD0%<vKg`ag{>@Mn{=bn^;9Q0#(1_qT0In@CS%h!| zd_Ev40$+v*uqq%hf|w720@FAMu0TVfTg)Y=F4mfxRdY6d$!Arf85i) z@^>!0jO%S`dp?B<@X9>7JM8wR zL!Kic7v@ z2=h?v*UCm^g#e-QZvC7h{2>M2B<^%~?t+x|+6CJWibt5%wJvqGq(v6)W9_~@dn9Go z*0(o**k|!PST`6Eksel}wB$E5y*1jI)8ygoo{Q&f5-;0abIa?xS?kd5XZk~2xOA@I zxrWY*WY3O97s{l!Kh?>yKRy){RA6v2GUp)P=RUCd7QdSYXFJ(cbC1tu=6UtyZ^MV;EK2?{_TAf8vgv-Vqxq zL)F`)X;4r-m|T-IAfiX-YD{TctX~{!?f=j_q&AbMC_F5f9j94y&8pzoU-t#$gE28J z-?1yKGHHS+Q@rsD!B zdUon`S3QGpb@dQ?ybs!64~Y*5)+J%wb?Kfy_O|Ll>aKxCWKX=e1_r0?Wnk<<4zaYe z4fdu{0s^sAx?Py756#!e+uRbb?QQHp*7Ac{orMBkUDqVg9D%h6#?pKV26%&DTbOwH z<2`~+jAh|qjKfX+QC<`atd+km%7^G+8t5B@4yMsCdQ_qV9uaQr;6o-MOTcLFCFI^W~mhzj91qNvnwq(ZJls)q^~Z4;3I42$@ozP zbq!ZpGsWOQ7=RE6feId!z+h7|cLyyiD=N-Hmg;GP3WCXp_3)c>$>rt)T9WV}L ziaXgP(9|wO0cej@sx<{i4E3YynUFEAWOG?dgts01RW-<(w%#P;a4dfH`UY%(#Ozo9*TBW-rH z85o2Iy4krSO@nYG4@)C`ZxdTICEP2}9^-EtXym4$Z)$-L^#(wFh`*JhrJJsgo(a`9 z$lY9BQ(fORG*HjZ&(}BH9Zy7t`)GL+eT-c_LrpzH)U{1Qaawp2PfL3bOA}e5t)-bE z(%;9(E6`Lg%vf7nF!VEl474Z#VOl}q zbW6CDLJiS2!GvJ55OunvT7)-YHwncgwhal7`=Ad}+-}8|FR}A_8H4nM^IsVwE zs7I7)yLzvW%I|Wzr_{)&8{^Yjm~OR%2HoFSw`S09vji7+_+n)F;_C$Mj&jVqs|w8@ z*sognGF_}<`m=dRbGyrjHrBQ4gi7PwE?%C@{BnJJCA*wNRc95Bn8vZxnYp>(bk^;S zd{M3S@9V=iKNC00CsTyK@B7;4^+37YAUWn_|%< z1PU}JKtY*AC4<#85W=uHpmczV571R$jm7|7Bf;zr1dCz0N&K5Z9CD~^mi*$XW$dx@ zY*Uz2w{Jh-v0jCZ}GHD)rT+3CO=7i9L}rTq1}Uf33`@_X}f?yMOV{Tt#W4i{_)ES(AV5`X3(@U>bmlLIAiJ5oR|)Isz&cP}=}wg$F(~kqAmsSRAZ3F<=`-pn!=M zEGB{ZhJ%(Yh@AedtXwalTPM6#Q~$g3B$D2F_ugkMsRgbJ3zJkg?Q7G%zP%Ja^X-^l zbGMa9PIi6A!!uM`Z#7L{qPJbl(|tP^Ho9tCD_4Xj)5w@_Xs_KGljJ1xMq1LakcZqPE0T7yhWFM^y4XOFPSkk$=2U;JWXwS<|Wq zRbSkl|HlUfKr!HU5dqW>PVC@nL4ZU7LZ)D)0GW$exxjxk+A^ zin>ym6&~!1*H?K_5*+zJ!0vLc^Sa!90g(;Tk1L35%AbA;zo2bD&i&B)>Td17Ish9s z9DE-;rY;rMKbXWstBwnd!B!<6?7ZCj)X9Q-|F+jt(}RAut{pAYP`qJuQJFobPVHsM z&p_qPjt>^|@h>LU7JBoUUzGcDAzhrc*!pYitB(Z#ZJ+l!z2T|qxh88PRzR#CsJLV< zpM!qxiaUjKDYkAR zc^eQ<4t%Sv^}2HsBXppgdEZD}=|d;Ql;o+*M=uRCg>rO!mrPH}dhB-UN+>h9#+^TD zXBV*}XlZFF3vr=mIC$p4+iWuGQCga8j@%4A>&%HzzCM*)5!s@!{gtS|?L2mc2QpaX zGTVGzZj843n-ujPZuHgZ*IalUyZw>MOke|l5xH=GpOuUK&Zqv1U%l<-MKYXTD{{GT zW!S$xDdVRT)PCm@s!8AHMvGj~x_Iu+j#H?-2&jEmT~FW{fcf9o6RiJ<^@NZmPfT;L z_Jb_j!I9KW?`>^u2jnKE)?|3GWKMt5`(hOJ>*&5x)Xq=L&HH_Bzh(LWA8EPTUtUkF z;M5rJ{m(wQVttca=v8Zbo0LJr!0iZOy+hu&J~;){*7j`vK89Euq=)kD*_D>E|46F2 zAWB2Q`L{hVHujGmLo~dw{8(O3Nax0dq+xZBqQG1Zyjp0$V8H=j6>z(FA_)G#n+zWw42b!yU{^n<>nrZl=l&M2hA{oI zI3wv)YQruanlGYR>``EZ-*sLsh5dNd`T33AUQWCx=JeaX#A?pr^#l$kd7Q9{`)IT( z67Ex7{%^vHtF$%9pgaW5>{t|J1S}D>k|8CaC@?+*WCP@L5($N1Xh(rg2oR32fF6f@ z0_|5|r(z)*<5t2D%X*zd%*F~u`S&Beqomzci&|bJ$0S`>^-!z|>S^1{RXi&YD%rhL zJEM{->%rsz)iClsXQEN4z|k^{6(+!V)r9f#63oRHW4RYz%%5U&JEp#k-ZsGFckfoZ?|MEq5k3~(wR#JjbUp!3+W>v9 zDr9A=I96-2F8_SC_dBP#(I3*A=Zd!Wp6aEIan^3UwAV)O&f61{uY$MNi{9(#J!T^| zW<2A@_SI^MzhdCZ*!}jpHRgpaZ6kWOrSv502`3e(l+;^y5$ZOl8#c0EPsohF$CI!< zX(%aK|JIpfyL*j7HlL8fT%MZ9vdYOjh4K4frS^-i`%yc7xMp(wQ)Rt&Nh{ZvL;JOj zqOWF$3mqMD+nx8M$>415K@HFFg9mIn?pf4Xp6m9daP1e12z2^#lW%?Q+aFrhT(iG8 zwRHEUd2JFM-{NO}Nb6jXujOcht?}4Yp4N^#xU1j_vwCbbb74f0-)4+#|B2Z8*fe@B zSz=-PVKyTP{*BDK(*wcHr-iIvW$#@wytek?X}Qrh%L_{SJD3k?bwoDu_P|H_*Vwv( zu3)_PKV$2^lldAAyI6iK$JRaaqcAuBe~1)Di*UXdLhRV<`}*}-D~{%3g4yxjeecf6 zroC(EAb;`CO1y}7dU-w_5rA?edU_o8)|y7SU)jG;ENhU@Qd#ZiyNmvHf?4Z^cz!+Y zOn#hB%6UBFGn=%tC}BxcMAq1r&muxcJ~Q^y_+mWT?}r+q=-Gn@pLQ^5wn#-NbzHPG zSGny?dfZamCmXq|Bqx(5tGY=wNA~N?Z!3dzg+I6_)~TE zT#k+C=^&iQO4=%oXE*z@aj6&WG_3#BD#VfEQ*XT^vEL)#g1tA^nY(PH<>VJmbasP# zW#W35m+6m1U+)d`#9R&#Jswb(p_-z;_v{ACvT!!tqh~4uq9kk)e9y*jS#$;@Snj%F z`7^w9Z@D9Jghnw6$y?&1eQZ6*X?kf?*ct?!+?kz2s*rImzzx%E}ZLTq> zcx3$ss)Mwo<7^J+S^+yyru@~k_(&saI%z6R%ORp?bNi|iB26u09e%y2=d&MW zIi3f8N}ozw+WufPXh4=&xApn1nsoUe>WA}lD_SM@j@W8MRCh+Y@~QV;DVbeZyQAXb zeSLlL10$=>HPTA%{3cSk%9#R0I}X;1cmmi!;bF)D$RZengK{L5jDykz5+VYOhVe9j z%L5UK$^cU#G4K}v^DB@~!LGR8dipCP~2nrYrfX z6aE%+Aojx6!iiJqE}m;r^zXkoq_x=x@JD6@yrr$;E3=Mg~8`1Btt))D~nbU-#Ry6Sa*Z< zXr+?_&z9OqzW%|EO9=;=^Kb4;@PbyJcz{#a8UMOrrnS|_nf8Z1e2agq>vflMQ*~cg zc}wq)H3q&Fj~muVW8ANt7>WIy5FBHE7)d#^?#=B6VckZ;V|yt_7lRhnJr4-QSLVjN zEgP4HPFhW0V!P);JwAUS=Ri@YmSdaDz4t6%m2Ngm8kxW6&RTq$^h1GZ;+Djb)Cjw+ z@%CGV#n)&oHNQ}kV;kPeCoQr^@q^3~Ey?JZL?qFwZ$L&q?zMMz_$i2st1hk%Y+|Ik z-wFWse?tKPqq={NSC6YD-*1_=IiOa!FWXpTPIYN#faKnFPwQi!KaNO=ckS`W5;VC@ zrDtxVj?ooVO2 z)07P**t6%7q%2<~92|4B<=832_X)m)8mX_f*3XY%n;v)P z?Wju_PuNSH&eDD8tNkIU!gSK-%J@}my=>(V)i)z)6$zj){d?RwDs@j-LU2RVto zY;WX`Y}pd@L9MYndL%7PB9nM=eUO-!8@rUQTdt@A-$&eciMrm7TARx{x_3u^hG9PM z-Yr_DR6Ffjyjf@BsD0-X?;TPD@BE~OtxlhveZE)7*1D~RLdY1_WM*%v*PERGsOB=b zr$DPi?_F!=NMBa}PyB@5bI~o_TGZZTfv=zAqYfAxO39gfQzG|G;bz%-PX0aWAMOpM zD$+z+O*o>3P-$(geFyh!K}+u4*!Z9|_uIqhi_$H(`p&MM8rT&sE;6gqdE)i0Z(%hf zrgt6g-Osh|3DQlyw@vSh{FwpH=-utiY>jvKjI9mkaC>&YD~9@fvs(BbQaLen0Lwgf zaA&J)l9cJ`GdY_ypDOLV>r(V6{AfC<;0IR>VyuYYSR#+?$;uvo=yFe5lEW#v!VUBq z??7>0rDJ(jA7|a+B&>4BvWw3!uGio1SdRb19lKZ7=)k_=iAC00*%Hq1?OvGly$AM6 zg=HY#9(wZ2IX=ZtHZZ?XX6rUz`bG}zU&bno%ZKr~;IVwit|BYC?e-LUf`vCpSB~e! zf*w~7Yuv~|?b`cSi37Kvgv7O8{eIxv$o@57Yy%#iyqMUg%Gmny9+r(UkvIPbCuAcQIcRd+yu=J=z$2cLcC! z?L&nu*|$Gwy|n9z)rD^BJHC?q&Qjmgj=8RB-X-K^n0NKLdp_^l%05a({G0F9k>}g& z+@Ey1?>A7>{^W~vPW{qnNR3Q zOS)R?r-L)Dv6(m6&Y9Z8nI(AONv#I7t6Ef#o$o1oh+XwA2m3Ex#&bVC-+{O+hSI3% zb{%T!Lcdn>#zkFX6>|7|lK=hXA{+~q(brs*8^eRWAv*lV3 zJE7+s=inY=|3p>sOzowY%*UxNO$MhfoR{VBNatImY}s{Of5MYhhnr{PEb=WXIfM)+ zW?EZ2F4wLiU;^S5fCd-+CfQ(CxS2|pasE}7=)}rH~@Oq;6+S8fQ%XV zzkvw_3ApJbkPx7(j1Cg+ALl+e^+nS~m@ma9-KzgM8)Y9ed0n&DdY_%E{MvWWm#0vRs zZVe4PHXcUnJsDt5+u5GORU@iFTK~x6tYKZ}nuWEez2EycSu5RIoEeTEnb>(tBeE<~ zVZd(vswMl;`={gn_@DqZ2-5*zAc0{U2I^bDBZ2-c3J=mp(3gbM19q;U{0_KIaIXQ6 z6v&;R3J4m9ir~#hL9eVsZpZxm`e^^lL+Y1yls)URAPuNlZLLbPR^H%V#}=sl^U&Dy zA^JnD9mjoTg9S&$*%iwrT(^ewQ`0ToI+TCuHUYo|v~TIQty`olnrTcW=T-Swo`1S@{N5f{Qey&re5l*_Ay?q3Z$Gxpi`=+4WqWD*xYNmy z&Y!lw4mHebW~^}=)4LrQA7d8Id&KIj$B+EiPhJFd_eVX})Zp?cL7e#{%`GKlZM$2u z_Dw#MkVqEyI!<#gZo6<>-_p&`HZV0^$f#03BGQStz5U~x0$tBKXrCQoLk=t2&M zvOka5oi3#M>U`mU$lhf21XHEbXPYD7Q)K1%ZH>8ubk?aFYv1ol_s*U_mlrK{{M&Oe z#Z242{2h`PZT60nm)dES-&HTua)bpV_wTtX`h2EJk0ra&z(HxV8(U}kYdRuhou-KS2w62(E>tpd!f(|r=YJws9!%BTu+z&ihU!xy{!|DezNJ6AY~5RE3YUoXM7RHK0!a+G{KD5f|ICG?<aepqp~4d3;u039)%tL(#~EFva;+CdK5Mr&acx}GS)g0{5oZr zlV?z5ttDH|R(U)6U6;t&fvkM~V@x~-jVTwfwyZDNRX%q3b2Za6wcp5wsIiChMkDRU zYqTQb)*osWb&Q&_dVhc7p!1EGtJcqsIc%KP)W(X&&1QH#jnLImls!;nZ^`jS*egO( z*b!Wm&cjxxUk4D!<}k+N_MqX7dyXn!o-=LB#FE@k+@B zy3A9FT^t65-MT_uPu3S7+FGh_=frL?Y7>Wslky*JZw;1LCjWT^hAt0GK`;oAW+76E zfN3IVq z9}+F|CixooKZ}?>Yma$)ZP(ALm*c&I7ppY)xV}x1Z2vO(nzr!mKv}3#+UMRQ?<{36 zDzB;ycxLo>f#y}t6bS$T(8$8kz=;E7?XloU$IxAcD~yDoG4RIVX$~DoFb@OQ7ltk{ zbSfz*P(FmZ0gS;{h=E70_>E)Ut7u~Ne_APRZ91^A<68k?4=a6*o_TxUtVYcJ4Lw)e zsy7)=f42?jr%tQrp~u_;_JSU@64RFO-%VCS-RIu7icAuLyndl zxxuSy87~Z;{mqScl{3XqG6e1r0fnZ4A|?3m0)rDgk)ad=A}5~4@PvUHjzk7cVCag2 zQ31#}!eEjH%Ex$6YhIZ|zPwsO%}P4@MxS+FXcyK&{d9-G25FN%KAVSY9IXaI;v3V+ zJNMm|-Ck&UAJ@u9yX??7UEaVs;*v7QbKUb@J~yLOW7%3rxpt%TedxiPfrChakU6=X zeF?pf@AjXPn2TZyvq||n>$isHxK?eO@Q>2z?q2h&LP6gHa?ZHroPIa8Gvfvq*Zxac ziuyDOT%;29y2~kA@2q#+bjvOQn$!_=n%drvU)w%zKbC0mN^4F0L?ln|gZGq}r-}g| zhYIZ{Z)_z>NM zW8V4=8OJBqc6-43aNToXyw>1b=9&97p69uoGrVpH&0qh3v!Vi4n^b#Z$J6E6b^9u)OYSu zsd3!8?|$jaBaE_QwNg!j*A9N__fn1PKT)c6z7nlqnn(D##y>w1|G-X29kHeeeKYL* z$PU@Qb?4Iimh_8udEfjdW$ud4;|tSDhmZ8HrP?YekN>nnu8h|n%pbEI2ivX{|8W=s zzy_=c8G=U$5LHKlsx^f`0UIrlzk|^M2&`d&G>9UB<|?SOAs9sfq)Qkp0QC&!3bd6* zWa9qCrp*HapB7P4)2^Mx5`+7%>zs6Wvy*-Q?EMj*uQkU^RKupyE|-5VzS`J0r`!AL zq~dy|EtNT`@9H<738Jk!xqAH|v+9oz3I~8V0*J?gdO8fsXk&K?D+6Uh711L3np=K zd8Ap)nlYmH>1Pd}rUz_tJ9+{x)qRXQjBoY7J$Ferjq>J8xz{7&{HoBEnrn5x5d&9= zt>8xwuGx^v!GQpTrf>|+erR2gz?v0?ZqTRzv0M_MRlyCN2F~pa?`+_u!a5mjwi$TH z<%E$WXkFNN^bo<+-70Xi^co&6x<^){^7YNm4pTy>Sir9uYVt>Tn znBT6I8y}liml+hXF#3H}h1}7`>EDQfs~i+K?tn=<22KhZW^dpnK?INuj2QuM#JDIy zpb9kc;erIUDuz-Of(pxi0Ih>UJXsNDbSrGfd!J$jlzG3D$%j)0Uj@zL>J;qIb;^4O zDc#t!pUf5~tog3s7lit|BtFr6hUD0S>vPH|!pc<+g9g$<6=@e%9mjo&UVQY&mmdM{ z4m_2L1*$OUYr!xMn5SUpPNrZ96sjT&+`#<|2hw7Akf4T?58OwXJYq3$2`Vx!!4&}K zO`*&K?>{ZLXSeE=-U~ka%;uBjuNy8SymzE!;vG-h-L-D+m_PPVSVi$#?YMcPSgq~J ziDBu=&<@>SUh%5pX25l0j`mDG`uzAC5%J;in{u?Q>b8tmJ{P?{#(FsPe;+D{bbKoM zY{Ttyxr=!jk@Z>x&qq1u*!e$yztd^Hsads(`@q11{bIVlr>^P8s-$|{mWYyyAVg- zRtbrBUoKrJ&YlV0EPhGETS0o7k^U+%{Sjp0S0tG5TI!#-{_R z=1F2|>kZ9LMA0Ab-X^Kr-T9u7-K>~3*Xe`{(pacBk?^~I!)H8YM3>*tL*%iQGnf1D zbu-WTwY-MU#|$_)@E6+)I&SQH-fp49O-VP4VhywmbC0f@FwjZ4UOzN~&6MAHS@~wf z`m1*=0-D3*J@;4PDd~qfUPpghoWkaM)wDVH4HXIH;qL2==CRaD5k%TLHuy|w-PUE_ zW}byOBs4R$@q@{mI@={D-K;lZxBbsQPuq2t#Z*VB@pUi%_>KjIBP0lP zgRe3Q)(?Pd1XUyGhXT1AZc#kU%z@zu4Pp$q|AE*lc#5H5X+cEe080BXXtoLN7j*g4z0TGH|lbzWBg$bRG7{yTbG zjK7l}y-TngZ&;N{{<^QR=Z_Bx3$7<%!T6W)4iUHvD8T#y)|v=TjARHoI2;5XGy?GD zpgV#^zyb?|F%_icZZo{ZJ-F3vAI72>r`qcfT^UOZF5 zO`fE0P(7e(KCC<$Id&vBE0M@QEw53{bzGL66qMLsy6UR^^_lMOKRzg6iW8yf09KL! zbpZM)L|GucBN&+x1KvX*5nEK$DhTJF{Yp~u!;;#RbZdzZHH-rT3@sXkrVT?f?MDA^h= z*G}m~tV)lsynC+yj}MAKfpY=`5)$b1Kmme=)j3opWIW@eGc1TOcq|eS9gy=BDG2C2 z(8y5VkdYKv>CynIx?%gcSNqiX6TQ|}ma2NAX_v9Z7oW5|Ay{NQ zSk!497x?%U*}r&Y|CQ|zUp_#m9SB7q-Eq;f4+xCR(VR6o##OFudkgZL_q|HE{5<`k zY-`!Iw+Wh`54#Dz)a8=lnOe7j4Xx26uszr7cK*{nJffLreD&`qUO%*>1ZRtM#eb5? zk<&q~)mNj$n!UN}l$NdBC;B<6biT+m+v&!IPnBL#=C38W8`jB~8r{J(JbZkl;rB14h)r z=~34o=NzgRHelPXE#rr7FIh` z+xV-`pXzBg$=qhZ`ew39oHywp&d9D5X>t)?Taobmxai2Ct5aC}8@&Rxs^p)q54=B!mlFoYT|gIp43Y*D%c^0hsb^^F?>lxCttvE_gn;%obPs7H z95`(;dN#l`V%()za2f$?3J9nG*#Pxt;E(~%8c^mije~Y0BoCNXEf?cwm{Yo*sR@?Q zr#UduxhyiYb=?ONObPz{s#$#Nbo;+1{@4~ne&o9MjOJM2=SzKO_R+qp45suszkB&& z^wlfksx7Ieuf2y^1=%*8i?jbPGyA#E!#oT(`)+1S9=1)zH(dn{4~kQWfkco^1ydt{ zpWuOj1ASfS6vA2pI8@+I2b>*1`QZVr2%s@A*4cihe>cpj5$5+U5>Dx9 z{~lSy^_kpU@X1c(_P)uiS<8X5*rah5{oHRfE=_vep(8XtQ&?@Lt=K*TyJPr7 zn3qbax9VU;ZCxa@$ZVTAueCjIGEBSadu06ASF-unJ=2bbRmgd{G)n%C$JZ(2m zp0Bsd;pz&n+W!mVuKB9KS?giQ&)j5biLb)yeBP1M@=*fdUx-|@bw{Y%7Bnv5l6o`$ zN!KWwpNqGuKkrKK`|c5tvBs?Z=Se-GplzqPdGpkXd{tjNTBQO+tcHhu0`qHB-jnVl zE`OhLtk_v#oxOPG*ZvDUlHUVd$12RiJf>={GP9@ zXpK6F*7$m;omYFbj0-P+Y*uC3db8WmkPkU{(z9Mq$jUWL{RCqx@Q?9eeEr8Bq8YFL z_hUH>N%s_G{&DxFy!mj_-(iUJ|Avt%f)zRfyo^AN8fGydpGN~sE@(I@!eWvHtzy81 zg4i=i@6doONFqYZ1_d^^R3r@$0$_`{M{b`_O1&kh`3c`E_N+*CTQ&y@W ziDC5_SKnz|btck~eeV^F;FnioC$EhQJ~Yt#He=xLxpR(B%B{XMH>4)Y|AE-(ldkZ0 z#!Na(Nsd^AnsVunFF$~h zFx1j9L=u=1LLWyFRK;L$L4{coSZ<+66tGGM=_g=D6M$+3U}Km{f%yguoq%49!mRY! zj>kEt#C&s9h>=KXJ(1KSEtY)jaE{~?4t}y7pIo2?`}5(hpXuJ;kB~^}90gA^r#f~X z`b-nNf9pj?XJh?%*s3k5jLh!e1~aRiDHtUo8M`3GT>bY<4mM@o)=WkH{sEYo2hR^F2R zGAyE+64Gy;;={1(w&v#k`gM`#^kJ`*?&p^D7A5C2rowdpL(j*oiLtMDRqkMGXbO2q z`g!`wApU*G6x}GO`1HF+o%t8ezV$T!n0?2IzsT0+`LSBOS@wcop|GQ2WGngE#?&3{ zA0ND!^@u!kx4ztLt9kkad$ZES!_WRS3ftb&i#Ic~RB!ZLuWdEhn6#6b&&5P4vB_3- zPQsY1CYCFFON^|3Z}*l9H7c*zK8lkHXOcfF&K89#4h~|L+BH%A*L_jA% z*9v^`z~l(K5?lT3y^_y%p1XI9_l21gawz0Zd|^$$K&z$bwyd?hqTR1|qU*O6HQcnZ zi1Oca_tA(%Las?^JJ*TCeQT$MgXd-b66MyvKINlaF~>Lf!@|w8MSF?*W85Z!t7LDH zobiPo^)`L&jh)6>#7CI$H@{{gkLf=17}IE#XfPiWllax?87|v-+1U4_wOEmxaWFY(lou_^YVVrIp=lGbHUWx^m=Yq z^mzTUb*Rg<(^#}g&$;Ej+eIL&kA$0scG~(%kOU+f-6{J{KcD5JM|E+W4cYVhhFd-x zU;9a}`keMQw*;-LH>% zK6xq{@rJhJg6EuqhOL-Mj@qs5aVG2CE|_naS@|fWl#qE{3O+?ligAt)*BI=E9MIOIRVkj(o-l2+VD$R+l*s*0RF0tzcdJ|;BCQ+sN*p(8yk*F`mnPTvT%X zle@&)oeYDP#W2J;79&V7=?SQ9xxMmQ&1T&P8gHHC!cLF8y-;m;<$IvyR}EchWXhO2 z{!_PF>cM8~Z=XjfV+WtVUUmlY|96J}+C)}rQF{B^VW_CH|Ht2mVkRss8h~!FjGiOx z@6lK?^aL1CVVKqZHRSE{U>KmaKxO6?b3Wo z)m{JEvrsuP{*_T~8Wm7xuX+)q7ieu0STJdtX)S>Zy}(bw$M7@EYp zfinLytL_gsS#Ev&PJu;ezOm}H2>oiO#w9(y;^M!!J!~ zbkHH}*PdBf-Qhil&UBRHg8z}ROZ+@gv?K)orgsG z`g>Flq$TH#RtGZiZKoDbnG{ogRO)Hr1P?5`jGBhUprohu7@<1QG} zsi_{mSMl5i6k6{5!FsLXTx{ zLZX4A2W+i@SO$Lum5-&uL4%8LEUHItvShgZN3^Pl<74dXuU|p~5j6s}GxN7=0)+@S z<=+iOhinUy8PefxpZKC>nB)`wo%^dH_vh~Fex}>!!J*ld-w#8u%}0@OAV^JSxHKaE zVr39u76R6J3_oNnOtHY#5uO1fT`)%m+8mOMfGG(9NE}cSE^ePMs{~oaot-#-X%^{F z9k=pTfr~yj-oN_YPKC4%_JH$ilDKuf9M)a54?H7OwPFozm02fCxlpf3JCU{g!6Vri zrjlIo*>7`xdr{D_A%I>g05kyUM*vJF^gVzW4uVIpSON+O8%t0>2CM)`mH-n44Twj8 z>VVBW9(LB~zeTy(C)BST9Qu{Om$!e;0mpicw-*Ju0l>fvGXpG0A0qK!y-h>| z)&Y3#1aLS6U@+*$LYhD`k|5}+fXg$~vKYo>71AeaDGRo^-8|<}L9J-d4ir;5dS+Ew z!M^jW)UQ%c@=o6$X!CL1cT#bm?U-w=%^K!XEq;>@R(c|A8UNya1pOxw;fbd;i z{Fv}T(*1&#imlonf;nnamMWF3b@h3FV{6lgu&qry;zUC0HY8?sDQQ29-ZF4s=yFxc zgO^cruF83Pk|ryjc9^CIrAsz%3XUgMAJFh5)-^YNFm*agdH8+hY8y6Zs_>JF@B@ ze$PoYRooY~P32hKwcB5;UtiSOJWGp99ErZeKK!1i#H8v|tKo-8?SsBInnu>79zP^20kD`dNq4NA<+6GrW^_Z@$KVmJQxCNdcVkSeH2 zozMtyiGd;&M4#wL90uBwU?d9=T{059NkK3G>=1}FBnqlMq7&f9DGXy1GI437{@s9R z88P(S{;D;p(T`wT{Q50MPjB7$C%&RQBZJpRRoK4cLg=~{8Fh-poO%rvZhf&tb!F=e z?CZRO-KE2ym^P7fKG!_{?L|Rz4$4mi;}0j;>qBzDQUMSL23MdJ2wD|*s8+$cfevOk zSb$ssR2)SGW*o>pVu341TmqJzb>DDWA=ZC*uZmePQ+RdpjgsEqUKA0O|DlTypm7@5 z#emERFvf`(P|pWY3cyl8pbPM2G$_1b%fY}_gYpv=kmJx#2Ym}@gDhsj-Vf4--}h~5 z+h30UX!7DaXXF>1o{Pa!SY5rrgt<0cupGYGerHPenTorOI^z-V=65J240Cr~xLMfI zQ!6X|mT9A7Jig?^Z!ZcDC>x+A0^$S%qf{K^IEJ4lXgC7=1{$%DM*yh;8jolsg)3_12Y;$I;1au>qj-|E z1Lk;Iy{y~OY9MKAaJq2h)aq;Y88u_>H4b95ymv9jP8=CJZKkx@XrQ3;#L!AP^YOKM zTN_WVP7YRjvuAhgOCvime^!ohEnP1Cj?f#{uR9WjO)}r%_ZP!C<5-MvzSM?beE;t|jQ_QIm4f?1 z{r|6e)y|BRgwDOr9M?5NM)(8oKbY-Lq9M*1>ZYJLx1o|6i8Ggu3{^KC<(EQE-dua= z#6(`LK}7n7wPxKny@|44tM`qI_j~F!nD^Cgz3{Q>o#ijqH)%3f-|%VOzv2c{sJTSa z11Ued8878RP9f_~oe+hiR~-0D&*g7Hqc0|O(v{h=I;uqCw#UZtxITY@LSDk|x)~_K zdHBp7U5@w-DYHBulRTz;Zt@2IDz=b$Q)V)w%gwx$THbl+5Z`#c zbeil}vo#;>PS#(UaGnw$_W9MPUA#}XJEg|L#NgsdR=f4zN;=MwD@iQHtMQk+T&Knd z-f3H*DY{Yz<%23Qi9h%2P~>%KzPCyqRh-`yg8Hz}l%Q!l@&5Hnojrv@pA~pJXLp8A zuQNaTh!P?FE%8~x-u@q+1i{HCuTQ2ZUtIszvZG{(v~vS*o#yED@#9l_WY)#ptwE7o zduvBrq%CLmKlZeaJj3}mwf_csQ2o$d)-NY{ZAfQ7#%jSW@js>a^%kD1{5t}R{r3U@ z2yOr$$Aak^P>=xn2X5sAus~wKv5C-A2L=GcI2Z>EGAsmSB9e~7;DDzDy&oJAeA{%1+2vo2-8Ck5Z_>0R zb;=tJ9r>oRu4em|Gy9n~u6p~Q7yb64fT#!3u`o-9O%_b+kW?~kHjqFEXRML{ipNfX z_C(XbJQpx^Fnk58dH5F4=Ai$`@Rnc9mCrfqI%-8j#eLU_SMjR2^JGl;pxZ1S)>g;m zJxnOt)3-f!^g-sjaMdDP@BO+*asq8qTTRN6H%~2$rmZ}qu$}2P_|2)rJHNdsJlITw z;}6URfZtAre|`egXk-8YP@tCx+j}T%urLM30ab&H$1)gzFp{J|-;Rm^@qgq}H_rS` za^a2JzS%COGP0A$c$JS5UU9mdyyQ`}0<}S8>O3a6YG#!QOQu;Bspr#JhfpW3_8sWrW*Fs=3&>&pD& zC%*;y4nA|0@>OwD{&+t-U)K>!3;x9#C9r>1q|S6xD;F2))?cKt*ybH7dv5cyG;FrG zF?srg>}8&-%E~AqDUpeOnI`F)jP0s|it4$IQKz)9T8ca0ME7D@GpqTBZE*9~`0&;Ppj*=w)nz=eLrN^9d2 z2TbISuU>suFMC#R^85&IhV+%artMEgPU|abNevWH}IxKLO$)s$ki*iU0nM59o=*EOHIb7y0^)Z)5XsoA=}q- z>*Z?|o7y?n?HzEfeetP8P5#J)lYxSDl(20s#Ve|)?XK4uwWCkS&WBjEPOt^+Kk<1s z{CNBHhR@ocRBzBZluxbIjV>)@?Aw?Y0J=r+s^tZM(Ep+U=suHV<6A-R!7YR;o{5Nd ztcLVi ziH0W@AB#Iir5vun--aEve9l|8V`TD)1>hr4paIK5U^J^hMh-rgz~F&E1L_+DP_zP= z2aG+4jE^d`e(-<}L4wl+jzGqbUY}CFvMTzkQG2f4~=FJ1c0@R0;(Ji3w%|O{s$ln5dig0O9&lH zIXiLDoRCan_YoD3ZT-jFkha#J(~Qi51NNFZ+xHmS(q)|PlesQmy~HDb`jYM=_S4h- z>fAd*OV`I)~I7Gn%wDMZ=8 zFM64FiyB#^QBF%$I^+J(nIT|T$6-CGs(W3T8_VqOT&*yydQ#$B~*`sd%hKNmgmP&%iyh7G^M zYR?K~Hgd$RG*RnvV{mfFP05~M)z8>2-4wznocHwm%=&nEZTM=0;-7PA$1dLNl&T3ibMzt2)Nx_%>_*kD_nCpLjb;LmPQ-rn9w`}KN zjUcu5>c%9F>Wx{Reh|r-W`IlQ{~G{U5Sm@|b4Soev-F z{8hU$I>jeh9TKM|iZriZdA|L^xR}K+;)@TXw;itt_oUa|>|oDrmSh|S{%1V?dZ)_~ z1<`*!{jWb3vml~~zZjljK!M@G8ZF#f=&^pFclv90m}3A(=oRZGsnGa@jRn>LLgV3Esi!LfR|2P^fspPA=}QuaHRoK@*L^|WcPhzsR9~ZZQDXiAslBVI9hvvqZ(Y`k&~0^W zy;5yN%Zu#smS|1!7gKP z5t!WDtF#i08P{sry)FC)?rlbw{U7)CpX}JfvH!!pef^a|YK`wcHC@?F<>&l5PO~YK z^1Bohc15$de)OmkS#d>oC7aIF*s39;I|oke3|TIL4srN5W&*))Mt-}(dwToeGQ}Ux z|K1WnFq{z(z)%K)BO1~z2DV$EWDP1SFu|iy=s1wa14kqv)nS1Cf&u6?jtrD%6mVUj zQo=292U(mr+#avqG51c)`qr${A*D%9tzb^koEg26N2lKPgr3#m`?>bp0XD_KLz@wr z-%!#i3`RM~wWbe)VqSJq;SSE|Y(trSn2f-hJEd#>^l%BDmyaJ-g zfU|>I71-B6lLUqf_#G1g>Wd%%7Y{gipp!>IgWctl)&WLSXU8GRCf)LST%}82Ps@Z7 zUOhp4&zHI#+4ZLzFN%sK{CpqM+>)_z9{iN$%N^m#-F}i zHtuJ#C@}v4m4>kCQ*MHe~y zD^;;ITb%X2PdSBHZ!$f5(vB>9lBw3gq4~09GDb`mg(1U60xY$GXar+?uo*$10eys^ z0(ljrtWkh$Afp*Lc^n<)|1hN@|8*C_`eEn{rTS7aR9}eosm(GlvJg3U+|S?qqP#4h zOTaIz^vR0+(&ecA#@Cyv!nbotN4a!G?ULrW({bUQ z(i4N))RkV9$$=gCak)obzXbW#ZoOssX6@Nf)mQvI6oZs9l5X1Fv!`!Z9=h_l`xXDj z@Ou%b38{qA=^bx)K46J_@%AV^kbS$q4ww7-?jJHoc#Wb zU-q)Mh-7$#=`O?V$8W^$jc4uOkgQRA<<2MM^l=t>f|+TkzA!Oj?NmpmMf_L$XU9?( zPRih(3!Ruwzo@obj^$3q%7sD`$6NcZm(9v4-=%JEl=1iCYFw+M>RXAbP)8{in{?RL zN1wAr?0q~p?7|v{%(CbSL?2dUB3`a42w^Te5g}pvLdhd zLUU~J9O~1%PmZH^j5-XX&n2wi_=Un{9mnf4mh`~u)A3Dr_Fl!E6eZSOO?*hY#+!Ek z!pnh+J0Q9<&4Otz@T%omP~;C}!GEjpjBHdV!~^t;v!~vOe8csS`ogBXI6}M2z18+j zV>2cKKdB#(!K-a}bMngkWWRiN2qP~s-WNO;yAVvERP)B4Tn%2{;mZ^?-Td@NZ?O4O zIW9NP^v((X$77qb4Mmb0))%b}*YMZ4^yTK>{poXX2$p7_A&8f(2^cX*V_ z5uUfTXKZr1p#ROx*L|Zj^=GO`@dv%8=6t*6-=3e#o4bj*C)c~-WX@jSjXVV_9?52B zo?X%LnCne~=gDgteCU~^y89NMRPLTh@0>8P&6gY3R9&`wpxsnAWqfXhtxE8&>-4%E z_PWoXsZ>j^Y3vL>wKaRIlf6o2!8^$c7M_PvSt@JKOeS(y6!3>0=O=aK{n&@>X}JBR zD5$}|jx>?$ZsVM_mZyI7rK!GVzyck|E%Au&vsT5>_c5VKxx6mfM>yN#RlyeBL!`u{ z^?j?}-Q&;kQ9Wj*d&a5%it>|+<8t<&x9#QU9!jW3=I98jdI#Le=XXA$LugSw_zT(b z%IwNBUp@6ogbvqSMfA#h7O4@+!j1vj0brTJ{NBDa4zF5%Z?E|S_x8V`m8VKGXep-( z^=9Qa-tI22eVxfJCv1Clc3;`*<5suR-`{w4m2l|61I0Mcl?qiqEDRZU-Qu0Mh*sVe z&3L2#di;k9Z~x4PhcBLbluR9T4EN_*C1m56b-J+Q;r@LN9ry0um=-&yx#`+WjS7b5 zt!4bc>{m4$Xz>h-w2Y-QME}3jFupAAUT`DZ27kN48z22q@OLr91SUua@jQ?_VyGND zfgc~84pSa*E&;_r1b8Y!rb1!CF_Gd#z<{Cxh6u`ZjFAuu+)bc(!aFTBQL?%32945v zWXtRWG_Si{Zkx?1y7F=S#}Uiok(#f+v`+t+O5clgI#Zp*A78X^w?rJ#bJyvq72!(p zE1W}LTQO5a-P7Fh-(M6U2cQte5Wp!H%*)VNnEg;0vN}-r0D%GzXgh#ALQf2aJ`_+w zqyY303z!frwEhq{5FT1AhWL-j÷-R(Z0YqaKKyI$%<#aNk!P%)?6>0547_>zQe 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≪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�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%0AAthZTv2oK%D^U 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%0AHoL2&Y|T4O-2eCM_HQlh=L^E4lS*Psk|Wj&q1}` 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